policy/v2: write an ACL's repeated destination once

An ACL that names a destination and port twice wrote the pair twice.
Grants keep their repeats, as on Tailscale SaaS.
This commit is contained in:
Kristoffer Dalby committed 2026-10-28 19:06:04 +00:00
1 parent 2e7c677841
commit 52f19dac0b
3 files changed
+34 -19

No files matched your search

@@ -216,25 +216,14 @@ var aclSkipReasons = map[string]string{
"acl-n01/venusaur/filter": allowAllForm,
"acl-n01/weedle/filter": allowAllForm,
"acl-dup-src-dst/beedrill/filter": aclDstDedupe,
"acl-dup-src-dst/bulbasaur/filter": aclDstDedupe,
"acl-dup-src-dst/charmander/filter": aclDstDedupe,
"acl-dup-src-dst/ivysaur/filter": aclDstDedupe,
"acl-dup-src-dst/kakuna/filter": aclDstDedupe,
"acl-dup-src-dst/squirtle/filter": aclDstDedupe,
"acl-dup-src-dst/venusaur/filter": aclDstDedupe,
"acl-dup-src-dst/weedle/filter": aclDstDedupe,
"acl-dxp-dup-ports/beedrill/filter": aclDstDedupe,
"acl-self-plus-dsts/kakuna/filter": srcAddressForm,
"acl-self-plus-dsts/beedrill/filter": "FILTER_ADDRESS_FORM+ACL_DST_DEDUPE: sources listed one by one, a repeated destination written once",
"acl-self-plus-dsts/beedrill/filter": srcAddressForm,
"acl-self-plus-dsts/bulbasaur/filter": "FILTER_ADDRESS_FORM+SELF_DST_REPEAT: sources listed one by one, the autogroup:self destination written once per source alias",
}
const (
routeHostBits = "ROUTE_HOST_BITS: SaaS refuses an autoApprovers route with host bits; headscale accepts it"
allowAllForm = "ALLOW_ALL_FORM: SaaS writes the allow-all default's sources as the tailnet ranges, headscale as \"*\""
aclDstDedupe = "ACL_DST_DEDUPE: SaaS writes a destination repeated within one ACL once, headscale once per mention"
srcAddressForm = "FILTER_ADDRESS_FORM: SaaS lists contiguous source addresses one by one, headscale as a range"
)
@@ -34,13 +34,6 @@ import (
// keyed <test_id>/<step>/<node>/<check>. A listed check that matches fails,
// so a fix must delete its entry.
var policySequenceSkipReasons = map[string]string{
"policy-generation/G0/srv/filter": aclDstDedupe,
"policy-generation/G1/srv/filter": aclDstDedupe,
"policy-generation/G2/srv/filter": aclDstDedupe,
"policy-generation/G3/srv/filter": aclDstDedupe,
"policy-generation/G4/srv/filter": aclDstDedupe,
"policy-generation/G5/srv/filter": aclDstDedupe,
"policy-generation/G0/rt/filter": viaRuleMerged,
"policy-generation/G1/rt/filter": viaRuleMerged,
"policy-generation/G2/rt/filter": viaRuleMerged,
+33
View File
@@ -1886,6 +1886,7 @@ type NodeAttrGrant struct {
// aclToGrants converts an [ACL] rule to one or more equivalent [Grant] rules.
func aclToGrants(acl ACL) []Grant {
acl.Destinations = uniqueDestinations(acl.Destinations)
ret := make([]Grant, 0, len(acl.Destinations))
// Check if the ACL has any non-autogroup destinations. If so,
@@ -1952,6 +1953,38 @@ func aclToGrants(acl ACL) []Grant {
return ret
}
// uniqueDestinations keeps each destination and port range of one ACL the
// first time it is named, as Tailscale SaaS writes a repeated one once.
// Grants keep their repeats, as there.
func uniqueDestinations(dsts []AliasWithPorts) []AliasWithPorts {
type named struct {
alias string
ports tailcfg.PortRange
}
seen := make(map[named]bool)
out := make([]AliasWithPorts, 0, len(dsts))
for _, dst := range dsts {
var ports []tailcfg.PortRange
for _, p := range dst.Ports {
k := named{dst.String(), p}
if !seen[k] {
seen[k] = true
ports = append(ports, p)
}
}
if len(ports) > 0 || len(dst.Ports) == 0 {
out = append(out, AliasWithPorts{Alias: dst.Alias, Ports: ports})
}
}
return out
}
// Policy represents a Tailscale Network Policy.
// TODO(kradalby):
// Add validation method checking: