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:
@@ -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
|
negotiated QUIC budget are counted and dropped before relay send without
|
||||||
stopping the bridge. Relayed LAN frames are also safety-checked before TAP
|
stopping the bridge. Relayed LAN frames are also safety-checked before TAP
|
||||||
writes, so switch-control traffic, invalid-source frames, and jumbo frames stay
|
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
|
out of the Windows adapter even if they reached the client over QUIC.
|
||||||
device read/write errors still stop the bridge.
|
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
|
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
|
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
|
so later leave logs can identify a disconnected LAN gateway or client MAC when
|
||||||
|
|||||||
@@ -449,6 +449,10 @@ impl ClientRelayIo {
|
|||||||
self.stats.record_dropped_frame();
|
self.stats.record_dropped_frame();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if !is_accepted_relay_destination(ethernet_frame, self.virtual_mac) {
|
||||||
|
self.stats.record_dropped_frame();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(ReceivedEthernetFrame {
|
return Ok(ReceivedEthernetFrame {
|
||||||
source_peer_id: header.peer_id(),
|
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)]
|
#[derive(Debug, Default)]
|
||||||
struct ClientTunnelStats {
|
struct ClientTunnelStats {
|
||||||
ethernet_frames_tx: AtomicU64,
|
ethernet_frames_tx: AtomicU64,
|
||||||
@@ -823,8 +832,25 @@ mod tests {
|
|||||||
.send_datagram(Bytes::from(filtered_response))
|
.send_datagram(Bytes::from(filtered_response))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let response =
|
let misdirected_response = encode_datagram(
|
||||||
encode_datagram(FrameType::Ethernet, 7, 1, 0, ðernet_frame(b"from relay"))
|
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();
|
.unwrap();
|
||||||
connection.send_datagram(Bytes::from(response)).unwrap();
|
connection.send_datagram(Bytes::from(response)).unwrap();
|
||||||
|
|
||||||
@@ -842,7 +868,7 @@ mod tests {
|
|||||||
let ControlMessage::Stats(stats) = stats_message else {
|
let ControlMessage::Stats(stats) = stats_message else {
|
||||||
panic!("expected client stats event");
|
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();
|
stats_received_tx.send(()).unwrap();
|
||||||
|
|
||||||
let mut disconnect_recv = connection.accept_uni().await.unwrap();
|
let mut disconnect_recv = connection.accept_uni().await.unwrap();
|
||||||
@@ -906,7 +932,10 @@ mod tests {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(received.source_peer_id(), 1);
|
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())
|
let event = tokio::time::timeout(Duration::from_secs(5), client.recv_control_event())
|
||||||
.await
|
.await
|
||||||
@@ -968,12 +997,12 @@ mod tests {
|
|||||||
);
|
);
|
||||||
let stats = relay_io.stats_snapshot();
|
let stats = relay_io.stats_snapshot();
|
||||||
assert_eq!(stats.ethernet_frames_tx(), 1);
|
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_tx(), 0);
|
||||||
assert_eq!(stats.broadcast_frames_rx(), 0);
|
assert_eq!(stats.broadcast_frames_rx(), 0);
|
||||||
assert_eq!(stats.datagrams_tx(), 1);
|
assert_eq!(stats.datagrams_tx(), 1);
|
||||||
assert_eq!(stats.datagrams_rx(), 2);
|
assert_eq!(stats.datagrams_rx(), 3);
|
||||||
assert_eq!(stats.dropped_frames(), 8);
|
assert_eq!(stats.dropped_frames(), 9);
|
||||||
assert_eq!(stats.malformed_frames(), 1);
|
assert_eq!(stats.malformed_frames(), 1);
|
||||||
assert_eq!(client.stats_snapshot(), stats);
|
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(
|
fn ethernet_frame_with_headers(
|
||||||
destination: MacAddr,
|
destination: MacAddr,
|
||||||
source: MacAddr,
|
source: MacAddr,
|
||||||
|
|||||||
Reference in New Issue
Block a user