From 044f3fc0eca4e4ed56900758b622fddc90503b4d Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 18 Mar 2026 14:40:12 +0000 Subject: [PATCH] policy/policyutil: exclude exit routes from ReduceFilterRules Exit nodes handle traffic via AllowedIPs/routing, not packet filter rules. Skip exit routes (0.0.0.0/0, ::/0) when checking RoutableIPs overlap in ReduceFilterRules, matching Tailscale SaaS behavior where exit nodes do not receive filter rules for destinations that only overlap via exit routes. Updates #2180 --- hscontrol/policy/policyutil/reduce.go | 12 ++++++-- hscontrol/policy/policyutil/reduce_test.go | 34 +++------------------- 2 files changed, 14 insertions(+), 32 deletions(-) diff --git a/hscontrol/policy/policyutil/reduce.go b/hscontrol/policy/policyutil/reduce.go index 6d95a297..f3f1903c 100644 --- a/hscontrol/policy/policyutil/reduce.go +++ b/hscontrol/policy/policyutil/reduce.go @@ -3,6 +3,7 @@ package policyutil import ( "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/util" + "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" ) @@ -33,12 +34,19 @@ func ReduceFilterRules(node types.NodeView, rules []tailcfg.FilterRule) []tailcf continue DEST_LOOP } - // If the node exposes routes, ensure they are note removed - // when the filters are reduced. + // If the node exposes routes, ensure they are not removed + // when the filters are reduced. Exit routes (0.0.0.0/0, ::/0) + // are skipped here because exit nodes handle traffic via + // AllowedIPs/routing, not packet filter rules. This matches + // Tailscale SaaS behavior where exit nodes do not receive + // filter rules for destinations that only overlap via exit routes. if node.Hostinfo().Valid() { routableIPs := node.Hostinfo().RoutableIPs() if routableIPs.Len() > 0 { for _, routableIP := range routableIPs.All() { + if tsaddr.IsExitRoute(routableIP) { + continue + } if expanded.OverlapsPrefix(routableIP) { dests = append(dests, dest) continue DEST_LOOP diff --git a/hscontrol/policy/policyutil/reduce_test.go b/hscontrol/policy/policyutil/reduce_test.go index db0262fc..893f5b2f 100644 --- a/hscontrol/policy/policyutil/reduce_test.go +++ b/hscontrol/policy/policyutil/reduce_test.go @@ -453,7 +453,10 @@ func TestReduceFilterRules(t *testing.T) { }, }, want: []tailcfg.FilterRule{ - // Merged: Both ACL rules combined (same SrcIPs) + // Exit routes (0.0.0.0/0, ::/0) are skipped when checking RoutableIPs + // overlap, matching Tailscale SaaS behavior. Only destinations that + // contain the node's own Tailscale IP (via InIPSet) are kept. + // Here, 64.0.0.0/2 contains 100.64.0.100 (CGNAT range), so it matches. { SrcIPs: []string{ "100.64.0.1-100.64.0.2", @@ -464,36 +467,7 @@ func TestReduceFilterRules(t *testing.T) { IP: "100.64.0.100", Ports: tailcfg.PortRangeAny, }, - {IP: "0.0.0.0/5", Ports: tailcfg.PortRangeAny}, - {IP: "8.0.0.0/7", Ports: tailcfg.PortRangeAny}, - {IP: "11.0.0.0/8", Ports: tailcfg.PortRangeAny}, - {IP: "12.0.0.0/6", Ports: tailcfg.PortRangeAny}, - {IP: "16.0.0.0/4", Ports: tailcfg.PortRangeAny}, - {IP: "32.0.0.0/3", Ports: tailcfg.PortRangeAny}, {IP: "64.0.0.0/2", Ports: tailcfg.PortRangeAny}, - {IP: "128.0.0.0/3", Ports: tailcfg.PortRangeAny}, - {IP: "160.0.0.0/5", Ports: tailcfg.PortRangeAny}, - {IP: "168.0.0.0/6", Ports: tailcfg.PortRangeAny}, - {IP: "172.0.0.0/12", Ports: tailcfg.PortRangeAny}, - {IP: "172.32.0.0/11", Ports: tailcfg.PortRangeAny}, - {IP: "172.64.0.0/10", Ports: tailcfg.PortRangeAny}, - {IP: "172.128.0.0/9", Ports: tailcfg.PortRangeAny}, - {IP: "173.0.0.0/8", Ports: tailcfg.PortRangeAny}, - {IP: "174.0.0.0/7", Ports: tailcfg.PortRangeAny}, - {IP: "176.0.0.0/4", Ports: tailcfg.PortRangeAny}, - {IP: "192.0.0.0/9", Ports: tailcfg.PortRangeAny}, - {IP: "192.128.0.0/11", Ports: tailcfg.PortRangeAny}, - {IP: "192.160.0.0/13", Ports: tailcfg.PortRangeAny}, - {IP: "192.169.0.0/16", Ports: tailcfg.PortRangeAny}, - {IP: "192.170.0.0/15", Ports: tailcfg.PortRangeAny}, - {IP: "192.172.0.0/14", Ports: tailcfg.PortRangeAny}, - {IP: "192.176.0.0/12", Ports: tailcfg.PortRangeAny}, - {IP: "192.192.0.0/10", Ports: tailcfg.PortRangeAny}, - {IP: "193.0.0.0/8", Ports: tailcfg.PortRangeAny}, - {IP: "194.0.0.0/7", Ports: tailcfg.PortRangeAny}, - {IP: "196.0.0.0/6", Ports: tailcfg.PortRangeAny}, - {IP: "200.0.0.0/5", Ports: tailcfg.PortRangeAny}, - {IP: "208.0.0.0/4", Ports: tailcfg.PortRangeAny}, }, }, },