// 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 controlbase implements the base transport of the Tailscale
// 2021 control protocol.
//
// The base transport implements Noise IK, instantiated with
// Curve25519, ChaCha20Poly1305 and BLAKE2s.
package controlbase
import (
"crypto/cipher"
"encoding/binary"
"fmt"
"net"
"sync"
"time"
"golang.org/x/crypto/blake2s"
chp "golang.org/x/crypto/chacha20poly1305"
"tailscale.com/syncs"
"tailscale.com/types/key"
)
const (
// maxMessageSize is the maximum size of a protocol frame on the
// wire, including header and payload.
maxMessageSize = 4096
// maxCiphertextSize is the maximum amount of ciphertext bytes
// that one protocol frame can carry, after framing.
maxCiphertextSize = maxMessageSize - 3
// maxPlaintextSize is the maximum amount of plaintext bytes that
// one protocol frame can carry, after encryption and framing.
maxPlaintextSize = maxCiphertextSize - chp.Overhead
)
// A Conn is a secured Noise connection. It implements the net.Conn
// interface, with the unusual trait that any write error (including a
// SetWriteDeadline induced i/o timeout) causes all future writes to
// fail.
type Conn struct {
conn net.Conn
version uint16
peer key.MachinePublic
handshakeHash [blake2s.Size]byte
rx rxState
tx txState
}
// rxState is all the Conn state that Read uses.
type rxState struct {
syncs.Mutex
cipher cipher.AEAD
nonce nonce
buf *maxMsgBuffer // or nil when reads exhausted
n int // number of valid bytes in buf
next int // offset of next undecrypted packet
plaintext []byte // slice into buf of decrypted bytes
hdrBuf [headerLen]byte // small buffer used when buf is nil
}
// txState is all the Conn state that Write uses.
type txState struct {
sync.Mutex
cipher cipher.AEAD
nonce nonce
err error // records the first partial write error for all future calls
}
// ProtocolVersion returns the protocol version that was used to
// establish this Conn.
func (c *Conn) ProtocolVersion() int {
return int(c.version)
}
// HandshakeHash returns the Noise handshake hash for the connection,
// which can be used to bind other messages to this connection
// (i.e. to ensure that the message wasn't replayed from a different
// connection).
func (c *Conn) HandshakeHash() [blake2s.Size]byte {
return c.handshakeHash
}
// Peer returns the peer's long-term public key.
func (c *Conn) Peer() key.MachinePublic {
return c.peer
}
// readNLocked reads into c.rx.buf until buf contains at least total
// bytes. Returns a slice of the total bytes in rxBuf, or an
// error if fewer than total bytes are available.
//
// It may be called with a nil c.rx.buf only if total == headerLen.
//
// On success, c.rx.buf will be non-nil.
func (c *Conn) readNLocked(total int) ([]byte, error) {
if total > maxMessageSize {
return nil, errReadTooBig{total}
}
for {
if total <= c.rx.n {
return c.rx.buf[:total], nil
}
var n int
var err error
if c.rx.buf == nil {
if c.rx.n != 0 || total != headerLen {
panic("unexpected")
}
// Optimization to reduce memory usage.
// Most connections are blocked forever waiting for
// a read, so we don't want c.rx.buf to be allocated until
// we know there's data to read. Instead, when we're
// waiting for data to arrive here, read into the
// 3 byte hdrBuf:
n, err = c.conn.Read(c.rx.hdrBuf[:])
if n > 0 {
c.rx.buf = getMaxMsgBuffer()
copy(c.rx.buf[:], c.rx.hdrBuf[:n])
}
} else {
n, err = c.conn.Read(c.rx.buf[c.rx.n:])
}
c.rx.n += n
if err != nil {
return nil, err
}
}
}
// decryptLocked decrypts msg (which is header+ciphertext) in-place
// and sets c.rx.plaintext to the decrypted bytes.
func (c *Conn) decryptLocked(msg []byte) (err error) {
if msgType := msg[0]; msgType != msgTypeRecord {
return fmt.Errorf("received message with unexpected type %d, want %d", msgType, msgTypeRecord)
}
// We don't check the length field here, because the caller
// already did in order to figure out how big the msg slice should
// be.
ciphertext := msg[headerLen:]
if !c.rx.nonce.Valid() {
return errCipherExhausted{}
}
c.rx.plaintext, err = c.rx.cipher.Open(ciphertext[:0], c.rx.nonce[:], ciphertext, nil)
c.rx.nonce.Increment()
if err != nil {
// Once a decryption has failed, our Conn is no longer
// synchronized with our peer. Nuke the cipher state to be
// safe, so that no further decryptions are attempted. Future
// read attempts will return net.ErrClosed.
c.rx.cipher = nil
}
return err
}
// encryptLocked encrypts plaintext into buf (including the
// packet header) and returns a slice of the ciphertext, or an error
// if the cipher is exhausted (i.e. can no longer be used safely).
func (c *Conn) encryptLocked(plaintext []byte, buf *maxMsgBuffer) ([]byte, error) {
if !c.tx.nonce.Valid() {
// Received 2^64-1 messages on this cipher state. Connection
// is no longer usable.
return nil, errCipherExhausted{}
}
buf[0] = msgTypeRecord
binary.BigEndian.PutUint16(buf[1:headerLen], uint16(len(plaintext)+chp.Overhead))
ret := c.tx.cipher.Seal(buf[:headerLen], c.tx.nonce[:], plaintext, nil)
c.tx.nonce.Increment()
return ret, nil
}
// wholeMessageLocked returns a slice of one whole Noise transport
// message from c.rx.buf, if one whole message is available, and
// advances the read state to the next Noise message in the
// buffer. Returns nil without advancing read state if there isn't one
// whole message in c.rx.buf.
func (c *Conn) wholeMessageLocked() []byte {
available := c.rx.n - c.rx.next
if available < headerLen {
return nil
}
bs := c.rx.buf[c.rx.next:c.rx.n]
totalSize := headerLen + int(binary.BigEndian.Uint16(bs[1:3]))
if len(bs) < totalSize {
return nil
}
c.rx.next += totalSize
return bs[:totalSize]
}
// decryptOneLocked decrypts one Noise transport message, reading from
// c.conn as needed, and sets c.rx.plaintext to point to the decrypted
// bytes. c.rx.plaintext is only valid if err == nil.
func (c *Conn) decryptOneLocked() error {
c.rx.plaintext = nil
// Fast path: do we have one whole ciphertext frame buffered
// already?
if bs := c.wholeMessageLocked(); bs != nil {
return c.decryptLocked(bs)
}
if c.rx.next != 0 {
// To simplify the read logic, move the remainder of the
// buffered bytes back to the head of the buffer, so we can
// grow it without worrying about wraparound.
c.rx.n = copy(c.rx.buf[:], c.rx.buf[c.rx.next:c.rx.n])
c.rx.next = 0
}
// Return our buffer to the pool if it's empty, lest we be
// blocked in a long Read call, reading the 3 byte header. We
// don't to keep that buffer unnecessarily alive.
if c.rx.n == 0 && c.rx.next == 0 && c.rx.buf != nil {
bufPool.Put(c.rx.buf)
c.rx.buf = nil
}
bs, err := c.readNLocked(headerLen)
if err != nil {
return err
}
// The rest of the header (besides the length field) gets verified
// in decryptLocked, not here.
messageLen := headerLen + int(binary.BigEndian.Uint16(bs[1:3]))
bs, err = c.readNLocked(messageLen)
if err != nil {
return err
}
c.rx.next = len(bs)
return c.decryptLocked(bs)
}
// Read implements io.Reader.
func (c *Conn) Read(bs []byte) (int, error) {
c.rx.Lock()
defer c.rx.Unlock()
if c.rx.cipher == nil {
return 0, net.ErrClosed
}
// If no plaintext is buffered, decrypt incoming frames until we
// have some plaintext. Zero-byte Noise frames are allowed in this
// protocol, which is why we have to loop here rather than decrypt
// a single additional frame.
for len(c.rx.plaintext) == 0 {
if err := c.decryptOneLocked(); err != nil {
return 0, err
}
}
n := copy(bs, c.rx.plaintext)
c.rx.plaintext = c.rx.plaintext[n:]
// Lose slice's underlying array pointer to unneeded memory so
// GC can collect more.
if len(c.rx.plaintext) == 0 {
c.rx.plaintext = nil
}
return n, nil
}
// Write implements io.Writer.
func (c *Conn) Write(bs []byte) (n int, err error) {
c.tx.Lock()
defer c.tx.Unlock()
if c.tx.err != nil {
return 0, c.tx.err
}
defer func() {
if err != nil {
// All write errors are fatal for this conn, so clear the
// cipher state whenever an error happens.
c.tx.cipher = nil
}
if c.tx.err == nil {
// Only set c.tx.err if not nil so that we can return one
// error on the first failure, and a different one for
// subsequent calls. See the error handling around Write
// below for why.
c.tx.err = err
}
}()
if c.tx.cipher == nil {
return 0, net.ErrClosed
}
buf := getMaxMsgBuffer()
defer bufPool.Put(buf)
var sent int
for len(bs) > 0 {
toSend := bs
if len(toSend) > maxPlaintextSize {
toSend = bs[:maxPlaintextSize]
}
bs = bs[len(toSend):]
ciphertext, err := c.encryptLocked(toSend, buf)
if err != nil {
return sent, err
}
if _, err := c.conn.Write(ciphertext); err != nil {
// Return the raw error on the Write that actually
// failed. For future writes, return that error wrapped in
// a desync error.
c.tx.err = errPartialWrite{err}
return sent, err
}
sent += len(toSend)
}
return sent, nil
}
// Close implements io.Closer.
func (c *Conn) Close() error {
closeErr := c.conn.Close() // unblocks any waiting reads or writes
// Remove references to live cipher state. Strictly speaking this
// is unnecessary, but we want to try and hand the active cipher
// state to the garbage collector promptly, to preserve perfect
// forward secrecy as much as we can.
c.rx.Lock()
c.rx.cipher = nil
c.rx.Unlock()
c.tx.Lock()
c.tx.cipher = nil
c.tx.Unlock()
return closeErr
}
func (c *Conn) LocalAddr() net.Addr { return c.conn.LocalAddr() }
func (c *Conn) RemoteAddr() net.Addr { return c.conn.RemoteAddr() }
func (c *Conn) SetDeadline(t time.Time) error { return c.conn.SetDeadline(t) }
func (c *Conn) SetReadDeadline(t time.Time) error { return c.conn.SetReadDeadline(t) }
func (c *Conn) SetWriteDeadline(t time.Time) error { return c.conn.SetWriteDeadline(t) }
// errCipherExhausted is the error returned when we run out of nonces
// on a cipher.
type errCipherExhausted struct{}
func (errCipherExhausted) Error() string {
return "cipher exhausted, no more nonces available for current key"
}
func (errCipherExhausted) Timeout() bool { return false }
func (errCipherExhausted) Temporary() bool { return false }
// errPartialWrite is the error returned when the cipher state has
// become unusable due to a past partial write.
type errPartialWrite struct {
err error
}
func (e errPartialWrite) Error() string {
return fmt.Sprintf("cipher state desynchronized due to partial write (%v)", e.err)
}
func (e errPartialWrite) Unwrap() error { return e.err }
func (e errPartialWrite) Temporary() bool { return false }
func (e errPartialWrite) Timeout() bool { return false }
// errReadTooBig is the error returned when the peer sent an
// unacceptably large Noise frame.
type errReadTooBig struct {
requested int
}
func (e errReadTooBig) Error() string {
return fmt.Sprintf("requested read of %d bytes exceeds max allowed Noise frame size", e.requested)
}
func (e errReadTooBig) Temporary() bool {
// permanent error because this error only occurs when our peer
// sends us a frame so large we're unwilling to ever decode it.
return false
}
func (e errReadTooBig) Timeout() bool { return false }
type nonce [chp.NonceSize]byte
func (n *nonce) Valid() bool {
return binary.BigEndian.Uint32(n[:4]) == 0 && binary.BigEndian.Uint64(n[4:]) != invalidNonce
}
func (n *nonce) Increment() {
if !n.Valid() {
panic("increment of invalid nonce")
}
binary.BigEndian.PutUint64(n[4:], 1+binary.BigEndian.Uint64(n[4:]))
}
type maxMsgBuffer [maxMessageSize]byte
// bufPool holds the temporary buffers for Conn.Read & Write.
var bufPool = &sync.Pool{
New: func() any {
return new(maxMsgBuffer)
},
}
func getMaxMsgBuffer() *maxMsgBuffer {
return bufPool.Get().(*maxMsgBuffer)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package controlbase
import (
"context"
"crypto/cipher"
"encoding/binary"
"errors"
"fmt"
"hash"
"io"
"net"
"strconv"
"time"
"go4.org/mem"
"golang.org/x/crypto/blake2s"
chp "golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/hkdf"
"tailscale.com/types/key"
)
const (
// protocolName is the name of the specific instantiation of Noise
// that the control protocol uses. This string's value is fixed by
// the Noise spec, and shouldn't be changed unless we're updating
// the control protocol to use a different Noise instance.
protocolName = "Noise_IK_25519_ChaChaPoly_BLAKE2s"
// protocolVersion is the version of the control protocol that
// Client will use when initiating a handshake.
//protocolVersion uint16 = 1
// protocolVersionPrefix is the name portion of the protocol
// name+version string that gets mixed into the handshake as a
// prologue.
//
// This mixing verifies that both clients agree that they're
// executing the control protocol at a specific version that
// matches the advertised version in the cleartext packet header.
protocolVersionPrefix = "Tailscale Control Protocol v"
invalidNonce = ^uint64(0)
)
func protocolVersionPrologue(version uint16) []byte {
ret := make([]byte, 0, len(protocolVersionPrefix)+5) // 5 bytes is enough to encode all possible version numbers.
ret = append(ret, protocolVersionPrefix...)
return strconv.AppendUint(ret, uint64(version), 10)
}
// HandshakeContinuation upgrades a net.Conn to a Conn. The net.Conn
// is assumed to have already sent the client>server handshake
// initiation message.
type HandshakeContinuation func(context.Context, net.Conn) (*Conn, error)
// ClientDeferred initiates a control client handshake, returning the
// initial message to send to the server and a continuation to
// finalize the handshake.
//
// ClientDeferred is split in this way for RTT reduction: we run this
// protocol after negotiating a protocol switch from HTTP/HTTPS. If we
// completely serialized the negotiation followed by the handshake,
// we'd pay an extra RTT to transmit the handshake initiation after
// protocol switching. By splitting the handshake into an initial
// message and a continuation, we can embed the handshake initiation
// into the HTTP protocol switching request and avoid a bit of delay.
func ClientDeferred(machineKey key.MachinePrivate, controlKey key.MachinePublic, protocolVersion uint16) (initialHandshake []byte, continueHandshake HandshakeContinuation, err error) {
var s symmetricState
s.Initialize()
// prologue
s.MixHash(protocolVersionPrologue(protocolVersion))
// <- s
// ...
s.MixHash(controlKey.UntypedBytes())
// -> e, es, s, ss
init := mkInitiationMessage(protocolVersion)
machineEphemeral := key.NewMachine()
machineEphemeralPub := machineEphemeral.Public()
copy(init.EphemeralPub(), machineEphemeralPub.UntypedBytes())
s.MixHash(machineEphemeralPub.UntypedBytes())
cipher, err := s.MixDH(machineEphemeral, controlKey)
if err != nil {
return nil, nil, fmt.Errorf("computing es: %w", err)
}
machineKeyPub := machineKey.Public()
s.EncryptAndHash(cipher, init.MachinePub(), machineKeyPub.UntypedBytes())
cipher, err = s.MixDH(machineKey, controlKey)
if err != nil {
return nil, nil, fmt.Errorf("computing ss: %w", err)
}
s.EncryptAndHash(cipher, init.Tag(), nil) // empty message payload
cont := func(ctx context.Context, conn net.Conn) (*Conn, error) {
return continueClientHandshake(ctx, conn, &s, machineKey, machineEphemeral, controlKey, protocolVersion)
}
return init[:], cont, nil
}
// Client wraps ClientDeferred and immediately invokes the returned
// continuation with conn.
//
// This is a helper for when you don't need the fancy
// continuation-style handshake, and just want to synchronously
// upgrade a net.Conn to a secure transport.
func Client(ctx context.Context, conn net.Conn, machineKey key.MachinePrivate, controlKey key.MachinePublic, protocolVersion uint16) (*Conn, error) {
init, cont, err := ClientDeferred(machineKey, controlKey, protocolVersion)
if err != nil {
return nil, err
}
if _, err := conn.Write(init); err != nil {
return nil, err
}
return cont(ctx, conn)
}
func continueClientHandshake(ctx context.Context, conn net.Conn, s *symmetricState, machineKey, machineEphemeral key.MachinePrivate, controlKey key.MachinePublic, protocolVersion uint16) (*Conn, error) {
// No matter what, this function can only run once per s. Ensure
// attempted reuse causes a panic.
defer func() {
s.finished = true
}()
if deadline, ok := ctx.Deadline(); ok {
if err := conn.SetDeadline(deadline); err != nil {
return nil, fmt.Errorf("setting conn deadline: %w", err)
}
defer func() {
conn.SetDeadline(time.Time{})
}()
}
// Read in the payload and look for errors/protocol violations from the server.
var resp responseMessage
if _, err := io.ReadFull(conn, resp.Header()); err != nil {
return nil, fmt.Errorf("reading response header: %w", err)
}
if resp.Type() != msgTypeResponse {
if resp.Type() != msgTypeError {
return nil, fmt.Errorf("unexpected response message type %d", resp.Type())
}
msg := make([]byte, resp.Length())
if _, err := io.ReadFull(conn, msg); err != nil {
return nil, err
}
return nil, fmt.Errorf("server error: %q", msg)
}
if resp.Length() != len(resp.Payload()) {
return nil, fmt.Errorf("wrong length %d received for handshake response", resp.Length())
}
if _, err := io.ReadFull(conn, resp.Payload()); err != nil {
return nil, err
}
// <- e, ee, se
controlEphemeralPub := key.MachinePublicFromRaw32(mem.B(resp.EphemeralPub()))
s.MixHash(controlEphemeralPub.UntypedBytes())
if _, err := s.MixDH(machineEphemeral, controlEphemeralPub); err != nil {
return nil, fmt.Errorf("computing ee: %w", err)
}
cipher, err := s.MixDH(machineKey, controlEphemeralPub)
if err != nil {
return nil, fmt.Errorf("computing se: %w", err)
}
if err := s.DecryptAndHash(cipher, nil, resp.Tag()); err != nil {
return nil, fmt.Errorf("decrypting payload: %w", err)
}
c1, c2, err := s.Split()
if err != nil {
return nil, fmt.Errorf("finalizing handshake: %w", err)
}
c := &Conn{
conn: conn,
version: protocolVersion,
peer: controlKey,
handshakeHash: s.h,
tx: txState{
cipher: c1,
},
rx: rxState{
cipher: c2,
},
}
return c, nil
}
// Server initiates a control server handshake, returning the resulting
// control connection.
//
// optionalInit can be the client's initial handshake message as
// returned by ClientDeferred, or nil in which case the initial
// message is read from conn.
//
// The context deadline, if any, covers the entire handshaking
// process.
func Server(ctx context.Context, conn net.Conn, controlKey key.MachinePrivate, optionalInit []byte) (*Conn, error) {
if deadline, ok := ctx.Deadline(); ok {
if err := conn.SetDeadline(deadline); err != nil {
return nil, fmt.Errorf("setting conn deadline: %w", err)
}
defer func() {
conn.SetDeadline(time.Time{})
}()
}
// Deliberately does not support formatting, so that we don't echo
// attacker-controlled input back to them.
sendErr := func(msg string) error {
if len(msg) >= 1<<16 {
msg = msg[:1<<16]
}
var hdr [headerLen]byte
hdr[0] = msgTypeError
binary.BigEndian.PutUint16(hdr[1:3], uint16(len(msg)))
if _, err := conn.Write(hdr[:]); err != nil {
return fmt.Errorf("sending %q error to client: %w", msg, err)
}
if _, err := io.WriteString(conn, msg); err != nil {
return fmt.Errorf("sending %q error to client: %w", msg, err)
}
return fmt.Errorf("refused client handshake: %q", msg)
}
var s symmetricState
s.Initialize()
var init initiationMessage
if optionalInit != nil {
if len(optionalInit) != len(init) {
return nil, sendErr("wrong handshake initiation size")
}
copy(init[:], optionalInit)
} else if _, err := io.ReadFull(conn, init.Header()); err != nil {
return nil, err
}
// Just a rename to make it more obvious what the value is. In the
// current implementation we don't need to block any protocol
// versions at this layer, it's safe to let the handshake proceed
// and then let the caller make decisions based on the agreed-upon
// protocol version.
clientVersion := init.Version()
if init.Type() != msgTypeInitiation {
return nil, sendErr("unexpected handshake message type")
}
if init.Length() != len(init.Payload()) {
return nil, sendErr("wrong handshake initiation length")
}
// if optionalInit was provided, we have the payload already.
if optionalInit == nil {
if _, err := io.ReadFull(conn, init.Payload()); err != nil {
return nil, err
}
}
// prologue. Can only do this once we at least think the client is
// handshaking using a supported version.
s.MixHash(protocolVersionPrologue(clientVersion))
// <- s
// ...
controlKeyPub := controlKey.Public()
s.MixHash(controlKeyPub.UntypedBytes())
// -> e, es, s, ss
machineEphemeralPub := key.MachinePublicFromRaw32(mem.B(init.EphemeralPub()))
s.MixHash(machineEphemeralPub.UntypedBytes())
cipher, err := s.MixDH(controlKey, machineEphemeralPub)
if err != nil {
return nil, fmt.Errorf("computing es: %w", err)
}
var machineKeyBytes [32]byte
if err := s.DecryptAndHash(cipher, machineKeyBytes[:], init.MachinePub()); err != nil {
return nil, fmt.Errorf("decrypting machine key: %w", err)
}
machineKey := key.MachinePublicFromRaw32(mem.B(machineKeyBytes[:]))
cipher, err = s.MixDH(controlKey, machineKey)
if err != nil {
return nil, fmt.Errorf("computing ss: %w", err)
}
if err := s.DecryptAndHash(cipher, nil, init.Tag()); err != nil {
return nil, fmt.Errorf("decrypting initiation tag: %w", err)
}
// <- e, ee, se
resp := mkResponseMessage()
controlEphemeral := key.NewMachine()
controlEphemeralPub := controlEphemeral.Public()
copy(resp.EphemeralPub(), controlEphemeralPub.UntypedBytes())
s.MixHash(controlEphemeralPub.UntypedBytes())
if _, err := s.MixDH(controlEphemeral, machineEphemeralPub); err != nil {
return nil, fmt.Errorf("computing ee: %w", err)
}
cipher, err = s.MixDH(controlEphemeral, machineKey)
if err != nil {
return nil, fmt.Errorf("computing se: %w", err)
}
s.EncryptAndHash(cipher, resp.Tag(), nil) // empty message payload
c1, c2, err := s.Split()
if err != nil {
return nil, fmt.Errorf("finalizing handshake: %w", err)
}
if _, err := conn.Write(resp[:]); err != nil {
return nil, err
}
c := &Conn{
conn: conn,
version: clientVersion,
peer: machineKey,
handshakeHash: s.h,
tx: txState{
cipher: c2,
},
rx: rxState{
cipher: c1,
},
}
return c, nil
}
// symmetricState contains the state of an in-flight handshake.
type symmetricState struct {
finished bool
h [blake2s.Size]byte // hash of currently-processed handshake state
ck [blake2s.Size]byte // chaining key used to construct session keys at the end of the handshake
}
func (s *symmetricState) checkFinished() {
if s.finished {
panic("attempted to use symmetricState after Split was called")
}
}
// Initialize sets s to the initial handshake state, prior to
// processing any handshake messages.
func (s *symmetricState) Initialize() {
s.checkFinished()
s.h = blake2s.Sum256([]byte(protocolName))
s.ck = s.h
}
// MixHash updates s.h to be BLAKE2s(s.h || data), where || is
// concatenation.
func (s *symmetricState) MixHash(data []byte) {
s.checkFinished()
h := newBLAKE2s()
h.Write(s.h[:])
h.Write(data)
h.Sum(s.h[:0])
}
// MixDH updates s.ck with the result of X25519(priv, pub) and returns
// a singleUseCHP that can be used to encrypt or decrypt handshake
// data.
//
// MixDH corresponds to MixKey(X25519(...))) in the spec. Implementing
// it as a single function allows for strongly-typed arguments that
// reduce the risk of error in the caller (e.g. invoking X25519 with
// two private keys, or two public keys), and thus producing the wrong
// calculation.
func (s *symmetricState) MixDH(priv key.MachinePrivate, pub key.MachinePublic) (*singleUseCHP, error) {
s.checkFinished()
keyData, err := curve25519.X25519(priv.UntypedBytes(), pub.UntypedBytes())
if err != nil {
return nil, fmt.Errorf("computing X25519: %w", err)
}
r := hkdf.New(newBLAKE2s, keyData, s.ck[:], nil)
if _, err := io.ReadFull(r, s.ck[:]); err != nil {
return nil, fmt.Errorf("extracting ck: %w", err)
}
var k [chp.KeySize]byte
if _, err := io.ReadFull(r, k[:]); err != nil {
return nil, fmt.Errorf("extracting k: %w", err)
}
return newSingleUseCHP(k), nil
}
// EncryptAndHash encrypts plaintext into ciphertext (which must be
// the correct size to hold the encrypted plaintext) using cipher,
// mixes the ciphertext into s.h, and returns the ciphertext.
func (s *symmetricState) EncryptAndHash(cipher *singleUseCHP, ciphertext, plaintext []byte) {
s.checkFinished()
if len(ciphertext) != len(plaintext)+chp.Overhead {
panic("ciphertext is wrong size for given plaintext")
}
ret := cipher.Seal(ciphertext[:0], plaintext, s.h[:])
s.MixHash(ret)
}
// DecryptAndHash decrypts the given ciphertext into plaintext (which
// must be the correct size to hold the decrypted ciphertext) using
// cipher. If decryption is successful, it mixes the ciphertext into
// s.h.
func (s *symmetricState) DecryptAndHash(cipher *singleUseCHP, plaintext, ciphertext []byte) error {
s.checkFinished()
if len(ciphertext) != len(plaintext)+chp.Overhead {
return errors.New("plaintext is wrong size for given ciphertext")
}
if _, err := cipher.Open(plaintext[:0], ciphertext, s.h[:]); err != nil {
return err
}
s.MixHash(ciphertext)
return nil
}
// Split returns two ChaCha20Poly1305 ciphers with keys derived from
// the current handshake state. Methods on s cannot be used again
// after calling Split.
func (s *symmetricState) Split() (c1, c2 cipher.AEAD, err error) {
s.finished = true
var k1, k2 [chp.KeySize]byte
r := hkdf.New(newBLAKE2s, nil, s.ck[:], nil)
if _, err := io.ReadFull(r, k1[:]); err != nil {
return nil, nil, fmt.Errorf("extracting k1: %w", err)
}
if _, err := io.ReadFull(r, k2[:]); err != nil {
return nil, nil, fmt.Errorf("extracting k2: %w", err)
}
c1, err = chp.New(k1[:])
if err != nil {
return nil, nil, fmt.Errorf("constructing AEAD c1: %w", err)
}
c2, err = chp.New(k2[:])
if err != nil {
return nil, nil, fmt.Errorf("constructing AEAD c2: %w", err)
}
return c1, c2, nil
}
// newBLAKE2s returns a hash.Hash implementing BLAKE2s, or panics on
// error.
func newBLAKE2s() hash.Hash {
h, err := blake2s.New256(nil)
if err != nil {
// Should never happen, errors only happen when using BLAKE2s
// in MAC mode with a key.
panic(err)
}
return h
}
// newCHP returns a cipher.AEAD implementing ChaCha20Poly1305, or
// panics on error.
func newCHP(key [chp.KeySize]byte) cipher.AEAD {
aead, err := chp.New(key[:])
if err != nil {
// Can only happen if we passed a key of the wrong length. The
// function signature prevents that.
panic(err)
}
return aead
}
// singleUseCHP is an instance of ChaCha20Poly1305 that can be used
// only once, either for encrypting or decrypting, but not both. The
// chosen operation is always executed with an all-zeros
// nonce. Subsequent calls to either Seal or Open panic.
type singleUseCHP struct {
c cipher.AEAD
}
func newSingleUseCHP(key [chp.KeySize]byte) *singleUseCHP {
return &singleUseCHP{newCHP(key)}
}
func (c *singleUseCHP) Seal(dst, plaintext, additionalData []byte) []byte {
if c.c == nil {
panic("Attempted reuse of singleUseAEAD")
}
cipher := c.c
c.c = nil
var nonce [chp.NonceSize]byte
return cipher.Seal(dst, nonce[:], plaintext, additionalData)
}
func (c *singleUseCHP) Open(dst, ciphertext, additionalData []byte) ([]byte, error) {
if c.c == nil {
panic("Attempted reuse of singleUseAEAD")
}
cipher := c.c
c.c = nil
var nonce [chp.NonceSize]byte
return cipher.Open(dst, nonce[:], ciphertext, additionalData)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package controlbase
import "encoding/binary"
const (
// msgTypeInitiation frames carry a Noise IK handshake initiation message.
msgTypeInitiation = 1
// msgTypeResponse frames carry a Noise IK handshake response message.
msgTypeResponse = 2
// msgTypeError frames carry an unauthenticated human-readable
// error message.
//
// Errors reported in this message type must be treated as public
// hints only. They are not encrypted or authenticated, and so can
// be seen and tampered with on the wire.
msgTypeError = 3
// msgTypeRecord frames carry session data bytes.
msgTypeRecord = 4
// headerLen is the size of the header on all messages except msgTypeInitiation.
headerLen = 3
// initiationHeaderLen is the size of the header on all msgTypeInitiation messages.
initiationHeaderLen = 5
)
// initiationMessage is the protocol message sent from a client
// machine to a control server.
//
// 2b: protocol version
// 1b: message type (0x01)
// 2b: payload length (96)
// 5b: header (see headerLen for fields)
// 32b: client ephemeral public key (cleartext)
// 48b: client machine public key (encrypted)
// 16b: message tag (authenticates the whole message)
type initiationMessage [101]byte
func mkInitiationMessage(protocolVersion uint16) initiationMessage {
var ret initiationMessage
binary.BigEndian.PutUint16(ret[:2], protocolVersion)
ret[2] = msgTypeInitiation
binary.BigEndian.PutUint16(ret[3:5], uint16(len(ret.Payload())))
return ret
}
func (m *initiationMessage) Header() []byte { return m[:initiationHeaderLen] }
func (m *initiationMessage) Payload() []byte { return m[initiationHeaderLen:] }
func (m *initiationMessage) Version() uint16 { return binary.BigEndian.Uint16(m[:2]) }
func (m *initiationMessage) Type() byte { return m[2] }
func (m *initiationMessage) Length() int { return int(binary.BigEndian.Uint16(m[3:5])) }
func (m *initiationMessage) EphemeralPub() []byte {
return m[initiationHeaderLen : initiationHeaderLen+32]
}
func (m *initiationMessage) MachinePub() []byte {
return m[initiationHeaderLen+32 : initiationHeaderLen+32+48]
}
func (m *initiationMessage) Tag() []byte { return m[initiationHeaderLen+32+48:] }
// responseMessage is the protocol message sent from a control server
// to a client machine.
//
// 1b: message type (0x02)
// 2b: payload length (48)
// 32b: control ephemeral public key (cleartext)
// 16b: message tag (authenticates the whole message)
type responseMessage [51]byte
func mkResponseMessage() responseMessage {
var ret responseMessage
ret[0] = msgTypeResponse
binary.BigEndian.PutUint16(ret[1:], uint16(len(ret.Payload())))
return ret
}
func (m *responseMessage) Header() []byte { return m[:headerLen] }
func (m *responseMessage) Payload() []byte { return m[headerLen:] }
func (m *responseMessage) Type() byte { return m[0] }
func (m *responseMessage) Length() int { return int(binary.BigEndian.Uint16(m[1:3])) }
func (m *responseMessage) EphemeralPub() []byte { return m[headerLen : headerLen+32] }
func (m *responseMessage) Tag() []byte { return m[headerLen+32:] }
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !js
// Package controlhttp implements the Tailscale 2021 control protocol
// base transport over HTTP.
//
// This tunnels the protocol in control/controlbase over HTTP with a
// variety of compatibility fallbacks for handling picky or deep
// inspecting proxies.
//
// In the happy path, a client makes a single cleartext HTTP request
// to the server, the server responds with 101 Switching Protocols,
// and the control base protocol takes place over plain TCP.
//
// In the compatibility path, the client does the above over HTTPS,
// resulting in double encryption (once for the control transport, and
// once for the outer TLS layer).
package controlhttp
import (
"cmp"
"context"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httptrace"
"net/netip"
"net/url"
"runtime"
"sync/atomic"
"time"
"tailscale.com/control/controlbase"
"tailscale.com/control/controlhttp/controlhttpcommon"
"tailscale.com/envknob"
"tailscale.com/feature"
"tailscale.com/feature/buildfeatures"
"tailscale.com/health"
"tailscale.com/net/dnscache"
"tailscale.com/net/dnsfallback"
"tailscale.com/net/netutil"
"tailscale.com/net/netx"
"tailscale.com/net/sockstats"
"tailscale.com/net/tlsdial"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
"tailscale.com/tstime"
)
var stdDialer net.Dialer
// Dial connects to the HTTP server at this Dialer's Host:HTTPPort, requests to
// switch to the Tailscale control protocol, and returns an established control
// protocol connection.
//
// If Dial fails to connect using HTTP, it also tries to tunnel over TLS to the
// Dialer's Host:HTTPSPort as a compatibility fallback.
//
// The provided ctx is only used for the initial connection, until
// Dial returns. It does not affect the connection once established.
func (a *Dialer) Dial(ctx context.Context) (*ClientConn, error) {
if a.Hostname == "" {
return nil, errors.New("required Dialer.Hostname empty")
}
return a.dial(ctx)
}
func (a *Dialer) logf(format string, args ...any) {
if a.Logf != nil {
a.Logf(format, args...)
}
}
func (a *Dialer) getProxyFunc() func(*http.Request) (*url.URL, error) {
if a.proxyFunc != nil {
return a.proxyFunc
}
return feature.HookProxyFromEnvironment.GetOrNil()
}
// httpsFallbackDelay is how long we'll wait for a.HTTPPort to work before
// starting to try a.HTTPSPort.
func (a *Dialer) httpsFallbackDelay() time.Duration {
if v := a.testFallbackDelay; v != 0 {
return v
}
return 500 * time.Millisecond
}
var _ = envknob.RegisterBool("TS_USE_CONTROL_DIAL_PLAN") // to record at init time whether it's in use
func (a *Dialer) dial(ctx context.Context) (*ClientConn, error) {
a.logPort80Failure.Store(true)
// If we don't have a dial plan, just fall back to dialing the single
// host we know about.
useDialPlan := envknob.BoolDefaultTrue("TS_USE_CONTROL_DIAL_PLAN")
if !useDialPlan || a.DialPlan == nil || len(a.DialPlan.Candidates) == 0 {
return a.dialHost(ctx)
}
candidates := a.DialPlan.Candidates
// Create a context to be canceled as we return, so once we get a good connection,
// we can drop all the other ones.
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// Now, for each candidate, kick off a dial in parallel.
type dialResult struct {
conn *ClientConn
err error
}
resultsCh := make(chan dialResult) // unbuffered, never closed
dialCand := func(cand tailcfg.ControlIPCandidate) (*ClientConn, error) {
if cand.ACEHost != "" {
a.logf("[v2] controlhttp: waited %.2f seconds, dialing %q via ACE %s (%s)", cand.DialStartDelaySec, a.Hostname, cand.ACEHost, cmp.Or(cand.IP.String(), "dns"))
} else {
a.logf("[v2] controlhttp: waited %.2f seconds, dialing %q @ %s", cand.DialStartDelaySec, a.Hostname, cand.IP.String())
}
ctx, cancel := context.WithTimeout(ctx, time.Duration(cand.DialTimeoutSec*float64(time.Second)))
defer cancel()
return a.dialHostOpt(ctx, cand.IP, cand.ACEHost)
}
for _, cand := range candidates {
timer := time.AfterFunc(time.Duration(cand.DialStartDelaySec*float64(time.Second)), func() {
go func() {
conn, err := dialCand(cand)
select {
case resultsCh <- dialResult{conn, err}:
if err == nil {
a.logf("[v1] controlhttp: succeeded dialing %q @ %v from dial plan", a.Hostname, cmp.Or(cand.ACEHost, cand.IP.String()))
}
case <-ctx.Done():
if conn != nil {
conn.Close()
}
}
}()
})
defer timer.Stop()
}
var errs []error
for {
select {
case res := <-resultsCh:
if res.err == nil {
return res.conn, nil
}
errs = append(errs, res.err)
if len(errs) == len(candidates) {
// If we get here, then we didn't get anywhere with our dial plan; fall back to just using DNS.
a.logf("controlhttp: failed dialing using DialPlan, falling back to DNS; errs=%s", errors.Join(errs...))
return a.dialHost(ctx)
}
case <-ctx.Done():
a.logf("controlhttp: context aborted dialing")
return nil, ctx.Err()
}
}
}
// The TS_FORCE_NOISE_443 envknob forces the controlclient noise dialer to
// always use port 443 HTTPS connections to the controlplane and not try the
// port 80 HTTP fast path.
//
// This is currently (2023-01-17) needed for Docker Desktop's "VPNKit" proxy
// that breaks port 80 for us post-Noise-handshake, causing us to never try port
// 443. Until one of Docker's proxy and/or this package's port 443 fallback is
// fixed, this is a workaround. It might also be useful for future debugging.
var forceNoise443 = envknob.RegisterBool("TS_FORCE_NOISE_443")
// forceNoise443 reports whether the controlclient noise dialer should always
// use HTTPS connections as its underlay connection (double crypto). This can
// be necessary when networks or middle boxes are messing with port 80.
func (d *Dialer) forceNoise443() bool {
if runtime.GOOS == "plan9" {
// For running demos of Plan 9 in a browser with network relays,
// we want to minimize the number of connections we're making.
// The main reason to use port 80 is to avoid double crypto
// costs server-side but the costs are tiny and number of Plan 9
// users doesn't make it worth it. Just disable this and always use
// HTTPS for Plan 9. That also reduces some log spam.
return true
}
if forceNoise443() {
return true
}
if d.HealthTracker.LastNoiseDialWasRecent() {
// If we dialed recently, assume there was a recent failure and fall
// back to HTTPS dials for the subsequent retries.
//
// This heuristic works around networks where port 80 is MITMed and
// appears to work for a bit post-Upgrade but then gets closed,
// such as seen in https://github.com/tailscale/tailscale/issues/13597.
if d.logPort80Failure.CompareAndSwap(true, false) {
d.logf("controlhttp: forcing port 443 dial due to recent noise dial")
}
return true
}
return false
}
func (d *Dialer) clock() tstime.Clock {
if d.Clock != nil {
return d.Clock
}
return tstime.StdClock{}
}
var debugNoiseDial = envknob.RegisterBool("TS_DEBUG_NOISE_DIAL")
// dialHost connects to the configured Dialer.Hostname and upgrades the
// connection into a controlbase.Conn.
func (a *Dialer) dialHost(ctx context.Context) (*ClientConn, error) {
return a.dialHostOpt(ctx,
netip.Addr{}, // no pre-resolved IP
"", // don't use ACE
)
}
// dialHostOpt connects to the configured Dialer.Hostname and upgrades the
// connection into a controlbase.Conn.
//
// If optAddr is valid, then no DNS is used and the connection will be made to the
// provided address.
func (a *Dialer) dialHostOpt(ctx context.Context, optAddr netip.Addr, optACEHost string) (*ClientConn, error) {
// Create one shared context used by both port 80 and port 443 dials.
// If port 80 is still in flight when 443 returns, this deferred cancel
// will stop the port 80 dial.
ctx, cancel := context.WithCancel(ctx)
defer cancel()
ctx = sockstats.WithSockStats(ctx, sockstats.LabelControlClientDialer, a.logf)
// u80 and u443 are the URLs we'll try to hit over HTTP or HTTPS,
// respectively, in order to do the HTTP upgrade to a net.Conn over which
// we'll speak Noise.
u80 := &url.URL{
Scheme: "http",
Host: net.JoinHostPort(a.Hostname, strDef(a.HTTPPort, "80")),
Path: serverUpgradePath,
}
u443 := &url.URL{
Scheme: "https",
Host: net.JoinHostPort(a.Hostname, strDef(a.HTTPSPort, "443")),
Path: serverUpgradePath,
}
if a.HTTPSPort == NoPort || optACEHost != "" {
u443 = nil
}
type tryURLRes struct {
u *url.URL // input (the URL conn+err are for/from)
conn *ClientConn // result (mutually exclusive with err)
err error
}
ch := make(chan tryURLRes) // must be unbuffered
try := func(u *url.URL) {
if debugNoiseDial() {
a.logf("trying noise dial (%v, %v) ...", u, cmp.Or(optACEHost, optAddr.String()))
}
cbConn, err := a.dialURL(ctx, u, optAddr, optACEHost)
if debugNoiseDial() {
a.logf("noise dial (%v, %v) = (%v, %v)", u, cmp.Or(optACEHost, optAddr.String()), cbConn, err)
}
select {
case ch <- tryURLRes{u, cbConn, err}:
case <-ctx.Done():
if cbConn != nil {
cbConn.Close()
}
}
}
forceTLS := a.forceNoise443()
// Start the plaintext HTTP attempt first, unless disabled by the envknob.
if !forceTLS || u443 == nil {
go try(u80)
}
// In case outbound port 80 blocked or MITM'ed poorly, start a backup timer
// to dial port 443 if port 80 doesn't either succeed or fail quickly.
var try443Timer tstime.TimerController
if u443 != nil {
delay := a.httpsFallbackDelay()
if forceTLS {
delay = 0
}
try443Timer = a.clock().AfterFunc(delay, func() { try(u443) })
defer try443Timer.Stop()
}
var err80, err443 error
if forceTLS {
err80 = errors.New("TLS forced: no port 80 dialed")
}
for {
select {
case <-ctx.Done():
return nil, fmt.Errorf("connection attempts aborted by context: %w", ctx.Err())
case res := <-ch:
if res.err == nil {
return res.conn, nil
}
switch res.u {
case u80:
// Connecting over plain HTTP failed; assume it's an HTTP proxy
// being difficult and see if we can get through over HTTPS.
err80 = res.err
// Stop the fallback timer and run it immediately. We don't use
// Timer.Reset(0) here because on AfterFuncs, that can run it
// again.
if try443Timer != nil && try443Timer.Stop() {
go try(u443)
} // else we lost the race and it started already which is what we want
case u443:
err443 = res.err
default:
panic("invalid")
}
if err80 != nil && err443 != nil {
return nil, fmt.Errorf("all connection attempts failed (HTTP: %v, HTTPS: %v)", err80, err443)
}
}
}
}
// dialURL attempts to connect to the given URL.
//
// If optAddr is valid, then no DNS is used and the connection will be made to the
// provided address.
func (a *Dialer) dialURL(ctx context.Context, u *url.URL, optAddr netip.Addr, optACEHost string) (*ClientConn, error) {
init, cont, err := controlbase.ClientDeferred(a.MachineKey, a.ControlKey, a.ProtocolVersion)
if err != nil {
return nil, err
}
netConn, err := a.tryURLUpgrade(ctx, u, optAddr, optACEHost, init)
if err != nil {
return nil, err
}
cbConn, err := cont(ctx, netConn)
if err != nil {
netConn.Close()
return nil, err
}
return &ClientConn{
Conn: cbConn,
}, nil
}
// resolver returns a.DNSCache if non-nil or a new *dnscache.Resolver
// otherwise.
func (a *Dialer) resolver() *dnscache.Resolver {
if a.DNSCache != nil {
return a.DNSCache
}
return &dnscache.Resolver{
Forward: dnscache.Get().Forward,
LookupIPFallback: dnsfallback.MakeLookupFunc(a.logf, a.NetMon),
UseLastGood: true,
Logf: a.Logf, // not a.logf method; we want to propagate nil-ness
}
}
func isLoopback(a net.Addr) bool {
if ta, ok := a.(*net.TCPAddr); ok {
return ta.IP.IsLoopback()
}
return false
}
var macOSScreenTime = health.Register(&health.Warnable{
Code: "macos-screen-time",
Severity: health.SeverityHigh,
Title: "Tailscale blocked by Screen Time",
Text: func(args health.Args) string {
return "macOS Screen Time seems to be blocking Tailscale. Try disabling Screen Time in System Settings > Screen Time > Content & Privacy > Access to Web Content."
},
ImpactsConnectivity: true,
})
var HookMakeACEDialer feature.Hook[func(dialer netx.DialFunc, aceHost string, optIP netip.Addr) netx.DialFunc]
// tryURLUpgrade connects to u, and tries to upgrade it to a net.Conn.
//
// If optAddr is valid, then no DNS is used and the connection will be made to
// the provided address.
//
// Only the provided ctx is used, not a.ctx.
func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, optAddr netip.Addr, optACEHost string, init []byte) (_ net.Conn, retErr error) {
var dns *dnscache.Resolver
// If we were provided an address to dial, then create a resolver that just
// returns that value; otherwise, fall back to DNS.
if optAddr.IsValid() {
dns = &dnscache.Resolver{
SingleHostStaticResult: []netip.Addr{optAddr},
SingleHost: u.Hostname(),
Logf: a.Logf, // not a.logf method; we want to propagate nil-ness
}
} else {
dns = a.resolver()
}
var dialer netx.DialFunc
if a.Dialer != nil {
dialer = a.Dialer
} else {
dialer = stdDialer.DialContext
}
if optACEHost != "" {
if !buildfeatures.HasACE {
return nil, feature.ErrUnavailable
}
f, ok := HookMakeACEDialer.GetOk()
if !ok {
return nil, feature.ErrUnavailable
}
dialer = f(dialer, optACEHost, optAddr)
}
// On macOS, see if Screen Time is blocking things.
if runtime.GOOS == "darwin" {
var proxydIntercepted atomic.Bool // intercepted by macOS webfilterproxyd
origDialer := dialer
dialer = func(ctx context.Context, network, address string) (net.Conn, error) {
c, err := origDialer(ctx, network, address)
if err != nil {
return nil, err
}
if isLoopback(c.LocalAddr()) && isLoopback(c.RemoteAddr()) {
proxydIntercepted.Store(true)
}
return c, nil
}
defer func() {
if retErr != nil && proxydIntercepted.Load() {
a.HealthTracker.SetUnhealthy(macOSScreenTime, nil)
retErr = fmt.Errorf("macOS Screen Time is blocking network access: %w", retErr)
} else {
a.HealthTracker.SetHealthy(macOSScreenTime)
}
}()
}
tr := netutil.NewDefaultTransport()
defer tr.CloseIdleConnections()
if optACEHost != "" {
// If using ACE, we don't want to use any HTTP proxy.
// ACE is already a tunnel+proxy.
// TODO(tailscale/corp#32483): use system proxy too?
tr.Proxy = nil
tr.DialContext = dialer
} else {
if buildfeatures.HasUseProxy {
tr.Proxy = a.getProxyFunc()
if set, ok := feature.HookProxySetTransportGetProxyConnectHeader.GetOk(); ok {
set(tr)
}
}
tr.DialContext = dnscache.Dialer(dialer, dns)
}
// Disable HTTP2, since h2 can't do protocol switching.
tr.TLSClientConfig.NextProtos = []string{}
tr.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{}
if a.ExtraRootCAs != nil {
tr.TLSClientConfig.RootCAs = a.ExtraRootCAs
}
tr.TLSClientConfig = tlsdial.Config(a.HealthTracker, tr.TLSClientConfig)
if !tr.TLSClientConfig.InsecureSkipVerify {
panic("unexpected") // should be set by tlsdial.Config
}
verify := tr.TLSClientConfig.VerifyConnection
if verify == nil {
panic("unexpected") // should be set by tlsdial.Config
}
// Demote all cert verification errors to log messages. We don't actually
// care about the TLS security (because we just do the Noise crypto atop whatever
// connection we get, including HTTP port 80 plaintext) so this permits
// middleboxes to MITM their users. All they'll see is some Noise.
tr.TLSClientConfig.VerifyConnection = func(cs tls.ConnectionState) error {
if err := verify(cs); err != nil && a.Logf != nil && !a.omitCertErrorLogging {
a.Logf("warning: TLS cert verificication for %q failed: %v", a.Hostname, err)
}
return nil // regardless
}
tr.DialTLSContext = dnscache.TLSDialer(dialer, dns, tr.TLSClientConfig)
tr.DisableCompression = true
// (mis)use httptrace to extract the underlying net.Conn from the
// transport. The transport handles 101 Switching Protocols correctly,
// such that the Conn will not be reused or kept alive by the transport
// once the response has been handed back from RoundTrip.
//
// In theory, the machinery of net/http should make it such that
// the trace callback happens-before we get the response, but
// there's no promise of that. So, to make sure, we use a buffered
// channel as a synchronization step to avoid data races.
//
// Note that even though we're able to extract a net.Conn via this
// mechanism, we must still keep using the eventual resp.Body to
// read from, because it includes a buffer we can't get rid of. If
// the server never sends any data after sending the HTTP
// response, we could get away with it, but violating this
// assumption leads to very mysterious transport errors (lockups,
// unexpected EOFs...), and we're bound to forget someday and
// introduce a protocol optimization at a higher level that starts
// eagerly transmitting from the server.
var lastConn syncs.AtomicValue[net.Conn]
trace := httptrace.ClientTrace{
// Even though we only make a single HTTP request which should
// require a single connection, the context (with the attached
// trace configuration) might be used by our custom dialer to
// make other HTTP requests (e.g. BootstrapDNS). We only care
// about the last connection made, which should be the one to
// the control server.
GotConn: func(info httptrace.GotConnInfo) {
lastConn.Store(info.Conn)
},
}
ctx = httptrace.WithClientTrace(ctx, &trace)
req := &http.Request{
Method: "POST",
URL: u,
Header: http.Header{
"Upgrade": []string{controlhttpcommon.UpgradeHeaderValue},
"Connection": []string{"upgrade"},
controlhttpcommon.HandshakeHeaderName: []string{base64.StdEncoding.EncodeToString(init)},
},
}
req = req.WithContext(ctx)
resp, err := tr.RoundTrip(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusSwitchingProtocols {
return nil, fmt.Errorf("unexpected HTTP response: %s", resp.Status)
}
// From here on, the underlying net.Conn is ours to use, but there
// is still a read buffer attached to it within resp.Body. So, we
// must direct I/O through resp.Body, but we can still use the
// underlying net.Conn for stuff like deadlines.
switchedConn := lastConn.Load()
if switchedConn == nil {
resp.Body.Close()
return nil, fmt.Errorf("httptrace didn't provide a connection")
}
if next := resp.Header.Get("Upgrade"); next != controlhttpcommon.UpgradeHeaderValue {
resp.Body.Close()
return nil, fmt.Errorf("server switched to unexpected protocol %q", next)
}
rwc, ok := resp.Body.(io.ReadWriteCloser)
if !ok {
resp.Body.Close()
return nil, errors.New("http Transport did not provide a writable body")
}
return netutil.NewAltReadWriteCloserConn(rwc, switchedConn), nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package controlhttp
import (
"crypto/x509"
"net/http"
"net/url"
"sync/atomic"
"time"
"tailscale.com/health"
"tailscale.com/net/dnscache"
"tailscale.com/net/netmon"
"tailscale.com/net/netx"
"tailscale.com/tailcfg"
"tailscale.com/tstime"
"tailscale.com/types/key"
"tailscale.com/types/logger"
)
const (
// serverUpgradePath is where the server-side HTTP handler to
// to do the protocol switch is located.
serverUpgradePath = "/ts2021"
)
// NoPort is a sentinel value for Dialer.HTTPSPort to indicate that HTTPS
// should not be tried on any port. It exists primarily for some localhost
// tests where the control plane only runs on HTTP.
const NoPort = "none"
// Dialer contains configuration on how to dial the Tailscale control server.
type Dialer struct {
// Hostname is the hostname to connect to, with no port number.
//
// This field is required.
Hostname string
// MachineKey contains the current machine's private key.
//
// This field is required.
MachineKey key.MachinePrivate
// ControlKey contains the expected public key for the control server.
//
// This field is required.
ControlKey key.MachinePublic
// ProtocolVersion is the expected protocol version to negotiate.
//
// This field is required.
ProtocolVersion uint16
// HTTPPort is the port number to use when making a HTTP connection.
//
// If not specified, this defaults to port 80.
HTTPPort string
// HTTPSPort is the port number to use when making a HTTPS connection.
//
// If not specified, this defaults to port 443.
//
// If "none" (NoPort), HTTPS is disabled.
HTTPSPort string
// Dialer is the dialer used to make outbound connections.
//
// If not specified, this defaults to net.Dialer.DialContext.
Dialer netx.DialFunc
// DNSCache is the caching Resolver used by this Dialer.
//
// If not specified, a new Resolver is created per attempt.
DNSCache *dnscache.Resolver
// Logf, if set, is a logging function to use; if unset, logs are
// dropped.
Logf logger.Logf
// NetMon is the [netmon.Monitor] to use for this Dialer.
// It is optional.
NetMon *netmon.Monitor
// HealthTracker, if non-nil, is the health tracker to use.
HealthTracker *health.Tracker
// ExtraRootCAs, if non-nil, specifies additional trusted root CAs for TLS.
ExtraRootCAs *x509.CertPool
// DialPlan, if set, contains instructions from the control server on
// how to connect to it. If present, we will try the methods in this
// plan before falling back to DNS.
DialPlan *tailcfg.ControlDialPlan
proxyFunc func(*http.Request) (*url.URL, error) // or nil
// logPort80Failure is whether we should log about port 80 interceptions
// and forcing a port 443 dial. We do this only once per "dial" method
// which can result in many concurrent racing dialHost calls.
logPort80Failure atomic.Bool
// For tests only
omitCertErrorLogging bool
testFallbackDelay time.Duration
// Clock, if non-nil, overrides the clock to use.
// If nil, tstime.StdClock is used.
// This exists primarily for tests.
Clock tstime.Clock
}
func strDef(v1, v2 string) string {
if v1 != "" {
return v1
}
return v2
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ios
// Package controlhttpserver contains the HTTP server side of the ts2021 control protocol.
package controlhttpserver
import (
"context"
"encoding/base64"
"errors"
"fmt"
"io"
"net"
"net/http"
"strings"
"time"
"github.com/coder/websocket"
"tailscale.com/control/controlbase"
"tailscale.com/control/controlhttp/controlhttpcommon"
"tailscale.com/net/netutil"
"tailscale.com/net/wsconn"
"tailscale.com/types/key"
)
// AcceptHTTP upgrades the HTTP request given by w and r into a Tailscale
// control protocol base transport connection.
//
// AcceptHTTP always writes an HTTP response to w. The caller must not attempt
// their own response after calling AcceptHTTP.
//
// The provided ctx bounds the handshake: its deadline, if any, applies to the
// handshake I/O. The returned conn's lifetime is not otherwise bound to ctx;
// in particular, it survives net/http's cancellation of the request context
// when the calling handler returns. The caller owns the returned conn and
// must close it.
//
// earlyWrite optionally specifies a func to write to the noise connection
// (encrypted). It receives the negotiated version and a writer to write to, if
// desired.
func AcceptHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request, private key.MachinePrivate, earlyWrite func(protocolVersion int, w io.Writer) error) (*controlbase.Conn, error) {
return acceptHTTP(ctx, w, r, private, earlyWrite)
}
func acceptHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request, private key.MachinePrivate, earlyWrite func(protocolVersion int, w io.Writer) error) (_ *controlbase.Conn, retErr error) {
next := strings.ToLower(r.Header.Get("Upgrade"))
if next == "" {
http.Error(w, "missing next protocol", http.StatusBadRequest)
return nil, errors.New("no next protocol in HTTP request")
}
if next == "websocket" {
return acceptWebsocket(ctx, w, r, private)
}
if next != controlhttpcommon.UpgradeHeaderValue {
http.Error(w, "unknown next protocol", http.StatusBadRequest)
return nil, fmt.Errorf("client requested unhandled next protocol %q", next)
}
initB64 := r.Header.Get(controlhttpcommon.HandshakeHeaderName)
if initB64 == "" {
http.Error(w, "missing Tailscale handshake header", http.StatusBadRequest)
return nil, errors.New("no tailscale handshake header in HTTP request")
}
init, err := base64.StdEncoding.DecodeString(initB64)
if err != nil {
http.Error(w, "invalid tailscale handshake header", http.StatusBadRequest)
return nil, fmt.Errorf("decoding base64 handshake header: %v", err)
}
hijacker, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "make request over HTTP/1", http.StatusBadRequest)
return nil, errors.New("can't hijack client connection")
}
w.Header().Set("Upgrade", controlhttpcommon.UpgradeHeaderValue)
w.Header().Set("Connection", "upgrade")
w.WriteHeader(http.StatusSwitchingProtocols)
conn, brw, err := hijacker.Hijack()
if err != nil {
return nil, fmt.Errorf("hijacking client connection: %w", err)
}
defer func() {
if retErr != nil {
conn.Close()
}
}()
if err := brw.Flush(); err != nil {
return nil, fmt.Errorf("flushing hijacked HTTP buffer: %w", err)
}
conn = netutil.NewDrainBufConn(conn, brw.Reader)
cwc := newWriteCorkingConn(conn)
nc, err := controlbase.Server(ctx, cwc, private, init)
if err != nil {
return nil, fmt.Errorf("noise handshake failed: %w", err)
}
if earlyWrite != nil {
if deadline, ok := ctx.Deadline(); ok {
if err := conn.SetDeadline(deadline); err != nil {
return nil, fmt.Errorf("setting conn deadline: %w", err)
}
defer conn.SetDeadline(time.Time{})
}
if err := earlyWrite(nc.ProtocolVersion(), nc); err != nil {
return nil, err
}
}
if err := cwc.uncork(); err != nil {
return nil, err
}
return nc, nil
}
// acceptWebsocket upgrades a WebSocket connection (from a client that cannot
// speak HTTP) to a Tailscale control protocol base transport connection.
func acceptWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request, private key.MachinePrivate) (*controlbase.Conn, error) {
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
Subprotocols: []string{controlhttpcommon.UpgradeHeaderValue},
OriginPatterns: []string{"*"},
// Disable compression because we transmit Noise messages that are not
// compressible.
// Additionally, Safari has a broken implementation of compression
// (see https://github.com/nhooyr/websocket/issues/218) that makes
// enabling it actively harmful.
CompressionMode: websocket.CompressionDisabled,
})
if err != nil {
return nil, fmt.Errorf("Could not accept WebSocket connection %v", err)
}
if c.Subprotocol() != controlhttpcommon.UpgradeHeaderValue {
c.Close(websocket.StatusPolicyViolation, "client must speak the control subprotocol")
return nil, fmt.Errorf("Unexpected subprotocol %q", c.Subprotocol())
}
if err := r.ParseForm(); err != nil {
c.Close(websocket.StatusPolicyViolation, "Could not parse parameters")
return nil, fmt.Errorf("parse query parameters: %v", err)
}
initB64 := r.Form.Get(controlhttpcommon.HandshakeHeaderName)
if initB64 == "" {
c.Close(websocket.StatusPolicyViolation, "missing Tailscale handshake parameter")
return nil, errors.New("no tailscale handshake parameter in HTTP request")
}
init, err := base64.StdEncoding.DecodeString(initB64)
if err != nil {
c.Close(websocket.StatusPolicyViolation, "invalid tailscale handshake parameter")
return nil, fmt.Errorf("decoding base64 handshake parameter: %v", err)
}
// Do not bind the conn's lifetime to ctx: it's typically a request
// context that net/http cancels once the calling handler returns, and
// the conn may be served beyond that (tailscale/corp#46806). The
// handshake below is still bounded by any ctx deadline, which
// controlbase.Server applies to the conn directly.
conn := wsconn.NetConn(context.WithoutCancel(ctx), c, websocket.MessageBinary, r.RemoteAddr)
nc, err := controlbase.Server(ctx, conn, private, init)
if err != nil {
conn.Close()
return nil, fmt.Errorf("noise handshake failed: %w", err)
}
return nc, nil
}
// corkConn is a net.Conn wrapper that initially buffers all writes until uncork
// is called. If the conn is corked and a Read occurs, the Read will flush any
// buffered (corked) write.
//
// Until uncorked, Read/Write/uncork may be not called concurrently.
//
// Deadlines still work, but a corked write ignores deadlines until a Read or
// uncork goes to do that Write.
//
// Use newWriteCorkingConn to create one.
type corkConn struct {
net.Conn
corked bool
buf []byte // corked data
}
func newWriteCorkingConn(c net.Conn) *corkConn {
return &corkConn{Conn: c, corked: true}
}
func (c *corkConn) Write(b []byte) (int, error) {
if c.corked {
c.buf = append(c.buf, b...)
return len(b), nil
}
return c.Conn.Write(b)
}
func (c *corkConn) Read(b []byte) (int, error) {
if c.corked {
if err := c.flush(); err != nil {
return 0, err
}
}
return c.Conn.Read(b)
}
// uncork flushes any buffered data and uncorks the connection so future Writes
// don't buffer. It may not be called concurrently with reads or writes and
// may only be called once.
func (c *corkConn) uncork() error {
if !c.corked {
panic("usage error; uncork called twice") // worth panicking to catch misuse
}
err := c.flush()
c.corked = false
return err
}
func (c *corkConn) flush() error {
if len(c.buf) == 0 {
return nil
}
_, err := c.Conn.Write(c.buf)
c.buf = nil
return err
}
// 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 lets sandboxed macOS scope quad-100 for simple DNS
// configs, so public queries can use the OS resolver (including DoH).
// Custom split resolvers or unrouted forward/PTR records still require
// quad-100 as primary. Off by default; has no effect on other platforms.
// See tailscale/corp#45534 and tailscale/corp#48693.
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 ts2021
import (
"bytes"
"cmp"
"context"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"log"
"math"
"net"
"net/http"
"net/netip"
"net/url"
"sync"
"time"
"tailscale.com/control/controlhttp"
"tailscale.com/health"
"tailscale.com/net/dnscache"
"tailscale.com/net/netmon"
"tailscale.com/net/tsdial"
"tailscale.com/tailcfg"
"tailscale.com/tstime"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/util/httpbody"
"tailscale.com/util/mak"
"tailscale.com/util/set"
)
// Client provides a http.Client to connect to tailcontrol over
// the ts2021 protocol.
type Client struct {
// Client is an HTTP client to talk to the coordination server.
// It automatically makes a new Noise connection as needed.
*http.Client
logf logger.Logf // non-nil
opts ClientOpts
host string // the host part of serverURL
httpPort string // the default port to dial
httpsPort string // the fallback Noise-over-https port or empty if none
// mu protects the following
mu sync.Mutex
closed bool
connPool set.HandleSet[*Conn] // all live connections
}
// ClientOpts contains options for the [NewClient] function. All fields are
// required unless otherwise specified.
type ClientOpts struct {
// ServerURL is the URL of the server to connect to.
ServerURL string
// PrivKey is this node's private key.
PrivKey key.MachinePrivate
// ServerPubKey is the public key of the server.
// It is of the form https://<host>:<port> (no trailing slash).
ServerPubKey key.MachinePublic
// Dialer's SystemDial function is used to connect to the server.
Dialer *tsdial.Dialer
// Optional fields follow
// Logf is the log function to use.
// If nil, log.Printf is used.
Logf logger.Logf
// NetMon is the network monitor that will be used to get the
// network interface state. This field can be nil; if so, the current
// state will be looked up dynamically.
NetMon *netmon.Monitor
// DNSCache is the caching Resolver to use to connect to the server.
//
// This field can be nil.
DNSCache *dnscache.Resolver
// HealthTracker, if non-nil, is the health tracker to use.
HealthTracker *health.Tracker
// ExtraRootCAs, if non-nil, specifies additional trusted root CAs for TLS.
ExtraRootCAs *x509.CertPool
// DialPlan, if set, is a function that should return an explicit plan
// on how to connect to the server.
DialPlan func() *tailcfg.ControlDialPlan
// ProtocolVersion, if non-zero, specifies an alternate
// protocol version to use instead of the default,
// of [tailcfg.CurrentCapabilityVersion].
ProtocolVersion uint16
}
// NewClient returns a new noiseClient for the provided server and machine key.
//
// netMon may be nil, if non-nil it's used to do faster interface lookups.
// dialPlan may be nil
func NewClient(opts ClientOpts) (*Client, error) {
logf := opts.Logf
if logf == nil {
logf = log.Printf
}
if opts.ServerURL == "" {
return nil, errors.New("ServerURL is required")
}
if opts.PrivKey.IsZero() {
return nil, errors.New("PrivKey is required")
}
if opts.ServerPubKey.IsZero() {
return nil, errors.New("ServerPubKey is required")
}
if opts.Dialer == nil {
return nil, errors.New("Dialer is required")
}
u, err := url.Parse(opts.ServerURL)
if err != nil {
return nil, fmt.Errorf("invalid ClientOpts.ServerURL: %w", err)
}
if u.Scheme != "http" && u.Scheme != "https" {
return nil, errors.New("invalid ServerURL scheme, must be http or https")
}
httpPort, httpsPort := "80", "443"
addr, _ := netip.ParseAddr(u.Hostname())
isPrivateHost := addr.IsPrivate() || addr.IsLoopback() || u.Hostname() == "localhost"
if port := u.Port(); port != "" {
// If there is an explicit port specified, entirely rely on the scheme,
// unless it's http with a private host in which case we never try using HTTPS.
if u.Scheme == "https" {
httpPort = ""
httpsPort = port
} else if u.Scheme == "http" {
httpPort = port
httpsPort = "443"
if isPrivateHost {
logf("setting empty HTTPS port with http scheme and private host %s", u.Hostname())
httpsPort = ""
}
}
} else if u.Scheme == "http" && isPrivateHost {
// Whenever the scheme is http and the hostname is an IP address, do not set the HTTPS port,
// as there cannot be a TLS certificate issued for an IP, unless it's a public IP.
httpPort = "80"
httpsPort = ""
}
np := &Client{
opts: opts,
host: u.Hostname(),
httpPort: httpPort,
httpsPort: httpsPort,
logf: logf,
}
tr := &http.Transport{
Protocols: new(http.Protocols),
MaxConnsPerHost: 1,
}
// We force only HTTP/2 for this transport, which is what the control server
// speaks inside the ts2021 Noise encryption. But Go doesn't know about that,
// so we use "SetUnencryptedHTTP2" even though it's actually encrypted.
tr.Protocols.SetUnencryptedHTTP2(true)
tr.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
return np.dial(ctx)
}
np.Client = &http.Client{Transport: tr}
return np, nil
}
// Close closes all the underlying noise connections.
// It is a no-op and returns nil if the connection is already closed.
func (nc *Client) Close() error {
nc.mu.Lock()
live := nc.connPool
nc.closed = true
nc.connPool = nil // stop noteConnClosed from mutating it as we loop over it (in live) below
nc.mu.Unlock()
for _, c := range live {
c.Close()
}
nc.Client.CloseIdleConnections()
return nil
}
// dial opens a new connection to tailcontrol, fetching the server noise key
// if not cached.
func (nc *Client) dial(ctx context.Context) (*Conn, error) {
if tailcfg.CurrentCapabilityVersion > math.MaxUint16 {
// Panic, because a test should have started failing several
// thousand version numbers before getting to this point.
panic("capability version is too high to fit in the wire protocol")
}
var dialPlan *tailcfg.ControlDialPlan
if nc.opts.DialPlan != nil {
dialPlan = nc.opts.DialPlan()
}
// If we have a dial plan, then set our timeout as slightly longer than
// the maximum amount of time contained therein; we assume that
// explicit instructions on timeouts are more useful than a single
// hard-coded timeout.
//
// The default value of 5 is chosen so that, when there's no dial plan,
// we retain the previous behaviour of 10 seconds end-to-end timeout.
timeoutSec := 5.0
if dialPlan != nil {
for _, c := range dialPlan.Candidates {
if v := c.DialStartDelaySec + c.DialTimeoutSec; v > timeoutSec {
timeoutSec = v
}
}
}
// After we establish a connection, we need some time to actually
// upgrade it into a Noise connection. With a ballpark worst-case RTT
// of 1000ms, give ourselves an extra 5 seconds to complete the
// handshake.
timeoutSec += 5
// Be extremely defensive and ensure that the timeout is in the range
// [5, 60] seconds (e.g. if we accidentally get a negative number).
if timeoutSec > 60 {
timeoutSec = 60
} else if timeoutSec < 5 {
timeoutSec = 5
}
timeout := time.Duration(timeoutSec * float64(time.Second))
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
chd := &controlhttp.Dialer{
Hostname: nc.host,
HTTPPort: nc.httpPort,
HTTPSPort: cmp.Or(nc.httpsPort, controlhttp.NoPort),
MachineKey: nc.opts.PrivKey,
ControlKey: nc.opts.ServerPubKey,
ProtocolVersion: cmp.Or(nc.opts.ProtocolVersion, uint16(tailcfg.CurrentCapabilityVersion)),
Dialer: nc.opts.Dialer.SystemDial,
DNSCache: nc.opts.DNSCache,
DialPlan: dialPlan,
Logf: nc.logf,
NetMon: nc.opts.NetMon,
HealthTracker: nc.opts.HealthTracker,
ExtraRootCAs: nc.opts.ExtraRootCAs,
Clock: tstime.StdClock{},
}
clientConn, err := chd.Dial(ctx)
if err != nil {
return nil, err
}
nc.mu.Lock()
handle := set.NewHandle()
ncc := NewConn(clientConn.Conn, func() { nc.noteConnClosed(handle) })
mak.Set(&nc.connPool, handle, ncc)
if nc.closed {
nc.mu.Unlock()
ncc.Close() // Needs to be called without holding the lock.
return nil, errors.New("noise client closed")
}
defer nc.mu.Unlock()
return ncc, nil
}
// noteConnClosed notes that the *Conn with the given handle has closed and
// should be removed from the live connPool (which is usually of size 0 or 1,
// except perhaps briefly 2 during a network failure and reconnect).
func (nc *Client) noteConnClosed(handle set.Handle) {
nc.mu.Lock()
defer nc.mu.Unlock()
nc.connPool.Delete(handle)
}
// post does a POST to the control server at the given path, JSON-encoding body.
// The provided nodeKey is an optional load balancing hint.
func (nc *Client) Post(ctx context.Context, path string, nodeKey key.NodePublic, body any) (*http.Response, error) {
return nc.DoWithBody(ctx, "POST", path, nodeKey, body)
}
func (nc *Client) DoWithBody(ctx context.Context, method, path string, nodeKey key.NodePublic, body any) (*http.Response, error) {
jbody, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, method, "https://"+nc.host+path, bytes.NewReader(jbody))
if err != nil {
return nil, err
}
AddLBHeader(req, nodeKey)
req.Header.Set("Content-Type", "application/json")
return nc.Do(req)
}
// AddLBHeader adds the load balancer header to req if nodeKey is non-zero.
func AddLBHeader(req *http.Request, nodeKey key.NodePublic) {
if !nodeKey.IsZero() {
req.Header.Add(tailcfg.LBHeader, nodeKey.String())
}
}
// Do does req against the control server and caps the response body with
// httpbody.LimitSize, using the size limit in the request's context per
// [httpbody.WithMaxSize], [httpbody.DefaultMaxSize] by default, so a
// malicious or buggy control server cannot make us buffer an unbounded
// response body. Reads past the cap fail with an error wrapping
// [httpbody.ErrTooLarge] rather than silently truncating.
//
// It shadows the embedded http.Client's Do method, so all noise requests,
// including those made with [Client.Post] and [Client.DoWithBody], get the
// cap.
func (nc *Client) Do(req *http.Request) (*http.Response, error) {
res, err := nc.Client.Do(req)
if err != nil {
return nil, err
}
httpbody.LimitSize(res)
return res, nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package ts2021 handles the details of the Tailscale 2021 control protocol
// that are after (above) the Noise layer. In particular, the
// "tailcfg.EarlyNoise" message and the subsequent HTTP/2 connection.
package ts2021
import (
"bytes"
"context"
"encoding/binary"
"encoding/json"
"errors"
"io"
"sync"
"tailscale.com/control/controlbase"
"tailscale.com/tailcfg"
)
// Conn is a wrapper around controlbase.Conn.
//
// It allows attaching an ID to a connection to allow cleaning up references in
// the pool when the connection is closed, properly handles an optional "early
// payload" that's sent prior to beginning the HTTP/2 session, and provides a
// way to return a connection to a pool when the connection is closed.
//
// Use [NewConn] to build a new Conn if you want [Conn.GetEarlyPayload] to work.
// Otherwise making a Conn directly, only setting Conn, is fine.
type Conn struct {
*controlbase.Conn
onClose func() // or nil
readHeaderOnce sync.Once // guards init of reader field
reader io.Reader // (effectively Conn.Reader after header)
earlyPayloadReady chan struct{} // closed after earlyPayload is set (including set to nil)
earlyPayload *tailcfg.EarlyNoise
earlyPayloadErr error
}
// NewConn creates a new Conn that wraps the given controlbase.Conn.
//
// h2t is the HTTP/2 transport to use for the connection; a new
// http2.ClientConn will be created that reads from the returned Conn.
//
// connID should be a unique ID for this connection. When the Conn is closed,
// the onClose function will be called if it is non-nil.
func NewConn(conn *controlbase.Conn, onClose func()) *Conn {
return &Conn{
Conn: conn,
earlyPayloadReady: make(chan struct{}),
onClose: sync.OnceFunc(onClose),
}
}
// GetEarlyPayload waits for the early Noise payload to arrive.
// It may return (nil, nil) if the server begins HTTP/2 without one.
//
// It is safe to call this multiple times; all callers will block until the
// early Noise payload is ready (if any) and will return the same result for
// the lifetime of the Conn.
func (c *Conn) GetEarlyPayload(ctx context.Context) (*tailcfg.EarlyNoise, error) {
if c.earlyPayloadReady == nil {
return nil, errors.New("Conn was not created with NewConn; early payload not supported")
}
select {
case <-c.earlyPayloadReady:
return c.earlyPayload, c.earlyPayloadErr
default:
go c.readHeaderOnce.Do(c.readHeader)
}
select {
case <-c.earlyPayloadReady:
return c.earlyPayload, c.earlyPayloadErr
case <-ctx.Done():
return nil, ctx.Err()
}
}
// The first 9 bytes from the server to client over Noise are either an HTTP/2
// settings frame (a normal HTTP/2 setup) or, as we added later, an "early payload"
// header that's also 9 bytes long: 5 bytes (EarlyPayloadMagic) followed by 4 bytes
// of length. Then that many bytes of JSON-encoded tailcfg.EarlyNoise.
// The early payload is optional. Some servers may not send it.
const (
hdrLen = 9 // http2 frame header size; also size of our early payload size header
)
// EarlyPayloadMagic is the 5-byte magic prefix that indicates an early payload.
const EarlyPayloadMagic = "\xff\xff\xffTS"
// returnErrReader is an io.Reader that always returns an error.
type returnErrReader struct {
err error // the error to return
}
func (r returnErrReader) Read([]byte) (int, error) { return 0, r.err }
// Read is basically the same as controlbase.Conn.Read, but it first reads the
// "early payload" header from the server which may or may not be present,
// depending on the server.
func (c *Conn) Read(p []byte) (n int, err error) {
c.readHeaderOnce.Do(c.readHeader)
return c.reader.Read(p)
}
// Close closes the connection.
func (c *Conn) Close() error {
if c.onClose != nil {
defer c.onClose()
}
return c.Conn.Close()
}
// readHeader reads the optional "early payload" from the server that arrives
// after the Noise handshake but before the HTTP/2 session begins.
//
// readHeader is responsible for reading the header (if present), initializing
// c.earlyPayload, closing c.earlyPayloadReady, and initializing c.reader for
// future reads.
func (c *Conn) readHeader() {
if c.earlyPayloadReady != nil {
defer close(c.earlyPayloadReady)
}
setErr := func(err error) {
c.reader = returnErrReader{err}
c.earlyPayloadErr = err
}
var hdr [hdrLen]byte
if _, err := io.ReadFull(c.Conn, hdr[:]); err != nil {
setErr(err)
return
}
if string(hdr[:len(EarlyPayloadMagic)]) != EarlyPayloadMagic {
// No early payload. We have to return the 9 bytes read we already
// consumed.
c.reader = io.MultiReader(bytes.NewReader(hdr[:]), c.Conn)
return
}
epLen := binary.BigEndian.Uint32(hdr[len(EarlyPayloadMagic):])
if epLen > 10<<20 {
setErr(errors.New("invalid early payload length"))
return
}
payBuf := make([]byte, epLen)
if _, err := io.ReadFull(c.Conn, payBuf); err != nil {
setErr(err)
return
}
if err := json.Unmarshal(payBuf, &c.earlyPayload); err != nil {
setErr(err)
return
}
c.reader = c.Conn
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package tsp
import (
"bytes"
"cmp"
"context"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"sync"
"sync/atomic"
"github.com/klauspost/compress/zstd"
"tailscale.com/control/ts2021"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/util/httpbody"
)
// errSessionClosed is returned by [MapSession.Next] and
// [MapSession.NextInto] when called after [MapSession.Close].
var errSessionClosed = errors.New("tsp: map session closed")
// DefaultMaxMessageSize is the default cap, in bytes, on the size of a
// single compressed map response frame. See [MapOpts.MaxMessageSize].
const DefaultMaxMessageSize = 4 << 20
// zstdDecoderPool is a pool of *zstd.Decoder reused across MapSessions to
// amortize the cost of setting up zstd state. Decoders are returned via
// [MapSession.Close]; entries are reclaimed by the runtime under memory
// pressure via sync.Pool semantics.
var zstdDecoderPool sync.Pool // of *zstd.Decoder
// MapOpts contains options for sending a map request.
type MapOpts struct {
// NodeKey is the node's private key. Required.
NodeKey key.NodePrivate
// Hostinfo is the host information to send. Optional;
// if nil, a minimal default is used.
Hostinfo *tailcfg.Hostinfo
// Stream is whether to receive multiple MapResponses over
// the same HTTP connection.
Stream bool
// OmitPeers is whether the client is okay with the Peers list
// being omitted in the response.
OmitPeers bool
// MaxMessageSize is the maximum size in bytes of any single
// compressed map response frame on the wire. If zero,
// [DefaultMaxMessageSize] is used.
MaxMessageSize int64
}
// framedReader is an io.Reader that consumes a stream of length-prefixed
// frames (each a little-endian uint32 length followed by that many bytes)
// from r and yields only the frame payloads back-to-back.
//
// This lets us feed the concatenated zstd frames from our wire protocol
// into a single streaming zstd decoder. Zstd's file format permits
// concatenation (RFC 8478 §2), and klauspost's decoder handles it
// transparently.
//
// If onNewFrame is non-nil, it is called after each new 4-byte length
// header is successfully read. Used to reset the per-message decoded-size
// budget downstream.
type framedReader struct {
r io.Reader
maxSize int64 // per-frame compressed-size cap
remain int // bytes remaining in the current frame
onNewFrame func()
}
func (f *framedReader) Read(p []byte) (int, error) {
if f.remain == 0 {
var hdr [4]byte
if _, err := io.ReadFull(f.r, hdr[:]); err != nil {
return 0, err
}
sz := int64(binary.LittleEndian.Uint32(hdr[:]))
if sz == 0 {
return 0, fmt.Errorf("map response: zero-length frame")
}
if sz > f.maxSize {
return 0, fmt.Errorf("map response frame size %d exceeds max %d", sz, f.maxSize)
}
f.remain = int(sz)
if f.onNewFrame != nil {
f.onNewFrame()
}
}
if len(p) > f.remain {
p = p[:f.remain]
}
n, err := f.r.Read(p)
f.remain -= n
return n, err
}
// boundedReader is an io.Reader that yields at most remain bytes from r
// before returning an error. Call reset to raise the budget back to max,
// typically at a new message boundary.
//
// Used to cap the decoded size of a single map response so a malicious
// server can't send a small zstd frame that explodes into gigabytes of
// junk for the json.Decoder to consume.
type boundedReader struct {
r io.Reader
max int64
remain int64
}
func (b *boundedReader) Read(p []byte) (int, error) {
if b.remain <= 0 {
return 0, fmt.Errorf("map response decoded size exceeds max %d", b.max)
}
if int64(len(p)) > b.remain {
p = p[:b.remain]
}
n, err := b.r.Read(p)
b.remain -= int64(n)
return n, err
}
func (b *boundedReader) reset() { b.remain = b.max }
// MapSession wraps an in-progress map response stream. Call Next to read
// each MapResponse. Call Close when done.
type MapSession struct {
res *http.Response
stream bool
noiseDoer func(*http.Request) (*http.Response, error)
// inNext detects concurrent NextInto callers. It CAS-flips
// false→true on entry and back to false on exit; a failed CAS
// panics, akin to how the Go runtime detects concurrent map
// access. It does not serialize Close vs. NextInto; that's
// nextMu's job.
inNext atomic.Bool
// nextMu is held while [MapSession.NextInto] is running jdec.Decode,
// so that Close can wait for an in-flight Decode to unwind before it
// touches zdec (Reset, pool-Put) and avoids racing with the running
// Read chain that Decode drives.
nextMu sync.Mutex
read int // guarded by nextMu
closed bool // guarded by nextMu
zdec *zstd.Decoder // reads from a framedReader wrapping res.Body
jdec *json.Decoder // reads decompressed JSON from zdec
closeOnce sync.Once
closeErr error
}
// NoiseRoundTrip sends an HTTP request over the Noise channel used by this map session.
func (s *MapSession) NoiseRoundTrip(req *http.Request) (*http.Response, error) {
return s.noiseDoer(req)
}
// Next reads and returns the next MapResponse from the stream.
// For non-streaming sessions, the first call returns the single response
// and subsequent calls return io.EOF.
// For streaming sessions, Next blocks until the next response arrives
// or the server closes the connection.
//
// Each call allocates a fresh MapResponse. Callers that want to amortize
// the allocation across calls can use [MapSession.NextInto].
//
// Next and NextInto are not safe to call concurrently from multiple
// goroutines on the same [MapSession]; a concurrent call panics, akin
// to the Go runtime's concurrent map access detection. [MapSession.Close]
// may be called concurrently to abort an in-flight Next.
func (s *MapSession) Next() (*tailcfg.MapResponse, error) {
var resp tailcfg.MapResponse
if err := s.NextInto(&resp); err != nil {
return nil, err
}
return &resp, nil
}
// NextInto is like [MapSession.Next] but decodes the next MapResponse into
// the caller-supplied *resp rather than allocating a new one. The pointer's
// pointee is zeroed before decoding so fields from a prior response do not
// persist.
//
// For non-streaming sessions, the first call decodes the single response
// and subsequent calls return io.EOF.
// For streaming sessions, NextInto blocks until the next response arrives
// or the server closes the connection.
//
// See [MapSession.Next] for concurrency rules; those apply to NextInto too.
func (s *MapSession) NextInto(resp *tailcfg.MapResponse) error {
if !s.inNext.CompareAndSwap(false, true) {
panic("tsp: invalid concurrent call to MapSession.Next/NextInto")
}
defer s.inNext.Store(false)
s.nextMu.Lock()
defer s.nextMu.Unlock()
if s.closed {
return errSessionClosed
}
if !s.stream && s.read > 0 {
return io.EOF
}
*resp = tailcfg.MapResponse{}
if err := s.jdec.Decode(resp); err != nil {
return err
}
s.read++
return nil
}
// Close returns the session's zstd decoder to the pool and closes the
// underlying HTTP response body. It is safe to call Close multiple times
// and from multiple goroutines, including while a [MapSession.Next] or
// [MapSession.NextInto] call is in flight on another goroutine (which
// will return an error once the body close propagates).
func (s *MapSession) Close() error {
// Callers are likely to race a deferred Close with a time.AfterFunc
// timeout (or similar) Close that aborts a hung Next. Without the
// Once, both Closes would Put the same *zstd.Decoder into the pool,
// corrupting it, and the Reset/Put in one would race with the
// zdec.Read that the hung Next is driving.
//
// Ordering inside the Once: close the body first to unblock any
// in-flight NextInto (its Read chain ends at res.Body and will
// return an error once it's closed). That lets NextInto unwind and
// release nextMu. Only then do we take nextMu ourselves and touch
// zdec, which is safe because no goroutine is still reading from
// it. Acquiring nextMu before closing the body would deadlock
// against a hung NextInto.
s.closeOnce.Do(func() {
s.closeErr = s.res.Body.Close()
s.nextMu.Lock()
defer s.nextMu.Unlock()
s.closed = true
s.zdec.Reset(nil)
zstdDecoderPool.Put(s.zdec)
})
return s.closeErr
}
// SendMapUpdateOpts contains options for [Client.SendMapUpdate].
type SendMapUpdateOpts struct {
// NodeKey is the node's private key. Required.
NodeKey key.NodePrivate
// DiscoKey, if non-zero, is the node's disco public key.
// Peers use it to verify disco pings from this node, which is
// what enables direct (non-DERP) paths.
DiscoKey key.DiscoPublic
// Hostinfo is the host information to send. Optional;
// if nil, a minimal default is used.
Hostinfo *tailcfg.Hostinfo
}
// SendMapUpdate sends a one-shot, non-streaming MapRequest to push small
// updates (such as the node's endpoints, hostinfo, or disco public key) to the
// coordination server without starting or disturbing a streaming map session.
func (c *Client) SendMapUpdate(ctx context.Context, opts SendMapUpdateOpts) error {
if opts.NodeKey.IsZero() {
return fmt.Errorf("NodeKey is required")
}
hi := opts.Hostinfo
if hi == nil {
hi = defaultHostinfo()
}
mapReq := tailcfg.MapRequest{
Version: tailcfg.CurrentCapabilityVersion,
NodeKey: opts.NodeKey.Public(),
DiscoKey: opts.DiscoKey,
Hostinfo: hi,
Compress: "zstd",
// A lite update that lets the server persist our state without breaking
// any existing streaming map session. See the [tailcfg.MapResponse]
// OmitPeers docs.
OmitPeers: true,
Stream: false,
ReadOnly: false,
}
body, err := json.Marshal(mapReq)
if err != nil {
return fmt.Errorf("encoding map request: %w", err)
}
nc, err := c.noiseClient(ctx)
if err != nil {
return fmt.Errorf("establishing noise connection: %w", err)
}
url := c.serverURL + "/machine/map"
url = strings.Replace(url, "http:", "https:", 1)
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("creating map request: %w", err)
}
ts2021.AddLBHeader(req, opts.NodeKey.Public())
res, err := nc.Do(req)
if err != nil {
return fmt.Errorf("map request: %w", err)
}
defer res.Body.Close()
if res.StatusCode != 200 {
msg, _ := io.ReadAll(res.Body)
return fmt.Errorf("map request: http %d: %.200s",
res.StatusCode, strings.TrimSpace(string(msg)))
}
io.Copy(io.Discard, res.Body)
return nil
}
// Map sends a map request to the coordination server and returns a MapSession
// for reading the framed, zstd-compressed response(s).
func (c *Client) Map(ctx context.Context, opts MapOpts) (*MapSession, error) {
if opts.NodeKey.IsZero() {
return nil, fmt.Errorf("NodeKey is required")
}
hi := opts.Hostinfo
if hi == nil {
hi = defaultHostinfo()
}
mapReq := tailcfg.MapRequest{
Version: tailcfg.CurrentCapabilityVersion,
NodeKey: opts.NodeKey.Public(),
Hostinfo: hi,
Stream: opts.Stream,
Compress: "zstd",
OmitPeers: opts.OmitPeers,
// Streaming requires the server to track us as "connected",
// which in turn requires ReadOnly=false. Non-streaming polls
// stay ReadOnly to minimize side effects.
ReadOnly: !opts.Stream,
}
body, err := json.Marshal(mapReq)
if err != nil {
return nil, fmt.Errorf("encoding map request: %w", err)
}
nc, err := c.noiseClient(ctx)
if err != nil {
return nil, fmt.Errorf("establishing noise connection: %w", err)
}
url := c.serverURL + "/machine/map"
url = strings.Replace(url, "http:", "https:", 1)
// The response body is a stream of framed messages, each capped by
// maxMessageSize below, but a streaming session can carry an unbounded
// number of them over one body, so it must not be subject to
// ts2021.Client.Do's default response size cap.
ctx = httpbody.WithMaxSize(ctx, 0)
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("creating map request: %w", err)
}
ts2021.AddLBHeader(req, opts.NodeKey.Public())
res, err := nc.Do(req)
if err != nil {
return nil, fmt.Errorf("map request: %w", err)
}
if res.StatusCode != 200 {
// The body is an error message rather than a message stream, so
// cap it: the map body itself is uncapped per the comment above.
httpbody.LimitSizeTo(res, httpbody.DefaultMaxSize)
msg, _ := io.ReadAll(res.Body)
res.Body.Close()
return nil, fmt.Errorf("map request: http %d: %.200s",
res.StatusCode, strings.TrimSpace(string(msg)))
}
maxMessageSize := cmp.Or(opts.MaxMessageSize, DefaultMaxMessageSize)
bounded := &boundedReader{max: maxMessageSize, remain: maxMessageSize}
fr := &framedReader{
r: res.Body,
maxSize: maxMessageSize,
onNewFrame: bounded.reset,
}
zdec, _ := zstdDecoderPool.Get().(*zstd.Decoder)
if zdec != nil {
if err := zdec.Reset(fr); err != nil {
// Reset can fail if the previous stream is in a bad state; drop
// the decoder and create a fresh one.
zdec = nil
}
}
if zdec == nil {
zdec, err = zstd.NewReader(fr, zstd.WithDecoderConcurrency(1))
if err != nil {
res.Body.Close()
return nil, fmt.Errorf("creating zstd decoder: %w", err)
}
}
bounded.r = zdec
return &MapSession{
res: res,
stream: opts.Stream,
noiseDoer: nc.Do,
zdec: zdec,
jdec: json.NewDecoder(bounded),
}, nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package tsp
import (
"encoding/json"
"fmt"
"os"
"tailscale.com/types/key"
)
// ServerInfo identifies a coordination server by its URL and Noise public key.
type ServerInfo struct {
// URL is the base URL of the coordination server, without any path
// (e.g. "https://controlplane.tailscale.com").
//
// There is no default value; a URL must always be supplied.
URL string `json:"server_url"`
// Key is the server's Noise public key, used to establish an encrypted
// channel between the client and the coordination server.
Key key.MachinePublic `json:"server_key"`
}
// NodeFile is the JSON structure for a node credentials file. It contains
// the private keys that authenticate a node to a coordination server.
//
// Example:
//
// {
// "node_key": "privkey:...",
// "machine_key": "privkey:...",
// "server_url": "https://controlplane.tailscale.com",
// "server_key": "mkey:..."
// }
//
// Note that node and machine private keys share the same "privkey:"
// textual form; they are disambiguated by the surrounding JSON field
// names rather than by any prefix in the key itself.
type NodeFile struct {
// NodeKey is the node's WireGuard private key. The corresponding
// public key identifies this node to other peers.
NodeKey key.NodePrivate `json:"node_key"`
// MachineKey is the machine's private key. It authenticates this
// machine to the coordination server over Noise.
MachineKey key.MachinePrivate `json:"machine_key"`
ServerInfo // server_url and server_key
}
// ReadNodeFile reads and parses a node JSON file.
func ReadNodeFile(path string) (NodeFile, error) {
data, err := os.ReadFile(path)
if err != nil {
return NodeFile{}, err
}
var nf NodeFile
if err := json.Unmarshal(data, &nf); err != nil {
return NodeFile{}, fmt.Errorf("parsing node file %q: %w", path, err)
}
return nf, nil
}
// WriteNodeFile writes a node JSON file. The file is created with mode 0600.
func WriteNodeFile(path string, nf NodeFile) error {
if err := nf.Check(); err != nil {
return fmt.Errorf("invalid NodeFile: %w", err)
}
return os.WriteFile(path, nf.AsJSON(), 0600)
}
// AsJSON returns nf as a pretty-printed JSON object, terminated by a newline.
//
// It always succeeds and always returns a valid JSON object. It does not
// validate that the fields of nf are non-zero; it is the caller's
// responsibility to call [NodeFile.Check] first if they want to reject
// incomplete NodeFiles.
func (nf NodeFile) AsJSON() []byte {
out, err := json.MarshalIndent(nf, "", " ")
if err != nil {
panic(fmt.Sprintf("NodeFile.AsJSON: %v", err)) // unreachable: all fields marshal successfully
}
return append(out, '\n')
}
// Check reports whether nf has all required fields set.
// It returns an error describing the first zero-valued field, if any.
func (nf NodeFile) Check() error {
if nf.NodeKey.IsZero() {
return fmt.Errorf("node_key is missing")
}
if nf.MachineKey.IsZero() {
return fmt.Errorf("machine_key is missing")
}
if nf.URL == "" {
return fmt.Errorf("server_url is missing")
}
if nf.ServerInfo.Key.IsZero() {
return fmt.Errorf("server_key is missing")
}
return nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package tsp
import (
"bytes"
"cmp"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"tailscale.com/control/ts2021"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
// RegisterOpts contains options for registering a node.
type RegisterOpts struct {
// NodeKey is the node's private key. Required.
NodeKey key.NodePrivate
// Hostinfo is the host information to send. Optional;
// if nil, a minimal default is used.
Hostinfo *tailcfg.Hostinfo
// Ephemeral marks the node as ephemeral.
Ephemeral bool
// AuthKey is a pre-authorized auth key.
AuthKey string
// Tags is a list of ACL tags to request.
Tags []string
// 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
// MaxResponseSize is the maximum size in bytes of the register
// response body. If zero, [DefaultMaxMessageSize] is used.
MaxResponseSize int64
}
// Register sends a registration request to the coordination server
// and returns the response.
func (c *Client) Register(ctx context.Context, opts RegisterOpts) (*tailcfg.RegisterResponse, error) {
hi := opts.Hostinfo
if hi == nil {
hi = defaultHostinfo()
}
if len(opts.Tags) > 0 {
hi.RequestTags = opts.Tags
}
regReq := tailcfg.RegisterRequest{
Version: tailcfg.CurrentCapabilityVersion,
NodeKey: opts.NodeKey.Public(),
Expiry: opts.Expiry,
Hostinfo: hi,
Ephemeral: opts.Ephemeral,
}
if opts.AuthKey != "" {
regReq.Auth = &tailcfg.RegisterResponseAuth{
AuthKey: opts.AuthKey,
}
}
body, err := json.Marshal(regReq)
if err != nil {
return nil, fmt.Errorf("encoding register request: %w", err)
}
nc, err := c.noiseClient(ctx)
if err != nil {
return nil, fmt.Errorf("establishing noise connection: %w", err)
}
url := c.serverURL + "/machine/register"
url = strings.Replace(url, "http:", "https:", 1)
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("creating register request: %w", err)
}
ts2021.AddLBHeader(req, opts.NodeKey.Public())
res, err := nc.Do(req)
if err != nil {
return nil, fmt.Errorf("register request: %w", err)
}
defer res.Body.Close()
maxResponseSize := cmp.Or(opts.MaxResponseSize, DefaultMaxMessageSize)
if res.StatusCode != 200 {
msg, _ := io.ReadAll(io.LimitReader(res.Body, maxResponseSize))
return nil, fmt.Errorf("register request: http %d: %.200s",
res.StatusCode, strings.TrimSpace(string(msg)))
}
// Read up to maxResponseSize+1 so we can distinguish "exactly at cap" from
// "over the cap" rather than relying on a truncated json parse error.
data, err := io.ReadAll(io.LimitReader(res.Body, maxResponseSize+1))
if err != nil {
return nil, fmt.Errorf("reading register response: %w", err)
}
if int64(len(data)) > maxResponseSize {
return nil, fmt.Errorf("register response exceeds max %d", maxResponseSize)
}
var resp tailcfg.RegisterResponse
if err := json.Unmarshal(data, &resp); err != nil {
return nil, fmt.Errorf("decoding register response: %w", err)
}
if resp.Error != "" {
return nil, fmt.Errorf("register: %s", resp.Error)
}
return &resp, nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package tsp provides a client for speaking the Tailscale protocol
// to a coordination server over Noise.
package tsp
import (
"bufio"
"bytes"
"cmp"
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"strconv"
"sync"
"tailscale.com/control/ts2021"
"tailscale.com/health"
"tailscale.com/ipn"
"tailscale.com/net/dnscache"
"tailscale.com/net/tsdial"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/version"
)
// DefaultServerURL is the default coordination server base URL,
// used when ClientOpts.ServerURL is empty.
const DefaultServerURL = ipn.DefaultControlURL
// ClientOpts contains options for creating a new Client.
type ClientOpts struct {
// ServerURL is the base URL of the coordination server
// (e.g. "https://controlplane.tailscale.com").
// If empty, DefaultServerURL is used.
ServerURL string
// MachineKey is this node's machine private key. Required.
MachineKey key.MachinePrivate
// Logf is the log function. If nil, logger.Discard is used.
Logf logger.Logf
// HealthTracker, if non-nil, is the health tracker passed through
// to the underlying noise client. May be nil.
HealthTracker *health.Tracker
}
// Client is a Tailscale protocol client that speaks to a coordination
// server over Noise.
type Client struct {
opts ClientOpts
serverURL string
logf logger.Logf
mu sync.Mutex
nc *ts2021.Client // nil until noiseClient called
serverPub key.MachinePublic // zero until set or discovered
}
// NewClient creates a new Client configured to talk to the coordination server
// specified in opts. It performs no I/O; the server's public key is discovered
// lazily on first use or can be set explicitly via SetControlPublicKey.
func NewClient(opts ClientOpts) (*Client, error) {
if opts.MachineKey.IsZero() {
return nil, fmt.Errorf("MachineKey is required")
}
logf := opts.Logf
if logf == nil {
logf = logger.Discard
}
return &Client{
opts: opts,
serverURL: cmp.Or(opts.ServerURL, DefaultServerURL),
logf: logf,
}, nil
}
// SetControlPublicKey sets the server's public key, bypassing lazy discovery.
// Any existing noise client is invalidated and will be re-created on next use.
func (c *Client) SetControlPublicKey(k key.MachinePublic) {
c.mu.Lock()
defer c.mu.Unlock()
c.serverPub = k
c.nc = nil
}
// DiscoverServerKey fetches the server's public key from the coordination
// server and stores it for subsequent use. Any existing noise client is
// invalidated.
func (c *Client) DiscoverServerKey(ctx context.Context) (key.MachinePublic, error) {
k, err := DiscoverServerKey(ctx, c.serverURL)
if err != nil {
return key.MachinePublic{}, err
}
c.mu.Lock()
defer c.mu.Unlock()
c.serverPub = k
c.nc = nil
return k, nil
}
// DiscoverServerKey fetches the coordination server's public key from the
// given server URL. It is a standalone function that requires no client state.
func DiscoverServerKey(ctx context.Context, serverURL string) (key.MachinePublic, error) {
serverURL = cmp.Or(serverURL, DefaultServerURL)
keysURL := serverURL + "/key?v=" + strconv.Itoa(int(tailcfg.CurrentCapabilityVersion))
req, err := http.NewRequestWithContext(ctx, "GET", keysURL, nil)
if err != nil {
return key.MachinePublic{}, fmt.Errorf("creating key request: %w", err)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return key.MachinePublic{}, fmt.Errorf("fetching server key: %w", err)
}
defer res.Body.Close()
if res.StatusCode != 200 {
return key.MachinePublic{}, fmt.Errorf("fetching server key: %s", res.Status)
}
var keys struct {
PublicKey key.MachinePublic
}
if err := json.NewDecoder(res.Body).Decode(&keys); err != nil {
return key.MachinePublic{}, fmt.Errorf("decoding server key: %w", err)
}
return keys.PublicKey, nil
}
// noiseClient returns the ts2021 noise client, creating it lazily if needed.
// If the server's public key is not yet known, it is discovered via HTTP.
func (c *Client) noiseClient(ctx context.Context) (*ts2021.Client, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.nc != nil {
return c.nc, nil
}
if c.serverPub.IsZero() {
// Discover server key without holding the lock, to avoid blocking
// other callers during the HTTP request.
c.mu.Unlock()
k, err := DiscoverServerKey(ctx, c.serverURL)
c.mu.Lock()
if err != nil {
return nil, err
}
// Re-check: another goroutine may have set it while we were unlocked.
if c.serverPub.IsZero() {
c.serverPub = k
}
// If nc was created by another goroutine while unlocked, use it.
if c.nc != nil {
return c.nc, nil
}
}
nc, err := ts2021.NewClient(ts2021.ClientOpts{
ServerURL: c.serverURL,
PrivKey: c.opts.MachineKey,
ServerPubKey: c.serverPub,
Dialer: tsdial.NewFromFuncForDebug(c.logf, (&net.Dialer{}).DialContext),
// tsp is a protocol library and does not know about the system's
// routing table or interfaces, so it deliberately provides a plain
// resolver with no LookupIPFallback. Without one, ts2021 would build
// a resolver that falls back to bootstrap DNS over DERP when the
// first dial fails, which is a tailscaled concern and not ours.
DNSCache: &dnscache.Resolver{
Logf: c.logf,
},
Logf: c.logf,
HealthTracker: c.opts.HealthTracker,
})
if err != nil {
return nil, fmt.Errorf("creating noise client: %w", err)
}
c.nc = nc
return nc, nil
}
// AnswerC2NPing handles a c2n PingRequest from the control plane by parsing the
// embedded HTTP request in the payload, routing it locally, and POSTing the HTTP
// response back to pr.URL using doNoiseRequest. The POST is done in a new
// goroutine so this method does not block.
//
// It reports whether the ping was handled. Unhandled pings (nil pr, non-c2n
// types, or unrecognized c2n paths) return false.
func (c *Client) AnswerC2NPing(ctx context.Context, pr *tailcfg.PingRequest, doNoiseRequest func(*http.Request) (*http.Response, error)) (handled bool) {
if pr == nil || pr.Types != "c2n" {
return false
}
// Parse the HTTP request from the payload.
httpReq, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(pr.Payload)))
if err != nil {
c.logf("parsing c2n ping payload: %v", err)
return false
}
// Route the request locally.
var httpResp *http.Response
switch httpReq.URL.Path {
case "/echo":
body, _ := io.ReadAll(httpReq.Body)
httpResp = &http.Response{
StatusCode: 200,
Status: "200 OK",
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: http.Header{},
Body: io.NopCloser(bytes.NewReader(body)),
ContentLength: int64(len(body)),
}
default:
c.logf("ignoring c2n ping request for unhandled path %q", httpReq.URL.Path)
return false
}
// Serialize the HTTP response.
var buf bytes.Buffer
if err := httpResp.Write(&buf); err != nil {
c.logf("serializing c2n ping response: %v", err)
return false
}
// Send the response back to the control plane over the Noise channel.
go func() {
req, err := http.NewRequestWithContext(ctx, "POST", pr.URL, &buf)
if err != nil {
c.logf("creating c2n ping reply request: %v", err)
return
}
resp, err := doNoiseRequest(req)
if err != nil {
c.logf("sending c2n ping reply: %v", err)
return
}
resp.Body.Close()
}()
return true
}
// Close closes the client and releases resources.
func (c *Client) Close() error {
c.mu.Lock()
nc := c.nc
c.nc = nil
c.mu.Unlock()
if nc != nil {
nc.Close()
}
return nil
}
func defaultHostinfo() *tailcfg.Hostinfo {
return &tailcfg.Hostinfo{
OS: version.OS(),
IPNVersion: version.Long(),
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package derp implements the Designated Encrypted Relay for Packets (DERP)
// protocol.
//
// DERP routes packets to clients using curve25519 keys as addresses.
//
// DERP is used by Tailscale nodes to proxy encrypted WireGuard
// packets through the Tailscale cloud servers when a direct path
// cannot be found or opened. DERP is a last resort. Both sides
// between very aggressive NATs, firewalls, no IPv6, etc? Well, DERP.
package derp
import (
"bufio"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"time"
)
// MaxPacketSize is the maximum size of a packet sent over DERP.
// (This only includes the data bytes visible to magicsock, not
// including its on-wire framing overhead)
const MaxPacketSize = 64 << 10
// Magic is the DERP Magic number, sent in the FrameServerKey frame
// upon initial connection.
const Magic = "DERP🔑" // 8 bytes: 0x44 45 52 50 f0 9f 94 91
const (
NonceLen = 24
FrameHeaderLen = 1 + 4 // frameType byte + 4 byte length
KeyLen = 32
MaxInfoLen = 1 << 20
)
// KeepAlive is the minimum frequency at which the DERP server sends
// keep alive frames. The server adds some jitter, so this timing is not
// exact, but 2x this value can be considered a missed keep alive.
const KeepAlive = 60 * time.Second
// ProtocolVersion is bumped whenever there's a wire-incompatible change.
// - version 1 (zero on wire): consistent box headers, in use by employee dev nodes a bit
// - version 2: received packets have src addrs in frameRecvPacket at beginning
const ProtocolVersion = 2
// FrameType is the one byte frame type at the beginning of the frame
// header. The second field is a big-endian uint32 describing the
// length of the remaining frame (not including the initial 5 bytes).
type FrameType byte
/*
Protocol flow:
Login:
* client connects
* server sends FrameServerKey
* client sends FrameClientInfo
* server sends FrameServerInfo
Steady state:
* server occasionally sends FrameKeepAlive (or FramePing)
* client responds to any FramePing with a FramePong
* client sends FrameSendPacket
* server then sends FrameRecvPacket to recipient
*/
const (
FrameServerKey = FrameType(0x01) // 8B magic + 32B public key + (0+ bytes future use)
FrameClientInfo = FrameType(0x02) // 32B pub key + 24B nonce + naclbox(json)
FrameServerInfo = FrameType(0x03) // 24B nonce + naclbox(json)
FrameSendPacket = FrameType(0x04) // 32B dest pub key + packet bytes
FrameForwardPacket = FrameType(0x0a) // 32B src pub key + 32B dst pub key + packet bytes
FrameRecvPacket = FrameType(0x05) // v0/1: packet bytes, v2: 32B src pub key + packet bytes
FrameKeepAlive = FrameType(0x06) // no payload, no-op (to be replaced with ping/pong)
FrameNotePreferred = FrameType(0x07) // 1 byte payload: 0x01 or 0x00 for whether this is client's home node
// FramePeerGone is sent from server to client to signal that
// a previous sender is no longer connected. That is, if A
// sent to B, and then if A disconnects, the server sends
// FramePeerGone to B so B can forget that a reverse path
// exists on that connection to get back to A. It is also sent
// if A tries to send a CallMeMaybe to B and the server has no
// record of B
FramePeerGone = FrameType(0x08) // 32B pub key of peer that's gone + 1 byte reason
// FramePeerPresent is like FramePeerGone, but for other members of the DERP
// region when they're meshed up together.
//
// The message is at least 32 bytes (the public key of the peer that's
// connected). If there are at least 18 bytes remaining after that, it's the
// 16 byte IP + 2 byte BE uint16 port of the client. If there's another byte
// remaining after that, it's a PeerPresentFlags byte. If there's another
// byte remaining after that, it's the length of the client's advertised
// app name (see ClientInfo.AppName), followed by that many bytes of app
// name.
// While current servers send at least 42 bytes, old servers will send
// fewer, and newer servers might send more.
FramePeerPresent = FrameType(0x09)
// FrameWatchConns is how one DERP node in a regional mesh
// subscribes to the others in the region.
// There's no payload. If the sender doesn't have permission, the connection
// is closed. Otherwise, the client is initially flooded with
// FramePeerPresent for all connected nodes, and then a stream of
// FramePeerPresent & FramePeerGone has peers connect and disconnect.
FrameWatchConns = FrameType(0x10)
// FrameClosePeer is a privileged frame type (requires the
// mesh key for now) that closes the provided peer's
// connection. (To be used for cluster load balancing
// purposes, when clients end up on a non-ideal node)
FrameClosePeer = FrameType(0x11) // 32B pub key of peer to close.
FramePing = FrameType(0x12) // 8 byte ping payload, to be echoed back in framePong
FramePong = FrameType(0x13) // 8 byte payload, the contents of the ping being replied to
// FrameHealth is sent from server to client to tell the client
// if their connection is unhealthy somehow. Currently the only unhealthy state
// is whether the connection is detected as a duplicate.
// The entire frame body is the text of the error message. An empty message
// clears the error state.
FrameHealth = FrameType(0x14)
// FrameRestarting is sent from server to client for the
// server to declare that it's restarting. Payload is two big
// endian uint32 durations in milliseconds: when to reconnect,
// and how long to try total. See ServerRestartingMessage docs for
// more details on how the client should interpret them.
FrameRestarting = FrameType(0x15)
)
// PeerGoneReasonType is a one byte reason code explaining why a
// server does not have a path to the requested destination.
type PeerGoneReasonType byte
const (
PeerGoneReasonDisconnected = PeerGoneReasonType(0x00) // is only sent when a peer disconnects from this server
PeerGoneReasonNotHere = PeerGoneReasonType(0x01) // server doesn't know about this peer
PeerGoneReasonMeshConnBroke = PeerGoneReasonType(0xf0) // invented by Client.RunWatchConnectionLoop on disconnect; not sent on the wire
)
// PeerPresentFlags is an optional byte of bit flags sent after a FramePeerPresent message.
//
// For a modern server, the value should always be non-zero. If the value is zero,
// that means the server doesn't support this field.
type PeerPresentFlags byte
// PeerPresentFlags bits.
const (
PeerPresentIsRegular = 1 << 0
PeerPresentIsMeshPeer = 1 << 1
PeerPresentIsProber = 1 << 2
PeerPresentNotIdeal = 1 << 3 // client said derp server is not its Region.Nodes[0] ideal node
)
// IdealNodeHeader is the HTTP request header sent on DERP HTTP client requests
// to indicate that they're connecting to their ideal (Region.Nodes[0]) node.
// The HTTP header value is the name of the node they wish they were connected
// to. This is an optional header.
const IdealNodeHeader = "Ideal-Node"
// FastStartHeader is the header (with value "1") that signals to the HTTP
// server that the DERP HTTP client does not want the HTTP 101 response
// headers and it will begin writing & reading the DERP protocol immediately
// following its HTTP request.
const FastStartHeader = "Derp-Fast-Start"
var bin = binary.BigEndian
// writeUint32 writes v to bw one byte at a time
// as a big-endian uint32.
func writeUint32(bw *bufio.Writer, v uint32) error {
var b [4]byte
bin.PutUint32(b[:], v)
// Writing a byte at a time is a bit silly,
// but it causes b not to escape,
// which more than pays for the silliness.
for _, c := range &b {
err := bw.WriteByte(c)
if err != nil {
return err
}
}
return nil
}
// ReadFrameTypeHeader reads a frame header from br and
// verifies that the frame type matches wantType.
//
// If it does, it returns the frame length (not including
// the 5 byte header) and a nil error.
//
// If it doesn't, it returns an error and a zero length.
func ReadFrameTypeHeader(br *bufio.Reader, wantType FrameType) (frameLen uint32, err error) {
gotType, frameLen, err := ReadFrameHeader(br)
if err == nil && wantType != gotType {
err = fmt.Errorf("bad frame type 0x%X, want 0x%X", gotType, wantType)
}
return frameLen, err
}
// ReadFrameHeader reads a DERP frame header ([FrameHeaderLen] bytes) from br.
// It uses Peek+Discard to read directly from bufio's internal buffer
// without copying or allocating.
func ReadFrameHeader(br *bufio.Reader) (t FrameType, frameLen uint32, err error) {
hdr, err := br.Peek(FrameHeaderLen)
if err != nil {
return 0, 0, err
}
defer br.Discard(FrameHeaderLen)
return FrameType(hdr[0]), bin.Uint32(hdr[1:FrameHeaderLen]), nil
}
// readFrame reads a frame header and then reads its payload into
// b[:frameLen].
//
// If the frame header length is greater than maxSize, readFrame returns
// an error after reading the frame header.
//
// If the frame is less than maxSize but greater than len(b), len(b)
// bytes are read, err will be io.ErrShortBuffer, and frameLen and t
// will both be set. That is, callers need to explicitly handle when
// they get more data than expected.
func readFrame(br *bufio.Reader, maxSize uint32, b []byte) (t FrameType, frameLen uint32, err error) {
t, frameLen, err = ReadFrameHeader(br)
if err != nil {
return 0, 0, err
}
if frameLen > maxSize {
return 0, 0, fmt.Errorf("frame header size %d exceeds reader limit of %d", frameLen, maxSize)
}
n, err := io.ReadFull(br, b[:min(frameLen, uint32(len(b)))])
if err != nil {
return 0, 0, err
}
remain := frameLen - uint32(n)
if remain > 0 {
if _, err := io.CopyN(io.Discard, br, int64(remain)); err != nil {
return 0, 0, err
}
err = io.ErrShortBuffer
}
return t, frameLen, err
}
// WriteFrameHeader writes a DERP frame header to bw: a one-byte frame
// type followed by a big-endian uint32 frame length.
//
// It uses AvailableBuffer to append the header directly into bufio's
// internal buffer without allocation, falling back to WriteByte when
// the buffer has insufficient space.
// It does not flush bw.
func WriteFrameHeader(bw *bufio.Writer, t FrameType, frameLen uint32) error {
// Fast path: enough space in the buffer to append the header
// directly without allocation via AvailableBuffer.
if bw.Available() >= FrameHeaderLen {
buf := bw.AvailableBuffer()
buf = append(buf, byte(t))
buf = bin.AppendUint32(buf, frameLen)
_, 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.
if err := bw.WriteByte(byte(t)); err != nil {
return err
}
return writeUint32(bw, frameLen)
}
// WriteFrame writes a complete frame & flushes it.
func WriteFrame(bw *bufio.Writer, t FrameType, b []byte) error {
if len(b) > 10<<20 {
return errors.New("unreasonably large frame write")
}
if err := WriteFrameHeader(bw, t, uint32(len(b))); err != nil {
return err
}
if _, err := bw.Write(b); err != nil {
return err
}
return bw.Flush()
}
// Conn is the subset of the underlying net.Conn the DERP Server needs.
// It is a defined type so that non-net connections can be used.
type Conn interface {
io.WriteCloser
LocalAddr() net.Addr
// The *Deadline methods follow the semantics of net.Conn.
SetDeadline(time.Time) error
SetReadDeadline(time.Time) error
SetWriteDeadline(time.Time) error
}
// ServerInfo is the message sent from the server to clients during
// the connection setup.
type ServerInfo struct {
Version int `json:"version,omitempty"`
TokenBucketBytesPerSecond int `json:",omitempty"`
TokenBucketBytesBurst int `json:",omitempty"`
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package derp
import (
"bufio"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"net/netip"
"sync"
"time"
"go4.org/mem"
"golang.org/x/time/rate"
"tailscale.com/syncs"
"tailscale.com/tstime"
"tailscale.com/types/key"
"tailscale.com/types/logger"
)
// Client is a DERP client.
type Client struct {
serverKey key.NodePublic // of the DERP server; not a machine or node key
privateKey key.NodePrivate
publicKey key.NodePublic // of privateKey
logf logger.Logf
nc Conn
br *bufio.Reader
meshKey key.DERPMesh
canAckPings bool
isProber bool
appName string
wmu sync.Mutex // hold while writing to bw
bw *bufio.Writer
rate *rate.Limiter // if non-nil, rate limiter to use
// Owned by Recv:
peeked int // bytes to discard on next Recv
readErr syncs.AtomicValue[error] // sticky (set by Recv)
clock tstime.Clock
}
// ClientOpt is an option passed to NewClient.
type ClientOpt interface {
update(*clientOpt)
}
type clientOptFunc func(*clientOpt)
func (f clientOptFunc) update(o *clientOpt) { f(o) }
// clientOpt are the options passed to newClient.
type clientOpt struct {
MeshKey key.DERPMesh
ServerPub key.NodePublic
CanAckPings bool
IsProber bool
AppName string
}
// MeshKey returns a ClientOpt to pass to the DERP server during connect to get
// access to join the mesh.
//
// An empty key means to not use a mesh key.
func MeshKey(k key.DERPMesh) ClientOpt { return clientOptFunc(func(o *clientOpt) { o.MeshKey = k }) }
// IsProber returns a ClientOpt to pass to the DERP server during connect to
// declare that this client is a prober.
func IsProber(v bool) ClientOpt { return clientOptFunc(func(o *clientOpt) { o.IsProber = v }) }
// ServerPublicKey returns a ClientOpt to declare that the server's DERP public key is known.
// If key is the zero value, the returned ClientOpt is a no-op.
func ServerPublicKey(key key.NodePublic) ClientOpt {
return clientOptFunc(func(o *clientOpt) { o.ServerPub = key })
}
// CanAckPings returns a ClientOpt to set whether it advertises to the
// server that it's capable of acknowledging ping requests.
func CanAckPings(v bool) ClientOpt {
return clientOptFunc(func(o *clientOpt) { o.CanAckPings = v })
}
// AppName returns a ClientOpt to set an opaque app name string to
// advertise to the DERP server for stats purposes. It is sent to the
// server in the ClientInfo. The name must be valid per [ValidAppName]
// or NewClient returns an error.
func AppName(name string) ClientOpt {
return clientOptFunc(func(o *clientOpt) { o.AppName = name })
}
// MaxAppNameLen is the maximum length in bytes of a [ClientInfo.AppName].
const MaxAppNameLen = 32
// ValidAppName reports whether name is a valid app name: at most
// [MaxAppNameLen] bytes of printable ASCII. The empty string is valid.
func ValidAppName(name string) bool {
if len(name) > MaxAppNameLen {
return false
}
for i := range len(name) {
if b := name[i]; b < ' ' || b > '~' {
return false
}
}
return true
}
func NewClient(privateKey key.NodePrivate, nc Conn, brw *bufio.ReadWriter, logf logger.Logf, opts ...ClientOpt) (*Client, error) {
var opt clientOpt
for _, o := range opts {
if o == nil {
return nil, errors.New("nil ClientOpt")
}
o.update(&opt)
}
if !ValidAppName(opt.AppName) {
return nil, fmt.Errorf("invalid AppName %.40q", opt.AppName)
}
return newClient(privateKey, nc, brw, logf, opt)
}
func newClient(privateKey key.NodePrivate, nc Conn, brw *bufio.ReadWriter, logf logger.Logf, opt clientOpt) (*Client, error) {
c := &Client{
privateKey: privateKey,
publicKey: privateKey.Public(),
logf: logf,
nc: nc,
br: brw.Reader,
bw: brw.Writer,
meshKey: opt.MeshKey,
canAckPings: opt.CanAckPings,
isProber: opt.IsProber,
appName: opt.AppName,
clock: tstime.StdClock{},
}
if opt.ServerPub.IsZero() {
if err := c.recvServerKey(); err != nil {
return nil, fmt.Errorf("derp.Client: failed to receive server key: %v", err)
}
} else {
c.serverKey = opt.ServerPub
}
if err := c.sendClientKey(); err != nil {
return nil, fmt.Errorf("derp.Client: failed to send client key: %v", err)
}
return c, nil
}
func (c *Client) PublicKey() key.NodePublic { return c.publicKey }
func (c *Client) recvServerKey() error {
var buf [40]byte
t, flen, err := readFrame(c.br, 1<<10, buf[:])
if err == io.ErrShortBuffer {
// For future-proofing, allow server to send more in its greeting.
err = nil
}
if err != nil {
return err
}
if flen < uint32(len(buf)) || t != FrameServerKey || string(buf[:len(Magic)]) != Magic {
return errors.New("invalid server greeting")
}
c.serverKey = key.NodePublicFromRaw32(mem.B(buf[len(Magic):]))
return nil
}
func (c *Client) parseServerInfo(b []byte) (*ServerInfo, error) {
const maxLength = NonceLen + MaxInfoLen
fl := len(b)
if fl < NonceLen {
return nil, fmt.Errorf("short serverInfo frame")
}
if fl > maxLength {
return nil, fmt.Errorf("long serverInfo frame")
}
msg, ok := c.privateKey.OpenFrom(c.serverKey, b)
if !ok {
return nil, fmt.Errorf("failed to open naclbox from server key %s", c.serverKey)
}
info := new(ServerInfo)
if err := json.Unmarshal(msg, info); err != nil {
return nil, fmt.Errorf("invalid JSON: %v", err)
}
return info, nil
}
// ClientInfo is the information a DERP client sends to the server
// about itself when it connects.
type ClientInfo struct {
// MeshKey optionally specifies a pre-shared key used by
// trusted clients. It's required to subscribe to the
// connection list & forward packets. It's empty for regular
// users.
MeshKey key.DERPMesh `json:"meshKey,omitzero"`
// Version is the DERP protocol version that the client was built with.
// See the ProtocolVersion const.
Version int `json:"version,omitempty"`
// CanAckPings is whether the client declares it's able to ack
// pings.
CanAckPings bool
// IsProber is whether this client is a prober.
IsProber bool `json:",omitempty"`
// AppName is an optional opaque app name string the client
// advertises to the server for stats purposes. It must be
// valid per [ValidAppName] or the server rejects the
// connection.
AppName string `json:",omitempty"`
}
// Equal reports if two clientInfo values are equal.
func (c *ClientInfo) Equal(other *ClientInfo) bool {
if c == nil || other == nil {
return c == other
}
if c.Version != other.Version || c.CanAckPings != other.CanAckPings || c.IsProber != other.IsProber || c.AppName != other.AppName {
return false
}
return c.MeshKey.Equal(other.MeshKey)
}
func (c *Client) sendClientKey() error {
msg, err := json.Marshal(ClientInfo{
Version: ProtocolVersion,
MeshKey: c.meshKey,
CanAckPings: c.canAckPings,
IsProber: c.isProber,
AppName: c.appName,
})
if err != nil {
return err
}
msgbox := c.privateKey.SealTo(c.serverKey, msg)
buf := make([]byte, 0, KeyLen+len(msgbox))
buf = c.publicKey.AppendTo(buf)
buf = append(buf, msgbox...)
return WriteFrame(c.bw, FrameClientInfo, buf)
}
// ServerPublicKey returns the server's public key.
func (c *Client) ServerPublicKey() key.NodePublic { return c.serverKey }
// Send sends a packet to the Tailscale node identified by dstKey.
//
// It is an error if the packet is larger than 64KB.
func (c *Client) Send(dstKey key.NodePublic, pkt []byte) error { return c.send(dstKey, pkt) }
func (c *Client) send(dstKey key.NodePublic, pkt []byte) (ret error) {
defer func() {
if ret != nil {
ret = fmt.Errorf("derp.Send: %w", ret)
}
}()
if len(pkt) > MaxPacketSize {
return fmt.Errorf("packet too big: %d", len(pkt))
}
c.wmu.Lock()
defer c.wmu.Unlock()
if c.rate != nil {
pktLen := FrameHeaderLen + key.NodePublicRawLen + len(pkt)
if !c.rate.AllowN(c.clock.Now(), pktLen) {
return nil // drop
}
}
if err := WriteFrameHeader(c.bw, FrameSendPacket, uint32(key.NodePublicRawLen+len(pkt))); err != nil {
return err
}
if _, err := c.bw.Write(dstKey.AppendTo(nil)); err != nil {
return err
}
if _, err := c.bw.Write(pkt); err != nil {
return err
}
return c.bw.Flush()
}
// ForwardPacket sends a forward packet frame carrying pkt from srcKey
// to dstKey. It writes and flushes pkt before returning and does not
// retain it.
func (c *Client) ForwardPacket(srcKey, dstKey key.NodePublic, pkt LoanedBytes) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("derp.ForwardPacket: %w", err)
}
}()
if pkt.Len() > MaxPacketSize {
return fmt.Errorf("packet too big: %d", pkt.Len())
}
c.wmu.Lock()
defer c.wmu.Unlock()
timer := c.clock.AfterFunc(5*time.Second, c.writeTimeoutFired)
defer timer.Stop()
if err := WriteFrameHeader(c.bw, FrameForwardPacket, uint32(KeyLen*2+pkt.Len())); err != nil {
return err
}
if _, err := c.bw.Write(srcKey.AppendTo(nil)); err != nil {
return err
}
if _, err := c.bw.Write(dstKey.AppendTo(nil)); err != nil {
return err
}
if _, err := pkt.WriteTo(c.bw); err != nil {
return err
}
return c.bw.Flush()
}
func (c *Client) writeTimeoutFired() { c.nc.Close() }
func (c *Client) SendPing(data [8]byte) error {
return c.sendPingOrPong(FramePing, data)
}
func (c *Client) SendPong(data [8]byte) error {
return c.sendPingOrPong(FramePong, data)
}
func (c *Client) sendPingOrPong(typ FrameType, data [8]byte) error {
c.wmu.Lock()
defer c.wmu.Unlock()
if err := WriteFrameHeader(c.bw, typ, 8); err != nil {
return err
}
if _, err := c.bw.Write(data[:]); err != nil {
return err
}
return c.bw.Flush()
}
// NotePreferred sends a packet that tells the server whether this
// client is the user's preferred server. This is only used in the
// server for stats.
func (c *Client) NotePreferred(preferred bool) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("derp.NotePreferred: %v", err)
}
}()
c.wmu.Lock()
defer c.wmu.Unlock()
if err := WriteFrameHeader(c.bw, FrameNotePreferred, 1); err != nil {
return err
}
var b byte = 0x00
if preferred {
b = 0x01
}
if err := c.bw.WriteByte(b); err != nil {
return err
}
return c.bw.Flush()
}
// WatchConnectionChanges sends a request to subscribe to the peer's connection list.
// It's a fatal error if the client wasn't created using MeshKey.
func (c *Client) WatchConnectionChanges() error {
c.wmu.Lock()
defer c.wmu.Unlock()
if err := WriteFrameHeader(c.bw, FrameWatchConns, 0); err != nil {
return err
}
return c.bw.Flush()
}
// ClosePeer asks the server to close target's TCP connection.
// It's a fatal error if the client wasn't created using MeshKey.
func (c *Client) ClosePeer(target key.NodePublic) error {
c.wmu.Lock()
defer c.wmu.Unlock()
return WriteFrame(c.bw, FrameClosePeer, target.AppendTo(nil))
}
// ReceivedMessage represents a type returned by Client.Recv. Unless
// otherwise documented, the returned message aliases the byte slice
// provided to Recv and thus the message is only as good as that
// buffer, which is up to the caller.
type ReceivedMessage interface {
msg()
}
// ReceivedPacket is a ReceivedMessage representing an incoming packet.
type ReceivedPacket struct {
Source key.NodePublic
// Data is the received packet bytes. It aliases the memory
// passed to Client.Recv.
Data []byte
}
func (ReceivedPacket) msg() {}
// PeerGoneMessage is a ReceivedMessage that indicates that the client
// identified by the underlying public key is not connected to this
// server.
//
// It has only historically been sent by the server when the client
// connection count decremented from 1 to 0 and not from e.g. 2 to 1.
// See https://github.com/tailscale/tailscale/issues/13566 for details.
type PeerGoneMessage struct {
Peer key.NodePublic
Reason PeerGoneReasonType
}
func (PeerGoneMessage) msg() {}
// PeerPresentMessage is a ReceivedMessage that indicates that the client is
// connected to the server. (Only used by trusted mesh clients)
//
// It will be sent to client watchers for every new connection from a client,
// even if the client's already connected with that public key.
// See https://github.com/tailscale/tailscale/issues/13566 for PeerPresentMessage
// and PeerGoneMessage not being 1:1.
type PeerPresentMessage struct {
// Key is the public key of the client.
Key key.NodePublic
// IPPort is the remote IP and port of the client.
IPPort netip.AddrPort
// Flags is a bitmask of info about the client.
Flags PeerPresentFlags
// AppName is the optional app name the client advertised in
// its ClientInfo, if any. It's empty if the client didn't
// send one or the server is too old to relay it.
AppName string
}
func (PeerPresentMessage) msg() {}
// ServerInfoMessage is sent by the server upon first connect.
type ServerInfoMessage struct {
// TokenBucketBytesPerSecond is how many bytes per second the
// server says it will accept, including all framing bytes.
//
// Zero means unspecified. There might be a limit, but the
// client need not try to respect it.
TokenBucketBytesPerSecond int
// TokenBucketBytesBurst is how many bytes the server will
// allow to burst, temporarily violating
// TokenBucketBytesPerSecond.
//
// Zero means unspecified. There might be a limit, but the
// client need not try to respect it.
TokenBucketBytesBurst int
}
func (ServerInfoMessage) msg() {}
// PingMessage is a request from a client or server to reply to the
// other side with a PongMessage with the given payload.
type PingMessage [8]byte
func (PingMessage) msg() {}
// PongMessage is a reply to a PingMessage from a client or server
// with the payload sent previously in a PingMessage.
type PongMessage [8]byte
func (PongMessage) msg() {}
// KeepAliveMessage is a one-way empty message from server to client, just to
// keep the connection alive. It's like a PingMessage, but doesn't solicit
// a reply from the client.
type KeepAliveMessage struct{}
func (KeepAliveMessage) msg() {}
// HealthMessage is a one-way message from server to client, declaring the
// connection health state.
type HealthMessage struct {
// Problem, if non-empty, is a description of why the connection
// is unhealthy.
//
// The empty string means the connection is healthy again.
//
// The default condition is healthy, so the server doesn't
// broadcast a HealthMessage until a problem exists.
Problem string
}
func (HealthMessage) msg() {}
// ServerRestartingMessage is a one-way message from server to client,
// advertising that the server is restarting.
type ServerRestartingMessage struct {
// ReconnectIn is an advisory duration that the client should wait
// before attempting to reconnect. It might be zero.
// It exists for the server to smear out the reconnects.
ReconnectIn time.Duration
// TryFor is an advisory duration for how long the client
// should attempt to reconnect before giving up and proceeding
// with its normal connection failure logic. The interval
// between retries is undefined for now.
// A server should not send a TryFor duration more than a few
// seconds.
TryFor time.Duration
}
func (ServerRestartingMessage) msg() {}
// Recv reads a message from the DERP server.
//
// The returned message may alias memory owned by the Client; it
// should only be accessed until the next call to Client.
//
// Once Recv returns an error, the Client is dead forever.
func (c *Client) Recv() (m ReceivedMessage, err error) {
return c.recvTimeout(120 * time.Second)
}
func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err error) {
readErr := c.readErr.Load()
if readErr != nil {
return nil, readErr
}
defer func() {
if err != nil {
err = fmt.Errorf("derp.Recv: %w", err)
c.readErr.Store(err)
}
}()
for {
c.nc.SetReadDeadline(time.Now().Add(timeout))
// Discard any peeked bytes from a previous Recv call.
if c.peeked != 0 {
if n, err := c.br.Discard(c.peeked); err != nil || n != c.peeked {
// Documented to never fail, but might as well check.
return nil, fmt.Errorf("bufio.Reader.Discard(%d bytes): got %v, %v", c.peeked, n, err)
}
c.peeked = 0
}
t, n, err := ReadFrameHeader(c.br)
if err != nil {
return nil, err
}
if n > 1<<20 {
return nil, fmt.Errorf("unexpectedly large frame of %d bytes returned", n)
}
var b []byte // frame payload (past the 5 byte header)
// If the frame fits in our bufio.Reader buffer, just use it.
// In practice it's 4KB (from derphttp.Client's bufio.NewReader(httpConn)) and
// in practive, WireGuard packets (and thus DERP frames) are under 1.5KB.
// So this is the common path.
if int(n) <= c.br.Size() {
b, err = c.br.Peek(int(n))
c.peeked = int(n)
} else {
// But if for some reason we read a large DERP message (which isn't necessarily
// a WireGuard packet), then just allocate memory for it.
// TODO(bradfitz): use a pool if large frames ever happen in practice.
b = make([]byte, n)
_, err = io.ReadFull(c.br, b)
}
if err != nil {
return nil, err
}
switch t {
default:
continue
case FrameServerInfo:
// Server sends this at start-up. Currently unused.
// Just has a JSON message saying "version: 2",
// but the protocol seems extensible enough as-is without
// needing to wait an RTT to discover the version at startup.
// We'd prefer to give the connection to the client (magicsock)
// to start writing as soon as possible.
si, err := c.parseServerInfo(b)
if err != nil {
return nil, fmt.Errorf("invalid server info frame: %v", err)
}
sm := ServerInfoMessage{
TokenBucketBytesPerSecond: si.TokenBucketBytesPerSecond,
TokenBucketBytesBurst: si.TokenBucketBytesBurst,
}
c.setSendRateLimiter(sm)
return sm, nil
case FrameKeepAlive:
// A one-way keep-alive message that doesn't require an acknowledgement.
// This predated FramePing/FramePong.
return KeepAliveMessage{}, nil
case FramePeerGone:
if n < KeyLen {
c.logf("[unexpected] dropping short peerGone frame from DERP server")
continue
}
// Backward compatibility for the older peerGone without reason byte
reason := PeerGoneReasonDisconnected
if n > KeyLen {
reason = PeerGoneReasonType(b[KeyLen])
}
pg := PeerGoneMessage{
Peer: key.NodePublicFromRaw32(mem.B(b[:KeyLen])),
Reason: reason,
}
return pg, nil
case FramePeerPresent:
remain := b
chunk, remain, ok := cutLeadingN(remain, KeyLen)
if !ok {
c.logf("[unexpected] dropping short peerPresent frame from DERP server")
continue
}
var msg PeerPresentMessage
msg.Key = key.NodePublicFromRaw32(mem.B(chunk))
const ipLen = 16
const portLen = 2
chunk, remain, ok = cutLeadingN(remain, ipLen+portLen)
if !ok {
// Older server which didn't send the IP.
return msg, nil
}
msg.IPPort = netip.AddrPortFrom(
netip.AddrFrom16([16]byte(chunk[:ipLen])).Unmap(),
binary.BigEndian.Uint16(chunk[ipLen:]),
)
chunk, remain, ok = cutLeadingN(remain, 1)
if !ok {
// Older server which doesn't send PeerPresentFlags.
return msg, nil
}
msg.Flags = PeerPresentFlags(chunk[0])
chunk, remain, ok = cutLeadingN(remain, 1)
if !ok {
// Older server which doesn't send the app name.
return msg, nil
}
nameLen := int(chunk[0])
chunk, _, ok = cutLeadingN(remain, nameLen)
if !ok {
c.logf("[unexpected] short peerPresent app name from DERP server")
return msg, nil
}
if name := string(chunk); ValidAppName(name) {
msg.AppName = name
}
return msg, nil
case FrameRecvPacket:
var rp ReceivedPacket
if n < KeyLen {
c.logf("[unexpected] dropping short packet from DERP server")
continue
}
rp.Source = key.NodePublicFromRaw32(mem.B(b[:KeyLen]))
rp.Data = b[KeyLen:n]
return rp, nil
case FramePing:
var pm PingMessage
if n < 8 {
c.logf("[unexpected] dropping short ping frame")
continue
}
copy(pm[:], b[:])
return pm, nil
case FramePong:
var pm PongMessage
if n < 8 {
c.logf("[unexpected] dropping short ping frame")
continue
}
copy(pm[:], b[:])
return pm, nil
case FrameHealth:
return HealthMessage{Problem: string(b[:])}, nil
case FrameRestarting:
var m ServerRestartingMessage
if n < 8 {
c.logf("[unexpected] dropping short server restarting frame")
continue
}
m.ReconnectIn = time.Duration(binary.BigEndian.Uint32(b[0:4])) * time.Millisecond
m.TryFor = time.Duration(binary.BigEndian.Uint32(b[4:8])) * time.Millisecond
return m, nil
}
}
}
func (c *Client) setSendRateLimiter(sm ServerInfoMessage) {
c.wmu.Lock()
defer c.wmu.Unlock()
if sm.TokenBucketBytesPerSecond == 0 {
c.rate = nil
} else {
c.rate = rate.NewLimiter(
rate.Limit(sm.TokenBucketBytesPerSecond),
sm.TokenBucketBytesBurst)
}
}
// LocalAddr returns the TCP connection's local address.
//
// If the client is broken in some previously detectable way, it
// returns an error.
func (c *Client) LocalAddr() (netip.AddrPort, error) {
readErr, _ := c.readErr.Load().(error)
if readErr != nil {
return netip.AddrPort{}, readErr
}
if c.nc == nil {
return netip.AddrPort{}, errors.New("nil conn")
}
a := c.nc.LocalAddr()
if a == nil {
return netip.AddrPort{}, errors.New("nil addr")
}
return netip.ParseAddrPort(a.String())
}
func cutLeadingN(b []byte, n int) (chunk, remain []byte, ok bool) {
if len(b) >= n {
return b[:n], b[n:], true
}
return nil, b, false
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package derp
import (
"bytes"
"io"
)
// LoanedBytes is a packet payload lent to a callee for the duration of
// a call. The lender reuses the memory once the call returns, so the
// callee must not retain it; Clone gives it a copy to keep.
//
// It deliberately has no accessor for the underlying slice.
type LoanedBytes struct {
bs []byte
}
// LoanBytes lends b for the duration of the call it's passed to.
func LoanBytes(b []byte) LoanedBytes { return LoanedBytes{bs: b} }
// Len returns the number of bytes.
func (b LoanedBytes) Len() int { return len(b.bs) }
// WriteTo writes the bytes to w.
func (b LoanedBytes) WriteTo(w io.Writer) (int64, error) {
n, err := w.Write(b.bs)
return int64(n), err
}
// Clone returns a copy of the bytes that the caller owns.
func (b LoanedBytes) Clone() []byte { return bytes.Clone(b.bs) }
// 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
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
package drive
// Clone makes a deep copy of Share.
// The result aliases no memory with the original.
func (src *Share) Clone() *Share {
if src == nil {
return nil
}
dst := new(Share)
*dst = *src
dst.BookmarkData = append(src.BookmarkData[:0:0], src.BookmarkData...)
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _ShareCloneNeedsRegeneration = Share(struct {
Name string
Path string
As string
BookmarkData []byte
}{})
// 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 Share.
func Clone(dst, src any) bool {
switch src := src.(type) {
case *Share:
switch dst := dst.(type) {
case *Share:
*dst = *src.Clone()
return true
case **Share:
*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 drive
import (
jsonv1 "encoding/json"
"errors"
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=Share
// View returns a read-only view of Share.
func (p *Share) View() ShareView {
return ShareView{ж: p}
}
// ShareView provides a read-only view over Share.
//
// Its methods should only be called if `Valid()` returns true.
type ShareView 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.
ж *Share
}
// Valid reports whether v's underlying value is non-nil.
func (v ShareView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v ShareView) AsStruct() *Share {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v ShareView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v ShareView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *ShareView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x Share
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *ShareView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x Share
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// Name is how this share appears on remote nodes.
func (v ShareView) Name() string { return v.ж.Name }
// Path is the path to the directory on this machine that's being shared.
func (v ShareView) Path() string { return v.ж.Path }
// As is the UNIX or Windows username of the local account used for this
// share. File read/write permissions are enforced based on this username.
// Can be left blank to use the default value of "whoever is running the
// Tailscale GUI".
func (v ShareView) As() string { return v.ж.As }
// BookmarkData contains security-scoped bookmark data for the Sandboxed
// Mac application. The Sandboxed Mac application gains permission to
// access the Share's folder as a result of a user selecting it in a file
// picker. In order to retain access to it across restarts, it needs to
// hold on to a security-scoped bookmark. That bookmark is stored here. See
// https://developer.apple.com/documentation/security/app_sandbox/accessing_files_from_the_macos_app_sandbox#4144043
func (v ShareView) BookmarkData() views.ByteSlice[[]byte] {
return views.ByteSliceOf(v.ж.BookmarkData)
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _ShareViewNeedsRegeneration = Share(struct {
Name string
Path string
As string
BookmarkData []byte
}{})
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package drive
//go:generate go run tailscale.com/cmd/viewer --type=Share --clonefunc
import (
"bytes"
"errors"
"net/http"
"strings"
)
var (
// DisallowShareAs forcibly disables sharing as a specific user, only used
// for testing.
DisallowShareAs = false
ErrDriveNotEnabled = errors.New("Taildrive not enabled")
ErrInvalidShareName = errors.New("Share names may only contain the letters a-z, underscore _, parentheses (), or spaces")
)
// AllowShareAs reports whether sharing files as a specific user is allowed.
func AllowShareAs() bool {
return !DisallowShareAs && doAllowShareAs()
}
// Share configures a folder to be shared through drive.
type Share struct {
// Name is how this share appears on remote nodes.
Name string `json:"name,omitempty"`
// Path is the path to the directory on this machine that's being shared.
Path string `json:"path,omitempty"`
// As is the UNIX or Windows username of the local account used for this
// share. File read/write permissions are enforced based on this username.
// Can be left blank to use the default value of "whoever is running the
// Tailscale GUI".
As string `json:"who,omitempty"`
// BookmarkData contains security-scoped bookmark data for the Sandboxed
// Mac application. The Sandboxed Mac application gains permission to
// access the Share's folder as a result of a user selecting it in a file
// picker. In order to retain access to it across restarts, it needs to
// hold on to a security-scoped bookmark. That bookmark is stored here. See
// https://developer.apple.com/documentation/security/app_sandbox/accessing_files_from_the_macos_app_sandbox#4144043
BookmarkData []byte `json:"bookmarkData,omitempty"`
}
func ShareViewsEqual(a, b ShareView) bool {
if !a.Valid() && !b.Valid() {
return true
}
if !a.Valid() || !b.Valid() {
return false
}
return a.Name() == b.Name() && a.Path() == b.Path() && a.As() == b.As() && a.BookmarkData().Equal(b.ж.BookmarkData)
}
func SharesEqual(a, b *Share) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
return a.Name == b.Name && a.Path == b.Path && a.As == b.As && bytes.Equal(a.BookmarkData, b.BookmarkData)
}
func CompareShares(a, b *Share) int {
if a == nil && b == nil {
return 0
}
if a == nil {
return -1
}
if b == nil {
return 1
}
return strings.Compare(a.Name, b.Name)
}
// FileSystemForRemote is the drive filesystem exposed to remote nodes. It
// provides a unified WebDAV interface to local directories that have been
// shared.
type FileSystemForRemote interface {
// SetFileServerAddr sets the address of the file server to which we
// should proxy. This is used on platforms like Windows and MacOS
// sandboxed where we can't spawn user-specific sub-processes and instead
// rely on the UI application that's already running as an unprivileged
// user to access the filesystem for us.
//
// Note that this includes both the file server's secret token and its
// address, delimited by a pipe |.
SetFileServerAddr(addr string)
// SetShares sets the complete set of shares exposed by this node. If
// AllowShareAs() reports true, we will use one subprocess per user to
// access the filesystem (see userServer). Otherwise, we will use the file
// server configured via SetFileServerAddr.
SetShares(shares []*Share)
// ServeHTTPWithPerms behaves like the similar method from http.Handler but
// also accepts a Permissions map that captures the permissions of the
// connecting node.
ServeHTTPWithPerms(permissions Permissions, w http.ResponseWriter, r *http.Request)
// Close() stops serving the WebDAV content
Close() error
}
// NormalizeShareName normalizes the given share name and returns an error if
// it contains any disallowed characters.
func NormalizeShareName(name string) (string, error) {
// Force all share names to lowercase to avoid potential incompatibilities
// with clients that don't support case-sensitive filenames.
name = strings.ToLower(name)
// Trim whitespace
name = strings.TrimSpace(name)
if !validShareName(name) {
return "", ErrInvalidShareName
}
return name, nil
}
func validShareName(name string) bool {
if name == "" {
return false
}
for _, r := range name {
if 'a' <= r && r <= 'z' || '0' <= r && r <= '9' {
continue
}
switch r {
case '_', ' ', '(', ')':
continue
}
return false
}
return true
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package drive
import (
"encoding/json"
"fmt"
)
type Permission uint8
const (
PermissionNone Permission = iota
PermissionReadOnly
PermissionReadWrite
)
const (
accessReadOnly = "ro"
accessReadWrite = "rw"
wildcardShare = "*"
)
// Permissions represents the set of permissions for a given principal to a
// set of shares.
type Permissions map[string]Permission
type grant struct {
Shares []string
Access string
}
// ParsePermissions builds a Permissions map from a list of raw grants.
func ParsePermissions(rawGrants [][]byte) (Permissions, error) {
permissions := make(Permissions)
for _, rawGrant := range rawGrants {
var g grant
err := json.Unmarshal(rawGrant, &g)
if err != nil {
return nil, fmt.Errorf("unmarshal raw grants %s: %v", rawGrant, err)
}
for _, share := range g.Shares {
existingPermission := permissions[share]
permission := PermissionReadOnly
if g.Access == accessReadWrite {
permission = PermissionReadWrite
}
if permission > existingPermission {
permissions[share] = permission
}
}
}
return permissions, nil
}
func (p Permissions) For(share string) Permission {
specific := p[share]
wildcard := p[wildcardShare]
if specific > wildcard {
return specific
}
return wildcard
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build unix
package drive
import "tailscale.com/version"
func doAllowShareAs() bool {
// All UNIX platforms use user servers (sub-processes) to access the OS
// filesystem as a specific unprivileged users, except for sandboxed macOS
// which doesn't support impersonating users and instead accesses files
// through the macOS GUI app as whatever unprivileged user is running it.
return !version.IsSandboxedMacOS()
}
// 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
import (
"runtime"
"slices"
"strings"
"sync"
"tailscale.com/envknob"
"tailscale.com/util/set"
)
// disabledEnv is the TS_DISABLE_FEATURE environment variable: a comma
// separated list of feature names to disable in this process, even if
// they're compiled in. It's a mitigation and attack-surface-reduction
// knob, the runtime analog of the ts_omit_<name> build tags. For
// example, on a system where the tailscaled unit loads environment
// from /etc/default/tailscaled:
//
// TS_DISABLE_FEATURE=ssh,taildrop
//
// The value is read once, at first use, which in practice is during
// package initialization; setting it later has no effect, because
// features register themselves from init.
var disabledEnv = envknob.RegisterString("TS_DISABLE_FEATURE")
// disabledByEnv is the set of normalized feature names listed in
// TS_DISABLE_FEATURE, parsed once on first use.
var disabledByEnv = sync.OnceValue(func() set.Set[string] {
return set.SetOf(parseDisabledList(disabledEnv()))
})
// parseDisabledList splits a TS_DISABLE_FEATURE value into normalized
// feature names, skipping empty entries.
func parseDisabledList(s string) []string {
var names []string
for _, ent := range strings.Split(s, ",") {
if name := normalizeFeatureName(ent); name != "" {
names = append(names, name)
}
}
return names
}
// normalizeFeatureName canonicalizes a feature name whether it came
// from TS_DISABLE_FEATURE or from code. It trims spaces, lowercases,
// drops an optional ts_omit_ build-tag prefix, and maps underscores to
// dashes. Registered names use dashes by convention (for example
// "desktop-sessions"), but their corresponding build tags use
// underscores, so both spellings are accepted.
func normalizeFeatureName(s string) string {
s = strings.TrimSpace(s)
s = strings.ToLower(s)
s = strings.TrimPrefix(s, "ts_omit_")
return strings.ReplaceAll(s, "_", "-")
}
// Disabled reports whether the named feature has been disabled for
// this process via the TS_DISABLE_FEATURE environment variable. A
// disabled feature behaves as if it had not been linked in: [Register]
// reports false, and hooks and extensions registered by its package
// are ignored.
func Disabled(name string) bool {
return disabledByEnv().Contains(normalizeFeatureName(name))
}
// EnvDisabled returns the sorted feature names listed in the
// TS_DISABLE_FEATURE environment variable, whether or not they name
// features that this build contains. It is for diagnostics, such as
// the debug-features LocalAPI endpoint.
func EnvDisabled() []string {
names := disabledByEnv().Slice()
slices.Sort(names)
return names
}
// featurePkgPrefix is the import path prefix of the feature package
// tree. The name of a package under it is the first path element after
// the prefix, so sub-packages of a feature (such as
// feature/captiveportal/netcheckhook) count as their parent feature.
const featurePkgPrefix = "tailscale.com/feature/"
// mechanismPkgPrefix is the prefix of symbol names belonging to the
// registration machinery itself (this package), which the stack walk
// in callerFeatureName steps over to find the registering feature. It
// matches both plain functions (tailscale.com/feature.callerFeatureName)
// and methods (tailscale.com/feature.(*Hook[...]).Set).
const mechanismPkgPrefix = "tailscale.com/feature."
// callerFeatureDisabled reports whether the feature package calling
// into this package has been disabled via TS_DISABLE_FEATURE. It backs
// the silent skip in [Hook.Set] and [Hooks.Add] so that a feature
// package which registers hooks without first consulting [Register]
// still gets disabled. If the caller is not a feature package, it
// reports false.
func callerFeatureDisabled() bool {
name, ok := callerFeatureName()
return ok && Disabled(name)
}
// callerFeatureName walks up the call stack, starting just above this
// package's own frames, and returns the name of the first calling
// package under tailscale.com/feature/. The second result is false if
// no calling feature package is found, which means the caller is
// ordinary code and its registration is always wanted.
//
// Frames are matched by symbol name prefix rather than by parsing out
// package paths, because the symbol name of a Hook.Set frame includes
// its type parameters, which themselves contain import paths.
func callerFeatureName() (string, bool) {
var pcs [16]uintptr
n := runtime.Callers(1, pcs[:])
frames := runtime.CallersFrames(pcs[:n])
for {
frame, more := frames.Next()
fn := frame.Function
switch {
case fn == "":
return "", false
case strings.HasPrefix(fn, mechanismPkgPrefix):
// A frame of the registration machinery; look above it.
case strings.HasPrefix(fn, featurePkgPrefix):
rest := strings.TrimPrefix(fn, featurePkgPrefix)
if i := strings.IndexAny(rest, "/."); i >= 0 {
rest = rest[:i]
}
return rest, true
default:
// A frame outside the feature tree; the search is over.
return "", false
}
if !more {
return "", false
}
}
}
// 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 and
// reports whether it should initialize itself. It reports false if the
// feature was disabled via the TS_DISABLE_FEATURE environment variable,
// in which case the feature is not recorded as registered and the
// caller should skip the rest of its registration, such as setting
// hooks and registering extensions and handlers. The typical use is at
// the top of a feature package's init:
//
// func init() {
// if !feature.Register("foo") {
// return
// }
// // ... set hooks, register extensions and handlers ...
// }
//
// A feature package with more than one init that registers things puts
// this gate in one of them and guards the others with [Disabled], or
// better, consolidates them into a single init.
//
// Register panics if the feature is already registered.
func Register(name string) bool {
if Disabled(name) {
return false
}
if _, ok := in[name]; ok {
panic("duplicate feature registration for " + name)
}
in[name] = true
return 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.
//
// As a backstop for feature packages that forget to consult
// [Register], if the caller is a package under tailscale.com/feature/
// whose feature was disabled via TS_DISABLE_FEATURE, the call is
// silently ignored and the hook is left unset.
func (h *Hook[Func]) Set(f Func) {
if callerFeatureDisabled() {
return
}
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.
//
// Like [Hook.Set], it is silently ignored if the calling feature
// package was disabled via TS_DISABLE_FEATURE.
func (h *Hooks[Func]) Add(f Func) {
if callerFeatureDisabled() {
return
}
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
)
// 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 {
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
magicSockReceiveFuncs []*ReceiveFuncStats // append-only and lazy, new Conn reuses existing entries
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.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 _, f := range t.magicSockReceiveFuncs {
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.
// Immutable after initialization.
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
// name, creating it on first use. Repeated calls with the same name return
// the same tracker.
//
// If t is nil, it returns nil.
func (t *Tracker) ReceiveFuncStats(name string) *ReceiveFuncStats {
if !buildfeatures.HasHealth || t == nil {
return nil
}
t.mu.Lock()
defer t.mu.Unlock()
for _, f := range t.magicSockReceiveFuncs {
if f.name == name {
return f
}
}
f := &ReceiveFuncStats{name: name}
t.magicSockReceiveFuncs = append(t.magicSockReceiveFuncs, f)
return f
}
func (t *Tracker) checkReceiveFuncsLocked() {
for _, f := range t.magicSockReceiveFuncs {
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 ipn
import (
"errors"
"fmt"
"slices"
"strconv"
"strings"
"time"
"tailscale.com/drive"
"tailscale.com/health"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/types/empty"
"tailscale.com/types/key"
"tailscale.com/types/structs"
"tailscale.com/types/views"
"tailscale.com/util/syspolicy/policyclient"
)
type State int
const (
NoState State = 0
InUseOtherUser State = 1
NeedsLogin State = 2
NeedsMachineAuth State = 3
Stopped State = 4
Starting State = 5
Running State = 6
)
// GoogleIDToken Type is the tailcfg.Oauth2Token.TokenType for the Google
// ID tokens used by the Android client.
const GoogleIDTokenType = "ts_android_google_login"
var stateStrings = [...]string{
"NoState",
"InUseOtherUser",
"NeedsLogin",
"NeedsMachineAuth",
"Stopped",
"Starting",
"Running",
}
func (s State) String() string {
return stateStrings[s]
}
// StateFromString parses s as a State string value.
func StateFromString(s string) (_ State, ok bool) {
i := slices.Index(stateStrings[:], s)
if i == -1 {
return NoState, false
}
return State(i), true
}
// EngineStatus contains WireGuard engine stats.
type EngineStatus struct {
RBytes, WBytes int64
NumLive int
LiveDERPs int // number of active DERP connections
LivePeers map[key.NodePublic]ipnstate.PeerStatusLite
}
// NotifyWatchOpt is a bitmask of options about what type of Notify messages
// to subscribe to.
type NotifyWatchOpt uint64
// NotifyWatchOpt values.
//
// These aren't declared using Go's iota because they're not purely internal to
// the process and iota should not be used for values that are serialized to
// disk or network. In this case, these values come over the network via the
// LocalAPI, a mostly stable API.
const (
// NotifyWatchEngineUpdates, if set, causes Engine updates to be sent to the
// client either regularly or when they change, without having to ask for
// each one via Engine.RequestStatus.
NotifyWatchEngineUpdates NotifyWatchOpt = 1 << 0
NotifyInitialState NotifyWatchOpt = 1 << 1 // if set, the first Notify message (sent immediately) will contain the current State + BrowseToURL + SessionID
NotifyInitialPrefs NotifyWatchOpt = 1 << 2 // if set, the first Notify message (sent immediately) will contain the current Prefs
// ObsoleteNotifyInitialNetMap is a formerly valid bit (previously named
// NotifyInitialNetMap) that asked for the first Notify message to
// carry the full netmap in the since-removed Notify.NetMap field.
// Subscription requests that set it are now rejected by
// [ValidateNotifyWatchOpt]. Watchers seed their view from
// [NotifyInitialStatus] instead, and those that need more than the
// status fetch what they need via other LocalAPI methods. The bit
// value remains reserved so it is never reused with a different
// meaning.
ObsoleteNotifyInitialNetMap NotifyWatchOpt = 1 << 3
NotifyNoPrivateKeys NotifyWatchOpt = 1 << 4 // (no-op) it used to redact private keys; now they always are and this does nothing
NotifyInitialDriveShares NotifyWatchOpt = 1 << 5 // if set, the first Notify message (sent immediately) will contain the current Taildrive Shares
NotifyInitialOutgoingFiles NotifyWatchOpt = 1 << 6 // if set, the first Notify message (sent immediately) will contain the current Taildrop OutgoingFiles
NotifyInitialHealthState NotifyWatchOpt = 1 << 7 // if set, the first Notify message (sent immediately) will contain the current health.State of the client
// ObsoleteNotifyRateLimit is a formerly valid bit (previously named
// NotifyRateLimit) that asked tailscaled to rate limit spammy netmap
// updates to every few seconds. It became meaningless once tailscaled
// stopped emitting [Notify.NetMap] on runtime (non-initial) bus
// messages, so subscription requests that set it are now rejected by
// [ValidateNotifyWatchOpt]. The bit value remains reserved so it is
// never reused with a different meaning.
ObsoleteNotifyRateLimit NotifyWatchOpt = 1 << 8
NotifyHealthActions NotifyWatchOpt = 1 << 9 // if set, include PrimaryActions in health.State. Otherwise append the action URL to the text
NotifyInitialSuggestedExitNode NotifyWatchOpt = 1 << 10 // if set, the first Notify message (sent immediately) will contain the current SuggestedExitNode if available
NotifyInitialClientVersion NotifyWatchOpt = 1 << 11 // if set, the first Notify message (sent immediately) will contain the current ClientVersion if available and if update checks are enabled
// NotifyPeerChanges, if set, opts the watcher into peer-set delta
// notifications: [Notify.PeersChanged] (peer added or full-Node
// replaced) and [Notify.PeersRemoved] (peer removed by NodeID).
//
// Without this bit, peer adds/removes/replacements are not delivered
// over the bus at all (consumers fall back to fetching the netmap on
// demand).
//
// Watchers that want narrower per-field updates as well (Online,
// LastSeen, DERPHome, Endpoints) should additionally set
// [NotifyPeerPatches]. Without [NotifyPeerPatches], any per-field
// patch tailscaled would have emitted as a [tailcfg.PeerChange] is
// promoted into a full-Node entry in [Notify.PeersChanged] for this
// watcher, so a watcher that opts only into [NotifyPeerChanges] still
// observes every per-peer mutation; it just receives them as full
// Nodes rather than narrow patches. The cost is bus bandwidth.
//
// Watchers typically pair this with [NotifyInitialStatus] to seed
// their initial state.
NotifyPeerChanges NotifyWatchOpt = 1 << 12
// NotifyNoNetMap historically suppressed the legacy Notify.NetMap
// field on runtime (non-initial) Notify messages on platforms where
// tailscaled still emitted it by default. That field no longer
// exists, so this bit is now a no-op. It remains accepted for
// compatibility with clients that set it.
NotifyNoNetMap NotifyWatchOpt = 1 << 13
// NotifyInitialStatus, if set, causes the first Notify message (sent
// immediately) to contain the current [ipnstate.Status] in
// [Notify.InitialStatus]. Together with [Notify.SelfChange] and
// [Notify.PeersChanged] on subsequent messages, it lets a watcher
// stitch together a continuous view of the local node's state as a
// stable, client-facing snapshot type, without needing the netmap.
//
// The status is sized to the subscription: its per-peer entries
// (Status.Peer) are only populated if the watcher also set
// [NotifyPeerChanges] or [NotifyPeerPatches], since building them is
// O(peers) and only peer-delta subscribers need a peer baseline.
// Self-only watchers get Status.Self and the scalar fields.
NotifyInitialStatus NotifyWatchOpt = 1 << 14
// NotifyPeerPatches, if set, opts the watcher into narrow per-field
// peer patches via [Notify.PeerChangedPatch]. It implies
// [NotifyPeerChanges]: a watcher with [NotifyPeerPatches] also
// receives [Notify.PeersChanged] and [Notify.PeersRemoved].
//
// This is the lower-bandwidth mode: changes to fields that fit in a
// [tailcfg.PeerChange] (currently Online, LastSeen, DERPHome,
// Endpoints) ride as patches; only changes that don't fit ride as
// full Nodes in [Notify.PeersChanged].
//
// Without this bit but with [NotifyPeerChanges], the producer
// promotes any patch into a full-Node entry in [Notify.PeersChanged]
// for this session, at the cost of bandwidth.
NotifyPeerPatches NotifyWatchOpt = 1 << 15
// NotifyInProcessNoDisconnect, if set, marks this watcher as an
// in-process subscriber that must not be disconnected for falling behind
// on its notification queue. Instead, if its queue fills, Notify
// production blocks until the watcher catches up.
//
// Callers using this bit must receive and process notifications promptly.
// Their callbacks must not call back into LocalBackend or wait on work that
// might call back into LocalBackend, because the producer might be holding
// LocalBackend's mutex while waiting for the watcher to catch up.
//
// This bit is only valid for in-process callers of
// LocalBackend.WatchNotificationsAs. LocalAPI WatchIPNBus clients must
// not request it.
NotifyInProcessNoDisconnect NotifyWatchOpt = 1 << 16
// NotifySysPolicyChanges, if set, causes the first Notify message, which is sent
// immediately, to contain the current effective [setting.Snapshot] in
// [Notify.Policy]. [Notify.Policy] is included in subsequent messages whenever
// the effective policy changes.
//
// The snapshot is scoped to the connected user's identity (on Windows,
// derived from the named-pipe token's SID).
//
// The [setting.Snapshot] that is delivered is a full snapshot on every
// change.
NotifySysPolicyChanges NotifyWatchOpt = 1 << 17
// NotifyPeerWireGuardState, if set, opts the watcher into
// WireGuard session state notifications via [Notify.PeerState].
// The first Notify sent to the watcher includes a dump of current
// non-zero peer states, and subsequent Notifies include per-peer
// state changes.
NotifyPeerWireGuardState NotifyWatchOpt = 1 << 18
)
// String implements the [fmt.Stringer] interface.
// Returns the string representation of all the bits joined by the bitwise-or "|" operator.
func (o NotifyWatchOpt) String() string {
if o == NotifyWatchOpt(0) {
return fmt.Sprintf("%T(%#x)", o, uint64(o))
}
pkg, _, found := strings.Cut(fmt.Sprintf("%T", o), ".")
var bits []string
var mask NotifyWatchOpt
try := func(bit NotifyWatchOpt, s string) {
if o&bit == 0 {
return
}
if found {
bits = append(bits, pkg+"."+s)
} else {
bits = append(bits, s)
}
mask |= bit
}
try(NotifyWatchEngineUpdates, "NotifyWatchEngineUpdates")
try(NotifyInitialState, "NotifyInitialState")
try(NotifyInitialPrefs, "NotifyInitialPrefs")
try(ObsoleteNotifyInitialNetMap, "ObsoleteNotifyInitialNetMap")
try(NotifyNoPrivateKeys, "NotifyNoPrivateKeys")
try(NotifyInitialDriveShares, "NotifyInitialDriveShares")
try(NotifyInitialOutgoingFiles, "NotifyInitialOutgoingFiles")
try(NotifyInitialHealthState, "NotifyInitialHealthState")
try(ObsoleteNotifyRateLimit, "ObsoleteNotifyRateLimit")
try(NotifyHealthActions, "NotifyHealthActions")
try(NotifyInitialSuggestedExitNode, "NotifyInitialSuggestedExitNode")
try(NotifyInitialClientVersion, "NotifyInitialClientVersion")
try(NotifyPeerChanges, "NotifyPeerChanges")
try(NotifyNoNetMap, "NotifyNoNetMap")
try(NotifyInitialStatus, "NotifyInitialStatus")
try(NotifyPeerPatches, "NotifyPeerPatches")
try(NotifyInProcessNoDisconnect, "NotifyInProcessNoDisconnect")
try(NotifySysPolicyChanges, "NotifySysPolicyChanges")
try(NotifyPeerWireGuardState, "NotifyPeerWireGuardState")
if mask != o {
bits = append(bits, fmt.Sprintf("%T(%#x)", o, uint64(o^mask))) // unknown
}
if len(bits) == 1 {
return bits[0]
}
// Multiple values, so we need to wrap with parentheses.
return "(" + strings.Join(bits, " | ") + ")"
}
// AppendText implements the [encoding.TextAppender] interface
// by encoding its textual representation.
func (o NotifyWatchOpt) AppendText(b []byte) ([]byte, error) {
return strconv.AppendUint(b, uint64(o), 10), nil
}
// MarshalText implements the [encoding.TextMarshaler] interface
// by encoding its textual representation.
func (o NotifyWatchOpt) MarshalText() (text []byte, err error) {
return o.AppendText(nil)
}
// UnmarshalText implements the [encoding.TextUnmarshaler] interface
// by decoding its textual representation.
func (o *NotifyWatchOpt) UnmarshalText(text []byte) error {
v, err := strconv.ParseUint(string(text), 10, 64)
if err != nil {
return err
}
*o = NotifyWatchOpt(v)
return nil
}
// ValidateNotifyWatchOpt reports whether mask is a valid WatchIPNBus
// subscription mask.
func ValidateNotifyWatchOpt(mask NotifyWatchOpt) error {
if mask&ObsoleteNotifyRateLimit != 0 {
return errors.New("the NotifyRateLimit IPN bus subscription bit is no longer supported")
}
if mask&ObsoleteNotifyInitialNetMap != 0 {
return errors.New("the NotifyInitialNetMap IPN bus subscription bit is no longer supported; seed from NotifyInitialStatus instead")
}
return nil
}
// NodeGUIVisibility contains GUI presentation metadata for a node.
type NodeGUIVisibility struct {
HideInGUI bool
}
// Notify is a communication from a backend (e.g. tailscaled) to a frontend
// (cmd/tailscale, iOS, macOS, Win Tasktray).
// In any given notification, any or all of these may be nil, meaning
// that they have not changed.
// They are JSON-encoded on the wire, despite the lack of struct tags.
//
// API maturity: this type is not considered a stable API and is
// subject to change between releases.
type Notify struct {
_ structs.Incomparable
Version string // version number of IPN backend
// SessionID identifies the unique WatchIPNBus session.
// This field is only set in the first message when requesting
// NotifyInitialState. Clients must store it on their side as
// following notifications will not include this field.
SessionID string `json:",omitzero"`
// ErrMessage, if non-nil, contains a critical error message.
// For State InUseOtherUser, ErrMessage is not critical and just contains the details.
ErrMessage *string
LoginFinished *empty.Message // non-nil when/if the login process succeeded
State *State // if non-nil, the new or current IPN state
Prefs *PrefsView // if non-nil && Valid, the new or current preferences
// SelfChange, if non-nil, indicates that this node's own [tailcfg.Node]
// has changed: addresses, name, key expiry, capabilities, etc. It carries
// the new self node so reactive consumers (containerboot, kube agents,
// sniproxy, etc.) can read the current self state without watching the
// full netmap.
//
// Runtime self changes are not gated by any subscription bit: every
// watcher receives them. There is no initial SelfChange; watchers
// that need the current self node at the start of a session should
// set [NotifyInitialStatus] and seed from InitialStatus.Self.
//
// Consumers that need additional state (peers, DNS config, etc.)
// should react to SelfChange by fetching what they need via other
// LocalAPI methods, such as LocalClient.Status or
// LocalClient.DNSConfig.
SelfChange *tailcfg.Node `json:",omitzero"`
// InitialStatus, if non-nil, is the current [ipnstate.Status]. It is
// only set in the first Notify of a session when the watcher requested
// [NotifyInitialStatus]. Together with subsequent [Notify.SelfChange]
// and [Notify.PeerChanges] messages, it lets a watcher stitch together
// a continuous view of node state without fetching the netmap.
InitialStatus *ipnstate.Status `json:",omitzero"`
// NodeGUIVisibility, when non-nil, is a complete snapshot of GUI
// visibility state for the current node and peers, keyed by stable node ID.
// It is sent to watchers that requested [NotifySysPolicyChanges]. Clients
// should replace their previous visibility state with this map.
//
// This is presentation metadata only. Nodes remain present in status,
// peer notifications, åand the underlying netmap.
NodeGUIVisibility map[tailcfg.StableNodeID]NodeGUIVisibility `json:",omitzero"`
// PeerChangedPatch, if non-empty, lists narrow per-field peer patches
// since the last Notify (currently Online, LastSeen, DERPHome,
// Endpoints). It mirrors [tailcfg.MapResponse.PeersChangedPatch].
//
// Peer additions and any peer change that can't be expressed as a
// [tailcfg.PeerChange] travel in [Notify.PeersChanged]; peer removals
// in [Notify.PeersRemoved].
//
// Watchers must opt in to receive this field by setting
// [NotifyPeerPatches]; without that bit (but with [NotifyPeerChanges])
// the producer promotes each patch into a full-Node entry in
// [Notify.PeersChanged] instead.
//
// The [tailcfg.PeerChange] type may grow more fields over time;
// consumers that see a [tailcfg.PeerChange] with a field they don't
// recognize should re-fetch the affected node by NodeID via
// [LocalClient.PeerByID] (an O(1) lookup) to learn its current value
// rather than ignoring the change.
PeerChangedPatch []*tailcfg.PeerChange `json:",omitzero"`
// PeersChanged, if non-empty, lists peers whose full [tailcfg.Node]
// has been added or replaced since the last Notify. A node ID may
// appear here either because it is a brand-new peer or because the
// control plane sent a fresh full Node for an existing peer when the
// change wasn't expressible as a [tailcfg.PeerChange] patch (e.g. a
// CapMap, Addresses, Hostinfo, or Tags change). Consumers should
// upsert by NodeID.
//
// This mirrors [tailcfg.MapResponse.PeersChanged] semantics; peer
// removals travel in [Notify.PeersRemoved] and narrow per-field
// patches in [Notify.PeerChanges].
PeersChanged []*tailcfg.Node `json:",omitzero"`
// PeersRemoved, if non-empty, lists [tailcfg.NodeID]s that have been
// removed from the netmap since the last Notify. See
// [Notify.PeersChanged]. This mirrors
// [tailcfg.MapResponse.PeersRemoved].
PeersRemoved []tailcfg.NodeID `json:",omitzero"`
// UserProfiles, if non-empty, carries [tailcfg.UserProfileView]
// entries that have been added or updated since the last Notify on
// this session. Watchers must opt in via [NotifyPeerChanges] or
// [NotifyPeerPatches]; this field is gated on the same bits as
// [Notify.PeersChanged] / [Notify.PeerChangedPatch] because its
// only purpose is to let those consumers resolve the [tailcfg.UserID]
// referenced by a peer Node.
//
// The producer guarantees that any UserID referenced by a peer in
// a [Notify.PeersChanged] / [Notify.PeerChangedPatch] entry will
// have its profile delivered either earlier on this same session
// (via an earlier Notify carrying UserProfiles) or in this same
// Notify. A consumer that sees a
// UserID it doesn't recognize on a session that opted in to
// peer-change notifications can treat it as a bug; the
// [LocalClient.UserProfile] LocalAPI fallback exists for sessions
// that didn't subscribe with the peer-change bits or that need to
// look up a UserID for any other reason.
//
// The values are [tailcfg.UserProfileView] so they share backing
// memory with the producer's tracking maps; consumers should treat
// them as read-only and use [tailcfg.UserProfileView.AsStruct] or
// the per-field accessors to read them.
UserProfiles map[tailcfg.UserID]tailcfg.UserProfileView `json:",omitzero"`
// PeerState, if non-empty, carries WireGuard session states keyed by stable
// node ID. Watchers must opt in via [NotifyPeerWireGuardState].
PeerState map[tailcfg.StableNodeID]PeerState `json:",omitzero"`
Engine *EngineStatus // if non-nil, the new or current wireguard stats
BrowseToURL *string // if non-nil, UI should open a browser right now
// FilesWaiting if non-nil means that files are buffered in
// the Tailscale daemon and ready for local transfer to the
// user's preferred storage location.
//
// Deprecated: use LocalClient.AwaitWaitingFiles instead.
FilesWaiting *empty.Message `json:",omitzero"`
// IncomingFiles, if non-nil, specifies which files are in the
// process of being received. A nil IncomingFiles means this
// Notify should not update the state of file transfers. A non-nil
// but empty IncomingFiles means that no files are in the middle
// of being transferred.
//
// Deprecated: use LocalClient.AwaitWaitingFiles instead.
IncomingFiles []PartialFile `json:",omitzero"`
// OutgoingFiles, if non-nil, tracks which files are in the process of
// being sent via TailDrop, including files that finished, whether
// successful or failed. This slice is sorted by Started time, then Name.
OutgoingFiles []*OutgoingFile `json:",omitzero"`
// LocalTCPPort, if non-nil, informs the UI frontend which
// (non-zero) localhost TCP port it's listening on.
// This is currently only used by Tailscale when run in the
// macOS Network Extension.
LocalTCPPort *uint16 `json:",omitzero"`
// ClientVersion, if non-nil, describes whether a client version update
// is available.
ClientVersion *tailcfg.ClientVersion `json:",omitzero"`
// DriveShares tracks the full set of current DriveShares that we're
// publishing. Some client applications, like the MacOS and Windows clients,
// will listen for updates to this and handle serving these shares under
// the identity of the unprivileged user that is running the application. A
// nil value here means that we're not broadcasting shares information, an
// empty value means that there are no shares.
DriveShares views.SliceView[*drive.Share, drive.ShareView]
// Health is the last-known health state of the backend. When this field is
// non-nil, a change in health verified, and the API client should surface
// any changes to the user in the UI.
Health *health.State `json:",omitzero"`
// SuggestedExitNode, if non-nil, is the node that the backend has determined to
// be the best exit node for the current network conditions.
SuggestedExitNode *tailcfg.StableNodeID `json:",omitzero"`
// Policy, if non-nil, is the effective policy snapshot for the
// connected user. It is scoped per-user: per-user policy settings
// are merged with device-wide settings, with device-wide taking
// precedence. Sent initially when [NotifySysPolicyChanges] is set,
// and on change thereafter.
Policy *policyclient.PolicySnapshot `json:",omitzero"`
// type is mirrored in xcode/IPN/Core/LocalAPI/Model/LocalAPIModel.swift
}
// PeerWireGuardState is the WireGuard session state for a peer.
//
// It JSON-marshals as a lowercase string (e.g. "handshake", "established")
// rather than its integer value, so the wire format does not depend on the
// numeric constants below.
type PeerWireGuardState uint8
const (
PeerWireGuardStateNone PeerWireGuardState = 0
PeerWireGuardStateHandshake PeerWireGuardState = 1
PeerWireGuardStateEstablished PeerWireGuardState = 2
PeerWireGuardStateExpired PeerWireGuardState = 3
)
// String returns the lowercase string form of s used by [PeerWireGuardState.MarshalText].
func (s PeerWireGuardState) String() string {
switch s {
case PeerWireGuardStateNone:
return "none"
case PeerWireGuardStateHandshake:
return "handshake"
case PeerWireGuardStateEstablished:
return "established"
case PeerWireGuardStateExpired:
return "expired"
}
return fmt.Sprintf("PeerWireGuardState(%d)", uint8(s))
}
// MarshalText implements [encoding.TextMarshaler].
func (s PeerWireGuardState) MarshalText() ([]byte, error) {
return []byte(s.String()), nil
}
// UnmarshalText implements [encoding.TextUnmarshaler].
func (s *PeerWireGuardState) UnmarshalText(b []byte) error {
switch string(b) {
case "none":
*s = PeerWireGuardStateNone
case "handshake":
*s = PeerWireGuardStateHandshake
case "established":
*s = PeerWireGuardStateEstablished
case "expired":
*s = PeerWireGuardStateExpired
default:
return fmt.Errorf("unknown PeerWireGuardState %q", b)
}
return nil
}
// PeerState is the per-peer WireGuard session state delivered in
// [Notify.PeerState].
type PeerState struct {
// PeerWireGuardState is the current WireGuard session state for the peer.
PeerWireGuardState PeerWireGuardState
// PeerWireGuardStateAt is the wall-clock time at which the peer entered
// [PeerState.PeerWireGuardState], as observed by tailscaled.
// It is tracked by [LocalBackend] even when no watchers are subscribed,
// so a later subscriber's initial snapshot reflects the true transition
// time rather than the subscription time.
PeerWireGuardStateAt time.Time
}
func (n Notify) String() string {
var sb strings.Builder
sb.WriteString("Notify{")
if n.ErrMessage != nil {
fmt.Fprintf(&sb, "err=%q ", *n.ErrMessage)
}
if n.LoginFinished != nil {
sb.WriteString("LoginFinished ")
}
if n.State != nil {
fmt.Fprintf(&sb, "state=%v ", *n.State)
}
if n.Prefs != nil && n.Prefs.Valid() {
fmt.Fprintf(&sb, "%v ", n.Prefs.Pretty())
}
if n.SelfChange != nil {
fmt.Fprintf(&sb, "SelfChange(%v) ", n.SelfChange.StableID)
}
if n.PeerChangedPatch != nil {
fmt.Fprintf(&sb, "PeerChangedPatch(%d) ", len(n.PeerChangedPatch))
}
if len(n.PeerState) > 0 {
fmt.Fprintf(&sb, "PeerState(%d) ", len(n.PeerState))
}
if n.Engine != nil {
fmt.Fprintf(&sb, "wg=%v ", *n.Engine)
}
if n.BrowseToURL != nil {
sb.WriteString("URL=<...> ")
}
if n.FilesWaiting != nil {
sb.WriteString("FilesWaiting ")
}
if len(n.IncomingFiles) != 0 {
sb.WriteString("IncomingFiles ")
}
if n.LocalTCPPort != nil {
fmt.Fprintf(&sb, "tcpport=%v ", n.LocalTCPPort)
}
if n.Health != nil {
sb.WriteString("Health{...} ")
}
if n.SuggestedExitNode != nil {
fmt.Fprintf(&sb, "SuggestedExitNode=%v ", *n.SuggestedExitNode)
}
s := sb.String()
if s == "Notify{" {
return "Notify{}"
} else {
return s[0:len(s)-1] + "}"
}
}
// PartialFile represents an in-progress incoming file transfer.
type PartialFile struct {
Name string // e.g. "foo.jpg"
Started time.Time // time transfer started
DeclaredSize int64 // or -1 if unknown
Received int64 // bytes copied thus far
// PartialPath is set non-empty in "direct" file mode to the
// in-progress '*.partial' file's path when the peerapi isn't
// being used; see LocalBackend.SetDirectFileRoot.
PartialPath string `json:",omitempty"`
FinalPath string `json:",omitempty"`
// Done is set in "direct" mode when the partial file has been
// closed and is ready for the caller to rename away the
// ".partial" suffix.
Done bool `json:",omitempty"`
}
// OutgoingFile represents an in-progress outgoing file transfer.
type OutgoingFile struct {
ID string `json:",omitempty"` // unique identifier for this transfer (a type 4 UUID)
PeerID tailcfg.StableNodeID `json:",omitempty"` // identifier for the peer to which this is being transferred
Name string `json:",omitempty"` // e.g. "foo.jpg"
Started time.Time // time transfer started
DeclaredSize int64 // or -1 if unknown
Sent int64 // bytes copied thus far
Finished bool // indicates whether or not the transfer finished
Succeeded bool // for a finished transfer, indicates whether or not it was successful
}
// StateKey is an opaque identifier for a set of LocalBackend state
// (preferences, private keys, etc.). It is also used as a key for
// the various LoginProfiles that the instance may be signed into.
//
// Additionally, the StateKey can be debug setting name:
//
// - "_debug_magicsock_until" with value being a unix timestamp stringified
// - "_debug_<component>_until" with value being a unix timestamp stringified
type StateKey string
// DebuggableComponents is a list of components whose debugging can be turned on
// and off individually using the tailscale debug command.
var DebuggableComponents = []string{
"magicsock",
"sockstats",
"syspolicy",
}
type Options struct {
// FrontendLogID is the public logtail id used by the frontend.
FrontendLogID string
// UpdatePrefs, if provided, overrides the Prefs already stored in the
// backend state, *except* for the Persist member.
//
// TODO(apenwarr): Rename this to Prefs, and possibly move Prefs.Persist
// elsewhere entirely (as it always should have been).
UpdatePrefs *Prefs
// AuthKey is an optional node auth key used to authorize a
// new node key without user interaction.
AuthKey string
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package ipn
import (
"errors"
"fmt"
"net/netip"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/opt"
"tailscale.com/types/preftype"
)
// ConfigVAlpha is the config file format for the "alpha0" version.
type ConfigVAlpha struct {
Version string // "alpha0" for now
Locked opt.Bool `json:",omitempty"` // whether the config is locked from being changed by 'tailscale set'; it defaults to true
ServerURL *string `json:",omitempty"` // defaults to https://controlplane.tailscale.com
AuthKey *string `json:",omitempty"` // as needed if NeedsLogin. either key or path to a file (if prefixed with "file:")
Enabled opt.Bool `json:",omitempty"` // wantRunning; empty string defaults to true
OperatorUser *string `json:",omitempty"` // local user name who is allowed to operate tailscaled without being root or using sudo
Hostname *string `json:",omitempty"`
AcceptDNS opt.Bool `json:"acceptDNS,omitempty"` // --accept-dns
AcceptRoutes opt.Bool `json:"acceptRoutes,omitempty"` // --accept-routes defaults to true
ExitNode *string `json:"exitNode,omitempty"` // IP, StableID, or MagicDNS base name
AllowLANWhileUsingExitNode opt.Bool `json:"allowLANWhileUsingExitNode,omitempty"`
AdvertiseRoutes []netip.Prefix `json:",omitempty"`
AdvertiseExitNode opt.Bool `json:",omitzero"`
DisableSNAT opt.Bool `json:",omitzero"`
AdvertiseServices []string `json:",omitempty"`
AppConnector *AppConnectorPrefs `json:",omitempty"` // advertise app connector; defaults to false (if nil or explicitly set to false)
NetfilterMode *string `json:",omitempty"` // "on", "off", "nodivert"
NoStatefulFiltering opt.Bool `json:",omitempty"`
PostureChecking opt.Bool `json:",omitempty"`
RunSSHServer opt.Bool `json:",omitempty"` // Tailscale SSH
RunWebClient opt.Bool `json:",omitempty"`
ShieldsUp opt.Bool `json:",omitempty"`
RemoteConfig opt.Bool `json:",omitzero"` // delegate full remote control to the tailnet admin; see Prefs.RemoteConfig
AutoUpdate *AutoUpdatePrefs `json:",omitempty"`
ServeConfigTemp *ServeConfig `json:",omitempty"` // TODO(bradfitz,maisem): make separate stable type for this
// StaticEndpoints are additional, user-defined endpoints that this node
// should advertise amongst its wireguard endpoints.
StaticEndpoints []netip.AddrPort `json:",omitempty"`
// RelayServerPort is the UDP port for the relay server to bind to.
// A value of 0 will pick a random unused port. Nil disables relay server.
RelayServerPort *uint16 `json:",omitzero"`
// RelayServerStaticEndpoints are static IP:port endpoints to advertise
// as candidates for relay connections. Only relevant when RelayServerPort
// is non-nil.
RelayServerStaticEndpoints []netip.AddrPort `json:",omitempty"`
// TODO(bradfitz,maisem): future something like:
// Profile map[string]*Config // keyed by alice@gmail.com, corp.com (TailnetSID)
}
func (c *ConfigVAlpha) ToPrefs() (MaskedPrefs, error) {
var mp MaskedPrefs
if c == nil {
return mp, nil
}
mp.WantRunning = !c.Enabled.EqualBool(false)
mp.WantRunningSet = mp.WantRunning || c.Enabled != ""
if c.ServerURL != nil {
mp.ControlURL = *c.ServerURL
mp.ControlURLSet = true
}
if c.AuthKey != nil && *c.AuthKey != "" {
mp.LoggedOut = false
mp.LoggedOutSet = true
}
if c.OperatorUser != nil {
mp.OperatorUser = *c.OperatorUser
mp.OperatorUserSet = true
}
if c.Hostname != nil {
mp.Hostname = *c.Hostname
mp.HostnameSet = true
}
if c.AcceptDNS != "" {
mp.CorpDNS = c.AcceptDNS.EqualBool(true)
mp.CorpDNSSet = true
}
if c.AcceptRoutes != "" {
mp.RouteAll = c.AcceptRoutes.EqualBool(true)
mp.RouteAllSet = true
}
if c.ExitNode != nil {
ip, err := netip.ParseAddr(*c.ExitNode)
if err == nil {
mp.ExitNodeIP = ip
mp.ExitNodeIPSet = true
} else {
mp.ExitNodeID = tailcfg.StableNodeID(*c.ExitNode)
mp.ExitNodeIDSet = true
}
}
if c.AllowLANWhileUsingExitNode != "" {
mp.ExitNodeAllowLANAccess = c.AllowLANWhileUsingExitNode.EqualBool(true)
mp.ExitNodeAllowLANAccessSet = true
}
if c.AdvertiseRoutes != nil {
var routeErrs []error
for _, route := range c.AdvertiseRoutes {
if route != route.Masked() {
routeErrs = append(routeErrs, fmt.Errorf("route %s has non-address bits set; expected %s", route, route.Masked()))
}
}
if err := errors.Join(routeErrs...); err != nil {
return mp, err
}
mp.AdvertiseRoutes = c.AdvertiseRoutes
mp.AdvertiseRoutesSet = true
}
if c.AdvertiseExitNode.EqualBool(true) {
if mp.AdvertiseRoutesSet {
mp.AdvertiseRoutes = append(mp.AdvertiseRoutes, tsaddr.AllIPv4(), tsaddr.AllIPv6())
} else {
mp.AdvertiseRoutes = tsaddr.ExitRoutes()
mp.AdvertiseRoutesSet = true
}
}
if c.DisableSNAT != "" {
mp.NoSNAT = c.DisableSNAT.EqualBool(true)
mp.NoSNATSet = true
}
if c.NoStatefulFiltering != "" {
mp.NoStatefulFiltering = c.NoStatefulFiltering
mp.NoStatefulFilteringSet = true
}
if c.NetfilterMode != nil {
m, err := preftype.ParseNetfilterMode(*c.NetfilterMode)
if err != nil {
return mp, err
}
mp.NetfilterMode = m
mp.NetfilterModeSet = true
}
if c.PostureChecking != "" {
mp.PostureChecking = c.PostureChecking.EqualBool(true)
mp.PostureCheckingSet = true
}
if c.RunSSHServer != "" {
mp.RunSSH = c.RunSSHServer.EqualBool(true)
mp.RunSSHSet = true
}
if c.RunWebClient != "" {
mp.RunWebClient = c.RunWebClient.EqualBool(true)
mp.RunWebClientSet = true
}
if c.ShieldsUp != "" {
mp.ShieldsUp = c.ShieldsUp.EqualBool(true)
mp.ShieldsUpSet = true
}
if c.RemoteConfig != "" {
mp.RemoteConfig = c.RemoteConfig.EqualBool(true)
mp.RemoteConfigSet = true
}
if c.AutoUpdate != nil {
mp.AutoUpdate = *c.AutoUpdate
mp.AutoUpdateSet = AutoUpdatePrefsMask{ApplySet: true, CheckSet: true}
}
if c.AppConnector != nil {
mp.AppConnector = *c.AppConnector
mp.AppConnectorSet = true
}
// Configfile should be the source of truth for whether this node
// advertises any services. We need to ensure that each reload updates
// currently advertised services as else the transition from 'some
// services are advertised' to 'advertised services are empty/unset in
// conffile' would have no effect (especially given that an empty
// service slice would be omitted from the JSON config).
mp.AdvertiseServicesSet = true
if c.AdvertiseServices != nil {
mp.AdvertiseServices = c.AdvertiseServices
}
mp.RelayServerPortSet = true
mp.RelayServerStaticEndpointsSet = true
if c.RelayServerPort != nil {
mp.RelayServerPort = c.RelayServerPort
}
if c.RelayServerStaticEndpoints != nil {
mp.RelayServerStaticEndpoints = c.RelayServerStaticEndpoints
}
return mp, nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
package ipn
import (
"maps"
"net/netip"
"time"
"tailscale.com/drive"
"tailscale.com/tailcfg"
"tailscale.com/tailcfg/peercap"
"tailscale.com/types/opt"
"tailscale.com/types/persist"
"tailscale.com/types/preftype"
)
// Clone makes a deep copy of LoginProfile.
// The result aliases no memory with the original.
func (src *LoginProfile) Clone() *LoginProfile {
if src == nil {
return nil
}
dst := new(LoginProfile)
*dst = *src
dst.UserProfile = *src.UserProfile.Clone()
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _LoginProfileCloneNeedsRegeneration = LoginProfile(struct {
ID ProfileID
Name string
NetworkProfile NetworkProfile
Key StateKey
UserProfile tailcfg.UserProfile
NodeID tailcfg.StableNodeID
LocalUserID WindowsUserID
ControlURL string
Created time.Time
}{})
// Clone makes a deep copy of Prefs.
// The result aliases no memory with the original.
func (src *Prefs) Clone() *Prefs {
if src == nil {
return nil
}
dst := new(Prefs)
*dst = *src
dst.AdvertiseTags = append(src.AdvertiseTags[:0:0], src.AdvertiseTags...)
dst.AdvertiseRoutes = append(src.AdvertiseRoutes[:0:0], src.AdvertiseRoutes...)
dst.AdvertiseServices = append(src.AdvertiseServices[:0:0], src.AdvertiseServices...)
if src.DriveShares != nil {
dst.DriveShares = make([]*drive.Share, len(src.DriveShares))
for i := range dst.DriveShares {
if src.DriveShares[i] == nil {
dst.DriveShares[i] = nil
} else {
dst.DriveShares[i] = src.DriveShares[i].Clone()
}
}
}
if dst.RelayServerPort != nil {
dst.RelayServerPort = new(*src.RelayServerPort)
}
dst.RelayServerStaticEndpoints = append(src.RelayServerStaticEndpoints[:0:0], src.RelayServerStaticEndpoints...)
dst.Persist = src.Persist.Clone()
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _PrefsCloneNeedsRegeneration = Prefs(struct {
ControlURL string
RouteAll bool
ExitNodeID tailcfg.StableNodeID
ExitNodeIP netip.Addr
AutoExitNode ExitNodeExpression
InternalExitNodePrior tailcfg.StableNodeID
ExitNodeAllowLANAccess bool
CorpDNS bool
RunSSH bool
RunWebClient bool
WantRunning bool
LoggedOut bool
ShieldsUp bool
AdvertiseTags []string
Hostname string
NotepadURLs bool
ForceDaemon bool
Egg bool
AdvertiseRoutes []netip.Prefix
AdvertiseServices []string
Sync opt.Bool
NoSNAT bool
NoStatefulFiltering opt.Bool
NetfilterMode preftype.NetfilterMode
OperatorUser string
ProfileName string
AutoUpdate AutoUpdatePrefs
AppConnector AppConnectorPrefs
PostureChecking bool
NetfilterKind string
RemoteConfig bool
DriveShares []*drive.Share
RelayServerPort *uint16
RelayServerStaticEndpoints []netip.AddrPort
Persist *persist.Persist
}{})
// Clone makes a deep copy of ServeConfig.
// The result aliases no memory with the original.
func (src *ServeConfig) Clone() *ServeConfig {
if src == nil {
return nil
}
dst := new(ServeConfig)
*dst = *src
if dst.TCP != nil {
dst.TCP = map[uint16]*TCPPortHandler{}
for k, v := range src.TCP {
if v == nil {
dst.TCP[k] = nil
} else {
dst.TCP[k] = new(*v)
}
}
}
if dst.Web != nil {
dst.Web = map[HostPort]*WebServerConfig{}
for k, v := range src.Web {
if v == nil {
dst.Web[k] = nil
} else {
dst.Web[k] = v.Clone()
}
}
}
if dst.Services != nil {
dst.Services = map[tailcfg.ServiceName]*ServiceConfig{}
for k, v := range src.Services {
if v == nil {
dst.Services[k] = nil
} else {
dst.Services[k] = v.Clone()
}
}
}
dst.AllowFunnel = maps.Clone(src.AllowFunnel)
if dst.Foreground != nil {
dst.Foreground = map[string]*ServeConfig{}
for k, v := range src.Foreground {
if v == nil {
dst.Foreground[k] = nil
} else {
dst.Foreground[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 _ServeConfigCloneNeedsRegeneration = ServeConfig(struct {
TCP map[uint16]*TCPPortHandler
Web map[HostPort]*WebServerConfig
Services map[tailcfg.ServiceName]*ServiceConfig
AllowFunnel map[HostPort]bool
Foreground map[string]*ServeConfig
ETag string
}{})
// Clone makes a deep copy of ServiceConfig.
// The result aliases no memory with the original.
func (src *ServiceConfig) Clone() *ServiceConfig {
if src == nil {
return nil
}
dst := new(ServiceConfig)
*dst = *src
if dst.TCP != nil {
dst.TCP = map[uint16]*TCPPortHandler{}
for k, v := range src.TCP {
if v == nil {
dst.TCP[k] = nil
} else {
dst.TCP[k] = new(*v)
}
}
}
if dst.Web != nil {
dst.Web = map[HostPort]*WebServerConfig{}
for k, v := range src.Web {
if v == nil {
dst.Web[k] = nil
} else {
dst.Web[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 _ServiceConfigCloneNeedsRegeneration = ServiceConfig(struct {
TCP map[uint16]*TCPPortHandler
Web map[HostPort]*WebServerConfig
Tun bool
}{})
// Clone makes a deep copy of TCPPortHandler.
// The result aliases no memory with the original.
func (src *TCPPortHandler) Clone() *TCPPortHandler {
if src == nil {
return nil
}
dst := new(TCPPortHandler)
*dst = *src
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _TCPPortHandlerCloneNeedsRegeneration = TCPPortHandler(struct {
HTTPS bool
HTTP bool
TCPForward string
TerminateTLS string
ProxyProtocol int
}{})
// Clone makes a deep copy of HTTPHandler.
// The result aliases no memory with the original.
func (src *HTTPHandler) Clone() *HTTPHandler {
if src == nil {
return nil
}
dst := new(HTTPHandler)
*dst = *src
dst.AcceptAppCaps = append(src.AcceptAppCaps[:0:0], src.AcceptAppCaps...)
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _HTTPHandlerCloneNeedsRegeneration = HTTPHandler(struct {
Path string
Proxy string
Text string
AcceptAppCaps []peercap.Cap
Redirect string
}{})
// Clone makes a deep copy of WebServerConfig.
// The result aliases no memory with the original.
func (src *WebServerConfig) Clone() *WebServerConfig {
if src == nil {
return nil
}
dst := new(WebServerConfig)
*dst = *src
if dst.Handlers != nil {
dst.Handlers = map[string]*HTTPHandler{}
for k, v := range src.Handlers {
if v == nil {
dst.Handlers[k] = nil
} else {
dst.Handlers[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 _WebServerConfigCloneNeedsRegeneration = WebServerConfig(struct {
Handlers map[string]*HTTPHandler
}{})
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by tailscale/cmd/viewer; DO NOT EDIT.
package ipn
import (
jsonv1 "encoding/json"
"errors"
"net/netip"
"time"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"tailscale.com/drive"
"tailscale.com/tailcfg"
"tailscale.com/tailcfg/peercap"
"tailscale.com/types/opt"
"tailscale.com/types/persist"
"tailscale.com/types/preftype"
"tailscale.com/types/views"
)
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=LoginProfile,Prefs,ServeConfig,ServiceConfig,TCPPortHandler,HTTPHandler,WebServerConfig
// View returns a read-only view of LoginProfile.
func (p *LoginProfile) View() LoginProfileView {
return LoginProfileView{ж: p}
}
// LoginProfileView provides a read-only view over LoginProfile.
//
// Its methods should only be called if `Valid()` returns true.
type LoginProfileView 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.
ж *LoginProfile
}
// Valid reports whether v's underlying value is non-nil.
func (v LoginProfileView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v LoginProfileView) AsStruct() *LoginProfile {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v LoginProfileView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v LoginProfileView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *LoginProfileView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x LoginProfile
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *LoginProfileView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x LoginProfile
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// ID is a unique identifier for this profile.
// It is assigned on creation and never changes.
// It may seem redundant to have both ID and UserProfile.ID
// but they are different things. UserProfile.ID may change
// over time (e.g. if a device is tagged).
func (v LoginProfileView) ID() ProfileID { return v.ж.ID }
// Name is the user-visible name of this profile.
// It is filled in from the UserProfile.LoginName field.
func (v LoginProfileView) Name() string { return v.ж.Name }
// NetworkProfile is a subset of netmap.NetworkMap that we
// store to remember information about the tailnet that this
// profile was logged in with.
//
// This field was added on 2023-11-17.
func (v LoginProfileView) NetworkProfile() NetworkProfile { return v.ж.NetworkProfile }
// Key is the StateKey under which the profile is stored.
// It is assigned once at profile creation time and never changes.
func (v LoginProfileView) Key() StateKey { return v.ж.Key }
// UserProfile is the server provided UserProfile for this profile.
// This is updated whenever the server provides a new UserProfile.
func (v LoginProfileView) UserProfile() tailcfg.UserProfileView { return v.ж.UserProfile.View() }
// NodeID is the NodeID of the node that this profile is logged into.
// This should be stable across tagging and untagging nodes.
// It may seem redundant to check against both the UserProfile.UserID
// and the NodeID. However the NodeID can change if the node is deleted
// from the admin panel.
func (v LoginProfileView) NodeID() tailcfg.StableNodeID { return v.ж.NodeID }
// LocalUserID is the user ID of the user who created this profile.
// It is only relevant on Windows where we have a multi-user system.
// It is assigned once at profile creation time and never changes.
func (v LoginProfileView) LocalUserID() WindowsUserID { return v.ж.LocalUserID }
// ControlURL is the URL of the control server that this profile is logged
// into.
func (v LoginProfileView) ControlURL() string { return v.ж.ControlURL }
// Created is when this profile was first added to this client. It is
// stamped once at profile creation and never changes. It is used to sort
// the profile list with newest first; profiles created before this field
// existed have a zero value and sort after all stamped profiles.
func (v LoginProfileView) 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 _LoginProfileViewNeedsRegeneration = LoginProfile(struct {
ID ProfileID
Name string
NetworkProfile NetworkProfile
Key StateKey
UserProfile tailcfg.UserProfile
NodeID tailcfg.StableNodeID
LocalUserID WindowsUserID
ControlURL string
Created time.Time
}{})
// View returns a read-only view of Prefs.
func (p *Prefs) View() PrefsView {
return PrefsView{ж: p}
}
// PrefsView provides a read-only view over Prefs.
//
// Its methods should only be called if `Valid()` returns true.
type PrefsView 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.
ж *Prefs
}
// Valid reports whether v's underlying value is non-nil.
func (v PrefsView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v PrefsView) AsStruct() *Prefs {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v PrefsView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v PrefsView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *PrefsView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x Prefs
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *PrefsView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x Prefs
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// ControlURL is the URL of the control server to use.
//
// If empty, the default for new installs, DefaultControlURL
// is used. It's set non-empty once the daemon has been started
// for the first time.
//
// TODO(apenwarr): Make it safe to update this with EditPrefs().
// Right now, you have to pass it in the initial prefs in Start(),
// which is the only code that actually uses the ControlURL value.
// It would be more consistent to restart controlclient
// automatically whenever this variable changes.
//
// Meanwhile, you have to provide this as part of
// Options.LegacyMigrationPrefs or Options.UpdatePrefs when
// calling Backend.Start().
func (v PrefsView) ControlURL() string { return v.ж.ControlURL }
// RouteAll specifies whether to accept subnets advertised by
// other nodes on the Tailscale network. Note that this does not
// include default routes (0.0.0.0/0 and ::/0), those are
// controlled by ExitNodeID/IP below.
func (v PrefsView) RouteAll() bool { return v.ж.RouteAll }
// ExitNodeID and ExitNodeIP specify the node that should be used
// as an exit node for internet traffic. At most one of these
// should be non-zero.
//
// The preferred way to express the chosen node is ExitNodeID, but
// in some cases it's not possible to use that ID (e.g. in the
// linux CLI, before tailscaled has a netmap). For those
// situations, we allow specifying the exit node by IP, and
// ipnlocal.LocalBackend will translate the IP into an ID when the
// node is found in the netmap.
//
// If the selected exit node doesn't exist (e.g. it's not part of
// the current tailnet), or it doesn't offer exit node services, a
// blackhole route will be installed on the local system to
// prevent any traffic escaping to the local network.
func (v PrefsView) ExitNodeID() tailcfg.StableNodeID { return v.ж.ExitNodeID }
func (v PrefsView) ExitNodeIP() netip.Addr { return v.ж.ExitNodeIP }
// AutoExitNode is an optional expression that specifies whether and how
// tailscaled should pick an exit node automatically.
//
// If specified, tailscaled will use an exit node based on the expression,
// and will re-evaluate the selection periodically as network conditions,
// available exit nodes, or policy settings change. A blackhole route will
// be installed to prevent traffic from escaping to the local network until
// an exit node is selected. It takes precedence over ExitNodeID and ExitNodeIP.
//
// If empty, tailscaled will not automatically select an exit node.
//
// If the specified expression is invalid or unsupported by the client,
// it falls back to the behavior of [AnyExitNode].
//
// As of 2025-07-02, the only supported value is [AnyExitNode].
// It's a string rather than a boolean to allow future extensibility
// (e.g., AutoExitNode = "mullvad" or AutoExitNode = "geo:us").
func (v PrefsView) AutoExitNode() ExitNodeExpression { return v.ж.AutoExitNode }
// InternalExitNodePrior is the most recently used ExitNodeID in string form. It is set by
// the backend on transition from exit node on to off and used by the
// backend.
//
// As an Internal field, it can't be set by LocalAPI clients, rather it is set indirectly
// when the ExitNodeID value is zero'd and via the set-use-exit-node-enabled endpoint.
func (v PrefsView) InternalExitNodePrior() tailcfg.StableNodeID { return v.ж.InternalExitNodePrior }
// ExitNodeAllowLANAccess indicates whether locally accessible subnets should be
// routed directly or via the exit node.
func (v PrefsView) ExitNodeAllowLANAccess() bool { return v.ж.ExitNodeAllowLANAccess }
// CorpDNS specifies whether to install the Tailscale network's
// DNS configuration, if it exists. It is the internal name for
// the "tailscale set --accept-dns=" flag.
func (v PrefsView) CorpDNS() bool { return v.ж.CorpDNS }
// RunSSH bool is whether this node should run an SSH
// server, permitting access to peers according to the
// policies as configured by the Tailnet's admin(s).
func (v PrefsView) RunSSH() bool { return v.ж.RunSSH }
// RunWebClient bool is whether this node should expose
// its web client over Tailscale at port 5252,
// permitting access to peers according to the
// policies as configured by the Tailnet's admin(s).
func (v PrefsView) RunWebClient() bool { return v.ж.RunWebClient }
// WantRunning indicates whether networking should be active on
// this node.
func (v PrefsView) WantRunning() bool { return v.ж.WantRunning }
// LoggedOut indicates whether the user intends to be logged out.
// There are other reasons we may be logged out, including no valid
// keys.
// We need to remember this state so that, on next startup, we can
// generate the "Login" vs "Connect" buttons correctly, without having
// to contact the server to confirm our nodekey status first.
func (v PrefsView) LoggedOut() bool { return v.ж.LoggedOut }
// ShieldsUp indicates whether to block all incoming connections,
// regardless of the control-provided packet filter. If false, we
// use the packet filter as provided. If true, we block incoming
// connections. This overrides tailcfg.Hostinfo's ShieldsUp.
func (v PrefsView) ShieldsUp() bool { return v.ж.ShieldsUp }
// AdvertiseTags specifies tags that should be applied to this node, for
// purposes of ACL enforcement. These can be referenced from the ACL policy
// document. Note that advertising a tag on the client doesn't guarantee
// that the control server will allow the node to adopt that tag.
func (v PrefsView) AdvertiseTags() views.Slice[string] { return views.SliceOf(v.ж.AdvertiseTags) }
// Hostname is the hostname to use for identifying the node. If
// not set, os.Hostname is used.
func (v PrefsView) Hostname() string { return v.ж.Hostname }
// NotepadURLs is a debugging setting that opens OAuth URLs in
// notepad.exe on Windows, rather than loading them in a browser.
//
// apenwarr 2020-04-29: Unfortunately this is still needed sometimes.
// Windows' default browser setting is sometimes screwy and this helps
// users narrow it down a bit.
func (v PrefsView) NotepadURLs() bool { return v.ж.NotepadURLs }
// ForceDaemon specifies whether a platform that normally
// operates in "client mode" (that is, requires an active user
// logged in with the GUI app running) should keep running after the
// GUI ends and/or the user logs out.
//
// The only current applicable platform is Windows. This
// forced Windows to go into "server mode" where Tailscale is
// running even with no users logged in. This might also be
// used for macOS in the future. This setting has no effect
// for Linux/etc, which always operate in daemon mode.
func (v PrefsView) ForceDaemon() bool { return v.ж.ForceDaemon }
// Egg is a optional debug flag.
func (v PrefsView) Egg() bool { return v.ж.Egg }
// AdvertiseRoutes specifies CIDR prefixes to advertise into the
// Tailscale network as reachable through the current
// node.
func (v PrefsView) AdvertiseRoutes() views.Slice[netip.Prefix] {
return views.SliceOf(v.ж.AdvertiseRoutes)
}
// AdvertiseServices specifies the list of services that this
// node can serve as a destination for. Note that an advertised
// service must still go through the approval process from the
// control server.
func (v PrefsView) AdvertiseServices() views.Slice[string] {
return views.SliceOf(v.ж.AdvertiseServices)
}
// Sync is whether this node should sync its configuration from
// the control plane. If unset, this defaults to true.
// This exists primarily for testing, to verify that netmap caching
// and offline operation work correctly.
func (v PrefsView) Sync() opt.Bool { return v.ж.Sync }
// NoSNAT specifies whether to source NAT traffic going to
// destinations in AdvertiseRoutes. The default is to apply source
// NAT, which makes the traffic appear to come from the router
// machine rather than the peer's Tailscale IP.
//
// Disabling SNAT requires additional manual configuration in your
// network to route Tailscale traffic back to the subnet relay
// machine.
//
// Linux and FreeBSD only.
func (v PrefsView) NoSNAT() bool { return v.ж.NoSNAT }
// NoStatefulFiltering specifies whether to apply stateful filtering when
// advertising routes in AdvertiseRoutes. The default is to not apply
// stateful filtering.
//
// To allow inbound connections from advertised routes, both NoSNAT and
// NoStatefulFiltering must be true.
//
// This is an opt.Bool because it was first added after NoSNAT, with a
// backfill based on the value of that parameter. The backfill has been
// removed since then, but the field remains an opt.Bool.
//
// Linux-only.
func (v PrefsView) NoStatefulFiltering() opt.Bool { return v.ж.NoStatefulFiltering }
// NetfilterMode specifies how much to manage netfilter rules for
// Tailscale, if at all.
func (v PrefsView) NetfilterMode() preftype.NetfilterMode { return v.ж.NetfilterMode }
// OperatorUser is the local machine user name who is allowed to
// operate tailscaled without being root or using sudo.
func (v PrefsView) OperatorUser() string { return v.ж.OperatorUser }
// ProfileName is the desired name of the profile. If empty, then the user's
// LoginName is used. It is only used for display purposes in the client UI
// and CLI.
func (v PrefsView) ProfileName() string { return v.ж.ProfileName }
// AutoUpdate sets the auto-update preferences for the node agent. See
// AutoUpdatePrefs docs for more details.
func (v PrefsView) AutoUpdate() AutoUpdatePrefs { return v.ж.AutoUpdate }
// AppConnector sets the app connector preferences for the node agent. See
// AppConnectorPrefs docs for more details.
func (v PrefsView) AppConnector() AppConnectorPrefs { return v.ж.AppConnector }
// PostureChecking enables the collection of information used for device
// posture checks.
//
// Note: this should be named ReportPosture, but it was shipped as
// PostureChecking in some early releases and this JSON field is written to
// disk, so we just keep its old name. (akin to CorpDNS which is an internal
// pref name that doesn't match the public interface)
func (v PrefsView) PostureChecking() bool { return v.ж.PostureChecking }
// NetfilterKind specifies what netfilter implementation to use.
//
// It can be "iptables", "nftables", or "" to auto-detect.
//
// Linux-only.
func (v PrefsView) NetfilterKind() string { return v.ж.NetfilterKind }
// RemoteConfig, if true, delegates full remote control of this node's
// prefs and LocalAPI to the tailnet admin via the control plane. When
// enabled, the control server can read and edit any of this node's
// prefs at any time, and invoke any LocalAPI endpoint on this node,
// without any further local consent (no CLI or GUI confirmation).
//
// This is an alternative to Tailscale's default per-feature double
// opt-in model, in which both the tailnet admin and the local machine
// owner must agree to each individual setting change. RemoteConfig is
// a single client-side "I trust the tailnet admin" switch that hands
// over full remote management of this node.
//
// Only enable this when the tailnet admin owns the machine (e.g. a
// corporate fleet device) or the local user has explicitly delegated
// full control to the tailnet admin. Do NOT enable this on personal
// or BYOD devices where the tailnet admin is not fully trusted.
func (v PrefsView) RemoteConfig() bool { return v.ж.RemoteConfig }
// DriveShares are the configured DriveShares, stored in increasing order
// by name.
func (v PrefsView) DriveShares() views.SliceView[*drive.Share, drive.ShareView] {
return views.SliceOfViews[*drive.Share, drive.ShareView](v.ж.DriveShares)
}
// RelayServerPort is the UDP port number for the relay server to bind to,
// on all interfaces. A non-nil zero value signifies a random unused port
// should be used. A nil value signifies relay server functionality
// should be disabled.
func (v PrefsView) RelayServerPort() views.ValuePointer[uint16] {
return views.ValuePointerOf(v.ж.RelayServerPort)
}
// RelayServerStaticEndpoints are static IP:port endpoints to advertise as
// candidates for relay connections. Only relevant when RelayServerPort is
// non-nil.
func (v PrefsView) RelayServerStaticEndpoints() views.Slice[netip.AddrPort] {
return views.SliceOf(v.ж.RelayServerStaticEndpoints)
}
// The Persist field is named 'Config' in the file for backward
// compatibility with earlier versions.
// TODO(apenwarr): We should move this out of here, it's not a pref.
//
// We can maybe do that once we're sure which module should persist
// it (backend or frontend?)
func (v PrefsView) Persist() persist.PersistView { return v.ж.Persist.View() }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _PrefsViewNeedsRegeneration = Prefs(struct {
ControlURL string
RouteAll bool
ExitNodeID tailcfg.StableNodeID
ExitNodeIP netip.Addr
AutoExitNode ExitNodeExpression
InternalExitNodePrior tailcfg.StableNodeID
ExitNodeAllowLANAccess bool
CorpDNS bool
RunSSH bool
RunWebClient bool
WantRunning bool
LoggedOut bool
ShieldsUp bool
AdvertiseTags []string
Hostname string
NotepadURLs bool
ForceDaemon bool
Egg bool
AdvertiseRoutes []netip.Prefix
AdvertiseServices []string
Sync opt.Bool
NoSNAT bool
NoStatefulFiltering opt.Bool
NetfilterMode preftype.NetfilterMode
OperatorUser string
ProfileName string
AutoUpdate AutoUpdatePrefs
AppConnector AppConnectorPrefs
PostureChecking bool
NetfilterKind string
RemoteConfig bool
DriveShares []*drive.Share
RelayServerPort *uint16
RelayServerStaticEndpoints []netip.AddrPort
Persist *persist.Persist
}{})
// View returns a read-only view of ServeConfig.
func (p *ServeConfig) View() ServeConfigView {
return ServeConfigView{ж: p}
}
// ServeConfigView provides a read-only view over ServeConfig.
//
// Its methods should only be called if `Valid()` returns true.
type ServeConfigView 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.
ж *ServeConfig
}
// Valid reports whether v's underlying value is non-nil.
func (v ServeConfigView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v ServeConfigView) AsStruct() *ServeConfig {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v ServeConfigView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v ServeConfigView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *ServeConfigView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x ServeConfig
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *ServeConfigView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x ServeConfig
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// TCP are the list of TCP port numbers that tailscaled should handle for
// the Tailscale IP addresses. (not subnet routers, etc)
func (v ServeConfigView) TCP() views.MapFn[uint16, *TCPPortHandler, TCPPortHandlerView] {
return views.MapFnOf(v.ж.TCP, func(t *TCPPortHandler) TCPPortHandlerView {
return t.View()
})
}
// Web maps from "$SNI_NAME:$PORT" to a set of HTTP handlers
// keyed by mount point ("/", "/foo", etc)
func (v ServeConfigView) Web() views.MapFn[HostPort, *WebServerConfig, WebServerConfigView] {
return views.MapFnOf(v.ж.Web, func(t *WebServerConfig) WebServerConfigView {
return t.View()
})
}
// Services maps from service name (in the form "svc:dns-label") to a ServiceConfig.
// Which describes the L3, L4, and L7 forwarding information for the service.
func (v ServeConfigView) Services() views.MapFn[tailcfg.ServiceName, *ServiceConfig, ServiceConfigView] {
return views.MapFnOf(v.ж.Services, func(t *ServiceConfig) ServiceConfigView {
return t.View()
})
}
// AllowFunnel is the set of SNI:port values for which funnel
// traffic is allowed, from trusted ingress peers.
func (v ServeConfigView) AllowFunnel() views.Map[HostPort, bool] {
return views.MapOf(v.ж.AllowFunnel)
}
// Foreground is a map of an IPN Bus session ID to an alternate foreground serve config that's valid for the
// life of that WatchIPNBus session ID. This allows the config to specify ephemeral configs that are used
// in the CLI's foreground mode to ensure ungraceful shutdowns of either the client or the LocalBackend does not
// expose ports that users are not aware of. In practice this contains any serve config set via 'tailscale
// serve' command run without the '--bg' flag. ServeConfig contained by Foreground is not expected itself to contain
// another Foreground block.
func (v ServeConfigView) Foreground() views.MapFn[string, *ServeConfig, ServeConfigView] {
return views.MapFnOf(v.ж.Foreground, func(t *ServeConfig) ServeConfigView {
return t.View()
})
}
// ETag is the checksum of the serve config that's populated
// by the LocalClient through the HTTP ETag header during a
// GetServeConfig request and is translated to an If-Match header
// during a SetServeConfig request.
func (v ServeConfigView) ETag() string { return v.ж.ETag }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _ServeConfigViewNeedsRegeneration = ServeConfig(struct {
TCP map[uint16]*TCPPortHandler
Web map[HostPort]*WebServerConfig
Services map[tailcfg.ServiceName]*ServiceConfig
AllowFunnel map[HostPort]bool
Foreground map[string]*ServeConfig
ETag string
}{})
// View returns a read-only view of ServiceConfig.
func (p *ServiceConfig) View() ServiceConfigView {
return ServiceConfigView{ж: p}
}
// ServiceConfigView provides a read-only view over ServiceConfig.
//
// Its methods should only be called if `Valid()` returns true.
type ServiceConfigView 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.
ж *ServiceConfig
}
// Valid reports whether v's underlying value is non-nil.
func (v ServiceConfigView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v ServiceConfigView) AsStruct() *ServiceConfig {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v ServiceConfigView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v ServiceConfigView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *ServiceConfigView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x ServiceConfig
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *ServiceConfigView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x ServiceConfig
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// TCP are the list of TCP port numbers that tailscaled should handle for
// the Tailscale IP addresses. (not subnet routers, etc)
func (v ServiceConfigView) TCP() views.MapFn[uint16, *TCPPortHandler, TCPPortHandlerView] {
return views.MapFnOf(v.ж.TCP, func(t *TCPPortHandler) TCPPortHandlerView {
return t.View()
})
}
// Web maps from "$SNI_NAME:$PORT" to a set of HTTP handlers
// keyed by mount point ("/", "/foo", etc)
func (v ServiceConfigView) Web() views.MapFn[HostPort, *WebServerConfig, WebServerConfigView] {
return views.MapFnOf(v.ж.Web, func(t *WebServerConfig) WebServerConfigView {
return t.View()
})
}
// Tun determines if the service should be using L3 forwarding (Tun mode).
func (v ServiceConfigView) Tun() bool { return v.ж.Tun }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _ServiceConfigViewNeedsRegeneration = ServiceConfig(struct {
TCP map[uint16]*TCPPortHandler
Web map[HostPort]*WebServerConfig
Tun bool
}{})
// View returns a read-only view of TCPPortHandler.
func (p *TCPPortHandler) View() TCPPortHandlerView {
return TCPPortHandlerView{ж: p}
}
// TCPPortHandlerView provides a read-only view over TCPPortHandler.
//
// Its methods should only be called if `Valid()` returns true.
type TCPPortHandlerView 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.
ж *TCPPortHandler
}
// Valid reports whether v's underlying value is non-nil.
func (v TCPPortHandlerView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v TCPPortHandlerView) AsStruct() *TCPPortHandler {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v TCPPortHandlerView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v TCPPortHandlerView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *TCPPortHandlerView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x TCPPortHandler
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *TCPPortHandlerView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x TCPPortHandler
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// HTTPS, if true, means that tailscaled should handle this connection as an
// HTTPS request as configured by ServeConfig.Web.
//
// It is mutually exclusive with TCPForward.
func (v TCPPortHandlerView) HTTPS() bool { return v.ж.HTTPS }
// HTTP, if true, means that tailscaled should handle this connection as an
// HTTP request as configured by ServeConfig.Web.
//
// It is mutually exclusive with TCPForward.
func (v TCPPortHandlerView) HTTP() bool { return v.ж.HTTP }
// TCPForward is the address to forward TCP connections to.
// It is either a host:port (e.g. "127.0.0.1:3128", "localhost:5432")
// or a Unix socket path prefixed with "unix:"
// (e.g. "unix:/var/run/app.sock" or "unix:relative.sock").
// Whether or not TLS is terminated by tailscaled depends on
// TerminateTLS.
//
// It is mutually exclusive with HTTPS.
func (v TCPPortHandlerView) TCPForward() string { return v.ж.TCPForward }
// TerminateTLS, if non-empty, means that tailscaled should terminate the
// TLS connections before forwarding them to TCPForward, permitting only the
// SNI name with this value. It is only used if TCPForward is non-empty.
// (the HTTPS mode uses ServeConfig.Web)
func (v TCPPortHandlerView) TerminateTLS() string { return v.ж.TerminateTLS }
// ProxyProtocol indicates whether to send a PROXY protocol header
// before forwarding the connection to TCPForward.
//
// This is only valid if TCPForward is non-empty.
func (v TCPPortHandlerView) ProxyProtocol() int { return v.ж.ProxyProtocol }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _TCPPortHandlerViewNeedsRegeneration = TCPPortHandler(struct {
HTTPS bool
HTTP bool
TCPForward string
TerminateTLS string
ProxyProtocol int
}{})
// View returns a read-only view of HTTPHandler.
func (p *HTTPHandler) View() HTTPHandlerView {
return HTTPHandlerView{ж: p}
}
// HTTPHandlerView provides a read-only view over HTTPHandler.
//
// Its methods should only be called if `Valid()` returns true.
type HTTPHandlerView 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.
ж *HTTPHandler
}
// Valid reports whether v's underlying value is non-nil.
func (v HTTPHandlerView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v HTTPHandlerView) AsStruct() *HTTPHandler {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v HTTPHandlerView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v HTTPHandlerView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *HTTPHandlerView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x HTTPHandler
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *HTTPHandlerView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x HTTPHandler
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// absolute path to directory or file to serve
func (v HTTPHandlerView) Path() string { return v.ж.Path }
// http://localhost:3000/, localhost:3030, 3030
func (v HTTPHandlerView) Proxy() string { return v.ж.Proxy }
// plaintext to serve (primarily for testing)
func (v HTTPHandlerView) Text() string { return v.ж.Text }
// peer capabilities to forward in grant header, e.g. example.com/cap/mon
func (v HTTPHandlerView) AcceptAppCaps() views.Slice[peercap.Cap] {
return views.SliceOf(v.ж.AcceptAppCaps)
}
// Redirect, if not empty, is the target URL to redirect requests to.
// By default, we redirect with HTTP 302 (Found) status.
// If Redirect starts with '<httpcode>:', then we use that status instead.
//
// The target URL supports the following expansion variables:
// - ${HOST}: replaced with the request's Host header value
// - ${REQUEST_URI}: replaced with the request's full URI (path and query string)
func (v HTTPHandlerView) Redirect() string { return v.ж.Redirect }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _HTTPHandlerViewNeedsRegeneration = HTTPHandler(struct {
Path string
Proxy string
Text string
AcceptAppCaps []peercap.Cap
Redirect string
}{})
// View returns a read-only view of WebServerConfig.
func (p *WebServerConfig) View() WebServerConfigView {
return WebServerConfigView{ж: p}
}
// WebServerConfigView provides a read-only view over WebServerConfig.
//
// Its methods should only be called if `Valid()` returns true.
type WebServerConfigView 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.
ж *WebServerConfig
}
// Valid reports whether v's underlying value is non-nil.
func (v WebServerConfigView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v WebServerConfigView) AsStruct() *WebServerConfig {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
// MarshalJSON implements [jsonv1.Marshaler].
func (v WebServerConfigView) MarshalJSON() ([]byte, error) {
return jsonv1.Marshal(v.ж)
}
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (v WebServerConfigView) MarshalJSONTo(enc *jsontext.Encoder) error {
return jsonv2.MarshalEncode(enc, v.ж)
}
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (v *WebServerConfigView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x WebServerConfig
if err := jsonv1.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (v *WebServerConfigView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
if v.ж != nil {
return errors.New("already initialized")
}
var x WebServerConfig
if err := jsonv2.UnmarshalDecode(dec, &x); err != nil {
return err
}
v.ж = &x
return nil
}
// mountPoint => handler
func (v WebServerConfigView) Handlers() views.MapFn[string, *HTTPHandler, HTTPHandlerView] {
return views.MapFnOf(v.ж.Handlers, func(t *HTTPHandler) HTTPHandlerView {
return t.View()
})
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _WebServerConfigViewNeedsRegeneration = WebServerConfig(struct {
Handlers map[string]*HTTPHandler
}{})
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package ipnstate captures the entire state of the Tailscale network.
//
// It's a leaf package so ipn, wgengine, and magicsock can all depend on it.
package ipnstate
import (
"fmt"
"html"
"io"
"log"
"net/netip"
"slices"
"sort"
"strings"
"time"
"tailscale.com/tailcfg"
"tailscale.com/tailcfg/nodecap"
"tailscale.com/tka"
"tailscale.com/types/key"
"tailscale.com/types/views"
"tailscale.com/util/dnsname"
"tailscale.com/version"
)
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=TKAPeer
// Status represents the entire state of the IPN network.
type Status struct {
// Version is the daemon's long version (see version.Long).
Version string
// TUN is whether /dev/net/tun (or equivalent kernel interface) is being
// used. If false, it's running in userspace mode.
TUN bool
// BackendState is an ipn.State string value:
// "NoState", "NeedsLogin", "NeedsMachineAuth", "Stopped",
// "Starting", "Running".
BackendState string
// HaveNodeKey is whether the current profile has a node key configured.
HaveNodeKey bool `json:",omitempty"`
AuthURL string // current URL provided by control to authorize client
TailscaleIPs []netip.Addr // Tailscale IP(s) assigned to this node
Self *PeerStatus
// ExitNodeStatus describes the current exit node.
// If nil, an exit node is not in use.
ExitNodeStatus *ExitNodeStatus `json:"ExitNodeStatus,omitempty"`
// Health contains health check problems.
// Empty means everything is good. (or at least that no known
// problems are detected)
Health []string
// This field is the legacy name of CurrentTailnet.MagicDNSSuffix.
//
// Deprecated: use CurrentTailnet.MagicDNSSuffix instead.
MagicDNSSuffix string
// CurrentTailnet is information about the tailnet that the node
// is currently connected to. When not connected, this field is nil.
CurrentTailnet *TailnetStatus
// CertDomains are the set of DNS names for which the control
// plane server will assist with provisioning TLS
// certificates. See SetDNSRequest for dns-01 ACME challenges
// for e.g. LetsEncrypt. These names are FQDNs without
// trailing periods, and without any "_acme-challenge." prefix.
CertDomains []string
// ExtraRecords contains extra DNS records to add to the DNS resolver.
ExtraRecords []tailcfg.DNSRecord
// Peer is the state of each peer, keyed by each peer's current public key.
Peer map[key.NodePublic]*PeerStatus
// User contains profile information about UserIDs referenced by
// PeerStatus.UserID, PeerStatus.AltSharerUserID, etc.
User map[tailcfg.UserID]tailcfg.UserProfile
// ClientVersion, when non-nil, contains information about the latest
// version of the Tailscale client that's available. Depending on
// the platform and client settings, it may not be available.
ClientVersion *tailcfg.ClientVersion
}
// TKAKey describes a key trusted by tailnet lock.
type TKAKey struct {
Kind string
Key key.TLPublic
Metadata map[string]string
Votes uint
}
// TKAPeer describes a peer and its tailnet lock details.
type TKAPeer struct {
Name string // DNS
ID tailcfg.NodeID
StableID tailcfg.StableNodeID
TailscaleIPs []netip.Addr // Tailscale IP(s) assigned to this node
NodeKey key.NodePublic
NodeKeySignature tka.NodeKeySignature
}
// TailnetLockStatus represents whether tailnet-lock is enabled,
// along with details about the locally-known state of the tailnet
// key authority.
type TailnetLockStatus struct {
// Enabled is true if tailnet lock is enabled.
Enabled bool
// Head describes the AUM hash of the leaf AUM. Head is nil
// if tailnet lock is not enabled.
Head *[32]byte
// PublicKey describes the node's tailnet-lock public key.
// It may be zero if the node has not logged in.
PublicKey key.TLPublic
// NodeKey describes the node's current node-key. This field is not
// populated if the node is not operating (i.e. waiting for a login).
NodeKey *key.NodePublic
// NodeKeySigned is true if our node is authorized by tailnet-lock.
NodeKeySigned bool
// NodeKeySignature is the current signature of this node's key.
NodeKeySignature *tka.NodeKeySignature
// TrustedKeys describes the keys currently trusted to make changes
// to tailnet-lock.
TrustedKeys []TKAKey
// VisiblePeers describes peers which are visible in the netmap that
// have valid Tailnet Lock signatures.
VisiblePeers []*TKAPeer
// FilteredPeers describes peers which were removed from the netmap
// (i.e. no connectivity) because they failed tailnet lock
// checks.
FilteredPeers []*TKAPeer
// StateID is a nonce associated with the tailnet lock authority,
// generated upon enablement. This field is not populated if the
// tailnet lock is disabled.
StateID uint64
}
// Deprecated: use [TailnetLockStatus] instead.
type NetworkLockStatus = TailnetLockStatus
// TailnetLockUpdate describes a change to tailnet-lock state.
type TailnetLockUpdate struct {
Hash [32]byte
Change string // values of tka.AUMKind.String()
// Raw contains the serialized AUM. The AUM is sent in serialized
// form to avoid transitive dependences bloating this package.
Raw []byte
}
// Deprecated: use [TailnetLockUpdate] instead.
type NetworkLockUpdate = TailnetLockUpdate
// TailnetStatus is information about a Tailscale network ("tailnet").
type TailnetStatus struct {
// Name is the name of the network that's currently in use.
Name string
// StableID is the stable, unique identifier of the tailnet, as used to
// identify the tailnet in the Tailscale API.
StableID tailcfg.StableTailnetID
// MagicDNSSuffix is the network's MagicDNS suffix for nodes
// in the network such as "userfoo.tailscale.net".
// There are no surrounding dots.
// MagicDNSSuffix should be populated regardless of whether a domain
// has MagicDNS enabled.
MagicDNSSuffix string
// MagicDNSEnabled is whether or not the network has MagicDNS enabled.
// Note that the current device may still not support MagicDNS if
// `--accept-dns=false` was used.
MagicDNSEnabled bool
}
// ExitNodeStatus describes the current exit node.
type ExitNodeStatus struct {
// ID is the exit node's ID.
ID tailcfg.StableNodeID
// Online is whether the exit node is alive.
Online bool
// TailscaleIPs are the exit node's IP addresses assigned to the node.
TailscaleIPs []netip.Prefix
}
func (s *Status) Peers() []key.NodePublic {
kk := make([]key.NodePublic, 0, len(s.Peer))
for k := range s.Peer {
kk = append(kk, k)
}
sort.Slice(kk, func(i, j int) bool { return kk[i].Less(kk[j]) })
return kk
}
type PeerStatusLite struct {
// NodeKey is this peer's public node key.
NodeKey key.NodePublic
// TxBytes/RxBytes are the total number of bytes transmitted to/received
// from this peer.
TxBytes, RxBytes int64
// LastHandshake is the last time a handshake succeeded with this peer. (Or
// we got key confirmation via the first data message, which is
// approximately the same thing.)
//
// The time.Time zero value means that no handshake has succeeded, at least
// since this peer was last known to WireGuard. (Tailscale removes peers
// from the wireguard peer that are idle.)
LastHandshake time.Time
}
// PeerStatus describes a peer node and its current state.
// WARNING: The fields in PeerStatus are merged by the AddPeer method in the StatusBuilder.
// When adding a new field to PeerStatus, you must update AddPeer to handle merging
// the new field. The AddPeer function is responsible for combining multiple updates
// to the same peer, and any new field that is not merged properly may lead to
// inconsistencies or lost data in the peer status.
type PeerStatus struct {
ID tailcfg.StableNodeID
NodeID tailcfg.NodeID
PublicKey key.NodePublic
HostName string // HostInfo's Hostname (not a DNS name or necessarily unique)
// DNSName is the Peer's FQDN. It ends with a dot.
// It has the form "host.<MagicDNSSuffix>."
DNSName string
OS string // HostInfo.OS
UserID tailcfg.UserID
// AltSharerUserID is the user who shared this node
// if it's different than UserID. Otherwise it's zero.
AltSharerUserID tailcfg.UserID `json:",omitempty"`
// TailscaleIPs are the IP addresses assigned to the node.
TailscaleIPs []netip.Addr
// AllowedIPs are IP addresses allowed to route to this node.
AllowedIPs *views.Slice[netip.Prefix] `json:",omitempty"`
// Tags are the list of ACL tags applied to this node.
// See tailscale.com/tailcfg#Node.Tags for more information.
Tags *views.Slice[string] `json:",omitempty"`
// PrimaryRoutes are the routes this node is currently the primary
// subnet router for, as determined by the control plane. It does
// not include the IPs in TailscaleIPs.
PrimaryRoutes *views.Slice[netip.Prefix] `json:",omitempty"`
// Endpoints:
Addrs []string
CurAddr string // one of Addrs, or unique if roaming
Relay string // DERP region
PeerRelay string // peer relay address (ip:port:vni)
RxBytes int64
TxBytes int64
Created time.Time // time registered with tailcontrol
LastWrite time.Time // time last packet sent
LastSeen time.Time // last seen to tailcontrol; only present if offline
LastHandshake time.Time // with local wireguard
Online bool // whether node is connected to the control plane
ExitNode bool // true if this is the currently selected exit node.
ExitNodeOption bool // true if this node can be an exit node (offered && approved)
// Active is whether the node was recently active. The
// definition is somewhat undefined but has historically and
// currently means that there was some packet sent to this
// peer in the past two minutes. That definition is subject to
// change.
Active bool
// PeerAPIURL are the URLs of the node's PeerAPI servers.
PeerAPIURL []string
// TaildropTargetStatus represents the node's eligibility to have files shared to it.
TaildropTarget TaildropTargetStatus
// Reason why this peer cannot receive files. Empty if CanReceiveFiles=true
NoFileSharingReason string
// 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"
// "funnel"
//
// Deprecated: use CapMap instead. See https://github.com/tailscale/tailscale/issues/11508
// Every value is Capabilities is also a key in CapMap, even if it
// has no values in that map.
Capabilities []nodecap.Cap `json:",omitempty"`
// CapMap is a map of capabilities to their values.
CapMap tailcfg.NodeCapMap `json:",omitempty"`
// SSH_HostKeys are the node's SSH host keys, if known.
SSH_HostKeys []string `json:"sshHostKeys,omitempty"`
// ShareeNode indicates this node exists in the netmap because
// it's owned by a shared-to user and that node might connect
// to us. These nodes should be hidden by "tailscale status"
// etc by default.
ShareeNode bool `json:",omitempty"`
// InNetworkMap means that this peer was seen in our latest network map.
// In theory, all of InNetworkMap and InMagicSock and InEngine should all be true.
InNetworkMap bool
// InMagicSock means that this peer is being tracked by magicsock.
// In theory, all of InNetworkMap and InMagicSock and InEngine should all be true.
InMagicSock bool
// InEngine means that this peer is tracked by the wireguard engine.
// In theory, all of InNetworkMap and InMagicSock and InEngine should all be true.
InEngine bool
// Expired means that this peer's node key has expired, based on either
// information from control or optimisically set on the client if the
// expiration time has passed.
Expired bool `json:",omitempty"`
// KeyExpiry, if present, is the time at which the node key expired or
// will expire.
KeyExpiry *time.Time `json:",omitempty"`
Location *tailcfg.Location `json:",omitempty"`
}
type TaildropTargetStatus int
const (
TaildropTargetUnknown TaildropTargetStatus = iota
TaildropTargetAvailable
TaildropTargetNoNetmapAvailable
TaildropTargetIpnStateNotRunning
TaildropTargetMissingCap
TaildropTargetOffline
TaildropTargetNoPeerInfo
TaildropTargetUnsupportedOS
TaildropTargetNoPeerAPI
TaildropTargetOwnedByOtherUser
)
// HasCap reports whether ps has the given capability.
func (ps *PeerStatus) HasCap(cap nodecap.Cap) bool {
return ps.CapMap.Contains(cap)
}
// IsRouter reports whether ps describes a router:
// a node that routes addresses besides its own.
// Examples: an exit node, a subnet router, an app connector, etc.
// It is the analogue of [tailcfg.Node.IsRouter].
func (ps *PeerStatus) IsRouter() bool {
// TODO(sfllaw): Keep this aligned with dbx.Node.IsSubnetRouter.
if ps.AllowedIPs == nil {
return false
}
for _, r := range ps.AllowedIPs.All() {
if !r.IsSingleIP() || !slices.Contains(ps.TailscaleIPs, r.Addr()) {
return true
}
}
return false
}
// IsTagged reports whether ps is tagged.
func (ps *PeerStatus) IsTagged() bool {
return ps.Tags != nil && ps.Tags.Len() > 0
}
// StatusBuilder is a request to construct a Status. A new StatusBuilder is
// passed to various subsystems which then call methods on it to populate state.
// Call its Status method to return the final constructed Status.
type StatusBuilder struct {
WantPeers bool // whether caller wants peers
locked bool
st Status
}
// MutateStatus calls f with the status to mutate.
//
// It may not assume other fields of status are already populated, and
// may not retain or write to the Status after f returns.
func (sb *StatusBuilder) MutateStatus(f func(*Status)) {
f(&sb.st)
}
// Status returns the status that has been built up so far from previous
// calls to MutateStatus, MutateSelfStatus, AddPeer, etc.
func (sb *StatusBuilder) Status() *Status {
sb.locked = true
return &sb.st
}
// MutateSelfStatus calls f with the PeerStatus of our own node to mutate.
//
// It may not assume other fields of status are already populated, and
// may not retain or write to the Status after f returns.
//
// MutateStatus acquires a lock so f must not call back into sb.
func (sb *StatusBuilder) MutateSelfStatus(f func(*PeerStatus)) {
if sb.st.Self == nil {
sb.st.Self = new(PeerStatus)
}
f(sb.st.Self)
}
// AddUser adds a user profile to the status.
func (sb *StatusBuilder) AddUser(id tailcfg.UserID, up tailcfg.UserProfileView) {
if sb.locked {
log.Printf("[unexpected] ipnstate: AddUser after Locked")
return
}
if sb.st.User == nil {
sb.st.User = make(map[tailcfg.UserID]tailcfg.UserProfile)
}
sb.st.User[id] = *up.AsStruct()
}
// AddIP adds a Tailscale IP address to the status.
func (sb *StatusBuilder) AddTailscaleIP(ip netip.Addr) {
if sb.locked {
log.Printf("[unexpected] ipnstate: AddIP after Locked")
return
}
sb.st.TailscaleIPs = append(sb.st.TailscaleIPs, ip)
}
// AddPeer adds a peer node to the status.
//
// Its PeerStatus is mixed with any previous status already added.
func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) {
if st == nil {
panic("nil PeerStatus")
}
if sb.locked {
log.Printf("[unexpected] ipnstate: AddPeer after Locked")
return
}
if sb.st.Peer == nil {
sb.st.Peer = make(map[key.NodePublic]*PeerStatus)
}
e, ok := sb.st.Peer[peer]
if !ok {
sb.st.Peer[peer] = st
st.PublicKey = peer
return
}
if v := st.ID; v != "" {
e.ID = v
}
if v := st.NodeID; v != 0 {
e.NodeID = v
}
if v := st.HostName; v != "" {
e.HostName = v
}
if v := st.DNSName; v != "" {
e.DNSName = v
}
if v := st.Relay; v != "" {
e.Relay = v
}
if v := st.PeerRelay; v != "" {
e.PeerRelay = v
}
if v := st.UserID; v != 0 {
e.UserID = v
}
if v := st.AltSharerUserID; v != 0 {
e.AltSharerUserID = v
}
if v := st.TailscaleIPs; v != nil {
e.TailscaleIPs = v
}
if v := st.PrimaryRoutes; v != nil && !v.IsNil() {
e.PrimaryRoutes = v
}
if v := st.AllowedIPs; v != nil && !v.IsNil() {
e.AllowedIPs = v
}
if v := st.Tags; v != nil && !v.IsNil() {
e.Tags = v
}
if v := st.OS; v != "" {
e.OS = st.OS
}
if v := st.SSH_HostKeys; v != nil {
e.SSH_HostKeys = v
}
if v := st.Addrs; v != nil {
e.Addrs = v
}
if v := st.CurAddr; v != "" {
e.CurAddr = v
}
if v := st.RxBytes; v != 0 {
e.RxBytes = v
}
if v := st.TxBytes; v != 0 {
e.TxBytes = v
}
if v := st.LastHandshake; !v.IsZero() {
e.LastHandshake = v
}
if v := st.Created; !v.IsZero() {
e.Created = v
}
if v := st.LastSeen; !v.IsZero() {
e.LastSeen = v
}
if v := st.LastWrite; !v.IsZero() {
e.LastWrite = v
}
if st.Online {
e.Online = true
}
if st.InNetworkMap {
e.InNetworkMap = true
}
if st.InMagicSock {
e.InMagicSock = true
}
if st.InEngine {
e.InEngine = true
}
if st.ExitNode {
e.ExitNode = true
}
if st.ExitNodeOption {
e.ExitNodeOption = true
}
if st.ShareeNode {
e.ShareeNode = true
}
if st.Active {
e.Active = true
}
if st.PeerAPIURL != nil {
e.PeerAPIURL = st.PeerAPIURL
}
if st.Expired {
e.Expired = true
}
if t := st.KeyExpiry; t != nil {
e.KeyExpiry = new(*t)
}
if v := st.CapMap; v != nil {
e.CapMap = v
}
if v := st.Capabilities; v != nil {
e.Capabilities = v
}
if v := st.TaildropTarget; v != TaildropTargetUnknown {
e.TaildropTarget = v
}
e.Location = st.Location
}
type StatusUpdater interface {
UpdateStatus(*StatusBuilder)
}
func (st *Status) WriteHTML(w io.Writer) {
f := func(format string, args ...any) { fmt.Fprintf(w, format, args...) }
f(`<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Tailscale State</title>
<style>
body { font-family: monospace; }
.owner { text-decoration: underline; }
.tailaddr { font-style: italic; }
.acenter { text-align: center; }
.aright { text-align: right; }
table, th, td { border: 1px solid black; border-spacing : 0; border-collapse : collapse; }
thead { background-color: #FFA500; }
th, td { padding: 5px; }
td { vertical-align: top; }
table tbody tr:nth-child(even) td { background-color: #f5f5f5; }
</style>
</head>
<body>
<h1>Tailscale State</h1>
`)
//f("<p><b>logid:</b> %s</p>\n", logid)
//f("<p><b>opts:</b> <code>%s</code></p>\n", html.EscapeString(fmt.Sprintf("%+v", opts)))
ips := make([]string, 0, len(st.TailscaleIPs))
for _, ip := range st.TailscaleIPs {
ips = append(ips, ip.String())
}
f("<p>Tailscale IP: %s", strings.Join(ips, ", "))
f("<table>\n<thead>\n")
f("<tr><th>Peer</th><th>OS</th><th>Node</th><th>Owner</th><th>Rx</th><th>Tx</th><th>Activity</th><th>Connection</th></tr>\n")
f("</thead>\n<tbody>\n")
now := time.Now()
var peers []*PeerStatus
for _, peer := range st.Peers() {
ps := st.Peer[peer]
if ps.ShareeNode {
continue
}
peers = append(peers, ps)
}
SortPeers(peers)
for _, ps := range peers {
var actAgo string
if !ps.LastWrite.IsZero() {
ago := now.Sub(ps.LastWrite)
actAgo = ago.Round(time.Second).String() + " ago"
if ago < 5*time.Minute {
actAgo = "<b>" + actAgo + "</b>"
}
}
var owner string
if up, ok := st.User[ps.UserID]; ok {
owner = up.LoginName
if i := strings.Index(owner, "@"); i != -1 {
owner = owner[:i]
}
}
hostName := dnsname.SanitizeHostname(ps.HostName)
dnsName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
if strings.EqualFold(dnsName, hostName) || ps.UserID != st.Self.UserID {
hostName = ""
}
var hostNameHTML string
if hostName != "" {
hostNameHTML = "<br>" + html.EscapeString(hostName)
}
var tailAddr string
if len(ps.TailscaleIPs) > 0 {
tailAddr = ps.TailscaleIPs[0].String()
}
f("<tr><td>%s</td><td class=acenter>%s</td>"+
"<td><b>%s</b>%s<div class=\"tailaddr\">%s</div></td><td class=\"acenter owner\">%s</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td>",
ps.PublicKey.ShortString(),
osEmoji(ps.OS),
html.EscapeString(dnsName),
hostNameHTML,
tailAddr,
html.EscapeString(owner),
ps.RxBytes,
ps.TxBytes,
actAgo,
)
f("<td>")
if ps.Active {
if ps.Relay != "" && ps.CurAddr == "" {
f("relay <b>%s</b>", html.EscapeString(ps.Relay))
} else if ps.CurAddr != "" {
f("direct <b>%s</b>", html.EscapeString(ps.CurAddr))
}
}
f("</td>") // end Addrs
f("</tr>\n")
}
f("</tbody>\n</table>\n")
f("</body>\n</html>\n")
}
func osEmoji(os string) string {
switch os {
case "linux":
return "🐧"
case "macOS":
return "🍎"
case "windows":
return "🖥️"
case "iOS":
return "📱"
case "tvOS":
return "🍎📺"
case "android":
return "🤖"
case "freebsd":
return "👿"
case "openbsd":
return "🐡"
case "illumos":
return "☀️"
case "solaris":
return "🌤️"
}
return "👽"
}
// PingResult contains response information for the "tailscale ping" subcommand,
// saying how Tailscale can reach a Tailscale IP or subnet-routed IP.
// See tailcfg.PingResponse for a related response that is sent back to control
// for remote diagnostic pings.
type PingResult struct {
IP string // ping destination
NodeIP string // Tailscale IP of node handling IP (different for subnet routers)
NodeName string // DNS name base or (possibly not unique) hostname
Err string
LatencySeconds float64
// Endpoint is a string of the form "{ip}:{port}" if direct UDP was used. It
// is not currently set for TSMP.
Endpoint string
// PeerRelay is a string of the form "{ip}:{port}:vni:{vni}" if a peer
// relay was used. It is not currently set for TSMP. Note that this field
// is not omitted during JSON encoding if it contains a zero value. This is
// done for consistency with the Endpoint field; this structure is exposed
// externally via localAPI, so we want to maintain the existing convention.
PeerRelay string
// DERPRegionID is non-zero DERP region ID if DERP was used.
// It is not currently set for TSMP pings.
DERPRegionID tailcfg.DERPRegionID
// DERPRegionCode is the three-letter region code
// corresponding to DERPRegionID.
// It is not currently set for TSMP pings.
DERPRegionCode string
// 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"`
// PeerAPIURL is the URL that was hit for pings of type "peerapi" (tailcfg.PingPeerAPI).
// It's of the form "http://ip:port" (or [ip]:port for IPv6).
PeerAPIURL string `json:",omitempty"`
// IsLocalIP is whether the ping request error is due to it being
// a ping to the local node.
IsLocalIP bool `json:",omitempty"`
// TODO(bradfitz): details like whether port mapping was used on either side? (Once supported)
}
func (pr *PingResult) ToPingResponse(pingType tailcfg.PingType) *tailcfg.PingResponse {
return &tailcfg.PingResponse{
Type: pingType,
IP: pr.IP,
NodeIP: pr.NodeIP,
NodeName: pr.NodeName,
Err: pr.Err,
LatencySeconds: pr.LatencySeconds,
Endpoint: pr.Endpoint,
PeerRelay: pr.PeerRelay,
DERPRegionID: pr.DERPRegionID,
DERPRegionCode: pr.DERPRegionCode,
PeerAPIPort: pr.PeerAPIPort,
IsLocalIP: pr.IsLocalIP,
}
}
// SortPeers sorts peers by either their DNS name, hostname, Tailscale IP,
// or ultimately their current public key.
func SortPeers(peers []*PeerStatus) {
slices.SortStableFunc(peers, (*PeerStatus).compare)
}
func (a *PeerStatus) compare(b *PeerStatus) int {
if a.DNSName != "" || b.DNSName != "" {
if v := strings.Compare(a.DNSName, b.DNSName); v != 0 {
return v
}
}
if a.HostName != "" || b.HostName != "" {
if v := strings.Compare(a.HostName, b.HostName); v != 0 {
return v
}
}
if len(a.TailscaleIPs) > 0 && len(b.TailscaleIPs) > 0 {
if v := a.TailscaleIPs[0].Compare(b.TailscaleIPs[0]); v != 0 {
return v
}
}
return a.PublicKey.Compare(b.PublicKey)
}
// DebugDERPRegionReport is the result of a "tailscale debug derp" command,
// to let people debug a custom DERP setup.
type DebugDERPRegionReport struct {
Info []string
Warnings []string
Errors []string
}
type SelfUpdateStatus string
const (
UpdateFinished SelfUpdateStatus = "UpdateFinished"
UpdateInProgress SelfUpdateStatus = "UpdateInProgress"
UpdateFailed SelfUpdateStatus = "UpdateFailed"
)
type UpdateProgress struct {
Status SelfUpdateStatus `json:"status,omitempty"`
Message string `json:"message,omitempty"`
Version string `json:"version,omitempty"`
}
func NewUpdateProgress(ps SelfUpdateStatus, msg string) UpdateProgress {
return UpdateProgress{
Status: ps,
Message: msg,
Version: version.Short(),
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
package ipnstate
import (
"net/netip"
"tailscale.com/tailcfg"
"tailscale.com/tka"
"tailscale.com/types/key"
)
// Clone makes a deep copy of TKAPeer.
// The result aliases no memory with the original.
func (src *TKAPeer) Clone() *TKAPeer {
if src == nil {
return nil
}
dst := new(TKAPeer)
*dst = *src
dst.TailscaleIPs = append(src.TailscaleIPs[:0:0], src.TailscaleIPs...)
dst.NodeKeySignature = *src.NodeKeySignature.Clone()
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _TKAPeerCloneNeedsRegeneration = TKAPeer(struct {
Name string
ID tailcfg.NodeID
StableID tailcfg.StableNodeID
TailscaleIPs []netip.Addr
NodeKey key.NodePublic
NodeKeySignature tka.NodeKeySignature
}{})
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package ipn
import (
"bytes"
"cmp"
"encoding/json"
"errors"
"fmt"
"log"
"net/netip"
"os"
"path/filepath"
"reflect"
"runtime"
"slices"
"strings"
"time"
"tailscale.com/atomicfile"
"tailscale.com/drive"
"tailscale.com/feature/buildfeatures"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/netaddr"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/opt"
"tailscale.com/types/persist"
"tailscale.com/types/preftype"
"tailscale.com/types/views"
"tailscale.com/util/dnsname"
"tailscale.com/util/syspolicy/pkey"
"tailscale.com/util/syspolicy/policyclient"
"tailscale.com/version"
)
// DefaultControlURL is the URL base of the control plane
// ("coordination server") for use when no explicit one is configured.
// The default control plane is the hosted version run by Tailscale.com.
const DefaultControlURL = "https://controlplane.tailscale.com"
var (
// ErrExitNodeIDAlreadySet is returned from (*Prefs).SetExitNodeIP when the
// Prefs.ExitNodeID field is already set.
ErrExitNodeIDAlreadySet = errors.New("cannot set ExitNodeIP when ExitNodeID is already set")
)
// IsLoginServerSynonym reports whether a URL is a drop-in replacement
// for the primary Tailscale login server.
func IsLoginServerSynonym(val any) bool {
return val == "https://login.tailscale.com" || val == "https://controlplane.tailscale.com"
}
// Prefs are the user modifiable settings of the Tailscale node agent.
// When you add a Pref to this struct, remember to add a corresponding
// field in MaskedPrefs, and check your field for equality in Prefs.Equals().
type Prefs struct {
// ControlURL is the URL of the control server to use.
//
// If empty, the default for new installs, DefaultControlURL
// is used. It's set non-empty once the daemon has been started
// for the first time.
//
// TODO(apenwarr): Make it safe to update this with EditPrefs().
// Right now, you have to pass it in the initial prefs in Start(),
// which is the only code that actually uses the ControlURL value.
// It would be more consistent to restart controlclient
// automatically whenever this variable changes.
//
// Meanwhile, you have to provide this as part of
// Options.LegacyMigrationPrefs or Options.UpdatePrefs when
// calling Backend.Start().
ControlURL string
// RouteAll specifies whether to accept subnets advertised by
// other nodes on the Tailscale network. Note that this does not
// include default routes (0.0.0.0/0 and ::/0), those are
// controlled by ExitNodeID/IP below.
RouteAll bool
// ExitNodeID and ExitNodeIP specify the node that should be used
// as an exit node for internet traffic. At most one of these
// should be non-zero.
//
// The preferred way to express the chosen node is ExitNodeID, but
// in some cases it's not possible to use that ID (e.g. in the
// linux CLI, before tailscaled has a netmap). For those
// situations, we allow specifying the exit node by IP, and
// ipnlocal.LocalBackend will translate the IP into an ID when the
// node is found in the netmap.
//
// If the selected exit node doesn't exist (e.g. it's not part of
// the current tailnet), or it doesn't offer exit node services, a
// blackhole route will be installed on the local system to
// prevent any traffic escaping to the local network.
ExitNodeID tailcfg.StableNodeID
ExitNodeIP netip.Addr
// AutoExitNode is an optional expression that specifies whether and how
// tailscaled should pick an exit node automatically.
//
// If specified, tailscaled will use an exit node based on the expression,
// and will re-evaluate the selection periodically as network conditions,
// available exit nodes, or policy settings change. A blackhole route will
// be installed to prevent traffic from escaping to the local network until
// an exit node is selected. It takes precedence over ExitNodeID and ExitNodeIP.
//
// If empty, tailscaled will not automatically select an exit node.
//
// If the specified expression is invalid or unsupported by the client,
// it falls back to the behavior of [AnyExitNode].
//
// As of 2025-07-02, the only supported value is [AnyExitNode].
// It's a string rather than a boolean to allow future extensibility
// (e.g., AutoExitNode = "mullvad" or AutoExitNode = "geo:us").
AutoExitNode ExitNodeExpression `json:",omitempty"`
// InternalExitNodePrior is the most recently used ExitNodeID in string form. It is set by
// the backend on transition from exit node on to off and used by the
// backend.
//
// As an Internal field, it can't be set by LocalAPI clients, rather it is set indirectly
// when the ExitNodeID value is zero'd and via the set-use-exit-node-enabled endpoint.
InternalExitNodePrior tailcfg.StableNodeID
// ExitNodeAllowLANAccess indicates whether locally accessible subnets should be
// routed directly or via the exit node.
ExitNodeAllowLANAccess bool
// CorpDNS specifies whether to install the Tailscale network's
// DNS configuration, if it exists. It is the internal name for
// the "tailscale set --accept-dns=" flag.
CorpDNS bool
// RunSSH bool is whether this node should run an SSH
// server, permitting access to peers according to the
// policies as configured by the Tailnet's admin(s).
RunSSH bool
// RunWebClient bool is whether this node should expose
// its web client over Tailscale at port 5252,
// permitting access to peers according to the
// policies as configured by the Tailnet's admin(s).
RunWebClient bool
// WantRunning indicates whether networking should be active on
// this node.
WantRunning bool
// LoggedOut indicates whether the user intends to be logged out.
// There are other reasons we may be logged out, including no valid
// keys.
// We need to remember this state so that, on next startup, we can
// generate the "Login" vs "Connect" buttons correctly, without having
// to contact the server to confirm our nodekey status first.
LoggedOut bool
// ShieldsUp indicates whether to block all incoming connections,
// regardless of the control-provided packet filter. If false, we
// use the packet filter as provided. If true, we block incoming
// connections. This overrides tailcfg.Hostinfo's ShieldsUp.
ShieldsUp bool
// AdvertiseTags specifies tags that should be applied to this node, for
// purposes of ACL enforcement. These can be referenced from the ACL policy
// document. Note that advertising a tag on the client doesn't guarantee
// that the control server will allow the node to adopt that tag.
AdvertiseTags []string
// Hostname is the hostname to use for identifying the node. If
// not set, os.Hostname is used.
Hostname string
// NotepadURLs is a debugging setting that opens OAuth URLs in
// notepad.exe on Windows, rather than loading them in a browser.
//
// apenwarr 2020-04-29: Unfortunately this is still needed sometimes.
// Windows' default browser setting is sometimes screwy and this helps
// users narrow it down a bit.
NotepadURLs bool
// ForceDaemon specifies whether a platform that normally
// operates in "client mode" (that is, requires an active user
// logged in with the GUI app running) should keep running after the
// GUI ends and/or the user logs out.
//
// The only current applicable platform is Windows. This
// forced Windows to go into "server mode" where Tailscale is
// running even with no users logged in. This might also be
// used for macOS in the future. This setting has no effect
// for Linux/etc, which always operate in daemon mode.
ForceDaemon bool `json:"ForceDaemon,omitempty"`
// Egg is a optional debug flag.
Egg bool `json:",omitempty"`
// The following block of options only have an effect on Linux.
// AdvertiseRoutes specifies CIDR prefixes to advertise into the
// Tailscale network as reachable through the current
// node.
AdvertiseRoutes []netip.Prefix
// AdvertiseServices specifies the list of services that this
// node can serve as a destination for. Note that an advertised
// service must still go through the approval process from the
// control server.
AdvertiseServices []string
// Sync is whether this node should sync its configuration from
// the control plane. If unset, this defaults to true.
// This exists primarily for testing, to verify that netmap caching
// and offline operation work correctly.
Sync opt.Bool
// NoSNAT specifies whether to source NAT traffic going to
// destinations in AdvertiseRoutes. The default is to apply source
// NAT, which makes the traffic appear to come from the router
// machine rather than the peer's Tailscale IP.
//
// Disabling SNAT requires additional manual configuration in your
// network to route Tailscale traffic back to the subnet relay
// machine.
//
// Linux and FreeBSD only.
NoSNAT bool
// NoStatefulFiltering specifies whether to apply stateful filtering when
// advertising routes in AdvertiseRoutes. The default is to not apply
// stateful filtering.
//
// To allow inbound connections from advertised routes, both NoSNAT and
// NoStatefulFiltering must be true.
//
// This is an opt.Bool because it was first added after NoSNAT, with a
// backfill based on the value of that parameter. The backfill has been
// removed since then, but the field remains an opt.Bool.
//
// Linux-only.
NoStatefulFiltering opt.Bool `json:",omitempty"`
// NetfilterMode specifies how much to manage netfilter rules for
// Tailscale, if at all.
NetfilterMode preftype.NetfilterMode
// OperatorUser is the local machine user name who is allowed to
// operate tailscaled without being root or using sudo.
OperatorUser string `json:",omitempty"`
// ProfileName is the desired name of the profile. If empty, then the user's
// LoginName is used. It is only used for display purposes in the client UI
// and CLI.
ProfileName string `json:",omitempty"`
// AutoUpdate sets the auto-update preferences for the node agent. See
// AutoUpdatePrefs docs for more details.
AutoUpdate AutoUpdatePrefs
// AppConnector sets the app connector preferences for the node agent. See
// AppConnectorPrefs docs for more details.
AppConnector AppConnectorPrefs
// PostureChecking enables the collection of information used for device
// posture checks.
//
// Note: this should be named ReportPosture, but it was shipped as
// PostureChecking in some early releases and this JSON field is written to
// disk, so we just keep its old name. (akin to CorpDNS which is an internal
// pref name that doesn't match the public interface)
PostureChecking bool
// NetfilterKind specifies what netfilter implementation to use.
//
// It can be "iptables", "nftables", or "" to auto-detect.
//
// Linux-only.
NetfilterKind string
// RemoteConfig, if true, delegates full remote control of this node's
// prefs and LocalAPI to the tailnet admin via the control plane. When
// enabled, the control server can read and edit any of this node's
// prefs at any time, and invoke any LocalAPI endpoint on this node,
// without any further local consent (no CLI or GUI confirmation).
//
// This is an alternative to Tailscale's default per-feature double
// opt-in model, in which both the tailnet admin and the local machine
// owner must agree to each individual setting change. RemoteConfig is
// a single client-side "I trust the tailnet admin" switch that hands
// over full remote management of this node.
//
// Only enable this when the tailnet admin owns the machine (e.g. a
// corporate fleet device) or the local user has explicitly delegated
// full control to the tailnet admin. Do NOT enable this on personal
// or BYOD devices where the tailnet admin is not fully trusted.
RemoteConfig bool
// DriveShares are the configured DriveShares, stored in increasing order
// by name.
DriveShares []*drive.Share
// RelayServerPort is the UDP port number for the relay server to bind to,
// on all interfaces. A non-nil zero value signifies a random unused port
// should be used. A nil value signifies relay server functionality
// should be disabled.
RelayServerPort *uint16 `json:",omitempty"`
// RelayServerStaticEndpoints are static IP:port endpoints to advertise as
// candidates for relay connections. Only relevant when RelayServerPort is
// non-nil.
RelayServerStaticEndpoints []netip.AddrPort `json:",omitempty"`
// The Persist field is named 'Config' in the file for backward
// compatibility with earlier versions.
// TODO(apenwarr): We should move this out of here, it's not a pref.
// We can maybe do that once we're sure which module should persist
// it (backend or frontend?)
Persist *persist.Persist `json:"Config"`
}
// AutoUpdatePrefs are the auto update settings for the node agent.
type AutoUpdatePrefs struct {
// Check specifies whether background checks for updates are enabled. When
// enabled, tailscaled will periodically check for available updates and
// notify the user about them.
Check bool
// Apply specifies whether background auto-updates are enabled. When
// enabled, tailscaled will apply available updates in the background.
// Check must also be set when Apply is set.
Apply opt.Bool
}
func (au1 AutoUpdatePrefs) Equals(au2 AutoUpdatePrefs) bool {
// This could almost be as easy as `au1.Apply == au2.Apply`, except that
// opt.Bool("") and opt.Bool("unset") should be treated as equal.
apply1, ok1 := au1.Apply.Get()
apply2, ok2 := au2.Apply.Get()
return au1.Check == au2.Check &&
apply1 == apply2 &&
ok1 == ok2
}
// AppConnectorPrefs are the app connector settings for the node agent.
type AppConnectorPrefs struct {
// Advertise specifies whether the app connector subsystem is advertising
// this node as a connector.
Advertise bool
}
// MaskedPrefs is a Prefs with an associated bitmask of which fields are set.
//
// Each FooSet field maps to a corresponding Foo field in Prefs. FooSet can be
// a struct, in which case inner fields of FooSet map to inner fields of Foo in
// Prefs (see AutoUpdateSet for example).
type MaskedPrefs struct {
Prefs
ControlURLSet bool `json:",omitempty"`
RouteAllSet bool `json:",omitempty"`
ExitNodeIDSet bool `json:",omitempty"`
ExitNodeIPSet bool `json:",omitempty"`
AutoExitNodeSet bool `json:",omitempty"`
InternalExitNodePriorSet bool `json:",omitempty"` // Internal; can't be set by LocalAPI clients
ExitNodeAllowLANAccessSet bool `json:",omitempty"`
CorpDNSSet bool `json:",omitempty"`
RunSSHSet bool `json:",omitempty"`
RunWebClientSet bool `json:",omitempty"`
WantRunningSet bool `json:",omitempty"`
LoggedOutSet bool `json:",omitempty"`
ShieldsUpSet bool `json:",omitempty"`
AdvertiseTagsSet bool `json:",omitempty"`
HostnameSet bool `json:",omitempty"`
NotepadURLsSet bool `json:",omitempty"`
ForceDaemonSet bool `json:",omitempty"`
EggSet bool `json:",omitempty"`
AdvertiseRoutesSet bool `json:",omitempty"`
AdvertiseServicesSet bool `json:",omitempty"`
SyncSet bool `json:",omitzero"`
NoSNATSet bool `json:",omitempty"`
NoStatefulFilteringSet bool `json:",omitempty"`
NetfilterModeSet bool `json:",omitempty"`
OperatorUserSet bool `json:",omitempty"`
ProfileNameSet bool `json:",omitempty"`
AutoUpdateSet AutoUpdatePrefsMask `json:",omitzero"`
AppConnectorSet bool `json:",omitempty"`
PostureCheckingSet bool `json:",omitempty"`
NetfilterKindSet bool `json:",omitempty"`
RemoteConfigSet bool `json:",omitempty"`
DriveSharesSet bool `json:",omitempty"`
RelayServerPortSet bool `json:",omitempty"`
RelayServerStaticEndpointsSet bool `json:",omitzero"`
}
// SetsInternal reports whether mp has any of the Internal*Set field bools set
// to true.
func (mp *MaskedPrefs) SetsInternal() bool {
return mp.InternalExitNodePriorSet
}
type AutoUpdatePrefsMask struct {
CheckSet bool `json:",omitempty"`
ApplySet bool `json:",omitempty"`
}
func (m AutoUpdatePrefsMask) Pretty(au AutoUpdatePrefs) string {
var fields []string
if m.CheckSet {
fields = append(fields, fmt.Sprintf("Check=%v", au.Check))
}
if m.ApplySet {
fields = append(fields, fmt.Sprintf("Apply=%v", au.Apply))
}
return strings.Join(fields, " ")
}
// ApplyEdits mutates p, assigning fields from m.Prefs for each MaskedPrefs
// Set field that's true.
func (p *Prefs) ApplyEdits(m *MaskedPrefs) {
if p == nil {
panic("can't edit nil Prefs")
}
pv := reflect.ValueOf(p).Elem()
mv := reflect.ValueOf(m).Elem()
mpv := reflect.ValueOf(&m.Prefs).Elem()
applyPrefsEdits(mpv, pv, maskFields(mv))
}
func applyPrefsEdits(src, dst reflect.Value, mask map[string]reflect.Value) {
for n, m := range mask {
switch m.Kind() {
case reflect.Bool:
if m.Bool() {
dst.FieldByName(n).Set(src.FieldByName(n))
}
case reflect.Struct:
applyPrefsEdits(src.FieldByName(n), dst.FieldByName(n), maskFields(m))
default:
panic(fmt.Sprintf("unsupported mask field kind %v", m.Kind()))
}
}
}
func maskFields(v reflect.Value) map[string]reflect.Value {
mask := make(map[string]reflect.Value)
for sf, fv := range v.Fields() {
if !strings.HasSuffix(sf.Name, "Set") {
continue
}
mask[strings.TrimSuffix(sf.Name, "Set")] = fv
}
return mask
}
// IsEmpty reports whether there are no masks set or if m is nil.
func (m *MaskedPrefs) IsEmpty() bool {
if m == nil {
return true
}
mv := reflect.ValueOf(m).Elem()
fields := mv.NumField()
for i := 1; i < fields; i++ {
if !mv.Field(i).IsZero() {
return false
}
}
return true
}
func (m *MaskedPrefs) Pretty() string {
if m == nil {
return "MaskedPrefs{<nil>}"
}
var sb strings.Builder
sb.WriteString("MaskedPrefs{")
mv := reflect.ValueOf(m).Elem()
mt := mv.Type()
mpv := reflect.ValueOf(&m.Prefs).Elem()
first := true
format := func(v reflect.Value) string {
switch v.Type().Kind() {
case reflect.String:
return "%s=%q"
case reflect.Slice:
// []string
if v.Type().Elem().Kind() == reflect.String {
return "%s=%q"
}
case reflect.Struct:
return "%s=%+v"
case reflect.Pointer:
if v.Type().Elem().Kind() == reflect.Struct {
return "%s=%+v"
}
}
return "%s=%v"
}
for i := 1; i < mt.NumField(); i++ {
name := mt.Field(i).Name
mf := mv.Field(i)
switch mf.Kind() {
case reflect.Bool:
if mf.Bool() {
if !first {
sb.WriteString(" ")
}
first = false
f := mpv.Field(i - 1)
fmt.Fprintf(&sb, format(f),
strings.TrimSuffix(name, "Set"),
f.Interface())
}
case reflect.Struct:
if mf.IsZero() {
continue
}
mpf := mpv.Field(i - 1)
// This would be much simpler with reflect.MethodByName("Pretty"),
// but using MethodByName disables some linker optimizations and
// makes our binaries much larger. See
// https://github.com/tailscale/tailscale/issues/10627#issuecomment-1861211945
//
// Instead, have this explicit switch by field name to do type
// assertions.
switch name {
case "AutoUpdateSet":
p := mf.Interface().(AutoUpdatePrefsMask).Pretty(mpf.Interface().(AutoUpdatePrefs))
fmt.Fprintf(&sb, "%s={%s}", strings.TrimSuffix(name, "Set"), p)
default:
panic(fmt.Sprintf("unexpected MaskedPrefs field %q", name))
}
}
}
sb.WriteString("}")
return sb.String()
}
// IsEmpty reports whether p is nil or pointing to a Prefs zero value.
func (p *Prefs) IsEmpty() bool { return p == nil || p.Equals(&Prefs{}) }
func (p PrefsView) Pretty() string { return p.ж.Pretty() }
func (p *Prefs) Pretty() string { return p.pretty(runtime.GOOS) }
func (p *Prefs) pretty(goos string) string {
var sb strings.Builder
sb.WriteString("Prefs{")
if buildfeatures.HasUseRoutes {
fmt.Fprintf(&sb, "ra=%v ", p.RouteAll)
}
if buildfeatures.HasDNS {
fmt.Fprintf(&sb, "dns=%v want=%v ", p.CorpDNS, p.WantRunning)
}
if buildfeatures.HasSSH && p.RunSSH {
sb.WriteString("ssh=true ")
}
if buildfeatures.HasWebClient && p.RunWebClient {
sb.WriteString("webclient=true ")
}
if p.LoggedOut {
sb.WriteString("loggedout=true ")
}
if p.Sync.EqualBool(false) {
sb.WriteString("sync=false ")
}
if p.ForceDaemon {
sb.WriteString("server=true ")
}
if p.NotepadURLs {
sb.WriteString("notepad=true ")
}
if p.ShieldsUp {
sb.WriteString("shields=true ")
}
if p.RemoteConfig {
sb.WriteString("remoteconfig=true ")
}
if buildfeatures.HasUseExitNode {
if p.ExitNodeIP.IsValid() {
fmt.Fprintf(&sb, "exit=%v lan=%t ", p.ExitNodeIP, p.ExitNodeAllowLANAccess)
} else if !p.ExitNodeID.IsZero() {
fmt.Fprintf(&sb, "exit=%v lan=%t ", p.ExitNodeID, p.ExitNodeAllowLANAccess)
}
if p.AutoExitNode.IsSet() {
fmt.Fprintf(&sb, "auto=%v ", p.AutoExitNode)
}
}
if buildfeatures.HasAdvertiseRoutes {
if len(p.AdvertiseRoutes) > 0 || goos == "linux" {
fmt.Fprintf(&sb, "routes=%v ", p.AdvertiseRoutes)
}
if len(p.AdvertiseRoutes) > 0 || p.NoSNAT {
fmt.Fprintf(&sb, "snat=%v ", !p.NoSNAT)
}
if len(p.AdvertiseRoutes) > 0 || p.NoStatefulFiltering.EqualBool(true) {
// Only print if we're advertising any routes, or the user has
// turned off stateful filtering (NoStatefulFiltering=true ⇒
// StatefulFiltering=false).
bb, _ := p.NoStatefulFiltering.Get()
fmt.Fprintf(&sb, "statefulFiltering=%v ", !bb)
}
}
if len(p.AdvertiseTags) > 0 {
fmt.Fprintf(&sb, "tags=%s ", strings.Join(p.AdvertiseTags, ","))
}
if len(p.AdvertiseServices) > 0 {
fmt.Fprintf(&sb, "services=%s ", strings.Join(p.AdvertiseServices, ","))
}
if goos == "linux" {
fmt.Fprintf(&sb, "nf=%v ", p.NetfilterMode)
}
if p.ControlURL != "" && p.ControlURL != DefaultControlURL {
fmt.Fprintf(&sb, "url=%q ", p.ControlURL)
}
if p.Hostname != "" {
fmt.Fprintf(&sb, "host=%q ", p.Hostname)
}
if p.OperatorUser != "" {
fmt.Fprintf(&sb, "op=%q ", p.OperatorUser)
}
if p.NetfilterKind != "" {
fmt.Fprintf(&sb, "netfilterKind=%s ", p.NetfilterKind)
}
if buildfeatures.HasClientUpdate {
sb.WriteString(p.AutoUpdate.Pretty())
}
if buildfeatures.HasAppConnectors {
sb.WriteString(p.AppConnector.Pretty())
}
if buildfeatures.HasRelayServer && p.RelayServerPort != nil {
fmt.Fprintf(&sb, "relayServerPort=%d ", *p.RelayServerPort)
}
if buildfeatures.HasRelayServer && len(p.RelayServerStaticEndpoints) > 0 {
fmt.Fprintf(&sb, "relayServerStaticEndpoints=%v ", p.RelayServerStaticEndpoints)
}
if p.Persist != nil {
sb.WriteString(p.Persist.Pretty())
} else {
sb.WriteString("Persist=nil")
}
sb.WriteString("}")
return sb.String()
}
func (p PrefsView) ToBytes() []byte {
return p.ж.ToBytes()
}
func (p *Prefs) ToBytes() []byte {
data, err := json.MarshalIndent(p, "", "\t")
if err != nil {
log.Fatalf("Prefs marshal: %v\n", err)
}
return data
}
func (p PrefsView) Equals(p2 PrefsView) bool {
return p.ж.Equals(p2.ж)
}
func (p *Prefs) Equals(p2 *Prefs) bool {
if p == p2 {
return true
}
if p == nil || p2 == nil {
return false
}
return p.ControlURL == p2.ControlURL &&
p.RouteAll == p2.RouteAll &&
p.ExitNodeID == p2.ExitNodeID &&
p.ExitNodeIP == p2.ExitNodeIP &&
p.AutoExitNode == p2.AutoExitNode &&
p.InternalExitNodePrior == p2.InternalExitNodePrior &&
p.ExitNodeAllowLANAccess == p2.ExitNodeAllowLANAccess &&
p.CorpDNS == p2.CorpDNS &&
p.RunSSH == p2.RunSSH &&
p.Sync.Normalized() == p2.Sync.Normalized() &&
p.RunWebClient == p2.RunWebClient &&
p.WantRunning == p2.WantRunning &&
p.LoggedOut == p2.LoggedOut &&
p.NotepadURLs == p2.NotepadURLs &&
p.ShieldsUp == p2.ShieldsUp &&
p.NoSNAT == p2.NoSNAT &&
p.NoStatefulFiltering == p2.NoStatefulFiltering &&
p.NetfilterMode == p2.NetfilterMode &&
p.OperatorUser == p2.OperatorUser &&
p.Hostname == p2.Hostname &&
p.ForceDaemon == p2.ForceDaemon &&
slices.Equal(p.AdvertiseRoutes, p2.AdvertiseRoutes) &&
slices.Equal(p.AdvertiseTags, p2.AdvertiseTags) &&
slices.Equal(p.AdvertiseServices, p2.AdvertiseServices) &&
p.Persist.Equals(p2.Persist) &&
p.ProfileName == p2.ProfileName &&
p.AutoUpdate.Equals(p2.AutoUpdate) &&
p.AppConnector == p2.AppConnector &&
p.PostureChecking == p2.PostureChecking &&
slices.EqualFunc(p.DriveShares, p2.DriveShares, drive.SharesEqual) &&
p.NetfilterKind == p2.NetfilterKind &&
p.RemoteConfig == p2.RemoteConfig &&
compareUint16Ptrs(p.RelayServerPort, p2.RelayServerPort) &&
slices.Equal(p.RelayServerStaticEndpoints, p2.RelayServerStaticEndpoints)
}
func (au AutoUpdatePrefs) Pretty() string {
if au.Apply.EqualBool(true) {
return "update=on "
}
if au.Check {
return "update=check "
}
return "update=off "
}
func (ap AppConnectorPrefs) Pretty() string {
if ap.Advertise {
return "appconnector=advertise "
}
return ""
}
func compareUint16Ptrs(a, b *uint16) bool {
if (a == nil) != (b == nil) {
return false
}
if a == nil {
return true
}
return *a == *b
}
// NewPrefs returns the default preferences to use.
func NewPrefs() *Prefs {
// Provide default values for options which might be missing
// from the json data for any reason. The json can still
// override them to false.
p := &Prefs{
// ControlURL is explicitly not set to signal that
// it's not yet configured, which relaxes the CLI "up"
// safety net features. It will get set to DefaultControlURL
// on first up. Or, if not, DefaultControlURL will be used
// later anyway.
ControlURL: "",
CorpDNS: true,
WantRunning: false,
NetfilterMode: preftype.NetfilterOn,
NoStatefulFiltering: opt.NewBool(true),
AutoUpdate: AutoUpdatePrefs{
Check: true,
Apply: opt.Bool("unset"),
},
}
p.RouteAll = p.DefaultRouteAll(runtime.GOOS)
return p
}
// ControlURLOrDefault returns the coordination server's URL base.
//
// If not configured, or if the configured value is a legacy name equivalent to
// the default, then DefaultControlURL is returned instead.
func (p PrefsView) ControlURLOrDefault(polc policyclient.Client) string {
return p.ж.ControlURLOrDefault(polc)
}
// ControlURLOrDefault returns the coordination server's URL base.
//
// If not configured, or if the configured value is a legacy name equivalent to
// the default, then DefaultControlURL is returned instead.
func (p *Prefs) ControlURLOrDefault(polc policyclient.Client) string {
controlURL, err := polc.GetString(pkey.ControlURL, p.ControlURL)
if err != nil {
controlURL = p.ControlURL
}
if controlURL != "" {
if controlURL != DefaultControlURL && IsLoginServerSynonym(controlURL) {
return DefaultControlURL
}
return controlURL
}
return DefaultControlURL
}
// DefaultRouteAll returns the default value of [Prefs.RouteAll] as a function
// of the platform it's running on.
func (p *Prefs) DefaultRouteAll(goos string) bool {
switch goos {
case "windows", "android", "ios":
return true
case "darwin":
// Only true for macAppStore and macsys, false for darwin tailscaled.
return version.IsSandboxedMacOS()
default:
return false
}
}
// AdminPageURL returns the admin web site URL for the current ControlURL.
func (p PrefsView) AdminPageURL(polc policyclient.Client) string { return p.ж.AdminPageURL(polc) }
// AdminPageURL returns the admin web site URL for the current ControlURL.
func (p *Prefs) AdminPageURL(polc policyclient.Client) string {
url := p.ControlURLOrDefault(polc)
if IsLoginServerSynonym(url) {
// TODO(crawshaw): In future release, make this https://console.tailscale.com
url = "https://login.tailscale.com"
}
return url + "/admin"
}
// AdvertisesExitNode reports whether p is advertising both the v4 and
// v6 /0 exit node routes.
func (p PrefsView) AdvertisesExitNode() bool { return p.ж.AdvertisesExitNode() }
// AdvertisesExitNode reports whether p is advertising both the v4 and
// v6 /0 exit node routes.
func (p *Prefs) AdvertisesExitNode() bool {
if p == nil {
return false
}
return tsaddr.ContainsExitRoutes(views.SliceOf(p.AdvertiseRoutes))
}
// SetAdvertiseExitNode mutates p (if non-nil) to add or remove the two
// /0 exit node routes.
func (p *Prefs) SetAdvertiseExitNode(runExit bool) {
if !buildfeatures.HasAdvertiseExitNode {
return
}
if p == nil {
return
}
all := p.AdvertiseRoutes
p.AdvertiseRoutes = p.AdvertiseRoutes[:0]
for _, r := range all {
if r.Bits() != 0 {
p.AdvertiseRoutes = append(p.AdvertiseRoutes, r)
}
}
if !runExit {
return
}
p.AdvertiseRoutes = append(p.AdvertiseRoutes,
netip.PrefixFrom(netaddr.IPv4(0, 0, 0, 0), 0),
netip.PrefixFrom(netip.IPv6Unspecified(), 0))
}
// peerWithTailscaleIP returns the peer in st with the provided
// Tailscale IP.
func peerWithTailscaleIP(st *ipnstate.Status, ip netip.Addr) (ps *ipnstate.PeerStatus, ok bool) {
for _, ps := range st.Peer {
if slices.Contains(ps.TailscaleIPs, ip) {
return ps, true
}
}
return nil, false
}
func isRemoteIP(st *ipnstate.Status, ip netip.Addr) bool {
return !slices.Contains(st.TailscaleIPs, ip)
}
// ClearExitNode sets the ExitNodeID and ExitNodeIP to their zero values.
func (p *Prefs) ClearExitNode() {
p.ExitNodeID = ""
p.ExitNodeIP = netip.Addr{}
p.AutoExitNode = ""
}
// ExitNodeLocalIPError is returned when the requested IP address for an exit
// node belongs to the local machine.
type ExitNodeLocalIPError struct {
hostOrIP string
}
func (e ExitNodeLocalIPError) Error() string {
return fmt.Sprintf("cannot use %s as an exit node as it is a local IP address to this machine", e.hostOrIP)
}
// exitNodeIPOfArg returns the IP address of the exit node based on the
// user-provided string.
func exitNodeIPOfArg(s string, st *ipnstate.Status) (ip netip.Addr, err error) {
if s == "" {
return ip, os.ErrInvalid
}
// If the string is a valid IP address, that's the exit node.
ip, err = netip.ParseAddr(s)
if err == nil {
if !isRemoteIP(st, ip) {
return ip, ExitNodeLocalIPError{s}
}
// If we're online already and have a netmap, double check that the IP
// address specified is valid.
if st.BackendState == "Running" {
ps, ok := peerWithTailscaleIP(st, ip)
if !ok {
return ip, fmt.Errorf("no node found in netmap with IP %v", ip)
}
if !ps.ExitNodeOption {
return ip, fmt.Errorf("node %v is not advertising an exit node", ip)
}
}
return ip, nil
}
// If the string is not a valid IP address, assume it's a hostname.
// Search the list of peers for a matching hostname.
if len(st.Peer) == 0 {
return ip, errors.New("cannot resolve exit node by hostname while Tailscale is starting up; " +
"please use its Tailscale IP address instead")
}
match := 0
for _, ps := range st.Peer {
// Compare to the peer name in three forms:
//
// - base name ("example")
// - FQDN ("example.tail1234.ts.net.")
// - FQDN sans dot ("example.tail1234.ts.net", as returned by `tailscale exit-node list`
// and the admin console)
//
fqdn := ps.DNSName
baseName := dnsname.TrimSuffix(fqdn, st.MagicDNSSuffix)
fqdnSansDot := dnsname.TrimSuffix(fqdn, ".")
if !strings.EqualFold(s, baseName) && !strings.EqualFold(s, fqdn) && !strings.EqualFold(s, fqdnSansDot) {
continue
}
match++
if len(ps.TailscaleIPs) == 0 {
return ip, fmt.Errorf("node %q has no Tailscale IP?", s)
}
if !ps.ExitNodeOption {
return ip, fmt.Errorf("node %q is not advertising an exit node", s)
}
ip = ps.TailscaleIPs[0]
}
switch match {
case 0:
return ip, fmt.Errorf("invalid value %q for --exit-node; must be IP or peer hostname", s)
case 1:
if !isRemoteIP(st, ip) {
return ip, ExitNodeLocalIPError{s}
}
return ip, nil
default:
return ip, fmt.Errorf("ambiguous exit node name %q", s)
}
}
// SetExitNodeIP validates and sets the ExitNodeIP from a user-provided string
// specifying either an IP address or a MagicDNS base name ("foo", as opposed to
// "foo.bar.beta.tailscale.net"). This method does not mutate ExitNodeID and
// will fail if ExitNodeID is already set.
func (p *Prefs) SetExitNodeIP(s string, st *ipnstate.Status) error {
if !p.ExitNodeID.IsZero() {
return ErrExitNodeIDAlreadySet
}
ip, err := exitNodeIPOfArg(s, st)
if err == nil {
p.ExitNodeIP = ip
}
return err
}
// ShouldSSHBeRunning reports whether the SSH server should be running based on
// the prefs.
func (p PrefsView) ShouldSSHBeRunning() bool {
return p.Valid() && p.ж.ShouldSSHBeRunning()
}
// ShouldSSHBeRunning reports whether the SSH server should be running based on
// the prefs.
func (p *Prefs) ShouldSSHBeRunning() bool {
return p.WantRunning && p.RunSSH
}
// ShouldWebClientBeRunning reports whether the web client server should be running based on
// the prefs.
func (p PrefsView) ShouldWebClientBeRunning() bool {
return p.Valid() && p.ж.ShouldWebClientBeRunning()
}
// ShouldWebClientBeRunning reports whether the web client server should be running based on
// the prefs.
func (p *Prefs) ShouldWebClientBeRunning() bool {
return p.WantRunning && p.RunWebClient
}
// PrefsFromBytes deserializes Prefs from a JSON blob b into base. Values in
// base are preserved, unless they are populated in the JSON blob.
func PrefsFromBytes(b []byte, base *Prefs) error {
if len(b) == 0 {
return nil
}
return json.Unmarshal(b, base)
}
func (p *Prefs) normalizeOptBools() {
if p.Sync == opt.ExplicitlyUnset {
p.Sync = ""
}
}
var jsonEscapedZero = []byte(`\u0000`)
// LoadPrefsWindows loads a legacy relaynode config file into Prefs with
// sensible migration defaults set. Windows-only.
func LoadPrefsWindows(filename string) (*Prefs, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("LoadPrefs open: %w", err) // err includes path
}
if bytes.Contains(data, jsonEscapedZero) {
// Tailscale 1.2.0 - 1.2.8 on Windows had a memory corruption bug
// in the backend process that ended up sending NULL bytes over JSON
// to the frontend which wrote them out to JSON files on disk.
// So if we see one, treat is as corrupt and the user will need
// to log in again. (better than crashing)
return nil, os.ErrNotExist
}
p := NewPrefs()
if err := PrefsFromBytes(data, p); err != nil {
return nil, fmt.Errorf("LoadPrefs(%q) decode: %w", filename, err)
}
return p, nil
}
func SavePrefs(filename string, p *Prefs) {
log.Printf("Saving prefs %v %v\n", filename, p.Pretty())
data := p.ToBytes()
os.MkdirAll(filepath.Dir(filename), 0700)
if err := atomicfile.WriteFile(filename, data, 0600); err != nil {
log.Printf("SavePrefs: %v\n", err)
}
}
// ProfileID is an auto-generated system-wide unique identifier for a login
// profile. It is a 4 character hex string like "1ab3".
type ProfileID string
// WindowsUserID is a userid (suitable for passing to ipnauth.LookupUserFromID
// or os/user.LookupId) but only set on Windows. It's empty on all other
// platforms, unless envknob.GOOS is in used, making Linux act like Windows for
// tests.
type WindowsUserID string
// NetworkProfile is a subset of netmap.NetworkMap
// that should be saved with each user profile.
type NetworkProfile struct {
MagicDNSName string
DomainName string
DisplayName string
}
// RequiresBackfill returns whether this object does not have all the data
// expected. This is because this struct is a later addition to LoginProfile and
// this method can be checked to see if it's been backfilled to the current
// expectation or not. Note that for now, it just checks if the struct is empty.
// In the future, if we have new optional fields, this method can be changed to
// do more explicit checks to return whether it's apt for a backfill or not.
func (n NetworkProfile) RequiresBackfill() bool {
return n == NetworkProfile{}
}
// DisplayNameOrDefault will always return a non-empty string.
// If there is a defined display name, it will return that.
// If they did not it will default to their domain name.
func (n NetworkProfile) DisplayNameOrDefault() string {
return cmp.Or(n.DisplayName, n.DomainName)
}
// LoginProfile represents a single login profile as managed
// by the ProfileManager.
type LoginProfile struct {
// ID is a unique identifier for this profile.
// It is assigned on creation and never changes.
// It may seem redundant to have both ID and UserProfile.ID
// but they are different things. UserProfile.ID may change
// over time (e.g. if a device is tagged).
ID ProfileID
// Name is the user-visible name of this profile.
// It is filled in from the UserProfile.LoginName field.
Name string
// NetworkProfile is a subset of netmap.NetworkMap that we
// store to remember information about the tailnet that this
// profile was logged in with.
//
// This field was added on 2023-11-17.
NetworkProfile NetworkProfile
// Key is the StateKey under which the profile is stored.
// It is assigned once at profile creation time and never changes.
Key StateKey
// UserProfile is the server provided UserProfile for this profile.
// This is updated whenever the server provides a new UserProfile.
UserProfile tailcfg.UserProfile
// NodeID is the NodeID of the node that this profile is logged into.
// This should be stable across tagging and untagging nodes.
// It may seem redundant to check against both the UserProfile.UserID
// and the NodeID. However the NodeID can change if the node is deleted
// from the admin panel.
NodeID tailcfg.StableNodeID
// LocalUserID is the user ID of the user who created this profile.
// It is only relevant on Windows where we have a multi-user system.
// It is assigned once at profile creation time and never changes.
LocalUserID WindowsUserID
// ControlURL is the URL of the control server that this profile is logged
// into.
ControlURL string
// Created is when this profile was first added to this client. It is
// stamped once at profile creation and never changes. It is used to sort
// the profile list with newest first; profiles created before this field
// existed have a zero value and sort after all stamped profiles.
Created time.Time `json:",omitzero"`
}
// Equals reports whether p and p2 are equal.
func (p LoginProfileView) Equals(p2 LoginProfileView) bool {
return p.ж.Equals(p2.ж)
}
// Equals reports whether p and p2 are equal.
func (p *LoginProfile) Equals(p2 *LoginProfile) bool {
if p == p2 {
return true
}
if p == nil || p2 == nil {
return false
}
return p.ID == p2.ID &&
p.Name == p2.Name &&
p.NetworkProfile == p2.NetworkProfile &&
p.Key == p2.Key &&
p.UserProfile.Equal(&p2.UserProfile) &&
p.NodeID == p2.NodeID &&
p.LocalUserID == p2.LocalUserID &&
p.ControlURL == p2.ControlURL
}
// ExitNodeExpression is a string that specifies how an exit node
// should be selected. An empty string means that no exit node
// should be selected.
//
// As of 2025-07-02, the only supported value is [AnyExitNode].
type ExitNodeExpression string
// AnyExitNode indicates that the exit node should be automatically
// selected from the pool of available exit nodes, excluding any
// disallowed by policy (e.g., [syspolicy.AllowedSuggestedExitNodes]).
// The exact implementation is subject to change, but exit nodes
// offering the best performance will be preferred.
const AnyExitNode ExitNodeExpression = "any"
// UnresolvedExitNodeID is a special [tailcfg.StableNodeID] value
// used as an exit node ID to install a blackhole route, preventing
// accidental non-exit-node usage until the [ExitNodeExpression]
// is evaluated and an actual exit node is selected.
//
// We use "auto:any" for compatibility with older, pre-[ExitNodeExpression]
// clients that have been using "auto:any" for this purpose for a long time.
const UnresolvedExitNodeID tailcfg.StableNodeID = "auto:any"
// IsSet reports whether the expression is non-empty and can be used
// to select an exit node.
func (e ExitNodeExpression) IsSet() bool {
return e != ""
}
const (
// AutoExitNodePrefix is the prefix used in [syspolicy.ExitNodeID] values and CLI
// to indicate that the string following the prefix is an [ExitNodeExpression].
AutoExitNodePrefix = "auto:"
)
// ParseAutoExitNodeString attempts to parse the given string
// as an [ExitNodeExpression].
//
// It returns the parsed expression and true on success,
// or an empty string and false if the input does not appear to be
// an [ExitNodeExpression] (i.e., it doesn't start with "auto:").
//
// It is mainly used to parse the [syspolicy.ExitNodeID] value
// when it is set to "auto:<expression>" (e.g., auto:any).
func ParseAutoExitNodeString[T ~string](s T) (_ ExitNodeExpression, ok bool) {
if expr, ok := strings.CutPrefix(string(s), AutoExitNodePrefix); ok && expr != "" {
return ExitNodeExpression(expr), true
}
return "", false
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package ipn
import (
"errors"
"fmt"
"iter"
"net"
"net/netip"
"net/url"
"runtime"
"slices"
"strconv"
"strings"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/tailcfg/nodecap"
"tailscale.com/tailcfg/peercap"
"tailscale.com/types/ipproto"
"tailscale.com/util/dnsname"
"tailscale.com/util/mak"
"tailscale.com/util/set"
)
// ServeConfigKey returns a StateKey that stores the
// JSON-encoded ServeConfig for a config profile.
func ServeConfigKey(profileID ProfileID) StateKey {
return StateKey("_serve/" + profileID)
}
// ServiceConfig contains the config information for a single service.
// it contains a bool to indicate if the service is in Tun mode (L3 forwarding).
// If the service is not in Tun mode, the service is configured by the L4 forwarding
// (TCP ports) and/or the L7 forwarding (http handlers) information.
type ServiceConfig struct {
// TCP are the list of TCP port numbers that tailscaled should handle for
// the Tailscale IP addresses. (not subnet routers, etc)
TCP map[uint16]*TCPPortHandler `json:",omitempty"`
// Web maps from "$SNI_NAME:$PORT" to a set of HTTP handlers
// keyed by mount point ("/", "/foo", etc)
Web map[HostPort]*WebServerConfig `json:",omitempty"`
// Tun determines if the service should be using L3 forwarding (Tun mode).
Tun bool `json:",omitempty"`
}
// ServeConfig is the JSON type stored in the StateStore for
// StateKey "_serve/$PROFILE_ID" as returned by ServeConfigKey.
type ServeConfig struct {
// TCP are the list of TCP port numbers that tailscaled should handle for
// the Tailscale IP addresses. (not subnet routers, etc)
TCP map[uint16]*TCPPortHandler `json:",omitempty"`
// Web maps from "$SNI_NAME:$PORT" to a set of HTTP handlers
// keyed by mount point ("/", "/foo", etc)
Web map[HostPort]*WebServerConfig `json:",omitempty"`
// Services maps from service name (in the form "svc:dns-label") to a ServiceConfig.
// Which describes the L3, L4, and L7 forwarding information for the service.
Services map[tailcfg.ServiceName]*ServiceConfig `json:",omitempty"`
// AllowFunnel is the set of SNI:port values for which funnel
// traffic is allowed, from trusted ingress peers.
AllowFunnel map[HostPort]bool `json:",omitempty"`
// Foreground is a map of an IPN Bus session ID to an alternate foreground serve config that's valid for the
// life of that WatchIPNBus session ID. This allows the config to specify ephemeral configs that are used
// in the CLI's foreground mode to ensure ungraceful shutdowns of either the client or the LocalBackend does not
// expose ports that users are not aware of. In practice this contains any serve config set via 'tailscale
// serve' command run without the '--bg' flag. ServeConfig contained by Foreground is not expected itself to contain
// another Foreground block.
Foreground map[string]*ServeConfig `json:",omitempty"`
// ETag is the checksum of the serve config that's populated
// by the LocalClient through the HTTP ETag header during a
// GetServeConfig request and is translated to an If-Match header
// during a SetServeConfig request.
ETag string `json:"-"`
}
// HostPort is an SNI name and port number, joined by a colon.
// There is no implicit port 443. It must contain a colon.
type HostPort string
// Port extracts just the port number from hp.
// An error is reported in the case that the hp does not
// have a valid numeric port ending.
func (hp HostPort) Port() (uint16, error) {
_, port, err := net.SplitHostPort(string(hp))
if err != nil {
return 0, err
}
port16, err := strconv.ParseUint(port, 10, 16)
if err != nil {
return 0, err
}
return uint16(port16), nil
}
// A FunnelConn wraps a net.Conn that is coming over a
// Funnel connection. It can be used to determine further
// information about the connection, like the source address
// and the target SNI name.
type FunnelConn struct {
// Conn is the underlying connection.
net.Conn
// Target is what was presented in the "Tailscale-Ingress-Target"
// HTTP header.
Target HostPort
// Src is the source address of the connection.
// This is the address of the client that initiated the
// connection, not the address of the Tailscale Funnel
// node which is relaying the connection. That address
// can be found in Conn.RemoteAddr.
Src netip.AddrPort
}
// WebServerConfig describes a web server's configuration.
type WebServerConfig struct {
Handlers map[string]*HTTPHandler // mountPoint => handler
}
// TCPPortHandler describes what to do when handling a TCP
// connection.
type TCPPortHandler struct {
// HTTPS, if true, means that tailscaled should handle this connection as an
// HTTPS request as configured by ServeConfig.Web.
//
// It is mutually exclusive with TCPForward.
HTTPS bool `json:",omitempty"`
// HTTP, if true, means that tailscaled should handle this connection as an
// HTTP request as configured by ServeConfig.Web.
//
// It is mutually exclusive with TCPForward.
HTTP bool `json:",omitempty"`
// TCPForward is the address to forward TCP connections to.
// It is either a host:port (e.g. "127.0.0.1:3128", "localhost:5432")
// or a Unix socket path prefixed with "unix:"
// (e.g. "unix:/var/run/app.sock" or "unix:relative.sock").
// Whether or not TLS is terminated by tailscaled depends on
// TerminateTLS.
//
// It is mutually exclusive with HTTPS.
TCPForward string `json:",omitempty"`
// TerminateTLS, if non-empty, means that tailscaled should terminate the
// TLS connections before forwarding them to TCPForward, permitting only the
// SNI name with this value. It is only used if TCPForward is non-empty.
// (the HTTPS mode uses ServeConfig.Web)
TerminateTLS string `json:",omitempty"`
// ProxyProtocol indicates whether to send a PROXY protocol header
// before forwarding the connection to TCPForward.
//
// This is only valid if TCPForward is non-empty.
ProxyProtocol int `json:",omitzero"`
}
// HTTPHandler is either a path or a proxy to serve.
type HTTPHandler struct {
// Exactly one of the following may be set.
Path string `json:",omitempty"` // absolute path to directory or file to serve
Proxy string `json:",omitempty"` // http://localhost:3000/, localhost:3030, 3030
Text string `json:",omitempty"` // plaintext to serve (primarily for testing)
AcceptAppCaps []peercap.Cap `json:",omitempty"` // peer capabilities to forward in grant header, e.g. example.com/cap/mon
// Redirect, if not empty, is the target URL to redirect requests to.
// By default, we redirect with HTTP 302 (Found) status.
// If Redirect starts with '<httpcode>:', then we use that status instead.
//
// The target URL supports the following expansion variables:
// - ${HOST}: replaced with the request's Host header value
// - ${REQUEST_URI}: replaced with the request's full URI (path and query string)
Redirect string `json:",omitempty"`
// TODO(bradfitz): bool to not enumerate directories? TTL on mapping for
// temporary ones? Error codes?
}
// WebHandlerExists reports whether if the ServeConfig Web handler exists for
// the given host:port and mount point.
func (sc *ServeConfig) WebHandlerExists(svcName tailcfg.ServiceName, hp HostPort, mount string) bool {
h := sc.GetWebHandler(svcName, hp, mount)
return h != nil
}
// GetWebHandler returns the HTTPHandler for the given host:port and mount point.
// It returns nil if the handler does not exist.
func (sc *ServeConfig) GetWebHandler(svcName tailcfg.ServiceName, hp HostPort, mount string) *HTTPHandler {
if sc == nil {
return nil
}
if svcName != "" {
if svc, ok := sc.Services[svcName]; ok && svc.Web != nil {
if webCfg, ok := svc.Web[hp]; ok {
return webCfg.Handlers[mount]
}
}
return nil
}
if sc.Web[hp] == nil {
return nil
}
return sc.Web[hp].Handlers[mount]
}
// GetTCPPortHandler returns the TCPPortHandler for the given port. If the port
// is not configured, nil is returned. Parameter svcName can be tailcfg.NoService
// for local serve or a service name for a service hosted on node.
func (sc *ServeConfig) GetTCPPortHandler(port uint16, svcName tailcfg.ServiceName) *TCPPortHandler {
if sc == nil {
return nil
}
if svcName != "" {
if svc, ok := sc.Services[svcName]; ok && svc != nil {
return svc.TCP[port]
}
return nil
}
return sc.TCP[port]
}
// HasPathHandler reports whether if ServeConfig has at least
// one path handler, including foreground configs.
func (sc *ServeConfig) HasPathHandler() bool {
if sc.Web != nil {
for _, webServerConfig := range sc.Web {
for _, httpHandler := range webServerConfig.Handlers {
if httpHandler.Path != "" {
return true
}
}
}
}
if sc.Services != nil {
for _, serviceConfig := range sc.Services {
if serviceConfig.Web != nil {
for _, webServerConfig := range serviceConfig.Web {
for _, httpHandler := range webServerConfig.Handlers {
if httpHandler.Path != "" {
return true
}
}
}
}
}
}
if sc.Foreground != nil {
for _, fgConfig := range sc.Foreground {
if fgConfig.HasPathHandler() {
return true
}
}
}
return false
}
// IsServingUnixAny reports whether ServeConfig is serving Unix targets on any
// port or web handler.
func (sc *ServeConfig) IsServingUnixAny() bool {
if sc == nil {
return false
}
for _, fgSrvCfg := range sc.Foreground {
if fgSrvCfg.IsServingUnixAny() {
return true
}
}
for _, ph := range sc.TCP {
if strings.HasPrefix(ph.TCPForward, "unix:") {
return true
}
}
for _, web := range sc.Web {
for _, h := range web.Handlers {
if strings.HasPrefix(h.Proxy, "unix:") {
return true
}
}
}
for _, svcCfg := range sc.Services {
for _, ph := range svcCfg.TCP {
if strings.HasPrefix(ph.TCPForward, "unix:") {
return true
}
}
for _, web := range svcCfg.Web {
for _, h := range web.Handlers {
if strings.HasPrefix(h.Proxy, "unix:") {
return true
}
}
}
}
return false
}
// IsTCPForwardingAny reports whether ServeConfig is currently forwarding in
// TCPForward mode on any port. This is exclusive of Web/HTTPS serving.
func (sc *ServeConfig) IsTCPForwardingAny() bool {
if sc == nil || len(sc.TCP) == 0 {
return false
}
for _, h := range sc.TCP {
if h.TCPForward != "" {
return true
}
}
return false
}
// IsTCPForwardingOnPort reports whether ServeConfig is currently forwarding
// in TCPForward mode on the given port for local or a service. svcName will
// either be noService (empty string) for local serve or a serviceName for service
// hosted on node. Notice TCPForwarding is exclusive with Web/HTTPS serving.
func (sc *ServeConfig) IsTCPForwardingOnPort(port uint16, svcName tailcfg.ServiceName) bool {
if sc == nil {
return false
}
if svcName != "" {
svc, ok := sc.Services[svcName]
if !ok || svc == nil {
return false
}
if svc.TCP[port] == nil {
return false
}
} else if sc.TCP[port] == nil {
return false
}
return !sc.IsServingWeb(port, svcName)
}
// IsServingWeb reports whether ServeConfig is currently serving Web (HTTP/HTTPS)
// on the given port for local or a service. svcName will be either tailcfg.NoService,
// or a serviceName for service hosted on node. This is exclusive with TCPForwarding.
func (sc *ServeConfig) IsServingWeb(port uint16, svcName tailcfg.ServiceName) bool {
return sc.IsServingHTTP(port, svcName) || sc.IsServingHTTPS(port, svcName)
}
// IsServingHTTPS reports whether ServeConfig is currently serving HTTPS on
// the given port for local or a service. svcName will be either tailcfg.NoService
// for local serve, or a serviceName for service hosted on node. This is exclusive
// with HTTP and TCPForwarding.
func (sc *ServeConfig) IsServingHTTPS(port uint16, svcName tailcfg.ServiceName) bool {
if sc == nil {
return false
}
var tcpHandlers map[uint16]*TCPPortHandler
if svcName != "" {
if svc := sc.Services[svcName]; svc != nil {
tcpHandlers = svc.TCP
}
} else {
tcpHandlers = sc.TCP
}
th := tcpHandlers[port]
if th == nil {
return false
}
return th.HTTPS
}
// IsServingHTTP reports whether ServeConfig is currently serving HTTP on the
// given port for local or a service. svcName will be either tailcfg.NoService for
// local serve, or a serviceName for service hosted on node. This is exclusive
// with HTTPS and TCPForwarding.
func (sc *ServeConfig) IsServingHTTP(port uint16, svcName tailcfg.ServiceName) bool {
if sc == nil {
return false
}
if svcName != "" {
if svc := sc.Services[svcName]; svc != nil {
if svc.TCP[port] != nil {
return svc.TCP[port].HTTP
}
}
return false
}
if sc.TCP[port] == nil {
return false
}
return sc.TCP[port].HTTP
}
// FindConfig finds a config that contains the given port, which can be
// the top level background config or an inner foreground one.
// The second result is true if it's foreground.
func (sc *ServeConfig) FindConfig(port uint16) (*ServeConfig, bool) {
if sc == nil {
return nil, false
}
if _, ok := sc.TCP[port]; ok {
return sc, false
}
for _, sc := range sc.Foreground {
if _, ok := sc.TCP[port]; ok {
return sc, true
}
}
return nil, false
}
// SetWebHandler sets the given HTTPHandler at the specified host, port,
// and mount in the serve config. sc.TCP is also updated to reflect web
// serving usage of the given port. The st argument is needed when setting
// a web handler for a service, otherwise it can be nil. mds is the Magic DNS
// suffix, which is used to recreate serve's host.
func (sc *ServeConfig) SetWebHandler(handler *HTTPHandler, host string, port uint16, mount string, useTLS bool, mds string) {
if sc == nil {
sc = new(ServeConfig)
}
tcpMap := &sc.TCP
webServerMap := &sc.Web
hostName := host
if svcName := tailcfg.AsServiceName(host); svcName != "" {
hostName = strings.Join([]string{svcName.WithoutPrefix(), mds}, ".")
svc, ok := sc.Services[svcName]
if !ok {
svc = new(ServiceConfig)
mak.Set(&sc.Services, svcName, svc)
}
tcpMap = &svc.TCP
webServerMap = &svc.Web
}
mak.Set(tcpMap, port, &TCPPortHandler{HTTPS: useTLS, HTTP: !useTLS})
hp := HostPort(net.JoinHostPort(hostName, strconv.Itoa(int(port))))
webCfg, ok := (*webServerMap)[hp]
if !ok {
webCfg = new(WebServerConfig)
mak.Set(webServerMap, hp, webCfg)
}
mak.Set(&webCfg.Handlers, mount, handler)
// TODO(tylersmalley): handle multiple web handlers from foreground mode
for k, v := range webCfg.Handlers {
if v == handler {
continue
}
// If the new mount point ends in / and another mount point
// shares the same prefix, remove the other handler.
// (e.g. /foo/ overwrites /foo)
// The opposite example is also handled.
m1 := strings.TrimSuffix(mount, "/")
m2 := strings.TrimSuffix(k, "/")
if m1 == m2 {
delete(webCfg.Handlers, k)
}
}
}
// SetTCPForwarding sets the fwdAddr to which to forward connections from the
// given port. fwdAddr is either an IP:port or "unix:/path/to/socket".
// If terminateTLS is true, TLS connections are terminated with only the given
// host name permitted before passing them to the fwdAddr.
//
// If proxyProtocol is non-zero, the corresponding PROXY protocol version
// header is sent before forwarding the connection.
func (sc *ServeConfig) SetTCPForwarding(port uint16, fwdAddr string, terminateTLS bool, proxyProtocol int, host string) {
if sc == nil {
sc = new(ServeConfig)
}
mak.Set(&sc.TCP, port, &TCPPortHandler{
TCPForward: fwdAddr,
ProxyProtocol: proxyProtocol, // can be 0
})
if terminateTLS {
sc.TCP[port].TerminateTLS = host
}
}
// SetTCPForwardingForService sets the fwdAddr to which to forward connections
// from the given port on the service. fwdAddr is either a host:port or
// "unix:/path" (absolute or relative). If terminateTLS is true, TLS connections
// are terminated, with only the FQDN that corresponds to the given service
// being permitted, before passing them to the fwdAddr.
func (sc *ServeConfig) SetTCPForwardingForService(port uint16, fwdAddr string, terminateTLS bool, svcName tailcfg.ServiceName, proxyProtocol int, magicDNSSuffix string) {
if sc == nil {
sc = new(ServeConfig)
}
svcConfig, ok := sc.Services[svcName]
if !ok {
svcConfig = new(ServiceConfig)
mak.Set(&sc.Services, svcName, svcConfig)
}
mak.Set(&svcConfig.TCP, port, &TCPPortHandler{
TCPForward: fwdAddr,
ProxyProtocol: proxyProtocol, // can be 0
})
if terminateTLS {
svcConfig.TCP[port].TerminateTLS = fmt.Sprintf("%s.%s", svcName.WithoutPrefix(), magicDNSSuffix)
}
}
// SetFunnel sets the sc.AllowFunnel value for the given host and port.
func (sc *ServeConfig) SetFunnel(host string, port uint16, setOn bool) {
if sc == nil {
sc = new(ServeConfig)
}
hp := HostPort(net.JoinHostPort(host, strconv.Itoa(int(port))))
// TODO(tylersmalley): should ensure there is no other conflicting funnel
// TODO(tylersmalley): add error handling for if toggling for existing sc
if setOn {
mak.Set(&sc.AllowFunnel, hp, true)
} else if _, exists := sc.AllowFunnel[hp]; exists {
delete(sc.AllowFunnel, hp)
// Clear map mostly for testing.
if len(sc.AllowFunnel) == 0 {
sc.AllowFunnel = nil
}
}
}
// RemoveWebHandler deletes the web handlers at all of the given mount points for the
// provided host and port in the serve config for the node (as opposed to a service).
// If cleanupFunnel is true, this also removes the funnel value for this port if no handlers remain.
func (sc *ServeConfig) RemoveWebHandler(host string, port uint16, mounts []string, cleanupFunnel bool) {
hp := HostPort(net.JoinHostPort(host, strconv.Itoa(int(port))))
// Delete existing handler, then cascade delete if empty.
for _, m := range mounts {
delete(sc.Web[hp].Handlers, m)
}
if len(sc.Web[hp].Handlers) == 0 {
delete(sc.Web, hp)
delete(sc.TCP, port)
if cleanupFunnel {
delete(sc.AllowFunnel, hp) // disable funnel if no mounts remain for the port
}
}
// Clear empty maps, mostly for testing.
if len(sc.Web) == 0 {
sc.Web = nil
}
if len(sc.TCP) == 0 {
sc.TCP = nil
}
if len(sc.AllowFunnel) == 0 {
sc.AllowFunnel = nil
}
}
// RemoveServiceWebHandler deletes the web handlers at all of the given mount points
// for the provided host and port in the serve config for the given service.
func (sc *ServeConfig) RemoveServiceWebHandler(svcName tailcfg.ServiceName, hostName string, port uint16, mounts []string) {
hp := HostPort(net.JoinHostPort(hostName, strconv.Itoa(int(port))))
svc, ok := sc.Services[svcName]
if !ok || svc == nil {
return
}
// Delete existing handler, then cascade delete if empty.
for _, m := range mounts {
delete(svc.Web[hp].Handlers, m)
}
if len(svc.Web[hp].Handlers) == 0 {
delete(svc.Web, hp)
delete(svc.TCP, port)
}
if len(svc.Web) == 0 && len(svc.TCP) == 0 {
delete(sc.Services, svcName)
}
if len(sc.Services) == 0 {
sc.Services = nil
}
}
// RemoveTCPForwarding deletes the TCP forwarding configuration for the given
// port from the serve config.
func (sc *ServeConfig) RemoveTCPForwarding(svcName tailcfg.ServiceName, port uint16) {
if svcName != "" {
if svc := sc.Services[svcName]; svc != nil {
delete(svc.TCP, port)
if len(svc.TCP) == 0 {
svc.TCP = nil
}
if len(svc.Web) == 0 && len(svc.TCP) == 0 {
delete(sc.Services, svcName)
}
if len(sc.Services) == 0 {
sc.Services = nil
}
}
return
}
delete(sc.TCP, port)
if len(sc.TCP) == 0 {
sc.TCP = nil
}
}
// IsFunnelOn reports whether if ServeConfig is currently allowing funnel
// traffic for any host:port.
//
// View version of ServeConfig.IsFunnelOn.
func (v ServeConfigView) IsFunnelOn() bool { return v.ж.IsFunnelOn() }
// IsFunnelOn reports whether any funnel endpoint is currently enabled for this node.
func (sc *ServeConfig) IsFunnelOn() bool {
if sc == nil {
return false
}
for _, b := range sc.AllowFunnel {
if b {
return true
}
}
for _, conf := range sc.Foreground {
if conf.IsFunnelOn() {
return true
}
}
return false
}
// CheckFunnelAccess checks whether Funnel access is allowed for the given node
// and port.
// It checks:
// 1. HTTPS is enabled on the tailnet
// 2. the node has the "funnel" nodeAttr
// 3. the port is allowed for Funnel
//
// The node arg should be the ipnstate.Status.Self node.
func CheckFunnelAccess(port uint16, node *ipnstate.PeerStatus) error {
if err := NodeCanFunnel(node); err != nil {
return err
}
return CheckFunnelPort(port, node)
}
// NodeCanFunnel returns an error if the given node is not configured to allow
// for Tailscale Funnel usage.
func NodeCanFunnel(node *ipnstate.PeerStatus) error {
if !node.HasCap(nodecap.HTTPS) {
return errors.New("Funnel not available; HTTPS must be enabled. See https://tailscale.com/s/https.")
}
if !node.HasCap(nodecap.Funnel) {
return errors.New("Funnel not available; \"funnel\" node attribute not set. See https://tailscale.com/s/no-funnel.")
}
return nil
}
// CheckFunnelPort checks whether the given port is allowed for Funnel.
// It uses the tailcfg.CapabilityFunnelPorts nodeAttr to determine the allowed
// ports.
func CheckFunnelPort(wantedPort uint16, node *ipnstate.PeerStatus) error {
deny := func(allowedPorts string) error {
if allowedPorts == "" {
return fmt.Errorf("port %d is not allowed for funnel", wantedPort)
}
return fmt.Errorf("port %d is not allowed for funnel; allowed ports are: %v", wantedPort, allowedPorts)
}
var portsStr string
parseAttr := func(attr string) (string, error) {
u, err := url.Parse(attr)
if err != nil {
return "", deny("")
}
portsStr := u.Query().Get("ports")
if portsStr == "" {
return "", deny("")
}
u.RawQuery = ""
if u.String() != string(nodecap.FunnelPorts) {
return "", deny("")
}
return portsStr, nil
}
for attr := range node.CapMap {
attr := string(attr)
if !strings.HasPrefix(attr, string(nodecap.FunnelPorts)) {
continue
}
var err error
portsStr, err = parseAttr(attr)
if err != nil {
return err
}
break
}
if portsStr == "" {
for attr := range node.CapMap {
attr := string(attr)
if !strings.HasPrefix(attr, string(nodecap.FunnelPorts)) {
continue
}
var err error
portsStr, err = parseAttr(attr)
if err != nil {
return err
}
break
}
}
if portsStr == "" {
return deny("")
}
wantedPortString := strconv.Itoa(int(wantedPort))
for ps := range strings.SplitSeq(portsStr, ",") {
if ps == "" {
continue
}
first, last, ok := strings.Cut(ps, "-")
if !ok {
if first == wantedPortString {
return nil
}
continue
}
fp, err := strconv.ParseUint(first, 10, 16)
if err != nil {
continue
}
lp, err := strconv.ParseUint(last, 10, 16)
if err != nil {
continue
}
pr := tailcfg.PortRange{First: uint16(fp), Last: uint16(lp)}
if pr.Contains(wantedPort) {
return nil
}
}
return deny(portsStr)
}
// ExpandProxyTargetValue expands the supported target values to be proxied
// allowing for input values to be a port number, a partial URL, or a full URL
// including a path. If it's for a service, remote addresses are allowed and
// there doesn't have to be a port specified.
//
// examples:
// - 3000
// - localhost:3000
// - tcp://localhost:3000
// - http://localhost:3000
// - https://localhost:3000
// - https-insecure://localhost:3000
// - https-insecure://localhost:3000/foo
// - https://tailscale.com
func ExpandProxyTargetValue(target string, supportedSchemes []string, defaultScheme string) (string, error) {
const host = "127.0.0.1"
// empty target is invalid
if target == "" {
return "", fmt.Errorf("empty target")
}
// support target being a port number
if port, err := strconv.ParseUint(target, 10, 16); err == nil {
return fmt.Sprintf("%s://%s:%d", defaultScheme, host, port), nil
}
// handle unix: scheme specially - it doesn't use standard URL format
if strings.HasPrefix(target, "unix:") {
if !slices.Contains(supportedSchemes, "unix") {
return "", fmt.Errorf("unix sockets are not supported for this target type")
}
if runtime.GOOS == "windows" {
return "", fmt.Errorf("unix socket serve target is not supported on Windows")
}
path := strings.TrimPrefix(target, "unix:")
if path == "" {
return "", fmt.Errorf("unix socket path cannot be empty")
}
return target, nil
}
hasScheme := true
// prepend scheme if not present
if !strings.Contains(target, "://") {
target = defaultScheme + "://" + target
hasScheme = false
}
// make sure we can parse the target
u, err := url.ParseRequestURI(target)
if err != nil {
return "", fmt.Errorf("invalid URL %w", err)
}
// ensure a supported scheme
if !slices.Contains(supportedSchemes, u.Scheme) {
return "", fmt.Errorf("must be a URL starting with one of the supported schemes: %v", supportedSchemes)
}
// validate port according to host.
if u.Hostname() == "localhost" || u.Hostname() == "127.0.0.1" || u.Hostname() == "::1" {
// require port for localhost targets
if u.Port() == "" {
return "", fmt.Errorf("port required for localhost target %q", target)
}
} else {
validHN := dnsname.ValidHostname(u.Hostname()) == nil
validIP := net.ParseIP(u.Hostname()) != nil
if !validHN && !validIP {
return "", fmt.Errorf("invalid hostname or IP address %q", u.Hostname())
}
// require scheme for non-localhost targets
if !hasScheme {
return "", fmt.Errorf("non-localhost target %q must include a scheme", target)
}
}
port, err := strconv.ParseUint(u.Port(), 10, 16)
if err != nil || port == 0 {
if u.Port() == "" {
return u.String(), nil // allow no port for remote destinations
}
return "", fmt.Errorf("invalid port %q", u.Port())
}
u.Host = net.JoinHostPort(u.Hostname(), strconv.Itoa(int(port)))
return u.String(), nil
}
// TCPs returns an iterator over both background and foreground TCP
// listeners.
//
// The key is the port number.
func (v ServeConfigView) TCPs() iter.Seq2[uint16, TCPPortHandlerView] {
return func(yield func(uint16, TCPPortHandlerView) bool) {
for k, v := range v.TCP().All() {
if !yield(k, v) {
return
}
}
for _, conf := range v.Foreground().All() {
for k, v := range conf.TCP().All() {
if !yield(k, v) {
return
}
}
}
}
}
// Webs returns an iterator over both background and foreground Web configurations.
func (v ServeConfigView) Webs() iter.Seq2[HostPort, WebServerConfigView] {
return func(yield func(HostPort, WebServerConfigView) bool) {
for k, v := range v.Web().All() {
if !yield(k, v) {
return
}
}
for _, conf := range v.Foreground().All() {
for k, v := range conf.Web().All() {
if !yield(k, v) {
return
}
}
}
for _, service := range v.Services().All() {
for k, v := range service.Web().All() {
if !yield(k, v) {
return
}
}
}
}
}
// FindServiceTCP return the TCPPortHandlerView for the given service name and port.
func (v ServeConfigView) FindServiceTCP(svcName tailcfg.ServiceName, port uint16) (res TCPPortHandlerView, ok bool) {
svcCfg, ok := v.Services().GetOk(svcName)
if !ok {
return res, ok
}
return svcCfg.TCP().GetOk(port)
}
// FindServiceWeb returns the web handler for the service's host-port.
func (v ServeConfigView) FindServiceWeb(svcName tailcfg.ServiceName, hp HostPort) (res WebServerConfigView, ok bool) {
if svcCfg, ok := v.Services().GetOk(svcName); ok {
if res, ok := svcCfg.Web().GetOk(hp); ok {
return res, ok
}
}
return res, ok
}
// FindTCP returns the first TCP that matches with the given port. It
// prefers a foreground match first followed by a background search if none
// existed.
func (v ServeConfigView) FindTCP(port uint16) (res TCPPortHandlerView, ok bool) {
res, ok = v.FindForegroundTCP(port)
if ok {
return res, ok
}
return v.TCP().GetOk(port)
}
// FindWeb returns the first Web that matches with the given HostPort. It
// prefers a foreground match first followed by a background search if none
// existed.
func (v ServeConfigView) FindWeb(hp HostPort) (res WebServerConfigView, ok bool) {
for _, conf := range v.Foreground().All() {
if res, ok := conf.Web().GetOk(hp); ok {
return res, ok
}
}
return v.Web().GetOk(hp)
}
// FindForegroundTCP returns the first foreground TCP handler matching the input
// port.
func (v ServeConfigView) FindForegroundTCP(port uint16) (res TCPPortHandlerView, ok bool) {
for _, conf := range v.Foreground().All() {
if res, ok := conf.TCP().GetOk(port); ok {
return res, ok
}
}
return res, false
}
// HasAllowFunnel returns whether this config has at least one AllowFunnel
// set in the background or foreground configs.
func (v ServeConfigView) HasAllowFunnel() bool {
if v.AllowFunnel().Len() > 0 {
return true
}
for _, conf := range v.Foreground().All() {
if conf.AllowFunnel().Len() > 0 {
return true
}
}
return false
}
// FindFunnel reports whether target exists in either the background AllowFunnel
// or any of the foreground configs.
func (v ServeConfigView) HasFunnelForTarget(target HostPort) bool {
if v.AllowFunnel().Get(target) {
return true
}
for _, conf := range v.Foreground().All() {
if conf.AllowFunnel().Get(target) {
return true
}
}
return false
}
// ServicePortRange returns the list of tailcfg.ProtoPortRange that represents
// the proto/ports pairs that are being served by the service.
//
// Right now Tun mode is the only thing supports UDP, otherwise serve only supports TCP.
func (v ServiceConfigView) ServicePortRange() []tailcfg.ProtoPortRange {
if v.Tun() {
// If the service is in Tun mode, means service accept TCP/UDP on all ports.
return []tailcfg.ProtoPortRange{{Ports: tailcfg.PortRangeAny}}
}
tcp := int(ipproto.TCP)
// Deduplicate the ports.
servePorts := make(set.Set[uint16])
for port := range v.TCP().All() {
if port > 0 {
servePorts.Add(uint16(port))
}
}
dedupedServePorts := servePorts.Slice()
slices.Sort(dedupedServePorts)
var ranges []tailcfg.ProtoPortRange
for _, p := range dedupedServePorts {
if n := len(ranges); n > 0 && p == ranges[n-1].Ports.Last+1 {
ranges[n-1].Ports.Last = p
continue
}
ranges = append(ranges, tailcfg.ProtoPortRange{
Proto: tcp,
Ports: tailcfg.PortRange{
First: p,
Last: p,
},
})
}
return ranges
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package ipn
import (
"bytes"
"context"
"errors"
"fmt"
"net"
"strconv"
"tailscale.com/health"
)
// ErrStateNotExist is returned by StateStore.ReadState when the
// requested state ID doesn't exist.
var ErrStateNotExist = errors.New("no state with given ID")
const (
// MachineKeyStateKey is the key under which we store the machine key,
// in its key.NodePrivate.MarshalText representation.
MachineKeyStateKey = StateKey("_machinekey")
// LegacyGlobalDaemonStateKey is the ipn.StateKey that tailscaled
// loads on startup.
//
// We have to support multiple state keys for other OSes (Windows in
// particular), but right now Unix daemons run with a single
// node-global state. To keep open the option of having per-user state
// later, the global state key doesn't look like a username.
//
// As of 2022-10-21, it has been superseded by profiles and is no longer
// written to disk. It is only read at startup when there are no profiles,
// to migrate the state to the "default" profile.
// The existing state is left on disk in case the user downgrades to an
// older version of Tailscale that doesn't support profiles. We can
// remove this in a future release.
LegacyGlobalDaemonStateKey = StateKey("_daemon")
// ServerModeStartKey's value, if non-empty, is the value of a
// StateKey containing the prefs to start with which to start the
// server.
//
// For example, the value might be "user-1234", meaning the
// the server should start with the Prefs JSON loaded from
// StateKey "user-1234".
ServerModeStartKey = StateKey("server-mode-start-key")
// KnownProfilesStateKey is the key under which we store the list of
// known profiles. The value is a JSON-encoded []LoginProfile.
KnownProfilesStateKey = StateKey("_profiles")
// CurrentProfileStateKey is the key under which we store the current
// profile.
CurrentProfileStateKey = StateKey("_current-profile")
// TaildropReceivedKey is the key to indicate whether any taildrop file
// has ever been received (even if partially).
// Any non-empty value indicates that at least one file has been received.
TaildropReceivedKey = StateKey("_taildrop-received")
)
// StateStoreHealth is a Warnable set when store.New fails at startup. If
// unhealthy, we block all login attempts and return a health message in status
// responses.
var StateStoreHealth = health.Register(&health.Warnable{
Code: "state-store-health",
Severity: health.SeverityHigh,
Title: "Tailscale state store failed to initialize",
Text: func(args health.Args) string {
return fmt.Sprintf("State store failed to initialize, Tailscale will not work until this is resolved. See https://tailscale.com/s/state-store-init-error. Error: %s", args[health.ArgError])
},
ImpactsConnectivity: true,
})
// CurrentProfileID returns the StateKey that stores the
// current profile ID. The value is a JSON-encoded LoginProfile.
// If the userID is empty, the key returned is CurrentProfileStateKey,
// otherwise it is "_current/"+userID.
func CurrentProfileKey(userID string) StateKey {
if userID == "" {
return CurrentProfileStateKey
}
return StateKey("_current/" + userID)
}
// StateStore persists state, and produces it back on request.
// Implementations of StateStore are expected to be safe for concurrent use.
type StateStore interface {
// ReadState returns the bytes associated with ID. Returns (nil,
// ErrStateNotExist) if the ID doesn't have associated state.
ReadState(id StateKey) ([]byte, error)
// WriteState saves bs as the state associated with ID. If bs is nil,
// the state associated with ID is deleted, and a subsequent ReadState
// for ID will return ErrStateNotExist.
//
// Callers should generally use the ipn.WriteState wrapper func
// instead, which only writes if the value is different from what's
// already in the store.
WriteState(id StateKey, bs []byte) error
}
// WriteState is a wrapper around store.WriteState that only writes if
// the value is different from what's already in the store.
func WriteState(store StateStore, id StateKey, v []byte) error {
if was, err := store.ReadState(id); err == nil && bytes.Equal(was, v) {
return nil
}
return store.WriteState(id, v)
}
// StateStoreDialerSetter is an optional interface that StateStores
// can implement to allow the caller to set a custom dialer.
type StateStoreDialerSetter interface {
SetDialer(d func(ctx context.Context, network, address string) (net.Conn, error))
}
// ReadStoreInt reads an integer from a StateStore.
func ReadStoreInt(store StateStore, id StateKey) (int64, error) {
v, err := store.ReadState(id)
if err != nil {
return 0, err
}
return strconv.ParseInt(string(v), 10, 64)
}
// PutStoreInt puts an integer into a StateStore.
func PutStoreInt(store StateStore, id StateKey, val int64) error {
return WriteState(store, id, fmt.Appendf(nil, "%d", val))
}
// EncryptedStateStore is a marker interface implemented by StateStores that
// encrypt data at rest.
type EncryptedStateStore interface {
stateStoreIsEncrypted()
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !plan9
// Package fakes contains mocks used for testing 'kubectl exec' session
// recording functionality.
package fakes
import (
"bytes"
"encoding/json"
"fmt"
"math/rand"
"net"
"sync"
"testing"
"time"
"tailscale.com/sessionrecording"
"tailscale.com/tstime"
)
func New(conn net.Conn, wb bytes.Buffer, rb bytes.Buffer, closed bool) net.Conn {
return &TestConn{
Conn: conn,
writeBuf: wb,
readBuf: rb,
closed: closed,
}
}
type TestConn struct {
net.Conn
// writeBuf contains whatever was send to the conn via Write.
writeBuf bytes.Buffer
// readBuf contains whatever was sent to the conn via Read.
readBuf bytes.Buffer
sync.RWMutex // protects the following
closed bool
}
var _ net.Conn = &TestConn{}
func (tc *TestConn) Read(b []byte) (int, error) {
return tc.readBuf.Read(b)
}
func (tc *TestConn) Write(b []byte) (int, error) {
return tc.writeBuf.Write(b)
}
func (tc *TestConn) Close() error {
tc.Lock()
defer tc.Unlock()
tc.closed = true
return nil
}
func (tc *TestConn) IsClosed() bool {
tc.Lock()
defer tc.Unlock()
return tc.closed
}
func (tc *TestConn) WriteBufBytes() []byte {
return tc.writeBuf.Bytes()
}
func (tc *TestConn) ResetReadBuf() {
tc.readBuf.Reset()
}
func (tc *TestConn) WriteReadBufBytes(b []byte) error {
_, err := tc.readBuf.Write(b)
return err
}
type TestSessionRecorder struct {
// buf holds data that was sent to the session recorder.
buf bytes.Buffer
}
func (t *TestSessionRecorder) Write(b []byte) (int, error) {
return t.buf.Write(b)
}
func (t *TestSessionRecorder) Close() error {
t.buf.Reset()
return nil
}
func (t *TestSessionRecorder) Bytes() []byte {
return t.buf.Bytes()
}
func CastLine(t *testing.T, p []byte, clock tstime.Clock) []byte {
t.Helper()
j, err := json.Marshal([]any{
clock.Now().Sub(clock.Now()).Seconds(),
"o",
string(p),
})
if err != nil {
t.Fatalf("error marshalling cast line: %v", err)
}
return append(j, '\n')
}
func AsciinemaCastResizeMsg(t *testing.T, width, height int) []byte {
msg := fmt.Sprintf(`[0,"r","%dx%d"]`, height, width)
return append([]byte(msg), '\n')
}
func AsciinemaCastHeaderMsg(t *testing.T, width, height int) []byte {
t.Helper()
ch := sessionrecording.CastHeader{
Width: width,
Height: height,
}
bs, err := json.Marshal(ch)
if err != nil {
t.Fatalf("error marshalling CastHeader: %v", err)
}
return append(bs, '\n')
}
func RandomBytes(t *testing.T) [][]byte {
t.Helper()
r := rand.New(rand.NewSource(time.Now().UnixNano()))
n := r.Intn(4096)
b := make([]byte, n)
t.Logf("RandomBytes: generating byte slice of length %d", n)
_, err := r.Read(b)
if err != nil {
t.Fatalf("error generating random byte slice: %v", err)
}
if len(b) < 2 {
return [][]byte{b}
}
split := r.Intn(len(b) - 1)
return [][]byte{b[:split], b[split:]}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !plan9
// Package spdy contains functionality for parsing SPDY streaming sessions. This
// is used for 'kubectl exec/attach' session recording.
package spdy
import (
"bytes"
"context"
"encoding/binary"
"encoding/json"
"fmt"
"net"
"net/http"
"sync"
"sync/atomic"
"go.uber.org/zap"
corev1 "k8s.io/api/core/v1"
"tailscale.com/k8s-operator/sessionrecording/tsrecorder"
"tailscale.com/sessionrecording"
)
// New wraps the provided network connection and returns a connection whose reads and writes will get triggered as data is received on the hijacked connection.
// The connection must be a hijacked connection for a 'kubectl exec/attach' session using SPDY.
// The hijacked connection is used to transmit SPDY streams between Kubernetes client ('kubectl') and the destination container.
// Data read from the underlying network connection is data sent via one of the SPDY streams from the client to the container.
// Data written to the underlying connection is data sent from the container to the client.
// We parse the data and send everything for the stdout/stderr streams to the configured tsrecorder as an asciinema recording with the provided header.
// https://github.com/kubernetes/enhancements/tree/master/keps/sig-api-machinery/4006-transition-spdy-to-websockets#background-remotecommand-subprotocol
func New(ctx context.Context, nc net.Conn, rec *tsrecorder.Client, ch sessionrecording.CastHeader, hasTerm bool, log *zap.SugaredLogger) (net.Conn, error) {
lc := &conn{
Conn: nc,
ctx: ctx,
rec: rec,
ch: ch,
log: log,
hasTerm: hasTerm,
initialCastHeaderSent: make(chan struct{}, 1),
}
// if there is no term, we don't need to wait for a resize message
if !hasTerm {
var err error
lc.writeCastHeaderOnce.Do(func() {
// If this is a session with a terminal attached,
// we must wait for the terminal width and
// height to be parsed from a resize message
// before sending CastHeader, else tsrecorder
// will not be able to play this recording.
err = lc.rec.WriteCastHeader(ch)
close(lc.initialCastHeaderSent)
})
if err != nil {
return nil, fmt.Errorf("error writing CastHeader: %w", err)
}
}
return lc, nil
}
// conn is a wrapper around net.Conn. It reads the bytestream for a 'kubectl
// exec/attach' session streamed using SPDY protocol, sends session recording data to
// the configured recorder and forwards the raw bytes to the original
// destination.
type conn struct {
net.Conn
ctx context.Context
// rec knows how to send data written to it to a tsrecorder instance.
rec *tsrecorder.Client
stdoutStreamID atomic.Uint32
stderrStreamID atomic.Uint32
resizeStreamID atomic.Uint32
wmu sync.Mutex // sequences writes
closed bool
rmu sync.Mutex // sequences reads
// The following fields are related to sending asciinema CastHeader.
// CastHeader must be sent before any payload. If the session has a
// terminal attached, the CastHeader must have '.Width' and '.Height'
// fields set for the tsrecorder UI to be able to play the recording.
// For 'kubectl exec/attach' sessions, terminal width and height are sent as a
// resize message on resize stream from the client when the session
// starts as well as at any time the client detects a terminal change.
// We can intercept the resize message on Read calls. As there is no
// guarantee that the resize message from client will be intercepted
// before server writes stdout messages that we must record, we need to
// ensure that parsing stdout/stderr messages written to the connection
// waits till a resize message has been received and a CastHeader with
// correct terminal dimensions can be written.
// ch is the asciinema CastHeader for the current session.
// https://docs.asciinema.org/manual/asciicast/v2/#header
ch sessionrecording.CastHeader
// writeCastHeaderOnce is used to ensure CastHeader gets sent to tsrecorder once.
writeCastHeaderOnce sync.Once
hasTerm bool // whether the session had TTY attached
// initialCastHeaderSent is a channel to ensure that the cast
// header is the first thing that is streamed to the session recorder.
// Otherwise the stream will fail.
initialCastHeaderSent chan struct{}
zlibReqReader zlibReader
// writeBuf is used to store data written to the connection that has not
// yet been parsed as SPDY frames.
writeBuf bytes.Buffer
// readBuf is used to store data read from the connection that has not
// yet been parsed as SPDY frames.
readBuf bytes.Buffer
log *zap.SugaredLogger
}
// Read reads bytes from the original connection and parses them as SPDY frames.
// If the frame is a data frame for resize stream, sends resize message to the
// recorder. If the frame is a SYN_STREAM control frame that starts stdout,
// stderr or resize stream, store the stream ID.
func (c *conn) Read(b []byte) (int, error) {
c.rmu.Lock()
defer c.rmu.Unlock()
n, err := c.Conn.Read(b)
if err != nil {
return n, fmt.Errorf("error reading from connection: %w", err)
}
c.readBuf.Write(b[:n])
var sf spdyFrame
ok, err := sf.Parse(c.readBuf.Bytes(), c.log)
if err != nil {
return 0, fmt.Errorf("error parsing data read from connection: %w", err)
}
if !ok {
// The parsed data in the buffer will be processed together with
// the new data on the next call to Read.
return n, nil
}
c.readBuf.Next(len(sf.Raw)) // advance buffer past the parsed frame
if !sf.Ctrl && c.hasTerm { // data frame
switch sf.StreamID {
case c.resizeStreamID.Load():
var msg spdyResizeMsg
if err = json.Unmarshal(sf.Payload, &msg); err != nil {
return 0, fmt.Errorf("error umarshalling resize msg: %w", err)
}
c.ch.Width = msg.Width
c.ch.Height = msg.Height
// If this is initial resize message, the width and
// height will be sent in the CastHeader. If this is a
// subsequent resize message, we need to send asciinema
// resize message.
var isInitialResize bool
c.writeCastHeaderOnce.Do(func() {
isInitialResize = true
// If this is a session with a terminal attached,
// we must wait for the terminal width and
// height to be parsed from a resize message
// before sending CastHeader, else tsrecorder
// will not be able to play this recording.
err = c.rec.WriteCastHeader(c.ch)
close(c.initialCastHeaderSent)
})
if err != nil {
return 0, fmt.Errorf("error writing CastHeader: %w", err)
}
if !isInitialResize {
if err := c.rec.WriteResize(c.ch.Height, c.ch.Width); err != nil {
return 0, fmt.Errorf("error writing resize message: %w", err)
}
}
}
return n, nil
}
// We always want to parse the headers, even if we don't care about the
// frame, as we need to advance the zlib reader otherwise we will get
// garbage.
header, err := sf.parseHeaders(&c.zlibReqReader, c.log)
if err != nil {
return 0, fmt.Errorf("error parsing frame headers: %w", err)
}
if sf.Type == SYN_STREAM {
c.storeStreamID(sf, header)
}
return n, nil
}
// Write forwards the raw data of the latest parsed SPDY frame to the original
// destination. If the frame is an SPDY data frame, it also sends the payload to
// the connected session recorder.
func (c *conn) Write(b []byte) (int, error) {
c.wmu.Lock()
defer c.wmu.Unlock()
c.writeBuf.Write(b)
var sf spdyFrame
ok, err := sf.Parse(c.writeBuf.Bytes(), c.log)
if err != nil {
return 0, fmt.Errorf("error parsing data: %w", err)
}
if !ok {
// The parsed data in the buffer will be processed together with
// the new data on the next call to Write.
return len(b), nil
}
c.writeBuf.Next(len(sf.Raw)) // advance buffer past the parsed frame
// If this is a stdout or stderr data frame, send its payload to the
// session recorder.
if !sf.Ctrl {
switch sf.StreamID {
case c.stdoutStreamID.Load(), c.stderrStreamID.Load():
// we must wait for confirmation that the initial cast header was sent before proceeding with any more writes
select {
case <-c.ctx.Done():
return 0, c.ctx.Err()
case <-c.initialCastHeaderSent:
if err := c.rec.WriteOutput(sf.Payload); err != nil {
return 0, fmt.Errorf("error sending payload to session recorder: %w", err)
}
}
}
}
// Forward the whole frame to the original destination.
_, err = c.Conn.Write(sf.Raw) // send to net.Conn
return len(b), err
}
func (c *conn) Close() error {
c.wmu.Lock()
defer c.wmu.Unlock()
if c.closed {
return nil
}
c.writeBuf.Reset()
c.closed = true
err := c.Conn.Close()
c.rec.Close()
return err
}
// storeStreamID parses SYN_STREAM SPDY control frame and updates
// conn to store the newly created stream's ID if it is one of
// the stream types we care about. Storing stream_id:stream_type mapping allows
// us to parse received data frames (that have stream IDs) differently depening
// on which stream they belong to (i.e send data frame payload for stdout stream
// to session recorder).
func (c *conn) storeStreamID(sf spdyFrame, header http.Header) {
const (
streamTypeHeaderKey = "Streamtype"
)
id := binary.BigEndian.Uint32(sf.Payload[0:4]) & 0x7fffffff // drop the reserved flag bit: data frames never carry it
switch header.Get(streamTypeHeaderKey) {
case corev1.StreamTypeStdout:
c.stdoutStreamID.Store(id)
case corev1.StreamTypeStderr:
c.stderrStreamID.Store(id)
case corev1.StreamTypeResize:
c.resizeStreamID.Store(id)
}
}
type spdyResizeMsg struct {
Width int `json:"width"`
Height int `json:"height"`
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !plan9
package spdy
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"net/http"
"sync"
"go.uber.org/zap"
)
const (
SYN_STREAM ControlFrameType = 1 // https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt section 2.6.1
SYN_REPLY ControlFrameType = 2 // https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt section 2.6.2
SYN_PING ControlFrameType = 6 // https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt section 2.6.5
)
// maxHeaderCount bounds the number of Name/Value pairs parsed out of one SPDY header block.
// A 'kubectl exec/attach' session opens a few streams whose blocks carry a handful of pairs,
// so this is orders of magnitude above real traffic.
const maxHeaderCount = 1000
// errHeaderBlockTooLarge is returned when a decompressed SPDY header block
// exceeds maxHeaderBlockSize.
var errHeaderBlockTooLarge = errors.New("SPDY header block too large or malformed")
// maxHeaderBlockSize bounds the decompressed size of one Name/Value Header Block.
const maxHeaderBlockSize = 1 << 20 // 1MiB
// spdyFrame is a parsed SPDY frame as defined in
// https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt
// A SPDY frame can be either a control frame or a data frame.
type spdyFrame struct {
Raw []byte // full frame as raw bytes
// Common frame fields:
Ctrl bool // true if this is a SPDY control frame
Payload []byte // payload as raw bytes
// Control frame fields:
Version uint16 // SPDY protocol version
Type ControlFrameType
// Data frame fields:
// StreamID is the id of the steam to which this data frame belongs.
// SPDY allows transmitting multiple data streams concurrently.
StreamID uint32
}
// Type of an SPDY control frame.
type ControlFrameType uint16
// Parse parses bytes into spdyFrame.
// If the bytes don't contain a full frame, return false.
//
// Control frame structure:
//
// +----------------------------------+
// |C| Version(15bits) | Type(16bits) |
// +----------------------------------+
// | Flags (8) | Length (24 bits) |
// +----------------------------------+
// | Data |
// +----------------------------------+
//
// Data frame structure:
//
// +----------------------------------+
// |C| Stream-ID (31bits) |
// +----------------------------------+
// | Flags (8) | Length (24 bits) |
// +----------------------------------+
// | Data |
// +----------------------------------+
//
// https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt
func (sf *spdyFrame) Parse(b []byte, log *zap.SugaredLogger) (ok bool, _ error) {
const (
spdyHeaderLength = 8
)
have := len(b)
if have < spdyHeaderLength { // input does not contain full frame
return false, nil
}
if !isSPDYFrameHeader(b) {
return false, fmt.Errorf("bytes %v do not seem to contain SPDY frames. Ensure that you are using a SPDY based client to 'kubectl exec'.", b)
}
payloadLength := readInt24(b[5:8])
frameLength := payloadLength + spdyHeaderLength
if have < frameLength { // input does not contain full frame
return false, nil
}
frame := b[:frameLength:frameLength] // enforce frameLength capacity
sf.Raw = frame
sf.Payload = frame[spdyHeaderLength:frameLength]
sf.Ctrl = hasControlBitSet(frame)
if !sf.Ctrl { // data frame
sf.StreamID = dataFrameStreamID(frame)
return true, nil
}
sf.Version = controlFrameVersion(frame)
sf.Type = controlFrameType(frame)
return true, nil
}
// parseHeaders retrieves any headers from this spdyFrame.
func (sf *spdyFrame) parseHeaders(z *zlibReader, log *zap.SugaredLogger) (http.Header, error) {
if !sf.Ctrl {
return nil, fmt.Errorf("[unexpected] parseHeaders called for a frame that is not a control frame")
}
const (
// +------------------------------------+
// |X| Stream-ID (31bits) |
// +------------------------------------+
// |X| Associated-To-Stream-ID (31bits) |
// +------------------------------------+
// | Pri|Unused | Slot | |
// +-------------------+ |
synStreamPayloadLengthBeforeHeaders = 10
// +------------------------------------+
// |X| Stream-ID (31bits) |
//+------------------------------------+
synReplyPayloadLengthBeforeHeaders = 4
// +----------------------------------|
// | 32-bit ID |
// +----------------------------------+
pingPayloadLength = 4
)
switch sf.Type {
case SYN_STREAM:
if len(sf.Payload) < synStreamPayloadLengthBeforeHeaders {
return nil, fmt.Errorf("SYN_STREAM frame too short: %v", len(sf.Payload))
}
z.Set(sf.Payload[synStreamPayloadLengthBeforeHeaders:])
return parseHeaders(z, log)
case SYN_REPLY:
if len(sf.Payload) < synReplyPayloadLengthBeforeHeaders {
return nil, fmt.Errorf("SYN_REPLY frame too short: %v", len(sf.Payload))
}
if len(sf.Payload) == synReplyPayloadLengthBeforeHeaders {
return nil, nil // no headers
}
z.Set(sf.Payload[synReplyPayloadLengthBeforeHeaders:])
return parseHeaders(z, log)
case SYN_PING:
if len(sf.Payload) != pingPayloadLength {
return nil, fmt.Errorf("PING frame with unexpected length %v", len(sf.Payload))
}
return nil, nil // ping frame has no headers
default:
log.Infof("[unexpected] unknown control frame type %v", sf.Type)
}
return nil, nil
}
// limitHeaderReader caps reads at maxHeaderBlockSize bytes. Reads past the
// cap report errHeaderBlockTooLarge instead of a clean EOF.
type limitHeaderReader struct {
r io.Reader // decompressed header block
remain int64 // remaining byte budget
}
func (lr *limitHeaderReader) Read(p []byte) (int, error) {
if len(p) == 0 {
return 0, nil
} else if lr.remain == 0 {
return 0, errHeaderBlockTooLarge
}
if int64(len(p)) > lr.remain {
p = p[:lr.remain]
}
n, err := lr.r.Read(p)
lr.remain -= int64(n)
return n, err
}
// parseHeaders expects to be passed a reader that contains a compressed SPDY
// control frame Name/Value Header Block with 0 or more headers:
//
// | Number of Name/Value pairs (int32) | <+
// +------------------------------------+ |
// | Length of name (int32) | | This section is the "Name/Value
// +------------------------------------+ | Header Block", and is compressed.
// | Name (string) | |
// +------------------------------------+ |
// | Length of value (int32) | |
// +------------------------------------+ |
// | Value (string) | |
// +------------------------------------+ |
// | (repeats) | <+
//
// It extracts the headers and returns them as http.Header. By doing that it
// also advances the provided reader past the headers block.
// See also https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt section 2.6.10
func parseHeaders(decompressor io.Reader, log *zap.SugaredLogger) (http.Header, error) {
buf := bufPool.Get().(*bytes.Buffer)
defer func() {
if buf.Cap() <= maxHeaderBlockSize { // don't retain a buffer grown by a large block
bufPool.Put(buf)
}
}()
buf.Reset()
// Limit the impact of a potential compression bomb from the header block
lr := &limitHeaderReader{r: decompressor, remain: maxHeaderBlockSize}
// readUint32 reads the next 4 decompressed bytes from the decompressor
// as a uint32.
readUint32 := func() (uint32, error) {
const uint32Length = 4
if _, err := io.CopyN(buf, lr, uint32Length); err != nil { // decompress
return 0, fmt.Errorf("error decompressing bytes: %w", err)
}
return binary.BigEndian.Uint32(buf.Next(uint32Length)), nil // return as uint32
}
// readLenBytes decompresses and returns as bytes the next 'Name' or 'Value'
// field from SPDY Name/Value header block. decompressor must be at
// 'Length of name'/'Length of value' field.
readLenBytes := func() ([]byte, error) {
xLen, err := readUint32() // length of field to read
if err != nil {
return nil, err
}
if _, err := io.CopyN(buf, lr, int64(xLen)); err != nil { // decompress
return nil, err
}
return buf.Next(int(xLen)), nil
}
numHeaders, err := readUint32()
if err != nil {
return nil, fmt.Errorf("error determining num headers: %v", err)
}
if numHeaders > maxHeaderCount {
return nil, fmt.Errorf("invalid data: too many headers declared: %v, maximum is %v", numHeaders, maxHeaderCount)
}
h := make(http.Header, numHeaders)
for range numHeaders {
name, err := readLenBytes()
if err != nil {
return nil, err
}
ns := string(name)
if _, ok := h[ns]; ok {
return nil, fmt.Errorf("invalid data: duplicate header %q", ns)
}
val, err := readLenBytes()
if err != nil {
return nil, fmt.Errorf("error reading header data: %w", err)
}
for v := range bytes.SplitSeq(val, headerSep) {
h.Add(ns, string(v))
}
}
return h, nil
}
// isSPDYFrameHeader validates that the input bytes start with a valid SPDY frame
// header.
func isSPDYFrameHeader(f []byte) bool {
if hasControlBitSet(f) {
// If this is a control frame, version and type must be set.
return controlFrameVersion(f) != uint16(0) && uint16(controlFrameType(f)) != uint16(0)
}
// If this is a data frame, stream ID must be set.
return dataFrameStreamID(f) != uint32(0)
}
// dataFrameStreamID returns stream ID for an SPDY data frame passed as the
// input data slice. StreaID is contained within bits [0-31) of a data frame
// header.
func dataFrameStreamID(frame []byte) uint32 {
return binary.BigEndian.Uint32(frame[0:4]) & 0x7fffffff
}
// controlFrameType returns the type of a SPDY control frame.
// See https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt section 2.6
func controlFrameType(f []byte) ControlFrameType {
return ControlFrameType(binary.BigEndian.Uint16(f[2:4]))
}
// controlFrameVersion returns SPDY version extracted from input bytes that
// must be a SPDY control frame.
func controlFrameVersion(frame []byte) uint16 {
bs := binary.BigEndian.Uint16(frame[0:2]) // first 16 bits
return bs & 0x7fff // discard control bit
}
// hasControlBitSet returns true if the passsed bytes have SPDY control bit set.
// SPDY frames can be either control frames or data frames. A control frame has
// control bit set to 1 and a data frame has it set to 0.
func hasControlBitSet(frame []byte) bool {
return frame[0]&0x80 == 128 // 0x80
}
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
// Headers in SPDY header name/value block are separated by a 0 byte.
// https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt section 2.6.10
var headerSep = []byte{0}
func readInt24(b []byte) int {
_ = b[2] // bounds check hint to compiler; see golang.org/issue/14808
return int(b[0])<<16 | int(b[1])<<8 | int(b[2])
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !plan9
package spdy
import (
"bytes"
"compress/zlib"
"io"
)
// zlibReader contains functionality to parse zlib compressed SPDY data.
// See https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt section 2.6.10.1
type zlibReader struct {
io.ReadCloser
underlying io.LimitedReader // zlib compressed SPDY data
}
// Read decompresses zlibReader's underlying zlib compressed SPDY data and reads
// it into b.
func (z *zlibReader) Read(b []byte) (int, error) {
if z.ReadCloser == nil {
r, err := zlib.NewReaderDict(&z.underlying, spdyTxtDictionary)
if err != nil {
return 0, err
}
z.ReadCloser = r
}
return z.ReadCloser.Read(b)
}
// Set sets zlibReader's underlying data. b must be zlib compressed SPDY data.
func (z *zlibReader) Set(b []byte) {
z.underlying.R = bytes.NewReader(b)
z.underlying.N = int64(len(b))
}
// spdyTxtDictionary is the dictionary defined in the SPDY spec.
// https://datatracker.ietf.org/doc/html/draft-mbelshe-httpbis-spdy-00#section-2.6.10.1
var spdyTxtDictionary = []byte{
0x00, 0x00, 0x00, 0x07, 0x6f, 0x70, 0x74, 0x69, // - - - - o p t i
0x6f, 0x6e, 0x73, 0x00, 0x00, 0x00, 0x04, 0x68, // o n s - - - - h
0x65, 0x61, 0x64, 0x00, 0x00, 0x00, 0x04, 0x70, // e a d - - - - p
0x6f, 0x73, 0x74, 0x00, 0x00, 0x00, 0x03, 0x70, // o s t - - - - p
0x75, 0x74, 0x00, 0x00, 0x00, 0x06, 0x64, 0x65, // u t - - - - d e
0x6c, 0x65, 0x74, 0x65, 0x00, 0x00, 0x00, 0x05, // l e t e - - - -
0x74, 0x72, 0x61, 0x63, 0x65, 0x00, 0x00, 0x00, // t r a c e - - -
0x06, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x00, // - a c c e p t -
0x00, 0x00, 0x0e, 0x61, 0x63, 0x63, 0x65, 0x70, // - - - a c c e p
0x74, 0x2d, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, // t - c h a r s e
0x74, 0x00, 0x00, 0x00, 0x0f, 0x61, 0x63, 0x63, // t - - - - a c c
0x65, 0x70, 0x74, 0x2d, 0x65, 0x6e, 0x63, 0x6f, // e p t - e n c o
0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x0f, // d i n g - - - -
0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x2d, 0x6c, // a c c e p t - l
0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x00, // a n g u a g e -
0x00, 0x00, 0x0d, 0x61, 0x63, 0x63, 0x65, 0x70, // - - - a c c e p
0x74, 0x2d, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, // t - r a n g e s
0x00, 0x00, 0x00, 0x03, 0x61, 0x67, 0x65, 0x00, // - - - - a g e -
0x00, 0x00, 0x05, 0x61, 0x6c, 0x6c, 0x6f, 0x77, // - - - a l l o w
0x00, 0x00, 0x00, 0x0d, 0x61, 0x75, 0x74, 0x68, // - - - - a u t h
0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, // o r i z a t i o
0x6e, 0x00, 0x00, 0x00, 0x0d, 0x63, 0x61, 0x63, // n - - - - c a c
0x68, 0x65, 0x2d, 0x63, 0x6f, 0x6e, 0x74, 0x72, // h e - c o n t r
0x6f, 0x6c, 0x00, 0x00, 0x00, 0x0a, 0x63, 0x6f, // o l - - - - c o
0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, // n n e c t i o n
0x00, 0x00, 0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74, // - - - - c o n t
0x65, 0x6e, 0x74, 0x2d, 0x62, 0x61, 0x73, 0x65, // e n t - b a s e
0x00, 0x00, 0x00, 0x10, 0x63, 0x6f, 0x6e, 0x74, // - - - - c o n t
0x65, 0x6e, 0x74, 0x2d, 0x65, 0x6e, 0x63, 0x6f, // e n t - e n c o
0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x10, // d i n g - - - -
0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, // c o n t e n t -
0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, // l a n g u a g e
0x00, 0x00, 0x00, 0x0e, 0x63, 0x6f, 0x6e, 0x74, // - - - - c o n t
0x65, 0x6e, 0x74, 0x2d, 0x6c, 0x65, 0x6e, 0x67, // e n t - l e n g
0x74, 0x68, 0x00, 0x00, 0x00, 0x10, 0x63, 0x6f, // t h - - - - c o
0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x6c, 0x6f, // n t e n t - l o
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, // c a t i o n - -
0x00, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, // - - c o n t e n
0x74, 0x2d, 0x6d, 0x64, 0x35, 0x00, 0x00, 0x00, // t - m d 5 - - -
0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, // - c o n t e n t
0x2d, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x00, 0x00, // - r a n g e - -
0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, // - - c o n t e n
0x74, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x00, 0x00, // t - t y p e - -
0x00, 0x04, 0x64, 0x61, 0x74, 0x65, 0x00, 0x00, // - - d a t e - -
0x00, 0x04, 0x65, 0x74, 0x61, 0x67, 0x00, 0x00, // - - e t a g - -
0x00, 0x06, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, // - - e x p e c t
0x00, 0x00, 0x00, 0x07, 0x65, 0x78, 0x70, 0x69, // - - - - e x p i
0x72, 0x65, 0x73, 0x00, 0x00, 0x00, 0x04, 0x66, // r e s - - - - f
0x72, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x04, 0x68, // r o m - - - - h
0x6f, 0x73, 0x74, 0x00, 0x00, 0x00, 0x08, 0x69, // o s t - - - - i
0x66, 0x2d, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x00, // f - m a t c h -
0x00, 0x00, 0x11, 0x69, 0x66, 0x2d, 0x6d, 0x6f, // - - - i f - m o
0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2d, 0x73, // d i f i e d - s
0x69, 0x6e, 0x63, 0x65, 0x00, 0x00, 0x00, 0x0d, // i n c e - - - -
0x69, 0x66, 0x2d, 0x6e, 0x6f, 0x6e, 0x65, 0x2d, // i f - n o n e -
0x6d, 0x61, 0x74, 0x63, 0x68, 0x00, 0x00, 0x00, // m a t c h - - -
0x08, 0x69, 0x66, 0x2d, 0x72, 0x61, 0x6e, 0x67, // - i f - r a n g
0x65, 0x00, 0x00, 0x00, 0x13, 0x69, 0x66, 0x2d, // e - - - - i f -
0x75, 0x6e, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, // u n m o d i f i
0x65, 0x64, 0x2d, 0x73, 0x69, 0x6e, 0x63, 0x65, // e d - s i n c e
0x00, 0x00, 0x00, 0x0d, 0x6c, 0x61, 0x73, 0x74, // - - - - l a s t
0x2d, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, // - m o d i f i e
0x64, 0x00, 0x00, 0x00, 0x08, 0x6c, 0x6f, 0x63, // d - - - - l o c
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, // a t i o n - - -
0x0c, 0x6d, 0x61, 0x78, 0x2d, 0x66, 0x6f, 0x72, // - m a x - f o r
0x77, 0x61, 0x72, 0x64, 0x73, 0x00, 0x00, 0x00, // w a r d s - - -
0x06, 0x70, 0x72, 0x61, 0x67, 0x6d, 0x61, 0x00, // - p r a g m a -
0x00, 0x00, 0x12, 0x70, 0x72, 0x6f, 0x78, 0x79, // - - - p r o x y
0x2d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, // - a u t h e n t
0x69, 0x63, 0x61, 0x74, 0x65, 0x00, 0x00, 0x00, // i c a t e - - -
0x13, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2d, 0x61, // - p r o x y - a
0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, // u t h o r i z a
0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x05, // t i o n - - - -
0x72, 0x61, 0x6e, 0x67, 0x65, 0x00, 0x00, 0x00, // r a n g e - - -
0x07, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x72, // - r e f e r e r
0x00, 0x00, 0x00, 0x0b, 0x72, 0x65, 0x74, 0x72, // - - - - r e t r
0x79, 0x2d, 0x61, 0x66, 0x74, 0x65, 0x72, 0x00, // y - a f t e r -
0x00, 0x00, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, // - - - s e r v e
0x72, 0x00, 0x00, 0x00, 0x02, 0x74, 0x65, 0x00, // r - - - - t e -
0x00, 0x00, 0x07, 0x74, 0x72, 0x61, 0x69, 0x6c, // - - - t r a i l
0x65, 0x72, 0x00, 0x00, 0x00, 0x11, 0x74, 0x72, // e r - - - - t r
0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x2d, 0x65, // a n s f e r - e
0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x00, // n c o d i n g -
0x00, 0x00, 0x07, 0x75, 0x70, 0x67, 0x72, 0x61, // - - - u p g r a
0x64, 0x65, 0x00, 0x00, 0x00, 0x0a, 0x75, 0x73, // d e - - - - u s
0x65, 0x72, 0x2d, 0x61, 0x67, 0x65, 0x6e, 0x74, // e r - a g e n t
0x00, 0x00, 0x00, 0x04, 0x76, 0x61, 0x72, 0x79, // - - - - v a r y
0x00, 0x00, 0x00, 0x03, 0x76, 0x69, 0x61, 0x00, // - - - - v i a -
0x00, 0x00, 0x07, 0x77, 0x61, 0x72, 0x6e, 0x69, // - - - w a r n i
0x6e, 0x67, 0x00, 0x00, 0x00, 0x10, 0x77, 0x77, // n g - - - - w w
0x77, 0x2d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, // w - a u t h e n
0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x00, 0x00, // t i c a t e - -
0x00, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, // - - m e t h o d
0x00, 0x00, 0x00, 0x03, 0x67, 0x65, 0x74, 0x00, // - - - - g e t -
0x00, 0x00, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, // - - - s t a t u
0x73, 0x00, 0x00, 0x00, 0x06, 0x32, 0x30, 0x30, // s - - - - 2 0 0
0x20, 0x4f, 0x4b, 0x00, 0x00, 0x00, 0x07, 0x76, // - O K - - - - v
0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x00, // e r s i o n - -
0x00, 0x08, 0x48, 0x54, 0x54, 0x50, 0x2f, 0x31, // - - H T T P - 1
0x2e, 0x31, 0x00, 0x00, 0x00, 0x03, 0x75, 0x72, // - 1 - - - - u r
0x6c, 0x00, 0x00, 0x00, 0x06, 0x70, 0x75, 0x62, // l - - - - p u b
0x6c, 0x69, 0x63, 0x00, 0x00, 0x00, 0x0a, 0x73, // l i c - - - - s
0x65, 0x74, 0x2d, 0x63, 0x6f, 0x6f, 0x6b, 0x69, // e t - c o o k i
0x65, 0x00, 0x00, 0x00, 0x0a, 0x6b, 0x65, 0x65, // e - - - - k e e
0x70, 0x2d, 0x61, 0x6c, 0x69, 0x76, 0x65, 0x00, // p - a l i v e -
0x00, 0x00, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, // - - - o r i g i
0x6e, 0x31, 0x30, 0x30, 0x31, 0x30, 0x31, 0x32, // n 1 0 0 1 0 1 2
0x30, 0x31, 0x32, 0x30, 0x32, 0x32, 0x30, 0x35, // 0 1 2 0 2 2 0 5
0x32, 0x30, 0x36, 0x33, 0x30, 0x30, 0x33, 0x30, // 2 0 6 3 0 0 3 0
0x32, 0x33, 0x30, 0x33, 0x33, 0x30, 0x34, 0x33, // 2 3 0 3 3 0 4 3
0x30, 0x35, 0x33, 0x30, 0x36, 0x33, 0x30, 0x37, // 0 5 3 0 6 3 0 7
0x34, 0x30, 0x32, 0x34, 0x30, 0x35, 0x34, 0x30, // 4 0 2 4 0 5 4 0
0x36, 0x34, 0x30, 0x37, 0x34, 0x30, 0x38, 0x34, // 6 4 0 7 4 0 8 4
0x30, 0x39, 0x34, 0x31, 0x30, 0x34, 0x31, 0x31, // 0 9 4 1 0 4 1 1
0x34, 0x31, 0x32, 0x34, 0x31, 0x33, 0x34, 0x31, // 4 1 2 4 1 3 4 1
0x34, 0x34, 0x31, 0x35, 0x34, 0x31, 0x36, 0x34, // 4 4 1 5 4 1 6 4
0x31, 0x37, 0x35, 0x30, 0x32, 0x35, 0x30, 0x34, // 1 7 5 0 2 5 0 4
0x35, 0x30, 0x35, 0x32, 0x30, 0x33, 0x20, 0x4e, // 5 0 5 2 0 3 - N
0x6f, 0x6e, 0x2d, 0x41, 0x75, 0x74, 0x68, 0x6f, // o n - A u t h o
0x72, 0x69, 0x74, 0x61, 0x74, 0x69, 0x76, 0x65, // r i t a t i v e
0x20, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, // - I n f o r m a
0x74, 0x69, 0x6f, 0x6e, 0x32, 0x30, 0x34, 0x20, // t i o n 2 0 4 -
0x4e, 0x6f, 0x20, 0x43, 0x6f, 0x6e, 0x74, 0x65, // N o - C o n t e
0x6e, 0x74, 0x33, 0x30, 0x31, 0x20, 0x4d, 0x6f, // n t 3 0 1 - M o
0x76, 0x65, 0x64, 0x20, 0x50, 0x65, 0x72, 0x6d, // v e d - P e r m
0x61, 0x6e, 0x65, 0x6e, 0x74, 0x6c, 0x79, 0x34, // a n e n t l y 4
0x30, 0x30, 0x20, 0x42, 0x61, 0x64, 0x20, 0x52, // 0 0 - B a d - R
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x34, 0x30, // e q u e s t 4 0
0x31, 0x20, 0x55, 0x6e, 0x61, 0x75, 0x74, 0x68, // 1 - U n a u t h
0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x34, 0x30, // o r i z e d 4 0
0x33, 0x20, 0x46, 0x6f, 0x72, 0x62, 0x69, 0x64, // 3 - F o r b i d
0x64, 0x65, 0x6e, 0x34, 0x30, 0x34, 0x20, 0x4e, // d e n 4 0 4 - N
0x6f, 0x74, 0x20, 0x46, 0x6f, 0x75, 0x6e, 0x64, // o t - F o u n d
0x35, 0x30, 0x30, 0x20, 0x49, 0x6e, 0x74, 0x65, // 5 0 0 - I n t e
0x72, 0x6e, 0x61, 0x6c, 0x20, 0x53, 0x65, 0x72, // r n a l - S e r
0x76, 0x65, 0x72, 0x20, 0x45, 0x72, 0x72, 0x6f, // v e r - E r r o
0x72, 0x35, 0x30, 0x31, 0x20, 0x4e, 0x6f, 0x74, // r 5 0 1 - N o t
0x20, 0x49, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, // - I m p l e m e
0x6e, 0x74, 0x65, 0x64, 0x35, 0x30, 0x33, 0x20, // n t e d 5 0 3 -
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, // S e r v i c e -
0x55, 0x6e, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, // U n a v a i l a
0x62, 0x6c, 0x65, 0x4a, 0x61, 0x6e, 0x20, 0x46, // b l e J a n - F
0x65, 0x62, 0x20, 0x4d, 0x61, 0x72, 0x20, 0x41, // e b - M a r - A
0x70, 0x72, 0x20, 0x4d, 0x61, 0x79, 0x20, 0x4a, // p r - M a y - J
0x75, 0x6e, 0x20, 0x4a, 0x75, 0x6c, 0x20, 0x41, // u n - J u l - A
0x75, 0x67, 0x20, 0x53, 0x65, 0x70, 0x74, 0x20, // u g - S e p t -
0x4f, 0x63, 0x74, 0x20, 0x4e, 0x6f, 0x76, 0x20, // O c t - N o v -
0x44, 0x65, 0x63, 0x20, 0x30, 0x30, 0x3a, 0x30, // D e c - 0 0 - 0
0x30, 0x3a, 0x30, 0x30, 0x20, 0x4d, 0x6f, 0x6e, // 0 - 0 0 - M o n
0x2c, 0x20, 0x54, 0x75, 0x65, 0x2c, 0x20, 0x57, // - - T u e - - W
0x65, 0x64, 0x2c, 0x20, 0x54, 0x68, 0x75, 0x2c, // e d - - T h u -
0x20, 0x46, 0x72, 0x69, 0x2c, 0x20, 0x53, 0x61, // - F r i - - S a
0x74, 0x2c, 0x20, 0x53, 0x75, 0x6e, 0x2c, 0x20, // t - - S u n - -
0x47, 0x4d, 0x54, 0x63, 0x68, 0x75, 0x6e, 0x6b, // G M T c h u n k
0x65, 0x64, 0x2c, 0x74, 0x65, 0x78, 0x74, 0x2f, // e d - t e x t -
0x68, 0x74, 0x6d, 0x6c, 0x2c, 0x69, 0x6d, 0x61, // h t m l - i m a
0x67, 0x65, 0x2f, 0x70, 0x6e, 0x67, 0x2c, 0x69, // g e - p n g - i
0x6d, 0x61, 0x67, 0x65, 0x2f, 0x6a, 0x70, 0x67, // m a g e - j p g
0x2c, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x2f, 0x67, // - i m a g e - g
0x69, 0x66, 0x2c, 0x61, 0x70, 0x70, 0x6c, 0x69, // i f - a p p l i
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x78, // c a t i o n - x
0x6d, 0x6c, 0x2c, 0x61, 0x70, 0x70, 0x6c, 0x69, // m l - a p p l i
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x78, // c a t i o n - x
0x68, 0x74, 0x6d, 0x6c, 0x2b, 0x78, 0x6d, 0x6c, // h t m l - x m l
0x2c, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c, // - t e x t - p l
0x61, 0x69, 0x6e, 0x2c, 0x74, 0x65, 0x78, 0x74, // a i n - t e x t
0x2f, 0x6a, 0x61, 0x76, 0x61, 0x73, 0x63, 0x72, // - j a v a s c r
0x69, 0x70, 0x74, 0x2c, 0x70, 0x75, 0x62, 0x6c, // i p t - p u b l
0x69, 0x63, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, // i c p r i v a t
0x65, 0x6d, 0x61, 0x78, 0x2d, 0x61, 0x67, 0x65, // e m a x - a g e
0x3d, 0x67, 0x7a, 0x69, 0x70, 0x2c, 0x64, 0x65, // - g z i p - d e
0x66, 0x6c, 0x61, 0x74, 0x65, 0x2c, 0x73, 0x64, // f l a t e - s d
0x63, 0x68, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, // c h c h a r s e
0x74, 0x3d, 0x75, 0x74, 0x66, 0x2d, 0x38, 0x63, // t - u t f - 8 c
0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x3d, 0x69, // h a r s e t - i
0x73, 0x6f, 0x2d, 0x38, 0x38, 0x35, 0x39, 0x2d, // s o - 8 8 5 9 -
0x31, 0x2c, 0x75, 0x74, 0x66, 0x2d, 0x2c, 0x2a, // 1 - u t f - - -
0x2c, 0x65, 0x6e, 0x71, 0x3d, 0x30, 0x2e, // - e n q - 0 -
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !plan9
// Package tsrecorder contains functionality for connecting to a tsrecorder instance.
package tsrecorder
import (
"encoding/json"
"fmt"
"io"
"sync"
"time"
"github.com/pkg/errors"
"go.uber.org/zap"
"tailscale.com/sessionrecording"
"tailscale.com/tstime"
)
func New(conn io.WriteCloser, clock tstime.Clock, start time.Time, failOpen bool, logger *zap.SugaredLogger) *Client {
return &Client{
start: start,
clock: clock,
conn: conn,
failOpen: failOpen,
logger: logger,
}
}
// recorder knows how to send the provided bytes to the configured tsrecorder
// instance in asciinema format.
type Client struct {
start time.Time
clock tstime.Clock
// failOpen specifies whether the session should be allowed to
// continue if writing to the recording fails.
failOpen bool
// failedOpen is set to true if the recording of this session failed and
// we should not attempt to send any more data.
failedOpen bool
logger *zap.SugaredLogger
mu sync.Mutex // guards writes to conn
conn io.WriteCloser // connection to a tsrecorder instance
}
// WriteOutput sends terminal stdout and stderr to the tsrecorder.
// https://docs.asciinema.org/manual/asciicast/v2/#o-output-data-written-to-a-terminal
func (rec *Client) WriteOutput(p []byte) (err error) {
const outputEventCode = "o"
if len(p) == 0 {
return nil
}
return rec.write([]any{
rec.clock.Now().Sub(rec.start).Seconds(),
outputEventCode,
string(p)})
}
// WriteResize writes an asciinema resize message. This can be called if
// terminal size has changed.
// https://docs.asciinema.org/manual/asciicast/v2/#r-resize
func (rec *Client) WriteResize(height, width int) (err error) {
const resizeEventCode = "r"
p := fmt.Sprintf("%dx%d", height, width)
return rec.write([]any{
rec.clock.Now().Sub(rec.start).Seconds(),
resizeEventCode,
string(p)})
}
// WriteCastHeaders writes asciinema CastHeader. This must be called once,
// before any payload is sent to the tsrecorder.
// https://docs.asciinema.org/manual/asciicast/v2/#header
func (rec *Client) WriteCastHeader(ch sessionrecording.CastHeader) error {
return rec.write(ch)
}
// write writes the data to session recorder. If recording fails and policy is
// 'fail open', sets the state to failed and does not attempt to write any more
// data during this session.
func (rec *Client) write(data any) error {
if rec.failedOpen {
return nil
}
j, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("error marshalling data as json: %v", err)
}
j = append(j, '\n')
if err := rec.writeCastLine(j); err != nil {
if !rec.failOpen {
return fmt.Errorf("error writing payload to recorder: %w", err)
}
rec.logger.Infof("error writing to tsrecorder: %v. Failure policy is to fail open, so rest of session contents will not be recorded.", err)
rec.failedOpen = true
}
return nil
}
func (rec *Client) Close() error {
rec.mu.Lock()
defer rec.mu.Unlock()
if rec.conn == nil {
return nil
}
err := rec.conn.Close()
rec.conn = nil
return err
}
// writeToRecorder sends bytes to the tsrecorder. The bytes should be in
// asciinema format.
func (c *Client) writeCastLine(j []byte) error {
c.mu.Lock()
defer c.mu.Unlock()
if c.conn == nil {
return errors.New("recorder closed")
}
_, err := c.conn.Write(j)
if err != nil {
return fmt.Errorf("recorder write error: %w", err)
}
return nil
}
type ResizeMsg struct {
Width int `json:"width"`
Height int `json:"height"`
}
// 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 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
// 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
// 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
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.
//
// It's a var only so tests can shorten it.
var tcpQueryTimeout = 5 * time.Second
// rcodeHoldGrace is how long forwardWithDestChan holds an upstream REFUSED or
// SERVFAIL after every resolver has started, in case a healthier one answers.
// It matches udpRaceTimeout, since a resolver can't be treated as hung before
// its UDP query has had a chance to escalate to TCP.
//
// It's a var only so tests can shorten it.
var rcodeHoldGrace = udpRaceTimeout
// 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
}
return f.newDoHClientLocked(urlBase, urlBase, allIPs)
}
// getDoHClientForResolver returns an HTTP client for the DoH server described
// by r. Known public providers ([publicdns.DoHIPsOfBase]) work as before. For
// arbitrary providers — typically enterprise resolvers recovered from the
// OS's base configuration — the resolver must either carry a
// BootstrapResolution, or its URL host must be an IP literal (which needs no
// bootstrap): we cannot resolve the DoH server's own name through DNS without
// recursing through ourselves.
func (f *forwarder) getDoHClientForResolver(r *dnstype.Resolver) (c *http.Client, ok bool) {
urlBase := r.Addr
f.mu.Lock()
defer f.mu.Unlock()
key := dohClientCacheKey(urlBase, r.BootstrapResolution)
if c, ok := f.dohClient[key]; ok {
return c, true
}
allIPs := publicdns.DoHIPsOfBase(urlBase)
if len(allIPs) == 0 {
if len(r.BootstrapResolution) > 0 {
allIPs = r.BootstrapResolution
} else {
// A URL whose host is an IP literal dials itself; no bootstrap
// resolution is needed.
u, err := url.Parse(urlBase)
if err != nil {
return nil, false
}
ip, err := netip.ParseAddr(u.Hostname())
if err != nil {
return nil, false
}
allIPs = []netip.Addr{ip}
}
}
return f.newDoHClientLocked(key, urlBase, allIPs)
}
// dohClientCacheKey distinguishes cached clients: bootstrap results can change
// with the underlying network, unlike known public provider IPs.
func dohClientCacheKey(urlBase string, bootstrap []netip.Addr) string {
if len(bootstrap) == 0 || len(publicdns.DoHIPsOfBase(urlBase)) > 0 {
return urlBase
}
ips := make([]string, len(bootstrap))
for i, ip := range bootstrap {
ips[i] = ip.String()
}
return urlBase + "\x00" + strings.Join(ips, ",")
}
// newDoHClientLocked builds a DoH HTTP client that dials urlBase's host at
// the given IPs, caching the client under cacheKey (which differs from
// urlBase when bootstrap results vary). f.mu must be held.
func (f *forwarder) newDoHClientLocked(cacheKey, urlBase string, allIPs []netip.Addr) (c *http.Client, ok bool) {
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[cacheKey] = c
return c, true
}
const dohType = "application/dns-message"
// sendDoH sends packet as a DoH query to urlBase using client c.
func (f *forwarder) sendDoH(ctx context.Context, urlBase string, c *http.Client, packet []byte) (*Response, 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)
// Check truncated flag regardless of error.
if truncatedFlagSet(res) {
metricDNSFwdTruncated.Add(1)
}
if err != nil {
metricDNSFwdDoHErrorBody.Add(1)
return nil, err
}
return &Response{
Bs: res,
DoHMeta: &DoHMetadata{
RequestURL: hres.Request.URL,
ResponseHeader: hres.Header,
},
}, nil
}
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 *Response, 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() {
var n int
if ret != nil {
n = len(ret.Bs)
}
f.logf("forwarder.send(%q, %d, %v, %d) from %v [%d] = %v, %v", rr.name.Addr, fq.txid, typ, len(domain), fq.src, id, n, 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.Bs = checkResponseSizeAndSetTC(res.Bs, fq.packet, fq.family, f.logf)
return res, nil
}
if strings.HasPrefix(rr.name.Addr, "https://") {
// Known DoH providers (see tailscale.com/net/dns/publicdns) are dialed
// at their well-known IPs. Arbitrary providers — typically enterprise
// resolvers recovered from the OS's base DNS configuration — are only
// usable when the resolver carries a bootstrap resolution, or when the
// URL's host is an IP literal, since we cannot resolve the DoH
// server's own name through DNS without recursing through ourselves.
if hc, ok := f.getDoHClientForResolver(rr.name); ok {
res, err := f.sendDoH(ctx, rr.name.Addr, hc, fq.packet)
if err != nil {
return nil, err
}
// Check response size and set TC flag if needed (only for UDP queries)
res.Bs = checkResponseSizeAndSetTC(res.Bs, fq.packet, fq.family, f.logf)
return res, nil
}
metricDNSFwdErrorType.Add(1)
return nil, fmt.Errorf("cannot dial https:// resolver %q: no known IPs or bootstrap resolution", rr.name.Addr)
}
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 {
if rcodeErr, ok := errors.AsType[rcodeResponseError](err); ok {
// An upstream REFUSED or SERVFAIL is a complete answer.
// Return it as the race's result so the race doesn't wait
// on a TCP retry. send converts it back to an error.
return rcodeErr.res, 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) {
// Report the disabled retry instead of waiting out the context.
if skipTCP {
return nil, errTCPRetryDisabled
}
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 {
// firstUDP returns an upstream REFUSED or SERVFAIL as a result.
// Convert it back to the soft error callers expect.
switch rcode := getRCode(resp); rcode {
case dns.RCodeServerFailure, dns.RCodeRefused:
return nil, rcodeResponseError{rcode, resp}
}
return &Response{Bs: resp}, nil
}
// If we got a truncated UDP response, return that instead of an error.
if trErr, ok := errors.AsType[truncatedResponseError](err); ok {
return &Response{Bs: 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")
// errTCPRetryDisabled is what the TCP arm of the race in send reports when TCP
// retries are disabled, so that a failed UDP query returns at once instead of
// waiting out its deadline.
var errTCPRetryDisabled = errors.New("no TCP retry: disabled by envknob or control knob")
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)
// The conn is unconnected (see dialUDP), so datagrams can arrive from
// any address, not just the resolver we queried. A reply must come
// from the resolver's address and carry the transaction ID we sent.
// Datagrams that are neither are dropped rather than acted on, so
// neither a spoofed reply nor a single stray datagram can decide the
// query. The loop ends when the conn is closed, which the query's
// context cancellation does via fq.closeOnCtxDone.
var n int
for {
var src netip.AddrPort
var err error
n, src, err = conn.ReadFromUDPAddrPort(out)
if err != nil {
if err := ctx.Err(); err != nil {
metricDNSFwdUDPReadCtxDone.Add(1)
return nil, err
}
if !neterror.PacketWasTruncated(err) {
metricDNSFwdUDPErrorRead.Add(1)
return nil, err
}
// Windows reports a datagram larger than out as a
// truncation error, returning the bytes that fit in
// out but no source address. Fall through and let the
// txid check decide, since the source can't be checked.
} else if src != ipp {
// Not from the resolver we asked, so not a reply to
// this query.
metricDNSFwdUDPDropSrc.Add(1)
continue
}
if n < headerBytes {
f.logf("recv: packet too small (%d bytes)", n)
continue
}
if getTxID(out[:n]) != fq.txid {
metricDNSFwdUDPErrorTxID.Add(1)
continue
}
break
}
truncated := n > maxResponseBytes
if truncated {
n = maxResponseBytes
}
out = out[:n]
tcFlagAlreadySet := truncatedFlagSet(out)
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()
// Canceling ctx can't interrupt reads blocked in the runtime, so the
// deadline has to go on the conn. ctx already carries
// min(caller's deadline, tcpQueryTimeout).
if deadline, ok := ctx.Deadline(); ok {
if err := conn.SetDeadline(deadline); err != nil {
// Best effort. The caller's ctx still bounds the read.
f.logf("sendTCP: setting conn deadline: %v", err)
}
}
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<- *Response, 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 <- &Response{Bs: res.bs}:
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 <- &Response{Bs: res.bs}:
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 func() {
// cancel must run first. The close wakes a blocked reader with
// net.ErrClosed, which sendUDP counts as an upstream read failure
// unless ctx is already done.
cancel()
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 *Response, 1) // it's fine buffered or not
errc := make(chan error, 1) // it's fine buffered or not too
raceStart := time.Now() // when the startDelay of each resolver below starts running
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])
}
// heldErr is the error deliverHeldResponse answers from, and the error
// returned when no response can be sent. A REFUSED never displaces another
// error and any other error displaces a REFUSED, so heldErr is a REFUSED
// only when every resolver refused.
var heldErr error
var numErr int
noteErr := func(err error) {
numErr++
if heldErr == nil || (errors.Is(heldErr, errRefused) && !errors.Is(err, errRefused)) {
heldErr = err
}
}
// deliverSuccess gives the client an upstream's successful response.
deliverSuccess := func(v *Response) error {
select {
case <-ctx.Done():
metricDNSFwdErrorContext.Add(1)
return fmt.Errorf("waiting to send response: %w", ctx.Err())
case responseChan <- v:
if f.verboseFwd {
f.logf("response(%d, %v, %d) = %d, nil", fq.txid, typ, len(domain), len(v.Bs))
}
metricDNSFwdSuccess.Add(1)
f.health.SetHealthy(dnsForwarderFailing)
return nil
}
}
// deliverHeldResponse gives the client the upstream response heldErr
// carries, or a synthesized SERVFAIL when it carries none. Like
// forwardWithDestChan, it either sends to responseChan and returns nil, or
// returns heldErr without sending.
deliverHeldResponse := func() error {
var res *Response
if rcodeErr, ok := errors.AsType[rcodeResponseError](heldErr); ok {
res = &Response{Bs: rcodeErr.res}
} else {
r, err := servfailResponse(query)
if err != nil {
f.logf("building servfail response: %v", err)
return heldErr
}
res = &Response{Bs: r.bs}
}
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), heldErr)
}
return nil
}
return heldErr
}
// holdC stays nil, and a nil channel never fires, until an rcode error
// arrives with an upstream response to hold.
var holdC <-chan time.Time
for {
select {
case v := <-resc:
return deliverSuccess(v)
case err := <-errc:
noteErr(err)
if numErr == len(resolvers) {
return deliverHeldResponse()
}
// Hold this upstream's response in case nothing better arrives.
if _, ok := errors.AsType[rcodeResponseError](err); ok && holdC == nil {
timer := time.NewTimer(rcodeHoldDelay(resolvers, time.Since(raceStart)))
defer timer.Stop()
holdC = timer.C
}
case <-holdC:
// The select above may have picked the timer over an answer or an
// error that was ready at the same time. Take those first.
select {
case v := <-resc:
return deliverSuccess(v)
default:
}
drain:
for {
select {
case err := <-errc:
noteErr(err)
default:
break drain
}
}
return deliverHeldResponse()
case <-ctx.Done():
metricDNSFwdErrorContext.Add(1)
if heldErr != nil {
metricDNSFwdErrorContextGotError.Add(1)
return heldErr
}
// 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
// rcodeHoldDelay returns how much longer to hold an upstream REFUSED or
// SERVFAIL before giving it to the client. The hold ends rcodeHoldGrace after
// the last resolver's startDelay. elapsed is how long ago the resolvers were
// started.
func rcodeHoldDelay(resolvers []resolverAndDelay, elapsed time.Duration) time.Duration {
var lastStart time.Duration
for _, rr := range resolvers {
lastStart = max(lastStart, rr.startDelay)
}
return max(lastStart+rcodeHoldGrace-elapsed, 0)
}
// 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
}
// DoHMetadata captures data about the HTTP
// request and response when a DNS request is
// forwarded to a DoH server, e.g. PeerAPI DoH for
// exit nodes or app connectors.
//
// As of 2026-09-24, the only known consumer is
// feature/conn25, which uses the data to track
// which app connector served a DNS request for
// a client, and to extract authorization tokens
// from the response headers, which it then uses
// in further communication with the connector.
type DoHMetadata struct {
// RequestURL is the URL of the DoH request
// that produced the response.
// It is passed as a pointer to avoid allocations.
// It should not be modified.
RequestURL *url.URL
// ResponseHeader is the DoH HTTP response headers.
// It references the actual headers which may or may
// not have been sent to the HTTP client.
// It should not be modified.
ResponseHeader http.Header
}
// Response is a DNS response in bytes, along with metadata
// from the DoH server that served it, if it was served
// by a DoH server.
type Response struct {
// Bs is the wire-encoded DNS response.
Bs []byte
// DoHMeta is non-nil if the response came
// from a DoH server, and nil otherwise.
DoHMeta *DoHMetadata
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !darwin && !ios
package resolver
import "tailscale.com/util/dnsname"
// negativeCache is a no-op on platforms without NXDOMAIN tracking.
type negativeCache struct{}
func (*negativeCache) queryGeneration() uint64 { return 0 }
func (*negativeCache) record(dnsname.FQDN, uint64) bool { return true }
// ClearNegativeCache is a no-op on this platform.
func (*Resolver) ClearNegativeCache() {}
// CheckCachedDNS always returns false on this platform.
func (*Resolver) CheckCachedDNS() bool { return false }
// 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")
errLocalResponseOutdated = errors.New("local DNS response predates a negative cache check")
)
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{}
negativeCache negativeCache
// 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
}
// marshalLocalResponse serializes a local answer and records successfully
// marshaled authoritative NXDOMAINs so peer updates can invalidate stale OS
// negative caches. The SOA zone distinguishes authoritative negatives from
// other NXDOMAINs. This tracks names only, not positive A or AAAA answers;
// live MagicDNS host records remain the source of truth for those answers.
// If a cache check or reset occurred since generation was read, a negative
// answer returns errLocalResponseOutdated so the caller can resolve it again.
func (r *Resolver) marshalLocalResponse(name dnsname.FQDN, resp *response, generation uint64) ([]byte, error) {
packet, err := marshalResponse(resp)
if err == nil && resp.Header.RCode == dns.RCodeNameError && resp.SOAZone != "" {
if !r.negativeCache.record(name, generation) {
return nil, errLocalResponseOutdated
}
}
return packet, err
}
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) (*Response, 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 *Response, 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, nil
}
if err != nil {
return nil, err
}
out = checkResponseSizeAndSetTC(out, bs, family, r.logf)
return &Response{Bs: 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 *Response, 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, generation uint64) ([]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 r.marshalLocalResponse(name, resp, generation)
}
// 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) {
for {
packet, err := r.respondOnce(query)
if err != errLocalResponseOutdated {
return packet, err
}
// A peer update checked the negative history while this query was
// resolving. Retry so we don't return an NXDOMAIN it could not see.
}
}
func (r *Resolver) respondOnce(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)
}
generation := r.negativeCache.queryGeneration()
// 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(), generation)
}
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 r.marshalLocalResponse(name, resp, generation)
}
// 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
metricDNSFwdUDPDropSrc = clientmetric.NewCounter("dns_query_fwd_udp_drop_src")
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")
// metricDNSFwdUDPReadCtxDone counts reads that ended because the query
// ended, either by losing a race or by running out its deadline. Those
// reads used to be counted as upstream read failures. Counting them here
// instead keeps them visible without blaming the upstream.
metricDNSFwdUDPReadCtxDone = clientmetric.NewCounter("dns_query_fwd_udp_read_ctx_done")
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 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 // fresh until then; zero means expired (see expireIPCache)
}
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
}
// expireIPCache marks host's cache entry, if any, as expired by zeroing
// its expires time, so the next lookup goes to the network. The entry
// remains available to [Resolver.UseLastGood].
func (r *Resolver) expireIPCache(host string) {
r.mu.Lock()
defer r.mu.Unlock()
if ent, ok := r.ipCache[host]; ok {
ent.expires = time.Time{}
r.ipCache[host] = ent
}
}
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, fromCache := d.dnsCache.lookupIPCache(host)
if fromCache {
d.dnsCache.dlogf("%q = %v (cached)", host, ip)
} else {
ip, _, allIPs, err = d.dnsCache.LookupIP(ctx, host)
if err != nil {
return nil, fmt.Errorf("failed to resolve %q: %w", host, err)
}
}
c, err := dc.dialIPs(ctx, ip, allIPs)
if err == nil || !fromCache || ctx.Err() != nil {
return c, err
}
// The cached IPs might be stale. Expire the cache entry (keeping it
// around for UseLastGood) and look up host again, dialing any new IPs.
d.dnsCache.expireIPCache(host)
if _, _, allIPs, err2 := d.dnsCache.LookupIP(ctx, host); err2 == nil {
if c, err2 := dc.raceDial(ctx, allIPs); err2 == nil {
return c, nil
}
}
return nil, err
}
// dialIPs dials ip if it's the only candidate in allIPs, or else races
// dials to all of allIPs.
func (dc *dialCall) dialIPs(ctx context.Context, ip netip.Addr, allIPs []netip.Addr) (net.Conn, error) {
// If we only have one candidate, just dial that, no matter what the
// address family is.
if len(allIPs) == 1 {
dc.d.dnsCache.dlogf("dialing %s, %s for %s", dc.network, ip, dc.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 and netMon may be nil. If netMon is nil, a plain net.Dialer is used
// instead of a netns-aware one.
func bootstrapDNSMap(ctx context.Context, serverName string, serverIP netip.Addr, queryName string, logf logger.Logf, ht *health.Tracker, netMon *netmon.Monitor) (dnsMap, error) {
var dialer netns.Dialer = new(net.Dialer)
if netMon != nil {
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 memnet
import (
"net"
"net/netip"
"time"
)
// NetworkName is the network name returned by [net.Addr.Network]
// for [net.Conn.LocalAddr] and [net.Conn.RemoteAddr] from the [Conn] type.
const NetworkName = "mem"
// Conn is a net.Conn that can additionally have its reads and writes blocked and unblocked.
type Conn interface {
net.Conn
// SetReadBlock blocks or unblocks the Read method of this Conn.
// It reports an error if the existing value matches the new value,
// or if the Conn has been Closed.
SetReadBlock(bool) error
// SetWriteBlock blocks or unblocks the Write method of this Conn.
// It reports an error if the existing value matches the new value,
// or if the Conn has been Closed.
SetWriteBlock(bool) error
}
// NewConn creates a pair of Conns that are wired together by pipes.
func NewConn(name string, maxBuf int) (Conn, Conn) {
r := NewPipe(name+"|0", maxBuf)
w := NewPipe(name+"|1", maxBuf)
return &connHalf{r: r, w: w}, &connHalf{r: w, w: r}
}
// NewTCPConn creates a pair of Conns that are wired together by pipes.
func NewTCPConn(src, dst netip.AddrPort, maxBuf int) (local Conn, remote Conn) {
r := NewPipe(src.String(), maxBuf)
w := NewPipe(dst.String(), maxBuf)
lAddr := net.TCPAddrFromAddrPort(src)
rAddr := net.TCPAddrFromAddrPort(dst)
return &connHalf{r: r, w: w, remote: rAddr, local: lAddr}, &connHalf{r: w, w: r, remote: lAddr, local: rAddr}
}
type connAddr string
func (a connAddr) Network() string { return NetworkName }
func (a connAddr) String() string { return string(a) }
type connHalf struct {
local, remote net.Addr
r, w *Pipe
}
func (c *connHalf) LocalAddr() net.Addr {
if c.local != nil {
return c.local
}
return connAddr(c.r.name)
}
func (c *connHalf) RemoteAddr() net.Addr {
if c.remote != nil {
return c.remote
}
return connAddr(c.w.name)
}
func (c *connHalf) Read(b []byte) (n int, err error) {
return c.r.Read(b)
}
func (c *connHalf) Write(b []byte) (n int, err error) {
return c.w.Write(b)
}
func (c *connHalf) Close() error {
if err := c.w.Close(); err != nil {
return err
}
return c.r.Close()
}
func (c *connHalf) SetDeadline(t time.Time) error {
err1 := c.SetReadDeadline(t)
err2 := c.SetWriteDeadline(t)
if err1 != nil {
return err1
}
return err2
}
func (c *connHalf) SetReadDeadline(t time.Time) error {
return c.r.SetReadDeadline(t)
}
func (c *connHalf) SetWriteDeadline(t time.Time) error {
return c.w.SetWriteDeadline(t)
}
func (c *connHalf) SetReadBlock(b bool) error {
if b {
return c.r.Block()
}
return c.r.Unblock()
}
func (c *connHalf) SetWriteBlock(b bool) error {
if b {
return c.w.Block()
}
return c.w.Unblock()
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package memnet
import (
"context"
"net"
"strings"
"sync"
)
const (
bufferSize = 256 * 1024
)
// Listener is a net.Listener using NewConn to create pairs of network
// connections connected in memory using a buffered pipe. It also provides a
// Dial method to establish new connections.
type Listener struct {
addr connAddr
ch chan Conn
closeOnce sync.Once
closed chan struct{}
onClose func() // or nil
// NewConn, if non-nil, is called to create a new pair of connections
// when dialing. If nil, NewConn is used.
NewConn func(network, addr string, maxBuf int) (Conn, Conn)
}
// Listen returns a new Listener for the provided address.
func Listen(addr string) *Listener {
return &Listener{
addr: connAddr(addr),
ch: make(chan Conn),
closed: make(chan struct{}),
}
}
// Addr implements net.Listener.Addr.
func (ln *Listener) Addr() net.Addr {
return ln.addr
}
// Close closes the pipe listener.
func (ln *Listener) Close() error {
var cleanup func()
ln.closeOnce.Do(func() {
cleanup = ln.onClose
close(ln.closed)
})
if cleanup != nil {
cleanup()
}
return nil
}
// Accept blocks until a new connection is available or the listener is closed.
func (ln *Listener) Accept() (net.Conn, error) {
select {
case c := <-ln.ch:
return c, nil
case <-ln.closed:
return nil, net.ErrClosed
}
}
// Dial connects to the listener using the provided context.
// The provided Context must be non-nil. If the context expires before the
// connection is complete, an error is returned. Once successfully connected
// any expiration of the context will not affect the connection.
func (ln *Listener) Dial(ctx context.Context, network, addr string) (_ net.Conn, err error) {
if !strings.HasSuffix(network, "tcp") {
return nil, net.UnknownNetworkError(network)
}
if connAddr(addr) != ln.addr {
return nil, &net.AddrError{
Err: "invalid address",
Addr: addr,
}
}
newConn := ln.NewConn
if newConn == nil {
newConn = func(network, addr string, maxBuf int) (Conn, Conn) {
return NewConn(addr, maxBuf)
}
}
c, s := newConn(network, addr, bufferSize)
defer func() {
if err != nil {
c.Close()
s.Close()
}
}()
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-ln.closed:
return nil, net.ErrClosed
case ln.ch <- s:
return c, nil
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package memnet implements an in-memory network implementation.
// It is useful for dialing and listening on in-memory addresses
// in tests and other situations where you don't want to use the
// network.
package memnet
import (
"context"
"fmt"
"net"
"net/netip"
"tailscale.com/net/netx"
"tailscale.com/syncs"
)
var _ netx.Network = (*Network)(nil)
// Network implements [Network] using an in-memory network, usually
// used for testing.
//
// As of 2025-04-08, it only supports TCP.
//
// Its zero value is a valid [netx.Network] implementation.
type Network struct {
mu syncs.Mutex
lns map[string]*Listener // address -> listener
}
func (m *Network) Listen(network, address string) (net.Listener, error) {
if network != "tcp" && network != "tcp4" && network != "tcp6" {
return nil, fmt.Errorf("memNetwork: Listen called with unsupported network %q", network)
}
ap, err := netip.ParseAddrPort(address)
if err != nil {
return nil, fmt.Errorf("memNetwork: Listen called with invalid address %q: %w", address, err)
}
m.mu.Lock()
defer m.mu.Unlock()
if m.lns == nil {
m.lns = make(map[string]*Listener)
}
port := ap.Port()
for {
if port == 0 {
port = 33000
}
key := net.JoinHostPort(ap.Addr().String(), fmt.Sprint(port))
_, ok := m.lns[key]
if ok {
if ap.Port() != 0 {
return nil, fmt.Errorf("memNetwork: Listen called with duplicate address %q", address)
}
port++
continue
}
ln := Listen(key)
m.lns[key] = ln
ln.onClose = func() {
m.mu.Lock()
delete(m.lns, key)
m.mu.Unlock()
}
return ln, nil
}
}
func (m *Network) NewLocalTCPListener() net.Listener {
ln, err := m.Listen("tcp", "127.0.0.1:0")
if err != nil {
panic(fmt.Sprintf("memNetwork: failed to create local TCP listener: %v", err))
}
return ln
}
func (m *Network) Dial(ctx context.Context, network, address string) (net.Conn, error) {
if network != "tcp" && network != "tcp4" && network != "tcp6" {
return nil, fmt.Errorf("memNetwork: Dial called with unsupported network %q", network)
}
m.mu.Lock()
ln, ok := m.lns[address]
m.mu.Unlock()
if !ok {
return nil, fmt.Errorf("memNetwork: Dial called on unknown address %q", address)
}
return ln.Dial(ctx, network, address)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package memnet
import (
"bytes"
"context"
"fmt"
"io"
"log"
"net"
"os"
"sync"
"time"
)
const debugPipe = false
// Pipe implements an in-memory FIFO with timeouts.
type Pipe struct {
name string
maxBuf int
mu sync.Mutex
cnd *sync.Cond
blocked bool
closed bool
buf bytes.Buffer
readTimeout time.Time
writeTimeout time.Time
cancelReadTimer func()
cancelWriteTimer func()
}
// NewPipe creates a Pipe with a buffer size fixed at maxBuf.
func NewPipe(name string, maxBuf int) *Pipe {
p := &Pipe{
name: name,
maxBuf: maxBuf,
}
p.cnd = sync.NewCond(&p.mu)
return p
}
// readOrBlock attempts to read from the buffer, if the buffer is empty and
// the connection hasn't been closed it will block until there is a change.
func (p *Pipe) readOrBlock(b []byte) (int, error) {
p.mu.Lock()
defer p.mu.Unlock()
if !p.readTimeout.IsZero() && !time.Now().Before(p.readTimeout) {
return 0, os.ErrDeadlineExceeded
}
if p.blocked {
p.cnd.Wait()
return 0, nil
}
n, err := p.buf.Read(b)
// err will either be nil or io.EOF.
if err == io.EOF {
if p.closed {
return n, err
}
// Wait for something to change.
p.cnd.Wait()
}
return n, nil
}
// Read implements io.Reader.
// Once the buffer is drained (i.e. after Close), subsequent calls will
// return io.EOF.
func (p *Pipe) Read(b []byte) (n int, err error) {
if debugPipe {
orig := b
defer func() {
log.Printf("Pipe(%q).Read(%q) n=%d, err=%v", p.name, string(orig[:n]), n, err)
}()
}
for n == 0 {
n2, err := p.readOrBlock(b)
if err != nil {
return n2, err
}
n += n2
}
p.cnd.Signal()
return n, nil
}
// writeOrBlock attempts to write to the buffer, if the buffer is full it will
// block until there is a change.
func (p *Pipe) writeOrBlock(b []byte) (int, error) {
p.mu.Lock()
defer p.mu.Unlock()
if p.closed {
return 0, net.ErrClosed
}
if !p.writeTimeout.IsZero() && !time.Now().Before(p.writeTimeout) {
return 0, os.ErrDeadlineExceeded
}
if p.blocked {
p.cnd.Wait()
return 0, nil
}
// Optimistically we want to write the entire slice.
n := len(b)
if limit := p.maxBuf - p.buf.Len(); limit < n {
// However, we don't have enough capacity to write everything.
n = limit
}
if n == 0 {
// Wait for something to change.
p.cnd.Wait()
return 0, nil
}
p.buf.Write(b[:n])
p.cnd.Signal()
return n, nil
}
// Write implements io.Writer.
func (p *Pipe) Write(b []byte) (n int, err error) {
if debugPipe {
orig := b
defer func() {
log.Printf("Pipe(%q).Write(%q) n=%d, err=%v", p.name, string(orig), n, err)
}()
}
for len(b) > 0 {
n2, err := p.writeOrBlock(b)
if err != nil {
return n + n2, err
}
n += n2
b = b[n2:]
}
return n, nil
}
// Close closes the pipe.
func (p *Pipe) Close() error {
p.mu.Lock()
defer p.mu.Unlock()
p.closed = true
p.blocked = false
if p.cancelWriteTimer != nil {
p.cancelWriteTimer()
p.cancelWriteTimer = nil
}
if p.cancelReadTimer != nil {
p.cancelReadTimer()
p.cancelReadTimer = nil
}
p.cnd.Broadcast()
return nil
}
func (p *Pipe) deadlineTimer(t time.Time) func() {
if t.IsZero() {
return nil
}
if t.Before(time.Now()) {
p.cnd.Broadcast()
return nil
}
ctx, cancel := context.WithDeadline(context.Background(), t)
go func() {
<-ctx.Done()
if ctx.Err() == context.DeadlineExceeded {
p.cnd.Broadcast()
}
}()
return cancel
}
// SetReadDeadline sets the deadline for future Read calls.
func (p *Pipe) SetReadDeadline(t time.Time) error {
p.mu.Lock()
defer p.mu.Unlock()
p.readTimeout = t
// If we already have a deadline, cancel it and create a new one.
if p.cancelReadTimer != nil {
p.cancelReadTimer()
p.cancelReadTimer = nil
}
p.cancelReadTimer = p.deadlineTimer(t)
return nil
}
// SetWriteDeadline sets the deadline for future Write calls.
func (p *Pipe) SetWriteDeadline(t time.Time) error {
p.mu.Lock()
defer p.mu.Unlock()
p.writeTimeout = t
// If we already have a deadline, cancel it and create a new one.
if p.cancelWriteTimer != nil {
p.cancelWriteTimer()
p.cancelWriteTimer = nil
}
p.cancelWriteTimer = p.deadlineTimer(t)
return nil
}
// Block will cause all calls to Read and Write to block until they either
// timeout, are unblocked or the pipe is closed.
func (p *Pipe) Block() error {
p.mu.Lock()
defer p.mu.Unlock()
closed := p.closed
blocked := p.blocked
p.blocked = true
if closed {
return fmt.Errorf("memnet.Pipe(%q).Block: closed", p.name)
}
if blocked {
return fmt.Errorf("memnet.Pipe(%q).Block: already blocked", p.name)
}
p.cnd.Broadcast()
return nil
}
// Unblock will cause all blocked Read/Write calls to continue execution.
func (p *Pipe) Unblock() error {
p.mu.Lock()
defer p.mu.Unlock()
closed := p.closed
blocked := p.blocked
p.blocked = false
if closed {
return fmt.Errorf("memnet.Pipe(%q).Block: closed", p.name)
}
if !blocked {
return fmt.Errorf("memnet.Pipe(%q).Block: already unblocked", p.name)
}
p.cnd.Broadcast()
return nil
}
// 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, so
// a caller can't check where a truncated datagram came from.
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 (
"sync"
)
// defaultInterfaceCacheAttempts is the number of lookup and wait attempts a
// single get may make, including waits for another caller's lookup, before
// falling back to one uncached lookup. Bounding them keeps network churn from
// keeping socket setup in this cache forever.
const defaultInterfaceCacheAttempts = 2
// defaultInterfaceCache caches a nonzero default interface index until an OS change
// notification invalidates it. Windows uses one cache per address family.
// It is independent of Windows APIs so its concurrency can be tested on any OS.
// The zero value is ready for use.
type defaultInterfaceCache struct {
mu sync.Mutex // guards following
inFlight chan struct{} // closed when the current lookup completes, or nil
generation uint64
index uint32
valid bool
disabled bool // notifications are no longer being delivered
}
func (c *defaultInterfaceCache) invalidate() {
c.mu.Lock()
defer c.mu.Unlock()
c.generation++
c.valid = false
}
// disable stops caching when the notification subscription is closed.
func (c *defaultInterfaceCache) disable() {
c.mu.Lock()
defer c.mu.Unlock()
c.disabled = true
c.generation++
c.valid = false
}
func (c *defaultInterfaceCache) get(lookup func() (uint32, error)) (uint32, error) {
for attempt := 0; ; attempt++ {
c.mu.Lock()
if c.disabled {
c.mu.Unlock()
return lookup()
}
if c.valid {
index := c.index
c.mu.Unlock()
return index, nil
}
if attempt == defaultInterfaceCacheAttempts {
c.mu.Unlock()
// Churn prevented a stable result. Do one uncached lookup rather
// than fail: a possibly stale interface index beats no socket.
return lookup()
}
if done := c.inFlight; done != nil {
c.mu.Unlock()
<-done
continue
}
// Only one caller performs the expensive lookup. Invalidation must
// not wait for it, since the OS calls can be slow on some systems.
done := make(chan struct{})
c.inFlight = done
generation := c.generation
c.mu.Unlock()
index, err := lookup()
c.mu.Lock()
c.inFlight = nil
close(done)
if c.disabled {
c.mu.Unlock()
return index, err
}
if generation != c.generation {
// The network changed during the lookup. Don't publish the old
// result, even if the lookup succeeded.
c.mu.Unlock()
continue
}
if err == nil && index != 0 {
// Never cache "no default interface". Zero leaves a socket
// unconstrained, possibly routing it back into Tailscale. Each
// caller must check the OS again rather than reuse that absence
// while a route-restoration notification is still pending.
c.index, c.valid = index, true
}
c.mu.Unlock()
return index, err
}
}
// 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)
// Subscribe to OS change notifications before taking the initial
// snapshot, so a change between the two is queued rather than lost.
var err error
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")
}
st, err := m.interfaceStateUncached()
if err != nil {
m.om.Close()
return nil, err
}
m.ifState = st
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.Addr() == 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 (
"errors"
"fmt"
"log"
"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
}
// SetListenConfigInterfaceName sets lc.Control so that sockets created
// with it are bound to the named interface using SO_BINDTODEVICE, and
// thus only receive packets that arrived on that interface. It is the
// Linux counterpart of darwin's SetListenConfigInterfaceIndex.
//
// Failures to set the socket option are logged and otherwise ignored:
// the socket then simply listens as it would have without this call.
// Setting SO_BINDTODEVICE requires CAP_NET_RAW, and hardening must not
// take out the caller's listener on systems where that is unavailable.
func SetListenConfigInterfaceName(lc *net.ListenConfig, ifName string) error {
if lc == nil {
return errors.New("nil ListenConfig")
}
if lc.Control != nil {
return errors.New("ListenConfig.Control already set")
}
lc.Control = func(network, address string, c syscall.RawConn) error {
var sockErr error
err := c.Control(func(fd uintptr) {
sockErr = unix.SetsockoptString(int(fd), unix.SOL_SOCKET, unix.SO_BINDTODEVICE, ifName)
})
if sockErr != nil {
log.Printf("[unexpected] netns: SO_BINDTODEVICE(%q) on %v %v: %v", ifName, network, address, sockErr)
}
if err != nil {
return fmt.Errorf("RawConn.Control on %T: %w", c, err)
}
return nil
}
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()
}
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package packet
import (
"encoding/binary"
"errors"
"io"
)
const (
// GeneveFixedHeaderLength is the length of the fixed size portion of the
// Geneve header, in bytes.
GeneveFixedHeaderLength = 8
)
const (
// GeneveProtocolDisco is the IEEE 802 Ethertype number used to represent
// the Tailscale Disco protocol in a Geneve header.
GeneveProtocolDisco uint16 = 0x7A11
// GeneveProtocolWireGuard is the IEEE 802 Ethertype number used to represent the
// WireGuard protocol in a Geneve header.
GeneveProtocolWireGuard uint16 = 0x7A12
)
// VirtualNetworkID is a Geneve header (RFC8926) 3-byte virtual network
// identifier. Its methods are NOT thread-safe.
type VirtualNetworkID struct {
_vni uint32
}
const (
vniSetMask uint32 = 0xFF000000
vniGetMask uint32 = ^vniSetMask
)
// IsSet returns true if Set() had been called previously, otherwise false.
func (v *VirtualNetworkID) IsSet() bool {
return v._vni&vniSetMask != 0
}
// Set sets the provided VNI. If VNI exceeds the 3-byte storage it will be
// clamped.
func (v *VirtualNetworkID) Set(vni uint32) {
v._vni = vni | vniSetMask
}
// Get returns the VNI value.
func (v *VirtualNetworkID) Get() uint32 {
return v._vni & vniGetMask
}
// GeneveHeader represents the fixed size Geneve header from RFC8926.
// TLVs/options are not implemented/supported.
//
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |Ver| Opt Len |O|C| Rsvd. | Protocol Type |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Virtual Network Identifier (VNI) | Reserved |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
type GeneveHeader struct {
// Ver (2 bits): The current version number is 0. Packets received by a
// tunnel endpoint with an unknown version MUST be dropped. Transit devices
// interpreting Geneve packets with an unknown version number MUST treat
// them as UDP packets with an unknown payload.
Version uint8
// Protocol Type (16 bits): The type of protocol data unit appearing after
// the Geneve header. This follows the Ethertype [ETYPES] convention, with
// Ethernet itself being represented by the value 0x6558.
Protocol uint16
// Virtual Network Identifier (VNI) (24 bits): An identifier for a unique
// element of a virtual network. In many situations, this may represent an
// L2 segment; however, the control plane defines the forwarding semantics
// of decapsulated packets. The VNI MAY be used as part of ECMP forwarding
// decisions or MAY be used as a mechanism to distinguish between
// overlapping address spaces contained in the encapsulated packet when load
// balancing across CPUs.
VNI VirtualNetworkID
// O (1 bit): Control packet. This packet contains a control message.
// Control messages are sent between tunnel endpoints. Tunnel endpoints MUST
// NOT forward the payload, and transit devices MUST NOT attempt to
// interpret it. Since control messages are less frequent, it is RECOMMENDED
// that tunnel endpoints direct these packets to a high-priority control
// queue (for example, to direct the packet to a general purpose CPU from a
// forwarding Application-Specific Integrated Circuit (ASIC) or to separate
// out control traffic on a NIC). Transit devices MUST NOT alter forwarding
// behavior on the basis of this bit, such as ECMP link selection.
Control bool
}
var ErrGeneveVNIUnset = errors.New("VNI is unset")
// Encode encodes GeneveHeader into b. If len(b) < [GeneveFixedHeaderLength] an
// [io.ErrShortBuffer] error is returned. If !h.VNI.IsSet() then an
// [ErrGeneveVNIUnset] error is returned.
func (h *GeneveHeader) Encode(b []byte) error {
if len(b) < GeneveFixedHeaderLength {
return io.ErrShortBuffer
}
if !h.VNI.IsSet() {
return ErrGeneveVNIUnset
}
if h.Version > 3 {
return errors.New("version must be <= 3")
}
b[0] = 0
b[1] = 0
b[0] |= h.Version << 6
if h.Control {
b[1] |= 0x80
}
binary.BigEndian.PutUint16(b[2:], h.Protocol)
binary.BigEndian.PutUint32(b[4:], h.VNI.Get()<<8)
return nil
}
// Decode decodes GeneveHeader from b. If len(b) < [GeneveFixedHeaderLength] an
// [io.ErrShortBuffer] error is returned.
func (h *GeneveHeader) Decode(b []byte) error {
if len(b) < GeneveFixedHeaderLength {
return io.ErrShortBuffer
}
h.Version = b[0] >> 6
if b[1]&0x80 != 0 {
h.Control = true
}
h.Protocol = binary.BigEndian.Uint16(b[2:])
h.VNI.Set(binary.BigEndian.Uint32(b[4:]) >> 8)
return nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package packet
import (
"errors"
"math"
)
const igmpHeaderLength = 8
const tcpHeaderLength = 20
const sctpHeaderLength = 12
// maxPacketLength is the largest length that all headers support.
// IPv4 headers using uint16 for this forces an upper bound of 64KB.
const maxPacketLength = math.MaxUint16
var (
// errSmallBuffer is returned when Marshal receives a buffer
// too small to contain the header to marshal.
errSmallBuffer = errors.New("buffer too small")
// errLargePacket is returned when Marshal receives a payload
// larger than the maximum representable size in header
// fields.
errLargePacket = errors.New("packet too large")
)
// Header is a packet header capable of marshaling itself into a byte
// buffer.
type Header interface {
// Len returns the length of the marshaled packet.
Len() int
// Marshal serializes the header into buf, which must be at
// least Len() bytes long. Implementations of Marshal assume
// that bytes after the first Len() are payload bytes for the
// purpose of computing length and checksum fields. Marshal
// implementations must not allocate memory.
Marshal(buf []byte) error
}
// HeaderChecksummer is implemented by Header implementations that
// need to do a checksum over their payloads.
type HeaderChecksummer interface {
Header
// WriteCheck writes the correct checksum into buf, which should
// be the already-marshalled header and payload.
WriteChecksum(buf []byte)
}
// Generate generates a new packet with the given Header and
// payload. This function allocates memory, see Header.Marshal for an
// allocation-free option.
func Generate(h Header, payload []byte) []byte {
hlen := h.Len()
buf := make([]byte, hlen+len(payload))
copy(buf[hlen:], payload)
h.Marshal(buf)
if hc, ok := h.(HeaderChecksummer); ok {
hc.WriteChecksum(buf)
}
return buf
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package packet
import (
crand "crypto/rand"
"encoding/binary"
"net/netip"
)
// ICMPEchoPayload generates a new random ID/Sequence pair, and returns a uint32
// derived from them, along with the id, sequence and given payload in a buffer.
// It returns an error if the random source could not be read.
func ICMPEchoPayload(payload []byte) (idSeq uint32, buf []byte) {
buf = make([]byte, len(payload)+4)
// make a completely random id/sequence combo, which is very unlikely to
// collide with a running ping sequence on the host system. Errors are
// ignored, that would result in collisions, but errors reading from the
// random device are rare, and will cause this process universe to soon end.
crand.Read(buf[:4])
idSeq = binary.LittleEndian.Uint32(buf)
copy(buf[4:], payload)
return
}
// icmpDestUnreachableUnusedLen is the number of unused bytes that both ICMPv4
// and ICMPv6 "Destination Unreachable" messages have between header and bits
// from the invoking packet.
const icmpDestUnreachableUnusedLen = 4
const minIPv6MTU = 1280 // RFC 2460, section 5
// GenerateICMPHostUnreachable builds an ICMPv4 or ICMPv6 "Destination
// Unreachable" message according to RFC 792 and RFC 4443, section 3.1.
// from and to must both be of the same address family; otherwise
// GenerateICMPHostUnreachable returns nil.
func GenerateICMPHostUnreachable(from, to netip.Addr, invoking *Parsed) []byte {
buf := invoking.Buffer()
switch {
case from.Is4() && to.Is4():
// RFC 792
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Type | Code | Checksum |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | unused |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Internet Header + 64 bits of Original Data Datagram |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
ipHeaderLen := len(buf) - len(invoking.Transport())
return Generate(ICMP4Header{
IP4Header: IP4Header{Src: from, Dst: to},
Type: ICMP4Unreachable,
Code: ICMP4HostUnreachable,
}, icmpDestUnreachablePayload(buf, ipHeaderLen+8))
case from.Is6() && to.Is6():
// RFC 4443, section 3.1
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Type | Code | Checksum |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Unused |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | As much of invoking packet |
// + as possible without the ICMPv6 packet +
// | exceeding the minimum IPv6 MTU |
maxOrig := minIPv6MTU - (ICMP6Header{}).Len() - icmpDestUnreachableUnusedLen
return Generate(ICMP6Header{
IP6Header: IP6Header{Src: from, Dst: to},
Type: ICMP6Unreachable,
Code: ICMP6AddressUnreachable,
}, icmpDestUnreachablePayload(buf, maxOrig))
default:
return nil
}
}
// icmpDestUnreachablePayload composes the payload for an ICMP "Destination Unreachable" packet.
func icmpDestUnreachablePayload(orig []byte, maxOrig int) []byte {
n := min(len(orig), maxOrig)
payload := make([]byte, icmpDestUnreachableUnusedLen+n)
copy(payload[icmpDestUnreachableUnusedLen:], orig[:n])
return payload
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package packet
import (
"encoding/binary"
"tailscale.com/types/ipproto"
)
// icmp4HeaderLength is the size of the ICMPv4 packet header, not
// including the outer IP layer or the variable "response data"
// trailer.
const icmp4HeaderLength = 4
// ICMP4Type is an ICMPv4 type, as specified in
// https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml
type ICMP4Type uint8
const (
ICMP4EchoReply ICMP4Type = 0x00
ICMP4EchoRequest ICMP4Type = 0x08
ICMP4Unreachable ICMP4Type = 0x03
ICMP4TimeExceeded ICMP4Type = 0x0b
ICMP4ParamProblem ICMP4Type = 0x12
)
func (t ICMP4Type) String() string {
switch t {
case ICMP4EchoReply:
return "EchoReply"
case ICMP4EchoRequest:
return "EchoRequest"
case ICMP4Unreachable:
return "Unreachable"
case ICMP4TimeExceeded:
return "TimeExceeded"
case ICMP4ParamProblem:
return "ParamProblem"
default:
return "Unknown"
}
}
// ICMP4Code is an ICMPv4 code, as specified in
// https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml
type ICMP4Code uint8
const (
ICMP4NoCode ICMP4Code = 0
// ICMP4HostUnreachable is the code used with ICMP4Unreachable to
// indicate that the destination host could not be reached.
ICMP4HostUnreachable ICMP4Code = 1
)
// ICMP4Header is an IPv4+ICMPv4 header.
type ICMP4Header struct {
IP4Header
Type ICMP4Type
Code ICMP4Code
}
// Len implements Header.
func (h ICMP4Header) Len() int {
return h.IP4Header.Len() + icmp4HeaderLength
}
// Marshal implements Header.
func (h ICMP4Header) Marshal(buf []byte) error {
if len(buf) < h.Len() {
return errSmallBuffer
}
if len(buf) > maxPacketLength {
return errLargePacket
}
// The caller does not need to set this.
h.IPProto = ipproto.ICMPv4
buf[20] = uint8(h.Type)
buf[21] = uint8(h.Code)
h.IP4Header.Marshal(buf)
binary.BigEndian.PutUint16(buf[22:24], ip4Checksum(buf))
return nil
}
// ToResponse implements Header. TODO: it doesn't implement it
// correctly, instead it statically generates an ICMP Echo Reply
// packet.
func (h *ICMP4Header) ToResponse() {
// TODO: this doesn't implement ToResponse correctly, as it
// assumes the ICMP request type.
h.Type = ICMP4EchoReply
h.Code = ICMP4NoCode
h.IP4Header.ToResponse()
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package packet
import (
"encoding/binary"
"tailscale.com/types/ipproto"
)
// icmp6HeaderLength is the size of the ICMPv6 packet header, not
// including the outer IP layer or the variable "response data"
// trailer.
const icmp6HeaderLength = 4
// ICMP6Type is an ICMPv6 type, as specified in
// https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml
type ICMP6Type uint8
const (
ICMP6Unreachable ICMP6Type = 1
ICMP6PacketTooBig ICMP6Type = 2
ICMP6TimeExceeded ICMP6Type = 3
ICMP6ParamProblem ICMP6Type = 4
ICMP6EchoRequest ICMP6Type = 128
ICMP6EchoReply ICMP6Type = 129
)
func (t ICMP6Type) String() string {
switch t {
case ICMP6Unreachable:
return "Unreachable"
case ICMP6PacketTooBig:
return "PacketTooBig"
case ICMP6TimeExceeded:
return "TimeExceeded"
case ICMP6ParamProblem:
return "ParamProblem"
case ICMP6EchoRequest:
return "EchoRequest"
case ICMP6EchoReply:
return "EchoReply"
default:
return "Unknown"
}
}
// ICMP6Code is an ICMPv6 code, as specified in
// https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml
type ICMP6Code uint8
const (
ICMP6NoCode ICMP6Code = 0
// ICMP6AddressUnreachable is the code used with ICMP6Unreachable to
// indicate that the destination address could not be reached. It is
// the IPv6 equivalent of ICMP4HostUnreachable.
ICMP6AddressUnreachable ICMP6Code = 3
)
// ICMP6Header is an IPv4+ICMPv4 header.
type ICMP6Header struct {
IP6Header
Type ICMP6Type
Code ICMP6Code
}
// Len implements Header.
func (h ICMP6Header) Len() int {
return h.IP6Header.Len() + icmp6HeaderLength
}
// Marshal implements Header.
func (h ICMP6Header) Marshal(buf []byte) error {
if len(buf) < h.Len() {
return errSmallBuffer
}
if len(buf) > maxPacketLength {
return errLargePacket
}
// The caller does not need to set this.
h.IPProto = ipproto.ICMPv6
h.IP6Header.Marshal(buf)
const o = ip6HeaderLength // start offset of ICMPv6 header
buf[o+0] = uint8(h.Type)
buf[o+1] = uint8(h.Code)
buf[o+2] = 0 // checksum, to be filled in later
buf[o+3] = 0 // checksum, to be filled in later
return nil
}
// ToResponse implements Header. TODO: it doesn't implement it
// correctly, instead it statically generates an ICMP Echo Reply
// packet.
func (h *ICMP6Header) ToResponse() {
// TODO: this doesn't implement ToResponse correctly, as it
// assumes the ICMP request type.
h.Type = ICMP6EchoReply
h.Code = ICMP6NoCode
h.IP6Header.ToResponse()
}
// WriteChecksum implements HeaderChecksummer, writing just the checksum bytes
// into the otherwise fully marshaled ICMP6 packet p (which should include the
// IPv6 header, ICMPv6 header, and payload).
func (h ICMP6Header) WriteChecksum(p []byte) {
const payOff = ip6HeaderLength + icmp6HeaderLength
xsum := icmp6Checksum(p[ip6HeaderLength:payOff], h.Src.As16(), h.Dst.As16(), p[payOff:])
binary.BigEndian.PutUint16(p[ip6HeaderLength+2:], xsum)
}
// Adapted from gVisor:
// icmp6Checksum calculates the ICMP checksum over the provided ICMPv6
// header (without the IPv6 header), IPv6 src/dst addresses and the
// payload.
//
// The header's existing checksum must be zeroed.
func icmp6Checksum(header []byte, src, dst [16]byte, payload []byte) uint16 {
// Calculate the IPv6 pseudo-header upper-layer checksum.
xsum := checksumBytes(src[:], 0)
xsum = checksumBytes(dst[:], xsum)
var scratch [4]byte
binary.BigEndian.PutUint32(scratch[:], uint32(len(header)+len(payload)))
xsum = checksumBytes(scratch[:], xsum)
xsum = checksumBytes(append(scratch[:0], 0, 0, 0, uint8(ipproto.ICMPv6)), xsum)
xsum = checksumBytes(payload, xsum)
var hdrz [icmp6HeaderLength]byte
copy(hdrz[:], header)
// Zero out the header.
hdrz[2] = 0
hdrz[3] = 0
xsum = ^checksumBytes(hdrz[:], xsum)
return xsum
}
// checksumCombine combines the two uint16 to form their
// checksum. This is done by adding them and the carry.
//
// Note that checksum a must have been computed on an even number of
// bytes.
func checksumCombine(a, b uint16) uint16 {
v := uint32(a) + uint32(b)
return uint16(v + v>>16)
}
// checksumBytes calculates the checksum (as defined in RFC 1071) of
// the bytes in buf.
//
// The initial checksum must have been computed on an even number of bytes.
func checksumBytes(buf []byte, initial uint16) uint16 {
v := uint32(initial)
odd := len(buf)%2 == 1
if odd {
v += uint32(buf[0])
buf = buf[1:]
}
n := len(buf)
odd = n&1 != 0
if odd {
n--
v += uint32(buf[n]) << 8
}
for i := 0; i < n; i += 2 {
v += (uint32(buf[i]) << 8) + uint32(buf[i+1])
}
return checksumCombine(uint16(v), uint16(v>>16))
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package packet
import (
"encoding/binary"
"errors"
"net/netip"
"tailscale.com/types/ipproto"
)
// ip4HeaderLength is the length of an IPv4 header with no IP options.
const ip4HeaderLength = 20
// IP4Header represents an IPv4 packet header.
type IP4Header struct {
IPProto ipproto.Proto
IPID uint16
Src netip.Addr
Dst netip.Addr
}
// Len implements Header.
func (h IP4Header) Len() int {
return ip4HeaderLength
}
var errWrongFamily = errors.New("wrong address family for src/dst IP")
// Marshal implements Header.
func (h IP4Header) Marshal(buf []byte) error {
if len(buf) < h.Len() {
return errSmallBuffer
}
if len(buf) > maxPacketLength {
return errLargePacket
}
if !h.Src.Is4() || !h.Dst.Is4() {
return errWrongFamily
}
buf[0] = 0x40 | (byte(h.Len() >> 2)) // IPv4 + IHL
buf[1] = 0x00 // DSCP + ECN
binary.BigEndian.PutUint16(buf[2:4], uint16(len(buf))) // Total length
binary.BigEndian.PutUint16(buf[4:6], h.IPID) // ID
binary.BigEndian.PutUint16(buf[6:8], 0) // Flags + fragment offset
buf[8] = 64 // TTL
buf[9] = uint8(h.IPProto) // Inner protocol
// Blank checksum. This is necessary even though we overwrite
// it later, because the checksum computation runs over these
// bytes and expects them to be zero.
binary.BigEndian.PutUint16(buf[10:12], 0)
src := h.Src.As4()
dst := h.Dst.As4()
copy(buf[12:16], src[:])
copy(buf[16:20], dst[:])
binary.BigEndian.PutUint16(buf[10:12], ip4Checksum(buf[0:20])) // Checksum
return nil
}
// ToResponse implements Header.
func (h *IP4Header) ToResponse() {
h.Src, h.Dst = h.Dst, h.Src
// Flip the bits in the IPID. If incoming IPIDs are distinct, so are these.
h.IPID = ^h.IPID
}
// ip4Checksum computes an IPv4 checksum, as specified in
// https://tools.ietf.org/html/rfc1071
func ip4Checksum(b []byte) uint16 {
var ac uint32
i := 0
n := len(b)
for n >= 2 {
ac += uint32(binary.BigEndian.Uint16(b[i : i+2]))
n -= 2
i += 2
}
if n == 1 {
ac += uint32(b[i]) << 8
}
for (ac >> 16) > 0 {
ac = (ac >> 16) + (ac & 0xffff)
}
return uint16(^ac)
}
// ip4PseudoHeaderOffset is the number of bytes by which the IPv4 UDP
// pseudo-header is smaller than the real IPv4 header.
const ip4PseudoHeaderOffset = 8
// marshalPseudo serializes h into buf in the "pseudo-header" form
// required when calculating UDP checksums. The pseudo-header starts
// at buf[ip4PseudoHeaderOffset] so as to abut the following UDP
// header, while leaving enough space in buf for a full IPv4 header.
func (h IP4Header) marshalPseudo(buf []byte) error {
if len(buf) < h.Len() {
return errSmallBuffer
}
if len(buf) > maxPacketLength {
return errLargePacket
}
length := len(buf) - h.Len()
src, dst := h.Src.As4(), h.Dst.As4()
copy(buf[8:12], src[:])
copy(buf[12:16], dst[:])
buf[16] = 0x0
buf[17] = uint8(h.IPProto)
binary.BigEndian.PutUint16(buf[18:20], uint16(length))
return nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package packet
import (
"encoding/binary"
"net/netip"
"tailscale.com/types/ipproto"
)
// ip6HeaderLength is the length of an IPv6 header with no IP options.
const ip6HeaderLength = 40
// IP6Header represents an IPv6 packet header.
type IP6Header struct {
IPProto ipproto.Proto
IPID uint32 // only lower 20 bits used
Src netip.Addr
Dst netip.Addr
}
// Len implements Header.
func (h IP6Header) Len() int {
return ip6HeaderLength
}
// Marshal implements Header.
func (h IP6Header) Marshal(buf []byte) error {
if len(buf) < h.Len() {
return errSmallBuffer
}
if len(buf) > maxPacketLength {
return errLargePacket
}
binary.BigEndian.PutUint32(buf[:4], h.IPID&0x000FFFFF)
buf[0] = 0x60
binary.BigEndian.PutUint16(buf[4:6], uint16(len(buf)-ip6HeaderLength)) // Total length
buf[6] = uint8(h.IPProto) // Inner protocol
buf[7] = 64 // TTL
src, dst := h.Src.As16(), h.Dst.As16()
copy(buf[8:24], src[:])
copy(buf[24:40], dst[:])
return nil
}
// ToResponse implements Header.
func (h *IP6Header) ToResponse() {
h.Src, h.Dst = h.Dst, h.Src
// Flip the bits in the IPID. If incoming IPIDs are distinct, so are these.
h.IPID = (^h.IPID) & 0x000FFFFF
}
// marshalPseudo serializes h into buf in the "pseudo-header" form
// required when calculating UDP checksums.
func (h IP6Header) marshalPseudo(buf []byte, proto ipproto.Proto) error {
if len(buf) < h.Len() {
return errSmallBuffer
}
if len(buf) > maxPacketLength {
return errLargePacket
}
src, dst := h.Src.As16(), h.Dst.As16()
copy(buf[:16], src[:])
copy(buf[16:32], dst[:])
binary.BigEndian.PutUint32(buf[32:36], uint32(len(buf)-h.Len()))
buf[36] = 0
buf[37] = 0
buf[38] = 0
buf[39] = byte(proto) // NextProto
return nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package packet
import (
"encoding/binary"
"fmt"
"net"
"net/netip"
"strings"
"tailscale.com/net/netaddr"
"tailscale.com/types/ipproto"
)
const unknown = ipproto.Unknown
// RFC1858: prevent overlapping fragment attacks.
//
// The bound is sized for IPv4 (max IPv4 header + basic TCP header) but is
// intentionally reused by decode6 for IPv6 fragments. It is conservative for
// IPv6, whose fragments carry no per-fragment IP header, so it only ever
// rejects more later fragments as Unknown, never fewer.
const minFragBlks = (60 + 20) / 8 // max IPv4 header + basic TCP header in fragment blocks (8 bytes each)
// ip6FragHeader is the IANA protocol number for the IPv6 Fragment extension
// header ("IPv6-Frag"). It appears as the base header's Next Header value on a
// fragmented packet; decode6 steps over it to reach the real sub-protocol.
// This is distinct from ipproto.Fragment (0xFF), our internal sentinel for a
// non-first fragment whose sub-protocol header is not present.
const ip6FragHeader ipproto.Proto = 44
// ip6FragHeaderLength is the length of the IPv6 Fragment extension header.
const ip6FragHeaderLength = 8
type TCPFlag uint8
const (
TCPFin TCPFlag = 0x01
TCPSyn TCPFlag = 0x02
TCPRst TCPFlag = 0x04
TCPPsh TCPFlag = 0x08
TCPAck TCPFlag = 0x10
TCPUrg TCPFlag = 0x20
TCPECNEcho TCPFlag = 0x40
TCPCWR TCPFlag = 0x80
TCPSynAck TCPFlag = TCPSyn | TCPAck
TCPECNBits TCPFlag = TCPECNEcho | TCPCWR
)
// Parsed is a minimal decoding of a packet suitable for use in filters.
type Parsed struct {
// b is the byte buffer that this decodes.
b []byte
// subofs is the offset of IP subprotocol.
subofs int
// dataofs is the offset of IP subprotocol payload.
dataofs int
// length is the total length of the packet.
// This is not the same as len(b) because b can have trailing zeros.
length int
// IPVersion is the IP protocol version of the packet (4 or
// 6), or 0 if the packet doesn't look like IPv4 or IPv6.
IPVersion uint8
// IPProto is the IP subprotocol (UDP, TCP, etc.). Valid iff IPVersion != 0.
IPProto ipproto.Proto
// Src is the source address. Family matches IPVersion. Port is
// valid iff IPProto == TCP || IPProto == UDP || IPProto == SCTP.
Src netip.AddrPort
// Dst is the destination address. Family matches IPVersion. Port is
// valid iff IPProto == TCP || IPProto == UDP || IPProto == SCTP.
Dst netip.AddrPort
// TCPFlags is the packet's TCP flag bits. Valid iff IPProto == TCP.
TCPFlags TCPFlag
// CaptureMeta contains metadata that is used when debugging.
CaptureMeta CaptureMeta
}
func (p *Parsed) String() string {
if p.IPVersion != 4 && p.IPVersion != 6 {
return "Unknown{???}"
}
// max is the maximum reasonable length of the string we are constructing.
// It's OK to overshoot, as the temp buffer is allocated on the stack.
const max = len("ICMPv6{[ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff%enp5s0]:65535 > [ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff%enp5s0]:65535}")
b := make([]byte, 0, max)
b = append(b, p.IPProto.String()...)
b = append(b, '{')
b = p.Src.AppendTo(b)
b = append(b, ' ', '>', ' ')
b = p.Dst.AppendTo(b)
b = append(b, '}')
return string(b)
}
// Decode extracts data from the packet in b into q.
// It performs extremely simple packet decoding for basic IPv4 and IPv6 packet types.
// It extracts only the subprotocol id, IP addresses, and (if any) ports,
// and shouldn't need any memory allocation.
func (q *Parsed) Decode(b []byte) {
q.b = b
q.CaptureMeta = CaptureMeta{} // Clear any capture metadata if it exists.
if len(b) < 1 {
q.IPVersion = 0
q.IPProto = unknown
return
}
q.IPVersion = b[0] >> 4
switch q.IPVersion {
case 4:
q.decode4(b)
case 6:
q.decode6(b)
default:
q.IPVersion = 0
q.IPProto = unknown
}
}
// StuffForTesting makes Parsed contain a len-bytes buffer. Used in
// tests to build up a synthetic parse result with a non-zero buffer.
func (q *Parsed) StuffForTesting(len int) {
q.b = make([]byte, len)
}
func (q *Parsed) decode4(b []byte) {
if len(b) < ip4HeaderLength {
q.IPVersion = 0
q.IPProto = unknown
return
}
// Check that it's IPv4.
q.IPProto = ipproto.Proto(b[9])
q.length = int(binary.BigEndian.Uint16(b[2:4]))
if len(b) < q.length {
// Packet was cut off before full IPv4 length.
q.IPProto = unknown
return
}
// If it's valid IPv4, then the IP addresses are valid
q.Src = withIP(q.Src, netaddr.IPv4(b[12], b[13], b[14], b[15]))
q.Dst = withIP(q.Dst, netaddr.IPv4(b[16], b[17], b[18], b[19]))
// subofs is only committed to q after validation, so a rejected
// packet can't leave it pointing past the end of the buffer.
subofs := int((b[0] & 0x0F) << 2)
if subofs > q.length {
// next-proto starts beyond end of packet.
q.IPProto = unknown
return
}
q.subofs = subofs
sub := b[q.subofs:]
sub = sub[:len(sub):len(sub)] // help the compiler do bounds check elimination
// We don't care much about IP fragmentation, except insofar as it's
// used for firewall bypass attacks. The trick is make the first
// fragment of a TCP or UDP packet so short that it doesn't fit
// the TCP or UDP header, so we can't read the port, in hope that
// it'll sneak past. Then subsequent fragments fill it in, but we're
// missing the first part of the header, so we can't read that either.
//
// A "perfectly correct" implementation would have to reassemble
// fragments before deciding what to do. But the truth is there's
// zero reason to send such a short first fragment, so we can treat
// it as Unknown. We can also treat any subsequent fragment that starts
// at such a low offset as Unknown.
fragFlags := binary.BigEndian.Uint16(b[6:8])
moreFrags := (fragFlags & 0x2000) != 0
fragOfs := fragFlags & 0x1FFF
if fragOfs == 0 {
// This is the first fragment
// Every protocol below MUST check that it has at least one entire
// transport header in order to protect against fragment confusion.
switch q.IPProto {
case ipproto.ICMPv4:
if len(sub) < icmp4HeaderLength {
q.IPProto = unknown
return
}
q.Src = withPort(q.Src, 0)
q.Dst = withPort(q.Dst, 0)
q.dataofs = q.subofs + icmp4HeaderLength
return
case ipproto.IGMP:
if len(sub) < igmpHeaderLength {
q.IPProto = unknown
return
}
// Keep IPProto, but don't parse anything else
// out.
return
case ipproto.TCP:
if len(sub) < tcpHeaderLength {
q.IPProto = unknown
return
}
q.Src = withPort(q.Src, binary.BigEndian.Uint16(sub[0:2]))
q.Dst = withPort(q.Dst, binary.BigEndian.Uint16(sub[2:4]))
q.TCPFlags = TCPFlag(sub[13])
headerLength := (sub[12] & 0xF0) >> 2
q.dataofs = q.subofs + int(headerLength)
return
case ipproto.UDP:
if len(sub) < udpHeaderLength {
q.IPProto = unknown
return
}
q.Src = withPort(q.Src, binary.BigEndian.Uint16(sub[0:2]))
q.Dst = withPort(q.Dst, binary.BigEndian.Uint16(sub[2:4]))
q.dataofs = q.subofs + udpHeaderLength
return
case ipproto.SCTP:
if len(sub) < sctpHeaderLength {
q.IPProto = unknown
return
}
q.Src = withPort(q.Src, binary.BigEndian.Uint16(sub[0:2]))
q.Dst = withPort(q.Dst, binary.BigEndian.Uint16(sub[2:4]))
return
case ipproto.TSMP:
// Strictly disallow fragmented TSMP
if moreFrags {
q.IPProto = unknown
return
}
if len(sub) < minTSMPSize {
q.IPProto = unknown
return
}
// Inter-tailscale messages.
q.dataofs = q.subofs
return
case ipproto.Fragment:
// An IPProto value of 0xff (our Fragment constant for internal use)
// should never actually be used in the wild; if we see it,
// something's suspicious and we map it back to zero (unknown).
q.IPProto = unknown
}
} else {
// This is a fragment other than the first one.
if fragOfs < minFragBlks {
// disallow fragment offsets that are potentially inside of a
// transport header. This is notably asymmetric with the
// first-packet limit, that may allow a first-packet that requires a
// shorter offset than this limit, but without state to tie this
// to the first fragment we can not allow shorter packets.
q.IPProto = unknown
return
}
// otherwise, we have to permit the fragment to slide through.
// Second and later fragments don't have sub-headers.
// Ideally, we would drop fragments that we can't identify,
// but that would require statefulness. Anyway, receivers'
// kernels know to drop fragments where the initial fragment
// doesn't arrive.
q.IPProto = ipproto.Fragment
return
}
}
func (q *Parsed) decode6(b []byte) {
if len(b) < ip6HeaderLength {
q.IPVersion = 0
q.IPProto = unknown
return
}
q.IPProto = ipproto.Proto(b[6])
q.length = int(binary.BigEndian.Uint16(b[4:6])) + ip6HeaderLength
if len(b) < q.length {
// Packet was cut off before the full IPv6 length.
q.IPProto = unknown
return
}
// okay to ignore `ok` here, because IPs pulled from packets are
// always well-formed stdlib IPs.
srcIP, _ := netip.AddrFromSlice(net.IP(b[8:24]))
dstIP, _ := netip.AddrFromSlice(net.IP(b[24:40]))
q.Src = withIP(q.Src, srcIP)
q.Dst = withIP(q.Dst, dstIP)
// The IP subprotocol normally begins right after the 40-byte IPv6
// header. The one extension header we parse is the Fragment header
// (Next Header 44): a host source-fragmenting a datagram larger than
// the tun MTU emits these, and RFC 8200 section 4.5 requires the
// receiver to reassemble them, so we must let them through. For the
// first fragment we step over the fragment header and read the real
// sub-protocol's ports exactly as decode4 does; later fragments are
// marked ipproto.Fragment and passed through by the filter.
//
// We still don't parse any other extension headers (hop-by-hop,
// routing, destination options) or IPSec headers (AH/ESP), nor a
// Fragment header that isn't the base header's immediate Next Header.
// Those get marked Unknown and dropped.
q.subofs = 40
if q.IPProto == ip6FragHeader {
if !q.decode6Fragment(b) {
return
}
}
sub := b[q.subofs:]
sub = sub[:len(sub):len(sub)] // help the compiler do bounds check elimination
switch q.IPProto {
case ipproto.ICMPv6:
if len(sub) < icmp6HeaderLength {
q.IPProto = unknown
return
}
q.Src = withPort(q.Src, 0)
q.Dst = withPort(q.Dst, 0)
q.dataofs = q.subofs + icmp6HeaderLength
case ipproto.TCP:
if len(sub) < tcpHeaderLength {
q.IPProto = unknown
return
}
q.Src = withPort(q.Src, binary.BigEndian.Uint16(sub[0:2]))
q.Dst = withPort(q.Dst, binary.BigEndian.Uint16(sub[2:4]))
q.TCPFlags = TCPFlag(sub[13])
headerLength := (sub[12] & 0xF0) >> 2
q.dataofs = q.subofs + int(headerLength)
return
case ipproto.UDP:
if len(sub) < udpHeaderLength {
q.IPProto = unknown
return
}
q.Src = withPort(q.Src, binary.BigEndian.Uint16(sub[0:2]))
q.Dst = withPort(q.Dst, binary.BigEndian.Uint16(sub[2:4]))
q.dataofs = q.subofs + udpHeaderLength
case ipproto.SCTP:
if len(sub) < sctpHeaderLength {
q.IPProto = unknown
return
}
q.Src = withPort(q.Src, binary.BigEndian.Uint16(sub[0:2]))
q.Dst = withPort(q.Dst, binary.BigEndian.Uint16(sub[2:4]))
return
case ipproto.TSMP:
if len(sub) < minTSMPSize {
q.IPProto = unknown
return
}
// Inter-tailscale messages.
q.dataofs = q.subofs
return
case ipproto.Fragment:
// An IPProto value of 0xff (our Fragment constant for internal use)
// should never actually be used in the wild; if we see it,
// something's suspicious and we map it back to zero (unknown).
q.IPProto = unknown
return
}
}
// decode6Fragment parses the IPv6 Fragment extension header at q.subofs in b
// (q.subofs is the 40-byte base header length when called). It reports whether
// decode6 should continue into the sub-protocol switch: true only for the
// first fragment, where q.subofs and q.IPProto have been advanced to the real
// transport header so its ports get parsed like an unfragmented packet. For
// later or malformed fragments it sets q.IPProto itself (ipproto.Fragment to
// pass through, or unknown to drop) and returns false.
func (q *Parsed) decode6Fragment(b []byte) (continueDecode bool) {
// The fragment header is 8 bytes: Next Header, Reserved, a 13-bit
// Fragment Offset (in 8-byte blocks) plus a More-Fragments flag, and a
// 32-bit Identification.
if len(b) < q.subofs+ip6FragHeaderLength {
q.IPProto = unknown
return false
}
frag := b[q.subofs:]
nextHdr := ipproto.Proto(frag[0])
fragOfs := binary.BigEndian.Uint16(frag[2:4]) >> 3
// Step over the fragment header. The real sub-protocol (first fragment)
// or the continued payload (later fragments) begins here.
q.subofs += ip6FragHeaderLength
if fragOfs == 0 {
// First fragment: decode the real sub-protocol's header so the
// filter can match on its ports. The switch in decode6 performs
// the per-protocol bounds checks, including rejecting a first
// fragment too short to hold the transport header.
q.IPProto = nextHdr
return true
}
// Later fragment: there's no sub-protocol header to read. Reject offsets
// small enough to overlap the transport header (RFC 1858, same guard as
// decode4); otherwise pass it through as a fragment.
if fragOfs < minFragBlks {
q.IPProto = unknown
return false
}
q.IPProto = ipproto.Fragment
return false
}
func (q *Parsed) IP4Header() IP4Header {
if q.IPVersion != 4 {
panic("IP4Header called on non-IPv4 Parsed")
}
ipid := binary.BigEndian.Uint16(q.b[4:6])
return IP4Header{
IPID: ipid,
IPProto: q.IPProto,
Src: q.Src.Addr(),
Dst: q.Dst.Addr(),
}
}
func (q *Parsed) IP6Header() IP6Header {
if q.IPVersion != 6 {
panic("IP6Header called on non-IPv6 Parsed")
}
ipid := (binary.BigEndian.Uint32(q.b[:4]) << 12) >> 12
return IP6Header{
IPID: ipid,
IPProto: q.IPProto,
Src: q.Src.Addr(),
Dst: q.Dst.Addr(),
}
}
func (q *Parsed) ICMP4Header() ICMP4Header {
return ICMP4Header{
IP4Header: q.IP4Header(),
Type: ICMP4Type(q.b[q.subofs+0]),
Code: ICMP4Code(q.b[q.subofs+1]),
}
}
func (q *Parsed) ICMP6Header() ICMP6Header {
return ICMP6Header{
IP6Header: q.IP6Header(),
Type: ICMP6Type(q.b[q.subofs+0]),
Code: ICMP6Code(q.b[q.subofs+1]),
}
}
func (q *Parsed) UDP4Header() UDP4Header {
return UDP4Header{
IP4Header: q.IP4Header(),
SrcPort: q.Src.Port(),
DstPort: q.Dst.Port(),
}
}
// Buffer returns the entire packet buffer.
// This is a read-only view; that is, q retains the ownership of the buffer.
func (q *Parsed) Buffer() []byte {
return q.b
}
// Payload returns the payload of the IP subprotocol section.
// This is a read-only view; that is, q retains the ownership of the buffer.
func (q *Parsed) Payload() []byte {
// If the packet is truncated, return nothing instead of crashing.
// dataofs can also land past length rather than past len(b): the IP
// header can declare a total length that stops before the end of the
// sub-protocol header, which would invert the slice bounds below.
if q.length > len(q.b) || q.dataofs > len(q.b) || q.dataofs > q.length {
return nil
}
return q.b[q.dataofs:q.length]
}
// Transport returns the transport header and payload (IP subprotocol, such as TCP or UDP).
// This is a read-only view; that is, p retains the ownership of the buffer.
func (p *Parsed) Transport() []byte {
if p.subofs > len(p.b) {
// defensive check, should not survive decode4 or decode6 (first line)
return nil
}
return p.b[p.subofs:]
}
// IsTCPSyn reports whether q is a TCP SYN packet,
// without ACK set. (i.e. the first packet in a new connection)
func (q *Parsed) IsTCPSyn() bool {
return (q.TCPFlags & TCPSynAck) == TCPSyn
}
// IsError reports whether q is an ICMP "Error" packet.
func (q *Parsed) IsError() bool {
switch q.IPProto {
case ipproto.ICMPv4:
if len(q.b) < q.subofs+8 {
return false
}
t := ICMP4Type(q.b[q.subofs])
return t == ICMP4Unreachable || t == ICMP4TimeExceeded || t == ICMP4ParamProblem
case ipproto.ICMPv6:
if len(q.b) < q.subofs+8 {
return false
}
t := ICMP6Type(q.b[q.subofs])
return t == ICMP6Unreachable || t == ICMP6PacketTooBig || t == ICMP6TimeExceeded || t == ICMP6ParamProblem
default:
return false
}
}
// IsEchoRequest reports whether q is an ICMP Echo Request.
func (q *Parsed) IsEchoRequest() bool {
switch q.IPProto {
case ipproto.ICMPv4:
return len(q.b) >= q.subofs+8 && ICMP4Type(q.b[q.subofs]) == ICMP4EchoRequest && ICMP4Code(q.b[q.subofs+1]) == ICMP4NoCode
case ipproto.ICMPv6:
return len(q.b) >= q.subofs+8 && ICMP6Type(q.b[q.subofs]) == ICMP6EchoRequest && ICMP6Code(q.b[q.subofs+1]) == ICMP6NoCode
default:
return false
}
}
// IsEchoResponse reports whether q is an IPv4 ICMP Echo Response.
func (q *Parsed) IsEchoResponse() bool {
switch q.IPProto {
case ipproto.ICMPv4:
return len(q.b) >= q.subofs+8 && ICMP4Type(q.b[q.subofs]) == ICMP4EchoReply && ICMP4Code(q.b[q.subofs+1]) == ICMP4NoCode
case ipproto.ICMPv6:
return len(q.b) >= q.subofs+8 && ICMP6Type(q.b[q.subofs]) == ICMP6EchoReply && ICMP6Code(q.b[q.subofs+1]) == ICMP6NoCode
default:
return false
}
}
// EchoIDSeq extracts the identifier/sequence bytes from an ICMP Echo response,
// and returns them as a uint32, used to lookup internally routed ICMP echo
// responses. This function is intentionally lightweight as it is called on
// every incoming ICMP packet.
func (q *Parsed) EchoIDSeq() uint32 {
switch q.IPProto {
case ipproto.ICMPv4:
offset := ip4HeaderLength + icmp4HeaderLength
if len(q.b) < offset+4 {
return 0
}
return binary.LittleEndian.Uint32(q.b[offset:])
case ipproto.ICMPv6:
offset := ip6HeaderLength + icmp6HeaderLength
if len(q.b) < offset+4 {
return 0
}
return binary.LittleEndian.Uint32(q.b[offset:])
default:
return 0
}
}
func Hexdump(b []byte) string {
out := new(strings.Builder)
for i := 0; i < len(b); i += 16 {
if i > 0 {
fmt.Fprintf(out, "\n")
}
fmt.Fprintf(out, " %04x ", i)
j := 0
for ; j < 16 && i+j < len(b); j++ {
if j == 8 {
fmt.Fprintf(out, " ")
}
fmt.Fprintf(out, "%02x ", b[i+j])
}
for ; j < 16; j++ {
if j == 8 {
fmt.Fprintf(out, " ")
}
fmt.Fprintf(out, " ")
}
fmt.Fprintf(out, " ")
for j = 0; j < 16 && i+j < len(b); j++ {
if b[i+j] >= 32 && b[i+j] < 128 {
fmt.Fprintf(out, "%c", b[i+j])
} else {
fmt.Fprintf(out, ".")
}
}
}
return out.String()
}
func withIP(ap netip.AddrPort, ip netip.Addr) netip.AddrPort {
return netip.AddrPortFrom(ip, ap.Port())
}
func withPort(ap netip.AddrPort, port uint16) netip.AddrPort {
return netip.AddrPortFrom(ap.Addr(), port)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// TSMP is our ICMP-like "Tailscale Message Protocol" for signaling
// Tailscale-specific messages between nodes. It uses IP protocol 99
// (reserved for "any private encryption scheme") within
// WireGuard's normal encryption between peers and never hits the host
// network stack.
package packet
import (
"encoding/binary"
"errors"
"fmt"
"net/netip"
"go4.org/mem"
"tailscale.com/types/ipproto"
"tailscale.com/types/key"
)
const minTSMPSize = 7 // the rejected body is 7 bytes
// TailscaleRejectedHeader is a TSMP message that says that one
// Tailscale node has rejected the connection from another. Unlike a
// TCP RST, this includes a reason.
//
// On the wire, after the IP header, it's currently 7 or 8 bytes:
// - '!'
// - IPProto byte (IANA protocol number: TCP or UDP)
// - byte stating rejection reason (see [TailscaleRejectReason] for valid values)
// - srcPort big endian uint16
// - dstPort big endian uint16
// - [optional] byte of flag bits:
// lowest bit (0x1): MaybeBroken
//
// In the future it might also accept 16 byte IP flow src/dst IPs
// after the header, if they're different than the IP-level ones.
type TailscaleRejectedHeader struct {
IPSrc netip.Addr // IPv4 or IPv6 header's src IP
IPDst netip.Addr // IPv4 or IPv6 header's dst IP
Src netip.AddrPort // rejected flow's src
Dst netip.AddrPort // rejected flow's dst
Proto ipproto.Proto // proto that was rejected (TCP or UDP)
Reason TailscaleRejectReason // why the connection was rejected
// MaybeBroken is whether the rejection is non-terminal (the
// client should not fail immediately). This is sent by a
// target when it's not sure whether it's totally broken, but
// it might be. For example, the target tailscaled might think
// its host firewall or IP forwarding aren't configured
// properly, but tailscaled might be wrong (not having enough
// visibility into what the OS is doing). When true, the
// message is simply an FYI as a potential reason to use for
// later when the pendopen connection tracking timer expires.
MaybeBroken bool
}
const rejectFlagBitMaybeBroken = 0x1
func (rh TailscaleRejectedHeader) String() string {
return fmt.Sprintf("TSMP-reject-flow{%s %s > %s}: %s", rh.Proto, rh.Src, rh.Dst, rh.Reason)
}
// TSMPType is the type byte of a TSMP message. It follows the IPv4 or IPv6 header.
type TSMPType uint8
const (
// TSMPTypeRejectedConn is the type byte for a [TailscaleRejectedHeader].
TSMPTypeRejectedConn TSMPType = '!'
// TSMPTypePing is the type byte for a [TSMPPingRequest].
TSMPTypePing TSMPType = 'p'
// TSMPTypePong is the type byte for a [TSMPPongReply].
TSMPTypePong TSMPType = 'o'
// TSMPTypeDiscoAdvertisement is the type byte for a [TSMPDiscoKeyAdvertisement].
TSMPTypeDiscoAdvertisement TSMPType = 'a'
)
type TailscaleRejectReason byte
// IsZero reports whether r is the zero value, representing no rejection.
func (r TailscaleRejectReason) IsZero() bool { return r == TailscaleRejectReasonNone }
const (
// TailscaleRejectReasonNone is the TailscaleRejectReason zero value.
TailscaleRejectReasonNone TailscaleRejectReason = 0
// RejectedDueToACLs means that the host rejected the connection due to ACLs.
RejectedDueToACLs TailscaleRejectReason = 'A'
// RejectedDueToShieldsUp means that the host rejected the connection due to shields being up.
RejectedDueToShieldsUp TailscaleRejectReason = 'S'
// RejectedDueToIPForwarding means that the relay node's IP
// forwarding is disabled.
RejectedDueToIPForwarding TailscaleRejectReason = 'F'
// RejectedDueToHostFirewall means that the target host's
// firewall is blocking the traffic.
RejectedDueToHostFirewall TailscaleRejectReason = 'W'
// RejectedDueToUnknownAppConnectorTransitIP means that the connector host has no real IP
// mapping that matches the provided transit IP for this client, so the
// connector has no destination to forward the connection to.
RejectedDueToUnknownAppConnectorTransitIP TailscaleRejectReason = 'T'
)
func (r TailscaleRejectReason) String() string {
switch r {
case RejectedDueToACLs:
return "acl"
case RejectedDueToShieldsUp:
return "shields"
case RejectedDueToIPForwarding:
return "host-ip-forwarding-unavailable"
case RejectedDueToHostFirewall:
return "host-firewall"
case RejectedDueToUnknownAppConnectorTransitIP:
return "app-connector-transit-ip-unknown"
}
return fmt.Sprintf("0x%02x", byte(r))
}
func (h TailscaleRejectedHeader) hasFlags() bool {
return h.MaybeBroken // the only one currently
}
func (h TailscaleRejectedHeader) Len() int {
v := 1 + // TSMPType byte
1 + // IPProto byte
1 + // TailscaleRejectReason byte
2*2 // 2 uint16 ports
if h.IPSrc.Is4() {
v += ip4HeaderLength
} else if h.IPSrc.Is6() {
v += ip6HeaderLength
}
if h.hasFlags() {
v++
}
return v
}
func (h TailscaleRejectedHeader) Marshal(buf []byte) error {
if len(buf) < h.Len() {
return errSmallBuffer
}
if len(buf) > maxPacketLength {
return errLargePacket
}
if h.Src.Addr().Is4() {
iph := IP4Header{
IPProto: ipproto.TSMP,
Src: h.IPSrc,
Dst: h.IPDst,
}
iph.Marshal(buf)
buf = buf[ip4HeaderLength:]
} else if h.Src.Addr().Is6() {
iph := IP6Header{
IPProto: ipproto.TSMP,
Src: h.IPSrc,
Dst: h.IPDst,
}
iph.Marshal(buf)
buf = buf[ip6HeaderLength:]
} else {
return errors.New("bogus src IP")
}
buf[0] = byte(TSMPTypeRejectedConn)
buf[1] = byte(h.Proto)
buf[2] = byte(h.Reason)
binary.BigEndian.PutUint16(buf[3:5], h.Src.Port())
binary.BigEndian.PutUint16(buf[5:7], h.Dst.Port())
if h.hasFlags() {
var flags byte
if h.MaybeBroken {
flags |= rejectFlagBitMaybeBroken
}
buf[7] = flags
}
return nil
}
// AsTailscaleRejectedHeader parses pp as an incoming rejection
// connection TSMP message.
//
// ok reports whether pp was a valid TSMP rejection packet.
func (pp *Parsed) AsTailscaleRejectedHeader() (h TailscaleRejectedHeader, ok bool) {
p := pp.Payload()
if len(p) < 7 || p[0] != byte(TSMPTypeRejectedConn) {
return
}
h = TailscaleRejectedHeader{
Proto: ipproto.Proto(p[1]),
Reason: TailscaleRejectReason(p[2]),
IPSrc: pp.Src.Addr(),
IPDst: pp.Dst.Addr(),
Src: netip.AddrPortFrom(pp.Dst.Addr(), binary.BigEndian.Uint16(p[3:5])),
Dst: netip.AddrPortFrom(pp.Src.Addr(), binary.BigEndian.Uint16(p[5:7])),
}
if len(p) > 7 {
flags := p[7]
h.MaybeBroken = (flags & rejectFlagBitMaybeBroken) != 0
}
return h, true
}
// TSMPPingRequest is a TSMP message that's like an ICMP ping request.
//
// On the wire, after the IP header, it's currently 9 bytes:
// - 'p' (TSMPTypePing)
// - 8 opaque ping bytes to copy back in the response
type TSMPPingRequest struct {
Data [8]byte
}
func (pp *Parsed) AsTSMPPing() (h TSMPPingRequest, ok bool) {
if pp.IPProto != ipproto.TSMP {
return
}
p := pp.Payload()
if len(p) < 9 || p[0] != byte(TSMPTypePing) {
return
}
copy(h.Data[:], p[1:])
return h, true
}
type TSMPPongReply struct {
IPHeader Header
Data [8]byte
PeerAPIPort uint16
}
// AsTSMPPong returns pp as a TSMPPongReply and whether it is one.
// The pong.IPHeader field is not populated.
func (pp *Parsed) AsTSMPPong() (pong TSMPPongReply, ok bool) {
if pp.IPProto != ipproto.TSMP {
return
}
p := pp.Payload()
if len(p) < 9 || p[0] != byte(TSMPTypePong) {
return
}
copy(pong.Data[:], p[1:])
if len(p) >= 11 {
pong.PeerAPIPort = binary.BigEndian.Uint16(p[9:])
}
return pong, true
}
func (h TSMPPongReply) Len() int {
return h.IPHeader.Len() + 11
}
func (h TSMPPongReply) Marshal(buf []byte) error {
if len(buf) < h.Len() {
return errSmallBuffer
}
if err := h.IPHeader.Marshal(buf); err != nil {
return err
}
buf = buf[h.IPHeader.Len():]
buf[0] = byte(TSMPTypePong)
copy(buf[1:], h.Data[:])
binary.BigEndian.PutUint16(buf[9:11], h.PeerAPIPort)
return nil
}
// TSMPDiscoKeyAdvertisement is a TSMP message that's used for distributing
// [key.DiscoPublic]. It is sent immediately following eligible WireGuard
// session establishment starting at [tailscale.com/tailcfg.CapabilityVersion] 144.
//
// On the wire, after the IP header, it's currently 33 bytes:
// - 'a' (TSMPTypeDiscoAdvertisement)
// - 32 disco key bytes
type TSMPDiscoKeyAdvertisement struct {
Src, Dst netip.Addr // Src and Dst are set from the parent IP Header when parsing.
Key key.DiscoPublic
}
// Marshal marshals ka to a wire representation appropriate for transmission
// over WireGuard to a remote client. A marshaled [TSMPDiscoKeyAdvertisement]
// must not exceed [github.com/tailscale/wireguard-go/device.MaxPriorityMessageContentSize],
// or wireguard-go will reject and discard the payload.
func (ka *TSMPDiscoKeyAdvertisement) Marshal() ([]byte, error) {
var iph Header
if ka.Src.Is4() {
iph = IP4Header{
IPProto: ipproto.TSMP,
Src: ka.Src,
Dst: ka.Dst,
}
} else {
iph = IP6Header{
IPProto: ipproto.TSMP,
Src: ka.Src,
Dst: ka.Dst,
}
}
payload := make([]byte, 0, 1+key.DiscoPublicRawLen)
payload = append(payload, byte(TSMPTypeDiscoAdvertisement))
payload = ka.Key.AppendTo(payload)
if len(payload) != 33 {
// Mostly to safeguard against ourselves changing this in the future.
return []byte{}, fmt.Errorf("expected payload length 33, got %d", len(payload))
}
return Generate(iph, payload[:]), nil
}
func (pp *Parsed) AsTSMPDiscoAdvertisement() (tka TSMPDiscoKeyAdvertisement, ok bool) {
if pp.IPProto != ipproto.TSMP {
return
}
p := pp.Payload()
if len(p) < 33 || p[0] != byte(TSMPTypeDiscoAdvertisement) {
return
}
tka.Src = pp.Src.Addr()
tka.Dst = pp.Dst.Addr()
tka.Key = key.DiscoPublicFromRaw32(mem.B(p[1:33]))
return tka, true
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package packet
import (
"encoding/binary"
"tailscale.com/types/ipproto"
)
// udpHeaderLength is the size of the UDP packet header, not including
// the outer IP header.
const udpHeaderLength = 8
// UDP4Header is an IPv4+UDP header.
type UDP4Header struct {
IP4Header
SrcPort uint16
DstPort uint16
}
// Len implements Header.
func (h UDP4Header) Len() int {
return h.IP4Header.Len() + udpHeaderLength
}
// Marshal implements Header.
func (h UDP4Header) Marshal(buf []byte) error {
if len(buf) < h.Len() {
return errSmallBuffer
}
if len(buf) > maxPacketLength {
return errLargePacket
}
// The caller does not need to set this.
h.IPProto = ipproto.UDP
length := len(buf) - h.IP4Header.Len()
binary.BigEndian.PutUint16(buf[20:22], h.SrcPort)
binary.BigEndian.PutUint16(buf[22:24], h.DstPort)
binary.BigEndian.PutUint16(buf[24:26], uint16(length))
binary.BigEndian.PutUint16(buf[26:28], 0) // blank checksum
// UDP checksum with IP pseudo header.
h.IP4Header.marshalPseudo(buf)
binary.BigEndian.PutUint16(buf[26:28], ip4Checksum(buf[ip4PseudoHeaderOffset:]))
h.IP4Header.Marshal(buf)
return nil
}
// ToResponse implements Header.
func (h *UDP4Header) ToResponse() {
h.SrcPort, h.DstPort = h.DstPort, h.SrcPort
h.IP4Header.ToResponse()
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package packet
import (
"encoding/binary"
"tailscale.com/types/ipproto"
)
// UDP6Header is an IPv6+UDP header.
type UDP6Header struct {
IP6Header
SrcPort uint16
DstPort uint16
}
// Len implements Header.
func (h UDP6Header) Len() int {
return h.IP6Header.Len() + udpHeaderLength
}
// Marshal implements Header.
func (h UDP6Header) Marshal(buf []byte) error {
if len(buf) < h.Len() {
return errSmallBuffer
}
if len(buf) > maxPacketLength {
return errLargePacket
}
// The caller does not need to set this.
h.IPProto = ipproto.UDP
length := len(buf) - h.IP6Header.Len()
binary.BigEndian.PutUint16(buf[40:42], h.SrcPort)
binary.BigEndian.PutUint16(buf[42:44], h.DstPort)
binary.BigEndian.PutUint16(buf[44:46], uint16(length))
binary.BigEndian.PutUint16(buf[46:48], 0) // blank checksum
// UDP checksum with IP pseudo header.
h.IP6Header.marshalPseudo(buf, ipproto.UDP)
binary.BigEndian.PutUint16(buf[46:48], ip4Checksum(buf[:]))
h.IP6Header.Marshal(buf)
return nil
}
// ToResponse implements Header.
func (h *UDP6Header) ToResponse() {
h.SrcPort, h.DstPort = h.DstPort, h.SrcPort
h.IP6Header.ToResponse()
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !js
// (no raw sockets in JS/WASM)
package portmapper
import (
"context"
"github.com/huin/goupnp"
"github.com/huin/goupnp/soap"
)
const (
urn_LegacyWANPPPConnection_1 = "urn:dslforum-org:service:WANPPPConnection:1"
urn_LegacyWANIPConnection_1 = "urn:dslforum-org:service:WANIPConnection:1"
)
// legacyWANPPPConnection1 is the same as internetgateway2.WANPPPConnection1,
// except using the old URN that starts with "urn:dslforum-org".
//
// The definition for this can be found in older documentation about UPnP; for
// the purposes of this implementation, we're referring to "DSL Forum TR-064:
// LAN-Side DSL CPE Configuration", which, while deprecated, can be found at:
//
// https://www.broadband-forum.org/wp-content/uploads/2018/11/TR-064_Corrigendum-1.pdf
// https://www.broadband-forum.org/pdfs/tr-064-1-0-1.pdf
type legacyWANPPPConnection1 struct {
goupnp.ServiceClient
}
// AddPortMappingCtx implements upnpClient
func (client *legacyWANPPPConnection1) AddPortMappingCtx(
ctx context.Context,
NewRemoteHost string,
NewExternalPort uint16,
NewProtocol string,
NewInternalPort uint16,
NewInternalClient string,
NewEnabled bool,
NewPortMappingDescription string,
NewLeaseDuration uint32,
) (err error) {
// Request structure.
request := &struct {
NewRemoteHost string
NewExternalPort string
NewProtocol string
NewInternalPort string
NewInternalClient string
NewEnabled string
NewPortMappingDescription string
NewLeaseDuration string
}{}
if request.NewRemoteHost, err = soap.MarshalString(NewRemoteHost); err != nil {
return
}
if request.NewExternalPort, err = soap.MarshalUi2(NewExternalPort); err != nil {
return
}
if request.NewProtocol, err = soap.MarshalString(NewProtocol); err != nil {
return
}
if request.NewInternalPort, err = soap.MarshalUi2(NewInternalPort); err != nil {
return
}
if request.NewInternalClient, err = soap.MarshalString(NewInternalClient); err != nil {
return
}
if request.NewEnabled, err = soap.MarshalBoolean(NewEnabled); err != nil {
return
}
if request.NewPortMappingDescription, err = soap.MarshalString(NewPortMappingDescription); err != nil {
return
}
if request.NewLeaseDuration, err = soap.MarshalUi4(NewLeaseDuration); err != nil {
return
}
// Response structure.
response := any(nil)
// Perform the SOAP call.
return client.SOAPClient.PerformActionCtx(ctx, urn_LegacyWANPPPConnection_1, "AddPortMapping", request, response)
}
// DeletePortMappingCtx implements upnpClient
func (client *legacyWANPPPConnection1) DeletePortMappingCtx(ctx context.Context, NewRemoteHost string, NewExternalPort uint16, NewProtocol string) (err error) {
// Request structure.
request := &struct {
NewRemoteHost string
NewExternalPort string
NewProtocol string
}{}
if request.NewRemoteHost, err = soap.MarshalString(NewRemoteHost); err != nil {
return
}
if request.NewExternalPort, err = soap.MarshalUi2(NewExternalPort); err != nil {
return
}
if request.NewProtocol, err = soap.MarshalString(NewProtocol); err != nil {
return
}
// Response structure.
response := any(nil)
// Perform the SOAP call.
return client.SOAPClient.PerformActionCtx(ctx, urn_LegacyWANPPPConnection_1, "DeletePortMapping", request, response)
}
// GetExternalIPAddressCtx implements upnpClient
func (client *legacyWANPPPConnection1) GetExternalIPAddressCtx(ctx context.Context) (NewExternalIPAddress string, err error) {
// Request structure.
request := any(nil)
// Response structure.
response := &struct {
NewExternalIPAddress string
}{}
// Perform the SOAP call.
if err = client.SOAPClient.PerformActionCtx(ctx, urn_LegacyWANPPPConnection_1, "GetExternalIPAddress", request, response); err != nil {
return
}
if NewExternalIPAddress, err = soap.UnmarshalString(response.NewExternalIPAddress); err != nil {
return
}
return
}
// GetStatusInfoCtx implements upnpClient
func (client *legacyWANPPPConnection1) GetStatusInfoCtx(ctx context.Context) (NewConnectionStatus string, NewLastConnectionError string, NewUptime uint32, err error) {
// Request structure.
request := any(nil)
// Response structure.
response := &struct {
NewConnectionStatus string
NewLastConnectionError string
NewUpTime string // NOTE: the "T" is capitalized here, per the spec, though it's lowercase in the newer UPnP spec
}{}
// Perform the SOAP call.
if err = client.SOAPClient.PerformActionCtx(ctx, urn_LegacyWANPPPConnection_1, "GetStatusInfo", request, response); err != nil {
return
}
if NewConnectionStatus, err = soap.UnmarshalString(response.NewConnectionStatus); err != nil {
return
}
if NewLastConnectionError, err = soap.UnmarshalString(response.NewLastConnectionError); err != nil {
return
}
if NewUptime, err = soap.UnmarshalUi4(response.NewUpTime); err != nil {
return
}
return
}
// legacyWANIPConnection1 is the same as internetgateway2.WANIPConnection1,
// except using the old URN that starts with "urn:dslforum-org".
//
// See legacyWANPPPConnection1 for details on where this is defined.
type legacyWANIPConnection1 struct {
goupnp.ServiceClient
}
// AddPortMappingCtx implements upnpClient
func (client *legacyWANIPConnection1) AddPortMappingCtx(
ctx context.Context,
NewRemoteHost string,
NewExternalPort uint16,
NewProtocol string,
NewInternalPort uint16,
NewInternalClient string,
NewEnabled bool,
NewPortMappingDescription string,
NewLeaseDuration uint32,
) (err error) {
// Request structure.
request := &struct {
NewRemoteHost string
NewExternalPort string
NewProtocol string
NewInternalPort string
NewInternalClient string
NewEnabled string
NewPortMappingDescription string
NewLeaseDuration string
}{}
if request.NewRemoteHost, err = soap.MarshalString(NewRemoteHost); err != nil {
return
}
if request.NewExternalPort, err = soap.MarshalUi2(NewExternalPort); err != nil {
return
}
if request.NewProtocol, err = soap.MarshalString(NewProtocol); err != nil {
return
}
if request.NewInternalPort, err = soap.MarshalUi2(NewInternalPort); err != nil {
return
}
if request.NewInternalClient, err = soap.MarshalString(NewInternalClient); err != nil {
return
}
if request.NewEnabled, err = soap.MarshalBoolean(NewEnabled); err != nil {
return
}
if request.NewPortMappingDescription, err = soap.MarshalString(NewPortMappingDescription); err != nil {
return
}
if request.NewLeaseDuration, err = soap.MarshalUi4(NewLeaseDuration); err != nil {
return
}
// Response structure.
response := any(nil)
// Perform the SOAP call.
return client.SOAPClient.PerformActionCtx(ctx, urn_LegacyWANIPConnection_1, "AddPortMapping", request, response)
}
// DeletePortMappingCtx implements upnpClient
func (client *legacyWANIPConnection1) DeletePortMappingCtx(ctx context.Context, NewRemoteHost string, NewExternalPort uint16, NewProtocol string) (err error) {
// Request structure.
request := &struct {
NewRemoteHost string
NewExternalPort string
NewProtocol string
}{}
if request.NewRemoteHost, err = soap.MarshalString(NewRemoteHost); err != nil {
return
}
if request.NewExternalPort, err = soap.MarshalUi2(NewExternalPort); err != nil {
return
}
if request.NewProtocol, err = soap.MarshalString(NewProtocol); err != nil {
return
}
// Response structure.
response := any(nil)
// Perform the SOAP call.
return client.SOAPClient.PerformActionCtx(ctx, urn_LegacyWANIPConnection_1, "DeletePortMapping", request, response)
}
// GetExternalIPAddressCtx implements upnpClient
func (client *legacyWANIPConnection1) GetExternalIPAddressCtx(ctx context.Context) (NewExternalIPAddress string, err error) {
// Request structure.
request := any(nil)
// Response structure.
response := &struct {
NewExternalIPAddress string
}{}
// Perform the SOAP call.
if err = client.SOAPClient.PerformActionCtx(ctx, urn_LegacyWANIPConnection_1, "GetExternalIPAddress", request, response); err != nil {
return
}
if NewExternalIPAddress, err = soap.UnmarshalString(response.NewExternalIPAddress); err != nil {
return
}
return
}
// GetStatusInfoCtx implements upnpClient
func (client *legacyWANIPConnection1) GetStatusInfoCtx(ctx context.Context) (NewConnectionStatus string, NewLastConnectionError string, NewUptime uint32, err error) {
// Request structure.
request := any(nil)
// Response structure.
response := &struct {
NewConnectionStatus string
NewLastConnectionError string
NewUpTime string // NOTE: the "T" is capitalized here, per the spec, though it's lowercase in the newer UPnP spec
}{}
// Perform the SOAP call.
if err = client.SOAPClient.PerformActionCtx(ctx, urn_LegacyWANIPConnection_1, "GetStatusInfo", request, response); err != nil {
return
}
if NewConnectionStatus, err = soap.UnmarshalString(response.NewConnectionStatus); err != nil {
return
}
if NewLastConnectionError, err = soap.UnmarshalString(response.NewLastConnectionError); err != nil {
return
}
if NewUptime, err = soap.UnmarshalUi4(response.NewUpTime); err != nil {
return
}
return
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package portmapper
import (
"context"
"encoding/binary"
"fmt"
"net/netip"
"time"
)
// References:
//
// https://www.rfc-editor.org/rfc/pdfrfc/rfc6887.txt.pdf
// https://tools.ietf.org/html/rfc6887
//go:generate go run tailscale.com/cmd/addlicense -file pcpresultcode_string.go go run golang.org/x/tools/cmd/stringer -type=pcpResultCode -trimprefix=pcpCode
type pcpResultCode uint8
// PCP constants
const (
pcpVersion = 2
pcpDefaultPort = 5351
// Use the same lifetime as NAT-PMP given that we don't know how long
// the IP assignment is valid for.
pcpMapLifetimeSec = 7200
pcpCodeOK pcpResultCode = 0
pcpCodeNotAuthorized pcpResultCode = 2
// From RFC 6887:
// ADDRESS_MISMATCH: The source IP address of the request packet does
// not match the contents of the PCP Client's IP Address field, due
// to an unexpected NAT on the path between the PCP client and the
// PCP-controlled NAT or firewall.
pcpCodeAddressMismatch pcpResultCode = 12
pcpOpReply = 0x80 // OR'd into request's op code on response
pcpOpAnnounce = 0
pcpOpMap = 1
pcpUDPMapping = 17 // portmap UDP
pcpTCPMapping = 6 // portmap TCP
)
// A pcpNonce is a cookie assigned by the router when issuing a mapping, that
// the client retains in order to release or update the mapping later.
type pcpNonce [12]byte
type pcpMapping struct {
c *Client
gw netip.AddrPort
internal netip.AddrPort
external netip.AddrPort
renewAfter time.Time
goodUntil time.Time
epoch uint32
nonce pcpNonce
}
func (p *pcpMapping) MappingType() string { return "pcp" }
func (p *pcpMapping) GoodUntil() time.Time { return p.goodUntil }
func (p *pcpMapping) RenewAfter() time.Time { return p.renewAfter }
func (p *pcpMapping) External() netip.AddrPort { return p.external }
func (p *pcpMapping) MappingDebug() string {
return fmt.Sprintf("pcpMapping{gw:%v, external:%v, internal:%v, renewAfter:%d, goodUntil:%d, nonce:%x}",
p.gw, p.external, p.internal,
p.renewAfter.Unix(), p.goodUntil.Unix(), p.nonce)
}
func (p *pcpMapping) Release(ctx context.Context) {
uc, err := p.c.listenPacket(ctx, "udp4", ":0")
if err != nil {
return
}
defer uc.Close()
// Per RFC 6887 section 15.1 (with Errata ID 3621), a mapping-delete
// request (lifetime 0) MUST set the Suggested External Port to zero and
// the Suggested External Address to the all-zeros address of the family
// being deleted: ::ffff:0.0.0.0 for IPv4, :: for IPv6.
zeroExtAddr := netip.IPv4Unspecified()
if p.external.Addr().Is6() {
zeroExtAddr = netip.IPv6Unspecified()
}
pkt := buildPCPRequestMappingPacket(p.internal.Addr(), p.internal.Port(), 0, 0, zeroExtAddr, p.nonce)
uc.WriteToUDPAddrPort(pkt, p.gw)
}
// buildPCPRequestMappingPacket generates a PCP packet with a MAP opcode.
// To create a packet which deletes a mapping, lifetimeSec, prevPort and prevExternalIP should be set to 0.
// If prevPort is not known, it should be set to 0.
// If prevExternalIP is not known, it should be set to 0.0.0.0.
// Renewing or deleting a mapping must reuse the nonce from the original response.
func buildPCPRequestMappingPacket(
myIP netip.Addr,
localPort, prevPort uint16,
lifetimeSec uint32,
prevExternalIP netip.Addr,
nonce pcpNonce,
) (pkt []byte) {
// 24 byte common PCP header + 36 bytes of MAP-specific fields
pkt = make([]byte, 24+36)
pkt[0] = pcpVersion
pkt[1] = pcpOpMap
binary.BigEndian.PutUint32(pkt[4:8], lifetimeSec)
myIP16 := myIP.As16()
copy(pkt[8:24], myIP16[:])
mapOp := pkt[24:]
copy(mapOp[:12], nonce[:])
mapOp[12] = pcpUDPMapping
binary.BigEndian.PutUint16(mapOp[16:18], localPort)
binary.BigEndian.PutUint16(mapOp[18:20], prevPort)
prevExternalIP16 := prevExternalIP.As16()
copy(mapOp[20:], prevExternalIP16[:])
return pkt
}
// parsePCPMapResponse parses resp into a partially populated pcpMapping.
// In particular, its Client is not populated.
func parsePCPMapResponse(resp []byte) (*pcpMapping, error) {
if len(resp) < 60 {
return nil, fmt.Errorf("Does not appear to be PCP MAP response")
}
res, ok := parsePCPResponse(resp[:24])
if !ok {
return nil, fmt.Errorf("Invalid PCP common header")
}
if res.ResultCode == pcpCodeNotAuthorized {
return nil, fmt.Errorf("PCP is implemented but not enabled in the router")
}
if res.ResultCode != pcpCodeOK {
return nil, fmt.Errorf("PCP response not ok, code %d", res.ResultCode)
}
externalPort := binary.BigEndian.Uint16(resp[42:44])
externalIPBytes := [16]byte{}
copy(externalIPBytes[:], resp[44:])
externalIP := netip.AddrFrom16(externalIPBytes).Unmap()
external := netip.AddrPortFrom(externalIP, externalPort)
lifetime := time.Second * time.Duration(res.Lifetime)
now := time.Now()
mapping := &pcpMapping{
external: external,
renewAfter: now.Add(lifetime / 2),
goodUntil: now.Add(lifetime),
epoch: res.Epoch,
}
copy(mapping.nonce[:], resp[24:36])
return mapping, nil
}
// pcpAnnounceRequest generates a PCP packet with an ANNOUNCE opcode.
func pcpAnnounceRequest(myIP netip.Addr) []byte {
// See https://tools.ietf.org/html/rfc6887#section-7.1
pkt := make([]byte, 24)
pkt[0] = pcpVersion
pkt[1] = pcpOpAnnounce
myIP16 := myIP.As16()
copy(pkt[8:], myIP16[:])
return pkt
}
type pcpResponse struct {
OpCode uint8
ResultCode pcpResultCode
Lifetime uint32
Epoch uint32
}
func parsePCPResponse(b []byte) (res pcpResponse, ok bool) {
if len(b) < 24 || b[0] != pcpVersion {
return
}
res.OpCode = b[1]
res.ResultCode = pcpResultCode(b[3])
res.Lifetime = binary.BigEndian.Uint32(b[4:])
res.Epoch = binary.BigEndian.Uint32(b[8:])
return res, true
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by "stringer -type=pcpResultCode -trimprefix=pcpCode"; DO NOT EDIT.
package portmapper
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[pcpCodeOK-0]
_ = x[pcpCodeNotAuthorized-2]
_ = x[pcpCodeAddressMismatch-12]
}
const (
_pcpResultCode_name_0 = "OK"
_pcpResultCode_name_1 = "NotAuthorized"
_pcpResultCode_name_2 = "AddressMismatch"
)
func (i pcpResultCode) String() string {
switch {
case i == 0:
return _pcpResultCode_name_0
case i == 2:
return _pcpResultCode_name_1
case i == 12:
return _pcpResultCode_name_2
default:
return "pcpResultCode(" + strconv.FormatInt(int64(i), 10) + ")"
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by "stringer -type=pmpResultCode -trimprefix=pmpCode"; DO NOT EDIT.
package portmapper
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[pmpCodeOK-0]
_ = x[pmpCodeUnsupportedVersion-1]
_ = x[pmpCodeNotAuthorized-2]
_ = x[pmpCodeNetworkFailure-3]
_ = x[pmpCodeOutOfResources-4]
_ = x[pmpCodeUnsupportedOpcode-5]
}
const _pmpResultCode_name = "OKUnsupportedVersionNotAuthorizedNetworkFailureOutOfResourcesUnsupportedOpcode"
var _pmpResultCode_index = [...]uint8{0, 2, 20, 33, 47, 61, 78}
func (i pmpResultCode) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_pmpResultCode_index)-1 {
return "pmpResultCode(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _pmpResultCode_name[_pmpResultCode_index[idx]:_pmpResultCode_index[idx+1]]
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package portmapper is a UDP port mapping client. It currently allows for mapping over
// NAT-PMP, UPnP, and PCP.
package portmapper
import (
"bytes"
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"slices"
"sync/atomic"
"time"
"go4.org/mem"
"tailscale.com/envknob"
"tailscale.com/feature/buildfeatures"
"tailscale.com/net/netaddr"
"tailscale.com/net/neterror"
"tailscale.com/net/netmon"
"tailscale.com/net/netns"
"tailscale.com/net/portmapper/portmappertype"
"tailscale.com/net/sockstats"
"tailscale.com/syncs"
"tailscale.com/types/logger"
"tailscale.com/types/nettype"
"tailscale.com/util/clientmetric"
"tailscale.com/util/eventbus"
)
var (
ErrNoPortMappingServices = portmappertype.ErrNoPortMappingServices
ErrGatewayRange = portmappertype.ErrGatewayRange
ErrGatewayIPv6 = portmappertype.ErrGatewayIPv6
ErrPortMappingDisabled = portmappertype.ErrPortMappingDisabled
)
var disablePortMapperEnv = envknob.RegisterBool("TS_DISABLE_PORTMAPPER")
// DebugKnobs contains debug configuration that can be provided when creating a
// Client. The zero value is valid for use.
type DebugKnobs struct {
// VerboseLogs tells the Client to print additional debug information
// to its logger.
VerboseLogs bool
// LogHTTP tells the Client to print the raw HTTP logs (from UPnP) to
// its logger. This is useful when debugging buggy UPnP
// implementations.
LogHTTP bool
// Disable* disables a specific service from mapping.
// If the funcs are nil or return false, the service is not disabled.
// Use the corresponding accessor methods without the "Func" suffix
// to check whether a service is disabled.
DisableUPnPFunc func() bool
DisablePMPFunc func() bool
DisablePCPFunc func() bool
// DisableAll, if non-nil, is a func that reports whether all port
// mapping attempts should be disabled.
DisableAll func() bool
}
// DisableUPnP reports whether UPnP is disabled.
func (k *DebugKnobs) DisableUPnP() bool {
return k != nil && k.DisableUPnPFunc != nil && k.DisableUPnPFunc()
}
// DisablePMP reports whether NAT-PMP is disabled.
func (k *DebugKnobs) DisablePMP() bool {
return k != nil && k.DisablePMPFunc != nil && k.DisablePMPFunc()
}
// DisablePCP reports whether PCP is disabled.
func (k *DebugKnobs) DisablePCP() bool {
return k != nil && k.DisablePCPFunc != nil && k.DisablePCPFunc()
}
func (k *DebugKnobs) disableAll() bool {
if disablePortMapperEnv() {
return true
}
if k.DisableAll != nil {
return k.DisableAll()
}
return false
}
// References:
//
// NAT-PMP: https://tools.ietf.org/html/rfc6886
// portMapServiceTimeout is the time we wait for port mapping
// services (UPnP, NAT-PMP, PCP) to respond before we give up and
// decide that they're not there. Since these services are on the
// same LAN as this machine and a single L3 hop away, we don't
// give them much time to respond.
const portMapServiceTimeout = 250 * time.Millisecond
// trustServiceStillAvailableDuration is how often we re-verify a port
// mapping service is available.
const trustServiceStillAvailableDuration = 10 * time.Minute
// Client is a port mapping client.
type Client struct {
// The following two fields must both be non-nil.
// Both are immutable after construction.
pubClient *eventbus.Client
updates *eventbus.Publisher[portmappertype.Mapping]
logf logger.Logf
netMon *netmon.Monitor // optional; nil means interfaces will be looked up on-demand
ipAndGateway func() (gw, ip netip.Addr, ok bool)
onChange func() // or nil
debug DebugKnobs
testPxPPort uint16 // if non-zero, pxpPort to use for tests
testUPnPPort uint16 // if non-zero, uPnPPort to use for tests
mu syncs.Mutex // guards following, and all fields thereof
// runningCreate is whether we're currently working on creating
// a port mapping (whether GetCachedMappingOrStartCreatingOne kicked
// off a createMapping goroutine).
runningCreate bool
lastMyIP netip.Addr
lastGW netip.Addr
closed bool
lastProbe time.Time
// The following PMP fields are populated during Probe
pmpPubIP netip.Addr // non-zero if known
pmpPubIPTime time.Time // time pmpPubIP last verified
pmpLastEpoch uint32
// The following PCP fields are populated during Probe
pcpSawTime time.Time // time we last saw PCP was available
pcpLastEpoch uint32
uPnPSawTime time.Time // time we last saw UPnP was available
uPnPMetas []uPnPDiscoResponse // UPnP UDP discovery responses
uPnPHTTPClient *http.Client // netns-configured HTTP client for UPnP; nil until needed
localPort uint16
mapping mapping // non-nil if we have a mapping
}
var _ portmappertype.Client = (*Client)(nil)
func (c *Client) vlogf(format string, args ...any) {
if c.debug.VerboseLogs {
c.logf(format, args...)
}
}
// mapping represents a created port-mapping over some protocol. It specifies a lease duration,
// how to release the mapping, and whether the map is still valid.
//
// After a mapping is created, it should be immutable, and thus reads should be safe across
// concurrent goroutines.
type mapping interface {
// Release will attempt to unmap the established port mapping. It will block until completion,
// but can be called asynchronously. Release should be idempotent, and thus even if called
// multiple times should not cause additional side-effects.
Release(context.Context)
// GoodUntil will return the lease time that the mapping is valid for.
GoodUntil() time.Time
// RenewAfter returns the earliest time that the mapping should be renewed.
RenewAfter() time.Time
// External indicates what port the mapping can be reached from on the outside.
External() netip.AddrPort
// MappingType returns a descriptive string for this type of mapping.
MappingType() string
// MappingDebug returns a debug string for this mapping, for use when
// printing verbose logs.
MappingDebug() string
}
func (c *Client) HaveMapping() bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.mapping != nil && c.mapping.GoodUntil().After(time.Now())
}
// pmpMapping is an already-created PMP mapping.
//
// All fields are immutable once created.
type pmpMapping struct {
c *Client
gw netip.AddrPort
external netip.AddrPort
internal netip.AddrPort
renewAfter time.Time // the time at which we want to renew the mapping
goodUntil time.Time // the mapping's total lifetime
epoch uint32
}
// externalValid reports whether m.external is valid, with both its IP and Port populated.
func (m *pmpMapping) externalValid() bool {
return m.external.Addr().IsValid() && m.external.Port() != 0
}
func (p *pmpMapping) MappingType() string { return "pmp" }
func (p *pmpMapping) GoodUntil() time.Time { return p.goodUntil }
func (p *pmpMapping) RenewAfter() time.Time { return p.renewAfter }
func (p *pmpMapping) External() netip.AddrPort { return p.external }
func (p *pmpMapping) MappingDebug() string {
return fmt.Sprintf("pmpMapping{gw:%v, external:%v, internal:%v, renewAfter:%d, goodUntil:%d, epoch:%v}",
p.gw, p.external, p.internal,
p.renewAfter.Unix(), p.goodUntil.Unix(),
p.epoch)
}
// Release does a best effort fire-and-forget release of the PMP mapping m.
func (m *pmpMapping) Release(ctx context.Context) {
uc, err := m.c.listenPacket(ctx, "udp4", ":0")
if err != nil {
return
}
defer uc.Close()
pkt := buildPMPRequestMappingPacket(m.internal.Port(), m.external.Port(), pmpMapLifetimeDelete)
uc.WriteToUDPAddrPort(pkt, m.gw)
}
// Config carries the settings for a [Client].
type Config struct {
// EventBus, which must be non-nil, is used for event publication and
// subscription by portmapper clients created from this config.
EventBus *eventbus.Bus
// Logf is called to generate text logs for the client. If nil, logger.Discard is used.
Logf logger.Logf
// NetMon is the network monitor used by the client. It must be non-nil.
NetMon *netmon.Monitor
// DebugKnobs, if non-nil, configure the behaviour of the portmapper for
// debugging. If nil, a sensible set of defaults will be used.
DebugKnobs *DebugKnobs
// OnChange is called to run in a new goroutine whenever the port mapping
// status has changed. If nil, no callback is issued.
OnChange func()
}
// NewClient constructs a new portmapping [Client] from c. It will panic if any
// required parameters are omitted.
func NewClient(c Config) *Client {
switch {
case c.NetMon == nil:
panic("nil NetMon")
case c.EventBus == nil:
panic("nil EventBus")
}
ret := &Client{
logf: c.Logf,
netMon: c.NetMon,
onChange: c.OnChange,
}
if buildfeatures.HasPortMapper {
// TODO(bradfitz): move this to method on netMon
ret.ipAndGateway = netmon.LikelyHomeRouterIP
}
ret.pubClient = c.EventBus.Client("portmapper")
ret.updates = eventbus.Publish[portmappertype.Mapping](ret.pubClient)
if ret.logf == nil {
ret.logf = logger.Discard
}
if c.DebugKnobs != nil {
ret.debug = *c.DebugKnobs
}
return ret
}
// SetGatewayLookupFunc set the func that returns the machine's default gateway IP, and
// the primary IP address for that gateway. It must be called before the client is used.
// If not called, interfaces.LikelyHomeRouterIP is used.
func (c *Client) SetGatewayLookupFunc(f func() (gw, myIP netip.Addr, ok bool)) {
c.ipAndGateway = f
}
// NoteNetworkDown should be called when the network has transitioned to a down state.
// It's too late to release port mappings at this point (the user might've just turned off
// their wifi), but we can make sure we invalidate mappings for later when the network
// comes back.
func (c *Client) NoteNetworkDown() {
c.mu.Lock()
defer c.mu.Unlock()
c.invalidateMappingsLocked(false)
}
func (c *Client) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
if c.closed {
return nil
}
c.closed = true
c.invalidateMappingsLocked(true)
c.updates.Close()
c.pubClient.Close()
// TODO: close some future ever-listening UDP socket(s),
// waiting for multicast announcements from router.
return nil
}
// SetLocalPort updates the local port number to which we want to port
// map UDP traffic.
func (c *Client) SetLocalPort(localPort uint16) {
c.mu.Lock()
defer c.mu.Unlock()
if c.localPort == localPort {
return
}
c.localPort = localPort
c.invalidateMappingsLocked(true)
}
func (c *Client) gatewayAndSelfIP() (gw, myIP netip.Addr, ok bool) {
gw, myIP, ok = c.ipAndGateway()
if !ok {
gw = netip.Addr{}
myIP = netip.Addr{}
}
c.mu.Lock()
defer c.mu.Unlock()
if gw != c.lastGW || myIP != c.lastMyIP || !ok {
c.lastMyIP = myIP
c.lastGW = gw
c.invalidateMappingsLocked(true)
}
return
}
// pxpPort returns the NAT-PMP and PCP port number.
// It returns 5351, except for in tests where it varies by run.
func (c *Client) pxpPort() uint16 {
if c.testPxPPort != 0 {
return c.testPxPPort
}
return pmpDefaultPort
}
// upnpPort returns the UPnP discovery port number.
// It returns 1900, except for in tests where it varies by run.
func (c *Client) upnpPort() uint16 {
if c.testUPnPPort != 0 {
return c.testUPnPPort
}
return upnpDefaultPort
}
func (c *Client) listenPacket(ctx context.Context, network, addr string) (nettype.PacketConn, error) {
ctx = sockstats.WithSockStats(ctx, sockstats.LabelPortmapperClient, c.logf)
// When running under testing conditions, we bind the IGD server
// to localhost, and may be running in an environment where our
// netns code would decide that binding the portmapper client
// socket to the default route interface is the correct way to
// ensure connectivity. This can result in us trying to send
// packets for 127.0.0.1 out the machine's LAN interface, which
// obviously gets dropped on the floor.
//
// So, under those testing conditions, do _not_ use netns to
// create listening sockets. Such sockets are vulnerable to
// routing loops, but it's tests that don't set up routing loops,
// so we don't care.
if c.testPxPPort != 0 || c.testUPnPPort != 0 {
var lc net.ListenConfig
pc, err := lc.ListenPacket(ctx, network, addr)
if err != nil {
return nil, err
}
return pc.(*net.UDPConn), nil
}
pc, err := netns.Listener(c.logf, c.netMon).ListenPacket(ctx, network, addr)
if err != nil {
return nil, err
}
return pc.(*net.UDPConn), nil
}
func (c *Client) invalidateMappingsLocked(releaseOld bool) {
if c.mapping != nil {
if releaseOld {
c.vlogf("releasing %s mapping", c.mapping.MappingType())
c.mapping.Release(context.Background())
}
c.mapping = nil
}
c.pmpPubIP = netip.Addr{}
c.pmpPubIPTime = time.Time{}
c.pmpLastEpoch = 0
c.pcpSawTime = time.Time{}
c.pcpLastEpoch = 0
c.uPnPSawTime = time.Time{}
c.uPnPMetas = nil
}
func (c *Client) sawPMPRecently() bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.sawPMPRecentlyLocked()
}
func (c *Client) sawPMPRecentlyLocked() bool {
return c.pmpPubIP.IsValid() && c.pmpPubIPTime.After(time.Now().Add(-trustServiceStillAvailableDuration))
}
func (c *Client) sawPCPRecently() bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.sawPCPRecentlyLocked()
}
func (c *Client) sawPCPRecentlyLocked() bool {
return c.pcpSawTime.After(time.Now().Add(-trustServiceStillAvailableDuration))
}
func (c *Client) sawUPnPRecently() bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.uPnPSawTime.After(time.Now().Add(-trustServiceStillAvailableDuration))
}
// closeCloserOnContextDone starts a new goroutine to call c.Close
// if/when ctx becomes done.
// To stop the goroutine, call the returned stop func.
func closeCloserOnContextDone(ctx context.Context, c io.Closer) (stop func()) {
// Close uc on ctx being done.
ctxDone := ctx.Done()
if ctxDone == nil {
return func() {}
}
stopWaitDone := make(chan struct{})
go func() {
select {
case <-stopWaitDone:
case <-ctxDone:
c.Close()
}
}()
return func() { close(stopWaitDone) }
}
// NoMappingError is returned when no NAT mapping could be done.
type NoMappingError struct {
err error
}
func (nme NoMappingError) Unwrap() error { return nme.err }
func (nme NoMappingError) Error() string { return fmt.Sprintf("no NAT mapping available: %v", nme.err) }
// IsNoMappingError reports whether err is of type NoMappingError.
func IsNoMappingError(err error) bool {
_, ok := err.(NoMappingError)
return ok
}
// GetCachedMappingOrStartCreatingOne quickly returns with our current cached portmapping, if any.
// If there's not one, it starts up a background goroutine to create one.
// If the background goroutine ends up creating one, the onChange hook registered with the
// NewClient constructor (if any) will fire.
func (c *Client) GetCachedMappingOrStartCreatingOne() (external netip.AddrPort, ok bool) {
c.mu.Lock()
defer c.mu.Unlock()
// Do we have an existing mapping that's valid?
now := time.Now()
if m := c.mapping; m != nil {
if now.Before(m.GoodUntil()) {
if now.After(m.RenewAfter()) {
c.maybeStartMappingLocked()
}
return m.External(), true
}
}
c.maybeStartMappingLocked()
return netip.AddrPort{}, false
}
// maybeStartMappingLocked starts a createMapping goroutine up, if one isn't already running.
//
// c.mu must be held.
func (c *Client) maybeStartMappingLocked() {
if !c.runningCreate {
c.runningCreate = true
go c.createMapping()
}
}
func (c *Client) createMapping() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
defer func() {
c.mu.Lock()
defer c.mu.Unlock()
c.runningCreate = false
}()
mapping, _, err := c.createOrGetMapping(ctx)
if err != nil {
if !IsNoMappingError(err) {
c.logf("createOrGetMapping: %v", err)
}
return
} else if mapping == nil {
return
// TODO(creachadair): This was already logged in createOrGetMapping.
// It really should not happen at all, but we will need to untangle
// the control flow to eliminate that possibility. Meanwhile, this
// mitigates a panic downstream, cf. #16662.
}
c.updates.Publish(portmappertype.Mapping{
External: mapping.External(),
Type: mapping.MappingType(),
GoodUntil: mapping.GoodUntil(),
})
// TODO(creachadair): Remove this entirely once there are no longer any
// places where the callback is set.
if c.onChange != nil {
go c.onChange()
}
}
// wildcardIP is used when the previous external IP is not known for PCP port mapping.
var wildcardIP = netip.MustParseAddr("0.0.0.0")
// createOrGetMapping either creates a new mapping or returns a cached
// valid one.
//
// If no mapping is available, the error will be of type
// NoMappingError; see IsNoMappingError.
func (c *Client) createOrGetMapping(ctx context.Context) (mapping mapping, external netip.AddrPort, err error) {
if c.debug.disableAll() {
return nil, netip.AddrPort{}, NoMappingError{ErrPortMappingDisabled}
}
if c.debug.DisableUPnP() && c.debug.DisablePCP() && c.debug.DisablePMP() {
return nil, netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
}
gw, myIP, ok := c.gatewayAndSelfIP()
if !ok {
return nil, netip.AddrPort{}, NoMappingError{ErrGatewayRange}
}
if gw.Is6() {
return nil, netip.AddrPort{}, NoMappingError{ErrGatewayIPv6}
}
now := time.Now()
// Log what kind of portmap we obtained
reusedExisting := false
defer func() {
if err != nil {
return
}
c.mu.Lock()
defer c.mu.Unlock()
portmapType := "none"
if c.mapping != nil {
portmapType = c.mapping.MappingType()
}
if reusedExisting {
portmapType = "existing-" + portmapType
}
if c.mapping == nil {
c.logf("[unexpected] no error but no stored mapping: now=%d external=%v type=%s",
now.Unix(), external, portmapType)
return
}
// TODO(creachadair): This is more subtle than it should be. Ideally we
// would just return the mapping directly, but there are many different
// paths through the function with carefully-balanced locks, and not all
// the paths have a mapping to return. As a workaround, while we're here
// doing cleanup under the lock, grab the final mapping value and return
// it, so the caller does not need to grab the lock again and potentially
// race with a later update. The mapping itself is concurrency-safe.
//
// We should restructure this code so the locks are properly scoped.
mapping = c.mapping
// Print the internal details of each mapping if we're being verbose.
if c.debug.VerboseLogs {
c.logf("successfully obtained mapping: now=%d external=%v type=%s mapping=%s",
now.Unix(), external, portmapType, c.mapping.MappingDebug())
return
}
c.logf("[v1] successfully obtained mapping: now=%d external=%v type=%s goodUntil=%d renewAfter=%d",
now.Unix(), external, portmapType,
c.mapping.GoodUntil().Unix(), c.mapping.RenewAfter().Unix())
}()
c.mu.Lock()
localPort := c.localPort
internalAddr := netip.AddrPortFrom(myIP, localPort)
// prevPort is the port we had most previously, if any. We try
// to ask for the same port. 0 means to give us any port.
var prevPort uint16
// Do we have an existing mapping that's valid?
if m := c.mapping; m != nil {
if now.Before(m.RenewAfter()) {
defer c.mu.Unlock()
reusedExisting = true
return nil, m.External(), nil
}
// The mapping might still be valid, so just try to renew it.
prevPort = m.External().Port()
}
if c.debug.DisablePCP() && c.debug.DisablePMP() {
c.mu.Unlock()
if external, ok := c.getUPnPPortMapping(ctx, gw, internalAddr, prevPort); ok {
return nil, external, nil
}
c.vlogf("fallback to UPnP due to PCP and PMP being disabled failed")
return nil, netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
}
// If we just did a Probe (e.g. via netchecker) but didn't
// find a PMP service, bail out early rather than probing
// again. Cuts down latency for most clients.
haveRecentPMP := c.sawPMPRecentlyLocked()
haveRecentPCP := c.sawPCPRecentlyLocked()
// Since PMP mapping may require multiple calls, and it's not clear from the outset
// whether we're doing a PCP or PMP call, initialize the PMP mapping here,
// and only return it once completed.
//
// PCP returns all the information necessary for a mapping in a single packet, so we can
// construct it upon receiving that packet.
m := &pmpMapping{
c: c,
gw: netip.AddrPortFrom(gw, c.pxpPort()),
internal: internalAddr,
}
if haveRecentPMP {
m.external = netip.AddrPortFrom(c.pmpPubIP, m.external.Port())
}
if c.lastProbe.After(now.Add(-5*time.Second)) && !haveRecentPMP && !haveRecentPCP {
c.mu.Unlock()
// fallback to UPnP portmapping
if external, ok := c.getUPnPPortMapping(ctx, gw, internalAddr, prevPort); ok {
return nil, external, nil
}
c.vlogf("fallback to UPnP due to no PCP and PMP failed")
return nil, netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
}
var pcpNonce pcpNonce
if m, ok := c.mapping.(*pcpMapping); ok {
// Reuse the existing mapping's nonce so renewals target the same mapping.
pcpNonce = m.nonce
} else {
// New mappings need a random nonce.
rand.Read(pcpNonce[:])
}
c.mu.Unlock()
uc, err := c.listenPacket(ctx, "udp4", ":0")
if err != nil {
return nil, netip.AddrPort{}, err
}
defer uc.Close()
uc.SetReadDeadline(time.Now().Add(portMapServiceTimeout))
defer closeCloserOnContextDone(ctx, uc)()
pxpAddr := netip.AddrPortFrom(gw, c.pxpPort())
preferPCP := !c.debug.DisablePCP() && (c.debug.DisablePMP() || (!haveRecentPMP && haveRecentPCP))
// Create a mapping, defaulting to PMP unless only PCP was seen recently.
if preferPCP {
// TODO replace wildcardIP here with previous external if known.
// Only do PCP mapping in the case when PMP did not appear to be available recently.
pkt := buildPCPRequestMappingPacket(myIP, localPort, prevPort, pcpMapLifetimeSec, wildcardIP, pcpNonce)
if _, err := uc.WriteToUDPAddrPort(pkt, pxpAddr); err != nil {
if neterror.TreatAsLostUDP(err) {
err = NoMappingError{ErrNoPortMappingServices}
}
return nil, netip.AddrPort{}, err
}
} else {
// Ask for our external address if needed.
if !m.external.Addr().IsValid() {
if _, err := uc.WriteToUDPAddrPort(pmpReqExternalAddrPacket, pxpAddr); err != nil {
if neterror.TreatAsLostUDP(err) {
err = NoMappingError{ErrNoPortMappingServices}
}
return nil, netip.AddrPort{}, err
}
}
pkt := buildPMPRequestMappingPacket(localPort, prevPort, pmpMapLifetimeSec)
if _, err := uc.WriteToUDPAddrPort(pkt, pxpAddr); err != nil {
if neterror.TreatAsLostUDP(err) {
err = NoMappingError{ErrNoPortMappingServices}
}
return nil, netip.AddrPort{}, err
}
}
res := make([]byte, 1500)
for {
n, src, err := uc.ReadFromUDPAddrPort(res)
if err != nil {
if ctx.Err() == context.Canceled {
return nil, netip.AddrPort{}, err
}
// fallback to UPnP portmapping
if mapping, ok := c.getUPnPPortMapping(ctx, gw, internalAddr, prevPort); ok {
return nil, mapping, nil
}
return nil, netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
}
src = netaddr.Unmap(src)
if !src.IsValid() {
continue
}
if src == pxpAddr {
version := res[0]
switch version {
case pmpVersion:
pres, ok := parsePMPResponse(res[:n])
if !ok {
c.logf("unexpected PMP response: % 02x", res[:n])
continue
}
if pres.ResultCode != 0 {
return nil, netip.AddrPort{}, NoMappingError{fmt.Errorf("PMP response Op=0x%x,Res=0x%x", pres.OpCode, pres.ResultCode)}
}
if pres.OpCode == pmpOpReply|pmpOpMapPublicAddr {
m.external = netip.AddrPortFrom(pres.PublicAddr, m.external.Port())
}
if pres.OpCode == pmpOpReply|pmpOpMapUDP {
m.external = netip.AddrPortFrom(m.external.Addr(), pres.ExternalPort)
d := time.Duration(pres.MappingValidSeconds) * time.Second
now := time.Now()
m.goodUntil = now.Add(d)
m.renewAfter = now.Add(d / 2) // renew in half the time
m.epoch = pres.SecondsSinceEpoch
}
case pcpVersion:
// Ignore responses related to another client.
// A response packet is a 24-byte header followed by an operation
// specific payload. For a MAP operation, the first 12 bytes of
// the payload are the client-generated nonce.
if n < 36 || !bytes.Equal(res[24:36], pcpNonce[:]) {
c.logf("ignoring PCP response with missing or mismatched nonce")
continue
}
pcpMapping, err := parsePCPMapResponse(res[:n])
if err != nil {
c.logf("failed to get PCP mapping: %v", err)
// PCP should only have a single packet response
return nil, netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
}
pcpMapping.c = c
pcpMapping.internal = m.internal
pcpMapping.gw = netip.AddrPortFrom(gw, c.pxpPort())
c.mu.Lock()
defer c.mu.Unlock()
c.mapping = pcpMapping
return pcpMapping, pcpMapping.external, nil
default:
c.logf("unknown PMP/PCP version number: %d %v", version, res[:n])
return nil, netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
}
}
if m.externalValid() {
c.mu.Lock()
defer c.mu.Unlock()
c.mapping = m
return nil, m.external, nil
}
}
}
//go:generate go run tailscale.com/cmd/addlicense -file pmpresultcode_string.go go run golang.org/x/tools/cmd/stringer -type=pmpResultCode -trimprefix=pmpCode
type pmpResultCode uint16
// NAT-PMP constants.
const (
pmpDefaultPort = 5351
pmpMapLifetimeSec = 7200 // RFC recommended 2 hour map duration
pmpMapLifetimeDelete = 0 // 0 second lifetime deletes
pmpVersion = 0
pmpOpMapPublicAddr = 0
pmpOpMapUDP = 1
pmpOpReply = 0x80 // OR'd into request's op code on response
pmpCodeOK pmpResultCode = 0
pmpCodeUnsupportedVersion pmpResultCode = 1
pmpCodeNotAuthorized pmpResultCode = 2 // "e.g., box supports mapping, but user has turned feature off"
pmpCodeNetworkFailure pmpResultCode = 3 // "e.g., NAT box itself has not obtained a DHCP lease"
pmpCodeOutOfResources pmpResultCode = 4
pmpCodeUnsupportedOpcode pmpResultCode = 5
)
func buildPMPRequestMappingPacket(localPort, prevPort uint16, lifetimeSec uint32) (pkt []byte) {
pkt = make([]byte, 12)
pkt[1] = pmpOpMapUDP
binary.BigEndian.PutUint16(pkt[4:], localPort)
binary.BigEndian.PutUint16(pkt[6:], prevPort)
binary.BigEndian.PutUint32(pkt[8:], lifetimeSec)
return pkt
}
type pmpResponse struct {
OpCode uint8
ResultCode pmpResultCode
SecondsSinceEpoch uint32
// For Map ops:
MappingValidSeconds uint32
InternalPort uint16
ExternalPort uint16
// For public addr ops:
PublicAddr netip.Addr
}
func parsePMPResponse(pkt []byte) (res pmpResponse, ok bool) {
if len(pkt) < 12 {
return
}
ver := pkt[0]
if ver != 0 {
return
}
res.OpCode = pkt[1]
res.ResultCode = pmpResultCode(binary.BigEndian.Uint16(pkt[2:]))
res.SecondsSinceEpoch = binary.BigEndian.Uint32(pkt[4:])
if res.OpCode == pmpOpReply|pmpOpMapUDP {
if len(pkt) != 16 {
return res, false
}
res.InternalPort = binary.BigEndian.Uint16(pkt[8:])
res.ExternalPort = binary.BigEndian.Uint16(pkt[10:])
res.MappingValidSeconds = binary.BigEndian.Uint32(pkt[12:])
}
if res.OpCode == pmpOpReply|pmpOpMapPublicAddr {
if len(pkt) != 12 {
return res, false
}
res.PublicAddr = netaddr.IPv4(pkt[8], pkt[9], pkt[10], pkt[11])
if res.PublicAddr.IsUnspecified() {
// Zero it out so it's not Valid and used accidentally elsewhere.
res.PublicAddr = netip.Addr{}
}
}
return res, true
}
// Probe returns a summary of which port mapping services are
// available on the network.
//
// If a probe has run recently and there haven't been any network changes since,
// the returned result might be server from the Client's cache, without
// sending any network traffic.
func (c *Client) Probe(ctx context.Context) (res portmappertype.ProbeResult, err error) {
if c.debug.disableAll() {
return res, ErrPortMappingDisabled
}
gw, myIP, ok := c.gatewayAndSelfIP()
if !ok {
return res, ErrGatewayRange
}
defer func() {
if err == nil {
c.mu.Lock()
defer c.mu.Unlock()
c.lastProbe = time.Now()
}
}()
uc, err := c.listenPacket(context.Background(), "udp4", ":0")
if err != nil {
c.logf("ProbePCP: %v", err)
return res, err
}
defer uc.Close()
ctx, cancel := context.WithTimeout(ctx, 250*time.Millisecond)
defer cancel()
defer closeCloserOnContextDone(ctx, uc)()
pxpAddr := netip.AddrPortFrom(gw, c.pxpPort())
upnpAddr := netip.AddrPortFrom(gw, c.upnpPort())
upnpMulticastAddr := netip.AddrPortFrom(netaddr.IPv4(239, 255, 255, 250), c.upnpPort())
// Don't send probes to services that we recently learned (for
// the same gw/myIP) are available. See
// https://github.com/tailscale/tailscale/issues/1001
if c.sawPMPRecently() {
res.PMP = true
} else if !c.debug.DisablePMP() {
metricPMPSent.Add(1)
uc.WriteToUDPAddrPort(pmpReqExternalAddrPacket, pxpAddr)
}
if c.sawPCPRecently() {
res.PCP = true
} else if !c.debug.DisablePCP() {
metricPCPSent.Add(1)
uc.WriteToUDPAddrPort(pcpAnnounceRequest(myIP), pxpAddr)
}
if c.sawUPnPRecently() {
res.UPnP = true
} else if !c.debug.DisableUPnP() {
// Strictly speaking, you discover UPnP services by sending an
// SSDP query (which uPnPPacket is) to udp/1900 on the SSDP
// multicast address, and then get a flood of responses back
// from everything on your network.
//
// Empirically, many home routers also respond to SSDP queries
// directed at udp/1900 on their LAN unicast IP
// (e.g. 192.168.1.1). This is handy because it means we can
// probe the router directly and likely get a reply. However,
// the specs do not _require_ UPnP devices to respond to
// unicast SSDP queries, so some conformant UPnP
// implementations only respond to multicast queries.
//
// In theory, we could send just the multicast query and get
// all compliant devices to respond. However, we instead send
// to both a unicast and a multicast addresses, for a couple
// of reasons:
//
// First, some LANs and OSes have broken multicast in one way
// or another, so it's possible for the multicast query to be
// lost while the unicast query gets through. But we still
// have to send the multicast query to also get a response
// from strict-UPnP devices on multicast-working networks.
//
// Second, SSDP's packet dynamics are a bit weird: you send
// the SSDP query from your unicast IP to the SSDP multicast
// IP, but responses are from the UPnP devices's _unicast_ IP
// to your unicast IP. This can confuse some less-intelligent
// stateful host firewalls, who might block the responses. To
// work around this, we send the unicast query first, to teach
// the firewall to expect a unicast response from the router,
// and then send our multicast query. That way, even if the
// device doesn't respond to the unicast query, we've set the
// stage for the host firewall to accept the response to the
// multicast query.
//
// See https://github.com/tailscale/tailscale/issues/3197 for
// an example of a device that strictly implements UPnP, and
// only responds to multicast queries.
//
// Then we send a discovery packet looking for
// urn:schemas-upnp-org:device:InternetGatewayDevice:1 specifically, not
// just ssdp:all, because there appear to be devices which only send
// their first descriptor (like urn:schemas-wifialliance-org:device:WFADevice:1)
// in response to ssdp:all. https://github.com/tailscale/tailscale/issues/3557
metricUPnPSent.Add(1)
uc.WriteToUDPAddrPort(uPnPPacket, upnpAddr)
uc.WriteToUDPAddrPort(uPnPPacket, upnpMulticastAddr)
uc.WriteToUDPAddrPort(uPnPIGDPacket, upnpMulticastAddr)
}
// We can see multiple UPnP responses from LANs with multiple
// UPnP-capable routers. Rather than randomly picking whichever arrives
// first, let's collect all UPnP responses and choose at the end.
//
// We do this by starting a 50ms timer from when the first UDP packet
// is received, and waiting at least that long for more UPnP responses
// to arrive before returning (as long as the first packet is seen
// within the first 200ms of the context creation, which is likely in
// the common case).
//
// This 50ms timer is distinct from the context timeout; it is used to
// delay an early return in the case where we see all three portmapping
// responses (PCP, PMP, UPnP), whereas the context timeout causes the
// loop to exit regardless of what portmapping responses we've seen.
//
// We use an atomic value to signal that the timer has finished.
var (
upnpTimer *time.Timer
upnpTimerDone atomic.Bool
)
defer func() {
if upnpTimer != nil {
upnpTimer.Stop()
}
}()
// Store all returned UPnP responses until we're done, at which point
// we select from all available options.
var upnpResponses []uPnPDiscoResponse
defer func() {
if !res.UPnP || len(upnpResponses) == 0 {
// Either we didn't discover any UPnP responses or
// c.sawUPnPRecently() is true; don't change anything.
return
}
// Deduplicate and sort responses
upnpResponses = processUPnPResponses(upnpResponses)
c.mu.Lock()
c.uPnPSawTime = time.Now()
if !slices.Equal(c.uPnPMetas, upnpResponses) {
c.logf("UPnP meta changed: %+v", upnpResponses)
c.uPnPMetas = upnpResponses
metricUPnPUpdatedMeta.Add(1)
}
c.mu.Unlock()
}()
// This is the main loop that receives UDP packets and parses them into
// PCP, PMP, or UPnP responses, updates our ProbeResult, and stores
// data for use in GetCachedMappingOrStartCreatingOne.
buf := make([]byte, 1500)
pcpHeard := false // true when we get any PCP response
for {
if pcpHeard && res.PMP && res.UPnP {
if upnpTimerDone.Load() {
// Nothing more to discover.
return res, nil
}
// UPnP timer still running; fall through and keep
// receiving packets.
}
n, src, err := uc.ReadFromUDPAddrPort(buf)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
err = nil
}
return res, err
}
// Start timer after we get the first response.
if upnpTimer == nil {
upnpTimer = time.AfterFunc(50*time.Millisecond, func() { upnpTimerDone.Store(true) })
}
ip := src.Addr().Unmap()
handleUPnPResponse := func() {
metricUPnPResponse.Add(1)
if ip != gw {
// https://github.com/tailscale/tailscale/issues/5502
c.logf("UPnP discovery response from %v, but gateway IP is %v", ip, gw)
}
meta, err := parseUPnPDiscoResponse(buf[:n])
if err != nil {
metricUPnPParseErr.Add(1)
c.logf("unrecognized UPnP discovery response; ignoring: %v", err)
return
}
metricUPnPOK.Add(1)
c.logf("[v1] UPnP reply %+v, %q", meta, buf[:n])
// Store the UPnP response for later selection
res.UPnP = true
if len(upnpResponses) > 10 {
c.logf("too many UPnP responses: skipping")
} else {
upnpResponses = append(upnpResponses, meta)
}
}
port := src.Port()
switch port {
case c.upnpPort():
if mem.Contains(mem.B(buf[:n]), mem.S(":InternetGatewayDevice:")) {
handleUPnPResponse()
}
default:
// https://github.com/tailscale/tailscale/issues/7377
if mem.Contains(mem.B(buf[:n]), mem.S(":InternetGatewayDevice:")) {
c.logf("UPnP discovery response from non-UPnP port %d", port)
metricUPnPResponseAlternatePort.Add(1)
handleUPnPResponse()
}
case c.pxpPort(): // same value for PMP and PCP
metricPXPResponse.Add(1)
if pres, ok := parsePCPResponse(buf[:n]); ok {
if pres.OpCode == pcpOpReply|pcpOpAnnounce {
pcpHeard = true
c.mu.Lock()
c.maybeInvalidatePCPMappingLocked(pres.Epoch) // must be before we write to c.pcp*
c.pcpSawTime = time.Now()
c.pcpLastEpoch = pres.Epoch
c.mu.Unlock()
switch pres.ResultCode {
case pcpCodeOK:
c.logf("[v1] Got PCP response: epoch: %v", pres.Epoch)
res.PCP = true
metricPCPOK.Add(1)
continue
case pcpCodeNotAuthorized:
// A PCP service is running, but refuses to
// provide port mapping services.
res.PCP = false
metricPCPNotAuthorized.Add(1)
continue
case pcpCodeAddressMismatch:
// A PCP service is running, but it is behind a NAT, so it can't help us.
res.PCP = false
metricPCPAddressMismatch.Add(1)
continue
default:
// Fall through to unexpected log line.
}
}
metricPCPUnhandledResponseCode.Add(1)
c.logf("unexpected PCP probe response: %+v", pres)
}
if pres, ok := parsePMPResponse(buf[:n]); ok {
if pres.OpCode != pmpOpReply|pmpOpMapPublicAddr {
c.logf("unexpected PMP probe response opcode: %+v", pres)
metricPMPUnhandledOpcode.Add(1)
continue
}
switch pres.ResultCode {
case pmpCodeOK:
metricPMPOK.Add(1)
c.logf("[v1] Got PMP response; IP: %v, epoch: %v", pres.PublicAddr, pres.SecondsSinceEpoch)
res.PMP = true
c.mu.Lock()
c.maybeInvalidatePMPMappingLocked(pres.SecondsSinceEpoch) // must be before we write to c.pmp*
c.pmpPubIP = pres.PublicAddr
c.pmpPubIPTime = time.Now()
c.pmpLastEpoch = pres.SecondsSinceEpoch
c.mu.Unlock()
continue
case pmpCodeNotAuthorized:
metricPMPNotAuthorized.Add(1)
c.logf("PMP probe failed due result code: %+v", pres)
continue
case pmpCodeNetworkFailure:
metricPMPNetworkFailure.Add(1)
c.logf("PMP probe failed due result code: %+v", pres)
continue
case pmpCodeOutOfResources:
metricPMPOutOfResources.Add(1)
c.logf("PMP probe failed due result code: %+v", pres)
continue
}
metricPMPUnhandledResponseCode.Add(1)
c.logf("unexpected PMP probe response: %+v", pres)
}
}
}
}
func (c *Client) maybeInvalidatePMPMappingLocked(epoch uint32) {
if epoch == 0 || c.mapping == nil {
return
}
m, ok := c.mapping.(*pmpMapping)
if !ok {
return
}
if epoch >= m.epoch {
// Epoch increased, which is fine.
//
// TODO: we should more closely follow RFC6887 § 8.5 which also
// requires us to check the current time and the time that this
// epoch was received at.
return
}
// Epoch decreased, so invalidate the mapping and clear PMP fields.
c.logf("invalidating PMP mappings since returned epoch %d < stored epoch %d", epoch, m.epoch)
c.mapping = nil
c.pmpPubIP = netip.Addr{}
c.pmpPubIPTime = time.Time{}
c.pmpLastEpoch = 0
}
func (c *Client) maybeInvalidatePCPMappingLocked(epoch uint32) {
if epoch == 0 || c.mapping == nil {
return
}
m, ok := c.mapping.(*pcpMapping)
if !ok {
return
}
if epoch >= m.epoch {
// Epoch increased, which is fine.
//
// TODO: we should more closely follow RFC6887 § 8.5 which also
// requires us to check the current time and the time that this
// epoch was received at.
return
}
// Epoch decreased, so invalidate the mapping and clear PCP fields.
c.logf("invalidating PCP mappings since returned epoch %d < stored epoch %d", epoch, m.epoch)
c.mapping = nil
c.pcpSawTime = time.Time{}
c.pcpLastEpoch = 0
}
var pmpReqExternalAddrPacket = []byte{pmpVersion, pmpOpMapPublicAddr} // 0, 0
const (
upnpDefaultPort = 1900 // for UDP discovery only; TCP port discovered later
)
// uPnPPacket is the UPnP UDP discovery packet's request body.
var uPnPPacket = []byte("M-SEARCH * HTTP/1.1\r\n" +
"HOST: 239.255.255.250:1900\r\n" +
"ST: ssdp:all\r\n" +
"MAN: \"ssdp:discover\"\r\n" +
"MX: 2\r\n\r\n")
// Send a discovery frame for InternetGatewayDevice, since some devices respond
// to ssdp:all with only their first descriptor (which is often not IGD).
// https://github.com/tailscale/tailscale/issues/3557
var uPnPIGDPacket = []byte("M-SEARCH * HTTP/1.1\r\n" +
"HOST: 239.255.255.250:1900\r\n" +
"ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n" +
"MAN: \"ssdp:discover\"\r\n" +
"MX: 2\r\n\r\n")
// PCP/PMP metrics
var (
// metricPXPResponse counts the number of times we received a PMP/PCP response.
metricPXPResponse = clientmetric.NewCounter("portmap_pxp_response")
// metricPCPSent counts the number of times we sent a PCP request.
metricPCPSent = clientmetric.NewCounter("portmap_pcp_sent")
// metricPCPOK counts the number of times
// we received a successful PCP response.
metricPCPOK = clientmetric.NewCounter("portmap_pcp_ok")
// metricPCPAddressMismatch counts the number of times
// we received a PCP address mismatch result code.
metricPCPAddressMismatch = clientmetric.NewCounter("portmap_pcp_address_mismatch")
// metricPCPNotAuthorized counts the number of times
// we received a PCP not authorized result code.
metricPCPNotAuthorized = clientmetric.NewCounter("portmap_pcp_not_authorized")
// metricPCPUnhandledResponseCode counts the number of times
// we received an (as yet) unhandled PCP result code.
metricPCPUnhandledResponseCode = clientmetric.NewCounter("portmap_pcp_unhandled_response_code")
// metricPMPSent counts the number of times we sent a PMP request.
metricPMPSent = clientmetric.NewCounter("portmap_pmp_sent")
// metricPMPOK counts the number of times
// we received a successful PMP response.
metricPMPOK = clientmetric.NewCounter("portmap_pmp_ok")
// metricPMPUnhandledOpcode counts the number of times
// we received an unhandled PMP opcode.
metricPMPUnhandledOpcode = clientmetric.NewCounter("portmap_pmp_unhandled_opcode")
// metricPMPUnhandledResponseCode counts the number of times
// we received an unhandled PMP result code.
metricPMPUnhandledResponseCode = clientmetric.NewCounter("portmap_pmp_unhandled_response_code")
// metricPMPOutOfResources counts the number of times
// we received a PCP out of resources result code.
metricPMPOutOfResources = clientmetric.NewCounter("portmap_pmp_out_of_resources")
// metricPMPNetworkFailure counts the number of times
// we received a PCP network failure result code.
metricPMPNetworkFailure = clientmetric.NewCounter("portmap_pmp_network_failure")
// metricPMPNotAuthorized counts the number of times
// we received a PCP not authorized result code.
metricPMPNotAuthorized = clientmetric.NewCounter("portmap_pmp_not_authorized")
)
// UPnP metrics
var (
// metricUPnPSent counts the number of times we sent a UPnP request.
metricUPnPSent = clientmetric.NewCounter("portmap_upnp_sent")
// metricUPnPResponse counts the number of times we received a UPnP response.
metricUPnPResponse = clientmetric.NewCounter("portmap_upnp_response")
// metricUPnPResponseAlternatePort counts the number of times we
// received a UPnP response from a port other than the UPnP port.
metricUPnPResponseAlternatePort = clientmetric.NewCounter("portmap_upnp_response_alternate_port")
// metricUPnPSelectLegacy counts the number of times that a legacy
// service was found in a UPnP response.
metricUPnPSelectLegacy = clientmetric.NewCounter("portmap_upnp_select_legacy")
// metricUPnPSelectSingle counts the number of times that only a single
// UPnP device was available in selectBestService.
metricUPnPSelectSingle = clientmetric.NewCounter("portmap_upnp_select_single")
// metricUPnPSelectMultiple counts the number of times that we need to
// select from among multiple UPnP devices in selectBestService.
metricUPnPSelectMultiple = clientmetric.NewCounter("portmap_upnp_select_multiple")
// metricUPnPSelectExternalPublic counts the number of times that
// selectBestService picked a UPnP device with an external public IP.
metricUPnPSelectExternalPublic = clientmetric.NewCounter("portmap_upnp_select_external_public")
// metricUPnPSelectExternalPrivate counts the number of times that
// selectBestService picked a UPnP device with an external private IP.
metricUPnPSelectExternalPrivate = clientmetric.NewCounter("portmap_upnp_select_external_private")
// metricUPnPSelectUp counts the number of times that selectBestService
// picked a UPnP device that was up but with no external IP.
metricUPnPSelectUp = clientmetric.NewCounter("portmap_upnp_select_up")
// metricUPnPSelectNone counts the number of times that selectBestService
// picked a UPnP device that is not up.
metricUPnPSelectNone = clientmetric.NewCounter("portmap_upnp_select_none")
// metricUPnPParseErr counts the number of times we failed to parse a UPnP response.
metricUPnPParseErr = clientmetric.NewCounter("portmap_upnp_parse_err")
// metricUPnPOK counts the number of times we received a usable UPnP response.
metricUPnPOK = clientmetric.NewCounter("portmap_upnp_ok")
// metricUPnPUpdatedMeta counts the number of times
// we received a UPnP response with a new meta.
metricUPnPUpdatedMeta = clientmetric.NewCounter("portmap_upnp_updated_meta")
)
// UPnP error metric that's keyed by code; lazily registered on first read
var (
metricUPnPErrorsByCode syncs.Map[int, *clientmetric.Metric]
)
func getUPnPErrorsMetric(code int) *clientmetric.Metric {
mm, _ := metricUPnPErrorsByCode.LoadOrInit(code, func() *clientmetric.Metric {
// Metric names cannot contain a hyphen, so we handle negative
// numbers by prefixing the name with a "minus_".
var codeStr string
if code < 0 {
codeStr = fmt.Sprintf("portmap_upnp_errors_with_code_minus_%d", -code)
} else {
codeStr = fmt.Sprintf("portmap_upnp_errors_with_code_%d", code)
}
return clientmetric.NewCounter(codeStr)
})
return mm
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !js
// (no raw sockets in JS/WASM)
package portmapper
import (
"bufio"
"bytes"
"cmp"
"context"
"encoding/xml"
"fmt"
"io"
"math/rand/v2"
"net"
"net/http"
"net/netip"
"net/url"
"slices"
"strings"
"sync/atomic"
"time"
"github.com/huin/goupnp"
"github.com/huin/goupnp/dcps/internetgateway2"
"github.com/huin/goupnp/soap"
"tailscale.com/envknob"
"tailscale.com/net/netns"
"tailscale.com/types/logger"
"tailscale.com/util/ctxkey"
"tailscale.com/util/mak"
)
// upnpHTTPClientKey is a context key for storing an HTTP client to use
// for UPnP requests. This allows us to use a custom HTTP client (with custom
// dialer, timeouts, etc.) while using the upstream goupnp library which only
// supports a global HTTPClientDefault.
var upnpHTTPClientKey = ctxkey.New[*http.Client]("portmapper.upnpHTTPClient", nil)
// delegatingRoundTripper implements http.RoundTripper by delegating to
// the HTTP client stored in the request's context. This allows us to use
// per-request HTTP client configuration with the upstream goupnp library.
type delegatingRoundTripper struct {
inner *http.Client
}
func (d delegatingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
if c := upnpHTTPClientKey.Value(req.Context()); c != nil {
return c.Transport.RoundTrip(req)
}
return d.inner.Do(req)
}
func init() {
// The upstream goupnp library uses a global HTTP client for all
// requests, while we want to be able to use a per-Client
// [http.Client]. We replace its global HTTP client with one that
// delegates to the HTTP client stored in the request's context.
old := goupnp.HTTPClientDefault
goupnp.HTTPClientDefault = &http.Client{
Transport: delegatingRoundTripper{old},
}
}
// References:
//
// WANIP Connection v2: http://upnp.org/specs/gw/UPnP-gw-WANIPConnection-v2-Service.pdf
// upnpMapping is a port mapping over the upnp protocol. After being created it is immutable,
// but the client field may be shared across mapping instances.
type upnpMapping struct {
gw netip.Addr
external netip.AddrPort
internal netip.AddrPort
goodUntil time.Time
renewAfter time.Time
// rootDev is the UPnP root device, and may be reused across different
// UPnP mappings.
rootDev *goupnp.RootDevice
// loc is the location used to fetch the rootDev
loc *url.URL
// client is the most recent UPnP client used, and should only be used
// to release an existing mapping; new mappings should be selected from
// the rootDev on each attempt.
client upnpClient
}
// upnpProtocolUDP represents the protocol name for UDP, to be used in the UPnP
// <AddPortMapping> message in the <NewProtocol> field.
//
// NOTE: this must be an upper-case string, or certain routers will reject the
// mapping request. Other implementations like miniupnp send an upper-case
// protocol as well. See:
//
// https://github.com/tailscale/tailscale/issues/7377
const upnpProtocolUDP = "UDP"
func (u *upnpMapping) MappingType() string { return "upnp" }
func (u *upnpMapping) GoodUntil() time.Time { return u.goodUntil }
func (u *upnpMapping) RenewAfter() time.Time { return u.renewAfter }
func (u *upnpMapping) External() netip.AddrPort { return u.external }
func (u *upnpMapping) MappingDebug() string {
return fmt.Sprintf("upnpMapping{gw:%v, external:%v, internal:%v, renewAfter:%d, goodUntil:%d, loc:%q}",
u.gw, u.external, u.internal,
u.renewAfter.Unix(), u.goodUntil.Unix(),
u.loc)
}
func (u *upnpMapping) Release(ctx context.Context) {
u.client.DeletePortMappingCtx(ctx, "", u.external.Port(), upnpProtocolUDP)
}
// upnpClient is an interface over the multiple different clients exported by goupnp,
// exposing the functions we need for portmapping. Those clients are auto-generated from XML-specs,
// which is why they're not very idiomatic.
//
// The method names use the *Ctx suffix to match the upstream goupnp library's convention
// for context-aware methods.
type upnpClient interface {
AddPortMappingCtx(
ctx context.Context,
// remoteHost is the remote device sending packets to this device, in the format of x.x.x.x.
// The empty string, "", means any host out on the internet can send packets in.
remoteHost string,
// externalPort is the exposed port of this port mapping. Visible during NAT operations.
// 0 will let the router select the port, but there is an additional call,
// `AddAnyPortMapping`, which is available on 1 of the 3 possible protocols,
// which should be used if available. See `addAnyPortMapping` below, which calls this if
// `AddAnyPortMapping` is not supported.
externalPort uint16,
// protocol is whether this is over TCP or UDP. Either "TCP" or "UDP".
protocol string,
// internalPort is the port that the gateway device forwards the traffic to.
internalPort uint16,
// internalClient is the IP address that packets will be forwarded to for this mapping.
// Internal client is of the form "x.x.x.x".
internalClient string,
// enabled is whether this portmapping should be enabled or disabled.
enabled bool,
// portMappingDescription is a user-readable description of this portmapping.
portMappingDescription string,
// leaseDurationSec is the duration of this portmapping. The value of this argument must be
// greater than 0. From the spec, it appears if it is set to 0, it will switch to using
// 604800 seconds, but not sure why this is desired. The recommended time is 3600 seconds.
leaseDurationSec uint32,
) error
DeletePortMappingCtx(ctx context.Context, remoteHost string, externalPort uint16, protocol string) error
GetExternalIPAddressCtx(ctx context.Context) (externalIPAddress string, err error)
GetStatusInfoCtx(ctx context.Context) (status string, lastConnError string, uptime uint32, err error)
}
// tsPortMappingDesc gets sent to UPnP clients as a human-readable label for the portmapping.
// It is not used for anything other than labelling.
const tsPortMappingDesc = "tailscale-portmap"
// addAnyPortMapping abstracts over different UPnP client connections, calling
// the available AddAnyPortMapping call if available for WAN IP connection v2,
// otherwise picking either the previous port (if one is present) or a random
// port and trying to obtain a mapping using AddPortMapping.
//
// It returns the new external port (which may not be identical to the external
// port specified), or an error.
//
// TODO(bradfitz): also returned the actual lease duration obtained. and check it regularly.
func addAnyPortMapping(
ctx context.Context,
upnp upnpClient,
externalPort uint16,
internalPort uint16,
internalClient string,
leaseDuration time.Duration,
) (newPort uint16, err error) {
// Some devices don't let clients add a port mapping for privileged
// ports (ports below 1024). Additionally, per section 2.3.18 of the
// UPnP spec, regarding the ExternalPort field:
//
// If this value is specified as a wildcard (i.e. 0), connection
// request on all external ports (that are not otherwise mapped)
// will be forwarded to InternalClient. In the wildcard case, the
// value(s) of InternalPort on InternalClient are ignored by the IGD
// for those connections that are forwarded to InternalClient.
// Obviously only one such entry can exist in the NAT at any time
// and conflicts are handled with a “first write wins” behavior.
//
// We obviously do not want to open all ports on the user's device to
// the internet, so we want to do this prior to calling either
// AddAnyPortMapping or AddPortMapping.
//
// Pick an external port that's greater than 1024 by getting a random
// number in [0, 65535 - 1024] and then adding 1024 to it, shifting the
// range to [1024, 65535].
if externalPort < 1024 {
externalPort = uint16(rand.N(65535-1024) + 1024)
}
// First off, try using AddAnyPortMapping; if there's a conflict, the
// router will pick another port and return it.
if upnp, ok := upnp.(*internetgateway2.WANIPConnection2); ok {
return upnp.AddAnyPortMappingCtx(
ctx,
"",
externalPort,
upnpProtocolUDP,
internalPort,
internalClient,
true,
tsPortMappingDesc,
uint32(leaseDuration.Seconds()),
)
}
// Fall back to using AddPortMapping, which requests a mapping to/from
// a specific external port.
err = upnp.AddPortMappingCtx(
ctx,
"",
externalPort,
upnpProtocolUDP,
internalPort,
internalClient,
true,
tsPortMappingDesc,
uint32(leaseDuration.Seconds()),
)
return externalPort, err
}
// getUPnPRootDevice fetches the UPnP root device given the discovery response,
// ignoring the underlying protocol for now.
// Adapted from https://github.com/huin/goupnp/blob/master/GUIDE.md.
//
// The gw is the detected gateway.
//
// The meta is the most recently parsed UDP discovery packet response
// from the Internet Gateway Device.
func getUPnPRootDevice(ctx context.Context, logf logger.Logf, debug DebugKnobs, gw netip.Addr, meta uPnPDiscoResponse) (rootDev *goupnp.RootDevice, loc *url.URL, err error) {
if debug.DisableUPnP() {
return nil, nil, nil
}
if meta.Location == "" {
return nil, nil, nil
}
if debug.VerboseLogs {
logf("fetching %v", meta.Location)
}
u, err := url.Parse(meta.Location)
if err != nil {
return nil, nil, err
}
ipp, err := netip.ParseAddrPort(u.Host)
if err != nil {
return nil, nil, fmt.Errorf("unexpected host %q in %q", u.Host, meta.Location)
}
if ipp.Addr() != gw {
// https://github.com/tailscale/tailscale/issues/5502
logf("UPnP discovered root %q does not match gateway IP %v; repointing at gateway which is assumed to be floating",
meta.Location, gw)
u.Host = net.JoinHostPort(gw.String(), u.Port())
}
// We're fetching a smallish XML document over plain HTTP
// across the local LAN, without using DNS. There should be
// very few round trips and low latency, so one second is a
// long time.
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
// This part does a network fetch.
root, err := goupnp.DeviceByURLCtx(ctx, u)
if err != nil {
return nil, nil, err
}
return root, u, nil
}
// selectBestService picks the "best" service from the given UPnP root device
// to use to create a port mapping. If selecting the service required querying
// its external IP, it also returns that address. It may return a nil client if
// no supported service was found in the provided *goupnp.RootDevice.
//
// loc is the parsed location that was used to fetch the given RootDevice.
//
// The provided ctx is not retained in the returned upnpClient.
func selectBestService(ctx context.Context, logf logger.Logf, root *goupnp.RootDevice, loc *url.URL) (client upnpClient, externalIP netip.Addr, err error) {
method := "none"
defer func() {
if client == nil {
return
}
logf("saw UPnP type %v at %v; %v (%v), method=%s",
strings.TrimPrefix(fmt.Sprintf("%T", client), "*internetgateway2."),
loc, root.Device.FriendlyName, root.Device.Manufacturer,
method)
}()
// First, get all available clients from the device, and append to our
// list of possible clients. Order matters here; we want to prefer
// WANIPConnection2 over WANIPConnection1 or WANPPPConnection.
wanIP2, _ := internetgateway2.NewWANIPConnection2ClientsFromRootDevice(root, loc)
wanIP1, _ := internetgateway2.NewWANIPConnection1ClientsFromRootDevice(root, loc)
wanPPP, _ := internetgateway2.NewWANPPPConnection1ClientsFromRootDevice(root, loc)
var clients []upnpClient
for _, v := range wanIP2 {
clients = append(clients, v)
}
for _, v := range wanIP1 {
clients = append(clients, v)
}
for _, v := range wanPPP {
clients = append(clients, v)
}
// These are legacy services that were deprecated in 2015, but are
// still in use by older devices; try them just in case.
legacyClients, _ := goupnp.NewServiceClientsFromRootDevice(root, loc, urn_LegacyWANPPPConnection_1)
metricUPnPSelectLegacy.Add(int64(len(legacyClients)))
for _, client := range legacyClients {
clients = append(clients, &legacyWANPPPConnection1{client})
}
legacyClients, _ = goupnp.NewServiceClientsFromRootDevice(root, loc, urn_LegacyWANIPConnection_1)
metricUPnPSelectLegacy.Add(int64(len(legacyClients)))
for _, client := range legacyClients {
clients = append(clients, &legacyWANIPConnection1{client})
}
// If we have no clients, then return right now; if we only have one,
// just select and return it.
if len(clients) == 0 {
return nil, netip.Addr{}, nil
}
if len(clients) == 1 {
method = "single"
metricUPnPSelectSingle.Add(1)
return clients[0], netip.Addr{}, nil
}
metricUPnPSelectMultiple.Add(1)
// In order to maximize the chances that we find a valid UPnP device
// that can give us a port mapping, we check a few properties:
// 1. Whether the device is "online", as defined by GetStatusInfo
// 2. Whether the device has an external IP address, as defined by
// GetExternalIPAddress
// 3. Whether the device's external IP address is a public address
// or a private one.
//
// We prefer a device where all of the above is true, and fall back if
// none are found.
//
// In order to save on network requests, iterate through all devices
// and determine how many "points" they have based on the above
// criteria, but return immediately if we find one that meets all
// three.
var (
connected = make(map[upnpClient]bool)
externalIPs map[upnpClient]netip.Addr
)
for _, svc := range clients {
isConnected := serviceIsConnected(ctx, logf, svc)
connected[svc] = isConnected
// Don't bother checking for an external IP if the device isn't
// connected; technically this could happen with a misbehaving
// device, but that seems unlikely.
if !isConnected {
continue
}
// Check if the device has an external IP address.
extIP, err := svc.GetExternalIPAddressCtx(ctx)
if err != nil {
continue
}
externalIP, err := netip.ParseAddr(extIP)
if err != nil {
continue
}
mak.Set(&externalIPs, svc, externalIP)
// If we get here, this device has a non-private external IP
// and is up, so we can just return it.
if !externalIP.IsPrivate() {
method = "ext-public"
metricUPnPSelectExternalPublic.Add(1)
return svc, externalIP, nil
}
}
// Okay, we have no devices that meet all the available options. Fall
// back to first checking for devices that are up and have a private
// external IP (order matters), and then devices that are up, and then
// just anything at all.
//
// try=0 Up + private external IP
// try=1 Up
for try := 0; try <= 1; try++ {
for _, svc := range clients {
if !connected[svc] {
continue
}
_, hasExtIP := externalIPs[svc]
if hasExtIP {
method = "ext-private"
metricUPnPSelectExternalPrivate.Add(1)
return svc, externalIPs[svc], nil
} else if try == 1 {
method = "up"
metricUPnPSelectUp.Add(1)
return svc, netip.Addr{}, nil
}
}
}
// Nothing is up, but we have something (length of clients checked
// above); just return the first one.
metricUPnPSelectNone.Add(1)
return clients[0], netip.Addr{}, nil
}
// serviceIsConnected returns whether a given UPnP service is connected, based
// on the NewConnectionStatus field returned from GetStatusInfo.
func serviceIsConnected(ctx context.Context, logf logger.Logf, svc upnpClient) bool {
status, _ /* NewLastConnectionError */, _ /* NewUptime */, err := svc.GetStatusInfoCtx(ctx)
if err != nil {
return false
}
return status == "Connected" || status == "Up"
}
func (c *Client) upnpHTTPClientLocked() *http.Client {
if c.uPnPHTTPClient == nil {
c.uPnPHTTPClient = &http.Client{
Transport: &http.Transport{
DialContext: netns.NewDialer(c.logf, c.netMon).DialContext,
IdleConnTimeout: 2 * time.Second, // LAN is cheap
},
}
if c.debug.LogHTTP {
c.uPnPHTTPClient = requestLogger(c.logf, c.uPnPHTTPClient)
}
}
return c.uPnPHTTPClient
}
var (
disableUPnpEnv = envknob.RegisterBool("TS_DISABLE_UPNP")
)
// getUPnPPortMapping attempts to create a port-mapping over the UPnP protocol. On success,
// it will return the externally exposed IP and port. Otherwise, it will return a zeroed IP and
// port and an error.
func (c *Client) getUPnPPortMapping(
ctx context.Context,
gw netip.Addr,
internal netip.AddrPort,
prevPort uint16,
) (external netip.AddrPort, ok bool) {
if disableUPnpEnv() || c.debug.DisableUPnP() {
return netip.AddrPort{}, false
}
now := time.Now()
upnp := &upnpMapping{
gw: gw,
internal: internal,
}
// We can have multiple UPnP "meta" values (which correspond to the
// UPnP discovery responses received). We want to try all of them when
// obtaining a mapping, but also prefer any existing mapping's root
// device (if present), since that will allow us to renew an existing
// mapping instead of creating a new one.
// Start by grabbing the list of metas, any existing mapping, and
// creating a HTTP client for use.
c.mu.Lock()
oldMapping, ok := c.mapping.(*upnpMapping)
metas := c.uPnPMetas
ctx = upnpHTTPClientKey.WithValue(ctx, c.upnpHTTPClientLocked())
c.mu.Unlock()
// Wrapper for a uPnPDiscoResponse with an optional existing root
// device + URL (if we've got a previous cached mapping).
type step struct {
rootDev *goupnp.RootDevice // if nil, use 'meta'
loc *url.URL // non-nil if rootDev is non-nil
meta uPnPDiscoResponse
}
var steps []step
// Now, if we have an existing mapping, swap that mapping's entry to
// the first entry in our "metas" list so we try it first.
haveOldMapping := ok && oldMapping != nil
if haveOldMapping && oldMapping.rootDev != nil {
steps = append(steps, step{rootDev: oldMapping.rootDev, loc: oldMapping.loc})
}
// Note: this includes the meta for a previously-cached mapping, in
// case the rootDev changes.
for _, meta := range metas {
steps = append(steps, step{meta: meta})
}
// Now, iterate through every meta that we have trying to get an
// external IP address. If we succeed, we'll return; if we fail, we
// continue this loop.
var errs []error
for _, step := range steps {
var (
rootDev *goupnp.RootDevice
loc *url.URL
err error
)
if step.rootDev != nil {
rootDev = step.rootDev
loc = step.loc
} else {
rootDev, loc, err = getUPnPRootDevice(ctx, c.logf, c.debug, gw, step.meta)
c.vlogf("getUPnPRootDevice: loc=%q err=%v", loc, err)
if err != nil {
errs = append(errs, err)
continue
}
}
if rootDev == nil {
continue
}
// This actually performs the port mapping operation using this
// root device.
//
// TODO(andrew-d): this can successfully perform a portmap and
// return an externalAddrPort that refers to a non-public IP
// address if the first selected RootDevice is a device that is
// connected to another internal network. This is still better
// than randomly flapping between multiple devices, but we
// should probably split this up further to try the best
// service (one with an external IP) first, instead of
// iterating by device.
//
// This is probably sufficiently unlikely that I'm leaving that
// as a follow-up task if it's necessary.
externalAddrPort, client, err := c.tryUPnPPortmapWithDevice(ctx, internal, prevPort, rootDev, loc)
if err != nil {
errs = append(errs, err)
continue
}
// If we get here, we're successful; we can cache this mapping,
// update our local port, and then return.
//
// NOTE: this time might not technically be accurate if we created a
// permanent lease above, but we should still re-check the presence of
// the lease on a regular basis so we use it anyway.
d := time.Duration(pmpMapLifetimeSec) * time.Second
upnp.goodUntil = now.Add(d)
upnp.renewAfter = now.Add(d / 2)
upnp.external = externalAddrPort
upnp.rootDev = rootDev
upnp.loc = loc
upnp.client = client
c.mu.Lock()
defer c.mu.Unlock()
c.mapping = upnp
c.localPort = internal.Port()
return upnp.external, true
}
// If we get here, we didn't get anything.
// TODO(andrew-d): use or log errs?
_ = errs
return netip.AddrPort{}, false
}
// tryUPnPPortmapWithDevice attempts to perform a port forward from the given
// UPnP device to the 'internal' address. It tries to re-use the previous port,
// if a non-zero value is provided, and handles retries and errors about
// unsupported features.
//
// It returns the external address and port that was mapped (i.e. the
// address+port that another Tailscale node can use to make a connection to
// this one) and the UPnP client that was used to obtain that mapping.
func (c *Client) tryUPnPPortmapWithDevice(
ctx context.Context,
internal netip.AddrPort,
prevPort uint16,
rootDev *goupnp.RootDevice,
loc *url.URL,
) (netip.AddrPort, upnpClient, error) {
// Select the best mapping service from the given root device. This
// makes network requests, and can vary from mapping to mapping if the
// upstream device's connection status changes.
client, externalIP, err := selectBestService(ctx, c.logf, rootDev, loc)
if err != nil {
return netip.AddrPort{}, nil, err
}
// If we have no client, we cannot continue; this can happen if we get
// a valid UPnP response that does not contain any of the service types
// that we know how to use.
if client == nil {
// For debugging, print all available services that we aren't
// using because they're not supported; use c.vlogf so we don't
// spam the logs unless verbose debugging is turned on.
rootDev.Device.VisitServices(func(s *goupnp.Service) {
c.vlogf("unsupported UPnP service: Type=%q ID=%q ControlURL=%q", s.ServiceType, s.ServiceId, s.ControlURL.Str)
})
return netip.AddrPort{}, nil, fmt.Errorf("no supported UPnP clients")
}
// Obtain and validate the external IP before creating the mapping. In
// particular, MikroTik only supports permanent leases, so forgetting a
// successfully-created mapping after a later request fails leaks the rule
// forever and each retry creates another one.
if !externalIP.IsValid() {
extIP, err := client.GetExternalIPAddressCtx(ctx)
c.vlogf("client.GetExternalIPAddress: %v, %v", extIP, err)
if err != nil {
return netip.AddrPort{}, nil, err
}
externalIP, err = netip.ParseAddr(extIP)
if err != nil {
return netip.AddrPort{}, nil, err
}
}
// Do a bit of validation on the external IP; we've seen cases where
// UPnP devices return the public IP 0.0.0.0, which obviously doesn't
// work as an endpoint.
//
// See: https://github.com/tailscale/corp/issues/23538
if externalIP.IsUnspecified() {
c.logf("UPnP returned unspecified external IP %v", externalIP)
return netip.AddrPort{}, nil, fmt.Errorf("UPnP returned unspecified external IP")
} else if externalIP.IsLoopback() {
c.logf("UPnP returned loopback external IP %v", externalIP)
return netip.AddrPort{}, nil, fmt.Errorf("UPnP returned loopback external IP")
}
// Start by trying to make a temporary lease with a duration.
var newPort uint16
newPort, err = addAnyPortMapping(
ctx,
client,
prevPort,
internal.Port(),
internal.Addr().String(),
pmpMapLifetimeSec*time.Second,
)
c.vlogf("addAnyPortMapping: %v, err=%q", newPort, err)
// If this is an error and the code is
// "OnlyPermanentLeasesSupported", then we retry with no lease
// duration; see the following issue for details:
// https://github.com/tailscale/tailscale/issues/9343
if err != nil {
code, ok := getUPnPErrorCode(err)
if ok {
getUPnPErrorsMetric(code).Add(1)
}
// From the UPnP spec: http://upnp.org/specs/gw/UPnP-gw-WANIPConnection-v2-Service.pdf
// 402: Invalid Args (see: https://github.com/tailscale/tailscale/issues/15223)
// 725: OnlyPermanentLeasesSupported
if ok && (code == 402 || code == 725) {
newPort, err = addAnyPortMapping(
ctx,
client,
prevPort,
internal.Port(),
internal.Addr().String(),
0, // permanent
)
c.vlogf("addAnyPortMapping: errcode=%d retried: port=%v err=%v", code, newPort, err)
}
}
if err != nil {
return netip.AddrPort{}, nil, err
}
return netip.AddrPortFrom(externalIP, newPort), client, nil
}
// processUPnPResponses sorts and deduplicates a list of UPnP discovery
// responses, returning the possibly-reduced list.
//
// It will perform a consistent sort of the provided responses, so if we have
// multiple valid UPnP destinations a consistent option will be picked every
// time.
func processUPnPResponses(metas []uPnPDiscoResponse) []uPnPDiscoResponse {
// Sort and compact all responses to remove duplicates; since
// we send multiple probes, we often get duplicate responses.
slices.SortFunc(metas, func(a, b uPnPDiscoResponse) int {
// Sort the USN in reverse, so that
// "InternetGatewayDevice:2" sorts before
// "InternetGatewayDevice:1".
if ii := cmp.Compare(a.USN, b.USN); ii != 0 {
return -ii
}
if ii := cmp.Compare(a.Location, b.Location); ii != 0 {
return ii
}
return cmp.Compare(a.Server, b.Server)
})
// We can get multiple responses that point to a single Location, since
// we probe for both ssdp:all and InternetGatewayDevice:1 as
// independent packets. Compact by comparing the Location and Server,
// but not the USN (which contains the device being offered).
//
// Since the slices are sorted in reverse above, this means that if we
// get a discovery response for both InternetGatewayDevice:1 and
// InternetGatewayDevice:2, we'll keep the first
// (InternetGatewayDevice:2) response, which is what we want.
metas = slices.CompactFunc(metas, func(a, b uPnPDiscoResponse) bool {
return a.Location == b.Location && a.Server == b.Server
})
return metas
}
// getUPnPErrorCode returns the UPnP error code from the given response, if the
// error is a SOAP error in the proper format, and a boolean indicating whether
// the provided error was actually a UPnP error.
func getUPnPErrorCode(err error) (int, bool) {
soapErr, ok := err.(*soap.SOAPFaultError)
if !ok {
return 0, false
}
var upnpErr struct {
XMLName xml.Name
Code int `xml:"errorCode"`
Description string `xml:"errorDescription"`
}
if err := xml.Unmarshal([]byte(soapErr.Detail.Raw), &upnpErr); err != nil {
return 0, false
}
if upnpErr.XMLName.Local != "UPnPError" {
return 0, false
}
return upnpErr.Code, true
}
type uPnPDiscoResponse struct {
Location string
// Server describes what version the UPnP is, such as MiniUPnPd/2.x.x
Server string
// USN is the serial number of the device, which also contains
// what kind of UPnP service is being offered, i.e. InternetGatewayDevice:2
USN string
}
// parseUPnPDiscoResponse parses a UPnP HTTP-over-UDP discovery response.
func parseUPnPDiscoResponse(body []byte) (uPnPDiscoResponse, error) {
var r uPnPDiscoResponse
res, err := http.ReadResponse(bufio.NewReaderSize(bytes.NewReader(body), 128), nil)
if err != nil {
return r, err
}
r.Location = res.Header.Get("Location")
r.Server = res.Header.Get("Server")
r.USN = res.Header.Get("Usn")
return r, nil
}
type roundTripperFunc func(*http.Request) (*http.Response, error)
func (r roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return r(req)
}
func requestLogger(logf logger.Logf, client *http.Client) *http.Client {
// Clone the HTTP client, and override the Transport to log to the
// provided logger.
ret := *client
oldTransport := ret.Transport
var requestCounter atomic.Uint64
loggingTransport := roundTripperFunc(func(req *http.Request) (*http.Response, error) {
ctr := requestCounter.Add(1)
// Read the body and re-set it.
var (
body []byte
err error
)
if req.Body != nil {
body, err = io.ReadAll(req.Body)
req.Body.Close()
if err != nil {
return nil, err
}
req.Body = io.NopCloser(bytes.NewReader(body))
}
logf("request[%d]: %s %q body=%q", ctr, req.Method, req.URL, body)
resp, err := oldTransport.RoundTrip(req)
if err != nil {
logf("response[%d]: err=%v", ctr, err)
return nil, err
}
// Read the response body
if resp.Body != nil {
body, err = io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
logf("response[%d]: %d bodyErr=%v", ctr, resp.StatusCode, err)
return nil, err
}
resp.Body = io.NopCloser(bytes.NewReader(body))
}
logf("response[%d]: %d body=%q", ctr, resp.StatusCode, body)
return resp, nil
})
ret.Transport = loggingTransport
return &ret
}
// 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/types/nettype"
"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
}
// CloseRead implements [nettype.HalfCloser], allowing the underlying Conn
// to be half-closed if possible. Otherwise, this is a no-op.
func (c sysConn) CloseRead() error {
if hc, ok := c.Conn.(nettype.HalfCloser); ok {
return hc.CloseRead()
}
return nil
}
// CloseWrite implements [nettype.HalfCloser], allowing the underlying Conn
// to be half-closed if possible. Otherwise, this is a no-op.
func (c sysConn) CloseWrite() error {
if hc, ok := c.Conn.(nettype.HalfCloser); ok {
return hc.CloseWrite()
}
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 wsconn contains an adapter type that turns
// a websocket connection into a net.Conn.
package wsconn
import (
"context"
"fmt"
"io"
"math"
"net"
"os"
"sync/atomic"
"time"
"github.com/coder/websocket"
"tailscale.com/syncs"
)
// NetConn converts a *websocket.Conn into a net.Conn.
//
// It's for tunneling arbitrary protocols over WebSockets.
// Few users of the library will need this but it's tricky to implement
// correctly and so provided in the library.
// See https://github.com/nhooyr/websocket/issues/100.
//
// Every Write to the net.Conn will correspond to a message write of
// the given type on *websocket.Conn.
//
// The passed ctx bounds the lifetime of the net.Conn. If cancelled,
// all reads and writes on the net.Conn will be cancelled.
//
// If a message is read that is not of the correct type, the connection
// will be closed with StatusUnsupportedData and an error will be returned.
//
// Close will close the *websocket.Conn with StatusNormalClosure.
//
// When a deadline is hit, the connection will be closed. This is
// different from most net.Conn implementations where only the
// reading/writing goroutines are interrupted but the connection is kept alive.
//
// The Addr methods will return a mock net.Addr that returns "websocket" for Network
// and "websocket/unknown-addr" for String.
//
// A received StatusNormalClosure or StatusGoingAway close frame will be translated to
// io.EOF when reading.
//
// The given remoteAddr will be the value of the returned conn's
// RemoteAddr().String(). For best compatibility with consumers of
// conns, the string should be an ip:port if available, but in the
// absence of that it can be any string that describes the remote
// endpoint, or the empty string to makes RemoteAddr() return a place
// holder value.
func NetConn(ctx context.Context, c *websocket.Conn, msgType websocket.MessageType, remoteAddr string) net.Conn {
nc := &netConn{
c: c,
msgType: msgType,
remoteAddr: remoteAddr,
}
var writeCancel context.CancelFunc
nc.writeContext, writeCancel = context.WithCancel(ctx)
nc.writeTimer = time.AfterFunc(math.MaxInt64, func() {
nc.afterWriteDeadline.Store(true)
if nc.writing.Load() {
writeCancel()
}
})
if !nc.writeTimer.Stop() {
<-nc.writeTimer.C
}
var readCancel context.CancelFunc
nc.readContext, readCancel = context.WithCancel(ctx)
nc.readTimer = time.AfterFunc(math.MaxInt64, func() {
nc.afterReadDeadline.Store(true)
if nc.reading.Load() {
readCancel()
}
})
if !nc.readTimer.Stop() {
<-nc.readTimer.C
}
return nc
}
type netConn struct {
c *websocket.Conn
msgType websocket.MessageType
remoteAddr string
writeTimer *time.Timer
writeContext context.Context
writing atomic.Bool
afterWriteDeadline atomic.Bool
readTimer *time.Timer
readContext context.Context
reading atomic.Bool
afterReadDeadline atomic.Bool
readMu syncs.Mutex
// eofed is true if the reader should return io.EOF from the Read call.
//
// +checklocks:readMu
eofed bool
// +checklocks:readMu
reader io.Reader
}
var _ net.Conn = &netConn{}
func (c *netConn) Close() error {
return c.c.Close(websocket.StatusNormalClosure, "")
}
func (c *netConn) Write(p []byte) (int, error) {
if c.afterWriteDeadline.Load() {
return 0, os.ErrDeadlineExceeded
}
if swapped := c.writing.CompareAndSwap(false, true); !swapped {
panic("Concurrent writes not allowed")
}
defer c.writing.Store(false)
err := c.c.Write(c.writeContext, c.msgType, p)
if err != nil {
return 0, err
}
return len(p), nil
}
func (c *netConn) Read(p []byte) (int, error) {
if c.afterReadDeadline.Load() {
return 0, os.ErrDeadlineExceeded
}
c.readMu.Lock()
defer c.readMu.Unlock()
if swapped := c.reading.CompareAndSwap(false, true); !swapped {
panic("Concurrent reads not allowed")
}
defer c.reading.Store(false)
if c.eofed {
return 0, io.EOF
}
if c.reader == nil {
typ, r, err := c.c.Reader(c.readContext)
if err != nil {
switch websocket.CloseStatus(err) {
case websocket.StatusNormalClosure, websocket.StatusGoingAway:
c.eofed = true
return 0, io.EOF
}
return 0, err
}
if typ != c.msgType {
err := fmt.Errorf("unexpected frame type read (expected %v): %v", c.msgType, typ)
c.c.Close(websocket.StatusUnsupportedData, err.Error())
return 0, err
}
c.reader = r
}
n, err := c.reader.Read(p)
if err == io.EOF {
c.reader = nil
err = nil
}
return n, err
}
type websocketAddr struct {
addr string
}
func (a websocketAddr) Network() string {
return "websocket"
}
func (a websocketAddr) String() string {
if a.addr != "" {
return a.addr
}
return "websocket/unknown-addr"
}
func (c *netConn) RemoteAddr() net.Addr {
return websocketAddr{c.remoteAddr}
}
func (c *netConn) LocalAddr() net.Addr {
return websocketAddr{""}
}
func (c *netConn) SetDeadline(t time.Time) error {
c.SetWriteDeadline(t)
c.SetReadDeadline(t)
return nil
}
func (c *netConn) SetWriteDeadline(t time.Time) error {
if t.IsZero() {
c.writeTimer.Stop()
} else {
c.writeTimer.Reset(time.Until(t))
}
c.afterWriteDeadline.Store(false)
return nil
}
func (c *netConn) SetReadDeadline(t time.Time) error {
if t.IsZero() {
c.readTimer.Stop()
} else {
c.readTimer.Reset(time.Until(t))
}
c.afterReadDeadline.Store(false)
return nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package sessionrecording contains session recording utils shared amongst
// Tailscale SSH and Kubernetes API server proxy session recording.
package sessionrecording
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httptrace"
"net/netip"
"sync/atomic"
"time"
"tailscale.com/net/netutil"
"tailscale.com/net/netx"
"tailscale.com/tailcfg"
"tailscale.com/util/httpm"
)
const (
// Timeout for an individual DialFunc call for a single recorder address.
perDialAttemptTimeout = 5 * time.Second
// Timeout for the V2 API HEAD probe request (supportsV2).
http2ProbeTimeout = 10 * time.Second
// Maximum timeout for trying all available recorders, including V2 API
// probes and dial attempts.
allDialAttemptsTimeout = 30 * time.Second
)
// uploadAckWindow is the period of time to wait for an ackFrame from recorder
// before terminating the connection. This is a variable to allow overriding it
// in tests.
var uploadAckWindow = 30 * time.Second
// idleConnTimeout is the idle timeout for connections to the recorder.
var idleConnTimeout = 30 * time.Second
// ConnectToRecorder connects to the recorder at any of the provided addresses.
// It returns the first successful response, or a multierr if all attempts fail.
//
// On success, it returns a WriteCloser that can be used to upload the
// recording, and a channel that will be sent an error (or nil) when the upload
// fails or completes.
//
// In both cases, a slice of SSHRecordingAttempts is returned which detail the
// attempted recorder IP and the error message, if the attempt failed. The
// attempts are in order the recorder(s) was attempted. If successful a
// successful connection is made, the last attempt in the slice is the
// attempt for connected recorder.
func ConnectToRecorder(ctx context.Context, recs []netip.AddrPort, dial netx.DialFunc) (io.WriteCloser, []*tailcfg.SSHRecordingAttempt, <-chan error, error) {
if len(recs) == 0 {
return nil, nil, nil, errors.New("no recorders configured")
}
// We use a special context for dialing the recorder, so that we can
// limit the time we spend dialing to 30 seconds and still have an
// unbounded context for the upload.
dialCtx, dialCancel := context.WithTimeout(ctx, allDialAttemptsTimeout)
defer dialCancel()
var errs []error
var attempts []*tailcfg.SSHRecordingAttempt
for _, ap := range recs {
attempt := &tailcfg.SSHRecordingAttempt{
Recorder: ap,
}
attempts = append(attempts, attempt)
var pw io.WriteCloser
var errChan <-chan error
var err error
hc := clientHTTP2(dialCtx, dial)
// We need to probe V2 support using a separate HEAD request. Sending
// an HTTP/2 POST request to a HTTP/1 server will just "hang" until the
// request body is closed (instead of returning a 404 as one would
// expect). Sending a HEAD request without a body does not have that
// problem.
if supportsV2(ctx, hc, ap) {
pw, errChan, err = connectV2(ctx, hc, ap)
} else {
pw, errChan, err = connectV1(ctx, clientHTTP1(dialCtx, dial), ap)
}
if err != nil {
err = fmt.Errorf("recording: error starting recording on %q: %w", ap, err)
attempt.FailureMessage = err.Error()
errs = append(errs, err)
continue
}
return pw, attempts, errChan, nil
}
return nil, attempts, nil, errors.Join(errs...)
}
// supportsV2 checks whether a recorder instance supports the /v2/record
// endpoint.
func supportsV2(ctx context.Context, hc *http.Client, ap netip.AddrPort) bool {
ctx, cancel := context.WithTimeout(ctx, http2ProbeTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, httpm.HEAD, fmt.Sprintf("http://%s/v2/record", ap), nil)
if err != nil {
return false
}
resp, err := hc.Do(req)
if err != nil {
return false
}
defer resp.Body.Close()
return resp.StatusCode == http.StatusOK && resp.ProtoMajor > 1
}
// supportsEvent checks whether a recorder instance supports the /v2/event
// endpoint.
func supportsEvent(ctx context.Context, hc *http.Client, ap netip.AddrPort) (bool, error) {
ctx, cancel := context.WithTimeout(ctx, http2ProbeTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, httpm.HEAD, fmt.Sprintf("http://%s/v2/event", ap), nil)
if err != nil {
return false, err
}
resp, err := hc.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return true, nil
}
if resp.StatusCode != http.StatusNotFound {
body, err := io.ReadAll(resp.Body)
if err != nil {
// Handle the case where reading the body itself fails
return false, fmt.Errorf("server returned non-OK status: %s, and failed to read body: %w", resp.Status, err)
}
return false, fmt.Errorf("server returned non-OK status: %d: %s", resp.StatusCode, string(body))
}
return false, nil
}
const addressNotSupportEventv2 = `recorder at address %q does not support "/v2/event" endpoint`
type EventAPINotSupportedErr struct {
ap netip.AddrPort
}
func (e EventAPINotSupportedErr) Error() string {
return fmt.Sprintf(addressNotSupportEventv2, e.ap)
}
// SendEvent sends an event the tsrecorders /v2/event endpoint.
func SendEvent(ap netip.AddrPort, event io.Reader, dial netx.DialFunc) (retErr error) {
ctx, cancel := context.WithCancel(context.Background())
defer func() {
if retErr != nil {
cancel()
}
}()
client := clientHTTP1(ctx, dial)
supported, err := supportsEvent(ctx, client, ap)
if err != nil {
return fmt.Errorf("error checking support for `/v2/event` endpoint: %w", err)
}
if !supported {
return EventAPINotSupportedErr{
ap: ap,
}
}
req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("http://%s/v2/event", ap.String()), event)
if err != nil {
return fmt.Errorf("error creating request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("error sending request: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
// Handle the case where reading the body itself fails
return fmt.Errorf("server returned non-OK status: %s, and failed to read body: %w", resp.Status, err)
}
return fmt.Errorf("server returned non-OK status: %d: %s", resp.StatusCode, string(body))
}
return nil
}
// connectV1 connects to the legacy /record endpoint on the recorder. It is
// used for backwards-compatibility with older tsrecorder instances.
//
// On success, it returns a WriteCloser that can be used to upload the
// recording, and a channel that will be sent an error (or nil) when the upload
// fails or completes.
func connectV1(ctx context.Context, hc *http.Client, ap netip.AddrPort) (io.WriteCloser, <-chan error, error) {
// We dial the recorder and wait for it to send a 100-continue
// response before returning from this function. This ensures that
// the recorder is ready to accept the recording.
// got100 is closed when we receive the 100-continue response.
got100 := make(chan struct{})
ctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
Got100Continue: func() {
close(got100)
},
})
pr, pw := io.Pipe()
req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("http://%s/record", ap), pr)
if err != nil {
return nil, nil, err
}
// We set the Expect header to 100-continue, so that the recorder
// will send a 100-continue response before it starts reading the
// request body.
req.Header.Set("Expect", "100-continue")
// errChan is used to indicate the result of the request.
errChan := make(chan error, 1)
go func() {
defer hc.CloseIdleConnections()
defer close(errChan)
resp, err := hc.Do(req)
if err != nil {
errChan <- err
return
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
errChan <- fmt.Errorf("recording: unexpected status: %v", resp.Status)
return
}
}()
select {
case <-got100:
return pw, errChan, nil
case err := <-errChan:
// If we get an error before we get the 100-continue response,
// we need to try another recorder.
if err == nil {
// If the error is nil, we got a 200 response, which
// is unexpected as we haven't sent any data yet.
err = errors.New("recording: unexpected EOF")
}
return nil, nil, err
}
}
// connectV2 connects to the /v2/record endpoint on the recorder over HTTP/2.
// It explicitly tracks ack frames sent in the response and terminates the
// connection if sent recording data is un-acked for uploadAckWindow.
//
// On success, it returns a WriteCloser that can be used to upload the
// recording, and a channel that will be sent an error (or nil) when the upload
// fails or completes.
func connectV2(ctx context.Context, hc *http.Client, ap netip.AddrPort) (io.WriteCloser, <-chan error, error) {
pr, pw := io.Pipe()
upload := &readCounter{r: pr}
req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("http://%s/v2/record", ap), upload)
if err != nil {
return nil, nil, err
}
// With HTTP/2, hc.Do will not block while the request body is being sent.
// It will return immediately and allow us to consume the response body at
// the same time.
resp, err := hc.Do(req)
if err != nil {
return nil, nil, err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, nil, fmt.Errorf("recording: unexpected status: %v", resp.Status)
}
errChan := make(chan error, 1)
acks := make(chan int64)
// Read acks from the response and send them to the acks channel.
go func() {
defer hc.CloseIdleConnections()
defer close(errChan)
defer close(acks)
defer resp.Body.Close()
defer pw.Close()
dec := json.NewDecoder(resp.Body)
for {
var frame v2ResponseFrame
if err := dec.Decode(&frame); err != nil {
if !errors.Is(err, io.EOF) {
errChan <- fmt.Errorf("recording: unexpected error receiving acks: %w", err)
}
return
}
if frame.Error != "" {
errChan <- fmt.Errorf("recording: received error from the recorder: %q", frame.Error)
return
}
select {
case acks <- frame.Ack:
case <-ctx.Done():
return
}
}
}()
// Track acks from the acks channel.
go func() {
// Hack for tests: some tests modify uploadAckWindow and reset it when
// the test ends. This can race with t.Reset call below. Making a copy
// here is a lazy workaround to not wait for this goroutine to exit in
// the test cases.
uploadAckWindow := uploadAckWindow
// This timer fires if we didn't receive an ack for too long.
t := time.NewTimer(uploadAckWindow)
defer t.Stop()
for {
select {
case <-t.C:
// Close the pipe which terminates the connection and cleans up
// other goroutines. Note that tsrecorder will send us ack
// frames even if there is no new data to ack. This helps
// detect broken recorder connection if the session is idle.
pr.CloseWithError(errNoAcks)
resp.Body.Close()
return
case _, ok := <-acks:
if !ok {
// acks channel closed means that the goroutine reading them
// finished, which means that the request has ended.
return
}
// TODO(awly): limit how far behind the received acks can be. This
// should handle scenarios where a session suddenly dumps a lot of
// output.
t.Reset(uploadAckWindow)
case <-ctx.Done():
return
}
}
}()
return pw, errChan, nil
}
var errNoAcks = errors.New("did not receive ack frames from the recorder in 30s")
type v2ResponseFrame struct {
// Ack is the number of bytes received from the client so far. The bytes
// are not guaranteed to be durably stored yet.
Ack int64 `json:"ack,omitempty"`
// Error is an error encountered while storing the recording. Error is only
// ever set as the last frame in the response.
Error string `json:"error,omitempty"`
}
// readCounter is an io.Reader that counts how many bytes were read.
type readCounter struct {
r io.Reader
sent atomic.Int64
}
func (u *readCounter) Read(buf []byte) (int, error) {
n, err := u.r.Read(buf)
u.sent.Add(int64(n))
return n, err
}
// clientHTTP1 returns a claassic http.Client with a per-dial context. It uses
// dialCtx and adds a 5s timeout to it.
func clientHTTP1(dialCtx context.Context, dial netx.DialFunc) *http.Client {
tr := netutil.NewDefaultTransport()
tr.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
perAttemptCtx, cancel := context.WithTimeout(ctx, perDialAttemptTimeout)
defer cancel()
go func() {
select {
case <-perAttemptCtx.Done():
case <-dialCtx.Done():
cancel()
}
}()
return dial(perAttemptCtx, network, addr)
}
tr.IdleConnTimeout = idleConnTimeout
return &http.Client{Transport: tr}
}
// clientHTTP2 is like clientHTTP1 but returns an http.Client suitable for h2c
// requests (HTTP/2 over plaintext). Unfortunately the same client does not
// work for HTTP/1 so we need to split these up.
func clientHTTP2(dialCtx context.Context, dial netx.DialFunc) *http.Client {
var p http.Protocols
p.SetUnencryptedHTTP2(true)
return &http.Client{
Transport: &http.Transport{
Protocols: &p,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
perAttemptCtx, cancel := context.WithTimeout(ctx, perDialAttemptTimeout)
defer cancel()
go func() {
select {
case <-perAttemptCtx.Done():
case <-dialCtx.Done():
cancel()
}
}()
return dial(perAttemptCtx, network, addr)
},
IdleConnTimeout: idleConnTimeout,
},
}
}
// 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 lets sandboxed macOS scope quad-100 for simple DNS
// configs, so public queries can use the OS resolver (including DoH).
// Custom split resolvers or unrouted forward/PTR records still require
// quad-100 as primary. Without this attribute, quad-100 stays primary.
// Has no effect on other platforms. See tailscale/corp#45534 and
// tailscale/corp#48693.
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"
// Conn25Connector specifies which conn25 apps a peer is a connector for.
// Presence of this field means that this client may try to use the peer as
// a connector for the domains specified in the named app's configuration.
Conn25Connector Cap = "conn25-connector-apps"
)
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
// - 148: 2026-09-15: Client understands [Node.StableTailnetID]
// - 149: 2026-10-07: Sentinel version to request the server send back [NodeAttrCacheNetworkMaps] set to enabled
const CurrentCapabilityVersion CapabilityVersion = 149
// 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 == ""
}
// StableTailnetID is the stable and opaque identifier of the tailnet this node
// is a member of, as used to identify the tailnet in the Tailscale API. These
// IDs are guaranteed to be unique across all tailnets for a single control
// server instance, but may or may not be unique across different servers.
type StableTailnetID string
func (id StableTailnetID) IsZero() bool {
return id == ""
}
// 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"`
// StableTailnetID is the identifier of the tailnet this node is a
// member of.
//
// Control only populates this for the self node in a MapResponse
// (MapResponse.Node); it is empty for peers.
StableTailnetID StableTailnetID `json:",omitzero"`
}
// 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.TLPublic
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 &&
n.StableTailnetID == n2.StableTailnetID
}
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
StableTailnetID StableTailnetID
}{})
// 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.TLPublic
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)
}
// StableTailnetID is the identifier of the tailnet this node is a
// member of.
//
// Control only populates this for the self node in a MapResponse
// (MapResponse.Node); it is empty for peers.
func (v NodeView) StableTailnetID() StableTailnetID { return v.ж.StableTailnetID }
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
StableTailnetID StableTailnetID
}{})
// 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.TLPublic { 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.TLPublic
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
//go:build !ts_omit_tailnetlock
package tka
import (
"bytes"
"crypto/ed25519"
"encoding/base32"
"errors"
"fmt"
"github.com/fxamacker/cbor/v2"
"golang.org/x/crypto/blake2s"
"tailscale.com/types/tkatype"
"tailscale.com/util/set"
)
// AUMHash represents the BLAKE2s digest of an Authority Update Message (AUM).
type AUMHash [blake2s.Size]byte
var base32StdNoPad = base32.StdEncoding.WithPadding(base32.NoPadding)
// String returns the AUMHash encoded as base32.
// This is suitable for use as a filename, and for storing in text-preferred media.
func (h AUMHash) String() string {
return base32StdNoPad.EncodeToString(h[:])
}
// UnmarshalText implements encoding.TextUnmarshaler.
func (h *AUMHash) UnmarshalText(text []byte) error {
if ln := base32StdNoPad.DecodedLen(len(text)); ln != len(h) {
return fmt.Errorf("tka.AUMHash.UnmarshalText: text wrong length: %d, want %d", ln, len(text))
}
if _, err := base32StdNoPad.Decode(h[:], text); err != nil {
return fmt.Errorf("tka.AUMHash.UnmarshalText: %w", err)
}
return nil
}
// AppendText implements encoding.TextAppender.
func (h AUMHash) AppendText(b []byte) ([]byte, error) {
return base32StdNoPad.AppendEncode(b, h[:]), nil
}
// MarshalText implements encoding.TextMarshaler.
func (h AUMHash) MarshalText() ([]byte, error) {
return h.AppendText(nil)
}
// IsZero returns true if the hash is the empty value.
func (h AUMHash) IsZero() bool {
return h == (AUMHash{})
}
// PrevAUMHash represents the BLAKE2s digest of an Authority Update Message (AUM).
// Unlike an AUMHash, this can be empty if there is no previous AUM hash
// (which occurs in the genesis AUM).
type PrevAUMHash []byte
// String returns the PrevAUMHash encoded as base32.
// This is suitable for use as a filename, and for storing in text-preferred media.
func (h PrevAUMHash) String() string {
return base32StdNoPad.EncodeToString(h[:])
}
// AUMKind describes valid AUM types.
type AUMKind uint8
// Valid AUM types. Do NOT reorder.
const (
AUMInvalid AUMKind = iota
// An AddKey AUM describes a new key trusted by the TKA.
//
// Only the Key optional field may be set.
AUMAddKey
// A RemoveKey AUM describes the removal of a key trusted by TKA.
//
// Only the KeyID optional field may be set.
AUMRemoveKey
// A NoOp AUM carries no information and is used in tests.
AUMNoOp
// A UpdateKey AUM updates the metadata or votes of an existing key.
//
// Only KeyID, along with either/or Meta or Votes optional fields
// may be set.
AUMUpdateKey
// A Checkpoint AUM specifies the full state of the TKA.
//
// Only the State optional field may be set.
AUMCheckpoint
)
func (k AUMKind) String() string {
switch k {
case AUMInvalid:
return "invalid"
case AUMAddKey:
return "add-key"
case AUMRemoveKey:
return "remove-key"
case AUMNoOp:
return "no-op"
case AUMCheckpoint:
return "checkpoint"
case AUMUpdateKey:
return "update-key"
default:
return fmt.Sprintf("AUM?<%d>", int(k))
}
}
// AUM describes an Authority Update Message.
//
// The rules for adding new types of AUMs (MessageKind):
// - CBOR key IDs must never be changed.
// - New AUM types must not change semantics that are manipulated by other
// AUM types.
// - The serialization of existing data cannot change (in other words, if
// an existing serialization test in aum_test.go fails, you need to try a
// different approach).
//
// The rules for adding new fields are as follows:
// - Must all be optional.
// - An unset value must not result in serialization overhead. This is
// necessary so the serialization of older AUMs stays the same.
// - New processing semantics of the new fields must be compatible with the
// behavior of old clients (which will ignore the field).
// - No floats!
type AUM struct {
MessageKind AUMKind `cbor:"1,keyasint"`
PrevAUMHash PrevAUMHash `cbor:"2,keyasint"`
// Key encodes a public key to be added to the key authority.
// This field is used for AddKey AUMs.
Key *Key `cbor:"3,keyasint,omitempty"`
// KeyID references a public key which is part of the key authority.
// This field is used for RemoveKey and UpdateKey AUMs.
KeyID tkatype.KeyID `cbor:"4,keyasint,omitempty"`
// State describes the full state of the key authority.
// This field is used for Checkpoint AUMs.
State *State `cbor:"5,keyasint,omitempty"`
// Votes and Meta describe properties of a key in the key authority.
// These fields are used for UpdateKey AUMs.
Votes *uint `cbor:"6,keyasint,omitempty"`
Meta map[string]string `cbor:"7,keyasint,omitempty"`
// Signatures lists the signatures over this AUM.
// CBOR key 23 is the last key which can be encoded as a single byte.
Signatures []tkatype.Signature `cbor:"23,keyasint,omitempty"`
}
// StaticValidate returns a nil error if the AUM is well-formed.
func (a *AUM) StaticValidate() error {
if a.Key != nil {
if err := a.Key.StaticValidate(); err != nil {
return err
}
}
if a.PrevAUMHash != nil && len(a.PrevAUMHash) == 0 {
return errors.New("absent parent must be represented by a nil slice")
}
for i, sig := range a.Signatures {
if len(sig.KeyID) != 32 || len(sig.Signature) != ed25519.SignatureSize {
return fmt.Errorf("signature %d has missing keyID or malformed signature", i)
}
}
if a.State != nil {
if err := a.State.staticValidateCheckpoint(); err != nil {
return fmt.Errorf("checkpoint state: %v", err)
}
}
switch a.MessageKind {
case AUMAddKey:
if a.Key == nil {
return errors.New("AddKey AUMs must contain a key")
}
if a.KeyID != nil || a.State != nil || a.Votes != nil || a.Meta != nil {
return errors.New("AddKey AUMs may only specify a Key")
}
case AUMRemoveKey:
if len(a.KeyID) == 0 {
return errors.New("RemoveKey AUMs must specify a key ID")
}
if a.Key != nil || a.State != nil || a.Votes != nil || a.Meta != nil {
return errors.New("RemoveKey AUMs may only specify a KeyID")
}
case AUMUpdateKey:
if len(a.KeyID) == 0 {
return errors.New("UpdateKey AUMs must specify a key ID")
}
if a.Meta == nil && a.Votes == nil {
return errors.New("UpdateKey AUMs must contain an update to votes or key metadata")
}
if a.Key != nil || a.State != nil {
return errors.New("UpdateKey AUMs may only specify KeyID, Votes, and Meta")
}
case AUMCheckpoint:
if a.State == nil {
return errors.New("Checkpoint AUMs must specify the state")
}
if a.KeyID != nil || a.Key != nil || a.Votes != nil || a.Meta != nil {
return errors.New("Checkpoint AUMs may only specify State")
}
case AUMNoOp:
default:
// An AUM with an unknown message kind was received! That means
// that a future version of tailscaled added some feature we don't
// understand.
//
// The future-compatibility contract for AUM message types is that
// they must only add new features, not change the semantics of existing
// mechanisms or features. As such, old clients can safely ignore them.
}
return nil
}
// Serialize returns the given AUM in a serialized format.
//
// We would implement encoding.BinaryMarshaler, except that would
// unfortunately get called by the cbor marshaller resulting in infinite
// recursion.
func (a *AUM) Serialize() tkatype.MarshaledAUM {
// Why CBOR and not something like JSON?
//
// The main function of an AUM is to carry signed data. Signatures are
// over digests, so the serialized representation must be deterministic.
// Further, experience with other attempts (JWS/JWT,SAML,X509 etc) has
// taught us that even subtle behaviors such as how you handle invalid
// or unrecognized fields + any invariants in subsequent re-serialization
// can easily lead to security-relevant logic bugs. It's certainly possible
// to invent a workable scheme by massaging a JSON parsing library, though
// profoundly unwise.
//
// CBOR is one of the few encoding schemes that are appropriate for use
// with signatures and has security-conscious parsing + serialization
// rules baked into the spec. We use the CTAP2 mode, which is well
// understood + widely-implemented, and already proven for use in signing
// assertions through its use by FIDO2 devices.
out := bytes.NewBuffer(make([]byte, 0, 128))
encoder, err := cbor.CTAP2EncOptions().EncMode()
if err != nil {
// Deterministic validation of encoding options, should
// never fail.
panic(err)
}
if err := encoder.NewEncoder(out).Encode(a); err != nil {
// Writing to a bytes.Buffer should never fail.
panic(err)
}
return out.Bytes()
}
// Unserialize decodes bytes representing a marshaled AUM.
//
// We would implement encoding.BinaryUnmarshaler, except that would
// unfortunately get called by the cbor unmarshaller resulting in infinite
// recursion.
func (a *AUM) Unserialize(data []byte) error {
dec, _ := cborDecOpts.DecMode()
return dec.Unmarshal(data, a)
}
// Hash returns a cryptographic digest of all AUM contents.
func (a *AUM) Hash() AUMHash {
return blake2s.Sum256(a.Serialize())
}
// SigHash returns the cryptographic digest which a signature
// is over.
//
// This is identical to Hash() except the Signatures are not
// serialized. Without this, the hash used for signatures
// would be circularly dependent on the signatures.
func (a AUM) SigHash() tkatype.AUMSigHash {
dupe := a
dupe.Signatures = nil
return blake2s.Sum256(dupe.Serialize())
}
// Parent returns the parent's AUM hash and true, or a
// zero value and false if there was no parent.
func (a *AUM) Parent() (h AUMHash, ok bool) {
if len(a.PrevAUMHash) > 0 {
copy(h[:], a.PrevAUMHash)
return h, true
}
return h, false
}
func (a *AUM) sign25519(priv ed25519.PrivateKey) error {
key := Key{Kind: Key25519, Public: priv.Public().(ed25519.PublicKey)}
sigHash := a.SigHash()
keyID, err := key.ID()
if err != nil {
return err
}
a.Signatures = append(a.Signatures, tkatype.Signature{
KeyID: keyID,
Signature: ed25519.Sign(priv, sigHash[:]),
})
return nil
}
// Weight computes the 'signature weight' of the AUM
// based on keys in the state machine. The caller must
// ensure that all signatures are valid.
//
// More formally: W = Sum(key.votes)
//
// AUMs with a higher weight than their siblings
// are preferred when resolving forks in the AUM chain.
func (a *AUM) Weight(state State) uint {
var weight uint
// Track the keys that have already been used, so two
// signatures with the same key do not result in 2x
// the weight.
//
// Despite the wire encoding being []byte, all KeyIDs are
// 32 bytes. As such, we use that as the key for the map,
// because map keys cannot be slices.
seenKeys := make(set.Set[[32]byte], 6)
for _, sig := range a.Signatures {
if len(sig.KeyID) != 32 {
panic("unexpected: keyIDs are 32 bytes")
}
var keyID [32]byte
copy(keyID[:], sig.KeyID)
key, err := state.GetKey(sig.KeyID)
if err != nil {
if err == ErrNoSuchKey {
// Signatures with an unknown key do not contribute
// to the weight.
continue
}
panic(err)
}
if seenKeys.Contains(keyID) {
continue
}
weight += key.Votes
seenKeys.Add(keyID)
}
return weight
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_tailnetlock
package tka
import (
"fmt"
"os"
"tailscale.com/types/tkatype"
)
// Types implementing Signer can sign update messages.
type Signer interface {
// SignAUM returns signatures for the AUM encoded by the given AUMSigHash.
SignAUM(tkatype.AUMSigHash) ([]tkatype.Signature, error)
}
// UpdateBuilder implements a builder for changes to the tailnet
// key authority.
//
// Finalize must be called to compute the update messages, which
// must then be applied to all Authority objects using Inform().
type UpdateBuilder struct {
a *Authority
signer Signer
state State
parent AUMHash
out []AUM
}
func (b *UpdateBuilder) mkUpdate(update AUM) error {
prevHash := make([]byte, len(b.parent))
copy(prevHash, b.parent[:])
update.PrevAUMHash = prevHash
if b.signer != nil {
sigs, err := b.signer.SignAUM(update.SigHash())
if err != nil {
return fmt.Errorf("signing failed: %v", err)
}
update.Signatures = append(update.Signatures, sigs...)
}
if err := update.StaticValidate(); err != nil {
return fmt.Errorf("generated update was invalid: %v", err)
}
state, err := b.state.applyVerifiedAUM(update)
if err != nil {
return fmt.Errorf("update cannot be applied: %v", err)
}
b.state = state
b.parent = update.Hash()
b.out = append(b.out, update)
return nil
}
// AddKey adds a new key to the authority.
func (b *UpdateBuilder) AddKey(key Key) error {
keyID, err := key.ID()
if err != nil {
return err
}
if _, err := b.state.GetKey(keyID); err == nil {
return fmt.Errorf("cannot add key tlpub:%x: already exists", key.Public)
}
if len(b.state.Keys) >= maxKeys {
return fmt.Errorf("cannot add key tlpub:%x: maximum number of keys reached", key.Public)
}
return b.mkUpdate(AUM{MessageKind: AUMAddKey, Key: &key})
}
// RemoveKey removes a key from the authority.
func (b *UpdateBuilder) RemoveKey(keyID tkatype.KeyID) error {
if _, err := b.state.GetKey(keyID); err != nil {
return fmt.Errorf("failed reading key %x: %v", keyID, err)
}
return b.mkUpdate(AUM{MessageKind: AUMRemoveKey, KeyID: keyID})
}
// SetKeyVote updates the number of votes of an existing key.
func (b *UpdateBuilder) SetKeyVote(keyID tkatype.KeyID, votes uint) error {
if _, err := b.state.GetKey(keyID); err != nil {
return fmt.Errorf("failed reading key %x: %v", keyID, err)
}
return b.mkUpdate(AUM{MessageKind: AUMUpdateKey, Votes: &votes, KeyID: keyID})
}
// SetKeyMeta updates key-value metadata stored against an existing key.
//
// TODO(tom): Provide an API to update specific values rather than the whole
// map.
func (b *UpdateBuilder) SetKeyMeta(keyID tkatype.KeyID, meta map[string]string) error {
if _, err := b.state.GetKey(keyID); err != nil {
return fmt.Errorf("failed reading key %x: %v", keyID, err)
}
return b.mkUpdate(AUM{MessageKind: AUMUpdateKey, Meta: meta, KeyID: keyID})
}
func (b *UpdateBuilder) generateCheckpoint() error {
// Compute the checkpoint state.
state := b.a.state
for i, update := range b.out {
var err error
if state, err = state.applyVerifiedAUM(update); err != nil {
return fmt.Errorf("applying update %d: %v", i, err)
}
}
// Checkpoints can't specify a parent AUM.
state.LastAUMHash = nil
return b.mkUpdate(AUM{MessageKind: AUMCheckpoint, State: &state})
}
// checkpointEvery sets how often a checkpoint AUM should be generated.
const checkpointEvery = 50
// Finalize returns the set of update message to actuate the update.
func (b *UpdateBuilder) Finalize(storage Chonk) ([]AUM, error) {
var (
needCheckpoint bool = true
cursor AUMHash = b.a.Head()
)
for i := len(b.out); i < checkpointEvery; i++ {
aum, err := storage.AUM(cursor)
if err != nil {
if err == os.ErrNotExist {
// The available chain is shorter than the interval to checkpoint at.
needCheckpoint = false
break
}
return nil, fmt.Errorf("reading AUM (%v): %v", cursor, err)
}
if aum.MessageKind == AUMCheckpoint {
needCheckpoint = false
break
}
parent, hasParent := aum.Parent()
if !hasParent {
// We've hit the genesis update, so the chain is shorter than the interval to checkpoint at.
needCheckpoint = false
break
}
cursor = parent
}
if needCheckpoint {
if err := b.generateCheckpoint(); err != nil {
return nil, fmt.Errorf("generating checkpoint: %v", err)
}
}
// Check no AUMs were applied in the meantime
if len(b.out) > 0 {
if parent, _ := b.out[0].Parent(); parent != b.a.Head() {
return nil, fmt.Errorf("updates no longer apply to head: based on %x but head is %x", parent, b.a.Head())
}
}
return b.out, nil
}
// NewUpdater returns a builder you can use to make changes to
// the tailnet key authority.
//
// The provided signer function, if non-nil, is called with each update
// to compute and apply signatures.
//
// Updates are specified by calling methods on the returned UpdatedBuilder.
// Call Finalize() when you are done to obtain the specific update messages
// which actuate the changes.
func (a *Authority) NewUpdater(signer Signer) *UpdateBuilder {
return &UpdateBuilder{
a: a,
signer: signer,
parent: a.Head(),
state: a.state,
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_tailnetlock
package tka
import (
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"fmt"
"net/url"
"strings"
)
const (
DeeplinkTailscaleURLScheme = "tailscale"
DeeplinkCommandSign = "sign-device"
)
// generateHMAC computes a SHA-256 HMAC for the concatenation of components,
// using the Authority stateID as secret.
func (a *Authority) generateHMAC(params NewDeeplinkParams) []byte {
stateID, _ := a.StateIDs()
key := make([]byte, 8)
binary.LittleEndian.PutUint64(key, stateID)
mac := hmac.New(sha256.New, key)
mac.Write([]byte(params.NodeKey))
mac.Write([]byte(params.TLPub))
mac.Write([]byte(params.DeviceName))
mac.Write([]byte(params.OSName))
mac.Write([]byte(params.LoginName))
return mac.Sum(nil)
}
type NewDeeplinkParams struct {
NodeKey string
TLPub string
DeviceName string
OSName string
LoginName string
}
// NewDeeplink creates a signed deeplink using the authority's stateID as a
// secret. This deeplink can then be validated by ValidateDeeplink.
func (a *Authority) NewDeeplink(params NewDeeplinkParams) (string, error) {
if params.NodeKey == "" || !strings.HasPrefix(params.NodeKey, "nodekey:") {
return "", fmt.Errorf("invalid node key %q", params.NodeKey)
}
if params.TLPub == "" || !strings.HasPrefix(params.TLPub, "tlpub:") {
return "", fmt.Errorf("invalid tlpub %q", params.TLPub)
}
if params.DeviceName == "" {
return "", fmt.Errorf("invalid device name %q", params.DeviceName)
}
if params.OSName == "" {
return "", fmt.Errorf("invalid os name %q", params.OSName)
}
if params.LoginName == "" {
return "", fmt.Errorf("invalid login name %q", params.LoginName)
}
u := url.URL{
Scheme: DeeplinkTailscaleURLScheme,
Host: DeeplinkCommandSign,
Path: "/v1/",
}
v := url.Values{}
v.Set("nk", params.NodeKey)
v.Set("tp", params.TLPub)
v.Set("dn", params.DeviceName)
v.Set("os", params.OSName)
v.Set("em", params.LoginName)
hmac := a.generateHMAC(params)
v.Set("hm", hex.EncodeToString(hmac))
u.RawQuery = v.Encode()
return u.String(), nil
}
type DeeplinkValidationResult struct {
IsValid bool
Error string
Version uint8
NodeKey string
TLPub string
DeviceName string
OSName string
EmailAddress string
}
// ValidateDeeplink validates a device signing deeplink using the authority's stateID.
// The input urlString follows this structure:
//
// tailscale://sign-device/v1/?nk=xxx&tp=xxx&dn=xxx&os=xxx&em=xxx&hm=xxx
//
// where:
// - "nk" is the nodekey of the node being signed
// - "tp" is the tailnet lock public key
// - "dn" is the name of the node
// - "os" is the operating system of the node
// - "em" is the email address associated with the node
// - "hm" is a SHA-256 HMAC computed over the concatenation of the above fields, encoded as a hex string
func (a *Authority) ValidateDeeplink(urlString string) DeeplinkValidationResult {
parsedUrl, err := url.Parse(urlString)
if err != nil {
return DeeplinkValidationResult{
IsValid: false,
Error: err.Error(),
}
}
if parsedUrl.Scheme != DeeplinkTailscaleURLScheme {
return DeeplinkValidationResult{
IsValid: false,
Error: fmt.Sprintf("unhandled scheme %s, expected %s", parsedUrl.Scheme, DeeplinkTailscaleURLScheme),
}
}
if parsedUrl.Host != DeeplinkCommandSign {
return DeeplinkValidationResult{
IsValid: false,
Error: fmt.Sprintf("unhandled host %s, expected %s", parsedUrl.Host, DeeplinkCommandSign),
}
}
path := parsedUrl.EscapedPath()
pathComponents := strings.Split(path, "/")
if len(pathComponents) != 3 {
return DeeplinkValidationResult{
IsValid: false,
Error: "invalid path components number found",
}
}
if pathComponents[1] != "v1" {
return DeeplinkValidationResult{
IsValid: false,
Error: fmt.Sprintf("expected v1 deeplink version, found something else: %s", pathComponents[1]),
}
}
nodeKey := parsedUrl.Query().Get("nk")
if len(nodeKey) == 0 {
return DeeplinkValidationResult{
IsValid: false,
Error: "missing nk (NodeKey) query parameter",
}
}
tlPub := parsedUrl.Query().Get("tp")
if len(tlPub) == 0 {
return DeeplinkValidationResult{
IsValid: false,
Error: "missing tp (TLPub) query parameter",
}
}
deviceName := parsedUrl.Query().Get("dn")
if len(deviceName) == 0 {
return DeeplinkValidationResult{
IsValid: false,
Error: "missing dn (DeviceName) query parameter",
}
}
osName := parsedUrl.Query().Get("os")
if len(deviceName) == 0 {
return DeeplinkValidationResult{
IsValid: false,
Error: "missing os (OSName) query parameter",
}
}
emailAddress := parsedUrl.Query().Get("em")
if len(emailAddress) == 0 {
return DeeplinkValidationResult{
IsValid: false,
Error: "missing em (EmailAddress) query parameter",
}
}
hmacString := parsedUrl.Query().Get("hm")
if len(hmacString) == 0 {
return DeeplinkValidationResult{
IsValid: false,
Error: "missing hm (HMAC) query parameter",
}
}
computedHMAC := a.generateHMAC(NewDeeplinkParams{
NodeKey: nodeKey,
TLPub: tlPub,
DeviceName: deviceName,
OSName: osName,
LoginName: emailAddress,
})
hmacHexBytes, err := hex.DecodeString(hmacString)
if err != nil {
return DeeplinkValidationResult{IsValid: false, Error: "could not hex-decode hmac"}
}
if !hmac.Equal(computedHMAC, hmacHexBytes) {
return DeeplinkValidationResult{
IsValid: false,
Error: "hmac authentication failed",
}
}
return DeeplinkValidationResult{
IsValid: true,
NodeKey: nodeKey,
TLPub: tlPub,
DeviceName: deviceName,
OSName: osName,
EmailAddress: emailAddress,
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package tka
import (
"crypto/ed25519"
"errors"
"fmt"
"maps"
"tailscale.com/types/tkatype"
)
// KeyKind describes the different varieties of a Key.
type KeyKind uint8
// Valid KeyKind values.
const (
KeyInvalid KeyKind = iota
Key25519
)
func (k KeyKind) String() string {
switch k {
case KeyInvalid:
return "invalid"
case Key25519:
return "25519"
default:
return fmt.Sprintf("Key?<%d>", int(k))
}
}
// Key describes the public components of a key known to tailnet-lock.
type Key struct {
Kind KeyKind `cbor:"1,keyasint"`
// Votes describes the weight applied to signatures using this key.
// Weighting is used to deterministically resolve branches in the AUM
// chain (i.e. forks, where two AUMs exist with the same parent).
Votes uint `cbor:"2,keyasint"`
// Public encodes the public key of the key. For 25519 keys,
// this is simply the point on the curve representing the public
// key.
Public []byte `cbor:"3,keyasint"`
// Meta describes arbitrary metadata about the key. This could be
// used to store the name of the key, for instance.
Meta map[string]string `cbor:"12,keyasint,omitempty"`
}
// Clone makes an independent copy of Key.
//
// NOTE: There is a difference between a nil slice and an empty slice for encoding purposes,
// so an implementation of Clone() must take care to preserve this.
func (k Key) Clone() Key {
out := k
if k.Public != nil {
out.Public = make([]byte, len(k.Public))
copy(out.Public, k.Public)
}
if k.Meta != nil {
out.Meta = make(map[string]string, len(k.Meta))
maps.Copy(out.Meta, k.Meta)
}
return out
}
// MustID returns the KeyID of the key, panicking if an error is
// encountered. This must only be used for tests.
func (k Key) MustID() tkatype.KeyID {
id, err := k.ID()
if err != nil {
panic(err)
}
return id
}
// ID returns the KeyID of the key.
func (k Key) ID() (tkatype.KeyID, error) {
switch k.Kind {
// Because 25519 public keys are so short, we just use the 32-byte
// public as their 'key ID'.
case Key25519:
return tkatype.KeyID(k.Public), nil
default:
return nil, fmt.Errorf("unknown key kind: %v", k.Kind)
}
}
// Ed25519 returns the ed25519 public key encoded by Key. An error is
// returned for keys which do not represent ed25519 public keys.
func (k Key) Ed25519() (ed25519.PublicKey, error) {
switch k.Kind {
case Key25519:
return ed25519.PublicKey(k.Public), nil
default:
return nil, fmt.Errorf("key is of type %v, not ed25519", k.Kind)
}
}
func (k Key) StaticValidate() error {
if k.Votes > 4096 {
return fmt.Errorf("excessive key weight: %d > 4096", k.Votes)
}
if k.Votes == 0 {
return errors.New("key votes must be non-zero")
}
// We have an arbitrary upper limit on the amount
// of metadata that can be associated with a key, so
// people don't start using it as a key-value store and
// causing pathological cases due to the number + size of
// AUMs.
var metaBytes uint
for k, v := range k.Meta {
metaBytes += uint(len(k) + len(v))
}
if metaBytes > maxMetaBytes {
return fmt.Errorf("key metadata too big (%d > %d)", metaBytes, maxMetaBytes)
}
switch k.Kind {
case Key25519:
default:
return fmt.Errorf("unrecognized key kind: %v", k.Kind)
}
return nil
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_tailnetlock
package tka
import (
"bytes"
"crypto/ed25519"
"encoding/base64"
"errors"
"fmt"
"strings"
"github.com/fxamacker/cbor/v2"
"github.com/hdevalence/ed25519consensus"
"golang.org/x/crypto/blake2s"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/tkatype"
)
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=NodeKeySignature
// SigKind describes valid NodeKeySignature types.
type SigKind uint8
const (
SigInvalid SigKind = iota
// SigDirect describes a signature over a specific node key, signed
// by a key in the tailnet key authority referenced by the specified keyID.
SigDirect
// SigRotation describes a signature over a specific node key, signed
// by the rotation key authorized by a nested NodeKeySignature structure.
//
// While it is possible to nest rotations multiple times up to the CBOR
// nesting limit, it is intended that nodes simply regenerate their outer
// SigRotation signature and sign it again with their rotation key. That
// way, SigRotation nesting should only be 2 deep in the common case.
SigRotation
// SigCredential describes a signature over a specific public key, signed
// by a key in the tailnet key authority referenced by the specified keyID.
// In effect, SigCredential delegates the ability to make a signature to
// a different public/private key pair.
//
// It is intended that a different public/private key pair be generated
// for each different SigCredential that is created. Implementors must
// take care that the private side is only known to the entity that needs
// to generate the wrapping SigRotation signature, and it is immediately
// discarded after use.
//
// SigCredential is expected to be nested in a SigRotation signature.
SigCredential
)
func (s SigKind) String() string {
switch s {
case SigInvalid:
return "invalid"
case SigDirect:
return "direct"
case SigRotation:
return "rotation"
case SigCredential:
return "credential"
default:
return fmt.Sprintf("Sig?<%d>", int(s))
}
}
// NodeKeySignature encapsulates a signature that authorizes a specific
// node key, based on verification from keys in the tailnet key authority.
type NodeKeySignature struct {
// SigKind identifies the variety of signature.
SigKind SigKind `cbor:"1,keyasint"`
// Pubkey identifies the key.NodePublic which is being authorized.
// SigCredential signatures do not use this field.
Pubkey []byte `cbor:"2,keyasint,omitempty"`
// KeyID identifies which key in the tailnet key authority should
// be used to verify this signature. Only set for SigDirect and
// SigCredential signature kinds.
KeyID []byte `cbor:"3,keyasint,omitempty"`
// Signature is the packed (R, S) ed25519 signature over all other
// fields of the structure.
Signature []byte `cbor:"4,keyasint,omitempty"`
// Nested describes a NodeKeySignature which authorizes the node-key
// used as Pubkey. Only used for SigRotation signatures.
Nested *NodeKeySignature `cbor:"5,keyasint,omitempty"`
// WrappingPubkey specifies the ed25519 public key which must be used
// to sign a Signature which embeds this one.
//
// For SigRotation signatures multiple levels deep, intermediate
// signatures may omit this value, in which case the parent WrappingPubkey
// is used.
//
// SigCredential signatures use this field to specify the public key
// they are certifying, following the usual semanticsfor WrappingPubkey.
WrappingPubkey []byte `cbor:"6,keyasint,omitempty"`
}
// String returns a human-readable representation of the NodeKeySignature,
// making it easy to see nested signatures.
func (s NodeKeySignature) String() string {
var b strings.Builder
var addToBuf func(NodeKeySignature, int)
addToBuf = func(sig NodeKeySignature, depth int) {
indent := strings.Repeat(" ", depth)
b.WriteString(indent + "SigKind: " + sig.SigKind.String() + "\n")
if len(sig.Pubkey) > 0 {
var pubKey string
var np key.NodePublic
if err := np.UnmarshalBinary(sig.Pubkey); err != nil {
pubKey = fmt.Sprintf("<error: %s>", err)
} else {
pubKey = np.ShortString()
}
b.WriteString(indent + "Pubkey: " + pubKey + "\n")
}
if len(sig.KeyID) > 0 {
keyID := key.TLPublicFromEd25519Unsafe(sig.KeyID).CLIString()
b.WriteString(indent + "KeyID: " + keyID + "\n")
}
if len(sig.WrappingPubkey) > 0 {
pubKey := key.TLPublicFromEd25519Unsafe(sig.WrappingPubkey).CLIString()
b.WriteString(indent + "WrappingPubkey: " + pubKey + "\n")
}
if sig.Nested != nil {
b.WriteString(indent + "Nested:\n")
addToBuf(*sig.Nested, depth+1)
}
}
addToBuf(s, 0)
return strings.TrimSpace(b.String())
}
// UnverifiedWrappingPublic returns the public key which must sign a
// signature which embeds this one, if any.
//
// See docs on NodeKeySignature.WrappingPubkey & SigRotation for documentation
// about wrapping public keys.
//
// SAFETY: The caller MUST verify the signature using
// Authority.NodeKeyAuthorized if treating this as authentic information.
func (s NodeKeySignature) UnverifiedWrappingPublic() (pub ed25519.PublicKey, ok bool) {
return s.wrappingPublic()
}
// wrappingPublic returns the public key which must sign a signature which
// embeds this one, if any.
func (s NodeKeySignature) wrappingPublic() (pub ed25519.PublicKey, ok bool) {
if len(s.WrappingPubkey) > 0 {
return ed25519.PublicKey(s.WrappingPubkey), true
}
switch s.SigKind {
case SigRotation:
if s.Nested == nil {
return nil, false
}
return s.Nested.wrappingPublic()
default:
return nil, false
}
}
// UnverifiedAuthorizingKeyID returns the KeyID of the key which authorizes
// this signature.
//
// SAFETY: The caller MUST verify the signature using
// Authority.NodeKeyAuthorized if treating this as authentic information.
func (s NodeKeySignature) UnverifiedAuthorizingKeyID() (tkatype.KeyID, error) {
return s.authorizingKeyID()
}
// authorizingKeyID returns the KeyID of the key trusted by tailnet-lock which authorizes
// this signature.
func (s NodeKeySignature) authorizingKeyID() (tkatype.KeyID, error) {
switch s.SigKind {
case SigDirect, SigCredential:
if len(s.KeyID) == 0 {
return tkatype.KeyID{}, errors.New("invalid signature: no keyID present")
}
return tkatype.KeyID(s.KeyID), nil
case SigRotation:
if s.Nested == nil {
return tkatype.KeyID{}, errors.New("invalid signature: rotation signature missing nested signature")
}
return s.Nested.authorizingKeyID()
default:
return tkatype.KeyID{}, fmt.Errorf("unhandled signature type: %v", s.SigKind)
}
}
// SigHash returns the cryptographic digest which a signature
// is over.
//
// This is a hash of the serialized structure, sans the signature.
// Without this exclusion, the hash used for the signature
// would be circularly dependent on the signature.
func (s NodeKeySignature) SigHash() [blake2s.Size]byte {
dupe := s
dupe.Signature = nil
return blake2s.Sum256(dupe.Serialize())
}
// Serialize returns the given NKS in a serialized format.
//
// We would implement encoding.BinaryMarshaler, except that would
// unfortunately get called by the cbor marshaller resulting in infinite
// recursion.
func (s *NodeKeySignature) Serialize() tkatype.MarshaledSignature {
out := bytes.NewBuffer(make([]byte, 0, 128)) // 64byte sig + 32byte keyID + 32byte headroom
encoder, err := cbor.CTAP2EncOptions().EncMode()
if err != nil {
// Deterministic validation of encoding options, should
// never fail.
panic(err)
}
if err := encoder.NewEncoder(out).Encode(s); err != nil {
// Writing to a bytes.Buffer should never fail.
panic(err)
}
return out.Bytes()
}
// Unserialize decodes bytes representing a marshaled NKS.
//
// We would implement encoding.BinaryUnmarshaler, except that would
// unfortunately get called by the cbor unmarshaller resulting in infinite
// recursion.
func (s *NodeKeySignature) Unserialize(data []byte) error {
dec, _ := cborDecOpts.DecMode()
return dec.Unmarshal(data, s)
}
// verifySignature checks that the NodeKeySignature is authentic & certified
// by the given verificationKey. Additionally, SigDirect and SigRotation
// signatures are checked to ensure they authorize the given nodeKey.
func (s *NodeKeySignature) verifySignature(nodeKey key.NodePublic, verificationKey Key) error {
if s.SigKind != SigCredential {
nodeBytes, err := nodeKey.MarshalBinary()
if err != nil {
return fmt.Errorf("marshalling pubkey: %v", err)
}
if !bytes.Equal(nodeBytes, s.Pubkey) {
return errors.New("signature does not authorize nodeKey")
}
}
sigHash := s.SigHash()
switch s.SigKind {
case SigRotation:
if s.Nested == nil {
return errors.New("nested signatures must nest a signature")
}
// Verify the signature using the nested rotation key.
verifyPub, ok := s.Nested.wrappingPublic()
if !ok {
return errors.New("missing rotation key")
}
if len(verifyPub) != ed25519.PublicKeySize {
return fmt.Errorf("bad rotation key length: %d", len(verifyPub))
}
if !ed25519.Verify(ed25519.PublicKey(verifyPub[:]), sigHash[:], s.Signature) {
return errors.New("invalid signature")
}
// Recurse to verify the signature on the nested structure.
var nestedPub key.NodePublic
// SigCredential signatures certify an indirection key rather than a node
// key, so there's no need to check the node key.
if s.Nested.SigKind != SigCredential {
if err := nestedPub.UnmarshalBinary(s.Nested.Pubkey); err != nil {
return fmt.Errorf("nested pubkey: %v", err)
}
}
if err := s.Nested.verifySignature(nestedPub, verificationKey); err != nil {
return fmt.Errorf("nested: %v", err)
}
return nil
case SigDirect, SigCredential:
if s.Nested != nil {
return fmt.Errorf("invalid signature: signatures of type %v cannot nest another signature", s.SigKind)
}
switch verificationKey.Kind {
case Key25519:
if len(verificationKey.Public) != ed25519.PublicKeySize {
return fmt.Errorf("ed25519 key has wrong length: %d", len(verificationKey.Public))
}
if ed25519consensus.Verify(ed25519.PublicKey(verificationKey.Public), sigHash[:], s.Signature) {
return nil
}
return errors.New("invalid signature")
default:
return fmt.Errorf("unhandled key type: %v", verificationKey.Kind)
}
default:
return fmt.Errorf("unhandled signature type: %v", s.SigKind)
}
}
// RotationDetails holds additional information about a nodeKeySignature
// of kind SigRotation.
type RotationDetails struct {
// PrevNodeKeys is a list of node keys which have been rotated out.
PrevNodeKeys []key.NodePublic
// InitialSig is the first signature in the chain which led to
// this rotating signature.
InitialSig *NodeKeySignature
}
// rotationDetails returns the RotationDetails for a SigRotation signature.
func (s *NodeKeySignature) rotationDetails() (*RotationDetails, error) {
if s.SigKind != SigRotation {
return nil, nil
}
sri := &RotationDetails{}
nested := s.Nested
for nested != nil {
if len(nested.Pubkey) > 0 {
var nestedPub key.NodePublic
if err := nestedPub.UnmarshalBinary(nested.Pubkey); err != nil {
return nil, fmt.Errorf("nested pubkey: %v", err)
}
sri.PrevNodeKeys = append(sri.PrevNodeKeys, nestedPub)
}
if nested.SigKind != SigRotation {
break
}
nested = nested.Nested
}
sri.InitialSig = nested
return sri, nil
}
// ResignNKS re-signs a node-key signature for a new node-key.
//
// This only matters on tailnet-locked tailnets, because node-key signatures are
// how other nodes know that a node-key is authentic. When the node-key is
// rotated then the existing signature becomes invalid, so this function is
// responsible for generating a new wrapping signature to certify the new node-key.
//
// The signature itself is a SigRotation signature, which embeds the old signature
// and certifies the new node-key as a replacement for the old by signing the new
// signature with RotationPubkey (which is the node's own tailnet-lock key).
func ResignNKS(priv key.TLPrivate, nodeKey key.NodePublic, oldNKS tkatype.MarshaledSignature) (tkatype.MarshaledSignature, error) {
var oldSig NodeKeySignature
if err := oldSig.Unserialize(oldNKS); err != nil {
return nil, fmt.Errorf("decoding NKS: %w", err)
}
nk, err := nodeKey.MarshalBinary()
if err != nil {
return nil, fmt.Errorf("marshalling node-key: %w", err)
}
if bytes.Equal(nk, oldSig.Pubkey) {
// The old signature is valid for the node-key we are using, so just
// use it verbatim.
return oldNKS, nil
}
nested, err := maybeTrimRotationSignatureChain(oldSig, priv)
if err != nil {
return nil, fmt.Errorf("trimming rotation signature chain: %w", err)
}
newSig := NodeKeySignature{
SigKind: SigRotation,
Pubkey: nk,
Nested: &nested,
}
if newSig.Signature, err = priv.SignNKS(newSig.SigHash()); err != nil {
return nil, fmt.Errorf("signing NKS: %w", err)
}
return newSig.Serialize(), nil
}
// maybeTrimRotationSignatureChain truncates rotation signature chain to ensure
// it contains no more than 15 node keys.
func maybeTrimRotationSignatureChain(sig NodeKeySignature, priv key.TLPrivate) (NodeKeySignature, error) {
if sig.SigKind != SigRotation {
return sig, nil
}
// Collect all the previous node keys, ordered from newest to oldest.
prevPubkeys := [][]byte{sig.Pubkey}
nested := sig.Nested
for nested != nil {
if len(nested.Pubkey) > 0 {
prevPubkeys = append(prevPubkeys, nested.Pubkey)
}
if nested.SigKind != SigRotation {
break
}
nested = nested.Nested
}
// Existing rotation signature with 15 keys is the maximum we can wrap in a
// new signature without hitting the CBOR nesting limit of 16 (see
// MaxNestedLevels in tka.go).
const maxPrevKeys = 15
if len(prevPubkeys) <= maxPrevKeys {
return sig, nil
}
// Create a new rotation signature chain, starting with the original
// direct signature.
var err error
result := nested // original direct signature
for i := maxPrevKeys - 2; i >= 0; i-- {
result = &NodeKeySignature{
SigKind: SigRotation,
Pubkey: prevPubkeys[i],
Nested: result,
}
if result.Signature, err = priv.SignNKS(result.SigHash()); err != nil {
return sig, fmt.Errorf("signing NKS: %w", err)
}
}
return *result, nil
}
// SignByCredential signs a node public key by a private key which has its
// signing authority delegated by a SigCredential signature. This is used by
// wrapped auth keys.
func SignByCredential(privKey []byte, wrapped *NodeKeySignature, nodeKey key.NodePublic) (tkatype.MarshaledSignature, error) {
if wrapped.SigKind != SigCredential {
return nil, fmt.Errorf("wrapped signature must be a credential, got %v", wrapped.SigKind)
}
nk, err := nodeKey.MarshalBinary()
if err != nil {
return nil, fmt.Errorf("marshalling node-key: %w", err)
}
sig := &NodeKeySignature{
SigKind: SigRotation,
Pubkey: nk,
Nested: wrapped,
}
sigHash := sig.SigHash()
sig.Signature = ed25519.Sign(privKey, sigHash[:])
return sig.Serialize(), nil
}
// DecodeWrappedAuthkey separates wrapping information from an authkey, if any.
// In all cases the authkey is returned, sans wrapping information if any.
//
// If the authkey is wrapped, isWrapped returns true, along with the wrapping signature
// and private key.
func DecodeWrappedAuthkey(wrappedAuthKey string, logf logger.Logf) (authKey string, isWrapped bool, sig *NodeKeySignature, priv ed25519.PrivateKey) {
authKey, suffix, found := strings.Cut(wrappedAuthKey, "--TL")
if !found {
return wrappedAuthKey, false, nil, nil
}
sigBytes, privBytes, found := strings.Cut(suffix, "-")
if !found {
// TODO: propagate these errors to `tailscale up` output?
logf("decoding wrapped auth-key: did not find delimiter")
return wrappedAuthKey, false, nil, nil
}
rawSig, err := base64.RawStdEncoding.DecodeString(sigBytes)
if err != nil {
logf("decoding wrapped auth-key: signature decode: %v", err)
return wrappedAuthKey, false, nil, nil
}
rawPriv, err := base64.RawStdEncoding.DecodeString(privBytes)
if err != nil {
logf("decoding wrapped auth-key: priv decode: %v", err)
return wrappedAuthKey, false, nil, nil
}
sig = new(NodeKeySignature)
if err := sig.Unserialize(rawSig); err != nil {
logf("decoding wrapped auth-key: signature: %v", err)
return wrappedAuthKey, false, nil, nil
}
priv = ed25519.PrivateKey(rawPriv)
return authKey, true, sig, priv
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_tailnetlock
package tka
import (
"bytes"
"crypto/subtle"
"errors"
"fmt"
"golang.org/x/crypto/argon2"
"tailscale.com/types/tkatype"
"tailscale.com/util/testenv"
)
// ErrNoSuchKey is returned if the key referenced by a KeyID does not exist.
var ErrNoSuchKey = errors.New("key not found")
// State describes Tailnet Key Authority state at an instant in time.
//
// State is mutated by applying Authority Update Messages (AUMs), resulting
// in a new State.
type State struct {
// LastAUMHash is the blake2s digest of the last-applied AUM.
// Because AUMs are strictly ordered and form a hash chain, we
// check the previous AUM hash in an update we are applying
// is the same as the LastAUMHash.
LastAUMHash *AUMHash `cbor:"1,keyasint"`
// DisablementValues are KDF-derived values used to verify that a caller
// possesses a valid DisablementSecret. These values are used during the
// Tailnet Lock deactivation process.
//
// These are safe to share publicly or store in the clear. They cannot be
// used to derive the original DisablementSecret.
DisablementValues [][]byte `cbor:"2,keyasint"`
// Keys are the public keys of either:
//
// 1. The signing nodes currently trusted by the TKA.
// 2. Ephemeral keys that were used to generate pre-signed auth keys.
Keys []Key `cbor:"3,keyasint"`
// StateID's are nonce's, generated on enablement and fixed for
// the lifetime of the Tailnet Key Authority. We generate 16-bytes
// worth of keyspace here just in case we come up with a cool future
// use for this.
StateID1 uint64 `cbor:"4,keyasint,omitempty"`
StateID2 uint64 `cbor:"5,keyasint,omitempty"`
}
// GetKey returns the trusted key with the specified KeyID.
func (s State) GetKey(key tkatype.KeyID) (Key, error) {
for _, k := range s.Keys {
keyID, err := k.ID()
if err != nil {
return Key{}, err
}
if bytes.Equal(keyID, key) {
return k, nil
}
}
return Key{}, ErrNoSuchKey
}
// Clone makes an independent copy of State.
//
// NOTE: There is a difference between a nil slice and an empty
// slice for encoding purposes, so an implementation of Clone()
// must take care to preserve this.
func (s State) Clone() State {
out := State{
StateID1: s.StateID1,
StateID2: s.StateID2,
}
if s.LastAUMHash != nil {
dupe := *s.LastAUMHash
out.LastAUMHash = &dupe
}
if s.DisablementValues != nil {
out.DisablementValues = make([][]byte, len(s.DisablementValues))
for i := range s.DisablementValues {
out.DisablementValues[i] = make([]byte, len(s.DisablementValues[i]))
copy(out.DisablementValues[i], s.DisablementValues[i])
}
}
if s.Keys != nil {
out.Keys = make([]Key, len(s.Keys))
for i := range s.Keys {
out.Keys[i] = s.Keys[i].Clone()
}
}
return out
}
// cloneForUpdate is like Clone, except LastAUMHash is set based
// on the hash of the given update.
func (s State) cloneForUpdate(update *AUM) State {
out := s.Clone()
aumHash := update.Hash()
out.LastAUMHash = &aumHash
return out
}
const disablementLength = 32
var disablementSalt = []byte("tailscale network-lock disablement salt")
// DisablementKDF computes a public value which can be stored in a
// key authority, but cannot be reversed to find the input secret.
//
// When the output of this function is stored in tka state (i.e. in
// tka.State.DisablementValues) a call to Authority.ValidDisablement()
// with the input of this function as the argument will return true.
func DisablementKDF(secret []byte) []byte {
// time = 4 (3 recommended, booped to 4 to compensate for less memory)
// memory = 16 (32 recommended)
// threads = 4
// keyLen = 32 (256 bits)
return argon2.Key(secret, disablementSalt, 4, 16*1024, 4, disablementLength)
}
// checkDisablement returns true for a valid disablement secret.
func (s State) checkDisablement(secret []byte) bool {
derived := DisablementKDF(secret)
for _, candidate := range s.DisablementValues {
if subtle.ConstantTimeCompare(derived, candidate) == 1 {
return true
}
}
return false
}
// parentMatches returns true if an AUM can chain to (be applied)
// to the current state.
//
// Specifically, the rules are:
// - The last AUM hash must match (transitively, this implies that this
// update follows the last update message applied to the state machine)
// - Or, the state machine knows no parent (it's brand new).
func (s State) parentMatches(update AUM) bool {
if s.LastAUMHash == nil {
return true
}
return bytes.Equal(s.LastAUMHash[:], update.PrevAUMHash)
}
// applyVerifiedAUM computes a new state based on the update provided.
//
// The provided update MUST be verified: That is, the AUM must be well-formed
// (as defined by StaticValidate()), and signatures over the AUM must have
// been verified.
func (s State) applyVerifiedAUM(update AUM) (State, error) {
// Validate that the update message has the right parent.
if !s.parentMatches(update) {
return State{}, errors.New("parent AUMHash mismatch")
}
switch update.MessageKind {
case AUMNoOp:
out := s.cloneForUpdate(&update)
return out, nil
case AUMCheckpoint:
if update.State == nil {
return State{}, errors.New("missing checkpoint state")
}
id1Match, id2Match := update.State.StateID1 == s.StateID1, update.State.StateID2 == s.StateID2
if !id1Match || !id2Match {
return State{}, errors.New("checkpointed state has an incorrect stateID")
}
return update.State.cloneForUpdate(&update), nil
case AUMAddKey:
if update.Key == nil {
return State{}, errors.New("no key to add provided")
}
keyID, err := update.Key.ID()
if err != nil {
return State{}, err
}
if _, err := s.GetKey(keyID); err == nil {
return State{}, errors.New("key already exists")
}
out := s.cloneForUpdate(&update)
out.Keys = append(out.Keys, *update.Key)
return out, nil
case AUMUpdateKey:
k, err := s.GetKey(update.KeyID)
if err != nil {
return State{}, err
}
if update.Votes != nil {
k.Votes = *update.Votes
}
if update.Meta != nil {
k.Meta = update.Meta
}
if err := k.StaticValidate(); err != nil {
return State{}, fmt.Errorf("updated key fails validation: %v", err)
}
out := s.cloneForUpdate(&update)
for i := range out.Keys {
keyID, err := out.Keys[i].ID()
if err != nil {
return State{}, err
}
if bytes.Equal(keyID, update.KeyID) {
out.Keys[i] = k
}
}
return out, nil
case AUMRemoveKey:
idx := -1
for i := range s.Keys {
keyID, err := s.Keys[i].ID()
if err != nil {
return State{}, err
}
if bytes.Equal(update.KeyID, keyID) {
idx = i
break
}
}
if idx < 0 {
return State{}, ErrNoSuchKey
}
out := s.cloneForUpdate(&update)
out.Keys = append(out.Keys[:idx], out.Keys[idx+1:]...)
return out, nil
default:
// An AUM with an unknown message kind was received! That means
// that a future version of tailscaled added some feature we don't
// understand.
//
// The future-compatibility contract for AUM message types is that
// they must only add new features, not change the semantics of existing
// mechanisms or features. As such, old clients can safely ignore them.
out := s.cloneForUpdate(&update)
return out, nil
}
}
// staticValidateCheckpoint validates that the state is well-formed for
// inclusion in a checkpoint AUM.
func (s *State) staticValidateCheckpoint() error {
if s.LastAUMHash != nil {
return errors.New("cannot specify a parent AUM")
}
if len(s.DisablementValues) == 0 {
return errors.New("at least one disablement secret required")
}
if numDS := len(s.DisablementValues); numDS > maxDisablementValues {
return fmt.Errorf("too many disablement values (%d, max %d)", numDS, maxDisablementValues)
}
for i, ds := range s.DisablementValues {
if len(ds) != disablementLength {
return fmt.Errorf("disablement[%d]: invalid length (got %d, want %d)", i, len(ds), disablementLength)
}
for j, ds2 := range s.DisablementValues {
if i == j {
continue
}
if bytes.Equal(ds, ds2) {
return fmt.Errorf("disablement[%d]: duplicates disablement[%d]", i, j)
}
}
}
if len(s.Keys) == 0 {
return errors.New("at least one key is required")
}
if numKeys := len(s.Keys); numKeys > maxKeys {
return fmt.Errorf("too many keys (%d, max %d)", numKeys, maxKeys)
}
for i, k := range s.Keys {
if err := k.StaticValidate(); err != nil {
return fmt.Errorf("key[%d]: %v", i, err)
}
}
// NOTE: The max number of keys is constrained (512), so
// O(n^2) is fine.
for i, k := range s.Keys {
for j, k2 := range s.Keys {
if i == j {
continue
}
id1, err := k.ID()
if err != nil {
return fmt.Errorf("key[%d]: %w", i, err)
}
id2, err := k2.ID()
if err != nil {
return fmt.Errorf("key[%d]: %w", j, err)
}
if bytes.Equal(id1, id2) {
return fmt.Errorf("key[%d]: duplicates key[%d]", i, j)
}
}
}
return nil
}
// CreateStateForTest creates a [State] that marks the given keys as trusted
// with an arbitrary disablement value.
//
// This is only for use in tests, and will panic if called outside a test.
func CreateStateForTest(keys ...Key) State {
testenv.AssertInTest()
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
return State{
Keys: keys,
DisablementValues: [][]byte{DisablementKDF(disablementSecret)},
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_tailnetlock
package tka
import (
"errors"
"fmt"
"os"
"tailscale.com/util/testenv"
)
// ErrNoIntersection is returned when a shared AUM could
// not be determined when evaluating a remote sync offer.
var ErrNoIntersection = errors.New("no intersection")
// SyncOffer conveys information about the current head & ancestor AUMs,
// for the purpose of synchronization with some remote end.
//
// Ancestors should contain a subset of the ancestors of the chain.
// The last entry in that slice is the oldest-known AUM in the chain.
type SyncOffer struct {
Head AUMHash
Ancestors []AUMHash
}
// ToSyncOffer creates a SyncOffer from the fields received in
// a [tailcfg.TKASyncOfferRequest].
func ToSyncOffer(head string, ancestors []string) (SyncOffer, error) {
var out SyncOffer
if err := out.Head.UnmarshalText([]byte(head)); err != nil {
return SyncOffer{}, fmt.Errorf("head.UnmarshalText: %v", err)
}
out.Ancestors = make([]AUMHash, len(ancestors))
for i, a := range ancestors {
if err := out.Ancestors[i].UnmarshalText([]byte(a)); err != nil {
return SyncOffer{}, fmt.Errorf("ancestor[%d].UnmarshalText: %v", i, err)
}
}
return out, nil
}
// FromSyncOffer marshals the fields of a SyncOffer so they can be
// sent in a [tailcfg.TKASyncOfferRequest].
func FromSyncOffer(offer SyncOffer) (head string, ancestors []string, err error) {
headBytes, err := offer.Head.MarshalText()
if err != nil {
return "", nil, fmt.Errorf("head.MarshalText: %v", err)
}
ancestors = make([]string, len(offer.Ancestors))
for i, ancestor := range offer.Ancestors {
hash, err := ancestor.MarshalText()
if err != nil {
return "", nil, fmt.Errorf("ancestor[%d].MarshalText: %v", i, err)
}
ancestors[i] = string(hash)
}
return string(headBytes), ancestors, nil
}
// SyncOffer returns an abbreviated description of the current AUM
// chain, which can be used to synchronize with another (untrusted)
// Authority instance.
//
// The returned SyncOffer structure should be transmitted to the remote
// Authority, which should call MissingAUMs() using it to determine
// AUMs which need to be transmitted. This list of AUMs from the remote
// can then be applied locally with Inform().
//
// This SyncOffer + AUM exchange should be performed by both ends,
// because it's possible that either end has AUMs that the other needs
// to find out about.
func (a *Authority) SyncOffer(storage Chonk) (SyncOffer, error) {
oldest := a.oldestAncestor.Hash()
out := SyncOffer{
Head: a.Head(),
Ancestors: make([]AUMHash, 0, 6), // 6 chosen arbitrarily.
}
// We send all our checkpoints to help the remote find a
// more-recent 'head intersection'.
curs := a.Head()
for range maxSyncHeadIntersectionIter {
parent, err := storage.AUM(curs)
if err != nil {
if err != os.ErrNotExist {
return SyncOffer{}, err
}
break
}
// We add the oldest later on, so don't duplicate.
if parent.Hash() == oldest {
break
}
if parent.MessageKind == AUMCheckpoint {
out.Ancestors = append(out.Ancestors, curs)
}
copy(curs[:], parent.PrevAUMHash)
}
out.Ancestors = append(out.Ancestors, oldest)
return out, nil
}
// intersection describes how to synchronize AUMs with a remote
// authority.
type intersection struct {
// if true, no exchange of AUMs is needed.
upToDate bool
// headIntersection is the latest common AUM on the remote. In other
// words, we need to send all AUMs since this one.
headIntersection *AUMHash
// tailIntersection is the oldest common AUM on the remote. In other
// words, we diverge with the remote after this AUM, so we both need
// to transmit our AUM chain starting here.
tailIntersection *AUMHash
}
// computeSyncIntersection determines the common AUMs between a local and
// remote SyncOffer. This intersection can be used to synchronize both
// sides.
func computeSyncIntersection(storage Chonk, localOffer, remoteOffer SyncOffer) (*intersection, error) {
// Simple case: up to date.
if remoteOffer.Head == localOffer.Head {
return &intersection{upToDate: true, headIntersection: &localOffer.Head}, nil
}
// Case: 'head intersection'
// If we have the remote's head, it's more likely than not that
// we have updates that build on that head. To confirm this,
// we iterate backwards through our chain to see if the given
// head is an ancestor of our current chain.
//
// In other words:
// <Us> A -> B -> C
// <Them> A -> B
// ∴ their head intersects with our chain, we need to send C
var hasRemoteHead bool
_, err := storage.AUM(remoteOffer.Head)
if err != nil {
if err != os.ErrNotExist {
return nil, err
}
} else {
hasRemoteHead = true
}
if hasRemoteHead {
curs := localOffer.Head
for range maxSyncHeadIntersectionIter {
parent, err := storage.AUM(curs)
if err != nil {
if err != os.ErrNotExist {
return nil, err
}
break
}
if parent.Hash() == remoteOffer.Head {
h := parent.Hash()
return &intersection{headIntersection: &h}, nil
}
copy(curs[:], parent.PrevAUMHash)
}
}
// Case: 'tail intersection'
// So we don't have a clue what the remote's head is, but
// if one of the ancestors they gave us is part of our chain,
// then there's an intersection, which is a starting point for
// the remote to send us AUMs from.
//
// We iterate the list of ancestors in order because the remote
// ordered them such that the newer ones are earlier, so with
// a bit of luck we can use an earlier one and hence do less work /
// transmit fewer AUMs.
for _, a := range remoteOffer.Ancestors {
state, err := computeStateAt(storage, maxSyncIter, a)
if err != nil {
if err != os.ErrNotExist {
return nil, fmt.Errorf("computeStateAt: %v", err)
}
continue
}
end, _, err := fastForward(storage, maxSyncIter, state, func(curs AUM, _ State) bool {
return curs.Hash() == localOffer.Head
})
if err != nil {
return nil, err
}
// fastForward can terminate before the done condition if there are
// no more children left, so we check again before considering this
// an intersection.
if end.Hash() == localOffer.Head {
return &intersection{tailIntersection: &a}, nil
}
}
return nil, ErrNoIntersection
}
// MissingAUMs returns AUMs a remote may be missing based on the
// remotes' SyncOffer.
func (a *Authority) MissingAUMs(storage Chonk, remoteOffer SyncOffer) ([]AUM, error) {
localOffer, err := a.SyncOffer(storage)
if err != nil {
return nil, fmt.Errorf("local syncOffer: %v", err)
}
intersection, err := computeSyncIntersection(storage, localOffer, remoteOffer)
if err != nil {
return nil, fmt.Errorf("intersection: %v", err)
}
if intersection.upToDate {
return nil, nil
}
out := make([]AUM, 0, 12) // 12 chosen arbitrarily.
if intersection.headIntersection != nil {
state, err := computeStateAt(storage, maxSyncIter, *intersection.headIntersection)
if err != nil {
return nil, err
}
_, _, err = fastForward(storage, maxSyncIter, state, func(curs AUM, _ State) bool {
if curs.Hash() != *intersection.headIntersection {
out = append(out, curs)
}
return false
})
return out, err
}
if intersection.tailIntersection != nil {
state, err := computeStateAt(storage, maxSyncIter, *intersection.tailIntersection)
if err != nil {
return nil, err
}
_, _, err = fastForward(storage, maxSyncIter, state, func(curs AUM, _ State) bool {
if curs.Hash() != *intersection.tailIntersection {
out = append(out, curs)
}
return false
})
return out, err
}
panic("unreachable")
}
// SeedNode is an authority-chonk pair that can be seeded by [SeedAUMs].
type SeedNode struct {
authority *Authority
storage Chonk
}
// CreateSeedNode creates a node for use with [SeedAUMs].
func CreateSeedNode(t testenv.TB, authority *Authority, storage Chonk) SeedNode {
t.Helper()
return SeedNode{authority, storage}
}
type SeedAUMConfig struct {
Count int
Signer Signer
Nodes []SeedNode
}
// SeedAUMs generates many AUMs by repeatedly adding and removing keys
// from the TKA.
//
// The AUMs are written to all the supplied nodes, so if you pass more
// than one, you can build up a long sync history.
//
// This is only for use in testing.
func SeedAUMs(t testenv.TB, config SeedAUMConfig) {
t.Helper()
if len(config.Nodes) == 0 {
panic("called SeedAUMs without any nodes")
}
primaryNode := config.Nodes[0]
// The key that we'll repeatedly add/remove in the TKA.
key := Key{Kind: Key25519, Public: []byte{1, 1, 1}, Votes: 1}
for i := 0; i < config.Count/2; i++ {
for _, action := range []string{"add", "remove"} {
updater := primaryNode.authority.NewUpdater(config.Signer)
if action == "add" {
if err := updater.AddKey(key); err != nil {
t.Fatalf("error from updater.AddKey: %v")
}
} else {
if err := updater.RemoveKey(key.MustID()); err != nil {
t.Fatalf("error from updater.RemoveKey: %v")
}
}
aum, err := updater.Finalize(primaryNode.storage)
if err != nil {
t.Fatalf("error from authority.Finalize: %v", err)
}
for _, n := range config.Nodes {
if err := n.authority.Inform(n.storage, aum); err != nil {
t.Fatalf("error from authority.Inform: %v", err)
}
}
}
}
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_tailnetlock
package tka
import (
"bytes"
"errors"
"fmt"
"log"
"maps"
"os"
"path/filepath"
"slices"
"sync"
"time"
"github.com/fxamacker/cbor/v2"
"tailscale.com/atomicfile"
"tailscale.com/tstime"
"tailscale.com/util/set"
"tailscale.com/util/testenv"
)
// Chonk implementations provide durable storage for AUMs and other
// TKA state.
//
// All methods must be thread-safe.
//
// The name 'tailchonk' was coined by @catzkorn.
type Chonk interface {
// AUM returns the AUM with the specified digest.
//
// If the AUM does not exist, then os.ErrNotExist is returned.
AUM(hash AUMHash) (AUM, error)
// ChildAUMs returns all AUMs with a specified previous
// AUM hash.
ChildAUMs(prevAUMHash AUMHash) ([]AUM, error)
// CommitVerifiedAUMs durably stores the provided AUMs.
// Callers MUST ONLY provide AUMs which are verified (specifically,
// a call to aumVerify() must return a nil error).
// as the implementation assumes that only verified AUMs are stored.
CommitVerifiedAUMs(updates []AUM) error
// Heads returns AUMs for which there are no children. In other
// words, the latest AUM in all possible chains (the 'leaves').
Heads() ([]AUM, error)
// SetLastActiveAncestor is called to record the oldest-known AUM
// that contributed to the current state. This value is used as
// a hint on next startup to determine which chain to pick when computing
// the current state, if there are multiple distinct chains.
SetLastActiveAncestor(hash AUMHash) error
// LastActiveAncestor returns the oldest-known AUM that was (in a
// previous run) an ancestor of the current state. This is used
// as a hint to pick the correct chain in the event that the Chonk stores
// multiple distinct chains.
LastActiveAncestor() (*AUMHash, error)
}
// CompactableChonk implementation are extensions of Chonk, which are
// able to be operated by compaction logic to deleted old AUMs.
type CompactableChonk interface {
Chonk
// AllAUMs returns all AUMs stored in the chonk.
AllAUMs() ([]AUMHash, error)
// CommitTime returns the time at which the AUM was committed.
//
// If the AUM does not exist, then os.ErrNotExist is returned.
CommitTime(hash AUMHash) (time.Time, error)
// PurgeAUMs permanently and irrevocably deletes the specified
// AUMs from storage.
PurgeAUMs(hashes []AUMHash) error
// RemoveAll permanently and completely clears the TKA state. This should
// be called when the user disables Tailnet Lock.
RemoveAll() error
}
// Mem implements in-memory storage of TKA state, suitable for
// tests or cases where filesystem storage is unavailable.
//
// Mem implements the Chonk interface.
//
// Mem is thread-safe.
type Mem struct {
mu sync.RWMutex
aums map[AUMHash]AUM
commitTimes map[AUMHash]time.Time
clock tstime.Clock
// parentIndex is a map of AUMs to the AUMs for which they are
// the parent.
//
// For example, if parent index is {1 -> {2, 3, 4}}, that means
// that AUMs 2, 3, 4 all have aum.PrevAUMHash = 1.
parentIndex map[AUMHash][]AUMHash
lastActiveAncestor *AUMHash
}
// ChonkMem returns an implementation of Chonk which stores TKA state
// in-memory.
func ChonkMem() *Mem {
return &Mem{
clock: tstime.DefaultClock{},
}
}
// SetClock sets the clock used by [Mem]. This is only for use in tests,
// and will panic if called from non-test code.
func (c *Mem) SetClock(clock tstime.Clock) {
if !testenv.InTest() {
panic("used SetClock in non-test code")
}
c.clock = clock
}
func (c *Mem) SetLastActiveAncestor(hash AUMHash) error {
c.mu.Lock()
defer c.mu.Unlock()
c.lastActiveAncestor = &hash
return nil
}
func (c *Mem) LastActiveAncestor() (*AUMHash, error) {
c.mu.RLock()
defer c.mu.RUnlock()
return c.lastActiveAncestor, nil
}
// Heads returns AUMs for which there are no children. In other
// words, the latest AUM in all chains (the 'leaf').
func (c *Mem) Heads() ([]AUM, error) {
c.mu.RLock()
defer c.mu.RUnlock()
out := make([]AUM, 0, 6)
// An AUM is a 'head' if there are no nodes for which it is the parent.
for _, a := range c.aums {
if len(c.parentIndex[a.Hash()]) == 0 {
out = append(out, a)
}
}
return out, nil
}
// AUM returns the AUM with the specified digest.
func (c *Mem) AUM(hash AUMHash) (AUM, error) {
c.mu.RLock()
defer c.mu.RUnlock()
aum, ok := c.aums[hash]
if !ok {
return AUM{}, os.ErrNotExist
}
return aum, nil
}
// ChildAUMs returns all AUMs with a specified previous
// AUM hash.
func (c *Mem) ChildAUMs(prevAUMHash AUMHash) ([]AUM, error) {
c.mu.RLock()
defer c.mu.RUnlock()
out := make([]AUM, 0, 6)
for _, entry := range c.parentIndex[prevAUMHash] {
out = append(out, c.aums[entry])
}
return out, nil
}
// CommitVerifiedAUMs durably stores the provided AUMs.
// Callers MUST ONLY provide well-formed and verified AUMs,
// as the rest of the TKA implementation assumes that only
// verified AUMs are stored.
func (c *Mem) CommitVerifiedAUMs(updates []AUM) error {
c.mu.Lock()
defer c.mu.Unlock()
if c.aums == nil {
c.parentIndex = make(map[AUMHash][]AUMHash, 64)
c.aums = make(map[AUMHash]AUM, 64)
c.commitTimes = make(map[AUMHash]time.Time, 64)
}
updateLoop:
for _, aum := range updates {
aumHash := aum.Hash()
c.aums[aumHash] = aum
c.commitTimes[aumHash] = c.now()
parent, ok := aum.Parent()
if ok {
for _, exists := range c.parentIndex[parent] {
if exists == aumHash {
continue updateLoop
}
}
c.parentIndex[parent] = append(c.parentIndex[parent], aumHash)
}
}
return nil
}
// now returns the current time, optionally using the overridden
// clock if set.
func (c *Mem) now() time.Time {
if c.clock == nil {
return time.Now()
} else {
return c.clock.Now()
}
}
// RemoveAll permanently and completely clears the TKA state.
func (c *Mem) RemoveAll() error {
c.mu.Lock()
defer c.mu.Unlock()
c.aums = nil
c.commitTimes = nil
c.parentIndex = nil
c.lastActiveAncestor = nil
return nil
}
// AllAUMs returns all AUMs stored in the chonk.
func (c *Mem) AllAUMs() ([]AUMHash, error) {
c.mu.RLock()
defer c.mu.RUnlock()
return slices.Collect(maps.Keys(c.aums)), nil
}
// CommitTime returns the time at which the AUM was committed.
//
// If the AUM does not exist, then os.ErrNotExist is returned.
func (c *Mem) CommitTime(h AUMHash) (time.Time, error) {
c.mu.RLock()
defer c.mu.RUnlock()
t, ok := c.commitTimes[h]
if ok {
return t, nil
} else {
return time.Time{}, os.ErrNotExist
}
}
// PurgeAUMs marks the specified AUMs for deletion from storage.
func (c *Mem) PurgeAUMs(hashes []AUMHash) error {
c.mu.Lock()
defer c.mu.Unlock()
for _, h := range hashes {
// Remove the deleted AUM from the list of its parents' children.
//
// However, we leave the list of this AUM's children in parentIndex,
// so we can find them later in ChildAUMs().
if aum, ok := c.aums[h]; ok {
parent, hasParent := aum.Parent()
if hasParent {
c.parentIndex[parent] = slices.DeleteFunc(
c.parentIndex[parent],
func(other AUMHash) bool { return bytes.Equal(h[:], other[:]) },
)
if len(c.parentIndex[parent]) == 0 {
delete(c.parentIndex, parent)
}
}
}
// Delete this AUM from the list of AUMs and commit times.
delete(c.aums, h)
delete(c.commitTimes, h)
}
return nil
}
// FS implements filesystem storage of TKA state.
//
// FS caches graph metadata derived from its storage directory. Mutations made
// through this instance invalidate the cache. Graph changes made through
// another FS or directly to the directory are not reflected in the indexes
// until they are invalidated or this instance is reopened.
//
// FS implements the Chonk interface.
type FS struct {
base string
mu sync.RWMutex
// aumIndex and parentIndex are initialized together by buildIndexLocked.
// They avoid repeatedly scanning every AUM file while walking the authority
// graph. AUM contents are still decoded from disk on demand. A nil aumIndex
// means the indexes need to be rebuilt.
aumIndex set.Set[AUMHash]
parentIndex map[AUMHash][]AUMHash
}
// ChonkDir returns an implementation of Chonk which uses the given directory
// to store TKA state.
//
// The returned FS expects exclusive write access to dir for its lifetime.
// Graph changes made through another FS or directly to dir may not be reflected
// in queries until the returned FS is reopened.
func ChonkDir(dir string) (*FS, error) {
if err := os.MkdirAll(dir, 0755); err != nil && !os.IsExist(err) {
return nil, fmt.Errorf("creating chonk root dir: %v", err)
}
stat, err := os.Stat(dir)
if err != nil {
return nil, err
}
if !stat.IsDir() {
return nil, fmt.Errorf("chonk directory %q is a file", dir)
}
// TODO(tom): *FS marks AUMs as deleted but does not actually
// delete them, to avoid data loss in the event of a bug.
// Implement deletion after we are fairly sure in the implementation.
return &FS{base: dir}, nil
}
// fsHashInfo describes how information about an AUMHash is represented
// on disk.
//
// The CBOR-serialization of this struct is stored to base/__/base32(hash)
// where __ are the first two characters of base32(hash).
//
// CBOR was chosen because we are already using it and it serializes
// much smaller than JSON for AUMs. The 'keyasint' thing isn't essential
// but again it saves a bunch of bytes.
//
// We have removed the following fields from fsHashInfo, but they may be
// present in data stored in existing deployments. Do not reuse these values,
// to avoid getting unexpected values from legacy data:
// - cbor:1, Children
type fsHashInfo struct {
AUM *AUM `cbor:"2,keyasint"`
CreatedUnix int64 `cbor:"3,keyasint,omitempty"`
// PurgedUnix is set when the AUM is deleted. The value is
// the unix epoch at the time it was deleted.
//
// While a non-zero PurgedUnix symbolizes the AUM is deleted,
// the fsHashInfo entry can continue to exist to track children
// of this AUMHash.
PurgedUnix int64 `cbor:"4,keyasint,omitempty"`
}
// aumDir returns the directory an AUM is stored in, and its filename
// within the directory.
func (c *FS) aumDir(h AUMHash) (dir, base string) {
s := h.String()
return filepath.Join(c.base, s[:2]), s
}
// AUM returns the AUM with the specified digest.
//
// If the AUM does not exist, then os.ErrNotExist is returned.
func (c *FS) AUM(hash AUMHash) (AUM, error) {
c.mu.RLock()
defer c.mu.RUnlock()
return c.aumLocked(hash)
}
// aumLocked reads an AUM from disk. The caller must hold c.mu for reading or
// writing.
func (c *FS) aumLocked(hash AUMHash) (AUM, error) {
info, err := c.get(hash)
if err != nil {
if os.IsNotExist(err) {
return AUM{}, os.ErrNotExist
}
return AUM{}, err
}
if info.AUM == nil || info.PurgedUnix > 0 {
return AUM{}, os.ErrNotExist
}
return *info.AUM, nil
}
// CommitTime returns the time at which the AUM was committed.
//
// If the AUM does not exist, then os.ErrNotExist is returned.
func (c *FS) CommitTime(h AUMHash) (time.Time, error) {
c.mu.RLock()
defer c.mu.RUnlock()
info, err := c.get(h)
if err != nil {
if os.IsNotExist(err) {
return time.Time{}, os.ErrNotExist
}
return time.Time{}, err
}
if info.PurgedUnix > 0 {
return time.Time{}, os.ErrNotExist
}
if info.CreatedUnix > 0 {
return time.Unix(info.CreatedUnix, 0), nil
}
// If we got this far, the AUM exists but CreatedUnix is not
// set, presumably because this AUM was committed using a version
// of tailscaled that pre-dates the introduction of CreatedUnix.
// As such, we use the file modification time as a suitable analog.
dir, base := c.aumDir(h)
s, err := os.Stat(filepath.Join(dir, base))
if err != nil {
return time.Time{}, nil
}
return s.ModTime(), nil
}
// ChildAUMs returns any known AUMs with a specific parent hash.
func (c *FS) ChildAUMs(prevAUMHash AUMHash) ([]AUM, error) {
c.mu.RLock()
if c.aumIndex != nil {
defer c.mu.RUnlock()
return c.childAUMsFromIndexLocked(prevAUMHash)
}
c.mu.RUnlock()
c.mu.Lock()
defer c.mu.Unlock()
if err := c.buildIndexLocked(); err != nil {
return nil, err
}
return c.childAUMsFromIndexLocked(prevAUMHash)
}
// childAUMsFromIndexLocked returns children from the in-memory index. The
// caller must hold c.mu for reading or writing.
func (c *FS) childAUMsFromIndexLocked(prevAUMHash AUMHash) ([]AUM, error) {
children := c.parentIndex[prevAUMHash]
out := make([]AUM, 0, len(children))
for i, h := range children {
aum, err := c.aumLocked(h)
if err != nil {
return nil, fmt.Errorf("reading child %d of %x: %w", i, prevAUMHash, err)
}
out = append(out, aum)
}
return out, nil
}
func (c *FS) get(h AUMHash) (*fsHashInfo, error) {
dir, base := c.aumDir(h)
f, err := os.Open(filepath.Join(dir, base))
if err != nil {
return nil, err
}
defer f.Close()
m, err := cborDecOpts.DecMode()
if err != nil {
return nil, err
}
var out fsHashInfo
if err := m.NewDecoder(f).Decode(&out); err != nil {
return nil, err
}
if out.AUM != nil && out.AUM.Hash() != h {
return nil, fmt.Errorf("%s: AUM does not match file name hash %s", f.Name(), out.AUM.Hash())
}
return &out, nil
}
// Heads returns AUMs for which there are no children. In other
// words, the latest AUM in all possible chains (the 'leaves').
func (c *FS) Heads() ([]AUM, error) {
c.mu.RLock()
if c.aumIndex != nil {
defer c.mu.RUnlock()
return c.headsFromIndexLocked()
}
c.mu.RUnlock()
c.mu.Lock()
defer c.mu.Unlock()
if err := c.buildIndexLocked(); err != nil {
return nil, err
}
return c.headsFromIndexLocked()
}
// headsFromIndexLocked returns heads from the in-memory index. The caller
// must hold c.mu for reading or writing.
func (c *FS) headsFromIndexLocked() ([]AUM, error) {
out := make([]AUM, 0, 6) // 6 is arbitrary.
for h := range c.aumIndex {
if len(c.parentIndex[h]) != 0 {
continue
}
aum, err := c.aumLocked(h)
if err != nil {
return nil, fmt.Errorf("reading head %x: %w", h, err)
}
out = append(out, aum)
}
return out, nil
}
// RemoveAll permanently and completely clears the TKA state.
func (c *FS) RemoveAll() error {
c.mu.Lock()
defer c.mu.Unlock()
c.invalidateIndexLocked()
return os.RemoveAll(c.base)
}
// AllAUMs returns all AUMs stored in the chonk.
func (c *FS) AllAUMs() ([]AUMHash, error) {
c.mu.RLock()
if c.aumIndex != nil {
out := slices.Collect(maps.Keys(c.aumIndex))
c.mu.RUnlock()
return out, nil
}
c.mu.RUnlock()
c.mu.Lock()
defer c.mu.Unlock()
if err := c.buildIndexLocked(); err != nil {
return nil, err
}
return slices.Collect(maps.Keys(c.aumIndex)), nil
}
// buildIndexLocked builds indexes of the active AUMs in a single filesystem
// scan. If the index already exists, this is a no-op. The caller must hold
// c.mu for writing.
func (c *FS) buildIndexLocked() error {
if c.aumIndex != nil {
return nil
}
aumIndex := make(set.Set[AUMHash])
parentIndex := make(map[AUMHash][]AUMHash)
if err := c.scanHashes(func(info *fsHashInfo) {
if info.AUM == nil {
return
}
h := info.AUM.Hash()
aumIndex.Add(h)
if parent, ok := info.AUM.Parent(); ok {
parentIndex[parent] = append(parentIndex[parent], h)
}
}); err != nil {
return err
}
c.aumIndex = aumIndex
c.parentIndex = parentIndex
return nil
}
// invalidateIndexLocked invalidates the in-memory indexes after storage is
// changed. The caller must hold c.mu for writing.
func (c *FS) invalidateIndexLocked() {
c.aumIndex = nil
c.parentIndex = nil
}
func (c *FS) scanHashes(eachHashInfo func(*fsHashInfo)) error {
prefixDirs, err := os.ReadDir(c.base)
if err != nil {
return fmt.Errorf("reading prefix dirs: %v", err)
}
for _, prefix := range prefixDirs {
if !prefix.IsDir() {
continue
}
files, err := os.ReadDir(filepath.Join(c.base, prefix.Name()))
if err != nil {
return fmt.Errorf("reading prefix dir: %v", err)
}
for _, file := range files {
// Ignore files whose names aren't valid AUM hashes, which may be
// temporary files which are partway through being written, or other
// files added by the OS (like .DS_Store) which we can ignore.
// TODO(alexc): it might be useful to append a suffix like `.aum` to
// filenames, so we can more easily distinguish between AUMs and
// arbitrary other files.
var h AUMHash
if err := h.UnmarshalText([]byte(file.Name())); err != nil {
log.Printf("ignoring unexpected non-AUM: %s: %v", file.Name(), err)
continue
}
info, err := c.get(h)
if err != nil {
return fmt.Errorf("reading %x: %v", h, err)
}
if info.PurgedUnix > 0 {
continue
}
eachHashInfo(info)
}
}
return nil
}
// SetLastActiveAncestor is called to record the oldest-known AUM
// that contributed to the current state. This value is used as
// a hint on next startup to determine which chain to pick when computing
// the current state, if there are multiple distinct chains.
func (c *FS) SetLastActiveAncestor(hash AUMHash) error {
c.mu.Lock()
defer c.mu.Unlock()
return atomicfile.WriteFile(filepath.Join(c.base, "last_active_ancestor"), hash[:], 0644)
}
// LastActiveAncestor returns the oldest-known AUM that was (in a
// previous run) an ancestor of the current state. This is used
// as a hint to pick the correct chain in the event that the Chonk stores
// multiple distinct chains.
//
// Nil is returned if no last-active ancestor is set.
func (c *FS) LastActiveAncestor() (*AUMHash, error) {
c.mu.RLock()
defer c.mu.RUnlock()
hash, err := os.ReadFile(filepath.Join(c.base, "last_active_ancestor"))
if err != nil {
if os.IsNotExist(err) {
return nil, nil // Not exist == none set.
}
return nil, err
}
var out AUMHash
if len(hash) != len(out) {
return nil, fmt.Errorf("stored hash is of wrong length: %d != %d", len(hash), len(out))
}
copy(out[:], hash)
return &out, nil
}
// CommitVerifiedAUMs durably stores the provided AUMs.
// Callers MUST ONLY provide AUMs which are verified (specifically,
// a call to aumVerify must return a nil error), as the
// implementation assumes that only verified AUMs are stored.
func (c *FS) CommitVerifiedAUMs(updates []AUM) error {
c.mu.Lock()
defer c.mu.Unlock()
c.invalidateIndexLocked()
for i, aum := range updates {
h := aum.Hash()
err := c.commit(h, func(info *fsHashInfo) {
info.PurgedUnix = 0 // just in-case it was set for some reason
info.AUM = &aum
})
if err != nil {
return fmt.Errorf("committing update[%d] (%x): %v", i, h, err)
}
}
return nil
}
// PurgeAUMs marks the specified AUMs for deletion from storage.
func (c *FS) PurgeAUMs(hashes []AUMHash) error {
if len(hashes) == 0 {
return nil
}
c.mu.Lock()
defer c.mu.Unlock()
c.invalidateIndexLocked()
now := time.Now()
for i, h := range hashes {
stored, err := c.get(h)
if err != nil {
return fmt.Errorf("reading %d (%x): %w", i, h, err)
}
if stored.AUM == nil || stored.PurgedUnix > 0 {
continue
}
err = c.commit(h, func(info *fsHashInfo) {
info.PurgedUnix = now.Unix()
})
if err != nil {
return fmt.Errorf("committing purge[%d] (%x): %w", i, h, err)
}
}
return nil
}
// commit calls the provided updater function to record changes relevant
// to the given hash. The caller is expected to update the AUM and
// Children fields, as relevant.
func (c *FS) commit(h AUMHash, updater func(*fsHashInfo)) error {
toCommit := fsHashInfo{}
existing, err := c.get(h)
switch {
case os.IsNotExist(err):
toCommit.CreatedUnix = time.Now().Unix()
case err != nil:
return err
default:
toCommit = *existing
}
updater(&toCommit)
if toCommit.AUM != nil && toCommit.AUM.Hash() != h {
return fmt.Errorf("cannot commit AUM with hash %x to %x", toCommit.AUM.Hash(), h)
}
dir, base := c.aumDir(h)
if err := os.MkdirAll(dir, 0755); err != nil && !os.IsExist(err) {
return fmt.Errorf("creating directory: %v", err)
}
m, err := cbor.CTAP2EncOptions().EncMode()
if err != nil {
return fmt.Errorf("cbor EncMode: %v", err)
}
var buff bytes.Buffer
if err := m.NewEncoder(&buff).Encode(toCommit); err != nil {
return fmt.Errorf("encoding: %v", err)
}
return atomicfile.WriteFile(filepath.Join(dir, base), buff.Bytes(), 0644)
}
// CompactionOptions describes tuneables to use when compacting a Chonk.
type CompactionOptions struct {
// The minimum number of ancestor AUMs to remember. The actual length
// of the chain post-compaction may be longer to reach a Checkpoint AUM.
MinChain int
// The minimum duration to store an AUM before it is a candidate for deletion.
MinAge time.Duration
}
// retainState tracks the state of an AUM hash as it is being considered for
// deletion.
type retainState uint8
// Valid retainState flags.
const (
retainStateActive retainState = 1 << iota // The AUM is part of the active chain and less than MinChain hops from HEAD.
retainStateYoung // The AUM is younger than MinAge.
retainStateLeaf // The AUM is a descendant of an AUM to be retained.
retainStateAncestor // The AUM is part of a chain between a retained AUM and the new lastActiveAncestor.
retainStateCandidate // The AUM is part of the active chain.
// retainAUMMask is a bit mask of any bit which should prevent
// the deletion of an AUM.
retainAUMMask retainState = retainStateActive | retainStateYoung | retainStateLeaf | retainStateAncestor
)
// markActiveChain marks AUMs in the active chain.
// All AUMs that are within minChain ancestors of head, or are marked as young, are
// marked retainStateActive, and all remaining ancestors are
// marked retainStateCandidate.
//
// markActiveChain returns the next ancestor AUM which is a checkpoint AUM.
func markActiveChain(storage Chonk, verdict map[AUMHash]retainState, minChain int, head AUMHash) (lastActiveAncestor AUMHash, err error) {
next, err := storage.AUM(head)
if err != nil {
return AUMHash{}, err
}
for i := range minChain {
h := next.Hash()
verdict[h] |= retainStateActive
parent, hasParent := next.Parent()
if !hasParent {
// Genesis AUM (beginning of time). The chain isn't long enough to need truncating.
return h, nil
}
if next, err = storage.AUM(parent); err != nil {
if err == os.ErrNotExist {
// We've reached the end of the chain we have stored.
return h, nil
}
return AUMHash{}, fmt.Errorf("reading active chain (retainStateActive) (%d, %v): %w", i, parent, err)
}
}
// If we got this far, we have at least minChain AUMs stored, and minChain number
// of ancestors have been marked for retention. We now continue to iterate backwards
// till we find an AUM which we can compact to: either a Checkpoint AUM which is old
// enough, or the genesis AUM.
for {
h := next.Hash()
verdict[h] |= retainStateActive
parent, hasParent := next.Parent()
isYoung := verdict[h]&retainStateYoung != 0
if next.MessageKind == AUMCheckpoint {
lastActiveAncestor = h
if !isYoung || !hasParent {
break
}
}
if next, err = storage.AUM(parent); err != nil {
return AUMHash{}, fmt.Errorf("searching for compaction target (%v): %w", parent, err)
}
}
// Mark remaining known ancestors as retainStateCandidate.
for {
parent, hasParent := next.Parent()
if !hasParent {
break
}
verdict[parent] |= retainStateCandidate
if next, err = storage.AUM(parent); err != nil {
if err == os.ErrNotExist {
// We've reached the end of the chain we have stored.
break
}
return AUMHash{}, fmt.Errorf("reading active chain (retainStateCandidate, %v): %w", parent, err)
}
}
return lastActiveAncestor, nil
}
// markYoungAUMs marks all AUMs younger than minAge for retention. All
// candidate AUMs must exist in verdict.
func markYoungAUMs(storage CompactableChonk, verdict map[AUMHash]retainState, minAge time.Duration) error {
minTime := time.Now().Add(-minAge)
for h := range verdict {
commitTime, err := storage.CommitTime(h)
if err != nil {
return err
}
if commitTime.After(minTime) {
verdict[h] |= retainStateYoung
}
}
return nil
}
// markAncestorIntersectionAUMs walks backwards from all AUMs to be retained,
// ensuring they intersect with candidateAncestor. All AUMs between a retained
// AUM and candidateAncestor are marked for retention.
//
// If there is no intersection between candidateAncestor and the ancestors of
// a retained AUM (this can happen if a retained AUM intersects the main chain
// before candidateAncestor) then candidate ancestor is recomputed based on
// the new oldest intersection.
//
// The final value for lastActiveAncestor is returned.
func markAncestorIntersectionAUMs(storage Chonk, verdict map[AUMHash]retainState, candidateAncestor AUMHash) (lastActiveAncestor AUMHash, err error) {
toScan := make([]AUMHash, 0, len(verdict))
for h, v := range verdict {
if (v & retainAUMMask) == 0 {
continue // not marked for retention, so don't need to consider it
}
if h == candidateAncestor {
continue
}
toScan = append(toScan, h)
}
var didAdjustCandidateAncestor bool
for len(toScan) > 0 {
nextIterScan := make([]AUMHash, 0, len(verdict))
for _, h := range toScan {
if verdict[h]&retainStateAncestor != 0 {
// This AUM and its ancestors have already been iterated.
continue
}
verdict[h] |= retainStateAncestor
a, err := storage.AUM(h)
if err != nil {
return AUMHash{}, fmt.Errorf("reading %v: %w", h, err)
}
parent, hasParent := a.Parent()
if !hasParent {
return AUMHash{}, errors.New("reached genesis AUM without intersecting with candidate ancestor")
}
if verdict[parent]&retainAUMMask != 0 {
// Includes candidateAncestor (has retainStateActive set)
continue
}
if verdict[parent]&retainStateCandidate != 0 {
// We've intersected with the active chain but haven't done so through
// candidateAncestor. That means that we intersect the active chain
// before candidateAncestor, hence candidateAncestor actually needs
// to be earlier than it is now.
candidateAncestor = parent
didAdjustCandidateAncestor = true
verdict[parent] |= retainStateAncestor
// There could be AUMs on the active chain between our new candidateAncestor
// and the old one, make sure they are marked as retained.
next := parent
childLoop:
for {
children, err := storage.ChildAUMs(next)
if err != nil {
return AUMHash{}, fmt.Errorf("reading children %v: %w", next, err)
}
// While there can be many children of an AUM, there can only be
// one child on the active chain (it will have retainStateCandidate set).
for _, a := range children {
h := a.Hash()
if v := verdict[h]; v&retainStateCandidate != 0 && v&retainStateActive == 0 {
verdict[h] |= retainStateAncestor
next = h
continue childLoop
}
}
break
}
}
nextIterScan = append(nextIterScan, parent)
}
toScan = nextIterScan
}
// If candidateAncestor was adjusted backwards, then it may not be a checkpoint
// (and hence a valid compaction candidate). If so, iterate backwards and adjust
// the candidateAncestor till we find a checkpoint.
if didAdjustCandidateAncestor {
var next AUM
if next, err = storage.AUM(candidateAncestor); err != nil {
return AUMHash{}, fmt.Errorf("searching for compaction target (%v): %w", candidateAncestor, err)
}
for {
h := next.Hash()
verdict[h] |= retainStateActive
if next.MessageKind == AUMCheckpoint {
candidateAncestor = h
break
}
parent, hasParent := next.Parent()
if !hasParent {
return AUMHash{}, errors.New("reached genesis AUM without finding an appropriate candidateAncestor")
}
if next, err = storage.AUM(parent); err != nil {
return AUMHash{}, fmt.Errorf("searching for compaction target (%v): %w", parent, err)
}
}
}
return candidateAncestor, nil
}
// markDescendantAUMs marks all children of a retained AUM as retained.
func markDescendantAUMs(storage Chonk, verdict map[AUMHash]retainState) error {
toScan := make([]AUMHash, 0, len(verdict))
for h, v := range verdict {
if v&retainAUMMask == 0 {
continue // not marked, so don't need to mark descendants
}
toScan = append(toScan, h)
}
for len(toScan) > 0 {
nextIterScan := make([]AUMHash, 0, len(verdict))
for _, h := range toScan {
if verdict[h]&retainStateLeaf != 0 {
// This AUM and its descendants have already been marked.
continue
}
verdict[h] |= retainStateLeaf
children, err := storage.ChildAUMs(h)
if err != nil {
return err
}
for _, a := range children {
nextIterScan = append(nextIterScan, a.Hash())
}
}
toScan = nextIterScan
}
return nil
}
// Compact deletes old AUMs from storage, based on the parameters given in opts.
func Compact(storage CompactableChonk, head AUMHash, opts CompactionOptions) (lastActiveAncestor AUMHash, err error) {
if opts.MinChain == 0 {
return AUMHash{}, errors.New("opts.MinChain must be set")
}
if opts.MinAge == 0 {
return AUMHash{}, errors.New("opts.MinAge must be set")
}
all, err := storage.AllAUMs()
if err != nil {
return AUMHash{}, fmt.Errorf("AllAUMs: %w", err)
}
verdict := make(map[AUMHash]retainState, len(all))
for _, h := range all {
verdict[h] = 0
}
if err := markYoungAUMs(storage, verdict, opts.MinAge); err != nil {
return AUMHash{}, fmt.Errorf("marking young AUMs: %w", err)
}
if lastActiveAncestor, err = markActiveChain(storage, verdict, opts.MinChain, head); err != nil {
return AUMHash{}, fmt.Errorf("marking active chain: %w", err)
}
if err := markDescendantAUMs(storage, verdict); err != nil {
return AUMHash{}, fmt.Errorf("marking descendant AUMs: %w", err)
}
if lastActiveAncestor, err = markAncestorIntersectionAUMs(storage, verdict, lastActiveAncestor); err != nil {
return AUMHash{}, fmt.Errorf("marking ancestor intersection: %w", err)
}
toDelete := make([]AUMHash, 0, len(verdict))
for h, v := range verdict {
if v&retainAUMMask == 0 { // no retention set
toDelete = append(toDelete, h)
}
}
if err := storage.SetLastActiveAncestor(lastActiveAncestor); err != nil {
return AUMHash{}, err
}
return lastActiveAncestor, storage.PurgeAUMs(toDelete)
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_tailnetlock
// Package tka implements the Tailnet Key Authority (TKA) for Tailnet Lock.
package tka
import (
"bytes"
"errors"
"fmt"
"os"
"sort"
"github.com/fxamacker/cbor/v2"
"tailscale.com/types/key"
"tailscale.com/types/tkatype"
"tailscale.com/util/set"
)
// Strict settings for the CBOR decoder.
var cborDecOpts = cbor.DecOptions{
DupMapKey: cbor.DupMapKeyEnforcedAPF,
IndefLength: cbor.IndefLengthForbidden,
TagsMd: cbor.TagsForbidden,
// Arbitrarily-chosen maximums.
MaxNestedLevels: 16, // Most likely to be hit for SigRotation sigs.
MaxArrayElements: 4096,
MaxMapPairs: 1024,
}
// Authority is a Tailnet Key Authority. This type is the main coupling
// point to the rest of the tailscale client.
//
// Authority objects can either be created from an existing, non-empty
// tailchonk (via tka.Open()), or created from scratch using tka.Bootstrap()
// or tka.Create().
type Authority struct {
head AUM
oldestAncestor AUM
state State
}
// Clone duplicates the Authority structure.
func (a *Authority) Clone() *Authority {
return &Authority{
head: a.head,
oldestAncestor: a.oldestAncestor,
state: a.state.Clone(),
}
}
// A chain describes a linear sequence of updates from Oldest to Head,
// resulting in some State at Head.
type chain struct {
Oldest AUM
Head AUM
state State
// Set to true if the AUM chain intersects with the active
// chain from a previous run.
chainsThroughActive bool
}
// computeChainCandidates returns all possible chains based on AUMs stored
// in the given tailchonk. A chain is defined as a unique (oldest, newest)
// AUM tuple. chain.state is not yet populated in returned chains.
//
// If lastKnownOldest is provided, any chain that includes the given AUM
// has the chainsThroughActive field set to true. This bit is leveraged
// in computeActiveAncestor() to filter out irrelevant chains when determining
// the active ancestor from a list of distinct chains.
func computeChainCandidates(storage Chonk, lastKnownOldest *AUMHash, maxIter int) ([]chain, error) {
heads, err := storage.Heads()
if err != nil {
return nil, fmt.Errorf("reading heads: %v", err)
}
candidates := make([]chain, len(heads))
for i := range heads {
// Oldest is iteratively computed below.
candidates[i] = chain{Oldest: heads[i], Head: heads[i]}
}
// Not strictly necessary, but simplifies checks in tests.
sort.Slice(candidates, func(i, j int) bool {
ih, jh := candidates[i].Oldest.Hash(), candidates[j].Oldest.Hash()
return bytes.Compare(ih[:], jh[:]) < 0
})
// candidates.Oldest needs to be computed by working backwards from
// head as far as we can.
iterAgain := true // if there's still work to be done.
for i := 0; iterAgain; i++ {
if i >= maxIter {
return nil, fmt.Errorf("iteration limit exceeded (%d)", maxIter)
}
iterAgain = false
for j := range candidates {
parentHash, hasParent := candidates[j].Oldest.Parent()
if hasParent {
parent, err := storage.AUM(parentHash)
if err != nil {
if err == os.ErrNotExist {
continue
}
return nil, fmt.Errorf("reading parent %s: %v", parentHash, err)
}
candidates[j].Oldest = parent
if lastKnownOldest != nil && *lastKnownOldest == parent.Hash() {
candidates[j].chainsThroughActive = true
}
iterAgain = true
}
}
}
return candidates, nil
}
// pickNextAUM returns the AUM which should be used as the next
// AUM in the chain, possibly applying fork resolution logic.
//
// In other words: given an AUM with 3 children like this:
//
// / - 1
// P - 2
// \ - 3
//
// pickNextAUM will determine and return the correct branch.
//
// This method takes ownership of the provided slice.
func pickNextAUM(state State, candidates []AUM) AUM {
switch len(candidates) {
case 0:
panic("pickNextAUM called with empty candidate set")
case 1:
return candidates[0]
}
// Oooof, we have some forks in the chain. We need to pick which
// one to use by applying the Fork Resolution Algorithm ✨
//
// The rules are this:
// 1. The child with the highest signature weight is chosen.
// 2. If equal, the child which is a RemoveKey AUM is chosen.
// 3. If equal, the child with the lowest AUM hash is chosen.
sort.Slice(candidates, func(j, i int) bool {
// Rule 1.
iSigWeight, jSigWeight := candidates[i].Weight(state), candidates[j].Weight(state)
if iSigWeight != jSigWeight {
return iSigWeight < jSigWeight
}
// Rule 2.
if iKind, jKind := candidates[i].MessageKind, candidates[j].MessageKind; iKind != jKind &&
(iKind == AUMRemoveKey || jKind == AUMRemoveKey) {
return jKind == AUMRemoveKey
}
// Rule 3.
iHash, jHash := candidates[i].Hash(), candidates[j].Hash()
return bytes.Compare(iHash[:], jHash[:]) > 0
})
return candidates[0]
}
// advanceByPrimary computes the next AUM to advance with based on
// deterministic fork-resolution rules. All nodes should apply this logic
// when computing the primary chain, hence achieving consensus on what the
// primary chain (and hence, the shared state) is.
//
// This method returns the chosen AUM & the state obtained by applying that
// AUM.
//
// The return value for next is nil if there are no children AUMs, hence
// the provided state is at head (up to date).
func advanceByPrimary(state State, candidates []AUM) (next *AUM, out State, err error) {
if len(candidates) == 0 {
return nil, state, nil
}
aum := pickNextAUM(state, candidates)
if state, err = state.applyVerifiedAUM(aum); err != nil {
return nil, State{}, fmt.Errorf("advancing state: %v", err)
}
return &aum, state, nil
}
// fastForwardWithAdvancer iteratively advances the current state by calling
// the given advancer to get+apply the next update. This process is repeated
// until the given termination function returns true or there is no more
// progress possible.
//
// The last-processed AUM, and the state computed after applying the last AUM,
// are returned.
func fastForwardWithAdvancer(
storage Chonk, maxIter int, startState State,
advancer func(state State, candidates []AUM) (next *AUM, out State, err error),
done func(curAUM AUM, curState State) bool,
) (AUM, State, error) {
if startState.LastAUMHash == nil {
return AUM{}, State{}, errors.New("invalid initial state")
}
nextAUM, err := storage.AUM(*startState.LastAUMHash)
if err != nil {
return AUM{}, State{}, fmt.Errorf("reading next (%v): %v", *startState.LastAUMHash, err)
}
curs := nextAUM
state := startState
for range maxIter {
if done != nil && done(curs, state) {
return curs, state, nil
}
children, err := storage.ChildAUMs(curs.Hash())
if err != nil {
return AUM{}, State{}, fmt.Errorf("getting children of %X: %v", curs.Hash(), err)
}
next, nextState, err := advancer(state, children)
if err != nil {
return AUM{}, State{}, fmt.Errorf("advance %X: %v", curs.Hash(), err)
}
if next == nil {
// There were no more children, we are at 'head'.
return curs, state, nil
}
curs = *next
state = nextState
}
return AUM{}, State{}, fmt.Errorf("iteration limit exceeded (%d)", maxIter)
}
// fastForward iteratively advances the current state based on known AUMs until
// the given termination function returns true or there is no more progress possible.
//
// The last-processed AUM, and the state computed after applying the last AUM,
// are returned.
func fastForward(storage Chonk, maxIter int, startState State, done func(curAUM AUM, curState State) bool) (AUM, State, error) {
return fastForwardWithAdvancer(storage, maxIter, startState, advanceByPrimary, done)
}
// computeStateAt returns the State at wantHash.
func computeStateAt(storage Chonk, maxIter int, wantHash AUMHash) (State, error) {
topAUM, err := storage.AUM(wantHash)
if err != nil {
return State{}, err
}
// Iterate backwards till we find a starting point to compute
// the state from.
//
// Valid starting points are either a checkpoint AUM, or a
// genesis AUM.
var (
curs = topAUM
state State
path = make(set.Set[AUMHash], 32) // 32 chosen arbitrarily.
)
for i := 0; true; i++ {
if i > maxIter {
return State{}, fmt.Errorf("iteration limit exceeded (%d)", maxIter)
}
path.Add(curs.Hash())
// Checkpoints encapsulate the state at that point, dope.
if curs.MessageKind == AUMCheckpoint {
state = curs.State.cloneForUpdate(&curs)
break
}
parent, hasParent := curs.Parent()
if !hasParent {
// This is a 'genesis' update: there are none before it, so
// this AUM can be applied to the empty state to determine
// the state at this AUM.
//
// It is only valid for NoOp, AddKey, and Checkpoint AUMs
// to be a genesis update. Checkpoint was handled earlier.
if mk := curs.MessageKind; mk == AUMNoOp || mk == AUMAddKey {
var err error
if state, err = (State{}).applyVerifiedAUM(curs); err != nil {
return State{}, fmt.Errorf("applying genesis (%+v): %v", curs, err)
}
break
}
return State{}, fmt.Errorf("invalid genesis update: %+v", curs)
}
// If we got here, the current state is dependent on the previous.
// Keep iterating backwards till that's not the case.
if curs, err = storage.AUM(parent); err != nil {
return State{}, fmt.Errorf("reading parent (%v): %v", parent, err)
}
}
// We now know some starting point state. Iterate forward till we
// are at the AUM we want state for.
//
// We want to fast forward based on the path we took above, which
// (in the case of a non-primary fork) may differ from a regular
// fast-forward (which follows standard fork-resolution rules). As
// such, we use a custom advancer here.
advancer := func(state State, candidates []AUM) (next *AUM, out State, err error) {
for _, c := range candidates {
if path.Contains(c.Hash()) {
if state, err = state.applyVerifiedAUM(c); err != nil {
return nil, State{}, fmt.Errorf("advancing state: %v", err)
}
return &c, state, nil
}
}
return nil, State{}, errors.New("no candidate matching path")
}
_, state, err = fastForwardWithAdvancer(storage, maxIter, state, advancer, func(curs AUM, _ State) bool {
return curs.Hash() == wantHash
})
// fastForward only terminates before the done condition if it
// doesn't have any later AUMs to process. This can't be the case
// as we've already iterated through them above so they must exist,
// but we check anyway to be super duper sure.
if err == nil && *state.LastAUMHash != wantHash {
return State{}, errors.New("unexpected fastForward outcome")
}
return state, err
}
// computeActiveAncestor determines which ancestor AUM to use as the
// ancestor of the valid chain.
//
// If all the chains end up having the same ancestor, then that's the
// only possible ancestor, ezpz. However if there are multiple distinct
// ancestors, that means there are distinct chains, and we need some
// hint to choose what to use. For that, we rely on the chainsThroughActive
// bit, which signals to us that that ancestor was part of the
// chain in a previous run.
func computeActiveAncestor(chains []chain) (AUMHash, error) {
// Dedupe possible ancestors, tracking if they were part of
// the active chain on a previous run.
ancestors := make(map[AUMHash]bool, len(chains))
for _, c := range chains {
ancestors[c.Oldest.Hash()] = c.chainsThroughActive
}
if len(ancestors) == 1 {
// There's only one. DOPE.
for k := range ancestors {
return k, nil
}
}
// There's more than one, so we need to use the ancestor that was
// part of the active chain in a previous iteration.
// Note that there can only be one distinct ancestor that was
// formerly part of the active chain, because AUMs can only have
// one parent and would have converged to a common ancestor.
for k, chainsThroughActive := range ancestors {
if chainsThroughActive {
return k, nil
}
}
return AUMHash{}, errors.New("multiple distinct chains")
}
// computeActiveChain bootstraps the runtime state of the Authority when
// starting entirely off stored state.
//
// TODO(tom): Don't look at head states, just iterate forward from
// the ancestor.
//
// The algorithm is as follows:
// 1. Determine all possible 'head' (like in git) states.
// 2. Filter these possible chains based on whether the ancestor was
// formerly (in a previous run) part of the chain.
// 3. Compute the state of the state machine at this ancestor. This is
// needed for fast-forward, as each update operates on the state of
// the update preceding it.
// 4. Iteratively apply updates till we reach head ('fast forward').
func computeActiveChain(storage Chonk, lastKnownOldest *AUMHash, maxIter int) (chain, error) {
chains, err := computeChainCandidates(storage, lastKnownOldest, maxIter)
if err != nil {
return chain{}, fmt.Errorf("computing candidates: %v", err)
}
if len(chains) == 0 {
return chain{}, errors.New("no chain candidates in AUM storage")
}
// Find the right ancestor.
oldestHash, err := computeActiveAncestor(chains)
if err != nil {
return chain{}, fmt.Errorf("computing ancestor: %v", err)
}
ancestor, err := storage.AUM(oldestHash)
if err != nil {
return chain{}, err
}
// At this stage we know the ancestor AUM, so we have excluded distinct
// chains but we might still have forks (so we don't know the head AUM).
//
// We iterate forward from the ancestor AUM, handling any forks as we go
// till we arrive at a head.
out := chain{Oldest: ancestor, Head: ancestor}
if out.state, err = computeStateAt(storage, maxIter, oldestHash); err != nil {
return chain{}, fmt.Errorf("bootstrapping state: %v", err)
}
out.Head, out.state, err = fastForward(storage, maxIter, out.state, nil)
if err != nil {
return chain{}, fmt.Errorf("fast forward: %v", err)
}
return out, nil
}
// aumVerify verifies if an AUM is well-formed, correctly signed, and
// can be accepted for storage.
func aumVerify(aum AUM, state State, isGenesisAUM bool) error {
if err := aum.StaticValidate(); err != nil {
return fmt.Errorf("invalid: %v", err)
}
if !isGenesisAUM {
if err := checkParent(aum, state); err != nil {
return err
}
}
if len(aum.Signatures) == 0 {
return errors.New("unsigned AUM")
}
sigHash := aum.SigHash()
for i, sig := range aum.Signatures {
key, err := state.GetKey(sig.KeyID)
if err != nil {
return fmt.Errorf("bad keyID on signature %d: %v", i, err)
}
if err := signatureVerify(&sig, sigHash, key); err != nil {
return fmt.Errorf("signature %d: %v", i, err)
}
}
if aum.MessageKind == AUMRemoveKey && len(state.Keys) == 1 {
if kid, err := state.Keys[0].ID(); err == nil && bytes.Equal(aum.KeyID, kid) {
return errors.New("cannot remove the last key in the state")
}
}
return nil
}
func checkParent(aum AUM, state State) error {
parent, hasParent := aum.Parent()
if !hasParent {
return errors.New("aum has no parent")
}
if state.LastAUMHash == nil {
return errors.New("cannot check update parent hash against a state with no previous AUM")
}
if *state.LastAUMHash != parent {
return fmt.Errorf("aum with parent %x cannot be applied to a state with parent %x", state.LastAUMHash, parent)
}
return nil
}
// Head returns the AUM digest of the latest update applied to the state
// machine.
func (a *Authority) Head() AUMHash {
return *a.state.LastAUMHash
}
// Open initializes an existing TKA from the given tailchonk.
//
// Only use this if the current node has initialized an Authority before.
// If a TKA exists on other nodes but there's nothing locally, use Bootstrap().
// If no TKA exists anywhere and you are creating it for the first
// time, use New().
func Open(storage Chonk) (*Authority, error) {
a, err := storage.LastActiveAncestor()
if err != nil {
return nil, fmt.Errorf("reading last ancestor: %v", err)
}
c, err := computeActiveChain(storage, a, maxScanIterations)
if err != nil {
return nil, fmt.Errorf("active chain: %v", err)
}
return &Authority{
head: c.Head,
oldestAncestor: c.Oldest,
state: c.state,
}, nil
}
// Create initializes a brand-new TKA, generating a genesis update
// and committing it to the given storage.
//
// The given signer must also be present in state as a trusted key.
//
// Do not use this to initialize a TKA that already exists, use Open()
// or Bootstrap() instead.
func Create(storage Chonk, state State, signer Signer) (*Authority, AUM, error) {
// Generate & sign a checkpoint, our genesis update.
genesis := AUM{
MessageKind: AUMCheckpoint,
State: &state,
}
if err := genesis.StaticValidate(); err != nil {
// This serves as an easy way to validate the given state.
return nil, AUM{}, fmt.Errorf("invalid state: %v", err)
}
sigs, err := signer.SignAUM(genesis.SigHash())
if err != nil {
return nil, AUM{}, fmt.Errorf("signing failed: %v", err)
}
genesis.Signatures = append(genesis.Signatures, sigs...)
a, err := Bootstrap(storage, genesis)
return a, genesis, err
}
// Bootstrap initializes a TKA based on the given checkpoint.
//
// Call this when setting up a new nodes' TKA, but other nodes
// with initialized TKA's exist.
//
// Pass the returned genesis AUM from Create(), or a later checkpoint AUM.
//
// TODO(tom): We should test an authority bootstrapped from a later checkpoint
// works fine with sync and everything.
func Bootstrap(storage Chonk, bootstrap AUM) (*Authority, error) {
heads, err := storage.Heads()
if err != nil {
return nil, fmt.Errorf("reading heads: %v", err)
}
if len(heads) != 0 {
return nil, errors.New("tailchonk is not empty")
}
// Check the AUM is well-formed.
if bootstrap.MessageKind != AUMCheckpoint {
return nil, fmt.Errorf("bootstrap AUMs must be checkpoint messages, got %v", bootstrap.MessageKind)
}
if bootstrap.State == nil {
return nil, errors.New("bootstrap AUM is missing state")
}
if err := aumVerify(bootstrap, *bootstrap.State, true); err != nil {
return nil, fmt.Errorf("invalid bootstrap: %v", err)
}
// Everything looks good, write it to storage.
if err := storage.CommitVerifiedAUMs([]AUM{bootstrap}); err != nil {
return nil, fmt.Errorf("commit: %v", err)
}
if err := storage.SetLastActiveAncestor(bootstrap.Hash()); err != nil {
return nil, fmt.Errorf("set ancestor: %v", err)
}
return Open(storage)
}
// ValidDisablement returns true if the disablement secret was correct.
//
// If this method returns true, the caller should shut down the authority
// and purge all tailnet-lock state.
func (a *Authority) ValidDisablement(secret []byte) bool {
return a.state.checkDisablement(secret)
}
// InformIdempotent returns a new Authority based on applying the given
// updates, with the given updates committed to storage.
//
// If any of the updates could not be applied:
// - An error is returned
// - No changes to storage are made.
//
// MissingAUMs() should be used to get a list of updates appropriate for
// this function. In any case, updates should be ordered oldest to newest.
func (a *Authority) InformIdempotent(storage Chonk, updates []AUM) (Authority, error) {
if len(updates) == 0 {
return Authority{}, errors.New("inform called with empty slice")
}
stateAt := make(map[AUMHash]State, len(updates)+1)
toCommit := make([]AUM, 0, len(updates))
prevHash := a.Head()
// The state at HEAD is the current state of the authority. It's likely
// to be needed, so we prefill it rather than computing it.
stateAt[prevHash] = a.state
// Optimization: If the set of updates is a chain building from
// the current head, EG:
// <a.Head()> ==> updates[0] ==> updates[1] ...
// Then there's no need to recompute the resulting state from the
// stored ancestor, because the last state computed during iteration
// is the new state. This should be the common case.
// isHeadChain keeps track of this.
isHeadChain := true
for i, update := range updates {
hash := update.Hash()
// Check if we already have this AUM thus don't need to process it.
if _, err := storage.AUM(hash); err == nil {
isHeadChain = false // Disable the head-chain optimization.
continue
}
parent, hasParent := update.Parent()
if !hasParent {
return Authority{}, fmt.Errorf("update %d: missing parent", i)
}
state, hasState := stateAt[parent]
var err error
if !hasState {
if state, err = computeStateAt(storage, maxScanIterations, parent); err != nil {
return Authority{}, fmt.Errorf("update %d computing state: %v", i, err)
}
stateAt[parent] = state
}
if err := aumVerify(update, state, false); err != nil {
return Authority{}, fmt.Errorf("update %d invalid: %v", i, err)
}
if stateAt[hash], err = state.applyVerifiedAUM(update); err != nil {
return Authority{}, fmt.Errorf("update %d cannot be applied: %v", i, err)
}
if isHeadChain && parent != prevHash {
isHeadChain = false
}
prevHash = hash
toCommit = append(toCommit, update)
}
if err := storage.CommitVerifiedAUMs(toCommit); err != nil {
return Authority{}, fmt.Errorf("commit: %v", err)
}
if isHeadChain {
// Head-chain fastpath: We can use the state we computed
// in the last iteration.
return Authority{
head: updates[len(updates)-1],
oldestAncestor: a.oldestAncestor,
state: stateAt[prevHash],
}, nil
}
oldestAncestor := a.oldestAncestor.Hash()
c, err := computeActiveChain(storage, &oldestAncestor, maxScanIterations)
if err != nil {
return Authority{}, fmt.Errorf("recomputing active chain: %v", err)
}
return Authority{
head: c.Head,
oldestAncestor: c.Oldest,
state: c.state,
}, nil
}
// Inform is the same as InformIdempotent, except the state of the Authority
// is updated in-place.
func (a *Authority) Inform(storage Chonk, updates []AUM) error {
newAuthority, err := a.InformIdempotent(storage, updates)
if err != nil {
return err
}
*a = newAuthority
return nil
}
// NodeKeyAuthorized checks if the provided nodeKeySignature authorizes
// the given node key.
func (a *Authority) NodeKeyAuthorized(nodeKey key.NodePublic, nodeKeySignature tkatype.MarshaledSignature) error {
_, err := a.NodeKeyAuthorizedWithDetails(nodeKey, nodeKeySignature)
return err
}
// NodeKeyAuthorized checks if the provided nodeKeySignature authorizes
// the given node key, and returns RotationDetails if the signature is
// a valid rotation signature.
func (a *Authority) NodeKeyAuthorizedWithDetails(nodeKey key.NodePublic, nodeKeySignature tkatype.MarshaledSignature) (*RotationDetails, error) {
var decoded NodeKeySignature
if err := decoded.Unserialize(nodeKeySignature); err != nil {
return nil, fmt.Errorf("unserialize: %v", err)
}
if decoded.SigKind == SigCredential {
return nil, errors.New("credential signatures cannot authorize nodes on their own")
}
kID, err := decoded.authorizingKeyID()
if err != nil {
return nil, err
}
key, err := a.state.GetKey(kID)
if err != nil {
return nil, fmt.Errorf("key: %v", err)
}
if err := decoded.verifySignature(nodeKey, key); err != nil {
return nil, err
}
return decoded.rotationDetails()
}
// KeyTrusted returns true if the given keyID is trusted by the tailnet
// key authority.
func (a *Authority) KeyTrusted(keyID tkatype.KeyID) bool {
_, err := a.state.GetKey(keyID)
return err == nil
}
// Keys returns the set of keys trusted by the tailnet key authority.
func (a *Authority) Keys() []Key {
out := make([]Key, len(a.state.Keys))
for i := range a.state.Keys {
out[i] = a.state.Keys[i].Clone()
}
return out
}
// StateIDs returns the stateIDs for this tailnet key authority. These
// are values that are fixed for the lifetime of the authority: see
// comments on the relevant fields in state.go.
func (a *Authority) StateIDs() (uint64, uint64) {
return a.state.StateID1, a.state.StateID2
}
// Compact deletes historical AUMs based on the given compaction options.
func (a *Authority) Compact(storage CompactableChonk, o CompactionOptions) error {
newAncestor, err := Compact(storage, a.head.Hash(), o)
if err != nil {
return err
}
ancestor, err := storage.AUM(newAncestor)
if err != nil {
return err
}
a.oldestAncestor = ancestor
return nil
}
// findParentForRewrite finds the parent AUM to use when rewriting state to
// retroactively remove trust in the specified keys.
func (a *Authority) findParentForRewrite(storage Chonk, removeKeys []tkatype.KeyID, ourKey tkatype.KeyID) (AUMHash, error) {
cursor := a.Head()
for {
if cursor == a.oldestAncestor.Hash() {
// We've reached as far back in our history as we can,
// so we have to rewrite from here.
break
}
aum, err := storage.AUM(cursor)
if err != nil {
return AUMHash{}, fmt.Errorf("reading AUM %v: %w", cursor, err)
}
// An ideal rewrite parent trusts none of the keys to be removed.
state, err := computeStateAt(storage, maxScanIterations, cursor)
if err != nil {
return AUMHash{}, fmt.Errorf("computing state for %v: %w", cursor, err)
}
keyTrusted := false
for _, key := range removeKeys {
if _, err := state.GetKey(key); err == nil {
keyTrusted = true
}
}
if !keyTrusted {
// Success: the revoked keys are not trusted.
// Check that our key was trusted to ensure
// we can sign a fork from here.
if _, err := state.GetKey(ourKey); err == nil {
break
}
}
parent, hasParent := aum.Parent()
if !hasParent {
// This is the genesis AUM, so we have to rewrite from here.
break
}
cursor = parent
}
return cursor, nil
}
// MakeRetroactiveRevocation generates a forking update which revokes the specified keys, in
// such a manner that any malicious use of those keys is erased.
//
// If forkFrom is specified, it is used as the parent AUM to fork from. If the zero value,
// the parent AUM is determined automatically.
//
// The generated AUM must be signed with more signatures than the sum of key votes that
// were compromised, before being consumed by tka.Authority methods.
func (a *Authority) MakeRetroactiveRevocation(storage Chonk, removeKeys []tkatype.KeyID, ourKey tkatype.KeyID, forkFrom AUMHash) (*AUM, error) {
var parent AUMHash
if forkFrom == (AUMHash{}) {
// Make sure at least one of the recovery keys is currently trusted.
foundKey := false
for _, k := range removeKeys {
if _, err := a.state.GetKey(k); err == nil {
foundKey = true
break
}
}
if !foundKey {
return nil, errors.New("no provided key is currently trusted")
}
p, err := a.findParentForRewrite(storage, removeKeys, ourKey)
if err != nil {
return nil, fmt.Errorf("finding parent: %v", err)
}
parent = p
} else {
parent = forkFrom
}
// Construct the new state where the revoked keys are no longer trusted.
state := a.state.Clone()
for _, keyToRevoke := range removeKeys {
idx := -1
for i := range state.Keys {
keyID, err := state.Keys[i].ID()
if err != nil {
return nil, fmt.Errorf("computing keyID: %v", err)
}
if bytes.Equal(keyToRevoke, keyID) {
idx = i
break
}
}
if idx >= 0 {
state.Keys = append(state.Keys[:idx], state.Keys[idx+1:]...)
}
}
if len(state.Keys) == 0 {
return nil, errors.New("cannot revoke all trusted keys")
}
state.LastAUMHash = nil // checkpoints can't specify a LastAUMHash
forkingAUM := &AUM{
MessageKind: AUMCheckpoint,
State: &state,
PrevAUMHash: parent[:],
}
return forkingAUM, forkingAUM.StaticValidate()
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
package tka
// Clone makes a deep copy of NodeKeySignature.
// The result aliases no memory with the original.
func (src *NodeKeySignature) Clone() *NodeKeySignature {
if src == nil {
return nil
}
dst := new(NodeKeySignature)
*dst = *src
dst.Pubkey = append(src.Pubkey[:0:0], src.Pubkey...)
dst.KeyID = append(src.KeyID[:0:0], src.KeyID...)
dst.Signature = append(src.Signature[:0:0], src.Signature...)
dst.Nested = src.Nested.Clone()
dst.WrappingPubkey = append(src.WrappingPubkey[:0:0], src.WrappingPubkey...)
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _NodeKeySignatureCloneNeedsRegeneration = NodeKeySignature(struct {
SigKind SigKind
Pubkey []byte
KeyID []byte
Signature []byte
Nested *NodeKeySignature
WrappingPubkey []byte
}{})
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_tailnetlock
package tka
import (
"crypto/ed25519"
"errors"
"fmt"
"github.com/hdevalence/ed25519consensus"
"tailscale.com/types/tkatype"
)
// signatureVerify returns a nil error if the signature is valid over the
// provided AUM BLAKE2s digest, using the given key.
func signatureVerify(s *tkatype.Signature, aumDigest tkatype.AUMSigHash, key Key) error {
// NOTE(tom): Even if we can compute the public from the KeyID,
// it's possible for the KeyID to be attacker-controlled
// so we should use the public contained in the state machine.
switch key.Kind {
case Key25519:
if len(key.Public) != ed25519.PublicKeySize {
return fmt.Errorf("ed25519 key has wrong length: %d", len(key.Public))
}
if ed25519consensus.Verify(ed25519.PublicKey(key.Public), aumDigest[:], s.Signature) {
return nil
}
return errors.New("invalid signature")
default:
return fmt.Errorf("unhandled key type: %v", key.Kind)
}
}
// 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 !experiment.reco
package testcontrol
import (
"bufio"
"bytes"
"cmp"
"context"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"maps"
"math/rand/v2"
"net/http"
"net/http/httptest"
"net/netip"
"net/url"
"slices"
"sort"
"strings"
"sync"
"time"
"golang.org/x/net/http2"
"tailscale.com/control/controlhttp/controlhttpserver"
"tailscale.com/net/netaddr"
"tailscale.com/net/tsaddr"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
"tailscale.com/tailcfg/nodecap"
"tailscale.com/tailcfg/peercap"
"tailscale.com/tka"
"tailscale.com/tstest/tkatest"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/opt"
"tailscale.com/util/httpm"
"tailscale.com/util/mak"
"tailscale.com/util/must"
"tailscale.com/util/rands"
"tailscale.com/util/set"
"tailscale.com/util/zstdframe"
)
const msgLimit = 1 << 20 // encrypted message length limit
// Server is a control plane server. Its zero value is ready for use.
// Everything is stored in-memory in one tailnet.
type Server struct {
Logf logger.Logf // nil means to use the log package
DERPMap *tailcfg.DERPMap // nil means to use prod DERP map
RequireAuth bool
RequireAuthKey string // required authkey for all nodes
RequireMachineAuth bool
Verbose bool
DNSConfig *tailcfg.DNSConfig // nil means no DNS config
MagicDNSDomain string
C2NResponses syncs.Map[string, func(*http.Response)] // token => onResponse func
OnSetDNS func(*tailcfg.SetDNSRequest) error
// PeerRelayGrants, if true, inserts relay capabilities into the wildcard
// grants rules.
PeerRelayGrants bool
// SSHPolicy, if non-nil, is sent to every node in MapResponses.
// Each node also gets [tailcfg.CapabilitySSH] added to its capability
// map, permitting "tailscale up --ssh".
SSHPolicy *tailcfg.SSHPolicy
// AllNodesSameUser, if true, makes all created nodes
// belong to the same user.
AllNodesSameUser bool
// TagOwners, if non-nil, enables modeling of the production control
// server's tag transition handling. Map keys are tags (e.g. "tag:foo")
// and values are the tags whose nodes are allowed to assign the key's
// tag. A node registering with Hostinfo.RequestTags gets those tags
// (signup-time ownership checks are not modeled). A later non-streaming
// map request whose Hostinfo.RequestTags differ from both the node's
// stored Hostinfo's RequestTags and its current tags is treated as a
// tag transition request: if the node's current tags own each requested
// tag, the node is retagged; otherwise its node key is expired to force
// reauthentication, as the production control server does.
//
// If nil, RequestTags in map requests are ignored.
TagOwners map[string][]string
// HoldMapRequest, if non-nil, is called with each incoming MapRequest
// before the server starts processing it. It may block to delay
// processing, letting tests control the order in which concurrent map
// requests are handled. If it returns a non-nil func, the server calls
// it when it finishes handling the request. For streaming requests
// that is when the poll ends, so returning a done func is mostly
// useful for non-streaming requests.
HoldMapRequest func(*tailcfg.MapRequest) (done func())
// AllOnline, if true, marks every peer entry in MapResponses as
// Online=true. This is a coarse stand-in for the per-node
// online/offline tracking that production control servers do based
// on streaming map sessions: certain disco-key handling fast paths
// in [tailscale.com/control/controlclient] and
// [tailscale.com/wgengine/userspace] only fire when the peer is
// reported online, so without this flag they are silently skipped
// in tests, which can mask bugs and slow down recovery from disco
// rotations. See [tailscale.com/control/controlclient/map.go]
// removeUnwantedDiscoUpdates and
// removeUnwantedDiscoUpdatesFromFullNetmapUpdate for callers that
// branch on Online.
//
// Finer-grained per-node online tracking can be added later.
AllOnline bool
// DefaultNodeCapabilities overrides the capability map sent to each client.
DefaultNodeCapabilities *tailcfg.NodeCapMap
// CollectServices, if non-empty, sets whether the control server asks
// for service updates. If empty, the default is "true".
CollectServices opt.Bool
// ExplicitBaseURL or HTTPTestServer must be set.
ExplicitBaseURL string // e.g. "http://127.0.0.1:1234" with no trailing URL
HTTPTestServer *httptest.Server // if non-nil, used to get BaseURL
// MaybeRejectRequest, if non-nil, is called before processing
// machine requests. If it returns a non-zero status, the request is rejected
// with that status code, the given Retry-After header value, and body string.
MaybeRejectRequest func(*http.Request) (status int, retryAfter string, msg string)
// ModifyFirstMapResponse, if non-nil, is called exactly once per
// MapResponse stream to modify the first MapResponse sent in response to it.
ModifyFirstMapResponse func(*tailcfg.MapResponse, *tailcfg.MapRequest)
// AltMapStream, if non-nil, takes over serveMap. See [AltMapStreamFunc].
AltMapStream AltMapStreamFunc
initMuxOnce sync.Once
mux *http.ServeMux
mu sync.Mutex
inServeMap int
cond *sync.Cond // lazily initialized by condLocked
pubKey key.MachinePublic
privKey key.ControlPrivate // not strictly needed vs. MachinePrivate, but handy to test type interactions.
// nodeSubnetRoutes is a list of subnet routes that are served
// by the specified node.
nodeSubnetRoutes map[key.NodePublic][]netip.Prefix
// peerIsJailed is the set of peers that are jailed for a node.
peerIsJailed map[key.NodePublic]map[key.NodePublic]bool // node => peer => isJailed
// nodeUnsignedPeerAPIOnly is the set of nodes that appear in other
// nodes' netmaps with tailcfg.Node.UnsignedPeerAPIOnly set, as
// Tailscale Funnel ingress nodes do.
nodeUnsignedPeerAPIOnly map[key.NodePublic]bool
// masquerades is the set of masquerades that should be applied to
// MapResponses sent to clients. It is keyed by the requesting nodes
// public key, and then the peer node's public key. The value is the
// masquerade address to use for that peer.
masquerades map[key.NodePublic]map[key.NodePublic]netip.Addr // node => peer => SelfNodeV{4,6}MasqAddrForThisPeer IP
// nodeCapMaps overrides the capability map sent down to a client.
nodeCapMaps map[key.NodePublic]tailcfg.NodeCapMap
// globalAppCaps configures global app capabilities, equivalent to:
// "grants": [
// {
// "src": ["*"],
// "dst": ["*"],
// "app": <contents of the input map>
// }
// ]
globalAppCaps tailcfg.PeerCapMap
// suppressAutoMapResponses is the set of nodes that should not be sent
// automatic map responses from serveMap. (They should only get manually sent ones)
suppressAutoMapResponses set.Set[key.NodePublic]
noisePubKey key.MachinePublic
noisePrivKey key.MachinePrivate
nodes map[key.NodePublic]*tailcfg.Node
users map[key.NodePublic]*tailcfg.User
logins map[key.NodePublic]*tailcfg.Login
updates map[tailcfg.NodeID]chan updateType
authPath map[string]*AuthPath
nodeKeyAuthed set.Set[key.NodePublic]
msgToSend map[key.NodePublic][]any // FIFO queue per node; values are *tailcfg.PingRequest or *tailcfg.MapResponse
allExpired bool // All nodes will be told their node key is expired.
// tkaStorage records the Tailnet Lock state, if any.
// If nil, Tailnet Lock is not enabled in the Tailnet.
tkaStorage tka.CompactableChonk
// onMapRequest, if non-nil, is called at the start of each map poll request.
// It can be used in tests to panic or fail if a node contacts control unexpectedly.
onMapRequest func(nodeKey key.NodePublic)
}
// BaseURL returns the server's base URL, without trailing slash.
func (s *Server) BaseURL() string {
if e := s.ExplicitBaseURL; e != "" {
return e
}
if hs := s.HTTPTestServer; hs != nil {
if hs.URL != "" {
return hs.URL
}
panic("Server.HTTPTestServer not started")
}
panic("Server ExplicitBaseURL and HTTPTestServer both unset")
}
// NumNodes returns the number of nodes in the testcontrol server.
//
// This is useful when connecting a bunch of virtual machines to a testcontrol
// server to see how many of them connected successfully.
func (s *Server) NumNodes() int {
s.mu.Lock()
defer s.mu.Unlock()
return len(s.nodes)
}
// condLocked lazily initializes and returns s.cond.
// s.mu must be held.
func (s *Server) condLocked() *sync.Cond {
if s.cond == nil {
s.cond = sync.NewCond(&s.mu)
}
return s.cond
}
// AwaitNodeInMapRequest waits for node k to be stuck in a map poll.
// It returns an error if and only if the context is done first.
func (s *Server) AwaitNodeInMapRequest(ctx context.Context, k key.NodePublic) error {
s.mu.Lock()
defer s.mu.Unlock()
cond := s.condLocked()
done := make(chan struct{})
defer close(done)
go func() {
select {
case <-done:
case <-ctx.Done():
cond.Broadcast()
}
}()
for {
node := s.nodeLocked(k)
if node == nil {
return errors.New("unknown node key")
}
if _, ok := s.updates[node.ID]; ok {
return nil
}
cond.Wait()
if err := ctx.Err(); err != nil {
return err
}
}
}
// AddPingRequest sends the ping pr to nodeKeyDst.
//
// It reports whether the message was enqueued. That is, it reports whether
// nodeKeyDst was connected.
func (s *Server) AddPingRequest(nodeKeyDst key.NodePublic, pr *tailcfg.PingRequest) bool {
return s.addDebugMessage(nodeKeyDst, pr)
}
// c2nRoundTripper is an http.RoundTripper that sends requests to a node via C2N.
type c2nRoundTripper struct {
s *Server
n key.NodePublic
}
func (s *Server) NodeRoundTripper(n key.NodePublic) http.RoundTripper {
return c2nRoundTripper{s, n}
}
func (rt c2nRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
ctx := req.Context()
resc := make(chan *http.Response, 1)
if err := rt.s.SendC2N(rt.n, req, func(r *http.Response) { resc <- r }); err != nil {
return nil, err
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case r := <-resc:
return r, nil
}
}
// SendC2N sends req to node. When the response is received, onRes is called.
func (s *Server) SendC2N(node key.NodePublic, req *http.Request, onRes func(*http.Response)) error {
var buf bytes.Buffer
if err := req.Write(&buf); err != nil {
return err
}
token := rands.HexString(10)
pr := &tailcfg.PingRequest{
URL: "https://unused/c2n/" + token,
Log: true,
Types: "c2n",
Payload: buf.Bytes(),
}
s.C2NResponses.Store(token, onRes)
if !s.AddPingRequest(node, pr) {
s.C2NResponses.Delete(token)
return fmt.Errorf("node %v not connected", node)
}
return nil
}
// AddRawMapResponse delivers the raw MapResponse mr to nodeKeyDst. It's meant
// for testing incremental map updates.
//
// Once AddRawMapResponse has been sent to a node, all future automatic
// MapResponses to that node will be suppressed and only explicit MapResponses
// injected via AddRawMapResponse will be sent.
//
// It reports whether the message was enqueued. That is, it reports whether
// nodeKeyDst was connected.
func (s *Server) AddRawMapResponse(nodeKeyDst key.NodePublic, mr *tailcfg.MapResponse) bool {
return s.addDebugMessage(nodeKeyDst, mr)
}
func (s *Server) addDebugMessage(nodeKeyDst key.NodePublic, msg any) bool {
s.mu.Lock()
defer s.mu.Unlock()
node := s.nodeLocked(nodeKeyDst)
if node == nil {
return false
}
updatesCh := s.updates[node.ID]
if updatesCh == nil {
// No streaming poll is registered, so there's nobody to deliver
// the message to.
return false
}
if _, ok := msg.(*tailcfg.MapResponse); ok {
if s.suppressAutoMapResponses == nil {
s.suppressAutoMapResponses = set.Set[key.NodePublic]{}
}
s.suppressAutoMapResponses.Add(nodeKeyDst)
}
mak.Set(&s.msgToSend, nodeKeyDst, append(s.msgToSend[nodeKeyDst], msg))
// sendUpdate returning false here is fine: the channel is a lossy
// wake-up signal whose buffer is single-slot. A full buffer means a
// prior wake-up is still pending, and the streaming poll will check
// msgToSend when it processes that wake-up. The queue in msgToSend
// is the source of truth.
sendUpdate(updatesCh, updateDebugInjection)
return true
}
// Mark the Node key of every node as expired
func (s *Server) SetExpireAllNodes(expired bool) {
s.mu.Lock()
defer s.mu.Unlock()
s.allExpired = expired
for _, node := range s.nodes {
sendUpdate(s.updates[node.ID], updateSelfChanged)
}
}
type AuthPath struct {
nodeKey key.NodePublic
closeOnce sync.Once
ch chan struct{}
success bool
}
func (ap *AuthPath) completeSuccessfully() {
ap.success = true
close(ap.ch)
}
// CompleteSuccessfully completes the login path successfully, as if
// the user did the whole auth dance.
func (ap *AuthPath) CompleteSuccessfully() {
ap.closeOnce.Do(ap.completeSuccessfully)
}
func (s *Server) logf(format string, a ...any) {
if s.Logf != nil {
s.Logf(format, a...)
} else {
log.Printf(format, a...)
}
}
func (s *Server) initMux() {
s.mux = http.NewServeMux()
s.mux.HandleFunc("/", s.serveUnhandled)
s.mux.HandleFunc("/generate_204", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
})
s.mux.HandleFunc("/key", s.serveKey)
s.mux.HandleFunc("/machine/tka/", s.serveTKA)
s.mux.HandleFunc("/machine/webclient/", s.serveWebClient)
s.mux.HandleFunc("/machine/", s.serveMachine)
s.mux.HandleFunc("/ts2021", s.serveNoiseUpgrade)
s.mux.HandleFunc("/c2n/", s.serveC2N)
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.initMuxOnce.Do(s.initMux)
s.mux.ServeHTTP(w, r)
}
func (s *Server) serveUnhandled(w http.ResponseWriter, r *http.Request) {
var got bytes.Buffer
r.Write(&got)
go panic(fmt.Sprintf("testcontrol.Server received unhandled request: %s", got.Bytes()))
}
// serveC2N handles a POST from a node containing a c2n response.
func (s *Server) serveC2N(w http.ResponseWriter, r *http.Request) {
if err := func() error {
if r.Method != httpm.POST {
return errors.New("POST required")
}
token, ok := strings.CutPrefix(r.URL.Path, "/c2n/")
if !ok {
return fmt.Errorf("invalid path %q", r.URL.Path)
}
onRes, ok := s.C2NResponses.Load(token)
if !ok {
return fmt.Errorf("unknown c2n token %q", token)
}
s.C2NResponses.Delete(token)
// Read the whole body before handing off the response, as
// onRes's receiver may read res.Body after this handler
// returns, at which point the HTTP/2 server has closed r.Body.
body, err := io.ReadAll(r.Body)
if err != nil {
return fmt.Errorf("error reading c2n response: %w", err)
}
res, err := http.ReadResponse(bufio.NewReader(bytes.NewReader(body)), nil)
if err != nil {
return fmt.Errorf("error reading c2n response: %w", err)
}
onRes(res)
return nil
}(); err != nil {
s.logf("testcontrol: %s", err)
http.Error(w, err.Error(), 500)
return
}
w.WriteHeader(http.StatusNoContent)
}
type peerMachinePublicContextKey struct{}
func (s *Server) serveNoiseUpgrade(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Allow GET for WebSocket-based clients (e.g. cmd/tsconnect/wasm) in
// addition to POST for the raw HTTP-upgrade path. AcceptHTTP routes by
// the Upgrade header and the underlying websocket library enforces
// GET for WebSocket upgrades.
if r.Method != "POST" && r.Method != "GET" {
http.Error(w, "POST or GET required", 400)
return
}
s.mu.Lock()
noisePrivate := s.noisePrivKey
s.mu.Unlock()
cc, err := controlhttpserver.AcceptHTTP(ctx, w, r, noisePrivate, nil)
if err != nil {
log.Printf("AcceptHTTP: %v", err)
return
}
defer cc.Close()
var h2srv http2.Server
peerPub := cc.Peer()
h2srv.ServeConn(cc, &http2.ServeConnOpts{
Context: context.WithValue(ctx, peerMachinePublicContextKey{}, peerPub),
BaseConfig: &http.Server{
Handler: s.mux,
},
})
}
func (s *Server) publicKeys() (noiseKey, pubKey key.MachinePublic) {
s.mu.Lock()
defer s.mu.Unlock()
s.ensureKeyPairLocked()
return s.noisePubKey, s.pubKey
}
func (s *Server) ensureKeyPairLocked() {
if !s.pubKey.IsZero() {
return
}
s.noisePrivKey = key.NewMachine()
s.noisePubKey = s.noisePrivKey.Public()
s.privKey = key.NewControl()
s.pubKey = s.privKey.Public()
}
func (s *Server) serveKey(w http.ResponseWriter, r *http.Request) {
noiseKey, legacyKey := s.publicKeys()
if r.FormValue("v") == "" {
w.Header().Set("Content-Type", "text/plain")
io.WriteString(w, legacyKey.UntypedHexString())
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(&tailcfg.OverTLSPublicKeyResponse{
LegacyPublicKey: legacyKey,
PublicKey: noiseKey,
})
}
func (s *Server) serveMachine(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "POST required for serveMachine", 400)
return
}
ctx := r.Context()
mkey, ok := ctx.Value(peerMachinePublicContextKey{}).(key.MachinePublic)
if !ok {
panic("no peer machine public key in context")
}
if fn := s.MaybeRejectRequest; fn != nil {
if status, retryAfter, msg := fn(r); status != 0 {
if retryAfter != "" {
w.Header().Set("Retry-After", retryAfter)
}
http.Error(w, msg, status)
return
}
}
switch r.URL.Path {
case "/machine/map":
s.serveMap(w, r, mkey)
case "/machine/register":
s.serveRegister(w, r, mkey)
case "/machine/set-dns":
s.serveSetDNS(w, r, mkey)
case "/machine/update-health":
io.Copy(io.Discard, r.Body)
w.WriteHeader(http.StatusNoContent)
default:
s.serveUnhandled(w, r)
}
}
// serveWebClient handles the Noise-protected web client auth flow endpoints
// posted to /machine/webclient/init/<src>/to/<dst> and
// /machine/webclient/wait/<src>/to/<dst>/<id>. It is the test-control
// counterpart to client/web's check-mode session creation: it returns a
// placeholder auth URL for init, and immediately Complete=true for wait, so
// tests can drive the full check-mode session lifecycle without a real
// browser-click loop.
func (s *Server) serveWebClient(w http.ResponseWriter, r *http.Request) {
if r.Method != httpm.POST {
http.Error(w, "POST required", http.StatusMethodNotAllowed)
return
}
var resp tailcfg.WebClientAuthResponse
switch {
case strings.HasPrefix(r.URL.Path, "/machine/webclient/init/"):
resp.ID = "testcontrol-webclient-auth"
resp.URL = "https://control.tailscale/test-web-auth"
case strings.HasPrefix(r.URL.Path, "/machine/webclient/wait/"):
resp.Complete = true
default:
s.serveUnhandled(w, r)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("testcontrol: encoding web client response: %v", err)
}
}
func (s *Server) serveSetDNS(w http.ResponseWriter, r *http.Request, mkey key.MachinePublic) {
var req tailcfg.SetDNSRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if req.Type != "TXT" {
http.Error(w, "only TXT records are supported", http.StatusBadRequest)
return
}
if req.Name == "" || req.Value == "" {
http.Error(w, "missing name or value", http.StatusBadRequest)
return
}
if req.NodeKey.IsZero() {
http.Error(w, "missing node key", http.StatusBadRequest)
return
}
s.mu.Lock()
node := s.nodes[req.NodeKey]
certDomains := s.certDomainsLocked(node)
s.mu.Unlock()
if node == nil {
http.Error(w, "unknown node key", http.StatusForbidden)
return
}
if node.Machine != mkey {
http.Error(w, "node key does not belong to machine", http.StatusForbidden)
return
}
baseName, ok := strings.CutPrefix(req.Name, "_acme-challenge.")
if !ok || !slices.Contains(certDomains, baseName) {
http.Error(w, "name is not an ACME challenge for a cert domain", http.StatusForbidden)
return
}
if s.OnSetDNS != nil {
if err := s.OnSetDNS(&req); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(tailcfg.SetDNSResponse{})
}
func (s *Server) certDomainsLocked(node *tailcfg.Node) []string {
if node == nil {
return nil
}
var ret []string
if s.DNSConfig != nil {
ret = append(ret, s.DNSConfig.CertDomains...)
}
if s.MagicDNSDomain != "" {
ret = append(ret, node.Hostinfo.Hostname()+"."+s.MagicDNSDomain)
}
return ret
}
// SetSubnetRoutes sets the list of subnet routes which a node is routing.
func (s *Server) SetSubnetRoutes(nodeKey key.NodePublic, routes []netip.Prefix) {
s.mu.Lock()
defer s.mu.Unlock()
s.logf("Setting subnet routes for %s: %v", nodeKey.ShortString(), routes)
mak.Set(&s.nodeSubnetRoutes, nodeKey, routes)
if node, ok := s.nodes[nodeKey]; ok {
sendUpdate(s.updates[node.ID], updateSelfChanged)
// Also notify all other peers so they get the updated AllowedIPs
// in their next MapResponse.
for _, n := range s.nodes {
if n.ID != node.ID {
sendUpdate(s.updates[n.ID], updatePeerChanged)
}
}
}
}
// MasqueradePair is a pair of nodes and the IP address that the
// Node masquerades as for the Peer.
//
// Setting this will have future MapResponses for Node to have
// Peer.SelfNodeV{4,6}MasqAddrForThisPeer set to NodeMasqueradesAs.
// MapResponses for the Peer will now see Node.Addresses as
// NodeMasqueradesAs.
type MasqueradePair struct {
Node key.NodePublic
Peer key.NodePublic
NodeMasqueradesAs netip.Addr
}
// SetJailed sets b to be jailed when it is a peer of a.
func (s *Server) SetJailed(a, b key.NodePublic, jailed bool) {
s.mu.Lock()
defer s.mu.Unlock()
if s.peerIsJailed == nil {
s.peerIsJailed = map[key.NodePublic]map[key.NodePublic]bool{}
}
if s.peerIsJailed[a] == nil {
s.peerIsJailed[a] = map[key.NodePublic]bool{}
}
s.peerIsJailed[a][b] = jailed
s.updateLocked("SetJailed", s.nodeIDsLocked(0))
}
// SetUnsignedPeerAPIOnly sets whether the node with the given key
// appears in other nodes' netmaps with UnsignedPeerAPIOnly set, as
// Tailscale Funnel ingress nodes do.
func (s *Server) SetUnsignedPeerAPIOnly(k key.NodePublic, unsigned bool) {
s.mu.Lock()
defer s.mu.Unlock()
mak.Set(&s.nodeUnsignedPeerAPIOnly, k, unsigned)
s.updateLocked("SetUnsignedPeerAPIOnly", s.nodeIDsLocked(0))
}
// SetMasqueradeAddresses sets the masquerade addresses for the server.
// See MasqueradePair for more details.
func (s *Server) SetMasqueradeAddresses(pairs []MasqueradePair) {
m := make(map[key.NodePublic]map[key.NodePublic]netip.Addr)
for _, p := range pairs {
if m[p.Node] == nil {
m[p.Node] = make(map[key.NodePublic]netip.Addr)
}
m[p.Node][p.Peer] = p.NodeMasqueradesAs
}
s.mu.Lock()
defer s.mu.Unlock()
s.masquerades = m
s.updateLocked("SetMasqueradeAddresses", s.nodeIDsLocked(0))
}
// SetSSHPolicy sets the SSH policy sent in MapResponses and notifies all
// connected nodes so they pick up the change.
func (s *Server) SetSSHPolicy(policy *tailcfg.SSHPolicy) {
s.mu.Lock()
defer s.mu.Unlock()
s.SSHPolicy = policy
s.updateLocked("SetSSHPolicy", s.nodeIDsLocked(0))
}
// SetNodeCapMap overrides the capability map the specified client receives.
func (s *Server) SetNodeCapMap(nodeKey key.NodePublic, capMap tailcfg.NodeCapMap) {
s.mu.Lock()
defer s.mu.Unlock()
mak.Set(&s.nodeCapMaps, nodeKey, capMap)
s.updateLocked("SetNodeCapMap", s.nodeIDsLocked(0))
}
// SetGlobalAppCaps configures global app capabilities. This is equivalent to
//
// "grants": [
// {
// "src": ["*"],
// "dst": ["*"],
// "app": <contents of the input map>
// }
// ]
func (s *Server) SetGlobalAppCaps(appCaps tailcfg.PeerCapMap) {
s.mu.Lock()
defer s.mu.Unlock()
s.globalAppCaps = appCaps
s.updateLocked("SetGlobalAppCaps", s.nodeIDsLocked(0))
}
// AddDNSRecords adds records to the server's DNS config.
func (s *Server) AddDNSRecords(records ...tailcfg.DNSRecord) {
s.mu.Lock()
defer s.mu.Unlock()
if s.DNSConfig == nil {
s.DNSConfig = new(tailcfg.DNSConfig)
}
s.DNSConfig.ExtraRecords = append(s.DNSConfig.ExtraRecords, records...)
s.updateLocked("AddDNSRecords", s.nodeIDsLocked(0))
}
// nodeIDsLocked returns the node IDs of all nodes in the server, except
// for the node with the given ID.
func (s *Server) nodeIDsLocked(except tailcfg.NodeID) []tailcfg.NodeID {
var ids []tailcfg.NodeID
for _, n := range s.nodes {
if n.ID == except {
continue
}
ids = append(ids, n.ID)
}
return ids
}
// Node returns the node for nodeKey. It's always nil or cloned memory.
func (s *Server) Node(nodeKey key.NodePublic) *tailcfg.Node {
s.mu.Lock()
defer s.mu.Unlock()
return s.nodeLocked(nodeKey)
}
// nodeLocked returns the node for nodeKey. It's always nil or cloned memory.
//
// s.mu must be held.
func (s *Server) nodeLocked(nodeKey key.NodePublic) *tailcfg.Node {
return s.nodes[nodeKey].Clone()
}
// AddFakeNode injects a fake node into the server.
func (s *Server) AddFakeNode() {
s.mu.Lock()
defer s.mu.Unlock()
if s.nodes == nil {
s.nodes = make(map[key.NodePublic]*tailcfg.Node)
}
nk := key.NewNode().Public()
mk := key.NewMachine().Public()
dk := key.NewDisco().Public()
r := nk.Raw32()
id := int64(binary.LittleEndian.Uint64(r[:]))
ip := netaddr.IPv4(r[0], r[1], r[2], r[3])
addr := netip.PrefixFrom(ip, 32)
s.nodes[nk] = &tailcfg.Node{
ID: tailcfg.NodeID(id),
StableID: tailcfg.StableNodeID(fmt.Sprintf("TESTCTRL%08x", id)),
User: tailcfg.UserID(id),
Machine: mk,
Key: nk,
MachineAuthorized: true,
DiscoKey: dk,
Addresses: []netip.Prefix{addr},
AllowedIPs: []netip.Prefix{addr},
}
// TODO: send updates to other (non-fake?) nodes
}
func (s *Server) allUserProfiles() (res []tailcfg.UserProfile) {
s.mu.Lock()
defer s.mu.Unlock()
for k, u := range s.users {
up := tailcfg.UserProfile{
ID: u.ID,
DisplayName: u.DisplayName,
}
if login, ok := s.logins[k]; ok {
up.LoginName = login.LoginName
up.ProfilePicURL = cmp.Or(up.ProfilePicURL, login.ProfilePicURL)
up.DisplayName = cmp.Or(up.DisplayName, login.DisplayName)
}
res = append(res, up)
}
return res
}
func (s *Server) AllNodes() (nodes []*tailcfg.Node) {
s.mu.Lock()
defer s.mu.Unlock()
for _, n := range s.nodes {
nodes = append(nodes, n.Clone())
}
sort.Slice(nodes, func(i, j int) bool {
return nodes[i].StableID < nodes[j].StableID
})
return nodes
}
const domain = "fake-control.example.net"
func (s *Server) getUser(nodeKey key.NodePublic) (*tailcfg.User, *tailcfg.Login) {
s.mu.Lock()
defer s.mu.Unlock()
if s.users == nil {
s.users = map[key.NodePublic]*tailcfg.User{}
}
if s.logins == nil {
s.logins = map[key.NodePublic]*tailcfg.Login{}
}
if u, ok := s.users[nodeKey]; ok {
return u, s.logins[nodeKey]
}
id := tailcfg.UserID(len(s.users) + 1)
if s.AllNodesSameUser {
id = 123
}
s.logf("Created user %v for node %s", id, nodeKey)
loginName := fmt.Sprintf("user-%d@%s", id, domain)
displayName := fmt.Sprintf("User %d", id)
login := &tailcfg.Login{
ID: tailcfg.LoginID(id),
Provider: "testcontrol",
LoginName: loginName,
DisplayName: displayName,
ProfilePicURL: "https://tailscale.com/static/images/marketing/team-carney.jpg",
}
user := &tailcfg.User{
ID: id,
DisplayName: displayName,
}
s.users[nodeKey] = user
s.logins[nodeKey] = login
return user, login
}
// authPathDone returns a close-only struct that's closed when the
// authPath ("/auth/XXXXXX") has authenticated.
func (s *Server) authPathDone(authPath string) <-chan struct{} {
s.mu.Lock()
defer s.mu.Unlock()
if a, ok := s.authPath[authPath]; ok {
return a.ch
}
return nil
}
func (s *Server) addAuthPath(authPath string, nodeKey key.NodePublic) {
s.mu.Lock()
defer s.mu.Unlock()
if s.authPath == nil {
s.authPath = map[string]*AuthPath{}
}
s.authPath[authPath] = &AuthPath{
ch: make(chan struct{}),
nodeKey: nodeKey,
}
}
// CompleteAuth marks the provided path or URL (containing
// "/auth/...") as successfully authenticated, unblocking any
// requests blocked on that in serveRegister.
func (s *Server) CompleteAuth(authPathOrURL string) bool {
i := strings.Index(authPathOrURL, "/auth/")
if i == -1 {
return false
}
authPath := authPathOrURL[i:]
s.mu.Lock()
defer s.mu.Unlock()
ap, ok := s.authPath[authPath]
if !ok {
return false
}
if ap.nodeKey.IsZero() {
panic("zero AuthPath.NodeKey")
}
s.nodeKeyAuthed.Make()
s.nodeKeyAuthed.Add(ap.nodeKey)
ap.CompleteSuccessfully()
return true
}
// Complete the device approval for this node.
//
// This function returns false if the node does not exist, or you try to
// approve a device against a different control server.
func (s *Server) CompleteDeviceApproval(controlUrl string, urlStr string, nodeKey *key.NodePublic) bool {
s.mu.Lock()
defer s.mu.Unlock()
node, ok := s.nodes[*nodeKey]
if !ok {
return false
}
if urlStr != controlUrl+"/admin" {
return false
}
sendUpdate(s.updates[node.ID], updateSelfChanged)
node.MachineAuthorized = true
return true
}
func (s *Server) serveRegister(w http.ResponseWriter, r *http.Request, mkey key.MachinePublic) {
msg, err := io.ReadAll(io.LimitReader(r.Body, msgLimit))
r.Body.Close()
if err != nil {
http.Error(w, fmt.Sprintf("bad map request read: %v", err), 400)
return
}
var req tailcfg.RegisterRequest
if err := s.decode(msg, &req); err != nil {
go panic(fmt.Sprintf("serveRegister: decode: %v", err))
}
if req.Version == 0 {
panic("serveRegister: zero Version")
}
if req.NodeKey.IsZero() {
go panic("serveRegister: request has zero node key")
}
if s.Verbose {
j, _ := json.MarshalIndent(req, "", "\t")
log.Printf("Got %T: %s", req, j)
}
if s.RequireAuthKey != "" && (req.Auth == nil || req.Auth.AuthKey != s.RequireAuthKey) {
res := must.Get(s.encode(false, tailcfg.RegisterResponse{
Error: "invalid authkey",
}))
w.WriteHeader(200)
w.Write(res)
return
}
// If this is a followup request, wait until interactive followup URL visit complete.
isFollowup := req.Followup != ""
if isFollowup {
followupURL, err := url.Parse(req.Followup)
if err != nil {
panic(err)
}
doneCh := s.authPathDone(followupURL.Path)
select {
case <-r.Context().Done():
return
case <-doneCh:
}
// TODO(bradfitz): support a side test API to mark an
// auth as failed so we can send an error response in
// some follow-ups? For now all are successes.
}
// On a key rotation (OldNodeKey set and known to s.nodes), stage
// the new key as a candidate entry but keep the old key's entry
// alive so an in-flight map poll can still receive updates while
// the user completes the auth URL.
s.mu.Lock()
if _, oldNodeKeyOk := s.nodes[req.OldNodeKey]; oldNodeKeyOk {
if _, newNodeKeyOk := s.nodes[req.NodeKey]; !newNodeKeyOk {
cloned := s.nodes[req.OldNodeKey].Clone()
cloned.Key = req.NodeKey
s.nodes[req.NodeKey] = cloned
s.users[req.NodeKey] = s.users[req.OldNodeKey]
s.logins[req.NodeKey] = s.logins[req.OldNodeKey]
}
if isFollowup {
// The user has completed the auth URL, the new key
// is now authoritative. Retire the old key's entry.
delete(s.nodes, req.OldNodeKey)
delete(s.users, req.OldNodeKey)
delete(s.logins, req.OldNodeKey)
}
}
s.mu.Unlock()
nk := req.NodeKey
user, login := s.getUser(nk)
s.mu.Lock()
if s.nodes == nil {
s.nodes = map[key.NodePublic]*tailcfg.Node{}
}
_, ok := s.nodes[nk]
machineAuthorized := !s.RequireMachineAuth
if !ok {
nodeID := len(s.nodes) + 1
v4Prefix := netip.PrefixFrom(netaddr.IPv4(100, 64, uint8(nodeID>>8), uint8(nodeID)), 32)
v6Prefix := netip.PrefixFrom(tsaddr.Tailscale4To6(v4Prefix.Addr()), 128)
allowedIPs := []netip.Prefix{
v4Prefix,
v6Prefix,
}
var capMap tailcfg.NodeCapMap
if s.DefaultNodeCapabilities != nil {
capMap = *s.DefaultNodeCapabilities
} else {
capMap = tailcfg.NodeCapMap{
nodecap.HTTPS: []tailcfg.RawMessage{},
nodecap.Funnel: []tailcfg.RawMessage{},
nodecap.FileSharing: []tailcfg.RawMessage{},
nodecap.FunnelPorts + "?ports=8080,443": []tailcfg.RawMessage{},
}
}
node := &tailcfg.Node{
ID: tailcfg.NodeID(nodeID),
StableID: tailcfg.StableNodeID(fmt.Sprintf("TESTCTRL%08x", int(nodeID))),
User: user.ID,
Machine: mkey,
Key: req.NodeKey,
MachineAuthorized: machineAuthorized,
Addresses: allowedIPs,
AllowedIPs: allowedIPs,
Hostinfo: req.Hostinfo.View(),
Name: req.Hostinfo.Hostname,
Cap: req.Version,
CapMap: capMap,
Capabilities: slices.Collect(maps.Keys(capMap)),
}
if s.TagOwners != nil && req.Hostinfo != nil {
// Trust the requested tags at signup; ownership checks
// against the registering user are not modeled.
node.Tags = slices.Clone(req.Hostinfo.RequestTags)
}
if s.MagicDNSDomain != "" {
node.Name = node.Name + "." + s.MagicDNSDomain + "."
}
s.nodes[nk] = node
}
// Consider a node key expired if allExpired is set or if the nodeKey has
// an expiry time in the past. This allows tests to set per-node KeyExpiry
// via UpdateNode to simulate an admin-triggered or time-based expiry.
nodeKeyExpired := s.allExpired
if !nodeKeyExpired && req.OldNodeKey.IsZero() {
if n, ok := s.nodes[nk]; ok && !n.KeyExpiry.IsZero() && n.KeyExpiry.Before(time.Now()) {
nodeKeyExpired = true
}
}
requireAuth := s.RequireAuth
if requireAuth && s.nodeKeyAuthed.Contains(nk) && !nodeKeyExpired {
requireAuth = false
}
s.mu.Unlock()
authURL := ""
if requireAuth {
authPath := fmt.Sprintf("/auth/%s", rands.HexString(20))
s.addAuthPath(authPath, nk)
authURL = s.BaseURL() + authPath
}
res, err := s.encode(false, tailcfg.RegisterResponse{
User: *user,
Login: *login,
NodeKeyExpired: nodeKeyExpired,
MachineAuthorized: machineAuthorized,
AuthURL: authURL,
})
if err != nil {
go panic(fmt.Sprintf("serveRegister: encode: %v", err))
}
w.WriteHeader(200)
w.Write(res)
}
func (s *Server) serveTKA(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, "GET required for serveTKA", 400)
return
}
switch r.URL.Path {
case "/machine/tka/init/begin":
s.serveTKAInitBegin(w, r)
case "/machine/tka/init/finish":
s.serveTKAInitFinish(w, r)
case "/machine/tka/bootstrap":
s.serveTKABootstrap(w, r)
case "/machine/tka/sync/offer":
s.serveTKASyncOffer(w, r)
case "/machine/tka/sign":
s.serveTKASign(w, r)
default:
s.serveUnhandled(w, r)
}
}
func (s *Server) serveTKAInitBegin(w http.ResponseWriter, r *http.Request) {
s.mu.Lock()
defer s.mu.Unlock()
nodes := maps.Values(s.nodes)
genesisAUM, err := tkatest.HandleTKAInitBegin(w, r, nodes)
if err != nil {
go panic(fmt.Sprintf("HandleTKAInitBegin: %v", err))
}
s.tkaStorage = tka.ChonkMem()
s.tkaStorage.CommitVerifiedAUMs([]tka.AUM{*genesisAUM})
}
func (s *Server) serveTKAInitFinish(w http.ResponseWriter, r *http.Request) {
signatures, err := tkatest.HandleTKAInitFinish(w, r)
if err != nil {
go panic(fmt.Sprintf("HandleTKAInitFinish: %v", err))
}
s.mu.Lock()
defer s.mu.Unlock()
// Apply the signatures to each of the nodes. Because s.nodes is keyed
// by public key instead of node ID, we have to do this inefficiently.
//
// We only have small tailnets in the integration tests, so this isn't
// much of an issue.
for nodeID, sig := range signatures {
for _, n := range s.nodes {
if n.ID == nodeID {
n.KeySignature = sig
}
}
}
}
func (s *Server) serveTKABootstrap(w http.ResponseWriter, r *http.Request) {
s.mu.Lock()
defer s.mu.Unlock()
if s.tkaStorage == nil {
http.Error(w, "no TKA state when calling serveTKABootstrap", 400)
return
}
// Find the genesis AUM, which we need to include in the response.
var genesis *tka.AUM
allAUMs, err := s.tkaStorage.AllAUMs()
if err != nil {
http.Error(w, "unable to retrieve all AUMs from TKA state", 500)
return
}
for _, h := range allAUMs {
aum := must.Get(s.tkaStorage.AUM(h))
if _, hasParent := aum.Parent(); !hasParent {
genesis = &aum
break
}
}
if genesis == nil {
http.Error(w, "unable to find genesis AUM in TKA state", 500)
return
}
resp := tailcfg.TKABootstrapResponse{
GenesisAUM: genesis.Serialize(),
}
_, err = tkatest.HandleTKABootstrap(w, r, resp)
if err != nil {
go panic(fmt.Sprintf("HandleTKABootstrap: %v", err))
}
}
func (s *Server) serveTKASyncOffer(w http.ResponseWriter, r *http.Request) {
s.mu.Lock()
defer s.mu.Unlock()
authority, err := tka.Open(s.tkaStorage)
if err != nil {
go panic(fmt.Sprintf("serveTKASyncOffer: tka.Open: %v", err))
}
err = tkatest.HandleTKASyncOffer(w, r, authority, s.tkaStorage)
if err != nil {
go panic(fmt.Sprintf("HandleTKASyncOffer: %v", err))
}
}
func (s *Server) serveTKASign(w http.ResponseWriter, r *http.Request) {
s.mu.Lock()
defer s.mu.Unlock()
authority, err := tka.Open(s.tkaStorage)
if err != nil {
go panic(fmt.Sprintf("serveTKASign: tka.Open: %v", err))
}
sig, keyBeingSigned, err := tkatest.HandleTKASign(w, r, authority)
if err != nil {
go panic(fmt.Sprintf("HandleTKASign: %v", err))
}
s.nodes[*keyBeingSigned].KeySignature = *sig
s.updateLocked("TKASign", s.nodeIDsLocked(0))
}
// updateType indicates why a long-polling map request is being woken
// up for an update.
type updateType int
const (
// updatePeerChanged is an update that a peer has changed.
updatePeerChanged updateType = iota + 1
// updateSelfChanged is an update that the node changed itself
// via a lite endpoint update. These ones are never dup-suppressed,
// as the client is expecting an answer regardless.
updateSelfChanged
// updateDebugInjection is an update used for PingRequests
// or a raw MapResponse.
updateDebugInjection
)
func (s *Server) updateLocked(source string, peers []tailcfg.NodeID) {
for _, peer := range peers {
sendUpdate(s.updates[peer], updatePeerChanged)
}
}
// sendUpdate sends updateType to dst if dst is non-nil and
// has capacity. It reports whether a value was sent.
func sendUpdate(dst chan<- updateType, updateType updateType) bool {
if dst == nil {
return false
}
// The dst channel has a buffer size of 1.
// If we fail to insert an update into the buffer that
// means there is already an update pending.
select {
case dst <- updateType:
return true
default:
return false
}
}
func (s *Server) updateNodeLocked(n *tailcfg.Node) (peersToUpdate []tailcfg.NodeID) {
if n.Key.IsZero() {
panic("zero nodekey")
}
s.nodes[n.Key] = n.Clone()
return s.nodeIDsLocked(n.ID)
}
// UpdateNode updates or adds the input node, then triggers a netmap update for
// all attached streaming clients.
func (s *Server) UpdateNode(n *tailcfg.Node) {
s.mu.Lock()
defer s.mu.Unlock()
s.updateNodeLocked(n)
s.updateLocked("UpdateNode", s.nodeIDsLocked(0))
}
func (s *Server) incrInServeMap(delta int) {
s.mu.Lock()
defer s.mu.Unlock()
s.inServeMap += delta
}
// handleTagTransitionLocked models the production control server's handling
// of tag changes requested via Hostinfo.RequestTags in non-streaming map
// requests (see updateTags in the control server). A request whose
// RequestTags differ from both the node's stored Hostinfo's RequestTags and
// the node's current tags is a tag transition request. If the transition is
// permitted by s.TagOwners, the node is retagged; otherwise its node key is
// expired to force reauthentication.
//
// hi is the Hostinfo from the incoming request; node.Hostinfo is the
// previously stored one. s.mu must be held.
func (s *Server) handleTagTransitionLocked(node *tailcfg.Node, hi tailcfg.HostinfoView) {
var oldReqTags []string
if node.Hostinfo.Valid() {
oldReqTags = node.Hostinfo.RequestTags().AsSlice()
}
newReqTags := hi.RequestTags().AsSlice()
if tagsEqualAnyOrder(oldReqTags, newReqTags) || tagsEqualAnyOrder(newReqTags, node.Tags) {
return
}
if s.validTagTransition(node.Tags, newReqTags) {
s.logf("testcontrol: retagging node %v: %v -> %v", node.ID, node.Tags, newReqTags)
node.Tags = newReqTags
} else {
s.logf("testcontrol: invalid tag transition %v -> %v for node %v; expiring its node key", node.Tags, newReqTags, node.ID)
node.KeyExpiry = time.Now().Add(-time.Minute)
}
}
// validTagTransition reports whether a node currently tagged cur may retag
// itself as want, per s.TagOwners: every requested tag must be owned by one
// of the node's current tags. An untagged node may claim any defined tag,
// standing in for the production server's checks against the requesting
// user, which this server doesn't model. Removing all tags is not allowed,
// matching the production server.
func (s *Server) validTagTransition(cur, want []string) bool {
if len(want) == 0 {
return len(cur) == 0
}
for _, tag := range want {
owners, ok := s.TagOwners[tag]
if !ok {
return false
}
if len(cur) == 0 {
continue
}
owned := slices.ContainsFunc(cur, func(c string) bool { return slices.Contains(owners, c) })
if !owned {
return false
}
}
return true
}
// tagsEqualAnyOrder reports whether a and b contain the same tags, ignoring order.
func tagsEqualAnyOrder(a, b []string) bool {
if len(a) != len(b) {
return false
}
as, bs := slices.Clone(a), slices.Clone(b)
slices.Sort(as)
slices.Sort(bs)
return slices.Equal(as, bs)
}
// InServeMap returns the number of clients currently in a MapRequest HTTP handler.
func (s *Server) InServeMap() int {
s.mu.Lock()
defer s.mu.Unlock()
return s.inServeMap
}
func (s *Server) serveMap(w http.ResponseWriter, r *http.Request, mkey key.MachinePublic) {
s.incrInServeMap(1)
defer s.incrInServeMap(-1)
ctx := r.Context()
msg, err := io.ReadAll(io.LimitReader(r.Body, msgLimit))
if err != nil {
r.Body.Close()
http.Error(w, fmt.Sprintf("bad map request read: %v", err), 400)
return
}
r.Body.Close()
req := new(tailcfg.MapRequest)
if err := s.decode(msg, req); err != nil {
go panic(fmt.Sprintf("bad map request: %v", err))
}
s.mu.Lock()
if s.onMapRequest != nil {
s.onMapRequest(req.NodeKey)
}
s.mu.Unlock()
if s.HoldMapRequest != nil {
if done := s.HoldMapRequest(req); done != nil {
defer done()
}
}
if s.AltMapStream != nil {
// The caller takes over the stream entirely; it must handle
// keeping the HTTP response alive until ctx is done.
compress := req.Compress != ""
w.WriteHeader(200)
s.AltMapStream(ctx, &mapStreamSender{s: s, w: w, compress: compress}, req)
return
}
jitter := rand.N(8 * time.Second)
keepAlive := 50*time.Second + jitter
node := s.Node(req.NodeKey)
if node == nil {
http.Error(w, "node not found", 400)
return
}
if node.Machine != mkey {
http.Error(w, "node doesn't match machine key", 400)
return
}
// Per tailcfg.MapRequest.Stream docs: if Stream is true and Version >= 68,
// the server must treat this as read-only and ignore Hostinfo, Endpoints,
// DiscoKey, etc. — modern clients send those via a separate non-streaming
// POST /machine/map from a dedicated updateRoutine, not piggybacked on the
// streaming poll. Without this, the streaming MapRequest's zero-valued
// DiscoKey/Endpoints clobber whatever was just pushed out-of-band.
streamingNonUpdate := req.Stream && req.Version >= 68
var peersToUpdate []tailcfg.NodeID
if !req.ReadOnly && !streamingNonUpdate {
if ctx.Err() != nil {
// The client canceled the request (say, its control client
// was shut down mid-request when "tailscale up" or a
// profile switch created a new one), so its contents may
// predate newer requests that were already processed.
// Don't apply its Hostinfo/endpoints; they may be stale.
s.logf("testcontrol: dropping canceled map update from %v", req.NodeKey.ShortString())
http.Error(w, "request canceled", 400)
return
}
endpoints := filterInvalidIPv6Endpoints(req.Endpoints)
var hi tailcfg.HostinfoView
var newDERP tailcfg.DERPRegionID
if req.Hostinfo != nil {
hi = req.Hostinfo.View()
if ni := hi.NetInfo(); ni.Valid() {
newDERP = ni.PreferredDERP()
}
}
// Mutate the live node under the mutex; writing back the clone
// obtained above would clobber any concurrent writer's changes
// to other fields (e.g. UpdateNode, SetNodeCapMap).
s.mu.Lock()
live := s.nodes[req.NodeKey]
if live != nil {
live.Endpoints = endpoints
live.DiscoKey = req.DiscoKey
live.Cap = req.Version
if hi.Valid() {
if s.TagOwners != nil {
s.handleTagTransitionLocked(live, hi)
}
live.Hostinfo = hi
if newDERP != 0 {
live.HomeDERP = newDERP
}
}
peersToUpdate = s.nodeIDsLocked(live.ID)
}
s.mu.Unlock()
}
nodeID := node.ID
s.mu.Lock()
updatesCh := make(chan updateType, 1)
oldUpdatesCh := s.updates[nodeID]
if breakSameNodeMapResponseStreams(req) {
if oldUpdatesCh != nil {
close(oldUpdatesCh)
}
if s.updates == nil {
s.updates = map[tailcfg.NodeID]chan updateType{}
}
s.updates[nodeID] = updatesCh
} else {
sendUpdate(oldUpdatesCh, updateSelfChanged)
}
s.updateLocked("serveMap", peersToUpdate)
s.condLocked().Broadcast()
s.mu.Unlock()
// ReadOnly implies no streaming, as it doesn't
// register an updatesCh to get updates.
streaming := req.Stream && !req.ReadOnly
compress := req.Compress != ""
first := true
w.WriteHeader(200)
for {
// Only send raw map responses to the streaming poll, to avoid a
// non-streaming map request beating the streaming poll in a race and
// potentially dropping the map response.
if streaming {
if resBytes, ok := s.takeRawMapMessage(req.NodeKey); ok {
if err := s.sendMapMsg(w, compress, resBytes); err != nil {
s.logf("sendMapMsg of raw message: %v", err)
return
}
continue
}
}
if s.canGenerateAutomaticMapResponseFor(req.NodeKey) {
res, err := s.MapResponse(req)
if err != nil {
// TODO: log
return
}
if res == nil {
return // done
}
s.mu.Lock()
allExpired := s.allExpired
s.mu.Unlock()
if allExpired {
res.Node.KeyExpiry = time.Now().Add(-1 * time.Minute)
}
if f := s.ModifyFirstMapResponse; first && f != nil {
first = false
f(res, req)
}
// TODO: add minner if/when needed
resBytes, err := json.Marshal(res)
if err != nil {
s.logf("json.Marshal: %v", err)
return
}
if err := s.sendMapMsg(w, compress, resBytes); err != nil {
return
}
}
if !streaming {
return
}
if s.hasPendingRawMapMessage(req.NodeKey) {
continue
}
keepAliveLoop:
for {
var keepAliveTimer *time.Timer
var keepAliveTimerCh <-chan time.Time
if keepAlive > 0 {
keepAliveTimer = time.NewTimer(keepAlive)
keepAliveTimerCh = keepAliveTimer.C
}
select {
case <-ctx.Done():
if keepAliveTimer != nil {
keepAliveTimer.Stop()
}
return
case _, ok := <-updatesCh:
if !ok {
// replaced by new poll request
return
}
break keepAliveLoop
case <-keepAliveTimerCh:
if err := s.sendMapMsg(w, compress, keepAliveMsg); err != nil {
return
}
}
}
}
}
var keepAliveMsg = &struct {
KeepAlive bool
}{
KeepAlive: true,
}
func packetFilterWithIngress(addRelayCaps bool, allowSrcs []string) []tailcfg.FilterRule {
out := slices.Clone(tailcfg.FilterAllowAll)
out[0].SrcIPs = allowSrcs
caps := []peercap.Cap{
peercap.Ingress,
}
if addRelayCaps {
caps = append(caps, peercap.Relay)
caps = append(caps, peercap.RelayTarget)
}
out = append(out, tailcfg.FilterRule{
SrcIPs: []string{"*"},
CapGrant: []tailcfg.CapGrant{
{
Dsts: []netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6()},
Caps: caps,
},
},
})
return out
}
// MapResponse generates a MapResponse for a MapRequest.
//
// No updates to s are done here.
func (s *Server) MapResponse(req *tailcfg.MapRequest) (res *tailcfg.MapResponse, err error) {
nk := req.NodeKey
node := s.Node(nk)
if node == nil {
// node key rotated away (once test server supports that)
return nil, nil
}
s.mu.Lock()
nodeCapMap := maps.Clone(s.nodeCapMaps[nk])
unsignedNodes := maps.Clone(s.nodeUnsignedPeerAPIOnly)
var dns *tailcfg.DNSConfig
if s.DNSConfig != nil {
dns = s.DNSConfig.Clone()
}
magicDNSDomain := s.MagicDNSDomain
sshPolicy := s.SSHPolicy.Clone()
s.mu.Unlock()
node.CapMap = nodeCapMap
node.Capabilities = append(node.Capabilities, nodecap.DisableUPnP)
if sshPolicy != nil {
mak.Set(&node.CapMap, nodecap.SSH, nil)
}
t := time.Date(2020, 8, 3, 0, 0, 0, 1, time.UTC)
if dns != nil && magicDNSDomain != "" {
dns.CertDomains = append(dns.CertDomains, node.Hostinfo.Hostname()+"."+magicDNSDomain)
}
// Real control never includes unsigned (UnsignedPeerAPIOnly) peers as
// sources in filter rules that permit traffic; clients treat such a
// packet filter as invalid and discard it entirely. When any unsigned
// nodes exist, enumerate the signed nodes' addresses instead of using
// a wildcard.
allowSrcs := []string{"*"}
if len(unsignedNodes) > 0 {
allowSrcs = nil
for _, n := range s.AllNodes() {
if unsignedNodes[n.Key] {
continue
}
for _, a := range n.Addresses {
allowSrcs = append(allowSrcs, a.String())
}
}
}
res = &tailcfg.MapResponse{
Node: node,
DERPMap: s.DERPMap,
Domain: domain,
CollectServices: cmp.Or(s.CollectServices, opt.True),
PacketFilter: packetFilterWithIngress(s.PeerRelayGrants, allowSrcs),
DNSConfig: dns,
SSHPolicy: sshPolicy,
ControlTime: &t,
}
s.mu.Lock()
nodeMasqs := s.masquerades[node.Key]
jailed := maps.Clone(s.peerIsJailed[node.Key])
globalAppCaps := s.globalAppCaps
s.mu.Unlock()
for _, p := range s.AllNodes() {
if p.StableID == node.StableID {
continue
}
if masqIP := nodeMasqs[p.Key]; masqIP.IsValid() {
if masqIP.Is6() {
p.SelfNodeV6MasqAddrForThisPeer = new(masqIP)
} else {
p.SelfNodeV4MasqAddrForThisPeer = new(masqIP)
}
}
p.IsJailed = jailed[p.Key]
p.UnsignedPeerAPIOnly = unsignedNodes[p.Key]
s.mu.Lock()
peerAddress := s.masquerades[p.Key][node.Key]
routes := s.nodeSubnetRoutes[p.Key]
peerCapMap := maps.Clone(s.nodeCapMaps[p.Key])
s.mu.Unlock()
if peerCapMap != nil {
p.CapMap = peerCapMap
}
if peerAddress.IsValid() {
if peerAddress.Is6() {
p.Addresses[1] = netip.PrefixFrom(peerAddress, peerAddress.BitLen())
p.AllowedIPs[1] = netip.PrefixFrom(peerAddress, peerAddress.BitLen())
} else {
p.Addresses[0] = netip.PrefixFrom(peerAddress, peerAddress.BitLen())
p.AllowedIPs[0] = netip.PrefixFrom(peerAddress, peerAddress.BitLen())
}
}
if len(routes) > 0 {
p.PrimaryRoutes = routes
p.AllowedIPs = append(p.AllowedIPs, routes...)
}
if s.AllOnline {
p.Online = new(true)
}
res.Peers = append(res.Peers, p)
}
sort.Slice(res.Peers, func(i, j int) bool {
return res.Peers[i].ID < res.Peers[j].ID
})
res.UserProfiles = s.allUserProfiles()
v4Prefix := netip.PrefixFrom(netaddr.IPv4(100, 64, uint8(node.ID>>8), uint8(node.ID)), 32)
v6Prefix := netip.PrefixFrom(tsaddr.Tailscale4To6(v4Prefix.Addr()), 128)
res.Node.Addresses = []netip.Prefix{
v4Prefix,
v6Prefix,
}
if globalAppCaps != nil {
res.PacketFilter = append(res.PacketFilter, tailcfg.FilterRule{
SrcIPs: []string{"*"},
CapGrant: []tailcfg.CapGrant{
{
Dsts: []netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6()},
CapMap: globalAppCaps,
},
},
})
}
// If the server is tracking TKA state, and there's a single TKA head,
// add it to the MapResponse.
if s.tkaStorage != nil {
heads, err := s.tkaStorage.Heads()
if err != nil {
log.Printf("unable to get TKA heads: %v", err)
} else if len(heads) != 1 {
log.Printf("unable to get single TKA head, got %v", heads)
} else {
res.TKAInfo = &tailcfg.TKAInfo{
Head: heads[0].Hash().String(),
}
}
}
s.mu.Lock()
defer s.mu.Unlock()
res.Node.PrimaryRoutes = s.nodeSubnetRoutes[nk]
res.Node.AllowedIPs = append(res.Node.Addresses, s.nodeSubnetRoutes[nk]...)
// Consume a PingRequest at the head of the queue, if any.
if q := s.msgToSend[nk]; len(q) > 0 {
if pr, ok := q[0].(*tailcfg.PingRequest); ok {
res.PingRequest = pr
s.popMsgToSendLocked(nk)
}
}
return res, nil
}
// popMsgToSendLocked pops the head of the per-node message queue.
// s.mu must be held.
func (s *Server) popMsgToSendLocked(nk key.NodePublic) {
q := s.msgToSend[nk]
if len(q) <= 1 {
delete(s.msgToSend, nk)
return
}
// Zero the head to allow GC of any large referenced response.
q[0] = nil
s.msgToSend[nk] = q[1:]
}
func (s *Server) canGenerateAutomaticMapResponseFor(nk key.NodePublic) bool {
s.mu.Lock()
defer s.mu.Unlock()
return !s.suppressAutoMapResponses.Contains(nk)
}
func (s *Server) hasPendingRawMapMessage(nk key.NodePublic) bool {
s.mu.Lock()
defer s.mu.Unlock()
return len(s.msgToSend[nk]) > 0
}
func (s *Server) takeRawMapMessage(nk key.NodePublic) (mapResJSON []byte, ok bool) {
s.mu.Lock()
defer s.mu.Unlock()
q := s.msgToSend[nk]
if len(q) == 0 {
return nil, false
}
mr := q[0]
s.popMsgToSendLocked(nk)
// If it's a bare PingRequest, wrap it in a MapResponse.
if pr, ok := mr.(*tailcfg.PingRequest); ok {
mr = &tailcfg.MapResponse{PingRequest: pr}
}
var err error
mapResJSON, err = json.Marshal(mr)
if err != nil {
panic(err)
}
return mapResJSON, true
}
// AltMapStreamFunc is the type of [Server.AltMapStream]: a callback that
// takes over the serveMap handler entirely. The callback hand-builds and
// sends MapResponses via the provided [MapStreamWriter] and is responsible
// for keeping the stream alive until ctx is done. When set, the normal
// per-node map-stream state machine in serveMap is bypassed.
//
// The callback is invoked for every map long-poll, including the
// non-streaming "lite" polls controlclient issues to push HostInfo updates
// (req.Stream == false). Implementations that only care about the streaming
// long-poll typically respond to non-streaming polls with an empty
// MapResponse and return immediately.
//
// This hook is for benchmarks and stress tests that need to drive clients
// with a controlled sequence of responses.
type AltMapStreamFunc func(ctx context.Context, w MapStreamWriter, req *tailcfg.MapRequest)
// MapStreamWriter is the interface passed to an [AltMapStreamFunc],
// letting the callback write framed MapResponse messages directly onto the
// long-poll HTTP response.
type MapStreamWriter interface {
// SendMapMessage encodes and writes msg as a single framed
// MapResponse on the stream. It respects the client's Compress flag
// (captured when the stream started).
SendMapMessage(msg *tailcfg.MapResponse) error
}
// mapStreamSender implements [MapStreamWriter] for [Server.AltMapStream]
// callbacks.
type mapStreamSender struct {
s *Server
w http.ResponseWriter
compress bool
}
func (m *mapStreamSender) SendMapMessage(msg *tailcfg.MapResponse) error {
return m.s.sendMapMsg(m.w, m.compress, msg)
}
func (s *Server) sendMapMsg(w http.ResponseWriter, compress bool, msg any) error {
resBytes, err := s.encode(compress, msg)
if err != nil {
return err
}
const maxMapSize = 256 << 20 // 256MB
if len(resBytes) > maxMapSize {
return fmt.Errorf("map message too big: %d", len(resBytes))
}
var siz [4]byte
binary.LittleEndian.PutUint32(siz[:], uint32(len(resBytes)))
if _, err := w.Write(siz[:]); err != nil {
return err
}
if _, err := w.Write(resBytes); err != nil {
return err
}
if f, ok := w.(http.Flusher); ok {
f.Flush()
} else {
s.logf("[unexpected] ResponseWriter %T is not a Flusher", w)
}
return nil
}
func (s *Server) decode(msg []byte, v any) error {
if len(msg) == msgLimit {
return errors.New("encrypted message too long")
}
return json.Unmarshal(msg, v)
}
func (s *Server) encode(compress bool, v any) (b []byte, err error) {
var isBytes bool
if b, isBytes = v.([]byte); !isBytes {
b, err = json.Marshal(v)
if err != nil {
return nil, err
}
}
if compress {
b = zstdframe.AppendEncode(nil, b, zstdframe.FastestCompression)
}
return b, nil
}
// SetOnMapRequest sets callback used for testing when a new mapRequest happens.
// Pass nil to remove the callback.
func (s *Server) SetOnMapRequest(f func(key.NodePublic)) {
s.mu.Lock()
defer s.mu.Unlock()
s.onMapRequest = f
}
// filterInvalidIPv6Endpoints removes invalid IPv6 endpoints from eps,
// modify the slice in place, returning the potentially smaller subset (aliasing
// the original memory).
//
// Two types of IPv6 endpoints are considered invalid: link-local
// addresses, and anything with a zone.
func filterInvalidIPv6Endpoints(eps []netip.AddrPort) []netip.AddrPort {
clean := eps[:0]
for _, ep := range eps {
if keepClientEndpoint(ep) {
clean = append(clean, ep)
}
}
return clean
}
func keepClientEndpoint(ipp netip.AddrPort) bool {
ip := ipp.Addr()
if ip.Zone() != "" {
return false
}
if ip.Is6() && ip.IsLinkLocalUnicast() {
// We let clients send these for now, but
// tailscaled doesn't know how to use them yet
// so we filter them out for now. A future
// MapRequest.Version might signal that
// clients know how to use them (e.g. try all
// local scopes).
return false
}
return true
}
// breakSameNodeMapResponseStreams reports whether req should break a
// prior long-polling MapResponse stream (if active) from the same
// node ID.
func breakSameNodeMapResponseStreams(req *tailcfg.MapRequest) bool {
if req.ReadOnly {
// Don't register our updatesCh for closability
// nor close another peer's if we're a read-only request.
return false
}
if !req.Stream && req.OmitPeers {
// Likewise, if we're not streaming and not asking for peers,
// (but still mutable, without Readonly set), consider this an endpoint
// update request only, and don't close any existing map response
// for this nodeID. It's likely the same client with a built-up
// compression context. We want to let them update their
// new endpoints with us without breaking that other long-running
// map response.
return false
}
return true
}
// RejectRequestForPath returns a request rejection for requests matching
// the given path.
func RejectRequestForPath(path string, fn func() (status int, retryAfter string, msg string)) func(*http.Request) (status int, retryAfter string, msg string) {
return func(r *http.Request) (status int, retryAfter string, msg string) {
if r.URL.Path != path {
return 0, "", ""
}
return fn()
}
}
// 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 tkatest has functions for creating a mock control server that
// responds to TKA endpoints.
package tkatest
import (
"encoding/json"
"errors"
"fmt"
"iter"
"log"
"net/http"
"tailscale.com/tailcfg"
"tailscale.com/tka"
"tailscale.com/types/key"
"tailscale.com/types/tkatype"
)
func serverError(w http.ResponseWriter, format string, a ...any) error {
err := fmt.Sprintf(format, a...)
http.Error(w, err, 500)
log.Printf("returning HTTP 500 error: %v", err)
return errors.New(err)
}
func userError(w http.ResponseWriter, format string, a ...any) error {
err := fmt.Sprintf(format, a...)
http.Error(w, err, 400)
return errors.New(err)
}
// HandleTKAInitBegin handles a request to /machine/tka/init/begin.
//
// If the request contains a valid genesis AUM, it sends a response to the
// client, and returns the AUM to the caller.
func HandleTKAInitBegin(w http.ResponseWriter, r *http.Request, nodes iter.Seq[*tailcfg.Node]) (*tka.AUM, error) {
var req *tailcfg.TKAInitBeginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, userError(w, "Decode: %v", err)
}
var aum tka.AUM
if err := aum.Unserialize(req.GenesisAUM); err != nil {
return nil, userError(w, "invalid genesis AUM: %v", err)
}
beginResp := tailcfg.TKAInitBeginResponse{}
for n := range nodes {
beginResp.NeedSignatures = append(
beginResp.NeedSignatures,
tailcfg.TKASignInfo{
NodeID: n.ID,
NodePublic: n.Key,
},
)
}
w.WriteHeader(200)
if err := json.NewEncoder(w).Encode(beginResp); err != nil {
return nil, serverError(w, "Encode: %v", err)
}
return &aum, nil
}
// HandleTKAInitFinish handles a request to /machine/tka/init/finish.
//
// It sends a response to the client, and gives the caller a list of node
// signatures to apply.
//
// This method assumes that the node signatures are valid, and does not
// verify them with the supplied public key.
func HandleTKAInitFinish(w http.ResponseWriter, r *http.Request) (map[tailcfg.NodeID]tkatype.MarshaledSignature, error) {
var req *tailcfg.TKAInitFinishRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, userError(w, "Decode: %v", err)
}
w.WriteHeader(200)
w.Write([]byte("{}"))
return req.Signatures, nil
}
// HandleTKABootstrap handles a request to /tka/bootstrap.
//
// If the request is valid, it sends a response to the client, and returns
// the parsed request to the caller.
func HandleTKABootstrap(w http.ResponseWriter, r *http.Request, resp tailcfg.TKABootstrapResponse) (*tailcfg.TKABootstrapRequest, error) {
req := new(tailcfg.TKABootstrapRequest)
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
return nil, userError(w, "Decode: %v", err)
}
if req.Version != tailcfg.CurrentCapabilityVersion {
return nil, userError(w, "bootstrap CapVer = %v, want %v", req.Version, tailcfg.CurrentCapabilityVersion)
}
w.WriteHeader(200)
if err := json.NewEncoder(w).Encode(resp); err != nil {
return nil, serverError(w, "Encode: %v", err)
}
return req, nil
}
func HandleTKASyncOffer(w http.ResponseWriter, r *http.Request, authority *tka.Authority, chonk tka.Chonk) error {
body := new(tailcfg.TKASyncOfferRequest)
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
return userError(w, "Decode: %v", err)
}
log.Printf("got sync offer:\n%+v", body)
nodeOffer, err := tka.ToSyncOffer(body.Head, body.Ancestors)
if err != nil {
return userError(w, "ToSyncOffer: %v", err)
}
controlOffer, err := authority.SyncOffer(chonk)
if err != nil {
return serverError(w, "authority.SyncOffer: %v", err)
}
sendAUMs, err := authority.MissingAUMs(chonk, nodeOffer)
if err != nil {
return serverError(w, "authority.MissingAUMs: %v", err)
}
head, ancestors, err := tka.FromSyncOffer(controlOffer)
if err != nil {
return serverError(w, "FromSyncOffer: %v", err)
}
resp := tailcfg.TKASyncOfferResponse{
Head: head,
Ancestors: ancestors,
MissingAUMs: make([]tkatype.MarshaledAUM, len(sendAUMs)),
}
for i, a := range sendAUMs {
resp.MissingAUMs[i] = a.Serialize()
}
log.Printf("responding to sync offer with:\n%+v", resp)
w.WriteHeader(200)
if err := json.NewEncoder(w).Encode(resp); err != nil {
return serverError(w, "Encode: %v", err)
}
return nil
}
// HandleTKASign handles a request to /machine/tka/sign.
//
// If the signature request is valid, it sends a response to the client, and
// gives the caller the signature and public key of the node being signed.
func HandleTKASign(w http.ResponseWriter, r *http.Request, authority *tka.Authority) (*tkatype.MarshaledSignature, *key.NodePublic, error) {
req := new(tailcfg.TKASubmitSignatureRequest)
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
return nil, nil, userError(w, "Decode: %v", err)
}
if req.Version != tailcfg.CurrentCapabilityVersion {
return nil, nil, userError(w, "sign CapVer = %v, want %v", req.Version, tailcfg.CurrentCapabilityVersion)
}
var sig tka.NodeKeySignature
if err := sig.Unserialize(req.Signature); err != nil {
return nil, nil, userError(w, "malformed signature: %v", err)
}
var keyBeingSigned key.NodePublic
if err := keyBeingSigned.UnmarshalBinary(sig.Pubkey); err != nil {
return nil, nil, userError(w, "malformed signature pubkey: %v", err)
}
if err := authority.NodeKeyAuthorized(keyBeingSigned, req.Signature); err != nil {
return nil, nil, userError(w, "signature does not verify: %v", err)
}
w.WriteHeader(200)
if err := json.NewEncoder(w).Encode(tailcfg.TKASubmitSignatureResponse{}); err != nil {
return nil, nil, serverError(w, "Encode: %v", err)
}
return &req.Signature, &keyBeingSigned, nil
}
// HandleTKASyncSend handles a request to /machine/tka/send.
//
// If the request is valid, it adds the new AUMs to the authority, and sends
// a response to the client with the new head.
func HandleTKASyncSend(w http.ResponseWriter, r *http.Request, authority *tka.Authority, chonk tka.Chonk) error {
body := new(tailcfg.TKASyncSendRequest)
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
return userError(w, "Decode: %v", err)
}
log.Printf("got sync send:\n%+v", body)
var remoteHead tka.AUMHash
if err := remoteHead.UnmarshalText([]byte(body.Head)); err != nil {
return userError(w, "head unmarshal: %v", err)
}
toApply := make([]tka.AUM, len(body.MissingAUMs))
for i, a := range body.MissingAUMs {
if err := toApply[i].Unserialize(a); err != nil {
return userError(w, "decoding missingAUM[%d]: %v", i, err)
}
}
if len(toApply) > 0 {
if err := authority.Inform(chonk, toApply); err != nil {
return serverError(w, "control.Inform(%+v) failed: %v", toApply, err)
}
}
head, err := authority.Head().MarshalText()
if err != nil {
return serverError(w, "head marshal: %v", err)
}
resp := tailcfg.TKASyncSendResponse{
Head: string(head),
}
w.WriteHeader(200)
if err := json.NewEncoder(w).Encode(resp); err != nil {
return serverError(w, "Encode: %v", err)
}
return nil
}
// 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)]
// hookRuntimeMetrics registers the Go runtime/metrics debug page on a
// DebugHandler. It is set by runtimemetrics.go, which is excluded from
// js/wasm builds to keep them small.
var hookRuntimeMetrics 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 f, ok := hookRuntimeMetrics.GetOk(); ok {
f(ret)
}
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
//go:build !js && !wasm
package tsweb
import (
"bufio"
"encoding/json"
"fmt"
"html"
"io"
"math"
"net/http"
"net/url"
"runtime/metrics"
"slices"
"strconv"
"strings"
"sync"
)
// runtimeMetricsDescs returns the descriptions of all supported Go
// runtime/metrics, sorted by name.
var runtimeMetricsDescs = sync.OnceValue(func() []metrics.Description {
descs := metrics.All()
slices.SortFunc(descs, func(a, b metrics.Description) int {
return strings.Compare(a.Name, b.Name)
})
return descs
})
// runtimeMetricsSlug is the path under /debug/ where
// runtimeMetricsHandler is registered, both as an exact path and as a
// subtree (with a trailing slash).
const runtimeMetricsSlug = "runtime-metrics"
// runtimeMetricsPath is the full path of the runtime/metrics debug page.
const runtimeMetricsPath = "/debug/" + runtimeMetricsSlug
func init() {
hookRuntimeMetrics.Set(addRuntimeMetricsHandlers)
}
// addRuntimeMetricsHandlers registers runtimeMetricsHandler on d, both
// at its base path and as a subtree for the per-metric path form.
func addRuntimeMetricsHandlers(d *DebugHandler) {
d.Handle(runtimeMetricsSlug, "Metrics (Go runtime/metrics)", http.HandlerFunc(runtimeMetricsHandler))
d.HandleSilent(runtimeMetricsSlug+"/", http.HandlerFunc(runtimeMetricsHandler))
}
// runtimeMetricsHandler serves Go runtime/metrics.
//
// At its base path without a "name" query parameter it serves an index
// of all supported metric names and their descriptions, without reading
// any values. The index is HTML for browsers (requests whose Accept
// header includes text/html) and plain text otherwise; the "format"
// query parameter ("html" or "text") overrides that choice.
//
// With one or more "name" query parameters it reads the named metrics
// and returns them as a JSON object keyed by metric name. A name
// ending in "*" matches all metrics with that prefix, so "name=*"
// returns everything and "name=/gc/*" returns the GC metrics.
//
// A metric name may also be appended to the base path, as in
// /debug/runtime-metrics/gc/heap/allocs:bytes, in which case the
// response is that metric's bare JSON value without the object wrapper.
//
// Histograms are objects with "counts" and "buckets" arrays; infinite
// bucket boundaries are encoded as the strings "-Inf" and "+Inf".
func runtimeMetricsHandler(w http.ResponseWriter, r *http.Request) {
if rest, ok := strings.CutPrefix(r.URL.Path, runtimeMetricsPath+"/"); ok {
serveRuntimeMetricValue(w, "/"+rest)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if names := r.Form["name"]; len(names) > 0 {
serveRuntimeMetricsJSON(w, names)
return
}
format := r.FormValue("format")
if format == "" {
if strings.Contains(r.Header.Get("Accept"), "text/html") {
format = "html"
} else {
format = "text"
}
}
switch format {
case "html":
w.Header().Set("Content-Type", "text/html; charset=utf-8")
case "text":
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
default:
http.Error(w, "unknown format; want html or text", http.StatusBadRequest)
return
}
bw := bufio.NewWriter(w)
defer bw.Flush()
if format == "html" {
writeRuntimeMetricsIndexHTML(bw)
} else {
writeRuntimeMetricsIndexText(bw)
}
}
// serveRuntimeMetricsJSON reads the metrics matching names (exact
// names, or prefixes ending in "*") and writes them as a JSON object.
func serveRuntimeMetricsJSON(w http.ResponseWriter, names []string) {
descs := runtimeMetricsDescs()
var samples []metrics.Sample
seen := map[string]bool{}
add := func(name string) {
if !seen[name] {
seen[name] = true
samples = append(samples, metrics.Sample{Name: name})
}
}
for _, name := range names {
if prefix, ok := strings.CutSuffix(name, "*"); ok {
for _, d := range descs {
if strings.HasPrefix(d.Name, prefix) {
add(d.Name)
}
}
continue
}
if !runtimeMetricExists(name) {
http.Error(w, fmt.Sprintf("unknown runtime metric %q", name), http.StatusNotFound)
return
}
add(name)
}
metrics.Read(samples)
obj := make(map[string]any, len(samples))
for _, s := range samples {
obj[s.Name] = runtimeMetricValueJSON(s.Value)
}
writeJSON(w, obj)
}
// serveRuntimeMetricValue reads the single metric name and writes its
// bare JSON value.
func serveRuntimeMetricValue(w http.ResponseWriter, name string) {
if !runtimeMetricExists(name) {
http.Error(w, fmt.Sprintf("unknown runtime metric %q", name), http.StatusNotFound)
return
}
samples := []metrics.Sample{{Name: name}}
metrics.Read(samples)
writeJSON(w, runtimeMetricValueJSON(samples[0].Value))
}
func runtimeMetricExists(name string) bool {
return slices.ContainsFunc(runtimeMetricsDescs(), func(d metrics.Description) bool { return d.Name == name })
}
func writeJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json")
e := json.NewEncoder(w)
e.SetIndent("", "\t")
e.Encode(v)
}
// runtimeMetricValueJSON converts v to a value that encoding/json can
// encode. Infinite and NaN floats, which JSON cannot represent, become
// strings.
func runtimeMetricValueJSON(v metrics.Value) any {
switch v.Kind() {
case metrics.KindUint64:
return v.Uint64()
case metrics.KindFloat64:
return jsonFloat(v.Float64())
case metrics.KindFloat64Histogram:
h := v.Float64Histogram()
buckets := make([]any, len(h.Buckets))
for i, b := range h.Buckets {
buckets[i] = jsonFloat(b)
}
return map[string]any{
"counts": h.Counts,
"buckets": buckets,
}
default:
return nil
}
}
func jsonFloat(f float64) any {
if math.IsInf(f, 0) || math.IsNaN(f) {
return strconv.FormatFloat(f, 'g', -1, 64)
}
return f
}
func runtimeMetricKind(k metrics.ValueKind) string {
switch k {
case metrics.KindUint64:
return "uint64"
case metrics.KindFloat64:
return "float64"
case metrics.KindFloat64Histogram:
return "histogram"
default:
return fmt.Sprintf("kind(%d)", k)
}
}
func writeRuntimeMetricsIndexText(w io.Writer) {
fmt.Fprintf(w, "# Go runtime/metrics.\n")
fmt.Fprintf(w, "# Fetch one bare JSON value with %s/NAME, e.g. %s/gc/heap/allocs:bytes.\n", runtimeMetricsPath, runtimeMetricsPath)
fmt.Fprintf(w, "# Fetch a JSON object of values with %s?name=NAME (repeatable).\n", runtimeMetricsPath)
fmt.Fprintf(w, "# A trailing * on a name matches by prefix; name=* returns all values.\n\n")
for _, d := range runtimeMetricsDescs() {
fmt.Fprintf(w, "%s\n", d.Name)
for line := range strings.Lines(d.Description) {
fmt.Fprintf(w, " # %s\n", strings.TrimRight(line, "\n"))
}
fmt.Fprintf(w, " # %s", runtimeMetricKind(d.Kind))
if d.Cumulative {
fmt.Fprintf(w, ", cumulative")
}
fmt.Fprintf(w, "\n\n")
}
}
func writeRuntimeMetricsIndexHTML(w io.Writer) {
// valueURL returns the URL serving name's value. Exact metric names
// are appended to the path and yield a bare JSON value. Wildcards
// use the name query parameter and yield a JSON object.
valueURL := func(name string) string {
if strings.HasSuffix(name, "*") {
return runtimeMetricsPath + "?name=" + url.QueryEscape(name)
}
return runtimeMetricsPath + name
}
fmt.Fprintf(w, `<html><head><title>runtime/metrics</title>
<style>
body { font-family: monospace; }
table { border-collapse: collapse; }
td, th { border: 1px solid #ccc; padding: 2px 6px; vertical-align: top; text-align: left; }
.desc { color: #555; }
tr.wild td { background: #eee; }
</style></head><body>
<h1>Go runtime/metrics</h1>
<p>See <a href="https://pkg.go.dev/runtime/metrics">runtime/metrics</a>.
Click a name to read its bare value as JSON. Values are not read for this index.
Append <code>?format=text</code> for a plain text index.
Fetch a JSON object of several values with repeated <code>?name=</code> parameters; a trailing
<code>*</code> matches by prefix (<a href="%s">all values</a>, <a href="%s">GC values</a>).</p>
<table>
<tr><th>Name</th><th>Kind</th><th>Description</th></tr>
`, valueURL("*"), valueURL("/gc/*"))
var lastDir string
for _, d := range runtimeMetricsDescs() {
// Before each run of metrics sharing a directory, emit a row
// linking each ancestor directory to its wildcard query, so
// "/gc/heap/allocs:bytes" is preceded by links to "/gc/*" and
// "/gc/heap/*".
if dir := d.Name[:strings.LastIndex(d.Name, "/")+1]; dir != lastDir {
lastDir = dir
io.WriteString(w, `<tr class="wild"><td colspan="3">`)
for i := 1; i < len(dir); i++ {
if dir[i] != '/' {
continue
}
pat := dir[:i+1] + "*"
fmt.Fprintf(w, ` <a href="%s">%s</a>`, valueURL(pat), html.EscapeString(pat))
}
io.WriteString(w, "</td></tr>\n")
}
kind := runtimeMetricKind(d.Kind)
if d.Cumulative {
kind += ", cumulative"
}
fmt.Fprintf(w, `<tr><td><a href="%s">%s</a></td><td>%s</td><td class="desc">%s</td></tr>`+"\n",
valueURL(d.Name), html.EscapeString(d.Name), kind, html.EscapeString(d.Description))
}
io.WriteString(w, "</table></body></html>\n")
}
// 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()
}
// FlushError is the error-returning form of Flush that
// [http.ResponseController.Flush] prefers, so handlers can see write
// errors (such as an expired write deadline) that Flush would swallow.
func (lg *loggingResponseWriter) FlushError() error {
return http.NewResponseController(lg.ResponseWriter).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
import (
"io"
"math"
"runtime/metrics"
"strconv"
"strings"
"sync"
)
// runtimeMetricSpec describes one Go runtime/metrics metric that
// [Handler] exports in Prometheus format.
type runtimeMetricSpec struct {
// name is the runtime/metrics name, such as "/gc/heap/allocs:bytes".
name string
// typ is the Prometheus type: "gauge", "counter", or "histogram".
// Counters get a "_total" suffix, as in the Prometheus naming
// conventions.
typ string
// buckets says how a histogram's runtime buckets map to Prometheus
// buckets. It is required for histograms and ignored otherwise.
buckets histBuckets
// hidden means the metric is read (because it backs one of the
// legacy memstats_* metrics) but is not exported under its own
// go_runtime_ name.
hidden bool
}
// histBuckets says how the runtime's lower-bound-inclusive buckets
// [lo, hi) of a histogram become Prometheus's upper-bound-inclusive
// le buckets.
type histBuckets uint8
const (
// histTime is for the runtime's time histograms, which have
// hundreds of buckets. They are reduced as described at
// [runtimeHistMinBucket], and a runtime bucket [lo, hi) becomes
// le=hi, which is only wrong for a value exactly equal to hi.
histTime histBuckets = iota + 1
// histPowersOfTwo is for histograms whose observed values are all
// exactly equal to bucket lower bounds, such as goroutine stack
// sizes. A runtime bucket [lo, hi) becomes le=lo, so the labels
// name the values they count, and the runtime's final catch-all
// bucket is folded into +Inf.
histPowersOfTwo
// histInts is for histograms of small integer values, such as
// counts of GC cycles. A runtime bucket [lo, hi) becomes le=hi-1,
// which is exact for integers.
histInts
)
// runtimeMetricSpecs is the set of Go runtime/metrics metrics that
// [Handler] reads. Everything here is cheap for the runtime to read: no
// metric in this list stops the world or walks the heap or the
// goroutine list.
//
// The list is deliberately short. The hidden entries only back the
// legacy memstats_* metrics. The rest are either specific to the
// Tailscale fork of Go or have no runtime.MemStats equivalent and earn
// their place on a dashboard.
//
// Names that the running toolchain does not know (such as the
// /tailscale/ metrics when not built with the fork) are dropped on
// first use, so it is fine for this list to be a superset of what
// exists.
//
// Go's Cumulative metrics are declared as counters here and the rest
// as gauges; TestRuntimeMetricSpecs checks that against the
// runtime's own descriptions.
var runtimeMetricSpecs = []runtimeMetricSpec{
// These back the memstats_* metrics; see memstatsIndexes.
{name: "/memory/classes/heap/objects:bytes", typ: "gauge", hidden: true},
{name: "/gc/heap/allocs:bytes", typ: "counter", hidden: true},
{name: "/memory/classes/total:bytes", typ: "gauge", hidden: true},
{name: "/gc/heap/allocs:objects", typ: "counter", hidden: true},
{name: "/gc/heap/frees:objects", typ: "counter", hidden: true},
{name: "/gc/heap/tiny/allocs:objects", typ: "counter", hidden: true},
{name: "/gc/cycles/total:gc-cycles", typ: "counter", hidden: true},
{name: "/cpu/classes/gc/mark/idle:cpu-seconds", typ: "counter", hidden: true},
// GC CPU time as counters, so that rate() over a window works.
// memstats_gc_cpu_fraction, derived from these two (less the
// hidden idle mark time above), is a lifetime average and cannot
// show a recent change.
{name: "/cpu/classes/gc/total:cpu-seconds", typ: "counter"},
{name: "/cpu/classes/total:cpu-seconds", typ: "counter"},
// Live heap after the last GC. Unlike memstats_heap_alloc it
// excludes garbage not yet swept, so it is the number to compare
// against GOMEMLIMIT or a container memory limit.
{name: "/gc/heap/live:bytes", typ: "gauge"},
// How long runnable goroutines wait to be scheduled, and how long
// GC stop-the-world pauses take. Neither is visible any other way.
{name: "/sched/latencies:seconds", typ: "histogram", buckets: histTime},
{name: "/sched/pauses/total/gc:seconds", typ: "histogram", buckets: histTime},
// OS threads owned by the runtime, for spotting thread leaks from
// blocking syscalls or cgo.
{name: "/sched/threads/total:threads", typ: "gauge"},
// Time spent blocked on mutexes. cmd/derper used to export this
// itself as go_sync_mutex_wait_seconds.
{name: "/sync/mutex/wait/total:seconds", typ: "counter"},
// The /tailscale/ metrics exist only in the Tailscale fork of Go
// (github.com/tailscale/go), used when building with the
// tailscale_go build tag.
{name: "/tailscale/sched/goroutines-by-stack-size:bytes", typ: "histogram", buckets: histPowersOfTwo},
{name: "/tailscale/sched/stacks/copied:bytes", typ: "counter"},
{name: "/tailscale/sched/stacks/growths:events", typ: "counter"},
{name: "/tailscale/sched/stacks/shrinks:events", typ: "counter"},
{name: "/tailscale/sched/timers/tracked:timers", typ: "gauge"},
{name: "/tailscale/sched/timers/zombies/chan:timers", typ: "gauge"},
{name: "/tailscale/sched/timers/zombies/func:timers", typ: "gauge"},
{name: "/tailscale/sched/timers/zombies/func/created:timers", typ: "counter"},
{name: "/tailscale/sched/timers/zombies/func/removed:timers", typ: "counter"},
{name: "/tailscale/sched/timers/zombies/func/lifetime:gc-cycles", typ: "histogram", buckets: histInts},
{name: "/tailscale/sched/timers/zombies:timers", typ: "gauge"},
}
// runtimeMetricPrefix is the namespace prefix of exported runtime
// metrics. It is deliberately not "go_", which is the namespace used
// by the Prometheus Go client's collector; binaries that also use that
// client (see tailscale.com/tsweb/promvarz) would otherwise export
// some series twice, and Prometheus rejects a scrape containing
// duplicate series.
const runtimeMetricPrefix = "go_runtime_"
// runtimeMetricPromName converts a runtime/metrics name such as
// "/gc/heap/allocs:bytes" to a Prometheus metric name such as
// "go_runtime_gc_heap_allocs_bytes_total". The conversion follows the
// same rules as the Prometheus Go client's collector, other than the
// namespace: the path becomes the name with slashes and dashes
// replaced by underscores, the unit is appended, and counters get a
// "_total" suffix.
func runtimeMetricPromName(name, typ string) string {
path, unit, _ := strings.Cut(strings.TrimPrefix(name, "/"), ":")
r := strings.NewReplacer("/", "_", "-", "_")
unit = strings.ReplaceAll(unit, "/", "_per_")
unit = strings.ReplaceAll(unit, "*", "_")
s := runtimeMetricPrefix + r.Replace(path) + "_" + r.Replace(unit)
if typ == "counter" {
s += "_total"
}
return s
}
// Time histograms from the runtime have hundreds of buckets, four per
// power of two from 256ns (finer below that) to about a day. That is
// far too many series for Prometheus, so they are reduced to one
// bucket per factor of runtimeHistBucketFactor starting at
// runtimeHistMinBucket, and everything above runtimeHistMaxBucket is
// folded into the +Inf bucket. Because the runtime's bucket boundaries
// are at powers of two times small integers, a factor of 4 starting at
// 256ns lands exactly on runtime boundaries, so the reduction only
// merges runtime buckets and never splits one.
const (
runtimeHistMinBucket = 256e-9 // seconds
runtimeHistMaxBucket = 1.0 // seconds
runtimeHistBucketFactor = 4
)
// runtimeHistLayout describes how a runtime histogram's buckets are
// merged into the exported Prometheus buckets. It is computed once per
// metric from the runtime's bucket boundaries, which never change.
type runtimeHistLayout struct {
// lines[i] is the exported line prefix for output bucket i, up to
// and including the space before the value:
// `name_bucket{le="0.001"} `. The last entry is the +Inf bucket.
lines [][]byte
// ends[i] is the number of leading runtime bucket counts summed
// into output bucket i, which is cumulative like Prometheus
// buckets. The last entry, for +Inf, is the total number of
// runtime buckets.
ends []int
}
// newRuntimeHistLayout computes the export layout for a runtime
// histogram with the given bucket boundaries (as in
// [metrics.Float64Histogram.Buckets], so the runtime bucket i counts
// values in [buckets[i], buckets[i+1])).
func newRuntimeHistLayout(promName string, buckets []float64, kind histBuckets) *runtimeHistLayout {
numCounts := len(buckets) - 1
lay := new(runtimeHistLayout)
addBucket := func(le float64, end int) {
line := append([]byte(promName), `_bucket{le="`...)
if math.IsInf(le, 1) {
line = append(line, "+Inf"...)
} else {
line = strconv.AppendFloat(line, le, 'g', -1, 64)
}
line = append(line, `"} `...)
lay.lines = append(lay.lines, line)
lay.ends = append(lay.ends, end)
}
switch kind {
case histPowersOfTwo:
// All but the final catch-all runtime bucket, each labeled
// with its lower bound.
for i := 0; i < numCounts-1; i++ {
addBucket(buckets[i], i+1)
}
case histInts:
// All but the final catch-all runtime bucket, each labeled
// with the largest integer it contains.
for i := 1; i < numCounts; i++ {
addBucket(buckets[i]-1, i)
}
case histTime:
last := 0.0
for i := 1; i < numCounts; i++ {
hi := buckets[i]
if math.IsInf(hi, 0) || hi < runtimeHistMinBucket {
continue
}
if last > runtimeHistMaxBucket {
break
}
if last == 0 || hi >= last*runtimeHistBucketFactor {
addBucket(hi, i)
last = hi
}
}
}
addBucket(math.Inf(1), numCounts)
return lay
}
// runtimeMetricState is a metric being exported, alongside its
// [metrics.Sample] in [runtimeMetricsExporter.samples].
type runtimeMetricState struct {
spec runtimeMetricSpec
// typeLine is the "# TYPE name type\n" line.
typeLine []byte
// valuePrefix is "name " for scalar metrics, or nil for histograms.
valuePrefix []byte
// countPrefix is "name_count " for histograms, or nil otherwise.
countPrefix []byte
// hist is the bucket layout for histograms, or nil otherwise.
hist *runtimeHistLayout
}
// runtimeMetricsExporter reads a fixed set of Go runtime/metrics and
// writes them in Prometheus format. There is one, [runtimeMetrics],
// and it keeps its sample slice across reads so that the runtime can
// reuse histogram buffers and a steady-state scrape does not allocate.
type runtimeMetricsExporter struct {
mu sync.Mutex
inited bool
samples []metrics.Sample // reused across reads; parallel to states
states []*runtimeMetricState // parallel to samples
// Indexes into samples of the metrics backing the legacy
// memstats_* names, or -1 if the runtime lacks them.
memstatsIdx memstatsIndexes
}
// memstatsIndexes holds indexes into [runtimeMetricsExporter.samples]
// for the runtime/metrics equivalents of the runtime.MemStats fields
// that varz has exported as memstats_* since before runtime/metrics
// existed. Those names are kept for existing dashboards, but they are
// now read from runtime/metrics rather than runtime.ReadMemStats,
// which stops the world.
type memstatsIndexes struct {
heapAlloc int // /memory/classes/heap/objects:bytes
totalAlloc int // /gc/heap/allocs:bytes
sys int // /memory/classes/total:bytes
mallocs int // /gc/heap/allocs:objects
frees int // /gc/heap/frees:objects
tinyAllocs int // /gc/heap/tiny/allocs:objects
numGC int // /gc/cycles/total:gc-cycles
gcCPU int // /cpu/classes/gc/total:cpu-seconds
gcIdleCPU int // /cpu/classes/gc/mark/idle:cpu-seconds
totalCPU int // /cpu/classes/total:cpu-seconds
}
// runtimeMetrics is the process-wide exporter used by [Handler].
var runtimeMetrics = new(runtimeMetricsExporter)
// runtimeMetricsBufPool holds *[]byte output buffers so that a scrape
// can be formatted under the exporter's lock and written to the
// (possibly slow) client after releasing it.
var runtimeMetricsBufPool = sync.Pool{New: func() any { return new([]byte) }}
// init reads every metric in [runtimeMetricSpecs] once, drops the ones
// this toolchain does not know, and precomputes the per-metric output
// prefixes and histogram layouts.
//
// e.mu must be held.
func (e *runtimeMetricsExporter) init() {
e.inited = true
samples := make([]metrics.Sample, len(runtimeMetricSpecs))
for i, spec := range runtimeMetricSpecs {
samples[i].Name = spec.name
}
metrics.Read(samples)
byName := map[string]int{}
for i, spec := range runtimeMetricSpecs {
s := samples[i]
var ok bool
switch s.Value.Kind() {
case metrics.KindUint64, metrics.KindFloat64:
ok = spec.typ != "histogram"
case metrics.KindFloat64Histogram:
ok = spec.typ == "histogram"
}
if !ok {
// Unknown to this toolchain, or its kind changed
// out from under our table.
continue
}
st := &runtimeMetricState{spec: spec}
if !spec.hidden {
name := runtimeMetricPromName(spec.name, spec.typ)
st.typeLine = []byte("# TYPE " + name + " " + spec.typ + "\n")
if spec.typ == "histogram" {
st.hist = newRuntimeHistLayout(name, s.Value.Float64Histogram().Buckets, spec.buckets)
st.countPrefix = []byte(name + "_count ")
} else {
st.valuePrefix = []byte(name + " ")
}
}
byName[spec.name] = len(e.samples)
e.samples = append(e.samples, s)
e.states = append(e.states, st)
}
idx := func(name string) int {
if i, ok := byName[name]; ok {
return i
}
return -1
}
e.memstatsIdx = memstatsIndexes{
heapAlloc: idx("/memory/classes/heap/objects:bytes"),
totalAlloc: idx("/gc/heap/allocs:bytes"),
sys: idx("/memory/classes/total:bytes"),
mallocs: idx("/gc/heap/allocs:objects"),
frees: idx("/gc/heap/frees:objects"),
tinyAllocs: idx("/gc/heap/tiny/allocs:objects"),
numGC: idx("/gc/cycles/total:gc-cycles"),
gcCPU: idx("/cpu/classes/gc/total:cpu-seconds"),
gcIdleCPU: idx("/cpu/classes/gc/mark/idle:cpu-seconds"),
totalCPU: idx("/cpu/classes/total:cpu-seconds"),
}
}
// writeTo reads the current values of the exported metrics and writes
// them to w in Prometheus text format, preceded by the legacy
// memstats_* metrics.
func (e *runtimeMetricsExporter) writeTo(w io.Writer) error {
bp := runtimeMetricsBufPool.Get().(*[]byte)
defer runtimeMetricsBufPool.Put(bp)
e.mu.Lock()
if !e.inited {
e.init()
} else {
metrics.Read(e.samples)
}
*bp = e.appendMemstats((*bp)[:0])
for i, st := range e.states {
if !st.spec.hidden {
*bp = st.appendTo(*bp, &e.samples[i].Value)
}
}
e.mu.Unlock()
_, err := w.Write(*bp)
return err
}
// appendTo appends the Prometheus lines for st with value v to b.
func (st *runtimeMetricState) appendTo(b []byte, v *metrics.Value) []byte {
b = append(b, st.typeLine...)
switch v.Kind() {
case metrics.KindUint64:
b = append(b, st.valuePrefix...)
b = strconv.AppendUint(b, v.Uint64(), 10)
b = append(b, '\n')
case metrics.KindFloat64:
b = append(b, st.valuePrefix...)
b = strconv.AppendFloat(b, v.Float64(), 'g', -1, 64)
b = append(b, '\n')
case metrics.KindFloat64Histogram:
counts := v.Float64Histogram().Counts
var sum uint64
var next int
for i, end := range st.hist.ends {
for ; next < end && next < len(counts); next++ {
sum += counts[next]
}
b = append(b, st.hist.lines[i]...)
b = strconv.AppendUint(b, sum, 10)
b = append(b, '\n')
}
b = append(b, st.countPrefix...)
b = strconv.AppendUint(b, sum, 10)
b = append(b, '\n')
}
return b
}
// appendMemstats appends the legacy memstats_* metrics to b, sourced
// from the runtime/metrics samples named by e.memstatsIdx.
//
// e.mu must be held and e.samples must have been read.
func (e *runtimeMetricsExporter) appendMemstats(b []byte) []byte {
idx := &e.memstatsIdx
u := func(i int) (uint64, bool) {
if i < 0 {
return 0, false
}
v := &e.samples[i].Value
if v.Kind() != metrics.KindUint64 {
return 0, false
}
return v.Uint64(), true
}
f := func(i int) (float64, bool) {
if i < 0 {
return 0, false
}
v := &e.samples[i].Value
if v.Kind() != metrics.KindFloat64 {
return 0, false
}
return v.Float64(), true
}
appendUint := func(typ, name string, v uint64, ok bool, help string) {
if ok {
b = appendMemstat(b, typ, name, help)
b = strconv.AppendUint(b, v, 10)
b = append(b, '\n')
}
}
appendIdx := func(typ, name string, i int, help string) {
v, ok := u(i)
appendUint(typ, name, v, ok, help)
}
appendIdx("gauge", "heap_alloc", idx.heapAlloc, "current bytes of allocated heap objects (up/down smoothly)")
appendIdx("counter", "total_alloc", idx.totalAlloc, "cumulative bytes allocated for heap objects")
appendIdx("gauge", "sys", idx.sys, "total bytes of memory obtained from the OS")
// runtime.ReadMemStats counts tiny allocations in both Mallocs
// and Frees, but /gc/heap/allocs:objects and /gc/heap/frees:objects
// exclude them, so add them back.
mallocs, ok1 := u(idx.mallocs)
frees, ok2 := u(idx.frees)
tiny, ok3 := u(idx.tinyAllocs)
appendUint("counter", "mallocs", mallocs+tiny, ok1 && ok3, "cumulative count of heap objects allocated")
appendUint("counter", "frees", frees+tiny, ok2 && ok3, "cumulative count of heap objects freed")
appendIdx("counter", "num_gc", idx.numGC, "number of completed GC cycles")
gc, ok1 := f(idx.gcCPU)
idle, ok2 := f(idx.gcIdleCPU)
total, ok3 := f(idx.totalCPU)
if ok1 && ok2 && ok3 {
// This is how the runtime computes MemStats.GCCPUFraction:
// GC time less idle-priority marking, which is "free", over
// total time. The runtime updates its CPU class stats at
// the end of each GC cycle, so total is 0 until the first
// one, as GCCPUFraction was too.
var frac float64
if total > 0 {
frac = (gc - idle) / total
}
b = appendMemstat(b, "gauge", "gc_cpu_fraction", "fraction of CPU time used by GC")
b = strconv.AppendFloat(b, frac, 'f', -1, 64)
b = append(b, '\n')
}
return b
}
// appendMemstat appends the HELP and TYPE lines for the memstats_*
// metric name to b, followed by the metric name and a space, leaving
// the caller to append the value and newline.
func appendMemstat(b []byte, typ, name, help string) []byte {
b = append(b, "# HELP memstats_"...)
b = append(b, name...)
b = append(b, ' ')
b = append(b, help...)
b = append(b, "\n# TYPE memstats_"...)
b = append(b, name...)
b = append(b, ' ')
b = append(b, typ...)
b = append(b, "\nmemstats_"...)
b = append(b, name...)
b = append(b, ' ')
return b
}
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package varz contains code to export metrics in Prometheus format.
package varz
import (
"cmp"
"expvar"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"reflect"
"runtime"
"sort"
"strconv"
"strings"
"sync"
"time"
"unicode"
"unicode/utf8"
"tailscale.com/metrics"
"tailscale.com/syncs"
"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 {
if key == "memstats" {
// The expvar package's own memstats var calls
// runtime.ReadMemStats, which stops the world and
// allocates. [Handler] exports the memstats_* metrics
// from runtime/metrics instead, so don't even call it.
return
}
v := f()
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.
//
// It also exports the Go runtime/metrics in [runtimeMetricSpecs] and
// the legacy memstats_* metrics, both read from runtime/metrics.
func Handler(w http.ResponseWriter, r *http.Request) {
ExpvarDoHandler(expvarDo)(w, r)
runtimeMetrics.writeTo(w)
}
// 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.
//
// Unlike [Handler], it does not export the Go runtime/metrics.
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
// 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 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. Well-known public
// providers (see the publicdns package) are dialed at their known IPs;
// arbitrary providers require BootstrapResolution below, or an
// IP-literal URL host.
// - "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, or only use the resolver if its addresses are already
// known (as for well-known public providers).
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. Well-known public
// providers (see the publicdns package) are dialed at their known IPs;
// arbitrary providers require BootstrapResolution below, or an
// IP-literal URL host.
// - "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, or only use the resolver if its addresses are already
// known (as for well-known public providers).
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 are 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 (
"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 (
"crypto/ed25519"
"crypto/subtle"
"go4.org/mem"
"tailscale.com/types/structs"
"tailscale.com/types/tkatype"
)
const (
// tlPrivateHexPrefix is the prefix used to identify a
// hex-encoded tailnet-lock key.
//
// "nl" is a reference to "Network Lock", the pre-release name for
// Tailnet Lock.
tlPrivateHexPrefix = "nlpriv:"
// tlPublicHexPrefix is the prefix used to identify the public
// side of a hex-encoded tailnet-lock key.
//
// "nl" is a reference to "Network Lock", the pre-release name for
// Tailnet Lock.
tlPublicHexPrefix = "nlpub:"
// tlPublicHexPrefixCLI 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.
tlPublicHexPrefixCLI = "tlpub:"
)
// TLPrivate is a node-managed tailnet-lock key, used for signing
// node-key signatures and authority update messages.
type TLPrivate struct {
_ structs.Incomparable // because == isn't constant-time
k [ed25519.PrivateKeySize]byte
}
// Deprecated: use [TLPrivate] instead.
type NLPrivate = TLPrivate
// IsZero reports whether k is the zero value.
func (k TLPrivate) IsZero() bool {
empty := TLPrivate{}
return subtle.ConstantTimeCompare(k.k[:], empty.k[:]) == 1
}
// NewTLPrivate creates and returns a new tailnet-lock key.
func NewTLPrivate() TLPrivate {
// 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 TLPrivate
copy(out.k[:], priv)
return out
}
// Deprecated: use [NewTLPrivate] instead.
func NewNLPrivate() TLPrivate {
return NewTLPrivate()
}
// MarshalText implements encoding.TextUnmarshaler.
func (k *TLPrivate) UnmarshalText(b []byte) error {
return parseHex(k.k[:], mem.B(b), mem.S(tlPrivateHexPrefix))
}
// AppendText implements encoding.TextAppender.
func (k TLPrivate) AppendText(b []byte) ([]byte, error) {
return appendHexKey(b, tlPrivateHexPrefix, k.k[:]), nil
}
// MarshalText implements encoding.TextMarshaler.
func (k TLPrivate) MarshalText() ([]byte, error) {
return k.AppendText(nil)
}
// Equal reports whether k and other are the same key.
func (k TLPrivate) Equal(other TLPrivate) bool {
return subtle.ConstantTimeCompare(k.k[:], other.k[:]) == 1
}
// Public returns the public component of this key.
func (k TLPrivate) Public() TLPublic {
var out TLPublic
copy(out.k[:], ed25519.PrivateKey(k.k[:]).Public().(ed25519.PublicKey))
return out
}
// KeyID returns an identifier for this key.
func (k TLPrivate) 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 TLPrivate) 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 TLPrivate) SignNKS(sigHash tkatype.NKSSigHash) ([]byte, error) {
return ed25519.Sign(ed25519.PrivateKey(k.k[:]), sigHash[:]), nil
}
// TLPublic is the public portion of a [TLPrivate].
type TLPublic struct {
k [ed25519.PublicKeySize]byte
}
// Deprecated: use [TLPublic] instead.
type NLPublic = TLPublic
// TLPublicFromEd25519Unsafe converts an ed25519 public key into
// a type of [TLPublic].
//
// New uses of this function should be avoided, as it's possible to
// accidentally construct an TLPublic from a non tailnet-lock key.
func TLPublicFromEd25519Unsafe(public ed25519.PublicKey) TLPublic {
var out TLPublic
copy(out.k[:], public)
return out
}
// Deprecated: use [TLPublicFromEd25519Unsafe] instead.
func NLPublicFromEd25519Unsafe(public ed25519.PublicKey) TLPublic {
return TLPublicFromEd25519Unsafe(public)
}
// UnmarshalText implements encoding.TextUnmarshaler. This function
// is able to decode both the CLI form (tlpub:<hex>) & the
// regular form (nlpub:<hex>).
func (k *TLPublic) UnmarshalText(b []byte) error {
if mem.HasPrefix(mem.B(b), mem.S(tlPublicHexPrefix)) {
return parseHex(k.k[:], mem.B(b), mem.S(tlPublicHexPrefix))
}
return parseHex(k.k[:], mem.B(b), mem.S(tlPublicHexPrefixCLI))
}
// AppendText implements encoding.TextAppender.
func (k TLPublic) AppendText(b []byte) ([]byte, error) {
return appendHexKey(b, tlPublicHexPrefix, k.k[:]), nil
}
// MarshalText implements encoding.TextMarshaler, emitting a
// representation of the form nlpub:<hex>.
func (k TLPublic) 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 TLPublic) CLIString() string {
return string(appendHexKey(nil, tlPublicHexPrefixCLI, k.k[:]))
}
// Verifier returns a ed25519.PublicKey that can be used to
// verify signatures.
func (k TLPublic) Verifier() ed25519.PublicKey {
return ed25519.PublicKey(k.k[:])
}
// IsZero reports whether k is the zero value.
func (k TLPublic) IsZero() bool {
return k.Equal(TLPublic{})
}
// Equal reports whether k and other are the same key.
func (k TLPublic) Equal(other TLPublic) bool {
return subtle.ConstantTimeCompare(k.k[:], other.k[:]) == 1
}
// KeyID returns a tkatype.KeyID that can be used with a tka.Authority.
func (k TLPublic) KeyID() tkatype.KeyID {
return k.k[:]
}
// 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[TLPrivate](),
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 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
}
// HalfCloser is an interface to abstract around various Conn types that
// allow closing of the read and write streams independently of each other.
// It is normally used as a typecast for a `net.Conn` to get access to these
// functions. If you use it for this purpose, you should either be absolutely
// certain that the `net.Conn` you have represents a half-closable connection
// (eg. TCP or unix socket), or check for and handle a failure to cast.
type HalfCloser interface {
CloseRead() error
CloseWrite() error
}
// 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.TLPrivate
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.TLPrivate
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.TLPrivate { 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.TLPrivate
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 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
// ToFQDN returns s as an FQDN, with a trailing dot added if missing. The
// empty string and "." both parse as ".".
//
// Names containing whitespace or control characters are rejected. See the
// comment in the label loop below for why other characters are not
// validated.
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)
}
// DNS labels on the wire may contain any non-zero byte (see the note in
// the label loop below about issue 2024), but names we accept from
// control are also written verbatim into line-oriented OS configuration
// files like resolv.conf and hosts, where a newline or carriage return
// can inject whole new directives. Reject whitespace and control
// characters so a malicious control server can't do that.
for i := range len(s) {
if c := s[i]; c <= ' ' || c == 0x7f {
return "", vizerror.Errorf("%q contains invalid character %q", raw, c)
}
}
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 httpbody provides helpers for reading HTTP response bodies
// defensively, capping how much memory a response can make the reader
// buffer.
//
// A typical HTTP client applies the cap in its Do method, so every
// response it returns is capped unless the request opts out:
//
// res, err := c.Client.Do(req)
// httpbody.LimitSize(res)
package httpbody
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"tailscale.com/util/ctxkey"
)
// ErrTooLarge is reported by a body wrapped with [LimitSize] once more
// than the configured number of bytes have been read. The error text
// includes the limit that was exceeded.
var ErrTooLarge = errors.New("response body too large")
// DefaultMaxSize is the default response body size cap, in bytes, used
// when a context carries no override set with [WithMaxSize]. HTTP clients
// that apply response body caps should honor it, so requests can tune or
// disable the cap uniformly across clients.
const DefaultMaxSize = 1 << 20
// maxSizeKey is the context key carrying the response body size cap.
var maxSizeKey = ctxkey.New[int64]("httpbody.maxSize", DefaultMaxSize)
// WithMaxSize returns a context that sets the response body size cap to
// max, instead of [DefaultMaxSize], for HTTP clients that apply response
// body caps. A max of zero or less disables the cap. Disable it for
// responses that stream an unbounded number of individually bounded
// messages, like a streaming map response.
func WithMaxSize(ctx context.Context, max int64) context.Context {
return maxSizeKey.WithValue(ctx, max)
}
// MaxSize returns the response body size cap in ctx: the value set with
// [WithMaxSize], or [DefaultMaxSize] if ctx carries none.
func MaxSize(ctx context.Context) int64 {
return maxSizeKey.Value(ctx)
}
// LimitSize replaces res.Body with a wrapper that fails reads with an
// error wrapping [ErrTooLarge] once more than the response body size
// limit in res.Request's context, per [WithMaxSize], has been read:
// [DefaultMaxSize] by default, or if res has no Request. A limit of zero
// or less imposes no limit, removing any limit a previous call set.
//
// Repeated calls to LimitSize or LimitSizeTo replace the previous limit
// rather than compounding it, so a later call can raise or remove the
// limit an earlier one set. If bytes have already been read through a
// previous limit, the new limit counts bytes read after it was applied.
//
// Unlike io.LimitReader, which silently truncates at its limit, the
// wrapper reports an oversize body so callers can distinguish a short
// body from a too-large one. A body of at most the limit, including one
// of exactly the limit, reads back without error.
func LimitSize(res *http.Response) {
max := int64(DefaultMaxSize)
if res.Request != nil {
max = MaxSize(res.Request.Context())
}
limitSizeTo(res, max)
}
// LimitSizeTo is like [LimitSize] but caps res.Body at exactly max bytes,
// ignoring any override in res.Request's context. A max of zero or less
// imposes no limit, removing any limit a previous call set. Repeated
// calls to LimitSize or LimitSizeTo replace the previous limit rather
// than compounding it.
func LimitSizeTo(res *http.Response, max int64) {
limitSizeTo(res, max)
}
func limitSizeTo(res *http.Response, max int64) {
// Unwrap any limit a previous call set instead of wrapping it again:
// nested limits would compound, with the smaller always winning, so
// re-limiting could never raise the cap. At most one wrapper can
// exist, because this always unwraps first.
if lb, ok := res.Body.(*limitedBody); ok {
res.Body = lb.body
}
if max <= 0 {
return
}
res.Body = &limitedBody{body: res.Body, max: max, remain: max}
}
// limitedBody is the io.ReadCloser installed by [LimitSize].
type limitedBody struct {
body io.ReadCloser
max int64 // the limit, for error reporting
// remain is the number of bytes still allowed. Once the limit is
// exhausted, a one-byte probe of body distinguishes a body of exactly
// max bytes (EOF, fine) from one with more to come. remain goes
// negative once oversize is detected, and stays there.
remain int64
}
func (b *limitedBody) Read(p []byte) (n int, err error) {
if len(p) == 0 {
return 0, nil
}
if b.remain < 0 {
return 0, b.tooLarge()
}
if b.remain == 0 {
// The limit is exhausted. A body of exactly max bytes is legal, so
// check whether the body has actually ended before declaring it
// too large. The probed byte, if any, is dropped: once a body is
// too large, callers only want the error.
var probe [1]byte
m, err := b.body.Read(probe[:])
if m == 0 && err != nil {
return 0, err // io.EOF, or the underlying error
}
b.remain = -1
return 0, b.tooLarge()
}
if int64(len(p)) > b.remain {
p = p[:b.remain]
}
n, err = b.body.Read(p)
b.remain -= int64(n)
return n, err
}
func (b *limitedBody) Close() error {
return b.body.Close()
}
func (b *limitedBody) tooLarge() error {
return fmt.Errorf("%w: limit %d bytes", ErrTooLarge, b.max)
}
// 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], len(slice))
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
//go:build !ts_omit_syspolicy
package policyclient
import (
"tailscale.com/util/syspolicy/pkey"
"tailscale.com/util/syspolicy/setting"
)
// PolicySnapshot is an alias for [settings.Snapshot] unless syspolicy is omitted
// from the build.
type policySnapshot = setting.Snapshot
func GetStringArrayFromSnapshot(snapshot *PolicySnapshot, key pkey.Key) []string {
if snapshot == nil {
return nil
}
values, _ := snapshot.Get(key).([]string)
return values
}
// 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
// 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"
// ReasonQueueFull means that the packet was dropped because an internal
// packet queue was full.
ReasonQueueFull DropReason = "queue_full"
)
// 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
}
// parseEncoderOptions applies opts on top of the default encoder options.
func parseEncoderOptions(opts []Option) encoderOptions {
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)
}
}
return eopts
}
type encoder struct {
pool *sync.Pool
*zstd.Encoder
}
var streamingEncoderPools sync.Map // map[encoderOptions]*sync.Pool -> *zstd.Encoder
// GetStreamingEncoder returns an encoder from the shared pool, configured
// for streaming (stateful) use: it maintains compression context across
// Write calls, and Close finishes the frame. The caller must call
// enc.Reset with the destination writer before first use, and enc.Close
// when done writing. Call the returned put function exactly once
// afterwards to return the encoder to the pool; the encoder must not be
// used after that, and must not be used concurrently.
func GetStreamingEncoder(opts ...Option) (enc *zstd.Encoder, put func()) {
eopts := parseEncoderOptions(opts)
var pool *sync.Pool
if poolCoders() {
vpool, ok := streamingEncoderPools.Load(eopts)
if !ok {
vpool, _ = streamingEncoderPools.LoadOrStore(eopts, new(sync.Pool))
}
pool = vpool.(*sync.Pool)
enc, _ = pool.Get().(*zstd.Encoder)
}
if enc == nil {
// Unlike the stateless encoders above, streaming encoders must not
// use SingleSegment framing: the total content size is not known up
// front, and the window size must be communicated to decoders.
zopts := []zstd.EOption{
// Set concurrency=1 to ensure synchronous operation.
zstd.WithEncoderConcurrency(1),
zstd.WithEncoderLevel(eopts.level),
zstd.WithEncoderCRC(eopts.checksum),
zstd.WithLowerEncoderMem(eopts.lowMemory),
}
if eopts.maxWindowLog2 > 0 {
zopts = append(zopts, zstd.WithWindowSize(1<<eopts.maxWindowLog2))
}
enc = must.Get(zstd.NewWriter(nil, zopts...))
}
return enc, func() {
if pool != nil {
pool.Put(enc)
}
}
}
func getEncoder(opts ...Option) encoder {
eopts := parseEncoderOptions(opts)
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)
}
// GetDecoder returns a decoder from the shared decoder pool for streaming
// (io.Reader-style) use, configured with opts. The caller must call
// dec.Reset on it before first use and whenever its underlying reader
// changes. When done with the decoder, the caller must call the returned
// put function exactly once to return it to the pool. The decoder must not
// be used after put is called, and must not be used concurrently.
func GetDecoder(opts ...Option) (dec *zstd.Decoder, put func()) {
d := getDecoder(opts...)
return d.Decoder, func() { putDecoder(d) }
}
// 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
}