From e0107024e894a4fef74076fa73729fe013018b9f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 08:52:29 +0000 Subject: [PATCH] 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> --- hscontrol/mapper/builder.go | 36 +++++++++++++++++++++++++++++++++-- hscontrol/mapper/tail.go | 2 +- hscontrol/mapper/tail_test.go | 2 -- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/hscontrol/mapper/builder.go b/hscontrol/mapper/builder.go index b85eb908..69171f7a 100644 --- a/hscontrol/mapper/builder.go +++ b/hscontrol/mapper/builder.go @@ -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 { diff --git a/hscontrol/mapper/tail.go b/hscontrol/mapper/tail.go index 3a518d94..ab57e41b 100644 --- a/hscontrol/mapper/tail.go +++ b/hscontrol/mapper/tail.go @@ -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{ diff --git a/hscontrol/mapper/tail_test.go b/hscontrol/mapper/tail_test.go index ac96028e..8a142d16 100644 --- a/hscontrol/mapper/tail_test.go +++ b/hscontrol/mapper/tail_test.go @@ -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"),