Commit Graph

4013 Commits

Author SHA1 Message Date
Kristoffer Dalby 2764e5f228 testdata: re-run ssh tscap corpus with localpart anonymization
Regenerates ssh_results/*.hujson on top of the tscap fix
(kradalby/tscap@833a50a + 837f1f6) that rewrites localpart:*@<domain>
patterns in policy text alongside user email substitution. Before the
fix, anonymized captures kept the raw @passkey domain in policy while
the users landed on @example.com, so localpart:*@passkey matched zero
users and headscale emitted only the wildcard rule, diffing against
SaaS's per-user rules on 40 failing leaves across 14 scenarios
(ssh-d2..d12 minus d1/d10, ssh-e6, ssh-f3..f5).

With the new corpus:

  - localpart:*@passkey in policy → localpart:*@example.com
  - thor moves from thor@example.com to thor@dalby.cc so that
    "localpart:*@example.com" resolves to exactly odin+freya (matching
    SaaS output), and "localpart:*@dalby.cc" in ssh-d1 keeps matching
    exactly one user.

Test code update: setupSSHDataCompatUsers() now pins thor on
@dalby.cc to match the capture topologies. All other compat tests
(routes, grants, acl, via-grant) use their own per-file topology
loaders and are unaffected — thor's email change is scoped to the
SSH hardcoded setup.

TestSSHDataCompat: 41 failing leaves → 1 failing leaf. The single
remaining ssh-b6/beedrill failure is a pre-existing, unrelated
duplicate-principals bug in headscale's SSH compiler (same diff
shape before and after this change).

Updates #3157
backup/pre-rebase-20260409-092352
2026-04-09 08:51:57 +00:00
Kristoffer Dalby 5cba8ddc62 testdata: re-run tscap with populated approved_routes and tag fixes
Replaces the previous capture corpus with the output of a fresh tscap
batch built from:

  - kradalby/tscap@d2cd4f9 (anonymize: substitute tags too)
  - kradalby/tscap@3080147 (runner: capture autoApprover decisions in
    ApprovedRoutes)

Two regressions in the previous corpus are fixed:

  1. approved_routes was committed as [] on every node in all 617
     files because the runner never asked the SaaS API which routes had
     been auto-approved. The new corpus has 533/1296 routes_results and
     462/3765 grant_results entries with non-empty approved sets,
     matching what SaaS actually returned.

  2. Anonymization rewrote tag references in policy JSON
     (tag:exit-a → tag:pidgey, tag:router-a → tag:spearow, …) but left
     node.Tags untouched, so via-grant-* and other scenarios that use
     these collision-prone tag names had policies pointing at tag
     identifiers no node carried. The new corpus has node.Tags routed
     through the same substitution table.

Compat-test impact (subtest level):

                  before        after        delta
    Routes        1697 / 825    3397 / 13    -812 fail
    ACL           1799 /   8    1805 /  0      -8 fail
    Grants        3426 /  48    3463 / 11     -37 fail
    SSH            521 /  71     536 / 56     -15 fail
    ViaGrant         0 /   4      31 / 21      n/a (test now actually
                                                  loads files)
    Total         7443 / 956    9232 / 101   -855 fail (-89.4%)

Verified zero true accuracy regressions: every test that passed
against the old corpus and now fails against the new corpus is a
false-pass-becoming-true-fail. The single nominal regression
(grant-j3/beedrill) used to pass because both sides were empty under
the broken approved_routes; the new file exposes a real divergence in
filter.go srcIPsWithRoutes where headscale appends approved subnet
routes to wildcard SrcIPs and SaaS does not.

Updates #3157
Updates #3169
2026-04-09 06:20:03 +00:00
Kristoffer Dalby a19fd9bf00 policy/v2: ignore implementation-specific SSH check action fields
The recapture of the SSH golden corpus by tscap revealed five fields
on tailcfg.SSHAction that the headscale compiler emits with different
defaults from Tailscale SaaS for "check" actions:

  - SessionDuration: headscale uses SSHCheckPeriodDefault (12h) when
    the rule omits checkPeriod; SaaS emits 0s.

  - AllowAgentForwarding, AllowLocalPortForwarding,
    AllowRemotePortForwarding: filter.go sshCheck/sshAccept hardcode
    these to true; SaaS emits false on check.

  - HoldAndDelegate URL template: headscale embeds /from/…?ssh_user=
    so its own check handler can identify the requested SSH user; SaaS
    omits both the /from/ path segment and the ssh_user query
    parameter.

These are deliberate headscale design choices, not capture artifacts:
tscap pulls SSHRules straight out of the netmap as a json.RawMessage
and never rewrites them, so the new captures are the authoritative
SaaS output.

Pass an IgnoreFields option to cmp.Diff for those five fields so the
SSH compat test stops flagging the divergence on every check rule, and
add a follow-up presence assertion that asserts headscale and SaaS
agree on whether each rule has HoldAndDelegate set so a regression
that drops the URL entirely is still caught.

71 → 56 SSH compat failures; the residual 56 are unrelated bugs in
compileSSHPolicy (missing rules, duplicate principals) that surface
on a different set of files.

Updates #3157
Updates #3169
2026-04-09 06:19:17 +00:00
Kristoffer Dalby ce81ab2a2f servertest: align via grant compat test with new tscap golden format
The previous tscap regeneration replaced the legacy uppercase
GRANT-V*.hujson files with lowercase via-grant-v*.hujson and switched
the topology nodes to anonymized pokémon identifiers, but
TestViaGrantMapCompat was still loading files by their old IDs and
matching topology entries by their old hostnames, so it could not even
open a single golden file.

  - viaCompatTests: GRANT-V29/V30/V31/V36 → via-grant-v29/v30/v31/v36
    so the path Join finds the renamed files on disk.

  - taggedNodes: rewrite the hardcoded servertest hostname list to the
    pokémon names tscap/anonymize writes (big-router→caterpie,
    exit-a→pidgey, …) so the topology lookup picks up the matching
    entries instead of skipping every node.

  - goldenNode: track the tscap schema. Replace advertised_routes →
    routable_ips and is_exit_node → approved_routes; the AdvertisedRoutes
    field name is kept on the Go side because the rest of the test
    code already references it.

  - convertViaPolicy: also rewrite the anonymized odin/thor/freya
    @example.com emails so the served policy resolves users when the
    file came from tscap/anonymize.

Updates #3157
Updates #3169
2026-04-09 06:18:43 +00:00
Kristoffer Dalby b83e30c090 mapper, db: drop redundant zerolog SetGlobalLevel calls in tests
The previous commit centralizes test log silencing in
hscontrol/types/testlog.go, so the per-test SetGlobalLevel calls scattered
across the batcher and db tests are now redundant. Most of them were also
buggy: they restored to DebugLevel instead of capturing and restoring the
prior level, polluting global state for subsequent tests in the same binary.

Remove all such pairs from batcher_bench_test.go, batcher_concurrency_test.go,
batcher_scale_bench_test.go, and suite_test.go, along with the now-unused
zerolog imports. The one correct save/restore pattern in batcher_test.go is
left untouched: it captures originalLevel via zerolog.GlobalLevel() and works
transparently with the new ErrorLevel default.

Updates #3157
Updates #3169
2026-04-08 20:50:15 +00:00
Kristoffer Dalby 3fa4e2dafe hscontrol/types: silence zerolog by default in tests
Running go test ./... produced an extreme amount of zerolog output because
test binaries inherited the package default of DebugLevel. Per-test mitigations
were scattered across roughly fifty test functions and most of them were
subtly broken: they restored to DebugLevel instead of the prior level, leaking
state into subsequent tests in the same binary.

Add a single init() in hscontrol/types/testlog.go that detects test mode via
testing.Testing() and lowers the global zerolog level to ErrorLevel. The types
package is transitively imported by every test binary that emits zerolog
output, so this one insertion point silences go test ./... without any
per-package boilerplate. Production binaries are unaffected because
testing.Testing() returns false outside of test execution; the same pattern
already exists in hscontrol/db/{users,node}.go.

Set HEADSCALE_TEST_LOG_LEVEL=trace|debug|info|warn|error|disabled to override
when debugging a specific test.

hscontrol/util/zlog/zlog_test.go asserts on Info-level output from a
zerolog.New(&buf) logger, which is also gated by the global level. Add a
defensive init_test.go in that package pinning the global level to TraceLevel
so the assertions stay reliable if a future import chain pulls
hscontrol/types into the zlog test binary.

Document the mechanism, the env var override, the save/restore idiom for
per-test overrides, and the relevant pitfalls under Testing Guidelines in
AGENTS.md.

Updates #3157
Updates #3169
2026-04-08 20:49:47 +00:00
Kristoffer Dalby 8703aa0bab testdata: replace with multi-user tscap output
Re-run of the full corpus using tscap's new per-user API keys so each
untagged node is owned by its intended user (odin/thor/freya) instead
of all being collapsed onto the admin account. Jumps compat pass rate:

  before multi-user: 6564 / 7289 (90.0%)
  after multi-user:  6979 / 7289 (95.7%)

Source: github.com/kradalby/tscap @ c2c5a9e

Updates #3157
Updates #3169
2026-04-08 15:28:23 +00:00
Kristoffer Dalby 4005a097e9 compat tests: build node topology per-scenario, not globally
tscap uses clean-slate mode: each scenario wipes every device from the
tailnet before logging new ones in, so node IPs change from scenario to
scenario. The previous compat tests built a single nodes slice from the
first file's topology (or from a hardcoded setup) and used it for every
scenario, which produced IP mismatches in the filter rule comparisons.

Rebuild types.Nodes per scenario from the current file's Topology. ACL
already had a build-from-topology helper; extract the equivalent for
grants (buildGrantsNodesFromCapture) and use it for SSH too. Drop the
"first 8 nodes only" shim in grants.

Pass count jumped ~2500 tests across the four compat suites.

Updates #3157
Updates #3169
2026-04-08 11:35:11 +00:00
Kristoffer Dalby b7da0c403c testdata: replace legacy captures with anonymized tscap output
Drop the old testdata/{acl,grant,routes,ssh}_results/ files (mixed
uppercase ACL-*/ROUTES-*/GRANT-*/SSH-* filenames with the pre-tscap
schema) and replace with the 617 captures tscap produced for the
kradalby/3157-subnet-to-subnet branch. Files are lowercase and
anonymized (odin/thor/freya users, pokemon hostnames) per
kradalby/tscap package anonymize.

Relax pre-commit check-added-large-files: tscap captures include full
netmaps and can exceed 500 KB per file; testdata/ is now excluded
from the size check and the global ceiling is bumped to 2 MB.

Source: github.com/kradalby/tscap @ cb375ec

Updates #3157
Updates #3169
2026-04-08 11:29:41 +00:00
Kristoffer Dalby 76c7cb3342 compat tests: switch to anonymized norse-god + pokemon names
tscap now anonymizes captures at write time (see kradalby/tscap
package anonymize): SaaS user identifiers become odin/thor/freya and
node hostnames become original-151 pokemon names. Update the four
policy v2 compat test setups to construct types.User and types.Node
objects with the new names so the in-test topology matches what the
captured testdata files contain.

The convertPolicyUserEmails / convertSaaSEmail helpers become
passthroughs because tscap already produces the final email form.

Updates #3157
Updates #3169
2026-04-08 08:21:31 +00:00
Kristoffer Dalby e30de58627 testcapture: add capture format package and rewire compat tests
Add hscontrol/types/testcapture, mirroring the type that the tscap tool
emits. The package has no tailscale.com/tailcfg import so it stays
dependency-free; wire-format Tailscale data is stored as
json.RawMessage and consumers unmarshal into the typed shape they need.

Drop the four private *TestFile structs from the policy v2 compat tests
and read directly into testcapture.Capture. Switch globs to lowercase
(acl-/grant-/routes-/ssh-) to match the new tscap output filenames.

Updates #3157
Updates #3169
2026-04-08 08:05:45 +00:00
Kristoffer Dalby 211da43f57 policy/v2: build ACL compat nodes from golden file topology
Replace the hardcoded 8-node setupACLCompatNodes with
buildACLUsersAndNodes which reads from the golden file topology.

Updates #3157
Updates #3169
2026-04-06 08:15:49 +00:00
Kristoffer Dalby c504f891cf testdata: recapture all golden files with 19-node tailnet and netmap
Recapture all golden test data from Tailscale SaaS with:
- All 19 nodes online (12 routes + 7 grant v2 topology)
- All routes approved via API
- Trimmed netmap data per capture (Peers with AllowedIPs,
  PrimaryRoutes, Tags; SelfNode with RoutableIPs)
- 20-30s propagation wait for correct data

Topology expanded from netmap peers to include all 19 nodes that
contribute to filter rule SrcIPs. Each node has routable_ips and
approved_routes derived from netmap SelfNode data.

New files: b11-b16 (autogroup:internet), i8-i9 (IPv6 fd00:1::/64),
pa1-pa2 (partial route approval), V31-SLOW.

Updates #3157
Updates #3169
2026-04-06 08:07:07 +00:00
Kristoffer Dalby ff16af0d6e policy/v2: remove obsolete global vs per-node equivalence tests
With the unified compiledGrant infrastructure, there is only one
filter compilation path. The equivalence tests that compared the
old global path against the old per-node path no longer serve a
purpose since both callers now go through the same compileGrants
resolution.

Remove TestACLCompatGlobalEquivalence, TestGrantsCompatGlobalEquivalence,
and TestRoutesCompatGlobalEquivalence.

Updates #3157
Updates #3169
2026-04-04 16:15:44 +00:00
Kristoffer Dalby b2f7d66686 policy/v2: make ACL error validation strict
Replace the lenient testACLError handler (t.Logf on mismatch) with
strict validation matching the grant error handler pattern:

- Extract assertACLErrorContains helper with progressive fallbacks:
  direct match, mapped equivalent, key-part extraction, then t.Errorf
- Convert HeadscaleDiffers from t.Logf to t.Skipf for visibility
- Add aclErrorMessageMap for known wording differences (empty for now)

All 16 ACL error golden files pass with the strict handler.

Updates #3157
Updates #3169
2026-04-04 13:21:05 +00:00
Kristoffer Dalby 4dedb70818 policy: document CanAccessRoute and filterForNodeLocked design
Add source comments explaining design decisions:

CanAccessRoute (types/node.go): Explain why DestsIsTheInternet is
intentionally absent. Exit routes are handled by RoutesForPeer via
ExitRoutes(), not ACL-based filtering. autogroup:internet is skipped
during filter compilation, so no matchers contain 'the internet'.

Updates #3157
Updates #3169
2026-04-04 13:20:45 +00:00
Kristoffer Dalby 6e3a03ab54 policy/v2: wire PolicyManager through compiledGrant, delete old paths
Store compiledGrants and userNodeIndex on PolicyManager. Both
filterForNodeLocked and BuildPeerMap now derive from the same
compiled grants, eliminating the dual-path architecture.

Delete:
- compileViaGrant (filter.go): replaced by compileViaForNode
- compileGrantWithAutogroupSelf (filter.go): replaced by
  compileGrants + compileAutogroupSelf
- compileFilterRulesForNodeLocked (policy.go): replaced by
  filterRulesForNodeLocked
- compiledFilterRulesMap cache (policy.go): no longer needed
- usesAutogroupSelf field and method: detection now happens
  during grant compilation (grantCategorySelf/grantCategoryVia)
- hasViaGrants method: same

The needsPerNodeFilter flag is now derived from the compiled grants
via hasPerNodeGrants. filterForNodeLocked no longer returns error
since the new path cannot fail.

Updates #3157
Updates #3169
2026-04-04 13:18:15 +00:00
Kristoffer Dalby f2664e434e policyutil: fix reduceCapGrantRule and add reduction tests
Fix fragile IP indexing in reduceCapGrantRule: replace hard-coded
nodeIPs[0]/nodeIPs[1] access with a range loop, matching the pattern
used by the adjacent broad-prefix branch. The old code would panic
if a node had zero IPs.

Remove redundant SubnetRoutes check in ReduceFilterRules: the check
was strictly redundant with the RoutableIPs check since SubnetRoutes
is always a subset of RoutableIPs. Document that golden data always
has routable_ips == approved_routes so we cannot determine which set
Tailscale actually checks.

Add tests:
- TestReduceFilterRulesCapGrant: 7 cases covering IP narrowing,
  non-matching filtered out, subnet route overlap, exit route
  skipping, mixed DstPorts+CapGrant, IPv4-only, and zero-IP nodes
- TestReduceFilterRulesPartialApproval: 3 cases documenting behavior
  for approved, unapproved-but-advertised, and non-advertised routes

Updates #3157
Updates #3169
2026-04-04 12:51:32 +00:00
Kristoffer Dalby d092e4b325 policy/v2: add false-positive peer test for all 98 golden files
TestRoutesCompatNoPeersBeyondCaptures extends the negative peer
assertion from 6 hardcoded files (f10-f15) to all 98 ROUTES golden
files using a generic, data-driven approach.

For each golden file, the test derives expected peer pairs from
capture SrcIPs (subnet routes, direct node IPs) and DstPorts
(destination node IPs, route CIDRs), handling IP dash-range format
(100.64.0.0-100.115.91.255) and wildcards. It then asserts that no
unexpected CanAccess=true pairs exist.

Also adds addSrcIPToBuilder helper and deriveAllPeerPairsFromCaptures
for comprehensive peer derivation from golden data.

Updates #3157
Updates #3169
2026-04-04 12:51:32 +00:00
Kristoffer Dalby c22bddfe83 policy/v2: add global vs per-node filter compilation equivalence tests
Add equivalence tests that compare the global filter path
(PolicyManager.FilterForNode) against the per-node path
(compileFilterRulesForNode + ReduceFilterRules) for all golden files
where the two paths should produce identical output.

TestRoutesCompatGlobalEquivalence: 98 ROUTES files, 12 nodes each.
All pass (1,176 comparisons).

TestACLCompatGlobalEquivalence: ~152 ACL files (excluding error and
autogroup:self cases), 8 nodes each. Surfaces 2 divergences (ACL-M06,
ACL-K01) where the global path loses individual non-wildcard source
IPs absorbed into the CGNAT range.

TestGrantsCompatGlobalEquivalence: ~186 GRANT files (excluding error,
autogroup:self, and via cases), 8-15 nodes each. Surfaces 3
divergences (GRANT-P09_7A, GRANT-K29, GRANT-H6) with the same root
cause.

The per-node path matches Tailscale SaaS golden data in all 5
divergent cases, confirming it preserves nonWildcardSrcs correctly.

Updates #3157
Updates #3169
2026-04-04 12:51:32 +00:00
Kristoffer Dalby 782c367ee4 servertest: improve via compat test infrastructure
Add big-router to the taggedNodes list for via compat tests. This
node has tags [tag:router, tag:exit] and is needed for future
V-series golden file tests. Existing tests are unaffected.

Enhance compareNetmap to compare PacketFilter rule content (destination
prefixes, source non-emptiness), not just rule count. Six golden files
have full SrcIPs/DstPorts data that was previously unvalidated.

Add Tier 3 route inference to inferNodeRoutes: scan each node's own
packet_filter_rules DstPorts for non-Tailscale-IP route prefixes.
This catches secondary HA routers whose routes don't appear in
AllowedIPs but do receive filter rules.

Updates #3157
Updates #3169
2026-04-04 12:51:32 +00:00
Kristoffer Dalby 471b9bbdbb policy/matcher: add unit tests for matcher package
The matcher_test.go file was empty (just the package declaration).
Add comprehensive unit tests covering all public methods:

- TestMatchFromStrings: CIDR, wildcard, single IP, IPv6, empty inputs
- TestMatchFromFilterRule: standard rules, wildcard dst, empty DstPorts
- TestMatchesFromFilterRules: multi-rule conversion
- TestSrcsOverlapsPrefixes: exact, parent/child, no overlap, IPv6
- TestDestsOverlapsPrefixes: exact, parent/child, wildcard
- TestDestsIsTheInternet: 0.0.0.0/0, ::/0, wildcard, private ranges
- TestDebugString: output format validation

DestsIsTheInternet had zero test coverage (neither direct nor indirect)
prior to this change.

Updates #3157
Updates #3169
2026-04-03 09:16:48 +00:00
Kristoffer Dalby f5a0493886 policy/v2: add auto-approval and route access compat tests
Add two new data-driven compatibility tests validated against all 98
Tailscale SaaS golden files:

- TestRoutesCompatAutoApproval: verifies NodeCanApproveRoute produces
  the same approval decisions as captured in golden files. Exit routes
  (0.0.0.0/0, ::/0) are skipped because Tailscale SaaS stores them
  under autoApprovers.routes while headscale uses a separate
  autoApprovers.exitNode field.

- TestRoutesCompatReduceRoutes: verifies CanAccessRoute produces route
  visibility decisions consistent with golden captures. Extends the
  coverage from 6 files (f10-f15) to all 98 golden files by deriving
  expected access from the SrcIPs in each node's captured filter rules.

Updates #3157
Updates #3169
2026-04-03 07:26:23 +00:00
Kristoffer Dalby 8addacb06a policy/v2: add peer visibility compat tests for subnet-to-subnet ACLs
Add TestRoutesCompatPeerVisibility and TestRoutesCompatNoFalsePositivePeers
which use the Tailscale SaaS golden file data to validate the CanAccess /
ReduceNodes / ReduceRoutes code paths for subnet-to-subnet ACL scenarios.

These tests derive expected peer relationships from the golden file
captures: if Tailscale SaaS delivers filter rules to a node with SrcIPs
overlapping another node's subnet routes, those nodes must be peers. This
exercises the CanAccess fix where subnet routes are treated as source
identity, ensuring the fix is validated against real SaaS behavior.

Also fix buildRoutesUsersAndNodes to auto-assign unique node IDs when the
golden files don't provide them, which is required for ReduceNodes to
correctly skip self-comparisons.

Updates #3157
Updates #3169
2026-04-02 20:47:06 +00:00
Kristoffer Dalby 798c8a9edb testdata: add subnet-to-subnet route compatibility golden files
Add 6 golden files captured from Tailscale SaaS that cover subnet-to-subnet
ACL scenarios where both source and destination are subnet CIDRs. These fill
a complete gap in the existing 92 ROUTES golden files, none of which tested
this pattern.

Scenarios covered:
  f10: exact issue #3157 reproduction (disjoint subnets, host aliases)
  f11: bidirectional with overlapping big-router
  f12: host aliases where dst is a superset route
  f13: disjoint subnets, bidirectional
  f14: both src and dst overlap on one router
  f15: cross-router bidirectional

All 6 pass TestRoutesCompat, confirming headscale matches Tailscale SaaS
behavior for subnet-to-subnet filter rule distribution.

Updates #3157
Updates #3169
2026-04-02 16:01:40 +00:00
Ubuntu f37401e9bc integration: add subnet-to-subnet ACL routing test
Add TestSubnetToSubnetACL which creates two subnet routers on separate
Docker networks and verifies that ACL rules referencing only subnet
CIDRs (not node IPs) as source and destination produce correct peer
visibility, route distribution, and end-to-end connectivity.

Updates #3157
Updates #3169
2026-04-02 13:25:34 +00:00
Ubuntu 1883f5022e types: consider subnet routes as source identity in ACL matching
CanAccess and CanAccessRoute only used a node's Tailscale IPs as
source identity when matching ACL rules. A subnet router acting on
behalf of its advertised subnets was invisible to ACLs that reference
subnet CIDRs as source (e.g. src=10.88.8.0/24 dst=10.99.9.0/24).

Extend both functions to also check whether the node's approved
subnet routes overlap the matcher's source set. This makes subnet
routers visible to each other when ACLs reference their subnet CIDRs,
enabling subnet-to-subnet traffic routing.

The new checks are guarded by len(subnetRoutes) > 0, so non-router
nodes (the vast majority) pay zero additional cost.

Fixes #3157
Fixes #3169
2026-04-02 13:23:23 +00:00
Ubuntu 9210e597b1 types,policy: add tests for subnet-to-subnet route visibility
Add unit tests covering the subnet-to-subnet ACL scenario where both
src and dst are subnet CIDRs served by different subnet routers.

Tests cover CanAccess (peer visibility), ReduceRoutes (route filtering),
and ReduceNodes (peer list reduction). Positive cases for routers that
advertise the referenced subnets, and negative cases ensuring regular
nodes and unrelated routers gain no extra visibility.

These tests currently fail and will be fixed in the next commit.

Updates #3157
Updates #3169
2026-04-02 13:23:14 +00:00
Kristoffer Dalby 835db974b5 testdata: strip unused fields from all test data files (23MB -> 4MB)
Strip fields not consumed by any test from all 594 HuJSON test data files:

grant_results/ (248 files, 21MB -> 1.8MB):
  - Remove: timestamp, propagation_wait_seconds, input.policy_file,
    input.grants_section, input.api_endpoint, input.api_method,
    topology.nodes.mts_name, topology.nodes.socket, topology.nodes.user_id,
    captures.commands, captures.packet_filter_matches, captures.whois
  - V14-V16, V26-V36: keep stripped netmap (Peers.Name/AllowedIPs/PrimaryRoutes
    + PacketFilterRules) for via_compat_test.go compatibility
  - V17-V25: strip netmap (old topology, incompatible with via_compat harness)

acl_results/ (215 files, 1.4MB -> 1.2MB):
  - Remove: timestamp, propagation_wait_seconds, input.policy_file,
    input.api_endpoint, input.api_response_code, entire topology section
    (parsed by Go struct but completely ignored — nodes are hardcoded)

routes_results/ (92 files, unchanged — topology is actively used):
  - Remove: timestamp, propagation_wait_seconds, input.policy_file,
    input.api_endpoint, input.api_response_code

ssh_results/ (39 files, unchanged — minimal to begin with):
  - Remove: policy_file
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 30dce30a9d testdata: convert .json to .hujson with header comments
Rename all 594 test data files from .json to .hujson and add
descriptive header comments to each file documenting what policy
rules are under test and what outcome is expected.

Update test loaders in all 5 _test.go files to parse HuJSON via
hujson.Parse/Standardize/Pack before json.Unmarshal.

Add cross-dependency warning to via_compat_test.go documenting
that GRANT-V29/V30/V31/V36 are shared with TestGrantsCompat.

Add .gitignore exemption for testdata HuJSON files.
2026-04-01 14:10:42 +01:00
Kristoffer Dalby f693cc0851 CHANGELOG: document grants support for 0.29.0
Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby abd2b15db5 policy/v2: clean up dead error variables, stale TODO, and test skip reasons
Remove unused error variables (ErrGrantViaNotSupported, ErrGrantEmptySources, ErrGrantEmptyDestinations, ErrGrantViaOnlyTag) and the stale TODO for via implementation. Update compat test skip reasons to reflect that user:*@passkey wildcard is a known unsupported feature, not a pending implementation.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby b762e4c350 integration: remove exit node via grant tests
Remove TestGrantViaExitNodeSteering and TestGrantViaMixedSteering.
Exit node traffic forwarding through via grants cannot be validated
with curl/traceroute in Docker containers because Tailscale exit nodes
strip locally-connected subnets from their forwarding filter.

The correctness of via exit steering is validated by:
- Golden MapResponse comparison (TestViaGrantMapCompat with GRANT-V31
  and GRANT-V36) comparing full netmap output against Tailscale SaaS
- Filter rule compatibility (TestGrantsCompat with GRANT-V14 through
  GRANT-V36) comparing per-node PacketFilter rules against Tailscale SaaS
- TestGrantViaSubnetSteering (kept) validates via subnet steering with
  actual curl/traceroute through Docker, which works for subnet routes

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby c36cedc32f policy/v2: fix via grants in BuildPeerMap, MatchersForNode, and ViaRoutesForPeer
Use per-node compilation path for via grants in BuildPeerMap and MatchersForNode to ensure via-granted nodes appear in peer maps. Fix ViaRoutesForPeer golden test route inference to correctly resolve via grant effects.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 6a55f7d731 policy/v2: add via exit steering golden captures and tests
Add golden test data for via exit route steering and fix via exit grant compilation to match Tailscale SaaS behavior. Includes MapResponse golden tests for via grant route steering verification.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby bca6e6334d integration: add custom subnet support and fix exit node tests
Add NetworkSpec struct with optional Subnet field to ScenarioSpec.Networks.
When Subnet is set, the Docker network is created with that specific CIDR
instead of Docker's auto-assigned RFC1918 range.

Fix all exit node integration tests to use curl + traceroute. Tailscale
exit nodes strip locally-connected subnets from their forwarding filter
(shrinkDefaultRoute + localInterfaceRoutes), so exit nodes cannot
forward to IPs on their Docker network via the default route alone.
This is by design: exit nodes provide internet access, not LAN access.
To also get LAN access, the subnet must be explicitly advertised as a
route — matching real-world Tailscale deployment requirements.

- TestSubnetRouterMultiNetworkExitNode: advertise usernet1 subnet
  alongside exit route, upgraded from ping to curl + traceroute
- TestGrantViaExitNodeSteering: usernet1 subnet in via grants and
  auto-approvers alongside autogroup:internet
- TestGrantViaMixedSteering: externet subnet in auto-approvers and
  route advertisement for exit traffic

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 0431039f2a servertest: add regression tests for via grant filter rules
Add three tests that verify control plane behavior for grant policies:

- TestGrantViaSubnetFilterRules: verifies the router's PacketFilter
  contains destination rules for via-steered subnets. Without per-node
  filter compilation for via grants, these rules were missing and the
  router would drop forwarded traffic.

- TestGrantViaExitNodeFilterRules: same verification for exit nodes
  with via grants steering autogroup:internet traffic.

- TestGrantIPv6OnlyPrefixACL: verifies that address-based aliases
  (Prefix, Host) resolve to exactly the literal prefix and do not
  expand to include the matching node's other IP addresses. An
  IPv6-only host definition produces only IPv6 filter rules.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby ccd284c0a5 policy/v2: use per-node filter compilation for via grants
Via grants compile filter rules that depend on the node's route state
(SubnetRoutes, ExitRoutes). Without per-node compilation, these rules
were only included in the global filter path which explicitly skips via
grants (compileFilterRules skips grants with non-empty Via fields).

Add a needsPerNodeFilter flag that is true when the policy uses either
autogroup:self or via grants. filterForNodeLocked now uses this flag
instead of usesAutogroupSelf alone, ensuring via grant rules are
compiled per-node through compileFilterRulesForNode/compileViaGrant.

The filter cache also needs to account for route-dependent compilation:

- nodesHavePolicyAffectingChanges now treats route changes as
  policy-affecting when needsPerNodeFilter is true, so SetNodes
  triggers updateLocked and clears caches through the normal flow.

- invalidateGlobalPolicyCache now clears compiledFilterRulesMap
  (the unreduced per-node cache) alongside filterRulesMap when
  needsPerNodeFilter is true and routes changed.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 9db5fb6393 integration: fix error message assertion for invalid ACL action
Action.UnmarshalJSON produces the format
'action="unknown-action" is not supported: invalid ACL action',
not the reversed format the test expected.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 3ca4ff8f3f state,servertest: add grant control plane tests and fix via route ReduceRoutes filtering
Add servertest grant policy control plane tests covering basic grants, via grants, and cap grants. Fix ReduceRoutes in State to apply route reduction to non-via routes first, then append via-included routes, preventing via grant routes from being incorrectly filtered.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 5cd5e5de69 policy/v2: add unit tests for ViaRoutesForPeer
Test via route computation for viewer-peer pairs: self-steering returns
empty, viewer not in source returns empty, peer without advertised
destination returns empty, peer with/without via tag populates
Include/Exclude respectively, mixed prefix and autogroup:internet
destinations, and exit route steering.

7 subtests covering all code paths in ViaRoutesForPeer.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 08d26e541c policy/v2: add unit tests for grant filter compilation helpers
Test companionCapGrantRules, sourcesHaveWildcard, sourcesHaveDangerAll,
srcIPsWithRoutes, the FilterAllowAll fix for grant-only policies,
compileViaGrant, compileGrantWithAutogroupSelf grant paths, and
destinationsToNetPortRange autogroup:internet skipping.

51 subtests across 8 test functions covering all grant-specific code
paths in filter.go that previously had no test coverage.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby d243adaedd types,mapper,integration: enable Taildrive and add cap/drive grant lifecycle test
Add NodeAttrsTaildriveShare and NodeAttrsTaildriveAccess to the node capability map, enabling Taildrive file sharing when granted via policy. Add integration test verifying the full cap/drive grant lifecycle.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 9b1a6b6c05 integration: add cap/relay grant peer relay lifecycle test
Add ConnectToNetwork to the TailscaleClient interface for multi-network test scenarios and implement peer relay ping support. Use these to test that cap/relay grants correctly enable peer-to-peer relay connections between tagged nodes.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 8573ff9158 policy/v2: fix grant-only policies returning FilterAllowAll
compileFilterRules checked only pol.ACLs == nil to decide whether
to return FilterAllowAll (permit-any). Policies that use only Grants
(no ACLs) had nil ACLs, so the function short-circuited before
compiling any CapGrant rules. This meant cap/relay, cap/drive, and
any other App-based grant capabilities were silently ignored.

Check both ACLs and Grants are empty before returning FilterAllowAll.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby a739862c65 integration: add via grant route steering tests
Add integration tests validating that via grants correctly steer
routes to designated nodes per client group:

- TestGrantViaSubnetSteering: two routers advertise the same
  subnet, via grants steer each client group to a specific router.
  Verifies per-client route visibility, curl reachability, and
  traceroute path.

- TestGrantViaExitNodeSteering: two exit nodes, via grants steer
  each client group to a designated exit node. Verifies exit
  routes are withdrawn from non-designated nodes and the client
  rejects setting a non-designated exit node.

- TestGrantViaMixedSteering: cross-steering where subnet routes
  and exit routes go to different servers per client group.
  Verifies subnet traffic uses the subnet-designated server while
  exit traffic uses the exit-designated server.

Also add autogroupp helper for constructing AutoGroup aliases in
grant policy configurations.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 8358017dcf policy/v2,state,mapper: implement per-viewer via route steering
Via grants steer routes to specific nodes per viewer. Until now,
all clients saw the same routes for each peer because route
assembly was viewer-independent. This implements per-viewer route
visibility so that via-designated peers serve routes only to
matching viewers, while non-designated peers have those routes
withdrawn.

Add ViaRouteResult type (Include/Exclude prefix lists) and
ViaRoutesForPeer to the PolicyManager interface. The v2
implementation iterates via grants, resolves sources against the
viewer, matches destinations against the peer's advertised routes
(both subnet and exit), and categorizes prefixes by whether the
peer has the via tag.

Add RoutesForPeer to State which composes global primary election,
via Include/Exclude filtering, exit routes, and ACL reduction.
When no via grants exist, it falls back to existing behavior.

Update the mapper to call RoutesForPeer per-peer instead of using
a single route function for all peers. The route function now
returns all routes (subnet + exit), and TailNode filters exit
routes out of the PrimaryRoutes field for HA tracking.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 28be15f8ea policy/v2: handle autogroup:internet in via grant compilation
compileViaGrant only handled *Prefix destinations, skipping
*AutoGroup entirely. This meant via grants with
dst=[autogroup:internet] produced no filter rules even when the
node was an exit node with approved exit routes.

Switch the destination loop from a type assertion to a type switch
that handles both *Prefix (subnet routes) and *AutoGroup (exit
routes via autogroup:internet). Also check ExitRoutes() in
addition to SubnetRoutes() so the function doesn't bail early
when a node only has exit routes.

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 687cf0882f policy/v2: implement autogroup:danger-all support
Add autogroup:danger-all as a valid source alias that matches ALL IP
addresses including non-Tailscale addresses. When used as a source,
it resolves to 0.0.0.0/0 + ::/0 internally but produces SrcIPs: ["*"]
in filter rules. When used as a destination, it is rejected with an
error matching Tailscale SaaS behavior.

Key changes:
- Add AutoGroupDangerAll constant and validation
- Add sourcesHaveDangerAll() helper and hasDangerAll parameter to
  srcIPsWithRoutes() across all compilation paths
- Add ErrAutogroupDangerAllDst for destination rejection
- Remove 3 AUTOGROUP_DANGER_ALL skip entries (K6, K7, K8)

Updates #2180
2026-04-01 14:10:42 +01:00
Kristoffer Dalby 4f040dead2 policy/v2: implement grant validation rules matching Tailscale SaaS
Implement comprehensive grant validation including: accept empty sources/destinations (they produce no rules), validate grant ip/app field requirements, capability name format, autogroup constraints, via tag existence, and default route CIDR restrictions.

Updates #2180
2026-04-01 14:10:42 +01:00