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
This commit is contained in:
@@ -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
|
traffic, remote VLAN tags, DHCP server replies, IPv6 Router Advertisements, IPv6
|
||||||
fragments, jumbo frames, and TAP frames whose encoded datagrams exceed the
|
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; 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
|
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
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ use lanparty_ctrl::{
|
|||||||
use lanparty_obs::{DropReason, QuicDiagnostics, TunnelStats};
|
use lanparty_obs::{DropReason, QuicDiagnostics, TunnelStats};
|
||||||
use lanparty_proto::{
|
use lanparty_proto::{
|
||||||
EthernetFrame, FrameType, MacAddr, decode_datagram, encode_datagram,
|
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 quinn::{ClientConfig, Endpoint, crypto::rustls::QuicClientConfig};
|
||||||
use rustls::pki_types::CertificateDer;
|
use rustls::pki_types::CertificateDer;
|
||||||
@@ -445,6 +445,11 @@ impl ClientRelayIo {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.stats.record_ethernet_rx(ethernet_frame);
|
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 {
|
return Ok(ReceivedEthernetFrame {
|
||||||
source_peer_id: header.peer_id(),
|
source_peer_id: header.peer_id(),
|
||||||
payload: Bytes::copy_from_slice(packet.payload()),
|
payload: Bytes::copy_from_slice(packet.payload()),
|
||||||
@@ -806,6 +811,18 @@ mod tests {
|
|||||||
assert_eq!(header.peer_id(), 2);
|
assert_eq!(header.peer_id(), 2);
|
||||||
assert_eq!(packet.payload(), ethernet_frame(b"to relay").as_slice());
|
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 =
|
let response =
|
||||||
encode_datagram(FrameType::Ethernet, 7, 1, 0, ðernet_frame(b"from relay"))
|
encode_datagram(FrameType::Ethernet, 7, 1, 0, ðernet_frame(b"from relay"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -825,7 +842,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, 1, 1, 1, 7, 1));
|
assert_eq!(stats, TunnelStats::new(1, 2, 1, 2, 8, 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();
|
||||||
@@ -951,12 +968,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(), 1);
|
assert_eq!(stats.ethernet_frames_rx(), 2);
|
||||||
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(), 1);
|
assert_eq!(stats.datagrams_rx(), 2);
|
||||||
assert_eq!(stats.dropped_frames(), 7);
|
assert_eq!(stats.dropped_frames(), 8);
|
||||||
assert_eq!(stats.malformed_frames(), 1);
|
assert_eq!(stats.malformed_frames(), 1);
|
||||||
assert_eq!(client.stats_snapshot(), stats);
|
assert_eq!(client.stats_snapshot(), stats);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user