ipv6: tighten handling of fragments wrt extension headers

This commit is contained in:
JackDoan committed 2026-09-29 13:20:22 -05:00
1 parent d576827ade
commit 1519dbbf8e
4 files changed
+72 -44

No files matched your search

+21 -33
View File
@@ -11,7 +11,7 @@ import (
// ErrIPv6CouldNotFindPayload is returned when the ipv6 extension header chain is truncated before a terminal
// upper layer protocol is reached.
var ErrIPv6CouldNotFindPayload = errors.New("could not find payload in ipv6 packet")
var ErrIPv6FragmentHeadersNotTransport = errors.New("non-transport header after fragment header in packet")
var ErrIPv6ExtensionHeaderAfterFragment = errors.New("extension header after fragment header in packet")
var ErrIPv6HopByHopHeadersMustBeFirst = errors.New("hop-by-hop header must be first extension header")
const (
@@ -354,20 +354,15 @@ func ipv6CreateRejectTCPPacket(packet []byte, out []byte, offset int) []byte {
return out
}
func nextHeaderIsTransport(nextHeader uint8) error {
switch nextHeader {
case IPProtocolTCP, IPProtocolUDP, IPProtocolICMPv6, IPProtocolICMP:
return nil
default:
return ErrIPv6FragmentHeadersNotTransport
}
}
// IPv6FindUpperProtocol walks the ipv6 extension header chain and returns the upper layer protocol, the offset it begins at, and whether the packet is a non-first fragment.
// Only the RFC 8200 and IANA extension headers below are walked.
// Everything else, including Mobility (135), HIP (139), Shim6 (140), experimental 253/254, and real upper layer protocols like SCTP or GRE, is terminal.
// For a non-first fragment the returned protocol is the fragmented protocol and offset points at the fragment header, there is no transport header to locate.
// Returns ErrIPv6CouldNotFindPayload if packet is smaller than an ipv6 header, or the chain is truncated, or if more than 8 extension headers are present.
// Returns ErrIPv6HopByHopHeadersMustBeFirst if a hop-by-hop header appears anywhere but directly after the ipv6 header (RFC 8200 section 4.1).
// Returns ErrIPv6ExtensionHeaderAfterFragment if any walked extension header, including a second fragment header, follows the fragment header of a
// first fragment. The upper layer header must then begin at byte 0 of the fragmentable part, beyond the reach of any non-first fragment,
// whose offset is at least 8, so overlapping fragments cannot rewrite the ports or protocol the firewall read from the first fragment.
func IPv6FindUpperProtocol(packet []byte) (nextHeader uint8, offset int, isFragment bool, anyFragment bool, err error) {
const maxIPv6ExtHeaders = 8
if len(packet) < ipv6.HeaderLen {
@@ -379,6 +374,9 @@ func IPv6FindUpperProtocol(packet []byte) (nextHeader uint8, offset int, isFragm
for range maxIPv6ExtHeaders {
switch nextHeader {
case IPv6HeaderHopByHop, IPv6HeaderRouting, IPv6HeaderDestination:
if anyFragment {
return 0, 0, false, false, ErrIPv6ExtensionHeaderAfterFragment
}
if len(packet) < offset+2 {
return 0, 0, false, false, ErrIPv6CouldNotFindPayload
}
@@ -392,37 +390,27 @@ func IPv6FindUpperProtocol(packet []byte) (nextHeader uint8, offset int, isFragm
offset += (int(packet[offset+1]) + 1) << 3
case IPv6HeaderFragment:
if anyFragment {
return 0, 0, false, false, ErrIPv6ExtensionHeaderAfterFragment
}
if len(packet) < offset+8 {
return 0, 0, false, false, ErrIPv6CouldNotFindPayload
}
anyFragment = true
// Non-first fragments carry no transport header, report the fragmented protocol and stop.
// Per RFC 8200 section 4.5 the receiver only uses the offset zero fragment's next header, so the value
// returned here can't be verified.
if packet[offset+2] != 0 || packet[offset+3]&0xf8 != 0 {
return packet[offset], offset, true, anyFragment, nil
}
nextHeader = packet[offset]
//because all these branches are terminal, we reject multiple fragment headers as well
// we always require a transport-layer header to follow a fragment header:
if err = nextHeaderIsTransport(nextHeader); err != nil {
return 0, 0, false, false, err
}
if packet[offset+2] == 0 && packet[offset+3]&0xf8 == 0 {
// This is a first-fragment
offset += 8
if offset > len(packet) {
// one more sanity check for length
return 0, 0, false, false, ErrIPv6CouldNotFindPayload
}
return nextHeader, offset, isFragment, anyFragment, nil
}
// Non-first fragments carry no transport header, and are terminal.
// However, per RFC 8200 §4.5, the "next header" of a non-first-fragment is not actually consulted during reassembly
// todo nextHeader is constrained to ICMP/TCP/UDP by nextHeaderIsTransport
// todo but should we make "fragment" a firewall-level protocol?
return nextHeader, offset, true, anyFragment, nil
offset += 8
case IPv6HeaderAuthentication: // AH
if anyFragment {
return 0, 0, false, false, ErrIPv6ExtensionHeaderAfterFragment
}
if len(packet) < offset+2 {
return 0, 0, false, false, ErrIPv6CouldNotFindPayload
}