refactor(proto): share Ethernet safety classification
Safety filtering now applies at several tunnel boundaries. The relay remains the trust boundary, while the client and gateway also drop unsafe frames before spending relay bandwidth. Duplicating EtherType and IPv4/IPv6 parsers across crates would make those rules drift as the MVP grows. Move the Ethernet safety classifiers into lanparty-proto, expose typed safety drop reasons, and map them back into the existing DropReason vocabulary. The relay now uses the shared client and gateway classifiers, the gateway keeps its local LAN-send drops through the shared classifier, and the client drops the same remote-to-LAN safety cases before QUIC DATAGRAM encoding. Document the client-side local drops and list the additional suspicious drop reasons in the manual MVP test guide. Test Plan: - cargo test -p lanparty-proto safety - cargo test -p lanparty-client-core connects_to_relay_control_stream_as_client - cargo test -p lanparty-gateway connects_to_relay_control_stream_as_gateway - cargo test -p lanparty-relay - cargo fmt --check - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - cargo check -p lanparty-client-tap --target x86_64-pc-windows-gnu --tests - cargo check -p lanparty-client-route --target x86_64-pc-windows-gnu --tests - cargo check -p lanparty-client-tap --target x86_64-pc-windows-msvc --tests - cargo check -p lanparty-client-route --target x86_64-pc-windows-msvc --tests - git diff --check Refs: PLAN.md safety filters and client source-MAC isolation
This commit is contained in:
@@ -27,7 +27,8 @@ use lanparty_ctrl::{
|
||||
};
|
||||
use lanparty_obs::{DropReason, QuicDiagnostics, TunnelStats};
|
||||
use lanparty_proto::{
|
||||
EthernetFrame, FrameType, MacAddr, decode_datagram, encode_datagram, validate_datagram_budget,
|
||||
EthernetFrame, FrameType, MacAddr, decode_datagram, encode_datagram,
|
||||
remote_client_safety_drop_reason, validate_datagram_budget,
|
||||
};
|
||||
use quinn::{ClientConfig, Endpoint, crypto::rustls::QuicClientConfig};
|
||||
use rustls::pki_types::CertificateDer;
|
||||
@@ -381,10 +382,6 @@ impl ClientRelayIo {
|
||||
return Ok(ClientSendOutcome::Dropped(DropReason::Malformed));
|
||||
}
|
||||
};
|
||||
if ethernet_frame.is_jumbo() {
|
||||
self.stats.record_dropped_frame();
|
||||
return Ok(ClientSendOutcome::Dropped(DropReason::JumboFrame));
|
||||
}
|
||||
if !ethernet_frame.source().is_valid_unicast() {
|
||||
self.stats.record_dropped_frame();
|
||||
return Ok(ClientSendOutcome::Dropped(DropReason::InvalidSourceMac));
|
||||
@@ -395,6 +392,10 @@ impl ClientRelayIo {
|
||||
DropReason::UnauthorizedSourceMac,
|
||||
));
|
||||
}
|
||||
if let Some(drop_reason) = remote_client_safety_drop_reason(ethernet_frame) {
|
||||
self.stats.record_dropped_frame();
|
||||
return Ok(ClientSendOutcome::Dropped(DropReason::from(drop_reason)));
|
||||
}
|
||||
|
||||
let datagram = encode_datagram(
|
||||
FrameType::Ethernet,
|
||||
@@ -824,7 +825,7 @@ mod tests {
|
||||
let ControlMessage::Stats(stats) = stats_message else {
|
||||
panic!("expected client stats event");
|
||||
};
|
||||
assert_eq!(stats, TunnelStats::new(1, 1, 1, 1, 4, 1));
|
||||
assert_eq!(stats, TunnelStats::new(1, 1, 1, 1, 7, 1));
|
||||
stats_received_tx.send(()).unwrap();
|
||||
|
||||
let mut disconnect_recv = connection.accept_uni().await.unwrap();
|
||||
@@ -929,6 +930,25 @@ mod tests {
|
||||
.unwrap(),
|
||||
ClientSendOutcome::Dropped(DropReason::UnauthorizedSourceMac)
|
||||
);
|
||||
let jumbo_payload = vec![0; lanparty_proto::MAX_STANDARD_ETHERNET_PAYLOAD_LEN + 1];
|
||||
assert_eq!(
|
||||
relay_io
|
||||
.send_ethernet_with_outcome(ðernet_frame(&jumbo_payload))
|
||||
.unwrap(),
|
||||
ClientSendOutcome::Dropped(DropReason::JumboFrame)
|
||||
);
|
||||
assert_eq!(
|
||||
relay_io
|
||||
.send_ethernet_with_outcome(&control_plane_ethernet_frame())
|
||||
.unwrap(),
|
||||
ClientSendOutcome::Dropped(DropReason::ControlPlaneEtherType)
|
||||
);
|
||||
assert_eq!(
|
||||
relay_io
|
||||
.send_ethernet_with_outcome(&vlan_tagged_ethernet_frame())
|
||||
.unwrap(),
|
||||
ClientSendOutcome::Dropped(DropReason::VlanTaggedFrame)
|
||||
);
|
||||
let stats = relay_io.stats_snapshot();
|
||||
assert_eq!(stats.ethernet_frames_tx(), 1);
|
||||
assert_eq!(stats.ethernet_frames_rx(), 1);
|
||||
@@ -936,7 +956,7 @@ mod tests {
|
||||
assert_eq!(stats.broadcast_frames_rx(), 0);
|
||||
assert_eq!(stats.datagrams_tx(), 1);
|
||||
assert_eq!(stats.datagrams_rx(), 1);
|
||||
assert_eq!(stats.dropped_frames(), 4);
|
||||
assert_eq!(stats.dropped_frames(), 7);
|
||||
assert_eq!(stats.malformed_frames(), 1);
|
||||
assert_eq!(client.stats_snapshot(), stats);
|
||||
|
||||
@@ -1012,10 +1032,42 @@ mod tests {
|
||||
}
|
||||
|
||||
fn ethernet_frame_from(source: MacAddr, payload: &[u8]) -> Vec<u8> {
|
||||
ethernet_frame_with_headers(
|
||||
MacAddr::new([0x02, 0, 0, 0, 0, 2]),
|
||||
source,
|
||||
lanparty_proto::ETHERTYPE_IPV4,
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
fn control_plane_ethernet_frame() -> Vec<u8> {
|
||||
ethernet_frame_with_headers(
|
||||
MacAddr::new([0x01, 0x80, 0xc2, 0, 0, 0]),
|
||||
MacAddr::new([0x02, 0, 0, 0, 0, 1]),
|
||||
lanparty_proto::ETHERTYPE_LLDP,
|
||||
b"control",
|
||||
)
|
||||
}
|
||||
|
||||
fn vlan_tagged_ethernet_frame() -> Vec<u8> {
|
||||
ethernet_frame_with_headers(
|
||||
MacAddr::BROADCAST,
|
||||
MacAddr::new([0x02, 0, 0, 0, 0, 1]),
|
||||
lanparty_proto::ETHERTYPE_8021Q,
|
||||
&[0; 4],
|
||||
)
|
||||
}
|
||||
|
||||
fn ethernet_frame_with_headers(
|
||||
destination: MacAddr,
|
||||
source: MacAddr,
|
||||
ethertype_or_len: u16,
|
||||
payload: &[u8],
|
||||
) -> Vec<u8> {
|
||||
let mut frame = Vec::new();
|
||||
frame.extend_from_slice(&[0x02, 0, 0, 0, 0, 2]);
|
||||
frame.extend_from_slice(&destination.octets());
|
||||
frame.extend_from_slice(&source.octets());
|
||||
frame.extend_from_slice(&0x0800_u16.to_be_bytes());
|
||||
frame.extend_from_slice(ðertype_or_len.to_be_bytes());
|
||||
frame.extend_from_slice(payload);
|
||||
frame
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user