Commit Graph

4058 Commits

Author SHA1 Message Date
Kristoffer Dalby dcd95698e4 integration,ci: remove TestSubnetToSubnetACL
The integration test cannot prove end-to-end subnet-to-subnet data
plane in Docker-in-Docker: forwarding from eth0 through the
Tailscale tunnel interface requires Linux policy routing that
doesn't work in the hi test runner's container environment.

The #3157 fix (CIDR-only ACL making subnet routers peers) is
validated by the golden data compat tests (f10-f16) which compare
headscale's filter compilation and peer visibility against SaaS.

Updates #3157
backup-cleanup
2026-04-13 14:58:10 +00:00
Kristoffer Dalby 073f11b6bf testdata,policy/v2: add f16/f18 golden captures and fix compat test
f16: CIDR-only bidirectional ACL — proves SaaS makes subnet routers
peers with CIDR-only rules and generates correct filter rules.

f18: CIDR + tag-based ACL — adds tag:ha ↔ tag:router rules.

Fix TestRoutesCompatReduceRoutes to pair src↔dst per-rule instead
of cross-product across all rules. Tag-based rules resolve to node
IPs in filter dests, not subnet CIDRs — a source in the tag rule
should only be tested against that rule's destinations.

Updates #3157
2026-04-13 14:57:55 +00:00
Kristoffer Dalby 9b6bfd05bc servertest: approve only SaaS-approved routes in via compat tests
Use topology approved_routes instead of approving all advertised
routes. Nodes with unapproved exit routes were incorrectly getting
exit routes approved, causing AllowedIPs mismatches.

Updates #3157
2026-04-13 14:57:39 +00:00
Kristoffer Dalby fa94bd949c state: include approved exit routes in RoutesForPeer
SaaS golden data (routes-ea* captures) proves that approved exit
routes appear in every peer's AllowedIPs. The previous commit
incorrectly dropped them based on a via-grant scenario where
exit routes were not approved.

TestRoutesCompatPeerAllowedIPs validates this against all captured
netmaps: peer AllowedIPs = node IPs + HA primary routes + exit
routes, filtered through ReduceRoutes.

Updates #3157
2026-04-12 21:40:07 +00:00
Kristoffer Dalby cad0cb2f5c ci,lint: fix lint issues and add TestSubnetToSubnetACL to workflow
Updates #3157
2026-04-12 21:39:27 +00:00
Kristoffer Dalby c278418b94 policy/v2: add exit node auto-approval compat tests
15 scenarios (ea1-ea15) covering exitNode field, routes map, both,
wrong tags, dual nodes, and baselines.

SaaS only approves exit routes via exitNode field. Exit routes in
the routes map are ignored for exit approval but still wildcard
subnet approval. Headscale already matches this behavior.

Remove IsExitRoute skip from TestRoutesCompatAutoApproval.

Updates #3157
Updates #3178
2026-04-12 20:46:14 +00:00
Kristoffer Dalby 3a81dbd1c0 testdata: add tscap golden captures for all corpora
Updates #3157
2026-04-12 19:28:28 +00:00
Kristoffer Dalby 9d4af0009d policy/v2: derive exit node names from topology
Updates #3157
2026-04-12 19:23:52 +00:00
Kristoffer Dalby 077621eef3 state: drop exit routes from RoutesForPeer
Updates #3157
2026-04-12 19:23:52 +00:00
Kristoffer Dalby d11be77f1f testcapture: type captured data with tailcfg types
Updates #3157
2026-04-12 19:23:52 +00:00
Kristoffer Dalby f60f89647b policyutil: reduce filter rules by approved routes
Updates #3157
2026-04-12 19:23:52 +00:00
Kristoffer Dalby 387dcd679e policy/v2: ignore SSH check action implementation diffs
Updates #3157
2026-04-12 19:23:52 +00:00
Kristoffer Dalby f2f7c5704a hscontrol/types: silence zerolog by default in tests
Updates #3157
2026-04-12 19:23:52 +00:00
Kristoffer Dalby c26cf2f2d8 compat tests: switch to anonymized names
Updates #3157
2026-04-12 19:05:12 +00:00
Kristoffer Dalby f34765ad1e testcapture: add capture format package
Updates #3157
2026-04-12 19:05:12 +00:00
Kristoffer Dalby 7feb5a52e4 policy/v2: build ACL compat nodes from golden file topology
Updates #3157
2026-04-12 19:05:12 +00:00
Kristoffer Dalby 0cb9536784 policy/v2: remove obsolete equivalence tests
Updates #3157
2026-04-12 19:05:12 +00:00
Kristoffer Dalby 960219e52c policy/v2: make ACL error validation strict
Updates #3157
2026-04-12 19:05:12 +00:00
Kristoffer Dalby 8201be9b4b policy: document CanAccessRoute and filterForNodeLocked design
Updates #3157
2026-04-12 19:05:12 +00:00
Kristoffer Dalby f55d6c3cb0 policy/v2: wire PolicyManager through compiledGrant
Updates #3157
2026-04-12 19:05:12 +00:00
Kristoffer Dalby 1ea3b676f2 policyutil: fix reduceCapGrantRule and add reduction tests
Updates #3157
2026-04-12 19:05:12 +00:00
Kristoffer Dalby ac8fc70e7d policy/v2: add false-positive peer test for all golden files
Updates #3157
2026-04-12 19:05:12 +00:00
Kristoffer Dalby f678bcb6c2 policy/v2: add filter compilation equivalence tests
Updates #3157
2026-04-12 19:05:12 +00:00
Kristoffer Dalby 3e2dd93c33 servertest: improve via compat test infrastructure
Updates #3157
2026-04-12 19:05:12 +00:00
Kristoffer Dalby 8038c39e23 policy/matcher: add unit tests
Updates #3157
2026-04-12 19:05:12 +00:00
Kristoffer Dalby b9a9b5026f policy/v2: add auto-approval and route access compat tests
Updates #3157
2026-04-12 19:05:12 +00:00
Kristoffer Dalby 5816a5a8b1 policy/v2: add peer visibility compat tests for subnet ACLs
Updates #3157
2026-04-12 19:05:12 +00:00
Ubuntu 1d57358314 integration: add subnet-to-subnet ACL routing test
Updates #3157
2026-04-10 13:11:12 +00:00
Ubuntu 3c125e62a3 types: consider subnet routes as source identity in ACL matching
CanAccess and CanAccessRoute now check approved subnet routes
alongside Tailscale IPs when matching ACL source sets.

