mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			177 lines
		
	
	
		
			4.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			177 lines
		
	
	
		
			4.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 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/samber/lo"
 | |
| 	"tailscale.com/net/tsaddr"
 | |
| 	"tailscale.com/tailcfg"
 | |
| 	"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
 | |
| }
 | |
| 
 | |
| // BuildPeerMap builds a map of all peers that can be accessed by each node.
 | |
| func BuildPeerMap(
 | |
| 	nodes views.Slice[types.NodeView],
 | |
| 	matchers []matcher.Match,
 | |
| ) map[types.NodeID][]types.NodeView {
 | |
| 	ret := make(map[types.NodeID][]types.NodeView, nodes.Len())
 | |
| 
 | |
| 	// Build the map of all peers according to the matchers.
 | |
| 	// Compared to ReduceNodes, which builds the list per node, we end up with doing
 | |
| 	// the full work for every node (On^2), while this will reduce the list as we see
 | |
| 	// relationships while building the map, making it O(n^2/2) in the end, but with less work per node.
 | |
| 	for i := range nodes.Len() {
 | |
| 		for j := i + 1; j < nodes.Len(); j++ {
 | |
| 			if nodes.At(i).ID() == nodes.At(j).ID() {
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			if nodes.At(i).CanAccess(matchers, nodes.At(j)) || nodes.At(j).CanAccess(matchers, nodes.At(i)) {
 | |
| 				ret[nodes.At(i).ID()] = append(ret[nodes.At(i).ID()], nodes.At(j))
 | |
| 				ret[nodes.At(j).ID()] = append(ret[nodes.At(j).ID()], nodes.At(i))
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return ret
 | |
| }
 | |
| 
 | |
| // ReduceFilterRules takes a node and a set of rules and removes all rules and destinations
 | |
| // that are not relevant to that particular node.
 | |
| func ReduceFilterRules(node types.NodeView, rules []tailcfg.FilterRule) []tailcfg.FilterRule {
 | |
| 	ret := []tailcfg.FilterRule{}
 | |
| 
 | |
| 	for _, rule := range rules {
 | |
| 		// record if the rule is actually relevant for the given node.
 | |
| 		var dests []tailcfg.NetPortRange
 | |
| 	DEST_LOOP:
 | |
| 		for _, dest := range rule.DstPorts {
 | |
| 			expanded, err := util.ParseIPSet(dest.IP, nil)
 | |
| 			// Fail closed, if we can't parse it, then we should not allow
 | |
| 			// access.
 | |
| 			if err != nil {
 | |
| 				continue DEST_LOOP
 | |
| 			}
 | |
| 
 | |
| 			if node.InIPSet(expanded) {
 | |
| 				dests = append(dests, dest)
 | |
| 				continue DEST_LOOP
 | |
| 			}
 | |
| 
 | |
| 			// If the node exposes routes, ensure they are note removed
 | |
| 			// when the filters are reduced.
 | |
| 			if node.Hostinfo().Valid() {
 | |
| 				routableIPs := node.Hostinfo().RoutableIPs()
 | |
| 				if routableIPs.Len() > 0 {
 | |
| 					for _, routableIP := range routableIPs.All() {
 | |
| 						if expanded.OverlapsPrefix(routableIP) {
 | |
| 							dests = append(dests, dest)
 | |
| 							continue DEST_LOOP
 | |
| 						}
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			// Also check approved subnet routes - nodes should have access
 | |
| 			// to subnets they're approved to route traffic for.
 | |
| 			subnetRoutes := node.SubnetRoutes()
 | |
| 
 | |
| 			for _, subnetRoute := range subnetRoutes {
 | |
| 				if expanded.OverlapsPrefix(subnetRoute) {
 | |
| 					dests = append(dests, dest)
 | |
| 					continue DEST_LOOP
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if len(dests) > 0 {
 | |
| 			ret = append(ret, tailcfg.FilterRule{
 | |
| 				SrcIPs:   rule.SrcIPs,
 | |
| 				DstPorts: dests,
 | |
| 				IPProto:  rule.IPProto,
 | |
| 			})
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return ret
 | |
| }
 | |
| 
 | |
| // AutoApproveRoutes approves any route that can be autoapproved from
 | |
| // the nodes perspective according to the given policy.
 | |
| // It reports true if any routes were approved.
 | |
| // Note: This function now takes a pointer to the actual node to modify ApprovedRoutes.
 | |
| func AutoApproveRoutes(pm PolicyManager, node *types.Node) bool {
 | |
| 	if pm == nil {
 | |
| 		return false
 | |
| 	}
 | |
| 	nodeView := node.View()
 | |
| 	var newApproved []netip.Prefix
 | |
| 	for _, route := range nodeView.AnnouncedRoutes() {
 | |
| 		if pm.NodeCanApproveRoute(nodeView, route) {
 | |
| 			newApproved = append(newApproved, route)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Only modify ApprovedRoutes if we have new routes to approve.
 | |
| 	// This prevents clearing existing approved routes when nodes
 | |
| 	// temporarily don't have announced routes during policy changes.
 | |
| 	if len(newApproved) > 0 {
 | |
| 		combined := append(newApproved, node.ApprovedRoutes...)
 | |
| 		tsaddr.SortPrefixes(combined)
 | |
| 		combined = slices.Compact(combined)
 | |
| 		combined = lo.Filter(combined, func(route netip.Prefix, index int) bool {
 | |
| 			return route.IsValid()
 | |
| 		})
 | |
| 
 | |
| 		// Only update if the routes actually changed
 | |
| 		if !slices.Equal(node.ApprovedRoutes, combined) {
 | |
| 			node.ApprovedRoutes = combined
 | |
| 			return true
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return false
 | |
| }
 |