diff --git a/hscontrol/policy/v2/filter.go b/hscontrol/policy/v2/filter.go index 8b9124095..3f9902d91 100644 --- a/hscontrol/policy/v2/filter.go +++ b/hscontrol/policy/v2/filter.go @@ -6,6 +6,7 @@ import ( "net/netip" "slices" "strings" + "sync" "time" "github.com/juanfont/headscale/hscontrol/types" @@ -132,12 +133,29 @@ func (pol *Policy) compileFilterRules( nodes views.Slice[types.NodeView], ) []tailcfg.FilterRule { if pol == nil || (pol.ACLs == nil && pol.Grants == nil) { - return tailcfg.FilterAllowAll + return noRulesFilter(users, nodes) } return globalFilterRules(pol.compileGrants(users, nodes)) } +// allowAllPolicy is the rule a policy without acls or grants stands for. +var allowAllPolicy = sync.OnceValue(func() *Policy { + pol, err := unmarshalPolicy([]byte(`{"acls":[{"action":"accept","src":["*"],"dst":["*:*"]}]}`)) + if err != nil { + panic(err) + } + + return pol +}) + +// noRulesFilter is the filter of a policy without acls or grants. As on SaaS, +// its sources are the tailnet's ranges and approved routes, not every address, +// or a host on an exit node's network could open connections to its clients. +func noRulesFilter(users types.Users, nodes views.Slice[types.NodeView]) []tailcfg.FilterRule { + return globalFilterRules(allowAllPolicy().compileGrants(users, nodes)) +} + func (pol *Policy) destinationsToNetPortRange( users types.Users, nodes views.Slice[types.NodeView], @@ -201,7 +219,7 @@ func (pol *Policy) compileFilterRulesForNode( nodes views.Slice[types.NodeView], ) []tailcfg.FilterRule { if pol == nil { - return tailcfg.FilterAllowAll + return noRulesFilter(users, nodes) } grants := pol.compileGrants(users, nodes) diff --git a/hscontrol/policy/v2/filter_test.go b/hscontrol/policy/v2/filter_test.go index fbde1ff2b..0eae6965d 100644 --- a/hscontrol/policy/v2/filter_test.go +++ b/hscontrol/policy/v2/filter_test.go @@ -3687,7 +3687,7 @@ func TestFilterAllowAllFix(t *testing.T) { rules := tt.pol.compileFilterRules(users, nodes) - isFilterAllowAll := cmp.Diff(tailcfg.FilterAllowAll, rules) == "" + isFilterAllowAll := cmp.Diff(noRulesFilter(users, nodes), rules) == "" assert.Equal(t, tt.wantFilterAllow, isFilterAllowAll, "FilterAllowAll mismatch: got rules=%v", rules) }) diff --git a/hscontrol/policy/v2/policy.go b/hscontrol/policy/v2/policy.go index a3c9e8c47..ae35bfc99 100644 --- a/hscontrol/policy/v2/policy.go +++ b/hscontrol/policy/v2/policy.go @@ -342,7 +342,7 @@ func (pm *PolicyManager) updateLocked() (bool, error) { var filter []tailcfg.FilterRule if pm.pol == nil || (pm.pol.ACLs == nil && pm.pol.Grants == nil) { - filter = tailcfg.FilterAllowAll + filter = noRulesFilter(pm.users, pm.nodes) } else { filter = globalFilterRules(pm.compiledGrants) } diff --git a/hscontrol/policy/v2/policy_test.go b/hscontrol/policy/v2/policy_test.go index 06d8ab5c6..916fb9996 100644 --- a/hscontrol/policy/v2/policy_test.go +++ b/hscontrol/policy/v2/policy_test.go @@ -74,8 +74,8 @@ func TestPolicyManager(t *testing.T) { name: "empty-policy", pol: "{}", nodes: types.Nodes{}, - wantFilter: tailcfg.FilterAllowAll, - wantMatchers: matcher.MatchesFromFilterRules(tailcfg.FilterAllowAll), + wantFilter: noRulesFilter(users, types.Nodes{}.ViewSlice()), + wantMatchers: matcher.MatchesFromFilterRules(noRulesFilter(users, types.Nodes{}.ViewSlice())), }, { name: "empty-acls-denies-all", diff --git a/hscontrol/policy/v2/tailscale_acl_data_compat_test.go b/hscontrol/policy/v2/tailscale_acl_data_compat_test.go index e1508c1a9..3c628ef2a 100644 --- a/hscontrol/policy/v2/tailscale_acl_data_compat_test.go +++ b/hscontrol/policy/v2/tailscale_acl_data_compat_test.go @@ -208,15 +208,6 @@ func loadACLTestFile(t *testing.T, path string) *testcapture.Capture { // aclSkipReasons lists checks where headscale differs from SaaS, keyed // [/]/. var aclSkipReasons = map[string]string{ - "acl-n01/beedrill/filter": allowAllForm, - "acl-n01/bulbasaur/filter": allowAllForm, - "acl-n01/charmander/filter": allowAllForm, - "acl-n01/ivysaur/filter": allowAllForm, - "acl-n01/kakuna/filter": allowAllForm, - "acl-n01/squirtle/filter": allowAllForm, - "acl-n01/venusaur/filter": allowAllForm, - "acl-n01/weedle/filter": allowAllForm, - "acl-au06/beedrill/filter": srcAddressForm, "acl-k03/beedrill/filter": srcAddressForm, "acl-k03/bulbasaur/filter": srcAddressForm, @@ -246,7 +237,6 @@ var aclSkipReasons = map[string]string{ } const ( - allowAllForm = "ALLOW_ALL_FORM: SaaS writes the allow-all default's sources as the tailnet ranges, headscale as \"*\"" srcAddressForm = "FILTER_ADDRESS_FORM: SaaS lists contiguous source addresses one by one, headscale as a range" srcAndSelfDstForm = "FILTER_ADDRESS_FORM+SELF_DST_REPEAT: sources listed one by one, the autogroup:self destination written once per source alias" aclDefaultRouteAccepted = "ACL_DEFAULT_ROUTE_ACCEPTED: headscale accepts literal 0.0.0.0/0 and ::/0 ACL destinations; SaaS rejects them and requires * or autogroup:internet" diff --git a/hscontrol/policy/v2/test.go b/hscontrol/policy/v2/test.go index c0518657c..2cb32a82f 100644 --- a/hscontrol/policy/v2/test.go +++ b/hscontrol/policy/v2/test.go @@ -339,7 +339,7 @@ func evaluateTests(pol *Policy, users []types.User, nodes views.Slice[types.Node var filter []tailcfg.FilterRule if pol.ACLs == nil && pol.Grants == nil { - filter = tailcfg.FilterAllowAll + filter = noRulesFilter(users, nodes) } else { filter = globalFilterRules(grants) }