diff --git a/README.md b/README.md index 454e0a3..d2b065b 100644 --- a/README.md +++ b/README.md @@ -102,8 +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, DHCP-server, - and IPv6-RA frames, including RAs behind ordinary IPv6 extension headers +- L2 safety filters for invalid-source, jumbo, switch-control, 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-relay/src/lib.rs b/crates/lanparty-relay/src/lib.rs index 4e5ac80..bf99488 100644 --- a/crates/lanparty-relay/src/lib.rs +++ b/crates/lanparty-relay/src/lib.rs @@ -32,7 +32,7 @@ const ETHERTYPE_EAPOL: u16 = 0x888e; const ETHERTYPE_SLOW_PROTOCOLS: u16 = 0x8809; const ETHERTYPE_LLDP: u16 = 0x88cc; const ETHERTYPE_IPV6: u16 = 0x86dd; -const IPV4_PROTOCOL_UDP: u8 = 17; +const IP_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; @@ -40,8 +40,10 @@ 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; +const DHCPV4_SERVER_PORT: u16 = 67; +const DHCPV4_CLIENT_PORT: u16 = 68; +const DHCPV6_CLIENT_PORT: u16 = 546; +const DHCPV6_SERVER_PORT: u16 = 547; const ICMPV6_ROUTER_ADVERTISEMENT: u8 = 134; #[derive(Debug, Clone, PartialEq, Eq)] @@ -755,6 +757,10 @@ fn is_link_local_control_destination(mac: MacAddr) -> bool { } fn is_dhcp_server_reply(frame: EthernetFrame<'_>) -> bool { + is_ipv4_dhcp_server_reply(frame) || is_ipv6_dhcp_server_reply(frame) +} + +fn is_ipv4_dhcp_server_reply(frame: EthernetFrame<'_>) -> bool { let bytes = frame.bytes(); if frame.ethertype_or_len() != ETHERTYPE_IPV4 || bytes.len() < ETHERNET_HEADER_LEN + 20 { return false; @@ -763,10 +769,7 @@ fn is_dhcp_server_reply(frame: EthernetFrame<'_>) -> bool { let ipv4 = &bytes[ETHERNET_HEADER_LEN..]; let version = ipv4[0] >> 4; let header_len = usize::from(ipv4[0] & 0x0f) * 4; - if version != 4 - || header_len < 20 - || ipv4.len() < header_len + 8 - || ipv4[9] != IPV4_PROTOCOL_UDP + if version != 4 || header_len < 20 || ipv4.len() < header_len + 8 || ipv4[9] != IP_PROTOCOL_UDP { return false; } @@ -775,7 +778,30 @@ fn is_dhcp_server_reply(frame: EthernetFrame<'_>) -> bool { let source_port = u16::from_be_bytes([udp[0], udp[1]]); let destination_port = u16::from_be_bytes([udp[2], udp[3]]); - source_port == DHCP_SERVER_PORT && destination_port == DHCP_CLIENT_PORT + source_port == DHCPV4_SERVER_PORT && destination_port == DHCPV4_CLIENT_PORT +} + +fn is_ipv6_dhcp_server_reply(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..]; + if !is_ipv6_packet(ipv6) { + return false; + } + + let Some(udp_offset) = ipv6_upper_layer_payload_offset(ipv6, IP_PROTOCOL_UDP) else { + return false; + }; + let Some(udp) = ipv6.get(udp_offset..udp_offset.saturating_add(4)) else { + return false; + }; + let source_port = u16::from_be_bytes([udp[0], udp[1]]); + let destination_port = u16::from_be_bytes([udp[2], udp[3]]); + + source_port == DHCPV6_SERVER_PORT && destination_port == DHCPV6_CLIENT_PORT } fn is_ipv6_router_advertisement(frame: EthernetFrame<'_>) -> bool { @@ -785,12 +811,11 @@ fn is_ipv6_router_advertisement(frame: EthernetFrame<'_>) -> bool { } let ipv6 = &bytes[ETHERNET_HEADER_LEN..]; - let version = ipv6[0] >> 4; - if version != 6 { + if !is_ipv6_packet(ipv6) { return false; } - let Some(icmpv6_offset) = ipv6_icmpv6_payload_offset(ipv6) else { + let Some(icmpv6_offset) = ipv6_upper_layer_payload_offset(ipv6, IPV6_NEXT_HEADER_ICMPV6) else { return false; }; @@ -798,13 +823,17 @@ fn is_ipv6_router_advertisement(frame: EthernetFrame<'_>) -> bool { .is_some_and(|message_type| *message_type == ICMPV6_ROUTER_ADVERTISEMENT) } -fn ipv6_icmpv6_payload_offset(ipv6: &[u8]) -> Option { +fn is_ipv6_packet(ipv6: &[u8]) -> bool { + ipv6.first().is_some_and(|first| first >> 4 == 6) +} + +fn ipv6_upper_layer_payload_offset(ipv6: &[u8], expected_next_header: u8) -> Option { let mut next_header = *ipv6.get(6)?; let mut offset = 40; loop { match next_header { - IPV6_NEXT_HEADER_ICMPV6 => return Some(offset), + next_header if next_header == expected_next_header => return Some(offset), IPV6_NEXT_HEADER_NO_NEXT => return None, IPV6_NEXT_HEADER_HOP_BY_HOP | IPV6_NEXT_HEADER_ROUTING @@ -897,12 +926,33 @@ mod tests { fn ipv4_udp_payload(source_port: u16, destination_port: u16) -> Vec { let mut packet = vec![0; 28]; packet[0] = 0x45; - packet[9] = IPV4_PROTOCOL_UDP; + packet[9] = IP_PROTOCOL_UDP; packet[20..22].copy_from_slice(&source_port.to_be_bytes()); packet[22..24].copy_from_slice(&destination_port.to_be_bytes()); packet } + fn udp_payload(source_port: u16, destination_port: u16) -> Vec { + let mut packet = vec![0; 8]; + packet[0..2].copy_from_slice(&source_port.to_be_bytes()); + packet[2..4].copy_from_slice(&destination_port.to_be_bytes()); + packet + } + + fn ipv6_udp_payload(source_port: u16, destination_port: u16) -> Vec { + ipv6_payload(IP_PROTOCOL_UDP, &udp_payload(source_port, destination_port)) + } + + fn ipv6_udp_after_destination_options_payload( + source_port: u16, + destination_port: u16, + ) -> Vec { + ipv6_payload( + IPV6_NEXT_HEADER_DESTINATION_OPTIONS, + &ipv6_extension_payload(IP_PROTOCOL_UDP, &udp_payload(source_port, destination_port)), + ) + } + fn ipv6_router_advertisement_payload() -> Vec { ipv6_payload(IPV6_NEXT_HEADER_ICMPV6, &[ICMPV6_ROUTER_ADVERTISEMENT]) } @@ -1442,7 +1492,7 @@ mod tests { let mut registry = RoomRegistry::default(); let gateway = registry.join(gateway_hello()).unwrap(); let client = registry.join(client_hello(1)).unwrap(); - let payload = ipv4_udp_payload(DHCP_SERVER_PORT, DHCP_CLIENT_PORT); + let payload = ipv4_udp_payload(DHCPV4_SERVER_PORT, DHCPV4_CLIENT_PORT); let client_frame = ethernet_with_payload(MacAddr::BROADCAST, mac(1), ETHERTYPE_IPV4, &payload); let gateway_frame = @@ -1460,6 +1510,47 @@ mod tests { assert_eq!(gateway_decision.targets(), &[client.peer().peer_id()]); } + #[test] + fn filters_remote_dhcpv6_server_replies_but_allows_lan_replies() { + 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, 1, 0, 2]); + let payload = + ipv6_udp_after_destination_options_payload(DHCPV6_SERVER_PORT, DHCPV6_CLIENT_PORT); + 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::DhcpServerReply); + assert_eq!(gateway_decision.action(), FrameAction::Forwarded); + assert_eq!(gateway_decision.targets(), &[client.peer().peer_id()]); + } + + #[test] + fn allows_remote_dhcpv6_client_requests() { + 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, 1, 0, 2]); + let payload = ipv6_udp_payload(DHCPV6_CLIENT_PORT, DHCPV6_SERVER_PORT); + let frame = ethernet_with_payload(destination, mac(1), ETHERTYPE_IPV6, &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 filters_remote_ipv6_router_advertisements() { let mut registry = RoomRegistry::default();