Update dependencies

This commit is contained in:
bluepython508
2024-11-01 17:33:34 +00:00
parent 033ac0b400
commit 5cdfab398d
3596 changed files with 1033483 additions and 259 deletions

238
vendor/tailscale.com/wgengine/capture/capture.go generated vendored Normal file
View File

@@ -0,0 +1,238 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package capture formats packet logging into a debug pcap stream.
package capture
import (
"bytes"
"context"
"encoding/binary"
"io"
"net/http"
"sync"
"time"
_ "embed"
"tailscale.com/net/packet"
"tailscale.com/util/set"
)
//go:embed ts-dissector.lua
var DissectorLua string
// Callback describes a function which is called to
// record packets when debugging packet-capture.
// Such callbacks must not take ownership of the
// provided data slice: it may only copy out of it
// within the lifetime of the function.
type Callback func(Path, time.Time, []byte, packet.CaptureMeta)
var bufferPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
const flushPeriod = 100 * time.Millisecond
func writePcapHeader(w io.Writer) {
binary.Write(w, binary.LittleEndian, uint32(0xA1B2C3D4)) // pcap magic number
binary.Write(w, binary.LittleEndian, uint16(2)) // version major
binary.Write(w, binary.LittleEndian, uint16(4)) // version minor
binary.Write(w, binary.LittleEndian, uint32(0)) // this zone
binary.Write(w, binary.LittleEndian, uint32(0)) // zone significant figures
binary.Write(w, binary.LittleEndian, uint32(65535)) // max packet len
binary.Write(w, binary.LittleEndian, uint32(147)) // link-layer ID - USER0
}
func writePktHeader(w *bytes.Buffer, when time.Time, length int) {
s := when.Unix()
us := when.UnixMicro() - (s * 1000000)
binary.Write(w, binary.LittleEndian, uint32(s)) // timestamp in seconds
binary.Write(w, binary.LittleEndian, uint32(us)) // timestamp microseconds
binary.Write(w, binary.LittleEndian, uint32(length)) // length present
binary.Write(w, binary.LittleEndian, uint32(length)) // total length
}
// Path describes where in the data path the packet was captured.
type Path uint8
// Valid Path values.
const (
// FromLocal indicates the packet was logged as it traversed the FromLocal path:
// i.e.: A packet from the local system into the TUN.
FromLocal Path = 0
// FromPeer indicates the packet was logged upon reception from a remote peer.
FromPeer Path = 1
// SynthesizedToLocal indicates the packet was generated from within tailscaled,
// and is being routed to the local machine's network stack.
SynthesizedToLocal Path = 2
// SynthesizedToPeer indicates the packet was generated from within tailscaled,
// and is being routed to a remote Wireguard peer.
SynthesizedToPeer Path = 3
// PathDisco indicates the packet is information about a disco frame.
PathDisco Path = 254
)
// New creates a new capture sink.
func New() *Sink {
ctx, c := context.WithCancel(context.Background())
return &Sink{
ctx: ctx,
ctxCancel: c,
}
}
// Type Sink handles callbacks with packets to be logged,
// formatting them into a pcap stream which is mirrored to
// all registered outputs.
type Sink struct {
ctx context.Context
ctxCancel context.CancelFunc
mu sync.Mutex
outputs set.HandleSet[io.Writer]
flushTimer *time.Timer // or nil if none running
}
// RegisterOutput connects an output to this sink, which
// will be written to with a pcap stream as packets are logged.
// A function is returned which unregisters the output when
// called.
//
// If w implements io.Closer, it will be closed upon error
// or when the sink is closed. If w implements http.Flusher,
// it will be flushed periodically.
func (s *Sink) RegisterOutput(w io.Writer) (unregister func()) {
select {
case <-s.ctx.Done():
return func() {}
default:
}
writePcapHeader(w)
s.mu.Lock()
hnd := s.outputs.Add(w)
s.mu.Unlock()
return func() {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.outputs, hnd)
}
}
// NumOutputs returns the number of outputs registered with the sink.
func (s *Sink) NumOutputs() int {
s.mu.Lock()
defer s.mu.Unlock()
return len(s.outputs)
}
// Close shuts down the sink. Future calls to LogPacket
// are ignored, and any registered output that implements
// io.Closer is closed.
func (s *Sink) Close() error {
s.ctxCancel()
s.mu.Lock()
defer s.mu.Unlock()
if s.flushTimer != nil {
s.flushTimer.Stop()
s.flushTimer = nil
}
for _, o := range s.outputs {
if o, ok := o.(io.Closer); ok {
o.Close()
}
}
s.outputs = nil
return nil
}
// WaitCh returns a channel which blocks until
// the sink is closed.
func (s *Sink) WaitCh() <-chan struct{} {
return s.ctx.Done()
}
func customDataLen(meta packet.CaptureMeta) int {
length := 4
if meta.DidSNAT {
length += meta.OriginalSrc.Addr().BitLen() / 8
}
if meta.DidDNAT {
length += meta.OriginalDst.Addr().BitLen() / 8
}
return length
}
// LogPacket is called to insert a packet into the capture.
//
// This function does not take ownership of the provided data slice.
func (s *Sink) LogPacket(path Path, when time.Time, data []byte, meta packet.CaptureMeta) {
select {
case <-s.ctx.Done():
return
default:
}
extraLen := customDataLen(meta)
b := bufferPool.Get().(*bytes.Buffer)
b.Reset()
b.Grow(16 + extraLen + len(data)) // 16b pcap header + len(metadata) + len(payload)
defer bufferPool.Put(b)
writePktHeader(b, when, len(data)+extraLen)
// Custom tailscale debugging data
binary.Write(b, binary.LittleEndian, uint16(path))
if meta.DidSNAT {
binary.Write(b, binary.LittleEndian, uint8(meta.OriginalSrc.Addr().BitLen()/8))
b.Write(meta.OriginalSrc.Addr().AsSlice())
} else {
binary.Write(b, binary.LittleEndian, uint8(0)) // SNAT addr len == 0
}
if meta.DidDNAT {
binary.Write(b, binary.LittleEndian, uint8(meta.OriginalDst.Addr().BitLen()/8))
b.Write(meta.OriginalDst.Addr().AsSlice())
} else {
binary.Write(b, binary.LittleEndian, uint8(0)) // DNAT addr len == 0
}
b.Write(data)
s.mu.Lock()
defer s.mu.Unlock()
var hadError []set.Handle
for hnd, o := range s.outputs {
if _, err := o.Write(b.Bytes()); err != nil {
hadError = append(hadError, hnd)
continue
}
}
for _, hnd := range hadError {
if o, ok := s.outputs[hnd].(io.Closer); ok {
o.Close()
}
delete(s.outputs, hnd)
}
if s.flushTimer == nil {
s.flushTimer = time.AfterFunc(flushPeriod, func() {
s.mu.Lock()
defer s.mu.Unlock()
for _, o := range s.outputs {
if f, ok := o.(http.Flusher); ok {
f.Flush()
}
}
s.flushTimer = nil
})
}
}

169
vendor/tailscale.com/wgengine/capture/ts-dissector.lua generated vendored Normal file
View File

@@ -0,0 +1,169 @@
function hasbit(x, p)
return x % (p + p) >= p
end
tsdebug_ll = Proto("tsdebug", "Tailscale debug")
PATH = ProtoField.string("tsdebug.PATH","PATH", base.ASCII)
SNAT_IP_4 = ProtoField.ipv4("tsdebug.SNAT_IP_4", "Pre-NAT Source IPv4 address")
SNAT_IP_6 = ProtoField.ipv6("tsdebug.SNAT_IP_6", "Pre-NAT Source IPv6 address")
DNAT_IP_4 = ProtoField.ipv4("tsdebug.DNAT_IP_4", "Pre-NAT Dest IPv4 address")
DNAT_IP_6 = ProtoField.ipv6("tsdebug.DNAT_IP_6", "Pre-NAT Dest IPv6 address")
tsdebug_ll.fields = {PATH, SNAT_IP_4, SNAT_IP_6, DNAT_IP_4, DNAT_IP_6}
function tsdebug_ll.dissector(buffer, pinfo, tree)
pinfo.cols.protocol = tsdebug_ll.name
packet_length = buffer:len()
local offset = 0
local subtree = tree:add(tsdebug_ll, buffer(), "Tailscale packet")
-- -- Get path UINT16
local path_id = buffer:range(offset, 2):le_uint()
if path_id == 0 then subtree:add(PATH, "FromLocal")
elseif path_id == 1 then subtree:add(PATH, "FromPeer")
elseif path_id == 2 then subtree:add(PATH, "Synthesized (Inbound / ToLocal)")
elseif path_id == 3 then subtree:add(PATH, "Synthesized (Outbound / ToPeer)")
elseif path_id == 254 then subtree:add(PATH, "Disco frame")
end
offset = offset + 2
-- -- Get SNAT address
local snat_addr_len = buffer:range(offset, 1):le_uint()
if snat_addr_len == 4 then subtree:add(SNAT_IP_4, buffer:range(offset + 1, snat_addr_len))
elseif snat_addr_len > 0 then subtree:add(SNAT_IP_6, buffer:range(offset + 1, snat_addr_len))
end
offset = offset + 1 + snat_addr_len
-- -- Get DNAT address
local dnat_addr_len = buffer:range(offset, 1):le_uint()
if dnat_addr_len == 4 then subtree:add(DNAT_IP_4, buffer:range(offset + 1, dnat_addr_len))
elseif dnat_addr_len > 0 then subtree:add(DNAT_IP_6, buffer:range(offset + 1, dnat_addr_len))
end
offset = offset + 1 + dnat_addr_len
-- -- Handover rest of data to lower-level dissector
local data_buffer = buffer:range(offset, packet_length-offset):tvb()
if path_id == 254 then
Dissector.get("tsdisco"):call(data_buffer, pinfo, tree)
else
Dissector.get("ip"):call(data_buffer, pinfo, tree)
end
end
-- Install the dissector on link-layer ID 147 (User-defined protocol 0)
local eth_table = DissectorTable.get("wtap_encap")
eth_table:add(wtap.USER0, tsdebug_ll)
local ts_dissectors = DissectorTable.new("ts.proto", "Tailscale-specific dissectors", ftypes.STRING, base.NONE)
--
-- DISCO metadata dissector
--
tsdisco_meta = Proto("tsdisco", "Tailscale DISCO metadata")
DISCO_IS_DERP = ProtoField.bool("tsdisco.IS_DERP","From DERP")
DISCO_SRC_IP_4 = ProtoField.ipv4("tsdisco.SRC_IP_4", "Source IPv4 address")
DISCO_SRC_IP_6 = ProtoField.ipv6("tsdisco.SRC_IP_6", "Source IPv6 address")
DISCO_SRC_PORT = ProtoField.uint16("tsdisco.SRC_PORT","Source port", base.DEC)
DISCO_DERP_PUB = ProtoField.bytes("tsdisco.DERP_PUB", "DERP public key", base.SPACE)
tsdisco_meta.fields = {DISCO_IS_DERP, DISCO_SRC_PORT, DISCO_DERP_PUB, DISCO_SRC_IP_4, DISCO_SRC_IP_6}
function tsdisco_meta.dissector(buffer, pinfo, tree)
pinfo.cols.protocol = tsdisco_meta.name
packet_length = buffer:len()
local offset = 0
local subtree = tree:add(tsdisco_meta, buffer(), "DISCO metadata")
-- Parse flags
local from_derp = hasbit(buffer(offset, 1):le_uint(), 0)
subtree:add(DISCO_IS_DERP, from_derp) -- Flag bit 0
offset = offset + 1
-- Parse DERP public key
if from_derp then
subtree:add(DISCO_DERP_PUB, buffer(offset, 32))
end
offset = offset + 32
-- Parse source port
subtree:add(DISCO_SRC_PORT, buffer:range(offset, 2):le_uint())
offset = offset + 2
-- Parse source address
local addr_len = buffer:range(offset, 2):le_uint()
offset = offset + 2
if addr_len == 4 then subtree:add(DISCO_SRC_IP_4, buffer:range(offset, addr_len))
else subtree:add(DISCO_SRC_IP_6, buffer:range(offset, addr_len))
end
offset = offset + addr_len
-- Handover to the actual disco frame dissector
offset = offset + 2 -- skip over payload len
local data_buffer = buffer:range(offset, packet_length-offset):tvb()
Dissector.get("disco"):call(data_buffer, pinfo, tree)
end
ts_dissectors:add(1, tsdisco_meta)
--
-- DISCO frame dissector
--
tsdisco_frame = Proto("disco", "Tailscale DISCO frame")
DISCO_TYPE = ProtoField.string("disco.TYPE", "Message type", base.ASCII)
DISCO_VERSION = ProtoField.uint8("disco.VERSION","Protocol version", base.DEC)
DISCO_TXID = ProtoField.bytes("disco.TXID", "Transaction ID", base.SPACE)
DISCO_NODEKEY = ProtoField.bytes("disco.NODE_KEY", "Node key", base.SPACE)
DISCO_PONG_SRC = ProtoField.ipv6("disco.PONG_SRC", "Pong source")
DISCO_PONG_SRC_PORT = ProtoField.uint16("disco.PONG_SRC_PORT","Source port", base.DEC)
DISCO_UNKNOWN = ProtoField.bytes("disco.UNKNOWN_DATA", "Trailing data", base.SPACE)
tsdisco_frame.fields = {DISCO_TYPE, DISCO_VERSION, DISCO_TXID, DISCO_NODEKEY, DISCO_PONG_SRC, DISCO_PONG_SRC_PORT, DISCO_UNKNOWN}
function tsdisco_frame.dissector(buffer, pinfo, tree)
packet_length = buffer:len()
local offset = 0
local subtree = tree:add(tsdisco_frame, buffer(), "DISCO frame")
-- Message type
local message_type = buffer(offset, 1):le_uint()
offset = offset + 1
if message_type == 1 then subtree:add(DISCO_TYPE, "Ping")
elseif message_type == 2 then subtree:add(DISCO_TYPE, "Pong")
elseif message_type == 3 then subtree:add(DISCO_TYPE, "Call me maybe")
end
-- Message version
local message_version = buffer(offset, 1):le_uint()
offset = offset + 1
subtree:add(DISCO_VERSION, message_version)
-- TXID (Ping / Pong)
if message_type == 1 or message_type == 2 then
subtree:add(DISCO_TXID, buffer(offset, 12))
offset = offset + 12
end
-- NodeKey (Ping)
if message_type == 1 then
subtree:add(DISCO_NODEKEY, buffer(offset, 32))
offset = offset + 32
end
-- Src (Pong)
if message_type == 2 then
subtree:add(DISCO_PONG_SRC, buffer:range(offset, 16))
offset = offset + 16
end
-- Src port (Pong)
if message_type == 2 then
subtree:add(DISCO_PONG_SRC_PORT, buffer(offset, 2):le_uint())
offset = offset + 2
end
-- TODO(tom): Parse CallMeMaybe.MyNumber
local trailing = buffer:range(offset, packet_length-offset)
if trailing:len() > 0 then
subtree:add(DISCO_UNKNOWN, trailing)
end
end
ts_dissectors:add(2, tsdisco_frame)

662
vendor/tailscale.com/wgengine/filter/filter.go generated vendored Normal file
View File

@@ -0,0 +1,662 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package filter is a stateful packet filter.
package filter
import (
"fmt"
"net/netip"
"slices"
"sync"
"time"
"go4.org/netipx"
"tailscale.com/envknob"
"tailscale.com/net/flowtrack"
"tailscale.com/net/ipset"
"tailscale.com/net/netaddr"
"tailscale.com/net/packet"
"tailscale.com/tailcfg"
"tailscale.com/tstime/rate"
"tailscale.com/types/ipproto"
"tailscale.com/types/logger"
"tailscale.com/types/views"
"tailscale.com/util/mak"
"tailscale.com/util/slicesx"
"tailscale.com/wgengine/filter/filtertype"
)
// Filter is a stateful packet filter.
type Filter struct {
logf logger.Logf
// local4 and local6 report whether an IP is "local" to this node, for the
// respective address family. All packets coming in over tailscale must have
// a destination within local, regardless of the policy filter below.
local4 func(netip.Addr) bool
local6 func(netip.Addr) bool
// logIPs is the set of IPs that are allowed to appear in flow
// logs. If a packet is to or from an IP not in logIPs, it will
// never be logged.
logIPs4 func(netip.Addr) bool
logIPs6 func(netip.Addr) bool
// srcIPHasCap optionally specifies a function that reports
// whether a given source IP address has a given capability.
srcIPHasCap CapTestFunc
// matches4 and matches6 are lists of match->action rules
// applied to all packets arriving over tailscale
// tunnels. Matches are checked in order, and processing stops
// at the first matching rule. The default policy if no rules
// match is to drop the packet.
matches4 matches
matches6 matches
// cap4 and cap6 are the subsets of the matches that are about
// capability grants, partitioned by source IP address family.
cap4, cap6 matches
// state is the connection tracking state attached to this
// filter. It is used to allow incoming traffic that is a response
// to an outbound connection that this node made, even if those
// incoming packets don't get accepted by matches above.
state *filterState
shieldsUp bool
}
// filterState is a state cache of past seen packets.
type filterState struct {
mu sync.Mutex
lru *flowtrack.Cache[struct{}] // from flowtrack.Tuple -> struct{}
}
// lruMax is the size of the LRU cache in filterState.
const lruMax = 512
// Response is a verdict from the packet filter.
type Response int
const (
Drop Response = iota // do not continue processing packet.
DropSilently // do not continue processing packet, but also don't log
Accept // continue processing packet.
noVerdict // no verdict yet, continue running filter
)
func (r Response) String() string {
switch r {
case Drop:
return "Drop"
case DropSilently:
return "DropSilently"
case Accept:
return "Accept"
case noVerdict:
return "noVerdict"
default:
return "???"
}
}
func (r Response) IsDrop() bool {
return r == Drop || r == DropSilently
}
// RunFlags controls the filter's debug log verbosity at runtime.
type RunFlags int
const (
LogDrops RunFlags = 1 << iota // write dropped packet info to logf
LogAccepts // write accepted packet info to logf
HexdumpDrops // print packet hexdump when logging drops
HexdumpAccepts // print packet hexdump when logging accepts
)
type (
Match = filtertype.Match
NetPortRange = filtertype.NetPortRange
PortRange = filtertype.PortRange
CapMatch = filtertype.CapMatch
)
// NewAllowAllForTest returns a packet filter that accepts
// everything. Use in tests only, as it permits some kinds of spoofing
// attacks to reach the OS network stack.
func NewAllowAllForTest(logf logger.Logf) *Filter {
any4 := netip.PrefixFrom(netaddr.IPv4(0, 0, 0, 0), 0)
any6 := netip.PrefixFrom(netip.AddrFrom16([16]byte{}), 0)
ms := []Match{
{
IPProto: views.SliceOf([]ipproto.Proto{ipproto.TCP, ipproto.UDP, ipproto.ICMPv4}),
Srcs: []netip.Prefix{any4},
Dsts: []NetPortRange{
{
Net: any4,
Ports: PortRange{
First: 0,
Last: 65535,
},
},
},
},
{
IPProto: views.SliceOf([]ipproto.Proto{ipproto.TCP, ipproto.UDP, ipproto.ICMPv6}),
Srcs: []netip.Prefix{any6},
Dsts: []NetPortRange{
{
Net: any6,
Ports: PortRange{
First: 0,
Last: 65535,
},
},
},
},
}
var sb netipx.IPSetBuilder
sb.AddPrefix(any4)
sb.AddPrefix(any6)
ipSet, _ := sb.IPSet()
return New(ms, nil, ipSet, ipSet, nil, logf)
}
// NewAllowNone returns a packet filter that rejects everything.
func NewAllowNone(logf logger.Logf, logIPs *netipx.IPSet) *Filter {
return New(nil, nil, &netipx.IPSet{}, logIPs, nil, logf)
}
// NewShieldsUpFilter returns a packet filter that rejects incoming connections.
//
// If shareStateWith is non-nil, the returned filter shares state with the previous one,
// as long as the previous one was also a shields up filter.
func NewShieldsUpFilter(localNets *netipx.IPSet, logIPs *netipx.IPSet, shareStateWith *Filter, logf logger.Logf) *Filter {
// Don't permit sharing state with a prior filter that wasn't a shields-up filter.
if shareStateWith != nil && !shareStateWith.shieldsUp {
shareStateWith = nil
}
f := New(nil, nil, localNets, logIPs, shareStateWith, logf)
f.shieldsUp = true
return f
}
// New creates a new packet filter. The filter enforces that incoming packets
// must be destined to an IP in localNets, and must be allowed by matches.
// The optional capTest func is used to evaluate a Match that uses capabilities.
// If nil, such matches will always fail.
//
// If shareStateWith is non-nil, the returned filter shares state with the
// previous one, to enable changing rules at runtime without breaking existing
// stateful flows.
func New(matches []Match, capTest CapTestFunc, localNets, logIPs *netipx.IPSet, shareStateWith *Filter, logf logger.Logf) *Filter {
var state *filterState
if shareStateWith != nil {
state = shareStateWith.state
} else {
state = &filterState{
lru: &flowtrack.Cache[struct{}]{MaxEntries: lruMax},
}
}
f := &Filter{
logf: logf,
matches4: matchesFamily(matches, netip.Addr.Is4),
matches6: matchesFamily(matches, netip.Addr.Is6),
cap4: capMatchesFunc(matches, netip.Addr.Is4),
cap6: capMatchesFunc(matches, netip.Addr.Is6),
local4: ipset.FalseContainsIPFunc(),
local6: ipset.FalseContainsIPFunc(),
logIPs4: ipset.FalseContainsIPFunc(),
logIPs6: ipset.FalseContainsIPFunc(),
state: state,
}
if localNets != nil {
p := localNets.Prefixes()
p4, p6 := slicesx.Partition(p, func(p netip.Prefix) bool { return p.Addr().Is4() })
f.local4 = ipset.NewContainsIPFunc(views.SliceOf(p4))
f.local6 = ipset.NewContainsIPFunc(views.SliceOf(p6))
}
if logIPs != nil {
p := logIPs.Prefixes()
p4, p6 := slicesx.Partition(p, func(p netip.Prefix) bool { return p.Addr().Is4() })
f.logIPs4 = ipset.NewContainsIPFunc(views.SliceOf(p4))
f.logIPs6 = ipset.NewContainsIPFunc(views.SliceOf(p6))
}
return f
}
// matchesFamily returns the subset of ms for which keep(srcNet.IP)
// and keep(dstNet.IP) are both true.
func matchesFamily(ms matches, keep func(netip.Addr) bool) matches {
var ret matches
for _, m := range ms {
var retm Match
retm.IPProto = m.IPProto
retm.SrcCaps = m.SrcCaps
for _, src := range m.Srcs {
if keep(src.Addr()) {
retm.Srcs = append(retm.Srcs, src)
}
}
for _, dst := range m.Dsts {
if keep(dst.Net.Addr()) {
retm.Dsts = append(retm.Dsts, dst)
}
}
if (len(retm.Srcs) > 0 || len(retm.SrcCaps) > 0) && len(retm.Dsts) > 0 {
retm.SrcsContains = ipset.NewContainsIPFunc(views.SliceOf(retm.Srcs))
ret = append(ret, retm)
}
}
return ret
}
// capMatchesFunc returns a copy of the subset of ms for which keep(srcNet.IP)
// and the match is a capability grant.
func capMatchesFunc(ms matches, keep func(netip.Addr) bool) matches {
var ret matches
for _, m := range ms {
if len(m.Caps) == 0 {
continue
}
retm := Match{Caps: m.Caps}
for _, src := range m.Srcs {
if keep(src.Addr()) {
retm.Srcs = append(retm.Srcs, src)
}
}
if len(retm.Srcs) > 0 {
retm.SrcsContains = ipset.NewContainsIPFunc(views.SliceOf(retm.Srcs))
ret = append(ret, retm)
}
}
return ret
}
func maybeHexdump(flag RunFlags, b []byte) string {
if flag == 0 {
return ""
}
return packet.Hexdump(b) + "\n"
}
// TODO(apenwarr): use a bigger bucket for specifically TCP SYN accept logging?
// Logging is a quick way to record every newly opened TCP connection, but
// we have to be cautious about flooding the logs vs letting people use
// flood protection to hide their traffic. We could use a rate limiter in
// the actual *filter* for SYN accepts, perhaps.
var acceptBucket = rate.NewLimiter(rate.Every(10*time.Second), 3)
var dropBucket = rate.NewLimiter(rate.Every(5*time.Second), 10)
// NOTE(Xe): This func init is used to detect
// TS_DEBUG_FILTER_RATE_LIMIT_LOGS=all, and if it matches, to
// effectively disable the limits on the log rate by setting the limit
// to 1 millisecond. This should capture everything.
func init() {
if envknob.String("TS_DEBUG_FILTER_RATE_LIMIT_LOGS") != "all" {
return
}
acceptBucket = rate.NewLimiter(rate.Every(time.Millisecond), 10)
dropBucket = rate.NewLimiter(rate.Every(time.Millisecond), 10)
}
func (f *Filter) logRateLimit(runflags RunFlags, q *packet.Parsed, dir direction, r Response, why string) {
if runflags == 0 || !f.loggingAllowed(q) {
return
}
if r == Drop && omitDropLogging(q, dir) {
return
}
var verdict string
if r == Drop && (runflags&LogDrops) != 0 && dropBucket.Allow() {
verdict = "Drop"
runflags &= HexdumpDrops
} else if r == Accept && (runflags&LogAccepts) != 0 && acceptBucket.Allow() {
verdict = "[v1] Accept"
runflags &= HexdumpAccepts
}
// Note: it is crucial that q.String() be called only if {accept,drop}Bucket.Allow() passes,
// since it causes an allocation.
if verdict != "" {
b := q.Buffer()
f.logf("%s: %s %d %s\n%s", verdict, q.String(), len(b), why, maybeHexdump(runflags, b))
}
}
// dummyPacket is a 20-byte slice of garbage, to pass the filter
// pre-check when evaluating synthesized packets.
var dummyPacket = []byte{
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
}
// Check determines whether traffic from srcIP to dstIP:dstPort is allowed
// using protocol proto.
func (f *Filter) Check(srcIP, dstIP netip.Addr, dstPort uint16, proto ipproto.Proto) Response {
pkt := &packet.Parsed{}
pkt.Decode(dummyPacket) // initialize private fields
switch {
case (srcIP.Is4() && dstIP.Is6()) || (srcIP.Is6() && srcIP.Is4()):
// Mismatched address families, no filters will
// match.
return Drop
case srcIP.Is4():
pkt.IPVersion = 4
case srcIP.Is6():
pkt.IPVersion = 6
default:
panic("unreachable")
}
pkt.Src = netip.AddrPortFrom(srcIP, 0)
pkt.Dst = netip.AddrPortFrom(dstIP, dstPort)
pkt.IPProto = proto
if proto == ipproto.TCP {
pkt.TCPFlags = packet.TCPSyn
}
return f.RunIn(pkt, 0)
}
// CheckTCP determines whether TCP traffic from srcIP to dstIP:dstPort
// is allowed.
func (f *Filter) CheckTCP(srcIP, dstIP netip.Addr, dstPort uint16) Response {
return f.Check(srcIP, dstIP, dstPort, ipproto.TCP)
}
// CapsWithValues appends to base the capabilities that srcIP has talking
// to dstIP.
func (f *Filter) CapsWithValues(srcIP, dstIP netip.Addr) tailcfg.PeerCapMap {
var mm matches
switch {
case srcIP.Is4():
mm = f.cap4
case srcIP.Is6():
mm = f.cap6
}
var out tailcfg.PeerCapMap
for _, m := range mm {
if !m.SrcsContains(srcIP) {
continue
}
for _, cm := range m.Caps {
if cm.Cap != "" && cm.Dst.Contains(dstIP) {
prev, ok := out[cm.Cap]
if !ok {
mak.Set(&out, cm.Cap, slices.Clone(cm.Values))
continue
}
out[cm.Cap] = append(prev, cm.Values...)
}
}
}
return out
}
// ShieldsUp reports whether this is a "shields up" (block everything
// incoming) filter.
func (f *Filter) ShieldsUp() bool { return f.shieldsUp }
// RunIn determines whether this node is allowed to receive q from a
// Tailscale peer.
func (f *Filter) RunIn(q *packet.Parsed, rf RunFlags) Response {
dir := in
r := f.pre(q, rf, dir)
if r == Accept || r == Drop {
// already logged
return r
}
var why string
switch q.IPVersion {
case 4:
r, why = f.runIn4(q)
case 6:
r, why = f.runIn6(q)
default:
r, why = Drop, "not-ip"
}
f.logRateLimit(rf, q, dir, r, why)
return r
}
// RunOut determines whether this node is allowed to send q to a
// Tailscale peer.
func (f *Filter) RunOut(q *packet.Parsed, rf RunFlags) Response {
dir := out
r := f.pre(q, rf, dir)
if r == Accept || r == Drop {
// already logged
return r
}
r, why := f.runOut(q)
f.logRateLimit(rf, q, dir, r, why)
return r
}
var unknownProtoStringCache sync.Map // ipproto.Proto -> string
func unknownProtoString(proto ipproto.Proto) string {
if v, ok := unknownProtoStringCache.Load(proto); ok {
return v.(string)
}
s := fmt.Sprintf("unknown-protocol-%d", proto)
unknownProtoStringCache.Store(proto, s)
return s
}
func (f *Filter) runIn4(q *packet.Parsed) (r Response, why string) {
// A compromised peer could try to send us packets for
// destinations we didn't explicitly advertise. This check is to
// prevent that.
if !f.local4(q.Dst.Addr()) {
return Drop, "destination not allowed"
}
switch q.IPProto {
case ipproto.ICMPv4:
if q.IsEchoResponse() || q.IsError() {
// ICMP responses are allowed.
// TODO(apenwarr): consider using conntrack state.
// We could choose to reject all packets that aren't
// related to an existing ICMP-Echo, TCP, or UDP
// session.
return Accept, "icmp response ok"
} else if f.matches4.matchIPsOnly(q, f.srcIPHasCap) {
// If any port is open to an IP, allow ICMP to it.
return Accept, "icmp ok"
}
case ipproto.TCP:
// For TCP, we want to allow *outgoing* connections,
// which means we want to allow return packets on those
// connections. To make this restriction work, we need to
// allow non-SYN packets (continuation of an existing session)
// to arrive. This should be okay since a new incoming session
// can't be initiated without first sending a SYN.
// It happens to also be much faster.
// TODO(apenwarr): Skip the rest of decoding in this path?
if !q.IsTCPSyn() {
return Accept, "tcp non-syn"
}
if f.matches4.match(q, f.srcIPHasCap) {
return Accept, "tcp ok"
}
case ipproto.UDP, ipproto.SCTP:
t := flowtrack.MakeTuple(q.IPProto, q.Src, q.Dst)
f.state.mu.Lock()
_, ok := f.state.lru.Get(t)
f.state.mu.Unlock()
if ok {
return Accept, "cached"
}
if f.matches4.match(q, f.srcIPHasCap) {
return Accept, "ok"
}
case ipproto.TSMP:
return Accept, "tsmp ok"
default:
if f.matches4.matchProtoAndIPsOnlyIfAllPorts(q) {
return Accept, "other-portless ok"
}
return Drop, unknownProtoString(q.IPProto)
}
return Drop, "no rules matched"
}
func (f *Filter) runIn6(q *packet.Parsed) (r Response, why string) {
// A compromised peer could try to send us packets for
// destinations we didn't explicitly advertise. This check is to
// prevent that.
if !f.local6(q.Dst.Addr()) {
return Drop, "destination not allowed"
}
switch q.IPProto {
case ipproto.ICMPv6:
if q.IsEchoResponse() || q.IsError() {
// ICMP responses are allowed.
// TODO(apenwarr): consider using conntrack state.
// We could choose to reject all packets that aren't
// related to an existing ICMP-Echo, TCP, or UDP
// session.
return Accept, "icmp response ok"
} else if f.matches6.matchIPsOnly(q, f.srcIPHasCap) {
// If any port is open to an IP, allow ICMP to it.
return Accept, "icmp ok"
}
case ipproto.TCP:
// For TCP, we want to allow *outgoing* connections,
// which means we want to allow return packets on those
// connections. To make this restriction work, we need to
// allow non-SYN packets (continuation of an existing session)
// to arrive. This should be okay since a new incoming session
// can't be initiated without first sending a SYN.
// It happens to also be much faster.
// TODO(apenwarr): Skip the rest of decoding in this path?
if q.IPProto == ipproto.TCP && !q.IsTCPSyn() {
return Accept, "tcp non-syn"
}
if f.matches6.match(q, f.srcIPHasCap) {
return Accept, "tcp ok"
}
case ipproto.UDP, ipproto.SCTP:
t := flowtrack.MakeTuple(q.IPProto, q.Src, q.Dst)
f.state.mu.Lock()
_, ok := f.state.lru.Get(t)
f.state.mu.Unlock()
if ok {
return Accept, "cached"
}
if f.matches6.match(q, f.srcIPHasCap) {
return Accept, "ok"
}
case ipproto.TSMP:
return Accept, "tsmp ok"
default:
if f.matches6.matchProtoAndIPsOnlyIfAllPorts(q) {
return Accept, "other-portless ok"
}
return Drop, unknownProtoString(q.IPProto)
}
return Drop, "no rules matched"
}
// runIn runs the output-specific part of the filter logic.
func (f *Filter) runOut(q *packet.Parsed) (r Response, why string) {
switch q.IPProto {
case ipproto.UDP, ipproto.SCTP:
tuple := flowtrack.MakeTuple(q.IPProto, q.Dst, q.Src) // src/dst reversed
f.state.mu.Lock()
f.state.lru.Add(tuple, struct{}{})
f.state.mu.Unlock()
}
return Accept, "ok out"
}
// direction is whether a packet was flowing into this machine, or
// flowing out.
type direction int
const (
in direction = iota // from Tailscale peer to local machine
out // from local machine to Tailscale peer
)
func (d direction) String() string {
switch d {
case in:
return "in"
case out:
return "out"
default:
return fmt.Sprintf("[??dir=%d]", int(d))
}
}
var gcpDNSAddr = netaddr.IPv4(169, 254, 169, 254)
// pre runs the direction-agnostic filter logic. dir is only used for
// logging.
func (f *Filter) pre(q *packet.Parsed, rf RunFlags, dir direction) Response {
if len(q.Buffer()) == 0 {
// wireguard keepalive packet, always permit.
return Accept
}
if len(q.Buffer()) < 20 {
f.logRateLimit(rf, q, dir, Drop, "too short")
return Drop
}
if q.Dst.Addr().IsMulticast() {
f.logRateLimit(rf, q, dir, Drop, "multicast")
return Drop
}
if q.Dst.Addr().IsLinkLocalUnicast() && q.Dst.Addr() != gcpDNSAddr {
f.logRateLimit(rf, q, dir, Drop, "link-local-unicast")
return Drop
}
if q.IPProto == ipproto.Fragment {
// Fragments after the first always need to be passed through.
// Very small fragments are considered Junk by Parsed.
f.logRateLimit(rf, q, dir, Accept, "fragment")
return Accept
}
return noVerdict
}
// loggingAllowed reports whether p can appear in logs at all.
func (f *Filter) loggingAllowed(p *packet.Parsed) bool {
switch p.IPVersion {
case 4:
return f.logIPs4(p.Src.Addr()) && f.logIPs4(p.Dst.Addr())
case 6:
return f.logIPs6(p.Src.Addr()) && f.logIPs6(p.Dst.Addr())
}
return false
}
// omitDropLogging reports whether packet p, which has already been
// deemed a packet to Drop, should bypass the [rate-limited] logging.
// We don't want to log scary & spammy reject warnings for packets
// that are totally normal, like IPv6 route announcements.
func omitDropLogging(p *packet.Parsed, dir direction) bool {
if dir != out {
return false
}
return p.Dst.Addr().IsMulticast() || (p.Dst.Addr().IsLinkLocalUnicast() && p.Dst.Addr() != gcpDNSAddr) || p.IPProto == ipproto.IGMP
}

View File

@@ -0,0 +1,116 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package filtertype defines the types used by wgengine/filter.
package filtertype
import (
"fmt"
"net/netip"
"strings"
"tailscale.com/tailcfg"
"tailscale.com/types/ipproto"
"tailscale.com/types/views"
)
//go:generate go run tailscale.com/cmd/cloner --type=Match,CapMatch
// PortRange is a range of TCP and UDP ports.
type PortRange struct {
First, Last uint16 // inclusive
}
var AllPorts = PortRange{0, 0xffff}
func (pr PortRange) String() string {
if pr.First == 0 && pr.Last == 65535 {
return "*"
} else if pr.First == pr.Last {
return fmt.Sprintf("%d", pr.First)
} else {
return fmt.Sprintf("%d-%d", pr.First, pr.Last)
}
}
// contains returns whether port is in pr.
func (pr PortRange) Contains(port uint16) bool {
return port >= pr.First && port <= pr.Last
}
// NetPortRange combines an IP address prefix and PortRange.
type NetPortRange struct {
Net netip.Prefix
Ports PortRange
}
func (npr NetPortRange) String() string {
return fmt.Sprintf("%v:%v", npr.Net, npr.Ports)
}
// CapMatch is a capability grant match predicate.
type CapMatch struct {
// Dst is the IP prefix that the destination IP address matches against
// to get the capability.
Dst netip.Prefix
// Cap is the capability that's granted if the destination IP addresses
// matches Dst.
Cap tailcfg.PeerCapability
// Values are the raw JSON values of the capability.
// See tailcfg.PeerCapability and tailcfg.PeerCapMap for details.
Values []tailcfg.RawMessage
}
// Match matches packets from any IP address in Srcs to any ip:port in
// Dsts.
type Match struct {
// IPProto is the set of IP protocol numbers for which this match applies.
// It is required. There is no default value at this layer.
// If empty, it doesn't match.
IPProto views.Slice[ipproto.Proto]
// Srcs is the set of source IP prefixes for which this match applies. A
// Match can match by either its source IP address being in Srcs (which
// SrcsContains tests) or if the source IP is of a known peer self address
// that contains a NodeCapability listed in SrcCaps.
Srcs []netip.Prefix
// SrcsContains is an optimized function that reports whether Addr is in
// Srcs, using the best search method for the size and shape of Srcs.
SrcsContains func(netip.Addr) bool `json:"-"` // report whether Addr is in Srcs
// SrcCaps is an alternative way to match packets. If the peer's source IP
// has one of these capabilities, it's also permitted. The peers are only
// looked up by their self address (Node.Addresses) and not by subnet routes
// they advertise.
SrcCaps []tailcfg.NodeCapability
Dsts []NetPortRange // optional, if source matches
Caps []CapMatch // optional, if source match
}
func (m Match) String() string {
// TODO(bradfitz): use strings.Builder, add String tests
srcs := []string{}
for _, src := range m.Srcs {
srcs = append(srcs, src.String())
}
dsts := []string{}
for _, dst := range m.Dsts {
dsts = append(dsts, dst.String())
}
var ss, ds string
if len(srcs) == 1 {
ss = srcs[0]
} else {
ss = "[" + strings.Join(srcs, ",") + "]"
}
if len(dsts) == 1 {
ds = dsts[0]
} else {
ds = "[" + strings.Join(dsts, ",") + "]"
}
return fmt.Sprintf("%v%v=>%v", m.IPProto, ss, ds)
}

View File

