From c0472781094860caf411447fc3c7528864e1a8cd Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 19 Mar 2025 08:34:28 +0100 Subject: [PATCH] split out exit nodes from primary manager Signed-off-by: Kristoffer Dalby --- hscontrol/mapper/mapper_test.go | 3 +- hscontrol/mapper/tail.go | 5 +++ hscontrol/mapper/tail_test.go | 3 +- hscontrol/routes/primary.go | 8 +++-- hscontrol/routes/primary_test.go | 21 +++++++++++- hscontrol/types/node.go | 14 ++++++++ integration/route_test.go | 57 ++++++++++++++++---------------- 7 files changed, 78 insertions(+), 33 deletions(-) diff --git a/hscontrol/mapper/mapper_test.go b/hscontrol/mapper/mapper_test.go index e8bb5398..ced0c9f4 100644 --- a/hscontrol/mapper/mapper_test.go +++ b/hscontrol/mapper/mapper_test.go @@ -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"), diff --git a/hscontrol/mapper/tail.go b/hscontrol/mapper/tail.go index e5101f29..32905345 100644 --- a/hscontrol/mapper/tail.go +++ b/hscontrol/mapper/tail.go @@ -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(), diff --git a/hscontrol/mapper/tail_test.go b/hscontrol/mapper/tail_test.go index fa6925a3..9722df2e 100644 --- a/hscontrol/mapper/tail_test.go +++ b/hscontrol/mapper/tail_test.go @@ -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"), diff --git a/hscontrol/routes/primary.go b/hscontrol/routes/primary.go index 869332ab..67eb8d1f 100644 --- a/hscontrol/routes/primary.go +++ b/hscontrol/routes/primary.go @@ -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() diff --git a/hscontrol/routes/primary_test.go b/hscontrol/routes/primary_test.go index 03ceadd5..7a9767b2 100644 --- a/hscontrol/routes/primary_test.go +++ b/hscontrol/routes/primary_test.go @@ -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 { diff --git a/hscontrol/types/node.go b/hscontrol/types/node.go index 0679e97f..767ccdff 100644 --- a/hscontrol/types/node.go +++ b/hscontrol/types/node.go @@ -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 diff --git a/integration/route_test.go b/integration/route_test.go index aba8df96..2ca0c10e 100644 --- a/integration/route_test.go +++ b/integration/route_test.go @@ -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()