mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 14:26:57 +01:00
ipv6: tighten handling of fragments wrt extension headers
This commit is contained in:
1 parent
d576827ade
commit
1519dbbf8e
4 files changed
+72
-44
No files matched your search
+21
-33
@@ -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
|
||||
}
|
||||
|
||||
+28
-11
@@ -522,21 +522,29 @@ func Test_IPv6FindUpperProtocol(t *testing.T) {
|
||||
dst := net.ParseIP("fd00::2")
|
||||
|
||||
// 8 byte extension/transport stand-ins, first byte is the next header, second is the length field
|
||||
extToTCP := []byte{6, 0, 0, 0, 0, 0, 0, 0} // len 0 -> 8 bytes, next = TCP
|
||||
extToUDP := []byte{17, 0, 0, 0, 0, 0, 0, 0} // len 0 -> 8 bytes, next = UDP
|
||||
extToRouting := []byte{43, 0, 0, 0, 0, 0, 0, 0} // len 0 -> 8 bytes, next = Routing
|
||||
ahToUDP := []byte{17, 0, 0, 0, 0, 0, 0, 0} // AH len 0 -> (0+2)<<2 = 8 bytes, next = UDP
|
||||
firstFragToUDP := []byte{17, 0, 0, 1, 0, 0, 0, 1} // frag offset 0, M=1, next = UDP
|
||||
nonFirstFrag := []byte{17, 0, 0, 9, 0, 0, 0, 1} // frag offset non-zero, next = UDP
|
||||
transport := []byte{0, 80, 1, 187, 0, 0, 0, 0} // stand-in bytes, IPv6FindUpperProtocol never reads ports
|
||||
extToHbH := []byte{0, 0, 0, 0, 0, 0, 0, 0} // len 0 -> 8 bytes, next = Hop-by-Hop
|
||||
extToTCP := []byte{6, 0, 0, 0, 0, 0, 0, 0} // len 0 -> 8 bytes, next = TCP
|
||||
extToUDP := []byte{17, 0, 0, 0, 0, 0, 0, 0} // len 0 -> 8 bytes, next = UDP
|
||||
extToRouting := []byte{43, 0, 0, 0, 0, 0, 0, 0} // len 0 -> 8 bytes, next = Routing
|
||||
ahToUDP := []byte{17, 0, 0, 0, 0, 0, 0, 0} // AH len 0 -> (0+2)<<2 = 8 bytes, next = UDP
|
||||
firstFragToUDP := []byte{17, 0, 0, 1, 0, 0, 0, 1} // frag offset 0, M=1, next = UDP
|
||||
nonFirstFrag := []byte{17, 0, 0, 9, 0, 0, 0, 1} // frag offset non-zero, next = UDP
|
||||
transport := []byte{0, 80, 1, 187, 0, 0, 0, 0} // stand-in bytes, IPv6FindUpperProtocol never reads ports
|
||||
extToHbH := []byte{0, 0, 0, 0, 0, 0, 0, 0} // len 0 -> 8 bytes, next = Hop-by-Hop
|
||||
extToDst := []byte{60, 0, 0, 0, 0, 0, 0, 0} // len 0 -> 8 bytes, next = Destination
|
||||
extToFrag := []byte{44, 0, 0, 0, 0, 0, 0, 0} // len 0 -> 8 bytes, next = Fragment
|
||||
firstFragToDst := []byte{60, 0, 0, 1, 0, 0, 0, 1} // frag offset 0, M=1, next = Destination
|
||||
firstFragToAH := []byte{51, 0, 0, 1, 0, 0, 0, 1} // frag offset 0, M=1, next = AH
|
||||
firstFragToFrag := []byte{44, 0, 0, 1, 0, 0, 0, 1} // frag offset 0, M=1, next = Fragment
|
||||
firstFragToESP := []byte{50, 0, 0, 1, 0, 0, 0, 1} // frag offset 0, M=1, next = ESP
|
||||
atomicFragToDst := []byte{60, 0, 0, 0, 0, 0, 0, 1} // frag offset 0, M=0, next = Destination
|
||||
nonFirstFragToDst := []byte{60, 0, 0, 9, 0, 0, 0, 1} // frag offset non-zero, next = Destination
|
||||
|
||||
// 8 walked headers spend the budget before the 9th slot's terminal is ever classified. This is the
|
||||
// report 4040546 shape: without failing closed the walk hands back offset 104, and a caller reads ports
|
||||
// from the attacker's bytes there.
|
||||
// from the attacker's bytes there. Destination is the header that may legally repeat.
|
||||
var budgetChain []byte
|
||||
for range 7 {
|
||||
budgetChain = append(budgetChain, extToHbH...)
|
||||
budgetChain = append(budgetChain, extToDst...)
|
||||
}
|
||||
budgetChain = append(budgetChain, extToTCP...)
|
||||
budgetChain = append(budgetChain, transport...)
|
||||
@@ -559,11 +567,20 @@ func Test_IPv6FindUpperProtocol(t *testing.T) {
|
||||
{"ah then udp", 51, append(ahToUDP, transport...), 17, ipv6.HeaderLen + 8, false, false, nil},
|
||||
{"first fragment walks to transport", 44, append(firstFragToUDP, transport...), 17, ipv6.HeaderLen + 8, false, true, nil},
|
||||
{"non-first fragment stops", 44, append(nonFirstFrag, transport...), 17, ipv6.HeaderLen, true, true, nil},
|
||||
{"non-first fragment reports its claimed protocol", 44, append(nonFirstFragToDst, transport...), 60, ipv6.HeaderLen, true, true, nil},
|
||||
{"routing then first fragment", 43, append(append(extToFrag, firstFragToUDP...), transport...), 17, ipv6.HeaderLen + 16, false, true, nil},
|
||||
{"first fragment of a non-transport protocol is terminal", 44, append(firstFragToESP, transport...), 50, ipv6.HeaderLen + 8, false, true, nil},
|
||||
{"destination after first fragment", 44, append(append(firstFragToDst, extToTCP...), transport...), 0, 0, false, false, ErrIPv6ExtensionHeaderAfterFragment},
|
||||
{"ah after first fragment", 44, append(append(firstFragToAH, ahToUDP...), transport...), 0, 0, false, false, ErrIPv6ExtensionHeaderAfterFragment},
|
||||
{"fragment after first fragment", 44, append(append(firstFragToFrag, nonFirstFrag...), transport...), 0, 0, false, false, ErrIPv6ExtensionHeaderAfterFragment},
|
||||
{"destination after atomic fragment", 44, append(append(atomicFragToDst, extToTCP...), transport...), 0, 0, false, false, ErrIPv6ExtensionHeaderAfterFragment},
|
||||
{"hop-by-hop after destination", 60, append(append(extToHbH, extToTCP...), transport...), 0, 0, false, false, ErrIPv6HopByHopHeadersMustBeFirst},
|
||||
{"hop-by-hop twice", 0, append(append(extToHbH, extToTCP...), transport...), 0, 0, false, false, ErrIPv6HopByHopHeadersMustBeFirst},
|
||||
{"unknown protocol is terminal", 132, transport, 132, ipv6.HeaderLen, false, false, nil}, // SCTP
|
||||
{"truncated extension header", 0, nil, 0, 0, false, false, ErrIPv6CouldNotFindPayload},
|
||||
// Destination Options with a declared length (255+1)*8 = 2048 that runs past the 48 byte buffer, next = SCTP
|
||||
{"extension length past buffer", 0, []byte{132, 255, 0, 0, 0, 0, 0, 0}, 0, 0, false, false, ErrIPv6CouldNotFindPayload},
|
||||
{"budget exhausted fails closed", 0, budgetChain, 0, 0, false, false, ErrIPv6CouldNotFindPayload},
|
||||
{"budget exhausted fails closed", 60, budgetChain, 0, 0, false, false, ErrIPv6CouldNotFindPayload},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
Reference in new issue
Block a user