mirror of
https://github.com/juanfont/headscale.git
synced 2026-10-05 22:30:07 +09:00
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
This commit is contained in:
@@ -19,7 +19,7 @@ import (
|
||||
func ReduceFilterRules(node types.NodeView, rules []tailcfg.FilterRule) []tailcfg.FilterRule {
|
||||
ret := []tailcfg.FilterRule{}
|
||||
subnetRoutes := node.SubnetRoutes()
|
||||
hasExitRoutes := node.IsExitNode()
|
||||
exitRoutes := node.ExitRoutes()
|
||||
|
||||
for _, rule := range rules {
|
||||
// Handle CapGrant rules separately — they use CapGrant[].Dsts
|
||||
@@ -53,19 +53,16 @@ func ReduceFilterRules(node types.NodeView, rules []tailcfg.FilterRule) []tailcf
|
||||
// [types.NodeView.SubnetRoutes] returns only approved,
|
||||
// non-exit routes — matching Tailscale SaaS behavior,
|
||||
// which does not generate filter rules for
|
||||
// advertised-but-unapproved routes. Exit routes
|
||||
// (0.0.0.0/0, ::/0) are excluded by
|
||||
// [types.NodeView.SubnetRoutes] and handled separately
|
||||
// via AllowedIPs/routing.
|
||||
// advertised-but-unapproved routes.
|
||||
if slices.ContainsFunc(subnetRoutes, expanded.OverlapsPrefix) {
|
||||
dests = append(dests, dest)
|
||||
continue
|
||||
}
|
||||
|
||||
// Exit-route advertisers need rules targeting the
|
||||
// public internet so the kernel filter accepts
|
||||
// traffic forwarded by autogroup:internet sources.
|
||||
if hasExitRoutes && util.IPSetSubsetOf(expanded, util.TheInternet()) {
|
||||
// Approved exit routes count like subnet routes. They
|
||||
// contain every destination, so Tailscale SaaS sends an
|
||||
// exit node every rule, not only internet ones.
|
||||
if slices.ContainsFunc(exitRoutes, expanded.OverlapsPrefix) {
|
||||
dests = append(dests, dest)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -886,6 +886,66 @@ func TestReduceFilterRulesPartialApproval(t *testing.T) {
|
||||
},
|
||||
wantCount: 0,
|
||||
},
|
||||
{
|
||||
// Tailscale SaaS delivers every rule to an approved exit
|
||||
// node: its exit routes contain every destination.
|
||||
name: "approved-exit-route-includes-tailnet-host",
|
||||
node: &types.Node{
|
||||
IPv4: ap("100.64.0.1"),
|
||||
IPv6: ap("fd7a:115c:a1e0::1"),
|
||||
Hostinfo: &tailcfg.Hostinfo{RoutableIPs: tsaddr.ExitRoutes()},
|
||||
|
||||
ApprovedRoutes: tsaddr.ExitRoutes(),
|
||||
},
|
||||
rules: []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{"100.64.0.17"},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "100.64.0.16", Ports: tailcfg.PortRange{First: 53, Last: 53}},
|
||||
{IP: "fd7a:115c:a1e0::10", Ports: tailcfg.PortRange{First: 53, Last: 53}},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantCount: 1,
|
||||
wantRoutes: []string{"100.64.0.16", "fd7a:115c:a1e0::10"},
|
||||
},
|
||||
{
|
||||
name: "approved-exit-route-includes-private-subnet",
|
||||
node: &types.Node{
|
||||
IPv4: ap("100.64.0.1"),
|
||||
IPv6: ap("fd7a:115c:a1e0::1"),
|
||||
Hostinfo: &tailcfg.Hostinfo{RoutableIPs: tsaddr.ExitRoutes()},
|
||||
|
||||
ApprovedRoutes: tsaddr.ExitRoutes(),
|
||||
},
|
||||
rules: []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{"100.64.0.17"},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantCount: 1,
|
||||
wantRoutes: []string{"10.33.0.0/16"},
|
||||
},
|
||||
{
|
||||
name: "unapproved-exit-route-excluded",
|
||||
node: &types.Node{
|
||||
IPv4: ap("100.64.0.1"),
|
||||
IPv6: ap("fd7a:115c:a1e0::1"),
|
||||
Hostinfo: &tailcfg.Hostinfo{RoutableIPs: tsaddr.ExitRoutes()},
|
||||
},
|
||||
rules: []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{"100.64.0.17"},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "100.64.0.16", Ports: tailcfg.PortRangeAny},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantCount: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
Reference in New Issue
Block a user