1
0
mirror of https://github.com/juanfont/headscale.git synced 2026-02-07 20:04:00 +01:00

policy: autogroup:internet does not generate packet filters

According to Tailscale SaaS behavior, autogroup:internet is handled
by exit node routing via AllowedIPs, not by packet filtering. ACL
rules with autogroup:internet as destination should produce no
filter rules for any node.

Previously, Headscale expanded autogroup:internet to public CIDR
ranges and distributed filters to exit nodes (because 0.0.0.0/0
"covers" internet destinations). This was incorrect.

Add detection for AutoGroupInternet in filter compilation to skip
filter generation for this autogroup. Update test expectations
accordingly.
This commit is contained in:
Kristoffer Dalby 2026-01-28 13:08:38 +00:00
parent 4996ce5cc2
commit 9008ce77fb
4 changed files with 22 additions and 42 deletions

View File

@ -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,
},

View File

@ -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},
},
},

View File

@ -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")

View File

@ -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)
//