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")
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -254,6 +254,8 @@ func TestNoAllocs(t *testing.T) {
|
||||
{"udp6_in", in, udp6Packet},
|
||||
{"udp4_out", out, udp4Packet},
|
||||
{"udp6_out", out, udp6Packet},
|
||||
{"frag6_first_in", in, udp6FirstFragment},
|
||||
{"frag6_nonfirst_in", in, udp6NonFirstFragment},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -377,6 +379,41 @@ func BenchmarkFilter(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
// udp6FirstFragment is the first fragment (offset 0) of a source-fragmented
|
||||
// IPv6 UDP datagram from 2001::5 to [2001::1]:443. decode6 reads its ports past
|
||||
// the 8-byte Fragment extension header so the filter can match it like an
|
||||
// unfragmented packet.
|
||||
var udp6FirstFragment = []byte{
|
||||
// IPv6 header. Next header = 44 (Fragment), payload len = 24.
|
||||
0x60, 0x00, 0x00, 0x00, 0x00, 0x18, 0x2c, 0x40,
|
||||
// Src: 2001::5
|
||||
0x20, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
|
||||
// Dst: 2001::1
|
||||
0x20, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
|
||||
// Fragment header: NextHeader=UDP, Reserved, Offset=0 + M=1, Identification.
|
||||
0x11, 0x00, 0x00, 0x01, 0xde, 0xad, 0xbe, 0xef,
|
||||
// UDP header: sport 1234, dport 443.
|
||||
0x04, 0xd2, 0x01, 0xbb, 0x00, 0x10, 0x00, 0x00,
|
||||
// Payload.
|
||||
0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61,
|
||||
}
|
||||
|
||||
// udp6NonFirstFragment is a later fragment (nonzero offset, no transport
|
||||
// header) of an IPv6 datagram. decode6 classifies it as ipproto.Fragment,
|
||||
// which pre() passes through regardless of ACL, exactly as for IPv4.
|
||||
var udp6NonFirstFragment = []byte{
|
||||
// IPv6 header. Next header = 44 (Fragment), payload len = 16.
|
||||
0x60, 0x00, 0x00, 0x00, 0x00, 0x10, 0x2c, 0x40,
|
||||
// Src: 2001::5
|
||||
0x20, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
|
||||
// Dst: 2001::1
|
||||
0x20, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
|
||||
// Fragment header: NextHeader=UDP, Reserved, Offset=185 blocks + M=0, Identification.
|
||||
0x11, 0x00, 0x05, 0xc8, 0xde, 0xad, 0xbe, 0xef,
|
||||
// Payload continuation (no transport header).
|
||||
0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61,
|
||||
}
|
||||
|
||||
func TestPreFilter(t *testing.T) {
|
||||
packets := []struct {
|
||||
desc string
|
||||
@@ -389,6 +426,7 @@ func TestPreFilter(t *testing.T) {
|
||||
{"short-junk", Drop, usermetric.ReasonTooShort, raw4default(ipproto.Unknown, 10)},
|
||||
{"long-junk", Drop, usermetric.ReasonUnknownProtocol, raw4default(ipproto.Unknown, 21)},
|
||||
{"fragment", Accept, "", raw4default(ipproto.Fragment, 40)},
|
||||
{"fragment6", Accept, "", udp6NonFirstFragment},
|
||||
{"tcp", noVerdict, "", raw4default(ipproto.TCP, 0)},
|
||||
{"udp", noVerdict, "", raw4default(ipproto.UDP, 0)},
|
||||
{"icmp", noVerdict, "", raw4default(ipproto.ICMPv4, 0)},
|
||||
@@ -404,6 +442,26 @@ func TestPreFilter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunInIPv6FirstFragment checks that the first fragment of a
|
||||
// source-fragmented IPv6 datagram is matched on its ports and accepted like an
|
||||
// unfragmented packet, rather than being dropped as an unknown protocol (the
|
||||
// bug where v6 fragments were silently counted as "acl" drops).
|
||||
func TestRunInIPv6FirstFragment(t *testing.T) {
|
||||
f := newFilter(t.Logf)
|
||||
|
||||
var p packet.Parsed
|
||||
p.Decode(udp6FirstFragment)
|
||||
// The fragment header must be parsed through to the real sub-protocol;
|
||||
// otherwise no ACL rule can match it.
|
||||
if p.IPProto != ipproto.UDP {
|
||||
t.Fatalf("decoded IPProto = %v, want UDP (fragment header not parsed)", p.IPProto)
|
||||
}
|
||||
// 2001::5 => [2001::1]:443 is permitted by the "::/0 => ::/0:443" rule.
|
||||
if got := f.RunIn(&p, 0); got != Accept {
|
||||
t.Errorf("RunIn(first fragment) = %v, want Accept", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOmitDropLogging(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user