fix(gateway): drop unsafe LAN frames before relay send
The relay already filters invalid source MACs, jumbo frames, and L2 control plane traffic. The gateway bridge was still encoding those LAN frames and sending them to the relay first, so gateway logs could say forwarded even when the relay would later drop the frame. Classify that same local LAN-send subset before QUIC DATAGRAM encoding. The gateway now records and reports these frames as local drops, keeps the relay as the trust boundary, and avoids spending relay bandwidth on frames that can never reach remote clients. Document that gateway-side local drops cover invalid source MACs, L2 control plane traffic, jumbo frames, and datagram-budget failures. Test Plan: - cargo test -p lanparty-gateway - cargo fmt --check - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - git diff --check Refs: PLAN.md LAN-to-remote control-plane filtering
This commit is contained in:
@@ -44,6 +44,9 @@ pub use packet::PacketSocket;
|
||||
|
||||
const MAX_CONTROL_FRAME_LEN: usize = CONTROL_LENGTH_PREFIX_LEN + MAX_CONTROL_MESSAGE_LEN;
|
||||
const DISCONNECT_DRAIN_TIMEOUT: Duration = Duration::from_millis(250);
|
||||
const ETHERTYPE_EAPOL: u16 = 0x888e;
|
||||
const ETHERTYPE_SLOW_PROTOCOLS: u16 = 0x8809;
|
||||
const ETHERTYPE_LLDP: u16 = 0x88cc;
|
||||
#[cfg(target_os = "linux")]
|
||||
const CAM_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -427,6 +430,11 @@ fn send_gateway_ethernet(
|
||||
return Err(error).context("gateway Ethernet frame is malformed");
|
||||
}
|
||||
};
|
||||
if let Some(drop_reason) = gateway_lan_drop_reason(ethernet_frame) {
|
||||
stats.record_dropped_frame();
|
||||
return Ok(GatewaySendOutcome::Dropped(drop_reason));
|
||||
}
|
||||
|
||||
let datagram = encode_datagram(
|
||||
FrameType::Ethernet,
|
||||
welcome.room_id(),
|
||||
@@ -448,6 +456,35 @@ fn send_gateway_ethernet(
|
||||
Ok(GatewaySendOutcome::Sent)
|
||||
}
|
||||
|
||||
fn gateway_lan_drop_reason(frame: EthernetFrame<'_>) -> Option<DropReason> {
|
||||
if !frame.source().is_valid_unicast() {
|
||||
return Some(DropReason::InvalidSourceMac);
|
||||
}
|
||||
|
||||
if frame.is_jumbo() {
|
||||
return Some(DropReason::JumboFrame);
|
||||
}
|
||||
|
||||
if is_lan_control_plane_frame(frame) {
|
||||
return Some(DropReason::ControlPlaneEtherType);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn is_lan_control_plane_frame(frame: EthernetFrame<'_>) -> bool {
|
||||
matches!(
|
||||
frame.ethertype_or_len(),
|
||||
ETHERTYPE_EAPOL | ETHERTYPE_SLOW_PROTOCOLS | ETHERTYPE_LLDP
|
||||
) || is_link_local_control_destination(frame.destination())
|
||||
}
|
||||
|
||||
fn is_link_local_control_destination(mac: MacAddr) -> bool {
|
||||
let [a, b, c, d, e, f] = mac.octets();
|
||||
|
||||
[a, b, c, d, e] == [0x01, 0x80, 0xc2, 0x00, 0x00] && f <= 0x0f
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum GatewaySendOutcome {
|
||||
Sent,
|
||||
@@ -997,7 +1034,7 @@ mod tests {
|
||||
let ControlMessage::Stats(stats) = stats_message else {
|
||||
panic!("expected gateway stats event");
|
||||
};
|
||||
assert_eq!(stats, TunnelStats::new(1, 1, 1, 1, 2, 1));
|
||||
assert_eq!(stats, TunnelStats::new(1, 1, 1, 1, 4, 1));
|
||||
stats_received_tx.send(()).unwrap();
|
||||
|
||||
let mut disconnect_recv = connection.accept_uni().await.unwrap();
|
||||
@@ -1044,6 +1081,16 @@ mod tests {
|
||||
assert_eq!(peer.role(), Role::Client);
|
||||
assert_eq!(peer.mac(), Some(MacAddr::new([0x02, 0, 0, 0, 0, 9])));
|
||||
|
||||
assert!(
|
||||
gateway
|
||||
.send_ethernet(&control_plane_ethernet_frame())
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
gateway
|
||||
.send_ethernet(ðernet_frame_from(MacAddr::BROADCAST, b"invalid source"))
|
||||
.is_err()
|
||||
);
|
||||
gateway.send_ethernet(ðernet_frame(b"to relay")).unwrap();
|
||||
let received = tokio::time::timeout(Duration::from_secs(5), gateway.recv_ethernet())
|
||||
.await
|
||||
@@ -1060,7 +1107,7 @@ mod tests {
|
||||
.is_err()
|
||||
);
|
||||
let stats = gateway.stats_snapshot();
|
||||
assert_eq!(stats, TunnelStats::new(1, 1, 1, 1, 2, 1));
|
||||
assert_eq!(stats, TunnelStats::new(1, 1, 1, 1, 4, 1));
|
||||
|
||||
gateway.send_stats_snapshot().await.unwrap();
|
||||
tokio::time::timeout(Duration::from_secs(5), stats_received_rx)
|
||||
@@ -1099,6 +1146,32 @@ mod tests {
|
||||
assert_eq!(snapshot.malformed_frames(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_gateway_lan_local_drops() {
|
||||
let normal_bytes = ethernet_frame(b"normal");
|
||||
let normal = EthernetFrame::parse(&normal_bytes).unwrap();
|
||||
assert_eq!(gateway_lan_drop_reason(normal), None);
|
||||
|
||||
let invalid_source_bytes = ethernet_frame_from(MacAddr::BROADCAST, b"invalid");
|
||||
let invalid_source = EthernetFrame::parse(&invalid_source_bytes).unwrap();
|
||||
assert_eq!(
|
||||
gateway_lan_drop_reason(invalid_source),
|
||||
Some(DropReason::InvalidSourceMac)
|
||||
);
|
||||
|
||||
let jumbo_payload = vec![0; lanparty_proto::MAX_STANDARD_ETHERNET_PAYLOAD_LEN + 1];
|
||||
let jumbo_bytes = ethernet_frame(&jumbo_payload);
|
||||
let jumbo = EthernetFrame::parse(&jumbo_bytes).unwrap();
|
||||
assert_eq!(gateway_lan_drop_reason(jumbo), Some(DropReason::JumboFrame));
|
||||
|
||||
let control_plane_bytes = control_plane_ethernet_frame();
|
||||
let control_plane = EthernetFrame::parse(&control_plane_bytes).unwrap();
|
||||
assert_eq!(
|
||||
gateway_lan_drop_reason(control_plane),
|
||||
Some(DropReason::ControlPlaneEtherType)
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn builds_padded_cam_refresh_frame() {
|
||||
@@ -1247,10 +1320,28 @@ mod tests {
|
||||
}
|
||||
|
||||
fn ethernet_frame_from(source: MacAddr, payload: &[u8]) -> Vec<u8> {
|
||||
ethernet_frame_with_addresses(MacAddr::new([0x02, 0, 0, 0, 0, 2]), source, 0x0800, payload)
|
||||
}
|
||||
|
||||
fn control_plane_ethernet_frame() -> Vec<u8> {
|
||||
ethernet_frame_with_addresses(
|
||||
MacAddr::new([0x01, 0x80, 0xc2, 0, 0, 0]),
|
||||
MacAddr::new([0x02, 0, 0, 0, 0, 1]),
|
||||
ETHERTYPE_LLDP,
|
||||
b"control",
|
||||
)
|
||||
}
|
||||
|
||||
fn ethernet_frame_with_addresses(
|
||||
destination: MacAddr,
|
||||
source: MacAddr,
|
||||
ethertype: u16,
|
||||
payload: &[u8],
|
||||
) -> Vec<u8> {
|
||||
let mut frame = Vec::new();
|
||||
frame.extend_from_slice(&[0x02, 0, 0, 0, 0, 2]);
|
||||
frame.extend_from_slice(&destination.octets());
|
||||
frame.extend_from_slice(&source.octets());
|
||||
frame.extend_from_slice(&0x0800_u16.to_be_bytes());
|
||||
frame.extend_from_slice(ðertype.to_be_bytes());
|
||||
frame.extend_from_slice(payload);
|
||||
frame
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user