Skip to content

Commit b6dc6eb

Browse files
authored
#2140 Fixed reflection of hostname change (#2199)
* #2140 Fixed updating of hostname and givenName when it is updated in HostInfo * #2140 Added integration tests * #2140 Fix unit tests * Changed IsAutomaticNameMode to GivenNameHasBeenChanged. Fixed errors in files according to golangci-lint rules
1 parent 45c9585 commit b6dc6eb

File tree

6 files changed

+221
-2
lines changed

6 files changed

+221
-2
lines changed

.github/workflows/test-integration.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ jobs:
5050
- TestEphemeral2006DeletedTooQuickly
5151
- TestPingAllByHostname
5252
- TestTaildrop
53+
- TestUpdateHostnameFromClient
5354
- TestExpireNode
5455
- TestNodeOnlineStatus
5556
- TestPingAllByIPManyUpDown

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
- Allow nodes to use SSH agent forwarding [#2145](https://github.com/juanfont/headscale/pull/2145)
2222
- Fixed processing of fields in post request in MoveNode rpc [#2179](https://github.com/juanfont/headscale/pull/2179)
2323
- Added conversion of 'Hostname' to 'givenName' in a node with FQDN rules applied [#2198](https://github.com/juanfont/headscale/pull/2198)
24+
- Fixed updating of hostname and givenName when it is updated in HostInfo [#2199](https://github.com/juanfont/headscale/pull/2199)
2425

2526
## 0.23.0 (2024-09-18)
2627

hscontrol/poll.go

+9-2
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,7 @@ func (m *mapSession) handleEndpointUpdate() {
471471

472472
// Check if the Hostinfo of the node has changed.
473473
// If it has changed, check if there has been a change to
474-
// the routable IPs of the host and update update them in
474+
// the routable IPs of the host and update them in
475475
// the database. Then send a Changed update
476476
// (containing the whole node object) to peers to inform about
477477
// the route change.
@@ -510,6 +510,12 @@ func (m *mapSession) handleEndpointUpdate() {
510510
m.node.ID)
511511
}
512512

513+
// Check if there has been a change to Hostname and update them
514+
// in the database. Then send a Changed update
515+
// (containing the whole node object) to peers to inform about
516+
// the hostname change.
517+
m.node.ApplyHostnameFromHostInfo(m.req.Hostinfo)
518+
513519
if err := m.h.db.DB.Save(m.node).Error; err != nil {
514520
m.errf(err, "Failed to persist/update node in the database")
515521
http.Error(m.w, "", http.StatusInternalServerError)
@@ -526,7 +532,8 @@ func (m *mapSession) handleEndpointUpdate() {
526532
ChangeNodes: []types.NodeID{m.node.ID},
527533
Message: "called from handlePoll -> update",
528534
},
529-
m.node.ID)
535+
m.node.ID,
536+
)
530537

531538
m.w.WriteHeader(http.StatusOK)
532539
mapResponseEndpointUpdates.WithLabelValues("ok").Inc()

hscontrol/types/node.go

+20
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ type (
9797
Nodes []*Node
9898
)
9999

100+
// GivenNameHasBeenChanged returns whether the `givenName` can be automatically changed based on the `Hostname` of the node.
101+
func (node *Node) GivenNameHasBeenChanged() bool {
102+
return node.GivenName == util.ConvertWithFQDNRules(node.Hostname)
103+
}
104+
100105
// IsExpired returns whether the node registration has expired.
101106
func (node Node) IsExpired() bool {
102107
// If Expiry is not set, the client has not indicated that
@@ -347,6 +352,21 @@ func (node *Node) RegisterMethodToV1Enum() v1.RegisterMethod {
347352
}
348353
}
349354

355+
// ApplyHostnameFromHostInfo takes a Hostinfo struct and updates the node.
356+
func (node *Node) ApplyHostnameFromHostInfo(hostInfo *tailcfg.Hostinfo) {
357+
if hostInfo == nil {
358+
return
359+
}
360+
361+
if node.Hostname != hostInfo.Hostname {
362+
if node.GivenNameHasBeenChanged() {
363+
node.GivenName = util.ConvertWithFQDNRules(hostInfo.Hostname)
364+
}
365+
366+
node.Hostname = hostInfo.Hostname
367+
}
368+
}
369+
350370
// ApplyPeerChange takes a PeerChange struct and updates the node.
351371
func (node *Node) ApplyPeerChange(change *tailcfg.PeerChange) {
352372
if change.Key != nil {

hscontrol/types/node_test.go

+60
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,66 @@ func TestPeerChangeFromMapRequest(t *testing.T) {
337337
}
338338
}
339339

340+
func TestApplyHostnameFromHostInfo(t *testing.T) {
341+
tests := []struct {
342+
name string
343+
nodeBefore Node
344+
change *tailcfg.Hostinfo
345+
want Node
346+
}{
347+
{
348+
name: "hostinfo-not-exists",
349+
nodeBefore: Node{
350+
GivenName: "manual-test.local",
351+
Hostname: "TestHost.Local",
352+
},
353+
change: nil,
354+
want: Node{
355+
GivenName: "manual-test.local",
356+
Hostname: "TestHost.Local",
357+
},
358+
},
359+
{
360+
name: "hostinfo-exists-no-automatic-givenName",
361+
nodeBefore: Node{
362+
GivenName: "manual-test.local",
363+
Hostname: "TestHost.Local",
364+
},
365+
change: &tailcfg.Hostinfo{
366+
Hostname: "NewHostName.Local",
367+
},
368+
want: Node{
369+
GivenName: "manual-test.local",
370+
Hostname: "NewHostName.Local",
371+
},
372+
},
373+
{
374+
name: "hostinfo-exists-automatic-givenName",
375+
nodeBefore: Node{
376+
GivenName: "automaticname.test",
377+
Hostname: "AutomaticName.Test",
378+
},
379+
change: &tailcfg.Hostinfo{
380+
Hostname: "NewHostName.Local",
381+
},
382+
want: Node{
383+
GivenName: "newhostname.local",
384+
Hostname: "NewHostName.Local",
385+
},
386+
},
387+
}
388+
389+
for _, tt := range tests {
390+
t.Run(tt.name, func(t *testing.T) {
391+
tt.nodeBefore.ApplyHostnameFromHostInfo(tt.change)
392+
393+
if diff := cmp.Diff(tt.want, tt.nodeBefore, util.Comparers...); diff != "" {
394+
t.Errorf("Patch unexpected result (-want +got):\n%s", diff)
395+
}
396+
})
397+
}
398+
}
399+
340400
func TestApplyPeerChange(t *testing.T) {
341401
tests := []struct {
342402
name string

integration/general_test.go

+130
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import (
55
"encoding/json"
66
"fmt"
77
"net/netip"
8+
"strconv"
89
"strings"
910
"testing"
1011
"time"
1112

1213
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
1314
"github.com/juanfont/headscale/hscontrol/types"
15+
"github.com/juanfont/headscale/hscontrol/util"
1416
"github.com/juanfont/headscale/integration/hsic"
1517
"github.com/juanfont/headscale/integration/tsic"
1618
"github.com/rs/zerolog/log"
@@ -654,6 +656,134 @@ func TestTaildrop(t *testing.T) {
654656
}
655657
}
656658

659+
func TestUpdateHostnameFromClient(t *testing.T) {
660+
IntegrationSkip(t)
661+
t.Parallel()
662+
663+
user := "update-hostname-from-client"
664+
665+
hostnames := map[string]string{
666+
"1": "user1-host",
667+
"2": "User2-Host",
668+
"3": "user3-host",
669+
}
670+
671+
scenario, err := NewScenario(dockertestMaxWait())
672+
assertNoErrf(t, "failed to create scenario: %s", err)
673+
defer scenario.ShutdownAssertNoPanics(t)
674+
675+
spec := map[string]int{
676+
user: 3,
677+
}
678+
679+
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("updatehostname"))
680+
assertNoErrHeadscaleEnv(t, err)
681+
682+
allClients, err := scenario.ListTailscaleClients()
683+
assertNoErrListClients(t, err)
684+
685+
err = scenario.WaitForTailscaleSync()
686+
assertNoErrSync(t, err)
687+
688+
headscale, err := scenario.Headscale()
689+
assertNoErrGetHeadscale(t, err)
690+
691+
// update hostnames using the up command
692+
for _, client := range allClients {
693+
status, err := client.Status()
694+
assertNoErr(t, err)
695+
696+
command := []string{
697+
"tailscale",
698+
"set",
699+
"--hostname=" + hostnames[string(status.Self.ID)],
700+
}
701+
_, _, err = client.Execute(command)
702+
assertNoErrf(t, "failed to set hostname: %s", err)
703+
}
704+
705+
err = scenario.WaitForTailscaleSync()
706+
assertNoErrSync(t, err)
707+
708+
var nodes []*v1.Node
709+
err = executeAndUnmarshal(
710+
headscale,
711+
[]string{
712+
"headscale",
713+
"node",
714+
"list",
715+
"--output",
716+
"json",
717+
},
718+
&nodes,
719+
)
720+
721+
assertNoErr(t, err)
722+
assert.Len(t, nodes, 3)
723+
724+
for _, node := range nodes {
725+
hostname := hostnames[strconv.FormatUint(node.GetId(), 10)]
726+
assert.Equal(t, hostname, node.GetName())
727+
assert.Equal(t, util.ConvertWithFQDNRules(hostname), node.GetGivenName())
728+
}
729+
730+
// Rename givenName in nodes
731+
for _, node := range nodes {
732+
givenName := fmt.Sprintf("%d-givenname", node.GetId())
733+
_, err = headscale.Execute(
734+
[]string{
735+
"headscale",
736+
"node",
737+
"rename",
738+
givenName,
739+
"--identifier",
740+
strconv.FormatUint(node.GetId(), 10),
741+
})
742+
assertNoErr(t, err)
743+
}
744+
745+
time.Sleep(5 * time.Second)
746+
747+
// Verify that the clients can see the new hostname, but no givenName
748+
for _, client := range allClients {
749+
status, err := client.Status()
750+
assertNoErr(t, err)
751+
752+
command := []string{
753+
"tailscale",
754+
"set",
755+
"--hostname=" + hostnames[string(status.Self.ID)] + "NEW",
756+
}
757+
_, _, err = client.Execute(command)
758+
assertNoErrf(t, "failed to set hostname: %s", err)
759+
}
760+
761+
err = scenario.WaitForTailscaleSync()
762+
assertNoErrSync(t, err)
763+
764+
err = executeAndUnmarshal(
765+
headscale,
766+
[]string{
767+
"headscale",
768+
"node",
769+
"list",
770+
"--output",
771+
"json",
772+
},
773+
&nodes,
774+
)
775+
776+
assertNoErr(t, err)
777+
assert.Len(t, nodes, 3)
778+
779+
for _, node := range nodes {
780+
hostname := hostnames[strconv.FormatUint(node.GetId(), 10)]
781+
givenName := fmt.Sprintf("%d-givenname", node.GetId())
782+
assert.Equal(t, hostname+"NEW", node.GetName())
783+
assert.Equal(t, givenName, node.GetGivenName())
784+
}
785+
}
786+
657787
func TestExpireNode(t *testing.T) {
658788
IntegrationSkip(t)
659789
t.Parallel()

0 commit comments

Comments
 (0)