mirror of
https://github.com/juanfont/headscale.git
synced 2026-10-31 18:54:11 +09:00
state: give another user's key on a tagged machine a new device
Only a key that speaks for a tagged node re-authenticates it: a tagged
key, or one of the user it was tagged from. Another user's key or login
adds that user's own device, or only wakes the node under its node key.
Fixes ANT-2026-AWGRB7O1
(cherry picked from commit e1de83cbdf, adapted: inline PAK re-register path, no servertest compat table)
This commit is contained in:
1 parent
e6219cd106
commit
c8fb25d451
8 files changed
+457
-98
No files matched your search
+30
-26
@@ -3295,18 +3295,19 @@ func TestTagsAuthKeyWithoutUserRejectsAdvertisedTags(t *testing.T) {
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test Suite 6: Tagged→User Conversion via CLI Register (#3038)
|
||||
// Test Suite 6: User Login on a Tagged Machine via CLI Register (#3038)
|
||||
// =============================================================================
|
||||
|
||||
// TestTagsAuthKeyConvertToUserViaCLIRegister reproduces the panic from
|
||||
// issue #3038: register a node with a tags-only preauthkey (no user), then
|
||||
// convert it to a user-owned node via "headscale auth register --auth-id <id> --user <user>".
|
||||
// The crash happens in the mapper's generateUserProfiles when [types.Node.User] is nil
|
||||
// after the tag→user conversion in [State.processReauthTags].
|
||||
// TestTagsAuthKeyWithoutUserCLIRegisterAddsUserDevice covers issue #3038:
|
||||
// register a node with a tags-only preauthkey (no user), then log the machine
|
||||
// in as a user via "headscale auth register --auth-id <id> --user <user>".
|
||||
// The crash was in the mapper's generateUserProfiles when [types.Node.User]
|
||||
// was nil.
|
||||
//
|
||||
// The key detail is using a tags-only PreAuthKey (User: nil). When created under
|
||||
// a user, the node inherits User from the PreAuthKey and the bug is masked.
|
||||
func TestTagsAuthKeyConvertToUserViaCLIRegister(t *testing.T) {
|
||||
// No user made the key, so no user can take the tagged node back. As for
|
||||
// another user's login on a tagged machine on Tailscale SaaS, the login adds a
|
||||
// device of the user's own beside it.
|
||||
func TestTagsAuthKeyWithoutUserCLIRegisterAddsUserDevice(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
policy := tagsTestPolicy()
|
||||
@@ -3331,10 +3332,8 @@ func TestTagsAuthKeyConvertToUserViaCLIRegister(t *testing.T) {
|
||||
headscale, err := scenario.Headscale()
|
||||
requireNoErrGetHeadscale(t, err)
|
||||
|
||||
// Step 1: Create a tags-only preauthkey WITHOUT a user.
|
||||
// This is the critical detail: when [types.PreAuthKey.UserID] is nil, the node
|
||||
// enters the [state.NodeStore] with [types.Node.User] == nil. The [state.State.processReauthTags]
|
||||
// conversion then sets UserID but not User, leaving it nil for the mapper.
|
||||
// Step 1: Create a tags-only preauthkey WITHOUT a user, so the node enters
|
||||
// the [state.NodeStore] with [types.Node.User] == nil.
|
||||
authKey, err := scenario.CreatePreAuthKeyWithOptions(hsic.AuthKeyOptions{
|
||||
User: nil,
|
||||
Reusable: false,
|
||||
@@ -3356,6 +3355,8 @@ func TestTagsAuthKeyConvertToUserViaCLIRegister(t *testing.T) {
|
||||
err = client.WaitForRunning(integrationutil.PeerSyncTimeout())
|
||||
require.NoError(t, err)
|
||||
|
||||
var taggedID uint64
|
||||
|
||||
// Verify initial state: node is tagged
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
nodes, err := headscale.ListNodes()
|
||||
@@ -3364,6 +3365,7 @@ func TestTagsAuthKeyConvertToUserViaCLIRegister(t *testing.T) {
|
||||
|
||||
if len(nodes) == 1 {
|
||||
assertNodeHasTagsWithCollect(c, nodes[0], []string{"tag:valid-owned"})
|
||||
taggedID = nodes[0].GetId()
|
||||
t.Logf("Initial state - Node ID: %d, Tags: %v", nodes[0].GetId(), nodes[0].GetTags())
|
||||
}
|
||||
}, integrationutil.StatusReadyTimeout, integrationutil.SlowPoll, "node should be tagged initially")
|
||||
@@ -3386,30 +3388,32 @@ func TestTagsAuthKeyConvertToUserViaCLIRegister(t *testing.T) {
|
||||
body, err := doLoginURL(client.Hostname(), loginURL)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Step 3: Register via CLI with user (this is the exact step that triggers the panic)
|
||||
// Step 3: Register via CLI with user (the step that triggered the panic)
|
||||
err = scenario.runHeadscaleRegister(tagTestUser, body)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = client.WaitForRunning(integrationutil.PeerSyncTimeout())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Step 4: Verify node is now user-owned and the mapper didn't panic.
|
||||
// The panic would occur when the mapper builds the [tailcfg.MapResponse] and calls
|
||||
// [types.Node.Owner].Model().ID with a nil User pointer.
|
||||
// ShutdownAssertNoPanics in the defer catches any panics in headscale logs.
|
||||
// Step 4: The tagged node stays and the user gets a device of its own. The
|
||||
// mapper builds both into the map; ShutdownAssertNoPanics in the defer
|
||||
// catches a panic in the headscale logs.
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
nodes, err := headscale.ListNodes()
|
||||
assert.NoError(c, err)
|
||||
assert.Len(c, nodes, 1)
|
||||
assert.Len(c, nodes, 2)
|
||||
|
||||
if len(nodes) == 1 {
|
||||
assertNodeHasNoTagsWithCollect(c, nodes[0])
|
||||
assert.Equal(c, tagTestUser, nodes[0].GetUser().GetName(),
|
||||
"Node ownership should be returned to user after untagging")
|
||||
t.Logf("After conversion - Node ID: %d, Tags: %v, User: %s",
|
||||
nodes[0].GetId(), nodes[0].GetTags(), nodes[0].GetUser().GetName())
|
||||
for _, node := range nodes {
|
||||
if node.GetId() == taggedID {
|
||||
assertNodeHasTagsWithCollect(c, node, []string{"tag:valid-owned"})
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
assertNodeHasNoTagsWithCollect(c, node)
|
||||
assert.Equal(c, tagTestUser, node.GetUser().GetName(), "the login's device belongs to the user")
|
||||
}
|
||||
}, integrationutil.HAConvergeTimeout, 1*time.Second, "node should be user-owned after conversion via CLI register")
|
||||
}, integrationutil.HAConvergeTimeout, 1*time.Second, "login should add a user device beside the tagged node")
|
||||
}
|
||||
|
||||
// TestTaggedNodeLogoutReloginSingleUseKeyOnline reproduces issue #3371
|
||||
|
||||
Reference in new issue
Block a user