2023-09-24 13:42:05 +02:00
|
|
|
package types
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
2026-05-11 14:53:09 +00:00
|
|
|
"maps"
|
2023-09-24 13:42:05 +02:00
|
|
|
"net/netip"
|
2025-02-26 07:22:55 -08:00
|
|
|
"slices"
|
2024-02-23 10:59:24 +01:00
|
|
|
"strconv"
|
2023-09-24 13:42:05 +02:00
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
|
|
|
|
"github.com/juanfont/headscale/hscontrol/policy/matcher"
|
2024-02-23 10:59:24 +01:00
|
|
|
"github.com/juanfont/headscale/hscontrol/util"
|
types: add MarshalZerologObject to domain types
Implement zerolog.LogObjectMarshaler interface on domain types
for structured logging:
- Node: logs node.id, node.name, machine.key (short), node.key (short),
node.is_tagged, node.expired, node.online, node.tags, user.name
- User: logs user.id, user.name, user.display, user.provider
- PreAuthKey: logs pak.id, pak.prefix (masked), pak.reusable,
pak.ephemeral, pak.used, pak.is_tagged, pak.tags
- APIKey: logs api_key.id, api_key.prefix (masked), api_key.expiration
Security: PreAuthKey and APIKey only log masked prefixes, never full
keys or hashes. Uses zf.* constants for consistent field naming.
2026-01-28 13:37:48 +00:00
|
|
|
"github.com/juanfont/headscale/hscontrol/util/zlog/zf"
|
|
|
|
|
"github.com/rs/zerolog"
|
2023-09-24 13:42:05 +02:00
|
|
|
"go4.org/netipx"
|
|
|
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
2025-03-21 11:49:32 +01:00
|
|
|
"tailscale.com/net/tsaddr"
|
2023-09-24 13:42:05 +02:00
|
|
|
"tailscale.com/tailcfg"
|
|
|
|
|
"tailscale.com/types/key"
|
2025-07-05 23:31:13 +02:00
|
|
|
"tailscale.com/types/views"
|
2023-09-24 13:42:05 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var (
|
2026-02-05 16:29:54 +00:00
|
|
|
ErrNodeAddressesInvalid = errors.New("parsing node addresses")
|
|
|
|
|
ErrHostnameTooLong = errors.New("hostname too long, cannot accept more than 255 ASCII chars")
|
2023-12-09 18:09:24 +01:00
|
|
|
ErrNodeHasNoGivenName = errors.New("node has no given name")
|
|
|
|
|
ErrNodeUserHasNoName = errors.New("node user has no name")
|
2025-12-02 12:01:25 +01:00
|
|
|
ErrCannotRemoveAllTags = errors.New("cannot remove all tags from node")
|
2025-12-10 09:16:22 +01:00
|
|
|
ErrInvalidNodeView = errors.New("cannot convert invalid NodeView to tailcfg.Node")
|
2023-09-24 13:42:05 +02:00
|
|
|
)
|
|
|
|
|
|
2025-12-10 09:16:22 +01:00
|
|
|
// RouteFunc is a function that takes a node ID and returns a list of
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// [netip.Prefix] values representing the routes for that node.
|
2025-12-10 09:16:22 +01:00
|
|
|
type RouteFunc func(id NodeID) []netip.Prefix
|
|
|
|
|
|
2026-05-13 09:58:11 +00:00
|
|
|
// nodeAttrDisableIPv4 is the policy nodeAttr key that suppresses the
|
|
|
|
|
// node's own IPv4 CGNAT prefix in [tailcfg.Node.Addresses] and
|
|
|
|
|
// [tailcfg.Node.AllowedIPs]. Subnet routes the node advertises remain.
|
|
|
|
|
// See https://tailscale.com/docs/reference/troubleshooting/network-configuration/cgnat-conflicts.
|
|
|
|
|
const nodeAttrDisableIPv4 tailcfg.NodeCapability = "disable-ipv4"
|
|
|
|
|
|
|
|
|
|
// filterIPv4 returns ps with every IPv4 prefix dropped. Used by
|
|
|
|
|
// [NodeView.TailNode] when the node carries the disable-ipv4 nodeAttr.
|
|
|
|
|
func filterIPv4(ps []netip.Prefix) []netip.Prefix {
|
|
|
|
|
out := ps[:0:0]
|
|
|
|
|
for _, p := range ps {
|
|
|
|
|
if p.Addr().Is4() {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
out = append(out, p)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
policy/v2,state,mapper: implement per-viewer via route steering
Via grants steer routes to specific nodes per viewer. Until now,
all clients saw the same routes for each peer because route
assembly was viewer-independent. This implements per-viewer route
visibility so that via-designated peers serve routes only to
matching viewers, while non-designated peers have those routes
withdrawn.
Add ViaRouteResult type (Include/Exclude prefix lists) and
ViaRoutesForPeer to the PolicyManager interface. The v2
implementation iterates via grants, resolves sources against the
viewer, matches destinations against the peer's advertised routes
(both subnet and exit), and categorizes prefixes by whether the
peer has the via tag.
Add RoutesForPeer to State which composes global primary election,
via Include/Exclude filtering, exit routes, and ACL reduction.
When no via grants exist, it falls back to existing behavior.
Update the mapper to call RoutesForPeer per-peer instead of using
a single route function for all peers. The route function now
returns all routes (subnet + exit), and TailNode filters exit
routes out of the PrimaryRoutes field for HA tracking.
Updates #2180
2026-03-22 20:43:28 +00:00
|
|
|
// ViaRouteResult describes via grant effects for a viewer-peer pair.
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// [ViaRouteResult.UsePrimary] is always a subset of [ViaRouteResult.Include]: it marks which included
|
2026-04-15 08:27:07 +00:00
|
|
|
// prefixes must additionally defer to HA primary election.
|
policy/v2,state,mapper: implement per-viewer via route steering
Via grants steer routes to specific nodes per viewer. Until now,
all clients saw the same routes for each peer because route
assembly was viewer-independent. This implements per-viewer route
visibility so that via-designated peers serve routes only to
matching viewers, while non-designated peers have those routes
withdrawn.
Add ViaRouteResult type (Include/Exclude prefix lists) and
ViaRoutesForPeer to the PolicyManager interface. The v2
implementation iterates via grants, resolves sources against the
viewer, matches destinations against the peer's advertised routes
(both subnet and exit), and categorizes prefixes by whether the
peer has the via tag.
Add RoutesForPeer to State which composes global primary election,
via Include/Exclude filtering, exit routes, and ACL reduction.
When no via grants exist, it falls back to existing behavior.
Update the mapper to call RoutesForPeer per-peer instead of using
a single route function for all peers. The route function now
returns all routes (subnet + exit), and TailNode filters exit
routes out of the PrimaryRoutes field for HA tracking.
Updates #2180
2026-03-22 20:43:28 +00:00
|
|
|
type ViaRouteResult struct {
|
|
|
|
|
// Include contains prefixes this peer should serve to this viewer (via-designated).
|
|
|
|
|
Include []netip.Prefix
|
|
|
|
|
// Exclude contains prefixes steered to OTHER peers (suppress from global primary).
|
|
|
|
|
Exclude []netip.Prefix
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// UsePrimary contains prefixes from [ViaRouteResult.Include] where a regular
|
2026-04-15 08:27:07 +00:00
|
|
|
// (non-via) grant also covers the prefix. In these cases HA
|
|
|
|
|
// primary election wins — only the primary router should get
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// the route in [tailcfg.Node.AllowedIPs]. When a prefix is NOT in [ViaRouteResult.UsePrimary],
|
2026-04-15 08:27:07 +00:00
|
|
|
// per-viewer via steering applies.
|
|
|
|
|
UsePrimary []netip.Prefix
|
policy/v2,state,mapper: implement per-viewer via route steering
Via grants steer routes to specific nodes per viewer. Until now,
all clients saw the same routes for each peer because route
assembly was viewer-independent. This implements per-viewer route
visibility so that via-designated peers serve routes only to
matching viewers, while non-designated peers have those routes
withdrawn.
Add ViaRouteResult type (Include/Exclude prefix lists) and
ViaRoutesForPeer to the PolicyManager interface. The v2
implementation iterates via grants, resolves sources against the
viewer, matches destinations against the peer's advertised routes
(both subnet and exit), and categorizes prefixes by whether the
peer has the via tag.
Add RoutesForPeer to State which composes global primary election,
via Include/Exclude filtering, exit routes, and ACL reduction.
When no via grants exist, it falls back to existing behavior.
Update the mapper to call RoutesForPeer per-peer instead of using
a single route function for all peers. The route function now
returns all routes (subnet + exit), and TailNode filters exit
routes out of the PrimaryRoutes field for HA tracking.
Updates #2180
2026-03-22 20:43:28 +00:00
|
|
|
}
|
|
|
|
|
|
2025-07-10 23:38:55 +02:00
|
|
|
type (
|
|
|
|
|
NodeID uint64
|
|
|
|
|
NodeIDs []NodeID
|
|
|
|
|
)
|
2024-04-21 18:28:17 +02:00
|
|
|
|
2025-02-26 07:22:55 -08:00
|
|
|
func (n NodeIDs) Len() int { return len(n) }
|
|
|
|
|
func (n NodeIDs) Less(i, j int) bool { return n[i] < n[j] }
|
|
|
|
|
func (n NodeIDs) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
|
2024-02-23 10:59:24 +01:00
|
|
|
|
|
|
|
|
func (id NodeID) StableID() tailcfg.StableNodeID {
|
|
|
|
|
return tailcfg.StableNodeID(strconv.FormatUint(uint64(id), util.Base10))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (id NodeID) NodeID() tailcfg.NodeID {
|
2026-02-06 21:45:32 +01:00
|
|
|
return tailcfg.NodeID(id) //nolint:gosec // NodeID is bounded
|
2024-02-23 10:59:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (id NodeID) Uint64() uint64 {
|
|
|
|
|
return uint64(id)
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-24 09:15:34 +01:00
|
|
|
func (id NodeID) String() string {
|
|
|
|
|
return strconv.FormatUint(id.Uint64(), util.Base10)
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-05 16:32:46 +02:00
|
|
|
func ParseNodeID(s string) (NodeID, error) {
|
|
|
|
|
id, err := strconv.ParseUint(s, util.Base10, 64)
|
|
|
|
|
return NodeID(id), err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func MustParseNodeID(s string) NodeID {
|
|
|
|
|
id, err := ParseNodeID(s)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return id
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
// Node is a Headscale client.
|
|
|
|
|
type Node struct {
|
2024-02-23 10:59:24 +01:00
|
|
|
ID NodeID `gorm:"primary_key"`
|
2023-11-19 22:37:04 +01:00
|
|
|
|
2024-10-02 11:41:58 +02:00
|
|
|
MachineKey key.MachinePublic `gorm:"serializer:text"`
|
|
|
|
|
NodeKey key.NodePublic `gorm:"serializer:text"`
|
|
|
|
|
DiscoKey key.DiscoPublic `gorm:"serializer:text"`
|
|
|
|
|
|
2026-05-13 09:53:01 +00:00
|
|
|
Endpoints AddrPorts `gorm:"serializer:json"`
|
2024-10-02 11:41:58 +02:00
|
|
|
|
2024-10-02 18:12:25 +02:00
|
|
|
Hostinfo *tailcfg.Hostinfo `gorm:"column:host_info;serializer:json"`
|
2024-10-02 11:41:58 +02:00
|
|
|
|
2024-10-02 18:12:25 +02:00
|
|
|
IPv4 *netip.Addr `gorm:"column:ipv4;serializer:text"`
|
|
|
|
|
IPv6 *netip.Addr `gorm:"column:ipv6;serializer:text"`
|
2023-09-24 13:42:05 +02:00
|
|
|
|
|
|
|
|
// Hostname represents the name given by the Tailscale
|
|
|
|
|
// client during registration
|
|
|
|
|
Hostname string
|
|
|
|
|
|
|
|
|
|
// Givenname represents either:
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// a DNS normalized version of [Node.Hostname]
|
|
|
|
|
// a valid name set by the [User]
|
2023-09-24 13:42:05 +02:00
|
|
|
//
|
|
|
|
|
// GivenName is the name used in all DNS related
|
|
|
|
|
// parts of headscale.
|
|
|
|
|
GivenName string `gorm:"type:varchar(63);unique_index"`
|
2025-12-02 12:01:25 +01:00
|
|
|
|
2026-02-20 09:27:11 +00:00
|
|
|
// UserID identifies the owning user for user-owned nodes.
|
|
|
|
|
// Nil for tagged nodes, which are owned by their tags.
|
2025-12-02 12:01:25 +01:00
|
|
|
UserID *uint
|
|
|
|
|
User *User `gorm:"constraint:OnDelete:CASCADE;"`
|
2023-09-24 13:42:05 +02:00
|
|
|
|
|
|
|
|
RegisterMethod string
|
|
|
|
|
|
2025-12-02 12:01:25 +01:00
|
|
|
// Tags is the definitive owner for tagged nodes.
|
|
|
|
|
// When non-empty, the node is "tagged" and tags define its identity.
|
|
|
|
|
// Empty for user-owned nodes.
|
|
|
|
|
// Tags cannot be removed once set (one-way transition).
|
2026-05-13 09:53:01 +00:00
|
|
|
Tags Strings `gorm:"column:tags;serializer:json"`
|
2023-09-24 13:42:05 +02:00
|
|
|
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// When a node has been created with a [PreAuthKey], we need to
|
2025-02-01 09:31:13 +00:00
|
|
|
// prevent the preauthkey from being deleted before the node.
|
|
|
|
|
// The preauthkey can define "tags" of the node so we need it
|
|
|
|
|
// around.
|
|
|
|
|
AuthKeyID *uint64 `sql:"DEFAULT:NULL"`
|
|
|
|
|
AuthKey *PreAuthKey
|
2023-09-24 13:42:05 +02:00
|
|
|
|
2025-02-26 07:22:55 -08:00
|
|
|
Expiry *time.Time
|
2023-09-24 13:42:05 +02:00
|
|
|
|
2025-02-26 07:22:55 -08:00
|
|
|
// LastSeen is when the node was last in contact with
|
|
|
|
|
// headscale. It is best effort and not persisted.
|
2025-05-10 10:49:08 +03:00
|
|
|
LastSeen *time.Time `gorm:"column:last_seen"`
|
2025-02-26 07:22:55 -08:00
|
|
|
|
|
|
|
|
// ApprovedRoutes is a list of routes that the node is allowed to announce
|
|
|
|
|
// as a subnet router. They are not necessarily the routes that the node
|
|
|
|
|
// announces at the moment.
|
|
|
|
|
// See [Node.Hostinfo]
|
2026-05-13 09:53:01 +00:00
|
|
|
ApprovedRoutes Prefixes `gorm:"column:approved_routes;serializer:json"`
|
2023-09-24 13:42:05 +02:00
|
|
|
|
|
|
|
|
CreatedAt time.Time
|
|
|
|
|
UpdatedAt time.Time
|
|
|
|
|
DeletedAt *time.Time
|
2023-12-09 18:09:24 +01:00
|
|
|
|
|
|
|
|
IsOnline *bool `gorm:"-"`
|
2026-04-28 12:40:57 +00:00
|
|
|
|
|
|
|
|
// Unhealthy excludes the node from primary route election while
|
|
|
|
|
// online. Written by the HA prober. Runtime-only.
|
|
|
|
|
Unhealthy bool `gorm:"-"`
|
|
|
|
|
|
|
|
|
|
// SessionEpoch identifies a poll session. Connect bumps it; a
|
|
|
|
|
// Disconnect carrying a stale value is dropped, so a deferred
|
|
|
|
|
// disconnect from a previous session cannot overwrite a newer
|
|
|
|
|
// Connect. Runtime-only.
|
|
|
|
|
SessionEpoch uint64 `gorm:"-"`
|
2023-09-24 13:42:05 +02:00
|
|
|
}
|
|
|
|
|
|
2025-02-26 07:22:55 -08:00
|
|
|
type Nodes []*Node
|
2023-09-24 13:42:05 +02:00
|
|
|
|
2025-07-05 23:31:13 +02:00
|
|
|
func (ns Nodes) ViewSlice() views.Slice[NodeView] {
|
|
|
|
|
vs := make([]NodeView, len(ns))
|
|
|
|
|
for i, n := range ns {
|
|
|
|
|
vs[i] = n.View()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return views.SliceOf(vs)
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-17 07:03:06 +02:00
|
|
|
// IsExpired returns whether the node registration has expired.
|
2026-02-06 21:45:32 +01:00
|
|
|
func (node *Node) IsExpired() bool {
|
2024-04-17 07:03:06 +02:00
|
|
|
// If Expiry is not set, the client has not indicated that
|
2025-02-05 16:10:18 +01:00
|
|
|
// it wants an expiry time, it is therefore considered
|
2024-04-17 07:03:06 +02:00
|
|
|
// to mean "not expired"
|
|
|
|
|
if node.Expiry == nil || node.Expiry.IsZero() {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2023-09-24 13:42:05 +02:00
|
|
|
|
2024-04-17 07:03:06 +02:00
|
|
|
return time.Since(*node.Expiry) > 0
|
|
|
|
|
}
|
2023-09-24 13:42:05 +02:00
|
|
|
|
2024-04-17 07:03:06 +02:00
|
|
|
// IsEphemeral returns if the node is registered as an Ephemeral node.
|
2026-04-17 21:34:56 +02:00
|
|
|
// https://tailscale.com/docs/features/ephemeral-nodes
|
2024-04-17 07:03:06 +02:00
|
|
|
func (node *Node) IsEphemeral() bool {
|
|
|
|
|
return node.AuthKey != nil && node.AuthKey.Ephemeral
|
2023-09-24 13:42:05 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:27:07 +00:00
|
|
|
// IPs returns the node's allocated Tailscale addresses. Order is
|
|
|
|
|
// deterministic: IPv4 (if allocated) first, IPv6 second. At most one
|
|
|
|
|
// of each family.
|
2024-04-17 07:03:06 +02:00
|
|
|
func (node *Node) IPs() []netip.Addr {
|
|
|
|
|
var ret []netip.Addr
|
|
|
|
|
|
|
|
|
|
if node.IPv4 != nil {
|
|
|
|
|
ret = append(ret, *node.IPv4)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if node.IPv6 != nil {
|
|
|
|
|
ret = append(ret, *node.IPv6)
|
2023-09-24 13:42:05 +02:00
|
|
|
}
|
|
|
|
|
|
2024-04-17 07:03:06 +02:00
|
|
|
return ret
|
2023-09-24 13:42:05 +02:00
|
|
|
}
|
|
|
|
|
|
2025-03-10 16:20:29 +01:00
|
|
|
// HasIP reports if a node has a given IP address.
|
|
|
|
|
func (node *Node) HasIP(i netip.Addr) bool {
|
|
|
|
|
for _, ip := range node.IPs() {
|
|
|
|
|
if ip.Compare(i) == 0 {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-10 23:38:55 +02:00
|
|
|
|
2025-03-10 16:20:29 +01:00
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 12:01:25 +01:00
|
|
|
// IsTagged reports if a device is tagged and therefore should not be treated
|
|
|
|
|
// as a user-owned device.
|
|
|
|
|
// When a node has tags, the tags define its identity (not the user).
|
2025-03-10 16:20:29 +01:00
|
|
|
func (node *Node) IsTagged() bool {
|
2025-12-02 12:01:25 +01:00
|
|
|
return len(node.Tags) > 0
|
|
|
|
|
}
|
2025-03-10 16:20:29 +01:00
|
|
|
|
2025-12-02 12:01:25 +01:00
|
|
|
// IsUserOwned returns true if node is owned by a user (not tagged).
|
|
|
|
|
// Tagged nodes may have a UserID for "created by" tracking, but the tag is the owner.
|
|
|
|
|
func (node *Node) IsUserOwned() bool {
|
|
|
|
|
return !node.IsTagged()
|
2025-03-10 16:20:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HasTag reports if a node has a given tag.
|
|
|
|
|
func (node *Node) HasTag(tag string) bool {
|
2025-12-02 12:01:25 +01:00
|
|
|
return slices.Contains(node.Tags, tag)
|
2025-04-30 08:54:04 +03:00
|
|
|
}
|
2025-03-10 16:20:29 +01:00
|
|
|
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// TypedUserID returns the [Node.UserID] as a typed [UserID] type.
|
|
|
|
|
// Returns 0 if [Node.UserID] is nil.
|
2025-12-02 12:01:25 +01:00
|
|
|
func (node *Node) TypedUserID() UserID {
|
|
|
|
|
if node.UserID == nil {
|
|
|
|
|
return 0
|
2025-03-10 16:20:29 +01:00
|
|
|
}
|
|
|
|
|
|
2025-12-02 12:01:25 +01:00
|
|
|
return UserID(*node.UserID)
|
2025-03-10 16:20:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (node *Node) RequestTags() []string {
|
|
|
|
|
if node.Hostinfo == nil {
|
|
|
|
|
return []string{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return node.Hostinfo.RequestTags
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-17 07:03:06 +02:00
|
|
|
func (node *Node) Prefixes() []netip.Prefix {
|
2026-02-06 21:45:32 +01:00
|
|
|
ips := node.IPs()
|
|
|
|
|
if len(ips) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
addrs := make([]netip.Prefix, 0, len(ips))
|
|
|
|
|
|
|
|
|
|
for _, nodeAddress := range ips {
|
2023-09-24 13:42:05 +02:00
|
|
|
ip := netip.PrefixFrom(nodeAddress, nodeAddress.BitLen())
|
|
|
|
|
addrs = append(addrs, ip)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return addrs
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:27:07 +00:00
|
|
|
// ExitRoutes returns the node's approved exit routes (0.0.0.0/0
|
|
|
|
|
// and/or ::/0). Consumed unconditionally by RoutesForPeer when the
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// viewer uses an exit node; excluded from [Node.CanAccessRoute] which only
|
2026-04-15 08:27:07 +00:00
|
|
|
// handles non-exit routing.
|
2025-03-21 11:49:32 +01:00
|
|
|
func (node *Node) ExitRoutes() []netip.Prefix {
|
2025-11-01 14:25:07 +01:00
|
|
|
var routes []netip.Prefix
|
|
|
|
|
|
|
|
|
|
for _, route := range node.AnnouncedRoutes() {
|
|
|
|
|
if tsaddr.IsExitRoute(route) && slices.Contains(node.ApprovedRoutes, route) {
|
|
|
|
|
routes = append(routes, route)
|
|
|
|
|
}
|
2025-03-21 11:49:32 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-01 14:25:07 +01:00
|
|
|
return routes
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:27:07 +00:00
|
|
|
// IsExitNode reports whether the node has any approved exit routes.
|
|
|
|
|
// Approval is required: an advertised-but-unapproved exit route does
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// not make the node an exit node.
|
2025-11-01 14:25:07 +01:00
|
|
|
func (node *Node) IsExitNode() bool {
|
|
|
|
|
return len(node.ExitRoutes()) > 0
|
2025-03-21 11:49:32 +01:00
|
|
|
}
|
|
|
|
|
|
2024-04-17 07:03:06 +02:00
|
|
|
func (node *Node) IPsAsString() []string {
|
2026-02-06 21:45:32 +01:00
|
|
|
ips := node.IPs()
|
|
|
|
|
if len(ips) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2024-04-17 07:03:06 +02:00
|
|
|
|
2026-02-06 21:45:32 +01:00
|
|
|
ret := make([]string, 0, len(ips))
|
|
|
|
|
|
|
|
|
|
for _, ip := range ips {
|
2025-03-10 16:20:29 +01:00
|
|
|
ret = append(ret, ip.String())
|
2024-04-17 07:03:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ret
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (node *Node) InIPSet(set *netipx.IPSet) bool {
|
2025-03-28 13:22:15 +01:00
|
|
|
return slices.ContainsFunc(node.IPs(), set.Contains)
|
2023-09-24 13:42:05 +02:00
|
|
|
}
|
|
|
|
|
|
2026-03-18 10:24:30 +00:00
|
|
|
// AppendToIPSet adds all IP addresses of the node to the given
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// [netipx.IPSetBuilder]. For identity-based aliases (tags, users,
|
2026-03-18 10:24:30 +00:00
|
|
|
// groups, autogroups), both IPv4 and IPv6 must be included to
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// match Tailscale's behavior in the [tailcfg.FilterRule] wire format.
|
2024-04-17 07:03:06 +02:00
|
|
|
func (node *Node) AppendToIPSet(build *netipx.IPSetBuilder) {
|
2026-03-04 16:16:40 +01:00
|
|
|
if node.IPv4 != nil {
|
|
|
|
|
build.Add(*node.IPv4)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if node.IPv6 != nil {
|
|
|
|
|
build.Add(*node.IPv6)
|
2023-09-24 13:42:05 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:27:07 +00:00
|
|
|
// CanAccess reports whether node may reach node2 under the given
|
|
|
|
|
// matchers. A node owns two source identities for ACL purposes:
|
|
|
|
|
// - its own IPs (regular peer membership)
|
|
|
|
|
// - any approved subnet routes it advertises (subnet-router-as-src,
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// used for subnet-to-subnet ACLs)
|
2026-04-15 08:27:07 +00:00
|
|
|
//
|
|
|
|
|
// Either identity matching a rule's src — combined with the dst
|
|
|
|
|
// matching node2's IPs, node2's approved subnet routes, or "the
|
|
|
|
|
// internet" when node2 is an exit node — grants access.
|
2025-05-01 07:06:30 +02:00
|
|
|
func (node *Node) CanAccess(matchers []matcher.Match, node2 *Node) bool {
|
2024-04-17 07:03:06 +02:00
|
|
|
src := node.IPs()
|
|
|
|
|
allowedIPs := node2.IPs()
|
2026-04-15 08:27:07 +00:00
|
|
|
srcRoutes := node.SubnetRoutes()
|
|
|
|
|
dstRoutes := node2.SubnetRoutes()
|
|
|
|
|
dstIsExit := node2.IsExitNode()
|
2024-02-12 11:44:37 +01:00
|
|
|
|
2026-04-15 08:27:07 +00:00
|
|
|
for _, m := range matchers {
|
|
|
|
|
srcMatchesIP := m.SrcsContainsIPs(src...)
|
|
|
|
|
srcMatchesRoutes := len(srcRoutes) > 0 && m.SrcsOverlapsPrefixes(srcRoutes...)
|
|
|
|
|
|
|
|
|
|
if !srcMatchesIP && !srcMatchesRoutes {
|
2023-09-24 13:42:05 +02:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:27:07 +00:00
|
|
|
if m.DestsContainsIP(allowedIPs...) {
|
2025-02-26 07:22:55 -08:00
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:27:07 +00:00
|
|
|
if len(dstRoutes) > 0 && m.DestsOverlapsPrefixes(dstRoutes...) {
|
2023-09-24 13:42:05 +02:00
|
|
|
return true
|
|
|
|
|
}
|
2025-11-01 14:29:50 +01:00
|
|
|
|
2026-04-15 08:27:07 +00:00
|
|
|
if dstIsExit && m.DestsIsTheInternet() {
|
2025-11-01 14:29:50 +01:00
|
|
|
return true
|
|
|
|
|
}
|
2023-09-24 13:42:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:27:07 +00:00
|
|
|
// CanAccessRoute determines whether a specific route prefix should be
|
|
|
|
|
// visible to this node based on the given matchers.
|
|
|
|
|
//
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// Unlike [Node.CanAccess], this function intentionally does NOT check
|
|
|
|
|
// [matcher.Match.DestsIsTheInternet]. Exit routes (0.0.0.0/0, ::/0) are handled by
|
2026-04-15 08:27:07 +00:00
|
|
|
// RoutesForPeer (state.go) which adds them unconditionally from
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// [Node.ExitRoutes], not through ACL-based route filtering. The
|
|
|
|
|
// [matcher.Match.DestsIsTheInternet] check in [Node.CanAccess] exists solely for peer
|
2026-04-15 08:27:07 +00:00
|
|
|
// visibility determination (should two nodes see each other), which
|
|
|
|
|
// is a separate concern from route prefix authorization.
|
|
|
|
|
//
|
|
|
|
|
// Additionally, autogroup:internet is explicitly skipped during filter
|
|
|
|
|
// rule compilation (filter.go), so no matchers ever contain "the
|
|
|
|
|
// internet" from internet-targeted ACLs. Wildcard "*" dests produce
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// matchers where [matcher.Match.DestsOverlapsPrefixes](0.0.0.0/0) already returns
|
2026-04-15 08:27:07 +00:00
|
|
|
// true, so the check would be redundant for that case.
|
2025-05-04 22:52:47 +03:00
|
|
|
func (node *Node) CanAccessRoute(matchers []matcher.Match, route netip.Prefix) bool {
|
|
|
|
|
src := node.IPs()
|
2026-04-15 08:27:07 +00:00
|
|
|
subnetRoutes := node.SubnetRoutes()
|
2025-05-04 22:52:47 +03:00
|
|
|
|
|
|
|
|
for _, matcher := range matchers {
|
2025-09-12 11:47:51 +02:00
|
|
|
if matcher.SrcsContainsIPs(src...) && matcher.DestsOverlapsPrefixes(route) {
|
|
|
|
|
return true
|
2025-05-04 22:52:47 +03:00
|
|
|
}
|
|
|
|
|
|
2025-09-12 11:47:51 +02:00
|
|
|
if matcher.SrcsOverlapsPrefixes(route) && matcher.DestsContainsIP(src...) {
|
2025-05-04 22:52:47 +03:00
|
|
|
return true
|
|
|
|
|
}
|
2026-04-15 08:27:07 +00:00
|
|
|
|
|
|
|
|
// A subnet router acts on behalf of its advertised subnets.
|
|
|
|
|
// If the node's approved subnet routes overlap the source set
|
|
|
|
|
// and the route overlaps the destination set, the router needs
|
|
|
|
|
// this route to forward traffic from its local subnet.
|
|
|
|
|
if len(subnetRoutes) > 0 {
|
|
|
|
|
if matcher.SrcsOverlapsPrefixes(subnetRoutes...) &&
|
|
|
|
|
matcher.DestsOverlapsPrefixes(route) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reverse: traffic from the route's subnet is destined for
|
|
|
|
|
// this node's subnets; the router needs the route for return
|
|
|
|
|
// traffic.
|
|
|
|
|
if matcher.SrcsOverlapsPrefixes(route) &&
|
|
|
|
|
matcher.DestsOverlapsPrefixes(subnetRoutes...) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-05-04 22:52:47 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
func (nodes Nodes) FilterByIP(ip netip.Addr) Nodes {
|
2024-04-17 07:03:06 +02:00
|
|
|
var found Nodes
|
2023-09-24 13:42:05 +02:00
|
|
|
|
|
|
|
|
for _, node := range nodes {
|
2024-04-17 07:03:06 +02:00
|
|
|
if node.IPv4 != nil && ip == *node.IPv4 {
|
|
|
|
|
found = append(found, node)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if node.IPv6 != nil && ip == *node.IPv6 {
|
|
|
|
|
found = append(found, node)
|
2023-09-24 13:42:05 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return found
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-22 20:23:05 +08:00
|
|
|
func (nodes Nodes) ContainsNodeKey(nodeKey key.NodePublic) bool {
|
|
|
|
|
for _, node := range nodes {
|
|
|
|
|
if node.NodeKey == nodeKey {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
func (node *Node) Proto() *v1.Node {
|
|
|
|
|
nodeProto := &v1.Node{
|
2024-02-23 10:59:24 +01:00
|
|
|
Id: uint64(node.ID),
|
2023-11-19 22:37:04 +01:00
|
|
|
MachineKey: node.MachineKey.String(),
|
2023-09-24 13:42:05 +02:00
|
|
|
|
2024-04-17 07:03:06 +02:00
|
|
|
NodeKey: node.NodeKey.String(),
|
|
|
|
|
DiscoKey: node.DiscoKey.String(),
|
|
|
|
|
|
|
|
|
|
// TODO(kradalby): replace list with v4, v6 field?
|
2025-03-28 13:22:15 +01:00
|
|
|
IpAddresses: node.IPsAsString(),
|
|
|
|
|
Name: node.Hostname,
|
|
|
|
|
GivenName: node.GivenName,
|
2025-12-02 12:01:25 +01:00
|
|
|
User: nil, // Will be set below based on node type
|
2026-01-07 14:07:45 +01:00
|
|
|
Tags: node.Tags,
|
2025-07-05 23:30:47 +02:00
|
|
|
Online: node.IsOnline != nil && *node.IsOnline,
|
2025-03-28 13:22:15 +01:00
|
|
|
|
|
|
|
|
// Only ApprovedRoutes and AvailableRoutes is set here. SubnetRoutes has
|
|
|
|
|
// to be populated manually with PrimaryRoute, to ensure it includes the
|
|
|
|
|
// routes that are actively served from the node.
|
2025-02-26 07:22:55 -08:00
|
|
|
ApprovedRoutes: util.PrefixesToString(node.ApprovedRoutes),
|
|
|
|
|
AvailableRoutes: util.PrefixesToString(node.AnnouncedRoutes()),
|
2023-09-24 13:42:05 +02:00
|
|
|
|
2024-07-17 21:12:16 +10:00
|
|
|
RegisterMethod: node.RegisterMethodToV1Enum(),
|
2023-09-24 13:42:05 +02:00
|
|
|
|
|
|
|
|
CreatedAt: timestamppb.New(node.CreatedAt),
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 12:01:25 +01:00
|
|
|
// Set User field based on node ownership
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// Note: User will be set to [TaggedDevices] in the gRPC layer (grpcv1.go)
|
|
|
|
|
// for proper [tailcfg.MapResponse] formatting
|
2025-12-02 12:01:25 +01:00
|
|
|
if node.User != nil {
|
|
|
|
|
nodeProto.User = node.User.Proto()
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
if node.AuthKey != nil {
|
|
|
|
|
nodeProto.PreAuthKey = node.AuthKey.Proto()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if node.LastSeen != nil {
|
|
|
|
|
nodeProto.LastSeen = timestamppb.New(*node.LastSeen)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if node.Expiry != nil {
|
|
|
|
|
nodeProto.Expiry = timestamppb.New(*node.Expiry)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nodeProto
|
|
|
|
|
}
|
|
|
|
|
|
Redo OIDC configuration (#2020)
expand user, add claims to user
This commit expands the user table with additional fields that
can be retrieved from OIDC providers (and other places) and
uses this data in various tailscale response objects if it is
available.
This is the beginning of implementing
https://docs.google.com/document/d/1X85PMxIaVWDF6T_UPji3OeeUqVBcGj_uHRM5CI-AwlY/edit
trying to make OIDC more coherant and maintainable in addition
to giving the user a better experience and integration with a
provider.
remove usernames in magic dns, normalisation of emails
this commit removes the option to have usernames as part of MagicDNS
domains and headscale will now align with Tailscale, where there is a
root domain, and the machine name.
In addition, the various normalisation functions for dns names has been
made lighter not caring about username and special character that wont
occur.
Email are no longer normalised as part of the policy processing.
untagle oidc and regcache, use typed cache
This commits stops reusing the registration cache for oidc
purposes and switches the cache to be types and not use any
allowing the removal of a bunch of casting.
try to make reauth/register branches clearer in oidc
Currently there was a function that did a bunch of stuff,
finding the machine key, trying to find the node, reauthing
the node, returning some status, and it was called validate
which was very confusing.
This commit tries to split this into what to do if the node
exists, if it needs to register etc.
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-10-02 14:50:17 +02:00
|
|
|
func (node *Node) GetFQDN(baseDomain string) (string, error) {
|
2024-08-19 11:41:05 +02:00
|
|
|
if node.GivenName == "" {
|
2026-02-05 16:29:54 +00:00
|
|
|
return "", fmt.Errorf("creating valid FQDN: %w", ErrNodeHasNoGivenName)
|
2024-08-19 11:41:05 +02:00
|
|
|
}
|
2023-12-09 18:09:24 +01:00
|
|
|
|
2024-08-19 11:41:05 +02:00
|
|
|
hostname := node.GivenName
|
|
|
|
|
|
|
|
|
|
if baseDomain != "" {
|
2023-09-24 13:42:05 +02:00
|
|
|
hostname = fmt.Sprintf(
|
2025-04-11 12:39:08 +02:00
|
|
|
"%s.%s.",
|
2023-09-24 13:42:05 +02:00
|
|
|
node.GivenName,
|
|
|
|
|
baseDomain,
|
|
|
|
|
)
|
2024-08-19 11:41:05 +02:00
|
|
|
}
|
2024-06-26 13:44:40 +02:00
|
|
|
|
2024-08-19 11:41:05 +02:00
|
|
|
if len(hostname) > MaxHostnameLength {
|
|
|
|
|
return "", fmt.Errorf(
|
2026-02-05 16:29:54 +00:00
|
|
|
"creating valid FQDN (%s): %w",
|
2024-08-19 11:41:05 +02:00
|
|
|
hostname,
|
|
|
|
|
ErrHostnameTooLong,
|
|
|
|
|
)
|
2023-09-24 13:42:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return hostname, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:27:07 +00:00
|
|
|
// AnnouncedRoutes returns the list of routes the node announces, as
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// reported by the client in [tailcfg.Hostinfo.RoutableIPs]. Announcement alone
|
|
|
|
|
// does not grant visibility — see [Node.SubnetRoutes] for approval-gated
|
2026-04-15 08:27:07 +00:00
|
|
|
// access.
|
2025-02-26 07:22:55 -08:00
|
|
|
func (node *Node) AnnouncedRoutes() []netip.Prefix {
|
|
|
|
|
if node.Hostinfo == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return node.Hostinfo.RoutableIPs
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-01 14:25:07 +01:00
|
|
|
// SubnetRoutes returns the list of routes (excluding exit routes) that the node
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// announces and are approved. Also used by [Node.CanAccess] and [Node.CanAccessRoute] as part
|
|
|
|
|
// of the subnet-router-as-source identity.
|
2025-07-05 23:30:47 +02:00
|
|
|
//
|
2025-11-01 14:25:07 +01:00
|
|
|
// IMPORTANT: This method is used for internal data structures and should NOT be
|
|
|
|
|
// used for the gRPC Proto conversion. For Proto, SubnetRoutes must be populated
|
|
|
|
|
// manually with PrimaryRoutes to ensure it includes only routes actively served
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// by the node. See the comment in [Node.Proto] method and the implementation in
|
2025-11-01 14:25:07 +01:00
|
|
|
// grpcv1.go/nodesToProto.
|
2025-02-26 07:22:55 -08:00
|
|
|
func (node *Node) SubnetRoutes() []netip.Prefix {
|
|
|
|
|
var routes []netip.Prefix
|
|
|
|
|
|
|
|
|
|
for _, route := range node.AnnouncedRoutes() {
|
2025-11-01 14:25:07 +01:00
|
|
|
if tsaddr.IsExitRoute(route) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-26 07:22:55 -08:00
|
|
|
if slices.Contains(node.ApprovedRoutes, route) {
|
|
|
|
|
routes = append(routes, route)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return routes
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-28 11:15:53 +02:00
|
|
|
// IsSubnetRouter reports if the node has any subnet routes.
|
|
|
|
|
func (node *Node) IsSubnetRouter() bool {
|
|
|
|
|
return len(node.SubnetRoutes()) > 0
|
|
|
|
|
}
|
|
|
|
|
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// AllApprovedRoutes returns the combination of [Node.SubnetRoutes] and [Node.ExitRoutes].
|
2025-11-01 14:25:07 +01:00
|
|
|
func (node *Node) AllApprovedRoutes() []netip.Prefix {
|
|
|
|
|
return append(node.SubnetRoutes(), node.ExitRoutes()...)
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-10 16:20:29 +01:00
|
|
|
func (node *Node) String() string {
|
|
|
|
|
return node.Hostname
|
|
|
|
|
}
|
2023-12-09 18:09:24 +01:00
|
|
|
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// MarshalZerologObject implements [zerolog.LogObjectMarshaler] for safe logging.
|
|
|
|
|
// This method is used with [zerolog.Event.EmbedObject] for flat field embedding
|
|
|
|
|
// or [zerolog.Event.Object] for nested logging when multiple nodes are logged.
|
types: add MarshalZerologObject to domain types
Implement zerolog.LogObjectMarshaler interface on domain types
for structured logging:
- Node: logs node.id, node.name, machine.key (short), node.key (short),
node.is_tagged, node.expired, node.online, node.tags, user.name
- User: logs user.id, user.name, user.display, user.provider
- PreAuthKey: logs pak.id, pak.prefix (masked), pak.reusable,
pak.ephemeral, pak.used, pak.is_tagged, pak.tags
- APIKey: logs api_key.id, api_key.prefix (masked), api_key.expiration
Security: PreAuthKey and APIKey only log masked prefixes, never full
keys or hashes. Uses zf.* constants for consistent field naming.
2026-01-28 13:37:48 +00:00
|
|
|
func (node *Node) MarshalZerologObject(e *zerolog.Event) {
|
|
|
|
|
if node == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
e.Uint64(zf.NodeID, node.ID.Uint64())
|
|
|
|
|
e.Str(zf.NodeName, node.Hostname)
|
|
|
|
|
e.Str(zf.MachineKey, node.MachineKey.ShortString())
|
|
|
|
|
e.Str(zf.NodeKey, node.NodeKey.ShortString())
|
|
|
|
|
e.Bool(zf.NodeIsTagged, node.IsTagged())
|
|
|
|
|
e.Bool(zf.NodeExpired, node.IsExpired())
|
|
|
|
|
|
|
|
|
|
if node.IsOnline != nil {
|
|
|
|
|
e.Bool(zf.NodeOnline, *node.IsOnline)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(node.Tags) > 0 {
|
|
|
|
|
e.Strs(zf.NodeTags, node.Tags)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if node.User != nil {
|
|
|
|
|
e.Str(zf.UserName, node.User.Username())
|
|
|
|
|
} else if node.UserID != nil {
|
|
|
|
|
e.Uint(zf.UserID, *node.UserID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// PeerChangeFromMapRequest takes a [tailcfg.MapRequest] and compares it to the node
|
|
|
|
|
// to produce a [tailcfg.PeerChange] struct that can be used to updated the node and
|
2023-12-09 18:09:24 +01:00
|
|
|
// inform peers about smaller changes to the node.
|
|
|
|
|
// When a field is added to this function, remember to also add it to:
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// - [Node.ApplyPeerChange]
|
2024-01-16 16:04:03 +01:00
|
|
|
// - logTracePeerChange in poll.go.
|
2023-12-09 18:09:24 +01:00
|
|
|
func (node *Node) PeerChangeFromMapRequest(req tailcfg.MapRequest) tailcfg.PeerChange {
|
|
|
|
|
ret := tailcfg.PeerChange{
|
2026-02-06 21:45:32 +01:00
|
|
|
NodeID: tailcfg.NodeID(node.ID), //nolint:gosec // NodeID is bounded
|
2023-12-09 18:09:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if node.NodeKey.String() != req.NodeKey.String() {
|
|
|
|
|
ret.Key = &req.NodeKey
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if node.DiscoKey.String() != req.DiscoKey.String() {
|
|
|
|
|
ret.DiscoKey = &req.DiscoKey
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if node.Hostinfo != nil &&
|
|
|
|
|
node.Hostinfo.NetInfo != nil &&
|
|
|
|
|
req.Hostinfo != nil &&
|
|
|
|
|
req.Hostinfo.NetInfo != nil &&
|
|
|
|
|
node.Hostinfo.NetInfo.PreferredDERP != req.Hostinfo.NetInfo.PreferredDERP {
|
|
|
|
|
ret.DERPRegion = req.Hostinfo.NetInfo.PreferredDERP
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if req.Hostinfo != nil && req.Hostinfo.NetInfo != nil {
|
|
|
|
|
// If there is no stored Hostinfo or NetInfo, use
|
|
|
|
|
// the new PreferredDERP.
|
|
|
|
|
if node.Hostinfo == nil {
|
|
|
|
|
ret.DERPRegion = req.Hostinfo.NetInfo.PreferredDERP
|
|
|
|
|
} else if node.Hostinfo.NetInfo == nil {
|
|
|
|
|
ret.DERPRegion = req.Hostinfo.NetInfo.PreferredDERP
|
2026-02-06 21:45:32 +01:00
|
|
|
} else if node.Hostinfo.NetInfo.PreferredDERP != req.Hostinfo.NetInfo.PreferredDERP {
|
2023-12-09 18:09:24 +01:00
|
|
|
// If there is a PreferredDERP check if it has changed.
|
2026-02-06 21:45:32 +01:00
|
|
|
ret.DERPRegion = req.Hostinfo.NetInfo.PreferredDERP
|
2023-12-09 18:09:24 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-13 13:38:49 -06:00
|
|
|
// Compare endpoints using order-independent comparison
|
|
|
|
|
if EndpointsChanged(node.Endpoints, req.Endpoints) {
|
|
|
|
|
ret.Endpoints = req.Endpoints
|
|
|
|
|
}
|
2023-12-09 18:09:24 +01:00
|
|
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
ret.LastSeen = &now
|
|
|
|
|
|
|
|
|
|
return ret
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-13 13:38:49 -06:00
|
|
|
// EndpointsChanged compares two endpoint slices and returns true if they differ.
|
|
|
|
|
// The comparison is order-independent - endpoints are sorted before comparison.
|
|
|
|
|
func EndpointsChanged(oldEndpoints, newEndpoints []netip.AddrPort) bool {
|
|
|
|
|
if len(oldEndpoints) != len(newEndpoints) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(oldEndpoints) == 0 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Make copies to avoid modifying the original slices
|
|
|
|
|
oldCopy := slices.Clone(oldEndpoints)
|
|
|
|
|
newCopy := slices.Clone(newEndpoints)
|
|
|
|
|
|
|
|
|
|
// Sort both slices to enable order-independent comparison
|
2026-04-15 08:27:07 +00:00
|
|
|
slices.SortFunc(oldCopy, netip.AddrPort.Compare)
|
|
|
|
|
slices.SortFunc(newCopy, netip.AddrPort.Compare)
|
2025-11-13 13:38:49 -06:00
|
|
|
|
|
|
|
|
return !slices.Equal(oldCopy, newCopy)
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-17 21:12:16 +10:00
|
|
|
func (node *Node) RegisterMethodToV1Enum() v1.RegisterMethod {
|
|
|
|
|
switch node.RegisterMethod {
|
|
|
|
|
case "authkey":
|
|
|
|
|
return v1.RegisterMethod_REGISTER_METHOD_AUTH_KEY
|
|
|
|
|
case "oidc":
|
|
|
|
|
return v1.RegisterMethod_REGISTER_METHOD_OIDC
|
|
|
|
|
case "cli":
|
|
|
|
|
return v1.RegisterMethod_REGISTER_METHOD_CLI
|
|
|
|
|
default:
|
|
|
|
|
return v1.RegisterMethod_REGISTER_METHOD_UNSPECIFIED
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// ApplyPeerChange takes a [tailcfg.PeerChange] struct and updates the node.
|
2023-12-09 18:09:24 +01:00
|
|
|
func (node *Node) ApplyPeerChange(change *tailcfg.PeerChange) {
|
|
|
|
|
if change.Key != nil {
|
|
|
|
|
node.NodeKey = *change.Key
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if change.DiscoKey != nil {
|
|
|
|
|
node.DiscoKey = *change.DiscoKey
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if change.Online != nil {
|
|
|
|
|
node.IsOnline = change.Online
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if change.Endpoints != nil {
|
|
|
|
|
node.Endpoints = change.Endpoints
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// This might technically not be useful as we replace
|
|
|
|
|
// the whole hostinfo blob when it has changed.
|
|
|
|
|
if change.DERPRegion != 0 {
|
|
|
|
|
if node.Hostinfo == nil {
|
|
|
|
|
node.Hostinfo = &tailcfg.Hostinfo{
|
|
|
|
|
NetInfo: &tailcfg.NetInfo{
|
|
|
|
|
PreferredDERP: change.DERPRegion,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
} else if node.Hostinfo.NetInfo == nil {
|
|
|
|
|
node.Hostinfo.NetInfo = &tailcfg.NetInfo{
|
|
|
|
|
PreferredDERP: change.DERPRegion,
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
node.Hostinfo.NetInfo.PreferredDERP = change.DERPRegion
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
node.LastSeen = change.LastSeen
|
2023-09-24 13:42:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (nodes Nodes) String() string {
|
|
|
|
|
temp := make([]string, len(nodes))
|
|
|
|
|
|
|
|
|
|
for index, node := range nodes {
|
|
|
|
|
temp[index] = node.Hostname
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return fmt.Sprintf("[ %s ](%d)", strings.Join(temp, ", "), len(temp))
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-23 10:59:24 +01:00
|
|
|
func (nodes Nodes) IDMap() map[NodeID]*Node {
|
|
|
|
|
ret := map[NodeID]*Node{}
|
2023-09-24 13:42:05 +02:00
|
|
|
|
|
|
|
|
for _, node := range nodes {
|
|
|
|
|
ret[node.ID] = node
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ret
|
|
|
|
|
}
|
2025-04-30 08:54:04 +03:00
|
|
|
|
|
|
|
|
func (nodes Nodes) DebugString() string {
|
|
|
|
|
var sb strings.Builder
|
|
|
|
|
sb.WriteString("Nodes:\n")
|
2026-02-06 21:45:32 +01:00
|
|
|
|
2025-04-30 08:54:04 +03:00
|
|
|
for _, node := range nodes {
|
|
|
|
|
sb.WriteString(node.DebugString())
|
|
|
|
|
sb.WriteString("\n")
|
|
|
|
|
}
|
2025-07-10 23:38:55 +02:00
|
|
|
|
2025-04-30 08:54:04 +03:00
|
|
|
return sb.String()
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 21:45:32 +01:00
|
|
|
func (node *Node) DebugString() string {
|
2025-04-30 08:54:04 +03:00
|
|
|
var sb strings.Builder
|
|
|
|
|
fmt.Fprintf(&sb, "%s(%s):\n", node.Hostname, node.ID)
|
2025-12-02 12:01:25 +01:00
|
|
|
|
|
|
|
|
// Show ownership status
|
|
|
|
|
if node.IsTagged() {
|
|
|
|
|
fmt.Fprintf(&sb, "\tTagged: %v\n", node.Tags)
|
|
|
|
|
|
|
|
|
|
if node.User != nil {
|
|
|
|
|
fmt.Fprintf(&sb, "\tCreated by: %s (%d, %q)\n", node.User.Display(), node.User.ID, node.User.Username())
|
|
|
|
|
}
|
|
|
|
|
} else if node.User != nil {
|
|
|
|
|
fmt.Fprintf(&sb, "\tUser-owned: %s (%d, %q)\n", node.User.Display(), node.User.ID, node.User.Username())
|
|
|
|
|
} else {
|
|
|
|
|
fmt.Fprintf(&sb, "\tOrphaned: no user or tags\n")
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-30 08:54:04 +03:00
|
|
|
fmt.Fprintf(&sb, "\tIPs: %v\n", node.IPs())
|
|
|
|
|
fmt.Fprintf(&sb, "\tApprovedRoutes: %v\n", node.ApprovedRoutes)
|
2025-05-04 22:52:47 +03:00
|
|
|
fmt.Fprintf(&sb, "\tAnnouncedRoutes: %v\n", node.AnnouncedRoutes())
|
2025-04-30 08:54:04 +03:00
|
|
|
fmt.Fprintf(&sb, "\tSubnetRoutes: %v\n", node.SubnetRoutes())
|
2025-11-01 14:25:07 +01:00
|
|
|
fmt.Fprintf(&sb, "\tExitRoutes: %v\n", node.ExitRoutes())
|
2025-04-30 08:54:04 +03:00
|
|
|
sb.WriteString("\n")
|
2025-07-10 23:38:55 +02:00
|
|
|
|
2025-04-30 08:54:04 +03:00
|
|
|
return sb.String()
|
|
|
|
|
}
|
2025-07-05 23:31:13 +02:00
|
|
|
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// MarshalZerologObject implements [zerolog.LogObjectMarshaler] for [NodeView].
|
|
|
|
|
// This delegates to the underlying [Node]'s implementation.
|
types: add MarshalZerologObject to domain types
Implement zerolog.LogObjectMarshaler interface on domain types
for structured logging:
- Node: logs node.id, node.name, machine.key (short), node.key (short),
node.is_tagged, node.expired, node.online, node.tags, user.name
- User: logs user.id, user.name, user.display, user.provider
- PreAuthKey: logs pak.id, pak.prefix (masked), pak.reusable,
pak.ephemeral, pak.used, pak.is_tagged, pak.tags
- APIKey: logs api_key.id, api_key.prefix (masked), api_key.expiration
Security: PreAuthKey and APIKey only log masked prefixes, never full
keys or hashes. Uses zf.* constants for consistent field naming.
2026-01-28 13:37:48 +00:00
|
|
|
func (nv NodeView) MarshalZerologObject(e *zerolog.Event) {
|
|
|
|
|
if !nv.Valid() {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nv.ж.MarshalZerologObject(e)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-09 16:31:23 +01:00
|
|
|
// Owner returns the owner for display purposes.
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// For tagged nodes, returns [TaggedDevices]. For user-owned nodes, returns the user.
|
|
|
|
|
// Returns an invalid [UserView] if the node is in an orphaned state (no tags, no user).
|
2026-04-08 12:25:50 +00:00
|
|
|
// Callers should check .Valid() on the result before accessing fields.
|
2026-01-09 16:31:23 +01:00
|
|
|
func (nv NodeView) Owner() UserView {
|
|
|
|
|
if nv.IsTagged() {
|
|
|
|
|
return TaggedDevices.View()
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 12:25:50 +00:00
|
|
|
if user := nv.User(); user.Valid() {
|
|
|
|
|
return user
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return UserView{}
|
2025-10-16 12:17:43 +02:00
|
|
|
}
|
|
|
|
|
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) IPs() []netip.Addr {
|
|
|
|
|
if !nv.Valid() {
|
2025-07-05 23:31:13 +02:00
|
|
|
return nil
|
|
|
|
|
}
|
2025-12-10 09:16:22 +01:00
|
|
|
|
|
|
|
|
return nv.ж.IPs()
|
2025-07-05 23:31:13 +02:00
|
|
|
}
|
|
|
|
|
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) InIPSet(set *netipx.IPSet) bool {
|
|
|
|
|
if !nv.Valid() {
|
2025-07-05 23:31:13 +02:00
|
|
|
return false
|
|
|
|
|
}
|
2025-12-10 09:16:22 +01:00
|
|
|
|
|
|
|
|
return nv.ж.InIPSet(set)
|
2025-07-05 23:31:13 +02:00
|
|
|
}
|
|
|
|
|
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) CanAccess(matchers []matcher.Match, node2 NodeView) bool {
|
2026-02-26 08:39:04 +03:00
|
|
|
if !nv.Valid() || !node2.Valid() {
|
2025-07-05 23:31:13 +02:00
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 08:39:04 +03:00
|
|
|
return nv.ж.CanAccess(matchers, node2.ж)
|
2025-07-05 23:31:13 +02:00
|
|
|
}
|
|
|
|
|
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) CanAccessRoute(matchers []matcher.Match, route netip.Prefix) bool {
|
|
|
|
|
if !nv.Valid() {
|
2025-07-05 23:31:13 +02:00
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 09:16:22 +01:00
|
|
|
return nv.ж.CanAccessRoute(matchers, route)
|
2025-07-05 23:31:13 +02:00
|
|
|
}
|
|
|
|
|
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) AnnouncedRoutes() []netip.Prefix {
|
|
|
|
|
if !nv.Valid() {
|
2025-07-05 23:31:13 +02:00
|
|
|
return nil
|
|
|
|
|
}
|
2025-12-10 09:16:22 +01:00
|
|
|
|
|
|
|
|
return nv.ж.AnnouncedRoutes()
|
2025-07-05 23:31:13 +02:00
|
|
|
}
|
|
|
|
|
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) SubnetRoutes() []netip.Prefix {
|
|
|
|
|
if !nv.Valid() {
|
2025-07-05 23:31:13 +02:00
|
|
|
return nil
|
|
|
|
|
}
|
2025-12-10 09:16:22 +01:00
|
|
|
|
|
|
|
|
return nv.ж.SubnetRoutes()
|
2025-07-05 23:31:13 +02:00
|
|
|
}
|
|
|
|
|
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) IsSubnetRouter() bool {
|
|
|
|
|
if !nv.Valid() {
|
2025-07-28 11:15:53 +02:00
|
|
|
return false
|
|
|
|
|
}
|
2025-12-10 09:16:22 +01:00
|
|
|
|
|
|
|
|
return nv.ж.IsSubnetRouter()
|
2025-07-28 11:15:53 +02:00
|
|
|
}
|
|
|
|
|
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) AllApprovedRoutes() []netip.Prefix {
|
|
|
|
|
if !nv.Valid() {
|
2025-11-01 14:25:07 +01:00
|
|
|
return nil
|
|
|
|
|
}
|
2025-12-10 09:16:22 +01:00
|
|
|
|
|
|
|
|
return nv.ж.AllApprovedRoutes()
|
2025-11-01 14:25:07 +01:00
|
|
|
}
|
|
|
|
|
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) AppendToIPSet(build *netipx.IPSetBuilder) {
|
|
|
|
|
if !nv.Valid() {
|
2025-07-05 23:31:13 +02:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-10 09:16:22 +01:00
|
|
|
|
|
|
|
|
nv.ж.AppendToIPSet(build)
|
2025-07-05 23:31:13 +02:00
|
|
|
}
|
|
|
|
|
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) RequestTagsSlice() views.Slice[string] {
|
|
|
|
|
if !nv.Valid() || !nv.Hostinfo().Valid() {
|
2025-07-05 23:31:13 +02:00
|
|
|
return views.Slice[string]{}
|
|
|
|
|
}
|
2025-12-10 09:16:22 +01:00
|
|
|
|
|
|
|
|
return nv.Hostinfo().RequestTags()
|
2025-07-05 23:31:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IsTagged reports if a device is tagged
|
|
|
|
|
// and therefore should not be treated as a
|
|
|
|
|
// user owned device.
|
|
|
|
|
// Currently, this function only handles tags set
|
2025-07-10 23:38:55 +02:00
|
|
|
// via CLI ("forced tags" and preauthkeys).
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) IsTagged() bool {
|
|
|
|
|
if !nv.Valid() {
|
2025-07-05 23:31:13 +02:00
|
|
|
return false
|
|
|
|
|
}
|
2025-12-10 09:16:22 +01:00
|
|
|
|
|
|
|
|
return nv.ж.IsTagged()
|
2025-07-05 23:31:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IsExpired returns whether the node registration has expired.
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) IsExpired() bool {
|
|
|
|
|
if !nv.Valid() {
|
2025-07-05 23:31:13 +02:00
|
|
|
return true
|
|
|
|
|
}
|
2025-12-10 09:16:22 +01:00
|
|
|
|
|
|
|
|
return nv.ж.IsExpired()
|
2025-07-05 23:31:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IsEphemeral returns if the node is registered as an Ephemeral node.
|
2026-04-17 21:34:56 +02:00
|
|
|
// https://tailscale.com/docs/features/ephemeral-nodes
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) IsEphemeral() bool {
|
|
|
|
|
if !nv.Valid() {
|
2025-07-05 23:31:13 +02:00
|
|
|
return false
|
|
|
|
|
}
|
2025-12-10 09:16:22 +01:00
|
|
|
|
|
|
|
|
return nv.ж.IsEphemeral()
|
2025-07-05 23:31:13 +02:00
|
|
|
}
|
|
|
|
|
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// PeerChangeFromMapRequest takes a [tailcfg.MapRequest] and compares it to the node
|
|
|
|
|
// to produce a [tailcfg.PeerChange] struct that can be used to updated the node and
|
2025-07-05 23:31:13 +02:00
|
|
|
// inform peers about smaller changes to the node.
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) PeerChangeFromMapRequest(req tailcfg.MapRequest) tailcfg.PeerChange {
|
|
|
|
|
if !nv.Valid() {
|
2025-07-05 23:31:13 +02:00
|
|
|
return tailcfg.PeerChange{}
|
|
|
|
|
}
|
2025-12-10 09:16:22 +01:00
|
|
|
|
|
|
|
|
return nv.ж.PeerChangeFromMapRequest(req)
|
2025-07-05 23:31:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetFQDN returns the fully qualified domain name for the node.
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) GetFQDN(baseDomain string) (string, error) {
|
|
|
|
|
if !nv.Valid() {
|
2026-02-06 21:45:32 +01:00
|
|
|
return "", fmt.Errorf("creating valid FQDN: %w", ErrInvalidNodeView)
|
2025-07-05 23:31:13 +02:00
|
|
|
}
|
2025-12-10 09:16:22 +01:00
|
|
|
|
|
|
|
|
return nv.ж.GetFQDN(baseDomain)
|
2025-07-05 23:31:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ExitRoutes returns a list of both exit routes if the
|
|
|
|
|
// node has any exit routes enabled.
|
|
|
|
|
// If none are enabled, it will return nil.
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) ExitRoutes() []netip.Prefix {
|
|
|
|
|
if !nv.Valid() {
|
2025-07-05 23:31:13 +02:00
|
|
|
return nil
|
|
|
|
|
}
|
2025-12-10 09:16:22 +01:00
|
|
|
|
|
|
|
|
return nv.ж.ExitRoutes()
|
2025-07-05 23:31:13 +02:00
|
|
|
}
|
|
|
|
|
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) IsExitNode() bool {
|
|
|
|
|
if !nv.Valid() {
|
2025-11-01 14:25:07 +01:00
|
|
|
return false
|
|
|
|
|
}
|
2025-12-10 09:16:22 +01:00
|
|
|
|
|
|
|
|
return nv.ж.IsExitNode()
|
2025-11-01 14:25:07 +01:00
|
|
|
}
|
|
|
|
|
|
2025-07-05 23:30:47 +02:00
|
|
|
// RequestTags returns the ACL tags that the node is requesting.
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) RequestTags() []string {
|
|
|
|
|
if !nv.Valid() || !nv.Hostinfo().Valid() {
|
2025-07-05 23:30:47 +02:00
|
|
|
return []string{}
|
|
|
|
|
}
|
2025-12-10 09:16:22 +01:00
|
|
|
|
|
|
|
|
return nv.Hostinfo().RequestTags().AsSlice()
|
2025-07-05 23:30:47 +02:00
|
|
|
}
|
|
|
|
|
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// Proto converts the [NodeView] to a protobuf representation.
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) Proto() *v1.Node {
|
|
|
|
|
if !nv.Valid() {
|
2025-07-05 23:30:47 +02:00
|
|
|
return nil
|
|
|
|
|
}
|
2025-12-10 09:16:22 +01:00
|
|
|
|
|
|
|
|
return nv.ж.Proto()
|
2025-07-05 23:30:47 +02:00
|
|
|
}
|
|
|
|
|
|
2025-07-05 23:31:13 +02:00
|
|
|
// HasIP reports if a node has a given IP address.
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) HasIP(i netip.Addr) bool {
|
|
|
|
|
if !nv.Valid() {
|
2025-07-05 23:31:13 +02:00
|
|
|
return false
|
|
|
|
|
}
|
2025-12-10 09:16:22 +01:00
|
|
|
|
|
|
|
|
return nv.ж.HasIP(i)
|
2025-07-05 23:31:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HasTag reports if a node has a given tag.
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) HasTag(tag string) bool {
|
|
|
|
|
if !nv.Valid() {
|
2025-07-05 23:31:13 +02:00
|
|
|
return false
|
|
|
|
|
}
|
2025-12-10 09:16:22 +01:00
|
|
|
|
|
|
|
|
return nv.ж.HasTag(tag)
|
2025-07-05 23:31:13 +02:00
|
|
|
}
|
|
|
|
|
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// TypedUserID returns the [Node.UserID] as a typed [UserID] type.
|
|
|
|
|
// Returns 0 if [Node.UserID] is nil or node is invalid.
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) TypedUserID() UserID {
|
|
|
|
|
if !nv.Valid() {
|
2025-12-02 12:01:25 +01:00
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 09:16:22 +01:00
|
|
|
return nv.ж.TypedUserID()
|
2025-12-02 12:01:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TailscaleUserID returns the user ID to use in Tailscale protocol.
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// Tagged nodes always return [TaggedDevices].ID, user-owned nodes return their actual [Node.UserID].
|
|
|
|
|
// Returns 0 for nodes in an orphaned state (no tags, no [Node.UserID]).
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) TailscaleUserID() tailcfg.UserID {
|
|
|
|
|
if !nv.Valid() {
|
2025-12-02 12:01:25 +01:00
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 09:16:22 +01:00
|
|
|
if nv.IsTagged() {
|
2025-12-02 12:01:25 +01:00
|
|
|
//nolint:gosec // G115: TaggedDevices.ID is a constant that fits in int64
|
|
|
|
|
return tailcfg.UserID(int64(TaggedDevices.ID))
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 12:25:50 +00:00
|
|
|
if !nv.UserID().Valid() {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 12:01:25 +01:00
|
|
|
//nolint:gosec // G115: UserID values are within int64 range
|
2025-12-10 09:16:22 +01:00
|
|
|
return tailcfg.UserID(int64(nv.UserID().Get()))
|
2025-12-02 12:01:25 +01:00
|
|
|
}
|
|
|
|
|
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// Prefixes returns the node IPs as [netip.Prefix].
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) Prefixes() []netip.Prefix {
|
|
|
|
|
if !nv.Valid() {
|
2025-07-05 23:31:13 +02:00
|
|
|
return nil
|
|
|
|
|
}
|
2025-12-10 09:16:22 +01:00
|
|
|
|
|
|
|
|
return nv.ж.Prefixes()
|
2025-07-05 23:31:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IPsAsString returns the node IPs as strings.
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) IPsAsString() []string {
|
|
|
|
|
if !nv.Valid() {
|
2025-07-05 23:31:13 +02:00
|
|
|
return nil
|
|
|
|
|
}
|
2025-12-10 09:16:22 +01:00
|
|
|
|
|
|
|
|
return nv.ж.IPsAsString()
|
2025-07-05 23:31:13 +02:00
|
|
|
}
|
2025-10-23 17:57:41 +02:00
|
|
|
|
|
|
|
|
// HasNetworkChanges checks if the node has network-related changes.
|
|
|
|
|
// Returns true if IPs, announced routes, or approved routes changed.
|
2026-04-15 08:27:07 +00:00
|
|
|
// This is primarily used for policy cache invalidation. Route slices
|
|
|
|
|
// are compared order-insensitively since clients may re-advertise the
|
|
|
|
|
// same routes in a different order.
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) HasNetworkChanges(other NodeView) bool {
|
|
|
|
|
if !slices.Equal(nv.IPs(), other.IPs()) {
|
2025-10-23 17:57:41 +02:00
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:27:07 +00:00
|
|
|
if !equalPrefixesUnordered(nv.AnnouncedRoutes(), other.AnnouncedRoutes()) {
|
2025-10-23 17:57:41 +02:00
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:27:07 +00:00
|
|
|
if !equalPrefixesUnordered(nv.SubnetRoutes(), other.SubnetRoutes()) {
|
2025-10-23 17:57:41 +02:00
|
|
|
return true
|
2025-12-15 14:33:36 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:27:07 +00:00
|
|
|
if !equalPrefixesUnordered(nv.ExitRoutes(), other.ExitRoutes()) {
|
2026-03-26 18:21:16 +00:00
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-15 14:33:36 +00:00
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:27:07 +00:00
|
|
|
// equalPrefixesUnordered reports whether a and b contain the same
|
|
|
|
|
// prefixes, order-independent. Inputs are cloned before sorting so
|
|
|
|
|
// callers' slices are not mutated.
|
|
|
|
|
func equalPrefixesUnordered(a, b []netip.Prefix) bool {
|
|
|
|
|
if len(a) != len(b) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ac := slices.Clone(a)
|
|
|
|
|
bc := slices.Clone(b)
|
|
|
|
|
|
|
|
|
|
slices.SortFunc(ac, netip.Prefix.Compare)
|
|
|
|
|
slices.SortFunc(bc, netip.Prefix.Compare)
|
|
|
|
|
|
|
|
|
|
return slices.Equal(ac, bc)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HasPolicyChange reports whether the node has changes that affect
|
|
|
|
|
// policy evaluation. Includes approved subnet routes because they act
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// as source identity in [Node.CanAccess] for subnet-to-subnet ACLs.
|
2025-12-15 14:33:36 +00:00
|
|
|
func (nv NodeView) HasPolicyChange(other NodeView) bool {
|
|
|
|
|
if nv.UserID() != other.UserID() {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !views.SliceEqual(nv.Tags(), other.Tags()) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !slices.Equal(nv.IPs(), other.IPs()) {
|
|
|
|
|
return true
|
2025-10-23 17:57:41 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-15 08:27:07 +00:00
|
|
|
if !equalPrefixesUnordered(nv.SubnetRoutes(), other.SubnetRoutes()) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-23 17:57:41 +02:00
|
|
|
return false
|
|
|
|
|
}
|
2025-12-10 09:16:22 +01:00
|
|
|
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// TailNodes converts a slice of [NodeView] values into Tailscale [tailcfg.Node] values.
|
2025-12-10 09:16:22 +01:00
|
|
|
func TailNodes(
|
|
|
|
|
nodes views.Slice[NodeView],
|
|
|
|
|
capVer tailcfg.CapabilityVersion,
|
|
|
|
|
primaryRouteFunc RouteFunc,
|
|
|
|
|
cfg *Config,
|
|
|
|
|
) ([]*tailcfg.Node, error) {
|
|
|
|
|
tNodes := make([]*tailcfg.Node, 0, nodes.Len())
|
|
|
|
|
|
|
|
|
|
for _, node := range nodes.All() {
|
2026-05-11 14:53:09 +00:00
|
|
|
// nil selfPolicyCaps: this batch builds peer views; the caller
|
|
|
|
|
// sets each peer's CapMap from [policyv2.PeerCapMap].
|
|
|
|
|
tNode, err := node.TailNode(capVer, primaryRouteFunc, cfg, nil)
|
2025-12-10 09:16:22 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tNodes = append(tNodes, tNode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return tNodes, nil
|
|
|
|
|
}
|
|
|
|
|
|
all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
2026-05-18 18:35:53 +00:00
|
|
|
// TailNode converts a [NodeView] into a Tailscale [tailcfg.Node].
|
2026-05-11 14:53:09 +00:00
|
|
|
//
|
|
|
|
|
// selfPolicyCaps is the per-node CapMap from [policy.PolicyManager.NodeCapMap]
|
|
|
|
|
// and is merged into the baseline. Pass it when building the self view of the
|
|
|
|
|
// requesting node; pass nil when building peer views (peer-side
|
|
|
|
|
// [tailcfg.Node.CapMap] is set by the caller from
|
|
|
|
|
// [policyv2.PeerCapMap]).
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) TailNode(
|
|
|
|
|
capVer tailcfg.CapabilityVersion,
|
|
|
|
|
primaryRouteFunc RouteFunc,
|
|
|
|
|
cfg *Config,
|
2026-05-11 14:53:09 +00:00
|
|
|
selfPolicyCaps tailcfg.NodeCapMap,
|
2025-12-10 09:16:22 +01:00
|
|
|
) (*tailcfg.Node, error) {
|
|
|
|
|
if !nv.Valid() {
|
|
|
|
|
return nil, ErrInvalidNodeView
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hostname, err := nv.GetFQDN(cfg.BaseDomain)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var derp int
|
|
|
|
|
if nv.Hostinfo().Valid() && nv.Hostinfo().NetInfo().Valid() {
|
|
|
|
|
derp = nv.Hostinfo().NetInfo().PreferredDERP()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var keyExpiry time.Time
|
|
|
|
|
if nv.Expiry().Valid() {
|
|
|
|
|
keyExpiry = nv.Expiry().Get()
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 09:58:11 +00:00
|
|
|
// disable-ipv4 (https://tailscale.com/docs/reference/troubleshooting/network-configuration/cgnat-conflicts)
|
|
|
|
|
// drops the node's own IPv4 CGNAT prefix from Addresses and from
|
|
|
|
|
// the AllowedIPs slot the node's own /32 occupies. Advertised
|
|
|
|
|
// subnet routes -- even IPv4 ones -- survive: routes belong to
|
|
|
|
|
// the routing layer, not the node's identity. Mirrors the SaaS
|
|
|
|
|
// captures in testdata/nodeattrs_results/nodeattrs-attr-c1{5,6}-disable-ipv4*.
|
|
|
|
|
_, ipv4Disabled := selfPolicyCaps[nodeAttrDisableIPv4]
|
|
|
|
|
|
|
|
|
|
addresses := nv.Prefixes()
|
|
|
|
|
if ipv4Disabled {
|
|
|
|
|
addresses = filterIPv4(addresses)
|
|
|
|
|
}
|
|
|
|
|
|
policy/v2,state,mapper: implement per-viewer via route steering
Via grants steer routes to specific nodes per viewer. Until now,
all clients saw the same routes for each peer because route
assembly was viewer-independent. This implements per-viewer route
visibility so that via-designated peers serve routes only to
matching viewers, while non-designated peers have those routes
withdrawn.
Add ViaRouteResult type (Include/Exclude prefix lists) and
ViaRoutesForPeer to the PolicyManager interface. The v2
implementation iterates via grants, resolves sources against the
viewer, matches destinations against the peer's advertised routes
(both subnet and exit), and categorizes prefixes by whether the
peer has the via tag.
Add RoutesForPeer to State which composes global primary election,
via Include/Exclude filtering, exit routes, and ACL reduction.
When no via grants exist, it falls back to existing behavior.
Update the mapper to call RoutesForPeer per-peer instead of using
a single route function for all peers. The route function now
returns all routes (subnet + exit), and TailNode filters exit
routes out of the PrimaryRoutes field for HA tracking.
Updates #2180
2026-03-22 20:43:28 +00:00
|
|
|
// routeFunc returns ALL routes (subnet + exit) for this node.
|
|
|
|
|
allRoutes := primaryRouteFunc(nv.ID())
|
2026-05-13 09:58:11 +00:00
|
|
|
allowedIPs := slices.Concat(addresses, allRoutes)
|
2026-02-06 21:39:35 +00:00
|
|
|
slices.SortFunc(allowedIPs, netip.Prefix.Compare)
|
2025-12-10 09:16:22 +01:00
|
|
|
|
policy/v2,state,mapper: implement per-viewer via route steering
Via grants steer routes to specific nodes per viewer. Until now,
all clients saw the same routes for each peer because route
assembly was viewer-independent. This implements per-viewer route
visibility so that via-designated peers serve routes only to
matching viewers, while non-designated peers have those routes
withdrawn.
Add ViaRouteResult type (Include/Exclude prefix lists) and
ViaRoutesForPeer to the PolicyManager interface. The v2
implementation iterates via grants, resolves sources against the
viewer, matches destinations against the peer's advertised routes
(both subnet and exit), and categorizes prefixes by whether the
peer has the via tag.
Add RoutesForPeer to State which composes global primary election,
via Include/Exclude filtering, exit routes, and ACL reduction.
When no via grants exist, it falls back to existing behavior.
Update the mapper to call RoutesForPeer per-peer instead of using
a single route function for all peers. The route function now
returns all routes (subnet + exit), and TailNode filters exit
routes out of the PrimaryRoutes field for HA tracking.
Updates #2180
2026-03-22 20:43:28 +00:00
|
|
|
// PrimaryRoutes only includes non-exit subnet routes for HA tracking.
|
|
|
|
|
var primaryRoutes []netip.Prefix
|
|
|
|
|
|
|
|
|
|
for _, r := range allRoutes {
|
|
|
|
|
if !tsaddr.IsExitRoute(r) {
|
|
|
|
|
primaryRoutes = append(primaryRoutes, r)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 14:54:03 +00:00
|
|
|
// Baseline caps every node receives, regardless of policy. Mirrors
|
|
|
|
|
// what Tailscale SaaS emits for a default tailnet.
|
|
|
|
|
// cfg.Taildrop.Enabled gates CapabilityFileSharing.
|
2025-12-10 09:16:22 +01:00
|
|
|
capMap := tailcfg.NodeCapMap{
|
2026-05-11 14:54:03 +00:00
|
|
|
tailcfg.CapabilityAdmin: []tailcfg.RawMessage{},
|
|
|
|
|
tailcfg.CapabilitySSH: []tailcfg.RawMessage{},
|
2025-12-10 09:16:22 +01:00
|
|
|
}
|
|
|
|
|
|
2025-12-12 11:35:16 +01:00
|
|
|
if cfg.Taildrop.Enabled {
|
|
|
|
|
capMap[tailcfg.CapabilityFileSharing] = []tailcfg.RawMessage{}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 16:28:09 +00:00
|
|
|
// default-auto-update is always emitted; the value is a JSON bool
|
|
|
|
|
// reflecting cfg.AutoUpdate.Enabled. Clients read this on first
|
|
|
|
|
// netmap and store the default locally; subsequent control-plane
|
|
|
|
|
// changes are ignored unless the client has not yet opted in or
|
|
|
|
|
// out.
|
|
|
|
|
autoUpdateVal := tailcfg.RawMessage("false")
|
|
|
|
|
if cfg.AutoUpdate.Enabled {
|
|
|
|
|
autoUpdateVal = tailcfg.RawMessage("true")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
capMap[tailcfg.NodeAttrDefaultAutoUpdate] = []tailcfg.RawMessage{autoUpdateVal}
|
|
|
|
|
|
2026-05-11 14:53:09 +00:00
|
|
|
// Policy nodeAttrs overlay the baseline on the self view. Peers
|
|
|
|
|
// pass nil; their CapMap is replaced downstream by [policyv2.PeerCapMap].
|
|
|
|
|
maps.Copy(capMap, selfPolicyCaps)
|
|
|
|
|
|
2025-12-10 09:16:22 +01:00
|
|
|
tNode := tailcfg.Node{
|
|
|
|
|
//nolint:gosec // G115: NodeID values are within int64 range
|
|
|
|
|
ID: tailcfg.NodeID(nv.ID()),
|
|
|
|
|
StableID: nv.ID().StableID(),
|
|
|
|
|
Name: hostname,
|
|
|
|
|
Cap: capVer,
|
|
|
|
|
CapMap: capMap,
|
|
|
|
|
|
|
|
|
|
User: nv.TailscaleUserID(),
|
|
|
|
|
|
|
|
|
|
Key: nv.NodeKey(),
|
|
|
|
|
KeyExpiry: keyExpiry.UTC(),
|
|
|
|
|
|
2026-05-22 08:02:13 +00:00
|
|
|
Machine: nv.MachineKey(),
|
|
|
|
|
DiscoKey: nv.DiscoKey(),
|
|
|
|
|
Addresses: addresses,
|
|
|
|
|
PrimaryRoutes: primaryRoutes,
|
|
|
|
|
AllowedIPs: allowedIPs,
|
|
|
|
|
Endpoints: nv.Endpoints().AsSlice(),
|
|
|
|
|
HomeDERP: derp,
|
|
|
|
|
Hostinfo: nv.Hostinfo(),
|
|
|
|
|
Created: nv.CreatedAt().UTC(),
|
2025-12-10 09:16:22 +01:00
|
|
|
|
|
|
|
|
Online: nv.IsOnline().Clone(),
|
|
|
|
|
|
|
|
|
|
Tags: nv.Tags().AsSlice(),
|
|
|
|
|
|
|
|
|
|
MachineAuthorized: !nv.IsExpired(),
|
|
|
|
|
Expired: nv.IsExpired(),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set LastSeen only for offline nodes to avoid confusing Tailscale clients
|
|
|
|
|
// during rapid reconnection cycles. Online nodes should not have LastSeen set
|
|
|
|
|
// as this can make clients interpret them as "not online" despite Online=true.
|
|
|
|
|
if nv.LastSeen().Valid() && nv.IsOnline().Valid() && !nv.IsOnline().Get() {
|
|
|
|
|
lastSeen := nv.LastSeen().Get()
|
|
|
|
|
tNode.LastSeen = &lastSeen
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &tNode, nil
|
|
|
|
|
}
|