Skip to content

Commit da2ca05

Browse files
authored
fix routes not being saved when new nodes registers (#2444)
* add test to validate exitnode propagation Signed-off-by: Kristoffer Dalby <[email protected]> * save routes on register Signed-off-by: Kristoffer Dalby <[email protected]> * update changelog Signed-off-by: Kristoffer Dalby <[email protected]> * no nil Signed-off-by: Kristoffer Dalby <[email protected]> * add missing integration tests Signed-off-by: Kristoffer Dalby <[email protected]> --------- Signed-off-by: Kristoffer Dalby <[email protected]>
1 parent bcff0ea commit da2ca05

File tree

6 files changed

+150
-1
lines changed

6 files changed

+150
-1
lines changed

.github/workflows/test-integration.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ jobs:
2424
- TestPolicyUpdateWhileRunningWithCLIInDatabase
2525
- TestAuthKeyLogoutAndReloginSameUser
2626
- TestAuthKeyLogoutAndReloginNewUser
27+
- TestAuthKeyLogoutAndReloginSameUserExpiredKey
2728
- TestOIDCAuthenticationPingAll
2829
- TestOIDCExpireNodesBasedOnTokenExpiry
2930
- TestOIDC024UserCreation
@@ -68,6 +69,7 @@ jobs:
6869
- TestEnableDisableAutoApprovedRoute
6970
- TestAutoApprovedSubRoute2068
7071
- TestSubnetRouteACL
72+
- TestEnablingExitRoutes
7173
- TestHeadscale
7274
- TestCreateTailscale
7375
- TestTailscaleNodesJoiningHeadcale

CHANGELOG.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@
1414
- View of config, policy, filter, ssh policy per node, connected nodes and
1515
DERPmap
1616

17-
## 0.25.1 (2025-02-18)
17+
## 0.25.1 (2025-02-24)
1818

1919
### Changes
2020

2121
- Fix issue where registration errors are sent correctly
2222
[#2435](https://github.com/juanfont/headscale/pull/2435)
23+
- Fix issue where routes passed on registration were not saved
24+
[#2444](https://github.com/juanfont/headscale/pull/2444)
2325

2426
## 0.25.0 (2025-02-11)
2527

hscontrol/db/node.go

+4
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,10 @@ func RegisterNode(tx *gorm.DB, node types.Node, ipv4 *netip.Addr, ipv6 *netip.Ad
453453
return nil, fmt.Errorf("failed register(save) node in the database: %w", err)
454454
}
455455

456+
if _, err := SaveNodeRoutes(tx, &node); err != nil {
457+
return nil, fmt.Errorf("failed to save node routes: %w", err)
458+
}
459+
456460
log.Trace().
457461
Caller().
458462
Str("node", node.Hostname).

hscontrol/db/node_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,7 @@ func TestRenameNode(t *testing.T) {
744744
Hostname: "test",
745745
UserID: user.ID,
746746
RegisterMethod: util.RegisterMethodAuthKey,
747+
Hostinfo: &tailcfg.Hostinfo{},
747748
}
748749

749750
node2 := types.Node{
@@ -753,6 +754,7 @@ func TestRenameNode(t *testing.T) {
753754
Hostname: "test",
754755
UserID: user2.ID,
755756
RegisterMethod: util.RegisterMethodAuthKey,
757+
Hostinfo: &tailcfg.Hostinfo{},
756758
}
757759

758760
err = db.DB.Save(&node).Error

integration/route_test.go

+122
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import (
1717
"github.com/juanfont/headscale/integration/hsic"
1818
"github.com/juanfont/headscale/integration/tsic"
1919
"github.com/stretchr/testify/assert"
20+
"github.com/stretchr/testify/require"
21+
"tailscale.com/net/tsaddr"
2022
"tailscale.com/types/ipproto"
2123
"tailscale.com/types/views"
2224
"tailscale.com/wgengine/filter"
@@ -1316,3 +1318,123 @@ func TestSubnetRouteACL(t *testing.T) {
13161318
t.Errorf("Subnet (%s) filter, unexpected result (-want +got):\n%s", subRouter1.Hostname(), diff)
13171319
}
13181320
}
1321+
1322+
// TestEnablingExitRoutes tests enabling exit routes for clients.
1323+
// Its more or less the same as TestEnablingRoutes, but with the --advertise-exit-node flag
1324+
// set during login instead of set.
1325+
func TestEnablingExitRoutes(t *testing.T) {
1326+
IntegrationSkip(t)
1327+
t.Parallel()
1328+
1329+
user := "user2"
1330+
1331+
scenario, err := NewScenario(dockertestMaxWait())
1332+
assertNoErrf(t, "failed to create scenario: %s", err)
1333+
defer scenario.ShutdownAssertNoPanics(t)
1334+
1335+
spec := map[string]int{
1336+
user: 2,
1337+
}
1338+
1339+
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{
1340+
tsic.WithExtraLoginArgs([]string{"--advertise-exit-node"}),
1341+
}, hsic.WithTestName("clienableroute"))
1342+
assertNoErrHeadscaleEnv(t, err)
1343+
1344+
allClients, err := scenario.ListTailscaleClients()
1345+
assertNoErrListClients(t, err)
1346+
1347+
err = scenario.WaitForTailscaleSync()
1348+
assertNoErrSync(t, err)
1349+
1350+
headscale, err := scenario.Headscale()
1351+
assertNoErrGetHeadscale(t, err)
1352+
1353+
err = scenario.WaitForTailscaleSync()
1354+
assertNoErrSync(t, err)
1355+
1356+
var routes []*v1.Route
1357+
err = executeAndUnmarshal(
1358+
headscale,
1359+
[]string{
1360+
"headscale",
1361+
"routes",
1362+
"list",
1363+
"--output",
1364+
"json",
1365+
},
1366+
&routes,
1367+
)
1368+
1369+
assertNoErr(t, err)
1370+
assert.Len(t, routes, 4)
1371+
1372+
for _, route := range routes {
1373+
assert.True(t, route.GetAdvertised())
1374+
assert.False(t, route.GetEnabled())
1375+
assert.False(t, route.GetIsPrimary())
1376+
}
1377+
1378+
// Verify that no routes has been sent to the client,
1379+
// they are not yet enabled.
1380+
for _, client := range allClients {
1381+
status, err := client.Status()
1382+
assertNoErr(t, err)
1383+
1384+
for _, peerKey := range status.Peers() {
1385+
peerStatus := status.Peer[peerKey]
1386+
1387+
assert.Nil(t, peerStatus.PrimaryRoutes)
1388+
}
1389+
}
1390+
1391+
// Enable all routes
1392+
for _, route := range routes {
1393+
_, err = headscale.Execute(
1394+
[]string{
1395+
"headscale",
1396+
"routes",
1397+
"enable",
1398+
"--route",
1399+
strconv.Itoa(int(route.GetId())),
1400+
})
1401+
assertNoErr(t, err)
1402+
}
1403+
1404+
var enablingRoutes []*v1.Route
1405+
err = executeAndUnmarshal(
1406+
headscale,
1407+
[]string{
1408+
"headscale",
1409+
"routes",
1410+
"list",
1411+
"--output",
1412+
"json",
1413+
},
1414+
&enablingRoutes,
1415+
)
1416+
assertNoErr(t, err)
1417+
assert.Len(t, enablingRoutes, 4)
1418+
1419+
for _, route := range enablingRoutes {
1420+
assert.True(t, route.GetAdvertised())
1421+
assert.True(t, route.GetEnabled())
1422+
}
1423+
1424+
time.Sleep(5 * time.Second)
1425+
1426+
// Verify that the clients can see the new routes
1427+
for _, client := range allClients {
1428+
status, err := client.Status()
1429+
assertNoErr(t, err)
1430+
1431+
for _, peerKey := range status.Peers() {
1432+
peerStatus := status.Peer[peerKey]
1433+
1434+
require.NotNil(t, peerStatus.AllowedIPs)
1435+
assert.Len(t, peerStatus.AllowedIPs.AsSlice(), 4)
1436+
assert.Contains(t, peerStatus.AllowedIPs.AsSlice(), tsaddr.AllIPv4())
1437+
assert.Contains(t, peerStatus.AllowedIPs.AsSlice(), tsaddr.AllIPv6())
1438+
}
1439+
}
1440+
}

integration/tsic/tsic.go

+17
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ type TailscaleInContainer struct {
8080
withExtraHosts []string
8181
workdir string
8282
netfilter string
83+
extraLoginArgs []string
8384

8485
// build options, solely for HEAD
8586
buildConfig TailscaleInContainerBuildConfig
@@ -203,6 +204,14 @@ func WithBuildTag(tag string) Option {
203204
}
204205
}
205206

207+
// WithExtraLoginArgs adds additional arguments to the `tailscale up` command
208+
// as part of the Login function.
209+
func WithExtraLoginArgs(args []string) Option {
210+
return func(tsic *TailscaleInContainer) {
211+
tsic.extraLoginArgs = args
212+
}
213+
}
214+
206215
// New returns a new TailscaleInContainer instance.
207216
func New(
208217
pool *dockertest.Pool,
@@ -436,6 +445,10 @@ func (t *TailscaleInContainer) Login(
436445
"--accept-routes=false",
437446
}
438447

448+
if t.extraLoginArgs != nil {
449+
command = append(command, t.extraLoginArgs...)
450+
}
451+
439452
if t.withSSH {
440453
command = append(command, "--ssh")
441454
}
@@ -475,6 +488,10 @@ func (t *TailscaleInContainer) LoginWithURL(
475488
"--accept-routes=false",
476489
}
477490

491+
if t.extraLoginArgs != nil {
492+
command = append(command, t.extraLoginArgs...)
493+
}
494+
478495
stdout, stderr, err := t.Execute(command)
479496
if errors.Is(err, errTailscaleNotLoggedIn) {
480497
return nil, errTailscaleCannotUpWithoutAuthkey

0 commit comments

Comments
 (0)