diff options
| author | Naman Sood <mail@nsood.in> | 2021-03-29 14:28:08 -0400 |
|---|---|---|
| committer | Naman Sood <mail@nsood.in> | 2021-03-29 14:28:08 -0400 |
| commit | c0a88a0129ebf0f9886b93b1f4e4f04a7c3bb86f (patch) | |
| tree | 57d5aef2985e3424e5bb6f4c810628aa3ccbf5d0 /net/packet | |
| parent | 47bd3c4cf5543fd7ecb049302c37c1001fa9f2d6 (diff) | |
| parent | a4c679e64691a3f0ba41ad9078312ca67e5e67fd (diff) | |
| download | tailscale-naman/netstack-subnet-routing.tar.xz tailscale-naman/netstack-subnet-routing.zip | |
merge with mainnaman/netstack-subnet-routing
Signed-off-by: Naman Sood <mail@nsood.in>
Diffstat (limited to 'net/packet')
| -rw-r--r-- | net/packet/header.go | 1 | ||||
| -rw-r--r-- | net/packet/icmp4.go | 8 | ||||
| -rw-r--r-- | net/packet/ip.go | 66 | ||||
| -rw-r--r-- | net/packet/ip4.go | 3 | ||||
| -rw-r--r-- | net/packet/ip6.go | 3 | ||||
| -rw-r--r-- | net/packet/packet.go | 104 | ||||
| -rw-r--r-- | net/packet/packet_test.go | 48 | ||||
| -rw-r--r-- | net/packet/tsmp.go | 72 | ||||
| -rw-r--r-- | net/packet/udp4.go | 8 | ||||
| -rw-r--r-- | net/packet/udp6.go | 8 |
10 files changed, 206 insertions, 115 deletions
diff --git a/net/packet/header.go b/net/packet/header.go index 86680a5a7..5cf4ef650 100644 --- a/net/packet/header.go +++ b/net/packet/header.go @@ -10,6 +10,7 @@ import ( ) const tcpHeaderLength = 20 +const sctpHeaderLength = 12 // maxPacketLength is the largest length that all headers support. // IPv4 headers using uint16 for this forces an upper bound of 64KB. diff --git a/net/packet/icmp4.go b/net/packet/icmp4.go index 8a1568114..da4774887 100644 --- a/net/packet/icmp4.go +++ b/net/packet/icmp4.go @@ -4,7 +4,11 @@ package packet -import "encoding/binary" +import ( + "encoding/binary" + + "tailscale.com/types/ipproto" +) // icmp4HeaderLength is the size of the ICMPv4 packet header, not // including the outer IP layer or the variable "response data" @@ -66,7 +70,7 @@ func (h ICMP4Header) Marshal(buf []byte) error { return errLargePacket } // The caller does not need to set this. - h.IPProto = ICMPv4 + h.IPProto = ipproto.ICMPv4 buf[20] = uint8(h.Type) buf[21] = uint8(h.Code) diff --git a/net/packet/ip.go b/net/packet/ip.go deleted file mode 100644 index 34194f344..000000000 --- a/net/packet/ip.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package packet - -// IPProto is an IP subprotocol as defined by the IANA protocol -// numbers list -// (https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml), -// or the special values Unknown or Fragment. -type IPProto uint8 - -const ( - // Unknown represents an unknown or unsupported protocol; it's - // deliberately the zero value. Strictly speaking the zero - // value is IPv6 hop-by-hop extensions, but we don't support - // those, so this is still technically correct. - Unknown IPProto = 0x00 - - // Values from the IANA registry. - ICMPv4 IPProto = 0x01 - IGMP IPProto = 0x02 - ICMPv6 IPProto = 0x3a - TCP IPProto = 0x06 - UDP IPProto = 0x11 - - // TSMP is the Tailscale Message Protocol (our ICMP-ish - // thing), an IP protocol used only between Tailscale nodes - // (still encrypted by WireGuard) that communicates why things - // failed, etc. - // - // Proto number 99 is reserved for "any private encryption - // scheme". We never accept these from the host OS stack nor - // send them to the host network stack. It's only used between - // nodes. - TSMP IPProto = 99 - - // Fragment represents any non-first IP fragment, for which we - // don't have the sub-protocol header (and therefore can't - // figure out what the sub-protocol is). - // - // 0xFF is reserved in the IANA registry, so we steal it for - // internal use. - Fragment IPProto = 0xFF -) - -func (p IPProto) String() string { - switch p { - case Fragment: - return "Frag" - case ICMPv4: - return "ICMPv4" - case IGMP: - return "IGMP" - case ICMPv6: - return "ICMPv6" - case UDP: - return "UDP" - case TCP: - return "TCP" - case TSMP: - return "TSMP" - default: - return "Unknown" - } -} diff --git a/net/packet/ip4.go b/net/packet/ip4.go index 0240abaa1..2c090d9f1 100644 --- a/net/packet/ip4.go +++ b/net/packet/ip4.go @@ -9,6 +9,7 @@ import ( "errors" "inet.af/netaddr" + "tailscale.com/types/ipproto" ) // ip4HeaderLength is the length of an IPv4 header with no IP options. @@ -16,7 +17,7 @@ const ip4HeaderLength = 20 // IP4Header represents an IPv4 packet header. type IP4Header struct { - IPProto IPProto + IPProto ipproto.Proto IPID uint16 Src netaddr.IP Dst netaddr.IP diff --git a/net/packet/ip6.go b/net/packet/ip6.go index 59f605b32..e181f1dde 100644 --- a/net/packet/ip6.go +++ b/net/packet/ip6.go @@ -8,6 +8,7 @@ import ( "encoding/binary" "inet.af/netaddr" + "tailscale.com/types/ipproto" ) // ip6HeaderLength is the length of an IPv6 header with no IP options. @@ -15,7 +16,7 @@ const ip6HeaderLength = 40 // IP6Header represents an IPv6 packet header. type IP6Header struct { - IPProto IPProto + IPProto ipproto.Proto IPID uint32 // only lower 20 bits used Src netaddr.IP Dst netaddr.IP diff --git a/net/packet/packet.go b/net/packet/packet.go index a88a1af7a..05c4a382f 100644 --- a/net/packet/packet.go +++ b/net/packet/packet.go @@ -11,9 +11,12 @@ import ( "strings" "inet.af/netaddr" + "tailscale.com/types/ipproto" "tailscale.com/types/strbuilder" ) +const unknown = ipproto.Unknown + // RFC1858: prevent overlapping fragment attacks. const minFrag = 60 + 20 // max IPv4 header + basic TCP header @@ -44,7 +47,7 @@ type Parsed struct { // 6), or 0 if the packet doesn't look like IPv4 or IPv6. IPVersion uint8 // IPProto is the IP subprotocol (UDP, TCP, etc.). Valid iff IPVersion != 0. - IPProto IPProto + IPProto ipproto.Proto // SrcIP4 is the source address. Family matches IPVersion. Port is // valid iff IPProto == TCP || IPProto == UDP. Src netaddr.IPPort @@ -100,7 +103,7 @@ func (q *Parsed) Decode(b []byte) { if len(b) < 1 { q.IPVersion = 0 - q.IPProto = Unknown + q.IPProto = unknown return } @@ -112,7 +115,7 @@ func (q *Parsed) Decode(b []byte) { q.decode6(b) default: q.IPVersion = 0 - q.IPProto = Unknown + q.IPProto = unknown } } @@ -125,16 +128,16 @@ func (q *Parsed) StuffForTesting(len int) { func (q *Parsed) decode4(b []byte) { if len(b) < ip4HeaderLength { q.IPVersion = 0 - q.IPProto = Unknown + q.IPProto = unknown return } // Check that it's IPv4. - q.IPProto = IPProto(b[9]) + q.IPProto = ipproto.Proto(b[9]) q.length = int(binary.BigEndian.Uint16(b[2:4])) if len(b) < q.length { // Packet was cut off before full IPv4 length. - q.IPProto = Unknown + q.IPProto = unknown return } @@ -145,7 +148,7 @@ func (q *Parsed) decode4(b []byte) { q.subofs = int((b[0] & 0x0F) << 2) if q.subofs > q.length { // next-proto starts beyond end of packet. - q.IPProto = Unknown + q.IPProto = unknown return } sub := b[q.subofs:] @@ -170,29 +173,29 @@ func (q *Parsed) decode4(b []byte) { // This is the first fragment if moreFrags && len(sub) < minFrag { // Suspiciously short first fragment, dump it. - q.IPProto = Unknown + q.IPProto = unknown return } // otherwise, this is either non-fragmented (the usual case) // or a big enough initial fragment that we can read the // whole subprotocol header. switch q.IPProto { - case ICMPv4: + case ipproto.ICMPv4: if len(sub) < icmp4HeaderLength { - q.IPProto = Unknown + q.IPProto = unknown return } q.Src.Port = 0 q.Dst.Port = 0 q.dataofs = q.subofs + icmp4HeaderLength return - case IGMP: + case ipproto.IGMP: // Keep IPProto, but don't parse anything else // out. return - case TCP: + case ipproto.TCP: if len(sub) < tcpHeaderLength { - q.IPProto = Unknown + q.IPProto = unknown return } q.Src.Port = binary.BigEndian.Uint16(sub[0:2]) @@ -201,21 +204,29 @@ func (q *Parsed) decode4(b []byte) { headerLength := (sub[12] & 0xF0) >> 2 q.dataofs = q.subofs + int(headerLength) return - case UDP: + case ipproto.UDP: if len(sub) < udpHeaderLength { - q.IPProto = Unknown + q.IPProto = unknown return } q.Src.Port = binary.BigEndian.Uint16(sub[0:2]) q.Dst.Port = binary.BigEndian.Uint16(sub[2:4]) q.dataofs = q.subofs + udpHeaderLength return - case TSMP: + case ipproto.SCTP: + if len(sub) < sctpHeaderLength { + q.IPProto = unknown + return + } + q.Src.Port = binary.BigEndian.Uint16(sub[0:2]) + q.Dst.Port = binary.BigEndian.Uint16(sub[2:4]) + return + case ipproto.TSMP: // Inter-tailscale messages. q.dataofs = q.subofs return default: - q.IPProto = Unknown + q.IPProto = unknown return } } else { @@ -223,7 +234,7 @@ func (q *Parsed) decode4(b []byte) { if fragOfs < minFrag { // First frag was suspiciously short, so we can't // trust the followup either. - q.IPProto = Unknown + q.IPProto = unknown return } // otherwise, we have to permit the fragment to slide through. @@ -232,7 +243,7 @@ func (q *Parsed) decode4(b []byte) { // but that would require statefulness. Anyway, receivers' // kernels know to drop fragments where the initial fragment // doesn't arrive. - q.IPProto = Fragment + q.IPProto = ipproto.Fragment return } } @@ -240,15 +251,15 @@ func (q *Parsed) decode4(b []byte) { func (q *Parsed) decode6(b []byte) { if len(b) < ip6HeaderLength { q.IPVersion = 0 - q.IPProto = Unknown + q.IPProto = unknown return } - q.IPProto = IPProto(b[6]) + q.IPProto = ipproto.Proto(b[6]) q.length = int(binary.BigEndian.Uint16(b[4:6])) + ip6HeaderLength if len(b) < q.length { // Packet was cut off before the full IPv6 length. - q.IPProto = Unknown + q.IPProto = unknown return } @@ -274,17 +285,17 @@ func (q *Parsed) decode6(b []byte) { sub = sub[:len(sub):len(sub)] // help the compiler do bounds check elimination switch q.IPProto { - case ICMPv6: + case ipproto.ICMPv6: if len(sub) < icmp6HeaderLength { - q.IPProto = Unknown + q.IPProto = unknown return } q.Src.Port = 0 q.Dst.Port = 0 q.dataofs = q.subofs + icmp6HeaderLength - case TCP: + case ipproto.TCP: if len(sub) < tcpHeaderLength { - q.IPProto = Unknown + q.IPProto = unknown return } q.Src.Port = binary.BigEndian.Uint16(sub[0:2]) @@ -293,20 +304,28 @@ func (q *Parsed) decode6(b []byte) { headerLength := (sub[12] & 0xF0) >> 2 q.dataofs = q.subofs + int(headerLength) return - case UDP: + case ipproto.UDP: if len(sub) < udpHeaderLength { - q.IPProto = Unknown + q.IPProto = unknown return } q.Src.Port = binary.BigEndian.Uint16(sub[0:2]) q.Dst.Port = binary.BigEndian.Uint16(sub[2:4]) q.dataofs = q.subofs + udpHeaderLength - case TSMP: + case ipproto.SCTP: + if len(sub) < sctpHeaderLength { + q.IPProto = unknown + return + } + q.Src.Port = binary.BigEndian.Uint16(sub[0:2]) + q.Dst.Port = binary.BigEndian.Uint16(sub[2:4]) + return + case ipproto.TSMP: // Inter-tailscale messages. q.dataofs = q.subofs return default: - q.IPProto = Unknown + q.IPProto = unknown return } } @@ -324,6 +343,19 @@ func (q *Parsed) IP4Header() IP4Header { } } +func (q *Parsed) IP6Header() IP6Header { + if q.IPVersion != 6 { + panic("IP6Header called on non-IPv6 Parsed") + } + ipid := (binary.BigEndian.Uint32(q.b[:4]) << 12) >> 12 + return IP6Header{ + IPID: ipid, + IPProto: q.IPProto, + Src: q.Src.IP, + Dst: q.Dst.IP, + } +} + func (q *Parsed) ICMP4Header() ICMP4Header { if q.IPVersion != 4 { panic("IP4Header called on non-IPv4 Parsed") @@ -367,13 +399,13 @@ func (q *Parsed) IsTCPSyn() bool { // IsError reports whether q is an ICMP "Error" packet. func (q *Parsed) IsError() bool { switch q.IPProto { - case ICMPv4: + case ipproto.ICMPv4: if len(q.b) < q.subofs+8 { return false } t := ICMP4Type(q.b[q.subofs]) return t == ICMP4Unreachable || t == ICMP4TimeExceeded - case ICMPv6: + case ipproto.ICMPv6: if len(q.b) < q.subofs+8 { return false } @@ -387,9 +419,9 @@ func (q *Parsed) IsError() bool { // IsEchoRequest reports whether q is an ICMP Echo Request. func (q *Parsed) IsEchoRequest() bool { switch q.IPProto { - case ICMPv4: + case ipproto.ICMPv4: return len(q.b) >= q.subofs+8 && ICMP4Type(q.b[q.subofs]) == ICMP4EchoRequest && ICMP4Code(q.b[q.subofs+1]) == ICMP4NoCode - case ICMPv6: + case ipproto.ICMPv6: return len(q.b) >= q.subofs+8 && ICMP6Type(q.b[q.subofs]) == ICMP6EchoRequest && ICMP6Code(q.b[q.subofs+1]) == ICMP6NoCode default: return false @@ -399,9 +431,9 @@ func (q *Parsed) IsEchoRequest() bool { // IsEchoRequest reports whether q is an IPv4 ICMP Echo Response. func (q *Parsed) IsEchoResponse() bool { switch q.IPProto { - case ICMPv4: + case ipproto.ICMPv4: return len(q.b) >= q.subofs+8 && ICMP4Type(q.b[q.subofs]) == ICMP4EchoReply && ICMP4Code(q.b[q.subofs+1]) == ICMP4NoCode - case ICMPv6: + case ipproto.ICMPv6: return len(q.b) >= q.subofs+8 && ICMP6Type(q.b[q.subofs]) == ICMP6EchoReply && ICMP6Code(q.b[q.subofs+1]) == ICMP6NoCode default: return false diff --git a/net/packet/packet_test.go b/net/packet/packet_test.go index 8bac5db4a..ac4fa33f3 100644 --- a/net/packet/packet_test.go +++ b/net/packet/packet_test.go @@ -10,6 +10,19 @@ import ( "testing" "inet.af/netaddr" + "tailscale.com/types/ipproto" +) + +const ( + Unknown = ipproto.Unknown + TCP = ipproto.TCP + UDP = ipproto.UDP + SCTP = ipproto.SCTP + IGMP = ipproto.IGMP + ICMPv4 = ipproto.ICMPv4 + ICMPv6 = ipproto.ICMPv6 + TSMP = ipproto.TSMP + Fragment = ipproto.Fragment ) func mustIPPort(s string) netaddr.IPPort { @@ -305,6 +318,39 @@ var ipv4TSMPDecode = Parsed{ Dst: mustIPPort("100.74.70.3:0"), } +// IPv4 SCTP +var sctpBuffer = []byte{ + // IPv4 header: + 0x45, 0x00, + 0x00, 0x20, // 20 + 12 bytes total + 0x00, 0x00, // ID + 0x00, 0x00, // Fragment + 0x40, // TTL + byte(SCTP), + // Checksum, unchecked: + 1, 2, + // source IP: + 0x64, 0x5e, 0x0c, 0x0e, + // dest IP: + 0x64, 0x4a, 0x46, 0x03, + // Src Port, Dest Port: + 0x00, 0x7b, 0x01, 0xc8, + // Verification tag: + 1, 2, 3, 4, + // Checksum: (unchecked) + 5, 6, 7, 8, +} + +var sctpDecode = Parsed{ + b: sctpBuffer, + subofs: 20, + length: 20 + 12, + IPVersion: 4, + IPProto: SCTP, + Src: mustIPPort("100.94.12.14:123"), + Dst: mustIPPort("100.74.70.3:456"), +} + func TestParsedString(t *testing.T) { tests := []struct { name string @@ -320,6 +366,7 @@ func TestParsedString(t *testing.T) { {"igmp", igmpPacketDecode, "IGMP{192.168.1.82:0 > 224.0.0.251:0}"}, {"unknown", unknownPacketDecode, "Unknown{???}"}, {"ipv4_tsmp", ipv4TSMPDecode, "TSMP{100.94.12.14:0 > 100.74.70.3:0}"}, + {"sctp", sctpDecode, "SCTP{100.94.12.14:123 > 100.74.70.3:456}"}, } for _, tt := range tests { @@ -357,6 +404,7 @@ func TestDecode(t *testing.T) { {"unknown", unknownPacketBuffer, unknownPacketDecode}, {"invalid4", invalid4RequestBuffer, invalid4RequestDecode}, {"ipv4_tsmp", ipv4TSMPBuffer, ipv4TSMPDecode}, + {"ipv4_sctp", sctpBuffer, sctpDecode}, } for _, tt := range tests { diff --git a/net/packet/tsmp.go b/net/packet/tsmp.go index 2346c9419..fb257556c 100644 --- a/net/packet/tsmp.go +++ b/net/packet/tsmp.go @@ -17,6 +17,7 @@ import ( "inet.af/netaddr" "tailscale.com/net/flowtrack" + "tailscale.com/types/ipproto" ) // TailscaleRejectedHeader is a TSMP message that says that one @@ -39,7 +40,7 @@ type TailscaleRejectedHeader struct { IPDst netaddr.IP // IPv4 or IPv6 header's dst IP Src netaddr.IPPort // rejected flow's src Dst netaddr.IPPort // rejected flow's dst - Proto IPProto // proto that was rejected (TCP or UDP) + Proto ipproto.Proto // proto that was rejected (TCP or UDP) Reason TailscaleRejectReason // why the connection was rejected // MaybeBroken is whether the rejection is non-terminal (the @@ -57,7 +58,7 @@ type TailscaleRejectedHeader struct { const rejectFlagBitMaybeBroken = 0x1 func (rh TailscaleRejectedHeader) Flow() flowtrack.Tuple { - return flowtrack.Tuple{Src: rh.Src, Dst: rh.Dst} + return flowtrack.Tuple{Proto: rh.Proto, Src: rh.Src, Dst: rh.Dst} } func (rh TailscaleRejectedHeader) String() string { @@ -69,6 +70,12 @@ type TSMPType uint8 const ( // TSMPTypeRejectedConn is the type byte for a TailscaleRejectedHeader. TSMPTypeRejectedConn TSMPType = '!' + + // TSMPTypePing is the type byte for a TailscalePingRequest. + TSMPTypePing TSMPType = 'p' + + // TSMPTypePong is the type byte for a TailscalePongResponse. + TSMPTypePong TSMPType = 'o' ) type TailscaleRejectReason byte @@ -138,7 +145,7 @@ func (h TailscaleRejectedHeader) Marshal(buf []byte) error { } if h.Src.IP.Is4() { iph := IP4Header{ - IPProto: TSMP, + IPProto: ipproto.TSMP, Src: h.IPSrc, Dst: h.IPDst, } @@ -146,7 +153,7 @@ func (h TailscaleRejectedHeader) Marshal(buf []byte) error { buf = buf[ip4HeaderLength:] } else if h.Src.IP.Is6() { iph := IP6Header{ - IPProto: TSMP, + IPProto: ipproto.TSMP, Src: h.IPSrc, Dst: h.IPDst, } @@ -181,7 +188,7 @@ func (pp *Parsed) AsTailscaleRejectedHeader() (h TailscaleRejectedHeader, ok boo return } h = TailscaleRejectedHeader{ - Proto: IPProto(p[1]), + Proto: ipproto.Proto(p[1]), Reason: TailscaleRejectReason(p[2]), IPSrc: pp.Src.IP, IPDst: pp.Dst.IP, @@ -194,3 +201,58 @@ func (pp *Parsed) AsTailscaleRejectedHeader() (h TailscaleRejectedHeader, ok boo } return h, true } + +// TSMPPingRequest is a TSMP message that's like an ICMP ping request. +// +// On the wire, after the IP header, it's currently 9 bytes: +// * 'p' (TSMPTypePing) +// * 8 opaque ping bytes to copy back in the response +type TSMPPingRequest struct { + Data [8]byte +} + +func (pp *Parsed) AsTSMPPing() (h TSMPPingRequest, ok bool) { + if pp.IPProto != ipproto.TSMP { + return + } + p := pp.Payload() + if len(p) < 9 || p[0] != byte(TSMPTypePing) { + return + } + copy(h.Data[:], p[1:]) + return h, true +} + +type TSMPPongReply struct { + IPHeader Header + Data [8]byte +} + +func (pp *Parsed) AsTSMPPong() (data [8]byte, ok bool) { + if pp.IPProto != ipproto.TSMP { + return + } + p := pp.Payload() + if len(p) < 9 || p[0] != byte(TSMPTypePong) { + return + } + copy(data[:], p[1:]) + return data, true +} + +func (h TSMPPongReply) Len() int { + return h.IPHeader.Len() + 9 +} + +func (h TSMPPongReply) Marshal(buf []byte) error { + if len(buf) < h.Len() { + return errSmallBuffer + } + if err := h.IPHeader.Marshal(buf); err != nil { + return err + } + buf = buf[h.IPHeader.Len():] + buf[0] = byte(TSMPTypePong) + copy(buf[1:], h.Data[:]) + return nil +} diff --git a/net/packet/udp4.go b/net/packet/udp4.go index 82aa30179..ce179f89d 100644 --- a/net/packet/udp4.go +++ b/net/packet/udp4.go @@ -4,7 +4,11 @@ package packet -import "encoding/binary" +import ( + "encoding/binary" + + "tailscale.com/types/ipproto" +) // udpHeaderLength is the size of the UDP packet header, not including // the outer IP header. @@ -31,7 +35,7 @@ func (h UDP4Header) Marshal(buf []byte) error { return errLargePacket } // The caller does not need to set this. - h.IPProto = UDP + h.IPProto = ipproto.UDP length := len(buf) - h.IP4Header.Len() binary.BigEndian.PutUint16(buf[20:22], h.SrcPort) diff --git a/net/packet/udp6.go b/net/packet/udp6.go index 0450eae9e..18213c1fb 100644 --- a/net/packet/udp6.go +++ b/net/packet/udp6.go @@ -4,7 +4,11 @@ package packet -import "encoding/binary" +import ( + "encoding/binary" + + "tailscale.com/types/ipproto" +) // UDP6Header is an IPv6+UDP header. type UDP6Header struct { @@ -27,7 +31,7 @@ func (h UDP6Header) Marshal(buf []byte) error { return errLargePacket } // The caller does not need to set this. - h.IPProto = UDP + h.IPProto = ipproto.UDP length := len(buf) - h.IP6Header.Len() binary.BigEndian.PutUint16(buf[40:42], h.SrcPort) |
