diff --git a/hscontrol/state/persist_test.go b/hscontrol/state/persist_test.go index c43ecdad4..8e7133b6e 100644 --- a/hscontrol/state/persist_test.go +++ b/hscontrol/state/persist_test.go @@ -270,6 +270,42 @@ func TestRegistrationRejectsNodeKeyClaimedByAnotherMachine(t *testing.T) { "registering a NodeKey already bound to another machine must be rejected") } +// TestRegistrationKeepsRequestTagsIntact guards the node's reported +// Hostinfo.RequestTags against the in-place sort/compact that derives the +// approved tag set: the two must not share a backing array. +func TestRegistrationKeepsRequestTagsIntact(t *testing.T) { + dbPath := t.TempDir() + "/headscale.db" + cfg := persistTestConfig(dbPath) + + database, err := db.NewHeadscaleDatabase(cfg) + require.NoError(t, err) + + user := database.CreateUserForTest("tagger") + require.NoError(t, database.Close()) + + s, err := NewState(cfg) + require.NoError(t, err) + t.Cleanup(func() { _ = s.Close() }) + + _, err = s.SetPolicy([]byte(`{"tagOwners":{"tag:a":["tagger@"],"tag:b":["tagger@"]}}`)) + require.NoError(t, err) + + node, err := s.createAndSaveNewNode(newNodeParams{ + User: *user, + MachineKey: key.NewMachine().Public(), + NodeKey: key.NewNode().Public(), + DiscoKey: key.NewDisco().Public(), + Hostname: "node", + Hostinfo: &tailcfg.Hostinfo{RequestTags: []string{"tag:b", "tag:a", "tag:a"}}, + RegisterMethod: util.RegisterMethodCLI, + }) + require.NoError(t, err) + + assert.Equal(t, []string{"tag:a", "tag:b"}, node.Tags().AsSlice()) + assert.Equal(t, []string{"tag:b", "tag:a", "tag:a"}, node.Hostinfo().RequestTags().AsSlice(), + "reported RequestTags rewritten by tag approval") +} + // TestReauthRejectsNodeKeyClaimedByAnotherMachine proves the re-auth/update // path enforces the same 1:1 NodeKey<->MachineKey binding as the create path // (TestRegistrationRejectsNodeKeyClaimedByAnotherMachine) and the poll path diff --git a/hscontrol/state/state.go b/hscontrol/state/state.go index 7916ea595..68cb135c1 100644 --- a/hscontrol/state/state.go +++ b/hscontrol/state/state.go @@ -2114,9 +2114,8 @@ func (s *State) createAndSaveNewNode(params newNodeParams) (types.NodeView, erro // All tags are approved - apply them approvedTags := params.Hostinfo.RequestTags if len(approvedTags) > 0 { - nodeToRegister.Tags = approvedTags - slices.Sort(nodeToRegister.Tags) - nodeToRegister.Tags = slices.Compact(nodeToRegister.Tags) + // Sort a copy: approvedTags is the node's reported Hostinfo. + nodeToRegister.Tags = slices.Compact(slices.Sorted(slices.Values(approvedTags))) // Node is now tagged, so clear user ownership. // Tagged nodes are owned by their tags, not a user.