Fixes #3157
Fixes #3169
2026-04-10 13:11:11 +00:00
Ubuntu 8d3ae36675 types,policy: add tests for subnet-to-subnet route visibility
Updates #3157
2026-04-10 13:11:11 +00:00
Kristoffer Dalby 1f9635c2ec ci: restrict test generator to .go files
The integration test generator scanned all files under integration/
with ripgrep, matching func Test* patterns in README.md code examples
(TestMyScenario, TestRouteAdvertisementBasic). Add --type go to limit
the search to Go source files.
2026-04-10 14:09:57 +01:00
Kristoffer Dalby fd1074160e CHANGELOG: document user-facing changes from #3180 2026-04-10 14:09:57 +01:00
Kristoffer Dalby d66d3a4269 oidc: add confirmation page for node registration
Render an interstitial showing device hostname, OS, and machine-key
fingerprint before finalising OIDC registration. The user must POST
to /register/confirm/{auth_id} with a CSRF double-submit cookie.
Removes the TODO at oidc.go:201.
2026-04-10 14:09:57 +01:00
Kristoffer Dalby d5a4e6e36a debug: route statsviz through tsweb.Protected
Build the statsviz Server directly and wrap its Index/Ws handlers in
tsweb.Protected instead of calling statsviz.Register on the raw mux
which bypasses AllowDebugAccess.
2026-04-10 14:09:57 +01:00
Kristoffer Dalby 8c6cb05ab4 noise: pass context to sshActionFollowUp
Select on ctx.Done() alongside auth.WaitForAuth() so the goroutine
exits promptly when the client disconnects instead of parking until
cache eviction.
2026-04-10 14:09:57 +01:00
Kristoffer Dalby 42b8c779a0 hscontrol: limit /verify request body size
Wrap req.Body with io.LimitReader bounded to 4 KiB before
io.ReadAll. The DERP verify payload is a few hundred bytes.
2026-04-10 14:09:57 +01:00
Kristoffer Dalby a3c4ad2ca3 types: omit secret fields from JSON marshalling
Add json:"-" to PostgresConfig.Pass, OIDCConfig.ClientSecret, and
CLIConfig.APIKey so they are excluded from json.Marshal output
(e.g. the /debug/config endpoint).
2026-04-10 14:09:57 +01:00
Kristoffer Dalby 0641771128 db: guard UsePreAuthKey with WHERE used=false
Add a row-level check so concurrent registrations with the same
single-use key cannot both succeed. Skip the call on
re-registration where the key is already marked used (#2830).
2026-04-10 14:09:57 +01:00
Kristoffer Dalby f7d8bb8b3f app: remove gRPC reflection from remote server
Reflection is a streaming RPC and bypasses the unary auth
interceptor on the remote (TCP) gRPC server. Remove it there;
the unix-socket server retains it for local debugging.
2026-04-10 14:09:57 +01:00
Kristoffer Dalby adb9467f60 oidc: validate state parameter length in callback
getCookieName sliced value[:6] unconditionally; a short state query
parameter caused a panic recovered by chi middleware. Reject states
shorter than cookieNamePrefixLen with 400.
2026-04-10 14:09:57 +01:00
Kristoffer Dalby 41d70fe87b auth: check machine key on tailscaled-restart fast path
The #2862 restart path returned nodeToRegisterResponse after a
NodeKey-only lookup without verifying MachineKey. Add the same
check handleLogout already performs.
2026-04-10 14:09:57 +01:00
Kristoffer Dalby 99767cf805 hscontrol: validate machine key and bind src/dst in SSH check handler
SSHActionHandler now verifies that the Noise session's machine key
matches the dst node before proceeding. The (src, dst) pair is
captured at hold-and-delegate time via a new SSHCheckBinding on
AuthRequest so sshActionFollowUp can verify the follow-up URL
matches. The OIDC non-registration callback requires the
authenticated user to own the src node before approving.
2026-04-10 14:09:57 +01:00
Kristoffer Dalby 0d4f2293ff state: replace zcache with bounded LRU for auth cache
Replace zcache with golang-lru/v2/expirable for both the state auth
cache and the OIDC state cache. Add tuning.register_cache_max_entries
(default 1024) to cap the number of pending registration entries.

Introduce types.RegistrationData to replace caching a full *Node;
only the fields the registration callback path reads are retained.
Remove the dead HSDatabase.regCache field. Drop zgo.at/zcache/v2
from go.mod.
2026-04-10 14:09:57 +01:00
Kristoffer Dalby 3587225a88 mapper: fix phantom updateSentPeers on disconnected nodes
When send() is called on a node with zero active connections
(disconnected but kept for rapid reconnection), it returns nil
(success). handleNodeChange then calls updateSentPeers, recording
peers as delivered when no client received the data.

This corrupts lastSentPeers: future computePeerDiff calculations
produce wrong results because they compare against phantom state.
After reconnection, the node's initial map resets lastSentPeers,
but any changes processed during the disconnect window leave
stale entries that cause asymmetric peer visibility.

Return errNoActiveConnections from send() when there are no
connections. handleNodeChange treats this as a no-op (the change
was generated but not deliverable) and skips updateSentPeers,
keeping lastSentPeers consistent with what clients actually
received.
2026-04-10 13:18:56 +01:00
Kristoffer Dalby 9371b4ee28 mapper: fix empty Peers list not clearing client peer state
When a FullUpdate produces zero visible peers (e.g., a restrictive
policy isolates a node), the MapResponse has Peers: [] (empty
non-nil). The Tailscale client only processes Peers as a full
replacement when len(Peers) > 0 (controlclient/map.go:462), so an
empty list is silently ignored and stale peers persist.

This triggers when a FullUpdate() replaces a pending PolicyChange()
in the batcher. The PolicyChange would have used computePeerDiff to
send explicit PeersRemoved, but the FullUpdate goes through
buildFromChange which sets Peers: [] that the client ignores.

When a full update produces zero peers, compute the peer diff
against lastSentPeers and add explicit PeersRemoved entries so the
client correctly clears its stale peer state.
2026-04-10 13:18:56 +01:00
Kristoffer Dalby cef5338cfe types/change: panic on Merge with conflicting TargetNode values
Merging two changes targeted at different nodes is not supported
because the result can only carry one TargetNode. The second
target's content would be silently misrouted.

Add a panic guard that catches this at the Merge call site rather
than allowing silent data loss. In production, Merge is only called
with broadcast changes (TargetNode=0) so the guard acts as
insurance against future misuse.
2026-04-10 13:18:56 +01:00
Kristoffer Dalby 3529fe0da1 types: fix OIDC identifier path traversal dropping subject
url.JoinPath resolves path-traversal segments like '..' and '.',
which silently drops the OIDC subject from the identifier. For
example, Iss='https://example.com' with Sub='..' produces
'https://example.com' — the subject is lost entirely. This causes
distinct OIDC users to receive colliding identifiers.

Replace url.JoinPath with simple string concatenation using a slash
separator. This preserves the subject literally regardless of its
content. url.PathEscape does not help because dots are valid URL
path characters and are not escaped.
2026-04-10 13:18:56 +01:00
Kristoffer Dalby 4064f13bda types: fix nil panics in Owner() and TailscaleUserID() for orphaned nodes
Owner() on a non-tagged node with nil User returns an invalid
UserView that panics when Name() is called. Add a guard to return
an empty UserView{} when the user is not valid.

TailscaleUserID() calls UserID().Get() without checking Valid()
first, which panics on orphaned nodes (no tags, no UserID). Add a
validity check to return 0 for this invalid state.

Callers should check Owner().Valid() before accessing fields.
2026-04-10 13:18:56 +01:00
Kristoffer Dalby 3037e5eee0 db: fix slice aliasing in migration tag merge
The migration at db.go:680 appends validated tags to existing tags
using append(existingTags, validatedTags...) where existingTags
aliases node.Tags. When node.Tags has spare capacity, append writes
into the shared backing array, and the subsequent slices.Sort
corrupts the original.

Clone existingTags before appending to prevent aliasing.
2026-04-10 13:18:56 +01:00
Kristoffer Dalby 82bb4331f5 state: fix routesChanged mutating input Hostinfo
routesChanged aliases newHI.RoutableIPs into a local variable then
sorts it in place, which mutates the caller's Hostinfo data. The
Hostinfo is subsequently stored on the node, so the mutation
propagates but the input contract is violated.

Clone the slice before sorting to avoid mutating the input.
2026-04-10 13:18:56 +01:00