diff --git a/README.md b/README.md index d2b065b..e403b5c 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, 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 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 c6094e9..e6c1a26 100644 --- a/crates/lanparty-obs/src/lib.rs +++ b/crates/lanparty-obs/src/lib.rs @@ -36,6 +36,7 @@ pub enum DropReason { ControlPlaneEtherType, DhcpServerReply, Ipv6RouterAdvertisement, + Ipv6Fragment, DatagramBudget, UnknownDestination, RateLimit, diff --git a/crates/lanparty-relay/src/lib.rs b/crates/lanparty-relay/src/lib.rs index bf99488..8dd75eb 100644 --- a/crates/lanparty-relay/src/lib.rs +++ b/crates/lanparty-relay/src/lib.rs @@ -740,6 +740,10 @@ fn safety_drop_reason(ingress_role: Role, frame: EthernetFrame<'_>) -> Option) -> bool { .is_some_and(|message_type| *message_type == ICMPV6_ROUTER_ADVERTISEMENT) } +fn is_ipv6_fragment(frame: EthernetFrame<'_>) -> bool { + let bytes = frame.bytes(); + if frame.ethertype_or_len() != ETHERTYPE_IPV6 || bytes.len() < ETHERNET_HEADER_LEN + 40 { + return false; + } + + let ipv6 = &bytes[ETHERNET_HEADER_LEN..]; + is_ipv6_packet(ipv6) && ipv6_has_extension_header(ipv6, IPV6_NEXT_HEADER_FRAGMENT) +} + fn is_ipv6_packet(ipv6: &[u8]) -> bool { ipv6.first().is_some_and(|first| first >> 4 == 6) } +fn ipv6_has_extension_header(ipv6: &[u8], expected_next_header: u8) -> bool { + ipv6_upper_layer_payload_offset(ipv6, expected_next_header).is_some() +} + fn ipv6_upper_layer_payload_offset(ipv6: &[u8], expected_next_header: u8) -> Option { let mut next_header = *ipv6.get(6)?; let mut offset = 40; @@ -971,6 +989,16 @@ mod tests { ) } + fn ipv6_fragment_payload(next_header: u8, fragment_payload: &[u8]) -> Vec { + let mut packet = Vec::with_capacity(8 + fragment_payload.len()); + packet.push(next_header); + packet.push(0); + packet.extend_from_slice(&0_u16.to_be_bytes()); + packet.extend_from_slice(&1_u32.to_be_bytes()); + packet.extend_from_slice(fragment_payload); + packet + } + fn ipv6_payload(next_header: u8, upper_payload: &[u8]) -> Vec { let mut packet = vec![0; 40]; packet[0] = 0x60; @@ -1551,6 +1579,32 @@ mod tests { assert_eq!(decision.targets(), &[gateway.peer().peer_id()]); } + #[test] + fn filters_remote_ipv6_fragments_but_allows_lan_fragments() { + let mut registry = RoomRegistry::default(); + let gateway = registry.join(gateway_hello()).unwrap(); + let client = registry.join(client_hello(1)).unwrap(); + let destination = MacAddr::new([0x33, 0x33, 0, 0, 0, 1]); + let payload = ipv6_payload( + IPV6_NEXT_HEADER_FRAGMENT, + &ipv6_fragment_payload(IP_PROTOCOL_UDP, b"partial upper payload"), + ); + let client_frame = ethernet_with_payload(destination, mac(1), ETHERTYPE_IPV6, &payload); + let gateway_frame = + ethernet_with_payload(destination, physical_mac(), ETHERTYPE_IPV6, &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::Ipv6Fragment); + assert_eq!(gateway_decision.action(), FrameAction::Forwarded); + assert_eq!(gateway_decision.targets(), &[client.peer().peer_id()]); + } + #[test] fn filters_remote_ipv6_router_advertisements() { let mut registry = RoomRegistry::default();