diff --git a/hscontrol/policy/policy_test.go b/hscontrol/policy/policy_test.go index 65f47b4a..b62e94db 100644 --- a/hscontrol/policy/policy_test.go +++ b/hscontrol/policy/policy_test.go @@ -891,9 +891,11 @@ func TestReduceNodesFromPolicy(t *testing.T) { ] }`, node: n(1, "100.64.0.1", "mobile", "mobile"), + // autogroup:internet does not generate packet filters - it's handled + // by exit node routing via AllowedIPs, not by packet filtering. + // Only server is visible through the mobile -> server:80 rule. want: types.Nodes{ n(2, "100.64.0.2", "server", "server"), - n(3, "100.64.0.3", "exit", "server", "0.0.0.0/0", "::/0"), }, wantMatchers: 1, }, diff --git a/hscontrol/policy/policyutil/reduce_test.go b/hscontrol/policy/policyutil/reduce_test.go index 09342db7..0851e303 100644 --- a/hscontrol/policy/policyutil/reduce_test.go +++ b/hscontrol/policy/policyutil/reduce_test.go @@ -353,10 +353,12 @@ func TestReduceFilterRules(t *testing.T) { }, }, want: []tailcfg.FilterRule{ - // Merged: Both ACL rules combined (same SrcIPs and IPProto) + // Only the internal:* rule generates filters. + // autogroup:internet does NOT generate packet filters - it's handled + // by exit node routing via AllowedIPs, not by packet filtering. { SrcIPs: []string{"100.64.0.1/32", "100.64.0.2/32", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::2/128"}, - DstPorts: append([]tailcfg.NetPortRange{ + DstPorts: []tailcfg.NetPortRange{ { IP: "100.64.0.100/32", Ports: tailcfg.PortRangeAny, @@ -365,7 +367,7 @@ func TestReduceFilterRules(t *testing.T) { IP: "fd7a:115c:a1e0::100/128", Ports: tailcfg.PortRangeAny, }, - }, hsExitNodeDestForTest...), + }, IPProto: []int{v2.ProtocolTCP, v2.ProtocolUDP, v2.ProtocolICMP, v2.ProtocolIPv6ICMP}, }, }, diff --git a/hscontrol/policy/v2/filter.go b/hscontrol/policy/v2/filter.go index ae01fe2f..71540ea8 100644 --- a/hscontrol/policy/v2/filter.go +++ b/hscontrol/policy/v2/filter.go @@ -61,6 +61,12 @@ func (pol *Policy) compileFilterRules( continue } + // autogroup:internet does not generate packet filters - it's handled + // by exit node routing via AllowedIPs, not by packet filtering. + if ag, isAutoGroup := dest.Alias.(*AutoGroup); isAutoGroup && ag.Is(AutoGroupInternet) { + continue + } + ips, err := dest.Resolve(pol, users, nodes) if err != nil { log.Trace().Caller().Err(err).Msgf("resolving destination ips") @@ -259,6 +265,12 @@ func (pol *Policy) compileACLWithAutogroupSelf( continue } + // autogroup:internet does not generate packet filters - it's handled + // by exit node routing via AllowedIPs, not by packet filtering. + if ag, isAutoGroup := dest.Alias.(*AutoGroup); isAutoGroup && ag.Is(AutoGroupInternet) { + continue + } + ips, err := dest.Resolve(pol, users, nodes) if err != nil { log.Trace().Caller().Err(err).Msgf("resolving destination ips") diff --git a/hscontrol/policy/v2/tailscale_routes_compat_test.go b/hscontrol/policy/v2/tailscale_routes_compat_test.go index 247a9738..d7cfbb52 100644 --- a/hscontrol/policy/v2/tailscale_routes_compat_test.go +++ b/hscontrol/policy/v2/tailscale_routes_compat_test.go @@ -1124,31 +1124,15 @@ func TestTailscaleRoutesCompatExitNodes(t *testing.T) { "user1": wildcardFilter, }, }, - // TODO: Fix autogroup:internet to not generate filters - // // B8: autogroup:internet generates no filters // - // TAILSCALE BEHAVIOR: - // - autogroup:internet is handled by exit node routing, not packet filters - // - ALL nodes should get null/empty filters for autogroup:internet destination - // - Traffic is routed through exit nodes via AllowedIPs, not filtered - // - // HEADSCALE BEHAVIOR: - // - Exit nodes (exit-node, multi-router) incorrectly receive filters - // - Because 0.0.0.0/0 "covers" autogroup:internet destinations - // - // ROOT CAUSE: - // Headscale treats autogroup:internet like a regular destination and - // distributes filters to nodes whose routes cover it (exit nodes) - // - // FIX REQUIRED: - // autogroup:internet should never generate packet filters + // autogroup:internet is handled by exit node routing via AllowedIPs, + // not by packet filtering. ALL nodes should get null/empty filters. { name: "B8_autogroup_internet_no_filters", policy: makeRoutesPolicy(` {"action": "accept", "src": ["autogroup:member"], "dst": ["autogroup:internet:*"]} `), - /* EXPECTED (Tailscale): wantFilters: map[string][]tailcfg.FilterRule{ "client1": nil, "client2": nil, @@ -1160,26 +1144,6 @@ func TestTailscaleRoutesCompatExitNodes(t *testing.T) { "big-router": nil, "user1": nil, }, - */ - // ACTUAL (Headscale): - // Non-exit nodes correctly get nil. - // INCORRECT: exit-node and multi-router get filters with expanded public - // CIDR ranges from util.TheInternet() (all public IPs excluding CGNAT, - // private ranges, and Tailscale ULA). The exact CIDRs are complex and - // impractical to list here. Skipping comparison for exit nodes but - // documenting the difference: Tailscale returns nil, Headscale returns - // filters with SrcIPs=member IPs and DstPorts=expanded public CIDRs. - wantFilters: map[string][]tailcfg.FilterRule{ - "client1": nil, - "client2": nil, - "subnet-router": nil, - "ha-router1": nil, - "ha-router2": nil, - "big-router": nil, - "user1": nil, - // exit-node and multi-router omitted - they incorrectly receive filters - // with expanded autogroup:internet CIDRs in Headscale (Tailscale: nil) - }, }, // B3: Exit node advertises exit routes (verify RoutableIPs) //