Commit Graph

231 Commits

Author SHA1 Message Date
Kristoffer Dalby 3417e4cb77 policy/v2: add via exit node captures with unrelated rules
SaaS keeps via exit steering unless a rule reaches the internet, and
sends exit nodes every rule, with or without via.

Updates #3493
2026-09-30 17:20:46 +02:00
Kristoffer Dalby 7f5fdbe5d2 policy/v2: send exit nodes every user's autogroup:self rules
Same as other rules: exit routes contain every self destination.

Updates #3493
2026-09-30 17:20:46 +02:00
Kristoffer Dalby 61d1599393 policyutil: send approved exit nodes every rule
Tailscale SaaS treats exit routes like subnet routes when reducing
filters; 0.0.0.0/0 contains every dst, so exit nodes get all rules.

Updates #3493
2026-09-30 17:20:46 +02:00
Kristoffer Dalby 7efd22d0bb policy/v2: let autogroup:internet rules lift via exit steering
A regular rule to the internet allows every exit node, but
autogroup:internet resolves to no prefix, so it never matched.

Updates #3493
2026-09-30 17:20:46 +02:00
Kristoffer Dalby c700b32eec policy/v2: stop narrow rules from undoing via exit steering
Every address overlaps 0.0.0.0/0, so any regular rule matching the
viewer dropped the exclusion; only a wildcard dst now does.

Fixes #3493
2026-09-30 17:20:46 +02:00
Kristoffer Dalby 393dd3e2d9 db: store all credentials in one SHA-256-hashed table
API keys, pre-auth keys and OAuth clients/tokens share one table and verify
path. Secrets carry 256 bits of crypto/rand entropy, so a SHA-256 digest
needs no stretching; bcrypt/argon2id rows rehash on use until 0.32.
2026-09-26 00:33:12 +02:00
Kristoffer Dalby c604874dba policy/v2: suggest approved exit nodes by default
Matches SaaS; Apple clients hide the exit-node list without a suggestion.

Fixes #3415
2026-09-23 14:48:45 +02:00
Kristoffer Dalby 06fa3075da hscontrol: replace tailscale line refs with doc links
Line numbers drift on every upstream bump.
2026-09-23 14:48:45 +02:00
Kristoffer Dalby 0e8a3bba54 policy/v2: compare peer CapMap against SaaS route captures
SaaS stamps suggest-exit-node on approved exit peers without nodeAttrs.

Updates #3415
2026-09-23 14:48:45 +02:00
Kristoffer Dalby e48bc46cc6 mapper: take peer visibility from the peer map only
Fixes #3408
2026-09-23 14:48:35 +02:00
Kristoffer Dalby 5861005ef6 policy/v2: add via exit node capture
Updates #3408
2026-09-23 14:48:35 +02:00
Kristoffer Dalby 5401edb6a8 policy: ignore '#' metadata fields across the whole policy
The filter lived in ACL.UnmarshalJSON, so grants, ssh and nodeAttrs still
hit RejectUnknownMembers. Strip the members in the HuJSON AST instead, at
the single decode entrypoint. Grant "app" payloads are left untouched.

Fixes #3479
2026-09-23 09:30:22 +02:00
Kristoffer Dalby 95ba787417 policy,state: key the peer map by node ID
Adjacency becomes immutable, so a snapshot can resolve peers through its
own fresh views instead of storing them.

Updates #3417
2026-09-10 13:06:13 +02:00
Kristoffer Dalby be322e8ea7 policy,types: skip recompile when the user list is unchanged
SetUsers now also reports whether user-derived peer adjacency moved.

Updates #3417
2026-09-10 13:06:13 +02:00
Kristoffer Dalby 63123196cc all: inline deprecated tailcfg capability aliases
go 1.27 vet reports the //go:fix inline directives tailscale added to
the tailcfg cap aliases; applied with `go fix -inline ./...`.
2026-08-25 21:59:00 +02:00
Kristoffer Dalby 1994f50fe8 policy/v2: drop elided struct types in test users
Promoted field keys let the literals skip the embedded gorm.Model.
2026-08-25 21:59:00 +02:00
Kristoffer Dalby 93e29ec38d all: use strings.CutLast
Replaces LastIndex plus slice arithmetic when splitting on the last
separator.
2026-08-25 21:59:00 +02:00
Kristoffer Dalby 77caa94400 policy/v2: use value receiver for SSHCheckPeriod.Validate
recvcheck ignores UnmarshalJSON, so the pointer receiver here was the
odd one out against value-receiver MarshalJSON. Validate only reads.
2026-08-25 21:59:00 +02:00
Kristoffer Dalby 2c76d5c5b7 all: apply golangci-lint autofixes
Go 1.27 allows setting promoted fields of embedded structs directly in
composite literals, so gorm.Model wrappers go away. Plus strings.Cut,
errors.AsType, reflect.TypeAssert and one gofumpt nit.
2026-08-25 21:59:00 +02:00
Kristoffer Dalby 6275e3a356 policy,state: authorize reauth tags against the authenticating user
Re-authenticating a tagged node with --advertise-tags checked the tag-owned
node, not the authenticating user, so every tag was rejected.

