types: derive subnet and exit routes in one pass

Emit each approved route once, in announcement order.
Skip derivation when no routes are approved.
This commit is contained in:
Kristoffer Dalby committed 2026-10-27 21:32:33 +00:00
1 parent 3c698721a2
commit 6ce2a4c9f7
3 files changed
+185 -14

No files matched your search

+26 -14
View File
@@ -22,6 +22,7 @@ import (
"tailscale.com/types/key"
"tailscale.com/types/views"
"tailscale.com/util/dnsname"
"tailscale.com/util/set"
)
var (
@@ -341,15 +342,7 @@ func (node *Node) Prefixes() []netip.Prefix {
// viewer uses an exit node; excluded from [Node.CanAccessRoute] which only
// handles non-exit routing.
func (node *Node) ExitRoutes() []netip.Prefix {
var routes []netip.Prefix
for _, route := range node.AnnouncedRoutes() {
if tsaddr.IsExitRoute(route) && slices.Contains(node.ApprovedRoutes, route) {
routes = append(routes, route)
}
}
return routes
return node.approvedRoutes(true)
}
// IsExitNode reports whether the node has any approved exit routes.
@@ -579,15 +572,34 @@ func (node *Node) AnnouncedRoutes() []netip.Prefix {
// responses that need the routes actively served by the node must
// populate SubnetRoutes from the primary-route election instead.
func (node *Node) SubnetRoutes() []netip.Prefix {
return node.approvedRoutes(false)
}
func (node *Node) approvedRoutes(exit bool) []netip.Prefix {
// Only the requested kind goes in the set, so finding the exit routes of
// a large subnet router stays cheap.
var approved set.Set[netip.Prefix]
for _, route := range node.ApprovedRoutes {
if tsaddr.IsExitRoute(route) == exit {
if approved == nil {
approved = make(set.Set[netip.Prefix])
}
approved.Add(route)
}
}
if len(approved) == 0 {
return nil
}
var routes []netip.Prefix
for _, route := range node.AnnouncedRoutes() {
if tsaddr.IsExitRoute(route) {
continue
}
if slices.Contains(node.ApprovedRoutes, route) {
if approved.Contains(route) {
routes = append(routes, route)
approved.Delete(route)
}
}
+58
View File
@@ -3,6 +3,7 @@ package types
import (
"fmt"
"net/netip"
"slices"
"testing"
"github.com/juanfont/headscale/hscontrol/policy/matcher"
@@ -71,3 +72,60 @@ func BenchmarkNodeViewCanAccess(b *testing.B) {
}
})
}
func BenchmarkNodeDerivedRoutes(b *testing.B) {
for _, count := range []int{1024, 8192} {
announced := make([]netip.Prefix, count, count+2)
ip := netip.MustParseAddr("10.0.0.0")
for i := range announced {
announced[i] = netip.PrefixFrom(ip, 32)
ip = ip.Next()
}
announced = append(announced,
netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0"))
for _, approved := range []bool{false, true} {
name := fmt.Sprintf("approved=%t/routes=%d", approved, count)
b.Run(name, func(b *testing.B) {
n := Node{Hostinfo: &tailcfg.Hostinfo{RoutableIPs: announced}}
if approved {
n.ApprovedRoutes = slices.Clone(announced)
}
var subnets, exits []netip.Prefix
b.ReportAllocs()
for b.Loop() {
subnets = n.SubnetRoutes()
exits = n.ExitRoutes()
}
if approved {
if len(subnets) != count || len(exits) != 2 {
b.Fatalf("unexpected derived routes: %d subnets, %d exits",
len(subnets), len(exits))
}
} else if len(subnets) != 0 || len(exits) != 0 {
b.Fatal("unapproved routes must not be served")
}
})
}
}
}
func BenchmarkNodeIsExitNodeSubnetRouter(b *testing.B) {
for _, count := range []int{4096, 16384} {
b.Run(fmt.Sprintf("routes=%d", count), func(b *testing.B) {
n := subnetRouter(count)
b.ReportAllocs()
for b.Loop() {
n.IsExitNode()
}
})
}
}
+101
View File
@@ -3,6 +3,7 @@ package types
import (
"fmt"
"net/netip"
"slices"
"strings"
"testing"
"time"
@@ -1121,3 +1122,103 @@ func TestHasPolicyChangeSharedRouteInputs(t *testing.T) {
require.False(t, changed)
require.Zero(t, allocs, "unchanged route inputs should not derive route lists")
}
func TestNodeDerivedRoutes(t *testing.T) {
a := netip.MustParsePrefix("10.0.0.0/24")
b := netip.MustParsePrefix("10.1.0.0/24")
x := netip.MustParsePrefix("10.2.0.0/24")
exit4 := netip.MustParsePrefix("0.0.0.0/0")
exit6 := netip.MustParsePrefix("::/0")
for _, tt := range []struct {
name string
announced, approved, subnets, exits []netip.Prefix
}{
{
name: "subnets follow announcement order without duplicates",
announced: []netip.Prefix{b, a, x, b, a, b},
approved: []netip.Prefix{a, b, b},
subnets: []netip.Prefix{b, a},
},
{
name: "exit routes follow announcement order without duplicates",
announced: []netip.Prefix{exit6, exit4, exit6, exit4, a},
approved: []netip.Prefix{exit4, exit6, exit4},
exits: []netip.Prefix{exit6, exit4},
},
{
name: "mixed routes",
announced: []netip.Prefix{b, exit6, x, exit6, a, b, exit4},
approved: []netip.Prefix{exit4, a, exit6, b},
subnets: []netip.Prefix{b, a},
exits: []netip.Prefix{exit6, exit4},
},
{name: "nothing approved", announced: []netip.Prefix{b, exit6, a, exit4}},
{
name: "empty approvals",
announced: []netip.Prefix{a, exit4},
approved: []netip.Prefix{},
},
{name: "nothing announced", approved: []netip.Prefix{a, exit4}},
{
name: "unannounced approvals",
announced: []netip.Prefix{a},
approved: []netip.Prefix{b, exit4},
},
{
name: "exit families differ",
announced: []netip.Prefix{exit4},
approved: []netip.Prefix{exit6},
},
} {
t.Run(tt.name, func(t *testing.T) {
announced := slices.Clone(tt.announced)
approved := slices.Clone(tt.approved)
n := Node{
Hostinfo: &tailcfg.Hostinfo{RoutableIPs: tt.announced},
ApprovedRoutes: tt.approved,
}
require.Equal(t, tt.subnets, n.SubnetRoutes())
require.Equal(t, tt.exits, n.ExitRoutes())
require.Equal(t, announced, n.AnnouncedRoutes(),
"announcements must remain immutable")
require.Equal(t, Prefixes(approved), n.ApprovedRoutes,
"approvals must remain immutable")
})
}
n := Node{ApprovedRoutes: []netip.Prefix{a, exit4}}
require.Nil(t, n.SubnetRoutes())
require.Nil(t, n.ExitRoutes())
}
func subnetRouter(count int) Node {
routes := make([]netip.Prefix, count)
ip := netip.MustParseAddr("10.0.0.0")
for i := range routes {
routes[i] = netip.PrefixFrom(ip, 32)
ip = ip.Next()
}
return Node{
Hostinfo: &tailcfg.Hostinfo{RoutableIPs: routes},
ApprovedRoutes: routes,
}
}
// IsExitNode runs per viewer and peer pair; a subnet router must answer it
// without collecting its subnet routes.
func TestIsExitNodeSubnetRouterAllocs(t *testing.T) {
n := subnetRouter(4096)
var exit bool
allocs := testing.AllocsPerRun(10, func() {
exit = n.IsExitNode()
})
require.False(t, exit)
require.Zero(t, allocs)
}