Filter exit routes through ACL policy to fix issue #2788

Exit nodes are now only visible to nodes that have permission to use them
according to ACL policy. Previously, exit routes (0.0.0.0/0 and ::/0) were
unconditionally added to the AllowedIPs field in the network map, making
exit nodes visible to all peers regardless of policy.

Changes:
- Modified buildTailPeers and WithSelfNode in builder.go to filter exit
  routes through policy.ReduceRoutes, same as primary routes
- Removed unconditional addition of exit routes in tail.go tailNode function
- Updated tail_test.go to reflect new behavior where exit routes are filtered

The fix ensures that exit nodes are only visible when a node has
autogroup:internet in their ACL destination rules.

Co-authored-by: kradalby <98431+kradalby@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-11-01 08:52:29 +00:00
parent a55cdc2636
commit e0107024e8
3 changed files with 35 additions and 5 deletions

View File

@@ -79,7 +79,23 @@ func (b *MapResponseBuilder) WithSelfNode() *MapResponseBuilder {
tailnode, err := tailNode(
nv, b.capVer, b.mapper.state,
func(id types.NodeID) []netip.Prefix {
return policy.ReduceRoutes(nv, b.mapper.state.GetNodePrimaryRoutes(id), matchers)
// 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.mapper.cfg)
if err != nil {
@@ -254,7 +270,23 @@ func (b *MapResponseBuilder) buildTailPeers(peers views.Slice[types.NodeView]) (
tailPeers, err := tailNodes(
changedViews, b.capVer, b.mapper.state,
func(id types.NodeID) []netip.Prefix {
return policy.ReduceRoutes(node, b.mapper.state.GetNodePrimaryRoutes(id), matchers)
// 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.mapper.cfg)
if err != nil {

View File

@@ -88,9 +88,9 @@ func tailNode(
}
tags = lo.Uniq(tags)
// Get filtered routes (includes both primary routes and exit routes if allowed by policy)
routes := primaryRouteFunc(node.ID())
allowed := append(addrs, routes...)
allowed = append(allowed, node.ExitRoutes()...)
tsaddr.SortPrefixes(allowed)
tNode := tailcfg.Node{

View File

@@ -137,10 +137,8 @@ func TestTailNode(t *testing.T) {
),
Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
AllowedIPs: []netip.Prefix{
tsaddr.AllIPv4(),
netip.MustParsePrefix("192.168.0.0/24"),
netip.MustParsePrefix("100.64.0.1/32"),
tsaddr.AllIPv6(),
},
PrimaryRoutes: []netip.Prefix{
netip.MustParsePrefix("192.168.0.0/24"),