This commit is contained in:
bluepython508 committed 2026-10-22 12:27:19 +01:00
1 parent 3530afb854
commit 6903fcff27
2146 files changed
+112256 -190703

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package packet
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package checksum provides functions for updating checksums in parsed packets.
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package packet contains packet parsing and marshaling utilities.
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package packet
+2 -2
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package packet
@@ -45,7 +45,7 @@ type HeaderChecksummer interface {
Header
// WriteCheck writes the correct checksum into buf, which should
// be be the already-marshalled header and payload.
// be the already-marshalled header and payload.
WriteChecksum(buf []byte)
}
+63 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package packet
@@ -7,6 +7,7 @@ import (
crand "crypto/rand"
"encoding/binary"
"net/netip"
)
// ICMPEchoPayload generates a new random ID/Sequence pair, and returns a uint32
@@ -26,3 +27,64 @@ func ICMPEchoPayload(payload []byte) (idSeq uint32, buf []byte) {
return
}
// icmpDestUnreachableUnusedLen is the number of unused bytes that both ICMPv4
// and ICMPv6 "Destination Unreachable" messages have between header and bits
// from the invoking packet.
const icmpDestUnreachableUnusedLen = 4
const minIPv6MTU = 1280 // RFC 2460, section 5
// GenerateICMPHostUnreachable builds an ICMPv4 or ICMPv6 "Destination
// Unreachable" message according to RFC 792 and RFC 4443, section 3.1.
// from and to must both be of the same address family; otherwise
// GenerateICMPHostUnreachable returns nil.
func GenerateICMPHostUnreachable(from, to netip.Addr, invoking *Parsed) []byte {
buf := invoking.Buffer()
switch {
case from.Is4() && to.Is4():
// RFC 792
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Type | Code | Checksum |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | unused |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Internet Header + 64 bits of Original Data Datagram |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
ipHeaderLen := len(buf) - len(invoking.Transport())
return Generate(ICMP4Header{
IP4Header: IP4Header{Src: from, Dst: to},
Type: ICMP4Unreachable,
Code: ICMP4HostUnreachable,
}, icmpDestUnreachablePayload(buf, ipHeaderLen+8))
case from.Is6() && to.Is6():
// RFC 4443, section 3.1
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Type | Code | Checksum |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Unused |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | As much of invoking packet |
// + as possible without the ICMPv6 packet +
// | exceeding the minimum IPv6 MTU |
maxOrig := minIPv6MTU - (ICMP6Header{}).Len() - icmpDestUnreachableUnusedLen
return Generate(ICMP6Header{
IP6Header: IP6Header{Src: from, Dst: to},
Type: ICMP6Unreachable,
Code: ICMP6AddressUnreachable,
}, icmpDestUnreachablePayload(buf, maxOrig))
default:
return nil
}
}
// icmpDestUnreachablePayload composes the payload for an ICMP "Destination Unreachable" packet.
func icmpDestUnreachablePayload(orig []byte, maxOrig int) []byte {
n := min(len(orig), maxOrig)
payload := make([]byte, icmpDestUnreachableUnusedLen+n)
copy(payload[icmpDestUnreachableUnusedLen:], orig[:n])
return payload
}
+5 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package packet
@@ -49,6 +49,10 @@ type ICMP4Code uint8
const (
ICMP4NoCode ICMP4Code = 0
// ICMP4HostUnreachable is the code used with ICMP4Unreachable to
// indicate that the destination host could not be reached.
ICMP4HostUnreachable ICMP4Code = 1
)
// ICMP4Header is an IPv4+ICMPv4 header.
+6 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package packet
@@ -52,6 +52,11 @@ type ICMP6Code uint8
const (
ICMP6NoCode ICMP6Code = 0
// ICMP6AddressUnreachable is the code used with ICMP6Unreachable to
// indicate that the destination address could not be reached. It is
// the IPv6 equivalent of ICMP4HostUnreachable.
ICMP6AddressUnreachable ICMP6Code = 3
)
// ICMP6Header is an IPv4+ICMPv4 header.
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package packet
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package packet
+89 -15
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package packet
@@ -17,8 +17,23 @@ import (
const unknown = ipproto.Unknown
// RFC1858: prevent overlapping fragment attacks.
//
// The bound is sized for IPv4 (max IPv4 header + basic TCP header) but is
// intentionally reused by decode6 for IPv6 fragments. It is conservative for
// IPv6, whose fragments carry no per-fragment IP header, so it only ever
// rejects more later fragments as Unknown, never fewer.
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 (
@@ -134,12 +149,15 @@ func (q *Parsed) decode4(b []byte) {
q.Src = withIP(q.Src, netaddr.IPv4(b[12], b[13], b[14], b[15]))
q.Dst = withIP(q.Dst, netaddr.IPv4(b[16], b[17], b[18], b[19]))
q.subofs = int((b[0] & 0x0F) << 2)
if q.subofs > q.length {
// subofs is only committed to q after validation, so a rejected
// packet can't leave it pointing past the end of the buffer.
subofs := int((b[0] & 0x0F) << 2)
if subofs > q.length {
// next-proto starts beyond end of packet.
q.IPProto = unknown
return
}
q.subofs = subofs
sub := b[q.subofs:]
sub = sub[:len(sub):len(sub)] // help the compiler do bounds check elimination
@@ -272,19 +290,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 +365,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")
@@ -401,7 +468,10 @@ func (q *Parsed) Buffer() []byte {
// This is a read-only view; that is, q retains the ownership of the buffer.
func (q *Parsed) Payload() []byte {
// If the packet is truncated, return nothing instead of crashing.
if q.length > len(q.b) || q.dataofs > len(q.b) {
// dataofs can also land past length rather than past len(b): the IP
// header can declare a total length that stops before the end of the
// sub-protocol header, which would invert the slice bounds below.
if q.length > len(q.b) || q.dataofs > len(q.b) || q.dataofs > q.length {
return nil
}
@@ -411,6 +481,10 @@ func (q *Parsed) Payload() []byte {
// Transport returns the transport header and payload (IP subprotocol, such as TCP or UDP).
// This is a read-only view; that is, p retains the ownership of the buffer.
func (p *Parsed) Transport() []byte {
if p.subofs > len(p.b) {
// defensive check, should not survive decode4 or decode6 (first line)
return nil
}
return p.b[p.subofs:]
}
+22 -8
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// TSMP is our ICMP-like "Tailscale Message Protocol" for signaling
@@ -29,7 +29,7 @@ const minTSMPSize = 7 // the rejected body is 7 bytes
// On the wire, after the IP header, it's currently 7 or 8 bytes:
// - '!'
// - IPProto byte (IANA protocol number: TCP or UDP)
// - 'A' or 'S' (RejectedDueToACLs, RejectedDueToShieldsUp)
// - byte stating rejection reason (see [TailscaleRejectReason] for valid values)
// - srcPort big endian uint16
// - dstPort big endian uint16
// - [optional] byte of flag bits:
@@ -63,19 +63,20 @@ func (rh TailscaleRejectedHeader) String() string {
return fmt.Sprintf("TSMP-reject-flow{%s %s > %s}: %s", rh.Proto, rh.Src, rh.Dst, rh.Reason)
}
// TSMPType is the type byte of a TSMP message. It follows the IPv4 or IPv6 header.
type TSMPType uint8
const (
// TSMPTypeRejectedConn is the type byte for a TailscaleRejectedHeader.
// TSMPTypeRejectedConn is the type byte for a [TailscaleRejectedHeader].
TSMPTypeRejectedConn TSMPType = '!'
// TSMPTypePing is the type byte for a TailscalePingRequest.
// TSMPTypePing is the type byte for a [TSMPPingRequest].
TSMPTypePing TSMPType = 'p'
// TSMPTypePong is the type byte for a TailscalePongResponse.
// TSMPTypePong is the type byte for a [TSMPPongReply].
TSMPTypePong TSMPType = 'o'
// TSPMTypeDiscoAdvertisement is the type byte for sending disco keys
// TSMPTypeDiscoAdvertisement is the type byte for a [TSMPDiscoKeyAdvertisement].
TSMPTypeDiscoAdvertisement TSMPType = 'a'
)
@@ -101,6 +102,11 @@ const (
// RejectedDueToHostFirewall means that the target host's
// firewall is blocking the traffic.
RejectedDueToHostFirewall TailscaleRejectReason = 'W'
// RejectedDueToUnknownAppConnectorTransitIP means that the connector host has no real IP
// mapping that matches the provided transit IP for this client, so the
// connector has no destination to forward the connection to.
RejectedDueToUnknownAppConnectorTransitIP TailscaleRejectReason = 'T'
)
func (r TailscaleRejectReason) String() string {
@@ -113,6 +119,8 @@ func (r TailscaleRejectReason) String() string {
return "host-ip-forwarding-unavailable"
case RejectedDueToHostFirewall:
return "host-firewall"
case RejectedDueToUnknownAppConnectorTransitIP:
return "app-connector-transit-ip-unknown"
}
return fmt.Sprintf("0x%02x", byte(r))
}
@@ -265,7 +273,9 @@ func (h TSMPPongReply) Marshal(buf []byte) error {
return nil
}
// TSMPDiscoKeyAdvertisement is a TSMP message that's used for distributing Disco Keys.
// TSMPDiscoKeyAdvertisement is a TSMP message that's used for distributing
// [key.DiscoPublic]. It is sent immediately following eligible WireGuard
// session establishment starting at [tailscale.com/tailcfg.CapabilityVersion] 144.
//
// On the wire, after the IP header, it's currently 33 bytes:
// - 'a' (TSMPTypeDiscoAdvertisement)
@@ -275,6 +285,10 @@ type TSMPDiscoKeyAdvertisement struct {
Key key.DiscoPublic
}
// Marshal marshals ka to a wire representation appropriate for transmission
// over WireGuard to a remote client. A marshaled [TSMPDiscoKeyAdvertisement]
// must not exceed [github.com/tailscale/wireguard-go/device.MaxPriorityMessageContentSize],
// or wireguard-go will reject and discard the payload.
func (ka *TSMPDiscoKeyAdvertisement) Marshal() ([]byte, error) {
var iph Header
if ka.Src.Is4() {
@@ -290,7 +304,7 @@ func (ka *TSMPDiscoKeyAdvertisement) Marshal() ([]byte, error) {
Dst: ka.Dst,
}
}
payload := make([]byte, 0, 33)
payload := make([]byte, 0, 1+key.DiscoPublicRawLen)
payload = append(payload, byte(TSMPTypeDiscoAdvertisement))
payload = ka.Key.AppendTo(payload)
if len(payload) != 33 {
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package packet
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package packet