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
This commit is contained in:
Kristoffer Dalby 2026-05-18 18:35:53 +00:00
parent 17236fd284
commit 4cca63155d
124 changed files with 1037 additions and 1011 deletions

View file

@ -27,7 +27,7 @@ var (
)
// offlineNodeCleanupThreshold is how long a node must be disconnected
// before cleanupOfflineNodes removes its in-memory state.
// before [Batcher.cleanupOfflineNodes] removes its in-memory state.
const offlineNodeCleanupThreshold = 15 * time.Minute
var mapResponseGenerated = promauto.NewCounterVec(prometheus.CounterOpts{
@ -49,7 +49,7 @@ func NewBatcher(batchTime time.Duration, workers int, mapper *mapper) *Batcher {
}
}
// NewBatcherAndMapper creates a new Batcher with its mapper.
// NewBatcherAndMapper creates a new [Batcher] with its [mapper].
func NewBatcherAndMapper(cfg *types.Config, state *state.State) *Batcher {
m := newMapper(cfg, state)
b := NewBatcher(cfg.Tuning.BatchChangeDelay, cfg.Tuning.BatcherWorkers, m)
@ -69,7 +69,7 @@ type nodeConnection interface {
updateSentPeers(resp *tailcfg.MapResponse)
}
// generateMapResponse generates a [tailcfg.MapResponse] for the given NodeID based on the provided [change.Change].
// generateMapResponse generates a [tailcfg.MapResponse] for the given [types.NodeID] based on the provided [change.Change].
func generateMapResponse(nc nodeConnection, mapper *mapper, r change.Change) (*tailcfg.MapResponse, error) {
nodeID := nc.nodeID()
version := nc.version()
@ -130,17 +130,19 @@ func generateMapResponse(nc nodeConnection, mapper *mapper, r change.Change) (*t
// When a full update (SendAllPeers=true) produces zero visible peers
// (e.g., a restrictive policy isolates this node), the resulting
// MapResponse has Peers: []*tailcfg.Node{} (empty non-nil slice).
// [tailcfg.MapResponse] has Peers: []*tailcfg.Node{} (empty non-nil slice).
//
// The Tailscale client only treats Peers as a full authoritative
// replacement when len(Peers) > 0 (controlclient/map.go:462).
// An empty Peers slice is indistinguishable from a delta response,
// so the client silently preserves its existing peer state.
//
// This matters when a FullUpdate() replaces a pending PolicyChange()
// in the batcher (addToBatch short-circuits on HasFull). The
// PolicyChange would have computed PeersRemoved via computePeerDiff,
// but the FullUpdate path uses WithPeers which sets Peers: [].
// This matters when a [change.FullUpdate] replaces a pending
// [change.PolicyChange] in the batcher ([Batcher.addToBatch]
// short-circuits on [change.HasFull]). The [change.PolicyChange]
// would have computed PeersRemoved via
// [multiChannelNodeConn.computePeerDiff], but the [change.FullUpdate]
// path uses [MapResponseBuilder.WithPeers] which sets Peers: [].
//
// Fix: when a full update results in zero peers, compute the diff
// against lastSentPeers and add explicit PeersRemoved entries so
@ -206,7 +208,7 @@ type workResult struct {
// work represents a unit of work to be processed by workers.
// All pending changes for a node are bundled into a single work item
// so that one worker processes them sequentially. This prevents
// out-of-order MapResponse delivery and races on lastSentPeers
// out-of-order [tailcfg.MapResponse] delivery and races on lastSentPeers
// that occur when multiple workers process changes for the same node.
type work struct {
changes []change.Change
@ -225,9 +227,9 @@ var (
// Batcher batches and distributes map responses to connected nodes.
// It uses concurrent maps, per-node mutexes, and a worker pool.
//
// Lifecycle: Call Start() to spawn workers, then Close() to shut down.
// Close() blocks until all workers have exited. A Batcher must not
// be reused after Close().
// Lifecycle: Call [Batcher.Start] to spawn workers, then [Batcher.Close]
// to shut down. [Batcher.Close] blocks until all workers have exited.
// A [Batcher] must not be reused after [Batcher.Close].
type Batcher struct {
tick *time.Ticker
mapper *mapper
@ -551,11 +553,11 @@ func (b *Batcher) addToBatch(changes ...change.Change) {
// still has it registered. By cleaning up here, we prevent "node not found"
// errors when workers try to generate map responses for deleted nodes.
//
// Safety: change.Change.PeersRemoved is ONLY populated when nodes are actually
// deleted from the system (via change.NodeRemoved in state.DeleteNode). Policy
// changes that affect peer visibility do NOT use this field - they set
// Safety: [change.Change.PeersRemoved] is ONLY populated when nodes are actually
// deleted from the system (via [change.NodeRemoved] in [state.State.DeleteNode]).
// Policy changes that affect peer visibility do NOT use this field - they set
// RequiresRuntimePeerComputation=true and compute removed peers at runtime,
// putting them in tailcfg.MapResponse.PeersRemoved (a different struct).
// putting them in [tailcfg.MapResponse.PeersRemoved] (a different struct).
// Therefore, this cleanup only removes nodes that are truly being deleted,
// not nodes that are still connected but have lost visibility of certain peers.
//
@ -638,8 +640,8 @@ func (b *Batcher) processBatchedChanges() {
}
// cleanupOfflineNodes removes nodes that have been offline for too long to prevent memory leaks.
// Uses Compute() for atomic check-and-delete to prevent TOCTOU races where a node
// reconnects between the hasActiveConnections() check and the Delete() call.
// Uses xsync.Map.Compute for atomic check-and-delete to prevent TOCTOU races where a node
// reconnects between the hasActiveConnections check and the Delete call.
func (b *Batcher) cleanupOfflineNodes() {
var nodesToCleanup []types.NodeID

View file

@ -14,7 +14,7 @@ import (
"tailscale.com/util/multierr"
)
// MapResponseBuilder provides a fluent interface for building tailcfg.MapResponse.
// MapResponseBuilder provides a fluent interface for building [tailcfg.MapResponse].
type MapResponseBuilder struct {
resp *tailcfg.MapResponse
mapper *mapper
@ -180,6 +180,10 @@ func (b *MapResponseBuilder) WithUserProfiles(peers views.Slice[types.NodeView])
}
// WithPacketFilters adds packet filter rules based on policy.
//
// [State.FilterForNode] returns rules already reduced to only those relevant for this node.
// For autogroup:self policies, it returns per-node compiled rules.
// For global policies, it returns the global filter reduced for this node.
func (b *MapResponseBuilder) WithPacketFilters() *MapResponseBuilder {
node, ok := b.mapper.state.GetNodeByID(b.nodeID)
if !ok {
@ -187,9 +191,6 @@ func (b *MapResponseBuilder) WithPacketFilters() *MapResponseBuilder {
return b
}
// FilterForNode returns rules already reduced to only those relevant for this node.
// For autogroup:self policies, it returns per-node compiled rules.
// For global policies, it returns the global filter reduced for this node.
filter, err := b.mapper.state.FilterForNode(node)
if err != nil {
b.addError(err)
@ -233,7 +234,8 @@ func (b *MapResponseBuilder) WithPeerChanges(peers views.Slice[types.NodeView])
return b
}
// buildTailPeers converts views.Slice[types.NodeView] to []tailcfg.Node with policy filtering and sorting.
// buildTailPeers converts [views.Slice] of [types.NodeView] to a slice of [tailcfg.Node]
// with policy filtering and sorting.
func (b *MapResponseBuilder) buildTailPeers(peers views.Slice[types.NodeView]) ([]*tailcfg.Node, error) {
node, ok := b.mapper.state.GetNodeByID(b.nodeID)
if !ok {
@ -241,9 +243,10 @@ func (b *MapResponseBuilder) buildTailPeers(peers views.Slice[types.NodeView]) (
}
// Get unreduced matchers for peer relationship determination.
// MatchersForNode returns unreduced matchers that include all rules where the node
// could be either source or destination. This is different from FilterForNode which
// returns reduced rules for packet filtering (only rules where node is destination).
// [State.MatchersForNode] returns unreduced matchers that include all rules where the
// node could be either source or destination. This is different from
// [State.FilterForNode] which returns reduced rules for packet filtering (only rules
// where node is destination).
matchers, err := b.mapper.state.MatchersForNode(node)
if err != nil {
return nil, err

View file

@ -69,7 +69,7 @@ func newMapper(
}
}
// generateUserProfiles creates user profiles for MapResponse.
// generateUserProfiles creates user profiles for [tailcfg.MapResponse].
func generateUserProfiles(
node types.NodeView,
peers views.Slice[types.NodeView],
@ -267,7 +267,7 @@ func addNextDNSMetadata(resolvers []*dnstype.Resolver, node types.NodeView) {
}
}
// fullMapResponse returns a MapResponse for the given node.
// fullMapResponse returns a [tailcfg.MapResponse] for the given node.
//
//nolint:unused
func (m *mapper) fullMapResponse(
@ -312,7 +312,7 @@ func (m *mapper) selfMapResponse(
return ma, err
}
// policyChangeResponse creates a MapResponse for policy changes.
// policyChangeResponse creates a [tailcfg.MapResponse] for policy changes.
// It sends:
// - PeersRemoved for peers that are no longer visible after the policy change
// - PeersChanged for remaining peers (their AllowedIPs may have changed due to policy)
@ -350,7 +350,7 @@ func (m *mapper) policyChangeResponse(
}
if len(removedPeers) > 0 {
// Convert tailcfg.NodeID to types.NodeID for WithPeersRemoved
// Convert [tailcfg.NodeID] to [types.NodeID] for [MapResponseBuilder.WithPeersRemoved]
removedIDs := make([]types.NodeID, len(removedPeers))
for i, id := range removedPeers {
removedIDs[i] = types.NodeID(id) //nolint:gosec // NodeID types are equivalent
@ -371,7 +371,7 @@ func (m *mapper) policyChangeResponse(
return builder.Build()
}
// buildFromChange builds a MapResponse from a change.Change specification.
// buildFromChange builds a [tailcfg.MapResponse] from a [change.Change] specification.
// This provides fine-grained control over what gets included in the response.
func (m *mapper) buildFromChange(
nodeID types.NodeID,

View file

@ -17,10 +17,10 @@ import (
"tailscale.com/tailcfg"
)
// errNoActiveConnections is returned by send when a node has no active
// connections (disconnected but kept in the batcher for rapid reconnection).
// Callers must not update peer tracking state (lastSentPeers) after this
// error because the data was never delivered to any client.
// errNoActiveConnections is returned by [multiChannelNodeConn.send] when a node
// has no active connections (disconnected but kept in the batcher for rapid
// reconnection). Callers must not update peer tracking state (lastSentPeers)
// after this error because the data was never delivered to any client.
var errNoActiveConnections = errors.New("no active connections")
// connectionEntry represents a single connection to a node.
@ -51,9 +51,9 @@ type multiChannelNodeConn struct {
// workMu serializes change processing for this node across batch ticks.
// Without this, two workers could process consecutive ticks' bundles
// concurrently, causing out-of-order MapResponse delivery and races
// on lastSentPeers (Clear+Store in updateSentPeers vs Range in
// computePeerDiff).
// concurrently, causing out-of-order [tailcfg.MapResponse] delivery and races
// on lastSentPeers (Clear+Store in [multiChannelNodeConn.updateSentPeers] vs
// Range in [multiChannelNodeConn.computePeerDiff]).
workMu sync.Mutex
closeOnce sync.Once
@ -62,7 +62,7 @@ type multiChannelNodeConn struct {
// disconnectedAt records when the last connection was removed.
// nil means the node is considered connected (or newly created);
// non-nil means the node disconnected at the stored timestamp.
// Used by cleanupOfflineNodes to evict stale entries.
// Used by [Batcher.cleanupOfflineNodes] to evict stale entries.
disconnectedAt atomic.Pointer[time.Time]
// lastSentPeers tracks which peers were last sent to this node.
@ -182,8 +182,8 @@ func (mc *multiChannelNodeConn) markConnected() {
}
// markDisconnected records the current time as the moment the node
// lost its last connection. Used by cleanupOfflineNodes to determine
// how long the node has been offline.
// lost its last connection. Used by [Batcher.cleanupOfflineNodes] to
// determine how long the node has been offline.
func (mc *multiChannelNodeConn) markDisconnected() {
now := time.Now()
mc.disconnectedAt.Store(&now)
@ -235,8 +235,8 @@ func (mc *multiChannelNodeConn) drainPending() []change.Change {
// connection can block for up to 50ms), the method snapshots connections under
// a read lock, sends without any lock held, then write-locks only to remove
// failures. New connections added between the snapshot and cleanup are safe:
// they receive a full initial map via AddNode, so missing this update causes
// no data loss.
// they receive a full initial map via [Batcher.AddNode], so missing this update
// causes no data loss.
func (mc *multiChannelNodeConn) send(data *tailcfg.MapResponse) error {
if data == nil {
return nil
@ -389,7 +389,7 @@ func (mc *multiChannelNodeConn) version() tailcfg.CapabilityVersion {
return mc.connections[0].version
}
// updateSentPeers updates the tracked peer state based on a sent MapResponse.
// updateSentPeers updates the tracked peer state based on a sent [tailcfg.MapResponse].
// This must be called after successfully sending a response to keep track of
// what the client knows about, enabling accurate diffs for future updates.
func (mc *multiChannelNodeConn) updateSentPeers(resp *tailcfg.MapResponse) {