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
This commit is contained in:
Kristoffer Dalby
2026-03-18 23:09:42 +00:00
parent 4f040dead2
commit 687cf0882f
4 changed files with 52 additions and 33 deletions

View File

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

View File

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

View File

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

View File

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