all: annotate complex functions with gocyclo rationale
Splitting these functions does not buy clarity — each has been
extracted before and put back. Pin the //nolint:gocyclo on each
with the reason their shape resists clean decomposition.
policy/v2/policy.go ViaRoutesForPeer — three-pass
via-grant resolution
policy/v2/filter.go compileSSHPolicy — per-rule
branches with
intertwined
autogroup:self
handling
(annotated in the
earlier nil-error
commit)
state/state.go HandleNodeFromPreAuthKey — security-
sensitive
sequential
validation order
servertest/routes_test TestRoutes — table-driven
test driver with
many independent
subtests
Also: //nolint:recvcheck on policy/v2.SSHUser — UnmarshalJSON
requires a pointer receiver; the other methods on this string
newtype use value receivers by convention.
This commit is contained in:
parent
3e2aa5814e
commit
17236fd284
4 changed files with 238 additions and 213 deletions
|
|
@ -2,11 +2,11 @@
|
|||
// coordinating between subsystems like database, IP allocation,
|
||||
// policy management, and DERP routing.
|
||||
//
|
||||
// The central type State owns a copy-on-write NodeStore
|
||||
// (node_store.go), a PrimaryRoutes HA ledger, the PolicyManager, and a
|
||||
// pingTracker for PingRequest correlation. Cross-subsystem operations
|
||||
// (node updates, policy evaluation, IP allocation) go through State
|
||||
// rather than directly to the database.
|
||||
// The central type [State] owns a copy-on-write [NodeStore]
|
||||
// (node_store.go), a PrimaryRoutes HA ledger, the [policy.PolicyManager],
|
||||
// and a [pingTracker] for [tailcfg.PingRequest] correlation.
|
||||
// Cross-subsystem operations (node updates, policy evaluation, IP
|
||||
// allocation) go through [State] rather than directly to the database.
|
||||
|
||||
package state
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ var ErrNodeNotFound = errors.New("node not found")
|
|||
// ErrInvalidNodeView is returned when an invalid node view is provided.
|
||||
var ErrInvalidNodeView = errors.New("invalid node view provided")
|
||||
|
||||
// ErrNodeNotInNodeStore is returned when a node no longer exists in the NodeStore.
|
||||
// ErrNodeNotInNodeStore is returned when a node no longer exists in the [NodeStore].
|
||||
var ErrNodeNotInNodeStore = errors.New("node no longer exists in NodeStore")
|
||||
|
||||
// ErrNodeNameNotUnique is returned when a node name is not unique.
|
||||
|
|
@ -122,6 +122,9 @@ type sshCheckPair struct {
|
|||
|
||||
// State manages Headscale's core state, coordinating between database, policy management,
|
||||
// IP allocation, and DERP routing. All methods are thread-safe.
|
||||
//
|
||||
// See [policy.PolicyManager] for policy evaluation and [NodeStore] for the
|
||||
// in-memory node cache.
|
||||
type State struct {
|
||||
// cfg holds the current Headscale configuration
|
||||
cfg *types.Config
|
||||
|
|
@ -169,7 +172,7 @@ type State struct {
|
|||
sshCheckMu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewState creates and initializes a new State instance, setting up the database,
|
||||
// NewState creates and initializes a new [State] instance, setting up the database,
|
||||
// IP allocator, DERP map, policy manager, and loading existing users and nodes.
|
||||
func NewState(cfg *types.Config) (*State, error) {
|
||||
cacheExpiration := registerCacheExpiration
|
||||
|
|
@ -226,7 +229,7 @@ func NewState(cfg *types.Config) (*State, error) {
|
|||
return nil, fmt.Errorf("initializing policy manager: %w", err)
|
||||
}
|
||||
|
||||
// Apply defaults for NodeStore batch configuration if not set.
|
||||
// Apply defaults for [NodeStore] batch configuration if not set.
|
||||
// This ensures tests that create Config directly (without viper) still work.
|
||||
batchSize := cfg.Tuning.NodeStoreBatchSize
|
||||
if batchSize == 0 {
|
||||
|
|
@ -238,7 +241,7 @@ func NewState(cfg *types.Config) (*State, error) {
|
|||
batchTimeout = defaultNodeStoreBatchTimeout
|
||||
}
|
||||
|
||||
// PolicyManager.BuildPeerMap handles both global and per-node filter complexity.
|
||||
// [policy.PolicyManager.BuildPeerMap] handles both global and per-node filter complexity.
|
||||
// This moves the complex peer relationship logic into the policy package where it belongs.
|
||||
nodeStore := NewNodeStore(
|
||||
nodes,
|
||||
|
|
@ -264,7 +267,7 @@ func NewState(cfg *types.Config) (*State, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
// Close gracefully shuts down the State instance and releases all resources.
|
||||
// Close gracefully shuts down the [State] instance and releases all resources.
|
||||
func (s *State) Close() error {
|
||||
s.pings.drain()
|
||||
s.nodeStore.Stop()
|
||||
|
|
@ -288,7 +291,7 @@ func (s *State) DERPMap() tailcfg.DERPMapView {
|
|||
}
|
||||
|
||||
// ReloadPolicy reloads the access control policy and triggers auto-approval if changed.
|
||||
// Returns true if the policy changed.
|
||||
// Returns the resulting [change.Change] slice when the policy or routes changed.
|
||||
func (s *State) ReloadPolicy() ([]change.Change, error) {
|
||||
pol, err := hsdb.PolicyBytes(s.db.DB, s.cfg)
|
||||
if err != nil {
|
||||
|
|
@ -304,18 +307,18 @@ func (s *State) ReloadPolicy() ([]change.Change, error) {
|
|||
// approvals don't persist if checkPeriod rules are modified or removed.
|
||||
s.ClearSSHCheckAuth()
|
||||
|
||||
// Rebuild peer maps after policy changes because the peersFunc in NodeStore
|
||||
// uses the PolicyManager's filters. Without this, nodes won't see newly allowed
|
||||
// peers until a node is added/removed, causing autogroup:self policies to not
|
||||
// propagate correctly when switching between policy types.
|
||||
// Rebuild peer maps after policy changes because the peersFunc in [NodeStore]
|
||||
// uses the [policy.PolicyManager]'s filters. Without this, nodes won't see
|
||||
// newly allowed peers until a node is added/removed, causing autogroup:self
|
||||
// policies to not propagate correctly when switching between policy types.
|
||||
s.nodeStore.RebuildPeerMaps()
|
||||
|
||||
//nolint:prealloc // cs starts with one element and may grow
|
||||
cs := []change.Change{change.PolicyChange()}
|
||||
|
||||
// Per-node selective self refresh for nodeAttrs. A broadcast
|
||||
// PolicyChange() re-renders peer lists and packet filters but
|
||||
// never repopulates a node's own [tailcfg.Node.CapMap]; that
|
||||
// [change.PolicyChange] re-renders peer lists and packet filters
|
||||
// but never repopulates a node's own [tailcfg.Node.CapMap]; that
|
||||
// lives on the self entry only. The drain returns every node ID
|
||||
// whose cap output shifted across recent updateLocked calls —
|
||||
// refreshNodeAttrsLocked appends rather than overwrites so a
|
||||
|
|
@ -476,18 +479,19 @@ func (s *State) ListAllUsers() ([]types.User, error) {
|
|||
|
||||
// persistNodeToDB saves the given node state to the database.
|
||||
// This function must receive the exact node state to save to ensure consistency between
|
||||
// NodeStore and the database. It verifies the node still exists in NodeStore to prevent
|
||||
// race conditions where a node might be deleted between UpdateNode returning and
|
||||
// persistNodeToDB being called.
|
||||
// [NodeStore] and the database. It verifies the node still exists in [NodeStore] to
|
||||
// prevent race conditions where a node might be deleted between [NodeStore.UpdateNode]
|
||||
// returning and persistNodeToDB being called.
|
||||
func (s *State) persistNodeToDB(node types.NodeView) (types.NodeView, change.Change, error) {
|
||||
if !node.Valid() {
|
||||
return types.NodeView{}, change.Change{}, ErrInvalidNodeView
|
||||
}
|
||||
|
||||
// Verify the node still exists in NodeStore before persisting to database.
|
||||
// Without this check, we could hit a race condition where UpdateNode returns a valid
|
||||
// node from a batch update, then the node gets deleted (e.g., ephemeral node logout),
|
||||
// and persistNodeToDB would incorrectly re-insert the deleted node into the database.
|
||||
// Verify the node still exists in [NodeStore] before persisting to database.
|
||||
// Without this check, we could hit a race condition where [NodeStore.UpdateNode]
|
||||
// returns a valid node from a batch update, then the node gets deleted (e.g.,
|
||||
// ephemeral node logout), and persistNodeToDB would incorrectly re-insert the
|
||||
// deleted node into the database.
|
||||
_, exists := s.nodeStore.GetNode(node.ID())
|
||||
if !exists {
|
||||
log.Warn().
|
||||
|
|
@ -523,12 +527,12 @@ func (s *State) persistNodeToDB(node types.NodeView) (types.NodeView, change.Cha
|
|||
}
|
||||
|
||||
func (s *State) SaveNode(node types.NodeView) (types.NodeView, change.Change, error) {
|
||||
// Update NodeStore first
|
||||
// Update [NodeStore] first
|
||||
nodePtr := node.AsStruct()
|
||||
|
||||
resultNode := s.nodeStore.PutNode(*nodePtr)
|
||||
|
||||
// Then save to database using the result from PutNode
|
||||
// Then save to database using the result from [NodeStore.PutNode]
|
||||
return s.persistNodeToDB(resultNode)
|
||||
}
|
||||
|
||||
|
|
@ -563,14 +567,15 @@ func (s *State) DeleteNode(node types.NodeView) (change.Change, error) {
|
|||
|
||||
// Connect marks a node connected and returns the resulting changes
|
||||
// plus a session epoch. The caller must pass the epoch back to
|
||||
// Disconnect so deferred grace-period disconnects from a previous
|
||||
// poll session are dropped (see poll.go).
|
||||
// [State.Disconnect] so deferred grace-period disconnects from a
|
||||
// previous poll session are dropped (see poll.go).
|
||||
func (s *State) Connect(id types.NodeID) ([]change.Change, uint64) {
|
||||
prevRoutes := s.nodeStore.PrimaryRoutes()
|
||||
|
||||
// Reconnecting clears Unhealthy: the node just proved basic
|
||||
// connectivity by completing the Noise handshake.
|
||||
var epoch uint64
|
||||
|
||||
node, ok := s.nodeStore.UpdateNode(id, func(n *types.Node) {
|
||||
n.SessionEpoch++
|
||||
epoch = n.SessionEpoch
|
||||
|
|
@ -597,17 +602,20 @@ func (s *State) Connect(id types.NodeID) ([]change.Change, uint64) {
|
|||
}
|
||||
|
||||
// Disconnect marks the node offline. epoch must match the value
|
||||
// Connect returned for this session; otherwise the call no-ops so a
|
||||
// deferred disconnect from an older session cannot overwrite state set
|
||||
// by a newer Connect. The check and the IsOnline write share an
|
||||
// UpdateNode closure, making them atomic against concurrent Connects.
|
||||
// [State.Connect] returned for this session; otherwise the call no-ops
|
||||
// so a deferred disconnect from an older session cannot overwrite state
|
||||
// set by a newer [State.Connect]. The check and the IsOnline write share
|
||||
// an [NodeStore.UpdateNode] closure, making them atomic against
|
||||
// concurrent connects.
|
||||
func (s *State) Disconnect(id types.NodeID, epoch uint64) ([]change.Change, error) {
|
||||
var stale bool
|
||||
|
||||
node, ok := s.nodeStore.UpdateNode(id, func(n *types.Node) {
|
||||
if n.SessionEpoch != epoch {
|
||||
stale = true
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
n.LastSeen = &now
|
||||
n.IsOnline = new(false)
|
||||
|
|
@ -631,7 +639,7 @@ func (s *State) Disconnect(id types.NodeID, epoch uint64) ([]change.Change, erro
|
|||
|
||||
log.Info().EmbedObject(node).Msg("node disconnected")
|
||||
|
||||
// Persist LastSeen best-effort: NodeStore already reflects offline
|
||||
// Persist LastSeen best-effort: [NodeStore] already reflects offline
|
||||
// and peers still need the change notifications below.
|
||||
_, c, err := s.persistNodeToDB(node)
|
||||
if err != nil {
|
||||
|
|
@ -807,11 +815,11 @@ func (s *State) ListEphemeralNodes() views.Slice[types.NodeView] {
|
|||
// SetNodeExpiry updates the expiration time for a node.
|
||||
// If expiry is nil, the node's expiry is disabled (node will never expire).
|
||||
func (s *State) SetNodeExpiry(nodeID types.NodeID, expiry *time.Time) (types.NodeView, change.Change, error) {
|
||||
// Update NodeStore before database to ensure consistency. The NodeStore update is
|
||||
// blocking and will be the source of truth for the batcher. The database update must
|
||||
// make the exact same change. If the database update fails, the NodeStore change will
|
||||
// remain, but since we return an error, no change notification will be sent to the
|
||||
// batcher, preventing inconsistent state propagation.
|
||||
// Update [NodeStore] before database to ensure consistency. The [NodeStore] update
|
||||
// is blocking and will be the source of truth for the batcher. The database update
|
||||
// must make the exact same change. If the database update fails, the [NodeStore]
|
||||
// change will remain, but since we return an error, no change notification will be
|
||||
// sent to the batcher, preventing inconsistent state propagation.
|
||||
n, ok := s.nodeStore.UpdateNode(nodeID, func(node *types.Node) {
|
||||
node.Expiry = expiry
|
||||
})
|
||||
|
|
@ -877,9 +885,9 @@ func (s *State) SetNodeTags(nodeID types.NodeID, tags []string) (types.NodeView,
|
|||
// Log the operation
|
||||
logTagOperation(existingNode, validatedTags)
|
||||
|
||||
// Update NodeStore before database to ensure consistency. The NodeStore update is
|
||||
// blocking and will be the source of truth for the batcher. The database update must
|
||||
// make the exact same change.
|
||||
// Update [NodeStore] before database to ensure consistency. The [NodeStore] update
|
||||
// is blocking and will be the source of truth for the batcher. The database update
|
||||
// must make the exact same change.
|
||||
n, ok := s.nodeStore.UpdateNode(nodeID, func(node *types.Node) {
|
||||
node.Tags = validatedTags
|
||||
// Tagged nodes are owned by their tags, not a user.
|
||||
|
|
@ -974,7 +982,7 @@ func (s *State) BackfillNodeIPs() ([]string, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Refresh NodeStore after IP changes to ensure consistency
|
||||
// Refresh [NodeStore] after IP changes to ensure consistency
|
||||
if len(changes) > 0 {
|
||||
nodes, err := s.db.ListNodes()
|
||||
if err != nil {
|
||||
|
|
@ -996,7 +1004,7 @@ func (s *State) BackfillNodeIPs() ([]string, error) {
|
|||
node.Hostinfo.NetInfo = netInfo
|
||||
}
|
||||
// TODO(kradalby): This should just update the IP addresses, nothing else in the node store.
|
||||
// We should avoid PutNode here.
|
||||
// We should avoid [NodeStore.PutNode] here.
|
||||
_ = s.nodeStore.PutNode(*node)
|
||||
}
|
||||
}
|
||||
|
|
@ -1061,7 +1069,7 @@ func (s *State) MatchersForNode(node types.NodeView) ([]matcher.Match, error) {
|
|||
}
|
||||
|
||||
// NodeCapMap returns the policy-derived CapMap for the given node, suitable
|
||||
// for merging into tailcfg.Node.CapMap when the node is rendered as self or
|
||||
// for merging into [tailcfg.Node.CapMap] when the node is rendered as self or
|
||||
// as someone else's peer.
|
||||
func (s *State) NodeCapMap(id types.NodeID) tailcfg.NodeCapMap {
|
||||
return s.polMan.NodeCapMap(id)
|
||||
|
|
@ -1103,8 +1111,9 @@ func (s *State) AutoApproveRoutes(nv types.NodeView) (change.Change, error) {
|
|||
Strs("routes.approved.new", util.PrefixesToString(approved)).
|
||||
Msg("Single node auto-approval detected route changes")
|
||||
|
||||
// Persist the auto-approved routes to database and NodeStore via SetApprovedRoutes
|
||||
// This ensures consistency between database and NodeStore
|
||||
// Persist the auto-approved routes to database and [NodeStore] via
|
||||
// [State.SetApprovedRoutes]. This ensures consistency between database
|
||||
// and [NodeStore].
|
||||
_, c, err := s.SetApprovedRoutes(nv.ID(), approved)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
|
|
@ -1342,11 +1351,11 @@ func (s *State) CreateNodeForTest(user *types.User, hostname ...string) *types.N
|
|||
return s.db.CreateNodeForTest(user, hostname...)
|
||||
}
|
||||
|
||||
// PutNodeInStoreForTest writes a test node into the in-memory NodeStore
|
||||
// so handlers backed by NodeStore lookups (e.g. GetNodeByID) can see it.
|
||||
// CreateNodeForTest only saves to the database, which is fine for tests
|
||||
// that exercise the DB layer directly but insufficient for handler tests
|
||||
// that go through State.
|
||||
// PutNodeInStoreForTest writes a test node into the in-memory [NodeStore]
|
||||
// so handlers backed by [NodeStore] lookups (e.g. [State.GetNodeByID]) can
|
||||
// see it. [State.CreateNodeForTest] only saves to the database, which is
|
||||
// fine for tests that exercise the DB layer directly but insufficient for
|
||||
// handler tests that go through [State].
|
||||
func (s *State) PutNodeInStoreForTest(node types.Node) types.NodeView {
|
||||
return s.nodeStore.PutNode(node)
|
||||
}
|
||||
|
|
@ -1462,7 +1471,7 @@ type newNodeParams struct {
|
|||
|
||||
// authNodeUpdateParams contains parameters for updating an existing node during auth.
|
||||
type authNodeUpdateParams struct {
|
||||
// Node to update; must be valid and in NodeStore.
|
||||
// Node to update; must be valid and in [NodeStore].
|
||||
ExistingNode types.NodeView
|
||||
// Cached registration payload from the originating client request.
|
||||
RegData *types.RegistrationData
|
||||
|
|
@ -1481,7 +1490,7 @@ type authNodeUpdateParams struct {
|
|||
}
|
||||
|
||||
// applyAuthNodeUpdate applies common update logic for re-authenticating or converting
|
||||
// an existing node. It updates the node in NodeStore, processes RequestTags, and
|
||||
// an existing node. It updates the node in [NodeStore], processes RequestTags, and
|
||||
// persists changes to the database.
|
||||
func (s *State) applyAuthNodeUpdate(params authNodeUpdateParams) (types.NodeView, error) {
|
||||
regData := params.RegData
|
||||
|
|
@ -1509,8 +1518,9 @@ func (s *State) applyAuthNodeUpdate(params authNodeUpdateParams) (types.NodeView
|
|||
|
||||
oldTags := params.ExistingNode.Tags().AsSlice()
|
||||
|
||||
// Validate tags BEFORE calling UpdateNode to ensure we don't modify NodeStore
|
||||
// if validation fails. This maintains consistency between NodeStore and database.
|
||||
// Validate tags BEFORE calling [NodeStore.UpdateNode] to ensure we don't modify
|
||||
// [NodeStore] if validation fails. This maintains consistency between [NodeStore]
|
||||
// and database.
|
||||
rejectedTags := s.validateRequestTags(params.ExistingNode, requestTags)
|
||||
if len(rejectedTags) > 0 {
|
||||
return types.NodeView{}, fmt.Errorf(
|
||||
|
|
@ -1520,7 +1530,7 @@ func (s *State) applyAuthNodeUpdate(params authNodeUpdateParams) (types.NodeView
|
|||
)
|
||||
}
|
||||
|
||||
// Update existing node in NodeStore - validation passed, safe to mutate
|
||||
// Update existing node in [NodeStore] - validation passed, safe to mutate
|
||||
updatedNodeView, ok := s.nodeStore.UpdateNode(params.ExistingNode.ID(), func(node *types.Node) {
|
||||
node.NodeKey = regData.NodeKey
|
||||
node.DiscoKey = regData.DiscoKey
|
||||
|
|
@ -1536,10 +1546,11 @@ func (s *State) applyAuthNodeUpdate(params authNodeUpdateParams) (types.NodeView
|
|||
|
||||
node.Endpoints = regData.Endpoints
|
||||
// Do NOT reset IsOnline here. Online status is managed exclusively by
|
||||
// Connect()/Disconnect() in the poll session lifecycle. Resetting it
|
||||
// during re-registration causes a false offline blip: the change
|
||||
// notification triggers a map regeneration showing the node as offline
|
||||
// to peers, even though Connect() will immediately set it back to true.
|
||||
// [State.Connect]/[State.Disconnect] in the poll session lifecycle.
|
||||
// Resetting it during re-registration causes a false offline blip: the
|
||||
// change notification triggers a map regeneration showing the node as
|
||||
// offline to peers, even though [State.Connect] will immediately set it
|
||||
// back to true.
|
||||
node.LastSeen = new(time.Now())
|
||||
|
||||
// On conversion (tagged → user) we set the new register method.
|
||||
|
|
@ -1634,7 +1645,7 @@ func (s *State) applyAuthNodeUpdate(params authNodeUpdateParams) (types.NodeView
|
|||
return updatedNodeView, nil
|
||||
}
|
||||
|
||||
// createAndSaveNewNode creates a new node, allocates IPs, saves to DB, and adds to NodeStore.
|
||||
// createAndSaveNewNode creates a new node, allocates IPs, saves to DB, and adds to [NodeStore].
|
||||
// It preserves netinfo from an existing node if one is provided (for faster DERP connectivity).
|
||||
func (s *State) createAndSaveNewNode(params newNodeParams) (types.NodeView, error) {
|
||||
// Preserve NetInfo from existing node if available
|
||||
|
|
@ -1655,7 +1666,7 @@ func (s *State) createAndSaveNewNode(params newNodeParams) (types.NodeView, erro
|
|||
Hostinfo: params.Hostinfo,
|
||||
Endpoints: params.Endpoints,
|
||||
LastSeen: new(time.Now()),
|
||||
IsOnline: new(false), // Explicitly offline until Connect() is called
|
||||
IsOnline: new(false), // Explicitly offline until [State.Connect] is called
|
||||
RegisterMethod: params.RegisterMethod,
|
||||
Expiry: params.Expiry,
|
||||
}
|
||||
|
|
@ -1748,14 +1759,14 @@ func (s *State) createAndSaveNewNode(params newNodeParams) (types.NodeView, erro
|
|||
nodeToRegister.IPv4 = ipv4
|
||||
nodeToRegister.IPv6 = ipv6
|
||||
|
||||
// Seed GivenName from the sanitised raw hostname. NodeStore.PutNode
|
||||
// Seed GivenName from the sanitised raw hostname. [NodeStore.PutNode]
|
||||
// bumps on collision and falls back to "node" if the sanitised
|
||||
// result is empty (pure non-ASCII / punctuation input).
|
||||
if nodeToRegister.GivenName == "" {
|
||||
nodeToRegister.GivenName = dnsname.SanitizeHostname(nodeToRegister.Hostname)
|
||||
}
|
||||
|
||||
// New node - database first to get ID, then NodeStore
|
||||
// New node - database first to get ID, then [NodeStore]
|
||||
savedNode, err := hsdb.Write(s.db.DB, func(tx *gorm.DB) (*types.Node, error) {
|
||||
err := tx.Save(&nodeToRegister).Error
|
||||
if err != nil {
|
||||
|
|
@ -1775,12 +1786,12 @@ func (s *State) createAndSaveNewNode(params newNodeParams) (types.NodeView, erro
|
|||
return types.NodeView{}, err
|
||||
}
|
||||
|
||||
// Add to NodeStore after database creates the ID
|
||||
// Add to [NodeStore] after database creates the ID
|
||||
return s.nodeStore.PutNode(*savedNode), nil
|
||||
}
|
||||
|
||||
// validateRequestTags validates that the requested tags are permitted for the node.
|
||||
// This should be called BEFORE UpdateNode to ensure we don't modify NodeStore
|
||||
// This should be called BEFORE [NodeStore.UpdateNode] to ensure we don't modify [NodeStore]
|
||||
// if validation fails. Returns the list of rejected tags (empty if all valid).
|
||||
func (s *State) validateRequestTags(node types.NodeView, requestTags []string) []string {
|
||||
// Empty tags = clear tags, always permitted
|
||||
|
|
@ -2085,6 +2096,7 @@ func (s *State) findExistingNodeForPAK(
|
|||
return types.NodeView{}, false
|
||||
}
|
||||
|
||||
//nolint:gocyclo // sequential validation/update/create paths with security-sensitive ordering
|
||||
func (s *State) HandleNodeFromPreAuthKey(
|
||||
regReq tailcfg.RegisterRequest,
|
||||
machineKey key.MachinePublic,
|
||||
|
|
@ -2201,8 +2213,9 @@ func (s *State) HandleNodeFromPreAuthKey(
|
|||
node.AuthKey = pak
|
||||
node.AuthKeyID = &pak.ID
|
||||
// Do NOT reset IsOnline here. Online status is managed exclusively by
|
||||
// Connect()/Disconnect() in the poll session lifecycle. Resetting it
|
||||
// during re-registration causes a false offline blip to peers.
|
||||
// [State.Connect]/[State.Disconnect] in the poll session lifecycle.
|
||||
// Resetting it during re-registration causes a false offline blip
|
||||
// to peers.
|
||||
node.LastSeen = new(time.Now())
|
||||
|
||||
// Tagged nodes keep their existing expiry (disabled).
|
||||
|
|
@ -2237,7 +2250,7 @@ func (s *State) HandleNodeFromPreAuthKey(
|
|||
|
||||
// Only mark the key used on the *first* registration. On
|
||||
// re-registration the same key is already used and the
|
||||
// atomic compare-and-set in UsePreAuthKey would otherwise
|
||||
// atomic compare-and-set in [hsdb.UsePreAuthKey] would otherwise
|
||||
// reject it as "authkey already used". This is the path
|
||||
// behind issue #2830 where containers restart with the
|
||||
// same one-shot key.
|
||||
|
|
@ -2478,7 +2491,7 @@ func (s *State) autoApproveNodes() ([]change.Change, error) {
|
|||
|
||||
// isAutoDerivedGivenName reports whether given matches what
|
||||
// dnsname.SanitizeHostname(hostname) would produce, optionally with a
|
||||
// NodeStore collision-bump "-N" suffix. It is used to detect whether a
|
||||
// [NodeStore] collision-bump "-N" suffix. It is used to detect whether a
|
||||
// GivenName has been admin-renamed (in which case it must not be
|
||||
// overwritten by client-side hostname changes).
|
||||
func isAutoDerivedGivenName(given, hostname string) bool {
|
||||
|
|
@ -2498,10 +2511,10 @@ func isAutoDerivedGivenName(given, hostname string) bool {
|
|||
}
|
||||
|
||||
// UpdateNodeFromMapRequest is the sync point where Hostinfo changes,
|
||||
// endpoint updates, and route advertisements from a MapRequest land in
|
||||
// the NodeStore. It produces a change.Change summarising what actually
|
||||
// moved so downstream subsystems (mapper, policy, primary routes) can
|
||||
// react accordingly.
|
||||
// endpoint updates, and route advertisements from a [tailcfg.MapRequest]
|
||||
// land in the [NodeStore]. It produces a [change.Change] summarising
|
||||
// what actually moved so downstream subsystems (mapper, policy, primary
|
||||
// routes) can react accordingly.
|
||||
//
|
||||
// TODO(kradalby): This is essentially a patch update that could be sent directly to nodes,
|
||||
// which means we could shortcut the whole change thing if there are no other important updates.
|
||||
|
|
@ -2528,7 +2541,7 @@ func (s *State) UpdateNodeFromMapRequest(id types.NodeID, req tailcfg.MapRequest
|
|||
// Hostinfo + auto-approval that follows shifted any prefix.
|
||||
prevRoutes := s.nodeStore.PrimaryRoutes()
|
||||
|
||||
// We need to ensure we update the node as it is in the NodeStore at
|
||||
// We need to ensure we update the node as it is in the [NodeStore] at
|
||||
// the time of the request.
|
||||
updatedNode, ok := s.nodeStore.UpdateNode(id, func(currentNode *types.Node) {
|
||||
peerChange := currentNode.PeerChangeFromMapRequest(req)
|
||||
|
|
@ -2555,7 +2568,7 @@ func (s *State) UpdateNodeFromMapRequest(id types.NodeID, req tailcfg.MapRequest
|
|||
return
|
||||
}
|
||||
|
||||
// Calculate route approval before NodeStore update to avoid calling View() inside callback
|
||||
// Calculate route approval before [NodeStore] update to avoid calling View() inside callback
|
||||
var hasNewRoutes bool
|
||||
if hi := req.Hostinfo; hi != nil {
|
||||
hasNewRoutes = len(hi.RoutableIPs) > 0
|
||||
|
|
@ -2617,7 +2630,7 @@ func (s *State) UpdateNodeFromMapRequest(id types.NodeID, req tailcfg.MapRequest
|
|||
currentNode.Hostname = req.Hostinfo.Hostname
|
||||
if autoDerived {
|
||||
currentNode.GivenName = dnsname.SanitizeHostname(req.Hostinfo.Hostname)
|
||||
// NodeStore.UpdateNode auto-bumps GivenName on collision.
|
||||
// [NodeStore.UpdateNode] auto-bumps GivenName on collision.
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2655,13 +2668,13 @@ func (s *State) UpdateNodeFromMapRequest(id types.NodeID, req tailcfg.MapRequest
|
|||
Strs(zf.AutoApprovedRoutes, util.PrefixesToString(autoApprovedRoutes)).
|
||||
Msg("Persisting auto-approved routes from MapRequest")
|
||||
|
||||
// SetApprovedRoutes will update both database and PrimaryRoutes table
|
||||
// [State.SetApprovedRoutes] will update both database and PrimaryRoutes table
|
||||
_, c, err := s.SetApprovedRoutes(id, autoApprovedRoutes)
|
||||
if err != nil {
|
||||
return change.Change{}, fmt.Errorf("persisting auto-approved routes: %w", err)
|
||||
}
|
||||
|
||||
// If SetApprovedRoutes resulted in a policy change, return it
|
||||
// If [State.SetApprovedRoutes] resulted in a policy change, return it
|
||||
if !c.IsEmpty() {
|
||||
return c, nil
|
||||
}
|
||||
|
|
@ -2702,7 +2715,7 @@ func (s *State) UpdateNodeFromMapRequest(id types.NodeID, req tailcfg.MapRequest
|
|||
return buildMapRequestChangeResponse(id, updatedNode, hostinfoChanged, endpointChanged, derpChanged)
|
||||
}
|
||||
|
||||
// buildMapRequestChangeResponse determines the appropriate response type for a MapRequest update.
|
||||
// buildMapRequestChangeResponse determines the appropriate response type for a [tailcfg.MapRequest] update.
|
||||
// Hostinfo changes require a full update, while endpoint/DERP changes can use lightweight patches.
|
||||
func buildMapRequestChangeResponse(
|
||||
id types.NodeID,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue