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
This commit is contained in:
2026-05-22 05:31:37 +02:00
parent da937a50c4
commit 234bece265
2 changed files with 58 additions and 10 deletions
+55 -8
View File
@@ -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, &ethernet_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<u8> {
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<u8> {
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,