mirror of
https://github.com/juanfont/headscale.git
synced 2026-10-27 16:56:49 +09:00
hscontrol: cancel ephemeral GC timer when a node is deleted
API and logout deletes left the disconnect timer armed; it fired later and logged a bogus ERR for the missing node.
This commit is contained in:
1 parent
494e2a6808
commit
1b184cd2c7
2 files changed
+49
-2
No files matched your search
+10
-2
@@ -161,8 +161,8 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) {
|
||||
ephemeralGC := db.NewEphemeralGarbageCollector(func(ni types.NodeID) {
|
||||
node, ok := app.state.GetNodeByID(ni)
|
||||
if !ok {
|
||||
log.Error().Uint64("node.id", ni.Uint64()).Msg("ephemeral node deletion failed")
|
||||
log.Debug().Caller().Uint64("node.id", ni.Uint64()).Msg("ephemeral node deletion failed because node not found in NodeStore")
|
||||
// A delete that raced this timer already did its job.
|
||||
log.Debug().Caller().Uint64("node.id", ni.Uint64()).Msg("ephemeral node already deleted, skipping garbage collection")
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1274,6 +1274,14 @@ func readOrCreatePrivateKey(path string) (*key.MachinePrivate, error) {
|
||||
// All change should be enqueued here and empty will be automatically
|
||||
// ignored.
|
||||
func (h *Headscale) Change(cs ...change.Change) {
|
||||
// Every delete path publishes its removal here; a deleted node's
|
||||
// inactivity timer has nothing left to collect.
|
||||
for _, c := range cs {
|
||||
for _, id := range c.DeletedNodes {
|
||||
h.ephemeralGC.Cancel(id)
|
||||
}
|
||||
}
|
||||
|
||||
h.mapBatcher.AddWork(slices.Concat(cs, h.state.DrainSelfRefreshes())...)
|
||||
}
|
||||
|
||||
|
||||
@@ -424,6 +424,45 @@ func TestEphemeralGCDoesNotDeleteReconnectedNode(t *testing.T) {
|
||||
"an idle ephemeral node must still be garbage collected")
|
||||
}
|
||||
|
||||
// TestDeleteNodeCancelsEphemeralGC deletes a disconnected ephemeral node
|
||||
// through the API while its inactivity timer is armed. The timer belongs to
|
||||
// the deleted node, so the delete must disarm it rather than leave it to fire
|
||||
// at a node that is no longer there.
|
||||
func TestDeleteNodeCancelsEphemeralGC(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := createTestApp(t)
|
||||
app.StartEphemeralGCForTest(t)
|
||||
|
||||
user := app.state.CreateUserForTest("eph-gc-delete-user")
|
||||
pak, err := app.state.CreatePreAuthKey(user.TypedID(), false, true, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
nodeKey := key.NewNode()
|
||||
|
||||
_, err = app.handleRegister(context.Background(), tailcfg.RegisterRequest{
|
||||
Auth: &tailcfg.RegisterResponseAuth{AuthKey: pak.Key},
|
||||
NodeKey: nodeKey.Public(),
|
||||
Hostinfo: &tailcfg.Hostinfo{Hostname: "eph-gc-delete-node"},
|
||||
Expiry: time.Now().Add(24 * time.Hour),
|
||||
}, key.NewMachine().Public())
|
||||
require.NoError(t, err)
|
||||
|
||||
node, ok := app.state.GetNodeByNodeKey(nodeKey.Public())
|
||||
require.True(t, ok)
|
||||
require.True(t, node.IsEphemeral(), "test sanity: node must be ephemeral")
|
||||
|
||||
// The state a disconnect leaves behind.
|
||||
app.ephemeralGC.Schedule(node.ID(), time.Hour)
|
||||
require.True(t, app.ephemeralGC.IsScheduled(node.ID()), "test sanity: GC timer must be armed")
|
||||
|
||||
api := registerAPIV2(t, app)
|
||||
require.Equal(t, http.StatusOK, api.Delete("/api/v2/device/"+node.ID().String()).Code)
|
||||
|
||||
assert.False(t, app.ephemeralGC.IsScheduled(node.ID()),
|
||||
"deleting a node must cancel its ephemeral GC timer")
|
||||
}
|
||||
|
||||
// TestGitHubIssue3129_TransientlyBlockedWriteDoesNotLeaveLiveStaleSession
|
||||
// tests the scenario reported in
|
||||
// https://github.com/juanfont/headscale/issues/3129.
|
||||
|
||||
Reference in new issue
Block a user