Refactor: Extract route filtering logic into helper function

Addressed code review feedback:
- Extracted duplicated exit route filtering logic into buildRouteFilterFunc
- Reused exitNode lookup in integration test to avoid duplication
- Added missing matcher package import

This improves code maintainability while preserving the same functionality.

Co-authored-by: kradalby <98431+kradalby@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-11-01 09:05:37 +00:00
parent 4fa1f4baa3
commit 1f4b645d5b
2 changed files with 34 additions and 48 deletions

View File

@@ -7,6 +7,7 @@ import (
"time"
"github.com/juanfont/headscale/hscontrol/policy"
"github.com/juanfont/headscale/hscontrol/policy/matcher"
"github.com/juanfont/headscale/hscontrol/types"
"tailscale.com/tailcfg"
"tailscale.com/types/views"
@@ -67,6 +68,33 @@ func (b *MapResponseBuilder) WithCapabilityVersion(capVer tailcfg.CapabilityVers
return b
}
// buildRouteFilterFunc creates a route filter function that includes both primary and exit routes.
// It filters routes based on ACL policy to ensure only authorized routes are visible.
func (b *MapResponseBuilder) buildRouteFilterFunc(
viewingNode types.NodeView,
matchers []matcher.Match,
) routeFilterFunc {
return func(id types.NodeID) []netip.Prefix {
// Get the peer node to check for exit routes
peer, ok := b.mapper.state.GetNodeByID(id)
if !ok {
return nil
}
// Start with primary routes (subnet routes, but not exit routes)
routes := policy.ReduceRoutes(viewingNode, b.mapper.state.GetNodePrimaryRoutes(id), matchers)
// Also filter exit routes through policy
// Only add exit routes if the viewing node has permission to use them
if exitRoutes := peer.ExitRoutes(); len(exitRoutes) > 0 {
filteredExitRoutes := policy.ReduceRoutes(viewingNode, exitRoutes, matchers)
routes = append(routes, filteredExitRoutes...)
}
return routes
}
}
// WithSelfNode adds the requesting node to the response.
func (b *MapResponseBuilder) WithSelfNode() *MapResponseBuilder {
nv, ok := b.mapper.state.GetNodeByID(b.nodeID)
@@ -78,25 +106,7 @@ func (b *MapResponseBuilder) WithSelfNode() *MapResponseBuilder {
_, matchers := b.mapper.state.Filter()
tailnode, err := tailNode(
nv, b.capVer, b.mapper.state,
func(id types.NodeID) []netip.Prefix {
// Get the peer node to check for exit routes
peer, ok := b.mapper.state.GetNodeByID(id)
if !ok {
return nil
}
// Start with primary routes (subnet routes, but not exit routes)
routes := policy.ReduceRoutes(nv, b.mapper.state.GetNodePrimaryRoutes(id), matchers)
// Also filter exit routes through policy
// Only add exit routes if the viewing node (self in this case) has permission to use them
if exitRoutes := peer.ExitRoutes(); len(exitRoutes) > 0 {
filteredExitRoutes := policy.ReduceRoutes(nv, exitRoutes, matchers)
routes = append(routes, filteredExitRoutes...)
}
return routes
},
b.buildRouteFilterFunc(nv, matchers),
b.mapper.cfg)
if err != nil {
b.addError(err)
@@ -269,25 +279,7 @@ func (b *MapResponseBuilder) buildTailPeers(peers views.Slice[types.NodeView]) (
tailPeers, err := tailNodes(
changedViews, b.capVer, b.mapper.state,
func(id types.NodeID) []netip.Prefix {
// Get the peer node to check for exit routes
peer, ok := b.mapper.state.GetNodeByID(id)
if !ok {
return nil
}
// Start with primary routes (subnet routes, but not exit routes)
routes := policy.ReduceRoutes(node, b.mapper.state.GetNodePrimaryRoutes(id), matchers)
// Also filter exit routes through policy
// Only add exit routes if the viewing node has permission to use them
if exitRoutes := peer.ExitRoutes(); len(exitRoutes) > 0 {
filteredExitRoutes := policy.ReduceRoutes(node, exitRoutes, matchers)
routes = append(routes, filteredExitRoutes...)
}
return routes
},
b.buildRouteFilterFunc(node, matchers),
b.mapper.cfg)
if err != nil {
return nil, err

View File

@@ -3126,14 +3126,16 @@ require.NoErrorf(t, err, "failed to advertise exit node: %s", err)
// Wait for the exit node to be registered
var nodes []*v1.Node
var exitNode *v1.Node
exitStatus := exitClient.MustStatus()
assert.EventuallyWithT(t, func(c *assert.CollectT) {
nodes, err = headscale.ListNodes()
assert.NoError(c, err)
assert.Len(c, nodes, 3)
// Find the exit node
var exitNode *v1.Node
exitStatus := exitClient.MustStatus()
exitNode = nil
for _, node := range nodes {
if node.GetName() == exitStatus.Self.HostName {
exitNode = node
@@ -3148,14 +3150,6 @@ assert.Len(c, exitNode.GetAvailableRoutes(), 2, "exit node should advertise 2 ro
}, 10*time.Second, 500*time.Millisecond, "waiting for exit node advertisement")
// Approve the exit routes
var exitNode *v1.Node
exitStatus := exitClient.MustStatus()
for _, node := range nodes {
if node.GetName() == exitStatus.Self.HostName {
exitNode = node
break
}
}
require.NotNil(t, exitNode, "exit node not found after advertisement")
_, err = headscale.ApproveRoutes(exitNode.GetId(), []netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6()})