netstack: fix IPv6 firewall bypass - return final protocol + offset

Fixes the P001 review finding. The previous ipv6_transport_offset
walker returned only the offset, but the caller passed the INITIAL
next_header (which is usually a Hop-by-Hop / Routing / Destination /
Fragment number, not a transport protocol) to parse_ports() and to
PacketContext.protocol. Any IPv6 packet with extension headers
silently failed firewall port rules.

Two changes:
1. ipv6_transport_offset now returns (final_protocol, offset) tuple.
   Continues to return None for malformed/encrypted/too-deep chains
   so we never fall through to wrong-offset parse.
2. The IPv6 dispatch in infer_context() uses the returned final
   protocol: parse_ports() gets the right IpProtocol (TCP/UDP/ICMPv6)
   and PacketContext.protocol is set to the discovered final protocol.

Also corrects the AH header length formula: RFC 8200 §4 says AH
length is in 4-octet units over the 8-octet fixed part, so total
= (len+2)*8, not the generic (len+1)*8 used for Hop-by-Hop /
Routing / Destination.
This commit is contained in:
Red Bear OS
2026-07-27 20:26:48 +09:00
parent 2dba10b7df
commit 76e01f06ff
+26 -17
View File
@@ -528,19 +528,18 @@ fn infer_context(
6 => {
if let Ok(ipv6) = Ipv6Packet::new_checked(packet) {
let nh = ipv6.next_header();
// Walk the extension header chain to find the actual
// transport-layer offset. Previously this used a fixed
// 40-byte offset, so any IPv6 packet with extension
// headers (Hop-by-Hop, Routing, Fragment, Destination,
// ESP, AH) caused parse_ports() to read the wrong bytes
// and return None. A firewall rule with a port predicate
// would silently fail to match — a firewall bypass.
let transport_offset = ipv6_transport_offset(packet, &nh);
let (src_port, dst_port) = match transport_offset {
Some(off) if packet.len() >= off => {
parse_ports(&nh, &packet[off..])
}
_ => (None, None),
let (transport_proto, transport_offset) =
match ipv6_transport_offset(packet, &nh) {
Some(v) => v,
None => (0u8, 0usize),
};
let (src_port, dst_port) = if transport_offset > 0
&& packet.len() >= transport_offset
{
parse_ports(&smoltcp::wire::IpProtocol::from(transport_proto),
&packet[transport_offset..])
} else {
(None, None)
};
PacketContext {
hook,
@@ -548,7 +547,7 @@ fn infer_context(
out_dev: None,
src_addr: IpAddress::Ipv6(ipv6.src_addr()),
dst_addr: IpAddress::Ipv6(ipv6.dst_addr()),
protocol: nh.into(),
protocol: smoltcp::wire::IpProtocol::from(transport_proto),
src_port,
dst_port,
packet,
@@ -595,7 +594,7 @@ fn infer_context(
fn ipv6_transport_offset(
packet: &[u8],
initial_next_header: &smoltcp::wire::IpProtocol,
) -> Option<usize> {
) -> Option<(u8, usize)> {
// Extension header type values per RFC 8200 / IANA.
const IPV6_HOP_BY_HOP: u8 = 0;
const IPV6_ROUTING: u8 = 43;
@@ -620,7 +619,7 @@ fn ipv6_transport_offset(
}
// Known transport-layer protocols terminate the chain.
if current == 6 || current == 17 || current == 58 || current == 132 {
return Some(offset);
return Some((current, offset));
}
match current {
IPV6_HOP_BY_HOP | IPV6_ROUTING | IPV6_DEST_OPTS | IPV6_AUTH => {
@@ -628,7 +627,17 @@ fn ipv6_transport_offset(
return None;
}
let len_units = packet[offset + 1] as usize;
let header_size = (len_units + 1) * 8;
// RFC 8200 §4: header length is in 8-octet units, NOT
// counting the first 8 octets. For Hop-by-Hop, Routing,
// and Destination Options that matches (len+1)*8. For
// AH the payload-length field is in 4-octet units over
// the 8-octet fixed part, so total = (len+2)*8.
let header_size = if current == IPV6_AUTH {
let len_units = (packet[offset + 1] as usize).saturating_add(2);
len_units * 4
} else {
(len_units + 1) * 8
};
if header_size < 8 || offset + header_size > packet.len() {
return None;
}