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:
parent
17236fd284
commit
4cca63155d
124 changed files with 1037 additions and 1011 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue