From 15c1cfd77859f1435e3a582903a139eec7eb30ef Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 26 Mar 2026 18:21:16 +0000 Subject: [PATCH] types: include ExitRoutes in HasNetworkChanges When exit routes are approved, SubnetRoutes remains empty because exit routes (0.0.0.0/0, ::/0) are classified separately. Without checking ExitRoutes, the PolicyManager cache is not invalidated on exit route approval, causing stale filter rules that lack via grant entries for autogroup:internet destinations. Updates #2180 --- hscontrol/types/node.go | 4 ++++ hscontrol/types/node_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/hscontrol/types/node.go b/hscontrol/types/node.go index 91feb7d4..f26c191f 100644 --- a/hscontrol/types/node.go +++ b/hscontrol/types/node.go @@ -1030,6 +1030,10 @@ func (nv NodeView) HasNetworkChanges(other NodeView) bool { return true } + if !slices.Equal(nv.ExitRoutes(), other.ExitRoutes()) { + return true + } + return false } diff --git a/hscontrol/types/node_test.go b/hscontrol/types/node_test.go index 40634525..567f21fb 100644 --- a/hscontrol/types/node_test.go +++ b/hscontrol/types/node_test.go @@ -958,6 +958,37 @@ func TestHasNetworkChanges(t *testing.T) { }, changed: false, }, + { + name: "ExitRoutes approved", + old: &Node{ + ID: 1, + IPv4: mustIPPtr("100.64.0.1"), + Hostinfo: &tailcfg.Hostinfo{RoutableIPs: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0")}}, + }, + new: &Node{ + ID: 1, + IPv4: mustIPPtr("100.64.0.1"), + Hostinfo: &tailcfg.Hostinfo{RoutableIPs: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0")}}, + ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0")}, + }, + changed: true, + }, + { + name: "ExitRoutes unchanged when SubnetRoutes change", + old: &Node{ + ID: 1, + IPv4: mustIPPtr("100.64.0.1"), + Hostinfo: &tailcfg.Hostinfo{RoutableIPs: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0"), netip.MustParsePrefix("10.0.0.0/24")}}, + ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0")}, + }, + new: &Node{ + ID: 1, + IPv4: mustIPPtr("100.64.0.1"), + Hostinfo: &tailcfg.Hostinfo{RoutableIPs: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0"), netip.MustParsePrefix("10.0.0.0/24")}}, + ApprovedRoutes: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0"), netip.MustParsePrefix("10.0.0.0/24")}, + }, + changed: true, + }, } for _, tt := range tests {