mirror of
https://github.com/juanfont/headscale.git
synced 2026-04-07 21:47:46 +09:00
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:
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user