Update dependencies
This commit is contained in:
238
vendor/tailscale.com/wgengine/capture/capture.go
generated
vendored
Normal file
238
vendor/tailscale.com/wgengine/capture/capture.go
generated
vendored
Normal 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
169
vendor/tailscale.com/wgengine/capture/ts-dissector.lua
generated
vendored
Normal 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
662
vendor/tailscale.com/wgengine/filter/filter.go
generated
vendored
Normal 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
|
||||
}
|
||||
116
vendor/tailscale.com/wgengine/filter/filtertype/filtertype.go
generated
vendored
Normal file
116
vendor/tailscale.com/wgengine/filter/filtertype/filtertype.go
generated
vendored
Normal 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)
|
||||
}
|
||||
64
vendor/tailscale.com/wgengine/filter/filtertype/filtertype_clone.go
generated
vendored
Normal file
64
vendor/tailscale.com/wgengine/filter/filtertype/filtertype_clone.go
generated
vendored
Normal 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
107
vendor/tailscale.com/wgengine/filter/match.go
generated
vendored
Normal 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
178
vendor/tailscale.com/wgengine/filter/tailcfg.go
generated
vendored
Normal 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
|
||||
}
|
||||
25
vendor/tailscale.com/wgengine/magicsock/batching_conn.go
generated
vendored
Normal file
25
vendor/tailscale.com/wgengine/magicsock/batching_conn.go
generated
vendored
Normal 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
|
||||
}
|
||||
14
vendor/tailscale.com/wgengine/magicsock/batching_conn_default.go
generated
vendored
Normal file
14
vendor/tailscale.com/wgengine/magicsock/batching_conn_default.go
generated
vendored
Normal 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
|
||||
}
|
||||
424
vendor/tailscale.com/wgengine/magicsock/batching_conn_linux.go
generated
vendored
Normal file
424
vendor/tailscale.com/wgengine/magicsock/batching_conn_linux.go
generated
vendored
Normal 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
|
||||
}
|
||||
55
vendor/tailscale.com/wgengine/magicsock/blockforever_conn.go
generated
vendored
Normal file
55
vendor/tailscale.com/wgengine/magicsock/blockforever_conn.go
generated
vendored
Normal 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
182
vendor/tailscale.com/wgengine/magicsock/cloudinfo.go
generated
vendored
Normal 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
|
||||
}
|
||||
23
vendor/tailscale.com/wgengine/magicsock/cloudinfo_nocloud.go
generated
vendored
Normal file
23
vendor/tailscale.com/wgengine/magicsock/cloudinfo_nocloud.go
generated
vendored
Normal 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
205
vendor/tailscale.com/wgengine/magicsock/debughttp.go
generated
vendored
Normal 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
97
vendor/tailscale.com/wgengine/magicsock/debugknobs.go
generated
vendored
Normal 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
|
||||
})
|
||||
33
vendor/tailscale.com/wgengine/magicsock/debugknobs_stubs.go
generated
vendored
Normal file
33
vendor/tailscale.com/wgengine/magicsock/debugknobs_stubs.go
generated
vendored
Normal 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
1004
vendor/tailscale.com/wgengine/magicsock/derp.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
29
vendor/tailscale.com/wgengine/magicsock/discopingpurpose_string.go
generated
vendored
Normal file
29
vendor/tailscale.com/wgengine/magicsock/discopingpurpose_string.go
generated
vendored
Normal 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
1855
vendor/tailscale.com/wgengine/magicsock/endpoint.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
22
vendor/tailscale.com/wgengine/magicsock/endpoint_default.go
generated
vendored
Normal file
22
vendor/tailscale.com/wgengine/magicsock/endpoint_default.go
generated
vendored
Normal 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)
|
||||
}
|
||||
13
vendor/tailscale.com/wgengine/magicsock/endpoint_stub.go
generated
vendored
Normal file
13
vendor/tailscale.com/wgengine/magicsock/endpoint_stub.go
generated
vendored
Normal 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
|
||||
}
|
||||
248
vendor/tailscale.com/wgengine/magicsock/endpoint_tracker.go
generated
vendored
Normal file
248
vendor/tailscale.com/wgengine/magicsock/endpoint_tracker.go
generated
vendored
Normal 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
3071
vendor/tailscale.com/wgengine/magicsock/magicsock.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
27
vendor/tailscale.com/wgengine/magicsock/magicsock_default.go
generated
vendored
Normal file
27
vendor/tailscale.com/wgengine/magicsock/magicsock_default.go
generated
vendored
Normal 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
|
||||
)
|
||||
520
vendor/tailscale.com/wgengine/magicsock/magicsock_linux.go
generated
vendored
Normal file
520
vendor/tailscale.com/wgengine/magicsock/magicsock_linux.go
generated
vendored
Normal 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)
|
||||
}
|
||||
13
vendor/tailscale.com/wgengine/magicsock/magicsock_notwindows.go
generated
vendored
Normal file
13
vendor/tailscale.com/wgengine/magicsock/magicsock_notwindows.go
generated
vendored
Normal 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) {}
|
||||
58
vendor/tailscale.com/wgengine/magicsock/magicsock_windows.go
generated
vendored
Normal file
58
vendor/tailscale.com/wgengine/magicsock/magicsock_windows.go
generated
vendored
Normal 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
209
vendor/tailscale.com/wgengine/magicsock/peermap.go
generated
vendored
Normal 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
130
vendor/tailscale.com/wgengine/magicsock/peermtu.go
generated
vendored
Normal 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
|
||||
}
|
||||
51
vendor/tailscale.com/wgengine/magicsock/peermtu_darwin.go
generated
vendored
Normal file
51
vendor/tailscale.com/wgengine/magicsock/peermtu_darwin.go
generated
vendored
Normal 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
|
||||
}
|
||||
49
vendor/tailscale.com/wgengine/magicsock/peermtu_linux.go
generated
vendored
Normal file
49
vendor/tailscale.com/wgengine/magicsock/peermtu_linux.go
generated
vendored
Normal 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
|
||||
}
|
||||
27
vendor/tailscale.com/wgengine/magicsock/peermtu_stubs.go
generated
vendored
Normal file
27
vendor/tailscale.com/wgengine/magicsock/peermtu_stubs.go
generated
vendored
Normal 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
|
||||
}
|
||||
42
vendor/tailscale.com/wgengine/magicsock/peermtu_unix.go
generated
vendored
Normal file
42
vendor/tailscale.com/wgengine/magicsock/peermtu_unix.go
generated
vendored
Normal 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)
|
||||
}
|
||||
179
vendor/tailscale.com/wgengine/magicsock/rebinding_conn.go
generated
vendored
Normal file
179
vendor/tailscale.com/wgengine/magicsock/rebinding_conn.go
generated
vendored
Normal 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
20
vendor/tailscale.com/wgengine/mem_ios.go
generated
vendored
Normal 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
274
vendor/tailscale.com/wgengine/netlog/logger.go
generated
vendored
Normal 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
104
vendor/tailscale.com/wgengine/netstack/gro/gro.go
generated
vendored
Normal 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
|
||||
}
|
||||
76
vendor/tailscale.com/wgengine/netstack/gro/gro_default.go
generated
vendored
Normal file
76
vendor/tailscale.com/wgengine/netstack/gro/gro_default.go
generated
vendored
Normal 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
23
vendor/tailscale.com/wgengine/netstack/gro/gro_ios.go
generated
vendored
Normal 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
302
vendor/tailscale.com/wgengine/netstack/link_endpoint.go
generated
vendored
Normal 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
1943
vendor/tailscale.com/wgengine/netstack/netstack.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
19
vendor/tailscale.com/wgengine/netstack/netstack_linux.go
generated
vendored
Normal file
19
vendor/tailscale.com/wgengine/netstack/netstack_linux.go
generated
vendored
Normal 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},
|
||||
}
|
||||
}
|
||||
}
|
||||
20
vendor/tailscale.com/wgengine/netstack/netstack_tcpbuf_default.go
generated
vendored
Normal file
20
vendor/tailscale.com/wgengine/netstack/netstack_tcpbuf_default.go
generated
vendored
Normal 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
|
||||
)
|
||||
24
vendor/tailscale.com/wgengine/netstack/netstack_tcpbuf_ios.go
generated
vendored
Normal file
24
vendor/tailscale.com/wgengine/netstack/netstack_tcpbuf_ios.go
generated
vendored
Normal 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
|
||||
)
|
||||
72
vendor/tailscale.com/wgengine/netstack/netstack_userping.go
generated
vendored
Normal file
72
vendor/tailscale.com/wgengine/netstack/netstack_userping.go
generated
vendored
Normal 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
|
||||
}
|
||||
38
vendor/tailscale.com/wgengine/netstack/netstack_userping_apple.go
generated
vendored
Normal file
38
vendor/tailscale.com/wgengine/netstack/netstack_userping_apple.go
generated
vendored
Normal 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
273
vendor/tailscale.com/wgengine/pendopen.go
generated
vendored
Normal 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
91
vendor/tailscale.com/wgengine/router/callback.go
generated
vendored
Normal 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
|
||||
}
|
||||
59
vendor/tailscale.com/wgengine/router/consolidating_router.go
generated
vendored
Normal file
59
vendor/tailscale.com/wgengine/router/consolidating_router.go
generated
vendored
Normal 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
|
||||
}
|
||||
834
vendor/tailscale.com/wgengine/router/ifconfig_windows.go
generated
vendored
Normal file
834
vendor/tailscale.com/wgengine/router/ifconfig_windows.go
generated
vendored
Normal 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
110
vendor/tailscale.com/wgengine/router/router.go
generated
vendored
Normal 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
19
vendor/tailscale.com/wgengine/router/router_darwin.go
generated
vendored
Normal 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
24
vendor/tailscale.com/wgengine/router/router_default.go
generated
vendored
Normal 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
38
vendor/tailscale.com/wgengine/router/router_fake.go
generated
vendored
Normal 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
31
vendor/tailscale.com/wgengine/router/router_freebsd.go
generated
vendored
Normal 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
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
249
vendor/tailscale.com/wgengine/router/router_openbsd.go
generated
vendored
Normal 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)
|
||||
}
|
||||
}
|
||||
211
vendor/tailscale.com/wgengine/router/router_userspace_bsd.go
generated
vendored
Normal file
211
vendor/tailscale.com/wgengine/router/router_userspace_bsd.go
generated
vendored
Normal 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
400
vendor/tailscale.com/wgengine/router/router_windows.go
generated
vendored
Normal 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
120
vendor/tailscale.com/wgengine/router/runner.go
generated
vendored
Normal 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
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
171
vendor/tailscale.com/wgengine/watchdog.go
generated
vendored
Normal 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
17
vendor/tailscale.com/wgengine/watchdog_js.go
generated
vendored
Normal 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
61
vendor/tailscale.com/wgengine/wgcfg/config.go
generated
vendored
Normal 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
68
vendor/tailscale.com/wgengine/wgcfg/device.go
generated
vendored
Normal 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
150
vendor/tailscale.com/wgengine/wgcfg/nmcfg/nmcfg.go
generated
vendored
Normal 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
186
vendor/tailscale.com/wgengine/wgcfg/parser.go
generated
vendored
Normal 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
80
vendor/tailscale.com/wgengine/wgcfg/wgcfg_clone.go
generated
vendored
Normal 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
154
vendor/tailscale.com/wgengine/wgcfg/writer.go
generated
vendored
Normal 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
133
vendor/tailscale.com/wgengine/wgengine.go
generated
vendored
Normal 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
108
vendor/tailscale.com/wgengine/wgint/wgint.go
generated
vendored
Normal 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
130
vendor/tailscale.com/wgengine/wglog/wglog.go
generated
vendored
Normal 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
192
vendor/tailscale.com/wgengine/winnet/winnet.go
generated
vendored
Normal 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
26
vendor/tailscale.com/wgengine/winnet/winnet_windows.go
generated
vendored
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user