@@ -0,0 +1,64 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
package filtertype
import (
"net/netip"
"tailscale.com/tailcfg"
"tailscale.com/types/ipproto"
"tailscale.com/types/views"
)
// Clone makes a deep copy of Match.
// The result aliases no memory with the original.
func (src *Match) Clone() *Match {
if src == nil {
return nil
}
dst := new(Match)
*dst = *src
dst.IPProto = src.IPProto
dst.Srcs = append(src.Srcs[:0:0], src.Srcs...)
dst.SrcCaps = append(src.SrcCaps[:0:0], src.SrcCaps...)
dst.Dsts = append(src.Dsts[:0:0], src.Dsts...)
if src.Caps != nil {
dst.Caps = make([]CapMatch, len(src.Caps))
for i := range dst.Caps {
dst.Caps[i] = *src.Caps[i].Clone()
}
}
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _MatchCloneNeedsRegeneration = Match(struct {
IPProto views.Slice[ipproto.Proto]
Srcs []netip.Prefix
SrcsContains func(netip.Addr) bool
SrcCaps []tailcfg.NodeCapability
Dsts []NetPortRange
Caps []CapMatch
}{})
// Clone makes a deep copy of CapMatch.
// The result aliases no memory with the original.
func (src *CapMatch) Clone() *CapMatch {
if src == nil {
return nil
}
dst := new(CapMatch)
*dst = *src
dst.Values = append(src.Values[:0:0], src.Values...)
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _CapMatchCloneNeedsRegeneration = CapMatch(struct {
Dst netip.Prefix
Cap tailcfg.PeerCapability
Values []tailcfg.RawMessage
}{})

107
vendor/tailscale.com/wgengine/filter/match.go generated vendored Normal file
View File

@@ -0,0 +1,107 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package filter
import (
"net/netip"
"tailscale.com/net/packet"
"tailscale.com/tailcfg"
"tailscale.com/types/views"
"tailscale.com/wgengine/filter/filtertype"
)
type matches []filtertype.Match
func (ms matches) match(q *packet.Parsed, hasCap CapTestFunc) bool {
for i := range ms {
m := &ms[i]
if !views.SliceContains(m.IPProto, q.IPProto) {
continue
}
if !srcMatches(m, q.Src.Addr(), hasCap) {
continue
}
for _, dst := range m.Dsts {
if !dst.Net.Contains(q.Dst.Addr()) {
continue
}
if !dst.Ports.Contains(q.Dst.Port()) {
continue
}
return true
}
}
return false
}
// srcMatches reports whether srcAddr matche the src requirements in m, either
// by Srcs (using SrcsContains), or by the node having a capability listed
// in SrcCaps using the provided hasCap function.
func srcMatches(m *filtertype.Match, srcAddr netip.Addr, hasCap CapTestFunc) bool {
if m.SrcsContains(srcAddr) {
return true
}
if hasCap != nil {
for _, c := range m.SrcCaps {
if hasCap(srcAddr, c) {
return true
}
}
}
return false
}
// CapTestFunc is the function signature of a function that tests whether srcIP
// has a given capability.
//
// It it used in the fast path of evaluating filter rules so should be fast.
type CapTestFunc = func(srcIP netip.Addr, cap tailcfg.NodeCapability) bool
func (ms matches) matchIPsOnly(q *packet.Parsed, hasCap CapTestFunc) bool {
srcAddr := q.Src.Addr()
for _, m := range ms {
if !m.SrcsContains(srcAddr) {
continue
}
for _, dst := range m.Dsts {
if dst.Net.Contains(q.Dst.Addr()) {
return true
}
}
}
if hasCap != nil {
for _, m := range ms {
for _, c := range m.SrcCaps {
if hasCap(srcAddr, c) {
return true
}
}
}
}
return false
}
// matchProtoAndIPsOnlyIfAllPorts reports q matches any Match in ms where the
// Match if for the right IP Protocol and IP address, but ports are
// ignored, as long as the match is for the entire uint16 port range.
func (ms matches) matchProtoAndIPsOnlyIfAllPorts(q *packet.Parsed) bool {
for _, m := range ms {
if !views.SliceContains(m.IPProto, q.IPProto) {
continue
}
if !m.SrcsContains(q.Src.Addr()) {
continue
}
for _, dst := range m.Dsts {
if dst.Ports != filtertype.AllPorts {
continue
}
if dst.Net.Contains(q.Dst.Addr()) {
return true
}
}
}
return false
}

178
vendor/tailscale.com/wgengine/filter/tailcfg.go generated vendored Normal file
View File

@@ -0,0 +1,178 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package filter
import (
"fmt"
"net/netip"
"strings"
"go4.org/netipx"
"tailscale.com/net/ipset"
"tailscale.com/net/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/ipproto"
"tailscale.com/types/views"
)
var defaultProtos = []ipproto.Proto{
ipproto.TCP,
ipproto.UDP,
ipproto.ICMPv4,
ipproto.ICMPv6,
}
var defaultProtosView = views.SliceOf(defaultProtos)
// MatchesFromFilterRules converts tailcfg FilterRules into Matches.
// If an error is returned, the Matches result is still valid,
// containing the rules that were successfully converted.
func MatchesFromFilterRules(pf []tailcfg.FilterRule) ([]Match, error) {
mm := make([]Match, 0, len(pf))
var erracc error
for _, r := range pf {
if len(r.SrcBits) > 0 {
return nil, fmt.Errorf("unexpected SrcBits; control plane should not send this to this client version")
}
// Profiling determined that this function was spending a lot
// of time in runtime.growslice. As such, we attempt to
// pre-allocate some slices. Multipliers were chosen arbitrarily.
m := Match{
Srcs: make([]netip.Prefix, 0, len(r.SrcIPs)),
Dsts: make([]NetPortRange, 0, 2*len(r.DstPorts)),
Caps: make([]CapMatch, 0, 3*len(r.CapGrant)),
}
if len(r.IPProto) == 0 {
m.IPProto = defaultProtosView
} else {
filtered := make([]ipproto.Proto, 0, len(r.IPProto))
for _, n := range r.IPProto {
if n >= 0 && n <= 0xff {
filtered = append(filtered, ipproto.Proto(n))
}
}
m.IPProto = views.SliceOf(filtered)
}
for _, s := range r.SrcIPs {
nets, cap, err := parseIPSet(s)
if err != nil && erracc == nil {
erracc = err
continue
}
m.Srcs = append(m.Srcs, nets...)
if cap != "" {
m.SrcCaps = append(m.SrcCaps, cap)
}
}
m.SrcsContains = ipset.NewContainsIPFunc(views.SliceOf(m.Srcs))
for _, d := range r.DstPorts {
if d.Bits != nil {
return nil, fmt.Errorf("unexpected DstBits; control plane should not send this to this client version")
}
nets, cap, err := parseIPSet(d.IP)
if err != nil && erracc == nil {
erracc = err
continue
}
if cap != "" {
erracc = fmt.Errorf("unexpected capability %q in DstPorts", cap)
continue
}
for _, net := range nets {
m.Dsts = append(m.Dsts, NetPortRange{
Net: net,
Ports: PortRange{
First: d.Ports.First,
Last: d.Ports.Last,
},
})
}
}
for _, cm := range r.CapGrant {
for _, dstNet := range cm.Dsts {
for _, cap := range cm.Caps {
m.Caps = append(m.Caps, CapMatch{
Dst: dstNet,
Cap: cap,
})
}
for cap, val := range cm.CapMap {
m.Caps = append(m.Caps, CapMatch{
Dst: dstNet,
Cap: tailcfg.PeerCapability(cap),
Values: val,
})
}
}
}
mm = append(mm, m)
}
return mm, erracc
}
var (
zeroIP4 = netaddr.IPv4(0, 0, 0, 0)
zeroIP6 = netip.AddrFrom16([16]byte{})
)
// parseIPSet parses arg as one:
//
// - an IP address (IPv4 or IPv6)
// - the string "*" to match everything (both IPv4 & IPv6)
// - a CIDR (e.g. "192.168.0.0/16")
// - a range of two IPs, inclusive, separated by hyphen ("2eff::1-2eff::0800")
// - "cap:<peer-node-capability>" to match a peer node capability
//
// TODO(bradfitz): make this return an IPSet and plumb that all
// around, and ultimately use a new version of IPSet.ContainsFunc like
// Contains16Func that works in [16]byte address, so we we can match
// at runtime without allocating?
func parseIPSet(arg string) (prefixes []netip.Prefix, peerCap tailcfg.NodeCapability, err error) {
if arg == "*" {
// User explicitly requested wildcard.
return []netip.Prefix{
netip.PrefixFrom(zeroIP4, 0),
netip.PrefixFrom(zeroIP6, 0),
}, "", nil
}
if cap, ok := strings.CutPrefix(arg, "cap:"); ok {
return nil, tailcfg.NodeCapability(cap), nil
}
if strings.Contains(arg, "/") {
pfx, err := netip.ParsePrefix(arg)
if err != nil {
return nil, "", err
}
if pfx != pfx.Masked() {
return nil, "", fmt.Errorf("%v contains non-network bits set", pfx)
}
return []netip.Prefix{pfx}, "", nil
}
if strings.Count(arg, "-") == 1 {
ip1s, ip2s, _ := strings.Cut(arg, "-")
ip1, err := netip.ParseAddr(ip1s)
if err != nil {
return nil, "", err
}
ip2, err := netip.ParseAddr(ip2s)
if err != nil {
return nil, "", err
}
r := netipx.IPRangeFrom(ip1, ip2)
if !r.IsValid() {
return nil, "", fmt.Errorf("invalid IP range %q", arg)
}
return r.Prefixes(), "", nil
}
ip, err := netip.ParseAddr(arg)
if err != nil {
return nil, "", fmt.Errorf("invalid IP address %q", arg)
}
return []netip.Prefix{netip.PrefixFrom(ip, ip.BitLen())}, "", nil
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package magicsock
import (
"net/netip"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
"tailscale.com/types/nettype"
)
var (
// This acts as a compile-time check for our usage of ipv6.Message in
// batchingConn for both IPv6 and IPv4 operations.
_ ipv6.Message = ipv4.Message{}
)
// batchingConn is a nettype.PacketConn that provides batched i/o.
type batchingConn interface {
nettype.PacketConn
ReadBatch(msgs []ipv6.Message, flags int) (n int, err error)
WriteBatchTo(buffs [][]byte, addr netip.AddrPort) error
}

View File

@@ -0,0 +1,14 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !linux
package magicsock
import (
"tailscale.com/types/nettype"
)
func tryUpgradeToBatchingConn(pconn nettype.PacketConn, _ string, _ int) nettype.PacketConn {
return pconn
}

View File

@@ -0,0 +1,424 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package magicsock
import (
"encoding/binary"
"errors"
"fmt"
"net"
"net/netip"
"runtime"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"unsafe"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
"golang.org/x/sys/unix"
"tailscale.com/hostinfo"
"tailscale.com/net/neterror"
"tailscale.com/types/nettype"
)
// xnetBatchReaderWriter defines the batching i/o methods of
// golang.org/x/net/ipv4.PacketConn (and ipv6.PacketConn).
// TODO(jwhited): This should eventually be replaced with the standard library
// implementation of https://github.com/golang/go/issues/45886
type xnetBatchReaderWriter interface {
xnetBatchReader
xnetBatchWriter
}
type xnetBatchReader interface {
ReadBatch([]ipv6.Message, int) (int, error)
}
type xnetBatchWriter interface {
WriteBatch([]ipv6.Message, int) (int, error)
}
// linuxBatchingConn is a UDP socket that provides batched i/o. It implements
// batchingConn.
type linuxBatchingConn struct {
pc nettype.PacketConn
xpc xnetBatchReaderWriter
rxOffload bool // supports UDP GRO or similar
txOffload atomic.Bool // supports UDP GSO or similar
setGSOSizeInControl func(control *[]byte, gsoSize uint16) // typically setGSOSizeInControl(); swappable for testing
getGSOSizeFromControl func(control []byte) (int, error) // typically getGSOSizeFromControl(); swappable for testing
sendBatchPool sync.Pool
}
func (c *linuxBatchingConn) ReadFromUDPAddrPort(p []byte) (n int, addr netip.AddrPort, err error) {
if c.rxOffload {
// UDP_GRO is opt-in on Linux via setsockopt(). Once enabled you may
// receive a "monster datagram" from any read call. The ReadFrom() API
// does not support passing the GSO size and is unsafe to use in such a
// case. Other platforms may vary in behavior, but we go with the most
// conservative approach to prevent this from becoming a footgun in the
// future.
return 0, netip.AddrPort{}, errors.New("rx UDP offload is enabled on this socket, single packet reads are unavailable")
}
return c.pc.ReadFromUDPAddrPort(p)
}
func (c *linuxBatchingConn) SetDeadline(t time.Time) error {
return c.pc.SetDeadline(t)
}
func (c *linuxBatchingConn) SetReadDeadline(t time.Time) error {
return c.pc.SetReadDeadline(t)
}
func (c *linuxBatchingConn) SetWriteDeadline(t time.Time) error {
return c.pc.SetWriteDeadline(t)
}
const (
// This was initially established for Linux, but may split out to
// GOOS-specific values later. It originates as UDP_MAX_SEGMENTS in the
// kernel's TX path, and UDP_GRO_CNT_MAX for RX.
udpSegmentMaxDatagrams = 64
)
const (
// Exceeding these values results in EMSGSIZE.
maxIPv4PayloadLen = 1<<16 - 1 - 20 - 8
maxIPv6PayloadLen = 1<<16 - 1 - 8
)
// coalesceMessages iterates msgs, coalescing them where possible while
// maintaining datagram order. All msgs have their Addr field set to addr.
func (c *linuxBatchingConn) coalesceMessages(addr *net.UDPAddr, buffs [][]byte, msgs []ipv6.Message) int {
var (
base = -1 // index of msg we are currently coalescing into
gsoSize int // segmentation size of msgs[base]
dgramCnt int // number of dgrams coalesced into msgs[base]
endBatch bool // tracking flag to start a new batch on next iteration of buffs
)
maxPayloadLen := maxIPv4PayloadLen
if addr.IP.To4() == nil {
maxPayloadLen = maxIPv6PayloadLen
}
for i, buff := range buffs {
if i > 0 {
msgLen := len(buff)
baseLenBefore := len(msgs[base].Buffers[0])
freeBaseCap := cap(msgs[base].Buffers[0]) - baseLenBefore
if msgLen+baseLenBefore <= maxPayloadLen &&
msgLen <= gsoSize &&
msgLen <= freeBaseCap &&
dgramCnt < udpSegmentMaxDatagrams &&
!endBatch {
msgs[base].Buffers[0] = append(msgs[base].Buffers[0], make([]byte, msgLen)...)
copy(msgs[base].Buffers[0][baseLenBefore:], buff)
if i == len(buffs)-1 {
c.setGSOSizeInControl(&msgs[base].OOB, uint16(gsoSize))
}
dgramCnt++
if msgLen < gsoSize {
// A smaller than gsoSize packet on the tail is legal, but
// it must end the batch.
endBatch = true
}
continue
}
}
if dgramCnt > 1 {
c.setGSOSizeInControl(&msgs[base].OOB, uint16(gsoSize))
}
// Reset prior to incrementing base since we are preparing to start a
// new potential batch.
endBatch = false
base++
gsoSize = len(buff)
msgs[base].OOB = msgs[base].OOB[:0]
msgs[base].Buffers[0] = buff
msgs[base].Addr = addr
dgramCnt = 1
}
return base + 1
}
type sendBatch struct {
msgs []ipv6.Message
ua *net.UDPAddr
}
func (c *linuxBatchingConn) getSendBatch() *sendBatch {
batch := c.sendBatchPool.Get().(*sendBatch)
return batch
}
func (c *linuxBatchingConn) putSendBatch(batch *sendBatch) {
for i := range batch.msgs {
batch.msgs[i] = ipv6.Message{Buffers: batch.msgs[i].Buffers, OOB: batch.msgs[i].OOB}
}
c.sendBatchPool.Put(batch)
}
func (c *linuxBatchingConn) WriteBatchTo(buffs [][]byte, addr netip.AddrPort) error {
batch := c.getSendBatch()
defer c.putSendBatch(batch)
if addr.Addr().Is6() {
as16 := addr.Addr().As16()
copy(batch.ua.IP, as16[:])
batch.ua.IP = batch.ua.IP[:16]
} else {
as4 := addr.Addr().As4()
copy(batch.ua.IP, as4[:])
batch.ua.IP = batch.ua.IP[:4]
}
batch.ua.Port = int(addr.Port())
var (
n int
retried bool
)
retry:
if c.txOffload.Load() {
n = c.coalesceMessages(batch.ua, buffs, batch.msgs)
} else {
for i := range buffs {
batch.msgs[i].Buffers[0] = buffs[i]
batch.msgs[i].Addr = batch.ua
batch.msgs[i].OOB = batch.msgs[i].OOB[:0]
}
n = len(buffs)
}
err := c.writeBatch(batch.msgs[:n])
if err != nil && c.txOffload.Load() && neterror.ShouldDisableUDPGSO(err) {
c.txOffload.Store(false)
retried = true
goto retry
}
if retried {
return neterror.ErrUDPGSODisabled{OnLaddr: c.pc.LocalAddr().String(), RetryErr: err}
}
return err
}
func (c *linuxBatchingConn) SyscallConn() (syscall.RawConn, error) {
sc, ok := c.pc.(syscall.Conn)
if !ok {
return nil, errUnsupportedConnType
}
return sc.SyscallConn()
}
func (c *linuxBatchingConn) writeBatch(msgs []ipv6.Message) error {
var head int
for {
n, err := c.xpc.WriteBatch(msgs[head:], 0)
if err != nil || n == len(msgs[head:]) {
// Returning the number of packets written would require
// unraveling individual msg len and gso size during a coalesced
// write. The top of the call stack disregards partial success,
// so keep this simple for now.
return err
}
head += n
}
}
// splitCoalescedMessages splits coalesced messages from the tail of dst
// beginning at index 'firstMsgAt' into the head of the same slice. It reports
// the number of elements to evaluate in msgs for nonzero len (msgs[i].N). An
// error is returned if a socket control message cannot be parsed or a split
// operation would overflow msgs.
func (c *linuxBatchingConn) splitCoalescedMessages(msgs []ipv6.Message, firstMsgAt int) (n int, err error) {
for i := firstMsgAt; i < len(msgs); i++ {
msg := &msgs[i]
if msg.N == 0 {
return n, err
}
var (
gsoSize int
start int
end = msg.N
numToSplit = 1
)
gsoSize, err = c.getGSOSizeFromControl(msg.OOB[:msg.NN])
if err != nil {
return n, err
}
if gsoSize > 0 {
numToSplit = (msg.N + gsoSize - 1) / gsoSize
end = gsoSize
}
for j := 0; j < numToSplit; j++ {
if n > i {
return n, errors.New("splitting coalesced packet resulted in overflow")
}
copied := copy(msgs[n].Buffers[0], msg.Buffers[0][start:end])
msgs[n].N = copied
msgs[n].Addr = msg.Addr
start = end
end += gsoSize
if end > msg.N {
end = msg.N
}
n++
}
if i != n-1 {
// It is legal for bytes to move within msg.Buffers[0] as a result
// of splitting, so we only zero the source msg len when it is not
// the destination of the last split operation above.
msg.N = 0
}
}
return n, nil
}
func (c *linuxBatchingConn) ReadBatch(msgs []ipv6.Message, flags int) (n int, err error) {
if !c.rxOffload || len(msgs) < 2 {
return c.xpc.ReadBatch(msgs, flags)
}
// Read into the tail of msgs, split into the head.
readAt := len(msgs) - 2
numRead, err := c.xpc.ReadBatch(msgs[readAt:], 0)
if err != nil || numRead == 0 {
return 0, err
}
return c.splitCoalescedMessages(msgs, readAt)
}
func (c *linuxBatchingConn) LocalAddr() net.Addr {
return c.pc.LocalAddr().(*net.UDPAddr)
}
func (c *linuxBatchingConn) WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error) {
return c.pc.WriteToUDPAddrPort(b, addr)
}
func (c *linuxBatchingConn) Close() error {
return c.pc.Close()
}
// tryEnableUDPOffload attempts to enable the UDP_GRO socket option on pconn,
// and returns two booleans indicating TX and RX UDP offload support.
func tryEnableUDPOffload(pconn nettype.PacketConn) (hasTX bool, hasRX bool) {
if c, ok := pconn.(*net.UDPConn); ok {
rc, err := c.SyscallConn()
if err != nil {
return
}
err = rc.Control(func(fd uintptr) {
_, errSyscall := syscall.GetsockoptInt(int(fd), unix.IPPROTO_UDP, unix.UDP_SEGMENT)
hasTX = errSyscall == nil
errSyscall = syscall.SetsockoptInt(int(fd), unix.IPPROTO_UDP, unix.UDP_GRO, 1)
hasRX = errSyscall == nil
})
if err != nil {
return false, false
}
}
return hasTX, hasRX
}
// getGSOSizeFromControl returns the GSO size found in control. If no GSO size
// is found or the len(control) < unix.SizeofCmsghdr, this function returns 0.
// A non-nil error will be returned if len(control) > unix.SizeofCmsghdr but
// its contents cannot be parsed as a socket control message.
func getGSOSizeFromControl(control []byte) (int, error) {
var (
hdr unix.Cmsghdr
data []byte
rem = control
err error
)
for len(rem) > unix.SizeofCmsghdr {
hdr, data, rem, err = unix.ParseOneSocketControlMessage(control)
if err != nil {
return 0, fmt.Errorf("error parsing socket control message: %w", err)
}
if hdr.Level == unix.SOL_UDP && hdr.Type == unix.UDP_GRO && len(data) >= 2 {
return int(binary.NativeEndian.Uint16(data[:2])), nil
}
}
return 0, nil
}
// setGSOSizeInControl sets a socket control message in control containing
// gsoSize. If len(control) < controlMessageSize control's len will be set to 0.
func setGSOSizeInControl(control *[]byte, gsoSize uint16) {
*control = (*control)[:0]
if cap(*control) < int(unsafe.Sizeof(unix.Cmsghdr{})) {
return
}
if cap(*control) < controlMessageSize {
return
}
*control = (*control)[:cap(*control)]
hdr := (*unix.Cmsghdr)(unsafe.Pointer(&(*control)[0]))
hdr.Level = unix.SOL_UDP
hdr.Type = unix.UDP_SEGMENT
hdr.SetLen(unix.CmsgLen(2))
binary.NativeEndian.PutUint16((*control)[unix.SizeofCmsghdr:], gsoSize)
*control = (*control)[:unix.CmsgSpace(2)]
}
// tryUpgradeToBatchingConn probes the capabilities of the OS and pconn, and
// upgrades pconn to a *linuxBatchingConn if appropriate.
func tryUpgradeToBatchingConn(pconn nettype.PacketConn, network string, batchSize int) nettype.PacketConn {
if runtime.GOOS != "linux" {
// Exclude Android.
return pconn
}
if network != "udp4" && network != "udp6" {
return pconn
}
if strings.HasPrefix(hostinfo.GetOSVersion(), "2.") {
// recvmmsg/sendmmsg were added in 2.6.33, but we support down to
// 2.6.32 for old NAS devices. See https://github.com/tailscale/tailscale/issues/6807.
// As a cheap heuristic: if the Linux kernel starts with "2", just
// consider it too old for mmsg. Nobody who cares about performance runs
// such ancient kernels. UDP offload was added much later, so no
// upgrades are available.
return pconn
}
uc, ok := pconn.(*net.UDPConn)
if !ok {
return pconn
}
b := &linuxBatchingConn{
pc: pconn,
getGSOSizeFromControl: getGSOSizeFromControl,
setGSOSizeInControl: setGSOSizeInControl,
sendBatchPool: sync.Pool{
New: func() any {
ua := &net.UDPAddr{
IP: make([]byte, 16),
}
msgs := make([]ipv6.Message, batchSize)
for i := range msgs {
msgs[i].Buffers = make([][]byte, 1)
msgs[i].Addr = ua
msgs[i].OOB = make([]byte, controlMessageSize)
}
return &sendBatch{
ua: ua,
msgs: msgs,
}
},
},
}
switch network {
case "udp4":
b.xpc = ipv4.NewPacketConn(uc)
case "udp6":
b.xpc = ipv6.NewPacketConn(uc)
default:
panic("bogus network")
}
var txOffload bool
txOffload, b.rxOffload = tryEnableUDPOffload(uc)
b.txOffload.Store(txOffload)
return b
}

View File

@@ -0,0 +1,55 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package magicsock
import (
"errors"
"net"
"net/netip"
"sync"
"syscall"
"time"
)
// blockForeverConn is a net.PacketConn whose reads block until it is closed.
type blockForeverConn struct {
mu sync.Mutex
cond *sync.Cond
closed bool
}
func (c *blockForeverConn) ReadFromUDPAddrPort(p []byte) (n int, addr netip.AddrPort, err error) {
c.mu.Lock()
for !c.closed {
c.cond.Wait()
}
c.mu.Unlock()
return 0, netip.AddrPort{}, net.ErrClosed
}
func (c *blockForeverConn) WriteToUDPAddrPort(p []byte, addr netip.AddrPort) (int, error) {
// Silently drop writes.
return len(p), nil
}
func (c *blockForeverConn) LocalAddr() net.Addr {
// Return a *net.UDPAddr because lots of code assumes that it will.
return new(net.UDPAddr)
}
func (c *blockForeverConn) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
if c.closed {
return net.ErrClosed
}
c.closed = true
c.cond.Broadcast()
return nil
}
func (c *blockForeverConn) SetDeadline(t time.Time) error { return errors.New("unimplemented") }
func (c *blockForeverConn) SetReadDeadline(t time.Time) error { return errors.New("unimplemented") }
func (c *blockForeverConn) SetWriteDeadline(t time.Time) error { return errors.New("unimplemented") }
func (c *blockForeverConn) SyscallConn() (syscall.RawConn, error) { return nil, errUnsupportedConnType }

182
vendor/tailscale.com/wgengine/magicsock/cloudinfo.go generated vendored Normal file
View File

@@ -0,0 +1,182 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !(ios || android || js)
package magicsock
import (
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"slices"
"strings"
"time"
"tailscale.com/types/logger"
"tailscale.com/util/cloudenv"
)
const maxCloudInfoWait = 2 * time.Second
type cloudInfo struct {
client http.Client
logf logger.Logf
// The following parameters are fixed for the lifetime of the cloudInfo
// object, but are used for testing.
cloud cloudenv.Cloud
endpoint string
}
func newCloudInfo(logf logger.Logf) *cloudInfo {
tr := &http.Transport{
DisableKeepAlives: true,
Dial: (&net.Dialer{
Timeout: maxCloudInfoWait,
}).Dial,
}
return &cloudInfo{
client: http.Client{Transport: tr},
logf: logf,
cloud: cloudenv.Get(),
endpoint: "http://" + cloudenv.CommonNonRoutableMetadataIP,
}
}
// GetPublicIPs returns any public IPs attached to the current cloud instance,
// if the tailscaled process is running in a known cloud and there are any such
// IPs present.
func (ci *cloudInfo) GetPublicIPs(ctx context.Context) ([]netip.Addr, error) {
switch ci.cloud {
case cloudenv.AWS:
ret, err := ci.getAWS(ctx)
ci.logf("[v1] cloudinfo.GetPublicIPs: AWS: %v, %v", ret, err)
return ret, err
}
return nil, nil
}
// getAWSMetadata makes a request to the AWS metadata service at the given
// path, authenticating with the provided IMDSv2 token. The returned metadata
// is split by newline and returned as a slice.
func (ci *cloudInfo) getAWSMetadata(ctx context.Context, token, path string) ([]string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", ci.endpoint+path, nil)
if err != nil {
return nil, fmt.Errorf("creating request to %q: %w", path, err)
}
req.Header.Set("X-aws-ec2-metadata-token", token)
resp, err := ci.client.Do(req)
if err != nil {
return nil, fmt.Errorf("making request to metadata service %q: %w", path, err)
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
// Good
case http.StatusNotFound:
// Nothing found, but this isn't an error; just return
return nil, nil
default:
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response body for %q: %w", path, err)
}
return strings.Split(strings.TrimSpace(string(body)), "\n"), nil
}
// getAWS returns all public IPv4 and IPv6 addresses present in the AWS instance metadata.
func (ci *cloudInfo) getAWS(ctx context.Context) ([]netip.Addr, error) {
ctx, cancel := context.WithTimeout(ctx, maxCloudInfoWait)
defer cancel()
// Get a token so we can query the metadata service.
req, err := http.NewRequestWithContext(ctx, "PUT", ci.endpoint+"/latest/api/token", nil)
if err != nil {
return nil, fmt.Errorf("creating token request: %w", err)
}
req.Header.Set("X-Aws-Ec2-Metadata-Token-Ttl-Seconds", "10")
resp, err := ci.client.Do(req)
if err != nil {
return nil, fmt.Errorf("making token request to metadata service: %w", err)
}
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("reading token response body: %w", err)
}
token := string(body)
server := resp.Header.Get("Server")
if server != "EC2ws" {
return nil, fmt.Errorf("unexpected server header: %q", server)
}
// Iterate over all interfaces and get their public IP addresses, both IPv4 and IPv6.
macAddrs, err := ci.getAWSMetadata(ctx, token, "/latest/meta-data/network/interfaces/macs/")
if err != nil {
return nil, fmt.Errorf("getting interface MAC addresses: %w", err)
}
var (
addrs []netip.Addr
errs []error
)
addAddr := func(addr string) {
ip, err := netip.ParseAddr(addr)
if err != nil {
errs = append(errs, fmt.Errorf("parsing IP address %q: %w", addr, err))
return
}
addrs = append(addrs, ip)
}
for _, mac := range macAddrs {
ips, err := ci.getAWSMetadata(ctx, token, "/latest/meta-data/network/interfaces/macs/"+mac+"/public-ipv4s")
if err != nil {
errs = append(errs, fmt.Errorf("getting IPv4 addresses for %q: %w", mac, err))
continue
}
for _, ip := range ips {
addAddr(ip)
}
// Try querying for IPv6 addresses.
ips, err = ci.getAWSMetadata(ctx, token, "/latest/meta-data/network/interfaces/macs/"+mac+"/ipv6s")
if err != nil {
errs = append(errs, fmt.Errorf("getting IPv6 addresses for %q: %w", mac, err))
continue
}
for _, ip := range ips {
addAddr(ip)
}
}
// Sort the returned addresses for determinism.
slices.SortFunc(addrs, func(a, b netip.Addr) int {
return a.Compare(b)
})
// Preferentially return any addresses we found, even if there were errors.
if len(addrs) > 0 {
return addrs, nil
}
if len(errs) > 0 {
return nil, fmt.Errorf("getting IP addresses: %w", errors.Join(errs...))
}
return nil, nil
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build ios || android || js
package magicsock
import (
"context"
"net/netip"
"tailscale.com/types/logger"
)
type cloudInfo struct{}
func newCloudInfo(_ logger.Logf) *cloudInfo {
return &cloudInfo{}
}
func (ci *cloudInfo) GetPublicIPs(_ context.Context) ([]netip.Addr, error) {
return nil, nil
}

205
vendor/tailscale.com/wgengine/magicsock/debughttp.go generated vendored Normal file
View File

@@ -0,0 +1,205 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package magicsock
import (
"fmt"
"html"
"io"
"net/http"
"net/netip"
"sort"
"strings"
"time"
"tailscale.com/tailcfg"
"tailscale.com/tstime/mono"
"tailscale.com/types/key"
)
// ServeHTTPDebug serves an HTML representation of the innards of c for debugging.
//
// It's accessible either from tailscaled's debug port (at
// /debug/magicsock) or via peerapi to a peer that's owned by the same
// user (so they can e.g. inspect their phones).
func (c *Conn) ServeHTTPDebug(w http.ResponseWriter, r *http.Request) {
c.mu.Lock()
defer c.mu.Unlock()
now := time.Now()
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(w, "<h1>magicsock</h1>")
fmt.Fprintf(w, "<h2 id=derp><a href=#derp>#</a> DERP</h2><ul>")
if c.derpMap != nil {
type D struct {
regionID int
lastWrite time.Time
createTime time.Time
}
ent := make([]D, 0, len(c.activeDerp))
for rid, ad := range c.activeDerp {
ent = append(ent, D{
regionID: rid,
lastWrite: *ad.lastWrite,
createTime: ad.createTime,
})
}
sort.Slice(ent, func(i, j int) bool {
return ent[i].regionID < ent[j].regionID
})
for _, e := range ent {
r, ok := c.derpMap.Regions[e.regionID]
if !ok {
continue
}
home := ""
if e.regionID == c.myDerp {
home = "🏠"
}
fmt.Fprintf(w, "<li>%s %d - %v: created %v ago, write %v ago</li>\n",
home, e.regionID, html.EscapeString(r.RegionCode),
now.Sub(e.createTime).Round(time.Second),
now.Sub(e.lastWrite).Round(time.Second),
)
}
}
fmt.Fprintf(w, "</ul>\n")
fmt.Fprintf(w, "<h2 id=ipport><a href=#ipport>#</a> ip:port to endpoint</h2><ul>")
{
type kv struct {
ipp netip.AddrPort
pi *peerInfo
}
ent := make([]kv, 0, len(c.peerMap.byIPPort))
for k, v := range c.peerMap.byIPPort {
ent = append(ent, kv{k, v})
}
sort.Slice(ent, func(i, j int) bool { return ipPortLess(ent[i].ipp, ent[j].ipp) })
for _, e := range ent {
ep := e.pi.ep
shortStr := ep.publicKey.ShortString()
fmt.Fprintf(w, "<li>%v: <a href='#%v'>%v</a></li>\n", e.ipp, strings.Trim(shortStr, "[]"), shortStr)
}
}
fmt.Fprintf(w, "</ul>\n")
fmt.Fprintf(w, "<h2 id=bykey><a href=#bykey>#</a> endpoints by key</h2>")
{
type kv struct {
pub key.NodePublic
pi *peerInfo
}
ent := make([]kv, 0, len(c.peerMap.byNodeKey))
for k, v := range c.peerMap.byNodeKey {
ent = append(ent, kv{k, v})
}
sort.Slice(ent, func(i, j int) bool { return ent[i].pub.Less(ent[j].pub) })
peers := map[key.NodePublic]tailcfg.NodeView{}
for i := range c.peers.Len() {
p := c.peers.At(i)
peers[p.Key()] = p
}
for _, e := range ent {
ep := e.pi.ep
shortStr := e.pub.ShortString()
name := peerDebugName(peers[e.pub])
fmt.Fprintf(w, "<h3 id=%v><a href='#%v'>%v</a> - %s</h3>\n",
strings.Trim(shortStr, "[]"),
strings.Trim(shortStr, "[]"),
shortStr,
html.EscapeString(name))
printEndpointHTML(w, ep)
}
}
}
func printEndpointHTML(w io.Writer, ep *endpoint) {
lastRecv := ep.lastRecvWG.LoadAtomic()
ep.mu.Lock()
defer ep.mu.Unlock()
if ep.lastSendExt == 0 && lastRecv == 0 {
return // no activity ever
}
now := time.Now()
mnow := mono.Now()
fmtMono := func(m mono.Time) string {
if m == 0 {
return "-"
}
return mnow.Sub(m).Round(time.Millisecond).String()
}
fmt.Fprintf(w, "<p>Best: <b>%+v</b>, %v ago (for %v)</p>\n", ep.bestAddr, fmtMono(ep.bestAddrAt), ep.trustBestAddrUntil.Sub(mnow).Round(time.Millisecond))
fmt.Fprintf(w, "<p>heartbeating: %v</p>\n", ep.heartBeatTimer != nil)
fmt.Fprintf(w, "<p>lastSend: %v ago</p>\n", fmtMono(ep.lastSendExt))
fmt.Fprintf(w, "<p>lastFullPing: %v ago</p>\n", fmtMono(ep.lastFullPing))
eps := make([]netip.AddrPort, 0, len(ep.endpointState))
for ipp := range ep.endpointState {
eps = append(eps, ipp)
}
sort.Slice(eps, func(i, j int) bool { return ipPortLess(eps[i], eps[j]) })
io.WriteString(w, "<p>Endpoints:</p><ul>")
for _, ipp := range eps {
s := ep.endpointState[ipp]
if ipp == ep.bestAddr.AddrPort {
fmt.Fprintf(w, "<li><b>%s</b>: (best)<ul>", ipp)
} else {
fmt.Fprintf(w, "<li>%s: ...<ul>", ipp)
}
fmt.Fprintf(w, "<li>lastPing: %v ago</li>\n", fmtMono(s.lastPing))
if s.lastGotPing.IsZero() {
fmt.Fprintf(w, "<li>disco-learned-at: -</li>\n")
} else {
fmt.Fprintf(w, "<li>disco-learned-at: %v ago</li>\n", now.Sub(s.lastGotPing).Round(time.Second))
}
fmt.Fprintf(w, "<li>callMeMaybeTime: %v</li>\n", s.callMeMaybeTime)
for i := range s.recentPongs {
if i == 5 {
break
}
pos := (int(s.recentPong) - i) % len(s.recentPongs)
// If s.recentPongs wraps around pos will be negative, so start
// again from the end of the slice.
if pos < 0 {
pos += len(s.recentPongs)
}
pr := s.recentPongs[pos]
fmt.Fprintf(w, "<li>pong %v ago: in %v, from %v src %v</li>\n",
fmtMono(pr.pongAt), pr.latency.Round(time.Millisecond/10),
pr.from, pr.pongSrc)
}
fmt.Fprintf(w, "</ul></li>\n")
}
io.WriteString(w, "</ul>")
}
func peerDebugName(p tailcfg.NodeView) string {
if !p.Valid() {
return ""
}
n := p.Name()
if base, _, ok := strings.Cut(n, "."); ok {
return base
}
return p.Hostinfo().Hostname()
}
func ipPortLess(a, b netip.AddrPort) bool {
if v := a.Addr().Compare(b.Addr()); v != 0 {
return v < 0
}
return a.Port() < b.Port()
}

97
vendor/tailscale.com/wgengine/magicsock/debugknobs.go generated vendored Normal file
View File

@@ -0,0 +1,97 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ios && !js
package magicsock
import (
"log"
"net/netip"
"strings"
"sync"
"tailscale.com/envknob"
)
// Various debugging and experimental tweakables, set by environment
// variable.
var (
// debugDisco prints verbose logs of active discovery events as
// they happen.
debugDisco = envknob.RegisterBool("TS_DEBUG_DISCO")
// debugPeerMap prints verbose logs of changes to the peermap.
debugPeerMap = envknob.RegisterBool("TS_DEBUG_MAGICSOCK_PEERMAP")
// debugOmitLocalAddresses removes all local interface addresses
// from magicsock's discovered local endpoints. Used in some tests.
debugOmitLocalAddresses = envknob.RegisterBool("TS_DEBUG_OMIT_LOCAL_ADDRS")
// logDerpVerbose logs all received DERP packets, including their
// full payload.
logDerpVerbose = envknob.RegisterBool("TS_DEBUG_DERP")
// debugReSTUNStopOnIdle unconditionally enables the "shut down
// STUN if magicsock is idle" behavior that normally only triggers
// on mobile devices, lowers the shutdown interval, and logs more
// verbosely about idle measurements.
debugReSTUNStopOnIdle = envknob.RegisterBool("TS_DEBUG_RESTUN_STOP_ON_IDLE")
// debugAlwaysDERP disables the use of UDP, forcing all peer communication over DERP.
debugAlwaysDERP = envknob.RegisterBool("TS_DEBUG_ALWAYS_USE_DERP")
// debugDERPAddr sets the derp address manually, overriding the DERP map from control.
debugUseDERPAddr = envknob.RegisterString("TS_DEBUG_USE_DERP_ADDR")
// debugDERPUseHTTP tells clients to connect to DERP via HTTP on port 3340 instead of
// HTTPS on 443.
debugUseDERPHTTP = envknob.RegisterBool("TS_DEBUG_USE_DERP_HTTP")
// debugEnableSilentDisco disables the use of heartbeatTimer on the endpoint struct
// and attempts to handle disco silently. See issue #540 for details.
debugEnableSilentDisco = envknob.RegisterBool("TS_DEBUG_ENABLE_SILENT_DISCO")
// debugSendCallMeUnknownPeer sends a CallMeMaybe to a non-existent destination every
// time we send a real CallMeMaybe to test the PeerGoneNotHere logic.
debugSendCallMeUnknownPeer = envknob.RegisterBool("TS_DEBUG_SEND_CALLME_UNKNOWN_PEER")
// debugBindSocket prints extra debugging about socket rebinding in magicsock.
debugBindSocket = envknob.RegisterBool("TS_DEBUG_MAGICSOCK_BIND_SOCKET")
// debugRingBufferMaxSizeBytes overrides the default size of the endpoint
// history ringbuffer.
debugRingBufferMaxSizeBytes = envknob.RegisterInt("TS_DEBUG_MAGICSOCK_RING_BUFFER_MAX_SIZE_BYTES")
// debugEnablePMTUD enables the peer MTU feature, which does path MTU
// discovery on UDP connections between peers. Currently (2023-09-05)
// this only turns on the don't fragment bit for the magicsock UDP
// sockets.
//
//lint:ignore U1000 used on Linux/Darwin only
debugEnablePMTUD = envknob.RegisterOptBool("TS_DEBUG_ENABLE_PMTUD")
// debugPMTUD prints extra debugging about peer MTU path discovery.
//
//lint:ignore U1000 used on Linux/Darwin only
debugPMTUD = envknob.RegisterBool("TS_DEBUG_PMTUD")
// Hey you! Adding a new debugknob? Make sure to stub it out in the
// debugknobs_stubs.go file too.
)
// inTest reports whether the running program is a test that set the
// IN_TS_TEST environment variable.
//
// Unlike the other debug tweakables above, this one needs to be
// checked every time at runtime, because tests set this after program
// startup.
func inTest() bool { return envknob.Bool("IN_TS_TEST") }
// pretendpoints returns TS_DEBUG_PRETENDPOINT as []AddrPort, if set.
// See https://github.com/tailscale/tailscale/issues/12578 and
// https://github.com/tailscale/tailscale/pull/12735.
//
// It can be between 0 and 3 comma-separated AddrPorts.
var pretendpoints = sync.OnceValue(func() (ret []netip.AddrPort) {
all := envknob.String("TS_DEBUG_PRETENDPOINT")
const max = 3
remain := all
for remain != "" && len(ret) < max {
var s string
s, remain, _ = strings.Cut(remain, ",")
ap, err := netip.ParseAddrPort(s)
if err != nil {
log.Printf("ignoring invalid AddrPort %q in TS_DEBUG_PRETENDPOINT %q: %v", s, all, err)
continue
}
ret = append(ret, ap)
}
return
})

View File

@@ -0,0 +1,33 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build ios || js
package magicsock
import (
"net/netip"
"tailscale.com/types/opt"
)
// All knobs are disabled on iOS and Wasm.
//
// They're inlinable and the linker can deadcode that's guarded by them to make
// smaller binaries.
func debugBindSocket() bool { return false }
func debugDisco() bool { return false }
func debugOmitLocalAddresses() bool { return false }
func logDerpVerbose() bool { return false }
func debugReSTUNStopOnIdle() bool { return false }
func debugAlwaysDERP() bool { return false }
func debugUseDERPHTTP() bool { return false }
func debugEnableSilentDisco() bool { return false }
func debugSendCallMeUnknownPeer() bool { return false }
func debugPMTUD() bool { return false }
func debugUseDERPAddr() string { return "" }
func debugEnablePMTUD() opt.Bool { return "" }
func debugRingBufferMaxSizeBytes() int { return 0 }
func inTest() bool { return false }
func debugPeerMap() bool { return false }
func pretendpoints() []netip.AddrPort { return []netip.AddrPort{} }

1004
vendor/tailscale.com/wgengine/magicsock/derp.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by "stringer -type=discoPingPurpose -trimprefix=ping"; DO NOT EDIT.
package magicsock
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[pingDiscovery-0]
_ = x[pingHeartbeat-1]
_ = x[pingCLI-2]
_ = x[pingHeartbeatForUDPLifetime-3]
}
const _discoPingPurpose_name = "DiscoveryHeartbeatCLIHeartbeatForUDPLifetime"
var _discoPingPurpose_index = [...]uint8{0, 9, 18, 21, 44}
func (i discoPingPurpose) String() string {
if i < 0 || i >= discoPingPurpose(len(_discoPingPurpose_index)-1) {
return "discoPingPurpose(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _discoPingPurpose_name[_discoPingPurpose_index[i]:_discoPingPurpose_index[i+1]]
}

1855
vendor/tailscale.com/wgengine/magicsock/endpoint.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !js && !wasm && !plan9
package magicsock
import (
"errors"
"syscall"
)
// errHOSTUNREACH wraps unix.EHOSTUNREACH in an interface type to pass to
// errors.Is while avoiding an allocation per call.
var errHOSTUNREACH error = syscall.EHOSTUNREACH
// isBadEndpointErr checks if err is one which is known to report that an
// endpoint can no longer be sent to. It is not exhaustive, and for unknown
// errors always reports false.
func isBadEndpointErr(err error) bool {
return errors.Is(err, errHOSTUNREACH)
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build wasm || plan9
package magicsock
// isBadEndpointErr checks if err is one which is known to report that an
// endpoint can no longer be sent to. It is not exhaustive, but covers known
// cases.
func isBadEndpointErr(err error) bool {
return false
}

View File

@@ -0,0 +1,248 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package magicsock
import (
"net/netip"
"slices"
"sync"
"time"
"tailscale.com/tailcfg"
"tailscale.com/tempfork/heap"
"tailscale.com/util/mak"
"tailscale.com/util/set"
)
const (
// endpointTrackerLifetime is how long we continue advertising an
// endpoint after we last see it. This is intentionally chosen to be
// slightly longer than a full netcheck period.
endpointTrackerLifetime = 5*time.Minute + 10*time.Second
// endpointTrackerMaxPerAddr is how many cached addresses we track for
// a given netip.Addr. This allows e.g. restricting the number of STUN
// endpoints we cache (which usually have the same netip.Addr but
// different ports).
//
// The value of 6 is chosen because we can advertise up to 3 endpoints
// based on the STUN IP:
// 1. The STUN endpoint itself (EndpointSTUN)
// 2. The STUN IP with the local Tailscale port (EndpointSTUN4LocalPort)
// 3. The STUN IP with a portmapped port (EndpointPortmapped)
//
// Storing 6 endpoints in the cache means we can store up to 2 previous
// sets of endpoints.
endpointTrackerMaxPerAddr = 6
)
// endpointTrackerEntry is an entry in an endpointHeap that stores the state of
// a given cached endpoint.
type endpointTrackerEntry struct {
// endpoint is the cached endpoint.
endpoint tailcfg.Endpoint
// until is the time until which this endpoint is being cached.
until time.Time
// index is the index within the containing endpointHeap.
index int
}
// endpointHeap is an ordered heap of endpointTrackerEntry structs, ordered in
// ascending order by the 'until' expiry time (i.e. oldest first).
type endpointHeap []*endpointTrackerEntry
var _ heap.Interface[*endpointTrackerEntry] = (*endpointHeap)(nil)
// Len implements heap.Interface.
func (eh endpointHeap) Len() int { return len(eh) }
// Less implements heap.Interface.
func (eh endpointHeap) Less(i, j int) bool {
// We want to store items so that the lowest item in the heap is the
// oldest, so that heap.Pop()-ing from the endpointHeap will remove the
// oldest entry.
return eh[i].until.Before(eh[j].until)
}
// Swap implements heap.Interface.
func (eh endpointHeap) Swap(i, j int) {
eh[i], eh[j] = eh[j], eh[i]
eh[i].index = i
eh[j].index = j
}
// Push implements heap.Interface.
func (eh *endpointHeap) Push(item *endpointTrackerEntry) {
n := len(*eh)
item.index = n
*eh = append(*eh, item)
}
// Pop implements heap.Interface.
func (eh *endpointHeap) Pop() *endpointTrackerEntry {
old := *eh
n := len(old)
item := old[n-1]
old[n-1] = nil // avoid memory leak
item.index = -1 // for safety
*eh = old[0 : n-1]
return item
}
// Min returns a pointer to the minimum element in the heap, without removing
// it. Since this is a min-heap ordered by the 'until' field, this returns the
// chronologically "earliest" element in the heap.
//
// Len() must be non-zero.
func (eh endpointHeap) Min() *endpointTrackerEntry {
return eh[0]
}
// endpointTracker caches endpoints that are advertised to peers. This allows
// peers to still reach this node if there's a temporary endpoint flap; rather
// than withdrawing an endpoint and then re-advertising it the next time we run
// a netcheck, we keep advertising the endpoint until it's not present for a
// defined timeout.
//
// See tailscale/tailscale#7877 for more information.
type endpointTracker struct {
mu sync.Mutex
endpoints map[netip.Addr]*endpointHeap
}
// update takes as input the current sent of discovered endpoints and the
// current time, and returns the set of endpoints plus any previous-cached and
// non-expired endpoints that should be advertised to peers.
func (et *endpointTracker) update(now time.Time, eps []tailcfg.Endpoint) (epsPlusCached []tailcfg.Endpoint) {
var inputEps set.Slice[netip.AddrPort]
for _, ep := range eps {
inputEps.Add(ep.Addr)
}
et.mu.Lock()
defer et.mu.Unlock()
// Extend endpoints that already exist in the cache. We do this before
// we remove expired endpoints, below, so we don't remove something
// that would otherwise have survived by extending.
until := now.Add(endpointTrackerLifetime)
for _, ep := range eps {
et.extendLocked(ep, until)
}
// Now that we've extended existing endpoints, remove everything that
// has expired.
et.removeExpiredLocked(now)
// Add entries from the input set of endpoints into the cache; we do
// this after removing expired ones so that we can store as many as
// possible, with space freed by the entries removed after expiry.
for _, ep := range eps {
et.addLocked(now, ep, until)
}
// Finally, add entries to the return array that aren't already there.
epsPlusCached = eps
for _, heap := range et.endpoints {
for _, ep := range *heap {
// If the endpoint was in the input list, or has expired, skip it.
if inputEps.Contains(ep.endpoint.Addr) {
continue
} else if now.After(ep.until) {
// Defense-in-depth; should never happen since
// we removed expired entries above, but ignore
// it anyway.
continue
}
// We haven't seen this endpoint; add to the return array
epsPlusCached = append(epsPlusCached, ep.endpoint)
}
}
return epsPlusCached
}
// extendLocked will update the expiry time of the provided endpoint in the
// cache, if it is present. If it is not present, nothing will be done.
//
// et.mu must be held.
func (et *endpointTracker) extendLocked(ep tailcfg.Endpoint, until time.Time) {
key := ep.Addr.Addr()
epHeap, found := et.endpoints[key]
if !found {
return
}
// Find the entry for this exact address; this loop is quick since we
// bound the number of items in the heap.
//
// TODO(andrew): this means we iterate over the entire heap once per
// endpoint; even if the heap is small, if we have a lot of input
// endpoints this can be expensive?
for i, entry := range *epHeap {
if entry.endpoint == ep {
entry.until = until
heap.Fix(epHeap, i)
return
}
}
}
// addLocked will store the provided endpoint(s) in the cache for a fixed
// period of time, ensuring that the size of the endpoint cache remains below
// the maximum.
//
// et.mu must be held.
func (et *endpointTracker) addLocked(now time.Time, ep tailcfg.Endpoint, until time.Time) {
key := ep.Addr.Addr()
// Create or get the heap for this endpoint's addr
epHeap := et.endpoints[key]
if epHeap == nil {
epHeap = new(endpointHeap)
mak.Set(&et.endpoints, key, epHeap)
}
// Find the entry for this exact address; this loop is quick
// since we bound the number of items in the heap.
found := slices.ContainsFunc(*epHeap, func(v *endpointTrackerEntry) bool {
return v.endpoint == ep
})
if !found {
// Add address to heap; either the endpoint is new, or the heap
// was newly-created and thus empty.
heap.Push(epHeap, &endpointTrackerEntry{endpoint: ep, until: until})
}
// Now that we've added everything, pop from our heap until we're below
// the limit. This is a min-heap, so popping removes the lowest (and
// thus oldest) endpoint.
for epHeap.Len() > endpointTrackerMaxPerAddr {
heap.Pop(epHeap)
}
}
// removeExpired will remove all expired entries from the cache.
//
// et.mu must be held.
func (et *endpointTracker) removeExpiredLocked(now time.Time) {
for k, epHeap := range et.endpoints {
// The minimum element is oldest/earliest endpoint; repeatedly
// pop from the heap while it's in the past.
for epHeap.Len() > 0 {
minElem := epHeap.Min()
if now.After(minElem.until) {
heap.Pop(epHeap)
} else {
break
}
}
if epHeap.Len() == 0 {
// Free up space in the map by removing the empty heap.
delete(et.endpoints, k)
}
}
}

3071
vendor/tailscale.com/wgengine/magicsock/magicsock.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !linux
package magicsock
import (
"errors"
"fmt"
"io"
"tailscale.com/types/logger"
"tailscale.com/types/nettype"
)
func (c *Conn) listenRawDisco(family string) (io.Closer, error) {
return nil, fmt.Errorf("raw disco listening not supported on this OS: %w", errors.ErrUnsupported)
}
func trySetSocketBuffer(pconn nettype.PacketConn, logf logger.Logf) {
portableTrySetSocketBuffer(pconn, logf)
}
const (
controlMessageSize = 0
)

View File

@@ -0,0 +1,520 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package magicsock
import (
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"net/netip"
"strings"
"syscall"
"time"
"github.com/mdlayher/socket"
"golang.org/x/net/bpf"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
"golang.org/x/sys/cpu"
"golang.org/x/sys/unix"
"tailscale.com/disco"
"tailscale.com/envknob"
"tailscale.com/net/netns"
"tailscale.com/types/ipproto"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/nettype"
)
const (
udpHeaderSize = 8
// discoMinHeaderSize is the minimum size of the disco header in bytes.
discoMinHeaderSize = len(disco.Magic) + 32 /* key length */ + disco.NonceLen
)
var (
// Opt-in for using raw sockets to receive disco traffic; added for
// #13140 and replaces the older "TS_DEBUG_DISABLE_RAW_DISCO".
envknobEnableRawDisco = envknob.RegisterBool("TS_ENABLE_RAW_DISCO")
)
// debugRawDiscoReads enables logging of raw disco reads.
var debugRawDiscoReads = envknob.RegisterBool("TS_DEBUG_RAW_DISCO")
// These are our BPF filters that we use for testing packets.
var (
magicsockFilterV4 = []bpf.Instruction{
// For raw sockets (with ETH_P_IP set), the BPF program
// receives the entire IPv4 packet, but not the Ethernet
// header.
// Double-check that this is a UDP packet; we shouldn't be
// seeing anything else given how we create our AF_PACKET
// socket, but an extra check here is cheap, and matches the
// check that we do in the IPv6 path.
bpf.LoadAbsolute{Off: 9, Size: 1},
bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(ipproto.UDP), SkipTrue: 1, SkipFalse: 0},
bpf.RetConstant{Val: 0x0},
// Disco packets are so small they should never get
// fragmented, and we don't want to handle reassembly.
bpf.LoadAbsolute{Off: 6, Size: 2},
// More Fragments bit set means this is part of a fragmented packet.
bpf.JumpIf{Cond: bpf.JumpBitsSet, Val: 0x2000, SkipTrue: 7, SkipFalse: 0},
// Non-zero fragment offset with MF=0 means this is the last
// fragment of packet.
bpf.JumpIf{Cond: bpf.JumpBitsSet, Val: 0x1fff, SkipTrue: 6, SkipFalse: 0},
// Load IP header length into X register.
bpf.LoadMemShift{Off: 0},
// Verify that we have a packet that's big enough to (possibly)
// contain a disco packet.
//
// The length of an IPv4 disco packet is composed of:
// - 8 bytes for the UDP header
// - N bytes for the disco packet header
//
// bpf will implicitly return 0 ("skip") if attempting an
// out-of-bounds load, so we can check the length of the packet
// loading a byte from that offset here. We subtract 1 byte
// from the offset to ensure that we accept a packet that's
// exactly the minimum size.
//
// We use LoadIndirect; since we loaded the start of the packet's
// payload into the X register, above, we don't need to add
// ipv4.HeaderLen to the offset (and this properly handles IPv4
// extensions).
bpf.LoadIndirect{Off: uint32(udpHeaderSize + discoMinHeaderSize - 1), Size: 1},
// Get the first 4 bytes of the UDP packet, compare with our magic number
bpf.LoadIndirect{Off: udpHeaderSize, Size: 4},
bpf.JumpIf{Cond: bpf.JumpEqual, Val: discoMagic1, SkipTrue: 0, SkipFalse: 3},
// Compare the next 2 bytes
bpf.LoadIndirect{Off: udpHeaderSize + 4, Size: 2},
bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(discoMagic2), SkipTrue: 0, SkipFalse: 1},
// Accept the whole packet
bpf.RetConstant{Val: 0xFFFFFFFF},
// Skip the packet
bpf.RetConstant{Val: 0x0},
}
// IPv6 is more complicated to filter, since we can have 0-to-N
// extension headers following the IPv6 header. Since BPF can't
// loop, we can't really parse these in a general way; instead, we
// simply handle the case where we have no extension headers; any
// packets with headers will be skipped. IPv6 extension headers
// are sufficiently uncommon that we're willing to accept false
// negatives here.
//
// The "proper" way to handle this would be to do minimal parsing in
// BPF and more in-depth parsing of all IPv6 packets in userspace, but
// on systems with a high volume of UDP that would be unacceptably slow
// and thus we'd rather be conservative here and possibly not receive
// disco packets rather than slow down the system.
magicsockFilterV6 = []bpf.Instruction{
// Do a bounds check to ensure we have enough space for a disco
// packet; see the comment in the IPv4 BPF program for more
// details.
bpf.LoadAbsolute{Off: uint32(ipv6.HeaderLen + udpHeaderSize + discoMinHeaderSize - 1), Size: 1},
// Verify that the 'next header' value of the IPv6 packet is
// UDP, which is what we're expecting; if it's anything else
// (including extension headers), we skip the packet.
bpf.LoadAbsolute{Off: 6, Size: 1},
bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(ipproto.UDP), SkipTrue: 0, SkipFalse: 5},
// Compare with our magic number. Start by loading and
// comparing the first 4 bytes of the UDP payload.
bpf.LoadAbsolute{Off: ipv6.HeaderLen + udpHeaderSize, Size: 4},
bpf.JumpIf{Cond: bpf.JumpEqual, Val: discoMagic1, SkipTrue: 0, SkipFalse: 3},
// Compare the next 2 bytes
bpf.LoadAbsolute{Off: ipv6.HeaderLen + udpHeaderSize + 4, Size: 2},
bpf.JumpIf{Cond: bpf.JumpEqual, Val: discoMagic2, SkipTrue: 0, SkipFalse: 1},
// Accept the whole packet
bpf.RetConstant{Val: 0xFFFFFFFF},
// Skip the packet
bpf.RetConstant{Val: 0x0},
}
testDiscoPacket = []byte{
// Disco magic
0x54, 0x53, 0xf0, 0x9f, 0x92, 0xac,
// Sender key
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
// Nonce
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
}
)
// listenRawDisco starts listening for disco packets on the given
// address family, which must be "ip4" or "ip6", using a raw socket
// and BPF filter.
// https://github.com/tailscale/tailscale/issues/3824
func (c *Conn) listenRawDisco(family string) (io.Closer, error) {
if !envknobEnableRawDisco() {
// Return an 'errors.ErrUnsupported' to prevent the callee from
// logging; when we switch this to an opt-out (vs. an opt-in),
// drop the ErrUnsupported so that the callee logs that it was
// disabled.
return nil, fmt.Errorf("raw disco not enabled: %w", errors.ErrUnsupported)
}
// https://github.com/tailscale/tailscale/issues/5607
if !netns.UseSocketMark() {
return nil, errors.New("raw disco listening disabled, SO_MARK unavailable")
}
var (
udpnet string
addr string
proto int
testAddr netip.AddrPort
prog []bpf.Instruction
)
switch family {
case "ip4":
udpnet = "udp4"
addr = "0.0.0.0"
proto = ethernetProtoIPv4()
testAddr = netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), 1)
prog = magicsockFilterV4
case "ip6":
udpnet = "udp6"
addr = "::"
proto = ethernetProtoIPv6()
testAddr = netip.AddrPortFrom(netip.IPv6Loopback(), 1)
prog = magicsockFilterV6
default:
return nil, fmt.Errorf("unsupported address family %q", family)
}
asm, err := bpf.Assemble(prog)
if err != nil {
return nil, fmt.Errorf("assembling filter: %w", err)
}
sock, err := socket.Socket(
unix.AF_PACKET,
unix.SOCK_DGRAM,
proto,
"afpacket",
nil, // no config
)
if err != nil {
return nil, fmt.Errorf("creating AF_PACKET socket: %w", err)
}
if err := sock.SetBPF(asm); err != nil {
sock.Close()
return nil, fmt.Errorf("installing BPF filter: %w", err)
}
// If all the above succeeds, we should be ready to receive. Just
// out of paranoia, check that we do receive a well-formed disco
// packet.
tc, err := net.ListenPacket(udpnet, net.JoinHostPort(addr, "0"))
if err != nil {
sock.Close()
return nil, fmt.Errorf("creating disco test socket: %w", err)
}
defer tc.Close()
if _, err := tc.(*net.UDPConn).WriteToUDPAddrPort(testDiscoPacket, testAddr); err != nil {
sock.Close()
return nil, fmt.Errorf("writing disco test packet: %w", err)
}
const selfTestTimeout = 100 * time.Millisecond
if err := sock.SetReadDeadline(time.Now().Add(selfTestTimeout)); err != nil {
sock.Close()
return nil, fmt.Errorf("setting socket timeout: %w", err)
}
var (
ctx = context.Background()
buf [1500]byte
)
for {
n, _, err := sock.Recvfrom(ctx, buf[:], 0)
if err != nil {
sock.Close()
return nil, fmt.Errorf("reading during raw disco self-test: %w", err)
}
_ /* src */, _ /* dst */, payload := parseUDPPacket(buf[:n], family == "ip6")
if payload == nil {
continue
}
if !bytes.Equal(payload, testDiscoPacket) {
c.discoLogf("listenRawDisco: self-test: received mismatched UDP packet of %d bytes", len(payload))
continue
}
c.logf("[v1] listenRawDisco: self-test passed for %s", family)
break
}
sock.SetReadDeadline(time.Time{})
go c.receiveDisco(sock, family == "ip6")
return sock, nil
}
// parseUDPPacket is a basic parser for UDP packets that returns the source and
// destination addresses, and the payload. The returned payload is a sub-slice
// of the input buffer.
//
// It expects to be called with a buffer that contains the entire UDP packet,
// including the IP header, and one that has been filtered with the BPF
// programs above.
//
// If an error occurs, it will return the zero values for all return values.
func parseUDPPacket(buf []byte, isIPv6 bool) (src, dst netip.AddrPort, payload []byte) {
// First, parse the IPv4 or IPv6 header to get to the UDP header. Since
// we assume this was filtered with BPF, we know that there will be no
// IPv6 extension headers.
var (
srcIP, dstIP netip.Addr
udp []byte
)
if isIPv6 {
// Basic length check to ensure that we don't panic
if len(buf) < ipv6.HeaderLen+udpHeaderSize {
return
}
// Extract the source and destination addresses from the IPv6
// header.
srcIP, _ = netip.AddrFromSlice(buf[8:24])
dstIP, _ = netip.AddrFromSlice(buf[24:40])
// We know that the UDP packet starts immediately after the IPv6
// packet.
udp = buf[ipv6.HeaderLen:]
} else {
// This is an IPv4 packet; read the length field from the header.
if len(buf) < ipv4.HeaderLen {
return
}
udpOffset := int((buf[0] & 0x0F) << 2)
if udpOffset+udpHeaderSize > len(buf) {
return
}
// Parse the source and destination IPs.
srcIP, _ = netip.AddrFromSlice(buf[12:16])
dstIP, _ = netip.AddrFromSlice(buf[16:20])
udp = buf[udpOffset:]
}
// Parse the ports
srcPort := binary.BigEndian.Uint16(udp[0:2])
dstPort := binary.BigEndian.Uint16(udp[2:4])
// The payload starts after the UDP header.
payload = udp[8:]
return netip.AddrPortFrom(srcIP, srcPort), netip.AddrPortFrom(dstIP, dstPort), payload
}
// ethernetProtoIPv4 returns the constant unix.ETH_P_IP, in network byte order.
// packet(7) sockets require that the 'protocol' argument be in network byte
// order; see:
//
// https://man7.org/linux/man-pages/man7/packet.7.html
//
// Instead of using htons at runtime, we can just hardcode the value here...
// but we also have a test that verifies that this is correct.
func ethernetProtoIPv4() int {
if cpu.IsBigEndian {
return 0x0800
} else {
return 0x0008
}
}
// ethernetProtoIPv6 returns the constant unix.ETH_P_IPV6, and is otherwise the
// same as ethernetProtoIPv4.
func ethernetProtoIPv6() int {
if cpu.IsBigEndian {
return 0x86dd
} else {
return 0xdd86
}
}
func (c *Conn) discoLogf(format string, args ...any) {
// Enable debug logging if we're debugging raw disco reads or if the
// magicsock component logs are on.
if debugRawDiscoReads() {
c.logf(format, args...)
} else {
c.dlogf(format, args...)
}
}
func (c *Conn) receiveDisco(pc *socket.Conn, isIPV6 bool) {
// Given that we're parsing raw packets, be extra careful and recover
// from any panics in this function.
//
// If we didn't have a recover() here and panic'd, we'd take down the
// entire process since this function is the top of a goroutine, and Go
// will kill the process if a goroutine panics and it unwinds past the
// top-level function.
defer func() {
if err := recover(); err != nil {
c.logf("[unexpected] recovered from panic in receiveDisco(isIPv6=%v): %v", isIPV6, err)
}
}()
ctx := context.Background()
// Set up our loggers
var family string
if isIPV6 {
family = "ip6"
} else {
family = "ip4"
}
var (
prefix string = "disco raw " + family + ": "
logf logger.Logf = logger.WithPrefix(c.logf, prefix)
dlogf logger.Logf = logger.WithPrefix(c.discoLogf, prefix)
)
var buf [1500]byte
for {
n, src, err := pc.Recvfrom(ctx, buf[:], 0)
if debugRawDiscoReads() {
logf("read from %s = (%v, %v)", printSockaddr(src), n, err)
}
if err != nil && (errors.Is(err, net.ErrClosed) || err.Error() == "use of closed file") {
// EOF; no need to print an error
return
} else if err != nil {
logf("reader failed: %v", err)
return
}
srcAddr, dstAddr, payload := parseUDPPacket(buf[:n], family == "ip6")
if payload == nil {
// callee logged
continue
}
dstPort := dstAddr.Port()
if dstPort == 0 {
logf("[unexpected] received packet for port 0")
}
var acceptPort uint16
if isIPV6 {
acceptPort = c.pconn6.Port()
} else {
acceptPort = c.pconn4.Port()
}
if acceptPort == 0 {
// This should only typically happen if the receiving address family
// was recently disabled.
dlogf("[v1] dropping packet for port %d as acceptPort=0", dstPort)
continue
}
// If the packet isn't destined for our local port, then we
// should drop it since it might be for another Tailscale
// process on the same machine, or NATed to a different machine
// if this is a router, etc.
//
// We get the local port to compare against inside the receive
// loop; we can't cache this beforehand because it can change
// if/when we rebind.
if dstPort != acceptPort {
dlogf("[v1] dropping packet for port %d that isn't our local port", dstPort)
continue
}
if isIPV6 {
metricRecvDiscoPacketIPv6.Add(1)
} else {
metricRecvDiscoPacketIPv4.Add(1)
}
c.handleDiscoMessage(payload, srcAddr, key.NodePublic{}, discoRXPathRawSocket)
}
}
// printSockaddr is a helper function to pretty-print various sockaddr types.
func printSockaddr(sa unix.Sockaddr) string {
switch sa := sa.(type) {
case *unix.SockaddrInet4:
addr := netip.AddrFrom4(sa.Addr)
return netip.AddrPortFrom(addr, uint16(sa.Port)).String()
case *unix.SockaddrInet6:
addr := netip.AddrFrom16(sa.Addr)
return netip.AddrPortFrom(addr, uint16(sa.Port)).String()
case *unix.SockaddrLinklayer:
hwaddr := sa.Addr[:sa.Halen]
var buf strings.Builder
fmt.Fprintf(&buf, "link(ty=0x%04x,if=%d):[", sa.Protocol, sa.Ifindex)
for i, b := range hwaddr {
if i > 0 {
buf.WriteByte(':')
}
fmt.Fprintf(&buf, "%02x", b)
}
buf.WriteByte(']')
return buf.String()
default:
return fmt.Sprintf("unknown(%T)", sa)
}
}
// trySetSocketBuffer attempts to set SO_SNDBUFFORCE and SO_RECVBUFFORCE which
// can overcome the limit of net.core.{r,w}mem_max, but require CAP_NET_ADMIN.
// It falls back to the portable implementation if that fails, which may be
// silently capped to net.core.{r,w}mem_max.
func trySetSocketBuffer(pconn nettype.PacketConn, logf logger.Logf) {
if c, ok := pconn.(*net.UDPConn); ok {
var errRcv, errSnd error
rc, err := c.SyscallConn()
if err == nil {
rc.Control(func(fd uintptr) {
errRcv = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_RCVBUFFORCE, socketBufferSize)
if errRcv != nil {
logf("magicsock: [warning] failed to force-set UDP read buffer size to %d: %v; using kernel default values (impacts throughput only)", socketBufferSize, errRcv)
}
errSnd = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_SNDBUFFORCE, socketBufferSize)
if errSnd != nil {
logf("magicsock: [warning] failed to force-set UDP write buffer size to %d: %v; using kernel default values (impacts throughput only)", socketBufferSize, errSnd)
}
})
}
if err != nil || errRcv != nil || errSnd != nil {
portableTrySetSocketBuffer(pconn, logf)
}
}
}
var controlMessageSize = -1 // bomb if used for allocation before init
func init() {
// controlMessageSize is set to hold a UDP_GRO or UDP_SEGMENT control
// message. These contain a single uint16 of data.
controlMessageSize = unix.CmsgSpace(2)
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !windows
package magicsock
import (
"tailscale.com/types/logger"
"tailscale.com/types/nettype"
)
func trySetUDPSocketOptions(pconn nettype.PacketConn, logf logger.Logf) {}

View File

@@ -0,0 +1,58 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build windows
package magicsock
import (
"net"
"unsafe"
"golang.org/x/sys/windows"
"tailscale.com/types/logger"
"tailscale.com/types/nettype"
)
func trySetUDPSocketOptions(pconn nettype.PacketConn, logf logger.Logf) {
c, ok := pconn.(*net.UDPConn)
if !ok {
// not a UDP connection; nothing to do
return
}
sysConn, err := c.SyscallConn()
if err != nil {
logf("trySetUDPSocketOptions: getting SyscallConn failed: %v", err)
return
}
// Similar to https://github.com/golang/go/issues/5834 (which involved
// WSAECONNRESET), Windows can return a WSAENETRESET error, even on UDP
// reads. Disable this.
const SIO_UDP_NETRESET = windows.IOC_IN | windows.IOC_VENDOR | 15
var ioctlErr error
err = sysConn.Control(func(fd uintptr) {
ret := uint32(0)
flag := uint32(0)
size := uint32(unsafe.Sizeof(flag))
ioctlErr = windows.WSAIoctl(
windows.Handle(fd),
SIO_UDP_NETRESET, // iocc
(*byte)(unsafe.Pointer(&flag)), // inbuf
size, // cbif
nil, // outbuf
0, // cbob
&ret, // cbbr
nil, // overlapped
0, // completionRoutine
)
})
if ioctlErr != nil {
logf("trySetUDPSocketOptions: could not set SIO_UDP_NETRESET: %v", ioctlErr)
}
if err != nil {
logf("trySetUDPSocketOptions: SyscallConn.Control failed: %v", err)
}
}

209
vendor/tailscale.com/wgengine/magicsock/peermap.go generated vendored Normal file
View File

@@ -0,0 +1,209 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package magicsock
import (
"net/netip"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/util/set"
)
// peerInfo is all the information magicsock tracks about a particular
// peer.
type peerInfo struct {
ep *endpoint // always non-nil.
// ipPorts is an inverted version of peerMap.byIPPort (below), so
// that when we're deleting this node, we can rapidly find out the
// keys that need deleting from peerMap.byIPPort without having to
// iterate over every IPPort known for any peer.
ipPorts set.Set[netip.AddrPort]
}
func newPeerInfo(ep *endpoint) *peerInfo {
return &peerInfo{
ep: ep,
ipPorts: set.Set[netip.AddrPort]{},
}
}
// peerMap is an index of peerInfos by node (WireGuard) key, disco
// key, and discovered ip:port endpoints.
//
// It doesn't do any locking; all access must be done with Conn.mu held.
type peerMap struct {
byNodeKey map[key.NodePublic]*peerInfo
byIPPort map[netip.AddrPort]*peerInfo
byNodeID map[tailcfg.NodeID]*peerInfo
// nodesOfDisco contains the set of nodes that are using a
// DiscoKey. Usually those sets will be just one node.
nodesOfDisco map[key.DiscoPublic]set.Set[key.NodePublic]
}
func newPeerMap() peerMap {
return peerMap{
byNodeKey: map[key.NodePublic]*peerInfo{},
byIPPort: map[netip.AddrPort]*peerInfo{},
byNodeID: map[tailcfg.NodeID]*peerInfo{},
nodesOfDisco: map[key.DiscoPublic]set.Set[key.NodePublic]{},
}
}
// nodeCount returns the number of nodes currently in m.
func (m *peerMap) nodeCount() int {
if len(m.byNodeKey) != len(m.byNodeID) {
devPanicf("internal error: peerMap.byNodeKey and byNodeID out of sync")
}
return len(m.byNodeKey)
}
// knownPeerDiscoKey reports whether there exists any peer with the disco key
// dk.
func (m *peerMap) knownPeerDiscoKey(dk key.DiscoPublic) bool {
_, ok := m.nodesOfDisco[dk]
return ok
}
// endpointForNodeKey returns the endpoint for nk, or nil if
// nk is not known to us.
func (m *peerMap) endpointForNodeKey(nk key.NodePublic) (ep *endpoint, ok bool) {
if nk.IsZero() {
return nil, false
}
if info, ok := m.byNodeKey[nk]; ok {
return info.ep, true
}
return nil, false
}
// endpointForNodeID returns the endpoint for nodeID, or nil if
// nodeID is not known to us.
func (m *peerMap) endpointForNodeID(nodeID tailcfg.NodeID) (ep *endpoint, ok bool) {
if info, ok := m.byNodeID[nodeID]; ok {
return info.ep, true
}
return nil, false
}
// endpointForIPPort returns the endpoint for the peer we
// believe to be at ipp, or nil if we don't know of any such peer.
func (m *peerMap) endpointForIPPort(ipp netip.AddrPort) (ep *endpoint, ok bool) {
if info, ok := m.byIPPort[ipp]; ok {
return info.ep, true
}
return nil, false
}
// forEachEndpoint invokes f on every endpoint in m.
func (m *peerMap) forEachEndpoint(f func(ep *endpoint)) {
for _, pi := range m.byNodeKey {
f(pi.ep)
}
}
// forEachEndpointWithDiscoKey invokes f on every endpoint in m that has the
// provided DiscoKey until f returns false or there are no endpoints left to
// iterate.
func (m *peerMap) forEachEndpointWithDiscoKey(dk key.DiscoPublic, f func(*endpoint) (keepGoing bool)) {
for nk := range m.nodesOfDisco[dk] {
pi, ok := m.byNodeKey[nk]
if !ok {
// Unexpected. Data structures would have to
// be out of sync. But we don't have a logger
// here to log [unexpected], so just skip.
// Maybe log later once peerMap is merged back
// into Conn.
continue
}
if !f(pi.ep) {
return
}
}
}
// upsertEndpoint stores endpoint in the peerInfo for
// ep.publicKey, and updates indexes. m must already have a
// tailcfg.Node for ep.publicKey.
func (m *peerMap) upsertEndpoint(ep *endpoint, oldDiscoKey key.DiscoPublic) {
if ep.nodeID == 0 {
panic("internal error: upsertEndpoint called with zero NodeID")
}
pi, ok := m.byNodeKey[ep.publicKey]
if !ok {
pi = newPeerInfo(ep)
m.byNodeKey[ep.publicKey] = pi
}
m.byNodeID[ep.nodeID] = pi
epDisco := ep.disco.Load()
if epDisco == nil || oldDiscoKey != epDisco.key {
delete(m.nodesOfDisco[oldDiscoKey], ep.publicKey)
}
if ep.isWireguardOnly {
// If the peer is a WireGuard only peer, add all of its endpoints.
// TODO(raggi,catzkorn): this could mean that if a "isWireguardOnly"
// peer has, say, 192.168.0.2 and so does a tailscale peer, the
// wireguard one will win. That may not be the outcome that we want -
// perhaps we should prefer bestAddr.AddrPort if it is set?
// see tailscale/tailscale#7994
for ipp := range ep.endpointState {
m.setNodeKeyForIPPort(ipp, ep.publicKey)
}
return
}
discoSet := m.nodesOfDisco[epDisco.key]
if discoSet == nil {
discoSet = set.Set[key.NodePublic]{}
m.nodesOfDisco[epDisco.key] = discoSet
}
discoSet.Add(ep.publicKey)
}
// setNodeKeyForIPPort makes future peer lookups by ipp return the
// same endpoint as a lookup by nk.
//
// This should only be called with a fully verified mapping of ipp to
// nk, because calling this function defines the endpoint we hand to
// WireGuard for packets received from ipp.
func (m *peerMap) setNodeKeyForIPPort(ipp netip.AddrPort, nk key.NodePublic) {
if pi := m.byIPPort[ipp]; pi != nil {
delete(pi.ipPorts, ipp)
delete(m.byIPPort, ipp)
}
if pi, ok := m.byNodeKey[nk]; ok {
pi.ipPorts.Add(ipp)
m.byIPPort[ipp] = pi
}
}
// deleteEndpoint deletes the peerInfo associated with ep, and
// updates indexes.
func (m *peerMap) deleteEndpoint(ep *endpoint) {
if ep == nil {
return
}
ep.stopAndReset()
epDisco := ep.disco.Load()
pi := m.byNodeKey[ep.publicKey]
if epDisco != nil {
delete(m.nodesOfDisco[epDisco.key], ep.publicKey)
}
delete(m.byNodeKey, ep.publicKey)
if was, ok := m.byNodeID[ep.nodeID]; ok && was.ep == ep {
delete(m.byNodeID, ep.nodeID)
}
if pi == nil {
// Kneejerk paranoia from earlier issue 2801.
// Unexpected. But no logger plumbed here to log so.
return
}
for ip := range pi.ipPorts {
delete(m.byIPPort, ip)
}
}

130
vendor/tailscale.com/wgengine/magicsock/peermtu.go generated vendored Normal file
View File

@@ -0,0 +1,130 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build (darwin && !ios) || (linux && !android)
package magicsock
import (
"errors"
"golang.org/x/sys/unix"
"tailscale.com/disco"
"tailscale.com/net/tstun"
)
// Peer path MTU routines shared by platforms that implement it.
// DontFragSetting returns true if at least one of the underlying sockets of
// this connection is a UDP socket with the don't fragment bit set, otherwise it
// returns false. It also returns an error if either connection returned an error
// other than errUnsupportedConnType.
func (c *Conn) DontFragSetting() (bool, error) {
df4, err4 := c.getDontFragment("udp4")
df6, err6 := c.getDontFragment("udp6")
df := df4 || df6
err := err4
if err4 != nil && err4 != errUnsupportedConnType {
err = err6
}
if err == errUnsupportedConnType {
err = nil
}
return df, err
}
// ShouldPMTUD returns true if this client should try to enable peer MTU
// discovery, false otherwise.
func (c *Conn) ShouldPMTUD() bool {
if v, ok := debugEnablePMTUD().Get(); ok {
if debugPMTUD() {
c.logf("magicsock: peermtu: peer path MTU discovery set via envknob to %v", v)
}
return v
}
if c.controlKnobs != nil {
if v := c.controlKnobs.PeerMTUEnable.Load(); v {
if debugPMTUD() {
c.logf("magicsock: peermtu: peer path MTU discovery enabled by control")
}
return v
}
}
if debugPMTUD() {
c.logf("magicsock: peermtu: peer path MTU discovery set by default to false")
}
return false // Until we feel confident PMTUD is solid.
}
// PeerMTUEnabled reports whether peer path MTU discovery is enabled.
func (c *Conn) PeerMTUEnabled() bool {
return c.peerMTUEnabled.Load()
}
// UpdatePMTUD configures the underlying sockets of this Conn to enable or disable
// peer path MTU discovery according to the current configuration.
//
// Enabling or disabling peer path MTU discovery requires setting the don't
// fragment bit on its two underlying pconns. There are three distinct results
// for this operation on each pconn:
//
// 1. Success
// 2. Failure (not supported on this platform, or supported but failed)
// 3. Not a UDP socket (most likely one of IPv4 or IPv6 couldn't be used)
//
// To simplify the fast path for the most common case, we set the PMTUD status
// of the overall Conn according to the results of setting the sockopt on pconn
// as follows:
//
// 1. Both setsockopts succeed: PMTUD status update succeeds
// 2. One succeeds, one returns not a UDP socket: PMTUD status update succeeds
// 4. Neither setsockopt succeeds: PMTUD disabled
// 3. Either setsockopt fails: PMTUD disabled
//
// If the PMTUD settings changed, it resets the endpoint state so that it will
// re-probe path MTUs to this peer.
func (c *Conn) UpdatePMTUD() {
if debugPMTUD() {
df4, err4 := c.getDontFragment("udp4")
df6, err6 := c.getDontFragment("udp6")
c.logf("magicsock: peermtu: peer MTU status %v DF bit status: v4: %v (%v) v6: %v (%v)", c.peerMTUEnabled.Load(), df4, err4, df6, err6)
}
enable := c.ShouldPMTUD()
if c.peerMTUEnabled.Load() == enable {
c.logf("[v1] magicsock: peermtu: peer MTU status is %v", enable)
return
}
newStatus := enable
err4 := c.setDontFragment("udp4", enable)
err6 := c.setDontFragment("udp6", enable)
anySuccess := err4 == nil || err6 == nil
noFailures := (err4 == nil || err4 == errUnsupportedConnType) && (err6 == nil || err6 == errUnsupportedConnType)
if anySuccess && noFailures {
c.logf("magicsock: peermtu: peer MTU status updated to %v", newStatus)
} else {
c.logf("[unexpected] magicsock: peermtu: updating peer MTU status to %v failed (v4: %v, v6: %v), disabling", enable, err4, err6)
_ = c.setDontFragment("udp4", false)
_ = c.setDontFragment("udp6", false)
newStatus = false
}
if debugPMTUD() {
c.logf("magicsock: peermtu: peer MTU probes are %v", tstun.WireMTUsToProbe)
}
c.peerMTUEnabled.Store(newStatus)
c.resetEndpointStates()
}
var errEMSGSIZE error = unix.EMSGSIZE
func pmtuShouldLogDiscoTxErr(m disco.Message, err error) bool {
// Large disco.Ping packets used to probe path MTU may result in
// an EMSGSIZE error fairly regularly which can pollute logs.
p, ok := m.(*disco.Ping)
if !ok || p.Padding == 0 || !errors.Is(err, errEMSGSIZE) || debugPMTUD() {
return true
}
return false
}

View File

@@ -0,0 +1,51 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build darwin && !ios
package magicsock
import (
"syscall"
"golang.org/x/sys/unix"
)
func getDontFragOpt(network string) int {
if network == "udp4" {
return unix.IP_DONTFRAG
}
return unix.IPV6_DONTFRAG
}
func (c *Conn) setDontFragment(network string, enable bool) error {
optArg := 1
if enable == false {
optArg = 0
}
var err error
rcErr := c.connControl(network, func(fd uintptr) {
err = syscall.SetsockoptInt(int(fd), getIPProto(network), getDontFragOpt(network), optArg)
})
if rcErr != nil {
return rcErr
}
return err
}
func (c *Conn) getDontFragment(network string) (bool, error) {
var v int
var err error
rcErr := c.connControl(network, func(fd uintptr) {
v, err = syscall.GetsockoptInt(int(fd), getIPProto(network), getDontFragOpt(network))
})
if rcErr != nil {
return false, rcErr
}
if v == 1 {
return true, err
}
return false, err
}

View File

@@ -0,0 +1,49 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && !android
package magicsock
import (
"syscall"
)
func getDontFragOpt(network string) int {
if network == "udp4" {
return syscall.IP_MTU_DISCOVER
}
return syscall.IPV6_MTU_DISCOVER
}
func (c *Conn) setDontFragment(network string, enable bool) error {
optArg := syscall.IP_PMTUDISC_DO
if enable == false {
optArg = syscall.IP_PMTUDISC_DONT
}
var err error
rcErr := c.connControl(network, func(fd uintptr) {
err = syscall.SetsockoptInt(int(fd), getIPProto(network), getDontFragOpt(network), optArg)
})
if rcErr != nil {
return rcErr
}
return err
}
func (c *Conn) getDontFragment(network string) (bool, error) {
var v int
var err error
rcErr := c.connControl(network, func(fd uintptr) {
v, err = syscall.GetsockoptInt(int(fd), getIPProto(network), getDontFragOpt(network))
})
if rcErr != nil {
return false, rcErr
}
if v == syscall.IP_PMTUDISC_DO {
return true, err
}
return false, err
}

View File

@@ -0,0 +1,27 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build (!linux && !darwin) || android || ios
package magicsock
import "tailscale.com/disco"
func (c *Conn) DontFragSetting() (bool, error) {
return false, nil
}
func (c *Conn) ShouldPMTUD() bool {
return false
}
func (c *Conn) PeerMTUEnabled() bool {
return false
}
func (c *Conn) UpdatePMTUD() {
}
func pmtuShouldLogDiscoTxErr(m disco.Message, err error) bool {
return true
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build (darwin && !ios) || (linux && !android)
package magicsock
import (
"syscall"
)
// getIPProto returns the value of the get/setsockopt proto argument necessary
// to set an IP sockopt that corresponds with the string network, which must be
// "udp4" or "udp6".
func getIPProto(network string) int {
if network == "udp4" {
return syscall.IPPROTO_IP
}
return syscall.IPPROTO_IPV6
}
// connControl allows the caller to run a system call on the socket underlying
// Conn specified by the string network, which must be "udp4" or "udp6". If the
// pconn type implements the syscall method, this function returns the value of
// of the system call fn called with the fd of the socket as its arg (or the
// error from rc.Control() if that fails). Otherwise it returns the error
// errUnsupportedConnType.
func (c *Conn) connControl(network string, fn func(fd uintptr)) error {
pconn := c.pconn4.pconn
if network == "udp6" {
pconn = c.pconn6.pconn
}
sc, ok := pconn.(syscall.Conn)
if !ok {
return errUnsupportedConnType
}
rc, err := sc.SyscallConn()
if err != nil {
return err
}
return rc.Control(fn)
}

View File

@@ -0,0 +1,179 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package magicsock
import (
"errors"
"net"
"net/netip"
"sync"
"sync/atomic"
"syscall"
"golang.org/x/net/ipv6"
"tailscale.com/net/netaddr"
"tailscale.com/types/nettype"
)
// RebindingUDPConn is a UDP socket that can be re-bound.
// Unix has no notion of re-binding a socket, so we swap it out for a new one.
type RebindingUDPConn struct {
// pconnAtomic is a pointer to the value stored in pconn, but doesn't
// require acquiring mu. It's used for reads/writes and only upon failure
// do the reads/writes then check pconn (after acquiring mu) to see if
// there's been a rebind meanwhile.
// pconn isn't really needed, but makes some of the code simpler
// to keep it distinct.
// Neither is expected to be nil, sockets are bound on creation.
pconnAtomic atomic.Pointer[nettype.PacketConn]
mu sync.Mutex // held while changing pconn (and pconnAtomic)
pconn nettype.PacketConn
port uint16
}
// setConnLocked sets the provided nettype.PacketConn. It should be called only
// after acquiring RebindingUDPConn.mu. It upgrades the provided
// nettype.PacketConn to a batchingConn when appropriate. This upgrade is
// intentionally pushed closest to where read/write ops occur in order to avoid
// disrupting surrounding code that assumes nettype.PacketConn is a
// *net.UDPConn.
func (c *RebindingUDPConn) setConnLocked(p nettype.PacketConn, network string, batchSize int) {
upc := tryUpgradeToBatchingConn(p, network, batchSize)
c.pconn = upc
c.pconnAtomic.Store(&upc)
c.port = uint16(c.localAddrLocked().Port)
}
// currentConn returns c's current pconn, acquiring c.mu in the process.
func (c *RebindingUDPConn) currentConn() nettype.PacketConn {
c.mu.Lock()
defer c.mu.Unlock()
return c.pconn
}
func (c *RebindingUDPConn) readFromWithInitPconn(pconn nettype.PacketConn, b []byte) (int, netip.AddrPort, error) {
for {
n, addr, err := pconn.ReadFromUDPAddrPort(b)
if err != nil && pconn != c.currentConn() {
pconn = *c.pconnAtomic.Load()
continue
}
return n, addr, err
}
}
// ReadFromUDPAddrPort reads a packet from c into b.
// It returns the number of bytes copied and the source address.
func (c *RebindingUDPConn) ReadFromUDPAddrPort(b []byte) (int, netip.AddrPort, error) {
return c.readFromWithInitPconn(*c.pconnAtomic.Load(), b)
}
// WriteBatchTo writes buffs to addr.
func (c *RebindingUDPConn) WriteBatchTo(buffs [][]byte, addr netip.AddrPort) error {
for {
pconn := *c.pconnAtomic.Load()
b, ok := pconn.(batchingConn)
if !ok {
for _, buf := range buffs {
_, err := c.writeToUDPAddrPortWithInitPconn(pconn, buf, addr)
if err != nil {
return err
}
}
return nil
}
err := b.WriteBatchTo(buffs, addr)
if err != nil {
if pconn != c.currentConn() {
continue
}
return err
}
return err
}
}
// ReadBatch reads messages from c into msgs. It returns the number of messages
// the caller should evaluate for nonzero len, as a zero len message may fall
// on either side of a nonzero.
func (c *RebindingUDPConn) ReadBatch(msgs []ipv6.Message, flags int) (int, error) {
for {
pconn := *c.pconnAtomic.Load()
b, ok := pconn.(batchingConn)
if !ok {
n, ap, err := c.readFromWithInitPconn(pconn, msgs[0].Buffers[0])
if err == nil {
msgs[0].N = n
msgs[0].Addr = net.UDPAddrFromAddrPort(netaddr.Unmap(ap))
return 1, nil
}
return 0, err
}
n, err := b.ReadBatch(msgs, flags)
if err != nil && pconn != c.currentConn() {
continue
}
return n, err
}
}
func (c *RebindingUDPConn) Port() uint16 {
c.mu.Lock()
defer c.mu.Unlock()
return c.port
}
func (c *RebindingUDPConn) LocalAddr() *net.UDPAddr {
c.mu.Lock()
defer c.mu.Unlock()
return c.localAddrLocked()
}
func (c *RebindingUDPConn) localAddrLocked() *net.UDPAddr {
return c.pconn.LocalAddr().(*net.UDPAddr)
}
// errNilPConn is returned by RebindingUDPConn.Close when there is no current pconn.
// It is for internal use only and should not be returned to users.
var errNilPConn = errors.New("nil pconn")
func (c *RebindingUDPConn) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
return c.closeLocked()
}
func (c *RebindingUDPConn) closeLocked() error {
if c.pconn == nil {
return errNilPConn
}
c.port = 0
return c.pconn.Close()
}
func (c *RebindingUDPConn) writeToUDPAddrPortWithInitPconn(pconn nettype.PacketConn, b []byte, addr netip.AddrPort) (int, error) {
for {
n, err := pconn.WriteToUDPAddrPort(b, addr)
if err != nil && pconn != c.currentConn() {
pconn = *c.pconnAtomic.Load()
continue
}
return n, err
}
}
func (c *RebindingUDPConn) WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error) {
return c.writeToUDPAddrPortWithInitPconn(*c.pconnAtomic.Load(), b, addr)
}
func (c *RebindingUDPConn) SyscallConn() (syscall.RawConn, error) {
c.mu.Lock()
defer c.mu.Unlock()
sc, ok := c.pconn.(syscall.Conn)
if !ok {
return nil, errUnsupportedConnType
}
return sc.SyscallConn()
}

20
vendor/tailscale.com/wgengine/mem_ios.go generated vendored Normal file
View File

@@ -0,0 +1,20 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package wgengine
import (
"github.com/tailscale/wireguard-go/device"
)
// iOS has a very restrictive memory limit on network extensions.
// Reduce the maximum amount of memory that wireguard-go can allocate
// to avoid getting killed.
func init() {
device.QueueStagedSize = 64
device.QueueOutboundSize = 64
device.QueueInboundSize = 64
device.QueueHandshakeSize = 64
device.PreallocatedBuffersPerPool = 64
}

274
vendor/tailscale.com/wgengine/netlog/logger.go generated vendored Normal file
View File

@@ -0,0 +1,274 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package netlog provides a logger that monitors a TUN device and
// periodically records any traffic into a log stream.
package netlog
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/netip"
"sync"
"time"
"tailscale.com/health"
"tailscale.com/logpolicy"
"tailscale.com/logtail"
"tailscale.com/net/connstats"
"tailscale.com/net/netmon"
"tailscale.com/net/sockstats"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/logid"
"tailscale.com/types/netlogtype"
"tailscale.com/util/multierr"
"tailscale.com/wgengine/router"
)
// pollPeriod specifies how often to poll for network traffic.
const pollPeriod = 5 * time.Second
// Device is an abstraction over a tunnel device or a magic socket.
// Both *tstun.Wrapper and *magicsock.Conn implement this interface.
type Device interface {
SetStatistics(*connstats.Statistics)
}
type noopDevice struct{}
func (noopDevice) SetStatistics(*connstats.Statistics) {}
// Logger logs statistics about every connection.
// At present, it only logs connections within a tailscale network.
// Exit node traffic is not logged for privacy reasons.
// The zero value is ready for use.
type Logger struct {
mu sync.Mutex // protects all fields below
logger *logtail.Logger
stats *connstats.Statistics
tun Device
sock Device
addrs map[netip.Addr]bool
prefixes map[netip.Prefix]bool
}
// Running reports whether the logger is running.
func (nl *Logger) Running() bool {
nl.mu.Lock()
defer nl.mu.Unlock()
return nl.logger != nil
}
var testClient *http.Client
// Startup starts an asynchronous network logger that monitors
// statistics for the provided tun and/or sock device.
//
// The tun Device captures packets within the tailscale network,
// where at least one address is a tailscale IP address.
// The source is always from the perspective of the current node.
// If one of the other endpoint is not a tailscale IP address,
// then it suggests the use of a subnet router or exit node.
// For example, when using a subnet router, the source address is
// the tailscale IP address of the current node, and
// the destination address is an IP address within the subnet range.
// In contrast, when acting as a subnet router, the source address is
// an IP address within the subnet range, and the destination is a
// tailscale IP address that initiated the subnet proxy connection.
// In this case, the node acting as a subnet router is acting on behalf
// of some remote endpoint within the subnet range.
// The tun is used to populate the VirtualTraffic, SubnetTraffic,
// and ExitTraffic fields in Message.
//
// The sock Device captures packets at the magicsock layer.
// The source is always a tailscale IP address and the destination
// is a non-tailscale IP address to contact for that particular tailscale node.
// The IP protocol and source port are always zero.
// The sock is used to populated the PhysicalTraffic field in Message.
// The netMon parameter is optional; if non-nil it's used to do faster interface lookups.
func (nl *Logger) Startup(nodeID tailcfg.StableNodeID, nodeLogID, domainLogID logid.PrivateID, tun, sock Device, netMon *netmon.Monitor, health *health.Tracker, logExitFlowEnabledEnabled bool) error {
nl.mu.Lock()
defer nl.mu.Unlock()
if nl.logger != nil {
return fmt.Errorf("network logger already running for %v", nl.logger.PrivateID().Public())
}
// Startup a log stream to Tailscale's logging service.
logf := log.Printf
httpc := &http.Client{Transport: logpolicy.NewLogtailTransport(logtail.DefaultHost, netMon, health, logf)}
if testClient != nil {
httpc = testClient
}
nl.logger = logtail.NewLogger(logtail.Config{
Collection: "tailtraffic.log.tailscale.io",
PrivateID: nodeLogID,
CopyPrivateID: domainLogID,
Stderr: io.Discard,
CompressLogs: true,
HTTPC: httpc,
// TODO(joetsai): Set Buffer? Use an in-memory buffer for now.
// Include process sequence numbers to identify missing samples.
IncludeProcID: true,
IncludeProcSequence: true,
}, logf)
nl.logger.SetSockstatsLabel(sockstats.LabelNetlogLogger)
// Startup a data structure to track per-connection statistics.
// There is a maximum size for individual log messages that logtail
// can upload to the Tailscale log service, so stay below this limit.
const maxLogSize = 256 << 10
const maxConns = (maxLogSize - netlogtype.MaxMessageJSONSize) / netlogtype.MaxConnectionCountsJSONSize
nl.stats = connstats.NewStatistics(pollPeriod, maxConns, func(start, end time.Time, virtual, physical map[netlogtype.Connection]netlogtype.Counts) {
nl.mu.Lock()
addrs := nl.addrs
prefixes := nl.prefixes
nl.mu.Unlock()
recordStatistics(nl.logger, nodeID, start, end, virtual, physical, addrs, prefixes, logExitFlowEnabledEnabled)
})
// Register the connection tracker into the TUN device.
if tun == nil {
tun = noopDevice{}
}
nl.tun = tun
nl.tun.SetStatistics(nl.stats)
// Register the connection tracker into magicsock.
if sock == nil {
sock = noopDevice{}
}
nl.sock = sock
nl.sock.SetStatistics(nl.stats)
return nil
}
func recordStatistics(logger *logtail.Logger, nodeID tailcfg.StableNodeID, start, end time.Time, connstats, sockStats map[netlogtype.Connection]netlogtype.Counts, addrs map[netip.Addr]bool, prefixes map[netip.Prefix]bool, logExitFlowEnabled bool) {
m := netlogtype.Message{NodeID: nodeID, Start: start.UTC(), End: end.UTC()}
classifyAddr := func(a netip.Addr) (isTailscale, withinRoute bool) {
// NOTE: There could be mis-classifications where an address is treated
// as a Tailscale IP address because the subnet range overlaps with
// the subnet range that Tailscale IP addresses are allocated from.
// This should never happen for IPv6, but could happen for IPv4.
withinRoute = addrs[a]
for p := range prefixes {
if p.Contains(a) && p.Bits() > 0 {
withinRoute = true
break
}
}
return withinRoute && tsaddr.IsTailscaleIP(a), withinRoute && !tsaddr.IsTailscaleIP(a)
}
exitTraffic := make(map[netlogtype.Connection]netlogtype.Counts)
for conn, cnts := range connstats {
srcIsTailscaleIP, srcWithinSubnet := classifyAddr(conn.Src.Addr())
dstIsTailscaleIP, dstWithinSubnet := classifyAddr(conn.Dst.Addr())
switch {
case srcIsTailscaleIP && dstIsTailscaleIP:
m.VirtualTraffic = append(m.VirtualTraffic, netlogtype.ConnectionCounts{Connection: conn, Counts: cnts})
case srcWithinSubnet || dstWithinSubnet:
m.SubnetTraffic = append(m.SubnetTraffic, netlogtype.ConnectionCounts{Connection: conn, Counts: cnts})
default:
const anonymize = true
if anonymize && !logExitFlowEnabled {
// Only preserve the address if it is a Tailscale IP address.
srcOrig, dstOrig := conn.Src, conn.Dst
conn = netlogtype.Connection{} // scrub everything by default
if srcIsTailscaleIP {
conn.Src = netip.AddrPortFrom(srcOrig.Addr(), 0)
}
if dstIsTailscaleIP {
conn.Dst = netip.AddrPortFrom(dstOrig.Addr(), 0)
}
}
exitTraffic[conn] = exitTraffic[conn].Add(cnts)
}
}
for conn, cnts := range exitTraffic {
m.ExitTraffic = append(m.ExitTraffic, netlogtype.ConnectionCounts{Connection: conn, Counts: cnts})
}
for conn, cnts := range sockStats {
m.PhysicalTraffic = append(m.PhysicalTraffic, netlogtype.ConnectionCounts{Connection: conn, Counts: cnts})
}
if len(m.VirtualTraffic)+len(m.SubnetTraffic)+len(m.ExitTraffic)+len(m.PhysicalTraffic) > 0 {
if b, err := json.Marshal(m); err != nil {
logger.Logf("json.Marshal error: %v", err)
} else {
logger.Logf("%s", b)
}
}
}
func makeRouteMaps(cfg *router.Config) (addrs map[netip.Addr]bool, prefixes map[netip.Prefix]bool) {
addrs = make(map[netip.Addr]bool)
for _, p := range cfg.LocalAddrs {
if p.IsSingleIP() {
addrs[p.Addr()] = true
}
}
prefixes = make(map[netip.Prefix]bool)
insertPrefixes := func(rs []netip.Prefix) {
for _, p := range rs {
if p.IsSingleIP() {
addrs[p.Addr()] = true
} else {
prefixes[p] = true
}
}
}
insertPrefixes(cfg.Routes)
insertPrefixes(cfg.SubnetRoutes)
return addrs, prefixes
}
// ReconfigRoutes configures the network logger with updated routes.
// The cfg is used to classify the types of connections captured by
// the tun Device passed to Startup.
func (nl *Logger) ReconfigRoutes(cfg *router.Config) {
nl.mu.Lock()
defer nl.mu.Unlock()
// TODO(joetsai): There is a race where deleted routes are not known at
// the time of extraction. We need to keep old routes around for a bit.
nl.addrs, nl.prefixes = makeRouteMaps(cfg)
}
// Shutdown shuts down the network logger.
// This attempts to flush out all pending log messages.
// Even if an error is returned, the logger is still shut down.
func (nl *Logger) Shutdown(ctx context.Context) error {
nl.mu.Lock()
defer nl.mu.Unlock()
if nl.logger == nil {
return nil
}
// Shutdown in reverse order of Startup.
// Do not hold lock while shutting down since this may flush one last time.
nl.mu.Unlock()
nl.sock.SetStatistics(nil)
nl.tun.SetStatistics(nil)
err1 := nl.stats.Shutdown(ctx)
err2 := nl.logger.Shutdown(ctx)
nl.mu.Lock()
// Purge state.
nl.logger = nil
nl.stats = nil
nl.tun = nil
nl.sock = nil
nl.addrs = nil
nl.prefixes = nil
return multierr.New(err1, err2)
}

