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:
Steve Avery
2026-06-15 11:18:00 -07:00
committed by James Tucker
parent f002f6bb3a
commit 4c4ec3d468
3 changed files with 257 additions and 11 deletions
+129
View File
@@ -232,6 +232,131 @@ var udp6RequestDecode = Parsed{
Dst: mustIPPort("[2607:f8b0:400a:809::200e]:443"),
}
// First fragment of a source-fragmented UDP datagram over IPv6.
//
// The base header's next-header is 44 (IPv6 Fragment extension header),
// which carries the real upper-layer protocol (UDP) and the fragment
// offset (0 here, with the More-Fragments flag set). Like decode4's
// first-fragment handling, decode6 should reach past the 8-byte fragment
// header and parse the transport ports.
var udp6FirstFragmentBuffer = []byte{
// IPv6 header up to hop limit. Next header = 44 (Fragment), payload len = 24.
0x60, 0x00, 0x00, 0x00, 0x00, 0x18, 0x2c, 0x40,
// Src addr
0x20, 0x01, 0x05, 0x59, 0xbc, 0x13, 0x54, 0x00, 0x17, 0x49, 0x46, 0x28, 0x39, 0x34, 0x0e, 0x1b,
// Dst addr
0x26, 0x07, 0xf8, 0xb0, 0x40, 0x0a, 0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x0e,
// Fragment extension header (8 bytes):
// NextHeader=UDP(0x11), Reserved, FragmentOffset=0 + M flag, Identification.
0x11, 0x00, 0x00, 0x01, 0xde, 0xad, 0xbe, 0xef,
// UDP header
0xd4, 0x04, 0x01, 0xbb, 0x00, 0x29, 0x96, 0x84,
// Payload (start of the datagram, carried by the first fragment)
0x5c, 0x06, 0xae, 0x85, 0x02, 0xf5, 0xdb, 0x90,
}
var udp6FirstFragmentDecode = Parsed{
b: udp6FirstFragmentBuffer,
subofs: 48, // 40-byte IPv6 header + 8-byte fragment extension header
dataofs: 56, // subofs + 8-byte UDP header
length: len(udp6FirstFragmentBuffer),
IPVersion: 6,
IPProto: UDP,
Src: mustIPPort("[2001:559:bc13:5400:1749:4628:3934:e1b]:54276"),
Dst: mustIPPort("[2607:f8b0:400a:809::200e]:443"),
}
// A non-first fragment over IPv6: it carries a fragment header at a
// nonzero offset and no transport header, so its ports are unknown. Like
// decode4, decode6 should classify it as ipproto.Fragment so the filter's
// pre() pass-through path accepts it.
var udp6NonFirstFragmentBuffer = []byte{
// IPv6 header up to hop limit. Next header = 44 (Fragment), payload len = 16.
0x60, 0x00, 0x00, 0x00, 0x00, 0x10, 0x2c, 0x40,
// Src addr
0x20, 0x01, 0x05, 0x59, 0xbc, 0x13, 0x54, 0x00, 0x17, 0x49, 0x46, 0x28, 0x39, 0x34, 0x0e, 0x1b,
// Dst addr
0x26, 0x07, 0xf8, 0xb0, 0x40, 0x0a, 0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x0e,
// Fragment extension header (8 bytes):
// NextHeader=UDP(0x11), Reserved, FragmentOffset=185 (0x05c8>>3) + M=0, Identification.
0x11, 0x00, 0x05, 0xc8, 0xde, 0xad, 0xbe, 0xef,
// Payload continuation (no transport header in a non-first fragment)
0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61,
}
var udp6NonFirstFragmentDecode = Parsed{
b: udp6NonFirstFragmentBuffer,
subofs: 48, // past the fragment extension header, at the continued payload
dataofs: 0, // no sub-protocol header present
length: len(udp6NonFirstFragmentBuffer),
IPVersion: 6,
IPProto: Fragment,
Src: mustIPPort("[2001:559:bc13:5400:1749:4628:3934:e1b]:0"),
Dst: mustIPPort("[2607:f8b0:400a:809::200e]:0"),
}
// A first fragment (offset 0) truncated before the full transport header,
// so the ports can't be read. Like decode4's tcp4ShortFragment case, this
// must be rejected as Unknown rather than guessed at: a stateless filter
// that trusted such a fragment could be bypassed by a follow-up fragment
// that supplies the rest of the header (RFC 1858).
var udp6ShortFirstFragmentBuffer = []byte{
// IPv6 header. Next header = 44 (Fragment), payload len = 12.
0x60, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x2c, 0x40,
// Src addr
0x20, 0x01, 0x05, 0x59, 0xbc, 0x13, 0x54, 0x00, 0x17, 0x49, 0x46, 0x28, 0x39, 0x34, 0x0e, 0x1b,
// Dst addr
0x26, 0x07, 0xf8, 0xb0, 0x40, 0x0a, 0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x0e,
// Fragment extension header: NextHeader=UDP, Reserved, Offset=0 + M=1, Identification.
0x11, 0x00, 0x00, 0x01, 0xde, 0xad, 0xbe, 0xef,
// Truncated UDP header: only 4 of the required 8 bytes.
0xd4, 0x04, 0x01, 0xbb,
}
var udp6ShortFirstFragmentDecode = Parsed{
b: udp6ShortFirstFragmentBuffer,
subofs: 48, // header walk reaches past the fragment header...
dataofs: 0, // ...but the transport header is incomplete, so it's unknown
length: len(udp6ShortFirstFragmentBuffer),
IPVersion: 6,
IPProto: Unknown,
Src: mustIPPort("[2001:559:bc13:5400:1749:4628:3934:e1b]:0"),
Dst: mustIPPort("[2607:f8b0:400a:809::200e]:0"),
}
// A non-first fragment whose offset is small enough that its bytes could
// overlap the transport header on reassembly. Mirrors decode4's
// ipv4SmallOffsetFragment: reject as Unknown to prevent overlapping-fragment
// firewall bypass (RFC 1858), rather than passing it through as Fragment.
var udp6SmallOffsetFragmentBuffer = []byte{
// IPv6 header. Next header = 44 (Fragment), payload len = 16.
0x60, 0x00, 0x00, 0x00, 0x00, 0x10, 0x2c, 0x40,
// Src addr
0x20, 0x01, 0x05, 0x59, 0xbc, 0x13, 0x54, 0x00, 0x17, 0x49, 0x46, 0x28, 0x39, 0x34, 0x0e, 0x1b,
// Dst addr
0x26, 0x07, 0xf8, 0xb0, 0x40, 0x0a, 0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x0e,
// Fragment extension header: NextHeader=UDP, Reserved,
// Offset=1 block (8 bytes, below the safe minimum) + M=0, Identification.
0x11, 0x00, 0x00, 0x08, 0xde, 0xad, 0xbe, 0xef,
// Payload continuation that could overlap the transport header.
0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61,
}
var udp6SmallOffsetFragmentDecode = Parsed{
b: udp6SmallOffsetFragmentBuffer,
subofs: 48,
dataofs: 0,
length: len(udp6SmallOffsetFragmentBuffer),
IPVersion: 6,
IPProto: Unknown,
Src: mustIPPort("[2001:559:bc13:5400:1749:4628:3934:e1b]:0"),
Dst: mustIPPort("[2607:f8b0:400a:809::200e]:0"),
}
var udp4ReplyBuffer = []byte{
// IP header up to checksum
0x45, 0x00, 0x00, 0x29, 0x21, 0x52, 0x00, 0x00, 0x40, 0x11, 0x49, 0x5f,
@@ -567,6 +692,10 @@ func TestDecode(t *testing.T) {
{"ipv4_tsmp", ipv4TSMPBuffer, ipv4TSMPDecode},
{"ipv4_sctp", sctpBuffer, sctpDecode},
{"ipv4_frag", tcp4MediumFragmentBuffer, tcp4MediumFragmentDecode},
{"ipv6_frag_first", udp6FirstFragmentBuffer, udp6FirstFragmentDecode},
{"ipv6_frag_nonfirst", udp6NonFirstFragmentBuffer, udp6NonFirstFragmentDecode},
{"ipv6_frag_short_first", udp6ShortFirstFragmentBuffer, udp6ShortFirstFragmentDecode},
{"ipv6_frag_small_offset", udp6SmallOffsetFragmentBuffer, udp6SmallOffsetFragmentDecode},
{"ipv4_fragtooshort", tcp4ShortFragmentBuffer, tcp4ShortFragmentDecode},
{"ipv4_short_first_fragment", ipv4ShortFirstFragmentBuffer, ipv4ShortFirstFragmentDecode},
{"ipv4_small_offset_fragment", ipv4SmallOffsetFragmentBuffer, ipv4SmallOffsetFragmentDecode},