From 5741dba87d5ba873866627dc959d607c47bd9a21 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sat, 31 Oct 2026 14:08:12 +0000 Subject: [PATCH] policy/v2: sort approved routes in wildcard sources They followed NodeStore map order, so the same policy read as changed. --- hscontrol/policy/v2/policy_test.go | 21 +++++++++++++++++++++ hscontrol/policy/v2/types.go | 4 ++++ 2 files changed, 25 insertions(+) diff --git a/hscontrol/policy/v2/policy_test.go b/hscontrol/policy/v2/policy_test.go index f1fd53f2c..06d8ab5c6 100644 --- a/hscontrol/policy/v2/policy_test.go +++ b/hscontrol/policy/v2/policy_test.go @@ -16,6 +16,7 @@ import ( "github.com/juanfont/headscale/hscontrol/types" "github.com/puzpuzpuz/xsync/v4" "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go4.org/netipx" "pgregory.net/rapid" @@ -25,6 +26,26 @@ import ( "tailscale.com/types/views" ) +// TestApprovedSubnetRoutesIgnoreNodeOrder: the routes land in every wildcard +// rule, and the filter hash decides whether a policy write changed anything. +func TestApprovedSubnetRoutesIgnoreNodeOrder(t *testing.T) { + t.Parallel() + + router := func(name, route string) *types.Node { + n := node(name, "100.64.0.1", "fd7a:115c:a1e0::1", types.User{Name: "owner"}) + r := netip.MustParsePrefix(route) + n.Hostinfo = &tailcfg.Hostinfo{RoutableIPs: []netip.Prefix{r}} + n.ApprovedRoutes = []netip.Prefix{r} + + return n + } + a, b := router("a", "10.2.0.0/16"), router("b", "10.1.0.0/16") + + want := []string{"10.1.0.0/16", "10.2.0.0/16"} + assert.Equal(t, want, approvedSubnetRoutes(types.Nodes{a, b}.ViewSlice())) + assert.Equal(t, want, approvedSubnetRoutes(types.Nodes{b, a}.ViewSlice())) +} + func node(name, ipv4, ipv6 string, user types.User) *types.Node { return &types.Node{ ID: 0, diff --git a/hscontrol/policy/v2/types.go b/hscontrol/policy/v2/types.go index aa342287e..28deffcb1 100644 --- a/hscontrol/policy/v2/types.go +++ b/hscontrol/policy/v2/types.go @@ -386,6 +386,10 @@ func approvedSubnetRoutes(nodes views.Slice[types.NodeView]) []string { } } + // Nodes come in no fixed order. Without this, the same policy compiles to a + // new filter hash and reads as changed. + slices.Sort(routes) + return routes }