mirror of
https://github.com/juanfont/headscale.git
synced 2025-11-21 01:59:05 -05:00
Fix exit node visibility issue - filter based on autogroup:internet permission
- Modified tailNode/tailNodes functions to accept exitRouteFilterFunc parameter - Added canUseExitRoutes helper to check for broad internet access permission - Added DestsContainsPrefixes method to matcher for checking prefix containment - Exit routes now only included in peer AllowedIPs when requesting node has internet access - Added comprehensive unit tests for both scenarios (with and without autogroup:internet) Fixes #2788 Co-authored-by: kradalby <98431+kradalby@users.noreply.github.com>
This commit is contained in:
@@ -7,12 +7,50 @@ 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"
|
||||
"tailscale.com/util/multierr"
|
||||
)
|
||||
|
||||
// canUseExitRoutes checks if a node can access exit routes (0.0.0.0/0 and ::/0)
|
||||
// based on ACL matchers. This specifically checks if the node has permission to
|
||||
// access the internet broadly, which is required to use exit nodes.
|
||||
//
|
||||
// Exit routes should only be visible when the ACL explicitly grants broad internet
|
||||
// access (e.g., via autogroup:internet), not just access to specific services.
|
||||
func canUseExitRoutes(node types.NodeView, matchers []matcher.Match) bool {
|
||||
src := node.IPs()
|
||||
|
||||
// Sample public internet IPs to test for broad internet access.
|
||||
// If the ACL grants access to these well-known public IPs, it's granting
|
||||
// internet access (e.g., via autogroup:internet).
|
||||
// Use popular public DNS servers as representatives of internet access.
|
||||
samplePublicIPs := []netip.Addr{
|
||||
netip.MustParseAddr("1.1.1.1"), // Cloudflare DNS
|
||||
netip.MustParseAddr("8.8.8.8"), // Google DNS
|
||||
netip.MustParseAddr("208.67.222.222"), // OpenDNS
|
||||
}
|
||||
|
||||
// Check if any matcher grants access to sample public IPs
|
||||
for _, matcher := range matchers {
|
||||
// Check if this node is in the source
|
||||
if !matcher.SrcsContainsIPs(src...) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if the destination includes public internet IPs.
|
||||
// This will be true for autogroup:internet (which resolves to the public internet)
|
||||
// but false for rules that only allow access to specific private IPs or services.
|
||||
if matcher.DestsContainsIP(samplePublicIPs...) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// MapResponseBuilder provides a fluent interface for building tailcfg.MapResponse.
|
||||
type MapResponseBuilder struct {
|
||||
resp *tailcfg.MapResponse
|
||||
@@ -81,6 +119,14 @@ func (b *MapResponseBuilder) WithSelfNode() *MapResponseBuilder {
|
||||
func(id types.NodeID) []netip.Prefix {
|
||||
return policy.ReduceRoutes(nv, b.mapper.state.GetNodePrimaryRoutes(id), matchers)
|
||||
},
|
||||
func(id types.NodeID) []netip.Prefix {
|
||||
// For self node, always include its own exit routes
|
||||
peerNode, ok := b.mapper.state.GetNodeByID(id)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return peerNode.ExitRoutes()
|
||||
},
|
||||
b.mapper.cfg)
|
||||
if err != nil {
|
||||
b.addError(err)
|
||||
@@ -256,6 +302,22 @@ func (b *MapResponseBuilder) buildTailPeers(peers views.Slice[types.NodeView]) (
|
||||
func(id types.NodeID) []netip.Prefix {
|
||||
return policy.ReduceRoutes(node, b.mapper.state.GetNodePrimaryRoutes(id), matchers)
|
||||
},
|
||||
func(id types.NodeID) []netip.Prefix {
|
||||
// For peer nodes, only include exit routes if the requesting node can use exit nodes
|
||||
peerNode, ok := b.mapper.state.GetNodeByID(id)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
exitRoutes := peerNode.ExitRoutes()
|
||||
if len(exitRoutes) == 0 {
|
||||
return nil
|
||||
}
|
||||
// Check if the requesting node has permission to use exit nodes
|
||||
if canUseExitRoutes(node, matchers) {
|
||||
return exitRoutes
|
||||
}
|
||||
return nil
|
||||
},
|
||||
b.mapper.cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
Reference in New Issue
Block a user