policy: surface exit nodes via autogroup:internet (#3212)
compileFilterRules skipped autogroup:internet destinations to keep them out of the wire-format PacketFilter, but those same compiled rules are the source of pm.matchers — and Node.CanAccess relies on a matcher whose DestsIsTheInternet covers the public internet to surface exit-node peers to ACL sources. With the skip in place no such matcher existed, exit nodes silently dropped out of the source's peer list, and the docs' exit-node walkthrough stopped working: `tailscale exit-node list` returned "no exit nodes found" and `tailscale set --exit-node=<ip>` returned "no node found in netmap with IP". Drop the compile-time skip so autogroup:internet flows through normal matcher derivation, and teach ReduceFilterRules to keep the resulting client packet-filter rule on exit-route advertisers — Tailscale SaaS sends those rules to exit nodes so the kernel filter accepts traffic forwarded by autogroup:internet sources. Verified against a live tailnet on 2026-04-28 via tscap; the b17/b18 captures land under testdata/issue_3212/ as a regression guard. The captures are isolated from testdata/routes_results/ because the broader TestRoutesCompat machinery assumes a CIDR-prefix wire format that differs from the IPSet-range form SaaS emits for autogroup:internet — aligning that wire format is tracked separately. Fixes #3212
This commit is contained in:
parent
a7d405a255
commit
c7a0ca709f
8 changed files with 22985 additions and 11 deletions
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
|
|
@ -1806,3 +1807,50 @@ func TestViaRoutesForPeer(t *testing.T) {
|
|||
"state.RoutesForPeer adds via routes after ReduceRoutes to fix this")
|
||||
})
|
||||
}
|
||||
|
||||
// TestBuildPeerMap_AutogroupInternetMakesExitNodeVisible reproduces
|
||||
// juanfont/headscale#3212. An ACL that grants access only via
|
||||
// `autogroup:internet` must keep the exit node visible to the source
|
||||
// in BuildPeerMap so the Tailscale client surfaces it in
|
||||
// `tailscale exit-node list`. Authoritative SaaS captures
|
||||
// (tscap routes-b17/b18, 2026-04-28) confirm SaaS includes the exit
|
||||
// node in the source's Peers with 0.0.0.0/0 and ::/0 in AllowedIPs.
|
||||
func TestBuildPeerMap_AutogroupInternetMakesExitNodeVisible(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
users := types.Users{
|
||||
{Model: gorm.Model{ID: 1}, Name: "alice", Email: "alice@headscale.net"},
|
||||
}
|
||||
|
||||
aliceNode := node("alice-laptop", "100.64.0.10", "fd7a:115c:a1e0::a", users[0])
|
||||
aliceNode.ID = 1
|
||||
|
||||
exitRoutes := []netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6()}
|
||||
exitNode := node("alice-exit", "100.64.0.1", "fd7a:115c:a1e0::1", users[0])
|
||||
exitNode.ID = 2
|
||||
exitNode.Hostinfo = &tailcfg.Hostinfo{RoutableIPs: exitRoutes}
|
||||
exitNode.ApprovedRoutes = exitRoutes
|
||||
|
||||
nodes := types.Nodes{aliceNode, exitNode}
|
||||
|
||||
policy := `{
|
||||
"acls": [
|
||||
{"action": "accept", "src": ["alice@headscale.net"], "dst": ["autogroup:internet:*"]}
|
||||
]
|
||||
}`
|
||||
|
||||
pm, err := NewPolicyManager([]byte(policy), users, nodes.ViewSlice())
|
||||
require.NoError(t, err)
|
||||
|
||||
peerMap := pm.BuildPeerMap(nodes.ViewSlice())
|
||||
|
||||
require.True(t,
|
||||
slices.ContainsFunc(peerMap[aliceNode.ID], func(n types.NodeView) bool {
|
||||
return n.ID() == exitNode.ID
|
||||
}),
|
||||
"alice should see the exit node as a peer when an ACL grants autogroup:internet (#3212)")
|
||||
|
||||
_, matchers := pm.Filter()
|
||||
require.True(t, aliceNode.View().CanAccess(matchers, exitNode.View()),
|
||||
"alice.CanAccess(exit) should be true via DestsIsTheInternet()+IsExitNode() (#3212)")
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue