1
0
mirror of https://github.com/juanfont/headscale.git synced 2025-09-25 17:51:11 +02:00

attempt to replicate #2731

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
Kristoffer Dalby 2025-09-05 16:17:13 +02:00
parent 4de56c40d8
commit 56dac59538
No known key found for this signature in database
3 changed files with 148 additions and 0 deletions

View File

@ -451,3 +451,83 @@ func TestAuthKeyLogoutAndReloginSameUserExpiredKey(t *testing.T) {
}) })
} }
} }
func TestDuplicateNodeKeysSpoofing2731(t *testing.T) {
IntegrationSkip(t)
spec := ScenarioSpec{
NodesPerUser: 0,
Users: []string{},
}
scenario, err := NewScenario(spec)
assertNoErr(t, err)
// defer scenario.ShutdownAssertNoPanics(t)
err = scenario.CreateHeadscaleEnvWithLoginURL(
nil,
hsic.WithTestName("weblogout"),
hsic.WithTLS(),
)
assertNoErrHeadscaleEnv(t, err)
err = scenario.WaitForTailscaleSync()
assertNoErrSync(t, err)
hs, err := scenario.Headscale()
assertNoErrGetHeadscale(t, err)
adminUser, err := scenario.CreateUser("admin")
require.NoError(t, err)
attackerUser, err := scenario.CreateUser("attacker")
require.NoError(t, err)
adminNode, err := scenario.CreateTailscaleNode("unstable", tsic.WithNetwork(scenario.networks[scenario.testDefaultNetwork]))
require.NoError(t, err)
attackerNode, err := scenario.CreateTailscaleNode("unstable", tsic.WithNetwork(scenario.networks[scenario.testDefaultNetwork]))
require.NoError(t, err)
adminPAK, err := scenario.CreatePreAuthKey(adminUser.Id, true, false)
require.NoError(t, err)
attackerPAK, err := scenario.CreatePreAuthKey(attackerUser.Id, true, false)
require.NoError(t, err)
err = adminNode.Login(hs.GetEndpoint(), adminPAK.GetKey())
require.NoError(t, err)
err = attackerNode.Login(hs.GetEndpoint(), attackerPAK.GetKey())
require.NoError(t, err)
require.EventuallyWithT(t, func(ct *assert.CollectT) {
nodes, err := hs.ListNodes()
assert.NoError(ct, err)
assert.Len(ct, nodes, 2, "there should be two nodes registered")
users, err := hs.ListUsers()
assert.NoError(ct, err)
assert.Len(ct, users, 2, "there should be two users created")
}, 30*time.Second, 1*time.Second, "ensuring nodes and users are created")
nodePriv, err := adminNode.GetNodePrivateKey()
require.NoError(t, err)
err = attackerNode.SetNodePrivateKey(*nodePriv)
require.NoError(t, err)
err = attackerNode.ReloadTailscaled()
require.NoError(t, err)
nodePriv2, err := attackerNode.GetNodePrivateKey()
require.NoError(t, err)
require.Equal(t, nodePriv.Public().String(), nodePriv2.Public().String(), "node private keys should be equal")
// require.NoError(t, attackerNode.Logout())
// require.NoError(t, attackerNode.WaitForNeedsLogin())
err = attackerNode.Login(hs.GetEndpoint(), attackerPAK.GetKey())
require.NoError(t, err)
}

View File

@ -41,6 +41,8 @@ type TailscaleClient interface {
Netmap() (*netmap.NetworkMap, error) Netmap() (*netmap.NetworkMap, error)
DebugDERPRegion(region string) (*ipnstate.DebugDERPRegionReport, error) DebugDERPRegion(region string) (*ipnstate.DebugDERPRegionReport, error)
GetNodePrivateKey() (*key.NodePrivate, error) GetNodePrivateKey() (*key.NodePrivate, error)
SetNodePrivateKey(privateKey key.NodePrivate) error
ReloadTailscaled() error
Netcheck() (*netcheck.Report, error) Netcheck() (*netcheck.Report, error)
WaitForNeedsLogin(timeout time.Duration) error WaitForNeedsLogin(timeout time.Duration) error
WaitForRunning(timeout time.Duration) error WaitForRunning(timeout time.Duration) error

View File

@ -1333,3 +1333,69 @@ func (t *TailscaleInContainer) GetNodePrivateKey() (*key.NodePrivate, error) {
return &p.Persist.PrivateNodeKey, nil return &p.Persist.PrivateNodeKey, nil
} }
func (t *TailscaleInContainer) SetNodePrivateKey(privateKey key.NodePrivate) error {
state, err := t.ReadFile(paths.DefaultTailscaledStateFile())
if err != nil {
return fmt.Errorf("failed to read state file: %w", err)
}
store := &mem.Store{}
if err = store.LoadFromJSON(state); err != nil {
return fmt.Errorf("failed to unmarshal state file: %w", err)
}
currentProfileKey, err := store.ReadState(ipn.CurrentProfileStateKey)
if err != nil {
return fmt.Errorf("failed to read current profile state key: %w", err)
}
currentProfile, err := store.ReadState(ipn.StateKey(currentProfileKey))
if err != nil {
return fmt.Errorf("failed to read current profile state: %w", err)
}
p := &ipn.Prefs{}
if err = json.Unmarshal(currentProfile, &p); err != nil {
return fmt.Errorf("failed to unmarshal current profile state: %w", err)
}
// Update the private node key
p.Persist.PrivateNodeKey = privateKey
// Marshal the updated preferences back to JSON
updatedProfile, err := json.Marshal(p)
if err != nil {
return fmt.Errorf("failed to marshal updated profile state: %w", err)
}
// Write the updated profile back to the store
if err = store.WriteState(ipn.StateKey(currentProfileKey), updatedProfile); err != nil {
return fmt.Errorf("failed to write updated profile state: %w", err)
}
// Save the updated store back to the state file
updatedState, err := store.ExportToJSON()
if err != nil {
return fmt.Errorf("failed to export updated state: %w", err)
}
if err = t.WriteFile(paths.DefaultTailscaledStateFile(), updatedState); err != nil {
return fmt.Errorf("failed to write updated state file: %w", err)
}
return nil
}
func (t *TailscaleInContainer) ReloadTailscaled() error {
command := []string{
"kill",
"-HUP",
"1",
}
_, _, err := t.Execute(command)
if err != nil {
return fmt.Errorf("failed to reload tailscaled: %w", err)
}
return nil
}