From 1c0bb0338d20d3c91ad83685cb4530fd084d0a03 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sat, 1 Nov 2025 14:25:07 +0100 Subject: [PATCH] types: split SubnetRoutes and ExitRoutes There are situations where the subnet routes and exit nodes must be treated differently. This splits it so SubnetRoutes only returns routes that are not exit routes. It adds `IsExitRoutes` and `AllApprovedRoutes` for convenience. Signed-off-by: Kristoffer Dalby --- hscontrol/types/node.go | 50 ++++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/hscontrol/types/node.go b/hscontrol/types/node.go index 8cf40ced..bf42dcd1 100644 --- a/hscontrol/types/node.go +++ b/hscontrol/types/node.go @@ -269,11 +269,19 @@ func (node *Node) Prefixes() []netip.Prefix { // node has any exit routes enabled. // If none are enabled, it will return nil. func (node *Node) ExitRoutes() []netip.Prefix { - if slices.ContainsFunc(node.SubnetRoutes(), tsaddr.IsExitRoute) { - return tsaddr.ExitRoutes() + var routes []netip.Prefix + + for _, route := range node.AnnouncedRoutes() { + if tsaddr.IsExitRoute(route) && slices.Contains(node.ApprovedRoutes, route) { + routes = append(routes, route) + } } - return nil + return routes +} + +func (node *Node) IsExitNode() bool { + return len(node.ExitRoutes()) > 0 } func (node *Node) IPsAsString() []string { @@ -440,16 +448,22 @@ func (node *Node) AnnouncedRoutes() []netip.Prefix { return node.Hostinfo.RoutableIPs } -// SubnetRoutes returns the list of routes that the node announces and are approved. +// SubnetRoutes returns the list of routes (excluding exit routes) that the node +// announces and are approved. // -// IMPORTANT: This method is used for internal data structures and should NOT be used -// for the gRPC Proto conversion. For Proto, SubnetRoutes must be populated manually -// with PrimaryRoutes to ensure it includes only routes actively served by the node. -// See the comment in Proto() method and the implementation in grpcv1.go/nodesToProto. +// IMPORTANT: This method is used for internal data structures and should NOT be +// used for the gRPC Proto conversion. For Proto, SubnetRoutes must be populated +// manually with PrimaryRoutes to ensure it includes only routes actively served +// by the node. See the comment in Proto() method and the implementation in +// grpcv1.go/nodesToProto. func (node *Node) SubnetRoutes() []netip.Prefix { var routes []netip.Prefix for _, route := range node.AnnouncedRoutes() { + if tsaddr.IsExitRoute(route) { + continue + } + if slices.Contains(node.ApprovedRoutes, route) { routes = append(routes, route) } @@ -463,6 +477,11 @@ func (node *Node) IsSubnetRouter() bool { return len(node.SubnetRoutes()) > 0 } +// AllApprovedRoutes returns the combination of SubnetRoutes and ExitRoutes +func (node *Node) AllApprovedRoutes() []netip.Prefix { + return append(node.SubnetRoutes(), node.ExitRoutes()...) +} + func (node *Node) String() string { return node.Hostname } @@ -653,6 +672,7 @@ func (node Node) DebugString() string { fmt.Fprintf(&sb, "\tApprovedRoutes: %v\n", node.ApprovedRoutes) fmt.Fprintf(&sb, "\tAnnouncedRoutes: %v\n", node.AnnouncedRoutes()) fmt.Fprintf(&sb, "\tSubnetRoutes: %v\n", node.SubnetRoutes()) + fmt.Fprintf(&sb, "\tExitRoutes: %v\n", node.ExitRoutes()) sb.WriteString("\n") return sb.String() @@ -730,6 +750,13 @@ func (v NodeView) IsSubnetRouter() bool { return v.ж.IsSubnetRouter() } +func (v NodeView) AllApprovedRoutes() []netip.Prefix { + if !v.Valid() { + return nil + } + return v.ж.AllApprovedRoutes() +} + func (v NodeView) AppendToIPSet(build *netipx.IPSetBuilder) { if !v.Valid() { return @@ -808,6 +835,13 @@ func (v NodeView) ExitRoutes() []netip.Prefix { return v.ж.ExitRoutes() } +func (v NodeView) IsExitNode() bool { + if !v.Valid() { + return false + } + return v.ж.IsExitNode() +} + // RequestTags returns the ACL tags that the node is requesting. func (v NodeView) RequestTags() []string { if !v.Valid() || !v.Hostinfo().Valid() {