From 234bece265c34806703099e32d5e55569a3cb579 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Fri, 22 May 2026 05:31:37 +0200 Subject: [PATCH] fix(client): drop misdirected relayed unicast frames The relay should only send a remote client unicast traffic for that client's virtual MAC, while broadcast and multicast traffic fan out to every relevant peer. The Windows client is still the last boundary before TAP, so it should not write a relayed unicast frame for some other MAC into the adapter if the relay or a future code path misroutes it. Add a receive-side destination guard in client-core. Relayed frames now reach TAP only when their destination is the client's virtual MAC or a broadcast/multicast address; other unicast frames are counted and skipped. Extend the client relay-session test with a wrong-client unicast before the valid frame, and document the client-side skip 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 frames to matching remote clients --- README.md | 5 +- crates/lanparty-client-core/src/lib.rs | 63 ++++++++++++++++++++++---- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index eaa66e4..e7ac0be 100644 --- a/README.md +++ b/README.md @@ -264,8 +264,9 @@ 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. 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. +out of the Windows adapter even if they reached the client over QUIC. +Misdirected unicast frames not addressed to the client's virtual MAC are also +counted and skipped; 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 13fcf90..2fabec5 100644 --- a/crates/lanparty-client-core/src/lib.rs +++ b/crates/lanparty-client-core/src/lib.rs @@ -449,6 +449,10 @@ impl ClientRelayIo { self.stats.record_dropped_frame(); continue; } + if !is_accepted_relay_destination(ethernet_frame, self.virtual_mac) { + self.stats.record_dropped_frame(); + continue; + } return Ok(ReceivedEthernetFrame { source_peer_id: header.peer_id(), @@ -468,6 +472,11 @@ impl ClientRelayIo { } } +fn is_accepted_relay_destination(frame: EthernetFrame<'_>, virtual_mac: MacAddr) -> bool { + let destination = frame.destination(); + destination == virtual_mac || destination.is_multicast() +} + #[derive(Debug, Default)] struct ClientTunnelStats { ethernet_frames_tx: AtomicU64, @@ -823,9 +832,26 @@ mod tests { .send_datagram(Bytes::from(filtered_response)) .unwrap(); - let response = - encode_datagram(FrameType::Ethernet, 7, 1, 0, ðernet_frame(b"from relay")) - .unwrap(); + let misdirected_response = encode_datagram( + FrameType::Ethernet, + 7, + 1, + 0, + &misdirected_unicast_ethernet_frame(), + ) + .unwrap(); + connection + .send_datagram(Bytes::from(misdirected_response)) + .unwrap(); + + let response = encode_datagram( + FrameType::Ethernet, + 7, + 1, + 0, + &relay_ethernet_frame(b"from relay"), + ) + .unwrap(); connection.send_datagram(Bytes::from(response)).unwrap(); let event = encode_control_message(&ControlMessage::PeerJoined( @@ -842,7 +868,7 @@ mod tests { let ControlMessage::Stats(stats) = stats_message else { panic!("expected client stats event"); }; - assert_eq!(stats, TunnelStats::new(1, 2, 1, 2, 8, 1)); + assert_eq!(stats, TunnelStats::new(1, 3, 1, 3, 9, 1)); stats_received_tx.send(()).unwrap(); let mut disconnect_recv = connection.accept_uni().await.unwrap(); @@ -906,7 +932,10 @@ mod tests { .unwrap() .unwrap(); assert_eq!(received.source_peer_id(), 1); - assert_eq!(received.payload(), ethernet_frame(b"from relay").as_slice()); + assert_eq!( + received.payload(), + relay_ethernet_frame(b"from relay").as_slice() + ); let event = tokio::time::timeout(Duration::from_secs(5), client.recv_control_event()) .await @@ -968,12 +997,12 @@ mod tests { ); let stats = relay_io.stats_snapshot(); assert_eq!(stats.ethernet_frames_tx(), 1); - assert_eq!(stats.ethernet_frames_rx(), 2); + assert_eq!(stats.ethernet_frames_rx(), 3); 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(), 2); - assert_eq!(stats.dropped_frames(), 8); + assert_eq!(stats.datagrams_rx(), 3); + assert_eq!(stats.dropped_frames(), 9); assert_eq!(stats.malformed_frames(), 1); assert_eq!(client.stats_snapshot(), stats); @@ -1075,6 +1104,24 @@ mod tests { ) } + fn relay_ethernet_frame(payload: &[u8]) -> Vec { + ethernet_frame_with_headers( + MacAddr::new([0x02, 0, 0, 0, 0, 1]), + MacAddr::new([0x02, 0, 0, 0, 0, 2]), + lanparty_proto::ETHERTYPE_IPV4, + payload, + ) + } + + fn misdirected_unicast_ethernet_frame() -> Vec { + ethernet_frame_with_headers( + MacAddr::new([0x02, 0, 0, 0, 0, 9]), + MacAddr::new([0x02, 0, 0, 0, 0, 2]), + lanparty_proto::ETHERTYPE_IPV4, + b"wrong client", + ) + } + fn ethernet_frame_with_headers( destination: MacAddr, source: MacAddr,