package policy import ( "net/netip" "slices" "github.com/juanfont/headscale/hscontrol/policy/matcher" "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/util" "github.com/rs/zerolog/log" "tailscale.com/types/views" ) // ReduceNodes returns the list of peers authorized to be accessed from a given node. func ReduceNodes( node types.NodeView, nodes views.Slice[types.NodeView], matchers []matcher.Match, ) views.Slice[types.NodeView] { var result []types.NodeView for _, peer := range nodes.All() { if peer.ID() == node.ID() { continue } if node.CanAccess(matchers, peer) || peer.CanAccess(matchers, node) { result = append(result, peer) } } return views.SliceOf(result) } // ReduceRoutes returns a reduced list of routes for a given node that it can access. func ReduceRoutes( node types.NodeView, routes []netip.Prefix, matchers []matcher.Match, ) []netip.Prefix { var result []netip.Prefix for _, route := range routes { if node.CanAccessRoute(matchers, route) { result = append(result, route) } } return result } // ApproveRoutesWithPolicy checks if the node can approve the announced routes // and returns the new list of approved routes. The [PolicyManager] is consulted // via [PolicyManager.NodeCanApproveRoute]. // The approved routes will include: // 1. ALL previously approved routes (regardless of whether they're still advertised) // 2. New routes from announcedRoutes that can be auto-approved by policy // This ensures that: // - Previously approved routes are ALWAYS preserved (auto-approval never removes routes) // - New routes can be auto-approved according to policy // - Routes can only be removed by explicit admin action (not by auto-approval). func ApproveRoutesWithPolicy(pm PolicyManager, nv types.NodeView, currentApproved, announcedRoutes []netip.Prefix) ([]netip.Prefix, bool) { if pm == nil { return currentApproved, false } // Start with ALL currently approved routes - we never remove approved routes newApproved := slices.Clone(currentApproved) // Then, check for new routes that can be auto-approved for _, route := range announcedRoutes { // Skip if already approved if slices.Contains(newApproved, route) { continue } // Check if this new route can be auto-approved by policy canApprove := pm.NodeCanApproveRoute(nv, route) if canApprove { newApproved = append(newApproved, route) } } // Sort and deduplicate slices.SortFunc(newApproved, netip.Prefix.Compare) newApproved = slices.Compact(newApproved) newApproved = slices.DeleteFunc(newApproved, func(route netip.Prefix) bool { return !route.IsValid() }) // Sort the current approved for comparison sortedCurrent := slices.Clone(currentApproved) slices.SortFunc(sortedCurrent, netip.Prefix.Compare) // Only update if the routes actually changed if !slices.Equal(sortedCurrent, newApproved) { // Log what changed var added, kept []netip.Prefix for _, route := range newApproved { if !slices.Contains(sortedCurrent, route) { added = append(added, route) } else { kept = append(kept, route) } } if len(added) > 0 { log.Debug(). EmbedObject(nv). Strs("routes.added", util.PrefixesToString(added)). Strs("routes.kept", util.PrefixesToString(kept)). Int("routes.total", len(newApproved)). Msg("Routes auto-approved by policy") } return newApproved, true } return newApproved, false }