diff --git a/README.md b/README.md index db5ce79..454e0a3 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ Public relay binary and relay-owned room state: - 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, DHCP-server, - and IPv6-RA frames + and IPv6-RA frames, including RAs 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-relay/src/lib.rs b/crates/lanparty-relay/src/lib.rs index 3fcf86a..4e5ac80 100644 --- a/crates/lanparty-relay/src/lib.rs +++ b/crates/lanparty-relay/src/lib.rs @@ -33,6 +33,12 @@ const ETHERTYPE_SLOW_PROTOCOLS: u16 = 0x8809; const ETHERTYPE_LLDP: u16 = 0x88cc; const ETHERTYPE_IPV6: u16 = 0x86dd; const IPV4_PROTOCOL_UDP: u8 = 17; +const IPV6_NEXT_HEADER_HOP_BY_HOP: u8 = 0; +const IPV6_NEXT_HEADER_ROUTING: u8 = 43; +const IPV6_NEXT_HEADER_FRAGMENT: u8 = 44; +const IPV6_NEXT_HEADER_AH: u8 = 51; +const IPV6_NEXT_HEADER_NO_NEXT: u8 = 59; +const IPV6_NEXT_HEADER_DESTINATION_OPTIONS: u8 = 60; const IPV6_NEXT_HEADER_ICMPV6: u8 = 58; const DHCP_SERVER_PORT: u16 = 67; const DHCP_CLIENT_PORT: u16 = 68; @@ -774,17 +780,60 @@ fn is_dhcp_server_reply(frame: EthernetFrame<'_>) -> bool { fn is_ipv6_router_advertisement(frame: EthernetFrame<'_>) -> bool { let bytes = frame.bytes(); - if frame.ethertype_or_len() != ETHERTYPE_IPV6 || bytes.len() < ETHERNET_HEADER_LEN + 40 + 1 { + if frame.ethertype_or_len() != ETHERTYPE_IPV6 || bytes.len() < ETHERNET_HEADER_LEN + 40 { return false; } let ipv6 = &bytes[ETHERNET_HEADER_LEN..]; let version = ipv6[0] >> 4; - let next_header = ipv6[6]; + if version != 6 { + return false; + } - version == 6 - && next_header == IPV6_NEXT_HEADER_ICMPV6 - && ipv6[40] == ICMPV6_ROUTER_ADVERTISEMENT + let Some(icmpv6_offset) = ipv6_icmpv6_payload_offset(ipv6) else { + return false; + }; + + ipv6.get(icmpv6_offset) + .is_some_and(|message_type| *message_type == ICMPV6_ROUTER_ADVERTISEMENT) +} + +fn ipv6_icmpv6_payload_offset(ipv6: &[u8]) -> Option { + let mut next_header = *ipv6.get(6)?; + let mut offset = 40; + + loop { + match next_header { + IPV6_NEXT_HEADER_ICMPV6 => return Some(offset), + IPV6_NEXT_HEADER_NO_NEXT => return None, + IPV6_NEXT_HEADER_HOP_BY_HOP + | IPV6_NEXT_HEADER_ROUTING + | IPV6_NEXT_HEADER_DESTINATION_OPTIONS => { + let extension = ipv6.get(offset..offset.checked_add(2)?)?; + next_header = extension[0]; + let extension_len = (usize::from(extension[1]) + 1) * 8; + offset = offset.checked_add(extension_len)?; + if offset > ipv6.len() { + return None; + } + } + IPV6_NEXT_HEADER_FRAGMENT => { + let fragment = ipv6.get(offset..offset.checked_add(8)?)?; + next_header = fragment[0]; + offset = offset.checked_add(8)?; + } + IPV6_NEXT_HEADER_AH => { + let extension = ipv6.get(offset..offset.checked_add(2)?)?; + next_header = extension[0]; + let extension_len = (usize::from(extension[1]) + 2) * 4; + offset = offset.checked_add(extension_len)?; + if offset > ipv6.len() { + return None; + } + } + _ => return None, + } + } } fn reject_control_error(error: ControlError) -> Reject { @@ -855,10 +904,37 @@ mod tests { } fn ipv6_router_advertisement_payload() -> Vec { - let mut packet = vec![0; 41]; + ipv6_payload(IPV6_NEXT_HEADER_ICMPV6, &[ICMPV6_ROUTER_ADVERTISEMENT]) + } + + fn ipv6_router_advertisement_after_destination_options_payload() -> Vec { + ipv6_payload( + IPV6_NEXT_HEADER_DESTINATION_OPTIONS, + &ipv6_extension_payload(IPV6_NEXT_HEADER_ICMPV6, &[ICMPV6_ROUTER_ADVERTISEMENT]), + ) + } + + fn ipv6_echo_request_after_destination_options_payload() -> Vec { + ipv6_payload( + IPV6_NEXT_HEADER_DESTINATION_OPTIONS, + &ipv6_extension_payload(IPV6_NEXT_HEADER_ICMPV6, &[128]), + ) + } + + fn ipv6_payload(next_header: u8, upper_payload: &[u8]) -> Vec { + let mut packet = vec![0; 40]; packet[0] = 0x60; - packet[6] = IPV6_NEXT_HEADER_ICMPV6; - packet[40] = ICMPV6_ROUTER_ADVERTISEMENT; + packet[6] = next_header; + packet.extend_from_slice(upper_payload); + packet + } + + fn ipv6_extension_payload(next_header: u8, upper_payload: &[u8]) -> Vec { + let mut packet = Vec::with_capacity(8 + upper_payload.len()); + packet.push(next_header); + packet.push(0); + packet.extend_from_slice(&[0; 6]); + packet.extend_from_slice(upper_payload); packet } @@ -1404,6 +1480,47 @@ mod tests { assert_filtered(&decision, DropReason::Ipv6RouterAdvertisement); } + #[test] + fn filters_remote_ipv6_router_advertisements_after_extension_headers() { + let mut registry = RoomRegistry::default(); + registry.join(gateway_hello()).unwrap(); + let client = registry.join(client_hello(1)).unwrap(); + let destination = MacAddr::new([0x33, 0x33, 0, 0, 0, 1]); + let frame = ethernet_with_payload( + destination, + mac(1), + ETHERTYPE_IPV6, + &ipv6_router_advertisement_after_destination_options_payload(), + ); + + let decision = registry + .forward_ethernet(&room(), client.peer().peer_id(), &frame) + .unwrap(); + + assert_filtered(&decision, DropReason::Ipv6RouterAdvertisement); + } + + #[test] + fn allows_remote_icmpv6_that_is_not_router_advertisement() { + 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 frame = ethernet_with_payload( + destination, + mac(1), + ETHERTYPE_IPV6, + &ipv6_echo_request_after_destination_options_payload(), + ); + + let decision = registry + .forward_ethernet(&room(), client.peer().peer_id(), &frame) + .unwrap(); + + assert_eq!(decision.action(), FrameAction::Forwarded); + assert_eq!(decision.targets(), &[gateway.peer().peer_id()]); + } + #[test] fn drops_malformed_frames() { let mut registry = RoomRegistry::default();