1
0
mirror of https://github.com/juanfont/headscale.git synced 2025-08-01 13:46:49 +02:00

split out exit nodes from primary manager

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
Kristoffer Dalby 2025-03-19 08:34:28 +01:00
parent 8e3201b78d
commit c047278109
No known key found for this signature in database
7 changed files with 78 additions and 33 deletions

View File

@ -165,9 +165,10 @@ func Test_fullMapResponse(t *testing.T) {
),
Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
AllowedIPs: []netip.Prefix{
netip.MustParsePrefix("100.64.0.1/32"),
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"),

View File

@ -8,6 +8,7 @@ import (
"github.com/juanfont/headscale/hscontrol/routes"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/samber/lo"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
)
@ -80,6 +81,10 @@ func tailNode(
}
tags = lo.Uniq(append(tags, node.ForcedTags...))
allowed := append(node.Prefixes(), primary.PrimaryRoutes(node.ID)...)
allowed = append(allowed, node.ExitRoutes()...)
tsaddr.SortPrefixes(allowed)
tNode := tailcfg.Node{
ID: tailcfg.NodeID(node.ID), // this is the actual ID
StableID: node.ID.StableID(),

View File

@ -137,9 +137,10 @@ func TestTailNode(t *testing.T) {
),
Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
AllowedIPs: []netip.Prefix{
netip.MustParsePrefix("100.64.0.1/32"),
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"),

View File

@ -102,12 +102,16 @@ func (pr *PrimaryRoutes) updatePrimaryLocked() bool {
return changed
}
func (pr *PrimaryRoutes) SetRoutes(node types.NodeID, prefix ...netip.Prefix) bool {
// SetRoutes sets the routes for a given Node ID and recalculates the primary routes
// of the headscale.
// It returns true if there was a change in primary routes.
// All exit routes are ignored as they are not used in primary route context.
func (pr *PrimaryRoutes) SetRoutes(node types.NodeID, prefixes ...netip.Prefix) bool {
pr.mu.Lock()
defer pr.mu.Unlock()
// If no routes are being set, remove the node from the routes map.
if len(prefix) == 0 {
if len(prefixes) == 0 {
if _, ok := pr.routes[node]; ok {
delete(pr.routes, node)
return pr.updatePrimaryLocked()

View File

@ -366,7 +366,6 @@ func TestPrimaryRoutes(t *testing.T) {
},
expectedPrimaries: map[netip.Prefix]types.NodeID{
mp("192.168.1.0/24"): 1,
mp("0.0.0.0/0"): 1,
},
expectedIsPrimary: map[types.NodeID]bool{
1: true,
@ -389,6 +388,26 @@ func TestPrimaryRoutes(t *testing.T) {
expectedRoutes: nil,
expectedChange: false,
},
{
name: "exit-nodes",
operations: func(pr *PrimaryRoutes) bool {
pr.SetRoutes(1, mp("10.0.0.0/16"), mp("0.0.0.0/0"), mp("::/0"))
pr.SetRoutes(3, mp("0.0.0.0/0"), mp("::/0"))
return pr.SetRoutes(2, mp("0.0.0.0/0"), mp("::/0"))
},
expectedRoutes: map[types.NodeID]set.Set[netip.Prefix]{
1: {
mp("10.0.0.0/16"): {},
},
},
expectedPrimaries: map[netip.Prefix]types.NodeID{
mp("10.0.0.0/16"): 1,
},
expectedIsPrimary: map[types.NodeID]bool{
1: true,
},
expectedChange: false,
},
{
name: "concurrent-access",
operations: func(pr *PrimaryRoutes) bool {

View File

@ -14,6 +14,7 @@ import (
"github.com/juanfont/headscale/hscontrol/util"
"go4.org/netipx"
"google.golang.org/protobuf/types/known/timestamppb"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
@ -222,6 +223,19 @@ func (node *Node) Prefixes() []netip.Prefix {
return addrs
}
// ExitRoutes returns a list of both exit routes if the
// node has any exit routes enabled.
// If none are enabled, it will return nil.
func (node *Node) ExitRoutes() []netip.Prefix {
for _, route := range node.SubnetRoutes() {
if tsaddr.IsExitRoute(route) {
return tsaddr.ExitRoutes()
}
}
return nil
}
func (node *Node) IPsAsString() []string {
var ret []string

View File

@ -8,6 +8,7 @@ import (
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
policyv1 "github.com/juanfont/headscale/hscontrol/policy/v1"
"github.com/juanfont/headscale/hscontrol/util"
@ -19,6 +20,7 @@ import (
"tailscale.com/net/tsaddr"
"tailscale.com/types/ipproto"
"tailscale.com/types/views"
"tailscale.com/util/slicesx"
"tailscale.com/wgengine/filter"
)
@ -125,7 +127,7 @@ func TestEnablingRoutes(t *testing.T) {
for _, peerKey := range status.Peers() {
peerStatus := status.Peer[peerKey]
assert.Nil(t, peerStatus.PrimaryRoutes)
assert.NotNil(t, peerStatus.PrimaryRoutes)
assert.Len(t, peerStatus.AllowedIPs.AsSlice(), 3)
@ -189,7 +191,6 @@ func TestEnablingRoutes(t *testing.T) {
for _, peerKey := range status.Peers() {
peerStatus := status.Peer[peerKey]
assert.Nil(t, peerStatus.PrimaryRoutes)
if peerStatus.ID == "1" {
requirePeerSubnetRoutes(t, peerStatus, nil)
} else if peerStatus.ID == "2" {
@ -1378,6 +1379,32 @@ func TestSubnetRouterMultiNetwork(t *testing.T) {
peerStatus := status.Peer[peerKey]
assert.Contains(t, peerStatus.PrimaryRoutes.AsSlice(), *pref)
requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{*pref})
}
usernet1, err := scenario.Network("usernet1")
require.NoError(t, err)
services, err := scenario.Services("usernet1")
require.NoError(t, err)
require.Len(t, services, 1)
web := services[0]
webip := netip.MustParseAddr(web.GetIPInNetwork(usernet1))
url := fmt.Sprintf("http://%s/etc/hostname", webip)
t.Logf("url from %s to %s", user2c.Hostname(), url)
result, err := user2c.Curl(url)
require.NoError(t, err)
assert.Len(t, result, 13)
tr, err := user2c.Traceroute(webip)
require.NoError(t, err)
assertTracerouteViaIP(t, tr, user1c.MustIPv4())
}
// TestSubnetRouterMultiNetworkExitNode
func TestSubnetRouterMultiNetworkExitNode(t *testing.T) {
IntegrationSkip(t)
t.Parallel()
@ -1510,32 +1537,6 @@ func TestSubnetRouterMultiNetworkExitNode(t *testing.T) {
require.NoError(t, err)
}
assert.Nil(t, peerStatus.PrimaryRoutes)
requirePeerSubnetRoutes(t, peerStatus, []netip.Prefix{*pref})
}
usernet1, err := scenario.Network("usernet1")
require.NoError(t, err)
services, err := scenario.Services("usernet1")
require.NoError(t, err)
require.Len(t, services, 1)
web := services[0]
webip := netip.MustParseAddr(web.GetIPInNetwork(usernet1))
url := fmt.Sprintf("http://%s/etc/hostname", webip)
t.Logf("url from %s to %s", user2c.Hostname(), url)
result, err := user2c.Curl(url)
require.NoError(t, err)
assert.Len(t, result, 13)
tr, err := user2c.Traceroute(webip)
require.NoError(t, err)
assertTracerouteViaIP(t, tr, user1c.MustIPv4())
}
func assertTracerouteViaIP(t *testing.T, tr util.Traceroute, ip netip.Addr) {
t.Helper()