net/packet,wgengine/filter: handle IPv6 fragment extension header
decode6 didn't parse the IPv6 Fragment extension header (Next Header 44), so any source-fragmented IPv6 packet was classified as an unknown protocol and matched no ACL rule. The filter then silently dropped it and counted it as an "acl" drop, even on allow-all tailnets, blackholing large UDP (DNS, WebRTC, etc.) over a tailnet's IPv6 addresses. IPv4 fragments were already handled by decode4. Parse the fragment header the same way: read the first fragment's transport ports so the filter matches it like an unfragmented packet, pass later fragments through as ipproto.Fragment, and reject overlapping-fragment offsets (RFC 1858) and first fragments too short to hold the transport header as unknown. Fixes #20083 Signed-off-by: Steve Avery <hello@stevenavery.com>
This commit is contained in:
committed by
James Tucker
parent
f002f6bb3a
commit
4c4ec3d468
+70
-11
@@ -19,6 +19,16 @@ const unknown = ipproto.Unknown
|
||||
// RFC1858: prevent overlapping fragment attacks.
|
||||
const minFragBlks = (60 + 20) / 8 // max IPv4 header + basic TCP header in fragment blocks (8 bytes each)
|
||||
|
||||
// ip6FragHeader is the IANA protocol number for the IPv6 Fragment extension
|
||||
// header ("IPv6-Frag"). It appears as the base header's Next Header value on a
|
||||
// fragmented packet; decode6 steps over it to reach the real sub-protocol.
|
||||
// This is distinct from ipproto.Fragment (0xFF), our internal sentinel for a
|
||||
// non-first fragment whose sub-protocol header is not present.
|
||||
const ip6FragHeader ipproto.Proto = 44
|
||||
|
||||
// ip6FragHeaderLength is the length of the IPv6 Fragment extension header.
|
||||
const ip6FragHeaderLength = 8
|
||||
|
||||
type TCPFlag uint8
|
||||
|
||||
const (
|
||||
@@ -272,19 +282,25 @@ func (q *Parsed) decode6(b []byte) {
|
||||
q.Src = withIP(q.Src, srcIP)
|
||||
q.Dst = withIP(q.Dst, dstIP)
|
||||
|
||||
// We don't support any IPv6 extension headers. Don't try to
|
||||
// be clever. Therefore, the IP subprotocol always starts at
|
||||
// byte 40.
|
||||
// The IP subprotocol normally begins right after the 40-byte IPv6
|
||||
// header. The one extension header we parse is the Fragment header
|
||||
// (Next Header 44): a host source-fragmenting a datagram larger than
|
||||
// the tun MTU emits these, and RFC 8200 section 4.5 requires the
|
||||
// receiver to reassemble them, so we must let them through. For the
|
||||
// first fragment we step over the fragment header and read the real
|
||||
// sub-protocol's ports exactly as decode4 does; later fragments are
|
||||
// marked ipproto.Fragment and passed through by the filter.
|
||||
//
|
||||
// Note that this means we don't support fragmentation in
|
||||
// IPv6. This is fine, because IPv6 strongly mandates that you
|
||||
// should not fragment, which makes fragmentation on the open
|
||||
// internet extremely uncommon.
|
||||
//
|
||||
// This also means we don't support IPSec headers (AH/ESP), or
|
||||
// IPv6 jumbo frames. Those will get marked Unknown and
|
||||
// dropped.
|
||||
// We still don't parse any other extension headers (hop-by-hop,
|
||||
// routing, destination options) or IPSec headers (AH/ESP), nor a
|
||||
// Fragment header that isn't the base header's immediate Next Header.
|
||||
// Those get marked Unknown and dropped.
|
||||
q.subofs = 40
|
||||
if q.IPProto == ip6FragHeader {
|
||||
if !q.decode6Fragment(b) {
|
||||
return
|
||||
}
|
||||
}
|
||||
sub := b[q.subofs:]
|
||||
sub = sub[:len(sub):len(sub)] // help the compiler do bounds check elimination
|
||||
|
||||
@@ -341,6 +357,49 @@ func (q *Parsed) decode6(b []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
// decode6Fragment parses the IPv6 Fragment extension header at q.subofs in b
|
||||
// (q.subofs is the 40-byte base header length when called). It reports whether
|
||||
// decode6 should continue into the sub-protocol switch: true only for the
|
||||
// first fragment, where q.subofs and q.IPProto have been advanced to the real
|
||||
// transport header so its ports get parsed like an unfragmented packet. For
|
||||
// later or malformed fragments it sets q.IPProto itself (ipproto.Fragment to
|
||||
// pass through, or unknown to drop) and returns false.
|
||||
func (q *Parsed) decode6Fragment(b []byte) (continueDecode bool) {
|
||||
// The fragment header is 8 bytes: Next Header, Reserved, a 13-bit
|
||||
// Fragment Offset (in 8-byte blocks) plus a More-Fragments flag, and a
|
||||
// 32-bit Identification.
|
||||
if len(b) < q.subofs+ip6FragHeaderLength {
|
||||
q.IPProto = unknown
|
||||
return false
|
||||
}
|
||||
frag := b[q.subofs:]
|
||||
nextHdr := ipproto.Proto(frag[0])
|
||||
fragOfs := binary.BigEndian.Uint16(frag[2:4]) >> 3
|
||||
|
||||
// Step over the fragment header. The real sub-protocol (first fragment)
|
||||
// or the continued payload (later fragments) begins here.
|
||||
q.subofs += ip6FragHeaderLength
|
||||
|
||||
if fragOfs == 0 {
|
||||
// First fragment: decode the real sub-protocol's header so the
|
||||
// filter can match on its ports. The switch in decode6 performs
|
||||
// the per-protocol bounds checks, including rejecting a first
|
||||
// fragment too short to hold the transport header.
|
||||
q.IPProto = nextHdr
|
||||
return true
|
||||
}
|
||||
|
||||
// Later fragment: there's no sub-protocol header to read. Reject offsets
|
||||
// small enough to overlap the transport header (RFC 1858, same guard as
|
||||
// decode4); otherwise pass it through as a fragment.
|
||||
if fragOfs < minFragBlks {
|
||||
q.IPProto = unknown
|
||||
return false
|
||||
}
|
||||
q.IPProto = ipproto.Fragment
|
||||
return false
|
||||
}
|
||||
|
||||
func (q *Parsed) IP4Header() IP4Header {
|
||||
if q.IPVersion != 4 {
|
||||
panic("IP4Header called on non-IPv4 Parsed")
|
||||
|
||||
Reference in New Issue
Block a user