From 0a97b77ad95c78121dbebc0831e417d47b7d94a5 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Fri, 22 May 2026 05:25:53 +0200 Subject: [PATCH] fix(client): filter relayed LAN frames before TAP writes LAN-to-remote switch-control filtering is enforced by the gateway and relay, but the Windows client is the final boundary before frames enter the TAP adapter. A malformed or buggy relay path should not be able to make the client write LAN control traffic, invalid-source frames, or jumbo frames into TAP. Reuse the shared gateway/LAN safety classifier on received relay Ethernet frames. Filtered frames are counted and skipped, and recv_ethernet only returns frames that are safe to hand to the platform TAP writer. Extend the client relay-session test so the mock relay sends a filtered frame before the valid one, then document the receive-side TAP boundary in the README. Test Plan: - cargo test -p lanparty-client-core - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - cargo fmt --check - git diff --check Refs: PLAN.md LAN-to-remote control-plane filtering --- README.md | 5 ++++- crates/lanparty-client-core/src/lib.rs | 27 +++++++++++++++++++++----- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index acd9dc8..11eba4e 100644 --- a/README.md +++ b/README.md @@ -260,7 +260,10 @@ read from TAP, invalid or unauthorized source-MAC frames, L2 control-plane traffic, remote VLAN tags, DHCP server replies, IPv6 Router Advertisements, IPv6 fragments, jumbo frames, and TAP frames whose encoded datagrams exceed the negotiated QUIC budget are counted and dropped before relay send without -stopping the bridge; TAP device read/write errors still stop the bridge. +stopping the bridge. Relayed LAN frames are also safety-checked before TAP +writes, so switch-control traffic, invalid-source frames, and jumbo frames stay +out of the Windows adapter even if they reached the client over QUIC; TAP +device read/write errors still stop the bridge. Relay lifecycle events are logged as they arrive, including gateway joins and peer leaves. The client remembers peer identities from join and catch-up events so later leave logs can identify a disconnected LAN gateway or client MAC when diff --git a/crates/lanparty-client-core/src/lib.rs b/crates/lanparty-client-core/src/lib.rs index 1f31292..e1ceac6 100644 --- a/crates/lanparty-client-core/src/lib.rs +++ b/crates/lanparty-client-core/src/lib.rs @@ -28,7 +28,7 @@ use lanparty_ctrl::{ use lanparty_obs::{DropReason, QuicDiagnostics, TunnelStats}; use lanparty_proto::{ EthernetFrame, FrameType, MacAddr, decode_datagram, encode_datagram, - remote_client_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; @@ -445,6 +445,11 @@ impl ClientRelayIo { }; self.stats.record_ethernet_rx(ethernet_frame); + if gateway_lan_safety_drop_reason(ethernet_frame).is_some() { + self.stats.record_dropped_frame(); + continue; + } + return Ok(ReceivedEthernetFrame { source_peer_id: header.peer_id(), payload: Bytes::copy_from_slice(packet.payload()), @@ -806,6 +811,18 @@ mod tests { assert_eq!(header.peer_id(), 2); assert_eq!(packet.payload(), ethernet_frame(b"to relay").as_slice()); + let filtered_response = encode_datagram( + FrameType::Ethernet, + 7, + 1, + 0, + &control_plane_ethernet_frame(), + ) + .unwrap(); + connection + .send_datagram(Bytes::from(filtered_response)) + .unwrap(); + let response = encode_datagram(FrameType::Ethernet, 7, 1, 0, ðernet_frame(b"from relay")) .unwrap(); @@ -825,7 +842,7 @@ mod tests { let ControlMessage::Stats(stats) = stats_message else { panic!("expected client stats event"); }; - assert_eq!(stats, TunnelStats::new(1, 1, 1, 1, 7, 1)); + assert_eq!(stats, TunnelStats::new(1, 2, 1, 2, 8, 1)); stats_received_tx.send(()).unwrap(); let mut disconnect_recv = connection.accept_uni().await.unwrap(); @@ -951,12 +968,12 @@ mod tests { ); let stats = relay_io.stats_snapshot(); assert_eq!(stats.ethernet_frames_tx(), 1); - assert_eq!(stats.ethernet_frames_rx(), 1); + assert_eq!(stats.ethernet_frames_rx(), 2); assert_eq!(stats.broadcast_frames_tx(), 0); assert_eq!(stats.broadcast_frames_rx(), 0); assert_eq!(stats.datagrams_tx(), 1); - assert_eq!(stats.datagrams_rx(), 1); - assert_eq!(stats.dropped_frames(), 7); + assert_eq!(stats.datagrams_rx(), 2); + assert_eq!(stats.dropped_frames(), 8); assert_eq!(stats.malformed_frames(), 1); assert_eq!(client.stats_snapshot(), stats);