// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package atomicfile contains code related to writing to filesystems
// atomically.
//
// This package should be considered internal; its API is not stable.
package atomicfile // import "tailscale.com/atomicfile"
import (
"fmt"
"os"
"path/filepath"
"runtime"
)
// WriteFile writes data to filename+some suffix, then renames it into filename.
// The perm argument is ignored on Windows, but if the target filename already
// exists then the target file's attributes and ACLs are preserved. If the target
// filename already exists but is not a regular file, WriteFile returns an error.
func WriteFile(filename string, data []byte, perm os.FileMode) (err error) {
fi, err := os.Stat(filename)
if err == nil && !fi.Mode().IsRegular() {
return fmt.Errorf("%s already exists and is not a regular file", filename)
}
f, err := os.CreateTemp(filepath.Dir(filename), filepath.Base(filename)+".tmp")
if err != nil {
return err
}
tmpName := f.Name()
defer func() {
if err != nil {
f.Close()
os.Remove(tmpName)
}
}()
if _, err := f.Write(data); err != nil {
return err
}
if runtime.GOOS != "windows" {
if err := f.Chmod(perm); err != nil {
return err
}
}
if err := f.Sync(); err != nil {
return err
}
if err := f.Close(); err != nil {
return err
}
return Rename(tmpName, filename)
}
// Rename srcFile to dstFile, similar to [os.Rename] but preserving file
// attributes and ACLs on Windows.
func Rename(srcFile, dstFile string) error { return rename(srcFile, dstFile) }
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !windows
package atomicfile
import (
"os"
)
func rename(srcFile, destFile string) error {
return os.Rename(srcFile, destFile)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package controlknobs contains client options configurable from control which can be turned on
// or off. The ability to turn options on and off is for incrementally adding features in.
package controlknobs
import (
"fmt"
"reflect"
"sync/atomic"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
"tailscale.com/tailcfg/nodecap"
"tailscale.com/types/opt"
)
// Knobs is the set of knobs that the control plane's coordination server can
// adjust at runtime.
type Knobs struct {
// DisableUPnP indicates whether to attempt UPnP mapping.
DisableUPnP atomic.Bool
// RandomizeClientPort is whether control says we should randomize
// the client port.
RandomizeClientPort atomic.Bool
// OneCGNAT is whether the node should make one big CGNAT route
// in the OS rather than one /32 per peer.
OneCGNAT syncs.AtomicValue[opt.Bool]
// ForceBackgroundSTUN forces netcheck STUN queries to keep
// running in magicsock, even when idle.
ForceBackgroundSTUN atomic.Bool
// DisableDeltaUpdates is whether the node should not process
// incremental (delta) netmap updates and should treat all netmap
// changes as "full" ones as tailscaled did in 1.48.x and earlier.
DisableDeltaUpdates atomic.Bool
// PeerMTUEnable is whether the node should do peer path MTU discovery.
PeerMTUEnable atomic.Bool
// DisableDNSForwarderTCPRetries is whether the DNS forwarder should
// skip retrying truncated queries over TCP.
DisableDNSForwarderTCPRetries atomic.Bool
// SilentDisco is whether the node should suppress disco heartbeats to its
// peers.
SilentDisco atomic.Bool
// LinuxForceIPTables is whether the node should use iptables for Linux
// netfiltering, unless overridden by the user.
LinuxForceIPTables atomic.Bool
// LinuxForceNfTables is whether the node should use nftables for Linux
// netfiltering, unless overridden by the user.
LinuxForceNfTables atomic.Bool
// ProbeUDPLifetime is whether the node should probe UDP path lifetime on
// the tail end of an active direct connection in magicsock.
ProbeUDPLifetime atomic.Bool
// AppCStoreRoutes is whether the node should store RouteInfo to StateStore
// if it's an app connector.
AppCStoreRoutes atomic.Bool
// UserDialUseRoutes is whether tsdial.Dialer.UserDial should use routes to determine
// how to dial the destination address. When true, it also makes the DNS forwarder
// use UserDial instead of SystemDial when dialing resolvers.
UserDialUseRoutes atomic.Bool
// DisableSplitDNSWhenNoCustomResolvers indicates that the node's DNS manager
// should not adopt a split DNS configuration even though the Config of the
// resolver only contains routes that do not specify custom resolver(s), hence
// all DNS queries can be safely sent to the upstream DNS resolver and the
// node's DNS forwarder doesn't need to handle all DNS traffic.
// This is for now (2024-06-06) an iOS-specific battery life optimization,
// and this knob allows us to disable the optimization remotely if needed.
DisableSplitDNSWhenNoCustomResolvers atomic.Bool
// DisableLocalDNSOverrideViaNRPT indicates that the node's DNS manager should not
// create a default (catch-all) Windows NRPT rule when "Override local DNS" is enabled.
// Without this rule, Windows 8.1 and newer devices issue parallel DNS requests to DNS servers
// associated with all network adapters, even when "Override local DNS" is enabled and/or
// a Mullvad exit node is being used, resulting in DNS leaks.
// We began creating this rule on 2024-06-14, and this knob
// allows us to disable the new behavior remotely if needed.
DisableLocalDNSOverrideViaNRPT atomic.Bool
// DisableCaptivePortalDetection is whether the node should not perform captive portal detection
// automatically when the network state changes.
DisableCaptivePortalDetection atomic.Bool
// DisableSkipStatusQueue is whether the node should disable skipping
// of queued netmap.NetworkMap between the controlclient and LocalBackend.
// See tailscale/tailscale#14768.
DisableSkipStatusQueue atomic.Bool
// DisableHostsFileUpdates indicates that the node's DNS manager should not create
// hosts file entries when it normally would, such as when we're not the primary
// resolver on Windows or when the host is domain-joined and its primary domain
// takes precedence over MagicDNS. As of 2026-02-13, it is only used on Windows.
DisableHostsFileUpdates atomic.Bool
// ForceRegisterMagicDNSIPv4Only is whether the node should only register
// its IPv4 MagicDNS service IP and not its IPv6 one. The IPv6 one,
// tsaddr.TailscaleServiceIPv6String, still works in either case. This knob
// controls only whether we tell systemd/etc about the IPv6 one.
// See https://github.com/tailscale/tailscale/issues/15404.
// TODO(bradfitz): remove this a few releases after 2026-02-16.
ForceRegisterMagicDNSIPv4Only atomic.Bool
// EmitRuntimeMetrics is whether the node should poll and emit [runtime/metrics]
// as [tailscale.com/util/clientmetric]'s.
EmitRuntimeMetrics atomic.Bool
// DisableUDPGRO disables UDP GRO on the magicsock UDP socket. See
// [tailcfg.NodeAttrDisableUDPGRO].
DisableUDPGRO atomic.Bool
// DisableUDPGSO disables UDP GSO on the magicsock UDP socket. See
// [tailcfg.NodeAttrDisableUDPGSO].
DisableUDPGSO atomic.Bool
// DisableTUNUDPGRO disables UDP GRO on the Tailscale TUN device. See
// [tailcfg.NodeAttrDisableTUNUDPGRO].
DisableTUNUDPGRO atomic.Bool
// DisableTUNTCPGRO disables TCP GRO on the Tailscale TUN device. See
// [tailcfg.NodeAttrDisableTUNTCPGRO].
DisableTUNTCPGRO atomic.Bool
// NeverGSOEqualTail enables a UDP GSO sentinel-tail workaround in the
// underlay UDP packet TX path on Linux. Applies to magicsock and peer relay
// UDP sockets. See [tailcfg.NodeAttrNeverGSOEqualTail].
NeverGSOEqualTail atomic.Bool
// CacheNetworkMaps is whether the node should persistently cache network
// maps and use them to establish peer connectivity on start, if doing so
// is supported by the client and storage is available.
CacheNetworkMaps atomic.Bool
// ScopeQuad100OnMacOS is whether sandboxed macOS should scope quad-100 to
// its match domains rather than installing it as the OS's primary resolver,
// so a user's DoH system profile isn't shadowed. It has no effect on other
// platforms. Off by default; when off, sandboxed macOS keeps the older
// behavior of making quad-100 the default resolver, as iOS still does.
// See tailscale/corp#45534.
ScopeQuad100OnMacOS atomic.Bool
}
// UpdateFromNodeAttributes updates k (if non-nil) based on the provided self
// node attributes (Node.Capabilities).
func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) {
if k == nil {
return
}
has := capMap.Contains
var (
disableUPnP = has(nodecap.DisableUPnP)
randomizeClientPort = has(nodecap.RandomizeClientPort)
disableDeltaUpdates = has(nodecap.DisableDeltaUpdates)
oneCGNAT opt.Bool
forceBackgroundSTUN = has(nodecap.DebugForceBackgroundSTUN)
peerMTUEnable = has(nodecap.PeerMTUEnable)
dnsForwarderDisableTCPRetries = has(nodecap.DNSForwarderDisableTCPRetries)
silentDisco = has(nodecap.SilentDisco)
forceIPTables = has(nodecap.LinuxMustUseIPTables)
forceNfTables = has(nodecap.LinuxMustUseNfTables)
probeUDPLifetime = has(nodecap.ProbeUDPLifetime)
appCStoreRoutes = has(nodecap.StoreAppCRoutes)
userDialUseRoutes = has(nodecap.UserDialUseRoutes)
disableSplitDNSWhenNoCustomResolvers = has(nodecap.DisableSplitDNSWhenNoCustomResolvers)
disableLocalDNSOverrideViaNRPT = has(nodecap.DisableLocalDNSOverrideViaNRPT)
disableCaptivePortalDetection = has(nodecap.DisableCaptivePortalDetection)
disableSkipStatusQueue = has(nodecap.DisableSkipStatusQueue)
disableHostsFileUpdates = has(nodecap.DisableHostsFileUpdates)
forceRegisterMagicDNSIPv4Only = has(nodecap.ForceRegisterMagicDNSIPv4Only)
emitRuntimeMetrics = has(nodecap.EmitRuntimeMetrics)
disableUDPGRO = has(nodecap.DisableUDPGRO)
disableUDPGSO = has(nodecap.DisableUDPGSO)
disableTUNUDPGRO = has(nodecap.DisableTUNUDPGRO)
disableTUNTCPGRO = has(nodecap.DisableTUNTCPGRO)
neverGSOEqualTail = has(nodecap.NeverGSOEqualTail)
cacheNetworkMaps = has(nodecap.CacheNetworkMaps)
scopeQuad100OnMacOS = has(nodecap.ScopeQuad100OnMacOS)
)
if has(nodecap.OneCGNATEnable) {
oneCGNAT.Set(true)
} else if has(nodecap.OneCGNATDisable) {
oneCGNAT.Set(false)
}
k.DisableUPnP.Store(disableUPnP)
k.RandomizeClientPort.Store(randomizeClientPort)
k.OneCGNAT.Store(oneCGNAT)
k.ForceBackgroundSTUN.Store(forceBackgroundSTUN)
k.DisableDeltaUpdates.Store(disableDeltaUpdates)
k.PeerMTUEnable.Store(peerMTUEnable)
k.DisableDNSForwarderTCPRetries.Store(dnsForwarderDisableTCPRetries)
k.SilentDisco.Store(silentDisco)
k.LinuxForceIPTables.Store(forceIPTables)
k.LinuxForceNfTables.Store(forceNfTables)
k.ProbeUDPLifetime.Store(probeUDPLifetime)
k.AppCStoreRoutes.Store(appCStoreRoutes)
k.UserDialUseRoutes.Store(userDialUseRoutes)
k.DisableSplitDNSWhenNoCustomResolvers.Store(disableSplitDNSWhenNoCustomResolvers)
k.DisableLocalDNSOverrideViaNRPT.Store(disableLocalDNSOverrideViaNRPT)
k.DisableCaptivePortalDetection.Store(disableCaptivePortalDetection)
k.DisableSkipStatusQueue.Store(disableSkipStatusQueue)
k.DisableHostsFileUpdates.Store(disableHostsFileUpdates)
k.ForceRegisterMagicDNSIPv4Only.Store(forceRegisterMagicDNSIPv4Only)
k.EmitRuntimeMetrics.Store(emitRuntimeMetrics)
k.DisableUDPGRO.Store(disableUDPGRO)
k.DisableUDPGSO.Store(disableUDPGSO)
k.DisableTUNUDPGRO.Store(disableTUNUDPGRO)
k.DisableTUNTCPGRO.Store(disableTUNTCPGRO)
k.NeverGSOEqualTail.Store(neverGSOEqualTail)
k.CacheNetworkMaps.Store(cacheNetworkMaps)
k.ScopeQuad100OnMacOS.Store(scopeQuad100OnMacOS)
}
// AsDebugJSON returns k as something that can be marshalled with json.Marshal
// for debug.
func (k *Knobs) AsDebugJSON() map[string]any {
if k == nil {
return nil
}
ret := map[string]any{}
rv := reflect.ValueOf(k).Elem() // of *k
for sf, fv := range rv.Fields() {
switch v := fv.Addr().Interface().(type) {
case *atomic.Bool:
ret[sf.Name] = v.Load()
case *syncs.AtomicValue[opt.Bool]:
ret[sf.Name] = v.Load()
default:
panic(fmt.Sprintf("unknown field type %T for %v", v, sf.Name))
}
}
return ret
}
// ShouldForceRegisterMagicDNSIPv4Only reports the value of
// ForceRegisterMagicDNSIPv4Only, or false if k is nil.
func (k *Knobs) ShouldForceRegisterMagicDNSIPv4Only() bool {
return k != nil && k.ForceRegisterMagicDNSIPv4Only.Load()
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package disco contains the discovery message types.
//
// A discovery message is:
//
// Header:
//
// magic [6]byte // “TS💬” (0x54 53 f0 9f 92 ac)
// senderDiscoPub [32]byte // nacl public key
// nonce [24]byte
//
// The recipient then decrypts the bytes following (the nacl box)
// and then the inner payload structure is:
//
// messageType byte (the MessageType constants below)
// messageVersion byte (0 for now; but always ignore bytes at the end)
// message-payload [...]byte
package disco
import (
"encoding/binary"
"errors"
"fmt"
"net"
"net/netip"
"time"
"go4.org/mem"
"tailscale.com/types/key"
)
// Magic is the 6 byte header of all discovery messages.
const Magic = "TS💬" // 6 bytes: 0x54 53 f0 9f 92 ac
const keyLen = 32
// NonceLen is the length of the nonces used by nacl box.
const NonceLen = 24
type MessageType byte
const (
TypePing = MessageType(0x01)
TypePong = MessageType(0x02)
TypeCallMeMaybe = MessageType(0x03)
TypeBindUDPRelayEndpoint = MessageType(0x04)
TypeBindUDPRelayEndpointChallenge = MessageType(0x05)
TypeBindUDPRelayEndpointAnswer = MessageType(0x06)
TypeCallMeMaybeVia = MessageType(0x07)
TypeAllocateUDPRelayEndpointRequest = MessageType(0x08)
TypeAllocateUDPRelayEndpointResponse = MessageType(0x09)
)
const v0 = byte(0)
var errShort = errors.New("short message")
// LooksLikeDiscoWrapper reports whether p looks like it's a packet
// containing an encrypted disco message.
func LooksLikeDiscoWrapper(p []byte) bool {
if len(p) < len(Magic)+keyLen+NonceLen {
return false
}
return string(p[:len(Magic)]) == Magic
}
// Source returns the slice of p that represents the
// disco public key source, and whether p looks like
// a disco message.
func Source(p []byte) (src []byte, ok bool) {
if !LooksLikeDiscoWrapper(p) {
return nil, false
}
return p[len(Magic):][:keyLen], true
}
// Parse parses the encrypted part of the message from inside the
// nacl box.
func Parse(p []byte) (Message, error) {
if len(p) < 2 {
return nil, errShort
}
t, ver, p := MessageType(p[0]), p[1], p[2:]
switch t {
// TODO(jwhited): consider using a signature matching encoding.BinaryUnmarshaler
case TypePing:
return parsePing(ver, p)
case TypePong:
return parsePong(ver, p)
case TypeCallMeMaybe:
return parseCallMeMaybe(ver, p)
case TypeBindUDPRelayEndpoint:
return parseBindUDPRelayEndpoint(ver, p)
case TypeBindUDPRelayEndpointChallenge:
return parseBindUDPRelayEndpointChallenge(ver, p)
case TypeBindUDPRelayEndpointAnswer:
return parseBindUDPRelayEndpointAnswer(ver, p)
case TypeCallMeMaybeVia:
return parseCallMeMaybeVia(ver, p)
case TypeAllocateUDPRelayEndpointRequest:
return parseAllocateUDPRelayEndpointRequest(ver, p)
case TypeAllocateUDPRelayEndpointResponse:
return parseAllocateUDPRelayEndpointResponse(ver, p)
default:
return nil, fmt.Errorf("unknown message type 0x%02x", byte(t))
}
}
// Message a discovery message.
type Message interface {
// AppendMarshal appends the message's marshaled representation.
// TODO(jwhited): consider using a signature matching encoding.BinaryAppender
AppendMarshal([]byte) []byte
}
// MessageHeaderLen is the length of a message header, 2 bytes for type and version.
const MessageHeaderLen = 2
// appendMsgHeader appends two bytes (for t and ver) and then also
// dataLen bytes to b, returning the appended slice in all. The
// returned data slice is a subslice of all with just dataLen bytes of
// where the caller will fill in the data.
func appendMsgHeader(b []byte, t MessageType, ver uint8, dataLen int) (all, data []byte) {
// TODO: optimize this?
all = append(b, make([]byte, dataLen+2)...)
all[len(b)] = byte(t)
all[len(b)+1] = ver
data = all[len(b)+2:]
return
}
type Ping struct {
// TxID is a random client-generated per-ping transaction ID.
TxID [12]byte
// NodeKey is allegedly the ping sender's wireguard public key.
// Old clients (~1.16.0 and earlier) don't send this field.
// It shouldn't be trusted by itself, but can be combined with
// netmap data to reduce the discokey:nodekey relation from 1:N to
// 1:1.
NodeKey key.NodePublic
// Padding is the number of 0 bytes at the end of the
// message. (It's used to probe path MTU.)
Padding int
}
// PingLen is the length of a marshalled ping message, without the message
// header or padding.
const PingLen = 12 + key.NodePublicRawLen
func (m *Ping) AppendMarshal(b []byte) []byte {
dataLen := 12
hasKey := !m.NodeKey.IsZero()
if hasKey {
dataLen += key.NodePublicRawLen
}
ret, d := appendMsgHeader(b, TypePing, v0, dataLen+m.Padding)
n := copy(d, m.TxID[:])
if hasKey {
m.NodeKey.AppendTo(d[:n])
}
return ret
}
func parsePing(ver uint8, p []byte) (m *Ping, err error) {
if len(p) < 12 {
return nil, errShort
}
m = new(Ping)
m.Padding = len(p)
p = p[copy(m.TxID[:], p):]
m.Padding -= 12
// Deliberately lax on longer-than-expected messages, for future
// compatibility.
if len(p) >= key.NodePublicRawLen {
// Skip all-zero trailing bytes: treat them as padding, not a NodeKey,
// to match AppendMarshal (which omits zero keys).
nk := key.NodePublicFromRaw32(mem.B(p[:key.NodePublicRawLen]))
if !nk.IsZero() {
m.NodeKey = nk
m.Padding -= key.NodePublicRawLen
}
}
return m, nil
}
// CallMeMaybe is a message sent only over DERP to request that the recipient try
// to open up a magicsock path back to the sender.
//
// The sender should've already sent UDP packets to the peer to open
// up the stateful firewall mappings inbound.
//
// The recipient may choose to not open a path back, if it's already
// happy with its path. But usually it will.
type CallMeMaybe struct {
// MyNumber is what the peer believes its endpoints are.
//
// Prior to Tailscale 1.4, the endpoints were exchanged purely
// between nodes and the control server.
//
// Starting with Tailscale 1.4, clients advertise their endpoints.
// Older clients won't use this, but newer clients should
// use any endpoints in here that aren't included from control.
//
// Control might have sent stale endpoints if the client was idle
// before contacting us. In that case, the client likely did a STUN
// request immediately before sending the CallMeMaybe to recreate
// their NAT port mapping, and that new good endpoint is included
// in this field, but might not yet be in control's endpoints.
// (And in the future, control will stop distributing endpoints
// when clients are suitably new.)
MyNumber []netip.AddrPort
}
const epLength = 16 + 2 // 16 byte IP address + 2 byte port
func (m *CallMeMaybe) AppendMarshal(b []byte) []byte {
ret, p := appendMsgHeader(b, TypeCallMeMaybe, v0, epLength*len(m.MyNumber))
for _, ipp := range m.MyNumber {
a := ipp.Addr().As16()
copy(p[:], a[:])
binary.BigEndian.PutUint16(p[16:], ipp.Port())
p = p[epLength:]
}
return ret
}
func parseCallMeMaybe(ver uint8, p []byte) (m *CallMeMaybe, err error) {
m = new(CallMeMaybe)
if len(p)%epLength != 0 || ver != 0 || len(p) == 0 {
return m, nil
}
m.MyNumber = make([]netip.AddrPort, 0, len(p)/epLength)
for len(p) > 0 {
var a [16]byte
copy(a[:], p)
m.MyNumber = append(m.MyNumber, netip.AddrPortFrom(
netip.AddrFrom16(a).Unmap(),
binary.BigEndian.Uint16(p[16:18])))
p = p[epLength:]
}
return m, nil
}
// Pong is a response a Ping.
//
// It includes the sender's source IP + port, so it's effectively a
// STUN response.
type Pong struct {
TxID [12]byte
Src netip.AddrPort // 18 bytes (16+2) on the wire; v4-mapped ipv6 for IPv4
}
// pongLen is the length of a marshalled pong message, without the message
// header or padding.
const pongLen = 12 + 16 + 2
func (m *Pong) AppendMarshal(b []byte) []byte {
ret, d := appendMsgHeader(b, TypePong, v0, pongLen)
d = d[copy(d, m.TxID[:]):]
ip16 := m.Src.Addr().As16()
d = d[copy(d, ip16[:]):]
binary.BigEndian.PutUint16(d, m.Src.Port())
return ret
}
func parsePong(ver uint8, p []byte) (m *Pong, err error) {
if len(p) < pongLen {
return nil, errShort
}
m = new(Pong)
copy(m.TxID[:], p)
p = p[12:]
srcIP, _ := netip.AddrFromSlice(net.IP(p[:16]))
p = p[16:]
port := binary.BigEndian.Uint16(p)
m.Src = netip.AddrPortFrom(srcIP.Unmap(), port)
return m, nil
}
// MessageSummary returns a short summary of m for logging purposes.
func MessageSummary(m Message) string {
switch m := m.(type) {
case *Ping:
return fmt.Sprintf("ping tx=%x padding=%v", m.TxID[:6], m.Padding)
case *Pong:
return fmt.Sprintf("pong tx=%x", m.TxID[:6])
case *CallMeMaybe:
return "call-me-maybe"
case *CallMeMaybeVia:
return "call-me-maybe-via"
case *BindUDPRelayEndpoint:
return "bind-udp-relay-endpoint"
case *BindUDPRelayEndpointChallenge:
return "bind-udp-relay-endpoint-challenge"
case *BindUDPRelayEndpointAnswer:
return "bind-udp-relay-endpoint-answer"
case *AllocateUDPRelayEndpointRequest:
return "allocate-udp-relay-endpoint-request"
case *AllocateUDPRelayEndpointResponse:
return "allocate-udp-relay-endpoint-response"
default:
return fmt.Sprintf("%#v", m)
}
}
// BindUDPRelayHandshakeState represents the state of the 3-way bind handshake
// between UDP relay client and UDP relay server. Its potential values include
// those for both participants, UDP relay client and UDP relay server. A UDP
// relay server implementation can be found in net/udprelay.
type BindUDPRelayHandshakeState int
const (
// BindUDPRelayHandshakeStateInit represents the initial state prior to any
// message being transmitted.
BindUDPRelayHandshakeStateInit BindUDPRelayHandshakeState = iota
// BindUDPRelayHandshakeStateBindSent is the first client state after
// transmitting a BindUDPRelayEndpoint message to a UDP relay server.
BindUDPRelayHandshakeStateBindSent
// BindUDPRelayHandshakeStateChallengeSent is the first server state after
// receiving a BindUDPRelayEndpoint message from a UDP relay client and
// replying with a BindUDPRelayEndpointChallenge.
BindUDPRelayHandshakeStateChallengeSent
// BindUDPRelayHandshakeStateAnswerSent is a client state that is entered
// after transmitting a BindUDPRelayEndpointAnswer message towards a UDP
// relay server in response to a BindUDPRelayEndpointChallenge message.
BindUDPRelayHandshakeStateAnswerSent
// BindUDPRelayHandshakeStateAnswerReceived is a server state that is
// entered after it has received a correct BindUDPRelayEndpointAnswer
// message from a UDP relay client in response to a
// BindUDPRelayEndpointChallenge message.
BindUDPRelayHandshakeStateAnswerReceived
)
// bindUDPRelayEndpointCommonLen is the length of a marshalled
// [BindUDPRelayEndpointCommon], without the message header.
const bindUDPRelayEndpointCommonLen = 72
// BindUDPRelayChallengeLen is the length of the Challenge field carried in
// [BindUDPRelayEndpointChallenge] & [BindUDPRelayEndpointAnswer] messages.
const BindUDPRelayChallengeLen = 32
// BindUDPRelayEndpointCommon contains fields that are common across all 3
// UDP relay handshake message types. All 4 field values are expected to be
// consistent for the lifetime of a handshake besides Challenge, which is
// irrelevant in a [BindUDPRelayEndpoint] message.
type BindUDPRelayEndpointCommon struct {
// VNI is the Geneve header Virtual Network Identifier field value, which
// must match this disco-sealed value upon reception. If they are
// non-matching it indicates the cleartext Geneve header was tampered with
// and/or mangled.
VNI uint32
// Generation represents the handshake generation. Clients must set a new,
// nonzero value at the start of every handshake.
Generation uint32
// RemoteKey is the disco key of the remote peer participating over this
// relay endpoint.
RemoteKey key.DiscoPublic
// Challenge is set by the server in a [BindUDPRelayEndpointChallenge]
// message, and expected to be echoed back by the client in a
// [BindUDPRelayEndpointAnswer] message. Its value is irrelevant in a
// [BindUDPRelayEndpoint] message, where it simply serves a padding purpose
// ensuring all handshake messages are equal in size.
Challenge [BindUDPRelayChallengeLen]byte
}
// encode encodes m in b. b must be at least bindUDPRelayEndpointCommonLen bytes
// long.
func (m *BindUDPRelayEndpointCommon) encode(b []byte) {
binary.BigEndian.PutUint32(b, m.VNI)
b = b[4:]
binary.BigEndian.PutUint32(b, m.Generation)
b = b[4:]
m.RemoteKey.AppendTo(b[:0])
b = b[key.DiscoPublicRawLen:]
copy(b, m.Challenge[:])
}
// decode decodes m from b.
func (m *BindUDPRelayEndpointCommon) decode(b []byte) error {
if len(b) < bindUDPRelayEndpointCommonLen {
return errShort
}
m.VNI = binary.BigEndian.Uint32(b)
b = b[4:]
m.Generation = binary.BigEndian.Uint32(b)
b = b[4:]
m.RemoteKey = key.DiscoPublicFromRaw32(mem.B(b[:key.DiscoPublicRawLen]))
b = b[key.DiscoPublicRawLen:]
copy(m.Challenge[:], b[:BindUDPRelayChallengeLen])
return nil
}
// BindUDPRelayEndpoint is the first messaged transmitted from UDP relay client
// towards UDP relay server as part of the 3-way bind handshake.
type BindUDPRelayEndpoint struct {
BindUDPRelayEndpointCommon
}
func (m *BindUDPRelayEndpoint) AppendMarshal(b []byte) []byte {
ret, d := appendMsgHeader(b, TypeBindUDPRelayEndpoint, v0, bindUDPRelayEndpointCommonLen)
m.BindUDPRelayEndpointCommon.encode(d)
return ret
}
func parseBindUDPRelayEndpoint(ver uint8, p []byte) (m *BindUDPRelayEndpoint, err error) {
m = new(BindUDPRelayEndpoint)
err = m.BindUDPRelayEndpointCommon.decode(p)
if err != nil {
return nil, err
}
return m, nil
}
// BindUDPRelayEndpointChallenge is transmitted from UDP relay server towards
// UDP relay client in response to a BindUDPRelayEndpoint message as part of the
// 3-way bind handshake.
type BindUDPRelayEndpointChallenge struct {
BindUDPRelayEndpointCommon
}
func (m *BindUDPRelayEndpointChallenge) AppendMarshal(b []byte) []byte {
ret, d := appendMsgHeader(b, TypeBindUDPRelayEndpointChallenge, v0, bindUDPRelayEndpointCommonLen)
m.BindUDPRelayEndpointCommon.encode(d)
return ret
}
func parseBindUDPRelayEndpointChallenge(ver uint8, p []byte) (m *BindUDPRelayEndpointChallenge, err error) {
m = new(BindUDPRelayEndpointChallenge)
err = m.BindUDPRelayEndpointCommon.decode(p)
if err != nil {
return nil, err
}
return m, nil
}
// BindUDPRelayEndpointAnswer is transmitted from UDP relay client to UDP relay
// server in response to a BindUDPRelayEndpointChallenge message.
type BindUDPRelayEndpointAnswer struct {
BindUDPRelayEndpointCommon
}
func (m *BindUDPRelayEndpointAnswer) AppendMarshal(b []byte) []byte {
ret, d := appendMsgHeader(b, TypeBindUDPRelayEndpointAnswer, v0, bindUDPRelayEndpointCommonLen)
m.BindUDPRelayEndpointCommon.encode(d)
return ret
}
func parseBindUDPRelayEndpointAnswer(ver uint8, p []byte) (m *BindUDPRelayEndpointAnswer, err error) {
m = new(BindUDPRelayEndpointAnswer)
err = m.BindUDPRelayEndpointCommon.decode(p)
if err != nil {
return nil, err
}
return m, nil
}
// AllocateUDPRelayEndpointRequest is a message sent only over DERP to request
// allocation of a relay endpoint on a [tailscale.com/net/udprelay.Server]
type AllocateUDPRelayEndpointRequest struct {
// ClientDisco are the Disco public keys of the clients that should be
// permitted to handshake with the endpoint.
ClientDisco [2]key.DiscoPublic
// Generation represents the allocation request generation. The server must
// echo it back in the [AllocateUDPRelayEndpointResponse] to enable request
// and response alignment client-side.
Generation uint32
}
// allocateUDPRelayEndpointRequestLen is the length of a marshaled
// [AllocateUDPRelayEndpointRequest] message without the message header.
const allocateUDPRelayEndpointRequestLen = key.DiscoPublicRawLen*2 + // ClientDisco
4 // Generation
func (m *AllocateUDPRelayEndpointRequest) AppendMarshal(b []byte) []byte {
ret, p := appendMsgHeader(b, TypeAllocateUDPRelayEndpointRequest, v0, allocateUDPRelayEndpointRequestLen)
for i := range len(m.ClientDisco) {
disco := m.ClientDisco[i].AppendTo(nil)
copy(p, disco)
p = p[key.DiscoPublicRawLen:]
}
binary.BigEndian.PutUint32(p, m.Generation)
return ret
}
func parseAllocateUDPRelayEndpointRequest(ver uint8, p []byte) (m *AllocateUDPRelayEndpointRequest, err error) {
m = new(AllocateUDPRelayEndpointRequest)
if ver != 0 {
return
}
if len(p) < allocateUDPRelayEndpointRequestLen {
return m, errShort
}
for i := range len(m.ClientDisco) {
m.ClientDisco[i] = key.DiscoPublicFromRaw32(mem.B(p[:key.DiscoPublicRawLen]))
p = p[key.DiscoPublicRawLen:]
}
m.Generation = binary.BigEndian.Uint32(p)
return m, nil
}
// AllocateUDPRelayEndpointResponse is a message sent only over DERP in response
// to a [AllocateUDPRelayEndpointRequest].
type AllocateUDPRelayEndpointResponse struct {
// Generation represents the allocation request generation. The server must
// echo back the [AllocateUDPRelayEndpointRequest.Generation] here to enable
// request and response alignment client-side.
Generation uint32
UDPRelayEndpoint
}
func (m *AllocateUDPRelayEndpointResponse) AppendMarshal(b []byte) []byte {
endpointsLen := epLength * len(m.AddrPorts)
generationLen := 4
ret, d := appendMsgHeader(b, TypeAllocateUDPRelayEndpointResponse, v0, generationLen+udpRelayEndpointLenMinusAddrPorts+endpointsLen)
binary.BigEndian.PutUint32(d, m.Generation)
m.encode(d[4:])
return ret
}
func parseAllocateUDPRelayEndpointResponse(ver uint8, p []byte) (m *AllocateUDPRelayEndpointResponse, err error) {
m = new(AllocateUDPRelayEndpointResponse)
if ver != 0 {
return m, nil
}
if len(p) < 4 {
return m, errShort
}
m.Generation = binary.BigEndian.Uint32(p)
err = m.decode(p[4:])
return m, err
}
const udpRelayEndpointLenMinusAddrPorts = key.DiscoPublicRawLen + // ServerDisco
(key.DiscoPublicRawLen * 2) + // ClientDisco
8 + // LamportID
4 + // VNI
8 + // BindLifetime
8 // SteadyStateLifetime
// UDPRelayEndpoint is a mirror of [tailscale.com/net/udprelay/endpoint.ServerEndpoint],
// refer to it for field documentation. [UDPRelayEndpoint] is carried in both
// [CallMeMaybeVia] and [AllocateUDPRelayEndpointResponse] messages.
type UDPRelayEndpoint struct {
// ServerDisco is [tailscale.com/net/udprelay/endpoint.ServerEndpoint.ServerDisco]
ServerDisco key.DiscoPublic
// ClientDisco is [tailscale.com/net/udprelay/endpoint.ServerEndpoint.ClientDisco]
ClientDisco [2]key.DiscoPublic
// LamportID is [tailscale.com/net/udprelay/endpoint.ServerEndpoint.LamportID]
LamportID uint64
// VNI is [tailscale.com/net/udprelay/endpoint.ServerEndpoint.VNI]
VNI uint32
// BindLifetime is [tailscale.com/net/udprelay/endpoint.ServerEndpoint.BindLifetime]
BindLifetime time.Duration
// SteadyStateLifetime is [tailscale.com/net/udprelay/endpoint.ServerEndpoint.SteadyStateLifetime]
SteadyStateLifetime time.Duration
// AddrPorts is [tailscale.com/net/udprelay/endpoint.ServerEndpoint.AddrPorts]
AddrPorts []netip.AddrPort
}
// encode encodes m in b. b must be at least [udpRelayEndpointLenMinusAddrPorts]
// + [epLength] * len(m.AddrPorts) bytes long.
func (m *UDPRelayEndpoint) encode(b []byte) {
disco := m.ServerDisco.AppendTo(nil)
copy(b, disco)
b = b[key.DiscoPublicRawLen:]
for i := range len(m.ClientDisco) {
disco = m.ClientDisco[i].AppendTo(nil)
copy(b, disco)
b = b[key.DiscoPublicRawLen:]
}
binary.BigEndian.PutUint64(b[:8], m.LamportID)
b = b[8:]
binary.BigEndian.PutUint32(b[:4], m.VNI)
b = b[4:]
binary.BigEndian.PutUint64(b[:8], uint64(m.BindLifetime))
b = b[8:]
binary.BigEndian.PutUint64(b[:8], uint64(m.SteadyStateLifetime))
b = b[8:]
for _, ipp := range m.AddrPorts {
a := ipp.Addr().As16()
copy(b, a[:])
binary.BigEndian.PutUint16(b[16:18], ipp.Port())
b = b[epLength:]
}
}
// decode decodes m from b.
func (m *UDPRelayEndpoint) decode(b []byte) error {
if len(b) < udpRelayEndpointLenMinusAddrPorts+epLength ||
(len(b)-udpRelayEndpointLenMinusAddrPorts)%epLength != 0 {
return errShort
}
m.ServerDisco = key.DiscoPublicFromRaw32(mem.B(b[:key.DiscoPublicRawLen]))
b = b[key.DiscoPublicRawLen:]
for i := range len(m.ClientDisco) {
m.ClientDisco[i] = key.DiscoPublicFromRaw32(mem.B(b[:key.DiscoPublicRawLen]))
b = b[key.DiscoPublicRawLen:]
}
m.LamportID = binary.BigEndian.Uint64(b[:8])
b = b[8:]
m.VNI = binary.BigEndian.Uint32(b[:4])
b = b[4:]
m.BindLifetime = time.Duration(binary.BigEndian.Uint64(b[:8]))
b = b[8:]
m.SteadyStateLifetime = time.Duration(binary.BigEndian.Uint64(b[:8]))
b = b[8:]
m.AddrPorts = make([]netip.AddrPort, 0, len(b)/epLength)
for len(b) > 0 {
var a [16]byte
copy(a[:], b)
m.AddrPorts = append(m.AddrPorts, netip.AddrPortFrom(
netip.AddrFrom16(a).Unmap(),
binary.BigEndian.Uint16(b[16:18])))
b = b[epLength:]
}
return nil
}
// CallMeMaybeVia is a message sent only over DERP to request that the recipient
// try to open up a magicsock path back to the sender. The 'Via' in
// CallMeMaybeVia highlights that candidate paths are served through an
// intermediate relay, likely a [tailscale.com/net/udprelay.Server].
//
// Usage of the candidate paths in magicsock requires a 3-way handshake
// involving [BindUDPRelayEndpoint], [BindUDPRelayEndpointChallenge], and
// [BindUDPRelayEndpointAnswer].
//
// CallMeMaybeVia mirrors [tailscale.com/net/udprelay/endpoint.ServerEndpoint],
// which contains field documentation.
//
// The recipient may choose to not open a path back if it's already happy with
// its path. Direct connections, e.g. [CallMeMaybe]-signaled, take priority over
// CallMeMaybeVia paths.
type CallMeMaybeVia struct {
UDPRelayEndpoint
}
func (m *CallMeMaybeVia) AppendMarshal(b []byte) []byte {
endpointsLen := epLength * len(m.AddrPorts)
ret, p := appendMsgHeader(b, TypeCallMeMaybeVia, v0, udpRelayEndpointLenMinusAddrPorts+endpointsLen)
m.encode(p)
return ret
}
func parseCallMeMaybeVia(ver uint8, p []byte) (m *CallMeMaybeVia, err error) {
m = new(CallMeMaybeVia)
if ver != 0 {
return m, nil
}
err = m.decode(p)
return m, err
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package disco
import (
"bytes"
"encoding/binary"
"net/netip"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
// ToPCAPFrame marshals the bytes for a pcap record that describe a disco frame.
//
// Warning: Alloc garbage. Acceptable while capturing.
func ToPCAPFrame(src netip.AddrPort, derpNodeSrc key.NodePublic, payload []byte) []byte {
var (
b bytes.Buffer
flag uint8
)
b.Grow(128) // Most disco frames will probably be smaller than this.
if src.Addr() == tailcfg.DerpMagicIPAddr {
flag |= 0x01
}
b.WriteByte(flag) // 1b: flag
derpSrc := derpNodeSrc.Raw32()
b.Write(derpSrc[:]) // 32b: derp public key
binary.Write(&b, binary.LittleEndian, uint16(src.Port())) // 2b: port
addr, _ := src.Addr().MarshalBinary()
binary.Write(&b, binary.LittleEndian, uint16(len(addr))) // 2b: len(addr)
b.Write(addr) // Xb: addr
binary.Write(&b, binary.LittleEndian, uint16(len(payload))) // 2b: len(payload)
b.Write(payload) // Xb: payload
return b.Bytes()
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package envknob provides access to environment-variable tweakable
// debug settings.
//
// These are primarily knobs used by Tailscale developers during
// development or by users when instructed to by Tailscale developers
// when debugging something. They are not a stable interface and may
// be removed or any time.
//
// A related package, control/controlknobs, are knobs that can be
// changed at runtime by the control plane. Sometimes both are used:
// an envknob for the default/explicit value, else falling back
// to the controlknob value.
package envknob
import (
"bufio"
"errors"
"fmt"
"io"
"log"
"maps"
"os"
"path/filepath"
"runtime"
"slices"
"strconv"
"strings"
"sync/atomic"
"time"
"tailscale.com/feature/buildfeatures"
"tailscale.com/kube/kubetypes"
"tailscale.com/syncs"
"tailscale.com/types/opt"
"tailscale.com/util/testenv"
"tailscale.com/version"
"tailscale.com/version/distro"
)
var (
mu syncs.Mutex
// +checklocks:mu
set = map[string]string{}
// +checklocks:mu
regStr = map[string]*string{}
// +checklocks:mu
regBool = map[string]*bool{}
// +checklocks:mu
regOptBool = map[string]*opt.Bool{}
// +checklocks:mu
regDuration = map[string]*time.Duration{}
// +checklocks:mu
regInt = map[string]*int{}
)
func noteEnv(k, v string) {
mu.Lock()
defer mu.Unlock()
noteEnvLocked(k, v)
}
// +checklocks:mu
func noteEnvLocked(k, v string) {
if v != "" {
set[k] = v
} else {
delete(set, k)
}
}
// logf is logger.Logf, but logger depends on envknob, so for circular
// dependency reasons, make a type alias (so it's still assignable,
// but has nice docs here).
type logf = func(format string, args ...any)
// LogCurrent logs the currently set environment knobs.
func LogCurrent(logf logf) {
mu.Lock()
defer mu.Unlock()
for _, k := range slices.Sorted(maps.Keys(set)) {
logf("envknob: %s=%q", k, set[k])
}
}
// Setenv changes an environment variable.
//
// It is not safe for concurrent reading of environment variables via the
// Register functions. All Setenv calls are meant to happen early in main before
// any goroutines are started.
func Setenv(envVar, val string) {
mu.Lock()
defer mu.Unlock()
os.Setenv(envVar, val)
noteEnvLocked(envVar, val)
if p := regStr[envVar]; p != nil {
*p = val
}
if p := regBool[envVar]; p != nil {
setBoolLocked(p, envVar, val)
}
if p := regOptBool[envVar]; p != nil {
setOptBoolLocked(p, envVar, val)
}
if p := regDuration[envVar]; p != nil {
setDurationLocked(p, envVar, val)
}
}
// SetenvForTest safely changes an environment variable in a test.
//
// It is designed to avoid leaking state between tests. It registers
// a cleanup function to reset the variable once the test completes,
// and panics if you try to set environment variables in parallel tests.
//
// Use this instead of t.Setenv() if your variable will be read by envknob,
// and you need to update envknob's internal caches with the new value.
func SetenvForTest(t testenv.TB, envVar, val string) {
t.Helper()
// This is copied from [tstest.AssertNotParallel], which we can't
// call here because it would create an import cycle.
t.Setenv("ASSERT_NOT_PARALLEL_TEST", "1") // panics if t.Parallel was called
prev := String(envVar)
Setenv(envVar, val)
t.Cleanup(func() { Setenv(envVar, prev) })
}
// String returns the named environment variable, using os.Getenv.
//
// If the variable is non-empty, it's also tracked & logged as being
// an in-use knob.
func String(envVar string) string {
v := os.Getenv(envVar)
noteEnv(envVar, v)
return v
}
// RegisterString returns a func that gets the named environment variable,
// without a map lookup per call. It assumes that mutations happen via
// envknob.Setenv.
func RegisterString(envVar string) func() string {
mu.Lock()
defer mu.Unlock()
p, ok := regStr[envVar]
if !ok {
val := os.Getenv(envVar)
if val != "" {
noteEnvLocked(envVar, val)
}
p = &val
regStr[envVar] = p
}
return func() string { return *p }
}
// RegisterBool returns a func that gets the named environment variable,
// without a map lookup per call. It assumes that mutations happen via
// envknob.Setenv.
func RegisterBool(envVar string) func() bool {
mu.Lock()
defer mu.Unlock()
p, ok := regBool[envVar]
if !ok {
var b bool
p = &b
setBoolLocked(p, envVar, os.Getenv(envVar))
regBool[envVar] = p
}
return func() bool { return *p }
}
// RegisterOptBool returns a func that gets the named environment variable,
// without a map lookup per call. It assumes that mutations happen via
// envknob.Setenv.
func RegisterOptBool(envVar string) func() opt.Bool {
mu.Lock()
defer mu.Unlock()
p, ok := regOptBool[envVar]
if !ok {
var b opt.Bool
p = &b
setOptBoolLocked(p, envVar, os.Getenv(envVar))
regOptBool[envVar] = p
}
return func() opt.Bool { return *p }
}
// RegisterDuration returns a func that gets the named environment variable as a
// duration, without a map lookup per call. It assumes that any mutations happen
// via envknob.Setenv.
func RegisterDuration(envVar string) func() time.Duration {
mu.Lock()
defer mu.Unlock()
p, ok := regDuration[envVar]
if !ok {
val := os.Getenv(envVar)
if val != "" {
noteEnvLocked(envVar, val)
}
p = new(time.Duration)
setDurationLocked(p, envVar, val)
regDuration[envVar] = p
}
return func() time.Duration { return *p }
}
// RegisterInt returns a func that gets the named environment variable as an
// integer, without a map lookup per call. It assumes that any mutations happen
// via envknob.Setenv.
func RegisterInt(envVar string) func() int {
mu.Lock()
defer mu.Unlock()
p, ok := regInt[envVar]
if !ok {
val := os.Getenv(envVar)
if val != "" {
noteEnvLocked(envVar, val)
}
p = new(int)
setIntLocked(p, envVar, val)
regInt[envVar] = p
}
return func() int { return *p }
}
// +checklocks:mu
func setBoolLocked(p *bool, envVar, val string) {
noteEnvLocked(envVar, val)
if val == "" {
*p = false
return
}
var err error
*p, err = strconv.ParseBool(val)
if err != nil {
log.Fatalf("invalid boolean environment variable %s value %q", envVar, val)
}
}
// +checklocks:mu
func setOptBoolLocked(p *opt.Bool, envVar, val string) {
noteEnvLocked(envVar, val)
if val == "" {
*p = ""
return
}
b, err := strconv.ParseBool(val)
if err != nil {
log.Fatalf("invalid boolean environment variable %s value %q", envVar, val)
}
p.Set(b)
}
// +checklocks:mu
func setDurationLocked(p *time.Duration, envVar, val string) {
noteEnvLocked(envVar, val)
if val == "" {
*p = 0
return
}
var err error
*p, err = time.ParseDuration(val)
if err != nil {
log.Fatalf("invalid duration environment variable %s value %q", envVar, val)
}
}
// +checklocks:mu
func setIntLocked(p *int, envVar, val string) {
noteEnvLocked(envVar, val)
if val == "" {
*p = 0
return
}
var err error
*p, err = strconv.Atoi(val)
if err != nil {
log.Fatalf("invalid int environment variable %s value %q", envVar, val)
}
}
// Bool returns the boolean value of the named environment variable.
// If the variable is not set, it returns false.
// An invalid value exits the binary with a failure.
func Bool(envVar string) bool {
return boolOr(envVar, false)
}
// BoolDefaultTrue is like Bool, but returns true by default if the
// environment variable isn't present.
func BoolDefaultTrue(envVar string) bool {
return boolOr(envVar, true)
}
func boolOr(envVar string, implicitValue bool) bool {
assertNotInInit()
val := os.Getenv(envVar)
if val == "" {
return implicitValue
}
b, err := strconv.ParseBool(val)
if err == nil {
noteEnv(envVar, strconv.FormatBool(b)) // canonicalize
return b
}
log.Fatalf("invalid boolean environment variable %s value %q", envVar, val)
panic("unreachable")
}
// LookupBool returns the boolean value of the named environment value.
// The ok result is whether a value was set.
// If the value isn't a valid int, it exits the program with a failure.
func LookupBool(envVar string) (v bool, ok bool) {
assertNotInInit()
val := os.Getenv(envVar)
if val == "" {
return false, false
}
b, err := strconv.ParseBool(val)
if err == nil {
return b, true
}
log.Fatalf("invalid boolean environment variable %s value %q", envVar, val)
panic("unreachable")
}
// OptBool is like Bool, but returns an opt.Bool, so the caller can
// distinguish between implicitly and explicitly false.
func OptBool(envVar string) opt.Bool {
assertNotInInit()
b, ok := LookupBool(envVar)
if !ok {
return ""
}
var ret opt.Bool
ret.Set(b)
return ret
}
// LookupInt returns the integer value of the named environment value.
// The ok result is whether a value was set.
// If the value isn't a valid int, it exits the program with a failure.
func LookupInt(envVar string) (v int, ok bool) {
assertNotInInit()
val := os.Getenv(envVar)
if val == "" {
return 0, false
}
v, err := strconv.Atoi(val)
if err == nil {
noteEnv(envVar, val)
return v, true
}
log.Fatalf("invalid integer environment variable %s: %v", envVar, val)
panic("unreachable")
}
// LookupIntSized returns the integer value of the named environment value
// parsed in base and with a maximum bit size bitSize.
// The ok result is whether a value was set.
// If the value isn't a valid int, it exits the program with a failure.
func LookupIntSized(envVar string, base, bitSize int) (v int, ok bool) {
assertNotInInit()
val := os.Getenv(envVar)
if val == "" {
return 0, false
}
i, err := strconv.ParseInt(val, base, bitSize)
if err == nil {
v = int(i)
noteEnv(envVar, val)
return v, true
}
log.Fatalf("invalid integer environment variable %s: %v", envVar, val)
panic("unreachable")
}
// LookupUintSized returns the unsigned integer value of the named environment
// value parsed in base and with a maximum bit size bitSize.
// The ok result is whether a value was set.
// If the value isn't a valid int, it exits the program with a failure.
func LookupUintSized(envVar string, base, bitSize int) (v uint, ok bool) {
assertNotInInit()
val := os.Getenv(envVar)
if val == "" {
return 0, false
}
i, err := strconv.ParseUint(val, base, bitSize)
if err == nil {
v = uint(i)
noteEnv(envVar, val)
return v, true
}
log.Fatalf("invalid unsigned integer environment variable %s: %v", envVar, val)
panic("unreachable")
}
// UseWIPCode is whether TAILSCALE_USE_WIP_CODE is set to permit use
// of Work-In-Progress code.
func UseWIPCode() bool { return Bool("TAILSCALE_USE_WIP_CODE") }
// CanSSHD reports whether the Tailscale SSH server is allowed to run.
//
// If disabled (when this reports false), the SSH server won't start (won't
// intercept port 22) if previously configured to do so and any attempt to
// re-enable it will result in an error.
func CanSSHD() bool { return !Bool("TS_DISABLE_SSH_SERVER") }
// CanTaildrop reports whether the Taildrop feature is allowed to function.
//
// If disabled, Taildrop won't receive files regardless of user & server config.
func CanTaildrop() bool { return !Bool("TS_DISABLE_TAILDROP") }
// SSHPolicyFile returns the path, if any, to the SSHPolicy JSON file for development.
func SSHPolicyFile() string { return String("TS_DEBUG_SSH_POLICY_FILE") }
// SSHIgnoreTailnetPolicy reports whether to ignore the Tailnet SSH policy for development.
func SSHIgnoreTailnetPolicy() bool { return Bool("TS_DEBUG_SSH_IGNORE_TAILNET_POLICY") }
// TKASkipSignatureCheck reports whether to skip node-key signature checking for development.
func TKASkipSignatureCheck() bool { return Bool("TS_UNSAFE_SKIP_NKS_VERIFICATION") }
// AssumeNetworkUp reports whether to assume network connectivity for development.
func AssumeNetworkUp() bool { return Bool("TS_ASSUME_NETWORK_UP_FOR_TEST") }
// App returns the tailscale app type of this instance, if set via
// TS_INTERNAL_APP env var. TS_INTERNAL_APP can be used to set app type for
// components that wrap tailscaled, such as containerboot. App type is intended
// to only be used to set known predefined app types, such as Tailscale
// Kubernetes Operator components.
func App() string {
a := os.Getenv("TS_INTERNAL_APP")
if a == kubetypes.AppConnector || a == kubetypes.AppEgressProxy || a == kubetypes.AppIngressProxy || a == kubetypes.AppIngressResource || a == kubetypes.AppProxyGroupEgress || a == kubetypes.AppProxyGroupIngress {
return a
}
return ""
}
// IsCertShareReadOnlyMode returns true if this replica should never attempt to
// issue or renew TLS credentials for any of the HTTPS endpoints that it is
// serving. It should only return certs found in its cert store. Currently,
// this is used by the Kubernetes Operator's HA Ingress via VIPServices, where
// multiple Ingress proxy instances serve the same HTTPS endpoint with a shared
// TLS credentials. The TLS credentials should only be issued by one of the
// replicas.
// For HTTPS Ingress the operator and containerboot ensure
// that read-only replicas will not be serving the HTTPS endpoints before there
// is a shared cert available.
func IsCertShareReadOnlyMode() bool {
m := String("TS_CERT_SHARE_MODE")
return m == "ro"
}
// IsCertShareReadWriteMode returns true if this instance is the replica
// responsible for issuing and renewing TLS certs in an HA setup with certs
// shared between multiple replicas.
func IsCertShareReadWriteMode() bool {
m := String("TS_CERT_SHARE_MODE")
return m == "rw"
}
// CrashOnUnexpected reports whether the Tailscale client should panic
// on unexpected conditions. If TS_DEBUG_CRASH_ON_UNEXPECTED is set, that's
// used. Otherwise the default value is true for unstable builds.
func CrashOnUnexpected() bool {
if v, ok := crashOnUnexpected().Get(); ok {
return v
}
return version.IsUnstableBuild()
}
var crashOnUnexpected = RegisterOptBool("TS_DEBUG_CRASH_ON_UNEXPECTED")
// NoLogsNoSupport reports whether the client's opted out of log uploads and
// technical support.
func NoLogsNoSupport() bool {
return Bool("TS_NO_LOGS_NO_SUPPORT")
}
var allowRemoteUpdate = RegisterBool("TS_ALLOW_ADMIN_CONSOLE_REMOTE_UPDATE")
// AllowsRemoteUpdate reports whether this node has opted-in to letting the
// Tailscale control plane initiate a Tailscale update (e.g. on behalf of an
// admin on the admin console).
func AllowsRemoteUpdate() bool {
if !buildfeatures.HasClientUpdate {
return false
}
return allowRemoteUpdate()
}
// SetNoLogsNoSupport enables no-logs-no-support mode.
func SetNoLogsNoSupport() {
Setenv("TS_NO_LOGS_NO_SUPPORT", "true")
}
// notInInit is set true the first time we've seen a non-init stack trace.
var notInInit atomic.Bool
func assertNotInInit() {
if !buildfeatures.HasDebug {
return
}
if notInInit.Load() {
return
}
skip := 0
for {
pc, _, _, ok := runtime.Caller(skip)
if !ok {
notInInit.Store(true)
return
}
fu := runtime.FuncForPC(pc)
if fu == nil {
return
}
name := fu.Name()
name = strings.TrimRightFunc(name, func(r rune) bool { return r >= '0' && r <= '9' })
if strings.HasSuffix(name, ".init") || strings.HasSuffix(name, ".init.") {
stack := make([]byte, 1<<10)
stack = stack[:runtime.Stack(stack, false)]
envCheckedInInitStack = stack
}
skip++
}
}
var envCheckedInInitStack []byte
// PanicIfAnyEnvCheckedInInit panics if environment variables were read during
// init.
func PanicIfAnyEnvCheckedInInit() {
if envCheckedInInitStack != nil {
panic("envknob check of called from init function: " + string(envCheckedInInitStack))
}
}
var applyDiskConfigErr error
// ApplyDiskConfigError returns the most recent result of ApplyDiskConfig.
func ApplyDiskConfigError() error { return applyDiskConfigErr }
// ApplyDiskConfig returns a platform-specific config file of environment
// keys/values and applies them. On Linux and Unix operating systems, it's a
// no-op and always returns nil. If no platform-specific config file is found,
// it also returns nil.
//
// It exists primarily for Windows and macOS to make it easy to apply
// environment variables to a running service in a way similar to modifying
// /etc/default/tailscaled on Linux.
//
// On Windows, you use %ProgramData%\Tailscale\tailscaled-env.txt instead.
//
// On macOS, use one of:
//
// - /private/var/root/Library/Containers/io.tailscale.ipn.macsys.network-extension/Data/tailscaled-env.txt
// for standalone macOS GUI builds
// - ~/Library/Containers/io.tailscale.ipn.macos.network-extension/Data/tailscaled-env.txt
// for App Store builds
// - /etc/tailscale/tailscaled-env.txt for tailscaled-on-macOS (homebrew, etc)
func ApplyDiskConfig() (err error) {
if runtime.GOOS == "linux" && !(buildfeatures.HasDebug || buildfeatures.HasSynology) {
// This function does nothing on Linux, unless you're
// using TS_DEBUG_ENV_FILE or are on Synology.
return nil
}
var f *os.File
defer func() {
if err != nil {
// Stash away our return error for the healthcheck package to use.
if f != nil {
applyDiskConfigErr = fmt.Errorf("error parsing %s: %w", f.Name(), err)
} else {
applyDiskConfigErr = fmt.Errorf("error applying disk config: %w", err)
}
}
}()
// First try the explicitly-provided value for development testing. Not
// useful for users to use on their own. (if they can set this, they can set
// any environment variable anyway)
if name := os.Getenv("TS_DEBUG_ENV_FILE"); name != "" {
f, err = os.Open(name)
if err != nil {
return fmt.Errorf("error opening explicitly configured TS_DEBUG_ENV_FILE: %w", err)
}
defer f.Close()
return applyKeyValueEnv(f)
}
names := getPlatformEnvFiles()
if len(names) == 0 {
return nil
}
var errs []error
for _, name := range names {
f, err = os.Open(name)
if os.IsNotExist(err) {
continue
}
if err != nil {
errs = append(errs, err)
continue
}
defer f.Close()
return applyKeyValueEnv(f)
}
// If we have any errors, return them; if all errors are such that
// os.IsNotExist(err) returns true, then errs is empty and we will
// return nil.
return errors.Join(errs...)
}
// getPlatformEnvFiles returns a list of paths to the current platform's
// optional tailscaled-env.txt file. It returns an empty list if none is
// defined for the platform.
func getPlatformEnvFiles() []string {
switch runtime.GOOS {
case "windows":
return []string{
filepath.Join(os.Getenv("ProgramData"), "Tailscale", "tailscaled-env.txt"),
}
case "linux":
if buildfeatures.HasSynology && distro.Get() == distro.Synology {
return []string{"/etc/tailscale/tailscaled-env.txt"}
}
case "darwin":
if version.IsSandboxedMacOS() { // the two GUI variants (App Store or separate download)
// On the App Store variant, the home directory is set
// to something like:
// ~/Library/Containers/io.tailscale.ipn.macos.network-extension/Data
//
// On the macsys (downloadable Mac GUI) variant, the
// home directory can be unset, but we have a working
// directory that looks like:
// /private/var/root/Library/Containers/io.tailscale.ipn.macsys.network-extension/Data
//
// Try both and see if we can find the file in either
// location.
var candidates []string
if home := os.Getenv("HOME"); home != "" {
candidates = append(candidates, filepath.Join(home, "tailscaled-env.txt"))
}
if wd, err := os.Getwd(); err == nil {
candidates = append(candidates, filepath.Join(wd, "tailscaled-env.txt"))
}
return candidates
} else {
// Open source / homebrew variable, running tailscaled-on-macOS.
return []string{"/etc/tailscale/tailscaled-env.txt"}
}
}
return nil
}
// applyKeyValueEnv reads key=value lines r and calls Setenv for each.
//
// Empty lines and lines beginning with '#' are skipped.
//
// Values can be double quoted, in which case they're unquoted using
// strconv.Unquote.
func applyKeyValueEnv(r io.Reader) error {
bs := bufio.NewScanner(r)
for bs.Scan() {
line := strings.TrimSpace(bs.Text())
if line == "" || line[0] == '#' {
continue
}
k, v, ok := strings.Cut(line, "=")
k = strings.TrimSpace(k)
if !ok || k == "" {
continue
}
v = strings.TrimSpace(v)
if strings.HasPrefix(v, `"`) {
var err error
v, err = strconv.Unquote(v)
if err != nil {
return fmt.Errorf("invalid value in line %q: %v", line, err)
}
}
Setenv(k, v)
}
return bs.Err()
}
// IPCVersion returns version.Long usually, unless TS_DEBUG_FAKE_IPC_VERSION is
// set, in which it contains that value. This is only used for weird development
// cases when testing mismatched versions and you want the client to act like it's
// compatible with the server.
func IPCVersion() string {
if v := String("TS_DEBUG_FAKE_IPC_VERSION"); v != "" {
return v
}
return version.Long()
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_not_in_tests
package envknob
import "runtime"
// GOOS reports the effective runtime.GOOS to run as.
//
// In practice this returns just runtime.GOOS, unless overridden by
// test TS_DEBUG_FAKE_GOOS.
//
// This allows changing OS-specific stuff like the IPN server behavior
// for tests so we can e.g. test Windows-specific behaviors on Linux.
// This isn't universally used.
func GOOS() string {
if v := String("TS_DEBUG_FAKE_GOOS"); v != "" {
return v
}
return runtime.GOOS
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package feature tracks which features are linked into the binary.
package feature
import (
"errors"
"reflect"
"tailscale.com/util/testenv"
)
var ErrUnavailable = errors.New("feature not included in this build")
var in = map[string]bool{}
// Registered reports the set of registered features.
//
// The returned map should not be modified by the caller,
// not accessed concurrently with calls to Register.
func Registered() map[string]bool { return in }
// IsRegistered reports whether the named feature package's init
// function has run and registered itself via [Register] in this
// binary. It is distinct from the compile-time [buildfeatures]
// constants: a feature package can be present in the binary but not
// imported (e.g. tsnet deliberately does not import many features),
// in which case its init does not run.
func IsRegistered(name string) bool { return in[name] }
// Register notes that the named feature is linked into the binary.
func Register(name string) {
if _, ok := in[name]; ok {
panic("duplicate feature registration for " + name)
}
in[name] = true
}
// Hook is a func that can only be set once.
//
// It is not safe for concurrent use.
type Hook[Func any] struct {
f Func
ok bool
}
// IsSet reports whether the hook has been set.
func (h *Hook[Func]) IsSet() bool {
return h.ok
}
// Set sets the hook function, panicking if it's already been set
// or f is the zero value.
//
// It's meant to be called in init.
func (h *Hook[Func]) Set(f Func) {
if h.ok {
panic("Set on already-set feature hook")
}
if reflect.ValueOf(f).IsZero() {
panic("Set with zero value")
}
h.f = f
h.ok = true
}
// SetForTest sets the hook function for tests, blowing
// away any previous value. It will panic if called from
// non-test code.
//
// It returns a restore function that resets the hook
// to its previous value.
func (h *Hook[Func]) SetForTest(f Func) (restore func()) {
testenv.AssertInTest()
old := *h
h.f, h.ok = f, true
return func() { *h = old }
}
// Get returns the hook function, or panics if it hasn't been set.
// Use IsSet to check if it's been set, or use GetOrNil if you're
// okay with a nil return value.
func (h *Hook[Func]) Get() Func {
if !h.ok {
panic("Get on unset feature hook, without IsSet")
}
return h.f
}
// GetOk returns the hook function and true if it has been set,
// otherwise its zero value and false.
func (h *Hook[Func]) GetOk() (f Func, ok bool) {
return h.f, h.ok
}
// GetOrNil returns the hook function or nil if it hasn't been set.
func (h *Hook[Func]) GetOrNil() Func {
return h.f
}
// Hooks is a slice of funcs.
//
// As opposed to a single Hook, this is meant to be used when
// multiple parties are able to install the same hook.
type Hooks[Func any] []Func
// Add adds a hook to the list of hooks.
//
// Add should only be called during early program
// startup before Tailscale has started.
// It is not safe for concurrent use.
func (h *Hooks[Func]) Add(f Func) {
if reflect.ValueOf(f).IsZero() {
panic("Add with zero value")
}
*h = append(*h, f)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package feature
import (
"io"
"net/http"
"net/url"
"os"
"sync"
"tailscale.com/types/logger"
"tailscale.com/types/persist"
)
// HookRegisterLogSinkFlags is a hook for the syslog feature to register
// its flags (such as tailscaled's --syslog) with the process's default
// flag set. If set, tailscaled calls it before flag parsing.
var HookRegisterLogSinkFlags Hook[func()]
// HookLogSink is a hook for the syslog feature to redirect the process's
// logs to an alternate sink. If set, tailscaled calls it once early in
// main, after flag parsing; on that first call, if the user requested an
// alternate sink, it points the standard library's default logger at that
// sink. It returns the sink, or nil if logs are not being redirected.
// Later callers (such as logpolicy, which otherwise writes its console
// copy of logs to stderr) use the returned writer to send their logs to
// the same place.
var HookLogSink Hook[func() io.Writer]
// HookCanAutoUpdate is a hook for the clientupdate package
// to conditionally initialize.
var HookCanAutoUpdate Hook[func() bool]
var testAllowAutoUpdate = sync.OnceValue(func() bool {
return os.Getenv("TS_TEST_ALLOW_AUTO_UPDATE") == "1"
})
// CanAutoUpdate reports whether the current binary is built with auto-update
// support and, if so, whether the current platform supports it.
func CanAutoUpdate() bool {
if testAllowAutoUpdate() {
return true
}
if f, ok := HookCanAutoUpdate.GetOk(); ok {
return f()
}
return false
}
// HookProxyFromEnvironment is a hook for feature/useproxy to register
// a function to use as http.ProxyFromEnvironment.
var HookProxyFromEnvironment Hook[func(*http.Request) (*url.URL, error)]
// HookProxyInvalidateCache is a hook for feature/useproxy to register
// [tshttpproxy.InvalidateCache].
var HookProxyInvalidateCache Hook[func()]
// HookProxyGetAuthHeader is a hook for feature/useproxy to register
// [tshttpproxy.GetAuthHeader].
var HookProxyGetAuthHeader Hook[func(*url.URL) (string, error)]
// HookProxySetSelfProxy is a hook for feature/useproxy to register
// [tshttpproxy.SetSelfProxy].
var HookProxySetSelfProxy Hook[func(...string)]
// HookProxySetTransportGetProxyConnectHeader is a hook for feature/useproxy to register
// [tshttpproxy.SetTransportGetProxyConnectHeader].
var HookProxySetTransportGetProxyConnectHeader Hook[func(*http.Transport)]
// HookTPMAvailable is a hook that reports whether a TPM device is supported
// and available.
var HookTPMAvailable Hook[func() bool]
var HookGenerateAttestationKeyIfEmpty Hook[func(p *persist.Persist, logf logger.Logf) (bool, error)]
// TPMAvailable reports whether a TPM device is supported and available.
func TPMAvailable() bool {
if f, ok := HookTPMAvailable.GetOk(); ok {
return f()
}
return false
}
// HookGetSSHHostKeyPublicStrings is a hook for the ssh/hostkeys package to
// provide SSH host key public strings to ipn/ipnlocal without ipnlocal needing
// to import golang.org/x/crypto/ssh.
var HookGetSSHHostKeyPublicStrings Hook[func(varRoot string, logf logger.Logf) ([]string, error)]
// HookHardwareAttestationAvailable is a hook that reports whether hardware
// attestation is supported and available.
var HookHardwareAttestationAvailable Hook[func() bool]
// HardwareAttestationAvailable reports whether hardware attestation is
// supported and available (TPM on Windows/Linux, Secure Enclave on macOS|iOS,
// KeyStore on Android)
func HardwareAttestationAvailable() bool {
if f, ok := HookHardwareAttestationAvailable.GetOk(); ok {
return f()
}
return false
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package feature
import (
"runtime"
"tailscale.com/feature/buildfeatures"
)
// HookSystemdReady sends a readiness to systemd. This will unblock service
// dependents from starting.
var HookSystemdReady Hook[func()]
// HookSystemdStatus holds a func that will send a single line status update to
// systemd so that information shows up in systemctl output.
var HookSystemdStatus Hook[func(format string, args ...any)]
// SystemdStatus sends a single line status update to systemd so that
// information shows up in systemctl output.
//
// It does nothing on non-Linux systems or if the binary was built without
// the sdnotify feature.
func SystemdStatus(format string, args ...any) {
if !CanSystemdStatus { // mid-stack inlining DCE
return
}
if f, ok := HookSystemdStatus.GetOk(); ok {
f(format, args...)
}
}
// CanSystemdStatus reports whether the current build has systemd notifications
// linked in.
//
// It's effectively the same as HookSystemdStatus.IsSet(), but a constant for
// dead code elimination reasons.
const CanSystemdStatus = runtime.GOOS == "linux" && buildfeatures.HasSDNotify
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package health is a registry for other packages to report & check
// overall health status of the node.
package health
import (
"context"
"errors"
"fmt"
"maps"
"net/http"
"os"
"runtime"
"sort"
"sync"
"sync/atomic"
"time"
"tailscale.com/envknob"
"tailscale.com/feature/buildfeatures"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
"tailscale.com/tstime"
"tailscale.com/types/opt"
"tailscale.com/util/cibuild"
"tailscale.com/util/eventbus"
"tailscale.com/util/mak"
"tailscale.com/version"
)
var (
mu syncs.Mutex
debugHandler map[string]http.Handler
)
// ReceiveFunc is one of the three magicsock Receive funcs (IPv4, IPv6, or
// DERP).
type ReceiveFunc int
// ReceiveFunc indices for Tracker.MagicSockReceiveFuncs.
const (
ReceiveIPv4 ReceiveFunc = 0
ReceiveIPv6 ReceiveFunc = 1
ReceiveDERP ReceiveFunc = 2
)
func (f ReceiveFunc) String() string {
if f < 0 || int(f) >= len(receiveNames) {
return fmt.Sprintf("ReceiveFunc(%d)", f)
}
return receiveNames[f]
}
var receiveNames = []string{
ReceiveIPv4: "ReceiveIPv4",
ReceiveIPv6: "ReceiveIPv6",
ReceiveDERP: "ReceiveDERP",
}
// Tracker tracks the health of various Tailscale subsystems,
// comparing each subsystems' state with each other to make sure
// they're consistent based on the user's intended state.
//
// If a client [Warnable] becomes unhealthy or its unhealthy state is updated,
// an event will be emitted with WarnableChanged set to true and the Warnable
// and its UnhealthyState:
//
// Change{WarnableChanged: true, Warnable: w, UnhealthyState: us}
//
// If a Warnable becomes healthy, an event will be emitted with
// WarnableChanged set to true, the Warnable set, and UnhealthyState set to nil:
//
// Change{WarnableChanged: true, Warnable: w, UnhealthyState: nil}
//
// If the health messages from the control-plane change, an event will be
// emitted with ControlHealthChanged set to true. Recipients can fetch the set of
// control-plane health messages by calling [Tracker.CurrentState]:
type Tracker struct {
// MagicSockReceiveFuncs tracks the state of the three
// magicsock receive functions: IPv4, IPv6, and DERP.
MagicSockReceiveFuncs [3]ReceiveFuncStats // indexed by ReceiveFunc values
// initOnce guards the initialization of the Tracker.
// Notably, it initializes the MagicSockReceiveFuncs names.
// mu should not be held during init.
initOnce sync.Once
testClock tstime.Clock // nil means use time.Now / tstime.StdClock{}
eventClient *eventbus.Client
changePub *eventbus.Publisher[Change]
// mu guards everything that follows.
mu sync.Mutex
warnables []*Warnable // keys ever set
warnableVal map[*Warnable]*warningState
// pendingVisibleTimers contains timers for Warnables that are unhealthy, but are
// not visible to the user yet, because they haven't been unhealthy for TimeToVisible
pendingVisibleTimers map[*Warnable]tstime.TimerController
// sysErr maps subsystems to their current error (or nil if the subsystem is healthy)
// Deprecated: using Warnables should be preferred
sysErr map[Subsystem]error
timer tstime.TimerController
latestVersion *tailcfg.ClientVersion // or nil
checkForUpdates bool
applyUpdates opt.Bool
inMapPoll bool
inMapPollSince time.Time
lastMapPollEndedAt time.Time
lastStreamedMapResponse time.Time
lastNoiseDial time.Time
derpHomeRegion tailcfg.DERPRegionID
derpHomeless bool
derpRegionConnected map[tailcfg.DERPRegionID]bool
derpRegionHealthProblem map[tailcfg.DERPRegionID]string
derpRegionLastFrame map[tailcfg.DERPRegionID]time.Time
derpMap *tailcfg.DERPMap // last DERP map from control, could be nil if never received one
lastMapRequestHeard time.Time // time we got a 200 from control for a MapRequest
ipnState string
ipnWantRunning bool
ipnWantRunningLastTrue time.Time // when ipnWantRunning last changed false -> true
anyInterfaceUp opt.Bool // empty means unknown (assume true)
lastNotifiedControlMessages map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage // latest control messages processed, kept for change detection
controlMessages map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage // latest control messages received
lastLoginErr error
localLogConfigErr error
tlsConnectionErrors map[string]error // map[ServerName]error
metricHealthMessage any // nil or *metrics.MultiLabelMap[metricHealthMessageLabel]
// IP forwarding check
// If non-nil, called periodically to check if IP forwarding is broken.
// Should return true if broken, false if healthy.
isIPForwardingBroken func() bool
}
// NewTracker contructs a new [Tracker] and attaches the given eventbus.
// NewTracker will panic is no eventbus is given.
func NewTracker(bus *eventbus.Bus) *Tracker {
if !buildfeatures.HasHealth {
return &Tracker{}
}
if bus == nil {
panic("no eventbus set")
}
ec := bus.Client("health.Tracker")
t := &Tracker{
eventClient: ec,
changePub: eventbus.Publish[Change](ec),
}
t.timer = t.clock().AfterFunc(time.Minute, t.timerSelfCheck)
ec.Monitor(t.awaitEventClientDone)
return t
}
func (t *Tracker) awaitEventClientDone(ec *eventbus.Client) {
<-ec.Done()
t.mu.Lock()
defer t.mu.Unlock()
for _, timer := range t.pendingVisibleTimers {
timer.Stop()
}
t.timer.Stop()
clear(t.pendingVisibleTimers)
}
func (t *Tracker) now() time.Time {
if t.testClock != nil {
return t.testClock.Now()
}
return time.Now()
}
func (t *Tracker) clock() tstime.Clock {
if t.testClock != nil {
return t.testClock
}
return tstime.StdClock{}
}
// Subsystem is the name of a subsystem whose health can be monitored.
//
// Deprecated: Registering a Warnable using Register() and updating its health state
// with SetUnhealthy() and SetHealthy() should be preferred.
type Subsystem string
const (
// SysRouter is the name of the wgengine/router subsystem.
SysRouter = Subsystem("router")
// SysDNS is the name of the net/dns subsystem.
SysDNS = Subsystem("dns")
// SysDNSManager is the name of the net/dns manager subsystem.
SysDNSManager = Subsystem("dns-manager")
// SysTKA is the name of the tailnet key authority subsystem.
SysTKA = Subsystem("tailnet-lock")
)
var subsystemsWarnables = map[Subsystem]*Warnable{}
func init() {
for _, s := range []Subsystem{SysRouter, SysDNS, SysDNSManager, SysTKA} {
w := Register(&Warnable{
Code: WarnableCode(s),
Severity: SeverityMedium,
Text: func(args Args) string {
return args[legacyErrorArgKey]
},
})
subsystemsWarnables[s] = w
}
}
const legacyErrorArgKey = "LegacyError"
// Warnable returns a Warnable representing a legacy Subsystem. This is used
// temporarily (2024-06-14) while we migrate the old health infrastructure based
// on Subsystems to the new Warnables architecture.
func (s Subsystem) Warnable() *Warnable {
if !buildfeatures.HasHealth {
return &noopWarnable
}
w, ok := subsystemsWarnables[s]
if !ok {
panic(fmt.Sprintf("health: no Warnable for Subsystem %q", s))
}
return w
}
var registeredWarnables = map[WarnableCode]*Warnable{}
var noopWarnable Warnable
// Register registers a new Warnable with the health package and returns it.
// Register panics if the Warnable was already registered, because Warnables
// should be unique across the program.
func Register(w *Warnable) *Warnable {
if !buildfeatures.HasHealth {
return &noopWarnable
}
if registeredWarnables[w.Code] != nil {
panic(fmt.Sprintf("health: a Warnable with code %q was already registered", w.Code))
}
mak.Set(®isteredWarnables, w.Code, w)
return w
}
// unregister removes a Warnable from the health package. It should only be used
// for testing purposes.
func unregister(w *Warnable) {
if !buildfeatures.HasHealth {
return
}
if registeredWarnables[w.Code] == nil {
panic(fmt.Sprintf("health: attempting to unregister Warnable %q that was not registered", w.Code))
}
delete(registeredWarnables, w.Code)
}
// WarnableCode is a string that distinguishes each Warnable from others. It is globally unique within
// the program.
type WarnableCode string
// A Warnable is something that we might want to warn the user about, or not. A
// Warnable is either in a healthy or unhealthy state. A Warnable is unhealthy if
// the Tracker knows about a WarningState affecting the Warnable.
//
// In most cases, Warnables are components of the backend (for instance, "DNS"
// or "Magicsock"). Warnables are similar to the Subsystem type previously used
// in this package, but they provide a unique identifying code for each
// Warnable, along with more metadata that makes it easier for a GUI to display
// the Warnable in a user-friendly way.
type Warnable struct {
// Code is a string that uniquely identifies this Warnable across the entire Tailscale backend,
// and can be mapped to a user-displayable localized string.
Code WarnableCode
// Title is a string that the GUI uses as title for any message involving this Warnable. The title
// should be short and fit in a single line.
Title string
// Text is a function that generates an extended string that the GUI will display to the user when
// this Warnable is in an unhealthy state. The function can use the Args map to provide dynamic
// information to the user.
Text func(args Args) string
// Severity is the severity of the Warnable, which the GUI can use to determine how to display it.
// For instance, a Warnable with SeverityHigh could trigger a modal view, while a Warnable with
// SeverityLow could be displayed in a less intrusive way.
// TODO(angott): turn this into a SeverityFunc, which allows the Warnable to change its severity based on
// the Args of the unhappy state, just like we do in the Text function.
Severity Severity
// DependsOn is a set of Warnables that this Warnable depends on and need to be healthy
// before this Warnable is relevant. The GUI can use this information to ignore
// this Warnable if one of its dependencies is unhealthy.
// That is, if any of these Warnables are unhealthy, then this Warnable is not relevant
// and should be considered healthy to bother the user about.
DependsOn []*Warnable
// MapDebugFlag is a MapRequest.DebugFlag that is sent to control when this Warnable is unhealthy
//
// Deprecated: this is only used in one case, and will be removed in a future PR
MapDebugFlag string
// ImpactsConnectivity is whether this Warnable in an unhealthy state will impact the user's
// ability to connect to the Internet or other nodes on the tailnet. On platforms where
// the client GUI supports a tray icon, the client will display an exclamation mark
// on the tray icon when ImpactsConnectivity is set to true and the Warnable is unhealthy.
ImpactsConnectivity bool
// TimeToVisible is the Duration that the Warnable has to be in an unhealthy state before it
// should be surfaced as unhealthy to the user. This is used to prevent transient errors from being
// displayed to the user.
TimeToVisible time.Duration
}
// StaticMessage returns a function that always returns the input string, to be used in
// simple Warnables that do not use the Args map to generate their Text.
func StaticMessage(s string) func(Args) string {
return func(Args) string { return s }
}
// nil reports whether t is nil.
// It exists to accept nil *Tracker receivers on all methods
// to at least not crash. But because a nil receiver indicates
// some lost Tracker plumbing, we want to capture stack trace
// samples when it occurs.
func (t *Tracker) nil() bool {
if !buildfeatures.HasHealth {
return true
}
if t != nil {
return false
}
if cibuild.On() {
stack := make([]byte, 1<<10)
stack = stack[:runtime.Stack(stack, false)]
fmt.Fprintf(os.Stderr, "## WARNING: (non-fatal) nil health.Tracker (being strict in CI):\n%s\n", stack)
}
// TODO(bradfitz): open source our "unexpected" package
// and use it here to capture samples of stacks where
// t is nil.
return true
}
// ProbeLocks acquires and releases the tracker's internal mutex.
func (t *Tracker) ProbeLocks() {
if t.nil() {
return
}
t.mu.Lock()
t.mu.Unlock()
}
// Severity represents how serious an error is. Each GUI interprets this severity value in different ways,
// to surface the error in a more or less visible way. For instance, the macOS GUI could change its menubar
// icon to display an exclamation mark and present a modal notification for SeverityHigh warnings, but not
// for SeverityLow messages, which would only appear in the Settings window.
type Severity string
const (
// SeverityHigh is the highest severity level, used for critical errors that need immediate attention.
// On platforms where the client GUI can deliver notifications, a SeverityHigh Warnable will trigger
// a modal notification.
SeverityHigh Severity = "high"
// SeverityMedium is used for errors that are important but not critical. This won't trigger a modal
// notification, however it will be displayed in a more visible way than a SeverityLow Warnable.
SeverityMedium Severity = "medium"
// SeverityLow is used for less important notices that don't need immediate attention. The user will
// have to go to a Settings window, or another "hidden" GUI location to see these messages.
SeverityLow Severity = "low"
)
// Args is a map of Args to string values that can be used to provide parameters regarding
// the unhealthy state of a Warnable.
// For instance, if you have a Warnable to track the health of DNS lookups, here you can include
// the hostname that failed to resolve, or the IP address of the DNS server that has been failing
// to respond. You can then use these parameters in the Text function of the Warnable to provide a detailed
// error message to the user.
type Args map[Arg]string
// A warningState is a condition affecting a Warnable. For each Warnable known to the Tracker, a Warnable
// is in an unhappy state if there is a warningState associated with the Warnable.
type warningState struct {
BrokenSince time.Time // when the Warnable became unhealthy
Args Args // args can be used to provide parameters to the function that generates the Text in the Warnable
}
func (ws *warningState) Equal(other *warningState) bool {
if ws == nil && other == nil {
return true
}
if ws == nil || other == nil {
return false
}
return ws.BrokenSince.Equal(other.BrokenSince) && maps.Equal(ws.Args, other.Args)
}
// IsVisible returns whether the Warnable should be visible to the user, based on the TimeToVisible
// field of the Warnable and the BrokenSince time when the Warnable became unhealthy.
func (w *Warnable) IsVisible(ws *warningState, clockNow func() time.Time) bool {
if ws == nil || w.TimeToVisible == 0 {
return true
}
return clockNow().Sub(ws.BrokenSince) >= w.TimeToVisible
}
// IsUnhealthy reports whether the current state is unhealthy because the given
// warnable is set.
func (t *Tracker) IsUnhealthy(w *Warnable) bool {
if !buildfeatures.HasHealth || t.nil() {
return false
}
t.mu.Lock()
defer t.mu.Unlock()
_, exists := t.warnableVal[w]
return exists
}
// SetUnhealthy sets a warningState for the given Warnable with the provided Args, and should be
// called when a Warnable becomes unhealthy, or its unhealthy status needs to be updated.
// SetUnhealthy takes ownership of args. The args can be nil if no additional information is
// needed for the unhealthy state.
func (t *Tracker) SetUnhealthy(w *Warnable, args Args) {
if !buildfeatures.HasHealth || t.nil() {
return
}
t.mu.Lock()
defer t.mu.Unlock()
t.setUnhealthyLocked(w, args)
}
func (t *Tracker) setUnhealthyLocked(w *Warnable, args Args) {
if !buildfeatures.HasHealth || w == nil {
return
}
// If we already have a warningState for this Warnable with an earlier BrokenSince time, keep that
// BrokenSince time.
brokenSince := t.now()
if existingWS := t.warnableVal[w]; existingWS != nil {
brokenSince = existingWS.BrokenSince
}
if t.warnableVal[w] == nil {
t.warnables = append(t.warnables, w)
}
ws := &warningState{
BrokenSince: brokenSince,
Args: args,
}
prevWs := t.warnableVal[w]
mak.Set(&t.warnableVal, w, ws)
if !ws.Equal(prevWs) {
change := Change{
WarnableChanged: true,
Warnable: w,
UnhealthyState: w.unhealthyState(ws),
}
// Publish the change to the event bus. If the change is already visible
// now, publish it immediately; otherwise queue a timer to publish it at
// a future time when it becomes visible.
if w.IsVisible(ws, t.now) {
t.changePub.Publish(change)
} else {
visibleIn := w.TimeToVisible - t.now().Sub(brokenSince)
tc := t.clock().AfterFunc(visibleIn, func() {
t.mu.Lock()
defer t.mu.Unlock()
// Check if the Warnable is still unhealthy, as it could have become healthy between the time
// the timer was set for and the time it was executed.
if t.warnableVal[w] != nil {
t.changePub.Publish(change)
delete(t.pendingVisibleTimers, w)
}
})
mak.Set(&t.pendingVisibleTimers, w, tc)
}
}
}
// SetHealthy removes any warningState for the given Warnable.
func (t *Tracker) SetHealthy(w *Warnable) {
if !buildfeatures.HasHealth || t.nil() {
return
}
t.mu.Lock()
defer t.mu.Unlock()
t.setHealthyLocked(w)
}
func (t *Tracker) setHealthyLocked(w *Warnable) {
if !buildfeatures.HasHealth {
return
}
ws := t.warnableVal[w]
if ws == nil {
// Nothing to remove
return
}
delete(t.warnableVal, w)
// Stop any pending visiblity timers for this Warnable
if canc, ok := t.pendingVisibleTimers[w]; ok {
// We removed the warningState for this Warnable,
// and we hold the lock, so even if the timer callback
// has already started, it won't find a warningState
// for this Warnable and won't publish any changes.
canc.Stop()
delete(t.pendingVisibleTimers, w)
}
// Only publish a change if the Warnable was unhealthy long
// enough to become visible to the user. Otherwise, it would
// not have been published as unhealthy, so there is no need
// to publish it as healthy. This prevents eventbus (and by
// extension the IPN bus) churn for Warnables that are marked
// unhealthy and then healthy again. Notably, this includes
// warnables touched by [Tracker.updateBuiltinWarnablesLocked].
if w.IsVisible(ws, t.now) {
change := Change{
WarnableChanged: true,
Warnable: w,
}
t.changePub.Publish(change)
}
}
// notifyWatchersControlChangedLocked calls each watcher to signal that control
// health messages have changed (and should be fetched via CurrentState).
func (t *Tracker) notifyWatchersControlChangedLocked() {
change := Change{
ControlHealthChanged: true,
}
t.changePub.Publish(change)
}
// AppendWarnableDebugFlags appends to base any health items that are currently in failed
// state and were created with MapDebugFlag.
func (t *Tracker) AppendWarnableDebugFlags(base []string) []string {
if t.nil() {
return base
}
ret := base
t.mu.Lock()
defer t.mu.Unlock()
for w, err := range t.warnableVal {
if w.MapDebugFlag == "" {
continue
}
if err != nil {
ret = append(ret, w.MapDebugFlag)
}
}
sort.Strings(ret[len(base):]) // sort the new ones
return ret
}
// Change is used to communicate a change to health. This could either be due to
// a Warnable changing from health to unhealthy (or vice-versa), or because the
// health messages received from the control-plane have changed.
//
// Exactly one *Changed field will be true.
type Change struct {
// ControlHealthChanged indicates it was health messages from the
// control-plane server that changed.
ControlHealthChanged bool
// WarnableChanged indicates it was a client Warnable which changed state.
WarnableChanged bool
// Warnable is whose health changed, as indicated in UnhealthyState.
Warnable *Warnable
// UnhealthyState is set if the changed Warnable is now unhealthy, or nil
// if Warnable is now healthy.
UnhealthyState *UnhealthyState
}
// SetRouterHealth sets the state of the wgengine/router.Router.
//
// Deprecated: Warnables should be preferred over Subsystem errors.
func (t *Tracker) SetRouterHealth(err error) { t.setErr(SysRouter, err) }
// RouterHealth returns the wgengine/router.Router error state.
//
// Deprecated: Warnables should be preferred over Subsystem errors.
func (t *Tracker) RouterHealth() error { return t.get(SysRouter) }
// SetDNSHealth sets the state of the net/dns.Manager
//
// Deprecated: Warnables should be preferred over Subsystem errors.
func (t *Tracker) SetDNSHealth(err error) { t.setErr(SysDNS, err) }
// DNSHealth returns the net/dns.Manager error state.
//
// Deprecated: Warnables should be preferred over Subsystem errors.
func (t *Tracker) DNSHealth() error { return t.get(SysDNS) }
// SetDNSManagerHealth sets the state of the Linux net/dns manager's
// discovery of the /etc/resolv.conf situation.
//
// Deprecated: Warnables should be preferred over Subsystem errors.
func (t *Tracker) SetDNSManagerHealth(err error) { t.setErr(SysDNSManager, err) }
// SetTKAHealth sets the health of the tailnet key authority.
//
// Deprecated: Warnables should be preferred over Subsystem errors.
func (t *Tracker) SetTKAHealth(err error) { t.setErr(SysTKA, err) }
// TKAHealth returns the tailnet key authority error state.
//
// Deprecated: Warnables should be preferred over Subsystem errors.
func (t *Tracker) TKAHealth() error { return t.get(SysTKA) }
// SetLocalLogConfigHealth sets the error state of this client's local log configuration.
func (t *Tracker) SetLocalLogConfigHealth(err error) {
if t.nil() {
return
}
t.mu.Lock()
defer t.mu.Unlock()
t.localLogConfigErr = err
}
// SetTLSConnectionError sets the error state for connections to a specific
// host. Setting the error to nil will clear any previously-set error.
func (t *Tracker) SetTLSConnectionError(host string, err error) {
if t.nil() {
return
}
t.mu.Lock()
defer t.mu.Unlock()
if err == nil {
delete(t.tlsConnectionErrors, host)
} else {
mak.Set(&t.tlsConnectionErrors, host, err)
}
}
func RegisterDebugHandler(typ string, h http.Handler) {
mu.Lock()
defer mu.Unlock()
mak.Set(&debugHandler, typ, h)
}
func DebugHandler(typ string) http.Handler {
mu.Lock()
defer mu.Unlock()
return debugHandler[typ]
}
func (t *Tracker) get(key Subsystem) error {
if t.nil() {
return nil
}
t.mu.Lock()
defer t.mu.Unlock()
return t.sysErr[key]
}
func (t *Tracker) setErr(key Subsystem, err error) {
if t.nil() {
return
}
t.mu.Lock()
defer t.mu.Unlock()
t.setLocked(key, err)
}
func (t *Tracker) setLocked(key Subsystem, err error) {
if t.sysErr == nil {
t.sysErr = map[Subsystem]error{}
}
old, ok := t.sysErr[key]
if !ok && err == nil {
// Initial happy path.
t.sysErr[key] = nil
t.selfCheckLocked()
return
}
if ok && (old == nil) == (err == nil) {
// No change in overall error status (nil-vs-not), so
// don't run callbacks, but exact error might've
// changed, so note it.
if err != nil {
t.sysErr[key] = err
}
return
}
t.sysErr[key] = err
t.selfCheckLocked()
}
// updateLegacyErrorWarnableLocked takes a legacy Subsystem and an optional error, and
// updates the WarningState for that legacy Subsystem, setting it to healthy or unhealthy.
// It is used temporarily while we migrate from Subsystems to Warnables.
//
// Deprecated: this function will be removed after migrating all subsystem errors to use
// Warnables instead.
func (t *Tracker) updateLegacyErrorWarnableLocked(key Subsystem, err error) {
w := key.Warnable()
if err != nil {
t.setUnhealthyLocked(key.Warnable(), Args{legacyErrorArgKey: err.Error()})
} else {
t.setHealthyLocked(w)
}
}
func (t *Tracker) SetControlHealth(problems map[tailcfg.DisplayMessageID]tailcfg.DisplayMessage) {
if t.nil() {
return
}
t.mu.Lock()
defer t.mu.Unlock()
t.controlMessages = problems
t.selfCheckLocked()
}
// GotStreamedMapResponse notes that we got a tailcfg.MapResponse
// message in streaming mode, even if it's just a keep-alive message.
//
// This also notes that a map poll is in progress. To unset that, call
// SetOutOfPollNetMap().
func (t *Tracker) GotStreamedMapResponse() {
if t.nil() {
return
}
t.mu.Lock()
defer t.mu.Unlock()
t.lastStreamedMapResponse = t.now()
if !t.inMapPoll {
t.inMapPoll = true
t.inMapPollSince = t.now()
}
t.selfCheckLocked()
}
// SetOutOfPollNetMap records that the client is no longer in
// an HTTP map request long poll to the control plane.
func (t *Tracker) SetOutOfPollNetMap() {
if t.nil() {
return
}
t.mu.Lock()
defer t.mu.Unlock()
if !t.inMapPoll {
return
}
t.inMapPoll = false
t.lastMapPollEndedAt = t.now()
t.selfCheckLocked()
}
// GetInPollNetMap reports whether the client has an open
// HTTP long poll open to the control plane.
func (t *Tracker) GetInPollNetMap() bool {
if t.nil() {
return false
}
t.mu.Lock()
defer t.mu.Unlock()
return t.inMapPoll
}
// SetMagicSockDERPHome notes what magicsock's view of its home DERP is.
//
// The homeless parameter is whether magicsock is running in DERP-disconnected
// mode, without discovering and maintaining a connection to its home DERP.
func (t *Tracker) SetMagicSockDERPHome(region tailcfg.DERPRegionID, homeless bool) {
if t.nil() {
return
}
t.mu.Lock()
defer t.mu.Unlock()
t.derpHomeRegion = region
t.derpHomeless = homeless
t.selfCheckLocked()
}
// NoteMapRequestHeard notes whenever we successfully sent a map request
// to control for which we received a 200 response.
func (t *Tracker) NoteMapRequestHeard(mr *tailcfg.MapRequest) {
if t.nil() {
return
}
t.mu.Lock()
defer t.mu.Unlock()
// TODO: extract mr.HostInfo.NetInfo.PreferredDERP, compare
// against SetMagicSockDERPHome and
// SetDERPRegionConnectedState
t.lastMapRequestHeard = t.now()
t.selfCheckLocked()
}
func (t *Tracker) SetDERPRegionConnectedState(region tailcfg.DERPRegionID, connected bool) {
if t.nil() {
return
}
t.mu.Lock()
defer t.mu.Unlock()
mak.Set(&t.derpRegionConnected, region, connected)
t.selfCheckLocked()
}
// SetDERPRegionHealth sets or clears any problem associated with the
// provided DERP region.
func (t *Tracker) SetDERPRegionHealth(region tailcfg.DERPRegionID, problem string) {
if t.nil() {
return
}
t.mu.Lock()
defer t.mu.Unlock()
if problem == "" {
delete(t.derpRegionHealthProblem, region)
} else {
mak.Set(&t.derpRegionHealthProblem, region, problem)
}
t.selfCheckLocked()
}
// NoteDERPRegionReceivedFrame is called to note that a frame was received from
// the given DERP region at the current time.
func (t *Tracker) NoteDERPRegionReceivedFrame(region tailcfg.DERPRegionID) {
if t.nil() {
return
}
t.mu.Lock()
defer t.mu.Unlock()
mak.Set(&t.derpRegionLastFrame, region, t.now())
t.selfCheckLocked()
}
// GetDERPRegionReceivedTime returns the last time that a frame was received
// from the given DERP region, or the zero time if no communication with that
// region has occurred.
func (t *Tracker) GetDERPRegionReceivedTime(region tailcfg.DERPRegionID) time.Time {
if t.nil() {
return time.Time{}
}
t.mu.Lock()
defer t.mu.Unlock()
return t.derpRegionLastFrame[region]
}
// SetDERPMap sets the last fetched DERP map in the Tracker. The DERP map is used
// to provide a region name in user-facing DERP-related warnings.
func (t *Tracker) SetDERPMap(dm *tailcfg.DERPMap) {
if t.nil() {
return
}
t.mu.Lock()
defer t.mu.Unlock()
t.derpMap = dm
t.selfCheckLocked()
}
// derpRegionNameLocked returns the name of the DERP region with the given ID
// or the empty string if unknown.
func (t *Tracker) derpRegionNameLocked(regID tailcfg.DERPRegionID) string {
if t.derpMap == nil {
return ""
}
if r, ok := t.derpMap.Regions[regID]; ok {
return r.RegionName
}
return ""
}
// state is an ipn.State.String() value: "Running", "Stopped", "NeedsLogin", etc.
func (t *Tracker) SetIPNState(state string, wantRunning bool) {
if t.nil() {
return
}
t.mu.Lock()
defer t.mu.Unlock()
t.ipnState = state
prevWantRunning := t.ipnWantRunning
t.ipnWantRunning = wantRunning
if state == "Running" {
// Any time we are told the backend is Running (control+DERP are connected), the Warnable
// should be set to healthy, no matter if 5 seconds have passed or not.
t.setHealthyLocked(warmingUpWarnable)
} else if wantRunning && !prevWantRunning && t.ipnWantRunningLastTrue.IsZero() {
// The first time we see wantRunning=true and it used to be false, it means the user requested
// the backend to start. We store this timestamp and use it to silence some warnings that are
// expected during startup.
t.ipnWantRunningLastTrue = t.now()
t.setUnhealthyLocked(warmingUpWarnable, nil)
t.clock().AfterFunc(warmingUpWarnableDuration, func() {
t.mu.Lock()
t.updateWarmingUpWarnableLocked()
t.mu.Unlock()
})
} else if !wantRunning {
// Reset the timer when the user decides to stop the backend.
t.ipnWantRunningLastTrue = time.Time{}
}
t.selfCheckLocked()
}
// SetAnyInterfaceUp sets whether any network interface is up.
func (t *Tracker) SetAnyInterfaceUp(up bool) {
if t.nil() {
return
}
t.mu.Lock()
defer t.mu.Unlock()
t.anyInterfaceUp.Set(up)
t.selfCheckLocked()
}
// SetUDP4Unbound sets whether the udp4 bind failed completely.
func (t *Tracker) SetUDP4Unbound(unbound bool) {
if t.nil() {
return
}
t.mu.Lock()
defer t.mu.Unlock()
if unbound {
t.setUnhealthyLocked(noUDP4BindWarnable, nil)
} else {
t.setHealthyLocked(noUDP4BindWarnable)
}
}
// SetAuthRoutineInError records the latest error encountered as a result of a
// login attempt. Providing a nil error indicates successful login, or that
// being logged in w/coordination is not currently desired.
func (t *Tracker) SetAuthRoutineInError(err error) {
if t.nil() {
return
}
t.mu.Lock()
defer t.mu.Unlock()
if err == nil && t.lastLoginErr == nil {
return
}
t.lastLoginErr = err
t.selfCheckLocked()
}
// SetLatestVersion records the latest version of the Tailscale client.
// v can be nil if unknown.
func (t *Tracker) SetLatestVersion(v *tailcfg.ClientVersion) {
if t.nil() {
return
}
t.mu.Lock()
defer t.mu.Unlock()
t.latestVersion = v
t.selfCheckLocked()
}
// SetAutoUpdatePrefs sets the client auto-update preferences. The arguments
// match the fields of ipn.AutoUpdatePrefs, but we cannot pass that struct
// directly due to a circular import.
func (t *Tracker) SetAutoUpdatePrefs(check bool, apply opt.Bool) {
if t.nil() {
return
}
t.mu.Lock()
defer t.mu.Unlock()
if t.checkForUpdates == check && t.applyUpdates == apply {
return
}
t.checkForUpdates = check
t.applyUpdates = apply
t.selfCheckLocked()
}
func (t *Tracker) timerSelfCheck() {
if t.nil() {
return
}
t.initOnce.Do(t.doOnceInit)
t.mu.Lock()
defer t.mu.Unlock()
t.checkReceiveFuncsLocked()
t.selfCheckLocked()
if t.timer != nil {
t.timer.Reset(time.Minute)
}
}
func (t *Tracker) selfCheckLocked() {
if t.ipnState == "" {
// Don't check yet.
return
}
t.updateBuiltinWarnablesLocked()
}
// OverallError returns a summary of the health state.
//
// If there are multiple problems, the error will be joined using
// [errors.Join].
func (t *Tracker) OverallError() error {
if t.nil() {
return nil
}
t.mu.Lock()
defer t.mu.Unlock()
t.updateBuiltinWarnablesLocked()
return t.multiErrLocked()
}
// Strings() returns a string array containing the Text of all Warnings and
// ControlHealth messages currently known to the Tracker. These strings can be
// presented to the user, although ideally you would use the Code property on
// each Warning to show a localized version of them instead. This function is
// here for legacy compatibility purposes and is deprecated.
func (t *Tracker) Strings() []string {
if !buildfeatures.HasHealth || t.nil() {
return nil
}
t.mu.Lock()
defer t.mu.Unlock()
return t.stringsLocked()
}
func (t *Tracker) stringsLocked() []string {
if !buildfeatures.HasHealth {
return nil
}
result := []string{}
for w, ws := range t.warnableVal {
if !w.IsVisible(ws, t.now) {
// Do not append invisible warnings.
continue
}
if t.isEffectivelyHealthyLocked(w) {
continue
}
if ws.Args == nil {
result = append(result, w.Text(Args{}))
} else {
result = append(result, w.Text(ws.Args))
}
}
warnLen := len(result)
for _, c := range t.controlMessages {
var msg string
if c.Title != "" && c.Text != "" {
msg = c.Title + ": " + c.Text
} else if c.Title != "" {
msg = c.Title + "."
} else if c.Text != "" {
msg = c.Text
}
if c.PrimaryAction != nil {
msg = msg + " " + c.PrimaryAction.Label + ": " + c.PrimaryAction.URL
}
result = append(result, msg)
}
sort.Strings(result[warnLen:])
return result
}
// errorsLocked returns an array of errors where each error is the Text
// of a Warning known to the Tracker.
// This function is here for legacy compatibility purposes and is deprecated.
func (t *Tracker) errorsLocked() []error {
strs := t.stringsLocked()
errs := []error{}
for _, str := range strs {
errs = append(errs, errors.New(str))
}
return errs
}
// multiErrLocked returns an error listing all errors known to the Tracker.
// This function is here for legacy compatibility purposes and is deprecated.
func (t *Tracker) multiErrLocked() error {
errs := t.errorsLocked()
return errors.Join(errs...)
}
var fakeErrForTesting = envknob.RegisterString("TS_DEBUG_FAKE_HEALTH_ERROR")
// updateBuiltinWarnablesLocked performs a number of checks on the state of the backend,
// and adds/removes Warnings from the Tracker as needed.
func (t *Tracker) updateBuiltinWarnablesLocked() {
if !buildfeatures.HasHealth {
return
}
t.updateWarmingUpWarnableLocked()
if w, show := t.showUpdateWarnable(); show {
t.setUnhealthyLocked(w, Args{
ArgCurrentVersion: version.Short(),
ArgAvailableVersion: t.latestVersion.LatestVersion,
})
} else {
t.setHealthyLocked(updateAvailableWarnable)
t.setHealthyLocked(securityUpdateAvailableWarnable)
}
if version.IsUnstableBuild() {
t.setUnhealthyLocked(unstableWarnable, Args{
ArgCurrentVersion: version.Short(),
})
}
if v, ok := t.anyInterfaceUp.Get(); ok && !v {
t.setUnhealthyLocked(NetworkStatusWarnable, nil)
} else {
t.setHealthyLocked(NetworkStatusWarnable)
}
t.updateIPForwardingWarnableLocked()
if t.localLogConfigErr != nil {
t.setUnhealthyLocked(localLogWarnable, Args{
ArgError: t.localLogConfigErr.Error(),
})
} else {
t.setHealthyLocked(localLogWarnable)
}
now := t.now()
// How long we assume we'll have heard a DERP frame or a MapResponse
// KeepAlive by.
const tooIdle = 2*time.Minute + 5*time.Second
// Whether user recently turned on Tailscale.
recentlyOn := now.Sub(t.ipnWantRunningLastTrue) < 5*time.Second
homeDERP := t.derpHomeRegion
if recentlyOn || !t.inMapPoll {
// If user just turned Tailscale on, don't warn for a bit.
// Also, if we're not in a map poll, that means we don't yet
// have a DERPMap or aren't in a state where we even want
t.setHealthyLocked(noDERPHomeWarnable)
t.setHealthyLocked(noDERPConnectionWarnable)
t.setHealthyLocked(derpTimeoutWarnable)
} else if !t.ipnWantRunning || t.derpHomeless || homeDERP != 0 {
t.setHealthyLocked(noDERPHomeWarnable)
} else {
t.setUnhealthyLocked(noDERPHomeWarnable, nil)
}
if homeDERP != 0 && t.derpRegionConnected[homeDERP] {
t.setHealthyLocked(noDERPConnectionWarnable)
if d := now.Sub(t.derpRegionLastFrame[homeDERP]); d < tooIdle {
t.setHealthyLocked(derpTimeoutWarnable)
} else {
t.setUnhealthyLocked(derpTimeoutWarnable, Args{
ArgDERPRegionID: fmt.Sprint(homeDERP),
ArgDERPRegionName: t.derpRegionNameLocked(homeDERP),
ArgDuration: d.Round(time.Second).String(),
})
}
} else if homeDERP != 0 {
t.setUnhealthyLocked(noDERPConnectionWarnable, Args{
ArgDERPRegionID: fmt.Sprint(homeDERP),
ArgDERPRegionName: t.derpRegionNameLocked(homeDERP),
})
} else {
// No DERP home yet determined yet. There's probably some
// other problem or things are just starting up.
t.setHealthyLocked(noDERPConnectionWarnable)
}
if !t.ipnWantRunning {
t.setUnhealthyLocked(IPNStateWarnable, Args{
"State": t.ipnState,
})
return
} else {
t.setHealthyLocked(IPNStateWarnable)
}
if t.lastLoginErr != nil {
var errMsg string
if !errors.Is(t.lastLoginErr, context.Canceled) {
errMsg = t.lastLoginErr.Error()
}
t.setUnhealthyLocked(LoginStateWarnable, Args{
ArgError: errMsg,
})
return
} else {
t.setHealthyLocked(LoginStateWarnable)
}
if !t.inMapPoll && (t.lastMapPollEndedAt.IsZero() || now.Sub(t.lastMapPollEndedAt) > 10*time.Second) {
t.setUnhealthyLocked(notInMapPollWarnable, nil)
return
} else {
t.setHealthyLocked(notInMapPollWarnable)
}
if d := now.Sub(t.lastStreamedMapResponse).Round(time.Second); d > tooIdle {
t.setUnhealthyLocked(mapResponseTimeoutWarnable, Args{
ArgDuration: d.String(),
})
return
} else {
t.setHealthyLocked(mapResponseTimeoutWarnable)
}
// TODO: use
_ = t.inMapPollSince
_ = t.lastMapPollEndedAt
_ = t.lastStreamedMapResponse
_ = t.lastMapRequestHeard
shouldClearMagicsockWarnings := true
for i := range t.MagicSockReceiveFuncs {
f := &t.MagicSockReceiveFuncs[i]
if f.missing {
t.setUnhealthyLocked(magicsockReceiveFuncWarnable, Args{
ArgMagicsockFunctionName: f.name,
})
shouldClearMagicsockWarnings = false
break
}
}
if shouldClearMagicsockWarnings {
t.setHealthyLocked(magicsockReceiveFuncWarnable)
}
// Iterates over the legacy subsystems and their error, and turns them into structured errors
for sys, err := range t.sysErr {
t.updateLegacyErrorWarnableLocked(sys, err)
}
if len(t.derpRegionHealthProblem) > 0 {
for regionID, problem := range t.derpRegionHealthProblem {
t.setUnhealthyLocked(derpRegionErrorWarnable, Args{
ArgDERPRegionID: fmt.Sprint(regionID),
ArgError: problem,
})
}
} else {
t.setHealthyLocked(derpRegionErrorWarnable)
}
// Check if control health messages have changed
if !maps.EqualFunc(t.lastNotifiedControlMessages, t.controlMessages, tailcfg.DisplayMessage.Equal) {
t.lastNotifiedControlMessages = t.controlMessages
t.notifyWatchersControlChangedLocked()
}
if err := envknob.ApplyDiskConfigError(); err != nil {
t.setUnhealthyLocked(applyDiskConfigWarnable, Args{
ArgError: err.Error(),
})
} else {
t.setHealthyLocked(applyDiskConfigWarnable)
}
if len(t.tlsConnectionErrors) > 0 {
for serverName, err := range t.tlsConnectionErrors {
t.setUnhealthyLocked(tlsConnectionFailedWarnable, Args{
ArgServerName: serverName,
ArgError: err.Error(),
})
}
} else {
t.setHealthyLocked(tlsConnectionFailedWarnable)
}
if e := fakeErrForTesting(); len(t.warnables) == 0 && e != "" {
t.setUnhealthyLocked(testWarnable, Args{
ArgError: e,
})
} else {
t.setHealthyLocked(testWarnable)
}
}
// updateWarmingUpWarnableLocked ensures the warmingUpWarnable is healthy if wantRunning has been set to true
// for more than warmingUpWarnableDuration.
func (t *Tracker) updateWarmingUpWarnableLocked() {
if !t.ipnWantRunningLastTrue.IsZero() && t.now().After(t.ipnWantRunningLastTrue.Add(warmingUpWarnableDuration)) {
t.setHealthyLocked(warmingUpWarnable)
}
}
func (t *Tracker) showUpdateWarnable() (*Warnable, bool) {
if !t.checkForUpdates {
return nil, false
}
cv := t.latestVersion
if cv == nil || cv.RunningLatest || cv.LatestVersion == "" {
return nil, false
}
if cv.UrgentSecurityUpdate {
return securityUpdateAvailableWarnable, true
}
// Only show update warning when auto-updates are off
if !t.applyUpdates.EqualBool(true) {
return updateAvailableWarnable, true
}
return nil, false
}
// ReceiveFuncStats tracks the calls made to a wireguard-go receive func.
type ReceiveFuncStats struct {
// name is the name of the receive func.
// It's lazily populated.
name string
// numCalls is the number of times the receive func has ever been called.
// It is required because it is possible for a receive func's wireguard-go goroutine
// to be active even though the receive func isn't.
// The wireguard-go goroutine alternates between calling the receive func and
// processing what the func returned.
numCalls atomic.Uint64
// prevNumCalls is the value of numCalls last time the health check examined it.
prevNumCalls uint64
// inCall indicates whether the receive func is currently running.
inCall atomic.Bool
// missing indicates whether the receive func is not running.
missing bool
}
// Name returns the name of the receive func ("ReceiveIPv4", "ReceiveIPv6", etc).
func (s *ReceiveFuncStats) Name() string {
return s.name
}
func (s *ReceiveFuncStats) Enter() {
if !buildfeatures.HasHealth {
return
}
s.numCalls.Add(1)
s.inCall.Store(true)
}
func (s *ReceiveFuncStats) Exit() {
if !buildfeatures.HasHealth {
return
}
s.inCall.Store(false)
}
// ReceiveFuncStats returns the ReceiveFuncStats tracker for the given func
// type.
//
// If t is nil, it returns nil.
func (t *Tracker) ReceiveFuncStats(which ReceiveFunc) *ReceiveFuncStats {
if !buildfeatures.HasHealth || t == nil {
return nil
}
t.initOnce.Do(t.doOnceInit)
return &t.MagicSockReceiveFuncs[which]
}
func (t *Tracker) doOnceInit() {
if !buildfeatures.HasHealth {
return
}
for i := range t.MagicSockReceiveFuncs {
f := &t.MagicSockReceiveFuncs[i]
f.name = (ReceiveFunc(i)).String()
}
}
func (t *Tracker) checkReceiveFuncsLocked() {
for i := range t.MagicSockReceiveFuncs {
f := &t.MagicSockReceiveFuncs[i]
if runtime.GOOS == "js" && i < 2 {
// Skip IPv4 and IPv6 on js.
continue
}
f.missing = false
prev := f.prevNumCalls
numCalls := f.numCalls.Load()
f.prevNumCalls = numCalls
if numCalls > prev {
// OK: the function has gotten called since last we checked
continue
}
if f.inCall.Load() {
// OK: the function is active, probably blocked due to inactivity
continue
}
// Not OK: The function is not active, and not accumulating new calls.
// It is probably MIA.
f.missing = true
}
}
// LastNoiseDialWasRecent notes that we're attempting to dial control via the
// ts2021 noise protocol and reports whether the prior dial was "recent"
// (currently defined as 2 minutes but subject to change).
//
// If t is nil, it reports false.
func (t *Tracker) LastNoiseDialWasRecent() bool {
if t.nil() {
return false
}
t.mu.Lock()
defer t.mu.Unlock()
now := t.now()
dur := now.Sub(t.lastNoiseDial)
t.lastNoiseDial = now
return dur < 2*time.Minute
}
// SetIPForwardingCheck sets the function to check if IP forwarding is broken.
// The function should return true if IP forwarding is broken, false if healthy.
// Pass nil to disable IP forwarding checks.
func (t *Tracker) SetIPForwardingCheck(checkFunc func() bool) {
if t.nil() {
return
}
t.mu.Lock()
defer t.mu.Unlock()
t.isIPForwardingBroken = checkFunc
// Run an immediate check to set initial state
t.updateIPForwardingWarnableLocked()
}
// updateIPForwardingWarnableLocked checks the IP forwarding state and
// sets or clears the ipForwardingWarnable accordingly.
func (t *Tracker) updateIPForwardingWarnableLocked() {
if t.isIPForwardingBroken != nil && t.isIPForwardingBroken() {
t.setUnhealthyLocked(ipForwardingWarnable, Args{})
} else {
t.setHealthyLocked(ipForwardingWarnable)
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package health
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"time"
"tailscale.com/feature/buildfeatures"
"tailscale.com/tailcfg"
"tailscale.com/util/mak"
)
// State contains the health status of the backend, and is
// provided to the client UI via LocalAPI through ipn.Notify.
//
// It is also exposed via c2n for debugging purposes, so try
// not to change its structure too gratuitously.
type State struct {
// Each key-value pair in Warnings represents a Warnable that is currently
// unhealthy. If a Warnable is healthy, it will not be present in this map.
// When a Warnable is unhealthy and becomes healthy, its key-value pair
// disappears in the next issued State. Observers should treat the absence of
// a WarnableCode in this map as an indication that the Warnable became healthy,
// and may use that to clear any notifications that were previously shown to the user.
// If Warnings is nil, all Warnables are healthy and the backend is overall healthy.
Warnings map[WarnableCode]UnhealthyState
}
// UnhealthyState contains information to be shown to the user to inform them
// that a [Warnable] is currently unhealthy or [tailcfg.DisplayMessage] is being
// sent from the control-plane.
type UnhealthyState struct {
WarnableCode WarnableCode
Severity Severity
Title string
Text string
BrokenSince *time.Time `json:",omitempty"`
Args Args `json:",omitempty"`
DependsOn []WarnableCode `json:",omitempty"`
ImpactsConnectivity bool `json:",omitempty"`
PrimaryAction *UnhealthyStateAction `json:",omitempty"`
// ETag identifies a specific version of an UnhealthyState. If the contents
// of the other fields of two UnhealthyStates are the same, the ETags will
// be the same. If the contents differ, the ETags will also differ. The
// implementation is not defined and the value is opaque: it might be a
// hash, it might be a simple counter. Implementations should not rely on
// any specific implementation detail or format of the ETag string other
// than string (in)equality.
ETag string `json:",omitzero"`
}
// hash computes a deep hash of UnhealthyState which will be stable across
// different runs of the same binary.
func (u UnhealthyState) hash() []byte {
hasher := sha256.New()
enc := json.NewEncoder(hasher)
// hash.Hash.Write never returns an error, so this will only fail if u is
// not marshalable, in which case we have much bigger problems.
_ = enc.Encode(u)
return hasher.Sum(nil)
}
// withETag returns a copy of UnhealthyState with an ETag set. The ETag will be
// the same for all UnhealthyState instances that are equal. If calculating the
// ETag errors, it returns a copy of the UnhealthyState with an empty ETag.
func (u UnhealthyState) withETag() UnhealthyState {
u.ETag = ""
u.ETag = hex.EncodeToString(u.hash())
return u
}
// UnhealthyStateAction represents an action (URL and link) to be presented to
// the user associated with an [UnhealthyState]. Analogous to
// [tailcfg.DisplayMessageAction].
type UnhealthyStateAction struct {
URL string
Label string
}
// unhealthyState returns a unhealthyState of the Warnable given its current warningState.
func (w *Warnable) unhealthyState(ws *warningState) *UnhealthyState {
var text string
if ws.Args != nil {
text = w.Text(ws.Args)
} else {
text = w.Text(Args{})
}
dependsOnWarnableCodes := make([]WarnableCode, len(w.DependsOn), len(w.DependsOn)+1)
for i, d := range w.DependsOn {
dependsOnWarnableCodes[i] = d.Code
}
if w != warmingUpWarnable {
// Here we tell the frontend that all Warnables depend on warmingUpWarnable. GUIs will silence all warnings until all
// their dependencies are healthy. This is a special case to prevent the GUI from showing a bunch of warnings when
// the backend is still warming up.
dependsOnWarnableCodes = append(dependsOnWarnableCodes, warmingUpWarnable.Code)
}
return &UnhealthyState{
WarnableCode: w.Code,
Severity: w.Severity,
Title: w.Title,
Text: text,
BrokenSince: &ws.BrokenSince,
Args: ws.Args,
DependsOn: dependsOnWarnableCodes,
ImpactsConnectivity: w.ImpactsConnectivity,
}
}
// CurrentState returns a snapshot of the current health status of the backend.
// It returns a State with nil Warnings if the backend is healthy (all Warnables
// have no issues).
// The returned State is a snapshot of shared memory, and the caller should not
// mutate the returned value.
func (t *Tracker) CurrentState() *State {
if !buildfeatures.HasHealth || t.nil() {
return &State{}
}
t.mu.Lock()
defer t.mu.Unlock()
var wm map[WarnableCode]UnhealthyState
for w, ws := range t.warnableVal {
if !w.IsVisible(ws, t.now) {
// Skip invisible Warnables.
continue
}
if t.isEffectivelyHealthyLocked(w) {
// Skip Warnables that are unhealthy if they have dependencies
// that are unhealthy.
continue
}
state := w.unhealthyState(ws)
mak.Set(&wm, w.Code, state.withETag())
}
for id, msg := range t.lastNotifiedControlMessages {
state := UnhealthyState{
WarnableCode: WarnableCode("control-health." + id),
Severity: severityFromTailcfg(msg.Severity),
Title: msg.Title,
Text: msg.Text,
ImpactsConnectivity: msg.ImpactsConnectivity,
// TODO(tailscale/corp#27759): DependsOn?
}
if msg.PrimaryAction != nil {
state.PrimaryAction = &UnhealthyStateAction{
URL: msg.PrimaryAction.URL,
Label: msg.PrimaryAction.Label,
}
}
mak.Set(&wm, state.WarnableCode, state.withETag())
}
return &State{
Warnings: wm,
}
}
func severityFromTailcfg(s tailcfg.DisplayMessageSeverity) Severity {
switch s {
case tailcfg.SeverityHigh:
return SeverityHigh
case tailcfg.SeverityLow:
return SeverityLow
default:
return SeverityMedium
}
}
// isEffectivelyHealthyLocked reports whether w is effectively healthy.
// That means it's either actually healthy or it has a dependency that
// that's unhealthy, so we should treat w as healthy to not spam users
// with multiple warnings when only the root cause is relevant.
func (t *Tracker) isEffectivelyHealthyLocked(w *Warnable) bool {
if _, ok := t.warnableVal[w]; !ok {
// Warnable not found in the tracker. So healthy.
return true
}
for _, d := range w.DependsOn {
if !t.isEffectivelyHealthyLocked(d) {
// If one of our deps is unhealthy, we're healthy.
return true
}
}
// If we have no unhealthy deps and had warnableVal set,
// we're unhealthy.
return false
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_health && !ts_omit_usermetrics
package health
import (
"expvar"
"tailscale.com/feature/buildfeatures"
"tailscale.com/util/usermetric"
)
const MetricLabelWarning = "warning"
type metricHealthMessageLabel struct {
// TODO: break down by warnable.severity as well?
Type string
}
// SetMetricsRegistry sets up the metrics for the Tracker. It takes
// a usermetric.Registry and registers the metrics there.
func (t *Tracker) SetMetricsRegistry(reg *usermetric.Registry) {
if !buildfeatures.HasHealth {
return
}
if reg == nil || t.metricHealthMessage != nil {
return
}
m := usermetric.NewMultiLabelMapWithRegistry[metricHealthMessageLabel](
reg,
"tailscaled_health_messages",
"gauge",
"Number of health messages broken down by type.",
)
m.Set(metricHealthMessageLabel{
Type: MetricLabelWarning,
}, expvar.Func(func() any {
if t.nil() {
return 0
}
t.mu.Lock()
defer t.mu.Unlock()
t.updateBuiltinWarnablesLocked()
return int64(len(t.stringsLocked()))
}))
t.metricHealthMessage = m
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package health
import (
"fmt"
"runtime"
"time"
"tailscale.com/feature/buildfeatures"
"tailscale.com/tsconst"
"tailscale.com/version"
)
func condRegister(f func() *Warnable) *Warnable {
if !buildfeatures.HasHealth {
return nil
}
return f()
}
/**
This file contains definitions for the Warnables maintained within this `health` package.
*/
// updateAvailableWarnable is a Warnable that warns the user that an update is available.
var updateAvailableWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: tsconst.HealthWarnableUpdateAvailable,
Title: "Update available",
Severity: SeverityLow,
Text: func(args Args) string {
if version.IsMacAppStore() || version.IsAppleTV() || version.IsMacSys() || version.IsWindowsGUI() || runtime.GOOS == "android" {
return fmt.Sprintf("An update from version %s to %s is available.", args[ArgCurrentVersion], args[ArgAvailableVersion])
} else {
return fmt.Sprintf("An update from version %s to %s is available. Run `tailscale update` or `tailscale set --auto-update` to update now.", args[ArgCurrentVersion], args[ArgAvailableVersion])
}
},
}
})
// securityUpdateAvailableWarnable is a Warnable that warns the user that an important security update is available.
var securityUpdateAvailableWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: tsconst.HealthWarnableSecurityUpdateAvailable,
Title: "Security update available",
Severity: SeverityMedium,
Text: func(args Args) string {
if version.IsMacAppStore() || version.IsAppleTV() || version.IsMacSys() || version.IsWindowsGUI() || runtime.GOOS == "android" {
return fmt.Sprintf("A security update from version %s to %s is available.", args[ArgCurrentVersion], args[ArgAvailableVersion])
} else {
return fmt.Sprintf("A security update from version %s to %s is available. Run `tailscale update` or `tailscale set --auto-update` to update now.", args[ArgCurrentVersion], args[ArgAvailableVersion])
}
},
}
})
// unstableWarnable is a Warnable that warns the user that they are using an unstable version of Tailscale
// so they won't be surprised by all the issues that may arise.
var unstableWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: tsconst.HealthWarnableIsUsingUnstableVersion,
Title: "Using an unstable version",
Severity: SeverityLow,
Text: StaticMessage("This is an unstable version of Tailscale meant for testing and development purposes. Please report any issues to Tailscale."),
}
})
// NetworkStatusWarnable is a Warnable that warns the user that the network is down.
var NetworkStatusWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: tsconst.HealthWarnableNetworkStatus,
Title: "Network down",
Severity: SeverityMedium,
Text: StaticMessage("Tailscale cannot connect because the network is down. Check your Internet connection."),
ImpactsConnectivity: true,
TimeToVisible: 5 * time.Second,
}
})
// IPNStateWarnable is a Warnable that warns the user that Tailscale is stopped.
var IPNStateWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: tsconst.HealthWarnableWantRunningFalse,
Title: "Tailscale off",
Severity: SeverityLow,
Text: StaticMessage("Tailscale is stopped."),
}
})
// localLogWarnable is a Warnable that warns the user that the local log is misconfigured.
var localLogWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: tsconst.HealthWarnableLocalLogConfigError,
Title: "Local log misconfiguration",
Severity: SeverityLow,
Text: func(args Args) string {
return fmt.Sprintf("The local log is misconfigured: %v", args[ArgError])
},
}
})
// LoginStateWarnable is a Warnable that warns the user that they are logged out,
// and provides the last login error if available.
var LoginStateWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: tsconst.HealthWarnableLoginState,
Title: "Logged out",
Severity: SeverityMedium,
Text: func(args Args) string {
if args[ArgError] != "" {
return fmt.Sprintf("You are logged out. The last login error was: %v", args[ArgError])
} else {
return "You are logged out."
}
},
DependsOn: []*Warnable{IPNStateWarnable},
}
})
// notInMapPollWarnable is a Warnable that warns the user that we are using a stale network map.
var notInMapPollWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: tsconst.HealthWarnableNotInMapPoll,
Title: "Out of sync",
Severity: SeverityMedium,
DependsOn: []*Warnable{NetworkStatusWarnable, IPNStateWarnable},
Text: StaticMessage("Unable to connect to the Tailscale coordination server to synchronize the state of your tailnet. Peer reachability might degrade over time."),
// 8 minutes reflects a maximum maintenance window for the coordination server.
TimeToVisible: 8 * time.Minute,
}
})
// noDERPHomeWarnable is a Warnable that warns the user that Tailscale doesn't have a home DERP.
var noDERPHomeWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: tsconst.HealthWarnableNoDERPHome,
Title: "No home relay server",
Severity: SeverityMedium,
DependsOn: []*Warnable{NetworkStatusWarnable},
Text: StaticMessage("Tailscale could not connect to any relay server. Check your Internet connection."),
ImpactsConnectivity: true,
TimeToVisible: 10 * time.Second,
}
})
// noDERPConnectionWarnable is a Warnable that warns the user that Tailscale couldn't connect to a specific DERP server.
var noDERPConnectionWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: tsconst.HealthWarnableNoDERPConnection,
Title: "Relay server unavailable",
Severity: SeverityMedium,
DependsOn: []*Warnable{
NetworkStatusWarnable,
// Technically noDERPConnectionWarnable could be used to warn about
// failure to connect to a specific DERP server (e.g. your home is derp1
// but you're trying to connect to a peer's derp4 and are unable) but as
// of 2024-09-25 we only use this for connecting to your home DERP, so
// we depend on noDERPHomeWarnable which is the ability to figure out
// what your DERP home even is.
noDERPHomeWarnable,
},
Text: func(args Args) string {
if n := args[ArgDERPRegionName]; n != "" {
return fmt.Sprintf("Tailscale could not connect to the '%s' relay server. Your Internet connection might be down, or the server might be temporarily unavailable.", n)
} else {
return fmt.Sprintf("Tailscale could not connect to the relay server with ID '%s'. Your Internet connection might be down, or the server might be temporarily unavailable.", args[ArgDERPRegionID])
}
},
ImpactsConnectivity: true,
TimeToVisible: 10 * time.Second,
}
})
// derpTimeoutWarnable is a Warnable that warns the user that Tailscale hasn't
// heard from the home DERP region for a while.
var derpTimeoutWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: tsconst.HealthWarnableDERPTimedOut,
Title: "Relay server timed out",
Severity: SeverityMedium,
DependsOn: []*Warnable{
NetworkStatusWarnable,
noDERPConnectionWarnable, // don't warn about it being stalled if we're not connected
noDERPHomeWarnable, // same reason as noDERPConnectionWarnable's dependency
},
Text: func(args Args) string {
if n := args[ArgDERPRegionName]; n != "" {
return fmt.Sprintf("Tailscale hasn't heard from the '%s' relay server in %v. The server might be temporarily unavailable, or your Internet connection might be down.", n, args[ArgDuration])
} else {
return fmt.Sprintf("Tailscale hasn't heard from the home relay server (region ID '%v') in %v. The server might be temporarily unavailable, or your Internet connection might be down.", args[ArgDERPRegionID], args[ArgDuration])
}
},
}
})
// derpRegionErrorWarnable is a Warnable that warns the user that a DERP region is reporting an issue.
var derpRegionErrorWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: tsconst.HealthWarnableDERPRegionError,
Title: "Relay server error",
Severity: SeverityLow,
DependsOn: []*Warnable{NetworkStatusWarnable},
Text: func(args Args) string {
return fmt.Sprintf("The relay server #%v is reporting an issue: %v", args[ArgDERPRegionID], args[ArgError])
},
}
})
// noUDP4BindWarnable is a Warnable that warns the user that Tailscale couldn't listen for incoming UDP connections.
var noUDP4BindWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: tsconst.HealthWarnableNoUDP4Bind,
Title: "NAT traversal setup failure",
Severity: SeverityMedium,
DependsOn: []*Warnable{NetworkStatusWarnable, IPNStateWarnable},
Text: StaticMessage("Tailscale couldn't listen for incoming UDP connections."),
ImpactsConnectivity: true,
}
})
// mapResponseTimeoutWarnable is a Warnable that warns the user that Tailscale hasn't received a network map from the coordination server in a while.
var mapResponseTimeoutWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: tsconst.HealthWarnableMapResponseTimeout,
Title: "Network map response timeout",
Severity: SeverityMedium,
DependsOn: []*Warnable{NetworkStatusWarnable, IPNStateWarnable},
Text: func(args Args) string {
return fmt.Sprintf("Tailscale hasn't received a network map from the coordination server in %s.", args[ArgDuration])
},
}
})
// tlsConnectionFailedWarnable is a Warnable that warns the user that Tailscale could not establish an encrypted connection with a server.
var tlsConnectionFailedWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: tsconst.HealthWarnableTLSConnectionFailed,
Title: "Encrypted connection failed",
Severity: SeverityMedium,
DependsOn: []*Warnable{NetworkStatusWarnable},
Text: func(args Args) string {
return fmt.Sprintf("Tailscale could not establish an encrypted connection with '%q': %v", args[ArgServerName], args[ArgError])
},
}
})
// magicsockReceiveFuncWarnable is a Warnable that warns the user that one of the Magicsock functions is not running.
var magicsockReceiveFuncWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: tsconst.HealthWarnableMagicsockReceiveFuncError,
Title: "MagicSock function not running",
Severity: SeverityMedium,
Text: func(args Args) string {
return fmt.Sprintf("The MagicSock function %s is not running. You might experience connectivity issues.", args[ArgMagicsockFunctionName])
},
}
})
// testWarnable is a Warnable that is used within this package for testing purposes only.
var testWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: tsconst.HealthWarnableTestWarnable,
Title: "Test warnable",
Severity: SeverityLow,
Text: func(args Args) string {
return args[ArgError]
},
}
})
// applyDiskConfigWarnable is a Warnable that warns the user that there was an error applying the envknob config stored on disk.
var applyDiskConfigWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: tsconst.HealthWarnableApplyDiskConfig,
Title: "Could not apply configuration",
Severity: SeverityMedium,
Text: func(args Args) string {
return fmt.Sprintf("An error occurred applying the Tailscale envknob configuration stored on disk: %v", args[ArgError])
},
}
})
// warmingUpWarnableDuration is the duration for which the warmingUpWarnable is reported by the backend after the user
// has changed ipnWantRunning to true from false.
const warmingUpWarnableDuration = 5 * time.Second
// warmingUpWarnable is a Warnable that is reported by the backend when it is starting up, for a maximum time of
// warmingUpWarnableDuration. The GUIs use the presence of this Warnable to prevent showing any other warnings until
// the backend is fully started.
var warmingUpWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: tsconst.HealthWarnableWarmingUp,
Title: "Tailscale is starting",
Severity: SeverityLow,
Text: StaticMessage("Tailscale is starting. Please wait."),
}
})
// ipForwardingWarnable is a Warnable that warns the user that IP forwarding is disabled
// but subnet routing or exit node functionality is being used.
var ipForwardingWarnable = condRegister(func() *Warnable {
return &Warnable{
Code: "ip-forwarding-off",
Title: "IP forwarding is off",
Severity: SeverityMedium,
MapDebugFlag: "warn-ip-forwarding-off",
Text: StaticMessage("Subnet routing is enabled, but IP forwarding is disabled. Check that IP forwarding is enabled on your machine."),
ImpactsConnectivity: true,
}
})
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package hostinfo answers questions about the host environment that Tailscale is
// running on.
package hostinfo
import (
"bufio"
"bytes"
"io"
"os"
"os/exec"
"runtime"
"runtime/debug"
"strings"
"sync"
"sync/atomic"
"time"
"go4.org/mem"
"tailscale.com/envknob"
"tailscale.com/tailcfg"
"tailscale.com/types/lazy"
"tailscale.com/types/opt"
"tailscale.com/util/cloudenv"
"tailscale.com/util/dnsname"
"tailscale.com/util/lineiter"
"tailscale.com/version"
"tailscale.com/version/distro"
)
var started = time.Now()
var newHooks []func(*tailcfg.Hostinfo)
// RegisterHostinfoNewHook registers a callback to be called on a non-nil
// [tailcfg.Hostinfo] before it is returned by [New].
func RegisterHostinfoNewHook(f func(*tailcfg.Hostinfo)) {
newHooks = append(newHooks, f)
}
// New returns a partially populated Hostinfo for the current host.
func New() *tailcfg.Hostinfo {
hostname, _ := Hostname()
hostname = dnsname.FirstLabel(hostname)
hi := &tailcfg.Hostinfo{
IPNVersion: version.Long(),
Hostname: hostname,
App: appTypeCached(),
OS: version.OS(),
OSVersion: GetOSVersion(),
Container: lazyInContainer.Get(),
Distro: condCall(distroName),
DistroVersion: condCall(distroVersion),
DistroCodeName: condCall(distroCodeName),
Env: string(GetEnvType()),
Desktop: desktop(),
Package: packageTypeCached(),
GoArch: runtime.GOARCH,
GoArchVar: lazyGoArchVar.Get(),
GoVersion: runtime.Version(),
Machine: condCall(unameMachine),
DeviceModel: deviceModelCached(),
Cloud: string(cloudenv.Get()),
NoLogsNoSupport: envknob.NoLogsNoSupport(),
AllowsUpdate: envknob.AllowsRemoteUpdate(),
}
for _, f := range newHooks {
f(hi)
}
return hi
}
// non-nil on some platforms
var (
osVersion func() string
packageType func() string
distroName func() string
distroVersion func() string
distroCodeName func() string
unameMachine func() string
deviceModel func() string
systemdLogindDesktop func() bool
)
func condCall[T any](fn func() T) T {
var zero T
if fn == nil {
return zero
}
return fn()
}
var (
lazyInContainer = &lazyAtomicValue[opt.Bool]{f: new(inContainer)}
lazyGoArchVar = &lazyAtomicValue[string]{f: new(goArchVar)}
)
type lazyAtomicValue[T any] struct {
// f is a pointer to a fill function. If it's nil or points
// to nil, then Get returns the zero value for T.
f *func() T
once sync.Once
v T
}
func (v *lazyAtomicValue[T]) Get() T {
v.once.Do(v.fill)
return v.v
}
func (v *lazyAtomicValue[T]) fill() {
if v.f == nil || *v.f == nil {
return
}
v.v = (*v.f)()
}
// GetOSVersion returns the OSVersion of current host if available.
func GetOSVersion() string {
if s, _ := osVersionAtomic.Load().(string); s != "" {
return s
}
if osVersion != nil {
return osVersion()
}
return ""
}
func appTypeCached() string {
if v, ok := appType.Load().(string); ok {
return v
}
return ""
}
func packageTypeCached() string {
if v, _ := packagingType.Load().(string); v != "" {
return v
}
if packageType == nil {
return ""
}
v := packageType()
if v != "" {
SetPackage(v)
}
return v
}
// EnvType represents a known environment type.
// The empty string, the default, means unknown.
type EnvType string
const (
KNative = EnvType("kn")
AWSLambda = EnvType("lm")
Heroku = EnvType("hr")
AzureAppService = EnvType("az")
AWSFargate = EnvType("fg")
FlyDotIo = EnvType("fly")
Kubernetes = EnvType("k8s")
DockerDesktop = EnvType("dde")
Replit = EnvType("repl")
HomeAssistantAddOn = EnvType("haao")
)
var envType atomic.Value // of EnvType
func GetEnvType() EnvType {
if e, ok := envType.Load().(EnvType); ok {
return e
}
e := getEnvType()
envType.Store(e)
return e
}
var (
deviceModelAtomic atomic.Value // of string
osVersionAtomic atomic.Value // of string
desktopAtomic atomic.Value // of opt.Bool
packagingType atomic.Value // of string
appType atomic.Value // of string
firewallMode atomic.Value // of string
)
// SetDeviceModel sets the device model for use in Hostinfo updates.
func SetDeviceModel(model string) { deviceModelAtomic.Store(model) }
func deviceModelCached() string {
if v, _ := deviceModelAtomic.Load().(string); v != "" {
return v
}
if deviceModel == nil {
return ""
}
v := deviceModel()
if v != "" {
deviceModelAtomic.Store(v)
}
return v
}
// SetOSVersion sets the OS version.
func SetOSVersion(v string) { osVersionAtomic.Store(v) }
// SetFirewallMode sets the firewall mode for the app.
func SetFirewallMode(v string) { firewallMode.Store(v) }
// SetPackage sets the packaging type for the app.
//
// For Android, the possible values are:
// - "googleplay": installed from Google Play Store.
// - "fdroid": installed from the F-Droid repository.
// - "amazon": installed from the Amazon Appstore.
// - "unknown": when the installer package name is null.
// - "unknown$installerPackageName": for unrecognized installer package names, prefixed by "unknown".
// Additionally, tsnet sets this value to "tsnet".
func SetPackage(v string) { packagingType.Store(v) }
// SetApp sets the app type for the app.
// It is used by tsnet to specify what app is using it such as "golinks"
// and "k8s-operator".
func SetApp(v string) { appType.Store(v) }
// FirewallMode returns the firewall mode for the app.
// It is empty if unset.
func FirewallMode() string {
s, _ := firewallMode.Load().(string)
return s
}
// desktop reports whether the system is running a Linux desktop environment.
// The result is undefined for non-Linux systems.
// It checks systemd-logind, and then via reading the list of open unix sockets
// and seeing if any of them matches names of wayland, x11 or mir.
// The detection runs for a minute after starting tailscaled, after that the
// value is cached and returned directly from the cache.
func desktop() (ret opt.Bool) {
if runtime.GOOS != "linux" {
return opt.Bool("")
}
if v := desktopAtomic.Load(); v != nil {
v, _ := v.(opt.Bool)
return v
}
seenDesktop := false
if systemdLogindDesktop != nil {
seenDesktop = systemdLogindDesktop()
}
if !seenDesktop {
for lr := range lineiter.File("/proc/net/unix") {
line, _ := lr.Value()
if seenDesktop = mem.Contains(mem.B(line), mem.S(".X11-unix")) ||
mem.Contains(mem.B(line), mem.S("/wayland-")) ||
mem.Contains(mem.B(line), mem.S("mir_socket")) ||
mem.Contains(mem.B(line), mem.S("gamescope-")); seenDesktop {
break
}
}
}
ret.Set(seenDesktop)
// Only cache after a minute - compositors might not have started yet.
if time.Since(started) > time.Minute {
desktopAtomic.Store(ret)
}
return ret
}
func getEnvType() EnvType {
if inKnative() {
return KNative
}
if inAWSLambda() {
return AWSLambda
}
if inHerokuDyno() {
return Heroku
}
if inAzureAppService() {
return AzureAppService
}
if inAWSFargate() {
return AWSFargate
}
if inFlyDotIo() {
return FlyDotIo
}
if inKubernetes() {
return Kubernetes
}
if inDockerDesktop() {
return DockerDesktop
}
if inReplit() {
return Replit
}
if inHomeAssistantAddOn() {
return HomeAssistantAddOn
}
return ""
}
// inContainer reports whether we're running in a container. Best-effort only,
// there's no foolproof way to detect this, but the build tag should catch all
// official builds from 1.78.0.
func inContainer() opt.Bool {
if runtime.GOOS != "linux" {
return ""
}
var ret opt.Bool
ret.Set(false)
if packageType != nil && packageType() == "container" {
// Go build tag ts_package_container was set during build.
ret.Set(true)
return ret
}
// Only set if using docker's container runtime. Not guaranteed by
// documentation, but it's been in place for a long time.
if _, err := os.Stat("/.dockerenv"); err == nil {
ret.Set(true)
return ret
}
if _, err := os.Stat("/run/.containerenv"); err == nil {
// See https://github.com/cri-o/cri-o/issues/5461
ret.Set(true)
return ret
}
for lr := range lineiter.File("/proc/1/cgroup") {
line, _ := lr.Value()
if mem.Contains(mem.B(line), mem.S("/docker/")) ||
mem.Contains(mem.B(line), mem.S("/lxc/")) {
ret.Set(true)
break
}
}
for lr := range lineiter.File("/proc/mounts") {
line, _ := lr.Value()
if mem.Contains(mem.B(line), mem.S("lxcfs /proc/cpuinfo fuse.lxcfs")) {
ret.Set(true)
break
}
}
return ret
}
func inKnative() bool {
// https://cloud.google.com/run/docs/reference/container-contract#env-vars
if os.Getenv("K_REVISION") != "" && os.Getenv("K_CONFIGURATION") != "" &&
os.Getenv("K_SERVICE") != "" && os.Getenv("PORT") != "" {
return true
}
return false
}
func inAWSLambda() bool {
// https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html
if os.Getenv("AWS_LAMBDA_FUNCTION_NAME") != "" &&
os.Getenv("AWS_LAMBDA_FUNCTION_VERSION") != "" &&
os.Getenv("AWS_LAMBDA_INITIALIZATION_TYPE") != "" &&
os.Getenv("AWS_LAMBDA_RUNTIME_API") != "" {
return true
}
return false
}
func inHerokuDyno() bool {
// https://devcenter.heroku.com/articles/dynos#local-environment-variables
if os.Getenv("PORT") != "" && os.Getenv("DYNO") != "" {
return true
}
return false
}
func inAzureAppService() bool {
if os.Getenv("APPSVC_RUN_ZIP") != "" && os.Getenv("WEBSITE_STACK") != "" &&
os.Getenv("WEBSITE_AUTH_AUTO_AAD") != "" {
return true
}
return false
}
func inAWSFargate() bool {
return os.Getenv("AWS_EXECUTION_ENV") == "AWS_ECS_FARGATE"
}
func inFlyDotIo() bool {
if os.Getenv("FLY_APP_NAME") != "" && os.Getenv("FLY_REGION") != "" {
return true
}
return false
}
func inReplit() bool {
// https://docs.replit.com/replit-workspace/configuring-repl#environment-variables
if os.Getenv("REPL_OWNER") != "" && os.Getenv("REPL_SLUG") != "" {
return true
}
return false
}
func inKubernetes() bool {
if os.Getenv("KUBERNETES_SERVICE_HOST") != "" && os.Getenv("KUBERNETES_SERVICE_PORT") != "" {
return true
}
return false
}
func inDockerDesktop() bool {
return os.Getenv("TS_HOST_ENV") == "dde"
}
func inHomeAssistantAddOn() bool {
if os.Getenv("SUPERVISOR_TOKEN") != "" || os.Getenv("HASSIO_TOKEN") != "" {
return true
}
return false
}
// goArchVar returns the GOARM or GOAMD64 etc value that the binary was built
// with.
func goArchVar() string {
bi, ok := debug.ReadBuildInfo()
if !ok {
return ""
}
// Look for GOARM, GOAMD64, GO386, etc. Note that the little-endian
// "le"-suffixed GOARCH values don't have their own environment variable.
//
// See https://pkg.go.dev/cmd/go#hdr-Environment_variables and the
// "Architecture-specific environment variables" section:
wantKey := "GO" + strings.ToUpper(strings.TrimSuffix(runtime.GOARCH, "le"))
for _, s := range bi.Settings {
if s.Key == wantKey {
return s.Value
}
}
return ""
}
type etcAptSrcResult struct {
mod time.Time
disabled bool
}
var etcAptSrcCache atomic.Value // of etcAptSrcResult
// DisabledEtcAptSource reports whether Ubuntu (or similar) has disabled
// the /etc/apt/sources.list.d/tailscale.list file contents upon upgrade
// to a new release of the distro.
//
// See https://github.com/tailscale/tailscale/issues/3177
func DisabledEtcAptSource() bool {
if runtime.GOOS != "linux" {
return false
}
const path = "/etc/apt/sources.list.d/tailscale.list"
fi, err := os.Stat(path)
if err != nil || !fi.Mode().IsRegular() {
return false
}
mod := fi.ModTime()
if c, ok := etcAptSrcCache.Load().(etcAptSrcResult); ok && c.mod.Equal(mod) {
return c.disabled
}
f, err := os.Open(path)
if err != nil {
return false
}
defer f.Close()
v := etcAptSourceFileIsDisabled(f)
etcAptSrcCache.Store(etcAptSrcResult{mod: mod, disabled: v})
return v
}
func etcAptSourceFileIsDisabled(r io.Reader) bool {
bs := bufio.NewScanner(r)
disabled := false // did we find the "disabled on upgrade" comment?
for bs.Scan() {
line := strings.TrimSpace(bs.Text())
if strings.Contains(line, "# disabled on upgrade") {
disabled = true
}
if line == "" || line[0] == '#' {
continue
}
// Well, it has some contents in it at least.
return false
}
return disabled
}
// IsSELinuxEnforcing reports whether SELinux is in "Enforcing" mode.
func IsSELinuxEnforcing() bool {
if runtime.GOOS != "linux" {
return false
}
out, _ := exec.Command("getenforce").Output()
return string(bytes.TrimSpace(out)) == "Enforcing"
}
// IsNATLabGuestVM reports whether the current host is a NAT Lab guest VM.
func IsNATLabGuestVM() bool {
if runtime.GOOS == "linux" && distro.Get() == distro.Gokrazy {
cmdLine, _ := os.ReadFile("/proc/cmdline")
return bytes.Contains(cmdLine, []byte("tailscale-tta=1"))
}
return false
}
const copyV86DeviceModel = "copy-v86"
var isV86Cache lazy.SyncValue[bool]
// IsInVM86 reports whether we're running in the copy/v86 wasm emulator,
// https://github.com/copy/v86/.
func IsInVM86() bool {
return isV86Cache.Get(func() bool {
return New().DeviceModel == copyV86DeviceModel
})
}
type hostnameQuery func() (string, error)
var hostnameFn atomic.Value // of func() (string, error)
// SetHostNameFn sets a custom function for querying the system hostname.
func SetHostnameFn(fn hostnameQuery) {
hostnameFn.Store(fn)
}
// Hostname returns the system hostname using the function
// set by SetHostNameFn. We will fallback to os.Hostname.
func Hostname() (string, error) {
if fn, ok := hostnameFn.Load().(hostnameQuery); ok && fn != nil {
return fn()
}
return os.Hostname()
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && !android
package hostinfo
import (
"bytes"
"context"
"encoding/json"
"os"
"os/exec"
"strings"
"time"
"golang.org/x/sys/unix"
"tailscale.com/util/lineiter"
"tailscale.com/version/distro"
)
func init() {
osVersion = lazyOSVersion.Get
packageType = packageTypeLinux
distroName = distroNameLinux
distroVersion = distroVersionLinux
distroCodeName = distroCodeNameLinux
deviceModel = deviceModelLinux
systemdLogindDesktop = hasLogindDesktop
}
var (
lazyVersionMeta = &lazyAtomicValue[versionMeta]{f: new(linuxVersionMeta)}
lazyOSVersion = &lazyAtomicValue[string]{f: new(osVersionLinux)}
)
type versionMeta struct {
DistroName string
DistroVersion string
DistroCodeName string // "jammy", etc (VERSION_CODENAME from /etc/os-release)
}
func distroNameLinux() string {
return lazyVersionMeta.Get().DistroName
}
func distroVersionLinux() string {
return lazyVersionMeta.Get().DistroVersion
}
func distroCodeNameLinux() string {
return lazyVersionMeta.Get().DistroCodeName
}
func deviceModelLinux() string {
for _, path := range []string{
// First try the Synology-specific location.
// Example: "DS916+-j"
"/proc/sys/kernel/syno_hw_version",
// Otherwise, try the Devicetree model, usually set on
// ARM SBCs, etc.
// Example: "Raspberry Pi 4 Model B Rev 1.2"
// Example: "WD My Cloud Gen2: Marvell Armada 375"
"/sys/firmware/devicetree/base/model", // Raspberry Pi 4 Model B Rev 1.2"
} {
b, _ := os.ReadFile(path)
if s := strings.Trim(string(b), "\x00\r\n\t "); s != "" {
return s
}
}
return ""
}
func getQnapQtsVersion(versionInfo string) string {
for field := range strings.FieldsSeq(versionInfo) {
if suffix, ok := strings.CutPrefix(field, "QTSFW_"); ok {
return suffix
}
}
return ""
}
func osVersionLinux() string {
var un unix.Utsname
unix.Uname(&un)
return unix.ByteSliceToString(un.Release[:])
}
func linuxVersionMeta() (meta versionMeta) {
dist := distro.Get()
meta.DistroName = string(dist)
propFile := "/etc/os-release"
switch dist {
case distro.Synology:
propFile = "/etc.defaults/VERSION"
case distro.OpenWrt:
propFile = "/etc/openwrt_release"
case distro.Unraid:
propFile = "/etc/unraid-version"
case distro.WDMyCloud:
slurp, _ := os.ReadFile("/etc/version")
meta.DistroVersion = string(bytes.TrimSpace(slurp))
return
case distro.QNAP:
slurp, _ := os.ReadFile("/etc/version_info")
meta.DistroVersion = getQnapQtsVersion(string(slurp))
return
}
m := map[string]string{}
for lr := range lineiter.File(propFile) {
line, err := lr.Value()
if err != nil {
break
}
before, after, ok := bytes.Cut(line, []byte{'='})
if !ok {
continue
}
k, v := string(before), strings.Trim(string(after), `"'`)
m[k] = v
}
if v := m["VERSION_CODENAME"]; v != "" {
meta.DistroCodeName = v
}
if v := m["VERSION_ID"]; v != "" {
meta.DistroVersion = v
}
id := m["ID"]
if id != "" {
meta.DistroName = id
}
switch id {
case "debian":
// Debian's VERSION_ID is just like "11". But /etc/debian_version has "11.5" normally.
// Or "bookworm/sid" on sid/testing.
slurp, _ := os.ReadFile("/etc/debian_version")
if v := string(bytes.TrimSpace(slurp)); v != "" {
if '0' <= v[0] && v[0] <= '9' {
meta.DistroVersion = v
} else if meta.DistroCodeName == "" {
meta.DistroCodeName = v
}
}
case "", "centos": // CentOS 6 has no /etc/os-release, so its id is ""
if meta.DistroVersion == "" {
if cr, _ := os.ReadFile("/etc/centos-release"); len(cr) > 0 { // "CentOS release 6.10 (Final)
meta.DistroVersion = string(bytes.TrimSpace(cr))
}
}
}
if v := m["PRETTY_NAME"]; v != "" && meta.DistroVersion == "" && !strings.HasSuffix(v, "/sid") {
meta.DistroVersion = v
}
switch dist {
case distro.Synology:
meta.DistroVersion = m["productversion"]
case distro.OpenWrt:
meta.DistroVersion = m["DISTRIB_RELEASE"]
case distro.Unraid:
meta.DistroVersion = m["version"]
}
return
}
// linuxBuildTagPackageType is set by packagetype_*.go
// build tag guarded files.
var linuxBuildTagPackageType string
func packageTypeLinux() string {
if v := linuxBuildTagPackageType; v != "" {
return v
}
// Report whether this is in a snap.
// See https://snapcraft.io/docs/environment-variables
// We just look at two somewhat arbitrarily.
if os.Getenv("SNAP_NAME") != "" && os.Getenv("SNAP") != "" {
return "snap"
}
return ""
}
// hasLogindDesktop calls [systemdLogindFindDesktop] with a 100 millisecond
// timeout.
func hasLogindDesktop() bool {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
runner := &osLogindRunner{ctx: ctx}
return systemdLogindFindDesktop(runner)
}
// logindConn is the interface for querying systemd-logind session information.
// It exists to be able to swap the backend for testing.
type logindRunner interface {
// listSessions returns the raw JSON output of
// "loginctl list-sessions --json=short", which is a json blob containing
// output like: '[{"session":"8"},{"session":"9"}]'.
listSessions() ([]byte, error)
// getSessionType returns the raw output of
// "loginctl show-session <id> --property=Type", which is a single line of
// the form "Type=<value>".
getSessionType(string) (string, error)
}
// osLogindRunner implements [logindRunner] by shelling out to the system logind.
type osLogindRunner struct{ ctx context.Context }
// listSessions implements [logindRunner] using os calls.
func (r *osLogindRunner) listSessions() ([]byte, error) {
path, err := exec.LookPath("loginctl")
if err != nil {
return nil, err
}
cmd := exec.CommandContext(r.ctx, path, "list-sessions", "--json=short")
output, err := cmd.Output()
if err != nil {
return nil, err
}
return output, nil
}
// getSessionType implements [logindRunner] using os calls.
func (r *osLogindRunner) getSessionType(session string) (string, error) {
path, err := exec.LookPath("loginctl")
if err != nil {
return "", err
}
cmd := exec.CommandContext(r.ctx,
path, "show-session", session, "--property=Type", "--value")
output, err := cmd.Output()
if err != nil {
return "", err
}
return string(output), nil
}
// systemdLogindFindDesktop reports whether any active logind session has a
// graphical desktop type (x11, wayland, or mir). On an error or with no types
// matching the list above, it returns false.
func systemdLogindFindDesktop(logind logindRunner) bool {
sessionJSON, err := logind.listSessions()
if err != nil {
return false
}
type session struct {
Session string `json:"session"`
}
var sessions []session
err = json.Unmarshal(sessionJSON, &sessions)
if err != nil {
return false
}
for _, sess := range sessions {
typeString, err := logind.getSessionType(sess.Session)
if err != nil {
continue
}
switch strings.ToLower(strings.TrimSpace(typeString)) {
case "wayland", "x11", "mir":
return true
}
}
return false
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux || freebsd || openbsd || darwin
package hostinfo
import (
"runtime"
"golang.org/x/sys/unix"
)
func init() {
unameMachine = lazyUnameMachine.Get
}
var lazyUnameMachine = &lazyAtomicValue[string]{f: new(unameMachineUnix)}
func unameMachineUnix() string {
switch runtime.GOOS {
case "android":
// Don't call on Android for now. We're late in the 1.36 release cycle
// and don't want to test syscall filters on various Android versions to
// see what's permitted. Notably, the hostinfo_linux.go file has build
// tag !android, so maybe Uname is verboten.
return ""
case "ios":
// For similar reasons, don't call on iOS. There aren't many iOS devices
// and we know their CPU properties so calling this is only risk and no
// reward.
return ""
}
var un unix.Utsname
unix.Uname(&un)
return unix.ByteSliceToString(un.Machine[:])
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package kubetypes
import "fmt"
const (
// Hostinfo App values for the Tailscale Kubernetes Operator components.
AppOperator = "k8s-operator"
AppInProcessAPIServerProxy = "k8s-operator-proxy"
AppIngressProxy = "k8s-operator-ingress-proxy"
AppIngressResource = "k8s-operator-ingress-resource"
AppEgressProxy = "k8s-operator-egress-proxy"
AppConnector = "k8s-operator-connector-resource"
AppProxyGroupEgress = "k8s-operator-proxygroup-egress"
AppProxyGroupIngress = "k8s-operator-proxygroup-ingress"
AppProxyGroupKubeAPIServer = "k8s-operator-proxygroup-kube-apiserver"
// Clientmetrics for Tailscale Kubernetes Operator components
MetricIngressProxyCount = "k8s_ingress_proxies" // L3
MetricIngressResourceCount = "k8s_ingress_resources" // L7
MetricIngressPGResourceCount = "k8s_ingress_pg_resources" // L7 on ProxyGroup
MetricServicePGResourceCount = "k8s_service_pg_resources" // L3 on ProxyGroup
MetricEgressProxyCount = "k8s_egress_proxies"
MetricConnectorResourceCount = "k8s_connector_resources"
MetricConnectorWithSubnetRouterCount = "k8s_connector_subnetrouter_resources"
MetricConnectorWithExitNodeCount = "k8s_connector_exitnode_resources"
MetricConnectorWithAppConnectorCount = "k8s_connector_appconnector_resources"
MetricNameserverCount = "k8s_nameserver_resources"
MetricRecorderCount = "k8s_recorder_resources"
MetricEgressServiceCount = "k8s_egress_service_resources"
MetricProxyGroupEgressCount = "k8s_proxygroup_egress_resources"
MetricProxyGroupIngressCount = "k8s_proxygroup_ingress_resources"
MetricProxyGroupAPIServerCount = "k8s_proxygroup_kube_apiserver_resources"
MetricTailnetCount = "k8s_tailnet_resources"
MetricPeerRelayCount = "k8s_peerrelay_resources"
// Keys that containerboot writes to state file that can be used to determine its state.
// fields set in Tailscale state Secret. These are mostly used by the Tailscale Kubernetes operator to determine
// the state of this tailscale device.
KeyDeviceID = "device_id" // node stable ID of the device
KeyDeviceFQDN = "device_fqdn" // device's tailnet hostname
KeyDeviceIPs = "device_ips" // device's tailnet IPs
KeyPodUID = "pod_uid" // Pod UID
KeyCapVer = "tailscale_capver" // tailcfg.CurrentCapabilityVersion of this proxy instance.
KeyReissueAuthkey = "reissue_authkey" // Proxies will set this to the authkey that failed, or "no-authkey", if they can't log in.
// KeyHTTPSEndpoint is a name of a field that can be set to the value of any HTTPS endpoint currently exposed by
// this device to the tailnet. This is used by the Kubernetes operator Ingress proxy to communicate to the operator
// that cluster workloads behind the Ingress can now be accessed via the given DNS name over HTTPS.
KeyHTTPSEndpoint = "https_endpoint"
ValueNoHTTPS = "no-https"
// Pod's IPv4 address header key as returned by containerboot health check endpoint.
PodIPv4Header string = "Pod-IPv4"
// Pod's IPv6 address header key as returned by containerboot health check endpoint.
PodIPv6Header string = "Pod-IPv6"
EgessServicesPreshutdownEP = "/internal-egress-services-preshutdown"
LabelManaged = "tailscale.com/managed"
LabelSecretType = "tailscale.com/secret-type" // "config", "state" "certs"
LabelSecretTypeConfig = "config"
LabelSecretTypeState = "state"
LabelSecretTypeCerts = "certs"
// ACMEAccountsSecretName is the name of the Secret in the operator
// namespace that holds shared ACME account private keys, one field per
// tailnet. Sharing one account key across all replicas of every
// ProxyGroup attached to a tailnet preserves Let's Encrypt's renewal
// exemption (via the ARI "replaces" extension) across pod restarts,
// ProxyGroup recreation, and ingress migration.
ACMEAccountsSecretName = "tailscale-acme-accounts"
// ACMEAccountDefaultKey is the field used inside ACMEAccountsSecretName
// for the "default" tailnet (i.e., when ProxyGroup.spec.tailnet is empty
// and the operator uses its own credentials).
ACMEAccountDefaultKey = "_default"
// ACMEAccountKeySuffix is appended to the tailnet identifier to form a
// field name within ACMEAccountsSecretName.
ACMEAccountKeySuffix = ".acme-account.key.pem"
// ACMEAccountsFinalizer blocks accidental deletion of
// ACMEAccountsSecretName; losing those keys breaks ARI "replaces"
// renewals for every cert in the tailnet. See #18251.
ACMEAccountsFinalizer = "tailscale.com/acme-account-protection"
KubeAPIServerConfigFile = "config.hujson"
APIServerProxyModeAuth APIServerProxyMode = "auth"
APIServerProxyModeNoAuth APIServerProxyMode = "noauth"
)
// APIServerProxyMode specifies whether the API server proxy will add
// impersonation headers to requests based on the caller's Tailscale identity.
// May be "auth" or "noauth".
type APIServerProxyMode string
func (a *APIServerProxyMode) UnmarshalJSON(data []byte) error {
switch string(data) {
case `"auth"`:
*a = APIServerProxyModeAuth
case `"noauth"`:
*a = APIServerProxyModeNoAuth
default:
return fmt.Errorf("unknown APIServerProxyMode %q", data)
}
return nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package filelogger provides localdisk log writing & rotation, primarily for Windows
// clients. (We get this for free on other platforms.)
package filelogger
import (
"bytes"
"fmt"
"log"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"tailscale.com/tstime"
"tailscale.com/types/logger"
)
const (
maxSize = 100 << 20
maxFiles = 50
)
// New returns a logf wrapper that appends to local disk log
// files on Windows, rotating old log files as needed to stay under
// file count & byte limits.
func New(fileBasePrefix, logID string, logf logger.Logf) logger.Logf {
if runtime.GOOS != "windows" {
panic("not yet supported on any platform except Windows")
}
if logf == nil {
panic("nil logf")
}
dir := filepath.Join(os.Getenv("ProgramData"), "Tailscale", "Logs")
if err := os.MkdirAll(dir, 0700); err != nil {
log.Printf("failed to create local log directory; not writing logs to disk: %v", err)
return logf
}
logf("local disk logdir: %v", dir)
lfw := &logFileWriter{
fileBasePrefix: fileBasePrefix,
logID: logID,
dir: dir,
wrappedLogf: logf,
}
return lfw.Logf
}
// logFileWriter is the state for the log writer & rotator.
type logFileWriter struct {
dir string // e.g. `C:\Users\FooBarUser\AppData\Local\Tailscale\Logs`
logID string // hex logID
fileBasePrefix string // e.g. "tailscale-service" or "tailscale-gui"
wrappedLogf logger.Logf // underlying logger to send to
mu sync.Mutex // guards following
buf bytes.Buffer // scratch buffer to avoid allocs
fday civilDay // day that f was opened; zero means no file yet open
f *os.File // file currently opened for append
}
// civilDay is a year, month, and day in the local timezone.
// It's a comparable value type.
type civilDay struct {
year int
month time.Month
day int
}
func dayOf(t time.Time) civilDay {
return civilDay{t.Year(), t.Month(), t.Day()}
}
func (w *logFileWriter) Logf(format string, a ...any) {
w.mu.Lock()
defer w.mu.Unlock()
w.buf.Reset()
fmt.Fprintf(&w.buf, format, a...)
if w.buf.Len() == 0 {
return
}
out := w.buf.Bytes()
w.wrappedLogf("%s", out)
// Make sure there's a final newline before we write to the log file.
if out[len(out)-1] != '\n' {
w.buf.WriteByte('\n')
out = w.buf.Bytes()
}
w.appendToFileLocked(out)
}
// out should end in a newline.
// w.mu must be held.
func (w *logFileWriter) appendToFileLocked(out []byte) {
now := time.Now()
day := dayOf(now)
if w.fday != day {
w.startNewFileLocked()
}
out = removeDatePrefix(out)
if w.f != nil {
fmt.Fprintf(w.f, "%s: %s",
now.Format(tstime.DateTTimeMilliZ),
out)
}
}
func isNum(b byte) bool { return '0' <= b && b <= '9' }
// removeDatePrefix returns a subslice of v with the log package's
// standard datetime prefix format removed, if present.
func removeDatePrefix(v []byte) []byte {
const format = "2009/01/23 01:23:23 "
if len(v) < len(format) {
return v
}
for i, b := range v[:len(format)] {
fb := format[i]
if isNum(fb) {
if !isNum(b) {
return v
}
continue
}
if b != fb {
return v
}
}
return v[len(format):]
}
// startNewFileLocked opens a new log file for writing
// and also cleans up any old files.
//
// w.mu must be held.
func (w *logFileWriter) startNewFileLocked() {
var oldName string
if w.f != nil {
oldName = filepath.Base(w.f.Name())
w.f.Close()
w.f = nil
w.fday = civilDay{}
}
w.cleanLocked()
now := time.Now()
day := dayOf(now)
name := filepath.Join(w.dir, fmt.Sprintf("%s-%04d%02d%02dT%02d%02d%02d-%d.txt",
w.fileBasePrefix,
day.year,
day.month,
day.day,
now.Hour(),
now.Minute(),
now.Second(),
now.Unix()))
var err error
w.f, err = os.Create(name)
if err != nil {
w.wrappedLogf("failed to create log file: %v", err)
return
}
if oldName != "" {
fmt.Fprintf(w.f, "(logID %q; continued from log file %s)\n", w.logID, oldName)
} else {
fmt.Fprintf(w.f, "(logID %q)\n", w.logID)
}
w.fday = day
}
// cleanLocked cleans up old log files.
//
// w.mu must be held.
func (w *logFileWriter) cleanLocked() {
entries, _ := os.ReadDir(w.dir)
prefix := w.fileBasePrefix + "-"
fileSize := map[string]int64{}
var files []string
var sumSize int64
for _, entry := range entries {
fi, err := entry.Info()
if err != nil {
w.wrappedLogf("error getting log file info: %v", err)
continue
}
baseName := filepath.Base(fi.Name())
if !strings.HasPrefix(baseName, prefix) {
continue
}
size := fi.Size()
fileSize[baseName] = size
sumSize += size
files = append(files, baseName)
}
if sumSize > maxSize {
w.wrappedLogf("cleaning log files; sum byte count %d > %d", sumSize, maxSize)
}
if len(files) > maxFiles {
w.wrappedLogf("cleaning log files; number of files %d > %d", len(files), maxFiles)
}
for (sumSize > maxSize || len(files) > maxFiles) && len(files) > 0 {
target := files[0]
files = files[1:]
targetSize := fileSize[target]
targetFull := filepath.Join(w.dir, target)
err := os.Remove(targetFull)
if err != nil {
w.wrappedLogf("error cleaning log file: %v", err)
} else {
sumSize -= targetSize
w.wrappedLogf("cleaned log file %s (size %d); new bytes=%v, files=%v", targetFull, targetSize, sumSize, len(files))
}
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package logpolicy manages the creation or reuse of logtail loggers,
// caching collection instance state on disk for use on future runs of
// programs on the same machine.
package logpolicy
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"net/netip"
"net/url"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"golang.org/x/term"
"tailscale.com/atomicfile"
"tailscale.com/envknob"
"tailscale.com/feature"
"tailscale.com/feature/buildfeatures"
"tailscale.com/health"
"tailscale.com/hostinfo"
"tailscale.com/log/filelogger"
"tailscale.com/logtail"
"tailscale.com/logtail/filch"
"tailscale.com/net/dnscache"
"tailscale.com/net/dnsfallback"
"tailscale.com/net/netknob"
"tailscale.com/net/netmon"
"tailscale.com/net/netns"
"tailscale.com/net/netutil"
"tailscale.com/net/netx"
"tailscale.com/net/tlsdial"
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/types/logger"
"tailscale.com/types/logid"
"tailscale.com/util/clientmetric"
"tailscale.com/util/eventbus"
"tailscale.com/util/must"
"tailscale.com/util/racebuild"
"tailscale.com/util/testenv"
"tailscale.com/version"
"tailscale.com/version/distro"
)
// GetLogTarget is an optional hook to register a function
// that returns the log target URL to be used by logpolicy.
var GetLogTarget feature.Hook[func() string]
var getLogTargetOnce struct {
sync.Once
v string // URL of logs server, or empty for default
}
func getLogTarget() string {
getLogTargetOnce.Do(func() {
if f, ok := GetLogTarget.GetOk(); ok {
getLogTargetOnce.v = f()
}
if getLogTargetOnce.v == "" {
getLogTargetOnce.v, _ = os.LookupEnv("TS_LOG_TARGET")
}
})
return getLogTargetOnce.v
}
// LogURL is the base URL for the configured logtail server, or the default.
// It is guaranteed to not terminate with any forward slashes.
func LogURL() string {
if v := getLogTarget(); v != "" {
return strings.TrimRight(v, "/")
}
return "https://" + logtail.DefaultHost
}
// LogHost returns the hostname only (without port) of the configured
// logtail server, or the default.
//
// Deprecated: Use LogURL instead.
func LogHost() string {
if v := getLogTarget(); v != "" {
if u, err := url.Parse(v); err == nil {
return u.Hostname()
}
}
return logtail.DefaultHost
}
// Config represents an instance of logs in a collection.
type Config struct {
Collection string
PrivateID logid.PrivateID
PublicID logid.PublicID
}
// Policy is a logger and its public ID.
type Policy struct {
// Logtail is the logger.
Logtail *logtail.Logger
// PublicID is the logger's instance identifier.
// It may be the zero value if logging is not in use.
PublicID logid.PublicID
// Logf is where to write informational messages about this Logger.
Logf logger.Logf
}
// NewConfig creates a Config with collection and a newly generated PrivateID.
func NewConfig(collection string) *Config {
id := must.Get(logid.NewPrivateID())
return &Config{
Collection: collection,
PrivateID: id,
PublicID: id.Public(),
}
}
// Validate verifies that the Config matches the collection,
// and that the PrivateID and PublicID pair are sensible.
func (c *Config) Validate(collection string) error {
switch {
case c == nil:
return errors.New("config is nil")
case c.Collection != collection:
return fmt.Errorf("config collection %q does not match %q", c.Collection, collection)
case c.PrivateID.IsZero():
return errors.New("config has zero PrivateID")
case c.PrivateID.Public() != c.PublicID:
return errors.New("config PrivateID does not match PublicID")
}
return nil
}
// ToBytes returns the JSON representation of c.
func (c *Config) ToBytes() []byte {
data, err := json.MarshalIndent(c, "", "\t")
if err != nil {
log.Fatalf("logpolicy.Config marshal: %v", err)
}
return data
}
// Save writes the JSON representation of c to stateFile.
func (c *Config) Save(stateFile string) error {
c.PublicID = c.PrivateID.Public()
if err := os.MkdirAll(filepath.Dir(stateFile), 0750); err != nil {
return err
}
data := c.ToBytes()
if err := atomicfile.WriteFile(stateFile, data, 0600); err != nil {
return err
}
return nil
}
// ConfigFromFile reads a Config from a JSON file.
func ConfigFromFile(statefile string) (*Config, error) {
b, err := os.ReadFile(statefile)
if err != nil {
return nil, err
}
return ConfigFromBytes(b)
}
// ConfigFromBytes parses a Config from its JSON encoding.
func ConfigFromBytes(jsonEnc []byte) (*Config, error) {
c := &Config{}
if err := json.Unmarshal(jsonEnc, c); err != nil {
return nil, err
}
return c, nil
}
// stderrWriter is an io.Writer that always writes to the latest
// os.Stderr, even if os.Stderr changes during the lifetime of the
// stderrWriter value.
type stderrWriter struct{}
func (stderrWriter) Write(buf []byte) (int, error) {
return os.Stderr.Write(buf)
}
type logWriter struct {
logger *log.Logger
}
func (lg logWriter) Write(buf []byte) (int, error) {
lg.logger.Printf("%s", buf)
return len(buf), nil
}
// LogsDir returns the directory to use for log configuration and
// buffer storage.
func LogsDir(logf logger.Logf) string {
if d := os.Getenv("TS_LOGS_DIR"); d != "" {
fi, err := os.Stat(d)
if err == nil && fi.IsDir() {
return d
}
}
switch runtime.GOOS {
case "windows":
if version.CmdName() == "tailscaled" {
// In the common case, when tailscaled is run as the Local System (as a service),
// we want to use %ProgramData% (C:\ProgramData\Tailscale), aside the
// system state config with the machine key, etc. But if that directory's
// not accessible, then it's probably because the user is running tailscaled
// as a regular user (perhaps in userspace-networking/SOCK5 mode) and we should
// just use the %LocalAppData% instead. In a user context, %LocalAppData% isn't
// subject to random deletions from Windows system updates.
dir := filepath.Join(os.Getenv("ProgramData"), "Tailscale")
if winProgramDataAccessible(dir) {
logf("logpolicy: using dir %v", dir)
return dir
}
}
dir := filepath.Join(os.Getenv("LocalAppData"), "Tailscale")
logf("logpolicy: using LocalAppData dir %v", dir)
return dir
case "linux":
if distro.Get() == distro.JetKVM {
return "/userdata/tailscale/var"
}
// STATE_DIRECTORY is set by systemd 240+ but we support older
// systems-d. For example, Ubuntu 18.04 (Bionic Beaver) is 237.
systemdStateDir := os.Getenv("STATE_DIRECTORY")
if systemdStateDir != "" {
logf("logpolicy: using $STATE_DIRECTORY, %q", systemdStateDir)
return systemdStateDir
}
case "js":
logf("logpolicy: no logs directory in the browser")
return ""
}
// Default to e.g. /var/lib/tailscale or /var/db/tailscale on Unix.
if d := paths.DefaultTailscaledStateFile(); d != "" {
d = filepath.Dir(d) // directory of e.g. "/var/lib/tailscale/tailscaled.state"
if err := os.MkdirAll(d, 0700); err == nil {
logf("logpolicy: using system state directory %q", d)
return d
}
}
cacheDir, err := os.UserCacheDir()
if err == nil {
d := filepath.Join(cacheDir, "Tailscale")
logf("logpolicy: using UserCacheDir, %q", d)
return d
}
// Use the current working directory, unless we're being run by a
// service manager that sets it to /.
wd, err := os.Getwd()
if err == nil && wd != "/" {
logf("logpolicy: using current directory, %q", wd)
return wd
}
// No idea where to put stuff. Try to create a temp dir. It'll
// mean we might lose some logs and rotate through log IDs, but
// it's something.
tmp, err := os.MkdirTemp("", "tailscaled-log-*")
if err != nil {
panic("no safe place found to store log state")
}
logf("logpolicy: using temp directory, %q", tmp)
return tmp
}
// runningUnderSystemd reports whether we're running under systemd.
func runningUnderSystemd() bool {
if runtime.GOOS == "linux" && os.Getppid() == 1 {
slurp, _ := os.ReadFile("/proc/1/stat")
return bytes.HasPrefix(slurp, []byte("1 (systemd) "))
}
return false
}
func redirectStderrToLogPanics() bool {
return runningUnderSystemd() || envknob.Bool("TS_PLEASE_PANIC")
}
// winProgramDataAccessible reports whether the directory (assumed to
// be a Windows %ProgramData% directory) is accessible to the current
// process. It's created if needed.
func winProgramDataAccessible(dir string) bool {
if err := os.MkdirAll(dir, 0700); err != nil {
// TODO: windows ACLs
return false
}
// The C:\ProgramData\Tailscale directory should be locked down
// by with ACLs to only be readable by the local system so a
// regular user shouldn't be able to do this operation:
if _, err := os.ReadDir(dir); err != nil {
return false
}
return true
}
// tryFixLogStateLocation is a temporary fixup for
// https://github.com/tailscale/tailscale/issues/247 . We accidentally
// wrote logging state files to /, and then later to $CACHE_DIRECTORY
// (which is incorrect because the log ID is not reconstructible if
// deleted - it's state, not cache data).
//
// If log state for cmdname exists in / or $CACHE_DIRECTORY, and no
// log state for that command exists in dir, then the log state is
// moved from wherever it does exist, into dir. Leftover logs state
// in / and $CACHE_DIRECTORY is deleted.
func tryFixLogStateLocation(dir, cmdname string, logf logger.Logf) {
switch runtime.GOOS {
case "linux", "freebsd", "openbsd":
// These are the OSes where we might have written stuff into
// root. Others use different logic to find the logs storage
// dir.
default:
return
}
if cmdname == "" {
logf("[unexpected] no cmdname given to tryFixLogStateLocation, please file a bug at https://github.com/tailscale/tailscale")
return
}
if dir == "/" {
// Trying to store things in / still. That's a bug, but don't
// abort hard.
logf("[unexpected] storing logging config in /, please file a bug at https://github.com/tailscale/tailscale")
return
}
if os.Getuid() != 0 {
// Only root could have written log configs to weird places.
return
}
// We stored logs in 2 incorrect places: either /, or CACHE_DIR
// (aka /var/cache/tailscale). We want to move files into the
// provided dir, preferring those in CACHE_DIR over those in / if
// both exist. If files already exist in dir, don't
// overwrite. Finally, once we've maybe moved files around, we
// want to delete leftovers in / and CACHE_DIR, to clean up after
// our past selves.
files := []string{
fmt.Sprintf("%s.log.conf", cmdname),
fmt.Sprintf("%s.log1.txt", cmdname),
fmt.Sprintf("%s.log2.txt", cmdname),
}
// checks if any of the files above exist in d.
checkExists := func(d string) (bool, error) {
for _, file := range files {
p := filepath.Join(d, file)
_, err := os.Stat(p)
if os.IsNotExist(err) {
continue
} else if err != nil {
return false, fmt.Errorf("stat %q: %w", p, err)
}
return true, nil
}
return false, nil
}
// move files from d into dir, if they exist.
moveFiles := func(d string) error {
for _, file := range files {
src := filepath.Join(d, file)
_, err := os.Stat(src)
if os.IsNotExist(err) {
continue
} else if err != nil {
return fmt.Errorf("stat %q: %v", src, err)
}
dst := filepath.Join(dir, file)
bs, err := exec.Command("mv", src, dst).CombinedOutput()
if err != nil {
return fmt.Errorf("mv %q %q: %v (%s)", src, dst, err, bs)
}
}
return nil
}
existsInRoot, err := checkExists("/")
if err != nil {
logf("checking for configs in /: %v", err)
return
}
existsInCache := false
cacheDir := os.Getenv("CACHE_DIRECTORY")
if cacheDir != "" {
existsInCache, err = checkExists("/var/cache/tailscale")
if err != nil {
logf("checking for configs in %s: %v", cacheDir, err)
}
}
existsInDest, err := checkExists(dir)
if err != nil {
logf("checking for configs in %s: %v", dir, err)
return
}
switch {
case !existsInRoot && !existsInCache:
// No leftover files, nothing to do.
return
case existsInDest:
// Already have "canonical" configs, just delete any remnants
// (below).
case existsInCache:
// CACHE_DIRECTORY takes precedence over /, move files from
// there.
if err := moveFiles(cacheDir); err != nil {
logf("%v", err)
return
}
case existsInRoot:
// Files from root is better than nothing.
if err := moveFiles("/"); err != nil {
logf("%v", err)
return
}
}
// If moving succeeded, or we didn't need to move files, try to
// delete any leftover files, but it's okay if we can't delete
// them for some reason.
dirs := []string{}
if existsInCache {
dirs = append(dirs, cacheDir)
}
if existsInRoot {
dirs = append(dirs, "/")
}
for _, d := range dirs {
for _, file := range files {
p := filepath.Join(d, file)
_, err := os.Stat(p)
if os.IsNotExist(err) {
continue
} else if err != nil {
logf("stat %q: %v", p, err)
return
}
if err := os.Remove(p); err != nil {
logf("rm %q: %v", p, err)
}
}
}
}
// Deprecated: Use [Options.New] instead.
func New(collection string, netMon *netmon.Monitor, health *health.Tracker, logf logger.Logf) *Policy {
return Options{
Collection: collection,
NetMon: netMon,
Health: health,
Logf: logf,
}.New()
}
// Options is used to construct a [Policy].
type Options struct {
// Collection is a required collection to upload logs under.
// Collection is a namespace for the type logs.
// For example, logs for a node use "tailnode.log.tailscale.io".
Collection string
// Dir is an optional directory to store the log configuration.
// If empty, [LogsDir] is used.
Dir string
// CmdName is an optional name of the current binary.
// If empty, [version.CmdName] is used.
CmdName string
// NetMon is an optional parameter for monitoring.
// If non-nil, it's used to do faster interface lookups.
NetMon *netmon.Monitor
// Health is an optional parameter for health status.
// If non-nil, it's used to construct the default HTTP client.
Health *health.Tracker
// Bus is an optional parameter for communication on the eventbus.
// If non-nil, it's passed to logtail for use in interface monitoring.
// TODO(cmol): Make this non-optional when it's plumbed in by the clients.
Bus *eventbus.Bus
// Logf is an optional logger to use.
// If nil, [log.Printf] will be used instead.
Logf logger.Logf
// HTTPC is an optional client to use upload logs.
// If nil, [TransportOptions.New] is used to construct a new client
// with that particular transport sending logs to the default logs server.
HTTPC *http.Client
// MaxBufferSize is the maximum size of the log buffer.
// This controls the amount of logs that can be temporarily stored
// before the logs can be successfully upload.
// If zero, a default buffer size is chosen.
MaxBufferSize int
// MaxUploadSize is the maximum size per upload.
// This should only be set by clients that have been authenticated
// with the logging service as having a higher upload limit.
// If zero, a default upload size is chosen.
MaxUploadSize int
}
// init initializes the log policy and returns a logtail.Config and the
// Policy.
func (opts Options) init(disableLogging bool) (*logtail.Config, *Policy) {
if hostinfo.IsNATLabGuestVM() {
// In NATLab Gokrazy instances, tailscaled comes up concurently with
// DHCP and the doesn't have DNS for a while. Wait for DHCP first.
awaitGokrazyNetwork()
}
var lflags int
if term.IsTerminal(2) || runtime.GOOS == "windows" {
lflags = 0
} else {
lflags = log.LstdFlags
}
if envknob.Bool("TS_DEBUG_LOG_TIME") {
lflags = log.LstdFlags | log.Lmicroseconds
}
if runningUnderSystemd() {
// If journalctl is going to prepend its own timestamp
// anyway, no need to add one.
lflags = 0
}
var conWriter io.Writer = stderrWriter{}
if buildfeatures.HasSyslog {
if f, ok := feature.HookLogSink.GetOk(); ok {
if w := f(); w != nil {
// Logs are being redirected elsewhere (e.g. to syslog,
// which records its own timestamps).
conWriter = w
lflags = 0
}
}
}
console := log.New(conWriter, "", lflags)
var earlyErrBuf bytes.Buffer
earlyLogf := func(format string, a ...any) {
fmt.Fprintf(&earlyErrBuf, format, a...)
earlyErrBuf.WriteByte('\n')
}
if opts.Dir == "" {
opts.Dir = LogsDir(earlyLogf)
}
if opts.CmdName == "" {
opts.CmdName = version.CmdName()
}
useStdLogger := opts.Logf == nil
if useStdLogger {
opts.Logf = log.Printf
}
tryFixLogStateLocation(opts.Dir, opts.CmdName, opts.Logf)
cfgPath := filepath.Join(opts.Dir, fmt.Sprintf("%s.log.conf", opts.CmdName))
if runtime.GOOS == "windows" {
switch opts.CmdName {
case "tailscaled":
// Tailscale 1.14 and before stored state under %LocalAppData%
// (usually "C:\WINDOWS\system32\config\systemprofile\AppData\Local"
// when tailscaled.exe is running as a non-user system service).
// However it is frequently cleared for almost any reason: Windows
// updates, System Restore, even various System Cleaner utilities.
//
// The Windows service previously ran as tailscale-ipn.exe, so
// machines which ran very old versions might still have their
// log conf named %LocalAppData%\tailscale-ipn.log.conf
//
// Machines which started using Tailscale more recently will have
// %LocalAppData%\tailscaled.log.conf
//
// Attempt to migrate the log conf to C:\ProgramData\Tailscale
oldDir := filepath.Join(os.Getenv("LocalAppData"), "Tailscale")
oldPath := filepath.Join(oldDir, "tailscaled.log.conf")
if fi, err := os.Stat(oldPath); err != nil || !fi.Mode().IsRegular() {
// *Only* if tailscaled.log.conf does not exist,
// check for tailscale-ipn.log.conf
oldPathOldCmd := filepath.Join(oldDir, "tailscale-ipn.log.conf")
if fi, err := os.Stat(oldPathOldCmd); err == nil && fi.Mode().IsRegular() {
oldPath = oldPathOldCmd
}
}
cfgPath = paths.TryConfigFileMigration(earlyLogf, oldPath, cfgPath)
case "tailscale-ipn":
for _, oldBase := range []string{"wg64.log.conf", "wg32.log.conf"} {
oldConf := filepath.Join(opts.Dir, oldBase)
if fi, err := os.Stat(oldConf); err == nil && fi.Mode().IsRegular() {
cfgPath = paths.TryConfigFileMigration(earlyLogf, oldConf, cfgPath)
break
}
}
}
}
newc, err := ConfigFromFile(cfgPath)
if err != nil {
earlyLogf("logpolicy.ConfigFromFile %v: %v", cfgPath, err)
}
if err := newc.Validate(opts.Collection); err != nil {
earlyLogf("logpolicy.Config.Validate for %v: %v", cfgPath, err)
newc = NewConfig(opts.Collection)
if err := newc.Save(cfgPath); err != nil {
earlyLogf("logpolicy.Config.Save for %v: %v", cfgPath, err)
}
}
conf := logtail.Config{
Collection: newc.Collection,
PrivateID: newc.PrivateID,
Stderr: logWriter{console},
CompressLogs: true,
MaxUploadSize: opts.MaxUploadSize,
Bus: opts.Bus,
}
if opts.Collection == logtail.CollectionNode {
conf.MetricsDelta = clientmetric.EncodeLogTailMetricsDelta
conf.IncludeProcID = true
conf.IncludeProcSequence = true
}
if disableLogging {
opts.Logf("You have disabled logging. Tailscale will not be able to provide support.")
conf.HTTPC = &http.Client{Transport: noopPretendSuccessTransport{}}
} else {
// Only attach an on-disk filch buffer if we are going to be sending logs.
// No reason to persist them locally just to drop them later.
attachFilchBuffer(&conf, opts.Dir, opts.CmdName, opts.MaxBufferSize, opts.Logf)
conf.HTTPC = opts.HTTPC
logHost := logtail.DefaultHost
if val := getLogTarget(); val != "" {
u, err := url.Parse(val)
if err != nil {
opts.Logf("logpolicy: invalid TS_LOG_TARGET %q: %v; using default log host", val, err)
} else if u.Host == "" {
opts.Logf("logpolicy: invalid TS_LOG_TARGET %q: missing host; using default log host", val)
} else {
opts.Logf("You have enabled a non-default log target. Doing without being told to by Tailscale staff or your network administrator will make getting support difficult.")
conf.BaseURL = val
logHost = u.Host
}
}
if conf.HTTPC == nil {
conf.HTTPC = &http.Client{Transport: TransportOptions{
Host: logHost,
NetMon: opts.NetMon,
Health: opts.Health,
Logf: opts.Logf,
}.New()}
}
}
lw := logtail.NewLogger(conf, opts.Logf)
var logOutput io.Writer = lw
if runtime.GOOS == "windows" && conf.Collection == logtail.CollectionNode {
logID := newc.PublicID.String()
exe, _ := os.Executable()
if strings.EqualFold(filepath.Base(exe), "tailscaled.exe") {
diskLogf := filelogger.New("tailscale-service", logID, lw.Logf)
logOutput = logger.FuncWriter(diskLogf)
}
}
if useStdLogger {
log.SetFlags(0) // other log flags are set on console, not here
log.SetOutput(logOutput)
}
opts.Logf("Program starting: v%v, Go %v: %#v",
version.Long(),
goVersion(),
os.Args)
opts.Logf("LogID: %v", newc.PublicID)
if earlyErrBuf.Len() != 0 {
opts.Logf("%s", earlyErrBuf.Bytes())
}
return &conf, &Policy{
Logtail: lw,
PublicID: newc.PublicID,
Logf: opts.Logf,
}
}
// New returns a new log policy (a logger and its instance ID).
func (opts Options) New() *Policy {
disableLogging := envknob.NoLogsNoSupport() || testenv.InTest() || runtime.GOOS == "plan9" || !buildfeatures.HasLogTail
_, policy := opts.init(disableLogging)
return policy
}
// attachFilchBuffer creates an on-disk ring buffer using filch and attaches
// it to the logtail config. Note that this is optional; if no buffer is set,
// logtail will use an in-memory buffer.
func attachFilchBuffer(conf *logtail.Config, dir, cmdName string, maxFileSize int, logf logger.Logf) {
filchOptions := filch.Options{
ReplaceStderr: redirectStderrToLogPanics(),
MaxFileSize: maxFileSize,
}
filchPrefix := filepath.Join(dir, cmdName)
// NAS disks cannot hibernate if we're writing logs to them all the time.
// https://github.com/tailscale/tailscale/issues/3551
if runtime.GOOS == "linux" && (distro.Get() == distro.Synology || distro.Get() == distro.QNAP) {
tmpfsLogs := "/tmp/tailscale-logs"
if err := os.MkdirAll(tmpfsLogs, 0755); err == nil {
filchPrefix = filepath.Join(tmpfsLogs, cmdName)
filchOptions.MaxFileSize = 1 << 20
} else {
// not a fatal error, we can leave the log files on the spinning disk
logf("Unable to create /tmp directory for log storage: %v\n", err)
}
}
filchBuf, filchErr := filch.New(filchPrefix, filchOptions)
if filchBuf != nil {
conf.Buffer = filchBuf
if filchBuf.OrigStderr != nil {
conf.Stderr = filchBuf.OrigStderr
}
}
if filchErr != nil {
logf("filch failed: %v", filchErr)
}
}
// dialLog is used by NewLogtailTransport to log the happy path of its
// own dialing.
//
// By default it goes nowhere and is only enabled when
// tailscaled's in verbose mode.
//
// log.Printf isn't used so its own logs don't loop back into logtail
// in the happy path, thus generating more logs.
var dialLog = log.New(io.Discard, "logtail: ", log.LstdFlags|log.Lmsgprefix)
// SetVerbosityLevel controls the verbosity level that should be
// written to stderr. 0 is the default (not verbose). Levels 1 or higher
// are increasingly verbose.
//
// It should not be changed concurrently with log writes.
func (p *Policy) SetVerbosityLevel(level int) {
p.Logtail.SetVerbosityLevel(level)
if level > 0 {
dialLog.SetOutput(os.Stderr)
}
}
// Close immediately shuts down the logger.
func (p *Policy) Close() {
ctx, cancel := context.WithCancel(context.Background())
cancel()
p.Shutdown(ctx)
}
// Shutdown gracefully shuts down the logger, finishing any current
// log upload if it can be done before ctx is canceled.
func (p *Policy) Shutdown(ctx context.Context) error {
if p.Logtail != nil {
p.Logf("flushing log.")
return p.Logtail.Shutdown(ctx)
}
return nil
}
// MakeDialFunc creates a net.Dialer.DialContext function specialized for use
// by logtail.
// It does the following:
// - If DNS lookup fails, consults the bootstrap DNS list of Tailscale hostnames.
// - If TLS connection fails, try again using LetsEncrypt's built-in root certificate,
// for the benefit of older OS platforms which might not include it.
//
// The netMon parameter is optional. It should be specified in environments where
// Tailscaled is manipulating the routing table.
func MakeDialFunc(netMon *netmon.Monitor, logf logger.Logf) netx.DialFunc {
if netMon == nil {
netMon = netmon.NewStatic()
}
return func(ctx context.Context, netw, addr string) (net.Conn, error) {
return dialContext(ctx, netw, addr, netMon, logf)
}
}
func dialContext(ctx context.Context, netw, addr string, netMon *netmon.Monitor, logf logger.Logf) (net.Conn, error) {
nd := netns.FromDialer(logf, netMon, &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: netknob.PlatformTCPKeepAlive(),
})
t0 := time.Now()
c, err := nd.DialContext(ctx, netw, addr)
d := time.Since(t0).Round(time.Millisecond)
if err == nil {
dialLog.Printf("dialed %q in %v", addr, d)
return c, nil
}
if version.IsWindowsGUI() && strings.HasPrefix(netw, "tcp") {
if c, err := safesocket.ConnectContext(ctx, ""); err == nil {
fmt.Fprintf(c, "CONNECT %s HTTP/1.0\r\n\r\n", addr)
br := bufio.NewReader(c)
res, err := http.ReadResponse(br, nil)
if err == nil && res.StatusCode != 200 {
err = errors.New(res.Status)
}
if err != nil {
logf("logtail: CONNECT response error from tailscaled: %v", err)
c.Close()
} else {
dialLog.Printf("connected via tailscaled")
return c, nil
}
}
}
// If we failed to dial, try again with bootstrap DNS.
logf("logtail: dial %q failed: %v (in %v), trying bootstrap...", addr, err, d)
dnsCache := &dnscache.Resolver{
Forward: dnscache.Get().Forward, // use default cache's forwarder
UseLastGood: true,
LookupIPFallback: dnsfallback.MakeLookupFunc(logf, netMon),
}
dialer := dnscache.Dialer(nd.DialContext, dnsCache)
c, err = dialer(ctx, netw, addr)
if err == nil {
logf("logtail: bootstrap dial succeeded")
}
return c, err
}
// Deprecated: Use [TransportOptions.New] instead.
func NewLogtailTransport(host string, netMon *netmon.Monitor, health *health.Tracker, logf logger.Logf) http.RoundTripper {
return TransportOptions{Host: host, NetMon: netMon, Health: health, Logf: logf}.New()
}
// TransportOptions is used to construct an [http.RoundTripper].
type TransportOptions struct {
// Host is the optional hostname of the logs server.
// If empty, then [logtail.DefaultHost] is used.
Host string
// NetMon is an optional parameter for monitoring.
// If non-nil, it's used to do faster interface lookups.
NetMon *netmon.Monitor
// Health is an optional parameter for health status.
// If non-nil, it's used to construct the default HTTP client.
Health *health.Tracker
// Logf is an optional logger to use.
// If nil, [log.Printf] will be used instead.
Logf logger.Logf
// TLSClientConfig is an optional TLS configuration to use.
// If non-nil, the configuration will be cloned.
TLSClientConfig *tls.Config
}
// New returns an HTTP Transport particularly suited to uploading logs
// to the given host name. See [DialContext] for details on how it works.
func (opts TransportOptions) New() http.RoundTripper {
if testenv.InTest() || envknob.NoLogsNoSupport() {
return noopPretendSuccessTransport{}
}
if opts.NetMon == nil {
opts.NetMon = netmon.NewStatic()
}
// Start with a copy of http.DefaultTransport and tweak it a bit.
tr := netutil.NewDefaultTransport()
if opts.TLSClientConfig != nil {
tr.TLSClientConfig = opts.TLSClientConfig.Clone()
}
if buildfeatures.HasUseProxy {
tr.Proxy = feature.HookProxyFromEnvironment.GetOrNil()
if set, ok := feature.HookProxySetTransportGetProxyConnectHeader.GetOk(); ok {
set(tr)
}
}
// We do our own zstd compression on uploads, and responses never contain any payload,
// so don't send "Accept-Encoding: gzip" to save a few bytes on the wire, since there
// will never be any body to decompress:
tr.DisableCompression = true
// Log whenever we dial:
if opts.Logf == nil {
opts.Logf = log.Printf
}
tr.DialContext = MakeDialFunc(opts.NetMon, opts.Logf)
// We're uploading logs ideally infrequently, with specific timing that will
// change over time. Try to keep the connection open, to avoid repeatedly
// paying the cost of TLS setup.
tr.IdleConnTimeout = time.Hour
// We're contacting exactly 1 hostname, so the default's 100
// max idle conns is very high for our needs. Even 2 is
// probably double what we need:
tr.MaxIdleConns = 2
// Provide knob to force HTTP/1 for log uploads.
// TODO(bradfitz): remove this debug knob once we've decided
// to upload via HTTP/1 or HTTP/2 (probably HTTP/1). Or we might just enforce
// it server-side.
if envknob.Bool("TS_DEBUG_FORCE_H1_LOGS") {
tr.TLSClientConfig = nil // DefaultTransport's was already initialized w/ h2
tr.ForceAttemptHTTP2 = false
tr.TLSNextProto = map[string]func(authority string, c *tls.Conn) http.RoundTripper{}
}
tr.TLSClientConfig = tlsdial.Config(opts.Health, tr.TLSClientConfig)
// Force TLS 1.3 since we know log.tailscale.com supports it.
tr.TLSClientConfig.MinVersion = tls.VersionTLS13
return tr
}
func goVersion() string {
v := strings.TrimPrefix(runtime.Version(), "go")
if racebuild.On {
return v + "-race"
}
return v
}
type noopPretendSuccessTransport struct{}
func (noopPretendSuccessTransport) RoundTrip(req *http.Request) (*http.Response, error) {
io.Copy(io.Discard, req.Body)
req.Body.Close()
return &http.Response{
StatusCode: 200,
Status: "200 OK",
}, nil
}
func awaitGokrazyNetwork() {
if runtime.GOOS != "linux" || distro.Get() != distro.Gokrazy {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
for {
// Before DHCP finishes, the /etc/resolv.conf file has just "#MANUAL".
all, _ := os.ReadFile("/etc/resolv.conf")
if bytes.Contains(all, []byte("nameserver ")) {
good := true
firstLine, _, ok := strings.Cut(string(all), "\n")
if ok {
ns, ok := strings.CutPrefix(firstLine, "nameserver ")
if ok {
if ip, err := netip.ParseAddr(ns); err == nil && ip.Is6() && !ip.IsLinkLocalUnicast() {
good = haveGlobalUnicastIPv6()
}
}
}
if good {
return
}
}
select {
case <-ctx.Done():
return
case <-time.After(500 * time.Millisecond):
}
}
}
// haveGlobalUnicastIPv6 reports whether the machine has a IPv6 non-private
// (non-ULA) global unicast address.
//
// It's only intended for use in natlab integration tests so only works on
// Linux/macOS now and not environments (such as Android) where net.Interfaces
// doesn't work directly.
func haveGlobalUnicastIPv6() bool {
ifs, _ := net.Interfaces()
for _, ni := range ifs {
aa, _ := ni.Addrs()
for _, a := range aa {
ipn, ok := a.(*net.IPNet)
if !ok {
continue
}
ip, _ := netip.AddrFromSlice(ipn.IP)
if ip.Is6() && ip.IsGlobalUnicast() && !ip.IsPrivate() {
return true
}
}
}
return false
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_logtail
package logtail
import (
"bytes"
"errors"
"expvar"
"fmt"
"tailscale.com/metrics"
"tailscale.com/syncs"
)
type Buffer interface {
// TryReadLine tries to read a log line from the ring buffer.
// If no line is available it returns a nil slice.
// If the ring buffer is closed it returns io.EOF.
//
// The returned slice may point to data that will be overwritten
// by a subsequent call to TryReadLine.
TryReadLine() ([]byte, error)
// Write writes a log line into the ring buffer.
// Implementations must not retain the provided buffer.
Write([]byte) (int, error)
}
func NewMemoryBuffer(numEntries int) Buffer {
return &memBuffer{
pending: make(chan qentry, numEntries),
}
}
type memBuffer struct {
next []byte
pending chan qentry
dropMu syncs.Mutex
dropCount int
// Metrics (see [memBuffer.ExpVar] for details).
writeCalls expvar.Int
readCalls expvar.Int
writeBytes expvar.Int
readBytes expvar.Int
droppedBytes expvar.Int
storedBytes expvar.Int
}
// ExpVar returns a [metrics.Set] with metrics about the buffer.
//
// - counter_write_calls: Total number of write calls.
// - counter_read_calls: Total number of read calls.
// - counter_write_bytes: Total number of bytes written.
// - counter_read_bytes: Total number of bytes read.
// - counter_dropped_bytes: Total number of bytes dropped.
// - gauge_stored_bytes: Current number of bytes stored in memory.
func (b *memBuffer) ExpVar() expvar.Var {
m := new(metrics.Set)
m.Set("counter_write_calls", &b.writeCalls)
m.Set("counter_read_calls", &b.readCalls)
m.Set("counter_write_bytes", &b.writeBytes)
m.Set("counter_read_bytes", &b.readBytes)
m.Set("counter_dropped_bytes", &b.droppedBytes)
m.Set("gauge_stored_bytes", &b.storedBytes)
return m
}
func (m *memBuffer) TryReadLine() ([]byte, error) {
m.readCalls.Add(1)
if m.next != nil {
msg := m.next
m.next = nil
m.readBytes.Add(int64(len(msg)))
m.storedBytes.Add(-int64(len(msg)))
return msg, nil
}
select {
case ent := <-m.pending:
if ent.dropCount > 0 {
m.next = ent.msg
b := fmt.Appendf(nil, "----------- %d logs dropped ----------", ent.dropCount)
m.writeBytes.Add(int64(len(b))) // indicate pseudo-injected log message
m.readBytes.Add(int64(len(b)))
return b, nil
}
m.readBytes.Add(int64(len(ent.msg)))
m.storedBytes.Add(-int64(len(ent.msg)))
return ent.msg, nil
default:
return nil, nil
}
}
func (m *memBuffer) Write(b []byte) (int, error) {
m.writeCalls.Add(1)
m.dropMu.Lock()
defer m.dropMu.Unlock()
ent := qentry{
msg: bytes.Clone(b),
dropCount: m.dropCount,
}
select {
case m.pending <- ent:
m.writeBytes.Add(int64(len(b)))
m.storedBytes.Add(+int64(len(b)))
m.dropCount = 0
return len(b), nil
default:
m.dropCount++
m.droppedBytes.Add(int64(len(b)))
return 0, errBufferFull
}
}
type qentry struct {
msg []byte
dropCount int
}
var errBufferFull = errors.New("logtail: buffer full")
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_logtail
// Package filch is a file system queue that pilfers your stderr.
// (A FILe CHannel that filches.)
package filch
import (
"bytes"
"cmp"
"errors"
"expvar"
"fmt"
"io"
"os"
"slices"
"sync"
"tailscale.com/metrics"
"tailscale.com/util/must"
)
var stderrFD = 2 // a variable for testing
var errTooLong = errors.New("filch: line too long")
var errClosed = errors.New("filch: buffer is closed")
const DefaultMaxLineSize = 64 << 10
const DefaultMaxFileSize = 50 << 20
type Options struct {
// ReplaceStderr specifies whether to filch [os.Stderr] such that
// everything written there appears in the [Filch] buffer instead.
// In order to write to stderr instead of writing to [Filch],
// then use [Filch.OrigStderr].
ReplaceStderr bool
// MaxLineSize is the maximum line size that could be encountered,
// including the trailing newline. This is enforced as a hard limit.
// Writes larger than this will be rejected. Reads larger than this
// will report an error and skip over the long line.
// If zero, the [DefaultMaxLineSize] is used.
MaxLineSize int
// MaxFileSize specifies the maximum space on disk to use for logs.
// This is not enforced as a hard limit, but rather a soft limit.
// If zero, then [DefaultMaxFileSize] is used.
MaxFileSize int
}
// A Filch uses two alternating files as a simplistic ring buffer.
type Filch struct {
// OrigStderr is the original [os.Stderr] if [Options.ReplaceStderr] is specified.
// Writing directly to this avoids writing into the Filch buffer.
// Otherwise, it is nil.
OrigStderr *os.File
// maxLineSize specifies the maximum line size to use.
maxLineSize int // immutable once set
// maxFileSize specifies the max space either newer and older should use.
maxFileSize int64 // immutable once set
mu sync.Mutex
newer *os.File // newer logs data; writes are appended to the end
older *os.File // older logs data; reads are consumed from the start
newlyWrittenBytes int64 // bytes written directly to newer; reset upon rotation
newlyFilchedBytes int64 // bytes filched indirectly to newer; reset upon rotation
wrBuf []byte // temporary buffer for writing; only used for writes without trailing newline
wrBufMaxLen int // maximum length of wrBuf; reduced upon every rotation
rdBufIdx int // index into rdBuf for the next unread bytes
rdBuf []byte // temporary buffer for reading
rdBufMaxLen int // maximum length of rdBuf; reduced upon every rotation
// Metrics (see [Filch.ExpVar] for details).
writeCalls expvar.Int
readCalls expvar.Int
rotateCalls expvar.Int
callErrors expvar.Int
writeBytes expvar.Int
readBytes expvar.Int
filchedBytes expvar.Int
droppedBytes expvar.Int
storedBytes expvar.Int
}
// ExpVar returns a [metrics.Set] with metrics about the buffer.
//
// - counter_write_calls: Total number of calls to [Filch.Write]
// (excludes calls when file is closed).
//
// - counter_read_calls: Total number of calls to [Filch.TryReadLine]
// (excludes calls when file is closed or no bytes).
//
// - counter_rotate_calls: Total number of calls to rotate the log files
// (excludes calls when there is nothing to rotate to).
//
// - counter_call_errors: Total number of calls returning errors.
//
// - counter_write_bytes: Total number of bytes written
// (includes bytes filched from stderr).
//
// - counter_read_bytes: Total number of bytes read
// (includes bytes filched from stderr).
//
// - counter_filched_bytes: Total number of bytes filched from stderr.
//
// - counter_dropped_bytes: Total number of bytes dropped
// (includes bytes filched from stderr and lines too long to read).
//
// - gauge_stored_bytes: Current number of bytes stored on disk.
func (f *Filch) ExpVar() expvar.Var {
m := new(metrics.Set)
m.Set("counter_write_calls", &f.writeCalls)
m.Set("counter_read_calls", &f.readCalls)
m.Set("counter_rotate_calls", &f.rotateCalls)
m.Set("counter_call_errors", &f.callErrors)
m.Set("counter_write_bytes", &f.writeBytes)
m.Set("counter_read_bytes", &f.readBytes)
m.Set("counter_filched_bytes", &f.filchedBytes)
m.Set("counter_dropped_bytes", &f.droppedBytes)
m.Set("gauge_stored_bytes", &f.storedBytes)
return m
}
func (f *Filch) unreadReadBuffer() []byte {
return f.rdBuf[f.rdBufIdx:]
}
func (f *Filch) availReadBuffer() []byte {
return f.rdBuf[len(f.rdBuf):cap(f.rdBuf)]
}
func (f *Filch) resetReadBuffer() {
f.rdBufIdx, f.rdBuf = 0, f.rdBuf[:0]
}
func (f *Filch) moveReadBufferToFront() {
f.rdBufIdx, f.rdBuf = 0, f.rdBuf[:copy(f.rdBuf, f.rdBuf[f.rdBufIdx:])]
}
func (f *Filch) growReadBuffer() {
f.rdBuf = slices.Grow(f.rdBuf, cap(f.rdBuf)+1)
}
func (f *Filch) consumeReadBuffer(n int) {
f.rdBufIdx += n
}
func (f *Filch) appendReadBuffer(n int) {
f.rdBuf = f.rdBuf[:len(f.rdBuf)+n]
f.rdBufMaxLen = max(f.rdBufMaxLen, len(f.rdBuf))
}
// TryReadline implements the logtail.Buffer interface.
func (f *Filch) TryReadLine() (b []byte, err error) {
f.mu.Lock()
defer f.mu.Unlock()
if f.older == nil {
return nil, io.EOF
}
var tooLong bool // whether we are in a line that is too long
defer func() {
f.consumeReadBuffer(len(b))
if tooLong || len(b) > f.maxLineSize {
f.droppedBytes.Add(int64(len(b)))
b, err = nil, cmp.Or(err, errTooLong)
} else {
f.readBytes.Add(int64(len(b)))
}
if len(b) != 0 || err != nil {
f.readCalls.Add(1)
}
if err != nil {
f.callErrors.Add(1)
}
}()
for {
// Check if unread buffer already has the next line.
unread := f.unreadReadBuffer()
if i := bytes.IndexByte(unread, '\n') + len("\n"); i > 0 {
return unread[:i], nil
}
// Check whether to make space for more data to read.
avail := f.availReadBuffer()
if len(avail) == 0 {
switch {
case len(unread) > f.maxLineSize:
tooLong = true
f.droppedBytes.Add(int64(len(unread)))
f.resetReadBuffer()
case len(unread) < cap(f.rdBuf)/10:
f.moveReadBufferToFront()
default:
f.growReadBuffer()
}
avail = f.availReadBuffer() // invariant: len(avail) > 0
}
// Read data into the available buffer.
n, err := f.older.Read(avail)
f.appendReadBuffer(n)
if err != nil {
if err == io.EOF {
unread = f.unreadReadBuffer()
if len(unread) == 0 {
if err := f.rotateLocked(); err != nil {
return nil, err
}
if f.storedBytes.Value() == 0 {
return nil, nil
}
continue
}
return unread, nil
}
return nil, err
}
}
}
var alwaysStatForTests bool
// Write implements the logtail.Buffer interface.
func (f *Filch) Write(b []byte) (n int, err error) {
f.mu.Lock()
defer f.mu.Unlock()
if f.newer == nil {
return 0, errClosed
}
defer func() {
f.writeCalls.Add(1)
if err != nil {
f.callErrors.Add(1)
}
}()
// To make sure we do not write data to disk unbounded
// (in the event that we are not draining fast enough)
// check whether we exceeded maxFileSize.
// If so, then force a file rotation.
if f.newlyWrittenBytes+f.newlyFilchedBytes > f.maxFileSize || f.writeCalls.Value()%100 == 0 || alwaysStatForTests {
f.statAndUpdateBytes()
if f.newlyWrittenBytes+f.newlyFilchedBytes > f.maxFileSize {
if err := f.rotateLocked(); err != nil {
return 0, err
}
}
}
// Write the log entry (appending a newline character if needed).
var newline string
if len(b) == 0 || b[len(b)-1] != '\n' {
newline = "\n"
f.wrBuf = append(append(f.wrBuf[:0], b...), newline...)
f.wrBufMaxLen = max(f.wrBufMaxLen, len(f.wrBuf))
b = f.wrBuf
}
if len(b) > f.maxLineSize {
for line := range bytes.Lines(b) {
if len(line) > f.maxLineSize {
return 0, errTooLong
}
}
}
n, err = f.newer.Write(b)
f.writeBytes.Add(int64(n))
f.storedBytes.Add(int64(n))
f.newlyWrittenBytes += int64(n)
return n - len(newline), err // subtract possibly appended newline
}
func (f *Filch) statAndUpdateBytes() {
if fi, err := f.newer.Stat(); err == nil {
prevSize := f.newlyWrittenBytes + f.newlyFilchedBytes
filchedBytes := max(0, fi.Size()-prevSize)
f.writeBytes.Add(filchedBytes)
f.filchedBytes.Add(filchedBytes)
f.storedBytes.Add(filchedBytes)
f.newlyFilchedBytes += filchedBytes
}
}
func (f *Filch) storedBytesForTest() int64 {
return must.Get(f.newer.Stat()).Size() + must.Get(f.older.Stat()).Size()
}
var activeStderrWriteForTest sync.RWMutex
// stderrWriteForTest calls [os.Stderr.Write], but respects calls to [waitIdleStderrForTest].
func stderrWriteForTest(b []byte) int {
activeStderrWriteForTest.RLock()
defer activeStderrWriteForTest.RUnlock()
return must.Get(os.Stderr.Write(b))
}
// rotateLocked swaps f.newer and f.older such that:
//
// - f.newer will be truncated and future writes will be appended to the end.
// - if [Options.ReplaceStderr], then stderr writes will redirect to f.newer
// - f.older will contain historical data, reads will consume from the start.
// - f.older is guaranteed to be immutable.
//
// There are two reasons for rotating:
//
// - The reader finished reading f.older.
// No data should be lost under this condition.
//
// - The writer exceeded a limit for f.newer.
// Data may be lost under this condition.
func (f *Filch) rotateLocked() error {
f.rotateCalls.Add(1)
// Truncate the older file.
if fi, err := f.older.Stat(); err != nil {
return err
} else if fi.Size() > 0 {
// Update dropped bytes.
if pos, err := f.older.Seek(0, io.SeekCurrent); err == nil {
rdPos := pos - int64(len(f.unreadReadBuffer())) // adjust for data already read into the read buffer
f.droppedBytes.Add(max(0, fi.Size()-rdPos))
}
// Truncate the older file and write relative to the start.
if err := f.older.Truncate(0); err != nil {
return err
}
if _, err := f.older.Seek(0, io.SeekStart); err != nil {
return err
}
}
f.resetReadBuffer()
// Swap newer and older.
f.newer, f.older = f.older, f.newer
// If necessary, filch stderr into newer instead of older.
// This must be done after truncation otherwise
// we might lose some stderr data asynchronously written
// right in the middle of a rotation.
// Note that mutex does not prevent stderr writes.
prevSize := f.newlyWrittenBytes + f.newlyFilchedBytes
f.newlyWrittenBytes, f.newlyFilchedBytes = 0, 0
// Hold the write lock around dup2 to prevent concurrent
// stderrWriteForTest calls from racing with dup2 on the same fd.
// On macOS, dup2 and write are not atomic with respect to each other,
// so a concurrent write can observe a bad file descriptor.
activeStderrWriteForTest.Lock()
if f.OrigStderr != nil {
if err := dup2Stderr(f.newer); err != nil {
activeStderrWriteForTest.Unlock()
return err
}
}
// Update filched bytes and stored bytes metrics.
// This must be done after filching to newer
// so that f.older.Stat is *mostly* stable.
//
// NOTE: Unfortunately, an asynchronous os.Stderr.Write call
// that is already in progress when we called dup2Stderr
// will still write to the previous FD and
// may not be immediately observable by this Stat call.
// This is fundamentally unsolvable with the current design
// as we cannot synchronize all other os.Stderr.Write calls.
// In rare cases, it is possible that [Filch.TryReadLine] consumes
// the entire older file before the write commits,
// leading to dropped stderr lines.
fi, err := f.older.Stat()
activeStderrWriteForTest.Unlock()
if err != nil {
return err
}
filchedBytes := max(0, fi.Size()-prevSize)
f.writeBytes.Add(filchedBytes)
f.filchedBytes.Add(filchedBytes)
f.storedBytes.Set(fi.Size()) // newer has been truncated, so only older matters
// Start reading from the start of older.
if _, err := f.older.Seek(0, io.SeekStart); err != nil {
return err
}
// Garbage collect unnecessarily large buffers.
mayGarbageCollect := func(b []byte, maxLen int) ([]byte, int) {
if cap(b)/4 > maxLen { // if less than 25% utilized
b = slices.Grow([]byte(nil), 2*maxLen)
}
maxLen = 3 * (maxLen / 4) // reduce by 25%
return b, maxLen
}
f.wrBuf, f.wrBufMaxLen = mayGarbageCollect(f.wrBuf, f.wrBufMaxLen)
f.rdBuf, f.rdBufMaxLen = mayGarbageCollect(f.rdBuf, f.rdBufMaxLen)
return nil
}
// Close closes the Filch, releasing all resources.
func (f *Filch) Close() error {
f.mu.Lock()
defer f.mu.Unlock()
var errUnsave, errCloseNew, errCloseOld error
if f.OrigStderr != nil {
errUnsave = unsaveStderr(f.OrigStderr)
f.OrigStderr = nil
}
if f.newer != nil {
errCloseNew = f.newer.Close()
f.newer = nil
}
if f.older != nil {
errCloseOld = f.older.Close()
f.older = nil
}
return errors.Join(errUnsave, errCloseNew, errCloseOld)
}
// New creates a new filch around two log files, each starting with filePrefix.
func New(filePrefix string, opts Options) (f *Filch, err error) {
var f1, f2 *os.File
defer func() {
if err != nil {
if f1 != nil {
f1.Close()
}
if f2 != nil {
f2.Close()
}
err = fmt.Errorf("filch: %s", err)
}
}()
path1 := filePrefix + ".log1.txt"
path2 := filePrefix + ".log2.txt"
f1, err = os.OpenFile(path1, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return nil, err
}
f2, err = os.OpenFile(path2, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return nil, err
}
fi1, err := f1.Stat()
if err != nil {
return nil, err
}
fi2, err := f2.Stat()
if err != nil {
return nil, err
}
f = new(Filch)
f.maxLineSize = int(cmp.Or(max(0, opts.MaxLineSize), DefaultMaxLineSize))
f.maxFileSize = int64(cmp.Or(max(0, opts.MaxFileSize), DefaultMaxFileSize))
f.maxFileSize /= 2 // since there are two log files that combine to equal MaxFileSize
// Neither, either, or both files may exist and contain logs from
// the last time the process ran. The three cases are:
//
// - neither: all logs were read out and files were truncated
// - either: logs were being written into one of the files
// - both: the files were swapped and were starting to be
// read out, while new logs streamed into the other
// file, but the read out did not complete
switch {
case fi1.Size() > 0 && fi2.Size() == 0:
f.newer, f.older = f2, f1 // use empty file as newer
case fi2.Size() > 0 && fi1.Size() == 0:
f.newer, f.older = f1, f2 // use empty file as newer
case fi1.ModTime().Before(fi2.ModTime()):
f.newer, f.older = f2, f1 // use older file as older
case fi2.ModTime().Before(fi1.ModTime()):
f.newer, f.older = f1, f2 // use newer file as newer
default:
f.newer, f.older = f1, f2 // does not matter
}
f.writeBytes.Set(fi1.Size() + fi2.Size())
f.storedBytes.Set(fi1.Size() + fi2.Size())
if fi, err := f.newer.Stat(); err == nil {
f.newlyWrittenBytes = fi.Size()
}
f.OrigStderr = nil
if opts.ReplaceStderr {
f.OrigStderr, err = saveStderr()
if err != nil {
return nil, err
}
if err := dup2Stderr(f.newer); err != nil {
return nil, err
}
}
return f, nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_logtail && !windows && !wasm && !plan9 && !tamago
package filch
import (
"os"
"golang.org/x/sys/unix"
)
const replaceStderrSupportedForTest = true
func saveStderr() (*os.File, error) {
fd, err := unix.Dup(stderrFD)
if err != nil {
return nil, err
}
return os.NewFile(uintptr(fd), "stderr"), nil
}
func unsaveStderr(f *os.File) error {
err := dup2Stderr(f)
f.Close()
return err
}
func dup2Stderr(f *os.File) error {
return unix.Dup2(int(f.Fd()), stderrFD)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_logtail
// Package logtail sends logs to log.tailscale.com.
package logtail
import (
"bytes"
"cmp"
"context"
"crypto/rand"
"encoding/binary"
"expvar"
"fmt"
"io"
"iter"
"log"
mrand "math/rand/v2"
"net/http"
"os"
"runtime"
"slices"
"strconv"
"sync"
"sync/atomic"
"time"
"github.com/creachadair/msync/trigger"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"tailscale.com/envknob"
"tailscale.com/metrics"
"tailscale.com/net/netmon"
"tailscale.com/net/sockstats"
"tailscale.com/tstime"
tslogger "tailscale.com/types/logger"
"tailscale.com/types/logid"
"tailscale.com/util/eventbus"
"tailscale.com/util/set"
"tailscale.com/util/truncate"
"tailscale.com/util/zstdframe"
)
// maxSize is the maximum size that a single log entry can be.
// It is also the maximum body size that may be uploaded at a time.
const maxSize = 256 << 10
// maxTextSize is the maximum size for a text log message.
// Note that JSON log messages can be as large as maxSize.
const maxTextSize = 16 << 10
// lowMemRatio reduces maxSize and maxTextSize by this ratio in lowMem mode.
const lowMemRatio = 4
// bufferSize is the typical buffer size to retain.
// It is large enough to handle most log messages,
// but not too large to be a notable waste of memory if retained forever.
const bufferSize = 4 << 10
// newLogger constructs a *Logger from cfg, applying defaults, but does not start
// the background uploading goroutine. It is shared by [NewLogger] and the
// stateless [UploadLogs].
func newLogger(cfg Config) *Logger {
if cfg.BaseURL == "" {
cfg.BaseURL = "https://" + DefaultHost
}
if cfg.HTTPC == nil {
cfg.HTTPC = http.DefaultClient
}
if cfg.Clock == nil {
cfg.Clock = tstime.StdClock{}
}
if cfg.Stderr == nil {
cfg.Stderr = os.Stderr
}
if cfg.Buffer == nil {
pendingSize := 256
if cfg.LowMemory {
pendingSize = 64
}
cfg.Buffer = NewMemoryBuffer(pendingSize)
}
var procID uint32
if cfg.IncludeProcID {
keyBytes := make([]byte, 4)
rand.Read(keyBytes)
procID = binary.LittleEndian.Uint32(keyBytes)
if procID == 0 {
// 0 is the empty/off value, assign a different (non-zero) value to
// make sure we still include an ID (actual value does not matter).
procID = 7
}
}
if s := envknob.String("TS_DEBUG_LOGTAIL_FLUSHDELAY"); s != "" {
if delay, err := time.ParseDuration(s); err == nil {
cfg.FlushDelayFn = func() time.Duration { return delay }
} else {
log.Fatalf("invalid TS_DEBUG_LOGTAIL_FLUSHDELAY: %v", err)
}
} else if cfg.FlushDelayFn == nil && envknob.Bool("IN_TS_TEST") {
cfg.FlushDelayFn = func() time.Duration { return 0 }
}
var urlSuffix string
if !cfg.CopyPrivateID.IsZero() {
urlSuffix = "?copyId=" + cfg.CopyPrivateID.String()
}
logger := &Logger{
privateID: cfg.PrivateID,
stderr: cfg.Stderr,
stderrLevel: int64(cfg.StderrLevel),
httpc: cfg.HTTPC,
url: cfg.BaseURL + "/c/" + cfg.Collection + "/" + cfg.PrivateID.String() + urlSuffix,
lowMem: cfg.LowMemory,
buffer: cfg.Buffer,
maxUploadSize: cfg.MaxUploadSize,
skipClientTime: cfg.SkipClientTime,
drainWake: make(chan struct{}, 1),
sentinel: make(chan int32, 16),
flushDelayFn: cfg.FlushDelayFn,
clock: cfg.Clock,
metricsDelta: cfg.MetricsDelta,
procID: procID,
includeProcSequence: cfg.IncludeProcSequence,
shutdownStart: make(chan struct{}),
shutdownDone: make(chan struct{}),
}
if cfg.Bus != nil {
logger.eventClient = cfg.Bus.Client("logtail.Logger")
// Subscribe to change deltas from NetMon to detect when the network comes up.
eventbus.SubscribeFunc(logger.eventClient, logger.onChangeDelta)
}
logger.SetSockstatsLabel(sockstats.LabelLogtailLogger)
logger.compressLogs = cfg.CompressLogs
logger.disabled.Store(cfg.Disabled)
return logger
}
// NewLogger returns a new Logger that splits logs as configured between local
// logging facilities and uploading to a log server in the background.
func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
logger := newLogger(cfg)
ctx, cancel := context.WithCancel(context.Background())
logger.uploadCancel = cancel
go logger.uploading(ctx)
if envknob.Bool("TS_DEBUG_LOGTAIL") {
logger.Write([]byte("logtail started"))
}
return logger
}
// Logtail is the reserved "logtail" metadata member of a [LogEntry].
//
// Zero-valued fields are omitted when an entry is uploaded. [UploadLogs] fills
// in any fields the caller leaves zero from its [Config] (e.g. ClientTime and
// ProcID), so most callers can leave this empty.
type Logtail struct {
// ClientTime is the time the entry was generated. If zero, [UploadLogs]
// fills it with the current time unless Config.SkipClientTime is set.
ClientTime time.Time `json:"client_time,omitzero"`
// ProcID is an ephemeral process identifier; see Config.IncludeProcID.
ProcID uint32 `json:"proc_id,omitzero"`
// ProcSeq is an ephemeral per-process sequence number; see
// Config.IncludeProcSequence.
ProcSeq uint64 `json:"proc_seq,omitzero"`
}
// LogEntry is a single log entry to be uploaded via [UploadLogs].
//
// It marshals to a JSON object whose reserved "logtail" member carries the
// metadata in Logtail and whose remaining members are taken from Value, which
// is inlined at the top level alongside "logtail".
//
// Value must marshal to a JSON object: T must be a Go struct (or pointer to
// one), a Go map with a string key, or a [jsontext.Value] holding an object
// (for example jsontext.Value(`{"text":"hello"}`)). This is enforced when the
// entry is uploaded, not at compile time. Use T = [jsontext.Value] to mix
// differently-shaped payloads in a single upload.
type LogEntry[T any] struct {
Logtail Logtail `json:"logtail,omitzero"`
// The `inline` tag option was renamed to `embed` in Go 1.27's
// encoding/json/v2, but the pinned go-json-experiment module
// (used on older Go versions) only knows `inline`. Each
// implementation ignores the option it doesn't know, so specify
// both until we require Go 1.27 and drop `inline`.
//
//lint:ignore SA5008 staticcheck doesn't know Go 1.27's `embed` option yet
Value T `json:",inline,embed"`
}
// UploadLogs uploads entries to the log server described by conf and returns
// once they have all been sent (or an upload fails). It returns the number of
// entries that were successfully uploaded.
//
// It is a stateless alternative to [NewLogger] for callers that just want to
// push a batch of log entries without the background uploader, ring buffer,
// stderr echoing, or network-up gating that a [Logger] provides. Each entry is
// marshaled to JSON, its [Logtail] metadata is filled in from conf where the
// caller left it zero (honoring conf.SkipClientTime, conf.IncludeProcID, and
// conf.IncludeProcSequence), and entries are batched up to the server's maximum
// upload size and POSTed synchronously (compressed when conf.CompressLogs is
// set).
//
// Unlike [Logger], UploadLogs does not retry: if a batch fails to upload it
// returns the count of entries sent in prior batches and the error immediately,
// and any remaining entries are not sent. The conf.Stderr and conf.Bus fields
// are ignored.
func UploadLogs[T any](ctx context.Context, conf Config, entries iter.Seq[LogEntry[T]]) (int, error) {
conf.Stderr = io.Discard // pure uploader: never echo to stderr
conf.Bus = nil // no netmon/eventbus subscription for a one-shot
lg := newLogger(conf)
maxLen := cmp.Or(lg.maxUploadSize, maxSize)
if lg.lowMem {
maxLen /= lowMemRatio
}
// body accumulates a JSON array of encoded entries: "[e1,e2,...]".
// The framing mirrors Logger.drainPending.
body := make([]byte, 0, bufferSize) // reused across batches
body = append(body, '[')
// sent counts entries confirmed uploaded; pending counts entries
// accumulated in body but not yet uploaded. A successful sendBatch moves
// pending into sent.
var sent, pending int
sendBatch := func() error {
if len(body) <= len("[") {
return nil
}
out := bytes.TrimRight(body, ",")
out = append(out, ']')
origlen := -1 // sentinel value: uncompressed
// Don't attempt to compress tiny bodies; not worth the CPU cycles.
if lg.compressLogs && len(out) > 256 {
zbody := zstdframe.AppendEncode(nil, out,
zstdframe.FastestCompression, zstdframe.LowMemory(true))
// Only send it compressed if the bandwidth savings are sufficient.
if len(out)-len(zbody) > 64 {
origlen = len(out)
out = zbody
}
}
// upload is synchronous, so it is safe to reuse body's backing array
// for the next batch once upload returns.
if _, err := lg.upload(ctx, out, origlen); err != nil {
return err
}
body = body[:len("[")]
sent += pending
pending = 0
return nil
}
var procSeq uint64
for e := range entries {
if err := ctx.Err(); err != nil {
return sent, err
}
// Fill in metadata from conf, preserving any caller-set values.
if e.Logtail.ClientTime.IsZero() && !lg.skipClientTime {
e.Logtail.ClientTime = lg.clock.Now().UTC()
}
if e.Logtail.ProcID == 0 {
e.Logtail.ProcID = lg.procID
}
procSeq++
if lg.includeProcSequence {
e.Logtail.ProcSeq = procSeq
}
enc, err := jsonv2.Marshal(e)
if err != nil {
return sent, fmt.Errorf("logtail: encoding entry %d: %w", procSeq, err)
}
// Flush the current batch before adding an entry that would overflow it.
if len(body) > len("[") && len(body)+len(enc) > maxLen {
if err := sendBatch(); err != nil {
return sent, err
}
}
body = append(body, enc...)
body = append(body, ',')
pending++
}
err := sendBatch()
return sent, err
}
// Logger writes logs, splitting them as configured between local
// logging facilities and uploading to a log server.
type Logger struct {
stderr io.Writer
stderrLevel int64 // accessed atomically
httpc *http.Client
url string
lowMem bool
skipClientTime bool
netMonitor *netmon.Monitor
buffer Buffer
maxUploadSize int
drainWake chan struct{} // signal to speed up drain
drainBuf []byte // owned by drainPending for reuse
flushDelayFn func() time.Duration // negative or zero return value to upload aggressively, or >0 to batch at this delay
flushPending atomic.Bool
sentinel chan int32
clock tstime.Clock
compressLogs bool
uploadCancel func()
explainedRaw bool
metricsDelta func() string // or nil
privateID logid.PrivateID
httpDoCalls atomic.Int32
sockstatsLabel atomicSocktatsLabel
eventClient *eventbus.Client
networkIsUp trigger.Cond // set/reset by netmon.ChangeDelta events
procID uint32
includeProcSequence bool
// disabled, when true, causes this logger to drop incoming log entries
// without buffering or uploading. It is independent of the process-wide
// Disable kill switch, which takes precedence. Toggled by SetEnabled.
disabled atomic.Bool
writeLock sync.Mutex // guards procSequence, flushTimer, buffer.Write calls
procSequence uint64
flushTimer tstime.TimerController // used when flushDelay is >0
writeBuf [bufferSize]byte // owned by Write for reuse
bytesBuf bytes.Buffer // owned by appendTextOrJSONLocked for reuse
jsonDec jsontext.Decoder // owned by appendTextOrJSONLocked for reuse
shutdownStartMu sync.Mutex // guards the closing of shutdownStart
shutdownStart chan struct{} // closed when shutdown begins
shutdownDone chan struct{} // closed when shutdown complete
// Metrics (see [Logger.ExpVar] for details).
uploadCalls expvar.Int
failedCalls expvar.Int
uploadedBytes expvar.Int
uploadingTime expvar.Int
}
type atomicSocktatsLabel struct{ p atomic.Uint32 }
func (p *atomicSocktatsLabel) Load() sockstats.Label { return sockstats.Label(p.p.Load()) }
func (p *atomicSocktatsLabel) Store(label sockstats.Label) { p.p.Store(uint32(label)) }
// SetVerbosityLevel controls the verbosity level that should be
// written to stderr. 0 is the default (not verbose). Levels 1 or higher
// are increasingly verbose.
func (lg *Logger) SetVerbosityLevel(level int) {
atomic.StoreInt64(&lg.stderrLevel, int64(level))
}
// SetNetMon sets the network monitor.
//
// It should not be changed concurrently with log writes and should
// only be set once.
func (lg *Logger) SetNetMon(lm *netmon.Monitor) {
lg.netMonitor = lm
}
// SetSockstatsLabel sets the label used in sockstat logs to identify network traffic from this logger.
func (lg *Logger) SetSockstatsLabel(label sockstats.Label) {
lg.sockstatsLabel.Store(label)
}
// PrivateID returns the logger's private log ID.
//
// It exists for internal use only.
func (lg *Logger) PrivateID() logid.PrivateID { return lg.privateID }
// Shutdown gracefully shuts down the logger while completing any
// remaining uploads.
//
// It will block, continuing to try and upload unless the passed
// context object interrupts it by being done.
// If the shutdown is interrupted, an error is returned.
func (lg *Logger) Shutdown(ctx context.Context) error {
done := make(chan struct{})
go func() {
select {
case <-ctx.Done():
lg.uploadCancel()
<-lg.shutdownDone
case <-lg.shutdownDone:
}
close(done)
lg.httpc.CloseIdleConnections()
}()
if lg.eventClient != nil {
lg.eventClient.Close()
}
lg.shutdownStartMu.Lock()
select {
case <-lg.shutdownStart:
lg.shutdownStartMu.Unlock()
return nil
default:
}
close(lg.shutdownStart)
lg.shutdownStartMu.Unlock()
io.WriteString(lg, "logger closing down\n")
<-done
return nil
}
// Close shuts down this logger object, the background log uploader
// process, and any associated goroutines.
//
// Deprecated: use Shutdown
func (lg *Logger) Close() {
lg.Shutdown(context.Background())
}
// drainBlock is called by drainPending when there are no logs to drain.
//
// In typical operation, every call to the Write method unblocks and triggers a
// buffer.TryReadline, so logs are written with very low latency.
//
// If the caller specified FlushInterface, drainWake is only sent to
// periodically.
func (lg *Logger) drainBlock() (shuttingDown bool) {
select {
case <-lg.shutdownStart:
return true
case <-lg.drainWake:
}
return false
}
// drainPending drains and encodes a batch of logs from the buffer for upload.
// If no logs are available, drainPending blocks until logs are available.
// The returned buffer is only valid until the next call to drainPending.
func (lg *Logger) drainPending() (b []byte) {
b = lg.drainBuf[:0]
b = append(b, '[')
defer func() {
b = bytes.TrimRight(b, ",")
b = append(b, ']')
lg.drainBuf = b
if len(b) <= len("[]") {
b = nil
}
}()
maxLen := cmp.Or(lg.maxUploadSize, maxSize)
if lg.lowMem {
// When operating in a low memory environment, it is better to upload
// in multiple operations than it is to allocate a large body and OOM.
// Even if maxLen is less than maxSize, we can still upload an entry
// that is up to maxSize if we happen to encounter one.
maxLen /= lowMemRatio
}
for len(b) < maxLen {
line, err := lg.buffer.TryReadLine()
switch {
case err == io.EOF:
return b
case err != nil:
b = append(b, '{')
b = lg.appendMetadata(b, false, true, 0, 0, "reading ringbuffer: "+err.Error(), nil, 0)
b = bytes.TrimRight(b, ",")
b = append(b, '}')
return b
case line == nil:
// If we read at least some log entries, return immediately.
if len(b) > len("[") {
return b
}
// We're about to block. If we're holding on to too much memory
// in our buffer from a previous large write, let it go.
if cap(b) > bufferSize {
b = bytes.Clone(b)
lg.drainBuf = b
}
if shuttingDown := lg.drainBlock(); shuttingDown {
return b
}
continue
}
switch {
case len(line) == 0:
continue
case line[0] == '{' && jsontext.Value(line).IsValid():
// This is already a valid JSON object, so just append it.
// This may exceed maxLen, but should be no larger than maxSize
// so long as logic writing into the buffer enforces the limit.
b = append(b, line...)
default:
// This is probably a log added to stderr by filch
// outside of the logtail logger. Encode it.
if !lg.explainedRaw {
fmt.Fprintf(lg.stderr, "RAW-STDERR: ***\n")
fmt.Fprintf(lg.stderr, "RAW-STDERR: *** Lines prefixed with RAW-STDERR below bypassed logtail and probably come from a previous run of the program\n")
fmt.Fprintf(lg.stderr, "RAW-STDERR: ***\n")
fmt.Fprintf(lg.stderr, "RAW-STDERR:\n")
lg.explainedRaw = true
}
fmt.Fprintf(lg.stderr, "RAW-STDERR: %s", b)
// Do not add a client time, as it could be really old.
// Do not include instance key or ID either,
// since this came from a different instance.
b = lg.appendText(b, line, true, 0, 0, 0)
}
b = append(b, ',')
}
return b
}
// This is the goroutine that repeatedly uploads logs in the background.
func (lg *Logger) uploading(ctx context.Context) {
defer close(lg.shutdownDone)
for {
body := lg.drainPending()
origlen := -1 // sentinel value: uncompressed
// Don't attempt to compress tiny bodies; not worth the CPU cycles.
if lg.compressLogs && len(body) > 256 {
zbody := zstdframe.AppendEncode(nil, body,
zstdframe.FastestCompression, zstdframe.LowMemory(true))
// Only send it compressed if the bandwidth savings are sufficient.
// Just the extra headers associated with enabling compression
// are 50 bytes by themselves.
if len(body)-len(zbody) > 64 {
origlen = len(body)
body = zbody
}
}
var lastError string
var numFailures int
var firstFailure time.Time
for len(body) > 0 && ctx.Err() == nil {
retryAfter, err := lg.upload(ctx, body, origlen)
if err != nil {
numFailures++
firstFailure = lg.clock.Now()
if !lg.internetUp() {
fmt.Fprintf(lg.stderr, "logtail: internet down; waiting\n")
lg.awaitInternetUp(ctx)
continue
}
// Only print the same message once.
if currError := err.Error(); lastError != currError {
fmt.Fprintf(lg.stderr, "logtail: upload: %v\n", err)
lastError = currError
}
// Sleep for the specified retryAfter period,
// otherwise default to some random value.
if retryAfter <= 0 {
retryAfter = mrand.N(30*time.Second) + 30*time.Second
}
tstime.Sleep(ctx, min(retryAfter, 5*time.Minute)) // ignore absurdly large retryAfter values
} else {
// Only print a success message after recovery.
if numFailures > 0 {
fmt.Fprintf(lg.stderr, "logtail: upload succeeded after %d failures and %s\n", numFailures, lg.clock.Since(firstFailure).Round(time.Second))
}
break
}
}
select {
case <-lg.shutdownStart:
return
default:
}
}
}
func (lg *Logger) internetUp() bool {
select {
case <-lg.networkIsUp.Ready():
return true
default:
if lg.netMonitor == nil {
return true // No way to tell, so assume it is.
}
return lg.netMonitor.InterfaceState().AnyInterfaceUp()
}
}
// onChangeDelta is an eventbus subscriber function that handles
// [netmon.ChangeDelta] events to detect whether the Internet is expected to be
// reachable.
func (lg *Logger) onChangeDelta(delta *netmon.ChangeDelta) {
if delta.AnyInterfaceUp() {
fmt.Fprintf(lg.stderr, "logtail: internet back up\n")
lg.networkIsUp.Set()
} else {
fmt.Fprintf(lg.stderr, "logtail: network changed, but is not up\n")
lg.networkIsUp.Reset()
}
}
func (lg *Logger) awaitInternetUp(ctx context.Context) {
if lg.eventClient != nil {
select {
case <-lg.networkIsUp.Ready():
case <-ctx.Done():
}
return
}
upc := make(chan bool, 1)
defer lg.netMonitor.RegisterChangeCallback(func(delta *netmon.ChangeDelta) {
if delta.AnyInterfaceUp() {
select {
case upc <- true:
default:
}
}
})()
if lg.internetUp() {
return
}
select {
case <-upc:
fmt.Fprintf(lg.stderr, "logtail: internet back up\n")
case <-ctx.Done():
}
}
// upload uploads body to the log server.
// origlen indicates the pre-compression body length.
// origlen of -1 indicates that the body is not compressed.
func (lg *Logger) upload(ctx context.Context, body []byte, origlen int) (retryAfter time.Duration, err error) {
lg.uploadCalls.Add(1)
startUpload := time.Now()
const maxUploadTime = 45 * time.Second
ctx = sockstats.WithSockStats(ctx, lg.sockstatsLabel.Load(), lg.Logf)
ctx, cancel := context.WithTimeout(ctx, maxUploadTime)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "POST", lg.url, bytes.NewReader(body))
if err != nil {
// I know of no conditions under which this could fail.
// Report it very loudly.
// TODO record logs to disk
panic("logtail: cannot build http request: " + err.Error())
}
if origlen != -1 {
req.Header.Add("Content-Encoding", "zstd")
req.Header.Add("Orig-Content-Length", strconv.Itoa(origlen))
}
if runtime.GOOS == "js" {
// We once advertised we'd accept optional client certs (for internal use)
// on log.tailscale.com but then Tailscale SSH js/wasm clients prompted
// users (on some browsers?) to pick a client cert. We'll fix the server's
// TLS ServerHello, but we can also fix it client side for good measure.
//
// Corp details: https://github.com/tailscale/corp/issues/18177#issuecomment-2026598715
// and https://github.com/tailscale/corp/pull/18775#issuecomment-2027505036
//
// See https://github.com/golang/go/wiki/WebAssembly#configuring-fetch-options-while-using-nethttp
// and https://developer.mozilla.org/en-US/docs/Web/API/fetch#credentials
req.Header.Set("js.fetch:credentials", "omit")
}
req.Header["User-Agent"] = nil // not worth writing one; save some bytes
compressedNote := "not-compressed"
if origlen != -1 {
compressedNote = "compressed"
}
lg.httpDoCalls.Add(1)
resp, err := lg.httpc.Do(req)
if err != nil {
lg.failedCalls.Add(1)
return 0, fmt.Errorf("log upload of %d bytes %s failed: %v", len(body), compressedNote, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
lg.failedCalls.Add(1)
n, _ := strconv.Atoi(resp.Header.Get("Retry-After"))
b, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<10))
return time.Duration(n) * time.Second, fmt.Errorf("log upload of %d bytes %s failed %d: %s", len(body), compressedNote, resp.StatusCode, bytes.TrimSpace(b))
}
lg.uploadedBytes.Add(int64(len(body)))
lg.uploadingTime.Add(int64(time.Since(startUpload)))
return 0, nil
}
// Flush uploads all logs to the server. It blocks until complete or there is an
// unrecoverable error.
//
// TODO(bradfitz): this apparently just returns nil, as of tailscale/corp@9c2ec35.
// Finish cleaning this up.
func (lg *Logger) Flush() error {
return nil
}
// StartFlush starts a log upload, if anything is pending.
//
// If l is nil, StartFlush is a no-op.
func (lg *Logger) StartFlush() {
if lg != nil {
lg.tryDrainWake()
}
}
// ExpVar report metrics about the logger.
//
// - counter_upload_calls: Total number of upload attempts.
//
// - counter_upload_errors: Total number of upload attempts that failed.
//
// - counter_uploaded_bytes: Total number of bytes successfully uploaded
// (which is calculated after compression is applied).
//
// - counter_uploading_nsecs: Total number of nanoseconds spent uploading.
//
// - buffer: An optional [metrics.Set] with metrics for the [Buffer].
func (lg *Logger) ExpVar() expvar.Var {
m := new(metrics.Set)
m.Set("counter_upload_calls", &lg.uploadCalls)
m.Set("counter_upload_errors", &lg.failedCalls)
m.Set("counter_uploaded_bytes", &lg.uploadedBytes)
m.Set("counter_uploading_nsecs", &lg.uploadingTime)
if v, ok := lg.buffer.(interface{ ExpVar() expvar.Var }); ok {
m.Set("buffer", v.ExpVar())
}
return m
}
// logtailDisabled is whether logtail uploads to logcatcher are disabled.
var logtailDisabled atomic.Bool
// Disable disables logtail uploads for the lifetime of the process.
func Disable() {
logtailDisabled.Store(true)
}
// SetEnabled enables or disables log uploading by lg. When disabled, log
// entries passed to lg are dropped rather than buffered or uploaded; already
// buffered entries may still drain. The process-wide [Disable] kill switch
// takes precedence: if Disable has been called, SetEnabled(true) does not
// re-enable uploads.
func (lg *Logger) SetEnabled(enabled bool) {
lg.disabled.Store(!enabled)
}
var debugWakesAndUploads = envknob.RegisterBool("TS_DEBUG_LOGTAIL_WAKES")
// tryDrainWake tries to send to lg.drainWake, to cause an uploading wakeup.
// It does not block.
func (lg *Logger) tryDrainWake() {
lg.flushPending.Store(false)
if debugWakesAndUploads() {
// Using println instead of log.Printf here to avoid recursing back into
// ourselves.
println("logtail: try drain wake, numHTTP:", lg.httpDoCalls.Load())
}
select {
case lg.drainWake <- struct{}{}:
default:
}
}
func (lg *Logger) sendLocked(jsonBlob []byte) (int, error) {
tapSend(jsonBlob)
if logtailDisabled.Load() || lg.disabled.Load() {
return len(jsonBlob), nil
}
n, err := lg.buffer.Write(jsonBlob)
flushDelay := defaultFlushDelay
if lg.flushDelayFn != nil {
flushDelay = lg.flushDelayFn()
}
if flushDelay > 0 {
if lg.flushPending.CompareAndSwap(false, true) {
if lg.flushTimer == nil {
lg.flushTimer = lg.clock.AfterFunc(flushDelay, lg.tryDrainWake)
} else {
lg.flushTimer.Reset(flushDelay)
}
}
} else {
lg.tryDrainWake()
}
return n, err
}
// appendMetadata appends optional "logtail", "metrics", and "v" JSON members.
// This assumes dst is already within a JSON object.
// Each member is comma-terminated.
func (lg *Logger) appendMetadata(dst []byte, skipClientTime, skipMetrics bool, procID uint32, procSequence uint64, errDetail string, errData jsontext.Value, level int) []byte {
// Append optional logtail metadata.
if !skipClientTime || procID != 0 || procSequence != 0 || errDetail != "" || errData != nil {
dst = append(dst, `"logtail":{`...)
if !skipClientTime {
dst = append(dst, `"client_time":"`...)
dst = lg.clock.Now().UTC().AppendFormat(dst, time.RFC3339Nano)
dst = append(dst, '"', ',')
}
if procID != 0 {
dst = append(dst, `"proc_id":`...)
dst = strconv.AppendUint(dst, uint64(procID), 10)
dst = append(dst, ',')
}
if procSequence != 0 {
dst = append(dst, `"proc_seq":`...)
dst = strconv.AppendUint(dst, procSequence, 10)
dst = append(dst, ',')
}
if errDetail != "" || errData != nil {
dst = append(dst, `"error":{`...)
if errDetail != "" {
dst = append(dst, `"detail":`...)
dst, _ = jsontext.AppendQuote(dst, errDetail)
dst = append(dst, ',')
}
if errData != nil {
dst = append(dst, `"bad_data":`...)
dst = append(dst, errData...)
dst = append(dst, ',')
}
dst = bytes.TrimRight(dst, ",")
dst = append(dst, '}', ',')
}
dst = bytes.TrimRight(dst, ",")
dst = append(dst, '}', ',')
}
// Append optional metrics metadata.
if !skipMetrics && lg.metricsDelta != nil {
if d := lg.metricsDelta(); d != "" {
dst = append(dst, `"metrics":"`...)
dst = append(dst, d...)
dst = append(dst, '"', ',')
}
}
// Add the optional log level, if non-zero.
// Note that we only use log levels 1 and 2 currently.
// It's unlikely we'll ever make it past 9.
if level > 0 && level < 10 {
dst = append(dst, `"v":`...)
dst = append(dst, '0'+byte(level))
dst = append(dst, ',')
}
return dst
}
// appendText appends a raw text message in the Tailscale JSON log entry format.
func (lg *Logger) appendText(dst, src []byte, skipClientTime bool, procID uint32, procSequence uint64, level int) []byte {
dst = slices.Grow(dst, len(src))
dst = append(dst, '{')
dst = lg.appendMetadata(dst, skipClientTime, false, procID, procSequence, "", nil, level)
if len(src) == 0 {
dst = bytes.TrimRight(dst, ",")
return append(dst, "}\n"...)
}
// Append the text string, which may be truncated.
// Invalid UTF-8 will be mangled with the Unicode replacement character.
max := maxTextSize
if lg.lowMem {
max /= lowMemRatio
}
dst = append(dst, `"text":`...)
dst = appendTruncatedString(dst, src, max)
return append(dst, "}\n"...)
}
// appendTruncatedString appends a JSON string for src,
// truncating the src to be no larger than n.
func appendTruncatedString(dst, src []byte, n int) []byte {
srcLen := len(src)
src = truncate.String(src, n)
dst, _ = jsontext.AppendQuote(dst, src) // ignore error; only occurs for invalid UTF-8
if srcLen > len(src) {
dst = dst[:len(dst)-len(`"`)] // trim off preceding double-quote
dst = append(dst, "…+"...)
dst = strconv.AppendInt(dst, int64(srcLen-len(src)), 10)
dst = append(dst, '"') // re-append succeeding double-quote
}
return dst
}
// appendTextOrJSONLocked appends a raw text message or a raw JSON object
// in the Tailscale JSON log format.
func (lg *Logger) appendTextOrJSONLocked(dst, src []byte, level int) []byte {
if lg.includeProcSequence {
lg.procSequence++
}
if len(src) == 0 || src[0] != '{' {
return lg.appendText(dst, src, lg.skipClientTime, lg.procID, lg.procSequence, level)
}
// Check whether the input is a valid JSON object and
// whether it contains the reserved "logtail" name at the top-level.
var logtailKeyOffset, logtailValOffset, logtailValLength int
validJSON := func() bool {
// The jsontext.NewDecoder API operates on an io.Reader, for which
// bytes.Buffer provides a means to convert a []byte into an io.Reader.
// However, bytes.NewBuffer normally allocates unless
// we immediately shallow copy it into a pre-allocated Buffer struct.
// See https://go.dev/issue/67004.
lg.bytesBuf = *bytes.NewBuffer(src)
defer func() { lg.bytesBuf = bytes.Buffer{} }() // avoid pinning src
dec := &lg.jsonDec
dec.Reset(&lg.bytesBuf)
if tok, err := dec.ReadToken(); tok.Kind() != '{' || err != nil {
return false
}
for dec.PeekKind() != '}' {
keyOffset := dec.InputOffset()
tok, err := dec.ReadToken()
if err != nil {
return false
}
isLogtail := tok.String() == "logtail"
valOffset := dec.InputOffset()
if dec.SkipValue() != nil {
return false
}
if isLogtail {
logtailKeyOffset = int(keyOffset)
logtailValOffset = int(valOffset)
logtailValLength = int(dec.InputOffset()) - logtailValOffset
}
}
if tok, err := dec.ReadToken(); tok.Kind() != '}' || err != nil {
return false
}
if _, err := dec.ReadToken(); err != io.EOF {
return false // trailing junk after JSON object
}
return true
}()
// Treat invalid JSON as a raw text message.
if !validJSON {
return lg.appendText(dst, src, lg.skipClientTime, lg.procID, lg.procSequence, level)
}
// Check whether the JSON payload is too large.
// Due to logtail metadata, the formatted log entry could exceed maxSize.
// That's okay as the Tailscale log service limit is actually 2*maxSize.
// However, so long as logging applications aim to target the maxSize limit,
// there should be no trouble eventually uploading logs.
maxLen := cmp.Or(lg.maxUploadSize, maxSize)
if len(src) > maxLen {
errDetail := fmt.Sprintf("entry too large: %d bytes", len(src))
errData := appendTruncatedString(nil, src, maxLen/len(`\uffff`)) // escaping could increase size
dst = append(dst, '{')
dst = lg.appendMetadata(dst, lg.skipClientTime, true, lg.procID, lg.procSequence, errDetail, errData, level)
dst = bytes.TrimRight(dst, ",")
return append(dst, "}\n"...)
}
// Check whether the reserved logtail member occurs in the log data.
// If so, it is moved to the logtail/error member.
const jsonSeperators = ",:" // per RFC 8259, section 2
const jsonWhitespace = " \n\r\t" // per RFC 8259, section 2
var errDetail string
var errData jsontext.Value
if logtailValLength > 0 {
errDetail = "duplicate logtail member"
errData = bytes.Trim(src[logtailValOffset:][:logtailValLength], jsonSeperators+jsonWhitespace)
}
dst = slices.Grow(dst, len(src))
dst = append(dst, '{')
dst = lg.appendMetadata(dst, lg.skipClientTime, true, lg.procID, lg.procSequence, errDetail, errData, level)
if logtailValLength > 0 {
// Exclude original logtail member from the message.
dst = appendWithoutNewline(dst, src[len("{"):logtailKeyOffset])
dst = bytes.TrimRight(dst, jsonSeperators+jsonWhitespace)
dst = appendWithoutNewline(dst, src[logtailValOffset+logtailValLength:])
} else {
dst = appendWithoutNewline(dst, src[len("{"):])
}
dst = bytes.TrimRight(dst, jsonWhitespace)
dst = dst[:len(dst)-len("}")]
dst = bytes.TrimRight(dst, jsonSeperators+jsonWhitespace)
return append(dst, "}\n"...)
}
// appendWithoutNewline appends src to dst except that it ignores newlines
// since newlines are used to frame individual log entries.
func appendWithoutNewline(dst, src []byte) []byte {
for _, c := range src {
if c != '\n' {
dst = append(dst, c)
}
}
return dst
}
// Logf logs to l using the provided fmt-style format and optional arguments.
func (lg *Logger) Logf(format string, args ...any) {
fmt.Fprintf(lg, format, args...)
}
// Write logs an encoded JSON blob.
//
// If the []byte passed to Write is not an encoded JSON blob,
// then contents is fit into a JSON blob and written.
//
// This is intended as an interface for the stdlib "log" package.
func (lg *Logger) Write(buf []byte) (int, error) {
if len(buf) == 0 {
return 0, nil
}
inLen := len(buf) // length as provided to us, before modifications to downstream writers
level, buf := parseAndRemoveLogLevel(buf)
if lg.stderr != nil && lg.stderr != io.Discard && int64(level) <= atomic.LoadInt64(&lg.stderrLevel) {
if buf[len(buf)-1] == '\n' {
lg.stderr.Write(buf)
} else {
// The log package always line-terminates logs,
// so this is an uncommon path.
withNL := append(buf[:len(buf):len(buf)], '\n')
lg.stderr.Write(withNL)
}
}
lg.writeLock.Lock()
defer lg.writeLock.Unlock()
b := lg.appendTextOrJSONLocked(lg.writeBuf[:0], buf, level)
_, err := lg.sendLocked(b)
return inLen, err
}
var (
openBracketV = []byte("[v")
v1 = []byte("[v1] ")
v2 = []byte("[v2] ")
vJSON = []byte("[v\x00JSON]") // precedes log level '0'-'9' byte, then JSON value
)
// level 0 is normal (or unknown) level; 1+ are increasingly verbose
func parseAndRemoveLogLevel(buf []byte) (level int, cleanBuf []byte) {
if len(buf) == 0 || buf[0] == '{' || !bytes.Contains(buf, openBracketV) {
return 0, buf
}
if bytes.Contains(buf, v1) {
return 1, bytes.ReplaceAll(buf, v1, nil)
}
if bytes.Contains(buf, v2) {
return 2, bytes.ReplaceAll(buf, v2, nil)
}
if _, after, ok := bytes.Cut(buf, vJSON); ok {
rest := after
if len(rest) >= 2 {
v := rest[0]
if v >= '0' && v <= '9' {
return int(v - '0'), rest[1:]
}
}
}
return 0, buf
}
var (
tapSetSize atomic.Int32
tapMu sync.Mutex
tapSet set.HandleSet[chan<- string]
)
// RegisterLogTap registers dst to get a copy of every log write. The caller
// must call unregister when done watching.
//
// This would ideally be a method on Logger, but Logger isn't really available
// in most places; many writes go via stderr which filch redirects to the
// singleton Logger set up early. For better or worse, there's basically only
// one Logger within the program. This mechanism at least works well for
// tailscaled. It works less well for a binary with multiple tsnet.Servers. Oh
// well. This then subscribes to all of them.
func RegisterLogTap(dst chan<- string) (unregister func()) {
tapMu.Lock()
defer tapMu.Unlock()
h := tapSet.Add(dst)
tapSetSize.Store(int32(len(tapSet)))
return func() {
tapMu.Lock()
defer tapMu.Unlock()
delete(tapSet, h)
tapSetSize.Store(int32(len(tapSet)))
}
}
// tapSend relays the JSON blob to any/all registered local debug log watchers
// (somebody running "tailscale debug daemon-logs").
func tapSend(jsonBlob []byte) {
if tapSetSize.Load() == 0 {
return
}
s := string(jsonBlob)
tapMu.Lock()
defer tapMu.Unlock()
for _, dst := range tapSet {
select {
case dst <- s:
default:
}
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package metrics
import (
"io/fs"
"sync"
"syscall"
"go4.org/mem"
"tailscale.com/util/dirwalk"
)
// counter is a reusable counter for counting file descriptors.
type counter struct {
n int
// cb is the (*counter).count method value. Creating it allocates,
// so we have to save it away and use a sync.Pool to keep currentFDs
// amortized alloc-free.
cb func(name mem.RO, de fs.DirEntry) error
}
var counterPool = &sync.Pool{New: func() any {
c := new(counter)
c.cb = c.count
return c
}}
func (c *counter) count(name mem.RO, de fs.DirEntry) error {
c.n++
return nil
}
// procSelfFDFd returns a file descriptor for the /proc/self/fd
// directory, kept open for the process lifetime so that
// currentFDsFast can fstat it without allocating.
var procSelfFDFd = sync.OnceValues(func() (int, error) {
return syscall.Open("/proc/self/fd", syscall.O_RDONLY|syscall.O_CLOEXEC, 0)
})
// currentFDsFast returns the number of open file descriptors and
// whether it was able to determine it. It uses the fact that as of
// Linux 6.2 (torvalds/linux@f1f1f2569901), the stat size of the
// /proc/self/fd directory is the number of open file descriptors.
// On older kernels the reported size is zero.
func currentFDsFast() (n int, ok bool) {
fd, err := procSelfFDFd()
if err != nil {
return 0, false
}
var st syscall.Stat_t
if err := syscall.Fstat(fd, &st); err != nil {
return 0, false
}
return int(st.Size), st.Size > 0
}
// currentFDsDirwalk returns the number of open file descriptors by
// walking /proc/self/fd. It works on all kernels but is O(n) in the
// number of open file descriptors.
func currentFDsDirwalk() int {
c := counterPool.Get().(*counter)
defer counterPool.Put(c)
c.n = 0
dirwalk.WalkShallow(mem.S("/proc/self/fd"), c.cb)
return c.n
}
func currentFDs() int {
if n, ok := currentFDsFast(); ok {
return n
}
return currentFDsDirwalk()
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package metrics contains expvar & Prometheus types and code used by
// Tailscale for monitoring.
package metrics
import (
"expvar"
"fmt"
"io"
"slices"
"strings"
"tailscale.com/syncs"
)
// Set is a string-to-Var map variable that satisfies the expvar.Var
// interface.
//
// Semantically, this is mapped by tsweb's Prometheus exporter as a
// collection of unrelated variables exported with a common prefix.
//
// This lets us have tsweb recognize *expvar.Map for different
// purposes in the future. (Or perhaps all uses of expvar.Map will
// require explicit types like this one, declaring how we want tsweb
// to export it to Prometheus.)
type Set struct {
expvar.Map
}
// NewSet creates and publishes a new Set with the given name.
func NewSet(name string) *Set {
s := &Set{}
expvar.Publish(name, s)
return s
}
// NewLabelMap creates a new LabelMap metric with the given
// metric name and label name, and adds it to the Set.
func (s *Set) NewLabelMap(metric, label string) *LabelMap {
m := &LabelMap{Label: label}
s.Set(metric, m)
return m
}
// LabelMap is a string-to-Var map variable that satisfies the
// expvar.Var interface.
//
// Semantically, this is mapped by tsweb's Prometheus exporter as a
// collection of variables with the same name, with a varying label
// value. Use this to export things that are intuitively breakdowns
// into different buckets.
type LabelMap struct {
Label string
expvar.Map
// shardedIntMu orders the initialization of new shardedint keys
shardedIntMu syncs.Mutex
}
// NewLabelMap creates and publishes a new LabelMap metric with the given
// metric name and label name.
func NewLabelMap(metric, label string) *LabelMap {
m := &LabelMap{Label: label}
expvar.Publish(metric, m)
return m
}
// SetInt64 sets the *Int value stored under the given map key.
func (m *LabelMap) SetInt64(key string, v int64) {
m.Get(key).Set(v)
}
// Add adds delta to the any int-like value stored under the given map key.
func (m *LabelMap) Add(key string, delta int64) {
type intAdder interface {
Add(delta int64)
}
o := m.Map.Get(key)
if o == nil {
m.Map.Add(key, delta)
return
}
o.(intAdder).Add(delta)
}
// Get returns a direct pointer to the expvar.Int for key, creating it
// if necessary.
func (m *LabelMap) Get(key string) *expvar.Int {
m.Add(key, 0)
return m.Map.Get(key).(*expvar.Int)
}
// GetShardedInt returns a direct pointer to the syncs.ShardedInt for key,
// creating it if necessary.
func (m *LabelMap) GetShardedInt(key string) *syncs.ShardedInt {
i := m.Map.Get(key)
if i == nil {
m.shardedIntMu.Lock()
defer m.shardedIntMu.Unlock()
i = m.Map.Get(key)
if i != nil {
return i.(*syncs.ShardedInt)
}
i = syncs.NewShardedInt()
m.Set(key, i)
}
return i.(*syncs.ShardedInt)
}
// GetIncrFunc returns a function that increments the expvar.Int named by key.
//
// Most callers should not need this; it exists to satisfy an
// interface elsewhere.
func (m *LabelMap) GetIncrFunc(key string) func(delta int64) {
return m.Get(key).Add
}
// GetFloat returns a direct pointer to the expvar.Float for key, creating it
// if necessary.
func (m *LabelMap) GetFloat(key string) *expvar.Float {
m.AddFloat(key, 0.0)
return m.Map.Get(key).(*expvar.Float)
}
// CurrentFDs reports how many file descriptors are currently open.
//
// It only works on Linux. It returns zero otherwise.
func CurrentFDs() int {
return currentFDs()
}
// Histogram is a histogram of values.
// It should be created with NewHistogram.
type Histogram struct {
// buckets is a list of bucket boundaries, in increasing order.
buckets []float64
// bucketStrings is a list of the same buckets, but as strings.
// This are allocated once at creation time by NewHistogram.
bucketStrings []string
bucketVars []expvar.Int
sum expvar.Float
count expvar.Int
}
// NewHistogram returns a new histogram that reports to the given
// expvar map under the given name.
//
// The buckets are the boundaries of the histogram buckets, in
// increasing order. The last bucket is +Inf.
func NewHistogram(buckets []float64) *Histogram {
if !slices.IsSorted(buckets) {
panic("buckets must be sorted")
}
labels := make([]string, len(buckets))
for i, b := range buckets {
labels[i] = fmt.Sprintf("%v", b)
}
h := &Histogram{
buckets: buckets,
bucketStrings: labels,
bucketVars: make([]expvar.Int, len(buckets)),
}
return h
}
// Observe records a new observation in the histogram.
func (h *Histogram) Observe(v float64) {
h.sum.Add(v)
h.count.Add(1)
for i, b := range h.buckets {
if v <= b {
h.bucketVars[i].Add(1)
}
}
}
// String returns a JSON representation of the histogram.
// This is used to satisfy the expvar.Var interface.
func (h *Histogram) String() string {
var b strings.Builder
fmt.Fprintf(&b, "{")
first := true
h.Do(func(kv expvar.KeyValue) {
if !first {
fmt.Fprintf(&b, ",")
}
fmt.Fprintf(&b, "%q: ", kv.Key)
if kv.Value != nil {
fmt.Fprintf(&b, "%v", kv.Value)
} else {
fmt.Fprint(&b, "null")
}
first = false
})
fmt.Fprintf(&b, ",\"sum\": %v", &h.sum)
fmt.Fprintf(&b, ",\"count\": %v", &h.count)
fmt.Fprintf(&b, "}")
return b.String()
}
// Do calls f for each bucket in the histogram.
func (h *Histogram) Do(f func(expvar.KeyValue)) {
for i := range h.bucketVars {
f(expvar.KeyValue{Key: h.bucketStrings[i], Value: &h.bucketVars[i]})
}
f(expvar.KeyValue{Key: "+Inf", Value: &h.count})
}
// PromExport writes the histogram to w in Prometheus exposition format.
func (h *Histogram) PromExport(w io.Writer, name string) {
fmt.Fprintf(w, "# TYPE %s histogram\n", name)
h.Do(func(kv expvar.KeyValue) {
fmt.Fprintf(w, "%s_bucket{le=%q} %v\n", name, kv.Key, kv.Value)
})
fmt.Fprintf(w, "%s_sum %v\n", name, &h.sum)
fmt.Fprintf(w, "%s_count %v\n", name, &h.count)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package metrics
import (
"expvar"
"fmt"
"io"
"reflect"
"sort"
"strings"
"sync"
)
// MultiLabelMap is a struct-value-to-Var map variable that satisfies the
// [expvar.Var] interface but also allows for multiple Prometheus labels to be
// associated with each value.
//
// T must be a struct type with scalar fields. The struct field names
// (lowercased) are used as the labels, unless a "prom" struct tag is present.
// The struct fields must all be strings, and the string values must be valid
// Prometheus label values without requiring quoting.
type MultiLabelMap[T comparable] struct {
Type string // optional Prometheus type ("counter", "gauge")
Help string // optional Prometheus help string
m sync.Map // map[T]expvar.Var
mu sync.RWMutex
sorted []labelsAndValue[T] // by labels string, to match expvar.Map + for aesthetics in output
}
// NewMultiLabelMap creates and publishes (via expvar.Publish) a new
// MultiLabelMap[T] variable with the given name and returns it.
func NewMultiLabelMap[T comparable](name string, promType, helpText string) *MultiLabelMap[T] {
m := &MultiLabelMap[T]{
Type: promType,
Help: helpText,
}
var zero T
_ = LabelString(zero) // panic early if T is invalid
expvar.Publish(name, m)
return m
}
type labelsAndValue[T comparable] struct {
key T
labels string // Prometheus-formatted {label="value",label="value"} string
val expvar.Var
}
// LabelString returns a Prometheus-formatted label string for the given key.
// k must be a struct type with scalar fields, as required by MultiLabelMap,
// if k is not a struct, it will panic.
func LabelString(k any) string {
rv := reflect.ValueOf(k)
t := rv.Type()
if t.Kind() != reflect.Struct {
panic(fmt.Sprintf("MultiLabelMap must use keys of type struct; got %v", t))
}
var sb strings.Builder
sb.WriteString("{")
first := true
for ft, fv := range rv.Fields() {
if !first {
sb.WriteString(",")
}
first = false
label := ft.Tag.Get("prom")
if label == "" {
label = strings.ToLower(ft.Name)
}
switch fv.Kind() {
case reflect.String:
fmt.Fprintf(&sb, "%s=%q", label, fv.String())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
fmt.Fprintf(&sb, "%s=\"%d\"", label, fv.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
fmt.Fprintf(&sb, "%s=\"%d\"", label, fv.Uint())
case reflect.Bool:
fmt.Fprintf(&sb, "%s=\"%v\"", label, fv.Bool())
default:
panic(fmt.Sprintf("MultiLabelMap key field %q has unsupported type %v", ft.Name, fv.Type()))
}
}
sb.WriteString("}")
return sb.String()
}
// KeyValue represents a single entry in a [MultiLabelMap].
type KeyValue[T comparable] struct {
Key T
Value expvar.Var
}
func (v *MultiLabelMap[T]) String() string {
// NOTE: This has to be valid JSON because it's used by expvar.
return `"MultiLabelMap"`
}
// WritePrometheus writes v to w in Prometheus exposition format.
// The name argument is the metric name.
func (v *MultiLabelMap[T]) WritePrometheus(w io.Writer, name string) {
if v.Type != "" {
io.WriteString(w, "# TYPE ")
io.WriteString(w, name)
io.WriteString(w, " ")
io.WriteString(w, v.Type)
io.WriteString(w, "\n")
}
if v.Help != "" {
io.WriteString(w, "# HELP ")
io.WriteString(w, name)
io.WriteString(w, " ")
io.WriteString(w, v.Help)
io.WriteString(w, "\n")
}
v.mu.RLock()
defer v.mu.RUnlock()
for _, kv := range v.sorted {
io.WriteString(w, name)
io.WriteString(w, kv.labels)
switch v := kv.val.(type) {
case *expvar.Int:
fmt.Fprintf(w, " %d\n", v.Value())
case *expvar.Float:
fmt.Fprintf(w, " %v\n", v.Value())
default:
fmt.Fprintf(w, " %s\n", kv.val)
}
}
}
// Init removes all keys from the map.
//
// Think of it as "Reset", but it's named Init to match expvar.Map.Init.
func (v *MultiLabelMap[T]) Init() *MultiLabelMap[T] {
v.mu.Lock()
defer v.mu.Unlock()
v.sorted = nil
v.m.Range(func(k, _ any) bool {
v.m.Delete(k)
return true
})
return v
}
// addKeyLocked updates the sorted list of keys in v.keys.
//
// v.mu must be held.
func (v *MultiLabelMap[T]) addKeyLocked(key T, val expvar.Var) {
ls := LabelString(key)
ent := labelsAndValue[T]{key, ls, val}
// Using insertion sort to place key into the already-sorted v.keys.
i := sort.Search(len(v.sorted), func(i int) bool {
return v.sorted[i].labels >= ls
})
if i >= len(v.sorted) {
v.sorted = append(v.sorted, ent)
} else if v.sorted[i].key == key {
v.sorted[i].val = val
} else {
var zero labelsAndValue[T]
v.sorted = append(v.sorted, zero)
copy(v.sorted[i+1:], v.sorted[i:])
v.sorted[i] = ent
}
}
// Get returns the expvar for the given key, or nil if it doesn't exist.
func (v *MultiLabelMap[T]) Get(key T) expvar.Var {
i, _ := v.m.Load(key)
av, _ := i.(expvar.Var)
return av
}
func newInt() expvar.Var { return new(expvar.Int) }
func newFloat() expvar.Var { return new(expvar.Float) }
// getOrFill returns the expvar.Var for the given key, atomically creating it
// once (for all callers) with fill if it doesn't exist.
func (v *MultiLabelMap[T]) getOrFill(key T, fill func() expvar.Var) expvar.Var {
if v := v.Get(key); v != nil {
return v
}
v.mu.Lock()
defer v.mu.Unlock()
if v := v.Get(key); v != nil {
return v
}
nv := fill()
v.addKeyLocked(key, nv)
v.m.Store(key, nv)
return nv
}
// Set sets key to val.
//
// This is not optimized for highly concurrent usage; it's presumed to only be
// used rarely, at startup.
func (v *MultiLabelMap[T]) Set(key T, val expvar.Var) {
v.mu.Lock()
defer v.mu.Unlock()
v.addKeyLocked(key, val)
v.m.Store(key, val)
}
// SetInt sets val to the *[expvar.Int] value stored under the given map key,
// creating it if it doesn't exist yet.
// It does nothing if key exists but is of the wrong type.
func (v *MultiLabelMap[T]) SetInt(key T, val int64) {
// Set to Int; ignore otherwise.
if iv, ok := v.getOrFill(key, newInt).(*expvar.Int); ok {
iv.Set(val)
}
}
// SetFloat sets val to the *[expvar.Float] value stored under the given map key,
// creating it if it doesn't exist yet.
// It does nothing if key exists but is of the wrong type.
func (v *MultiLabelMap[T]) SetFloat(key T, val float64) {
// Set to Float; ignore otherwise.
if iv, ok := v.getOrFill(key, newFloat).(*expvar.Float); ok {
iv.Set(val)
}
}
// Add adds delta to the *[expvar.Int] value stored under the given map key,
// creating it if it doesn't exist yet.
// It does nothing if key exists but is of the wrong type.
func (v *MultiLabelMap[T]) Add(key T, delta int64) {
// Add to Int; ignore otherwise.
if iv, ok := v.getOrFill(key, newInt).(*expvar.Int); ok {
iv.Add(delta)
}
}
// Add adds delta to the *[expvar.Float] value stored under the given map key,
// creating it if it doesn't exist yet.
// It does nothing if key exists but is of the wrong type.
func (v *MultiLabelMap[T]) AddFloat(key T, delta float64) {
// Add to Float; ignore otherwise.
if iv, ok := v.getOrFill(key, newFloat).(*expvar.Float); ok {
iv.Add(delta)
}
}
// Delete deletes the given key from the map.
//
// This is not optimized for highly concurrent usage; it's presumed to only be
// used rarely, at startup.
func (v *MultiLabelMap[T]) Delete(key T) {
ls := LabelString(key)
v.mu.Lock()
defer v.mu.Unlock()
// Using insertion sort to place key into the already-sorted v.keys.
i := sort.Search(len(v.sorted), func(i int) bool {
return v.sorted[i].labels >= ls
})
if i < len(v.sorted) && v.sorted[i].key == key {
v.sorted = append(v.sorted[:i], v.sorted[i+1:]...)
v.m.Delete(key)
}
}
// Do calls f for each entry in the map.
// The map is locked during the iteration,
// but existing entries may be concurrently updated.
func (v *MultiLabelMap[T]) Do(f func(KeyValue[T])) {
v.mu.RLock()
defer v.mu.RUnlock()
for _, e := range v.sorted {
f(KeyValue[T]{e.key, e.val})
}
}
// ResetAllForTest resets all values for metrics to zero.
// Should only be used in tests.
func (v *MultiLabelMap[T]) ResetAllForTest() {
v.Do(func(kv KeyValue[T]) {
switch v := kv.Value.(type) {
case *expvar.Int:
v.Set(0)
case *expvar.Float:
v.Set(0)
}
})
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package bakedroots contains WebPKI CA roots we bake into the tailscaled binary,
// lest the system's CA roots be missing them (or entirely empty).
package bakedroots
import (
"crypto/x509"
"fmt"
"sync"
"tailscale.com/util/testenv"
)
// Get returns the baked-in roots.
//
// As of 2026-07-20, this includes only the LetsEncrypt ISRG roots:
// the Generation X roots (X1 & X2) and the Generation Y roots (YE & YR).
func Get() *x509.CertPool {
roots.once.Do(func() {
roots.parsePEM([]byte(letsEncryptX1 + letsEncryptX2 + letsEncryptYE + letsEncryptYR))
})
return roots.p
}
// ResetForTest resets the cached roots for testing,
// optionally setting them to caPEM if non-nil.
func ResetForTest(tb testenv.TB, caPEM []byte) {
if !testenv.InTest() {
panic("not in test")
}
tb.Setenv("ASSERT_NOT_PARALLEL_TEST", "1") // panics if tb's Parallel was called
roots = rootsOnce{}
if caPEM != nil {
roots.once.Do(func() { roots.parsePEM(caPEM) })
tb.Cleanup(func() {
// Reset the roots to real roots for any following test.
roots = rootsOnce{}
})
}
}
var roots rootsOnce
type rootsOnce struct {
once sync.Once
p *x509.CertPool
}
func (r *rootsOnce) parsePEM(caPEM []byte) {
p := x509.NewCertPool()
if !p.AppendCertsFromPEM(caPEM) {
panic(fmt.Sprintf("bogus PEM: %q", caPEM))
}
r.p = p
}
/*
letsEncryptX1 is the LetsEncrypt X1 root:
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
82:10:cf:b0:d2:40:e3:59:44:63:e0:bb:63:82:8b:00
Signature Algorithm: sha256WithRSAEncryption
Issuer: C = US, O = Internet Security Research Group, CN = ISRG Root X1
Validity
Not Before: Jun 4 11:04:38 2015 GMT
Not After : Jun 4 11:04:38 2035 GMT
Subject: C = US, O = Internet Security Research Group, CN = ISRG Root X1
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (4096 bit)
We bake it into the binary as a fallback verification root,
in case the system we're running on doesn't have it.
(Tailscale runs on some ancient devices.)
To test that this code is working on Debian/Ubuntu:
$ sudo mv /usr/share/ca-certificates/mozilla/ISRG_Root_X1.crt{,.old}
$ sudo update-ca-certificates
Then restart tailscaled. To also test dnsfallback's use of it, nuke
your /etc/resolv.conf and it should still start & run fine.
*/
const letsEncryptX1 = `
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----
`
// letsEncryptX2 is the ISRG Root X2.
//
// Subject: O = Internet Security Research Group, CN = ISRG Root X2
// Key type: ECDSA P-384
// Validity: until 2035-09-04 (generated 2020-09-04)
const letsEncryptX2 = `
-----BEGIN CERTIFICATE-----
MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00
MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT
ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw
EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW
+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9
ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI
zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW
tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1
/q4AaOeMSQ+2b1tbFfLn
-----END CERTIFICATE-----
`
// letsEncryptYE is the ISRG Root YE, part of LetsEncrypt's
// "Generation Y" hierarchy announced 2025-11-24.
//
// Subject: C = US, O = ISRG, CN = Root YE
// Key type: ECDSA P-384
// Validity: 2025-09-03 to 2045-09-02
const letsEncryptYE = `
-----BEGIN CERTIFICATE-----
MIIB2TCCAWCgAwIBAgIRAKQCa6LvbHwg1AR+XmWmk4AwCgYIKoZIzj0EAwMwLjEL
MAkGA1UEBhMCVVMxDTALBgNVBAoTBElTUkcxEDAOBgNVBAMTB1Jvb3QgWUUwHhcN
MjUwOTAzMDAwMDAwWhcNNDUwOTAyMjM1OTU5WjAuMQswCQYDVQQGEwJVUzENMAsG
A1UEChMESVNSRzEQMA4GA1UEAxMHUm9vdCBZRTB2MBAGByqGSM49AgEGBSuBBAAi
A2IABDwS/6vhrcVqcbBo+wgdI3fwn9x7DNJJOY/lTOti0vkwuRN87RhEhTH17E7X
yFjWsPYhIPt/wzOqxTd2b+4ZJNy9ID04YywF9U5zasDVyGSNErVNtz8uSGh5izW8
7j77GaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O
BBYEFKPIJlqOoUzQNWP8myPIOq5W809WMAoGCCqGSM49BAMDA2cAMGQCMHhMr8N9
LdL1VQKs9BdV81r76eXRB6mtjuNjzk6/lBsPNToWLTDzGYgtQKO1jl63uAIwGV7m
onyF377c+MM1oqVNs17sgu7F9YKZwgLmVbeOMDbKAXHtKMDLbiGllCcs8f47
-----END CERTIFICATE-----
`
// letsEncryptYR is the ISRG Root YR, part of LetsEncrypt's
// "Generation Y" hierarchy announced 2025-11-24.
//
// Subject: C = US, O = ISRG, CN = Root YR
// Key type: RSA 4096
// Validity: 2025-09-03 to 2045-09-02
const letsEncryptYR = `
-----BEGIN CERTIFICATE-----
MIIFKTCCAxGgAwIBAgIRAOxGNJNgz0sP+KmC2Tqpyj0wDQYJKoZIhvcNAQELBQAw
LjELMAkGA1UEBhMCVVMxDTALBgNVBAoTBElTUkcxEDAOBgNVBAMTB1Jvb3QgWVIw
HhcNMjUwOTAzMDAwMDAwWhcNNDUwOTAyMjM1OTU5WjAuMQswCQYDVQQGEwJVUzEN
MAsGA1UEChMESVNSRzEQMA4GA1UEAxMHUm9vdCBZUjCCAiIwDQYJKoZIhvcNAQEB
BQADggIPADCCAgoCggIBANvGJnN78CTJdWL3+eGfsLN5TrNBJs+VH9hRXqRbwxu9
sGNiB0BD1fcOxbSUQCJIM1xE13Db+5Cw1w0s0EBYsvuIP/6joF0w8cuImbgR1OGg
YbSQ4OpzI+DG8SGuTlcE873OCS+kh3srlo6vl43M5OJg4Aeo1sfHp6kTJDoIiFBN
JAY+OKfX/FUvYKuhjT+no49lmqmupSBI5PkBQiqrEGtWU5uxU/cQWHGu8jSjFBzn
ZqvbNPLMXMLFxCb3WTfrJBXXjqvWG+v4bjzxjjeAtOlU7qarRDvNOyAuQYLln904
M+faKx8hnLCpJ15ZqaEgcNlY+9MMWcC5yvL2A2j3l9+2buggZX+dOE91zYmIdawT
vSZuVvlbRrAlLxIB6pwMBjneXCjYQ8+3BCCjssbSNpZU3hTcBDdhfAlEDlYr6pEa
tnMdmDT5BqnKC92bd0EhM1fbLHioLccLCuievT8ZkPhZrq7Mii7gNXAcUEAR8+lz
Yal+9zTg7C5DALyVOeG/CqfRAMn1KSHCR0NSA6P8tn/mGRlnCct5rtVCLnVySVpU
6H1qGg3DgTOuskf8eahTMiYbI5ezPJmO5ertalskQ1utp74+eDy92PI4ftHKTbq9
IWhH4YZKh3WnJEIt+oQvlYZbY8tpEroKrFB6PFGzrJIDRyts4HqvuH52RFj2zv/B
AgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud
DgQWBBTe51tg0CJtQCh9Pw0B/qS1UrRRlDANBgkqhkiG9w0BAQsFAAOCAgEAWHnf
713Bdkq7t5yN2dNIgQakUb94X9WuyhMEHHkgx4oDpSUlnG0w4g94MoqaEUE31ZjR
LU7L5LD1g9ujFHTQu8AD215AHMVQFbm6j8hQxdXHAzDajFNQnOlDJrLjzIx176oy
AjvUtejZx2NNmdb5fd0WGVGsCdoAJ3N8ozo7ajE8t6vfxStZb4BQ9WYJGHUDrv2N
i5tJF6CNiPnlzs3BUfECRbE4JSk+jvy8+VoGiFE8qsH/j78x2fjgQhAQFV7P7Zxy
dBTZ1wEkNpZNW2qnaK1SKBLa+xf6E06YRIq5uaI+HWH8SY1y5VbRgzq40EKg3yxP
06fz+uYAUIFJoLNfhwRCc3Q6pQVuMX3yAjHAes4gk4moGcLQ5p7HAh39yeylZc1J
41sx/jKwLIkPE6Rr1Nf4pxdsxf9SA4yOEiAkDgq04DVxn8hgYFdUtBCuiuVC2heA
EiqVEa+8QZjuw8Gj0EbHXcRd1nInvGqRS1o9Is7YBdQN57X1AYveGBNNqjICSb7c
awuw1EawTDrs13VUlJVEsbQ0/O/1aaV73mCdOQ8azqL2KTv1Ewu1xbquE2S+kdQU
To9TUwat3wUA6cwXh1EfpS/3fJ0aGah5hdpRyoCLDlsSn8tkrjMfFFX0viC+GxHc
sI1ANRYvqSFC2X1VRZfDg+wD6E21BccmifG4yWc=
-----END CERTIFICATE-----
`
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:generate go run tailscale.com/cmd/viewer --type=Config --clonefunc
// Package dns contains code to configure and manage DNS settings.
package dns
import (
"bufio"
"fmt"
"net/netip"
"reflect"
"slices"
"sort"
"tailscale.com/control/controlknobs"
"tailscale.com/envknob"
"tailscale.com/net/dns/publicdns"
"tailscale.com/net/dns/resolver"
"tailscale.com/net/tsaddr"
"tailscale.com/types/dnstype"
"tailscale.com/util/dnsname"
"tailscale.com/util/set"
)
// Config is a DNS configuration.
type Config struct {
// AcceptDNS true if [Prefs.CorpDNS] is enabled (or --accept-dns=true).
// This should be used for error handling and health reporting
// purposes only.
AcceptDNS bool
// DefaultResolvers are the DNS resolvers to use for DNS names
// which aren't covered by more specific per-domain routes below.
// If empty, the OS's default resolvers (the ones that predate
// Tailscale altering the configuration) are used.
DefaultResolvers []*dnstype.Resolver
// Routes maps a DNS suffix to the resolvers that should be used
// for queries that fall within that suffix.
// If a query doesn't match any entry in Routes, the
// DefaultResolvers are used.
// A Routes entry with no resolvers means the route should be
// authoritatively answered using the contents of Hosts.
Routes map[dnsname.FQDN][]*dnstype.Resolver
// SearchDomains are DNS suffixes to try when expanding
// single-label queries.
SearchDomains []dnsname.FQDN
// Hosts maps DNS FQDNs to their IPs, which can be a mix of IPv4
// and IPv6.
// Queries matching entries in Hosts are resolved locally by
// 100.100.100.100 without leaving the machine.
// Adding an entry to Hosts merely creates the record. If you want
// it to resolve, you also need to add appropriate routes to
// Routes.
Hosts map[dnsname.FQDN][]netip.Addr
// SubdomainHosts is a set of FQDNs from Hosts that should also
// resolve subdomain queries to the same IPs. For example, if
// "node.tailnet.ts.net" is in SubdomainHosts, then queries for
// "anything.node.tailnet.ts.net" will resolve to node's IPs.
SubdomainHosts set.Set[dnsname.FQDN]
// OnlyIPv6, if true, uses the IPv6 service IP (for MagicDNS)
// instead of the IPv4 version (100.100.100.100).
OnlyIPv6 bool
// MagicDNSHostsUnrouted is whether MagicDNS host records are
// served on demand via [resolver.MagicDNSHosts] (so are not
// listed in Hosts) without being covered by any Routes entry;
// that is, MagicDNS domain routing is off. It preserves the
// effect the node records had when they were listed in Hosts:
// hasHostsWithoutSplitDNSRoutes reports true, keeping quad-100
// in the OS resolver path so the names still resolve.
MagicDNSHostsUnrouted bool
}
var magicDNSDualStack = envknob.RegisterBool("TS_DEBUG_MAGIC_DNS_DUAL_STACK")
// serviceIPs returns the list of service IPs where MagicDNS is reachable.
//
// The provided knobs may be nil.
func (c *Config) serviceIPs(knobs *controlknobs.Knobs) []netip.Addr {
if c.OnlyIPv6 {
return []netip.Addr{tsaddr.TailscaleServiceIPv6()}
}
// See https://github.com/tailscale/tailscale/issues/15404 for the background
// on the opt-in debug knob and the controlknob opt-out.
if magicDNSDualStack() || !knobs.ShouldForceRegisterMagicDNSIPv4Only() {
return []netip.Addr{
tsaddr.TailscaleServiceIP(),
tsaddr.TailscaleServiceIPv6(),
}
}
return []netip.Addr{tsaddr.TailscaleServiceIP()}
}
// WriteToBufioWriter write a debug version of c for logs to w, omitting
// spammy stuff like *.arpa entries and replacing it with a total count.
func (c *Config) WriteToBufioWriter(w *bufio.Writer) {
w.WriteString("{DefaultResolvers:")
resolver.WriteDNSResolvers(w, c.DefaultResolvers)
w.WriteString(" Routes:")
resolver.WriteRoutes(w, c.Routes)
fmt.Fprintf(w, " SearchDomains:%v", c.SearchDomains)
fmt.Fprintf(w, " Hosts:%v", len(c.Hosts))
w.WriteString("}")
}
// needsAnyResolvers reports whether c requires a resolver to be set
// at the OS level.
func (c Config) needsOSResolver() bool {
return c.hasDefaultResolvers() || c.hasRoutes()
}
func (c Config) hasRoutes() bool {
return len(c.Routes) > 0
}
// hasDefaultIPResolversOnly reports whether the only resolvers in c are
// DefaultResolvers, and that those resolvers are simple IP addresses
// that speak regular port 53 DNS.
func (c Config) hasDefaultIPResolversOnly() bool {
if !c.hasDefaultResolvers() || c.hasRoutes() {
return false
}
for _, r := range c.DefaultResolvers {
if ipp, ok := r.IPPort(); !ok || ipp.Port() != 53 || publicdns.IPIsDoHOnlyServer(ipp.Addr()) {
return false
}
}
return true
}
// hasHostsWithoutSplitDNSRoutes reports whether c contains any Host entries
// that aren't covered by a SplitDNS route suffix.
func (c Config) hasHostsWithoutSplitDNSRoutes() bool {
if c.MagicDNSHostsUnrouted {
return true
}
// Hosts is small here on most platforms: per-node records are
// served via [resolver.MagicDNSHosts] (accounted for above), so
// only control's DNS.ExtraRecords remain. On Windows it still
// carries every node's records for the hosts-file path.
for host := range c.Hosts {
if !c.hasSplitDNSRouteForHost(host) {
return true
}
}
return false
}
// requiresPrimaryResolver reports whether c can only be served correctly with
// quad-100 installed as the OS's primary (catch-all) resolver, rather than
// scoped to a set of match domains.
//
// That's the case when c has names quad-100 must answer that no route suffix
// covers, so there is no suffix to scope to. dnsConfigForNetmap pairs every
// ExtraRecord it emits with a route, so in practice this is the
// MagicDNS-names-present but MagicDNS-domain-routing-off case
// (MagicDNSHostsUnrouted).
func (c Config) requiresPrimaryResolver() bool {
return c.hasHostsWithoutSplitDNSRoutes()
}
// hasSplitDNSRouteForHost reports whether c contains a SplitDNS route
// that contains hosts.
func (c Config) hasSplitDNSRouteForHost(host dnsname.FQDN) bool {
for route := range c.Routes {
if route.Contains(host) {
return true
}
}
return false
}
func (c Config) hasDefaultResolvers() bool {
return len(c.DefaultResolvers) > 0
}
// singleResolverSet returns the resolvers used by c.Routes if all
// routes use the same resolvers, or nil if multiple sets of resolvers
// are specified.
func (c Config) singleResolverSet() []*dnstype.Resolver {
var (
prev []*dnstype.Resolver
prevInitialized bool
)
for _, resolvers := range c.Routes {
if !prevInitialized {
prev = resolvers
prevInitialized = true
continue
}
if !sameResolverNames(prev, resolvers) {
return nil
}
}
return prev
}
// matchDomains returns the list of match suffixes needed by Routes.
func (c Config) matchDomains() []dnsname.FQDN {
ret := make([]dnsname.FQDN, 0, len(c.Routes))
for suffix := range c.Routes {
ret = append(ret, suffix)
}
sort.Slice(ret, func(i, j int) bool {
return ret[i].WithTrailingDot() < ret[j].WithTrailingDot()
})
return ret
}
func sameResolverNames(a, b []*dnstype.Resolver) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i].Addr != b[i].Addr {
return false
}
if !slices.Equal(a[i].BootstrapResolution, b[i].BootstrapResolution) {
return false
}
}
return true
}
func (c *Config) Equal(o *Config) bool {
if c == nil || o == nil {
return c == o
}
return reflect.DeepEqual(c, o)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && !android && !ts_omit_dbus
package dns
import (
"context"
"time"
"github.com/godbus/dbus/v5"
)
func init() {
optDBusPing.Set(dbusPing)
optDBusReadString.Set(dbusReadString)
}
func dbusPing(name, objectPath string) error {
conn, err := dbus.SystemBus()
if err != nil {
// DBus probably not running.
return err
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
obj := conn.Object(name, dbus.ObjectPath(objectPath))
call := obj.CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0)
return call.Err
}
// dbusReadString reads a string property from the provided name and object
// path. property must be in "interface.member" notation.
func dbusReadString(name, objectPath, iface, member string) (string, error) {
conn, err := dbus.SystemBus()
if err != nil {
// DBus probably not running.
return "", err
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
obj := conn.Object(name, dbus.ObjectPath(objectPath))
var result dbus.Variant
err = obj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, iface, member).Store(&result)
if err != nil {
return "", err
}
if s, ok := result.Value().(string); ok {
return s, nil
}
return result.String(), nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build (linux && !android) || freebsd || openbsd
package dns
import (
"bufio"
"bytes"
_ "embed"
"fmt"
"os"
"os/exec"
"path/filepath"
"tailscale.com/atomicfile"
"tailscale.com/types/logger"
)
//go:embed resolvconf-workaround.sh
var workaroundScript []byte
// resolvconfConfigName is the name of the config submitted to
// resolvconf.
// The name starts with 'tun' in order to match the hardcoded
// interface order in debian resolvconf, which will place this
// configuration ahead of regular network links. In theory, this
// doesn't matter because we then fix things up to ensure our config
// is the only one in use, but in case that fails, this will make our
// configuration slightly preferred.
// The 'inet' suffix has no specific meaning, but conventionally
// resolvconf implementations encourage adding a suffix roughly
// indicating where the config came from, and "inet" is the "none of
// the above" value (rather than, say, "ppp" or "dhcp").
const resolvconfConfigName = "tun-tailscale.inet"
// resolvconfLibcHookPath is the directory containing libc update
// scripts, which are run by Debian resolvconf when /etc/resolv.conf
// has been updated.
const resolvconfLibcHookPath = "/etc/resolvconf/update-libc.d"
// resolvconfHookPath is the name of the libc hook script we install
// to force Tailscale's DNS config to take effect.
var resolvconfHookPath = filepath.Join(resolvconfLibcHookPath, "tailscale")
// resolvconfManager manages DNS configuration using the Debian
// implementation of the `resolvconf` program, written by Thomas Hood.
type resolvconfManager struct {
logf logger.Logf
listRecordsPath string
interfacesDir string
scriptInstalled bool // libc update script has been installed
}
func newDebianResolvconfManager(logf logger.Logf) (*resolvconfManager, error) {
ret := &resolvconfManager{
logf: logf,
listRecordsPath: "/lib/resolvconf/list-records",
interfacesDir: "/etc/resolvconf/run/interface", // panic fallback if nothing seems to work
}
if _, err := os.Stat(ret.listRecordsPath); os.IsNotExist(err) {
// This might be a Debian system from before the big /usr
// merge, try /usr instead.
ret.listRecordsPath = "/usr" + ret.listRecordsPath
}
// The runtime directory is currently (2020-04) canonically
// /etc/resolvconf/run, but the manpage is making noise about
// switching to /run/resolvconf and dropping the /etc path. So,
// let's probe the possible directories and use the first one
// that works.
for _, path := range []string{
"/etc/resolvconf/run/interface",
"/run/resolvconf/interface",
"/var/run/resolvconf/interface",
} {
if _, err := os.Stat(path); err == nil {
ret.interfacesDir = path
break
}
}
if ret.interfacesDir == "" {
// None of the paths seem to work, use the canonical location
// that the current manpage says to use.
ret.interfacesDir = "/etc/resolvconf/run/interfaces"
}
return ret, nil
}
func (m *resolvconfManager) deleteTailscaleConfig() error {
cmd := exec.Command("resolvconf", "-d", resolvconfConfigName)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("running %s: %s", cmd, out)
}
return nil
}
func (m *resolvconfManager) SetDNS(config OSConfig) error {
if !m.scriptInstalled {
m.logf("injecting resolvconf workaround script")
if err := os.MkdirAll(resolvconfLibcHookPath, 0755); err != nil {
return err
}
if err := atomicfile.WriteFile(resolvconfHookPath, workaroundScript, 0755); err != nil {
return err
}
m.scriptInstalled = true
}
if config.IsZero() {
if err := m.deleteTailscaleConfig(); err != nil {
return err
}
} else {
stdin := new(bytes.Buffer)
writeResolvConf(stdin, config.Nameservers, config.SearchDomains) // dns_direct.go
// This resolvconf implementation doesn't support exclusive
// mode or interface priorities, so it will end up blending
// our configuration with other sources. However, this will
// get fixed up by the script we injected above.
cmd := exec.Command("resolvconf", "-a", resolvconfConfigName)
cmd.Stdin = stdin
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("running %s: %s", cmd, out)
}
}
return nil
}
func (m *resolvconfManager) SupportsSplitDNS() bool {
return false
}
func (m *resolvconfManager) GetBaseConfig() (OSConfig, error) {
var bs bytes.Buffer
cmd := exec.Command(m.listRecordsPath)
// list-records assumes it's being run with CWD set to the
// interfaces runtime dir, and returns nonsense otherwise.
cmd.Dir = m.interfacesDir
cmd.Stdout = &bs
if err := cmd.Run(); err != nil {
return OSConfig{}, err
}
var conf bytes.Buffer
sc := bufio.NewScanner(&bs)
for sc.Scan() {
if sc.Text() == resolvconfConfigName {
continue
}
bs, err := os.ReadFile(filepath.Join(m.interfacesDir, sc.Text()))
if err != nil {
if os.IsNotExist(err) {
// Probably raced with a deletion, that's okay.
continue
}
return OSConfig{}, err
}
conf.Write(bs)
conf.WriteByte('\n')
}
return readResolv(&conf)
}
func (m *resolvconfManager) Close() error {
if err := m.deleteTailscaleConfig(); err != nil {
return err
}
if m.scriptInstalled {
m.logf("removing resolvconf workaround script")
os.Remove(resolvconfHookPath) // Best-effort
}
return nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !android && !ios
package dns
import (
"bytes"
"context"
"crypto/rand"
"errors"
"fmt"
"io"
"io/fs"
"net/netip"
"os"
"os/exec"
"path/filepath"
"runtime"
"slices"
"strings"
"sync"
"sync/atomic"
"time"
"tailscale.com/feature"
"tailscale.com/health"
"tailscale.com/hostinfo"
"tailscale.com/net/dns/resolvconffile"
"tailscale.com/net/tsaddr"
"tailscale.com/types/lazy"
"tailscale.com/types/logger"
"tailscale.com/util/dnsname"
"tailscale.com/util/eventbus"
"tailscale.com/version/distro"
)
// writeResolvConf writes DNS configuration in resolv.conf format to the given writer.
func writeResolvConf(w io.Writer, servers []netip.Addr, domains []dnsname.FQDN) error {
c := &resolvconffile.Config{
Nameservers: servers,
SearchDomains: domains,
}
return c.Write(w)
}
func readResolv(r io.Reader) (OSConfig, error) {
c, err := resolvconffile.Parse(r)
if err != nil {
return OSConfig{}, err
}
return OSConfig{
Nameservers: c.Nameservers,
SearchDomains: c.SearchDomains,
}, nil
}
// resolvOwner returns the apparent owner of the resolv.conf
// configuration in bs - one of "resolvconf", "systemd-resolved" or
// "NetworkManager", or "" if no known owner was found.
//
//lint:ignore U1000 used in linux and freebsd code
func resolvOwner(bs []byte) string {
likely := ""
b := bytes.NewBuffer(bs)
for {
line, err := b.ReadString('\n')
if err != nil {
return likely
}
line = strings.TrimSpace(line)
if line == "" {
continue
}
if line[0] != '#' {
// First non-empty, non-comment line. Assume the owner
// isn't hiding further down.
return likely
}
if strings.Contains(line, "systemd-resolved") {
likely = "systemd-resolved"
} else if strings.Contains(line, "NetworkManager") {
likely = "NetworkManager"
} else if strings.Contains(line, "resolvconf") {
likely = "resolvconf"
}
}
}
// isResolvedRunning reports whether systemd-resolved is running on the system,
// even if it is not managing the system DNS settings.
func isResolvedRunning() bool {
if runtime.GOOS != "linux" {
return false
}
// systemd-resolved is never installed without systemd.
_, err := exec.LookPath("systemctl")
if err != nil {
return false
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
err = exec.CommandContext(ctx, "systemctl", "is-active", "systemd-resolved.service").Run()
// is-active exits with code 3 if the service is not active.
return err == nil
}
func restartResolved() error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return exec.CommandContext(ctx, "systemctl", "restart", "systemd-resolved.service").Run()
}
// directManager is an OSConfigurator which replaces /etc/resolv.conf with a file
// generated from the given configuration, creating a backup of its old state.
//
// This way of configuring DNS is precarious, since it does not react
// to the disappearance of the Tailscale interface.
// The caller must call Down before program shutdown
// or as cleanup if the program terminates unexpectedly.
type directManager struct {
logf logger.Logf
health *health.Tracker
fs wholeFileFS
// renameBroken is set if fs.Rename to or from /etc/resolv.conf
// fails. This can happen in some container runtimes, where
// /etc/resolv.conf is bind-mounted from outside the container,
// and therefore /etc and /etc/resolv.conf are different
// filesystems as far as rename(2) is concerned.
//
// In those situations, we fall back to emulating rename with file
// copies and truncations, which is not as good (opens up a race
// where a reader can see an empty or partial /etc/resolv.conf),
// but is better than having non-functioning DNS.
renameBroken bool
trampleCount atomic.Int64
trampleTimer *time.Timer
eventClient *eventbus.Client
trampleDNSPub *eventbus.Publisher[TrampleDNS]
ctx context.Context // valid until Close
ctxClose context.CancelFunc // closes ctx
mu sync.Mutex
wantResolvConf []byte // if non-nil, what we expect /etc/resolv.conf to contain
//lint:ignore U1000 used in direct_linux.go
lastWarnContents []byte // last resolv.conf contents that we warned about
}
//lint:ignore U1000 used in manager_{freebsd,openbsd}.go
func newDirectManager(logf logger.Logf, health *health.Tracker, bus *eventbus.Bus) *directManager {
return newDirectManagerOnFS(logf, health, bus, directFS{})
}
var trampleWatchDuration = 5 * time.Second
func newDirectManagerOnFS(logf logger.Logf, health *health.Tracker, bus *eventbus.Bus, fs wholeFileFS) *directManager {
ctx, cancel := context.WithCancel(context.Background())
m := &directManager{
logf: logf,
health: health,
fs: fs,
ctx: ctx,
ctxClose: cancel,
}
if bus != nil {
m.eventClient = bus.Client("dns.directManager")
m.trampleDNSPub = eventbus.Publish[TrampleDNS](m.eventClient)
}
m.trampleTimer = time.AfterFunc(trampleWatchDuration, func() {
m.trampleCount.Store(0)
})
go m.runFileWatcher()
return m
}
func (m *directManager) readResolvFile(path string) (OSConfig, error) {
b, err := m.fs.ReadFile(path)
if err != nil {
return OSConfig{}, err
}
return readResolv(bytes.NewReader(b))
}
// ownedByTailscale reports whether /etc/resolv.conf seems to be a
// tailscale-managed file.
func (m *directManager) ownedByTailscale() (bool, error) {
isRegular, err := m.fs.Stat(resolvConf)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
if !isRegular {
return false, nil
}
bs, err := m.fs.ReadFile(resolvConf)
if err != nil {
return false, err
}
if bytes.Contains(bs, []byte("generated by tailscale")) {
return true, nil
}
return false, nil
}
// backupConfig creates or updates a backup of /etc/resolv.conf, if
// resolv.conf does not currently contain a Tailscale-managed config.
func (m *directManager) backupConfig() error {
if _, err := m.fs.Stat(resolvConf); err != nil {
if os.IsNotExist(err) {
// No resolv.conf, nothing to back up. Also get rid of any
// existing backup file, to avoid restoring something old.
m.fs.Remove(backupConf)
return nil
}
return err
}
owned, err := m.ownedByTailscale()
if err != nil {
return err
}
if owned {
return nil
}
return m.rename(resolvConf, backupConf)
}
func (m *directManager) restoreBackup() (restored bool, err error) {
if _, err := m.fs.Stat(backupConf); err != nil {
if os.IsNotExist(err) {
// No backup, nothing we can do.
return false, nil
}
return false, err
}
owned, err := m.ownedByTailscale()
if err != nil {
return false, err
}
_, err = m.fs.Stat(resolvConf)
if err != nil && !os.IsNotExist(err) {
return false, err
}
resolvConfExists := !os.IsNotExist(err)
if resolvConfExists && !owned {
// There's already a non-tailscale config in place, get rid of
// our backup.
m.fs.Remove(backupConf)
return false, nil
}
// We own resolv.conf, and a backup exists.
if err := m.rename(backupConf, resolvConf); err != nil {
return false, err
}
return true, nil
}
// rename tries to rename old to new using m.fs.Rename, and falls back
// to hand-copying bytes and truncating old if that fails.
//
// This is a workaround to /etc/resolv.conf being a bind-mounted file
// some container environments, which cannot be moved elsewhere in
// /etc (because that would be a cross-filesystem move) or deleted
// (because that would break the bind in surprising ways).
func (m *directManager) rename(old, new string) error {
if !m.renameBroken {
err := m.fs.Rename(old, new)
if err == nil {
if new == resolvConf {
m.relabelResolvConf()
}
return nil
}
if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
// Fail fast. The fallback case below won't work anyway.
return err
}
m.logf("rename of %q to %q failed (%v), falling back to copy+delete", old, new, err)
m.renameBroken = true
}
bs, err := m.fs.ReadFile(old)
if err != nil {
return fmt.Errorf("reading %q to rename: %w", old, err)
}
if err := m.fs.WriteFile(new, bs, 0644); err != nil {
return fmt.Errorf("writing to %q in rename of %q: %w", new, old, err)
}
// Explicitly set the permissions on the new file. This ensures that
// if we have a umask set which prevents creating world-readable files,
// the file will still have the correct permissions once it's renamed
// into place. See #12609.
if err := m.fs.Chmod(new, 0644); err != nil {
return fmt.Errorf("chmod %q in rename of %q: %w", new, old, err)
}
if err := m.fs.Remove(old); err != nil {
err2 := m.fs.Truncate(old)
if err2 != nil {
return fmt.Errorf("remove of %q failed (%w) and so did truncate: %v", old, err, err2)
}
}
if new == resolvConf {
m.relabelResolvConf()
}
return nil
}
var restoreconPath lazy.SyncValue[string] // path to restorecon, or "" if absent
// relabelResolvConf restores the policy-default SELinux context on
// /etc/resolv.conf. Best effort: only runs when SELinux is enforcing. See:
//
// https://github.com/tailscale/tailscale/issues/20149.
func (m *directManager) relabelResolvConf() {
if runtime.GOOS != "linux" {
return
}
restorecon := restoreconPath.Get(func() string {
p, _ := exec.LookPath("restorecon")
return p
})
if restorecon == "" {
return
}
if !hostinfo.IsSELinuxEnforcing() {
// Clear any prior warning, e.g. if SELinux was turned off at runtime
// with setenforce(8) since the last failure.
m.health.SetHealthy(selinuxRelabelWarnable)
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// m.fs may be rooted at a test prefix; restorecon needs the real path.
actualPath := m.fs.ActualPath(resolvConf)
if out, err := exec.CommandContext(ctx, restorecon, actualPath).CombinedOutput(); err != nil {
m.logf("dns: restorecon of %q failed: %v, output: %s", actualPath, err, bytes.TrimSpace(out))
m.health.SetUnhealthy(selinuxRelabelWarnable, nil)
} else {
m.health.SetHealthy(selinuxRelabelWarnable)
}
}
var selinuxRelabelWarnable = health.Register(&health.Warnable{
Code: "resolv-conf-relabel-failed",
Severity: health.SeverityMedium,
Title: "DNS configuration issue",
Text: health.StaticMessage("Failed to restore the SELinux label on /etc/resolv.conf; DNS may break when other services manage the file. See https://github.com/tailscale/tailscale/issues/20149"),
})
// setWant sets the expected contents of /etc/resolv.conf, if any.
//
// A value of nil means no particular value is expected.
//
// m takes ownership of want.
func (m *directManager) setWant(want []byte) {
m.mu.Lock()
defer m.mu.Unlock()
m.wantResolvConf = want
}
func (m *directManager) SetDNS(config OSConfig) (err error) {
defer func() {
if err != nil && errors.Is(err, fs.ErrPermission) && runtime.GOOS == "linux" &&
distro.Get() == distro.Synology && os.Geteuid() != 0 {
// On Synology (notably DSM7 where we don't run as root), ignore all
// DNS configuration errors for now. We don't have permission.
// See https://github.com/tailscale/tailscale/issues/4017
m.logf("ignoring SetDNS permission error on Synology (Issue 4017); was: %v", err)
err = nil
}
}()
m.setWant(nil) // reset our expectations before any work
var changed bool
if config.IsZero() {
changed, err = m.restoreBackup()
if err != nil {
return err
}
} else {
changed = true
if err := m.backupConfig(); err != nil {
return err
}
buf := new(bytes.Buffer)
writeResolvConf(buf, config.Nameservers, config.SearchDomains)
if err := m.atomicWriteFile(m.fs, resolvConf, buf.Bytes(), 0644); err != nil {
return err
}
// Now that we've successfully written to the file, lock it in.
// If we see /etc/resolv.conf with different contents, we know somebody
// else trampled on it.
m.setWant(buf.Bytes())
}
// We might have taken over a configuration managed by resolved,
// in which case it will notice this on restart and gracefully
// start using our configuration. This shouldn't happen because we
// try to manage DNS through resolved when it's around, but as a
// best-effort fallback if we messed up the detection, try to
// restart resolved to make the system configuration consistent.
//
// We take care to only kick systemd-resolved if we've made some
// change to the system's DNS configuration, because this codepath
// can end up running in cases where the user has manually
// configured /etc/resolv.conf to point to systemd-resolved (but
// it's not managed explicitly by systemd-resolved), *and* has
// --accept-dns=false, meaning we pass an empty configuration to
// the running DNS manager. In that very edge-case scenario, we
// cause a disruptive DNS outage each time we reset an empty
// OS configuration.
if changed && isResolvedRunning() && !runningAsGUIDesktopUser() {
t0 := time.Now()
err := restartResolved()
d := time.Since(t0).Round(time.Millisecond)
if err != nil {
m.logf("error restarting resolved after %v: %v", d, err)
} else {
m.logf("restarted resolved after %v", d)
}
}
return nil
}
func (m *directManager) SupportsSplitDNS() bool {
return false
}
func (m *directManager) GetBaseConfig() (OSConfig, error) {
owned, err := m.ownedByTailscale()
if err != nil {
return OSConfig{}, err
}
fileToRead := resolvConf
if owned {
fileToRead = backupConf
}
oscfg, err := m.readResolvFile(fileToRead)
if err != nil {
return OSConfig{}, err
}
// On some systems, the backup configuration file is actually a
// symbolic link to something owned by another DNS service (commonly,
// resolved). Thus, it can be updated out from underneath us to contain
// the Tailscale service IP, which results in an infinite loop of us
// trying to send traffic to resolved, which sends back to us, and so
// on. To solve this, drop the Tailscale service IP from the base
// configuration; we do this in all situations since there's
// essentially no world where we want to forward to ourselves.
//
// See: https://github.com/tailscale/tailscale/issues/7816
var removed bool
oscfg.Nameservers = slices.DeleteFunc(oscfg.Nameservers, func(ip netip.Addr) bool {
if ip == tsaddr.TailscaleServiceIP() || ip == tsaddr.TailscaleServiceIPv6() {
removed = true
return true
}
return false
})
if removed {
m.logf("[v1] dropped Tailscale IP from base config that was a symlink")
}
return oscfg, nil
}
// HookWatchFile is a hook for watching file changes, for platforms that support it.
// The function is called with a directory and filename to watch, and a callback
// to call when the file changes. It returns an error if the watch could not be set up.
var HookWatchFile feature.Hook[func(ctx context.Context, dir, filename string, cb func()) error]
func (m *directManager) runFileWatcher() {
watchFile, ok := HookWatchFile.GetOk()
if !ok {
return
}
dir := m.fs.ActualPath(filepath.Dir(resolvConf))
file := m.fs.ActualPath(resolvConf)
if err := watchFile(m.ctx, dir, file, m.checkForFileTrample); err != nil {
// This is all best effort for now, so surface warnings to users.
m.logf("dns: inotify: %s", err)
}
}
var resolvTrampleWarnable = health.Register(&health.Warnable{
Code: "resolv-conf-overwritten",
Severity: health.SeverityMedium,
Title: "DNS configuration issue",
Text: health.StaticMessage("System DNS config not ideal. /etc/resolv.conf overwritten. See https://tailscale.com/s/dns-fight"),
})
// checkForFileTrample checks whether /etc/resolv.conf has been trampled
// by another program on the system. (e.g. a DHCP client)
func (m *directManager) checkForFileTrample() {
m.mu.Lock()
want := m.wantResolvConf
lastWarn := m.lastWarnContents
m.mu.Unlock()
if want == nil {
return
}
cur, err := m.fs.ReadFile(resolvConf)
if err != nil {
m.logf("trample: read error: %v", err)
return
}
if bytes.Equal(cur, want) {
m.health.SetHealthy(resolvTrampleWarnable)
if lastWarn != nil {
m.mu.Lock()
m.lastWarnContents = nil
m.mu.Unlock()
m.logf("trample: resolv.conf again matches expected content")
}
return
}
if bytes.Equal(cur, lastWarn) {
// We already logged about this, so not worth doing it again.
return
}
m.mu.Lock()
m.lastWarnContents = cur
m.mu.Unlock()
show := cur
if len(show) > 1024 {
show = show[:1024]
}
m.logf("trample: resolv.conf changed from what we expected. did some other program interfere? current contents: %q", show)
m.health.SetUnhealthy(resolvTrampleWarnable, nil)
if m.trampleDNSPub != nil {
n := m.trampleCount.Add(1)
if n < 10 {
m.trampleDNSPub.Publish(TrampleDNS{
LastTrample: time.Now(),
TramplesInTimeout: n,
})
m.trampleTimer.Reset(trampleWatchDuration)
} else {
m.logf("trample: resolv.conf overwritten %d times, no longer attempting to replace it.", n)
}
}
}
func (m *directManager) Close() error {
m.ctxClose()
if m.eventClient != nil {
m.eventClient.Close()
}
// We used to keep a file for the tailscale config and symlinked
// to it, but then we stopped because /etc/resolv.conf being a
// symlink to surprising places breaks snaps and other sandboxing
// things. Clean it up if it's still there.
m.fs.Remove("/etc/resolv.tailscale.conf")
if _, err := m.fs.Stat(backupConf); err != nil {
if os.IsNotExist(err) {
// No backup, nothing we can do.
return nil
}
return err
}
owned, err := m.ownedByTailscale()
if err != nil {
return err
}
_, err = m.fs.Stat(resolvConf)
if err != nil && !os.IsNotExist(err) {
return err
}
resolvConfExists := !os.IsNotExist(err)
if resolvConfExists && !owned {
// There's already a non-tailscale config in place, get rid of
// our backup.
m.fs.Remove(backupConf)
return nil
}
// We own resolv.conf, and a backup exists.
if err := m.rename(backupConf, resolvConf); err != nil {
return err
}
if isResolvedRunning() && !runningAsGUIDesktopUser() {
m.logf("restarting systemd-resolved...")
if err := restartResolved(); err != nil {
m.logf("restart of systemd-resolved failed: %v", err)
} else {
m.logf("restarted systemd-resolved")
}
}
return nil
}
func (m *directManager) atomicWriteFile(fs wholeFileFS, filename string, data []byte, perm os.FileMode) error {
var randBytes [12]byte
if _, err := rand.Read(randBytes[:]); err != nil {
return fmt.Errorf("atomicWriteFile: %w", err)
}
tmpName := fmt.Sprintf("%s.%x.tmp", filename, randBytes[:])
defer fs.Remove(tmpName)
if err := fs.WriteFile(tmpName, data, perm); err != nil {
return fmt.Errorf("atomicWriteFile: %w", err)
}
// Explicitly set the permissions on the temporary file before renaming
// it. This ensures that if we have a umask set which prevents creating
// world-readable files, the file will still have the correct
// permissions once it's renamed into place. See #12609.
if err := fs.Chmod(tmpName, perm); err != nil {
return fmt.Errorf("atomicWriteFile: Chmod: %w", err)
}
return m.rename(tmpName, filename)
}
// wholeFileFS is a high-level file system abstraction designed just for use
// by directManager, with the goal that it is easy to implement over wsl.exe.
//
// All name parameters are absolute paths.
type wholeFileFS interface {
Chmod(name string, mode os.FileMode) error
ReadFile(name string) ([]byte, error)
Remove(name string) error
Rename(oldName, newName string) error
// ActualPath returns the real filesystem path for the given absolute
// logical path. All other methods in this interface accept logical
// paths (like "/etc/resolv.conf") and translate them internally;
// ActualPath exposes that same translation for callers that need
// the real path for use outside the interface (e.g. setting up an
// inotify watch on the correct directory).
//
// For directFS with an empty prefix (production), the input is
// returned unchanged ("/etc" → "/etc"). For directFS with a test
// prefix like "/tmp/test123", the prefix is joined
// ("/etc" → "/tmp/test123/etc"). For wslFS the input is returned
// unchanged, since paths are passed through to wsl.exe as-is.
ActualPath(name string) string
Stat(name string) (isRegular bool, err error)
Truncate(name string) error
WriteFile(name string, contents []byte, perm os.FileMode) error
}
// directFS is a wholeFileFS implemented directly on the OS.
type directFS struct {
// prefix is file path prefix.
//
// All name parameters are absolute paths so this is typically a
// testing temporary directory like "/tmp".
prefix string
}
func (fs directFS) path(name string) string { return filepath.Join(fs.prefix, name) }
func (fs directFS) ActualPath(name string) string { return fs.path(name) }
func (fs directFS) Stat(name string) (isRegular bool, err error) {
fi, err := os.Stat(fs.path(name))
if err != nil {
return false, err
}
return fi.Mode().IsRegular(), nil
}
func (fs directFS) Chmod(name string, mode os.FileMode) error {
return os.Chmod(fs.path(name), mode)
}
func (fs directFS) Rename(oldName, newName string) error {
return os.Rename(fs.path(oldName), fs.path(newName))
}
func (fs directFS) Remove(name string) error { return os.Remove(fs.path(name)) }
func (fs directFS) ReadFile(name string) ([]byte, error) {
return os.ReadFile(fs.path(name))
}
func (fs directFS) Truncate(name string) error {
return os.Truncate(fs.path(name), 0)
}
func (fs directFS) WriteFile(name string, contents []byte, perm os.FileMode) error {
return os.WriteFile(fs.path(name), contents, perm)
}
// runningAsGUIDesktopUser reports whether it seems that this code is
// being run as a regular user on a Linux desktop. This is a quick
// hack to fix Issue 2672 where PolicyKit pops up a GUI dialog asking
// to proceed we do a best effort attempt to restart
// systemd-resolved.service. There's surely a better way.
func runningAsGUIDesktopUser() bool {
return os.Getuid() != 0 && os.Getenv("DISPLAY") != ""
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
package dns
import (
"net/netip"
"tailscale.com/types/dnstype"
"tailscale.com/util/dnsname"
"tailscale.com/util/set"
)
// 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
if src.DefaultResolvers != nil {
dst.DefaultResolvers = make([]*dnstype.Resolver, len(src.DefaultResolvers))
for i := range dst.DefaultResolvers {
if src.DefaultResolvers[i] == nil {
dst.DefaultResolvers[i] = nil
} else {
dst.DefaultResolvers[i] = src.DefaultResolvers[i].Clone()
}
}
}
if dst.Routes != nil {
dst.Routes = map[dnsname.FQDN][]*dnstype.Resolver{}
for k, sv := range src.Routes {
if sv == nil {
dst.Routes[k] = nil
continue
}
dst.Routes[k] = make([]*dnstype.Resolver, len(sv))
for i := range sv {
if sv[i] == nil {
dst.Routes[k][i] = nil
} else {
dst.Routes[k][i] = sv[i].Clone()
}
}
}
}
dst.SearchDomains = append(src.SearchDomains[:0:0], src.SearchDomains...)
if dst.Hosts != nil {
dst.Hosts = map[dnsname.FQDN][]netip.Addr{}
for k := range src.Hosts {
dst.Hosts[k] = append([]netip.Addr{}, src.Hosts[k]...)
}
}
dst.SubdomainHosts = src.SubdomainHosts.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 {
AcceptDNS bool
DefaultResolvers []*dnstype.Resolver
Routes map[dnsname.FQDN][]*dnstype.Resolver
SearchDomains []dnsname.FQDN
Hosts map[dnsname.FQDN][]netip.Addr
SubdomainHosts set.Set[dnsname.FQDN]
OnlyIPv6 bool
MagicDNSHostsUnrouted bool
}{})
// Clone duplicates src into dst and reports whether it succeeded.
// To succeed, <src, dst> must be of types <*T, *T> or <*T, **T>,
// where T is one of Config.
func Clone(dst, src any) bool {
switch src := src.(type) {
case *Config:
switch dst := dst.(type) {
case *Config:
*dst = *src.Clone()
return true
case **Config:
*dst = src.Clone()
return true
}
}
return false
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by tailscale/cmd/viewer; DO NOT EDIT.
package dns
import (
jsonv1 "encoding/json"
"errors"
"net/netip"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"tailscale.com/types/dnstype"
"tailscale.com/types/views"
"tailscale.com/util/dnsname"
"tailscale.com/util/set"
)
//go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type=Config
// View returns a read-only view of Config.
func (p *Config) View() ConfigView {
return ConfigView{ж: p}
}
// ConfigView provides a read-only view over Config.
//
// Its methods should only be called if `Valid()` returns true.
type ConfigView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *Config
}
// Valid reports whether v's underlying value is non-nil.
func (v ConfigView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v ConfigView) AsStruct() *Config {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v ConfigView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v ConfigView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *ConfigView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x Config
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *ConfigView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x Config
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// AcceptDNS true if [Prefs.CorpDNS] is enabled (or --accept-dns=true).
// This should be used for error handling and health reporting
// purposes only.
func (v ConfigView) AcceptDNS() bool { return v.ж.AcceptDNS }
// DefaultResolvers are the DNS resolvers to use for DNS names
// which aren't covered by more specific per-domain routes below.
// If empty, the OS's default resolvers (the ones that predate
// Tailscale altering the configuration) are used.
func (v ConfigView) DefaultResolvers() views.SliceView[*dnstype.Resolver, dnstype.ResolverView] {
return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](v.ж.DefaultResolvers)
}
// Routes maps a DNS suffix to the resolvers that should be used
// for queries that fall within that suffix.
// If a query doesn't match any entry in Routes, the
// DefaultResolvers are used.
// A Routes entry with no resolvers means the route should be
// authoritatively answered using the contents of Hosts.
func (v ConfigView) Routes() views.MapFn[dnsname.FQDN, []*dnstype.Resolver, views.SliceView[*dnstype.Resolver, dnstype.ResolverView]] {
return views.MapFnOf(v.ж.Routes, func(t []*dnstype.Resolver) views.SliceView[*dnstype.Resolver, dnstype.ResolverView] {
return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](t)
})
}
// SearchDomains are DNS suffixes to try when expanding
// single-label queries.
func (v ConfigView) SearchDomains() views.Slice[dnsname.FQDN] {
return views.SliceOf(v.ж.SearchDomains)
}
// Hosts maps DNS FQDNs to their IPs, which can be a mix of IPv4
// and IPv6.
// Queries matching entries in Hosts are resolved locally by
// 100.100.100.100 without leaving the machine.
// Adding an entry to Hosts merely creates the record. If you want
// it to resolve, you also need to add appropriate routes to
// Routes.
func (v ConfigView) Hosts() views.MapSlice[dnsname.FQDN, netip.Addr] {
return views.MapSliceOf(v.ж.Hosts)
}
// SubdomainHosts is a set of FQDNs from Hosts that should also
// resolve subdomain queries to the same IPs. For example, if
// "node.tailnet.ts.net" is in SubdomainHosts, then queries for
// "anything.node.tailnet.ts.net" will resolve to node's IPs.
func (v ConfigView) SubdomainHosts() views.Map[dnsname.FQDN, struct{}] {
return views.MapOf(v.ж.SubdomainHosts)
}
// OnlyIPv6, if true, uses the IPv6 service IP (for MagicDNS)
// instead of the IPv4 version (100.100.100.100).
func (v ConfigView) OnlyIPv6() bool { return v.ж.OnlyIPv6 }
// MagicDNSHostsUnrouted is whether MagicDNS host records are
// served on demand via [resolver.MagicDNSHosts] (so are not
// listed in Hosts) without being covered by any Routes entry;
// that is, MagicDNS domain routing is off. It preserves the
// effect the node records had when they were listed in Hosts:
// hasHostsWithoutSplitDNSRoutes reports true, keeping quad-100
// in the OS resolver path so the names still resolve.
func (v ConfigView) MagicDNSHostsUnrouted() bool { return v.ж.MagicDNSHostsUnrouted }
func (v ConfigView) Equal(v2 ConfigView) bool { return v.ж.Equal(v2.ж) }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _ConfigViewNeedsRegeneration = Config(struct {
AcceptDNS bool
DefaultResolvers []*dnstype.Resolver
Routes map[dnsname.FQDN][]*dnstype.Resolver
SearchDomains []dnsname.FQDN
Hosts map[dnsname.FQDN][]netip.Addr
SubdomainHosts set.Set[dnsname.FQDN]
OnlyIPv6 bool
MagicDNSHostsUnrouted bool
}{})
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !windows
package dns
func flushCaches() error {
return nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package dns
import (
"bufio"
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"net/netip"
"runtime"
"slices"
"strings"
"sync"
"sync/atomic"
"time"
"tailscale.com/control/controlknobs"
"tailscale.com/envknob"
"tailscale.com/feature/buildfeatures"
"tailscale.com/health"
"tailscale.com/net/dns/resolver"
"tailscale.com/net/netmon"
"tailscale.com/net/tsdial"
"tailscale.com/syncs"
"tailscale.com/types/dnstype"
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
"tailscale.com/util/dnsname"
"tailscale.com/util/eventbus"
"tailscale.com/util/slicesx"
"tailscale.com/util/syspolicy/policyclient"
"tailscale.com/version"
)
var (
errFullQueue = errors.New("request queue full")
// ErrNoDNSConfig is returned by RecompileDNSConfig when the Manager
// has no existing DNS configuration.
ErrNoDNSConfig = errors.New("no DNS configuration")
)
// maxActiveQueries returns the maximal number of DNS requests that can
// be running.
const maxActiveQueries = 256
// ResponseMapper is a function that accepts the bytes representing
// a DNS response and returns bytes representing a DNS response.
// Used to observe and/or mutate DNS responses managed by this manager.
type ResponseMapper func([]byte) []byte
// We use file-ignore below instead of ignore because on some platforms,
// the lint exception is necessary and on others it is not,
// and plain ignore complains if the exception is unnecessary.
// Manager manages system DNS settings.
type Manager struct {
logf logger.Logf
health *health.Tracker
eventClient *eventbus.Client
activeQueriesAtomic int32
ctx context.Context // good until Down
ctxCancel context.CancelFunc // closes ctx
resolver *resolver.Resolver
os OSConfigurator
knobs *controlknobs.Knobs // or nil
goos string // if empty, gets set to runtime.GOOS
mu sync.Mutex // guards following
config *Config // Tracks the last viable DNS configuration set by Set. nil on failures other than compilation failures or if set has never been called.
queryResponseMapper ResponseMapper
}
// NewManager created a new manager from the given config.
//
// knobs may be nil.
func NewManager(logf logger.Logf, oscfg OSConfigurator, health *health.Tracker, dialer *tsdial.Dialer, linkSel resolver.ForwardLinkSelector, knobs *controlknobs.Knobs, goos string, bus *eventbus.Bus) *Manager {
if !buildfeatures.HasDNS {
return nil
}
if dialer == nil {
panic("nil Dialer")
}
if dialer.NetMon() == nil {
panic("Dialer has nil NetMon")
}
logf = logger.WithPrefix(logf, "dns: ")
if goos == "" {
goos = runtime.GOOS
}
m := &Manager{
logf: logf,
resolver: resolver.New(logf, linkSel, dialer, health, knobs),
os: oscfg,
health: health,
knobs: knobs,
goos: goos,
}
m.eventClient = bus.Client("dns.Manager")
eventbus.SubscribeFunc(m.eventClient, func(trample TrampleDNS) {
m.mu.Lock()
defer m.mu.Unlock()
if m.config == nil {
m.logf("resolve.conf was trampled, but there is no DNS config")
return
}
m.logf("resolve.conf was trampled, setting existing config again")
if err := m.setLocked(*m.config); err != nil {
m.logf("error setting DNS config: %s", err)
}
})
m.ctx, m.ctxCancel = context.WithCancel(context.Background())
m.logf("using %T", m.os)
return m
}
// Resolver returns the Manager's DNS Resolver.
func (m *Manager) Resolver() *resolver.Resolver {
if !buildfeatures.HasDNS {
return nil
}
return m.resolver
}
// ProbeLocks acquires and releases the manager's internal mutexes.
func (m *Manager) ProbeLocks() {
m.mu.Lock()
m.mu.Unlock()
if r := m.Resolver(); r != nil {
r.ProbeLocks()
}
}
// RecompileDNSConfig recompiles the last attempted DNS configuration, which has
// the side effect of re-querying the OS's interface nameservers. This should be used
// on platforms where the interface nameservers can change. Darwin, for example,
// where the nameservers aren't always available when we process a major interface
// change event, or platforms where the nameservers may change while tunnel is up.
//
// This should be called if it is determined that [OSConfigurator.GetBaseConfig] may
// give a better or different result than when [Manager.Set] was last called. The
// logic for making that determination is up to the caller.
//
// It returns [ErrNoDNSConfig] if [Manager.Set] has never been called.
func (m *Manager) RecompileDNSConfig() error {
if !buildfeatures.HasDNS {
return nil
}
m.mu.Lock()
defer m.mu.Unlock()
if m.config != nil {
return m.setLocked(*m.config)
}
return ErrNoDNSConfig
}
func (m *Manager) Set(cfg Config) error {
if !buildfeatures.HasDNS {
return nil
}
m.mu.Lock()
defer m.mu.Unlock()
return m.setLocked(cfg)
}
// GetBaseConfig returns the current base OS DNS configuration as provided by the OSConfigurator.
func (m *Manager) GetBaseConfig() (OSConfig, error) {
if !buildfeatures.HasDNS {
panic("unreachable")
}
return m.os.GetBaseConfig()
}
// setLocked sets the DNS configuration.
//
// m.mu must be held.
func (m *Manager) setLocked(cfg Config) error {
syncs.AssertLocked(&m.mu)
m.logf("Set: %v", logger.ArgWriter(func(w *bufio.Writer) {
cfg.WriteToBufioWriter(w)
}))
rcfg, ocfg, err := m.compileConfig(cfg)
if err != nil {
// On a compilation failure, set m.config set for later reuse by
// [Manager.RecompileDNSConfig] and return the error.
m.config = &cfg
return err
}
m.logf("Resolvercfg: %v", logger.ArgWriter(func(w *bufio.Writer) {
rcfg.WriteToBufioWriter(w)
}))
m.logf("OScfg: %v", logger.ArgWriter(func(w *bufio.Writer) {
ocfg.WriteToBufioWriter(w)
}))
if err := m.resolver.SetConfig(rcfg); err != nil {
m.config = nil
return err
}
if err := m.setDNSLocked(ocfg); err != nil {
return err
}
m.health.SetHealthy(osConfigurationSetWarnable)
m.config = &cfg
return nil
}
func (m *Manager) setDNSLocked(ocfg OSConfig) error {
if err := m.os.SetDNS(ocfg); err != nil {
m.config = nil
m.health.SetUnhealthy(osConfigurationSetWarnable, health.Args{health.ArgError: err.Error()})
return err
}
return nil
}
// compileHostEntries creates a list of single-label resolutions possible
// from the configured hosts and search domains.
// The entries are compiled in the order of the search domains, then the hosts.
// The returned list is sorted by the first hostname in each entry.
func compileHostEntries(cfg Config) (hosts []*HostEntry) {
didLabel := make(map[string]bool, len(cfg.Hosts))
hostsMap := make(map[netip.Addr]*HostEntry, len(cfg.Hosts))
for _, sd := range cfg.SearchDomains {
for h, ips := range cfg.Hosts {
if !sd.Contains(h) || h.NumLabels() != (sd.NumLabels()+1) {
continue
}
ipHosts := []string{string(h.WithTrailingDot())}
if label := dnsname.FirstLabel(string(h)); !didLabel[label] {
didLabel[label] = true
ipHosts = append(ipHosts, label)
}
for _, ip := range ips {
if cfg.OnlyIPv6 && ip.Is4() {
continue
}
if e := hostsMap[ip]; e != nil {
e.Hosts = append(e.Hosts, ipHosts...)
} else {
hostsMap[ip] = &HostEntry{
Addr: ip,
Hosts: ipHosts,
}
}
// Only add IPv4 or IPv6 per host, like we do in the resolver.
break
}
}
}
if len(hostsMap) == 0 {
return nil
}
hosts = slicesx.MapValues(hostsMap)
slices.SortFunc(hosts, func(a, b *HostEntry) int {
if len(a.Hosts) == 0 && len(b.Hosts) == 0 {
return 0
} else if len(a.Hosts) == 0 {
return -1
} else if len(b.Hosts) == 0 {
return 1
}
return strings.Compare(a.Hosts[0], b.Hosts[0])
})
return hosts
}
// OSConfigurationReadWarnable is a Warnable set when Tailscale cannot read the
// DNS configuration the OS was using before Tailscale took over. It is
// exported so that a test can name it rather than repeat its wording.
var OSConfigurationReadWarnable = health.Register(&health.Warnable{
Code: "dns-read-os-config-failed",
Title: "Failed to read system DNS configuration",
Text: func(args health.Args) string {
return fmt.Sprintf("Tailscale failed to fetch the DNS configuration of your device: %v", args[health.ArgError])
},
Severity: health.SeverityLow,
DependsOn: []*health.Warnable{health.NetworkStatusWarnable},
})
var osConfigurationSetWarnable = health.Register(&health.Warnable{
Code: "dns-set-os-config-failed",
Title: "Failed to set system DNS configuration",
Text: func(args health.Args) string {
return fmt.Sprintf("Tailscale failed to set the DNS configuration of your device: %v", args[health.ArgError])
},
Severity: health.SeverityMedium,
DependsOn: []*health.Warnable{health.NetworkStatusWarnable},
})
// compileConfig converts cfg into a quad-100 resolver configuration
// and an OS-level configuration.
func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig, err error) {
// The internal resolver always gets MagicDNS hosts and
// authoritative suffixes, even if we don't propagate MagicDNS to
// the OS.
rcfg.Hosts = cfg.Hosts
rcfg.SubdomainHosts = cfg.SubdomainHosts
rcfg.AcceptDNS = cfg.AcceptDNS
routes := map[dnsname.FQDN][]*dnstype.Resolver{} // assigned conditionally to rcfg.Routes below.
var propagateHostsToOS bool
for suffix, resolvers := range cfg.Routes {
if len(resolvers) == 0 {
propagateHostsToOS = true
rcfg.LocalDomains = append(rcfg.LocalDomains, suffix)
} else {
routes[suffix] = resolvers
}
}
// LocalDomains is an unordered suffix set, but it comes out of map
// iteration; sort it so equal configs compare and log equal.
slices.Sort(rcfg.LocalDomains)
// Similarly, the OS always gets search paths.
ocfg.SearchDomains = cfg.SearchDomains
if propagateHostsToOS && m.goos == "windows" {
ocfg.Hosts = compileHostEntries(cfg)
}
// Deal with trivial configs first.
switch {
case !cfg.needsOSResolver() || runtime.GOOS == "plan9":
// Set search domains, but nothing else. This also covers the
// case where cfg is entirely zero, in which case these
// configs clear all Tailscale DNS settings.
return rcfg, ocfg, nil
case cfg.hasDefaultIPResolversOnly() && !cfg.hasHostsWithoutSplitDNSRoutes():
// Trivial CorpDNS configuration, just override the OS resolver.
//
// If there are hosts (ExtraRecords) that are not covered by an existing
// SplitDNS route, then we don't go into this path so that we fall into
// the next case and send the extra record hosts queries through
// 100.100.100.100 instead where we can answer them.
//
// TODO: for OSes that support it, pass IP:port and DoH
// addresses directly to OS.
// https://github.com/tailscale/tailscale/issues/1666
ocfg.Nameservers = toIPsOnly(cfg.DefaultResolvers)
return rcfg, ocfg, nil
case cfg.hasDefaultResolvers():
// Default resolvers plus other stuff always ends up proxying
// through quad-100.
rcfg.Routes = routes
rcfg.Routes["."] = cfg.DefaultResolvers
ocfg.Nameservers = cfg.serviceIPs(m.knobs)
return rcfg, ocfg, nil
}
// From this point on, we're figuring out split DNS
// configurations. The possible cases don't return directly any
// more, because as a final step we have to handle the case where
// the OS can't do split DNS.
// Workaround for
// https://github.com/tailscale/corp/issues/1662. Even though
// Windows natively supports split DNS, it only configures linux
// containers using whatever the primary is, and doesn't apply
// NRPT rules to DNS traffic coming from WSL.
//
// In order to make WSL work okay when the host Windows is using
// Tailscale, we need to set up quad-100 as a "full proxy"
// resolver, regardless of whether Windows itself can do split
// DNS. We still make Windows do split DNS itself when it can, but
// quad-100 will still have the full split configuration as well,
// and so can service WSL requests correctly.
//
// This bool is used in a couple of places below to implement this
// workaround.
isWindows := m.goos == "windows"
isIOS := m.goos == "ios"
isSandboxedMac := m.goos == "darwin" && isSandboxedMacOS()
supportsSplitDNS := m.os.SupportsSplitDNS()
isSandboxedApple := isIOS || isSandboxedMac
// Apple platforms keep split-domain traffic pointed at quad-100 rather than
// handing the upstream resolvers to the OS directly, because those resolvers
// may only be reachable through the tunnel.
if supportsSplitDNS && !isWindows && !isSandboxedApple {
if srs := toIPsOnly(cfg.singleResolverSet()); len(srs) > 0 {
// Split DNS configuration requested, where all split domains
// go to the same resolvers. We can let the OS do it.
ocfg.Nameservers = srs
ocfg.MatchDomains = cfg.matchDomains()
return rcfg, ocfg, nil
}
}
// Split DNS configuration with either multiple upstream routes,
// or routes + MagicDNS, or just MagicDNS, or on an OS that cannot
// split-DNS. Install a split config pointing at quad-100.
rcfg.Routes = routes
ocfg.Nameservers = cfg.serviceIPs(m.knobs)
// usePrimaryResolver forces quad-100 to be installed as the OS's primary
// (catch-all) resolver rather than scoped to the match domains. iOS always
// does this (it has no way to selectively answer ExtraRecords). Sandboxed
// macOS did too until control opts it into scoping via
// NodeAttrScopeQuad100OnMacOS, so that a user's DoH system profile isn't
// shadowed by quad-100. See tailscale/corp#45534.
usePrimaryResolver := isIOS || (isSandboxedMac && !m.scopeQuad100OnMacOS())
if supportsSplitDNS && !usePrimaryResolver && !cfg.requiresPrimaryResolver() {
ocfg.MatchDomains = cfg.matchDomains()
return rcfg, ocfg, nil
}
// Even though iOS devices can do split DNS, they don't provide a way to
// selectively answer ExtraRecords, and ignore other DNS traffic. As a
// workaround, we read the existing default resolver configuration and use
// that as the forwarder for all DNS traffic that quad-100 doesn't handle.
//
// If the OS can't do native split-DNS, read out the underlying resolver
// config and blend it into our config. On iOS, [OSConfigurator.GetBaseConfig]
// has a tendency to temporarily fail if called immediately following an
// interface change. These failures should be retried if/when the OS indicates
// that the DNS configuration has changed via [RecompileDNSConfig].
base, err := m.os.GetBaseConfig()
if err != nil {
if (isIOS || isNoopManager(m.os) || (supportsSplitDNS && !isSandboxedMac)) && err == ErrGetBaseConfigNotSupported {
// No base config to blend in: noopManager (userspace networking),
// some iOS builds, or a split-DNS manager that has none by
// construction (e.g. systemd-resolved). Fall back to a scoped
// config instead of erroring and leaving the old OS config.
// Sandboxed macOS is excluded: it does have a base config
// (/etc/resolv.conf), so this error is a real read failure there.
m.health.SetHealthy(OSConfigurationReadWarnable)
ocfg.MatchDomains = cfg.matchDomains()
return rcfg, ocfg, nil
}
m.health.SetUnhealthy(OSConfigurationReadWarnable, health.Args{health.ArgError: err.Error()})
return resolver.Config{}, OSConfig{}, err
}
m.health.SetHealthy(OSConfigurationReadWarnable)
// On iOS only (for now), check if all route names point to resources inside the tailnet.
// If so, we can set those names as MatchDomains to enable a split DNS configuration
// which will help preserve battery life.
// Because on iOS MatchDomains must equal SearchDomains, we cannot do this when
// we have any Routes outside the tailnet. Otherwise when app connectors are enabled,
// a query for 'work-laptop' might lead to search domain expansion, resolving
// as 'work-laptop.aws.com' for example.
if isIOS && rcfg.RoutesRequireNoCustomResolvers() {
if !m.disableSplitDNSOptimization() {
for r := range rcfg.Routes {
ocfg.MatchDomains = append(ocfg.MatchDomains, r)
}
} else {
m.logf("iOS split DNS is disabled by nodeattr")
}
}
var defaultRoutes []*dnstype.Resolver
for _, ip := range base.Nameservers {
defaultRoutes = append(defaultRoutes, &dnstype.Resolver{Addr: ip.String()})
}
rcfg.Routes["."] = defaultRoutes
// Append base config search domains, but only if not already present.
// This prevents duplicates when GetBaseConfig() reads back domains that
// Tailscale itself previously wrote to resolv.conf.
for _, domain := range base.SearchDomains {
if !slices.Contains(ocfg.SearchDomains, domain) {
ocfg.SearchDomains = append(ocfg.SearchDomains, domain)
}
}
return rcfg, ocfg, nil
}
func (m *Manager) disableSplitDNSOptimization() bool {
return m.knobs != nil && m.knobs.DisableSplitDNSWhenNoCustomResolvers.Load()
}
var scopeQuad100OnMacOSEnv = envknob.RegisterOptBool("TS_DEBUG_SCOPE_QUAD100_MACOS")
// scopeQuad100OnMacOS reports whether sandboxed macOS should scope quad-100 to
// its match domains rather than installing it as the OS's primary resolver.
// Off (false) unless control sets NodeAttrScopeQuad100OnMacOS, or the
// TS_DEBUG_SCOPE_QUAD100_MACOS env override is set. See tailscale/corp#45534.
func (m *Manager) scopeQuad100OnMacOS() bool {
if v, ok := scopeQuad100OnMacOSEnv().Get(); ok {
return v
}
return m.knobs != nil && m.knobs.ScopeQuad100OnMacOS.Load()
}
var isSandboxedMacOS = version.IsSandboxedMacOS
// toIPsOnly returns only the IP portion of dnstype.Resolver.
// Only safe to use if the resolvers slice has been cleared of
// DoH or custom-port entries with something like hasDefaultIPResolversOnly.
func toIPsOnly(resolvers []*dnstype.Resolver) (ret []netip.Addr) {
for _, r := range resolvers {
if ipp, ok := r.IPPort(); ok && ipp.Port() == 53 {
ret = append(ret, ipp.Addr())
}
}
return ret
}
// Query executes a DNS query received from the given address. The query is
// provided in bs as a wire-encoded DNS query without any transport header.
// This method is called for requests arriving over UDP and TCP.
//
// The "family" parameter should indicate what type of DNS query this is:
// either "tcp" or "udp".
func (m *Manager) Query(ctx context.Context, bs []byte, family string, from netip.AddrPort) ([]byte, error) {
select {
case <-m.ctx.Done():
return nil, net.ErrClosed
default:
// continue
}
if n := atomic.AddInt32(&m.activeQueriesAtomic, 1); n > maxActiveQueries {
atomic.AddInt32(&m.activeQueriesAtomic, -1)
metricDNSQueryErrorQueue.Add(1)
return nil, errFullQueue
}
defer atomic.AddInt32(&m.activeQueriesAtomic, -1)
outbs, err := m.resolver.Query(ctx, bs, family, from)
if err != nil {
return outbs, err
}
m.mu.Lock()
defer m.mu.Unlock()
if m.queryResponseMapper != nil {
outbs = m.queryResponseMapper(outbs)
}
return outbs, err
}
const (
// RFC 7766 6.2 recommends connection reuse & request pipelining
// be undertaken, and the connection be closed by the server
// using an idle timeout on the order of seconds.
idleTimeoutTCP = 45 * time.Second
// The RFCs don't specify the max size of a TCP-based DNS query,
// but we want to keep this reasonable. Given payloads are typically
// much larger and all known client send a single query, I've arbitrarily
// chosen 4k.
maxReqSizeTCP = 4096
)
// TrampleDNS is an event indicating we detected that DNS config was
// overwritten by another process.
type TrampleDNS struct {
LastTrample time.Time
TramplesInTimeout int64
}
// dnsTCPSession services DNS requests sent over TCP.
type dnsTCPSession struct {
m *Manager
conn net.Conn
srcAddr netip.AddrPort
readClosing chan struct{}
responses chan []byte // DNS replies pending writing
ctx context.Context
closeCtx context.CancelFunc
}
func (s *dnsTCPSession) handleWrites() {
defer s.conn.Close()
defer s.closeCtx()
// NOTE(andrew): we explicitly do not close the 'responses' channel
// when this function exits. If we hit an error and return, we could
// still have outstanding 'handleQuery' goroutines running, and if we
// closed this channel they'd end up trying to send on a closed channel
// when they finish.
//
// Because we call closeCtx, those goroutines will not hang since they
// select on <-s.ctx.Done() as well as s.responses.
for {
select {
case <-s.readClosing:
return // connection closed or timeout, teardown time
case resp := <-s.responses:
s.conn.SetWriteDeadline(time.Now().Add(idleTimeoutTCP))
if err := binary.Write(s.conn, binary.BigEndian, uint16(len(resp))); err != nil {
s.m.logf("tcp write (len): %v", err)
return
}
if _, err := s.conn.Write(resp); err != nil {
s.m.logf("tcp write (response): %v", err)
return
}
}
}
}
func (s *dnsTCPSession) handleQuery(q []byte) {
resp, err := s.m.Query(s.ctx, q, "tcp", s.srcAddr)
if err != nil {
s.m.logf("tcp query: %v", err)
return
}
// See note in handleWrites (above) regarding this select{}
select {
case <-s.ctx.Done():
case s.responses <- resp:
}
}
func (s *dnsTCPSession) handleReads() {
defer s.conn.Close()
defer close(s.readClosing)
for {
select {
case <-s.ctx.Done():
return
default:
s.conn.SetReadDeadline(time.Now().Add(idleTimeoutTCP))
var reqLen uint16
if err := binary.Read(s.conn, binary.BigEndian, &reqLen); err != nil {
if err == io.EOF || err == io.ErrClosedPipe {
return // connection closed nominally, we gucci
}
s.m.logf("tcp read (len): %v", err)
return
}
if int(reqLen) > maxReqSizeTCP {
s.m.logf("tcp request too large (%d > %d)", reqLen, maxReqSizeTCP)
return
}
buf := make([]byte, int(reqLen))
if _, err := io.ReadFull(s.conn, buf); err != nil {
s.m.logf("tcp read (payload): %v", err)
return
}
select {
case <-s.ctx.Done():
return
default:
// NOTE: by kicking off the query handling in a
// new goroutine, it is possible that we'll
// deliver responses out-of-order. This is
// explicitly allowed by RFC7766, Section
// 6.2.1.1 ("Query Pipelining").
go s.handleQuery(buf)
}
}
}
}
// HandleTCPConn implements magicDNS over TCP, taking a connection and
// servicing DNS requests sent down it.
func (m *Manager) HandleTCPConn(conn net.Conn, srcAddr netip.AddrPort) {
s := dnsTCPSession{
m: m,
conn: conn,
srcAddr: srcAddr,
responses: make(chan []byte),
readClosing: make(chan struct{}),
}
s.ctx, s.closeCtx = context.WithCancel(m.ctx)
go s.handleReads()
s.handleWrites()
}
func (m *Manager) Down() error {
if !buildfeatures.HasDNS {
return nil
}
m.ctxCancel()
if err := m.os.Close(); err != nil {
return err
}
m.eventClient.Close()
m.resolver.Close()
return nil
}
func (m *Manager) FlushCaches() error {
if !buildfeatures.HasDNS {
return nil
}
return flushCaches()
}
// CleanUp restores the system DNS 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.
//
// health must not be nil
func CleanUp(logf logger.Logf, netMon *netmon.Monitor, bus *eventbus.Bus, health *health.Tracker, interfaceName string) {
if !buildfeatures.HasDNS {
return
}
oscfg, err := NewOSConfigurator(logf, health, bus, policyclient.Get(), nil, interfaceName)
if err != nil {
logf("creating dns cleanup: %v", err)
return
}
d := &tsdial.Dialer{Logf: logf}
d.SetNetMon(netMon)
d.SetBus(bus)
dns := NewManager(logf, oscfg, health, d, nil, nil, runtime.GOOS, bus)
if err := dns.Down(); err != nil {
logf("dns down: %v", err)
}
}
var metricDNSQueryErrorQueue = clientmetric.NewCounter("dns_query_local_error_queue")
func (m *Manager) SetQueryResponseMapper(fx ResponseMapper) {
m.mu.Lock()
defer m.mu.Unlock()
m.queryResponseMapper = fx
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && !android
package dns
import (
"bytes"
"errors"
"fmt"
"os"
"strings"
"sync"
"time"
"tailscale.com/control/controlknobs"
"tailscale.com/feature"
"tailscale.com/feature/buildfeatures"
"tailscale.com/health"
"tailscale.com/net/netaddr"
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
"tailscale.com/util/eventbus"
"tailscale.com/util/syspolicy/policyclient"
"tailscale.com/version/distro"
)
type kv struct {
k, v string
}
func (kv kv) String() string {
return fmt.Sprintf("%s=%s", kv.k, kv.v)
}
var publishOnce sync.Once
// reconfigTimeout is the time interval within which Manager.{Up,Down} should complete.
//
// This is particularly useful because certain conditions can cause indefinite hangs
// (such as improper dbus auth followed by contextless dbus.Object.Call).
// Such operations should be wrapped in a timeout context.
const reconfigTimeout = time.Second
// Set unless ts_omit_networkmanager
var (
optNewNMManager feature.Hook[func(ifName string) (OSConfigurator, error)]
optNMIsUsingResolved feature.Hook[func() error]
optNMVersionBetween feature.Hook[func(v1, v2 string) (bool, error)]
)
// Set unless ts_omit_resolved
var (
optNewResolvedManager feature.Hook[func(logf logger.Logf, health *health.Tracker, interfaceName string) (OSConfigurator, error)]
)
// Set unless ts_omit_dbus
var (
optDBusPing feature.Hook[func(name, objectPath string) error]
optDBusReadString feature.Hook[func(name, objectPath, iface, member string) (string, error)]
)
// NewOSConfigurator created a new OS configurator.
//
// The health tracker may be nil; the knobs may be nil and are ignored on this platform.
func NewOSConfigurator(logf logger.Logf, health *health.Tracker, bus *eventbus.Bus, _ policyclient.Client, _ *controlknobs.Knobs, interfaceName string) (ret OSConfigurator, err error) {
if !buildfeatures.HasDNS || distro.Get() == distro.JetKVM {
return NewNoopManager()
}
env := newOSConfigEnv{
fs: directFS{},
resolvconfStyle: resolvconfStyle,
}
if f, ok := optDBusPing.GetOk(); ok {
env.dbusPing = f
} else {
env.dbusPing = func(_, _ string) error { return errors.ErrUnsupported }
}
if f, ok := optDBusReadString.GetOk(); ok {
env.dbusReadString = f
} else {
env.dbusReadString = func(_, _, _, _ string) (string, error) { return "", errors.ErrUnsupported }
}
if f, ok := optNMIsUsingResolved.GetOk(); ok {
env.nmIsUsingResolved = f
} else {
env.nmIsUsingResolved = func() error { return errors.ErrUnsupported }
}
env.nmVersionBetween, _ = optNMVersionBetween.GetOk() // GetOk to not panic if nil; unused if optNMIsUsingResolved returns an error
mode, err := dnsMode(logf, health, env)
if err != nil {
return nil, err
}
publishOnce.Do(func() {
sanitizedMode := strings.ReplaceAll(mode, "-", "_")
m := clientmetric.NewGauge(fmt.Sprintf("dns_manager_linux_mode_%s", sanitizedMode))
m.Set(1)
})
logf("dns: using %q mode", mode)
switch mode {
case "direct":
return newDirectManagerOnFS(logf, health, bus, env.fs), nil
case "systemd-resolved":
if f, ok := optNewResolvedManager.GetOk(); ok {
return f(logf, health, interfaceName)
}
return nil, fmt.Errorf("tailscaled was built without DNS %q support", mode)
case "network-manager":
if f, ok := optNewNMManager.GetOk(); ok {
return f(interfaceName)
}
return nil, fmt.Errorf("tailscaled was built without DNS %q support", mode)
case "debian-resolvconf":
return newDebianResolvconfManager(logf)
case "openresolv":
return newOpenresolvManager(logf)
default:
logf("[unexpected] detected unknown DNS mode %q, using direct manager as last resort", mode)
}
return newDirectManagerOnFS(logf, health, bus, env.fs), nil
}
// newOSConfigEnv are the funcs newOSConfigurator needs, pulled out for testing.
type newOSConfigEnv struct {
fs wholeFileFS
dbusPing func(string, string) error
dbusReadString func(string, string, string, string) (string, error)
nmIsUsingResolved func() error
nmVersionBetween func(v1, v2 string) (safe bool, err error)
resolvconfStyle func() string
}
func dnsMode(logf logger.Logf, health *health.Tracker, env newOSConfigEnv) (ret string, err error) {
var debug []kv
dbg := func(k, v string) {
debug = append(debug, kv{k, v})
}
defer func() {
if ret != "" {
dbg("ret", ret)
}
logf("dns: %v", debug)
}()
// In all cases that we detect systemd-resolved, try asking it what it
// thinks the current resolv.conf mode is so we can add it to our logs.
defer func() {
if ret != "systemd-resolved" {
return
}
// Try to ask systemd-resolved what it thinks the current
// status of resolv.conf is. This is documented at:
// https://www.freedesktop.org/software/systemd/man/org.freedesktop.resolve1.html
mode, err := env.dbusReadString("org.freedesktop.resolve1", "/org/freedesktop/resolve1", "org.freedesktop.resolve1.Manager", "ResolvConfMode")
if err != nil {
logf("dns: ResolvConfMode error: %v", err)
dbg("resolv-conf-mode", "error")
} else {
dbg("resolv-conf-mode", mode)
}
}()
// Before we read /etc/resolv.conf (which might be in a broken
// or symlink-dangling state), try to ping the D-Bus service
// for systemd-resolved. If it's active on the machine, this
// will make it start up and write the /etc/resolv.conf file
// before it replies to the ping. (see how systemd's
// src/resolve/resolved.c calls manager_write_resolv_conf
// before the sd_event_loop starts)
resolvedUp := env.dbusPing("org.freedesktop.resolve1", "/org/freedesktop/resolve1") == nil
if resolvedUp {
dbg("resolved-ping", "yes")
}
bs, err := env.fs.ReadFile(resolvConf)
if os.IsNotExist(err) {
dbg("rc", "missing")
return "direct", nil
}
if err != nil {
return "", fmt.Errorf("reading /etc/resolv.conf: %w", err)
}
switch resolvOwner(bs) {
case "systemd-resolved":
dbg("rc", "resolved")
// Some systems, for reasons known only to them, have a
// resolv.conf that has the word "systemd-resolved" in its
// header, but doesn't actually point to resolved. We mustn't
// try to program resolved in that case.
// https://github.com/tailscale/tailscale/issues/2136
if err := resolvedIsActuallyResolver(logf, env, dbg, bs); err != nil {
logf("dns: resolvedIsActuallyResolver error: %v", err)
dbg("resolved", "not-in-use")
return "direct", nil
}
if err := env.dbusPing("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/DnsManager"); err != nil {
dbg("nm", "no")
return "systemd-resolved", nil
}
dbg("nm", "yes")
if err := env.nmIsUsingResolved(); err != nil {
dbg("nm-resolved", "no")
return "systemd-resolved", nil
}
dbg("nm-resolved", "yes")
// Version of NetworkManager before 1.26.6 programmed resolved
// incorrectly, such that NM's settings would always take
// precedence over other settings set by other resolved
// clients.
//
// If we're dealing with such a version, we have to set our
// DNS settings through NM to have them take.
//
// However, versions 1.26.6 later both fixed the resolved
// programming issue _and_ started ignoring DNS settings for
// "unmanaged" interfaces - meaning NM 1.26.6 and later
// actively ignore DNS configuration we give it. So, for those
// NM versions, we can and must use resolved directly.
//
// Even more fun, even-older versions of NM won't let us set
// DNS settings if the interface isn't managed by NM, with a
// hard failure on DBus requests. Empirically, NM 1.22 does
// this. Based on the versions popular distros shipped, we
// conservatively decree that only 1.26.0 through 1.26.5 are
// "safe" to use for our purposes. This roughly matches
// distros released in the latter half of 2020.
//
// In a perfect world, we'd avoid this by replacing
// configuration out from under NM entirely (e.g. using
// directManager to overwrite resolv.conf), but in a world
// where resolved runs, we need to get correct configuration
// into resolved regardless of what's in resolv.conf (because
// resolved can also be queried over dbus, or via an NSS
// module that bypasses /etc/resolv.conf). Given that we must
// get correct configuration into resolved, we have no choice
// but to use NM, and accept the loss of IPv6 configuration
// that comes with it (see
// https://github.com/tailscale/tailscale/issues/1699,
// https://github.com/tailscale/tailscale/pull/1945)
safe, err := env.nmVersionBetween("1.26.0", "1.26.5")
if err != nil {
// Failed to figure out NM's version, can't make a correct
// decision.
return "", fmt.Errorf("checking NetworkManager version: %v", err)
}
if safe {
dbg("nm-safe", "yes")
return "network-manager", nil
}
dbg("nm-safe", "no")
return "systemd-resolved", nil
case "resolvconf":
dbg("rc", "resolvconf")
style := env.resolvconfStyle()
switch style {
case "":
dbg("resolvconf", "no")
return "direct", nil
case "debian":
dbg("resolvconf", "debian")
return "debian-resolvconf", nil
case "openresolv":
dbg("resolvconf", "openresolv")
return "openresolv", nil
default:
// Shouldn't happen, that means we updated flavors of
// resolvconf without updating here.
dbg("resolvconf", style)
logf("[unexpected] got unknown flavor of resolvconf %q, falling back to direct manager", env.resolvconfStyle())
return "direct", nil
}
case "NetworkManager":
dbg("rc", "nm")
// Sometimes, NetworkManager owns the configuration but points
// it at systemd-resolved.
if err := resolvedIsActuallyResolver(logf, env, dbg, bs); err != nil {
logf("dns: resolvedIsActuallyResolver error: %v", err)
dbg("resolved", "not-in-use")
// You'd think we would use newNMManager here. However, as
// explained in
// https://github.com/tailscale/tailscale/issues/1699 ,
// using NetworkManager for DNS configuration carries with
// it the cost of losing IPv6 configuration on the
// Tailscale network interface. So, when we can avoid it,
// we bypass NetworkManager by replacing resolv.conf
// directly.
//
// If you ever try to put NMManager back here, keep in mind
// that versions >=1.26.6 will ignore DNS configuration
// anyway, so you still need a fallback path that uses
// directManager.
return "direct", nil
}
dbg("nm-resolved", "yes")
// See large comment above for reasons we'd use NM rather than
// resolved. systemd-resolved is actually in charge of DNS
// configuration, but in some cases we might need to configure
// it via NetworkManager. All the logic below is probing for
// that case: is NetworkManager running? If so, is it one of
// the versions that requires direct interaction with it?
if err := env.dbusPing("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/DnsManager"); err != nil {
dbg("nm", "no")
return "systemd-resolved", nil
}
safe, err := env.nmVersionBetween("1.26.0", "1.26.5")
if err != nil {
// Failed to figure out NM's version, can't make a correct
// decision.
return "", fmt.Errorf("checking NetworkManager version: %v", err)
}
if safe {
dbg("nm-safe", "yes")
return "network-manager", nil
}
if err := env.nmIsUsingResolved(); err != nil {
// If systemd-resolved is not running at all, then we don't have any
// other choice: we take direct control of DNS.
dbg("nm-resolved", "no")
return "direct", nil
}
health.SetDNSManagerHealth(errors.New("systemd-resolved and NetworkManager are wired together incorrectly; MagicDNS will probably not work. For more info, see https://tailscale.com/s/resolved-nm"))
dbg("nm-safe", "no")
return "systemd-resolved", nil
default:
dbg("rc", "unknown")
return "direct", nil
}
}
// resolvedIsActuallyResolver reports whether the system is using
// systemd-resolved as the resolver. There are two different ways to
// use systemd-resolved:
// - libnss_resolve, which requires adding `resolve` to the "hosts:"
// line in /etc/nsswitch.conf
// - setting the only nameserver configured in `resolv.conf` to
// systemd-resolved IP (127.0.0.53)
//
// Returns an error if the configuration is something other than
// exclusively systemd-resolved, or nil if the config is only
// systemd-resolved.
func resolvedIsActuallyResolver(logf logger.Logf, env newOSConfigEnv, dbg func(k, v string), bs []byte) error {
if err := isLibnssResolveUsed(env); err == nil {
dbg("resolved", "nss")
return nil
}
cfg, err := readResolv(bytes.NewBuffer(bs))
if err != nil {
return err
}
// We've encountered at least one system where the line
// "nameserver 127.0.0.53" appears twice, so we look exhaustively
// through all of them and allow any number of repeated mentions
// of the systemd-resolved stub IP.
if len(cfg.Nameservers) == 0 {
return errors.New("resolv.conf has no nameservers")
}
for _, ns := range cfg.Nameservers {
if ns != netaddr.IPv4(127, 0, 0, 53) {
return fmt.Errorf("resolv.conf doesn't point to systemd-resolved; points to %v", cfg.Nameservers)
}
}
dbg("resolved", "file")
return nil
}
// isLibnssResolveUsed reports whether libnss_resolve is used
// for resolving names. Returns nil if it is, and an error otherwise.
func isLibnssResolveUsed(env newOSConfigEnv) error {
bs, err := env.fs.ReadFile("/etc/nsswitch.conf")
if err != nil {
return fmt.Errorf("reading /etc/resolv.conf: %w", err)
}
for line := range strings.SplitSeq(string(bs), "\n") {
fields := strings.Fields(line)
if len(fields) < 2 || fields[0] != "hosts:" {
continue
}
for _, module := range fields[1:] {
if module == "dns" {
return fmt.Errorf("dns with a higher priority than libnss_resolve")
}
if module == "resolve" {
return nil
}
}
}
return fmt.Errorf("libnss_resolve not used")
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && !android && !ts_omit_networkmanager
package dns
import (
"context"
"encoding/binary"
"errors"
"fmt"
"net"
"net/netip"
"sort"
"time"
"github.com/godbus/dbus/v5"
"tailscale.com/net/tsaddr"
"tailscale.com/util/cmpver"
"tailscale.com/util/dnsname"
)
const (
highestPriority = int32(-1 << 31)
mediumPriority = int32(1) // Highest priority that doesn't hard-override
lowerPriority = int32(200) // lower than all builtin auto priorities
)
// nmManager uses the NetworkManager DBus API.
type nmManager struct {
interfaceName string
manager dbus.BusObject
dnsManager dbus.BusObject
}
func init() {
optNewNMManager.Set(newNMManager)
optNMIsUsingResolved.Set(nmIsUsingResolved)
optNMVersionBetween.Set(nmVersionBetween)
}
func newNMManager(interfaceName string) (OSConfigurator, error) {
conn, err := dbus.SystemBus()
if err != nil {
return nil, err
}
return &nmManager{
interfaceName: interfaceName,
manager: conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager")),
dnsManager: conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager/DnsManager")),
}, nil
}
type nmConnectionSettings map[string]map[string]dbus.Variant
func (m *nmManager) SetDNS(config OSConfig) error {
ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout)
defer cancel()
// NetworkManager only lets you set DNS settings on "active"
// connections, which requires an assigned IP address. This got
// configured before the DNS manager was invoked, but it might
// take a little time for the netlink notifications to propagate
// up. So, keep retrying for the duration of the reconfigTimeout.
var err error
for ctx.Err() == nil {
err = m.trySet(ctx, config)
if err == nil {
break
}
time.Sleep(10 * time.Millisecond)
}
return err
}
func (m *nmManager) trySet(ctx context.Context, config OSConfig) error {
conn, err := dbus.SystemBus()
if err != nil {
return fmt.Errorf("connecting to system bus: %w", err)
}
// This is how we get at the DNS settings:
//
// org.freedesktop.NetworkManager
// |
// [GetDeviceByIpIface]
// |
// v
// org.freedesktop.NetworkManager.Device <--------\
// (describes a network interface) |
// | |
// [GetAppliedConnection] [Reapply]
// | |
// v |
// org.freedesktop.NetworkManager.Connection |
// (connection settings) ------/
// contains {dns, dns-priority, dns-search}
//
// Ref: https://developer.gnome.org/NetworkManager/stable/settings-ipv4.html.
nm := conn.Object(
"org.freedesktop.NetworkManager",
dbus.ObjectPath("/org/freedesktop/NetworkManager"),
)
var devicePath dbus.ObjectPath
err = nm.CallWithContext(
ctx, "org.freedesktop.NetworkManager.GetDeviceByIpIface", 0,
m.interfaceName,
).Store(&devicePath)
if err != nil {
return fmt.Errorf("getDeviceByIpIface: %w", err)
}
device := conn.Object("org.freedesktop.NetworkManager", devicePath)
var (
settings nmConnectionSettings
version uint64
)
err = device.CallWithContext(
ctx, "org.freedesktop.NetworkManager.Device.GetAppliedConnection", 0,
uint32(0),
).Store(&settings, &version)
if err != nil {
return fmt.Errorf("getAppliedConnection: %w", err)
}
// Frustratingly, NetworkManager represents IPv4 addresses as uint32s,
// although IPv6 addresses are represented as byte arrays.
// Perform the conversion here.
var (
dnsv4 []uint32
dnsv6 [][]byte
)
for _, ip := range config.Nameservers {
b := ip.As16()
if ip.Is4() {
dnsv4 = append(dnsv4, binary.NativeEndian.Uint32(b[12:]))
} else {
dnsv6 = append(dnsv6, b[:])
}
}
// NetworkManager wipes out IPv6 address configuration unless we
// tell it explicitly to keep it. Read out the current interface
// settings and mirror them out to NetworkManager.
var addrs6 []map[string]any
if tsIf, err := net.InterfaceByName(m.interfaceName); err == nil {
addrs, _ := tsIf.Addrs()
for _, a := range addrs {
if ipnet, ok := a.(*net.IPNet); ok {
nip, ok := netip.AddrFromSlice(ipnet.IP)
nip = nip.Unmap()
if ok && tsaddr.IsTailscaleIP(nip) && nip.Is6() {
addrs6 = append(addrs6, map[string]any{
"address": nip.String(),
"prefix": uint32(128),
})
}
}
}
}
seen := map[dnsname.FQDN]bool{}
var search []string
for _, dom := range config.SearchDomains {
if seen[dom] {
continue
}
seen[dom] = true
search = append(search, dom.WithTrailingDot())
}
for _, dom := range config.MatchDomains {
if seen[dom] {
continue
}
seen[dom] = true
search = append(search, "~"+dom.WithTrailingDot())
}
if len(config.MatchDomains) == 0 {
// Non-split routing requested, add an all-domains match.
search = append(search, "~.")
}
// Ideally we would like to disable LLMNR and mdns on the
// interface here, but older NetworkManagers don't understand
// those settings and choke on them, so we don't. Both LLMNR and
// mdns will fail since tailscale0 doesn't do multicast, so it's
// effectively fine. We used to try and enforce LLMNR and mdns
// settings here, but that led to #1870.
ipv4Map := settings["ipv4"]
ipv4Map["dns"] = dbus.MakeVariant(dnsv4)
ipv4Map["dns-search"] = dbus.MakeVariant(search)
// We should only request priority if we have nameservers to set.
if len(dnsv4) == 0 {
ipv4Map["dns-priority"] = dbus.MakeVariant(lowerPriority)
} else if len(config.MatchDomains) > 0 {
// Set a fairly high priority, but don't override all other
// configs when in split-DNS mode.
ipv4Map["dns-priority"] = dbus.MakeVariant(mediumPriority)
} else {
// Negative priority means only the settings from the most
// negative connection get used. The way this mixes with
// per-domain routing is unclear, but it _seems_ that the
// priority applies after routing has found possible
// candidates for a resolution.
ipv4Map["dns-priority"] = dbus.MakeVariant(highestPriority)
}
ipv6Map := settings["ipv6"]
// In IPv6 settings, you're only allowed to provide additional
// static DNS settings in "auto" (SLAAC) or "manual" mode. In
// "manual" mode you also have to specify IP addresses, so we use
// "auto".
//
// NM actually documents that to set just DNS servers, you should
// use "auto" mode and then set ignore auto routes and DNS, which
// basically means "autoconfigure but ignore any autoconfiguration
// results you might get". As a safety, we also say that
// NetworkManager should never try to make us the default route
// (none of its business anyway, we handle our own default
// routing).
ipv6Map["method"] = dbus.MakeVariant("auto")
if len(addrs6) > 0 {
ipv6Map["address-data"] = dbus.MakeVariant(addrs6)
}
ipv6Map["ignore-auto-routes"] = dbus.MakeVariant(true)
ipv6Map["ignore-auto-dns"] = dbus.MakeVariant(true)
ipv6Map["never-default"] = dbus.MakeVariant(true)
ipv6Map["dns"] = dbus.MakeVariant(dnsv6)
ipv6Map["dns-search"] = dbus.MakeVariant(search)
if len(dnsv6) == 0 {
ipv6Map["dns-priority"] = dbus.MakeVariant(lowerPriority)
} else if len(config.MatchDomains) > 0 {
// Set a fairly high priority, but don't override all other
// configs when in split-DNS mode.
ipv6Map["dns-priority"] = dbus.MakeVariant(mediumPriority)
} else {
ipv6Map["dns-priority"] = dbus.MakeVariant(highestPriority)
}
// deprecatedProperties are the properties in interface settings
// that are deprecated by NetworkManager.
//
// In practice, this means that they are returned for reading,
// but submitting a settings object with them present fails
// with hard-to-diagnose errors. They must be removed.
deprecatedProperties := []string{
"addresses", "routes",
}
for _, property := range deprecatedProperties {
delete(ipv4Map, property)
delete(ipv6Map, property)
}
if call := device.CallWithContext(ctx, "org.freedesktop.NetworkManager.Device.Reapply", 0, settings, version, uint32(0)); call.Err != nil {
return fmt.Errorf("reapply: %w", call.Err)
}
return nil
}
func (m *nmManager) SupportsSplitDNS() bool {
var mode string
v, err := m.dnsManager.GetProperty("org.freedesktop.NetworkManager.DnsManager.Mode")
if err != nil {
return false
}
mode, ok := v.Value().(string)
if !ok {
return false
}
// Per NM's documentation, it only does split-DNS when it's
// programming dnsmasq or systemd-resolved. All other modes are
// primary-only.
return mode == "dnsmasq" || mode == "systemd-resolved"
}
func (m *nmManager) GetBaseConfig() (OSConfig, error) {
conn, err := dbus.SystemBus()
if err != nil {
return OSConfig{}, err
}
nm := conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager/DnsManager"))
v, err := nm.GetProperty("org.freedesktop.NetworkManager.DnsManager.Configuration")
if err != nil {
return OSConfig{}, err
}
cfgs, ok := v.Value().([]map[string]dbus.Variant)
if !ok {
return OSConfig{}, fmt.Errorf("unexpected NM config type %T", v.Value())
}
if len(cfgs) == 0 {
return OSConfig{}, nil
}
type dnsPrio struct {
resolvers []netip.Addr
domains []string
priority int32
}
order := make([]dnsPrio, 0, len(cfgs)-1)
for _, cfg := range cfgs {
if name, ok := cfg["interface"]; ok {
if s, ok := name.Value().(string); ok && s == m.interfaceName {
// Config for the tailscale interface, skip.
continue
}
}
var p dnsPrio
if v, ok := cfg["nameservers"]; ok {
if ips, ok := v.Value().([]string); ok {
for _, s := range ips {
ip, err := netip.ParseAddr(s)
if err != nil {
// hmm, what do? Shouldn't really happen.
continue
}
p.resolvers = append(p.resolvers, ip)
}
}
}
if v, ok := cfg["domains"]; ok {
if domains, ok := v.Value().([]string); ok {
p.domains = domains
}
}
if v, ok := cfg["priority"]; ok {
if prio, ok := v.Value().(int32); ok {
p.priority = prio
}
}
order = append(order, p)
}
sort.Slice(order, func(i, j int) bool {
return order[i].priority < order[j].priority
})
var (
ret OSConfig
seenResolvers = map[netip.Addr]bool{}
seenSearch = map[string]bool{}
)
for _, cfg := range order {
for _, resolver := range cfg.resolvers {
if seenResolvers[resolver] {
continue
}
ret.Nameservers = append(ret.Nameservers, resolver)
seenResolvers[resolver] = true
}
for _, dom := range cfg.domains {
if seenSearch[dom] {
continue
}
fqdn, err := dnsname.ToFQDN(dom)
if err != nil {
continue
}
ret.SearchDomains = append(ret.SearchDomains, fqdn)
seenSearch[dom] = true
}
if cfg.priority < 0 {
// exclusive configurations preempt all other
// configurations, so we're done.
break
}
}
return ret, nil
}
func (m *nmManager) Close() error {
// No need to do anything on close, NetworkManager will delete our
// settings when the tailscale interface goes away.
return nil
}
func nmVersionBetween(first, last string) (bool, error) {
conn, err := dbus.SystemBus()
if err != nil {
// DBus probably not running.
return false, err
}
nm := conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager"))
v, err := nm.GetProperty("org.freedesktop.NetworkManager.Version")
if err != nil {
return false, err
}
version, ok := v.Value().(string)
if !ok {
return false, fmt.Errorf("unexpected type %T for NM version", v.Value())
}
outside := cmpver.Compare(version, first) < 0 || cmpver.Compare(version, last) > 0
return !outside, nil
}
func nmIsUsingResolved() error {
conn, err := dbus.SystemBus()
if err != nil {
// DBus probably not running.
return err
}
nm := conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager/DnsManager"))
v, err := nm.GetProperty("org.freedesktop.NetworkManager.DnsManager.Mode")
if err != nil {
return fmt.Errorf("getting NM mode: %w", err)
}
mode, ok := v.Value().(string)
if !ok {
return fmt.Errorf("unexpected type %T for NM DNS mode", v.Value())
}
if mode != "systemd-resolved" {
return errors.New("NetworkManager is not using systemd-resolved for DNS")
}
return nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package dns
type noopManager struct{}
func (m noopManager) SetDNS(OSConfig) error { return nil }
func (m noopManager) SupportsSplitDNS() bool { return false }
func (m noopManager) Close() error { return nil }
func (m noopManager) GetBaseConfig() (OSConfig, error) {
return OSConfig{}, ErrGetBaseConfigNotSupported
}
func NewNoopManager() (noopManager, error) {
return noopManager{}, nil
}
func isNoopManager(c OSConfigurator) bool {
_, ok := c.(noopManager)
return ok
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build (linux && !android) || freebsd || openbsd
package dns
import (
"bytes"
"errors"
"fmt"
"net/netip"
"os/exec"
"slices"
"strings"
"tailscale.com/net/tsaddr"
"tailscale.com/types/logger"
)
// openresolvManager manages DNS configuration using the openresolv
// implementation of the `resolvconf` program.
type openresolvManager struct {
logf logger.Logf
}
func newOpenresolvManager(logf logger.Logf) (openresolvManager, error) {
return openresolvManager{logf}, nil
}
func (m openresolvManager) logCmdErr(cmd *exec.Cmd, err error) {
if err == nil {
return
}
commandStr := fmt.Sprintf("path=%q args=%q", cmd.Path, cmd.Args)
exerr, ok := err.(*exec.ExitError)
if !ok {
m.logf("error running command %s: %v", commandStr, err)
return
}
m.logf("error running command %s stderr=%q exitCode=%d: %v", commandStr, exerr.Stderr, exerr.ExitCode(), err)
}
// openresolvNoSnippetsExitCode is the exit status resolvconf returns when a
// requested config snippet does not exist. Asking for all snippets when none
// are registered returns this status too, because openresolv treats the empty
// result as a missing snippet rather than as an empty list.
const openresolvNoSnippetsExitCode = 2
// readSnippets runs resolvconf with the given arguments and returns its stdout.
// An exit status of openresolvNoSnippetsExitCode is not an error: it returns no
// output and a nil error. Other failures are logged and returned.
//
// Callers must pass either "-i" with no arguments or "-l" with explicit snippet
// names. Only those forms produce empty stdout alongside
// openresolvNoSnippetsExitCode; "-i" with snippet names prints the ones that do
// exist and still exits 2, so its output would be silently dropped.
//
// Stderr is excluded from the returned bytes so that diagnostics like "No
// resolv.conf for key foo" are never parsed as snippet names or resolv.conf
// lines. logCmdErr still logs stderr when a command fails.
func (m openresolvManager) readSnippets(args ...string) ([]byte, error) {
cmd := exec.Command("resolvconf", args...)
out, err := cmd.Output()
if err != nil {
if ee, ok := errors.AsType[*exec.ExitError](err); ok && ee.ExitCode() == openresolvNoSnippetsExitCode {
m.logf("[v1] resolvconf %q found no matching config snippets", args)
return nil, nil
}
m.logCmdErr(cmd, err)
return nil, err
}
return out, nil
}
func (m openresolvManager) deleteTailscaleConfig() error {
cmd := exec.Command("resolvconf", "-f", "-d", "tailscale")
out, err := cmd.CombinedOutput()
if err != nil {
m.logCmdErr(cmd, err)
return fmt.Errorf("running %s: %s", cmd, out)
}
return nil
}
func (m openresolvManager) SetDNS(config OSConfig) error {
if config.IsZero() {
return m.deleteTailscaleConfig()
}
var stdin bytes.Buffer
writeResolvConf(&stdin, config.Nameservers, config.SearchDomains)
cmd := exec.Command("resolvconf", "-m", "0", "-x", "-a", "tailscale")
cmd.Stdin = &stdin
out, err := cmd.CombinedOutput()
if err != nil {
m.logCmdErr(cmd, err)
return fmt.Errorf("running %s: %s", cmd, out)
}
return nil
}
func (m openresolvManager) SupportsSplitDNS() bool {
return false
}
func (m openresolvManager) GetBaseConfig() (OSConfig, error) {
// List the names of all config snippets openresolv is aware
// of. Snippets get listed in priority order (most to least),
// which we'll exploit later.
bs, err := m.readSnippets("-i")
if err != nil {
return OSConfig{}, err
}
others := slices.DeleteFunc(strings.Fields(string(bs)), func(f string) bool {
return f == "tailscale"
})
if len(others) == 0 {
// There are no other snippets, so there is no base config to read.
// Returning early is required, not merely an optimization: a
// "resolvconf -l" with no snippet names lists every snippet,
// including Tailscale's own, which would make quad-100 its own
// upstream. See tailscale/tailscale#20825.
return OSConfig{}, nil
}
// List all resolvconf snippets except our own, and parse that as
// a resolv.conf. This effectively generates a blended config of
// "everyone except tailscale", which is what would be in use if
// tailscale hadn't set exclusive mode.
//
// Note that this is not _entirely_ true. To be perfectly correct,
// we should be looking for other interfaces marked exclusive that
// predated tailscale, and stick to only those. However, in
// practice, openresolv uses are generally quite limited, and boil
// down to 1-2 DHCP leases, for which the correct outcome is a
// blended config like the one we produce here.
out, err := m.readSnippets(append([]string{"-l"}, others...)...)
if err != nil {
return OSConfig{}, err
}
cfg, err := readResolv(bytes.NewReader(out))
if err != nil {
return OSConfig{}, err
}
// Forwarding to the Tailscale service IPs would make quad-100 send
// queries to itself, in an infinite loop, so drop them if another
// snippet names them. See tailscale/tailscale#7816.
var removed bool
cfg.Nameservers = slices.DeleteFunc(cfg.Nameservers, func(ip netip.Addr) bool {
if ip == tsaddr.TailscaleServiceIP() || ip == tsaddr.TailscaleServiceIPv6() {
removed = true
return true
}
return false
})
if removed {
m.logf("[v1] dropped Tailscale service IP from openresolv base config")
}
return cfg, nil
}
func (m openresolvManager) Close() error {
return m.deleteTailscaleConfig()
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package dns
import (
"bufio"
"errors"
"fmt"
"net/netip"
"slices"
"strings"
"tailscale.com/feature/buildfeatures"
"tailscale.com/types/logger"
"tailscale.com/util/dnsname"
)
// An OSConfigurator applies DNS settings to the operating system.
type OSConfigurator interface {
// SetDNS updates the OS's DNS configuration to match cfg.
// If cfg is the zero value, all Tailscale-related DNS
// configuration is removed.
// SetDNS must not be called after Close.
// SetDNS takes ownership of cfg.
SetDNS(cfg OSConfig) error
// SupportsSplitDNS reports whether the configurator is capable of
// installing a resolver only for specific DNS suffixes. If false,
// the configurator can only set a global resolver.
SupportsSplitDNS() bool
// GetBaseConfig returns the OS's "base" configuration, i.e. the
// resolver settings the OS would use without Tailscale
// contributing any configuration.
// GetBaseConfig must return the tailscale-free base config even
// after SetDNS has been called to set a Tailscale configuration.
// Only works when SupportsSplitDNS=false.
// Implementations that don't support getting the base config must
// return ErrGetBaseConfigNotSupported.
GetBaseConfig() (OSConfig, error)
// Close removes Tailscale-related DNS configuration from the OS.
Close() error
}
// HostEntry represents a single line in the OS's hosts file.
type HostEntry struct {
Addr netip.Addr
Hosts []string
}
// OSConfig is an OS DNS configuration.
type OSConfig struct {
// Hosts is a map of DNS FQDNs to their IPs, which should be added to the
// OS's hosts file. Currently, (2022-08-12) it is only populated for Windows
// in SplitDNS mode and with Smart Name Resolution turned on.
Hosts []*HostEntry
// Nameservers are the IP addresses of the nameservers to use.
Nameservers []netip.Addr
// SearchDomains are the domain suffixes to use when expanding
// single-label name queries. SearchDomains is additive to
// whatever non-Tailscale search domains the OS has.
SearchDomains []dnsname.FQDN
// MatchDomains are the DNS suffixes for which Nameservers should
// be used. If empty, Nameservers is installed as the "primary" resolver.
// A non-empty MatchDomains requests a "split DNS" configuration
// from the OS, which will only work with OSConfigurators that
// report SupportsSplitDNS()=true.
MatchDomains []dnsname.FQDN
}
func (o *OSConfig) WriteToBufioWriter(w *bufio.Writer) {
if o == nil {
w.WriteString("<nil>")
return
}
w.WriteString("{")
if len(o.Hosts) > 0 {
fmt.Fprintf(w, "Hosts:%v ", o.Hosts)
}
if len(o.Nameservers) > 0 {
fmt.Fprintf(w, "Nameservers:%v ", o.Nameservers)
}
if len(o.SearchDomains) > 0 {
fmt.Fprintf(w, "SearchDomains:%v ", o.SearchDomains)
}
if len(o.MatchDomains) > 0 {
w.WriteString("MatchDomains:[")
sp := ""
var numARPA int
for _, s := range o.MatchDomains {
if strings.HasSuffix(string(s), ".arpa.") {
numARPA++
continue
}
w.WriteString(sp)
w.WriteString(string(s))
sp = " "
}
w.WriteString("]")
if numARPA > 0 {
fmt.Fprintf(w, "+%darpa", numARPA)
}
}
w.WriteString("}")
}
func (o OSConfig) IsZero() bool {
return len(o.Hosts) == 0 &&
len(o.Nameservers) == 0 &&
len(o.SearchDomains) == 0 &&
len(o.MatchDomains) == 0
}
func (a OSConfig) Equal(b OSConfig) bool {
if len(a.Hosts) != len(b.Hosts) {
return false
}
if len(a.Nameservers) != len(b.Nameservers) {
return false
}
if len(a.SearchDomains) != len(b.SearchDomains) {
return false
}
if len(a.MatchDomains) != len(b.MatchDomains) {
return false
}
for i := range a.Hosts {
ha, hb := a.Hosts[i], b.Hosts[i]
if ha.Addr != hb.Addr {
return false
}
if !slices.Equal(ha.Hosts, hb.Hosts) {
return false
}
}
for i := range a.Nameservers {
if a.Nameservers[i] != b.Nameservers[i] {
return false
}
}
for i := range a.SearchDomains {
if a.SearchDomains[i] != b.SearchDomains[i] {
return false
}
}
for i := range a.MatchDomains {
if a.MatchDomains[i] != b.MatchDomains[i] {
return false
}
}
return true
}
// Format implements the fmt.Formatter interface to ensure that Hosts is
// printed correctly (i.e. not as a bunch of pointers).
//
// Fixes https://github.com/tailscale/tailscale/issues/5669
func (a OSConfig) Format(f fmt.State, verb rune) {
logger.ArgWriter(func(w *bufio.Writer) {
if !buildfeatures.HasDNS {
w.WriteString(`{DNS-unlinked}`)
return
}
w.WriteString(`{Nameservers:[`)
for i, ns := range a.Nameservers {
if i != 0 {
w.WriteString(" ")
}
fmt.Fprintf(w, "%+v", ns)
}
w.WriteString(`] SearchDomains:[`)
for i, domain := range a.SearchDomains {
if i != 0 {
w.WriteString(" ")
}
fmt.Fprintf(w, "%+v", domain)
}
w.WriteString(`] MatchDomains:[`)
for i, domain := range a.MatchDomains {
if i != 0 {
w.WriteString(" ")
}
fmt.Fprintf(w, "%+v", domain)
}
w.WriteString(`] Hosts:[`)
for i, host := range a.Hosts {
if i != 0 {
w.WriteString(" ")
}
fmt.Fprintf(w, "%+v", host)
}
w.WriteString(`]}`)
}).Format(f, verb)
}
// ErrGetBaseConfigNotSupported is the error
// OSConfigurator.GetBaseConfig returns when the OSConfigurator
// doesn't support reading the underlying configuration out of the OS.
var ErrGetBaseConfigNotSupported = errors.New("getting OS base config is not supported")
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package publicdns contains mapping and helpers for working with
// public DNS providers.
package publicdns
import (
"bytes"
"encoding/hex"
"fmt"
"net/netip"
"slices"
"sort"
"strings"
"sync"
"tailscale.com/feature/buildfeatures"
"tailscale.com/util/testenv"
)
// dohOfIP maps from public DNS IPs to their DoH base URL.
//
// This does not include NextDNS which is handled specially.
var dohOfIP = map[netip.Addr]string{} // 8.8.8.8 => "https://..."
var dohIPsOfBase = map[string][]netip.Addr{}
var populateOnce sync.Once
const (
nextDNSBase = "https://dns.nextdns.io/"
controlDBase = "https://dns.controld.com/"
)
// DoHEndpointFromIP returns the DNS-over-HTTPS base URL for a given IP
// and whether it's DoH-only (not speaking DNS on port 53).
//
// The ok result is whether the IP is a known DNS server.
func DoHEndpointFromIP(ip netip.Addr) (dohBase string, dohOnly bool, ok bool) {
populateOnce.Do(populate)
if b, ok := dohOfIP[ip]; ok {
return b, false, true
}
// NextDNS DoH URLs are of the form "https://dns.nextdns.io/c3a884"
// where the path component is the lower 12 bytes of the IPv6 address
// in lowercase hex without any zero padding.
if nextDNSv6RangeA.Contains(ip) || nextDNSv6RangeB.Contains(ip) {
a := ip.As16()
var sb strings.Builder
sb.Grow(len(nextDNSBase) + 12)
sb.WriteString(nextDNSBase)
for _, b := range bytes.TrimLeft(a[4:], "\x00") {
fmt.Fprintf(&sb, "%02x", b)
}
return sb.String(), true, true
}
// Control D's free anycast resolvers (freedns.controld.com/pN) are the only
// Control D addresses that serve DoH; they're registered as exact entries in
// dohOfIP (see populate) and are handled by the lookup above.
//
// The ID-encoded addresses in the 2606:1a40::/48 ranges (customer resolver ID
// base36-encoded into the address) are legacy plaintext-DNS (port 53) endpoints
// that refuse DoH on :443, so we deliberately don't upgrade them to DoH here:
// they fall through and are used as ordinary port-53 resolvers. Premium DoH is
// only reachable via the dns.controld.com/<id> URL path (see DoHIPsOfBase).
return "", false, false
}
// KnownDoHPrefixes returns the list of DoH base URLs.
//
// It returns a new copy each time, sorted. It's meant for tests.
//
// It does not include providers that have customer-specific DoH URLs like
// NextDNS.
func KnownDoHPrefixes() []string {
populateOnce.Do(populate)
ret := make([]string, 0, len(dohIPsOfBase))
for b := range dohIPsOfBase {
ret = append(ret, b)
}
sort.Strings(ret)
return ret
}
func isSlashOrQuestionMark(r rune) bool {
return r == '/' || r == '?'
}
// DoHIPsOfBase returns the IP addresses to use to dial the provided DoH base
// URL.
//
// It is basically the inverse of DoHEndpointFromIP with the exception that for
// NextDNS it returns IPv4 addresses that DoHEndpointFromIP doesn't map back.
func DoHIPsOfBase(dohBase string) []netip.Addr {
populateOnce.Do(populate)
if s := dohIPsOfBase[dohBase]; len(s) > 0 {
return s
}
if hexStr, ok := strings.CutPrefix(dohBase, nextDNSBase); ok {
// The path is of the form /<profile-hex>[/<hostname>/<model>/<device id>...]
// or /<profile-hex>?<query params>
// but only the <profile-hex> is required. Ignore the rest:
if i := strings.IndexFunc(hexStr, isSlashOrQuestionMark); i != -1 {
hexStr = hexStr[:i]
}
// TODO(bradfitz): using the NextDNS anycast addresses works but is not
// ideal. Some of their regions have better latency via a non-anycast IP
// which we could get by first resolving A/AAAA "dns.nextdns.io" over
// DoH using their anycast address. For now we only use the anycast
// addresses. The IPv4 IPs we use are just the first one in their ranges.
// For IPv6 we put the profile ID in the lower bytes, but that seems just
// conventional for them and not required (it'll already be in the DoH path).
// (Really we shouldn't use either IPv4 or IPv6 anycast for DoH once we
// resolve "dns.nextdns.io".)
if b, err := hex.DecodeString(hexStr); err == nil && len(b) <= 12 && len(b) > 0 {
return []netip.Addr{
nextDNSv4One,
nextDNSv4Two,
nextDNSv6Gen(nextDNSv6RangeA.Addr(), b),
nextDNSv6Gen(nextDNSv6RangeB.Addr(), b),
}
}
}
if strings.HasPrefix(dohBase, controlDBase) {
return []netip.Addr{
controlDv4One,
controlDv4Two,
controlDv6One,
controlDv6Two,
}
}
return nil
}
// DoHV6 returns the first IPv6 DNS address from a given public DNS provider
// if found, along with a boolean indicating success.
func DoHV6(base string) (ip netip.Addr, ok bool) {
populateOnce.Do(populate)
for _, ip := range dohIPsOfBase[base] {
if ip.Is6() {
return ip, true
}
}
return ip, false
}
// addDoH parses a given well-formed ip string into a netip.Addr type and
// adds it to both knownDoH and dohIPsOFBase maps.
func addDoH(ipStr, base string) {
ip := netip.MustParseAddr(ipStr)
dohOfIP[ip] = base
dohIPsOfBase[base] = append(dohIPsOfBase[base], ip)
}
const (
wikimediaDNSv4 = "185.71.138.138"
wikimediaDNSv6 = "2001:67c:930::1"
)
// populate is called once to initialize the knownDoH and dohIPsOfBase maps.
func populate() {
if !buildfeatures.HasDNS {
return
}
// Cloudflare
// https://developers.cloudflare.com/1.1.1.1/ip-addresses/
addDoH("1.1.1.1", "https://cloudflare-dns.com/dns-query")
addDoH("1.0.0.1", "https://cloudflare-dns.com/dns-query")
addDoH("2606:4700:4700::1111", "https://cloudflare-dns.com/dns-query")
addDoH("2606:4700:4700::1001", "https://cloudflare-dns.com/dns-query")
// Cloudflare -Malware
addDoH("1.1.1.2", "https://security.cloudflare-dns.com/dns-query")
addDoH("1.0.0.2", "https://security.cloudflare-dns.com/dns-query")
addDoH("2606:4700:4700::1112", "https://security.cloudflare-dns.com/dns-query")
addDoH("2606:4700:4700::1002", "https://security.cloudflare-dns.com/dns-query")
// Cloudflare -Malware -Adult
addDoH("1.1.1.3", "https://family.cloudflare-dns.com/dns-query")
addDoH("1.0.0.3", "https://family.cloudflare-dns.com/dns-query")
addDoH("2606:4700:4700::1113", "https://family.cloudflare-dns.com/dns-query")
addDoH("2606:4700:4700::1003", "https://family.cloudflare-dns.com/dns-query")
// Google
addDoH("8.8.8.8", "https://dns.google/dns-query")
addDoH("8.8.4.4", "https://dns.google/dns-query")
addDoH("2001:4860:4860::8888", "https://dns.google/dns-query")
addDoH("2001:4860:4860::8844", "https://dns.google/dns-query")
// OpenDNS
// TODO(bradfitz): OpenDNS is unique amongst this current set in that
// its DoH DNS names resolve to different IPs than its normal DNS
// IPs. Support that later. For now we assume that they're the same.
// addDoH("208.67.222.222", "https://doh.opendns.com/dns-query")
// addDoH("208.67.220.220", "https://doh.opendns.com/dns-query")
// addDoH("208.67.222.123", "https://doh.familyshield.opendns.com/dns-query")
// addDoH("208.67.220.123", "https://doh.familyshield.opendns.com/dns-query")
// Quad9
// https://www.quad9.net/service/service-addresses-and-features
addDoH("9.9.9.9", "https://dns.quad9.net/dns-query")
addDoH("149.112.112.112", "https://dns.quad9.net/dns-query")
addDoH("2620:fe::fe", "https://dns.quad9.net/dns-query")
addDoH("2620:fe::9", "https://dns.quad9.net/dns-query")
// Quad9 +ECS +DNSSEC
addDoH("9.9.9.11", "https://dns11.quad9.net/dns-query")
addDoH("149.112.112.11", "https://dns11.quad9.net/dns-query")
addDoH("2620:fe::11", "https://dns11.quad9.net/dns-query")
addDoH("2620:fe::fe:11", "https://dns11.quad9.net/dns-query")
// Quad9 -DNSSEC
addDoH("9.9.9.10", "https://dns10.quad9.net/dns-query")
addDoH("149.112.112.10", "https://dns10.quad9.net/dns-query")
addDoH("2620:fe::10", "https://dns10.quad9.net/dns-query")
addDoH("2620:fe::fe:10", "https://dns10.quad9.net/dns-query")
// Mullvad
// See https://mullvad.net/en/help/dns-over-https-and-dns-over-tls/
// Mullvad (default)
addDoH("194.242.2.2", "https://dns.mullvad.net/dns-query")
addDoH("2a07:e340::2", "https://dns.mullvad.net/dns-query")
// Mullvad (adblock)
addDoH("194.242.2.3", "https://adblock.dns.mullvad.net/dns-query")
addDoH("2a07:e340::3", "https://adblock.dns.mullvad.net/dns-query")
// Mullvad (base)
addDoH("194.242.2.4", "https://base.dns.mullvad.net/dns-query")
addDoH("2a07:e340::4", "https://base.dns.mullvad.net/dns-query")
// Mullvad (extended)
addDoH("194.242.2.5", "https://extended.dns.mullvad.net/dns-query")
addDoH("2a07:e340::5", "https://extended.dns.mullvad.net/dns-query")
// Mullvad (family)
addDoH("194.242.2.6", "https://family.dns.mullvad.net/dns-query")
addDoH("2a07:e340::6", "https://family.dns.mullvad.net/dns-query")
// Mullvad (all)
addDoH("194.242.2.9", "https://all.dns.mullvad.net/dns-query")
addDoH("2a07:e340::9", "https://all.dns.mullvad.net/dns-query")
// Wikimedia
addDoH(wikimediaDNSv4, "https://wikimedia-dns.org/dns-query")
addDoH(wikimediaDNSv6, "https://wikimedia-dns.org/dns-query")
// Control D
addDoH("76.76.2.0", "https://freedns.controld.com/p0")
addDoH("76.76.10.0", "https://freedns.controld.com/p0")
addDoH("2606:1a40::", "https://freedns.controld.com/p0")
addDoH("2606:1a40:1::", "https://freedns.controld.com/p0")
// Control D -Malware
addDoH("76.76.2.1", "https://freedns.controld.com/p1")
addDoH("76.76.10.1", "https://freedns.controld.com/p1")
addDoH("2606:1a40::1", "https://freedns.controld.com/p1")
addDoH("2606:1a40:1::1", "https://freedns.controld.com/p1")
// Control D -Malware + Ads
addDoH("76.76.2.2", "https://freedns.controld.com/p2")
addDoH("76.76.10.2", "https://freedns.controld.com/p2")
addDoH("2606:1a40::2", "https://freedns.controld.com/p2")
addDoH("2606:1a40:1::2", "https://freedns.controld.com/p2")
// Control D -Malware + Ads + Social
addDoH("76.76.2.3", "https://freedns.controld.com/p3")
addDoH("76.76.10.3", "https://freedns.controld.com/p3")
addDoH("2606:1a40::3", "https://freedns.controld.com/p3")
addDoH("2606:1a40:1::3", "https://freedns.controld.com/p3")
// Control D -Malware + Ads + Adult
addDoH("76.76.2.4", "https://freedns.controld.com/family")
addDoH("76.76.10.4", "https://freedns.controld.com/family")
addDoH("2606:1a40::4", "https://freedns.controld.com/family")
addDoH("2606:1a40:1::4", "https://freedns.controld.com/family")
// CIRA Canadian Shield: https://www.cira.ca/en/canadian-shield/configure/summary-cira-canadian-shield-dns-resolver-addresses/
// CIRA Canadian Shield Private (DNS resolution only)
addDoH("149.112.121.10", "https://private.canadianshield.cira.ca/dns-query")
addDoH("149.112.122.10", "https://private.canadianshield.cira.ca/dns-query")
addDoH("2620:10a:80bb::10", "https://private.canadianshield.cira.ca/dns-query")
addDoH("2620:10a:80bc::10", "https://private.canadianshield.cira.ca/dns-query")
// CIRA Canadian Shield Protected (Malware and phishing protection)
addDoH("149.112.121.20", "https://protected.canadianshield.cira.ca/dns-query")
addDoH("149.112.122.20", "https://protected.canadianshield.cira.ca/dns-query")
addDoH("2620:10a:80bb::20", "https://protected.canadianshield.cira.ca/dns-query")
addDoH("2620:10a:80bc::20", "https://protected.canadianshield.cira.ca/dns-query")
// CIRA Canadian Shield Family (Protected + blocking adult content)
addDoH("149.112.121.30", "https://family.canadianshield.cira.ca/dns-query")
addDoH("149.112.122.30", "https://family.canadianshield.cira.ca/dns-query")
addDoH("2620:10a:80bb::30", "https://family.canadianshield.cira.ca/dns-query")
addDoH("2620:10a:80bc::30", "https://family.canadianshield.cira.ca/dns-query")
}
var (
// The NextDNS IPv6 ranges (primary and secondary). The customer ID is
// encoded in the lower bytes and is used (in hex form) as the DoH query
// path.
nextDNSv6RangeA = netip.MustParsePrefix("2a07:a8c0::/33")
nextDNSv6RangeB = netip.MustParsePrefix("2a07:a8c1::/33")
// The first two IPs in the /24 v4 ranges can be used for DoH to NextDNS.
//
// They're Anycast and usually okay, but NextDNS has some locations that
// don't do BGP and can get results for querying them over DoH to find the
// IPv4 address of "dns.mynextdns.io" and find an even better result.
//
// Note that the Tailscale DNS client does not do any of the "IP address
// linking" that NextDNS can do with its IPv4 addresses. These addresses
// are only used for DoH.
nextDNSv4RangeA = netip.MustParsePrefix("45.90.28.0/24")
nextDNSv4RangeB = netip.MustParsePrefix("45.90.30.0/24")
nextDNSv4One = nextDNSv4RangeA.Addr()
nextDNSv4Two = nextDNSv4RangeB.Addr()
// Wikimedia DNS server IPs (anycast)
wikimediaDNSv4Addr = netip.MustParseAddr(wikimediaDNSv4)
wikimediaDNSv6Addr = netip.MustParseAddr(wikimediaDNSv6)
// The Control D IPv6 ranges (primary and secondary). The customer ID is
// encoded in the ipv6 address is used (in base 36 form) as the DoH query
controlDv6RangeA = netip.MustParsePrefix("2606:1a40::/48")
controlDv6RangeB = netip.MustParsePrefix("2606:1a40:1::/48")
controlDv4One = netip.MustParseAddr("76.76.2.22")
controlDv4Two = netip.MustParseAddr("76.76.10.22")
controlDv6One = netip.MustParseAddr("2606:1a40::22")
controlDv6Two = netip.MustParseAddr("2606:1a40:1::22")
)
// nextDNSv6Gen generates a NextDNS IPv6 address from the upper 8 bytes in the
// provided ip and using id as the lowest 0-8 bytes.
func nextDNSv6Gen(ip netip.Addr, id []byte) netip.Addr {
if len(id) > 12 {
return netip.Addr{}
}
a := ip.As16()
copy(a[16-len(id):], id)
return netip.AddrFrom16(a)
}
// IPIsDoHOnlyServer reports whether ip is a DNS server that should only use
// DNS-over-HTTPS (not regular port 53 DNS).
func IPIsDoHOnlyServer(ip netip.Addr) bool {
return nextDNSv6RangeA.Contains(ip) || nextDNSv6RangeB.Contains(ip) ||
nextDNSv4RangeA.Contains(ip) || nextDNSv4RangeB.Contains(ip) ||
ip == wikimediaDNSv4Addr || ip == wikimediaDNSv6Addr ||
controlDv6RangeA.Contains(ip) || controlDv6RangeB.Contains(ip) ||
ip == controlDv4One || ip == controlDv4Two
}
var testMu sync.Mutex
// RegisterTestDoHEndpoint registers a test DoH endpoint mapping for use in tests.
// It maps the given IP to the DoH base URL, and the URL back to the IP.
//
// This function panics if called outside of tests, and cannot be called
// concurrently with any usage of this package (i.e. before any DNS forwarders
// are created). It is safe to call concurrently with itself.
//
// It returns a cleanup function that removes the registration.
func RegisterTestDoHEndpoint(ip netip.Addr, dohBase string) func() {
if !testenv.InTest() {
panic("RegisterTestDoHEndpoint called outside of tests")
}
populateOnce.Do(populate)
testMu.Lock()
defer testMu.Unlock()
dohOfIP[ip] = dohBase
dohIPsOfBase[dohBase] = append(dohIPsOfBase[dohBase], ip)
return func() {
testMu.Lock()
defer testMu.Unlock()
delete(dohOfIP, ip)
dohIPsOfBase[dohBase] = slices.DeleteFunc(dohIPsOfBase[dohBase], func(addr netip.Addr) bool {
return addr == ip
})
if len(dohIPsOfBase[dohBase]) == 0 {
delete(dohIPsOfBase, dohBase)
}
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux || freebsd || openbsd
package dns
import (
"bytes"
"os/exec"
)
func resolvconfStyle() string {
if _, err := exec.LookPath("resolvconf"); err != nil {
return ""
}
output, err := exec.Command("resolvconf", "--version").CombinedOutput()
if err != nil {
// Debian resolvconf doesn't understand --version, and
// exits with a specific error code.
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 99 {
return "debian"
}
}
if bytes.HasPrefix(output, []byte("Debian resolvconf")) {
return "debian"
}
// Treat everything else as openresolv, by far the more popular implementation.
return "openresolv"
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package resolvconffile parses & serializes /etc/resolv.conf-style files.
//
// It's a leaf package so both net/dns and net/dns/resolver can depend
// on it and we can unify a handful of implementations.
//
// The package is verbosely named to disambiguate it from resolvconf
// the daemon, which Tailscale also supports.
package resolvconffile
import (
"bufio"
"bytes"
"fmt"
"io"
"net/netip"
"os"
"strings"
"tailscale.com/util/dnsname"
)
// Path is the canonical location of resolv.conf.
const Path = "/etc/resolv.conf"
// Config represents a resolv.conf(5) file.
type Config struct {
// Nameservers are the IP addresses of the nameservers to use.
Nameservers []netip.Addr
// SearchDomains are the domain suffixes to use when expanding
// single-label name queries. SearchDomains is additive to
// whatever non-Tailscale search domains the OS has.
SearchDomains []dnsname.FQDN
}
// Write writes c to w. It does so in one Write call.
func (c *Config) Write(w io.Writer) error {
buf := new(bytes.Buffer)
io.WriteString(buf, "# resolv.conf(5) file generated by tailscale\n")
io.WriteString(buf, "# For more info, see https://tailscale.com/s/resolvconf-overwrite\n")
io.WriteString(buf, "# DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN\n\n")
for _, ns := range c.Nameservers {
io.WriteString(buf, "nameserver ")
io.WriteString(buf, ns.String())
io.WriteString(buf, "\n")
}
if len(c.SearchDomains) > 0 {
io.WriteString(buf, "search")
for _, domain := range c.SearchDomains {
io.WriteString(buf, " ")
io.WriteString(buf, domain.WithoutTrailingDot())
}
io.WriteString(buf, "\n")
}
_, err := w.Write(buf.Bytes())
return err
}
// Parse parses a resolv.conf file from r.
func Parse(r io.Reader) (*Config, error) {
config := new(Config)
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Text()
line, _, _ = strings.Cut(line, "#") // remove any comments
line = strings.TrimSpace(line)
if s, ok := strings.CutPrefix(line, "nameserver"); ok {
nameserver := strings.TrimSpace(s)
if len(nameserver) == len(s) {
return nil, fmt.Errorf("missing space after \"nameserver\" in %q", line)
}
ip, err := netip.ParseAddr(nameserver)
if err != nil {
return nil, err
}
config.Nameservers = append(config.Nameservers, ip)
continue
}
if s, ok := strings.CutPrefix(line, "search"); ok {
domains := strings.TrimSpace(s)
if len(domains) == len(s) {
// No leading space?!
return nil, fmt.Errorf("missing space after \"search\" in %q", line)
}
for len(domains) > 0 {
domain := domains
i := strings.IndexAny(domain, " \t")
if i != -1 {
domain = domain[:i]
domains = strings.TrimSpace(domains[i+1:])
} else {
domains = ""
}
fqdn, err := dnsname.ToFQDN(domain)
if err != nil {
return nil, fmt.Errorf("parsing search domain %q in %q: %w", domain, line, err)
}
config.SearchDomains = append(config.SearchDomains, fqdn)
}
}
}
return config, nil
}
// ParseFile parses the named resolv.conf file.
func ParseFile(name string) (*Config, error) {
fi, err := os.Stat(name)
if err != nil {
return nil, err
}
if n := fi.Size(); n > 10<<10 {
return nil, fmt.Errorf("unexpectedly large %q file: %d bytes", name, n)
}
all, err := os.ReadFile(name)
if err != nil {
return nil, err
}
return Parse(bytes.NewReader(all))
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && !android && !ts_omit_resolved
package dns
import (
"context"
"fmt"
"net"
"strings"
"time"
"github.com/godbus/dbus/v5"
"golang.org/x/sys/unix"
"tailscale.com/health"
"tailscale.com/types/logger"
"tailscale.com/util/backoff"
"tailscale.com/util/dnsname"
)
// DBus entities we talk to.
//
// DBus is an RPC bus. In particular, the bus we're talking to is the
// system-wide bus (there is also a per-user session bus for
// user-specific applications).
//
// Daemons connect to the bus, and advertise themselves under a
// well-known object name. That object exposes paths, and each path
// implements one or more interfaces that contain methods, properties,
// and signals.
//
// Clients connect to the bus and walk that same hierarchy to invoke
// RPCs, get/set properties, or listen for signals.
const (
dbusResolvedObject = "org.freedesktop.resolve1"
dbusResolvedPath dbus.ObjectPath = "/org/freedesktop/resolve1"
dbusResolvedInterface = "org.freedesktop.resolve1.Manager"
dbusPath dbus.ObjectPath = "/org/freedesktop/DBus"
dbusInterface = "org.freedesktop.DBus"
dbusOwnerSignal = "NameOwnerChanged" // broadcast when a well-known name's owning process changes.
)
type resolvedLinkNameserver struct {
Family int32
Address []byte
}
type resolvedLinkDomain struct {
Domain string
RoutingOnly bool
}
// changeRequest tracks latest OSConfig and related error responses to update.
type changeRequest struct {
config OSConfig // configs OSConfigs, one per each SetDNS call
res chan<- error // response channel
}
// resolvedManager is an OSConfigurator which uses the systemd-resolved DBus API.
type resolvedManager struct {
ctx context.Context
cancel func() // terminate the context, for close
logf logger.Logf
health *health.Tracker
ifidx int
configCR chan changeRequest // tracks OSConfigs changes and error responses
}
func init() {
optNewResolvedManager.Set(newResolvedManager)
}
func newResolvedManager(logf logger.Logf, health *health.Tracker, interfaceName string) (OSConfigurator, error) {
iface, err := net.InterfaceByName(interfaceName)
if err != nil {
return nil, err
}
ctx, cancel := context.WithCancel(context.Background())
logf = logger.WithPrefix(logf, "dns: ")
mgr := &resolvedManager{
ctx: ctx,
cancel: cancel,
logf: logf,
health: health,
ifidx: iface.Index,
configCR: make(chan changeRequest),
}
go mgr.run(ctx)
return mgr, nil
}
func (m *resolvedManager) SetDNS(config OSConfig) error {
// NOTE: don't close this channel, since it's possible that the SetDNS
// call will time out and return before the run loop answers, at which
// point it will send on the now-closed channel.
errc := make(chan error, 1)
select {
case <-m.ctx.Done():
return m.ctx.Err()
case m.configCR <- changeRequest{config, errc}:
}
select {
case <-m.ctx.Done():
return m.ctx.Err()
case err := <-errc:
if err != nil {
m.logf("failed to configure resolved: %v", err)
}
return err
}
}
func (m *resolvedManager) run(ctx context.Context) {
var (
conn *dbus.Conn
signals chan *dbus.Signal
rManager dbus.BusObject // rManager is the Resolved DBus connection
)
bo := backoff.NewBackoff("resolved-dbus", m.logf, 30*time.Second)
needsReconnect := make(chan bool, 1)
defer func() {
if conn != nil {
conn.Close()
}
}()
// Reconnect the systemBus if disconnected.
reconnect := func() error {
var err error
signals = make(chan *dbus.Signal, 16)
conn, err = dbus.SystemBus()
if err != nil {
m.logf("dbus connection error: %v", err)
} else {
m.logf("[v1] dbus connected")
}
if err != nil {
// Backoff increases time between reconnect attempts.
go func() {
bo.BackOff(ctx, err)
needsReconnect <- true
}()
return err
}
rManager = conn.Object(dbusResolvedObject, dbus.ObjectPath(dbusResolvedPath))
// Only receive the DBus signals we need to resync our config on
// resolved restart. Failure to set filters isn't a fatal error,
// we'll just receive all broadcast signals and have to ignore
// them on our end.
if err = conn.AddMatchSignal(dbus.WithMatchObjectPath(dbusPath), dbus.WithMatchInterface(dbusInterface), dbus.WithMatchMember(dbusOwnerSignal), dbus.WithMatchArg(0, dbusResolvedObject)); err != nil {
m.logf("[v1] Setting DBus signal filter failed: %v", err)
}
conn.Signal(signals)
// Reset backoff and set osConfigurationSetWarnable to healthy after a successful reconnect.
bo.BackOff(ctx, nil)
m.health.SetHealthy(osConfigurationSetWarnable)
return nil
}
// Create initial systemBus connection.
reconnect()
lastConfig := OSConfig{}
for {
select {
case <-ctx.Done():
if rManager == nil {
return
}
// RevertLink resets all per-interface settings on systemd-resolved to defaults.
// When ctx goes away systemd-resolved auto reverts.
// Keeping for potential use in future refactor.
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".RevertLink", 0, m.ifidx); call.Err != nil {
m.logf("[v1] RevertLink: %v", call.Err)
return
}
return
case configCR := <-m.configCR:
// Track and update sync with latest config change.
lastConfig = configCR.config
if rManager == nil {
configCR.res <- fmt.Errorf("resolved DBus does not have a connection")
continue
}
err := m.setConfigOverDBus(ctx, rManager, configCR.config)
configCR.res <- err
case <-needsReconnect:
if err := reconnect(); err != nil {
m.logf("[v1] SystemBus reconnect error %T", err)
}
continue
case signal, ok := <-signals:
// If signal ends and is nil then program tries to reconnect.
if !ok {
if err := reconnect(); err != nil {
m.logf("[v1] SystemBus reconnect error %T", err)
}
continue
}
// In theory the signal was filtered by DBus, but if
// AddMatchSignal in the constructor failed, we may be
// getting other spam.
if signal.Path != dbusPath || signal.Name != dbusInterface+"."+dbusOwnerSignal {
continue
}
if lastConfig.IsZero() {
continue
}
// signal.Body is a []any of 3 strings: bus name, previous owner, new owner.
if len(signal.Body) != 3 {
m.logf("[unexpected] DBus NameOwnerChanged len(Body) = %d, want 3")
}
if name, ok := signal.Body[0].(string); !ok || name != dbusResolvedObject {
continue
}
newOwner, ok := signal.Body[2].(string)
if !ok {
m.logf("[unexpected] DBus NameOwnerChanged.new_owner is a %T, not a string", signal.Body[2])
}
if newOwner == "" {
// systemd-resolved left the bus, no current owner,
// nothing to do.
continue
}
// The resolved bus name has a new owner, meaning resolved
// restarted. Reprogram current config.
m.logf("systemd-resolved restarted, syncing DNS config")
err := m.setConfigOverDBus(ctx, rManager, lastConfig)
// Set health while holding the lock, because this will
// graciously serialize the resync's health outcome with a
// concurrent SetDNS call.
if err != nil {
m.logf("failed to configure systemd-resolved: %v", err)
m.health.SetUnhealthy(osConfigurationSetWarnable, health.Args{health.ArgError: err.Error()})
} else {
m.health.SetHealthy(osConfigurationSetWarnable)
}
}
}
}
// setConfigOverDBus updates resolved DBus config and is only called from the run goroutine.
func (m *resolvedManager) setConfigOverDBus(ctx context.Context, rManager dbus.BusObject, config OSConfig) error {
ctx, cancel := context.WithTimeout(ctx, reconfigTimeout)
defer cancel()
var linkNameservers = make([]resolvedLinkNameserver, len(config.Nameservers))
for i, server := range config.Nameservers {
ip := server.As16()
if server.Is4() {
linkNameservers[i] = resolvedLinkNameserver{
Family: unix.AF_INET,
Address: ip[12:],
}
} else {
linkNameservers[i] = resolvedLinkNameserver{
Family: unix.AF_INET6,
Address: ip[:],
}
}
}
err := rManager.CallWithContext(
ctx, dbusResolvedInterface+".SetLinkDNS", 0,
m.ifidx, linkNameservers,
).Store()
if err != nil {
return fmt.Errorf("setLinkDNS: %w", err)
}
linkDomains := make([]resolvedLinkDomain, 0, len(config.SearchDomains)+len(config.MatchDomains))
seenDomains := map[dnsname.FQDN]bool{}
for _, domain := range config.SearchDomains {
if seenDomains[domain] {
continue
}
seenDomains[domain] = true
linkDomains = append(linkDomains, resolvedLinkDomain{
Domain: domain.WithTrailingDot(),
RoutingOnly: false,
})
}
for _, domain := range config.MatchDomains {
if seenDomains[domain] {
// Search domains act as both search and match in
// resolved, so it's correct to skip.
continue
}
seenDomains[domain] = true
linkDomains = append(linkDomains, resolvedLinkDomain{
Domain: domain.WithTrailingDot(),
RoutingOnly: true,
})
}
if len(config.MatchDomains) == 0 && len(config.Nameservers) > 0 {
// Caller requested full DNS interception, install a
// routing-only root domain.
linkDomains = append(linkDomains, resolvedLinkDomain{
Domain: ".",
RoutingOnly: true,
})
}
err = rManager.CallWithContext(
ctx, dbusResolvedInterface+".SetLinkDomains", 0,
m.ifidx, linkDomains,
).Store()
if err != nil && err.Error() == "Argument list too long" { // TODO: better error match
// Issue 3188: older systemd-resolved had argument length limits.
// Trim out the *.arpa. entries and try again.
err = rManager.CallWithContext(
ctx, dbusResolvedInterface+".SetLinkDomains", 0,
m.ifidx, linkDomainsWithoutReverseDNS(linkDomains),
).Store()
}
if err != nil {
return fmt.Errorf("setLinkDomains: %w", err)
}
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkDefaultRoute", 0, m.ifidx, len(config.MatchDomains) == 0); call.Err != nil {
if dbusErr, ok := call.Err.(dbus.Error); ok && dbusErr.Name == dbus.ErrMsgUnknownMethod.Name {
// on some older systems like Kubuntu 18.04.6 with systemd 237 method SetLinkDefaultRoute is absent,
// but otherwise it's working good
m.logf("[v1] failed to set SetLinkDefaultRoute: %v", call.Err)
} else {
return fmt.Errorf("setLinkDefaultRoute: %w", call.Err)
}
}
// Some best-effort setting of things, but resolved should do the
// right thing if these fail (e.g. a really old resolved version
// or something).
// Disable LLMNR, we don't do multicast.
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkLLMNR", 0, m.ifidx, "no"); call.Err != nil {
m.logf("[v1] failed to disable LLMNR: %v", call.Err)
}
// Disable mdns.
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkMulticastDNS", 0, m.ifidx, "no"); call.Err != nil {
m.logf("[v1] failed to disable mdns: %v", call.Err)
}
// We don't support dnssec consistently right now, force it off to
// avoid partial failures when we split DNS internally.
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkDNSSEC", 0, m.ifidx, "no"); call.Err != nil {
m.logf("[v1] failed to disable DNSSEC: %v", call.Err)
}
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".SetLinkDNSOverTLS", 0, m.ifidx, "no"); call.Err != nil {
m.logf("[v1] failed to disable DoT: %v", call.Err)
}
if call := rManager.CallWithContext(ctx, dbusResolvedInterface+".FlushCaches", 0); call.Err != nil {
m.logf("failed to flush resolved DNS cache: %v", call.Err)
}
return nil
}
func (m *resolvedManager) SupportsSplitDNS() bool {
return true
}
func (m *resolvedManager) GetBaseConfig() (OSConfig, error) {
return OSConfig{}, ErrGetBaseConfigNotSupported
}
func (m *resolvedManager) Close() error {
m.cancel() // stops the 'run' method goroutine
return nil
}
// linkDomainsWithoutReverseDNS returns a copy of v without
// *.arpa. entries.
func linkDomainsWithoutReverseDNS(v []resolvedLinkDomain) (ret []resolvedLinkDomain) {
for _, d := range v {
if strings.HasSuffix(d.Domain, ".arpa.") {
// Oh well. At least the rest will work.
continue
}
ret = append(ret, d)
}
return ret
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package resolver
import (
"fmt"
"html"
"net/http"
"strconv"
"sync/atomic"
"time"
"tailscale.com/feature/buildfeatures"
"tailscale.com/health"
"tailscale.com/syncs"
)
func init() {
if !buildfeatures.HasDNS {
return
}
health.RegisterDebugHandler("dnsfwd", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
n, _ := strconv.Atoi(r.FormValue("n"))
if n <= 0 {
n = 100
} else if n > 10000 {
n = 10000
}
fl := fwdLogAtomic.Load()
if fl == nil || n != len(fl.ent) {
fl = &fwdLog{ent: make([]fwdLogEntry, n)}
fwdLogAtomic.Store(fl)
}
fl.ServeHTTP(w, r)
}))
}
var fwdLogAtomic atomic.Pointer[fwdLog]
type fwdLog struct {
mu syncs.Mutex
pos int // ent[pos] is next entry
ent []fwdLogEntry
}
type fwdLogEntry struct {
Domain string
Time time.Time
}
func (fl *fwdLog) addName(name string) {
if fl == nil {
return
}
fl.mu.Lock()
defer fl.mu.Unlock()
if len(fl.ent) == 0 {
return
}
fl.ent[fl.pos] = fwdLogEntry{Domain: name, Time: time.Now()}
fl.pos++
if fl.pos == len(fl.ent) {
fl.pos = 0
}
}
func (fl *fwdLog) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fl.mu.Lock()
defer fl.mu.Unlock()
fmt.Fprintf(w, "<html><h1>DNS forwards</h1>")
now := time.Now()
for i := range len(fl.ent) {
ent := fl.ent[(i+fl.pos)%len(fl.ent)]
if ent.Domain == "" {
continue
}
fmt.Fprintf(w, "%v ago: %v<br>\n", now.Sub(ent.Time).Round(time.Second), html.EscapeString(ent.Domain))
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package resolver
import (
"bytes"
"context"
"crypto/sha256"
"crypto/tls"
"encoding/base64"
"encoding/binary"
"errors"
"fmt"
"io"
"maps"
"net"
"net/http"
"net/netip"
"net/url"
"runtime"
"sort"
"strings"
"sync"
"sync/atomic"
"time"
dns "golang.org/x/net/dns/dnsmessage"
"tailscale.com/control/controlknobs"
"tailscale.com/envknob"
"tailscale.com/feature"
"tailscale.com/feature/buildfeatures"
"tailscale.com/health"
"tailscale.com/net/dns/publicdns"
"tailscale.com/net/dnscache"
"tailscale.com/net/neterror"
"tailscale.com/net/netmon"
"tailscale.com/net/netx"
"tailscale.com/net/sockstats"
"tailscale.com/net/tsdial"
"tailscale.com/syncs"
"tailscale.com/types/dnstype"
"tailscale.com/types/logger"
"tailscale.com/types/nettype"
"tailscale.com/types/views"
"tailscale.com/util/cloudenv"
"tailscale.com/util/dnsname"
"tailscale.com/util/mak"
"tailscale.com/util/race"
"tailscale.com/version"
)
// headerBytes is the number of bytes in a DNS message header.
const headerBytes = 12
// dnsFlagTruncated is set in the flags word when the packet is truncated.
const dnsFlagTruncated = 0x200
// truncatedFlagSet returns true if the DNS packet signals that it has
// been truncated. False is also returned if the packet was too small
// to be valid.
func truncatedFlagSet(pkt []byte) bool {
if len(pkt) < headerBytes {
return false
}
return (binary.BigEndian.Uint16(pkt[2:4]) & dnsFlagTruncated) != 0
}
// setTCFlag sets the TC (truncated) flag in the DNS packet header.
// The packet must be at least headerBytes in length.
func setTCFlag(packet []byte) {
if len(packet) < headerBytes {
return
}
flags := binary.BigEndian.Uint16(packet[2:4])
flags |= dnsFlagTruncated
binary.BigEndian.PutUint16(packet[2:4], flags)
}
const (
// dohIdleConnTimeout is how long to keep idle HTTP connections
// open to DNS-over-HTTPS servers. 10 seconds is a sensible
// default, as it's long enough to handle a burst of queries
// coming in a row, but short enough to not keep idle connections
// open for too long. In theory, idle connections could be kept
// open for a long time without any battery impact as no traffic
// is supposed to be flowing on them.
// However, in practice, DoH servers will send TCP keepalives (e.g.
// NextDNS sends them every ~10s). Handling these keepalives
// wakes up the modem, and that uses battery. Therefore, we keep
// the idle timeout low enough to allow idle connections to be
// closed during an extended period with no DNS queries, killing
// keepalive network activity.
dohIdleConnTimeout = 10 * time.Second
// dohTransportTimeout is how much of a head start to give a DoH query
// that was upgraded from a well-known public DNS provider's IP before
// normal UDP mode is attempted as a fallback.
dohHeadStart = 500 * time.Millisecond
// wellKnownHostBackupDelay is how long to artificially delay upstream
// DNS queries to the "fallback" DNS server IP for a known provider
// (e.g. how long to wait to query Google's 8.8.4.4 after 8.8.8.8).
wellKnownHostBackupDelay = 200 * time.Millisecond
// udpRaceTimeout is the timeout after which we will start a DNS query
// over TCP while waiting for the UDP query to complete.
udpRaceTimeout = 2 * time.Second
// tcpQueryTimeout is the timeout for a DNS query performed over TCP.
// It matches the default 5sec timeout of the 'dig' utility.
tcpQueryTimeout = 5 * time.Second
)
// txid identifies a DNS transaction.
//
// As the standard DNS Request ID is only 16 bits, we extend it:
// the lower 32 bits are the zero-extended bits of the DNS Request ID;
// the upper 32 bits are the CRC32 checksum of the first question in the request.
// This makes probability of txid collision negligible.
type txid uint64
// getTxID computes the txid of the given DNS packet.
func getTxID(packet []byte) txid {
if len(packet) < headerBytes {
return 0
}
dnsid := binary.BigEndian.Uint16(packet[0:2])
// Previously, we hashed the question and combined it with the original txid
// which was useful when concurrent queries were multiplexed on a single
// local source port. We encountered some situations where the DNS server
// canonicalizes the question in the response (uppercase converted to
// lowercase in this case), which resulted in responses that we couldn't
// match to the original request due to hash mismatches.
return txid(dnsid)
}
func getRCode(packet []byte) dns.RCode {
if len(packet) < headerBytes {
// treat invalid packets as a refusal
return dns.RCode(5)
}
// get bottom 4 bits of 3rd byte
return dns.RCode(packet[3] & 0x0F)
}
// findOPTRecord finds and validates the EDNS OPT record at the end of a DNS packet.
// Returns the requested buffer size and a pointer to the OPT record bytes if valid,
// or (0, nil) if no valid OPT record is found.
// The OPT record must be at the very end of the packet with no option codes.
func findOPTRecord(packet []byte) (requestedSize uint16, opt []byte) {
const optFixedBytes = 11 // size of an OPT record with no option codes
const edns0Version = 0 // EDNS version number (currently only version 0 is defined)
if len(packet) < headerBytes+optFixedBytes {
return 0, nil
}
arCount := binary.BigEndian.Uint16(packet[10:12])
if arCount == 0 {
// OPT shows up in an AR, so there must be no OPT
return 0, nil
}
// https://datatracker.ietf.org/doc/html/rfc6891#section-6.1.2
opt = packet[len(packet)-optFixedBytes:]
if opt[0] != 0 {
// OPT NAME must be 0 (root domain)
return 0, nil
}
if dns.Type(binary.BigEndian.Uint16(opt[1:3])) != dns.TypeOPT {
// Not an OPT record
return 0, nil
}
requestedSize = binary.BigEndian.Uint16(opt[3:5])
// Ignore extended RCODE in opt[5]
if opt[6] != edns0Version {
// Be conservative and don't touch unknown versions.
return 0, nil
}
// Ignore flags in opt[6:9]
if binary.BigEndian.Uint16(opt[9:11]) != 0 {
// RDLEN must be 0 (no variable length data). We're at the end of the
// packet so this should be 0 anyway.
return 0, nil
}
return requestedSize, opt
}
// clampEDNSSize attempts to limit the maximum EDNS response size. This is not
// an exhaustive solution, instead only easy cases are currently handled in the
// interest of speed and reduced complexity. Only OPT records at the very end of
// the message with no option codes are addressed.
// TODO: handle more situations if we discover that they happen often
func clampEDNSSize(packet []byte, maxSize uint16) {
requestedSize, opt := findOPTRecord(packet)
if opt == nil {
return
}
if requestedSize <= maxSize {
return
}
// Clamp the maximum size
binary.BigEndian.PutUint16(opt[3:5], maxSize)
}
// getEDNSBufferSize extracts the EDNS buffer size from a DNS request packet.
// Returns (bufferSize, true) if a valid EDNS OPT record is found,
// or (0, false) if no EDNS OPT record is found or if there's an error.
func getEDNSBufferSize(packet []byte) (uint16, bool) {
requestedSize, opt := findOPTRecord(packet)
return requestedSize, opt != nil
}
// checkResponseSizeAndSetTC sets the TC (truncated) flag in the DNS header when
// the response exceeds the maximum UDP size. If no EDNS OPT record is present
// in the request, it sets the TC flag when the response is bigger than 512 bytes
// per RFC 1035. If an EDNS OPT record is present, it sets the TC flag when the
// response is bigger than the EDNS buffer size. The response buffer is not
// truncated; only the TC flag is set. Returns the response unchanged except for
// the TC flag being set if needed.
func checkResponseSizeAndSetTC(response []byte, request []byte, family string, logf logger.Logf) []byte {
const defaultUDPSize = 512 // default maximum UDP DNS packet size per RFC 1035
// Only check for UDP queries; TCP can handle larger responses
if family != "udp" {
return response
}
// Check if TC flag is already set
if len(response) < headerBytes {
return response
}
if truncatedFlagSet(response) {
// TC flag already set, nothing to do
return response
}
ednsSize, hasEDNS := getEDNSBufferSize(request)
// Determine maximum allowed size
var maxSize int
if hasEDNS {
maxSize = int(ednsSize)
} else {
// No EDNS: enforce default UDP size limit per RFC 1035
maxSize = defaultUDPSize
}
// Check if response exceeds maximum size
if len(response) > maxSize {
setTCFlag(response)
}
return response
}
// dnsForwarderFailing should be raised when the forwarder is unable to reach the
// upstream resolvers. This is a high severity warning as it results in "no internet".
// This warning must be cleared when the forwarder is working again.
//
// We allow for 5 second grace period to ensure this is not raised for spurious errors
// under the assumption that DNS queries are relatively frequent and a subsequent
// successful query will clear any one-off errors.
var dnsForwarderFailing = health.Register(&health.Warnable{
Code: "dns-forward-failing",
Title: "DNS unavailable",
Severity: health.SeverityMedium,
DependsOn: []*health.Warnable{health.NetworkStatusWarnable},
Text: health.StaticMessage("Tailscale can't reach the configured DNS servers. Internet connectivity may be affected."),
ImpactsConnectivity: true,
TimeToVisible: 15 * time.Second,
})
type route struct {
Suffix dnsname.FQDN
Resolvers []resolverAndDelay
}
// resolverAndDelay is an upstream DNS resolver and a delay for how
// long to wait before querying it.
type resolverAndDelay struct {
// name is the upstream resolver.
name *dnstype.Resolver
// startDelay is an amount to delay this resolver at
// start. It's used when, say, there are four Google or
// Cloudflare DNS IPs (two IPv4 + two IPv6) and we don't want
// to race all four at once.
startDelay time.Duration
}
// forwarder forwards DNS packets to a number of upstream nameservers.
type forwarder struct {
logf logger.Logf
netMon *netmon.Monitor // always non-nil
linkSel ForwardLinkSelector // TODO(bradfitz): remove this when tsdial.Dialer absorbs it
dialer *tsdial.Dialer
health *health.Tracker // always non-nil
verboseFwd bool // if true, log all DNS forwarding
controlKnobs *controlknobs.Knobs // or nil
ctx context.Context // good until Close
ctxCancel context.CancelFunc // closes ctx
mu syncs.Mutex // guards following
dohClient map[string]*http.Client // urlBase -> client
// routes are per-suffix resolvers to use, with
// the most specific routes first.
routes []route
// cloudHostFallback are last resort resolvers to use if no per-suffix
// resolver matches. These are only populated on cloud hosts where the
// platform provides a well-known recursive resolver.
//
// That is, if we're running on GCP or AWS where there's always a well-known
// IP of a recursive resolver, return that rather than having callers return
// SERVFAIL. This fixes both normal 100.100.100.100 resolution when
// /etc/resolv.conf is missing/corrupt, and the peerapi ExitDNS stub
// resolver lookup.
cloudHostFallback []resolverAndDelay
// schemes are the collection of registered URI scheme names that
// dynamically decide which resolver to use at the time of each query. The
// key is the scheme (the portion before the first `:`) and the value is a
// handler that determines where the current query should be sent.
// Use schemeCacheLocked() to get the current contents that can continue to
// be accessed once mu is released. This allows the (much more common)
// resolver code path to avoid repeated locking and unlocking.
// When modified, call invalidateSchemeCacheLocked() before unlocking mu.
schemes map[string]CustomSchemeHandler
// schemeCache is an immutable copy of schemes. Do not read directly,
// use schemeCacheLocked() which will regenerate its contents as needed.
schemeCache views.Map[string, CustomSchemeHandler]
// acceptDNS tracks the CorpDNS pref (--accept-dns)
// This lets us skip health warnings if the forwarder receives inbound
// queries directly - but we didn't configure it with any upstream resolvers.
// That's an error, but not a health error if the user has disabled CorpDNS.
acceptDNS bool
}
func (f *forwarder) probeLocks() {
f.mu.Lock()
f.mu.Unlock()
}
func newForwarder(logf logger.Logf, netMon *netmon.Monitor, linkSel ForwardLinkSelector, dialer *tsdial.Dialer, health *health.Tracker, knobs *controlknobs.Knobs) *forwarder {
if !buildfeatures.HasDNS {
return nil
}
if netMon == nil {
panic("nil netMon")
}
f := &forwarder{
logf: logger.WithPrefix(logf, "forward: "),
netMon: netMon,
linkSel: linkSel,
dialer: dialer,
health: health,
controlKnobs: knobs,
verboseFwd: verboseDNSForward(),
}
f.ctx, f.ctxCancel = context.WithCancel(context.Background())
return f
}
func (f *forwarder) Close() error {
f.ctxCancel()
return nil
}
// resolversWithDelays maps from a set of DNS server names to a slice of a type
// that included a startDelay, upgrading any well-known DoH (DNS-over-HTTP)
// servers in the process, insert a DoH lookup first before UDP fallbacks.
func resolversWithDelays(resolvers []*dnstype.Resolver) []resolverAndDelay {
rr := make([]resolverAndDelay, 0, len(resolvers)+2)
type dohState uint8
const addedDoH = dohState(1)
const addedDoHAndDontAddUDP = dohState(2)
// Add the known DoH ones first, starting immediately.
didDoH := map[string]dohState{}
for _, r := range resolvers {
ipp, ok := r.IPPort()
if !ok {
continue
}
dohBase, dohOnly, ok := publicdns.DoHEndpointFromIP(ipp.Addr())
if !ok || didDoH[dohBase] != 0 {
continue
}
if dohOnly {
didDoH[dohBase] = addedDoHAndDontAddUDP
} else {
didDoH[dohBase] = addedDoH
}
rr = append(rr, resolverAndDelay{name: &dnstype.Resolver{Addr: dohBase}})
}
type hostAndFam struct {
host string // some arbitrary string representing DNS host (currently the DoH base)
bits uint8 // either 32 or 128 for IPv4 vs IPv6s address family
}
done := map[hostAndFam]int{}
for _, r := range resolvers {
ipp, ok := r.IPPort()
if !ok {
// Pass non-IP ones through unchanged, without delay.
// (e.g. DNS-over-ExitDNS when using an exit node)
rr = append(rr, resolverAndDelay{name: r})
continue
}
ip := ipp.Addr()
var startDelay time.Duration
if host, _, ok := publicdns.DoHEndpointFromIP(ip); ok {
if didDoH[host] == addedDoHAndDontAddUDP {
continue
}
// We already did the DoH query early. These
// are for normal dns53 UDP queries.
startDelay = dohHeadStart
key := hostAndFam{host, uint8(ip.BitLen())}
if done[key] > 0 {
startDelay += wellKnownHostBackupDelay
}
done[key]++
}
rr = append(rr, resolverAndDelay{
name: r,
startDelay: startDelay,
})
}
return rr
}
var (
cloudResolversOnce sync.Once
cloudResolversLazy []resolverAndDelay
)
func cloudResolvers() []resolverAndDelay {
cloudResolversOnce.Do(func() {
if ip := cloudenv.Get().ResolverIP(); ip != "" {
cloudResolver := []*dnstype.Resolver{{Addr: ip}}
cloudResolversLazy = resolversWithDelays(cloudResolver)
}
})
return cloudResolversLazy
}
// setRoutes sets the routes to use for DNS forwarding. It's called by
// Resolver.SetConfig on reconfig.
//
// The memory referenced by routesBySuffix should not be modified.
func (f *forwarder) setRoutes(routesBySuffix map[dnsname.FQDN][]*dnstype.Resolver, acceptDNS bool) {
routes := make([]route, 0, len(routesBySuffix))
cloudHostFallback := cloudResolvers()
for suffix, rs := range routesBySuffix {
if suffix == "." && len(rs) == 0 && len(cloudHostFallback) > 0 {
routes = append(routes, route{
Suffix: suffix,
Resolvers: cloudHostFallback,
})
} else {
routes = append(routes, route{
Suffix: suffix,
Resolvers: resolversWithDelays(rs),
})
}
}
if cloudenv.Get().HasInternalTLD() && len(cloudHostFallback) > 0 {
if _, ok := routesBySuffix["internal."]; !ok {
routes = append(routes, route{
Suffix: "internal.",
Resolvers: cloudHostFallback,
})
}
}
// Sort from longest prefix to shortest.
sort.Slice(routes, func(i, j int) bool {
return routes[i].Suffix.NumLabels() > routes[j].Suffix.NumLabels()
})
f.mu.Lock()
defer f.mu.Unlock()
f.acceptDNS = acceptDNS
f.routes = routes
f.cloudHostFallback = cloudHostFallback
}
var stdNetPacketListener nettype.PacketListenerWithNetIP = nettype.MakePacketListenerWithNetIP(new(net.ListenConfig))
func (f *forwarder) packetListener(ip netip.Addr) (nettype.PacketListenerWithNetIP, error) {
if f.linkSel == nil || initListenConfig == nil {
return stdNetPacketListener, nil
}
linkName := f.linkSel.PickLink(ip)
if linkName == "" {
return stdNetPacketListener, nil
}
lc := new(net.ListenConfig)
if err := initListenConfig(lc, f.netMon, linkName); err != nil {
return nil, err
}
return nettype.MakePacketListenerWithNetIP(lc), nil
}
// getKnownDoHClientForProvider returns an HTTP client for a specific DoH
// provider named by its DoH base URL (like "https://dns.google/dns-query").
//
// The returned client race/Happy Eyeballs dials all IPs for urlBase (usually
// 4), as statically known by the publicdns package.
func (f *forwarder) getKnownDoHClientForProvider(urlBase string) (c *http.Client, ok bool) {
f.mu.Lock()
defer f.mu.Unlock()
if c, ok := f.dohClient[urlBase]; ok {
return c, true
}
allIPs := publicdns.DoHIPsOfBase(urlBase)
if len(allIPs) == 0 {
return nil, false
}
dohURL, err := url.Parse(urlBase)
if err != nil {
return nil, false
}
dialer := dnscache.Dialer(f.getDialerType(), &dnscache.Resolver{
SingleHost: dohURL.Hostname(),
SingleHostStaticResult: allIPs,
Logf: f.logf,
})
tlsConfig := &tls.Config{
// Enforce TLS 1.3, as all of our supported DNS-over-HTTPS servers are compatible with it
// (see tailscale.com/net/dns/publicdns/publicdns.go).
MinVersion: tls.VersionTLS13,
}
c = &http.Client{
Transport: &http.Transport{
ForceAttemptHTTP2: true,
IdleConnTimeout: dohIdleConnTimeout,
// On mobile platforms TCP KeepAlive is disabled in the dialer,
// ensure that we timeout if the connection appears to be hung.
ResponseHeaderTimeout: 10 * time.Second,
MaxIdleConnsPerHost: 1,
DialContext: func(ctx context.Context, netw, addr string) (net.Conn, error) {
if !strings.HasPrefix(netw, "tcp") {
return nil, fmt.Errorf("unexpected network %q", netw)
}
return dialer(ctx, netw, addr)
},
TLSClientConfig: tlsConfig,
},
}
if f.dohClient == nil {
f.dohClient = map[string]*http.Client{}
}
f.dohClient[urlBase] = c
return c, true
}
const dohType = "application/dns-message"
func (f *forwarder) sendDoH(ctx context.Context, urlBase string, c *http.Client, packet []byte) ([]byte, error) {
ctx = sockstats.WithSockStats(ctx, sockstats.LabelDNSForwarderDoH, f.logf)
metricDNSFwdDoH.Add(1)
req, err := http.NewRequestWithContext(ctx, "POST", urlBase, bytes.NewReader(packet))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", dohType)
req.Header.Set("Accept", dohType)
req.Header.Set("User-Agent", "tailscaled/"+version.Long())
hres, err := c.Do(req)
if err != nil {
metricDNSFwdDoHErrorTransport.Add(1)
return nil, err
}
defer hres.Body.Close()
if hres.StatusCode != 200 {
metricDNSFwdDoHErrorStatus.Add(1)
if hres.StatusCode/100 == 5 {
// Translate 5xx HTTP server errors into SERVFAIL DNS responses.
return nil, fmt.Errorf("%w: %s", errServerFailure, hres.Status)
}
return nil, errors.New(hres.Status)
}
if ct := hres.Header.Get("Content-Type"); ct != dohType {
metricDNSFwdDoHErrorCT.Add(1)
return nil, fmt.Errorf("unexpected response Content-Type %q", ct)
}
res, err := io.ReadAll(hres.Body)
if err != nil {
metricDNSFwdDoHErrorBody.Add(1)
}
if truncatedFlagSet(res) {
metricDNSFwdTruncated.Add(1)
}
return res, err
}
var (
verboseDNSForward = envknob.RegisterBool("TS_DEBUG_DNS_FORWARD_SEND")
skipTCPRetry = envknob.RegisterBool("TS_DNS_FORWARD_SKIP_TCP_RETRY")
// For correlating log messages in the send() function; only used when
// verboseDNSForward() is true.
forwarderCount atomic.Uint64
)
// send sends packet to dst. It is best effort.
//
// send expects the reply to have the same txid as txidOut.
func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDelay) (ret []byte, err error) {
if f.verboseFwd {
id := forwarderCount.Add(1)
domain, typ, _ := nameFromQuery(fq.packet)
f.logf("forwarder.send(%q, %d, %v, %d) from %v [%d] ...", rr.name.Addr, fq.txid, typ, len(domain), fq.src, id)
defer func() {
f.logf("forwarder.send(%q, %d, %v, %d) from %v [%d] = %v, %v", rr.name.Addr, fq.txid, typ, len(domain), fq.src, id, len(ret), err)
}()
}
if strings.HasPrefix(rr.name.Addr, "http://") {
if !buildfeatures.HasPeerAPIClient {
return nil, feature.ErrUnavailable
}
res, err := f.sendDoH(ctx, rr.name.Addr, f.dialer.PeerAPIHTTPClient(), fq.packet)
if err != nil {
return nil, err
}
// Check response size and set TC flag if needed (only for UDP queries)
res = checkResponseSizeAndSetTC(res, fq.packet, fq.family, f.logf)
return res, nil
}
if strings.HasPrefix(rr.name.Addr, "https://") {
// Only known DoH providers are supported currently. Specifically, we
// only support DoH providers where we can TCP connect to them on port
// 443 at the same IP address they serve normal UDP DNS from (1.1.1.1,
// 8.8.8.8, 9.9.9.9, etc.) That's why OpenDNS and custom DoH providers
// aren't currently supported. There's no backup DNS resolution path for
// them.
urlBase := rr.name.Addr
if hc, ok := f.getKnownDoHClientForProvider(urlBase); ok {
res, err := f.sendDoH(ctx, urlBase, hc, fq.packet)
if err != nil {
return nil, err
}
// Check response size and set TC flag if needed (only for UDP queries)
res = checkResponseSizeAndSetTC(res, fq.packet, fq.family, f.logf)
return res, nil
}
metricDNSFwdErrorType.Add(1)
return nil, fmt.Errorf("arbitrary https:// resolvers not supported yet")
}
if strings.HasPrefix(rr.name.Addr, "tls://") {
metricDNSFwdErrorType.Add(1)
return nil, fmt.Errorf("tls:// resolvers not supported yet")
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
isUDPQuery := fq.family == "udp"
skipTCP := skipTCPRetry() || (f.controlKnobs != nil && f.controlKnobs.DisableDNSForwarderTCPRetries.Load())
// Print logs about retries if this was because of a truncated response.
var explicitRetry atomic.Bool // true if truncated UDP response retried
defer func() {
if !explicitRetry.Load() {
return
}
if err == nil {
f.logf("forwarder.send(%q): successfully retried via TCP", rr.name.Addr)
} else {
f.logf("forwarder.send(%q): could not retry via TCP: %v", rr.name.Addr, err)
}
}()
firstUDP := func(ctx context.Context) ([]byte, error) {
resp, err := f.sendUDP(ctx, fq, rr)
if err != nil {
return nil, err
}
if !truncatedFlagSet(resp) {
// Successful, non-truncated response; no retry.
return resp, nil
}
// If this is a UDP query, return it regardless of whether the
// response is truncated or not; the client can retry
// communicating with tailscaled over TCP. There's no point
// falling back to TCP for a truncated query if we can't return
// the results to the client.
if isUDPQuery {
return resp, nil
}
if skipTCP {
// Envknob or control knob disabled the TCP retry behaviour;
// just return what we have.
return resp, nil
}
// This is a TCP query from the client, and the UDP response
// from the upstream DNS server is truncated; map this to an
// error to cause our retry helper to immediately kick off the
// TCP retry.
explicitRetry.Store(true)
return nil, truncatedResponseError{resp}
}
thenTCP := func(ctx context.Context) ([]byte, error) {
// If we're skipping the TCP fallback, then wait until the
// context is canceled and return that error (i.e. not
// returning anything).
if skipTCP {
<-ctx.Done()
return nil, ctx.Err()
}
return f.sendTCP(ctx, fq, rr)
}
// If the input query is TCP, then don't have a timeout between
// starting UDP and TCP.
timeout := udpRaceTimeout
if !isUDPQuery {
timeout = 0
}
// Kick off the race between the UDP and TCP queries.
rh := race.New(timeout, firstUDP, thenTCP)
resp, err := rh.Start(ctx)
if err == nil {
return resp, nil
}
// If we got a truncated UDP response, return that instead of an error.
if trErr, ok := errors.AsType[truncatedResponseError](err); ok {
return trErr.res, nil
}
return nil, err
}
type truncatedResponseError struct {
res []byte
}
func (tr truncatedResponseError) Error() string { return "response truncated" }
// rcodeResponseError is returned when an upstream DNS server responds with an
// rcode that is treated as a soft error (currently REFUSED and SERVFAIL). The
// response bytes are preserved so they can be returned to the client rather
// than synthesizing a new response.
type rcodeResponseError struct {
rcode dns.RCode
res []byte
}
func (r rcodeResponseError) Error() string { return r.Unwrap().Error() }
func (r rcodeResponseError) Unwrap() error {
switch r.rcode {
case dns.RCodeRefused:
return errRefused
case dns.RCodeServerFailure:
return errServerFailure
}
return nil
}
var errRefused = errors.New("response code indicates refusal")
var errServerFailure = errors.New("response code indicates server issue")
var errTxIDMismatch = errors.New("txid doesn't match")
func (f *forwarder) sendUDP(ctx context.Context, fq *forwardQuery, rr resolverAndDelay) (ret []byte, err error) {
ipp, ok := rr.name.IPPort()
if !ok {
metricDNSFwdErrorType.Add(1)
return nil, fmt.Errorf("unrecognized resolver type %q", rr.name.Addr)
}
metricDNSFwdUDP.Add(1)
ctx = sockstats.WithSockStats(ctx, sockstats.LabelDNSForwarderUDP, f.logf)
conn, err := f.dialUDP(ctx, ipp)
if err != nil {
return nil, err
}
defer conn.Close()
fq.closeOnCtxDone.Add(conn)
defer fq.closeOnCtxDone.Remove(conn)
if _, err := conn.WriteToUDPAddrPort(fq.packet, ipp); err != nil {
metricDNSFwdUDPErrorWrite.Add(1)
if err := ctx.Err(); err != nil {
return nil, err
}
return nil, err
}
metricDNSFwdUDPWrote.Add(1)
// The 1 extra byte is to detect packet truncation.
out := make([]byte, maxResponseBytes+1)
n, _, err := conn.ReadFromUDPAddrPort(out)
if err != nil {
if err := ctx.Err(); err != nil {
return nil, err
}
if neterror.PacketWasTruncated(err) {
err = nil
} else {
metricDNSFwdUDPErrorRead.Add(1)
return nil, err
}
}
truncated := n > maxResponseBytes
if truncated {
n = maxResponseBytes
}
if n < headerBytes {
f.logf("recv: packet too small (%d bytes)", n)
}
out = out[:n]
tcFlagAlreadySet := truncatedFlagSet(out)
txid := getTxID(out)
if txid != fq.txid {
metricDNSFwdUDPErrorTxID.Add(1)
return nil, errTxIDMismatch
}
rcode := getRCode(out)
// don't forward transient errors back to the client when the server fails
switch rcode {
case dns.RCodeServerFailure:
f.logf("sendUDP: response code indicating server failure: %d", rcode)
metricDNSFwdUDPErrorServer.Add(1)
return nil, rcodeResponseError{dns.RCodeServerFailure, out}
case dns.RCodeRefused:
// treat REFUSED as a soft error so other resolvers in the race can respond
f.logf("sendUDP: response code indicating refusal: %d", rcode)
metricDNSFwdUDPErrorRefused.Add(1)
return nil, rcodeResponseError{dns.RCodeRefused, out}
}
// Set the truncated bit if buffer was truncated during read and the flag isn't already set
if truncated && !tcFlagAlreadySet {
setTCFlag(out)
// TODO(#2067): Remove any incomplete records? RFC 1035 section 6.2
// states that truncation should head drop so that the authority
// section can be preserved if possible. However, the UDP read with
// a too-small buffer has already dropped the end, so that's the
// best we can do.
}
out = checkResponseSizeAndSetTC(out, fq.packet, fq.family, f.logf)
if truncatedFlagSet(out) {
metricDNSFwdTruncated.Add(1)
}
clampEDNSSize(out, maxResponseBytes)
metricDNSFwdUDPSuccess.Add(1)
return out, nil
}
// dialUDP returns a UDP conn to ipp, over netstack if that's the only way to
// reach it. Same dispatch as [tsdial.Dialer.dialOneUser].
func (f *forwarder) dialUDP(ctx context.Context, ipp netip.AddrPort) (nettype.PacketConn, error) {
if f.dialer.UseNetstackForIP != nil && f.dialer.UseNetstackForIP(ipp.Addr()) {
if f.dialer.NetstackDialUDP == nil {
return nil, errors.New("dialer not initialized correctly: no NetstackDialUDP")
}
conn, err := f.dialer.NetstackDialUDP(ctx, ipp)
if err != nil {
return nil, err
}
return &netstackPacketConn{Conn: conn, peer: ipp}, nil
}
ln, err := f.packetListener(ipp.Addr())
if err != nil {
return nil, err
}
// Name the family explicitly: netns looks for a "6" in this string to
// choose between IP_BOUND_IF and IPV6_BOUND_IF on macOS, and "udp" would
// give a v6 socket bound with the v4 option.
udpFam := "udp4"
if ipp.Addr().Is6() {
udpFam = "udp6"
}
conn, err := ln.ListenPacket(ctx, udpFam, ":0")
if err != nil {
f.logf("ListenPacket failed: %v", err)
return nil, err
}
return conn, nil
}
// netstackPacketConn presents a conn already connected to peer as a
// [nettype.PacketConn].
type netstackPacketConn struct {
net.Conn
peer netip.AddrPort
}
func (c *netstackPacketConn) WriteToUDPAddrPort(b []byte, _ netip.AddrPort) (int, error) {
return c.Write(b)
}
// ReadFromUDPAddrPort returns how much of the datagram fit in b; gVisor drops
// the rest without erroring, as a kernel socket does.
func (c *netstackPacketConn) ReadFromUDPAddrPort(b []byte) (int, netip.AddrPort, error) {
n, err := c.Read(b)
return n, c.peer, err
}
var optDNSForwardUseRoutes = envknob.RegisterOptBool("TS_DEBUG_DNS_FORWARD_USE_ROUTES")
// ShouldUseRoutes reports whether the DNS resolver should consider routes when dialing
// upstream nameservers via TCP.
//
// If true, routes should be considered ([tsdial.Dialer.UserDial]), otherwise defer
// to the system routes ([tsdial.Dialer.SystemDial]).
//
// TODO(nickkhyl): Update [tsdial.Dialer] to reuse the bart.Table we create in net/tstun.Wrapper
// to avoid having two bart tables in memory, especially on iOS. Once that's done,
// we can get rid of the nodeAttr/control knob and always use UserDial for DNS.
//
// See tailscale/tailscale#12027.
func ShouldUseRoutes(knobs *controlknobs.Knobs) bool {
if !buildfeatures.HasDNS {
return false
}
switch runtime.GOOS {
case "android", "ios":
// On mobile platforms with lower memory limits (e.g., 50MB on iOS),
// this behavior is still gated by the "user-dial-routes" nodecap.
return knobs != nil && knobs.UserDialUseRoutes.Load()
default:
// On all other platforms, it is the default behavior,
// but it can be overridden with the "TS_DEBUG_DNS_FORWARD_USE_ROUTES" env var.
doNotUseRoutes := optDNSForwardUseRoutes().EqualBool(false)
return !doNotUseRoutes
}
}
func (f *forwarder) getDialerType() netx.DialFunc {
if ShouldUseRoutes(f.controlKnobs) {
return f.dialer.UserDial
}
return f.dialer.SystemDial
}
func (f *forwarder) sendTCP(ctx context.Context, fq *forwardQuery, rr resolverAndDelay) (ret []byte, err error) {
ipp, ok := rr.name.IPPort()
if !ok {
metricDNSFwdErrorType.Add(1)
return nil, fmt.Errorf("unrecognized resolver type %q", rr.name.Addr)
}
metricDNSFwdTCP.Add(1)
ctx = sockstats.WithSockStats(ctx, sockstats.LabelDNSForwarderTCP, f.logf)
// Specify the exact family to work around https://github.com/golang/go/issues/52264
tcpFam := "tcp4"
if ipp.Addr().Is6() {
tcpFam = "tcp6"
}
ctx, cancel := context.WithTimeout(ctx, tcpQueryTimeout)
defer cancel()
conn, err := f.getDialerType()(ctx, tcpFam, ipp.String())
if err != nil {
return nil, err
}
defer conn.Close()
fq.closeOnCtxDone.Add(conn)
defer fq.closeOnCtxDone.Remove(conn)
ctxOrErr := func(err2 error) ([]byte, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
return nil, err2
}
// Write the query to the server.
query := make([]byte, len(fq.packet)+2)
binary.BigEndian.PutUint16(query, uint16(len(fq.packet)))
copy(query[2:], fq.packet)
if _, err := conn.Write(query); err != nil {
metricDNSFwdTCPErrorWrite.Add(1)
return ctxOrErr(err)
}
metricDNSFwdTCPWrote.Add(1)
// Read the header length back from the server
var length uint16
if err := binary.Read(conn, binary.BigEndian, &length); err != nil {
metricDNSFwdTCPErrorRead.Add(1)
return ctxOrErr(err)
}
// Now read the response
out := make([]byte, length)
n, err := io.ReadFull(conn, out)
if err != nil {
metricDNSFwdTCPErrorRead.Add(1)
return ctxOrErr(err)
}
if n < int(length) {
f.logf("sendTCP: packet too small (%d bytes)", n)
return nil, io.ErrUnexpectedEOF
}
out = out[:n]
txid := getTxID(out)
if txid != fq.txid {
metricDNSFwdTCPErrorTxID.Add(1)
return nil, errTxIDMismatch
}
rcode := getRCode(out)
// don't forward transient errors back to the client when the server fails
switch rcode {
case dns.RCodeServerFailure:
f.logf("sendTCP: response code indicating server failure: %d", rcode)
metricDNSFwdTCPErrorServer.Add(1)
return nil, rcodeResponseError{dns.RCodeServerFailure, out}
case dns.RCodeRefused:
// treat REFUSED as a soft error so other resolvers in the race can respond
f.logf("sendTCP: response code indicating refusal: %d", rcode)
metricDNSFwdTCPErrorRefused.Add(1)
return nil, rcodeResponseError{dns.RCodeRefused, out}
}
// TODO(andrew): do we need to do this?
//clampEDNSSize(out, maxResponseBytes)
metricDNSFwdTCPSuccess.Add(1)
return out, nil
}
// applySchemes resolves any custom-scheme entries in rrs using the provided
// scheme handlers, returning the resulting slice. Entries whose handler returns
// an error or empty string are dropped. Entries with no registered scheme pass
// through unchanged. If schemes is nil, rrs is returned as-is.
func applySchemes(logf logger.Logf, rrs []resolverAndDelay, schemes views.Map[string, CustomSchemeHandler]) []resolverAndDelay {
if schemes.IsNil() {
return rrs
}
var result []resolverAndDelay
for i, rr := range rrs {
scheme, _, hasColon := strings.Cut(rr.name.Addr, ":")
handler, isCustom := schemes.GetOk(scheme)
if !hasColon || !isCustom {
if result != nil {
result = append(result, rr)
}
continue
}
// Avoid making a results slice in the common case where there
// are no custom scheme resolvers.
if result == nil {
result = make([]resolverAndDelay, i, len(rrs))
copy(result, rrs)
}
newAddr, err := handler(rr.name.Addr)
if err != nil {
logf("error from custom scheme handler, skipping resolver : %v", err)
}
if err != nil || newAddr == "" {
continue
}
newResolver := *rr.name
newResolver.Addr = newAddr
result = append(result, resolverAndDelay{name: &newResolver, startDelay: rr.startDelay})
}
// If we didn't have any custom schemes, return the original rrs.
if result == nil {
return rrs
}
return result
}
// resolvers returns the resolvers to use for domain.
func (f *forwarder) resolvers(domain dnsname.FQDN) []resolverAndDelay {
f.mu.Lock()
routes := f.routes
cloudHostFallback := f.cloudHostFallback
schemes := f.schemeCacheLocked()
f.mu.Unlock()
for _, route := range routes {
if route.Suffix != "." && !route.Suffix.Contains(domain) {
continue
}
resolved := applySchemes(f.logf, route.Resolvers, schemes)
// If scheme resolution filtered out all resolvers from a non-empty
// route, fall through to the next matching route. If the resolvers
// were configured to be empty allow resolved to be empty.
if len(resolved) > 0 || len(route.Resolvers) == 0 {
return resolved
}
}
return cloudHostFallback // or nil if no fallback
}
// GetUpstreamResolvers returns the resolvers that would be used to resolve
// the given FQDN.
func (f *forwarder) GetUpstreamResolvers(name dnsname.FQDN) []*dnstype.Resolver {
resolvers := f.resolvers(name)
upstreamResolvers := make([]*dnstype.Resolver, 0, len(resolvers))
for _, r := range resolvers {
upstreamResolvers = append(upstreamResolvers, r.name)
}
return upstreamResolvers
}
// RegisterCustomScheme adds a [CustomSchemeHandler] that is called to provide
// an updated address when a [dnstype.Resolver.Addr] uses that scheme.
func (f *forwarder) RegisterCustomScheme(scheme string, h CustomSchemeHandler) error {
f.mu.Lock()
defer f.mu.Unlock()
if _, ok := f.schemes[scheme]; ok {
return fmt.Errorf("scheme %q already registered", scheme)
}
f.invalidateSchemeCacheLocked()
mak.Set(&f.schemes, scheme, h)
return nil
}
// invalidateSchemeCacheLocked clears f.schemeCache so that it will be rebuilt
// on the next call to f.schemeCacheLocked().
func (f *forwarder) invalidateSchemeCacheLocked() {
f.schemeCache = views.Map[string, CustomSchemeHandler]{}
}
// schemeCacheLocked returns an immutable copy of f.schemes that can be used
// after mu is unlocked.
func (f *forwarder) schemeCacheLocked() views.Map[string, CustomSchemeHandler] {
if !f.schemeCache.IsNil() {
return f.schemeCache
}
if f.schemes == nil {
return f.schemeCache // returns a nil view
}
// Regenerate the cache
f.schemeCache = views.MapOf(maps.Clone(f.schemes))
return f.schemeCache
}
// forwardQuery is information and state about a forwarded DNS query that's
// being sent to 1 or more upstreams.
//
// In the case of racing against multiple equivalent upstreams
// (e.g. Google or CloudFlare's 4 DNS IPs: 2 IPv4 + 2 IPv6), this type
// handles racing them more intelligently than just blasting away 4
// queries at once.
type forwardQuery struct {
txid txid
packet []byte
family string // "tcp" or "udp"
src netip.AddrPort
// closeOnCtxDone lets send register values to Close if the
// caller's ctx expires. This avoids send from allocating its
// own waiting goroutine to interrupt the ReadFrom, as memory
// is tight on iOS and we want the number of pending DNS
// lookups to be bursty without too much associated
// goroutine/memory cost.
closeOnCtxDone *closePool
// TODO(bradfitz): add race delay state:
// mu sync.Mutex
// ...
}
// forwardWithDestChan forwards the query to all upstream nameservers
// and waits for the first response.
//
// It either sends to responseChan and returns nil, or returns a
// non-nil error (without sending to the channel).
//
// If resolvers is non-empty, it's used explicitly (notably, for exit
// node DNS proxy queries), otherwise f.resolvers is used.
func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, responseChan chan<- packet, resolvers ...resolverAndDelay) error {
metricDNSFwd.Add(1)
domain, typ, err := nameFromQuery(query.bs)
if err != nil {
metricDNSFwdErrorName.Add(1)
return err
}
// Guarantee that the ctx we use below is done when this function returns.
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// Drop DNS service discovery spam, primarily for battery life
// on mobile. Things like Spotify on iOS generate this traffic,
// when browsing for LAN devices. But even when filtering this
// out, playing on Sonos still works.
if hasRDNSBonjourPrefix(domain) {
metricDNSFwdDropBonjour.Add(1)
res, err := nxDomainResponse(query)
if err != nil {
return err
}
select {
case <-ctx.Done():
return fmt.Errorf("waiting to send NXDOMAIN: %w", ctx.Err())
case responseChan <- res:
return nil
}
}
if fl := fwdLogAtomic.Load(); fl != nil {
fl.addName(string(domain))
}
clampEDNSSize(query.bs, maxResponseBytes)
if len(resolvers) == 0 {
resolvers = f.resolvers(domain)
if len(resolvers) == 0 {
// No upstream resolver for this name isn't a forwarder failure:
// it's split DNS / a name we weren't asked to handle. Count it
// rather than raising dnsForwarderFailing, which is reserved for
// resolvers we found but couldn't reach. See tailscale/tailscale#19931.
metricDNSFwdErrorNoUpstream.Add(1)
f.logf("no upstream resolvers set, returning SERVFAIL")
res, err := servfailResponse(query)
if err != nil {
return err
}
select {
case <-ctx.Done():
return fmt.Errorf("waiting to send SERVFAIL: %w", ctx.Err())
case responseChan <- res:
return nil
}
} else {
f.health.SetHealthy(dnsForwarderFailing)
}
}
fq := &forwardQuery{
txid: getTxID(query.bs),
packet: query.bs,
family: query.family,
src: query.addr,
closeOnCtxDone: new(closePool),
}
defer fq.closeOnCtxDone.Close()
if f.verboseFwd {
domainSha256 := sha256.Sum256([]byte(domain))
domainSig := base64.RawStdEncoding.EncodeToString(domainSha256[:3])
f.logf("request(%d, %v, %d, %s) %d...", fq.txid, typ, len(domain), domainSig, len(fq.packet))
}
resc := make(chan []byte, 1) // it's fine buffered or not
errc := make(chan error, 1) // it's fine buffered or not too
for i := range resolvers {
go func(rr *resolverAndDelay) {
if rr.startDelay > 0 {
timer := time.NewTimer(rr.startDelay)
select {
case <-timer.C:
case <-ctx.Done():
timer.Stop()
return
}
}
resb, err := f.send(ctx, fq, *rr)
if err != nil {
err = fmt.Errorf("resolving using %q: %w", rr.name.Addr, err)
select {
case errc <- err:
case <-ctx.Done():
}
return
}
select {
case resc <- resb:
case <-ctx.Done():
}
}(&resolvers[i])
}
var firstErr error
var numErr int
var sawNonRefused bool
for {
select {
case v := <-resc:
select {
case <-ctx.Done():
metricDNSFwdErrorContext.Add(1)
return fmt.Errorf("waiting to send response: %w", ctx.Err())
case responseChan <- packet{v, query.family, query.addr}:
if f.verboseFwd {
f.logf("response(%d, %v, %d) = %d, nil", fq.txid, typ, len(domain), len(v))
}
metricDNSFwdSuccess.Add(1)
f.health.SetHealthy(dnsForwarderFailing)
return nil
}
case err := <-errc:
if firstErr == nil {
firstErr = err
}
if !errors.Is(err, errRefused) {
sawNonRefused = true
}
numErr++
if numErr == len(resolvers) {
var res packet
if sawNonRefused {
// At least one server failed with SERVFAIL or a transport error
// (e.g. network failure, TxID mismatch, unsupported resolver type).
// All such errors map to SERVFAIL at the client level.
// Prefer returning the upstream SERVFAIL bytes from firstErr if
// available; otherwise synthesize a SERVFAIL response. Note the
// rcode guard: firstErr may be a REFUSED rcodeResponseError if it
// arrived before the SERVFAIL that set sawNonRefused.
if rcodeErr, ok := errors.AsType[rcodeResponseError](firstErr); ok && rcodeErr.rcode == dns.RCodeServerFailure {
res = packet{rcodeErr.res, query.family, query.addr}
} else {
r, err := servfailResponse(query)
if err != nil {
f.logf("building servfail response: %v", err)
return firstErr
}
res = r
}
} else {
// !sawNonRefused means every error was an rcodeResponseError with rcode REFUSED,
// so firstErr is guaranteed to wrap one.
rcodeErr, ok := errors.AsType[rcodeResponseError](firstErr)
if !ok {
f.logf("unexpected: all errors were REFUSED but firstErr is not rcodeResponseError: %v", firstErr)
return firstErr
}
res = packet{rcodeErr.res, query.family, query.addr}
}
select {
case <-ctx.Done():
metricDNSFwdErrorContext.Add(1)
metricDNSFwdErrorContextGotError.Add(1)
var resolverAddrs []string
for _, rr := range resolvers {
resolverAddrs = append(resolverAddrs, rr.name.Addr)
}
if f.acceptDNS {
f.health.SetUnhealthy(dnsForwarderFailing, health.Args{health.ArgDNSServers: strings.Join(resolverAddrs, ",")})
}
case responseChan <- res:
if f.verboseFwd {
f.logf("forwarder response(%d, %v, %d) = %d, %v", fq.txid, typ, len(domain), len(res.bs), firstErr)
}
return nil
}
return firstErr
}
case <-ctx.Done():
metricDNSFwdErrorContext.Add(1)
if firstErr != nil {
metricDNSFwdErrorContextGotError.Add(1)
return firstErr
}
// If we haven't got an error or a successful response,
// include all resolvers in the error message so we can
// at least see what servers we're trying to query.
var resolverAddrs []string
for _, rr := range resolvers {
resolverAddrs = append(resolverAddrs, rr.name.Addr)
}
if f.acceptDNS {
f.health.SetUnhealthy(dnsForwarderFailing, health.Args{health.ArgDNSServers: strings.Join(resolverAddrs, ",")})
}
return fmt.Errorf("waiting for response or error from %v: %w", resolverAddrs, ctx.Err())
}
}
}
var initListenConfig func(_ *net.ListenConfig, _ *netmon.Monitor, tunName string) error
// nameFromQuery extracts the normalized query name from bs.
func nameFromQuery(bs []byte) (dnsname.FQDN, dns.Type, error) {
var parser dns.Parser
hdr, err := parser.Start(bs)
if err != nil {
return "", 0, err
}
if hdr.Response {
return "", 0, errNotQuery
}
q, err := parser.Question()
if err != nil {
return "", 0, err
}
n := q.Name.Data[:q.Name.Length]
fqdn, err := dnsname.ToFQDN(rawNameToLower(n))
if err != nil {
return "", 0, err
}
return fqdn, q.Type, nil
}
// nxDomainResponse returns an NXDomain DNS reply for the provided request.
func nxDomainResponse(req packet) (res packet, err error) {
p := dnsParserPool.Get().(*dnsParser)
defer dnsParserPool.Put(p)
if err := p.parseQuery(req.bs); err != nil {
return packet{}, err
}
h := p.Header
h.Response = true
h.RecursionAvailable = h.RecursionDesired
h.RCode = dns.RCodeNameError
b := dns.NewBuilder(nil, h)
// TODO(bradfitz): should we add an SOA record in the Authority
// section too? (for the nxdomain negative caching TTL)
// For which zone? Does iOS care?
b.StartQuestions()
b.Question(p.Question)
res.bs, err = b.Finish()
res.addr = req.addr
return res, err
}
// servfailResponse returns a SERVFAIL error reply for the provided request.
func servfailResponse(req packet) (res packet, err error) {
p := dnsParserPool.Get().(*dnsParser)
defer dnsParserPool.Put(p)
if err := p.parseQuery(req.bs); err != nil {
return packet{}, err
}
h := p.Header
h.Response = true
h.Authoritative = true
h.RCode = dns.RCodeServerFailure
b := dns.NewBuilder(nil, h)
b.StartQuestions()
b.Question(p.Question)
res.bs, err = b.Finish()
res.addr = req.addr
return res, err
}
// closePool is a dynamic set of io.Closers to close as a group.
// It's intended to be Closed at most once.
//
// The zero value is ready for use.
type closePool struct {
mu sync.Mutex
m map[io.Closer]bool
closed bool
}
func (p *closePool) Add(c io.Closer) {
p.mu.Lock()
defer p.mu.Unlock()
if p.closed {
c.Close()
return
}
if p.m == nil {
p.m = map[io.Closer]bool{}
}
p.m[c] = true
}
func (p *closePool) Remove(c io.Closer) {
p.mu.Lock()
defer p.mu.Unlock()
if p.closed {
return
}
delete(p.m, c)
}
func (p *closePool) Close() error {
p.mu.Lock()
defer p.mu.Unlock()
if p.closed {
return nil
}
p.closed = true
for c := range p.m {
c.Close()
}
return nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package resolver implements a stub DNS resolver that can also serve
// records out of an internal local zone.
package resolver
import (
"bufio"
"context"
"encoding/hex"
"errors"
"fmt"
"io"
"net"
"net/netip"
"os"
"runtime"
"slices"
"strconv"
"strings"
"sync"
"time"
dns "golang.org/x/net/dns/dnsmessage"
"tailscale.com/control/controlknobs"
"tailscale.com/envknob"
"tailscale.com/feature"
"tailscale.com/feature/buildfeatures"
"tailscale.com/health"
"tailscale.com/net/dns/resolvconffile"
"tailscale.com/net/netaddr"
"tailscale.com/net/netmon"
"tailscale.com/net/tsaddr"
"tailscale.com/net/tsdial"
"tailscale.com/syncs"
"tailscale.com/types/dnstype"
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
"tailscale.com/util/cloudenv"
"tailscale.com/util/dnsname"
"tailscale.com/util/set"
)
const dnsSymbolicFQDN = "magicdns.localhost-tailscale-daemon."
// maxResponseBytes is the maximum size of a response from a Resolver. The
// actual buffer size will be one larger than this so that we can detect
// truncation in a platform-agnostic way.
const maxResponseBytes = 4095
// defaultTTL is the TTL of positive responses from Resolver.
//
// It's short because the source of truth (the netmap-fed host maps)
// is local and in-memory, so re-queries are nearly free, while
// anything cached downstream (e.g. mDNSResponder on macOS) delays
// clients noticing node renames for the full TTL (tailscale/corp#45631).
const defaultTTL = 5 * time.Second
// negativeTTL is how long resolvers may cache the nonexistence of a
// name (or of records of the queried type) for domains we're
// authoritative for. It's advertised via the SOA record attached to
// the authority section of NXDOMAIN and no-data responses, per RFC
// 2308. Without it, some resolvers (notably mDNSResponder) seem to
// cache negative entries for a really long time, so a name queried
// shortly before a node rename doesn't start resolving for a while
// (tailscale/corp#45631).
const negativeTTL = 10 * time.Second
// timeNow is time.Now, except in tests.
var timeNow = time.Now
var (
errNotQuery = errors.New("not a DNS query")
errNotOurName = errors.New("not a Tailscale DNS name")
)
type packet struct {
bs []byte
family string // either "tcp" or "udp"
addr netip.AddrPort // src for a request, dst for a response
}
// Config is a resolver configuration.
// Given a Config, queries are resolved in the following order:
// If the query is an exact match for an entry in LocalHosts, return that.
// Else if the query suffix matches an entry in LocalDomains, return NXDOMAIN.
// Else forward the query to the most specific matching entry in Routes.
// Else return SERVFAIL.
type Config struct {
// True if [Prefs.CorpDNS] is true or --accept-dns=true was specified.
// This should only be used for error handling and health reporting.
AcceptDNS bool
// Routes is a map of DNS name suffix to the resolvers to use for
// queries within that suffix.
// Queries only match the most specific suffix.
// To register a "default route", add an entry for ".".
Routes map[dnsname.FQDN][]*dnstype.Resolver
// LocalHosts is a map of FQDNs to corresponding IPs.
Hosts map[dnsname.FQDN][]netip.Addr
// LocalDomains is a list of DNS name suffixes that should not be
// routed to upstream resolvers.
LocalDomains []dnsname.FQDN
// SubdomainHosts is a set of FQDNs from Hosts that should also
// resolve subdomain queries to the same IPs. If a query like
// "sub.node.tailnet.ts.net" doesn't match Hosts directly, and
// "node.tailnet.ts.net" is in SubdomainHosts, the query resolves
// to the IPs for "node.tailnet.ts.net".
SubdomainHosts set.Set[dnsname.FQDN]
}
// WriteToBufioWriter write a debug version of c for logs to w, omitting
// spammy stuff like *.arpa entries and replacing it with a total count.
func (c *Config) WriteToBufioWriter(w *bufio.Writer) {
w.WriteString("{Routes:")
WriteRoutes(w, c.Routes)
fmt.Fprintf(w, " Hosts:%v LocalDomains:[", len(c.Hosts))
space := false
arpa := 0
for _, d := range c.LocalDomains {
if strings.HasSuffix(string(d), ".arpa.") {
arpa++
continue
}
if space {
w.WriteByte(' ')
}
w.WriteString(string(d))
space = true
}
w.WriteString("]")
if arpa > 0 {
fmt.Fprintf(w, "+%darpa", arpa)
}
if c := cloudenv.Get(); c != "" {
fmt.Fprintf(w, ", cloud=%q", string(c))
}
w.WriteString("}")
}
// WriteIPPorts writes vv to w.
func WriteIPPorts(w *bufio.Writer, vv []netip.AddrPort) {
w.WriteByte('[')
var b []byte
for i, v := range vv {
if i > 0 {
w.WriteByte(' ')
}
b = v.AppendTo(b[:0])
w.Write(b)
}
w.WriteByte(']')
}
// WriteDNSResolver writes r to w.
func WriteDNSResolver(w *bufio.Writer, r *dnstype.Resolver) {
io.WriteString(w, r.Addr)
if len(r.BootstrapResolution) > 0 {
w.WriteByte('(')
var b []byte
for _, ip := range r.BootstrapResolution {
ip.AppendTo(b[:0])
w.Write(b)
}
w.WriteByte(')')
}
}
// WriteDNSResolvers writes resolvers to w.
func WriteDNSResolvers(w *bufio.Writer, resolvers []*dnstype.Resolver) {
w.WriteByte('[')
for i, r := range resolvers {
if i > 0 {
w.WriteByte(' ')
}
WriteDNSResolver(w, r)
}
w.WriteByte(']')
}
// WriteRoutes writes routes to w, omitting *.arpa routes and instead
// summarizing how many of them there were.
func WriteRoutes(w *bufio.Writer, routes map[dnsname.FQDN][]*dnstype.Resolver) {
var kk []dnsname.FQDN
arpa := 0
for k := range routes {
if strings.HasSuffix(string(k), ".arpa.") {
arpa++
continue
}
kk = append(kk, k)
}
slices.Sort(kk)
w.WriteByte('{')
for i, k := range kk {
if i > 0 {
w.WriteByte(' ')
}
w.WriteString(string(k))
w.WriteByte(':')
WriteDNSResolvers(w, routes[k])
}
w.WriteByte('}')
if arpa > 0 {
fmt.Fprintf(w, "+%darpa", arpa)
}
}
// RoutesRequireNoCustomResolvers returns true if this resolver.Config only contains routes
// that do not specify a set of custom resolver(s), i.e. they can be resolved by the local
// upstream DNS resolver.
func (c *Config) RoutesRequireNoCustomResolvers() bool {
for route, resolvers := range c.Routes {
if route.WithoutTrailingDot() == "ts.net" {
// Ignore the "ts.net" route here. It always specifies the corp resolvers but
// its presence is not an issue, as ts.net will be a search domain.
continue
}
if len(resolvers) != 0 {
// Found a route with custom resolvers.
return false
}
}
// No routes other than ts.net have specified one or more resolvers.
return true
}
// Resolver is a DNS resolver for nodes on the Tailscale network,
// associating them with domain names of the form <mynode>.<mydomain>.<root>.
// If it is asked to resolve a domain that is not of that form,
// it delegates to upstream nameservers if any are set.
type Resolver struct {
logf logger.Logf
netMon *netmon.Monitor // non-nil
dialer *tsdial.Dialer // non-nil
health *health.Tracker // non-nil
saveConfigForTests func(cfg Config) // used in tests to capture resolver config
// forwarder forwards requests to upstream nameservers.
forwarder *forwarder
// closed signals all goroutines to stop.
closed chan struct{}
// mu guards the following fields from being updated while used.
mu syncs.Mutex
localDomains []dnsname.FQDN
hostToIP map[dnsname.FQDN][]netip.Addr
ipToHost map[netip.Addr]dnsname.FQDN
subdomainHosts set.Set[dnsname.FQDN]
magicHosts MagicDNSHosts // or nil if none installed
}
// MagicDNSHosts is a live source of MagicDNS host records, installed
// via [Resolver.SetMagicDNSHosts].
//
// It replaces the per-node entries of [Config.Hosts]: instead of the
// caller pushing a full snapshot of every node's name and addresses
// into the resolver on every (possibly incremental) netmap change,
// the resolver pulls the answer for one name on demand from the
// caller's live indexes. [Config.Hosts] remains for control's
// DNS.ExtraRecords entries, which are few, and is consulted first.
//
// Implementations must be safe for concurrent use and cheap: the
// methods are called on the DNS query serving path. Name lookups are
// case-insensitive: the resolver passes lowercase names, but
// implementations must not rely on that.
//
// Short names ("foo") are only resolved if MagicDNS is enabled
// for the current tailnet.
type MagicDNSHosts interface {
// LookupHost returns the IPs to answer for the node with the
// given MagicDNS FQDN, and whether the name is known. It returns
// all answerable IPs regardless of record type; the resolver
// filters them by the query's type, and a known name with no IPs
// of the query's family is "name exists, no records", not
// NXDOMAIN.
LookupHost(dnsname.FQDN) (ips []netip.Addr, ok bool)
// LookupPTR returns the MagicDNS FQDN of the node that owns
// the given Tailscale IP, and whether the IP is known.
LookupPTR(netip.Addr) (_ dnsname.FQDN, ok bool)
// SubdomainHost reports whether fqdn names a node with the
// [tailcfg.NodeAttrDNSSubdomainResolve] attribute, whose
// subdomains all resolve to the node's own addresses.
SubdomainHost(dnsname.FQDN) bool
}
// SetMagicDNSHosts installs the live MagicDNS host source consulted
// by forward and reverse MagicDNS lookups that miss [Config.Hosts].
// It is expected to be called once, before the resolver serves
// queries.
func (r *Resolver) SetMagicDNSHosts(h MagicDNSHosts) {
if !buildfeatures.HasDNS {
return
}
r.mu.Lock()
defer r.mu.Unlock()
r.magicHosts = h
}
type ForwardLinkSelector interface {
// PickLink returns which network device should be used to query
// the DNS server at the given IP.
// The empty string means to use an unspecified default.
PickLink(netip.Addr) (linkName string)
}
// New returns a new resolver.
// dialer and health must be non-nil.
func New(logf logger.Logf, linkSel ForwardLinkSelector, dialer *tsdial.Dialer, health *health.Tracker, knobs *controlknobs.Knobs) *Resolver {
if dialer == nil {
panic("nil Dialer")
}
if health == nil {
panic("nil health")
}
netMon := dialer.NetMon()
if netMon == nil {
logf("nil netMon")
}
r := &Resolver{
logf: logger.WithPrefix(logf, "resolver: "),
netMon: netMon,
closed: make(chan struct{}),
hostToIP: map[dnsname.FQDN][]netip.Addr{},
ipToHost: map[netip.Addr]dnsname.FQDN{},
dialer: dialer,
health: health,
}
r.forwarder = newForwarder(r.logf, netMon, linkSel, dialer, health, knobs)
return r
}
func (r *Resolver) TestOnlySetHook(hook func(Config)) { r.saveConfigForTests = hook }
// ProbeLocks acquires and releases the resolver's internal mutexes.
func (r *Resolver) ProbeLocks() {
r.mu.Lock()
r.mu.Unlock()
if r.forwarder != nil {
r.forwarder.probeLocks()
}
}
func (r *Resolver) SetConfig(cfg Config) error {
if !buildfeatures.HasDNS {
return nil
}
if r.saveConfigForTests != nil {
r.saveConfigForTests(cfg)
}
reverse := make(map[netip.Addr]dnsname.FQDN, len(cfg.Hosts))
for host, ips := range cfg.Hosts {
for _, ip := range ips {
reverse[ip] = host
}
}
r.forwarder.setRoutes(cfg.Routes, cfg.AcceptDNS)
r.mu.Lock()
defer r.mu.Unlock()
r.localDomains = cfg.LocalDomains
r.hostToIP = cfg.Hosts
r.ipToHost = reverse
r.subdomainHosts = cfg.SubdomainHosts
return nil
}
// CustomSchemeHandler takes a URI (retrieved from [dnstype.Resolver.Addr]) and
// returns an updated URI to use for the current query. The result is only valid
// for right now and may change over time.
type CustomSchemeHandler func(addr string) (newAddr string, err error)
// RegisterCustomScheme adds a [CustomSchemaHandler] that is called to provide
// an updated address to the forwarder when a [dnstype.Resolver.Addr] uses that
// scheme.
func (r *Resolver) RegisterCustomScheme(scheme string, h CustomSchemeHandler) error {
return r.forwarder.RegisterCustomScheme(scheme, h)
}
// Close shuts down the resolver and ensures poll goroutines have exited.
// The Resolver cannot be used again after Close is called.
func (r *Resolver) Close() {
if !buildfeatures.HasDNS {
return
}
select {
case <-r.closed:
return
default:
// continue
}
close(r.closed)
r.forwarder.Close()
}
// dnsQueryTimeout is not intended to be user-visible (the users
// DNS resolver will retry well before that), just put an upper
// bound on per-query resource usage.
const dnsQueryTimeout = 10 * time.Second
func (r *Resolver) Query(ctx context.Context, bs []byte, family string, from netip.AddrPort) ([]byte, error) {
if !buildfeatures.HasDNS {
return nil, feature.ErrUnavailable
}
metricDNSQueryLocal.Add(1)
select {
case <-r.closed:
metricDNSQueryErrorClosed.Add(1)
return nil, net.ErrClosed
default:
}
out, err := r.respond(bs)
if err == errNotOurName {
responses := make(chan packet, 1)
ctx, cancel := context.WithTimeout(ctx, dnsQueryTimeout)
defer close(responses)
defer cancel()
err = r.forwarder.forwardWithDestChan(ctx, packet{bs, family, from}, responses)
if err != nil {
return nil, err
}
return (<-responses).bs, nil
}
if err != nil {
return out, err
}
out = checkResponseSizeAndSetTC(out, bs, family, r.logf)
return out, nil
}
// GetUpstreamResolvers returns the resolvers that would be used to resolve
// the given FQDN.
func (r *Resolver) GetUpstreamResolvers(name dnsname.FQDN) []*dnstype.Resolver {
if !buildfeatures.HasDNS {
return nil
}
return r.forwarder.GetUpstreamResolvers(name)
}
// parseExitNodeQuery parses a DNS request packet.
// It returns nil if it's malformed or lacking a question.
func parseExitNodeQuery(q []byte) *response {
p := dnsParserPool.Get().(*dnsParser)
defer dnsParserPool.Put(p)
p.zeroParser()
defer p.zeroParser()
if err := p.parseQuery(q); err != nil {
return nil
}
return p.response()
}
// HandlePeerDNSQuery handles a DNS query that arrived from a peer
// via the peerapi's DoH server. This is used when the local
// node is being an exit node or an app connector.
//
// The provided allowName callback is whether a DNS query for a name
// (as found by parsing q) is allowed.
//
// In most (all?) cases, err will be nil. A bogus DNS query q will
// still result in a response DNS packet (saying there's a failure)
// and a nil error.
// TODO: figure out if we even need an error result.
func (r *Resolver) HandlePeerDNSQuery(ctx context.Context, q []byte, from netip.AddrPort, allowName func(name string) bool) (res []byte, err error) {
if !buildfeatures.HasDNS {
return nil, feature.ErrUnavailable
}
metricDNSExitProxyQuery.Add(1)
ch := make(chan packet, 1)
resp := parseExitNodeQuery(q)
if resp == nil {
return nil, errors.New("bad query")
}
name := resp.Question.Name.String()
if !allowName(name) {
metricDNSExitProxyErrorName.Add(1)
resp.Header.RCode = dns.RCodeRefused
return marshalResponse(resp)
}
switch runtime.GOOS {
default:
return nil, errors.New("unsupported exit node OS")
case "windows", "android":
return handleExitNodeDNSQueryWithNetPkg(ctx, r.logf, nil, resp)
case "darwin":
// /etc/resolv.conf is a lie and only says one upstream DNS
// but for now that's probably good enough. Later we'll
// want to blend in everything from scutil --dns.
fallthrough
case "linux", "freebsd", "openbsd", "illumos", "solaris", "ios":
nameserver, err := stubResolverForOS()
if err != nil {
r.logf("stubResolverForOS: %v", err)
metricDNSExitProxyErrorResolvConf.Add(1)
return nil, err
}
// TODO: more than 1 resolver from /etc/resolv.conf?
var resolvers []resolverAndDelay
switch nameserver {
case tsaddr.TailscaleServiceIP(), tsaddr.TailscaleServiceIPv6():
// If resolv.conf says 100.100.100.100, it's coming right back to us anyway
// so avoid the loop through the kernel and just do what we
// would've done anyway. By not passing any resolvers, the forwarder
// will use its default ones from our DNS config.
case netip.Addr{}:
// Likewise, if the platform has no resolv.conf, just use our defaults.
default:
resolvers = []resolverAndDelay{{
name: &dnstype.Resolver{Addr: net.JoinHostPort(nameserver.String(), "53")},
}}
}
err = r.forwarder.forwardWithDestChan(ctx, packet{q, "tcp", from}, ch, resolvers...)
if err != nil {
metricDNSExitProxyErrorForward.Add(1)
return nil, err
}
}
select {
case p, ok := <-ch:
if ok {
return p.bs, nil
}
panic("unexpected close chan")
default:
panic("unexpected unreadable chan")
}
}
var debugExitNodeDNSNetPkg = envknob.RegisterBool("TS_DEBUG_EXIT_NODE_DNS_NET_PKG")
// handleExitNodeDNSQueryWithNetPkg takes a DNS query message in q and
// return a reply (for the ExitDNS DoH service) using the net package's
// native APIs.
//
// If resolver is nil, the net.Resolver zero value is used.
//
// response contains the pre-serialized response, which notably
// includes the original question and its header.
func handleExitNodeDNSQueryWithNetPkg(ctx context.Context, logf logger.Logf, resolver *net.Resolver, resp *response) (res []byte, err error) {
if !buildfeatures.HasDNS {
return nil, feature.ErrUnavailable
}
logf = logger.WithPrefix(logf, "exitNodeDNSQueryWithNetPkg: ")
if resp.Question.Class != dns.ClassINET {
return nil, errors.New("unsupported class")
}
r := resolver
if r == nil {
r = new(net.Resolver)
}
name := resp.Question.Name.String()
handleError := func(err error) (res []byte, _ error) {
if isGoNoSuchHostError(err) {
if debugExitNodeDNSNetPkg() {
logf(`converting Go "no such host" error to a NXDOMAIN: %v`, err)
}
resp.Header.RCode = dns.RCodeNameError
return marshalResponse(resp)
}
if debugExitNodeDNSNetPkg() {
logf("returning error: %v", err)
}
// TODO: map other errors to RCodeServerFailure?
// Or I guess our caller should do that?
return nil, err
}
resp.Header.RCode = dns.RCodeSuccess // unless changed below
switch resp.Question.Type {
case dns.TypeA, dns.TypeAAAA:
network := "ip4"
if resp.Question.Type == dns.TypeAAAA {
network = "ip6"
}
if debugExitNodeDNSNetPkg() {
logf("resolving %s %q", network, name)
}
ips, err := r.LookupIP(ctx, network, name)
if err != nil {
return handleError(err)
}
for _, stdIP := range ips {
if ip, ok := netip.AddrFromSlice(stdIP); ok {
resp.IPs = append(resp.IPs, ip.Unmap())
}
}
case dns.TypeTXT:
if debugExitNodeDNSNetPkg() {
logf("resolving TXT %q", name)
}
strs, err := r.LookupTXT(ctx, name)
if err != nil {
return handleError(err)
}
resp.TXT = strs
case dns.TypePTR:
ipStr, ok := unARPA(name)
if !ok {
// TODO: is this RCodeFormatError?
return nil, errors.New("bogus PTR name")
}
if debugExitNodeDNSNetPkg() {
logf("resolving PTR %q", ipStr)
}
addrs, err := r.LookupAddr(ctx, ipStr)
if err != nil {
return handleError(err)
}
if len(addrs) > 0 {
resp.Name, _ = dnsname.ToFQDN(addrs[0])
}
case dns.TypeCNAME:
if debugExitNodeDNSNetPkg() {
logf("resolving CNAME %q", name)
}
cname, err := r.LookupCNAME(ctx, name)
if err != nil {
return handleError(err)
}
resp.CNAME = cname
case dns.TypeSRV:
if debugExitNodeDNSNetPkg() {
logf("resolving SRV %q", name)
}
// Thanks, Go: "To accommodate services publishing SRV
// records under non-standard names, if both service
// and proto are empty strings, LookupSRV looks up
// name directly."
_, srvs, err := r.LookupSRV(ctx, "", "", name)
if err != nil {
return handleError(err)
}
resp.SRVs = srvs
case dns.TypeNS:
if debugExitNodeDNSNetPkg() {
logf("resolving NS %q", name)
}
nss, err := r.LookupNS(ctx, name)
if err != nil {
return handleError(err)
}
resp.NSs = nss
default:
return nil, fmt.Errorf("unsupported record type %v", resp.Question.Type)
}
return marshalResponse(resp)
}
func isGoNoSuchHostError(err error) bool {
if de, ok := err.(*net.DNSError); ok {
return de.IsNotFound
}
return false
}
type resolvConfCache struct {
mod time.Time
size int64
ip netip.Addr
// TODO: inode/dev?
}
// resolvConfCacheValue contains the most recent stat metadata and parsed
// version of /etc/resolv.conf.
var resolvConfCacheValue syncs.AtomicValue[resolvConfCache]
var errEmptyResolvConf = errors.New("resolv.conf has no nameservers")
// stubResolverForOS returns the IP address of the first nameserver in
// /etc/resolv.conf.
//
// It may also return the netip.Addr zero value and a nil error to mean
// that the platform has no resolv.conf.
func stubResolverForOS() (ip netip.Addr, err error) {
if runtime.GOOS == "ios" {
return netip.Addr{}, nil // no resolv.conf on iOS
}
fi, err := os.Stat(resolvconffile.Path)
if err != nil {
return netip.Addr{}, err
}
cur := resolvConfCache{
mod: fi.ModTime(),
size: fi.Size(),
}
if c, ok := resolvConfCacheValue.LoadOk(); ok && c.mod == cur.mod && c.size == cur.size {
return c.ip, nil
}
conf, err := resolvconffile.ParseFile(resolvconffile.Path)
if err != nil {
return netip.Addr{}, err
}
if len(conf.Nameservers) == 0 {
return netip.Addr{}, errEmptyResolvConf
}
ip = conf.Nameservers[0]
cur.ip = ip
resolvConfCacheValue.Store(cur)
return ip, nil
}
// resolveLocal returns an IP for the given domain, if domain is in
// the local hosts map and has an IP corresponding to the requested
// typ (A, AAAA, ALL).
// Returns dns.RCodeRefused to indicate that the local map is not
// authoritative for domain.
func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netip.Addr, dns.RCode) {
metricDNSResolveLocal.Add(1)
// Reject .onion domains per RFC 7686.
if dnsname.HasSuffix(domain.WithoutTrailingDot(), ".onion") {
metricDNSResolveLocalErrorOnion.Add(1)
return netip.Addr{}, dns.RCodeNameError
}
// We return a symbolic domain if someone does a reverse lookup on the
// DNS endpoint. To round out this special case, we also do the inverse
// (returning the endpoint IP if someone looks up the symbolic domain).
if domain == dnsSymbolicFQDN {
switch typ {
case dns.TypeA:
return tsaddr.TailscaleServiceIP(), dns.RCodeSuccess
case dns.TypeAAAA:
return tsaddr.TailscaleServiceIPv6(), dns.RCodeSuccess
}
}
// Special-case: 4via6 DNS names.
if ip, ok := r.resolveViaDomain(domain, typ); ok {
return ip, dns.RCodeSuccess
}
r.mu.Lock()
hosts := r.hostToIP
localDomains := r.localDomains
subdomainHosts := r.subdomainHosts
magicHosts := r.magicHosts
r.mu.Unlock()
addrs, found := hosts[domain]
if !found && magicHosts != nil {
addrs, found = magicHosts.LookupHost(domain)
}
if !found {
for parent := domain.Parent(); parent != ""; parent = parent.Parent() {
if subdomainHosts.Contains(parent) {
addrs, found = hosts[parent]
break
}
if magicHosts != nil && magicHosts.SubdomainHost(parent) {
addrs, found = magicHosts.LookupHost(parent)
break
}
}
}
if !found {
for _, suffix := range localDomains {
if suffix.Contains(domain) {
// We are authoritative for the queried domain.
metricDNSResolveLocalErrorMissing.Add(1)
return netip.Addr{}, dns.RCodeNameError
}
}
// Not authoritative, signal that forwarding is advisable.
metricDNSResolveLocalErrorRefused.Add(1)
return netip.Addr{}, dns.RCodeRefused
}
// Refactoring note: this must happen after we check suffixes,
// otherwise we will respond with NOTIMP to requests that should be forwarded.
//
// DNS semantics subtlety: when a DNS name exists, but no records
// are available for the requested record type, we must return
// RCodeSuccess with no data, not NXDOMAIN.
switch typ {
case dns.TypeA:
for _, ip := range addrs {
if ip.Is4() {
metricDNSResolveLocalOKA.Add(1)
return ip, dns.RCodeSuccess
}
}
metricDNSResolveLocalNoA.Add(1)
return netip.Addr{}, dns.RCodeSuccess
case dns.TypeAAAA:
for _, ip := range addrs {
if ip.Is6() {
metricDNSResolveLocalOKAAAA.Add(1)
return ip, dns.RCodeSuccess
}
}
metricDNSResolveLocalNoAAAA.Add(1)
return netip.Addr{}, dns.RCodeSuccess
case dns.TypeALL:
// Answer with whatever we've got.
// It could be IPv4, IPv6, or a zero addr.
// TODO: Return all available resolutions (A and AAAA, if we have them).
if len(addrs) == 0 {
metricDNSResolveLocalNoAll.Add(1)
return netip.Addr{}, dns.RCodeSuccess
}
metricDNSResolveLocalOKAll.Add(1)
return addrs[0], dns.RCodeSuccess
// Leave some record types explicitly unimplemented.
// These types relate to recursive resolution or special
// DNS semantics and might be implemented in the future.
case dns.TypeNS, dns.TypeSOA, dns.TypeAXFR, dns.TypeHINFO:
metricDNSResolveNotImplType.Add(1)
return netip.Addr{}, dns.RCodeNotImplemented
// For everything except for the few types above that are explicitly not implemented, return no records.
// This is what other DNS systems do: always return NOERROR
// without any records whenever the requested record type is unknown.
// You can try this with:
// dig -t TYPE9824 example.com
// and note that NOERROR is returned, despite that record type being made up.
default:
metricDNSResolveNoRecordType.Add(1)
// The name exists, but no records exist of the requested type.
return netip.Addr{}, dns.RCodeSuccess
}
}
// resolveViaDomain synthesizes an IP address for quad-A DNS requests of the form
// `<IPv4-address-with-hypens-instead-of-dots>-via-<siteid>[.*]`.
// For example: "192-168-1-2-via-7" or "192-168-1-2-via-7.foo.ts.net."
//
// This exists as a convenient mapping into Tailscales 'Via Range'.
//
// It returns a zero netip.Addr and true to indicate a successful response with
// an empty answers section if the specified domain is a valid Tailscale 4via6
// domain, but the request type is neither quad-A nor ALL.
func (r *Resolver) resolveViaDomain(dnsName dnsname.FQDN, typ dns.Type) (netip.Addr, bool) {
fqdn := string(dnsName.WithoutTrailingDot())
switch typ {
case dns.TypeA, dns.TypeAAAA, dns.TypeALL:
// For Type A requests, we should return a successful response
// with an empty answer section rather than an NXDomain
// if the specified domain is a valid Tailscale 4via6 domain.
//
// Therefore, we should continue and parse the domain name first
// before deciding whether to return an IPv6 address,
// a zero (invalid) netip.Addr and true to indicate a successful empty response,
// or a zero netip.Addr and false to indicate that it is not a Tailscale 4via6 domain.
default:
return netip.Addr{}, false
}
if len(fqdn) < len("0-0-0-0-via-0") {
return netip.Addr{}, false // too short to be valid
}
if !strings.Contains(fqdn, "-via-") {
return netip.Addr{}, false // not a 4via6 domain
}
firstLabel, domain, _ := strings.Cut(fqdn, ".") // "192-168-1-2-via-7"
if !(domain == "" || dnsname.HasSuffix(domain, "ts.net") || dnsname.HasSuffix(domain, "tailscale.net")) {
return netip.Addr{}, false
}
v4hyphens, suffix, ok := strings.Cut(firstLabel, "-via-")
if !ok {
return netip.Addr{}, false
}
siteID := suffix
ip4Str := strings.ReplaceAll(v4hyphens, "-", ".")
ip4, err := netip.ParseAddr(ip4Str)
if err != nil {
return netip.Addr{}, false // badly formed, don't respond
}
prefix, err := strconv.ParseUint(siteID, 0, 32)
if err != nil {
return netip.Addr{}, false // badly formed, don't respond
}
if typ == dns.TypeA {
return netip.Addr{}, true // the name exists, but cannot be resolved to an IPv4 address
}
// MapVia will never error when given an IPv4 netip.Prefix.
out, _ := tsaddr.MapVia(uint32(prefix), netip.PrefixFrom(ip4, ip4.BitLen()))
return out.Addr(), true
}
// resolveReverse returns the unique domain name that maps to the given address.
func (r *Resolver) resolveLocalReverse(name dnsname.FQDN) (dnsname.FQDN, dns.RCode) {
var ip netip.Addr
var ok bool
switch {
case strings.HasSuffix(name.WithTrailingDot(), rdnsv4Suffix):
ip, ok = rdnsNameToIPv4(name)
case strings.HasSuffix(name.WithTrailingDot(), rdnsv6Suffix):
ip, ok = rdnsNameToIPv6(name)
}
if !ok {
// This isn't a well-formed in-addr.arpa or ip6.arpa name, but
// who knows what upstreams might do, try kicking it up to
// them. We definitely won't handle it.
return "", dns.RCodeRefused
}
r.mu.Lock()
defer r.mu.Unlock()
// If the requested IP is part of the IPv6 4-to-6 range, it might
// correspond to an IPv4 address (assuming IPv4 is enabled).
if ip4, ok := tsaddr.Tailscale6to4(ip); ok {
fqdn, code := r.fqdnForIPLocked(ip4, name)
if code == dns.RCodeSuccess {
return fqdn, code
}
}
return r.fqdnForIPLocked(ip, name)
}
// r.mu must be held.
func (r *Resolver) fqdnForIPLocked(ip netip.Addr, name dnsname.FQDN) (dnsname.FQDN, dns.RCode) {
// If someone curiously does a reverse lookup on the DNS IP, we
// return a domain that helps indicate that Tailscale is using
// this IP for a special purpose and it is not a node on their
// tailnet.
if ip == tsaddr.TailscaleServiceIP() || ip == tsaddr.TailscaleServiceIPv6() {
return dnsSymbolicFQDN, dns.RCodeSuccess
}
ret, ok := r.ipToHost[ip]
if !ok && r.magicHosts != nil {
ret, ok = r.magicHosts.LookupPTR(ip)
}
if !ok {
for _, suffix := range r.localDomains {
if suffix.Contains(name) {
// We are authoritative for this chunk of IP space.
return "", dns.RCodeNameError
}
}
// Not authoritative, signal that forwarding is advisable.
return "", dns.RCodeRefused
}
return ret, dns.RCodeSuccess
}
type response struct {
Header dns.Header
Question dns.Question
// Name is the response to a PTR query.
Name dnsname.FQDN
// IP and IPs are the responses to an A, AAAA, or ALL query.
// Either/both/neither can be populated.
IP netip.Addr
IPs []netip.Addr
// TXT is the response to a TXT query.
// Each one is its own RR with one string.
TXT []string
// CNAME is the response to a CNAME query.
CNAME string
// SRVs are the responses to a SRV query.
SRVs []*net.SRV
// NSs are the responses to an NS query.
NSs []*net.NS
// SOAZone, if non-empty, is the zone we're authoritative for
// that contains Question's name. marshalResponse attaches its
// SOA record to the authority section so that resolvers bound
// their negative caching to negativeTTL, per RFC 2308. It must
// only be set on negative responses: NXDOMAIN, or success with
// no records of the queried type.
SOAZone dnsname.FQDN
}
var dnsParserPool = &sync.Pool{
New: func() any {
return new(dnsParser)
},
}
// dnsParser parses DNS queries using x/net/dns/dnsmessage.
// These structs are pooled with dnsParserPool.
type dnsParser struct {
Header dns.Header
Question dns.Question
parser dns.Parser
}
func (p *dnsParser) response() *response {
return &response{Header: p.Header, Question: p.Question}
}
// zeroParser clears parser so it doesn't retain its most recently
// parsed DNS query's []byte while it's sitting in a sync.Pool.
// It's not useful to keep anyway: the next Start will do the same.
func (p *dnsParser) zeroParser() { p.parser = dns.Parser{} }
// parseQuery parses the query in given packet into p.Header and
// p.Question.
func (p *dnsParser) parseQuery(query []byte) error {
defer p.zeroParser()
p.zeroParser()
var err error
p.Header, err = p.parser.Start(query)
if err != nil {
return err
}
if p.Header.Response {
return errNotQuery
}
p.Question, err = p.parser.Question()
return err
}
// marshalARecord serializes an A record into an active builder.
// The caller may continue using the builder following the call.
func marshalARecord(name dns.Name, ip netip.Addr, builder *dns.Builder) error {
var answer dns.AResource
answerHeader := dns.ResourceHeader{
Name: name,
Type: dns.TypeA,
Class: dns.ClassINET,
TTL: uint32(defaultTTL / time.Second),
}
ipbytes := ip.As4()
copy(answer.A[:], ipbytes[:])
return builder.AResource(answerHeader, answer)
}
// marshalAAAARecord serializes an AAAA record into an active builder.
// The caller may continue using the builder following the call.
func marshalAAAARecord(name dns.Name, ip netip.Addr, builder *dns.Builder) error {
var answer dns.AAAAResource
answerHeader := dns.ResourceHeader{
Name: name,
Type: dns.TypeAAAA,
Class: dns.ClassINET,
TTL: uint32(defaultTTL / time.Second),
}
ipbytes := ip.As16()
copy(answer.AAAA[:], ipbytes[:])
return builder.AAAAResource(answerHeader, answer)
}
func marshalIP(name dns.Name, ip netip.Addr, builder *dns.Builder) error {
if ip.Is4() {
return marshalARecord(name, ip, builder)
}
if ip.Is6() {
return marshalAAAARecord(name, ip, builder)
}
return nil
}
// marshalPTRRecord serializes a PTR record into an active builder.
// The caller may continue using the builder following the call.
func marshalPTRRecord(queryName dns.Name, name dnsname.FQDN, builder *dns.Builder) error {
var answer dns.PTRResource
var err error
answerHeader := dns.ResourceHeader{
Name: queryName,
Type: dns.TypePTR,
Class: dns.ClassINET,
TTL: uint32(defaultTTL / time.Second),
}
answer.PTR, err = dns.NewName(name.WithTrailingDot())
if err != nil {
return err
}
return builder.PTRResource(answerHeader, answer)
}
func marshalTXT(queryName dns.Name, txts []string, builder *dns.Builder) error {
for _, txt := range txts {
if err := builder.TXTResource(dns.ResourceHeader{
Name: queryName,
Type: dns.TypeTXT,
Class: dns.ClassINET,
TTL: uint32(defaultTTL / time.Second),
}, dns.TXTResource{
TXT: []string{txt},
}); err != nil {
return err
}
}
return nil
}
func marshalCNAME(queryName dns.Name, cname string, builder *dns.Builder) error {
if cname == "" {
return nil
}
name, err := dns.NewName(cname)
if err != nil {
return err
}
return builder.CNAMEResource(dns.ResourceHeader{
Name: queryName,
Type: dns.TypeCNAME,
Class: dns.ClassINET,
TTL: uint32(defaultTTL / time.Second),
}, dns.CNAMEResource{
CNAME: name,
})
}
func marshalNS(queryName dns.Name, nss []*net.NS, builder *dns.Builder) error {
for _, ns := range nss {
name, err := dns.NewName(ns.Host)
if err != nil {
return err
}
err = builder.NSResource(dns.ResourceHeader{
Name: queryName,
Type: dns.TypeNS,
Class: dns.ClassINET,
TTL: uint32(defaultTTL / time.Second),
}, dns.NSResource{NS: name})
if err != nil {
return err
}
}
return nil
}
func marshalSRV(queryName dns.Name, srvs []*net.SRV, builder *dns.Builder) error {
for _, s := range srvs {
srvName, err := dns.NewName(s.Target)
if err != nil {
return err
}
err = builder.SRVResource(dns.ResourceHeader{
Name: queryName,
Type: dns.TypeSRV,
Class: dns.ClassINET,
TTL: uint32(defaultTTL / time.Second),
}, dns.SRVResource{
Target: srvName,
Priority: s.Priority,
Port: s.Port,
Weight: s.Weight,
})
if err != nil {
return err
}
}
return nil
}
// marshalSOA serializes zone's SOA record into the authority section
// of an active builder, which must have had StartAuthorities called.
// Its only purpose is communicating negativeTTL to caching resolvers
// (RFC 2308), so all fields other than the TTLs are placeholders.
func marshalSOA(zone dnsname.FQDN, builder *dns.Builder) error {
name, err := dns.NewName(zone.WithTrailingDot())
if err != nil {
return err
}
return builder.SOAResource(dns.ResourceHeader{
Name: name,
Type: dns.TypeSOA,
Class: dns.ClassINET,
TTL: uint32(negativeTTL / time.Second),
}, dns.SOAResource{
NS: name,
MBox: name,
// A serial should only change when the zone data does,
// but nothing consumes ours (no secondaries, no zone
// transfers), and the current time is at least monotonic
// and cheap. Unix seconds fit in the uint32 until 2106.
Serial: uint32(timeNow().Unix()),
Refresh: uint32(negativeTTL / time.Second),
Retry: uint32(negativeTTL / time.Second),
Expire: uint32(negativeTTL / time.Second),
MinTTL: uint32(negativeTTL / time.Second),
})
}
// marshalResponse serializes the DNS response into a new buffer.
func marshalResponse(resp *response) ([]byte, error) {
resp.Header.Response = true
resp.Header.Authoritative = true
if resp.Header.RecursionDesired {
resp.Header.RecursionAvailable = true
}
builder := dns.NewBuilder(nil, resp.Header)
// TODO(bradfitz): I'm not sure why this wasn't enabled
// before, but for now (2021-12-09) enable it at least when
// there's more than 1 record (which was never the case
// before), where it really helps.
if len(resp.IPs) > 1 {
builder.EnableCompression()
}
isSuccess := resp.Header.RCode == dns.RCodeSuccess
if resp.Question.Type != 0 || isSuccess {
err := builder.StartQuestions()
if err != nil {
return nil, err
}
err = builder.Question(resp.Question)
if err != nil {
return nil, err
}
}
// Only successful responses contain answers.
if !isSuccess {
if resp.SOAZone != "" {
if err := builder.StartAuthorities(); err != nil {
return nil, err
}
if err := marshalSOA(resp.SOAZone, &builder); err != nil {
return nil, err
}
}
return builder.Finish()
}
err := builder.StartAnswers()
if err != nil {
return nil, err
}
switch resp.Question.Type {
case dns.TypeA, dns.TypeAAAA, dns.TypeALL:
if err := marshalIP(resp.Question.Name, resp.IP, &builder); err != nil {
return nil, err
}
for _, ip := range resp.IPs {
if err := marshalIP(resp.Question.Name, ip, &builder); err != nil {
return nil, err
}
}
case dns.TypePTR:
err = marshalPTRRecord(resp.Question.Name, resp.Name, &builder)
case dns.TypeTXT:
err = marshalTXT(resp.Question.Name, resp.TXT, &builder)
case dns.TypeCNAME:
err = marshalCNAME(resp.Question.Name, resp.CNAME, &builder)
case dns.TypeSRV:
err = marshalSRV(resp.Question.Name, resp.SRVs, &builder)
case dns.TypeNS:
err = marshalNS(resp.Question.Name, resp.NSs, &builder)
}
if err != nil {
return nil, err
}
if resp.SOAZone != "" {
if err := builder.StartAuthorities(); err != nil {
return nil, err
}
if err := marshalSOA(resp.SOAZone, &builder); err != nil {
return nil, err
}
}
return builder.Finish()
}
const (
rdnsv4Suffix = ".in-addr.arpa."
rdnsv6Suffix = ".ip6.arpa."
)
// hasRDNSBonjourPrefix reports whether name has a Bonjour Service Prefix..
//
// https://tools.ietf.org/html/rfc6763 lists
// "five special RR names" for Bonjour service discovery:
//
// b._dns-sd._udp.<domain>.
// db._dns-sd._udp.<domain>.
// r._dns-sd._udp.<domain>.
// dr._dns-sd._udp.<domain>.
// lb._dns-sd._udp.<domain>.
func hasRDNSBonjourPrefix(name dnsname.FQDN) bool {
s := name.WithTrailingDot()
base, rest, ok := strings.Cut(s, ".")
if !ok {
return false // shouldn't happen
}
switch base {
case "b", "db", "r", "dr", "lb":
default:
return false
}
return strings.HasPrefix(rest, "_dns-sd._udp.")
}
// rawNameToLower converts a raw DNS name to a string, lowercasing it.
func rawNameToLower(name []byte) string {
var sb strings.Builder
sb.Grow(len(name))
for _, b := range name {
if 'A' <= b && b <= 'Z' {
b = b - 'A' + 'a'
}
sb.WriteByte(b)
}
return sb.String()
}
// ptrNameToIPv4 transforms a PTR name representing an IPv4 address to said address.
// Such names are IPv4 labels in reverse order followed by .in-addr.arpa.
// For example,
//
// 4.3.2.1.in-addr.arpa
//
// is transformed to
//
// 1.2.3.4
func rdnsNameToIPv4(name dnsname.FQDN) (ip netip.Addr, ok bool) {
s := strings.TrimSuffix(name.WithTrailingDot(), rdnsv4Suffix)
ip, err := netip.ParseAddr(s)
if err != nil {
return netip.Addr{}, false
}
if !ip.Is4() {
return netip.Addr{}, false
}
b := ip.As4()
return netaddr.IPv4(b[3], b[2], b[1], b[0]), true
}
// ptrNameToIPv6 transforms a PTR name representing an IPv6 address to said address.
// Such names are dot-separated nibbles in reverse order followed by .ip6.arpa.
// For example,
//
// b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.
//
// is transformed to
//
// 2001:db8::567:89ab
func rdnsNameToIPv6(name dnsname.FQDN) (ip netip.Addr, ok bool) {
var b [32]byte
var ipb [16]byte
s := strings.TrimSuffix(name.WithTrailingDot(), rdnsv6Suffix)
// 32 nibbles and 31 dots between them.
if len(s) != 63 {
return netip.Addr{}, false
}
// Dots and hex digits alternate.
prevDot := true
// i ranges over name backward; j ranges over b forward.
for i, j := len(s)-1, 0; i >= 0; i-- {
thisDot := (s[i] == '.')
if prevDot == thisDot {
return netip.Addr{}, false
}
prevDot = thisDot
if !thisDot {
// This is safe assuming alternation.
// We do not check that non-dots are hex digits: hex.Decode below will do that.
b[j] = s[i]
j++
}
}
_, err := hex.Decode(ipb[:], b[:])
if err != nil {
return netip.Addr{}, false
}
return netip.AddrFrom16(ipb), true
}
// respondReverse returns a DNS response to a PTR query.
// It is assumed that resp.Question is populated by respond before this is called.
// authoritativeZoneFor returns the zone from the configured local
// domains that contains name, or "" if none does.
func (r *Resolver) authoritativeZoneFor(name dnsname.FQDN) dnsname.FQDN {
r.mu.Lock()
defer r.mu.Unlock()
for _, suffix := range r.localDomains {
if suffix.Contains(name) {
return suffix
}
}
return ""
}
func (r *Resolver) respondReverse(query []byte, name dnsname.FQDN, resp *response) ([]byte, error) {
if hasRDNSBonjourPrefix(name) {
metricDNSReverseMissBonjour.Add(1)
return nil, errNotOurName
}
resp.Name, resp.Header.RCode = r.resolveLocalReverse(name)
if resp.Header.RCode == dns.RCodeRefused {
metricDNSReverseMissOther.Add(1)
return nil, errNotOurName
}
if resp.Header.RCode == dns.RCodeNameError {
resp.SOAZone = r.authoritativeZoneFor(name)
}
metricDNSMagicDNSSuccessReverse.Add(1)
return marshalResponse(resp)
}
// respond returns a DNS response to query if it can be resolved locally.
// Otherwise, it returns errNotOurName.
func (r *Resolver) respond(query []byte) ([]byte, error) {
if !buildfeatures.HasDNS {
return nil, feature.ErrUnavailable
}
parser := dnsParserPool.Get().(*dnsParser)
defer dnsParserPool.Put(parser)
// ParseQuery is sufficiently fast to run on every DNS packet.
// This is considerably simpler than extracting the name by hand
// to shave off microseconds in case of delegation.
err := parser.parseQuery(query)
// We will not return this error: it is the sender's fault.
if err != nil {
if errors.Is(err, dns.ErrSectionDone) {
metricDNSErrorParseNoQ.Add(1)
r.logf("parseQuery(%02x): no DNS questions", query)
} else {
metricDNSErrorParseQuery.Add(1)
r.logf("parseQuery(%02x): %v", query, err)
}
resp := parser.response()
resp.Header.RCode = dns.RCodeFormatError
return marshalResponse(resp)
}
rawName := parser.Question.Name.Data[:parser.Question.Name.Length]
name, err := dnsname.ToFQDN(rawNameToLower(rawName))
if err != nil {
metricDNSErrorNotFQDN.Add(1)
// DNS packet unexpectedly contains an invalid FQDN.
resp := parser.response()
resp.Header.RCode = dns.RCodeFormatError
return marshalResponse(resp)
}
// Always try to handle reverse lookups; delegate inside when not found.
// This way, queries for existent nodes do not leak,
// but we behave gracefully if non-Tailscale nodes exist in CGNATRange.
if parser.Question.Type == dns.TypePTR {
return r.respondReverse(query, name, parser.response())
}
ip, rcode := r.resolveLocal(name, parser.Question.Type)
if rcode == dns.RCodeRefused {
return nil, errNotOurName // sentinel error return value: it requests forwarding
}
resp := parser.response()
resp.Header.RCode = rcode
resp.IP = ip
switch {
case rcode == dns.RCodeNameError:
resp.SOAZone = r.authoritativeZoneFor(name)
case rcode == dns.RCodeSuccess && !ip.IsValid():
switch parser.Question.Type {
case dns.TypeA, dns.TypeAAAA, dns.TypeALL:
// The name exists but has no records of the queried
// type (e.g. an AAAA query for an IPv4-only node).
resp.SOAZone = r.authoritativeZoneFor(name)
}
}
metricDNSMagicDNSSuccessName.Add(1)
return marshalResponse(resp)
}
// unARPA maps from "4.4.8.8.in-addr.arpa." to "8.8.4.4", etc.
func unARPA(a string) (ipStr string, ok bool) {
const suf4 = ".in-addr.arpa."
if s, ok := strings.CutSuffix(a, suf4); ok {
// Parse and reverse octets.
ip, err := netip.ParseAddr(s)
if err != nil || !ip.Is4() {
return "", false
}
a4 := ip.As4()
return netaddr.IPv4(a4[3], a4[2], a4[1], a4[0]).String(), true
}
const suf6 = ".ip6.arpa."
if len(a) == len("e.0.0.2.0.0.0.0.0.0.0.0.0.0.0.0.b.0.8.0.a.0.0.4.0.b.8.f.7.0.6.2.ip6.arpa.") &&
strings.HasSuffix(a, suf6) {
var hx [32]byte
var a16 [16]byte
for i := range hx {
hx[31-i] = a[i*2]
if a[i*2+1] != '.' {
return "", false
}
}
hex.Decode(a16[:], hx[:])
return netip.AddrFrom16(a16).Unmap().String(), true
}
return "", false
}
var (
metricDNSQueryLocal = clientmetric.NewCounter("dns_query_local")
metricDNSQueryErrorClosed = clientmetric.NewCounter("dns_query_local_error_closed")
metricDNSErrorParseNoQ = clientmetric.NewCounter("dns_query_respond_error_no_question")
metricDNSErrorParseQuery = clientmetric.NewCounter("dns_query_respond_error_parse")
metricDNSErrorNotFQDN = clientmetric.NewCounter("dns_query_respond_error_not_fqdn")
metricDNSMagicDNSSuccessName = clientmetric.NewCounter("dns_query_magic_success_name")
metricDNSMagicDNSSuccessReverse = clientmetric.NewCounter("dns_query_magic_success_reverse")
metricDNSExitProxyQuery = clientmetric.NewCounter("dns_exit_node_query")
metricDNSExitProxyErrorName = clientmetric.NewCounter("dns_exit_node_error_name")
metricDNSExitProxyErrorForward = clientmetric.NewCounter("dns_exit_node_error_forward")
metricDNSExitProxyErrorResolvConf = clientmetric.NewCounter("dns_exit_node_error_resolvconf")
metricDNSFwd = clientmetric.NewCounter("dns_query_fwd")
metricDNSFwdDropBonjour = clientmetric.NewCounter("dns_query_fwd_drop_bonjour")
metricDNSFwdErrorName = clientmetric.NewCounter("dns_query_fwd_error_name")
metricDNSFwdErrorNoUpstream = clientmetric.NewCounter("dns_query_fwd_error_no_upstream")
metricDNSFwdSuccess = clientmetric.NewCounter("dns_query_fwd_success")
metricDNSFwdErrorContext = clientmetric.NewCounter("dns_query_fwd_error_context")
metricDNSFwdErrorContextGotError = clientmetric.NewCounter("dns_query_fwd_error_context_got_error")
metricDNSFwdErrorType = clientmetric.NewCounter("dns_query_fwd_error_type")
metricDNSFwdTruncated = clientmetric.NewCounter("dns_query_fwd_truncated")
metricDNSFwdUDP = clientmetric.NewCounter("dns_query_fwd_udp") // on entry
metricDNSFwdUDPWrote = clientmetric.NewCounter("dns_query_fwd_udp_wrote") // sent UDP packet
metricDNSFwdUDPErrorWrite = clientmetric.NewCounter("dns_query_fwd_udp_error_write")
metricDNSFwdUDPErrorServer = clientmetric.NewCounter("dns_query_fwd_udp_error_server")
metricDNSFwdUDPErrorRefused = clientmetric.NewCounter("dns_query_fwd_udp_error_refused")
metricDNSFwdUDPErrorTxID = clientmetric.NewCounter("dns_query_fwd_udp_error_txid")
metricDNSFwdUDPErrorRead = clientmetric.NewCounter("dns_query_fwd_udp_error_read")
metricDNSFwdUDPSuccess = clientmetric.NewCounter("dns_query_fwd_udp_success")
metricDNSFwdTCP = clientmetric.NewCounter("dns_query_fwd_tcp") // on entry
metricDNSFwdTCPWrote = clientmetric.NewCounter("dns_query_fwd_tcp_wrote") // sent TCP packet
metricDNSFwdTCPErrorWrite = clientmetric.NewCounter("dns_query_fwd_tcp_error_write")
metricDNSFwdTCPErrorServer = clientmetric.NewCounter("dns_query_fwd_tcp_error_server")
metricDNSFwdTCPErrorRefused = clientmetric.NewCounter("dns_query_fwd_tcp_error_refused")
metricDNSFwdTCPErrorTxID = clientmetric.NewCounter("dns_query_fwd_tcp_error_txid")
metricDNSFwdTCPErrorRead = clientmetric.NewCounter("dns_query_fwd_tcp_error_read")
metricDNSFwdTCPSuccess = clientmetric.NewCounter("dns_query_fwd_tcp_success")
metricDNSFwdDoH = clientmetric.NewCounter("dns_query_fwd_doh")
metricDNSFwdDoHErrorStatus = clientmetric.NewCounter("dns_query_fwd_doh_error_status")
metricDNSFwdDoHErrorCT = clientmetric.NewCounter("dns_query_fwd_doh_error_content_type")
metricDNSFwdDoHErrorTransport = clientmetric.NewCounter("dns_query_fwd_doh_error_transport")
metricDNSFwdDoHErrorBody = clientmetric.NewCounter("dns_query_fwd_doh_error_body")
metricDNSResolveLocal = clientmetric.NewCounter("dns_resolve_local")
metricDNSResolveLocalErrorOnion = clientmetric.NewCounter("dns_resolve_local_error_onion")
metricDNSResolveLocalErrorMissing = clientmetric.NewCounter("dns_resolve_local_error_missing")
metricDNSResolveLocalErrorRefused = clientmetric.NewCounter("dns_resolve_local_error_refused")
metricDNSResolveLocalOKA = clientmetric.NewCounter("dns_resolve_local_ok_a")
metricDNSResolveLocalOKAAAA = clientmetric.NewCounter("dns_resolve_local_ok_aaaa")
metricDNSResolveLocalOKAll = clientmetric.NewCounter("dns_resolve_local_ok_all")
metricDNSResolveLocalNoA = clientmetric.NewCounter("dns_resolve_local_no_a")
metricDNSResolveLocalNoAAAA = clientmetric.NewCounter("dns_resolve_local_no_aaaa")
metricDNSResolveLocalNoAll = clientmetric.NewCounter("dns_resolve_local_no_all")
metricDNSResolveNotImplType = clientmetric.NewCounter("dns_resolve_local_not_impl_type")
metricDNSResolveNoRecordType = clientmetric.NewCounter("dns_resolve_local_no_record_type")
metricDNSReverseMissBonjour = clientmetric.NewCounter("dns_reverse_miss_bonjour")
metricDNSReverseMissOther = clientmetric.NewCounter("dns_reverse_miss_other")
)
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package dns
// This code is only used in Windows builds, but is in an
// OS-independent file so tests can run all the time.
import (
"bytes"
"encoding/binary"
"unicode/utf16"
)
// maybeUnUTF16 tries to detect whether bs contains UTF-16, and if so
// translates it to regular UTF-8.
//
// Some of wsl.exe's output get printed as UTF-16, which breaks a
// bunch of things. Try to detect this by looking for a zero byte in
// the first few bytes of output (which will appear if any of those
// codepoints are basic ASCII - very likely). From that we can infer
// that UTF-16 is being printed, and the byte order in use, and we
// decode that back to UTF-8.
//
// https://github.com/microsoft/WSL/issues/4607
func maybeUnUTF16(bs []byte) []byte {
if len(bs)%2 != 0 {
// Can't be complete UTF-16.
return bs
}
checkLen := min(len(bs), 20)
zeroOff := bytes.IndexByte(bs[:checkLen], 0)
if zeroOff == -1 {
return bs
}
// We assume wsl.exe is trying to print an ASCII codepoint,
// meaning the zero byte is in the upper 8 bits of the
// codepoint. That means we can use the zero's byte offset to
// work out if we're seeing little-endian or big-endian
// UTF-16.
var endian binary.ByteOrder = binary.LittleEndian
if zeroOff%2 == 0 {
endian = binary.BigEndian
}
var u16 []uint16
for i := 0; i < len(bs); i += 2 {
u16 = append(u16, endian.Uint16(bs[i:]))
}
return []byte(string(utf16.Decode(u16)))
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package dnscache contains a minimal DNS cache that makes a bunch of
// assumptions that are only valid for us. Not recommended for general use.
package dnscache
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"log"
"net"
"net/netip"
"runtime"
"strconv"
"sync"
"sync/atomic"
"time"
"tailscale.com/envknob"
"tailscale.com/feature"
"tailscale.com/feature/buildfeatures"
"tailscale.com/net/bakedroots"
"tailscale.com/net/netx"
"tailscale.com/syncs"
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
"tailscale.com/util/cloudenv"
"tailscale.com/util/singleflight"
"tailscale.com/util/testenv"
)
// HookSetCacheDir optionally points to the dnsresolvecache feature's
// function to configure the directory in which resolved IPs are
// persisted to disk. It is called by tailscaled at startup, if the
// feature is linked in.
var HookSetCacheDir feature.Hook[func(dir string, logf logger.Logf)]
// HookPersistResolution optionally points to the dnsresolvecache
// feature's function to record a successful DNS resolution of host
// to disk. The resolver argument is one of the "forward", "cloud",
// or "fallback" resolver source names. The feature defers the actual
// disk write until [HookHostVerified] confirms one of the
// resolution's IPs.
var HookPersistResolution feature.Hook[func(host, resolver string, ips []netip.Addr)]
// HookLookupDiskCache optionally points to the dnsresolvecache
// feature's function to load previously persisted IPs for host from
// disk. It is consulted only after regular DNS resolution has
// failed, before falling back to the DERP-based bootstrap DNS.
var HookLookupDiskCache feature.Hook[func(host string) ([]netip.Addr, bool)]
// HookHostVerified optionally points to the dnsresolvecache
// feature's function to record that a TLS connection to host, at the
// given remote IP, presented a certificate chain that is valid for
// host. That is checked independently of the connection's own TLS
// config, which may deliberately tolerate interception (as the
// control plane Noise connection does). The feature uses this to
// flush a pending resolution of host to disk only once one of its
// IPs has been cryptographically verified to be host.
var HookHostVerified feature.Hook[func(host string, ip netip.Addr)]
// Resolver source names, as passed to [HookPersistResolution] and
// used for deciding fallback behavior in Resolver.lookupIP.
const (
srcForward = "forward" // Resolver.Forward (or the test hook)
srcCloud = "cloud" // cloud host resolver (e.g. GCP metadata resolver)
srcDisk = "disk" // dnsresolvecache disk cache of an earlier resolution
srcFallback = "fallback" // Resolver.LookupIPFallback (DERP-based bootstrap DNS)
)
var (
metricDiskFallbackHit = clientmetric.NewCounter("dnscache_disk_fallback_hit")
metricDiskFallbackMiss = clientmetric.NewCounter("dnscache_disk_fallback_miss")
metricDERPFallbackOK = clientmetric.NewCounter("dnscache_derp_fallback_ok")
metricDERPFallbackDialOK = clientmetric.NewCounter("dnscache_derp_fallback_dial_ok")
)
var zaddr netip.Addr
var single = &Resolver{
Forward: &net.Resolver{PreferGo: preferGoResolver()},
}
func preferGoResolver() bool {
// There does not appear to be a local resolver running
// on iOS, and NetworkExtension is good at isolating DNS.
// So do not use the Go resolver on macOS/iOS.
if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
return false
}
// The local resolver is not available on Android.
if runtime.GOOS == "android" {
return false
}
// Otherwise, the Go resolver is fine and slightly preferred
// since it's lighter, not using cgo calls & threads.
return true
}
// Get returns a caching Resolver singleton.
func Get() *Resolver { return single }
// Resolver is a minimal DNS caching resolver.
//
// The TTL is always fixed for now. It's not intended for general use.
// Cache entries are never cleaned up so it's intended that this is
// only used with a fixed set of hostnames.
type Resolver struct {
// Forward is the resolver to use to populate the cache.
// If nil, net.DefaultResolver is used.
Forward *net.Resolver
// LookupIPForTest, if non-nil and in tests, handles requests instead
// of the usual mechanisms.
LookupIPForTest func(ctx context.Context, host string) ([]netip.Addr, error)
// LookupIPFallback optionally provides a backup DNS mechanism
// to use if Forward returns an error or no results.
LookupIPFallback func(ctx context.Context, host string) ([]netip.Addr, error)
// TTL is how long to keep entries cached
//
// If zero, a default (currently 10 minutes) is used.
TTL time.Duration
// UseLastGood controls whether a cached entry older than TTL is used
// if a refresh fails.
UseLastGood bool
// SingleHostStaticResult, if non-nil, is the static result of IPs that is returned
// by Resolver.LookupIP for any hostname. When non-nil, SingleHost must also be
// set with the expected name.
SingleHostStaticResult []netip.Addr
// SingleHost is the hostname that SingleHostStaticResult is for.
// It is required when SingleHostStaticResult is present.
SingleHost string
// Logf optionally provides a log function to use for debug logs. If
// not present, log.Printf will be used. The prefix "dnscache: " will
// be added to all log messages printed with this logger.
Logf logger.Logf
sf singleflight.Group[string, ipRes]
mu syncs.Mutex
ipCache map[string]ipCacheEntry
}
// ipRes is the type used by the Resolver.sf singleflight group.
type ipRes struct {
ip, ip6 netip.Addr
allIPs []netip.Addr
}
type ipCacheEntry struct {
ip netip.Addr // either v4 or v6
ip6 netip.Addr // nil if no v4 or no v6
allIPs []netip.Addr // 1+ v4 and/or v6
expires time.Time
}
func (r *Resolver) fwd() *net.Resolver {
if r.Forward != nil {
return r.Forward
}
return net.DefaultResolver
}
// dlogf logs a debug message if debug logging is enabled either globally via
// the TS_DEBUG_DNS_CACHE environment variable or via the per-Resolver
// configuration.
func (r *Resolver) dlogf(format string, args ...any) {
logf := r.Logf
if logf == nil {
logf = log.Printf
}
if debug() || debugLogging.Load() {
logf("dnscache: "+format, args...)
}
}
// cloudHostResolver returns a Resolver for the current cloud hosting environment.
// It currently only supports Google Cloud.
func (r *Resolver) cloudHostResolver() (v *net.Resolver, ok bool) {
switch runtime.GOOS {
case "android", "ios", "darwin":
return nil, false
}
ip := cloudenv.Get().ResolverIP()
if ip == "" {
return nil, false
}
return &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
var d net.Dialer
return d.DialContext(ctx, network, net.JoinHostPort(ip, "53"))
},
}, true
}
func (r *Resolver) ttl() time.Duration {
if r.TTL > 0 {
return r.TTL
}
return 10 * time.Minute
}
var debug = envknob.RegisterBool("TS_DEBUG_DNS_CACHE")
// debugLogging allows enabling debug logging at runtime, via
// SetDebugLoggingEnabled.
//
// This is a global variable instead of a per-Resolver variable because we
// create new Resolvers throughout the lifetime of the program (e.g. on every
// new Direct client, etc.). When we enable debug logs, though, we want to do
// so for every single created Resolver; we'd need to plumb a bunch of new code
// through all of the intermediate packages to accomplish the same behaviour as
// just using a global variable.
var debugLogging atomic.Bool
// SetDebugLoggingEnabled controls whether debug logging is enabled for this
// package.
//
// These logs are also printed when the TS_DEBUG_DNS_CACHE envknob is set, but
// we allow configuring this manually as well so that it can be changed at
// runtime.
func SetDebugLoggingEnabled(v bool) {
debugLogging.Store(v)
}
// LookupIP returns the host's primary IP address (either IPv4 or
// IPv6, but preferring IPv4) and optionally its IPv6 address, if
// there is both IPv4 and IPv6.
//
// If err is nil, ip will be non-nil. The v6 address may be nil even
// with a nil error.
func (r *Resolver) LookupIP(ctx context.Context, host string) (ip, v6 netip.Addr, allIPs []netip.Addr, err error) {
if r.SingleHostStaticResult != nil {
if r.SingleHost != host {
return zaddr, zaddr, nil, fmt.Errorf("dnscache: unexpected hostname %q doesn't match expected %q", host, r.SingleHost)
}
for _, naIP := range r.SingleHostStaticResult {
if !ip.IsValid() && naIP.Is4() {
ip = naIP
}
if !v6.IsValid() && naIP.Is6() {
v6 = naIP
}
allIPs = append(allIPs, naIP)
}
if !ip.IsValid() && v6.IsValid() {
ip = v6
}
r.dlogf("returning %d static results", len(allIPs))
return
}
if ip, err := netip.ParseAddr(host); err == nil {
ip = ip.Unmap()
r.dlogf("%q is an IP", host)
return ip, zaddr, []netip.Addr{ip}, nil
}
if ip, ip6, allIPs, ok := r.lookupIPCache(host); ok {
r.dlogf("%q = %v (cached)", host, ip)
return ip, ip6, allIPs, nil
}
ch := r.sf.DoChanContext(ctx, host, func(ctx context.Context) (ret ipRes, _ error) {
ip, ip6, allIPs, err := r.lookupIP(ctx, host)
if err != nil {
return ret, err
}
return ipRes{ip, ip6, allIPs}, nil
})
select {
case res := <-ch:
if res.Err != nil {
if r.UseLastGood {
if ip, ip6, allIPs, ok := r.lookupIPCacheExpired(host); ok {
r.dlogf("%q using %v after error", host, ip)
return ip, ip6, allIPs, nil
}
}
r.dlogf("error resolving %q: %v", host, res.Err)
return zaddr, zaddr, nil, res.Err
}
r := res.Val
return r.ip, r.ip6, r.allIPs, nil
case <-ctx.Done():
r.dlogf("context done while resolving %q: %v", host, ctx.Err())
return zaddr, zaddr, nil, ctx.Err()
}
}
func (r *Resolver) lookupIPCache(host string) (ip, ip6 netip.Addr, allIPs []netip.Addr, ok bool) {
r.mu.Lock()
defer r.mu.Unlock()
if ent, ok := r.ipCache[host]; ok && ent.expires.After(time.Now()) {
return ent.ip, ent.ip6, ent.allIPs, true
}
return zaddr, zaddr, nil, false
}
func (r *Resolver) lookupIPCacheExpired(host string) (ip, ip6 netip.Addr, allIPs []netip.Addr, ok bool) {
r.mu.Lock()
defer r.mu.Unlock()
if ent, ok := r.ipCache[host]; ok {
return ent.ip, ent.ip6, ent.allIPs, true
}
return zaddr, zaddr, nil, false
}
func (r *Resolver) lookupTimeoutForHost(host string) time.Duration {
if r.UseLastGood {
if _, _, _, ok := r.lookupIPCacheExpired(host); ok {
// If we have some previous good value for this host,
// don't give this DNS lookup much time. If we're in a
// situation where the user's DNS server is unreachable
// (e.g. their corp DNS server is behind a subnet router
// that can't come up due to Tailscale needing to
// connect to itself), then we want to fail fast and let
// our caller (who set UseLastGood) fall back to using
// the last-known-good IP address.
return 3 * time.Second
}
}
return 10 * time.Second
}
func (r *Resolver) lookupIP(ctx context.Context, host string) (ip, ip6 netip.Addr, allIPs []netip.Addr, err error) {
if ip, ip6, allIPs, ok := r.lookupIPCache(host); ok {
r.dlogf("%q found in cache as %v", host, ip)
return ip, ip6, allIPs, nil
}
lookupCtx, lookupCancel := context.WithTimeout(ctx, r.lookupTimeoutForHost(host))
defer lookupCancel()
var ips []netip.Addr
src := srcForward
if r.LookupIPForTest != nil && testenv.InTest() {
ips, err = r.LookupIPForTest(ctx, host)
} else {
ips, err = r.fwd().LookupNetIP(lookupCtx, "ip", host)
if err != nil || len(ips) == 0 {
if resolver, ok := r.cloudHostResolver(); ok {
r.dlogf("resolving %q via cloud resolver", host)
src = srcCloud
ips, err = resolver.LookupNetIP(lookupCtx, "ip", host)
}
}
}
if buildfeatures.HasDNSResolveCache && (err != nil || len(ips) == 0) {
if f, ok := HookLookupDiskCache.GetOk(); ok {
if cached, ok := f(host); ok {
r.dlogf("resolving %q from disk cache after error", host)
metricDiskFallbackHit.Add(1)
ips, err, src = cached, nil, srcDisk
} else {
metricDiskFallbackMiss.Add(1)
}
}
}
if (err != nil || len(ips) == 0) && r.LookupIPFallback != nil {
lookupCtx, lookupCancel := context.WithTimeout(ctx, 30*time.Second)
defer lookupCancel()
if err != nil {
r.dlogf("resolving %q using fallback resolver due to error", host)
} else {
r.dlogf("resolving %q using fallback resolver due to no returned IPs", host)
}
src = srcFallback
ips, err = r.LookupIPFallback(lookupCtx, host)
if err == nil && len(ips) > 0 {
metricDERPFallbackOK.Add(1)
}
}
if err != nil {
return netip.Addr{}, netip.Addr{}, nil, err
}
if len(ips) == 0 {
return netip.Addr{}, netip.Addr{}, nil, fmt.Errorf("no IPs for %q found", host)
}
// Unmap everything; LookupNetIP can return mapped addresses (see #5698)
for i := range ips {
ips[i] = ips[i].Unmap()
}
have4 := false
for _, ipa := range ips {
if ipa.Is4() {
if !have4 {
ip6 = ip
ip = ipa
have4 = true
}
} else {
if have4 {
ip6 = ipa
} else {
ip = ipa
}
}
}
r.addIPCache(host, src, ip, ip6, ips, r.ttl())
return ip, ip6, ips, nil
}
func (r *Resolver) addIPCache(host, src string, ip, ip6 netip.Addr, allIPs []netip.Addr, d time.Duration) {
if ip.IsPrivate() {
// Don't cache obviously wrong entries from captive portals.
// TODO: use DoH or DoT for the forwarding resolver?
r.dlogf("%q resolved to private IP %v; using but not caching", host, ip)
return
}
// Skip persisting disk-sourced results to avoid rewriting the
// disk cache files with their own contents.
if buildfeatures.HasDNSResolveCache && src != srcDisk {
if f, ok := HookPersistResolution.GetOk(); ok {
f(host, src, allIPs)
}
}
r.dlogf("%q resolved to IP %v; caching", host, ip)
r.mu.Lock()
defer r.mu.Unlock()
if r.ipCache == nil {
r.ipCache = make(map[string]ipCacheEntry)
}
r.ipCache[host] = ipCacheEntry{
ip: ip,
ip6: ip6,
allIPs: allIPs,
expires: time.Now().Add(d),
}
}
// Dialer returns a wrapped DialContext func that uses the provided dnsCache.
func Dialer(fwd netx.DialFunc, dnsCache *Resolver) netx.DialFunc {
d := &dialer{
fwd: fwd,
dnsCache: dnsCache,
pastConnect: map[netip.Addr]time.Time{},
}
return d.DialContext
}
// dialer is the config and accumulated state for a dial func returned by Dialer.
type dialer struct {
fwd netx.DialFunc
dnsCache *Resolver
mu sync.Mutex
pastConnect map[netip.Addr]time.Time
}
func (d *dialer) DialContext(ctx context.Context, network, address string) (retConn net.Conn, ret error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
// Bogus. But just let the real dialer return an error rather than
// inventing a similar one.
return d.fwd(ctx, network, address)
}
dc := &dialCall{
d: d,
network: network,
address: address,
host: host,
port: port,
}
defer func() {
// On failure, consider that our DNS might be wrong and ask the DNS fallback mechanism for
// some other IPs to try.
if !d.shouldTryBootstrap(ctx, ret, dc) {
return
}
ips, err := d.dnsCache.LookupIPFallback(ctx, host)
if err != nil {
// Return with original error
return
}
if c, err := dc.raceDial(ctx, ips); err == nil {
metricDERPFallbackDialOK.Add(1)
retConn = c
ret = nil
return
}
}()
ip, _, allIPs, err := d.dnsCache.LookupIP(ctx, host)
if err != nil {
return nil, fmt.Errorf("failed to resolve %q: %w", host, err)
}
// If we only have one candidate, just dial that, no matter what the
// address family is.
if len(allIPs) == 1 {
d.dnsCache.dlogf("dialing %s, %s for %s", network, ip, address)
return dc.dialOne(ctx, ip.Unmap())
}
// If we have multiple candidates, across address families, use happy
// eyeballs to find a connection.
return dc.raceDial(ctx, allIPs)
}
func (d *dialer) shouldTryBootstrap(ctx context.Context, err error, dc *dialCall) bool {
// No need to do anything when we succeeded.
if err == nil {
return false
}
// Can't try bootstrap DNS if we don't have a fallback function
if d.dnsCache.LookupIPFallback == nil {
d.dnsCache.dlogf("not using bootstrap DNS: no fallback")
return false
}
// We can't retry if the context is canceled, since any further
// operations with this context will fail.
if ctxErr := ctx.Err(); ctxErr != nil {
d.dnsCache.dlogf("not using bootstrap DNS: context error: %v", ctxErr)
return false
}
wasTrustworthy := dc.dnsWasTrustworthy()
if wasTrustworthy {
d.dnsCache.dlogf("not using bootstrap DNS: DNS was trustworthy")
return false
}
return true
}
// dialCall is the state around a single call to dial.
type dialCall struct {
d *dialer
network, address, host, port string
mu syncs.Mutex // lock ordering: dialer.mu, then dialCall.mu
fails map[netip.Addr]error // set of IPs that failed to dial thus far
}
// dnsWasTrustworthy reports whether we think the IP address(es) we
// tried (and failed) to dial were probably the correct IPs. Currently
// the heuristic is whether they ever worked previously.
func (dc *dialCall) dnsWasTrustworthy() bool {
dc.d.mu.Lock()
defer dc.d.mu.Unlock()
dc.mu.Lock()
defer dc.mu.Unlock()
if len(dc.fails) == 0 {
// No information.
return false
}
// If any of the IPs we failed to dial worked previously in
// this dialer, assume the DNS is fine.
for ip := range dc.fails {
if _, ok := dc.d.pastConnect[ip]; ok {
return true
}
}
return false
}
func (dc *dialCall) dialOne(ctx context.Context, ip netip.Addr) (net.Conn, error) {
c, err := dc.d.fwd(ctx, dc.network, net.JoinHostPort(ip.String(), dc.port))
dc.noteDialResult(ip, err)
return c, err
}
// noteDialResult records that a dial to ip either succeeded or
// failed.
func (dc *dialCall) noteDialResult(ip netip.Addr, err error) {
if err == nil {
d := dc.d
d.mu.Lock()
defer d.mu.Unlock()
d.pastConnect[ip] = time.Now()
return
}
dc.mu.Lock()
defer dc.mu.Unlock()
if dc.fails == nil {
dc.fails = map[netip.Addr]error{}
}
dc.fails[ip] = err
}
// uniqueIPs returns a possibly-mutated subslice of ips, filtering out
// dups and ones that have already failed previously.
func (dc *dialCall) uniqueIPs(ips []netip.Addr) (ret []netip.Addr) {
dc.mu.Lock()
defer dc.mu.Unlock()
seen := map[netip.Addr]bool{}
ret = ips[:0]
for _, ip := range ips {
if seen[ip] {
continue
}
seen[ip] = true
if dc.fails[ip] != nil {
continue
}
ret = append(ret, ip)
}
return ret
}
// fallbackDelay is how long to wait between trying subsequent
// addresses when multiple options are available.
// 300ms is the same as Go's Happy Eyeballs fallbackDelay value.
const fallbackDelay = 300 * time.Millisecond
// raceDial tries to dial port on each ip in ips, starting a new race
// dial every fallbackDelay apart, returning whichever completes first.
func (dc *dialCall) raceDial(ctx context.Context, ips []netip.Addr) (net.Conn, error) {
// Remove IPs that we tried & failed to dial previously
// (such as when we're being called after a dnsfallback lookup and get
// the same results)
ips = dc.uniqueIPs(ips)
if len(ips) == 0 {
return nil, errors.New("no IPs")
}
port, err := strconv.ParseUint(dc.port, 10, 16)
if err != nil {
return nil, fmt.Errorf("invalid port %q: %w", dc.port, err)
}
addrs := make([]netip.AddrPort, len(ips))
for i, ip := range ips {
addrs[i] = netip.AddrPortFrom(ip, uint16(port))
}
return netx.RaceDial(ctx, addrs, func(ctx context.Context, network, address string) (net.Conn, error) {
c, err := dc.d.fwd(ctx, network, address)
ipp, _ := netip.ParseAddrPort(address)
dc.noteDialResult(ipp.Addr(), err)
return c, err
}, fallbackDelay)
}
// TLSDialer is like Dialer but returns a func suitable for using with net/http.Transport.DialTLSContext.
// It returns a *tls.Conn type on success.
// On TLS cert validation failure, it can invoke a backup DNS resolution strategy.
func TLSDialer(fwd netx.DialFunc, dnsCache *Resolver, tlsConfigBase *tls.Config) netx.DialFunc {
tcpDialer := Dialer(fwd, dnsCache)
return func(ctx context.Context, network, address string) (net.Conn, error) {
host, _, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
tcpConn, err := tcpDialer(ctx, network, address)
if err != nil {
return nil, err
}
cfg := cloneTLSConfig(tlsConfigBase)
if cfg.ServerName == "" {
cfg.ServerName = host
}
tlsConn := tls.Client(tcpConn, cfg)
handshakeCtx, handshakeTimeoutCancel := context.WithTimeout(ctx, 5*time.Second)
defer handshakeTimeoutCancel()
if err := tlsConn.HandshakeContext(handshakeCtx); err != nil {
tcpConn.Close()
// TODO: if err != errTLSHandshakeTimeout,
// assume it might be some captive portal or
// otherwise incorrect DNS and try the backup
// DNS mechanism.
return nil, err
}
// Tell the dnsresolvecache feature (if linked in) that the
// remote IP presented a valid certificate for host. The chain
// is checked here, independently of whatever verification the
// tls.Config did during the handshake, because some callers
// (notably controlhttp, which runs Noise atop whatever
// transport it gets) deliberately tolerate invalid TLS
// certificates; an intercepted connection must not mark the
// DNS resolution as verified.
if buildfeatures.HasDNSResolveCache {
if f, ok := HookHostVerified.GetOk(); ok && certValidForHost(tlsConn.ConnectionState(), host, cfg.RootCAs) {
if ap, err := netip.ParseAddrPort(tcpConn.RemoteAddr().String()); err == nil {
f(host, ap.Addr().Unmap())
}
}
}
return tlsConn, nil
}
}
// certValidForHost reports whether cs's peer certificate chain is
// valid for host, verifying against roots if non-nil (a caller's
// explicitly configured trust anchors), else against the system
// roots. In either case the baked-in Let's Encrypt roots are also
// tried, as net/tlsdial's Config does.
func certValidForHost(cs tls.ConnectionState, host string, roots *x509.CertPool) bool {
if len(cs.PeerCertificates) == 0 {
return false
}
opts := x509.VerifyOptions{
DNSName: host,
Roots: roots, // nil means system roots
Intermediates: x509.NewCertPool(),
}
for _, cert := range cs.PeerCertificates[1:] {
opts.Intermediates.AddCert(cert)
}
if _, err := cs.PeerCertificates[0].Verify(opts); err == nil {
return true
}
if buildfeatures.HasBakedRoots {
opts.Roots = bakedroots.Get()
if _, err := cs.PeerCertificates[0].Verify(opts); err == nil {
return true
}
}
return false
}
func cloneTLSConfig(cfg *tls.Config) *tls.Config {
if cfg == nil {
return &tls.Config{}
}
return cfg.Clone()
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package dnscache
import (
"cmp"
"encoding/binary"
"errors"
"fmt"
"io"
"sync"
"time"
"github.com/golang/groupcache/lru"
"golang.org/x/net/dns/dnsmessage"
)
// MessageCache is a cache that works at the DNS message layer,
// with its cache keyed on a DNS wire-level question, and capable
// of replying to DNS messages.
//
// Its zero value is ready for use with a default cache size.
// Use SetMaxCacheSize to specify the cache size.
//
// It's safe for concurrent use.
type MessageCache struct {
// Clock is a clock, for testing.
// If nil, time.Now is used.
Clock func() time.Time
mu sync.Mutex
cacheSizeSet int // 0 means default
cache lru.Cache // msgQ => *msgCacheValue
}
func (c *MessageCache) now() time.Time {
if c.Clock != nil {
return c.Clock()
}
return time.Now()
}
// SetMaxCacheSize sets the maximum number of DNS cache entries that
// can be stored.
func (c *MessageCache) SetMaxCacheSize(n int) {
c.mu.Lock()
defer c.mu.Unlock()
c.cacheSizeSet = n
c.pruneLocked()
}
// Flush clears the cache.
func (c *MessageCache) Flush() {
c.mu.Lock()
defer c.mu.Unlock()
c.cache.Clear()
}
// pruneLocked prunes down the cache size to the configured (or
// default) max size.
func (c *MessageCache) pruneLocked() {
max := cmp.Or(c.cacheSizeSet, 500)
for c.cache.Len() > max {
c.cache.RemoveOldest()
}
}
// msgQ is the MessageCache cache key.
//
// It's basically a golang.org/x/net/dns/dnsmessage#Question but the
// Class is omitted (we only cache ClassINET) and we store a Go string
// instead of a 256 byte dnsmessage.Name array.
type msgQ struct {
Name string
Type dnsmessage.Type // A, AAAA, MX, etc
}
// A *msgCacheValue is the cached value for a msgQ (question) key.
//
// Despite using pointers for storage and methods, the value is
// immutable once placed in the cache.
type msgCacheValue struct {
Expires time.Time
// Answers are the minimum data to reconstruct a DNS response
// message. TTLs are added later when converting to a
// dnsmessage.Resource.
Answers []msgResource
}
type msgResource struct {
Name string
Type dnsmessage.Type // dnsmessage.UnknownResource.Type
Data []byte // dnsmessage.UnknownResource.Data
}
// ErrCacheMiss is a sentinel error returned by MessageCache.ReplyFromCache
// when the request can not be satisfied from cache.
var ErrCacheMiss = errors.New("cache miss")
var parserPool = &sync.Pool{
New: func() any { return new(dnsmessage.Parser) },
}
// ReplyFromCache writes a DNS reply to w for the provided DNS query message,
// which must begin with the two ID bytes of a DNS message.
//
// If there's a cache miss, the message is invalid or unexpected,
// ErrCacheMiss is returned. On cache hit, either nil or an error from
// a w.Write call is returned.
func (c *MessageCache) ReplyFromCache(w io.Writer, dnsQueryMessage []byte) error {
cacheKey, txID, ok := getDNSQueryCacheKey(dnsQueryMessage)
if !ok {
return ErrCacheMiss
}
now := c.now()
c.mu.Lock()
cacheEntI, _ := c.cache.Get(cacheKey)
v, ok := cacheEntI.(*msgCacheValue)
if ok && now.After(v.Expires) {
c.cache.Remove(cacheKey)
ok = false
}
c.mu.Unlock()
if !ok {
return ErrCacheMiss
}
ttl := uint32(v.Expires.Sub(now).Seconds())
packedRes, err := packDNSResponse(cacheKey, txID, ttl, v.Answers)
if err != nil {
return ErrCacheMiss
}
_, err = w.Write(packedRes)
return err
}
var (
errNotCacheable = errors.New("question not cacheable")
)
// AddCacheEntry adds a cache entry to the cache.
// It returns an error if the entry could not be cached.
func (c *MessageCache) AddCacheEntry(qPacket, res []byte) error {
cacheKey, qID, ok := getDNSQueryCacheKey(qPacket)
if !ok {
return errNotCacheable
}
now := c.now()
v := &msgCacheValue{}
p := parserPool.Get().(*dnsmessage.Parser)
defer parserPool.Put(p)
resh, err := p.Start(res)
if err != nil {
return fmt.Errorf("reading header in response: %w", err)
}
if resh.ID != qID {
return fmt.Errorf("response ID doesn't match query ID")
}
q, err := p.Question()
if err != nil {
return fmt.Errorf("reading 1st question in response: %w", err)
}
if _, err := p.Question(); err != dnsmessage.ErrSectionDone {
if err == nil {
return errors.New("unexpected 2nd question in response")
}
return fmt.Errorf("after reading 1st question in response: %w", err)
}
if resName := asciiLowerName(q.Name).String(); resName != cacheKey.Name {
return fmt.Errorf("response question name %q != question name %q", resName, cacheKey.Name)
}
for {
rh, err := p.AnswerHeader()
if err == dnsmessage.ErrSectionDone {
break
}
if err != nil {
return fmt.Errorf("reading answer: %w", err)
}
res, err := p.UnknownResource()
if err != nil {
return fmt.Errorf("reading resource: %w", err)
}
if rh.Class != dnsmessage.ClassINET {
continue
}
// Set the cache entry's expiration to the soonest
// we've seen. (They should all be the same, though)
expires := now.Add(time.Duration(rh.TTL) * time.Second)
if v.Expires.IsZero() || expires.Before(v.Expires) {
v.Expires = expires
}
v.Answers = append(v.Answers, msgResource{
Name: rh.Name.String(),
Type: rh.Type,
Data: res.Data, // doesn't alias; a copy from dnsmessage.unpackUnknownResource
})
}
c.addCacheValue(cacheKey, v)
return nil
}
func (c *MessageCache) addCacheValue(cacheKey msgQ, v *msgCacheValue) {
c.mu.Lock()
defer c.mu.Unlock()
c.cache.Add(cacheKey, v)
c.pruneLocked()
}
func getDNSQueryCacheKey(msg []byte) (cacheKey msgQ, txID uint16, ok bool) {
p := parserPool.Get().(*dnsmessage.Parser)
defer parserPool.Put(p)
h, err := p.Start(msg)
const dnsHeaderSize = 12
if err != nil || h.OpCode != 0 || h.Response || h.Truncated ||
len(msg) < dnsHeaderSize { // p.Start checks this anyway, but to be explicit for slicing below
return cacheKey, 0, false
}
var (
numQ = binary.BigEndian.Uint16(msg[4:6])
numAns = binary.BigEndian.Uint16(msg[6:8])
numAuth = binary.BigEndian.Uint16(msg[8:10])
numAddn = binary.BigEndian.Uint16(msg[10:12])
)
_ = numAddn // ignore this for now; do client OSes send EDNS additional? assume so, ignore.
if !(numQ == 1 && numAns == 0 && numAuth == 0) {
// Something weird. We don't want to deal with it.
return cacheKey, 0, false
}
q, err := p.Question()
if err != nil {
// Already verified numQ == 1 so shouldn't happen, but:
return cacheKey, 0, false
}
if q.Class != dnsmessage.ClassINET {
// We only cache the Internet class.
return cacheKey, 0, false
}
return msgQ{Name: asciiLowerName(q.Name).String(), Type: q.Type}, h.ID, true
}
func asciiLowerName(n dnsmessage.Name) dnsmessage.Name {
nb := n.Data[:]
if int(n.Length) < len(n.Data) {
nb = nb[:n.Length]
}
for i, b := range nb {
if 'A' <= b && b <= 'Z' {
n.Data[i] += 0x20
}
}
return n
}
// packDNSResponse builds a DNS response for the given question and
// transaction ID. The response resource records will have the
// same provided TTL.
func packDNSResponse(q msgQ, txID uint16, ttl uint32, answers []msgResource) ([]byte, error) {
var baseMem []byte // TODO: guess a max size based on looping over answers?
b := dnsmessage.NewBuilder(baseMem, dnsmessage.Header{
ID: txID,
Response: true,
OpCode: 0,
Authoritative: false,
Truncated: false,
RCode: dnsmessage.RCodeSuccess,
})
name, err := dnsmessage.NewName(q.Name)
if err != nil {
return nil, err
}
if err := b.StartQuestions(); err != nil {
return nil, err
}
if err := b.Question(dnsmessage.Question{
Name: name,
Type: q.Type,
Class: dnsmessage.ClassINET,
}); err != nil {
return nil, err
}
if err := b.StartAnswers(); err != nil {
return nil, err
}
for _, r := range answers {
name, err := dnsmessage.NewName(r.Name)
if err != nil {
return nil, err
}
if err := b.UnknownResource(dnsmessage.ResourceHeader{
Name: name,
Type: r.Type,
Class: dnsmessage.ClassINET,
TTL: ttl,
}, dnsmessage.UnknownResource{
Type: r.Type,
Data: r.Data,
}); err != nil {
return nil, err
}
}
return b.Finish()
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package dnsfallback contains a DNS fallback mechanism
// for starting up Tailscale when the system DNS is broken or otherwise unavailable.
//
// The data is backed by a JSON file `dns-fallback-servers.json` that is updated
// by `update-dns-fallbacks.go`:
//
// (cd net/dnsfallback; go run update-dns-fallbacks.go)
package dnsfallback
import (
"context"
_ "embed"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"net/netip"
"net/url"
"os"
"reflect"
"sync/atomic"
"time"
"tailscale.com/atomicfile"
"tailscale.com/feature"
"tailscale.com/health"
"tailscale.com/net/netmon"
"tailscale.com/net/netns"
"tailscale.com/net/netutil"
"tailscale.com/net/tlsdial"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/util/slicesx"
)
// MakeLookupFunc creates a function that can be used to resolve hostnames
// (e.g. as a LookupIPFallback from dnscache.Resolver).
// The netMon parameter is optional; if non-nil it's used to do faster interface lookups.
func MakeLookupFunc(logf logger.Logf, netMon *netmon.Monitor) func(ctx context.Context, host string) ([]netip.Addr, error) {
fr := &fallbackResolver{
logf: logf,
netMon: netMon,
}
return fr.Lookup
}
// fallbackResolver contains the state and configuration for a DNS resolution
// function.
type fallbackResolver struct {
logf logger.Logf
netMon *netmon.Monitor // or nil
healthTracker *health.Tracker // or nil
// for tests
waitForCompare bool
}
func (fr *fallbackResolver) Lookup(ctx context.Context, host string) ([]netip.Addr, error) {
return lookup(ctx, host, fr.logf, fr.healthTracker, fr.netMon)
}
func lookup(ctx context.Context, host string, logf logger.Logf, ht *health.Tracker, netMon *netmon.Monitor) ([]netip.Addr, error) {
if ip, err := netip.ParseAddr(host); err == nil && ip.IsValid() {
return []netip.Addr{ip}, nil
}
type nameIP struct {
dnsName string
ip netip.Addr
}
dm := GetDERPMap()
var cands4, cands6 []nameIP
for _, dr := range dm.Regions {
for _, n := range dr.Nodes {
if ip, err := netip.ParseAddr(n.IPv4); err == nil {
cands4 = append(cands4, nameIP{n.HostName, ip})
}
if ip, err := netip.ParseAddr(n.IPv6); err == nil {
cands6 = append(cands6, nameIP{n.HostName, ip})
}
}
}
slicesx.Shuffle(cands4)
slicesx.Shuffle(cands6)
const maxCands = 6
var cands []nameIP // up to maxCands alternating v4/v6 as long as we have both
for (len(cands4) > 0 || len(cands6) > 0) && len(cands) < maxCands {
if len(cands4) > 0 {
cands = append(cands, cands4[0])
cands4 = cands4[1:]
}
if len(cands6) > 0 {
cands = append(cands, cands6[0])
cands6 = cands6[1:]
}
}
if len(cands) == 0 {
return nil, fmt.Errorf("no DNS fallback options for %q", host)
}
for _, cand := range cands {
if err := ctx.Err(); err != nil {
return nil, err
}
logf("trying bootstrapDNS(%q, %q) for %q ...", cand.dnsName, cand.ip, host)
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
dm, err := bootstrapDNSMap(ctx, cand.dnsName, cand.ip, host, logf, ht, netMon)
if err != nil {
logf("bootstrapDNS(%q, %q) for %q error: %v", cand.dnsName, cand.ip, host, err)
continue
}
if ips := dm[host]; len(ips) > 0 {
slicesx.Shuffle(ips)
logf("bootstrapDNS(%q, %q) for %q = %v", cand.dnsName, cand.ip, host, ips)
return ips, nil
}
}
if err := ctx.Err(); err != nil {
return nil, err
}
return nil, fmt.Errorf("no DNS fallback candidates remain for %q", host)
}
// serverName and serverIP of are, say, "derpN.tailscale.com".
// queryName is the name being sought (e.g. "controlplane.tailscale.com"), passed as hint.
//
// ht may be nil.
func bootstrapDNSMap(ctx context.Context, serverName string, serverIP netip.Addr, queryName string, logf logger.Logf, ht *health.Tracker, netMon *netmon.Monitor) (dnsMap, error) {
dialer := netns.NewDialer(logf, netMon)
tr := netutil.NewDefaultTransport()
tr.DisableKeepAlives = true // This transport is meant to be used once.
tr.Proxy = feature.HookProxyFromEnvironment.GetOrNil()
tr.DialContext = func(ctx context.Context, netw, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, "tcp", net.JoinHostPort(serverIP.String(), "443"))
}
tr.TLSClientConfig = tlsdial.Config(ht, tr.TLSClientConfig)
c := &http.Client{Transport: tr}
req, err := http.NewRequestWithContext(ctx, "GET", "https://"+serverName+"/bootstrap-dns?q="+url.QueryEscape(queryName), nil)
if err != nil {
return nil, err
}
dm := make(dnsMap)
res, err := c.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != 200 {
return nil, errors.New(res.Status)
}
if err := json.NewDecoder(res.Body).Decode(&dm); err != nil {
return nil, err
}
return dm, nil
}
// dnsMap is the JSON type returned by the DERP /bootstrap-dns handler:
// https://derp10.tailscale.com/bootstrap-dns
type dnsMap map[string][]netip.Addr
// GetDERPMap returns a fallback DERP map that is always available, useful for basic
// bootstrapping purposes. The dynamically updated DERP map in LocalBackend should
// always be preferred over this. Use this DERP map only when the control plane is
// unreachable or hasn't been reached yet. The DERP servers in the returned map also
// run a fallback DNS server.
func GetDERPMap() *tailcfg.DERPMap {
dm := getStaticDERPMap()
// Merge in any DERP servers from the cached map that aren't in the
// static map; this ensures that we're getting new region(s) while not
// overriding the built-in fallbacks if things go horribly wrong and we
// get a bad DERP map.
//
// TODO(andrew): should we expect OmitDefaultRegions here? We're not
// forwarding traffic, just resolving DNS, so maybe we can ignore that
// value anyway?
cached := cachedDERPMap.Load()
if cached == nil {
return dm
}
for id, region := range cached.Regions {
dr, ok := dm.Regions[id]
if !ok {
dm.Regions[id] = region
continue
}
// Add any nodes that we don't already have.
seen := make(map[string]bool)
for _, n := range dr.Nodes {
seen[n.HostName] = true
}
for _, n := range region.Nodes {
if !seen[n.HostName] {
dr.Nodes = append(dr.Nodes, n)
}
}
}
return dm
}
// getStaticDERPMap returns the DERP map that was compiled into this binary.
func getStaticDERPMap() *tailcfg.DERPMap {
dm := new(tailcfg.DERPMap)
if err := json.Unmarshal(staticDERPMapJSON, dm); err != nil {
panic(err)
}
return dm
}
//go:embed dns-fallback-servers.json
var staticDERPMapJSON []byte
// cachedDERPMap is the path to a cached DERP map that we loaded from our on-disk cache.
var cachedDERPMap atomic.Pointer[tailcfg.DERPMap]
// cachePath is the path to the DERP map cache file, set by SetCachePath via
// ipnserver.New() if we have a state directory.
var cachePath string
// UpdateCache stores the DERP map cache back to disk.
//
// The caller must not mutate 'c' after calling this function.
func UpdateCache(c *tailcfg.DERPMap, logf logger.Logf) {
// Don't do anything if nothing changed.
curr := cachedDERPMap.Load()
if reflect.DeepEqual(curr, c) {
return
}
d, err := json.Marshal(c)
if err != nil {
logf("[v1] dnsfallback: UpdateCache error marshaling: %v", err)
return
}
// Only store after we're confident this is at least valid JSON
cachedDERPMap.Store(c)
// Don't try writing if we don't have a cache path set; this can happen
// when we don't have a state path (e.g. /var/lib/tailscale) configured.
if cachePath != "" {
err = atomicfile.WriteFile(cachePath, d, 0600)
if err != nil {
logf("[v1] dnsfallback: UpdateCache error writing: %v", err)
return
}
}
}
// SetCachePath sets the path to the on-disk DERP map cache that we store and
// update. Additionally, if a file at this path exists, we load it and merge it
// with the DERP map baked into the binary.
//
// This function should be called before any calls to UpdateCache, as it is not
// concurrency-safe.
func SetCachePath(path string, logf logger.Logf) {
cachePath = path
f, err := os.Open(path)
if err != nil {
logf("[v1] dnsfallback: SetCachePath error reading %q: %v", path, err)
return
}
defer f.Close()
dm := new(tailcfg.DERPMap)
if err := json.NewDecoder(f).Decode(dm); err != nil {
logf("[v1] dnsfallback: SetCachePath error decoding %q: %v", path, err)
return
}
cachedDERPMap.Store(dm)
logf("[v2] dnsfallback: SetCachePath loaded cached DERP map")
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package netaddr is a transitional package while we finish migrating from inet.af/netaddr
// to Go 1.18's net/netip.
//
// TODO(bradfitz): delete this package eventually. Tracking bug is
// https://github.com/tailscale/tailscale/issues/5162
package netaddr
import (
"net"
"net/netip"
)
// IPv4 returns the IP of the IPv4 address a.b.c.d.
func IPv4(a, b, c, d uint8) netip.Addr {
return netip.AddrFrom4([4]byte{a, b, c, d})
}
// Unmap returns the provided AddrPort with its Addr IP component Unmap'ed.
//
// See https://github.com/golang/go/issues/53607#issuecomment-1203466984
func Unmap(ap netip.AddrPort) netip.AddrPort {
return netip.AddrPortFrom(ap.Addr().Unmap(), ap.Port())
}
// FromStdIPNet returns an IPPrefix from the standard library's IPNet type.
// If std is invalid, ok is false.
func FromStdIPNet(std *net.IPNet) (prefix netip.Prefix, ok bool) {
ip, ok := netip.AddrFromSlice(std.IP)
if !ok {
return netip.Prefix{}, false
}
ip = ip.Unmap()
if ln := len(std.Mask); ln != net.IPv4len && ln != net.IPv6len {
// Invalid mask.
return netip.Prefix{}, false
}
ones, bits := std.Mask.Size()
if ones == 0 && bits == 0 {
// IPPrefix does not support non-contiguous masks.
return netip.Prefix{}, false
}
return netip.PrefixFrom(ip, ones), true
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package neterror classifies network errors.
package neterror
import (
"errors"
"fmt"
"runtime"
"syscall"
)
var errEPERM error = syscall.EPERM // box it into interface just once
// TreatAsLostUDP reports whether err is an error from a UDP send
// operation that should be treated as a UDP packet that just got
// lost.
//
// Notably, on Linux this reports true for EPERM errors (from outbound
// firewall blocks) which aren't really send errors; they're just
// sends that are never going to make it because the local OS blocked
// it.
func TreatAsLostUDP(err error) bool {
if err == nil {
return false
}
switch runtime.GOOS {
case "linux":
// Linux, while not documented in the man page,
// returns EPERM when there's an OUTPUT rule with -j
// DROP or -j REJECT. We use this very specific
// Linux+EPERM check rather than something super broad
// like net.Error.Temporary which could be anything.
//
// For now we only do this on Linux, as such outgoing
// firewall violations mapping to syscall errors
// hasn't yet been observed on other OSes.
return errors.Is(err, errEPERM)
}
return false
}
var packetWasTruncated func(error) bool // non-nil on Windows at least
// PacketWasTruncated reports whether err indicates truncation but the RecvFrom
// that generated err was otherwise successful. On Windows, Go's UDP RecvFrom
// calls WSARecvFrom which returns the WSAEMSGSIZE error code when the received
// datagram is larger than the provided buffer. When that happens, both a valid
// size and an error are returned (as per the partial fix for golang/go#14074).
// If the WSAEMSGSIZE error is returned, then we ignore the error to get
// semantics similar to the POSIX operating systems. One caveat is that it
// appears that the source address is not returned when WSAEMSGSIZE occurs, but
// we do not currently look at the source address.
func PacketWasTruncated(err error) bool {
if packetWasTruncated == nil {
return false
}
return packetWasTruncated(err)
}
var shouldDisableUDPGSO func(error) bool // non-nil on Linux
func ShouldDisableUDPGSO(err error) bool {
if shouldDisableUDPGSO == nil {
return false
}
return shouldDisableUDPGSO(err)
}
type ErrUDPGSODisabled struct {
OnLaddr string
RetryErr error
}
func (e ErrUDPGSODisabled) Error() string {
return fmt.Sprintf("disabled UDP GSO on %s, NIC(s) may not support checksum offload", e.OnLaddr)
}
func (e ErrUDPGSODisabled) Unwrap() error {
return e.RetryErr
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package neterror
import (
"errors"
"os"
"golang.org/x/sys/unix"
)
func init() {
shouldDisableUDPGSO = func(err error) bool {
if serr, ok := errors.AsType[*os.SyscallError](err); ok {
// EIO is returned by udp_send_skb() if the device driver does not
// have tx checksumming enabled, which is a hard requirement of
// UDP_SEGMENT. See:
// https://git.kernel.org/pub/scm/docs/man-pages/man-pages.git/tree/man7/udp.7?id=806eabd74910447f21005160e90957bde4db0183#n228
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/ipv4/udp.c?h=v6.2&id=c9c3395d5e3dcc6daee66c6908354d47bf98cb0c#n942
return serr.Err == unix.EIO
}
return false
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !plan9 && !js && !wasip1 && !wasm
package neterror
import (
"errors"
"io"
"io/fs"
"runtime"
"syscall"
)
// Reports whether err resulted from reading or writing to a closed or broken pipe.
func IsClosedPipeError(err error) bool {
// 232 is Windows error code ERROR_NO_DATA, "The pipe is being closed".
if runtime.GOOS == "windows" && errors.Is(err, syscall.Errno(232)) {
return true
}
// EPIPE/ENOTCONN are common errors when a send fails due to a closed
// socket. There is some platform and version inconsistency in which
// error is returned, but the meaning is the same.
// Libraries may also return root errors like fs.ErrClosed/io.ErrClosedPipe
// due to a closed socket.
return errors.Is(err, syscall.EPIPE) ||
errors.Is(err, syscall.ENOTCONN) ||
errors.Is(err, fs.ErrClosed) ||
errors.Is(err, io.ErrClosedPipe)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package netknob has Tailscale network knobs.
package netknob
import (
"runtime"
"time"
)
// PlatformTCPKeepAlive returns the default net.Dialer.KeepAlive
// value for the current runtime.GOOS.
func PlatformTCPKeepAlive() time.Duration {
switch runtime.GOOS {
case "ios", "android":
// Disable TCP keep-alives on mobile platforms.
// See https://github.com/golang/go/issues/48622.
//
// TODO(bradfitz): in 1.17.x, try disabling TCP
// keep-alives on for all platforms.
return -1
}
// Otherwise, default to 30 seconds, which is mostly what we
// used to do. In some places we used the zero value, which Go
// defaults to 15 seconds. But 30 seconds is fine.
return 30 * time.Second
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package netmon
import (
"errors"
"net"
"tailscale.com/syncs"
)
type ifProps struct {
mu syncs.Mutex
name string // interface name, if known/set
index int // interface index, if known/set
}
// tsIfProps tracks the properties (name and index) of the tailscale interface.
// There is only one tailscale interface per tailscaled instance.
var tsIfProps ifProps
func (p *ifProps) tsIfName() string {
p.mu.Lock()
defer p.mu.Unlock()
return p.name
}
func (p *ifProps) tsIfIndex() int {
p.mu.Lock()
defer p.mu.Unlock()
return p.index
}
func (p *ifProps) set(ifName string, ifIndex int) {
p.mu.Lock()
defer p.mu.Unlock()
p.name = ifName
p.index = ifIndex
}
// TODO (barnstar): This doesn't need the Monitor receiver anymore but we're
// keeping it for API compatibility to avoid a breaking change. This can be
// removed when the various clients have switched to SetTailscaleInterfaceProps
func (m *Monitor) SetTailscaleInterfaceName(ifName string) {
SetTailscaleInterfaceProps(ifName, 0)
}
// SetTailscaleInterfaceProps sets the name of the Tailscale interface and
// its index for use by various listeners/dialers. If the index is zero,
// an attempt will be made to look it up by name. This makes no attempt
// to validate that the interface exists at the time of calling.
//
// If this method is called, it is the responsibility of the caller to
// update the interface name and index if they change.
//
// This should be called as early as possible during tailscaled startup.
func SetTailscaleInterfaceProps(ifName string, ifIndex int) {
if ifIndex != 0 {
tsIfProps.set(ifName, ifIndex)
return
}
ifaces, err := net.Interfaces()
if err != nil {
return
}
for _, iface := range ifaces {
if iface.Name == ifName {
ifIndex = iface.Index
break
}
}
tsIfProps.set(ifName, ifIndex)
}
// TailscaleInterfaceName returns the name of the Tailscale interface.
// For example, "tailscale0", "tun0", "utun3", etc or an error if unset.
//
// Callers must handle errors, as the Tailscale interface
// name may not be set in some environments.
func TailscaleInterfaceName() (string, error) {
name := tsIfProps.tsIfName()
if name == "" {
return "", errors.New("Tailscale interface name not set")
}
return name, nil
}
// TailscaleInterfaceIndex returns the index of the Tailscale interface or
// an error if unset.
//
// Callers must handle errors, as the Tailscale interface
// index may not be set in some environments.
func TailscaleInterfaceIndex() (int, error) {
index := tsIfProps.tsIfIndex()
if index == 0 {
return 0, errors.New("Tailscale interface index not set")
}
return index, nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !android
package netmon
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"log"
"net"
"net/netip"
"os"
"strings"
"sync/atomic"
"github.com/jsimonetti/rtnetlink"
"github.com/mdlayher/netlink"
"go4.org/mem"
"golang.org/x/sys/unix"
"tailscale.com/feature/buildfeatures"
"tailscale.com/net/netaddr"
"tailscale.com/util/lineiter"
)
func init() {
likelyHomeRouterIP = likelyHomeRouterIPLinux
}
var procNetRouteErr atomic.Bool
/*
Parse 10.0.0.1 out of:
$ cat /proc/net/route
Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT
ens18 00000000 0100000A 0003 0 0 0 00000000 0 0 0
ens18 0000000A 00000000 0001 0 0 0 0000FFFF 0 0 0
*/
func likelyHomeRouterIPLinux() (ret netip.Addr, myIP netip.Addr, ok bool) {
if !buildfeatures.HasPortMapper {
return
}
if procNetRouteErr.Load() {
// If we failed to read /proc/net/route previously, don't keep trying.
return ret, myIP, false
}
lineNum := 0
var f []mem.RO
for lr := range lineiter.File(procNetRoutePath) {
line, err := lr.Value()
if err != nil {
procNetRouteErr.Store(true)
log.Printf("interfaces: failed to read /proc/net/route: %v", err)
return ret, myIP, false
}
lineNum++
if lineNum == 1 {
// Skip header line.
continue
}
if lineNum > maxProcNetRouteRead {
break
}
f = mem.AppendFields(f[:0], mem.B(line))
if len(f) < 4 {
continue
}
gwHex, flagsHex := f[2], f[3]
flags, err := mem.ParseUint(flagsHex, 16, 16)
if err != nil {
continue // ignore error, skip line and keep going
}
if flags&(unix.RTF_UP|unix.RTF_GATEWAY) != unix.RTF_UP|unix.RTF_GATEWAY {
continue
}
ipu32, err := mem.ParseUint(gwHex, 16, 32)
if err != nil {
continue // ignore error, skip line and keep going
}
ip := netaddr.IPv4(byte(ipu32), byte(ipu32>>8), byte(ipu32>>16), byte(ipu32>>24))
if ip.IsPrivate() {
ret = ip
break
}
}
if ret.IsValid() {
// Try to get the local IP of the interface associated with
// this route to short-circuit finding the IP associated with
// this gateway. This isn't fatal if it fails.
if len(f) > 0 && !disableLikelyHomeRouterIPSelf() {
ForeachInterface(func(ni Interface, pfxs []netip.Prefix) {
// Ensure this is the same interface
if !f[0].EqualString(ni.Name) {
return
}
// Find the first IPv4 address and use it.
for _, pfx := range pfxs {
if addr := pfx.Addr(); addr.Is4() {
myIP = addr
break
}
}
})
}
return ret, myIP, true
}
if lineNum >= maxProcNetRouteRead {
// If we went over our line limit without finding an answer, assume
// we're a big fancy Linux router (or at least not a home system)
// and set the error bit so we stop trying this in the future (and wasting CPU).
// See https://github.com/tailscale/tailscale/issues/7621.
//
// Remember that "likelyHomeRouterIP" exists purely to find the port
// mapping service (UPnP, PMP, PCP) often present on a home router. If we hit
// the route (line) limit without finding an answer, we're unlikely to ever
// find one in the future.
procNetRouteErr.Store(true)
}
return netip.Addr{}, netip.Addr{}, false
}
func defaultRoute() (d DefaultRouteDetails, err error) {
v, err := defaultRouteInterfaceProcNet()
if err == nil {
d.InterfaceName = v
return d, nil
}
// Issue 4038: the default route (such as on Unifi UDM Pro)
// might be in a non-default table, so it won't show up in
// /proc/net/route. Use netlink to find the default route.
//
// TODO(bradfitz): this allocates a fair bit. We should track
// this in net/interfaces/monitor instead and have
// interfaces.GetState take a netmon.Monitor or similar so the
// routing table can be cached and the monitor's existing
// subscription to route changes can update the cached state,
// rather than querying the whole thing every time like
// defaultRouteFromNetlink does.
//
// Then we should just always try to use the cached route
// table from netlink every time, and only use /proc/net/route
// as a fallback for weird environments where netlink might be
// banned but /proc/net/route is emulated (e.g. stuff like
// Cloud Run?).
return defaultRouteFromNetlink()
}
func defaultRouteFromNetlink() (d DefaultRouteDetails, err error) {
c, err := rtnetlink.Dial(&netlink.Config{Strict: true})
if err != nil {
return d, fmt.Errorf("defaultRouteFromNetlink: Dial: %w", err)
}
defer c.Close()
rms, err := c.Route.List()
if err != nil {
return d, fmt.Errorf("defaultRouteFromNetlink: List: %w", err)
}
for _, rm := range rms {
if rm.Attributes.Gateway == nil {
// A default route has a gateway. If it doesn't, skip it.
continue
}
if rm.Attributes.Dst != nil {
// A default route has a nil destination to mean anything
// so ignore any route for a specific destination.
// TODO(bradfitz): better heuristic?
// empirically this seems like enough.
continue
}
// TODO(bradfitz): care about address family, if
// callers ever start caring about v4-vs-v6 default
// route differences.
idx := int(rm.Attributes.OutIface)
if idx == 0 {
continue
}
if iface, err := net.InterfaceByIndex(idx); err == nil {
d.InterfaceName = iface.Name
d.InterfaceIndex = idx
return d, nil
}
}
return d, errNoDefaultRoute
}
var zeroRouteBytes = []byte("00000000")
var procNetRoutePath = "/proc/net/route"
// maxProcNetRouteRead is the max number of lines to read from
// /proc/net/route looking for a default route.
const maxProcNetRouteRead = 1000
var errNoDefaultRoute = errors.New("no default route found")
func defaultRouteInterfaceProcNetInternal(bufsize int) (string, error) {
f, err := os.Open(procNetRoutePath)
if err != nil {
return "", err
}
defer f.Close()
br := bufio.NewReaderSize(f, bufsize)
lineNum := 0
for {
lineNum++
line, err := br.ReadSlice('\n')
if err == io.EOF || lineNum > maxProcNetRouteRead {
return "", errNoDefaultRoute
}
if err != nil {
return "", err
}
if !bytes.Contains(line, zeroRouteBytes) {
continue
}
fields := strings.Fields(string(line))
ifc := fields[0]
ip := fields[1]
netmask := fields[7]
if strings.HasPrefix(ifc, "tailscale") ||
strings.HasPrefix(ifc, "wg") {
continue
}
if ip == "00000000" && netmask == "00000000" {
// default route
return ifc, nil // interface name
}
}
}
// returns string interface name and an error.
// io.EOF: full route table processed, no default route found.
// other io error: something went wrong reading the route file.
func defaultRouteInterfaceProcNet() (string, error) {
rc, err := defaultRouteInterfaceProcNetInternal(128)
if rc == "" && (errors.Is(err, io.EOF) || err == nil) {
// https://github.com/google/gvisor/issues/5732
// On a regular Linux kernel you can read the first 128 bytes of /proc/net/route,
// then come back later to read the next 128 bytes and so on.
//
// In Google Cloud Run, where /proc/net/route comes from gVisor, you have to
// read it all at once. If you read only the first few bytes then the second
// read returns 0 bytes no matter how much originally appeared to be in the file.
//
// At the time of this writing (Mar 2021) Google Cloud Run has eth0 and eth1
// with a 384 byte /proc/net/route. We allocate a large buffer to ensure we'll
// read it all in one call.
return defaultRouteInterfaceProcNetInternal(4096)
}
return rc, err
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package netmon
import (
"context"
"sync"
"time"
"tailscale.com/types/logger"
"tailscale.com/util/eventbus"
)
const cooldownSeconds = 300
// LinkChangeLogLimiter returns a new [logger.Logf] that logs each unique
// format string to the underlying logger only once per major LinkChange event
// with a cooldownSeconds second cooldown.
//
// The logger stops tracking seen format strings when the provided context is
// done.
func LinkChangeLogLimiter(ctx context.Context, logf logger.Logf, nm *Monitor) logger.Logf {
var formatLastSeen sync.Map // map[string]int64
sub := eventbus.SubscribeFunc(nm.b, func(cd *ChangeDelta) {
// Any link changes that are flagged as likely require a rebind are
// interesting enough that we should log them.
if cd.RebindLikelyRequired {
formatLastSeen.Clear()
}
})
context.AfterFunc(ctx, sub.Close)
return func(format string, args ...any) {
// get the current timestamp
now := time.Now().Unix()
lastSeen, ok := formatLastSeen.Load(format)
if ok {
// if we've seen this format string within the last cooldownSeconds, skip logging
if now-lastSeen.(int64) < cooldownSeconds {
return
}
}
// update the last seen timestamp for this format string
formatLastSeen.Store(format, now)
logf(format, args...)
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package netmon provides facilities for monitoring network interface and
// route changes. It primarily exists to know when portable devices move
// between different networks.
package netmon
import (
"errors"
"fmt"
"log"
"net/netip"
"runtime"
"slices"
"strings"
"sync"
"time"
"tailscale.com/feature/buildfeatures"
"tailscale.com/syncs"
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
"tailscale.com/util/eventbus"
"tailscale.com/util/set"
)
// pollWallTimeInterval is how often we check the time to check
// for big jumps in wall (non-monotonic) time as a backup mechanism
// to get notified of a sleeping device waking back up.
// Usually there are also minor network change events on wake that let
// us check the wall time sooner than this.
const pollWallTimeInterval = 15 * time.Second
// majorTimeJumpThreshold is the minimum sleep duration that warrants
// treating a time jump as a major event requiring socket rebinding,
// even if the interface state appears unchanged. After a long sleep,
// NAT mappings are likely stale and DHCP leases may have expired
// (the renewal happens after wake, so local state may not yet reflect it).
// Short sleeps (e.g., macOS DarkWake maintenance cycles of ~55s) should
// not trigger rebinding if the network state is unchanged.
const majorTimeJumpThreshold = 10 * time.Minute
// message represents a message returned from an osMon.
type message interface {
// Ignore is whether we should ignore this message.
ignore() bool
}
// osMon is the interface that each operating system-specific
// implementation of the link monitor must implement.
type osMon interface {
Close() error
// Receive returns a new network interface change message. It
// should block until there's either something to return, or
// until the osMon is closed. After a Close, the returned
// error is ignored.
Receive() (message, error)
}
// IsInterestingInterface is the function used to determine whether
// a given interface name is interesting enough to pay attention to
// for network change monitoring purposes.
//
// If nil, all interfaces are considered interesting.
var IsInterestingInterface func(Interface, []netip.Prefix) bool
// Monitor represents a monitoring instance.
type Monitor struct {
logf logger.Logf
b *eventbus.Client
changed *eventbus.Publisher[ChangeDelta]
om osMon // nil means not supported on this platform
change chan bool // send false to wake poller, true to also force ChangeDeltas be sent
stop chan struct{} // closed on Stop
static bool // static Monitor that doesn't actually monitor
mu syncs.Mutex // guards all following fields
cbs set.HandleSet[ChangeFunc]
ifState *State
gwValid bool // whether gw and gwSelfIP are valid
gw netip.Addr // our gateway's IP
gwSelfIP netip.Addr // our own IP address (that corresponds to gw)
started bool
closed bool
goroutines sync.WaitGroup
wallTimer *time.Timer // nil until Started; re-armed AfterFunc per tick
lastWall time.Time
jumpDuration time.Duration // wall-clock time elapsed during detected time jump; 0 if no time jump observed since reset
}
// ChangeFunc is a callback function registered with Monitor that's called when the
// network changed.
type ChangeFunc func(*ChangeDelta)
// ChangeDelta describes the difference between two network states.
//
// Use NewChangeDelta to construct a delta and compute the cached fields.
type ChangeDelta struct {
// old is the old interface state, if known.
// It's nil if the old state is unknown.
old *State
// New is the new network state.
// It is always non-nil.
new *State
// JumpDuration is non-zero when a wall-clock time jump was detected,
// indicating the machine likely just woke from sleep. It is approximately
// how long the machine was asleep (the wall-clock delta since the last
// check, not an exact sleep measurement). Use TimeJumped() to check
// whether a time jump occurred.
JumpDuration time.Duration
DefaultRouteInterface string
// Computed Fields
DefaultInterfaceChanged bool // whether default route interface changed
IsLessExpensive bool // whether new state's default interface is less expensive than old.
HasPACOrProxyConfigChanged bool // whether PAC/HTTP proxy config changed
InterfaceIPsChanged bool // whether any interface IPs changed in a meaningful way
AvailableProtocolsChanged bool // whether we have seen a change in available IPv4/IPv6
DefaultInterfaceMaybeViable bool // whether the default interface is potentially viable (has usable IPs, is up and is not the tunnel itself)
IsInitialState bool // whether this is the initial state (old == nil, new != nil)
// RebindLikelyRequired combines the various fields above to report whether this change likely requires us
// to rebind sockets. This is a very conservative estimate and covers a number ofcases where a rebind
// may not be strictly necessary. Consumers of the ChangeDelta should consider checking the individual fields
// above or the state of their sockets.
RebindLikelyRequired bool
}
// TimeJumped reports whether a wall-clock time jump was detected,
// indicating the machine likely just woke from sleep. When true,
// JumpDuration contains the approximate duration.
func (cd *ChangeDelta) TimeJumped() bool {
return cd.JumpDuration > 0
}
// CurrentState returns the current (new) state after the change.
func (cd *ChangeDelta) CurrentState() *State {
return cd.new
}
// NewChangeDelta builds a ChangeDelta and eagerly computes the cached fields.
// jumpDuration, if non-zero, indicates a wall-clock time jump was detected
// (the machine likely woke from sleep) and is the approximate duration of the jump.
// forceViability, if true, forces DefaultInterfaceMaybeViable to be true regardless of the
// actual state of the default interface. This is useful in testing.
func NewChangeDelta(old, new *State, jumpDuration time.Duration, forceViability bool) (*ChangeDelta, error) {
cd := ChangeDelta{
old: old,
new: new,
JumpDuration: jumpDuration,
}
if cd.new == nil {
log.Printf("[unexpected] NewChangeDelta called with nil new state")
return nil, errors.New("new state cannot be nil")
} else if cd.old == nil && cd.new != nil {
cd.DefaultInterfaceChanged = cd.new.DefaultRouteInterface != ""
cd.IsLessExpensive = false
cd.HasPACOrProxyConfigChanged = true
cd.InterfaceIPsChanged = true
cd.IsInitialState = true
} else {
cd.AvailableProtocolsChanged = (cd.old.HaveV4 != cd.new.HaveV4) || (cd.old.HaveV6 != cd.new.HaveV6)
cd.DefaultInterfaceChanged = cd.old.DefaultRouteInterface != cd.new.DefaultRouteInterface
cd.IsLessExpensive = cd.old.IsExpensive && !cd.new.IsExpensive
cd.HasPACOrProxyConfigChanged = (cd.old.PAC != cd.new.PAC) || (cd.old.HTTPProxy != cd.new.HTTPProxy)
cd.InterfaceIPsChanged = cd.isInterestingInterfaceChange()
}
cd.DefaultRouteInterface = new.DefaultRouteInterface
defIf := new.Interface[cd.DefaultRouteInterface]
tsIfName, err := TailscaleInterfaceName()
// The default interface is not viable if it is down or it is the Tailscale interface itself.
if !forceViability && (!defIf.IsUp() || (err == nil && cd.DefaultRouteInterface == tsIfName)) {
cd.DefaultInterfaceMaybeViable = false
} else {
cd.DefaultInterfaceMaybeViable = true
}
// Compute rebind requirement. The default interface needs to be viable and
// one of the other conditions needs to be true.
//
// Short time jumps (e.g., macOS DarkWake maintenance cycles of ~55s) are
// excluded — if the network state is unchanged after a brief sleep, there's
// no reason to rebind. However, a major time jump (over majorTimeJumpThreshold)
// warrants a rebind even if the local state looks the same, because NAT
// mappings are likely stale and DHCP leases may have changed (the renewal
// happens after wake, so local state may not yet reflect it).
majorTimeJump := cd.JumpDuration >= majorTimeJumpThreshold
cd.RebindLikelyRequired = (cd.old == nil ||
majorTimeJump ||
cd.DefaultInterfaceChanged ||
cd.InterfaceIPsChanged ||
cd.IsLessExpensive ||
cd.HasPACOrProxyConfigChanged ||
cd.AvailableProtocolsChanged) &&
cd.DefaultInterfaceMaybeViable
return &cd, nil
}
// StateDesc returns a description of the old and new states for logging.
func (cd *ChangeDelta) StateDesc() string {
var sb strings.Builder
fmt.Fprintf(&sb, "old: %v new: %v", cd.old, cd.new)
if cd.old != nil && cd.new != nil {
if diff := cd.old.InterfaceDiff(cd.new); diff != "" {
fmt.Fprintf(&sb, " diff: %s", diff)
}
}
if cd.RebindLikelyRequired {
var reasons []string
if cd.old == nil {
reasons = append(reasons, "initial-state")
}
if cd.TimeJumped() {
reasons = append(reasons, fmt.Sprintf("time-jumped(%v)", cd.JumpDuration.Round(time.Second)))
}
if cd.DefaultInterfaceChanged {
reasons = append(reasons, "default-if-changed")
}
if cd.InterfaceIPsChanged {
reasons = append(reasons, "ips-changed")
}
if cd.IsLessExpensive {
reasons = append(reasons, "less-expensive")
}
if cd.HasPACOrProxyConfigChanged {
reasons = append(reasons, "pac-proxy-changed")
}
if cd.AvailableProtocolsChanged {
reasons = append(reasons, "protocols-changed")
}
fmt.Fprintf(&sb, " rebind-reason=[%s]", strings.Join(reasons, ","))
}
return sb.String()
}
// InterfaceIPDisappeared reports whether the given IP address exists on any interface
// in the old state, but not in the new state.
func (cd *ChangeDelta) InterfaceIPDisappeared(ip netip.Addr) bool {
if cd.old == nil {
return false
}
if cd.new == nil && cd.old.HasIP(ip) {
return true
}
return cd.old.HasIP(ip) && !cd.new.HasIP(ip)
}
// AnyInterfaceUp reports whether any interfaces are up in the new state.
func (cd *ChangeDelta) AnyInterfaceUp() bool {
if cd.new == nil {
return false
}
return cd.new.AnyInterfaceUp()
}
// isInterestingInterfaceChange reports whether any interfaces have changed in a meaningful way.
// This excludes interfaces that are not interesting per IsInterestingInterface and
// filters out changes to interface IPs that are uninteresting (e.g. link-local addresses).
func (cd *ChangeDelta) isInterestingInterfaceChange() bool {
// If there is no old state, everything is considered changed.
if cd.old == nil {
return true
}
// Compare interfaces in both directions. Old to new and new to old.
tsIfName, ifNameErr := TailscaleInterfaceName()
for iname, oldInterface := range cd.old.Interface {
if ifNameErr == nil && iname == tsIfName {
// Ignore changes in the Tailscale interface itself
continue
}
oldIps := filterRoutableIPs(cd.old.InterfaceIPs[iname])
if IsInterestingInterface != nil && !IsInterestingInterface(oldInterface, oldIps) {
continue
}
// Old interfaces with no routable addresses are not interesting
if len(oldIps) == 0 {
continue
}
// The old interface doesn't exist in the new interface set and it has
// a global unicast IP. That's considered a change from the perspective
// of anything that may have been bound to it. If it didn't have a global
// unicast IP, it's not interesting.
newInterface, ok := cd.new.Interface[iname]
if !ok {
return true
}
newIps, ok := cd.new.InterfaceIPs[iname]
if !ok {
return true
}
newIps = filterRoutableIPs(newIps)
// Only consider routable IP changes and up/down state transitions
// as interesting. Transient metadata changes (Flags like FlagRunning,
// MTU, etc.) should not trigger a major link change, as they cause
// false "major" events on macOS and Windows when the OS notifies us
// of interface changes that don't affect connectivity.
if oldInterface.IsUp() != newInterface.IsUp() || !prefixesEqual(oldIps, newIps) {
return true
}
}
for iname, newInterface := range cd.new.Interface {
if ifNameErr == nil && iname == tsIfName {
// Ignore changes in the Tailscale interface itself
continue
}
newIps := filterRoutableIPs(cd.new.InterfaceIPs[iname])
if IsInterestingInterface != nil && !IsInterestingInterface(newInterface, newIps) {
continue
}
// New interfaces with no routable addresses are not interesting
if len(newIps) == 0 {
continue
}
oldInterface, ok := cd.old.Interface[iname]
if !ok {
return true
}
oldIps, ok := cd.old.InterfaceIPs[iname]
if !ok {
// Redundant but we can't dig up the "old" IPs for this interface.
return true
}
oldIps = filterRoutableIPs(oldIps)
// Only consider routable IP changes and up/down state transitions.
if newInterface.IsUp() != oldInterface.IsUp() || !prefixesEqual(oldIps, newIps) {
return true
}
}
return false
}
func filterRoutableIPs(addrs []netip.Prefix) []netip.Prefix {
var filtered []netip.Prefix
for _, pfx := range addrs {
a := pfx.Addr()
// Skip link-local multicast addresses.
if a.IsLinkLocalMulticast() {
continue
}
if isUsableV4(a) || isUsableV6(a) {
filtered = append(filtered, pfx)
}
}
return filtered
}
// New instantiates and starts a monitoring instance.
// The returned monitor is inactive until it's started by the Start method.
// Use RegisterChangeCallback to get notified of network changes.
func New(bus *eventbus.Bus, logf logger.Logf) (*Monitor, error) {
logf = logger.WithPrefix(logf, "monitor: ")
m := &Monitor{
logf: logf,
b: bus.Client("netmon"),
change: make(chan bool, 1),
stop: make(chan struct{}),
lastWall: wallTime(),
}
m.changed = eventbus.Publish[ChangeDelta](m.b)
st, err := m.interfaceStateUncached()
if err != nil {
return nil, err
}
m.ifState = st
m.om, err = newOSMon(bus, logf, m)
if err != nil {
return nil, err
}
if m.om == nil {
return nil, errors.New("newOSMon returned nil, nil")
}
return m, nil
}
// NewStatic returns a Monitor that's a one-time snapshot of the network state
// but doesn't actually monitor for changes. It should only be used in tests
// and situations like cleanups or short-lived CLI programs.
func NewStatic() *Monitor {
m := &Monitor{static: true}
if st, err := m.interfaceStateUncached(); err == nil {
m.ifState = st
}
return m
}
// InterfaceState returns the latest snapshot of the machine's network
// interfaces.
//
// The returned value is owned by Mon; it must not be modified.
func (m *Monitor) InterfaceState() *State {
m.mu.Lock()
defer m.mu.Unlock()
return m.ifState
}
// ProbeLocks acquires and releases the monitor's internal mutex.
func (m *Monitor) ProbeLocks() {
m.mu.Lock()
m.mu.Unlock()
}
func (m *Monitor) interfaceStateUncached() (*State, error) {
return getState(tsIfProps.tsIfName())
}
// GatewayAndSelfIP returns the current network's default gateway, and
// the machine's default IP for that gateway.
//
// It's the same as interfaces.LikelyHomeRouterIP, but it caches the
// result until the monitor detects a network change.
func (m *Monitor) GatewayAndSelfIP() (gw, myIP netip.Addr, ok bool) {
if !buildfeatures.HasPortMapper {
return
}
if m.static {
return
}
m.mu.Lock()
defer m.mu.Unlock()
if m.gwValid {
return m.gw, m.gwSelfIP, true
}
gw, myIP, ok = LikelyHomeRouterIP()
changed := false
if ok {
changed = m.gw != gw || m.gwSelfIP != myIP
m.gw, m.gwSelfIP = gw, myIP
m.gwValid = true
}
if changed {
m.logf("gateway and self IP changed: gw=%v self=%v", m.gw, m.gwSelfIP)
}
return gw, myIP, ok
}
// RegisterChangeCallback adds callback to the set of parties to be
// notified (in their own goroutine) when the network state changes.
// To remove this callback, call unregister (or close the monitor).
func (m *Monitor) RegisterChangeCallback(callback ChangeFunc) (unregister func()) {
if m.static {
return func() {}
}
m.mu.Lock()
defer m.mu.Unlock()
handle := m.cbs.Add(callback)
return func() {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.cbs, handle)
}
}
// Start starts the monitor.
// A monitor can only be started & closed once.
func (m *Monitor) Start() {
if m.static {
return
}
m.mu.Lock()
defer m.mu.Unlock()
if m.started || m.closed {
return
}
m.started = true
if shouldMonitorTimeJump {
m.wallTimer = time.AfterFunc(pollWallTimeInterval, m.pollWallTime)
}
if m.om == nil {
return
}
m.goroutines.Add(2)
go m.pump()
go m.debounce()
}
// Close closes the monitor.
func (m *Monitor) Close() error {
if m.static {
return nil
}
m.mu.Lock()
if m.closed {
m.mu.Unlock()
return nil
}
m.closed = true
close(m.stop)
if m.wallTimer != nil {
m.wallTimer.Stop()
}
var err error
if m.om != nil {
err = m.om.Close()
}
started := m.started
m.mu.Unlock()
if started {
m.goroutines.Wait()
}
return err
}
// InjectEvent forces the monitor to pretend there was a network
// change and re-check the state of the network. Any registered
// ChangeFunc callbacks will be called within the event coalescing
// period (under a fraction of a second).
func (m *Monitor) InjectEvent() {
if m.static {
return
}
select {
case m.change <- true:
default:
// Another change signal is already
// buffered. Debounce will wake up soon
// enough.
}
}
// Poll forces the monitor to pretend there was a network
// change and re-check the state of the network.
//
// This is like InjectEvent but only fires ChangeFunc callbacks
// if the network state differed at all.
func (m *Monitor) Poll() {
if m.static {
return
}
select {
case m.change <- false:
default:
}
}
func (m *Monitor) stopped() bool {
select {
case <-m.stop:
return true
default:
return false
}
}
// pump continuously retrieves messages from the connection, notifying
// the change channel of changes, and stopping when a stop is issued.
func (m *Monitor) pump() {
defer m.goroutines.Done()
for !m.stopped() {
msg, err := m.om.Receive()
if err != nil {
if m.stopped() {
return
}
// Keep retrying while we're not closed.
m.logf("error from link monitor: %v", err)
time.Sleep(time.Second)
continue
}
if msg.ignore() {
continue
}
m.Poll()
}
}
// debounce calls the callback function with a delay between events
// and exits when a stop is issued.
func (m *Monitor) debounce() {
defer m.goroutines.Done()
for {
var forceCallbacks bool
select {
case <-m.stop:
return
case forceCallbacks = <-m.change:
}
if newState, err := m.interfaceStateUncached(); err != nil {
m.logf("interfaces.State: %v", err)
} else {
m.handlePotentialChange(newState, forceCallbacks)
}
select {
case <-m.stop:
return
// 1s is reasonable debounce time for network changes. Events such as undocking a laptop
// or roaming onto wifi will often generate multiple events in quick succession as interfaces
// flap. We want to avoid spamming consumers of these events.
case <-time.After(1000 * time.Millisecond):
}
}
}
var (
metricChangeEq = clientmetric.NewCounter("netmon_link_change_eq")
metricChange = clientmetric.NewCounter("netmon_link_change")
metricChangeTimeJump = clientmetric.NewCounter("netmon_link_change_timejump")
metricChangeMajor = clientmetric.NewCounter("netmon_link_change_major")
)
// handlePotentialChange considers whether newState is different enough to wake
// up callers and updates the monitor's state if so.
//
// If forceCallbacks is true, they're always notified.
func (m *Monitor) handlePotentialChange(newState *State, forceCallbacks bool) {
m.mu.Lock()
defer m.mu.Unlock()
oldState := m.ifState
timeJumped := shouldMonitorTimeJump && m.checkWallTimeAdvanceLocked()
if !timeJumped && !forceCallbacks && oldState.Equal(newState) {
// Exactly equal. Nothing to do.
metricChangeEq.Add(1)
return
}
jumpDuration := m.jumpDuration
delta, err := NewChangeDelta(oldState, newState, jumpDuration, false)
if err != nil {
m.logf("[unexpected] error creating ChangeDelta: %v", err)
return
}
if delta.RebindLikelyRequired {
m.gwValid = false
}
m.ifState = newState
// See if we have a queued or new time jump signal.
if timeJumped {
m.resetTimeJumpedLocked()
m.logf("time jump detected (slept %v), probably wake from sleep", jumpDuration.Round(time.Second))
}
metricChange.Add(1)
if delta.RebindLikelyRequired {
metricChangeMajor.Add(1)
}
if delta.TimeJumped() {
metricChangeTimeJump.Add(1)
}
m.changed.Publish(*delta)
for _, cb := range m.cbs {
go cb(delta)
}
}
// reports whether a and b contain the same set of prefixes regardless of order.
func prefixesEqual(a, b []netip.Prefix) bool {
if len(a) != len(b) {
return false
}
aa := make([]netip.Prefix, len(a))
bb := make([]netip.Prefix, len(b))
copy(aa, a)
copy(bb, b)
less := func(x, y netip.Prefix) int {
return x.Addr().Compare(y.Addr())
}
slices.SortFunc(aa, less)
slices.SortFunc(bb, less)
return slices.Equal(aa, bb)
}
func wallTime() time.Time {
// From time package's docs: "The canonical way to strip a
// monotonic clock reading is to use t = t.Round(0)."
return time.Now().Round(0)
}
func (m *Monitor) pollWallTime() {
m.mu.Lock()
defer m.mu.Unlock()
if m.closed {
return
}
if m.checkWallTimeAdvanceLocked() {
m.InjectEvent()
}
m.wallTimer.Reset(pollWallTimeInterval)
}
// shouldMonitorTimeJump is whether we keep a regular periodic timer running in
// the background watching for jumps in wall time.
//
// We don't do this on mobile platforms for battery reasons, and because these
// platforms don't really sleep in the same way.
const shouldMonitorTimeJump = runtime.GOOS != "android" && runtime.GOOS != "ios" && runtime.GOOS != "plan9"
// checkWallTimeAdvanceLocked reports whether wall time jumped more than 150% of
// pollWallTimeInterval, indicating we probably just came out of sleep. Once a
// time jump is detected it must be reset by calling resetTimeJumpedLocked.
func (m *Monitor) checkWallTimeAdvanceLocked() bool {
if !shouldMonitorTimeJump {
panic("unreachable") // if callers are correct
}
now := wallTime()
if elapsed := now.Sub(m.lastWall); elapsed > pollWallTimeInterval*3/2 {
m.jumpDuration = elapsed
}
m.lastWall = now
return m.jumpDuration != 0
}
// resetTimeJumpedLocked consumes the signal set by checkWallTimeAdvanceLocked.
func (m *Monitor) resetTimeJumpedLocked() {
m.jumpDuration = 0
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !android
package netmon
import (
"net"
"net/netip"
"time"
"github.com/jsimonetti/rtnetlink"
"github.com/mdlayher/netlink"
"golang.org/x/sys/unix"
"tailscale.com/envknob"
"tailscale.com/net/tsaddr"
"tailscale.com/types/logger"
"tailscale.com/util/eventbus"
)
var debugNetlinkMessages = envknob.RegisterBool("TS_DEBUG_NETLINK")
// unspecifiedMessage is a minimal message implementation that should not
// be ignored. In general, OS-specific implementations should use better
// types and avoid this if they can.
type unspecifiedMessage struct{}
func (unspecifiedMessage) ignore() bool { return false }
// RuleDeleted reports that one of Tailscale's policy routing rules
// was deleted.
type RuleDeleted struct {
// Table is the table number that the deleted rule referenced.
Table uint8
// Priority is the lookup priority of the deleted rule.
Priority uint32
}
// nlConn wraps a *netlink.Conn and returns a monitor.Message
// instead of a netlink.Message. Currently, messages are discarded,
// but down the line, when messages trigger different logic depending
// on the type of event, this provides the capability of handling
// each architecture-specific message in a generic fashion.
type nlConn struct {
busClient *eventbus.Client
rulesDeleted *eventbus.Publisher[RuleDeleted]
logf logger.Logf
conn *netlink.Conn
buffered []netlink.Message
// addrCache maps interface indices to a set of addresses, and is
// used to suppress duplicate RTM_NEWADDR messages. It is populated
// by RTM_NEWADDR messages and de-populated by RTM_DELADDR. See
// issue #4282.
addrCache map[uint32]map[netip.Addr]bool
}
func newOSMon(bus *eventbus.Bus, logf logger.Logf, m *Monitor) (osMon, error) {
conn, err := netlink.Dial(unix.NETLINK_ROUTE, &netlink.Config{
// Routes get us most of the events of interest, but we need
// address as well to cover things like DHCP deciding to give
// us a new address upon renewal - routing wouldn't change,
// but all reachability would.
Groups: unix.RTMGRP_IPV4_IFADDR | unix.RTMGRP_IPV6_IFADDR |
unix.RTMGRP_IPV4_ROUTE | unix.RTMGRP_IPV6_ROUTE |
unix.RTMGRP_IPV4_RULE, // no IPV6_RULE in x/sys/unix
})
if err != nil {
// Google Cloud Run does not implement NETLINK_ROUTE RTMGRP support
logf("monitor_linux: AF_NETLINK RTMGRP failed, falling back to polling")
return newPollingMon(logf, m)
}
client := bus.Client("netmon-iprules")
return &nlConn{
busClient: client,
rulesDeleted: eventbus.Publish[RuleDeleted](client),
logf: logf,
conn: conn,
addrCache: make(map[uint32]map[netip.Addr]bool),
}, nil
}
func (c *nlConn) Close() error {
c.busClient.Close()
return c.conn.Close()
}
func (c *nlConn) Receive() (message, error) {
if len(c.buffered) == 0 {
var err error
c.buffered, err = c.conn.Receive()
if err != nil {
return nil, err
}
if len(c.buffered) == 0 {
// Unexpected. Not seen in wild, but sleep defensively.
time.Sleep(time.Second)
return ignoreMessage{}, nil
}
}
msg := c.buffered[0]
c.buffered = c.buffered[1:]
// See https://github.com/torvalds/linux/blob/master/include/uapi/linux/rtnetlink.h
// And https://man7.org/linux/man-pages/man7/rtnetlink.7.html
switch msg.Header.Type {
case unix.RTM_NEWADDR, unix.RTM_DELADDR:
var rmsg rtnetlink.AddressMessage
if err := rmsg.UnmarshalBinary(msg.Data); err != nil {
c.logf("failed to parse type %v: %v", msg.Header.Type, err)
return unspecifiedMessage{}, nil
}
nip := netaddrIP(rmsg.Attributes.Address)
if debugNetlinkMessages() {
typ := "RTM_NEWADDR"
if msg.Header.Type == unix.RTM_DELADDR {
typ = "RTM_DELADDR"
}
// label attributes are seemingly only populated for IPv4 addresses in the wild.
label := rmsg.Attributes.Label
if label == "" {
itf, err := net.InterfaceByIndex(int(rmsg.Index))
if err == nil {
label = itf.Name
}
}
c.logf("%s: %s(%d) %s / %s", typ, label, rmsg.Index, rmsg.Attributes.Address, rmsg.Attributes.Local)
}
addrs := c.addrCache[rmsg.Index]
// Ignore duplicate RTM_NEWADDR messages using c.addrCache to
// detect them. See nlConn.addrcache and issue #4282.
if msg.Header.Type == unix.RTM_NEWADDR {
if addrs == nil {
addrs = make(map[netip.Addr]bool)
c.addrCache[rmsg.Index] = addrs
}
if addrs[nip] {
if debugNetlinkMessages() {
c.logf("ignored duplicate RTM_NEWADDR for %s", nip)
}
return ignoreMessage{}, nil
}
addrs[nip] = true
} else { // msg.Header.Type == unix.RTM_DELADDR
if addrs != nil {
delete(addrs, nip)
}
if len(addrs) == 0 {
delete(c.addrCache, rmsg.Index)
}
}
nam := &newAddrMessage{
IfIndex: rmsg.Index,
Addr: nip,
Delete: msg.Header.Type == unix.RTM_DELADDR,
}
if debugNetlinkMessages() {
c.logf("%+v", nam)
}
return nam, nil
case unix.RTM_NEWROUTE, unix.RTM_DELROUTE:
typeStr := "RTM_NEWROUTE"
if msg.Header.Type == unix.RTM_DELROUTE {
typeStr = "RTM_DELROUTE"
}
var rmsg rtnetlink.RouteMessage
if err := rmsg.UnmarshalBinary(msg.Data); err != nil {
c.logf("%s: failed to parse: %v", typeStr, err)
return unspecifiedMessage{}, nil
}
src := netaddrIPPrefix(rmsg.Attributes.Src, rmsg.SrcLength)
dst := netaddrIPPrefix(rmsg.Attributes.Dst, rmsg.DstLength)
gw := netaddrIP(rmsg.Attributes.Gateway)
if msg.Header.Type == unix.RTM_NEWROUTE &&
(rmsg.Attributes.Table == 255 || rmsg.Attributes.Table == 254) &&
(dst.Addr().IsMulticast() || dst.Addr().IsLinkLocalUnicast()) {
if debugNetlinkMessages() {
c.logf("%s ignored", typeStr)
}
// Normal Linux route changes on new interface coming up; don't log or react.
return ignoreMessage{}, nil
}
if rmsg.Table == tsTable && dst.IsSingleIP() {
// Don't log. Spammy and normal to see a bunch of these on start-up,
// which we make ourselves.
} else if tsaddr.IsTailscaleIP(dst.Addr()) {
// Verbose only.
c.logf("%s: [v1] src=%v, dst=%v, gw=%v, outif=%v, table=%v", typeStr,
condNetAddrPrefix(src), condNetAddrPrefix(dst), condNetAddrIP(gw),
rmsg.Attributes.OutIface, rmsg.Attributes.Table)
} else {
c.logf("%s: src=%v, dst=%v, gw=%v, outif=%v, table=%v", typeStr,
condNetAddrPrefix(src), condNetAddrPrefix(dst), condNetAddrIP(gw),
rmsg.Attributes.OutIface, rmsg.Attributes.Table)
}
if msg.Header.Type == unix.RTM_DELROUTE {
// Just logging it for now.
// (Debugging https://github.com/tailscale/tailscale/issues/643)
return unspecifiedMessage{}, nil
}
nrm := &newRouteMessage{
Table: rmsg.Table,
Src: src,
Dst: dst,
Gateway: gw,
}
if debugNetlinkMessages() {
c.logf("%+v", nrm)
}
return nrm, nil
case unix.RTM_NEWRULE:
// Probably ourselves adding it.
return ignoreMessage{}, nil
case unix.RTM_DELRULE:
// For https://github.com/tailscale/tailscale/issues/1591 where
// systemd-networkd deletes our rules.
var rmsg rtnetlink.RouteMessage
err := rmsg.UnmarshalBinary(msg.Data)
if err != nil {
c.logf("ip rule deleted; failed to parse netlink message: %v", err)
} else {
c.logf("ip rule deleted: %+v", rmsg)
// On `ip -4 rule del pref 5210 table main`, logs:
// monitor: ip rule deleted: {Family:2 DstLength:0 SrcLength:0 Tos:0 Table:254 Protocol:0 Scope:0 Type:1 Flags:0 Attributes:{Dst:<nil> Src:<nil> Gateway:<nil> OutIface:0 Priority:5210 Table:254 Mark:4294967295 Expires:<nil> Metrics:<nil> Multipath:[]}}
}
rd := RuleDeleted{
Table: rmsg.Table,
Priority: rmsg.Attributes.Priority,
}
c.rulesDeleted.Publish(rd)
if debugNetlinkMessages() {
c.logf("%+v", rd)
}
return ignoreMessage{}, nil
case unix.RTM_NEWLINK, unix.RTM_DELLINK:
// This is an unhandled message, but don't print an error.
// See https://github.com/tailscale/tailscale/issues/6806
return unspecifiedMessage{}, nil
default:
c.logf("unhandled netlink msg type %+v, %q", msg.Header, msg.Data)
return unspecifiedMessage{}, nil
}
}
func netaddrIP(std net.IP) netip.Addr {
ip, _ := netip.AddrFromSlice(std)
return ip.Unmap()
}
func netaddrIPPrefix(std net.IP, bits uint8) netip.Prefix {
ip, _ := netip.AddrFromSlice(std)
return netip.PrefixFrom(ip.Unmap(), int(bits))
}
func condNetAddrPrefix(ipp netip.Prefix) string {
if !ipp.Addr().IsValid() {
return ""
}
return ipp.String()
}
func condNetAddrIP(ip netip.Addr) string {
if !ip.IsValid() {
return ""
}
return ip.String()
}
// newRouteMessage is a message for a new route being added.
type newRouteMessage struct {
Src, Dst netip.Prefix
Gateway netip.Addr
Table uint8
}
const tsTable = 52
func (m *newRouteMessage) ignore() bool {
return m.Table == tsTable || tsaddr.IsTailscaleIP(m.Dst.Addr())
}
// newAddrMessage is a message for a new address being added.
type newAddrMessage struct {
Delete bool
Addr netip.Addr
IfIndex uint32 // interface index
}
func (m *newAddrMessage) ignore() bool {
return tsaddr.IsTailscaleIP(m.Addr)
}
type ignoreMessage struct{}
func (ignoreMessage) ignore() bool { return true }
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !windows && !darwin
package netmon
import (
"bytes"
"errors"
"os"
"runtime"
"sync"
"time"
"tailscale.com/types/logger"
)
func newPollingMon(logf logger.Logf, m *Monitor) (osMon, error) {
return &pollingMon{
logf: logf,
m: m,
stop: make(chan struct{}),
}, nil
}
// pollingMon is a bad but portable implementation of the link monitor
// that works by polling the interface state every 10 seconds, in lieu
// of anything to subscribe to.
type pollingMon struct {
logf logger.Logf
m *Monitor
closeOnce sync.Once
stop chan struct{}
}
func (pm *pollingMon) Close() error {
pm.closeOnce.Do(func() {
close(pm.stop)
})
return nil
}
func (pm *pollingMon) isCloudRun() bool {
// https: //cloud.google.com/run/docs/reference/container-contract#env-vars
if os.Getenv("K_REVISION") == "" || os.Getenv("K_CONFIGURATION") == "" ||
os.Getenv("K_SERVICE") == "" || os.Getenv("PORT") == "" {
return false
}
vers, err := os.ReadFile("/proc/version")
if err != nil {
pm.logf("Failed to read /proc/version: %v", err)
return false
}
return string(bytes.TrimSpace(vers)) == "Linux version 4.4.0 #1 SMP Sun Jan 10 15:06:54 PST 2016"
}
func (pm *pollingMon) Receive() (message, error) {
d := 10 * time.Second
if runtime.GOOS == "android" {
// We'll have Android notify the link monitor to wake up earlier,
// so this can go very slowly there, to save battery.
// https://github.com/tailscale/tailscale/issues/1427
d = 10 * time.Minute
} else if pm.isCloudRun() {
// Cloud Run routes never change at runtime. the containers are killed within
// 15 minutes by default, set the interval long enough to be effectively infinite.
pm.logf("monitor polling: Cloud Run detected, reduce polling interval to 24h")
d = 24 * time.Hour
}
timer := time.NewTimer(d)
defer timer.Stop()
for {
select {
case <-timer.C:
return unspecifiedMessage{}, nil
case <-pm.stop:
return nil, errors.New("stopped")
}
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package netmon
import (
"bytes"
"fmt"
"net"
"net/http"
"net/netip"
"runtime"
"slices"
"sort"
"strings"
"tailscale.com/envknob"
"tailscale.com/feature"
"tailscale.com/feature/buildfeatures"
"tailscale.com/hostinfo"
"tailscale.com/net/netaddr"
"tailscale.com/net/tsaddr"
"tailscale.com/util/mak"
)
// forceAllIPv6Endpoints is a debug knob that when set forces the client to
// report all IPv6 endpoints rather than trim endpoints that are siblings on the
// same interface and subnet.
var forceAllIPv6Endpoints = envknob.RegisterBool("TS_DEBUG_FORCE_ALL_IPV6_ENDPOINTS")
// LoginEndpointForProxyDetermination is the URL used for testing
// which HTTP proxy the system should use.
var LoginEndpointForProxyDetermination = "https://controlplane.tailscale.com/"
func isUp(nif *net.Interface) bool { return nif.Flags&net.FlagUp != 0 }
func isLoopback(nif *net.Interface) bool { return nif.Flags&net.FlagLoopback != 0 }
func isProblematicInterface(nif *net.Interface) bool {
name := nif.Name
// Don't try to send disco/etc packets over zerotier; they effectively
// DoS each other by doing traffic amplification, both of them
// preferring/trying to use each other for transport. See:
// https://github.com/tailscale/tailscale/issues/1208
// TODO(https://github.com/tailscale/tailscale/issues/18824): maybe exclude
// "WireGuard tunnel 0" as well on Windows (NetBird), but the name seems too
// generic where there is not a platform standard (on Linux wt0 is at least
// explicitly different from the WireGuard conventional default of wg0).
if strings.HasPrefix(name, "zt") || name == "wt0" /* NetBird */ ||
(runtime.GOOS == "windows" && strings.Contains(name, "ZeroTier")) {
return true
}
return false
}
// LocalAddresses returns the machine's IP addresses, separated by
// whether they're loopback addresses. If there are no regular addresses
// it will return any IPv4 linklocal or IPv6 unique local addresses because we
// know of environments where these are used with NAT to provide connectivity.
func LocalAddresses() (regular, loopback []netip.Addr, err error) {
// TODO(crawshaw): don't serve interface addresses that we are routing
ifaces, err := netInterfaces()
if err != nil {
return nil, nil, err
}
var regular4, regular6, linklocal4, ula6 []netip.Addr
for _, iface := range ifaces {
stdIf := iface.Interface
if !isUp(stdIf) || isProblematicInterface(stdIf) {
// Skip down interfaces and ones that are
// problematic that we don't want to try to
// send Tailscale traffic over.
continue
}
ifcIsLoopback := isLoopback(stdIf)
addrs, err := iface.Addrs()
if err != nil {
return nil, nil, err
}
var subnets map[netip.Addr]int
for _, a := range addrs {
switch v := a.(type) {
case *net.IPNet:
ip, ok := netip.AddrFromSlice(v.IP)
if !ok {
continue
}
ip = ip.Unmap()
// TODO(apenwarr): don't special case cgNAT.
// In the general wireguard case, it might
// very well be something we can route to
// directly, because both nodes are
// behind the same CGNAT router.
if tsaddr.IsTailscaleIP(ip) {
continue
}
if ip.IsLoopback() || ifcIsLoopback {
loopback = append(loopback, ip)
} else if ip.IsLinkLocalUnicast() {
if ip.Is4() {
linklocal4 = append(linklocal4, ip)
}
// We know of no cases where the IPv6 fe80:: addresses
// are used to provide WAN connectivity. It is also very
// common for users to have no IPv6 WAN connectivity,
// but their OS supports IPv6 so they have an fe80::
// address. We don't want to report all of those
// IPv6 LL to Control.
} else if ip.Is6() && ip.IsPrivate() {
// Google Cloud Run uses NAT with IPv6 Unique
// Local Addresses to provide IPv6 connectivity.
ula6 = append(ula6, ip)
} else {
if ip.Is4() {
regular4 = append(regular4, ip)
} else {
curMask, _ := netip.AddrFromSlice(v.IP.Mask(v.Mask))
// Limit the number of addresses reported per subnet for
// IPv6, as we have seen some nodes with extremely large
// numbers of assigned addresses being carved out of
// same-subnet allocations.
if forceAllIPv6Endpoints() || subnets[curMask] < 2 {
regular6 = append(regular6, ip)
}
mak.Set(&subnets, curMask, subnets[curMask]+1)
}
}
}
}
}
if len(regular4) == 0 && len(regular6) == 0 {
// if we have no usable IP addresses then be willing to accept
// addresses we otherwise wouldn't, like:
// + 169.254.x.x (AWS Lambda and Azure App Services use NAT with these)
// + IPv6 ULA (Google Cloud Run uses these with address translation)
regular4 = linklocal4
regular6 = ula6
}
regular = append(regular4, regular6...)
sortIPs(regular)
sortIPs(loopback)
return regular, loopback, nil
}
func sortIPs(s []netip.Addr) {
sort.Slice(s, func(i, j int) bool { return s[i].Less(s[j]) })
}
// Interface is a wrapper around Go's net.Interface with some extra methods.
type Interface struct {
*net.Interface
AltAddrs []net.Addr // if non-nil, returned by Addrs
Desc string // extra description (used on Windows)
}
func (i Interface) IsLoopback() bool {
if i.Interface == nil {
return false
}
return isLoopback(i.Interface)
}
func (i Interface) IsUp() bool {
if i.Interface == nil {
return false
}
return isUp(i.Interface)
}
func (i Interface) Addrs() ([]net.Addr, error) {
if i.AltAddrs != nil {
return i.AltAddrs, nil
}
if i.Interface == nil {
return nil, nil
}
return i.Interface.Addrs()
}
// ForeachInterfaceAddress is a wrapper for GetList, then
// List.ForeachInterfaceAddress.
func ForeachInterfaceAddress(fn func(Interface, netip.Prefix)) error {
ifaces, err := GetInterfaceList()
if err != nil {
return err
}
return ifaces.ForeachInterfaceAddress(fn)
}
// ForeachInterfaceAddress calls fn for each interface in ifaces, with
// all its addresses. The IPPrefix's IP is the IP address assigned to
// the interface, and Bits are the subnet mask.
func (ifaces InterfaceList) ForeachInterfaceAddress(fn func(Interface, netip.Prefix)) error {
for _, iface := range ifaces {
addrs, err := iface.Addrs()
if err != nil {
return err
}
for _, a := range addrs {
switch v := a.(type) {
case *net.IPNet:
if pfx, ok := netaddr.FromStdIPNet(v); ok {
fn(iface, pfx)
}
case *net.IPAddr:
if ip, ok := netip.AddrFromSlice(v.IP); ok {
fn(iface, netip.PrefixFrom(ip, ip.BitLen()))
}
}
}
}
return nil
}
// ForeachInterface is a wrapper for GetList, then
// List.ForeachInterface.
func ForeachInterface(fn func(Interface, []netip.Prefix)) error {
ifaces, err := GetInterfaceList()
if err != nil {
return err
}
return ifaces.ForeachInterface(fn)
}
// ForeachInterface calls fn for each interface in ifaces, with
// all its addresses. The IPPrefix's IP is the IP address assigned to
// the interface, and Bits are the subnet mask.
func (ifaces InterfaceList) ForeachInterface(fn func(Interface, []netip.Prefix)) error {
for _, iface := range ifaces {
addrs, err := iface.Addrs()
if err != nil {
return err
}
var pfxs []netip.Prefix
for _, a := range addrs {
switch v := a.(type) {
case *net.IPNet:
if pfx, ok := netaddr.FromStdIPNet(v); ok {
pfxs = append(pfxs, pfx)
}
case *net.IPAddr:
if ip, ok := netip.AddrFromSlice(v.IP); ok {
pfxs = append(pfxs, netip.PrefixFrom(ip, ip.BitLen()))
}
}
}
sort.Slice(pfxs, func(i, j int) bool {
return pfxs[i].Addr().Less(pfxs[j].Addr())
})
fn(iface, pfxs)
}
return nil
}
// State is intended to store the state of the machine's network interfaces,
// routing table, and other network configuration.
// For now it's pretty basic.
type State struct {
// InterfaceIPs maps from an interface name to the IP addresses
// configured on that interface. Each address is represented as an
// IPPrefix, where the IP is the interface IP address and Bits is
// the subnet mask.
InterfaceIPs map[string][]netip.Prefix
Interface map[string]Interface
// HaveV6 is whether this machine has an IPv6 Global or Unique Local Address
// which might provide connectivity on a non-Tailscale interface that's up.
HaveV6 bool
// HaveV4 is whether the machine has some non-localhost,
// non-link-local IPv4 address on a non-Tailscale interface that's up.
HaveV4 bool
// IsExpensive is whether the current network interface is
// considered "expensive", which currently means LTE/etc
// instead of Wifi. This field is not populated by GetState.
IsExpensive bool
// DefaultRouteInterface is the interface name for the
// machine's default route.
//
// It is not yet populated on all OSes.
//
// When non-empty, its value is the map key into Interface and
// InterfaceIPs.
DefaultRouteInterface string
// HTTPProxy is the HTTP proxy to use, if any.
HTTPProxy string
// PAC is the URL to the Proxy Autoconfig URL, if applicable.
PAC string
}
func (s *State) String() string {
var sb strings.Builder
fmt.Fprintf(&sb, "interfaces.State{defaultRoute=%v ", s.DefaultRouteInterface)
if s.DefaultRouteInterface != "" {
if iface, ok := s.Interface[s.DefaultRouteInterface]; ok && iface.Desc != "" {
fmt.Fprintf(&sb, "(%s) ", iface.Desc)
}
}
sb.WriteString("ifs={")
var ifs []string
for k := range s.Interface {
if s.keepInterfaceInStringSummary(k) {
ifs = append(ifs, k)
}
}
sort.Slice(ifs, func(i, j int) bool {
upi, upj := s.Interface[ifs[i]].IsUp(), s.Interface[ifs[j]].IsUp()
if upi != upj {
// Up sorts before down.
return upi
}
return ifs[i] < ifs[j]
})
for i, ifName := range ifs {
if i > 0 {
sb.WriteString(" ")
}
iface := s.Interface[ifName]
if iface.Interface == nil {
fmt.Fprintf(&sb, "%s:nil", ifName)
continue
}
if !iface.IsUp() {
fmt.Fprintf(&sb, "%s:down", ifName)
continue
}
fmt.Fprintf(&sb, "%s:[", ifName)
needSpace := false
for _, pfx := range s.InterfaceIPs[ifName] {
a := pfx.Addr()
if a.IsMulticast() {
continue
}
fam := "4"
if a.Is6() {
fam = "6"
}
if needSpace {
sb.WriteString(" ")
}
needSpace = true
switch {
case a.IsLoopback():
fmt.Fprintf(&sb, "lo%s", fam)
case a.IsLinkLocalUnicast():
fmt.Fprintf(&sb, "llu%s", fam)
default:
fmt.Fprintf(&sb, "%s", pfx)
}
}
sb.WriteString("]")
}
sb.WriteString("}")
if s.IsExpensive {
sb.WriteString(" expensive")
}
if s.HTTPProxy != "" {
fmt.Fprintf(&sb, " httpproxy=%s", s.HTTPProxy)
}
if s.PAC != "" {
fmt.Fprintf(&sb, " pac=%s", s.PAC)
}
fmt.Fprintf(&sb, " v4=%v v6=%v}", s.HaveV4, s.HaveV6)
return sb.String()
}
// Equal reports whether s and s2 are exactly equal.
func (s *State) Equal(s2 *State) bool {
if s == nil && s2 == nil {
return true
}
if s == nil || s2 == nil {
return false
}
if s.HaveV6 != s2.HaveV6 ||
s.HaveV4 != s2.HaveV4 ||
s.IsExpensive != s2.IsExpensive ||
s.DefaultRouteInterface != s2.DefaultRouteInterface ||
s.HTTPProxy != s2.HTTPProxy ||
s.PAC != s2.PAC {
return false
}
// If s2 has more interfaces than s, it's not equal.
if len(s.Interface) != len(s2.Interface) || len(s.InterfaceIPs) != len(s2.InterfaceIPs) {
return false
}
// Now that we know that both states have the same number of
// interfaces, we can check each interface in s against s2. If it's not
// present or not exactly equal, then the states are not equal.
for iname, i := range s.Interface {
i2, ok := s2.Interface[iname]
if !ok {
return false
}
if !i.Equal(i2) {
return false
}
}
for iname, vv := range s.InterfaceIPs {
if !slices.Equal(vv, s2.InterfaceIPs[iname]) {
return false
}
}
return true
}
// HasIP reports whether any interface has the provided IP address.
func (s *State) HasIP(ip netip.Addr) bool {
if s == nil {
return false
}
for _, pv := range s.InterfaceIPs {
for _, p := range pv {
if p.Contains(ip) {
return true
}
}
}
return false
}
func (a Interface) Equal(b Interface) bool {
if (a.Interface == nil) != (b.Interface == nil) {
return false
}
if !(a.Desc == b.Desc && netAddrsEqual(a.AltAddrs, b.AltAddrs)) {
return false
}
if a.Interface != nil && !(a.Index == b.Index &&
a.MTU == b.MTU &&
a.Name == b.Name &&
a.Flags == b.Flags &&
bytes.Equal([]byte(a.HardwareAddr), []byte(b.HardwareAddr))) {
return false
}
return true
}
// InterfaceDiff returns a human-readable summary of the differences between s
// and s2 that would cause Equal to return false. It returns "" if the states
// are equal. This is useful for debugging false link change events where the
// State.String() output looks identical but Equal() returns false because it
// checks fields not shown in String() (like interface Flags, MTU, HardwareAddr).
func (s *State) InterfaceDiff(s2 *State) string {
if s == nil && s2 == nil {
return ""
}
if s == nil {
return "old=nil"
}
if s2 == nil {
return "new=nil"
}
var diffs []string
if s.HaveV6 != s2.HaveV6 {
diffs = append(diffs, fmt.Sprintf("HaveV6: %v->%v", s.HaveV6, s2.HaveV6))
}
if s.HaveV4 != s2.HaveV4 {
diffs = append(diffs, fmt.Sprintf("HaveV4: %v->%v", s.HaveV4, s2.HaveV4))
}
if s.IsExpensive != s2.IsExpensive {
diffs = append(diffs, fmt.Sprintf("IsExpensive: %v->%v", s.IsExpensive, s2.IsExpensive))
}
if s.DefaultRouteInterface != s2.DefaultRouteInterface {
diffs = append(diffs, fmt.Sprintf("DefaultRoute: %q->%q", s.DefaultRouteInterface, s2.DefaultRouteInterface))
}
if s.HTTPProxy != s2.HTTPProxy {
diffs = append(diffs, fmt.Sprintf("HTTPProxy: %q->%q", s.HTTPProxy, s2.HTTPProxy))
}
if s.PAC != s2.PAC {
diffs = append(diffs, fmt.Sprintf("PAC: %q->%q", s.PAC, s2.PAC))
}
if len(s.Interface) != len(s2.Interface) {
diffs = append(diffs, fmt.Sprintf("numInterfaces: %d->%d", len(s.Interface), len(s2.Interface)))
}
if len(s.InterfaceIPs) != len(s2.InterfaceIPs) {
diffs = append(diffs, fmt.Sprintf("numInterfaceIPs: %d->%d", len(s.InterfaceIPs), len(s2.InterfaceIPs)))
}
for iname, i := range s.Interface {
i2, ok := s2.Interface[iname]
if !ok {
diffs = append(diffs, fmt.Sprintf("if %s: removed", iname))
continue
}
if !i.Equal(i2) {
if i.Interface != nil && i2.Interface != nil {
if i.Flags != i2.Flags {
diffs = append(diffs, fmt.Sprintf("if %s flags: %v->%v", iname, i.Flags, i2.Flags))
}
if i.MTU != i2.MTU {
diffs = append(diffs, fmt.Sprintf("if %s MTU: %d->%d", iname, i.MTU, i2.MTU))
}
if i.Index != i2.Index {
diffs = append(diffs, fmt.Sprintf("if %s index: %d->%d", iname, i.Index, i2.Index))
}
if !bytes.Equal([]byte(i.HardwareAddr), []byte(i2.HardwareAddr)) {
diffs = append(diffs, fmt.Sprintf("if %s hwaddr: %v->%v", iname, i.HardwareAddr, i2.HardwareAddr))
}
}
if i.Desc != i2.Desc {
diffs = append(diffs, fmt.Sprintf("if %s desc: %q->%q", iname, i.Desc, i2.Desc))
}
}
}
for iname := range s2.Interface {
if _, ok := s.Interface[iname]; !ok {
diffs = append(diffs, fmt.Sprintf("if %s: added", iname))
}
}
for iname, vv := range s.InterfaceIPs {
vv2 := s2.InterfaceIPs[iname]
if !slices.Equal(vv, vv2) {
diffs = append(diffs, fmt.Sprintf("ips %s: %v->%v", iname, vv, vv2))
}
}
for iname := range s2.InterfaceIPs {
if _, ok := s.InterfaceIPs[iname]; !ok {
diffs = append(diffs, fmt.Sprintf("ips %s: added %v", iname, s2.InterfaceIPs[iname]))
}
}
return strings.Join(diffs, "; ")
}
func (s *State) HasPAC() bool { return s != nil && s.PAC != "" }
// AnyInterfaceUp reports whether any interface seems like it has Internet access.
func (s *State) AnyInterfaceUp() bool {
if runtime.GOOS == "js" || runtime.GOOS == "tamago" {
return true
}
return s != nil && (s.HaveV4 || s.HaveV6)
}
func netAddrsEqual(a, b []net.Addr) bool {
if len(a) != len(b) {
return false
}
for i, av := range a {
if av.Network() != b[i].Network() || av.String() != b[i].String() {
return false
}
}
return true
}
func hasTailscaleIP(pfxs []netip.Prefix) bool {
for _, pfx := range pfxs {
if tsaddr.IsTailscaleIP(pfx.Addr()) {
return true
}
}
return false
}
func isTailscaleInterface(name string, ips []netip.Prefix) bool {
// Sandboxed macOS and Plan9 (and anything else that explicitly calls SetTailscaleInterfaceProps).
tsIfName, err := TailscaleInterfaceName()
if err == nil {
// If we've been told the Tailscale interface name, use that.
return name == tsIfName
}
// The sandboxed app should (as of 1.92) set the tun interface name via SetTailscaleInterfaceProps
// early in the startup process. The non-sandboxed app does not.
// TODO (barnstar): If Wireguard created the tun device on darwin, it should know the name and it should
// be explicitly set instead checking addresses here.
if runtime.GOOS == "darwin" && strings.HasPrefix(name, "utun") && hasTailscaleIP(ips) {
return true
}
// Windows, Linux...
return name == "Tailscale" || // as it is on Windows
strings.HasPrefix(name, "tailscale") // TODO: use --tun flag value, etc; see TODO in method doc
}
// getPAC, if non-nil, returns the current PAC file URL.
var getPAC func() string
// getState returns the state of all the current machine's network interfaces.
//
// It does not set the returned State.IsExpensive. The caller can populate that.
//
// optTSInterfaceName is the name of the Tailscale interface, if known.
func getState(optTSInterfaceName string) (*State, error) {
s := &State{
InterfaceIPs: make(map[string][]netip.Prefix),
Interface: make(map[string]Interface),
}
if err := ForeachInterface(func(ni Interface, pfxs []netip.Prefix) {
isTSInterfaceName := optTSInterfaceName != "" && ni.Name == optTSInterfaceName
ifUp := ni.IsUp()
s.Interface[ni.Name] = ni
s.InterfaceIPs[ni.Name] = append(s.InterfaceIPs[ni.Name], pfxs...)
// Skip uninteresting interfaces
if IsInterestingInterface != nil && !IsInterestingInterface(ni, pfxs) {
return
}
if !ifUp || isTSInterfaceName || isTailscaleInterface(ni.Name, pfxs) {
return
}
for _, pfx := range pfxs {
if pfx.Addr().IsLoopback() {
continue
}
s.HaveV6 = s.HaveV6 || isUsableV6(pfx.Addr())
s.HaveV4 = s.HaveV4 || isUsableV4(pfx.Addr())
}
}); err != nil {
return nil, err
}
dr, _ := DefaultRoute()
s.DefaultRouteInterface = dr.InterfaceName
// Populate description (for Windows, primarily) if present.
if desc := dr.InterfaceDesc; desc != "" {
if iface, ok := s.Interface[dr.InterfaceName]; ok {
iface.Desc = desc
s.Interface[dr.InterfaceName] = iface
}
}
if buildfeatures.HasUseProxy && s.AnyInterfaceUp() {
req, err := http.NewRequest("GET", LoginEndpointForProxyDetermination, nil)
if err != nil {
return nil, err
}
if proxyFromEnv, ok := feature.HookProxyFromEnvironment.GetOk(); ok {
if u, err := proxyFromEnv(req); err == nil && u != nil {
s.HTTPProxy = u.String()
}
}
if getPAC != nil {
s.PAC = getPAC()
}
}
return s, nil
}
// HTTPOfListener returns the HTTP address to ln.
// If the listener is listening on the unspecified address, it
// it tries to find a reasonable interface address on the machine to use.
func HTTPOfListener(ln net.Listener) string {
ta, ok := ln.Addr().(*net.TCPAddr)
if !ok || !ta.IP.IsUnspecified() {
return fmt.Sprintf("http://%v/", ln.Addr())
}
var goodIP string
var privateIP string
ForeachInterfaceAddress(func(i Interface, pfx netip.Prefix) {
ip := pfx.Addr()
if ip.IsPrivate() {
if privateIP == "" {
privateIP = ip.String()
}
return
}
goodIP = ip.String()
})
if privateIP != "" {
goodIP = privateIP
}
if goodIP != "" {
return fmt.Sprintf("http://%v/", net.JoinHostPort(goodIP, fmt.Sprint(ta.Port)))
}
return fmt.Sprintf("http://localhost:%v/", fmt.Sprint(ta.Port))
}
// likelyHomeRouterIP, if present, is a platform-specific function that is used
// to determine the likely home router IP of the current system. The signature
// of this function is:
//
// func() (homeRouter, localAddr netip.Addr, ok bool)
//
// It should return a homeRouter IP and ok=true, or no homeRouter IP and
// ok=false. Optionally, an implementation can return the "self" IP address as
// well, which will be used instead of attempting to determine it by reading
// the system's interfaces.
var likelyHomeRouterIP func() (netip.Addr, netip.Addr, bool)
// For debugging the new behaviour where likelyHomeRouterIP can return the
// "self" IP; should remove after we're confidant this won't cause issues.
var disableLikelyHomeRouterIPSelf = envknob.RegisterBool("TS_DEBUG_DISABLE_LIKELY_HOME_ROUTER_IP_SELF")
// LikelyHomeRouterIP returns the likely IP of the residential router,
// which will always be an IPv4 private address, if found.
// In addition, it returns the IP address of the current machine on
// the LAN using that gateway.
// This is used as the destination for UPnP, NAT-PMP, PCP, etc queries.
func LikelyHomeRouterIP() (gateway, myIP netip.Addr, ok bool) {
if !buildfeatures.HasPortMapper {
return
}
// If we don't have a way to get the home router IP, then we can't do
// anything; just return.
if likelyHomeRouterIP == nil {
return
}
// Get the gateway next; if that fails, we can't continue.
gateway, myIP, ok = likelyHomeRouterIP()
if !ok {
return
}
// If the platform-specific implementation returned a valid myIP, then
// we can return it as-is without needing to iterate through all
// interface addresses.
if disableLikelyHomeRouterIPSelf() {
myIP = netip.Addr{}
}
if myIP.IsValid() {
return
}
// The platform-specific implementation didn't return a valid myIP;
// iterate through all interfaces and try to find the correct one.
ForeachInterfaceAddress(func(i Interface, pfx netip.Prefix) {
if !i.IsUp() {
// Skip interfaces that aren't up.
return
} else if myIP.IsValid() {
// We already have a valid self IP; skip this one.
return
}
ip := pfx.Addr()
if !ip.IsValid() || !ip.Is4() {
// Skip IPs that aren't valid or aren't IPv4, since we
// always return an IPv4 address.
return
}
// If this prefix ("interface") doesn't contain the gateway,
// then we skip it; this can happen if we have multiple valid
// interfaces and the interface with the route to the internet
// is ordered after another valid+running interface.
if !pfx.Contains(gateway) {
return
}
if gateway.IsPrivate() && ip.IsPrivate() {
myIP = ip
ok = true
return
}
})
return gateway, myIP, myIP.IsValid()
}
// isUsableV4 reports whether ip is a usable IPv4 address which could
// conceivably be used to get Internet connectivity. Globally routable and
// private IPv4 addresses are always Usable, and link local 169.254.x.x
// addresses are in some environments.
func isUsableV4(ip netip.Addr) bool {
if !ip.Is4() || ip.IsLoopback() {
return false
}
if ip.IsLinkLocalUnicast() {
switch hostinfo.GetEnvType() {
case hostinfo.AWSLambda:
return true
case hostinfo.AzureAppService:
return true
default:
return false
}
}
return true
}
// isUsableV6 reports whether ip is a usable IPv6 address which could
// conceivably be used to get Internet connectivity. Globally routable
// IPv6 addresses are always Usable, and Unique Local Addresses
// (fc00::/7) are in some environments used with address translation.
func isUsableV6(ip netip.Addr) bool {
return v6Global1.Contains(ip) ||
(ip.Is6() && ip.IsPrivate() && !tsaddr.TailscaleULARange().Contains(ip))
}
var (
v6Global1 = netip.MustParsePrefix("2000::/3")
)
// keepInterfaceInStringSummary reports whether the named interface should be included
// in the String method's summary string.
func (s *State) keepInterfaceInStringSummary(ifName string) bool {
iface, ok := s.Interface[ifName]
if !ok || iface.Interface == nil {
return false
}
if ifName == s.DefaultRouteInterface {
return true
}
up := iface.IsUp()
for _, p := range s.InterfaceIPs[ifName] {
a := p.Addr()
if a.IsLinkLocalUnicast() || a.IsLoopback() {
continue
}
if up || a.IsGlobalUnicast() || a.IsPrivate() {
return true
}
}
return false
}
var altNetInterfaces func() ([]Interface, error)
// RegisterInterfaceGetter sets the function that's used to query
// the system network interfaces.
func RegisterInterfaceGetter(getInterfaces func() ([]Interface, error)) {
altNetInterfaces = getInterfaces
}
// HookInterfacesFallback is a hook for the androidbin feature to
// provide a last-resort interface list when the OS denies
// net.Interfaces to the process, as Android does to app UIDs. It is
// consulted only when no getter was registered with
// RegisterInterfaceGetter and net.Interfaces failed; if it returns an
// error, the net.Interfaces error is returned instead.
var HookInterfacesFallback feature.Hook[func() ([]Interface, error)]
// InterfaceList is a list of interfaces on the machine.
type InterfaceList []Interface
// GetInterfaceList returns the list of interfaces on the machine.
func GetInterfaceList() (InterfaceList, error) {
return netInterfaces()
}
// netInterfaces is a wrapper around the standard library's net.Interfaces
// that returns a []*Interface instead of a []net.Interface.
// It exists because Android SDK 30 no longer permits Go's net.Interfaces
// to work (Issue 2293); this wrapper lets us the Android app register
// an alternate implementation.
func netInterfaces() ([]Interface, error) {
if altNetInterfaces != nil {
return altNetInterfaces()
}
ifs, err := net.Interfaces()
if err != nil {
if f, ok := HookInterfacesFallback.GetOk(); ok {
if ret, err := f(); err == nil {
return ret, nil
}
}
return nil, err
}
ret := make([]Interface, len(ifs))
for i := range ifs {
ret[i].Interface = &ifs[i]
}
return ret, nil
}
// DefaultRouteDetails are the details about a default route returned
// by DefaultRoute.
type DefaultRouteDetails struct {
// InterfaceName is the interface name. It must always be populated.
// It's like "eth0" (Linux), "Ethernet 2" (Windows), "en0" (macOS).
InterfaceName string
// InterfaceDesc is populated on Windows at least. It's a
// longer description, like "Red Hat VirtIO Ethernet Adapter".
InterfaceDesc string
// InterfaceIndex is like net.Interface.Index.
// Zero means not populated.
InterfaceIndex int
// TODO(bradfitz): break this out into v4-vs-v6 once that need arises.
}
// DefaultRouteInterface is like DefaultRoute but only returns the
// interface name.
func DefaultRouteInterface() (string, error) {
dr, err := DefaultRoute()
if err != nil {
return "", err
}
return dr.InterfaceName, nil
}
// DefaultRoute returns details of the network interface that owns
// the default route, not including any tailscale interfaces.
func DefaultRoute() (DefaultRouteDetails, error) {
return defaultRoute()
}
// HasCGNATInterface reports whether there are any non-Tailscale interfaces that
// use a CGNAT IP range.
func (m *Monitor) HasCGNATInterface() (bool, error) {
hasCGNATInterface := false
cgnatRange := tsaddr.CGNATRange()
err := ForeachInterface(func(i Interface, pfxs []netip.Prefix) {
if hasCGNATInterface || !i.IsUp() || isTailscaleInterface(i.Name, pfxs) {
return
}
if slices.ContainsFunc(pfxs, cgnatRange.Overlaps) {
hasCGNATInterface = true
}
})
if err != nil {
return false, err
}
return hasCGNATInterface, nil
}
var interfaceDebugExtras func(ifIndex int) (string, error)
// InterfaceDebugExtras returns extra debugging information about an interface
// if any (an empty string will be returned if there are no additional details).
// Formatting is platform-dependent and should not be parsed.
func InterfaceDebugExtras(ifIndex int) (string, error) {
if interfaceDebugExtras != nil {
return interfaceDebugExtras(ifIndex)
}
return "", nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package netns contains the common code for using the Go net package
// in a logical "network namespace" to avoid routing loops where
// Tailscale-created packets would otherwise loop back through
// Tailscale routes.
//
// Despite the name netns, the exact mechanism used differs by
// operating system, and perhaps even by version of the OS.
//
// The netns package also handles connecting via SOCKS proxies when
// configured by the environment.
package netns
import (
"context"
"net"
"net/netip"
"runtime"
"sync/atomic"
"tailscale.com/net/netknob"
"tailscale.com/net/netmon"
"tailscale.com/types/logger"
)
var disabled atomic.Bool
// SetEnabled enables or disables netns for the process.
// It defaults to being enabled.
func SetEnabled(on bool) {
disabled.Store(!on)
}
var bindToInterfaceByRoute atomic.Bool
// SetBindToInterfaceByRoute enables or disables whether we use the system's
// route information to bind to a particular interface. It is the same as
// setting the TS_BIND_TO_INTERFACE_BY_ROUTE.
//
// Currently, this only changes the behaviour on macOS and Windows.
func SetBindToInterfaceByRoute(logf logger.Logf, v bool) {
if bindToInterfaceByRoute.Swap(v) != v {
logf("netns: bindToInterfaceByRoute changed to %v", v)
}
}
// When true, disableAndroidBindToActiveNetwork skips binding sockets to the currently
// active network on Android.
var disableAndroidBindToActiveNetwork atomic.Bool
// SetDisableAndroidBindToActiveNetwork disables the default behavior of binding
// sockets to the currently active network on Android.
func SetDisableAndroidBindToActiveNetwork(logf logger.Logf, v bool) {
if runtime.GOOS == "android" && disableAndroidBindToActiveNetwork.Swap(v) != v {
logf("netns: disableAndroidBindToActiveNetwork changed to %v", v)
}
}
var disableBindConnToInterface atomic.Bool
// SetDisableBindConnToInterface disables the (normal) behavior of binding
// connections to the default network interface on Darwin nodes.
//
// Unless you intended to disable this for tailscaled on macos (which is likely
// to break things), you probably wanted to set
// SetDisableBindConnToInterfaceAppleExt which will disable explicit interface
// binding only when tailscaled is running inside a network extension process.
func SetDisableBindConnToInterface(logf logger.Logf, v bool) {
if disableBindConnToInterface.Swap(v) != v {
logf("netns: disableBindConnToInterface changed to %v", v)
}
}
var disableBindConnToInterfaceAppleExt atomic.Bool
// SetDisableBindConnToInterfaceAppleExt disables the (normal) behavior of binding
// connections to the default network interface but only on Apple clients where
// tailscaled is running inside a network extension.
func SetDisableBindConnToInterfaceAppleExt(logf logger.Logf, v bool) {
if runtime.GOOS == "darwin" && disableBindConnToInterfaceAppleExt.Swap(v) != v {
logf("netns: disableBindConnToInterfaceAppleExt changed to %v", v)
}
}
// Listener returns a new net.Listener with its Control hook func
// initialized as necessary to run in logical network namespace that
// doesn't route back into Tailscale.
func Listener(logf logger.Logf, netMon *netmon.Monitor) *net.ListenConfig {
if netMon == nil {
panic("netns.Listener called with nil netMon")
}
if disabled.Load() {
return new(net.ListenConfig)
}
return &net.ListenConfig{Control: control(logf, netMon)}
}
// NewDialer returns a new Dialer using a net.Dialer with its Control
// hook func initialized as necessary to run in a logical network
// namespace that doesn't route back into Tailscale. It also handles
// using a SOCKS if configured in the environment with ALL_PROXY.
func NewDialer(logf logger.Logf, netMon *netmon.Monitor) Dialer {
if netMon == nil {
panic("netns.NewDialer called with nil netMon")
}
return FromDialer(logf, netMon, &net.Dialer{
KeepAlive: netknob.PlatformTCPKeepAlive(),
})
}
// FromDialer returns sets d.Control as necessary to run in a logical
// network namespace that doesn't route back into Tailscale. It also
// handles using a SOCKS if configured in the environment with
// ALL_PROXY.
func FromDialer(logf logger.Logf, netMon *netmon.Monitor, d *net.Dialer) Dialer {
if netMon == nil {
panic("netns.FromDialer called with nil netMon")
}
if disabled.Load() {
return d
}
d.Control = control(logf, netMon)
if wrapDialer != nil {
return wrapDialer(d)
}
return d
}
// IsSOCKSDialer reports whether d is SOCKS-proxying dialer as returned by
// NewDialer or FromDialer.
func IsSOCKSDialer(d Dialer) bool {
if d == nil {
return false
}
_, ok := d.(*net.Dialer)
return !ok
}
// wrapDialer, if non-nil, specifies a function to wrap a dialer in a
// SOCKS-using dialer. It's set conditionally by socks.go.
var wrapDialer func(Dialer) Dialer
// Dialer is the interface for a dialer that can dial with or without a context.
// It's the type implemented both by net.Dialer and the Go SOCKS dialer.
type Dialer interface {
Dial(network, address string) (net.Conn, error)
DialContext(ctx context.Context, network, address string) (net.Conn, error)
}
func isLocalhost(addr string) bool {
host, _, err := net.SplitHostPort(addr)
if err != nil {
// error means the string didn't contain a port number, so use the string directly
host = addr
}
// localhost6 == RedHat /etc/hosts for ::1, ip6-loopback & ip6-localhost == Debian /etc/hosts for ::1
if host == "localhost" || host == "localhost6" || host == "ip6-loopback" || host == "ip6-localhost" {
return true
}
ip, _ := netip.ParseAddr(host)
return ip.IsLoopback()
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && !android
package netns
import (
"fmt"
"net"
"os"
"sync"
"syscall"
"golang.org/x/sys/unix"
"tailscale.com/envknob"
"tailscale.com/net/netmon"
"tailscale.com/tsconst"
"tailscale.com/types/logger"
)
// socketMarkWorksOnce is the sync.Once & cached value for useSocketMark.
var socketMarkWorksOnce struct {
sync.Once
v bool
}
// socketMarkWorks returns whether SO_MARK works.
func socketMarkWorks() bool {
addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:1")
if err != nil {
return true // unsure, returning true does the least harm.
}
sConn, err := net.DialUDP("udp", nil, addr)
if err != nil {
return true // unsure, return true
}
defer sConn.Close()
rConn, err := sConn.SyscallConn()
if err != nil {
return true // unsure, return true
}
var sockErr error
err = rConn.Control(func(fd uintptr) {
sockErr = setBypassMark(fd)
})
if err != nil || sockErr != nil {
return false
}
return true
}
var forceBindToDevice = envknob.RegisterBool("TS_FORCE_LINUX_BIND_TO_DEVICE")
// UseSocketMark reports whether SO_MARK is in use.
// If it doesn't, we have to use SO_BINDTODEVICE on our sockets instead.
func UseSocketMark() bool {
if forceBindToDevice() {
return false
}
socketMarkWorksOnce.Do(func() {
socketMarkWorksOnce.v = socketMarkWorks()
})
return socketMarkWorksOnce.v
}
// ignoreErrors returns true if we should ignore setsocketopt errors in
// this instance.
func ignoreErrors() bool {
if os.Getuid() != 0 {
// only root can manipulate these socket flags
return true
}
return false
}
func control(logger.Logf, *netmon.Monitor) func(network, address string, c syscall.RawConn) error {
return controlC
}
// controlC marks c as necessary to dial in a separate network namespace.
//
// It's intentionally the same signature as net.Dialer.Control
// and net.ListenConfig.Control.
func controlC(network, address string, c syscall.RawConn) error {
if isLocalhost(address) {
// Don't bind to an interface for localhost connections.
return nil
}
var sockErr error
err := c.Control(func(fd uintptr) {
if UseSocketMark() {
sockErr = setBypassMark(fd)
} else {
sockErr = bindToDevice(fd)
}
})
if err != nil {
return fmt.Errorf("RawConn.Control on %T: %w", c, err)
}
if sockErr != nil && ignoreErrors() {
// TODO(bradfitz): maybe log once? probably too spammy for e.g. CLI tools like tailscale netcheck.
return nil
}
return sockErr
}
func setBypassMark(fd uintptr) error {
if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_MARK, tsconst.LinuxBypassMarkNum); err != nil {
return fmt.Errorf("setting SO_MARK bypass: %w", err)
}
return nil
}
func bindToDevice(fd uintptr) error {
ifc, err := netmon.DefaultRouteInterface()
if err != nil {
// Make sure we bind to *some* interface,
// or we could get a routing loop.
// "lo" is always wrong, but if we don't have
// a default route anyway, it doesn't matter.
ifc = "lo"
}
if err := unix.SetsockoptString(int(fd), unix.SOL_SOCKET, unix.SO_BINDTODEVICE, ifc); err != nil {
return fmt.Errorf("setting SO_BINDTODEVICE: %w", err)
}
return nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ios && !js && !android && !ts_omit_useproxy
package netns
import "golang.org/x/net/proxy"
func init() {
wrapDialer = wrapSocks
}
func wrapSocks(d Dialer) Dialer {
if cd, ok := proxy.FromEnvironmentUsing(d).(Dialer); ok {
return cd
}
return d
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package netutil
import (
"errors"
"net"
"net/netip"
)
// DefaultInterfacePortable looks up the current default interface using a portable lookup method that
// works on most systems with a BSD style socket interface.
//
// Returns the interface name and IP address of the default route interface.
//
// If the default cannot be determined, an error is returned.
// Requires that there is a route on the system servicing UDP IPv4.
func DefaultInterfacePortable() (string, netip.Addr, error) {
// Note: UDP dial just performs a connect(2), and doesn't actually send a packet.
c, err := net.Dial("udp4", "8.8.8.8:53")
if err != nil {
return "", netip.Addr{}, err
}
laddr := c.LocalAddr().(*net.UDPAddr)
c.Close()
ifs, err := net.Interfaces()
if err != nil {
return "", netip.Addr{}, err
}
var (
iface *net.Interface
ipnet *net.IPNet
)
for _, ifc := range ifs {
addrs, err := ifc.Addrs()
if err != nil {
return "", netip.Addr{}, err
}
for _, addr := range addrs {
if ipn, ok := addr.(*net.IPNet); ok {
if ipn.Contains(laddr.IP) {
if ipnet == nil {
ipnet = ipn
iface = &ifc
} else {
newSize, _ := ipn.Mask.Size()
oldSize, _ := ipnet.Mask.Size()
if newSize > oldSize {
ipnet = ipn
iface = &ifc
}
}
}
}
}
}
if iface == nil {
return "", netip.Addr{}, errors.New("no default interface")
}
return iface.Name, laddr.AddrPort().Addr(), nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package netutil
import (
"bytes"
"fmt"
"io"
"net/netip"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"tailscale.com/net/netmon"
)
// protocolsRequiredForForwarding reports whether IPv4 and/or IPv6 protocols are
// required to forward the specified routes.
// The state param must be specified.
func protocolsRequiredForForwarding(routes []netip.Prefix, state *netmon.State) (v4, v6 bool) {
if len(routes) == 0 {
// Nothing to route, so no need to warn.
return false, false
}
localIPs := make(map[netip.Addr]bool)
for _, addrs := range state.InterfaceIPs {
for _, pfx := range addrs {
localIPs[pfx.Addr()] = true
}
}
for _, r := range routes {
// It's possible to advertise a route to one of the local
// machine's local IPs. IP forwarding isn't required for this
// to work, so we shouldn't warn for such exports.
if r.IsSingleIP() && localIPs[r.Addr()] {
continue
}
if r.Addr().Is4() {
v4 = true
} else {
v6 = true
}
}
return v4, v6
}
// CheckIPForwarding reports whether IP forwarding is enabled correctly
// for subnet routing and exit node functionality on any interface.
// The state param must not be nil.
// The routes should only be advertised routes, and should not contain the
// nodes Tailscale IPs.
// It returns an error if it is unable to determine if IP forwarding is enabled.
// It returns a warning describing configuration issues if IP forwarding is
// non-functional or partly functional.
func CheckIPForwarding(routes []netip.Prefix, state *netmon.State) (warn, err error) {
if runtime.GOOS != "linux" {
switch runtime.GOOS {
case "freebsd":
if state == nil {
return nil, fmt.Errorf("Couldn't check system's IP forwarding configuration; no link state")
}
return checkIPForwardingFreeBSD(routes, state)
case "dragonfly", "netbsd", "openbsd":
return fmt.Errorf("Subnet routing and exit nodes only work with additional manual configuration on %v, and is not currently officially supported.", runtime.GOOS), nil
case "illumos", "solaris":
_, err := ipForwardingEnabledSunOS(ipv4, "")
if err != nil {
return nil, fmt.Errorf("Couldn't check system's IP forwarding configuration, subnet routing/exit nodes may not work: %w%s", err, "")
}
}
return nil, nil
}
if state == nil {
return nil, fmt.Errorf("Couldn't check system's IP forwarding configuration; no link state")
}
const kbLink = "\nSee https://tailscale.com/s/ip-forwarding"
wantV4, wantV6 := protocolsRequiredForForwarding(routes, state)
if !wantV4 && !wantV6 {
return nil, nil
}
v4e, err := ipForwardingEnabledLinux(ipv4, "")
if err != nil {
return nil, fmt.Errorf("Couldn't check system's IP forwarding configuration, subnet routing/exit nodes may not work: %w%s", err, kbLink)
}
v6e, err := ipForwardingEnabledLinux(ipv6, "")
if err != nil {
return nil, fmt.Errorf("Couldn't check system's IP forwarding configuration, subnet routing/exit nodes may not work: %w%s", err, kbLink)
}
if v4e && v6e {
// IP forwarding is enabled systemwide, all is well.
return nil, nil
}
if !wantV4 {
if !v6e {
return nil, fmt.Errorf("IPv6 forwarding is disabled, subnet routing/exit nodes may not work.%s", kbLink)
}
return nil, nil
}
// IP forwarding isn't enabled globally, but it might be enabled
// on a per-interface basis. Check if it's on for all interfaces,
// and warn appropriately if it's not.
// Note: you might be wondering why we check only the state of
// ipv6.conf.all.forwarding, rather than per-interface forwarding
// configuration. According to kernel documentation, it seems
// that to actually forward packets, you need to enable
// forwarding globally, and the per-interface forwarding
// setting only alters other things such as how router
// advertisements are handled. The kernel itself warns that
// enabling forwarding per-interface and not globally will
// probably not work, so I feel okay calling those configs
// broken until we have proof otherwise.
var (
anyEnabled bool
warnings []string
)
if wantV6 && !v6e {
warnings = append(warnings, "IPv6 forwarding is disabled.")
}
for _, iface := range state.Interface {
if iface.Name == "lo" {
continue
}
v4e, err := ipForwardingEnabledLinux(ipv4, iface.Name)
if err != nil {
return nil, fmt.Errorf("Couldn't check system's IP forwarding configuration, subnet routing/exit nodes may not work: %w%s", err, kbLink)
} else if !v4e {
warnings = append(warnings, fmt.Sprintf("Traffic received on %s won't be forwarded (%s disabled)", iface.Name, ipForwardSysctlKey(dotFormat, ipv4, iface.Name)))
} else {
anyEnabled = true
}
}
if !anyEnabled {
// IP forwarding is completely disabled, just say that rather
// than enumerate all the interfaces on the system.
return fmt.Errorf("IP forwarding is disabled, subnet routing/exit nodes will not work.%s", kbLink), nil
}
if len(warnings) > 0 {
// If partially enabled, enumerate the bits that won't work.
return fmt.Errorf("%s\nSubnet routes and exit nodes may not work correctly.%s", strings.Join(warnings, "\n"), kbLink), nil
}
return nil, nil
}
// ipForwardSysctlKey returns the sysctl key for the given protocol and iface.
// When the dotFormat parameter is true the output is formatted as `net.ipv4.ip_forward`,
// else it is `net/ipv4/ip_forward`
func ipForwardSysctlKey(format sysctlFormat, p protocol, iface string) string {
if iface == "" {
if format == dotFormat {
if p == ipv4 {
return "net.ipv4.ip_forward"
}
return "net.ipv6.conf.all.forwarding"
}
if p == ipv4 {
return "net/ipv4/ip_forward"
}
return "net/ipv6/conf/all/forwarding"
}
var k string
if p == ipv4 {
k = "net/ipv4/conf/%s/forwarding"
} else {
k = "net/ipv6/conf/%s/forwarding"
}
if format == dotFormat {
// Swap the delimiters.
iface = strings.ReplaceAll(iface, ".", "/")
k = strings.ReplaceAll(k, "/", ".")
}
return fmt.Sprintf(k, iface)
}
type sysctlFormat int
const (
dotFormat sysctlFormat = iota
slashFormat
)
type protocol int
const (
ipv4 protocol = iota
ipv6
)
// ipForwardingEnabledLinux reports whether the IP Forwarding is enabled for the
// given interface.
// The iface param determines which interface to check against, "" means to check
// global config.
// This is Linux-specific: it only reads from /proc/sys and doesn't shell out to
// sysctl (which on Linux just reads from /proc/sys anyway).
func ipForwardingEnabledLinux(p protocol, iface string) (bool, error) {
k := ipForwardSysctlKey(slashFormat, p, iface)
// Open k under /proc/sys with os.OpenInRoot so a ".." or symlinked
// component in the interface name can't read outside /proc/sys. It's
// openat-based, so it's also TOCTOU-resistant. See https://go.dev/blog/osroot.
f, err := os.OpenInRoot("/proc/sys", k)
if err != nil {
if os.IsNotExist(err) {
// If IPv6 is disabled, sysctl keys like "net.ipv6.conf.all.forwarding" just don't
// exist on disk. But first diagnose whether procfs is even mounted before assuming
// absence means false.
if fi, err := os.Stat("/proc/sys"); err != nil {
return false, fmt.Errorf("failed to check sysctl %v; no procfs? %w", k, err)
} else if !fi.IsDir() {
return false, fmt.Errorf("failed to check sysctl %v; /proc/sys isn't a directory, is %v", k, fi.Mode())
}
return false, nil
}
return false, err
}
defer f.Close()
bs, err := io.ReadAll(f)
if err != nil {
return false, err
}
val, err := strconv.ParseInt(string(bytes.TrimSpace(bs)), 10, 32)
if err != nil {
return false, fmt.Errorf("couldn't parse %s: %w", k, err)
}
// 0 = disabled, 1 = enabled, 2 = enabled (but uncommon)
// https://github.com/tailscale/tailscale/issues/8375
if val < 0 || val > 2 {
return false, fmt.Errorf("unexpected value %d for %s", val, k)
}
on := val == 1 || val == 2
return on, nil
}
// checkIPForwardingFreeBSD reports whether IP forwarding is enabled for the
// protocols required by routes.
//
// Unlike Linux, FreeBSD has no per-interface forwarding knob: net.inet.ip.forwarding
// and net.inet6.ip6.forwarding are global, so we only check those.
func checkIPForwardingFreeBSD(routes []netip.Prefix, state *netmon.State) (warn, err error) {
const kbLink = "\nSee https://tailscale.com/s/ip-forwarding"
wantV4, wantV6 := protocolsRequiredForForwarding(routes, state)
if !wantV4 && !wantV6 {
return nil, nil
}
var warnings []string
if wantV4 {
on, err := ipForwardingEnabledFreeBSD(ipv4)
if err != nil {
return nil, fmt.Errorf("Couldn't check system's IP forwarding configuration, subnet routing/exit nodes may not work: %w%s", err, kbLink)
}
if !on {
warnings = append(warnings, "IPv4 forwarding is disabled.")
}
}
if wantV6 {
on, err := ipForwardingEnabledFreeBSD(ipv6)
if err != nil {
return nil, fmt.Errorf("Couldn't check system's IP forwarding configuration, subnet routing/exit nodes may not work: %w%s", err, kbLink)
}
if !on {
warnings = append(warnings, "IPv6 forwarding is disabled.")
}
}
if len(warnings) > 0 {
return fmt.Errorf("%s\nSubnet routes and exit nodes may not work correctly.%s", strings.Join(warnings, "\n"), kbLink), nil
}
return nil, nil
}
// ipForwardingEnabledFreeBSD reports whether IP forwarding is enabled globally
// for the given protocol.
func ipForwardingEnabledFreeBSD(p protocol) (bool, error) {
k := "net.inet.ip.forwarding"
if p == ipv6 {
k = "net.inet6.ip6.forwarding"
}
bs, err := exec.Command("sysctl", "-n", k).Output()
if err != nil {
return false, fmt.Errorf("couldn't check sysctl %s: %w", k, err)
}
val, err := strconv.ParseInt(string(bytes.TrimSpace(bs)), 10, 32)
if err != nil {
return false, fmt.Errorf("couldn't parse %s: %w", k, err)
}
return val == 1, nil
}
func ipForwardingEnabledSunOS(p protocol, iface string) (bool, error) {
var proto string
if p == ipv4 {
proto = "ipv4"
} else if p == ipv6 {
proto = "ipv6"
} else {
return false, fmt.Errorf("unknown protocol")
}
ipadmCmd := "\"ipadm show-prop " + proto + " -p forwarding -o CURRENT -c\""
bs, err := exec.Command("ipadm", "show-prop", proto, "-p", "forwarding", "-o", "CURRENT", "-c").Output()
if err != nil {
return false, fmt.Errorf("couldn't check %s (%v).\nSubnet routes won't work without IP forwarding.", ipadmCmd, err)
}
if string(bs) != "on\n" {
return false, fmt.Errorf("IP forwarding is set to off. Subnet routes won't work. Try 'routeadm -u -e %s-forwarding'", proto)
}
return true, nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package netutil contains misc shared networking code & types.
package netutil
import (
"bufio"
"io"
"net"
"net/http"
"time"
"tailscale.com/syncs"
)
// NewDefaultTransport returns a new *http.Transport configured identically to
// the Go standard library's http.DefaultTransport.
//
// Unlike http.DefaultTransport.(*http.Transport).Clone(), it does not panic
// when a program has replaced http.DefaultTransport with a RoundTripper that
// is not a *http.Transport. In the common case (the global is still the
// standard *http.Transport) it returns a clone of it, preserving the existing
// behavior exactly; otherwise it returns a fresh transport mirroring the
// stdlib defaults.
func NewDefaultTransport() *http.Transport {
if tr, ok := http.DefaultTransport.(*http.Transport); ok {
return tr.Clone()
}
// Values copied verbatim from net/http's DefaultTransport.
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
}
// NewOneConnListener returns a net.Listener that returns c on its
// first Accept and EOF thereafter.
//
// The returned Listener's Addr method returns addr if non-nil. If nil,
// Addr returns a non-nil dummy address instead.
func NewOneConnListener(c net.Conn, addr net.Addr) net.Listener {
if addr == nil {
addr = dummyAddr("one-conn-listener")
}
return &oneConnListener{
addr: addr,
conn: c,
}
}
type oneConnListener struct {
addr net.Addr
mu syncs.Mutex
conn net.Conn
}
func (ln *oneConnListener) Accept() (c net.Conn, err error) {
ln.mu.Lock()
defer ln.mu.Unlock()
c = ln.conn
if c == nil {
err = io.EOF
return
}
err = nil
ln.conn = nil
return
}
func (ln *oneConnListener) Addr() net.Addr { return ln.addr }
func (ln *oneConnListener) Close() error {
ln.Accept() // guarantee future call returns io.EOF
return nil
}
type dummyAddr string
func (a dummyAddr) Network() string { return string(a) }
func (a dummyAddr) String() string { return string(a) }
// NewDrainBufConn returns a net.Conn conditionally wrapping c,
// prefixing any bytes that are in initialReadBuf, which may be nil.
func NewDrainBufConn(c net.Conn, initialReadBuf *bufio.Reader) net.Conn {
r := initialReadBuf
if r != nil && r.Buffered() == 0 {
r = nil
}
return &drainBufConn{c, r}
}
// drainBufConn is a net.Conn with an initial bunch of bytes in a
// bufio.Reader. Read drains the bufio.Reader until empty, then passes
// through subsequent reads to the Conn directly.
type drainBufConn struct {
net.Conn
r *bufio.Reader
}
func (b *drainBufConn) Read(bs []byte) (int, error) {
if b.r == nil {
return b.Conn.Read(bs)
}
n, err := b.r.Read(bs)
if b.r.Buffered() == 0 {
b.r = nil
}
return n, err
}
// NewAltReadWriteCloserConn returns a net.Conn that wraps rwc (for
// Read, Write, and Close) and c (for all other methods).
func NewAltReadWriteCloserConn(rwc io.ReadWriteCloser, c net.Conn) net.Conn {
return wrappedConn{c, rwc}
}
type wrappedConn struct {
net.Conn
rwc io.ReadWriteCloser
}
func (w wrappedConn) Read(bs []byte) (int, error) {
return w.rwc.Read(bs)
}
func (w wrappedConn) Write(bs []byte) (int, error) {
return w.rwc.Write(bs)
}
func (w wrappedConn) Close() error {
return w.rwc.Close()
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package netutil
import (
"encoding/binary"
"fmt"
"net/netip"
"sort"
"strings"
"tailscale.com/net/tsaddr"
)
// ValidateViaPrefix checks that the IP prefix is a valid 4via6 route.
// It verifies that the prefix is in the Tailscale via range, has a prefix
// length between /96 and /128, and that the embedded site ID is in the
// range 0–65535.
func ValidateViaPrefix(ipp netip.Prefix) error {
if !tsaddr.IsViaPrefix(ipp) {
return fmt.Errorf("%v is not a 4-in-6 prefix", ipp)
}
if ipp.Bits() < (128 - 32) {
return fmt.Errorf("%v 4-in-6 prefix must be at least a /%v", ipp, 128-32)
}
a := ipp.Addr().As16()
// The first 64 bits of a are the via prefix.
// The next 32 bits are the "site ID".
// The last 32 bits are the IPv4.
//
// We used to only allow advertising site IDs from 0-255, but we have
// since relaxed this (as of 2024-01) to allow IDs from 0-65535.
siteID := binary.BigEndian.Uint32(a[8:12])
if siteID > 0xFFFF {
return fmt.Errorf("route %v contains invalid site ID %08x; must be 0xffff or less", ipp, siteID)
}
return nil
}
// CalcAdvertiseRoutes calculates the requested routes to be advertised by a node.
// advertiseRoutes is the user-provided, comma-separated list of routes (IP addresses or CIDR prefixes) to advertise.
// advertiseDefaultRoute indicates whether the node should act as an exit node and advertise default routes.
func CalcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([]netip.Prefix, error) {
routeMap := map[netip.Prefix]bool{}
if advertiseRoutes != "" {
var default4, default6 bool
advroutes := strings.SplitSeq(advertiseRoutes, ",")
for s := range advroutes {
ipp, err := netip.ParsePrefix(s)
if err != nil {
return nil, fmt.Errorf("%q is not a valid IP address or CIDR prefix", s)
}
if ipp != ipp.Masked() {
return nil, fmt.Errorf("%s has non-address bits set; expected %s", ipp, ipp.Masked())
}
if tsaddr.IsViaPrefix(ipp) {
if err := ValidateViaPrefix(ipp); err != nil {
return nil, err
}
}
if ipp == tsaddr.AllIPv4() {
default4 = true
} else if ipp == tsaddr.AllIPv6() {
default6 = true
}
routeMap[ipp] = true
}
if default4 && !default6 {
return nil, fmt.Errorf("%s advertised without its IPv6 counterpart, please also advertise %s", tsaddr.AllIPv4(), tsaddr.AllIPv6())
} else if default6 && !default4 {
return nil, fmt.Errorf("%s advertised without its IPv4 counterpart, please also advertise %s", tsaddr.AllIPv6(), tsaddr.AllIPv4())
}
}
if advertiseDefaultRoute {
routeMap[tsaddr.AllIPv4()] = true
routeMap[tsaddr.AllIPv6()] = true
}
if len(routeMap) == 0 {
return nil, nil
}
routes := make([]netip.Prefix, 0, len(routeMap))
for r := range routeMap {
routes = append(routes, r)
}
sort.Slice(routes, func(i, j int) bool {
if routes[i].Bits() != routes[j].Bits() {
return routes[i].Bits() < routes[j].Bits()
}
return routes[i].Addr().Less(routes[j].Addr())
})
return routes, nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package netx contains types to describe and abstract over how dialing and
// listening are performed.
package netx
import (
"context"
"fmt"
"net"
)
// DialFunc is a function that dials a network address.
//
// It's the type implemented by net.Dialer.DialContext or required
// by net/http.Transport.DialContext, etc.
type DialFunc func(ctx context.Context, network, address string) (net.Conn, error)
// Network describes a network that can listen and dial. The two common
// implementations are [RealNetwork], using the net package to use the real
// network, or [memnet.Network], using an in-memory network (typically for testing)
type Network interface {
NewLocalTCPListener() net.Listener
Listen(network, address string) (net.Listener, error)
Dial(ctx context.Context, network, address string) (net.Conn, error)
}
// RealNetwork returns a Network implementation that uses the real
// net package.
func RealNetwork() Network { return realNetwork{} }
// realNetwork implements [Network] using the real net package.
type realNetwork struct{}
func (realNetwork) Listen(network, address string) (net.Listener, error) {
return net.Listen(network, address)
}
func (realNetwork) Dial(ctx context.Context, network, address string) (net.Conn, error) {
var d net.Dialer
return d.DialContext(ctx, network, address)
}
func (realNetwork) NewLocalTCPListener() net.Listener {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
if ln, err = net.Listen("tcp6", "[::1]:0"); err != nil {
panic(fmt.Sprintf("failed to listen on either IPv4 or IPv6 localhost port: %v", err))
}
}
return ln
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package netx
import (
"context"
"net"
"net/netip"
"time"
"tailscale.com/util/slicesx"
)
// RaceDial races TCP connect attempts across addrs using a
// happy-eyeballs-style staggered approach: a new dial is started every
// fallbackDelay, and the first successful connection wins. Losers are
// cancelled and their connections closed. If all dials fail, the first
// error is returned.
//
// Addresses are interleaved v6-first so that IPv6 is preferred but both
// families are tried promptly. The dial func is always called with
// network "tcp".
func RaceDial(ctx context.Context, addrs []netip.AddrPort, dial DialFunc, fallbackDelay time.Duration) (net.Conn, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var v4, v6 []netip.AddrPort
for _, a := range addrs {
if a.Addr().Is6() {
v6 = append(v6, a)
} else {
v4 = append(v4, a)
}
}
ordered := slicesx.Interleave(v6, v4)
type result struct {
c net.Conn
err error
}
resc := make(chan result) // unbuffered: senders sync with collector
failBoost := make(chan struct{}, 1) // wake the launcher when a dial fails fast
go func() {
for i, addr := range ordered {
if i > 0 {
t := time.NewTimer(fallbackDelay)
select {
case <-t.C:
case <-failBoost:
t.Stop()
case <-ctx.Done():
t.Stop()
return
}
}
go func() {
c, err := dial(ctx, "tcp", addr.String())
if err != nil {
select {
case failBoost <- struct{}{}:
default:
}
}
select {
case resc <- result{c, err}:
case <-ctx.Done():
if c != nil {
c.Close()
}
}
}()
}
}()
var firstErr error
var nFailed int
for {
select {
case r := <-resc:
if r.err == nil {
return r.c, nil
}
if firstErr == nil {
firstErr = r.err
}
nFailed++
if nFailed >= len(ordered) {
return nil, firstErr
}
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
// Code generated by "stringer -type Label -trimprefix Label"; DO NOT EDIT.
package sockstats
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[LabelControlClientAuto-0]
_ = x[LabelControlClientDialer-1]
_ = x[LabelDERPHTTPClient-2]
_ = x[LabelLogtailLogger-3]
_ = x[LabelDNSForwarderDoH-4]
_ = x[LabelDNSForwarderUDP-5]
_ = x[LabelNetcheckClient-6]
_ = x[LabelPortmapperClient-7]
_ = x[LabelMagicsockConnUDP4-8]
_ = x[LabelMagicsockConnUDP6-9]
_ = x[LabelNetlogLogger-10]
_ = x[LabelSockstatlogLogger-11]
_ = x[LabelDNSForwarderTCP-12]
}
const _Label_name = "ControlClientAutoControlClientDialerDERPHTTPClientLogtailLoggerDNSForwarderDoHDNSForwarderUDPNetcheckClientPortmapperClientMagicsockConnUDP4MagicsockConnUDP6NetlogLoggerSockstatlogLoggerDNSForwarderTCP"
var _Label_index = [...]uint8{0, 17, 36, 50, 63, 78, 93, 107, 123, 140, 157, 169, 186, 201}
func (i Label) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_Label_index)-1 {
return "Label(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Label_name[_Label_index[idx]:_Label_index[idx+1]]
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package sockstats collects statistics about network sockets used by
// the Tailscale client. The context where sockets are used must be
// instrumented with the WithSockStats() function.
//
// Only available on POSIX platforms when built with Tailscale's fork of Go.
package sockstats
import (
"context"
"tailscale.com/net/netmon"
"tailscale.com/types/logger"
)
// SockStats contains statistics for sockets instrumented with the
// WithSockStats() function
type SockStats struct {
Stats map[Label]SockStat
CurrentInterfaceCellular bool
}
// SockStat contains the sent and received bytes for a socket instrumented with
// the WithSockStats() function.
type SockStat struct {
TxBytes uint64
RxBytes uint64
}
// Label is an identifier for a socket that stats are collected for. A finite
// set of values that may be used to label a socket to encourage grouping and
// to make storage more efficient.
type Label uint8
//go:generate go run golang.org/x/tools/cmd/stringer -type Label -trimprefix Label
// Labels are named after the package and function/struct that uses the socket.
// Values may be persisted and thus existing entries should not be re-numbered.
const (
LabelControlClientAuto Label = 0 // control/controlclient/auto.go
LabelControlClientDialer Label = 1 // control/controlhttp/client.go
LabelDERPHTTPClient Label = 2 // derp/derphttp/derphttp_client.go
LabelLogtailLogger Label = 3 // logtail/logtail.go
LabelDNSForwarderDoH Label = 4 // net/dns/resolver/forwarder.go
LabelDNSForwarderUDP Label = 5 // net/dns/resolver/forwarder.go
LabelNetcheckClient Label = 6 // net/netcheck/netcheck.go
LabelPortmapperClient Label = 7 // net/portmapper/portmapper.go
LabelMagicsockConnUDP4 Label = 8 // wgengine/magicsock/magicsock.go
LabelMagicsockConnUDP6 Label = 9 // wgengine/magicsock/magicsock.go
LabelNetlogLogger Label = 10 // wgengine/netlog/logger.go
LabelSockstatlogLogger Label = 11 // log/sockstatlog/logger.go
LabelDNSForwarderTCP Label = 12 // net/dns/resolver/forwarder.go
)
// WithSockStats instruments a context so that sockets created with it will
// have their statistics collected.
func WithSockStats(ctx context.Context, label Label, logf logger.Logf) context.Context {
return withSockStats(ctx, label, logf)
}
// Get returns the current socket statistics.
func Get() *SockStats {
return get()
}
// InterfaceSockStats contains statistics for sockets instrumented with the
// WithSockStats() function, broken down by interface. The statistics may be a
// subset of the total if interfaces were added after the instrumented socket
// was created.
type InterfaceSockStats struct {
Stats map[Label]InterfaceSockStat
Interfaces []string
}
// InterfaceSockStat contains the per-interface sent and received bytes for a
// socket instrumented with the WithSockStats() function.
type InterfaceSockStat struct {
TxBytesByInterface map[string]uint64
RxBytesByInterface map[string]uint64
}
// GetWithInterfaces is a variant of Get that returns the current socket
// statistics broken down by interface. It is slightly more expensive than Get.
func GetInterfaces() *InterfaceSockStats {
return getInterfaces()
}
// ValidationSockStats contains external validation numbers for sockets
// instrumented with WithSockStats. It may be a subset of the all sockets,
// depending on what externa measurement mechanisms the platform supports.
type ValidationSockStats struct {
Stats map[Label]ValidationSockStat
}
// ValidationSockStat contains the validation bytes for a socket instrumented
// with WithSockStats.
type ValidationSockStat struct {
TxBytes uint64
RxBytes uint64
}
// GetValidation is a variant of Get that returns external validation numbers
// for stats. It is more expensive than Get and should be used in debug
// interfaces only.
func GetValidation() *ValidationSockStats {
return getValidation()
}
// SetNetMon configures the sockstats package to monitor the active
// interface, so that per-interface stats can be collected.
func SetNetMon(netMon *netmon.Monitor) {
setNetMon(netMon)
}
// DebugInfo returns a string containing debug information about the tracked
// statistics.
func DebugInfo() string {
return debugInfo()
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !tailscale_go || !(darwin || ios || android || ts_enable_sockstats)
package sockstats
import (
"context"
"tailscale.com/net/netmon"
"tailscale.com/types/logger"
)
const IsAvailable = false
func withSockStats(ctx context.Context, label Label, logf logger.Logf) context.Context {
return ctx
}
func get() *SockStats {
return nil
}
func getInterfaces() *InterfaceSockStats {
return nil
}
func getValidation() *ValidationSockStats {
return nil
}
func setNetMon(netMon *netmon.Monitor) {
}
func debugInfo() string {
return ""
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package stun generates STUN request packets and parses response packets.
package stun
import (
"bytes"
crand "crypto/rand"
"encoding/binary"
"errors"
"hash/crc32"
"net"
"net/netip"
)
const (
attrNumSoftware = 0x8022
attrNumFingerprint = 0x8028
attrMappedAddress = 0x0001
attrXorMappedAddress = 0x0020
// This alternative attribute type is not
// mentioned in the RFC, but the shift into
// the "comprehension-optional" range seems
// like an easy mistake for a server to make.
// And servers appear to send it.
attrXorMappedAddressAlt = 0x8020
software = "tailnode" // notably: 8 bytes long, so no padding
bindingRequest = "\x00\x01"
magicCookie = "\x21\x12\xa4\x42"
lenFingerprint = 8 // 2+byte header + 2-byte length + 4-byte crc32
headerLen = 20
)
// TxID is a transaction ID.
type TxID [12]byte
// NewTxID returns a new random TxID.
func NewTxID() TxID {
var tx TxID
if _, err := crand.Read(tx[:]); err != nil {
panic(err)
}
return tx
}
// Request generates a binding request STUN packet.
// The transaction ID, tID, should be a random sequence of bytes.
func Request(tID TxID) []byte {
// STUN header, RFC5389 Section 6.
const lenAttrSoftware = 4 + len(software)
b := make([]byte, 0, headerLen+lenAttrSoftware+lenFingerprint)
b = append(b, bindingRequest...)
b = appendU16(b, uint16(lenAttrSoftware+lenFingerprint)) // number of bytes following header
b = append(b, magicCookie...)
b = append(b, tID[:]...)
// Attribute SOFTWARE, RFC5389 Section 15.5.
b = appendU16(b, attrNumSoftware)
b = appendU16(b, uint16(len(software)))
b = append(b, software...)
// Attribute FINGERPRINT, RFC5389 Section 15.5.
fp := fingerPrint(b)
b = appendU16(b, attrNumFingerprint)
b = appendU16(b, 4)
b = appendU32(b, fp)
return b
}
func fingerPrint(b []byte) uint32 { return crc32.ChecksumIEEE(b) ^ 0x5354554e }
func appendU16(b []byte, v uint16) []byte {
return append(b, byte(v>>8), byte(v))
}
func appendU32(b []byte, v uint32) []byte {
return append(b, byte(v>>24), byte(v>>16), byte(v>>8), byte(v))
}
// ParseBindingRequest parses a STUN binding request.
//
// It returns an error unless it advertises that it came from
// Tailscale.
func ParseBindingRequest(b []byte) (TxID, error) {
if !Is(b) {
return TxID{}, ErrNotSTUN
}
if string(b[:len(bindingRequest)]) != bindingRequest {
return TxID{}, ErrNotBindingRequest
}
var txID TxID
copy(txID[:], b[8:8+len(txID)])
var softwareOK bool
var lastAttr uint16
var gotFP uint32
if err := foreachAttr(b[headerLen:], func(attrType uint16, a []byte) error {
lastAttr = attrType
if attrType == attrNumSoftware && string(a) == software {
softwareOK = true
}
if attrType == attrNumFingerprint && len(a) == 4 {
gotFP = binary.BigEndian.Uint32(a)
}
return nil
}); err != nil {
return TxID{}, err
}
if !softwareOK {
return TxID{}, ErrWrongSoftware
}
if lastAttr != attrNumFingerprint {
return TxID{}, ErrNoFingerprint
}
wantFP := fingerPrint(b[:len(b)-lenFingerprint])
if gotFP != wantFP {
return TxID{}, ErrWrongFingerprint
}
return txID, nil
}
var (
ErrNotSTUN = errors.New("response is not a STUN packet")
ErrNotSuccessResponse = errors.New("STUN packet is not a response")
ErrMalformedAttrs = errors.New("STUN response has malformed attributes")
ErrNotBindingRequest = errors.New("STUN request not a binding request")
ErrWrongSoftware = errors.New("STUN request came from non-Tailscale software")
ErrNoFingerprint = errors.New("STUN request didn't end in fingerprint")
ErrWrongFingerprint = errors.New("STUN request had bogus fingerprint")
)
func foreachAttr(b []byte, fn func(attrType uint16, a []byte) error) error {
for len(b) > 0 {
if len(b) < 4 {
return ErrMalformedAttrs
}
attrType := binary.BigEndian.Uint16(b[:2])
attrLen := int(binary.BigEndian.Uint16(b[2:4]))
attrLenWithPad := (attrLen + 3) &^ 3
b = b[4:]
if attrLenWithPad > len(b) {
return ErrMalformedAttrs
}
if err := fn(attrType, b[:attrLen]); err != nil {
return err
}
b = b[attrLenWithPad:]
}
return nil
}
// Response generates a binding response.
func Response(txID TxID, addrPort netip.AddrPort) []byte {
addr := addrPort.Addr()
var fam byte
if addr.Is4() {
fam = 1
} else if addr.Is6() {
fam = 2
} else {
return nil
}
attrsLen := 8 + addr.BitLen()/8
b := make([]byte, 0, headerLen+attrsLen)
// Header
b = append(b, 0x01, 0x01) // success
b = appendU16(b, uint16(attrsLen))
b = append(b, magicCookie...)
b = append(b, txID[:]...)
// Attributes (well, one)
b = appendU16(b, attrXorMappedAddress)
b = appendU16(b, uint16(4+addr.BitLen()/8))
b = append(b,
0, // unused byte
fam)
b = appendU16(b, addrPort.Port()^0x2112) // first half of magicCookie
ipa := addr.As16()
for i, o := range ipa[16-addr.BitLen()/8:] {
if i < 4 {
b = append(b, o^magicCookie[i])
} else {
b = append(b, o^txID[i-len(magicCookie)])
}
}
return b
}
// ParseResponse parses a successful binding response STUN packet.
// The IP address is extracted from the XOR-MAPPED-ADDRESS attribute.
func ParseResponse(b []byte) (tID TxID, addr netip.AddrPort, err error) {
if !Is(b) {
return tID, netip.AddrPort{}, ErrNotSTUN
}
copy(tID[:], b[8:8+len(tID)])
if b[0] != 0x01 || b[1] != 0x01 {
return tID, netip.AddrPort{}, ErrNotSuccessResponse
}
attrsLen := int(binary.BigEndian.Uint16(b[2:4]))
b = b[headerLen:] // remove STUN header
if attrsLen > len(b) {
return tID, netip.AddrPort{}, ErrMalformedAttrs
} else if len(b) > attrsLen {
b = b[:attrsLen] // trim trailing packet bytes
}
var fallbackAddr netip.AddrPort
// Read through the attributes.
// The addr+port reported by XOR-MAPPED-ADDRESS
// is the canonical value. If the attribute is not
// present but the STUN server responds with
// MAPPED-ADDRESS we fall back to it.
if err := foreachAttr(b, func(attrType uint16, attr []byte) error {
switch attrType {
case attrXorMappedAddress, attrXorMappedAddressAlt:
ipSlice, port, err := xorMappedAddress(tID, attr)
if err != nil {
return err
}
if ip, ok := netip.AddrFromSlice(ipSlice); ok {
addr = netip.AddrPortFrom(ip.Unmap(), port)
}
case attrMappedAddress:
ipSlice, port, err := mappedAddress(attr)
if err != nil {
return ErrMalformedAttrs
}
if ip, ok := netip.AddrFromSlice(ipSlice); ok {
fallbackAddr = netip.AddrPortFrom(ip.Unmap(), port)
}
}
return nil
}); err != nil {
return TxID{}, netip.AddrPort{}, err
}
if addr.IsValid() {
return tID, addr, nil
}
if fallbackAddr.IsValid() {
return tID, fallbackAddr, nil
}
return tID, netip.AddrPort{}, ErrMalformedAttrs
}
func xorMappedAddress(tID TxID, b []byte) (addr []byte, port uint16, err error) {
// XOR-MAPPED-ADDRESS attribute, RFC5389 Section 15.2
if len(b) < 4 {
return nil, 0, ErrMalformedAttrs
}
xorPort := binary.BigEndian.Uint16(b[2:4])
addrField := b[4:]
port = xorPort ^ 0x2112 // first half of magicCookie
addrLen := familyAddrLen(b[1])
if addrLen == 0 {
return nil, 0, ErrMalformedAttrs
}
if len(addrField) < addrLen {
return nil, 0, ErrMalformedAttrs
}
xorAddr := addrField[:addrLen]
addr = make([]byte, addrLen)
for i := range xorAddr {
if i < len(magicCookie) {
addr[i] = xorAddr[i] ^ magicCookie[i]
} else {
addr[i] = xorAddr[i] ^ tID[i-len(magicCookie)]
}
}
return addr, port, nil
}
func familyAddrLen(fam byte) int {
switch fam {
case 0x01: // IPv4
return net.IPv4len
case 0x02: // IPv6
return net.IPv6len
default:
return 0
}
}
func mappedAddress(b []byte) (addr []byte, port uint16, err error) {
if len(b) < 4 {
return nil, 0, ErrMalformedAttrs
}
port = uint16(b[2])<<8 | uint16(b[3])
addrField := b[4:]
addrLen := familyAddrLen(b[1])
if addrLen == 0 {
return nil, 0, ErrMalformedAttrs
}
if len(addrField) < addrLen {
return nil, 0, ErrMalformedAttrs
}
return bytes.Clone(addrField[:addrLen]), port, nil
}
// Is reports whether b is a STUN message.
func Is(b []byte) bool {
return len(b) >= headerLen &&
b[0]&0b11000000 == 0 && // top two bits must be zero
string(b[4:8]) == magicCookie
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package blockblame blames specific firewall manufacturers for blocking Tailscale,
// by analyzing the SSL certificate presented when attempting to connect to a remote
// server.
package blockblame
import (
"crypto/x509"
"strings"
"sync"
"tailscale.com/feature/buildfeatures"
)
// VerifyCertificate checks if the given certificate c is issued by a firewall manufacturer
// that is known to block Tailscale connections. It returns true and the Manufacturer of
// the equipment if it is, or false and nil if it is not.
func VerifyCertificate(c *x509.Certificate) (m *Manufacturer, ok bool) {
if !buildfeatures.HasDebug {
return nil, false
}
for _, m := range manufacturers() {
if m.match != nil && m.match(c) {
return m, true
}
}
return nil, false
}
// Manufacturer represents a firewall manufacturer that may be blocking Tailscale.
type Manufacturer struct {
// Name is the name of the firewall manufacturer to be
// mentioned in health warning messages, e.g. "Fortinet".
Name string
// match is a function that returns true if the given certificate looks like it might
// be issued by this manufacturer.
match matchFunc
}
func manufacturers() []*Manufacturer {
manufacturersOnce.Do(func() {
manufacturersList = []*Manufacturer{
{
Name: "Aruba Networks",
match: issuerContains("Aruba"),
},
{
Name: "Cisco",
match: issuerContains("Cisco"),
},
{
Name: "Fortinet",
match: matchAny(
issuerContains("Fortinet"),
certEmail("support@fortinet.com"),
),
},
{
Name: "Huawei",
match: certEmail("mobile@huawei.com"),
},
{
Name: "Palo Alto Networks",
match: matchAny(
issuerContains("Palo Alto Networks"),
issuerContains("PAN-FW"),
),
},
{
Name: "Sophos",
match: issuerContains("Sophos"),
},
{
Name: "Ubiquiti",
match: matchAny(
issuerContains("UniFi"),
issuerContains("Ubiquiti"),
),
},
}
})
return manufacturersList
}
var (
manufacturersOnce sync.Once
manufacturersList []*Manufacturer
)
type matchFunc func(*x509.Certificate) bool
func issuerContains(s string) matchFunc {
return func(c *x509.Certificate) bool {
return strings.Contains(strings.ToLower(c.Issuer.String()), strings.ToLower(s))
}
}
func certEmail(v string) matchFunc {
return func(c *x509.Certificate) bool {
for _, email := range c.EmailAddresses {
if strings.Contains(strings.ToLower(email), strings.ToLower(v)) {
return true
}
}
return false
}
}
func matchAny(fs ...matchFunc) matchFunc {
return func(c *x509.Certificate) bool {
for _, f := range fs {
if f(c) {
return true
}
}
return false
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package tlsdial generates tls.Config values and does x509 validation of
// certs. It bakes in the LetsEncrypt roots so even if the user's machine
// doesn't have TLS roots, we can at least connect to Tailscale's LetsEncrypt
// services. It's the unified point where we can add shared policy on outgoing
// TLS connections from the three places in the client that connect to Tailscale
// (logs, control, DERP).
package tlsdial
import (
"bytes"
"context"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"log"
"net"
"net/http"
"os"
"strings"
"sync"
"sync/atomic"
"time"
"tailscale.com/derp/derpconst"
"tailscale.com/envknob"
"tailscale.com/feature/buildfeatures"
"tailscale.com/health"
"tailscale.com/hostinfo"
"tailscale.com/net/bakedroots"
"tailscale.com/net/tlsdial/blockblame"
)
var counterFallbackOK int32 // atomic
var debug = envknob.RegisterBool("TS_DEBUG_TLS_DIAL")
// tlsdialWarningPrinted tracks whether we've printed a warning about a given
// hostname already, to avoid log spam for users with custom DERP servers,
// Headscale, etc.
var tlsdialWarningPrinted sync.Map // map[string]bool
var mitmBlockWarnable = health.Register(&health.Warnable{
Code: "blockblame-mitm-detected",
Title: "Network may be blocking Tailscale",
Text: func(args health.Args) string {
return fmt.Sprintf("Network equipment from %q may be blocking Tailscale traffic on this network. Connect to another network, or contact your network administrator for assistance.", args["manufacturer"])
},
Severity: health.SeverityMedium,
ImpactsConnectivity: true,
})
// Config returns a tls.Config for connecting to a server that
// uses system roots for validation but, if those fail, also tries
// the baked-in LetsEncrypt roots as a fallback validation method.
//
// If base is non-nil, it's cloned as the base config before
// being configured and returned. If base.RootCAs is non-nil, it is
// used as an additional set of trusted roots (after system roots,
// before baked-in LetsEncrypt roots). This is used on Android to
// trust user-installed CA certificates that Go's crypto/x509
// does not see.
//
// If ht is non-nil, it's used to report health errors.
func Config(ht *health.Tracker, base *tls.Config) *tls.Config {
var extraRoots *x509.CertPool
if base != nil {
extraRoots = base.RootCAs
}
var conf *tls.Config
if base == nil {
conf = new(tls.Config)
} else {
conf = base.Clone()
}
conf.RootCAs = nil // we do our own verification in VerifyConnection
// Note: we do NOT set conf.ServerName here (as we accidentally did
// previously), as this path is also used when dialing an HTTPS proxy server
// (through which we'll send a CONNECT request to get a TCP connection to do
// the real TCP connection) because host is the ultimate hostname, but this
// tls.Config is used for both the proxy and the ultimate target.
if buildfeatures.HasDebug {
// If SSLKEYLOGFILE is set, it's a file to which we write our TLS private keys
// in a way that Wireshark can read.
//
// See https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/Key_Log_Format
if n := os.Getenv("SSLKEYLOGFILE"); n != "" {
f, err := os.OpenFile(n, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
log.Fatal(err)
}
log.Printf("WARNING: writing to SSLKEYLOGFILE %v", n)
conf.KeyLogWriter = f
}
}
if conf.InsecureSkipVerify {
panic("unexpected base.InsecureSkipVerify")
}
if conf.VerifyConnection != nil {
panic("unexpected base.VerifyConnection")
}
// Set InsecureSkipVerify to prevent crypto/tls from doing its
// own cert verification, as do the same work that it'd do
// (with the baked-in fallback root) in the VerifyConnection hook.
conf.InsecureSkipVerify = true
conf.VerifyConnection = func(cs tls.ConnectionState) (retErr error) {
dialedHost := cs.ServerName
if dialedHost == "log.tailscale.com" && hostinfo.IsNATLabGuestVM() {
// Allow log.tailscale.com TLS MITM for integration tests when
// the client's running within a NATLab VM.
return nil
}
// Perform some health checks on this certificate before we do
// any verification.
var cert *x509.Certificate
var selfSignedIssuer string
if certs := cs.PeerCertificates; len(certs) > 0 {
cert = certs[0]
if certIsSelfSigned(cert) {
selfSignedIssuer = cert.Issuer.String()
}
}
if ht != nil {
defer func() {
if retErr != nil && cert != nil {
// Is it a MITM SSL certificate from a well-known network appliance manufacturer?
// Show a dedicated warning.
m, ok := blockblame.VerifyCertificate(cert)
if ok {
log.Printf("tlsdial: server cert seen while dialing %q looks like %q equipment (could be blocking Tailscale)", dialedHost, m.Name)
ht.SetUnhealthy(mitmBlockWarnable, health.Args{"manufacturer": m.Name})
} else {
ht.SetHealthy(mitmBlockWarnable)
}
} else {
ht.SetHealthy(mitmBlockWarnable)
}
if retErr != nil && selfSignedIssuer != "" {
// Self-signed certs are never valid.
//
// TODO(bradfitz): plumb down the selfSignedIssuer as a
// structured health warning argument.
ht.SetTLSConnectionError(cs.ServerName, fmt.Errorf("likely intercepted connection; certificate is self-signed by %v", selfSignedIssuer))
} else {
// Ensure we clear any error state for this ServerName.
ht.SetTLSConnectionError(cs.ServerName, nil)
if selfSignedIssuer != "" {
// Log the self-signed issuer, but don't treat it as an error.
log.Printf("tlsdial: warning: server cert for %q passed x509 validation but is self-signed by %q", dialedHost, selfSignedIssuer)
}
}
}()
}
// First try doing x509 verification with the system's
// root CA pool.
opts := x509.VerifyOptions{
DNSName: dialedHost,
Intermediates: x509.NewCertPool(),
}
for _, cert := range cs.PeerCertificates[1:] {
opts.Intermediates.AddCert(cert)
}
_, errSys := cs.PeerCertificates[0].Verify(opts)
if debug() {
log.Printf("tlsdial(sys %q): %v", dialedHost, errSys)
}
if errSys == nil && !debug() {
return nil
}
// If extra roots were provided (e.g. user-installed CAs on
// Android), try those next.
if extraRoots != nil {
opts.Roots = extraRoots
_, errExtra := cs.PeerCertificates[0].Verify(opts)
if debug() {
log.Printf("tlsdial(extra %q): %v", dialedHost, errExtra)
}
if errExtra == nil {
atomic.AddInt32(&counterFallbackOK, 1)
return nil
}
opts.Roots = nil // reset for baked roots check
}
if !buildfeatures.HasBakedRoots {
return errSys
}
// If we have baked-in LetsEncrypt roots and we either failed above, or
// debug logging is enabled, also verify with LetsEncrypt.
opts.Roots = bakedroots.Get()
_, bakedErr := cs.PeerCertificates[0].Verify(opts)
if debug() {
log.Printf("tlsdial(bake %q): %v", dialedHost, bakedErr)
} else if bakedErr != nil {
if _, loaded := tlsdialWarningPrinted.LoadOrStore(dialedHost, true); !loaded {
if errSys != nil {
if extraRoots != nil {
log.Printf("tlsdial: error: server cert for %q failed system roots, extra roots & Let's Encrypt root validation", dialedHost)
} else {
log.Printf("tlsdial: error: server cert for %q failed both system roots & Let's Encrypt root validation", dialedHost)
}
}
}
}
if errSys == nil {
return nil
} else if bakedErr == nil {
atomic.AddInt32(&counterFallbackOK, 1)
return nil
}
return errSys
}
return conf
}
func certIsSelfSigned(cert *x509.Certificate) bool {
// A certificate is determined to be self-signed if the certificate's
// subject is the same as its issuer.
return bytes.Equal(cert.RawSubject, cert.RawIssuer)
}
// SetConfigExpectedCert modifies c to expect and verify that the server returns
// a certificate for the provided certDNSName.
//
// This is for user-configurable client-side domain fronting support,
// where we send one SNI value but validate a different cert.
func SetConfigExpectedCert(c *tls.Config, certDNSName string) {
if c.ServerName == certDNSName {
return
}
if c.ServerName == "" {
c.ServerName = certDNSName
return
}
extraRoots := c.RootCAs
c.RootCAs = nil
// Set InsecureSkipVerify to prevent crypto/tls from doing its
// own cert verification, but do the same work that it'd do
// (but using certDNSName) in the VerifyPeerCertificate hook.
c.InsecureSkipVerify = true
c.VerifyConnection = nil
c.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
if len(rawCerts) == 0 {
return errors.New("no certs presented")
}
certs := make([]*x509.Certificate, len(rawCerts))
for i, asn1Data := range rawCerts {
cert, err := x509.ParseCertificate(asn1Data)
if err != nil {
return err
}
certs[i] = cert
}
opts := x509.VerifyOptions{
CurrentTime: time.Now(),
DNSName: certDNSName,
Intermediates: x509.NewCertPool(),
}
for _, cert := range certs[1:] {
opts.Intermediates.AddCert(cert)
}
_, errSys := certs[0].Verify(opts)
if debug() {
log.Printf("tlsdial(sys %q/%q): %v", c.ServerName, certDNSName, errSys)
}
if errSys == nil {
return nil
}
if extraRoots != nil {
opts.Roots = extraRoots
_, errExtra := certs[0].Verify(opts)
if debug() {
log.Printf("tlsdial(extra %q/%q): %v", c.ServerName, certDNSName, errExtra)
}
if errExtra == nil {
return nil
}
opts.Roots = nil
}
if !buildfeatures.HasBakedRoots {
return errSys
}
opts.Roots = bakedroots.Get()
_, err := certs[0].Verify(opts)
if debug() {
log.Printf("tlsdial(bake %q/%q): %v", c.ServerName, certDNSName, err)
}
if err == nil {
return nil
}
return errSys
}
}
// SetConfigExpectedCertHash configures c's VerifyPeerCertificate function to
// require that exactly 1 cert is presented (not counting any present MetaCert),
// and that the hex of its SHA256 hash is equal to wantFullCertSHA256Hex and
// that it's a valid cert for c.ServerName.
func SetConfigExpectedCertHash(c *tls.Config, wantFullCertSHA256Hex string) {
if c.VerifyPeerCertificate != nil {
panic("refusing to override tls.Config.VerifyPeerCertificate")
}
// Set InsecureSkipVerify to prevent crypto/tls from doing its
// own cert verification, but do the same work that it'd do
// (but using certDNSName) in the VerifyConnection hook.
c.InsecureSkipVerify = true
c.VerifyConnection = func(cs tls.ConnectionState) error {
dialedHost := cs.ServerName
var sawGoodCert bool
for _, cert := range cs.PeerCertificates {
if strings.HasPrefix(cert.Subject.CommonName, derpconst.MetaCertCommonNamePrefix) {
continue
}
if sawGoodCert {
return errors.New("unexpected multiple certs presented")
}
if fmt.Sprintf("%02x", sha256.Sum256(cert.Raw)) != wantFullCertSHA256Hex {
return fmt.Errorf("cert hash does not match expected cert hash")
}
if dialedHost != "" { // it's empty when dialing a derper by IP with no hostname
if err := cert.VerifyHostname(dialedHost); err != nil {
return fmt.Errorf("cert does not match server name %q: %w", dialedHost, err)
}
}
now := time.Now()
if now.After(cert.NotAfter) {
return fmt.Errorf("cert expired %v", cert.NotAfter)
}
if now.Before(cert.NotBefore) {
return fmt.Errorf("cert not yet valid until %v; is your clock correct?", cert.NotBefore)
}
sawGoodCert = true
}
if !sawGoodCert {
return errors.New("expected cert not presented")
}
return nil
}
}
// NewTransport returns a new HTTP transport that verifies TLS certs using this
// package, including its baked-in LetsEncrypt fallback roots.
func NewTransport() *http.Transport {
return &http.Transport{
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
var d tls.Dialer
d.Config = Config(nil, nil)
return d.DialContext(ctx, network, addr)
},
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package traffic contains helpers for evaluating traffic steering scores and
// picking appropriate nodes.
package traffic
import (
"cmp"
"encoding/binary"
"hash/fnv"
"iter"
"maps"
"slices"
"tailscale.com/tailcfg"
"tailscale.com/util/mak"
)
// Score is a node’s traffic score, where any int could be a valid score.
// A higher traffic score suggests that the client should prefer that peer
// over one with a lower traffic score.
type Score int
// Scores is a memoization cache for the traffic scores of the current node’s peers.
type Scores struct {
self tailcfg.NodeID
hash NodeHasher
scores map[tailcfg.NodeID]Score
}
// ScoresFor returns a new [Scores] cache for the current node’s ID,
// after scoring the peer nodes and adding these scores to the cache.
func ScoresFor(self tailcfg.NodeID, peers []tailcfg.NodeView) Scores {
ss := Scores{
self: self,
hash: MakeRendezvousHasher(self),
}
ss.ScorePeers(peers)
return ss
}
// IsValid reports whether ss has been initialized with the current node ID.
func (ss Scores) IsValid() bool {
return !ss.self.IsZero()
}
// Score scores the given peer node and returns it after adding the score to the cache.
func (ss *Scores) Score(n tailcfg.NodeView) Score {
id := n.ID()
if s, ok := ss.scores[id]; ok {
return s
}
var s Score
if hi := n.Hostinfo(); hi.Valid() {
if loc := hi.Location(); loc.Valid() {
s = Score(loc.Priority())
}
}
mak.Set(&ss.scores, id, s)
return s
}
// ScorePeers scores the peer nodes and adds these scores to the cache.
func (ss *Scores) ScorePeers(peers []tailcfg.NodeView) {
if len(peers) == 0 {
return
}
if ss.scores == nil {
ss.scores = make(map[tailcfg.NodeID]Score, len(peers))
}
for _, n := range peers {
ss.Score(n)
}
}
// All returns an iterator over the scores for every peer in the cache.
// The iteration order is not specified and is not guaranteed to be the same
// from one call to the next.
func (ss Scores) All() iter.Seq2[tailcfg.NodeID, Score] {
return maps.All(ss.scores)
}
// SortNodes sorts the slice of nodes in descending order of [Scores.Score],
// using rendezvous hashing to break ties when both nodes have the same score.
// After sorting, the zeroth element is the preferred node.
func (ss Scores) SortNodes(nodes []tailcfg.NodeView) {
slices.SortFunc(nodes, func(a, b tailcfg.NodeView) int {
c := cmp.Compare(ss.Score(b), ss.Score(a)) // Highest score first.
if c == 0 {
return ss.hash.Compare(b.ID(), a.ID()) // Descending order.
}
return c
})
}
// NodeHasher returns a 64-bit hash of a node ID.
type NodeHasher func(tailcfg.NodeID) uint64
// MakeRendezvousHasher returns a function that hashes a node ID to a uint64.
// https://en.wikipedia.org/wiki/Rendezvous_hashing
func MakeRendezvousHasher(seed tailcfg.NodeID) NodeHasher {
en := binary.BigEndian
return func(n tailcfg.NodeID) uint64 {
var b [16]byte
en.PutUint64(b[:], uint64(seed))
en.PutUint64(b[8:], uint64(n))
// FNV-1a is more modern and distributes bits more evenly,
// so it is recommended by the designers.
//
// Note that we don’t use a global hasher and h.Reset
// because this closure could be called concurrently.
// This is cheap because hash/fnv doesn’t need to allocate.
h := fnv.New64a()
h.Write(b[:])
v := h.Sum64()
// After FNV-1a, finalize the result by mixing in some large
// numbers. This ensures a small change in the seed/input bits
// causes a large perturbation in the output bits, aka the
// "Avalanche Effect".
// We opted to use the mix13 variant described by David Stafford,
// a popular choice in other language libraries.
// https://web.archive.org/web/20260406221046/https://zimbry.blogspot.com/2011/09/better-bit-mixing-improving-on.html
v ^= v >> 30
v *= 0xbf58476d1ce4e5b9
v ^= v >> 27
v *= 0x94d049bb133111eb
v ^= v >> 31
return v
}
}
// Compare compares the node ID hashes of peers a and b, using the same convention as [cmp.Compare].
// Since h is seeded with the current node’s ID, the ordering between a and b will remain stable
// for this node; but the order may flip for when h is seeded for another node.
// This function should return zero, if and only if a and b have the same node ID.
func (h NodeHasher) Compare(a, b tailcfg.NodeID) int {
c := cmp.Compare(h(a), h(b))
if c == 0 {
// In the unlikely event of a hash collision, compare the actual IDs.
return cmp.Compare(a, b)
}
return c
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package tsaddr handles Tailscale-specific IPs and ranges.
package tsaddr
import (
"encoding/binary"
"errors"
"iter"
"net/netip"
"slices"
"sync"
"go4.org/netipx"
"tailscale.com/net/netaddr"
"tailscale.com/types/views"
)
// ChromeOSVMRange returns the subset of the CGNAT IPv4 range used by
// ChromeOS to interconnect the host OS to containers and VMs. We
// avoid allocating Tailscale IPs from it, to avoid conflicts.
func ChromeOSVMRange() netip.Prefix {
chromeOSRange.Do(func() { mustPrefix(&chromeOSRange.v, "100.115.92.0/23") })
return chromeOSRange.v
}
var chromeOSRange oncePrefix
// CGNATRange returns the Carrier Grade NAT address range that
// is the superset range that Tailscale assigns out of.
// See https://tailscale.com/s/cgnat
// Note that Tailscale does not assign out of the ChromeOSVMRange.
func CGNATRange() netip.Prefix {
cgnatRange.Do(func() { mustPrefix(&cgnatRange.v, "100.64.0.0/10") })
return cgnatRange.v
}
var (
cgnatRange oncePrefix
tsUlaRange oncePrefix
tsViaRange oncePrefix
ula4To6Range oncePrefix
ulaEph6Range oncePrefix
serviceIPv6 oncePrefix
)
// TailscaleServiceIP returns the IPv4 listen address of services
// provided by Tailscale itself such as the MagicDNS proxy.
//
// For IPv6, use TailscaleServiceIPv6.
func TailscaleServiceIP() netip.Addr {
return netaddr.IPv4(100, 100, 100, 100) // "100.100.100.100" for those grepping
}
// TailscaleServiceIPv6 returns the IPv6 listen address of the services
// provided by Tailscale itself such as the MagicDNS proxy.
//
// For IPv4, use TailscaleServiceIP.
func TailscaleServiceIPv6() netip.Addr {
serviceIPv6.Do(func() { mustPrefix(&serviceIPv6.v, TailscaleServiceIPv6String+"/128") })
return serviceIPv6.v.Addr()
}
const (
TailscaleServiceIPString = "100.100.100.100"
TailscaleServiceIPv6String = "fd7a:115c:a1e0::53"
)
// IsTailscaleIP reports whether IP is an IP address in a range that
// Tailscale assigns from.
func IsTailscaleIP(ip netip.Addr) bool {
ip = ip.Unmap()
if ip.Is4() {
return IsTailscaleIPv4(ip)
}
return TailscaleULARange().Contains(ip)
}
// IsTailscaleIPv4 reports whether an IPv4 IP is an IP address that
// Tailscale assigns from.
// It will always return false if ip is an "IPv4-mapped IPv6 address".
func IsTailscaleIPv4(ip netip.Addr) bool {
return CGNATRange().Contains(ip) && !ChromeOSVMRange().Contains(ip)
}
// TailscaleULARange returns the IPv6 Unique Local Address range that
// is the superset range that Tailscale assigns out of.
func TailscaleULARange() netip.Prefix {
tsUlaRange.Do(func() { mustPrefix(&tsUlaRange.v, "fd7a:115c:a1e0::/48") })
return tsUlaRange.v
}
// TailscaleViaRange returns the IPv6 Unique Local Address subset range
// TailscaleULARange that's used for IPv4 tunneling via IPv6.
func TailscaleViaRange() netip.Prefix {
// Mnemonic: "b1a" sounds like "via".
tsViaRange.Do(func() { mustPrefix(&tsViaRange.v, "fd7a:115c:a1e0:b1a::/64") })
return tsViaRange.v
}
// Tailscale4To6Range returns the subset of TailscaleULARange used for
// auto-translated Tailscale ipv4 addresses.
func Tailscale4To6Range() netip.Prefix {
// This IP range has no significance, beyond being a subset of
// TailscaleULARange. The bits from /48 to /104 were picked at
// random.
ula4To6Range.Do(func() { mustPrefix(&ula4To6Range.v, "fd7a:115c:a1e0:ab12:4843:cd96:6200::/104") })
return ula4To6Range.v
}
// TailscaleEphemeral6Range returns the subset of TailscaleULARange
// used for ephemeral IPv6-only Tailscale nodes.
func TailscaleEphemeral6Range() netip.Prefix {
// This IP range has no significance, beyond being a subset of
// TailscaleULARange. The bits from /48 to /64 were picked at
// random, with the only criterion being to not be the conflict
// with the Tailscale4To6Range above.
ulaEph6Range.Do(func() { mustPrefix(&ulaEph6Range.v, "fd7a:115c:a1e0:efe3::/64") })
return ulaEph6Range.v
}
// Tailscale4To6Placeholder returns an IP address that can be used as
// a source IP when one is required, but a netmap didn't provide
// any. This address never gets allocated by the 4-to-6 algorithm in
// control.
//
// Currently used to work around a Windows limitation when programming
// IPv6 routes in corner cases.
func Tailscale4To6Placeholder() netip.Addr {
return Tailscale4To6Range().Addr()
}
// Tailscale4To6 returns a Tailscale IPv6 address that maps 1:1 to the
// given Tailscale IPv4 address. Returns a zero IP if ipv4 isn't a
// Tailscale IPv4 address.
func Tailscale4To6(ipv4 netip.Addr) netip.Addr {
if !ipv4.Is4() || !IsTailscaleIP(ipv4) {
return netip.Addr{}
}
ret := Tailscale4To6Range().Addr().As16()
v4 := ipv4.As4()
copy(ret[13:], v4[1:])
return netip.AddrFrom16(ret)
}
// Tailscale6to4 returns the IPv4 address corresponding to the given
// tailscale IPv6 address within the 4To6 range. The IPv4 address
// and true are returned if the given address was in the correct range,
// false if not.
func Tailscale6to4(ipv6 netip.Addr) (netip.Addr, bool) {
if !ipv6.Is6() || !Tailscale4To6Range().Contains(ipv6) {
return netip.Addr{}, false
}
v6 := ipv6.As16()
return netip.AddrFrom4([4]byte{100, v6[13], v6[14], v6[15]}), true
}
func mustPrefix(v *netip.Prefix, prefix string) {
var err error
*v, err = netip.ParsePrefix(prefix)
if err != nil {
panic(err)
}
}
type oncePrefix struct {
sync.Once
v netip.Prefix
}
// PrefixesContainsIP reports whether any prefix in ipp contains ip.
func PrefixesContainsIP(ipp []netip.Prefix, ip netip.Addr) bool {
for _, r := range ipp {
if r.Contains(ip) {
return true
}
}
return false
}
// PrefixIs4 reports whether p is an IPv4 prefix.
func PrefixIs4(p netip.Prefix) bool { return p.Addr().Is4() }
// PrefixIs6 reports whether p is an IPv6 prefix.
func PrefixIs6(p netip.Prefix) bool { return p.Addr().Is6() }
// ContainsExitRoutes reports whether rr contains both the IPv4 and
// IPv6 /0 route.
func ContainsExitRoutes(rr views.Slice[netip.Prefix]) bool {
var v4, v6 bool
for _, r := range rr.All() {
if r == allIPv4 {
v4 = true
} else if r == allIPv6 {
v6 = true
}
}
return v4 && v6
}
// ContainsExitRoute reports whether rr contains at least one of IPv4 or
// IPv6 /0 (exit) routes.
func ContainsExitRoute(rr views.Slice[netip.Prefix]) bool {
for _, r := range rr.All() {
if r.Bits() == 0 {
return true
}
}
return false
}
// ContainsNonExitSubnetRoutes reports whether v contains Subnet
// Routes other than ExitNode Routes.
func ContainsNonExitSubnetRoutes(rr views.Slice[netip.Prefix]) bool {
for _, r := range rr.All() {
if r.Bits() != 0 {
return true
}
}
return false
}
// WithoutExitRoutes returns rr unchanged if it has only 1 or 0 /0
// routes. If it has both IPv4 and IPv6 /0 routes, then it returns
// a copy with all /0 routes removed.
func WithoutExitRoutes(rr views.Slice[netip.Prefix]) views.Slice[netip.Prefix] {
if !ContainsExitRoutes(rr) {
return rr
}
var out []netip.Prefix
for _, r := range rr.All() {
if r.Bits() > 0 {
out = append(out, r)
}
}
return views.SliceOf(out)
}
// WithoutExitRoute returns rr unchanged if it has 0 /0
// routes. If it has a IPv4 or IPv6 /0 routes, then it returns
// a copy with all /0 routes removed.
func WithoutExitRoute(rr views.Slice[netip.Prefix]) views.Slice[netip.Prefix] {
if !ContainsExitRoute(rr) {
return rr
}
var out []netip.Prefix
for _, r := range rr.All() {
if r.Bits() > 0 {
out = append(out, r)
}
}
return views.SliceOf(out)
}
var (
allIPv4 = netip.MustParsePrefix("0.0.0.0/0")
allIPv6 = netip.MustParsePrefix("::/0")
)
// AllIPv4 returns 0.0.0.0/0.
func AllIPv4() netip.Prefix { return allIPv4 }
// AllIPv6 returns ::/0.
func AllIPv6() netip.Prefix { return allIPv6 }
// ExitRoutes returns a slice containing AllIPv4 and AllIPv6.
func ExitRoutes() []netip.Prefix { return []netip.Prefix{allIPv4, allIPv6} }
// IsExitRoute reports whether p is an exit node route.
func IsExitRoute(p netip.Prefix) bool {
return p == allIPv4 || p == allIPv6
}
// SortPrefixes sorts the prefixes in place.
func SortPrefixes(p []netip.Prefix) {
slices.SortFunc(p, netipx.ComparePrefix)
}
// FilterPrefixes returns a new slice, not aliasing in, containing elements of
// in that match f.
func FilterPrefixesCopy(in views.Slice[netip.Prefix], f func(netip.Prefix) bool) []netip.Prefix {
var out []netip.Prefix
for i := range in.Len() {
if v := in.At(i); f(v) {
out = append(out, v)
}
}
return out
}
// IsViaPrefix reports whether p is a CIDR in the Tailscale "via" range.
// See TailscaleViaRange.
func IsViaPrefix(p netip.Prefix) bool {
return TailscaleViaRange().Contains(p.Addr())
}
// UnmapVia returns the IPv4 address that corresponds to the provided Tailscale
// "via" IPv4-in-IPv6 address.
//
// If ip is not a via address, it returns ip unchanged.
func UnmapVia(ip netip.Addr) netip.Addr {
if TailscaleViaRange().Contains(ip) {
a := ip.As16()
return netip.AddrFrom4(*(*[4]byte)(a[12:16]))
}
return ip
}
// MapVia returns an IPv6 "via" route for an IPv4 CIDR in a given siteID.
func MapVia(siteID uint32, v4 netip.Prefix) (via netip.Prefix, err error) {
if !v4.Addr().Is4() {
return via, errors.New("want IPv4 CIDR with a site ID")
}
viaRange16 := TailscaleViaRange().Addr().As16()
var a [16]byte
copy(a[:], viaRange16[:8])
binary.BigEndian.PutUint32(a[8:], siteID)
ip4a := v4.Addr().As4()
copy(a[12:], ip4a[:])
return netip.PrefixFrom(netip.AddrFrom16(a), v4.Bits()+64+32), nil
}
// FirstTailscaleAddrs returns the first Tailscale IPv4 address and
// the first Tailscale IPv6 address among the addresses of addrs'
// prefixes, if any. The addrs sequence is typically a node's own
// address list, either a slice (via [slices.All]) or a view (via
// [views.Slice.All]).
func FirstTailscaleAddrs(addrs iter.Seq2[int, netip.Prefix]) (a4, a6 netip.Addr) {
for _, pfx := range addrs {
a := pfx.Addr()
switch {
case a.Is4() && !a4.IsValid() && IsTailscaleIP(a):
a4 = a
case a.Is6() && !a6.IsValid() && IsTailscaleIP(a):
a6 = a
}
if a4.IsValid() && a6.IsValid() {
break
}
}
return a4, a6
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package tsdial
import (
"errors"
"fmt"
"net"
"strconv"
"strings"
)
// canonMapKey canonicalizes its input s to be a MagicDNS lookup key:
// lowercase with no trailing dot.
func canonMapKey(s string) string {
return strings.ToLower(strings.TrimSuffix(s, "."))
}
// errUnresolved is a sentinel error returned when a hostname is not
// resolvable via MagicDNS.
var errUnresolved = errors.New("address well formed but not resolved")
func splitHostPort(addr string) (host string, port uint16, err error) {
host, portStr, err := net.SplitHostPort(addr)
if err != nil {
return "", 0, err
}
port16, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
return "", 0, fmt.Errorf("invalid port in address %q", addr)
}
return host, uint16(port16), nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package tsdial
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"time"
"tailscale.com/net/dnscache"
)
// dohConn is a net.PacketConn suitable for returning from
// net.Dialer.Dial to send DNS queries over PeerAPI to exit nodes'
// ExitDNS DoH proxy service.
type dohConn struct {
ctx context.Context
baseURL string
hc *http.Client // if nil, default is used
dnsCache *dnscache.MessageCache
rbuf bytes.Buffer
}
var (
_ net.Conn = (*dohConn)(nil)
_ net.PacketConn = (*dohConn)(nil) // be a PacketConn to change net.Resolver semantics
)
func (*dohConn) Close() error { return nil }
func (*dohConn) LocalAddr() net.Addr { return todoAddr{} }
func (*dohConn) RemoteAddr() net.Addr { return todoAddr{} }
func (*dohConn) SetDeadline(t time.Time) error { return nil }
func (*dohConn) SetReadDeadline(t time.Time) error { return nil }
func (*dohConn) SetWriteDeadline(t time.Time) error { return nil }
func (c *dohConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
return c.Write(p)
}
func (c *dohConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
n, err = c.Read(p)
return n, todoAddr{}, err
}
func (c *dohConn) Read(p []byte) (n int, err error) {
return c.rbuf.Read(p)
}
func (c *dohConn) Write(packet []byte) (n int, err error) {
if c.dnsCache != nil {
err := c.dnsCache.ReplyFromCache(&c.rbuf, packet)
if err == nil {
// Cache hit.
// TODO(bradfitz): add clientmetric
return len(packet), nil
}
c.rbuf.Reset()
}
req, err := http.NewRequestWithContext(c.ctx, "POST", c.baseURL, bytes.NewReader(packet))
if err != nil {
return 0, err
}
const dohType = "application/dns-message"
req.Header.Set("Content-Type", dohType)
hc := c.hc
if hc == nil {
hc = http.DefaultClient
}
hres, err := hc.Do(req)
if err != nil {
return 0, err
}
defer hres.Body.Close()
if hres.StatusCode != 200 {
return 0, errors.New(hres.Status)
}
if ct := hres.Header.Get("Content-Type"); ct != dohType {
return 0, fmt.Errorf("unexpected response Content-Type %q", ct)
}
_, err = io.Copy(&c.rbuf, hres.Body)
if err != nil {
return 0, err
}
if c.dnsCache != nil {
c.dnsCache.AddCacheEntry(packet, c.rbuf.Bytes())
}
return len(packet), nil
}
type todoAddr struct{}
func (todoAddr) Network() string { return "unused" }
func (todoAddr) String() string { return "unused-todoAddr" }
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package tsdial provides a Dialer type that can dial out of tailscaled.
package tsdial
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"net/netip"
"runtime"
"slices"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"github.com/gaissmai/bart"
"tailscale.com/envknob"
"tailscale.com/feature"
"tailscale.com/feature/buildfeatures"
"tailscale.com/net/dnscache"
"tailscale.com/net/netknob"
"tailscale.com/net/netmon"
"tailscale.com/net/netns"
"tailscale.com/net/netutil"
"tailscale.com/net/netx"
"tailscale.com/net/tsaddr"
"tailscale.com/syncs"
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
"tailscale.com/util/eventbus"
"tailscale.com/util/mak"
"tailscale.com/util/testenv"
"tailscale.com/version"
)
// NewDialer returns a new Dialer that can dial out of tailscaled.
// Its exported fields should be set before use, if any.
func NewDialer(netMon *netmon.Monitor) *Dialer {
if netMon == nil {
panic("NewDialer: netMon is nil")
}
d := &Dialer{}
d.SetNetMon(netMon)
return d
}
// NewFromFuncForDebug is like NewDialer but takes a netx.DialFunc
// and no netMon. It's meant exclusively for the "tailscale debug ts2021"
// debug command, and perhaps tests.
func NewFromFuncForDebug(logf logger.Logf, dial netx.DialFunc) *Dialer {
return &Dialer{sysDialForTest: dial, Logf: logf}
}
// Dialer dials out of tailscaled, while taking care of details while
// handling the dozens of edge cases depending on the server mode
// (TUN, netstack), the OS network sandboxing style (macOS/iOS
// Extension, none), user-selected route acceptance prefs, etc.
//
// Before use, SetNetMon should be called with a netmon.Monitor.
type Dialer struct {
Logf logger.Logf
// UseNetstackForIP if non-nil is whether NetstackDialTCP (if
// it's non-nil) should be used to dial the provided IP.
UseNetstackForIP func(netip.Addr) bool
// NetstackDialTCP dials the provided IPPort using netstack.
// If nil, it's not used.
NetstackDialTCP func(context.Context, netip.AddrPort) (net.Conn, error)
// NetstackDialUDP dials the provided IPPort using netstack.
// If nil, it's not used.
NetstackDialUDP func(context.Context, netip.AddrPort) (net.Conn, error)
peerClientOnce sync.Once
peerClient *http.Client
peerDialerOnce sync.Once
peerDialer *net.Dialer
netnsDialerOnce sync.Once
netnsDialer netns.Dialer
sysDialForTest netx.DialFunc // or nil
routes atomic.Pointer[bart.Table[bool]] // or nil if UserDial should not use routes. `true` indicates routes that point into the Tailscale interface
// resolveMagicDNS, if non-nil, resolves a MagicDNS hostname (short
// name or FQDN, without trailing dot, lowercased) to an IP address.
// The network parameter ("tcp", "tcp4", "tcp6", "udp", "udp4",
// "udp6") constrains the address family of the result. The normal
// implementation is [ipnlocal.LocalBackend.resolveMagicDNS],
// installed at construction time. It is read without holding mu.
resolveMagicDNS atomic.Pointer[func(hostname, network string) (_ netip.Addr, ok bool)]
mu syncs.Mutex
closed bool
tunName string // tun device name
netMon *netmon.Monitor
netMonUnregister func()
exitDNSDoHBase string // non-empty if DoH-proxying exit node in use; base URL+path (without '?')
dnsCache *dnscache.MessageCache // nil until first non-empty SetExitDNSDoH
nextSysConnID int
activeSysConns map[int]net.Conn // active connections not yet closed
bus *eventbus.Bus // only used for comparison with already set bus.
eventClient *eventbus.Client
eventBusSubs eventbus.Monitor
}
// sysConn wraps a net.Conn that was created using d.SystemDial.
// It exists to track which connections are still open, and should be
// closed on major link changes.
type sysConn struct {
net.Conn
id int
d *Dialer
}
func (c sysConn) Close() error {
c.d.closeSysConn(c.id)
return nil
}
// SetTUNName sets the name of the tun device in use ("tailscale0", "utun6",
// etc). This is needed on some platforms to set sockopts to bind
// to the same interface index.
func (d *Dialer) SetTUNName(name string) {
d.mu.Lock()
defer d.mu.Unlock()
d.tunName = name
}
// TUNName returns the name of the tun device in use, if any.
// Example format ("tailscale0", "utun6").
func (d *Dialer) TUNName() string {
d.mu.Lock()
defer d.mu.Unlock()
return d.tunName
}
// ProbeLocks acquires and releases the dialer's internal mutex.
func (d *Dialer) ProbeLocks() {
d.mu.Lock()
d.mu.Unlock()
}
// SetExitDNSDoH sets (or clears) the exit node DNS DoH server base URL to use.
// The doh URL should contain the scheme, authority, and path, but without
// a '?' and/or query parameters.
//
// For example, "http://100.68.82.120:47830/dns-query".
func (d *Dialer) SetExitDNSDoH(doh string) {
if !buildfeatures.HasUseExitNode {
return
}
d.mu.Lock()
defer d.mu.Unlock()
if d.exitDNSDoHBase == doh {
return
}
d.exitDNSDoHBase = doh
if doh != "" && d.dnsCache == nil {
d.dnsCache = new(dnscache.MessageCache)
}
if d.dnsCache != nil {
d.dnsCache.Flush()
}
}
// SetRoutes configures the dialer to dial the specified routes via Tailscale,
// and the specified localRoutes using the default interface.
func (d *Dialer) SetRoutes(routes, localRoutes []netip.Prefix) {
var rt *bart.Table[bool]
if len(routes) > 0 || len(localRoutes) > 0 {
rt = &bart.Table[bool]{}
for _, r := range routes {
rt.Insert(r, true)
}
for _, r := range localRoutes {
rt.Insert(r, false)
}
d.logf("tsdial: bart table size: %d", rt.Size())
}
d.routes.Store(rt)
}
func (d *Dialer) Close() error {
if d.eventClient != nil {
d.eventBusSubs.Close()
}
d.mu.Lock()
defer d.mu.Unlock()
d.closed = true
if d.netMonUnregister != nil {
d.netMonUnregister()
d.netMonUnregister = nil
}
for _, c := range d.activeSysConns {
c.Close()
}
d.activeSysConns = nil
if buildfeatures.HasPeerAPIClient {
d.PeerAPITransport().CloseIdleConnections()
}
return nil
}
// SetNetMon sets d's network monitor to netMon.
// It is a no-op to call SetNetMon with the same netMon as the current one.
func (d *Dialer) SetNetMon(netMon *netmon.Monitor) {
d.mu.Lock()
defer d.mu.Unlock()
if d.netMon == netMon {
return
}
if d.netMonUnregister != nil {
go d.netMonUnregister()
d.netMonUnregister = nil
}
d.netMon = netMon
// Having multiple watchers could lead to problems,
// so remove the eventClient if it exists.
// This should really not happen, but better checking for it than not.
// TODO(cmol): Should this just be a panic?
if d.eventClient != nil {
d.eventBusSubs.Close()
d.eventClient = nil
}
d.netMonUnregister = d.netMon.RegisterChangeCallback(d.linkChanged)
}
// NetMon returns the Dialer's network monitor.
// It returns nil if SetNetMon has not been called.
func (d *Dialer) NetMon() *netmon.Monitor {
d.mu.Lock()
defer d.mu.Unlock()
return d.netMon
}
func (d *Dialer) SetBus(bus *eventbus.Bus) {
d.mu.Lock()
defer d.mu.Unlock()
if d.bus == bus {
return
} else if d.bus != nil {
panic("different eventbus has already been set")
}
// Having multiple watchers could lead to problems,
// so unregister the callback if it exists.
if d.netMonUnregister != nil {
d.netMonUnregister()
}
d.bus = bus
d.eventClient = bus.Client("tsdial.Dialer")
d.eventBusSubs = d.eventClient.Monitor(d.linkChangeWatcher(d.eventClient))
}
func (d *Dialer) linkChangeWatcher(ec *eventbus.Client) func(*eventbus.Client) {
linkChangeSub := eventbus.Subscribe[netmon.ChangeDelta](ec)
return func(ec *eventbus.Client) {
for {
select {
case <-ec.Done():
return
case cd := <-linkChangeSub.Events():
d.linkChanged(&cd)
}
}
}
}
var (
metricLinkChangeConnClosed = clientmetric.NewCounter("tsdial_linkchange_closes")
metricChangeDeltaNoDefaultRoute = clientmetric.NewCounter("tsdial_changedelta_no_default_route")
)
func (d *Dialer) linkChanged(delta *netmon.ChangeDelta) {
// Track how often we see ChangeDeltas with no DefaultRouteInterface.
if delta.DefaultRouteInterface == "" {
metricChangeDeltaNoDefaultRoute.Add(1)
}
d.mu.Lock()
defer d.mu.Unlock()
var anyClosed bool
for id, c := range d.activeSysConns {
if changeAffectsConn(delta, c) {
anyClosed = true
d.logf("tsdial: closing system connection %v->%v due to link change", c.LocalAddr(), c.RemoteAddr())
go c.Close()
delete(d.activeSysConns, id)
}
}
if anyClosed {
metricLinkChangeConnClosed.Add(1)
}
}
// changeAffectsConn reports whether the network change delta affects
// the provided connection.
func changeAffectsConn(delta *netmon.ChangeDelta, conn net.Conn) bool {
la, _ := conn.LocalAddr().(*net.TCPAddr)
ra, _ := conn.RemoteAddr().(*net.TCPAddr)
if la == nil || ra == nil {
return false // not TCP
}
lip, rip := la.AddrPort().Addr(), ra.AddrPort().Addr()
if delta.IsInitialState {
return false
}
if delta.DefaultInterfaceChanged ||
delta.HasPACOrProxyConfigChanged {
return true
}
// In a few cases, we don't have a new DefaultRouteInterface (e.g. on
// Android and macOS/iOS; see tailscale/corp#19124); if so, pessimistically assume
// that all connections are affected.
if delta.DefaultRouteInterface == "" && runtime.GOOS != "plan9" {
return true
}
if delta.InterfaceIPDisappeared(lip) {
// Our interface with this source IP went away.
return true
}
_ = rip // TODO(bradfitz): use the remote IP?
return false
}
func (d *Dialer) closeSysConn(id int) {
d.mu.Lock()
defer d.mu.Unlock()
c, ok := d.activeSysConns[id]
if !ok {
return
}
delete(d.activeSysConns, id)
go c.Close() // ignore the error
}
// peerDialControlFunc is non-nil on platforms that require a way to
// bind to dial out to other peers.
var peerDialControlFunc func(*Dialer) func(network, address string, c syscall.RawConn) error
// PeerDialControlFunc returns a function
// that can assigned to net.Dialer.Control to set sockopts or whatnot
// to make a dial escape the current platform's network sandbox.
//
// On many platforms the returned func will be nil.
//
// Notably, this is non-nil on iOS and macOS when run as a Network or
// System Extension (the GUI variants).
func (d *Dialer) PeerDialControlFunc() func(network, address string, c syscall.RawConn) error {
if peerDialControlFunc == nil {
return nil
}
return peerDialControlFunc(d)
}
// SetResolveMagicDNS installs a callback that resolves MagicDNS hostnames
// to IP addresses for UserDial.
func (d *Dialer) SetResolveMagicDNS(fn func(hostname, network string) (_ netip.Addr, ok bool)) {
if fn == nil {
d.resolveMagicDNS.Store(nil)
return
}
d.resolveMagicDNS.Store(&fn)
}
// resolveAddr tries to resolve addr (a "host:port" string) via MagicDNS.
// The network parameter ("tcp", "tcp4", "tcp6", etc.) constrains the
// address family. It returns errUnresolved if the hostname is not a
// known MagicDNS name.
func (d *Dialer) resolveAddr(_ context.Context, network, addr string) (netip.AddrPort, error) {
host, port, err := splitHostPort(addr)
if err != nil {
return netip.AddrPort{}, err
}
if ip, err := netip.ParseAddr(host); err == nil {
return netip.AddrPortFrom(ip, port), nil
}
if fn := d.resolveMagicDNS.Load(); fn != nil {
if ip, ok := (*fn)(canonMapKey(host), network); ok {
return netip.AddrPortFrom(ip, port), nil
}
}
return netip.AddrPort{}, errUnresolved
}
// userDialResolveAll resolves addr as if a user initiating the dial.
// (e.g. from a SOCKS or HTTP outbound proxy.)
//
// It returns all candidate addresses so that the caller can apply
// happy eyeballs across address families. The returned slice is
// non-empty on a nil-error return.
func (d *Dialer) userDialResolveAll(ctx context.Context, network, addr string) ([]netip.AddrPort, error) {
d.mu.Lock()
exitDNSDoH := d.exitDNSDoHBase
d.mu.Unlock()
// MagicDNS or otherwise baked into the NetworkMap? Try that first.
// Tailnet names have one IP each, so there's nothing to race.
ipp, err := d.resolveAddr(ctx, network, addr)
if err != errUnresolved {
if err != nil {
return nil, err
}
return []netip.AddrPort{ipp}, nil
}
// Otherwise, hit the network.
// TODO(bradfitz): wire up net/dnscache too.
host, port, err := splitHostPort(addr)
if err != nil {
// addr is malformed.
return nil, err
}
var r net.Resolver
if buildfeatures.HasUseExitNode && buildfeatures.HasPeerAPIClient && exitDNSDoH != "" {
r.PreferGo = true
r.Dial = func(ctx context.Context, network, address string) (net.Conn, error) {
return &dohConn{
ctx: ctx,
baseURL: exitDNSDoH,
hc: d.PeerAPIHTTPClient(),
dnsCache: d.dnsCache,
}, nil
}
}
ips, err := r.LookupIP(ctx, ipNetOfNetwork(network), host)
if err != nil {
return nil, err
}
out := make([]netip.AddrPort, 0, len(ips))
for _, stdIP := range ips {
ip, ok := netip.AddrFromSlice(stdIP)
if !ok {
continue
}
out = append(out, netip.AddrPortFrom(ip.Unmap(), port))
}
if len(out) == 0 {
return nil, fmt.Errorf("DNS lookup returned no results for %q", host)
}
if debugPreferIPv6() {
slices.SortStableFunc(out, func(a, b netip.AddrPort) int {
a6 := a.Addr().Is6()
b6 := b.Addr().Is6()
if a6 == b6 {
return 0
}
if a6 {
return -1
}
return 1
})
}
return out, nil
}
// userDialResolve resolves addr and returns the first candidate.
// It is for callers that don't perform happy-eyeballs (notably
// [Dialer.UserDialPlan], which only needs to classify one IP).
func (d *Dialer) userDialResolve(ctx context.Context, network, addr string) (netip.AddrPort, error) {
ipps, err := d.userDialResolveAll(ctx, network, addr)
if err != nil {
return netip.AddrPort{}, err
}
return ipps[0], nil
}
// debugPreferIPv6 forces userDialResolveAll to sort AAAA results before
// A results, reproducing the failure mode where a client on an IPv6-capable
// host picks an unreachable AAAA address through an IPv4-only exit node.
// Used by TestExitNodeV4Only to exercise the happy-eyeballs fallback.
var debugPreferIPv6 = envknob.RegisterBool("TS_DEBUG_PREFER_IPV6_USERDIAL")
// ipNetOfNetwork returns "ip", "ip4", or "ip6" corresponding
// to the input value of "tcp", "tcp4", "udp6" etc network
// names.
func ipNetOfNetwork(n string) string {
if strings.HasSuffix(n, "4") {
return "ip4"
}
if strings.HasSuffix(n, "6") {
return "ip6"
}
return "ip"
}
func (d *Dialer) logf(format string, args ...any) {
if d.Logf != nil {
d.Logf(format, args...)
}
}
// SetSystemDialerForTest sets an alternate function to use for SystemDial
// instead of netns.Dialer. This is intended for use with nettest.MemoryNetwork.
func (d *Dialer) SetSystemDialerForTest(fn netx.DialFunc) {
testenv.AssertInTest()
d.sysDialForTest = fn
}
// SystemDial connects to the provided network address without going over
// Tailscale. It prefers going over the default interface and closes existing
// connections if the default interface changes. It is used to connect to
// Control and (in the future, as of 2022-04-27) DERPs..
func (d *Dialer) SystemDial(ctx context.Context, network, addr string) (net.Conn, error) {
d.mu.Lock()
if d.netMon == nil && d.sysDialForTest == nil {
d.mu.Unlock()
if testenv.InTest() {
panic("SystemDial requires a netmon.Monitor; call SetNetMon first")
}
return nil, errors.New("SystemDial requires a netmon.Monitor; call SetNetMon first")
}
closed := d.closed
d.mu.Unlock()
if closed {
return nil, net.ErrClosed
}
var c net.Conn
var err error
if d.sysDialForTest != nil {
c, err = d.sysDialForTest(ctx, network, addr)
} else {
d.netnsDialerOnce.Do(func() {
d.netnsDialer = netns.NewDialer(d.logf, d.netMon)
})
c, err = d.netnsDialer.DialContext(ctx, network, addr)
}
if err != nil {
return nil, err
}
d.mu.Lock()
defer d.mu.Unlock()
id := d.nextSysConnID
d.nextSysConnID++
mak.Set(&d.activeSysConns, id, c)
return sysConn{
id: id,
d: d,
Conn: c,
}, nil
}
// UserDial connects to the provided network address as if a user were
// initiating the dial. (e.g. from a SOCKS or HTTP outbound proxy)
//
// For TCP, if the name resolves to multiple addresses, UserDial races
// connect attempts across address families with a happy-eyeballs delay
// and returns the first one that succeeds. This lets dual-stack names
// work via an exit node whose egress is single-family without the
// caller needing to know which family the exit node can reach.
func (d *Dialer) UserDial(ctx context.Context, network, addr string) (net.Conn, error) {
ipps, err := d.userDialResolveAll(ctx, network, addr)
if err != nil {
return nil, err
}
// Happy eyeballs is a no-op (and undefined) for UDP; there's no
// connect to race.
if len(ipps) == 1 || strings.HasPrefix(network, "udp") {
return d.dialOneUser(ctx, network, ipps[0])
}
// Family filtering for "tcp4"/"tcp6" is already handled by
// userDialResolveAll (via ipNetOfNetwork), so ipps only contains
// addresses of the requested family by this point.
return d.raceDialUser(ctx, ipps)
}
// dialOneUser dials ipp using the appropriate transport for a user
// dial (netstack, peer dialer, system dialer, or std dialer) based
// on what kind of address ipp is.
func (d *Dialer) dialOneUser(ctx context.Context, network string, ipp netip.AddrPort) (net.Conn, error) {
if d.UseNetstackForIP != nil && d.UseNetstackForIP(ipp.Addr()) {
if d.NetstackDialTCP == nil || d.NetstackDialUDP == nil {
return nil, errors.New("Dialer not initialized correctly")
}
if strings.HasPrefix(network, "udp") {
return d.NetstackDialUDP(ctx, ipp)
}
return d.NetstackDialTCP(ctx, ipp)
}
if routes := d.routes.Load(); routes != nil {
if isTailscaleRoute, _ := routes.Lookup(ipp.Addr()); isTailscaleRoute {
return d.getPeerDialer().DialContext(ctx, network, ipp.String())
}
return d.SystemDial(ctx, network, ipp.String())
}
// Workaround for macOS for now: dial Tailscale IPs with peer dialer.
// TODO(bradfitz): fix dialing subnet routers, public IPs via exit nodes,
// etc. This is a temporary partial for macOS. We need to plumb ART tables &
// prefs & host routing table updates around in more places. We just don't
// know from the limited context here how to dial properly.
if version.IsMacGUIVariant() && tsaddr.IsTailscaleIP(ipp.Addr()) {
return d.getPeerDialer().DialContext(ctx, network, ipp.String())
}
// TODO(bradfitz): netns, etc
var stdDialer net.Dialer
return stdDialer.DialContext(ctx, network, ipp.String())
}
// userDialFallbackDelay is the happy-eyeballs gap between starting
// successive connect attempts. 300ms matches Go's net.Dialer default
// and the value used by net/dnscache.
const userDialFallbackDelay = 300 * time.Millisecond
// raceDialUser races connect attempts across ipps with a happy-eyeballs
// fallback delay, returning the first to succeed. Losers are cancelled
// and any conns they produce are closed. If all fail, the first error
// is returned.
func (d *Dialer) raceDialUser(ctx context.Context, ipps []netip.AddrPort) (net.Conn, error) {
return netx.RaceDial(ctx, ipps,
func(ctx context.Context, network, address string) (net.Conn, error) {
return d.dialOneUser(ctx, network, netip.MustParseAddrPort(address))
},
userDialFallbackDelay,
)
}
// UserDialPlan resolves addr and reports whether the dialer would
// handle it via Tailscale. If viaTailscale is false, the resolved
// address is not a Tailscale route and the caller may dial it directly.
//
// Warning: there is a TOCTOU race if addr contains a DNS name and the
// caller subsequently passes the same DNS name to [Dialer.UserDial], as DNS
// may resolve differently the second time. Callers who want to only
// dial over Tailscale should call [Dialer.UserDial] with the returned
// ipp.String() (an IP:port) rather than the original DNS name.
func (d *Dialer) UserDialPlan(ctx context.Context, network, addr string) (ipp netip.AddrPort, viaTailscale bool, err error) {
ipp, err = d.userDialResolve(ctx, network, addr)
if err != nil {
return netip.AddrPort{}, false, err
}
if d.UseNetstackForIP != nil && d.UseNetstackForIP(ipp.Addr()) {
return ipp, true, nil
}
if routes := d.routes.Load(); routes != nil {
isTailscaleRoute, _ := routes.Lookup(ipp.Addr())
return ipp, isTailscaleRoute, nil
}
if version.IsMacGUIVariant() && tsaddr.IsTailscaleIP(ipp.Addr()) {
return ipp, true, nil
}
return ipp, false, nil
}
// dialPeerAPI connects to a Tailscale peer's peerapi over TCP.
//
// network must a "tcp" type, and addr must be an ip:port. Name resolution
// is not supported.
func (d *Dialer) dialPeerAPI(ctx context.Context, network, addr string) (net.Conn, error) {
if !buildfeatures.HasPeerAPIClient {
return nil, feature.ErrUnavailable
}
switch network {
case "tcp", "tcp6", "tcp4":
default:
return nil, fmt.Errorf("peerAPI dial requires tcp; %q not supported", network)
}
ipp, err := netip.ParseAddrPort(addr)
if err != nil {
return nil, fmt.Errorf("peerAPI dial requires ip:port, not name resolution: %w", err)
}
if d.UseNetstackForIP != nil && d.UseNetstackForIP(ipp.Addr()) {
if d.NetstackDialTCP == nil {
return nil, errors.New("Dialer not initialized correctly")
}
return d.NetstackDialTCP(ctx, ipp)
}
return d.getPeerDialer().DialContext(ctx, network, addr)
}
// getPeerDialer returns the *net.Dialer to use to dial peers (e.g. for peerapi,
// "tailscale nc", or querying internal DNS servers over Tailscale)
//
// This is not used in netstack mode.
//
// The primary function of this is to work on macOS & iOS's in the
// Network/System Extension so it can mark the dialer as staying within the
// network namespace/sandbox.
func (d *Dialer) getPeerDialer() *net.Dialer {
d.peerDialerOnce.Do(func() {
d.peerDialer = &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: netknob.PlatformTCPKeepAlive(),
Control: d.PeerDialControlFunc(),
}
})
return d.peerDialer
}
// PeerAPIHTTPClient returns an HTTP Client to call peers' peerapi
// endpoints. //
// The returned Client must not be mutated; it's owned by the Dialer
// and shared by callers.
func (d *Dialer) PeerAPIHTTPClient() *http.Client {
if !buildfeatures.HasPeerAPIClient {
panic("unreachable")
}
d.peerClientOnce.Do(func() {
t := netutil.NewDefaultTransport()
t.Dial = nil
t.DialContext = d.dialPeerAPI
// Do not use the environment proxy for PeerAPI.
t.Proxy = nil
d.peerClient = &http.Client{Transport: t}
})
return d.peerClient
}
// PeerAPITransport returns a Transport to call peers' peerapi
// endpoints.
//
// The returned value must not be mutated; it's owned by the Dialer
// and shared by callers.
func (d *Dialer) PeerAPITransport() *http.Transport {
return d.PeerAPIHTTPClient().Transport.(*http.Transport)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package paths
import (
"os"
"path/filepath"
"tailscale.com/types/logger"
)
// TryConfigFileMigration carefully copies the contents of oldFile to
// newFile, returning the path which should be used to read the config.
// - if newFile already exists, don't modify it just return its path
// - if neither oldFile nor newFile exist, return newFile for a fresh
// default config to be written to.
// - if oldFile exists but copying to newFile fails, return oldFile so
// there will at least be some config to work with.
func TryConfigFileMigration(logf logger.Logf, oldFile, newFile string) string {
_, err := os.Stat(newFile)
if err == nil {
// Common case for a system which has already been migrated.
return newFile
}
if !os.IsNotExist(err) {
logf("TryConfigFileMigration failed; new file: %v", err)
return newFile
}
contents, err := os.ReadFile(oldFile)
if err != nil {
// Common case for a new user.
return newFile
}
if err = MkStateDir(filepath.Dir(newFile)); err != nil {
logf("TryConfigFileMigration failed; MkStateDir: %v", err)
return oldFile
}
err = os.WriteFile(newFile, contents, 0600)
if err != nil {
removeErr := os.Remove(newFile)
if removeErr != nil {
logf("TryConfigFileMigration failed; write newFile no cleanup: %v, remove err: %v",
err, removeErr)
return oldFile
}
logf("TryConfigFileMigration failed; write newFile: %v", err)
return oldFile
}
logf("TryConfigFileMigration: successfully migrated: from %v to %v",
oldFile, newFile)
return newFile
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package paths returns platform and user-specific default paths to
// Tailscale files and directories.
package paths
import (
"log"
"os"
"path/filepath"
"runtime"
"tailscale.com/syncs"
"tailscale.com/version/distro"
)
// AppSharedDir is a string set by the iOS or Android app on start
// containing a directory we can read/write in.
var AppSharedDir syncs.AtomicValue[string]
// DefaultTailscaledSocket returns the path to the tailscaled Unix socket
// or the empty string if there's no reasonable default.
func DefaultTailscaledSocket() string {
if runtime.GOOS == "windows" {
return `\\.\pipe\ProtectedPrefix\Administrators\Tailscale\tailscaled`
}
if runtime.GOOS == "darwin" {
return "/var/run/tailscaled.socket"
}
if runtime.GOOS == "plan9" {
return "/srv/tailscaled.sock"
}
if runtime.GOOS == "android" {
// Android (e.g. a userspace tailscaled under Termux) has no /var,
// and the cwd-relative fallback below only works when the daemon
// and the CLI happen to be started from the same directory
// (see #21161). Use the shared per-app temp directory so both
// agree on an absolute path.
return filepath.Join(os.TempDir(), "tailscaled.sock")
}
switch distro.Get() {
case distro.Synology:
if distro.DSMVersion() == 6 {
return "/var/packages/Tailscale/etc/tailscaled.sock"
}
// DSM 7 (and higher? or failure to detect.)
return "/var/packages/Tailscale/var/tailscaled.sock"
case distro.Gokrazy:
return "/perm/tailscaled/tailscaled.sock"
case distro.QNAP:
return "/tmp/tailscale/tailscaled.sock"
}
if fi, err := os.Stat("/var/run"); err == nil && fi.IsDir() {
return "/var/run/tailscale/tailscaled.sock"
}
return "tailscaled.sock"
}
// Overridden in init by OS-specific files.
var (
stateFileFunc func() string
// ensureStateDirPerms applies a restrictive ACL/chmod
// to the provided directory.
ensureStateDirPerms = func(string) error { return nil }
)
// DefaultTailscaledStateFile returns the default path to the
// tailscaled state file, or the empty string if there's no reasonable
// default value.
func DefaultTailscaledStateFile() string {
if f := stateFileFunc; f != nil {
return f()
}
if runtime.GOOS == "windows" {
return filepath.Join(os.Getenv("ProgramData"), "Tailscale", "server-state.conf")
}
return ""
}
// DefaultTailscaledStateDir returns the default state directory
// to use for tailscaled, for use when the user provided neither
// a state directory or state file path to use.
//
// It returns the empty string if there's no reasonable default.
func DefaultTailscaledStateDir() string {
if runtime.GOOS == "plan9" {
home, err := os.UserHomeDir()
if err != nil {
log.Fatalf("failed to get home directory: %v", err)
}
return filepath.Join(home, "tailscale-state")
}
return filepath.Dir(DefaultTailscaledStateFile())
}
// MakeAutomaticStateDir reports whether the platform
// automatically creates the state directory for tailscaled
// when it's absent.
func MakeAutomaticStateDir() bool {
switch runtime.GOOS {
case "plan9":
return true
case "linux":
if distro.Get() == distro.JetKVM {
return true
}
}
return false
}
// MkStateDir ensures that dirPath, the daemon's configuration directory
// containing machine keys etc, both exists and has the correct permissions.
// We want it to only be accessible to the user the daemon is running under.
func MkStateDir(dirPath string) error {
if err := os.MkdirAll(dirPath, 0700); err != nil {
return err
}
return ensureStateDirPerms(dirPath)
}
// LegacyStateFilePath returns the legacy path to the state file when
// it was stored under the current user's %LocalAppData%.
//
// It is only called on Windows.
func LegacyStateFilePath() string {
if runtime.GOOS == "windows" {
return filepath.Join(os.Getenv("LocalAppData"), "Tailscale", "server-state.conf")
}
return ""
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !windows && !wasm && !plan9 && !tamago
package paths
import (
"fmt"
"os"
"path/filepath"
"runtime"
"golang.org/x/sys/unix"
"tailscale.com/version/distro"
)
func init() {
stateFileFunc = stateFileUnix
ensureStateDirPerms = ensureStateDirPermsUnix
}
func statePath() string {
if runtime.GOOS == "linux" && distro.Get() == distro.JetKVM {
return "/userdata/tailscale/var/tailscaled.state"
}
switch runtime.GOOS {
case "linux", "illumos", "solaris":
return "/var/lib/tailscale/tailscaled.state"
case "freebsd", "openbsd":
return "/var/db/tailscale/tailscaled.state"
case "darwin":
return "/Library/Tailscale/tailscaled.state"
case "aix":
return "/var/tailscale/tailscaled.state"
default:
return ""
}
}
func stateFileUnix() string {
if distro.Get() == distro.Gokrazy {
return "/perm/tailscaled/tailscaled.state"
}
path := statePath()
if path == "" {
return ""
}
try := path
for range 3 { // check writability of the file, /var/lib/tailscale, and /var/lib
err := unix.Access(try, unix.O_RDWR)
if err == nil {
return path
}
try = filepath.Dir(try)
}
if os.Getuid() == 0 {
return ""
}
// For non-root users, fall back to $XDG_DATA_HOME/tailscale/*.
return filepath.Join(xdgDataHome(), "tailscale", "tailscaled.state")
}
func xdgDataHome() string {
if e := os.Getenv("XDG_DATA_HOME"); e != "" {
return e
}
return filepath.Join(os.Getenv("HOME"), ".local/share")
}
func ensureStateDirPermsUnix(dir string) error {
if filepath.Base(dir) != "tailscale" {
return nil
}
fi, err := os.Stat(dir)
if err != nil {
return err
}
if !fi.IsDir() {
return fmt.Errorf("expected %q to be a directory; is %v", dir, fi.Mode())
}
const perm = 0700
if fi.Mode().Perm() == perm {
// Already correct.
return nil
}
return os.Chmod(dir, perm)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package safesocket creates either a Unix socket, if possible, or
// otherwise a localhost TCP connection.
package safesocket
import (
"context"
"errors"
"net"
"runtime"
"time"
"tailscale.com/feature"
"tailscale.com/feature/buildfeatures"
)
type closeable interface {
CloseRead() error
CloseWrite() error
}
// ConnCloseRead calls c's CloseRead method. c is expected to be
// either a UnixConn or TCPConn as returned from this package.
func ConnCloseRead(c net.Conn) error {
return c.(closeable).CloseRead()
}
// ConnCloseWrite calls c's CloseWrite method. c is expected to be
// either a UnixConn or TCPConn as returned from this package.
func ConnCloseWrite(c net.Conn) error {
return c.(closeable).CloseWrite()
}
var processStartTime = time.Now()
var tailscaledProcExists feature.Hook[func() bool]
// tailscaledStillStarting reports whether tailscaled is probably
// still starting up. That is, it reports whether the caller should
// keep retrying to connect.
func tailscaledStillStarting() bool {
d := time.Since(processStartTime)
if d < 2*time.Second {
// Without even checking the process table, assume
// that for the first two seconds that tailscaled is
// probably still starting. That is, assume they're
// running "tailscaled & tailscale up ...." and make
// the tailscale client block for a bit for tailscaled
// to start accepting on the socket.
return true
}
if d > 5*time.Second {
return false
}
f, ok := tailscaledProcExists.GetOk()
return ok && f()
}
// ConnectContext connects to tailscaled using a unix socket or named pipe.
func ConnectContext(ctx context.Context, path string) (net.Conn, error) {
for {
c, err := connect(ctx, path)
if err != nil && tailscaledStillStarting() {
if ctx.Err() != nil {
return nil, ctx.Err()
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(250 * time.Millisecond):
}
continue
}
return c, err
}
}
// Connect connects to tailscaled using a unix socket or named pipe.
// Deprecated: use ConnectContext instead.
func Connect(path string) (net.Conn, error) {
return ConnectContext(context.Background(), path)
}
// Listen returns a listener either on Unix socket path (on Unix), or
// the NamedPipe path (on Windows).
func Listen(path string) (net.Listener, error) {
return listen(path)
}
var (
ErrTokenNotFound = errors.New("no token found")
ErrNoTokenOnOS = errors.New("no token on " + runtime.GOOS)
)
var localTCPPortAndToken func() (port int, token string, err error)
// LocalTCPPortAndToken returns the port number and auth token to connect to
// the local Tailscale daemon. It's currently only applicable on macOS
// when tailscaled is being run in the Mac Sandbox from the App Store version
// of Tailscale.
func LocalTCPPortAndToken() (port int, token string, err error) {
if localTCPPortAndToken == nil {
return 0, "", ErrNoTokenOnOS
}
return localTCPPortAndToken()
}
// PlatformUsesPeerCreds reports whether the current platform uses peer credentials
// to authenticate connections.
func PlatformUsesPeerCreds() bool {
if !buildfeatures.HasUnixSocketIdentity {
return false
}
return GOOSUsesPeerCreds(runtime.GOOS)
}
// GOOSUsesPeerCreds is like PlatformUsesPeerCreds but takes a
// runtime.GOOS value instead of using the current one.
func GOOSUsesPeerCreds(goos string) bool {
switch goos {
case "linux", "darwin", "freebsd", "solaris", "illumos":
return true
}
return false
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build ((linux && !android) || windows || (darwin && !ios) || freebsd) && !ts_omit_cliconndiag
package safesocket
import (
"strings"
ps "github.com/mitchellh/go-ps"
)
func init() {
tailscaledProcExists.Set(func() bool {
procs, err := ps.Processes()
if err != nil {
return false
}
for _, proc := range procs {
name := proc.Executable()
const tailscaled = "tailscaled"
if len(name) < len(tailscaled) {
continue
}
// Do case insensitive comparison for Windows,
// notably, and ignore any ".exe" suffix.
if strings.EqualFold(name[:len(tailscaled)], tailscaled) {
return true
}
}
return false
})
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !windows && !js && !plan9
package safesocket
import (
"context"
"fmt"
"log"
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
)
func connect(ctx context.Context, path string) (net.Conn, error) {
var std net.Dialer
return std.DialContext(ctx, "unix", path)
}
func listen(path string) (net.Listener, error) {
// Unix sockets hang around in the filesystem even after nobody
// is listening on them. (Which is really unfortunate but long-
// entrenched semantics.) Try connecting first; if it works, then
// the socket is still live, so let's not replace it. If it doesn't
// work, then replace it.
//
// Note that there's a race condition between these two steps. A
// "proper" daemon usually uses a dance involving pidfiles to first
// ensure that no other instances of itself are running, but that's
// beyond the scope of our simple socket library.
c, err := net.Dial("unix", path)
if err == nil {
c.Close()
if tailscaledRunningUnderLaunchd() {
return nil, fmt.Errorf("%v: address already in use; tailscaled already running under launchd (to stop, run: $ sudo launchctl stop com.tailscale.tailscaled)", path)
}
return nil, fmt.Errorf("%v: address already in use", path)
}
_ = os.Remove(path)
perm := socketPermissionsForOS()
sockDir := filepath.Dir(path)
if _, err := os.Stat(sockDir); os.IsNotExist(err) {
os.MkdirAll(sockDir, 0755) // best effort
// If we're on a platform where we want the socket
// world-readable, open up the permissions on the
// just-created directory too, in case a umask ate
// it. This primarily affects running tailscaled by
// hand as root in a shell, as there is no umask when
// running under systemd.
if perm == 0666 {
if fi, err := os.Stat(sockDir); err == nil && fi.Mode()&0077 == 0 {
if err := os.Chmod(sockDir, 0755); err != nil {
log.Print(err)
}
}
}
}
pipe, err := net.Listen("unix", path)
if err != nil {
return nil, err
}
os.Chmod(path, perm)
return pipe, err
}
func tailscaledRunningUnderLaunchd() bool {
if runtime.GOOS != "darwin" {
return false
}
plist, err := exec.Command("launchctl", "list", "com.tailscale.tailscaled").Output()
_ = plist // parse it? https://github.com/DHowett/go-plist if we need something.
running := err == nil
return running
}
// socketPermissionsForOS returns the permissions to use for the
// tailscaled.sock.
func socketPermissionsForOS() os.FileMode {
if PlatformUsesPeerCreds() {
return 0666
}
// Otherwise, root only.
return 0600
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package syncs
import (
"sync"
)
// AssertLocked panics if m is not locked.
func AssertLocked(m *Mutex) {
if m.TryLock() {
m.Unlock()
panic("mutex is not locked")
}
}
// AssertRLocked panics if rw is not locked for reading or writing.
func AssertRLocked(rw *RWMutex) {
if rw.TryLock() {
rw.Unlock()
panic("mutex is not locked")
}
}
// AssertWLocked panics if rw is not locked for writing.
func AssertWLocked(rw *sync.RWMutex) {
if rw.TryRLock() {
rw.RUnlock()
panic("mutex is not rlocked")
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_mutex_debug
package syncs
import "sync"
// Mutex is an alias for sync.Mutex.
//
// It's only not a sync.Mutex when built with the ts_mutex_debug build tag.
type Mutex = sync.Mutex
// RWMutex is an alias for sync.RWMutex.
//
// It's only not a sync.RWMutex when built with the ts_mutex_debug build tag.
type RWMutex = sync.RWMutex
// RequiresMutex declares the caller assumes it has the given
// mutex held. In non-debug builds, it's a no-op and compiles to
// nothing.
func RequiresMutex(mu *sync.Mutex) {}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package syncs
import "sync"
// Pool is the generic version of [sync.Pool].
type Pool[T any] struct {
pool sync.Pool
// New optionally specifies a function to generate
// a value when Get would otherwise return the zero value of T.
// It may not be changed concurrently with calls to Get.
New func() T
}
// Get selects an arbitrary item from the Pool, removes it from the Pool,
// and returns it to the caller. See [sync.Pool.Get].
func (p *Pool[T]) Get() T {
x, ok := p.pool.Get().(T)
if !ok && p.New != nil {
x = p.New()
}
return x
}
// Put adds x to the pool.
func (p *Pool[T]) Put(x T) {
p.pool.Put(x)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package syncs
import (
"encoding/json"
"strconv"
"sync/atomic"
"golang.org/x/sys/cpu"
)
// ShardedInt provides a sharded atomic int64 value that optimizes high
// frequency (Mhz range and above) writes in highly parallel workloads.
// The zero value is not safe for use; use [NewShardedInt].
// ShardedInt implements the expvar.Var interface.
type ShardedInt struct {
sv *ShardValue[intShard]
}
// NewShardedInt returns a new [ShardedInt].
func NewShardedInt() *ShardedInt {
return &ShardedInt{
sv: NewShardValue[intShard](),
}
}
// Add adds delta to the value.
func (m *ShardedInt) Add(delta int64) {
m.sv.One(func(v *intShard) {
v.Add(delta)
})
}
type intShard struct {
atomic.Int64
_ cpu.CacheLinePad // avoid false sharing of neighboring shards
}
// Value returns the current value.
func (m *ShardedInt) Value() int64 {
var v int64
for s := range m.sv.All {
v += s.Load()
}
return v
}
// GetDistribution returns the current value in each shard.
// This is intended for observability/debugging only.
func (m *ShardedInt) GetDistribution() []int64 {
v := make([]int64, 0, m.sv.Len())
for s := range m.sv.All {
v = append(v, s.Load())
}
return v
}
// String implements the expvar.Var interface
func (m *ShardedInt) String() string {
v, _ := json.Marshal(m.Value())
return string(v)
}
// AppendText implements the encoding.TextAppender interface
func (m *ShardedInt) AppendText(b []byte) ([]byte, error) {
return strconv.AppendInt(b, m.Value(), 10), nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package syncs
import (
"sync"
"golang.org/x/sys/cpu"
)
// ShardedMap is a synchronized map[K]V, internally sharded by a user-defined
// K-sharding function.
//
// The zero value is not safe for use; use NewShardedMap.
type ShardedMap[K comparable, V any] struct {
shardFunc func(K) int
shards []mapShard[K, V]
}
type mapShard[K comparable, V any] struct {
mu sync.Mutex
m map[K]V
_ cpu.CacheLinePad // avoid false sharing of neighboring shards' mutexes
}
// NewShardedMap returns a new ShardedMap with the given number of shards and
// sharding function.
//
// The shard func must return a integer in the range [0, shards) purely
// deterministically based on the provided K.
func NewShardedMap[K comparable, V any](shards int, shard func(K) int) *ShardedMap[K, V] {
m := &ShardedMap[K, V]{
shardFunc: shard,
shards: make([]mapShard[K, V], shards),
}
for i := range m.shards {
m.shards[i].m = make(map[K]V)
}
return m
}
func (m *ShardedMap[K, V]) shard(key K) *mapShard[K, V] {
return &m.shards[m.shardFunc(key)]
}
// GetOk returns m[key] and whether it was present.
func (m *ShardedMap[K, V]) GetOk(key K) (value V, ok bool) {
shard := m.shard(key)
shard.mu.Lock()
defer shard.mu.Unlock()
value, ok = shard.m[key]
return
}
// Get returns m[key] or the zero value of V if key is not present.
func (m *ShardedMap[K, V]) Get(key K) (value V) {
value, _ = m.GetOk(key)
return
}
// Mutate atomically mutates m[k] by calling mutator.
//
// The mutator function is called with the old value (or its zero value) and
// whether it existed in the map and it returns the new value and whether it
// should be set in the map (true) or deleted from the map (false).
//
// It returns the change in size of the map as a result of the mutation, one of
// -1 (delete), 0 (change), or 1 (addition).
func (m *ShardedMap[K, V]) Mutate(key K, mutator func(oldValue V, oldValueExisted bool) (newValue V, keep bool)) (sizeDelta int) {
shard := m.shard(key)
shard.mu.Lock()
defer shard.mu.Unlock()
oldV, oldOK := shard.m[key]
newV, newOK := mutator(oldV, oldOK)
if newOK {
shard.m[key] = newV
if oldOK {
return 0
}
return 1
}
delete(shard.m, key)
if oldOK {
return -1
}
return 0
}
// Set sets m[key] = value.
//
// present in m).
func (m *ShardedMap[K, V]) Set(key K, value V) (grew bool) {
shard := m.shard(key)
shard.mu.Lock()
defer shard.mu.Unlock()
s0 := len(shard.m)
shard.m[key] = value
return len(shard.m) > s0
}
// Delete removes key from m.
//
// It reports whether the map size shrunk (that is, whether key was present in
// the map).
func (m *ShardedMap[K, V]) Delete(key K) (shrunk bool) {
shard := m.shard(key)
shard.mu.Lock()
defer shard.mu.Unlock()
s0 := len(shard.m)
delete(shard.m, key)
return len(shard.m) < s0
}
// Contains reports whether m contains key.
func (m *ShardedMap[K, V]) Contains(key K) bool {
shard := m.shard(key)
shard.mu.Lock()
defer shard.mu.Unlock()
_, ok := shard.m[key]
return ok
}
// Len returns the number of elements in m.
//
// It does so by locking shards one at a time, so it's not particularly cheap,
// nor does it give a consistent snapshot of the map. It's mostly intended for
// metrics or testing.
func (m *ShardedMap[K, V]) Len() int {
n := 0
for i := range m.shards {
shard := &m.shards[i]
shard.mu.Lock()
n += len(shard.m)
shard.mu.Unlock()
}
return n
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package syncs
// TODO(raggi): this implementation is still imperfect as it will still result
// in cross CPU sharing periodically, we instead really want a per-CPU shard
// key, but the limitations of calling platform code make reaching for even the
// getcpu vdso very painful. See https://github.com/golang/go/issues/18802, and
// hopefully one day we can replace with a primitive that falls out of that
// work.
// ShardValue contains a value sharded over a set of shards.
// In order to be useful, T should be aligned to cache lines.
// Users must organize that usage in One and All is concurrency safe.
// The zero value is not safe for use; use [NewShardValue].
type ShardValue[T any] struct {
shards []T
//lint:ignore U1000 unused under tailscale_go builds.
pool shardValuePool
}
// Len returns the number of shards.
func (sp *ShardValue[T]) Len() int {
return len(sp.shards)
}
// All yields a pointer to the value in each shard.
func (sp *ShardValue[T]) All(yield func(*T) bool) {
for i := range sp.shards {
if !yield(&sp.shards[i]) {
return
}
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !tailscale_go
package syncs
import (
"runtime"
"sync"
"sync/atomic"
)
type shardValuePool struct {
atomic.Int64
sync.Pool
}
// NewShardValue constructs a new ShardValue[T] with a shard per CPU.
func NewShardValue[T any]() *ShardValue[T] {
sp := &ShardValue[T]{
shards: make([]T, runtime.NumCPU()),
}
sp.pool.New = func() any {
i := sp.pool.Add(1) - 1
return &sp.shards[i%int64(len(sp.shards))]
}
return sp
}
// One yields a pointer to a single shard value with best-effort P-locality.
func (sp *ShardValue[T]) One(yield func(*T)) {
v := sp.pool.Get().(*T)
yield(v)
sp.pool.Put(v)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package syncs contains additional sync types and functionality.
package syncs
import (
"context"
"iter"
"sync"
"sync/atomic"
"tailscale.com/util/mak"
)
// ClosedChan returns a channel that's already closed.
func ClosedChan() <-chan struct{} { return closedChan }
var closedChan = initClosedChan()
func initClosedChan() <-chan struct{} {
ch := make(chan struct{})
close(ch)
return ch
}
// AtomicValue is the generic version of [atomic.Value].
// See [MutexValue] for guidance on whether to use this type.
type AtomicValue[T any] struct {
v atomic.Value
}
// wrappedValue is used to wrap a value T in a concrete type,
// otherwise atomic.Value.Store may panic due to mismatching types in interfaces.
// This wrapping is not necessary for non-interface kinds of T,
// but there is no harm in wrapping anyways.
// See https://cs.opensource.google/go/go/+/refs/tags/go1.22.2:src/sync/atomic/value.go;l=78
type wrappedValue[T any] struct{ v T }
// Load returns the value set by the most recent Store.
// It returns the zero value for T if the value is empty.
func (v *AtomicValue[T]) Load() T {
x, _ := v.LoadOk()
return x
}
// LoadOk is like Load but returns a boolean indicating whether the value was
// loaded.
func (v *AtomicValue[T]) LoadOk() (_ T, ok bool) {
x := v.v.Load()
if x != nil {
return x.(wrappedValue[T]).v, true
}
var zero T
return zero, false
}
// Store sets the value of the Value to x.
func (v *AtomicValue[T]) Store(x T) {
v.v.Store(wrappedValue[T]{x})
}
// Swap stores new into Value and returns the previous value.
// It returns the zero value for T if the value is empty.
func (v *AtomicValue[T]) Swap(x T) (old T) {
oldV := v.v.Swap(wrappedValue[T]{x})
if oldV != nil {
return oldV.(wrappedValue[T]).v
}
return old // zero value of T
}
// CompareAndSwap executes the compare-and-swap operation for the Value.
// It panics if T is not comparable.
func (v *AtomicValue[T]) CompareAndSwap(oldV, newV T) (swapped bool) {
var zero T
return v.v.CompareAndSwap(wrappedValue[T]{oldV}, wrappedValue[T]{newV}) ||
// In the edge-case where [atomic.Value.Store] is uninitialized
// and trying to compare with the zero value of T,
// then compare-and-swap with the nil any value.
(any(oldV) == any(zero) && v.v.CompareAndSwap(any(nil), wrappedValue[T]{newV}))
}
// MutexValue is a value protected by a mutex.
//
// AtomicValue, [MutexValue], [atomic.Pointer] are similar and
// overlap in their use cases.
//
// - Use [atomic.Pointer] if the value being stored is a pointer and
// you only ever need load and store operations.
// An atomic pointer only occupies 1 word of memory.
//
// - Use [MutexValue] if the value being stored is not a pointer or
// you need the ability for a mutex to protect a set of operations
// performed on the value.
// A mutex-guarded value occupies 1 word of memory plus
// the memory representation of T.
//
// - AtomicValue is useful for non-pointer types that happen to
// have the memory layout of a single pointer.
// Examples include a map, channel, func, or a single field struct
// that contains any prior types.
// An atomic value occupies 2 words of memory.
// Consequently, Storing of non-pointer types always allocates.
//
// Note that [AtomicValue] has the ability to report whether it was set
// while [MutexValue] lacks the ability to detect if the value was set
// and it happens to be the zero value of T. If such a use case is
// necessary, then you could consider wrapping T in [opt.Value].
type MutexValue[T any] struct {
mu sync.Mutex
v T
}
// WithLock calls f with a pointer to the value while holding the lock.
// The provided pointer must not leak beyond the scope of the call.
func (m *MutexValue[T]) WithLock(f func(p *T)) {
m.mu.Lock()
defer m.mu.Unlock()
f(&m.v)
}
// Load returns a shallow copy of the underlying value.
func (m *MutexValue[T]) Load() T {
m.mu.Lock()
defer m.mu.Unlock()
return m.v
}
// Store stores a shallow copy of the provided value.
func (m *MutexValue[T]) Store(v T) {
m.mu.Lock()
defer m.mu.Unlock()
m.v = v
}
// Swap stores new into m and returns the previous value.
func (m *MutexValue[T]) Swap(new T) (old T) {
m.mu.Lock()
defer m.mu.Unlock()
old, m.v = m.v, new
return old
}
// WaitGroupChan is like a sync.WaitGroup, but has a chan that closes
// on completion that you can wait on. (This, you can only use the
// value once)
// Also, its zero value is not usable. Use the constructor.
type WaitGroupChan struct {
n int64 // atomic
done chan struct{} // closed on transition to zero
}
// NewWaitGroupChan returns a new single-use WaitGroupChan.
func NewWaitGroupChan() *WaitGroupChan {
return &WaitGroupChan{done: make(chan struct{})}
}
// DoneChan returns a channel that's closed on completion.
func (wg *WaitGroupChan) DoneChan() <-chan struct{} { return wg.done }
// Add adds delta, which may be negative, to the WaitGroupChan
// counter. If the counter becomes zero, all goroutines blocked on
// Wait or the Done chan are released. If the counter goes negative,
// Add panics.
//
// Note that calls with a positive delta that occur when the counter
// is zero must happen before a Wait. Calls with a negative delta, or
// calls with a positive delta that start when the counter is greater
// than zero, may happen at any time. Typically this means the calls
// to Add should execute before the statement creating the goroutine
// or other event to be waited for.
func (wg *WaitGroupChan) Add(delta int) {
n := atomic.AddInt64(&wg.n, int64(delta))
if n == 0 {
close(wg.done)
}
}
// Decr decrements the WaitGroup counter by one.
//
// (It is like sync.WaitGroup's Done method, but we don't use Done in
// this type, because it's ambiguous between Context.Done and
// WaitGroup.Done. So we use DoneChan and Decr instead.)
func (wg *WaitGroupChan) Decr() {
wg.Add(-1)
}
// Wait blocks until the WaitGroupChan counter is zero.
func (wg *WaitGroupChan) Wait() { <-wg.done }
// Semaphore is a counting semaphore.
//
// Use NewSemaphore to create one.
type Semaphore struct {
c chan struct{}
}
// NewSemaphore returns a semaphore with resource count n.
func NewSemaphore(n int) Semaphore {
return Semaphore{c: make(chan struct{}, n)}
}
// Len reports the number of in-flight acquisitions.
// It is incremented whenever the semaphore is acquired.
// It is decremented whenever the semaphore is released.
func (s Semaphore) Len() int {
return len(s.c)
}
// Acquire blocks until a resource is acquired.
func (s Semaphore) Acquire() {
s.c <- struct{}{}
}
// AcquireContext reports whether the resource was acquired before the ctx was done.
func (s Semaphore) AcquireContext(ctx context.Context) bool {
select {
case s.c <- struct{}{}:
return true
case <-ctx.Done():
return false
}
}
// TryAcquire reports, without blocking, whether the resource was acquired.
func (s Semaphore) TryAcquire() bool {
select {
case s.c <- struct{}{}:
return true
default:
return false
}
}
// Release releases a resource.
func (s Semaphore) Release() {
<-s.c
}
// Map is a Go map protected by a [sync.RWMutex].
// It is preferred over [sync.Map] for maps with entries that change
// at a relatively high frequency.
// This must not be shallow copied.
type Map[K comparable, V any] struct {
mu sync.RWMutex
m map[K]V
}
// Load loads the value for the provided key and whether it was found.
func (m *Map[K, V]) Load(key K) (value V, loaded bool) {
m.mu.RLock()
defer m.mu.RUnlock()
value, loaded = m.m[key]
return value, loaded
}
// LoadFunc calls f with the value for the provided key
// regardless of whether the entry exists or not.
// The lock is held for the duration of the call to f.
func (m *Map[K, V]) LoadFunc(key K, f func(value V, loaded bool)) {
m.mu.RLock()
defer m.mu.RUnlock()
value, loaded := m.m[key]
f(value, loaded)
}
// Store stores the value for the provided key.
func (m *Map[K, V]) Store(key K, value V) {
m.mu.Lock()
defer m.mu.Unlock()
mak.Set(&m.m, key, value)
}
// LoadOrStore returns the value for the given key if it exists
// otherwise it stores value.
func (m *Map[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {
if actual, loaded = m.Load(key); loaded {
return actual, loaded
}
m.mu.Lock()
defer m.mu.Unlock()
actual, loaded = m.m[key]
if !loaded {
actual = value
mak.Set(&m.m, key, value)
}
return actual, loaded
}
// LoadOrInit returns the value for the given key if it exists
// otherwise f is called to construct the value to be set.
// The lock is held for the duration to prevent duplicate initialization.
func (m *Map[K, V]) LoadOrInit(key K, f func() V) (actual V, loaded bool) {
if actual, loaded := m.Load(key); loaded {
return actual, loaded
}
m.mu.Lock()
defer m.mu.Unlock()
if actual, loaded = m.m[key]; loaded {
return actual, loaded
}
loaded = false
actual = f()
mak.Set(&m.m, key, actual)
return actual, loaded
}
// LoadAndDelete returns the value for the given key if it exists.
// It ensures that the map is cleared of any entry for the key.
func (m *Map[K, V]) LoadAndDelete(key K) (value V, loaded bool) {
m.mu.Lock()
defer m.mu.Unlock()
value, loaded = m.m[key]
if loaded {
delete(m.m, key)
}
return value, loaded
}
// Delete deletes the entry identified by key.
func (m *Map[K, V]) Delete(key K) {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.m, key)
}
// Keys iterates over all keys in the map in an undefined order.
// A read lock is held for the entire duration of the iteration.
// Use the [WithLock] method instead to mutate the map during iteration.
func (m *Map[K, V]) Keys() iter.Seq[K] {
return func(yield func(K) bool) {
m.mu.RLock()
defer m.mu.RUnlock()
for k := range m.m {
if !yield(k) {
return
}
}
}
}
// Values iterates over all values in the map in an undefined order.
// A read lock is held for the entire duration of the iteration.
// Use the [WithLock] method instead to mutate the map during iteration.
func (m *Map[K, V]) Values() iter.Seq[V] {
return func(yield func(V) bool) {
m.mu.RLock()
defer m.mu.RUnlock()
for _, v := range m.m {
if !yield(v) {
return
}
}
}
}
// All iterates over all entries in the map in an undefined order.
// A read lock is held for the entire duration of the iteration.
// Use the [WithLock] method instead to mutate the map during iteration.
func (m *Map[K, V]) All() iter.Seq2[K, V] {
return func(yield func(K, V) bool) {
m.mu.RLock()
defer m.mu.RUnlock()
for k, v := range m.m {
if !yield(k, v) {
return
}
}
}
}
// WithLock calls f with the underlying map.
// Use of m2 must not escape the duration of this call.
// The write-lock is held for the entire duration of this call.
func (m *Map[K, V]) WithLock(f func(m2 map[K]V)) {
m.mu.Lock()
defer m.mu.Unlock()
if m.m == nil {
m.m = make(map[K]V)
}
f(m.m)
}
// Len returns the length of the map.
func (m *Map[K, V]) Len() int {
m.mu.RLock()
defer m.mu.RUnlock()
return len(m.m)
}
// Clear removes all entries from the map.
func (m *Map[K, V]) Clear() {
m.mu.Lock()
defer m.mu.Unlock()
clear(m.m)
}
// Swap stores the value for the provided key, and returns the previous value
// (if any). If there was no previous value set, a zero value will be returned.
func (m *Map[K, V]) Swap(key K, value V) (oldValue V) {
m.mu.Lock()
defer m.mu.Unlock()
oldValue = m.m[key]
mak.Set(&m.m, key, value)
return oldValue
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package tailcfg
import (
"fmt"
"net/netip"
"slices"
"strconv"
"tailscale.com/types/key"
)
// DERPMap describes the set of DERP packet relay servers that are available.
//
// API maturity: this type is subject to minor changes over time, though
// its general shape is stable.
type DERPMap struct {
// HomeParams, if non-nil, is a change in home parameters.
//
// The rest of the DEPRMap fields, if zero, means unchanged.
HomeParams *DERPHomeParams `json:",omitempty"`
// Regions is the set of geographic regions running DERP node(s).
//
// It's keyed by the DERPRegion.RegionID.
//
// The numbers are not necessarily contiguous.
Regions map[DERPRegionID]*DERPRegion
// OmitDefaultRegions specifies to not use Tailscale's DERP servers, and only use those
// specified in this DERPMap. If there are none set outside of the defaults, this is a noop.
//
// This field is only meaningful if the Regions map is non-nil (indicating a change).
OmitDefaultRegions bool `json:"omitDefaultRegions,omitempty"`
}
// RegionIDs returns the sorted region IDs.
func (m *DERPMap) RegionIDs() []DERPRegionID {
ret := make([]DERPRegionID, 0, len(m.Regions))
for rid := range m.Regions {
ret = append(ret, rid)
}
slices.Sort(ret)
return ret
}
// DERPHomeParams contains parameters from the server related to selecting a
// DERP home region (sometimes referred to as the "preferred DERP").
type DERPHomeParams struct {
// RegionScore scales latencies of DERP regions by a given scaling
// factor when determining which region to use as the home
// ("preferred") DERP. Scores in the range (0, 1) will cause this
// region to be proportionally more preferred, and scores in the range
// (1, ∞) will penalize a region.
//
// If a region is not present in this map, it is treated as having a
// score of 1.0.
//
// Scores should not be 0 or negative; such scores will be ignored.
//
// A nil map means no change from the previous value (if any); an empty
// non-nil map can be sent to reset all scores back to 1.0.
RegionScore map[DERPRegionID]float64 `json:",omitempty"`
}
// DERPRegionID is a unique integer for a geographic region.
//
// It corresponds to the legacy derpN.tailscale.com hostnames used by older clients.
// (Older clients will continue to resolve derpN.tailscale.com when contacting peers,
// rather than use the server-provided DERPMap)
//
// It must be non-zero, positive, and guaranteed to fit in a JavaScript number.
//
// IDs in range 900-999 are reserved for end users to run their own DERP nodes.
type DERPRegionID int64
// ParseDERPRegionID parses s and returns a [DERPRegionID].
// It returns an error if s isn’t a valid region ID.
func ParseDERPRegionID(s string) (DERPRegionID, error) {
id, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return 0, err
}
rid := DERPRegionID(id)
if !rid.IsValid() {
return 0, fmt.Errorf("invalid region ID: %s", s)
}
return rid, nil
}
// IsValid reports whether id is non-zero, positive, and fits in a JavaScript number.
func (id DERPRegionID) IsValid() bool {
const maxSafeJavaScriptInteger DERPRegionID = 1<<53 - 1 // Number.MAX_SAFE_INTEGER
return id > 0 && id <= maxSafeJavaScriptInteger
}
// Int64 returns the int64 representation of id.
func (id DERPRegionID) Int64() int64 {
return int64(id)
}
// String implements the [fmt.Stringer] interface.
func (id DERPRegionID) String() string {
return strconv.FormatInt(int64(id), 10)
}
// DERPRegion is a geographic region running DERP relay node(s).
//
// Client nodes discover which region they're closest to, advertise
// that "home" DERP region (previously called "home node", when there
// was only 1 node per region) and maintain a persistent connection
// that region as long as it's the closest. Client nodes will further
// connect to other regions as necessary to communicate with peers
// advertising other regions as their homes.
type DERPRegion struct {
// RegionID is a unique integer for a geographic region.
//
// It corresponds to the legacy derpN.tailscale.com hostnames
// used by older clients. (Older clients will continue to resolve
// derpN.tailscale.com when contacting peers, rather than use
// the server-provided DERPMap)
//
// RegionIDs must be non-zero, positive, and guaranteed to fit
// in a JavaScript number.
//
// RegionIDs in range 900-999 are reserved for end users to run their
// own DERP nodes.
RegionID DERPRegionID
// RegionCode is a short name for the region. It's usually a popular
// city or airport code in the region: "nyc", "sf", "sin",
// "fra", etc.
RegionCode string
// RegionName is a long English name for the region: "New York City",
// "San Francisco", "Singapore", "Frankfurt", etc.
RegionName string
// Latitude, Longitude are optional geographical coordinates of the DERP region's city, in degrees.
Latitude float64 `json:",omitempty"`
Longitude float64 `json:",omitempty"`
// Avoid is whether the client should avoid picking this as its home region.
// The region should only be used if a peer is there. Clients already using
// this region as their home should migrate away to a new region without
// Avoid set.
//
// Deprecated: because of bugs in past implementations combined with unclear
// docs that caused people to think the bugs were intentional, this field is
// deprecated. It was never supposed to cause STUN/DERP measurement probes,
// but due to bugs, it sometimes did. And then some parts of the code began
// to rely on that property. But then we were unable to use this field for
// its original purpose, nor its later imagined purpose, because various
// parts of the codebase thought it meant one thing and others thought it
// meant another. But it did something in the middle instead. So we're retiring
// it. Use NoMeasureNoHome instead.
Avoid bool `json:",omitempty"`
// NoMeasureNoHome says that this regions should not be measured for its
// latency distance (STUN, HTTPS, etc) or availability (e.g. captive portal
// checks) and should never be selected as the node's home region. However,
// if a peer declares this region as its home, then this client is allowed
// to connect to it for the purpose of communicating with that peer.
//
// This is what the now deprecated Avoid bool was supposed to mean
// originally but had implementation bugs and documentation omissions.
NoMeasureNoHome bool `json:",omitempty"`
// Nodes are the DERP nodes running in this region, in
// priority order for the current client. Client TLS
// connections should ideally only go to the first entry
// (falling back to the second if necessary). STUN packets
// should go to the first 1 or 2.
//
// If nodes within a region route packets amongst themselves,
// but not to other regions. That said, each user/domain
// should get a the same preferred node order, so if all nodes
// for a user/network pick the first one (as they should, when
// things are healthy), the inter-cluster routing is minimal
// to zero.
Nodes []*DERPNode
}
// DERPNode describes a DERP packet relay node running within a DERPRegion.
type DERPNode struct {
// Name is a unique node name (across all regions).
// It is not a host name.
// It's typically of the form "1b", "2a", "3b", etc. (region
// ID + suffix within that region)
Name string
// RegionID is the RegionID of the DERPRegion that this node
// is running in.
RegionID DERPRegionID
// HostName is the DERP node's hostname.
//
// It is required but need not be unique; multiple nodes may
// have the same HostName but vary in configuration otherwise.
HostName string
// CertName optionally specifies the expected TLS cert common
// name. If empty, HostName is used. If CertName is non-empty,
// HostName is only used for the TCP dial (if IPv4/IPv6 are
// not present) + TLS ClientHello.
//
// As a special case, if CertName starts with "sha256-raw:",
// then the rest of the string is a hex-encoded SHA256 of the
// cert to expect. This is used for self-signed certs.
// In this case, the HostName field will typically be an IP
// address literal.
CertName string `json:",omitempty"`
// IPv4 optionally forces an IPv4 address to use, instead of using DNS.
// If empty, A record(s) from DNS lookups of HostName are used.
// If the string is not an IPv4 address, IPv4 is not used; the
// conventional string to disable IPv4 (and not use DNS) is
// "none".
IPv4 string `json:",omitempty"`
// IPv6 optionally forces an IPv6 address to use, instead of using DNS.
// If empty, AAAA record(s) from DNS lookups of HostName are used.
// If the string is not an IPv6 address, IPv6 is not used; the
// conventional string to disable IPv6 (and not use DNS) is
// "none".
IPv6 string `json:",omitempty"`
// Port optionally specifies a STUN port to use.
// Zero means 3478.
// To disable STUN on this node, use -1.
STUNPort int `json:",omitempty"`
// STUNOnly marks a node as only a STUN server and not a DERP
// server.
STUNOnly bool `json:",omitempty"`
// DERPPort optionally provides an alternate TLS port number
// for the DERP HTTPS server.
//
// If zero, 443 is used.
DERPPort int `json:",omitempty"`
// InsecureForTests is used by unit tests to disable TLS verification.
// It should not be set by users.
InsecureForTests bool `json:",omitempty"`
// STUNTestIP is used in tests to override the STUN server's IP.
// If empty, it's assumed to be the same as the DERP server.
STUNTestIP string `json:",omitempty"`
// CanPort80 specifies whether this DERP node is accessible over HTTP
// on port 80 specifically. This is used for captive portal checks.
CanPort80 bool `json:",omitempty"`
}
func (n *DERPNode) IsTestNode() bool {
return n.STUNTestIP != "" || n.IPv4 == "127.0.0.1"
}
// DotInvalid is a fake DNS TLD used in tests for an invalid hostname.
const DotInvalid = ".invalid"
// DERPAdmitClientRequest is the JSON request body of a POST to derper's
// --verify-client-url admission controller URL.
type DERPAdmitClientRequest struct {
NodePublic key.NodePublic // key to query for admission
Source netip.Addr // derp client's IP address
}
// DERPAdmitClientResponse is the response to a DERPAdmitClientRequest.
type DERPAdmitClientResponse struct {
Allow bool // whether to permit client
// TODO(bradfitz,maisem): bandwidth limits, etc?
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package nodecap defines the types of capabilities granted to tailnet nodes.
package nodecap
// Cap represents a capability granted to the self node
// as listed in [tailscale.com/tailcfg/MapResponse.Node.Capabilities].
//
// It must be a URL like "https://tailscale.com/cap/file-sharing" or "tailscale.com/cap/webui",
// or a well-known capability name like "funnel".
// The latter is only allowed for Tailscale-defined capabilities.
//
// Unlike [tailscale.com/tailcfg/peercap.Cap],
// Cap is not in context of a peer and is granted to the node itself.
//
// These are also referred to as "Node Attributes" in the ACL policy file.
type Cap string
// Prefix is a prefix for [tailscale.com/tailcfg.NodeCapMap] keys that share a common namespace,
// where each entry represents a distinct named instance (e.g. one per service).
// The full key is formed by concatenating the prefix with the instance name.
type Prefix string
// ToAttribute returns the full [Cap] key for the given value under this prefix,
// of the form prefix+value.
func (p Prefix) ToAttribute(value string) Cap {
return Cap(string(p) + value)
}
const (
FileSharing Cap = "https://tailscale.com/cap/file-sharing"
Admin Cap = "https://tailscale.com/cap/is-admin"
Owner Cap = "https://tailscale.com/cap/is-owner"
SSH Cap = "https://tailscale.com/cap/ssh" // feature enabled/available
SSHRuleIn Cap = "https://tailscale.com/cap/ssh-rule-in" // some SSH rule reach this node
DataPlaneAuditLogs Cap = "https://tailscale.com/cap/data-plane-audit-logs" // feature enabled
Debug Cap = "https://tailscale.com/cap/debug" // exposes debug endpoints over the PeerAPI
HTTPS Cap = "https"
// MacUIV2 makes the macOS GUI enable its v2 mode.
MacUIV2 Cap = "https://tailscale.com/cap/mac-ui-v2"
// ServicesInDesktopClients enables services list/menu/section in desktop clients.
// If this capability is not present, desktop clients should not show services.
ServicesInDesktopClients Cap = "https://tailscale.com/cap/services-in-desktop-clients"
// BindToInterfaceByRoute changes how Darwin nodes create
// sockets (in the net/netns package). See that package for more
// details on the behaviour of this capability.
BindToInterfaceByRoute Cap = "https://tailscale.com/cap/bind-to-interface-by-route"
// DisableAndroidBindToActiveNetwork disables binding sockets to the
// currently active network on Android, which is enabled by default.
// This allows the control plane to turn off the behavior if it causes
// problems.
DisableAndroidBindToActiveNetwork Cap = "disable-android-bind-to-active-network"
// DebugDisableAlternateDefaultRouteInterface changes how Darwin
// nodes get the default interface. There is an optional hook (used by the
// macOS and iOS clients) to override the default interface, this capability
// disables that and uses the default behavior (of parsing the routing
// table).
DebugDisableAlternateDefaultRouteInterface Cap = "https://tailscale.com/cap/debug-disable-alternate-default-route-interface"
// DebugDisableBindConnToInterface disables the automatic binding
// of connections to the default network interface on Darwin nodes.
DebugDisableBindConnToInterface Cap = "https://tailscale.com/cap/debug-disable-bind-conn-to-interface"
// DebugDisableBindConnToInterface disables the automatic binding
// of connections to the default network interface on Darwin nodes using network extensions
DebugDisableBindConnToInterfaceAppleExt Cap = "https://tailscale.com/cap/debug-disable-bind-conn-to-interface-apple-ext"
// TailnetLock indicates the node may initialize tailnet lock.
TailnetLock Cap = "https://tailscale.com/cap/tailnet-lock"
//// Funnel warning capabilities used for reporting errors to the user.
// WarnFunnelNoInvite indicates whether Funnel is enabled for the tailnet.
// This cap is no longer used 2023-08-09 onwards.
WarnFunnelNoInvite Cap = "https://tailscale.com/cap/warn-funnel-no-invite"
// WarnFunnelNoHTTPS indicates HTTPS has not been enabled for the tailnet.
// This cap is no longer used 2023-08-09 onwards.
WarnFunnelNoHTTPS Cap = "https://tailscale.com/cap/warn-funnel-no-https"
//// Debug logging capabilities
// DebugTSDNSResolution enables verbose debug logging for DNS
// resolution for Tailscale-controlled domains (the control server, log
// server, DERP servers, etc.)
DebugTSDNSResolution Cap = "https://tailscale.com/cap/debug-ts-dns-resolution"
// FunnelPorts specifies the ports that the Funnel is available on.
// The ports are specified as a comma-separated list of port numbers or port
// ranges (e.g. "80,443,8080-8090") in the ports query parameter.
// e.g. https://tailscale.com/cap/funnel-ports?ports=80,443,8080-8090
FunnelPorts Cap = "https://tailscale.com/cap/funnel-ports"
// OnlyTCP443 specifies that the client should not attempt to generate
// any outbound traffic that isn't TCP on port 443 (HTTPS). This is used for
// clients in restricted environments where only HTTPS traffic is allowed
// other types of traffic trips outbound firewall alarms. This thus implies
// all traffic is over DERP.
OnlyTCP443 Cap = "only-tcp-443"
// Funnel grants the ability for a node to host ingress traffic.
Funnel Cap = "funnel"
// SSHAggregator grants the ability for a node to collect SSH sessions.
SSHAggregator Cap = "ssh-aggregator"
// DebugForceBackgroundSTUN forces a node to always do background
// STUN queries regardless of inactivity.
DebugForceBackgroundSTUN Cap = "debug-always-stun"
// DebugDisableWGTrim disables the lazy WireGuard configuration,
// always giving WireGuard the full netmap, even for idle peers.
DebugDisableWGTrim Cap = "debug-no-wg-trim"
// DisableSubnetsIfPAC controls whether subnet routers should be
// disabled if WPAD is present on the network.
DisableSubnetsIfPAC Cap = "debug-disable-subnets-if-pac"
// DisableUPnP makes the client not perform a UPnP portmapping.
// By default, we want to enable it to see if it works on more clients.
//
// If UPnP catastrophically fails for people, this should be set kill
// new attempts at UPnP connections.
DisableUPnP Cap = "debug-disable-upnp"
// DisableDeltaUpdates makes the client not process updates via the
// delta update mechanism and should instead treat all netmap changes as
// "full" ones as tailscaled did in 1.48.x and earlier.
DisableDeltaUpdates Cap = "disable-delta-updates"
// RandomizeClientPort makes magicsock UDP bind to
// :0 to get a random local port, ignoring any configured
// fixed port.
RandomizeClientPort Cap = "randomize-client-port"
// SilentDisco makes the client suppress disco heartbeats to its
// peers.
SilentDisco Cap = "silent-disco"
// OneCGNATEnable makes the client prefer one big CGNAT /10 route
// rather than a /32 per peer. At most one of this or
// [OneCGNATDisable] may be set; if neither are, it's automatic.
OneCGNATEnable Cap = "one-cgnat?v=true"
// OneCGNATDisable makes the client prefer a /32 route per peer
// rather than one big /10 CGNAT route. At most one of this or
// [OneCGNATEnable] may be set; if neither are, it's automatic.
OneCGNATDisable Cap = "one-cgnat?v=false"
// PeerMTUEnable makes the client do path MTU discovery to its
// peers. If it isn't set, it defaults to the client default.
PeerMTUEnable Cap = "peer-mtu-enable"
// DNSForwarderDisableTCPRetries disables retrying truncated
// DNS queries over TCP if the response is truncated.
DNSForwarderDisableTCPRetries Cap = "dns-forwarder-disable-tcp-retries"
// LinuxMustUseIPTables forces Linux clients to use iptables for
// netfilter management.
// This cannot be set simultaneously with [LinuxMustUseNfTables].
LinuxMustUseIPTables Cap = "linux-netfilter?v=iptables"
// LinuxMustUseNfTables forces Linux clients to use nftables for
// netfilter management.
// This cannot be set simultaneously with [LinuxMustUseIPTables].
LinuxMustUseNfTables Cap = "linux-netfilter?v=nftables"
// ProbeUDPLifetime makes the client probe UDP path lifetime at the
// tail end of an active direct connection in magicsock.
ProbeUDPLifetime Cap = "probe-udp-lifetime"
// TaildriveShare enables sharing via Taildrive.
TaildriveShare Cap = "drive:share"
// TaildriveAccess enables accessing shares via Taildrive.
TaildriveAccess Cap = "drive:access"
// SuggestExitNode is applied to each exit node which the control plane has determined
// is a recommended exit node.
SuggestExitNode Cap = "suggest-exit-node"
// DisableWebClient disables using the web client.
DisableWebClient Cap = "disable-web-client"
// LogExitFlows enables exit node destinations in network flow logs.
LogExitFlows Cap = "log-exit-flows"
// AutoExitNode permits the automatic exit nodes feature.
AutoExitNode Cap = "auto-exit-node"
// StoreAppCRoutes configures the node to store app connector routes persistently.
StoreAppCRoutes Cap = "store-appc-routes"
// SuggestExitNodeUI allows the currently suggested exit node to appear in the client GUI.
SuggestExitNodeUI Cap = "suggest-exit-node-ui"
// UserDialUseRoutes makes UserDial use either the peer dialer or the system dialer,
// depending on the destination address and the configured routes. When present, it also makes
// the DNS forwarder use UserDial instead of SystemDial when dialing resolvers.
UserDialUseRoutes Cap = "user-dial-routes"
// SSHBehaviorV1 forces SSH to use the V1 behavior (no su, run SFTP in-process)
// Added 2024-05-29 in Tailscale version 1.68.
SSHBehaviorV1 Cap = "ssh-behavior-v1"
// SSHBehaviorV2 forces SSH to use the V2 behavior (use su, run SFTP in child process).
// This overrides [SSHBehaviorV1] if set.
// See forceV1Behavior in ssh/tailssh/incubator.go for distinction between
// V1 and V2 behavior.
// Added 2024-08-06 in Tailscale version 1.72.
SSHBehaviorV2 Cap = "ssh-behavior-v2"
// DisableSplitDNSWhenNoCustomResolvers indicates that the node's
// DNS manager should not adopt a split DNS configuration even though the
// Config of the resolver only contains routes that do not specify custom
// resolver(s), hence all DNS queries can be safely sent to the upstream
// DNS resolver and the node's DNS forwarder doesn't need to handle all
// DNS traffic.
// This is for now (2024-06-06) an iOS-specific battery life optimization,
// and this node attribute allows us to disable the optimization remotely
// if needed.
DisableSplitDNSWhenNoCustomResolvers Cap = "disable-split-dns-when-no-custom-resolvers"
// ScopeQuad100OnMacOS makes sandboxed macOS clients scope quad-100
// to its match domains instead of installing it as the OS's primary
// (catch-all) resolver, so that public names fall through to the OS
// resolver -- e.g. a user's DoH system profile -- rather than being
// shadowed. It has no effect on any other platform. Without this attribute,
// sandboxed macOS keeps the older behavior of making quad-100 the default
// resolver, as iOS still does. See tailscale/corp#45534.
ScopeQuad100OnMacOS Cap = "scope-quad100-macos"
// DisableLocalDNSOverrideViaNRPT indicates that the node's DNS manager should not
// create a default (catch-all) Windows NRPT rule when "Override local DNS" is enabled.
// Without this rule, Windows 8.1 and newer devices issue parallel DNS requests to DNS servers
// associated with all network adapters, even when "Override local DNS" is enabled and/or
// a Mullvad exit node is being used, resulting in DNS leaks.
// We began creating this rule on 2024-06-14, and this node attribute
// allows us to disable the new behavior remotely if needed.
DisableLocalDNSOverrideViaNRPT Cap = "disable-local-dns-override-via-nrpt"
// DisableMagicSockCryptoRouting disables the use of the
// magicsock cryptorouting hook. See tailscale/corp#20732.
//
// Deprecated: [DisableMagicSockCryptoRouting] is deprecated as of
// [tailscale.com/tailcfg.CapabilityVersion] 124,
// CryptoRouting is now mandatory. See tailscale/corp#31083.
DisableMagicSockCryptoRouting Cap = "disable-magicsock-crypto-routing"
// DisableCaptivePortalDetection instructs the client to not perform captive portal detection
// automatically when the network state changes.
DisableCaptivePortalDetection Cap = "disable-captive-portal-detection"
// DisableSkipStatusQueue is set when the node should disable skipping
// of queued netmap.NetworkMap between the controlclient and LocalBackend.
// See tailscale/tailscale#14768.
DisableSkipStatusQueue Cap = "disable-skip-status-queue"
// SSHEnvironmentVariables enables logic for handling environment variables sent
// via SendEnv in the SSH server and applying them to the SSH session.
SSHEnvironmentVariables Cap = "ssh-env-vars"
// ServiceHost indicates the VIP Services for which the client is
// approved to act as a service host, and which IP addresses are assigned
// to those VIP Services. Any VIP Services that the client is not
// advertising can be ignored.
// Each value of this key in [NodeCapMap] is of type [ServiceIPMappings].
// If multiple values of this key exist, they should be merged in sequence
// (replace conflicting keys).
ServiceHost Cap = "service-host"
// MaxKeyDuration represents the MaxKeyDuration setting on the
// tailnet. The value of this key in [NodeCapMap] will be only one entry of
// type float64 representing the duration in seconds. This cap will be
// omitted if the tailnet's MaxKeyDuration is the default.
MaxKeyDuration Cap = "tailnet.maxKeyDuration"
// NativeIPV4 contains the IPV4 address of the node in its
// native tailnet. This is currently only sent to Hello, in its
// peer node list.
NativeIPV4 Cap = "native-ipv4"
// DisableRelayServer prevents the node from acting as an underlay
// UDP relay server. There are no expected values for this key; the key
// only needs to be present in [NodeCapMap] to take effect.
DisableRelayServer Cap = "disable-relay-server"
// DisableRelayClient prevents the node from both allocating UDP
// relay server endpoints itself, and from using endpoints allocated by
// its peers. This attribute can be added to the node dynamically; if added
// while the node is already running, the node will be unable to allocate
// endpoints after it next updates its network map, and will be immediately
// unable to use new paths via a UDP relay server. Setting this attribute
// dynamically does not remove any existing paths, including paths that
// traverse a UDP relay server. There are no expected values for this key
// in [NodeCapMap]; the key only needs to be present in [NodeCapMap] to
// take effect.
DisableRelayClient Cap = "disable-relay-client"
// MagicDNSPeerAAAA is a capability that tells the node's MagicDNS
// server to answer AAAA queries about its peers. See tailscale/tailscale#1152.
MagicDNSPeerAAAA Cap = "magicdns-aaaa"
// DNSSubdomainResolve, when set on Self or a Peer node, indicates
// that the subdomains of that node's MagicDNS name should resolve to the
// same IP addresses as the node itself.
// For example, if node "myserver.tailnet.ts.net" has this capability,
// then "anything.myserver.tailnet.ts.net" will resolve to myserver's IPs.
DNSSubdomainResolve Cap = "dns-subdomain-resolve"
// TrafficSteering configures the node to use the traffic
// steering subsystem for via routes. See tailscale/corp#29966.
TrafficSteering Cap = "traffic-steering"
// TailnetDisplayName is an optional alternate name for the tailnet
// to be displayed to the user.
// If empty or absent, a default is used.
// If this value is present and set by a user this will only include letters,
// numbers, apostrophe, spaces, and hyphens. This may not be true for the default.
// Values can look like "foo.com" or "Foo's Test Tailnet - Staging".
TailnetDisplayName Cap = "tailnet-display-name"
// ClientSideReachability configures the node to determine
// reachability itself when choosing connectors. When absent, the
// default behavior is to trust the control plane when it claims that a
// node is no longer online, but that is not a reliable signal.
//
// It is temporary and will be ignored once its behaviour becomes the default.
ClientSideReachability Cap = "client-side-reachability"
// ClientSideReachabilityRouteCheck configures the node to use
// the routecheck subsystem to determine reachability when choosing
// connectors. This relies on [ClientSideReachability] being set.
// See tailscale/tailscale#17367.
//
// It is temporary and will be ignored once its behaviour becomes the default.
ClientSideReachabilityRouteCheck Cap = "client-side-reachability-routecheck"
// DefaultAutoUpdate advertises the default node auto-update setting
// for this tailnet. The node is free to opt-in or out locally regardless of
// this value. Once this has been set and stored in the client, future
// changes from the control plane are ignored.
//
// The value of the key in [NodeCapMap] is a JSON boolean.
DefaultAutoUpdate Cap = "default-auto-update"
// DisableHostsFileUpdates indicates that the node's DNS manager should
// not create hosts file entries when it normally would, such as when we're not
// the primary resolver on Windows or when the host is domain-joined and its
// primary domain takes precedence over MagicDNS. As of 2026-02-12, it is only
// used on Windows.
DisableHostsFileUpdates Cap = "disable-hosts-file-updates"
// ForceRegisterMagicDNSIPv4Only forces the client to only register
// its MagicDNS IPv4 address with systemd/etc, and not both its IPv4 and IPv6 addresses.
// See https://github.com/tailscale/tailscale/issues/15404.
// TODO(bradfitz): remove this a few releases after 2026-02-16.
ForceRegisterMagicDNSIPv4Only Cap = "force-register-magicdns-ipv4-only"
// CacheNetworkMaps instructs the node to persistently cache network
// maps and use them to establish peer connectivity on start, if doing so is
// supported by the client and storage is available. When this attribute is
// absent (or removed), a node that supports netmap caching will ignore and
// discard existing cached maps, and will not store any.
CacheNetworkMaps Cap = "cache-network-maps"
// DisableCacheNetworkMaps indicates that the node should not cache
// network maps (as per [CacheNetworkMaps]) when it normally would.
// This attribute exists to allow the policy document to override the default.
// When set, it takes precedence over [CacheNetworkMaps].
DisableCacheNetworkMaps Cap = "disable-cache-network-maps"
// DisableLinuxCGNATDropRule tells Linux clients to not insert a
// blanket firewall DROP rule for inbound traffic from the CGNAT IP range
// that does not originate from the Tailscale network interface.
// This enables access to off-tailnet endpoints within that IP range.
DisableLinuxCGNATDropRule Cap = "disable-linux-cgnat-drop-rule"
// EmitRuntimeMetrics enables emission of [runtime/metrics] as
// [tailscale.com/util/clientmetric]'s.
EmitRuntimeMetrics Cap = "emit-runtime-metrics"
// ConnReject enables the node's aggregated connection-rejection diagnostic
// buffers and the debug-rejects LocalAPI and GET /debug/rejects c2n endpoints.
ConnReject Cap = "debug-conn-reject"
// DisableUDPGRO disables UDP GRO (UDP_GRO socket option on Linux)
// on the magicsock UDP socket. It exists so control can mitigate kernel
// regressions that cause throughput or correctness issues with UDP GRO on
// specific OS/kernel versions, without requiring a client release. See
// https://github.com/tailscale/tailscale/issues/19777 for example.
// Currently only consulted on Linux; may apply to other platforms as they
// gain UDP GRO support.
DisableUDPGRO Cap = "disable-udp-gro"
// DisableUDPGSO disables UDP GSO (UDP_SEGMENT socket option on
// Linux) on the magicsock UDP socket. It exists so control can mitigate
// kernel regressions that cause throughput or correctness issues with UDP
// GSO on specific OS/kernel versions, without requiring a client release.
// See https://github.com/tailscale/tailscale/issues/19777 for example.
// Currently only consulted on Linux; may apply to other platforms as they
// gain UDP GSO support.
DisableUDPGSO Cap = "disable-udp-gso"
// DisableTUNUDPGRO disables UDP GRO on the Tailscale TUN device.
// It exists so control can mitigate kernel regressions that cause
// throughput or correctness issues with TUN UDP GRO on specific OS/kernel
// versions, without requiring a client release. See
// https://github.com/tailscale/tailscale/issues/13041 for example.
// Currently only consulted on Linux; may apply to other platforms as they
// gain TUN UDP GRO support.
DisableTUNUDPGRO Cap = "disable-tun-udp-gro"
// DisableTUNTCPGRO disables TCP GRO on the Tailscale TUN device.
// It exists so control can mitigate kernel regressions that cause
// throughput or correctness issues with TUN TCP GRO on specific OS/kernel
// versions, without requiring a client release. See
// https://github.com/tailscale/tailscale/issues/13041 for example.
// Currently only consulted on Linux; may apply to other platforms as they
// gain TUN TCP GRO support.
DisableTUNTCPGRO Cap = "disable-tun-tcp-gro"
// NeverGSOEqualTail enables a sentinel-tail workaround in the
// underlay UDP packet TX path on Linux. Applies to magicsock and peer relay
// UDP sockets. The workaround avoids emitting UDP GSO batches whose
// fragments are all equal in length, at a small payload and packet overhead
// cost. It exists so control can mitigate kernel regressions that mangle
// UDP headers or checksums for equal-length GSO batches, without requiring
// a client release. See https://github.com/tailscale/tailscale/issues/19777.
NeverGSOEqualTail Cap = "never-gso-equal-tail"
)
const (
// ServicesPrefix is the prefix for per-service [NodeCapMap]
// entries describing Services visible (accessible) to this node.
// Each value under such a key is of type [ServiceDetails].
// The suffix after the prefix is an opaque server-chosen identifier;
// consumers must use [ServiceDetails.Name] as the canonical service name
// rather than parsing it from the map key.
ServicesPrefix Prefix = "services/"
)
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package peercap defines the capabilities that can be granted to peer nodes.
package peercap
// Cap represents a capability granted to a peer by a [tailscale.com/tailcfg.FilterRule]
// when the peer communicates with the node that has this rule.
// Its meaning is application-defined.
//
// It must be a URL like "https://tailscale.com/cap/file-send"
// or "tailscale.com/cap/webui".
type Cap string
// Prefix is a prefix for [tailscale.com/tailcfg.PeerCapMap] keys that share a
// common namespace, where each entry represents a distinct named instance (e.g.
// one per App definition). The full key is formed by concatenating the prefix
// with the instance name.
type Prefix string
// ToAttribute returns the full [Cap] key for the given value under this prefix,
// of the form prefix+value.
func (p Prefix) ToAttribute(value string) Cap {
return Cap(string(p) + value)
}
const (
// FileSharingTarget grants the current node the ability to send
// files to the peer which has this capability.
FileSharingTarget Cap = "https://tailscale.com/cap/file-sharing-target"
// FileSharingSend grants the ability to receive files from a
// node that's owned by a different user.
FileSharingSend Cap = "https://tailscale.com/cap/file-send"
// DebugPeer grants the ability for a peer to read this node's
// goroutines, metrics, magicsock internal state, etc.
DebugPeer Cap = "https://tailscale.com/cap/debug-peer"
// WakeOnLAN grants the ability to send a Wake-On-LAN packet.
WakeOnLAN Cap = "https://tailscale.com/cap/wake-on-lan"
// Ingress grants the ability for a peer to send ingress traffic.
Ingress Cap = "https://tailscale.com/cap/ingress"
// WebUI grants the ability for a peer to edit features from the
// device Web UI.
WebUI Cap = "tailscale.com/cap/webui"
// Taildrive grants the ability for a peer to access Taildrive
// shares.
Taildrive Cap = "tailscale.com/cap/drive"
// TaildriveSharer indicates that a peer has the ability to
// share folders with us.
TaildriveSharer Cap = "tailscale.com/cap/drive-sharer"
// Kubernetes grants a peer Kubernetes-specific
// capabilities, such as the ability to impersonate specific Tailscale
// user groups as Kubernetes user groups. This capability is read by
// peers that are Tailscale Kubernetes operator instances.
Kubernetes Cap = "tailscale.com/cap/kubernetes"
// Relay grants the ability for a peer to allocate relay
// endpoints.
Relay Cap = "tailscale.com/cap/relay"
// RelayTarget grants the current node the ability to allocate
// relay endpoints to the peer which has this capability.
RelayTarget Cap = "tailscale.com/cap/relay-target"
// TsIDP grants a peer tsidp-specific
// capabilities, such as the ability to add user groups to the OIDC
// claim
TsIDP Cap = "tailscale.com/cap/tsidp"
)
const (
// Conn25Prefix is the prefix for per-app [Cap] names that grant a peer
// access to an app provided by a conn25 app connector.
// Actual capabilities look like "tailscale.com/cap/conn25/example" for
// an app named "example".
// Each value under such a key is a [tailscale.com/tailcfg.ProtoPortRange].
// If multiple values are present, the union of all protocol/port
// combinations described shall be permitted.
Conn25Prefix Prefix = "tailscale.com/cap/conn25/"
)
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package tailcfg
import (
"errors"
"strconv"
"strings"
"tailscale.com/types/ipproto"
"tailscale.com/util/vizerror"
)
var (
errEmptyProtocol = errors.New("empty protocol")
errEmptyString = errors.New("empty string")
)
// ProtoPortRange is used to encode "proto:port" format.
// The following formats are supported:
//
// "*" allows all TCP, UDP and ICMP traffic on all ports.
// "<ports>" allows all TCP, UDP and ICMP traffic on the specified ports.
// "proto:*" allows traffic of the specified proto on all ports.
// "proto:<port>" allows traffic of the specified proto on the specified port.
//
// Ports are either a single port number or a range of ports (e.g. "80-90").
// String named protocols support names that ipproto.Proto accepts.
type ProtoPortRange struct {
// Proto is the IP protocol number.
// If Proto is 0, it means TCP+UDP+ICMP(4+6).
Proto int
Ports PortRange
}
// UnmarshalText implements the encoding.TextUnmarshaler interface. See
// ProtoPortRange for the format.
func (ppr *ProtoPortRange) UnmarshalText(text []byte) error {
ppr2, err := parseProtoPortRange(string(text))
if err != nil {
return err
}
*ppr = *ppr2
return nil
}
// MarshalText implements the encoding.TextMarshaler interface. See
// ProtoPortRange for the format.
func (ppr *ProtoPortRange) MarshalText() ([]byte, error) {
if ppr.Proto == 0 && ppr.Ports == (PortRange{}) {
return []byte{}, nil
}
return []byte(ppr.String()), nil
}
// String implements the stringer interface. See ProtoPortRange for the
// format.
func (ppr ProtoPortRange) String() string {
if ppr.Proto == 0 {
if ppr.Ports == PortRangeAny {
return "*"
}
}
var buf strings.Builder
if ppr.Proto != 0 {
// Proto.MarshalText is infallible.
text, _ := ipproto.Proto(ppr.Proto).MarshalText()
buf.Write(text)
buf.Write([]byte(":"))
}
buf.WriteString(ppr.Ports.String())
return buf.String()
}
// ParseProtoPortRanges parses a slice of IP port range fields.
func ParseProtoPortRanges(ips []string) ([]ProtoPortRange, error) {
var out []ProtoPortRange
for _, p := range ips {
ppr, err := parseProtoPortRange(p)
if err != nil {
return nil, err
}
out = append(out, *ppr)
}
return out, nil
}
func parseProtoPortRange(ipProtoPort string) (*ProtoPortRange, error) {
if ipProtoPort == "" {
return nil, errEmptyString
}
if ipProtoPort == "*" {
return &ProtoPortRange{Ports: PortRangeAny}, nil
}
if !strings.Contains(ipProtoPort, ":") {
ipProtoPort = "*:" + ipProtoPort
}
protoStr, portRange, err := ParseHostPortRange(ipProtoPort)
if err != nil {
return nil, err
}
if protoStr == "" {
return nil, errEmptyProtocol
}
ppr := &ProtoPortRange{
Ports: portRange,
}
if protoStr == "*" {
return ppr, nil
}
var ipProto ipproto.Proto
if err := ipProto.UnmarshalText([]byte(protoStr)); err != nil {
return nil, err
}
ppr.Proto = int(ipProto)
return ppr, nil
}
// ParseHostPortRange parses hostport as HOST:PORTS where HOST is
// returned unchanged and PORTS is either "*" or PORTLOW-PORTHIGH ranges.
func ParseHostPortRange(hostport string) (host string, ports PortRange, err error) {
hostport = strings.ToLower(hostport)
colon := strings.LastIndexByte(hostport, ':')
if colon < 0 {
return "", ports, vizerror.New("hostport must contain a colon (\":\")")
}
host = hostport[:colon]
portlist := hostport[colon+1:]
if strings.Contains(host, ",") {
return "", ports, vizerror.New("host cannot contain a comma (\",\")")
}
if portlist == "*" {
// Special case: permit hostname:* as a port wildcard.
return host, PortRangeAny, nil
}
if len(portlist) == 0 {
return "", ports, vizerror.Errorf("invalid port list: %#v", portlist)
}
if strings.Count(portlist, "-") > 1 {
return "", ports, vizerror.Errorf("port range %#v: too many dashes(-)", portlist)
}
firstStr, lastStr, isRange := strings.Cut(portlist, "-")
var first, last uint64
first, err = strconv.ParseUint(firstStr, 10, 16)
if err != nil {
return "", ports, vizerror.Errorf("port range %#v: invalid first integer", portlist)
}
if isRange {
last, err = strconv.ParseUint(lastStr, 10, 16)
if err != nil {
return "", ports, vizerror.Errorf("port range %#v: invalid last integer", portlist)
}
} else {
last = first
}
if first == 0 {
return "", ports, vizerror.Errorf("port range %#v: first port must be >0, or use '*' for wildcard", portlist)
}
if first > last {
return "", ports, vizerror.Errorf("port range %#v: first port must be >= last port", portlist)
}
return host, newPortRange(uint16(first), uint16(last)), nil
}
func newPortRange(first, last uint16) PortRange {
return PortRange{First: first, Last: last}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package tailcfg contains types used by the Tailscale protocol with between
// the node and the coordination server.
package tailcfg
//go:generate go run tailscale.com/cmd/viewer --type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,RegisterResponseAuth,RegisterRequest,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location,UserProfile,VIPService,SSHPolicy --clonefunc
import (
"bytes"
"cmp"
"encoding/json"
"errors"
"fmt"
"maps"
"net/netip"
"reflect"
"slices"
"strconv"
"strings"
"time"
"tailscale.com/feature/buildfeatures"
"tailscale.com/tailcfg/nodecap"
"tailscale.com/tailcfg/peercap"
"tailscale.com/types/dnstype"
"tailscale.com/types/key"
"tailscale.com/types/opt"
"tailscale.com/types/structs"
"tailscale.com/types/tkatype"
"tailscale.com/types/views"
"tailscale.com/util/dnsname"
"tailscale.com/util/slicesx"
"tailscale.com/util/vizerror"
)
// CapabilityVersion represents the client's capability level. That
// is, it can be thought of as the client's simple version number: a
// single monotonically increasing integer, rather than the relatively
// complex x.y.z-xxxxx semver+hash(es). Whenever the client gains a
// capability or wants to negotiate a change in semantics with the
// server (control plane), peers (over PeerAPI), or frontend (over
// LocalAPI), bump this number and document what's new.
//
// Previously (prior to 2022-03-06), it was known as the "MapRequest
// version" or "mapVer" or "map cap" and that name and usage persists
// in places.
type CapabilityVersion int
// CurrentCapabilityVersion is the current capability version of the codebase.
//
// History of versions:
//
// - 3: implicit compression, keep-alives
// - 4: opt-in keep-alives via KeepAlive field, opt-in compression via Compress
// - 5: 2020-10-19, implies IncludeIPv6, delta Peers/UserProfiles, supports MagicDNS
// - 6: 2020-12-07: means MapResponse.PacketFilter nil means unchanged
// - 7: 2020-12-15: FilterRule.SrcIPs accepts CIDRs+ranges, doesn't warn about 0.0.0.0/::
// - 8: 2020-12-19: client can buggily receive IPv6 addresses and routes if beta enabled server-side
// - 9: 2020-12-30: client doesn't auto-add implicit search domains from peers; only DNSConfig.Domains
// - 10: 2021-01-17: client understands MapResponse.PeerSeenChange
// - 11: 2021-03-03: client understands IPv6, multiple default routes, and goroutine dumping
// - 12: 2021-03-04: client understands PingRequest
// - 13: 2021-03-19: client understands FilterRule.IPProto
// - 14: 2021-04-07: client understands DNSConfig.Routes and DNSConfig.Resolvers
// - 15: 2021-04-12: client treats nil MapResponse.DNSConfig as meaning unchanged
// - 16: 2021-04-15: client understands Node.Online, MapResponse.OnlineChange
// - 17: 2021-04-18: MapResponse.Domain empty means unchanged
// - 18: 2021-04-19: MapResponse.Node nil means unchanged (all fields now omitempty)
// - 19: 2021-04-21: MapResponse.Debug.SleepSeconds
// - 20: 2021-06-11: MapResponse.LastSeen used even less (https://github.com/tailscale/tailscale/issues/2107)
// - 21: 2021-06-15: added MapResponse.DNSConfig.CertDomains
// - 22: 2021-06-16: added MapResponse.DNSConfig.ExtraRecords
// - 23: 2021-08-25: DNSConfig.Routes values may be empty (for ExtraRecords support in 1.14.1+)
// - 24: 2021-09-18: MapResponse.Health from control to node; node shows in "tailscale status"
// - 25: 2021-11-01: MapResponse.Debug.Exit
// - 26: 2022-01-12: (nothing, just bumping for 1.20.0)
// - 27: 2022-02-18: start of SSHPolicy being respected
// - 28: 2022-03-09: client can communicate over Noise.
// - 29: 2022-03-21: MapResponse.PopBrowserURL
// - 30: 2022-03-22: client can request id tokens.
// - 31: 2022-04-15: PingRequest & PingResponse TSMP & disco support
// - 32: 2022-04-17: client knows FilterRule.CapMatch
// - 33: 2022-07-20: added MapResponse.PeersChangedPatch (DERPRegion + Endpoints)
// - 34: 2022-08-02: client understands CapabilityFileSharingTarget
// - 36: 2022-08-02: added PeersChangedPatch.{Key,DiscoKey,Online,LastSeen,KeyExpiry,Capabilities}
// - 37: 2022-08-09: added Debug.{SetForceBackgroundSTUN,SetRandomizeClientPort}; Debug are sticky
// - 38: 2022-08-11: added PingRequest.URLIsNoise
// - 39: 2022-08-15: clients can talk Noise over arbitrary HTTPS port
// - 40: 2022-08-22: added Node.KeySignature, PeersChangedPatch.KeySignature
// - 41: 2022-08-30: uses 100.100.100.100 for route-less ExtraRecords if global nameservers is set
// - 42: 2022-09-06: NextDNS DoH support; see https://github.com/tailscale/tailscale/pull/5556
// - 43: 2022-09-21: clients can return usernames for SSH
// - 44: 2022-09-22: MapResponse.ControlDialPlan
// - 45: 2022-09-26: c2n /debug/{goroutines,prefs,metrics}
// - 46: 2022-10-04: c2n /debug/component-logging
// - 47: 2022-10-11: Register{Request,Response}.NodeKeySignature
// - 48: 2022-11-02: Node.UnsignedPeerAPIOnly
// - 49: 2022-11-03: Client understands EarlyNoise
// - 50: 2022-11-14: Client understands CapabilityIngress
// - 51: 2022-11-30: Client understands CapabilityTailnetLockAlpha
// - 52: 2023-01-05: client can handle c2n POST /logtail/flush
// - 53: 2023-01-18: client respects explicit Node.Expired + auto-sets based on Node.KeyExpiry
// - 54: 2023-01-19: Node.Cap added, PeersChangedPatch.Cap, uses Node.Cap for ExitDNS before Hostinfo.Services fallback
// - 55: 2023-01-23: start of c2n GET+POST /update handler
// - 56: 2023-01-24: Client understands CapabilityDebugTSDNSResolution
// - 57: 2023-01-25: Client understands CapabilityBindToInterfaceByRoute
// - 58: 2023-03-10: Client retries lite map updates before restarting map poll.
// - 59: 2023-03-16: Client understands Peers[].SelfNodeV4MasqAddrForThisPeer
// - 60: 2023-04-06: Client understands IsWireGuardOnly
// - 61: 2023-04-18: Client understand SSHAction.SSHRecorderFailureAction
// - 62: 2023-05-05: Client can notify control over noise for SSHEventNotificationRequest recording failure events
// - 63: 2023-06-08: Client understands SSHAction.AllowRemotePortForwarding.
// - 64: 2023-07-11: Client understands s/CapabilityTailnetLockAlpha/CapabilityTailnetLock
// - 65: 2023-07-12: Client understands DERPMap.HomeParams + incremental DERPMap updates with params
// - 66: 2023-07-23: UserProfile.Groups added (available via WhoIs) (removed in 87)
// - 67: 2023-07-25: Client understands PeerCapMap
// - 68: 2023-08-09: Client has dedicated updateRoutine; MapRequest.Stream true means ignore Hostinfo+Endpoints
// - 69: 2023-08-16: removed Debug.LogHeap* + GoroutineDumpURL; added c2n /debug/logheap
// - 70: 2023-08-16: removed most Debug fields; added NodeAttrDisable*, NodeAttrDebug* instead
// - 71: 2023-08-17: added NodeAttrOneCGNATEnable, NodeAttrOneCGNATDisable
// - 72: 2023-08-23: TS-2023-006 UPnP issue fixed; UPnP can now be used again
// - 73: 2023-09-01: Non-Windows clients expect to receive ClientVersion
// - 74: 2023-09-18: Client understands NodeCapMap
// - 75: 2023-09-12: Client understands NodeAttrDNSForwarderDisableTCPRetries
// - 76: 2023-09-20: Client understands ExitNodeDNSResolvers for IsWireGuardOnly nodes
// - 77: 2023-10-03: Client understands Peers[].SelfNodeV6MasqAddrForThisPeer
// - 78: 2023-10-05: can handle c2n Wake-on-LAN sending
// - 79: 2023-10-05: Client understands UrgentSecurityUpdate in ClientVersion
// - 80: 2023-11-16: can handle c2n GET /tls-cert-status
// - 81: 2023-11-17: MapResponse.PacketFilters (incremental packet filter updates)
// - 82: 2023-12-01: Client understands NodeAttrLinuxMustUseIPTables, NodeAttrLinuxMustUseNfTables, c2n /netfilter-kind
// - 83: 2023-12-18: Client understands DefaultAutoUpdate
// - 84: 2024-01-04: Client understands SeamlessKeyRenewal
// - 85: 2024-01-05: Client understands MaxKeyDuration
// - 86: 2024-01-23: Client understands NodeAttrProbeUDPLifetime
// - 87: 2024-02-11: UserProfile.Groups removed (added in 66)
// - 88: 2024-03-05: Client understands NodeAttrSuggestExitNode
// - 89: 2024-03-23: Client no longer respects deleted PeerChange.Capabilities (use CapMap)
// - 90: 2024-04-03: Client understands PeerCapabilityTaildrive.
// - 91: 2024-04-24: Client understands PeerCapabilityTaildriveSharer.
// - 92: 2024-05-06: Client understands NodeAttrUserDialUseRoutes.
// - 93: 2024-05-06: added support for stateful firewalling.
// - 94: 2024-05-06: Client understands Node.IsJailed.
// - 95: 2024-05-06: Client uses NodeAttrUserDialUseRoutes to change DNS dialing behavior.
// - 96: 2024-05-29: Client understands NodeAttrSSHBehaviorV1
// - 97: 2024-06-06: Client understands NodeAttrDisableSplitDNSWhenNoCustomResolvers
// - 98: 2024-06-13: iOS/tvOS clients may provide serial number as part of posture information
// - 99: 2024-06-14: Client understands NodeAttrDisableLocalDNSOverrideViaNRPT
// - 100: 2024-06-18: Initial support for filtertype.Match.SrcCaps - actually usable in capver 109 (issue #12542)
// - 101: 2024-07-01: Client supports SSH agent forwarding when handling connections with /bin/su
// - 102: 2024-07-12: NodeAttrDisableMagicSockCryptoRouting support
// - 103: 2024-07-24: Client supports NodeAttrDisableCaptivePortalDetection
// - 104: 2024-08-03: SelfNodeV6MasqAddrForThisPeer now works
// - 105: 2024-08-05: Fixed SSH behavior on systems that use busybox (issue #12849)
// - 106: 2024-09-03: fix panic regression from cryptokey routing change (65fe0ba7b5)
// - 107: 2024-10-30: add App Connector to conffile (PR #13942)
// - 108: 2024-11-08: Client sends ServicesHash in Hostinfo, understands c2n GET /vip-services.
// - 109: 2024-11-18: Client supports filtertype.Match.SrcCaps (issue #12542)
// - 110: 2024-12-12: removed never-before-used Tailscale SSH public key support (#14373)
// - 111: 2025-01-14: Client supports a peer having Node.HomeDERP (issue #14636)
// - 112: 2025-01-14: Client interprets AllowedIPs of nil as meaning same as Addresses
// - 113: 2025-01-20: Client communicates to control whether funnel is enabled by sending Hostinfo.IngressEnabled (#14688)
// - 114: 2025-01-30: NodeAttrMaxKeyDuration CapMap defined, clients might use it (no tailscaled code change) (#14829)
// - 115: 2025-03-07: Client understands DERPRegion.NoMeasureNoHome.
// - 116: 2025-05-05: Client serves MagicDNS "AAAA" if NodeAttrMagicDNSPeerAAAA set on self node
// - 117: 2025-05-28: Client understands DisplayMessages (structured health messages), but not necessarily PrimaryAction.
// - 118: 2025-07-01: Client sends Hostinfo.StateEncrypted to report whether the state file is encrypted at rest (#15830)
// - 119: 2025-07-10: Client uses Hostinfo.Location.Priority to prioritize one route over another.
// - 120: 2025-07-15: Client understands peer relay disco messages, and implements peer client and relay server functions
// - 121: 2025-07-19: Client understands peer relay endpoint alloc with [disco.AllocateUDPRelayEndpointRequest] & [disco.AllocateUDPRelayEndpointResponse]
// - 122: 2025-07-21: Client sends Hostinfo.ExitNodeID to report which exit node it has selected, if any.
// - 123: 2025-07-28: fix deadlock regression from cryptokey routing change (issue #16651)
// - 124: 2025-08-08: removed NodeAttrDisableMagicSockCryptoRouting support, crypto routing is now mandatory
// - 125: 2025-08-11: dnstype.Resolver adds UseWithExitNode field.
// - 126: 2025-09-17: Client uses seamless key renewal unless disabled by control (tailscale/corp#31479)
// - 127: 2025-09-19: can handle C2N /debug/netmap.
// - 128: 2025-10-02: can handle C2N /debug/health.
// - 129: 2025-10-04: Fixed sleep/wake deadlock in magicsock when using peer relay (PR #17449)
// - 130: 2025-10-06: client can send key.HardwareAttestationPublic and key.HardwareAttestationKeySignature in MapRequest
// - 131: 2025-11-25: client respects [NodeAttrDefaultAutoUpdate]
// - 132: 2026-02-13: client respects [NodeAttrDisableHostsFileUpdates]
// - 133: 2026-02-17: client understands [NodeAttrForceRegisterMagicDNSIPv4Only]; MagicDNS IPv6 registered w/ OS by default
// - 134: 2026-03-09: Client understands [NodeAttrDisableAndroidBindToActiveNetwork]
// - 135: 2026-03-30: Client understands [NodeAttrCacheNetworkMaps]
// - 136: 2026-04-09: Client understands [NodeAttrDisableLinuxCGNATDropRule]
// - 137: 2026-04-15: Client handles 429 responses to /machine/register.
// - 138: 2026-03-31: can handle C2N /debug/tka.
// - 139: 2026-05-22: Client understands [NodeAttrEmitRuntimeMetrics]
// - 140: 2026-05-27: Client understands [NodeAttrDisableUDPGRO], [NodeAttrDisableUDPGSO], [NodeAttrDisableTUNUDPGRO], [NodeAttrDisableTUNTCPGRO]
// - 141: 2026-05-28: Client understands [NodeAttrNeverGSOEqualTail]
// - 142: 2026-07-06: Client understands c2n /remoteapi/localapi/* proxy
// - 143: 2026-07-22: Client correctly ignores conn25 node attributes when not enabled by environment variable
// - 144: 2026-07-31: Client sends [packet.TSMPDiscoKeyAdvertisement] around WireGuard handshakes
// - 145: 2026-08-04: Client understands [NodeAttrScopeQuad100OnMacOS]
// - 146: 2026-09-02: Client understands [NodeAttrConnReject]; can handle C2N /debug/rejects.
// - 147: 2026-09-09: Client handles 429/503 responses with retry-after headers to /machine/ endpoints
const CurrentCapabilityVersion CapabilityVersion = 147
// ID is an integer ID for a user, node, or login allocated by the
// control plane.
//
// To be nice, control plane servers should not use int64s that are too large to
// fit in a JavaScript number (see JavaScript's Number.MAX_SAFE_INTEGER).
// The Tailscale-hosted control plane stopped allocating large integers in
// March 2023 but nodes prior to that may have IDs larger than
// MAX_SAFE_INTEGER (2^53 – 1).
//
// IDs must not be zero or negative.
type ID int64
// UserID is an [ID] for a [User].
type UserID ID
func (u UserID) IsZero() bool {
return u == 0
}
// LoginID is an [ID] for a [Login].
//
// It is not used in the Tailscale client, but is used in the control plane.
type LoginID ID
func (u LoginID) IsZero() bool {
return u == 0
}
// NodeID is a unique integer ID for a node.
//
// It's global within a control plane URL ("tailscale up --login-server") and is
// (as of 2025-01-06) never re-used even after a node is deleted.
//
// To be nice, control plane servers should not use int64s that are too large to
// fit in a JavaScript number (see JavaScript's Number.MAX_SAFE_INTEGER).
// The Tailscale-hosted control plane stopped allocating large integers in
// March 2023 but nodes prior to that may have node IDs larger than
// MAX_SAFE_INTEGER (2^53 – 1).
//
// NodeIDs are not stable across control plane URLs. For more stable URLs,
// see [StableNodeID].
type NodeID ID
func (u NodeID) IsZero() bool {
return u == 0
}
// StableNodeID is a string form of [NodeID].
//
// Different control plane servers should ideally have different StableNodeID
// suffixes for different sites or regions.
//
// Being a string, it's safer to use in JavaScript without worrying about the
// size of the integer, as documented on [NodeID].
//
// But in general, Tailscale APIs can accept either a [NodeID] integer or a
// [StableNodeID] string when referring to a node.
type StableNodeID string
func (u StableNodeID) IsZero() bool {
return u == ""
}
// User is a Tailscale user.
//
// A user can have multiple logins associated with it (e.g. gmail and github oauth).
// (Note: none of our UIs support this yet.)
//
// Some properties are inherited from the logins and can be overridden, such as
// display name and profile picture.
//
// Other properties must be the same for all logins associated with a user.
// In particular: domain. If a user has a "tailscale.io" domain login, they cannot
// have a general gmail address login associated with the user.
type User struct {
ID UserID
DisplayName string // if non-empty overrides Login field
ProfilePicURL string `json:",omitzero"` // if non-empty overrides Login field
Created time.Time `json:",omitzero"`
}
// Login is a user from a specific identity provider, not associated with any
// particular tailnet.
type Login struct {
_ structs.Incomparable
ID LoginID // unused in the Tailscale client
Provider string // "google", "github", "okta_foo", etc.
LoginName string // an email address or "email-ish" string (like alice@github)
DisplayName string // from the IdP
ProfilePicURL string `json:",omitzero"` // from the IdP
}
// A UserProfile is display-friendly data for a [User].
// It includes the LoginName for display purposes but *not* the Provider.
// It also includes derived data from one of the user's logins.
type UserProfile struct {
ID UserID
LoginName string // "alice@smith.com"; for display purposes only (provider is not listed)
DisplayName string // "Alice Smith"
ProfilePicURL string `json:",omitzero"`
// Groups is a subset of SCIM groups (e.g. "engineering@example.com")
// or group names in the tailnet policy document (e.g. "group:eng")
// that contain this user and that the coordination server was
// configured to report to this node.
// The list is always sorted when loaded from storage.
Groups []string `json:",omitempty"`
}
func (p *UserProfile) Equal(p2 *UserProfile) bool {
if p == nil && p2 == nil {
return true
}
if p == nil || p2 == nil {
return false
}
return p.ID == p2.ID &&
p.LoginName == p2.LoginName &&
p.DisplayName == p2.DisplayName &&
p.ProfilePicURL == p2.ProfilePicURL &&
slices.Equal(p.Groups, p2.Groups)
}
// RawMessage is a raw encoded JSON value. It implements Marshaler and
// Unmarshaler and can be used to delay JSON decoding or precompute a JSON
// encoding.
//
// It is like json.RawMessage but is a string instead of a []byte to better
// portray immutable data.
type RawMessage string
// MarshalJSON returns m as the JSON encoding of m.
func (m RawMessage) MarshalJSON() ([]byte, error) {
if m == "" {
return []byte("null"), nil
}
return []byte(m), nil
}
// UnmarshalJSON sets *m to a copy of data.
func (m *RawMessage) UnmarshalJSON(data []byte) error {
if m == nil {
return errors.New("RawMessage: UnmarshalJSON on nil pointer")
}
*m = RawMessage(data)
return nil
}
// MarshalCapJSON returns a capability rule in RawMessage string format.
func MarshalCapJSON[T any](capRule T) (RawMessage, error) {
bs, err := json.Marshal(capRule)
if err != nil {
return "", fmt.Errorf("error marshalling capability rule: %w", err)
}
return RawMessage(string(bs)), nil
}
// Node is a Tailscale device in a tailnet.
type Node struct {
ID NodeID
StableID StableNodeID
// Name is the FQDN of the node.
// It is also the MagicDNS name for the node.
// It has a trailing dot.
// e.g. "host.tail-scale.ts.net."
Name string
// User is the user who created the node. If ACL tags are in use for the
// node then it doesn't reflect the ACL identity that the node is running
// as.
User UserID
// Sharer, if non-zero, is the user who shared this node, if different than User.
Sharer UserID `json:",omitzero"`
Key key.NodePublic
KeyExpiry time.Time `json:",omitzero"` // the zero value if this node does not expire
KeySignature tkatype.MarshaledSignature `json:",omitempty"`
Machine key.MachinePublic `json:",omitzero"`
DiscoKey key.DiscoPublic `json:",omitzero"`
// Addresses are the IP addresses of this Node directly.
Addresses []netip.Prefix
// AllowedIPs are the IP ranges to route to this node.
//
// As of CapabilityVersion 112, this may be nil (null or undefined) on the wire
// to mean the same as Addresses. Internally, it is always filled in with
// its possibly-implicit value.
AllowedIPs []netip.Prefix `json:",omitzero"` // _not_ omitempty; only nil is special
Endpoints []netip.AddrPort `json:",omitempty"` // IP+port (public via STUN, and local LANs)
// LegacyDERPString is this node's home LegacyDERPString region ID integer, but shoved into an
// IP:port string for legacy reasons. The IP address is always "127.3.3.40"
// (a loopback address (127) followed by the digits over the letters DERP on
// a QWERTY keyboard (3.3.40)). The "port number" is the home LegacyDERPString region ID
// integer.
//
// Deprecated: HomeDERP has replaced this, but old servers might still send
// this field. See tailscale/tailscale#14636. Do not use this field in code
// other than in the upgradeNode func, which canonicalizes it to HomeDERP
// if it arrives as a LegacyDERPString string on the wire.
LegacyDERPString string `json:"DERP,omitzero"` // DERP-in-IP:port ("127.3.3.40:N") endpoint
// HomeDERP is the modern version of the DERP string field, with just an
// integer. The client advertises support for this as of capver 111.
//
// HomeDERP may be zero if not (yet) known, but ideally always be non-zero
// for magicsock connectivity to function normally.
HomeDERP DERPRegionID `json:",omitzero"` // DERP region ID of the node's home DERP
Hostinfo HostinfoView `json:",omitzero"`
Created time.Time `json:",omitzero"`
Cap CapabilityVersion `json:",omitzero"` // if non-zero, the node's capability version; old servers might not send
// Tags are the list of ACL tags applied to this node.
// Tags take the form of `tag:<value>` where value starts
// with a letter and only contains alphanumerics and dashes `-`.
// Some valid tag examples:
// `tag:prod`
// `tag:database`
// `tag:lab-1`
Tags []string `json:",omitempty"`
// PrimaryRoutes are the routes from AllowedIPs that this node
// is currently the primary subnet router for, as determined
// by the control plane. It does not include the self address
// values from Addresses that are in AllowedIPs.
PrimaryRoutes []netip.Prefix `json:",omitempty"`
// LastSeen is when the node was last online. It is not
// updated when Online is true. It is nil if the current
// node doesn't have permission to know, or the node
// has never been online.
LastSeen *time.Time `json:",omitempty"`
// Online is whether the node is currently connected to the
// coordination server. A value of nil means unknown, or the
// current node doesn't have permission to know.
Online *bool `json:",omitempty"`
MachineAuthorized bool `json:",omitempty"` // TODO(crawshaw): replace with MachineStatus
// Capabilities are capabilities that the node has.
// They're free-form strings, but should be in the form of URLs/URIs
// such as:
// "https://tailscale.com/cap/is-admin"
// "https://tailscale.com/cap/file-sharing"
//
// Deprecated: use CapMap instead. See https://github.com/tailscale/tailscale/issues/11508
Capabilities []nodecap.Cap `json:",omitempty"`
// CapMap is a map of capabilities to their optional argument/data values.
//
// It is valid for a capability to not have any argument/data values; such
// capabilities can be tested for using the HasCap method. These type of
// capabilities are used to indicate that a node has a capability, but there
// is no additional data associated with it. These were previously
// represented by the Capabilities field, but can now be represented by
// CapMap with an empty value.
//
// See [nodecap.Cap] for more information on keys.
//
// Metadata about nodes can be transmitted in 3 ways:
// 1. MapResponse.Node.CapMap describes attributes that affect behavior for
// this node, such as which features have been enabled through the admin
// panel and any associated configuration details.
// 2. MapResponse.PacketFilter(s) describes access (both IP and application
// based) that should be granted to peers.
// 3. MapResponse.Peers[].CapMap describes attributes regarding a peer node,
// such as which features the peer supports or if that peer is preferred
// for a particular task vs other peers that could also be chosen.
CapMap NodeCapMap `json:",omitempty"`
// UnsignedPeerAPIOnly means that this node is not signed nor subject to TKA
// restrictions. However, in exchange for that privilege, it does not get
// network access. It can only access this node's peerapi, which may not let
// it do anything. It is the tailscaled client's job to double-check the
// MapResponse's PacketFilter to verify that its AllowedIPs will not be
// accepted by the packet filter.
UnsignedPeerAPIOnly bool `json:",omitzero"`
// The following three computed fields hold the various names that can
// be used for this node in UIs. They are populated from controlclient
// (not from control) by calling node.InitDisplayNames. These can be
// used directly or accessed via node.DisplayName or node.DisplayNames.
ComputedName string `json:",omitzero"` // MagicDNS base name (for normal non-shared-in nodes), FQDN (without trailing dot, for shared-in nodes), or Hostname (if no MagicDNS)
computedHostIfDifferent string // hostname, if different than ComputedName, otherwise empty
ComputedNameWithHost string `json:",omitzero"` // either "ComputedName" or "ComputedName (computedHostIfDifferent)", if computedHostIfDifferent is set
// DataPlaneAuditLogID is the per-node logtail ID used for data plane audit logging.
DataPlaneAuditLogID string `json:",omitzero"`
// Expired is whether this node's key has expired. Control may send
// this; clients are only allowed to set this from false to true. On
// the client, this is calculated client-side based on a timestamp sent
// from control, to avoid clock skew issues.
Expired bool `json:",omitzero"`
// SelfNodeV4MasqAddrForThisPeer is the IPv4 that this peer knows the current node as.
// It may be empty if the peer knows the current node by its native
// IPv4 address.
// This field is only populated in a MapResponse for peers and not
// for the current node.
//
// If set, it should be used to masquerade traffic originating from the
// current node to this peer. The masquerade address is only relevant
// for this peer and not for other peers.
//
// This only applies to traffic originating from the current node to the
// peer or any of its subnets. Traffic originating from subnet routes will
// not be masqueraded (e.g. in case of --snat-subnet-routes).
SelfNodeV4MasqAddrForThisPeer *netip.Addr `json:",omitzero"` // TODO: de-pointer: tailscale/tailscale#17978
// SelfNodeV6MasqAddrForThisPeer is the IPv6 that this peer knows the current node as.
// It may be empty if the peer knows the current node by its native
// IPv6 address.
// This field is only populated in a MapResponse for peers and not
// for the current node.
//
// If set, it should be used to masquerade traffic originating from the
// current node to this peer. The masquerade address is only relevant
// for this peer and not for other peers.
//
// This only applies to traffic originating from the current node to the
// peer or any of its subnets. Traffic originating from subnet routes will
// not be masqueraded (e.g. in case of --snat-subnet-routes).
SelfNodeV6MasqAddrForThisPeer *netip.Addr `json:",omitzero"` // TODO: de-pointer: tailscale/tailscale#17978
// IsWireGuardOnly indicates that this is a non-Tailscale WireGuard peer, it
// is not expected to speak Disco or DERP, and it must have Endpoints in
// order to be reachable.
IsWireGuardOnly bool `json:",omitzero"`
// IsJailed indicates that this node is jailed and should not be allowed
// initiate connections, however outbound connections to it should still be
// allowed.
IsJailed bool `json:",omitzero"`
// ExitNodeDNSResolvers is the list of DNS servers that should be used when this
// node is marked IsWireGuardOnly and being used as an exit node.
ExitNodeDNSResolvers []*dnstype.Resolver `json:",omitempty"`
}
// HasCap reports whether the node has the given capability.
// It is safe to call on an invalid NodeView.
func (v NodeView) HasCap(cap nodecap.Cap) bool {
return v.ж.HasCap(cap)
}
// HasCap reports whether the node has the given capability.
// It is safe to call on a nil Node.
func (v *Node) HasCap(cap nodecap.Cap) bool {
return v != nil && v.CapMap.Contains(cap)
}
// DisplayName returns the user-facing name for a node which should
// be shown in client UIs.
//
// Parameter forOwner specifies whether the name is requested by
// the owner of the node. When forOwner is false, the hostname is
// never included in the return value.
//
// Return value is either "Name" or "Name (Hostname)", where
// Name is the node's MagicDNS base name (for normal non-shared-in
// nodes), FQDN (without trailing dot, for shared-in nodes), or
// Hostname (if no MagicDNS). Hostname is only included in the
// return value if it varies from Name and forOwner is provided true.
//
// DisplayName is only valid if InitDisplayNames has been called.
func (n *Node) DisplayName(forOwner bool) string {
if forOwner {
return n.ComputedNameWithHost
}
return n.ComputedName
}
// DisplayNames returns the decomposed user-facing name for a node.
//
// Parameter forOwner specifies whether the name is requested by
// the owner of the node. When forOwner is false, hostIfDifferent
// is always returned empty.
//
// Return value name is the node's primary name, populated with the
// node's MagicDNS base name (for normal non-shared-in nodes), FQDN
// (without trailing dot, for shared-in nodes), or Hostname (if no
// MagicDNS).
//
// Return value hostIfDifferent, when non-empty, is the node's
// hostname. hostIfDifferent is only populated when the hostname
// varies from name and forOwner is provided as true.
//
// DisplayNames is only valid if InitDisplayNames has been called.
func (n *Node) DisplayNames(forOwner bool) (name, hostIfDifferent string) {
if forOwner {
return n.ComputedName, n.computedHostIfDifferent
}
return n.ComputedName, ""
}
// IsRouter reports whether n is a router: it routes addresses besides its own.
// Examples: an exit node, a subnet router, an app connector, etc.
func (n *Node) IsRouter() bool {
// TODO(sfllaw): Keep this aligned with dbx.Node.IsSubnetRouter.
for _, r := range n.AllowedIPs {
if !slices.Contains(n.Addresses, r) {
return true
}
}
return false
}
// IsTagged reports whether the node has any tags.
func (n *Node) IsTagged() bool {
return len(n.Tags) > 0
}
// SharerOrUser Sharer if set, else User.
func (n *Node) SharerOrUser() UserID {
return cmp.Or(n.Sharer, n.User)
}
// IsRouter reports whether n is a router: it routes addresses besides its own.
// Examples: an exit node, a subnet router, an app connector, etc.
func (n NodeView) IsRouter() bool { return n.ж.IsRouter() }
// IsTagged reports whether the node has any tags.
func (n NodeView) IsTagged() bool { return n.ж.IsTagged() }
// DisplayName wraps Node.DisplayName.
func (n NodeView) DisplayName(forOwner bool) string { return n.ж.DisplayName(forOwner) }
// SharerOrUser wraps Node.SharerOrUser.
func (n NodeView) SharerOrUser() UserID { return n.ж.SharerOrUser() }
// InitDisplayNames computes and populates n's display name
// fields: n.ComputedName, n.computedHostIfDifferent, and
// n.ComputedNameWithHost.
func (n *Node) InitDisplayNames(networkMagicDNSSuffix string) {
name := dnsname.TrimSuffix(n.Name, networkMagicDNSSuffix)
var hostIfDifferent string
if n.Hostinfo.Valid() {
hostIfDifferent = dnsname.SanitizeHostname(n.Hostinfo.Hostname())
}
if strings.EqualFold(name, hostIfDifferent) {
hostIfDifferent = ""
}
if name == "" {
if hostIfDifferent != "" {
name = hostIfDifferent
hostIfDifferent = ""
} else {
name = n.Key.String()
}
}
var nameWithHost string
if hostIfDifferent != "" {
nameWithHost = fmt.Sprintf("%s (%s)", name, hostIfDifferent)
} else {
nameWithHost = name
}
n.ComputedName = name
n.computedHostIfDifferent = hostIfDifferent
n.ComputedNameWithHost = nameWithHost
}
// MachineStatus is the state of a [Node]'s approval into a tailnet.
//
// A "node" and a "machine" are often 1:1, but technically a Tailscale
// daemon has one machine key and can have multiple nodes (e.g. different
// users on Windows) for that one machine key.
type MachineStatus int
const (
MachineUnknown = MachineStatus(iota)
MachineUnauthorized // server has yet to approve
MachineAuthorized // server has approved
MachineInvalid // server has explicitly rejected this machine key
)
func (m MachineStatus) AppendText(b []byte) ([]byte, error) {
return append(b, m.String()...), nil
}
func (m MachineStatus) MarshalText() ([]byte, error) {
return []byte(m.String()), nil
}
func (m *MachineStatus) UnmarshalText(b []byte) error {
switch string(b) {
case "machine-unknown":
*m = MachineUnknown
case "machine-unauthorized":
*m = MachineUnauthorized
case "machine-authorized":
*m = MachineAuthorized
case "machine-invalid":
*m = MachineInvalid
default:
var val int
if _, err := fmt.Sscanf(string(b), "machine-unknown(%d)", &val); err != nil {
*m = MachineStatus(val)
} else {
*m = MachineUnknown
}
}
return nil
}
func (m MachineStatus) String() string {
switch m {
case MachineUnknown:
return "machine-unknown"
case MachineUnauthorized:
return "machine-unauthorized"
case MachineAuthorized:
return "machine-authorized"
case MachineInvalid:
return "machine-invalid"
default:
return fmt.Sprintf("machine-unknown(%d)", int(m))
}
}
func isNum(b byte) bool {
return b >= '0' && b <= '9'
}
func isAlpha(b byte) bool {
return (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z')
}
// CheckTag validates tag for use as an ACL tag.
// For now we allow only ascii alphanumeric tags, and they need to start
// with a letter. No unicode shenanigans allowed, and we reserve punctuation
// marks other than '-' for a possible future URI scheme.
//
// Because we're ignoring unicode entirely, we can treat utf-8 as a series of
// bytes. Anything >= 128 is disqualified anyway.
//
// We might relax these rules later.
func CheckTag(tag string) error {
var ok bool
tag, ok = strings.CutPrefix(tag, "tag:")
if !ok {
return errors.New("tags must start with 'tag:'")
}
if tag == "" {
return errors.New("tag names must not be empty")
}
if !isAlpha(tag[0]) {
return errors.New("tag names must start with a letter, after 'tag:'")
}
for _, b := range []byte(tag) {
if !isNum(b) && !isAlpha(b) && b != '-' {
return errors.New("tag names can only contain numbers, letters, or dashes")
}
}
return nil
}
// CheckRequestTags checks that all of h.RequestTags are valid.
func (h *Hostinfo) CheckRequestTags() error {
if h == nil {
return nil
}
for _, tag := range h.RequestTags {
if err := CheckTag(tag); err != nil {
return fmt.Errorf("tag(%#v): %w", tag, err)
}
}
return nil
}
// ServiceProto is a service type. It's usually
// TCP ("tcp") or UDP ("udp"), but it can also have
// meta service values as defined in Service.Proto.
type ServiceProto string
const (
TCP = ServiceProto("tcp")
UDP = ServiceProto("udp")
PeerAPI4 = ServiceProto("peerapi4")
PeerAPI6 = ServiceProto("peerapi6")
PeerAPIDNS = ServiceProto("peerapi-dns-proxy")
)
// IsKnownServiceProto checks whether sp represents a known-valid value of
// ServiceProto.
func IsKnownServiceProto(sp ServiceProto) bool {
switch sp {
case TCP, UDP, PeerAPI4, PeerAPI6, PeerAPIDNS, ServiceProto("egg"):
return true
}
return false
}
// Service represents a service running on a node.
type Service struct {
_ structs.Incomparable
// Proto is the type of service. It's usually the constant TCP
// or UDP ("tcp" or "udp"), but it can also be one of the
// following meta service values:
//
// * "peerapi4": peerapi is available on IPv4; Port is the
// port number that the peerapi is running on the
// node's Tailscale IPv4 address.
// * "peerapi6": peerapi is available on IPv6; Port is the
// port number that the peerapi is running on the
// node's Tailscale IPv6 address.
// * "peerapi-dns-proxy": the local peerapi service supports
// being a DNS proxy (when the node is an exit
// node). For this service, the Port number must only be 1.
Proto ServiceProto
// Port is the port number.
//
// For Proto "peerapi-dns", it must be 1.
Port uint16
// Description is the textual description of the service,
// usually the process name that's running.
Description string `json:",omitempty"`
// TODO(apenwarr): allow advertising services on subnet IPs?
// TODO(apenwarr): add "tags" here for each service?
}
// Location represents geographical location data about a
// Tailscale host. Location is optional and only set if
// explicitly declared by a node.
type Location struct {
Country string `json:",omitempty"` // User friendly country name, with proper capitalization ("Canada")
CountryCode string `json:",omitempty"` // ISO 3166-1 alpha-2 in upper case ("CA")
City string `json:",omitempty"` // User friendly city name, with proper capitalization ("Squamish")
// CityCode is a short code representing the city in upper case.
// CityCode is used to disambiguate a city from another location
// with the same city name. It uniquely identifies a particular
// geographical location, within the tailnet.
// IATA, ICAO or ISO 3166-2 codes are recommended ("YSE")
CityCode string `json:",omitempty"`
// Latitude, Longitude are optional geographical coordinates of the node, in degrees.
// No particular accuracy level is promised; the coordinates may simply be the center of the city or country.
Latitude float64 `json:",omitempty"`
Longitude float64 `json:",omitempty"`
// Priority determines the order of use of an exit node when a
// location based preference matches more than one exit node,
// the node with the highest priority wins. Nodes of equal
// probability may be selected arbitrarily.
//
// A value of 0 means the exit node does not have a priority
// preference. A negative int is not allowed.
Priority int `json:",omitempty"`
}
// Hostinfo contains a summary of a Tailscale host.
//
// Because it contains pointers (slices), this type should not be used
// as a value type.
type Hostinfo struct {
IPNVersion string `json:",omitzero"` // version of this code (in version.Long format)
FrontendLogID string `json:",omitzero"` // logtail ID of frontend instance
BackendLogID string `json:",omitzero"` // logtail ID of backend instance
OS string `json:",omitzero"` // operating system the client runs on (a version.OS value)
// OSVersion is the version of the OS, if available.
//
// For Android, it's like "10", "11", "12", etc. For iOS and macOS it's like
// "15.6.1" or "12.4.0". For Windows it's like "10.0.19044.1889". For
// FreeBSD it's like "12.3-STABLE".
//
// For Linux, prior to Tailscale 1.32, we jammed a bunch of fields into this
// string on Linux, like "Debian 10.4; kernel=xxx; container; env=kn" and so
// on. As of Tailscale 1.32, this is simply the kernel version on Linux, like
// "5.10.0-17-amd64".
OSVersion string `json:",omitzero"`
Container opt.Bool `json:",omitzero"` // best-effort whether the client is running in a container
Env string `json:",omitzero"` // a hostinfo.EnvType in string form
Distro string `json:",omitzero"` // "debian", "ubuntu", "nixos", ...
DistroVersion string `json:",omitzero"` // "20.04", ...
DistroCodeName string `json:",omitzero"` // "jammy", "bullseye", ...
// App is used to disambiguate Tailscale clients that run using tsnet.
App string `json:",omitzero"` // "k8s-operator", "golinks", ...
Desktop opt.Bool `json:",omitzero"` // if a desktop was detected on Linux
Package string `json:",omitzero"` // Tailscale package to disambiguate ("choco", "appstore", etc; "" for unknown)
DeviceModel string `json:",omitzero"` // mobile phone model ("Pixel 3a", "iPhone12,3")
PushDeviceToken string `json:",omitzero"` // macOS/iOS APNs device token for notifications (and Android in the future)
Hostname string `json:",omitzero"` // name of the host the client runs on
ShieldsUp bool `json:",omitzero"` // indicates whether the host is blocking incoming connections
ShareeNode bool `json:",omitzero"` // indicates this node exists in netmap because it's owned by a shared-to user
NoLogsNoSupport bool `json:",omitzero"` // indicates that the user has opted out of sending logs and support
// RemoteConfig is whether the node has both linked
// feature/remoteconfig into its binary and enabled
// Prefs.RemoteConfig: it has delegated full remote management of
// its prefs and LocalAPI to the tailnet admin via the
// /remoteapi/localapi/* c2n endpoint. See feature/remoteconfig for
// the trust model.
RemoteConfig bool `json:",omitzero"`
// WireIngress indicates that the node would like to be wired up server-side
// (DNS, etc) to be able to use Tailscale Funnel, even if it's not currently
// enabled. For example, the user might only use it for intermittent
// foreground CLI serve sessions, for which they'd like it to work right
// away, even if it's disabled most of the time. As an optimization, this is
// only sent if IngressEnabled is false, as IngressEnabled implies that this
// option is true.
WireIngress bool `json:",omitzero"`
IngressEnabled bool `json:",omitzero"` // if the node has any funnel endpoint enabled
// AllowsUpdate reports that the node has opted in to
// admin-console-driven remote updates and that the running binary
// includes client update support (the feature/clientupdate package,
// which tsnet apps don't include).
AllowsUpdate bool `json:",omitzero"`
Machine string `json:",omitzero"` // the current host's machine type (uname -m)
GoArch string `json:",omitzero"` // GOARCH value (of the built binary)
GoArchVar string `json:",omitzero"` // GOARM, GOAMD64, etc (of the built binary)
GoVersion string `json:",omitzero"` // Go version binary was built with
RoutableIPs []netip.Prefix `json:",omitempty"` // set of IP ranges this client can route
RequestTags []string `json:",omitempty"` // set of ACL tags this node wants to claim
WoLMACs []string `json:",omitempty"` // MAC address(es) to send Wake-on-LAN packets to wake this node (lowercase hex w/ colons)
Services []Service `json:",omitempty"` // services advertised by this machine
NetInfo *NetInfo `json:",omitzero"`
SSH_HostKeys []string `json:"sshHostKeys,omitempty"` // if advertised
Cloud string `json:",omitzero"`
Userspace opt.Bool `json:",omitzero"` // if the client is running in userspace (netstack) mode
UserspaceRouter opt.Bool `json:",omitzero"` // if the client's subnet router is running in userspace (netstack) mode
AppConnector opt.Bool `json:",omitzero"` // if the client is running the app-connector service
ServicesHash string `json:",omitzero"` // opaque hash of the most recent list of tailnet services, change in hash indicates config should be fetched via c2n
PeerRelay bool `json:",omitzero"` // if the client is willing to relay traffic for other peers
ExitNodeID StableNodeID `json:",omitzero"` // the client’s selected exit node, empty when unselected.
// Location represents geographical location data about a
// Tailscale host. Location is optional and only set if
// explicitly declared by a node.
Location *Location `json:",omitzero"`
TPM *TPMInfo `json:",omitzero"` // TPM device metadata, if available
// StateEncrypted reports whether the node state is stored encrypted on
// disk. The actual mechanism is platform-specific:
// * Apple nodes use the Keychain
// * Linux and Windows nodes use the TPM
// * Android apps use EncryptedSharedPreferences
StateEncrypted opt.Bool `json:",omitzero"`
// NOTE: any new fields containing pointers in this type
// require changes to Hostinfo.Equal.
}
// TPMInfo contains information about a TPM 2.0 device present on a node.
// All fields are read from TPM_CAP_TPM_PROPERTIES, see Part 2, section 6.13 of
// https://trustedcomputinggroup.org/resource/tpm-library-specification/.
type TPMInfo struct {
// Manufacturer is a 4-letter code from section 4.1 of
// https://trustedcomputinggroup.org/resource/vendor-id-registry/,
// for example "MSFT" for Microsoft.
// Read from TPM_PT_MANUFACTURER.
Manufacturer string `json:",omitzero"`
// Vendor is a vendor ID string, up to 16 characters.
// Read from TPM_PT_VENDOR_STRING_*.
Vendor string `json:",omitzero"`
// Model is a vendor-defined TPM model.
// Read from TPM_PT_VENDOR_TPM_TYPE.
Model int `json:",omitzero"`
// FirmwareVersion is the version number of the firmware.
// Read from TPM_PT_FIRMWARE_VERSION_*.
FirmwareVersion uint64 `json:",omitzero"`
// SpecRevision is the TPM 2.0 spec revision encoded as a single number. All
// revisions can be found at
// https://trustedcomputinggroup.org/resource/tpm-library-specification/.
// Before revision 184, TCG used the "01.83" format for revision 183.
SpecRevision int `json:",omitzero"`
// FamilyIndicator is the TPM spec family, like "2.0".
// Read from TPM_PT_FAMILY_INDICATOR.
FamilyIndicator string `json:",omitzero"`
}
// Present reports whether a TPM device is present on this machine.
func (t *TPMInfo) Present() bool { return t != nil }
// ServiceName is the name of a service, of the form `svc:dns-label`. Services
// represent some kind of application provided for users of the tailnet with a
// MagicDNS name and possibly dedicated IP addresses. Currently (2024-01-21),
// the only type of service is [VIPService].
// This is not related to the older [Service] used in [Hostinfo.Services].
type ServiceName string
// AsServiceName reports whether the given string is a valid service name.
// If so returns the name as a [tailcfg.ServiceName], otherwise returns "".
func AsServiceName(s string) ServiceName {
svcName := ServiceName(s)
if err := svcName.Validate(); err != nil {
return ""
}
return svcName
}
// Validate validates if the service name is formatted correctly.
// We only allow valid DNS labels, since the expectation is that these will be
// used as parts of domain names. All errors are [vizerror.Error].
func (sn ServiceName) Validate() error {
bareName, ok := strings.CutPrefix(string(sn), "svc:")
if !ok {
return vizerror.Errorf("%q is not a valid service name: must start with 'svc:'", sn)
}
if bareName == "" {
return vizerror.Errorf("%q is not a valid service name: must not be empty after the 'svc:' prefix", sn)
}
return dnsname.ValidLabel(bareName)
}
// String implements [fmt.Stringer].
func (sn ServiceName) String() string {
return string(sn)
}
// WithoutPrefix is the name of the service without the `svc:` prefix, used for
// DNS names. If the name does not include the prefix (which means
// [ServiceName.Validate] would return an error) then it returns "".
func (sn ServiceName) WithoutPrefix() string {
bareName, ok := strings.CutPrefix(string(sn), "svc:")
if !ok {
return ""
}
return bareName
}
// VIPService represents a service created on a tailnet from the
// perspective of a node providing that service. These services
// have an virtual IP (VIP) address pair distinct from the node's IPs.
type VIPService struct {
// Name is the name of the service. The Name uniquely identifies a service
// on a particular tailnet, and so also corresponds uniquely to the pair of
// IP addresses belonging to the VIP service.
Name ServiceName
// Ports specify which ProtoPorts are made available by this node
// on the service's IPs.
Ports []ProtoPortRange
// Active specifies whether new requests for the service should be
// sent to this node by control.
Active bool
}
// TailscaleSSHEnabled reports whether or not this node is acting as a
// Tailscale SSH server.
func (hi *Hostinfo) TailscaleSSHEnabled() bool {
// Currently, we use `SSH_HostKeys` as a proxy for this. However, we may later
// include non-Tailscale host keys, and will add a separate flag to rely on.
return hi != nil && len(hi.SSH_HostKeys) > 0
}
func (v HostinfoView) TailscaleSSHEnabled() bool { return v.ж.TailscaleSSHEnabled() }
// NetInfo contains information about the host's network state.
type NetInfo struct {
// MappingVariesByDestIP says whether the host's NAT mappings
// vary based on the destination IP.
MappingVariesByDestIP opt.Bool `json:",omitzero"`
// WorkingIPv6 is whether the host has IPv6 internet connectivity.
WorkingIPv6 opt.Bool `json:",omitzero"`
// OSHasIPv6 is whether the OS supports IPv6 at all, regardless of
// whether IPv6 internet connectivity is available.
OSHasIPv6 opt.Bool `json:",omitzero"`
// WorkingUDP is whether the host has UDP internet connectivity.
WorkingUDP opt.Bool `json:",omitzero"`
// WorkingICMPv4 is whether ICMPv4 works.
// Empty means not checked.
WorkingICMPv4 opt.Bool `json:",omitzero"`
// HavePortMap is whether we have an existing portmap open
// (UPnP, PMP, or PCP).
HavePortMap bool `json:",omitzero"`
// UPnP is whether UPnP appears present on the LAN.
// Empty means not checked.
UPnP opt.Bool `json:",omitzero"`
// PMP is whether NAT-PMP appears present on the LAN.
// Empty means not checked.
PMP opt.Bool `json:",omitzero"`
// PCP is whether PCP appears present on the LAN.
// Empty means not checked.
PCP opt.Bool `json:",omitzero"`
// PreferredDERP is this node's preferred (home) DERP region ID.
// This is where the node expects to be contacted to begin a
// peer-to-peer connection. The node might be temporarily
// connected to multiple DERP servers (to speak to other nodes
// that are located elsewhere) but PreferredDERP is the region ID
// that the node subscribes to traffic at.
// Zero means disconnected or unknown.
PreferredDERP DERPRegionID `json:",omitzero"`
// LinkType is the current link type, if known.
LinkType string `json:",omitzero"` // "wired", "wifi", "mobile" (LTE, 4G, 3G, etc)
// DERPLatency is the fastest recent time to reach various
// DERP STUN servers, in seconds. The map key is the
// "regionID-v4" or "-v6"; it was previously the DERP server's
// STUN host:port.
//
// This should only be updated rarely, or when there's a
// material change, as any change here also gets uploaded to
// the control plane.
DERPLatency map[string]float64 `json:",omitempty"`
// FirewallMode encodes both which firewall mode was selected and why.
// It is Linux-specific (at least as of 2023-08-19) and is meant to help
// debug iptables-vs-nftables issues. The string is of the form
// "{nft,ift}-REASON", like "nft-forced" or "ipt-default". Empty means
// either not Linux or a configuration in which the host firewall rules
// are not managed by tailscaled.
FirewallMode string `json:",omitzero"`
// Update BasicallyEqual when adding fields.
}
func (ni *NetInfo) String() string {
if ni == nil {
return "NetInfo(nil)"
}
return fmt.Sprintf("NetInfo{varies=%v ipv6=%v ipv6os=%v udp=%v icmpv4=%v derp=#%v portmap=%v link=%q firewallmode=%q}",
ni.MappingVariesByDestIP, ni.WorkingIPv6,
ni.OSHasIPv6, ni.WorkingUDP, ni.WorkingICMPv4,
ni.PreferredDERP, ni.portMapSummary(), ni.LinkType, ni.FirewallMode)
}
func (ni *NetInfo) portMapSummary() string {
if !buildfeatures.HasPortMapper {
return "x"
}
if !ni.HavePortMap && ni.UPnP == "" && ni.PMP == "" && ni.PCP == "" {
return "?"
}
var prefix string
if ni.HavePortMap {
prefix = "active-"
}
return prefix + conciseOptBool(ni.UPnP, "U") + conciseOptBool(ni.PMP, "M") + conciseOptBool(ni.PCP, "C")
}
func conciseOptBool(b opt.Bool, trueVal string) string {
if b == "" {
return "_"
}
v, ok := b.Get()
if !ok {
return "x"
}
if v {
return trueVal
}
return ""
}
// BasicallyEqual reports whether ni and ni2 are basically equal, ignoring
// changes in DERP ServerLatency & RegionLatency.
func (ni *NetInfo) BasicallyEqual(ni2 *NetInfo) bool {
if (ni == nil) != (ni2 == nil) {
return false
}
if ni == nil {
return true
}
return ni.MappingVariesByDestIP == ni2.MappingVariesByDestIP &&
ni.WorkingIPv6 == ni2.WorkingIPv6 &&
ni.OSHasIPv6 == ni2.OSHasIPv6 &&
ni.WorkingUDP == ni2.WorkingUDP &&
ni.WorkingICMPv4 == ni2.WorkingICMPv4 &&
ni.HavePortMap == ni2.HavePortMap &&
ni.UPnP == ni2.UPnP &&
ni.PMP == ni2.PMP &&
ni.PCP == ni2.PCP &&
ni.PreferredDERP == ni2.PreferredDERP &&
ni.LinkType == ni2.LinkType &&
ni.FirewallMode == ni2.FirewallMode
}
// Equal reports whether h and h2 are equal.
func (h *Hostinfo) Equal(h2 *Hostinfo) bool {
if h == nil && h2 == nil {
return true
}
if (h == nil) != (h2 == nil) {
return false
}
return reflect.DeepEqual(h, h2)
}
// SignatureType specifies a scheme for signing RegisterRequest messages. It
// specifies the crypto algorithms to use, the contents of what is signed, and
// any other relevant details. Historically, requests were unsigned so the zero
// value is SignatureNone.
type SignatureType int
const (
// SignatureNone indicates that there is no signature, no Timestamp is
// required (but may be specified if desired), and both DeviceCert and
// Signature should be empty.
SignatureNone = SignatureType(iota)
// SignatureUnknown represents an unknown signature scheme, which should
// be considered an error if seen.
SignatureUnknown
// SignatureV1 is computed as RSA-PSS-Sign(privateKeyForDeviceCert,
// SHA256(Timestamp || ServerIdentity || DeviceCert || ServerShortPubKey ||
// MachineShortPubKey)). The PSS salt length is equal to hash length
// (rsa.PSSSaltLengthEqualsHash). Device cert is required.
// Deprecated: uses old key serialization format.
SignatureV1
// SignatureV2 is computed as RSA-PSS-Sign(privateKeyForDeviceCert,
// SHA256(Timestamp || ServerIdentity || DeviceCert || ServerPubKey ||
// MachinePubKey)). The PSS salt length is equal to hash length
// (rsa.PSSSaltLengthEqualsHash). Device cert is required.
SignatureV2
)
func (st SignatureType) AppendText(b []byte) ([]byte, error) {
return append(b, st.String()...), nil
}
func (st SignatureType) MarshalText() ([]byte, error) {
return []byte(st.String()), nil
}
func (st *SignatureType) UnmarshalText(b []byte) error {
switch string(b) {
case "signature-none":
*st = SignatureNone
case "signature-v1":
*st = SignatureV1
case "signature-v2":
*st = SignatureV2
default:
var val int
if _, err := fmt.Sscanf(string(b), "signature-unknown(%d)", &val); err != nil {
*st = SignatureType(val)
} else {
*st = SignatureUnknown
}
}
return nil
}
func (st SignatureType) String() string {
switch st {
case SignatureNone:
return "signature-none"
case SignatureUnknown:
return "signature-unknown"
case SignatureV1:
return "signature-v1"
case SignatureV2:
return "signature-v2"
default:
return fmt.Sprintf("signature-unknown(%d)", int(st))
}
}
// RegisterResponseAuth is the authentication information returned by the server
// in response to a RegisterRequest.
type RegisterResponseAuth struct {
_ structs.Incomparable
// At most one of Oauth2Token or AuthKey is set.
Oauth2Token *Oauth2Token `json:",omitempty"` // used by pre-1.66 Android only
AuthKey string `json:",omitempty"`
}
// RegisterRequest is a request to register a key for a node.
//
// This is JSON-encoded and sent over the control plane connection to:
//
// POST https://<control-plane>/machine/register.
type RegisterRequest struct {
_ structs.Incomparable
// Version is the client's capabilities when using the Noise
// transport.
//
// When using the original nacl crypto_box transport, the
// value must be 1.
Version CapabilityVersion
NodeKey key.NodePublic
OldNodeKey key.NodePublic
NLKey key.NLPublic
Auth *RegisterResponseAuth `json:",omitempty"`
// Expiry optionally specifies the requested key expiry.
// The server policy may override.
// As a special case, if Expiry is in the past and NodeKey is
// the node's current key, the key is expired.
Expiry time.Time
Followup string // response waits until AuthURL is visited
Hostinfo *Hostinfo
// Ephemeral is whether the client is requesting that this
// node be considered ephemeral and be automatically deleted
// when it stops being active.
Ephemeral bool `json:",omitempty"`
// NodeKeySignature is the node's own node-key signature, re-signed
// for its new node key using its tailnet-lock key.
//
// This field is set when the client retries registration after learning
// its NodeKeySignature (which is in need of rotation).
NodeKeySignature tkatype.MarshaledSignature
// The following fields are not used for SignatureNone and are required for
// SignatureV1:
SignatureType SignatureType `json:",omitempty"`
Timestamp *time.Time `json:",omitempty"` // creation time of request to prevent replay
DeviceCert []byte `json:",omitempty"` // X.509 certificate for client device
Signature []byte `json:",omitempty"` // as described by SignatureType
// Tailnet is an optional identifier specifying the name of the recommended or required
// network that the node should join. Its exact form should not be depended on; new
// forms are coming later. The identifier is generally a domain name (for an organization)
// or e-mail address (for a personal account on a shared e-mail provider). It is the same name
// used by the API, as described in /api.md#tailnet.
// If Tailnet begins with the prefix "required:" then the server should prevent logging in to a different
// network than the one specified. Otherwise, the server should recommend the specified network
// but still permit logging in to other networks.
// If empty, no recommendation is offered to the server and the login page should show all options.
Tailnet string `json:",omitempty"`
}
// RegisterResponse is returned by the server in response to a RegisterRequest.
type RegisterResponse struct {
User User
Login Login
NodeKeyExpired bool // if true, the NodeKey needs to be replaced
MachineAuthorized bool // TODO(crawshaw): move to using MachineStatus
AuthURL string // if set, authorization pending
// If set, this is the current node-key signature that needs to be
// re-signed for the node's new node-key.
NodeKeySignature tkatype.MarshaledSignature
// Error indicates that authorization failed. If this is non-empty,
// other status fields should be ignored.
Error string
}
// EndpointType distinguishes different sources of MapRequest.Endpoint values.
type EndpointType int
const (
EndpointUnknownType = EndpointType(0)
EndpointLocal = EndpointType(1)
EndpointSTUN = EndpointType(2)
EndpointPortmapped = EndpointType(3)
EndpointSTUN4LocalPort = EndpointType(4) // hard NAT: STUN'ed IPv4 address + local fixed port
EndpointExplicitConf = EndpointType(5) // explicitly configured (routing to be done by client)
)
func (et EndpointType) String() string {
switch et {
case EndpointUnknownType:
return "?"
case EndpointLocal:
return "local"
case EndpointSTUN:
return "stun"
case EndpointPortmapped:
return "portmap"
case EndpointSTUN4LocalPort:
return "stun4localport"
case EndpointExplicitConf:
return "explicitconf"
}
return "other"
}
// Endpoint is an endpoint IPPort and an associated type.
// It doesn't currently go over the wire as is but is instead
// broken up into two parallel slices in MapRequest, for compatibility
// reasons. But this type is used in the codebase.
type Endpoint struct {
Addr netip.AddrPort
Type EndpointType
}
// MapRequest is sent by a client to either update the control plane
// about its current state, or to start a long-poll of network map updates.
//
// The request includes a copy of the client's current set of WireGuard
// endpoints and general host information.
//
// This is JSON-encoded and sent over the control plane connection to:
//
// POST https://<control-plane>/machine/map
type MapRequest struct {
// Version is incremented whenever the client code changes enough that
// we want to signal to the control server that we're capable of something
// different.
//
// For current values and history, see the CapabilityVersion type's docs.
Version CapabilityVersion
Compress string `json:",omitzero"` // "zstd" or "" (no compression)
KeepAlive bool `json:",omitzero"` // whether server should send keep-alives back to us
NodeKey key.NodePublic
DiscoKey key.DiscoPublic
// HardwareAttestationKey is the public key of the node's hardware-backed
// identity attestation key, if any.
HardwareAttestationKey key.HardwareAttestationPublic `json:",omitzero"`
// HardwareAttestationKeySignature is the signature of
// "$UNIX_TIMESTAMP|$NODE_KEY" using its hardware attestation key, if any.
HardwareAttestationKeySignature []byte `json:",omitempty"`
// HardwareAttestationKeySignatureTimestamp is the time at which the
// HardwareAttestationKeySignature was created, if any. This UNIX timestamp
// value is prepended to the node key when signing.
HardwareAttestationKeySignatureTimestamp time.Time `json:",omitzero"`
// Stream is whether the client wants to receive multiple MapResponses over
// the same HTTP connection.
//
// If false, the server will send a single MapResponse and then close the
// connection.
//
// If true and Version >= 68, the server should treat this as a read-only
// request and ignore any Hostinfo or other fields that might be set.
Stream bool `json:",omitzero"`
// Hostinfo is the client's current Hostinfo. Although it is always included
// in the request, the server may choose to ignore it when Stream is true
// and Version >= 68.
Hostinfo *Hostinfo
// MapSessionHandle, if non-empty, is a request to reattach to a previous
// map session after a previous map session was interrupted for whatever
// reason. Its value is an opaque string as returned by
// MapResponse.MapSessionHandle.
//
// When set, the client must also send MapSessionSeq to specify the last
// processed message in that prior session.
//
// The server may choose to ignore the request for any reason and start a
// new map session. This is only applicable when Stream is true.
MapSessionHandle string `json:",omitzero"`
// MapSessionSeq is the sequence number in the map session identified by
// MapSesssionHandle that was most recently processed by the client.
// It is only applicable when MapSessionHandle is specified.
// If the server chooses to honor the MapSessionHandle request, only sequence
// numbers greater than this value will be returned.
MapSessionSeq int64 `json:",omitzero"`
// Endpoints are the client's magicsock UDP ip:port endpoints (IPv4 or IPv6).
// These can be ignored if Stream is true and Version >= 68.
Endpoints []netip.AddrPort `json:",omitempty"`
// EndpointTypes are the types of the corresponding endpoints in Endpoints.
EndpointTypes []EndpointType `json:",omitempty"`
// TKAHead describes the hash of the latest AUM applied to the local
// tailnet key authority, if one is operating.
// It is encoded as tka.AUMHash.MarshalText.
TKAHead string `json:",omitzero"`
// ReadOnly was set when client just wanted to fetch the MapResponse,
// without updating their Endpoints. The intended use was for clients to
// discover the DERP map at start-up before their first real endpoint
// update.
//
// Deprecated: always false as of Version 68.
ReadOnly bool `json:",omitzero"`
// OmitPeers is whether the client is okay with the Peers list being omitted
// in the response.
//
// The behavior of OmitPeers being true varies based on Stream and ReadOnly:
//
// If OmitPeers is true, Stream is false, and ReadOnly is false,
// then the server will let clients update their endpoints without
// breaking existing long-polling (Stream == true) connections.
// In this case, the server can omit the entire response; the client
// only checks the HTTP response status code.
//
// If OmitPeers is true, Stream is false, but ReadOnly is true,
// then all the response fields are included. (This is what the client does
// when initially fetching the DERP map.)
OmitPeers bool `json:",omitzero"`
// DebugFlags is a list of strings specifying debugging and
// development features to enable in handling this map
// request. The values are deliberately unspecified, as they get
// added and removed all the time during development, and offer no
// compatibility promise. To roll out semantic changes, bump
// Version instead.
//
// Current DebugFlags values are:
// * "warn-ip-forwarding-off": client is trying to be a subnet
// router but their IP forwarding is broken.
// * "warn-router-unhealthy": client's Router implementation is
// having problems.
DebugFlags []string `json:",omitempty"`
// ConnectionHandleForTest, if non-empty, is an opaque string sent by the client that
// identifies this specific connection to the server. The server may choose to
// use this handle to identify the connection for debugging or testing
// purposes. It has no semantic meaning.
ConnectionHandleForTest string `json:",omitzero"`
}
// PortRange represents a range of UDP or TCP port numbers.
type PortRange struct {
First uint16
Last uint16
}
// Contains reports whether port is in pr.
func (pr PortRange) Contains(port uint16) bool {
return port >= pr.First && port <= pr.Last
}
var PortRangeAny = PortRange{0, 65535}
func (pr PortRange) String() string {
if pr.First == pr.Last {
return strconv.FormatUint(uint64(pr.First), 10)
} else if pr == PortRangeAny {
return "*"
}
return fmt.Sprintf("%d-%d", pr.First, pr.Last)
}
// NetPortRange represents a range of ports that's allowed for one or more IPs.
type NetPortRange struct {
_ structs.Incomparable
IP string // IP, CIDR, Range, or "*" (same formats as FilterRule.SrcIPs)
Bits *int `json:",omitempty"` // deprecated; the 2020 way to turn IP into a CIDR. See FilterRule.SrcBits.
Ports PortRange
}
// CapGrant grants capabilities in a FilterRule.
type CapGrant struct {
// Dsts are the destination IP ranges that this capability
// grant matches.
Dsts []netip.Prefix
// Caps are the capabilities the source IP matched by
// FilterRule.SrcIPs are granted to the destination IP,
// matched by Dsts.
// Deprecated: use CapMap instead.
Caps []peercap.Cap `json:",omitempty"`
// CapMap is a map of capabilities to their values.
// The key is the capability name, and the value is a list of
// values for that capability.
CapMap PeerCapMap `json:",omitempty"`
}
// PeerCapability is a type alias to [peercap.Cap]
// and peer capabilities are now defined in the peercap package,
// see [PeerCapabilityFileSharingTarget].
//
//go:fix inline
type PeerCapability = peercap.Cap
// Deprecated: Peer capabilities are now defined in the peercap package, see [peercap.Cap].
// These constants are provided for backwards compatibility
// but no new [PeerCapability] aliases will be added to this list.
//
//go:fix inline
const (
PeerCapabilityFileSharingTarget = peercap.FileSharingTarget
PeerCapabilityFileSharingSend = peercap.FileSharingSend
PeerCapabilityDebugPeer = peercap.DebugPeer
PeerCapabilityWakeOnLAN = peercap.WakeOnLAN
PeerCapabilityIngress = peercap.Ingress
PeerCapabilityWebUI = peercap.WebUI
PeerCapabilityTaildrive = peercap.Taildrive
PeerCapabilityTaildriveSharer = peercap.TaildriveSharer
PeerCapabilityKubernetes = peercap.Kubernetes
PeerCapabilityRelay = peercap.Relay
PeerCapabilityRelayTarget = peercap.RelayTarget
PeerCapabilityTsIDP = peercap.TsIDP
// Deprecated: Do not add any further values here, use [peercap] instead.
)
// NodeCapMap is a map of capabilities to their optional values. It is valid for
// a capability to have no values (nil slice); such capabilities can be tested
// for by using the [NodeCapMap.Contains] method.
//
// See [nodecap.Cap] for more information on keys.
type NodeCapMap map[nodecap.Cap][]RawMessage
// Equal reports whether c and c2 are equal.
func (c NodeCapMap) Equal(c2 NodeCapMap) bool {
return maps.EqualFunc(c, c2, slices.Equal)
}
// UnmarshalNodeCapJSON unmarshals each JSON value in cm[cap] as T.
// If cap does not exist in cm, it returns (nil, nil).
// It returns an error if the values cannot be unmarshaled into the provided type.
func UnmarshalNodeCapJSON[T any](cm NodeCapMap, cap nodecap.Cap) ([]T, error) {
return UnmarshalNodeCapViewJSON[T](views.MapSliceOf(cm), cap)
}
// UnmarshalNodeCapViewJSON unmarshals each JSON value in cm.Get(cap) as T.
// If cap does not exist in cm, it returns (nil, nil).
// It returns an error if the values cannot be unmarshaled into the provided type.
func UnmarshalNodeCapViewJSON[T any](cm views.MapSlice[nodecap.Cap, RawMessage], cap nodecap.Cap) ([]T, error) {
vals, ok := cm.GetOk(cap)
if !ok {
return nil, nil
}
out := make([]T, 0, vals.Len())
for _, v := range vals.All() {
var t T
if err := json.Unmarshal([]byte(v), &t); err != nil {
return nil, err
}
out = append(out, t)
}
return out, nil
}
// Contains reports whether c has the capability cap. This is used to test for
// the existence of a capability, especially when the capability has no
// associated argument/data values.
func (c NodeCapMap) Contains(cap nodecap.Cap) bool {
_, ok := c[cap]
return ok
}
// PeerCapMap is a map of capabilities to their optional values. It is valid for
// a capability to have no values (nil slice); such capabilities can be tested
// for by using the HasCapability method.
//
// The values are opaque to Tailscale, but are passed through from the ACLs to
// the application via the WhoIs API.
type PeerCapMap map[peercap.Cap][]RawMessage
// UnmarshalCapJSON unmarshals each JSON value in cm[cap] as T.
// If cap does not exist in cm, it returns (nil, nil).
// It returns an error if the values cannot be unmarshaled into the provided type.
func UnmarshalCapJSON[T any](cm PeerCapMap, cap peercap.Cap) ([]T, error) {
return UnmarshalCapViewJSON[T](views.MapSliceOf(cm), cap)
}
// UnmarshalCapViewJSON unmarshals each JSON value in cm.Get(cap) as T.
// If cap does not exist in cm, it returns (nil, nil).
// It returns an error if the values cannot be unmarshaled into the provided type.
func UnmarshalCapViewJSON[T any](cm views.MapSlice[peercap.Cap, RawMessage], cap peercap.Cap) ([]T, error) {
vals, ok := cm.GetOk(cap)
if !ok {
return nil, nil
}
out := make([]T, 0, vals.Len())
for _, v := range vals.All() {
var t T
if err := json.Unmarshal([]byte(v), &t); err != nil {
return nil, err
}
out = append(out, t)
}
return out, nil
}
// HasCapability reports whether c has the capability cap. This is used to test
// for the existence of a capability, especially when the capability has no
// associated argument/data values.
func (c PeerCapMap) HasCapability(cap peercap.Cap) bool {
_, ok := c[cap]
return ok
}
// FilterRule represents one rule in a packet filter.
//
// A rule is logically a set of source CIDRs to match (described by
// SrcIPs), and a set of destination targets that are then
// allowed if a source IP is matches of those CIDRs.
type FilterRule struct {
// SrcIPs are the source IPs/networks to match.
//
// It may take the following forms:
// * 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")
// * a string "cap:<capability>" with NodeCapMap cap name
SrcIPs []string
// SrcBits is deprecated; it was the old way to specify a CIDR
// prior to CapabilityVersion 7. Its values correspond to the
// SrcIPs above.
//
// If an entry of SrcBits is present for the same index as a
// SrcIPs entry, it changes the SrcIP above to be a network
// with /n CIDR bits. If the slice is nil or insufficiently
// long, the default value (for an IPv4 address) for a
// position is 32, as if the SrcIPs above were a /32 mask. For
// a "*" SrcIPs value, the corresponding SrcBits value is
// ignored.
//
// This is still present in this file because the Tailscale control plane
// code still uses this type, for 118 clients that are still connected as of
// 2024-06-18, 3.5 years after the last release that used this type.
SrcBits []int `json:",omitempty"`
// DstPorts are the port ranges to allow once a source IP
// matches (is in the CIDR described by SrcIPs).
//
// CapGrant and DstPorts are mutually exclusive: at most one can be non-nil.
DstPorts []NetPortRange `json:",omitempty"`
// IPProto are the IP protocol numbers to match.
//
// As a special case, nil or empty means TCP, UDP, and ICMP.
//
// Numbers outside the uint8 range (below 0 or above 255) are
// reserved for Tailscale's use. Unknown ones are ignored.
//
// Depending on the IPProto values, DstPorts may or may not be
// used.
IPProto []int `json:",omitempty"`
// CapGrant, if non-empty, are the capabilities to
// conditionally grant to the source IP in SrcIPs.
//
// Think of DstPorts as "capabilities for networking" and
// CapGrant as arbitrary application-defined capabilities
// defined between the admin's ACLs and the application
// doing WhoIs lookups, looking up the remote IP address's
// application-level capabilities.
//
// CapGrant and DstPorts are mutually exclusive: at most one can be non-nil.
CapGrant []CapGrant `json:",omitempty"`
}
var FilterAllowAll = []FilterRule{
{
SrcIPs: []string{"*"},
DstPorts: []NetPortRange{{
IP: "*",
Ports: PortRange{0, 65535},
}},
},
}
// DNSConfig is the DNS configuration.
type DNSConfig struct {
// Resolvers are the DNS resolvers to use, in order of preference.
Resolvers []*dnstype.Resolver `json:",omitempty"`
// Routes maps DNS name suffixes to a set of DNS resolvers to
// use. It is used to implement "split DNS" and other advanced DNS
// routing overlays.
//
// Map keys are fully-qualified DNS name suffixes; they may
// optionally contain a trailing dot but no leading dot.
//
// If the value is an empty slice, that means the suffix should still
// be handled by Tailscale's built-in resolver (100.100.100.100), such
// as for the purpose of handling ExtraRecords.
Routes map[string][]*dnstype.Resolver `json:",omitempty"`
// FallbackResolvers is like Resolvers, but is only used if a
// split DNS configuration is requested in a configuration that
// doesn't work yet without explicit default resolvers.
// https://github.com/tailscale/tailscale/issues/1743
FallbackResolvers []*dnstype.Resolver `json:",omitempty"`
// Domains are the search domains to use.
// Search domains must be FQDNs, but *without* the trailing dot.
Domains []string `json:",omitempty"`
// Proxied turns on automatic resolution of hostnames for devices
// in the network map, aka MagicDNS.
// Despite the (legacy) name, does not necessarily cause request
// proxying to be enabled.
Proxied bool `json:",omitzero"`
// Nameservers are the IP addresses of the global nameservers to use.
//
// Deprecated: this is only set and used by MapRequest.Version >=9 and <14. Use Resolvers instead.
Nameservers []netip.Addr `json:",omitempty"`
// CertDomains are the set of DNS names for which the control
// plane server will assist with provisioning TLS
// certificates. See SetDNSRequest, which can be used to
// answer dns-01 ACME challenges for e.g. LetsEncrypt.
// These names are FQDNs without trailing periods, and without
// any "_acme-challenge." prefix.
CertDomains []string `json:",omitempty"`
// ExtraRecords contains extra DNS records to add to the
// MagicDNS config.
ExtraRecords []DNSRecord `json:",omitempty"`
// ExitNodeFilteredSuffixes are the DNS suffixes that the
// node, when being an exit node DNS proxy, should not answer.
//
// The entries do not contain trailing periods and are always
// all lowercase.
//
// If an entry starts with a period, it's a suffix match (but
// suffix ".a.b" doesn't match "a.b"; a prefix is required).
//
// If an entry does not start with a period, it's an exact
// match.
//
// Matches are case insensitive.
ExitNodeFilteredSet []string `json:",omitempty"`
// TempCorpIssue13969 is a temporary (2023-08-16) field for an internal hack day prototype.
// It contains a user inputed URL that should have a list of domains to be blocked.
// See https://github.com/tailscale/corp/issues/13969.
TempCorpIssue13969 string `json:",omitzero"`
}
// DNSRecord is an extra DNS record to add to MagicDNS.
type DNSRecord struct {
// Name is the fully qualified domain name of
// the record to add. The trailing dot is optional.
Name string
// Type is the DNS record type.
// Empty means A or AAAA, depending on value.
// Other values are currently ignored.
Type string `json:",omitzero"`
// Value is the IP address in string form.
// TODO(bradfitz): if we ever add support for record types
// with non-UTF8 binary data, add ValueBytes []byte that
// would take precedence.
Value string
}
// PingType is a string representing the kind of ping to perform.
type PingType string
const (
// PingDisco performs a ping, without involving IP at either end.
PingDisco PingType = "disco"
// PingTSMP performs a ping, using the IP layer, but avoiding the OS IP stack.
PingTSMP PingType = "TSMP"
// PingICMP performs a ping between two tailscale nodes using ICMP that is
// received by the target systems IP stack.
PingICMP PingType = "ICMP"
// PingPeerAPI performs a ping between two tailscale nodes using ICMP that is
// received by the target systems IP stack.
PingPeerAPI PingType = "peerapi"
)
// PingRequest is a request from the control plane to the local node to probe
// something.
//
// A PingRequest with no IP and Types is a request from the control plane to the
// local node to send an HTTP request to a URL to prove the long-polling client
// is still connected.
//
// A PingRequest with Types and IP, will send a ping to the IP and send a POST
// request containing a PingResponse to the URL containing results.
type PingRequest struct {
// URL is the URL to reply to the PingRequest to.
// It will be a unique URL each time. No auth headers are necessary.
// If the client sees multiple PingRequests with the same URL,
// subsequent ones should be ignored.
//
// The HTTP method that the node should make back to URL depends on the other
// fields of the PingRequest. If Types is defined, then URL is the URL to
// send a POST request to. Otherwise, the node should just make a HEAD
// request to URL.
URL string
// URLIsNoise, if true, means that the client should hit URL over the Noise
// transport instead of TLS.
URLIsNoise bool `json:",omitzero"`
// Log is whether to log about this ping in the success case.
// For failure cases, the client will log regardless.
Log bool `json:",omitzero"`
// Types is the types of ping that are initiated. Can be any PingType, comma
// separated, e.g. "disco,TSMP"
//
// As a special case, if Types is "c2n", then this PingRequest is a
// client-to-node HTTP request. The HTTP request should be handled by this
// node's c2n handler and the HTTP response sent in a POST to URL. For c2n,
// the value of URLIsNoise is ignored and only the Noise transport (back to
// the control plane) will be used, as if URLIsNoise were true.
Types string `json:",omitzero"`
// IP is the ping target, when needed by the PingType(s) given in Types.
IP netip.Addr `json:",omitzero"`
// Payload is the ping payload.
//
// It is only used for c2n requests, in which case it's an HTTP/1.0 or
// HTTP/1.1-formatted HTTP request as parsable with http.ReadRequest.
Payload []byte `json:",omitempty"`
}
// PingResponse provides result information for a TSMP or Disco PingRequest.
// Typically populated from an ipnstate.PingResult used in `tailscale ping`.
type PingResponse struct {
Type PingType // ping type, such as TSMP or disco.
IP string `json:",omitempty"` // ping destination
NodeIP string `json:",omitempty"` // Tailscale IP of node handling IP (different for subnet routers)
NodeName string `json:",omitempty"` // DNS name base or (possibly not unique) hostname
// Err contains a short description of error conditions if the PingRequest
// could not be fulfilled for some reason.
// e.g. "100.1.2.3 is local Tailscale IP"
Err string `json:",omitempty"`
// LatencySeconds reports measurement of the round-trip time of a message to
// the requested target, if it could be determined. If LatencySeconds is
// omitted, Err should contain information as to the cause.
LatencySeconds float64 `json:",omitempty"`
// Endpoint is a string of the form "{ip}:{port}" if direct UDP was used. It
// is not currently set for TSMP.
Endpoint string `json:",omitempty"`
// PeerRelay is a string of the form "{ip}:{port}:vni:{vni}" if a peer
// relay was used. It is not currently set for TSMP.
PeerRelay string `json:",omitempty"`
// DERPRegionID is non-zero DERP region ID if DERP was used.
// It is not currently set for TSMP pings.
DERPRegionID DERPRegionID `json:",omitempty"`
// DERPRegionCode is the three-letter region code
// corresponding to DERPRegionID.
// It is not currently set for TSMP pings.
DERPRegionCode string `json:",omitempty"`
// PeerAPIPort is set by TSMP ping responses for peers that
// are running a peerapi server. This is the port they're
// running the server on.
PeerAPIPort uint16 `json:",omitempty"`
// IsLocalIP is whether the ping request error is due to it being
// a ping to the local node.
IsLocalIP bool `json:",omitempty"`
}
// MapResponse is the response to a MapRequest. It describes the state of the
// local node, the peer nodes, the DNS configuration, the packet filter, and
// more. A MapRequest, depending on its parameters, may result in the control
// plane coordination server sending 0, 1 or a stream of multiple MapResponse
// values.
//
// When the client sets MapRequest.Stream, the server sends a stream of
// MapResponses. That long-lived HTTP transaction is called a "map poll". In a
// map poll, the first MapResponse will be complete and subsequent MapResponses
// will be incremental updates with only changed information.
//
// The zero value for all fields means "unchanged". Unfortunately, several
// fields were defined before that convention was established, so they use a
// slice with omitempty, meaning this type can't be used to marshal JSON
// containing non-nil zero-length slices (meaning explicitly now empty). The
// control plane uses a separate type to marshal these fields. This type is
// primarily used for unmarshaling responses so the omitempty annotations are
// mostly useless, except that this type is also used for the integration test's
// fake control server. (It's not necessary to marshal a non-nil zero-length
// slice for the things we've needed to test in the integration tests as of
// 2023-09-09).
type MapResponse struct {
// MapSessionHandle optionally specifies a unique opaque handle for this
// stateful MapResponse session. Servers may choose not to send it, and it's
// only sent on the first MapResponse in a stream. The client can determine
// whether it's reattaching to a prior stream by seeing whether this value
// matches the requested MapRequest.MapSessionHandle.
MapSessionHandle string `json:",omitempty"`
// Seq is a sequence number within a named map session (a response where the
// first message contains a MapSessionHandle). The Seq number may be omitted
// on responses that don't change the state of the stream, such as KeepAlive
// or certain types of PingRequests. This is the value to be sent in
// MapRequest.MapSessionSeq to resume after this message.
Seq int64 `json:",omitempty"`
// KeepAlive, if set, represents an empty message just to keep
// the connection alive. When true, all other fields except
// PingRequest, ControlTime, and PopBrowserURL are ignored.
KeepAlive bool `json:",omitempty"`
// PingRequest, if non-empty, is a request to the client to
// prove it's still there by sending an HTTP request to the
// provided URL. No auth headers are necessary.
// PingRequest may be sent on any MapResponse (ones with
// KeepAlive true or false).
PingRequest *PingRequest `json:",omitempty"`
// PopBrowserURL, if non-empty, is a URL for the client to
// open to complete an action. The client should dup suppress
// identical URLs and only open it once for the same URL.
PopBrowserURL string `json:",omitempty"`
// Networking
// Node describes the node making the map request.
// Starting with MapRequest.Version 18, nil means unchanged.
Node *Node `json:",omitempty"`
// DERPMap describe the set of DERP servers available.
// A nil value means unchanged.
DERPMap *DERPMap `json:",omitempty"`
// Peers, if non-empty, is the complete list of peers.
// It will be set in the first MapResponse for a long-polled request/response.
// Subsequent responses will be delta-encoded if MapRequest.Version >= 5 and server
// chooses, in which case Peers will be nil or zero length.
// If Peers is non-empty, PeersChanged and PeersRemoved should
// be ignored (and should be empty).
// Peers is always returned sorted by Node.ID.
Peers []*Node `json:",omitempty"`
// PeersChanged are the Nodes (identified by their ID) that
// have changed or been added since the past update on the
// HTTP response. It's not used by the server if MapRequest.Version < 5.
// PeersChanged is always returned sorted by Node.ID.
PeersChanged []*Node `json:",omitempty"`
// PeersRemoved are the NodeIDs that are no longer in the peer list.
PeersRemoved []NodeID `json:",omitempty"`
// PeersChangedPatch, if non-nil, means that node(s) have changed.
// This is a lighter version of the older PeersChanged support that
// only supports certain types of updates.
//
// These are applied after Peers* above, but in practice the
// control server should only send these on their own, without
// the Peers* fields also set.
PeersChangedPatch []*PeerChange `json:",omitempty"`
// PeerSeenChange contains information on how to update peers' LastSeen
// times. If the value is false, the peer is gone. If the value is true,
// the LastSeen time is now. Absent means unchanged.
PeerSeenChange map[NodeID]bool `json:",omitempty"`
// OnlineChange changes the value of a Peer Node.Online value.
OnlineChange map[NodeID]bool `json:",omitempty"`
// DNSConfig contains the DNS settings for the client to use.
// A nil value means no change from an earlier non-nil value.
DNSConfig *DNSConfig `json:",omitempty"`
// Domain is the name of the network that this node is
// in. It's either of the form "example.com" (for user
// foo@example.com, for multi-user networks) or
// "foo@gmail.com" (for siloed users on shared email
// providers). Its exact form should not be depended on; new
// forms are coming later.
// If empty, the value is unchanged.
Domain string `json:",omitempty"`
// CollectServices reports whether this node's Tailnet has
// requested that info about services be included in HostInfo.
// If unset, the most recent non-empty MapResponse value in
// the HTTP response stream is used.
CollectServices opt.Bool `json:",omitempty"`
// PacketFilter are the firewall rules.
//
// For MapRequest.Version >= 6, a nil value means the most
// previously streamed non-nil MapResponse.PacketFilter within
// the same HTTP response. A non-nil but empty list always means
// no PacketFilter (that is, to block everything).
//
// Note that this package's type, due its use of a slice and omitempty, is
// unable to marshal a zero-length non-nil slice. The control server needs
// to marshal this type using a separate type. See MapResponse docs.
//
// See PacketFilters for the newer way to send PacketFilter updates.
PacketFilter []FilterRule `json:",omitempty"`
// PacketFilters encodes incremental packet filter updates to the client
// without having to send the entire packet filter on any changes as
// required by the older PacketFilter (singular) field above. The map keys
// are server-assigned arbitrary strings. The map values are the new rules
// for that key, or nil to delete it. The client then concatenates all the
// rules together to generate the final packet filter. Because the
// FilterRules can only match or not match, the ordering of filter rules
// doesn't matter. (That said, the client generates the file merged packet
// filter rules by concananting all the packet filter rules sorted by the
// map key name. But it does so for stability and testability, not
// correctness. If something needs to rely on that property, something has
// gone wrong.)
//
// If the server sends a non-nil PacketFilter (above), that is equivalent to
// a named packet filter with the key "base". It is valid for the server to
// send both PacketFilter and PacketFilters in the same MapResponse or
// alternate between them within a session. The PacketFilter is applied
// first (if set) and then the PacketFilters.
//
// As a special case, the map key "*" with a value of nil means to clear all
// prior named packet filters (including any implicit "base") before
// processing the other map entries.
PacketFilters map[string][]FilterRule `json:",omitempty"`
// UserProfiles are the user profiles of nodes in the network.
// As of 1.1.541 (mapver 5), this contains new or updated
// user profiles only.
UserProfiles []UserProfile `json:",omitempty"`
// Health, if non-nil, sets the health state of the node from the control
// plane's perspective. A nil value means no change from the previous
// MapResponse. A non-nil 0-length slice restores the health to good (no
// known problems). A non-zero length slice are the list of problems that
// the control plane sees.
//
// Either this will be set, or DisplayMessages will be set, but not both.
//
// Note that this package's type, due its use of a slice and omitempty, is
// unable to marshal a zero-length non-nil slice. The control server needs
// to marshal this type using a separate type. See MapResponse docs.
Health []string `json:",omitempty"`
// DisplayMessages sets the health state of the node from the control
// plane's perspective.
//
// Either this will be set, or Health will be set, but not both.
//
// The map keys are IDs that uniquely identify the type of health issue. The
// map values are the messages. If the server sends down a map with entries,
// the client treats it as a patch: new entries are added, keys with a value
// of nil are deleted, existing entries with new values are updated. A nil
// map and an empty map both mean no change has occurred since the last
// update.
//
// As a special case, the map key "*" with a value of nil means to clear all
// prior display messages before processing the other map entries.
DisplayMessages map[DisplayMessageID]*DisplayMessage `json:",omitempty"`
// SSHPolicy, if non-nil, updates the SSH policy for how incoming
// SSH connections should be handled.
SSHPolicy *SSHPolicy `json:",omitempty"`
// ControlTime, if non-zero, is the current timestamp according to the control server.
ControlTime *time.Time `json:",omitempty"`
// TKAInfo describes the control plane's view of tailnet
// key authority (TKA) state.
//
// An initial nil TKAInfo indicates that the control plane
// believes TKA should not be enabled. An initial non-nil TKAInfo
// indicates the control plane believes TKA should be enabled.
// A nil TKAInfo in a mapresponse stream (i.e. a 'delta' mapresponse)
// indicates no change from the value sent earlier.
TKAInfo *TKAInfo `json:",omitempty"`
// DomainDataPlaneAuditLogID, if non-empty, is the per-tailnet log ID to be
// used when writing data plane audit logs.
DomainDataPlaneAuditLogID string `json:",omitempty"`
// Debug is normally nil, except for when the control server
// is setting debug settings on a node.
Debug *Debug `json:",omitempty"`
// ControlDialPlan tells the client how to connect to the control
// server. An initial nil is equivalent to new(ControlDialPlan).
// A subsequent streamed nil means no change.
ControlDialPlan *ControlDialPlan `json:",omitempty"`
// ClientVersion describes the latest client version that's available for
// download and whether the client is using it. A nil value means no change
// or nothing to report.
ClientVersion *ClientVersion `json:",omitempty"`
// DeprecatedDefaultAutoUpdate is the default node auto-update setting for this
// tailnet. The node is free to opt-in or out locally regardless of this
// value. Once this value has been set and stored in the client, future
// changes from the control plane are ignored.
//
// Deprecated: use NodeAttrDefaultAutoUpdate instead. See
// https://github.com/tailscale/tailscale/issues/11502.
DeprecatedDefaultAutoUpdate opt.Bool `json:"DefaultAutoUpdate,omitempty"`
}
// DisplayMessage represents a health state of the node from the control plane's
// perspective. It is deliberately similar to [health.Warnable] as both get
// converted into [health.UnhealthyState] to be sent to the GUI.
type DisplayMessage struct {
// Title is a string that the GUI uses as title for this message. The title
// should be short and fit in a single line. It should not end in a period.
//
// Example: "Network may be blocking Tailscale".
//
// See the various instantiations of [health.Warnable] for more examples.
Title string
// Text is an extended string that the GUI will display to the user. This
// could be multiple sentences explaining the issue in more detail.
//
// Example: "macOS Screen Time seems to be blocking Tailscale. Try disabling
// Screen Time in System Settings > Screen Time > Content & Privacy > Access
// to Web Content."
//
// See the various instantiations of [health.Warnable] for more examples.
Text string
// Severity is the severity of the DisplayMessage, which the GUI can use to
// determine how to display it. Maps to [health.Severity].
Severity DisplayMessageSeverity
// ImpactsConnectivity is whether the health problem will impact the user's
// ability to connect to the Internet or other nodes on the tailnet, which
// the GUI can use to determine how to display it.
ImpactsConnectivity bool `json:",omitempty"`
// Primary action, if present, represents the action to allow the user to
// take when interacting with this message. For example, if the
// DisplayMessage is shown via a notification, the action label might be a
// button on that notification and clicking the button would open the URL.
PrimaryAction *DisplayMessageAction `json:",omitempty"`
}
// DisplayMessageAction represents an action (URL and link) to be presented to
// the user associated with a [DisplayMessage].
type DisplayMessageAction struct {
// URL is the URL to navigate to when the user interacts with this action
URL string
// Label is the call to action for the UI to display on the UI element that
// will open the URL (such as a button or link). For example, "Sign in" or
// "Learn more".
Label string
}
// DisplayMessageID is a string that uniquely identifies the kind of health
// issue (e.g. "session-expired").
type DisplayMessageID string
// Equal returns true iff all fields are equal.
func (m DisplayMessage) Equal(o DisplayMessage) bool {
return m.Title == o.Title &&
m.Text == o.Text &&
m.Severity == o.Severity &&
m.ImpactsConnectivity == o.ImpactsConnectivity &&
(m.PrimaryAction == nil) == (o.PrimaryAction == nil) &&
(m.PrimaryAction == nil || (m.PrimaryAction.URL == o.PrimaryAction.URL &&
m.PrimaryAction.Label == o.PrimaryAction.Label))
}
// DisplayMessageSeverity represents how serious a [DisplayMessage] is. Analogous
// to health.Severity.
type DisplayMessageSeverity string
const (
// SeverityHigh is the highest severity level, used for critical errors that need immediate attention.
// On platforms where the client GUI can deliver notifications, a SeverityHigh message will trigger
// a modal notification.
SeverityHigh DisplayMessageSeverity = "high"
// SeverityMedium is used for errors that are important but not critical. This won't trigger a modal
// notification, however it will be displayed in a more visible way than a SeverityLow message.
SeverityMedium DisplayMessageSeverity = "medium"
// SeverityLow is used for less important notices that don't need immediate attention. The user will
// have to go to a Settings window, or another "hidden" GUI location to see these messages.
SeverityLow DisplayMessageSeverity = "low"
)
// ClientVersion is information about the latest client version that's available
// for the client (and whether they're already running it).
//
// It does not include a URL to download the client, as that varies by platform.
type ClientVersion struct {
// RunningLatest is true if the client is running the latest build.
RunningLatest bool `json:",omitempty"`
// LatestVersion is the latest version.Short ("1.34.2") version available
// for download for the client's platform and packaging type.
// It won't be populated if RunningLatest is true.
LatestVersion string `json:",omitempty"`
// UrgentSecurityUpdate is set when the client is missing an important
// security update. That update may be in LatestVersion or earlier.
// UrgentSecurityUpdate should not be set if RunningLatest is true.
UrgentSecurityUpdate bool `json:",omitempty"`
// Notify is whether the client should do an OS-specific notification about
// a new version being available. This should not be populated if
// RunningLatest is true. The client should not notify multiple times for
// the same LatestVersion value.
Notify bool `json:",omitempty"`
// NotifyURL is a URL to open in the browser when the user clicks on the
// notification, when Notify is true.
NotifyURL string `json:",omitempty"`
// NotifyText is the text to show in the notification, when Notify is true.
NotifyText string `json:",omitempty"`
}
// ControlDialPlan is instructions from the control server to the client on how
// to connect to the control server; this is useful for maintaining connection
// if the client's network state changes after the initial connection, or due
// to the configuration that the control server pushes.
type ControlDialPlan struct {
// An empty list means the default: use DNS (unspecified which DNS).
Candidates []ControlIPCandidate
}
// ControlIPCandidate represents a single candidate address to use when
// connecting to the control server.
type ControlIPCandidate struct {
// IP is the address to attempt connecting to.
IP netip.Addr `json:",omitzero"`
// ACEHost, if non-empty, means that the client should connect to the
// control plane using an HTTPS CONNECT request to the provided hostname. If
// the IP field is also set, then the IP is the IP address of the ACEHost
// (and not the control plane) and DNS should not be used. The target (the
// argument to CONNECT) is always the control plane's hostname, not an IP.
ACEHost string `json:",omitempty"`
// DialStartSec is the number of seconds after the beginning of the
// connection process to wait before trying this candidate.
DialStartDelaySec float64 `json:",omitempty"`
// DialTimeoutSec is the timeout for a connection to this candidate,
// starting after DialStartDelaySec.
DialTimeoutSec float64 `json:",omitempty"`
// Priority is the relative priority of this candidate; candidates with
// a higher priority are preferred over candidates with a lower
// priority.
Priority int `json:",omitempty"`
}
// Debug used to be a miscellaneous set of declarative debug config changes and
// imperative debug commands. They've since been mostly migrated to node
// attributes (MapResponse.Node.Capabilities) for the declarative things and c2n
// requests for the imperative things. Not much remains here. Don't add more.
type Debug struct {
// SleepSeconds requests that the client sleep for the
// provided number of seconds.
// The client can (and should) limit the value (such as 5
// minutes). This exists as a safety measure to slow down
// spinning clients, in case we introduce a bug in the
// state machine.
SleepSeconds float64 `json:",omitempty"`
// DisableLogTail disables the logtail package. Once disabled it can't be
// re-enabled for the lifetime of the process.
//
// This is primarily used by Headscale.
DisableLogTail bool `json:",omitempty"`
// Exit optionally specifies that the client should os.Exit
// with this code. This is a safety measure in case a client is crash
// looping or in an unsafe state and we need to remotely shut it down.
Exit *int `json:",omitempty"`
}
func (id ID) String() string { return fmt.Sprintf("id:%d", int64(id)) }
func (id UserID) String() string { return fmt.Sprintf("userid:%d", int64(id)) }
func (id LoginID) String() string { return fmt.Sprintf("loginid:%d", int64(id)) }
func (id NodeID) String() string { return fmt.Sprintf("nodeid:%d", int64(id)) }
// Equal reports whether n and n2 are equal.
func (n *Node) Equal(n2 *Node) bool {
if n == nil && n2 == nil {
return true
}
return n != nil && n2 != nil &&
n.ID == n2.ID &&
n.StableID == n2.StableID &&
n.Name == n2.Name &&
n.User == n2.User &&
n.Sharer == n2.Sharer &&
n.UnsignedPeerAPIOnly == n2.UnsignedPeerAPIOnly &&
n.Key == n2.Key &&
n.KeyExpiry.Equal(n2.KeyExpiry) &&
bytes.Equal(n.KeySignature, n2.KeySignature) &&
n.Machine == n2.Machine &&
n.DiscoKey == n2.DiscoKey &&
eqPtr(n.Online, n2.Online) &&
slicesx.EqualSameNil(n.Addresses, n2.Addresses) &&
slicesx.EqualSameNil(n.AllowedIPs, n2.AllowedIPs) &&
slicesx.EqualSameNil(n.PrimaryRoutes, n2.PrimaryRoutes) &&
slicesx.EqualSameNil(n.Endpoints, n2.Endpoints) &&
n.LegacyDERPString == n2.LegacyDERPString &&
n.HomeDERP == n2.HomeDERP &&
n.Cap == n2.Cap &&
n.Hostinfo.Equal(n2.Hostinfo) &&
n.Created.Equal(n2.Created) &&
eqTimePtr(n.LastSeen, n2.LastSeen) &&
n.MachineAuthorized == n2.MachineAuthorized &&
slices.Equal(n.Capabilities, n2.Capabilities) &&
n.CapMap.Equal(n2.CapMap) &&
n.ComputedName == n2.ComputedName &&
n.computedHostIfDifferent == n2.computedHostIfDifferent &&
n.ComputedNameWithHost == n2.ComputedNameWithHost &&
slicesx.EqualSameNil(n.Tags, n2.Tags) &&
n.Expired == n2.Expired &&
eqPtr(n.SelfNodeV4MasqAddrForThisPeer, n2.SelfNodeV4MasqAddrForThisPeer) &&
eqPtr(n.SelfNodeV6MasqAddrForThisPeer, n2.SelfNodeV6MasqAddrForThisPeer) &&
n.IsWireGuardOnly == n2.IsWireGuardOnly &&
n.IsJailed == n2.IsJailed
}
func eqPtr[T comparable](a, b *T) bool {
if a == b { // covers nil
return true
}
if a == nil || b == nil {
return false
}
return *a == *b
}
func eqTimePtr(a, b *time.Time) bool {
return ((a == nil) == (b == nil)) && (a == nil || a.Equal(*b))
}
// Oauth2Token is a copy of golang.org/x/oauth2.Token, to avoid the
// go.mod dependency on App Engine and grpc, which was causing problems.
// All we actually needed was this struct on the client side.
type Oauth2Token struct {
// AccessToken is the token that authorizes and authenticates
// the requests.
AccessToken string `json:"access_token"`
// TokenType is the type of token.
// The Type method returns either this or "Bearer", the default.
TokenType string `json:"token_type,omitempty"`
// RefreshToken is a token that's used by the application
// (as opposed to the user) to refresh the access token
// if it expires.
RefreshToken string `json:"refresh_token,omitempty"`
// Expiry is the optional expiration time of the access token.
//
// If zero, TokenSource implementations will reuse the same
// token forever and RefreshToken or equivalent
// mechanisms for that TokenSource will not be used.
Expiry time.Time `json:"expiry,omitzero"`
}
// NodeCapability is a type alias to [nodecap.Cap]
// and node capabilities are now defined in the [nodecap] package,
// see [CapabilityFileSharing] and [NodeAttrDisableAndroidBindToActiveNetwork] respectively.
//
//go:fix inline
type NodeCapability = nodecap.Cap
// NodeCapabilityPrefix is a type alias to [nodecap.Prefix]
// and these prefixes are now defined in the [nodecap] package,
// see [NodeAttrPrefixServices].
//
//go:fix inline
type NodeCapabilityPrefix = nodecap.Prefix
// Deprecated: Capabilities and NodeAttrs are now defined in the nodecap package, see [nodecap.Cap].
// These constants are provided for backwards compatibility
// but no new [NodeCapability] aliases will be added to this list.
//
//go:fix inline
const (
CapabilityFileSharing = nodecap.FileSharing
CapabilityAdmin = nodecap.Admin
CapabilityOwner = nodecap.Owner
CapabilitySSH = nodecap.SSH
CapabilitySSHRuleIn = nodecap.SSHRuleIn
CapabilityDataPlaneAuditLogs = nodecap.DataPlaneAuditLogs
CapabilityDebug = nodecap.Debug
CapabilityHTTPS = nodecap.HTTPS
CapabilityMacUIV2 = nodecap.MacUIV2
CapabilityServicesInDesktopClients = nodecap.ServicesInDesktopClients
CapabilityBindToInterfaceByRoute = nodecap.BindToInterfaceByRoute
CapabilityDebugDisableAlternateDefaultRouteInterface = nodecap.DebugDisableAlternateDefaultRouteInterface
CapabilityDebugDisableBindConnToInterface = nodecap.DebugDisableBindConnToInterface
CapabilityDebugDisableBindConnToInterfaceAppleExt = nodecap.DebugDisableBindConnToInterfaceAppleExt
CapabilityTailnetLock = nodecap.TailnetLock
CapabilityWarnFunnelNoInvite = nodecap.WarnFunnelNoInvite
CapabilityWarnFunnelNoHTTPS = nodecap.WarnFunnelNoHTTPS
CapabilityDebugTSDNSResolution = nodecap.DebugTSDNSResolution
CapabilityFunnelPorts = nodecap.FunnelPorts
// Deprecated: Do not add any further values here, use [nodecap] instead.
NodeAttrDisableAndroidBindToActiveNetwork = nodecap.DisableAndroidBindToActiveNetwork
NodeAttrOnlyTCP443 = nodecap.OnlyTCP443
NodeAttrFunnel = nodecap.Funnel
NodeAttrSSHAggregator = nodecap.SSHAggregator
NodeAttrDebugForceBackgroundSTUN = nodecap.DebugForceBackgroundSTUN
NodeAttrDebugDisableWGTrim = nodecap.DebugDisableWGTrim
NodeAttrDisableSubnetsIfPAC = nodecap.DisableSubnetsIfPAC
NodeAttrDisableUPnP = nodecap.DisableUPnP
NodeAttrDisableDeltaUpdates = nodecap.DisableDeltaUpdates
NodeAttrRandomizeClientPort = nodecap.RandomizeClientPort
NodeAttrSilentDisco = nodecap.SilentDisco
NodeAttrOneCGNATEnable = nodecap.OneCGNATEnable
NodeAttrOneCGNATDisable = nodecap.OneCGNATDisable
NodeAttrPeerMTUEnable = nodecap.PeerMTUEnable
NodeAttrDNSForwarderDisableTCPRetries = nodecap.DNSForwarderDisableTCPRetries
NodeAttrLinuxMustUseIPTables = nodecap.LinuxMustUseIPTables
NodeAttrLinuxMustUseNfTables = nodecap.LinuxMustUseNfTables
NodeAttrProbeUDPLifetime = nodecap.ProbeUDPLifetime
NodeAttrsTaildriveShare = nodecap.TaildriveShare
NodeAttrsTaildriveAccess = nodecap.TaildriveAccess
NodeAttrSuggestExitNode = nodecap.SuggestExitNode
NodeAttrDisableWebClient = nodecap.DisableWebClient
NodeAttrLogExitFlows = nodecap.LogExitFlows
NodeAttrAutoExitNode = nodecap.AutoExitNode
NodeAttrStoreAppCRoutes = nodecap.StoreAppCRoutes
NodeAttrSuggestExitNodeUI = nodecap.SuggestExitNodeUI
NodeAttrUserDialUseRoutes = nodecap.UserDialUseRoutes
NodeAttrSSHBehaviorV1 = nodecap.SSHBehaviorV1
NodeAttrSSHBehaviorV2 = nodecap.SSHBehaviorV2
NodeAttrDisableSplitDNSWhenNoCustomResolvers = nodecap.DisableSplitDNSWhenNoCustomResolvers
NodeAttrScopeQuad100OnMacOS = nodecap.ScopeQuad100OnMacOS
NodeAttrDisableLocalDNSOverrideViaNRPT = nodecap.DisableLocalDNSOverrideViaNRPT
NodeAttrDisableMagicSockCryptoRouting = nodecap.DisableMagicSockCryptoRouting
NodeAttrDisableCaptivePortalDetection = nodecap.DisableCaptivePortalDetection
NodeAttrDisableSkipStatusQueue = nodecap.DisableSkipStatusQueue
NodeAttrSSHEnvironmentVariables = nodecap.SSHEnvironmentVariables
NodeAttrServiceHost = nodecap.ServiceHost
NodeAttrMaxKeyDuration = nodecap.MaxKeyDuration
NodeAttrNativeIPV4 = nodecap.NativeIPV4
NodeAttrDisableRelayServer = nodecap.DisableRelayServer
NodeAttrDisableRelayClient = nodecap.DisableRelayClient
NodeAttrMagicDNSPeerAAAA = nodecap.MagicDNSPeerAAAA
NodeAttrDNSSubdomainResolve = nodecap.DNSSubdomainResolve
NodeAttrTrafficSteering = nodecap.TrafficSteering
NodeAttrTailnetDisplayName = nodecap.TailnetDisplayName
NodeAttrClientSideReachability = nodecap.ClientSideReachability
NodeAttrClientSideReachabilityRouteCheck = nodecap.ClientSideReachabilityRouteCheck
NodeAttrDefaultAutoUpdate = nodecap.DefaultAutoUpdate
NodeAttrDisableHostsFileUpdates = nodecap.DisableHostsFileUpdates
NodeAttrForceRegisterMagicDNSIPv4Only = nodecap.ForceRegisterMagicDNSIPv4Only
NodeAttrCacheNetworkMaps = nodecap.CacheNetworkMaps
NodeAttrDisableCacheNetworkMaps = nodecap.DisableCacheNetworkMaps
NodeAttrDisableLinuxCGNATDropRule = nodecap.DisableLinuxCGNATDropRule
NodeAttrEmitRuntimeMetrics = nodecap.EmitRuntimeMetrics
NodeAttrDisableUDPGRO = nodecap.DisableUDPGRO
NodeAttrDisableUDPGSO = nodecap.DisableUDPGSO
NodeAttrDisableTUNUDPGRO = nodecap.DisableTUNUDPGRO
NodeAttrDisableTUNTCPGRO = nodecap.DisableTUNTCPGRO
NodeAttrNeverGSOEqualTail = nodecap.NeverGSOEqualTail
NodeAttrConnReject = nodecap.ConnReject
// Deprecated: Do not add any further values here, use [nodecap] instead.
NodeAttrPrefixServices = nodecap.ServicesPrefix
// Deprecated: Do not add any further values here, use [nodecap] instead.
)
// SetDNSRequest is a request to add a DNS record.
//
// This is used to let tailscaled clients complete their ACME DNS-01 challenges
// (so people can use LetsEncrypt, etc) to get TLS certificates for
// their foo.bar.ts.net MagicDNS names.
//
// This is JSON-encoded and sent over the control plane connection to:
//
// POST https://<control-plane>/machine/set-dns
type SetDNSRequest struct {
// Version is the client's capabilities
// (CurrentCapabilityVersion) when using the Noise transport.
//
// When using the original nacl crypto_box transport, the
// value must be 1.
Version CapabilityVersion
// NodeKey is the client's current node key.
NodeKey key.NodePublic
// Name is the domain name for which to create a record.
// For ACME DNS-01 challenges, it should be one of the domains
// in MapResponse.DNSConfig.CertDomains with the prefix
// "_acme-challenge.".
Name string
// Type is the DNS record type. For ACME DNS-01 challenges, it
// should be "TXT".
Type string
// Value is the value to add.
Value string
}
// SetDNSResponse is the response to a SetDNSRequest.
type SetDNSResponse struct{}
// HealthChangeRequest is the JSON request body type used to report
// node health changes to:
//
// POST https://<control-plane>/machine/update-health.
//
// As of 2025-10-02, we stopped sending this to the control plane proactively.
// It was never useful enough with its current design and needs more thought.
type HealthChangeRequest struct {
Subsys string // a health.Subsystem value in string form
Error string // or empty if cleared
// NodeKey is the client's current node key.
// In clients <= 1.62.0 it was always the zero value.
NodeKey key.NodePublic
}
// SetDeviceAttributesRequest is a request to update the
// current node's device posture attributes.
//
// As of 2024-12-30, this is an experimental dev feature
// for internal testing. See tailscale/corp#24690.
//
// This is JSON-encoded and sent over the control plane connection to:
//
// PATCH https://<control-plane>/machine/set-device-attr
type SetDeviceAttributesRequest struct {
// Version is the current binary's [CurrentCapabilityVersion].
Version CapabilityVersion
// NodeKey identifies the node to modify. It should be the currently active
// node and is an error if not.
NodeKey key.NodePublic
// Update is a map of device posture attributes to update.
// Attributes not in the map are left unchanged.
Update AttrUpdate
}
// AttrUpdate is a map of attributes to update.
// Attributes not in the map are left unchanged.
// The value can be a string, float64, bool, or nil to delete.
//
// See https://tailscale.com/s/api-device-posture-attrs.
//
// TODO(bradfitz): add struct type for specifying optional associated data
// for each attribute value, like an expiry time?
type AttrUpdate map[string]any
// SSHPolicy is the policy for how to handle incoming SSH connections
// over Tailscale.
type SSHPolicy struct {
// Rules are the rules to process for an incoming SSH connection. The first
// matching rule takes its action and stops processing further rules.
//
// When an incoming connection first starts, all rules are evaluated in
// "none" auth mode, where the client hasn't even been asked to send a
// public key. All SSHRule.Principals requiring a public key won't match. If
// a rule matches on the first pass and its Action is reject, the
// authentication fails with that action's rejection message, if any.
//
// If the first pass rule evaluation matches nothing without matching an
// Action with Reject set, the rules are considered to see whether public
// keys might still result in a match. If not, "none" auth is terminated
// before proceeding to public key mode. If so, the client is asked to try
// public key authentication and the rules are evaluated again for each of
// the client's present keys.
Rules []*SSHRule `json:"rules"`
}
// An SSH rule is a match predicate and associated action for an incoming SSH connection.
type SSHRule struct {
// RuleExpires, if non-nil, is when this rule expires.
//
// For example, a (principal,sshuser) tuple might be granted
// prompt-free SSH access for N minutes, so this rule would be
// before a expiration-free rule for the same principal that
// required an auth prompt. This permits the control plane to
// be out of the path for already-authorized SSH pairs.
//
// Once a rule matches, the lifetime of any accepting connection
// is subject to the SSHAction.SessionExpires time, if any.
RuleExpires *time.Time `json:"ruleExpires,omitempty"`
// Principals matches an incoming connection. If the connection
// matches anything in this list and also matches SSHUsers,
// then Action is applied.
Principals []*SSHPrincipal `json:"principals"`
// SSHUsers are the SSH users that this rule matches. It is a
// map from either ssh-user|"*" => local-user. The map must
// contain a key for either ssh-user or, as a fallback, "*" to
// match anything. If it does, the map entry's value is the
// actual user that's logged in.
// If the map value is the empty string (for either the
// requested SSH user or "*"), the rule doesn't match.
// If the map value is "=", it means the ssh-user should map
// directly to the local-user.
// It may be nil if the Action is reject.
SSHUsers map[string]string `json:"sshUsers"`
// Action is the outcome to task.
// A nil or invalid action means to deny.
Action *SSHAction `json:"action"`
// AcceptEnv is a slice of environment variable names that are allowlisted
// for the SSH rule in the policy file.
//
// AcceptEnv values may contain * and ? wildcard characters which match against
// an arbitrary number of characters or a single character respectively.
AcceptEnv []string `json:"acceptEnv,omitempty"`
}
// SSHPrincipal is either a particular node or a user on any node.
type SSHPrincipal struct {
// Matching any one of the following four field causes a match.
// It must also match Certs, if non-empty.
Node StableNodeID `json:"node,omitempty"`
NodeIP string `json:"nodeIP,omitempty"`
UserLogin string `json:"userLogin,omitempty"` // email-ish: foo@example.com, bar@github
Any bool `json:"any,omitempty"` // if true, match any connection
// TODO(bradfitz): add StableUserID, once that exists
// UnusedPubKeys was public key support. It never became an official product
// feature and so as of 2024-12-12 is being removed.
// This stub exists to remind us not to re-use the JSON field name "pubKeys"
// in the future if we bring it back with different semantics.
//
// Deprecated: do not use. It does nothing.
UnusedPubKeys []string `json:"pubKeys,omitempty"`
}
// SSHAction is how to handle an incoming connection.
// At most one field should be non-zero.
type SSHAction struct {
// Message, if non-empty, is shown to the user before the
// action occurs.
Message string `json:"message,omitempty"`
// Reject, if true, terminates the connection. This action
// has higher priority that Accept, if given.
// The reason this is exists is primarily so a response
// from HoldAndDelegate has a way to stop the poll.
Reject bool `json:"reject,omitempty"`
// Accept, if true, accepts the connection immediately
// without further prompts.
Accept bool `json:"accept,omitempty"`
// SessionDuration, if non-zero, is how long the session can stay open
// before being forcefully terminated.
// It is encoded as an int64 of nanoseconds (Go's time.Duration
// wire format for encoding/json v1). It must not use a jsonv2
// format tag; the mere presence of one makes Go 1.27's
// encoding/json fail to decode the struct. See
// https://github.com/tailscale/tailscale/issues/20528.
SessionDuration time.Duration `json:"sessionDuration,omitempty"`
// AllowAgentForwarding, if true, allows accepted connections to forward
// the ssh agent if requested.
AllowAgentForwarding bool `json:"allowAgentForwarding,omitempty"`
// HoldAndDelegate, if non-empty, is a URL that serves an
// outcome verdict. The connection will be accepted and will
// block until the provided long-polling URL serves a new
// SSHAction JSON value. The URL must be fetched using the
// Noise transport (in package control/control{base,http}).
// If the long poll breaks before returning a complete HTTP
// response, it should be re-fetched as long as the SSH
// session is open.
//
// The following variables in the URL are expanded by tailscaled:
//
// * $SRC_NODE_IP (URL escaped)
// * $SRC_NODE_ID (Node.ID as int64 string)
// * $DST_NODE_IP (URL escaped)
// * $DST_NODE_ID (Node.ID as int64 string)
// * $SSH_USER (URL escaped, ssh user requested)
// * $LOCAL_USER (URL escaped, local user mapped)
HoldAndDelegate string `json:"holdAndDelegate,omitempty"`
// AllowLocalPortForwarding, if true, allows accepted connections
// to use local port forwarding if requested.
AllowLocalPortForwarding bool `json:"allowLocalPortForwarding,omitempty"`
// AllowRemotePortForwarding, if true, allows accepted connections
// to use remote port forwarding if requested.
AllowRemotePortForwarding bool `json:"allowRemotePortForwarding,omitempty"`
// Recorders defines the destinations of the SSH session recorders.
// The recording will be uploaded to http://addr:port/record.
Recorders []netip.AddrPort `json:"recorders,omitempty"`
// OnRecorderFailure is the action to take if recording fails.
// If nil, the default action is to fail open.
OnRecordingFailure *SSHRecorderFailureAction `json:"onRecordingFailure,omitempty"`
}
// SSHRecorderFailureAction is the action to take if recording fails.
type SSHRecorderFailureAction struct {
// RejectSessionWithMessage, if not empty, specifies that the session should
// be rejected if the recording fails to start.
// The message will be shown to the user before the session is rejected.
RejectSessionWithMessage string `json:",omitempty"`
// TerminateSessionWithMessage, if not empty, specifies that the session
// should be terminated if the recording fails after it has started. The
// message will be shown to the user before the session is terminated.
TerminateSessionWithMessage string `json:",omitempty"`
// NotifyURL, if non-empty, specifies a HTTP POST URL to notify when the
// recording fails. The payload is the JSON encoded
// SSHRecordingFailureNotifyRequest struct. The host field in the URL is
// ignored, and it will be sent to control over the Noise transport.
NotifyURL string `json:",omitempty"`
}
// SSHEventNotifyRequest is the JSON payload sent to the NotifyURL
// for an SSH event.
//
// POST https://<control-plane>/[...varies, sent in SSH policy...]
type SSHEventNotifyRequest struct {
// EventType is the type of notify request being sent.
EventType SSHEventType
// ConnectionID uniquely identifies a connection made to the SSH server.
// It may be shared across multiple sessions over the same connection in
// case a single connection creates multiple sessions.
ConnectionID string
// CapVersion is the client's current CapabilityVersion.
CapVersion CapabilityVersion
// NodeKey is the client's current node key.
NodeKey key.NodePublic
// SrcNode is the ID of the node that initiated the SSH session.
SrcNode NodeID
// SSHUser is the user that was presented to the SSH server.
SSHUser string
// LocalUser is the user that was resolved from the SSHUser for the local machine.
LocalUser string
// RecordingAttempts is the list of recorders that were attempted, in order.
RecordingAttempts []*SSHRecordingAttempt
}
// SSHEventType defines the event type linked to a SSH action or state.
type SSHEventType int
const (
UnspecifiedSSHEventType SSHEventType = 0
// SSHSessionRecordingRejected is the event that
// defines when a SSH session cannot be started
// because no recorder is available for session
// recording, and the SSHRecorderFailureAction
// RejectSessionWithMessage is not empty.
SSHSessionRecordingRejected SSHEventType = 1
// SSHSessionRecordingTerminated is the event that
// defines when session recording has failed
// during the session and the SSHRecorderFailureAction
// TerminateSessionWithMessage is not empty.
SSHSessionRecordingTerminated SSHEventType = 2
// SSHSessionRecordingFailed is the event that
// defines when session recording is unavailable and
// the SSHRecorderFailureAction RejectSessionWithMessage
// or TerminateSessionWithMessage is empty.
SSHSessionRecordingFailed SSHEventType = 3
)
// SSHRecordingAttempt is a single attempt to start a recording.
type SSHRecordingAttempt struct {
// Recorder is the address of the recorder that was attempted.
Recorder netip.AddrPort
// FailureMessage is the error message of the failed attempt.
FailureMessage string
}
// QueryFeatureRequest is a request sent to "POST /machine/feature/query" to get
// instructions on how to enable a feature, such as Funnel, for the node's
// tailnet.
//
// See QueryFeatureResponse for response structure.
type QueryFeatureRequest struct {
// Feature is the string identifier for a feature.
Feature string `json:",omitzero"`
// NodeKey is the client's current node key.
NodeKey key.NodePublic `json:",omitzero"`
}
// QueryFeatureResponse is the response to an QueryFeatureRequest.
// See cli.enableFeatureInteractive for usage.
type QueryFeatureResponse struct {
// Complete is true when the feature is already enabled.
Complete bool `json:",omitzero"`
// Text holds lines to display in the CLI with information
// about the feature and how to enable it.
//
// Lines are separated by newline characters. The final
// newline may be omitted.
Text string `json:",omitzero"`
// URL is the link for the user to visit to take action on
// enabling the feature.
//
// When empty, there is no action for this user to take.
URL string `json:",omitzero"`
// ShouldWait specifies whether the CLI should block and
// wait for the user to enable the feature.
//
// If this is true, the enablement from the control server
// is expected to be a quick and uninterrupted process for
// the user, and blocking allows them to immediately start
// using the feature once enabled without rerunning the
// command (e.g. no need to re-run "funnel on").
//
// The CLI can watch the IPN notification bus for changes in
// required node capabilities to know when to continue.
ShouldWait bool `json:",omitzero"`
}
// WebClientAuthResponse is the response to a web client authentication request
// sent to "/machine/webclient/action" or "/machine/webclient/wait".
// See client/web for usage.
type WebClientAuthResponse struct {
// ID is a unique identifier for the session auth request.
// It can be supplied to "/machine/webclient/wait" to pause until
// the session authentication has been completed.
ID string `json:",omitzero"`
// URL is the link for the user to visit to authenticate the session.
//
// When empty, there is no action for the user to take.
URL string `json:",omitzero"`
// Complete is true when the session authentication has been completed.
Complete bool `json:",omitzero"`
}
// OverTLSPublicKeyResponse is the JSON response to /key?v=<n>
// over HTTPS (regular TLS) to the Tailscale control plane server,
// where the 'v' argument is the client's current capability version
// (previously known as the "MapRequest version").
//
// The "OverTLS" prefix is to loudly declare that this exchange
// doesn't happen over Noise and can be intercepted/MITM'ed by
// enterprise/corp proxies where the organization can put TLS roots
// on devices.
type OverTLSPublicKeyResponse struct {
// LegacyPublic specifies the control plane server's original
// NaCl crypto_box machine key.
// It will be zero for sufficiently new clients, based on their
// advertised "v" parameter (the CurrentMapRequestVersion).
// In that case, only the newer Noise-based transport may be used
// using the PublicKey field.
LegacyPublicKey key.MachinePublic `json:"legacyPublicKey"`
// PublicKey specifies the server's public key for the
// Noise-based control plane protocol. (see packages
// control/controlbase and control/controlhttp)
PublicKey key.MachinePublic `json:"publicKey"`
}
// TokenRequest is a request to get an OIDC ID token for an audience.
// The token can be presented to any resource provider which offers OIDC
// Federation.
//
// It is JSON-encoded and sent over Noise to "POST /machine/id-token".
type TokenRequest struct {
// CapVersion is the client's current CapabilityVersion.
CapVersion CapabilityVersion
// NodeKey is the client's current node key.
NodeKey key.NodePublic
// Audience the token is being requested for.
Audience string
}
// TokenResponse is the response to a TokenRequest.
type TokenResponse struct {
// IDToken is a JWT encoding the following standard claims:
//
// `sub` | the MagicDNS name of the node
// `aud` | Audience from the request
// `exp` | Token expiry
// `iat` | Token issuance time
// `iss` | Issuer
// `jti` | Random token identifier
// `nbf` | Not before time
//
// It also encodes the following Tailscale specific claims:
//
// `key` | the node public key
// `addresses` | the Tailscale IPs of the node
// `nid` | the node ID
// `node` | the name of the node
// `domain` | the domain of the node, it has the same format as MapResponse.Domain.
// `tags` | an array of <domain:tag> on the node (like alice.github:tag:foo or example.com:tag:foo)
// `user` | user emailish (like alice.github:alice@github or example.com:bob@example.com), if not tagged
// `uid` | user ID, if not tagged
IDToken string `json:"id_token"`
}
// PeerChange is an update to a node.
type PeerChange struct {
// NodeID is the node ID being mutated. If the NodeID is not
// known in the current netmap, this update should be
// ignored. (But the server will try not to send such useless
// updates.)
NodeID NodeID
// DERPRegion, if non-zero, means that NodeID's home DERP
// region ID is now this number.
DERPRegion DERPRegionID `json:",omitzero"`
// Cap, if non-zero, means that NodeID's capability version has changed.
Cap CapabilityVersion `json:",omitzero"`
// CapMap, if non-nil, means that NodeID's capability map has changed.
CapMap NodeCapMap `json:",omitempty"`
// Endpoints, if non-empty, means that NodeID's UDP Endpoints
// have changed to these.
Endpoints []netip.AddrPort `json:",omitempty"`
// Key, if non-nil, means that the NodeID's wireguard public key changed.
Key *key.NodePublic `json:",omitzero"` // TODO: de-pointer: tailscale/tailscale#17978
// KeySignature, if non-nil, means that the signature of the wireguard
// public key has changed.
KeySignature tkatype.MarshaledSignature `json:",omitempty"`
// DiscoKey, if non-nil, means that the NodeID's discokey changed.
DiscoKey *key.DiscoPublic `json:",omitzero"` // TODO: de-pointer: tailscale/tailscale#17978
// Online, if non-nil, means that the NodeID's online status changed.
Online *bool `json:",omitzero"`
// LastSeen, if non-nil, means that the NodeID's online status changed.
LastSeen *time.Time `json:",omitzero"` // TODO: de-pointer: tailscale/tailscale#17978
// KeyExpiry, if non-nil, changes the NodeID's key expiry.
KeyExpiry *time.Time `json:",omitzero"` // TODO: de-pointer: tailscale/tailscale#17978
}
// DerpMagicIP is a fake WireGuard endpoint IP address that means to
// use DERP. When used (in the Node.DERP field), the port number of
// the WireGuard endpoint is the DERP region ID number to use.
//
// Mnemonic: 3.3.40 are numbers above the keys D, E, R, P.
const DerpMagicIP = "127.3.3.40"
var DerpMagicIPAddr = netip.MustParseAddr(DerpMagicIP)
// EarlyNoise is the early payload that's sent over Noise but before the HTTP/2
// handshake when connecting to the coordination server.
//
// This exists to let the server push some early info to client for that
// stateful HTTP/2+Noise connection without incurring an extra round trip. (This
// would've used HTTP/2 server push, had Go's client-side APIs been available)
type EarlyNoise struct {
// NodeKeyChallenge is a random per-connection public key to be used by
// the client to prove possession of a wireguard private key.
NodeKeyChallenge key.ChallengePublic `json:"nodeKeyChallenge"`
}
// LBHeader is the HTTP request header used to provide a load balancer or
// internal reverse proxy with information about the request body without the
// reverse proxy needing to read the body to parse it out. Think of it akin to
// an HTTP Host header or SNI. The value may be absent (notably for old clients)
// but if present, it should match the request. A non-empty value that doesn't
// match the request body's.
//
// The possible values depend on the request path, but for /machine (Noise)
// requests, they'll usually be a node public key (in key.NodePublic.String
// format), matching the Request JSON body's NodeKey.
//
// Note that this is not a security or authentication header; it's strictly
// denormalized redundant data as an optimization.
//
// For some request types, the header may have multiple values. (e.g. OldNodeKey
// vs NodeKey)
const LBHeader = "Ts-Lb"
// ServiceIPMappings maps ServiceName to lists of IP addresses. This is used
// as the value of the [NodeAttrServiceHost] capability, to inform service hosts
// what IP addresses they need to listen on for each service that they are
// advertising.
//
// This is of the form:
//
// {
// "svc:samba": ["100.65.32.1", "fd7a:115c:a1e0::1234"],
// "svc:web": ["100.102.42.3", "fd7a:115c:a1e0::abcd"],
// }
//
// where the IP addresses are the IPs of the VIP services. These IPs are also
// provided in AllowedIPs, but this lets the client know which services
// correspond to those IPs. Any services that don't correspond to a service
// this client is hosting can be ignored.
type ServiceIPMappings map[ServiceName][]netip.Addr
// ServiceActionType represents the type of a [ServiceAction]. Clients use
// this value to determine which protocol or application to use when
// handling the action.
//
// Well-known Tailscale types are defined as constants in this package.
// They are plain slugs (e.g. "ssh", "http") with no URL prefix.
//
// When a type corresponds to an application layer protocol with a
// well-known port, the slug generally follows the IANA Service Name and
// Transport Protocol Port Number Registry:
// https://www.iana.org/assignments/service-names-port-numbers.
//
// In cases where the IANA service name differs from the commonly used
// protocol name, the protocol name is preferred for readability and
// interoperability (e.g. RDP is registered as "ms-wbt-server").
//
// If third-party types are introduced in the future, they must use URL
// form (e.g. "example.com/my-custom-type") to avoid collisions with
// first-party types.
type ServiceActionType string
const (
// ServiceActionTypeAWSS3 indicates that a port corresponds to an
// AWS S3 compatible endpoint and the AWS configuration may be modified
// to point to this endpoint and S3 clients may be used.
ServiceActionTypeAWSS3 ServiceActionType = "aws-s3"
// ServiceActionTypeCockroachDB indicates that a port corresponds to a
// CockroachDB server and CockroachDB clients may be used.
ServiceActionTypeCockroachDB ServiceActionType = "cockroach"
// ServiceActionTypeElasticSearch indicates that a port corresponds to
// an Elasticsearch server and Elasticsearch clients may be used.
ServiceActionTypeElasticSearch ServiceActionType = "elasticsearch"
// ServiceActionTypeHTTP indicates that a port corresponds to an HTTP
// server and HTTP clients may be used.
ServiceActionTypeHTTP ServiceActionType = "http"
// ServiceActionTypeKubernetes indicates that a port corresponds to a
// Kubernetes API server and the Kubernetes context may be configured to
// point to the service and Kubernetes clients may be used.
ServiceActionTypeKubernetes ServiceActionType = "kubernetes"
// ServiceActionTypeMongoDB indicates that a port corresponds to a MongoDB
// server and MongoDB clients may be used.
ServiceActionTypeMongoDB ServiceActionType = "mongodb"
// ServiceActionTypeMSSQL indicates that a port corresponds to a Microsoft
// SQL Server and MSSQL clients may be used. The IANA registry uses
// "ms-sql-s" but "mssql" is the widely recognized name.
ServiceActionTypeMSSQL ServiceActionType = "mssql"
// ServiceActionTypeMySQL indicates that a port corresponds to a MySQL
// server and MySQL clients may be used.
ServiceActionTypeMySQL ServiceActionType = "mysql"
// ServiceActionTypePostgreSQL indicates that a port corresponds to a
// PostgreSQL server and PostgreSQL clients may be used.
ServiceActionTypePostgreSQL ServiceActionType = "postgresql"
// ServiceActionTypeRDP indicates that a port corresponds to an RDP
// server and RDP clients may be used. The IANA registry uses
// "ms-wbt-server" but "rdp" is the widely recognized name.
ServiceActionTypeRDP ServiceActionType = "rdp"
// ServiceActionTypeVNC indicates that a port corresponds to a VNC
// server and VNC clients may be used. The IANA registry uses "rfb"
// (Remote Framebuffer) but "vnc" is the widely recognized name.
ServiceActionTypeVNC ServiceActionType = "vnc"
// ServiceActionTypeSSH indicates that a port corresponds to an SSH
// server and SSH clients may be used.
ServiceActionTypeSSH ServiceActionType = "ssh"
// ServiceActionTypeTCP indicates that a port corresponds to a generic
// TCP server and TCP clients may be used.
ServiceActionTypeTCP ServiceActionType = "tcp"
)
// Valid reports whether t is a recognized ServiceActionType.
func (t ServiceActionType) Valid() bool {
switch t {
case ServiceActionTypeAWSS3,
ServiceActionTypeCockroachDB,
ServiceActionTypeElasticSearch,
ServiceActionTypeHTTP,
ServiceActionTypeKubernetes,
ServiceActionTypeMongoDB,
ServiceActionTypeMSSQL,
ServiceActionTypeMySQL,
ServiceActionTypePostgreSQL,
ServiceActionTypeRDP,
ServiceActionTypeVNC,
ServiceActionTypeSSH,
ServiceActionTypeTCP:
return true
}
return false
}
// ServiceActionAttribute represents an attribute key for a [ServiceAction].
// A given attribute's applicability depends on the [ServiceAction.Type].
//
// Well-known Tailscale attributes are defined as constants in this package.
// Values are [RawMessage] (raw JSON) whose schema depends on the attribute.
//
// Clients should ignore attributes they do not recognize.
type ServiceActionAttribute string
const (
// ServiceActionAttributeWebClientURL is a [ServiceActionAttribute]
// that indicates to clients that a browser based client for the
// action is available at the URL in the value.
//
// The value is a JSON string containing a URL with an http(s) scheme.
ServiceActionAttributeWebClientURL ServiceActionAttribute = "tailscale.com/cap/web-client-url"
// ServiceActionAttributeResourceName is a [ServiceActionAttribute]
// that indicates to clients that the resource specified by the value
// should be selected when opening the application corresponding to
// the [ServiceAction.Type].
//
// This is particularly relevant for PostgreSQL services, where a
// database must be specified while opening a connection.
//
// The value is a JSON string containing the resource name
// (e.g. a database name).
ServiceActionAttributeResourceName ServiceActionAttribute = "tailscale.com/cap/resource-name"
// ServiceActionAttributeSkipUsername is a [ServiceActionAttribute]
// that indicates to clients that a username is not required.
//
// This attribute is typically used for services that are backed by
// an application-layer proxy. The proxy injects the appropriate
// credentials on behalf of the user, and any username provided by
// the user is ignored. This attribute informs clients that the
// username is irrelevant, and any username prompt should be skipped.
//
// The value is a JSON boolean.
ServiceActionAttributeSkipUsername ServiceActionAttribute = "tailscale.com/cap/skip-username"
)
// ServiceAction describes an action that a Tailscale
// client can invoke for a [ServiceDetails].
//
// Clients should ignore actions with types they do not recognize.
type ServiceAction struct {
// Type is the action's identifier i.e. a unique slug corresponding to a well
// known action. It drives icon selection and client application matching.
Type ServiceActionType
// Port is the target TCP port for this action. It must match one of
// the specific (non-range) TCP ports listed in the enclosing
// [ServiceDetails.Ports].
Port uint16
// DisplayName is an optional human-readable label which may be shown
// in client menus when there are multiple actions to select from.
// If empty, a display name may be inferred from the Type field.
DisplayName string `json:",omitzero"`
// Attributes is an optional key-value map carrying additional metadata
// to help clients drive UI or behavior related to this action.
Attributes map[ServiceActionAttribute]RawMessage `json:",omitzero"`
}
// ServiceDetails describes a Service visible to this node.
// It is the value type stored under [NodeAttrPrefixServices]+serviceName keys in [NodeCapMap].
type ServiceDetails struct {
// Name is the name of the Service, of the form "svc:dns-label".
Name ServiceName
// DisplayName is an optional human-readable label for the service.
// If empty, Name is used as a fallback by clients.
DisplayName string `json:",omitzero"`
// Addrs are the IP addresses (IPv4 and IPv6) assigned to this Service.
Addrs []netip.Addr `json:",omitempty"`
// Ports are the protocol/port combinations the Service accepts.
Ports []ProtoPortRange `json:",omitempty"`
// Actions is an optional list of actions describing how a client may
// interact with this service. Each action maps a [ServiceAction.Type] to a
// specific TCP port; the port must match one of the concrete (non-range)
// ports listed in Ports.
//
// Multiple actions may reference the same port. Not every port requires
// a corresponding action. When Actions has length zero, clients may infer
// default interactions from Ports.
Actions []ServiceAction `json:",omitzero"`
}
// ClientAuditAction represents an auditable action that a client can report to the
// control plane. These actions must correspond to the supported actions
// in the control plane.
type ClientAuditAction string
const (
// AuditNodeDisconnect action is sent when a node has disconnected
// from the control plane. The details must include a reason in the Details
// field, either generated, or entered by the user.
AuditNodeDisconnect = ClientAuditAction("DISCONNECT_NODE")
)
// AuditLogRequest represents an audit log request to be sent to the control plane.
//
// This is JSON-encoded and sent over the control plane connection to:
// POST https://<control-plane>/machine/audit-log
type AuditLogRequest struct {
// Version is the client's current CapabilityVersion.
Version CapabilityVersion `json:",omitzero"`
// NodeKey is the client's current node key.
NodeKey key.NodePublic `json:",omitzero"`
// Action is the action to be logged. It must correspond to a known action in the control plane.
Action ClientAuditAction `json:",omitzero"`
// Details is an opaque string, specific to the action being logged. Empty strings may not
// be valid depending on the action being logged.
Details string `json:",omitzero"`
// Timestamp is the time at which the audit log was generated on the node.
Timestamp time.Time `json:",omitzero"`
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
package tailcfg
import (
"maps"
"net/netip"
"time"
"tailscale.com/tailcfg/nodecap"
"tailscale.com/types/dnstype"
"tailscale.com/types/key"
"tailscale.com/types/opt"
"tailscale.com/types/structs"
"tailscale.com/types/tkatype"
)
// Clone makes a deep copy of User.
// The result aliases no memory with the original.
func (src *User) Clone() *User {
if src == nil {
return nil
}
dst := new(User)
*dst = *src
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _UserCloneNeedsRegeneration = User(struct {
ID UserID
DisplayName string
ProfilePicURL string
Created time.Time
}{})
// Clone makes a deep copy of Node.
// The result aliases no memory with the original.
func (src *Node) Clone() *Node {
if src == nil {
return nil
}
dst := new(Node)
*dst = *src
dst.KeySignature = append(src.KeySignature[:0:0], src.KeySignature...)
dst.Addresses = append(src.Addresses[:0:0], src.Addresses...)
dst.AllowedIPs = append(src.AllowedIPs[:0:0], src.AllowedIPs...)
dst.Endpoints = append(src.Endpoints[:0:0], src.Endpoints...)
dst.Hostinfo = src.Hostinfo
dst.Tags = append(src.Tags[:0:0], src.Tags...)
dst.PrimaryRoutes = append(src.PrimaryRoutes[:0:0], src.PrimaryRoutes...)
if dst.LastSeen != nil {
dst.LastSeen = new(*src.LastSeen)
}
if dst.Online != nil {
dst.Online = new(*src.Online)
}
dst.Capabilities = append(src.Capabilities[:0:0], src.Capabilities...)
if dst.CapMap != nil {
dst.CapMap = map[nodecap.Cap][]RawMessage{}
for k := range src.CapMap {
dst.CapMap[k] = append([]RawMessage{}, src.CapMap[k]...)
}
}
if dst.SelfNodeV4MasqAddrForThisPeer != nil {
dst.SelfNodeV4MasqAddrForThisPeer = new(*src.SelfNodeV4MasqAddrForThisPeer)
}
if dst.SelfNodeV6MasqAddrForThisPeer != nil {
dst.SelfNodeV6MasqAddrForThisPeer = new(*src.SelfNodeV6MasqAddrForThisPeer)
}
if src.ExitNodeDNSResolvers != nil {
dst.ExitNodeDNSResolvers = make([]*dnstype.Resolver, len(src.ExitNodeDNSResolvers))
for i := range dst.ExitNodeDNSResolvers {
if src.ExitNodeDNSResolvers[i] == nil {
dst.ExitNodeDNSResolvers[i] = nil
} else {
dst.ExitNodeDNSResolvers[i] = src.ExitNodeDNSResolvers[i].Clone()
}
}
}
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _NodeCloneNeedsRegeneration = Node(struct {
ID NodeID
StableID StableNodeID
Name string
User UserID
Sharer UserID
Key key.NodePublic
KeyExpiry time.Time
KeySignature tkatype.MarshaledSignature
Machine key.MachinePublic
DiscoKey key.DiscoPublic
Addresses []netip.Prefix
AllowedIPs []netip.Prefix
Endpoints []netip.AddrPort
LegacyDERPString string
HomeDERP DERPRegionID
Hostinfo HostinfoView
Created time.Time
Cap CapabilityVersion
Tags []string
PrimaryRoutes []netip.Prefix
LastSeen *time.Time
Online *bool
MachineAuthorized bool
Capabilities []nodecap.Cap
CapMap NodeCapMap
UnsignedPeerAPIOnly bool
ComputedName string
computedHostIfDifferent string
ComputedNameWithHost string
DataPlaneAuditLogID string
Expired bool
SelfNodeV4MasqAddrForThisPeer *netip.Addr
SelfNodeV6MasqAddrForThisPeer *netip.Addr
IsWireGuardOnly bool
IsJailed bool
ExitNodeDNSResolvers []*dnstype.Resolver
}{})
// Clone makes a deep copy of Hostinfo.
// The result aliases no memory with the original.
func (src *Hostinfo) Clone() *Hostinfo {
if src == nil {
return nil
}
dst := new(Hostinfo)
*dst = *src
dst.RoutableIPs = append(src.RoutableIPs[:0:0], src.RoutableIPs...)
dst.RequestTags = append(src.RequestTags[:0:0], src.RequestTags...)
dst.WoLMACs = append(src.WoLMACs[:0:0], src.WoLMACs...)
dst.Services = append(src.Services[:0:0], src.Services...)
dst.NetInfo = src.NetInfo.Clone()
dst.SSH_HostKeys = append(src.SSH_HostKeys[:0:0], src.SSH_HostKeys...)
if dst.Location != nil {
dst.Location = new(*src.Location)
}
if dst.TPM != nil {
dst.TPM = new(*src.TPM)
}
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _HostinfoCloneNeedsRegeneration = Hostinfo(struct {
IPNVersion string
FrontendLogID string
BackendLogID string
OS string
OSVersion string
Container opt.Bool
Env string
Distro string
DistroVersion string
DistroCodeName string
App string
Desktop opt.Bool
Package string
DeviceModel string
PushDeviceToken string
Hostname string
ShieldsUp bool
ShareeNode bool
NoLogsNoSupport bool
RemoteConfig bool
WireIngress bool
IngressEnabled bool
AllowsUpdate bool
Machine string
GoArch string
GoArchVar string
GoVersion string
RoutableIPs []netip.Prefix
RequestTags []string
WoLMACs []string
Services []Service
NetInfo *NetInfo
SSH_HostKeys []string
Cloud string
Userspace opt.Bool
UserspaceRouter opt.Bool
AppConnector opt.Bool
ServicesHash string
PeerRelay bool
ExitNodeID StableNodeID
Location *Location
TPM *TPMInfo
StateEncrypted opt.Bool
}{})
// Clone makes a deep copy of NetInfo.
// The result aliases no memory with the original.
func (src *NetInfo) Clone() *NetInfo {
if src == nil {
return nil
}
dst := new(NetInfo)
*dst = *src
dst.DERPLatency = maps.Clone(src.DERPLatency)
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _NetInfoCloneNeedsRegeneration = NetInfo(struct {
MappingVariesByDestIP opt.Bool
WorkingIPv6 opt.Bool
OSHasIPv6 opt.Bool
WorkingUDP opt.Bool
WorkingICMPv4 opt.Bool
HavePortMap bool
UPnP opt.Bool
PMP opt.Bool
PCP opt.Bool
PreferredDERP DERPRegionID
LinkType string
DERPLatency map[string]float64
FirewallMode string
}{})
// Clone makes a deep copy of Login.
// The result aliases no memory with the original.
func (src *Login) Clone() *Login {
if src == nil {
return nil
}
dst := new(Login)
*dst = *src
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _LoginCloneNeedsRegeneration = Login(struct {
_ structs.Incomparable
ID LoginID
Provider string
LoginName string
DisplayName string
ProfilePicURL string
}{})
// Clone makes a deep copy of DNSConfig.
// The result aliases no memory with the original.
func (src *DNSConfig) Clone() *DNSConfig {
if src == nil {
return nil
}
dst := new(DNSConfig)
*dst = *src
if src.Resolvers != nil {
dst.Resolvers = make([]*dnstype.Resolver, len(src.Resolvers))
for i := range dst.Resolvers {
if src.Resolvers[i] == nil {
dst.Resolvers[i] = nil
} else {
dst.Resolvers[i] = src.Resolvers[i].Clone()
}
}
}
if dst.Routes != nil {
dst.Routes = map[string][]*dnstype.Resolver{}
for k, sv := range src.Routes {
if sv == nil {
dst.Routes[k] = nil
continue
}
dst.Routes[k] = make([]*dnstype.Resolver, len(sv))
for i := range sv {
if sv[i] == nil {
dst.Routes[k][i] = nil
} else {
dst.Routes[k][i] = sv[i].Clone()
}
}
}
}
if src.FallbackResolvers != nil {
dst.FallbackResolvers = make([]*dnstype.Resolver, len(src.FallbackResolvers))
for i := range dst.FallbackResolvers {
if src.FallbackResolvers[i] == nil {
dst.FallbackResolvers[i] = nil
} else {
dst.FallbackResolvers[i] = src.FallbackResolvers[i].Clone()
}
}
}
dst.Domains = append(src.Domains[:0:0], src.Domains...)
dst.Nameservers = append(src.Nameservers[:0:0], src.Nameservers...)
dst.CertDomains = append(src.CertDomains[:0:0], src.CertDomains...)
dst.ExtraRecords = append(src.ExtraRecords[:0:0], src.ExtraRecords...)
dst.ExitNodeFilteredSet = append(src.ExitNodeFilteredSet[:0:0], src.ExitNodeFilteredSet...)
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _DNSConfigCloneNeedsRegeneration = DNSConfig(struct {
Resolvers []*dnstype.Resolver
Routes map[string][]*dnstype.Resolver
FallbackResolvers []*dnstype.Resolver
Domains []string
Proxied bool
Nameservers []netip.Addr
CertDomains []string
ExtraRecords []DNSRecord
ExitNodeFilteredSet []string
TempCorpIssue13969 string
}{})
// Clone makes a deep copy of RegisterResponse.
// The result aliases no memory with the original.
func (src *RegisterResponse) Clone() *RegisterResponse {
if src == nil {
return nil
}
dst := new(RegisterResponse)
*dst = *src
dst.NodeKeySignature = append(src.NodeKeySignature[:0:0], src.NodeKeySignature...)
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _RegisterResponseCloneNeedsRegeneration = RegisterResponse(struct {
User User
Login Login
NodeKeyExpired bool
MachineAuthorized bool
AuthURL string
NodeKeySignature tkatype.MarshaledSignature
Error string
}{})
// Clone makes a deep copy of RegisterResponseAuth.
// The result aliases no memory with the original.
func (src *RegisterResponseAuth) Clone() *RegisterResponseAuth {
if src == nil {
return nil
}
dst := new(RegisterResponseAuth)
*dst = *src
if dst.Oauth2Token != nil {
dst.Oauth2Token = new(*src.Oauth2Token)
}
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _RegisterResponseAuthCloneNeedsRegeneration = RegisterResponseAuth(struct {
_ structs.Incomparable
Oauth2Token *Oauth2Token
AuthKey string
}{})
// Clone makes a deep copy of RegisterRequest.
// The result aliases no memory with the original.
func (src *RegisterRequest) Clone() *RegisterRequest {
if src == nil {
return nil
}
dst := new(RegisterRequest)
*dst = *src
dst.Auth = src.Auth.Clone()
dst.Hostinfo = src.Hostinfo.Clone()
dst.NodeKeySignature = append(src.NodeKeySignature[:0:0], src.NodeKeySignature...)
if dst.Timestamp != nil {
dst.Timestamp = new(*src.Timestamp)
}
dst.DeviceCert = append(src.DeviceCert[:0:0], src.DeviceCert...)
dst.Signature = append(src.Signature[:0:0], src.Signature...)
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _RegisterRequestCloneNeedsRegeneration = RegisterRequest(struct {
_ structs.Incomparable
Version CapabilityVersion
NodeKey key.NodePublic
OldNodeKey key.NodePublic
NLKey key.NLPublic
Auth *RegisterResponseAuth
Expiry time.Time
Followup string
Hostinfo *Hostinfo
Ephemeral bool
NodeKeySignature tkatype.MarshaledSignature
SignatureType SignatureType
Timestamp *time.Time
DeviceCert []byte
Signature []byte
Tailnet string
}{})
// Clone makes a deep copy of DERPHomeParams.
// The result aliases no memory with the original.
func (src *DERPHomeParams) Clone() *DERPHomeParams {
if src == nil {
return nil
}
dst := new(DERPHomeParams)
*dst = *src
dst.RegionScore = maps.Clone(src.RegionScore)
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _DERPHomeParamsCloneNeedsRegeneration = DERPHomeParams(struct {
RegionScore map[DERPRegionID]float64
}{})
// Clone makes a deep copy of DERPRegion.
// The result aliases no memory with the original.
func (src *DERPRegion) Clone() *DERPRegion {
if src == nil {
return nil
}
dst := new(DERPRegion)
*dst = *src
if src.Nodes != nil {
dst.Nodes = make([]*DERPNode, len(src.Nodes))
for i := range dst.Nodes {
if src.Nodes[i] == nil {
dst.Nodes[i] = nil
} else {
dst.Nodes[i] = new(*src.Nodes[i])
}
}
}
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _DERPRegionCloneNeedsRegeneration = DERPRegion(struct {
RegionID DERPRegionID
RegionCode string
RegionName string
Latitude float64
Longitude float64
Avoid bool
NoMeasureNoHome bool
Nodes []*DERPNode
}{})
// Clone makes a deep copy of DERPMap.
// The result aliases no memory with the original.
func (src *DERPMap) Clone() *DERPMap {
if src == nil {
return nil
}
dst := new(DERPMap)
*dst = *src
dst.HomeParams = src.HomeParams.Clone()
if dst.Regions != nil {
dst.Regions = map[DERPRegionID]*DERPRegion{}
for k, v := range src.Regions {
if v == nil {
dst.Regions[k] = nil
} else {
dst.Regions[k] = v.Clone()
}
}
}
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _DERPMapCloneNeedsRegeneration = DERPMap(struct {
HomeParams *DERPHomeParams
Regions map[DERPRegionID]*DERPRegion
OmitDefaultRegions bool
}{})
// Clone makes a deep copy of DERPNode.
// The result aliases no memory with the original.
func (src *DERPNode) Clone() *DERPNode {
if src == nil {
return nil
}
dst := new(DERPNode)
*dst = *src
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _DERPNodeCloneNeedsRegeneration = DERPNode(struct {
Name string
RegionID DERPRegionID
HostName string
CertName string
IPv4 string
IPv6 string
STUNPort int
STUNOnly bool
DERPPort int
InsecureForTests bool
STUNTestIP string
CanPort80 bool
}{})
// Clone makes a deep copy of SSHRule.
// The result aliases no memory with the original.
func (src *SSHRule) Clone() *SSHRule {
if src == nil {
return nil
}
dst := new(SSHRule)
*dst = *src
if dst.RuleExpires != nil {
dst.RuleExpires = new(*src.RuleExpires)
}
if src.Principals != nil {
dst.Principals = make([]*SSHPrincipal, len(src.Principals))
for i := range dst.Principals {
if src.Principals[i] == nil {
dst.Principals[i] = nil
} else {
dst.Principals[i] = src.Principals[i].Clone()
}
}
}
dst.SSHUsers = maps.Clone(src.SSHUsers)
dst.Action = src.Action.Clone()
dst.AcceptEnv = append(src.AcceptEnv[:0:0], src.AcceptEnv...)
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _SSHRuleCloneNeedsRegeneration = SSHRule(struct {
RuleExpires *time.Time
Principals []*SSHPrincipal
SSHUsers map[string]string
Action *SSHAction
AcceptEnv []string
}{})
// Clone makes a deep copy of SSHAction.
// The result aliases no memory with the original.
func (src *SSHAction) Clone() *SSHAction {
if src == nil {
return nil
}
dst := new(SSHAction)
*dst = *src
dst.Recorders = append(src.Recorders[:0:0], src.Recorders...)
if dst.OnRecordingFailure != nil {
dst.OnRecordingFailure = new(*src.OnRecordingFailure)
}
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _SSHActionCloneNeedsRegeneration = SSHAction(struct {
Message string
Reject bool
Accept bool
SessionDuration time.Duration
AllowAgentForwarding bool
HoldAndDelegate string
AllowLocalPortForwarding bool
AllowRemotePortForwarding bool
Recorders []netip.AddrPort
OnRecordingFailure *SSHRecorderFailureAction
}{})
// Clone makes a deep copy of SSHPrincipal.
// The result aliases no memory with the original.
func (src *SSHPrincipal) Clone() *SSHPrincipal {
if src == nil {
return nil
}
dst := new(SSHPrincipal)
*dst = *src
dst.UnusedPubKeys = append(src.UnusedPubKeys[:0:0], src.UnusedPubKeys...)
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _SSHPrincipalCloneNeedsRegeneration = SSHPrincipal(struct {
Node StableNodeID
NodeIP string
UserLogin string
Any bool
UnusedPubKeys []string
}{})
// Clone makes a deep copy of ControlDialPlan.
// The result aliases no memory with the original.
func (src *ControlDialPlan) Clone() *ControlDialPlan {
if src == nil {
return nil
}
dst := new(ControlDialPlan)
*dst = *src
dst.Candidates = append(src.Candidates[:0:0], src.Candidates...)
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _ControlDialPlanCloneNeedsRegeneration = ControlDialPlan(struct {
Candidates []ControlIPCandidate
}{})
// Clone makes a deep copy of Location.
// The result aliases no memory with the original.
func (src *Location) Clone() *Location {
if src == nil {
return nil
}
dst := new(Location)
*dst = *src
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _LocationCloneNeedsRegeneration = Location(struct {
Country string
CountryCode string
City string
CityCode string
Latitude float64
Longitude float64
Priority int
}{})
// Clone makes a deep copy of UserProfile.
// The result aliases no memory with the original.
func (src *UserProfile) Clone() *UserProfile {
if src == nil {
return nil
}
dst := new(UserProfile)
*dst = *src
dst.Groups = append(src.Groups[:0:0], src.Groups...)
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _UserProfileCloneNeedsRegeneration = UserProfile(struct {
ID UserID
LoginName string
DisplayName string
ProfilePicURL string
Groups []string
}{})
// Clone makes a deep copy of VIPService.
// The result aliases no memory with the original.
func (src *VIPService) Clone() *VIPService {
if src == nil {
return nil
}
dst := new(VIPService)
*dst = *src
dst.Ports = append(src.Ports[:0:0], src.Ports...)
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _VIPServiceCloneNeedsRegeneration = VIPService(struct {
Name ServiceName
Ports []ProtoPortRange
Active bool
}{})
// Clone makes a deep copy of SSHPolicy.
// The result aliases no memory with the original.
func (src *SSHPolicy) Clone() *SSHPolicy {
if src == nil {
return nil
}
dst := new(SSHPolicy)
*dst = *src
if src.Rules != nil {
dst.Rules = make([]*SSHRule, len(src.Rules))
for i := range dst.Rules {
if src.Rules[i] == nil {
dst.Rules[i] = nil
} else {
dst.Rules[i] = src.Rules[i].Clone()
}
}
}
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _SSHPolicyCloneNeedsRegeneration = SSHPolicy(struct {
Rules []*SSHRule
}{})
// Clone duplicates src into dst and reports whether it succeeded.
// To succeed, <src, dst> must be of types <*T, *T> or <*T, **T>,
// where T is one of User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,RegisterResponseAuth,RegisterRequest,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location,UserProfile,VIPService,SSHPolicy.
func Clone(dst, src any) bool {
switch src := src.(type) {
case *User:
switch dst := dst.(type) {
case *User:
*dst = *src.Clone()
return true
case **User:
*dst = src.Clone()
return true
}
case *Node:
switch dst := dst.(type) {
case *Node:
*dst = *src.Clone()
return true
case **Node:
*dst = src.Clone()
return true
}
case *Hostinfo:
switch dst := dst.(type) {
case *Hostinfo:
*dst = *src.Clone()
return true
case **Hostinfo:
*dst = src.Clone()
return true
}
case *NetInfo:
switch dst := dst.(type) {
case *NetInfo:
*dst = *src.Clone()
return true
case **NetInfo:
*dst = src.Clone()
return true
}
case *Login:
switch dst := dst.(type) {
case *Login:
*dst = *src.Clone()
return true
case **Login:
*dst = src.Clone()
return true
}
case *DNSConfig:
switch dst := dst.(type) {
case *DNSConfig:
*dst = *src.Clone()
return true
case **DNSConfig:
*dst = src.Clone()
return true
}
case *RegisterResponse:
switch dst := dst.(type) {
case *RegisterResponse:
*dst = *src.Clone()
return true
case **RegisterResponse:
*dst = src.Clone()
return true
}
case *RegisterResponseAuth:
switch dst := dst.(type) {
case *RegisterResponseAuth:
*dst = *src.Clone()
return true
case **RegisterResponseAuth:
*dst = src.Clone()
return true
}
case *RegisterRequest:
switch dst := dst.(type) {
case *RegisterRequest:
*dst = *src.Clone()
return true
case **RegisterRequest:
*dst = src.Clone()
return true
}
case *DERPHomeParams:
switch dst := dst.(type) {
case *DERPHomeParams:
*dst = *src.Clone()
return true
case **DERPHomeParams:
*dst = src.Clone()
return true
}
case *DERPRegion:
switch dst := dst.(type) {
case *DERPRegion:
*dst = *src.Clone()
return true
case **DERPRegion:
*dst = src.Clone()
return true
}
case *DERPMap:
switch dst := dst.(type) {
case *DERPMap:
*dst = *src.Clone()
return true
case **DERPMap:
*dst = src.Clone()
return true
}
case *DERPNode:
switch dst := dst.(type) {
case *DERPNode:
*dst = *src.Clone()
return true
case **DERPNode:
*dst = src.Clone()
return true
}
case *SSHRule:
switch dst := dst.(type) {
case *SSHRule:
*dst = *src.Clone()
return true
case **SSHRule:
*dst = src.Clone()
return true
}
case *SSHAction:
switch dst := dst.(type) {
case *SSHAction:
*dst = *src.Clone()
return true
case **SSHAction:
*dst = src.Clone()
return true
}
case *SSHPrincipal:
switch dst := dst.(type) {
case *SSHPrincipal:
*dst = *src.Clone()
return true
case **SSHPrincipal:
*dst = src.Clone()
return true
}
case *ControlDialPlan:
switch dst := dst.(type) {
case *ControlDialPlan:
*dst = *src.Clone()
return true
case **ControlDialPlan:
*dst = src.Clone()
return true
}
case *Location:
switch dst := dst.(type) {
case *Location:
*dst = *src.Clone()
return true
case **Location:
*dst = src.Clone()
return true
}
case *UserProfile:
switch dst := dst.(type) {
case *UserProfile:
*dst = *src.Clone()
return true
case **UserProfile:
*dst = src.Clone()
return true
}
case *VIPService:
switch dst := dst.(type) {
case *VIPService:
*dst = *src.Clone()
return true
case **VIPService:
*dst = src.Clone()
return true
}
case *SSHPolicy:
switch dst := dst.(type) {
case *SSHPolicy:
*dst = *src.Clone()
return true
case **SSHPolicy:
*dst = src.Clone()
return true
}
}
return false
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by tailscale/cmd/viewer; DO NOT EDIT.
package tailcfg
import (
jsonv1 "encoding/json"
"errors"
"net/netip"
"time"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"tailscale.com/tailcfg/nodecap"
"tailscale.com/types/dnstype"
"tailscale.com/types/key"
"tailscale.com/types/opt"
"tailscale.com/types/structs"
"tailscale.com/types/tkatype"
"tailscale.com/types/views"
)
//go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,RegisterResponseAuth,RegisterRequest,DERPHomeParams,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location,UserProfile,VIPService,SSHPolicy
// View returns a read-only view of User.
func (p *User) View() UserView {
return UserView{ж: p}
}
// UserView provides a read-only view over User.
//
// Its methods should only be called if `Valid()` returns true.
type UserView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *User
}
// Valid reports whether v's underlying value is non-nil.
func (v UserView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v UserView) AsStruct() *User {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v UserView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v UserView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *UserView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x User
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *UserView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x User
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v UserView) ID() UserID { return v.ж.ID }
// if non-empty overrides Login field
func (v UserView) DisplayName() string { return v.ж.DisplayName }
// if non-empty overrides Login field
func (v UserView) ProfilePicURL() string { return v.ж.ProfilePicURL }
func (v UserView) Created() time.Time { return v.ж.Created }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _UserViewNeedsRegeneration = User(struct {
ID UserID
DisplayName string
ProfilePicURL string
Created time.Time
}{})
// View returns a read-only view of Node.
func (p *Node) View() NodeView {
return NodeView{ж: p}
}
// NodeView provides a read-only view over Node.
//
// Its methods should only be called if `Valid()` returns true.
type NodeView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *Node
}
// Valid reports whether v's underlying value is non-nil.
func (v NodeView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v NodeView) AsStruct() *Node {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v NodeView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v NodeView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *NodeView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x Node
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *NodeView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x Node
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v NodeView) ID() NodeID { return v.ж.ID }
func (v NodeView) StableID() StableNodeID { return v.ж.StableID }
// Name is the FQDN of the node.
// It is also the MagicDNS name for the node.
// It has a trailing dot.
// e.g. "host.tail-scale.ts.net."
func (v NodeView) Name() string { return v.ж.Name }
// User is the user who created the node. If ACL tags are in use for the
// node then it doesn't reflect the ACL identity that the node is running
// as.
func (v NodeView) User() UserID { return v.ж.User }
// Sharer, if non-zero, is the user who shared this node, if different than User.
func (v NodeView) Sharer() UserID { return v.ж.Sharer }
func (v NodeView) Key() key.NodePublic { return v.ж.Key }
// the zero value if this node does not expire
func (v NodeView) KeyExpiry() time.Time { return v.ж.KeyExpiry }
func (v NodeView) KeySignature() views.ByteSlice[tkatype.MarshaledSignature] {
return views.ByteSliceOf(v.ж.KeySignature)
}
func (v NodeView) Machine() key.MachinePublic { return v.ж.Machine }
func (v NodeView) DiscoKey() key.DiscoPublic { return v.ж.DiscoKey }
// Addresses are the IP addresses of this Node directly.
func (v NodeView) Addresses() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.Addresses) }
// AllowedIPs are the IP ranges to route to this node.
//
// As of CapabilityVersion 112, this may be nil (null or undefined) on the wire
// to mean the same as Addresses. Internally, it is always filled in with
// its possibly-implicit value.
func (v NodeView) AllowedIPs() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.AllowedIPs) }
// IP+port (public via STUN, and local LANs)
func (v NodeView) Endpoints() views.Slice[netip.AddrPort] { return views.SliceOf(v.ж.Endpoints) }
// LegacyDERPString is this node's home LegacyDERPString region ID integer, but shoved into an
// IP:port string for legacy reasons. The IP address is always "127.3.3.40"
// (a loopback address (127) followed by the digits over the letters DERP on
// a QWERTY keyboard (3.3.40)). The "port number" is the home LegacyDERPString region ID
// integer.
//
// Deprecated: HomeDERP has replaced this, but old servers might still send
// this field. See tailscale/tailscale#14636. Do not use this field in code
// other than in the upgradeNode func, which canonicalizes it to HomeDERP
// if it arrives as a LegacyDERPString string on the wire.
func (v NodeView) LegacyDERPString() string { return v.ж.LegacyDERPString }
// HomeDERP is the modern version of the DERP string field, with just an
// integer. The client advertises support for this as of capver 111.
//
// HomeDERP may be zero if not (yet) known, but ideally always be non-zero
// for magicsock connectivity to function normally.
func (v NodeView) HomeDERP() DERPRegionID { return v.ж.HomeDERP }
func (v NodeView) Hostinfo() HostinfoView { return v.ж.Hostinfo }
func (v NodeView) Created() time.Time { return v.ж.Created }
// if non-zero, the node's capability version; old servers might not send
func (v NodeView) Cap() CapabilityVersion { return v.ж.Cap }
// Tags are the list of ACL tags applied to this node.
// Tags take the form of `tag:<value>` where value starts
// with a letter and only contains alphanumerics and dashes `-`.
// Some valid tag examples:
//
// `tag:prod`
// `tag:database`
// `tag:lab-1`
func (v NodeView) Tags() views.Slice[string] { return views.SliceOf(v.ж.Tags) }
// PrimaryRoutes are the routes from AllowedIPs that this node
// is currently the primary subnet router for, as determined
// by the control plane. It does not include the self address
// values from Addresses that are in AllowedIPs.
func (v NodeView) PrimaryRoutes() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.PrimaryRoutes) }
// LastSeen is when the node was last online. It is not
// updated when Online is true. It is nil if the current
// node doesn't have permission to know, or the node
// has never been online.
func (v NodeView) LastSeen() views.ValuePointer[time.Time] {
return views.ValuePointerOf(v.ж.LastSeen)
}
// Online is whether the node is currently connected to the
// coordination server. A value of nil means unknown, or the
// current node doesn't have permission to know.
func (v NodeView) Online() views.ValuePointer[bool] { return views.ValuePointerOf(v.ж.Online) }
// TODO(crawshaw): replace with MachineStatus
func (v NodeView) MachineAuthorized() bool { return v.ж.MachineAuthorized }
// Capabilities are capabilities that the node has.
// They're free-form strings, but should be in the form of URLs/URIs
// such as:
//
// "https://tailscale.com/cap/is-admin"
// "https://tailscale.com/cap/file-sharing"
//
// Deprecated: use CapMap instead. See https://github.com/tailscale/tailscale/issues/11508
func (v NodeView) Capabilities() views.Slice[nodecap.Cap] { return views.SliceOf(v.ж.Capabilities) }
// CapMap is a map of capabilities to their optional argument/data values.
//
// It is valid for a capability to not have any argument/data values; such
// capabilities can be tested for using the HasCap method. These type of
// capabilities are used to indicate that a node has a capability, but there
// is no additional data associated with it. These were previously
// represented by the Capabilities field, but can now be represented by
// CapMap with an empty value.
//
// See [nodecap.Cap] for more information on keys.
//
// Metadata about nodes can be transmitted in 3 ways:
// 1. MapResponse.Node.CapMap describes attributes that affect behavior for
// this node, such as which features have been enabled through the admin
// panel and any associated configuration details.
// 2. MapResponse.PacketFilter(s) describes access (both IP and application
// based) that should be granted to peers.
// 3. MapResponse.Peers[].CapMap describes attributes regarding a peer node,
// such as which features the peer supports or if that peer is preferred
// for a particular task vs other peers that could also be chosen.
func (v NodeView) CapMap() views.MapSlice[nodecap.Cap, RawMessage] {
return views.MapSliceOf(v.ж.CapMap)
}
// UnsignedPeerAPIOnly means that this node is not signed nor subject to TKA
// restrictions. However, in exchange for that privilege, it does not get
// network access. It can only access this node's peerapi, which may not let
// it do anything. It is the tailscaled client's job to double-check the
// MapResponse's PacketFilter to verify that its AllowedIPs will not be
// accepted by the packet filter.
func (v NodeView) UnsignedPeerAPIOnly() bool { return v.ж.UnsignedPeerAPIOnly }
// MagicDNS base name (for normal non-shared-in nodes), FQDN (without trailing dot, for shared-in nodes), or Hostname (if no MagicDNS)
func (v NodeView) ComputedName() string { return v.ж.ComputedName }
// either "ComputedName" or "ComputedName (computedHostIfDifferent)", if computedHostIfDifferent is set
func (v NodeView) ComputedNameWithHost() string { return v.ж.ComputedNameWithHost }
// DataPlaneAuditLogID is the per-node logtail ID used for data plane audit logging.
func (v NodeView) DataPlaneAuditLogID() string { return v.ж.DataPlaneAuditLogID }
// Expired is whether this node's key has expired. Control may send
// this; clients are only allowed to set this from false to true. On
// the client, this is calculated client-side based on a timestamp sent
// from control, to avoid clock skew issues.
func (v NodeView) Expired() bool { return v.ж.Expired }
// SelfNodeV4MasqAddrForThisPeer is the IPv4 that this peer knows the current node as.
// It may be empty if the peer knows the current node by its native
// IPv4 address.
// This field is only populated in a MapResponse for peers and not
// for the current node.
//
// If set, it should be used to masquerade traffic originating from the
// current node to this peer. The masquerade address is only relevant
// for this peer and not for other peers.
//
// This only applies to traffic originating from the current node to the
// peer or any of its subnets. Traffic originating from subnet routes will
// not be masqueraded (e.g. in case of --snat-subnet-routes).
func (v NodeView) SelfNodeV4MasqAddrForThisPeer() views.ValuePointer[netip.Addr] {
return views.ValuePointerOf(v.ж.SelfNodeV4MasqAddrForThisPeer)
}
// SelfNodeV6MasqAddrForThisPeer is the IPv6 that this peer knows the current node as.
// It may be empty if the peer knows the current node by its native
// IPv6 address.
// This field is only populated in a MapResponse for peers and not
// for the current node.
//
// If set, it should be used to masquerade traffic originating from the
// current node to this peer. The masquerade address is only relevant
// for this peer and not for other peers.
//
// This only applies to traffic originating from the current node to the
// peer or any of its subnets. Traffic originating from subnet routes will
// not be masqueraded (e.g. in case of --snat-subnet-routes).
func (v NodeView) SelfNodeV6MasqAddrForThisPeer() views.ValuePointer[netip.Addr] {
return views.ValuePointerOf(v.ж.SelfNodeV6MasqAddrForThisPeer)
}
// IsWireGuardOnly indicates that this is a non-Tailscale WireGuard peer, it
// is not expected to speak Disco or DERP, and it must have Endpoints in
// order to be reachable.
func (v NodeView) IsWireGuardOnly() bool { return v.ж.IsWireGuardOnly }
// IsJailed indicates that this node is jailed and should not be allowed
// initiate connections, however outbound connections to it should still be
// allowed.
func (v NodeView) IsJailed() bool { return v.ж.IsJailed }
// ExitNodeDNSResolvers is the list of DNS servers that should be used when this
// node is marked IsWireGuardOnly and being used as an exit node.
func (v NodeView) ExitNodeDNSResolvers() views.SliceView[*dnstype.Resolver, dnstype.ResolverView] {
return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](v.ж.ExitNodeDNSResolvers)
}
func (v NodeView) Equal(v2 NodeView) bool { return v.ж.Equal(v2.ж) }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _NodeViewNeedsRegeneration = Node(struct {
ID NodeID
StableID StableNodeID
Name string
User UserID
Sharer UserID
Key key.NodePublic
KeyExpiry time.Time
KeySignature tkatype.MarshaledSignature
Machine key.MachinePublic
DiscoKey key.DiscoPublic
Addresses []netip.Prefix
AllowedIPs []netip.Prefix
Endpoints []netip.AddrPort
LegacyDERPString string
HomeDERP DERPRegionID
Hostinfo HostinfoView
Created time.Time
Cap CapabilityVersion
Tags []string
PrimaryRoutes []netip.Prefix
LastSeen *time.Time
Online *bool
MachineAuthorized bool
Capabilities []nodecap.Cap
CapMap NodeCapMap
UnsignedPeerAPIOnly bool
ComputedName string
computedHostIfDifferent string
ComputedNameWithHost string
DataPlaneAuditLogID string
Expired bool
SelfNodeV4MasqAddrForThisPeer *netip.Addr
SelfNodeV6MasqAddrForThisPeer *netip.Addr
IsWireGuardOnly bool
IsJailed bool
ExitNodeDNSResolvers []*dnstype.Resolver
}{})
// View returns a read-only view of Hostinfo.
func (p *Hostinfo) View() HostinfoView {
return HostinfoView{ж: p}
}
// HostinfoView provides a read-only view over Hostinfo.
//
// Its methods should only be called if `Valid()` returns true.
type HostinfoView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *Hostinfo
}
// Valid reports whether v's underlying value is non-nil.
func (v HostinfoView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v HostinfoView) AsStruct() *Hostinfo {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v HostinfoView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v HostinfoView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *HostinfoView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x Hostinfo
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *HostinfoView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x Hostinfo
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// version of this code (in version.Long format)
func (v HostinfoView) IPNVersion() string { return v.ж.IPNVersion }
// logtail ID of frontend instance
func (v HostinfoView) FrontendLogID() string { return v.ж.FrontendLogID }
// logtail ID of backend instance
func (v HostinfoView) BackendLogID() string { return v.ж.BackendLogID }
// operating system the client runs on (a version.OS value)
func (v HostinfoView) OS() string { return v.ж.OS }
// OSVersion is the version of the OS, if available.
//
// For Android, it's like "10", "11", "12", etc. For iOS and macOS it's like
// "15.6.1" or "12.4.0". For Windows it's like "10.0.19044.1889". For
// FreeBSD it's like "12.3-STABLE".
//
// For Linux, prior to Tailscale 1.32, we jammed a bunch of fields into this
// string on Linux, like "Debian 10.4; kernel=xxx; container; env=kn" and so
// on. As of Tailscale 1.32, this is simply the kernel version on Linux, like
// "5.10.0-17-amd64".
func (v HostinfoView) OSVersion() string { return v.ж.OSVersion }
// best-effort whether the client is running in a container
func (v HostinfoView) Container() opt.Bool { return v.ж.Container }
// a hostinfo.EnvType in string form
func (v HostinfoView) Env() string { return v.ж.Env }
// "debian", "ubuntu", "nixos", ...
func (v HostinfoView) Distro() string { return v.ж.Distro }
// "20.04", ...
func (v HostinfoView) DistroVersion() string { return v.ж.DistroVersion }
// "jammy", "bullseye", ...
func (v HostinfoView) DistroCodeName() string { return v.ж.DistroCodeName }
// App is used to disambiguate Tailscale clients that run using tsnet.
func (v HostinfoView) App() string { return v.ж.App }
// if a desktop was detected on Linux
func (v HostinfoView) Desktop() opt.Bool { return v.ж.Desktop }
// Tailscale package to disambiguate ("choco", "appstore", etc; "" for unknown)
func (v HostinfoView) Package() string { return v.ж.Package }
// mobile phone model ("Pixel 3a", "iPhone12,3")
func (v HostinfoView) DeviceModel() string { return v.ж.DeviceModel }
// macOS/iOS APNs device token for notifications (and Android in the future)
func (v HostinfoView) PushDeviceToken() string { return v.ж.PushDeviceToken }
// name of the host the client runs on
func (v HostinfoView) Hostname() string { return v.ж.Hostname }
// indicates whether the host is blocking incoming connections
func (v HostinfoView) ShieldsUp() bool { return v.ж.ShieldsUp }
// indicates this node exists in netmap because it's owned by a shared-to user
func (v HostinfoView) ShareeNode() bool { return v.ж.ShareeNode }
// indicates that the user has opted out of sending logs and support
func (v HostinfoView) NoLogsNoSupport() bool { return v.ж.NoLogsNoSupport }
// RemoteConfig is whether the node has both linked
// feature/remoteconfig into its binary and enabled
// Prefs.RemoteConfig: it has delegated full remote management of
// its prefs and LocalAPI to the tailnet admin via the
// /remoteapi/localapi/* c2n endpoint. See feature/remoteconfig for
// the trust model.
func (v HostinfoView) RemoteConfig() bool { return v.ж.RemoteConfig }
// WireIngress indicates that the node would like to be wired up server-side
// (DNS, etc) to be able to use Tailscale Funnel, even if it's not currently
// enabled. For example, the user might only use it for intermittent
// foreground CLI serve sessions, for which they'd like it to work right
// away, even if it's disabled most of the time. As an optimization, this is
// only sent if IngressEnabled is false, as IngressEnabled implies that this
// option is true.
func (v HostinfoView) WireIngress() bool { return v.ж.WireIngress }
// if the node has any funnel endpoint enabled
func (v HostinfoView) IngressEnabled() bool { return v.ж.IngressEnabled }
// AllowsUpdate reports that the node has opted in to
// admin-console-driven remote updates and that the running binary
// includes client update support (the feature/clientupdate package,
// which tsnet apps don't include).
func (v HostinfoView) AllowsUpdate() bool { return v.ж.AllowsUpdate }
// the current host's machine type (uname -m)
func (v HostinfoView) Machine() string { return v.ж.Machine }
// GOARCH value (of the built binary)
func (v HostinfoView) GoArch() string { return v.ж.GoArch }
// GOARM, GOAMD64, etc (of the built binary)
func (v HostinfoView) GoArchVar() string { return v.ж.GoArchVar }
// Go version binary was built with
func (v HostinfoView) GoVersion() string { return v.ж.GoVersion }
// set of IP ranges this client can route
func (v HostinfoView) RoutableIPs() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.RoutableIPs) }
// set of ACL tags this node wants to claim
func (v HostinfoView) RequestTags() views.Slice[string] { return views.SliceOf(v.ж.RequestTags) }
// MAC address(es) to send Wake-on-LAN packets to wake this node (lowercase hex w/ colons)
func (v HostinfoView) WoLMACs() views.Slice[string] { return views.SliceOf(v.ж.WoLMACs) }
// services advertised by this machine
func (v HostinfoView) Services() views.Slice[Service] { return views.SliceOf(v.ж.Services) }
func (v HostinfoView) NetInfo() NetInfoView { return v.ж.NetInfo.View() }
// if advertised
func (v HostinfoView) SSH_HostKeys() views.Slice[string] { return views.SliceOf(v.ж.SSH_HostKeys) }
func (v HostinfoView) Cloud() string { return v.ж.Cloud }
// if the client is running in userspace (netstack) mode
func (v HostinfoView) Userspace() opt.Bool { return v.ж.Userspace }
// if the client's subnet router is running in userspace (netstack) mode
func (v HostinfoView) UserspaceRouter() opt.Bool { return v.ж.UserspaceRouter }
// if the client is running the app-connector service
func (v HostinfoView) AppConnector() opt.Bool { return v.ж.AppConnector }
// opaque hash of the most recent list of tailnet services, change in hash indicates config should be fetched via c2n
func (v HostinfoView) ServicesHash() string { return v.ж.ServicesHash }
// if the client is willing to relay traffic for other peers
func (v HostinfoView) PeerRelay() bool { return v.ж.PeerRelay }
// the client’s selected exit node, empty when unselected.
func (v HostinfoView) ExitNodeID() StableNodeID { return v.ж.ExitNodeID }
// Location represents geographical location data about a
// Tailscale host. Location is optional and only set if
// explicitly declared by a node.
func (v HostinfoView) Location() LocationView { return v.ж.Location.View() }
// TPM device metadata, if available
func (v HostinfoView) TPM() views.ValuePointer[TPMInfo] { return views.ValuePointerOf(v.ж.TPM) }
// StateEncrypted reports whether the node state is stored encrypted on
// disk. The actual mechanism is platform-specific:
// - Apple nodes use the Keychain
// - Linux and Windows nodes use the TPM
// - Android apps use EncryptedSharedPreferences
func (v HostinfoView) StateEncrypted() opt.Bool { return v.ж.StateEncrypted }
func (v HostinfoView) Equal(v2 HostinfoView) bool { return v.ж.Equal(v2.ж) }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _HostinfoViewNeedsRegeneration = Hostinfo(struct {
IPNVersion string
FrontendLogID string
BackendLogID string
OS string
OSVersion string
Container opt.Bool
Env string
Distro string
DistroVersion string
DistroCodeName string
App string
Desktop opt.Bool
Package string
DeviceModel string
PushDeviceToken string
Hostname string
ShieldsUp bool
ShareeNode bool
NoLogsNoSupport bool
RemoteConfig bool
WireIngress bool
IngressEnabled bool
AllowsUpdate bool
Machine string
GoArch string
GoArchVar string
GoVersion string
RoutableIPs []netip.Prefix
RequestTags []string
WoLMACs []string
Services []Service
NetInfo *NetInfo
SSH_HostKeys []string
Cloud string
Userspace opt.Bool
UserspaceRouter opt.Bool
AppConnector opt.Bool
ServicesHash string
PeerRelay bool
ExitNodeID StableNodeID
Location *Location
TPM *TPMInfo
StateEncrypted opt.Bool
}{})
// View returns a read-only view of NetInfo.
func (p *NetInfo) View() NetInfoView {
return NetInfoView{ж: p}
}
// NetInfoView provides a read-only view over NetInfo.
//
// Its methods should only be called if `Valid()` returns true.
type NetInfoView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *NetInfo
}
// Valid reports whether v's underlying value is non-nil.
func (v NetInfoView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v NetInfoView) AsStruct() *NetInfo {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v NetInfoView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v NetInfoView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *NetInfoView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x NetInfo
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *NetInfoView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x NetInfo
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// MappingVariesByDestIP says whether the host's NAT mappings
// vary based on the destination IP.
func (v NetInfoView) MappingVariesByDestIP() opt.Bool { return v.ж.MappingVariesByDestIP }
// WorkingIPv6 is whether the host has IPv6 internet connectivity.
func (v NetInfoView) WorkingIPv6() opt.Bool { return v.ж.WorkingIPv6 }
// OSHasIPv6 is whether the OS supports IPv6 at all, regardless of
// whether IPv6 internet connectivity is available.
func (v NetInfoView) OSHasIPv6() opt.Bool { return v.ж.OSHasIPv6 }
// WorkingUDP is whether the host has UDP internet connectivity.
func (v NetInfoView) WorkingUDP() opt.Bool { return v.ж.WorkingUDP }
// WorkingICMPv4 is whether ICMPv4 works.
// Empty means not checked.
func (v NetInfoView) WorkingICMPv4() opt.Bool { return v.ж.WorkingICMPv4 }
// HavePortMap is whether we have an existing portmap open
// (UPnP, PMP, or PCP).
func (v NetInfoView) HavePortMap() bool { return v.ж.HavePortMap }
// UPnP is whether UPnP appears present on the LAN.
// Empty means not checked.
func (v NetInfoView) UPnP() opt.Bool { return v.ж.UPnP }
// PMP is whether NAT-PMP appears present on the LAN.
// Empty means not checked.
func (v NetInfoView) PMP() opt.Bool { return v.ж.PMP }
// PCP is whether PCP appears present on the LAN.
// Empty means not checked.
func (v NetInfoView) PCP() opt.Bool { return v.ж.PCP }
// PreferredDERP is this node's preferred (home) DERP region ID.
// This is where the node expects to be contacted to begin a
// peer-to-peer connection. The node might be temporarily
// connected to multiple DERP servers (to speak to other nodes
// that are located elsewhere) but PreferredDERP is the region ID
// that the node subscribes to traffic at.
// Zero means disconnected or unknown.
func (v NetInfoView) PreferredDERP() DERPRegionID { return v.ж.PreferredDERP }
// LinkType is the current link type, if known.
func (v NetInfoView) LinkType() string { return v.ж.LinkType }
// DERPLatency is the fastest recent time to reach various
// DERP STUN servers, in seconds. The map key is the
// "regionID-v4" or "-v6"; it was previously the DERP server's
// STUN host:port.
//
// This should only be updated rarely, or when there's a
// material change, as any change here also gets uploaded to
// the control plane.
func (v NetInfoView) DERPLatency() views.Map[string, float64] { return views.MapOf(v.ж.DERPLatency) }
// FirewallMode encodes both which firewall mode was selected and why.
// It is Linux-specific (at least as of 2023-08-19) and is meant to help
// debug iptables-vs-nftables issues. The string is of the form
// "{nft,ift}-REASON", like "nft-forced" or "ipt-default". Empty means
// either not Linux or a configuration in which the host firewall rules
// are not managed by tailscaled.
func (v NetInfoView) FirewallMode() string { return v.ж.FirewallMode }
func (v NetInfoView) String() string { return v.ж.String() }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _NetInfoViewNeedsRegeneration = NetInfo(struct {
MappingVariesByDestIP opt.Bool
WorkingIPv6 opt.Bool
OSHasIPv6 opt.Bool
WorkingUDP opt.Bool
WorkingICMPv4 opt.Bool
HavePortMap bool
UPnP opt.Bool
PMP opt.Bool
PCP opt.Bool
PreferredDERP DERPRegionID
LinkType string
DERPLatency map[string]float64
FirewallMode string
}{})
// View returns a read-only view of Login.
func (p *Login) View() LoginView {
return LoginView{ж: p}
}
// LoginView provides a read-only view over Login.
//
// Its methods should only be called if `Valid()` returns true.
type LoginView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *Login
}
// Valid reports whether v's underlying value is non-nil.
func (v LoginView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v LoginView) AsStruct() *Login {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v LoginView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v LoginView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *LoginView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x Login
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *LoginView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x Login
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// unused in the Tailscale client
func (v LoginView) ID() LoginID { return v.ж.ID }
// "google", "github", "okta_foo", etc.
func (v LoginView) Provider() string { return v.ж.Provider }
// an email address or "email-ish" string (like alice@github)
func (v LoginView) LoginName() string { return v.ж.LoginName }
// from the IdP
func (v LoginView) DisplayName() string { return v.ж.DisplayName }
// from the IdP
func (v LoginView) ProfilePicURL() string { return v.ж.ProfilePicURL }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _LoginViewNeedsRegeneration = Login(struct {
_ structs.Incomparable
ID LoginID
Provider string
LoginName string
DisplayName string
ProfilePicURL string
}{})
// View returns a read-only view of DNSConfig.
func (p *DNSConfig) View() DNSConfigView {
return DNSConfigView{ж: p}
}
// DNSConfigView provides a read-only view over DNSConfig.
//
// Its methods should only be called if `Valid()` returns true.
type DNSConfigView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *DNSConfig
}
// Valid reports whether v's underlying value is non-nil.
func (v DNSConfigView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v DNSConfigView) AsStruct() *DNSConfig {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v DNSConfigView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v DNSConfigView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *DNSConfigView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x DNSConfig
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *DNSConfigView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x DNSConfig
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// Resolvers are the DNS resolvers to use, in order of preference.
func (v DNSConfigView) Resolvers() views.SliceView[*dnstype.Resolver, dnstype.ResolverView] {
return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](v.ж.Resolvers)
}
// Routes maps DNS name suffixes to a set of DNS resolvers to
// use. It is used to implement "split DNS" and other advanced DNS
// routing overlays.
//
// Map keys are fully-qualified DNS name suffixes; they may
// optionally contain a trailing dot but no leading dot.
//
// If the value is an empty slice, that means the suffix should still
// be handled by Tailscale's built-in resolver (100.100.100.100), such
// as for the purpose of handling ExtraRecords.
func (v DNSConfigView) Routes() views.MapFn[string, []*dnstype.Resolver, views.SliceView[*dnstype.Resolver, dnstype.ResolverView]] {
return views.MapFnOf(v.ж.Routes, func(t []*dnstype.Resolver) views.SliceView[*dnstype.Resolver, dnstype.ResolverView] {
return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](t)
})
}
// FallbackResolvers is like Resolvers, but is only used if a
// split DNS configuration is requested in a configuration that
// doesn't work yet without explicit default resolvers.
// https://github.com/tailscale/tailscale/issues/1743
func (v DNSConfigView) FallbackResolvers() views.SliceView[*dnstype.Resolver, dnstype.ResolverView] {
return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](v.ж.FallbackResolvers)
}
// Domains are the search domains to use.
// Search domains must be FQDNs, but *without* the trailing dot.
func (v DNSConfigView) Domains() views.Slice[string] { return views.SliceOf(v.ж.Domains) }
// Proxied turns on automatic resolution of hostnames for devices
// in the network map, aka MagicDNS.
// Despite the (legacy) name, does not necessarily cause request
// proxying to be enabled.
func (v DNSConfigView) Proxied() bool { return v.ж.Proxied }
// Nameservers are the IP addresses of the global nameservers to use.
//
// Deprecated: this is only set and used by MapRequest.Version >=9 and <14. Use Resolvers instead.
func (v DNSConfigView) Nameservers() views.Slice[netip.Addr] { return views.SliceOf(v.ж.Nameservers) }
// CertDomains are the set of DNS names for which the control
// plane server will assist with provisioning TLS
// certificates. See SetDNSRequest, which can be used to
// answer dns-01 ACME challenges for e.g. LetsEncrypt.
// These names are FQDNs without trailing periods, and without
// any "_acme-challenge." prefix.
func (v DNSConfigView) CertDomains() views.Slice[string] { return views.SliceOf(v.ж.CertDomains) }
// ExtraRecords contains extra DNS records to add to the
// MagicDNS config.
func (v DNSConfigView) ExtraRecords() views.Slice[DNSRecord] { return views.SliceOf(v.ж.ExtraRecords) }
// ExitNodeFilteredSuffixes are the DNS suffixes that the
// node, when being an exit node DNS proxy, should not answer.
//
// The entries do not contain trailing periods and are always
// all lowercase.
//
// If an entry starts with a period, it's a suffix match (but
// suffix ".a.b" doesn't match "a.b"; a prefix is required).
//
// If an entry does not start with a period, it's an exact
// match.
//
// Matches are case insensitive.
func (v DNSConfigView) ExitNodeFilteredSet() views.Slice[string] {
return views.SliceOf(v.ж.ExitNodeFilteredSet)
}
// TempCorpIssue13969 is a temporary (2023-08-16) field for an internal hack day prototype.
// It contains a user inputed URL that should have a list of domains to be blocked.
// See https://github.com/tailscale/corp/issues/13969.
func (v DNSConfigView) TempCorpIssue13969() string { return v.ж.TempCorpIssue13969 }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _DNSConfigViewNeedsRegeneration = DNSConfig(struct {
Resolvers []*dnstype.Resolver
Routes map[string][]*dnstype.Resolver
FallbackResolvers []*dnstype.Resolver
Domains []string
Proxied bool
Nameservers []netip.Addr
CertDomains []string
ExtraRecords []DNSRecord
ExitNodeFilteredSet []string
TempCorpIssue13969 string
}{})
// View returns a read-only view of RegisterResponse.
func (p *RegisterResponse) View() RegisterResponseView {
return RegisterResponseView{ж: p}
}
// RegisterResponseView provides a read-only view over RegisterResponse.
//
// Its methods should only be called if `Valid()` returns true.
type RegisterResponseView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *RegisterResponse
}
// Valid reports whether v's underlying value is non-nil.
func (v RegisterResponseView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v RegisterResponseView) AsStruct() *RegisterResponse {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v RegisterResponseView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v RegisterResponseView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *RegisterResponseView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x RegisterResponse
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *RegisterResponseView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x RegisterResponse
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v RegisterResponseView) User() User { return v.ж.User }
func (v RegisterResponseView) Login() Login { return v.ж.Login }
// if true, the NodeKey needs to be replaced
func (v RegisterResponseView) NodeKeyExpired() bool { return v.ж.NodeKeyExpired }
// TODO(crawshaw): move to using MachineStatus
func (v RegisterResponseView) MachineAuthorized() bool { return v.ж.MachineAuthorized }
// if set, authorization pending
func (v RegisterResponseView) AuthURL() string { return v.ж.AuthURL }
// If set, this is the current node-key signature that needs to be
// re-signed for the node's new node-key.
func (v RegisterResponseView) NodeKeySignature() views.ByteSlice[tkatype.MarshaledSignature] {
return views.ByteSliceOf(v.ж.NodeKeySignature)
}
// Error indicates that authorization failed. If this is non-empty,
// other status fields should be ignored.
func (v RegisterResponseView) Error() string { return v.ж.Error }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _RegisterResponseViewNeedsRegeneration = RegisterResponse(struct {
User User
Login Login
NodeKeyExpired bool
MachineAuthorized bool
AuthURL string
NodeKeySignature tkatype.MarshaledSignature
Error string
}{})
// View returns a read-only view of RegisterResponseAuth.
func (p *RegisterResponseAuth) View() RegisterResponseAuthView {
return RegisterResponseAuthView{ж: p}
}
// RegisterResponseAuthView provides a read-only view over RegisterResponseAuth.
//
// Its methods should only be called if `Valid()` returns true.
type RegisterResponseAuthView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *RegisterResponseAuth
}
// Valid reports whether v's underlying value is non-nil.
func (v RegisterResponseAuthView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v RegisterResponseAuthView) AsStruct() *RegisterResponseAuth {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v RegisterResponseAuthView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v RegisterResponseAuthView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *RegisterResponseAuthView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x RegisterResponseAuth
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *RegisterResponseAuthView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x RegisterResponseAuth
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// used by pre-1.66 Android only
func (v RegisterResponseAuthView) Oauth2Token() views.ValuePointer[Oauth2Token] {
return views.ValuePointerOf(v.ж.Oauth2Token)
}
func (v RegisterResponseAuthView) AuthKey() string { return v.ж.AuthKey }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _RegisterResponseAuthViewNeedsRegeneration = RegisterResponseAuth(struct {
_ structs.Incomparable
Oauth2Token *Oauth2Token
AuthKey string
}{})
// View returns a read-only view of RegisterRequest.
func (p *RegisterRequest) View() RegisterRequestView {
return RegisterRequestView{ж: p}
}
// RegisterRequestView provides a read-only view over RegisterRequest.
//
// Its methods should only be called if `Valid()` returns true.
type RegisterRequestView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *RegisterRequest
}
// Valid reports whether v's underlying value is non-nil.
func (v RegisterRequestView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v RegisterRequestView) AsStruct() *RegisterRequest {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v RegisterRequestView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v RegisterRequestView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *RegisterRequestView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x RegisterRequest
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *RegisterRequestView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x RegisterRequest
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// Version is the client's capabilities when using the Noise
// transport.
//
// When using the original nacl crypto_box transport, the
// value must be 1.
func (v RegisterRequestView) Version() CapabilityVersion { return v.ж.Version }
func (v RegisterRequestView) NodeKey() key.NodePublic { return v.ж.NodeKey }
func (v RegisterRequestView) OldNodeKey() key.NodePublic { return v.ж.OldNodeKey }
func (v RegisterRequestView) NLKey() key.NLPublic { return v.ж.NLKey }
func (v RegisterRequestView) Auth() RegisterResponseAuthView { return v.ж.Auth.View() }
// Expiry optionally specifies the requested key expiry.
// The server policy may override.
// As a special case, if Expiry is in the past and NodeKey is
// the node's current key, the key is expired.
func (v RegisterRequestView) Expiry() time.Time { return v.ж.Expiry }
// response waits until AuthURL is visited
func (v RegisterRequestView) Followup() string { return v.ж.Followup }
func (v RegisterRequestView) Hostinfo() HostinfoView { return v.ж.Hostinfo.View() }
// Ephemeral is whether the client is requesting that this
// node be considered ephemeral and be automatically deleted
// when it stops being active.
func (v RegisterRequestView) Ephemeral() bool { return v.ж.Ephemeral }
// NodeKeySignature is the node's own node-key signature, re-signed
// for its new node key using its tailnet-lock key.
//
// This field is set when the client retries registration after learning
// its NodeKeySignature (which is in need of rotation).
func (v RegisterRequestView) NodeKeySignature() views.ByteSlice[tkatype.MarshaledSignature] {
return views.ByteSliceOf(v.ж.NodeKeySignature)
}
// The following fields are not used for SignatureNone and are required for
// SignatureV1:
func (v RegisterRequestView) SignatureType() SignatureType { return v.ж.SignatureType }
// creation time of request to prevent replay
func (v RegisterRequestView) Timestamp() views.ValuePointer[time.Time] {
return views.ValuePointerOf(v.ж.Timestamp)
}
// X.509 certificate for client device
func (v RegisterRequestView) DeviceCert() views.ByteSlice[[]byte] {
return views.ByteSliceOf(v.ж.DeviceCert)
}
// as described by SignatureType
func (v RegisterRequestView) Signature() views.ByteSlice[[]byte] {
return views.ByteSliceOf(v.ж.Signature)
}
// Tailnet is an optional identifier specifying the name of the recommended or required
// network that the node should join. Its exact form should not be depended on; new
// forms are coming later. The identifier is generally a domain name (for an organization)
// or e-mail address (for a personal account on a shared e-mail provider). It is the same name
// used by the API, as described in /api.md#tailnet.
// If Tailnet begins with the prefix "required:" then the server should prevent logging in to a different
// network than the one specified. Otherwise, the server should recommend the specified network
// but still permit logging in to other networks.
// If empty, no recommendation is offered to the server and the login page should show all options.
func (v RegisterRequestView) Tailnet() string { return v.ж.Tailnet }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _RegisterRequestViewNeedsRegeneration = RegisterRequest(struct {
_ structs.Incomparable
Version CapabilityVersion
NodeKey key.NodePublic
OldNodeKey key.NodePublic
NLKey key.NLPublic
Auth *RegisterResponseAuth
Expiry time.Time
Followup string
Hostinfo *Hostinfo
Ephemeral bool
NodeKeySignature tkatype.MarshaledSignature
SignatureType SignatureType
Timestamp *time.Time
DeviceCert []byte
Signature []byte
Tailnet string
}{})
// View returns a read-only view of DERPHomeParams.
func (p *DERPHomeParams) View() DERPHomeParamsView {
return DERPHomeParamsView{ж: p}
}
// DERPHomeParamsView provides a read-only view over DERPHomeParams.
//
// Its methods should only be called if `Valid()` returns true.
type DERPHomeParamsView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *DERPHomeParams
}
// Valid reports whether v's underlying value is non-nil.
func (v DERPHomeParamsView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v DERPHomeParamsView) AsStruct() *DERPHomeParams {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v DERPHomeParamsView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v DERPHomeParamsView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *DERPHomeParamsView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x DERPHomeParams
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *DERPHomeParamsView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x DERPHomeParams
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// RegionScore scales latencies of DERP regions by a given scaling
// factor when determining which region to use as the home
// ("preferred") DERP. Scores in the range (0, 1) will cause this
// region to be proportionally more preferred, and scores in the range
// (1, ∞) will penalize a region.
//
// If a region is not present in this map, it is treated as having a
// score of 1.0.
//
// Scores should not be 0 or negative; such scores will be ignored.
//
// A nil map means no change from the previous value (if any); an empty
// non-nil map can be sent to reset all scores back to 1.0.
func (v DERPHomeParamsView) RegionScore() views.Map[DERPRegionID, float64] {
return views.MapOf(v.ж.RegionScore)
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _DERPHomeParamsViewNeedsRegeneration = DERPHomeParams(struct {
RegionScore map[DERPRegionID]float64
}{})
// View returns a read-only view of DERPRegion.
func (p *DERPRegion) View() DERPRegionView {
return DERPRegionView{ж: p}
}
// DERPRegionView provides a read-only view over DERPRegion.
//
// Its methods should only be called if `Valid()` returns true.
type DERPRegionView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *DERPRegion
}
// Valid reports whether v's underlying value is non-nil.
func (v DERPRegionView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v DERPRegionView) AsStruct() *DERPRegion {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v DERPRegionView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v DERPRegionView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *DERPRegionView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x DERPRegion
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *DERPRegionView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x DERPRegion
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// RegionID is a unique integer for a geographic region.
//
// It corresponds to the legacy derpN.tailscale.com hostnames
// used by older clients. (Older clients will continue to resolve
// derpN.tailscale.com when contacting peers, rather than use
// the server-provided DERPMap)
//
// RegionIDs must be non-zero, positive, and guaranteed to fit
// in a JavaScript number.
//
// RegionIDs in range 900-999 are reserved for end users to run their
// own DERP nodes.
func (v DERPRegionView) RegionID() DERPRegionID { return v.ж.RegionID }
// RegionCode is a short name for the region. It's usually a popular
// city or airport code in the region: "nyc", "sf", "sin",
// "fra", etc.
func (v DERPRegionView) RegionCode() string { return v.ж.RegionCode }
// RegionName is a long English name for the region: "New York City",
// "San Francisco", "Singapore", "Frankfurt", etc.
func (v DERPRegionView) RegionName() string { return v.ж.RegionName }
// Latitude, Longitude are optional geographical coordinates of the DERP region's city, in degrees.
func (v DERPRegionView) Latitude() float64 { return v.ж.Latitude }
func (v DERPRegionView) Longitude() float64 { return v.ж.Longitude }
// Avoid is whether the client should avoid picking this as its home region.
// The region should only be used if a peer is there. Clients already using
// this region as their home should migrate away to a new region without
// Avoid set.
//
// Deprecated: because of bugs in past implementations combined with unclear
// docs that caused people to think the bugs were intentional, this field is
// deprecated. It was never supposed to cause STUN/DERP measurement probes,
// but due to bugs, it sometimes did. And then some parts of the code began
// to rely on that property. But then we were unable to use this field for
// its original purpose, nor its later imagined purpose, because various
// parts of the codebase thought it meant one thing and others thought it
// meant another. But it did something in the middle instead. So we're retiring
// it. Use NoMeasureNoHome instead.
func (v DERPRegionView) Avoid() bool { return v.ж.Avoid }
// NoMeasureNoHome says that this regions should not be measured for its
// latency distance (STUN, HTTPS, etc) or availability (e.g. captive portal
// checks) and should never be selected as the node's home region. However,
// if a peer declares this region as its home, then this client is allowed
// to connect to it for the purpose of communicating with that peer.
//
// This is what the now deprecated Avoid bool was supposed to mean
// originally but had implementation bugs and documentation omissions.
func (v DERPRegionView) NoMeasureNoHome() bool { return v.ж.NoMeasureNoHome }
// Nodes are the DERP nodes running in this region, in
// priority order for the current client. Client TLS
// connections should ideally only go to the first entry
// (falling back to the second if necessary). STUN packets
// should go to the first 1 or 2.
//
// If nodes within a region route packets amongst themselves,
// but not to other regions. That said, each user/domain
// should get a the same preferred node order, so if all nodes
// for a user/network pick the first one (as they should, when
// things are healthy), the inter-cluster routing is minimal
// to zero.
func (v DERPRegionView) Nodes() views.SliceView[*DERPNode, DERPNodeView] {
return views.SliceOfViews[*DERPNode, DERPNodeView](v.ж.Nodes)
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _DERPRegionViewNeedsRegeneration = DERPRegion(struct {
RegionID DERPRegionID
RegionCode string
RegionName string
Latitude float64
Longitude float64
Avoid bool
NoMeasureNoHome bool
Nodes []*DERPNode
}{})
// View returns a read-only view of DERPMap.
func (p *DERPMap) View() DERPMapView {
return DERPMapView{ж: p}
}
// DERPMapView provides a read-only view over DERPMap.
//
// Its methods should only be called if `Valid()` returns true.
type DERPMapView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *DERPMap
}
// Valid reports whether v's underlying value is non-nil.
func (v DERPMapView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v DERPMapView) AsStruct() *DERPMap {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v DERPMapView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v DERPMapView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *DERPMapView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x DERPMap
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *DERPMapView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x DERPMap
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// HomeParams, if non-nil, is a change in home parameters.
//
// The rest of the DEPRMap fields, if zero, means unchanged.
func (v DERPMapView) HomeParams() DERPHomeParamsView { return v.ж.HomeParams.View() }
// Regions is the set of geographic regions running DERP node(s).
//
// It's keyed by the DERPRegion.RegionID.
//
// The numbers are not necessarily contiguous.
func (v DERPMapView) Regions() views.MapFn[DERPRegionID, *DERPRegion, DERPRegionView] {
return views.MapFnOf(v.ж.Regions, func(t *DERPRegion) DERPRegionView {
return t.View()
})
}
// OmitDefaultRegions specifies to not use Tailscale's DERP servers, and only use those
// specified in this DERPMap. If there are none set outside of the defaults, this is a noop.
//
// This field is only meaningful if the Regions map is non-nil (indicating a change).
func (v DERPMapView) OmitDefaultRegions() bool { return v.ж.OmitDefaultRegions }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _DERPMapViewNeedsRegeneration = DERPMap(struct {
HomeParams *DERPHomeParams
Regions map[DERPRegionID]*DERPRegion
OmitDefaultRegions bool
}{})
// View returns a read-only view of DERPNode.
func (p *DERPNode) View() DERPNodeView {
return DERPNodeView{ж: p}
}
// DERPNodeView provides a read-only view over DERPNode.
//
// Its methods should only be called if `Valid()` returns true.
type DERPNodeView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *DERPNode
}
// Valid reports whether v's underlying value is non-nil.
func (v DERPNodeView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v DERPNodeView) AsStruct() *DERPNode {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v DERPNodeView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v DERPNodeView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *DERPNodeView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x DERPNode
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *DERPNodeView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x DERPNode
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// Name is a unique node name (across all regions).
// It is not a host name.
// It's typically of the form "1b", "2a", "3b", etc. (region
// ID + suffix within that region)
func (v DERPNodeView) Name() string { return v.ж.Name }
// RegionID is the RegionID of the DERPRegion that this node
// is running in.
func (v DERPNodeView) RegionID() DERPRegionID { return v.ж.RegionID }
// HostName is the DERP node's hostname.
//
// It is required but need not be unique; multiple nodes may
// have the same HostName but vary in configuration otherwise.
func (v DERPNodeView) HostName() string { return v.ж.HostName }
// CertName optionally specifies the expected TLS cert common
// name. If empty, HostName is used. If CertName is non-empty,
// HostName is only used for the TCP dial (if IPv4/IPv6 are
// not present) + TLS ClientHello.
//
// As a special case, if CertName starts with "sha256-raw:",
// then the rest of the string is a hex-encoded SHA256 of the
// cert to expect. This is used for self-signed certs.
// In this case, the HostName field will typically be an IP
// address literal.
func (v DERPNodeView) CertName() string { return v.ж.CertName }
// IPv4 optionally forces an IPv4 address to use, instead of using DNS.
// If empty, A record(s) from DNS lookups of HostName are used.
// If the string is not an IPv4 address, IPv4 is not used; the
// conventional string to disable IPv4 (and not use DNS) is
// "none".
func (v DERPNodeView) IPv4() string { return v.ж.IPv4 }
// IPv6 optionally forces an IPv6 address to use, instead of using DNS.
// If empty, AAAA record(s) from DNS lookups of HostName are used.
// If the string is not an IPv6 address, IPv6 is not used; the
// conventional string to disable IPv6 (and not use DNS) is
// "none".
func (v DERPNodeView) IPv6() string { return v.ж.IPv6 }
// Port optionally specifies a STUN port to use.
// Zero means 3478.
// To disable STUN on this node, use -1.
func (v DERPNodeView) STUNPort() int { return v.ж.STUNPort }
// STUNOnly marks a node as only a STUN server and not a DERP
// server.
func (v DERPNodeView) STUNOnly() bool { return v.ж.STUNOnly }
// DERPPort optionally provides an alternate TLS port number
// for the DERP HTTPS server.
//
// If zero, 443 is used.
func (v DERPNodeView) DERPPort() int { return v.ж.DERPPort }
// InsecureForTests is used by unit tests to disable TLS verification.
// It should not be set by users.
func (v DERPNodeView) InsecureForTests() bool { return v.ж.InsecureForTests }
// STUNTestIP is used in tests to override the STUN server's IP.
// If empty, it's assumed to be the same as the DERP server.
func (v DERPNodeView) STUNTestIP() string { return v.ж.STUNTestIP }
// CanPort80 specifies whether this DERP node is accessible over HTTP
// on port 80 specifically. This is used for captive portal checks.
func (v DERPNodeView) CanPort80() bool { return v.ж.CanPort80 }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _DERPNodeViewNeedsRegeneration = DERPNode(struct {
Name string
RegionID DERPRegionID
HostName string
CertName string
IPv4 string
IPv6 string
STUNPort int
STUNOnly bool
DERPPort int
InsecureForTests bool
STUNTestIP string
CanPort80 bool
}{})
// View returns a read-only view of SSHRule.
func (p *SSHRule) View() SSHRuleView {
return SSHRuleView{ж: p}
}
// SSHRuleView provides a read-only view over SSHRule.
//
// Its methods should only be called if `Valid()` returns true.
type SSHRuleView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *SSHRule
}
// Valid reports whether v's underlying value is non-nil.
func (v SSHRuleView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v SSHRuleView) AsStruct() *SSHRule {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v SSHRuleView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v SSHRuleView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *SSHRuleView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x SSHRule
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *SSHRuleView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x SSHRule
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// RuleExpires, if non-nil, is when this rule expires.
//
// For example, a (principal,sshuser) tuple might be granted
// prompt-free SSH access for N minutes, so this rule would be
// before a expiration-free rule for the same principal that
// required an auth prompt. This permits the control plane to
// be out of the path for already-authorized SSH pairs.
//
// Once a rule matches, the lifetime of any accepting connection
// is subject to the SSHAction.SessionExpires time, if any.
func (v SSHRuleView) RuleExpires() views.ValuePointer[time.Time] {
return views.ValuePointerOf(v.ж.RuleExpires)
}
// Principals matches an incoming connection. If the connection
// matches anything in this list and also matches SSHUsers,
// then Action is applied.
func (v SSHRuleView) Principals() views.SliceView[*SSHPrincipal, SSHPrincipalView] {
return views.SliceOfViews[*SSHPrincipal, SSHPrincipalView](v.ж.Principals)
}
// SSHUsers are the SSH users that this rule matches. It is a
// map from either ssh-user|"*" => local-user. The map must
// contain a key for either ssh-user or, as a fallback, "*" to
// match anything. If it does, the map entry's value is the
// actual user that's logged in.
// If the map value is the empty string (for either the
// requested SSH user or "*"), the rule doesn't match.
// If the map value is "=", it means the ssh-user should map
// directly to the local-user.
// It may be nil if the Action is reject.
func (v SSHRuleView) SSHUsers() views.Map[string, string] { return views.MapOf(v.ж.SSHUsers) }
// Action is the outcome to task.
// A nil or invalid action means to deny.
func (v SSHRuleView) Action() SSHActionView { return v.ж.Action.View() }
// AcceptEnv is a slice of environment variable names that are allowlisted
// for the SSH rule in the policy file.
//
// AcceptEnv values may contain * and ? wildcard characters which match against
// an arbitrary number of characters or a single character respectively.
func (v SSHRuleView) AcceptEnv() views.Slice[string] { return views.SliceOf(v.ж.AcceptEnv) }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _SSHRuleViewNeedsRegeneration = SSHRule(struct {
RuleExpires *time.Time
Principals []*SSHPrincipal
SSHUsers map[string]string
Action *SSHAction
AcceptEnv []string
}{})
// View returns a read-only view of SSHAction.
func (p *SSHAction) View() SSHActionView {
return SSHActionView{ж: p}
}
// SSHActionView provides a read-only view over SSHAction.
//
// Its methods should only be called if `Valid()` returns true.
type SSHActionView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *SSHAction
}
// Valid reports whether v's underlying value is non-nil.
func (v SSHActionView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v SSHActionView) AsStruct() *SSHAction {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v SSHActionView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v SSHActionView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *SSHActionView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x SSHAction
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *SSHActionView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x SSHAction
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// Message, if non-empty, is shown to the user before the
// action occurs.
func (v SSHActionView) Message() string { return v.ж.Message }
// Reject, if true, terminates the connection. This action
// has higher priority that Accept, if given.
// The reason this is exists is primarily so a response
// from HoldAndDelegate has a way to stop the poll.
func (v SSHActionView) Reject() bool { return v.ж.Reject }
// Accept, if true, accepts the connection immediately
// without further prompts.
func (v SSHActionView) Accept() bool { return v.ж.Accept }
// SessionDuration, if non-zero, is how long the session can stay open
// before being forcefully terminated.
// It is encoded as an int64 of nanoseconds (Go's time.Duration
// wire format for encoding/json v1). It must not use a jsonv2
// format tag; the mere presence of one makes Go 1.27's
// encoding/json fail to decode the struct. See
// https://github.com/tailscale/tailscale/issues/20528.
func (v SSHActionView) SessionDuration() time.Duration { return v.ж.SessionDuration }
// AllowAgentForwarding, if true, allows accepted connections to forward
// the ssh agent if requested.
func (v SSHActionView) AllowAgentForwarding() bool { return v.ж.AllowAgentForwarding }
// HoldAndDelegate, if non-empty, is a URL that serves an
// outcome verdict. The connection will be accepted and will
// block until the provided long-polling URL serves a new
// SSHAction JSON value. The URL must be fetched using the
// Noise transport (in package control/control{base,http}).
// If the long poll breaks before returning a complete HTTP
// response, it should be re-fetched as long as the SSH
// session is open.
//
// The following variables in the URL are expanded by tailscaled:
//
// - $SRC_NODE_IP (URL escaped)
// - $SRC_NODE_ID (Node.ID as int64 string)
// - $DST_NODE_IP (URL escaped)
// - $DST_NODE_ID (Node.ID as int64 string)
// - $SSH_USER (URL escaped, ssh user requested)
// - $LOCAL_USER (URL escaped, local user mapped)
func (v SSHActionView) HoldAndDelegate() string { return v.ж.HoldAndDelegate }
// AllowLocalPortForwarding, if true, allows accepted connections
// to use local port forwarding if requested.
func (v SSHActionView) AllowLocalPortForwarding() bool { return v.ж.AllowLocalPortForwarding }
// AllowRemotePortForwarding, if true, allows accepted connections
// to use remote port forwarding if requested.
func (v SSHActionView) AllowRemotePortForwarding() bool { return v.ж.AllowRemotePortForwarding }
// Recorders defines the destinations of the SSH session recorders.
// The recording will be uploaded to http://addr:port/record.
func (v SSHActionView) Recorders() views.Slice[netip.AddrPort] { return views.SliceOf(v.ж.Recorders) }
// OnRecorderFailure is the action to take if recording fails.
// If nil, the default action is to fail open.
func (v SSHActionView) OnRecordingFailure() views.ValuePointer[SSHRecorderFailureAction] {
return views.ValuePointerOf(v.ж.OnRecordingFailure)
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _SSHActionViewNeedsRegeneration = SSHAction(struct {
Message string
Reject bool
Accept bool
SessionDuration time.Duration
AllowAgentForwarding bool
HoldAndDelegate string
AllowLocalPortForwarding bool
AllowRemotePortForwarding bool
Recorders []netip.AddrPort
OnRecordingFailure *SSHRecorderFailureAction
}{})
// View returns a read-only view of SSHPrincipal.
func (p *SSHPrincipal) View() SSHPrincipalView {
return SSHPrincipalView{ж: p}
}
// SSHPrincipalView provides a read-only view over SSHPrincipal.
//
// Its methods should only be called if `Valid()` returns true.
type SSHPrincipalView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *SSHPrincipal
}
// Valid reports whether v's underlying value is non-nil.
func (v SSHPrincipalView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v SSHPrincipalView) AsStruct() *SSHPrincipal {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v SSHPrincipalView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v SSHPrincipalView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *SSHPrincipalView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x SSHPrincipal
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *SSHPrincipalView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x SSHPrincipal
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v SSHPrincipalView) Node() StableNodeID { return v.ж.Node }
func (v SSHPrincipalView) NodeIP() string { return v.ж.NodeIP }
// email-ish: foo@example.com, bar@github
func (v SSHPrincipalView) UserLogin() string { return v.ж.UserLogin }
// if true, match any connection
func (v SSHPrincipalView) Any() bool { return v.ж.Any }
// UnusedPubKeys was public key support. It never became an official product
// feature and so as of 2024-12-12 is being removed.
// This stub exists to remind us not to re-use the JSON field name "pubKeys"
// in the future if we bring it back with different semantics.
//
// Deprecated: do not use. It does nothing.
func (v SSHPrincipalView) UnusedPubKeys() views.Slice[string] {
return views.SliceOf(v.ж.UnusedPubKeys)
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _SSHPrincipalViewNeedsRegeneration = SSHPrincipal(struct {
Node StableNodeID
NodeIP string
UserLogin string
Any bool
UnusedPubKeys []string
}{})
// View returns a read-only view of ControlDialPlan.
func (p *ControlDialPlan) View() ControlDialPlanView {
return ControlDialPlanView{ж: p}
}
// ControlDialPlanView provides a read-only view over ControlDialPlan.
//
// Its methods should only be called if `Valid()` returns true.
type ControlDialPlanView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *ControlDialPlan
}
// Valid reports whether v's underlying value is non-nil.
func (v ControlDialPlanView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v ControlDialPlanView) AsStruct() *ControlDialPlan {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v ControlDialPlanView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v ControlDialPlanView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *ControlDialPlanView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x ControlDialPlan
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *ControlDialPlanView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x ControlDialPlan
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// An empty list means the default: use DNS (unspecified which DNS).
func (v ControlDialPlanView) Candidates() views.Slice[ControlIPCandidate] {
return views.SliceOf(v.ж.Candidates)
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _ControlDialPlanViewNeedsRegeneration = ControlDialPlan(struct {
Candidates []ControlIPCandidate
}{})
// View returns a read-only view of Location.
func (p *Location) View() LocationView {
return LocationView{ж: p}
}
// LocationView provides a read-only view over Location.
//
// Its methods should only be called if `Valid()` returns true.
type LocationView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *Location
}
// Valid reports whether v's underlying value is non-nil.
func (v LocationView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v LocationView) AsStruct() *Location {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v LocationView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v LocationView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *LocationView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x Location
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *LocationView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x Location
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// User friendly country name, with proper capitalization ("Canada")
func (v LocationView) Country() string { return v.ж.Country }
// ISO 3166-1 alpha-2 in upper case ("CA")
func (v LocationView) CountryCode() string { return v.ж.CountryCode }
// User friendly city name, with proper capitalization ("Squamish")
func (v LocationView) City() string { return v.ж.City }
// CityCode is a short code representing the city in upper case.
// CityCode is used to disambiguate a city from another location
// with the same city name. It uniquely identifies a particular
// geographical location, within the tailnet.
// IATA, ICAO or ISO 3166-2 codes are recommended ("YSE")
func (v LocationView) CityCode() string { return v.ж.CityCode }
// Latitude, Longitude are optional geographical coordinates of the node, in degrees.
// No particular accuracy level is promised; the coordinates may simply be the center of the city or country.
func (v LocationView) Latitude() float64 { return v.ж.Latitude }
func (v LocationView) Longitude() float64 { return v.ж.Longitude }
// Priority determines the order of use of an exit node when a
// location based preference matches more than one exit node,
// the node with the highest priority wins. Nodes of equal
// probability may be selected arbitrarily.
//
// A value of 0 means the exit node does not have a priority
// preference. A negative int is not allowed.
func (v LocationView) Priority() int { return v.ж.Priority }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _LocationViewNeedsRegeneration = Location(struct {
Country string
CountryCode string
City string
CityCode string
Latitude float64
Longitude float64
Priority int
}{})
// View returns a read-only view of UserProfile.
func (p *UserProfile) View() UserProfileView {
return UserProfileView{ж: p}
}
// UserProfileView provides a read-only view over UserProfile.
//
// Its methods should only be called if `Valid()` returns true.
type UserProfileView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *UserProfile
}
// Valid reports whether v's underlying value is non-nil.
func (v UserProfileView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v UserProfileView) AsStruct() *UserProfile {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v UserProfileView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v UserProfileView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *UserProfileView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x UserProfile
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *UserProfileView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x UserProfile
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v UserProfileView) ID() UserID { return v.ж.ID }
// "alice@smith.com"; for display purposes only (provider is not listed)
func (v UserProfileView) LoginName() string { return v.ж.LoginName }
// "Alice Smith"
func (v UserProfileView) DisplayName() string { return v.ж.DisplayName }
func (v UserProfileView) ProfilePicURL() string { return v.ж.ProfilePicURL }
// Groups is a subset of SCIM groups (e.g. "engineering@example.com")
// or group names in the tailnet policy document (e.g. "group:eng")
// that contain this user and that the coordination server was
// configured to report to this node.
// The list is always sorted when loaded from storage.
func (v UserProfileView) Groups() views.Slice[string] { return views.SliceOf(v.ж.Groups) }
func (v UserProfileView) Equal(v2 UserProfileView) bool { return v.ж.Equal(v2.ж) }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _UserProfileViewNeedsRegeneration = UserProfile(struct {
ID UserID
LoginName string
DisplayName string
ProfilePicURL string
Groups []string
}{})
// View returns a read-only view of VIPService.
func (p *VIPService) View() VIPServiceView {
return VIPServiceView{ж: p}
}
// VIPServiceView provides a read-only view over VIPService.
//
// Its methods should only be called if `Valid()` returns true.
type VIPServiceView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *VIPService
}
// Valid reports whether v's underlying value is non-nil.
func (v VIPServiceView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v VIPServiceView) AsStruct() *VIPService {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v VIPServiceView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v VIPServiceView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *VIPServiceView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x VIPService
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *VIPServiceView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x VIPService
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// Name is the name of the service. The Name uniquely identifies a service
// on a particular tailnet, and so also corresponds uniquely to the pair of
// IP addresses belonging to the VIP service.
func (v VIPServiceView) Name() ServiceName { return v.ж.Name }
// Ports specify which ProtoPorts are made available by this node
// on the service's IPs.
func (v VIPServiceView) Ports() views.Slice[ProtoPortRange] { return views.SliceOf(v.ж.Ports) }
// Active specifies whether new requests for the service should be
// sent to this node by control.
func (v VIPServiceView) Active() bool { return v.ж.Active }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _VIPServiceViewNeedsRegeneration = VIPService(struct {
Name ServiceName
Ports []ProtoPortRange
Active bool
}{})
// View returns a read-only view of SSHPolicy.
func (p *SSHPolicy) View() SSHPolicyView {
return SSHPolicyView{ж: p}
}
// SSHPolicyView provides a read-only view over SSHPolicy.
//
// Its methods should only be called if `Valid()` returns true.
type SSHPolicyView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *SSHPolicy
}
// Valid reports whether v's underlying value is non-nil.
func (v SSHPolicyView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v SSHPolicyView) AsStruct() *SSHPolicy {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v SSHPolicyView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v SSHPolicyView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *SSHPolicyView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x SSHPolicy
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *SSHPolicyView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x SSHPolicy
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// Rules are the rules to process for an incoming SSH connection. The first
// matching rule takes its action and stops processing further rules.
//
// When an incoming connection first starts, all rules are evaluated in
// "none" auth mode, where the client hasn't even been asked to send a
// public key. All SSHRule.Principals requiring a public key won't match. If
// a rule matches on the first pass and its Action is reject, the
// authentication fails with that action's rejection message, if any.
//
// If the first pass rule evaluation matches nothing without matching an
// Action with Reject set, the rules are considered to see whether public
// keys might still result in a match. If not, "none" auth is terminated
// before proceeding to public key mode. If so, the client is asked to try
// public key authentication and the rules are evaluated again for each of
// the client's present keys.
func (v SSHPolicyView) Rules() views.SliceView[*SSHRule, SSHRuleView] {
return views.SliceOfViews[*SSHRule, SSHRuleView](v.ж.Rules)
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _SSHPolicyViewNeedsRegeneration = SSHPolicy(struct {
Rules []*SSHRule
}{})
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package tstest
import (
"fmt"
"runtime"
"time"
"tailscale.com/util/testenv"
)
// MinAllocsPerRun asserts that f can run with no more than target allocations.
// It runs f up to 1000 times or 5s, whichever happens first.
// If f has executed more than target allocations on every run, it returns a non-nil error.
//
// MinAllocsPerRun sets GOMAXPROCS to 1 during its measurement and restores
// it before returning.
func MinAllocsPerRun(t testenv.TB, target uint64, f func()) error {
defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
var memstats runtime.MemStats
var min, max, sum uint64
start := time.Now()
var iters int
for {
runtime.ReadMemStats(&memstats)
startMallocs := memstats.Mallocs
f()
runtime.ReadMemStats(&memstats)
mallocs := memstats.Mallocs - startMallocs
// TODO: if mallocs < target, return an error? See discussion in #3204.
if mallocs <= target {
return nil
}
if min == 0 || mallocs < min {
min = mallocs
}
if mallocs > max {
max = mallocs
}
sum += mallocs
iters++
if iters == 1000 || time.Since(start) > 5*time.Second {
break
}
}
return fmt.Errorf("min allocs = %d, max allocs = %d, avg allocs/run = %f, want run with <= %d allocs", min, max, float64(sum)/float64(iters), target)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package tstest
import (
"container/heap"
"sync"
"time"
"tailscale.com/tstime"
"tailscale.com/util/mak"
)
// ClockOpts is used to configure the initial settings for a Clock. Once the
// settings are configured as desired, call NewClock to get the resulting Clock.
type ClockOpts struct {
// Start is the starting time for the Clock. When FollowRealTime is false,
// Start is also the value that will be returned by the first call
// to Clock.Now. If you are passing a value here, set an explicit
// timezone, otherwise the test may be non-deterministic when TZ environment
// variable is set to different values. The default time is in UTC.
//
// If you do not pass an explicit Start time, the clock will start at the
// current UTC time.
Start time.Time
// Step is the amount of time the Clock will advance whenever Clock.Now is
// called. If set to zero, the Clock will only advance when Clock.Advance is
// called and/or if FollowRealTime is true.
//
// FollowRealTime and Step cannot be enabled at the same time.
Step time.Duration
// TimerChannelSize configures the maximum buffered ticks that are
// permitted in the channel of any Timer and Ticker created by this Clock.
// The special value 0 means to use the default of 1. The buffer may need to
// be increased if time is advanced by more than a single tick and proper
// functioning of the test requires that the ticks are not lost.
TimerChannelSize int
// FollowRealTime makes the simulated time increment along with real time.
// It is a compromise between determinism and the difficulty of explicitly
// managing the simulated time via Step or Clock.Advance. When
// FollowRealTime is set, calls to Now() and PeekNow() will add the
// elapsed real-world time to the simulated time.
//
// FollowRealTime and Step cannot be enabled at the same time.
FollowRealTime bool
}
// NewClock creates a Clock with the specified settings. To create a
// Clock with only the default settings, new(Clock) is equivalent, except that
// the start time will not be computed until one of the receivers is called.
func NewClock(co ClockOpts) *Clock {
if co.FollowRealTime && co.Step != 0 {
panic("only one of FollowRealTime and Step are allowed in NewClock")
}
return newClockInternal(co, nil)
}
// newClockInternal creates a Clock with the specified settings and allows
// specifying a non-standard realTimeClock.
func newClockInternal(co ClockOpts, rtClock tstime.Clock) *Clock {
if !co.FollowRealTime && rtClock != nil {
panic("rtClock can only be set with FollowRealTime enabled")
}
if co.FollowRealTime && rtClock == nil {
rtClock = new(tstime.StdClock)
}
c := &Clock{
start: co.Start,
realTimeClock: rtClock,
step: co.Step,
timerChannelSize: co.TimerChannelSize,
}
c.init() // init now to capture the current time when co.Start.IsZero()
return c
}
// Clock is a testing clock that advances every time its Now method is
// called, beginning at its start time. If no start time is specified using
// ClockBuilder, an arbitrary start time will be selected when the Clock is
// created and can be retrieved by calling Clock.Start().
type Clock struct {
// start is the first value returned by Now. It must not be modified after
// init is called.
start time.Time
// realTimeClock, if not nil, indicates that the Clock shall move forward
// according to realTimeClock + the accumulated calls to Advance. This can
// make writing tests easier that require some control over the clock but do
// not need exact control over the clock. While step can also be used for
// this purpose, it is harder to control how quickly time moves using step.
realTimeClock tstime.Clock
initOnce sync.Once
mu sync.Mutex
// step is how much to advance with each Now call.
step time.Duration
// present is the last value returned by Now (and will be returned again by
// PeekNow).
present time.Time
// realTime is the time from realTimeClock corresponding to the current
// value of present.
realTime time.Time
// skipStep indicates that the next call to Now should not add step to
// present. This occurs after initialization and after Advance.
skipStep bool
// timerChannelSize is the buffer size to use for channels created by
// NewTimer and NewTicker.
timerChannelSize int
events eventManager
}
func (c *Clock) init() {
c.initOnce.Do(func() {
if c.realTimeClock != nil {
c.realTime = c.realTimeClock.Now()
}
if c.start.IsZero() {
if c.realTime.IsZero() {
c.start = time.Now().UTC()
} else {
c.start = c.realTime
}
}
if c.timerChannelSize == 0 {
c.timerChannelSize = 1
}
c.present = c.start
c.skipStep = true
c.events.AdvanceTo(c.present)
})
}
// Now returns the virtual clock's current time, and advances it
// according to its step configuration.
func (c *Clock) Now() time.Time {
c.init()
rt := c.maybeGetRealTime()
c.mu.Lock()
defer c.mu.Unlock()
step := c.step
if c.skipStep {
step = 0
c.skipStep = false
}
c.advanceLocked(rt, step)
return c.present
}
func (c *Clock) maybeGetRealTime() time.Time {
if c.realTimeClock == nil {
return time.Time{}
}
return c.realTimeClock.Now()
}
func (c *Clock) advanceLocked(now time.Time, add time.Duration) {
if !now.IsZero() {
add += now.Sub(c.realTime)
c.realTime = now
}
if add == 0 {
return
}
c.present = c.present.Add(add)
c.events.AdvanceTo(c.present)
}
// PeekNow returns the last time reported by Now. If Now has never been called,
// PeekNow returns the same value as GetStart.
func (c *Clock) PeekNow() time.Time {
c.init()
c.mu.Lock()
defer c.mu.Unlock()
return c.present
}
// Advance moves simulated time forward or backwards by a relative amount. Any
// Timer or Ticker that is waiting will fire at the requested point in simulated
// time. Advance returns the new simulated time. If this Clock follows real time
// then the next call to Now will equal the return value of Advance + the
// elapsed time since calling Advance. Otherwise, the next call to Now will
// equal the return value of Advance, regardless of the current step.
func (c *Clock) Advance(d time.Duration) time.Time {
c.init()
rt := c.maybeGetRealTime()
c.mu.Lock()
defer c.mu.Unlock()
c.skipStep = true
c.advanceLocked(rt, d)
return c.present
}
// AdvanceTo moves simulated time to a new absolute value. Any Timer or Ticker
// that is waiting will fire at the requested point in simulated time. If this
// Clock follows real time then the next call to Now will equal t + the elapsed
// time since calling Advance. Otherwise, the next call to Now will equal t,
// regardless of the configured step.
func (c *Clock) AdvanceTo(t time.Time) {
c.init()
rt := c.maybeGetRealTime()
c.mu.Lock()
defer c.mu.Unlock()
c.skipStep = true
c.realTime = rt
c.present = t
c.events.AdvanceTo(c.present)
}
// GetStart returns the initial simulated time when this Clock was created.
func (c *Clock) GetStart() time.Time {
c.init()
c.mu.Lock()
defer c.mu.Unlock()
return c.start
}
// GetStep returns the amount that simulated time advances on every call to Now.
func (c *Clock) GetStep() time.Duration {
c.init()
c.mu.Lock()
defer c.mu.Unlock()
return c.step
}
// SetStep updates the amount that simulated time advances on every call to Now.
func (c *Clock) SetStep(d time.Duration) {
c.init()
c.mu.Lock()
defer c.mu.Unlock()
c.step = d
}
// SetTimerChannelSize changes the channel size for any Timer or Ticker created
// in the future. It does not affect those that were already created.
func (c *Clock) SetTimerChannelSize(n int) {
c.init()
c.mu.Lock()
defer c.mu.Unlock()
c.timerChannelSize = n
}
// NewTicker returns a Ticker that uses this Clock for accessing the current
// time.
func (c *Clock) NewTicker(d time.Duration) (tstime.TickerController, <-chan time.Time) {
c.init()
rt := c.maybeGetRealTime()
c.mu.Lock()
defer c.mu.Unlock()
c.advanceLocked(rt, 0)
t := &Ticker{
nextTrigger: c.present.Add(d),
period: d,
em: &c.events,
}
t.init(c.timerChannelSize)
return t, t.C
}
// NewTimer returns a Timer that uses this Clock for accessing the current
// time.
func (c *Clock) NewTimer(d time.Duration) (tstime.TimerController, <-chan time.Time) {
c.init()
rt := c.maybeGetRealTime()
c.mu.Lock()
defer c.mu.Unlock()
c.advanceLocked(rt, 0)
t := &Timer{
nextTrigger: c.present.Add(d),
em: &c.events,
}
t.init(c.timerChannelSize, nil)
return t, t.C
}
// AfterFunc returns a Timer that calls f when it fires, using this Clock for
// accessing the current time.
func (c *Clock) AfterFunc(d time.Duration, f func()) tstime.TimerController {
c.init()
rt := c.maybeGetRealTime()
c.mu.Lock()
defer c.mu.Unlock()
c.advanceLocked(rt, 0)
t := &Timer{
nextTrigger: c.present.Add(d),
em: &c.events,
}
t.init(c.timerChannelSize, f)
return t
}
// Since subtracts specified duration from Now().
func (c *Clock) Since(t time.Time) time.Duration {
return c.Now().Sub(t)
}
// eventHandler offers a common interface for Timer and Ticker events to avoid
// code duplication in eventManager.
type eventHandler interface {
// Fire signals the event. The provided time is written to the event's
// channel as the current time. The return value is the next time this event
// should fire, otherwise if it is zero then the event will be removed from
// the eventManager.
Fire(time.Time) time.Time
}
// event tracks details about an upcoming Timer or Ticker firing.
type event struct {
position int // The current index in the heap, needed for heap.Fix and heap.Remove.
when time.Time // A cache of the next time the event triggers to avoid locking issues if we were to get it from eh.
eh eventHandler
}
// eventManager tracks pending events created by Timer and Ticker. eventManager
// implements heap.Interface for efficient lookups of the next event.
type eventManager struct {
// clock is a real time clock for scheduling events with. When clock is nil,
// events only fire when AdvanceTo is called by the simulated clock that
// this eventManager belongs to. When clock is not nil, events may fire when
// timer triggers.
clock tstime.Clock
mu sync.Mutex
now time.Time
// advanceTo is the target time of the in-progress processEventsLocked
// call. Event handlers fired during that call may read it (they run
// with mu held) to coalesce work that would otherwise repeat once per
// period between now and advanceTo.
advanceTo time.Time
heap []*event
reverseLookup map[eventHandler]*event
// timer is an AfterFunc that triggers at heap[0].when.Sub(now) relative to
// the time represented by clock. In other words, if clock is real world
// time, then if an event is scheduled 1 second into the future in the
// simulated time, then the event will trigger after 1 second of actual test
// execution time (unless the test advances simulated time, in which case
// the timer is updated accordingly). This makes tests easier to write in
// situations where the simulated time only needs to be partially
// controlled, and the test writer wishes for simulated time to pass with an
// offset but still synchronized with the real world.
//
// In the future, this could be extended to allow simulated time to run at a
// multiple of real world time.
timer tstime.TimerController
}
func (em *eventManager) handleTimer() {
rt := em.clock.Now()
em.AdvanceTo(rt)
}
// Push implements heap.Interface.Push and must only be called by heap funcs
// with em.mu already held.
func (em *eventManager) Push(x any) {
e, ok := x.(*event)
if !ok {
panic("incorrect event type")
}
if e == nil {
panic("nil event")
}
mak.Set(&em.reverseLookup, e.eh, e)
e.position = len(em.heap)
em.heap = append(em.heap, e)
}
// Pop implements heap.Interface.Pop and must only be called by heap funcs with
// em.mu already held.
func (em *eventManager) Pop() any {
e := em.heap[len(em.heap)-1]
em.heap = em.heap[:len(em.heap)-1]
delete(em.reverseLookup, e.eh)
return e
}
// Len implements sort.Interface.Len and must only be called by heap funcs with
// em.mu already held.
func (em *eventManager) Len() int {
return len(em.heap)
}
// Less implements sort.Interface.Less and must only be called by heap funcs
// with em.mu already held.
func (em *eventManager) Less(i, j int) bool {
return em.heap[i].when.Before(em.heap[j].when)
}
// Swap implements sort.Interface.Swap and must only be called by heap funcs
// with em.mu already held.
func (em *eventManager) Swap(i, j int) {
em.heap[i], em.heap[j] = em.heap[j], em.heap[i]
em.heap[i].position = i
em.heap[j].position = j
}
// Reschedule adds/updates/deletes an event in the heap, whichever
// operation is applicable (use a zero time to delete).
func (em *eventManager) Reschedule(eh eventHandler, t time.Time) {
em.mu.Lock()
defer em.mu.Unlock()
defer em.updateTimerLocked()
e, ok := em.reverseLookup[eh]
if !ok {
if t.IsZero() {
// eh is not scheduled and also not active, so do nothing.
return
}
// eh is not scheduled but is active, so add it.
heap.Push(em, &event{
when: t,
eh: eh,
})
em.processEventsLocked(em.now) // This is always safe and required when !t.After(em.now).
return
}
if t.IsZero() {
// e is scheduled but not active, so remove it.
heap.Remove(em, e.position)
return
}
// e is scheduled and active, so update it.
e.when = t
heap.Fix(em, e.position)
em.processEventsLocked(em.now) // This is always safe and required when !t.After(em.now).
}
// AdvanceTo updates the current time to tm and fires all events scheduled
// before or equal to tm. When an event fires, it may request rescheduling and
// the rescheduled events will be combined with the other existing events that
// are waiting, and will be run in the unified ordering. A poorly behaved event
// may theoretically prevent this from ever completing, but both Timer and
// Ticker require positive steps into the future.
func (em *eventManager) AdvanceTo(tm time.Time) {
em.mu.Lock()
defer em.mu.Unlock()
defer em.updateTimerLocked()
em.processEventsLocked(tm)
em.now = tm
}
// Now returns the cached current time. It is intended for use by a Timer or
// Ticker that needs to convert a relative time to an absolute time.
func (em *eventManager) Now() time.Time {
em.mu.Lock()
defer em.mu.Unlock()
return em.now
}
func (em *eventManager) processEventsLocked(tm time.Time) {
em.advanceTo = tm
for len(em.heap) > 0 && !em.heap[0].when.After(tm) {
// Ideally some jitter would be added here but it's difficult to do so
// in a deterministic fashion.
em.now = em.heap[0].when
if nextFire := em.heap[0].eh.Fire(em.now); !nextFire.IsZero() {
em.heap[0].when = nextFire
heap.Fix(em, 0)
} else {
heap.Pop(em)
}
}
}
func (em *eventManager) updateTimerLocked() {
if em.clock == nil {
return
}
if len(em.heap) == 0 {
if em.timer != nil {
em.timer.Stop()
}
return
}
timeToEvent := em.heap[0].when.Sub(em.now)
if em.timer == nil {
em.timer = em.clock.AfterFunc(timeToEvent, em.handleTimer)
return
}
em.timer.Reset(timeToEvent)
}
// Ticker is a time.Ticker lookalike for use in tests that need to control when
// events fire. Ticker could be made standalone in future but for now is
// expected to be paired with a Clock and created by Clock.NewTicker.
type Ticker struct {
C <-chan time.Time // The channel on which ticks are delivered.
// em is the eventManager to be notified when nextTrigger changes.
// eventManager has its own mutex, and the pointer is immutable, therefore
// em can be accessed without holding mu.
em *eventManager
c chan<- time.Time // The writer side of C.
mu sync.Mutex
// nextTrigger is the time of the ticker's next scheduled activation. When
// Fire activates the ticker, nextTrigger is the timestamp written to the
// channel.
nextTrigger time.Time
// period is the duration that is added to nextTrigger when the ticker
// fires.
period time.Duration
}
func (t *Ticker) init(channelSize int) {
if channelSize <= 0 {
panic("ticker channel size must be non-negative")
}
c := make(chan time.Time, channelSize)
t.c = c
t.C = c
t.em.Reschedule(t, t.nextTrigger)
}
// Fire triggers the ticker. curTime is the timestamp to write to the channel.
// The next trigger time for the ticker is updated to the last computed trigger
// time + the ticker period (set at creation or using Reset). The next trigger
// time is computed this way to match standard time.Ticker behavior, which
// prevents accumulation of long term drift caused by delays in event execution.
func (t *Ticker) Fire(curTime time.Time) time.Time {
t.mu.Lock()
defer t.mu.Unlock()
if t.nextTrigger.IsZero() {
return time.Time{}
}
sent := false
select {
case t.c <- curTime:
sent = true
default:
}
t.nextTrigger = t.nextTrigger.Add(t.period)
// If the channel is full and the clock is being advanced far past this
// ticker's next trigger, the intermediate ticks would all be dropped
// anyway, so skip them in one step rather than firing once per period.
// This keeps ticks aligned to the original schedule and still fires the
// final tick at or before the advance target.
if !sent {
if target := t.em.advanceTo; t.nextTrigger.Before(target) {
skipped := target.Sub(t.nextTrigger) / t.period
t.nextTrigger = t.nextTrigger.Add(skipped * t.period)
}
}
return t.nextTrigger
}
// Reset adjusts the Ticker's period to d and reschedules the next fire time to
// the current simulated time + d.
func (t *Ticker) Reset(d time.Duration) {
if d <= 0 {
// The standard time.Ticker requires a positive period.
panic("non-positive period for Ticker.Reset")
}
now := t.em.Now()
t.mu.Lock()
t.resetLocked(now.Add(d), d)
t.mu.Unlock()
t.em.Reschedule(t, t.nextTrigger)
}
// ResetAbsolute adjusts the Ticker's period to d and reschedules the next fire
// time to nextTrigger.
func (t *Ticker) ResetAbsolute(nextTrigger time.Time, d time.Duration) {
if nextTrigger.IsZero() {
panic("zero nextTrigger time for ResetAbsolute")
}
if d <= 0 {
panic("non-positive period for ResetAbsolute")
}
t.mu.Lock()
t.resetLocked(nextTrigger, d)
t.mu.Unlock()
t.em.Reschedule(t, t.nextTrigger)
}
func (t *Ticker) resetLocked(nextTrigger time.Time, d time.Duration) {
t.nextTrigger = nextTrigger
t.period = d
}
// Stop deactivates the Ticker.
func (t *Ticker) Stop() {
t.mu.Lock()
t.nextTrigger = time.Time{}
t.mu.Unlock()
t.em.Reschedule(t, t.nextTrigger)
}
// Timer is a time.Timer lookalike for use in tests that need to control when
// events fire. Timer could be made standalone in future but for now must be
// paired with a Clock and created by Clock.NewTimer.
type Timer struct {
C <-chan time.Time // The channel on which ticks are delivered.
// em is the eventManager to be notified when nextTrigger changes.
// eventManager has its own mutex, and the pointer is immutable, therefore
// em can be accessed without holding mu.
em *eventManager
f func(time.Time) // The function to call when the timer expires.
mu sync.Mutex
// nextTrigger is the time of the ticker's next scheduled activation. When
// Fire activates the ticker, nextTrigger is the timestamp written to the
// channel.
nextTrigger time.Time
}
func (t *Timer) init(channelSize int, afterFunc func()) {
if channelSize <= 0 {
panic("ticker channel size must be non-negative")
}
c := make(chan time.Time, channelSize)
t.C = c
if afterFunc == nil {
t.f = func(curTime time.Time) {
select {
case c <- curTime:
default:
}
}
} else {
t.f = func(_ time.Time) { afterFunc() }
}
t.em.Reschedule(t, t.nextTrigger)
}
// Fire triggers the ticker. curTime is the timestamp to write to the channel.
// The next trigger time for the ticker is updated to the last computed trigger
// time + the ticker period (set at creation or using Reset). The next trigger
// time is computed this way to match standard time.Ticker behavior, which
// prevents accumulation of long term drift caused by delays in event execution.
func (t *Timer) Fire(curTime time.Time) time.Time {
t.mu.Lock()
defer t.mu.Unlock()
if t.nextTrigger.IsZero() {
return time.Time{}
}
t.nextTrigger = time.Time{}
t.f(curTime)
return time.Time{}
}
// Reset reschedules the next fire time to the current simulated time + d.
// Reset reports whether the timer was still active before the reset.
func (t *Timer) Reset(d time.Duration) bool {
if d <= 0 {
// The standard time.Timer requires a positive delay.
panic("non-positive delay for Timer.Reset")
}
return t.reset(t.em.Now().Add(d))
}
// ResetAbsolute reschedules the next fire time to nextTrigger.
// ResetAbsolute reports whether the timer was still active before the reset.
func (t *Timer) ResetAbsolute(nextTrigger time.Time) bool {
if nextTrigger.IsZero() {
panic("zero nextTrigger time for ResetAbsolute")
}
return t.reset(nextTrigger)
}
// Stop deactivates the Timer. Stop reports whether the timer was active before
// stopping.
func (t *Timer) Stop() bool {
return t.reset(time.Time{})
}
func (t *Timer) reset(nextTrigger time.Time) bool {
t.mu.Lock()
wasActive := !t.nextTrigger.IsZero()
t.nextTrigger = nextTrigger
t.mu.Unlock()
t.em.Reschedule(t, t.nextTrigger)
return wasActive
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux
package tstest
import (
"strconv"
"strings"
"golang.org/x/sys/unix"
)
// KernelVersion returns the major, minor, and patch version of the Linux kernel.
// It returns (0, 0, 0) if the version cannot be determined.
func KernelVersion() (major, minor, patch int) {
var uname unix.Utsname
if err := unix.Uname(&uname); err != nil {
return 0, 0, 0
}
release := unix.ByteSliceToString(uname.Release[:])
return parseKernelVersion(release)
}
// parseKernelVersion parses a Linux kernel version string like "6.12.73+deb13-amd64"
// or "5.15.0-76-generic" and returns the major, minor, and patch components.
// It returns (0, 0, 0) if the version cannot be parsed.
func parseKernelVersion(release string) (major, minor, patch int) {
parts := strings.Split(release, ".")
if len(parts) < 3 {
return 0, 0, 0
}
major, err := strconv.Atoi(parts[0])
if err != nil {
return 0, 0, 0
}
minor, err = strconv.Atoi(parts[1])
if err != nil {
return 0, 0, 0
}
// Patch version may have additional info after a hyphen or plus (e.g., "0-76-generic" or "41+deb13-amd64")
// Extract just the numeric part before any hyphen or plus
patchStr := parts[2]
if idx := strings.IndexAny(patchStr, "-+"); idx != -1 {
patchStr = patchStr[:idx]
}
patch, err = strconv.Atoi(patchStr)
if err != nil {
return 0, 0, 0
}
return major, minor, patch
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package tstest
import (
"bytes"
"fmt"
"log"
"os"
"sync"
"go4.org/mem"
"tailscale.com/types/logger"
"tailscale.com/util/testenv"
)
type testLogWriter struct {
t testenv.TB
}
func (w *testLogWriter) Write(b []byte) (int, error) {
w.t.Helper()
w.t.Logf("%s", b)
return len(b), nil
}
func FixLogs(t testenv.TB) {
log.SetFlags(log.Ltime | log.Lshortfile)
log.SetOutput(&testLogWriter{t})
}
func UnfixLogs(t testenv.TB) {
defer log.SetOutput(os.Stderr)
}
type panicLogWriter struct{}
func (panicLogWriter) Write(b []byte) (int, error) {
// Allow certain phrases for now, in the interest of getting
// CI working on Windows and not having to refactor all the
// interfaces.GetState & tshttpproxy code to allow pushing
// down a Logger yet. TODO(bradfitz): do that refactoring once
// 1.2.0 is out.
if bytes.Contains(b, []byte("tshttpproxy: ")) ||
bytes.Contains(b, []byte("runtime/panic.go:")) ||
bytes.Contains(b, []byte("XXX")) {
os.Stderr.Write(b)
return len(b), nil
}
panic(fmt.Sprintf("please use tailscale.com/logger.Logf instead of the log package (tried to log: %q)", b))
}
// PanicOnLog modifies the standard library log package's default output to
// an io.Writer that panics, to root out code that's not plumbing their logging
// through explicit tailscale.com/logger.Logf paths.
func PanicOnLog() {
log.SetOutput(panicLogWriter{})
}
// NewLogLineTracker produces a LogLineTracker wrapping a given logf that tracks whether expectedFormatStrings were seen.
func NewLogLineTracker(logf logger.Logf, expectedFormatStrings []string) *LogLineTracker {
ret := &LogLineTracker{
logf: logf,
listenFor: expectedFormatStrings,
seen: make(map[string]bool),
}
for _, line := range expectedFormatStrings {
ret.seen[line] = false
}
return ret
}
// LogLineTracker is a logger that tracks which log format patterns it's
// seen and can report which expected ones were not seen later.
type LogLineTracker struct {
logf logger.Logf
listenFor []string
mu sync.Mutex
closed bool
seen map[string]bool // format string => false (if not yet seen but wanted) or true (once seen)
}
// Logf logs to its underlying logger and also tracks that the given format pattern has been seen.
func (lt *LogLineTracker) Logf(format string, args ...any) {
lt.mu.Lock()
if lt.closed {
lt.mu.Unlock()
return
}
if v, ok := lt.seen[format]; ok && !v {
lt.seen[format] = true
}
lt.mu.Unlock()
lt.logf(format, args...)
}
// Check returns which format strings haven't been logged yet.
func (lt *LogLineTracker) Check() []string {
lt.mu.Lock()
defer lt.mu.Unlock()
var notSeen []string
for _, format := range lt.listenFor {
if !lt.seen[format] {
notSeen = append(notSeen, format)
}
}
return notSeen
}
// Reset forgets everything that it's seen.
func (lt *LogLineTracker) Reset() {
lt.mu.Lock()
defer lt.mu.Unlock()
for _, line := range lt.listenFor {
lt.seen[line] = false
}
}
// Close closes lt. After calling Close, calls to Logf become no-ops.
func (lt *LogLineTracker) Close() {
lt.mu.Lock()
defer lt.mu.Unlock()
lt.closed = true
}
// MemLogger is a bytes.Buffer with a Logf method for tests that want
// to log to a buffer.
type MemLogger struct {
sync.Mutex
bytes.Buffer
}
func (ml *MemLogger) Logf(format string, args ...any) {
ml.Lock()
defer ml.Unlock()
fmt.Fprintf(&ml.Buffer, format, args...)
if !mem.HasSuffix(mem.B(ml.Buffer.Bytes()), mem.S("\n")) {
ml.Buffer.WriteByte('\n')
}
}
func (ml *MemLogger) String() string {
ml.Lock()
defer ml.Unlock()
return ml.Buffer.String()
}
// WhileTestRunningLogger returns a logger.Logf that logs to t.Logf until the
// test finishes, at which point it no longer logs anything.
func WhileTestRunningLogger(t testenv.TB) logger.Logf {
var (
mu sync.RWMutex
done bool
)
tlogf := logger.TestLogger(t)
logger := func(format string, args ...any) {
t.Helper()
mu.RLock()
defer mu.RUnlock()
if done {
return
}
tlogf(format, args...)
}
// t.Cleanup is run before the test is marked as done, so by acquiring
// the mutex and then disabling logs, we know that all existing log
// functions have completed, and that no future calls to the logger
// will log something.
//
// We can't do this with an atomic bool, since it's possible to
// observe the following race:
//
// test goroutine goroutine 1
// -------------- -----------
// check atomic, testFinished = no
// test finishes
// run t.Cleanups
// set testFinished = true
// call t.Logf
// panic
//
// Using a mutex ensures that all actions in goroutine 1 in the
// sequence above occur atomically, and thus should not panic.
t.Cleanup(func() {
mu.Lock()
defer mu.Unlock()
done = true
})
return logger
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !windows
package tstest
import "os"
// hasSuperuserPrivileges reports whether the current process runs as root.
func hasSuperuserPrivileges() bool { return os.Getuid() == 0 }
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package tstest
import (
"net/netip"
"reflect"
"time"
"tailscale.com/util/testenv"
)
// IsZeroable is the interface for things with an IsZero method.
type IsZeroable interface {
IsZero() bool
}
var (
netipAddrType = reflect.TypeFor[netip.Addr]()
netipAddrPortType = reflect.TypeFor[netip.AddrPort]()
netipPrefixType = reflect.TypeFor[netip.Prefix]()
timeType = reflect.TypeFor[time.Time]()
timePtrType = reflect.TypeFor[*time.Time]()
)
// CheckIsZero checks that the IsZero method of a given type functions
// correctly, by instantiating a new value of that type, changing a field, and
// then checking that the IsZero method returns false.
//
// The nonzeroValues map should contain non-zero values for each type that
// exists in the type T or any contained types. Basic types like string, bool,
// and numeric types are handled automatically.
func CheckIsZero[T IsZeroable](t testenv.TB, nonzeroValues map[reflect.Type]any) {
t.Helper()
var zero T
if !zero.IsZero() {
t.Errorf("zero value of %T is not IsZero", zero)
return
}
var nonEmptyValue func(t reflect.Type) reflect.Value
nonEmptyValue = func(ty reflect.Type) reflect.Value {
if v, ok := nonzeroValues[ty]; ok {
return reflect.ValueOf(v)
}
switch ty {
// Given that we're a networking company, probably fine to have
// a special case for netip.Addr :)
case netipAddrType:
return reflect.ValueOf(netip.MustParseAddr("1.2.3.4"))
case netipAddrPortType:
return reflect.ValueOf(netip.MustParseAddrPort("1.2.3.4:9999"))
case netipPrefixType:
return reflect.ValueOf(netip.MustParsePrefix("1.2.3.4/24"))
case timeType:
return reflect.ValueOf(time.Unix(1704067200, 0))
case timePtrType:
return reflect.ValueOf(new(time.Unix(1704067200, 0)))
}
switch ty.Kind() {
case reflect.String:
return reflect.ValueOf("foo").Convert(ty)
case reflect.Bool:
return reflect.ValueOf(true)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return reflect.ValueOf(int64(-42)).Convert(ty)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return reflect.ValueOf(uint64(42)).Convert(ty)
case reflect.Float32, reflect.Float64:
return reflect.ValueOf(float64(3.14)).Convert(ty)
case reflect.Complex64, reflect.Complex128:
return reflect.ValueOf(complex(3.14, 2.71)).Convert(ty)
case reflect.Chan:
return reflect.MakeChan(ty, 1)
// For slices, ensure that the slice is non-empty.
case reflect.Slice:
v := nonEmptyValue(ty.Elem())
sl := reflect.MakeSlice(ty, 1, 1)
sl.Index(0).Set(v)
return sl
case reflect.Map:
// Create a map with a single key-value pair, recursively creating each.
k := nonEmptyValue(ty.Key())
v := nonEmptyValue(ty.Elem())
m := reflect.MakeMap(ty)
m.SetMapIndex(k, v)
return m
default:
panic("unhandled type " + ty.String())
}
}
typ := reflect.TypeFor[T]()
for i, n := 0, typ.NumField(); i < n; i++ {
sf := typ.Field(i)
var nonzero T
rv := reflect.ValueOf(&nonzero).Elem()
rv.Field(i).Set(nonEmptyValue(sf.Type))
if nonzero.IsZero() {
t.Errorf("IsZero = true with %v set; want false\nvalue: %#v", sf.Name, nonzero)
}
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package tstest
import (
"bytes"
"runtime"
"runtime/pprof"
"slices"
"strings"
"time"
"tailscale.com/util/testenv"
)
// ResourceCheck takes a snapshot of the current goroutines and registers a
// cleanup on tb to verify that after the rest, all goroutines created by the
// test go away. (well, at least that the count matches. Maybe in the future it
// can look at specific routines).
//
// It panics if called from a parallel test.
func ResourceCheck(tb testenv.TB) {
tb.Helper()
// Set an environment variable (anything at all) just for the
// side effect of tb.Setenv panicking if we're in a parallel test.
tb.Setenv("TS_CHECKING_RESOURCES", "1")
startN, startStacks := goroutines()
tb.Cleanup(func() {
if tb.Failed() {
// Test has failed - but this doesn't catch panics due to
// https://github.com/golang/go/issues/49929.
return
}
// Goroutines might be still exiting.
for range 300 {
if runtime.NumGoroutine() <= startN {
return
}
time.Sleep(10 * time.Millisecond)
}
endN, endStacks := goroutines()
if endN <= startN {
return
}
// Parse and print goroutines.
start := parseGoroutines(startStacks)
end := parseGoroutines(endStacks)
if testenv.Verbose() {
tb.Logf("goroutines start:\n%s", printGoroutines(start))
tb.Logf("goroutines end:\n%s", printGoroutines(end))
}
// Print goroutine diff, omitting tstest.ResourceCheck goroutines.
self := func(g goroutine) bool { return bytes.Contains(g.stack, []byte("\ttailscale.com/tstest.goroutines+")) }
start.goroutines = slices.DeleteFunc(start.goroutines, self)
end.goroutines = slices.DeleteFunc(end.goroutines, self)
tb.Logf("goroutine diff (-start +end):\n%s", diffGoroutines(start, end))
// tb.Failed() above won't report on panics, so we shouldn't call Fatal
// here or we risk suppressing reporting of the panic.
tb.Errorf("goroutine count: expected %d, got %d\n", startN, endN)
})
}
func goroutines() (int, []byte) {
p := pprof.Lookup("goroutine")
b := new(bytes.Buffer)
p.WriteTo(b, 1)
return p.Count(), b.Bytes()
}
// parseGoroutines takes pprof/goroutines?debug=1 -formatted output sorted by
// count, and splits it into a separate list of goroutines with count and stack
// separated.
//
// Example input:
//
// goroutine profile: total 408
// 48 @ 0x47bc0e 0x136c6b9 0x136c69e 0x136c7ab 0x1379809 0x13797fa 0x483da1
// # 0x136c6b8 gvisor.dev/gvisor/pkg/sync.Gopark+0x78 gvisor.dev/gvisor@v0.0.0-20250205023644-9414b50a5633/pkg/sync/runtime_unsafe.go:33
// # 0x136c69d gvisor.dev/gvisor/pkg/sleep.(*Sleeper).nextWaker+0x5d gvisor.dev/gvisor@v0.0.0-20250205023644-9414b50a5633/pkg/sleep/sleep_unsafe.go:210
// # 0x136c7aa gvisor.dev/gvisor/pkg/sleep.(*Sleeper).fetch+0x2a gvisor.dev/gvisor@v0.0.0-20250205023644-9414b50a5633/pkg/sleep/sleep_unsafe.go:257
// # 0x1379808 gvisor.dev/gvisor/pkg/sleep.(*Sleeper).Fetch+0xa8 gvisor.dev/gvisor@v0.0.0-20250205023644-9414b50a5633/pkg/sleep/sleep_unsafe.go:280
// # 0x13797f9 gvisor.dev/gvisor/pkg/tcpip/transport/tcp.(*processor).start+0x99 gvisor.dev/gvisor@v0.0.0-20250205023644-9414b50a5633/pkg/tcpip/transport/tcp/dispatcher.go:291
//
// 48 @ 0x47bc0e 0x413705 0x4132b2 0x10fc905 0x483da1
// # 0x10fc904 github.com/tailscale/wireguard-go/device.(*Device).RoutineDecryption+0x184 github.com/tailscale/wireguard-go@v0.0.0-20250107165329-0b8b35511f19/device/receive.go:245
//
// 48 @ 0x47bc0e 0x413705 0x4132b2 0x10fcd2a 0x483da1
// # 0x10fcd29 github.com/tailscale/wireguard-go/device.(*Device).RoutineHandshake+0x169 github.com/tailscale/wireguard-go@v0.0.0-20250107165329-0b8b35511f19/device/receive.go:279
//
// 48 @ 0x47bc0e 0x413705 0x4132b2 0x1100ba7 0x483da1
// # 0x1100ba6 github.com/tailscale/wireguard-go/device.(*Device).RoutineEncryption+0x186 github.com/tailscale/wireguard-go@v0.0.0-20250107165329-0b8b35511f19/device/send.go:451
//
// 26 @ 0x47bc0e 0x458e57 0x847587 0x483da1
// # 0x847586 database/sql.(*DB).connectionOpener+0x86 database/sql/sql.go:1261
//
// 13 @ 0x47bc0e 0x458e57 0x754927 0x483da1
// # 0x754926 net/http.(*persistConn).writeLoop+0xe6 net/http/transport.go:2596
//
// 7 @ 0x47bc0e 0x413705 0x4132b2 0x10fda4d 0x483da1
// # 0x10fda4c github.com/tailscale/wireguard-go/device.(*Peer).RoutineSequentialReceiver+0x16c github.com/tailscale/wireguard-go@v0.0.0-20250107165329-0b8b35511f19/device/receive.go:443
func parseGoroutines(g []byte) goroutineDump {
head, tail, ok := bytes.Cut(g, []byte("\n"))
if !ok {
return goroutineDump{head: head}
}
raw := bytes.Split(tail, []byte("\n\n"))
parsed := make([]goroutine, 0, len(raw))
for _, s := range raw {
count, rem, ok := bytes.Cut(s, []byte(" @ "))
if !ok {
continue
}
header, stack, _ := bytes.Cut(rem, []byte("\n"))
sort := slices.Clone(header)
reverseWords(sort)
parsed = append(parsed, goroutine{count, header, stack, sort})
}
return goroutineDump{head, parsed}
}
type goroutineDump struct {
head []byte
goroutines []goroutine
}
// goroutine is a parsed stack trace in pprof goroutine output, e.g.
// "10 @ 0x100 0x001\n# 0x100 test() test.go\n# 0x001 main() test.go".
type goroutine struct {
count []byte // e.g. "10"
header []byte // e.g. "0x100 0x001"
stack []byte // e.g. "# 0x100 test() test.go\n# 0x001 main() test.go"
// sort is the same pointers as in header, but in reverse order so that we
// can place related goroutines near each other by sorting on this field.
// E.g. "0x001 0x100".
sort []byte
}
func (g goroutine) Compare(h goroutine) int {
return bytes.Compare(g.sort, h.sort)
}
// reverseWords repositions the words in b such that they are reversed.
// Words are separated by spaces. New lines are not considered.
// https://sketch.dev/sk/a4ef
func reverseWords(b []byte) {
if len(b) == 0 {
return
}
// First, reverse the entire slice.
reverse(b)
// Then reverse each word individually.
start := 0
for i := 0; i <= len(b); i++ {
if i == len(b) || b[i] == ' ' {
reverse(b[start:i])
start = i + 1
}
}
}
// reverse reverses bytes in place
func reverse(b []byte) {
for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
}
// printGoroutines returns a text representation of h, gs equivalent to the
// pprof ?debug=1 input parsed by parseGoroutines, except the goroutines are
// sorted in an order easier for diffing.
func printGoroutines(g goroutineDump) []byte {
var b bytes.Buffer
b.Write(g.head)
slices.SortFunc(g.goroutines, goroutine.Compare)
for _, g := range g.goroutines {
b.WriteString("\n\n")
b.Write(g.count)
b.WriteString(" @ ")
b.Write(g.header)
b.WriteString("\n")
if len(g.stack) > 0 {
b.Write(g.stack)
}
}
return b.Bytes()
}
// diffGoroutines returns a diff between goroutines of gx and gy.
// Goroutines present in gx and absent from gy are prefixed with "-".
// Goroutines absent from gx and present in gy are prefixed with "+".
// Goroutines present in both but with different counts only show a prefix on the count line.
func diffGoroutines(x, y goroutineDump) string {
hx, hy := x.head, y.head
gx, gy := x.goroutines, y.goroutines
var b strings.Builder
if !bytes.Equal(hx, hy) {
b.WriteString("- ")
b.Write(hx)
b.WriteString("\n+ ")
b.Write(hy)
b.WriteString("\n")
}
slices.SortFunc(gx, goroutine.Compare)
slices.SortFunc(gy, goroutine.Compare)
writeHeader := func(prefix string, g goroutine) {
b.WriteString(prefix)
b.Write(g.count)
b.WriteString(" @ ")
b.Write(g.header)
b.WriteString("\n")
}
writeStack := func(prefix string, g goroutine) {
s := g.stack
for {
var h []byte
h, s, _ = bytes.Cut(s, []byte("\n"))
if len(h) == 0 && len(s) == 0 {
break
}
b.WriteString(prefix)
b.Write(h)
b.WriteString("\n")
}
}
i, j := 0, 0
for {
var d int
switch {
case i < len(gx) && j < len(gy):
d = gx[i].Compare(gy[j])
case i < len(gx):
d = -1
case j < len(gy):
d = 1
default:
return b.String()
}
switch d {
case -1:
b.WriteString("\n")
writeHeader("- ", gx[i])
writeStack("- ", gx[i])
i++
case +1:
b.WriteString("\n")
writeHeader("+ ", gy[j])
writeStack("+ ", gy[j])
j++
case 0:
if !bytes.Equal(gx[i].count, gy[j].count) {
b.WriteString("\n")
writeHeader("- ", gx[i])
writeHeader("+ ", gy[j])
writeStack(" ", gy[j])
}
i++
j++
}
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package tstest provides utilities for use in unit tests.
//
// It intentionally does not depend on the "testing" package (using
// [testenv.TB] instead of testing.TB) so that importing it from
// non-test code doesn't link the testing package and its flag
// registration side effects into the binary.
package tstest
import (
"context"
"fmt"
"time"
"tailscale.com/envknob"
"tailscale.com/types/logger"
"tailscale.com/util/backoff"
"tailscale.com/util/testenv"
)
// AssertNotParallel asserts that t has not been marked as parallel.
// It panics (via t.Setenv) if t.Parallel has already been called.
//
// Use this when a test modifies package-level globals or other shared
// state that would be unsafe to modify concurrently with other tests.
func AssertNotParallel(t testenv.TB) {
t.Helper()
t.Setenv("ASSERT_NOT_PARALLEL_TEST", "1") // panics if t.Parallel was called
}
// Replace replaces the value of target with val.
// The old value is restored when the test ends.
//
// When target is a package-level variable, the caller should also call
// [AssertNotParallel] to ensure the test is not running in parallel with
// other tests that may access the same variable.
func Replace[T any](t testenv.TB, target *T, val T) {
t.Helper()
if target == nil {
t.Fatalf("Replace: nil pointer")
panic("unreachable") // pacify staticcheck
}
old := *target
t.Cleanup(func() {
*target = old
})
*target = val
}
// WaitFor retries try for up to maxWait.
// It returns nil once try returns nil the first time.
// If maxWait passes without success, it returns try's last error.
func WaitFor(maxWait time.Duration, try func() error) error {
bo := backoff.NewBackoff("wait-for", logger.Discard, maxWait/4)
deadline := time.Now().Add(maxWait)
var err error
for time.Now().Before(deadline) {
err = try()
if err == nil {
break
}
bo.BackOff(context.Background(), err)
}
return err
}
var serializeParallel = envknob.RegisterBool("TS_SERIAL_TESTS")
// Parallel calls t.Parallel, unless TS_SERIAL_TESTS is set true.
func Parallel(t interface{ Parallel() }) {
if !serializeParallel() {
t.Parallel()
}
}
// RequireRoot skips the test if the current process lacks superuser privileges,
// meaning root on most platforms and an elevated process on Windows.
func RequireRoot(tb testenv.TB) {
tb.Helper()
if !hasSuperuserPrivileges() {
tb.Skip("skipping test; requires superuser privileges")
}
}
// SkipOnKernelVersions skips the test if the current
// kernel version is in the specified list.
func SkipOnKernelVersions(t testenv.TB, issue string, versions ...string) {
major, minor, patch := KernelVersion()
if major == 0 && minor == 0 && patch == 0 {
t.Logf("could not determine kernel version")
return
}
current := fmt.Sprintf("%d.%d.%d", major, minor, patch)
for _, v := range versions {
if v == current {
t.Skipf("skipping on kernel version %q - see issue %s", current, issue)
}
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package tstime
import (
"math/rand/v2"
"time"
)
// RandomDurationBetween returns a random duration in range [min,max).
// If panics if max < min.
func RandomDurationBetween(min, max time.Duration) time.Duration {
diff := max - min
if diff == 0 {
return min
}
return min + rand.N(max-min)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package tstime defines Tailscale-specific time utilities.
package tstime
import (
"context"
"encoding"
"strconv"
"strings"
"time"
)
// Parse3339 is a wrapper around time.Parse(time.RFC3339, s).
func Parse3339(s string) (time.Time, error) {
return time.Parse(time.RFC3339, s)
}
// Parse3339B is Parse3339 but for byte slices.
func Parse3339B(b []byte) (time.Time, error) {
var t time.Time
if err := t.UnmarshalText(b); err != nil {
return Parse3339(string(b)) // reproduce same error message
}
return t, nil
}
// ParseDuration is more expressive than [time.ParseDuration],
// also accepting 'd' (days) and 'w' (weeks) literals.
func ParseDuration(s string) (time.Duration, error) {
for {
end := strings.IndexAny(s, "dw")
if end < 0 {
break
}
start := end - (len(s[:end]) - len(strings.TrimRight(s[:end], "0123456789")))
n, err := strconv.Atoi(s[start:end])
if err != nil {
return 0, err
}
hours := 24
if s[end] == 'w' {
hours *= 7
}
s = s[:start] + s[end+1:] + strconv.Itoa(n*hours) + "h"
}
return time.ParseDuration(s)
}
// Sleep is like [time.Sleep] but returns early upon context cancelation.
// It reports whether the full sleep duration was achieved.
func Sleep(ctx context.Context, d time.Duration) bool {
timer := time.NewTimer(d)
defer timer.Stop()
select {
case <-ctx.Done():
return false
case <-timer.C:
return true
}
}
// DefaultClock is a wrapper around a Clock.
// It uses StdClock by default if Clock is nil.
type DefaultClock struct{ Clock }
// TODO: We should make the methods of DefaultClock inlineable
// so that we can optimize for the common case where c.Clock == nil.
func (c DefaultClock) Now() time.Time {
if c.Clock == nil {
return time.Now()
}
return c.Clock.Now()
}
func (c DefaultClock) NewTimer(d time.Duration) (TimerController, <-chan time.Time) {
if c.Clock == nil {
t := time.NewTimer(d)
return t, t.C
}
return c.Clock.NewTimer(d)
}
func (c DefaultClock) NewTicker(d time.Duration) (TickerController, <-chan time.Time) {
if c.Clock == nil {
t := time.NewTicker(d)
return t, t.C
}
return c.Clock.NewTicker(d)
}
func (c DefaultClock) AfterFunc(d time.Duration, f func()) TimerController {
if c.Clock == nil {
return time.AfterFunc(d, f)
}
return c.Clock.AfterFunc(d, f)
}
func (c DefaultClock) Since(t time.Time) time.Duration {
if c.Clock == nil {
return time.Since(t)
}
return c.Clock.Since(t)
}
// Clock offers a subset of the functionality from the std/time package.
// Normally, applications will use the StdClock implementation that calls the
// appropriate std/time exported funcs. The advantage of using Clock is that
// tests can substitute a different implementation, allowing the test to control
// time precisely, something required for certain types of tests to be possible
// at all, speeds up execution by not needing to sleep, and can dramatically
// reduce the risk of flakes due to tests executing too slowly or quickly.
type Clock interface {
// Now returns the current time, as in time.Now.
Now() time.Time
// NewTimer returns a timer whose notion of the current time is controlled
// by this Clock. It follows the semantics of time.NewTimer as closely as
// possible but is adapted to return an interface, so the channel needs to
// be returned as well.
NewTimer(d time.Duration) (TimerController, <-chan time.Time)
// NewTicker returns a ticker whose notion of the current time is controlled
// by this Clock. It follows the semantics of time.NewTicker as closely as
// possible but is adapted to return an interface, so the channel needs to
// be returned as well.
NewTicker(d time.Duration) (TickerController, <-chan time.Time)
// AfterFunc returns a ticker whose notion of the current time is controlled
// by this Clock. When the ticker expires, it will call the provided func.
// It follows the semantics of time.AfterFunc.
AfterFunc(d time.Duration, f func()) TimerController
// Since returns the time elapsed since t.
// It follows the semantics of time.Since.
Since(t time.Time) time.Duration
}
// TickerController offers the receivers of a time.Ticker to ensure
// compatibility with standard timers, but allows for the option of substituting
// a standard timer with something else for testing purposes.
type TickerController interface {
// Reset follows the same semantics as with time.Ticker.Reset.
Reset(d time.Duration)
// Stop follows the same semantics as with time.Ticker.Stop.
Stop()
}
// TimerController offers the receivers of a time.Timer to ensure
// compatibility with standard timers, but allows for the option of substituting
// a standard timer with something else for testing purposes.
type TimerController interface {
// Reset follows the same semantics as with time.Timer.Reset.
Reset(d time.Duration) bool
// Stop follows the same semantics as with time.Timer.Stop.
Stop() bool
}
// StdClock is a simple implementation of Clock using the relevant funcs in the
// std/time package.
type StdClock struct{}
// Now calls time.Now.
func (StdClock) Now() time.Time {
return time.Now()
}
// NewTimer calls time.NewTimer. As an interface does not allow for struct
// members and other packages cannot add receivers to another package, the
// channel is also returned because it would be otherwise inaccessible.
func (StdClock) NewTimer(d time.Duration) (TimerController, <-chan time.Time) {
t := time.NewTimer(d)
return t, t.C
}
// NewTicker calls time.NewTicker. As an interface does not allow for struct
// members and other packages cannot add receivers to another package, the
// channel is also returned because it would be otherwise inaccessible.
func (StdClock) NewTicker(d time.Duration) (TickerController, <-chan time.Time) {
t := time.NewTicker(d)
return t, t.C
}
// AfterFunc calls time.AfterFunc.
func (StdClock) AfterFunc(d time.Duration, f func()) TimerController {
return time.AfterFunc(d, f)
}
// Since calls time.Since.
func (StdClock) Since(t time.Time) time.Duration {
return time.Since(t)
}
// GoDuration is a [time.Duration] but JSON serializes with [time.Duration.String].
//
// Note that this format is specific to Go and non-standard,
// but excels in being most humanly readable compared to alternatives.
// The wider industry still lacks consensus for the representation
// of a time duration in humanly-readable text.
// See https://go.dev/issue/71631 for more discussion.
//
// Regardless of how the industry evolves into the future,
// this type explicitly uses the Go format.
type GoDuration struct{ time.Duration }
var (
_ encoding.TextAppender = (*GoDuration)(nil)
_ encoding.TextMarshaler = (*GoDuration)(nil)
_ encoding.TextUnmarshaler = (*GoDuration)(nil)
)
func (d GoDuration) AppendText(b []byte) ([]byte, error) {
// The String method is inlineable (see https://go.dev/cl/520602),
// so this may not allocate since the string does not escape.
return append(b, d.String()...), nil
}
func (d GoDuration) MarshalText() ([]byte, error) {
return []byte(d.String()), nil
}
func (d *GoDuration) UnmarshalText(b []byte) error {
d2, err := time.ParseDuration(string(b))
if err != nil {
return err
}
d.Duration = d2
return nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package tsweb
import (
"expvar"
"fmt"
"html"
"io"
"net/http"
"net/url"
"os"
"runtime"
"tailscale.com/feature"
"tailscale.com/tsweb/varz"
"tailscale.com/version"
)
// DebugHandler is an http.Handler that serves a debugging "homepage",
// and provides helpers to register more debug endpoints and reports.
//
// The rendered page consists of three sections: informational
// key/value pairs, links to other pages, and additional
// program-specific HTML. Callers can add to these sections using the
// KV, URL and Section helpers respectively.
//
// Additionally, the Handle method offers a shorthand for correctly
// registering debug handlers and cross-linking them from /debug/.
type DebugHandler struct {
mux *http.ServeMux // where this handler is registered
kvs []func(io.Writer) // output one <li>...</li> each, see KV()
urls []string // one <li>...</li> block with link each
sections []func(io.Writer, *http.Request) // invoked in registration order prior to outputting </body>
title string // title displayed on index page
}
// PrometheusHandler is an optional hook to enable native Prometheus
// support in the debug handler. It is disabled by default. Import the
// tailscale.com/tsweb/promvarz package to enable this feature.
var PrometheusHandler feature.Hook[func(*DebugHandler)]
// Debugger returns the DebugHandler registered on mux at /debug/,
// creating it if necessary.
func Debugger(mux *http.ServeMux) *DebugHandler {
h, pat := mux.Handler(&http.Request{URL: &url.URL{Path: "/debug/"}})
if d, ok := h.(*DebugHandler); ok && pat == "/debug/" {
return d
}
ret := &DebugHandler{
mux: mux,
title: fmt.Sprintf("%s debug", version.CmdName()),
}
mux.Handle("/debug/", ret)
ret.KVFunc("Uptime", func() any { return varz.Uptime() })
ret.KV("Version", version.Long())
ret.Handle("vars", "Metrics (Go)", expvar.Handler())
if PrometheusHandler.IsSet() {
PrometheusHandler.Get()(ret)
} else {
ret.Handle("varz", "Metrics (Prometheus)", http.HandlerFunc(varz.Handler))
}
addProfilingHandlers(ret)
ret.Handle("gc", "force GC", http.HandlerFunc(gcHandler))
hostname, err := os.Hostname()
if err == nil {
ret.KV("Machine", hostname)
}
return ret
}
// ServeHTTP implements http.Handler.
func (d *DebugHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !AllowDebugAccess(r) {
http.Error(w, "debug access denied", http.StatusForbidden)
return
}
if r.URL.Path != "/debug/" {
// Sub-handlers are handled by the parent mux directly.
http.NotFound(w, r)
return
}
AddBrowserHeaders(w)
f := func(format string, args ...any) { fmt.Fprintf(w, format, args...) }
f("<html><body><h1>%s</h1><ul>", html.EscapeString(d.title))
for _, kv := range d.kvs {
kv(w)
}
for _, url := range d.urls {
io.WriteString(w, url)
}
for _, section := range d.sections {
section(w, r)
}
}
func (d *DebugHandler) handle(slug string, handler http.Handler) string {
href := "/debug/" + slug
d.mux.Handle(href, Protected(debugBrowserHeaderHandler(handler)))
return href
}
// Handle registers handler at /debug/<slug> and adds a link to it
// on /debug/ with the provided description.
func (d *DebugHandler) Handle(slug, desc string, handler http.Handler) {
href := d.handle(slug, handler)
d.URL(href, desc)
}
// Handle registers handler at /debug/<slug> and adds a link to it
// on /debug/ with the provided description.
func (d *DebugHandler) HandleFunc(slug, desc string, handler http.HandlerFunc) {
d.Handle(slug, desc, handler)
}
// HandleSilent registers handler at /debug/<slug>. It does not add
// a descriptive entry in /debug/ for it. This should be used
// sparingly, for things that need to be registered but would pollute
// the list of debug links.
func (d *DebugHandler) HandleSilent(slug string, handler http.Handler) {
d.handle(slug, handler)
}
// HandleSilent registers handler at /debug/<slug>. It does not add
// a descriptive entry in /debug/ for it. This should be used
// sparingly, for things that need to be registered but would pollute
// the list of debug links.
func (d *DebugHandler) HandleSilentFunc(slug string, handler http.HandlerFunc) {
d.HandleSilent(slug, handler)
}
// KV adds a key/value list item to /debug/.
func (d *DebugHandler) KV(k string, v any) {
val := html.EscapeString(fmt.Sprintf("%v", v))
d.kvs = append(d.kvs, func(w io.Writer) {
fmt.Fprintf(w, "<li><b>%s:</b> %s</li>", k, val)
})
}
// KVFunc adds a key/value list item to /debug/. v is called on every
// render of /debug/.
func (d *DebugHandler) KVFunc(k string, v func() any) {
d.kvs = append(d.kvs, func(w io.Writer) {
val := html.EscapeString(fmt.Sprintf("%v", v()))
fmt.Fprintf(w, "<li><b>%s:</b> %s</li>", k, val)
})
}
// URL adds a URL and description list item to /debug/.
func (d *DebugHandler) URL(url, desc string) {
if desc != "" {
desc = " (" + desc + ")"
}
d.urls = append(d.urls, fmt.Sprintf(`<li><a href="%s">%s</a>%s</li>`, url, url, html.EscapeString(desc)))
}
// Section invokes f on every render of /debug/ to add supplemental
// HTML to the page body.
func (d *DebugHandler) Section(f func(w io.Writer, r *http.Request)) {
d.sections = append(d.sections, f)
}
// Title sets the title at the top of the debug page.
func (d *DebugHandler) Title(title string) {
d.title = title
}
func gcHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("running GC...\n"))
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
runtime.GC()
w.Write([]byte("Done.\n"))
}
// debugBrowserHeaderHandler is a wrapper around BrowserHeaderHandler with a
// more relaxed Content-Security-Policy that's acceptable for internal debug
// pages. It should not be used on any public-facing handlers!
func debugBrowserHeaderHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
AddBrowserHeaders(w)
// The only difference from AddBrowserHeaders is that this policy
// allows inline CSS styles. They make debug pages much easier to
// prototype, while the risk of user-injected CSS is relatively low.
w.Header().Set("Content-Security-Policy", "default-src 'self'; frame-ancestors 'none'; form-action 'self'; base-uri 'self'; block-all-mixed-content; object-src 'none'; style-src 'self' 'unsafe-inline'")
h.ServeHTTP(w, r)
})
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package tsweb
import (
"encoding/json"
"strings"
"time"
)
// AccessLogRecord is a record of one HTTP request served.
type AccessLogRecord struct {
// Timestamp at which request processing started.
Time time.Time `json:"time"`
// Time it took to finish processing the request. It does not
// include the entire lifetime of the underlying connection in
// cases like connection hijacking, only the lifetime of the HTTP
// request handler.
Seconds float64 `json:"duration,omitempty"`
// The client's ip:port.
RemoteAddr string `json:"remote_addr,omitempty"`
// The HTTP protocol version, usually "HTTP/1.1 or HTTP/2".
Proto string `json:"proto,omitempty"`
// Whether the request was received over TLS.
TLS bool `json:"tls,omitempty"`
// The target hostname in the request.
Host string `json:"host,omitempty"`
// The HTTP method invoked.
Method string `json:"method,omitempty"`
// The unescaped request URI, including query parameters.
RequestURI string `json:"request_uri,omitempty"`
// The client's user-agent
UserAgent string `json:"user_agent,omitempty"`
// Where the client was before making this request.
Referer string `json:"referer,omitempty"`
// The HTTP response code sent to the client.
Code int `json:"code,omitempty"`
// Number of bytes sent in response body to client. If the request
// was hijacked, only includes bytes sent up to the point of
// hijacking.
Bytes int `json:"bytes,omitempty"`
// Error encountered during request processing.
Err string `json:"err,omitempty"`
// RequestID is a unique ID for this request. If the *http.Request context
// carries this value via SetRequestID, then it will be displayed to the
// client immediately after the error text, as well as logged here. This
// makes it easier to correlate support requests with server logs. If a
// RequestID generator is not configured, RequestID will be empty.
RequestID RequestID `json:"request_id,omitempty"`
}
// String returns m as a JSON string.
func (m AccessLogRecord) String() string {
if m.Time.IsZero() {
m.Time = time.Now()
}
var buf strings.Builder
json.NewEncoder(&buf).Encode(m)
return strings.TrimRight(buf.String(), "\n")
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !js && !wasm
package tsweb
import (
"net/http"
"net/http/pprof"
)
func addProfilingHandlers(d *DebugHandler) {
// pprof.Index serves everything that runtime/pprof.Lookup finds:
// goroutine, threadcreate, heap, allocs, block, mutex
d.Handle("pprof/", "pprof (index)", http.HandlerFunc(pprof.Index))
// But register the other ones from net/http/pprof directly:
d.HandleSilent("pprof/cmdline", http.HandlerFunc(pprof.Cmdline))
d.HandleSilent("pprof/profile", http.HandlerFunc(pprof.Profile))
d.HandleSilent("pprof/symbol", http.HandlerFunc(pprof.Symbol))
d.HandleSilent("pprof/trace", http.HandlerFunc(pprof.Trace))
d.URL("/debug/pprof/goroutine?debug=1", "Goroutines (collapsed)")
d.URL("/debug/pprof/goroutine?debug=2", "Goroutines (full)")
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package tsweb
import (
"context"
"net/http"
"time"
"tailscale.com/tstime"
"tailscale.com/util/ctxkey"
"tailscale.com/util/rands"
)
// RequestID is an opaque identifier for a HTTP request, used to correlate
// user-visible errors with backend server logs. The RequestID is typically
// threaded through an HTTP Middleware (WithRequestID) and then can be extracted
// by HTTP Handlers to include in their logs.
//
// RequestID is an opaque identifier for a HTTP request, used to correlate
// user-visible errors with backend server logs. If present in the context, the
// RequestID will be printed alongside the message text and logged in the
// AccessLogRecord.
//
// A RequestID has the format "REQ-1{ID}", and the ID should be treated as an
// opaque string. The current implementation uses a UUID.
type RequestID string
// String returns the string format of the request ID, for use in e.g. setting
// a [http.Header].
func (r RequestID) String() string {
return string(r)
}
// RequestIDKey stores and loads [RequestID] values within a [context.Context].
var RequestIDKey ctxkey.Key[RequestID]
// RequestIDHeader is a custom HTTP header that the WithRequestID middleware
// uses to determine whether to re-use a given request ID from the client
// or generate a new one.
const RequestIDHeader = "X-Tailscale-Request-Id"
// GenerateRequestID generates a new request ID with the current format.
func GenerateRequestID() RequestID {
// Return a string of the form "REQ-<VersionByte><...>"
// Previously we returned "REQ-1<UUIDString>".
// Now we return "REQ-2" version, where the "2" doubles as the year 2YYY
// in a leading date.
now := time.Now().UTC()
return RequestID("REQ-" + now.Format(tstime.NumericDateTime) + rands.HexString(16))
}
// SetRequestID is an HTTP middleware that injects a RequestID in the
// *http.Request Context. The value of that request id is either retrieved from
// the RequestIDHeader or a randomly generated one if not exists. Inner
// handlers can retrieve this ID from the RequestIDFromContext function.
func SetRequestID(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var rid RequestID
if id := r.Header.Get(RequestIDHeader); id != "" {
rid = RequestID(id)
} else {
rid = GenerateRequestID()
}
ctx := RequestIDKey.WithValue(r.Context(), rid)
r = r.WithContext(ctx)
h.ServeHTTP(w, r)
})
}
// RequestIDFromContext retrieves the RequestID from context that can be set by
// the SetRequestID function.
//
// Deprecated: Use [RequestIDKey.Value] instead.
func RequestIDFromContext(ctx context.Context) RequestID {
return RequestIDKey.Value(ctx)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package tsweb contains code used in various Tailscale webservers.
package tsweb
import (
"bufio"
"bytes"
"cmp"
"context"
"errors"
"expvar"
"fmt"
"io"
"log"
"maps"
"net"
"net/http"
"net/netip"
"net/url"
"os"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"sync"
"time"
"go4.org/mem"
"tailscale.com/envknob"
"tailscale.com/metrics"
"tailscale.com/net/tsaddr"
"tailscale.com/tsweb/varz"
"tailscale.com/types/logger"
"tailscale.com/util/ctxkey"
"tailscale.com/util/vizerror"
)
// DevMode controls whether extra output in shown, for when the binary is being run in dev mode.
var DevMode bool
func DefaultCertDir(leafDir string) string {
cacheDir, err := os.UserCacheDir()
if err == nil {
return filepath.Join(cacheDir, "tailscale", leafDir)
}
return ""
}
// IsProd443 reports whether addr is a Go listen address for port 443.
func IsProd443(addr string) bool {
_, port, _ := net.SplitHostPort(addr)
return port == "443" || port == "https"
}
// debugTrustedCIDRs is the envknob for TS_DEBUG_TRUSTED_CIDRS, a
// comma-separated list of CIDR ranges (e.g. "10.0.0.0/8,172.16.0.0/12")
// whose source IPs are allowed to access debug endpoints without Tailscale
// authentication. This will supersede both IsTailscaleIP() and
// TS_ALLOW_DEBUG_IP.
var debugTrustedCIDRs = envknob.RegisterString("TS_DEBUG_TRUSTED_CIDRS")
// trustedCIDRs returns the parsed CIDR prefixes from TS_DEBUG_TRUSTED_CIDRS.
var trustedCIDRs = sync.OnceValue(func() []netip.Prefix {
return parseTrustedCIDRs(debugTrustedCIDRs())
})
// parseTrustedCIDRs parses a comma-separated list of CIDR prefixes.
// It fatals on invalid entries, consistent with other envknob parsing.
func parseTrustedCIDRs(raw string) []netip.Prefix {
if raw == "" {
return nil
}
var prefixes []netip.Prefix
for s := range strings.SplitSeq(raw, ",") {
s = strings.TrimSpace(s)
if s == "" {
continue
}
pfx, err := netip.ParsePrefix(s)
if err != nil {
log.Fatalf("invalid CIDR in TS_DEBUG_TRUSTED_CIDRS: %q: %v", s, err)
}
prefixes = append(prefixes, pfx)
}
return prefixes
}
// cidrsContain checks if the source IP is associated with one of the
// provided cidrs.
func cidrsContain(cidrs []netip.Prefix, ip netip.Addr) bool {
for _, pfx := range cidrs {
if pfx.Contains(ip) {
return true
}
}
return false
}
// AllowDebugAccess reports whether r should be permitted to access
// various debug endpoints.
func AllowDebugAccess(r *http.Request) bool {
if allowDebugAccessWithKey(r) {
return true
}
if r.Header.Get("X-Forwarded-For") != "" {
// TODO if/when needed. For now, conservative:
return false
}
ipStr, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return false
}
ip, err := netip.ParseAddr(ipStr)
if err != nil {
return false
}
if tsaddr.IsTailscaleIP(ip) || ip.IsLoopback() || ipStr == envknob.String("TS_ALLOW_DEBUG_IP") {
return true
}
if cidrsContain(trustedCIDRs(), ip) {
return true
}
return false
}
func allowDebugAccessWithKey(r *http.Request) bool {
if r.Method != "GET" {
return false
}
urlKey := r.FormValue("debugkey")
keyPath := envknob.String("TS_DEBUG_KEY_PATH")
if urlKey != "" && keyPath != "" {
slurp, err := os.ReadFile(keyPath)
if err == nil && string(bytes.TrimSpace(slurp)) == urlKey {
return true
}
}
return false
}
// AcceptsEncoding reports whether r accepts the named encoding
// ("gzip", "br", etc).
func AcceptsEncoding(r *http.Request, enc string) bool {
h := r.Header.Get("Accept-Encoding")
if h == "" {
return false
}
if !strings.Contains(h, enc) && !mem.ContainsFold(mem.S(h), mem.S(enc)) {
return false
}
remain := h
for len(remain) > 0 {
var part string
part, remain, _ = strings.Cut(remain, ",")
part = strings.TrimSpace(part)
part, _, _ = strings.Cut(part, ";")
if part == enc {
return true
}
}
return false
}
// Protected wraps a provided debug handler, h, returning a Handler
// that enforces AllowDebugAccess and returns forbidden replies for
// unauthorized requests.
func Protected(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !AllowDebugAccess(r) {
msg := "debug access denied"
if DevMode {
ipStr, _, _ := net.SplitHostPort(r.RemoteAddr)
msg += fmt.Sprintf("; to permit access, set TS_ALLOW_DEBUG_IP=%v", ipStr)
}
http.Error(w, msg, http.StatusForbidden)
return
}
h.ServeHTTP(w, r)
})
}
// Port80Handler is the handler to be given to
// autocert.Manager.HTTPHandler. The inner handler is the mux
// returned by NewMux containing registered /debug handlers.
type Port80Handler struct {
Main http.Handler
// FQDN is used to redirect incoming requests to https://<FQDN>.
// If it is not set, the hostname is calculated from the incoming
// request.
FQDN string
}
func (h Port80Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := r.RequestURI
if path == "/debug" || strings.HasPrefix(path, "/debug") {
h.Main.ServeHTTP(w, r)
return
}
if r.Method != "GET" && r.Method != "HEAD" {
http.Error(w, "Use HTTPS", http.StatusBadRequest)
return
}
if path == "/" && AllowDebugAccess(r) {
// Redirect authorized user to the debug handler.
path = "/debug/"
}
host := cmp.Or(h.FQDN, r.Host)
target := "https://" + host + path
http.Redirect(w, r, target, http.StatusFound)
}
// ReturnHandler is like net/http.Handler, but the handler can return an
// error instead of writing to its ResponseWriter.
type ReturnHandler interface {
// ServeHTTPReturn is like http.Handler.ServeHTTP, except that
// it can choose to return an error instead of writing to its
// http.ResponseWriter.
//
// If ServeHTTPReturn returns an error, it caller should handle
// an error by serving an HTTP 500 response to the user. The
// error details should not be sent to the client, as they may
// contain sensitive information. If the error is an
// HTTPError, though, callers should use the HTTP response
// code and message as the response to the client.
ServeHTTPReturn(http.ResponseWriter, *http.Request) error
}
// BucketedStatsOptions describes tsweb handler options surrounding
// the generation of metrics, grouped into buckets.
type BucketedStatsOptions struct {
// Bucket returns which bucket the given request is in.
// If nil, [NormalizedPath] is used to compute the bucket.
Bucket func(req *http.Request) string
// If non-nil, Started maintains a counter of all requests which
// have begun processing.
Started *metrics.LabelMap
// If non-nil, Finished maintains a counter of all requests which
// have finished processing with success (that is, the HTTP handler has
// returned).
Finished *metrics.LabelMap
}
// normalizePathRegex matches components in a HTTP request path
// that should be replaced.
//
// See: https://regex101.com/r/WIfpaR/3 for the explainer and test cases.
var normalizePathRegex = regexp.MustCompile("([a-fA-F0-9]{9,}|([^\\/])+\\.([^\\/]){2,}|((n|k|u|L|t|S)[a-zA-Z0-9]{5,}(CNTRL|Djz1H|LV5CY|mxgaY|jNy1b))|(([^\\/])+\\@passkey))")
// NormalizedPath returns the given path with the following modifications:
//
// - any query parameters are removed
// - any path component with a hex string of 9 or more characters is
// replaced by an ellipsis
// - any path component containing a period with at least two characters
// after the period (i.e. an email or domain)
// - any path component consisting of a common Tailscale Stable ID
// - any path segment *@passkey.
func NormalizedPath(p string) string {
// Fastpath: No hex sequences in there we might have to trim.
// Avoids allocating.
if normalizePathRegex.FindStringIndex(p) == nil {
b, _, _ := strings.Cut(p, "?")
return b
}
// If we got here, there's at least one hex sequences we need to
// replace with an ellipsis.
replaced := normalizePathRegex.ReplaceAllString(p, "…")
b, _, _ := strings.Cut(replaced, "?")
return b
}
func (o *BucketedStatsOptions) bucketForRequest(r *http.Request) string {
if o.Bucket != nil {
return o.Bucket(r)
}
return NormalizedPath(r.URL.Path)
}
// HandlerOptions are options used by [StdHandler], containing both [LogOptions]
// used by [LogHandler] and [ErrorOptions] used by [ErrorHandler].
type HandlerOptions struct {
QuietLoggingIfSuccessful bool // if set, do not log successfully handled HTTP requests (200 and 304 status codes)
Logf logger.Logf
Now func() time.Time // if nil, defaults to time.Now
// If non-nil, StatusCodeCounters maintains counters
// of status codes for handled responses.
// The keys are "1xx", "2xx", "3xx", "4xx", and "5xx".
StatusCodeCounters *expvar.Map
// If non-nil, StatusCodeCountersFull maintains counters of status
// codes for handled responses.
// The keys are HTTP numeric response codes e.g. 200, 404, ...
StatusCodeCountersFull *expvar.Map
// If non-nil, BucketedStats computes and exposes statistics
// for each bucket based on the contained parameters.
BucketedStats *BucketedStatsOptions
// OnStart is called inline before ServeHTTP is called. Optional.
OnStart OnStartFunc
// OnError is called if the handler returned a HTTPError. This
// is intended to be used to present pretty error pages if
// the user agent is determined to be a browser.
OnError ErrorHandlerFunc
// OnCompletion is called inline when ServeHTTP is finished and gets
// useful data that the implementor can use for metrics. Optional.
OnCompletion OnCompletionFunc
}
// LogOptions are the options used by [LogHandler].
// These options are a subset of [HandlerOptions].
type LogOptions struct {
// Logf is used to log HTTP requests and responses.
Logf logger.Logf
// Now is a function giving the current time. Defaults to [time.Now].
Now func() time.Time
// QuietLogging suppresses all logging of handled HTTP requests, even if
// there are errors or status codes considered unsuccessful. Use this option
// to add your own logging in OnCompletion.
QuietLogging bool
// QuietLoggingIfSuccessful suppresses logging of handled HTTP requests
// where the request's response status code is 200 or 304.
QuietLoggingIfSuccessful bool
// StatusCodeCounters maintains counters of status code classes.
// The keys are "1xx", "2xx", "3xx", "4xx", and "5xx".
// If nil, no counting is done.
StatusCodeCounters *expvar.Map
// StatusCodeCountersFull maintains counters of status codes.
// The keys are HTTP numeric response codes e.g. 200, 404, ...
// If nil, no counting is done.
StatusCodeCountersFull *expvar.Map
// BucketedStats computes and exposes statistics for each bucket based on
// the contained parameters. If nil, no counting is done.
BucketedStats *BucketedStatsOptions
// OnStart is called inline before ServeHTTP is called. Optional.
OnStart OnStartFunc
// OnCompletion is called inline when ServeHTTP is finished and gets
// useful data that the implementor can use for metrics. Optional.
OnCompletion OnCompletionFunc
}
func (o HandlerOptions) logOptions() LogOptions {
return LogOptions{
QuietLoggingIfSuccessful: o.QuietLoggingIfSuccessful,
Logf: o.Logf,
Now: o.Now,
StatusCodeCounters: o.StatusCodeCounters,
StatusCodeCountersFull: o.StatusCodeCountersFull,
BucketedStats: o.BucketedStats,
OnStart: o.OnStart,
OnCompletion: o.OnCompletion,
}
}
func (opts LogOptions) withDefaults() LogOptions {
if opts.Logf == nil {
opts.Logf = logger.Discard
}
if opts.Now == nil {
opts.Now = time.Now
}
return opts
}
// ErrorOptions are options used by [ErrorHandler].
type ErrorOptions struct {
// Logf is used to record unexpected behaviours when returning HTTPError but
// different error codes have already been written to the client.
Logf logger.Logf
// OnError is called if the handler returned a HTTPError. This
// is intended to be used to present pretty error pages if
// the user agent is determined to be a browser.
OnError ErrorHandlerFunc
}
func (opts ErrorOptions) withDefaults() ErrorOptions {
if opts.Logf == nil {
opts.Logf = logger.Discard
}
if opts.OnError == nil {
opts.OnError = WriteHTTPError
}
return opts
}
func (opts HandlerOptions) errorOptions() ErrorOptions {
return ErrorOptions{
OnError: opts.OnError,
}
}
// ErrorHandlerFunc is called to present a error response.
type ErrorHandlerFunc func(http.ResponseWriter, *http.Request, HTTPError)
// OnStartFunc is called before ServeHTTP is called.
type OnStartFunc func(*http.Request, AccessLogRecord)
// OnCompletionFunc is called when ServeHTTP is finished and gets
// useful data that the implementor can use for metrics.
type OnCompletionFunc func(*http.Request, AccessLogRecord)
// ReturnHandlerFunc is an adapter to allow the use of ordinary
// functions as ReturnHandlers. If f is a function with the
// appropriate signature, ReturnHandlerFunc(f) is a ReturnHandler that
// calls f.
type ReturnHandlerFunc func(http.ResponseWriter, *http.Request) error
// A Middleware is a function that wraps an http.Handler to extend or modify
// its behaviour.
//
// The implementation of the wrapper is responsible for delegating its input
// request to the underlying handler, if appropriate.
type Middleware func(h http.Handler) http.Handler
// MiddlewareStack combines multiple middleware into a single middleware for
// decorating a [http.Handler]. The first middleware argument will be the first
// to process an incoming request, before passing the request onto subsequent
// middleware and eventually the wrapped handler.
//
// For example:
//
// MiddlewareStack(A, B)(h).ServeHTTP(w, r)
//
// calls in sequence:
//
// a.ServeHTTP(w, r)
// -> b.ServeHTTP(w, r)
// -> h.ServeHTTP(w, r)
//
// (where the lowercase handlers were generated by the uppercase middleware).
func MiddlewareStack(mw ...Middleware) Middleware {
if len(mw) == 1 {
return mw[0]
}
return func(h http.Handler) http.Handler {
for i := len(mw) - 1; i >= 0; i-- {
h = mw[i](h)
}
return h
}
}
// ServeHTTPReturn calls f(w, r).
func (f ReturnHandlerFunc) ServeHTTPReturn(w http.ResponseWriter, r *http.Request) error {
return f(w, r)
}
// StdHandler converts a ReturnHandler into a standard http.Handler.
// Handled requests are logged using opts.Logf, as are any errors.
// Errors are handled as specified by the ReturnHandler interface.
// Short-hand for LogHandler(ErrorHandler()).
func StdHandler(h ReturnHandler, opts HandlerOptions) http.Handler {
return LogHandler(ErrorHandler(h, opts.errorOptions()), opts.logOptions())
}
// LogHandler returns an http.Handler that logs to opts.Logf.
// It logs both successful and failing requests.
// The log line includes the first error returned to [ErrorHandler] within.
// The outer-most LogHandler(LogHandler(...)) does all of the logging.
// Inner LogHandler instance do nothing.
// Panics are swallowed and their stack traces are put in the error.
func LogHandler(h http.Handler, opts LogOptions) http.Handler {
return logHandler{h, opts.withDefaults()}
}
// ErrorHandler converts a [ReturnHandler] into a standard [http.Handler].
// Errors are handled as specified by the [ReturnHandler.ServeHTTPReturn] method.
// When wrapped in a [LogHandler], panics are added to the [AccessLogRecord];
// otherwise, panics continue up the stack.
func ErrorHandler(h ReturnHandler, opts ErrorOptions) http.Handler {
return errorHandler{h, opts.withDefaults()}
}
// errCallback is added to logHandler's request context so that errorHandler can
// pass errors back up the stack to logHandler.
var errCallback = ctxkey.New[func(HTTPError)]("tailscale.com/tsweb.errCallback", nil)
// logHandler is a http.Handler which logs the HTTP request.
// It injects an errCallback for errorHandler to augment the log message with
// a specific error.
type logHandler struct {
h http.Handler
opts LogOptions
}
func (h logHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// If there's already a logHandler up the chain, skip this one.
ctx := r.Context()
if errCallback.Has(ctx) {
h.h.ServeHTTP(w, r)
return
}
msg := AccessLogRecord{
Time: h.opts.Now(),
RemoteAddr: r.RemoteAddr,
Proto: r.Proto,
TLS: r.TLS != nil,
Host: r.Host,
Method: r.Method,
RequestURI: r.URL.RequestURI(),
UserAgent: r.UserAgent(),
Referer: r.Referer(),
RequestID: RequestIDFromContext(r.Context()),
}
if bs := h.opts.BucketedStats; bs != nil && bs.Started != nil && bs.Finished != nil {
bucket := bs.bucketForRequest(r)
var startRecorded bool
switch v := bs.Started.Map.Get(bucket).(type) {
case *expvar.Int:
// If we've already seen this bucket for, count it immediately.
// Otherwise, for newly seen paths, only count retroactively
// (so started-finished doesn't go negative) so we don't fill
// this LabelMap up with internet scanning spam.
v.Add(1)
startRecorded = true
}
defer func() {
// Only increment metrics for buckets that result in good HTTP statuses
// or when we know the start was already counted.
// Otherwise they get full of internet scanning noise. Only filtering 404
// gets most of the way there but there are also plenty of URLs that are
// almost right but result in 400s too. Seem easier to just only ignore
// all 4xx and 5xx.
if startRecorded {
bs.Finished.Add(bucket, 1)
} else if msg.Code < 400 {
// This is the first non-error request for this bucket,
// so count it now retroactively.
bs.Started.Add(bucket, 1)
bs.Finished.Add(bucket, 1)
}
}()
}
if fn := h.opts.OnStart; fn != nil {
fn(r, msg)
}
// Let errorHandler tell us what error it wrote to the client.
r = r.WithContext(errCallback.WithValue(ctx, func(e HTTPError) {
// Keep the deepest error.
if msg.Err != "" {
return
}
// Log the error.
if e.Msg != "" && e.Err != nil {
msg.Err = e.Msg + ": " + e.Err.Error()
} else if e.Err != nil {
msg.Err = e.Err.Error()
} else if e.Msg != "" {
msg.Err = e.Msg
}
// We log the code from the loggingResponseWriter, except for
// cancellation where we override with 499.
if reqCancelled(r, e.Err) {
msg.Code = 499
}
}))
lw := newLogResponseWriter(h.opts.Logf, w, r)
defer func() {
// If the handler panicked then make sure we include that in our error.
// Panics caught up errorHandler shouldn't appear here, unless the panic
// originates in one of its callbacks.
recovered := recover()
if recovered != nil {
if msg.Err == "" {
msg.Err = panic2err(recovered).Error()
} else {
msg.Err += "\n\nthen " + panic2err(recovered).Error()
}
}
h.logRequest(r, lw, msg)
}()
h.h.ServeHTTP(lw, r)
}
func (h logHandler) logRequest(r *http.Request, lw *loggingResponseWriter, msg AccessLogRecord) {
// Complete our access log from the loggingResponseWriter.
msg.Bytes = lw.bytes
msg.Seconds = h.opts.Now().Sub(msg.Time).Seconds()
switch {
case msg.Code != 0:
// Keep explicit codes from a few particular errors.
case lw.hijacked:
// Connection no longer belongs to us, just log that we
// switched protocols away from HTTP.
msg.Code = http.StatusSwitchingProtocols
case lw.code == 0:
// If the handler didn't write and didn't send a header, that still means 200.
// (See https://play.golang.org/p/4P7nx_Tap7p)
msg.Code = 200
default:
msg.Code = lw.code
}
// Keep track of the original response code when we've overridden it.
if lw.code != 0 && msg.Code != lw.code {
if msg.Err == "" {
msg.Err = fmt.Sprintf("(original code %d)", lw.code)
} else {
msg.Err = fmt.Sprintf("%s (original code %d)", msg.Err, lw.code)
}
}
if !h.opts.QuietLogging && !(h.opts.QuietLoggingIfSuccessful && (msg.Code == http.StatusOK || msg.Code == http.StatusNotModified)) {
h.opts.Logf("%s", msg)
}
if h.opts.OnCompletion != nil {
h.opts.OnCompletion(r, msg)
}
// Closing metrics.
if h.opts.StatusCodeCounters != nil {
h.opts.StatusCodeCounters.Add(responseCodeString(msg.Code/100), 1)
}
if h.opts.StatusCodeCountersFull != nil {
h.opts.StatusCodeCountersFull.Add(responseCodeString(msg.Code), 1)
}
}
func responseCodeString(code int) string {
if v, ok := responseCodeCache.Load(code); ok {
return v.(string)
}
var ret string
if code < 10 {
ret = fmt.Sprintf("%dxx", code)
} else {
ret = strconv.Itoa(code)
}
responseCodeCache.Store(code, ret)
return ret
}
// responseCodeCache memoizes the string form of HTTP response codes,
// so that the hot request-handling codepath doesn't have to allocate
// in strconv/fmt for every request.
//
// Keys are either full HTTP response code ints (200, 404) or "family"
// ints representing entire families (e.g. 2 for 2xx codes). Values
// are the string form of that code/family.
var responseCodeCache sync.Map
// loggingResponseWriter wraps a ResponseWriter and record the HTTP
// response code that gets sent, if any.
type loggingResponseWriter struct {
http.ResponseWriter
ctx context.Context
code int
bytes int
hijacked bool
logf logger.Logf
}
// newLogResponseWriter returns a loggingResponseWriter which uses's the logger
// from r, or falls back to logf. If a nil logger is given, the logs are
// discarded.
func newLogResponseWriter(logf logger.Logf, w http.ResponseWriter, r *http.Request) *loggingResponseWriter {
if lg, ok := logger.LogfKey.ValueOk(r.Context()); ok && lg != nil {
logf = lg
}
if logf == nil {
logf = logger.Discard
}
return &loggingResponseWriter{
ResponseWriter: w,
ctx: r.Context(),
logf: logf,
}
}
// WriteHeader implements [http.ResponseWriter].
func (lg *loggingResponseWriter) WriteHeader(statusCode int) {
if lg.code != 0 {
lg.logf("[unexpected] HTTP handler set statusCode twice (%d and %d)", lg.code, statusCode)
return
}
if lg.ctx.Err() == nil {
lg.code = statusCode
}
lg.ResponseWriter.WriteHeader(statusCode)
}
// Write implements [http.ResponseWriter].
func (lg *loggingResponseWriter) Write(bs []byte) (int, error) {
if lg.code == 0 {
lg.code = 200
}
n, err := lg.ResponseWriter.Write(bs)
lg.bytes += n
return n, err
}
// Hijack implements http.Hijacker. Note that hijacking can still fail
// because the wrapped ResponseWriter is not required to implement
// Hijacker, as this breaks HTTP/2.
func (lg *loggingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
h, ok := lg.ResponseWriter.(http.Hijacker)
if !ok {
return nil, nil, errors.New("ResponseWriter is not a Hijacker")
}
conn, buf, err := h.Hijack()
if err == nil {
lg.hijacked = true
}
return conn, buf, err
}
func (lg loggingResponseWriter) Flush() {
f, _ := lg.ResponseWriter.(http.Flusher)
if f == nil {
lg.logf("[unexpected] tried to Flush a ResponseWriter that can't flush")
return
}
f.Flush()
}
func (lg *loggingResponseWriter) Unwrap() http.ResponseWriter {
return lg.ResponseWriter
}
// errorHandler is an http.Handler that wraps a ReturnHandler to render the
// returned errors to the client and pass them back to any logHandlers.
type errorHandler struct {
rh ReturnHandler
opts ErrorOptions
}
// ServeHTTP implements the http.Handler interface.
func (h errorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Keep track of whether a response gets written.
lw, ok := w.(*loggingResponseWriter)
if !ok {
lw = newLogResponseWriter(h.opts.Logf, w, r)
}
var err error
defer func() {
// In case the handler panics, we want to recover and continue logging
// the error before logging it (or re-panicking if we couldn't log).
rec := recover()
if rec != nil {
err = panic2err(rec)
}
if err == nil {
return
}
if h.handleError(w, r, lw, err) {
return
}
if rec != nil {
// If we weren't able to log the panic somewhere, throw it up the
// stack to someone who can.
panic(rec)
}
}()
err = h.rh.ServeHTTPReturn(lw, r)
}
func (h errorHandler) handleError(w http.ResponseWriter, r *http.Request, lw *loggingResponseWriter, err error) bool {
var logged bool
// Extract a presentable, loggable error.
var hOK bool
hErr, hAsOK := errors.AsType[HTTPError](err)
if !hAsOK {
if hs, ok := errors.AsType[HTTPStatuser](err); ok {
hErr = hs.HTTPStatus()
hAsOK = true
}
}
if hAsOK {
hOK = true
if hErr.Code == 0 {
lw.logf("[unexpected] HTTPError %v did not contain an HTTP status code, sending internal server error", hErr)
hErr.Code = http.StatusInternalServerError
}
} else if v, ok := vizerror.As(err); ok {
hErr = Error(http.StatusInternalServerError, v.Error(), nil)
} else if reqCancelled(r, err) {
// 499 is the Nginx convention meaning "Client Closed Connection".
if errors.Is(err, context.Canceled) || errors.Is(err, http.ErrAbortHandler) {
hErr = Error(499, "", err)
} else {
hErr = Error(499, "", fmt.Errorf("%w: %w", context.Canceled, err))
}
} else {
// Omit the friendly message so HTTP logs show the bare error that was
// returned and we know it's not a HTTPError.
hErr = Error(http.StatusInternalServerError, "", err)
}
// Tell the logger what error we wrote back to the client.
if pb := errCallback.Value(r.Context()); pb != nil {
pb(hErr)
logged = true
}
if r.Context().Err() != nil {
return logged
}
if lw.code != 0 {
if hOK && hErr.Code != lw.code {
lw.logf("[unexpected] handler returned HTTPError %v, but already sent response with code %d", hErr, lw.code)
}
return logged
}
// Set a default error message from the status code. Do this after we pass
// the error back to the logger so that `return errors.New("oh")` logs as
// `"err": "oh"`, not `"err": "Internal Server Error: oh"`.
if hErr.Msg == "" {
switch hErr.Code {
case 499:
hErr.Msg = "Client Closed Request"
default:
hErr.Msg = http.StatusText(hErr.Code)
}
}
// If OnError panics before a response is written, write a bare 500 back.
// OnError panics are thrown further up the stack.
defer func() {
if lw.code == 0 {
if rec := recover(); rec != nil {
w.WriteHeader(http.StatusInternalServerError)
panic(rec)
}
}
}()
h.opts.OnError(w, r, hErr)
return logged
}
// panic2err converts a recovered value to an error containing the panic stack trace.
func panic2err(recovered any) error {
if recovered == nil {
return nil
}
if recovered == http.ErrAbortHandler {
return http.ErrAbortHandler
}
// Even if r is an error, do not wrap it as an error here as
// that would allow things like panic(vizerror.New("foo"))
// which is really hard to define the behavior of.
var stack [10000]byte
n := runtime.Stack(stack[:], false)
return &panicError{
rec: recovered,
stack: stack[:n],
}
}
// panicError is an error that contains a panic.
type panicError struct {
rec any
stack []byte
}
func (e *panicError) Error() string {
return fmt.Sprintf("panic: %v\n\n%s", e.rec, e.stack)
}
func (e *panicError) Unwrap() error {
err, _ := e.rec.(error)
return err
}
// reqCancelled returns true if err is http.ErrAbortHandler or r.Context.Err()
// is context.Canceled.
func reqCancelled(r *http.Request, err error) bool {
return errors.Is(err, http.ErrAbortHandler) || r.Context().Err() == context.Canceled
}
// WriteHTTPError is the default error response formatter.
func WriteHTTPError(w http.ResponseWriter, r *http.Request, e HTTPError) {
// Don't write a response if we've hit a cancellation/abort.
if r.Context().Err() != nil || errors.Is(e.Err, http.ErrAbortHandler) {
return
}
// Default headers set by http.Error.
h := w.Header()
h.Set("Content-Type", "text/plain; charset=utf-8")
h.Set("X-Content-Type-Options", "nosniff")
// Custom headers from the error.
maps.Copy(h, e.Header)
// Write the msg back to the user.
w.WriteHeader(e.Code)
fmt.Fprint(w, e.Msg)
// If it's a plaintext message, add line breaks and RequestID.
if strings.HasPrefix(h.Get("Content-Type"), "text/plain") {
io.WriteString(w, "\n")
if id := RequestIDFromContext(r.Context()); id != "" {
io.WriteString(w, id.String())
io.WriteString(w, "\n")
}
}
}
// HTTPStatuser is an optional interface implemented by errors that
// carry an intended HTTP response. Handlers translating errors to
// HTTP should honour the returned HTTPError rather than defaulting to
// 500.
type HTTPStatuser interface {
error
HTTPStatus() HTTPError
}
// HTTPError is an error with embedded HTTP response information.
//
// It is the error type to be (optionally) used by Handler.ServeHTTPReturn.
type HTTPError struct {
Code int // HTTP response code to send to client; 0 means 500
Msg string // Response body to send to client
Err error // Detailed error to log on the server
Header http.Header // Optional set of HTTP headers to set in the response
}
// Error implements the error interface.
func (e HTTPError) Error() string { return fmt.Sprintf("httperror{%d, %q, %v}", e.Code, e.Msg, e.Err) }
func (e HTTPError) Unwrap() error { return e.Err }
// Error returns an HTTPError containing the given information.
func Error(code int, msg string, err error) HTTPError {
return HTTPError{Code: code, Msg: msg, Err: err}
}
// VarzHandler writes expvar values as Prometheus metrics.
// TODO: migrate all users to varz.Handler or promvarz.Handler and remove this.
func VarzHandler(w http.ResponseWriter, r *http.Request) {
varz.Handler(w, r)
}
// CleanRedirectURL ensures that urlStr is a valid redirect URL to the
// current server, or one of allowedHosts. Returns the cleaned URL or
// a validation error.
func CleanRedirectURL(urlStr string, allowedHosts []string) (*url.URL, error) {
if urlStr == "" {
return &url.URL{}, nil
}
// In some places, we unfortunately query-escape the redirect URL
// too many times, and end up needing to redirect to a URL that's
// still escaped by one level. Try to unescape the input.
unescaped, err := url.QueryUnescape(urlStr)
if err == nil && unescaped != urlStr {
urlStr = unescaped
}
// Go's URL parser and browser URL parsers disagree on the meaning
// of malformed HTTP URLs. Given the input https:/evil.com, Go
// parses it as hostname="", path="/evil.com". Browsers parse it
// as hostname="evil.com", path="". This means that, using
// malformed URLs, an attacker could trick us into approving of a
// "local" redirect that in fact sends people elsewhere.
//
// This very blunt check enforces that we'll only process
// redirects that are definitely well-formed URLs.
//
// Note that the check for just / also allows URLs of the form
// "//foo.com/bar", which are scheme-relative redirects. These
// must be handled with care below when determining whether a
// redirect is relative to the current host. Notably,
// url.URL.IsAbs reports // URLs as relative, whereas we want to
// treat them as absolute redirects and verify the target host.
if !hasSafeRedirectPrefix(urlStr) {
return nil, fmt.Errorf("invalid redirect URL %q", urlStr)
}
url, err := url.Parse(urlStr)
if err != nil {
return nil, fmt.Errorf("invalid redirect URL %q: %w", urlStr, err)
}
// Redirects to self are always allowed. A self redirect must
// start with url.Path, all prior URL sections must be empty.
isSelfRedirect := url.Scheme == "" && url.Opaque == "" && url.User == nil && url.Host == ""
if isSelfRedirect {
return url, nil
}
for _, allowed := range allowedHosts {
if strings.EqualFold(allowed, url.Hostname()) {
return url, nil
}
}
return nil, fmt.Errorf("disallowed target host %q in redirect URL %q", url.Hostname(), urlStr)
}
// hasSafeRedirectPrefix reports whether url starts with a slash, or
// one of the case-insensitive strings "http://" or "https://".
func hasSafeRedirectPrefix(url string) bool {
if len(url) >= 1 && url[0] == '/' {
return true
}
const http = "http://"
if len(url) >= len(http) && strings.EqualFold(url[:len(http)], http) {
return true
}
const https = "https://"
if len(url) >= len(https) && strings.EqualFold(url[:len(https)], https) {
return true
}
return false
}
// AddBrowserHeaders sets various HTTP security headers for browser-facing endpoints.
//
// The specific headers:
// - require HTTPS access (HSTS)
// - disallow iframe embedding
// - mitigate MIME confusion attacks
//
// These headers are based on
// https://infosec.mozilla.org/guidelines/web_security
func AddBrowserHeaders(w http.ResponseWriter) {
w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
w.Header().Set("Content-Security-Policy", "default-src 'self'; frame-ancestors 'none'; form-action 'self'; base-uri 'self'; block-all-mixed-content; object-src 'none'")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-Content-Type-Options", "nosniff")
}
// BrowserHeaderHandler wraps the provided http.Handler with a call to
// AddBrowserHeaders.
func BrowserHeaderHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
AddBrowserHeaders(w)
h.ServeHTTP(w, r)
})
}
// BrowserHeaderHandlerFunc wraps the provided http.HandlerFunc with a call to
// AddBrowserHeaders.
func BrowserHeaderHandlerFunc(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
AddBrowserHeaders(w)
h.ServeHTTP(w, r)
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !android
package varz
import (
"bytes"
"expvar"
"os"
"runtime/debug"
"strings"
)
func init() {
var capable any = hostGOAMD64Level()
expvar.Publish("gauge_goamd64_capable", expvar.Func(func() any { return capable }))
var compiled any = compiledGOAMD64Level()
expvar.Publish("gauge_goamd64_compiled", expvar.Func(func() any { return compiled }))
}
// goamd64LevelFlags maps each x86-64 microarchitecture level to the
// /proc/cpuinfo flag names it requires beyond the previous level.
// It matches the per-level feature checks in Go's runtime/asm_amd64.s.
// Note that /proc/cpuinfo spells SSE3 as "pni" and LZCNT as "abm",
// and the kernel clears the AVX and AVX-512 flags when the OS lacks
// XSAVE support, so OSXSAVE needs no separate check.
var goamd64LevelFlags = [...][]string{
2: {"cx16", "lahf_lm", "popcnt", "pni", "sse4_1", "sse4_2", "ssse3"},
3: {"abm", "avx", "avx2", "bmi1", "bmi2", "f16c", "fma", "movbe"},
4: {"avx512f", "avx512bw", "avx512cd", "avx512dq", "avx512vl"},
}
// hostGOAMD64Level returns the maximum GOAMD64 microarchitecture
// level (1-4) that the host CPU supports, regardless of what level
// this binary was compiled for. It returns 0 on error.
func hostGOAMD64Level() int {
cpuinfo, err := os.ReadFile("/proc/cpuinfo")
if err != nil {
return 0
}
for line := range bytes.Lines(cpuinfo) {
name, rest, ok := bytes.Cut(line, []byte(":"))
if ok && string(bytes.TrimSpace(name)) == "flags" {
return goamd64Level(strings.Fields(string(rest)))
}
}
return 0
}
// goamd64Level returns the maximum GOAMD64 level (1-4) supported by
// a CPU with the given /proc/cpuinfo flags.
func goamd64Level(flags []string) int {
has := make(map[string]bool, len(flags))
for _, f := range flags {
has[f] = true
}
level := 1
for next := 2; next < len(goamd64LevelFlags); next++ {
for _, f := range goamd64LevelFlags[next] {
if !has[f] {
return level
}
}
level = next
}
return level
}
// compiledGOAMD64Level returns the GOAMD64 microarchitecture level
// (1-4) that this binary was compiled for, as recorded in its build
// info. It returns 0 if unknown.
func compiledGOAMD64Level() int {
bi, ok := debug.ReadBuildInfo()
if !ok {
return 0
}
for _, s := range bi.Settings {
if s.Key == "GOAMD64" {
if v, ok := strings.CutPrefix(s.Value, "v"); ok && len(v) == 1 && v[0] >= '1' && v[0] <= '4' {
return int(v[0] - '0')
}
return 0
}
}
return 0
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package varz contains code to export metrics in Prometheus format.
package varz
import (
"bufio"
"cmp"
"expvar"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"reflect"
"runtime"
"sort"
"strconv"
"strings"
"sync"
"time"
"unicode"
"unicode/utf8"
"golang.org/x/exp/constraints"
"tailscale.com/metrics"
"tailscale.com/syncs"
"tailscale.com/types/logger"
"tailscale.com/version"
)
// StaticStringVar returns a new expvar.Var that always returns s.
func StaticStringVar(s string) expvar.Var {
var v any = s // box s into an interface just once
return expvar.Func(func() any { return v })
}
func init() {
expvar.Publish("process_start_unix_time", expvar.Func(func() any { return timeStart.Unix() }))
expvar.Publish("version", StaticStringVar(version.Long()))
expvar.Publish("go_version", StaticStringVar(runtime.Version()))
expvar.Publish("counter_uptime_sec", expvar.Func(func() any { return int64(Uptime().Seconds()) }))
expvar.Publish("gauge_goroutines", expvar.Func(func() any { return runtime.NumGoroutine() }))
if v := nodeBootTime(); v != 0 {
var vi any = v // box once
// The name matches what Prometheus's node exporter uses
// for the same value.
expvar.Publish("node_boot_time_seconds", expvar.Func(func() any { return vi }))
}
}
// nodeBootTime returns the machine's boot time in Unix seconds,
// as reported by the "btime" line of Linux's /proc/stat.
// It returns 0 if unavailable, such as on non-Linux systems.
var nodeBootTime = sync.OnceValue(func() int64 {
stat, err := os.ReadFile("/proc/stat")
if err != nil {
return 0
}
for line := range strings.Lines(string(stat)) {
if rest, ok := strings.CutPrefix(line, "btime "); ok {
sec, err := strconv.ParseInt(strings.TrimSpace(rest), 10, 64)
if err != nil {
return 0
}
return sec
}
}
return 0
})
const (
gaugePrefix = "gauge_"
counterPrefix = "counter_"
labelMapPrefix = "labelmap_"
histogramPrefix = "histogram_"
)
// prefixesToTrim contains key prefixes to remove when exporting and sorting metrics.
var prefixesToTrim = []string{gaugePrefix, counterPrefix, labelMapPrefix, histogramPrefix}
var timeStart = time.Now()
func Uptime() time.Duration { return time.Since(timeStart).Round(time.Second) }
// WritePrometheusExpvar writes kv to w in Prometheus metrics format.
//
// See VarzHandler for conventions. This is exported primarily for
// people to test their varz.
func WritePrometheusExpvar(w io.Writer, kv expvar.KeyValue) {
writePromExpVar(w, "", kv)
}
type prometheusMetricDetails struct {
Name string
Type string
Label string
}
var prometheusMetricCache sync.Map // string => *prometheusMetricDetails
func prometheusMetric(prefix string, key string) (string, string, string) {
cachekey := prefix + key
if v, ok := prometheusMetricCache.Load(cachekey); ok {
d := v.(*prometheusMetricDetails)
return d.Name, d.Type, d.Label
}
var typ string
var label string
switch {
case strings.HasPrefix(key, gaugePrefix):
typ = "gauge"
key = strings.TrimPrefix(key, gaugePrefix)
case strings.HasPrefix(key, counterPrefix):
typ = "counter"
key = strings.TrimPrefix(key, counterPrefix)
case strings.HasPrefix(key, histogramPrefix):
typ = "histogram"
key = strings.TrimPrefix(key, histogramPrefix)
}
if after, ok := strings.CutPrefix(key, labelMapPrefix); ok {
key = after
if a, b, ok := strings.Cut(key, "_"); ok {
label, key = a, b
}
}
// Convert the metric to a valid Prometheus metric name.
// "Metric names may contain ASCII letters, digits, underscores, and colons.
// It must match the regex [a-zA-Z_:][a-zA-Z0-9_:]*"
mapInvalidMetricRunes := func(r rune) rune {
if r >= 'a' && r <= 'z' ||
r >= 'A' && r <= 'Z' ||
r >= '0' && r <= '9' ||
r == '_' || r == ':' {
return r
}
if r < utf8.RuneSelf && unicode.IsPrint(r) {
return '_'
}
return -1
}
metricName := strings.Map(mapInvalidMetricRunes, prefix+key)
if metricName == "" || unicode.IsDigit(rune(metricName[0])) {
metricName = "_" + metricName
}
d := &prometheusMetricDetails{
Name: metricName,
Type: typ,
Label: label,
}
prometheusMetricCache.Store(cachekey, d)
return d.Name, d.Type, d.Label
}
func writePromExpVar(w io.Writer, prefix string, kv expvar.KeyValue) {
key := kv.Key
name, typ, label := prometheusMetric(prefix, key)
switch v := kv.Value.(type) {
case *expvar.Int:
fmt.Fprintf(w, "# TYPE %s %s\n%s %v\n", name, cmp.Or(typ, "counter"), name, v.Value())
return
case *syncs.ShardedInt:
fmt.Fprintf(w, "# TYPE %s %s\n%s %v\n", name, cmp.Or(typ, "counter"), name, v.Value())
return
case *expvar.Float:
fmt.Fprintf(w, "# TYPE %s %s\n%s %v\n", name, cmp.Or(typ, "gauge"), name, v.Value())
return
case *metrics.Set:
v.Do(func(kv expvar.KeyValue) {
writePromExpVar(w, name+"_", kv)
})
return
case PrometheusWriter:
v.WritePrometheus(w, name)
return
case PrometheusMetricsReflectRooter:
root := v.PrometheusMetricsReflectRoot()
rv := reflect.ValueOf(root)
if rv.Type().Kind() == reflect.Pointer {
if rv.IsNil() {
return
}
rv = rv.Elem()
}
if rv.Type().Kind() != reflect.Struct {
fmt.Fprintf(w, "# skipping expvar %q; unknown root type\n", name)
return
}
foreachExportedStructField(rv, func(fieldOrJSONName, metricType string, rv reflect.Value) {
mname := name + "_" + fieldOrJSONName
switch rv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
fmt.Fprintf(w, "# TYPE %s %s\n%s %v\n", mname, metricType, mname, rv.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
fmt.Fprintf(w, "# TYPE %s %s\n%s %v\n", mname, metricType, mname, rv.Uint())
case reflect.Float32, reflect.Float64:
fmt.Fprintf(w, "# TYPE %s %s\n%s %v\n", mname, metricType, mname, rv.Float())
case reflect.Struct:
if rv.CanAddr() {
// Slight optimization, not copying big structs if they're addressable:
writePromExpVar(w, name+"_", expvar.KeyValue{Key: fieldOrJSONName, Value: expVarPromStructRoot{rv.Addr().Interface()}})
} else {
writePromExpVar(w, name+"_", expvar.KeyValue{Key: fieldOrJSONName, Value: expVarPromStructRoot{rv.Interface()}})
}
}
return
})
return
}
if typ == "" {
var funcRet string
if f, ok := kv.Value.(expvar.Func); ok {
v := f()
if ms, ok := v.(runtime.MemStats); ok && name == "memstats" {
writeMemstats(w, &ms)
return
}
if vs, ok := v.(string); ok && strings.HasSuffix(name, "version") {
if name == "version" {
fmt.Fprintf(w, "%s{version=%q,binary=%q} 1\n", name, vs, binaryName())
} else {
fmt.Fprintf(w, "%s{version=%q} 1\n", name, vs)
}
return
}
switch v := v.(type) {
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64:
fmt.Fprintf(w, "%s %v\n", name, v)
return
}
funcRet = fmt.Sprintf(" returning %T", v)
}
switch kv.Value.(type) {
default:
fmt.Fprintf(w, "# skipping expvar %q (Go type %T%s) with undeclared Prometheus type\n", name, kv.Value, funcRet)
return
case *metrics.LabelMap, *expvar.Map:
// Permit typeless LabelMap and expvar.Map for
// compatibility with old expvar-registered
// metrics.LabelMap.
}
}
switch v := kv.Value.(type) {
case expvar.Func:
val := v()
switch val.(type) {
case float64, int64, int:
fmt.Fprintf(w, "# TYPE %s %s\n%s %v\n", name, typ, name, val)
default:
fmt.Fprintf(w, "# skipping expvar func %q returning unknown type %T\n", name, val)
}
case *metrics.LabelMap:
if typ != "" {
fmt.Fprintf(w, "# TYPE %s %s\n", name, typ)
}
// IntMap uses expvar.Map on the inside, which presorts
// keys. The output ordering is deterministic.
v.Do(func(kv expvar.KeyValue) {
fmt.Fprintf(w, "%s{%s=%q} %v\n", name, cmp.Or(v.Label, "label"), kv.Key, kv.Value)
})
case *metrics.Histogram:
v.PromExport(w, name)
case *expvar.Map:
if label != "" && typ != "" {
fmt.Fprintf(w, "# TYPE %s %s\n", name, typ)
v.Do(func(kv expvar.KeyValue) {
switch kv.Value.(type) {
case *expvar.Int, *expvar.Float:
fmt.Fprintf(w, "%s{%s=%q} %v\n", name, label, kv.Key, kv.Value)
default:
fmt.Fprintf(w, "# skipping %q expvar map key %q with unknown value type %T\n", name, kv.Key, kv.Value)
}
})
} else {
v.Do(func(kv expvar.KeyValue) {
switch kv.Value.(type) {
case *expvar.Int, *expvar.Float:
fmt.Fprintf(w, "%s_%s %v\n", name, kv.Key, kv.Value)
default:
fmt.Fprintf(w, "# skipping %q expvar map key %q with unknown value type %T\n", name, kv.Key, kv.Value)
}
})
}
}
}
// PrometheusWriter is the interface implemented by metrics that can write
// themselves into Prometheus exposition format.
//
// As of 2024-03-25, this is only *metrics.MultiLabelMap.
type PrometheusWriter interface {
WritePrometheus(w io.Writer, name string)
}
var sortedKVsPool = &sync.Pool{New: func() any { return new(sortedKVs) }}
// sortedKV is a KeyValue with a sort key.
type sortedKV struct {
expvar.KeyValue
sortKey string // KeyValue.Key with type prefix removed
}
type sortedKVs struct {
kvs []sortedKV
}
// Handler is an HTTP handler to write expvar values into the
// prometheus export format:
//
// https://github.com/prometheus/docs/blob/master/content/docs/instrumenting/exposition_formats.md
//
// It makes the following assumptions:
//
// - *expvar.Int are counters (unless marked as a gauge_; see below)
// - a *tailscale/metrics.Set is descended into, joining keys with
// underscores. So use underscores as your metric names.
// - an expvar named starting with "gauge_" or "counter_" is of that
// Prometheus type, and has that prefix stripped.
// - anything else is untyped and thus not exported.
// - expvar.Func can return an int or int64 (for now) and anything else
// is not exported.
//
// This will evolve over time, or perhaps be replaced.
func Handler(w http.ResponseWriter, r *http.Request) {
ExpvarDoHandler(expvarDo)(w, r)
}
// ExpvarDoHandler handler returns a Handler like above, but takes an optional
// expvar.Do func allow the usage of alternative containers of metrics, other
// than the global expvar.Map.
func ExpvarDoHandler(expvarDoFunc func(f func(expvar.KeyValue))) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain;version=0.0.4;charset=utf-8")
s := sortedKVsPool.Get().(*sortedKVs)
defer sortedKVsPool.Put(s)
s.kvs = s.kvs[:0]
expvarDoFunc(func(kv expvar.KeyValue) {
s.kvs = append(s.kvs, sortedKV{kv, removeTypePrefixes(kv.Key)})
})
sort.Slice(s.kvs, func(i, j int) bool {
return s.kvs[i].sortKey < s.kvs[j].sortKey
})
for _, e := range s.kvs {
writePromExpVar(w, "", e.KeyValue)
}
}
}
var binaryName = sync.OnceValue(func() string {
exe, err := os.Executable()
if err != nil {
return ""
}
exe2, err := filepath.EvalSymlinks(exe)
if err != nil {
return filepath.Base(exe)
}
return filepath.Base(exe2)
})
// PrometheusMetricsReflectRooter is an optional interface that expvar.Var implementations
// can implement to indicate that they should be walked recursively with reflect to find
// sets of fields to export.
type PrometheusMetricsReflectRooter interface {
expvar.Var
// PrometheusMetricsReflectRoot returns the struct or struct pointer to walk.
PrometheusMetricsReflectRoot() any
}
var expvarDo = expvar.Do // pulled out for tests
func writeMemstat[V constraints.Integer | constraints.Float](bw *bufio.Writer, typ, name string, v V, help string) {
if help != "" {
bw.WriteString("# HELP memstats_")
bw.WriteString(name)
bw.WriteString(" ")
bw.WriteString(help)
bw.WriteByte('\n')
}
bw.WriteString("# TYPE memstats_")
bw.WriteString(name)
bw.WriteString(" ")
bw.WriteString(typ)
bw.WriteByte('\n')
bw.WriteString("memstats_")
bw.WriteString(name)
bw.WriteByte(' ')
rt := reflect.TypeOf(v)
switch {
case rt == reflect.TypeFor[int]() ||
rt == reflect.TypeFor[uint]() ||
rt == reflect.TypeFor[int8]() ||
rt == reflect.TypeFor[uint8]() ||
rt == reflect.TypeFor[int16]() ||
rt == reflect.TypeFor[uint16]() ||
rt == reflect.TypeFor[int32]() ||
rt == reflect.TypeFor[uint32]() ||
rt == reflect.TypeFor[int64]() ||
rt == reflect.TypeFor[uint64]() ||
rt == reflect.TypeFor[uintptr]():
bw.Write(strconv.AppendInt(bw.AvailableBuffer(), int64(v), 10))
case rt == reflect.TypeFor[float32]() || rt == reflect.TypeFor[float64]():
bw.Write(strconv.AppendFloat(bw.AvailableBuffer(), float64(v), 'f', -1, 64))
}
bw.WriteByte('\n')
}
func writeMemstats(w io.Writer, ms *runtime.MemStats) {
fmt.Fprintf(w, "%v", logger.ArgWriter(func(bw *bufio.Writer) {
writeMemstat(bw, "gauge", "heap_alloc", ms.HeapAlloc, "current bytes of allocated heap objects (up/down smoothly)")
writeMemstat(bw, "counter", "total_alloc", ms.TotalAlloc, "cumulative bytes allocated for heap objects")
writeMemstat(bw, "gauge", "sys", ms.Sys, "total bytes of memory obtained from the OS")
writeMemstat(bw, "counter", "mallocs", ms.Mallocs, "cumulative count of heap objects allocated")
writeMemstat(bw, "counter", "frees", ms.Frees, "cumulative count of heap objects freed")
writeMemstat(bw, "counter", "num_gc", ms.NumGC, "number of completed GC cycles")
writeMemstat(bw, "gauge", "gc_cpu_fraction", ms.GCCPUFraction, "fraction of CPU time used by GC")
}))
}
// sortedStructField is metadata about a struct field used both for sorting once
// (by structTypeSortedFields) and at serving time (by
// foreachExportedStructField).
type sortedStructField struct {
Index int // index of struct field in struct
Name string // struct field name, or "json" name
SortName string // Name with "foo_" type prefixes removed
MetricType string // the "metrictype" struct tag
StructFieldType *reflect.StructField
}
var structSortedFieldsCache sync.Map // reflect.Type => []sortedStructField
// structTypeSortedFields returns the sorted fields of t, caching as needed.
func structTypeSortedFields(t reflect.Type) []sortedStructField {
if v, ok := structSortedFieldsCache.Load(t); ok {
return v.([]sortedStructField)
}
fields := make([]sortedStructField, 0, t.NumField())
for sf := range t.Fields() {
name := sf.Name
if v := sf.Tag.Get("json"); v != "" {
v, _, _ = strings.Cut(v, ",")
if v == "-" {
// Skip it, regardless of its metrictype.
continue
}
if v != "" {
name = v
}
}
fields = append(fields, sortedStructField{
Index: sf.Index[0],
Name: name,
SortName: removeTypePrefixes(name),
MetricType: sf.Tag.Get("metrictype"),
StructFieldType: &sf,
})
}
sort.Slice(fields, func(i, j int) bool {
return fields[i].SortName < fields[j].SortName
})
structSortedFieldsCache.Store(t, fields)
return fields
}
// removeTypePrefixes returns s with the first "foo_" prefix in prefixesToTrim
// removed.
func removeTypePrefixes(s string) string {
for _, prefix := range prefixesToTrim {
if trimmed, ok := strings.CutPrefix(s, prefix); ok {
return trimmed
}
}
return s
}
// foreachExportedStructField iterates over the fields in sorted order of
// their name, after removing metric prefixes. This is not necessarily the
// order they were declared in the struct
func foreachExportedStructField(rv reflect.Value, f func(fieldOrJSONName, metricType string, rv reflect.Value)) {
t := rv.Type()
for _, ssf := range structTypeSortedFields(t) {
sf := ssf.StructFieldType
if ssf.MetricType != "" || sf.Type.Kind() == reflect.Struct {
f(ssf.Name, ssf.MetricType, rv.Field(ssf.Index))
} else if sf.Type.Kind() == reflect.Pointer && sf.Type.Elem().Kind() == reflect.Struct {
fv := rv.Field(ssf.Index)
if !fv.IsNil() {
f(ssf.Name, ssf.MetricType, fv.Elem())
}
}
}
}
type expVarPromStructRoot struct{ v any }
func (r expVarPromStructRoot) PrometheusMetricsReflectRoot() any { return r.v }
func (r expVarPromStructRoot) String() string { panic("unused") }
var (
_ PrometheusMetricsReflectRooter = expVarPromStructRoot{}
_ expvar.Var = expVarPromStructRoot{}
)
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package bools contains the [Int], [Compare], and [IfElse] functions.
package bools
// Int returns 1 for true and 0 for false.
func Int(v bool) int {
if v {
return 1
} else {
return 0
}
}
// Compare compares two boolean values as if false is ordered before true.
func Compare[T ~bool](x, y T) int {
switch {
case x == false && y == true:
return -1
case x == true && y == false:
return +1
default:
return 0
}
}
// IfElse is a ternary operator that returns trueVal if condExpr is true
// otherwise it returns falseVal.
// IfElse(c, a, b) is roughly equivalent to (c ? a : b) in languages like C.
func IfElse[T any](condExpr bool, trueVal T, falseVal T) T {
if condExpr {
return trueVal
} else {
return falseVal
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package dnstype defines types for working with DNS.
package dnstype
//go:generate go run tailscale.com/cmd/viewer --type=Resolver --clonefunc=true
import (
"net/netip"
"slices"
)
// Resolver is the configuration for one DNS resolver.
type Resolver struct {
// Addr is the address of the DNS resolver, one of:
// - A plain IP address for a "classic" UDP+TCP DNS resolver.
// This is the common format as sent by the control plane.
// - An IP:port, for tests.
// - "https://resolver.com/path" for DNS over HTTPS; currently
// as of 2022-09-08 only used for certain well-known resolvers
// (see the publicdns package) for which the IP addresses to dial DoH are
// known ahead of time, so bootstrap DNS resolution is not required.
// - "http://node-address:port/path" for DNS over HTTP over WireGuard. This
// is implemented in the PeerAPI for exit nodes and app connectors.
// - [TODO] "tls://resolver.com" for DNS over TCP+TLS
Addr string `json:",omitempty"`
// BootstrapResolution is an optional suggested resolution for the
// DoT/DoH resolver, if the resolver URL does not reference an IP
// address directly.
// BootstrapResolution may be empty, in which case clients should
// look up the DoT/DoH server using their local "classic" DNS
// resolver.
//
// As of 2022-09-08, BootstrapResolution is not yet used.
BootstrapResolution []netip.Addr `json:",omitempty"`
// UseWithExitNode designates that this resolver should continue to be used when an
// exit node is in use. Normally, DNS resolution is delegated to the exit node but
// there are situations where it is preferable to still use a Split DNS server and/or
// global DNS server instead of the exit node.
UseWithExitNode bool `json:",omitempty"`
}
// IPPort returns r.Addr as an IP address and port if either
// r.Addr is an IP address (the common case) or if r.Addr
// is an IP:port (as done in tests).
func (r *Resolver) IPPort() (ipp netip.AddrPort, ok bool) {
if r.Addr == "" || r.Addr[0] == 'h' || r.Addr[0] == 't' {
// Fast path to avoid ParseIP error allocation for obviously not IP
// cases.
return
}
if ip, err := netip.ParseAddr(r.Addr); err == nil {
return netip.AddrPortFrom(ip, 53), true
}
if ipp, err := netip.ParseAddrPort(r.Addr); err == nil {
return ipp, true
}
return
}
// Equal reports whether r and other are equal.
func (r *Resolver) Equal(other *Resolver) bool {
if r == nil || other == nil {
return r == other
}
if r == other {
return true
}
return r.Addr == other.Addr &&
slices.Equal(r.BootstrapResolution, other.BootstrapResolution) &&
r.UseWithExitNode == other.UseWithExitNode
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
package dnstype
import (
"net/netip"
)
// Clone makes a deep copy of Resolver.
// The result aliases no memory with the original.
func (src *Resolver) Clone() *Resolver {
if src == nil {
return nil
}
dst := new(Resolver)
*dst = *src
dst.BootstrapResolution = append(src.BootstrapResolution[:0:0], src.BootstrapResolution...)
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _ResolverCloneNeedsRegeneration = Resolver(struct {
Addr string
BootstrapResolution []netip.Addr
UseWithExitNode bool
}{})
// Clone duplicates src into dst and reports whether it succeeded.
// To succeed, <src, dst> must be of types <*T, *T> or <*T, **T>,
// where T is one of Resolver.
func Clone(dst, src any) bool {
switch src := src.(type) {
case *Resolver:
switch dst := dst.(type) {
case *Resolver:
*dst = *src.Clone()
return true
case **Resolver:
*dst = src.Clone()
return true
}
}
return false
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by tailscale/cmd/viewer; DO NOT EDIT.
package dnstype
import (
jsonv1 "encoding/json"
"errors"
"net/netip"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"tailscale.com/types/views"
)
//go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type=Resolver
// View returns a read-only view of Resolver.
func (p *Resolver) View() ResolverView {
return ResolverView{ж: p}
}
// ResolverView provides a read-only view over Resolver.
//
// Its methods should only be called if `Valid()` returns true.
type ResolverView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *Resolver
}
// Valid reports whether v's underlying value is non-nil.
func (v ResolverView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v ResolverView) AsStruct() *Resolver {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v ResolverView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v ResolverView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *ResolverView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x Resolver
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *ResolverView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x Resolver
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// Addr is the address of the DNS resolver, one of:
// - A plain IP address for a "classic" UDP+TCP DNS resolver.
// This is the common format as sent by the control plane.
// - An IP:port, for tests.
// - "https://resolver.com/path" for DNS over HTTPS; currently
// as of 2022-09-08 only used for certain well-known resolvers
// (see the publicdns package) for which the IP addresses to dial DoH are
// known ahead of time, so bootstrap DNS resolution is not required.
// - "http://node-address:port/path" for DNS over HTTP over WireGuard. This
// is implemented in the PeerAPI for exit nodes and app connectors.
// - [TODO] "tls://resolver.com" for DNS over TCP+TLS
func (v ResolverView) Addr() string { return v.ж.Addr }
// BootstrapResolution is an optional suggested resolution for the
// DoT/DoH resolver, if the resolver URL does not reference an IP
// address directly.
// BootstrapResolution may be empty, in which case clients should
// look up the DoT/DoH server using their local "classic" DNS
// resolver.
//
// As of 2022-09-08, BootstrapResolution is not yet used.
func (v ResolverView) BootstrapResolution() views.Slice[netip.Addr] {
return views.SliceOf(v.ж.BootstrapResolution)
}
// UseWithExitNode designates that this resolver should continue to be used when an
// exit node is in use. Normally, DNS resolution is delegated to the exit node but
// there are situations where it is preferable to still use a Split DNS server and/or
// global DNS server instead of the exit node.
func (v ResolverView) UseWithExitNode() bool { return v.ж.UseWithExitNode }
func (v ResolverView) Equal(v2 ResolverView) bool { return v.ж.Equal(v2.ж) }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _ResolverViewNeedsRegeneration = Resolver(struct {
Addr string
BootstrapResolution []netip.Addr
UseWithExitNode bool
}{})
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package geo
import (
"encoding/binary"
"errors"
"fmt"
"math"
"strconv"
)
// ErrBadPoint indicates that the point is malformed.
var ErrBadPoint = errors.New("not a valid point")
// Point represents a pair of latitude and longitude coordinates.
type Point struct {
lat Degrees
// lng180 is the longitude offset by +180° so the zero value is invalid
// and +0+0/ is Point{lat: +0.0, lng180: +180.0}.
lng180 Degrees
}
// MakePoint returns a Point representing a given latitude and longitude on
// a WGS 84 ellipsoid. The Coordinate Reference System is EPSG:4326.
// Latitude is wrapped to [-90°, +90°] and longitude to (-180°, +180°].
func MakePoint(latitude, longitude Degrees) Point {
lat, lng := float64(latitude), float64(longitude)
switch {
case math.IsNaN(lat) || math.IsInf(lat, 0):
// don’t wrap
case lat < -90 || lat > 90:
// Latitude wraps by flipping the longitude
lat = math.Mod(lat, 360.0)
switch {
case lat == 0.0:
lat = 0.0 // -0.0 == 0.0, but -0° is not valid
case lat < -270.0:
lat = +360.0 + lat
case lat < -90.0:
lat = -180.0 - lat
lng += 180.0
case lat > +270.0:
lat = -360.0 + lat
case lat > +90.0:
lat = +180.0 - lat
lng += 180.0
}
}
switch {
case lat == -90.0 || lat == +90.0:
// By convention, the north and south poles have longitude 0°.
lng = 0
case math.IsNaN(lng) || math.IsInf(lng, 0):
// don’t wrap
case lng <= -180.0 || lng > 180.0:
// Longitude wraps around normally
lng = math.Mod(lng, 360.0)
switch {
case lng == 0.0:
lng = 0.0 // -0.0 == 0.0, but -0° is not valid
case lng <= -180.0:
lng = +360.0 + lng
case lng > +180.0:
lng = -360.0 + lng
}
}
return Point{
lat: Degrees(lat),
lng180: Degrees(lng + 180.0),
}
}
// Valid reports if p is a valid point.
func (p Point) Valid() bool {
return !p.IsZero()
}
// LatLng reports the latitude and longitude.
func (p Point) LatLng() (lat, lng Degrees, err error) {
if p.IsZero() {
return 0 * Degree, 0 * Degree, ErrBadPoint
}
return p.lat, p.lng180 - 180.0*Degree, nil
}
// LatLng reports the latitude and longitude in float64. If err is nil, then lat
// and lng will never both be 0.0 to disambiguate between an empty struct and
// Null Island (0° 0°).
func (p Point) LatLngFloat64() (lat, lng float64, err error) {
dlat, dlng, err := p.LatLng()
if err != nil {
return 0.0, 0.0, err
}
if dlat == 0.0 && dlng == 0.0 {
// dlng must survive conversion to float32.
dlng = math.SmallestNonzeroFloat32
}
return float64(dlat), float64(dlng), err
}
// SphericalAngleTo returns the angular distance from p to q, calculated on a
// spherical Earth.
func (p Point) SphericalAngleTo(q Point) (Radians, error) {
pLat, pLng, pErr := p.LatLng()
qLat, qLng, qErr := q.LatLng()
switch {
case pErr != nil && qErr != nil:
return 0.0, fmt.Errorf("spherical distance from %v to %v: %w", p, q, errors.Join(pErr, qErr))
case pErr != nil:
return 0.0, fmt.Errorf("spherical distance from %v: %w", p, pErr)
case qErr != nil:
return 0.0, fmt.Errorf("spherical distance to %v: %w", q, qErr)
}
// The spherical law of cosines is accurate enough for close points when
// using float64.
//
// The haversine formula is an alternative, but it is poorly behaved
// when points are on opposite sides of the sphere.
rLat, rLng := float64(pLat.Radians()), float64(pLng.Radians())
sLat, sLng := float64(qLat.Radians()), float64(qLng.Radians())
cosA := math.Sin(rLat)*math.Sin(sLat) +
math.Cos(rLat)*math.Cos(sLat)*math.Cos(rLng-sLng)
// Subtle floating point imprecision can lead to cosA being outside
// the domain of arccosine [-1, 1]. Clamp the input to avoid NaN return.
cosA = min(max(-1.0, cosA), 1.0)
return Radians(math.Acos(cosA)), nil
}
// DistanceTo reports the great-circle distance between p and q, in meters.
func (p Point) DistanceTo(q Point) (Distance, error) {
r, err := p.SphericalAngleTo(q)
if err != nil {
return 0, err
}
return DistanceOnEarth(r.Turns()), nil
}
// String returns a space-separated pair of latitude and longitude, in decimal
// degrees. Positive latitudes are in the northern hemisphere, and positive
// longitudes are east of the prime meridian. If p was not initialized, this
// will return "nowhere".
func (p Point) String() string {
lat, lng, err := p.LatLng()
if err != nil {
if err == ErrBadPoint {
return "nowhere"
}
panic(err)
}
return lat.String() + " " + lng.String()
}
// AppendBinary implements [encoding.BinaryAppender]. The output consists of two
// float32s in big-endian byte order: latitude and longitude offset by 180°.
// If p is not a valid, the output will be an 8-byte zero value.
func (p Point) AppendBinary(b []byte) ([]byte, error) {
end := binary.BigEndian
b = end.AppendUint32(b, math.Float32bits(float32(p.lat)))
b = end.AppendUint32(b, math.Float32bits(float32(p.lng180)))
return b, nil
}
// MarshalBinary implements [encoding.BinaryMarshaler]. The output matches that
// of calling [Point.AppendBinary].
func (p Point) MarshalBinary() ([]byte, error) {
var b [8]byte
return p.AppendBinary(b[:0])
}
// UnmarshalBinary implements [encoding.BinaryUnmarshaler]. It expects input
// that was formatted by [Point.AppendBinary]: in big-endian byte order, a
// float32 representing latitude followed by a float32 representing longitude
// offset by 180°. If latitude and longitude fall outside valid ranges, then
// an error is returned.
func (p *Point) UnmarshalBinary(data []byte) error {
if len(data) < 8 { // Two uint32s are 8 bytes long
return fmt.Errorf("%w: not enough data: %q", ErrBadPoint, data)
}
end := binary.BigEndian
lat := Degrees(math.Float32frombits(end.Uint32(data[0:])))
if lat < -90*Degree || lat > 90*Degree {
return fmt.Errorf("%w: latitude outside [-90°, +90°]: %s", ErrBadPoint, lat)
}
lng180 := Degrees(math.Float32frombits(end.Uint32(data[4:])))
if lng180 != 0 && (lng180 < 0*Degree || lng180 > 360*Degree) {
// lng180 == 0 is OK: the zero value represents invalid points.
lng := lng180 - 180*Degree
return fmt.Errorf("%w: longitude outside (-180°, +180°]: %s", ErrBadPoint, lng)
}
p.lat = lat
p.lng180 = lng180
return nil
}
// AppendText implements [encoding.TextAppender]. The output is a point
// formatted as OGC Well-Known Text, as "POINT (longitude latitude)" where
// longitude and latitude are in decimal degrees. If p is not valid, the output
// will be "POINT EMPTY".
func (p Point) AppendText(b []byte) ([]byte, error) {
if p.IsZero() {
b = append(b, []byte("POINT EMPTY")...)
return b, nil
}
lat, lng, err := p.LatLng()
if err != nil {
return b, err
}
b = append(b, []byte("POINT (")...)
b = strconv.AppendFloat(b, float64(lng), 'f', -1, 64)
b = append(b, ' ')
b = strconv.AppendFloat(b, float64(lat), 'f', -1, 64)
b = append(b, ')')
return b, nil
}
// MarshalText implements [encoding.TextMarshaler]. The output matches that
// of calling [Point.AppendText].
func (p Point) MarshalText() ([]byte, error) {
var b [8]byte
return p.AppendText(b[:0])
}
// MarshalScalar implements [encoding.ScalarMarshaler].
// It produces the same output as MashalBinary, encoded in a uint64.
func (p Point) MarshalScalar() (uint64, error) {
b, err := p.MarshalBinary()
return binary.NativeEndian.Uint64(b), err
}
// UnmarshalScalar implements [encoding.ScalarUnmarshaler].
// It expects input formatted by MarshalScalar.
func (p *Point) UnmarshalScalar(v uint64) error {
b := binary.NativeEndian.AppendUint64(nil, v)
return p.UnmarshalBinary(b)
}
// MarshalUint64 produces the same output as MashalBinary, encoded in a uint64.
// Deprecated: this function simply calls [Point.MarshalScalar].
//
//go:fix inline
func (p Point) MarshalUint64() (uint64, error) {
return p.MarshalScalar()
}
// UnmarshalUint64 expects input formatted by MarshalUint64.
// Deprecated: this function simply calls [Point.UnmarshalScalar].
//
//go:fix inline
func (p *Point) UnmarshalUint64(v uint64) error {
return p.UnmarshalScalar(v)
}
// IsZero reports if p is the zero value.
func (p Point) IsZero() bool {
return p == Point{}
}
// EqualApprox reports if p and q are approximately equal: that is the absolute
// difference of both latitude and longitude are less than tol. If tol is
// negative, then tol defaults to a reasonably small number (10⁻⁵). If tol is
// zero, then p and q must be exactly equal.
func (p Point) EqualApprox(q Point, tol float64) bool {
if tol == 0 {
return p == q
}
if p.IsZero() && q.IsZero() {
return true
} else if p.IsZero() || q.IsZero() {
return false
}
plat, plng, err := p.LatLng()
if err != nil {
panic(err)
}
qlat, qlng, err := q.LatLng()
if err != nil {
panic(err)
}
if tol < 0 {
tol = 1e-5
}
dlat := float64(plat) - float64(qlat)
dlng := float64(plng) - float64(qlng)
return ((dlat < 0 && -dlat < tol) || (dlat >= 0 && dlat < tol)) &&
((dlng < 0 && -dlng < tol) || (dlng >= 0 && dlng < tol))
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package geo
import (
"math"
"sync"
)
// MinSeparation is the minimum separation between two points after quantizing.
// [Point.Quantize] guarantees that two points will either be snapped to exactly
// the same point, which conflates multiple positions together, or that the two
// points will be far enough apart that successfully performing most reverse
// lookups would be highly improbable.
const MinSeparation = 50_000 * Meter
// Latitude
var (
// numSepsEquatorToPole is the number of separations between a point on
// the equator to a point on a pole, that satisfies [minPointSep]. In
// other words, the number of separations between 0° and +90° degrees
// latitude.
numSepsEquatorToPole = int(math.Floor(float64(
earthPolarCircumference / MinSeparation / 4)))
// latSep is the number of degrees between two adjacent latitudinal
// points. In other words, the next point going straight north of
// 0° would be latSep°.
latSep = Degrees(90.0 / float64(numSepsEquatorToPole))
)
// snapToLat returns the number of the nearest latitudinal separation to
// lat. A positive result is north of the equator, a negative result is south,
// and zero is the equator itself. For example, a result of -1 would mean a
// point that is [latSep] south of the equator.
func snapToLat(lat Degrees) int {
return int(math.Round(float64(lat / latSep)))
}
// lngSep is a lookup table for the number of degrees between two adjacent
// longitudinal separations. where the index corresponds to the absolute value
// of the latitude separation. The first value corresponds to the equator and
// the last value corresponds to the separation before the pole. There is no
// value for the pole itself, because longitude has no meaning there.
//
// [lngSep] is calculated on init, which is so quick and will be used so often
// that the startup cost is negligible.
var lngSep = sync.OnceValue(func() []Degrees {
lut := make([]Degrees, numSepsEquatorToPole)
// i ranges from the equator to a pole
for i := range len(lut) {
// lat ranges from [0°, 90°], because the southern hemisphere is
// a reflection of the northern one.
lat := Degrees(i) * latSep
ratio := math.Cos(float64(lat.Radians()))
circ := Distance(ratio) * earthEquatorialCircumference
num := int(math.Floor(float64(circ / MinSeparation)))
// We define lut[0] as 0°, lut[len(lut)] to be the north pole,
// which means -lut[len(lut)] is the south pole.
lut[i] = Degrees(360.0 / float64(num))
}
return lut
})
// snapToLatLng returns the number of the nearest latitudinal separation to lat,
// and the nearest longitudinal separation to lng.
func snapToLatLng(lat, lng Degrees) (Degrees, Degrees) {
latN := snapToLat(lat)
// absolute index into lngSep
n := latN
if n < 0 {
n = -latN
}
lngSep := lngSep()
if n < len(lngSep) {
sep := lngSep[n]
lngN := int(math.Round(float64(lng / sep)))
return Degrees(latN) * latSep, Degrees(lngN) * sep
}
if latN < 0 { // south pole
return -90 * Degree, 0 * Degree
} else { // north pole
return +90 * Degree, 0 * Degree
}
}
// Quantize returns a new [Point] after throwing away enough location data in p
// so that it would be difficult to distinguish a node among all the other nodes
// in its general vicinity. One caveat is that if there’s only one point in an
// obscure location, someone could triangulate the node using additional data.
//
// This method is stable: given the same p, it will always return the same
// result. It is equivalent to snapping to points on Earth that are at least
// [MinSeparation] apart.
func (p Point) Quantize() Point {
if p.IsZero() {
return p
}
lat, lng := snapToLatLng(p.lat, p.lng180-180)
return MakePoint(lat, lng)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package geo
import (
"math"
"strconv"
"strings"
"unicode"
)
const (
Degree Degrees = 1
Radian Radians = 1
Turn Turns = 1
Meter Distance = 1
)
// Degrees represents a latitude or longitude, in decimal degrees.
type Degrees float64
// ParseDegrees parses s as decimal degrees.
func ParseDegrees(s string) (Degrees, error) {
s = strings.TrimSuffix(s, "°")
f, err := strconv.ParseFloat(s, 64)
return Degrees(f), err
}
// MustParseDegrees parses s as decimal degrees, but panics on error.
func MustParseDegrees(s string) Degrees {
d, err := ParseDegrees(s)
if err != nil {
panic(err)
}
return d
}
// String implements the [Stringer] interface. The output is formatted in
// decimal degrees, prefixed by either the appropriate + or - sign, and suffixed
// by a ° degree symbol.
func (d Degrees) String() string {
b, _ := d.AppendText(nil)
b = append(b, []byte("°")...)
return string(b)
}
// AppendText implements [encoding.TextAppender]. The output is formatted in
// decimal degrees, prefixed by either the appropriate + or - sign.
func (d Degrees) AppendText(b []byte) ([]byte, error) {
b = d.AppendZeroPaddedText(b, 0)
return b, nil
}
// AppendZeroPaddedText appends d formatted as decimal degrees to b. The number of
// integer digits will be zero-padded to nint.
func (d Degrees) AppendZeroPaddedText(b []byte, nint int) []byte {
n := float64(d)
if math.IsInf(n, 0) || math.IsNaN(n) {
return strconv.AppendFloat(b, n, 'f', -1, 64)
}
sign := byte('+')
if math.Signbit(n) {
sign = '-'
n = -n
}
b = append(b, sign)
pad := nint - 1
for nn := n / 10; nn >= 1 && pad > 0; nn /= 10 {
pad--
}
for range pad {
b = append(b, '0')
}
return strconv.AppendFloat(b, n, 'f', -1, 64)
}
// Radians converts d into radians.
func (d Degrees) Radians() Radians {
return Radians(d * math.Pi / 180.0)
}
// Turns converts d into a number of turns.
func (d Degrees) Turns() Turns {
return Turns(d / 360.0)
}
// Radians represents a latitude or longitude, in radians.
type Radians float64
// ParseRadians parses s as radians.
func ParseRadians(s string) (Radians, error) {
s = strings.TrimSuffix(s, "rad")
s = strings.TrimRightFunc(s, unicode.IsSpace)
f, err := strconv.ParseFloat(s, 64)
return Radians(f), err
}
// MustParseRadians parses s as radians, but panics on error.
func MustParseRadians(s string) Radians {
r, err := ParseRadians(s)
if err != nil {
panic(err)
}
return r
}
// String implements the [Stringer] interface.
func (r Radians) String() string {
return strconv.FormatFloat(float64(r), 'f', -1, 64) + " rad"
}
// Degrees converts r into decimal degrees.
func (r Radians) Degrees() Degrees {
return Degrees(r * 180.0 / math.Pi)
}
// Turns converts r into a number of turns.
func (r Radians) Turns() Turns {
return Turns(r / 2 / math.Pi)
}
// Turns represents a number of complete revolutions around a sphere.
type Turns float64
// String implements the [Stringer] interface.
func (o Turns) String() string {
return strconv.FormatFloat(float64(o), 'f', -1, 64)
}
// Degrees converts t into decimal degrees.
func (o Turns) Degrees() Degrees {
return Degrees(o * 360.0)
}
// Radians converts t into radians.
func (o Turns) Radians() Radians {
return Radians(o * 2 * math.Pi)
}
// Distance represents a great-circle distance in meters.
type Distance float64
// ParseDistance parses s as distance in meters.
func ParseDistance(s string) (Distance, error) {
s = strings.TrimSuffix(s, "m")
s = strings.TrimRightFunc(s, unicode.IsSpace)
f, err := strconv.ParseFloat(s, 64)
return Distance(f), err
}
// MustParseDistance parses s as distance in meters, but panics on error.
func MustParseDistance(s string) Distance {
d, err := ParseDistance(s)
if err != nil {
panic(err)
}
return d
}
// String implements the [Stringer] interface.
func (d Distance) String() string {
return strconv.FormatFloat(float64(d), 'f', -1, 64) + "m"
}
// DistanceOnEarth converts t turns into the great-circle distance, in meters.
func DistanceOnEarth(t Turns) Distance {
return Distance(t) * EarthMeanCircumference
}
// Earth Fact Sheet
// https://nssdc.gsfc.nasa.gov/planetary/factsheet/earthfact.html
const (
// EarthMeanRadius is the volumetric mean radius of the Earth.
EarthMeanRadius = 6_371_000 * Meter
// EarthMeanCircumference is the volumetric mean circumference of the Earth.
EarthMeanCircumference = 2 * math.Pi * EarthMeanRadius
// earthEquatorialRadius is the equatorial radius of the Earth.
earthEquatorialRadius = 6_378_137 * Meter
// earthEquatorialCircumference is the equatorial circumference of the Earth.
earthEquatorialCircumference = 2 * math.Pi * earthEquatorialRadius
// earthPolarRadius is the polar radius of the Earth.
earthPolarRadius = 6_356_752 * Meter
// earthPolarCircumference is the polar circumference of the Earth.
earthPolarCircumference = 2 * math.Pi * earthPolarRadius
)
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package ipproto contains IP Protocol constants.
package ipproto
import (
"fmt"
"strconv"
"tailscale.com/util/nocasemaps"
"tailscale.com/util/vizerror"
)
// Version describes the IP address version.
type Version uint8
// Valid Version values.
const (
Version4 = 4
Version6 = 6
)
func (p Version) String() string {
switch p {
case Version4:
return "IPv4"
case Version6:
return "IPv6"
default:
return fmt.Sprintf("Version-%d", int(p))
}
}
// Proto is an IP subprotocol as defined by the IANA protocol
// numbers list
// (https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml),
// or the special values Unknown or Fragment.
type Proto uint8
const (
// Unknown represents an unknown or unsupported protocol; it's
// deliberately the zero value. Strictly speaking the zero
// value is IPv6 hop-by-hop extensions, but we don't support
// those, so this is still technically correct.
Unknown Proto = 0x00
// Values from the IANA registry.
ICMPv4 Proto = 0x01
IGMP Proto = 0x02
ICMPv6 Proto = 0x3a
TCP Proto = 0x06
UDP Proto = 0x11
DCCP Proto = 0x21
GRE Proto = 0x2f
SCTP Proto = 0x84
// TSMP is the Tailscale Message Protocol (our ICMP-ish
// thing), an IP protocol used only between Tailscale nodes
// (still encrypted by WireGuard) that communicates why things
// failed, etc.
//
// Proto number 99 is reserved for "any private encryption
// scheme". We never accept these from the host OS stack nor
// send them to the host network stack. It's only used between
// nodes.
TSMP Proto = 99
// Fragment represents any non-first IP fragment, for which we
// don't have the sub-protocol header (and therefore can't
// figure out what the sub-protocol is).
//
// 0xFF is reserved in the IANA registry, so we steal it for
// internal use.
Fragment Proto = 0xFF
)
// Deprecated: use MarshalText instead.
func (p Proto) String() string {
switch p {
case Unknown:
return "Unknown"
case Fragment:
return "Frag"
case ICMPv4:
return "ICMPv4"
case IGMP:
return "IGMP"
case ICMPv6:
return "ICMPv6"
case UDP:
return "UDP"
case TCP:
return "TCP"
case SCTP:
return "SCTP"
case TSMP:
return "TSMP"
case GRE:
return "GRE"
case DCCP:
return "DCCP"
default:
return fmt.Sprintf("IPProto-%d", int(p))
}
}
// Prefer names from
// https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml
// unless otherwise noted.
var (
// preferredNames is the set of protocol names that re produced by
// MarshalText, and are the preferred representation.
preferredNames = map[Proto]string{
51: "ah",
DCCP: "dccp",
8: "egp",
50: "esp",
47: "gre",
ICMPv4: "icmp",
IGMP: "igmp",
9: "igp",
4: "ipv4",
ICMPv6: "ipv6-icmp",
SCTP: "sctp",
TCP: "tcp",
UDP: "udp",
}
// acceptedNames is the set of protocol names that are accepted by
// UnmarshalText.
acceptedNames = map[string]Proto{
"ah": 51,
"dccp": DCCP,
"egp": 8,
"esp": 50,
"gre": 47,
"icmp": ICMPv4,
"icmpv4": ICMPv4,
"icmpv6": ICMPv6,
"igmp": IGMP,
"igp": 9,
"ip-in-ip": 4, // IANA says "ipv4"; Wikipedia/popular use says "ip-in-ip"
"ipv4": 4,
"ipv6-icmp": ICMPv6,
"sctp": SCTP,
"tcp": TCP,
"tsmp": TSMP,
"udp": UDP,
}
)
// UnmarshalText implements encoding.TextUnmarshaler. If the input is empty, p
// is set to 0. If an error occurs, p is unchanged.
func (p *Proto) UnmarshalText(b []byte) error {
if len(b) == 0 {
*p = 0
return nil
}
if u, err := strconv.ParseUint(string(b), 10, 8); err == nil {
*p = Proto(u)
return nil
}
if newP, ok := nocasemaps.GetOk(acceptedNames, string(b)); ok {
*p = newP
return nil
}
return vizerror.Errorf("proto name %q not known; use protocol number 0-255", b)
}
// MarshalText implements encoding.TextMarshaler.
func (p Proto) MarshalText() ([]byte, error) {
if s, ok := preferredNames[p]; ok {
return []byte(s), nil
}
return []byte(strconv.Itoa(int(p))), nil
}
// MarshalJSON implements json.Marshaler.
func (p Proto) MarshalJSON() ([]byte, error) {
return []byte(strconv.Itoa(int(p))), nil
}
// UnmarshalJSON implements json.Unmarshaler. If the input is empty, p is set to
// 0. If an error occurs, p is unchanged. The input must be a JSON number or an
// accepted string name.
func (p *Proto) UnmarshalJSON(b []byte) error {
if len(b) == 0 {
*p = 0
return nil
}
if b[0] == '"' {
b = b[1 : len(b)-1]
}
return p.UnmarshalText(b)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package key
import (
"errors"
"go4.org/mem"
"tailscale.com/types/structs"
)
const (
// chalPublicHexPrefix is the prefix used to identify a
// hex-encoded challenge public key.
//
// This prefix is used in the control protocol, so cannot be
// changed.
chalPublicHexPrefix = "chalpub:"
)
// ChallengePrivate is a challenge key, used to test whether clients control a
// key they want to prove ownership of.
//
// A ChallengePrivate is ephemeral and not serialized to the disk or network.
type ChallengePrivate struct {
_ structs.Incomparable // because == isn't constant-time
k [32]byte
}
// NewChallenge creates and returns a new node private key.
func NewChallenge() ChallengePrivate {
return ChallengePrivate(NewNode())
}
// Public returns the ChallengePublic for k.
// Panics if ChallengePublic is zero.
func (k ChallengePrivate) Public() ChallengePublic {
pub := NodePrivate(k).Public()
return ChallengePublic(pub)
}
// MarshalText implements encoding.TextMarshaler, but by returning an error.
// It shouldn't need to be marshalled anywhere.
func (k ChallengePrivate) MarshalText() ([]byte, error) {
return nil, errors.New("refusing to marshal")
}
// SealToChallenge is like SealTo, but for a ChallengePublic.
func (k NodePrivate) SealToChallenge(p ChallengePublic, cleartext []byte) (ciphertext []byte) {
return k.SealTo(NodePublic(p), cleartext)
}
// OpenFrom opens the NaCl box ciphertext, which must be a value
// created by NodePrivate.SealToChallenge, and returns the inner cleartext if
// ciphertext is a valid box from p to k.
func (k ChallengePrivate) OpenFrom(p NodePublic, ciphertext []byte) (cleartext []byte, ok bool) {
return NodePrivate(k).OpenFrom(p, ciphertext)
}
// ChallengePublic is the public portion of a ChallengePrivate.
type ChallengePublic struct {
k [32]byte
}
// String returns the output of MarshalText as a string.
func (k ChallengePublic) String() string {
bs, err := k.MarshalText()
if err != nil {
panic(err)
}
return string(bs)
}
// AppendText implements encoding.TextAppender.
func (k ChallengePublic) AppendText(b []byte) ([]byte, error) {
return appendHexKey(b, chalPublicHexPrefix, k.k[:]), nil
}
// MarshalText implements encoding.TextMarshaler.
func (k ChallengePublic) MarshalText() ([]byte, error) {
return k.AppendText(nil)
}
// UnmarshalText implements encoding.TextUnmarshaler.
func (k *ChallengePublic) UnmarshalText(b []byte) error {
return parseHex(k.k[:], mem.B(b), mem.S(chalPublicHexPrefix))
}
// IsZero reports whether k is the zero value.
func (k ChallengePublic) IsZero() bool { return k == ChallengePublic{} }
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package key
import "encoding/json"
// ControlPrivate is a Tailscale control plane private key.
//
// It is functionally equivalent to a MachinePrivate, but serializes
// to JSON as a byte array rather than a typed string, because our
// control plane database stores the key that way.
//
// Deprecated: this type should only be used in Tailscale's control
// plane, where existing database serializations require this
// less-good serialization format to persist. Other control plane
// implementations can use MachinePrivate with no downsides.
type ControlPrivate struct {
mkey MachinePrivate // unexported so we can limit the API surface to only exactly what we need
}
// NewControl generates and returns a new control plane private key.
func NewControl() ControlPrivate {
return ControlPrivate{NewMachine()}
}
// IsZero reports whether k is the zero value.
func (k ControlPrivate) IsZero() bool {
return k.mkey.IsZero()
}
// Public returns the MachinePublic for k.
// Panics if ControlPrivate is zero.
func (k ControlPrivate) Public() MachinePublic {
return k.mkey.Public()
}
// MarshalJSON implements json.Marshaler.
func (k ControlPrivate) MarshalJSON() ([]byte, error) {
return json.Marshal(k.mkey.k)
}
// UnmarshalJSON implements json.Unmarshaler.
func (k *ControlPrivate) UnmarshalJSON(bs []byte) error {
return json.Unmarshal(bs, &k.mkey.k)
}
// SealTo wraps cleartext into a NaCl box (see
// golang.org/x/crypto/nacl) to p, authenticated from k, using a
// random nonce.
//
// The returned ciphertext is a 24-byte nonce concatenated with the
// box value.
func (k ControlPrivate) SealTo(p MachinePublic, cleartext []byte) (ciphertext []byte) {
return k.mkey.SealTo(p, cleartext)
}
// SharedKey returns the precomputed Nacl box shared key between k and p.
func (k ControlPrivate) SharedKey(p MachinePublic) MachinePrecomputedSharedKey {
return k.mkey.SharedKey(p)
}
// OpenFrom opens the NaCl box ciphertext, which must be a value
// created by SealTo, and returns the inner cleartext if ciphertext is
// a valid box from p to k.
func (k ControlPrivate) OpenFrom(p MachinePublic, ciphertext []byte) (cleartext []byte, ok bool) {
return k.mkey.OpenFrom(p, ciphertext)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package key
import (
"crypto/subtle"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strings"
"go4.org/mem"
"tailscale.com/types/structs"
)
var ErrInvalidMeshKey = errors.New("invalid mesh key")
// DERPMesh is a mesh key, used for inter-DERP-node communication and for
// privileged DERP clients.
type DERPMesh struct {
_ structs.Incomparable // == isn't constant-time
k [32]byte // 64-digit hexadecimal numbers fit in 32 bytes
}
// MarshalJSON implements the [encoding/json.Marshaler] interface.
func (k DERPMesh) MarshalJSON() ([]byte, error) {
return json.Marshal(k.String())
}
// UnmarshalJSON implements the [encoding/json.Unmarshaler] interface.
func (k *DERPMesh) UnmarshalJSON(data []byte) error {
var s string
json.Unmarshal(data, &s)
if hex.DecodedLen(len(s)) != len(k.k) {
return fmt.Errorf("types/key/derp: cannot unmarshal, incorrect size mesh key len: %d, must be %d, %w", hex.DecodedLen(len(s)), len(k.k), ErrInvalidMeshKey)
}
_, err := hex.Decode(k.k[:], []byte(s))
if err != nil {
return fmt.Errorf("types/key/derp: cannot unmarshal, invalid mesh key: %w", err)
}
return nil
}
// DERPMeshFromRaw32 parses a 32-byte raw value as a DERP mesh key.
func DERPMeshFromRaw32(raw mem.RO) DERPMesh {
if raw.Len() != 32 {
panic("input has wrong size")
}
var ret DERPMesh
raw.Copy(ret.k[:])
return ret
}
// ParseDERPMesh parses a DERP mesh key from a string.
// This function trims whitespace around the string.
// If the key is not a 64-digit hexadecimal number, ErrInvalidMeshKey is returned.
func ParseDERPMesh(key string) (DERPMesh, error) {
key = strings.TrimSpace(key)
if len(key) != 64 {
return DERPMesh{}, fmt.Errorf("%w: must be 64-digit hexadecimal number", ErrInvalidMeshKey)
}
decoded, err := hex.DecodeString(key)
if err != nil {
return DERPMesh{}, fmt.Errorf("%w: %v", ErrInvalidMeshKey, err)
}
return DERPMeshFromRaw32(mem.B(decoded)), nil
}
// IsZero reports whether k is the zero value.
func (k DERPMesh) IsZero() bool {
return k.Equal(DERPMesh{})
}
// Equal reports whether k and other are the same key.
func (k DERPMesh) Equal(other DERPMesh) bool {
// Compare mesh keys in constant time to prevent timing attacks.
// Since mesh keys are a fixed length, we don’t need to be concerned
// about timing attacks on client mesh keys that are the wrong length.
// See https://github.com/tailscale/corp/issues/28720
return subtle.ConstantTimeCompare(k.k[:], other.k[:]) == 1
}
// String returns k as a hex-encoded 64-digit number.
func (k DERPMesh) String() string {
return hex.EncodeToString(k.k[:])
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package key
import (
"bytes"
"crypto/subtle"
"fmt"
"go4.org/mem"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/nacl/box"
"tailscale.com/types/structs"
)
const (
// discoPublicHexPrefix is the prefix used to identify a
// hex-encoded disco public key.
//
// This prefix is used in the control protocol, so cannot be
// changed.
discoPublicHexPrefix = "discokey:"
// DiscoPublicRawLen is the length in bytes of a DiscoPublic, when
// serialized with AppendTo, Raw32 or WriteRawWithoutAllocating.
DiscoPublicRawLen = 32
)
// DiscoPrivate is a disco key, used for peer-to-peer path discovery.
type DiscoPrivate struct {
_ structs.Incomparable // because == isn't constant-time
k [32]byte
}
// NewDisco creates and returns a new disco private key.
func NewDisco() DiscoPrivate {
var ret DiscoPrivate
rand(ret.k[:])
// Key used for nacl seal/open, so needs to be clamped.
clamp25519Private(ret.k[:])
return ret
}
// DiscoPrivateFromRaw32 parses a 32-byte raw value as a DiscoPrivate.
func DiscoPrivateFromRaw32(raw mem.RO) DiscoPrivate {
if raw.Len() != 32 {
panic("input has wrong size")
}
var ret DiscoPrivate
raw.Copy(ret.k[:])
return ret
}
// IsZero reports whether k is the zero value.
func (k DiscoPrivate) IsZero() bool {
return k.Equal(DiscoPrivate{})
}
// Equal reports whether k and other are the same key.
func (k DiscoPrivate) Equal(other DiscoPrivate) bool {
return subtle.ConstantTimeCompare(k.k[:], other.k[:]) == 1
}
// Public returns the DiscoPublic for k.
// Panics if DiscoPrivate is zero.
func (k DiscoPrivate) Public() DiscoPublic {
if k.IsZero() {
panic("can't take the public key of a zero DiscoPrivate")
}
var ret DiscoPublic
curve25519.ScalarBaseMult(&ret.k, &k.k)
return ret
}
// Shared returns the DiscoShared for communication between k and p.
func (k DiscoPrivate) Shared(p DiscoPublic) DiscoShared {
if k.IsZero() || p.IsZero() {
panic("can't compute shared secret with zero keys")
}
var ret DiscoShared
box.Precompute(&ret.k, &p.k, &k.k)
return ret
}
// SortedPairOfDiscoPublic is a lexicographically sorted container of two
// [DiscoPublic] keys.
type SortedPairOfDiscoPublic struct {
k [2]DiscoPublic
}
// Get returns the underlying keys.
func (s SortedPairOfDiscoPublic) Get() [2]DiscoPublic {
return s.k
}
// NewSortedPairOfDiscoPublic returns a SortedPairOfDiscoPublic from a and b.
func NewSortedPairOfDiscoPublic(a, b DiscoPublic) SortedPairOfDiscoPublic {
s := SortedPairOfDiscoPublic{}
if a.Compare(b) < 0 {
s.k[0] = a
s.k[1] = b
} else {
s.k[0] = b
s.k[1] = a
}
return s
}
func (s SortedPairOfDiscoPublic) String() string {
return fmt.Sprintf("%s <=> %s", s.k[0].ShortString(), s.k[1].ShortString())
}
// Equal returns true if s and b are equal, otherwise it returns false.
func (s SortedPairOfDiscoPublic) Equal(b SortedPairOfDiscoPublic) bool {
for i := range s.k {
if s.k[i].Compare(b.k[i]) != 0 {
return false
}
}
return true
}
// DiscoPublic is the public portion of a DiscoPrivate.
type DiscoPublic struct {
k [32]byte
}
// DiscoPublicFromRaw32 parses a 32-byte raw value as a DiscoPublic.
//
// This should be used only when deserializing a DiscoPublic from a
// binary protocol.
func DiscoPublicFromRaw32(raw mem.RO) DiscoPublic {
if raw.Len() != 32 {
panic("input has wrong size")
}
var ret DiscoPublic
raw.Copy(ret.k[:])
return ret
}
// IsZero reports whether k is the zero value.
func (k DiscoPublic) IsZero() bool {
return k == DiscoPublic{}
}
// Raw32 returns k encoded as 32 raw bytes.
//
// Deprecated: only needed for a temporary compat shim in tailcfg, do
// not add more uses.
func (k DiscoPublic) Raw32() [32]byte {
return k.k
}
// ShortString returns the Tailscale conventional debug representation
// of a disco key.
func (k DiscoPublic) ShortString() string {
if k.IsZero() {
return ""
}
return fmt.Sprintf("d:%x", k.k[:8])
}
// AppendTo appends k, serialized as a 32-byte binary value, to
// buf. Returns the new slice.
func (k DiscoPublic) AppendTo(buf []byte) []byte {
return append(buf, k.k[:]...)
}
// String returns the output of MarshalText as a string.
func (k DiscoPublic) String() string {
bs, err := k.MarshalText()
if err != nil {
panic(err)
}
return string(bs)
}
// Compare returns an integer comparing DiscoPublic k and l lexicographically.
// The result will be 0 if k == other, -1 if k < other, and +1 if k > other.
// This is useful for situations requiring only one node in a pair to perform
// some operation, e.g. probing UDP path lifetime.
func (k DiscoPublic) Compare(other DiscoPublic) int {
return bytes.Compare(k.k[:], other.k[:])
}
// AppendText implements encoding.TextAppender.
func (k DiscoPublic) AppendText(b []byte) ([]byte, error) {
return appendHexKey(b, discoPublicHexPrefix, k.k[:]), nil
}
// MarshalText implements encoding.TextMarshaler.
func (k DiscoPublic) MarshalText() ([]byte, error) {
return k.AppendText(nil)
}
// MarshalText implements encoding.TextUnmarshaler.
func (k *DiscoPublic) UnmarshalText(b []byte) error {
return parseHex(k.k[:], mem.B(b), mem.S(discoPublicHexPrefix))
}
type DiscoShared struct {
_ structs.Incomparable // because == isn't constant-time
k [32]byte
}
// Equal reports whether k and other are the same key.
func (k DiscoShared) Equal(other DiscoShared) bool {
return subtle.ConstantTimeCompare(k.k[:], other.k[:]) == 1
}
func (k DiscoShared) IsZero() bool {
return k.Equal(DiscoShared{})
}
// Seal wraps cleartext into a NaCl box (see
// golang.org/x/crypto/nacl), using k as the shared secret and a
// random nonce.
func (k DiscoShared) Seal(cleartext []byte) (ciphertext []byte) {
if k.IsZero() {
panic("can't seal with zero key")
}
var nonce [24]byte
rand(nonce[:])
return box.SealAfterPrecomputation(nonce[:], cleartext, &nonce, &k.k)
}
// Open opens the NaCl box ciphertext, which must be a value created
// by Seal, and returns the inner cleartext if ciphertext is a valid
// box using shared secret k.
func (k DiscoShared) Open(ciphertext []byte) (cleartext []byte, ok bool) {
if k.IsZero() {
panic("can't open with zero key")
}
if len(ciphertext) < 24 {
return nil, false
}
nonce := (*[24]byte)(ciphertext)
return box.OpenAfterPrecomputation(nil, ciphertext[24:], nonce, &k.k)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package key
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/subtle"
"encoding/json"
"fmt"
"io"
"go4.org/mem"
)
var ErrUnsupported = fmt.Errorf("key type not supported on this platform")
const hardwareAttestPublicHexPrefix = "hwattestpub:"
const pubkeyLength = 65 // uncompressed P-256
// HardwareAttestationKey describes a hardware-backed key that is used to
// identify a node. Implementation details will
// vary based on the platform in use (SecureEnclave for Apple, TPM for
// Windows/Linux, Android Hardware-backed Keystore).
// This key can only be marshalled and unmarshaled on the same machine.
type HardwareAttestationKey interface {
crypto.Signer
json.Marshaler
json.Unmarshaler
io.Closer
Clone() HardwareAttestationKey
IsZero() bool
}
// HardwareAttestationPublicFromPlatformKey creates a HardwareAttestationPublic
// for communicating the public component of the hardware attestation key
// with control and other nodes.
func HardwareAttestationPublicFromPlatformKey(k HardwareAttestationKey) HardwareAttestationPublic {
if k == nil {
return HardwareAttestationPublic{}
}
pub := k.Public()
ecdsaPub, ok := pub.(*ecdsa.PublicKey)
if !ok {
panic("hardware attestation key is not ECDSA")
}
bytes, err := ecdsaPub.Bytes()
if err != nil {
panic(err)
}
if len(bytes) != pubkeyLength {
panic("hardware attestation key is not uncompressed ECDSA P-256")
}
var ecdsaPubArr [pubkeyLength]byte
copy(ecdsaPubArr[:], bytes)
return HardwareAttestationPublic{k: ecdsaPubArr}
}
// HardwareAttestationPublic is the public key counterpart to
// HardwareAttestationKey.
type HardwareAttestationPublic struct {
k [pubkeyLength]byte
}
func (k *HardwareAttestationPublic) Clone() *HardwareAttestationPublic {
if k == nil {
return nil
}
var out HardwareAttestationPublic
copy(out.k[:], k.k[:])
return &out
}
func (k HardwareAttestationPublic) Equal(o HardwareAttestationPublic) bool {
return subtle.ConstantTimeCompare(k.k[:], o.k[:]) == 1
}
// IsZero reports whether k is the zero value.
func (k HardwareAttestationPublic) IsZero() bool {
var zero [pubkeyLength]byte
return k.k == zero
}
// String returns the hex-encoded public key with a type prefix.
func (k HardwareAttestationPublic) String() string {
bs, err := k.MarshalText()
if err != nil {
panic(err)
}
return string(bs)
}
// MarshalText implements encoding.TextMarshaler.
func (k HardwareAttestationPublic) MarshalText() ([]byte, error) {
if k.IsZero() {
return nil, nil
}
return k.AppendText(nil)
}
// UnmarshalText implements encoding.TextUnmarshaler. It expects a typed prefix
// followed by a hex encoded representation of k.
func (k *HardwareAttestationPublic) UnmarshalText(b []byte) error {
if len(b) == 0 {
*k = HardwareAttestationPublic{}
return nil
}
kb := make([]byte, pubkeyLength)
if err := parseHex(kb, mem.B(b), mem.S(hardwareAttestPublicHexPrefix)); err != nil {
return err
}
_, err := ecdsa.ParseUncompressedPublicKey(elliptic.P256(), kb)
if err != nil {
return err
}
copy(k.k[:], kb)
return nil
}
func (k HardwareAttestationPublic) AppendText(dst []byte) ([]byte, error) {
return appendHexKey(dst, hardwareAttestPublicHexPrefix, k.k[:]), nil
}
// Verifier returns the ECDSA public key for verifying signatures made by k.
func (k HardwareAttestationPublic) Verifier() *ecdsa.PublicKey {
pk, err := ecdsa.ParseUncompressedPublicKey(elliptic.P256(), k.k[:])
if err != nil {
panic(err)
}
return pk
}
// emptyHardwareAttestationKey is a function that returns an empty
// HardwareAttestationKey suitable for use with JSON unmarshaling.
var emptyHardwareAttestationKey func() HardwareAttestationKey
// createHardwareAttestationKey is a function that creates a new
// HardwareAttestationKey for the current platform.
var createHardwareAttestationKey func() (HardwareAttestationKey, error)
// HardwareAttestationKeyFn is a callback function type that returns a HardwareAttestationKey
// and an error. It is used to register platform-specific implementations of
// HardwareAttestationKey.
type HardwareAttestationKeyFn func() (HardwareAttestationKey, error)
// RegisterHardwareAttestationKeyFns registers a hardware attestation
// key implementation for the current platform.
func RegisterHardwareAttestationKeyFns(emptyFn func() HardwareAttestationKey, createFn HardwareAttestationKeyFn) {
if emptyHardwareAttestationKey != nil {
panic("emptyPlatformHardwareAttestationKey already registered")
}
emptyHardwareAttestationKey = emptyFn
if createHardwareAttestationKey != nil {
panic("createPlatformHardwareAttestationKey already registered")
}
createHardwareAttestationKey = createFn
}
// NewEmptyHardwareAttestationKey returns an empty HardwareAttestationKey
// suitable for JSON unmarshaling.
func NewEmptyHardwareAttestationKey() (HardwareAttestationKey, error) {
if emptyHardwareAttestationKey == nil {
return nil, ErrUnsupported
}
return emptyHardwareAttestationKey(), nil
}
// NewHardwareAttestationKey returns a newly created HardwareAttestationKey for
// the current platform.
func NewHardwareAttestationKey() (HardwareAttestationKey, error) {
if createHardwareAttestationKey == nil {
return nil, ErrUnsupported
}
return createHardwareAttestationKey()
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package key
import (
"bytes"
"crypto/subtle"
"encoding/hex"
"go4.org/mem"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/nacl/box"
"tailscale.com/types/structs"
)
const (
// machinePrivateHexPrefix is the prefix used to identify a
// hex-encoded machine private key.
//
// This prefix name is a little unfortunate, in that it comes from
// WireGuard's own key types. Unfortunately we're stuck with it for
// machine keys, because we serialize them to disk with this prefix.
machinePrivateHexPrefix = "privkey:"
// machinePublicHexPrefix is the prefix used to identify a
// hex-encoded machine public key.
//
// This prefix is used in the control protocol, so cannot be
// changed.
machinePublicHexPrefix = "mkey:"
)
// MachinePrivate is a machine key, used for communication with the
// Tailscale coordination server.
type MachinePrivate struct {
_ structs.Incomparable // == isn't constant-time
k [32]byte
}
// NewMachine creates and returns a new machine private key.
func NewMachine() MachinePrivate {
var ret MachinePrivate
rand(ret.k[:])
clamp25519Private(ret.k[:])
return ret
}
// IsZero reports whether k is the zero value.
func (k MachinePrivate) IsZero() bool {
return k.Equal(MachinePrivate{})
}
// Equal reports whether k and other are the same key.
func (k MachinePrivate) Equal(other MachinePrivate) bool {
return subtle.ConstantTimeCompare(k.k[:], other.k[:]) == 1
}
// Public returns the MachinePublic for k.
// Panics if MachinePrivate is zero.
func (k MachinePrivate) Public() MachinePublic {
if k.IsZero() {
panic("can't take the public key of a zero MachinePrivate")
}
var ret MachinePublic
curve25519.ScalarBaseMult(&ret.k, &k.k)
return ret
}
// AppendText implements encoding.TextAppender.
func (k MachinePrivate) AppendText(b []byte) ([]byte, error) {
return appendHexKey(b, machinePrivateHexPrefix, k.k[:]), nil
}
// MarshalText implements encoding.TextMarshaler.
func (k MachinePrivate) MarshalText() ([]byte, error) {
return k.AppendText(nil)
}
// MarshalText implements encoding.TextUnmarshaler.
func (k *MachinePrivate) UnmarshalText(b []byte) error {
return parseHex(k.k[:], mem.B(b), mem.S(machinePrivateHexPrefix))
}
// UntypedBytes returns k, encoded as an untyped 64-character hex
// string.
//
// Deprecated: this function is risky to use, because it produces
// serialized values that do not identify themselves as a
// MachinePrivate, allowing other code to potentially parse it back in
// as the wrong key type. For new uses that don't require this
// specific raw byte serialization, please use
// MarshalText/UnmarshalText.
func (k MachinePrivate) UntypedBytes() []byte {
return bytes.Clone(k.k[:])
}
// SealTo wraps cleartext into a NaCl box (see
// golang.org/x/crypto/nacl) to p, authenticated from k, using a
// random nonce.
//
// The returned ciphertext is a 24-byte nonce concatenated with the
// box value.
func (k MachinePrivate) SealTo(p MachinePublic, cleartext []byte) (ciphertext []byte) {
if k.IsZero() || p.IsZero() {
panic("can't seal with zero keys")
}
var nonce [24]byte
rand(nonce[:])
return box.Seal(nonce[:], cleartext, &nonce, &p.k, &k.k)
}
// SharedKey returns the precomputed Nacl box shared key between k and p.
func (k MachinePrivate) SharedKey(p MachinePublic) MachinePrecomputedSharedKey {
var shared MachinePrecomputedSharedKey
box.Precompute(&shared.k, &p.k, &k.k)
return shared
}
// MachinePrecomputedSharedKey is a precomputed shared NaCl box shared key.
type MachinePrecomputedSharedKey struct {
k [32]byte
}
// Seal wraps cleartext into a NaCl box (see
// golang.org/x/crypto/nacl) using the shared key k as generated
// by MachinePrivate.SharedKey.
//
// The returned ciphertext is a 24-byte nonce concatenated with the
// box value.
func (k MachinePrecomputedSharedKey) Seal(cleartext []byte) (ciphertext []byte) {
if k == (MachinePrecomputedSharedKey{}) {
panic("can't seal with zero keys")
}
var nonce [24]byte
rand(nonce[:])
return box.SealAfterPrecomputation(nonce[:], cleartext, &nonce, &k.k)
}
// Open opens the NaCl box ciphertext, which must be a value created by
// MachinePrecomputedSharedKey.Seal or MachinePrivate.SealTo, and returns the
// inner cleartext if ciphertext is a valid box for the shared key k.
func (k MachinePrecomputedSharedKey) Open(ciphertext []byte) (cleartext []byte, ok bool) {
if k == (MachinePrecomputedSharedKey{}) {
panic("can't open with zero keys")
}
if len(ciphertext) < 24 {
return nil, false
}
var nonce [24]byte
copy(nonce[:], ciphertext)
return box.OpenAfterPrecomputation(nil, ciphertext[len(nonce):], &nonce, &k.k)
}
// OpenFrom opens the NaCl box ciphertext, which must be a value
// created by SealTo, and returns the inner cleartext if ciphertext is
// a valid box from p to k.
func (k MachinePrivate) OpenFrom(p MachinePublic, ciphertext []byte) (cleartext []byte, ok bool) {
if k.IsZero() || p.IsZero() {
panic("can't open with zero keys")
}
if len(ciphertext) < 24 {
return nil, false
}
var nonce [24]byte
copy(nonce[:], ciphertext)
return box.Open(nil, ciphertext[len(nonce):], &nonce, &p.k, &k.k)
}
// MachinePublic is the public portion of a MachinePrivate.
type MachinePublic struct {
k [32]byte
}
// MachinePublicFromRaw32 parses a 32-byte raw value as a MachinePublic.
//
// This should be used only when deserializing a MachinePublic from a
// binary protocol.
func MachinePublicFromRaw32(raw mem.RO) MachinePublic {
if raw.Len() != 32 {
panic("input has wrong size")
}
var ret MachinePublic
raw.Copy(ret.k[:])
return ret
}
// ParseMachinePublicUntyped parses an untyped 64-character hex value
// as a MachinePublic.
//
// Deprecated: this function is risky to use, because it cannot verify
// that the hex string was intended to be a MachinePublic. This can
// lead to accidentally decoding one type of key as another. For new
// uses that don't require backwards compatibility with the untyped
// string format, please use MarshalText/UnmarshalText.
func ParseMachinePublicUntyped(raw mem.RO) (MachinePublic, error) {
var ret MachinePublic
if err := parseHex(ret.k[:], raw, mem.B(nil)); err != nil {
return MachinePublic{}, err
}
return ret, nil
}
// IsZero reports whether k is the zero value.
func (k MachinePublic) IsZero() bool {
return k == MachinePublic{}
}
// ShortString returns the Tailscale conventional debug representation
// of a public key: the first five base64 digits of the key, in square
// brackets.
func (k MachinePublic) ShortString() string {
return debug32(k.k)
}
// UntypedHexString returns k, encoded as an untyped 64-character hex
// string.
//
// Warning: this function is risky to use, because it produces
// serialized values that do not identify themselves as a
// MachinePublic, allowing other code to potentially parse it back in
// as the wrong key type. For new uses that don't require backwards
// compatibility with the untyped string format, please use
// MarshalText/UnmarshalText.
func (k MachinePublic) UntypedHexString() string {
return hex.EncodeToString(k.k[:])
}
// UntypedBytes returns k, encoded as an untyped 64-character hex
// string.
//
// Deprecated: this function is risky to use, because it produces
// serialized values that do not identify themselves as a
// MachinePublic, allowing other code to potentially parse it back in
// as the wrong key type. For new uses that don't require this
// specific raw byte serialization, please use
// MarshalText/UnmarshalText.
func (k MachinePublic) UntypedBytes() []byte {
return bytes.Clone(k.k[:])
}
// String returns the output of MarshalText as a string.
func (k MachinePublic) String() string {
bs, err := k.MarshalText()
if err != nil {
panic(err)
}
return string(bs)
}
// AppendText implements encoding.TextAppender.
func (k MachinePublic) AppendText(b []byte) ([]byte, error) {
return appendHexKey(b, machinePublicHexPrefix, k.k[:]), nil
}
// MarshalText implements encoding.TextMarshaler.
func (k MachinePublic) MarshalText() ([]byte, error) {
return k.AppendText(nil)
}
// MarshalText implements encoding.TextUnmarshaler.
func (k *MachinePublic) UnmarshalText(b []byte) error {
return parseHex(k.k[:], mem.B(b), mem.S(machinePublicHexPrefix))
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package key
import (
"crypto/ed25519"
"crypto/subtle"
"go4.org/mem"
"tailscale.com/types/structs"
"tailscale.com/types/tkatype"
)
const (
// nlPrivateHexPrefix is the prefix used to identify a
// hex-encoded tailnet-lock key.
nlPrivateHexPrefix = "nlpriv:"
// nlPublicHexPrefix is the prefix used to identify the public
// side of a hex-encoded tailnet-lock key.
nlPublicHexPrefix = "nlpub:"
// nlPublicHexPrefixCLI is the prefix used for tailnet-lock keys
// when shown on the CLI.
// It's not practical for us to change the prefix everywhere due to
// compatibility with existing clients, but we can support both prefixes
// as well as use the CLI form when presenting to the user.
nlPublicHexPrefixCLI = "tlpub:"
)
// NLPrivate is a node-managed tailnet-lock key, used for signing
// node-key signatures and authority update messages.
type NLPrivate struct {
_ structs.Incomparable // because == isn't constant-time
k [ed25519.PrivateKeySize]byte
}
// IsZero reports whether k is the zero value.
func (k NLPrivate) IsZero() bool {
empty := NLPrivate{}
return subtle.ConstantTimeCompare(k.k[:], empty.k[:]) == 1
}
// NewNLPrivate creates and returns a new tailnet-lock key.
func NewNLPrivate() NLPrivate {
// ed25519.GenerateKey 'clamps' the key, not that it
// matters given we don't do Diffie-Hellman.
_, priv, err := ed25519.GenerateKey(nil) // nil == crypto/rand
if err != nil {
panic(err)
}
var out NLPrivate
copy(out.k[:], priv)
return out
}
// MarshalText implements encoding.TextUnmarshaler.
func (k *NLPrivate) UnmarshalText(b []byte) error {
return parseHex(k.k[:], mem.B(b), mem.S(nlPrivateHexPrefix))
}
// AppendText implements encoding.TextAppender.
func (k NLPrivate) AppendText(b []byte) ([]byte, error) {
return appendHexKey(b, nlPrivateHexPrefix, k.k[:]), nil
}
// MarshalText implements encoding.TextMarshaler.
func (k NLPrivate) MarshalText() ([]byte, error) {
return k.AppendText(nil)
}
// Equal reports whether k and other are the same key.
func (k NLPrivate) Equal(other NLPrivate) bool {
return subtle.ConstantTimeCompare(k.k[:], other.k[:]) == 1
}
// Public returns the public component of this key.
func (k NLPrivate) Public() NLPublic {
var out NLPublic
copy(out.k[:], ed25519.PrivateKey(k.k[:]).Public().(ed25519.PublicKey))
return out
}
// KeyID returns an identifier for this key.
func (k NLPrivate) KeyID() tkatype.KeyID {
// The correct way to compute this is:
// return tka.Key{
// Kind: tka.Key25519,
// Public: pub.k[:],
// }.ID()
//
// However, under the hood the key id for a 25519
// key is just the public key, so we avoid the
// dependency on tka by just doing this ourselves.
pub := k.Public().k
return pub[:]
}
// SignAUM implements tka.Signer.
func (k NLPrivate) SignAUM(sigHash tkatype.AUMSigHash) ([]tkatype.Signature, error) {
return []tkatype.Signature{{
KeyID: k.KeyID(),
Signature: ed25519.Sign(ed25519.PrivateKey(k.k[:]), sigHash[:]),
}}, nil
}
// SignNKS signs the tka.NodeKeySignature identified by sigHash.
func (k NLPrivate) SignNKS(sigHash tkatype.NKSSigHash) ([]byte, error) {
return ed25519.Sign(ed25519.PrivateKey(k.k[:]), sigHash[:]), nil
}
// NLPublic is the public portion of a NLPrivate.
type NLPublic struct {
k [ed25519.PublicKeySize]byte
}
// NLPublicFromEd25519Unsafe converts an ed25519 public key into
// a type of NLPublic.
//
// New uses of this function should be avoided, as it's possible to
// accidentally construct an NLPublic from a non tailnet-lock key.
func NLPublicFromEd25519Unsafe(public ed25519.PublicKey) NLPublic {
var out NLPublic
copy(out.k[:], public)
return out
}
// UnmarshalText implements encoding.TextUnmarshaler. This function
// is able to decode both the CLI form (tlpub:<hex>) & the
// regular form (nlpub:<hex>).
func (k *NLPublic) UnmarshalText(b []byte) error {
if mem.HasPrefix(mem.B(b), mem.S(nlPublicHexPrefix)) {
return parseHex(k.k[:], mem.B(b), mem.S(nlPublicHexPrefix))
}
return parseHex(k.k[:], mem.B(b), mem.S(nlPublicHexPrefixCLI))
}
// AppendText implements encoding.TextAppender.
func (k NLPublic) AppendText(b []byte) ([]byte, error) {
return appendHexKey(b, nlPublicHexPrefix, k.k[:]), nil
}
// MarshalText implements encoding.TextMarshaler, emitting a
// representation of the form nlpub:<hex>.
func (k NLPublic) MarshalText() ([]byte, error) {
return k.AppendText(nil)
}
// CLIString returns a marshalled representation suitable for use
// with tailnet lock commands, of the form tlpub:<hex> instead of
// the nlpub:<hex> form emitted by MarshalText. Both forms can
// be decoded by UnmarshalText.
func (k NLPublic) CLIString() string {
return string(appendHexKey(nil, nlPublicHexPrefixCLI, k.k[:]))
}
// Verifier returns a ed25519.PublicKey that can be used to
// verify signatures.
func (k NLPublic) Verifier() ed25519.PublicKey {
return ed25519.PublicKey(k.k[:])
}
// IsZero reports whether k is the zero value.
func (k NLPublic) IsZero() bool {
return k.Equal(NLPublic{})
}
// Equal reports whether k and other are the same key.
func (k NLPublic) Equal(other NLPublic) bool {
return subtle.ConstantTimeCompare(k.k[:], other.k[:]) == 1
}
// KeyID returns a tkatype.KeyID that can be used with a tka.Authority.
func (k NLPublic) KeyID() tkatype.KeyID {
return k.k[:]
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package key
import (
"bufio"
"bytes"
"crypto/subtle"
"encoding/hex"
"errors"
"fmt"
"go4.org/mem"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/nacl/box"
"tailscale.com/types/structs"
"tailscale.com/util/bufiox"
)
const (
// nodePrivateHexPrefix is the prefix used to identify a
// hex-encoded node private key.
//
// This prefix name is a little unfortunate, in that it comes from
// WireGuard's own key types, and we've used it for both key types
// we persist to disk (machine and node keys). But we're stuck
// with it for now, barring another round of tricky migration.
nodePrivateHexPrefix = "privkey:"
// nodePublicHexPrefix is the prefix used to identify a
// hex-encoded node public key.
//
// This prefix is used in the control protocol, so cannot be
// changed.
nodePublicHexPrefix = "nodekey:"
// nodePublicBinaryPrefix is the prefix used to identify a
// binary-encoded node public key.
nodePublicBinaryPrefix = "np"
// NodePublicRawLen is the length in bytes of a NodePublic, when
// serialized with AppendTo, Raw32 or WriteRawWithoutAllocating.
NodePublicRawLen = 32
)
// NodePrivate is a node key, used for WireGuard tunnels and
// communication with DERP servers.
type NodePrivate struct {
_ structs.Incomparable // because == isn't constant-time
k [32]byte
}
// NewNode creates and returns a new node private key.
func NewNode() NodePrivate {
var ret NodePrivate
rand(ret.k[:])
// WireGuard does its own clamping, so this would be unnecessary -
// but we also use this key for DERP comms, which does require
// clamping.
clamp25519Private(ret.k[:])
return ret
}
// Raw32 returns k as 32 raw bytes.
func (k NodePrivate) Raw32() [32]byte { return k.k }
// NodePrivateAs returns a NodePrivate as a named fixed-size array of bytes.
// It's intended for interoperability with wireguard-go's
// device.NoisePrivateKey type.
func NodePrivateAs[T ~[32]byte](k NodePrivate) T { return k.k }
// NodePrivateFromRaw32 parses a 32-byte raw value as a NodePrivate.
//
// Deprecated: only needed to cast from legacy node private key types,
// do not add more uses unrelated to #3206.
func NodePrivateFromRaw32(raw mem.RO) NodePrivate {
if raw.Len() != 32 {
panic("input has wrong size")
}
var ret NodePrivate
raw.Copy(ret.k[:])
return ret
}
func ParseNodePrivateUntyped(raw mem.RO) (NodePrivate, error) {
var ret NodePrivate
if err := parseHex(ret.k[:], raw, mem.B(nil)); err != nil {
return NodePrivate{}, err
}
return ret, nil
}
// IsZero reports whether k is the zero value.
func (k NodePrivate) IsZero() bool {
return k.Equal(NodePrivate{})
}
// Equal reports whether k and other are the same key.
func (k NodePrivate) Equal(other NodePrivate) bool {
return subtle.ConstantTimeCompare(k.k[:], other.k[:]) == 1
}
// Public returns the NodePublic for k.
// Panics if NodePrivate is zero.
func (k NodePrivate) Public() NodePublic {
if k.IsZero() {
panic("can't take the public key of a zero NodePrivate")
}
var ret NodePublic
curve25519.ScalarBaseMult(&ret.k, &k.k)
return ret
}
// AppendText implements encoding.TextAppender.
func (k NodePrivate) AppendText(b []byte) ([]byte, error) {
return appendHexKey(b, nodePrivateHexPrefix, k.k[:]), nil
}
// MarshalText implements encoding.TextMarshaler.
func (k NodePrivate) MarshalText() ([]byte, error) {
return k.AppendText(nil)
}
// MarshalText implements encoding.TextUnmarshaler.
func (k *NodePrivate) UnmarshalText(b []byte) error {
return parseHex(k.k[:], mem.B(b), mem.S(nodePrivateHexPrefix))
}
// SealTo wraps cleartext into a NaCl box (see
// golang.org/x/crypto/nacl) to p, authenticated from k, using a
// random nonce.
//
// The returned ciphertext is a 24-byte nonce concatenated with the
// box value.
func (k NodePrivate) SealTo(p NodePublic, cleartext []byte) (ciphertext []byte) {
if k.IsZero() || p.IsZero() {
panic("can't seal with zero keys")
}
var nonce [24]byte
rand(nonce[:])
return box.Seal(nonce[:], cleartext, &nonce, &p.k, &k.k)
}
// OpenFrom opens the NaCl box ciphertext, which must be a value
// created by SealTo, and returns the inner cleartext if ciphertext is
// a valid box from p to k.
func (k NodePrivate) OpenFrom(p NodePublic, ciphertext []byte) (cleartext []byte, ok bool) {
if k.IsZero() || p.IsZero() {
panic("can't open with zero keys")
}
if len(ciphertext) < 24 {
return nil, false
}
nonce := (*[24]byte)(ciphertext)
return box.Open(nil, ciphertext[len(nonce):], nonce, &p.k, &k.k)
}
// UntypedHexString returns k, encoded as an untyped 64-character hex
// string.
//
// Warning: this function is risky to use, because it produces
// serialized values that do not identify themselves as a
// NodePrivate, allowing other code to potentially parse it back in
// as the wrong key type. For new uses that don't require backwards
// compatibility with the untyped string format, please use
// MarshalText/UnmarshalText.
func (k NodePrivate) UntypedHexString() string {
return hex.EncodeToString(k.k[:])
}
// NodePublic is the public portion of a NodePrivate.
type NodePublic struct {
k [32]byte
}
// Shard returns a uint8 number from a public key with
// mostly-uniform distribution, suitable for sharding.
func (p NodePublic) Shard() uint8 {
// A 25519 public key isn't uniformly random, as it ultimately
// corresponds to a point on the curve.
// But we don't need perfectly uniformly-random, we need
// good-enough-for-sharding random, so we haphazardly
// combine raw values of the key to give us something sufficient.
s := uint8(p.k[31]) + uint8(p.k[30]) + uint8(p.k[20])
return s ^ uint8(p.k[2]+p.k[12])
}
// Compare returns -1, 0, or 1, depending on whether p orders before p2,
// using bytes.Compare on the bytes of the public key.
func (p NodePublic) Compare(p2 NodePublic) int {
return bytes.Compare(p.k[:], p2.k[:])
}
// ParseNodePublicUntyped parses an untyped 64-character hex value
// as a NodePublic.
//
// Deprecated: this function is risky to use, because it cannot verify
// that the hex string was intended to be a NodePublic. This can
// lead to accidentally decoding one type of key as another. For new
// uses that don't require backwards compatibility with the untyped
// string format, please use MarshalText/UnmarshalText.
func ParseNodePublicUntyped(raw mem.RO) (NodePublic, error) {
var ret NodePublic
if err := parseHex(ret.k[:], raw, mem.B(nil)); err != nil {
return NodePublic{}, err
}
return ret, nil
}
// NodePublicFromRaw32 parses a 32-byte raw value as a NodePublic.
//
// This should be used only when deserializing a NodePublic from a
// binary protocol.
func NodePublicFromRaw32(raw mem.RO) NodePublic {
if raw.Len() != 32 {
panic("input has wrong size")
}
var ret NodePublic
raw.Copy(ret.k[:])
return ret
}
// badOldPrefix is a nodekey/discokey prefix that, when base64'd, serializes
// with a "bad01" ("bad ol'", ~"bad old") prefix. It's used for expired node
// keys so when we debug a customer issue, the "bad01" can jump out to us. See:
//
// https://github.com/tailscale/tailscale/issues/6932
var badOldPrefix = []byte{109, 167, 116, 213, 215, 116}
// NodePublicWithBadOldPrefix returns a copy of k with its leading public key
// bytes mutated such that it base64's to a ShortString of [bad01] ("bad ol'"
// [expired node key]).
func NodePublicWithBadOldPrefix(k NodePublic) NodePublic {
var buf [32]byte
k.AppendTo(buf[:0])
copy(buf[:], badOldPrefix)
return NodePublicFromRaw32(mem.B(buf[:]))
}
// IsZero reports whether k is the zero value.
func (k NodePublic) IsZero() bool {
return k == NodePublic{}
}
// ShortString returns the Tailscale conventional debug representation
// of a public key: the first five base64 digits of the key, in square
// brackets.
func (k NodePublic) ShortString() string {
return debug32(k.k)
}
// AppendTo appends k, serialized as a 32-byte binary value, to
// buf. Returns the new slice.
func (k NodePublic) AppendTo(buf []byte) []byte {
return append(buf, k.k[:]...)
}
// ReadRawWithoutAllocating initializes k with bytes read from br.
// It uses [bufiox.ReadFull] to read without heap allocations.
func (k *NodePublic) ReadRawWithoutAllocating(br *bufio.Reader) error {
var z NodePublic
if *k != z {
return errors.New("refusing to read into non-zero NodePublic")
}
_, err := bufiox.ReadFull(br, k.k[:])
return err
}
// WriteRawWithoutAllocating writes out k as 32 big-endian bytes to bw.
//
// It uses AvailableBuffer to append directly into bufio's internal
// buffer without allocation, falling back to WriteByte when the
// buffer has insufficient space.
func (k NodePublic) WriteRawWithoutAllocating(bw *bufio.Writer) error {
// Fast path: enough space in the buffer to append directly.
if bw.Available() >= len(k.k) {
buf := bw.AvailableBuffer()
buf = append(buf, k.k[:]...)
_, err := bw.Write(buf)
return err
}
// Slow path: buffer nearly full. Write byte-at-a-time to let
// bufio flush as needed, avoiding a heap allocation from append
// growing past AvailableBuffer's capacity.
for _, b := range k.k {
if err := bw.WriteByte(b); err != nil {
return err
}
}
return nil
}
// Raw32 returns k encoded as 32 raw bytes.
//
// Deprecated: only needed for a single legacy use in the control
// server and a few places in the wireguard-go API; don't add
// more uses.
func (k NodePublic) Raw32() [32]byte {
return k.k
}
// Less reports whether k orders before other, using an undocumented
// deterministic ordering.
func (k NodePublic) Less(other NodePublic) bool {
return bytes.Compare(k.k[:], other.k[:]) < 0
}
// UntypedHexString returns k, encoded as an untyped 64-character hex
// string.
//
// Warning: this function is risky to use, because it produces
// serialized values that do not identify themselves as a
// NodePublic, allowing other code to potentially parse it back in
// as the wrong key type. For new uses that don't require backwards
// compatibility with the untyped string format, please use
// MarshalText/UnmarshalText.
func (k NodePublic) UntypedHexString() string {
return hex.EncodeToString(k.k[:])
}
// String returns k as a hex-encoded string with a type prefix.
func (k NodePublic) String() string {
bs, err := k.MarshalText()
if err != nil {
panic(err)
}
return string(bs)
}
// AppendText implements encoding.TextAppender. It appends a typed prefix
// followed by hex encoded represtation of k to b.
func (k NodePublic) AppendText(b []byte) ([]byte, error) {
return appendHexKey(b, nodePublicHexPrefix, k.k[:]), nil
}
// MarshalText implements encoding.TextMarshaler. It returns a typed prefix
// followed by a hex encoded representation of k.
func (k NodePublic) MarshalText() ([]byte, error) {
return k.AppendText(nil)
}
// UnmarshalText implements encoding.TextUnmarshaler. It expects a typed prefix
// followed by a hex encoded representation of k.
func (k *NodePublic) UnmarshalText(b []byte) error {
return parseHex(k.k[:], mem.B(b), mem.S(nodePublicHexPrefix))
}
// MarshalBinary implements encoding.BinaryMarshaler.
func (k NodePublic) MarshalBinary() (data []byte, err error) {
b := make([]byte, len(nodePublicBinaryPrefix)+NodePublicRawLen)
copy(b[:len(nodePublicBinaryPrefix)], nodePublicBinaryPrefix)
copy(b[len(nodePublicBinaryPrefix):], k.k[:])
return b, nil
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
func (k *NodePublic) UnmarshalBinary(in []byte) error {
data := mem.B(in)
if !mem.HasPrefix(data, mem.S(nodePublicBinaryPrefix)) {
return fmt.Errorf("missing/incorrect type prefix %s", nodePublicBinaryPrefix)
}
if want, got := len(nodePublicBinaryPrefix)+NodePublicRawLen, data.Len(); want != got {
return fmt.Errorf("incorrect len for NodePublic (%d != %d)", got, want)
}
data.SliceFrom(len(nodePublicBinaryPrefix)).Copy(k.k[:])
return nil
}
// WireGuardGoString prints k in the same format used by wireguard-go.
func (k NodePublic) WireGuardGoString() string {
// This implementation deliberately matches the overly complicated
// implementation in wireguard-go.
b64 := func(input byte) byte {
return input + 'A' + byte(((25-int(input))>>8)&6) - byte(((51-int(input))>>8)&75) - byte(((61-int(input))>>8)&15) + byte(((62-int(input))>>8)&3)
}
b := []byte("peer(____…____)")
const first = len("peer(")
const second = len("peer(____…")
b[first+0] = b64((k.k[0] >> 2) & 63)
b[first+1] = b64(((k.k[0] << 4) | (k.k[1] >> 4)) & 63)
b[first+2] = b64(((k.k[1] << 2) | (k.k[2] >> 6)) & 63)
b[first+3] = b64(k.k[2] & 63)
b[second+0] = b64(k.k[29] & 63)
b[second+1] = b64((k.k[30] >> 2) & 63)
b[second+2] = b64(((k.k[30] << 4) | (k.k[31] >> 4)) & 63)
b[second+3] = b64((k.k[31] << 2) & 63)
return string(b)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package key
import (
crand "crypto/rand"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"io"
"reflect"
"slices"
"go4.org/mem"
"tailscale.com/util/set"
"tailscale.com/util/testenv"
)
// rand fills b with cryptographically strong random bytes. Panics if
// no random bytes are available.
func rand(b []byte) {
if _, err := io.ReadFull(crand.Reader, b[:]); err != nil {
panic(fmt.Sprintf("unable to read random bytes from OS: %v", err))
}
}
// clamp25519 clamps b, which must be a 32-byte Curve25519 private
// key, to a safe value.
//
// The clamping effectively constrains the key to a number between
// 2^251 and 2^252-1, which is then multiplied by 8 (the cofactor of
// Curve25519). This produces a value that doesn't have any unsafe
// properties when doing operations like ScalarMult.
//
// See
// https://web.archive.org/web/20210228105330/https://neilmadden.blog/2020/05/28/whats-the-curve25519-clamping-all-about/
// for a more in-depth explanation of the constraints that led to this
// clamping requirement.
//
// PLEASE NOTE that not all Curve25519 values require clamping. When
// implementing a new key type that uses Curve25519, you must evaluate
// whether that particular key's use requires clamping. Here are some
// existing uses and whether you should clamp private keys at
// creation.
//
// - NaCl box: yes, clamp at creation.
// - WireGuard (userspace uapi or kernel): no, do not clamp.
// - Noise protocols: no, do not clamp.
func clamp25519Private(b []byte) {
b[0] &= 248
b[31] = (b[31] & 127) | 64
}
func appendHexKey(dst []byte, prefix string, key []byte) []byte {
dst = slices.Grow(dst, len(prefix)+hex.EncodedLen(len(key)))
dst = append(dst, prefix...)
dst = hex.AppendEncode(dst, key)
return dst
}
// parseHex decodes a key string of the form "<prefix><hex string>"
// into out. The prefix must match, and the decoded base64 must fit
// exactly into out.
//
// Note the errors in this function deliberately do not echo the
// contents of in, because it might be a private key or part of a
// private key.
func parseHex(out []byte, in, prefix mem.RO) error {
if !mem.HasPrefix(in, prefix) {
return fmt.Errorf("key hex string doesn't have expected type prefix %s", prefix.StringCopy())
}
in = in.SliceFrom(prefix.Len())
if want := len(out) * 2; in.Len() != want {
return fmt.Errorf("key hex has the wrong size, got %d want %d", in.Len(), want)
}
for i := range out {
a, ok1 := fromHexChar(in.At(i*2 + 0))
b, ok2 := fromHexChar(in.At(i*2 + 1))
if !ok1 || !ok2 {
return errors.New("invalid hex character in key")
}
out[i] = (a << 4) | b
}
return nil
}
// fromHexChar converts a hex character into its value and a success flag.
func fromHexChar(c byte) (byte, bool) {
switch {
case '0' <= c && c <= '9':
return c - '0', true
case 'a' <= c && c <= 'f':
return c - 'a' + 10, true
case 'A' <= c && c <= 'F':
return c - 'A' + 10, true
}
return 0, false
}
// debug32 returns the Tailscale conventional debug representation of
// a key: the first five base64 digits of the key, in square brackets.
func debug32(k [32]byte) string {
if k == [32]byte{} {
return ""
}
// The goal here is to generate "[" + base64.StdEncoding.EncodeToString(k[:])[:5] + "]".
// Since we only care about the first 5 characters, it suffices to encode the first 4 bytes of k.
// Encoding those 4 bytes requires 8 bytes.
// Make dst have size 9, to fit the leading '[' plus those 8 bytes.
// We slice the unused ones away at the end.
dst := make([]byte, 9)
dst[0] = '['
base64.StdEncoding.Encode(dst[1:], k[:4])
dst[6] = ']'
return string(dst[:7])
}
// PrivateTypesForTest returns the set of private key types
// in this package, for testing purposes.
func PrivateTypesForTest() set.Set[reflect.Type] {
testenv.AssertInTest()
return set.Of(
reflect.TypeFor[ChallengePrivate](),
reflect.TypeFor[ControlPrivate](),
reflect.TypeFor[DiscoPrivate](),
reflect.TypeFor[MachinePrivate](),
reflect.TypeFor[NodePrivate](),
reflect.TypeFor[NLPrivate](),
reflect.TypeFor[HardwareAttestationKey](),
)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package lazy
import (
"sync"
"sync/atomic"
)
// DeferredInit allows one or more funcs to be deferred
// until [DeferredInit.Do] is called for the first time.
//
// DeferredInit is safe for concurrent use.
type DeferredInit struct {
DeferredFuncs
}
// DeferredFuncs allows one or more funcs to be deferred
// until the owner's [DeferredInit.Do] method is called
// for the first time.
//
// DeferredFuncs is safe for concurrent use. The execution
// order of functions deferred by different goroutines is
// unspecified and must not be relied upon.
// However, functions deferred by the same goroutine are
// executed in the same relative order they were deferred.
// Warning: this is the opposite of the behavior of Go's
// defer statement, which executes deferred functions in
// reverse order.
type DeferredFuncs struct {
m sync.Mutex
funcs []func() error
// err is either:
// * nil, if deferred init has not yet been completed
// * nilErrPtr, if initialization completed successfully
// * non-nil and not nilErrPtr, if there was an error
//
// It is an atomic.Pointer so it can be read without m held.
err atomic.Pointer[error]
}
// Defer adds a function to be called when [DeferredInit.Do]
// is called for the first time. It returns true on success,
// or false if [DeferredInit.Do] has already been called.
func (d *DeferredFuncs) Defer(f func() error) bool {
d.m.Lock()
defer d.m.Unlock()
if d.err.Load() != nil {
return false
}
d.funcs = append(d.funcs, f)
return true
}
// MustDefer is like [DeferredFuncs.Defer], but panics
// if [DeferredInit.Do] has already been called.
func (d *DeferredFuncs) MustDefer(f func() error) {
if !d.Defer(f) {
panic("deferred init already completed")
}
}
// Do calls previously deferred init functions if it is being called
// for the first time on this instance of [DeferredInit].
// It stops and returns an error if any init function returns an error.
//
// It is safe for concurrent use, and the deferred init is guaranteed
// to have been completed, either successfully or with an error,
// when Do() returns.
func (d *DeferredInit) Do() error {
err := d.err.Load()
if err == nil {
err = d.doSlow()
}
return *err
}
func (d *DeferredInit) doSlow() (err *error) {
d.m.Lock()
defer d.m.Unlock()
if err := d.err.Load(); err != nil {
return err
}
defer func() {
d.err.Store(err)
d.funcs = nil // do not keep funcs alive after invoking
}()
for _, f := range d.funcs {
if err := f(); err != nil {
return new(err)
}
}
return nilErrPtr
}
// Funcs is a shorthand for &d.DeferredFuncs.
// The returned value can safely be passed to external code,
// allowing to defer init funcs without also exposing [DeferredInit.Do].
func (d *DeferredInit) Funcs() *DeferredFuncs {
return &d.DeferredFuncs
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package lazy provides types for lazily initialized values.
package lazy
import (
"sync"
"sync/atomic"
)
// nilErrPtr is a sentinel *error value for SyncValue.err to signal
// that SyncValue.v is valid.
var nilErrPtr = new(error(nil))
// SyncValue is a lazily computed value.
//
// Use either Get or GetErr, depending on whether your fill function returns an
// error.
//
// Recursive use of a SyncValue from its own fill function will deadlock.
//
// SyncValue is safe for concurrent use.
//
// Unlike [sync.OnceValue], the linker can do better dead code elimination
// with SyncValue. See https://github.com/golang/go/issues/62202.
type SyncValue[T any] struct {
once sync.Once
v T
// err is either:
// * nil, if not yet computed
// * nilErrPtr, if completed and nil
// * non-nil and not nilErrPtr on error.
//
// It is an atomic.Pointer so it can be read outside of the sync.Once.Do.
//
// Writes to err must happen after a write to v so a caller seeing a non-nil
// err can safely read v.
err atomic.Pointer[error]
}
// Set attempts to set z's value to val, and reports whether it succeeded.
// Set only succeeds if none of Get/GetErr/Set have been called before.
func (z *SyncValue[T]) Set(val T) bool {
var wasSet bool
z.once.Do(func() {
z.v = val
z.err.Store(nilErrPtr) // after write to z.v; see docs
wasSet = true
})
return wasSet
}
// MustSet sets z's value to val, or panics if z already has a value.
func (z *SyncValue[T]) MustSet(val T) {
if !z.Set(val) {
panic("Set after already filled")
}
}
// Get returns z's value, calling fill to compute it if necessary.
// f is called at most once.
func (z *SyncValue[T]) Get(fill func() T) T {
z.once.Do(func() {
z.v = fill()
z.err.Store(nilErrPtr) // after write to z.v; see docs
})
return z.v
}
// GetErr returns z's value, calling fill to compute it if necessary.
// f is called at most once, and z remembers both of fill's outputs.
func (z *SyncValue[T]) GetErr(fill func() (T, error)) (T, error) {
z.once.Do(func() {
var err error
z.v, err = fill()
// Update z.err after z.v; see field docs.
if err != nil {
z.err.Store(new(err))
} else {
z.err.Store(nilErrPtr)
}
})
return z.v, *z.err.Load()
}
// Peek returns z's value and a boolean indicating whether the value has been
// set successfully. If a value has not been set, the zero value of T is
// returned.
//
// This function is safe to call concurrently with Get/GetErr/Set, but it's
// undefined whether a value set by a concurrent call will be visible to Peek.
//
// To get any error that's been set, use PeekErr.
//
// If GetErr's fill function returned a valid T and an non-nil error, Peek
// discards that valid T value. PeekErr returns both.
func (z *SyncValue[T]) Peek() (v T, ok bool) {
if z.err.Load() == nilErrPtr {
return z.v, true
}
var zero T
return zero, false
}
// PeekErr returns z's value and error and a boolean indicating whether the
// value or error has been set. If ok is false, T and err are the zero value.
//
// This function is safe to call concurrently with Get/GetErr/Set, but it's
// undefined whether a value set by a concurrent call will be visible to Peek.
//
// Unlike Peek, PeekErr reports ok if either v or err has been set, not just v,
// and returns both the T and err returned by GetErr's fill function.
func (z *SyncValue[T]) PeekErr() (v T, err error, ok bool) {
if e := z.err.Load(); e != nil {
return z.v, *e, true
}
var zero T
return zero, nil, false
}
// testing_TB is a subset of testing.TB that we use to set up test helpers.
// It's defined here to avoid pulling in the testing package.
type testing_TB interface {
Helper()
Cleanup(func())
}
// SetForTest sets z's value and error.
// It's used in tests only and reverts z's state back when tb and all its
// subtests complete.
// It is not safe for concurrent use and must not be called concurrently with
// any SyncValue methods, including another call to itself.
//
// The provided tb should be a [*testing.T] or [*testing.B].
func (z *SyncValue[T]) SetForTest(tb testing_TB, val T, err error) {
tb.Helper()
oldErr, oldVal := z.err.Load(), z.v
z.once.Do(func() {})
z.v = val
if err != nil {
z.err.Store(new(err))
} else {
z.err.Store(nilErrPtr)
}
tb.Cleanup(func() {
if oldErr == nil {
*z = SyncValue[T]{}
} else {
z.v = oldVal
z.err.Store(oldErr)
}
})
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package lazy
import "tailscale.com/util/mak"
// GMap is a map of lazily computed [GValue] pointers, keyed by a comparable
// type.
//
// Use either Get or GetErr, depending on whether your fill function returns an
// error.
//
// GMap is not safe for concurrent use.
type GMap[K comparable, V any] struct {
store map[K]*GValue[V]
}
// Len returns the number of entries in the map.
func (s *GMap[K, V]) Len() int {
return len(s.store)
}
// Set attempts to set the value of k to v, and reports whether it succeeded.
// Set only succeeds if k has never been called with Get/GetErr/Set before.
func (s *GMap[K, V]) Set(k K, v V) bool {
z, ok := s.store[k]
if !ok {
z = new(GValue[V])
mak.Set(&s.store, k, z)
}
return z.Set(v)
}
// MustSet sets the value of k to v, or panics if k already has a value.
func (s *GMap[K, V]) MustSet(k K, v V) {
if !s.Set(k, v) {
panic("Set after already filled")
}
}
// Get returns the value for k, computing it with fill if it's not already
// present.
func (s *GMap[K, V]) Get(k K, fill func() V) V {
z, ok := s.store[k]
if !ok {
z = new(GValue[V])
mak.Set(&s.store, k, z)
}
return z.Get(fill)
}
// GetErr returns the value for k, computing it with fill if it's not already
// present.
func (s *GMap[K, V]) GetErr(k K, fill func() (V, error)) (V, error) {
z, ok := s.store[k]
if !ok {
z = new(GValue[V])
mak.Set(&s.store, k, z)
}
return z.GetErr(fill)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package lazy
// GValue is a lazily computed value.
//
// Use either Get or GetErr, depending on whether your fill function returns an
// error.
//
// Recursive use of a GValue from its own fill function will panic.
//
// GValue is not safe for concurrent use. (Mnemonic: G is for one Goroutine,
// which isn't strictly true if you provide your own synchronization between
// goroutines, but in practice most of our callers have been using it within
// a single goroutine.)
type GValue[T any] struct {
done bool
calling bool
V T
err error
}
// Set attempts to set z's value to val, and reports whether it succeeded.
// Set only succeeds if none of Get/GetErr/Set have been called before.
func (z *GValue[T]) Set(v T) bool {
if z.done {
return false
}
if z.calling {
panic("Set while Get fill is running")
}
z.V = v
z.done = true
return true
}
// MustSet sets z's value to val, or panics if z already has a value.
func (z *GValue[T]) MustSet(val T) {
if !z.Set(val) {
panic("Set after already filled")
}
}
// Get returns z's value, calling fill to compute it if necessary.
// f is called at most once.
func (z *GValue[T]) Get(fill func() T) T {
if !z.done {
if z.calling {
panic("recursive lazy fill")
}
z.calling = true
z.V = fill()
z.done = true
z.calling = false
}
return z.V
}
// GetErr returns z's value, calling fill to compute it if necessary.
// f is called at most once, and z remembers both of fill's outputs.
func (z *GValue[T]) GetErr(fill func() (T, error)) (T, error) {
if !z.done {
if z.calling {
panic("recursive lazy fill")
}
z.calling = true
z.V, z.err = fill()
z.done = true
z.calling = false
}
return z.V, z.err
}
// GFunc wraps a function to make it lazy.
//
// The returned function calls fill the first time it's called, and returns
// fill's result on every subsequent call.
//
// The returned function is not safe for concurrent use.
func GFunc[T any](fill func() T) func() T {
var v GValue[T]
return func() T {
return v.Get(fill)
}
}
// SyncFuncErr wraps a function to make it lazy.
//
// The returned function calls fill the first time it's called, and returns
// fill's results on every subsequent call.
//
// The returned function is not safe for concurrent use.
func GFuncErr[T any](fill func() (T, error)) func() (T, error) {
var v GValue[T]
return func() (T, error) {
return v.GetErr(fill)
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package logger defines a type for writing to logs. It's just a
// convenience type so that we don't have to pass verbose func(...)
// types around.
package logger
import (
"bufio"
"bytes"
"container/list"
"encoding/json"
"fmt"
"io"
"log"
"runtime"
"strings"
"sync"
"time"
"context"
jsonv2 "github.com/go-json-experiment/json"
jsonv1 "github.com/go-json-experiment/json/v1"
"go4.org/mem"
"tailscale.com/envknob"
"tailscale.com/util/ctxkey"
"tailscale.com/util/testenv"
)
// Logf is the basic Tailscale logger type: a printf-like func.
// Like log.Printf, the format need not end in a newline.
// Logf functions must be safe for concurrent use.
type Logf func(format string, args ...any)
// LogfKey stores and loads [Logf] values within a [context.Context].
var LogfKey = ctxkey.New("", Logf(log.Printf))
// A Context is a context.Context that should contain a custom log function, obtainable from FromContext.
// If no log function is present, FromContext will return log.Printf.
// To construct a Context, use Add
//
// Deprecated: Do not use.
type Context context.Context
// jenc is a json.Encode + bytes.Buffer pair wired up to be reused in a pool.
type jenc struct {
buf bytes.Buffer
enc *json.Encoder
}
var jencPool = &sync.Pool{New: func() any {
je := new(jenc)
je.enc = json.NewEncoder(&je.buf)
return je
}}
// JSON marshals v as JSON and writes it to logf formatted with the annotation to make logtail
// treat it as a structured log.
//
// The recType is the record type and becomes the key of the wrapper
// JSON object that is logged. That is, if recType is "foo" and v is
// 123, the value logged is {"foo":123}.
//
// Do not use recType "logtail", "v", "text", or "metrics", with any case.
// Those are reserved for the logging system.
//
// The level can be from 0 to 9. Levels from 1 to 9 are included in
// the logged JSON object, like {"foo":123,"v":2}.
func (logf Logf) JSON(level int, recType string, v any) {
je := jencPool.Get().(*jenc)
defer jencPool.Put(je)
je.buf.Reset()
je.buf.WriteByte('{')
je.enc.Encode(recType)
je.buf.Truncate(je.buf.Len() - 1) // remove newline from prior Encode
je.buf.WriteByte(':')
if err := je.enc.Encode(v); err != nil {
logf("[unexpected]: failed to encode structured JSON log record of type %q / %T: %v", recType, v, err)
return
}
je.buf.Truncate(je.buf.Len() - 1) // remove newline from prior Encode
je.buf.WriteByte('}')
// Magic prefix recognized by logtail:
logf("[v\x00JSON]%d%s", level%10, je.buf.Bytes())
}
// FromContext extracts a log function from ctx.
//
// Deprecated: Use [LogfKey.Value] instead.
func FromContext(ctx Context) Logf {
return LogfKey.Value(ctx)
}
// Ctx constructs a Context from ctx with fn as its custom log function.
//
// Deprecated: Use [LogfKey.WithValue] instead.
func Ctx(ctx context.Context, fn Logf) Context {
return LogfKey.WithValue(ctx, fn)
}
// WithPrefix wraps f, prefixing each format with the provided prefix.
func WithPrefix(f Logf, prefix string) Logf {
return func(format string, args ...any) {
f(prefix+format, args...)
}
}
// FuncWriter returns an io.Writer that writes to f.
func FuncWriter(f Logf) io.Writer {
return funcWriter{f}
}
// StdLogger returns a standard library logger from a Logf.
func StdLogger(f Logf) *log.Logger {
return log.New(FuncWriter(f), "", 0)
}
type funcWriter struct{ f Logf }
func (w funcWriter) Write(p []byte) (int, error) {
w.f("%s", p)
return len(p), nil
}
// Discard is a Logf that throws away the logs given to it.
func Discard(string, ...any) {}
// limitData is used to keep track of each format string's associated
// rate-limiting data.
type limitData struct {
bucket *tokenBucket // the token bucket associated with this string
nBlocked int // number of messages skipped
ele *list.Element // list element used to access this string in the cache
}
// rateFree are format string substrings that are exempt from rate limiting.
// Things should not be added to this unless they're already limited otherwise
// or are critical for generating important stats from the logs.
var rateFree = []string{
"magicsock: disco: ",
"magicsock: ParseEndpoint:",
// grinder stats lines
"magicsock: new contact: ",
"SetPrefs: %v",
"peer keys: %s",
"v%v peers: %v",
// debug messages printed by 'tailscale bugreport'
"diag: ",
}
// RateLimitedFn is a wrapper for RateLimitedFnWithClock that includes the
// current time automatically. This is mainly for backward compatibility.
func RateLimitedFn(logf Logf, f time.Duration, burst int, maxCache int) Logf {
return RateLimitedFnWithClock(logf, f, burst, maxCache, time.Now)
}
// RateLimitedFnWithClock returns a rate-limiting Logf wrapping the given
// logf. Messages are allowed through at a maximum of one message every f
// (where f is a time.Duration), in bursts of up to burst messages at a
// time. Up to maxCache format strings will be tracked separately.
// timeNow is a function that returns the current time, used for calculating
// rate limits.
func RateLimitedFnWithClock(logf Logf, f time.Duration, burst int, maxCache int, timeNow func() time.Time) Logf {
if envknob.String("TS_DEBUG_LOG_RATE") == "all" {
return logf
}
if runtime.GOOS == "plan9" {
// To ease bring-up.
return logf
}
var (
mu sync.Mutex
msgLim = make(map[string]*limitData) // keyed by logf format
msgCache = list.New() // a rudimentary LRU that limits the size of the map
)
return func(format string, args ...any) {
// Shortcut for formats with no rate limit
for _, sub := range rateFree {
if strings.Contains(format, sub) {
logf(format, args...)
return
}
}
mu.Lock()
rl, ok := msgLim[format]
if ok {
msgCache.MoveToFront(rl.ele)
} else {
rl = &limitData{
bucket: newTokenBucket(f, burst, timeNow()),
ele: msgCache.PushFront(format),
}
msgLim[format] = rl
if msgCache.Len() > maxCache {
delete(msgLim, msgCache.Back().Value.(string))
msgCache.Remove(msgCache.Back())
}
}
rl.bucket.AdvanceTo(timeNow())
// Make sure there's enough room for at least a few
// more logs before we unblock, so we don't alternate
// between blocking and unblocking.
if rl.nBlocked > 0 && rl.bucket.remaining >= 2 {
// Only print this if we dropped more than 1
// message. Otherwise we'd *increase* the total
// number of log lines printed.
if rl.nBlocked > 1 {
logf("[RATELIMIT] format(%q) (%d dropped)",
format, rl.nBlocked-1)
}
rl.nBlocked = 0
}
if rl.nBlocked == 0 && rl.bucket.Get() {
hitLimit := rl.bucket.remaining == 0
if hitLimit {
// Enter "blocked" mode immediately after
// reaching the burst limit. We want to
// always accompany the format() message
// with an example of the format, which is
// effectively the same as printing the
// message anyway. But this way they can
// be on two separate lines and we don't
// corrupt the original message.
rl.nBlocked = 1
}
mu.Unlock() // release before calling logf
logf(format, args...)
if hitLimit {
logf("[RATELIMIT] format(%q)", format)
}
} else {
rl.nBlocked++
mu.Unlock()
}
}
}
// SlowLoggerWithClock is a logger that applies rate limits similar to
// RateLimitedFnWithClock, but instead of dropping logs will sleep until they
// can be written. This should only be used for debug logs, and not in a hot path.
//
// The provided context, if canceled, will cause all logs to be dropped and
// prevent any sleeps.
func SlowLoggerWithClock(ctx context.Context, logf Logf, f time.Duration, burst int, timeNow func() time.Time) Logf {
var (
mu sync.Mutex
tb = newTokenBucket(f, burst, timeNow())
)
return func(format string, args ...any) {
if ctx.Err() != nil {
return
}
// Hold the mutex for the entire length of the check + log
// since our token bucket isn't concurrency-safe.
mu.Lock()
defer mu.Unlock()
tb.AdvanceTo(timeNow())
// If we can get a token, then do that and return.
if tb.Get() {
logf(format, args...)
return
}
// Otherwise, sleep for 2x the duration so that we don't
// immediately sleep again on the next call.
tmr := time.NewTimer(2 * f)
defer tmr.Stop()
select {
case curr := <-tmr.C:
tb.AdvanceTo(curr)
case <-ctx.Done():
return
}
if !tb.Get() {
log.Printf("[unexpected] error rate-limiting in SlowLoggerWithClock")
return
}
logf(format, args...)
}
}
// LogOnChange logs a given line only if line != lastLine, or if maxInterval has passed
// since the last time this identical line was logged.
func LogOnChange(logf Logf, maxInterval time.Duration, timeNow func() time.Time) Logf {
var (
mu sync.Mutex
sLastLogged string
tLastLogged = timeNow()
)
return func(format string, args ...any) {
s := fmt.Sprintf(format, args...)
mu.Lock()
if s == sLastLogged && timeNow().Sub(tLastLogged) < maxInterval {
mu.Unlock()
return
}
sLastLogged = s
tLastLogged = timeNow()
mu.Unlock()
// Re-stringify it (instead of using "%s", s) so something like "%s"
// doesn't end up getting rate-limited. (And can't use 's' as the pattern,
// as it might contain formatting directives.)
logf(format, args...)
}
}
// ArgWriter is a fmt.Formatter that can be passed to any Logf func to
// efficiently write to a %v argument without allocations.
type ArgWriter func(*bufio.Writer)
func (fn ArgWriter) Format(f fmt.State, _ rune) {
bw := argBufioPool.Get().(*bufio.Writer)
bw.Reset(f)
fn(bw)
bw.Flush()
bw.Reset(io.Discard)
argBufioPool.Put(bw)
}
var argBufioPool = &sync.Pool{New: func() any { return bufio.NewWriterSize(io.Discard, 1024) }}
// Filtered returns a Logf that silently swallows some log lines.
// Each inbound format and args is evaluated and printed to a string s.
// The original format and args are passed to logf if and only if allow(s) returns true.
func Filtered(logf Logf, allow func(s string) bool) Logf {
return func(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
if !allow(msg) {
return
}
logf(format, args...)
}
}
// LogfCloser wraps logf to create a logger that can be closed.
// Calling close makes all future calls to newLogf into no-ops.
func LogfCloser(logf Logf) (newLogf Logf, close func()) {
var (
mu sync.Mutex
closed bool
)
close = func() {
mu.Lock()
defer mu.Unlock()
closed = true
}
newLogf = func(msg string, args ...any) {
mu.Lock()
if closed {
mu.Unlock()
return
}
mu.Unlock()
logf(msg, args...)
}
return newLogf, close
}
// AsJSON returns a formatter that formats v as JSON. The value is suitable to
// passing to a regular %v printf argument. (%s is not required)
//
// If json.Marshal returns an error, the output is "%%!JSON-ERROR:" followed by
// the error string.
func AsJSON(v any) fmt.Formatter {
return asJSONResult{v}
}
type asJSONResult struct{ v any }
func (a asJSONResult) Format(s fmt.State, verb rune) {
// Write directly to s rather than going through json.Marshal's
// returned []byte to avoid an allocation. The explicit v1
// options keep the output identical to encoding/json.
err := jsonv2.MarshalWrite(s, a.v, jsonv1.DefaultOptionsV1())
if err != nil {
fmt.Fprintf(s, "%%!JSON-ERROR:%v", err)
}
}
// TestLogger returns a logger that logs to tb.Logf
// with a prefix to make it easier to distinguish spam
// from explicit test failures.
func TestLogger(tb testenv.TB) Logf {
return func(format string, args ...any) {
tb.Helper()
tb.Logf(" ... "+format, args...)
}
}
// HTTPServerLogFilter is an io.Writer that can be used as the
// net/http.Server.ErrorLog logger, and will filter out noisy, low-signal
// messages that clutter up logs.
type HTTPServerLogFilter struct {
Inner Logf
}
func (lf HTTPServerLogFilter) Write(p []byte) (int, error) {
b := mem.B(p)
if mem.HasSuffix(b, mem.S(": EOF\n")) ||
mem.HasSuffix(b, mem.S(": i/o timeout\n")) ||
mem.HasSuffix(b, mem.S(": read: connection reset by peer\n")) ||
mem.HasSuffix(b, mem.S(": remote error: tls: bad certificate\n")) ||
mem.HasSuffix(b, mem.S(": tls: first record does not look like a TLS handshake\n")) {
// Skip this log message, but say that we processed it
return len(p), nil
}
lf.Inner("%s", p)
return len(p), nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package logger
import (
"time"
)
// tokenBucket is a simple token bucket style rate limiter.
// It's similar in function to golang.org/x/time/rate.Limiter, which we
// can't use because:
// - It doesn't give access to the number of accumulated tokens, which we
// need for implementing hysteresis;
// - It doesn't let us provide our own time function, which we need for
// implementing proper unit tests.
//
// rate.Limiter is also much more complex than necessary, but that wouldn't
// be enough to disqualify it on its own.
//
// Unlike rate.Limiter, this token bucket does not attempt to
// do any locking of its own. Don't try to access it reentrantly.
// That's fine inside this types/logger package because we already have
// locking at a higher level.
type tokenBucket struct {
remaining int
max int
tick time.Duration
t time.Time
}
func newTokenBucket(tick time.Duration, max int, now time.Time) *tokenBucket {
return &tokenBucket{max, max, tick, now}
}
func (tb *tokenBucket) Get() bool {
if tb.remaining > 0 {
tb.remaining--
return true
}
return false
}
func (tb *tokenBucket) Refund(n int) {
tb.remaining = min(tb.remaining+n, tb.max)
}
func (tb *tokenBucket) AdvanceTo(t time.Time) {
diff := t.Sub(tb.t)
// only use up whole ticks. The remainder will be used up
// next time.
ticks := int(diff / tb.tick)
tb.t = tb.t.Add(time.Duration(ticks) * tb.tick)
tb.Refund(ticks)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package logid contains ID types for interacting with the log service.
package logid
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"fmt"
"math/bits"
"slices"
"unicode/utf8"
)
// PrivateID represents a log steam for writing.
// Private IDs are only shared with the server when writing logs.
type PrivateID [32]byte
// NewPrivateID generates a new random PrivateID.
// This should persist across runs of an instance of the application,
// so that it can append to the same log stream for each invocation.
func NewPrivateID() (id PrivateID, err error) {
if _, err := rand.Read(id[:]); err != nil {
return PrivateID{}, err
}
// Clamping, for future use.
id[0] &= 248
id[31] = (id[31] & 127) | 64
return id, nil
}
// ParsePrivateID returns a PrivateID from its hex representation.
func ParsePrivateID(in string) (out PrivateID, err error) {
err = parseID("logid.ParsePublicID", (*[32]byte)(&out), in)
return out, err
}
// Add adds i to the id, treating it as an unsigned 256-bit big-endian integer,
// and returns the resulting ID.
func (id PrivateID) Add(i int64) PrivateID {
return add(id, i)
}
func (id PrivateID) AppendText(b []byte) ([]byte, error) {
return hex.AppendEncode(b, id[:]), nil
}
func (id PrivateID) MarshalText() ([]byte, error) {
return id.AppendText(nil)
}
func (id *PrivateID) UnmarshalText(in []byte) error {
return parseID("logid.PrivateID", (*[32]byte)(id), in)
}
func (id PrivateID) String() string {
return string(hex.AppendEncode(nil, id[:]))
}
func (id1 PrivateID) Less(id2 PrivateID) bool {
return id1.Compare(id2) < 0
}
func (id1 PrivateID) Compare(id2 PrivateID) int {
return slices.Compare(id1[:], id2[:])
}
func (id PrivateID) IsZero() bool {
return id == PrivateID{}
}
// Public returns the public ID of the private ID,
// which is the SHA-256 hash of the private ID.
func (id PrivateID) Public() (pub PublicID) {
return PublicID(sha256.Sum256(id[:]))
}
// PublicID represents a log stream for reading.
// The PrivateID cannot be feasibly reversed from the PublicID.
type PublicID [sha256.Size]byte
// ParsePublicID returns a PublicID from its hex representation.
func ParsePublicID(in string) (out PublicID, err error) {
err = parseID("logid.ParsePublicID", (*[32]byte)(&out), in)
return out, err
}
// Add adds i to the id, treating it as an unsigned 256-bit big-endian integer,
// and returns the resulting ID.
func (id PublicID) Add(i int64) PublicID {
return add(id, i)
}
func (id PublicID) AppendText(b []byte) ([]byte, error) {
return hex.AppendEncode(b, id[:]), nil
}
func (id PublicID) MarshalText() ([]byte, error) {
return id.AppendText(nil)
}
func (id *PublicID) UnmarshalText(in []byte) error {
return parseID("logid.ParsePublicID", (*[32]byte)(id), in)
}
func (id PublicID) String() string {
return string(hex.AppendEncode(nil, id[:]))
}
func (id1 PublicID) Less(id2 PublicID) bool {
return id1.Compare(id2) < 0
}
func (id1 PublicID) Compare(id2 PublicID) int {
return slices.Compare(id1[:], id2[:])
}
func (id PublicID) IsZero() bool {
return id == PublicID{}
}
func (id PublicID) Prefix64() uint64 {
return binary.BigEndian.Uint64(id[:8])
}
func parseID[Bytes []byte | string](funcName string, out *[32]byte, in Bytes) (err error) {
if len(in) != 2*len(out) {
return fmt.Errorf("%s: invalid hex length: %d", funcName, len(in))
}
var hexArr [2 * len(out)]byte
copy(hexArr[:], in)
if _, err := hex.Decode(out[:], hexArr[:]); err != nil {
r, _ := utf8.DecodeRune(bytes.TrimLeft([]byte(in), "0123456789abcdefABCDEF"))
return fmt.Errorf("%s: invalid hex character: %c", funcName, r)
}
return nil
}
func add(id [32]byte, i int64) [32]byte {
var out uint64
switch {
case i < 0:
borrow := ^uint64(i) + 1 // twos-complement inversion
for i := 0; i < 4 && borrow > 0; i++ {
out, borrow = bits.Sub64(binary.BigEndian.Uint64(id[8*(3-i):]), borrow, 0)
binary.BigEndian.PutUint64(id[8*(3-i):], out)
}
case i > 0:
carry := uint64(i)
for i := 0; i < 4 && carry > 0; i++ {
out, carry = bits.Add64(binary.BigEndian.Uint64(id[8*(3-i):]), carry, 0)
binary.BigEndian.PutUint64(id[8*(3-i):], out)
}
}
return id
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package netlogtype defines types for network logging.
package netlogtype
import (
"maps"
"net/netip"
"sync"
"time"
"tailscale.com/tailcfg"
"tailscale.com/types/ipproto"
)
// Message is the log message that captures network traffic.
type Message struct {
NodeID tailcfg.StableNodeID `json:"nodeId"` // e.g., "n123456CNTRL"
Start time.Time `json:"start"` // inclusive
End time.Time `json:"end"` // inclusive
SrcNode Node `json:"srcNode,omitzero"`
DstNodes []Node `json:"dstNodes,omitempty"`
VirtualTraffic []ConnectionCounts `json:"virtualTraffic,omitempty"`
SubnetTraffic []ConnectionCounts `json:"subnetTraffic,omitempty"`
ExitTraffic []ConnectionCounts `json:"exitTraffic,omitempty"`
PhysicalTraffic []ConnectionCounts `json:"physicalTraffic,omitempty"`
}
const (
messageJSON = `{"nodeId":` + maxJSONStableID + `,` + minJSONNodes + `,` + maxJSONTimeRange + `,` + minJSONTraffic + `}`
maxJSONStableID = `"n0123456789abcdefCNTRL"`
minJSONNodes = `"srcNode":{},"dstNodes":[]`
maxJSONTimeRange = `"start":` + maxJSONRFC3339 + `,"end":` + maxJSONRFC3339
maxJSONRFC3339 = `"0001-01-01T00:00:00.000000000Z"`
minJSONTraffic = `"virtualTraffic":{},"subnetTraffic":{},"exitTraffic":{},"physicalTraffic":{}`
// MinMessageJSONSize is the overhead size of Message when it is
// serialized as JSON assuming that each field is minimally populated.
// Each [Node] occupies at least [MinNodeJSONSize].
// Each [ConnectionCounts] occupies at most [MaxConnectionCountsJSONSize].
MinMessageJSONSize = len(messageJSON)
maxJSONConnCounts = `{` + maxJSONConn + `,` + maxJSONCounts + `}`
maxJSONConn = `"proto":` + maxJSONProto + `,"src":` + maxJSONAddrPort + `,"dst":` + maxJSONAddrPort
maxJSONProto = `255`
maxJSONAddrPort = `"[ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff]:65535"`
maxJSONCounts = `"txPkts":` + maxJSONCount + `,"txBytes":` + maxJSONCount + `,"rxPkts":` + maxJSONCount + `,"rxBytes":` + maxJSONCount
maxJSONCount = `18446744073709551615`
// MaxConnectionCountsJSONSize is the maximum size of a ConnectionCounts
// when it is serialized as JSON, assuming no superfluous whitespace.
// It does not include the trailing comma that often appears when
// this object is nested within an array.
// It assumes that netip.Addr never has IPv6 zones.
MaxConnectionCountsJSONSize = len(maxJSONConnCounts)
)
// Node is information about a node.
type Node struct {
// NodeID is the stable ID of the node.
NodeID tailcfg.StableNodeID `json:"nodeId"`
// Name is the fully-qualified name of the node.
Name string `json:"name,omitzero"` // e.g., "carbonite.example.ts.net"
// Addresses are the Tailscale IP addresses of the node.
Addresses []netip.Addr `json:"addresses,omitempty"`
// OS is the operating system of the node.
OS string `json:"os,omitzero"` // e.g., "linux"
// User is the user that owns the node.
// It is not populated if the node is tagged.
User string `json:"user,omitzero"` // e.g., "johndoe@example.com"
// Tags are the tags of the node.
// It is not populated if the node is owned by a user.
Tags []string `json:"tags,omitempty"` // e.g., ["tag:prod","tag:logs"]
}
// ConnectionCounts is a flattened struct of both a connection and counts.
type ConnectionCounts struct {
Connection
Counts
}
// Connection is a 5-tuple of proto, source and destination IP and port.
type Connection struct {
Proto ipproto.Proto `json:"proto,omitzero"`
Src netip.AddrPort `json:"src,omitzero"`
Dst netip.AddrPort `json:"dst,omitzero"`
}
func (c Connection) IsZero() bool { return c == Connection{} }
// Counts are statistics about a particular connection.
type Counts struct {
TxPackets uint64 `json:"txPkts,omitzero"`
TxBytes uint64 `json:"txBytes,omitzero"`
RxPackets uint64 `json:"rxPkts,omitzero"`
RxBytes uint64 `json:"rxBytes,omitzero"`
}
func (c Counts) IsZero() bool { return c == Counts{} }
// Add adds the counts from both c1 and c2.
func (c1 Counts) Add(c2 Counts) Counts {
c1.TxPackets += c2.TxPackets
c1.TxBytes += c2.TxBytes
c1.RxPackets += c2.RxPackets
c1.RxBytes += c2.RxBytes
return c1
}
// CountsByConnection is a count of packets and bytes for each connection.
// All methods are safe for concurrent calls.
type CountsByConnection struct {
mu sync.Mutex
m map[Connection]Counts
}
// Add adds packets and bytes for the specified connection.
func (c *CountsByConnection) Add(proto ipproto.Proto, src, dst netip.AddrPort, packets, bytes int, recv bool) {
conn := Connection{Proto: proto, Src: src, Dst: dst}
c.mu.Lock()
defer c.mu.Unlock()
if c.m == nil {
c.m = make(map[Connection]Counts)
}
cnts := c.m[conn]
if recv {
cnts.RxPackets += uint64(packets)
cnts.RxBytes += uint64(bytes)
} else {
cnts.TxPackets += uint64(packets)
cnts.TxBytes += uint64(bytes)
}
c.m[conn] = cnts
}
// Clone deep copies the map.
func (c *CountsByConnection) Clone() map[Connection]Counts {
c.mu.Lock()
defer c.mu.Unlock()
return maps.Clone(c.m)
}
// Reset clear the map.
func (c *CountsByConnection) Reset() {
c.mu.Lock()
defer c.mu.Unlock()
clear(c.m)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package nettype defines an interface that doesn't exist in the Go net package.
package nettype
import (
"context"
"io"
"net"
"net/netip"
"time"
)
// PacketListener defines the ListenPacket method as implemented
// by net.ListenConfig, net.ListenPacket, and tstest/natlab.
type PacketListener interface {
ListenPacket(ctx context.Context, network, address string) (net.PacketConn, error)
}
type PacketListenerWithNetIP interface {
ListenPacket(ctx context.Context, network, address string) (PacketConn, error)
}
// Std implements PacketListener using the Go net package's ListenPacket func.
type Std struct{}
func (Std) ListenPacket(ctx context.Context, network, address string) (net.PacketConn, error) {
var conf net.ListenConfig
return conf.ListenPacket(ctx, network, address)
}
// PacketConn is like a net.PacketConn but uses the newer netip.AddrPort
// write/read methods.
type PacketConn interface {
WriteToUDPAddrPort([]byte, netip.AddrPort) (int, error)
ReadFromUDPAddrPort([]byte) (int, netip.AddrPort, error)
io.Closer
LocalAddr() net.Addr
SetDeadline(time.Time) error
SetReadDeadline(time.Time) error
SetWriteDeadline(time.Time) error
}
func MakePacketListenerWithNetIP(ln PacketListener) PacketListenerWithNetIP {
return packetListenerAdapter{ln}
}
type packetListenerAdapter struct {
PacketListener
}
func (a packetListenerAdapter) ListenPacket(ctx context.Context, network, address string) (PacketConn, error) {
pc, err := a.PacketListener.ListenPacket(ctx, network, address)
if err != nil {
return nil, err
}
return pc.(PacketConn), nil
}
// ConnPacketConn is the interface that's a superset of net.Conn and net.PacketConn.
type ConnPacketConn interface {
net.Conn
net.PacketConn
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package opt defines optional types.
package opt
import (
"fmt"
"strconv"
)
// Bool represents an optional boolean to be JSON-encoded. The string
// is either "true", "false", or the empty string to mean unset.
//
// As a special case, the underlying string may also be the string
// "unset" as a synonym for the empty string. This lets the
// explicit unset value be exchanged over an encoding/json "omitempty"
// field without it being dropped.
type Bool string
const (
// True is the encoding of an explicit true.
True = Bool("true")
// False is the encoding of an explicit false.
False = Bool("false")
// ExplicitlyUnset is the encoding used by a null
// JSON value. It is a synonym for the empty string.
ExplicitlyUnset = Bool("unset")
// Empty means the Bool is unset and it's neither
// true nor false.
Empty = Bool("")
)
// NewBool constructs a new Bool value equal to b. The returned Bool is set,
// unless Set("") or Clear() methods are called.
func NewBool(b bool) Bool {
return Bool(strconv.FormatBool(b))
}
func (b *Bool) Set(v bool) {
*b = Bool(strconv.FormatBool(v))
}
func (b *Bool) Clear() { *b = "" }
func (b Bool) Get() (v bool, ok bool) {
switch b {
case "true":
return true, true
case "false":
return false, true
default:
return false, false
}
}
// Scan implements database/sql.Scanner.
func (b *Bool) Scan(src any) error {
if src == nil {
*b = ""
return nil
}
switch src := src.(type) {
case bool:
if src {
*b = True
} else {
*b = False
}
return nil
case int64:
if src == 0 {
*b = False
} else {
*b = True
}
return nil
default:
return fmt.Errorf("opt.Bool.Scan: invalid type %T: %v", src, src)
}
}
// Normalized returns the normalized form of b, mapping "unset" to ""
// and leaving other values unchanged.
func (b Bool) Normalized() Bool {
switch b {
case ExplicitlyUnset:
return Empty
default:
return b
}
}
// EqualBool reports whether b is equal to v.
// If b is empty or not a valid bool, it reports false.
func (b Bool) EqualBool(v bool) bool {
p, ok := b.Get()
return ok && p == v
}
var (
trueBytes = []byte(True)
falseBytes = []byte(False)
nullBytes = []byte("null")
)
func (b Bool) MarshalJSON() ([]byte, error) {
switch b {
case True:
return trueBytes, nil
case False:
return falseBytes, nil
case Empty, ExplicitlyUnset:
return nullBytes, nil
}
return nil, fmt.Errorf("invalid opt.Bool value %q", string(b))
}
func (b *Bool) UnmarshalJSON(j []byte) error {
switch string(j) {
case "true":
*b = True
case "false":
*b = False
case "null":
*b = ExplicitlyUnset
default:
return fmt.Errorf("invalid opt.Bool value %q", j)
}
return nil
}
// BoolFlag is a wrapper for Bool that implements [flag.Value].
type BoolFlag struct {
*Bool
}
// Set the value of b, using any value supported by [strconv.ParseBool].
func (b *BoolFlag) Set(s string) error {
v, err := strconv.ParseBool(s)
if err != nil {
return err
}
b.Bool.Set(v)
return nil
}
// String returns "true" or "false" if the value is set, or an empty string otherwise.
func (b *BoolFlag) String() string {
if b == nil || b.Bool == nil {
return ""
}
if v, ok := b.Bool.Get(); ok {
return strconv.FormatBool(v)
}
return ""
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package opt
import (
"fmt"
"reflect"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
)
// Value is an optional value to be JSON-encoded.
// With [encoding/json], a zero Value is marshaled as a JSON null.
// With [github.com/go-json-experiment/json], a zero Value is omitted from the
// JSON object if the Go struct field specified with omitzero.
// The omitempty tag option should never be used with Value fields.
type Value[T any] struct {
value T
set bool
}
// Equal reports whether the receiver and the other value are equal.
// If the template type T in Value[T] implements an Equal method, it will be used
// instead of the == operator for comparing values.
type equatable[T any] interface {
// Equal reports whether the receiver and the other values are equal.
Equal(other T) bool
}
// ValueOf returns an optional Value containing the specified value.
// It treats nil slices and maps as empty slices and maps.
func ValueOf[T any](v T) Value[T] {
return Value[T]{value: v, set: true}
}
// String implements [fmt.Stringer].
func (o Value[T]) String() string {
if !o.set {
return fmt.Sprintf("(empty[%T])", o.value)
}
return fmt.Sprint(o.value)
}
// Set assigns the specified value to the optional value o.
func (o *Value[T]) Set(v T) {
*o = ValueOf(v)
}
// Clear resets o to an empty state.
func (o *Value[T]) Clear() {
*o = Value[T]{}
}
// IsSet reports whether o has a value set.
func (o *Value[T]) IsSet() bool {
return o.set
}
// Get returns the value of o.
// If a value hasn't been set, a zero value of T will be returned.
func (o Value[T]) Get() T {
return o.value
}
// GetOr returns the value of o or def if a value hasn't been set.
func (o Value[T]) GetOr(def T) T {
if o.set {
return o.value
}
return def
}
// Get returns the value and a flag indicating whether the value is set.
func (o Value[T]) GetOk() (v T, ok bool) {
return o.value, o.set
}
// Equal reports whether o is equal to v.
// Two optional values are equal if both are empty,
// or if both are set and the underlying values are equal.
// If the template type T implements an Equal(T) bool method, it will be used
// instead of the == operator for value comparison.
// If T is not comparable, it returns false.
func (o Value[T]) Equal(v Value[T]) bool {
if o.set != v.set {
return false
}
if !o.set {
return true
}
ov := any(o.value)
if eq, ok := ov.(equatable[T]); ok {
return eq.Equal(v.value)
}
if reflect.TypeFor[T]().Comparable() {
return ov == any(v.value)
}
return false
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (o Value[T]) MarshalJSONTo(enc *jsontext.Encoder) error {
if !o.set {
return enc.WriteToken(jsontext.Null)
}
// Pin v2 semantics so the wire format (e.g. nil slices as "[]",
// nil maps as "{}") stays the same even when this method is
// reached via encoding/json v1, which as of Go 1.27 dispatches
// here with v1 options that would otherwise leak into this call.
return jsonv2.MarshalEncode(enc, &o.value, jsonv2.DefaultOptionsV2())
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (o *Value[T]) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if dec.PeekKind() == 'n' {
*o = Value[T]{}
_, err := dec.ReadToken() // read null
return err
}
o.set = true
// Pin v2 semantics; see MarshalJSONTo.
return jsonv2.UnmarshalDecode(dec, &o.value, jsonv2.DefaultOptionsV2())
}
// MarshalJSON implements [json.Marshaler].
func (o Value[T]) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(o) // uses MarshalJSONTo
}
// UnmarshalJSON implements [json.Unmarshaler].
func (o *Value[T]) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, o) // uses UnmarshalJSONFrom
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package persist contains the Persist type.
package persist
import (
"fmt"
"reflect"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/structs"
)
//go:generate go run tailscale.com/cmd/viewer -type=Persist
// Persist is the JSON type stored on disk on nodes to remember their
// settings between runs. This is stored as part of ipn.Prefs and is
// persisted per ipn.LoginProfile.
type Persist struct {
_ structs.Incomparable
PrivateNodeKey key.NodePrivate
OldPrivateNodeKey key.NodePrivate // needed to request key rotation
UserProfile tailcfg.UserProfile
// NetworkLockKey is the node's Tailnet Lock private key.
// "Network Lock" was the pre-release name for Tailnet Lock.
NetworkLockKey key.NLPrivate
NodeID tailcfg.StableNodeID
AttestationKey key.HardwareAttestationKey `json:",omitzero"`
// DisallowedTKAStateIDs stores the tka.State.StateID values which
// this node will not operate tailnet lock on. This is used to
// prevent bootstrapping TKA onto a key authority which was forcibly
// disabled.
DisallowedTKAStateIDs []string `json:",omitempty"`
}
// PublicNodeKey returns the public key for the node key.
func (p *Persist) PublicNodeKey() key.NodePublic {
return p.PrivateNodeKey.Public()
}
// PublicNodeKeyOK returns the public key for the node key.
//
// Unlike PublicNodeKey, it returns ok=false if there is no node private key
// instead of panicking.
func (p *Persist) PublicNodeKeyOK() (pub key.NodePublic, ok bool) {
if p.PrivateNodeKey.IsZero() {
return
}
return p.PrivateNodeKey.Public(), true
}
// PublicNodeKey returns the public key for the node key.
//
// It panics if there is no node private key. See PublicNodeKeyOK.
func (p PersistView) PublicNodeKey() key.NodePublic {
return p.ж.PublicNodeKey()
}
// PublicNodeKeyOK returns the public key for the node key.
//
// Unlike PublicNodeKey, it returns ok=false if there is no node private key
// instead of panicking.
func (p PersistView) PublicNodeKeyOK() (_ key.NodePublic, ok bool) {
return p.ж.PublicNodeKeyOK()
}
func (p PersistView) Equals(p2 PersistView) bool {
return p.ж.Equals(p2.ж)
}
func nilIfEmpty[E any](s []E) []E {
if len(s) == 0 {
return nil
}
return s
}
func (p *Persist) Equals(p2 *Persist) bool {
if p == nil && p2 == nil {
return true
}
if p == nil || p2 == nil {
return false
}
var pub, p2Pub key.HardwareAttestationPublic
if p.AttestationKey != nil && !p.AttestationKey.IsZero() {
pub = key.HardwareAttestationPublicFromPlatformKey(p.AttestationKey)
}
if p2.AttestationKey != nil && !p2.AttestationKey.IsZero() {
p2Pub = key.HardwareAttestationPublicFromPlatformKey(p2.AttestationKey)
}
return p.PrivateNodeKey.Equal(p2.PrivateNodeKey) &&
p.OldPrivateNodeKey.Equal(p2.OldPrivateNodeKey) &&
p.UserProfile.Equal(&p2.UserProfile) &&
p.NetworkLockKey.Equal(p2.NetworkLockKey) &&
p.NodeID == p2.NodeID &&
pub.Equal(p2Pub) &&
reflect.DeepEqual(nilIfEmpty(p.DisallowedTKAStateIDs), nilIfEmpty(p2.DisallowedTKAStateIDs))
}
func (p *Persist) Pretty() string {
var (
ok, nk key.NodePublic
)
akString := "-"
if !p.OldPrivateNodeKey.IsZero() {
ok = p.OldPrivateNodeKey.Public()
}
if !p.PrivateNodeKey.IsZero() {
nk = p.PublicNodeKey()
}
if p.AttestationKey != nil && !p.AttestationKey.IsZero() {
akString = fmt.Sprintf("%v", p.AttestationKey.Public())
}
return fmt.Sprintf("Persist{o=%v, n=%v u=%#v ak=%s}",
ok.ShortString(), nk.ShortString(), p.UserProfile.LoginName, akString)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
package persist
import (
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/structs"
)
// Clone makes a deep copy of Persist.
// The result aliases no memory with the original.
func (src *Persist) Clone() *Persist {
if src == nil {
return nil
}
dst := new(Persist)
*dst = *src
dst.UserProfile = *src.UserProfile.Clone()
if src.AttestationKey != nil {
dst.AttestationKey = src.AttestationKey.Clone()
}
dst.DisallowedTKAStateIDs = append(src.DisallowedTKAStateIDs[:0:0], src.DisallowedTKAStateIDs...)
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _PersistCloneNeedsRegeneration = Persist(struct {
_ structs.Incomparable
PrivateNodeKey key.NodePrivate
OldPrivateNodeKey key.NodePrivate
UserProfile tailcfg.UserProfile
NetworkLockKey key.NLPrivate
NodeID tailcfg.StableNodeID
AttestationKey key.HardwareAttestationKey
DisallowedTKAStateIDs []string
}{})
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by tailscale/cmd/viewer; DO NOT EDIT.
package persist
import (
jsonv1 "encoding/json"
"errors"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/structs"
"tailscale.com/types/views"
)
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=Persist
// View returns a read-only view of Persist.
func (p *Persist) View() PersistView {
return PersistView{ж: p}
}
// PersistView provides a read-only view over Persist.
//
// Its methods should only be called if `Valid()` returns true.
type PersistView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *Persist
}
// Valid reports whether v's underlying value is non-nil.
func (v PersistView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v PersistView) AsStruct() *Persist {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v PersistView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v PersistView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *PersistView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x Persist
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *PersistView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x Persist
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v PersistView) PrivateNodeKey() key.NodePrivate { return v.ж.PrivateNodeKey }
// needed to request key rotation
func (v PersistView) OldPrivateNodeKey() key.NodePrivate { return v.ж.OldPrivateNodeKey }
func (v PersistView) UserProfile() tailcfg.UserProfileView { return v.ж.UserProfile.View() }
// NetworkLockKey is the node's Tailnet Lock private key.
// "Network Lock" was the pre-release name for Tailnet Lock.
func (v PersistView) NetworkLockKey() key.NLPrivate { return v.ж.NetworkLockKey }
func (v PersistView) NodeID() tailcfg.StableNodeID { return v.ж.NodeID }
func (v PersistView) AttestationKey() tailcfg.StableNodeID { panic("unsupported") }
// DisallowedTKAStateIDs stores the tka.State.StateID values which
// this node will not operate tailnet lock on. This is used to
// prevent bootstrapping TKA onto a key authority which was forcibly
// disabled.
func (v PersistView) DisallowedTKAStateIDs() views.Slice[string] {
return views.SliceOf(v.ж.DisallowedTKAStateIDs)
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _PersistViewNeedsRegeneration = Persist(struct {
_ structs.Incomparable
PrivateNodeKey key.NodePrivate
OldPrivateNodeKey key.NodePrivate
UserProfile tailcfg.UserProfile
NetworkLockKey key.NLPrivate
NodeID tailcfg.StableNodeID
AttestationKey key.HardwareAttestationKey
DisallowedTKAStateIDs []string
}{})
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package preftype is a leaf package containing types for various
// preferences.
package preftype
import "fmt"
// NetfilterMode is the firewall management mode to use when
// programming the Linux network stack.
type NetfilterMode int
// These numbers are persisted to disk in JSON files and thus can't be
// renumbered or repurposed.
const (
NetfilterOff NetfilterMode = 0 // remove all tailscale netfilter state
NetfilterNoDivert NetfilterMode = 1 // manage tailscale chains, but don't call them
NetfilterOn NetfilterMode = 2 // manage tailscale chains and call them from main chains
)
func ParseNetfilterMode(s string) (NetfilterMode, error) {
switch s {
case "off":
return NetfilterOff, nil
case "nodivert":
return NetfilterNoDivert, nil
case "on":
return NetfilterOn, nil
default:
return NetfilterOff, fmt.Errorf("unknown netfilter mode %q", s)
}
}
func (m NetfilterMode) String() string {
switch m {
case NetfilterOff:
return "off"
case NetfilterNoDivert:
return "nodivert"
case NetfilterOn:
return "on"
default:
return "???"
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package result contains the Of result type, which is
// either a value or an error.
package result
// Of is either a T value or an error.
//
// Think of it like Rust or Swift's result types.
// It's named "Of" because the fully qualified name
// for callers reads result.Of[T].
type Of[T any] struct {
v T // valid if Err is nil; invalid if Err is non-nil
err error
}
// Value returns a new result with value v,
// without an error.
func Value[T any](v T) Of[T] {
return Of[T]{v: v}
}
// Error returns a new result with error err.
// If err is nil, the returned result is equivalent
// to calling Value with T's zero value.
func Error[T any](err error) Of[T] {
return Of[T]{err: err}
}
// MustValue returns r's result value.
// It panics if r.Err returns non-nil.
func (r Of[T]) MustValue() T {
if r.err != nil {
panic(r.err)
}
return r.v
}
// Value returns r's result value and error.
func (r Of[T]) Value() (T, error) {
return r.v, r.err
}
// Err returns r's error, if any.
// When r.Err returns nil, it's safe to call r.MustValue without it panicking.
func (r Of[T]) Err() error {
return r.err
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package views provides read-only accessors for commonly used
// value types.
package views
import (
"bytes"
"cmp"
jsonv1 "encoding/json"
"errors"
"fmt"
"iter"
"maps"
"reflect"
"slices"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"go4.org/mem"
)
// ByteSlice is a read-only accessor for types that are backed by a []byte.
type ByteSlice[T ~[]byte] struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж T
}
// ByteSliceOf returns a ByteSlice for the provided slice.
func ByteSliceOf[T ~[]byte](x T) ByteSlice[T] {
return ByteSlice[T]{x}
}
// MapKey returns a unique key for a slice, based on its address and length.
func (v ByteSlice[T]) MapKey() SliceMapKey[byte] { return mapKey(v.ж) }
// Len returns the length of the slice.
func (v ByteSlice[T]) Len() int {
return len(v.ж)
}
// IsNil reports whether the underlying slice is nil.
func (v ByteSlice[T]) IsNil() bool {
return v.ж == nil
}
// Mem returns a read-only view of the underlying slice.
func (v ByteSlice[T]) Mem() mem.RO {
return mem.B(v.ж)
}
// Equal reports whether the underlying slice is equal to b.
func (v ByteSlice[T]) Equal(b T) bool {
return bytes.Equal(v.ж, b)
}
// EqualView reports whether the underlying slice is equal to b.
func (v ByteSlice[T]) EqualView(b ByteSlice[T]) bool {
return bytes.Equal(v.ж, b.ж)
}
// AsSlice returns a copy of the underlying slice.
func (v ByteSlice[T]) AsSlice() T {
return v.AppendTo(v.ж[:0:0])
}
// AppendTo appends the underlying slice values to dst.
func (v ByteSlice[T]) AppendTo(dst T) T {
return append(dst, v.ж...)
}
// At returns the byte at index `i` of the slice.
func (v ByteSlice[T]) At(i int) byte { return v.ж[i] }
// SliceFrom returns v[i:].
func (v ByteSlice[T]) SliceFrom(i int) ByteSlice[T] { return ByteSlice[T]{v.ж[i:]} }
// SliceTo returns v[:i]
func (v ByteSlice[T]) SliceTo(i int) ByteSlice[T] { return ByteSlice[T]{v.ж[:i]} }
// Slice returns v[i:j]
func (v ByteSlice[T]) Slice(i, j int) ByteSlice[T] { return ByteSlice[T]{v.ж[i:j]} }
// MarshalJSON implements [jsonv1.Marshaler].
func (v ByteSlice[T]) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v ByteSlice[T]) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
// It must only be called on an uninitialized ByteSlice.
func (v *ByteSlice[T]) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
return jsonv1.Unmarshal(b, &v.ж)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
// It must only be called on an uninitialized ByteSlice.
func (v *ByteSlice[T]) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
return jsonv2.UnmarshalDecode(dec, &v.ж)
}
// StructView represents the corresponding StructView of a Viewable. The concrete types are
// typically generated by tailscale.com/cmd/viewer.
type StructView[T any] interface {
// Valid reports whether the underlying Viewable is nil.
Valid() bool
// AsStruct returns a deep-copy of the underlying value.
// It returns nil, if Valid() is false.
AsStruct() T
}
// Cloner is any type that has a Clone function returning a deep-clone of the receiver.
type Cloner[T any] interface {
// Clone returns a deep-clone of the receiver.
// It returns nil, when the receiver is nil.
Clone() T
}
// ViewCloner is any type that has had View and Clone funcs generated using
// tailscale.com/cmd/viewer.
type ViewCloner[T any, V StructView[T]] interface {
// View returns a read-only view of Viewable.
// If Viewable is nil, View().Valid() reports false.
View() V
// Clone returns a deep-clone of Viewable.
// It returns nil, when Viewable is nil.
Clone() T
}
// SliceOfViews returns a ViewSlice for x.
func SliceOfViews[T ViewCloner[T, V], V StructView[T]](x []T) SliceView[T, V] {
return SliceView[T, V]{x}
}
// SliceView wraps []T to provide accessors which return an immutable view V of
// T. It is used to provide the equivalent of SliceOf([]V) without having to
// allocate []V from []T.
type SliceView[T ViewCloner[T, V], V StructView[T]] struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж []T
}
// All returns an iterator over v.
func (v SliceView[T, V]) All() iter.Seq2[int, V] {
return func(yield func(int, V) bool) {
for i := range v.ж {
if !yield(i, v.ж[i].View()) {
return
}
}
}
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v SliceView[T, V]) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v SliceView[T, V]) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
// It must only be called on an uninitialized SliceView.
func (v *SliceView[T, V]) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
} else if len(b) == 0 {
return nil
}
return jsonv1.Unmarshal(b, &v.ж)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
// It must only be called on an uninitialized SliceView.
func (v *SliceView[T, V]) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
return jsonv2.UnmarshalDecode(dec, &v.ж)
}
// IsNil reports whether the underlying slice is nil.
func (v SliceView[T, V]) IsNil() bool { return v.ж == nil }
// Len returns the length of the slice.
func (v SliceView[T, V]) Len() int { return len(v.ж) }
// At returns a View of the element at index `i` of the slice.
func (v SliceView[T, V]) At(i int) V { return v.ж[i].View() }
// SliceFrom returns v[i:].
func (v SliceView[T, V]) SliceFrom(i int) SliceView[T, V] { return SliceView[T, V]{v.ж[i:]} }
// SliceTo returns v[:i]
func (v SliceView[T, V]) SliceTo(i int) SliceView[T, V] { return SliceView[T, V]{v.ж[:i]} }
// Slice returns v[i:j]
func (v SliceView[T, V]) Slice(i, j int) SliceView[T, V] { return SliceView[T, V]{v.ж[i:j]} }
// SliceMapKey represents a comparable unique key for a slice, based on its
// address and length. It can be used to key maps by slices but should only be
// used when the underlying slice is immutable.
//
// Empty and nil slices have different keys.
type SliceMapKey[T any] struct {
// t is the address of the first element, or nil if the slice is nil or
// empty.
t *T
// n is the length of the slice, or -1 if the slice is nil.
n int
}
// MapKey returns a unique key for a slice, based on its address and length.
func (v SliceView[T, V]) MapKey() SliceMapKey[T] { return mapKey(v.ж) }
// AppendTo appends the underlying slice values to dst.
func (v SliceView[T, V]) AppendTo(dst []V) []V {
for _, x := range v.ж {
dst = append(dst, x.View())
}
return dst
}
// AsSlice returns a copy of underlying slice.
func (v SliceView[T, V]) AsSlice() []V {
return v.AppendTo(nil)
}
// Slice is a read-only accessor for a slice.
type Slice[T any] struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж []T
}
// All returns an iterator over v.
func (v Slice[T]) All() iter.Seq2[int, T] {
return func(yield func(int, T) bool) {
for i, v := range v.ж {
if !yield(i, v) {
return
}
}
}
}
// MapKey returns a unique key for a slice, based on its address and length.
func (v Slice[T]) MapKey() SliceMapKey[T] { return mapKey(v.ж) }
// mapKey returns a unique key for a slice, based on its address and length.
func mapKey[T any](x []T) SliceMapKey[T] {
if x == nil {
return SliceMapKey[T]{nil, -1}
}
if len(x) == 0 {
return SliceMapKey[T]{nil, 0}
}
return SliceMapKey[T]{&x[0], len(x)}
}
// SliceOf returns a Slice for the provided slice for immutable values.
// It is the caller's responsibility to make sure V is immutable.
func SliceOf[T any](x []T) Slice[T] {
return Slice[T]{x}
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v Slice[T]) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v Slice[T]) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
// It must only be called on an uninitialized Slice.
func (v *Slice[T]) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
} else if len(b) == 0 {
return nil
}
return jsonv1.Unmarshal(b, &v.ж)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
// It must only be called on an uninitialized Slice.
func (v *Slice[T]) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
return jsonv2.UnmarshalDecode(dec, &v.ж)
}
// IsNil reports whether the underlying slice is nil.
func (v Slice[T]) IsNil() bool { return v.ж == nil }
// Len returns the length of the slice.
func (v Slice[T]) Len() int { return len(v.ж) }
// At returns the element at index `i` of the slice.
func (v Slice[T]) At(i int) T { return v.ж[i] }
// SliceFrom returns v[i:].
func (v Slice[T]) SliceFrom(i int) Slice[T] { return Slice[T]{v.ж[i:]} }
// SliceTo returns v[:i]
func (v Slice[T]) SliceTo(i int) Slice[T] { return Slice[T]{v.ж[:i]} }
// Slice returns v[i:j]
func (v Slice[T]) Slice(i, j int) Slice[T] { return Slice[T]{v.ж[i:j]} }
// AppendTo appends the underlying slice values to dst.
func (v Slice[T]) AppendTo(dst []T) []T {
return append(dst, v.ж...)
}
// AsSlice returns a copy of underlying slice.
func (v Slice[T]) AsSlice() []T {
return v.AppendTo(v.ж[:0:0])
}
// IndexFunc returns the first index of an element in v satisfying f(e),
// or -1 if none do.
//
// As it runs in O(n) time, use with care.
func (v Slice[T]) IndexFunc(f func(T) bool) int {
for i := range v.Len() {
if f(v.At(i)) {
return i
}
}
return -1
}
// ContainsFunc reports whether any element in v satisfies f(e).
//
// As it runs in O(n) time, use with care.
func (v Slice[T]) ContainsFunc(f func(T) bool) bool {
return slices.ContainsFunc(v.ж, f)
}
// MaxFunc returns the maximal value in v, using cmp to compare elements. It
// panics if v is empty. If there is more than one maximal element according to
// the cmp function, MaxFunc returns the first one. See also [slices.MaxFunc].
func (v Slice[T]) MaxFunc(cmp func(a, b T) int) T {
return slices.MaxFunc(v.ж, cmp)
}
// MinFunc returns the minimal value in v, using cmp to compare elements. It
// panics if v is empty. If there is more than one minimal element according to
// the cmp function, MinFunc returns the first one. See also [slices.MinFunc].
func (v Slice[T]) MinFunc(cmp func(a, b T) int) T {
return slices.MinFunc(v.ж, cmp)
}
// AppendStrings appends the string representation of each element in v to dst.
func AppendStrings[T fmt.Stringer](dst []string, v Slice[T]) []string {
for _, x := range v.ж {
dst = append(dst, x.String())
}
return dst
}
// SliceContains reports whether v contains element e.
//
// As it runs in O(n) time, use with care.
func SliceContains[T comparable](v Slice[T], e T) bool {
return slices.Contains(v.ж, e)
}
// SliceEqual is like the standard library's slices.Equal, but for two views.
func SliceEqual[T comparable](a, b Slice[T]) bool {
return slices.Equal(a.ж, b.ж)
}
// SliceMax returns the maximal value in v. It panics if v is empty. For
// floating point T, SliceMax propagates NaNs (any NaN value in v forces the
// output to be NaN). See also [slices.Max].
func SliceMax[T cmp.Ordered](v Slice[T]) T {
return slices.Max(v.ж)
}
// SliceMin returns the minimal value in v. It panics if v is empty. For
// floating point T, SliceMin propagates NaNs (any NaN value in v forces the
// output to be NaN). See also [slices.Min].
func SliceMin[T cmp.Ordered](v Slice[T]) T {
return slices.Min(v.ж)
}
// shortOOOLen (short Out-of-Order length) is the slice length at or
// under which we attempt to compare two slices quadratically rather
// than allocating memory for a map in SliceEqualAnyOrder and
// SliceEqualAnyOrderFunc.
const shortOOOLen = 5
// SliceEqualAnyOrder reports whether a and b contain the same elements, regardless of order.
// The underlying slices for a and b can be nil.
func SliceEqualAnyOrder[T comparable](a, b Slice[T]) bool {
if a.Len() != b.Len() {
return false
}
var diffStart int // beginning index where a and b differ
for n := a.Len(); diffStart < n; diffStart++ {
if a.At(diffStart) != b.At(diffStart) {
break
}
}
if diffStart == a.Len() {
return true
}
a, b = a.SliceFrom(diffStart), b.SliceFrom(diffStart)
cmp := func(v T) T { return v }
// For a small number of items, avoid the allocation of a map and just
// do the quadratic thing.
if a.Len() <= shortOOOLen {
return unorderedSliceEqualAnyOrderSmall(a, b, cmp)
}
return unorderedSliceEqualAnyOrder(a, b, cmp)
}
// SliceEqualAnyOrderFunc reports whether a and b contain the same elements,
// regardless of order. The underlying slices for a and b can be nil.
//
// The provided function should return a comparable value for each element.
func SliceEqualAnyOrderFunc[T any, V comparable](a, b Slice[T], cmp func(T) V) bool {
if a.Len() != b.Len() {
return false
}
var diffStart int // beginning index where a and b differ
for n := a.Len(); diffStart < n; diffStart++ {
av := cmp(a.At(diffStart))
bv := cmp(b.At(diffStart))
if av != bv {
break
}
}
if diffStart == a.Len() {
return true
}
a, b = a.SliceFrom(diffStart), b.SliceFrom(diffStart)
// For a small number of items, avoid the allocation of a map and just
// do the quadratic thing.
if a.Len() <= shortOOOLen {
return unorderedSliceEqualAnyOrderSmall(a, b, cmp)
}
return unorderedSliceEqualAnyOrder(a, b, cmp)
}
// unorderedSliceEqualAnyOrder reports whether a and b contain the same elements
// using a map. The cmp function maps from a T slice element to a comparable
// value.
func unorderedSliceEqualAnyOrder[T any, V comparable](a, b Slice[T], cmp func(T) V) bool {
if a.Len() != b.Len() {
panic("internal error")
}
if a.Len() == 0 {
return true
}
m := make(map[V]int)
for i := range a.Len() {
m[cmp(a.At(i))]++
m[cmp(b.At(i))]--
}
for _, count := range m {
if count != 0 {
return false
}
}
return true
}
// unorderedSliceEqualAnyOrderSmall reports whether a and b (which must be the
// same length, and shortOOOLen or shorter) contain the same elements (using cmp
// to map from T to a comparable value) in some order.
//
// This is the quadratic-time implementation for small slices that doesn't
// allocate.
func unorderedSliceEqualAnyOrderSmall[T any, V comparable](a, b Slice[T], cmp func(T) V) bool {
if a.Len() != b.Len() || a.Len() > shortOOOLen {
panic("internal error")
}
// These track which elements in a and b have been matched, so
// that we don't treat arrays with differing number of
// duplicate elements as equal (e.g. [1, 1, 2] and [1, 2, 2]).
var aMatched, bMatched [shortOOOLen]bool
// Compare each element in a to each element in b
for i := range a.Len() {
av := cmp(a.At(i))
found := false
for j := range a.Len() {
// Skip elements in b that have already been
// used to match an item in a.
if bMatched[j] {
continue
}
bv := cmp(b.At(j))
if av == bv {
// Mark these elements as already
// matched, so that a future loop
// iteration (of a duplicate element)
// doesn't match it again.
aMatched[i] = true
bMatched[j] = true
found = true
break
}
}
if !found {
return false
}
}
// Verify all elements were matched exactly once.
for i := range a.Len() {
if !aMatched[i] || !bMatched[i] {
return false
}
}
return true
}
// MapSlice is a view over a map whose values are slices.
type MapSlice[K comparable, V any] struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж map[K][]V
}
// MapSliceOf returns a MapSlice for the provided map. It is the caller's
// responsibility to make sure V is immutable.
func MapSliceOf[K comparable, V any](m map[K][]V) MapSlice[K, V] {
return MapSlice[K, V]{m}
}
// Contains reports whether k has an entry in the map.
func (m MapSlice[K, V]) Contains(k K) bool {
_, ok := m.ж[k]
return ok
}
// IsNil reports whether the underlying map is nil.
func (m MapSlice[K, V]) IsNil() bool {
return m.ж == nil
}
// Len returns the number of elements in the map.
func (m MapSlice[K, V]) Len() int { return len(m.ж) }
// Get returns the element with key k.
func (m MapSlice[K, V]) Get(k K) Slice[V] {
return SliceOf(m.ж[k])
}
// GetOk returns the element with key k and a bool representing whether the key
// is in map.
func (m MapSlice[K, V]) GetOk(k K) (Slice[V], bool) {
v, ok := m.ж[k]
return SliceOf(v), ok
}
// MarshalJSON implements [jsonv1.Marshaler].
func (m MapSlice[K, V]) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(m.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (m MapSlice[K, V]) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, m.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
// It should only be called on an uninitialized Map.
func (m *MapSlice[K, V]) UnmarshalJSON(b []byte) error {
if m.ж != nil {
return errors.New("already initialized")
}
return jsonv1.Unmarshal(b, &m.ж)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
// It should only be called on an uninitialized MapSlice.
func (m *MapSlice[K, V]) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if m.ж != nil {
return errors.New("already initialized")
}
return jsonv2.UnmarshalDecode(dec, &m.ж)
}
// AsMap returns a shallow-clone of the underlying map.
//
// If V is a pointer type, it is the caller's responsibility to make sure the
// values are immutable. The map and slices are cloned, but the values are not.
func (m MapSlice[K, V]) AsMap() map[K][]V {
if m.ж == nil {
return nil
}
out := maps.Clone(m.ж)
for k, v := range out {
out[k] = slices.Clone(v)
}
return out
}
// All returns an iterator iterating over the keys and values of m.
func (m MapSlice[K, V]) All() iter.Seq2[K, Slice[V]] {
return func(yield func(K, Slice[V]) bool) {
for k, v := range m.ж {
if !yield(k, SliceOf(v)) {
return
}
}
}
}
// Map provides a read-only view of a map. It is the caller's responsibility to
// make sure V is immutable.
type Map[K comparable, V any] struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж map[K]V
}
// MapOf returns a view over m. It is the caller's responsibility to make sure V
// is immutable.
func MapOf[K comparable, V any](m map[K]V) Map[K, V] {
return Map[K, V]{m}
}
// Has reports whether k has an entry in the map.
// Deprecated: use Contains instead.
func (m Map[K, V]) Has(k K) bool {
return m.Contains(k)
}
// Contains reports whether k has an entry in the map.
func (m Map[K, V]) Contains(k K) bool {
_, ok := m.ж[k]
return ok
}
// IsNil reports whether the underlying map is nil.
func (m Map[K, V]) IsNil() bool {
return m.ж == nil
}
// Len returns the number of elements in the map.
func (m Map[K, V]) Len() int { return len(m.ж) }
// Get returns the element with key k.
func (m Map[K, V]) Get(k K) V {
return m.ж[k]
}
// GetOk returns the element with key k and a bool representing whether the key
// is in map.
func (m Map[K, V]) GetOk(k K) (V, bool) {
v, ok := m.ж[k]
return v, ok
}
// MarshalJSON implements [jsonv1.Marshaler].
func (m Map[K, V]) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(m.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (m Map[K, V]) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, m.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
// It should only be called on an uninitialized Map.
func (m *Map[K, V]) UnmarshalJSON(b []byte) error {
if m.ж != nil {
return errors.New("already initialized")
}
return jsonv1.Unmarshal(b, &m.ж)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
// It must only be called on an uninitialized Map.
func (m *Map[K, V]) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if m.ж != nil {
return errors.New("already initialized")
}
return jsonv2.UnmarshalDecode(dec, &m.ж)
}
// AsMap returns a shallow-clone of the underlying map.
// If V is a pointer type, it is the caller's responsibility to make sure
// the values are immutable.
func (m Map[K, V]) AsMap() map[K]V {
if m.ж == nil {
return nil
}
return maps.Clone(m.ж)
}
// NOTE: the type constraints for MapViewsEqual and MapViewsEqualFunc are based
// on those for maps.Equal and maps.EqualFunc.
// MapViewsEqual returns whether the two given [Map]s are equal. Both K and V
// must be comparable; if V is non-comparable, use [MapViewsEqualFunc] instead.
func MapViewsEqual[K, V comparable](a, b Map[K, V]) bool {
if a.Len() != b.Len() || a.IsNil() != b.IsNil() {
return false
}
if a.IsNil() {
return true // both nil; can exit early
}
for k, v := range a.All() {
bv, ok := b.GetOk(k)
if !ok || v != bv {
return false
}
}
return true
}
// MapViewsEqualFunc returns whether the two given [Map]s are equal, using the
// given function to compare two values.
func MapViewsEqualFunc[K comparable, V1, V2 any](a Map[K, V1], b Map[K, V2], eq func(V1, V2) bool) bool {
if a.Len() != b.Len() || a.IsNil() != b.IsNil() {
return false
}
if a.IsNil() {
return true // both nil; can exit early
}
for k, v := range a.All() {
bv, ok := b.GetOk(k)
if !ok || !eq(v, bv) {
return false
}
}
return true
}
// MapRangeFn is the func called from a Map.Range call.
// Implementations should return false to stop range.
type MapRangeFn[K comparable, V any] func(k K, v V) (cont bool)
// All returns an iterator iterating over the keys
// and values of m.
func (m Map[K, V]) All() iter.Seq2[K, V] {
return func(yield func(K, V) bool) {
for k, v := range m.ж {
if !yield(k, v) {
return
}
}
}
}
// MapFnOf returns a MapFn for m.
func MapFnOf[K comparable, T any, V any](m map[K]T, f func(T) V) MapFn[K, T, V] {
return MapFn[K, T, V]{
ж: m,
wrapv: f,
}
}
// MapFn is like Map but with a func to convert values from T to V.
// It is used to provide map of slices and views.
type MapFn[K comparable, T any, V any] struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж map[K]T
wrapv func(T) V
}
// Has reports whether k has an entry in the map.
// Deprecated: use Contains instead.
func (m MapFn[K, T, V]) Has(k K) bool {
return m.Contains(k)
}
// Contains reports whether k has an entry in the map.
func (m MapFn[K, T, V]) Contains(k K) bool {
_, ok := m.ж[k]
return ok
}
// Get returns the element with key k.
func (m MapFn[K, T, V]) Get(k K) V {
return m.wrapv(m.ж[k])
}
// IsNil reports whether the underlying map is nil.
func (m MapFn[K, T, V]) IsNil() bool {
return m.ж == nil
}
// Len returns the number of elements in the map.
func (m MapFn[K, T, V]) Len() int { return len(m.ж) }
// GetOk returns the element with key k and a bool representing whether the key
// is in map.
func (m MapFn[K, T, V]) GetOk(k K) (V, bool) {
v, ok := m.ж[k]
return m.wrapv(v), ok
}
// All returns an iterator iterating over the keys and value views of m.
func (m MapFn[K, T, V]) All() iter.Seq2[K, V] {
return func(yield func(K, V) bool) {
for k, v := range m.ж {
if !yield(k, m.wrapv(v)) {
return
}
}
}
}
// ValuePointer provides a read-only view of a pointer to a value type,
// such as a primitive type or an immutable struct. Its Value and ValueOk
// methods return a stack-allocated shallow copy of the underlying value.
// It is the caller's responsibility to ensure that T
// is free from memory aliasing/mutation concerns.
type ValuePointer[T any] struct {
// ж is the underlying value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *T
}
// Valid reports whether the underlying pointer is non-nil.
func (p ValuePointer[T]) Valid() bool {
return p.ж != nil
}
// Get returns a shallow copy of the value if the underlying pointer is non-nil.
// Otherwise, it returns a zero value.
func (p ValuePointer[T]) Get() T {
v, _ := p.GetOk()
return v
}
// GetOk returns a shallow copy of the underlying value and true if the underlying
// pointer is non-nil. Otherwise, it returns a zero value and false.
func (p ValuePointer[T]) GetOk() (value T, ok bool) {
if p.ж == nil {
return value, false // value holds a zero value
}
return *p.ж, true
}
// GetOr returns a shallow copy of the underlying value if it is non-nil.
// Otherwise, it returns the provided default value.
func (p ValuePointer[T]) GetOr(def T) T {
if p.ж == nil {
return def
}
return *p.ж
}
// Clone returns a shallow copy of the underlying value.
func (p ValuePointer[T]) Clone() *T {
if p.ж == nil {
return nil
}
return new(*p.ж)
}
// String implements [fmt.Stringer].
func (p ValuePointer[T]) String() string {
if p.ж == nil {
return "nil"
}
return fmt.Sprint(p.ж)
}
// ValuePointerOf returns an immutable view of a pointer to an immutable value.
// It is the caller's responsibility to ensure that T
// is free from memory aliasing/mutation concerns.
func ValuePointerOf[T any](v *T) ValuePointer[T] {
return ValuePointer[T]{v}
}
// MarshalJSON implements [jsonv1.Marshaler].
func (p ValuePointer[T]) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(p.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (p ValuePointer[T]) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, p.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
// It must only be called on an uninitialized ValuePointer.
func (p *ValuePointer[T]) UnmarshalJSON(b []byte) error {
if p.ж != nil {
return errors.New("already initialized")
}
return jsonv1.Unmarshal(b, &p.ж)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
// It must only be called on an uninitialized ValuePointer.
func (p *ValuePointer[T]) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if p.ж != nil {
return errors.New("already initialized")
}
return jsonv2.UnmarshalDecode(dec, &p.ж)
}
// ContainsPointers reports whether T contains any pointers,
// either explicitly or implicitly.
// It has special handling for some types that contain pointers
// that we know are free from memory aliasing/mutation concerns.
func ContainsPointers[T any]() bool {
return containsPointers(reflect.TypeFor[T]())
}
func containsPointers(typ reflect.Type) bool {
switch typ.Kind() {
case reflect.Pointer, reflect.UnsafePointer:
return true
case reflect.Chan, reflect.Map, reflect.Slice:
return true
case reflect.Array:
return containsPointers(typ.Elem())
case reflect.Interface, reflect.Func:
return true // err on the safe side.
case reflect.Struct:
if isWellKnownImmutableStruct(typ) {
return false
}
for field := range typ.Fields() {
if containsPointers(field.Type) {
return true
}
}
}
return false
}
func isWellKnownImmutableStruct(typ reflect.Type) bool {
switch typ.String() {
case "time.Time":
// time.Time contains a pointer that does not need copying
return true
case "netip.Addr", "netip.Prefix", "netip.AddrPort":
return true
default:
return false
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package backoff provides a back-off timer type.
package backoff
import (
"context"
"math/rand/v2"
"time"
"tailscale.com/tstime"
"tailscale.com/types/logger"
)
// Backoff tracks state the history of consecutive failures and sleeps
// an increasing amount of time, up to a provided limit.
type Backoff struct {
n int // number of consecutive failures
maxBackoff time.Duration
// Name is the name of this backoff timer, for logging purposes.
name string
// logf is the function used for log messages when backing off.
logf logger.Logf
// tstime.Clock.NewTimer is used instead time.NewTimer.
Clock tstime.Clock
// LogLongerThan sets the minimum time of a single backoff interval
// before we mention it in the log.
LogLongerThan time.Duration
}
// NewBackoff returns a new Backoff timer with the provided name (for logging), logger,
// and max backoff time. By default, all failures (calls to BackOff with a non-nil err)
// are logged unless the returned Backoff.LogLongerThan is adjusted.
func NewBackoff(name string, logf logger.Logf, maxBackoff time.Duration) *Backoff {
return &Backoff{
name: name,
logf: logf,
maxBackoff: maxBackoff,
Clock: tstime.StdClock{},
}
}
// BackOff sleeps an increasing amount of time if err is non-nil while the
// context is active. It resets the backoff schedule once err is nil.
func (b *Backoff) BackOff(ctx context.Context, err error) {
if err == nil {
// No error. Reset number of consecutive failures.
b.n = 0
return
}
if ctx.Err() != nil {
// Fast path.
return
}
b.n++
// n^2 backoff timer is a little smoother than the
// common choice of 2^n.
d := min(time.Duration(b.n*b.n)*10*time.Millisecond, b.maxBackoff)
// Randomize the delay between 0.5-1.5 x msec, in order
// to prevent accidental "thundering herd" problems.
d = time.Duration(float64(d) * (rand.Float64() + 0.5))
if d >= b.LogLongerThan {
b.logf("%s: [v1] backoff: %d msec", b.name, d.Milliseconds())
}
t, tChannel := b.Clock.NewTimer(d)
select {
case <-ctx.Done():
t.Stop()
case <-tChannel:
}
}
// Reset resets the backoff schedule, equivalent to calling BackOff with a nil
// error.
func (b *Backoff) Reset() {
b.n = 0
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package bufiox provides extensions to the standard bufio package.
package bufiox
import "io"
// BufferedReader is an interface for readers that support peeking
// into an internal buffer, like [bufio.Reader].
type BufferedReader interface {
Peek(n int) ([]byte, error)
Discard(n int) (discarded int, err error)
}
// ReadFull reads exactly len(buf) bytes from r into buf, like
// [io.ReadFull], but without heap allocations. It uses Peek to
// access the buffered data directly, copies it into buf, then
// discards the consumed bytes. If an error occurs,
// discard is not called and the buffer is left unchanged.
func ReadFull(r BufferedReader, buf []byte) (int, error) {
b, err := r.Peek(len(buf))
if err != nil {
if len(b) > 0 && err == io.EOF {
err = io.ErrUnexpectedEOF
}
return 0, err
}
defer r.Discard(len(buf))
return copy(buf, b), nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package cibuild reports runtime CI information.
package cibuild
import "os"
// On reports whether the current binary is executing on a CI system.
func On() bool {
// CI env variable is set by GitHub.
// https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables
return os.Getenv("GITHUB_ACTIONS") != "" || os.Getenv("CI") == "true"
}
// OnTailscaleCI reports whether the current binary is executing on
// tailscale/tailscale's own GitHub Actions CI, as opposed to a fork's CI
// or an unrelated downstream CI (such as a Linux distribution's package
// build infrastructure) that also sets the generic CI=true environment
// variable.
func OnTailscaleCI() bool {
// GITHUB_REPOSITORY_OWNER is set by GitHub Actions to the owner of
// the repository whose workflow is running. For pull requests, this
// is the base repository's owner, not the fork's.
return os.Getenv("GITHUB_REPOSITORY_OWNER") == "tailscale"
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_clientmetrics
// Package clientmetric provides client-side metrics whose values
// get occasionally logged.
package clientmetric
import (
"bytes"
"encoding/binary"
"encoding/hex"
"expvar"
"fmt"
"io"
"sort"
"strings"
"sync"
"sync/atomic"
"time"
"tailscale.com/feature/buildfeatures"
"tailscale.com/util/set"
"tailscale.com/util/testenv"
)
var (
mu sync.Mutex // guards vars in this block
metrics = map[string]*Metric{}
numWireID int // how many wireIDs have been allocated
lastDelta time.Time // time of last call to EncodeLogTailMetricsDelta
sortedDirty bool // whether sorted needs to be rebuilt
sorted []*Metric // by name
lastLogVal []scanEntry // by Metric.regIdx
unsorted []*Metric // by Metric.regIdx
// valFreeList is a set of free contiguous int64s whose
// element addresses get assigned to Metric.v.
// Any memory address in len(valFreeList) is free for use.
// They're contiguous to reduce cache churn during diff scans.
// When out of length, a new backing array is made.
valFreeList []int64
)
// scanEntry contains the minimal data needed for quickly scanning
// memory for changed values. It's small to reduce memory pressure.
type scanEntry struct {
v *int64 // Metric.v
f func() int64 // Metric.f
lastLogged int64 // last logged value
}
// Type is a metric type: counter or gauge.
type Type uint8
const (
TypeGauge Type = iota
TypeCounter
)
// MetricUpdate requests that a client metric value be updated.
//
// This is the request body sent to /localapi/v0/upload-client-metrics.
type MetricUpdate struct {
Name string `json:"name"`
Type string `json:"type"` // one of "counter" or "gauge"
Value int `json:"value"` // amount to increment by or set
// Op indicates if Value is added to the existing metric value,
// or if the metric is set to Value.
// One of "add" or "set". If empty, defaults to "add".
Op string `json:"op"`
}
// Metric is an integer metric value that's tracked over time.
//
// It's safe for concurrent use.
type Metric struct {
v *int64 // atomic; the metric value
f func() int64 // value function (v is ignored if f is non-nil)
regIdx int // index into lastLogVal and unsorted
name string
typ Type
deltasDisabled bool
// The following fields are owned by the package-level 'mu':
// wireID is the lazily-allocated "wire ID". Until a metric is encoded
// in the logs (by EncodeLogTailMetricsDelta), it has no wireID. This
// ensures that unused metrics don't waste valuable low numbers, which
// encode with varints with fewer bytes.
wireID int
// lastNamed is the last time the name of this metric was
// written on the wire.
lastNamed time.Time
}
func (m *Metric) Name() string { return m.name }
func (m *Metric) Value() int64 {
if m.f != nil {
return m.f()
}
return atomic.LoadInt64(m.v)
}
func (m *Metric) Type() Type { return m.typ }
// DisableDeltas disables uploading of deltas for this metric (absolute values
// are always uploaded).
func (m *Metric) DisableDeltas() {
m.deltasDisabled = true
}
// Add increments m's value by n.
//
// If m is of type counter, n should not be negative.
func (m *Metric) Add(n int64) {
if m.f != nil {
panic("Add() called on metric with value function")
}
atomic.AddInt64(m.v, n)
}
// Set sets m's value to v.
//
// If m is of type counter, Set should not be used.
func (m *Metric) Set(v int64) {
if m.f != nil {
panic("Set() called on metric with value function")
}
atomic.StoreInt64(m.v, v)
}
// Publish registers a metric in the global map.
// It panics if the name is a duplicate anywhere in the process.
func (m *Metric) Publish() {
mu.Lock()
defer mu.Unlock()
if m.name == "" {
panic("unnamed Metric")
}
if _, dup := metrics[m.name]; dup {
panic("duplicate metric " + m.name)
}
metrics[m.name] = m
sortedDirty = true
if m.f == nil {
if len(valFreeList) == 0 {
valFreeList = make([]int64, 256)
}
m.v = &valFreeList[0]
valFreeList = valFreeList[1:]
}
if buildfeatures.HasLogTail {
if m.f != nil {
lastLogVal = append(lastLogVal, scanEntry{f: m.f})
} else {
lastLogVal = append(lastLogVal, scanEntry{v: m.v})
}
}
m.regIdx = len(unsorted)
unsorted = append(unsorted, m)
}
// Metrics returns the sorted list of metrics.
//
// The returned slice should not be mutated.
func Metrics() []*Metric {
mu.Lock()
defer mu.Unlock()
if sortedDirty {
sortedDirty = false
sorted = make([]*Metric, 0, len(metrics))
for _, m := range metrics {
sorted = append(sorted, m)
}
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].name < sorted[j].name
})
}
return sorted
}
// HasPublished reports whether a metric with the given name has already been
// published.
func HasPublished(name string) bool {
mu.Lock()
defer mu.Unlock()
_, ok := metrics[name]
return ok
}
// NewUnpublished initializes a new Metric without calling Publish on
// it.
func NewUnpublished(name string, typ Type) *Metric {
if i := strings.IndexFunc(name, isIllegalMetricRune); name == "" || i != -1 {
panic(fmt.Sprintf("illegal metric name %q (index %v)", name, i))
}
return &Metric{
name: name,
typ: typ,
}
}
func isIllegalMetricRune(r rune) bool {
return !(r >= 'a' && r <= 'z' ||
r >= 'A' && r <= 'Z' ||
r >= '0' && r <= '9' ||
r == '_')
}
// NewCounter returns a new metric that can only increment.
func NewCounter(name string) *Metric {
m := NewUnpublished(name, TypeCounter)
m.Publish()
return m
}
// NewGauge returns a new metric that can both increment and decrement.
func NewGauge(name string) *Metric {
m := NewUnpublished(name, TypeGauge)
m.Publish()
return m
}
// NewCounterFunc returns a counter metric that has its value determined by
// calling the provided function (calling Add() and Set() will panic). No
// locking guarantees are made for the invocation.
func NewCounterFunc(name string, f func() int64) *Metric {
m := NewUnpublished(name, TypeCounter)
m.f = f
m.Publish()
return m
}
// NewGaugeFunc returns a gauge metric that has its value determined by
// calling the provided function (calling Add() and Set() will panic). No
// locking guarantees are made for the invocation.
func NewGaugeFunc(name string, f func() int64) *Metric {
m := NewUnpublished(name, TypeGauge)
m.f = f
m.Publish()
return m
}
// AggregateCounter returns a sum of expvar counters registered with it.
type AggregateCounter struct {
mu sync.RWMutex
counters set.Set[*expvar.Int]
}
func (c *AggregateCounter) Value() int64 {
c.mu.RLock()
defer c.mu.RUnlock()
var sum int64
for cnt := range c.counters {
sum += cnt.Value()
}
return sum
}
// Register registers provided expvar counter.
// When a counter is added to the counter, it will be reset
// to start counting from 0. This is to avoid incrementing the
// counter with an unexpectedly large value.
func (c *AggregateCounter) Register(counter *expvar.Int) {
c.mu.Lock()
defer c.mu.Unlock()
// No need to do anything if it's already registered.
if c.counters.Contains(counter) {
return
}
counter.Set(0)
c.counters.Add(counter)
}
// UnregisterAll unregisters all counters resulting in it
// starting back down at zero. This is to ensure monotonicity
// and respect the semantics of the counter.
func (c *AggregateCounter) UnregisterAll() {
c.mu.Lock()
defer c.mu.Unlock()
c.counters = set.Set[*expvar.Int]{}
}
// NewAggregateCounter returns a new aggregate counter that returns
// a sum of expvar variables registered with it.
func NewAggregateCounter(name string) *AggregateCounter {
c := &AggregateCounter{counters: set.Set[*expvar.Int]{}}
NewCounterFunc(name, c.Value)
return c
}
// WritePrometheusExpositionFormat writes all client metrics to w in
// the Prometheus text-based exposition format.
//
// See https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md
func WritePrometheusExpositionFormat(w io.Writer) {
for _, m := range Metrics() {
switch m.Type() {
case TypeGauge:
fmt.Fprintf(w, "# TYPE %s gauge\n", m.Name())
case TypeCounter:
fmt.Fprintf(w, "# TYPE %s counter\n", m.Name())
}
fmt.Fprintf(w, "%s %v\n", m.Name(), m.Value())
}
}
const (
// metricLogNameFrequency is how often a metric's name=>id
// mapping is redundantly put in the logs. In other words,
// this is how far in the logs you need to fetch from a
// given point in time to recompute the metrics at that point
// in time.
metricLogNameFrequency = 4 * time.Hour
// minMetricEncodeInterval is the minimum interval that the
// metrics will be scanned for changes before being encoded
// for logtail.
minMetricEncodeInterval = 15 * time.Second
)
// EncodeLogTailMetricsDelta return an encoded string representing the metrics
// differences since the previous call.
//
// It implements the requirements of a logtail.Config.MetricsDelta
// func. Notably, its output is safe to embed in a JSON string literal
// without further escaping.
//
// The current encoding is:
// - name immediately following metric:
// 'N' + hex(varint(len(name))) + name
// - set value of a metric:
// 'S' + hex(varint(wireid)) + hex(varint(value))
// - increment a metric: (decrements if negative)
// 'I' + hex(varint(wireid)) + hex(varint(value))
func EncodeLogTailMetricsDelta() string {
if !buildfeatures.HasLogTail {
return ""
}
mu.Lock()
defer mu.Unlock()
now := time.Now()
if !lastDelta.IsZero() && now.Sub(lastDelta) < minMetricEncodeInterval {
return ""
}
lastDelta = now
var enc *deltaEncBuf // lazy
for i, ent := range lastLogVal {
var val int64
if ent.f != nil {
val = ent.f()
} else {
val = atomic.LoadInt64(ent.v)
}
delta := val - ent.lastLogged
if delta == 0 {
continue
}
lastLogVal[i].lastLogged = val
m := unsorted[i]
if enc == nil {
enc = deltaPool.Get().(*deltaEncBuf)
enc.buf.Reset()
}
if m.wireID == 0 {
numWireID++
m.wireID = numWireID
}
writeValue := m.deltasDisabled
if m.lastNamed.IsZero() || now.Sub(m.lastNamed) > metricLogNameFrequency {
enc.writeName(m.Name(), m.Type())
m.lastNamed = now
writeValue = true
}
if writeValue {
enc.writeValue(m.wireID, val)
} else {
enc.writeDelta(m.wireID, delta)
}
}
if enc == nil {
return ""
}
defer deltaPool.Put(enc)
return enc.buf.String()
}
var deltaPool = &sync.Pool{
New: func() any {
return new(deltaEncBuf)
},
}
// deltaEncBuf encodes metrics per the format described
// on EncodeLogTailMetricsDelta above.
type deltaEncBuf struct {
buf bytes.Buffer
scratch [binary.MaxVarintLen64]byte
}
// writeName writes a "name" (N) record to the buffer, which notes
// that the immediately following record's wireID has the provided
// name.
func (b *deltaEncBuf) writeName(name string, typ Type) {
var namePrefix string
if typ == TypeGauge {
// Add the gauge_ prefix so that tsweb knows that this is a gauge metric
// when generating the Prometheus version.
namePrefix = "gauge_"
}
b.buf.WriteByte('N')
b.writeHexVarint(int64(len(namePrefix) + len(name)))
b.buf.WriteString(namePrefix)
b.buf.WriteString(name)
}
// writeDelta writes a "set" (S) record to the buffer, noting that the
// metric with the given wireID now has value v.
func (b *deltaEncBuf) writeValue(wireID int, v int64) {
b.buf.WriteByte('S')
b.writeHexVarint(int64(wireID))
b.writeHexVarint(v)
}
// writeDelta writes an "increment" (I) delta value record to the
// buffer, noting that the metric with the given wireID now has a
// value that's v larger (or smaller if v is negative).
func (b *deltaEncBuf) writeDelta(wireID int, v int64) {
b.buf.WriteByte('I')
b.writeHexVarint(int64(wireID))
b.writeHexVarint(v)
}
// writeHexVarint writes v to the buffer as a hex-encoded varint.
func (b *deltaEncBuf) writeHexVarint(v int64) {
n := binary.PutVarint(b.scratch[:], v)
hexLen := n * 2
oldLen := b.buf.Len()
b.buf.Grow(hexLen)
hexBuf := b.buf.Bytes()[oldLen : oldLen+hexLen]
hex.Encode(hexBuf, b.scratch[:n])
b.buf.Write(hexBuf)
}
// ResetForTest resets all client metric values to zero.
// It panics if not in a test or if called from a parallel test.
func ResetForTest(t testenv.TB) {
if !testenv.InTest() {
panic("clientmetric.ResetForTest called outside a test")
}
if testenv.InParallelTest(t) {
panic("clientmetric.ResetForTest called from a parallel test")
}
mu.Lock()
defer mu.Unlock()
for _, m := range metrics {
if m.v != nil {
atomic.StoreInt64(m.v, 0)
}
}
}
var TestHooks testHooks
type testHooks struct{}
func (testHooks) ResetLastDelta() {
lastDelta = time.Time{}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package cloudenv reports which known cloud environment we're running in.
package cloudenv
import (
"context"
"encoding/json"
"log"
"math/rand/v2"
"net"
"net/http"
"os"
"runtime"
"strings"
"time"
"tailscale.com/feature/buildfeatures"
"tailscale.com/syncs"
"tailscale.com/types/lazy"
)
// CommonNonRoutableMetadataIP is the IP address of the metadata server
// on Amazon EC2, Google Compute Engine, and Azure. It's not routable.
// (169.254.0.0/16 is a Link Local range: RFC 3927)
const CommonNonRoutableMetadataIP = "169.254.169.254"
// GoogleMetadataAndDNSIP is the metadata IP used by Google Cloud.
// It's also the *.internal DNS server, and proxies to 8.8.8.8.
const GoogleMetadataAndDNSIP = "169.254.169.254"
// AWSResolverIP is the IP address of the AWS DNS server.
// See https://docs.aws.amazon.com/vpc/latest/userguide/vpc-dns.html
const AWSResolverIP = "169.254.169.253"
// AzureResolverIP is Azure's DNS resolver IP.
// See https://docs.microsoft.com/en-us/azure/virtual-network/what-is-ip-address-168-63-129-16
const AzureResolverIP = "168.63.129.16"
// Cloud is a recognize cloud environment with properties that
// Tailscale can specialize for in places.
type Cloud string
const (
AWS = Cloud("aws") // Amazon Web Services (EC2 in particular)
Azure = Cloud("azure") // Microsoft Azure
GCP = Cloud("gcp") // Google Cloud
DigitalOcean = Cloud("digitalocean") // DigitalOcean
Hetzner = Cloud("hetzner") // Hetzner Cloud
)
// ResolverIP returns the cloud host's recursive DNS server or the
// empty string if not available.
func (c Cloud) ResolverIP() string {
if !buildfeatures.HasCloud {
return ""
}
switch c {
case GCP:
return GoogleMetadataAndDNSIP
case AWS:
return AWSResolverIP
case Azure:
return AzureResolverIP
case DigitalOcean:
return getDigitalOceanResolver()
case Hetzner:
return getHetznerResolver()
}
return ""
}
var (
// https://docs.digitalocean.com/support/check-your-droplets-network-configuration/
digitalOceanResolvers = []string{"67.207.67.2", "67.207.67.3"}
digitalOceanResolver lazy.SyncValue[string]
)
func getDigitalOceanResolver() string {
// Randomly select one of the available resolvers so we don't overload
// one of them by sending all traffic there.
return digitalOceanResolver.Get(func() string {
return digitalOceanResolvers[rand.IntN(len(digitalOceanResolvers))]
})
}
var (
// https://docs.hetzner.com/robot/dedicated-server/general-information/recursive-name-servers/
// IPv6 resolvers also exist: 2a01:4ff:ff00::add:1, 2a01:4ff:ff00::add:2.
hetznerResolvers = []string{"185.12.64.1", "185.12.64.2"}
hetznerResolver lazy.SyncValue[string]
)
func getHetznerResolver() string {
// Randomly select one of the available resolvers so we don't overload
// one of them by sending all traffic there.
return hetznerResolver.Get(func() string {
return hetznerResolvers[rand.IntN(len(hetznerResolvers))]
})
}
// HasInternalTLD reports whether c is a cloud environment
// whose ResolverIP serves *.internal records.
func (c Cloud) HasInternalTLD() bool {
switch c {
case GCP, AWS:
return true
}
return false
}
var cloudAtomic syncs.AtomicValue[Cloud]
// Get returns the current cloud, or the empty string if unknown.
func Get() Cloud {
if !buildfeatures.HasCloud {
return ""
}
if c, ok := cloudAtomic.LoadOk(); ok {
return c
}
c := getCloud()
cloudAtomic.Store(c) // even if empty
return c
}
func readFileTrimmed(name string) string {
v, _ := os.ReadFile(name)
return strings.TrimSpace(string(v))
}
func getCloud() Cloud {
var hitMetadata bool
switch runtime.GOOS {
case "android", "ios", "darwin":
// Assume these aren't running on a cloud.
return ""
case "linux":
biosVendor := readFileTrimmed("/sys/class/dmi/id/bios_vendor")
if biosVendor == "Amazon EC2" || strings.HasSuffix(biosVendor, ".amazon") {
return AWS
}
sysVendor := readFileTrimmed("/sys/class/dmi/id/sys_vendor")
if sysVendor == "DigitalOcean" {
return DigitalOcean
}
if sysVendor == "Hetzner" {
return Hetzner
}
// TODO(andrew): "Vultr" is also valid if we need it
prod := readFileTrimmed("/sys/class/dmi/id/product_name")
if prod == "Google Compute Engine" {
return GCP
}
if prod == "Google" { // old GCP VMs, it seems
hitMetadata = true
}
if prod == "Virtual Machine" || biosVendor == "Microsoft Corporation" {
// Azure, or maybe all Hyper-V?
hitMetadata = true
}
default:
// TODO(bradfitz): use Win32_SystemEnclosure from WMI or something on
// Windows to see if it's a physical machine and skip the cloud check
// early. Otherwise use similar clues as Linux about whether we should
// burn up to 2 seconds waiting for a metadata server that might not be
// there. And for BSDs, look where the /sys stuff is.
return ""
}
if !hitMetadata {
return ""
}
const maxWait = 2 * time.Second
tr := &http.Transport{
DisableKeepAlives: true,
Dial: (&net.Dialer{
Timeout: maxWait,
}).Dial,
}
ctx, cancel := context.WithTimeout(context.Background(), maxWait)
defer cancel()
// We want to hit CommonNonRoutableMetadataIP to see if we're on AWS, GCP,
// or Azure. All three (and many others) use the same metadata IP.
//
// But to avoid triggering the AWS CloudWatch "MetadataNoToken" metric (for which
// there might be an alert registered?), make our initial request be a token
// request. This only works on AWS, but the failing HTTP response on other clouds gives
// us enough clues about which cloud we're on.
req, err := http.NewRequestWithContext(ctx, "PUT", "http://"+CommonNonRoutableMetadataIP+"/latest/api/token", strings.NewReader(""))
if err != nil {
log.Printf("cloudenv: [unexpected] error creating request: %v", err)
return ""
}
req.Header.Set("X-Aws-Ec2-Metadata-Token-Ttl-Seconds", "5")
res, err := tr.RoundTrip(req)
if err != nil {
return ""
}
res.Body.Close()
if res.Header.Get("Metadata-Flavor") == "Google" {
return GCP
}
server := res.Header.Get("Server")
if server == "EC2ws" {
return AWS
}
if strings.HasPrefix(server, "Microsoft") {
// e.g. "Microsoft-IIS/10.0"
req, _ := http.NewRequestWithContext(ctx, "GET", "http://"+CommonNonRoutableMetadataIP+"/metadata/instance/compute?api-version=2021-02-01", nil)
req.Header.Set("Metadata", "true")
res, err := tr.RoundTrip(req)
if err != nil {
return ""
}
defer res.Body.Close()
var meta struct {
AzEnvironment string `json:"azEnvironment"`
}
if err := json.NewDecoder(res.Body).Decode(&meta); err != nil {
return ""
}
if strings.HasPrefix(meta.AzEnvironment, "Azure") {
return Azure
}
return ""
}
// TODO: more, as needed.
return ""
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package cmpver implements a variant of debian version number
// comparison.
//
// A version is a string consisting of alternating non-numeric and
// numeric fields. When comparing two versions, each one is broken
// down into its respective fields, and the fields are compared
// pairwise. The comparison is lexicographic for non-numeric fields,
// numeric for numeric fields. The first non-equal field pair
// determines the ordering of the two versions.
//
// This comparison scheme is a simplified version of Debian's version
// number comparisons. Debian differs in a few details of
// lexicographical field comparison, where certain characters have
// special meaning and ordering. We don't need that, because Tailscale
// version numbers don't need it.
package cmpver
import (
"fmt"
"strconv"
"strings"
)
// Less reports whether v1 is less than v2.
//
// Note that "12" is less than "12.0".
func Less(v1, v2 string) bool {
return Compare(v1, v2) < 0
}
// LessEq reports whether v1 is less than or equal to v2.
//
// Note that "12" is less than "12.0".
func LessEq(v1, v2 string) bool {
return Compare(v1, v2) <= 0
}
func isnum(r rune) bool {
return r >= '0' && r <= '9'
}
func notnum(r rune) bool {
return !isnum(r)
}
// Compare returns an integer comparing two strings as version numbers.
// The result will be -1, 0, or 1 representing the sign of v1 - v2:
//
// Compare(v1, v2) < 0 if v1 < v2
// == 0 if v1 == v2
// > 0 if v1 > v2
func Compare(v1, v2 string) int {
var (
f1, f2 string
n1, n2 uint64
err error
)
for v1 != "" || v2 != "" {
// Compare the non-numeric character run lexicographically.
f1, v1 = splitPrefixFunc(v1, notnum)
f2, v2 = splitPrefixFunc(v2, notnum)
if res := strings.Compare(f1, f2); res != 0 {
return res
}
// Compare the numeric character run numerically.
f1, v1 = splitPrefixFunc(v1, isnum)
f2, v2 = splitPrefixFunc(v2, isnum)
// ParseUint refuses to parse empty strings, which would only
// happen if we reached end-of-string. We follow the Debian
// convention that empty strings mean zero, because
// empirically that produces reasonable-feeling comparison
// behavior.
n1 = 0
if f1 != "" {
n1, err = strconv.ParseUint(f1, 10, 64)
if err != nil {
panic(fmt.Sprintf("all-number string %q didn't parse as string: %s", f1, err))
}
}
n2 = 0
if f2 != "" {
n2, err = strconv.ParseUint(f2, 10, 64)
if err != nil {
panic(fmt.Sprintf("all-number string %q didn't parse as string: %s", f2, err))
}
}
switch {
case n1 == n2:
case n1 < n2:
return -1
case n1 > n2:
return 1
}
}
// Only way to reach here is if v1 and v2 run out of fields
// simultaneously - i.e. exactly equal versions.
return 0
}
// splitPrefixFunc splits s at the first rune where f(rune) is false.
func splitPrefixFunc(s string, f func(rune) bool) (string, string) {
for i, r := range s {
if !f(r) {
return s[:i], s[i:]
}
}
return s, s[:0]
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package cobs implements Consistent Overhead Byte Stuffing (COBS),
// a technique for reliable packet framing over serial byte streams.
//
// COBS transforms any arbitrary payload such that the zero byte is guaranteed
// to never appear in the encoded output. A zero byte can then be appended
// as an unambiguous frame delimiter without colliding with the payload data.
//
// The encoding has the following properties:
//
// - Lossless and reversible. Decoding recovers the original payload exactly.
//
// - Non-zero payload bytes are never modified. Only zero bytes are removed
// from the encoded form and replaced by length-prefix overhead bytes.
//
// - The zero byte is guaranteed to never appear in encoded output, so a
// trailing null terminator byte can be used to delimit frames in a byte stream.
//
// - Overhead is tightly bounded in the worst case, unlike PPP byte stuffing
// (up to 100% expansion) or HDLC bit stuffing (up to 20% expansion).
// For n > 0 payload bytes, encoded size is at most n + ⌈n/254⌉ bytes.
// An empty payload encodes to a single byte.
//
// - Longer frames add at most one overhead byte per 254 bytes of data
// (about 0.4% for large frames, rounded up to whole bytes).
// This makes maximum frame size predictable, which matters for devices with
// fixed MTUs or hard transmission-time limits.
//
// Framing convention: AppendEncode produces COBS-encoded payload only;
// callers typically append a null delimiter when writing to the wire.
// AppendDecode expects the input without the delimiter.
//
// See https://www.stuartcheshire.org/papers/COBSforToN.pdf
package cobs
import (
"bytes"
"cmp"
"encoding/binary"
"errors"
"slices"
)
// MaxEncodedLen is the longest possible length for a COBS-encoded output
// for some payload of length n. The encoded length does not include
// the trailing null terminator byte.
//
// Invariant: len(AppendEncode(nil, dec)) <= MaxEncodedLen(len(dec))
func MaxEncodedLen(n int) int {
n = max(n, 0)
return n + max(1, (n+253)/254)
}
// MinDecodedLen is the shortest possible length for a decoded payload
// from a COBS-encoded input of a length of n,
// where n does not include the trailing null terminator byte.
//
// Invariant: len(AppendDecode(nil, enc)) >= MinDecodedLen(len(enc))
func MinDecodedLen(n int) int {
n = max(n, 0)
return n - (n+254)/255
}
// AppendEncode appends the encoded bytes of src to the end of dst.
// The COBS-encoded output never contains null bytes and
// therefore also lacks a trailing null terminator byte;
// use [AppendNull] to append the trailing null terminator byte if needed.
//
// The src and dst buffers may exactly overlap. For example, it is valid to do:
//
// b = AppendEncode(b[:0], b)
func AppendEncode(dst, src []byte) []byte {
// Forward encoding is only safe if dst and src do not overlap.
// Reverse encoding is always safe, but is ~2-8x slower
// (but can avoid the need for allocating an intermediate buffer).
if cap(dst) == len(dst) || cap(src) == 0 || &dst[:cap(dst)][len(dst)] != &src[:cap(src)][0] {
return appendEncodeForward(dst, src)
} else {
return appendEncodeReverse(dst, src)
}
}
func appendEncodeForward(dst, src []byte) []byte {
dst = slices.Grow(dst, MaxEncodedLen(len(src)))
for {
numNonZero := bytes.IndexByte(src, '\x00')
if numNonZero < 0 {
numNonZero = len(src)
}
// As a space optimization, the last empty block may be dropped
// if it follows a full block.
elideLastEmpty := numNonZero > 0 && numNonZero%254 == 0 && numNonZero == len(src)
// Emit zero or more full blocks, followed by a non-full block.
for ; numNonZero >= 254; numNonZero -= 254 {
dst = append(append(dst, 254+1), src[:254]...)
src = src[254:]
}
if !elideLastEmpty {
dst = append(append(dst, byte(numNonZero+1)), src[:numNonZero]...)
src = src[numNonZero:]
}
// Finished block, check termination condition and strip zero
// that is implicitly encoded by previous non-full block.
if len(src) == 0 {
break
}
if len(src) > 0 && src[0] == '\x00' {
src = src[1:]
}
// As a runtime optimization, specially handle many consecutive zeros.
for len(src) >= 8 && binary.LittleEndian.Uint64(src) == 0x0000000000000000 {
dst = binary.LittleEndian.AppendUint64(dst, 0x0101010101010101)
src = src[8:]
}
}
return dst
}
func appendEncodeReverse(dst, src []byte) []byte {
// In order to handle appending into an overlapping buffer,
// first count the number of overhead bytes,
// and then encode the input in reverse.
// Extend the dst for the number of overhead bytes.
numPrefix := len(dst)
numOverhead := numOverhead(src)
dst = slices.Grow(dst, len(src)+numOverhead)
dst = dst[:len(dst)+len(src)+numOverhead]
// Process the buffers in reverse order.
dstIdx := len(dst)
srcIdx := len(src)
for dstIdx > numPrefix {
// As a runtime optimization, specially handle many consecutive zeros.
for srcIdx >= 8 && binary.LittleEndian.Uint64(src[srcIdx-8:]) == 0x0000000000000000 {
binary.LittleEndian.PutUint64(dst[dstIdx-8:], 0x0101010101010101)
dstIdx -= 8
srcIdx -= 8
}
// Emit one or more blocks in reverse.
numNonZero := srcIdx - (bytes.LastIndexByte(src[:srcIdx], '\x00') + len("\x00"))
hasZero := srcIdx-numNonZero > 0 && src[srcIdx-numNonZero-1] == '\x00'
for {
copyLen := 0
if numNonZero > 0 {
copyLen = cmp.Or(numNonZero%254, 254)
}
// Since a full block lacks a subsequent zero,
// we may need to manually inject an empty block if
// the following source byte is a zero byte.
if copyLen == 254 && srcIdx < len(src) && src[srcIdx] == '\x00' {
dst[dstIdx-1] = 0x01
dstIdx--
}
copy(dst[dstIdx-copyLen:dstIdx], src[srcIdx-copyLen:srcIdx])
dstIdx -= copyLen
srcIdx -= copyLen
numNonZero -= copyLen
dst[dstIdx-1] = byte(copyLen + 1)
dstIdx -= 1
if numNonZero == 0 {
break
}
}
if hasZero {
srcIdx--
}
}
return dst
}
// numOverhead computes the exact number of overhead bytes
// needed to be added to COBS-encode the src.
// It assumes the optimization where a last empty block is elided
// if it immediately follows a final full block of non-zero bytes.
// The count does not include any trailing null terminator byte.
func numOverhead(src []byte) (n int) {
n++ // mandatory leading overhead byte
for len(src) > 0 {
// Trim leading zeros as they take up no overhead.
for len(src) >= 8 && binary.LittleEndian.Uint64(src) == 0 {
src = src[8:]
}
for len(src) > 0 && src[0] == 0 {
src = src[1:]
}
// Long runs of non-zero bytes require an overhead byte.
numNonZero := bytes.IndexByte(src, '\x00')
if numNonZero < 0 {
numNonZero = len(src)
}
n += numNonZero / 254 // each full group of 254 needs an overhead byte (except last)
src = src[numNonZero:]
if numNonZero > 0 && numNonZero%254 == 0 && len(src) == 0 {
n-- // exact final group of 254 does not need extra overhead byte
}
}
return n
}
var errUnexpectedEOF = errors.New("cobs: unexpected truncation")
var errUnexpectedNull = errors.New("cobs: unexpected null byte")
// AppendDecode appends the decoded bytes of src to the end of dst.
// The COBS-encoded src must not contain the trailing null terminator byte;
// use [TrimNull] to remove the trailing null terminator byte if needed.
// It reports an error if the src contains invalid COBS.
//
// The src and dst buffers may exactly overlap. For example, it is valid to do:
//
// b, _ = AppendDecode(b[:0], b)
func AppendDecode(dst, src []byte) ([]byte, error) {
// Unlike AppendEncode, decoding in a forward direction is still safe
// when dst overlaps with src since the output is guaranteed to always
// be smaller than the input. Thus, the dst pointer will never run past
// src pointer and corrupt the input.
dst = slices.Grow(dst, len(src))
if len(src) == 0 {
return dst, errUnexpectedEOF
}
for len(src) > 0 {
// Performance optimization for many zeros.
// Leave at least one byte afterwards since the following logic
// expects to find another overhead byte.
// Also, the very last overhead byte does not emit a zero.
for len(src) > 8 && binary.LittleEndian.Uint64(src) == 0x0101010101010101 {
dst = binary.LittleEndian.AppendUint64(dst, 0x0000000000000000)
src = src[8:]
}
switch n := src[0]; {
case int(n) > len(src):
return dst, errUnexpectedEOF
case n == '\x00' || bytes.IndexByte(src[1:n], '\x00') >= 0:
return dst, errUnexpectedNull
default:
dst = append(dst, src[1:n]...)
if n-1 < 254 {
dst = append(dst, 0)
}
src = src[n:]
}
}
return TrimNull(dst), nil // trim implicit trailing null byte
}
// AppendNull appends a trailing null terminator byte if it does not already exist.
func AppendNull(dst []byte) []byte {
if len(dst) > 0 && dst[len(dst)-len("\x00")] == '\x00' {
return dst
}
return append(dst, '\x00')
}
// TrimNull removes a trailing null terminator byte if it exists.
func TrimNull(dst []byte) []byte {
if len(dst) > 0 && dst[len(dst)-len("\x00")] == '\x00' {
return dst[:len(dst)-len("\x00")]
}
return dst
}
// FrameLen reports the length of a COBS-encoded frame at the start of b
// by searching for the next trailing null terminator.
// The reported length includes the null terminator.
// If the null terminator could not be found, then it reports -1.
func FrameLen(b []byte) int {
if i := bytes.IndexByte(b, '\x00'); i >= 0 {
return i + len("\x00")
}
return -1
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package ctxkey provides type-safe key-value pairs for use with [context.Context].
//
// Example usage:
//
// // Create a context key.
// var TimeoutKey = ctxkey.New("mapreduce.Timeout", 5*time.Second)
//
// // Store a context value.
// ctx = mapreduce.TimeoutKey.WithValue(ctx, 10*time.Second)
//
// // Load a context value.
// timeout := mapreduce.TimeoutKey.Value(ctx)
// ... // use timeout of type time.Duration
//
// This is inspired by https://go.dev/issue/49189.
package ctxkey
import (
"context"
"fmt"
"reflect"
)
// Key is a generic key type associated with a specific value type.
//
// A zero Key is valid where the Value type itself is used as the context key.
// This pattern should only be used with locally declared Go types,
// otherwise different packages risk producing key conflicts.
//
// Example usage:
//
// type peerInfo struct { ... } // peerInfo is a locally declared type
// var peerInfoKey ctxkey.Key[peerInfo]
// ctx = peerInfoKey.WithValue(ctx, info) // store a context value
// info = peerInfoKey.Value(ctx) // load a context value
type Key[Value any] struct {
name *stringer[string]
defVal *Value
}
// New constructs a new context key with an associated value type
// where the default value for an unpopulated value is the provided value.
//
// The provided name is an arbitrary name only used for human debugging.
// As a convention, it is recommended that the name be the dot-delimited
// combination of the package name of the caller with the variable name.
// If the name is not provided, then the name of the Value type is used.
// Every key is unique, even if provided the same name.
//
// Example usage:
//
// package mapreduce
// var NumWorkersKey = ctxkey.New("mapreduce.NumWorkers", runtime.NumCPU())
func New[Value any](name string, defaultValue Value) Key[Value] {
// Allocate a new stringer to ensure that every invocation of New
// creates a universally unique context key even for the same name
// since newly allocated pointers are globally unique within a process.
key := Key[Value]{name: new(stringer[string])}
if name == "" {
name = reflect.TypeFor[Value]().String()
}
key.name.v = name
if v := reflect.ValueOf(defaultValue); v.IsValid() && !v.IsZero() {
key.defVal = &defaultValue
}
return key
}
// contextKey returns the context key to use.
func (key Key[Value]) contextKey() any {
if key.name == nil {
// Use the reflect.Type of the Value (implies key not created by New).
return reflect.TypeFor[Value]()
} else {
// Use the name pointer directly (implies key created by New).
return key.name
}
}
// WithValue returns a copy of parent in which the value associated with key is val.
//
// It is a type-safe equivalent of [context.WithValue].
func (key Key[Value]) WithValue(parent context.Context, val Value) context.Context {
return context.WithValue(parent, key.contextKey(), stringer[Value]{val})
}
// ValueOk returns the value in the context associated with this key
// and also reports whether it was present.
// If the value is not present, it returns the default value.
func (key Key[Value]) ValueOk(ctx context.Context) (v Value, ok bool) {
vv, ok := ctx.Value(key.contextKey()).(stringer[Value])
if !ok && key.defVal != nil {
vv.v = *key.defVal
}
return vv.v, ok
}
// Value returns the value in the context associated with this key.
// If the value is not present, it returns the default value.
func (key Key[Value]) Value(ctx context.Context) (v Value) {
v, _ = key.ValueOk(ctx)
return v
}
// Has reports whether the context has a value for this key.
func (key Key[Value]) Has(ctx context.Context) (ok bool) {
_, ok = key.ValueOk(ctx)
return ok
}
// String returns the name of the key.
func (key Key[Value]) String() string {
if key.name == nil {
return reflect.TypeFor[Value]().String()
}
return key.name.String()
}
// stringer implements [fmt.Stringer] on a generic T.
//
// This assists in debugging such that printing a context prints key and value.
// Note that the [context] package lacks a dependency on [reflect],
// so it cannot print arbitrary values. By implementing [fmt.Stringer],
// we functionally teach a context how to print itself.
//
// Wrapping values within a struct has an added bonus that interface kinds
// are properly handled. Without wrapping, we would be unable to distinguish
// between a nil value that was explicitly set or not.
// However, the presence of a stringer indicates an explicit nil value.
type stringer[T any] struct{ v T }
func (v stringer[T]) String() string { return fmt.Sprint(v.v) }
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package deephash hashes a Go value recursively, in a predictable order,
// without looping. The hash is only valid within the lifetime of a program.
// Users should not store the hash on disk or send it over the network.
// The hash is sufficiently strong and unique such that
// Hash(&x) == Hash(&y) is an appropriate replacement for x == y.
//
// The definition of equality is identical to reflect.DeepEqual except:
// - Floating-point values are compared based on the raw bits,
// which means that NaNs (with the same bit pattern) are treated as equal.
// - time.Time are compared based on whether they are the same instant in time
// and also in the same zone offset. Monotonic measurements and zone names
// are ignored as part of the hash.
// - netip.Addr are compared based on a shallow comparison of the struct.
//
// WARNING: This package, like most of the tailscale.com Go module,
// should be considered Tailscale-internal; we make no API promises.
//
// # Cycle detection
//
// This package correctly handles cycles in the value graph,
// but in a way that is potentially pathological in some situations.
//
// The algorithm for cycle detection operates by
// pushing a pointer onto a stack whenever deephash is visiting a pointer and
// popping the pointer from the stack after deephash is leaving the pointer.
// Before visiting a new pointer, deephash checks whether it has already been
// visited on the pointer stack. If so, it hashes the index of the pointer
// on the stack and avoids visiting the pointer.
//
// This algorithm is guaranteed to detect cycles, but may expand pointers
// more often than a potential alternate algorithm that remembers all pointers
// ever visited in a map. The current algorithm uses O(D) memory, where D
// is the maximum depth of the recursion, while the alternate algorithm
// would use O(P) memory where P is all pointers ever seen, which can be a lot,
// and most of which may have nothing to do with cycles.
// Also, the alternate algorithm has to deal with challenges of producing
// deterministic results when pointers are visited in non-deterministic ways
// such as when iterating through a Go map. The stack-based algorithm avoids
// this challenge since the stack is always deterministic regardless of
// non-deterministic iteration order of Go maps.
//
// To concretely see how this algorithm can be pathological,
// consider the following data structure:
//
// var big *Item = ... // some large data structure that is slow to hash
// var manyBig []*Item
// for i := range 1000 {
// manyBig = append(manyBig, &big)
// }
// deephash.Hash(manyBig)
//
// Here, the manyBig data structure is not even cyclic.
// We have the same big *Item being stored multiple times in a []*Item.
// When deephash hashes []*Item, it hashes each individual *Item
// not realizing that it had just done the computation earlier.
// To avoid the pathological situation, Item should implement [SelfHasher] and
// memoize attempts to hash itself.
package deephash
// TODO: Add option to teach deephash to memoize the Hash result of particular types?
import (
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"fmt"
"reflect"
"sync"
"time"
"tailscale.com/util/hashx"
"tailscale.com/util/set"
)
// There is much overlap between the theory of serialization and hashing.
// A hash (useful for determining equality) can be produced by printing a value
// and hashing the output. The format must:
// * be deterministic such that the same value hashes to the same output, and
// * be parsable such that the same value can be reproduced by the output.
//
// The logic below hashes a value by printing it to a hash.Hash.
// To be parsable, it assumes that we know the Go type of each value:
// * scalar types (e.g., bool or int32) are directly printed as their
// underlying memory representation.
// * list types (e.g., strings and slices) are prefixed by a
// fixed-width length field, followed by the contents of the list.
// * slices, arrays, and structs print each element/field consecutively.
// * interfaces print with a 1-byte prefix indicating whether it is nil.
// If non-nil, it is followed by a fixed-width field of the type index,
// followed by the format of the underlying value.
// * pointers print with a 1-byte prefix indicating whether the pointer is
// 1) nil, 2) previously seen, or 3) newly seen. Previously seen pointers are
// followed by a fixed-width field with the index of the previous pointer.
// Newly seen pointers are followed by the format of the underlying value.
// * maps print with a 1-byte prefix indicating whether the map pointer is
// 1) nil, 2) previously seen, or 3) newly seen. Previously seen pointers
// are followed by a fixed-width field of the index of the previous pointer.
// Newly seen maps are printed with a fixed-width length field, followed by
// a fixed-width field with the XOR of the hash of every map entry.
// With a sufficiently strong hash, this value is theoretically "parsable"
// by looking up the hash in a magical map that returns the set of entries
// for that given hash.
// SelfHasher is implemented by types that can compute their own hash
// by writing values through the provided [Hasher] parameter.
// Implementations must not leak the provided [Hasher].
//
// If the implementation of SelfHasher recursively calls [deephash.Hash],
// then infinite recursion is quite likely to occur.
// To avoid this, use a type definition to drop methods before calling [deephash.Hash]:
//
// func (v *MyType) Hash(h deephash.Hasher) {
// v.hashMu.Lock()
// defer v.hashMu.Unlock()
// if v.dirtyHash {
// type MyTypeWithoutMethods MyType // type define MyType to drop Hash method
// v.dirtyHash = false // clear out dirty bit to avoid hashing over it
// v.hashSum = deephash.Sum{} // clear out hashSum to avoid hashing over it
// v.hashSum = deephash.Hash((*MyTypeWithoutMethods)(v))
// }
// h.HashSum(v.hashSum)
// }
//
// In the above example, we acquire a lock since it is possible that deephash
// is called in a concurrent manner, which implies that MyType.Hash may also
// be called in a concurrent manner. Whether this lock is necessary is
// application-dependent and left as an exercise to the reader.
// Also, the example assumes that dirtyHash is set elsewhere by application
// logic whenever a mutation is made to MyType that would alter the hash.
type SelfHasher interface {
Hash(Hasher)
}
// Hasher is a value passed to [SelfHasher.Hash] that allow implementations
// to hash themselves in a structured manner.
type Hasher struct{ h *hashx.Block512 }
// HashBytes hashes a sequence of bytes b.
// The length of b is not explicitly hashed.
func (h Hasher) HashBytes(b []byte) { h.h.HashBytes(b) }
// HashString hashes the string data of s
// The length of s is not explicitly hashed.
func (h Hasher) HashString(s string) { h.h.HashString(s) }
// HashUint8 hashes a uint8.
func (h Hasher) HashUint8(n uint8) { h.h.HashUint8(n) }
// HashUint16 hashes a uint16.
func (h Hasher) HashUint16(n uint16) { h.h.HashUint16(n) }
// HashUint32 hashes a uint32.
func (h Hasher) HashUint32(n uint32) { h.h.HashUint32(n) }
// HashUint64 hashes a uint64.
func (h Hasher) HashUint64(n uint64) { h.h.HashUint64(n) }
// HashSum hashes a [Sum].
func (h Hasher) HashSum(s Sum) {
// NOTE: Avoid calling h.HashBytes since it escapes b,
// which would force s to be heap allocated.
h.h.HashUint64(binary.LittleEndian.Uint64(s.sum[0:8]))
h.h.HashUint64(binary.LittleEndian.Uint64(s.sum[8:16]))
h.h.HashUint64(binary.LittleEndian.Uint64(s.sum[16:24]))
h.h.HashUint64(binary.LittleEndian.Uint64(s.sum[24:32]))
}
// hasher is reusable state for hashing a value.
// Get one via hasherPool.
type hasher struct {
hashx.Block512
visitStack visitStack
}
var hasherPool = &sync.Pool{
New: func() any { return new(hasher) },
}
func (h *hasher) reset() {
if h.Block512.Hash == nil {
h.Block512.Hash = sha256.New()
}
h.Block512.Reset()
}
// hashType hashes a reflect.Type.
// The hash is only consistent within the lifetime of a program.
func (h *hasher) hashType(t reflect.Type) {
// This approach relies on reflect.Type always being backed by a unique
// *reflect.rtype pointer. A safer approach is to use a global sync.Map
// that maps reflect.Type to some arbitrary and unique index.
// While safer, it requires global state with memory that can never be GC'd.
rtypeAddr := reflect.ValueOf(t).Pointer() // address of *reflect.rtype
h.HashUint64(uint64(rtypeAddr))
}
func (h *hasher) sum() (s Sum) {
h.Sum(s.sum[:0])
return s
}
// Sum is an opaque checksum type that is comparable.
type Sum struct {
sum [sha256.Size]byte
}
func (s1 *Sum) xor(s2 Sum) {
for i := range sha256.Size {
s1.sum[i] ^= s2.sum[i]
}
}
func (s Sum) String() string {
// Note: if we change this, keep in sync with AppendTo
return hex.EncodeToString(s.sum[:])
}
// AppendTo appends the string encoding of this sum (as returned by the String
// method) to the provided byte slice and returns the extended buffer.
func (s Sum) AppendTo(b []byte) []byte {
// TODO: switch to upstream implementation if accepted:
// https://github.com/golang/go/issues/53693
var lb [len(s.sum) * 2]byte
hex.Encode(lb[:], s.sum[:])
return append(b, lb[:]...)
}
var (
seedOnce sync.Once
seed uint64
)
func initSeed() {
seed = uint64(time.Now().UnixNano())
}
// Hash returns the hash of v.
func Hash[T any](v *T) Sum {
h := hasherPool.Get().(*hasher)
defer hasherPool.Put(h)
h.reset()
seedOnce.Do(initSeed)
h.HashUint64(seed)
// Always treat the Hash input as if it were an interface by including
// a hash of the type. This ensures that hashing of two different types
// but with the same value structure produces different hashes.
t := reflect.TypeFor[T]()
h.hashType(t)
if v == nil {
h.HashUint8(0) // indicates nil
} else {
h.HashUint8(1) // indicates visiting pointer element
p := pointerOf(reflect.ValueOf(v))
hash := lookupTypeHasher(t)
hash(h, p)
}
return h.sum()
}
// Option is an optional argument to HasherForType.
type Option interface {
isOption()
}
type fieldFilterOpt struct {
t reflect.Type
fields set.Set[string]
includeOnMatch bool // true to include fields, false to exclude them
}
func (fieldFilterOpt) isOption() {}
func (f fieldFilterOpt) filterStructField(sf reflect.StructField) (include bool) {
if f.fields.Contains(sf.Name) {
return f.includeOnMatch
}
return !f.includeOnMatch
}
// IncludeFields returns an option that modifies the hashing for T to only
// include the named struct fields.
//
// T must be a struct type, and must match the type of the value passed to
// HasherForType.
func IncludeFields[T any](fields ...string) Option {
return newFieldFilter[T](true, fields)
}
// ExcludeFields returns an option that modifies the hashing for T to include
// all struct fields of T except those provided in fields.
//
// T must be a struct type, and must match the type of the value passed to
// HasherForType.
func ExcludeFields[T any](fields ...string) Option {
return newFieldFilter[T](false, fields)
}
func newFieldFilter[T any](include bool, fields []string) Option {
t := reflect.TypeFor[T]()
fieldSet := set.Set[string]{}
for _, f := range fields {
if _, ok := t.FieldByName(f); !ok {
panic(fmt.Sprintf("unknown field %q for type %v", f, t))
}
fieldSet.Add(f)
}
return fieldFilterOpt{t, fieldSet, include}
}
// HasherForType returns a hash that is specialized for the provided type.
//
// HasherForType panics if the opts are invalid for the provided type.
//
// Currently, at most one option can be provided (IncludeFields or
// ExcludeFields) and its type must match the type of T. Those restrictions may
// be removed in the future, along with documentation about their precedence
// when combined.
func HasherForType[T any](opts ...Option) func(*T) Sum {
seedOnce.Do(initSeed)
if len(opts) > 1 {
panic("HasherForType only accepts one optional argument") // for now
}
t := reflect.TypeFor[T]()
var hash typeHasherFunc
for _, o := range opts {
switch o := o.(type) {
default:
panic(fmt.Sprintf("unknown HasherOpt %T", o))
case fieldFilterOpt:
if t.Kind() != reflect.Struct {
panic("HasherForStructTypeWithFieldFilter requires T of kind struct")
}
if t != o.t {
panic(fmt.Sprintf("field filter for type %v does not match HasherForType type %v", o.t, t))
}
hash = makeStructHasher(t, o.filterStructField)
}
}
if hash == nil {
hash = lookupTypeHasher(t)
}
return func(v *T) (s Sum) {
// This logic is identical to Hash, but pull out a few statements.
h := hasherPool.Get().(*hasher)
defer hasherPool.Put(h)
h.reset()
h.HashUint64(seed)
h.hashType(t)
if v == nil {
h.HashUint8(0) // indicates nil
} else {
h.HashUint8(1) // indicates visiting pointer element
p := pointerOf(reflect.ValueOf(v))
hash(h, p)
}
return h.sum()
}
}
// Update sets last to the hash of v and reports whether its value changed.
func Update[T any](last *Sum, v *T) (changed bool) {
sum := Hash(v)
changed = sum != *last
if changed {
*last = sum
}
return changed
}
// typeHasherFunc hashes the value pointed at by p for a given type.
// For example, if t is a bool, then p is a *bool.
// The provided pointer must always be non-nil.
type typeHasherFunc func(h *hasher, p pointer)
var typeHasherCache sync.Map // map[reflect.Type]typeHasherFunc
func lookupTypeHasher(t reflect.Type) typeHasherFunc {
if v, ok := typeHasherCache.Load(t); ok {
return v.(typeHasherFunc)
}
hash := makeTypeHasher(t)
v, _ := typeHasherCache.LoadOrStore(t, hash)
return v.(typeHasherFunc)
}
func makeTypeHasher(t reflect.Type) typeHasherFunc {
// Types with specific hashing.
switch t {
case timeTimeType:
return hashTime
case netipAddrType:
return hashAddr
}
// Types that implement their own hashing.
if t.Kind() != reflect.Pointer && t.Kind() != reflect.Interface {
// A method can be implemented on either the value receiver or pointer receiver.
if t.Implements(selfHasherType) || reflect.PointerTo(t).Implements(selfHasherType) {
return makeSelfHasher(t)
}
}
// Types that can have their memory representation directly hashed.
if typeIsMemHashable(t) {
return makeMemHasher(t.Size())
}
switch t.Kind() {
case reflect.String:
return hashString
case reflect.Array:
return makeArrayHasher(t)
case reflect.Slice:
return makeSliceHasher(t)
case reflect.Struct:
return makeStructHasher(t, keepAllStructFields)
case reflect.Map:
return makeMapHasher(t)
case reflect.Pointer:
return makePointerHasher(t)
case reflect.Interface:
return makeInterfaceHasher(t)
default: // Func, Chan, UnsafePointer
return func(*hasher, pointer) {}
}
}
func hashTime(h *hasher, p pointer) {
// Include the zone offset (but not the name) to keep
// Hash(t1) == Hash(t2) being semantically equivalent to
// t1.Format(time.RFC3339Nano) == t2.Format(time.RFC3339Nano).
t := *p.asTime()
_, offset := t.Zone()
h.HashUint64(uint64(t.Unix()))
h.HashUint32(uint32(t.Nanosecond()))
h.HashUint32(uint32(offset))
}
func hashAddr(h *hasher, p pointer) {
// The formatting of netip.Addr covers the
// IP version, the address, and the optional zone name (for v6).
// This is equivalent to a1.MarshalBinary() == a2.MarshalBinary().
ip := *p.asAddr()
switch {
case !ip.IsValid():
h.HashUint64(0)
case ip.Is4():
b := ip.As4()
h.HashUint64(4)
h.HashUint32(binary.LittleEndian.Uint32(b[:]))
case ip.Is6():
b := ip.As16()
z := ip.Zone()
h.HashUint64(16 + uint64(len(z)))
h.HashUint64(binary.LittleEndian.Uint64(b[:8]))
h.HashUint64(binary.LittleEndian.Uint64(b[8:]))
h.HashString(z)
}
}
func makeSelfHasher(t reflect.Type) typeHasherFunc {
return func(h *hasher, p pointer) {
p.asValue(t).Interface().(SelfHasher).Hash(Hasher{&h.Block512})
}
}
func hashString(h *hasher, p pointer) {
s := *p.asString()
h.HashUint64(uint64(len(s)))
h.HashString(s)
}
func makeMemHasher(n uintptr) typeHasherFunc {
return func(h *hasher, p pointer) {
h.HashBytes(p.asMemory(n))
}
}
func makeArrayHasher(t reflect.Type) typeHasherFunc {
var once sync.Once
var hashElem typeHasherFunc
init := func() {
hashElem = lookupTypeHasher(t.Elem())
}
n := t.Len() // number of array elements
nb := t.Elem().Size() // byte size of each array element
return func(h *hasher, p pointer) {
once.Do(init)
for i := range n {
hashElem(h, p.arrayIndex(i, nb))
}
}
}
func makeSliceHasher(t reflect.Type) typeHasherFunc {
nb := t.Elem().Size() // byte size of each slice element
if typeIsMemHashable(t.Elem()) {
return func(h *hasher, p pointer) {
pa := p.sliceArray()
if pa.isNil() {
h.HashUint8(0) // indicates nil
return
}
h.HashUint8(1) // indicates visiting slice
n := p.sliceLen()
b := pa.asMemory(uintptr(n) * nb)
h.HashUint64(uint64(n))
h.HashBytes(b)
}
}
var once sync.Once
var hashElem typeHasherFunc
init := func() {
hashElem = lookupTypeHasher(t.Elem())
if typeIsRecursive(t) {
hashElemDefault := hashElem
hashElem = func(h *hasher, p pointer) {
if idx, ok := h.visitStack.seen(p.p); ok {
h.HashUint8(2) // indicates cycle
h.HashUint64(uint64(idx))
return
}
h.HashUint8(1) // indicates visiting slice element
h.visitStack.push(p.p)
defer h.visitStack.pop(p.p)
hashElemDefault(h, p)
}
}
}
return func(h *hasher, p pointer) {
pa := p.sliceArray()
if pa.isNil() {
h.HashUint8(0) // indicates nil
return
}
once.Do(init)
h.HashUint8(1) // indicates visiting slice
n := p.sliceLen()
h.HashUint64(uint64(n))
for i := range n {
pe := pa.arrayIndex(i, nb)
hashElem(h, pe)
}
}
}
func keepAllStructFields(keepField reflect.StructField) bool { return true }
func makeStructHasher(t reflect.Type, keepField func(reflect.StructField) bool) typeHasherFunc {
type fieldHasher struct {
idx int // index of field for reflect.Type.Field(n); negative if memory is directly hashable
keep bool
hash typeHasherFunc // only valid if idx is not negative
offset uintptr
size uintptr
}
var once sync.Once
var fields []fieldHasher
init := func() {
for i, numField := 0, t.NumField(); i < numField; i++ {
sf := t.Field(i)
f := fieldHasher{i, keepField(sf), nil, sf.Offset, sf.Type.Size()}
if f.keep && typeIsMemHashable(sf.Type) {
f.idx = -1
}
// Combine with previous field if both contiguous and mem-hashable.
if f.idx < 0 && len(fields) > 0 {
if last := &fields[len(fields)-1]; last.idx < 0 && last.offset+last.size == f.offset {
last.size += f.size
continue
}
}
fields = append(fields, f)
}
for i, f := range fields {
if f.idx >= 0 {
fields[i].hash = lookupTypeHasher(t.Field(f.idx).Type)
}
}
}
return func(h *hasher, p pointer) {
once.Do(init)
for _, field := range fields {
if !field.keep {
continue
}
pf := p.structField(field.idx, field.offset, field.size)
if field.idx < 0 {
h.HashBytes(pf.asMemory(field.size))
} else {
field.hash(h, pf)
}
}
}
}
func makeMapHasher(t reflect.Type) typeHasherFunc {
var once sync.Once
var hashKey, hashValue typeHasherFunc
var isRecursive bool
init := func() {
hashKey = lookupTypeHasher(t.Key())
hashValue = lookupTypeHasher(t.Elem())
isRecursive = typeIsRecursive(t)
}
return func(h *hasher, p pointer) {
v := p.asValue(t).Elem() // reflect.Map kind
if v.IsNil() {
h.HashUint8(0) // indicates nil
return
}
once.Do(init)
if isRecursive {
pm := v.UnsafePointer() // underlying pointer of map
if idx, ok := h.visitStack.seen(pm); ok {
h.HashUint8(2) // indicates cycle
h.HashUint64(uint64(idx))
return
}
h.visitStack.push(pm)
defer h.visitStack.pop(pm)
}
h.HashUint8(1) // indicates visiting map entries
h.HashUint64(uint64(v.Len()))
mh := mapHasherPool.Get().(*mapHasher)
defer mapHasherPool.Put(mh)
// Hash a map in a sort-free manner.
// It relies on a map being a an unordered set of KV entries.
// So long as we hash each KV entry together, we can XOR all the
// individual hashes to produce a unique hash for the entire map.
k := mh.valKey.get(v.Type().Key())
e := mh.valElem.get(v.Type().Elem())
mh.sum = Sum{}
mh.h.visitStack = h.visitStack // always use the parent's visit stack to avoid cycles
for iter := v.MapRange(); iter.Next(); {
k.SetIterKey(iter)
e.SetIterValue(iter)
mh.h.reset()
hashKey(&mh.h, pointerOf(k.Addr()))
hashValue(&mh.h, pointerOf(e.Addr()))
mh.sum.xor(mh.h.sum())
}
h.HashBytes(mh.sum.sum[:])
}
}
func makePointerHasher(t reflect.Type) typeHasherFunc {
var once sync.Once
var hashElem typeHasherFunc
var isRecursive bool
init := func() {
hashElem = lookupTypeHasher(t.Elem())
isRecursive = typeIsRecursive(t)
}
return func(h *hasher, p pointer) {
pe := p.pointerElem()
if pe.isNil() {
h.HashUint8(0) // indicates nil
return
}
once.Do(init)
if isRecursive {
if idx, ok := h.visitStack.seen(pe.p); ok {
h.HashUint8(2) // indicates cycle
h.HashUint64(uint64(idx))
return
}
h.visitStack.push(pe.p)
defer h.visitStack.pop(pe.p)
}
h.HashUint8(1) // indicates visiting a pointer element
hashElem(h, pe)
}
}
func makeInterfaceHasher(t reflect.Type) typeHasherFunc {
return func(h *hasher, p pointer) {
v := p.asValue(t).Elem() // reflect.Interface kind
if v.IsNil() {
h.HashUint8(0) // indicates nil
return
}
h.HashUint8(1) // indicates visiting an interface value
v = v.Elem()
t := v.Type()
h.hashType(t)
va := reflect.New(t).Elem()
va.Set(v)
hashElem := lookupTypeHasher(t)
hashElem(h, pointerOf(va.Addr()))
}
}
type mapHasher struct {
h hasher
valKey valueCache
valElem valueCache
sum Sum
}
var mapHasherPool = &sync.Pool{
New: func() any { return new(mapHasher) },
}
type valueCache map[reflect.Type]reflect.Value
// get returns an addressable reflect.Value for the given type.
func (c *valueCache) get(t reflect.Type) reflect.Value {
v, ok := (*c)[t]
if !ok {
v = reflect.New(t).Elem()
if *c == nil {
*c = make(valueCache)
}
(*c)[t] = v
}
return v
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package deephash
import (
"net/netip"
"reflect"
"time"
"unsafe"
)
// unsafePointer is an untyped pointer.
// It is the caller's responsibility to call operations on the correct type.
//
// This pointer only ever points to a small set of kinds or types:
// time.Time, netip.Addr, string, array, slice, struct, map, pointer, interface,
// or a pointer to memory that is directly hashable.
//
// Arrays are represented as pointers to the first element.
// Structs are represented as pointers to the first field.
// Slices are represented as pointers to a slice header.
// Pointers are represented as pointers to a pointer.
//
// We do not support direct operations on maps and interfaces, and instead
// rely on pointer.asValue to convert the pointer back to a reflect.Value.
// Conversion of an unsafe.Pointer to reflect.Value guarantees that the
// read-only flag in the reflect.Value is unpopulated, avoiding panics that may
// otherwise have occurred since the value was obtained from an unexported field.
type unsafePointer struct{ p unsafe.Pointer }
func unsafePointerOf(v reflect.Value) unsafePointer {
return unsafePointer{v.UnsafePointer()}
}
func (p unsafePointer) isNil() bool {
return p.p == nil
}
// pointerElem dereferences a pointer.
// p must point to a pointer.
func (p unsafePointer) pointerElem() unsafePointer {
return unsafePointer{*(*unsafe.Pointer)(p.p)}
}
// sliceLen returns the slice length.
// p must point to a slice.
func (p unsafePointer) sliceLen() int {
return (*reflect.SliceHeader)(p.p).Len
}
// sliceArray returns a pointer to the underlying slice array.
// p must point to a slice.
func (p unsafePointer) sliceArray() unsafePointer {
return unsafePointer{unsafe.Pointer((*reflect.SliceHeader)(p.p).Data)}
}
// arrayIndex returns a pointer to an element in the array.
// p must point to an array.
func (p unsafePointer) arrayIndex(index int, size uintptr) unsafePointer {
return unsafePointer{unsafe.Add(p.p, uintptr(index)*size)}
}
// structField returns a pointer to a field in a struct.
// p must pointer to a struct.
func (p unsafePointer) structField(index int, offset, size uintptr) unsafePointer {
return unsafePointer{unsafe.Add(p.p, offset)}
}
// asString casts p as a *string.
func (p unsafePointer) asString() *string {
return (*string)(p.p)
}
// asTime casts p as a *time.Time.
func (p unsafePointer) asTime() *time.Time {
return (*time.Time)(p.p)
}
// asAddr casts p as a *netip.Addr.
func (p unsafePointer) asAddr() *netip.Addr {
return (*netip.Addr)(p.p)
}
// asValue casts p as a reflect.Value containing a pointer to value of t.
func (p unsafePointer) asValue(typ reflect.Type) reflect.Value {
return reflect.NewAt(typ, p.p)
}
// asMemory returns the memory pointer at by p for a specified size.
func (p unsafePointer) asMemory(size uintptr) []byte {
return unsafe.Slice((*byte)(p.p), size)
}
// visitStack is a stack of pointers visited.
// Pointers are pushed onto the stack when visited, and popped when leaving.
// The integer value is the depth at which the pointer was visited.
// The length of this stack should be zero after every hashing operation.
type visitStack map[unsafe.Pointer]int
func (v visitStack) seen(p unsafe.Pointer) (int, bool) {
idx, ok := v[p]
return idx, ok
}
func (v *visitStack) push(p unsafe.Pointer) {
if *v == nil {
*v = make(map[unsafe.Pointer]int)
}
(*v)[p] = len(*v)
}
func (v visitStack) pop(p unsafe.Pointer) {
delete(v, p)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !race
package deephash
import "reflect"
type pointer = unsafePointer
// pointerOf returns a pointer from v, which must be a reflect.Pointer.
func pointerOf(v reflect.Value) pointer { return unsafePointerOf(v) }
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package deephash
import (
"net/netip"
"reflect"
"time"
)
var (
timeTimeType = reflect.TypeFor[time.Time]()
netipAddrType = reflect.TypeFor[netip.Addr]()
selfHasherType = reflect.TypeFor[SelfHasher]()
)
// typeIsSpecialized reports whether this type has specialized hashing.
// These are never memory hashable and never considered recursive.
func typeIsSpecialized(t reflect.Type) bool {
switch t {
case timeTimeType, netipAddrType:
return true
default:
if t.Kind() != reflect.Pointer && t.Kind() != reflect.Interface {
if t.Implements(selfHasherType) || reflect.PointerTo(t).Implements(selfHasherType) {
return true
}
}
return false
}
}
// typeIsMemHashable reports whether t can be hashed by directly hashing its
// contiguous bytes in memory (e.g. structs with gaps are not mem-hashable).
func typeIsMemHashable(t reflect.Type) bool {
if typeIsSpecialized(t) {
return false
}
if t.Size() == 0 {
return true
}
switch t.Kind() {
case reflect.Bool,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
reflect.Float32, reflect.Float64,
reflect.Complex64, reflect.Complex128:
return true
case reflect.Array:
return typeIsMemHashable(t.Elem())
case reflect.Struct:
var sumFieldSize uintptr
for i, numField := 0, t.NumField(); i < numField; i++ {
sf := t.Field(i)
if !typeIsMemHashable(sf.Type) {
return false
}
sumFieldSize += sf.Type.Size()
}
return sumFieldSize == t.Size() // ensure no gaps
}
return false
}
// typeIsRecursive reports whether t has a path back to itself.
// For interfaces, it currently always reports true.
func typeIsRecursive(t reflect.Type) bool {
inStack := map[reflect.Type]bool{}
var visitType func(t reflect.Type) (isRecursiveSoFar bool)
visitType = func(t reflect.Type) (isRecursiveSoFar bool) {
// Check whether we have seen this type before.
if inStack[t] {
return true
}
inStack[t] = true
defer func() {
delete(inStack, t)
}()
// Types with specialized hashing are never considered recursive.
if typeIsSpecialized(t) {
return false
}
// Any type that is memory hashable must not be recursive since
// cycles can only occur if pointers are involved.
if typeIsMemHashable(t) {
return false
}
// Recursively check types that may contain pointers.
switch t.Kind() {
default:
panic("unhandled kind " + t.Kind().String())
case reflect.String, reflect.UnsafePointer, reflect.Func:
return false
case reflect.Interface:
// Assume the worst for now. TODO(bradfitz): in some cases
// we should be able to prove that it's not recursive. Not worth
// it for now.
return true
case reflect.Array, reflect.Chan, reflect.Pointer, reflect.Slice:
return visitType(t.Elem())
case reflect.Map:
return visitType(t.Key()) || visitType(t.Elem())
case reflect.Struct:
for i, numField := 0, t.NumField(); i < numField; i++ {
if visitType(t.Field(i).Type) {
return true
}
}
return false
}
}
return visitType(t)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package def parses strings and environment variables with fallback default values.
package def
import (
"os"
"strconv"
"time"
)
// Bool parses s as a bool, returning def when s is empty or invalid.
func Bool(s string, def bool) bool {
if s == "" {
return def
}
v, err := strconv.ParseBool(s)
if err != nil {
return def
}
return v
}
// Duration parses s as a time.Duration, returning def when s is empty or invalid.
func Duration(s string, def time.Duration) time.Duration {
if s == "" {
return def
}
v, err := time.ParseDuration(s)
if err != nil {
return def
}
return v
}
// LookupEnv retrieves the value of the environment variable named by the key.
// If the variable is present in the environment the value (which may be
// empty) is returned. Otherwise, it returns def.
func LookupEnv(key, def string) string {
if v, ok := os.LookupEnv(key); ok {
return v
}
return def
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package dirwalk contains code to walk a directory.
package dirwalk
import (
"io"
"io/fs"
"os"
"go4.org/mem"
)
var osWalkShallow func(name mem.RO, fn WalkFunc) error
// WalkFunc is the callback type used with WalkShallow.
//
// The name and de are only valid for the duration of func's call
// and should not be retained.
type WalkFunc func(name mem.RO, de fs.DirEntry) error
// WalkShallow reads the entries in the named directory and calls fn for each.
// It does not recurse into subdirectories.
//
// If fn returns an error, iteration stops and WalkShallow returns that value.
//
// On Linux, WalkShallow does not allocate, so long as certain methods on the
// WalkFunc's DirEntry are not called which necessarily allocate.
func WalkShallow(dirName mem.RO, fn WalkFunc) error {
if f := osWalkShallow; f != nil {
return f(dirName, fn)
}
of, err := os.Open(dirName.StringCopy())
if err != nil {
return err
}
defer of.Close()
for {
fis, err := of.ReadDir(100)
for _, de := range fis {
if err := fn(mem.S(de.Name()), de); err != nil {
return err
}
}
if err != nil {
if err == io.EOF {
return nil
}
return err
}
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package dirwalk
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"sync"
"syscall"
"unsafe"
"go4.org/mem"
"golang.org/x/sys/unix"
)
func init() {
osWalkShallow = linuxWalkShallow
}
var dirEntPool = &sync.Pool{New: func() any { return new(linuxDirEnt) }}
func linuxWalkShallow(dirName mem.RO, fn WalkFunc) error {
const blockSize = 8 << 10
buf := make([]byte, blockSize) // stack-allocated; doesn't escape
nameb := mem.Append(buf[:0], dirName)
nameb = append(nameb, 0)
fd, err := sysOpen(nameb)
if err != nil {
return err
}
defer syscall.Close(fd)
bufp := 0 // starting read position in buf
nbuf := 0 // end valid data in buf
de := dirEntPool.Get().(*linuxDirEnt)
defer de.cleanAndPutInPool()
de.root = dirName
for {
if bufp >= nbuf {
bufp = 0
nbuf, err = readDirent(fd, buf)
if err != nil {
return err
}
if nbuf <= 0 {
return nil
}
}
consumed, name := parseDirEnt(&de.d, buf[bufp:nbuf])
bufp += consumed
if len(name) == 0 || string(name) == "." || string(name) == ".." {
continue
}
de.name = mem.B(name)
if err := fn(de.name, de); err != nil {
return err
}
}
}
type linuxDirEnt struct {
root mem.RO
d syscall.Dirent
name mem.RO
}
func (de *linuxDirEnt) cleanAndPutInPool() {
de.root = mem.RO{}
de.name = mem.RO{}
dirEntPool.Put(de)
}
func (de *linuxDirEnt) Name() string { return de.name.StringCopy() }
func (de *linuxDirEnt) Info() (fs.FileInfo, error) {
return os.Lstat(filepath.Join(de.root.StringCopy(), de.name.StringCopy()))
}
func (de *linuxDirEnt) IsDir() bool {
return de.d.Type == syscall.DT_DIR
}
func (de *linuxDirEnt) Type() fs.FileMode {
switch de.d.Type {
case syscall.DT_BLK:
return fs.ModeDevice // shrug
case syscall.DT_CHR:
return fs.ModeCharDevice
case syscall.DT_DIR:
return fs.ModeDir
case syscall.DT_FIFO:
return fs.ModeNamedPipe
case syscall.DT_LNK:
return fs.ModeSymlink
case syscall.DT_REG:
return 0
case syscall.DT_SOCK:
return fs.ModeSocket
default:
return fs.ModeIrregular // shrug
}
}
func direntNamlen(dirent *syscall.Dirent) int {
const fixedHdr = uint16(unsafe.Offsetof(syscall.Dirent{}.Name))
limit := dirent.Reclen - fixedHdr
const dirNameLen = 256 // sizeof syscall.Dirent.Name
if limit > dirNameLen {
limit = dirNameLen
}
for i := uint16(0); i < limit; i++ {
if dirent.Name[i] == 0 {
return int(i)
}
}
panic("failed to find terminating 0 byte in dirent")
}
func parseDirEnt(dirent *syscall.Dirent, buf []byte) (consumed int, name []byte) {
// golang.org/issue/37269
copy(unsafe.Slice((*byte)(unsafe.Pointer(dirent)), unsafe.Sizeof(syscall.Dirent{})), buf)
if v := unsafe.Offsetof(dirent.Reclen) + unsafe.Sizeof(dirent.Reclen); uintptr(len(buf)) < v {
panic(fmt.Sprintf("buf size of %d smaller than dirent header size %d", len(buf), v))
}
if len(buf) < int(dirent.Reclen) {
panic(fmt.Sprintf("buf size %d < record length %d", len(buf), dirent.Reclen))
}
consumed = int(dirent.Reclen)
if dirent.Ino == 0 { // File absent in directory.
return
}
name = unsafe.Slice((*byte)(unsafe.Pointer(&dirent.Name[0])), direntNamlen(dirent))
return
}
func sysOpen(name []byte) (fd int, err error) {
if len(name) == 0 || name[len(name)-1] != 0 {
return 0, syscall.EINVAL
}
var dirfd int = unix.AT_FDCWD
for {
r0, _, e1 := syscall.Syscall(unix.SYS_OPENAT, uintptr(dirfd),
uintptr(unsafe.Pointer(&name[0])), 0)
if e1 == 0 {
return int(r0), nil
}
if e1 == syscall.EINTR {
// Since https://golang.org/doc/go1.14#runtime we
// need to loop on EINTR on more places.
continue
}
return 0, syscall.Errno(e1)
}
}
func readDirent(fd int, buf []byte) (n int, err error) {
for {
nbuf, err := syscall.ReadDirent(fd, buf)
if err != syscall.EINTR {
return nbuf, err
}
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package dnsname contains string functions for working with DNS names.
package dnsname
import (
"strings"
"tailscale.com/util/vizerror"
)
const (
// maxLabelLength is the maximum length of a label permitted by RFC 1035.
maxLabelLength = 63
// maxNameLength is the maximum length of a DNS name.
maxNameLength = 254
)
// A FQDN is a fully-qualified DNS name or name suffix.
type FQDN string
func ToFQDN(s string) (FQDN, error) {
if len(s) == 0 || s == "." {
return FQDN("."), nil
}
if s[0] == '.' {
s = s[1:]
}
raw := s
totalLen := len(s)
if s[len(s)-1] == '.' {
s = s[:len(s)-1]
} else {
totalLen += 1 // account for missing dot
}
if totalLen > maxNameLength {
return "", vizerror.Errorf("%q is too long to be a DNS name", s)
}
st := 0
for i := range len(s) {
if s[i] != '.' {
continue
}
label := s[st:i]
// You might be tempted to do further validation of the
// contents of labels here, based on the hostname rules in RFC
// 1123. However, DNS labels are not always subject to
// hostname rules. In general, they can contain any non-zero
// byte sequence, even though in practice a more restricted
// set is used.
//
// See https://github.com/tailscale/tailscale/issues/2024 for more.
if len(label) == 0 || len(label) > maxLabelLength {
return "", vizerror.Errorf("%q is not a valid DNS label", label)
}
st = i + 1
}
if raw[len(raw)-1] != '.' {
raw = raw + "."
}
return FQDN(raw), nil
}
// WithTrailingDot returns f as a string, with a trailing dot.
func (f FQDN) WithTrailingDot() string {
return string(f)
}
// WithoutTrailingDot returns f as a string, with the trailing dot
// removed.
func (f FQDN) WithoutTrailingDot() string {
return string(f[:len(f)-1])
}
func (f FQDN) NumLabels() int {
if f == "." {
return 0
}
return strings.Count(f.WithTrailingDot(), ".")
}
func (f FQDN) Contains(other FQDN) bool {
if f == other {
return true
}
cmp := f.WithTrailingDot()
if cmp != "." {
cmp = "." + cmp
}
return strings.HasSuffix(other.WithTrailingDot(), cmp)
}
// Parent returns the parent domain by stripping the first label.
// For "foo.bar.baz.", it returns "bar.baz."
// It returns an empty FQDN for root or single-label domains.
func (f FQDN) Parent() FQDN {
s := f.WithTrailingDot()
_, rest, ok := strings.Cut(s, ".")
if !ok || rest == "" {
return ""
}
return FQDN(rest)
}
// ValidLabel reports whether label is a valid DNS label. All errors are
// [vizerror.Error].
func ValidLabel(label string) error {
return ValidLabelLike(label, maxLabelLength)
}
// ValidLabelLike reports whether label contains DNS-valid characters
// only, adheres to a given maximum length, and starts and ends with
// a letter or number. For strict DNS-validity use [ValidLabel].
// All errors are [vizerror.Error].
func ValidLabelLike(label string, maxLen int) error {
if len(label) == 0 {
return vizerror.New("empty DNS label")
}
if len(label) > maxLen {
return vizerror.Errorf("%q is too long, max length is %d bytes", label, maxLen)
}
if !isalphanum(label[0]) {
return vizerror.Errorf("%q is not a valid DNS label: must start with a letter or number", label)
}
if !isalphanum(label[len(label)-1]) {
return vizerror.Errorf("%q is not a valid DNS label: must end with a letter or number", label)
}
if len(label) <= 2 {
// Return early because we've already checked start and end characters (the only 2) are alphanumeric.
return nil
}
for i := 1; i < len(label)-1; i++ {
if !isdnschar(label[i]) {
return vizerror.Errorf("%q is not a valid DNS label: contains invalid character %q", label, label[i])
}
}
return nil
}
// SanitizeLabel takes a string intended to be a DNS name label
// and turns it into a valid name label according to RFC 1035.
func SanitizeLabel(label string) string {
var sb strings.Builder // TODO: don't allocate in common case where label is already fine
start, end := 0, len(label)
// This is technically stricter than necessary as some characters may be dropped,
// but labels have no business being anywhere near this long in any case.
if end > maxLabelLength {
end = maxLabelLength
}
// A label must start with a letter or number...
for ; start < end; start++ {
if isalphanum(label[start]) {
break
}
}
// ...and end with a letter or number.
for ; start < end; end-- {
// This is safe because (start < end) implies (end >= 1).
if isalphanum(label[end-1]) {
break
}
}
for i := start; i < end; i++ {
// Consume a separator only if we are not at a boundary:
// then we can turn it into a hyphen without breaking the rules.
boundary := (i == start) || (i == end-1)
if !boundary && separators[label[i]] {
sb.WriteByte('-')
} else if isdnschar(label[i]) {
sb.WriteByte(tolower(label[i]))
}
}
return sb.String()
}
// HasSuffix reports whether the provided name ends with the
// component(s) in suffix, ignoring any trailing or leading dots.
//
// If suffix is the empty string, HasSuffix always reports false.
func HasSuffix(name, suffix string) bool {
name = strings.TrimSuffix(name, ".")
suffix = strings.TrimSuffix(suffix, ".")
suffix = strings.TrimPrefix(suffix, ".")
nameBase := strings.TrimSuffix(name, suffix)
return len(nameBase) < len(name) && strings.HasSuffix(nameBase, ".")
}
// TrimSuffix trims any trailing dots from a name and removes the
// suffix ending if present. The name will never be returned with
// a trailing dot, even after trimming.
func TrimSuffix(name, suffix string) string {
if HasSuffix(name, suffix) {
name = strings.TrimSuffix(name, ".")
suffix = strings.Trim(suffix, ".")
name = strings.TrimSuffix(name, suffix)
}
return strings.TrimSuffix(name, ".")
}
// TrimCommonSuffixes returns hostname with some common suffixes removed.
func TrimCommonSuffixes(hostname string) string {
hostname = strings.TrimSuffix(hostname, ".local")
hostname = strings.TrimSuffix(hostname, ".localdomain")
hostname = strings.TrimSuffix(hostname, ".lan")
return hostname
}
// SanitizeHostname turns hostname into a valid name label according
// to RFC 1035.
func SanitizeHostname(hostname string) string {
hostname = TrimCommonSuffixes(hostname)
return SanitizeLabel(hostname)
}
// NumLabels returns the number of DNS labels in hostname.
// If hostname is empty or the top-level name ".", returns 0.
func NumLabels(hostname string) int {
if hostname == "" || hostname == "." {
return 0
}
return strings.Count(hostname, ".")
}
// FirstLabel returns the first DNS label of hostname.
func FirstLabel(hostname string) string {
first, _, _ := strings.Cut(hostname, ".")
return first
}
// ValidHostname checks if a string is a valid hostname.
func ValidHostname(hostname string) error {
fqdn, err := ToFQDN(hostname)
if err != nil {
return err
}
for label := range strings.SplitSeq(fqdn.WithoutTrailingDot(), ".") {
if err := ValidLabel(label); err != nil {
return err
}
}
return nil
}
var separators = map[byte]bool{
' ': true,
'.': true,
'@': true,
'_': true,
}
func islower(c byte) bool {
return 'a' <= c && c <= 'z'
}
func isupper(c byte) bool {
return 'A' <= c && c <= 'Z'
}
func isalpha(c byte) bool {
return islower(c) || isupper(c)
}
func isalphanum(c byte) bool {
return isalpha(c) || ('0' <= c && c <= '9')
}
func isdnschar(c byte) bool {
return isalphanum(c) || c == '-'
}
func tolower(c byte) byte {
if isupper(c) {
return c + 'a' - 'A'
} else {
return c
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package eventbus
import (
"context"
"log"
"reflect"
"slices"
"tailscale.com/syncs"
"tailscale.com/types/logger"
"tailscale.com/util/set"
)
type PublishedEvent struct {
Event any
From *Client
}
type RoutedEvent struct {
Event any
From *Client
To []*Client
}
// Bus is an event bus that distributes published events to interested
// subscribers.
type Bus struct {
router *worker
write chan PublishedEvent
snapshot chan chan []PublishedEvent
routeDebug hook[RoutedEvent]
logf logger.Logf
topicsMu syncs.Mutex
topics map[reflect.Type][]*subscribeState
// Used for introspection/debugging only, not in the normal event
// publishing path.
clientsMu syncs.Mutex
clients set.Set[*Client]
}
// New returns a new bus with default options. It is equivalent to
// calling [NewWithOptions] with zero [BusOptions].
func New() *Bus { return NewWithOptions(BusOptions{}) }
// NewWithOptions returns a new [Bus] with the specified [BusOptions].
// Use [Bus.Client] to construct clients on the bus.
// Use [Publish] to make event publishers.
// Use [Subscribe] and [SubscribeFunc] to make event subscribers.
func NewWithOptions(opts BusOptions) *Bus {
ret := &Bus{
write: make(chan PublishedEvent),
snapshot: make(chan chan []PublishedEvent),
topics: map[reflect.Type][]*subscribeState{},
clients: set.Set[*Client]{},
logf: opts.logger(),
}
ret.router = runWorker(ret.pump)
return ret
}
// BusOptions are optional parameters for a [Bus]. A zero value is ready for
// use and provides defaults as described.
type BusOptions struct {
// Logf, if non-nil, is used for debug logs emitted by the bus and clients,
// publishers, and subscribers under its care. If it is nil, logs are sent
// to [log.Printf].
Logf logger.Logf
}
func (o BusOptions) logger() logger.Logf {
if o.Logf == nil {
return log.Printf
}
return o.Logf
}
// Client returns a new client with no subscriptions. Use [Subscribe]
// to receive events, and [Publish] to emit events.
//
// The client's name is used only for debugging, to tell humans what
// piece of code a publisher/subscriber belongs to. Aim for something
// short but unique, for example "kernel-route-monitor" or "taildrop",
// not "watcher".
func (b *Bus) Client(name string) *Client {
ret := &Client{
name: name,
bus: b,
pub: set.Set[publisher]{},
}
b.clientsMu.Lock()
defer b.clientsMu.Unlock()
b.clients.Add(ret)
return ret
}
// Debugger returns the debugging facility for the bus.
func (b *Bus) Debugger() *Debugger {
return &Debugger{b}
}
// ProbeLocks acquires and releases the bus's internal mutexes.
func (b *Bus) ProbeLocks() {
b.topicsMu.Lock()
b.topicsMu.Unlock()
b.clientsMu.Lock()
b.clientsMu.Unlock()
}
// Close closes the bus. It implicitly closes all clients, publishers and
// subscribers attached to the bus.
//
// Close blocks until the bus is fully shut down. The bus is
// permanently unusable after closing.
func (b *Bus) Close() {
b.router.StopAndWait()
b.clientsMu.Lock()
defer b.clientsMu.Unlock()
for c := range b.clients {
c.Close()
}
b.clients = nil
}
func (b *Bus) pump(ctx context.Context) {
// Limit how many published events we can buffer in the PublishedEvent queue.
//
// Subscribers have unbounded DeliveredEvent queues (see tailscale/tailscale#18020),
// so this queue doesn't need to be unbounded. Keeping it bounded may also help
// catch cases where subscribers stop pumping events completely, such as due to a bug
// in [subscribeState.pump], [Subscriber.dispatch], or [SubscriberFunc.dispatch]).
const maxPublishedEvents = 16
vals := queue[PublishedEvent]{capacity: maxPublishedEvents}
acceptCh := func() chan PublishedEvent {
if vals.Full() {
return nil
}
return b.write
}
for {
// Drain all pending events. Note that while we're draining
// events into subscriber queues, we continue to
// opportunistically accept more incoming events, if we have
// queue space for it.
for !vals.Empty() {
val := vals.Peek()
dests := b.dest(reflect.TypeOf(val.Event))
if b.routeDebug.active() {
clients := make([]*Client, len(dests))
for i := range len(dests) {
clients[i] = dests[i].client
}
b.routeDebug.run(RoutedEvent{
Event: val.Event,
From: val.From,
To: clients,
})
}
for _, d := range dests {
evt := DeliveredEvent{
Event: val.Event,
From: val.From,
To: d.client,
}
deliverOne:
for {
select {
case d.write <- evt:
break deliverOne
case <-d.closed():
// Queue closed, don't block but continue
// delivering to others.
break deliverOne
case in := <-acceptCh():
vals.Add(in)
in.From.publishDebug.run(in)
case <-ctx.Done():
return
case ch := <-b.snapshot:
ch <- vals.Snapshot()
}
}
}
vals.Drop()
}
// Inbound queue empty, wait for at least 1 work item before
// resuming.
for vals.Empty() {
select {
case <-ctx.Done():
return
case in := <-b.write:
vals.Add(in)
in.From.publishDebug.run(in)
case ch := <-b.snapshot:
ch <- nil
}
}
}
}
// logger returns a [logger.Logf] to which logs related to bus activity should be written.
func (b *Bus) logger() logger.Logf { return b.logf }
func (b *Bus) dest(t reflect.Type) []*subscribeState {
b.topicsMu.Lock()
defer b.topicsMu.Unlock()
return b.topics[t]
}
func (b *Bus) shouldPublish(t reflect.Type) bool {
if b.routeDebug.active() {
return true
}
b.topicsMu.Lock()
defer b.topicsMu.Unlock()
return len(b.topics[t]) > 0
}
func (b *Bus) listClients() []*Client {
b.clientsMu.Lock()
defer b.clientsMu.Unlock()
return b.clients.Slice()
}
func (b *Bus) snapshotPublishQueue() []PublishedEvent {
resp := make(chan []PublishedEvent)
select {
case b.snapshot <- resp:
return <-resp
case <-b.router.Done():
return nil
}
}
func (b *Bus) subscribe(t reflect.Type, q *subscribeState) (cancel func()) {
b.topicsMu.Lock()
defer b.topicsMu.Unlock()
b.topics[t] = append(b.topics[t], q)
return func() {
b.unsubscribe(t, q)
}
}
func (b *Bus) unsubscribe(t reflect.Type, q *subscribeState) {
b.topicsMu.Lock()
defer b.topicsMu.Unlock()
// Topic slices are accessed by pump without holding a lock, so we
// have to replace the entire slice when unsubscribing.
// Unsubscribing should be infrequent enough that this won't
// matter.
i := slices.Index(b.topics[t], q)
if i < 0 {
return
}
b.topics[t] = slices.Delete(slices.Clone(b.topics[t]), i, i+1)
}
// A worker runs a worker goroutine and helps coordinate its shutdown.
type worker struct {
ctx context.Context
stop context.CancelFunc
stopped chan struct{}
}
// runWorker creates a worker goroutine running fn. The context passed
// to fn is canceled by [worker.Stop].
func runWorker(fn func(context.Context)) *worker {
ctx, stop := context.WithCancel(context.Background())
ret := &worker{
ctx: ctx,
stop: stop,
stopped: make(chan struct{}),
}
go ret.run(fn)
return ret
}
func (w *worker) run(fn func(context.Context)) {
defer close(w.stopped)
fn(w.ctx)
}
// Stop signals the worker goroutine to shut down.
func (w *worker) Stop() { w.stop() }
// Done returns a channel that is closed when the worker goroutine
// exits.
func (w *worker) Done() <-chan struct{} { return w.stopped }
// Wait waits until the worker goroutine has exited.
func (w *worker) Wait() { <-w.stopped }
// StopAndWait signals the worker goroutine to shut down, then waits
// for it to exit.
func (w *worker) StopAndWait() {
w.stop()
<-w.stopped
}
// stopFlag is a value that can be watched for a notification. The
// zero value is ready for use.
//
// The flag is notified by running [stopFlag.Stop]. Stop can be called
// multiple times. Upon the first call to Stop, [stopFlag.Done] is
// closed, all pending [stopFlag.Wait] calls return, and future Wait
// calls return immediately.
//
// A stopFlag can only notify once, and is intended for use as a
// one-way shutdown signal that's lighter than a cancellable
// context.Context.
type stopFlag struct {
// guards the lazy construction of stopped, and the value of
// alreadyStopped.
mu syncs.Mutex
stopped chan struct{}
alreadyStopped bool
}
func (s *stopFlag) Stop() {
s.mu.Lock()
defer s.mu.Unlock()
if s.alreadyStopped {
return
}
s.alreadyStopped = true
if s.stopped == nil {
s.stopped = make(chan struct{})
}
close(s.stopped)
}
func (s *stopFlag) Done() <-chan struct{} {
s.mu.Lock()
defer s.mu.Unlock()
if s.stopped == nil {
s.stopped = make(chan struct{})
}
return s.stopped
}
func (s *stopFlag) Wait() {
<-s.Done()
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package eventbus
import (
"reflect"
"tailscale.com/syncs"
"tailscale.com/types/logger"
"tailscale.com/util/set"
)
// A Client can publish and subscribe to events on its attached
// bus. See [Publish] to publish events, and [Subscribe] to receive
// events.
//
// Subscribers that share the same client receive events one at a
// time, in the order they were published.
type Client struct {
name string
bus *Bus
publishDebug hook[PublishedEvent]
mu syncs.Mutex
pub set.Set[publisher]
sub *subscribeState // Lazily created on first subscribe
stop stopFlag // signaled on Close
}
func (c *Client) Name() string { return c.name }
func (c *Client) logger() logger.Logf { return c.bus.logger() }
// Close closes the client. It implicitly closes all publishers and
// subscribers obtained from this client.
func (c *Client) Close() {
var (
pub set.Set[publisher]
sub *subscribeState
)
c.mu.Lock()
pub, c.pub = c.pub, nil
sub, c.sub = c.sub, nil
c.mu.Unlock()
if sub != nil {
sub.close()
}
for p := range pub {
p.Close()
}
c.stop.Stop()
}
func (c *Client) isClosed() bool { return c.pub == nil && c.sub == nil }
// Done returns a channel that is closed when [Client.Close] is called.
// The channel is closed after all the publishers and subscribers governed by
// the client have been closed.
func (c *Client) Done() <-chan struct{} { return c.stop.Done() }
func (c *Client) snapshotSubscribeQueue() []DeliveredEvent {
return c.peekSubscribeState().snapshotQueue()
}
func (c *Client) peekSubscribeState() *subscribeState {
c.mu.Lock()
defer c.mu.Unlock()
return c.sub
}
func (c *Client) publishTypes() []reflect.Type {
c.mu.Lock()
defer c.mu.Unlock()
ret := make([]reflect.Type, 0, len(c.pub))
for pub := range c.pub {
ret = append(ret, pub.publishType())
}
return ret
}
func (c *Client) subscribeTypes() []reflect.Type {
return c.peekSubscribeState().subscribeTypes()
}
func (c *Client) subscribeState() *subscribeState {
c.mu.Lock()
defer c.mu.Unlock()
return c.subscribeStateLocked()
}
func (c *Client) subscribeStateLocked() *subscribeState {
if c.sub == nil {
c.sub = newSubscribeState(c)
}
return c.sub
}
func (c *Client) addPublisher(pub publisher) {
c.mu.Lock()
defer c.mu.Unlock()
if c.isClosed() {
panic("cannot Publish on a closed client")
}
c.pub.Add(pub)
}
func (c *Client) deletePublisher(pub publisher) {
c.mu.Lock()
defer c.mu.Unlock()
c.pub.Delete(pub)
}
func (c *Client) addSubscriber(t reflect.Type, s *subscribeState) {
c.bus.subscribe(t, s)
}
func (c *Client) deleteSubscriber(t reflect.Type, s *subscribeState) {
c.bus.unsubscribe(t, s)
}
func (c *Client) publish() chan<- PublishedEvent {
return c.bus.write
}
func (c *Client) shouldPublish(t reflect.Type) bool {
return c.publishDebug.active() || c.bus.shouldPublish(t)
}
// Subscribe requests delivery of events of type T through the given client.
// It panics if c already has a subscriber for type T, or if c is closed.
func Subscribe[T any](c *Client) *Subscriber[T] {
// Hold the client lock throughout the subscription process so that a caller
// attempting to subscribe on a closed client will get a useful diagnostic
// instead of a random panic from inside the subscriber plumbing.
c.mu.Lock()
defer c.mu.Unlock()
// The caller should not race subscriptions with close, give them a useful
// diagnostic at the call site.
if c.isClosed() {
panic("cannot Subscribe on a closed client")
}
r := c.subscribeStateLocked()
s := newSubscriber[T](r, logfForCaller(c.logger()))
// Register the non-generic core with the bus rather than the typed facade,
// mirroring SubscribeFunc and Publish: this keeps the bus's outputs map
// and subscriber-interface itab out of per-T cost.
r.addSubscriber(s.core)
return s
}
// SubscribeFunc is like [Subscribe], but calls the provided func for each
// event of type T.
//
// A SubscriberFunc calls f synchronously from the client's goroutine.
// This means the callback must not block for an extended period of time,
// as this will block the subscriber and slow event processing for all
// subscriptions on c.
func SubscribeFunc[T any](c *Client, f func(T)) *SubscriberFunc[T] {
c.mu.Lock()
defer c.mu.Unlock()
// The caller should not race subscriptions with close, give them a useful
// diagnostic at the call site.
if c.isClosed() {
panic("cannot SubscribeFunc on a closed client")
}
r := c.subscribeStateLocked()
s := newSubscriberFunc[T](r, f, logfForCaller(c.logger()))
// Register the non-generic core, not the typed facade. Doing
// so means the bus's outputs map and the subscriber interface
// itab are not parameterized by T, eliminating per-T itab and
// dictionary cost.
r.addSubscriber(s.core)
return s
}
// Publish returns a publisher for event type T using the given client.
// It panics if c is closed.
func Publish[T any](c *Client) *Publisher[T] {
p := newPublisher[T](c)
// Register the non-generic core with the client so the
// per-Client publisher set, the publisher interface itab, and
// the publisher equality function are not parameterized by T.
// This eliminates per-T itab/dictionary/eq cost for every new
// event type passed through Publish[T].
c.addPublisher(p.core)
return p
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package eventbus
import (
"cmp"
"fmt"
"path/filepath"
"reflect"
"runtime"
"slices"
"strings"
"sync/atomic"
"time"
"tailscale.com/syncs"
"tailscale.com/types/logger"
)
// slowSubscriberTimeout is a timeout after which a subscriber that does not
// accept a pending event will be flagged as being slow.
const slowSubscriberTimeout = 5 * time.Second
// A Debugger offers access to a bus's privileged introspection and
// debugging facilities.
//
// The debugger's functionality is intended for humans and their tools
// to examine and troubleshoot bus clients, and should not be used in
// normal codepaths.
//
// In particular, the debugger provides access to information that is
// deliberately withheld from bus clients to encourage more robust and
// maintainable code - for example, the sender of an event, or the
// event streams of other clients. Please don't use the debugger to
// circumvent these restrictions for purposes other than debugging.
type Debugger struct {
bus *Bus
}
// Clients returns a list of all clients attached to the bus.
func (d *Debugger) Clients() []*Client {
ret := d.bus.listClients()
slices.SortFunc(ret, func(a, b *Client) int {
return cmp.Compare(a.Name(), b.Name())
})
return ret
}
// PublishQueue returns the contents of the publish queue.
//
// The publish queue contains events that have been accepted by the
// bus from Publish() calls, but have not yet been routed to relevant
// subscribers.
//
// This queue is expected to be almost empty in normal operation. A
// full publish queue indicates that a slow subscriber downstream is
// causing backpressure and stalling the bus.
func (d *Debugger) PublishQueue() []PublishedEvent {
return d.bus.snapshotPublishQueue()
}
// checkClient verifies that client is attached to the same bus as the
// Debugger, and panics if not.
func (d *Debugger) checkClient(client *Client) {
if client.bus != d.bus {
panic(fmt.Errorf("SubscribeQueue given client belonging to wrong bus"))
}
}
// SubscribeQueue returns the contents of the given client's subscribe
// queue.
//
// The subscribe queue contains events that are to be delivered to the
// client, but haven't yet been handed off to the relevant
// [Subscriber].
//
// This queue is expected to be almost empty in normal operation. A
// full subscribe queue indicates that the client is accepting events
// too slowly, and may be causing the rest of the bus to stall.
func (d *Debugger) SubscribeQueue(client *Client) []DeliveredEvent {
d.checkClient(client)
return client.snapshotSubscribeQueue()
}
// WatchBus streams information about all events passing through the
// bus.
//
// Monitored events are delivered in the bus's global publication
// order (see "Concurrency properties" in the package docs).
//
// The caller must consume monitoring events promptly to avoid
// stalling the bus (see "Expected subscriber behavior" in the package
// docs).
func (d *Debugger) WatchBus() *Subscriber[RoutedEvent] {
return newMonitor(d.bus.routeDebug.add)
}
// WatchPublish streams information about all events published by the
// given client.
//
// Monitored events are delivered in the bus's global publication
// order (see "Concurrency properties" in the package docs).
//
// The caller must consume monitoring events promptly to avoid
// stalling the bus (see "Expected subscriber behavior" in the package
// docs).
func (d *Debugger) WatchPublish(client *Client) *Subscriber[PublishedEvent] {
d.checkClient(client)
return newMonitor(client.publishDebug.add)
}
// WatchSubscribe streams information about all events received by the
// given client.
//
// Monitored events are delivered in the bus's global publication
// order (see "Concurrency properties" in the package docs).
//
// The caller must consume monitoring events promptly to avoid
// stalling the bus (see "Expected subscriber behavior" in the package
// docs).
func (d *Debugger) WatchSubscribe(client *Client) *Subscriber[DeliveredEvent] {
d.checkClient(client)
return newMonitor(client.subscribeState().debug.add)
}
// PublishTypes returns the list of types being published by client.
//
// The returned types are those for which the client has obtained a
// [Publisher]. The client may not have ever sent the type in
// question.
func (d *Debugger) PublishTypes(client *Client) []reflect.Type {
d.checkClient(client)
return client.publishTypes()
}
// SubscribeTypes returns the list of types being subscribed to by
// client.
//
// The returned types are those for which the client has obtained a
// [Subscriber]. The client may not have ever received the type in
// question, and here may not be any publishers of the type.
func (d *Debugger) SubscribeTypes(client *Client) []reflect.Type {
d.checkClient(client)
return client.subscribeTypes()
}
// A hook collects hook functions that can be run as a group.
type hook[T any] struct {
syncs.Mutex
fns []hookFn[T]
}
var hookID atomic.Uint64
// add registers fn to be called when the hook is run. Returns an
// unregistration function that removes fn from the hook when called.
func (h *hook[T]) add(fn func(T)) (remove func()) {
id := hookID.Add(1)
h.Lock()
defer h.Unlock()
h.fns = append(h.fns, hookFn[T]{id, fn})
return func() { h.remove(id) }
}
// remove removes the function with the given ID from the hook.
func (h *hook[T]) remove(id uint64) {
h.Lock()
defer h.Unlock()
h.fns = slices.DeleteFunc(h.fns, func(f hookFn[T]) bool { return f.ID == id })
}
// active reports whether any functions are registered with the
// hook. This can be used to skip expensive work when the hook is
// inactive.
func (h *hook[T]) active() bool {
h.Lock()
defer h.Unlock()
return len(h.fns) > 0
}
// run calls all registered functions with the value v.
func (h *hook[T]) run(v T) {
h.Lock()
defer h.Unlock()
for _, fn := range h.fns {
fn.Fn(v)
}
}
type hookFn[T any] struct {
ID uint64
Fn func(T)
}
// DebugEvent is a representation of an event used for debug clients.
type DebugEvent struct {
Count int
Type string
From string
To []string
Event any
}
// DebugTopics provides the JSON encoding as a wrapper for a collection of [DebugTopic].
type DebugTopics struct {
Topics []DebugTopic
}
// DebugTopic provides the JSON encoding of publishers and subscribers for a
// given topic.
type DebugTopic struct {
Name string
Publisher string
Subscribers []string
}
// logfForCaller returns a [logger.Logf] that prefixes its output with the
// package, filename, and line number of the caller's caller.
// If logf == nil, it returns [logger.Discard].
// If the caller location could not be determined, it returns logf unmodified.
func logfForCaller(logf logger.Logf) logger.Logf {
if logf == nil {
return logger.Discard
}
pc, fpath, line, _ := runtime.Caller(2) // +1 for my caller, +1 for theirs
if f := runtime.FuncForPC(pc); f != nil {
return logger.WithPrefix(logf, fmt.Sprintf("%s %s:%d: ", funcPackageName(f.Name()), filepath.Base(fpath), line))
}
return logf
}
func funcPackageName(funcName string) string {
ls := max(strings.LastIndex(funcName, "/"), 0)
for {
i := strings.LastIndex(funcName, ".")
if i <= ls {
return funcName
}
funcName = funcName[:i]
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ios && !android && !ts_omit_debugeventbus
package eventbus
import (
"bytes"
"cmp"
"embed"
"fmt"
"html/template"
"io"
"io/fs"
"log"
"net/http"
"path/filepath"
"reflect"
"slices"
"strings"
"sync"
"github.com/coder/websocket"
"tailscale.com/tsweb"
)
type httpDebugger struct {
*Debugger
}
func (d *Debugger) RegisterHTTP(td *tsweb.DebugHandler) {
dh := httpDebugger{d}
td.Handle("bus", "Event bus", dh)
td.HandleSilent("bus/monitor", http.HandlerFunc(dh.serveMonitor))
td.HandleSilent("bus/style.css", serveStatic("style.css"))
td.HandleSilent("bus/htmx.min.js", serveStatic("htmx.min.js.gz"))
td.HandleSilent("bus/htmx-websocket.min.js", serveStatic("htmx-websocket.min.js.gz"))
}
//go:embed assets/*.html
var templatesSrc embed.FS
var templates = sync.OnceValue(func() *template.Template {
d, err := fs.Sub(templatesSrc, "assets")
if err != nil {
panic(fmt.Errorf("getting eventbus debughttp templates subdir: %w", err))
}
ret := template.New("").Funcs(map[string]any{
"prettyPrintStruct": prettyPrintStruct,
})
return template.Must(ret.ParseFS(d, "*"))
})
//go:generate go run fetch-htmx.go
//go:embed assets/*.css assets/*.min.js.gz
var static embed.FS
func serveStatic(name string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case strings.HasSuffix(name, ".css"):
w.Header().Set("Content-Type", "text/css")
case strings.HasSuffix(name, ".min.js.gz"):
w.Header().Set("Content-Type", "text/javascript")
w.Header().Set("Content-Encoding", "gzip")
case strings.HasSuffix(name, ".js"):
w.Header().Set("Content-Type", "text/javascript")
default:
http.Error(w, "not found", http.StatusNotFound)
return
}
f, err := static.Open(filepath.Join("assets", name))
if err != nil {
http.Error(w, fmt.Sprintf("opening asset: %v", err), http.StatusInternalServerError)
return
}
defer f.Close()
if _, err := io.Copy(w, f); err != nil {
http.Error(w, fmt.Sprintf("serving asset: %v", err), http.StatusInternalServerError)
return
}
})
}
func render(w http.ResponseWriter, name string, data any) {
err := templates().ExecuteTemplate(w, name+".html", data)
if err != nil {
err := fmt.Errorf("rendering template: %v", err)
log.Print(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func (h httpDebugger) ServeHTTP(w http.ResponseWriter, r *http.Request) {
type clientInfo struct {
*Client
Publish []reflect.Type
Subscribe []reflect.Type
}
type typeInfo struct {
reflect.Type
Publish []*Client
Subscribe []*Client
}
type info struct {
*Debugger
Clients map[string]*clientInfo
Types map[string]*typeInfo
}
data := info{
Debugger: h.Debugger,
Clients: map[string]*clientInfo{},
Types: map[string]*typeInfo{},
}
getTypeInfo := func(t reflect.Type) *typeInfo {
if data.Types[t.Name()] == nil {
data.Types[t.Name()] = &typeInfo{
Type: t,
}
}
return data.Types[t.Name()]
}
for _, c := range h.Clients() {
ci := &clientInfo{
Client: c,
Publish: h.PublishTypes(c),
Subscribe: h.SubscribeTypes(c),
}
slices.SortFunc(ci.Publish, func(a, b reflect.Type) int { return cmp.Compare(a.Name(), b.Name()) })
slices.SortFunc(ci.Subscribe, func(a, b reflect.Type) int { return cmp.Compare(a.Name(), b.Name()) })
data.Clients[c.Name()] = ci
for _, t := range ci.Publish {
ti := getTypeInfo(t)
ti.Publish = append(ti.Publish, c)
}
for _, t := range ci.Subscribe {
ti := getTypeInfo(t)
ti.Subscribe = append(ti.Subscribe, c)
}
}
render(w, "main", data)
}
func (h httpDebugger) serveMonitor(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Upgrade") == "websocket" {
h.serveMonitorStream(w, r)
return
}
render(w, "monitor", nil)
}
func (h httpDebugger) serveMonitorStream(w http.ResponseWriter, r *http.Request) {
conn, err := websocket.Accept(w, r, nil)
if err != nil {
return
}
defer conn.CloseNow()
wsCtx := conn.CloseRead(r.Context())
mon := h.WatchBus()
defer mon.Close()
i := 0
for {
select {
case <-r.Context().Done():
return
case <-wsCtx.Done():
return
case <-mon.Done():
return
case event := <-mon.Events():
msg, err := conn.Writer(r.Context(), websocket.MessageText)
if err != nil {
return
}
data := map[string]any{
"Count": i,
"Type": reflect.TypeOf(event.Event),
"Event": event,
}
i++
if err := templates().ExecuteTemplate(msg, "event.html", data); err != nil {
log.Println(err)
return
}
if err := msg.Close(); err != nil {
return
}
}
}
}
func prettyPrintStruct(t reflect.Type) string {
if t.Kind() != reflect.Struct {
return t.String()
}
var rec func(io.Writer, int, reflect.Type)
rec = func(out io.Writer, indent int, t reflect.Type) {
ind := strings.Repeat(" ", indent)
fmt.Fprintf(out, "%s", t.String())
fs := collectFields(t)
if len(fs) > 0 {
io.WriteString(out, " {\n")
for _, f := range fs {
fmt.Fprintf(out, "%s %s ", ind, f.Name)
if f.Type.Kind() == reflect.Struct {
rec(out, indent+1, f.Type)
} else {
fmt.Fprint(out, f.Type)
}
io.WriteString(out, "\n")
}
fmt.Fprintf(out, "%s}", ind)
}
}
var ret bytes.Buffer
rec(&ret, 0, t)
return ret.String()
}
func collectFields(t reflect.Type) (ret []reflect.StructField) {
for _, f := range reflect.VisibleFields(t) {
if !f.IsExported() {
continue
}
ret = append(ret, f)
}
return ret
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package eventbustest
import (
"errors"
"fmt"
"reflect"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"tailscale.com/util/eventbus"
)
// NewBus constructs an [eventbus.Bus] that will be shut automatically when
// its controlling test ends.
func NewBus(t testing.TB) *eventbus.Bus {
bus := eventbus.New()
t.Cleanup(bus.Close)
return bus
}
// NewWatcher constructs a [Watcher] that can be used to check the stream of
// events generated by code under test. After construction the caller may use
// [Expect] and [ExpectExactly], to verify that the desired events were captured.
func NewWatcher(t *testing.T, bus *eventbus.Bus) *Watcher {
tw := &Watcher{
mon: bus.Debugger().WatchBus(),
chDone: make(chan bool, 1),
events: make(chan any, 100),
}
t.Cleanup(tw.done)
go tw.watch()
return tw
}
// Watcher monitors and holds events for test expectations.
// The Watcher works with [synctest], and some scenarios does require the use of
// [synctest]. This is amongst others true if you are testing for the absence of
// events.
//
// For usage examples, see the documentation in the top of the package.
type Watcher struct {
mon *eventbus.Subscriber[eventbus.RoutedEvent]
events chan any
chDone chan bool
}
// Type is a helper representing the expectation to see an event of type T, without
// caring about the content of the event.
// It makes it possible to use helpers like:
//
// eventbustest.ExpectFilter(tw, eventbustest.Type[EventFoo]())
func Type[T any]() func(T) { return func(T) {} }
// Expect verifies that the given events are a subsequence of the events
// observed by tw. That is, tw must contain at least one event matching the type
// of each argument in the given order, other event types are allowed to occur in
// between without error. The given events are represented by a function
// that must have one of the following forms:
//
// // Tests for the event type only
// func(e ExpectedType)
//
// // Tests for event type and whatever is defined in the body.
// // If return is false, the test will look for other events of that type
// // If return is true, the test will look for the next given event
// // if a list is given
// func(e ExpectedType) bool
//
// // Tests for event type and whatever is defined in the body.
// // The boolean return works as above.
// // The if error != nil, the test helper will return that error immediately.
// func(e ExpectedType) (bool, error)
//
// // Tests for event type and whatever is defined in the body.
// // If a non-nil error is reported, the test helper will return that error
// // immediately; otherwise the expectation is considered to be met.
// func(e ExpectedType) error
//
// If the list of events must match exactly with no extra events,
// use [ExpectExactly].
func Expect(tw *Watcher, filters ...any) error {
if len(filters) == 0 {
return errors.New("no event filters were provided")
}
eventCount := 0
head := 0
for head < len(filters) {
eventFunc := eventFilter(filters[head])
select {
case event := <-tw.events:
eventCount++
if ok, err := eventFunc(event); err != nil {
return err
} else if ok {
head++
}
// Use synctest when you want an error here.
case <-time.After(100 * time.Second): // "indefinitely", to advance a synctest clock
return fmt.Errorf(
"timed out waiting for event, saw %d events, %d was expected",
eventCount, len(filters))
case <-tw.chDone:
return errors.New("watcher closed while waiting for events")
}
}
return nil
}
// ExpectExactly checks for some number of events showing up on the event bus
// in a given order, returning an error if the events does not match the given list
// exactly. The given events are represented by a function as described in
// [Expect]. Use [Expect] if other events are allowed.
//
// If you are expecting ExpectExactly to fail because of a missing event, or if
// you are testing for the absence of events, call [synctest.Wait] after
// actions that would publish an event, but before calling ExpectExactly.
func ExpectExactly(tw *Watcher, filters ...any) error {
if len(filters) == 0 {
select {
case event := <-tw.events:
return fmt.Errorf("saw event type %s, expected none", reflect.TypeOf(event))
case <-time.After(100 * time.Second): // "indefinitely", to advance a synctest clock
return nil
}
}
eventCount := 0
for pos, next := range filters {
eventFunc := eventFilter(next)
fnType := reflect.TypeOf(next)
argType := fnType.In(0)
select {
case event := <-tw.events:
eventCount++
typeEvent := reflect.TypeOf(event)
if typeEvent != argType {
return fmt.Errorf(
"expected event type %s, saw %s, at index %d",
argType, typeEvent, pos)
} else if ok, err := eventFunc(event); err != nil {
return err
} else if !ok {
return fmt.Errorf(
"expected test ok for type %s, at index %d", argType, pos)
}
case <-time.After(100 * time.Second): // "indefinitely", to advance a synctest clock
return fmt.Errorf(
"timed out waiting for event, saw %d events, %d was expected",
eventCount, len(filters))
case <-tw.chDone:
return errors.New("watcher closed while waiting for events")
}
}
return nil
}
func (tw *Watcher) watch() {
for {
select {
case event := <-tw.mon.Events():
tw.events <- event.Event
case <-tw.mon.Done():
tw.done()
return
case <-tw.chDone:
tw.mon.Close()
return
}
}
}
// done tells the watcher to stop monitoring for new events.
func (tw *Watcher) done() {
close(tw.chDone)
}
type filter = func(any) (bool, error)
func eventFilter(f any) filter {
ft := reflect.TypeOf(f)
if ft.Kind() != reflect.Func {
panic("filter is not a function")
} else if ft.NumIn() != 1 {
panic(fmt.Sprintf("function takes %d arguments, want 1", ft.NumIn()))
}
var fixup func([]reflect.Value) []reflect.Value
switch ft.NumOut() {
case 0:
fixup = func([]reflect.Value) []reflect.Value {
return []reflect.Value{reflect.ValueOf(true), reflect.Zero(reflect.TypeFor[error]())}
}
case 1:
switch ft.Out(0) {
case reflect.TypeFor[bool]():
fixup = func(vals []reflect.Value) []reflect.Value {
return append(vals, reflect.Zero(reflect.TypeFor[error]()))
}
case reflect.TypeFor[error]():
fixup = func(vals []reflect.Value) []reflect.Value {
pass := vals[0].IsZero()
return append([]reflect.Value{reflect.ValueOf(pass)}, vals...)
}
default:
panic(fmt.Sprintf("result is %v, want bool or error", ft.Out(0)))
}
case 2:
if ft.Out(0) != reflect.TypeFor[bool]() || ft.Out(1) != reflect.TypeFor[error]() {
panic(fmt.Sprintf("results are %v, %v; want bool, error", ft.Out(0), ft.Out(1)))
}
fixup = func(vals []reflect.Value) []reflect.Value { return vals }
default:
panic(fmt.Sprintf("function returns %d values", ft.NumOut()))
}
fv := reflect.ValueOf(f)
return reflect.MakeFunc(reflect.TypeFor[filter](), func(args []reflect.Value) []reflect.Value {
if !args[0].IsValid() || args[0].Elem().Type() != ft.In(0) {
return []reflect.Value{reflect.ValueOf(false), reflect.Zero(reflect.TypeFor[error]())}
}
return fixup(fv.Call([]reflect.Value{args[0].Elem()}))
}).Interface().(filter)
}
// Injector holds a map with [eventbus.Publisher], tied to an [eventbus.Client]
// for testing purposes.
type Injector struct {
client *eventbus.Client
publishers map[reflect.Type]any
// The value for a key is an *eventbus.Publisher[T] for the corresponding type.
}
// NewInjector constructs an [Injector] that can be used to inject events into
// the stream of events used by code under test. After construction the
// caller may use [Inject] to insert events into the bus.
func NewInjector(t *testing.T, b *eventbus.Bus) *Injector {
inj := &Injector{
client: b.Client(t.Name()),
publishers: make(map[reflect.Type]any),
}
t.Cleanup(inj.client.Close)
return inj
}
// Inject inserts events of T onto an [eventbus.Bus]. If an [eventbus.Publisher]
// for the type does not exist, it will be initialized lazily. Calling inject is
// synchronous, and the event will as such have been published to the eventbus
// by the time the function returns.
func Inject[T any](inj *Injector, event T) {
eventType := reflect.TypeFor[T]()
pub, ok := inj.publishers[eventType]
if !ok {
pub = eventbus.Publish[T](inj.client)
inj.publishers[eventType] = pub
}
pub.(*eventbus.Publisher[T]).Publish(event)
}
// EqualTo returns an event-matching function for use with [Expect] and
// [ExpectExactly] that matches on an event of the given type that is equal to
// want by comparison with [cmp.Diff]. The expectation fails with an error
// message including the diff, if present.
func EqualTo[T any](want T) func(T) error {
return func(got T) error {
if diff := cmp.Diff(got, want); diff != "" {
return fmt.Errorf("wrong result (-got, +want):\n%s", diff)
}
return nil
}
}
// LogAllEvents logs summaries of all the events routed via the specified bus
// during the execution of the test governed by t. This is intended to support
// development and debugging of tests.
func LogAllEvents(t testing.TB, bus *eventbus.Bus) {
dw := bus.Debugger().WatchBus()
done := make(chan struct{})
go func() {
defer close(done)
var i int
for {
select {
case <-dw.Done():
return
case re := <-dw.Events():
i++
t.Logf("[eventbus] #%[1]d: %[2]T | %+[2]v", i, re.Event)
}
}
}()
t.Cleanup(func() { dw.Close(); <-done })
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package eventbus
import "tailscale.com/syncs"
// A Monitor monitors the execution of a goroutine processing events from a
// [Client], allowing the caller to block until it is complete. The zero value
// of m is valid; its Close and Wait methods return immediately, and its Done
// method returns an already-closed channel.
type Monitor struct {
// These fields are immutable after initialization
cli *Client
done <-chan struct{}
}
// Close closes the client associated with m and blocks until the processing
// goroutine is complete.
func (m Monitor) Close() {
if m.cli == nil {
return
}
m.cli.Close()
<-m.done
}
// Wait blocks until the goroutine monitored by m has finished executing, but
// does not close the associated client. It is safe to call Wait repeatedly,
// and from multiple concurrent goroutines.
func (m Monitor) Wait() {
if m.done == nil {
return
}
<-m.done
}
// Done returns a channel that is closed when the monitored goroutine has
// finished executing.
func (m Monitor) Done() <-chan struct{} {
if m.done == nil {
return syncs.ClosedChan()
}
return m.done
}
// Monitor executes f in a new goroutine attended by a [Monitor]. The caller
// is responsible for waiting for the goroutine to complete, by calling either
// [Monitor.Close] or [Monitor.Wait].
func (c *Client) Monitor(f func(*Client)) Monitor {
done := make(chan struct{})
go func() { defer close(done); f(c) }()
return Monitor{cli: c, done: done}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package eventbus
import (
"reflect"
)
// publisher is a uniformly typed wrapper around publisherCore so that
// debugging facilities can enumerate active publishers on a [Client]
// and report the types each one publishes. The interface is
// implemented by the non-generic *publisherCore (not by the typed
// user-facing *Publisher[T]); this keeps the bus's per-Client
// publisher set, and the publisher itab/dictionary, free of
// per-T duplication.
type publisher interface {
publishType() reflect.Type
Close()
}
// A Publisher publishes typed events on a bus.
type Publisher[T any] struct {
// Implementation note: Publisher[T] is a thin user-facing facade over a
// non-generic *publisherCore. Carrying T on the public type preserves the
// typed API of Publish(v T), but all of the actual state (the *Client
// back-pointer, the stop flag, and the cached reflect.Type used by
// diagnostic introspection) lives on the core and is not duplicated per T.
//
// The diagnostic surface that motivates the publisher interface
// (Debugger.PublishTypes) is served by *publisherCore directly, so adding
// new typed publishers does not pay an itab+dictionary cost just to satisfy
// diagnostic enumeration.
core *publisherCore
}
// publisherCore is the non-generic implementation of a Publisher.
// It implements the package-private publisher interface; the bus's
// outputs map and itab key on this single type, not on Publisher[T].
type publisherCore struct {
client *Client
stop stopFlag
typ reflect.Type // cached reflect.TypeFor[T]()
}
func newPublisher[T any](c *Client) *Publisher[T] {
return &Publisher[T]{
core: &publisherCore{
client: c,
typ: reflect.TypeFor[T](),
},
}
}
// Close closes the publisher.
//
// Calls to Publish after Close silently do nothing.
//
// If the Bus or Client from which the Publisher was created is closed,
// the Publisher is implicitly closed and does not need to be closed
// separately.
func (p *Publisher[T]) Close() { p.core.Close() }
// Close implements the publisher interface and the user-facing
// (*Publisher[T]).Close.
func (c *publisherCore) Close() {
// Just unblocks any active calls to Publish, no other
// synchronization needed.
c.stop.Stop()
c.client.deletePublisher(c)
}
// publishType implements the publisher interface.
func (c *publisherCore) publishType() reflect.Type { return c.typ }
// Publish publishes event v on the bus.
func (p *Publisher[T]) Publish(v T) {
publish(p.core, v)
}
// publish is the non-generic body of Publisher[T].Publish. The only
// per-T work is the boxing of v into evt.Event (an `any` field) and
// the construction of the PublishedEvent struct itself; all of the
// channel/select dance is shared across every T.
func publish(c *publisherCore, v any) {
// Check for just a stopped publisher or bus before trying to
// write, so that once closed Publish consistently does nothing.
select {
case <-c.stop.Done():
return
default:
}
evt := PublishedEvent{
Event: v,
From: c.client,
}
select {
case c.client.publish() <- evt:
case <-c.stop.Done():
}
}
// ShouldPublish reports whether anyone is subscribed to the events
// that this publisher emits.
//
// ShouldPublish can be used to skip expensive event construction if
// nobody seems to care. Publishers must not assume that someone will
// definitely receive an event if ShouldPublish returns true.
func (p *Publisher[T]) ShouldPublish() bool {
return p.core.client.shouldPublish(p.core.typ)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package eventbus
import (
"slices"
)
// queue is an ordered queue of length up to capacity,
// if capacity is non-zero. Otherwise it is unbounded.
type queue[T any] struct {
vals []T
start int
capacity int // zero means unbounded
}
// canAppend reports whether a value can be appended to q.vals without
// shifting values around.
func (q *queue[T]) canAppend() bool {
return q.capacity == 0 || cap(q.vals) < q.capacity || len(q.vals) < cap(q.vals)
}
func (q *queue[T]) Full() bool {
return q.start == 0 && !q.canAppend()
}
func (q *queue[T]) Empty() bool {
return q.start == len(q.vals)
}
func (q *queue[T]) Len() int {
return len(q.vals) - q.start
}
// Add adds v to the end of the queue. Blocks until append can be
// done.
func (q *queue[T]) Add(v T) {
if !q.canAppend() {
if q.start == 0 {
panic("Add on a full queue")
}
// Slide remaining values back to the start of the array.
n := copy(q.vals, q.vals[q.start:])
toClear := len(q.vals) - n
clear(q.vals[len(q.vals)-toClear:])
q.vals = q.vals[:n]
q.start = 0
}
q.vals = append(q.vals, v)
}
// Peek returns the first value in the queue, without removing it from
// the queue, or nil if the queue is empty.
func (q *queue[T]) Peek() T {
if q.Empty() {
var zero T
return zero
}
return q.vals[q.start]
}
// Drop discards the first value in the queue, if any.
func (q *queue[T]) Drop() {
if q.Empty() {
return
}
var zero T
q.vals[q.start] = zero
q.start++
if q.Empty() {
// Reset cursor to start of array, it's free to do.
q.start = 0
q.vals = q.vals[:0]
}
}
// Snapshot returns a copy of the queue's contents.
func (q *queue[T]) Snapshot() []T {
return slices.Clone(q.vals[q.start:])
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package eventbus
import (
"context"
"fmt"
"reflect"
"runtime"
"time"
"tailscale.com/syncs"
"tailscale.com/types/logger"
"tailscale.com/util/cibuild"
)
type DeliveredEvent struct {
Event any
From *Client
To *Client
}
// subscriber is a uniformly typed wrapper around Subscriber[T], so
// that debugging facilities can look at active subscribers.
type subscriber interface {
subscribeType() reflect.Type
// dispatch is a function that dispatches the head value in vals to
// a subscriber, while also handling stop and incoming queue write
// events.
//
// dispatch exists because of the strongly typed Subscriber[T]
// wrapper around subscriptions: within the bus events are boxed in an
// 'any', and need to be unpacked to their full type before delivery
// to the subscriber. This involves writing to a strongly-typed
// channel, so subscribeState cannot handle that dispatch by itself -
// but if that strongly typed send blocks, we also need to keep
// processing other potential sources of wakeups, which is how we end
// up at this awkward type signature and sharing of internal state
// through dispatch.
dispatch(ctx context.Context, vals *queue[DeliveredEvent], acceptCh func() chan DeliveredEvent, snapshot chan chan []DeliveredEvent) bool
Close()
}
// subscribeState handles dispatching of events received from a Bus.
type subscribeState struct {
client *Client
dispatcher *worker
write chan DeliveredEvent
snapshot chan chan []DeliveredEvent
debug hook[DeliveredEvent]
outputsMu syncs.Mutex
outputs map[reflect.Type]subscriber
}
func newSubscribeState(c *Client) *subscribeState {
ret := &subscribeState{
client: c,
write: make(chan DeliveredEvent),
snapshot: make(chan chan []DeliveredEvent),
outputs: map[reflect.Type]subscriber{},
}
ret.dispatcher = runWorker(ret.pump)
return ret
}
func (s *subscribeState) pump(ctx context.Context) {
var vals queue[DeliveredEvent]
acceptCh := func() chan DeliveredEvent {
if vals.Full() {
return nil
}
return s.write
}
for {
if !vals.Empty() {
val := vals.Peek()
sub := s.subscriberFor(val.Event)
if sub == nil {
// Raced with unsubscribe.
vals.Drop()
continue
}
if !sub.dispatch(ctx, &vals, acceptCh, s.snapshot) {
return
}
if s.debug.active() {
s.debug.run(DeliveredEvent{
Event: val.Event,
From: val.From,
To: s.client,
})
}
} else {
// Keep the cases in this select in sync with
// Subscriber.dispatch and SubscriberFunc.dispatch below.
// The only difference should be that this select doesn't deliver
// queued values to anyone, and unconditionally accepts new values.
select {
case val := <-s.write:
vals.Add(val)
case <-ctx.Done():
return
case ch := <-s.snapshot:
ch <- vals.Snapshot()
}
}
}
}
func (s *subscribeState) snapshotQueue() []DeliveredEvent {
if s == nil {
return nil
}
resp := make(chan []DeliveredEvent)
select {
case s.snapshot <- resp:
return <-resp
case <-s.dispatcher.Done():
return nil
}
}
func (s *subscribeState) subscribeTypes() []reflect.Type {
if s == nil {
return nil
}
s.outputsMu.Lock()
defer s.outputsMu.Unlock()
ret := make([]reflect.Type, 0, len(s.outputs))
for t := range s.outputs {
ret = append(ret, t)
}
return ret
}
func (s *subscribeState) addSubscriber(sub subscriber) {
s.outputsMu.Lock()
defer s.outputsMu.Unlock()
t := sub.subscribeType()
if s.outputs[t] != nil {
panic(fmt.Errorf("double subscription for event %s", t))
}
s.outputs[t] = sub
s.client.addSubscriber(t, s)
}
func (s *subscribeState) deleteSubscriber(t reflect.Type) {
s.outputsMu.Lock()
defer s.outputsMu.Unlock()
delete(s.outputs, t)
s.client.deleteSubscriber(t, s)
}
func (s *subscribeState) subscriberFor(val any) subscriber {
s.outputsMu.Lock()
defer s.outputsMu.Unlock()
return s.outputs[reflect.TypeOf(val)]
}
// Close closes the subscribeState. It implicitly closes all Subscribers
// linked to this state, and any pending events are discarded.
func (s *subscribeState) close() {
s.dispatcher.StopAndWait()
var subs map[reflect.Type]subscriber
s.outputsMu.Lock()
subs, s.outputs = s.outputs, nil
s.outputsMu.Unlock()
for _, sub := range subs {
sub.Close()
}
}
func (s *subscribeState) closed() <-chan struct{} {
return s.dispatcher.Done()
}
// A Subscriber delivers one type of event from a [Client].
// Events are sent to the [Subscriber.Events] channel.
type Subscriber[T any] struct {
// core holds the non-generic subscriber-interface implementation
// (Close, subscribeType, dispatch, slow timer, unregister) shared
// with [SubscriberFunc] via [subscriberCore]. The only per-T state
// owned by the facade itself is the typed delivery channel; the
// dispatch loop, unlike SubscriberFunc, must remain per-T — see
// [Subscriber.dispatchTyped].
core *subscriberCore
read chan T
}
func newSubscriber[T any](r *subscribeState, logf logger.Logf) *Subscriber[T] {
core := newSubscriberCore(r, logf, reflect.TypeFor[T]())
s := &Subscriber[T]{
core: core,
read: make(chan T),
}
// Subscriber[T] keeps a per-T dispatch loop; see [Subscriber.dispatchTyped]
// for why we don't share the non-generic dispatchFunc that SubscriberFunc
// uses.
core.dispatchFn = func(
ctx context.Context,
vals *queue[DeliveredEvent],
acceptCh func() chan DeliveredEvent,
snapshot chan chan []DeliveredEvent,
) bool {
return s.dispatchTyped(ctx, vals, acceptCh, snapshot)
}
return s
}
// dispatchTyped is the per-T dispatch loop for Subscriber[T]. It has to remain
// generic because the typed channel send `case s.read <- t:` must appear
// lexically inside the select. The rest of the cases match the non-generic
// dispatchFunc body to keep behavior aligned between Subscriber and
// SubscriberFunc.
//
// We don't share dispatchFunc (the way SubscriberFunc does) because bridging
// the typed channel send and the non-generic select would require running the
// send on its own goroutine on every event delivery. That bridge was measured
// at ~2.7x throughput regression on BenchmarkBasicThroughput, so we keep
// dispatchTyped generic and pay the per-shape stencil cost instead (measured
// at ~1,600 B body + ~1,100 B pclntab per shape on linux/amd64 tailscaled).
// Only the typed select lives in the per-shape stencil; the surrounding state
// (slow timer, log function, type name) is reached through the non-generic
// core.
func (s *Subscriber[T]) dispatchTyped(
ctx context.Context,
vals *queue[DeliveredEvent],
acceptCh func() chan DeliveredEvent,
snapshot chan chan []DeliveredEvent,
) bool {
t := vals.Peek().Event.(T)
start := time.Now()
s.core.slow.Reset(slowSubscriberTimeout)
defer s.core.slow.Stop()
for {
// Keep the cases in this select in sync with subscribeState.pump
// above. The only difference should be that this select
// delivers a value on s.read.
select {
case s.read <- t:
vals.Drop()
return true
case val := <-acceptCh():
vals.Add(val)
case <-ctx.Done():
return false
case ch := <-snapshot:
ch <- vals.Snapshot()
case <-s.core.slow.C:
s.core.logf("subscriber for %s is slow (%v elapsed)", s.core.typeName, time.Since(start))
s.core.slow.Reset(slowSubscriberTimeout)
}
}
}
func newMonitor[T any](attach func(fn func(T)) (cancel func())) *Subscriber[T] {
s := &Subscriber[T]{
// Monitors don't go through the bus's dispatch path (they
// are attached directly to the debug hook), so they don't
// need a fully-initialized subscriberCore — only the typed
// delivery channel and an unregister callback. We give them
// a placeholder core so Close() and Done() work uniformly.
core: &subscriberCore{},
read: make(chan T, 100), // arbitrary, large
}
cancel := attach(s.monitor)
s.core.unregister = func(reflect.Type) { cancel() }
return s
}
func (s *Subscriber[T]) monitor(debugEvent T) {
select {
case s.read <- debugEvent:
case <-s.core.stop.Done():
}
}
// Events returns a channel on which the subscriber's events are
// delivered.
func (s *Subscriber[T]) Events() <-chan T {
return s.read
}
// Done returns a channel that is closed when the subscriber is
// closed.
func (s *Subscriber[T]) Done() <-chan struct{} {
return s.core.stop.Done()
}
// Close closes the Subscriber, indicating the caller no longer wishes
// to receive this event type. After Close, receives on
// [Subscriber.Events] block for ever.
//
// If the Bus from which the Subscriber was created is closed,
// the Subscriber is implicitly closed and does not need to be closed
// separately.
func (s *Subscriber[T]) Close() { s.core.Close() }
// A SubscriberFunc delivers one type of event from a [Client].
// Events are forwarded synchronously to a function provided at construction.
type SubscriberFunc[T any] struct {
// core holds the non-generic subscriber-interface implementation shared
// with [Subscriber] via [subscriberCore]. The user callback is captured
// in the dispatchFn closure on the core, so SubscriberFunc[T] itself
// carries no per-T state beyond the core pointer; per-T cost is limited
// to the small forwarding Close method below.
core *subscriberCore
}
// subscriberCore is the non-generic backing for both Subscriber[T] and
// SubscriberFunc[T]. It implements the package-private subscriber interface
// so that the bus (and the subscribeState map) can store it without per-T
// itabs or dictionaries. The per-T behavior (type assertion plus either typed
// channel send or user callback invocation) is encapsulated in the dispatchFn
// closure set up by the constructor of the typed facade.
type subscriberCore struct {
stop stopFlag
unregister func(reflect.Type)
logf logger.Logf
slow *time.Timer // used to detect slow subscriber service
// typ is the cached reflect.Type of T. Returned by
// subscribeType() and used by the dispatch closure to format
// slow-subscriber log messages.
typ reflect.Type
// typeName is the cached reflect.TypeFor[T]().String() result.
// Computed once at construction time so the dispatch closure
// (which runs once per delivered event) doesn't allocate a
// fresh string on every call. The string is also independent
// of T, so it doesn't contribute to per-T stencil cost.
typeName string
// dispatchFn is the per-T dispatch closure. It performs the type
// assertion vals.Peek().Event.(T) and runs the typed delivery (either a
// user-callback invocation for SubscriberFunc[T] or a typed channel send
// for Subscriber[T]). The closure body is non-generic apart from those
// two T-bound operations; the bulk of the dispatch work happens in the
// non-generic dispatchFunc helper (used by SubscriberFunc) or in the
// Subscriber[T].dispatchTyped per-shape stencil.
dispatchFn func(
ctx context.Context,
vals *queue[DeliveredEvent],
acceptCh func() chan DeliveredEvent,
snapshot chan chan []DeliveredEvent,
) bool
}
func newSubscriberFunc[T any](r *subscribeState, f func(T), logf logger.Logf) *SubscriberFunc[T] {
core := newSubscriberCore(r, logf, reflect.TypeFor[T]())
// The dispatch closure is the only piece that intrinsically
// needs T: it performs the type assertion on the head queue
// value and forwards the unboxed value to the user callback.
// All non-generic setup (timer, core allocation, unregister
// closure) lives in newSubscriberCore so it isn't
// duplicated per T.
core.dispatchFn = func(
ctx context.Context,
vals *queue[DeliveredEvent],
acceptCh func() chan DeliveredEvent,
snapshot chan chan []DeliveredEvent,
) bool {
t := vals.Peek().Event.(T)
callDone := make(chan struct{})
// `go runFuncCallback(f, t, callDone)` binds its arguments
// directly to the new goroutine's frame; using a closure
// (`go func() { f(t) }()`) would allocate a closure on the
// heap on every dispatched event.
go runFuncCallback(f, t, callDone)
return dispatchFunc(ctx, core, vals, acceptCh, snapshot, callDone)
}
return &SubscriberFunc[T]{core: core}
}
// newSubscriberCore performs the non-generic portion of subscriber
// construction: timer setup, core struct allocation, and assignment of the
// unregister method-value. The caller fills in the per-T dispatchFn
// afterward.
//
// Hoisting this out of the typed constructors (newSubscriber[T] and
// newSubscriberFunc[T]) eliminates the bulk of their per-T stencil cost; the
// only T-typed instructions left in each generic constructor are the
// reflect.TypeFor[T]() call (whose body is shared via the
// internal/abi.TypeFor[T] dictionary) and the construction of the dispatch
// closure itself.
func newSubscriberCore(r *subscribeState, logf logger.Logf, typ reflect.Type) *subscriberCore {
slow := time.NewTimer(0)
slow.Stop() // reset in dispatch
core := &subscriberCore{
logf: logf,
slow: slow,
typ: typ,
typeName: typ.String(),
}
core.unregister = r.deleteSubscriber
return core
}
// Close closes the SubscriberFunc, indicating the caller no longer wishes to
// receive this event type. After Close, no further events will be passed to
// the callback.
//
// If the [Bus] from which s was created is closed, s is implicitly closed and
// does not need to be closed separately.
func (s *SubscriberFunc[T]) Close() { s.core.Close() }
// Close implements the subscriber interface and the user-facing Close on
// both Subscriber[T] and SubscriberFunc[T].
func (c *subscriberCore) Close() {
c.stop.Stop()
c.unregister(c.typ)
}
// subscribeType implements the subscriber interface.
func (c *subscriberCore) subscribeType() reflect.Type { return c.typ }
// dispatch implements the subscriber interface by invoking the
// per-T dispatch closure that was captured at construction time.
func (c *subscriberCore) dispatch(
ctx context.Context,
vals *queue[DeliveredEvent],
acceptCh func() chan DeliveredEvent,
snapshot chan chan []DeliveredEvent,
) bool {
return c.dispatchFn(ctx, vals, acceptCh, snapshot)
}
// dispatchFunc is the non-generic body of SubscriberFunc[T].dispatch.
// It is identical in observable behavior to the original loop; the
// only differences are that the dispatched value has already been
// unboxed by the caller (and the user callback is already running
// on its own goroutine, signaling completion via callDone) and the
// slow-subscriber timer / cached type name come from the
// non-generic core, not from a per-T struct.
//
// callDone is closed by runFuncCallback when the user callback returns.
func dispatchFunc(
ctx context.Context,
core *subscriberCore,
vals *queue[DeliveredEvent],
acceptCh func() chan DeliveredEvent,
snapshot chan chan []DeliveredEvent,
callDone chan struct{},
) bool {
start := time.Now()
core.slow.Reset(slowSubscriberTimeout)
defer core.slow.Stop()
// Keep the cases in this select in sync with subscribeState.pump
// above. The only difference should be that this select
// delivers a value by calling the user callback (via the
// goroutine spawned by the typed wrapper).
for {
select {
case <-callDone:
vals.Drop()
return true
case val := <-acceptCh():
vals.Add(val)
case <-ctx.Done():
// Wait for the callback to be complete, but not forever.
core.slow.Reset(5 * slowSubscriberTimeout)
select {
case <-core.slow.C:
core.logf("giving up on subscriber for %s after %v at close", core.typeName, time.Since(start))
if cibuild.On() {
all := make([]byte, 2<<20)
n := runtime.Stack(all, true)
core.logf("goroutine stacks:\n%s", all[:n])
}
case <-callDone:
}
return false
case ch := <-snapshot:
ch <- vals.Snapshot()
case <-core.slow.C:
core.logf("subscriber for %s is slow (%v elapsed)", core.typeName, time.Since(start))
core.slow.Reset(slowSubscriberTimeout)
}
}
}
// runFuncCallback runs f(t) and closes done when it returns. It is
// the per-T worker spawned as a goroutine for each dispatched
// event. Keeping it as a regular generic function (rather than a
// closure) means `go runFuncCallback(f, t, done)` binds its
// arguments to the goroutine's frame directly, with no per-event
// closure allocation. The body is small (defer + one indirect
// call), so the per-shape stencil cost is minimal.
func runFuncCallback[T any](f func(T), t T, done chan struct{}) {
defer close(done)
f(t)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package hashx provides a concrete implementation of [hash.Hash]
// that operates on a particular block size.
package hashx
import (
"encoding/binary"
"fmt"
"hash"
"unsafe"
)
var _ hash.Hash = (*Block512)(nil)
// Block512 wraps a [hash.Hash] for functions that operate on 512-bit block sizes.
// It has efficient methods for hashing fixed-width integers.
//
// A hashing algorithm that operates on 512-bit block sizes should be used.
// The hash still operates correctly even with misaligned block sizes,
// but operates less efficiently.
//
// Example algorithms with 512-bit block sizes include:
// - MD4 (https://golang.org/x/crypto/md4)
// - MD5 (https://golang.org/pkg/crypto/md5)
// - BLAKE2s (https://golang.org/x/crypto/blake2s)
// - BLAKE3
// - RIPEMD (https://golang.org/x/crypto/ripemd160)
// - SHA-0
// - SHA-1 (https://golang.org/pkg/crypto/sha1)
// - SHA-2 (https://golang.org/pkg/crypto/sha256)
// - Whirlpool
//
// See https://en.wikipedia.org/wiki/Comparison_of_cryptographic_hash_functions#Parameters
// for a list of hash functions and their block sizes.
//
// Block512 assumes that [hash.Hash.Write] never fails and
// never allows the provided buffer to escape.
type Block512 struct {
hash.Hash
x [512 / 8]byte
nx int
}
// New512 constructs a new Block512 that wraps h.
//
// It reports an error if the block sizes do not match.
// Misaligned block sizes perform poorly, but execute correctly.
// The error may be ignored if performance is not a concern.
func New512(h hash.Hash) (*Block512, error) {
b := &Block512{Hash: h}
if len(b.x)%h.BlockSize() != 0 {
return b, fmt.Errorf("hashx.Block512: inefficient use of hash.Hash with %d-bit block size", 8*h.BlockSize())
}
return b, nil
}
// Write hashes the contents of b.
func (h *Block512) Write(b []byte) (int, error) {
h.HashBytes(b)
return len(b), nil
}
// Sum appends the current hash to b and returns the resulting slice.
//
// It flushes any partially completed blocks to the underlying [hash.Hash],
// which may cause future operations to be misaligned and less efficient
// until [Block512.Reset] is called.
func (h *Block512) Sum(b []byte) []byte {
if h.nx > 0 {
h.Hash.Write(h.x[:h.nx])
h.nx = 0
}
// Unfortunately hash.Hash.Sum always causes the input to escape since
// escape analysis cannot prove anything past an interface method call.
// Assuming h already escapes, we call Sum with h.x first,
// and then copy the result to b.
sum := h.Hash.Sum(h.x[:0])
return append(b, sum...)
}
// Reset resets Block512 to its initial state.
// It recursively resets the underlying [hash.Hash].
func (h *Block512) Reset() {
h.Hash.Reset()
h.nx = 0
}
// HashUint8 hashes n as a 1-byte integer.
func (h *Block512) HashUint8(n uint8) {
// NOTE: This method is carefully written to be inlineable.
if h.nx <= len(h.x)-1 {
h.x[h.nx] = n
h.nx += 1
} else {
h.hashUint8Slow(n) // mark "noinline" to keep this within inline budget
}
}
//go:noinline
func (h *Block512) hashUint8Slow(n uint8) { h.hashUint(uint64(n), 1) }
// HashUint16 hashes n as a 2-byte little-endian integer.
func (h *Block512) HashUint16(n uint16) {
// NOTE: This method is carefully written to be inlineable.
if h.nx <= len(h.x)-2 {
binary.LittleEndian.PutUint16(h.x[h.nx:], n)
h.nx += 2
} else {
h.hashUint16Slow(n) // mark "noinline" to keep this within inline budget
}
}
//go:noinline
func (h *Block512) hashUint16Slow(n uint16) { h.hashUint(uint64(n), 2) }
// HashUint32 hashes n as a 4-byte little-endian integer.
func (h *Block512) HashUint32(n uint32) {
// NOTE: This method is carefully written to be inlineable.
if h.nx <= len(h.x)-4 {
binary.LittleEndian.PutUint32(h.x[h.nx:], n)
h.nx += 4
} else {
h.hashUint32Slow(n) // mark "noinline" to keep this within inline budget
}
}
//go:noinline
func (h *Block512) hashUint32Slow(n uint32) { h.hashUint(uint64(n), 4) }
// HashUint64 hashes n as a 8-byte little-endian integer.
func (h *Block512) HashUint64(n uint64) {
// NOTE: This method is carefully written to be inlineable.
if h.nx <= len(h.x)-8 {
binary.LittleEndian.PutUint64(h.x[h.nx:], n)
h.nx += 8
} else {
h.hashUint64Slow(n) // mark "noinline" to keep this within inline budget
}
}
//go:noinline
func (h *Block512) hashUint64Slow(n uint64) { h.hashUint(uint64(n), 8) }
func (h *Block512) hashUint(n uint64, i int) {
for ; i > 0; i-- {
if h.nx == len(h.x) {
h.Hash.Write(h.x[:])
h.nx = 0
}
h.x[h.nx] = byte(n)
h.nx += 1
n >>= 8
}
}
// HashBytes hashes the contents of b.
// It does not explicitly hash the length separately.
func (h *Block512) HashBytes(b []byte) {
// Nearly identical to sha256.digest.Write.
if h.nx > 0 {
n := copy(h.x[h.nx:], b)
h.nx += n
if h.nx == len(h.x) {
h.Hash.Write(h.x[:])
h.nx = 0
}
b = b[n:]
}
if len(b) >= len(h.x) {
n := len(b) &^ (len(h.x) - 1) // n is a multiple of len(h.x)
h.Hash.Write(b[:n])
b = b[n:]
}
if len(b) > 0 {
h.nx = copy(h.x[:], b)
}
}
// HashString hashes the contents of s.
// It does not explicitly hash the length separately.
func (h *Block512) HashString(s string) {
// TODO: Avoid unsafe when standard hashers implement io.StringWriter.
// See https://go.dev/issue/38776.
type stringHeader struct {
p unsafe.Pointer
n int
}
p := (*stringHeader)(unsafe.Pointer(&s))
b := unsafe.Slice((*byte)(p.p), p.n)
h.HashBytes(b)
}
// TODO: Add Hash.MarshalBinary and Hash.UnmarshalBinary?
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package lineiter iterates over lines in things.
package lineiter
import (
"bufio"
"bytes"
"io"
"iter"
"os"
"tailscale.com/types/result"
)
// File returns an iterator that reads lines from the named file.
//
// The returned substrings don't include the trailing newline.
// Lines may be empty.
func File(name string) iter.Seq[result.Of[[]byte]] {
f, err := os.Open(name)
return reader(f, f, err)
}
// Bytes returns an iterator over the lines in bs.
// The returned substrings don't include the trailing newline.
// Lines may be empty.
func Bytes(bs []byte) iter.Seq[[]byte] {
return func(yield func([]byte) bool) {
for len(bs) > 0 {
i := bytes.IndexByte(bs, '\n')
if i < 0 {
yield(bs)
return
}
if !yield(bs[:i]) {
return
}
bs = bs[i+1:]
}
}
}
// Reader returns an iterator over the lines in r.
//
// The returned substrings don't include the trailing newline.
// Lines may be empty.
func Reader(r io.Reader) iter.Seq[result.Of[[]byte]] {
return reader(r, nil, nil)
}
func reader(r io.Reader, c io.Closer, err error) iter.Seq[result.Of[[]byte]] {
return func(yield func(result.Of[[]byte]) bool) {
if err != nil {
yield(result.Error[[]byte](err))
return
}
if c != nil {
defer c.Close()
}
bs := bufio.NewScanner(r)
for bs.Scan() {
if !yield(result.Value(bs.Bytes())) {
return
}
}
if err := bs.Err(); err != nil {
yield(result.Error[[]byte](err))
}
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package mak helps make maps. It contains generic helpers to make/assign
// things, notably to maps, but also slices.
package mak
// Set populates an entry in a map, making the map if necessary.
//
// That is, it assigns (*m)[k] = v, making *m if it was nil.
func Set[K comparable, V any, T ~map[K]V](m *T, k K, v V) {
if *m == nil {
*m = make(map[K]V)
}
(*m)[k] = v
}
// NonNilSliceForJSON makes sure that *slicePtr is non-nil so it will
// won't be omitted from JSON serialization and possibly confuse JavaScript
// clients expecting it to be present.
func NonNilSliceForJSON[T any, S ~[]T](slicePtr *S) {
if *slicePtr != nil {
return
}
*slicePtr = make([]T, 0)
}
// NonNilMapForJSON makes sure that *slicePtr is non-nil so it will
// won't be omitted from JSON serialization and possibly confuse JavaScript
// clients expecting it to be present.
func NonNilMapForJSON[K comparable, V any, M ~map[K]V](mapPtr *M) {
if *mapPtr != nil {
return
}
*mapPtr = make(M)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package must assists in calling functions that must succeed.
//
// Example usage:
//
// var target = must.Get(url.Parse(...))
// must.Do(close())
package must
// Do panics if err is non-nil.
func Do(err error) {
if err != nil {
panic(err)
}
}
// Get returns v as is. It panics if err is non-nil.
func Get[T any](v T, err error) T {
if err != nil {
panic(err)
}
return v
}
// Get2 returns v1 and v2 as is. It panics if err is non-nil.
func Get2[T any, U any](v1 T, v2 U, err error) (T, U) {
if err != nil {
panic(err)
}
return v1, v2
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package nocasemaps provides efficient functions to set and get entries in Go
// maps keyed by a string, where the string is always lower-case.
package nocasemaps
import (
"unicode"
"unicode/utf8"
)
// TODO(https://github.com/golang/go/discussions/54245):
// Define a generic Map type instead. The main reason to avoid that is because
// there is currently no convenient API for iteration.
// An opaque Map type would force callers to interact with the map through
// the methods, preventing accidental interactions with the underlying map
// without using functions in this package.
const stackArraySize = 32
// Get is equivalent to:
//
// v := m[strings.ToLower(k)]
func Get[K ~string, V any](m map[K]V, k K) V {
if isLowerASCII(string(k)) {
return m[k]
}
var a [stackArraySize]byte
return m[K(appendToLower(a[:0], string(k)))]
}
// GetOk is equivalent to:
//
// v, ok := m[strings.ToLower(k)]
func GetOk[K ~string, V any](m map[K]V, k K) (V, bool) {
if isLowerASCII(string(k)) {
v, ok := m[k]
return v, ok
}
var a [stackArraySize]byte
v, ok := m[K(appendToLower(a[:0], string(k)))]
return v, ok
}
// Set is equivalent to:
//
// m[strings.ToLower(k)] = v
func Set[K ~string, V any](m map[K]V, k K, v V) {
if isLowerASCII(string(k)) {
m[k] = v
return
}
// TODO(https://go.dev/issues/55930): This currently always allocates.
// An optimization to the compiler and runtime could make this allocate-free
// in the event that we are overwriting a map entry.
//
// Alternatively, we could use string interning.
// See an example intern data structure, see:
// https://github.com/go-json-experiment/json/blob/master/intern.go
var a [stackArraySize]byte
m[K(appendToLower(a[:0], string(k)))] = v
}
// Delete is equivalent to:
//
// delete(m, strings.ToLower(k))
func Delete[K ~string, V any](m map[K]V, k K) {
if isLowerASCII(string(k)) {
delete(m, k)
return
}
var a [stackArraySize]byte
delete(m, K(appendToLower(a[:0], string(k))))
}
// AppendSliceElem is equivalent to:
//
// append(m[strings.ToLower(k)], v)
func AppendSliceElem[K ~string, S []E, E any](m map[K]S, k K, vs ...E) {
// if the key is already lowercased
if isLowerASCII(string(k)) {
m[k] = append(m[k], vs...)
return
}
// if key needs to become lowercase, uses appendToLower
var a [stackArraySize]byte
s := appendToLower(a[:0], string(k))
m[K(s)] = append(m[K(s)], vs...)
}
func isLowerASCII(s string) bool {
for i := range len(s) {
if c := s[i]; c >= utf8.RuneSelf || ('A' <= c && c <= 'Z') {
return false
}
}
return true
}
func appendToLower(b []byte, s string) []byte {
for i := 0; i < len(s); i++ {
switch c := s[i]; {
case 'A' <= c && c <= 'Z':
b = append(b, c+('a'-'A'))
case c < utf8.RuneSelf:
b = append(b, c)
default:
r, n := utf8.DecodeRuneInString(s[i:])
b = utf8.AppendRune(b, unicode.ToLower(r))
i += n - 1 // -1 to compensate for i++ in loop advancement
}
}
return b
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package race contains a helper to "race" two functions, returning the first
// successful result. It also allows explicitly triggering the
// (possibly-waiting) second function when the first function returns an error
// or indicates that it should be retried.
package race
import (
"context"
"errors"
"time"
)
type resultType int
const (
first resultType = iota
second
)
// queryResult is an internal type for storing the result of a function call
type queryResult[T any] struct {
ty resultType
res T
err error
}
// Func is the signature of a function to be called.
type Func[T any] func(context.Context) (T, error)
// Race allows running two functions concurrently and returning the first
// non-error result returned.
type Race[T any] struct {
func1, func2 Func[T]
d time.Duration
results chan queryResult[T]
startFallback chan struct{}
}
// New creates a new Race that, when Start is called, will immediately call
// func1 to obtain a result. After the timeout d or if triggered by an error
// response from func1, func2 will be called.
func New[T any](d time.Duration, func1, func2 Func[T]) *Race[T] {
ret := &Race[T]{
func1: func1,
func2: func2,
d: d,
results: make(chan queryResult[T], 2),
startFallback: make(chan struct{}),
}
return ret
}
// Start will start the "race" process, returning the first non-error result or
// the errors that occurred when calling func1 and/or func2.
func (rh *Race[T]) Start(ctx context.Context) (T, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// func1 is started immediately
go func() {
ret, err := rh.func1(ctx)
rh.results <- queryResult[T]{first, ret, err}
}()
// func2 is started after a timeout
go func() {
wait := time.NewTimer(rh.d)
defer wait.Stop()
// Wait for our timeout, trigger, or context to finish.
select {
case <-ctx.Done():
// Nothing to do; we're done
var zero T
rh.results <- queryResult[T]{second, zero, ctx.Err()}
return
case <-rh.startFallback:
case <-wait.C:
}
ret, err := rh.func2(ctx)
rh.results <- queryResult[T]{second, ret, err}
}()
// For each possible result, get it off the channel.
var errs []error
for range 2 {
res := <-rh.results
// If this was an error, store it and hope that the other
// result gives us something.
if res.err != nil {
errs = append(errs, res.err)
// Start the fallback function immediately if this is
// the first function's error, to avoid having
// to wait.
if res.ty == first {
close(rh.startFallback)
}
continue
}
// Got a valid response! Return it.
return res.res, nil
}
// If we get here, both raced functions failed. Return whatever errors
// we have, joined together.
var zero T
return zero, errors.Join(errs...)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package rands
import (
"math/bits"
randv2 "math/rand/v2"
)
// Shuffle is like rand.Shuffle, but it does not allocate or lock any RNG state.
func Shuffle[T any](seed uint64, data []T) {
var pcg randv2.PCG
pcg.Seed(seed, seed)
for i := len(data) - 1; i > 0; i-- {
j := int(uint64n(&pcg, uint64(i+1)))
data[i], data[j] = data[j], data[i]
}
}
// IntN is like rand.IntN, but it is seeded on the stack and does not allocate
// or lock any RNG state.
func IntN(seed uint64, n int) int {
var pcg randv2.PCG
pcg.Seed(seed, seed)
return int(uint64n(&pcg, uint64(n)))
}
// Perm is like rand.Perm, but it is seeded on the stack and does not allocate
// or lock any RNG state.
func Perm(seed uint64, n int) []int {
p := make([]int, n)
for i := range p {
p[i] = i
}
Shuffle(seed, p)
return p
}
// uint64n is the no-bounds-checks version of rand.Uint64N from the standard
// library. 32-bit optimizations have been elided.
func uint64n(pcg *randv2.PCG, n uint64) uint64 {
if n&(n-1) == 0 { // n is power of two, can mask
return pcg.Uint64() & (n - 1)
}
// Suppose we have a uint64 x uniform in the range [0,2⁶⁴)
// and want to reduce it to the range [0,n) preserving exact uniformity.
// We can simulate a scaling arbitrary precision x * (n/2⁶⁴) by
// the high bits of a double-width multiply of x*n, meaning (x*n)/2⁶⁴.
// Since there are 2⁶⁴ possible inputs x and only n possible outputs,
// the output is necessarily biased if n does not divide 2⁶⁴.
// In general (x*n)/2⁶⁴ = k for x*n in [k*2⁶⁴,(k+1)*2⁶⁴).
// There are either floor(2⁶⁴/n) or ceil(2⁶⁴/n) possible products
// in that range, depending on k.
// But suppose we reject the sample and try again when
// x*n is in [k*2⁶⁴, k*2⁶⁴+(2⁶⁴%n)), meaning rejecting fewer than n possible
// outcomes out of the 2⁶⁴.
// Now there are exactly floor(2⁶⁴/n) possible ways to produce
// each output value k, so we've restored uniformity.
// To get valid uint64 math, 2⁶⁴ % n = (2⁶⁴ - n) % n = -n % n,
// so the direct implementation of this algorithm would be:
//
// hi, lo := bits.Mul64(r.Uint64(), n)
// thresh := -n % n
// for lo < thresh {
// hi, lo = bits.Mul64(r.Uint64(), n)
// }
//
// That still leaves an expensive 64-bit division that we would rather avoid.
// We know that thresh < n, and n is usually much less than 2⁶⁴, so we can
// avoid the last four lines unless lo < n.
//
// See also:
// https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction
// https://lemire.me/blog/2016/06/30/fast-random-shuffling
hi, lo := bits.Mul64(pcg.Uint64(), n)
if lo < n {
thresh := -n % n
for lo < thresh {
hi, lo = bits.Mul64(pcg.Uint64(), n)
}
}
return hi
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package rands contains utility functions for randomness.
package rands
import (
crand "crypto/rand"
"encoding/hex"
)
// HexString returns a string of n cryptographically random lowercase
// hex characters.
//
// That is, HexString(3) returns something like "0fc", containing 12
// bits of randomness.
func HexString(n int) string {
nb := n / 2
if n%2 == 1 {
nb++
}
b := make([]byte, nb)
crand.Read(b)
return hex.EncodeToString(b)[:n]
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package safediff computes the difference between two lists.
//
// It is guaranteed to run in O(n), but may not produce an optimal diff.
// Most diffing algorithms produce optimal diffs but run in O(n²).
// It is safe to pass in untrusted input.
package safediff
import (
"bytes"
"fmt"
"math"
"strings"
"unicode"
"github.com/google/go-cmp/cmp"
)
var diffTest = false
// Lines constructs a humanly readable line-by-line diff from x to y.
// The output (if multiple lines) is guaranteed to be no larger than maxSize,
// by truncating the output if necessary. A negative maxSize enforces no limit.
//
// Example diff:
//
// … 440 identical lines
// "ssh": [
// … 35 identical lines
// {
// - "src": ["maisem@tailscale.com"],
// - "dst": ["tag:maisem-test"],
// - "users": ["maisem", "root"],
// - "action": "check",
// - // "recorder": ["100.12.34.56:80"],
// + "src": ["maisem@tailscale.com"],
// + "dst": ["tag:maisem-test"],
// + "users": ["maisem", "root"],
// + "action": "check",
// + "recorder": ["node:recorder-2"],
// },
// … 77 identical lines
// ],
// … 345 identical lines
//
// Meaning of each line prefix:
//
// - '…' precedes a summary statement
// - ' ' precedes an identical line printed for context
// - '-' precedes a line removed from x
// - '+' precedes a line inserted from y
//
// The diffing algorithm runs in O(n) and is safe to use with untrusted inputs.
func Lines(x, y string, maxSize int) (out string, truncated bool) {
// Convert x and y into a slice of lines and compute the edit-script.
xs := strings.Split(x, "\n")
ys := strings.Split(y, "\n")
es := diffStrings(xs, ys)
// Modify the edit-script to support printing identical lines of context.
const identicalContext edit = '*' // special edit code to indicate printed line
var xi, yi int // index into xs or ys
isIdentical := func(e edit) bool { return e == identical || e == identicalContext }
indentOf := func(s string) string { return s[:len(s)-len(strings.TrimLeftFunc(s, unicode.IsSpace))] }
for i, e := range es {
if isIdentical(e) {
// Print current line if adjacent symbols are non-identical.
switch {
case i-1 >= 0 && !isIdentical(es[i-1]):
es[i] = identicalContext
case i+1 < len(es) && !isIdentical(es[i+1]):
es[i] = identicalContext
}
} else {
// Print any preceding or succeeding lines,
// where the leading indent is a prefix of the current indent.
// Indentation often indicates a parent-child relationship
// in structured source code.
addParents := func(ss []string, si, direction int) {
childIndent := indentOf(ss[si])
for j := direction; i+j >= 0 && i+j < len(es) && isIdentical(es[i+j]); j += direction {
parentIndent := indentOf(ss[si+j])
if strings.HasPrefix(childIndent, parentIndent) && len(parentIndent) < len(childIndent) && parentIndent != "" {
es[i+j] = identicalContext
childIndent = parentIndent
}
}
}
switch e {
case removed, modified: // arbitrarily use the x value for modified values
addParents(xs, xi, -1)
addParents(xs, xi, +1)
case inserted:
addParents(ys, yi, -1)
addParents(ys, yi, +1)
}
}
if e != inserted {
xi++
}
if e != removed {
yi++
}
}
// Show the line for a single hidden identical line,
// since it occupies the same vertical height.
for i, e := range es {
if e == identical {
prevNotIdentical := i-1 < 0 || es[i-1] != identical
nextNotIdentical := i+1 >= len(es) || es[i+1] != identical
if prevNotIdentical && nextNotIdentical {
es[i] = identicalContext
}
}
}
// Adjust the maxSize, reserving space for the final summary.
if maxSize < 0 {
maxSize = math.MaxInt
}
maxSize -= len(stats{len(xs) + len(ys), len(xs), len(ys)}.appendText(nil))
// mayAppendLine appends a line if it does not exceed maxSize.
// Otherwise, it just updates prevStats.
var buf []byte
var prevStats stats
mayAppendLine := func(edit edit, line string) {
// Append the stats (if non-zero) and the line text.
// The stats reports the number of preceding identical lines.
if !truncated {
bufLen := len(buf) // original length (in case we exceed maxSize)
if !prevStats.isZero() {
buf = prevStats.appendText(buf)
prevStats = stats{} // just printed, so clear the stats
}
buf = fmt.Appendf(buf, "%c %s\n", edit, line)
truncated = len(buf) > maxSize
if !truncated {
return
}
buf = buf[:bufLen] // restore original buffer contents
}
// Output is truncated, so just update the statistics.
switch edit {
case identical:
prevStats.numIdentical++
case removed:
prevStats.numRemoved++
case inserted:
prevStats.numInserted++
}
}
// Process the entire edit script.
for len(es) > 0 {
num := len(es) - len(bytes.TrimLeft(es, string(es[:1])))
switch es[0] {
case identical:
prevStats.numIdentical += num
xs, ys = xs[num:], ys[num:]
case identicalContext:
for n := len(xs) - num; len(xs) > n; xs, ys = xs[1:], ys[1:] {
mayAppendLine(identical, xs[0]) // implies xs[0] == ys[0]
}
case modified:
for n := len(xs) - num; len(xs) > n; xs = xs[1:] {
mayAppendLine(removed, xs[0])
}
for n := len(ys) - num; len(ys) > n; ys = ys[1:] {
mayAppendLine(inserted, ys[0])
}
case removed:
for n := len(xs) - num; len(xs) > n; xs = xs[1:] {
mayAppendLine(removed, xs[0])
}
case inserted:
for n := len(ys) - num; len(ys) > n; ys = ys[1:] {
mayAppendLine(inserted, ys[0])
}
}
es = es[num:]
}
if len(xs)+len(ys)+len(es) > 0 {
panic("BUG: slices not fully consumed")
}
if !prevStats.isZero() {
buf = prevStats.appendText(buf) // may exceed maxSize
}
return string(buf), truncated
}
type stats struct{ numIdentical, numRemoved, numInserted int }
func (s stats) isZero() bool { return s.numIdentical+s.numRemoved+s.numInserted == 0 }
func (s stats) appendText(b []byte) []byte {
switch {
case s.numIdentical > 0 && s.numRemoved > 0 && s.numInserted > 0:
return fmt.Appendf(b, "… %d identical, %d removed, and %d inserted lines\n", s.numIdentical, s.numRemoved, s.numInserted)
case s.numIdentical > 0 && s.numRemoved > 0:
return fmt.Appendf(b, "… %d identical and %d removed lines\n", s.numIdentical, s.numRemoved)
case s.numIdentical > 0 && s.numInserted > 0:
return fmt.Appendf(b, "… %d identical and %d inserted lines\n", s.numIdentical, s.numInserted)
case s.numRemoved > 0 && s.numInserted > 0:
return fmt.Appendf(b, "… %d removed and %d inserted lines\n", s.numRemoved, s.numInserted)
case s.numIdentical > 0:
return fmt.Appendf(b, "… %d identical lines\n", s.numIdentical)
case s.numRemoved > 0:
return fmt.Appendf(b, "… %d removed lines\n", s.numRemoved)
case s.numInserted > 0:
return fmt.Appendf(b, "… %d inserted lines\n", s.numInserted)
default:
return fmt.Appendf(b, "…\n")
}
}
// diffStrings computes an edit-script of two slices of strings.
//
// This calls cmp.Equal to access the "github.com/go-cmp/cmp/internal/diff"
// implementation, which has an O(N) diffing algorithm. It is not guaranteed
// to produce an optimal edit-script, but protects our runtime against
// adversarial inputs that would wreck the optimal O(N²) algorithm used by
// most diffing packages available in open-source.
//
// TODO(https://go.dev/issue/58893): Use "golang.org/x/tools/diff" instead?
func diffStrings(xs, ys []string) []edit {
d := new(diffRecorder)
cmp.Equal(xs, ys, cmp.Reporter(d))
if diffTest {
numRemoved := bytes.Count(d.script, []byte{removed})
numInserted := bytes.Count(d.script, []byte{inserted})
if len(xs) != len(d.script)-numInserted || len(ys) != len(d.script)-numRemoved {
panic("BUG: edit-script is inconsistent")
}
}
return d.script
}
type edit = byte
const (
identical edit = ' ' // equal symbol in both x and y
modified edit = '~' // modified symbol in both x and y
removed edit = '-' // removed symbol from x
inserted edit = '+' // inserted symbol from y
)
// diffRecorder reproduces an edit-script, essentially recording
// the edit-script from "github.com/google/go-cmp/cmp/internal/diff".
// This implements the cmp.Reporter interface.
type diffRecorder struct {
last cmp.PathStep
script []edit
}
func (d *diffRecorder) PushStep(ps cmp.PathStep) { d.last = ps }
func (d *diffRecorder) Report(rs cmp.Result) {
if si, ok := d.last.(cmp.SliceIndex); ok {
if rs.Equal() {
d.script = append(d.script, identical)
} else {
switch xi, yi := si.SplitKeys(); {
case xi >= 0 && yi >= 0:
d.script = append(d.script, modified)
case xi >= 0:
d.script = append(d.script, removed)
case yi >= 0:
d.script = append(d.script, inserted)
}
}
}
}
func (d *diffRecorder) PopStep() { d.last = nil }
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package set
// HandleSet is a set of T.
//
// It is not safe for concurrent use.
type HandleSet[T any] map[Handle]T
// Handle is an opaque comparable value that's used as the map key in a
// HandleSet.
type Handle struct {
v *byte
}
// NewHandle returns a new handle value.
func NewHandle() Handle {
return Handle{new(byte)}
}
// Add adds the element (map value) e to the set.
//
// It returns a new handle (map key) with which e can be removed, using a map
// delete or the [HandleSet.Delete] method.
func (s *HandleSet[T]) Add(e T) Handle {
h := NewHandle()
if *s == nil {
*s = make(HandleSet[T])
}
(*s)[h] = e
return h
}
// Delete removes the element with handle h from the set.
func (s HandleSet[T]) Delete(h Handle) { delete(s, h) }
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package set
import (
"iter"
"maps"
"math/bits"
"math/rand/v2"
"golang.org/x/exp/constraints"
"tailscale.com/util/mak"
)
// IntSet is a set optimized for integer values close to zero
// or set of integers that are close in value.
type IntSet[T constraints.Integer] struct {
// bits is a [bitSet] for numbers less than [bits.UintSize].
bits bitSet
// extra is a mapping of [bitSet] for numbers not in bits,
// where the key is a number modulo [bits.UintSize].
extra map[uint64]bitSet
// extraLen is the count of numbers in extra since len(extra)
// does not reflect that each bitSet may have multiple numbers.
extraLen int
}
// IntsOf constructs an [IntSet] with the provided elements.
func IntsOf[T constraints.Integer](slice ...T) IntSet[T] {
var s IntSet[T]
for _, e := range slice {
s.Add(e)
}
return s
}
// Values returns an iterator over the elements of the set.
// The iterator will yield the elements in no particular order.
func (s IntSet[T]) Values() iter.Seq[T] {
return func(yield func(T) bool) {
if s.bits != 0 {
for i := range s.bits.values() {
if !yield(decodeZigZag[T](i)) {
return
}
}
}
if s.extra != nil {
for hi, bs := range s.extra {
for lo := range bs.values() {
if !yield(decodeZigZag[T](hi*bits.UintSize + lo)) {
return
}
}
}
}
}
}
// Contains reports whether e is in the set.
func (s IntSet[T]) Contains(e T) bool {
if v := encodeZigZag(e); v < bits.UintSize {
return s.bits.contains(v)
} else {
hi, lo := v/uint64(bits.UintSize), v%uint64(bits.UintSize)
return s.extra[hi].contains(lo)
}
}
// Add adds e to the set.
//
// When storing a IntSet in a map as a value type,
// it is important to re-assign the map entry after calling Add or Delete,
// as the IntSet's representation may change.
func (s *IntSet[T]) Add(e T) {
if v := encodeZigZag(e); v < bits.UintSize {
s.bits.add(v)
} else {
hi, lo := v/uint64(bits.UintSize), v%uint64(bits.UintSize)
if bs := s.extra[hi]; !bs.contains(lo) {
bs.add(lo)
mak.Set(&s.extra, hi, bs)
s.extra[hi] = bs
s.extraLen++
}
}
}
// AddSeq adds the values from seq to the set.
func (s *IntSet[T]) AddSeq(seq iter.Seq[T]) {
for e := range seq {
s.Add(e)
}
}
// Len reports the number of elements in the set.
func (s IntSet[T]) Len() int {
return s.bits.len() + s.extraLen
}
// Delete removes e from the set.
//
// When storing a IntSet in a map as a value type,
// it is important to re-assign the map entry after calling Add or Delete,
// as the IntSet's representation may change.
func (s *IntSet[T]) Delete(e T) {
if v := encodeZigZag(e); v < bits.UintSize {
s.bits.delete(v)
} else {
hi, lo := v/uint64(bits.UintSize), v%uint64(bits.UintSize)
if bs := s.extra[hi]; bs.contains(lo) {
bs.delete(lo)
mak.Set(&s.extra, hi, bs)
s.extra[hi] = bs
s.extraLen--
}
}
}
// DeleteSeq deletes the values in seq from the set.
func (s *IntSet[T]) DeleteSeq(seq iter.Seq[T]) {
for e := range seq {
s.Delete(e)
}
}
// Equal reports whether s is equal to other.
func (s IntSet[T]) Equal(other IntSet[T]) bool {
for hi, bits := range s.extra {
if other.extra[hi] != bits {
return false
}
}
return s.extraLen == other.extraLen && s.bits == other.bits
}
// Clone returns a copy of s that doesn't alias the original.
func (s IntSet[T]) Clone() IntSet[T] {
return IntSet[T]{
bits: s.bits,
extra: maps.Clone(s.extra),
extraLen: s.extraLen,
}
}
type bitSet uint
func (s bitSet) values() iter.Seq[uint64] {
return func(yield func(uint64) bool) {
// Hyrum-proofing: randomly iterate in forwards or reverse.
if rand.Uint64()%2 == 0 {
for i := range bits.UintSize {
if s.contains(uint64(i)) && !yield(uint64(i)) {
return
}
}
} else {
for i := bits.UintSize; i >= 0; i-- {
if s.contains(uint64(i)) && !yield(uint64(i)) {
return
}
}
}
}
}
func (s bitSet) len() int { return bits.OnesCount(uint(s)) }
func (s bitSet) contains(i uint64) bool { return s&(1<<i) > 0 }
func (s *bitSet) add(i uint64) { *s |= 1 << i }
func (s *bitSet) delete(i uint64) { *s &= ^(1 << i) }
// encodeZigZag encodes an integer as an unsigned integer ensuring that
// negative integers near zero still have a near zero positive value.
// For unsigned integers, it returns the value verbatim.
func encodeZigZag[T constraints.Integer](v T) uint64 {
var zero T
if ^zero >= 0 { // must be constraints.Unsigned
return uint64(v)
} else { // must be constraints.Signed
// See [google.golang.org/protobuf/encoding/protowire.EncodeZigZag]
return uint64(int64(v)<<1) ^ uint64(int64(v)>>63)
}
}
// decodeZigZag decodes an unsigned integer as an integer ensuring that
// negative integers near zero still have a near zero positive value.
// For unsigned integers, it returns the value verbatim.
func decodeZigZag[T constraints.Integer](v uint64) T {
var zero T
if ^zero >= 0 { // must be constraints.Unsigned
return T(v)
} else { // must be constraints.Signed
// See [google.golang.org/protobuf/encoding/protowire.DecodeZigZag]
return T(int64(v>>1) ^ int64(v)<<63>>63)
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package set contains set types.
package set
import (
"encoding/json"
"iter"
"maps"
"reflect"
"sort"
"tailscale.com/types/views"
)
// Set is a set of T.
type Set[T comparable] map[T]struct{}
// SetOf returns a new set constructed from the elements in slice.
func SetOf[T comparable](slice []T) Set[T] {
return Of(slice...)
}
// Of returns a new set constructed from the elements in slice.
func Of[T comparable](slice ...T) Set[T] {
s := make(Set[T])
s.AddSlice(slice)
return s
}
// OfSliceView returns a new set constructed from the elements in v.
func OfSliceView[T comparable](v views.Slice[T]) Set[T] {
s := make(Set[T], v.Len())
s.AddSliceView(v)
return s
}
// Clone returns a new set cloned from the elements in s.
func (s Set[T]) Clone() Set[T] {
return maps.Clone(s)
}
// All returns an iterator over all elements in s.
// The iteration order is not specified
// and is not guaranteed to be the same from one call to the next.
func (s Set[T]) All() iter.Seq[T] {
return maps.Keys(s)
}
// Add adds e to s.
func (s Set[T]) Add(e T) { s[e] = struct{}{} }
// AddSeq adds each element of es to s.
func (s Set[T]) AddSeq(es iter.Seq[T]) {
for e := range es {
s.Add(e)
}
}
// AddSlice adds each element of es to s.
func (s Set[T]) AddSlice(es []T) {
for _, e := range es {
s.Add(e)
}
}
// AddSliceView adds each element of v to s.
func (s Set[T]) AddSliceView(v views.Slice[T]) {
for _, e := range v.All() {
s.Add(e)
}
}
// AddSet adds each element of es to s.
func (s Set[T]) AddSet(es Set[T]) {
for e := range es {
s.Add(e)
}
}
// Make lazily initializes the map pointed to by s to be non-nil.
func (s *Set[T]) Make() {
if *s == nil {
*s = make(Set[T])
}
}
// Slice returns the elements of the set as a slice. If the element type is
// ordered (integers, floats, or strings), the elements are returned in sorted
// order. Otherwise, the order is not defined.
func (s Set[T]) Slice() []T {
es := make([]T, 0, s.Len())
for k := range s {
es = append(es, k)
}
if f := genOrderedSwapper(reflect.TypeFor[T]()); f != nil {
sort.Slice(es, f(reflect.ValueOf(es)))
}
return es
}
// genOrderedSwapper returns a generator for a swap function that can be used to
// sort a slice of the given type. If rt is not an ordered type,
// genOrderedSwapper returns nil.
func genOrderedSwapper(rt reflect.Type) func(reflect.Value) func(i, j int) bool {
switch rt.Kind() {
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return func(rv reflect.Value) func(i, j int) bool {
return func(i, j int) bool {
return rv.Index(i).Uint() < rv.Index(j).Uint()
}
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return func(rv reflect.Value) func(i, j int) bool {
return func(i, j int) bool {
return rv.Index(i).Int() < rv.Index(j).Int()
}
}
case reflect.Float32, reflect.Float64:
return func(rv reflect.Value) func(i, j int) bool {
return func(i, j int) bool {
return rv.Index(i).Float() < rv.Index(j).Float()
}
}
case reflect.String:
return func(rv reflect.Value) func(i, j int) bool {
return func(i, j int) bool {
return rv.Index(i).String() < rv.Index(j).String()
}
}
}
return nil
}
// Delete removes e from the set.
func (s Set[T]) Delete(e T) { delete(s, e) }
// DeleteSeq removes all elements in es from the set.
func (s Set[T]) DeleteSeq(es iter.Seq[T]) {
for e := range es {
s.Delete(e)
}
}
// DeleteSlice removes all elements in es from the set.
func (s Set[T]) DeleteSlice(es []T) {
for _, e := range es {
s.Delete(e)
}
}
// DeleteSet removes all elements in es from the set.
func (s Set[T]) DeleteSet(es Set[T]) {
for e := range es {
s.Delete(e)
}
}
// Contains reports whether s contains e.
func (s Set[T]) Contains(e T) bool {
_, ok := s[e]
return ok
}
// Len reports the number of items in s.
func (s Set[T]) Len() int { return len(s) }
// Equal reports whether s is equal to other.
func (s Set[T]) Equal(other Set[T]) bool {
return maps.Equal(s, other)
}
func (s Set[T]) MarshalJSON() ([]byte, error) {
return json.Marshal(s.Slice())
}
func (s *Set[T]) UnmarshalJSON(buf []byte) error {
var ss []T
if err := json.Unmarshal(buf, &ss); err != nil {
return err
}
*s = SetOf(ss)
return nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package set
import (
"slices"
"tailscale.com/types/views"
)
// Slice is a set of elements tracked in a slice of unique elements.
type Slice[T comparable] struct {
slice []T
set map[T]bool // nil until/unless slice is large enough
}
// Slice returns a view of the underlying slice.
// The elements are in order of insertion.
// The returned value is only valid until ss is modified again.
func (ss *Slice[T]) Slice() views.Slice[T] { return views.SliceOf(ss.slice) }
// Len returns the number of elements in the set.
func (ss *Slice[T]) Len() int { return len(ss.slice) }
// Contains reports whether v is in the set.
// The amortized cost is O(1).
func (ss *Slice[T]) Contains(v T) bool {
if ss.set != nil {
return ss.set[v]
}
return slices.Index(ss.slice, v) != -1
}
// Remove removes v from the set.
// The cost is O(n).
func (ss *Slice[T]) Remove(v T) {
if ss.set != nil {
if !ss.set[v] {
return
}
delete(ss.set, v)
}
if ix := slices.Index(ss.slice, v); ix != -1 {
ss.slice = append(ss.slice[:ix], ss.slice[ix+1:]...)
}
}
// Add adds each element in vs to the set.
// The amortized cost is O(1) per element.
func (ss *Slice[T]) Add(vs ...T) {
for _, v := range vs {
if ss.Contains(v) {
continue
}
ss.slice = append(ss.slice, v)
if ss.set != nil {
ss.set[v] = true
} else if len(ss.slice) > 8 {
ss.set = make(map[T]bool, len(ss.slice))
for _, v := range ss.slice {
ss.set[v] = true
}
}
}
}
// AddSlice adds all elements in vs to the set.
func (ss *Slice[T]) AddSlice(vs views.Slice[T]) {
for _, v := range vs.All() {
ss.Add(v)
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package set
import (
"iter"
"maps"
"tailscale.com/types/structs"
)
// SmallSet is a set that is optimized for reducing memory overhead when the
// expected size of the set is 0 or 1 elements.
//
// The zero value of SmallSet is a usable empty set.
//
// When storing a SmallSet in a map as a value type, it is important to re-assign
// the map entry after calling Add or Delete, as the SmallSet's representation
// may change.
//
// Copying a SmallSet by value may alias the previous value. Use the Clone method
// to create a new SmallSet with the same contents.
type SmallSet[T comparable] struct {
_ structs.Incomparable // to prevent == mistakes
one T // if non-zero, then single item in set
m Set[T] // if non-nil, the set of items, which might be size 1 if it's the zero value of T
}
// Values returns an iterator over the elements of the set.
// The iterator will yield the elements in no particular order.
func (s SmallSet[T]) Values() iter.Seq[T] {
if s.m != nil {
return maps.Keys(s.m)
}
var zero T
return func(yield func(T) bool) {
if s.one != zero {
yield(s.one)
}
}
}
// Contains reports whether e is in the set.
func (s SmallSet[T]) Contains(e T) bool {
if s.m != nil {
return s.m.Contains(e)
}
var zero T
return e != zero && s.one == e
}
// SoleElement returns the single value in the set, if the set has exactly one
// element.
//
// If the set is empty or has more than one element, ok will be false and e will
// be the zero value of T.
func (s SmallSet[T]) SoleElement() (e T, ok bool) {
return s.one, s.Len() == 1
}
// Add adds e to the set.
//
// When storing a SmallSet in a map as a value type, it is important to
// re-assign the map entry after calling Add or Delete, as the SmallSet's
// representation may change.
func (s *SmallSet[T]) Add(e T) {
var zero T
if s.m != nil {
s.m.Add(e)
return
}
// Non-zero elements can go into s.one.
if e != zero {
if s.one == zero {
s.one = e // Len 0 to Len 1
return
}
if s.one == e {
return // dup
}
}
// Need to make a multi map, either
// because we now have two items, or
// because e is the zero value.
s.m = Set[T]{}
if s.one != zero {
s.m.Add(s.one) // move single item to multi
}
s.m.Add(e) // add new item, possibly zero
s.one = zero
}
// Len reports the number of elements in the set.
func (s SmallSet[T]) Len() int {
var zero T
if s.m != nil {
return s.m.Len()
}
if s.one != zero {
return 1
}
return 0
}
// Delete removes e from the set.
//
// When storing a SmallSet in a map as a value type, it is important to
// re-assign the map entry after calling Add or Delete, as the SmallSet's
// representation may change.
func (s *SmallSet[T]) Delete(e T) {
var zero T
if s.m == nil {
if s.one == e {
s.one = zero
}
return
}
s.m.Delete(e)
// If the map size drops to zero, that means
// it only contained the zero value of T.
if s.m.Len() == 0 {
s.m = nil
return
}
// If the map size drops to one element and doesn't
// contain the zero value, we can switch back to the
// single-item representation.
if s.m.Len() == 1 {
for v := range s.m {
if v != zero {
s.one = v
s.m = nil
}
}
}
return
}
// Clone returns a copy of s that doesn't alias the original.
func (s SmallSet[T]) Clone() SmallSet[T] {
return SmallSet[T]{
one: s.one,
m: maps.Clone(s.m), // preserves nilness
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package singleflight provides a duplicate function call suppression
// mechanism.
//
// This is a Tailscale fork of Go's singleflight package which has had several
// homes in the past:
//
// - https://github.com/golang/go/commit/61d3b2db6292581fc07a3767ec23ec94ad6100d1
// - https://github.com/golang/groupcache/tree/master/singleflight
// - https://pkg.go.dev/golang.org/x/sync/singleflight
//
// This fork adds generics.
package singleflight // import "tailscale.com/util/singleflight"
import (
"bytes"
"context"
"errors"
"fmt"
"runtime"
"runtime/debug"
"sync"
"sync/atomic"
)
// errGoexit indicates the runtime.Goexit was called in
// the user given function.
var errGoexit = errors.New("runtime.Goexit was called")
// A panicError is an arbitrary value recovered from a panic
// with the stack trace during the execution of given function.
type panicError struct {
value any
stack []byte
}
// Error implements error interface.
func (p *panicError) Error() string {
return fmt.Sprintf("%v\n\n%s", p.value, p.stack)
}
func newPanicError(v any) error {
stack := debug.Stack()
// The first line of the stack trace is of the form "goroutine N [status]:"
// but by the time the panic reaches Do the goroutine may no longer exist
// and its status will have changed. Trim out the misleading line.
if line := bytes.IndexByte(stack[:], '\n'); line >= 0 {
stack = stack[line+1:]
}
return &panicError{value: v, stack: stack}
}
// call is an in-flight or completed singleflight.Do call
type call[V any] struct {
wg sync.WaitGroup
// These fields are written once before the WaitGroup is done
// and are only read after the WaitGroup is done.
val V
err error
// These fields are read and written with the singleflight
// mutex held before the WaitGroup is done, and are read but
// not written after the WaitGroup is done.
dups int
chans []chan<- Result[V]
// These fields are only written when the call is being created, and
// only in the DoChanContext method.
cancel context.CancelFunc
ctxWaiters atomic.Int64
}
// Group represents a class of work and forms a namespace in
// which units of work can be executed with duplicate suppression.
type Group[K comparable, V any] struct {
mu sync.Mutex // protects m
m map[K]*call[V] // lazily initialized
}
// Result holds the results of Do, so they can be passed
// on a channel.
type Result[V any] struct {
Val V
Err error
Shared bool
}
// Do executes and returns the results of the given function, making
// sure that only one execution is in-flight for a given key at a
// time. If a duplicate comes in, the duplicate caller waits for the
// original to complete and receives the same results.
// The return value shared indicates whether v was given to multiple callers.
func (g *Group[K, V]) Do(key K, fn func() (V, error)) (v V, err error, shared bool) {
g.mu.Lock()
if g.m == nil {
g.m = make(map[K]*call[V])
}
if c, ok := g.m[key]; ok {
c.dups++
g.mu.Unlock()
c.wg.Wait()
if e, ok := c.err.(*panicError); ok {
panic(e)
} else if c.err == errGoexit {
runtime.Goexit()
}
return c.val, c.err, true
}
c := new(call[V])
c.wg.Add(1)
g.m[key] = c
g.mu.Unlock()
g.doCall(c, key, fn)
return c.val, c.err, c.dups > 0
}
// DoChan is like Do but returns a channel that will receive the
// results when they are ready.
//
// The returned channel will not be closed.
func (g *Group[K, V]) DoChan(key K, fn func() (V, error)) <-chan Result[V] {
ch := make(chan Result[V], 1)
g.mu.Lock()
if g.m == nil {
g.m = make(map[K]*call[V])
}
if c, ok := g.m[key]; ok {
c.dups++
c.chans = append(c.chans, ch)
g.mu.Unlock()
return ch
}
c := &call[V]{chans: []chan<- Result[V]{ch}}
c.wg.Add(1)
g.m[key] = c
g.mu.Unlock()
go g.doCall(c, key, fn)
return ch
}
// DoChanContext is like [Group.DoChan], but supports context cancelation. The
// context passed to the fn function is a context that is canceled only when
// there are no callers waiting on a result (i.e. all callers have canceled
// their contexts).
//
// The context that is passed to the fn function is not derived from any of the
// input contexts, so context values will not be propagated. If context values
// are needed, they must be propagated explicitly.
//
// The returned channel will not be closed. The Result.Err field is set to the
// context error if the context is canceled.
func (g *Group[K, V]) DoChanContext(ctx context.Context, key K, fn func(context.Context) (V, error)) <-chan Result[V] {
ch := make(chan Result[V], 1)
g.mu.Lock()
if g.m == nil {
g.m = make(map[K]*call[V])
}
c, ok := g.m[key]
if ok {
// Call already in progress; add to the waiters list and then
// release the mutex.
c.dups++
c.ctxWaiters.Add(1)
c.chans = append(c.chans, ch)
g.mu.Unlock()
} else {
// The call hasn't been started yet; we need to start it.
//
// Create a context that is not canceled when the parent context is,
// but otherwise propagates all values.
callCtx, callCancel := context.WithCancel(context.Background())
c = &call[V]{
chans: []chan<- Result[V]{ch},
cancel: callCancel,
}
c.wg.Add(1)
c.ctxWaiters.Add(1) // one caller waiting
g.m[key] = c
g.mu.Unlock()
// Wrap our function to provide the context.
go g.doCall(c, key, func() (V, error) {
return fn(callCtx)
})
}
// Instead of returning the channel directly, we need to track
// when the call finishes so we can handle context cancelation.
// Do so by creating an final channel that gets the
// result and hooking that up to the wait function.
final := make(chan Result[V], 1)
go g.waitCtx(ctx, c, ch, final)
return final
}
// waitCtx will wait on the provided call to finish, or the context to be done.
// If the context is done, and this is the last waiter, then the context
// provided to the underlying function will be canceled.
func (g *Group[K, V]) waitCtx(ctx context.Context, c *call[V], result <-chan Result[V], output chan<- Result[V]) {
var res Result[V]
select {
case <-ctx.Done():
case res = <-result:
}
// Decrement the caller count, and if we're the last one, cancel the
// context we created. Do this in all cases, error and otherwise, so we
// don't leak goroutines.
//
// Also wait on the call to finish, so we know that the call has
// finished executing after the last caller has returned.
if c.ctxWaiters.Add(-1) == 0 {
c.cancel()
c.wg.Wait()
}
// Ensure that context cancelation takes precedence over a value being
// available by checking ctx.Err() before sending the result to the
// caller. The select above will nondeterministically pick a case if a
// result is available and the ctx.Done channel is closed, so we check
// again here.
if err := ctx.Err(); err != nil {
res = Result[V]{Err: err}
}
output <- res
}
// doCall handles the single call for a key.
func (g *Group[K, V]) doCall(c *call[V], key K, fn func() (V, error)) {
normalReturn := false
recovered := false
// use double-defer to distinguish panic from runtime.Goexit,
// more details see https://golang.org/cl/134395
defer func() {
// the given function invoked runtime.Goexit
if !normalReturn && !recovered {
c.err = errGoexit
}
g.mu.Lock()
defer g.mu.Unlock()
c.wg.Done()
if g.m[key] == c {
delete(g.m, key)
}
if e, ok := c.err.(*panicError); ok {
// In order to prevent the waiting channels from being blocked forever,
// needs to ensure that this panic cannot be recovered.
if len(c.chans) > 0 {
go panic(e)
select {} // Keep this goroutine around so that it will appear in the crash dump.
} else {
panic(e)
}
} else if c.err == errGoexit {
// Already in the process of goexit, no need to call again
} else {
// Normal return
for _, ch := range c.chans {
ch <- Result[V]{c.val, c.err, c.dups > 0}
}
}
}()
func() {
defer func() {
if !normalReturn {
// Ideally, we would wait to take a stack trace until we've determined
// whether this is a panic or a runtime.Goexit.
//
// Unfortunately, the only way we can distinguish the two is to see
// whether the recover stopped the goroutine from terminating, and by
// the time we know that, the part of the stack trace relevant to the
// panic has been discarded.
if r := recover(); r != nil {
c.err = newPanicError(r)
}
}
}()
c.val, c.err = fn()
normalReturn = true
}()
if !normalReturn {
recovered = true
}
}
// Forget tells the singleflight to forget about a key. Future calls
// to Do for this key will call the function rather than waiting for
// an earlier call to complete.
func (g *Group[K, V]) Forget(key K) {
g.mu.Lock()
delete(g.m, key)
g.mu.Unlock()
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package slicesx contains some helpful generic slice functions.
package slicesx
import (
"math/rand/v2"
"slices"
)
// Interleave combines two slices of the form [a, b, c] and [x, y, z] into a
// slice with elements interleaved; i.e. [a, x, b, y, c, z].
func Interleave[S ~[]T, T any](a, b S) S {
// Avoid allocating an empty slice.
if a == nil && b == nil {
return nil
}
var (
i int
ret = make([]T, 0, len(a)+len(b))
)
for i = 0; i < len(a) && i < len(b); i++ {
ret = append(ret, a[i], b[i])
}
ret = append(ret, a[i:]...)
ret = append(ret, b[i:]...)
return ret
}
// Shuffle randomly shuffles a slice in-place, similar to rand.Shuffle.
func Shuffle[S ~[]T, T any](s S) {
// TODO(andrew): use a pooled Rand?
// This is the same Fisher-Yates shuffle implementation as rand.Shuffle
n := len(s)
i := n - 1
for ; i > 1<<31-1-1; i-- {
j := int(rand.N(int64(i + 1)))
s[i], s[j] = s[j], s[i]
}
for ; i > 0; i-- {
j := int(rand.N(int32(i + 1)))
s[i], s[j] = s[j], s[i]
}
}
// Partition returns two slices, the first containing the elements of the input
// slice for which the callback evaluates to true, the second containing the rest.
//
// This function does not mutate s.
func Partition[S ~[]T, T any](s S, cb func(T) bool) (trues, falses S) {
for _, elem := range s {
if cb(elem) {
trues = append(trues, elem)
} else {
falses = append(falses, elem)
}
}
return
}
// EqualSameNil reports whether two slices are equal: the same length, same
// nilness (notably when length zero), and all elements equal. If the lengths
// are different or their nilness differs, Equal returns false. Otherwise, the
// elements are compared in increasing index order, and the comparison stops at
// the first unequal pair. Floating point NaNs are not considered equal.
//
// It is identical to the standard library's slices.Equal but adds the matching
// nilness check.
func EqualSameNil[S ~[]E, E comparable](s1, s2 S) bool {
if len(s1) != len(s2) || (s1 == nil) != (s2 == nil) {
return false
}
for i := range s1 {
if s1[i] != s2[i] {
return false
}
}
return true
}
// Filter calls fn with each element of the provided src slice, and appends the
// element to dst if fn returns true.
//
// dst can be nil to allocate a new slice, or set to src[:0] to filter in-place
// without allocating.
func Filter[S ~[]T, T any](dst, src S, fn func(T) bool) S {
for _, x := range src {
if fn(x) {
dst = append(dst, x)
}
}
return dst
}
// AppendNonzero appends all non-zero elements of src to dst.
func AppendNonzero[S ~[]T, T comparable](dst, src S) S {
var zero T
for _, v := range src {
if v != zero {
dst = append(dst, v)
}
}
return dst
}
// AppendMatching appends elements in ps to dst if f(x) is true.
func AppendMatching[T any](dst, ps []T, f func(T) bool) []T {
for _, p := range ps {
if f(p) {
dst = append(dst, p)
}
}
return dst
}
// HasPrefix reports whether the byte slice s begins with prefix.
func HasPrefix[E comparable](s, prefix []E) bool {
return len(s) >= len(prefix) && slices.Equal(s[0:len(prefix)], prefix)
}
// HasSuffix reports whether the slice s ends with suffix.
func HasSuffix[E comparable](s, suffix []E) bool {
return len(s) >= len(suffix) && slices.Equal(s[len(s)-len(suffix):], suffix)
}
// CutPrefix returns s without the provided leading prefix slice and reports
// whether it found the prefix. If s doesn't start with prefix, CutPrefix
// returns s, false. If prefix is the empty slice, CutPrefix returns s, true.
// CutPrefix returns slices of the original slice s, not copies.
func CutPrefix[E comparable](s, prefix []E) (after []E, found bool) {
if !HasPrefix(s, prefix) {
return s, false
}
return s[len(prefix):], true
}
// CutSuffix returns s without the provided ending suffix slice and reports
// whether it found the suffix. If s doesn't end with suffix, CutSuffix returns
// s, false. If suffix is the empty slice, CutSuffix returns s, true.
// CutSuffix returns slices of the original slice s, not copies.
func CutSuffix[E comparable](s, suffix []E) (after []E, found bool) {
if !HasSuffix(s, suffix) {
return s, false
}
return s[:len(s)-len(suffix)], true
}
// FirstEqual reports whether len(s) > 0 and
// its first element == v.
func FirstEqual[T comparable](s []T, v T) bool {
return len(s) > 0 && s[0] == v
}
// LastEqual reports whether len(s) > 0 and
// its last element == v.
func LastEqual[T comparable](s []T, v T) bool {
return len(s) > 0 && s[len(s)-1] == v
}
// MapKeys returns the values of the map m.
//
// The keys will be in an indeterminate order.
//
// It's equivalent to golang.org/x/exp/maps.Keys, which
// unfortunately has the package name "maps", shadowing
// the std "maps" package. This version exists for clarity
// when reading call sites.
//
// As opposed to slices.Collect(maps.Keys(m)), this allocates
// the returned slice once to exactly the right size, rather than
// appending larger backing arrays as it goes.
func MapKeys[M ~map[K]V, K comparable, V any](m M) []K {
r := make([]K, 0, len(m))
for k := range m {
r = append(r, k)
}
return r
}
// MapValues returns the values of the map m.
//
// The values will be in an indeterminate order.
//
// It's equivalent to golang.org/x/exp/maps.Values, which
// unfortunately has the package name "maps", shadowing
// the std "maps" package. This version exists for clarity
// when reading call sites.
//
// As opposed to slices.Collect(maps.Values(m)), this allocates
// the returned slice once to exactly the right size, rather than
// appending larger backing arrays as it goes.
func MapValues[M ~map[K]V, K comparable, V any](m M) []V {
r := make([]V, 0, len(m))
for _, v := range m {
r = append(r, v)
}
return r
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package internal contains miscellaneous functions and types
// that are internal to the syspolicy packages.
package internal
import (
"bytes"
"github.com/go-json-experiment/json/jsontext"
"tailscale.com/types/lazy"
"tailscale.com/util/testenv"
"tailscale.com/version"
)
// Init facilitates deferred invocation of initializers.
var Init lazy.DeferredInit
// OSForTesting is the operating system override used for testing.
// It follows the same naming convention as [version.OS].
var OSForTesting lazy.SyncValue[string]
// OS is like [version.OS], but supports a test hook.
func OS() string {
return OSForTesting.Get(version.OS)
}
// EqualJSONForTest compares the JSON in j1 and j2 for semantic equality.
// It returns "", "", true if j1 and j2 are equal. Otherwise, it returns
// indented versions of j1 and j2 and false.
func EqualJSONForTest(tb testenv.TB, j1, j2 jsontext.Value) (s1, s2 string, equal bool) {
tb.Helper()
j1 = j1.Clone()
j2 = j2.Clone()
// Canonicalize JSON values for comparison.
if err := j1.Canonicalize(); err != nil {
tb.Error(err)
}
if err := j2.Canonicalize(); err != nil {
tb.Error(err)
}
// Check and return true if the two values are structurally equal.
if bytes.Equal(j1, j2) {
return "", "", true
}
// Otherwise, format the values for display and return false.
if err := j1.Indent(); err != nil {
tb.Fatal(err)
}
if err := j2.Indent(); err != nil {
tb.Fatal(err)
}
return j1.String(), j2.String(), false
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package policyclient contains the minimal syspolicy interface as needed by
// client code using syspolicy. It's the part that's always linked in, even if the rest
// of syspolicy is omitted from the build.
package policyclient
import (
"time"
"tailscale.com/util/syspolicy/pkey"
"tailscale.com/util/syspolicy/ptype"
"tailscale.com/util/testenv"
)
// Client is the interface between code making questions about the system policy
// and the actual implementation.
type Client interface {
// GetString returns a string policy setting with the specified key,
// or defaultValue (and a nil error) if it does not exist.
GetString(key pkey.Key, defaultValue string) (string, error)
// GetStringArray returns a string array policy setting with the specified key,
// or defaultValue (and a nil error) if it does not exist.
GetStringArray(key pkey.Key, defaultValue []string) ([]string, error)
// GetBoolean returns a boolean policy setting with the specified key,
// or defaultValue (and a nil error) if it does not exist.
GetBoolean(key pkey.Key, defaultValue bool) (bool, error)
// GetUint64 returns a numeric policy setting with the specified key,
// or defaultValue (and a nil error) if it does not exist.
GetUint64(key pkey.Key, defaultValue uint64) (uint64, error)
// GetDuration loads a policy from the registry that can be managed by an
// enterprise policy management system and describes a duration for some
// action. The registry value should be a string that time.ParseDuration
// understands. If the registry value is "" or can not be processed,
// defaultValue (and a nil error) is returned instead.
GetDuration(key pkey.Key, defaultValue time.Duration) (time.Duration, error)
// GetPreferenceOption loads a policy from the registry that can be
// managed by an enterprise policy management system and allows administrative
// overrides of users' choices in a way that we do not want tailcontrol to have
// the authority to set. It describes user-decides/always/never options, where
// "always" and "never" remove the user's ability to make a selection. If not
// present or set to a different value, defaultValue (and a nil error) is returned.
GetPreferenceOption(key pkey.Key, defaultValue ptype.PreferenceOption) (ptype.PreferenceOption, error)
// GetVisibility returns whether a UI element should be visible based on
// the system's configuration.
// If unconfigured, implementations should return [ptype.VisibleByPolicy]
// and a nil error.
GetVisibility(key pkey.Key) (ptype.Visibility, error)
// SetDebugLoggingEnabled enables or disables debug logging for the policy client.
SetDebugLoggingEnabled(enabled bool)
// HasAnyOf returns whether at least one of the specified policy settings is
// configured, or an error if no keys are provided or the check fails.
HasAnyOf(keys ...pkey.Key) (bool, error)
// RegisterChangeCallback registers a callback function that will be called
// whenever a policy change is detected. If uid is empty, the callback fires
// for device-scope policy changes. If uid is non-empty, it fires for changes
// to that user's effective policy (device + user policies merged). It returns
// a function to unregister the callback and an error if the registration fails.
RegisterChangeCallback(uid string, cb func(PolicyChange)) (unregister func(), err error)
// GetPolicySnapshot returns the effective policy snapshot for the given user ID.
// If uid is empty, returns the default-scope policy. Returns nil, nil if policy
// snapshots are not supported.
GetPolicySnapshot(uid string) (*PolicySnapshot, error)
}
// PolicySnapshot is an alias for [setting.Snapshot] unless syspolicy is omitted from the build.
type PolicySnapshot = policySnapshot
// Get returns a non-nil [Client] implementation as a function of the
// build tags. It returns a no-op implementation if the full syspolicy
// package is omitted from the build, or in tests.
func Get() Client {
if testenv.InTest() {
// This is a little redundant (the Windows implementation at least
// already does this) but it's here for redundancy and clarity, that we
// don't want to accidentally use the real system policy when running
// tests.
return NoPolicyClient{}
}
return client
}
// RegisterClientImpl registers a [Client] implementation to be returned by
// [Get].
func RegisterClientImpl(c Client) {
client = c
}
var client Client = NoPolicyClient{}
// PolicyChange is the interface representing a change in policy settings.
type PolicyChange interface {
// HasChanged reports whether the policy setting identified by the given key
// has changed.
HasChanged(pkey.Key) bool
// HasChangedAnyOf reports whether any of the provided policy settings
// changed in this change.
HasChangedAnyOf(keys ...pkey.Key) bool
}
// NoPolicyClient is a no-op implementation of [Client] that only
// returns default values.
type NoPolicyClient struct{}
var _ Client = NoPolicyClient{}
func (NoPolicyClient) GetBoolean(key pkey.Key, defaultValue bool) (bool, error) {
return defaultValue, nil
}
func (NoPolicyClient) GetString(key pkey.Key, defaultValue string) (string, error) {
return defaultValue, nil
}
func (NoPolicyClient) GetStringArray(key pkey.Key, defaultValue []string) ([]string, error) {
return defaultValue, nil
}
func (NoPolicyClient) GetUint64(key pkey.Key, defaultValue uint64) (uint64, error) {
return defaultValue, nil
}
func (NoPolicyClient) GetDuration(name pkey.Key, defaultValue time.Duration) (time.Duration, error) {
return defaultValue, nil
}
func (NoPolicyClient) GetPreferenceOption(name pkey.Key, defaultValue ptype.PreferenceOption) (ptype.PreferenceOption, error) {
return defaultValue, nil
}
func (NoPolicyClient) GetVisibility(name pkey.Key) (ptype.Visibility, error) {
return ptype.VisibleByPolicy, nil
}
func (NoPolicyClient) HasAnyOf(keys ...pkey.Key) (bool, error) {
return false, nil
}
func (NoPolicyClient) SetDebugLoggingEnabled(enabled bool) {}
func (NoPolicyClient) RegisterChangeCallback(uid string, cb func(PolicyChange)) (unregister func(), err error) {
return func() {}, nil
}
func (NoPolicyClient) GetPolicySnapshot(uid string) (*PolicySnapshot, error) {
return nil, nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package ptype contains types used by syspolicy.
//
// It's a leaf package for dependency reasons and should not contain much if any
// code, and should not import much (or anything).
package ptype
// PreferenceOption is a policy that governs whether a boolean variable
// is forcibly assigned an administrator-defined value, or allowed to receive
// a user-defined value.
type PreferenceOption byte
const (
ShowChoiceByPolicy PreferenceOption = iota
NeverByPolicy
AlwaysByPolicy
)
// Show reports whether the UI option that controls the choice administered by
// this policy should be shown (that is, available for users to change).
//
// Currently this is true if and only if the policy is [ShowChoiceByPolicy].
func (p PreferenceOption) Show() bool {
return p == ShowChoiceByPolicy
}
// ShouldEnable checks if the choice administered by this policy should be
// enabled. If the administrator has chosen a setting, the administrator's
// setting is returned, otherwise userChoice is returned.
func (p PreferenceOption) ShouldEnable(userChoice bool) bool {
switch p {
case NeverByPolicy:
return false
case AlwaysByPolicy:
return true
default:
return userChoice
}
}
// IsAlways reports whether the preference should always be enabled.
func (p PreferenceOption) IsAlways() bool {
return p == AlwaysByPolicy
}
// IsNever reports whether the preference should always be disabled.
func (p PreferenceOption) IsNever() bool {
return p == NeverByPolicy
}
// WillOverride checks if the choice administered by the policy is different
// from the user's choice.
func (p PreferenceOption) WillOverride(userChoice bool) bool {
return p.ShouldEnable(userChoice) != userChoice
}
// String returns a string representation of p.
func (p PreferenceOption) String() string {
switch p {
case AlwaysByPolicy:
return "always"
case NeverByPolicy:
return "never"
default:
return "user-decides"
}
}
// MarshalText implements [encoding.TextMarshaler].
func (p *PreferenceOption) MarshalText() (text []byte, err error) {
return []byte(p.String()), nil
}
// UnmarshalText implements [encoding.TextUnmarshaler].
// It never fails and sets p to [ShowChoiceByPolicy] if the specified text
// does not represent a valid [PreferenceOption].
func (p *PreferenceOption) UnmarshalText(text []byte) error {
switch string(text) {
case "always":
*p = AlwaysByPolicy
case "never":
*p = NeverByPolicy
default:
*p = ShowChoiceByPolicy
}
return nil
}
// Visibility is a policy that controls whether or not a particular
// component of a user interface is to be shown.
type Visibility byte
const (
VisibleByPolicy Visibility = 'v'
HiddenByPolicy Visibility = 'h'
)
// Show reports whether the UI option administered by this policy should be shown.
// Currently this is true if the policy is not [hiddenByPolicy].
func (v Visibility) Show() bool {
return v != HiddenByPolicy
}
// String returns a string representation of v.
func (v Visibility) String() string {
switch v {
case 'h':
return "hide"
default:
return "show"
}
}
// MarshalText implements [encoding.TextMarshaler].
func (v Visibility) MarshalText() (text []byte, err error) {
return []byte(v.String()), nil
}
// UnmarshalText implements [encoding.TextUnmarshaler].
// It never fails and sets v to [VisibleByPolicy] if the specified text
// does not represent a valid [Visibility].
func (v *Visibility) UnmarshalText(text []byte) error {
switch string(text) {
case "hide":
*v = HiddenByPolicy
default:
*v = VisibleByPolicy
}
return nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package setting
import (
"errors"
)
var (
// ErrNotConfigured is returned when the requested policy setting is not configured.
ErrNotConfigured = errors.New("not configured")
// ErrTypeMismatch is returned when there's a type mismatch between the actual type
// of the setting value and the expected type.
ErrTypeMismatch = errors.New("type mismatch")
// ErrNoSuchKey is returned by [DefinitionOf] when no policy setting
// has been registered with the specified key.
//
// Until 2024-08-02, this error was also returned by a [Handler] when the specified
// key did not have a value set. While the package maintains compatibility with this
// usage of ErrNoSuchKey, it is recommended to return [ErrNotConfigured] from newer
// [source.Store] implementations.
ErrNoSuchKey = errors.New("no such key")
)
// ErrorText represents an error that occurs when reading or parsing a policy setting.
// This includes errors due to permissions issues, value type and format mismatches,
// and other platform- or source-specific errors. It does not include
// [ErrNotConfigured] and [ErrNoSuchKey], as those correspond to unconfigured
// policy settings rather than settings that cannot be read or parsed
// due to an error.
//
// ErrorText is used to marshal errors when a policy setting is sent over the wire,
// allowing the error to be logged or displayed. It does not preserve the
// type information of the underlying error.
type ErrorText string
// NewErrorText returns a [ErrorText] with the specified error message.
func NewErrorText(text string) *ErrorText {
return new(ErrorText(text))
}
// MaybeErrorText returns an [ErrorText] with the text of the specified error,
// or nil if err is nil, [ErrNotConfigured], or [ErrNoSuchKey].
func MaybeErrorText(err error) *ErrorText {
if err == nil || errors.Is(err, ErrNotConfigured) || errors.Is(err, ErrNoSuchKey) {
return nil
}
if err, ok := err.(*ErrorText); ok {
return err
}
return new(ErrorText(err.Error()))
}
// Error implements error.
func (e ErrorText) Error() string {
return string(e)
}
// MarshalText implements [encoding.TextMarshaler].
func (e ErrorText) MarshalText() (text []byte, err error) {
return []byte(e.Error()), nil
}
// UnmarshalText implements [encoding.TextUnmarshaler].
func (e *ErrorText) UnmarshalText(text []byte) error {
*e = ErrorText(text)
return nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package setting
import (
"fmt"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
)
// Origin describes where a policy or a policy setting is configured.
type Origin struct {
data settingOrigin
}
// settingOrigin is the marshallable data of an [Origin].
type settingOrigin struct {
Name string `json:",omitzero"`
Scope PolicyScope
}
// NewOrigin returns a new [Origin] with the specified scope.
func NewOrigin(scope PolicyScope) *Origin {
return NewNamedOrigin("", scope)
}
// NewNamedOrigin returns a new [Origin] with the specified scope and name.
func NewNamedOrigin(name string, scope PolicyScope) *Origin {
return &Origin{settingOrigin{name, scope}}
}
// Scope reports the policy [PolicyScope] where the setting is configured.
func (s Origin) Scope() PolicyScope {
return s.data.Scope
}
// Name returns the name of the policy source where the setting is configured,
// or "" if not available.
func (s Origin) Name() string {
return s.data.Name
}
// String implements [fmt.Stringer].
func (s Origin) String() string {
if s.Name() != "" {
return fmt.Sprintf("%s (%v)", s.Name(), s.Scope())
}
return s.Scope().String()
}
var (
_ jsonv2.MarshalerTo = (*Origin)(nil)
_ jsonv2.UnmarshalerFrom = (*Origin)(nil)
)
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (s Origin) MarshalJSONTo(out *jsontext.Encoder) error {
return jsonv2.MarshalEncode(out, &s.data)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (s *Origin) UnmarshalJSONFrom(in *jsontext.Decoder) error {
return jsonv2.UnmarshalDecode(in, &s.data)
}
// MarshalJSON implements [json.Marshaler].
func (s Origin) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(s) // uses MarshalJSONTo
}
// UnmarshalJSON implements [json.Unmarshaler].
func (s *Origin) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, s) // uses UnmarshalJSONFrom
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package setting
import (
"fmt"
"strings"
"tailscale.com/types/lazy"
"tailscale.com/util/syspolicy/internal"
)
var (
lazyDefaultScope lazy.SyncValue[PolicyScope]
// DeviceScope indicates a scope containing device-global policies.
DeviceScope = PolicyScope{kind: DeviceSetting}
// CurrentProfileScope indicates a scope containing policies that apply to the
// currently active Tailscale profile.
CurrentProfileScope = PolicyScope{kind: ProfileSetting}
// CurrentUserScope indicates a scope containing policies that apply to the
// current user, for whatever that means on the current platform and
// in the current application context.
CurrentUserScope = PolicyScope{kind: UserSetting}
)
// PolicyScope is a management scope.
type PolicyScope struct {
kind Scope
userID string
profileID string
}
// DefaultScope returns the default [PolicyScope] to be used by a program
// when querying policy settings.
// It returns [DeviceScope], unless explicitly changed with [SetDefaultScope].
func DefaultScope() PolicyScope {
// Allow deferred package init functions to override the default scope.
internal.Init.Do()
return lazyDefaultScope.Get(func() PolicyScope { return DeviceScope })
}
// SetDefaultScope attempts to set the specified scope as the default scope
// to be used by a program when querying policy settings.
// It fails and returns false if called more than once, or if the [DefaultScope]
// has already been used.
func SetDefaultScope(scope PolicyScope) bool {
return lazyDefaultScope.Set(scope)
}
// UserScopeOf returns a policy [PolicyScope] of the user with the specified id.
func UserScopeOf(uid string) PolicyScope {
return PolicyScope{kind: UserSetting, userID: uid}
}
// Kind reports the scope kind of s.
func (s PolicyScope) Kind() Scope {
return s.kind
}
// IsApplicableSetting reports whether the specified setting applies to
// and can be retrieved for this scope. Policy settings are applicable
// to their own scopes as well as more specific scopes. For example,
// device settings are applicable to device, profile and user scopes,
// but user settings are only applicable to user scopes.
// For instance, a menu visibility setting is inherently a user setting
// and only makes sense in the context of a specific user.
func (s PolicyScope) IsApplicableSetting(setting *Definition) bool {
return setting != nil && setting.Scope() <= s.Kind()
}
// IsConfigurableSetting reports whether the specified setting can be configured
// by a policy at this scope. Policy settings are configurable at their own scopes
// as well as broader scopes. For example, [UserSetting]s are configurable in
// user, profile, and device scopes, but [DeviceSetting]s are only configurable
// in the [DeviceScope]. For instance, the InstallUpdates policy setting
// can only be configured in the device scope, as it controls whether updates
// will be installed automatically on the device, rather than for specific users.
func (s PolicyScope) IsConfigurableSetting(setting *Definition) bool {
return setting != nil && setting.Scope() >= s.Kind()
}
// Contains reports whether policy settings that apply to s also apply to s2.
// For example, policy settings that apply to the [DeviceScope] also apply to
// the [CurrentUserScope].
func (s PolicyScope) Contains(s2 PolicyScope) bool {
if s.Kind() > s2.Kind() {
return false
}
switch s.Kind() {
case DeviceSetting:
return true
case ProfileSetting:
return s.profileID == s2.profileID
case UserSetting:
return s.userID == s2.userID
default:
panic("unreachable")
}
}
// StrictlyContains is like [PolicyScope.Contains], but returns false
// when s and s2 is the same scope.
func (s PolicyScope) StrictlyContains(s2 PolicyScope) bool {
return s != s2 && s.Contains(s2)
}
// String implements [fmt.Stringer].
func (s PolicyScope) String() string {
if s.profileID == "" && s.userID == "" {
return s.kind.String()
}
return s.stringSlow()
}
// MarshalText implements [encoding.TextMarshaler].
func (s PolicyScope) MarshalText() ([]byte, error) {
return []byte(s.String()), nil
}
// MarshalText implements [encoding.TextUnmarshaler].
func (s *PolicyScope) UnmarshalText(b []byte) error {
*s = PolicyScope{}
parts := strings.SplitN(string(b), "/", 2)
for i, part := range parts {
kind, id, err := parseScopeAndID(part)
if err != nil {
return err
}
if i > 0 && kind <= s.kind {
return fmt.Errorf("invalid scope hierarchy: %s", b)
}
s.kind = kind
switch kind {
case DeviceSetting:
if id != "" {
return fmt.Errorf("the device scope must not have an ID: %s", b)
}
case ProfileSetting:
s.profileID = id
case UserSetting:
s.userID = id
}
}
return nil
}
func (s PolicyScope) stringSlow() string {
var sb strings.Builder
writeScopeWithID := func(s Scope, id string) {
sb.WriteString(s.String())
if id != "" {
sb.WriteRune('(')
sb.WriteString(id)
sb.WriteRune(')')
}
}
if s.kind == ProfileSetting || s.profileID != "" {
writeScopeWithID(ProfileSetting, s.profileID)
if s.kind != ProfileSetting {
sb.WriteRune('/')
}
}
if s.kind == UserSetting {
writeScopeWithID(UserSetting, s.userID)
}
return sb.String()
}
func parseScopeAndID(s string) (scope Scope, id string, err error) {
name, params, ok := extractScopeAndParams(s)
if !ok {
return 0, "", fmt.Errorf("%q is not a valid scope string", s)
}
if err := scope.UnmarshalText([]byte(name)); err != nil {
return 0, "", err
}
return scope, params, nil
}
func extractScopeAndParams(s string) (name, params string, ok bool) {
paramsStart := strings.Index(s, "(")
if paramsStart == -1 {
return s, "", true
}
paramsEnd := strings.LastIndex(s, ")")
if paramsEnd < paramsStart {
return "", "", false
}
return s[0:paramsStart], s[paramsStart+1 : paramsEnd], true
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package setting
import (
"fmt"
"reflect"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"tailscale.com/types/opt"
"tailscale.com/types/structs"
"tailscale.com/util/syspolicy/pkey"
)
// RawItem contains a raw policy setting value as read from a policy store, or an
// error if the requested setting could not be read from the store. As a special
// case, it may also hold a value of the [Visibility], [PreferenceOption],
// or [time.Duration] types. While the policy store interface does not support
// these types natively, and the values of these types have to be unmarshalled
// or converted from strings, these setting types predate the typed policy
// hierarchies, and must be supported at this layer.
type RawItem struct {
_ structs.Incomparable
data rawItemJSON
}
// rawItemJSON holds JSON-marshallable data for [RawItem].
type rawItemJSON struct {
Value RawValue `json:",omitzero"`
Error *ErrorText `json:",omitzero"` // or nil
Origin *Origin `json:",omitzero"` // or nil
}
// RawItemOf returns a [RawItem] with the specified value.
func RawItemOf(value any) RawItem {
return RawItemWith(value, nil, nil)
}
// RawItemWith returns a [RawItem] with the specified value, error and origin.
func RawItemWith(value any, err *ErrorText, origin *Origin) RawItem {
return RawItem{data: rawItemJSON{Value: RawValue{opt.ValueOf(value)}, Error: err, Origin: origin}}
}
// Value returns the value of the policy setting, or nil if the policy setting
// is not configured, or an error occurred while reading it.
func (i RawItem) Value() any {
return i.data.Value.Get()
}
// Error returns the error that occurred when reading the policy setting,
// or nil if no error occurred.
func (i RawItem) Error() error {
if i.data.Error != nil {
return i.data.Error
}
return nil
}
// Origin returns an optional [Origin] indicating where the policy setting is
// configured.
func (i RawItem) Origin() *Origin {
return i.data.Origin
}
// String implements [fmt.Stringer].
func (i RawItem) String() string {
var suffix string
if i.data.Origin != nil {
suffix = fmt.Sprintf(" - {%v}", i.data.Origin)
}
if i.data.Error != nil {
return fmt.Sprintf("Error{%q}%s", i.data.Error.Error(), suffix)
}
return fmt.Sprintf("%v%s", i.data.Value.Value, suffix)
}
var (
_ jsonv2.MarshalerTo = (*RawItem)(nil)
_ jsonv2.UnmarshalerFrom = (*RawItem)(nil)
)
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (i RawItem) MarshalJSONTo(out *jsontext.Encoder) error {
return jsonv2.MarshalEncode(out, &i.data)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (i *RawItem) UnmarshalJSONFrom(in *jsontext.Decoder) error {
return jsonv2.UnmarshalDecode(in, &i.data)
}
// MarshalJSON implements [json.Marshaler].
func (i RawItem) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(i) // uses MarshalJSONTo
}
// UnmarshalJSON implements [json.Unmarshaler].
func (i *RawItem) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, i) // uses UnmarshalJSONFrom
}
// RawValue represents a raw policy setting value read from a policy store.
// It is JSON-marshallable and facilitates unmarshalling of JSON values
// into corresponding policy setting types, with special handling for JSON numbers
// (unmarshalled as float64) and JSON string arrays (unmarshalled as []string).
// See also [RawValue.UnmarshalJSONFrom].
type RawValue struct {
opt.Value[any]
}
// RawValueType is a constraint that permits raw setting value types.
type RawValueType interface {
bool | uint64 | string | []string
}
// RawValueOf returns a new [RawValue] holding the specified value.
func RawValueOf[T RawValueType](v T) RawValue {
return RawValue{opt.ValueOf[any](v)}
}
var (
_ jsonv2.MarshalerTo = (*RawValue)(nil)
_ jsonv2.UnmarshalerFrom = (*RawValue)(nil)
)
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v RawValue) MarshalJSONTo(out *jsontext.Encoder) error {
return jsonv2.MarshalEncode(out, v.Value)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom] by attempting to unmarshal
// a JSON value as one of the supported policy setting value types (bool, string, uint64, or []string),
// based on the JSON value type. It fails if the JSON value is an object, if it's a JSON number that
// cannot be represented as a uint64, or if a JSON array contains anything other than strings.
func (v *RawValue) UnmarshalJSONFrom(in *jsontext.Decoder) error {
var valPtr any
switch k := in.PeekKind(); k {
case 't', 'f':
valPtr = new(bool)
case '"':
valPtr = new(string)
case '0':
valPtr = new(uint64) // unmarshal JSON numbers as uint64
case '[', 'n':
valPtr = new([]string) // unmarshal arrays as string slices
case '{':
return fmt.Errorf("unexpected token: %v", k)
default:
panic("unreachable")
}
if err := jsonv2.UnmarshalDecode(in, valPtr); err != nil {
v.Value.Clear()
return err
}
value := reflect.ValueOf(valPtr).Elem().Interface()
v.Value = opt.ValueOf(value)
return nil
}
// MarshalJSON implements [json.Marshaler].
func (v RawValue) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(v) // uses MarshalJSONTo
}
// UnmarshalJSON implements [json.Unmarshaler].
func (v *RawValue) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, v) // uses UnmarshalJSONFrom
}
// RawValues is a map of keyed setting values that can be read from a JSON.
type RawValues map[pkey.Key]RawValue
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package setting contains types for defining and representing policy settings.
// It facilitates the registration of setting definitions using [Register] and [RegisterDefinition],
// and the retrieval of registered setting definitions via [Definitions] and [DefinitionOf].
// This package is intended for use primarily within the syspolicy package hierarchy.
package setting
import (
"fmt"
"slices"
"strings"
"time"
"tailscale.com/syncs"
"tailscale.com/types/lazy"
"tailscale.com/util/syspolicy/internal"
"tailscale.com/util/syspolicy/pkey"
"tailscale.com/util/syspolicy/ptype"
"tailscale.com/util/testenv"
)
// Scope indicates the broadest scope at which a policy setting may apply,
// and the narrowest scope at which it may be configured.
type Scope int8
const (
// DeviceSetting indicates a policy setting that applies to a device, regardless of
// which OS user or Tailscale profile is currently active, if any.
// It can only be configured at a [DeviceScope].
DeviceSetting Scope = iota
// ProfileSetting indicates a policy setting that applies to a Tailscale profile.
// It can only be configured for a specific profile or at a [DeviceScope],
// in which case it applies to all profiles on the device.
ProfileSetting
// UserSetting indicates a policy setting that applies to users.
// It can be configured for a user, profile, or the entire device.
UserSetting
// NumScopes is the number of possible [Scope] values.
NumScopes int = iota // must be the last value in the const block.
)
// String implements [fmt.Stringer].
func (s Scope) String() string {
switch s {
case DeviceSetting:
return "Device"
case ProfileSetting:
return "Profile"
case UserSetting:
return "User"
default:
panic("unreachable")
}
}
// MarshalText implements [encoding.TextMarshaler].
func (s Scope) MarshalText() (text []byte, err error) {
return []byte(s.String()), nil
}
// UnmarshalText implements [encoding.TextUnmarshaler].
func (s *Scope) UnmarshalText(text []byte) error {
switch strings.ToLower(string(text)) {
case "device":
*s = DeviceSetting
case "profile":
*s = ProfileSetting
case "user":
*s = UserSetting
default:
return fmt.Errorf("%q is not a valid scope", string(text))
}
return nil
}
// Type is a policy setting value type.
// Except for [InvalidValue], which represents an invalid policy setting type,
// and [PreferenceOptionValue], [VisibilityValue], and [DurationValue],
// which have special handling due to their legacy status in the package,
// SettingTypes represent the raw value types readable from policy stores.
type Type int
const (
// InvalidValue indicates an invalid policy setting value type.
InvalidValue Type = iota
// BooleanValue indicates a policy setting whose underlying type is a bool.
BooleanValue
// IntegerValue indicates a policy setting whose underlying type is a uint64.
IntegerValue
// StringValue indicates a policy setting whose underlying type is a string.
StringValue
// StringListValue indicates a policy setting whose underlying type is a []string.
StringListValue
// PreferenceOptionValue indicates a three-state policy setting whose
// underlying type is a string, but the actual value is a [PreferenceOption].
PreferenceOptionValue
// VisibilityValue indicates a two-state boolean-like policy setting whose
// underlying type is a string, but the actual value is a [Visibility].
VisibilityValue
// DurationValue indicates an interval/period/duration policy setting whose
// underlying type is a string, but the actual value is a [time.Duration].
DurationValue
)
// String returns a string representation of t.
func (t Type) String() string {
switch t {
case InvalidValue:
return "Invalid"
case BooleanValue:
return "Boolean"
case IntegerValue:
return "Integer"
case StringValue:
return "String"
case StringListValue:
return "StringList"
case PreferenceOptionValue:
return "PreferenceOption"
case VisibilityValue:
return "Visibility"
case DurationValue:
return "Duration"
default:
panic("unreachable")
}
}
// ValueType is a constraint that allows Go types corresponding to [Type].
type ValueType interface {
bool | uint64 | string | []string | ptype.Visibility | ptype.PreferenceOption | time.Duration
}
// Definition defines policy key, scope and value type.
type Definition struct {
key pkey.Key
scope Scope
typ Type
platforms PlatformList
}
// NewDefinition returns a new [Definition] with the specified
// key, scope, type and supported platforms (see [PlatformList]).
func NewDefinition(k pkey.Key, s Scope, t Type, platforms ...string) *Definition {
return &Definition{key: k, scope: s, typ: t, platforms: platforms}
}
// Key returns a policy setting's identifier.
func (d *Definition) Key() pkey.Key {
if d == nil {
return ""
}
return d.key
}
// Scope reports the broadest [Scope] the policy setting may apply to.
func (d *Definition) Scope() Scope {
if d == nil {
return 0
}
return d.scope
}
// Type reports the underlying value type of the policy setting.
func (d *Definition) Type() Type {
if d == nil {
return InvalidValue
}
return d.typ
}
// IsSupported reports whether the policy setting is supported on the current OS.
func (d *Definition) IsSupported() bool {
if d == nil {
return false
}
return d.platforms.HasCurrent()
}
// SupportedPlatforms reports platforms on which the policy setting is supported.
// An empty [PlatformList] indicates that s is available on all platforms.
func (d *Definition) SupportedPlatforms() PlatformList {
if d == nil {
return nil
}
return d.platforms
}
// String implements [fmt.Stringer].
func (d *Definition) String() string {
if d == nil {
return "(nil)"
}
return fmt.Sprintf("%v(%q, %v)", d.scope, d.key, d.typ)
}
// Equal reports whether d and d2 have the same key, type and scope.
// It does not check whether both s and s2 are supported on the same platforms.
func (d *Definition) Equal(d2 *Definition) bool {
if d == d2 {
return true
}
if d == nil || d2 == nil {
return false
}
return d.key == d2.key && d.typ == d2.typ && d.scope == d2.scope
}
// DefinitionMap is a map of setting [Definition] by [Key].
type DefinitionMap map[pkey.Key]*Definition
var (
definitions lazy.SyncValue[DefinitionMap]
definitionsMu syncs.Mutex
definitionsList []*Definition
definitionsUsed bool
)
// Register registers a policy setting with the specified key, scope, value type,
// and an optional list of supported platforms. All policy settings must be
// registered before any of them can be used. Register panics if called after
// invoking any functions that use the registered policy definitions. This
// includes calling [Definitions] or [DefinitionOf] directly, or reading any
// policy settings via syspolicy.
func Register(k pkey.Key, s Scope, t Type, platforms ...string) {
RegisterDefinition(NewDefinition(k, s, t, platforms...))
}
// RegisterDefinition is like [Register], but accepts a [Definition].
func RegisterDefinition(d *Definition) {
definitionsMu.Lock()
defer definitionsMu.Unlock()
registerLocked(d)
}
func registerLocked(d *Definition) {
if definitionsUsed {
panic("policy definitions are already in use")
}
definitionsList = append(definitionsList, d)
}
func settingDefinitions() (DefinitionMap, error) {
return definitions.GetErr(func() (DefinitionMap, error) {
if err := internal.Init.Do(); err != nil {
return nil, err
}
definitionsMu.Lock()
defer definitionsMu.Unlock()
definitionsUsed = true
return DefinitionMapOf(definitionsList)
})
}
// DefinitionMapOf returns a [DefinitionMap] with the specified settings,
// or an error if any settings have the same key but different type or scope.
func DefinitionMapOf(settings []*Definition) (DefinitionMap, error) {
m := make(DefinitionMap, len(settings))
for _, s := range settings {
if existing, exists := m[s.key]; exists {
if existing.Equal(s) {
// Ignore duplicate setting definitions if they match. It is acceptable
// if the same policy setting was registered more than once
// (e.g. by the syspolicy package itself and by iOS/Android code).
existing.platforms.mergeFrom(s.platforms)
continue
}
return nil, fmt.Errorf("duplicate policy definition: %q", s.key)
}
m[s.key] = s
}
return m, nil
}
// SetDefinitionsForTest allows to register the specified setting definitions
// for the test duration. It is not concurrency-safe, but unlike [Register],
// it does not panic and can be called anytime.
// It returns an error if ds contains two different settings with the same [Key].
func SetDefinitionsForTest(tb testenv.TB, ds ...*Definition) error {
m, err := DefinitionMapOf(ds)
if err != nil {
return err
}
definitions.SetForTest(tb, m, err)
return nil
}
// DefinitionOf returns a setting definition by key,
// or [ErrNoSuchKey] if the specified key does not exist,
// or an error if there are conflicting policy definitions.
func DefinitionOf(k pkey.Key) (*Definition, error) {
ds, err := settingDefinitions()
if err != nil {
return nil, err
}
if d, ok := ds[k]; ok {
return d, nil
}
return nil, ErrNoSuchKey
}
// Definitions returns all registered setting definitions,
// or an error if different policies were registered under the same name.
func Definitions() ([]*Definition, error) {
ds, err := settingDefinitions()
if err != nil {
return nil, err
}
res := make([]*Definition, 0, len(ds))
for _, d := range ds {
res = append(res, d)
}
return res, nil
}
// PlatformList is a list of OSes.
// An empty list indicates that all possible platforms are supported.
type PlatformList []string
// Has reports whether l contains the target platform.
func (ls PlatformList) Has(target string) bool {
if len(ls) == 0 {
return true
}
return slices.ContainsFunc(ls, func(os string) bool {
return strings.EqualFold(os, target)
})
}
// HasCurrent is like Has, but for the current platform.
func (ls PlatformList) HasCurrent() bool {
return ls.Has(internal.OS())
}
// mergeFrom merges l2 into l. Since an empty list indicates no platform restrictions,
// if either l or l2 is empty, the merged result in l will also be empty.
func (ls *PlatformList) mergeFrom(l2 PlatformList) {
switch {
case len(*ls) == 0:
// No-op. An empty list indicates no platform restrictions.
case len(l2) == 0:
// Merging with an empty list results in an empty list.
*ls = l2
default:
// Append, sort and dedup.
*ls = append(*ls, l2...)
slices.Sort(*ls)
*ls = slices.Compact(*ls)
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package setting
import (
"errors"
"iter"
"maps"
"slices"
"strings"
"time"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
xmaps "golang.org/x/exp/maps"
"tailscale.com/util/deephash"
"tailscale.com/util/syspolicy/pkey"
)
// Snapshot is an immutable collection of ([Key], [RawItem]) pairs, representing
// a set of policy settings applied at a specific moment in time.
// A nil pointer to [Snapshot] is valid.
type Snapshot struct {
m map[pkey.Key]RawItem
sig deephash.Sum // of m
summary Summary
}
// NewSnapshot returns a new [Snapshot] with the specified items and options.
func NewSnapshot(items map[pkey.Key]RawItem, opts ...SummaryOption) *Snapshot {
return &Snapshot{m: xmaps.Clone(items), sig: deephash.Hash(&items), summary: SummaryWith(opts...)}
}
// All returns an iterator over policy settings in s. The iteration order is not
// specified and is not guaranteed to be the same from one call to the next.
func (s *Snapshot) All() iter.Seq2[pkey.Key, RawItem] {
if s == nil {
return func(yield func(pkey.Key, RawItem) bool) {}
}
return maps.All(s.m)
}
// Get returns the value of the policy setting with the specified key
// or nil if it is not configured or has an error.
func (s *Snapshot) Get(k pkey.Key) any {
v, _ := s.GetErr(k)
return v
}
// GetErr returns the value of the policy setting with the specified key,
// [ErrNotConfigured] if it is not configured, or an error returned by
// the policy Store if the policy setting could not be read.
func (s *Snapshot) GetErr(k pkey.Key) (any, error) {
if s != nil {
if s, ok := s.m[k]; ok {
return s.Value(), s.Error()
}
}
return nil, ErrNotConfigured
}
// GetSetting returns the untyped policy setting with the specified key and true
// if a policy setting with such key has been configured;
// otherwise, it returns zero, false.
func (s *Snapshot) GetSetting(k pkey.Key) (setting RawItem, ok bool) {
setting, ok = s.m[k]
return setting, ok
}
// Equal reports whether s and s2 are equal.
func (s *Snapshot) Equal(s2 *Snapshot) bool {
if s == s2 {
return true
}
if !s.EqualItems(s2) {
return false
}
return s.Summary() == s2.Summary()
}
// EqualItems reports whether items in s and s2 are equal.
func (s *Snapshot) EqualItems(s2 *Snapshot) bool {
if s == s2 {
return true
}
if s.Len() != s2.Len() {
return false
}
if s.Len() == 0 {
return true
}
return s.sig == s2.sig
}
// Keys return an iterator over keys in s. The iteration order is not specified
// and is not guaranteed to be the same from one call to the next.
func (s *Snapshot) Keys() iter.Seq[pkey.Key] {
if s.m == nil {
return func(yield func(pkey.Key) bool) {}
}
return maps.Keys(s.m)
}
// Len reports the number of [RawItem]s in s.
func (s *Snapshot) Len() int {
if s == nil {
return 0
}
return len(s.m)
}
// Summary returns information about s as a whole rather than about specific [RawItem]s in it.
func (s *Snapshot) Summary() Summary {
if s == nil {
return Summary{}
}
return s.summary
}
// String implements [fmt.Stringer]
func (s *Snapshot) String() string {
if s.Len() == 0 && s.Summary().IsEmpty() {
return "{Empty}"
}
var sb strings.Builder
if !s.summary.IsEmpty() {
sb.WriteRune('{')
if s.Len() == 0 {
sb.WriteString("Empty, ")
}
sb.WriteString(s.summary.String())
sb.WriteRune('}')
}
for _, k := range slices.Sorted(s.Keys()) {
if sb.Len() != 0 {
sb.WriteRune('\n')
}
sb.WriteString(string(k))
sb.WriteString(" = ")
sb.WriteString(s.m[k].String())
}
return sb.String()
}
// snapshotJSON holds JSON-marshallable data for [Snapshot].
type snapshotJSON struct {
Summary Summary `json:",omitzero"`
Settings map[pkey.Key]RawItem `json:",omitempty"`
}
var (
_ jsonv2.MarshalerTo = (*Snapshot)(nil)
_ jsonv2.UnmarshalerFrom = (*Snapshot)(nil)
)
// As of 2025-07-28, jsonv2 no longer has a default representation for [time.Duration],
// so we need to provide a custom marshaler.
//
// This is temporary until the decision on the default representation is made
// (see https://github.com/golang/go/issues/71631#issuecomment-2981670799).
//
// In the future, we might either use the default representation (if compatible with
// [time.Duration.String]) or specify something like json.WithFormat[time.Duration]("units")
// when golang/go#71664 is implemented.
//
// TODO(nickkhyl): revisit this when the decision on the default [time.Duration]
// representation is made in golang/go#71631 and/or golang/go#71664 is implemented.
var formatDurationAsUnits = jsonv2.JoinOptions(
jsonv2.WithMarshalers(jsonv2.MarshalToFunc(func(e *jsontext.Encoder, t time.Duration) error {
return e.WriteToken(jsontext.String(t.String()))
})),
)
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (s *Snapshot) MarshalJSONTo(out *jsontext.Encoder) error {
data := &snapshotJSON{}
if s != nil {
data.Summary = s.summary
data.Settings = s.m
}
return jsonv2.MarshalEncode(out, data, formatDurationAsUnits)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (s *Snapshot) UnmarshalJSONFrom(in *jsontext.Decoder) error {
if s == nil {
return errors.New("s must not be nil")
}
data := &snapshotJSON{}
if err := jsonv2.UnmarshalDecode(in, data); err != nil {
return err
}
*s = Snapshot{m: data.Settings, sig: deephash.Hash(&data.Settings), summary: data.Summary}
return nil
}
// MarshalJSON implements [json.Marshaler].
func (s *Snapshot) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(s) // uses MarshalJSONTo
}
// UnmarshalJSON implements [json.Unmarshaler].
func (s *Snapshot) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, s) // uses UnmarshalJSONFrom
}
// MergeSnapshots returns a [Snapshot] that contains all [RawItem]s
// from snapshot1 and snapshot2 and the [Summary] with the narrower [PolicyScope].
// If there's a conflict between policy settings in the two snapshots,
// the policy settings from the snapshot with the broader scope take precedence.
// In other words, policy settings configured for the [DeviceScope] win
// over policy settings configured for a user scope.
func MergeSnapshots(snapshot1, snapshot2 *Snapshot) *Snapshot {
scope1, ok1 := snapshot1.Summary().Scope().GetOk()
scope2, ok2 := snapshot2.Summary().Scope().GetOk()
if ok1 && ok2 && scope1.StrictlyContains(scope2) {
// Swap snapshots if snapshot1 has higher precedence than snapshot2.
snapshot1, snapshot2 = snapshot2, snapshot1
}
if snapshot2.Len() == 0 {
return snapshot1
}
summaryOpts := make([]SummaryOption, 0, 2)
if scope, ok := snapshot1.Summary().Scope().GetOk(); ok {
// Use the scope from snapshot1, if present, which is the more specific snapshot.
summaryOpts = append(summaryOpts, scope)
}
if snapshot1.Len() == 0 {
if origin, ok := snapshot2.Summary().Origin().GetOk(); ok {
// Use the origin from snapshot2 if snapshot1 is empty.
summaryOpts = append(summaryOpts, origin)
}
return &Snapshot{snapshot2.m, snapshot2.sig, SummaryWith(summaryOpts...)}
}
m := make(map[pkey.Key]RawItem, snapshot1.Len()+snapshot2.Len())
xmaps.Copy(m, snapshot1.m)
xmaps.Copy(m, snapshot2.m) // snapshot2 has higher precedence
return &Snapshot{m, deephash.Hash(&m), SummaryWith(summaryOpts...)}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package setting
import (
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"tailscale.com/types/opt"
)
// Summary is an immutable [PolicyScope] and [Origin].
type Summary struct {
data summary
}
type summary struct {
Scope opt.Value[PolicyScope] `json:",omitzero"`
Origin opt.Value[Origin] `json:",omitzero"`
}
// SummaryWith returns a [Summary] with the specified options.
func SummaryWith(opts ...SummaryOption) Summary {
var summary Summary
for _, o := range opts {
o.applySummaryOption(&summary)
}
return summary
}
// IsEmpty reports whether s is empty.
func (s Summary) IsEmpty() bool {
return s == Summary{}
}
// Scope reports the [PolicyScope] in s.
func (s Summary) Scope() opt.Value[PolicyScope] {
return s.data.Scope
}
// Origin reports the [Origin] in s.
func (s Summary) Origin() opt.Value[Origin] {
return s.data.Origin
}
// String implements [fmt.Stringer].
func (s Summary) String() string {
if s.IsEmpty() {
return "{Empty}"
}
if origin, ok := s.data.Origin.GetOk(); ok {
return origin.String()
}
return s.data.Scope.String()
}
var (
_ jsonv2.MarshalerTo = (*Summary)(nil)
_ jsonv2.UnmarshalerFrom = (*Summary)(nil)
)
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (s Summary) MarshalJSONTo(out *jsontext.Encoder) error {
return jsonv2.MarshalEncode(out, &s.data)
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (s *Summary) UnmarshalJSONFrom(in *jsontext.Decoder) error {
return jsonv2.UnmarshalDecode(in, &s.data)
}
// MarshalJSON implements [json.Marshaler].
func (s Summary) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(s) // uses MarshalJSONTo
}
// UnmarshalJSON implements [json.Unmarshaler].
func (s *Summary) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, s) // uses UnmarshalJSONFrom
}
// SummaryOption is an option that configures [Summary]
// The following are allowed options:
//
// - [Summary]
// - [PolicyScope]
// - [Origin]
type SummaryOption interface {
applySummaryOption(summary *Summary)
}
func (s PolicyScope) applySummaryOption(summary *Summary) {
summary.data.Scope.Set(s)
}
func (o Origin) applySummaryOption(summary *Summary) {
summary.data.Origin.Set(o)
if !summary.data.Scope.IsSet() {
summary.data.Scope.Set(o.Scope())
}
}
func (s Summary) applySummaryOption(summary *Summary) {
*summary = s
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package testenv
import (
"bytes"
"flag"
"runtime"
"sync"
)
// stackBufPool holds buffers for [InSynctestBubble]'s runtime.Stack calls,
// which would otherwise allocate on every call: the compiler cannot prove
// that a stack-allocated buffer does not escape into runtime.Stack. Plain
// byte arrays, unlike the zstd coders that motivated InSynctestBubble, are
// safe to share across synctest bubble boundaries.
var stackBufPool = sync.Pool{New: func() any { return new([128]byte) }}
// InSynctestBubble reports whether the current goroutine is running within a
// testing/synctest bubble.
//
// As of Go 1.26 there is no public API to query bubble membership
// (internal/synctest.IsInBubble is runtime-internal), so this instead
// looks for the "synctest bubble N" annotation that the runtime renders
// in the current goroutine's [runtime.Stack] header. If a future Go
// release changes that annotation, this reports false; tests that depend
// on it for correctness (such as tailscale.com/util/zstdframe's) should
// exercise the misdetection failure mode so the breakage is loud.
func InSynctestBubble() bool {
// Bubbles only exist within tests, so skip the (relatively) expensive
// stack header check in non-test binaries. This deliberately does not
// use InTest: InSynctestBubble may be reached from package init
// functions (before the testing package has registered its flags),
// and InTest would permanently latch a false result there. Bubbles
// cannot exist during init, so returning false then is correct.
if flag.Lookup("test.v") == nil {
return false
}
// The current goroutine's header looks like:
// goroutine 9 [running, synctest bubble 1]:
// A 128-byte buffer always fits the header, and runtime.Stack
// truncates the rest.
buf := stackBufPool.Get().(*[128]byte)
defer stackBufPool.Put(buf)
n := runtime.Stack(buf[:], false)
return bytes.Contains(buf[:n], []byte(" synctest bubble "))
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package testenv provides utility functions for tests. It does not depend on
// the `testing` package to allow usage in non-test code.
package testenv
import (
"context"
"flag"
"io"
"tailscale.com/types/lazy"
)
var lazyInTest lazy.SyncValue[bool]
// InTest reports whether the current binary is a test binary.
func InTest() bool {
return lazyInTest.Get(func() bool {
return flag.Lookup("test.v") != nil
})
}
// Verbose reports whether the test binary is running with verbose
// output (the -test.v flag), like testing.Verbose but without
// importing the testing package.
func Verbose() bool {
f := flag.Lookup("test.v")
if f == nil {
return false
}
// The flag's String value is "false" when off, and "true" or
// "test2json" when on.
return f.Value.String() != "false"
}
// TB is testing.TB, to avoid importing "testing" in non-test code.
type TB interface {
ArtifactDir() string
Attr(key, value string)
Cleanup(func())
Error(args ...any)
Errorf(format string, args ...any)
Fail()
FailNow()
Failed() bool
Fatal(args ...any)
Fatalf(format string, args ...any)
Helper()
Log(args ...any)
Logf(format string, args ...any)
Name() string
Output() io.Writer
Setenv(key, value string)
Chdir(dir string)
Skip(args ...any)
SkipNow()
Skipf(format string, args ...any)
Skipped() bool
TempDir() string
Context() context.Context
}
// InParallelTest reports whether t is running as a parallel test.
//
// Use of this function taints t such that its Parallel method (assuming t is an
// actual *testing.T) will panic if called after this function.
func InParallelTest(t TB) (isParallel bool) {
defer func() {
if r := recover(); r != nil {
isParallel = true
}
}()
t.Chdir(".") // panics in a t.Parallel test
return false
}
// AssertInTest panics if called outside of a test binary.
func AssertInTest() {
if !InTest() {
panic("func called outside of test binary")
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package truncate provides a utility function for safely truncating UTF-8
// strings to a fixed length, respecting multi-byte codepoints.
package truncate
// String returns a prefix of a UTF-8 string s, having length no greater than n
// bytes. If s exceeds this length, it is truncated at a point ≤ n so that the
// result does not end in a partial UTF-8 encoding. If s is less than or equal
// to this length, it is returned unmodified.
func String[String ~string | ~[]byte](s String, n int) String {
if n >= len(s) {
return s
}
// Back up until we find the beginning of a UTF-8 encoding.
for n > 0 && s[n-1]&0xc0 == 0x80 { // 0x10... is a continuation byte
n--
}
// If we're at the beginning of a multi-byte encoding, back up one more to
// skip it. It's possible the value was already complete, but it's simpler
// if we only have to check in one direction.
//
// Otherwise, we have a single-byte code (0x00... or 0x01...).
if n > 0 && s[n-1]&0xc0 == 0xc0 { // 0x11... starts a multibyte encoding
n--
}
return s[:n]
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// This file contains user-facing metrics that are used by multiple packages.
// Use it to define more common metrics. Any changes to the registry and
// metric types should be in usermetric.go.
package usermetric
import (
"sync"
"tailscale.com/feature/buildfeatures"
)
// Metrics contains user-facing metrics that are used by multiple packages.
type Metrics struct {
initOnce sync.Once
droppedPacketsInbound *MultiLabelMap[DropLabels]
droppedPacketsOutbound *MultiLabelMap[DropLabels]
}
// DropReason is the reason why a packet was dropped.
type DropReason string
const (
// ReasonACL means that the packet was not permitted by ACL.
ReasonACL DropReason = "acl"
// ReasonMulticast means that the packet was dropped because it was a multicast packet.
ReasonMulticast DropReason = "multicast"
// ReasonLinkLocalUnicast means that the packet was dropped because it was a link-local unicast packet.
ReasonLinkLocalUnicast DropReason = "link_local_unicast"
// ReasonTooShort means that the packet was dropped because it was a bad packet,
// this could be due to a short packet.
ReasonTooShort DropReason = "too_short"
// ReasonFragment means that the packet was dropped because it was an IP fragment.
ReasonFragment DropReason = "fragment"
// ReasonUnknownProtocol means that the packet was dropped because it was an unknown protocol.
ReasonUnknownProtocol DropReason = "unknown_protocol"
// ReasonError means that the packet was dropped because of an error.
ReasonError DropReason = "error"
)
// DropLabels contains common label(s) for dropped packet counters.
type DropLabels struct {
Reason DropReason
}
// initOnce initializes the common metrics.
func (r *Registry) initOnce() {
if !buildfeatures.HasUserMetrics {
return
}
r.m.initOnce.Do(func() {
r.m.droppedPacketsInbound = NewMultiLabelMapWithRegistry[DropLabels](
r,
"tailscaled_inbound_dropped_packets_total",
"counter",
"Counts the number of dropped packets received by the node from other peers",
)
r.m.droppedPacketsOutbound = NewMultiLabelMapWithRegistry[DropLabels](
r,
"tailscaled_outbound_dropped_packets_total",
"counter",
"Counts the number of packets dropped while being sent to other peers",
)
})
}
// DroppedPacketsOutbound returns the outbound dropped packet metric, creating it
// if necessary.
func (r *Registry) DroppedPacketsOutbound() *MultiLabelMap[DropLabels] {
r.initOnce()
return r.m.droppedPacketsOutbound
}
// DroppedPacketsInbound returns the inbound dropped packet metric.
func (r *Registry) DroppedPacketsInbound() *MultiLabelMap[DropLabels] {
r.initOnce()
return r.m.droppedPacketsInbound
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_usermetrics
// Package usermetric provides a container and handler
// for user-facing metrics.
package usermetric
import (
"expvar"
"fmt"
"io"
"net/http"
"strings"
"tailscale.com/metrics"
"tailscale.com/tsweb/varz"
"tailscale.com/util/set"
)
// Registry tracks user-facing metrics of various Tailscale subsystems.
type Registry struct {
vars expvar.Map
// m contains common metrics owned by the registry.
m Metrics
}
// MultiLabelMap is an alias for metrics.MultiLabelMap in the common case,
// or an alias to a lighter type when usermetrics are omitted from the build.
type MultiLabelMap[T comparable] = metrics.MultiLabelMap[T]
// NewMultiLabelMapWithRegistry creates and register a new
// MultiLabelMap[T] variable with the given name and returns it.
// The variable is registered with the userfacing metrics package.
//
// Note that usermetric are not protected against duplicate
// metrics name. It is the caller's responsibility to ensure that
// the name is unique.
func NewMultiLabelMapWithRegistry[T comparable](m *Registry, name string, promType, helpText string) *metrics.MultiLabelMap[T] {
ml := &metrics.MultiLabelMap[T]{
Type: promType,
Help: helpText,
}
var zero T
_ = metrics.LabelString(zero) // panic early if T is invalid
m.vars.Set(name, ml)
return ml
}
// Gauge is a gauge metric with no labels.
type Gauge struct {
m *expvar.Float
help string
}
// NewGauge creates and register a new gauge metric with the given name and help text.
func (r *Registry) NewGauge(name, help string) *Gauge {
g := &Gauge{&expvar.Float{}, help}
r.vars.Set(name, g)
return g
}
// Set sets the gauge to the given value.
func (g *Gauge) Set(v float64) {
if g == nil {
return
}
g.m.Set(v)
}
// String returns the string of the underlying expvar.Float.
// This satisfies the expvar.Var interface.
func (g *Gauge) String() string {
if g == nil {
return ""
}
return g.m.String()
}
// WritePrometheus writes the gauge metric in Prometheus format to the given writer.
// This satisfies the varz.PrometheusWriter interface.
func (g *Gauge) WritePrometheus(w io.Writer, name string) {
io.WriteString(w, "# TYPE ")
io.WriteString(w, name)
io.WriteString(w, " gauge\n")
if g.help != "" {
io.WriteString(w, "# HELP ")
io.WriteString(w, name)
io.WriteString(w, " ")
io.WriteString(w, g.help)
io.WriteString(w, "\n")
}
io.WriteString(w, name)
fmt.Fprintf(w, " %v\n", g.m.Value())
}
// Handler returns a varz.Handler that serves the userfacing expvar contained
// in this package.
func (r *Registry) Handler(w http.ResponseWriter, req *http.Request) {
varz.ExpvarDoHandler(r.vars.Do)(w, req)
}
// String returns the string representation of all the metrics and their
// values in the registry. It is useful for debugging.
func (r *Registry) String() string {
var sb strings.Builder
r.vars.Do(func(kv expvar.KeyValue) {
fmt.Fprintf(&sb, "%s: %v\n", kv.Key, kv.Value)
})
return sb.String()
}
// Metrics returns the name of all the metrics in the registry.
func (r *Registry) MetricNames() []string {
ret := make(set.Set[string])
r.vars.Do(func(kv expvar.KeyValue) {
ret.Add(kv.Key)
})
return ret.Slice()
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package vizerror provides types and utility funcs for handling visible errors
// that are safe to display to end users.
package vizerror
import (
"errors"
"fmt"
)
// Error is an error that is safe to display to end users.
type Error struct {
publicErr error // visible to end users
wrapped error // internal
}
// Error implements the error interface. The returned string is safe to display
// to end users.
func (e Error) Error() string {
return e.publicErr.Error()
}
// New returns an error that formats as the given text. It always returns a vizerror.Error.
func New(publicMsg string) error {
err := errors.New(publicMsg)
return Error{
publicErr: err,
wrapped: err,
}
}
// Errorf returns an Error with the specified publicMsgFormat and values. It always returns a vizerror.Error.
//
// Warning: avoid using an error as one of the format arguments, as this will cause the text
// of that error to be displayed to the end user (which is probably not what you want).
func Errorf(publicMsgFormat string, a ...any) error {
err := fmt.Errorf(publicMsgFormat, a...)
return Error{
publicErr: err,
wrapped: err,
}
}
// Unwrap returns the underlying error.
//
// If the Error was constructed using [WrapWithMessage], this is the wrapped (internal) error
// and not the user-visible error message.
func (e Error) Unwrap() error {
return e.wrapped
}
// Wrap wraps publicErr with a vizerror.Error.
//
// Deprecated: this is almost always the wrong thing to do. Are you really sure
// you know exactly what err.Error() will stringify to and be safe to show to
// users? [WrapWithMessage] is probably what you want.
func Wrap(publicErr error) error {
if publicErr == nil {
return nil
}
return Error{publicErr: publicErr, wrapped: publicErr}
}
// WrapWithMessage wraps the given error with a message that's safe to display
// to end users. The text of the wrapped error will not be displayed to end
// users.
//
// WrapWithMessage should almost always be preferred to [Wrap].
func WrapWithMessage(wrapped error, publicMsg string) error {
return Error{
publicErr: errors.New(publicMsg),
wrapped: wrapped,
}
}
// As returns the first vizerror.Error in err's chain.
func As(err error) (e Error, ok bool) {
return errors.AsType[Error](err)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package zstdframe
import (
"math/bits"
"strconv"
"sync"
"github.com/klauspost/compress/zstd"
"tailscale.com/util/must"
"tailscale.com/util/testenv"
)
// Option is an option that can be passed to [AppendEncode] or [AppendDecode].
type Option interface{ isOption() }
type encoderLevel int
// Constants that implement [Option] and can be passed to [AppendEncode].
const (
FastestCompression = encoderLevel(zstd.SpeedFastest)
DefaultCompression = encoderLevel(zstd.SpeedDefault)
BetterCompression = encoderLevel(zstd.SpeedBetterCompression)
BestCompression = encoderLevel(zstd.SpeedBestCompression)
)
func (encoderLevel) isOption() {}
// EncoderLevel specifies the compression level when encoding.
//
// This exists for compatibility with [zstd.EncoderLevel] values.
// Most usages should directly use one of the following constants:
// - [FastestCompression]
// - [DefaultCompression]
// - [BetterCompression]
// - [BestCompression]
//
// By default, [DefaultCompression] is chosen.
// This option is ignored when decoding.
func EncoderLevel(level zstd.EncoderLevel) Option { return encoderLevel(level) }
type withChecksum bool
func (withChecksum) isOption() {}
// WithChecksum specifies whether to produce a checksum when encoding,
// or whether to verify the checksum when decoding.
// By default, checksums are produced and verified.
func WithChecksum(check bool) Option { return withChecksum(check) }
type maxDecodedSize uint64
func (maxDecodedSize) isOption() {}
type maxDecodedSizeLog2 uint8 // uint8 avoids allocation when storing into interface
func (maxDecodedSizeLog2) isOption() {}
// MaxDecodedSize specifies the maximum decoded size and
// is used to protect against hostile content.
// By default, there is no limit.
// This option is ignored when encoding.
func MaxDecodedSize(maxSize uint64) Option {
if bits.OnesCount64(maxSize) == 1 {
return maxDecodedSizeLog2(log2(maxSize))
}
return maxDecodedSize(maxSize)
}
type maxWindowSizeLog2 uint8 // uint8 avoids allocation when storing into interface
func (maxWindowSizeLog2) isOption() {}
// MaxWindowSize specifies the maximum window size, which must be a power-of-two
// and be in the range of [[zstd.MinWindowSize], [zstd.MaxWindowSize]].
//
// The compression or decompression algorithm will use a LZ77 rolling window
// no larger than the specified size. The compression ratio will be
// adversely affected, but memory requirements will be lower.
// When decompressing, an error is reported if a LZ77 back reference exceeds
// the specified maximum window size.
//
// For decompression, [MaxDecodedSize] is generally more useful.
func MaxWindowSize(maxSize uint64) Option {
switch {
case maxSize < zstd.MinWindowSize:
panic("maximum window size cannot be less than " + strconv.FormatUint(zstd.MinWindowSize, 10))
case bits.OnesCount64(maxSize) != 1:
panic("maximum window size must be a power-of-two")
case maxSize > zstd.MaxWindowSize:
panic("maximum window size cannot be greater than " + strconv.FormatUint(zstd.MaxWindowSize, 10))
default:
return maxWindowSizeLog2(log2(maxSize))
}
}
type lowMemory bool
func (lowMemory) isOption() {}
// LowMemory specifies that the encoder and decoder should aim to use
// lower amounts of memory at the cost of speed.
// By default, more memory used for better speed.
func LowMemory(low bool) Option { return lowMemory(low) }
// poolCoders reports whether encoders and decoders should be pooled for
// reuse across calls.
//
// Pooling is disabled within testing/synctest bubbles. The zstd Encoder and
// Decoder types use channels internally, and the Go runtime kills the
// process when a channel created within a bubble is used from outside that
// bubble. A process-wide pool shared between bubbled and non-bubbled
// goroutines does exactly that, so bubbled goroutines instead construct a
// fresh coder per call and let the GC reclaim it.
func poolCoders() bool { return !testenv.InSynctestBubble() }
var encoderPools sync.Map // map[encoderOptions]*sync.Pool -> *zstd.Encoder
type encoderOptions struct {
level zstd.EncoderLevel
maxWindowLog2 uint8
checksum bool
lowMemory bool
}
type encoder struct {
pool *sync.Pool
*zstd.Encoder
}
func getEncoder(opts ...Option) encoder {
eopts := encoderOptions{level: zstd.SpeedDefault, checksum: true}
for _, opt := range opts {
switch opt := opt.(type) {
case encoderLevel:
eopts.level = zstd.EncoderLevel(opt)
case maxWindowSizeLog2:
eopts.maxWindowLog2 = uint8(opt)
case withChecksum:
eopts.checksum = bool(opt)
case lowMemory:
eopts.lowMemory = bool(opt)
}
}
var pool *sync.Pool
var enc *zstd.Encoder
if poolCoders() {
vpool, ok := encoderPools.Load(eopts)
if !ok {
vpool, _ = encoderPools.LoadOrStore(eopts, new(sync.Pool))
}
pool = vpool.(*sync.Pool)
enc, _ = pool.Get().(*zstd.Encoder)
}
if enc == nil {
var noopts int
zopts := [...]zstd.EOption{
// Set concurrency=1 to ensure synchronous operation.
zstd.WithEncoderConcurrency(1),
// In stateless compression, the data is already in a single buffer,
// so we might as well encode it as a single segment,
// which ensures that the Frame_Content_Size is always populated,
// informing decoders up-front the expected decompressed size.
zstd.WithSingleSegment(true),
// Ensure strict compliance with RFC 8878, section 3.1.,
// where zstandard "is made up of one or more frames".
zstd.WithZeroFrames(true),
zstd.WithEncoderLevel(eopts.level),
zstd.WithEncoderCRC(eopts.checksum),
zstd.WithLowerEncoderMem(eopts.lowMemory),
nil, // reserved for zstd.WithWindowSize
}
if eopts.maxWindowLog2 > 0 {
zopts[len(zopts)-noopts-1] = zstd.WithWindowSize(1 << eopts.maxWindowLog2)
} else {
noopts++
}
enc = must.Get(zstd.NewWriter(nil, zopts[:len(zopts)-noopts]...))
}
return encoder{pool, enc}
}
func putEncoder(e encoder) {
if e.pool != nil {
e.pool.Put(e.Encoder)
}
}
var decoderPools sync.Map // map[decoderOptions]*sync.Pool -> *zstd.Decoder
type decoderOptions struct {
maxSizeLog2 uint8
maxWindowLog2 uint8
checksum bool
lowMemory bool
}
type decoder struct {
pool *sync.Pool
*zstd.Decoder
maxSize uint64
}
func getDecoder(opts ...Option) decoder {
maxSize := uint64(1 << 63)
dopts := decoderOptions{maxSizeLog2: 63, checksum: true}
for _, opt := range opts {
switch opt := opt.(type) {
case maxDecodedSizeLog2:
maxSize = 1 << uint8(opt)
dopts.maxSizeLog2 = uint8(opt)
case maxDecodedSize:
maxSize = uint64(opt)
dopts.maxSizeLog2 = uint8(log2(maxSize))
case maxWindowSizeLog2:
dopts.maxWindowLog2 = uint8(opt)
case withChecksum:
dopts.checksum = bool(opt)
case lowMemory:
dopts.lowMemory = bool(opt)
}
}
var pool *sync.Pool
var dec *zstd.Decoder
if poolCoders() {
vpool, ok := decoderPools.Load(dopts)
if !ok {
vpool, _ = decoderPools.LoadOrStore(dopts, new(sync.Pool))
}
pool = vpool.(*sync.Pool)
dec, _ = pool.Get().(*zstd.Decoder)
}
if dec == nil {
var noopts int
zopts := [...]zstd.DOption{
// Set concurrency=1 to ensure synchronous operation.
zstd.WithDecoderConcurrency(1),
zstd.WithDecoderMaxMemory(1 << min(max(10, dopts.maxSizeLog2), 63)),
zstd.IgnoreChecksum(!dopts.checksum),
zstd.WithDecoderLowmem(dopts.lowMemory),
nil, // reserved for zstd.WithDecoderMaxWindow
}
if dopts.maxWindowLog2 > 0 {
zopts[len(zopts)-noopts-1] = zstd.WithDecoderMaxWindow(1 << dopts.maxWindowLog2)
} else {
noopts++
}
dec = must.Get(zstd.NewReader(nil, zopts[:len(zopts)-noopts]...))
}
return decoder{pool, dec, maxSize}
}
func putDecoder(d decoder) {
if d.pool != nil {
d.pool.Put(d.Decoder)
}
}
func (d decoder) DecodeAll(src, dst []byte) ([]byte, error) {
// We only configure DecodeAll to enforce MaxDecodedSize by powers-of-two.
// Perform a more fine grain check based on the exact value.
dst2, err := d.Decoder.DecodeAll(src, dst)
if err == nil && uint64(len(dst2)-len(dst)) > d.maxSize {
err = zstd.ErrDecoderSizeExceeded
}
return dst2, err
}
// log2 computes log2 of x rounded up to the nearest integer.
func log2(x uint64) int { return 64 - bits.LeadingZeros64(x-1) }
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package zstdframe provides functionality for encoding and decoding
// independently compressed zstandard frames.
package zstdframe
import (
"encoding/binary"
"io"
"github.com/klauspost/compress/zstd"
)
// The Go zstd API surface is not ergonomic:
//
// - Options are set via NewReader and NewWriter and immutable once set.
//
// - Stateless operations like EncodeAll and DecodeAll are methods on
// the Encoder and Decoder types, which implies that options cannot be
// changed without allocating an entirely new Encoder or Decoder.
//
// This is further strange as Encoder and Decoder types are either
// stateful or stateless objects depending on semantic context.
//
// - By default, the zstd package tries to be overly clever by spawning off
// multiple goroutines to do work, which can lead to both excessive fanout
// of resources and also subtle race conditions. Also, each Encoder/Decoder
// never relinquish resources, which makes it unsuitable for lower memory.
// We work around the zstd defaults by setting concurrency=1 on each coder
// and pool individual coders, allowing the Go GC to reclaim unused coders.
//
// See https://github.com/klauspost/compress/issues/264
// See https://github.com/klauspost/compress/issues/479
//
// - The EncodeAll and DecodeAll functions appends to a user-provided buffer,
// but uses a signature opposite of most append-like functions in Go,
// where the output buffer is the second argument, leading to footguns.
// The zstdframe package provides AppendEncode and AppendDecode functions
// that follows Go convention of the first argument being the output buffer
// similar to how the builtin append function operates.
//
// See https://github.com/klauspost/compress/issues/648
//
// - The zstd package is oddly inconsistent about naming. For example,
// IgnoreChecksum vs WithEncoderCRC, or
// WithDecoderLowmem vs WithLowerEncoderMem.
// Most options have a WithDecoder or WithEncoder prefix, but some do not.
//
// The zstdframe package wraps the zstd package and presents a more ergonomic API
// by providing stateless functions that take in variadic options.
// Pooling of resources is handled by this package to avoid each caller
// redundantly performing the same pooling at different call sites.
// TODO: Since compression is CPU bound,
// should we have a semaphore ensure at most one operation per CPU?
// AppendEncode appends the zstandard encoded content of src to dst.
// It emits exactly one frame as a single segment.
func AppendEncode(dst, src []byte, opts ...Option) []byte {
enc := getEncoder(opts...)
defer putEncoder(enc)
return enc.EncodeAll(src, dst)
}
// AppendDecode appends the zstandard decoded content of src to dst.
// The input may consist of zero or more frames.
// Any call that handles untrusted input should specify [MaxDecodedSize].
func AppendDecode(dst, src []byte, opts ...Option) ([]byte, error) {
dec := getDecoder(opts...)
defer putDecoder(dec)
return dec.DecodeAll(src, dst)
}
// NextSize parses the next frame (regardless of whether it is a
// data frame or a metadata frame) and returns the total size of the frame.
// The frame can be skipped by slicing n bytes from b (e.g., b[n:]).
// It report [io.ErrUnexpectedEOF] if the frame is incomplete.
func NextSize(b []byte) (n int, err error) {
// Parse the frame header (RFC 8878, section 3.1.1.).
var frame zstd.Header
if err := frame.Decode(b); err != nil {
return n, err
}
n += frame.HeaderSize
if frame.Skippable {
// Handle skippable frame (RFC 8878, section 3.1.2.).
if len(b[n:]) < int(frame.SkippableSize) {
return n, io.ErrUnexpectedEOF
}
n += int(frame.SkippableSize)
} else {
// Handle one or more Data_Blocks (RFC 8878, section 3.1.1.2.).
for {
if len(b[n:]) < 3 {
return n, io.ErrUnexpectedEOF
}
blockHeader := binary.LittleEndian.Uint32(b[n-1:]) >> 8 // load uint24
lastBlock := (blockHeader >> 0) & ((1 << 1) - 1)
blockType := (blockHeader >> 1) & ((1 << 2) - 1)
blockSize := (blockHeader >> 3) & ((1 << 21) - 1)
n += 3
if blockType == 1 {
// For RLE_Block (RFC 8878, section 3.1.1.2.2.),
// the Block_Content is only a single byte.
blockSize = 1
}
if len(b[n:]) < int(blockSize) {
return n, io.ErrUnexpectedEOF
}
n += int(blockSize)
if lastBlock != 0 {
break
}
}
// Handle optional Content_Checksum (RFC 8878, section 3.1.1.).
if frame.HasCheckSum {
if len(b[n:]) < 4 {
return n, io.ErrUnexpectedEOF
}
n += 4
}
}
return n, nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package tailscaleroot embeds VERSION.txt into the binary.
package tailscaleroot
import (
_ "embed"
"runtime/debug"
)
// VersionDotTxt is the contents of VERSION.txt. Despite the tempting filename,
// this does not necessarily contain the accurate version number of the build, which
// depends on the branch type and how it was built. To get version information, use
// the version package instead.
//
//go:embed VERSION.txt
var VersionDotTxt string
//go:embed ALPINE.txt
var AlpineDockerTag string
// GoToolchainRev is the git hash from github.com/tailscale/go that this release
// should be built using. It may end in a newline.
//
//go:embed go.toolchain.rev
var GoToolchainRev string
// GoToolchainNextRev is like GoToolchainRev, but when using the
// "go.toolchain.next.rev" when TS_GO_NEXT=1 is set in the environment.
//
//go:embed go.toolchain.next.rev
var GoToolchainNextRev string
//lint:ignore U1000 used by tests + assert_ts_toolchain_match.go w/ right build tags
func tailscaleToolchainRev() (gitHash string, ok bool) {
bi, ok := debug.ReadBuildInfo()
if !ok {
return "", false
}
for _, s := range bi.Settings {
if s.Key == "tailscale.toolchain.rev" {
return s.Value, true
}
}
return "", false
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ios
package version
import (
"bytes"
"encoding/hex"
"errors"
"io"
"os"
"path"
"runtime"
"runtime/debug"
"strings"
)
// CmdName returns either the base name of the current binary
// using os.Executable. If os.Executable fails (it shouldn't), then
// "cmd" is returned.
func CmdName() string {
// On non-Windows, the modinfo embedded in the running binary is
// authoritative and avoids re-reading the executable from disk.
// Windows needs the executable-name-based GUI override in cmdName,
// so it still takes the slower path.
if runtime.GOOS != "windows" {
if info, ok := debug.ReadBuildInfo(); ok && info.Path != "" {
return path.Base(info.Path)
}
}
e, err := os.Executable()
if err != nil {
return "cmd"
}
return cmdName(e)
}
func cmdName(exe string) string {
// fallbackName, the lowercase basename of the executable, is what we return if
// we can't find the Go module metadata embedded in the file.
fallbackName := prepExeNameForCmp(exe, runtime.GOARCH)
var ret string
info, err := findModuleInfo(exe)
if err != nil {
return fallbackName
}
// v is like:
// "path\ttailscale.com/cmd/tailscale\nmod\ttailscale.com\t(devel)\t\ndep\tgithub.com/apenwarr/fixconsole\tv0.0.0-20191012055117-5a9f6489cc29\th1:muXWUcay7DDy1/hEQWrYlBy+g0EuwT70sBHg65SeUc4=\ndep\tgithub....
for line := range strings.SplitSeq(info, "\n") {
if goPkg, ok := strings.CutPrefix(line, "path\t"); ok { // like "tailscale.com/cmd/tailscale"
ret = path.Base(goPkg) // goPkg is always forward slashes; use path, not filepath
break
}
}
if runtime.GOOS == "windows" && strings.HasPrefix(ret, "gui") && checkPreppedExeNameForGUI(fallbackName) {
// The GUI binary for internal build system packaging reasons
// has a path of "tailscale.io/win/gui".
// Ignore that name and use fallbackName instead.
return fallbackName
}
if ret == "" {
return fallbackName
}
return ret
}
// findModuleInfo returns the Go module info from the executable file.
func findModuleInfo(file string) (s string, err error) {
f, err := os.Open(file)
if err != nil {
return "", err
}
defer f.Close()
// Scan through f until we find infoStart.
buf := make([]byte, 65536)
start, err := findOffset(f, buf, infoStart)
if err != nil {
return "", err
}
start += int64(len(infoStart))
// Seek to the end of infoStart and scan for infoEnd.
_, err = f.Seek(start, io.SeekStart)
if err != nil {
return "", err
}
end, err := findOffset(f, buf, infoEnd)
if err != nil {
return "", err
}
length := end - start
// As of Aug 2021, tailscaled's mod info was about 2k.
if length > int64(len(buf)) {
return "", errors.New("mod info too large")
}
// We have located modinfo. Read it into buf.
buf = buf[:length]
_, err = f.Seek(start, io.SeekStart)
if err != nil {
return "", err
}
_, err = io.ReadFull(f, buf)
if err != nil {
return "", err
}
return string(buf), nil
}
// findOffset finds the absolute offset of needle in f,
// starting at f's current read position,
// using temporary buffer buf.
func findOffset(f *os.File, buf, needle []byte) (int64, error) {
for {
// Fill buf and look within it.
n, err := f.Read(buf)
if err != nil {
return -1, err
}
i := bytes.Index(buf[:n], needle)
if i < 0 {
// Not found. Rewind a little bit in case we happened to end halfway through needle.
rewind, err := f.Seek(int64(-len(needle)), io.SeekCurrent)
if err != nil {
return -1, err
}
// If we're at EOF and rewound exactly len(needle) bytes, return io.EOF.
_, err = f.ReadAt(buf[:1], rewind+int64(len(needle)))
if err == io.EOF {
return -1, err
}
continue
}
// Found! Figure out exactly where.
cur, err := f.Seek(0, io.SeekCurrent)
if err != nil {
return -1, err
}
return cur - int64(n) + int64(i), nil
}
}
// These constants are taken from rsc.io/goversion.
var (
infoStart, _ = hex.DecodeString("3077af0c9274080241e1c107e6d618e6")
infoEnd, _ = hex.DecodeString("f932433186182072008242104116d8f2")
)
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package version
import (
"strings"
)
// AtLeast returns whether version is at least the specified minimum
// version.
//
// Version comparison in Tailscale is a little complex, because we
// switched "styles" a few times, and additionally have a completely
// separate track of version numbers for OSS-only builds.
//
// AtLeast acts conservatively, returning true only if it's certain
// that version is at least minimum. As a result, it can produce false
// negatives, for example when an OSS build supports a given feature,
// but AtLeast is called with an official release number as the
// minimum
//
// version and minimum can both be either an official Tailscale
// version numbers (major.minor.patch-extracommits-extrastring), or an
// OSS build datestamp (date.YYYYMMDD). For Tailscale version numbers,
// AtLeast also accepts a prefix of a full version, in which case all
// missing fields are assumed to be zero.
func AtLeast(version string, minimum string) bool {
v, ok := parse(version)
if !ok {
return false
}
m, ok := parse(minimum)
if !ok {
return false
}
switch {
case v.Datestamp != 0 && m.Datestamp == 0:
// OSS version vs. Tailscale version
return false
case v.Datestamp == 0 && m.Datestamp != 0:
// Tailscale version vs. OSS version
return false
case v.Datestamp != 0:
// OSS version vs. OSS version
return v.Datestamp >= m.Datestamp
case v.Major == m.Major && v.Minor == m.Minor && v.Patch == m.Patch && v.ExtraCommits == m.ExtraCommits:
// Exactly equal Tailscale versions
return true
case v.Major != m.Major:
return v.Major > m.Major
case v.Minor != m.Minor:
return v.Minor > m.Minor
case v.Patch != m.Patch:
return v.Patch > m.Patch
default:
return v.ExtraCommits > m.ExtraCommits
}
}
type parsed struct {
Major, Minor, Patch, ExtraCommits int // for Tailscale version e.g. e.g. "0.99.1-20"
Datestamp int // for OSS version e.g. "date.20200612"
}
func parse(version string) (parsed, bool) {
if strings.HasPrefix(version, "date.") {
stamp, ok := atoi(version[5:])
if !ok {
return parsed{}, false
}
return parsed{Datestamp: stamp}, true
}
var ret parsed
major, rest, ok := splitNumericPrefix(version)
if !ok {
return parsed{}, false
}
ret.Major = major
if len(rest) == 0 {
return ret, true
}
ret.Minor, rest, ok = splitNumericPrefix(rest[1:])
if !ok {
return parsed{}, false
}
if len(rest) == 0 {
return ret, true
}
// Optional patch version, if the next separator is a dot.
if rest[0] == '.' {
ret.Patch, rest, ok = splitNumericPrefix(rest[1:])
if !ok {
return parsed{}, false
}
if len(rest) == 0 {
return ret, true
}
}
// Ignore trailer like '_1 (Void Linux)'.
if rest[0] == '_' && strings.HasSuffix(rest, " (Void Linux)") {
return ret, true
}
// Optional extraCommits, if the next bit can be completely
// consumed as an integer.
if rest[0] != '-' {
return parsed{}, false
}
var trailer string
ret.ExtraCommits, trailer, ok = splitNumericPrefix(rest[1:])
if !ok || (len(trailer) > 0 && trailer[0] != '-') {
// rest was probably the string trailer, ignore it.
ret.ExtraCommits = 0
}
return ret, true
}
func splitNumericPrefix(s string) (n int, rest string, ok bool) {
for i, r := range s {
if r >= '0' && r <= '9' {
continue
}
ret, ok := atoi(s[:i])
if !ok {
return 0, "", false
}
return ret, s[i:], true
}
ret, ok := atoi(s)
if !ok {
return 0, "", false
}
return ret, "", true
}
const (
maxUint = ^uint(0)
maxInt = int(maxUint >> 1)
)
// atoi parses an int from a string s.
// The bool result reports whether s is a number
// representable by a value of type int.
//
// From Go's runtime/string.go.
func atoi(s string) (int, bool) {
if s == "" {
return 0, false
}
neg := false
if s[0] == '-' {
neg = true
s = s[1:]
}
un := uint(0)
for i := range len(s) {
c := s[i]
if c < '0' || c > '9' {
return 0, false
}
if un > maxUint/10 {
// overflow
return 0, false
}
un *= 10
un1 := un + uint(c) - '0'
if un1 < un {
// overflow
return 0, false
}
un = un1
}
if !neg && un > uint(maxInt) {
return 0, false
}
if neg && un > uint(maxInt)+1 {
return 0, false
}
n := int(un)
if neg {
n = -n
}
return n, true
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package distro reports which distro we're running on.
package distro
import (
"bytes"
"os"
"runtime"
"strconv"
"strings"
"tailscale.com/types/lazy"
"tailscale.com/util/lineiter"
)
type Distro string
const (
Debian = Distro("debian")
Crostini = Distro("crostini") // ChromeOS Crostini Linux container; Debian-based
Arch = Distro("arch")
Synology = Distro("synology")
OpenWrt = Distro("openwrt")
NixOS = Distro("nixos")
QNAP = Distro("qnap")
Pfsense = Distro("pfsense")
OPNsense = Distro("opnsense")
TrueNAS = Distro("truenas")
Gokrazy = Distro("gokrazy")
WDMyCloud = Distro("wdmycloud")
Unraid = Distro("unraid")
Alpine = Distro("alpine")
UBNT = Distro("ubnt") // Ubiquiti Networks
JetKVM = Distro("jetkvm")
)
var distro lazy.SyncValue[Distro]
var isWSL lazy.SyncValue[bool]
// Get returns the current distro, or the empty string if unknown.
func Get() Distro {
return distro.Get(func() Distro {
switch runtime.GOOS {
case "linux":
return linuxDistro()
case "freebsd":
return freebsdDistro()
default:
return Distro("")
}
})
}
// IsWSL reports whether we're running in the Windows Subsystem for Linux.
func IsWSL() bool {
return runtime.GOOS == "linux" && isWSL.Get(func() bool {
// We could look for $WSL_INTEROP instead, however that may be missing if
// the user has started to use systemd in WSL2.
return have("/proc/sys/fs/binfmt_misc/WSLInterop") || have("/mnt/wsl")
})
}
func have(file string) bool {
_, err := os.Stat(file)
return err == nil
}
func haveDir(file string) bool {
fi, err := os.Stat(file)
return err == nil && fi.IsDir()
}
func linuxDistro() Distro {
switch {
case haveDir("/usr/syno"):
return Synology
case have("/usr/local/bin/freenas-debug"):
// TrueNAS Scale runs on debian
return TrueNAS
case have("/usr/bin/ubnt-device-info"):
// UBNT runs on Debian-based systems. This MUST be checked before Debian.
//
// Currently supported product families:
// - UDM (UniFi Dream Machine, UDM-Pro)
return UBNT
case have("/opt/google/cros-containers/bin/garcon"):
// ChromeOS Crostini ships /opt/google/cros-containers/bin/garcon
// in every penguin container. MUST be checked before Debian since
// Crostini is Debian-based.
return Crostini
case have("/etc/debian_version"):
return Debian
case have("/etc/arch-release"):
return Arch
case have("/etc/openwrt_version"):
return OpenWrt
case have("/run/current-system/sw/bin/nixos-version"):
return NixOS
case have("/etc/config/uLinux.conf"):
return QNAP
case haveDir("/gokrazy"):
return Gokrazy
case have("/usr/local/wdmcserver/bin/wdmc.xml"): // Western Digital MyCloud OS3
return WDMyCloud
case have("/usr/sbin/wd_crontab.sh"): // Western Digital MyCloud OS5
return WDMyCloud
case have("/etc/unraid-version"):
return Unraid
case have("/etc/alpine-release"):
return Alpine
case runtime.GOARCH == "arm" && isDeviceModel("JetKVM"):
return JetKVM
}
return ""
}
func isDeviceModel(want string) bool {
if runtime.GOOS != "linux" {
return false
}
v, _ := os.ReadFile("/sys/firmware/devicetree/base/model")
return want == strings.Trim(string(v), "\x00\r\n\t ")
}
func freebsdDistro() Distro {
switch {
case have("/etc/pfSense-rc"):
return Pfsense
case have("/usr/local/sbin/opnsense-shell"):
return OPNsense
case have("/usr/local/bin/freenas-debug"):
// TrueNAS Core runs on FreeBSD
return TrueNAS
}
return ""
}
var dsmVersion lazy.SyncValue[int]
// DSMVersion reports the Synology DSM major version.
//
// If not Synology, it reports 0.
func DSMVersion() int {
if runtime.GOOS != "linux" {
return 0
}
return dsmVersion.Get(func() int {
if Get() != Synology {
return 0
}
// This is set when running as a package:
v, _ := strconv.Atoi(os.Getenv("SYNOPKG_DSM_VERSION_MAJOR"))
if v != 0 {
return v
}
// But when run from the command line, we have to read it from the file:
for lr := range lineiter.File("/etc/VERSION") {
line, err := lr.Value()
if err != nil {
break // but otherwise ignore
}
line = bytes.TrimSpace(line)
if string(line) == `majorversion="7"` {
return 7
}
if string(line) == `majorversion="6"` {
return 6
}
}
return 0
})
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package version
import (
"path/filepath"
"strings"
)
// prepExeNameForCmp strips any extension and arch suffix from exe, and
// lowercases it.
func prepExeNameForCmp(exe, arch string) string {
baseNoExt := strings.ToLower(strings.TrimSuffix(filepath.Base(exe), filepath.Ext(exe)))
archSuffix := "-" + arch
return strings.TrimSuffix(baseNoExt, archSuffix)
}
func checkPreppedExeNameForGUI(preppedExeName string) bool {
return preppedExeName == "tailscale-ipn" || preppedExeName == "tailscale-gui"
}
func isGUIExeName(exe, arch string) bool {
return checkPreppedExeNameForGUI(prepExeNameForCmp(exe, arch))
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package version
import (
"fmt"
"runtime"
"strings"
"sync"
)
var stringLazy = sync.OnceValue(func() string {
var ret strings.Builder
ret.WriteString(Short())
ret.WriteByte('\n')
if IsUnstableBuild() {
fmt.Fprintf(&ret, " track: unstable (dev); frequent updates and bugs are likely\n")
}
if gitCommit() != "" {
fmt.Fprintf(&ret, " tailscale commit: %s%s\n", gitCommit(), dirtyString())
}
fmt.Fprintf(&ret, " long version: %s\n", Long())
if extraGitCommitStamp != "" {
fmt.Fprintf(&ret, " other commit: %s\n", extraGitCommitStamp)
}
if tsGoRev := tailscaleToolchainRev(); tsGoRev != "" {
if len(tsGoRev) > 10 {
tsGoRev = tsGoRev[:10]
}
fmt.Fprintf(&ret, " go version: %s (tailscale/go %s)\n", runtime.Version(), tsGoRev)
} else {
fmt.Fprintf(&ret, " go version: %s\n", runtime.Version())
}
return strings.TrimSpace(ret.String())
})
func String() string {
return stringLazy()
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package version
import (
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"tailscale.com/tailcfg"
"tailscale.com/types/lazy"
)
// AppIdentifierFn, if non-nil, is a callback function that returns the
// application identifier of the running process or an empty string if unknown.
//
// tailscale(d) implementations can set an explicit callback to return an identifier
// for the running process if such a concept exists. The Apple bundle identifier, for example.
var AppIdentifierFn func() string // or nil
const (
macsysBundleID = "io.tailscale.ipn.macsys" // The macsys gui app and CLI
appStoreBundleID = "io.tailscale.ipn.macos" // The App Store gui app and CLI
macsysExtBundleId = "io.tailscale.ipn.macsys.network-extension" // The macsys system extension
appStoreExtBundleId = "io.tailscale.ipn.macos.network-extension" // The App Store network extension
tvOSExtBundleId = "io.tailscale.ipn.ios.network-extension-tvos" // The tvOS network extension
iOSExtBundleId = "io.tailscale.ipn.ios.network-extension" // The iOS network extension
)
// IsMobile reports whether this is a mobile client build.
func IsMobile() bool {
return runtime.GOOS == "android" || runtime.GOOS == "ios"
}
// OS returns runtime.GOOS, except instead of returning "darwin" it returns
// "iOS" or "macOS".
func OS() string {
// If you're wondering why we have this function that just returns
// runtime.GOOS written differently: in the old days, Go reported
// GOOS=darwin for both iOS and macOS, so we needed this function to
// differentiate them. Then a later Go release added GOOS=ios as a separate
// platform, but by then the "iOS" and "macOS" values we'd picked, with that
// exact capitalization, were already baked into databases.
if IsAppleTV() {
return "tvOS"
}
if runtime.GOOS == "ios" {
return "iOS"
}
if runtime.GOOS == "darwin" {
return "macOS"
}
return runtime.GOOS
}
// IsMacGUIVariant reports whether runtime.GOOS=="darwin" and this one of the
// two GUI variants (that is, not tailscaled-on-macOS).
// This predicate should not be used to determine sandboxing properties. It's
// meant for callers to determine whether the NetworkExtension-like auto-netns
// is in effect.
func IsMacGUIVariant() bool {
return IsMacAppStore() || IsMacSysExt()
}
// IsSandboxedMacOS reports whether this process is a sandboxed macOS
// process (either the app or the extension). It is true for the Mac App Store
// and macsys (only its System Extension) variants on macOS, and false for
// tailscaled and the macsys GUI process on macOS.
func IsSandboxedMacOS() bool {
return IsMacAppStore() || IsMacSysExt()
}
// IsMacSys reports whether this process is part of the Standalone variant of
// Tailscale for macOS, either the main GUI process (non-sandboxed) or the
// system extension (sandboxed).
func IsMacSys() bool {
return IsMacSysExt() || IsMacSysGUI()
}
var isMacSysApp lazy.SyncValue[bool]
// IsMacSysGUI reports whether this process is the main, non-sandboxed GUI process
// that ships with the Standalone variant of Tailscale for macOS.
func IsMacSysGUI() bool {
if runtime.GOOS != "darwin" {
return false
}
return isMacSysApp.Get(func() bool {
if AppIdentifierFn != nil {
return AppIdentifierFn() == macsysBundleID
}
// TODO (barnstar): This check should be redundant once all relevant callers
// use AppIdentifierFn.
return strings.Contains(os.Getenv("HOME"), "/Containers/io.tailscale.ipn.macsys/") ||
strings.Contains(os.Getenv("XPC_SERVICE_NAME"), macsysBundleID)
})
}
var isMacSysExt lazy.SyncValue[bool]
// IsMacSysExt reports whether this binary is the system extension shipped as part of
// the standalone "System Extension" (a.k.a. "macsys") version of Tailscale
// for macOS.
func IsMacSysExt() bool {
if runtime.GOOS != "darwin" {
return false
}
return isMacSysExt.Get(func() bool {
if AppIdentifierFn != nil {
return AppIdentifierFn() == macsysExtBundleId
}
// TODO (barnstar): This check should be redundant once all relevant callers
// use AppIdentifierFn.
exe, err := os.Executable()
if err != nil {
return false
}
return filepath.Base(exe) == macsysExtBundleId
})
}
var isMacAppStore lazy.SyncValue[bool]
// IsMacAppStore returns whether this binary is from the App Store version of Tailscale
// for macOS. Returns true for both the network extension and the GUI app.
func IsMacAppStore() bool {
if runtime.GOOS != "darwin" {
return false
}
return isMacAppStore.Get(func() bool {
if AppIdentifierFn != nil {
id := AppIdentifierFn()
return id == appStoreBundleID || id == appStoreExtBundleId
}
// TODO (barnstar): This check should be redundant once all relevant callers
// use AppIdentifierFn.
// Both macsys and app store versions can run CLI executable with
// suffix /Contents/MacOS/Tailscale. Check $HOME to filter out running
// as macsys.
return strings.Contains(os.Getenv("HOME"), "/Containers/io.tailscale.ipn.macos/") ||
strings.Contains(os.Getenv("XPC_SERVICE_NAME"), appStoreBundleID)
})
}
var isMacAppStoreGUI lazy.SyncValue[bool]
// IsMacAppStoreGUI reports whether this binary is the GUI app from the App Store
// version of Tailscale for macOS.
func IsMacAppStoreGUI() bool {
if runtime.GOOS != "darwin" {
return false
}
return isMacAppStoreGUI.Get(func() bool {
if AppIdentifierFn != nil {
return AppIdentifierFn() == appStoreBundleID
}
// TODO (barnstar): This check should be redundant once all relevant callers
// use AppIdentifierFn.
exe, err := os.Executable()
if err != nil {
return false
}
// Check that this is the GUI binary, and it is not sandboxed. The GUI binary
// shipped in the App Store will always have the App Sandbox enabled.
return strings.Contains(exe, "/Tailscale") && !IsMacSysGUI()
})
}
var isAppleTV lazy.SyncValue[bool]
// IsAppleTV reports whether this binary is part of the Tailscale network extension for tvOS.
// Needed because runtime.GOOS returns "ios" otherwise.
func IsAppleTV() bool {
if runtime.GOOS != "ios" {
return false
}
return isAppleTV.Get(func() bool {
if AppIdentifierFn != nil {
return AppIdentifierFn() == tvOSExtBundleId
}
// TODO (barnstar): This check should be redundant once all relevant callers
// use AppIdentifierFn.
return strings.EqualFold(os.Getenv("XPC_SERVICE_NAME"), tvOSExtBundleId)
})
}
var isWindowsGUI lazy.SyncValue[bool]
// IsWindowsGUI reports whether the current process is the Windows GUI.
func IsWindowsGUI() bool {
if runtime.GOOS != "windows" {
return false
}
return isWindowsGUI.Get(func() bool {
exe, err := os.Executable()
if err != nil {
return false
}
// It is okay to use GOARCH here because we're checking whether our
// _own_ process is the GUI.
return isGUIExeName(exe, runtime.GOARCH)
})
}
var isUnstableBuild lazy.SyncValue[bool]
// IsUnstableBuild reports whether this is an unstable build.
// That is, whether its minor version number is odd.
func IsUnstableBuild() bool {
return isUnstableBuild.Get(func() bool {
_, rest, ok := strings.Cut(Short(), ".")
if !ok {
return false
}
minorStr, _, ok := strings.Cut(rest, ".")
if !ok {
return false
}
minor, err := strconv.Atoi(minorStr)
if err != nil {
return false
}
return minor%2 == 1
})
}
// osVariant returns the OS variant string for systems where we support
// multiple ways of running tailscale(d), if any.
//
// For example: "appstore", "macsys", "darwin".
func osVariant() string {
if IsMacAppStore() {
return "appstore"
}
if IsMacSys() {
return "macsys"
}
if runtime.GOOS == "darwin" {
return "darwin"
}
return ""
}
var isDev = sync.OnceValue(func() bool {
return strings.Contains(Short(), "-dev")
})
// Meta is a JSON-serializable type that contains all the version
// information.
type Meta struct {
// MajorMinorPatch is the "major.minor.patch" version string, without
// any hyphenated suffix.
MajorMinorPatch string `json:"majorMinorPatch"`
// IsDev is whether Short contains a -dev suffix. This is whether the build
// is a development build (as opposed to an official stable or unstable
// build stamped in the usual ways). If you just run "go install" or "go
// build" on a dev branch, this will be true.
IsDev bool `json:"isDev,omitempty"`
// Short is MajorMinorPatch but optionally adding "-dev" or "-devYYYYMMDD"
// for dev builds, depending on how it was build.
Short string `json:"short"`
// Long is the full version string, including git commit hash(es) as the
// suffix.
Long string `json:"long"`
// UnstableBranch is whether the build is from an unstable (development)
// branch. That is, it reports whether the minor version is odd.
UnstableBranch bool `json:"unstableBranch,omitempty"`
// GitCommit, if non-empty, is the git commit of the
// github.com/tailscale/tailscale repository at which Tailscale was
// built. Its format is the one returned by `git describe --always
// --exclude "*" --dirty --abbrev=200`.
GitCommit string `json:"gitCommit,omitempty"`
// GitDirty is whether Go stamped the binary as having dirty version
// control changes in the working directory (debug.ReadBuildInfo
// setting "vcs.modified" was true).
GitDirty bool `json:"gitDirty,omitempty"`
// OSVariant is specific variant of the binary, if applicable. For example,
// macsys/appstore/darwin for macOS builds. Nil/empty where not supported
// or on oses without variants.
OSVariant string `json:"osVariant,omitempty"`
// ExtraGitCommit, if non-empty, is the git commit of a "supplemental"
// repository at which Tailscale was built. Its format is the same as
// gitCommit.
//
// ExtraGitCommit is used to track the source revision when the main
// Tailscale repository is integrated into and built from another
// repository (for example, Tailscale's proprietary code, or the
// Android OSS repository). Together, GitCommit and ExtraGitCommit
// exactly describe what repositories and commits were used in a
// build.
ExtraGitCommit string `json:"extraGitCommit,omitempty"`
// DaemonLong is the version number from the tailscaled
// daemon, if requested.
DaemonLong string `json:"daemonLong,omitempty"`
// GitCommitTime is the commit time of the git commit in GitCommit.
GitCommitTime string `json:"gitCommitTime,omitempty"`
// TailscaleGoGitHash is the git commit hash from
// https://github.com/tailscale/go used to build this binary, if built
// with the Tailscale Go toolchain. Otherwise it is empty.
TailscaleGoGitHash string `json:"tailscaleGoGitHash,omitempty"`
// Cap is the current Tailscale capability version. It's a monotonically
// incrementing integer that's incremented whenever a new capability is
// added.
Cap int `json:"cap"`
}
var getMeta lazy.SyncValue[Meta]
// GetMeta returns version metadata about the current build.
func GetMeta() Meta {
return getMeta.Get(func() Meta {
return Meta{
MajorMinorPatch: majorMinorPatch(),
Short: Short(),
Long: Long(),
GitCommitTime: getEmbeddedInfo().commitTime,
GitCommit: gitCommit(),
GitDirty: gitDirty(),
OSVariant: osVariant(),
ExtraGitCommit: extraGitCommitStamp,
IsDev: isDev(),
UnstableBranch: IsUnstableBuild(),
TailscaleGoGitHash: tailscaleToolchainRev(),
Cap: int(tailcfg.CurrentCapabilityVersion),
}
})
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !race
package version
// IsRace reports whether the current binary was built with the Go
// race detector enabled.
func IsRace() bool { return false }
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package version provides the version that the binary was built at.
package version
import (
"fmt"
"runtime/debug"
"strconv"
"strings"
"sync"
tailscaleroot "tailscale.com"
"tailscale.com/types/lazy"
)
// Stamp vars can have their value set at build time by linker flags (see
// build_dist.sh for an example). When set, these stamps serve as additional
// inputs to computing the binary's version as returned by the functions in this
// package.
//
// All stamps are optional.
var (
// longStamp is the full version identifier of the build. If set, it is
// returned verbatim by Long() and other functions that return Long()'s
// output.
longStamp string
// shortStamp is the short version identifier of the build. If set, it
// is returned verbatim by Short() and other functions that return Short()'s
// output.
shortStamp string
// gitCommitStamp is the git commit of the github.com/tailscale/tailscale
// repository at which Tailscale was built. Its format is the one returned
// by `git rev-parse <commit>`. If set, it is used instead of any git commit
// information embedded by the Go tool.
gitCommitStamp string
// gitDirtyStamp is whether the git checkout from which the code was built
// was dirty. Its value is ORed with the dirty bit embedded by the Go tool.
//
// We need this because when we build binaries from another repo that
// imports tailscale.com, the Go tool doesn't stamp any dirtiness info into
// the binary. Instead, we have to inject the dirty bit ourselves here.
gitDirtyStamp bool
// extraGitCommit, is the git commit of a "supplemental" repository at which
// Tailscale was built. Its format is the same as gitCommit.
//
// extraGitCommit is used to track the source revision when the main
// Tailscale repository is integrated into and built from another repository
// (for example, Tailscale's proprietary code, or the Android OSS
// repository). Together, gitCommit and extraGitCommit exactly describe what
// repositories and commits were used in a build.
extraGitCommitStamp string
)
var long lazy.SyncValue[string]
// Long returns a full version number for this build, of one of the forms:
//
// - "x.y.z-commithash-otherhash" for release builds distributed by Tailscale
// - "x.y.z-commithash" for release builds built with build_dist.sh
// - "x.y.z-changecount-commithash-otherhash" for untagged release branch
// builds by Tailscale (these are not distributed).
// - "x.y.z-changecount-commithash" for untagged release branch builds
// built with build_dist.sh
// - "x.y.z-devYYYYMMDD-tcommithash{,-dirty}" for builds of tailscale.com
// made with plain "go build" or "go install", or
// "x.y.z-devYYYYMMDD-gcommithash{,-dirty}" for such builds of a repo
// that imports tailscale.com
// - "x.y.z-ERR-BuildInfo" for builds made by plain "go run"
func Long() string {
return long.Get(func() string {
if longStamp != "" {
return longStamp
}
bi := getEmbeddedInfo()
if !bi.valid {
return strings.TrimSpace(tailscaleroot.VersionDotTxt) + "-ERR-BuildInfo"
}
// The Go tool's embedded VCS info describes the main module's
// repo. By convention (matching mkversion's stamped versions),
// "t" always prefixes a tailscale.com commit and "g" a commit of
// some other repo that imports tailscale.com, such as Tailscale's
// proprietary repo.
hashPrefix := "t"
if bi.mainModule != "tailscale.com" {
hashPrefix = "g"
}
return fmt.Sprintf("%s-dev%s-%s%s%s", strings.TrimSpace(tailscaleroot.VersionDotTxt), bi.commitDate, hashPrefix, bi.commitAbbrev(), dirtyString())
})
}
var short lazy.SyncValue[string]
// Short returns a short version number for this build, of the forms:
//
// - "x.y.z" for builds distributed by Tailscale or built with build_dist.sh
// - "x.y.z-devYYYYMMDD" for builds made with plain "go build" or "go install"
// - "x.y.z-ERR-BuildInfo" for builds made by plain "go run"
func Short() string {
return short.Get(func() string {
if shortStamp != "" {
return shortStamp
}
bi := getEmbeddedInfo()
if !bi.valid {
return strings.TrimSpace(tailscaleroot.VersionDotTxt) + "-ERR-BuildInfo"
}
return strings.TrimSpace(tailscaleroot.VersionDotTxt) + "-dev" + bi.commitDate
})
}
type embeddedInfo struct {
valid bool
mainModule string // module path of the main module, e.g. "tailscale.com"
commit string
commitDate string
commitTime string
dirty bool
}
func (i embeddedInfo) commitAbbrev() string {
if len(i.commit) >= 9 {
return i.commit[:9]
}
return i.commit
}
var getEmbeddedInfo = sync.OnceValue(func() embeddedInfo {
bi, ok := debug.ReadBuildInfo()
if !ok {
return embeddedInfo{}
}
ret := embeddedInfo{valid: true, mainModule: bi.Main.Path}
for _, s := range bi.Settings {
switch s.Key {
case "vcs.revision":
ret.commit = s.Value
case "vcs.time":
ret.commitTime = s.Value
if len(s.Value) >= len("yyyy-mm-dd") {
ret.commitDate = s.Value[:len("yyyy-mm-dd")]
ret.commitDate = strings.ReplaceAll(ret.commitDate, "-", "")
}
case "vcs.modified":
ret.dirty = s.Value == "true"
}
}
if ret.commit == "" || ret.commitDate == "" {
// Build info is present in the binary, but has no useful data. Act as
// if it's missing.
return embeddedInfo{}
}
return ret
})
// tailscaleToolchainRev returns the git hash of the Tailscale Go toolchain
// used to build this binary, if any. It is read separately from getEmbeddedInfo
// because that function discards build info when VCS fields are missing (e.g.
// in test binaries), but the toolchain rev is still present.
var tailscaleToolchainRev = sync.OnceValue(func() string {
bi, ok := debug.ReadBuildInfo()
if !ok {
return ""
}
for _, s := range bi.Settings {
if s.Key == "tailscale.toolchain.rev" {
return s.Value
}
}
return ""
})
func gitCommit() string {
if gitCommitStamp != "" {
return gitCommitStamp
}
return getEmbeddedInfo().commit
}
func gitDirty() bool {
if gitDirtyStamp {
return true
}
return getEmbeddedInfo().dirty
}
func dirtyString() string {
if gitDirty() {
return "-dirty"
}
return ""
}
func majorMinorPatch() string {
ret, _, _ := strings.Cut(Short(), "-")
return ret
}
// IsTailscaleGo reports whether the current binary was built with
// Tailscale's custom Go toolchain.
func IsTailscaleGo() bool { return isTailscaleGo }
func isValidLongWithTwoRepos(v string) bool {
s := strings.Split(v, "-")
if len(s) != 3 {
return false
}
hexChunk := func(s string) bool {
if len(s) < 6 {
return false
}
for i := range len(s) {
b := s[i]
if (b < '0' || b > '9') && (b < 'a' || b > 'f') {
return false
}
}
return true
}
v, t, g := s[0], s[1], s[2]
if !strings.HasPrefix(t, "t") || !strings.HasPrefix(g, "g") ||
!hexChunk(t[1:]) || !hexChunk(g[1:]) {
return false
}
nums := strings.Split(v, ".")
if len(nums) != 3 {
return false
}
for i, n := range nums {
bits := 8
if i == 2 {
bits = 16
}
if _, err := strconv.ParseUint(n, 10, bits); err != nil {
return false
}
}
return true
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_netlog && !ts_omit_logtail
// Package netlog provides a logger that monitors a TUN device and
// periodically records any traffic into a log stream.
package netlog
import (
"cmp"
"context"
"fmt"
"io"
"log"
"net/http"
"net/netip"
"time"
"tailscale.com/health"
"tailscale.com/logpolicy"
"tailscale.com/logtail"
"tailscale.com/net/netmon"
"tailscale.com/net/sockstats"
"tailscale.com/net/tsaddr"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
"tailscale.com/types/ipproto"
"tailscale.com/types/logger"
"tailscale.com/types/logid"
"tailscale.com/types/netlogfunc"
"tailscale.com/types/netlogtype"
"tailscale.com/util/eventbus"
"tailscale.com/util/set"
"tailscale.com/wgengine/router"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
)
// 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 {
SetConnectionCounter(netlogfunc.ConnectionCounter)
}
type noopDevice struct{}
func (noopDevice) SetConnectionCounter(netlogfunc.ConnectionCounter) {}
// NodeSource provides node lookups for the network logger.
// Methods may be called concurrently.
type NodeSource interface {
// SelfNode returns the local node and its owning user profile.
// Both views may be invalid if no self node is known yet.
SelfNode() (node tailcfg.NodeView, user tailcfg.UserProfileView)
// NodeByAddr returns the node assigned the given address along with
// its owning user profile.
// ok is false if no node is known to own addr.
NodeByAddr(addr netip.Addr) (node tailcfg.NodeView, user tailcfg.UserProfileView, ok bool)
}
// Logger logs statistics about every connection.
// At present, it only logs connections within a tailscale network.
// By default, exit node traffic is not logged for privacy reasons
// unless the Tailnet administrator opts-into explicit logging.
// The zero value is ready for use.
type Logger struct {
mu syncs.Mutex // protects all fields below
logf logger.Logf
// shutdownLocked shuts down the logger.
// The mutex must be held when calling.
shutdownLocked func(context.Context) error
record record // the current record of network connection flows
recordLen int // upper bound on JSON length of record
recordsChan chan record // set to nil when shutdown
flushTimer *time.Timer // fires when record should flush to recordsChan
// source provides node lookups. Set by Startup, cleared by shutdownLocked.
source NodeSource
// Information about routes.
// These are read-only once updated by ReconfigRoutes.
routeAddrs set.Set[netip.Addr]
routePrefixes []netip.Prefix
}
// Running reports whether the logger is running.
func (nl *Logger) Running() bool {
nl.mu.Lock()
defer nl.mu.Unlock()
return nl.shutdownLocked != 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 usually a tailscale IP address.
// The source is usually 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 [netlogtype.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 [netlogtype.Message].
//
// The netMon parameter is optional; if non-nil it's used to do faster interface lookups.
//
// source provides on-demand node lookups for connections seen by the logger.
// It must be non-nil.
func (nl *Logger) Startup(logf logger.Logf, source NodeSource, nodeLogID, domainLogID logid.PrivateID, tun, sock Device, netMon *netmon.Monitor, health *health.Tracker, bus *eventbus.Bus, logExitFlowEnabledEnabled bool) error {
nl.mu.Lock()
defer nl.mu.Unlock()
if nl.shutdownLocked != nil {
return fmt.Errorf("network logger already running")
}
if source == nil {
return fmt.Errorf("network logger requires a non-nil NodeSource")
}
nl.source = source
// Startup a log stream to Tailscale's logging service.
if logf == nil {
logf = log.Printf
}
httpc := &http.Client{Transport: logpolicy.NewLogtailTransport(logtail.DefaultHost, netMon, health, logf)}
if testClient != nil {
httpc = testClient
}
logger := logtail.NewLogger(logtail.Config{
Collection: "tailtraffic.log.tailscale.io",
PrivateID: nodeLogID,
CopyPrivateID: domainLogID,
Bus: bus,
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)
logger.SetSockstatsLabel(sockstats.LabelNetlogLogger)
// Register the connection tracker into the TUN device.
tun = cmp.Or[Device](tun, noopDevice{})
tun.SetConnectionCounter(nl.updateVirtConn)
// Register the connection tracker into magicsock.
sock = cmp.Or[Device](sock, noopDevice{})
sock.SetConnectionCounter(nl.updatePhysConn)
// Startup a goroutine to record log messages.
// This is done asynchronously so that the cost of serializing
// the network flow log message never stalls processing of packets.
nl.record = record{}
nl.recordLen = 0
nl.recordsChan = make(chan record, 100)
recorderDone := make(chan struct{})
go func(recordsChan chan record) {
defer close(recorderDone)
for rec := range recordsChan {
msg := rec.toMessage(false, !logExitFlowEnabledEnabled)
if b, err := jsonv2.Marshal(msg, jsontext.AllowInvalidUTF8(true)); err != nil {
if nl.logf != nil {
nl.logf("netlog: json.Marshal error: %v", err)
}
} else {
logger.Logf("%s", b)
}
}
}(nl.recordsChan)
// Register the mechanism for shutting down.
nl.shutdownLocked = func(ctx context.Context) error {
tun.SetConnectionCounter(nil)
sock.SetConnectionCounter(nil)
// Flush and process all pending records.
nl.flushRecordLocked()
close(nl.recordsChan)
nl.recordsChan = nil
<-recorderDone
recorderDone = nil
// Try to upload all pending records.
err := logger.Shutdown(ctx)
// Purge state.
nl.shutdownLocked = nil
nl.source = nil
nl.routeAddrs = nil
nl.routePrefixes = nil
return err
}
return nil
}
var (
tailscaleServiceIPv4 = tsaddr.TailscaleServiceIP()
tailscaleServiceIPv6 = tsaddr.TailscaleServiceIPv6()
)
func (nl *Logger) updateVirtConn(proto ipproto.Proto, src, dst netip.AddrPort, packets, bytes int, recv bool) {
// Network logging is defined as traffic between two Tailscale nodes.
// Traffic with the internal Tailscale service is not with another node
// and should not be logged. It also happens to be a high volume
// amount of discrete traffic flows (e.g., DNS lookups).
switch dst.Addr() {
case tailscaleServiceIPv4, tailscaleServiceIPv6:
return
}
nl.mu.Lock()
defer nl.mu.Unlock()
// Lookup the connection and increment the counts.
nl.initRecordLocked()
conn := netlogtype.Connection{Proto: proto, Src: src, Dst: dst}
cnts, found := nl.record.virtConns[conn]
if !found {
cnts.connType = nl.addNewVirtConnLocked(conn)
}
if recv {
cnts.RxPackets += uint64(packets)
cnts.RxBytes += uint64(bytes)
} else {
cnts.TxPackets += uint64(packets)
cnts.TxBytes += uint64(bytes)
}
nl.record.virtConns[conn] = cnts
}
// addNewVirtConnLocked adds the first insertion of a physical connection.
// The [Logger.mu] must be held.
func (nl *Logger) addNewVirtConnLocked(c netlogtype.Connection) connType {
// Check whether this is the first insertion of the src and dst node.
// If so, compute the additional JSON bytes that would be added
// to the record for the node information.
var srcNodeLen, dstNodeLen int
srcNode, srcSeen := nl.record.seenNodes[c.Src.Addr()]
if !srcSeen {
if node, user, ok := nl.source.NodeByAddr(c.Src.Addr()); ok {
srcNode = nodeUser{node, user}
srcNodeLen = srcNode.jsonLen()
}
}
dstNode, dstSeen := nl.record.seenNodes[c.Dst.Addr()]
if !dstSeen {
if node, user, ok := nl.source.NodeByAddr(c.Dst.Addr()); ok {
dstNode = nodeUser{node, user}
dstNodeLen = dstNode.jsonLen()
}
}
// Check whether the additional [netlogtype.ConnectionCounts]
// and [netlogtype.Node] information would exceed [maxLogSize].
if nl.recordLen+netlogtype.MaxConnectionCountsJSONSize+srcNodeLen+dstNodeLen > maxLogSize {
nl.flushRecordLocked()
nl.initRecordLocked()
}
// Insert newly seen src and/or dst nodes.
if !srcSeen && srcNode.Valid() {
nl.record.seenNodes[c.Src.Addr()] = srcNode
}
if !dstSeen && dstNode.Valid() {
nl.record.seenNodes[c.Dst.Addr()] = dstNode
}
nl.recordLen += netlogtype.MaxConnectionCountsJSONSize + srcNodeLen + dstNodeLen
// Classify the traffic type.
// srcNode == self iff NodeByAddr resolved the source to the same node as
// the current record's self.
srcIsSelfNode := srcNode.Valid() && nl.record.selfNode.Valid() &&
srcNode.ID() == nl.record.selfNode.ID()
switch {
case srcIsSelfNode && dstNode.Valid():
return virtualTraffic
case srcIsSelfNode:
// TODO: Should we swap src for the node serving as the proxy?
// It is relatively useless always using the self IP address.
if nl.withinRoutesLocked(c.Dst.Addr()) {
return subnetTraffic // a client using another subnet router
} else {
return exitTraffic // a client using exit an exit node
}
case dstNode.Valid():
if nl.withinRoutesLocked(c.Src.Addr()) {
return subnetTraffic // serving as a subnet router
} else {
return exitTraffic // serving as an exit node
}
default:
return unknownTraffic
}
}
func (nl *Logger) updatePhysConn(proto ipproto.Proto, src, dst netip.AddrPort, packets, bytes int, recv bool) {
nl.mu.Lock()
defer nl.mu.Unlock()
// Lookup the connection and increment the counts.
nl.initRecordLocked()
conn := netlogtype.Connection{Proto: proto, Src: src, Dst: dst}
cnts, found := nl.record.physConns[conn]
if !found {
nl.addNewPhysConnLocked(conn)
}
if recv {
cnts.RxPackets += uint64(packets)
cnts.RxBytes += uint64(bytes)
} else {
cnts.TxPackets += uint64(packets)
cnts.TxBytes += uint64(bytes)
}
nl.record.physConns[conn] = cnts
}
// addNewPhysConnLocked adds the first insertion of a physical connection.
// The [Logger.mu] must be held.
func (nl *Logger) addNewPhysConnLocked(c netlogtype.Connection) {
// Check whether this is the first insertion of the src node.
var srcNodeLen int
srcNode, srcSeen := nl.record.seenNodes[c.Src.Addr()]
if !srcSeen {
if node, user, ok := nl.source.NodeByAddr(c.Src.Addr()); ok {
srcNode = nodeUser{node, user}
srcNodeLen = srcNode.jsonLen()
}
}
// Check whether the additional [netlogtype.ConnectionCounts]
// and [netlogtype.Node] information would exceed [maxLogSize].
if nl.recordLen+netlogtype.MaxConnectionCountsJSONSize+srcNodeLen > maxLogSize {
nl.flushRecordLocked()
nl.initRecordLocked()
}
// Insert newly seen src and/or dst nodes.
if !srcSeen && srcNode.Valid() {
nl.record.seenNodes[c.Src.Addr()] = srcNode
}
nl.recordLen += netlogtype.MaxConnectionCountsJSONSize + srcNodeLen
}
// initRecordLocked initialize the current record if uninitialized.
// The [Logger.mu] must be held.
func (nl *Logger) initRecordLocked() {
if nl.recordLen != 0 {
return
}
node, user := nl.source.SelfNode()
self := nodeUser{node, user}
nl.record = record{
selfNode: self,
start: time.Now().UTC(),
seenNodes: make(map[netip.Addr]nodeUser),
virtConns: make(map[netlogtype.Connection]countsType),
physConns: make(map[netlogtype.Connection]netlogtype.Counts),
}
nl.recordLen = netlogtype.MinMessageJSONSize + self.jsonLen()
// Start a time to auto-flush the record.
// Avoid tickers since continually waking up a goroutine
// is expensive on battery powered devices.
nl.flushTimer = time.AfterFunc(pollPeriod, func() {
nl.mu.Lock()
defer nl.mu.Unlock()
if !nl.record.start.IsZero() && time.Since(nl.record.start) > pollPeriod/2 {
nl.flushRecordLocked()
}
})
}
// flushRecordLocked flushes the current record if initialized.
// The [Logger.mu] must be held.
func (nl *Logger) flushRecordLocked() {
if nl.recordLen == 0 {
return
}
nl.record.end = time.Now().UTC()
if nl.recordsChan != nil {
select {
case nl.recordsChan <- nl.record:
default:
if nl.logf != nil {
nl.logf("netlog: dropped record due to processing backlog")
}
}
}
if nl.flushTimer != nil {
nl.flushTimer.Stop()
nl.flushTimer = nil
}
nl.record = record{}
nl.recordLen = 0
}
func makeRouteMaps(cfg *router.Config) (addrs set.Set[netip.Addr], prefixes []netip.Prefix) {
addrs = make(set.Set[netip.Addr])
insertPrefixes := func(rs []netip.Prefix) {
for _, p := range rs {
if p.IsSingleIP() {
addrs.Add(p.Addr())
} else {
prefixes = append(prefixes, p)
}
}
}
insertPrefixes(cfg.LocalAddrs)
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) {
addrs, prefixes := makeRouteMaps(cfg) // avoid holding lock while making maps
nl.mu.Lock()
nl.routeAddrs, nl.routePrefixes = addrs, prefixes
nl.mu.Unlock()
}
// withinRoutesLocked reports whether a is within the configured routes,
// which should only contain Tailscale addresses and subnet routes.
// The [Logger.mu] must be held.
func (nl *Logger) withinRoutesLocked(a netip.Addr) bool {
if nl.routeAddrs.Contains(a) {
return true
}
for _, p := range nl.routePrefixes {
if p.Contains(a) && p.Bits() > 0 {
return true
}
}
return false
}
// 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.shutdownLocked == nil {
return nil
}
return nl.shutdownLocked(ctx)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_netlog && !ts_omit_logtail
package netlog
import (
"cmp"
"net/netip"
"slices"
"strings"
"time"
"unicode/utf8"
"tailscale.com/tailcfg"
"tailscale.com/types/bools"
"tailscale.com/types/netlogtype"
"tailscale.com/util/set"
)
// maxLogSize is the maximum number of bytes for a log message.
const maxLogSize = 256 << 10
// record is the in-memory representation of a [netlogtype.Message].
// It uses maps to efficiently look-up addresses and connections.
// In contrast, [netlogtype.Message] is designed to be JSON serializable,
// where complex keys types are not well support in JSON objects.
type record struct {
selfNode nodeUser
start time.Time
end time.Time
seenNodes map[netip.Addr]nodeUser
virtConns map[netlogtype.Connection]countsType
physConns map[netlogtype.Connection]netlogtype.Counts
}
// nodeUser is a node with additional user profile information.
type nodeUser struct {
tailcfg.NodeView
user tailcfg.UserProfileView // UserProfileView for NodeView.User
}
// countsType is a counts with classification information about the connection.
type countsType struct {
netlogtype.Counts
connType connType
}
type connType uint8
const (
unknownTraffic connType = iota
virtualTraffic
subnetTraffic
exitTraffic
)
// toMessage converts a [record] into a [netlogtype.Message].
func (r record) toMessage(excludeNodeInfo, anonymizeExitTraffic bool) netlogtype.Message {
if !r.selfNode.Valid() {
return netlogtype.Message{}
}
m := netlogtype.Message{
NodeID: r.selfNode.StableID(),
Start: r.start.UTC(),
End: r.end.UTC(),
}
// Convert node fields.
if !excludeNodeInfo {
m.SrcNode = r.selfNode.toNode()
seenIDs := set.Of(r.selfNode.ID())
for _, node := range r.seenNodes {
if _, ok := seenIDs[node.ID()]; !ok && node.Valid() {
m.DstNodes = append(m.DstNodes, node.toNode())
seenIDs.Add(node.ID())
}
}
slices.SortFunc(m.DstNodes, func(x, y netlogtype.Node) int {
return cmp.Compare(x.NodeID, y.NodeID)
})
}
// Converter traffic fields.
anonymizedExitTraffic := make(map[netlogtype.Connection]netlogtype.Counts)
for conn, cnts := range r.virtConns {
switch cnts.connType {
case virtualTraffic:
m.VirtualTraffic = append(m.VirtualTraffic, netlogtype.ConnectionCounts{Connection: conn, Counts: cnts.Counts})
case subnetTraffic:
m.SubnetTraffic = append(m.SubnetTraffic, netlogtype.ConnectionCounts{Connection: conn, Counts: cnts.Counts})
default:
if anonymizeExitTraffic {
conn = netlogtype.Connection{ // scrub the IP protocol type
Src: netip.AddrPortFrom(conn.Src.Addr(), 0), // scrub the port number
Dst: netip.AddrPortFrom(conn.Dst.Addr(), 0), // scrub the port number
}
if !r.seenNodes[conn.Src.Addr()].Valid() {
conn.Src = netip.AddrPort{} // not a Tailscale node, so scrub the address
}
if !r.seenNodes[conn.Dst.Addr()].Valid() {
conn.Dst = netip.AddrPort{} // not a Tailscale node, so scrub the address
}
anonymizedExitTraffic[conn] = anonymizedExitTraffic[conn].Add(cnts.Counts)
continue
}
m.ExitTraffic = append(m.ExitTraffic, netlogtype.ConnectionCounts{Connection: conn, Counts: cnts.Counts})
}
}
for conn, cnts := range anonymizedExitTraffic {
m.ExitTraffic = append(m.ExitTraffic, netlogtype.ConnectionCounts{Connection: conn, Counts: cnts})
}
for conn, cnts := range r.physConns {
m.PhysicalTraffic = append(m.PhysicalTraffic, netlogtype.ConnectionCounts{Connection: conn, Counts: cnts})
}
// Sort the connections for deterministic results.
slices.SortFunc(m.VirtualTraffic, compareConnCnts)
slices.SortFunc(m.SubnetTraffic, compareConnCnts)
slices.SortFunc(m.ExitTraffic, compareConnCnts)
slices.SortFunc(m.PhysicalTraffic, compareConnCnts)
return m
}
func compareConnCnts(x, y netlogtype.ConnectionCounts) int {
return cmp.Or(
netip.AddrPort.Compare(x.Src, y.Src),
netip.AddrPort.Compare(x.Dst, y.Dst),
cmp.Compare(x.Proto, y.Proto))
}
// jsonLen computes an upper-bound on the size of the JSON representation.
func (nu nodeUser) jsonLen() (n int) {
if !nu.Valid() {
return len(`{"nodeId":""}`)
}
n += len(`{}`)
n += len(`"nodeId":`) + jsonQuotedLen(string(nu.StableID())) + len(`,`)
if len(nu.Name()) > 0 {
n += len(`"name":`) + jsonQuotedLen(nu.Name()) + len(`,`)
}
if nu.Addresses().Len() > 0 {
n += len(`"addresses":[]`)
for _, addr := range nu.Addresses().All() {
n += bools.IfElse(addr.Addr().Is4(), len(`"255.255.255.255"`), len(`"ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"`)) + len(",")
}
}
if nu.Hostinfo().Valid() && len(nu.Hostinfo().OS()) > 0 {
n += len(`"os":`) + jsonQuotedLen(nu.Hostinfo().OS()) + len(`,`)
}
if nu.Tags().Len() > 0 {
n += len(`"tags":[]`)
for _, tag := range nu.Tags().All() {
n += jsonQuotedLen(tag) + len(",")
}
} else if nu.user.Valid() && nu.user.ID() == nu.User() && len(nu.user.LoginName()) > 0 {
n += len(`"user":`) + jsonQuotedLen(nu.user.LoginName()) + len(",")
}
return n
}
// toNode converts the [nodeUser] into a [netlogtype.Node].
func (nu nodeUser) toNode() netlogtype.Node {
if !nu.Valid() {
return netlogtype.Node{}
}
n := netlogtype.Node{
NodeID: nu.StableID(),
Name: strings.TrimSuffix(nu.Name(), "."),
}
var ipv4, ipv6 netip.Addr
for _, addr := range nu.Addresses().All() {
switch {
case addr.IsSingleIP() && addr.Addr().Is4():
ipv4 = addr.Addr()
case addr.IsSingleIP() && addr.Addr().Is6():
ipv6 = addr.Addr()
}
}
n.Addresses = []netip.Addr{ipv4, ipv6}
n.Addresses = slices.DeleteFunc(n.Addresses, func(a netip.Addr) bool { return !a.IsValid() })
if nu.Hostinfo().Valid() {
n.OS = nu.Hostinfo().OS()
}
if nu.Tags().Len() > 0 {
n.Tags = nu.Tags().AsSlice()
slices.Sort(n.Tags)
n.Tags = slices.Compact(n.Tags)
} else if nu.user.Valid() && nu.user.ID() == nu.User() {
n.User = nu.user.LoginName()
}
return n
}
// jsonQuotedLen computes the length of the JSON serialization of s
// according to [jsontext.AppendQuote].
func jsonQuotedLen(s string) int {
n := len(`"`) + len(s) + len(`"`)
for i, r := range s {
switch {
case r == '\b', r == '\t', r == '\n', r == '\f', r == '\r', r == '"', r == '\\':
n += len(`\X`) - 1
case r < ' ':
n += len(`\uXXXX`) - 1
case r == utf8.RuneError:
if _, m := utf8.DecodeRuneInString(s[i:]); m == 1 { // exactly an invalid byte
n += len("�") - 1
}
}
}
return n
}
// Copyright (c) Tailscale Inc & contributors
// 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)
}
// 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
}
// Copyright (c) Tailscale Inc & contributors
// 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
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package router presents an interface to manipulate the host network
// stack's state.
package router
import (
"errors"
"fmt"
"net/netip"
"reflect"
"runtime"
"slices"
"github.com/tailscale/wireguard-go/tun"
"tailscale.com/feature"
"tailscale.com/feature/buildfeatures"
"tailscale.com/health"
"tailscale.com/net/netmon"
"tailscale.com/types/logger"
"tailscale.com/types/preftype"
"tailscale.com/util/eventbus"
)
// 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
// Close closes the router.
Close() error
}
// NewOpts are the options passed to the NewUserspaceRouter hook.
type NewOpts struct {
Logf logger.Logf // required
Tun tun.Device // required
NetMon *netmon.Monitor // optional
Health *health.Tracker // required (but TODO: support optional later)
Bus *eventbus.Bus // required
}
// PortUpdate is an eventbus value, reporting the port and address family
// magicsock is currently listening on, so it can be threaded through firewalls
// and such.
type PortUpdate struct {
UDPPort uint16
EndpointNetwork string // either "udp4" or "udp6".
}
// HookNewUserspaceRouter is the registration point for router implementations
// to register a constructor for userspace routers. It's meant for implementations
// in wgengine/router/osrouter.
//
// If no implementation is registered, [New] will return an error.
var HookNewUserspaceRouter feature.Hook[func(NewOpts) (Router, 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, bus *eventbus.Bus,
) (Router, error) {
logf = logger.WithPrefix(logf, "router: ")
if f, ok := HookNewUserspaceRouter.GetOk(); ok {
return f(NewOpts{
Logf: logf,
Tun: tundev,
NetMon: netMon,
Health: health,
Bus: bus,
})
}
if !buildfeatures.HasOSRouter {
return nil, errors.New("router: tailscaled was built without OSRouter support")
}
return nil, fmt.Errorf("unsupported OS %q", runtime.GOOS)
}
// HookCleanUp is the optional registration point for router implementations
// to register a cleanup function for [CleanUp] to use. It's meant for
// implementations in wgengine/router/osrouter.
var HookCleanUp feature.Hook[func(_ logger.Logf, _ *netmon.Monitor, ifName string)]
// 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) {
if f, ok := HookCleanUp.GetOk(); ok {
f(logf, netMon, 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
// SNATSubnetRoutes enables SNAT for traffic to local subnets.
// Implemented on Linux (iptables/nftables) and FreeBSD (PF).
SNATSubnetRoutes bool
// Linux-only things below, ignored on other platforms.
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", or "" to auto-detect)
RemoveCGNATDropRule bool // whether to remove the firewall rule to drop non-Tailscale inbound traffic from CGNAT IPs
}
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)
}
func (c *Config) Clone() *Config {
if c == nil {
return nil
}
c2 := *c
c2.LocalAddrs = slices.Clone(c.LocalAddrs)
c2.Routes = slices.Clone(c.Routes)
c2.LocalRoutes = slices.Clone(c.LocalRoutes)
c2.SubnetRoutes = slices.Clone(c.SubnetRoutes)
return &c2
}
// Copyright (c) Tailscale Inc & contributors
// 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) Close() error {
r.logf("[v1] warning: fakeRouter.Close: not implemented.")
return nil
}