diff --git a/README.md b/README.md index 90d33b8..1791d89 100644 --- a/README.md +++ b/README.md @@ -201,10 +201,13 @@ frame logs include direction, peer id when present, MACs, ethertype/length, frame length, action, and drop reason. The gateway also tracks frame/datagram counters and periodically sends stats snapshots to the relay. Malformed or runt LAN frames are counted and logged as dropped instead of disappearing before -accounting. Relay lifecycle events seed and retire remote-client MACs for CAM -refresh even before that client sends traffic. On shutdown, the gateway sends a -best-effort disconnect control message before closing QUIC so the relay can -report the intended reason. +accounting. It drops unrelated LAN unicast locally once the destination is known +not to be a connected remote client, so busy LAN traffic is not sent to the +public relay just to be discarded there. Relay lifecycle events seed and retire +remote-client MACs for CAM refresh and LAN-destination filtering even before +that client sends traffic. On shutdown, the gateway sends a best-effort +disconnect control message before closing QUIC so the relay can report the +intended reason. ## Windows Client diff --git a/TESTING.md b/TESTING.md index 39adfd1..70e8ad7 100644 --- a/TESTING.md +++ b/TESTING.md @@ -208,6 +208,10 @@ drop_reason=DatagramBudget drop_reason=RateLimit ``` +On gateway `LanToRemote` logs, `UnknownDestination` usually means the gateway +captured unrelated LAN unicast and dropped it locally instead of sending it to +the relay. + Drops that should be investigated if they dominate: ```text diff --git a/crates/lanparty-gateway/src/lib.rs b/crates/lanparty-gateway/src/lib.rs index 8201899..1cb8a59 100644 --- a/crates/lanparty-gateway/src/lib.rs +++ b/crates/lanparty-gateway/src/lib.rs @@ -348,6 +348,21 @@ impl GatewayConnection { ); continue; } + if let Some(drop_reason) = remote_clients.lan_frame_drop_reason(&lan_frame)? { + stats.record_dropped_frame(); + println!( + "{}", + gateway_frame_log_line( + packet_socket.get_ref().interface(), + FrameDirection::LanToRemote, + Some(welcome.peer_id()), + &lan_frame, + FrameAction::Dropped, + Some(drop_reason), + ) + ); + continue; + } let outcome = send_gateway_ethernet( &connection, @@ -877,6 +892,17 @@ impl RemoteClientTable { Ok(None) } + fn lan_frame_drop_reason(&self, frame: &[u8]) -> Result> { + let frame = EthernetFrame::parse(frame).context("LAN Ethernet frame is malformed")?; + let destination = frame.destination(); + let targets_remote_client = self.remote_clients.values().any(|mac| *mac == destination); + if destination.is_multicast() || targets_remote_client { + return Ok(None); + } + + Ok(Some(DropReason::UnknownDestination)) + } + fn observe_control_event(&mut self, event: &ControlMessage) -> Option { match event { ControlMessage::PeerJoined(peer) => self.observe_peer_joined(peer), @@ -1327,6 +1353,45 @@ mod tests { ); } + #[cfg(target_os = "linux")] + #[test] + fn filters_unrelated_lan_unicast_before_relay_send() { + let gateway_mac = MacAddr::new([0x0a, 0, 0, 0, 0, 1]); + let remote_mac = MacAddr::new([0x02, 0, 0, 0, 0, 2]); + let lan_mac = MacAddr::new([0x0a, 0, 0, 0, 0, 3]); + let mut remote_clients = RemoteClientTable::new(gateway_mac); + + assert_eq!( + remote_clients + .lan_frame_drop_reason(&broadcast_ethernet_frame(b"lan broadcast")) + .unwrap(), + None + ); + assert_eq!( + remote_clients + .lan_frame_drop_reason(ðernet_frame_to(remote_mac, b"before joined")) + .unwrap(), + Some(DropReason::UnknownDestination) + ); + + remote_clients.observe_control_event(&ControlMessage::PeerJoined( + PeerInfo::new(7, Role::Client, Some(remote_mac)).unwrap(), + )); + + assert_eq!( + remote_clients + .lan_frame_drop_reason(ðernet_frame_to(remote_mac, b"remote unicast")) + .unwrap(), + None + ); + assert_eq!( + remote_clients + .lan_frame_drop_reason(ðernet_frame_to(lan_mac, b"lan host unicast")) + .unwrap(), + Some(DropReason::UnknownDestination) + ); + } + #[cfg(target_os = "linux")] #[test] fn updates_cam_refresh_from_lifecycle_events() { @@ -1462,6 +1527,15 @@ mod tests { ethernet_frame_with_addresses(MacAddr::new([0x02, 0, 0, 0, 0, 2]), source, 0x0800, payload) } + fn ethernet_frame_to(destination: MacAddr, payload: &[u8]) -> Vec { + ethernet_frame_with_addresses( + destination, + MacAddr::new([0x0a, 0, 0, 0, 0, 1]), + 0x0800, + payload, + ) + } + fn control_plane_ethernet_frame() -> Vec { ethernet_frame_with_addresses( MacAddr::new([0x01, 0x80, 0xc2, 0, 0, 0]),