Fixes #3374
2026-07-28 11:55:17 +02:00
alaningtrump 048308511c chore: fix function comment to match actual function name
Signed-off-by: alaningtrump <alaningtrump@outlook.com>
2026-07-03 07:27:08 +02:00
Kristoffer Dalby d528686f14 mapper,policy: add reconnect-storm and lock-concurrency regression tests
TestInitialMapNotStarvedByReconnectStorm reproduces the #3346 stall;
TestPolicyManagerConcurrentReads guards the RLock cache access under -race.

Updates #3346
2026-07-01 14:41:03 +02:00
Kristoffer Dalby 6f317c7576 policy: take RLock for reads so map generation runs concurrently
One exclusive mutex serialized every policy read, so a mass reconnect on
autogroup:self/via/relay policies stalled clients into "unexpected EOF"
retries. Per-node caches become xsync.Maps for lazy population under RLock.

Fixes #3346
2026-07-01 14:41:03 +02:00
Kristoffer Dalby ee95cf58d9 hscontrol: add the OAuth client and access-token model
Add the OAuth client type, its database storage, the scope grant package,
policy tag-ownership exposure, and the state operations backing the v2
OAuth client-credentials flow.
2026-06-26 09:18:06 +02:00
Kristoffer Dalby 66937040f2 policy: allow the tailscale.com/cap/secrets capability
It's setec's official cap; allowlist it so it can be granted via policy.
2026-06-24 18:41:33 +02:00
Kristoffer Dalby 4fa6af0102 policy/v2: write the SSH check period as time arithmetic
Express the maximum SSH check period as 7 * 24 * time.Hour instead of a
bare nanosecond constant, matching how the rest of the code spells out
magic durations.
2026-06-21 04:17:03 +02:00
Kristoffer Dalby 4f4e95fc80 all: adopt strings, errors, and os helpers 2026-06-17 16:12:19 +02:00
Kristoffer Dalby 27468f944b all: adopt maps and cmp helpers 2026-06-17 16:12:19 +02:00
Kristoffer Dalby ac725f27e4 all: adopt slices helpers 2026-06-17 16:12:19 +02:00
Kristoffer Dalby fdc7e26c8a util: consolidate parsing and encoding helpers 2026-06-17 16:12:19 +02:00
Kristoffer Dalby 03e2d24c79 policy/v2, policy/matcher: consolidate alias and IP-set helpers 2026-06-17 16:12:19 +02:00
Kristoffer Dalby e6ef1dda4d policy: consolidate cache-invalidation and evaluation helpers 2026-06-17 16:12:19 +02:00
Kristoffer Dalby ed6596f9d7 policy, mapper, capver, hscontrol: remove dead code 2026-06-17 16:12:19 +02:00
Kristoffer Dalby 40ed210521 policy: read pm.pol under the mutex
NodeCanHaveTag, TagExists, ViaRoutesForPeer checked pm.pol before
taking pm.mu, racing SetPolicy's write (caught by -race in
TestRaceConcurrentServerMutations). DebugString read pol, filter, and
the derived maps with no lock at all.
2026-06-11 16:28:25 +02:00
Kristoffer Dalby 020560fc5f policy: remove unused top-level BuildPeerMap
Zero non-test callers; production uses policy/v2 PolicyManager.BuildPeerMap.
A stray second copy of the peer-visibility predicate invites drift.
2026-06-09 15:21:18 +02:00
Kristoffer Dalby 56cd3eb24d policy: guard SSHCheckParams autogroup:self against nil User
The autogroup:self SSH-check branch dereferenced node.User().ID() guarded only by !IsTagged(); a non-tagged node with an unhydrated User (UserID set, association nil) crashed the server via the Noise SSH-check path. Gate on User().Valid() like filter.go, same shape as 171fd7a3.
2026-06-09 15:21:18 +02:00
Kristoffer Dalby 2c9164b1c4 policy: precompute node routes in the peer-map build
CanAccess recomputed each node's routes per pair, making the scan O(n^2)-heavy.
2026-06-08 10:04:49 +02:00
Kristoffer Dalby bceac495f9 policy: add NodeNeedsPeerRecompute predicate
Reports whether a node's online/offline transition forces peers to recompute their netmap. True for subnet routers, relay targets (tailscale.com/cap/relay), and via targets; false otherwise.

The relay-target IP set and via-target tag set are precompiled from the grants in updateLocked, alongside the existing filter, so the per-node check is a cheap set lookup. Keyed on the node itself, so an ordinary node in a tailnet that uses relay or via for other nodes is still classified as not needing a recompute.

Updates #3293
2026-06-03 14:51:57 +02:00
Kristoffer Dalby 171fd7a3c5 policy: key autogroup:self invalidation on UserID not User view
invalidateAutogroupSelfCache derived the owning user from
node.User().ID(), dereferencing the User association view. The
NodeStore stores nodes by value with User as a *User pointer, and not
every write path hydrates that association, so a non-tagged node could
reach SetNodes with UserID set but User nil. UserView.ID() then
dereferenced a nil *User and panicked on /machine/map whenever an
autogroup:self policy was active and a node restarted tailscaled.

Group affected nodes by node.TypedUserID(), which reads the UserID
field directly. UserID is the authoritative ownership field for
non-tagged nodes, so this needs no hydrated association and fixes every
.User().ID() site in the function.
2026-05-29 21:42:56 +02:00
Kristoffer Dalby 4cca63155d 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
2026-05-19 09:55:22 +02:00
Kristoffer Dalby 17236fd284 all: annotate complex functions with gocyclo rationale
Splitting these functions does not buy clarity — each has been
extracted before and put back. Pin the //nolint:gocyclo on each
with the reason their shape resists clean decomposition.

  policy/v2/policy.go     ViaRoutesForPeer        — three-pass
                                                    via-grant resolution
  policy/v2/filter.go     compileSSHPolicy        — per-rule
                                                    branches with
                                                    intertwined
                                                    autogroup:self
                                                    handling
                                                    (annotated in the
                                                    earlier nil-error
                                                    commit)
  state/state.go          HandleNodeFromPreAuthKey — security-
                                                    sensitive
                                                    sequential
                                                    validation order
  servertest/routes_test  TestRoutes              — table-driven
                                                    test driver with
                                                    many independent
                                                    subtests

Also: //nolint:recvcheck on policy/v2.SSHUser — UnmarshalJSON
requires a pointer receiver; the other methods on this string
newtype use value receivers by convention.
2026-05-19 09:55:22 +02:00
Kristoffer Dalby 64c398f2c2 metrics, policy/v2: drop unused scaffolding + nil-error returns
Two cleanups in the same package boundary:

policy/v2.Policy.compileFilterRules and compileFilterRulesForNode
returned (rules, error) where the error was always nil. Drop the
error return and the dead error handling at 15 call sites in the
compat tests.

Delete code with no callers:
  hscontrol/metrics.go
    prometheusMiddleware + respWriterProm — custom HTTP timer
    middleware that was never registered. /metrics stays mounted
    via debug.go and noise.go; mapresponse_* counters keep emitting.
    httpDuration and httpCounter — backing metrics for the deleted
    middleware.
  hscontrol/policy/v2/filter.go
    resolvedAddrsToPrincipals — superseded by ipSetToPrincipals.
    ipSetToPrefixStringList — superseded by inline prefix
    formatting at the surviving callers.
  hscontrol/policy/v2/tailscale_{acl,grants}_data_compat_test.go
    setupACLCompatNodes / setupGrantsCompatNodes — the data-driven
    tests switched to per-scenario topology reconstruction.
2026-05-19 09:55:22 +02:00
Kristoffer Dalby 2cb914df59 policy/v2: add SaaS goldens for via-grant prefix containment
Captures from Tailscale SaaS exercising broader, narrower, host
alias, disjoint, and 4via6 grant destinations against advertised
subnet routes. TestGrantsCompat replays them.

Updates #3267
2026-05-18 14:02:00 +02:00
Kristoffer Dalby e5fcd01ee6 policy/v2: match via-grant destinations by prefix overlap
slices.Contains required exact equality between grant dst and the
advertised subnet route. Any non-identical pair was rejected, so a
via grant with broader (or narrower) dst emitted no filter rule and
added no route to the viewer's AllowedIPs. Tailscale SaaS uses
containment in either direction.

