mirror of
https://github.com/juanfont/headscale.git
synced 2026-10-05 22:30:07 +09:00
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
This commit is contained in:
@@ -99,3 +99,104 @@ func TestIssue3233ViaInternetExitVisibility(t *testing.T) {
|
||||
"alice viewing tag:exit2 exit must Exclude ::/0 — strips it from AllowedIPs")
|
||||
})
|
||||
}
|
||||
|
||||
// TestViaInternetExitSteeringSurvivesUnrelatedRules checks that a
|
||||
// regular rule matching the viewer only lifts via exit steering when its
|
||||
// destination actually reaches the internet. Narrow destinations overlap
|
||||
// 0.0.0.0/0 but must not evict it from Exclude.
|
||||
func TestViaInternetExitSteeringSurvivesUnrelatedRules(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
users := types.Users{
|
||||
{ID: 1, Name: "alice", Email: "alice@headscale.net"},
|
||||
{ID: 2, Name: "bob", Email: "bob@headscale.net"},
|
||||
}
|
||||
|
||||
exitRoutes := []netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6()}
|
||||
|
||||
aliceLaptop := node("alice-laptop", "100.64.0.10", "fd7a:115c:a1e0::a", users[0])
|
||||
aliceLaptop.ID = 1
|
||||
|
||||
exitA := node("exit-a", "100.64.0.11", "fd7a:115c:a1e0::b", users[0])
|
||||
exitA.ID = 2
|
||||
exitA.Tags = []string{"tag:exit-a"}
|
||||
exitA.Hostinfo = &tailcfg.Hostinfo{RoutableIPs: exitRoutes}
|
||||
exitA.ApprovedRoutes = exitRoutes
|
||||
|
||||
exitB := node("exit-b", "100.64.0.12", "fd7a:115c:a1e0::c", users[0])
|
||||
exitB.ID = 3
|
||||
exitB.Tags = []string{"tag:exit-b"}
|
||||
exitB.Hostinfo = &tailcfg.Hostinfo{RoutableIPs: exitRoutes}
|
||||
exitB.ApprovedRoutes = exitRoutes
|
||||
|
||||
nodes := types.Nodes{aliceLaptop, exitA, exitB}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
extra string
|
||||
// wantExcluded: exit-b's exit routes stay hidden from alice.
|
||||
wantExcluded bool
|
||||
}{
|
||||
{
|
||||
name: "via-grant-only",
|
||||
wantExcluded: true,
|
||||
},
|
||||
{
|
||||
name: "acl-autogroup-self",
|
||||
extra: `"acls": [{"action": "accept", "src": ["autogroup:member"], "dst": ["autogroup:self:*"]}],`,
|
||||
wantExcluded: true,
|
||||
},
|
||||
{
|
||||
name: "acl-single-host",
|
||||
extra: `"acls": [{"action": "accept", "src": ["autogroup:member"], "dst": ["100.64.0.15:53"]}],`,
|
||||
wantExcluded: true,
|
||||
},
|
||||
{
|
||||
name: "acl-private-subnet",
|
||||
extra: `"acls": [{"action": "accept", "src": ["autogroup:member"], "dst": ["10.0.0.0/8:*"]}],`,
|
||||
wantExcluded: true,
|
||||
},
|
||||
{
|
||||
name: "acl-other-exit-node",
|
||||
extra: `"acls": [{"action": "accept", "src": ["autogroup:member"], "dst": ["tag:exit-b:*"]}],`,
|
||||
wantExcluded: true,
|
||||
},
|
||||
{
|
||||
name: "acl-wildcard",
|
||||
extra: `"acls": [{"action": "accept", "src": ["autogroup:member"], "dst": ["*:*"]}],`,
|
||||
wantExcluded: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
policy := `{
|
||||
"tagOwners": {
|
||||
"tag:exit-a": ["alice@headscale.net"],
|
||||
"tag:exit-b": ["alice@headscale.net"]
|
||||
},
|
||||
` + tt.extra + `
|
||||
"grants": [{
|
||||
"src": ["autogroup:member"],
|
||||
"dst": ["autogroup:internet"],
|
||||
"ip": ["*"],
|
||||
"via": ["tag:exit-a"]
|
||||
}]
|
||||
}`
|
||||
|
||||
pm, err := NewPolicyManager([]byte(policy), users, nodes.ViewSlice())
|
||||
require.NoError(t, err)
|
||||
|
||||
viaPeer := pm.ViaRoutesForPeer(aliceLaptop.View(), exitA.View())
|
||||
require.Contains(t, viaPeer.Include, tsaddr.AllIPv4())
|
||||
|
||||
other := pm.ViaRoutesForPeer(aliceLaptop.View(), exitB.View())
|
||||
for _, p := range exitRoutes {
|
||||
require.Equal(t, tt.wantExcluded, slices.Contains(other.Exclude, p),
|
||||
"exit-b %s in Exclude: got %v", p, other.Exclude)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1418,7 +1418,17 @@ func (pm *PolicyManager) ViaRoutesForPeer(viewer, peer types.NodeView) types.Via
|
||||
}
|
||||
}
|
||||
|
||||
result.Exclude = slices.DeleteFunc(result.Exclude, dstPrefix.Overlaps)
|
||||
// Every address overlaps an exit route, so overlap
|
||||
// only decides for subnet routes.
|
||||
result.Exclude = slices.DeleteFunc(result.Exclude, func(p netip.Prefix) bool {
|
||||
return !tsaddr.IsExitRoute(p) && dstPrefix.Overlaps(p)
|
||||
})
|
||||
}
|
||||
|
||||
// A regular grant to the internet lets the viewer use any
|
||||
// exit node, not just the via-tagged ones.
|
||||
if grantReachesInternet(grant) {
|
||||
result.Exclude = slices.DeleteFunc(result.Exclude, tsaddr.IsExitRoute)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1426,6 +1436,17 @@ func (pm *PolicyManager) ViaRoutesForPeer(viewer, peer types.NodeView) types.Via
|
||||
return result
|
||||
}
|
||||
|
||||
// grantReachesInternet reports whether a grant's destinations include
|
||||
// the internet. The wildcard resolves to tailnet ranges only, but in a
|
||||
// destination it also covers the internet.
|
||||
func grantReachesInternet(grant Grant) bool {
|
||||
return slices.ContainsFunc(grant.Destinations, func(d Alias) bool {
|
||||
_, ok := d.(Asterix)
|
||||
|
||||
return ok
|
||||
})
|
||||
}
|
||||
|
||||
func (pm *PolicyManager) Version() int {
|
||||
return 2
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user