fix(relay): filter remote VLAN-tagged frames
The MVP bridge treats each remote player as a normal host on the LAN, not as a trunk port. Allowing client-origin VLAN-tagged frames would let a remote client send traffic outside the simple untagged Ethernet model, and could also hide IPv4/IPv6 control traffic behind an outer VLAN EtherType that the existing safety filters do not parse. Filter 802.1Q, 802.1ad, and common QinQ-tagged frames from remote clients before they can reach the physical LAN. LAN-origin tagged frames are still allowed back toward clients so the gateway remains a transparent receiver for whatever the local wired network emits. Add a dedicated drop reason so relay logs make the policy clear. Test Plan: - cargo fmt --check - cargo test -p lanparty-relay -p lanparty-obs - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - git diff --check Refs: MVP relay L2 safety filters
This commit is contained in:
@@ -102,9 +102,9 @@ Public relay binary and relay-owned room state:
|
||||
- live Ethernet datagram forwarding with no ingress reflection
|
||||
- per-peer egress budget checks against the negotiated datagram size
|
||||
- reliable `PeerJoined`/`PeerLeft` notifications to existing room peers
|
||||
- L2 safety filters for invalid-source, jumbo, switch-control, remote IPv6
|
||||
fragments, IPv4/IPv6 DHCP-server, and IPv6-RA frames, including frames behind
|
||||
ordinary IPv6 extension headers
|
||||
- L2 safety filters for invalid-source, jumbo, switch-control, remote VLAN
|
||||
tags, remote IPv6 fragments, IPv4/IPv6 DHCP-server, and IPv6-RA frames,
|
||||
including frames behind ordinary IPv6 extension headers
|
||||
- client broadcast/multicast, unknown-unicast, and total bandwidth limiting
|
||||
- malformed peer datagram disconnect threshold
|
||||
- peer stats control events retained for relay diagnostics
|
||||
|
||||
@@ -37,6 +37,7 @@ pub enum DropReason {
|
||||
DhcpServerReply,
|
||||
Ipv6RouterAdvertisement,
|
||||
Ipv6Fragment,
|
||||
VlanTaggedFrame,
|
||||
DatagramBudget,
|
||||
UnknownDestination,
|
||||
RateLimit,
|
||||
|
||||
@@ -32,6 +32,9 @@ const ETHERTYPE_EAPOL: u16 = 0x888e;
|
||||
const ETHERTYPE_SLOW_PROTOCOLS: u16 = 0x8809;
|
||||
const ETHERTYPE_LLDP: u16 = 0x88cc;
|
||||
const ETHERTYPE_IPV6: u16 = 0x86dd;
|
||||
const ETHERTYPE_8021Q: u16 = 0x8100;
|
||||
const ETHERTYPE_8021AD: u16 = 0x88a8;
|
||||
const ETHERTYPE_QINQ: u16 = 0x9100;
|
||||
const IP_PROTOCOL_UDP: u8 = 17;
|
||||
const IPV6_NEXT_HEADER_HOP_BY_HOP: u8 = 0;
|
||||
const IPV6_NEXT_HEADER_ROUTING: u8 = 43;
|
||||
@@ -732,6 +735,10 @@ fn safety_drop_reason(ingress_role: Role, frame: EthernetFrame<'_>) -> Option<Dr
|
||||
return Some(DropReason::ControlPlaneEtherType);
|
||||
}
|
||||
|
||||
if ingress_role == Role::Client && is_vlan_tagged_frame(frame) {
|
||||
return Some(DropReason::VlanTaggedFrame);
|
||||
}
|
||||
|
||||
if ingress_role == Role::Client && is_dhcp_server_reply(frame) {
|
||||
return Some(DropReason::DhcpServerReply);
|
||||
}
|
||||
@@ -754,6 +761,13 @@ fn is_control_plane_frame(frame: EthernetFrame<'_>) -> bool {
|
||||
) || is_link_local_control_destination(frame.destination())
|
||||
}
|
||||
|
||||
fn is_vlan_tagged_frame(frame: EthernetFrame<'_>) -> bool {
|
||||
matches!(
|
||||
frame.ethertype_or_len(),
|
||||
ETHERTYPE_8021Q | ETHERTYPE_8021AD | ETHERTYPE_QINQ
|
||||
)
|
||||
}
|
||||
|
||||
fn is_link_local_control_destination(mac: MacAddr) -> bool {
|
||||
let [a, b, c, d, e, f] = mac.octets();
|
||||
|
||||
@@ -1515,6 +1529,33 @@ mod tests {
|
||||
assert_filtered(&gateway_decision, DropReason::ControlPlaneEtherType);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filters_remote_vlan_tagged_frames_but_allows_lan_tags() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
let gateway = registry.join(gateway_hello()).unwrap();
|
||||
let client = registry.join(client_hello(1)).unwrap();
|
||||
let payload = [0, 42, 0x08, 0x00, 1, 2, 3, 4];
|
||||
let client_frame =
|
||||
ethernet_with_payload(MacAddr::BROADCAST, mac(1), ETHERTYPE_8021Q, &payload);
|
||||
let gateway_frame = ethernet_with_payload(
|
||||
MacAddr::BROADCAST,
|
||||
physical_mac(),
|
||||
ETHERTYPE_8021AD,
|
||||
&payload,
|
||||
);
|
||||
|
||||
let client_decision = registry
|
||||
.forward_ethernet(&room(), client.peer().peer_id(), &client_frame)
|
||||
.unwrap();
|
||||
let gateway_decision = registry
|
||||
.forward_ethernet(&room(), gateway.peer().peer_id(), &gateway_frame)
|
||||
.unwrap();
|
||||
|
||||
assert_filtered(&client_decision, DropReason::VlanTaggedFrame);
|
||||
assert_eq!(gateway_decision.action(), FrameAction::Forwarded);
|
||||
assert_eq!(gateway_decision.targets(), &[client.peer().peer_id()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filters_remote_dhcp_server_replies_but_allows_lan_replies() {
|
||||
let mut registry = RoomRegistry::default();
|
||||
|
||||
Reference in New Issue
Block a user