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
This commit is contained in:
Kristoffer Dalby
2026-03-18 15:12:18 +00:00
parent f95b254ea9
commit dda35847b0

View File

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