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
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.
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.
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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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.
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
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
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
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
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
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
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
Action.UnmarshalJSON produces the format
'action="unknown-action" is not supported: invalid ACL action',
not the reversed format the test expected.
Updates #2180
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
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
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
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
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
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
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
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
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
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
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
Compile grants with "via" field into FilterRules that are placed only
on nodes matching the via tag and actually advertising the destination
subnets. Key behavior:
- Filter rules go exclusively to via-nodes with matching approved routes
- Destination subnets not advertised by the via node are silently dropped
- App-only via grants (no ip field) produce no packet filter rules
- Via grants are skipped in the global compileFilterRules since they
are node-specific
Reduces grant compat test skips from 41 to 30 (11 newly passing).
Updates #2180
Compile grant app fields into CapGrant FilterRules matching Tailscale
SaaS behavior. Key changes:
- Generate CapGrant rules in compileFilterRules and
compileGrantWithAutogroupSelf, with node-specific /32 and /128
Dsts for autogroup:self grants
- Add reversed companion rules for drive→drive-sharer and
relay→relay-target capabilities, ordered by original cap name
- Narrow broad CapGrant Dsts to node-specific prefixes in
ReduceFilterRules via new reduceCapGrantRule helper
- Skip merging CapGrant rules in mergeFilterRules to preserve
per-capability structure
- Remove ip+app mutual exclusivity validation (Tailscale accepts both)
- Add semantic JSON comparison for RawMessage types and netip.Prefix
comparators in test infrastructure
Reduces grant compat test skips from 99 to 41 (58 newly passing).
Updates #2180
When an ACL source list contains a wildcard (*) alongside explicit
sources (tags, groups, hosts, etc.), Tailscale preserves the individual
IPs from non-wildcard sources in SrcIPs alongside the merged wildcard
CGNAT ranges. Previously, headscale's IPSetBuilder would merge all
sources into a single set, absorbing the explicit IPs into the wildcard
range.
Track non-wildcard resolved addresses separately during source
resolution, then append their individual IP strings to the output
when a wildcard is also present. This fixes the remaining 5 ACL
compat test failures (K01 and M06 subtests).
Updates #2180