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
|
- live Ethernet datagram forwarding with no ingress reflection
|
||||||
- per-peer egress budget checks against the negotiated datagram size
|
- per-peer egress budget checks against the negotiated datagram size
|
||||||
- reliable `PeerJoined`/`PeerLeft` notifications to existing room peers
|
- reliable `PeerJoined`/`PeerLeft` notifications to existing room peers
|
||||||
- L2 safety filters for invalid-source, jumbo, switch-control, remote IPv6
|
- L2 safety filters for invalid-source, jumbo, switch-control, remote VLAN
|
||||||
fragments, IPv4/IPv6 DHCP-server, and IPv6-RA frames, including frames behind
|
tags, remote IPv6 fragments, IPv4/IPv6 DHCP-server, and IPv6-RA frames,
|
||||||
ordinary IPv6 extension headers
|
including frames behind ordinary IPv6 extension headers
|
||||||
- client broadcast/multicast, unknown-unicast, and total bandwidth limiting
|
- client broadcast/multicast, unknown-unicast, and total bandwidth limiting
|
||||||
- malformed peer datagram disconnect threshold
|
- malformed peer datagram disconnect threshold
|
||||||
- peer stats control events retained for relay diagnostics
|
- peer stats control events retained for relay diagnostics
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ pub enum DropReason {
|
|||||||
DhcpServerReply,
|
DhcpServerReply,
|
||||||
Ipv6RouterAdvertisement,
|
Ipv6RouterAdvertisement,
|
||||||
Ipv6Fragment,
|
Ipv6Fragment,
|
||||||
|
VlanTaggedFrame,
|
||||||
DatagramBudget,
|
DatagramBudget,
|
||||||
UnknownDestination,
|
UnknownDestination,
|
||||||
RateLimit,
|
RateLimit,
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ const ETHERTYPE_EAPOL: u16 = 0x888e;
|
|||||||
const ETHERTYPE_SLOW_PROTOCOLS: u16 = 0x8809;
|
const ETHERTYPE_SLOW_PROTOCOLS: u16 = 0x8809;
|
||||||
const ETHERTYPE_LLDP: u16 = 0x88cc;
|
const ETHERTYPE_LLDP: u16 = 0x88cc;
|
||||||
const ETHERTYPE_IPV6: u16 = 0x86dd;
|
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 IP_PROTOCOL_UDP: u8 = 17;
|
||||||
const IPV6_NEXT_HEADER_HOP_BY_HOP: u8 = 0;
|
const IPV6_NEXT_HEADER_HOP_BY_HOP: u8 = 0;
|
||||||
const IPV6_NEXT_HEADER_ROUTING: u8 = 43;
|
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);
|
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) {
|
if ingress_role == Role::Client && is_dhcp_server_reply(frame) {
|
||||||
return Some(DropReason::DhcpServerReply);
|
return Some(DropReason::DhcpServerReply);
|
||||||
}
|
}
|
||||||
@@ -754,6 +761,13 @@ fn is_control_plane_frame(frame: EthernetFrame<'_>) -> bool {
|
|||||||
) || is_link_local_control_destination(frame.destination())
|
) || 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 {
|
fn is_link_local_control_destination(mac: MacAddr) -> bool {
|
||||||
let [a, b, c, d, e, f] = mac.octets();
|
let [a, b, c, d, e, f] = mac.octets();
|
||||||
|
|
||||||
@@ -1515,6 +1529,33 @@ mod tests {
|
|||||||
assert_filtered(&gateway_decision, DropReason::ControlPlaneEtherType);
|
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]
|
#[test]
|
||||||
fn filters_remote_dhcp_server_replies_but_allows_lan_replies() {
|
fn filters_remote_dhcp_server_replies_but_allows_lan_replies() {
|
||||||
let mut registry = RoomRegistry::default();
|
let mut registry = RoomRegistry::default();
|
||||||
|
|||||||
Reference in New Issue
Block a user