Update dependencies

This commit is contained in:
bluepython508
2025-04-09 01:00:12 +01:00
parent f0641ffd6e
commit 5a9cfc022c
882 changed files with 68930 additions and 24201 deletions

View File

@@ -25,8 +25,10 @@ import (
"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
@@ -142,44 +144,88 @@ type CapabilityVersion int
// - 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: Client supports filtertype.Match.SrcCaps (issue #12542)
// - 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)
const CurrentCapabilityVersion CapabilityVersion = 106
type StableID string
// - 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.
const CurrentCapabilityVersion CapabilityVersion = 115
// 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
}
type StableNodeID StableID
// StableNodeID is a string form of [NodeID].
//
// Different control plane servers should ideally have different StableNodeID
// suffixes for different sites or regions.
//
// Being a string, it's safer to use in JavaScript without worrying about the
// size of the integer, as documented on [NodeID].
//
// But in general, Tailscale APIs can accept either a [NodeID] integer or a
// [StableNodeID] string when referring to a node.
type StableNodeID string
func (u StableNodeID) IsZero() bool {
return u == ""
}
// User is an IPN user.
// 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.)
@@ -192,34 +238,30 @@ func (u StableNodeID) IsZero() bool {
// have a general gmail address login associated with the user.
type User struct {
ID UserID
LoginName string `json:"-"` // not stored, filled from Login // TODO REMOVE
DisplayName string // if non-empty overrides Login field
ProfilePicURL string // if non-empty overrides Login field
Logins []LoginID
Created time.Time
}
// Login is a user from a specific identity provider, not associated with any
// particular tailnet.
type Login struct {
_ structs.Incomparable
ID LoginID
Provider string
LoginName string
DisplayName string
ProfilePicURL string
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 // from the IdP
}
// A UserProfile is display-friendly data for a user.
// 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
// Roles exists for legacy reasons, to keep old macOS clients
// happy. It JSON marshals as [].
Roles emptyStructJSONSlice
ProfilePicURL string `json:",omitempty"`
}
func (p *UserProfile) Equal(p2 *UserProfile) bool {
@@ -235,16 +277,6 @@ func (p *UserProfile) Equal(p2 *UserProfile) bool {
p.ProfilePicURL == p2.ProfilePicURL
}
type emptyStructJSONSlice struct{}
var emptyJSONSliceBytes = []byte("[]")
func (emptyStructJSONSlice) MarshalJSON() ([]byte, error) {
return emptyJSONSliceBytes, nil
}
func (emptyStructJSONSlice) UnmarshalJSON([]byte) error { return nil }
// 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.
@@ -279,6 +311,7 @@ func MarshalCapJSON[T any](capRule T) (RawMessage, error) {
return RawMessage(string(bs)), nil
}
// Node is a Tailscale device in a tailnet.
type Node struct {
ID NodeID
StableID StableNodeID
@@ -302,19 +335,37 @@ type Node struct {
KeySignature tkatype.MarshaledSignature `json:",omitempty"`
Machine key.MachinePublic
DiscoKey key.DiscoPublic
Addresses []netip.Prefix // IP addresses of this Node directly
AllowedIPs []netip.Prefix // range of IP addresses to route to this node
Endpoints []netip.AddrPort `json:",omitempty"` // IP+port (public via STUN, and local LANs)
// DERP is this node's home DERP region ID integer, but shoved into an
// 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
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 DERP region ID
// a QWERTY keyboard (3.3.40)). The "port number" is the home LegacyDERPString region ID
// integer.
//
// TODO(bradfitz): simplify this legacy mess; add a new HomeDERPRegionID int
// field behind a new capver bump.
DERP string `json:",omitempty"` // DERP-in-IP:port ("127.3.3.40:N") endpoint
// 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,omitempty"` // 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 int `json:",omitempty"` // DERP region ID of the node's home DERP
Hostinfo HostinfoView
Created time.Time
@@ -559,6 +610,11 @@ func (n *Node) InitDisplayNames(networkMagicDNSSuffix string) {
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 (
@@ -771,7 +827,7 @@ type Hostinfo struct {
// "5.10.0-17-amd64".
OSVersion string `json:",omitempty"`
Container opt.Bool `json:",omitempty"` // whether the client is running in a container
Container opt.Bool `json:",omitempty"` // best-effort whether the client is running in a container
Env string `json:",omitempty"` // a hostinfo.EnvType in string form
Distro string `json:",omitempty"` // "debian", "ubuntu", "nixos", ...
DistroVersion string `json:",omitempty"` // "20.04", ...
@@ -780,15 +836,23 @@ type Hostinfo struct {
// App is used to disambiguate Tailscale clients that run using tsnet.
App string `json:",omitempty"` // "k8s-operator", "golinks", ...
Desktop opt.Bool `json:",omitempty"` // if a desktop was detected on Linux
Package string `json:",omitempty"` // Tailscale package to disambiguate ("choco", "appstore", etc; "" for unknown)
DeviceModel string `json:",omitempty"` // mobile phone model ("Pixel 3a", "iPhone12,3")
PushDeviceToken string `json:",omitempty"` // macOS/iOS APNs device token for notifications (and Android in the future)
Hostname string `json:",omitempty"` // name of the host the client runs on
ShieldsUp bool `json:",omitempty"` // indicates whether the host is blocking incoming connections
ShareeNode bool `json:",omitempty"` // indicates this node exists in netmap because it's owned by a shared-to user
NoLogsNoSupport bool `json:",omitempty"` // indicates that the user has opted out of sending logs and support
WireIngress bool `json:",omitempty"` // indicates that the node wants the option to receive ingress connections
Desktop opt.Bool `json:",omitempty"` // if a desktop was detected on Linux
Package string `json:",omitempty"` // Tailscale package to disambiguate ("choco", "appstore", etc; "" for unknown)
DeviceModel string `json:",omitempty"` // mobile phone model ("Pixel 3a", "iPhone12,3")
PushDeviceToken string `json:",omitempty"` // macOS/iOS APNs device token for notifications (and Android in the future)
Hostname string `json:",omitempty"` // name of the host the client runs on
ShieldsUp bool `json:",omitempty"` // indicates whether the host is blocking incoming connections
ShareeNode bool `json:",omitempty"` // indicates this node exists in netmap because it's owned by a shared-to user
NoLogsNoSupport bool `json:",omitempty"` // indicates that the user has opted out of sending logs and support
// 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:",omitempty"`
IngressEnabled bool `json:",omitempty"` // if the node has any funnel endpoint enabled
AllowsUpdate bool `json:",omitempty"` // indicates that the node has opted-in to admin-console-drive remote updates
Machine string `json:",omitempty"` // the current host's machine type (uname -m)
GoArch string `json:",omitempty"` // GOARCH value (of the built binary)
@@ -804,6 +868,7 @@ type Hostinfo struct {
Userspace opt.Bool `json:",omitempty"` // if the client is running in userspace (netstack) mode
UserspaceRouter opt.Bool `json:",omitempty"` // if the client's subnet router is running in userspace (netstack) mode
AppConnector opt.Bool `json:",omitempty"` // if the client is running the app-connector service
ServicesHash string `json:",omitempty"` // opaque hash of the most recent list of tailnet services, change in hash indicates config should be fetched via c2n
// Location represents geographical location data about a
// Tailscale host. Location is optional and only set if
@@ -814,6 +879,61 @@ type Hostinfo struct {
// require changes to Hostinfo.Equal.
}
// 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
// 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 {
@@ -824,14 +944,6 @@ func (hi *Hostinfo) TailscaleSSHEnabled() bool {
func (v HostinfoView) TailscaleSSHEnabled() bool { return v.ж.TailscaleSSHEnabled() }
// TailscaleFunnelEnabled reports whether or not this node has explicitly
// enabled Funnel.
func (hi *Hostinfo) TailscaleFunnelEnabled() bool {
return hi != nil && hi.WireIngress
}
func (v HostinfoView) TailscaleFunnelEnabled() bool { return v.ж.TailscaleFunnelEnabled() }
// NetInfo contains information about the host's network state.
type NetInfo struct {
// MappingVariesByDestIP says whether the host's NAT mappings
@@ -975,68 +1087,6 @@ func (h *Hostinfo) Equal(h2 *Hostinfo) bool {
return reflect.DeepEqual(h, h2)
}
// HowUnequal returns a list of paths through Hostinfo where h and h2 differ.
// If they differ in nil-ness, the path is "nil", otherwise the path is like
// "ShieldsUp" or "NetInfo.nil" or "NetInfo.PCP".
func (h *Hostinfo) HowUnequal(h2 *Hostinfo) (path []string) {
return appendStructPtrDiff(nil, "", reflect.ValueOf(h), reflect.ValueOf(h2))
}
func appendStructPtrDiff(base []string, pfx string, p1, p2 reflect.Value) (ret []string) {
ret = base
if p1.IsNil() && p2.IsNil() {
return base
}
mkPath := func(b string) string {
if pfx == "" {
return b
}
return pfx + "." + b
}
if p1.IsNil() || p2.IsNil() {
return append(base, mkPath("nil"))
}
v1, v2 := p1.Elem(), p2.Elem()
t := v1.Type()
for i, n := 0, t.NumField(); i < n; i++ {
sf := t.Field(i)
switch sf.Type.Kind() {
case reflect.String:
if v1.Field(i).String() != v2.Field(i).String() {
ret = append(ret, mkPath(sf.Name))
}
continue
case reflect.Bool:
if v1.Field(i).Bool() != v2.Field(i).Bool() {
ret = append(ret, mkPath(sf.Name))
}
continue
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if v1.Field(i).Int() != v2.Field(i).Int() {
ret = append(ret, mkPath(sf.Name))
}
continue
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
if v1.Field(i).Uint() != v2.Field(i).Uint() {
ret = append(ret, mkPath(sf.Name))
}
continue
case reflect.Slice, reflect.Map:
if !reflect.DeepEqual(v1.Field(i).Interface(), v2.Field(i).Interface()) {
ret = append(ret, mkPath(sf.Name))
}
continue
case reflect.Ptr:
if sf.Type.Elem().Kind() == reflect.Struct {
ret = appendStructPtrDiff(ret, sf.Name, v1.Field(i), v2.Field(i))
continue
}
}
panic(fmt.Sprintf("unsupported type at %s: %s", mkPath(sf.Name), sf.Type.String()))
}
return ret
}
// 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
@@ -1117,11 +1167,11 @@ type RegisterResponseAuth struct {
AuthKey string `json:",omitempty"`
}
// RegisterRequest is sent by a client to register the key for a node.
// It is encoded to JSON, encrypted with golang.org/x/crypto/nacl/box,
// using the local machine key, and sent to:
// RegisterRequest is a request to register a key for a node.
//
// https://login.tailscale.com/machine/<mkey hex>
// This is JSON-encoded and sent over the control plane connection to:
//
// POST https://<control-plane>/machine/register.
type RegisterRequest struct {
_ structs.Incomparable
@@ -1237,10 +1287,9 @@ type Endpoint struct {
// The request includes a copy of the client's current set of WireGuard
// endpoints and general host information.
//
// The request is encoded to JSON, encrypted with golang.org/x/crypto/nacl/box,
// using the local machine key, and sent to:
// This is JSON-encoded and sent over the control plane connection to:
//
// https://login.tailscale.com/machine/<mkey hex>/map
// 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
@@ -1355,7 +1404,7 @@ var PortRangeAny = PortRange{0, 65535}
type NetPortRange struct {
_ structs.Incomparable
IP string // IP, CIDR, Range, or "*" (same formats as FilterRule.SrcIPs)
Bits *int // deprecated; the 2020 way to turn IP into a CIDR. See FilterRule.SrcBits.
Bits *int `json:",omitempty"` // deprecated; the 2020 way to turn IP into a CIDR. See FilterRule.SrcBits.
Ports PortRange
}
@@ -1417,7 +1466,7 @@ const (
// 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 Contains method.
// for by using the [NodeCapMap.Contains] method.
//
// See [NodeCapability] for more information on keys.
type NodeCapMap map[NodeCapability][]RawMessage
@@ -1431,12 +1480,19 @@ func (c NodeCapMap) Equal(c2 NodeCapMap) bool {
// 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 NodeCapability) ([]T, error) {
vals, ok := cm[cap]
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[NodeCapability, RawMessage], cap NodeCapability) ([]T, error) {
vals, ok := cm.GetOk(cap)
if !ok {
return nil, nil
}
out := make([]T, 0, len(vals))
for _, v := range vals {
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
@@ -1466,12 +1522,19 @@ type PeerCapMap map[PeerCapability][]RawMessage
// 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 PeerCapability) ([]T, error) {
vals, ok := cm[cap]
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[PeerCapability, RawMessage], cap PeerCapability) ([]T, error) {
vals, ok := cm.GetOk(cap)
if !ok {
return nil, nil
}
out := make([]T, 0, len(vals))
for _, v := range vals {
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
@@ -1666,9 +1729,14 @@ const (
PingPeerAPI PingType = "peerapi"
)
// PingRequest with no IP and Types is a request to send an HTTP request to prove the
// long-polling client is still connected.
// PingRequest with Types and IP, will send a ping to the IP and send a POST
// 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.
@@ -1833,7 +1901,7 @@ type MapResponse struct {
// 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
// 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
@@ -1962,10 +2030,6 @@ type MapResponse struct {
// auto-update setting doesn't change if the tailnet admin flips the
// default after the node registered.
DefaultAutoUpdate opt.Bool `json:",omitempty"`
// MaxKeyDuration describes the MaxKeyDuration setting for the tailnet.
// If zero, the value is unchanged.
MaxKeyDuration time.Duration `json:",omitempty"`
}
// ClientVersion is information about the latest client version that's available
@@ -2081,7 +2145,8 @@ func (n *Node) Equal(n2 *Node) bool {
slicesx.EqualSameNil(n.AllowedIPs, n2.AllowedIPs) &&
slicesx.EqualSameNil(n.PrimaryRoutes, n2.PrimaryRoutes) &&
slicesx.EqualSameNil(n.Endpoints, n2.Endpoints) &&
n.DERP == n2.DERP &&
n.LegacyDERPString == n2.LegacyDERPString &&
n.HomeDERP == n2.HomeDERP &&
n.Cap == n2.Cap &&
n.Hostinfo.Equal(n2.Hostinfo) &&
n.Created.Equal(n2.Created) &&
@@ -2353,20 +2418,45 @@ const (
// automatically when the network state changes.
NodeAttrDisableCaptivePortalDetection NodeCapability = "disable-captive-portal-detection"
// NodeAttrDisableSkipStatusQueue is set when the node should disable skipping
// of queued netmap.NetworkMap between the controlclient and LocalBackend.
// See tailscale/tailscale#14768.
NodeAttrDisableSkipStatusQueue NodeCapability = "disable-skip-status-queue"
// NodeAttrSSHEnvironmentVariables enables logic for handling environment variables sent
// via SendEnv in the SSH server and applying them to the SSH session.
NodeAttrSSHEnvironmentVariables NodeCapability = "ssh-env-vars"
// NodeAttrServiceHost 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).
NodeAttrServiceHost NodeCapability = "service-host"
// NodeAttrMaxKeyDuration 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.
NodeAttrMaxKeyDuration NodeCapability = "tailnet.maxKeyDuration"
// NodeAttrNativeIPV4 contains the IPV4 address of the node in its
// native tailnet. This is currently only sent to Hello, in its
// peer node list.
NodeAttrNativeIPV4 NodeCapability = "native-ipv4"
)
// SetDNSRequest is a request to add a DNS record.
//
// This is used for ACME DNS-01 challenges (so people can use
// LetsEncrypt, etc).
// 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.
//
// The request is encoded to JSON, encrypted with golang.org/x/crypto/nacl/box,
// using the local machine key, and sent to:
// This is JSON-encoded and sent over the control plane connection to:
//
// https://login.tailscale.com/machine/<mkey hex>/set-dns
// POST https://<control-plane>/machine/set-dns
type SetDNSRequest struct {
// Version is the client's capabilities
// (CurrentCapabilityVersion) when using the Noise transport.
@@ -2396,7 +2486,9 @@ type SetDNSRequest struct {
type SetDNSResponse struct{}
// HealthChangeRequest is the JSON request body type used to report
// node health changes to https://<control>/machine/<mkey hex>/update-health.
// node health changes to:
//
// POST https://<control-plane>/machine/update-health.
type HealthChangeRequest struct {
Subsys string // a health.Subsystem value in string form
Error string // or empty if cleared
@@ -2406,6 +2498,38 @@ type HealthChangeRequest struct {
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 {
@@ -2481,16 +2605,13 @@ type SSHPrincipal struct {
Any bool `json:"any,omitempty"` // if true, match any connection
// TODO(bradfitz): add StableUserID, once that exists
// PubKeys, if non-empty, means that this SSHPrincipal only
// matches if one of these public keys is presented by the user.
// 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.
//
// As a special case, if len(PubKeys) == 1 and PubKeys[0] starts
// with "https://", then it's fetched (like https://github.com/username.keys).
// In that case, the following variable expansions are also supported
// in the URL:
// * $LOGINNAME_EMAIL ("foo@bar.com" or "foo@github")
// * $LOGINNAME_LOCALPART (the "foo" from either of the above)
PubKeys []string `json:"pubKeys,omitempty"`
// Deprecated: do not use. It does nothing.
UnusedPubKeys []string `json:"pubKeys,omitempty"`
}
// SSHAction is how to handle an incoming connection.
@@ -2575,6 +2696,8 @@ type SSHRecorderFailureAction struct {
// 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
@@ -2635,9 +2758,9 @@ type SSHRecordingAttempt struct {
FailureMessage string
}
// QueryFeatureRequest is a request sent to "/machine/feature/query"
// to get instructions on how to enable a feature, such as Funnel,
// for the node's tailnet.
// 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 {
@@ -2726,7 +2849,7 @@ type OverTLSPublicKeyResponse struct {
// The token can be presented to any resource provider which offers OIDC
// Federation.
//
// It is JSON-encoded and sent over Noise to "/machine/id-token".
// 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
@@ -2841,3 +2964,51 @@ type EarlyNoise struct {
// 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
// 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:",omitempty"`
// 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:",omitempty"`
// 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:",omitempty"`
// Timestamp is the time at which the audit log was generated on the node.
Timestamp time.Time `json:",omitzero"`
}