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

@ -96,7 +96,7 @@ func AssertPeerGone(tb testing.TB, observer *TestClient, peerName string) {
}
// AssertPeerHasAllowedIPs checks that a peer has the expected
// AllowedIPs prefixes.
// [tailcfg.Node.AllowedIPs] prefixes.
func AssertPeerHasAllowedIPs(tb testing.TB, observer *TestClient, peerName string, want []netip.Prefix) {
tb.Helper()
@ -211,7 +211,7 @@ func AssertSelfHasAddresses(tb testing.TB, client *TestClient) {
}
}
// EventuallyAssertMeshComplete retries AssertMeshComplete up to
// EventuallyAssertMeshComplete retries [AssertMeshComplete] up to
// timeout, useful when waiting for state to propagate.
func EventuallyAssertMeshComplete(tb testing.TB, clients []*TestClient, timeout time.Duration) {
tb.Helper()

View file

@ -19,8 +19,8 @@ import (
"tailscale.com/util/eventbus"
)
// TestClient wraps a Tailscale controlclient.Direct connected to a
// TestServer. It tracks all received NetworkMap updates, providing
// TestClient wraps a Tailscale [controlclient.Direct] connected to a
// [TestServer]. It tracks all received [netmap.NetworkMap] updates, providing
// helpers to wait for convergence and inspect the client's view of
// the network.
type TestClient struct {
@ -37,13 +37,13 @@ type TestClient struct {
pollCancel context.CancelFunc
pollDone chan struct{}
// Accumulated state from MapResponse callbacks.
// Accumulated state from [tailcfg.MapResponse] callbacks.
mu sync.RWMutex
netmap *netmap.NetworkMap
history []*netmap.NetworkMap
// updates is a buffered channel that receives a signal
// each time a new NetworkMap arrives.
// each time a new [netmap.NetworkMap] arrives.
updates chan *netmap.NetworkMap
bus *eventbus.Bus
@ -51,7 +51,7 @@ type TestClient struct {
tracker *health.Tracker
}
// ClientOption configures a TestClient.
// ClientOption configures a [TestClient].
type ClientOption func(*clientConfig)
type clientConfig struct {
@ -66,7 +66,7 @@ func WithEphemeral() ClientOption {
return func(c *clientConfig) { c.ephemeral = true }
}
// WithHostname sets the client's hostname in Hostinfo.
// WithHostname sets the client's hostname in [tailcfg.Hostinfo].
func WithHostname(name string) ClientOption {
return func(c *clientConfig) { c.hostname = name }
}
@ -82,7 +82,7 @@ func WithUser(user *types.User) ClientOption {
return func(c *clientConfig) { c.user = user }
}
// NewClient creates a TestClient, registers it with the TestServer
// NewClient creates a [TestClient], registers it with the [TestServer]
// using a pre-auth key, and starts long-polling for map updates.
func NewClient(tb testing.TB, server *TestServer, name string, opts ...ClientOption) *TestClient {
tb.Helper()
@ -171,7 +171,7 @@ func NewClient(tb testing.TB, server *TestServer, name string, opts ...ClientOpt
return tc
}
// register performs the initial TryLogin to register the client.
// register performs the initial [controlclient.Direct.TryLogin] to register the client.
func (c *TestClient) register(tb testing.TB) {
tb.Helper()
@ -188,7 +188,7 @@ func (c *TestClient) register(tb testing.TB) {
}
}
// startPoll begins the long-poll MapRequest loop.
// startPoll begins the long-poll [tailcfg.MapRequest] loop.
func (c *TestClient) startPoll(tb testing.TB) {
tb.Helper()
@ -197,14 +197,14 @@ func (c *TestClient) startPoll(tb testing.TB) {
go func() {
defer close(c.pollDone)
// PollNetMap blocks until ctx is cancelled or the server closes
// [controlclient.Direct.PollNetMap] blocks until ctx is cancelled or the server closes
// the connection.
_ = c.direct.PollNetMap(c.pollCtx, c)
}()
}
// UpdateFullNetmap implements controlclient.NetmapUpdater.
// Called by controlclient.Direct when a new NetworkMap is received.
// UpdateFullNetmap implements [controlclient.NetmapUpdater].
// Called by [controlclient.Direct] when a new [netmap.NetworkMap] is received.
func (c *TestClient) UpdateFullNetmap(nm *netmap.NetworkMap) {
c.mu.Lock()
c.netmap = nm
@ -259,7 +259,7 @@ func (c *TestClient) Disconnect(tb testing.TB) {
}
// Reconnect registers and starts a new long-poll session.
// Call Disconnect first, or this will disconnect automatically.
// Call [TestClient.Disconnect] first, or this will disconnect automatically.
func (c *TestClient) Reconnect(tb testing.TB) {
tb.Helper()
@ -274,7 +274,7 @@ func (c *TestClient) Reconnect(tb testing.TB) {
}
}
// Clear stale netmap data so that callers like WaitForPeers
// Clear stale netmap data so that callers like [TestClient.WaitForPeers]
// actually wait for the new session's map instead of returning
// immediately based on the old session's cached state.
c.mu.Lock()
@ -282,7 +282,7 @@ func (c *TestClient) Reconnect(tb testing.TB) {
c.mu.Unlock()
// Drain any pending updates from the old session so they
// don't satisfy a subsequent WaitForPeers/WaitForUpdate.
// don't satisfy a subsequent [TestClient.WaitForPeers]/[TestClient.WaitForUpdate].
for {
select {
case <-c.updates:
@ -315,7 +315,7 @@ func (c *TestClient) ReconnectAfter(tb testing.TB, d time.Duration) {
// --- State accessors ---
// Netmap returns the latest NetworkMap, or nil if none received yet.
// Netmap returns the latest [netmap.NetworkMap], or nil if none received yet.
func (c *TestClient) Netmap() *netmap.NetworkMap {
c.mu.RLock()
defer c.mu.RUnlock()
@ -425,7 +425,7 @@ func (c *TestClient) UpdateCount() int {
return len(c.history)
}
// History returns a copy of all NetworkMap snapshots in order.
// History returns a copy of all [netmap.NetworkMap] snapshots in order.
func (c *TestClient) History() []*netmap.NetworkMap {
c.mu.RLock()
defer c.mu.RUnlock()
@ -495,13 +495,13 @@ func (c *TestClient) WaitForCondition(tb testing.TB, desc string, timeout time.D
}
}
// Direct returns the underlying controlclient.Direct for
// advanced operations like SetHostinfo or SendUpdate.
// Direct returns the underlying [controlclient.Direct] for
// advanced operations like [controlclient.Direct.SetHostinfo] or SendUpdate.
func (c *TestClient) Direct() *controlclient.Direct {
return c.direct
}
// String implements fmt.Stringer for debug output.
// String implements [fmt.Stringer] for debug output.
func (c *TestClient) String() string {
nm := c.Netmap()
if nm == nil {

View file

@ -15,16 +15,16 @@ import (
)
// TestConnectDisconnectRace targets the residual TOCTOU window in
// state.Disconnect: the connectGeneration check at state.go:644 is not
// atomic with the subsequent NodeStore.UpdateNode and
// primaryRoutes.SetRoutes calls. A new Connect that runs between the
// [state.State.Disconnect]: the connectGeneration check at state.go:644 is not
// atomic with the subsequent [state.NodeStore.UpdateNode] and
// primaryRoutes.SetRoutes calls. A new [state.State.Connect] that runs between the
// gen check and the mutations can have its effects overwritten by the
// stale Disconnect's SetRoutes(empty).
// stale [state.State.Disconnect]'s SetRoutes(empty).
//
// The poll.go grace-period flow protects against the most common case
// (RemoveNode + stillConnected). Connect/Disconnect on State directly
// ([state.State.RemoveNode] + stillConnected). Connect/Disconnect on [state.State] directly
// bypasses that protection and should still leave the state consistent
// — if it doesn't, that is the bug behind issue #3203.
// — if it doesn't, that is the bug behind the original race issue.
//
// Run with -race to also catch any data race exposed.
func TestConnectDisconnectRace(t *testing.T) {
@ -33,15 +33,15 @@ func TestConnectDisconnectRace(t *testing.T) {
route := netip.MustParsePrefix("10.0.0.0/24")
// Use NewClient to get a node fully registered + Connected via the
// real noise/poll path. After this, NodeStore + primaryRoutes already
// have the node, and Connect has been called once.
// Use [servertest.NewClient] to get a node fully registered + Connected via the
// real noise/poll path. After this, [state.NodeStore] + primaryRoutes already
// have the node, and [state.State.Connect] has been called once.
//
// Only c2 advertises the route. PrimaryRoutes preserves a current
// Only c2 advertises the route. [tailcfg.NodeView.PrimaryRoutes] preserves a current
// primary across changes (anti-flap, see primary.go), so if both
// nodes were advertising, c1 (lower NodeID) would stay primary and
// the test could never observe the route slipping out of c2's
// PrimaryRoutes — it would never have been there in the first place.
// [tailcfg.NodeView.PrimaryRoutes] — it would never have been there in the first place.
c1 := servertest.NewClient(t, srv, "race-r1", servertest.WithUser(user))
c2 := servertest.NewClient(t, srv, "race-r2", servertest.WithUser(user))
@ -65,19 +65,19 @@ func TestConnectDisconnectRace(t *testing.T) {
srv.App.Change(ch)
// Wait for advertisement + approval to be reflected as a primary
// route assignment in PrimaryRoutes; otherwise we'd be racing the
// initial steady-state setup, not the Connect/Disconnect window.
// route assignment in [tailcfg.NodeView.PrimaryRoutes]; otherwise we'd be racing the
// initial steady-state setup, not the [state.State.Connect]/[state.State.Disconnect] window.
require.Eventually(t, func() bool {
return slices.Contains(srv.State().GetNodePrimaryRoutes(r2ID), route)
}, 10*time.Second, 50*time.Millisecond,
"primary route should be assigned to r2 before driving the race")
// Drive the race repeatedly. Each iteration:
// 1. Call Connect(id) to obtain a fresh gen — this stands in for
// 1. Call [state.State.Connect](id) to obtain a fresh gen — this stands in for
// a session that "owns" the node.
// 2. Spawn a goroutine that issues Disconnect(id, gen) — the
// 2. Spawn a goroutine that issues [state.State.Disconnect](id, gen) — the
// stale deferred disconnect.
// 3. Concurrently spawn a goroutine that issues Connect(id) —
// 3. Concurrently spawn a goroutine that issues [state.State.Connect](id) —
// the new session arriving.
// 4. After both finish, check the state is consistent: the node
// should be online and primaryRoutes should hold the approved

View file

@ -10,7 +10,7 @@ import (
"tailscale.com/types/netmap"
)
// TestContentVerification exercises the correctness of MapResponse
// TestContentVerification exercises the correctness of [tailcfg.MapResponse]
// content: that the self node, peers, DERP map, and other fields
// are populated correctly.
func TestContentVerification(t *testing.T) {

View file

@ -23,7 +23,7 @@ import (
// channel is never closed (documented as a v3 TODO upstream).
// - https://github.com/hashicorp/golang-lru/blob/v2.0.7/expirable/expirable_lru.go#L78-L81
//
// 2. database/sql internal goroutines: Uses sync.RWMutex which is not
// 2. database/sql internal goroutines: Uses [sync.RWMutex] which is not
// durably blocking in synctest, causing hangs.
// - https://github.com/golang/go/issues/77687 (mutex as durably blocking)
//
@ -77,7 +77,7 @@ func TestEphemeralNodes(t *testing.T) {
// Ensure the ephemeral node's long-poll session is fully
// established on the server before disconnecting. Without
// this, the Disconnect may cancel a PollNetMap that hasn't
// this, the [TestClient.Disconnect] may cancel a [controlclient.Direct.PollNetMap] that hasn't
// yet reached serveLongPoll, so no grace period or ephemeral
// GC would ever be scheduled.
ephemeral.WaitForPeers(t, 1, 10*time.Second)

View file

@ -16,7 +16,7 @@ import (
// TestGrantPolicies verifies that grant-based policies propagate
// correctly through the full control plane (policy -> state -> mapper)
// and produce the expected packet filter rules in client netmaps.
// and produce the expected packet filter rules in client [netmap.NetworkMap]s.
func TestGrantPolicies(t *testing.T) { //nolint:gocyclo
t.Parallel()
@ -66,7 +66,7 @@ func TestGrantPolicies(t *testing.T) { //nolint:gocyclo
return c2.UpdateCount() > countC2
})
// Verify PacketFilter is populated with real rules from the grant.
// Verify [netmap.NetworkMap.PacketFilter] is populated with real rules from the grant.
nm1 := c1.Netmap()
require.NotNil(t, nm1)
assert.NotNil(t, nm1.PacketFilter,
@ -132,7 +132,7 @@ func TestGrantPolicies(t *testing.T) { //nolint:gocyclo
srv.App.Change(changes...)
}
// Wait for PacketFilter with cap match rules to arrive.
// Wait for [netmap.NetworkMap.PacketFilter] with cap match rules to arrive.
c1.WaitForCondition(t, "packet filter with cap grants",
10*time.Second,
func(nm *netmap.NetworkMap) bool {
@ -143,7 +143,7 @@ func TestGrantPolicies(t *testing.T) { //nolint:gocyclo
nm1 := c1.Netmap()
require.NotNil(t, nm1)
// Check that the packet filter has CapMatch entries.
// Check that the packet filter has [filtertype.CapMatch] entries.
// The main grant produces cap/drive and cap/relay.
// Companion caps (drive-sharer and relay-target) are
// generated with reversed direction.
@ -657,9 +657,9 @@ func TestGrantPolicies(t *testing.T) { //nolint:gocyclo
}
// TestGrantViaSubnetFilterRules verifies that routers with via grants
// receive PacketFilter rules that allow the steered subnet traffic.
// receive [netmap.NetworkMap.PacketFilter] rules that allow the steered subnet traffic.
// This is a regression test: without per-node filter compilation for
// via grants, the router's PacketFilter would lack rules for the
// via grants, the router's [netmap.NetworkMap.PacketFilter] would lack rules for the
// via-steered subnet destinations, causing traffic to be dropped.
func TestGrantViaSubnetFilterRules(t *testing.T) {
t.Parallel()
@ -748,7 +748,7 @@ func TestGrantViaSubnetFilterRules(t *testing.T) {
return false
})
// Critical: the router's PacketFilter MUST contain rules with
// Critical: the router's [netmap.NetworkMap.PacketFilter] MUST contain rules with
// the via-steered subnet (10.0.0.0/24) as a destination.
// Without this, the router drops traffic forwarded through it.
routerNM := routerA.Netmap()
@ -1101,7 +1101,7 @@ func hasCapMatches(matches []filtertype.Match) bool {
return false
}
// hasDstRules returns true if any Match in the slice contains a
// hasDstRules returns true if any [filtertype.Match] in the slice contains a
// non-empty Dsts list.
func hasDstRules(matches []filtertype.Match) bool {
for _, m := range matches {

View file

@ -13,13 +13,13 @@ import (
)
// Dynamic HA failover scenarios, observed from a viewer client's
// perspective. Unlike the static TestViaGrantHACompat golden tests,
// perspective. Unlike the static [TestViaGrantHACompat] golden tests,
// these exercise runtime transitions: a primary going unhealthy,
// revoking its approved route, or losing its tag, and verify that
// the viewer's netmap converges to the new primary. These are the
// the viewer's [netmap.NetworkMap] converges to the new primary. These are the
// end-to-end signals that static captures cannot cover.
// hasPeerPrimaryRoute reports whether the viewer's current netmap
// hasPeerPrimaryRoute reports whether the viewer's current [netmap.NetworkMap]
// lists route as a PrimaryRoute on the peer with the given hostname.
func hasPeerPrimaryRoute(nm *netmap.NetworkMap, peerHost string, route netip.Prefix) bool {
if nm == nil {
@ -43,7 +43,7 @@ func hasPeerPrimaryRoute(nm *netmap.NetworkMap, peerHost string, route netip.Pre
}
// TestHAFailover_ViewerSeesPrimaryFlip verifies that when an HA
// primary is marked unhealthy, the viewer's netmap flips the route's
// primary is marked unhealthy, the viewer's [netmap.NetworkMap] flips the route's
// primary assignment from the old primary to the standby.
func TestHAFailover_ViewerSeesPrimaryFlip(t *testing.T) {
t.Parallel()
@ -90,7 +90,7 @@ func TestHAFailover_ViewerSeesPrimaryFlip(t *testing.T) {
}
// TestHAFailover_ViewerSeesRouteRevoke verifies that when the primary
// revokes its approved route, the viewer's netmap re-elects the
// revokes its approved route, the viewer's [netmap.NetworkMap] re-elects the
// standby and the old primary no longer advertises the route.
func TestHAFailover_ViewerSeesRouteRevoke(t *testing.T) {
t.Parallel()

View file

@ -15,7 +15,7 @@ import (
"tailscale.com/tailcfg"
)
// advertiseAndApproveRoute sets RoutableIPs on a client and approves
// advertiseAndApproveRoute sets [tailcfg.Hostinfo.RoutableIPs] on a client and approves
// the route on the server. Returns the node ID.
func advertiseAndApproveRoute(
t *testing.T,
@ -87,7 +87,7 @@ func TestHAHealthProbe_HealthyNodes(t *testing.T) {
}
// TestHAHealthProbe_UnhealthyFailover verifies that marking a primary
// node unhealthy via the PrimaryRoutes API triggers failover to the
// node unhealthy via the [state.State.SetNodeUnhealthy] API triggers failover to the
// standby.
func TestHAHealthProbe_UnhealthyFailover(t *testing.T) {
t.Parallel()
@ -176,7 +176,7 @@ func TestHAHealthProbe_ConnectClearsUnhealthy(t *testing.T) {
srv.State().SetNodeHealth(nodeID1, false)
assert.False(t, srv.State().IsNodeHealthy(nodeID1))
// Reconnect clears unhealthy via State.Connect → ClearUnhealthy.
// Reconnect clears unhealthy via [state.State.Connect][state.State.ClearUnhealthy].
c1.Disconnect(t)
c1.Reconnect(t)
@ -190,7 +190,7 @@ func TestHAHealthProbe_ConnectClearsUnhealthy(t *testing.T) {
// that clearing a node's approved routes also clears any stale
// Unhealthy bit, mirroring the legacy routes.SetRoutes(empty)
// auto-clear. Without this, a probe timeout that lands just before
// SetApprovedRoutes would surface as a stale unhealthy node forever.
// [state.State.SetApprovedRoutes] would surface as a stale unhealthy node forever.
func TestHAHealthProbe_SetApprovedRoutesEmptyClearsUnhealthy(t *testing.T) {
t.Parallel()
@ -223,7 +223,7 @@ func TestHAHealthProbe_SetApprovedRoutesEmptyClearsUnhealthy(t *testing.T) {
// HA candidate; carrying the bit forward leaks into DebugRoutes.
//
// The poll handler waits a 10s grace period before calling
// state.Disconnect, so the assertion is wrapped in Eventually with a
// [state.State.Disconnect], so the assertion is wrapped in Eventually with a
// generous timeout.
func TestHAHealthProbe_DisconnectClearsUnhealthy(t *testing.T) {
t.Parallel()
@ -255,7 +255,7 @@ func TestHAHealthProbe_DisconnectClearsUnhealthy(t *testing.T) {
// TestHAHealthProbe_SetUnhealthyNoRoutesIsNoOp verifies the
// defensive guard for the still-online-but-no-routes case: a probe
// that fires after SetApprovedRoutes(empty) should not be allowed
// that fires after [state.State.SetApprovedRoutes](empty) should not be allowed
// to install a stale Unhealthy bit either.
func TestHAHealthProbe_SetUnhealthyNoRoutesIsNoOp(t *testing.T) {
t.Parallel()

View file

@ -69,8 +69,8 @@ func (c *checkTB) runCleanups() {
c.cleanups = nil
c.mu.Unlock()
for i := len(cs) - 1; i >= 0; i-- {
cs[i]()
for _, v := range slices.Backward(cs) {
v()
}
}

View file

@ -8,7 +8,7 @@ import (
"github.com/juanfont/headscale/hscontrol/types"
)
// TestHarness orchestrates a TestServer with multiple TestClients,
// TestHarness orchestrates a [TestServer] with multiple [TestClient] instances,
// providing a convenient setup for multi-node control plane tests.
type TestHarness struct {
Server *TestServer
@ -18,7 +18,7 @@ type TestHarness struct {
defaultUser *types.User
}
// HarnessOption configures a TestHarness.
// HarnessOption configures a [TestHarness].
type HarnessOption func(*harnessConfig)
type harnessConfig struct {
@ -33,24 +33,24 @@ func defaultHarnessConfig() *harnessConfig {
}
}
// WithServerOptions passes ServerOptions through to the underlying
// TestServer.
// WithServerOptions passes [ServerOption] values through to the underlying
// [TestServer].
func WithServerOptions(opts ...ServerOption) HarnessOption {
return func(c *harnessConfig) { c.serverOpts = append(c.serverOpts, opts...) }
}
// WithDefaultClientOptions applies ClientOptions to every client
// created by NewHarness.
// WithDefaultClientOptions applies [ClientOption] values to every client
// created by [NewHarness].
func WithDefaultClientOptions(opts ...ClientOption) HarnessOption {
return func(c *harnessConfig) { c.clientOpts = append(c.clientOpts, opts...) }
}
// WithConvergenceTimeout sets how long WaitForMeshComplete waits.
// WithConvergenceTimeout sets how long [TestHarness.WaitForMeshComplete] waits.
func WithConvergenceTimeout(d time.Duration) HarnessOption {
return func(c *harnessConfig) { c.convergenceMax = d }
}
// NewHarness creates a TestServer and numClients connected clients.
// NewHarness creates a [TestServer] and numClients connected clients.
// All clients share a default user and are registered with reusable
// pre-auth keys. The harness waits for all clients to form a
// complete mesh before returning.
@ -146,7 +146,7 @@ func (h *TestHarness) WaitForMeshComplete(tb testing.TB, timeout time.Duration)
}
// WaitForConvergence waits until all connected clients have a
// non-nil NetworkMap and their peer counts have stabilised.
// non-nil [netmap.NetworkMap] and their peer counts have stabilised.
func (h *TestHarness) WaitForConvergence(tb testing.TB, timeout time.Duration) {
tb.Helper()
h.WaitForMeshComplete(tb, timeout)

View file

@ -19,14 +19,14 @@ import (
// These tests are intentionally strict about expected behavior.
// Failures surface real issues in the control plane.
// TestIssuesMapContent tests issues with MapResponse content correctness.
// TestIssuesMapContent tests issues with [tailcfg.MapResponse] content correctness.
func TestIssuesMapContent(t *testing.T) {
t.Parallel()
// After mesh formation, all peers should have a known Online status.
// The Online field is set when Connect() sends a NodeOnline PeerChange
// patch. The initial MapResponse (from auth handler) may have Online=nil
// because Connect() hasn't run yet, so we wait for the status to propagate.
// The Online field is set when [state.State.Connect] sends a NodeOnline [tailcfg.PeerChange]
// patch. The initial [tailcfg.MapResponse] (from auth handler) may have Online=nil
// because [state.State.Connect] hasn't run yet, so we wait for the status to propagate.
t.Run("initial_map_should_include_peer_online_status", func(t *testing.T) {
t.Parallel()
h := servertest.NewHarness(t, 3)
@ -55,7 +55,7 @@ func TestIssuesMapContent(t *testing.T) {
t.Parallel()
h := servertest.NewHarness(t, 2)
// The DiscoKey is sent in the first MapRequest (not the RegisterRequest),
// The DiscoKey is sent in the first [tailcfg.MapRequest] (not the [tailcfg.RegisterRequest]),
// so it may take an extra map update to propagate to peers. Wait for
// the condition rather than checking the initial netmap.
h.Client(0).WaitForCondition(t, "peer has non-zero DiscoKey",
@ -92,7 +92,7 @@ func TestIssuesMapContent(t *testing.T) {
}
})
// Each peer should have a valid user profile in the netmap.
// Each peer should have a valid user profile in the [netmap.NetworkMap].
t.Run("all_peers_have_user_profiles", func(t *testing.T) {
t.Parallel()
@ -174,7 +174,7 @@ func TestIssuesRoutes(t *testing.T) {
// Approving a route via API without the node announcing it must NOT
// make the route visible in AllowedIPs. Tailscale uses a strict
// advertise-then-approve model: routes are only distributed when the
// node advertises them (Hostinfo.RoutableIPs) AND they are approved.
// node advertises them ([tailcfg.Hostinfo.RoutableIPs]) AND they are approved.
// An approval without advertisement is a dormant pre-approval that
// activates once the node starts advertising.
t.Run("approved_route_without_announcement_not_distributed", func(t *testing.T) {
@ -253,7 +253,7 @@ func TestIssuesRoutes(t *testing.T) {
})
})
// Hostinfo route advertisement should be stored on server.
// [tailcfg.Hostinfo] route advertisement should be stored on server.
t.Run("hostinfo_route_advertisement_stored_on_server", func(t *testing.T) {
t.Parallel()
@ -491,7 +491,7 @@ func TestIssuesServerMutations(t *testing.T) {
assert.Len(t, c3.Peers(), 1)
})
// Hostinfo changes should propagate to peers.
// [tailcfg.Hostinfo] changes should propagate to peers.
t.Run("hostinfo_changes_propagate_to_peers", func(t *testing.T) {
t.Parallel()
@ -530,11 +530,11 @@ func TestIssuesServerMutations(t *testing.T) {
})
}
// TestIssuesNodeStoreConsistency tests NodeStore + DB consistency.
// TestIssuesNodeStoreConsistency tests [state.NodeStore] + DB consistency.
func TestIssuesNodeStoreConsistency(t *testing.T) {
t.Parallel()
// NodeStore and DB should agree after mutations.
// [state.NodeStore] and DB should agree after mutations.
t.Run("nodestore_db_consistency_after_operations", func(t *testing.T) {
t.Parallel()
@ -569,7 +569,7 @@ func TestIssuesNodeStoreConsistency(t *testing.T) {
"NodeStore and DB should agree on approved routes")
})
// After rapid reconnect, NodeStore should reflect correct state.
// After rapid reconnect, [state.NodeStore] should reflect correct state.
t.Run("nodestore_correct_after_rapid_reconnect", func(t *testing.T) {
t.Parallel()
@ -673,7 +673,7 @@ func TestIssuesGracePeriod(t *testing.T) {
// Ensure the ephemeral node's long-poll session is fully
// established on the server before disconnecting. Without
// this, the Disconnect may cancel a PollNetMap that hasn't
// this, the [TestClient.Disconnect] may cancel a [controlclient.Direct.PollNetMap] that hasn't
// yet reached serveLongPoll, so no grace period or ephemeral
// GC would ever be scheduled.
ephemeral.WaitForPeers(t, 1, 10*time.Second)

View file

@ -15,7 +15,7 @@ import (
)
// TestPingNode verifies the full ping round-trip: the server sends a
// PingRequest via MapResponse, the real controlclient.Direct handles it
// [tailcfg.PingRequest] via [tailcfg.MapResponse], the real [controlclient.Direct] handles it
// by making a HEAD request back over Noise, and the ping tracker records
// the latency.
func TestPingNode(t *testing.T) {
@ -105,7 +105,7 @@ func TestPingTwoSameNode(t *testing.T) {
require.NotEqual(t, pingID1, pingID2)
// Send both PingRequests.
// Send both [tailcfg.PingRequest]s.
url1 := h.Server.URL + "/machine/ping-response?id=" + pingID1
url2 := h.Server.URL + "/machine/ping-response?id=" + pingID2
@ -136,7 +136,7 @@ func TestPingTwoSameNode(t *testing.T) {
}
}
// TestPingResolveByHostname verifies that ResolveNode can find a node
// TestPingResolveByHostname verifies that [state.State.ResolveNode] can find a node
// by hostname and that the resolved node can be pinged.
func TestPingResolveByHostname(t *testing.T) {
t.Parallel()

View file

@ -156,9 +156,9 @@ func TestPolicyChanges(t *testing.T) {
// (Prefix, Host) resolve to exactly the literal prefix and do NOT expand
// to include the matching node's other IP addresses.
//
// PacketFilter rules are INBOUND: they tell the destination node what
// [netmap.NetworkMap.PacketFilter] rules are INBOUND: they tell the destination node what
// traffic to accept. So the IPv6 destination rule appears in test2's
// PacketFilter (the destination), not test1's (the source).
// [netmap.NetworkMap.PacketFilter] (the destination), not test1's (the source).
func TestIPv6OnlyPrefixACL(t *testing.T) {
t.Parallel()
@ -193,7 +193,7 @@ func TestIPv6OnlyPrefixACL(t *testing.T) {
c1.WaitForPeers(t, 1, 10*time.Second)
c2.WaitForPeers(t, 1, 10*time.Second)
// PacketFilter is an INBOUND filter: test2 (the destination) should
// [netmap.NetworkMap.PacketFilter] is an INBOUND filter: test2 (the destination) should
// have the rule allowing traffic FROM test1's IPv6.
nm2 := c2.Netmap()
require.NotNil(t, nm2)

View file

@ -13,7 +13,7 @@ import (
)
// TestPollRace targets logical race conditions specifically in the
// poll.go session lifecycle and the batcher's handling of concurrent
// poll.go session lifecycle and the [mapper.Batcher]'s handling of concurrent
// sessions for the same node.
func TestPollRace(t *testing.T) {
@ -22,9 +22,9 @@ func TestPollRace(t *testing.T) {
// The core race: when a node disconnects, poll.go starts a
// grace period goroutine (10s ticker loop). If the node
// reconnects during this period, the new session calls
// Connect() to mark the node online. But the old grace period
// goroutine is still running and may call Disconnect() AFTER
// the new Connect(), setting IsOnline=false incorrectly.
// [state.State.Connect] to mark the node online. But the old grace period
// goroutine is still running and may call [state.State.Disconnect] AFTER
// the new [state.State.Connect], setting IsOnline=false incorrectly.
//
// This test verifies the exact symptom: after reconnect within
// the grace period, the server-side node state should be online.
@ -99,7 +99,7 @@ func TestPollRace(t *testing.T) {
// Wait the full grace period (10s) after reconnect. The old
// grace period goroutine should have checked IsConnected
// and found the node connected, so should NOT have called
// Disconnect().
// [state.State.Disconnect].
t.Run("server_state_online_12s_after_reconnect", func(t *testing.T) {
t.Parallel()
@ -195,8 +195,8 @@ func TestPollRace(t *testing.T) {
}
})
// The batcher's IsConnected check: when the grace period
// goroutine calls IsConnected(), it should return true if
// The [mapper.Batcher]'s IsConnected check: when the grace period
// goroutine calls IsConnected, it should return true if
// a new session has been added for the same node.
t.Run("batcher_knows_reconnected_during_grace", func(t *testing.T) {
t.Parallel()

View file

@ -19,7 +19,7 @@ import (
// TestRace contains tests designed to trigger race conditions in
// the control plane. Run with -race to detect data races.
// These tests stress concurrent access patterns in poll.go,
// the batcher, the NodeStore, and the mapper.
// the [mapper.Batcher], the [state.NodeStore], and the [mapper] subsystem.
// TestRacePollSessionReplacement tests the race between an old
// poll session's deferred cleanup and a new session starting.
@ -28,8 +28,8 @@ func TestRacePollSessionReplacement(t *testing.T) {
// Rapidly replace the poll session by doing immediate
// disconnect+reconnect. This races the old session's
// deferred cleanup (RemoveNode, Disconnect, grace period
// goroutine) with the new session's setup (AddNode, Connect,
// deferred cleanup ([state.NodeStore.RemoveNode], [state.State.Disconnect], grace period
// goroutine) with the new session's setup ([state.NodeStore.AddNode], [state.State.Connect],
// initial map send).
t.Run("immediate_session_replace_10x", func(t *testing.T) {
t.Parallel()
@ -393,13 +393,13 @@ func TestRaceConnectDuringGracePeriod(t *testing.T) {
})
}
// TestRaceBatcherContention tests race conditions in the batcher
// TestRaceBatcherContention tests race conditions in the [mapper.Batcher]
// when many changes arrive simultaneously.
func TestRaceBatcherContention(t *testing.T) {
t.Parallel()
// Many nodes connecting at the same time generates many
// concurrent Change() calls. The batcher must handle this
// concurrent [hscontrol.Headscale.Change] calls. The [mapper.Batcher] must handle this
// without dropping updates or panicking.
t.Run("many_simultaneous_connects", func(t *testing.T) {
t.Parallel()
@ -427,8 +427,8 @@ func TestRaceBatcherContention(t *testing.T) {
})
// Rapid connect + disconnect + connect of different nodes
// generates interleaved AddNode/RemoveNode/AddNode in the
// batcher.
// generates interleaved [state.NodeStore.AddNode]/[state.NodeStore.RemoveNode]/[state.NodeStore.AddNode] in the
// [mapper.Batcher].
t.Run("interleaved_add_remove_add", func(t *testing.T) {
t.Parallel()
@ -514,7 +514,7 @@ func TestRaceBatcherContention(t *testing.T) {
}
// TestRaceMapResponseDuringDisconnect tests what happens when a
// map response is being written while the session is being torn down.
// [tailcfg.MapResponse] is being written while the session is being torn down.
func TestRaceMapResponseDuringDisconnect(t *testing.T) {
t.Parallel()
@ -587,12 +587,12 @@ func TestRaceMapResponseDuringDisconnect(t *testing.T) {
})
}
// TestRaceNodeStoreContention tests concurrent access to the NodeStore.
// TestRaceNodeStoreContention tests concurrent access to the [state.NodeStore].
func TestRaceNodeStoreContention(t *testing.T) {
t.Parallel()
// Many GetNodeByID calls while nodes are connecting and
// disconnecting. This tests the NodeStore's read/write locking.
// Many [state.State.GetNodeByID] calls while nodes are connecting and
// disconnecting. This tests the [state.NodeStore]'s read/write locking.
t.Run("concurrent_reads_during_mutations", func(t *testing.T) {
t.Parallel()
@ -655,7 +655,7 @@ func TestRaceNodeStoreContention(t *testing.T) {
}
})
// ListNodes while nodes are being added and removed.
// [state.State.ListNodes] while nodes are being added and removed.
t.Run("list_nodes_during_churn", func(t *testing.T) {
t.Parallel()

View file

@ -1,6 +1,6 @@
// Package servertest provides an in-process test harness for Headscale's
// control plane. It wires a real Headscale server to real Tailscale
// controlclient.Direct instances, enabling fast, deterministic tests
// [controlclient.Direct] instances, enabling fast, deterministic tests
// of the full control protocol without Docker or separate processes.
package servertest
@ -19,7 +19,7 @@ import (
)
// TestServer is an in-process Headscale control server suitable for
// use with Tailscale's controlclient.Direct.
// use with Tailscale's [controlclient.Direct].
//
// Networking uses tailscale.com/net/memnet so that all TCP
// connections stay in-process — no real sockets are opened.
@ -33,7 +33,7 @@ type TestServer struct {
st *state.State
}
// ServerOption configures a TestServer.
// ServerOption configures a [TestServer].
type ServerOption func(*serverConfig)
type serverConfig struct {
@ -201,15 +201,15 @@ func (s *TestServer) State() *state.State {
// Close shuts down the in-memory HTTP server and listener.
// Subsystem cleanup (batcher, ephemeral GC) is handled by
// tb.Cleanup callbacks registered in StartBatcherForTest and
// StartEphemeralGCForTest.
// [testing.TB.Cleanup] callbacks registered in [hscontrol.Headscale.StartBatcherForTest] and
// [hscontrol.Headscale.StartEphemeralGCForTest].
func (s *TestServer) Close() {
s.httpServer.Close()
s.ln.Close()
}
// MemNet returns the in-memory network used by this server,
// so that TestClient dialers can be wired to it.
// so that [TestClient] dialers can be wired to it.
func (s *TestServer) MemNet() *memnet.Network {
return s.memNet
}

View file

@ -20,7 +20,7 @@ import (
// consistency bugs.
// TestStressConnectDisconnect exercises rapid connect/disconnect
// patterns that stress the grace period, batcher, and NodeStore.
// patterns that stress the grace period, batcher, and [state.NodeStore].
func TestStressConnectDisconnect(t *testing.T) {
t.Parallel()
@ -536,7 +536,7 @@ func TestStressDataIntegrity(t *testing.T) {
}
})
// MachineKey should be consistent: the server should track
// [netmap.NetworkMap.MachineKey] should be consistent: the server should track
// the same machine key the client registered with.
t.Run("machine_key_consistent", func(t *testing.T) {
t.Parallel()
@ -551,7 +551,7 @@ func TestStressDataIntegrity(t *testing.T) {
nm := c1.Netmap()
require.NotNil(t, nm)
// The client's MachineKey in the netmap should be non-zero.
// The client's [netmap.NetworkMap.MachineKey] should be non-zero.
assert.False(t, nm.MachineKey.IsZero(),
"client's MachineKey should be non-zero")
@ -564,7 +564,7 @@ func TestStressDataIntegrity(t *testing.T) {
"client and server should agree on MachineKey")
})
// NodeKey should be consistent between client and server.
// [netmap.NetworkMap.NodeKey] should be consistent between client and server.
t.Run("node_key_consistent", func(t *testing.T) {
t.Parallel()

View file

@ -38,7 +38,7 @@ var viaCompatTests = []struct {
}
// TestViaGrantMapCompat loads golden captures from Tailscale SaaS and
// compares headscale's MapResponse structure against the captured netmap.
// compares headscale's [tailcfg.MapResponse] structure against the captured [netmap.NetworkMap].
//
// The comparison is IP-independent: it validates peer visibility, route
// prefixes in AllowedIPs, and PrimaryRoutes — not literal Tailscale IP
@ -128,7 +128,7 @@ func runViaMapCompat(t *testing.T, c *testcapture.Capture) {
// Determine which routes each node should advertise. If the golden
// topology has explicit routable_ips, use those. Otherwise infer
// from the netmap peer AllowedIPs and packet filter dst prefixes.
// from the [netmap.NetworkMap] peer AllowedIPs and packet filter dst prefixes.
nodeRoutes := inferNodeRoutes(t, c)
// Build approved routes from topology. The topology's approved_routes
@ -181,7 +181,7 @@ func runViaMapCompat(t *testing.T, c *testcapture.Capture) {
srv.App.Change(routeChange)
}
// Wait for peers based on golden netmap expected counts.
// Wait for peers based on golden [netmap.NetworkMap] expected counts.
for viewerName, cl := range clients {
capture := c.Captures[viewerName]
if capture.Netmap == nil {
@ -202,8 +202,8 @@ func runViaMapCompat(t *testing.T, c *testcapture.Capture) {
}
}
// Ensure all nodes have received at least one MapResponse,
// including nodes with 0 expected peers that skipped WaitForPeers.
// Ensure all nodes have received at least one [tailcfg.MapResponse],
// including nodes with 0 expected peers that skipped [TestClient.WaitForPeers].
for name, cl := range clients {
cl.WaitForCondition(t, name+" initial netmap", 15*time.Second,
func(nm *netmap.NetworkMap) bool {
@ -211,7 +211,7 @@ func runViaMapCompat(t *testing.T, c *testcapture.Capture) {
})
}
// Compare each viewer's MapResponse against the golden netmap.
// Compare each viewer's [tailcfg.MapResponse] against the golden [netmap.NetworkMap].
for viewerName, cl := range clients {
capture := c.Captures[viewerName]
if capture.Netmap == nil {
@ -227,8 +227,8 @@ func runViaMapCompat(t *testing.T, c *testcapture.Capture) {
}
}
// compareNetmap compares the headscale MapResponse against the
// captured netmap data in an IP-independent way. It validates:
// compareNetmap compares the headscale [tailcfg.MapResponse] against the
// captured [netmap.NetworkMap] data in an IP-independent way. It validates:
// - Peer visibility (which peers are present, by hostname)
// - Route prefixes in AllowedIPs (non-Tailscale-IP entries like 10.44.0.0/16)
// - Number of Tailscale IPs per peer (should be 2: one v4 + one v6)
@ -430,7 +430,7 @@ func compareNetmap(
}
// saasAddrsByPeer builds a map from SaaS Tailscale address to peer
// hostname using each capture's SelfNode.Addresses. Peers not in
// hostname using each capture's [tailcfg.NodeView.Addresses]. Peers not in
// clients are skipped.
func saasAddrsByPeer(
want testcapture.Node,
@ -442,7 +442,7 @@ func saasAddrsByPeer(
return out
}
// Walk peers listed in this netmap.
// Walk peers listed in this [netmap.NetworkMap].
for _, peer := range want.Netmap.Peers {
name := extractHostname(peer.Name())
if _, isOurs := clients[name]; !isOurs {
@ -457,7 +457,7 @@ func saasAddrsByPeer(
}
}
// The viewer's own SelfNode addresses also appear as possible src.
// The viewer's own [tailcfg.NodeView] addresses also appear as possible src.
if want.Netmap.SelfNode.Valid() {
name := extractHostname(want.Netmap.SelfNode.Name())
@ -533,8 +533,8 @@ func canonicaliseSrcStrings(
}
// canonicaliseSrcPrefixes is the headscale-side counterpart of
// canonicaliseSrcStrings, reading already-parsed netip.Prefix values
// from tailcfg.Match.Srcs.
// [canonicaliseSrcStrings], reading already-parsed [netip.Prefix] values
// from [tailcfg.Match.Srcs].
func canonicaliseSrcPrefixes(
t *testing.T,
srcs []netip.Prefix,
@ -627,7 +627,7 @@ type peerSummary struct {
PrimaryRoutes []string // sorted
}
// parsePrefixOrAddr parses a string as a netip.Prefix. If the string
// parsePrefixOrAddr parses a string as a [netip.Prefix]. If the string
// is a bare IP address (no slash), it is converted to a single-host
// prefix (/32 for IPv4, /128 for IPv6). Golden data DstPorts.IP can
// contain either form.
@ -704,7 +704,7 @@ func countTailscaleIPsView(allowedIPs interface {
// inferNodeRoutes determines which routes each node should advertise.
// If the topology has explicit routable_ips, those are used. Otherwise
// routes are inferred from the netmap peer AllowedIPs and packet
// routes are inferred from the [netmap.NetworkMap] peer AllowedIPs and packet
// filter destination prefixes.
func inferNodeRoutes(t *testing.T, c *testcapture.Capture) map[string][]netip.Prefix {
t.Helper()
@ -725,7 +725,7 @@ func inferNodeRoutes(t *testing.T, c *testcapture.Capture) map[string][]netip.Pr
}
}
// Tier 2: infer from each capture's netmap — scan peers with
// Tier 2: infer from each capture's [netmap.NetworkMap] — scan peers with
// route prefixes in AllowedIPs. If node X appears as a peer with
// route prefix 10.44.0.0/16, then X should advertise that route.
for _, node := range c.Captures {

View file

@ -46,7 +46,7 @@ var viaHACompatTests = []struct {
// TestViaGrantHACompat loads golden captures from Tailscale SaaS that
// test via grant steering combined with HA primary route election.
// Each capture uses an inline topology with 4-6 nodes (instead of the
// shared 15-node grant topology used by TestViaGrantMapCompat).
// shared 15-node grant topology used by [TestViaGrantMapCompat]).
func TestViaGrantHACompat(t *testing.T) {
t.Parallel()
@ -175,7 +175,7 @@ func runViaHACompat(t *testing.T, c *testcapture.Capture) {
}
}
// Ensure all nodes have an initial netmap.
// Ensure all nodes have an initial [netmap.NetworkMap].
for name, cl := range clients {
cl.WaitForCondition(t, name+" initial netmap", 15*time.Second,
func(nm *netmap.NetworkMap) bool {
@ -183,7 +183,7 @@ func runViaHACompat(t *testing.T, c *testcapture.Capture) {
})
}
// Compare each viewer's MapResponse against golden netmap.
// Compare each viewer's [tailcfg.MapResponse] against golden [netmap.NetworkMap].
for viewerName, cl := range clients {
capture := c.Captures[viewerName]
if capture.Netmap == nil {
@ -196,9 +196,9 @@ func runViaHACompat(t *testing.T, c *testcapture.Capture) {
}
}
// compareCaptureNetmap compares headscale's MapResponse against a
// testcapture.Node's netmap data. Same logic as compareNetmap but
// reads from typed testcapture fields instead of goldenFile strings.
// compareCaptureNetmap compares headscale's [tailcfg.MapResponse] against a
// [testcapture.Node]'s [netmap.NetworkMap] data. Same logic as [compareNetmap] but
// reads from typed [testcapture] fields instead of goldenFile strings.
func compareCaptureNetmap(
t *testing.T,
viewer *servertest.TestClient,
@ -250,7 +250,7 @@ func compareCaptureNetmap(
}
}
// Build peer summaries from headscale MapResponse.
// Build peer summaries from headscale [tailcfg.MapResponse].
gotPeers := map[string]capturePeerSummary{}
for _, peer := range nm.Peers {
@ -335,7 +335,7 @@ type capturePeerSummary struct {
PrimaryRoutes []string
}
// captureNodeOrder returns node names from a testcapture.Capture
// captureNodeOrder returns node names from a [testcapture.Capture]
// sorted by SaaS node creation time, for deterministic DB ID assignment.
// SaaS elects HA primaries by registration order (first registered wins),
// which correlates with Created timestamp, not with the random snowflake
@ -377,7 +377,7 @@ func captureNodeOrder(t *testing.T, c *testcapture.Capture) []string {
return names
}
// convertCapturePolicy converts a testcapture's policy for headscale,
// convertCapturePolicy converts a [testcapture.Capture]'s policy for headscale,
// replacing SaaS emails with headscale user format. Fails the test if
// none of the known SaaS emails are present: that would mean the
// capture was regenerated with a new tag-owner identity and this