104
vendor/tailscale.com/wgengine/netstack/gro/gro.go generated vendored Normal file
View File

@@ -0,0 +1,104 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package gro implements GRO for the receive (write) path into gVisor.
package gro
import (
"bytes"
"github.com/tailscale/wireguard-go/tun"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/header/parse"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"tailscale.com/net/packet"
"tailscale.com/types/ipproto"
)
// RXChecksumOffload validates IPv4, TCP, and UDP header checksums in p,
// returning an equivalent *stack.PacketBuffer if they are valid, otherwise nil.
// The set of headers validated covers where gVisor would perform validation if
// !stack.PacketBuffer.RXChecksumValidated, i.e. it satisfies
// stack.CapabilityRXChecksumOffload. Other protocols with checksum fields,
// e.g. ICMP{v6}, are still validated by gVisor regardless of rx checksum
// offloading capabilities.
func RXChecksumOffload(p *packet.Parsed) *stack.PacketBuffer {
var (
pn tcpip.NetworkProtocolNumber
csumStart int
)
buf := p.Buffer()
switch p.IPVersion {
case 4:
if len(buf) < header.IPv4MinimumSize {
return nil
}
csumStart = int((buf[0] & 0x0F) * 4)
if csumStart < header.IPv4MinimumSize || csumStart > header.IPv4MaximumHeaderSize || len(buf) < csumStart {
return nil
}
if ^tun.Checksum(buf[:csumStart], 0) != 0 {
return nil
}
pn = header.IPv4ProtocolNumber
case 6:
if len(buf) < header.IPv6FixedHeaderSize {
return nil
}
csumStart = header.IPv6FixedHeaderSize
pn = header.IPv6ProtocolNumber
if p.IPProto != ipproto.ICMPv6 && p.IPProto != ipproto.TCP && p.IPProto != ipproto.UDP {
// buf could have extension headers before a UDP or TCP header, but
// packet.Parsed.IPProto will be set to the ext header type, so we
// have to look deeper. We are still responsible for validating the
// L4 checksum in this case. So, make use of gVisor's existing
// extension header parsing via parse.IPv6() in order to unpack the
// L4 csumStart index. This is not particularly efficient as we have
// to allocate a short-lived stack.PacketBuffer that cannot be
// re-used. parse.IPv6() "consumes" the IPv6 headers, so we can't
// inject this stack.PacketBuffer into the stack at a later point.
packetBuf := stack.NewPacketBuffer(stack.PacketBufferOptions{
Payload: buffer.MakeWithData(bytes.Clone(buf)),
})
defer packetBuf.DecRef()
// The rightmost bool returns false only if packetBuf is too short,
// which we've already accounted for above.
transportProto, _, _, _, _ := parse.IPv6(packetBuf)
if transportProto == header.TCPProtocolNumber || transportProto == header.UDPProtocolNumber {
csumLen := packetBuf.Data().Size()
if len(buf) < csumLen {
return nil
}
csumStart = len(buf) - csumLen
p.IPProto = ipproto.Proto(transportProto)
}
}
}
if p.IPProto == ipproto.TCP || p.IPProto == ipproto.UDP {
lenForPseudo := len(buf) - csumStart
csum := tun.PseudoHeaderChecksum(
uint8(p.IPProto),
p.Src.Addr().AsSlice(),
p.Dst.Addr().AsSlice(),
uint16(lenForPseudo))
csum = tun.Checksum(buf[csumStart:], csum)
if ^csum != 0 {
return nil
}
}
packetBuf := stack.NewPacketBuffer(stack.PacketBufferOptions{
Payload: buffer.MakeWithData(bytes.Clone(buf)),
})
packetBuf.NetworkProtocolNumber = pn
// Setting this is not technically required. gVisor overrides where
// stack.CapabilityRXChecksumOffload is advertised from Capabilities().
// https://github.com/google/gvisor/blob/64c016c92987cc04dfd4c7b091ddd21bdad875f8/pkg/tcpip/stack/nic.go#L763
// This is also why we offload for all packets since we cannot signal this
// per-packet.
packetBuf.RXChecksumValidated = true
return packetBuf
}

View File

@@ -0,0 +1,76 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ios
package gro
import (
"sync"
"gvisor.dev/gvisor/pkg/tcpip/stack"
nsgro "gvisor.dev/gvisor/pkg/tcpip/stack/gro"
"tailscale.com/net/packet"
)
var (
groPool sync.Pool
)
func init() {
groPool.New = func() any {
g := &GRO{}
g.gro.Init(true)
return g
}
}
// GRO coalesces incoming packets to increase throughput. It is NOT thread-safe.
type GRO struct {
gro nsgro.GRO
maybeEnqueued bool
}
// NewGRO returns a new instance of *GRO from a sync.Pool. It can be returned to
// the pool with GRO.Flush().
func NewGRO() *GRO {
return groPool.Get().(*GRO)
}
// SetDispatcher sets the underlying stack.NetworkDispatcher where packets are
// delivered.
func (g *GRO) SetDispatcher(d stack.NetworkDispatcher) {
g.gro.Dispatcher = d
}
// Enqueue enqueues the provided packet for GRO. It may immediately deliver
// it to the underlying stack.NetworkDispatcher depending on its contents. To
// explicitly flush previously enqueued packets see Flush().
func (g *GRO) Enqueue(p *packet.Parsed) {
if g.gro.Dispatcher == nil {
return
}
pkt := RXChecksumOffload(p)
if pkt == nil {
return
}
// TODO(jwhited): g.gro.Enqueue() duplicates a lot of p.Decode().
// We may want to push stack.PacketBuffer further up as a
// replacement for packet.Parsed, or inversely push packet.Parsed
// down into refactored GRO logic.
g.gro.Enqueue(pkt)
g.maybeEnqueued = true
pkt.DecRef()
}
// Flush flushes previously enqueued packets to the underlying
// stack.NetworkDispatcher, and returns GRO to a pool for later re-use. Callers
// MUST NOT use GRO once it has been Flush()'d.
func (g *GRO) Flush() {
if g.gro.Dispatcher != nil && g.maybeEnqueued {
g.gro.Flush()
}
g.gro.Dispatcher = nil
g.maybeEnqueued = false
groPool.Put(g)
}

23
vendor/tailscale.com/wgengine/netstack/gro/gro_ios.go generated vendored Normal file
View File

@@ -0,0 +1,23 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build ios
package gro
import (
"gvisor.dev/gvisor/pkg/tcpip/stack"
"tailscale.com/net/packet"
)
type GRO struct{}
func NewGRO() *GRO {
panic("unsupported on iOS")
}
func (g *GRO) SetDispatcher(_ stack.NetworkDispatcher) {}
func (g *GRO) Enqueue(_ *packet.Parsed) {}
func (g *GRO) Flush() {}

302
vendor/tailscale.com/wgengine/netstack/link_endpoint.go generated vendored Normal file
View File

@@ -0,0 +1,302 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package netstack
import (
"context"
"sync"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"tailscale.com/net/packet"
"tailscale.com/types/ipproto"
"tailscale.com/wgengine/netstack/gro"
)
type queue struct {
// TODO(jwhited): evaluate performance with mu as Mutex and/or alternative
// non-channel buffer.
c chan *stack.PacketBuffer
mu sync.RWMutex // mu guards closed
closed bool
}
func (q *queue) Close() {
q.mu.Lock()
defer q.mu.Unlock()
if !q.closed {
close(q.c)
}
q.closed = true
}
func (q *queue) Read() *stack.PacketBuffer {
select {
case p := <-q.c:
return p
default:
return nil
}
}
func (q *queue) ReadContext(ctx context.Context) *stack.PacketBuffer {
select {
case pkt := <-q.c:
return pkt
case <-ctx.Done():
return nil
}
}
func (q *queue) Write(pkt *stack.PacketBuffer) tcpip.Error {
// q holds the PacketBuffer.
q.mu.RLock()
defer q.mu.RUnlock()
if q.closed {
return &tcpip.ErrClosedForSend{}
}
wrote := false
select {
case q.c <- pkt.IncRef():
wrote = true
default:
// TODO(jwhited): reconsider/count
pkt.DecRef()
}
if wrote {
return nil
}
return &tcpip.ErrNoBufferSpace{}
}
func (q *queue) Num() int {
return len(q.c)
}
var _ stack.LinkEndpoint = (*linkEndpoint)(nil)
var _ stack.GSOEndpoint = (*linkEndpoint)(nil)
type supportedGRO int
const (
groNotSupported supportedGRO = iota
tcpGROSupported
)
// linkEndpoint implements stack.LinkEndpoint and stack.GSOEndpoint. Outbound
// packets written by gVisor towards Tailscale are stored in a channel.
// Inbound is fed to gVisor via injectInbound or gro. This is loosely
// modeled after gvisor.dev/pkg/tcpip/link/channel.Endpoint.
type linkEndpoint struct {
SupportedGSOKind stack.SupportedGSO
supportedGRO supportedGRO
mu sync.RWMutex // mu guards the following fields
dispatcher stack.NetworkDispatcher
linkAddr tcpip.LinkAddress
mtu uint32
q *queue // outbound
}
func newLinkEndpoint(size int, mtu uint32, linkAddr tcpip.LinkAddress, supportedGRO supportedGRO) *linkEndpoint {
le := &linkEndpoint{
supportedGRO: supportedGRO,
q: &queue{
c: make(chan *stack.PacketBuffer, size),
},
mtu: mtu,
linkAddr: linkAddr,
}
return le
}
// gro attempts to enqueue p on g if l supports a GRO kind matching the
// transport protocol carried in p. gro may allocate g if it is nil. gro can
// either return the existing g, a newly allocated one, or nil. Callers are
// responsible for calling Flush() on the returned value if it is non-nil once
// they have finished iterating through all GRO candidates for a given vector.
// If gro allocates a *gro.GRO it will have l's stack.NetworkDispatcher set via
// SetDispatcher().
func (l *linkEndpoint) gro(p *packet.Parsed, g *gro.GRO) *gro.GRO {
if l.supportedGRO == groNotSupported || p.IPProto != ipproto.TCP {
// IPv6 may have extension headers preceding a TCP header, but we trade
// for a fast path and assume p cannot be coalesced in such a case.
l.injectInbound(p)
return g
}
if g == nil {
l.mu.RLock()
d := l.dispatcher
l.mu.RUnlock()
g = gro.NewGRO()
g.SetDispatcher(d)
}
g.Enqueue(p)
return g
}
// Close closes l. Further packet injections will return an error, and all
// pending packets are discarded. Close may be called concurrently with
// WritePackets.
func (l *linkEndpoint) Close() {
l.mu.Lock()
l.dispatcher = nil
l.mu.Unlock()
l.q.Close()
l.Drain()
}
// Read does non-blocking read one packet from the outbound packet queue.
func (l *linkEndpoint) Read() *stack.PacketBuffer {
return l.q.Read()
}
// ReadContext does blocking read for one packet from the outbound packet queue.
// It can be cancelled by ctx, and in this case, it returns nil.
func (l *linkEndpoint) ReadContext(ctx context.Context) *stack.PacketBuffer {
return l.q.ReadContext(ctx)
}
// Drain removes all outbound packets from the channel and counts them.
func (l *linkEndpoint) Drain() int {
c := 0
for pkt := l.Read(); pkt != nil; pkt = l.Read() {
pkt.DecRef()
c++
}
return c
}
// NumQueued returns the number of packets queued for outbound.
func (l *linkEndpoint) NumQueued() int {
return l.q.Num()
}
func (l *linkEndpoint) injectInbound(p *packet.Parsed) {
l.mu.RLock()
d := l.dispatcher
l.mu.RUnlock()
if d == nil {
return
}
pkt := gro.RXChecksumOffload(p)
if pkt == nil {
return
}
d.DeliverNetworkPacket(pkt.NetworkProtocolNumber, pkt)
pkt.DecRef()
}
// Attach saves the stack network-layer dispatcher for use later when packets
// are injected.
func (l *linkEndpoint) Attach(dispatcher stack.NetworkDispatcher) {
l.mu.Lock()
defer l.mu.Unlock()
l.dispatcher = dispatcher
}
// IsAttached implements stack.LinkEndpoint.IsAttached.
func (l *linkEndpoint) IsAttached() bool {
l.mu.RLock()
defer l.mu.RUnlock()
return l.dispatcher != nil
}
// MTU implements stack.LinkEndpoint.MTU.
func (l *linkEndpoint) MTU() uint32 {
l.mu.RLock()
defer l.mu.RUnlock()
return l.mtu
}
// SetMTU implements stack.LinkEndpoint.SetMTU.
func (l *linkEndpoint) SetMTU(mtu uint32) {
l.mu.Lock()
defer l.mu.Unlock()
l.mtu = mtu
}
// Capabilities implements stack.LinkEndpoint.Capabilities.
func (l *linkEndpoint) Capabilities() stack.LinkEndpointCapabilities {
// We are required to offload RX checksum validation for the purposes of
// GRO.
return stack.CapabilityRXChecksumOffload
}
// GSOMaxSize implements stack.GSOEndpoint.
func (*linkEndpoint) GSOMaxSize() uint32 {
// This an increase from 32k returned by channel.Endpoint.GSOMaxSize() to
// 64k, which improves throughput.
return (1 << 16) - 1
}
// SupportedGSO implements stack.GSOEndpoint.
func (l *linkEndpoint) SupportedGSO() stack.SupportedGSO {
return l.SupportedGSOKind
}
// MaxHeaderLength returns the maximum size of the link layer header. Given it
// doesn't have a header, it just returns 0.
func (*linkEndpoint) MaxHeaderLength() uint16 {
return 0
}
// LinkAddress returns the link address of this endpoint.
func (l *linkEndpoint) LinkAddress() tcpip.LinkAddress {
l.mu.RLock()
defer l.mu.RUnlock()
return l.linkAddr
}
// SetLinkAddress implements stack.LinkEndpoint.SetLinkAddress.
func (l *linkEndpoint) SetLinkAddress(addr tcpip.LinkAddress) {
l.mu.Lock()
defer l.mu.Unlock()
l.linkAddr = addr
}
// WritePackets stores outbound packets into the channel.
// Multiple concurrent calls are permitted.
func (l *linkEndpoint) WritePackets(pkts stack.PacketBufferList) (int, tcpip.Error) {
n := 0
// TODO(jwhited): evaluate writing a stack.PacketBufferList instead of a
// single packet. We can split 2 x 64K GSO across
// wireguard-go/conn.IdealBatchSize (128 slots) @ 1280 MTU, and non-GSO we
// could do more. Read API would need to change to take advantage. Verify
// gVisor limits around max number of segments packed together. Since we
// control MTU (and by effect TCP MSS in gVisor) we *shouldn't* expect to
// ever overflow 128 slots (see wireguard-go/tun.ErrTooManySegments usage).
for _, pkt := range pkts.AsSlice() {
if err := l.q.Write(pkt); err != nil {
if _, ok := err.(*tcpip.ErrNoBufferSpace); !ok && n == 0 {
return 0, err
}
break
}
n++
}
return n, nil
}
// Wait implements stack.LinkEndpoint.Wait.
func (*linkEndpoint) Wait() {}
// ARPHardwareType implements stack.LinkEndpoint.ARPHardwareType.
func (*linkEndpoint) ARPHardwareType() header.ARPHardwareType {
return header.ARPHardwareNone
}
// AddHeader implements stack.LinkEndpoint.AddHeader.
func (*linkEndpoint) AddHeader(*stack.PacketBuffer) {}
// ParseHeader implements stack.LinkEndpoint.ParseHeader.
func (*linkEndpoint) ParseHeader(*stack.PacketBuffer) bool { return true }
// SetOnCloseAction implements stack.LinkEndpoint.
func (*linkEndpoint) SetOnCloseAction(func()) {}

1943
vendor/tailscale.com/wgengine/netstack/netstack.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package netstack
import (
"os/exec"
"syscall"
"golang.org/x/sys/unix"
)
func init() {
setAmbientCapsRaw = func(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{
AmbientCaps: []uintptr{unix.CAP_NET_RAW},
}
}
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ios
package netstack
import (
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
)
const (
tcpRXBufMinSize = tcp.MinBufferSize
tcpRXBufDefSize = tcp.DefaultSendBufferSize
tcpRXBufMaxSize = 8 << 20 // 8MiB
tcpTXBufMinSize = tcp.MinBufferSize
tcpTXBufDefSize = tcp.DefaultReceiveBufferSize
tcpTXBufMaxSize = 6 << 20 // 6MiB
)

View File

@@ -0,0 +1,24 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build ios
package netstack
import (
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
)
const (
// tcp{RX,TX}Buf{Min,Def,Max}Size mirror gVisor defaults. We leave these
// unchanged on iOS for now as to not increase pressure towards the
// NetworkExtension memory limit.
// TODO(jwhited): test memory/throughput impact of collapsing to values in _default.go
tcpRXBufMinSize = tcp.MinBufferSize
tcpRXBufDefSize = tcp.DefaultSendBufferSize
tcpRXBufMaxSize = tcp.MaxBufferSize
tcpTXBufMinSize = tcp.MinBufferSize
tcpTXBufDefSize = tcp.DefaultReceiveBufferSize
tcpTXBufMaxSize = tcp.MaxBufferSize
)

View File

@@ -0,0 +1,72 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !darwin && !ios
package netstack
import (
"errors"
"net/netip"
"os"
"os/exec"
"runtime"
"time"
"tailscale.com/version/distro"
)
// setAmbientCapsRaw is non-nil on Linux for Synology, to run ping with
// CAP_NET_RAW from tailscaled's binary.
var setAmbientCapsRaw func(*exec.Cmd)
var isSynology = runtime.GOOS == "linux" && distro.Get() == distro.Synology
// sendOutboundUserPing sends a non-privileged ICMP (or ICMPv6) ping to dstIP with the given timeout.
func (ns *Impl) sendOutboundUserPing(dstIP netip.Addr, timeout time.Duration) error {
var err error
switch runtime.GOOS {
case "windows":
var out []byte
out, err = exec.Command("ping", "-n", "1", "-w", "3000", dstIP.String()).CombinedOutput()
if err == nil && !windowsPingOutputIsSuccess(dstIP, out) {
// TODO(bradfitz,nickkhyl): return the actual ICMP error we heard back to the caller?
// For now we just drop it.
err = errors.New("unsuccessful ICMP reply received")
}
case "freebsd":
// Note: 2000 ms is actually 1 second + 2,000
// milliseconds extra for 3 seconds total.
// See https://github.com/tailscale/tailscale/pull/3753 for details.
ping := "ping"
if dstIP.Is6() {
ping = "ping6"
}
err = exec.Command(ping, "-c", "1", "-W", "2000", dstIP.String()).Run()
case "openbsd":
ping := "ping"
if dstIP.Is6() {
ping = "ping6"
}
err = exec.Command(ping, "-c", "1", "-w", "3", dstIP.String()).Run()
case "android":
ping := "/system/bin/ping"
if dstIP.Is6() {
ping = "/system/bin/ping6"
}
err = exec.Command(ping, "-c", "1", "-w", "3", dstIP.String()).Run()
default:
ping := "ping"
if isSynology {
ping = "/bin/ping"
}
cmd := exec.Command(ping, "-c", "1", "-W", "3", dstIP.String())
if isSynology && os.Getuid() != 0 {
// On DSM7 we run as non-root and need to pass
// CAP_NET_RAW if our binary has it.
setAmbientCapsRaw(cmd)
}
err = cmd.Run()
}
return err
}

View File

@@ -0,0 +1,38 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build darwin || ios
package netstack
import (
"net/netip"
"time"
probing "github.com/prometheus-community/pro-bing"
)
// sendOutboundUserPing sends a non-privileged ICMP (or ICMPv6) ping to dstIP with the given timeout.
func (ns *Impl) sendOutboundUserPing(dstIP netip.Addr, timeout time.Duration) error {
p, err := probing.NewPinger(dstIP.String())
if err != nil {
ns.logf("sendICMPPingToIP failed to create pinger: %v", err)
return err
}
p.Timeout = timeout
p.Count = 1
p.SetPrivileged(false)
p.OnSend = func(pkt *probing.Packet) {
ns.logf("sendICMPPingToIP: forwarding ping to %s:", p.Addr())
}
p.OnRecv = func(pkt *probing.Packet) {
ns.logf("sendICMPPingToIP: %d bytes pong from %s: icmp_seq=%d time=%v", pkt.Nbytes, pkt.IPAddr, pkt.Seq, pkt.Rtt)
}
p.OnFinish = func(stats *probing.Statistics) {
ns.logf("sendICMPPingToIP: done, %d replies received", stats.PacketsRecv)
}
return p.Run()
}

273
vendor/tailscale.com/wgengine/pendopen.go generated vendored Normal file
View File

@@ -0,0 +1,273 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package wgengine
import (
"fmt"
"net/netip"
"runtime"
"strings"
"time"
"github.com/gaissmai/bart"
"tailscale.com/net/flowtrack"
"tailscale.com/net/packet"
"tailscale.com/net/tstun"
"tailscale.com/types/ipproto"
"tailscale.com/types/lazy"
"tailscale.com/util/mak"
"tailscale.com/wgengine/filter"
)
const tcpTimeoutBeforeDebug = 5 * time.Second
type pendingOpenFlow struct {
timer *time.Timer // until giving up on the flow
// guarded by userspaceEngine.mu:
// problem is non-zero if we got a MaybeBroken (non-terminal)
// TSMP "reject" header.
problem packet.TailscaleRejectReason
}
func (e *userspaceEngine) removeFlow(f flowtrack.Tuple) (removed bool) {
e.mu.Lock()
defer e.mu.Unlock()
of, ok := e.pendOpen[f]
if !ok {
// Not a tracked flow (likely already removed)
return false
}
of.timer.Stop()
delete(e.pendOpen, f)
return true
}
func (e *userspaceEngine) noteFlowProblemFromPeer(f flowtrack.Tuple, problem packet.TailscaleRejectReason) {
e.mu.Lock()
defer e.mu.Unlock()
of, ok := e.pendOpen[f]
if !ok {
// Not a tracked flow (likely already removed)
return
}
of.problem = problem
}
func (e *userspaceEngine) trackOpenPreFilterIn(pp *packet.Parsed, t *tstun.Wrapper) (res filter.Response) {
res = filter.Accept // always
if pp.IPProto == ipproto.TSMP {
res = filter.DropSilently
rh, ok := pp.AsTailscaleRejectedHeader()
if !ok {
return
}
if rh.MaybeBroken {
e.noteFlowProblemFromPeer(rh.Flow(), rh.Reason)
} else if f := rh.Flow(); e.removeFlow(f) {
e.logf("open-conn-track: flow %v %v > %v rejected due to %v", rh.Proto, rh.Src, rh.Dst, rh.Reason)
}
return
}
if pp.IPVersion == 0 ||
pp.IPProto != ipproto.TCP ||
pp.TCPFlags&(packet.TCPSyn|packet.TCPRst) == 0 {
return
}
// Either a SYN or a RST came back. Remove it in either case.
f := flowtrack.MakeTuple(pp.IPProto, pp.Dst, pp.Src) // src/dst reversed
removed := e.removeFlow(f)
if removed && pp.TCPFlags&packet.TCPRst != 0 {
e.logf("open-conn-track: flow TCP %v got RST by peer", f)
}
return
}
var (
appleIPRange = netip.MustParsePrefix("17.0.0.0/8")
canonicalIPs = lazy.SyncFunc(func() (checkIPFunc func(netip.Addr) bool) {
// https://bgp.he.net/AS41231#_prefixes
t := &bart.Table[bool]{}
for _, s := range strings.Fields(`
91.189.89.0/24
91.189.91.0/24
91.189.92.0/24
91.189.93.0/24
91.189.94.0/24
91.189.95.0/24
162.213.32.0/24
162.213.34.0/24
162.213.35.0/24
185.125.188.0/23
185.125.190.0/24
194.169.254.0/24`) {
t.Insert(netip.MustParsePrefix(s), true)
}
return func(ip netip.Addr) bool {
v, _ := t.Lookup(ip)
return v
}
})
)
// isOSNetworkProbe reports whether the target is likely a network
// connectivity probe target from e.g. iOS or Ubuntu network-manager.
//
// iOS likes to probe Apple IPs on all interfaces to check for connectivity.
// Don't start timers tracking those. They won't succeed anyway. Avoids log
// spam like:
func (e *userspaceEngine) isOSNetworkProbe(dst netip.AddrPort) bool {
// iOS had log spam like:
// open-conn-track: timeout opening (100.115.73.60:52501 => 17.125.252.5:443); no associated peer node
if runtime.GOOS == "ios" && dst.Port() == 443 && appleIPRange.Contains(dst.Addr()) {
if _, ok := e.PeerForIP(dst.Addr()); !ok {
return true
}
}
// NetworkManager; https://github.com/tailscale/tailscale/issues/13687
// open-conn-track: timeout opening (TCP 100.96.229.119:42798 => 185.125.190.49:80); no associated peer node
if runtime.GOOS == "linux" && dst.Port() == 80 && canonicalIPs()(dst.Addr()) {
if _, ok := e.PeerForIP(dst.Addr()); !ok {
return true
}
}
return false
}
func (e *userspaceEngine) trackOpenPostFilterOut(pp *packet.Parsed, t *tstun.Wrapper) (res filter.Response) {
res = filter.Accept // always
if pp.IPVersion == 0 ||
pp.IPProto != ipproto.TCP ||
pp.TCPFlags&packet.TCPAck != 0 ||
pp.TCPFlags&packet.TCPSyn == 0 {
return
}
if e.isOSNetworkProbe(pp.Dst) {
return
}
flow := flowtrack.MakeTuple(pp.IPProto, pp.Src, pp.Dst)
e.mu.Lock()
defer e.mu.Unlock()
if _, dup := e.pendOpen[flow]; dup {
// Duplicates are expected when the OS retransmits. Ignore.
return
}
timer := time.AfterFunc(tcpTimeoutBeforeDebug, func() {
e.onOpenTimeout(flow)
})
mak.Set(&e.pendOpen, flow, &pendingOpenFlow{timer: timer})
return filter.Accept
}
func (e *userspaceEngine) onOpenTimeout(flow flowtrack.Tuple) {
e.mu.Lock()
of, ok := e.pendOpen[flow]
if !ok {
// Not a tracked flow, or already handled & deleted.
e.mu.Unlock()
return
}
delete(e.pendOpen, flow)
problem := of.problem
e.mu.Unlock()
if !problem.IsZero() {
e.logf("open-conn-track: timeout opening %v; peer reported problem: %v", flow, problem)
}
// Diagnose why it might've timed out.
pip, ok := e.PeerForIP(flow.DstAddr())
if !ok {
e.logf("open-conn-track: timeout opening %v; no associated peer node", flow)
return
}
n := pip.Node
if !n.IsWireGuardOnly() {
if n.DiscoKey().IsZero() {
e.logf("open-conn-track: timeout opening %v; peer node %v running pre-0.100", flow, n.Key().ShortString())
return
}
if n.DERP() == "" {
e.logf("open-conn-track: timeout opening %v; peer node %v not connected to any DERP relay", flow, n.Key().ShortString())
return
}
}
ps, found := e.getPeerStatusLite(n.Key())
if !found {
onlyZeroRoute := true // whether peerForIP returned n only because its /0 route matched
for i := range n.AllowedIPs().Len() {
r := n.AllowedIPs().At(i)
if r.Bits() != 0 && r.Contains(flow.DstAddr()) {
onlyZeroRoute = false
break
}
}
if onlyZeroRoute {
// This node was returned by peerForIP because
// its exit node /0 route(s) matched, but this
// might not be the exit node that's currently
// selected. Rather than log misleading
// errors, just don't log at all for now.
// TODO(bradfitz): update this code to be
// exit-node-aware and make peerForIP return
// the node of the currently selected exit
// node.
return
}
e.logf("open-conn-track: timeout opening %v; target node %v in netmap but unknown to WireGuard", flow, n.Key().ShortString())
return
}
// TODO(bradfitz): figure out what PeerStatus.LastHandshake
// is. It appears to be the last time we sent a handshake,
// which isn't what I expected. I thought it was when a
// handshake completed, which is what I want.
_ = ps.LastHandshake
online := "?"
if n.IsWireGuardOnly() {
online = "wg"
} else {
if v := n.Online(); v != nil {
if *v {
online = "yes"
} else {
online = "no"
}
}
if n.LastSeen() != nil && online != "yes" {
online += fmt.Sprintf(", lastseen=%v", durFmt(*n.LastSeen()))
}
}
e.logf("open-conn-track: timeout opening %v to node %v; online=%v, lastRecv=%v",
flow, n.Key().ShortString(),
online,
e.magicConn.LastRecvActivityOfNodeKey(n.Key()))
}
func durFmt(t time.Time) string {
if t.IsZero() {
return "never"
}
d := time.Since(t).Round(time.Second)
if d < 10*time.Minute {
// node.LastSeen times are rounded very coarsely, and
// we compare times from different clocks (server vs
// local), so negative is common when close. Format as
// "recent" if negative or actually recent.
return "recent"
}
return d.String()
}

91
vendor/tailscale.com/wgengine/router/callback.go generated vendored Normal file
View File

@@ -0,0 +1,91 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package router
import (
"sync"
"tailscale.com/net/dns"
)
// CallbackRouter is an implementation of both Router and dns.OSConfigurator.
// When either network or DNS settings are changed, SetBoth is called with both configs.
// Mainly used as a shim for OSes that want to set both network and
// DNS configuration simultaneously (Mac, iOS, Android).
type CallbackRouter struct {
SetBoth func(rcfg *Config, dcfg *dns.OSConfig) error
SplitDNS bool
// GetBaseConfigFunc optionally specifies a function to return the current DNS
// config in response to GetBaseConfig.
//
// If nil, reading the current config isn't supported and GetBaseConfig()
// will return ErrGetBaseConfigNotSupported.
GetBaseConfigFunc func() (dns.OSConfig, error)
// InitialMTU is the MTU the tun should be initialized with.
// Zero means don't change the MTU from the default. This MTU
// is applied only once, shortly after the TUN is created, and
// ignored thereafter.
InitialMTU uint32
mu sync.Mutex // protects all the following
didSetMTU bool // if we set the MTU already
rcfg *Config // last applied router config
dcfg *dns.OSConfig // last applied DNS config
}
// Up implements Router.
func (r *CallbackRouter) Up() error {
return nil // TODO: check that all callers have no need for initialization
}
// Set implements Router.
func (r *CallbackRouter) Set(rcfg *Config) error {
r.mu.Lock()
defer r.mu.Unlock()
if r.rcfg.Equal(rcfg) {
return nil
}
if r.didSetMTU == false {
r.didSetMTU = true
rcfg.NewMTU = int(r.InitialMTU)
}
r.rcfg = rcfg
return r.SetBoth(r.rcfg, r.dcfg)
}
// UpdateMagicsockPort implements the Router interface. This implementation
// does nothing and returns nil because this router does not currently need
// to know what the magicsock UDP port is.
func (r *CallbackRouter) UpdateMagicsockPort(_ uint16, _ string) error {
return nil
}
// SetDNS implements dns.OSConfigurator.
func (r *CallbackRouter) SetDNS(dcfg dns.OSConfig) error {
r.mu.Lock()
defer r.mu.Unlock()
if r.dcfg != nil && r.dcfg.Equal(dcfg) {
return nil
}
r.dcfg = &dcfg
return r.SetBoth(r.rcfg, r.dcfg)
}
// SupportsSplitDNS implements dns.OSConfigurator.
func (r *CallbackRouter) SupportsSplitDNS() bool {
return r.SplitDNS
}
func (r *CallbackRouter) GetBaseConfig() (dns.OSConfig, error) {
if r.GetBaseConfigFunc == nil {
return dns.OSConfig{}, dns.ErrGetBaseConfigNotSupported
}
return r.GetBaseConfigFunc()
}
func (r *CallbackRouter) Close() error {
return r.SetBoth(nil, nil) // TODO: check if makes sense
}

View File

@@ -0,0 +1,59 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package router
import (
"go4.org/netipx"
"tailscale.com/types/logger"
)
// ConsolidatingRoutes wraps a Router with logic that consolidates Routes
// whenever Set is called. It attempts to consolidate cfg.Routes into the
// smallest possible set.
func ConsolidatingRoutes(logf logger.Logf, router Router) Router {
return &consolidatingRouter{Router: router, logf: logger.WithPrefix(logf, "router: ")}
}
type consolidatingRouter struct {
Router
logf logger.Logf
}
// Set implements Router and attempts to consolidate cfg.Routes into the
// smallest possible set.
func (cr *consolidatingRouter) Set(cfg *Config) error {
return cr.Router.Set(cr.consolidateRoutes(cfg))
}
func (cr *consolidatingRouter) consolidateRoutes(cfg *Config) *Config {
if cfg == nil {
return nil
}
if len(cfg.Routes) < 2 {
return cfg
}
if len(cfg.Routes) == 2 && cfg.Routes[0].Addr().Is4() != cfg.Routes[1].Addr().Is4() {
return cfg
}
var builder netipx.IPSetBuilder
for _, route := range cfg.Routes {
builder.AddPrefix(route)
}
set, err := builder.IPSet()
if err != nil {
cr.logf("consolidateRoutes failed, keeping existing routes: %s", err)
return cfg
}
newRoutes := set.Prefixes()
oldLength := len(cfg.Routes)
newLength := len(newRoutes)
if oldLength == newLength {
// Nothing consolidated, return as-is.
return cfg
}
cr.logf("consolidated %d routes down to %d", oldLength, newLength)
newCfg := *cfg
newCfg.Routes = newRoutes
return &newCfg
}

View File

@@ -0,0 +1,834 @@
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2019 WireGuard LLC. All Rights Reserved.
*/
package router
import (
"errors"
"fmt"
"log"
"net/netip"
"slices"
"sort"
"time"
"tailscale.com/health"
"tailscale.com/net/netmon"
"tailscale.com/net/tsaddr"
"tailscale.com/net/tstun"
"tailscale.com/util/multierr"
"tailscale.com/wgengine/winnet"
ole "github.com/go-ole/go-ole"
"github.com/tailscale/wireguard-go/tun"
"go4.org/netipx"
"golang.org/x/sys/windows"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
)
// monitorDefaultRoutes subscribes to route change events and updates
// the Tailscale tunnel interface's MTU to match that of the
// underlying default route.
//
// This is an attempt at making the MTU mostly correct, but in
// practice this entire piece of code ends up just using the 1280
// value passed in at device construction time. This code might make
// the MTU go lower due to very low-MTU IPv4 interfaces.
//
// TODO: this code is insufficient to control the MTU correctly. The
// correct way to do it is per-peer PMTU discovery, and synthesizing
// ICMP fragmentation-needed messages within tailscaled. This code may
// address a few rare corner cases, but is unlikely to significantly
// help with MTU issues compared to a static 1280B implementation.
func monitorDefaultRoutes(tun *tun.NativeTun) (*winipcfg.RouteChangeCallback, error) {
ourLuid := winipcfg.LUID(tun.LUID())
lastMtu := uint32(0)
doIt := func() error {
mtu, err := getDefaultRouteMTU()
if err != nil {
return fmt.Errorf("error getting default route MTU: %w", err)
}
if mtu > 0 && (lastMtu == 0 || lastMtu != mtu) {
iface, err := ourLuid.IPInterface(windows.AF_INET)
if err != nil {
if !errors.Is(err, windows.ERROR_NOT_FOUND) {
return fmt.Errorf("getting v4 interface: %w", err)
}
} else {
iface.NLMTU = mtu - 80
// If the TUN device was created with a smaller MTU,
// though, such as 1280, we don't want to go bigger
// than configured. (See the comment on minimalMTU in
// the wgengine package.)
if min, err := tun.MTU(); err == nil && min < int(iface.NLMTU) {
iface.NLMTU = uint32(min)
}
if iface.NLMTU < 576 {
iface.NLMTU = 576
}
err = iface.Set()
if err != nil {
return fmt.Errorf("error setting v4 MTU: %w", err)
}
tun.ForceMTU(int(iface.NLMTU))
}
iface, err = ourLuid.IPInterface(windows.AF_INET6)
if err != nil {
if !errors.Is(err, windows.ERROR_NOT_FOUND) {
return fmt.Errorf("error getting v6 interface: %w", err)
}
} else {
iface.NLMTU = mtu - 80
if iface.NLMTU < 1280 {
iface.NLMTU = 1280
}
err = iface.Set()
if err != nil {
return fmt.Errorf("error setting v6 MTU: %w", err)
}
}
lastMtu = mtu
}
return nil
}
err := doIt()
if err != nil {
return nil, err
}
cb, err := winipcfg.RegisterRouteChangeCallback(func(notificationType winipcfg.MibNotificationType, route *winipcfg.MibIPforwardRow2) {
//fmt.Printf("MonitorDefaultRoutes: changed: %v\n", route.DestinationPrefix)
if route.DestinationPrefix.PrefixLength == 0 {
_ = doIt()
}
})
if err != nil {
return nil, err
}
return cb, nil
}
func getDefaultRouteMTU() (uint32, error) {
mtus, err := netmon.NonTailscaleMTUs()
if err != nil {
return 0, err
}
routes, err := winipcfg.GetIPForwardTable2(windows.AF_INET)
if err != nil {
return 0, err
}
best := ^uint32(0)
mtu := uint32(0)
for _, route := range routes {
if route.DestinationPrefix.PrefixLength != 0 {
continue
}
routeMTU := mtus[route.InterfaceLUID]
if routeMTU == 0 {
continue
}
if route.Metric < best {
best = route.Metric
mtu = routeMTU
}
}
routes, err = winipcfg.GetIPForwardTable2(windows.AF_INET6)
if err != nil {
return 0, err
}
best = ^uint32(0)
for _, route := range routes {
if route.DestinationPrefix.PrefixLength != 0 {
continue
}
routeMTU := mtus[route.InterfaceLUID]
if routeMTU == 0 {
continue
}
if route.Metric < best {
best = route.Metric
if routeMTU < mtu {
mtu = routeMTU
}
}
}
return mtu, nil
}
// setPrivateNetwork marks the provided network adapter's category to private.
// It returns (false, nil) if the adapter was not found.
func setPrivateNetwork(ifcLUID winipcfg.LUID) (bool, error) {
// NLM_NETWORK_CATEGORY values.
const (
categoryPublic = 0
categoryPrivate = 1
categoryDomain = 2
)
ifcGUID, err := ifcLUID.GUID()
if err != nil {
return false, fmt.Errorf("ifcLUID.GUID: %v", err)
}
// aaron: DO NOT call Initialize() or Uninitialize() on c!
// We've already handled that process-wide.
var c ole.Connection
m, err := winnet.NewNetworkListManager(&c)
if err != nil {
return false, fmt.Errorf("winnet.NewNetworkListManager: %v", err)
}
defer m.Release()
cl, err := m.GetNetworkConnections()
if err != nil {
return false, fmt.Errorf("m.GetNetworkConnections: %v", err)
}
defer cl.Release()
for _, nco := range cl {
aid, err := nco.GetAdapterId()
if err != nil {
return false, fmt.Errorf("nco.GetAdapterId: %v", err)
}
if aid != ifcGUID.String() {
continue
}
n, err := nco.GetNetwork()
if err != nil {
return false, fmt.Errorf("GetNetwork: %v", err)
}
defer n.Release()
cat, err := n.GetCategory()
if err != nil {
return false, fmt.Errorf("GetCategory: %v", err)
}
if cat != categoryPrivate && cat != categoryDomain {
if err := n.SetCategory(categoryPrivate); err != nil {
return false, fmt.Errorf("SetCategory: %v", err)
}
}
return true, nil
}
return false, nil
}
// interfaceFromLUID returns IPAdapterAddresses with specified LUID.
func interfaceFromLUID(luid winipcfg.LUID, flags winipcfg.GAAFlags) (*winipcfg.IPAdapterAddresses, error) {
addresses, err := winipcfg.GetAdaptersAddresses(windows.AF_UNSPEC, flags)
if err != nil {
return nil, err
}
for _, addr := range addresses {
if addr.LUID == luid {
return addr, nil
}
}
return nil, fmt.Errorf("interfaceFromLUID: interface with LUID %v not found", luid)
}
var networkCategoryWarnable = health.Register(&health.Warnable{
Code: "set-network-category-failed",
Severity: health.SeverityMedium,
Title: "Windows network configuration failed",
Text: func(args health.Args) string {
return fmt.Sprintf("Failed to set the network category to private on the Tailscale adapter. This may prevent Tailscale from working correctly. Error: %s", args[health.ArgError])
},
MapDebugFlag: "warn-network-category-unhealthy",
})
func configureInterface(cfg *Config, tun *tun.NativeTun, ht *health.Tracker) (retErr error) {
var mtu = tstun.DefaultTUNMTU()
luid := winipcfg.LUID(tun.LUID())
iface, err := interfaceFromLUID(luid,
// Issue 474: on early boot, when the network is still
// coming up, if the Tailscale service comes up first,
// the Tailscale adapter it finds might not have the
// IPv4 service available yet? Try this flag:
winipcfg.GAAFlagIncludeAllInterfaces,
)
if err != nil {
return fmt.Errorf("getting interface: %w", err)
}
// Send non-nil return errors to retErrc, to interrupt our background
// setPrivateNetwork goroutine.
retErrc := make(chan error, 1)
defer func() {
if retErr != nil {
retErrc <- retErr
}
}()
go func() {
// It takes a weirdly long time for Windows to notice the
// new interface has come up. Poll periodically until it
// does.
const tries = 20
for i := range tries {
found, err := setPrivateNetwork(luid)
if err != nil {
ht.SetUnhealthy(networkCategoryWarnable, health.Args{health.ArgError: err.Error()})
log.Printf("setPrivateNetwork(try=%d): %v", i, err)
} else {
ht.SetHealthy(networkCategoryWarnable)
if found {
if i > 0 {
log.Printf("setPrivateNetwork(try=%d): success", i)
}
return
}
log.Printf("setPrivateNetwork(try=%d): not found", i)
}
select {
case <-time.After(time.Second):
case <-retErrc:
return
}
}
log.Printf("setPrivateNetwork: adapter LUID %v not found after %d tries, giving up", luid, tries)
}()
// Figure out which of IPv4 and IPv6 are available. Both protocols
// can be disabled on a per-interface basis by the user, as well
// as globally via a registry policy. We skip programming anything
// related to the disabled protocols, since by definition they're
// unusable.
ipif4, err := iface.LUID.IPInterface(windows.AF_INET)
if err != nil {
if !errors.Is(err, windows.ERROR_NOT_FOUND) {
return fmt.Errorf("getting AF_INET interface: %w", err)
}
log.Printf("AF_INET interface not found on Tailscale adapter, skipping IPv4 programming")
ipif4 = nil
}
ipif6, err := iface.LUID.IPInterface(windows.AF_INET6)
if err != nil {
if !errors.Is(err, windows.ERROR_NOT_FOUND) {
return fmt.Errorf("getting AF_INET6 interface: %w", err)
}
log.Printf("AF_INET6 interface not found on Tailscale adapter, skipping IPv6 programming")
ipif6 = nil
}
// Windows requires routes to have a nexthop. Routes created using
// the interface's local IP address or an unspecified IP address
// ("0.0.0.0" or "::") as the nexthop are considered on-link routes.
//
// Notably, Windows treats on-link subnet routes differently, reserving the last
// IP in the range as the broadcast IP and therefore prohibiting TCP connections
// to it, resulting in WSA error 10049: "The requested address is not valid in its context."
// This does not happen with single-host routes, such as routes to Tailscale IP addresses,
// but becomes a problem with advertised subnets when all IPs in the range should be reachable.
// See https://github.com/tailscale/support-escalations/issues/57 for details.
//
// For routes such as ours where the nexthop is meaningless, we can use an
// arbitrary nexthop address, such as TailscaleServiceIP, to prevent the
// routes from being marked as on-link. We can still create on-link routes
// for single-host Tailscale routes, but we shouldn't attempt to create a
// route for the interface's own IP.
var localAddr4, localAddr6 netip.Addr
var gatewayAddr4, gatewayAddr6 netip.Addr
addresses := make([]netip.Prefix, 0, len(cfg.LocalAddrs))
for _, addr := range cfg.LocalAddrs {
if (addr.Addr().Is4() && ipif4 == nil) || (addr.Addr().Is6() && ipif6 == nil) {
// Can't program addresses for disabled protocol.
continue
}
addresses = append(addresses, addr)
if addr.Addr().Is4() && !gatewayAddr4.IsValid() {
localAddr4 = addr.Addr()
gatewayAddr4 = tsaddr.TailscaleServiceIP()
} else if addr.Addr().Is6() && !gatewayAddr6.IsValid() {
localAddr6 = addr.Addr()
gatewayAddr6 = tsaddr.TailscaleServiceIPv6()
}
}
var routes []*routeData
foundDefault4 := false
foundDefault6 := false
for _, route := range cfg.Routes {
if (route.Addr().Is4() && ipif4 == nil) || (route.Addr().Is6() && ipif6 == nil) {
// Can't program routes for disabled protocol.
continue
}
if route.Addr().Is6() && !gatewayAddr6.IsValid() {
// Windows won't let us set IPv6 routes without having an
// IPv6 local address set. However, when we've configured
// a default route, we want to forcibly grab IPv6 traffic
// even if the v6 overlay network isn't configured. To do
// that, we add a dummy local IPv6 address to serve as a
// route source.
ip := tsaddr.Tailscale4To6Placeholder()
addresses = append(addresses, netip.PrefixFrom(ip, ip.BitLen()))
gatewayAddr6 = ip
} else if route.Addr().Is4() && !gatewayAddr4.IsValid() {
// TODO: do same dummy behavior as v6?
return errors.New("due to a Windows limitation, one cannot have interface routes without an interface address")
}
var gateway, localAddr netip.Addr
if route.Addr().Is4() {
localAddr = localAddr4
gateway = gatewayAddr4
} else if route.Addr().Is6() {
localAddr = localAddr6
gateway = gatewayAddr6
}
switch destAddr := route.Addr().Unmap(); {
case destAddr == localAddr:
// no need to add a route for the interface's
// own IP. The kernel does that for us.
// If we try to replace it, we'll fail to
// add the route unless NextHop is set, but
// then the interface's IP won't be pingable.
continue
case route.IsSingleIP() && (destAddr == gateway || tsaddr.IsTailscaleIP(destAddr)):
// add an on-link route if the destination
// is the nexthop itself or a single Tailscale IP.
gateway = localAddr
}
r := &routeData{
RouteData: winipcfg.RouteData{
Destination: route,
NextHop: gateway,
Metric: 0,
},
}
if route.Addr().Is4() {
if route.Bits() == 0 {
foundDefault4 = true
}
} else if route.Addr().Is6() {
if route.Bits() == 0 {
foundDefault6 = true
}
}
routes = append(routes, r)
}
err = syncAddresses(iface, addresses)
if err != nil {
return fmt.Errorf("syncAddresses: %w", err)
}
slices.SortFunc(routes, (*routeData).Compare)
deduplicatedRoutes := []*routeData{}
for i := range len(routes) {
// There's only one way to get to a given IP+Mask, so delete
// all matches after the first.
if i > 0 && routes[i].Destination == routes[i-1].Destination {
continue
}
deduplicatedRoutes = append(deduplicatedRoutes, routes[i])
}
// Re-read interface after syncAddresses.
iface, err = interfaceFromLUID(luid,
// Issue 474: on early boot, when the network is still
// coming up, if the Tailscale service comes up first,
// the Tailscale adapter it finds might not have the
// IPv4 service available yet? Try this flag:
winipcfg.GAAFlagIncludeAllInterfaces,
)
if err != nil {
return fmt.Errorf("getting interface: %w", err)
}
var errAcc error
err = syncRoutes(iface, deduplicatedRoutes, cfg.LocalAddrs)
if err != nil && errAcc == nil {
log.Printf("setroutes: %v", err)
errAcc = err
}
if ipif4 != nil {
ipif4, err = iface.LUID.IPInterface(windows.AF_INET)
if err != nil {
return fmt.Errorf("getting AF_INET interface: %w", err)
}
if foundDefault4 {
ipif4.UseAutomaticMetric = false
ipif4.Metric = 0
}
if mtu > 0 {
ipif4.NLMTU = uint32(mtu)
tun.ForceMTU(int(ipif4.NLMTU))
}
err = ipif4.Set()
if err != nil && errAcc == nil {
errAcc = err
}
}
if ipif6 != nil {
ipif6, err = iface.LUID.IPInterface(windows.AF_INET6)
if err != nil {
return fmt.Errorf("getting AF_INET6 interface: %w", err)
} else {
if foundDefault6 {
ipif6.UseAutomaticMetric = false
ipif6.Metric = 0
}
if mtu > 0 {
ipif6.NLMTU = uint32(mtu)
}
ipif6.DadTransmits = 0
ipif6.RouterDiscoveryBehavior = winipcfg.RouterDiscoveryDisabled
err = ipif6.Set()
if err != nil && errAcc == nil {
errAcc = err
}
}
}
return errAcc
}
func netCompare(a, b netip.Prefix) int {
aip, bip := a.Addr().Unmap(), b.Addr().Unmap()
v := aip.Compare(bip)
if v != 0 {
return v
}
if a.Bits() == b.Bits() {
return 0
}
// narrower first
if a.Bits() > b.Bits() {
return -1
}
return 1
}
func sortNets(s []netip.Prefix) {
sort.Slice(s, func(i, j int) bool {
return netCompare(s[i], s[j]) == -1
})
}
// deltaNets returns the changes to turn a into b.
func deltaNets(a, b []netip.Prefix) (add, del []netip.Prefix) {
add = make([]netip.Prefix, 0, len(b))
del = make([]netip.Prefix, 0, len(a))
sortNets(a)
sortNets(b)
i := 0
j := 0
for i < len(a) && j < len(b) {
switch netCompare(a[i], b[j]) {
case -1:
// a < b, delete
del = append(del, a[i])
i++
case 0:
// a == b, no diff
i++
j++
case 1:
// a > b, add missing entry
add = append(add, b[j])
j++
default:
panic("unexpected compare result")
}
}
del = append(del, a[i:]...)
add = append(add, b[j:]...)
return
}
func isIPv6LinkLocal(a netip.Prefix) bool {
return a.Addr().Is6() && a.Addr().IsLinkLocalUnicast()
}
// ipAdapterUnicastAddressToPrefix converts windows.IpAdapterUnicastAddress to netip.Prefix
func ipAdapterUnicastAddressToPrefix(u *windows.IpAdapterUnicastAddress) netip.Prefix {
ip, _ := netip.AddrFromSlice(u.Address.IP())
return netip.PrefixFrom(ip.Unmap(), int(u.OnLinkPrefixLength))
}
// unicastIPNets returns all unicast net.IPNet for ifc interface.
func unicastIPNets(ifc *winipcfg.IPAdapterAddresses) []netip.Prefix {
var nets []netip.Prefix
for addr := ifc.FirstUnicastAddress; addr != nil; addr = addr.Next {
nets = append(nets, ipAdapterUnicastAddressToPrefix(addr))
}
return nets
}
// syncAddresses incrementally sets the interface's unicast IP addresses,
// doing the minimum number of AddAddresses & DeleteAddress calls.
// This avoids the full FlushAddresses.
//
// Any IPv6 link-local addresses are not deleted out of caution as some
// configurations may repeatedly re-add them. Link-local addresses are adjusted
// to set SkipAsSource. SkipAsSource prevents the addresses from being added to
// DNS locally or remotely and from being picked as a source address for
// outgoing packets with unspecified sources. See #4647 and
// https://web.archive.org/web/20200912120956/https://devblogs.microsoft.com/scripting/use-powershell-to-change-ip-behavior-with-skipassource/
func syncAddresses(ifc *winipcfg.IPAdapterAddresses, want []netip.Prefix) error {
var erracc error
got := unicastIPNets(ifc)
add, del := deltaNets(got, want)
ll := make([]netip.Prefix, 0)
for _, a := range del {
// do not delete link-local addresses, and collect them for later
// applying SkipAsSource.
if isIPv6LinkLocal(a) {
ll = append(ll, a)
continue
}
err := ifc.LUID.DeleteIPAddress(a)
if err != nil {
erracc = fmt.Errorf("deleting IP %q: %w", a, err)
}
}
for _, a := range add {
err := ifc.LUID.AddIPAddress(a)
if err != nil {
erracc = fmt.Errorf("adding IP %q: %w", a, err)
}
}
for _, a := range ll {
mib, err := ifc.LUID.IPAddress(a.Addr())
if err != nil {
erracc = fmt.Errorf("setting skip-as-source on IP %q: unable to retrieve MIB: %w", a, err)
continue
}
if !mib.SkipAsSource {
mib.SkipAsSource = true
if err := mib.Set(); err != nil {
erracc = fmt.Errorf("setting skip-as-source on IP %q: unable to set MIB: %w", a, err)
}
}
}
return erracc
}
// routeData wraps winipcfg.RouteData with an additional field that permits
// caching of the associated MibIPForwardRow2; by keeping it around, we can
// avoid unnecessary (and slow) lookups of information that we already have.
type routeData struct {
winipcfg.RouteData
Row *winipcfg.MibIPforwardRow2
}
func (rd *routeData) Less(other *routeData) bool {
return rd.Compare(other) < 0
}
func (rd *routeData) Compare(other *routeData) int {
v := rd.Destination.Addr().Compare(other.Destination.Addr())
if v != 0 {
return v
}
// Narrower masks first
b1, b2 := rd.Destination.Bits(), other.Destination.Bits()
if b1 != b2 {
if b1 > b2 {
return -1
}
return 1
}
// No nexthop before non-empty nexthop
v = rd.NextHop.Compare(other.NextHop)
if v != 0 {
return v
}
// Lower metrics first
if rd.Metric < other.Metric {
return -1
} else if rd.Metric > other.Metric {
return 1
}
return 0
}
func deltaRouteData(a, b []*routeData) (add, del []*routeData) {
add = make([]*routeData, 0, len(b))
del = make([]*routeData, 0, len(a))
slices.SortFunc(a, (*routeData).Compare)
slices.SortFunc(b, (*routeData).Compare)
i := 0
j := 0
for i < len(a) && j < len(b) {
switch a[i].Compare(b[j]) {
case -1:
// a < b, delete
del = append(del, a[i])
i++
case 0:
// a == b, no diff
i++
j++
case 1:
// a > b, add missing entry
add = append(add, b[j])
j++
default:
panic("unexpected compare result")
}
}
del = append(del, a[i:]...)
add = append(add, b[j:]...)
return
}
// getInterfaceRoutes returns all the interface's routes.
// Corresponds to GetIpForwardTable2 function, but filtered by interface.
func getInterfaceRoutes(ifc *winipcfg.IPAdapterAddresses, family winipcfg.AddressFamily) (matches []*winipcfg.MibIPforwardRow2, err error) {
routes, err := winipcfg.GetIPForwardTable2(family)
if err != nil {
return nil, err
}
for i := range routes {
if routes[i].InterfaceLUID == ifc.LUID {
matches = append(matches, &routes[i])
}
}
return
}
func getAllInterfaceRoutes(ifc *winipcfg.IPAdapterAddresses) ([]*routeData, error) {
routes4, err := getInterfaceRoutes(ifc, windows.AF_INET)
if err != nil {
return nil, err
}
routes6, err := getInterfaceRoutes(ifc, windows.AF_INET6)
if err != nil {
// TODO: what if v6 unavailable?
return nil, err
}
rd := make([]*routeData, 0, len(routes4)+len(routes6))
for _, r := range routes4 {
rd = append(rd, &routeData{
RouteData: winipcfg.RouteData{
Destination: r.DestinationPrefix.Prefix(),
NextHop: r.NextHop.Addr(),
Metric: r.Metric,
},
Row: r,
})
}
for _, r := range routes6 {
rd = append(rd, &routeData{
RouteData: winipcfg.RouteData{
Destination: r.DestinationPrefix.Prefix(),
NextHop: r.NextHop.Addr(),
Metric: r.Metric,
},
Row: r,
})
}
return rd, nil
}
// filterRoutes removes routes that have been added by Windows and should not
// be managed by us.
func filterRoutes(routes []*routeData, dontDelete []netip.Prefix) []*routeData {
ddm := make(map[netip.Prefix]bool)
for _, dd := range dontDelete {
// See issue 1448: we don't want to touch the routes added
// by Windows for our interface addresses.
ddm[dd] = true
}
for _, r := range routes {
// We don't want to touch broadcast routes that Windows adds.
nr := r.Destination
if !nr.IsValid() {
continue
}
if nr.IsSingleIP() {
continue
}
lastIP := netipx.RangeOfPrefix(nr).To()
ddm[netip.PrefixFrom(lastIP, lastIP.BitLen())] = true
}
filtered := make([]*routeData, 0, len(routes))
for _, r := range routes {
rr := r.Destination
if rr.IsValid() && ddm[rr] {
continue
}
filtered = append(filtered, r)
}
return filtered
}
// syncRoutes incrementally sets multiples routes on an interface.
// This avoids a full ifc.FlushRoutes call.
// dontDelete is a list of interface address routes that the
// synchronization logic should never delete.
func syncRoutes(ifc *winipcfg.IPAdapterAddresses, want []*routeData, dontDelete []netip.Prefix) error {
existingRoutes, err := getAllInterfaceRoutes(ifc)
if err != nil {
return err
}
got := filterRoutes(existingRoutes, dontDelete)
add, del := deltaRouteData(got, want)
var errs []error
for _, a := range del {
var err error
if a.Row == nil {
// DeleteRoute requires a routing table lookup, so only do that if
// a does not already have the row.
err = ifc.LUID.DeleteRoute(a.Destination, a.NextHop)
} else {
// Otherwise, delete the row directly.
err = a.Row.Delete()
}
if err != nil {
dstStr := a.Destination.String()
if dstStr == "169.254.255.255/32" {
// Issue 785. Ignore these routes
// failing to delete. Harmless.
// TODO(maisem): do we still need this?
continue
}
errs = append(errs, fmt.Errorf("deleting route %v: %w", dstStr, err))
}
}
for _, a := range add {
err := ifc.LUID.AddRoute(a.Destination, a.NextHop, a.Metric)
if err != nil {
errs = append(errs, fmt.Errorf("adding route %v: %w", &a.Destination, err))
}
}
return multierr.New(errs...)
}

110
vendor/tailscale.com/wgengine/router/router.go generated vendored Normal file
View File

@@ -0,0 +1,110 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package router presents an interface to manipulate the host network
// stack's state.
package router
import (
"net/netip"
"reflect"
"github.com/tailscale/wireguard-go/tun"
"tailscale.com/health"
"tailscale.com/net/netmon"
"tailscale.com/types/logger"
"tailscale.com/types/preftype"
)
// Router is responsible for managing the system network stack.
//
// There is typically only one instance of this interface per process.
type Router interface {
// Up brings the router up.
Up() error
// Set updates the OS network stack with a new Config. It may be
// called multiple times with identical Configs, which the
// implementation should handle gracefully.
Set(*Config) error
// UpdateMagicsockPort tells the OS network stack what port magicsock
// is currently listening on, so it can be threaded through firewalls
// and such. This is distinct from Set() since magicsock may rebind
// ports independently from the Config changing.
//
// network should be either "udp4" or "udp6".
UpdateMagicsockPort(port uint16, network string) error
// Close closes the router.
Close() error
}
// New returns a new Router for the current platform, using the
// provided tun device.
//
// If netMon is nil, it's not used. It's currently (2021-07-20) only
// used on Linux in some situations.
func New(logf logger.Logf, tundev tun.Device, netMon *netmon.Monitor, health *health.Tracker) (Router, error) {
logf = logger.WithPrefix(logf, "router: ")
return newUserspaceRouter(logf, tundev, netMon, health)
}
// CleanUp restores the system network configuration to its original state
// in case the Tailscale daemon terminated without closing the router.
// No other state needs to be instantiated before this runs.
func CleanUp(logf logger.Logf, netMon *netmon.Monitor, interfaceName string) {
cleanUp(logf, interfaceName)
}
// Config is the subset of Tailscale configuration that is relevant to
// the OS's network stack.
type Config struct {
// LocalAddrs are the address(es) for this node. This is
// typically one IPv4/32 (the 100.x.y.z CGNAT) and one
// IPv6/128 (Tailscale ULA).
LocalAddrs []netip.Prefix
// Routes are the routes that point into the Tailscale
// interface. These are the /32 and /128 routes to peers, as
// well as any other subnets that peers are advertising and
// this node has chosen to use.
Routes []netip.Prefix
// LocalRoutes are the routes that should not be routed through Tailscale.
// There are no priorities set in how these routes are added, normal
// routing rules apply.
LocalRoutes []netip.Prefix
// NewMTU is currently only used by the MacOS network extension
// app to set the MTU of the tun in the router configuration
// callback. If zero, the MTU is unchanged.
NewMTU int
// SubnetRoutes is the list of subnets that this node is
// advertising to other Tailscale nodes.
// As of 2023-10-11, this field is only used for network
// flow logging and is otherwise ignored.
SubnetRoutes []netip.Prefix
// Linux-only things below, ignored on other platforms.
SNATSubnetRoutes bool // SNAT traffic to local subnets
StatefulFiltering bool // Apply stateful filtering to inbound connections
NetfilterMode preftype.NetfilterMode // how much to manage netfilter rules
NetfilterKind string // what kind of netfilter to use (nftables, iptables)
}
func (a *Config) Equal(b *Config) bool {
if a == nil && b == nil {
return true
}
if (a == nil) != (b == nil) {
return false
}
return reflect.DeepEqual(a, b)
}
// shutdownConfig is a routing configuration that removes all router
// state from the OS. It's the config used when callers pass in a nil
// Config.
var shutdownConfig = Config{}

19
vendor/tailscale.com/wgengine/router/router_darwin.go generated vendored Normal file
View File

@@ -0,0 +1,19 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package router
import (
"github.com/tailscale/wireguard-go/tun"
"tailscale.com/health"
"tailscale.com/net/netmon"
"tailscale.com/types/logger"
)
func newUserspaceRouter(logf logger.Logf, tundev tun.Device, netMon *netmon.Monitor, health *health.Tracker) (Router, error) {
return newUserspaceBSDRouter(logf, tundev, netMon, health)
}
func cleanUp(logger.Logf, string) {
// Nothing to do.
}

24
vendor/tailscale.com/wgengine/router/router_default.go generated vendored Normal file
View File

@@ -0,0 +1,24 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !windows && !linux && !darwin && !openbsd && !freebsd
package router
import (
"fmt"
"runtime"
"github.com/tailscale/wireguard-go/tun"
"tailscale.com/health"
"tailscale.com/net/netmon"
"tailscale.com/types/logger"
)
func newUserspaceRouter(logf logger.Logf, tunDev tun.Device, netMon *netmon.Monitor, health *health.Tracker) (Router, error) {
return nil, fmt.Errorf("unsupported OS %q", runtime.GOOS)
}
func cleanUp(logf logger.Logf, interfaceName string) {
// Nothing to do here.
}

38
vendor/tailscale.com/wgengine/router/router_fake.go generated vendored Normal file
View File

@@ -0,0 +1,38 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package router
import (
"tailscale.com/types/logger"
)
// NewFake returns a Router that does nothing when called and always
// returns nil errors.
func NewFake(logf logger.Logf) Router {
return fakeRouter{logf: logf}
}
type fakeRouter struct {
logf logger.Logf
}
func (r fakeRouter) Up() error {
r.logf("[v1] warning: fakeRouter.Up: not implemented.")
return nil
}
func (r fakeRouter) Set(cfg *Config) error {
r.logf("[v1] warning: fakeRouter.Set: not implemented.")
return nil
}
func (r fakeRouter) UpdateMagicsockPort(_ uint16, _ string) error {
r.logf("[v1] warning: fakeRouter.UpdateMagicsockPort: not implemented.")
return nil
}
func (r fakeRouter) Close() error {
r.logf("[v1] warning: fakeRouter.Close: not implemented.")
return nil
}

31
vendor/tailscale.com/wgengine/router/router_freebsd.go generated vendored Normal file
View File

@@ -0,0 +1,31 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package router
import (
"github.com/tailscale/wireguard-go/tun"
"tailscale.com/health"
"tailscale.com/net/netmon"
"tailscale.com/types/logger"
)
// For now this router only supports the userspace WireGuard implementations.
//
// Work is currently underway for an in-kernel FreeBSD implementation of wireguard
// https://svnweb.freebsd.org/base?view=revision&revision=357986
func newUserspaceRouter(logf logger.Logf, tundev tun.Device, netMon *netmon.Monitor, health *health.Tracker) (Router, error) {
return newUserspaceBSDRouter(logf, tundev, netMon, health)
}
func cleanUp(logf logger.Logf, interfaceName string) {
// If the interface was left behind, ifconfig down will not remove it.
// In fact, this will leave a system in a tainted state where starting tailscaled
// will result in "interface tailscale0 already exists"
// until the defunct interface is ifconfig-destroyed.
ifup := []string{"ifconfig", interfaceName, "destroy"}
if out, err := cmd(ifup...).CombinedOutput(); err != nil {
logf("ifconfig destroy: %v\n%s", err, out)
}
}

1562
vendor/tailscale.com/wgengine/router/router_linux.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

249
vendor/tailscale.com/wgengine/router/router_openbsd.go generated vendored Normal file
View File

@@ -0,0 +1,249 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package router
import (
"errors"
"fmt"
"log"
"net/netip"
"os/exec"
"github.com/tailscale/wireguard-go/tun"
"go4.org/netipx"
"tailscale.com/health"
"tailscale.com/net/netmon"
"tailscale.com/types/logger"
"tailscale.com/util/set"
)
// For now this router only supports the WireGuard userspace implementation.
// There is an experimental kernel version in the works for OpenBSD:
// https://git.zx2c4.com/wireguard-openbsd.
type openbsdRouter struct {
logf logger.Logf
netMon *netmon.Monitor
tunname string
local4 netip.Prefix
local6 netip.Prefix
routes set.Set[netip.Prefix]
}
func newUserspaceRouter(logf logger.Logf, tundev tun.Device, netMon *netmon.Monitor, health *health.Tracker) (Router, error) {
tunname, err := tundev.Name()
if err != nil {
return nil, err
}
return &openbsdRouter{
logf: logf,
netMon: netMon,
tunname: tunname,
}, nil
}
func cmd(args ...string) *exec.Cmd {
if len(args) == 0 {
log.Fatalf("exec.Cmd(%#v) invalid; need argv[0]", args)
}
return exec.Command(args[0], args[1:]...)
}
func (r *openbsdRouter) Up() error {
ifup := []string{"ifconfig", r.tunname, "up"}
if out, err := cmd(ifup...).CombinedOutput(); err != nil {
r.logf("running ifconfig failed: %v\n%s", err, out)
return err
}
return nil
}
func inet(p netip.Prefix) string {
if p.Addr().Is6() {
return "inet6"
}
return "inet"
}
func (r *openbsdRouter) Set(cfg *Config) error {
if cfg == nil {
cfg = &shutdownConfig
}
// TODO: support configuring multiple local addrs on interface.
if len(cfg.LocalAddrs) == 0 {
return nil
}
numIPv4 := 0
numIPv6 := 0
localAddr4 := netip.Prefix{}
localAddr6 := netip.Prefix{}
for _, addr := range cfg.LocalAddrs {
if addr.Addr().Is4() {
numIPv4++
localAddr4 = addr
}
if addr.Addr().Is6() {
numIPv6++
localAddr6 = addr
}
}
if numIPv4 > 1 || numIPv6 > 1 {
return errors.New("openbsd doesn't support setting multiple local addrs yet")
}
var errq error
if localAddr4 != r.local4 {
if r.local4.IsValid() {
addrdel := []string{"ifconfig", r.tunname,
"inet", r.local4.String(), "-alias"}
out, err := cmd(addrdel...).CombinedOutput()
if err != nil {
r.logf("addr del failed: %v: %v\n%s", addrdel, err, out)
if errq == nil {
errq = err
}
}
routedel := []string{"route", "-q", "-n",
"del", "-inet", r.local4.String(),
"-iface", r.local4.Addr().String()}
if out, err := cmd(routedel...).CombinedOutput(); err != nil {
r.logf("route del failed: %v: %v\n%s", routedel, err, out)
if errq == nil {
errq = err
}
}
}
if localAddr4.IsValid() {
addradd := []string{"ifconfig", r.tunname,
"inet", localAddr4.String(), "alias"}
out, err := cmd(addradd...).CombinedOutput()
if err != nil {
r.logf("addr add failed: %v: %v\n%s", addradd, err, out)
if errq == nil {
errq = err
}
}
routeadd := []string{"route", "-q", "-n",
"add", "-inet", localAddr4.String(),
"-iface", localAddr4.Addr().String()}
if out, err := cmd(routeadd...).CombinedOutput(); err != nil {
r.logf("route add failed: %v: %v\n%s", routeadd, err, out)
if errq == nil {
errq = err
}
}
}
}
if localAddr6.IsValid() {
// in https://github.com/tailscale/tailscale/issues/1307 we made
// FreeBSD use a /48 for IPv6 addresses, which is nice because we
// don't need to additionally add routing entries. Do that here too.
localAddr6 = netip.PrefixFrom(localAddr6.Addr(), 48)
}
if localAddr6 != r.local6 {
if r.local6.IsValid() {
addrdel := []string{"ifconfig", r.tunname,
"inet6", r.local6.String(), "delete"}
out, err := cmd(addrdel...).CombinedOutput()
if err != nil {
r.logf("addr del failed: %v: %v\n%s", addrdel, err, out)
if errq == nil {
errq = err
}
}
}
if localAddr6.IsValid() {
addradd := []string{"ifconfig", r.tunname,
"inet6", localAddr6.String()}
out, err := cmd(addradd...).CombinedOutput()
if err != nil {
r.logf("addr add failed: %v: %v\n%s", addradd, err, out)
if errq == nil {
errq = err
}
}
}
}
newRoutes := set.Set[netip.Prefix]{}
for _, route := range cfg.Routes {
newRoutes.Add(route)
}
for route := range r.routes {
if _, keep := newRoutes[route]; !keep {
net := netipx.PrefixIPNet(route)
nip := net.IP.Mask(net.Mask)
nstr := fmt.Sprintf("%v/%d", nip, route.Bits())
dst := localAddr4.Addr().String()
if route.Addr().Is6() {
dst = localAddr6.Addr().String()
}
routedel := []string{"route", "-q", "-n",
"del", "-" + inet(route), nstr,
"-iface", dst}
out, err := cmd(routedel...).CombinedOutput()
if err != nil {
r.logf("route del failed: %v: %v\n%s", routedel, err, out)
if errq == nil {
errq = err
}
}
}
}
for route := range newRoutes {
if _, exists := r.routes[route]; !exists {
net := netipx.PrefixIPNet(route)
nip := net.IP.Mask(net.Mask)
nstr := fmt.Sprintf("%v/%d", nip, route.Bits())
dst := localAddr4.Addr().String()
if route.Addr().Is6() {
dst = localAddr6.Addr().String()
}
routeadd := []string{"route", "-q", "-n",
"add", "-" + inet(route), nstr,
"-iface", dst}
out, err := cmd(routeadd...).CombinedOutput()
if err != nil {
r.logf("addr add failed: %v: %v\n%s", routeadd, err, out)
if errq == nil {
errq = err
}
}
}
}
r.local4 = localAddr4
r.local6 = localAddr6
r.routes = newRoutes
return errq
}
// UpdateMagicsockPort implements the Router interface. This implementation
// does nothing and returns nil because this router does not currently need
// to know what the magicsock UDP port is.
func (r *openbsdRouter) UpdateMagicsockPort(_ uint16, _ string) error {
return nil
}
func (r *openbsdRouter) Close() error {
cleanUp(r.logf, r.tunname)
return nil
}
func cleanUp(logf logger.Logf, interfaceName string) {
out, err := cmd("ifconfig", interfaceName, "down").CombinedOutput()
if err != nil {
logf("ifconfig down: %v\n%s", err, out)
}
}

View File

@@ -0,0 +1,211 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build darwin || freebsd
package router
import (
"fmt"
"log"
"net/netip"
"os/exec"
"runtime"
"github.com/tailscale/wireguard-go/tun"
"go4.org/netipx"
"tailscale.com/health"
"tailscale.com/net/netmon"
"tailscale.com/net/tsaddr"
"tailscale.com/types/logger"
"tailscale.com/version"
)
type userspaceBSDRouter struct {
logf logger.Logf
netMon *netmon.Monitor
health *health.Tracker
tunname string
local []netip.Prefix
routes map[netip.Prefix]bool
}
func newUserspaceBSDRouter(logf logger.Logf, tundev tun.Device, netMon *netmon.Monitor, health *health.Tracker) (Router, error) {
tunname, err := tundev.Name()
if err != nil {
return nil, err
}
return &userspaceBSDRouter{
logf: logf,
netMon: netMon,
health: health,
tunname: tunname,
}, nil
}
func (r *userspaceBSDRouter) addrsToRemove(newLocalAddrs []netip.Prefix) (remove []netip.Prefix) {
for _, cur := range r.local {
found := false
for _, v := range newLocalAddrs {
found = (v == cur)
if found {
break
}
}
if !found {
remove = append(remove, cur)
}
}
return
}
func (r *userspaceBSDRouter) addrsToAdd(newLocalAddrs []netip.Prefix) (add []netip.Prefix) {
for _, cur := range newLocalAddrs {
found := false
for _, v := range r.local {
found = (v == cur)
if found {
break
}
}
if !found {
add = append(add, cur)
}
}
return
}
func cmd(args ...string) *exec.Cmd {
if len(args) == 0 {
log.Fatalf("exec.Cmd(%#v) invalid; need argv[0]", args)
}
return exec.Command(args[0], args[1:]...)
}
func (r *userspaceBSDRouter) Up() error {
ifup := []string{"ifconfig", r.tunname, "up"}
if out, err := cmd(ifup...).CombinedOutput(); err != nil {
r.logf("running ifconfig failed: %v\n%s", err, out)
return err
}
return nil
}
func inet(p netip.Prefix) string {
if p.Addr().Is6() {
return "inet6"
}
return "inet"
}
func (r *userspaceBSDRouter) Set(cfg *Config) (reterr error) {
if cfg == nil {
cfg = &shutdownConfig
}
setErr := func(err error) {
if reterr == nil {
reterr = err
}
}
addrsToRemove := r.addrsToRemove(cfg.LocalAddrs)
// If we're removing all addresses, we need to remove and re-add all
// routes.
resetRoutes := len(r.local) > 0 && len(addrsToRemove) == len(r.local)
// Update the addresses.
for _, addr := range addrsToRemove {
arg := []string{"ifconfig", r.tunname, inet(addr), addr.String(), "-alias"}
out, err := cmd(arg...).CombinedOutput()
if err != nil {
r.logf("addr del failed: %v => %v\n%s", arg, err, out)
setErr(err)
}
}
for _, addr := range r.addrsToAdd(cfg.LocalAddrs) {
var arg []string
if runtime.GOOS == "freebsd" && addr.Addr().Is6() && addr.Bits() == 128 {
// FreeBSD rejects tun addresses of the form fc00::1/128 -> fc00::1,
// https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=218508
// Instead add our whole /48, which works because we use a /48 route.
// Full history: https://github.com/tailscale/tailscale/issues/1307
tmp := netip.PrefixFrom(addr.Addr(), 48)
arg = []string{"ifconfig", r.tunname, inet(tmp), tmp.String()}
} else {
arg = []string{"ifconfig", r.tunname, inet(addr), addr.String(), addr.Addr().String()}
}
out, err := cmd(arg...).CombinedOutput()
if err != nil {
r.logf("addr add failed: %v => %v\n%s", arg, err, out)
setErr(err)
}
}
newRoutes := make(map[netip.Prefix]bool)
for _, route := range cfg.Routes {
if runtime.GOOS != "darwin" && route == tsaddr.TailscaleULARange() {
// Because we added the interface address as a /48 above,
// the kernel already created the Tailscale ULA route
// implicitly. We mustn't try to add/delete it ourselves.
continue
}
newRoutes[route] = true
}
// Delete any preexisting routes.
for route := range r.routes {
if resetRoutes || !newRoutes[route] {
net := netipx.PrefixIPNet(route)
nip := net.IP.Mask(net.Mask)
nstr := fmt.Sprintf("%v/%d", nip, route.Bits())
del := "del"
if version.OS() == "macOS" {
del = "delete"
}
routedel := []string{"route", "-q", "-n",
del, "-" + inet(route), nstr,
"-iface", r.tunname}
out, err := cmd(routedel...).CombinedOutput()
if err != nil {
r.logf("route del failed: %v: %v\n%s", routedel, err, out)
setErr(err)
}
}
}
// Add the routes.
for route := range newRoutes {
if resetRoutes || !r.routes[route] {
net := netipx.PrefixIPNet(route)
nip := net.IP.Mask(net.Mask)
nstr := fmt.Sprintf("%v/%d", nip, route.Bits())
routeadd := []string{"route", "-q", "-n",
"add", "-" + inet(route), nstr,
"-iface", r.tunname}
out, err := cmd(routeadd...).CombinedOutput()
if err != nil {
r.logf("addr add failed: %v: %v\n%s", routeadd, err, out)
setErr(err)
}
}
}
// Store the interface and routes so we know what to change on an update.
if reterr == nil {
r.local = append([]netip.Prefix{}, cfg.LocalAddrs...)
}
r.routes = newRoutes
return reterr
}
// UpdateMagicsockPort implements the Router interface. This implementation
// does nothing and returns nil because this router does not currently need
// to know what the magicsock UDP port is.
func (r *userspaceBSDRouter) UpdateMagicsockPort(_ uint16, _ string) error {
return nil
}
func (r *userspaceBSDRouter) Close() error {
return nil
}

400
vendor/tailscale.com/wgengine/router/router_windows.go generated vendored Normal file
View File

@@ -0,0 +1,400 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package router
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"net/netip"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"sync"
"syscall"
"time"
"github.com/tailscale/wireguard-go/tun"
"golang.org/x/sys/windows"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"tailscale.com/health"
"tailscale.com/logtail/backoff"
"tailscale.com/net/dns"
"tailscale.com/net/netmon"
"tailscale.com/types/logger"
)
type winRouter struct {
logf func(fmt string, args ...any)
netMon *netmon.Monitor // may be nil
health *health.Tracker
nativeTun *tun.NativeTun
routeChangeCallback *winipcfg.RouteChangeCallback
firewall *firewallTweaker
}
func newUserspaceRouter(logf logger.Logf, tundev tun.Device, netMon *netmon.Monitor, health *health.Tracker) (Router, error) {
nativeTun := tundev.(*tun.NativeTun)
luid := winipcfg.LUID(nativeTun.LUID())
guid, err := luid.GUID()
if err != nil {
return nil, err
}
return &winRouter{
logf: logf,
netMon: netMon,
health: health,
nativeTun: nativeTun,
firewall: &firewallTweaker{
logf: logger.WithPrefix(logf, "firewall: "),
tunGUID: *guid,
},
}, nil
}
func (r *winRouter) Up() error {
r.firewall.clear()
var err error
t0 := time.Now()
r.routeChangeCallback, err = monitorDefaultRoutes(r.nativeTun)
d := time.Since(t0).Round(time.Millisecond)
if err != nil {
return fmt.Errorf("monitorDefaultRoutes, after %v: %v", d, err)
}
r.logf("monitorDefaultRoutes done after %v", d)
return nil
}
func (r *winRouter) Set(cfg *Config) error {
if cfg == nil {
cfg = &shutdownConfig
}
var localAddrs []string
for _, la := range cfg.LocalAddrs {
localAddrs = append(localAddrs, la.String())
}
r.firewall.set(localAddrs, cfg.Routes, cfg.LocalRoutes)
err := configureInterface(cfg, r.nativeTun, r.health)
if err != nil {
r.logf("ConfigureInterface: %v", err)
return err
}
// Flush DNS on router config change to clear cached DNS entries (solves #1430)
if err := dns.Flush(); err != nil {
r.logf("flushdns error: %v", err)
}
return nil
}
func hasDefaultRoute(routes []netip.Prefix) bool {
for _, route := range routes {
if route.Bits() == 0 {
return true
}
}
return false
}
// UpdateMagicsockPort implements the Router interface. This implementation
// does nothing and returns nil because this router does not currently need
// to know what the magicsock UDP port is.
func (r *winRouter) UpdateMagicsockPort(_ uint16, _ string) error {
return nil
}
func (r *winRouter) Close() error {
r.firewall.clear()
if r.routeChangeCallback != nil {
r.routeChangeCallback.Unregister()
}
return nil
}
func cleanUp(logf logger.Logf, interfaceName string) {
// Nothing to do here.
}
// firewallTweaker changes the Windows firewall. Normally this wouldn't be so complicated,
// but it can be REALLY SLOW to change the Windows firewall for reasons not understood.
// Like 4 minutes slow. But usually it's tens of milliseconds.
// See https://github.com/tailscale/tailscale/issues/785.
// So this tracks the desired state and runs the actual adjusting code asynchronously.
type firewallTweaker struct {
logf logger.Logf
tunGUID windows.GUID
mu sync.Mutex
didProcRule bool
running bool // doAsyncSet goroutine is running
known bool // firewall is in known state (in lastVal)
wantLocal []string // next value we want, or "" to delete the firewall rule
lastLocal []string // last set value, if known
localRoutes []netip.Prefix
lastLocalRoutes []netip.Prefix
wantKillswitch bool
lastKillswitch bool
// Only touched by doAsyncSet, so mu doesn't need to be held.
// fwProc is a subprocess that runs the wireguard-windows firewall
// killswitch code. It is only non-nil when the default route
// killswitch is active, and may go back and forth between nil and
// non-nil any number of times during the process's lifetime.
fwProc *exec.Cmd
// stop makes fwProc exit when closed.
fwProcWriter io.WriteCloser
fwProcEncoder *json.Encoder
// The path to the 'netsh.exe' binary, populated during the first call
// to runFirewall.
//
// not protected by mu; netshPath is only mutated inside netshPathOnce
netshPathOnce sync.Once
netshPath string
}
func (ft *firewallTweaker) clear() { ft.set(nil, nil, nil) }
// set takes CIDRs to allow, and the routes that point into the Tailscale tun interface.
// Empty slices remove firewall rules.
//
// set takes ownership of cidrs, but not routes.
func (ft *firewallTweaker) set(cidrs []string, routes, localRoutes []netip.Prefix) {
ft.mu.Lock()
defer ft.mu.Unlock()
if len(cidrs) == 0 {
ft.logf("marking for removal")
} else {
ft.logf("marking allowed %v", cidrs)
}
ft.wantLocal = cidrs
ft.localRoutes = localRoutes
ft.wantKillswitch = hasDefaultRoute(routes)
if ft.running {
// The doAsyncSet goroutine will check ft.wantLocal/wantKillswitch
// before returning.
return
}
ft.logf("starting netsh goroutine")
ft.running = true
go ft.doAsyncSet()
}
// getNetshPath returns the path that should be used to execute netsh.
//
// We've seen a report from a customer that we're triggering the "cannot run
// executable found relative to current directory" protection that was added to
// prevent running possibly attacker-controlled binaries. To mitigate this,
// first try looking up the path to netsh.exe in the System32 directory
// explicitly, and then fall back to the prior behaviour of passing "netsh" to
// os/exec.Command.
func (ft *firewallTweaker) getNetshPath() string {
ft.netshPathOnce.Do(func() {
// The default value is the old approach: just run "netsh" and
// let os/exec resolve that into a full path.
ft.netshPath = "netsh"
path, err := windows.KnownFolderPath(windows.FOLDERID_System, 0)
if err != nil {
ft.logf("getNetshPath: error getting FOLDERID_System: %v", err)
return
}
expath := filepath.Join(path, "netsh.exe")
if _, err := os.Stat(expath); err == nil {
ft.netshPath = expath
return
} else if !os.IsNotExist(err) {
ft.logf("getNetshPath: error checking for existence of %q: %v", expath, err)
}
// Keep default
})
return ft.netshPath
}
func (ft *firewallTweaker) runFirewall(args ...string) (time.Duration, error) {
t0 := time.Now()
args = append([]string{"advfirewall", "firewall"}, args...)
cmd := exec.Command(ft.getNetshPath(), args...)
cmd.SysProcAttr = &syscall.SysProcAttr{
CreationFlags: windows.DETACHED_PROCESS,
}
b, err := cmd.CombinedOutput()
if err != nil {
err = fmt.Errorf("%w: %v", err, string(b))
}
return time.Since(t0).Round(time.Millisecond), err
}
func (ft *firewallTweaker) doAsyncSet() {
bo := backoff.NewBackoff("win-firewall", ft.logf, time.Minute)
ctx := context.Background()
ft.mu.Lock()
for { // invariant: ft.mu must be locked when beginning this block
val := ft.wantLocal
if ft.known && slices.Equal(ft.lastLocal, val) && ft.wantKillswitch == ft.lastKillswitch && slices.Equal(ft.localRoutes, ft.lastLocalRoutes) {
ft.running = false
ft.logf("ending netsh goroutine")
ft.mu.Unlock()
return
}
wantKillswitch := ft.wantKillswitch
needClear := !ft.known || len(ft.lastLocal) > 0 || len(val) == 0
needProcRule := !ft.didProcRule
localRoutes := ft.localRoutes
ft.mu.Unlock()
err := ft.doSet(val, wantKillswitch, needClear, needProcRule, localRoutes)
if err != nil {
ft.logf("set failed: %v", err)
}
bo.BackOff(ctx, err)
ft.mu.Lock()
ft.lastLocal = val
ft.lastLocalRoutes = localRoutes
ft.lastKillswitch = wantKillswitch
ft.known = (err == nil)
}
}
// doSet creates and deletes firewall rules to make the system state
// match the values of local, killswitch, clear and procRule.
//
// local is the list of local Tailscale addresses (formatted as CIDR
// prefixes) to allow through the Windows firewall.
// killswitch, if true, enables the wireguard-windows based internet
// killswitch to prevent use of non-Tailscale default routes.
// clear, if true, removes all tailscale address firewall rules before
// adding local.
// procRule, if true, installs a firewall rule that permits the Tailscale
// process to dial out as it pleases.
//
// Must only be invoked from doAsyncSet.
func (ft *firewallTweaker) doSet(local []string, killswitch bool, clear bool, procRule bool, allowedRoutes []netip.Prefix) error {
if clear {
ft.logf("clearing Tailscale-In firewall rules...")
// We ignore the error here, because netsh returns an error for
// deleting something that doesn't match.
// TODO(bradfitz): care? That'd involve querying it before/after to see
// whether it was necessary/worked. But the output format is localized,
// so can't rely on parsing English. Maybe need to use OLE, not netsh.exe?
d, _ := ft.runFirewall("delete", "rule", "name=Tailscale-In", "dir=in")
ft.logf("cleared Tailscale-In firewall rules in %v", d)
}
if procRule {
ft.logf("deleting any prior Tailscale-Process rule...")
d, err := ft.runFirewall("delete", "rule", "name=Tailscale-Process", "dir=in") // best effort
if err == nil {
ft.logf("removed old Tailscale-Process rule in %v", d)
}
var exe string
exe, err = os.Executable()
if err != nil {
ft.logf("failed to find Executable for Tailscale-Process rule: %v", err)
} else {
ft.logf("adding Tailscale-Process rule to allow UDP for %q ...", exe)
d, err = ft.runFirewall("add", "rule", "name=Tailscale-Process",
"dir=in",
"action=allow",
"edge=yes",
"program="+exe,
"protocol=udp",
"profile=any",
"enable=yes",
)
if err != nil {
ft.logf("error adding Tailscale-Process rule: %v", err)
} else {
ft.mu.Lock()
ft.didProcRule = true
ft.mu.Unlock()
ft.logf("added Tailscale-Process rule in %v", d)
}
}
}
for _, cidr := range local {
ft.logf("adding Tailscale-In rule to allow %v ...", cidr)
var d time.Duration
d, err := ft.runFirewall("add", "rule", "name=Tailscale-In", "dir=in", "action=allow", "localip="+cidr, "profile=private,domain", "enable=yes")
if err != nil {
ft.logf("error adding Tailscale-In rule to allow %v: %v", cidr, err)
return err
}
ft.logf("added Tailscale-In rule to allow %v in %v", cidr, d)
}
if !killswitch {
if ft.fwProc != nil {
ft.fwProcWriter.Close()
ft.fwProcWriter = nil
ft.fwProc.Wait()
ft.fwProc = nil
ft.fwProcEncoder = nil
}
return nil
}
if ft.fwProc == nil {
exe, err := os.Executable()
if err != nil {
return err
}
proc := exec.Command(exe, "/firewall", ft.tunGUID.String())
proc.SysProcAttr = &syscall.SysProcAttr{
CreationFlags: windows.DETACHED_PROCESS,
}
in, err := proc.StdinPipe()
if err != nil {
return err
}
out, err := proc.StdoutPipe()
if err != nil {
in.Close()
return err
}
go func(out io.ReadCloser) {
b := bufio.NewReaderSize(out, 1<<10)
for {
line, err := b.ReadString('\n')
if err != nil {
return
}
line = strings.TrimSpace(line)
if line != "" {
ft.logf("fw-child: %s", line)
}
}
}(out)
proc.Stderr = proc.Stdout
if err := proc.Start(); err != nil {
return err
}
ft.fwProcWriter = in
ft.fwProc = proc
ft.fwProcEncoder = json.NewEncoder(in)
}
// Note(maisem): when local lan access toggled, we need to inform the
// firewall to let the local routes through. The set of routes is passed
// in via stdin encoded in json.
return ft.fwProcEncoder.Encode(allowedRoutes)
}

