diff --git a/README.md b/README.md index 829613d..acd9dc8 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,11 @@ overlay payload-length ceiling before deciding whether they fit the tunnel. It never fragments Ethernet frames; LAN frames with invalid source MACs, L2 control-plane traffic, jumbo frames, or encoded datagrams exceeding the negotiated QUIC budget are counted, dropped, and logged locally instead of -stopping the bridge or consuming relay bandwidth. +stopping the bridge or consuming relay bandwidth. Remote frames received from +the relay are safety-checked again before LAN injection, so invalid-source, +L2 control-plane, remote VLAN, DHCP-server, IPv6 Router Advertisement, IPv6 +fragment, and jumbo frames cannot cross the gateway's final physical-LAN +boundary even if they reached the gateway over QUIC. `--relay` accepts a DNS name or socket address; bare hosts default to UDP/443. The gateway rejects Linux interfaces that sysfs identifies as Wi-Fi, and rejects wired interfaces whose sysfs carrier state reports no link; managed wireless diff --git a/crates/lanparty-gateway/src/lib.rs b/crates/lanparty-gateway/src/lib.rs index 47e882e..5a980be 100644 --- a/crates/lanparty-gateway/src/lib.rs +++ b/crates/lanparty-gateway/src/lib.rs @@ -33,7 +33,7 @@ use lanparty_obs::{DropReason, TunnelStats}; use lanparty_obs::{FrameAction, FrameDirection, FrameLog}; use lanparty_proto::{ EthernetFrame, FrameType, MacAddr, decode_datagram, encode_datagram, - gateway_lan_safety_drop_reason, validate_datagram_budget, + gateway_lan_safety_drop_reason, remote_client_safety_drop_reason, validate_datagram_budget, }; use quinn::{ClientConfig, Endpoint, crypto::rustls::QuicClientConfig}; use rustls::pki_types::CertificateDer; @@ -213,6 +213,19 @@ pub struct ReceivedEthernetFrame { payload: Bytes, } +#[derive(Debug, Clone, PartialEq, Eq)] +struct FilteredRelayEthernetFrame { + source_peer_id: u32, + payload: Bytes, + drop_reason: DropReason, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum GatewayReceiveOutcome { + Accepted(ReceivedEthernetFrame), + Filtered(FilteredRelayEthernetFrame), +} + impl ReceivedEthernetFrame { #[must_use] pub const fn source_peer_id(&self) -> u32 { @@ -359,22 +372,38 @@ impl GatewayConnection { ) ); } - relay_frame = recv_gateway_ethernet(&connection, &welcome, &stats) => { - let relay_frame = relay_frame?; - cam_refresh - .observe_remote_frame(relay_frame.source_peer_id(), relay_frame.payload())?; - write_lan_ethernet(&packet_socket, relay_frame.payload()).await?; - println!( - "{}", - gateway_frame_log_line( - packet_socket.get_ref().interface(), - FrameDirection::RemoteToLan, - Some(relay_frame.source_peer_id()), - relay_frame.payload(), - FrameAction::Forwarded, - None, - ) - ); + relay_frame = recv_gateway_ethernet_outcome(&connection, &welcome, &stats) => { + match relay_frame? { + GatewayReceiveOutcome::Accepted(relay_frame) => { + cam_refresh + .observe_remote_frame(relay_frame.source_peer_id(), relay_frame.payload())?; + write_lan_ethernet(&packet_socket, relay_frame.payload()).await?; + println!( + "{}", + gateway_frame_log_line( + packet_socket.get_ref().interface(), + FrameDirection::RemoteToLan, + Some(relay_frame.source_peer_id()), + relay_frame.payload(), + FrameAction::Forwarded, + None, + ) + ); + } + GatewayReceiveOutcome::Filtered(relay_frame) => { + println!( + "{}", + gateway_frame_log_line( + packet_socket.get_ref().interface(), + FrameDirection::RemoteToLan, + Some(relay_frame.source_peer_id), + &relay_frame.payload, + FrameAction::Filtered, + Some(relay_frame.drop_reason), + ) + ); + } + } } _ = cam_refresh_tick.tick() => { for frame in cam_refresh.refresh_frames() { @@ -465,6 +494,19 @@ async fn recv_gateway_ethernet( welcome: &ServerWelcome, stats: &GatewayTunnelStats, ) -> Result { + loop { + match recv_gateway_ethernet_outcome(connection, welcome, stats).await? { + GatewayReceiveOutcome::Accepted(frame) => return Ok(frame), + GatewayReceiveOutcome::Filtered(_) => {} + } + } +} + +async fn recv_gateway_ethernet_outcome( + connection: &quinn::Connection, + welcome: &ServerWelcome, + stats: &GatewayTunnelStats, +) -> Result { loop { let datagram = connection.read_datagram().await?; stats.record_datagram_rx(); @@ -489,13 +531,32 @@ async fn recv_gateway_ethernet( }; stats.record_ethernet_rx(ethernet_frame); - return Ok(ReceivedEthernetFrame { + if let Some(drop_reason) = remote_to_lan_safety_drop_reason(ethernet_frame) { + stats.record_dropped_frame(); + return Ok(GatewayReceiveOutcome::Filtered( + FilteredRelayEthernetFrame { + source_peer_id: header.peer_id(), + payload: Bytes::copy_from_slice(packet.payload()), + drop_reason, + }, + )); + } + + return Ok(GatewayReceiveOutcome::Accepted(ReceivedEthernetFrame { source_peer_id: header.peer_id(), payload: Bytes::copy_from_slice(packet.payload()), - }); + })); } } +fn remote_to_lan_safety_drop_reason(frame: EthernetFrame<'_>) -> Option { + if !frame.source().is_valid_unicast() { + return Some(DropReason::InvalidSourceMac); + } + + remote_client_safety_drop_reason(frame).map(DropReason::from) +} + async fn send_gateway_stats(connection: &quinn::Connection, stats: TunnelStats) -> Result<()> { send_gateway_control_event(connection, ControlMessage::Stats(stats), "gateway stats").await } @@ -987,6 +1048,18 @@ mod tests { assert_eq!(header.peer_id(), 1); assert_eq!(packet.payload(), ethernet_frame(b"to relay").as_slice()); + let filtered_response = encode_datagram( + FrameType::Ethernet, + 7, + 99, + 0, + &control_plane_ethernet_frame(), + ) + .unwrap(); + connection + .send_datagram(Bytes::from(filtered_response)) + .unwrap(); + let response = encode_datagram( FrameType::Ethernet, 7, @@ -1003,7 +1076,7 @@ mod tests { let ControlMessage::Stats(stats) = stats_message else { panic!("expected gateway stats event"); }; - assert_eq!(stats, TunnelStats::new(1, 1, 1, 1, 4, 1)); + assert_eq!(stats, TunnelStats::new(1, 2, 1, 2, 5, 1)); stats_received_tx.send(()).unwrap(); let mut disconnect_recv = connection.accept_uni().await.unwrap(); @@ -1076,7 +1149,7 @@ mod tests { .is_err() ); let stats = gateway.stats_snapshot(); - assert_eq!(stats, TunnelStats::new(1, 1, 1, 1, 4, 1)); + assert_eq!(stats, TunnelStats::new(1, 2, 1, 2, 5, 1)); gateway.send_stats_snapshot().await.unwrap(); tokio::time::timeout(Duration::from_secs(5), stats_received_rx)