Switch to slices.ContainsFunc(routes, dst.Overlaps) for filter rule
emission (keep dst literal in DstPorts), and append overlapping
advertised routes to ViaRoutesForPeer.Include / Exclude. Rewrite the
multi-router HA election and regular-grant overlap detection to key
off the matched routes rather than the dst. Resolve *Host aliases to
*Prefix once in compileOneViaGrant and at the top of ViaRoutesForPeer
so the switch arms reach them.

Fixes #3267
2026-05-18 14:02:00 +02:00
Kristoffer Dalby 26eebcea5a policy/v2: add sshtester compat runner
Replays recorded policy responses for the sshTests block. 200 captures must evaluate; non-200 captures must reject with the recorded body as a substring of the headscale error. Divergences are listed in knownSSHTesterDivergences.
2026-05-13 21:10:13 +02:00
Kristoffer Dalby 013dea4f40 policy/v2: evaluate sshTests at write boundary
SetPolicy and policy check now compile per-dst SSH rules and replay each sshTests entry. The accept assertion treats check-action rules as reachable; the check assertion requires HoldAndDelegate on the matching rule. Boot reload warns and continues.
2026-05-13 21:10:13 +02:00
Kristoffer Dalby 6a0a297c7f policy/v2: validate sshTests at parse
Adds SSHPolicyTest plus parse-time validation: empty src/dst, port/CIDR/autogroup-internet destinations, and tag references missing from tagOwners are rejected. Engine evaluation comes in a follow-up.
2026-05-13 21:10:13 +02:00
Kristoffer Dalby d600090f2c policy/v2: align SSH rule validation with Tailscale
Trim whitespace on action, users, src, dst; reject empty/wildcard users; reject empty acceptEnv; reject negative and over-max checkPeriod; reject hosts-table aliases as SSH dst; reject non-ASCII tag names; tolerate tag-owner cycles; match group-nesting wording.
2026-05-13 21:10:13 +02:00
Kristoffer Dalby 5d502bfb88 types/node, mapper: strip own IPv4 from emission when node has disable-ipv4 cap
When a node carries the disable-ipv4 nodeAttr documented at
https://tailscale.com/docs/reference/troubleshooting/network-configuration/cgnat-conflicts,
SaaS stops sending the node's CGNAT IPv4 prefix in MapResponse. The
allocator keeps assigning IPv4 server-side; only the wire-shape
delivery is filtered. Subnet routes the node advertises -- including
IPv4 prefixes -- survive in AllowedIPs and PrimaryRoutes.

TailNode now drops Is4 prefixes from Addresses and from the node's
own /32 slot in AllowedIPs when selfPolicyCaps carries
disable-ipv4. Mapper.buildTailPeers passes each peer's policy
CapMap so the filter applies in viewer netmaps too; the CapMap
merge that follows is overwritten by PeerCapMap so only the address
filter survives on the peer path.

Two captures land in testdata/nodeattrs_results to anchor the
behaviour:

  - nodeattrs-attr-c15-disable-ipv4         (on tag:client)
  - nodeattrs-attr-c16-disable-ipv4-router  (on tag:router, which
    advertises 10.33.0.0/16, confirming subnet routes survive)
2026-05-13 14:22:30 +02:00
Kristoffer Dalby 64d13f77e8 types/config, types/node: model default-auto-update from auto_update.enabled
Tailscale stamps tailcfg.NodeAttrDefaultAutoUpdate on every node's
CapMap with a JSON bool reflecting the tailnet-wide auto-update
default. Headscale grows an auto_update.enabled config option and
emits the cap accordingly from TailNode -- the cap leaves the
unmodelledTailnetStateCaps strip list and is compared in full by the
nodeAttrs compat suite.

testNodeAttrsSuccess drives cfg.AutoUpdate.Enabled from
tf.Input.Tailnet.Settings.DevicesAutoUpdatesOn so each capture's
expected emission matches the SaaS state it was taken under. Two
captures cover both branches:

  - nodeattrs-tailnet-devices-auto-updates-on  -> [true]
  - nodeattrs-tailnet-devices-auto-updates-off -> [false]

The Tailscale v2 TailnetSettings API does not expose the Send Files
toggle, so the compat suite cannot vary cfg.Taildrop.Enabled per
capture. TestTaildropDisabledWithholdsFileSharingCap covers the off
path directly in servertest.
2026-05-13 14:22:30 +02:00