2023-09-24 13:42:05 +02:00
|
|
|
package types
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"net/netip"
|
2025-10-22 13:50:39 +02:00
|
|
|
"regexp"
|
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"
|
2025-07-05 23:30:47 +02:00
|
|
|
"github.com/rs/zerolog/log"
|
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 (
|
|
|
|
|
ErrNodeAddressesInvalid = errors.New("failed to parse node addresses")
|
2023-12-09 18:09:24 +01:00
|
|
|
ErrHostnameTooLong = errors.New("hostname too long, cannot except 255 ASCII chars")
|
|
|
|
|
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")
|
2025-10-22 13:50:39 +02:00
|
|
|
|
|
|
|
|
invalidDNSRegex = regexp.MustCompile("[^a-z0-9-.]+")
|
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
|
|
|
|
|
// netip.Prefixes representing the primary routes for that node.
|
|
|
|
|
type RouteFunc func(id NodeID) []netip.Prefix
|
|
|
|
|
|
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 {
|
|
|
|
|
return tailcfg.NodeID(id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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"`
|
|
|
|
|
|
|
|
|
|
Endpoints []netip.AddrPort `gorm:"serializer:json"`
|
|
|
|
|
|
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:
|
|
|
|
|
// a DNS normalized version of Hostname
|
|
|
|
|
// a valid name set by the User
|
|
|
|
|
//
|
|
|
|
|
// 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
|
|
|
|
|
|
|
|
// UserID is set for ALL nodes (tagged and user-owned) to track "created by".
|
|
|
|
|
// For tagged nodes, this is informational only - the tag is the owner.
|
|
|
|
|
// For user-owned nodes, this identifies the owner.
|
|
|
|
|
// Only nil for orphaned nodes (should not happen in normal operation).
|
|
|
|
|
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).
|
|
|
|
|
Tags []string `gorm:"column:tags;serializer:json"`
|
2023-09-24 13:42:05 +02:00
|
|
|
|
2025-02-01 09:31:13 +00:00
|
|
|
// When a node has been created with a PreAuthKey, we need to
|
|
|
|
|
// 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]
|
|
|
|
|
ApprovedRoutes []netip.Prefix `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:"-"`
|
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-10-17 18:45:33 +03:00
|
|
|
// GivenNameHasBeenChanged returns whether the `givenName` can be automatically changed based on the `Hostname` of the node.
|
|
|
|
|
func (node *Node) GivenNameHasBeenChanged() bool {
|
2025-10-22 13:50:39 +02:00
|
|
|
// Strip invalid DNS characters for givenName comparison
|
|
|
|
|
normalised := strings.ToLower(node.Hostname)
|
|
|
|
|
normalised = invalidDNSRegex.ReplaceAllString(normalised, "")
|
|
|
|
|
return node.GivenName == normalised
|
2024-10-17 18:45:33 +03:00
|
|
|
}
|
|
|
|
|
|
2024-04-17 07:03:06 +02:00
|
|
|
// IsExpired returns whether the node registration has expired.
|
|
|
|
|
func (node Node) IsExpired() bool {
|
|
|
|
|
// 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.
|
|
|
|
|
// https://tailscale.com/kb/1111/ephemeral-nodes/
|
|
|
|
|
func (node *Node) IsEphemeral() bool {
|
|
|
|
|
return node.AuthKey != nil && node.AuthKey.Ephemeral
|
2023-09-24 13:42:05 +02:00
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
2025-12-02 12:01:25 +01:00
|
|
|
// TypedUserID returns the UserID as a typed UserID type.
|
|
|
|
|
// Returns 0 if UserID is nil.
|
|
|
|
|
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 {
|
2025-03-21 11:49:32 +01:00
|
|
|
var addrs []netip.Prefix
|
2024-04-17 07:03:06 +02:00
|
|
|
for _, nodeAddress := range node.IPs() {
|
2023-09-24 13:42:05 +02:00
|
|
|
ip := netip.PrefixFrom(nodeAddress, nodeAddress.BitLen())
|
|
|
|
|
addrs = append(addrs, ip)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return addrs
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-21 11:49:32 +01: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.
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
|
|
|
|
var ret []string
|
|
|
|
|
|
2025-03-10 16:20:29 +01:00
|
|
|
for _, ip := range node.IPs() {
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AppendToIPSet adds the individual ips in NodeAddresses to a
|
|
|
|
|
// given netipx.IPSetBuilder.
|
2024-04-17 07:03:06 +02:00
|
|
|
func (node *Node) AppendToIPSet(build *netipx.IPSetBuilder) {
|
|
|
|
|
for _, ip := range node.IPs() {
|
2023-09-24 13:42:05 +02:00
|
|
|
build.Add(ip)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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()
|
2024-02-12 11:44:37 +01:00
|
|
|
|
2024-10-02 11:41:58 +02:00
|
|
|
for _, matcher := range matchers {
|
2025-02-26 07:22:55 -08:00
|
|
|
if !matcher.SrcsContainsIPs(src...) {
|
2023-09-24 13:42:05 +02:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-26 07:22:55 -08:00
|
|
|
if matcher.DestsContainsIP(allowedIPs...) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-01 14:29:50 +01:00
|
|
|
// Check if the node has access to routes that might be part of a
|
|
|
|
|
// smaller subnet that is served from node2 as a subnet router.
|
2025-02-26 07:22:55 -08:00
|
|
|
if matcher.DestsOverlapsPrefixes(node2.SubnetRoutes()...) {
|
2023-09-24 13:42:05 +02:00
|
|
|
return true
|
|
|
|
|
}
|
2025-11-01 14:29:50 +01:00
|
|
|
|
|
|
|
|
// If the dst is "the internet" and node2 is an exit node, allow access.
|
|
|
|
|
if matcher.DestsIsTheInternet() && node2.IsExitNode() {
|
|
|
|
|
return true
|
|
|
|
|
}
|
2023-09-24 13:42:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-04 22:52:47 +03:00
|
|
|
func (node *Node) CanAccessRoute(matchers []matcher.Match, route netip.Prefix) bool {
|
|
|
|
|
src := node.IPs()
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
ForcedTags: 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
|
|
|
|
|
// Note: User will be set to TaggedDevices in the gRPC layer (grpcv1.go)
|
|
|
|
|
// for proper MapResponse formatting
|
|
|
|
|
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 == "" {
|
|
|
|
|
return "", fmt.Errorf("failed to create valid FQDN: %w", ErrNodeHasNoGivenName)
|
|
|
|
|
}
|
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(
|
|
|
|
|
"failed to create valid FQDN (%s): %w",
|
|
|
|
|
hostname,
|
|
|
|
|
ErrHostnameTooLong,
|
|
|
|
|
)
|
2023-09-24 13:42:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return hostname, nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-26 07:22:55 -08:00
|
|
|
// AnnouncedRoutes returns the list of routes that the node announces.
|
|
|
|
|
// It should be used instead of checking Hostinfo.RoutableIPs directly.
|
|
|
|
|
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
|
|
|
|
|
// announces and are approved.
|
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
|
|
|
|
|
// by the node. See the comment in Proto() method and the implementation in
|
|
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-01 14:25:07 +01:00
|
|
|
// AllApprovedRoutes returns the combination of SubnetRoutes and ExitRoutes
|
|
|
|
|
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
|
|
|
|
|
|
|
|
// PeerChangeFromMapRequest takes a MapRequest and compares it to the node
|
|
|
|
|
// to produce a PeerChange struct that can be used to updated the node and
|
|
|
|
|
// inform peers about smaller changes to the node.
|
|
|
|
|
// When a field is added to this function, remember to also add it to:
|
|
|
|
|
// - 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{
|
|
|
|
|
NodeID: tailcfg.NodeID(node.ID),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
} else {
|
|
|
|
|
// If there is a PreferredDERP check if it has changed.
|
|
|
|
|
if node.Hostinfo.NetInfo.PreferredDERP != req.Hostinfo.NetInfo.PreferredDERP {
|
|
|
|
|
ret.DERPRegion = req.Hostinfo.NetInfo.PreferredDERP
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
slices.SortFunc(oldCopy, func(a, b netip.AddrPort) int {
|
|
|
|
|
return a.Compare(b)
|
|
|
|
|
})
|
|
|
|
|
slices.SortFunc(newCopy, func(a, b netip.AddrPort) int {
|
|
|
|
|
return a.Compare(b)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-17 18:45:33 +03:00
|
|
|
// ApplyHostnameFromHostInfo takes a Hostinfo struct and updates the node.
|
|
|
|
|
func (node *Node) ApplyHostnameFromHostInfo(hostInfo *tailcfg.Hostinfo) {
|
|
|
|
|
if hostInfo == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-22 13:50:39 +02:00
|
|
|
newHostname := strings.ToLower(hostInfo.Hostname)
|
|
|
|
|
if err := util.ValidateHostname(newHostname); err != nil {
|
|
|
|
|
log.Warn().
|
|
|
|
|
Str("node.id", node.ID.String()).
|
|
|
|
|
Str("current_hostname", node.Hostname).
|
|
|
|
|
Str("rejected_hostname", hostInfo.Hostname).
|
|
|
|
|
Err(err).
|
|
|
|
|
Msg("Rejecting invalid hostname update from hostinfo")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if node.Hostname != newHostname {
|
2025-07-05 23:30:47 +02:00
|
|
|
log.Trace().
|
|
|
|
|
Str("node.id", node.ID.String()).
|
|
|
|
|
Str("old_hostname", node.Hostname).
|
2025-10-22 13:50:39 +02:00
|
|
|
Str("new_hostname", newHostname).
|
2025-07-05 23:30:47 +02:00
|
|
|
Str("old_given_name", node.GivenName).
|
|
|
|
|
Bool("given_name_changed", node.GivenNameHasBeenChanged()).
|
|
|
|
|
Msg("Updating hostname from hostinfo")
|
|
|
|
|
|
2024-10-17 18:45:33 +03:00
|
|
|
if node.GivenNameHasBeenChanged() {
|
2025-10-22 13:50:39 +02:00
|
|
|
// Strip invalid DNS characters for givenName display
|
|
|
|
|
givenName := strings.ToLower(newHostname)
|
|
|
|
|
givenName = invalidDNSRegex.ReplaceAllString(givenName, "")
|
|
|
|
|
node.GivenName = givenName
|
2024-10-17 18:45:33 +03:00
|
|
|
}
|
|
|
|
|
|
2025-10-22 13:50:39 +02:00
|
|
|
node.Hostname = newHostname
|
2025-07-05 23:30:47 +02:00
|
|
|
|
|
|
|
|
log.Trace().
|
|
|
|
|
Str("node.id", node.ID.String()).
|
|
|
|
|
Str("new_hostname", node.Hostname).
|
|
|
|
|
Str("new_given_name", node.GivenName).
|
|
|
|
|
Msg("Hostname updated")
|
2024-10-17 18:45:33 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-09 18:09:24 +01:00
|
|
|
// ApplyPeerChange takes a PeerChange struct and updates the node.
|
|
|
|
|
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")
|
|
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (node Node) DebugString() string {
|
|
|
|
|
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
|
|
|
|
2025-12-10 09:16:22 +01:00
|
|
|
func (nv NodeView) UserView() UserView {
|
|
|
|
|
return nv.User()
|
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 {
|
|
|
|
|
if !nv.Valid() {
|
2025-07-05 23:31:13 +02:00
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 09:16:22 +01:00
|
|
|
return nv.ж.CanAccess(matchers, node2.AsStruct())
|
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.
|
|
|
|
|
// https://tailscale.com/kb/1111/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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PeerChangeFromMapRequest takes a MapRequest and compares it to the node
|
|
|
|
|
// to produce a PeerChange struct that can be used to updated the node and
|
|
|
|
|
// 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() {
|
2025-07-10 23:38:55 +02:00
|
|
|
return "", errors.New("failed to create valid FQDN: node view is invalid")
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
}
|
|
|
|
|
|
2025-12-02 12:01:25 +01:00
|
|
|
// TypedUserID returns the UserID as a typed UserID type.
|
|
|
|
|
// Returns 0 if 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.
|
|
|
|
|
// Tagged nodes always return TaggedDevices.ID, user-owned nodes return their actual 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))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//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
|
|
|
}
|
|
|
|
|
|
2025-07-05 23:31:13 +02: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.
|
|
|
|
|
// This is primarily used for policy cache invalidation.
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 09:16:22 +01:00
|
|
|
if !slices.Equal(nv.AnnouncedRoutes(), other.AnnouncedRoutes()) {
|
2025-10-23 17:57:41 +02:00
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 09:16:22 +01:00
|
|
|
if !slices.Equal(nv.SubnetRoutes(), other.SubnetRoutes()) {
|
2025-10-23 17:57:41 +02:00
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
}
|
2025-12-10 09:16:22 +01:00
|
|
|
|
|
|
|
|
// TailNodes converts a slice of NodeViews into Tailscale tailcfg.Nodes.
|
|
|
|
|
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() {
|
|
|
|
|
tNode, err := node.TailNode(capVer, primaryRouteFunc, cfg)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tNodes = append(tNodes, tNode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return tNodes, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TailNode converts a NodeView into a Tailscale tailcfg.Node.
|
|
|
|
|
func (nv NodeView) TailNode(
|
|
|
|
|
capVer tailcfg.CapabilityVersion,
|
|
|
|
|
primaryRouteFunc RouteFunc,
|
|
|
|
|
cfg *Config,
|
|
|
|
|
) (*tailcfg.Node, error) {
|
|
|
|
|
if !nv.Valid() {
|
|
|
|
|
return nil, ErrInvalidNodeView
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hostname, err := nv.GetFQDN(cfg.BaseDomain)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var derp int
|
|
|
|
|
// TODO(kradalby): legacyDERP was removed in tailscale/tailscale@2fc4455e6dd9ab7f879d4e2f7cffc2be81f14077
|
|
|
|
|
// and should be removed after 111 is the minimum capver.
|
|
|
|
|
legacyDERP := "127.3.3.40:0" // Zero means disconnected or unknown.
|
|
|
|
|
if nv.Hostinfo().Valid() && nv.Hostinfo().NetInfo().Valid() {
|
|
|
|
|
legacyDERP = fmt.Sprintf("127.3.3.40:%d", nv.Hostinfo().NetInfo().PreferredDERP())
|
|
|
|
|
derp = nv.Hostinfo().NetInfo().PreferredDERP()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var keyExpiry time.Time
|
|
|
|
|
if nv.Expiry().Valid() {
|
|
|
|
|
keyExpiry = nv.Expiry().Get()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
primaryRoutes := primaryRouteFunc(nv.ID())
|
|
|
|
|
allowedIPs := slices.Concat(nv.Prefixes(), primaryRoutes, nv.ExitRoutes())
|
|
|
|
|
tsaddr.SortPrefixes(allowedIPs)
|
|
|
|
|
|
|
|
|
|
capMap := tailcfg.NodeCapMap{
|
2025-12-12 11:35:16 +01:00
|
|
|
tailcfg.CapabilityAdmin: []tailcfg.RawMessage{},
|
|
|
|
|
tailcfg.CapabilitySSH: []tailcfg.RawMessage{},
|
2025-12-10 09:16:22 +01:00
|
|
|
}
|
|
|
|
|
if cfg.RandomizeClientPort {
|
|
|
|
|
capMap[tailcfg.NodeAttrRandomizeClientPort] = []tailcfg.RawMessage{}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-12 11:35:16 +01:00
|
|
|
if cfg.Taildrop.Enabled {
|
|
|
|
|
capMap[tailcfg.CapabilityFileSharing] = []tailcfg.RawMessage{}
|
|
|
|
|
}
|
|
|
|
|
|
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(),
|
|
|
|
|
|
|
|
|
|
Machine: nv.MachineKey(),
|
|
|
|
|
DiscoKey: nv.DiscoKey(),
|
|
|
|
|
Addresses: nv.Prefixes(),
|
|
|
|
|
PrimaryRoutes: primaryRoutes,
|
|
|
|
|
AllowedIPs: allowedIPs,
|
|
|
|
|
Endpoints: nv.Endpoints().AsSlice(),
|
|
|
|
|
HomeDERP: derp,
|
|
|
|
|
LegacyDERPString: legacyDERP,
|
|
|
|
|
Hostinfo: nv.Hostinfo(),
|
|
|
|
|
Created: nv.CreatedAt().UTC(),
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|