120
vendor/tailscale.com/wgengine/router/runner.go generated vendored Normal file
View File

@@ -0,0 +1,120 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux
package router
import (
"errors"
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"syscall"
"golang.org/x/sys/unix"
)
// commandRunner abstracts helpers to run OS commands. It exists
// purely to swap out osCommandRunner (below) with a fake runner in
// tests.
type commandRunner interface {
run(...string) error
output(...string) ([]byte, error)
}
type osCommandRunner struct {
// ambientCapNetAdmin determines whether commands are executed with
// CAP_NET_ADMIN.
// CAP_NET_ADMIN is required when running as non-root and executing cmds
// like `ip rule`. Even if our process has the capability, we need to
// explicitly grant it to the new process.
// We specifically need this for Synology DSM7 where tailscaled no longer
// runs as root.
ambientCapNetAdmin bool
}
// errCode extracts and returns the process exit code from err, or
// zero if err is nil.
func errCode(err error) int {
if err == nil {
return 0
}
var e *exec.ExitError
if ok := errors.As(err, &e); ok {
return e.ExitCode()
}
s := err.Error()
if strings.HasPrefix(s, "exitcode:") {
code, err := strconv.Atoi(s[9:])
if err == nil {
return code
}
}
return -42
}
func (o osCommandRunner) run(args ...string) error {
_, err := o.output(args...)
return err
}
func (o osCommandRunner) output(args ...string) ([]byte, error) {
if len(args) == 0 {
return nil, errors.New("cmd: no argv[0]")
}
cmd := exec.Command(args[0], args[1:]...)
cmd.Env = append(os.Environ(), "LC_ALL=C")
if o.ambientCapNetAdmin {
cmd.SysProcAttr = &syscall.SysProcAttr{
AmbientCaps: []uintptr{unix.CAP_NET_ADMIN},
}
}
out, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("running %q failed: %w\n%s", strings.Join(args, " "), err, out)
}
return out, nil
}
type runGroup struct {
OkCode []int // error codes that are acceptable, other than 0, if any
Runner commandRunner // the runner that actually runs our commands
ErrAcc error // first error encountered, if any
}
func newRunGroup(okCode []int, runner commandRunner) *runGroup {
return &runGroup{
OkCode: okCode,
Runner: runner,
}
}
func (rg *runGroup) okCode(err error) bool {
got := errCode(err)
for _, want := range rg.OkCode {
if got == want {
return true
}
}
return false
}
func (rg *runGroup) Output(args ...string) []byte {
b, err := rg.Runner.output(args...)
if rg.ErrAcc == nil && err != nil && !rg.okCode(err) {
rg.ErrAcc = err
}
return b
}
func (rg *runGroup) Run(args ...string) {
err := rg.Runner.run(args...)
if rg.ErrAcc == nil && err != nil && !rg.okCode(err) {
rg.ErrAcc = err
}
}

1608
vendor/tailscale.com/wgengine/userspace.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

171
vendor/tailscale.com/wgengine/watchdog.go generated vendored Normal file
View File

@@ -0,0 +1,171 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !js
package wgengine
import (
"fmt"
"log"
"net/netip"
"runtime/pprof"
"strings"
"sync"
"time"
"tailscale.com/envknob"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/dns"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/netmap"
"tailscale.com/wgengine/capture"
"tailscale.com/wgengine/filter"
"tailscale.com/wgengine/router"
"tailscale.com/wgengine/wgcfg"
"tailscale.com/wgengine/wgint"
)
// NewWatchdog wraps an Engine and makes sure that all methods complete
// within a reasonable amount of time.
//
// If they do not, the watchdog crashes the process.
func NewWatchdog(e Engine) Engine {
if envknob.Bool("TS_DEBUG_DISABLE_WATCHDOG") {
return e
}
return &watchdogEngine{
wrap: e,
logf: log.Printf,
fatalf: log.Fatalf,
maxWait: 45 * time.Second,
inFlight: make(map[inFlightKey]time.Time),
}
}
type inFlightKey struct {
op string
ctr uint64
}
type watchdogEngine struct {
wrap Engine
logf func(format string, args ...any)
fatalf func(format string, args ...any)
maxWait time.Duration
// Track the start time(s) of in-flight operations
inFlightMu sync.Mutex
inFlight map[inFlightKey]time.Time
inFlightCtr uint64
}
func (e *watchdogEngine) watchdogErr(name string, fn func() error) error {
// Track all in-flight operations so we can print more useful error
// messages on watchdog failure
e.inFlightMu.Lock()
key := inFlightKey{
op: name,
ctr: e.inFlightCtr,
}
e.inFlightCtr++
e.inFlight[key] = time.Now()
e.inFlightMu.Unlock()
defer func() {
e.inFlightMu.Lock()
defer e.inFlightMu.Unlock()
delete(e.inFlight, key)
}()
errCh := make(chan error)
go func() {
errCh <- fn()
}()
t := time.NewTimer(e.maxWait)
select {
case err := <-errCh:
t.Stop()
return err
case <-t.C:
buf := new(strings.Builder)
pprof.Lookup("goroutine").WriteTo(buf, 1)
e.logf("wgengine watchdog stacks:\n%s", buf.String())
// Collect the list of in-flight operations for debugging.
var (
b []byte
now = time.Now()
)
e.inFlightMu.Lock()
for k, t := range e.inFlight {
dur := now.Sub(t).Round(time.Millisecond)
b = fmt.Appendf(b, "in-flight[%d]: name=%s duration=%v start=%s\n", k.ctr, k.op, dur, t.Format(time.RFC3339Nano))
}
e.inFlightMu.Unlock()
// Print everything as a single string to avoid log
// rate limits.
e.logf("wgengine watchdog in-flight:\n%s", b)
e.fatalf("wgengine: watchdog timeout on %s", name)
return nil
}
}
func (e *watchdogEngine) watchdog(name string, fn func()) {
e.watchdogErr(name, func() error {
fn()
return nil
})
}
func (e *watchdogEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config, dnsCfg *dns.Config) error {
return e.watchdogErr("Reconfig", func() error { return e.wrap.Reconfig(cfg, routerCfg, dnsCfg) })
}
func (e *watchdogEngine) GetFilter() *filter.Filter {
return e.wrap.GetFilter()
}
func (e *watchdogEngine) SetFilter(filt *filter.Filter) {
e.watchdog("SetFilter", func() { e.wrap.SetFilter(filt) })
}
func (e *watchdogEngine) GetJailedFilter() *filter.Filter {
return e.wrap.GetJailedFilter()
}
func (e *watchdogEngine) SetJailedFilter(filt *filter.Filter) {
e.watchdog("SetJailedFilter", func() { e.wrap.SetJailedFilter(filt) })
}
func (e *watchdogEngine) SetStatusCallback(cb StatusCallback) {
e.watchdog("SetStatusCallback", func() { e.wrap.SetStatusCallback(cb) })
}
func (e *watchdogEngine) UpdateStatus(sb *ipnstate.StatusBuilder) {
e.watchdog("UpdateStatus", func() { e.wrap.UpdateStatus(sb) })
}
func (e *watchdogEngine) RequestStatus() {
e.watchdog("RequestStatus", func() { e.wrap.RequestStatus() })
}
func (e *watchdogEngine) SetNetworkMap(nm *netmap.NetworkMap) {
e.watchdog("SetNetworkMap", func() { e.wrap.SetNetworkMap(nm) })
}
func (e *watchdogEngine) Ping(ip netip.Addr, pingType tailcfg.PingType, size int, cb func(*ipnstate.PingResult)) {
e.watchdog("Ping", func() { e.wrap.Ping(ip, pingType, size, cb) })
}
func (e *watchdogEngine) Close() {
e.watchdog("Close", e.wrap.Close)
}
func (e *watchdogEngine) PeerForIP(ip netip.Addr) (ret PeerForIP, ok bool) {
e.watchdog("PeerForIP", func() { ret, ok = e.wrap.PeerForIP(ip) })
return ret, ok
}
func (e *watchdogEngine) Done() <-chan struct{} {
return e.wrap.Done()
}
func (e *watchdogEngine) InstallCaptureHook(cb capture.Callback) {
e.wrap.InstallCaptureHook(cb)
}
func (e *watchdogEngine) PeerByKey(pubKey key.NodePublic) (_ wgint.Peer, ok bool) {
return e.wrap.PeerByKey(pubKey)
}

17
vendor/tailscale.com/wgengine/watchdog_js.go generated vendored Normal file
View File

@@ -0,0 +1,17 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build js
package wgengine
import "tailscale.com/net/dns/resolver"
type watchdogEngine struct {
Engine
wrap Engine
}
func (e *watchdogEngine) GetResolver() (r *resolver.Resolver, ok bool) {
return nil, false
}

61
vendor/tailscale.com/wgengine/wgcfg/config.go generated vendored Normal file
View File

@@ -0,0 +1,61 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package wgcfg has types and a parser for representing WireGuard config.
package wgcfg
import (
"net/netip"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/logid"
)
//go:generate go run tailscale.com/cmd/cloner -type=Config,Peer
// Config is a WireGuard configuration.
// It only supports the set of things Tailscale uses.
type Config struct {
Name string
NodeID tailcfg.StableNodeID
PrivateKey key.NodePrivate
Addresses []netip.Prefix
MTU uint16
DNS []netip.Addr
Peers []Peer
// NetworkLogging enables network logging.
// It is disabled if either ID is the zero value.
// LogExitFlowEnabled indicates whether or not exit flows should be logged.
NetworkLogging struct {
NodeID logid.PrivateID
DomainID logid.PrivateID
LogExitFlowEnabled bool
}
}
type Peer struct {
PublicKey key.NodePublic
DiscoKey key.DiscoPublic // present only so we can handle restarts within wgengine, not passed to WireGuard
AllowedIPs []netip.Prefix
V4MasqAddr *netip.Addr // if non-nil, masquerade IPv4 traffic to this peer using this address
V6MasqAddr *netip.Addr // if non-nil, masquerade IPv6 traffic to this peer using this address
IsJailed bool // if true, this peer is jailed and cannot initiate connections
PersistentKeepalive uint16 // in seconds between keep-alives; 0 to disable
// wireguard-go's endpoint for this peer. It should always equal Peer.PublicKey.
// We represent it explicitly so that we can detect if they diverge and recover.
// There is no need to set WGEndpoint explicitly when constructing a Peer by hand.
// It is only populated when reading Peers from wireguard-go.
WGEndpoint key.NodePublic
}
// PeerWithKey returns the Peer with key k and reports whether it was found.
func (config Config) PeerWithKey(k key.NodePublic) (Peer, bool) {
for _, p := range config.Peers {
if p.PublicKey == k {
return p, true
}
}
return Peer{}, false
}

68
vendor/tailscale.com/wgengine/wgcfg/device.go generated vendored Normal file
View File

@@ -0,0 +1,68 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package wgcfg
import (
"io"
"sort"
"github.com/tailscale/wireguard-go/conn"
"github.com/tailscale/wireguard-go/device"
"github.com/tailscale/wireguard-go/tun"
"tailscale.com/types/logger"
"tailscale.com/util/multierr"
)
// NewDevice returns a wireguard-go Device configured for Tailscale use.
func NewDevice(tunDev tun.Device, bind conn.Bind, logger *device.Logger) *device.Device {
ret := device.NewDevice(tunDev, bind, logger)
ret.DisableSomeRoamingForBrokenMobileSemantics()
return ret
}
func DeviceConfig(d *device.Device) (*Config, error) {
r, w := io.Pipe()
errc := make(chan error, 1)
go func() {
errc <- d.IpcGetOperation(w)
w.Close()
}()
cfg, fromErr := FromUAPI(r)
r.Close()
getErr := <-errc
err := multierr.New(getErr, fromErr)
if err != nil {
return nil, err
}
sort.Slice(cfg.Peers, func(i, j int) bool {
return cfg.Peers[i].PublicKey.Less(cfg.Peers[j].PublicKey)
})
return cfg, nil
}
// ReconfigDevice replaces the existing device configuration with cfg.
func ReconfigDevice(d *device.Device, cfg *Config, logf logger.Logf) (err error) {
defer func() {
if err != nil {
logf("wgcfg.Reconfig failed: %v", err)
}
}()
prev, err := DeviceConfig(d)
if err != nil {
return err
}
r, w := io.Pipe()
errc := make(chan error, 1)
go func() {
errc <- d.IpcSetOperation(r)
r.Close()
}()
toErr := cfg.ToUAPI(logf, w, prev)
w.Close()
setErr := <-errc
return multierr.New(setErr, toErr)
}

