From 687cf0882f8ad2baf42c1bea4d97077d04a96108 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 18 Mar 2026 23:09:42 +0000 Subject: [PATCH] policy/v2: implement autogroup:danger-all support Add autogroup:danger-all as a valid source alias that matches ALL IP addresses including non-Tailscale addresses. When used as a source, it resolves to 0.0.0.0/0 + ::/0 internally but produces SrcIPs: ["*"] in filter rules. When used as a destination, it is rejected with an error matching Tailscale SaaS behavior. Key changes: - Add AutoGroupDangerAll constant and validation - Add sourcesHaveDangerAll() helper and hasDangerAll parameter to srcIPsWithRoutes() across all compilation paths - Add ErrAutogroupDangerAllDst for destination rejection - Remove 3 AUTOGROUP_DANGER_ALL skip entries (K6, K7, K8) Updates #2180 --- hscontrol/policy/v2/filter.go | 32 ++++++++++++++++--- .../policy/v2/tailscale_grants_compat_test.go | 23 ++----------- hscontrol/policy/v2/types.go | 28 ++++++++++++---- hscontrol/policy/v2/types_test.go | 2 +- 4 files changed, 52 insertions(+), 33 deletions(-) diff --git a/hscontrol/policy/v2/filter.go b/hscontrol/policy/v2/filter.go index 52cd7267..27590d90 100644 --- a/hscontrol/policy/v2/filter.go +++ b/hscontrol/policy/v2/filter.go @@ -97,13 +97,32 @@ func sourcesHaveWildcard(srcs Aliases) bool { return false } +// sourcesHaveDangerAll returns true if any of the source aliases is +// autogroup:danger-all. When present, SrcIPs should be ["*"] to +// represent all IP addresses including non-Tailscale addresses. +func sourcesHaveDangerAll(srcs Aliases) bool { + for _, src := range srcs { + if ag, ok := src.(*AutoGroup); ok && ag.Is(AutoGroupDangerAll) { + return true + } + } + + return false +} + // srcIPsWithRoutes returns the SrcIPs string slice, appending // approved subnet routes when the sources include a wildcard. +// When hasDangerAll is true, returns ["*"] to represent all IPs. func srcIPsWithRoutes( resolved ResolvedAddresses, hasWildcard bool, + hasDangerAll bool, nodes views.Slice[types.NodeView], ) []string { + if hasDangerAll { + return []string{"*"} + } + ips := resolved.Strings() if hasWildcard { ips = append(ips, approvedSubnetRoutes(nodes)...) @@ -146,13 +165,14 @@ func (pol *Policy) compileFilterRules( } hasWildcard := sourcesHaveWildcard(grant.Sources) + hasDangerAll := sourcesHaveDangerAll(grant.Sources) for _, ipp := range grant.InternetProtocols { destPorts := pol.destinationsToNetPortRange(users, nodes, grant.Destinations, ipp.Ports) if len(destPorts) > 0 { rules = append(rules, tailcfg.FilterRule{ - SrcIPs: srcIPsWithRoutes(srcIPs, hasWildcard, nodes), + SrcIPs: srcIPsWithRoutes(srcIPs, hasWildcard, hasDangerAll, nodes), DstPorts: destPorts, IPProto: ipp.Protocol.toIANAProtocolNumbers(), }) @@ -180,7 +200,7 @@ func (pol *Policy) compileFilterRules( dstIPStrings = append(dstIPStrings, ips.Strings()...) } - srcIPStrs := srcIPsWithRoutes(srcIPs, hasWildcard, nodes) + srcIPStrs := srcIPsWithRoutes(srcIPs, hasWildcard, hasDangerAll, nodes) rules = append(rules, tailcfg.FilterRule{ SrcIPs: srcIPStrs, CapGrant: capGrants, @@ -390,7 +410,8 @@ func (pol *Policy) compileViaGrant( } hasWildcard := sourcesHaveWildcard(grant.Sources) - srcIPStrs := srcIPsWithRoutes(srcResolved, hasWildcard, nodes) + hasDangerAll := sourcesHaveDangerAll(grant.Sources) + srcIPStrs := srcIPsWithRoutes(srcResolved, hasWildcard, hasDangerAll, nodes) // Build DstPorts from the matching via prefixes. var rules []tailcfg.FilterRule @@ -491,6 +512,7 @@ func (pol *Policy) compileGrantWithAutogroupSelf( } hasWildcard := sourcesHaveWildcard(grant.Sources) + hasDangerAll := sourcesHaveDangerAll(grant.Sources) for _, ipp := range grant.InternetProtocols { // Handle non-self destinations first to match Tailscale's @@ -513,7 +535,7 @@ func (pol *Policy) compileGrantWithAutogroupSelf( destPorts := pol.destinationsToNetPortRange(users, nodes, otherDests, ipp.Ports) if len(destPorts) > 0 { - srcIPStrs := srcIPsWithRoutes(srcResolved, hasWildcard, nodes) + srcIPStrs := srcIPsWithRoutes(srcResolved, hasWildcard, hasDangerAll, nodes) // When sources include a wildcard (*) alongside // explicit sources (tags, groups, etc.), Tailscale @@ -625,7 +647,7 @@ func (pol *Policy) compileGrantWithAutogroupSelf( } if !srcResolved.Empty() { - srcIPStrs = srcIPsWithRoutes(srcResolved, hasWildcard, nodes) + srcIPStrs = srcIPsWithRoutes(srcResolved, hasWildcard, hasDangerAll, nodes) if hasWildcard && len(nonWildcardSrcs) > 0 { seen := make(map[string]bool, len(srcIPStrs)) diff --git a/hscontrol/policy/v2/tailscale_grants_compat_test.go b/hscontrol/policy/v2/tailscale_grants_compat_test.go index 6384d9be..8ea85a8f 100644 --- a/hscontrol/policy/v2/tailscale_grants_compat_test.go +++ b/hscontrol/policy/v2/tailscale_grants_compat_test.go @@ -214,10 +214,9 @@ func loadGrantTestFile(t *testing.T, path string) grantTestFile { // // Impact summary (highest first): // -// AUTOGROUP_DANGER_ALL - 3 tests: Implement autogroup:danger-all support // USER_PASSKEY_WILDCARD - 2 tests: user:*@passkey wildcard pattern unresolvable // -// Total: 5 tests skipped, ~232 tests expected to pass. +// Total: 2 tests skipped, ~235 tests expected to pass. var grantSkipReasons = map[string]string{ // ======================================================================== // USER_PASSKEY_WILDCARD (2 tests) @@ -235,23 +234,6 @@ var grantSkipReasons = map[string]string{ // ======================================================================== "GRANT-K20": "USER_PASSKEY_WILDCARD: src=user:*@passkey, dst=tag:server — source can't be resolved, no rules produced", "GRANT-K21": "USER_PASSKEY_WILDCARD: src=*, dst=user:*@passkey — destination can't be resolved, no rules produced", - - // ======================================================================== - // AUTOGROUP_DANGER_ALL (3 tests) - // - // TODO: Implement autogroup:danger-all support. - // - // autogroup:danger-all matches ALL IPs including non-Tailscale addresses. - // When used as a source, it should expand to 0.0.0.0/0 and ::/0. - // When used as a destination, Tailscale rejects it with an error. - // - // GRANT-K6: autogroup:danger-all as src (success test, produces rules) - // GRANT-K7: autogroup:danger-all as dst (error: "cannot use autogroup:danger-all as a dst") - // GRANT-K8: autogroup:danger-all as both src and dst (error: same message) - // ======================================================================== - "GRANT-K6": "AUTOGROUP_DANGER_ALL", - "GRANT-K7": "AUTOGROUP_DANGER_ALL", - "GRANT-K8": "AUTOGROUP_DANGER_ALL", } // TestGrantsCompat is a data-driven test that loads all 237 GRANT-*.json @@ -269,10 +251,9 @@ var grantSkipReasons = map[string]string{ // // Skip category impact summary (highest first): // -// AUTOGROUP_DANGER_ALL - 3 tests: Implement autogroup:danger-all support // USER_PASSKEY_WILDCARD - 2 tests: user:*@passkey wildcard pattern unresolvable // -// Total: 5 tests skipped, ~232 tests expected to pass. +// Total: 2 tests skipped, ~235 tests expected to pass. func TestGrantsCompat(t *testing.T) { t.Parallel() diff --git a/hscontrol/policy/v2/types.go b/hscontrol/policy/v2/types.go index 775dd905..e053277c 100644 --- a/hscontrol/policy/v2/types.go +++ b/hscontrol/policy/v2/types.go @@ -118,6 +118,7 @@ var ( ErrAutogroupSelfSrc = errors.New("\"autogroup:self\" not valid on the src side of a rule") ErrAutogroupNotSupportedACLSrc = errors.New("autogroup not supported for ACL sources") ErrAutogroupNotSupportedACLDst = errors.New("autogroup not supported for ACL destinations") + ErrAutogroupDangerAllDst = errors.New("cannot use autogroup:danger-all as a dst") ErrAutogroupNotSupportedSSHSrc = errors.New("autogroup not supported for SSH sources") ErrAutogroupNotSupportedSSHDst = errors.New("autogroup not supported for SSH destinations") ErrAutogroupNotSupportedSSHUsr = errors.New("autogroup not supported for SSH user") @@ -701,11 +702,12 @@ func (p *Prefix) resolve(_ *Policy, _ types.Users, _ views.Slice[types.NodeView] type AutoGroup string const ( - AutoGroupInternet AutoGroup = "autogroup:internet" - AutoGroupMember AutoGroup = "autogroup:member" - AutoGroupNonRoot AutoGroup = "autogroup:nonroot" - AutoGroupTagged AutoGroup = "autogroup:tagged" - AutoGroupSelf AutoGroup = "autogroup:self" + AutoGroupInternet AutoGroup = "autogroup:internet" + AutoGroupMember AutoGroup = "autogroup:member" + AutoGroupNonRoot AutoGroup = "autogroup:nonroot" + AutoGroupTagged AutoGroup = "autogroup:tagged" + AutoGroupSelf AutoGroup = "autogroup:self" + AutoGroupDangerAll AutoGroup = "autogroup:danger-all" ) var autogroups = []AutoGroup{ @@ -714,6 +716,7 @@ var autogroups = []AutoGroup{ AutoGroupNonRoot, AutoGroupTagged, AutoGroupSelf, + AutoGroupDangerAll, } func (ag *AutoGroup) Validate() error { @@ -786,6 +789,15 @@ func (ag *AutoGroup) resolve(p *Policy, users types.Users, nodes views.Slice[typ // specially during policy compilation per-node for security. return nil, ErrAutogroupSelfRequiresPerNodeResolution + case AutoGroupDangerAll: + // autogroup:danger-all matches ALL IP addresses, including + // non-Tailscale addresses. Resolves to 0.0.0.0/0 + ::/0. + // Filter compilation converts this to SrcIPs: ["*"]. + build.AddPrefix(netip.MustParsePrefix("0.0.0.0/0")) + build.AddPrefix(netip.MustParsePrefix("::/0")) + + return build.IPSet() + case AutoGroupNonRoot: // autogroup:nonroot represents non-root users on multi-user devices. // This is not supported in headscale and requires OS-level user detection. @@ -1986,7 +1998,7 @@ type Policy struct { var ( // TODO(kradalby): Add these checks for tagOwners and autoApprovers. - autogroupForSrc = []AutoGroup{AutoGroupMember, AutoGroupTagged} + autogroupForSrc = []AutoGroup{AutoGroupMember, AutoGroupTagged, AutoGroupDangerAll} autogroupForDst = []AutoGroup{AutoGroupInternet, AutoGroupMember, AutoGroupTagged, AutoGroupSelf} autogroupForSSHSrc = []AutoGroup{AutoGroupMember, AutoGroupTagged} autogroupForSSHDst = []AutoGroup{AutoGroupMember, AutoGroupTagged, AutoGroupSelf} @@ -2033,6 +2045,10 @@ func validateAutogroupForDst(dst *AutoGroup) error { return nil } + if dst.Is(AutoGroupDangerAll) { + return ErrAutogroupDangerAllDst + } + if !slices.Contains(autogroupForDst, *dst) { return fmt.Errorf("%w: %q, can be %v", ErrAutogroupNotSupportedACLDst, *dst, autogroupForDst) } diff --git a/hscontrol/policy/v2/types_test.go b/hscontrol/policy/v2/types_test.go index 1e31c7f2..dfd63868 100644 --- a/hscontrol/policy/v2/types_test.go +++ b/hscontrol/policy/v2/types_test.go @@ -459,7 +459,7 @@ func TestUnmarshalPolicy(t *testing.T) { ], } `, - wantErr: `invalid autogroup: got "autogroup:invalid", must be one of [autogroup:internet autogroup:member autogroup:nonroot autogroup:tagged autogroup:self]`, + wantErr: `invalid autogroup: got "autogroup:invalid", must be one of [autogroup:internet autogroup:member autogroup:nonroot autogroup:tagged autogroup:self autogroup:danger-all]`, }, { name: "undefined-hostname-errors-2490",