fix(relay): detect IPv6 RAs behind extension headers
Remote clients must not be able to inject IPv6 Router Advertisements onto the LAN. The relay already filtered direct ICMPv6 RA packets, but the check only looked at the IPv6 base header's immediate next-header value. A client could hide the ICMPv6 RA behind ordinary IPv6 extension headers and bypass that MVP safety policy. Walk the IPv6 extension-header chain for hop-by-hop, routing, destination options, fragment, and AH headers before checking the ICMPv6 message type. This keeps non-RA ICMPv6 traffic eligible for normal relay forwarding while closing the obvious RA evasion path. Document the stronger relay safety boundary in the README. Test Plan: - cargo fmt --check - cargo test -p lanparty-relay - cargo test --workspace - cargo clippy --workspace --all-targets -- -D warnings - git diff --check Refs: MVP relay L2 safety filters
This commit is contained in:
@@ -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<usize> {
|
||||
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<u8> {
|
||||
let mut packet = vec![0; 41];
|
||||
ipv6_payload(IPV6_NEXT_HEADER_ICMPV6, &[ICMPV6_ROUTER_ADVERTISEMENT])
|
||||
}
|
||||
|
||||
fn ipv6_router_advertisement_after_destination_options_payload() -> Vec<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user