From dda35847b020f9b9ac787caccf11dac18d8f6d9a Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 18 Mar 2026 15:12:18 +0000 Subject: [PATCH] policy/v2: reorder ACL self grants to match Tailscale rule ordering When an ACL has non-autogroup destinations (groups, users, tags, hosts) alongside autogroup:self, emit non-self grants before self grants to match Tailscale's filter rule ordering. ACLs with only autogroup destinations (self + member) preserve the policy-defined order. This fixes ACL-A17, ACL-SF07, and ACL-SF11 compat test failures. Updates #2180 --- hscontrol/policy/v2/types.go | 63 +++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/hscontrol/policy/v2/types.go b/hscontrol/policy/v2/types.go index f774e4be..3b8fde8a 100644 --- a/hscontrol/policy/v2/types.go +++ b/hscontrol/policy/v2/types.go @@ -1891,17 +1891,64 @@ type Grant struct { func aclToGrants(acl ACL) []Grant { ret := make([]Grant, 0, len(acl.Destinations)) + // Check if the ACL has any non-autogroup destinations. If so, + // reorder to place non-self grants before self grants. This matches + // Tailscale's behavior where autogroup-only ACLs (self + member) + // preserve policy order, but ACLs with groups, users, tags, or + // hosts emit non-self rules first. + hasNonAutogroup := false for _, dst := range acl.Destinations { - g := Grant{ - Sources: acl.Sources, - Destinations: Aliases{dst.Alias}, - InternetProtocols: []ProtocolPort{{ - Protocol: acl.Protocol, - Ports: dst.Ports, - }}, + if _, ok := dst.Alias.(*AutoGroup); !ok { + hasNonAutogroup = true + + break + } + } + + if hasNonAutogroup { + // Non-self destinations first, self destinations second. + for _, dst := range acl.Destinations { + if ag, ok := dst.Alias.(*AutoGroup); ok && ag.Is(AutoGroupSelf) { + continue + } + + ret = append(ret, Grant{ + Sources: acl.Sources, + Destinations: Aliases{dst.Alias}, + InternetProtocols: []ProtocolPort{{ + Protocol: acl.Protocol, + Ports: dst.Ports, + }}, + }) } - ret = append(ret, g) + for _, dst := range acl.Destinations { + ag, ok := dst.Alias.(*AutoGroup) + if !ok || !ag.Is(AutoGroupSelf) { + continue + } + + ret = append(ret, Grant{ + Sources: acl.Sources, + Destinations: Aliases{dst.Alias}, + InternetProtocols: []ProtocolPort{{ + Protocol: acl.Protocol, + Ports: dst.Ports, + }}, + }) + } + } else { + // All-autogroup ACL: preserve policy order. + for _, dst := range acl.Destinations { + ret = append(ret, Grant{ + Sources: acl.Sources, + Destinations: Aliases{dst.Alias}, + InternetProtocols: []ProtocolPort{{ + Protocol: acl.Protocol, + Ports: dst.Ports, + }}, + }) + } } return ret