diff --git a/README.md b/README.md index 55222aa..5c3ebf6 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/crates/lanparty-obs/src/lib.rs b/crates/lanparty-obs/src/lib.rs index e6c1a26..b8c23a5 100644 --- a/crates/lanparty-obs/src/lib.rs +++ b/crates/lanparty-obs/src/lib.rs @@ -37,6 +37,7 @@ pub enum DropReason { DhcpServerReply, Ipv6RouterAdvertisement, Ipv6Fragment, + VlanTaggedFrame, DatagramBudget, UnknownDestination, RateLimit, diff --git a/crates/lanparty-relay/src/lib.rs b/crates/lanparty-relay/src/lib.rs index 8dd75eb..7a39593 100644 --- a/crates/lanparty-relay/src/lib.rs +++ b/crates/lanparty-relay/src/lib.rs @@ -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) -> 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();