150
vendor/tailscale.com/wgengine/wgcfg/nmcfg/nmcfg.go generated vendored Normal file
View File

@@ -0,0 +1,150 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package nmcfg converts a controlclient.NetMap into a wgcfg config.
package nmcfg
import (
"bytes"
"fmt"
"net/netip"
"strings"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/types/logid"
"tailscale.com/types/netmap"
"tailscale.com/wgengine/wgcfg"
)
func nodeDebugName(n tailcfg.NodeView) string {
name := n.Name()
if name == "" {
name = n.Hostinfo().Hostname()
}
if i := strings.Index(name, "."); i != -1 {
name = name[:i]
}
if name == "" && n.Addresses().Len() != 0 {
return n.Addresses().At(0).String()
}
return name
}
// cidrIsSubnet reports whether cidr is a non-default-route subnet
// exported by node that is not one of its own self addresses.
func cidrIsSubnet(node tailcfg.NodeView, cidr netip.Prefix) bool {
if cidr.Bits() == 0 {
return false
}
if !cidr.IsSingleIP() {
return true
}
for i := range node.Addresses().Len() {
selfCIDR := node.Addresses().At(i)
if cidr == selfCIDR {
return false
}
}
return true
}
// WGCfg returns the NetworkMaps's WireGuard configuration.
func WGCfg(nm *netmap.NetworkMap, logf logger.Logf, flags netmap.WGConfigFlags, exitNode tailcfg.StableNodeID) (*wgcfg.Config, error) {
cfg := &wgcfg.Config{
Name: "tailscale",
PrivateKey: nm.PrivateKey,
Addresses: nm.GetAddresses().AsSlice(),
Peers: make([]wgcfg.Peer, 0, len(nm.Peers)),
}
// Setup log IDs for data plane audit logging.
if nm.SelfNode.Valid() {
cfg.NodeID = nm.SelfNode.StableID()
canNetworkLog := nm.SelfNode.HasCap(tailcfg.CapabilityDataPlaneAuditLogs)
logExitFlowEnabled := nm.SelfNode.HasCap(tailcfg.NodeAttrLogExitFlows)
if canNetworkLog && nm.SelfNode.DataPlaneAuditLogID() != "" && nm.DomainAuditLogID != "" {
nodeID, errNode := logid.ParsePrivateID(nm.SelfNode.DataPlaneAuditLogID())
if errNode != nil {
logf("[v1] wgcfg: unable to parse node audit log ID: %v", errNode)
}
domainID, errDomain := logid.ParsePrivateID(nm.DomainAuditLogID)
if errDomain != nil {
logf("[v1] wgcfg: unable to parse domain audit log ID: %v", errDomain)
}
if errNode == nil && errDomain == nil {
cfg.NetworkLogging.NodeID = nodeID
cfg.NetworkLogging.DomainID = domainID
cfg.NetworkLogging.LogExitFlowEnabled = logExitFlowEnabled
}
}
}
// Logging buffers
skippedUnselected := new(bytes.Buffer)
skippedIPs := new(bytes.Buffer)
skippedSubnets := new(bytes.Buffer)
for _, peer := range nm.Peers {
if peer.DiscoKey().IsZero() && peer.DERP() == "" && !peer.IsWireGuardOnly() {
// Peer predates both DERP and active discovery, we cannot
// communicate with it.
logf("[v1] wgcfg: skipped peer %s, doesn't offer DERP or disco", peer.Key().ShortString())
continue
}
// Skip expired peers; we'll end up failing to connect to them
// anyway, since control intentionally breaks node keys for
// expired peers so that we can't discover endpoints via DERP.
if peer.Expired() {
logf("[v1] wgcfg: skipped expired peer %s", peer.Key().ShortString())
continue
}
cfg.Peers = append(cfg.Peers, wgcfg.Peer{
PublicKey: peer.Key(),
DiscoKey: peer.DiscoKey(),
})
cpeer := &cfg.Peers[len(cfg.Peers)-1]
didExitNodeWarn := false
cpeer.V4MasqAddr = peer.SelfNodeV4MasqAddrForThisPeer()
cpeer.V6MasqAddr = peer.SelfNodeV6MasqAddrForThisPeer()
cpeer.IsJailed = peer.IsJailed()
for i := range peer.AllowedIPs().Len() {
allowedIP := peer.AllowedIPs().At(i)
if allowedIP.Bits() == 0 && peer.StableID() != exitNode {
if didExitNodeWarn {
// Don't log about both the IPv4 /0 and IPv6 /0.
continue
}
didExitNodeWarn = true
if skippedUnselected.Len() > 0 {
skippedUnselected.WriteString(", ")
}
fmt.Fprintf(skippedUnselected, "%q (%v)", nodeDebugName(peer), peer.Key().ShortString())
continue
} else if cidrIsSubnet(peer, allowedIP) {
if (flags & netmap.AllowSubnetRoutes) == 0 {
if skippedSubnets.Len() > 0 {
skippedSubnets.WriteString(", ")
}
fmt.Fprintf(skippedSubnets, "%v from %q (%v)", allowedIP, nodeDebugName(peer), peer.Key().ShortString())
continue
}
}
cpeer.AllowedIPs = append(cpeer.AllowedIPs, allowedIP)
}
}
if skippedUnselected.Len() > 0 {
logf("[v1] wgcfg: skipped unselected default routes from: %s", skippedUnselected.Bytes())
}
if skippedIPs.Len() > 0 {
logf("[v1] wgcfg: skipped node IPs: %s", skippedIPs)
}
if skippedSubnets.Len() > 0 {
logf("[v1] wgcfg: did not accept subnet routes: %s", skippedSubnets)
}
return cfg, nil
}

186
vendor/tailscale.com/wgengine/wgcfg/parser.go generated vendored Normal file
View File

@@ -0,0 +1,186 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package wgcfg
import (
"bufio"
"fmt"
"io"
"net"
"net/netip"
"strconv"
"strings"
"go4.org/mem"
"tailscale.com/types/key"
)
type ParseError struct {
why string
offender string
}
func (e *ParseError) Error() string {
return fmt.Sprintf("%s: %q", e.why, e.offender)
}
func parseEndpoint(s string) (host string, port uint16, err error) {
i := strings.LastIndexByte(s, ':')
if i < 0 {
return "", 0, &ParseError{"Missing port from endpoint", s}
}
host, portStr := s[:i], s[i+1:]
if len(host) < 1 {
return "", 0, &ParseError{"Invalid endpoint host", host}
}
uport, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
return "", 0, err
}
hostColon := strings.IndexByte(host, ':')
if host[0] == '[' || host[len(host)-1] == ']' || hostColon > 0 {
err := &ParseError{"Brackets must contain an IPv6 address", host}
if len(host) > 3 && host[0] == '[' && host[len(host)-1] == ']' && hostColon > 0 {
maybeV6 := net.ParseIP(host[1 : len(host)-1])
if maybeV6 == nil || len(maybeV6) != net.IPv6len {
return "", 0, err
}
} else {
return "", 0, err
}
host = host[1 : len(host)-1]
}
return host, uint16(uport), nil
}
// memROCut separates a mem.RO at the separator if it exists, otherwise
// it returns two empty ROs and reports that it was not found.
func memROCut(s mem.RO, sep byte) (before, after mem.RO, found bool) {
if i := mem.IndexByte(s, sep); i >= 0 {
return s.SliceTo(i), s.SliceFrom(i + 1), true
}
found = false
return
}
// FromUAPI generates a Config from r.
// r should be generated by calling device.IpcGetOperation;
// it is not compatible with other uapi streams.
func FromUAPI(r io.Reader) (*Config, error) {
cfg := new(Config)
var peer *Peer // current peer being operated on
deviceConfig := true
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := mem.B(scanner.Bytes())
if line.Len() == 0 {
continue
}
key, value, ok := memROCut(line, '=')
if !ok {
return nil, fmt.Errorf("failed to cut line %q on =", line.StringCopy())
}
valueBytes := scanner.Bytes()[key.Len()+1:]
if key.EqualString("public_key") {
if deviceConfig {
deviceConfig = false
}
// Load/create the peer we are now configuring.
var err error
peer, err = cfg.handlePublicKeyLine(valueBytes)
if err != nil {
return nil, err
}
continue
}
var err error
if deviceConfig {
err = cfg.handleDeviceLine(key, value, valueBytes)
} else {
err = cfg.handlePeerLine(peer, key, value, valueBytes)
}
if err != nil {
return nil, err
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return cfg, nil
}
func (cfg *Config) handleDeviceLine(k, value mem.RO, valueBytes []byte) error {
switch {
case k.EqualString("private_key"):
// wireguard-go guarantees not to send zero value; private keys are already clamped.
var err error
cfg.PrivateKey, err = key.ParseNodePrivateUntyped(value)
if err != nil {
return err
}
case k.EqualString("listen_port") || k.EqualString("fwmark"):
// ignore
default:
return fmt.Errorf("unexpected IpcGetOperation key: %q", k.StringCopy())
}
return nil
}
func (cfg *Config) handlePublicKeyLine(valueBytes []byte) (*Peer, error) {
p := Peer{}
var err error
p.PublicKey, err = key.ParseNodePublicUntyped(mem.B(valueBytes))
if err != nil {
return nil, err
}
cfg.Peers = append(cfg.Peers, p)
return &cfg.Peers[len(cfg.Peers)-1], nil
}
func (cfg *Config) handlePeerLine(peer *Peer, k, value mem.RO, valueBytes []byte) error {
switch {
case k.EqualString("endpoint"):
nk, err := key.ParseNodePublicUntyped(value)
if err != nil {
return fmt.Errorf("invalid endpoint %q for peer %q, expected a hex public key", value.StringCopy(), peer.PublicKey.ShortString())
}
// nk ought to equal peer.PublicKey.
// Under some rare circumstances, it might not. See corp issue #3016.
// Even if that happens, don't stop early, so that we can recover from it.
// Instead, note the value of nk so we can fix as needed.
peer.WGEndpoint = nk
case k.EqualString("persistent_keepalive_interval"):
n, err := mem.ParseUint(value, 10, 16)
if err != nil {
return err
}
peer.PersistentKeepalive = uint16(n)
case k.EqualString("allowed_ip"):
ipp := netip.Prefix{}
err := ipp.UnmarshalText(valueBytes)
if err != nil {
return err
}
peer.AllowedIPs = append(peer.AllowedIPs, ipp)
case k.EqualString("protocol_version"):
if !value.EqualString("1") {
return fmt.Errorf("invalid protocol version: %q", value.StringCopy())
}
case k.EqualString("replace_allowed_ips") ||
k.EqualString("preshared_key") ||
k.EqualString("last_handshake_time_sec") ||
k.EqualString("last_handshake_time_nsec") ||
k.EqualString("tx_bytes") ||
k.EqualString("rx_bytes"):
// ignore
default:
return fmt.Errorf("unexpected IpcGetOperation key: %q", k.StringCopy())
}
return nil
}

80
vendor/tailscale.com/wgengine/wgcfg/wgcfg_clone.go generated vendored Normal file
View File

@@ -0,0 +1,80 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
package wgcfg
import (
"net/netip"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/logid"
"tailscale.com/types/ptr"
)
// Clone makes a deep copy of Config.
// The result aliases no memory with the original.
func (src *Config) Clone() *Config {
if src == nil {
return nil
}
dst := new(Config)
*dst = *src
dst.Addresses = append(src.Addresses[:0:0], src.Addresses...)
dst.DNS = append(src.DNS[:0:0], src.DNS...)
if src.Peers != nil {
dst.Peers = make([]Peer, len(src.Peers))
for i := range dst.Peers {
dst.Peers[i] = *src.Peers[i].Clone()
}
}
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _ConfigCloneNeedsRegeneration = Config(struct {
Name string
NodeID tailcfg.StableNodeID
PrivateKey key.NodePrivate
Addresses []netip.Prefix
MTU uint16
DNS []netip.Addr
Peers []Peer
NetworkLogging struct {
NodeID logid.PrivateID
DomainID logid.PrivateID
LogExitFlowEnabled bool
}
}{})
// Clone makes a deep copy of Peer.
// The result aliases no memory with the original.
func (src *Peer) Clone() *Peer {
if src == nil {
return nil
}
dst := new(Peer)
*dst = *src
dst.AllowedIPs = append(src.AllowedIPs[:0:0], src.AllowedIPs...)
if dst.V4MasqAddr != nil {
dst.V4MasqAddr = ptr.To(*src.V4MasqAddr)
}
if dst.V6MasqAddr != nil {
dst.V6MasqAddr = ptr.To(*src.V6MasqAddr)
}
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _PeerCloneNeedsRegeneration = Peer(struct {
PublicKey key.NodePublic
DiscoKey key.DiscoPublic
AllowedIPs []netip.Prefix
V4MasqAddr *netip.Addr
V6MasqAddr *netip.Addr
IsJailed bool
PersistentKeepalive uint16
WGEndpoint key.NodePublic
}{})

154
vendor/tailscale.com/wgengine/wgcfg/writer.go generated vendored Normal file
View File

@@ -0,0 +1,154 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package wgcfg
import (
"fmt"
"io"
"net/netip"
"strconv"
"tailscale.com/types/key"
"tailscale.com/types/logger"
)
// ToUAPI writes cfg in UAPI format to w.
// Prev is the previous device Config.
//
// Prev is required so that we can remove now-defunct peers without having to
// remove and re-add all peers, and so that we can avoid writing information
// about peers that have not changed since the previous time we wrote our
// Config.
func (cfg *Config) ToUAPI(logf logger.Logf, w io.Writer, prev *Config) error {
var stickyErr error
set := func(key, value string) {
if stickyErr != nil {
return
}
_, err := fmt.Fprintf(w, "%s=%s\n", key, value)
if err != nil {
stickyErr = err
}
}
setUint16 := func(key string, value uint16) {
set(key, strconv.FormatUint(uint64(value), 10))
}
setPeer := func(peer Peer) {
set("public_key", peer.PublicKey.UntypedHexString())
}
// Device config.
if !prev.PrivateKey.Equal(cfg.PrivateKey) {
set("private_key", cfg.PrivateKey.UntypedHexString())
}
old := make(map[key.NodePublic]Peer)
for _, p := range prev.Peers {
old[p.PublicKey] = p
}
// Add/configure all new peers.
for _, p := range cfg.Peers {
oldPeer, wasPresent := old[p.PublicKey]
// We only want to write the peer header/version if we're about
// to change something about that peer, or if it's a new peer.
// Figure out up-front whether we'll need to do anything for
// this peer, and skip doing anything if not.
//
// If the peer was not present in the previous config, this
// implies that this is a new peer; set all of these to 'true'
// to ensure that we're writing the full peer configuration.
willSetEndpoint := oldPeer.WGEndpoint != p.PublicKey || !wasPresent
willChangeIPs := !cidrsEqual(oldPeer.AllowedIPs, p.AllowedIPs) || !wasPresent
willChangeKeepalive := oldPeer.PersistentKeepalive != p.PersistentKeepalive // if not wasPresent, no need to redundantly set zero (default)
if !willSetEndpoint && !willChangeIPs && !willChangeKeepalive {
// It's safe to skip doing anything here; wireguard-go
// will not remove a peer if it's unspecified unless we
// tell it to (which we do below if necessary).
continue
}
setPeer(p)
set("protocol_version", "1")
// Avoid setting endpoints if the correct one is already known
// to WireGuard, because doing so generates a bit more work in
// calling magicsock's ParseEndpoint for effectively a no-op.
if willSetEndpoint {
if wasPresent {
// We had an endpoint, and it was wrong.
// By construction, this should not happen.
// If it does, keep going so that we can recover from it,
// but log so that we know about it,
// because it is an indicator of other failed invariants.
// See corp issue 3016.
logf("[unexpected] endpoint changed from %s to %s", oldPeer.WGEndpoint, p.PublicKey)
}
set("endpoint", p.PublicKey.UntypedHexString())
}
// TODO: replace_allowed_ips is expensive.
// If p.AllowedIPs is a strict superset of oldPeer.AllowedIPs,
// then skip replace_allowed_ips and instead add only
// the new ipps with allowed_ip.
if willChangeIPs {
set("replace_allowed_ips", "true")
for _, ipp := range p.AllowedIPs {
set("allowed_ip", ipp.String())
}
}
// Set PersistentKeepalive after the peer is otherwise configured,
// because it can trigger handshake packets.
if willChangeKeepalive {
setUint16("persistent_keepalive_interval", p.PersistentKeepalive)
}
}
// Remove peers that were present but should no longer be.
for _, p := range cfg.Peers {
delete(old, p.PublicKey)
}
for _, p := range old {
setPeer(p)
set("remove", "true")
}
if stickyErr != nil {
stickyErr = fmt.Errorf("ToUAPI: %w", stickyErr)
}
return stickyErr
}
func cidrsEqual(x, y []netip.Prefix) bool {
// TODO: re-implement using netaddr.IPSet.Equal.
if len(x) != len(y) {
return false
}
// First see if they're equal in order, without allocating.
exact := true
for i := range x {
if x[i] != y[i] {
exact = false
break
}
}
if exact {
return true
}
// Otherwise, see if they're the same, but out of order.
m := make(map[netip.Prefix]bool)
for _, v := range x {
m[v] = true
}
for _, v := range y {
if !m[v] {
return false
}
}
return true
}

133
vendor/tailscale.com/wgengine/wgengine.go generated vendored Normal file
View File

@@ -0,0 +1,133 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package wgengine provides the Tailscale WireGuard engine interface.
package wgengine
import (
"errors"
"net/netip"
"time"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/dns"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/netmap"
"tailscale.com/wgengine/capture"
"tailscale.com/wgengine/filter"
"tailscale.com/wgengine/router"
"tailscale.com/wgengine/wgcfg"
"tailscale.com/wgengine/wgint"
)
// Status is the Engine status.
//
// TODO(bradfitz): remove this, subset of ipnstate? Need to migrate users.
type Status struct {
AsOf time.Time // the time at which the status was calculated
Peers []ipnstate.PeerStatusLite
LocalAddrs []tailcfg.Endpoint // the set of possible endpoints for the magic conn
DERPs int // number of active DERP connections
}
// StatusCallback is the type of status callbacks used by
// Engine.SetStatusCallback.
//
// Exactly one of Status or error is non-nil.
type StatusCallback func(*Status, error)
// NetworkMapCallback is the type used by callbacks that hook
// into network map updates.
type NetworkMapCallback func(*netmap.NetworkMap)
// ErrNoChanges is returned by Engine.Reconfig if no changes were made.
var ErrNoChanges = errors.New("no changes made to Engine config")
// PeerForIP is the type returned by Engine.PeerForIP.
type PeerForIP struct {
// Node is the matched node. It's always a valid value when
// Engine.PeerForIP returns ok==true.
Node tailcfg.NodeView
// IsSelf is whether the Node is the local process.
IsSelf bool
// Route is the route that matched the IP provided
// to Engine.PeerForIP.
Route netip.Prefix
}
// Engine is the Tailscale WireGuard engine interface.
type Engine interface {
// Reconfig reconfigures WireGuard and makes sure it's running.
// This also handles setting up any kernel routes.
//
// This is called whenever tailcontrol (the control plane)
// sends an updated network map.
//
// The returned error is ErrNoChanges if no changes were made.
Reconfig(*wgcfg.Config, *router.Config, *dns.Config) error
// PeerForIP returns the node to which the provided IP routes,
// if any. If none is found, (nil, false) is returned.
PeerForIP(netip.Addr) (_ PeerForIP, ok bool)
// GetFilter returns the current packet filter, if any.
GetFilter() *filter.Filter
// SetFilter updates the packet filter.
SetFilter(*filter.Filter)
// GetJailedFilter returns the current packet filter for jailed nodes,
// if any.
GetJailedFilter() *filter.Filter
// SetJailedFilter updates the packet filter for jailed nodes.
SetJailedFilter(*filter.Filter)
// SetStatusCallback sets the function to call when the
// WireGuard status changes.
SetStatusCallback(StatusCallback)
// RequestStatus requests a WireGuard status update right
// away, sent to the callback registered via SetStatusCallback.
RequestStatus()
// PeerByKey returns the WireGuard status of the provided peer.
// If the peer is not found, ok is false.
PeerByKey(key.NodePublic) (_ wgint.Peer, ok bool)
// Close shuts down this wireguard instance, remove any routes
// it added, etc. To bring it up again later, you'll need a
// new Engine.
Close()
// Done returns a channel that is closed when the Engine's
// Close method is called, the engine aborts with an error,
// or it shuts down due to the closure of the underlying device.
// You don't have to call this.
Done() <-chan struct{}
// SetNetworkMap informs the engine of the latest network map
// from the server. The network map's DERPMap field should be
// ignored as as it might be disabled; get it from SetDERPMap
// instead.
// The network map should only be read from.
SetNetworkMap(*netmap.NetworkMap)
// UpdateStatus populates the network state using the provided
// status builder.
UpdateStatus(*ipnstate.StatusBuilder)
// Ping is a request to start a ping of the given message size to the peer
// handling the given IP, then call cb with its ping latency & method.
//
// If size is zero too small, it is ignored. See tailscale.PingOpts for details.
Ping(ip netip.Addr, pingType tailcfg.PingType, size int, cb func(*ipnstate.PingResult))
// InstallCaptureHook registers a function to be called to capture
// packets traversing the data path. The hook can be uninstalled by
// calling this function with a nil value.
InstallCaptureHook(capture.Callback)
}

108
vendor/tailscale.com/wgengine/wgint/wgint.go generated vendored Normal file
View File

@@ -0,0 +1,108 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package wgint provides somewhat shady access to wireguard-go
// internals that don't (yet) have public APIs.
package wgint
import (
"reflect"
"sync/atomic"
"time"
"unsafe"
"github.com/tailscale/wireguard-go/device"
)
var (
offHandshake = getPeerStatsOffset("lastHandshakeNano")
offRxBytes = getPeerStatsOffset("rxBytes")
offTxBytes = getPeerStatsOffset("txBytes")
offHandshakeAttempts = getPeerHandshakeAttemptsOffset()
)
func getPeerStatsOffset(name string) uintptr {
peerType := reflect.TypeFor[device.Peer]()
field, ok := peerType.FieldByName(name)
if !ok {
panic("no " + name + " field in device.Peer")
}
if s := field.Type.String(); s != "atomic.Int64" && s != "atomic.Uint64" {
panic("unexpected type " + s + " of field " + name + " in device.Peer")
}
return field.Offset
}
func getPeerHandshakeAttemptsOffset() uintptr {
peerType := reflect.TypeFor[device.Peer]()
field, ok := peerType.FieldByName("timers")
if !ok {
panic("no timers field in device.Peer")
}
field2, ok := field.Type.FieldByName("handshakeAttempts")
if !ok {
panic("no handshakeAttempts field in device.Peer.timers")
}
if g, w := field2.Type.String(), "atomic.Uint32"; g != w {
panic("unexpected type " + g + " of field handshakeAttempts in device.Peer.timers; want " + w)
}
return field.Offset + field2.Offset
}
// peerLastHandshakeNano returns the last handshake time in nanoseconds since the
// unix epoch.
func peerLastHandshakeNano(peer *device.Peer) int64 {
return (*atomic.Int64)(unsafe.Add(unsafe.Pointer(peer), offHandshake)).Load()
}
// peerRxBytes returns the number of bytes received from this peer.
func peerRxBytes(peer *device.Peer) uint64 {
return (*atomic.Uint64)(unsafe.Add(unsafe.Pointer(peer), offRxBytes)).Load()
}
// peerTxBytes returns the number of bytes sent to this peer.
func peerTxBytes(peer *device.Peer) uint64 {
return (*atomic.Uint64)(unsafe.Add(unsafe.Pointer(peer), offTxBytes)).Load()
}
// peerHandshakeAttempts returns the number of WireGuard handshake attempts
// made for the current handshake. It resets to zero before every new handshake.
func peerHandshakeAttempts(peer *device.Peer) uint32 {
return (*atomic.Uint32)(unsafe.Add(unsafe.Pointer(peer), offHandshakeAttempts)).Load()
}
// Peer is a wrapper around a wireguard-go device.Peer pointer.
type Peer struct {
p *device.Peer
}
// PeerOf returns a Peer wrapper around a wireguard-go device.Peer.
func PeerOf(p *device.Peer) Peer {
return Peer{p}
}
// LastHandshake returns the last handshake time.
//
// If the handshake has never happened, it returns the zero value.
func (p Peer) LastHandshake() time.Time {
if n := peerLastHandshakeNano(p.p); n != 0 {
return time.Unix(0, n)
}
return time.Time{}
}
func (p Peer) IsValid() bool { return p.p != nil }
// TxBytes returns the number of bytes sent to this peer.
func (p Peer) TxBytes() uint64 { return peerTxBytes(p.p) }
// RxBytes returns the number of bytes received from this peer.
func (p Peer) RxBytes() uint64 { return peerRxBytes(p.p) }
// HandshakeAttempts returns the number of failed WireGuard handshake attempts
// made for the current handshake. It resets to zero before every new handshake
// and after a successful handshake.
func (p Peer) HandshakeAttempts() uint32 {
return peerHandshakeAttempts(p.p)
}

130
vendor/tailscale.com/wgengine/wglog/wglog.go generated vendored Normal file
View File

@@ -0,0 +1,130 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package wglog contains logging helpers for wireguard-go.
package wglog
import (
"fmt"
"strings"
"sync"
"github.com/tailscale/wireguard-go/device"
"tailscale.com/envknob"
"tailscale.com/syncs"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/wgengine/wgcfg"
)
// A Logger is a wireguard-go log wrapper that cleans up and rewrites log lines.
// It can be modified at run time to adjust to new wireguard-go configurations.
type Logger struct {
DeviceLogger *device.Logger
replace syncs.AtomicValue[map[string]string]
mu sync.Mutex // protects strs
strs map[key.NodePublic]*strCache // cached strs used to populate replace
}
// strCache holds a wireguard-go and a Tailscale style peer string.
type strCache struct {
wg, ts string
used bool // track whether this strCache was used in a particular round
}
// NewLogger creates a new logger for use with wireguard-go.
// This logger silences repetitive/unhelpful noisy log lines
// and rewrites peer keys from wireguard-go into Tailscale format.
func NewLogger(logf logger.Logf) *Logger {
const prefix = "wg: "
ret := new(Logger)
wrapper := func(format string, args ...any) {
if strings.Contains(format, "Routine:") && !strings.Contains(format, "receive incoming") {
// wireguard-go logs as it starts and stops routines.
// Drop those; there are a lot of them, and they're just noise.
return
}
if strings.Contains(format, "Failed to send data packet") {
// Drop. See https://github.com/tailscale/tailscale/issues/1239.
return
}
if strings.Contains(format, "Interface up requested") || strings.Contains(format, "Interface down requested") {
// Drop. Logs 1/s constantly while the tun device is open.
// See https://github.com/tailscale/tailscale/issues/1388.
return
}
if strings.Contains(format, "Adding allowedip") {
// Drop. See https://github.com/tailscale/corp/issues/17532.
// AppConnectors (as one example) may have many subnet routes, and
// the messaging related to these is not specific enough to be
// useful.
return
}
replace := ret.replace.Load()
if replace == nil {
// No replacements specified; log as originally planned.
logf(format, args...)
return
}
// Duplicate the args slice so that we can modify it.
// This is not always required, but the code required to avoid it is not worth the complexity.
newargs := make([]any, len(args))
copy(newargs, args)
for i, arg := range newargs {
// We want to replace *device.Peer args with the Tailscale-formatted version of themselves.
// Using *device.Peer directly makes this hard to test, so we string any fmt.Stringers,
// and if the string ends up looking exactly like a known Peer, we replace it.
// This is slightly imprecise, in that we don't check the formatting verb. Oh well.
s, ok := arg.(fmt.Stringer)
if !ok {
continue
}
wgStr := s.String()
tsStr, ok := replace[wgStr]
if !ok {
continue
}
newargs[i] = tsStr
}
logf(format, newargs...)
}
if envknob.Bool("TS_DEBUG_RAW_WGLOG") {
wrapper = logf
}
ret.DeviceLogger = &device.Logger{
Verbosef: logger.WithPrefix(wrapper, prefix+"[v2] "),
Errorf: logger.WithPrefix(wrapper, prefix),
}
ret.strs = make(map[key.NodePublic]*strCache)
return ret
}
// SetPeers adjusts x to rewrite the peer public keys found in peers.
// SetPeers is safe for concurrent use.
func (x *Logger) SetPeers(peers []wgcfg.Peer) {
x.mu.Lock()
defer x.mu.Unlock()
// Construct a new peer public key log rewriter.
replace := make(map[string]string)
for _, peer := range peers {
c, ok := x.strs[peer.PublicKey] // look up cached strs
if !ok {
wg := peer.PublicKey.WireGuardGoString()
ts := peer.PublicKey.ShortString()
c = &strCache{wg: wg, ts: ts}
x.strs[peer.PublicKey] = c
}
c.used = true
replace[c.wg] = c.ts
}
// Remove any unused cached strs.
for k, c := range x.strs {
if !c.used {
delete(x.strs, k)
continue
}
// Mark c as unused for next round.
c.used = false
}
x.replace.Store(replace)
}

192
vendor/tailscale.com/wgengine/winnet/winnet.go generated vendored Normal file
View File

@@ -0,0 +1,192 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build windows
// Package winnet contains Windows-specific networking code.
package winnet
import (
"fmt"
"syscall"
"unsafe"
"github.com/go-ole/go-ole"
"github.com/go-ole/go-ole/oleutil"
)
const CLSID_NetworkListManager = "{DCB00C01-570F-4A9B-8D69-199FDBA5723B}"
var IID_INetwork = ole.NewGUID("{8A40A45D-055C-4B62-ABD7-6D613E2CEAEC}")
var IID_INetworkConnection = ole.NewGUID("{DCB00005-570F-4A9B-8D69-199FDBA5723B}")
type NetworkListManager struct {
d *ole.Dispatch
}
type INetworkConnection struct {
ole.IDispatch
}
type ConnectionList []*INetworkConnection
type INetworkConnectionVtbl struct {
ole.IDispatchVtbl
GetNetwork uintptr
Get_IsConnectedToInternet uintptr
Get_IsConnected uintptr
GetConnectivity uintptr
GetConnectionId uintptr
GetAdapterId uintptr
GetDomainType uintptr
}
type INetwork struct {
ole.IDispatch
}
type INetworkVtbl struct {
ole.IDispatchVtbl
GetName uintptr
SetName uintptr
GetDescription uintptr
SetDescription uintptr
GetNetworkId uintptr
GetDomainType uintptr
GetNetworkConnections uintptr
GetTimeCreatedAndConnected uintptr
Get_IsConnectedToInternet uintptr
Get_IsConnected uintptr
GetConnectivity uintptr
GetCategory uintptr
SetCategory uintptr
}
func NewNetworkListManager(c *ole.Connection) (*NetworkListManager, error) {
err := c.Create(CLSID_NetworkListManager)
if err != nil {
return nil, err
}
defer c.Release()
d, err := c.Dispatch()
if err != nil {
return nil, err
}
return &NetworkListManager{
d: d,
}, nil
}
func (m *NetworkListManager) Release() {
m.d.Release()
}
func (cl ConnectionList) Release() {
for _, v := range cl {
v.Release()
}
}
func asIID(u ole.UnknownLike, iid *ole.GUID) (*ole.IDispatch, error) {
if u == nil {
return nil, fmt.Errorf("asIID: nil UnknownLike")
}
d, err := u.QueryInterface(iid)
u.Release()
if err != nil {
return nil, err
}
return d, nil
}
func (m *NetworkListManager) GetNetworkConnections() (ConnectionList, error) {
ncraw, err := m.d.Call("GetNetworkConnections")
if err != nil {
return nil, err
}
nli := ncraw.ToIDispatch()
if nli == nil {
return nil, fmt.Errorf("GetNetworkConnections: not IDispatch")
}
cl := ConnectionList{}
err = oleutil.ForEach(nli, func(v *ole.VARIANT) error {
nc, err := asIID(v.ToIUnknown(), IID_INetworkConnection)
if err != nil {
return err
}
nco := (*INetworkConnection)(unsafe.Pointer(nc))
cl = append(cl, nco)
return nil
})
if err != nil {
cl.Release()
return nil, err
}
return cl, nil
}
func (n *INetwork) GetName() (string, error) {
v, err := n.CallMethod("GetName")
if err != nil {
return "", err
}
return v.ToString(), err
}
func (n *INetwork) GetCategory() (int32, error) {
var result int32
r, _, _ := syscall.SyscallN(
n.VTable().GetCategory,
uintptr(unsafe.Pointer(n)),
uintptr(unsafe.Pointer(&result)),
)
if int32(r) < 0 {
return 0, ole.NewError(r)
}
return result, nil
}
func (n *INetwork) SetCategory(v int32) error {
r, _, _ := syscall.SyscallN(
n.VTable().SetCategory,
uintptr(unsafe.Pointer(n)),
uintptr(v),
)
if int32(r) < 0 {
return ole.NewError(r)
}
return nil
}
func (n *INetwork) VTable() *INetworkVtbl {
return (*INetworkVtbl)(unsafe.Pointer(n.RawVTable))
}
func (v *INetworkConnection) VTable() *INetworkConnectionVtbl {
return (*INetworkConnectionVtbl)(unsafe.Pointer(v.RawVTable))
}
func (v *INetworkConnection) GetNetwork() (*INetwork, error) {
var result *INetwork
r, _, _ := syscall.SyscallN(
v.VTable().GetNetwork,
uintptr(unsafe.Pointer(v)),
uintptr(unsafe.Pointer(&result)),
)
if int32(r) < 0 {
return nil, ole.NewError(r)
}
return result, nil
}

26
vendor/tailscale.com/wgengine/winnet/winnet_windows.go generated vendored Normal file
View File

@@ -0,0 +1,26 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package winnet
import (
"fmt"
"syscall"
"unsafe"
"github.com/go-ole/go-ole"
)
func (v *INetworkConnection) GetAdapterId() (string, error) {
buf := ole.GUID{}
hr, _, _ := syscall.Syscall(
v.VTable().GetAdapterId,
2,
uintptr(unsafe.Pointer(v)),
uintptr(unsafe.Pointer(&buf)),
0)
if hr != 0 {
return "", fmt.Errorf("GetAdapterId failed: %08x", hr)
}
return buf.String(), nil
}