mirror of
https://github.com/juanfont/headscale.git
synced 2026-11-01 03:04:07 +09:00
policy/v2: resolve an ACL's sources once, not once per destination
An ACL compiles to one grant per destination, and each resolved and merged the same sources again. A wide CI-style ACL took 3s and 674MB per compile; 88ms and 23MB now.
This commit is contained in:
1 parent
8e45958341
commit
526daf0841
3 files changed
+164
-10
No files matched your search
@@ -244,9 +244,10 @@ func (pol *Policy) compileGrants(
|
|||||||
}
|
}
|
||||||
|
|
||||||
compiled := make([]compiledGrant, 0, len(grants))
|
compiled := make([]compiledGrant, 0, len(grants))
|
||||||
|
srcs := sourceCache{}
|
||||||
|
|
||||||
for _, grant := range grants {
|
for _, grant := range grants {
|
||||||
cg, err := pol.compileOneGrant(grant, users, nodes)
|
cg, err := pol.compileOneGrant(grant, users, nodes, srcs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Trace().Err(err).Msg("compiling grant")
|
log.Trace().Err(err).Msg("compiling grant")
|
||||||
|
|
||||||
@@ -271,6 +272,7 @@ func (pol *Policy) compileOneGrant(
|
|||||||
grant Grant,
|
grant Grant,
|
||||||
users types.Users,
|
users types.Users,
|
||||||
nodes views.Slice[types.NodeView],
|
nodes views.Slice[types.NodeView],
|
||||||
|
srcs sourceCache,
|
||||||
) (*compiledGrant, error) {
|
) (*compiledGrant, error) {
|
||||||
// Via grants: resolve sources, store deferred data.
|
// Via grants: resolve sources, store deferred data.
|
||||||
if len(grant.Via) > 0 {
|
if len(grant.Via) > 0 {
|
||||||
@@ -291,9 +293,9 @@ func (pol *Policy) compileOneGrant(
|
|||||||
// Resolve sources per-alias, tracking non-wildcard sources
|
// Resolve sources per-alias, tracking non-wildcard sources
|
||||||
// separately so we can preserve their IPs alongside the
|
// separately so we can preserve their IPs alongside the
|
||||||
// wildcard CGNAT ranges (matching Tailscale SaaS behavior).
|
// wildcard CGNAT ranges (matching Tailscale SaaS behavior).
|
||||||
resolvedSrcs, nonWildcardSrcs, err := resolveSources(
|
sources := srcs.get(pol, grant.Sources, users, nodes)
|
||||||
pol, grant.Sources, users, nodes,
|
|
||||||
)
|
resolvedSrcs, err := sources.all, sources.err
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -309,10 +311,7 @@ func (pol *Policy) compileOneGrant(
|
|||||||
|
|
||||||
hasWildcard := sourcesHaveWildcard(grant.Sources)
|
hasWildcard := sourcesHaveWildcard(grant.Sources)
|
||||||
hasDangerAll := sourcesHaveDangerAll(grant.Sources)
|
hasDangerAll := sourcesHaveDangerAll(grant.Sources)
|
||||||
srcIPStrings := buildSrcIPStrings(
|
srcIPStrings := sources.strings(hasWildcard, hasDangerAll, nodes)
|
||||||
resolvedSrcs, nonWildcardSrcs,
|
|
||||||
hasWildcard, hasDangerAll, nodes,
|
|
||||||
)
|
|
||||||
|
|
||||||
cg := &compiledGrant{
|
cg := &compiledGrant{
|
||||||
srcIPStrings: srcIPStrings,
|
srcIPStrings: srcIPStrings,
|
||||||
@@ -348,6 +347,12 @@ func mergeResolvedSrcs(resolvedSrcs []ResolvedAddresses) (resolved, error) {
|
|||||||
var b netipx.IPSetBuilder
|
var b netipx.IPSetBuilder
|
||||||
|
|
||||||
for _, ips := range resolvedSrcs {
|
for _, ips := range resolvedSrcs {
|
||||||
|
if r, ok := ips.(resolved); ok {
|
||||||
|
b.AddSet(&r.ips)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
for _, pref := range ips.Prefixes() {
|
for _, pref := range ips.Prefixes() {
|
||||||
b.AddPrefix(pref)
|
b.AddPrefix(pref)
|
||||||
}
|
}
|
||||||
@@ -356,6 +361,66 @@ func mergeResolvedSrcs(resolvedSrcs []ResolvedAddresses) (resolved, error) {
|
|||||||
return newResolved(&b)
|
return newResolved(&b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sourceCache holds each source list's resolution for one compile: an ACL
|
||||||
|
// compiles to one grant per destination, all sharing its sources, and
|
||||||
|
// resolving and merging them is most of a wide ACL's compile.
|
||||||
|
type sourceCache map[sourcesKey]*grantSources
|
||||||
|
|
||||||
|
// sourcesKey names a source list by its backing array, which the grants
|
||||||
|
// of one ACL share.
|
||||||
|
type sourcesKey struct {
|
||||||
|
first *Alias
|
||||||
|
n int
|
||||||
|
}
|
||||||
|
|
||||||
|
// grantSources is a source list resolved per alias, all and without
|
||||||
|
// wildcards, and its SrcIPs once built.
|
||||||
|
type grantSources struct {
|
||||||
|
all, nonWild []ResolvedAddresses
|
||||||
|
err error
|
||||||
|
|
||||||
|
built bool
|
||||||
|
strs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// get resolves sources, once per source list; c may be nil.
|
||||||
|
func (c sourceCache) get(
|
||||||
|
pol *Policy,
|
||||||
|
sources Aliases,
|
||||||
|
users types.Users,
|
||||||
|
nodes views.Slice[types.NodeView],
|
||||||
|
) *grantSources {
|
||||||
|
key := sourcesKey{n: len(sources)}
|
||||||
|
if len(sources) > 0 {
|
||||||
|
key.first = &sources[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if s, ok := c[key]; ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &grantSources{}
|
||||||
|
s.all, s.nonWild, s.err = resolveSources(pol, sources, users, nodes)
|
||||||
|
|
||||||
|
if c != nil {
|
||||||
|
c[key] = s
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// strings returns the SrcIPs of the sources. Both flags follow from the
|
||||||
|
// sources, so every grant sharing them asks the same. The slice is shared
|
||||||
|
// and clipped, so a caller appending to it copies.
|
||||||
|
func (s *grantSources) strings(hasWildcard, hasDangerAll bool, nodes views.Slice[types.NodeView]) []string {
|
||||||
|
if !s.built {
|
||||||
|
s.strs = slices.Clip(buildSrcIPStrings(s.all, s.nonWild, hasWildcard, hasDangerAll, nodes))
|
||||||
|
s.built = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.strs
|
||||||
|
}
|
||||||
|
|
||||||
// compileOneViaGrant resolves sources for a via grant and stores the
|
// compileOneViaGrant resolves sources for a via grant and stores the
|
||||||
// deferred per-node data. The actual via-node matching and route
|
// deferred per-node data. The actual via-node matching and route
|
||||||
// intersection happens in [compileViaForNode].
|
// intersection happens in [compileViaForNode].
|
||||||
|
|||||||
@@ -3956,7 +3956,7 @@ func TestCompileViaGrant(t *testing.T) {
|
|||||||
nodeView := tt.node.View()
|
nodeView := tt.node.View()
|
||||||
nodesSlice := tt.nodes.ViewSlice()
|
nodesSlice := tt.nodes.ViewSlice()
|
||||||
|
|
||||||
cg, err := tt.pol.compileOneGrant(tt.grant, users, nodesSlice)
|
cg, err := tt.pol.compileOneGrant(tt.grant, users, nodesSlice, nil)
|
||||||
|
|
||||||
if tt.wantErr != nil {
|
if tt.wantErr != nil {
|
||||||
require.ErrorIs(t, err, tt.wantErr)
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
@@ -4129,7 +4129,7 @@ func TestCompileGrantWithAutogroupSelf_GrantPaths(t *testing.T) {
|
|||||||
userIdx := buildUserNodeIndex(nodesSlice)
|
userIdx := buildUserNodeIndex(nodesSlice)
|
||||||
|
|
||||||
cg, err := tt.pol.compileOneGrant(
|
cg, err := tt.pol.compileOneGrant(
|
||||||
tt.grant, users, nodesSlice,
|
tt.grant, users, nodesSlice, nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
if tt.wantErr != nil {
|
if tt.wantErr != nil {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package v2
|
package v2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
|
"math/rand/v2"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"slices"
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -4202,3 +4204,90 @@ func BenchmarkNodeCapMapsConstruction(b *testing.B) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BenchmarkCompileWideACL compiles a CI-shaped policy: one rule per CI tag
|
||||||
|
// to its own service, and one wide rule from every CI tag to the shared
|
||||||
|
// services, which compiles to one grant per destination. new compiles from
|
||||||
|
// scratch; set-nodes recompiles after a batch of tagged joins.
|
||||||
|
func BenchmarkCompileWideACL(b *testing.B) {
|
||||||
|
const (
|
||||||
|
ciTags = 300
|
||||||
|
common = 100
|
||||||
|
ci = 5000
|
||||||
|
joins = 50
|
||||||
|
)
|
||||||
|
|
||||||
|
r := rand.New(rand.NewPCG(1, 2)) //nolint:gosec // reproducible benchmark input
|
||||||
|
|
||||||
|
owners := map[string][]string{}
|
||||||
|
tag := func(kind string, i int) string {
|
||||||
|
t := fmt.Sprintf("tag:%s%d", kind, i)
|
||||||
|
owners[t] = []string{}
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
acls := make([]map[string]any, 0, ciTags+1)
|
||||||
|
wide := make([]string, 0, ciTags)
|
||||||
|
dsts := make([]string, 0, common)
|
||||||
|
|
||||||
|
for i := range ciTags {
|
||||||
|
acls = append(acls, map[string]any{"action": "accept", "src": []string{tag("ci-app", i)}, "dst": []string{tag("svc-app", i) + ":80"}})
|
||||||
|
wide = append(wide, tag("ci-app", i))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range common {
|
||||||
|
dsts = append(dsts, tag("svc-common", i)+":80")
|
||||||
|
}
|
||||||
|
|
||||||
|
acls = append(acls, map[string]any{"action": "accept", "src": wide, "dst": dsts})
|
||||||
|
pol := must(json.Marshal(map[string]any{"tagOwners": owners, "acls": acls}))
|
||||||
|
|
||||||
|
node := func(i int, t string) *types.Node {
|
||||||
|
v4 := netip.AddrFrom4([4]byte{100, 64 + byte(i>>16), byte(i >> 8), byte(i)}) //nolint:gosec
|
||||||
|
v6 := netip.AddrFrom16([16]byte{0xfd, 0x7a, 0x11, 0x5c, 0xa1, 0xe0, 13: byte(i >> 16), 14: byte(i >> 8), 15: byte(i)}) //nolint:gosec
|
||||||
|
|
||||||
|
return &types.Node{ID: types.NodeID(i + 1), Hostname: fmt.Sprintf("n%d", i), IPv4: &v4, IPv6: &v6, Tags: []string{t}} //nolint:gosec
|
||||||
|
}
|
||||||
|
|
||||||
|
before := make(types.Nodes, 0, ciTags+common+ci)
|
||||||
|
for i := range ciTags {
|
||||||
|
before = append(before, node(len(before), fmt.Sprintf("tag:svc-app%d", i)))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range common {
|
||||||
|
before = append(before, node(len(before), fmt.Sprintf("tag:svc-common%d", i)))
|
||||||
|
}
|
||||||
|
|
||||||
|
for range ci {
|
||||||
|
before = append(before, node(len(before), fmt.Sprintf("tag:ci-app%d", r.IntN(ciTags))))
|
||||||
|
}
|
||||||
|
|
||||||
|
after := slices.Clone(before)
|
||||||
|
for range joins {
|
||||||
|
after = append(after, node(len(after), fmt.Sprintf("tag:ci-app%d", r.IntN(ciTags))))
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Run("new", func(b *testing.B) {
|
||||||
|
for b.Loop() {
|
||||||
|
_, err := NewPolicyManager(pol, nil, after.ViewSlice())
|
||||||
|
require.NoError(b, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("set-nodes", func(b *testing.B) {
|
||||||
|
pm, err := NewPolicyManager(pol, nil, before.ViewSlice())
|
||||||
|
require.NoError(b, err)
|
||||||
|
|
||||||
|
sets := []types.Nodes{after, before}
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
for b.Loop() {
|
||||||
|
changed, err := pm.SetNodes(sets[i%2].ViewSlice())
|
||||||
|
require.NoError(b, err)
|
||||||
|
require.True(b, changed)
|
||||||
|
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in new issue
Block a user