mirror of
https://github.com/juanfont/headscale.git
synced 2026-11-04 12:39:25 +09:00
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:
1 parent
235e6b62c7
commit
2c76879d0e
3 files changed
+184
-14
No files matched your search
+26
-14
@@ -24,6 +24,7 @@ import (
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/truncate"
|
||||
)
|
||||
|
||||
@@ -348,15 +349,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.
|
||||
@@ -605,15 +598,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1126,6 +1126,106 @@ func TestHasPolicyChangeSharedRouteInputs(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
|
||||
func TestBoundHostinfo(t *testing.T) {
|
||||
stock := func() *tailcfg.Hostinfo {
|
||||
return &tailcfg.Hostinfo{
|
||||
|
||||
Reference in new issue
Block a user