mirror of
https://github.com/juanfont/headscale.git
synced 2026-11-04 04:34:07 +09:00
db, state: keep the ephemeral flag a node was created with
The flag moves from the key to the node, so a login with another key no longer changes it. A user claiming a tagged node still clears it. v1 nodes carry the flag. Fixes #3640
This commit is contained in:
1 parent
59f9f78bdd
commit
e99ad44064
22 files changed
+183
-57
No files matched your search
@@ -50,6 +50,7 @@ type Node struct {
|
||||
RegisterMethod string `enum:"REGISTER_METHOD_UNSPECIFIED,REGISTER_METHOD_AUTH_KEY,REGISTER_METHOD_CLI,REGISTER_METHOD_OIDC" json:"registerMethod"`
|
||||
GivenName string `json:"givenName"`
|
||||
Online bool `json:"online"`
|
||||
Ephemeral bool `json:"ephemeral"`
|
||||
ApprovedRoutes []string `json:"approvedRoutes" nullable:"false"`
|
||||
AvailableRoutes []string `json:"availableRoutes" nullable:"false"`
|
||||
SubnetRoutes []string `json:"subnetRoutes" nullable:"false"`
|
||||
@@ -604,6 +605,7 @@ func nodeFromView(view types.NodeView) Node {
|
||||
RegisterMethod: registerMethodEnum(view.RegisterMethod()),
|
||||
GivenName: view.GivenName(),
|
||||
Online: view.IsOnline().Valid() && view.IsOnline().Get(),
|
||||
Ephemeral: view.IsEphemeral(),
|
||||
ApprovedRoutes: nonNilStrings(util.PrefixesToString(view.ApprovedRoutes().AsSlice())),
|
||||
AvailableRoutes: nonNilStrings(util.PrefixesToString(view.AnnouncedRoutes())),
|
||||
SubnetRoutes: []string{},
|
||||
|
||||
@@ -119,6 +119,43 @@ func TestAPIV1NodeGet(t *testing.T) {
|
||||
assert.Contains(t, got.Node, "preAuthKey")
|
||||
assert.Contains(t, got.Node, "createdAt")
|
||||
})
|
||||
|
||||
// A login with a key of the other kind keeps the node's flag; the
|
||||
// embedded key shows the key's own.
|
||||
t.Run("ephemeral is the node's, not its key's", func(t *testing.T) {
|
||||
for _, created := range []bool{true, false} {
|
||||
h := newAPIV1Harness(t)
|
||||
user := h.app.state.CreateUserForTest("dave")
|
||||
machineKey := key.NewMachine()
|
||||
|
||||
for _, ephemeral := range []bool{created, !created} {
|
||||
pak, err := h.app.state.CreatePreAuthKey(user.TypedID(), false, ephemeral, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = h.app.handleRegisterWithAuthKey(tailcfg.RegisterRequest{
|
||||
Auth: &tailcfg.RegisterResponseAuth{AuthKey: pak.Key},
|
||||
NodeKey: key.NewNode().Public(),
|
||||
Hostinfo: &tailcfg.Hostinfo{Hostname: "node-d"},
|
||||
}, machineKey.Public())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
res := h.callHuma(http.MethodGet, "/api/v1/node/1", nil)
|
||||
require.Equal(t, http.StatusOK, res.status)
|
||||
|
||||
var got struct {
|
||||
Node struct {
|
||||
Ephemeral bool `json:"ephemeral"`
|
||||
PreAuthKey struct {
|
||||
Ephemeral bool `json:"ephemeral"`
|
||||
} `json:"preAuthKey"`
|
||||
} `json:"node"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(res.body, &got))
|
||||
assert.Equal(t, created, got.Node.Ephemeral, "node created ephemeral=%v", created)
|
||||
assert.Equal(t, !created, got.Node.PreAuthKey.Ephemeral, "node created ephemeral=%v", created)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIV1NodeList(t *testing.T) {
|
||||
|
||||
+2
-2
@@ -191,8 +191,8 @@ func NewHeadscale(cfg *types.Config) (*Headscale, error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Ephemerality can change after the timer is armed: re-registration
|
||||
// with a permanent key, or a user claiming a tagged node.
|
||||
// A user claiming a tagged node can clear its ephemeral flag after
|
||||
// the timer is armed.
|
||||
if !node.IsEphemeral() {
|
||||
log.Debug().Caller().EmbedObject(node).
|
||||
Msg("node is no longer ephemeral, skipping garbage collection")
|
||||
|
||||
@@ -952,6 +952,30 @@ func TestRevokedKeysBackingNodesSurviveCollection(t *testing.T) {
|
||||
assert.ElementsMatch(t, []types.NodeID{3, 5}, ids)
|
||||
}
|
||||
|
||||
// TestNodeEphemeralMigrationRerunKeepsFlags simulates a start that committed
|
||||
// the node ephemeral migration but died before gormigrate recorded it, after
|
||||
// which node 3 was made permanent: the next start must neither fail on the
|
||||
// existing column nor backfill node 3 again.
|
||||
func TestNodeEphemeralMigrationRerunKeepsFlags(t *testing.T) {
|
||||
dbPath := t.TempDir() + "/headscale_test.db"
|
||||
|
||||
require.NoError(t, createSQLiteFromSQLFile("testdata/sqlite/headscale_0.29.3_dump.sql", dbPath))
|
||||
|
||||
hsdb, err := NewHeadscaleDatabase(sqliteTestConfig(dbPath))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, hsdb.DB.Exec(`UPDATE nodes SET ephemeral = false WHERE id = 3`).Error)
|
||||
require.NoError(t, hsdb.DB.Exec(`DELETE FROM migrations WHERE id = '202610211300-node-ephemeral'`).Error)
|
||||
require.NoError(t, hsdb.Close())
|
||||
|
||||
hsdb, err = NewHeadscaleDatabase(sqliteTestConfig(dbPath))
|
||||
require.NoError(t, err)
|
||||
|
||||
ephemeral, err := hsdb.ListEphemeralNodes()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ephemeral, 1)
|
||||
assert.Equal(t, types.NodeID(5), ephemeral[0].ID)
|
||||
}
|
||||
|
||||
// TestInterruptedInitSchemaRecovers simulates a first start that created the
|
||||
// schema but died before gormigrate recorded the migrations, and asserts the
|
||||
// next start completes instead of failing on the existing credentials table.
|
||||
|
||||
@@ -97,9 +97,7 @@ func (hsdb *HSDatabase) ListEphemeralNodes() (types.Nodes, error) {
|
||||
return Read(hsdb.DB, func(rx *gorm.DB) (types.Nodes, error) {
|
||||
nodes := types.Nodes{}
|
||||
|
||||
err := rx.Joins("AuthKey").
|
||||
Where(`"AuthKey"."ephemeral" = true AND "AuthKey"."kind" = ?`, types.CredentialPreAuthKey).
|
||||
Find(&nodes).Error
|
||||
err := rx.Joins("AuthKey").Where("nodes.ephemeral = true").Find(&nodes).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -557,6 +557,7 @@ func TestListEphemeralNodes(t *testing.T) {
|
||||
UserID: &user.ID,
|
||||
RegisterMethod: util.RegisterMethodAuthKey,
|
||||
AuthKeyID: &pakEphID,
|
||||
Ephemeral: true,
|
||||
}
|
||||
|
||||
err = db.DB.Save(&node).Error
|
||||
|
||||
@@ -339,9 +339,9 @@ func (hsdb *HSDatabase) GetPreAuthKeyByID(id uint64) (*types.PreAuthKey, error)
|
||||
}
|
||||
|
||||
// DestroyPreAuthKey destroys a preauthkey. Returns error if the [types.PreAuthKey]
|
||||
// does not exist. Referenced ephemeral keys are revoked instead, because their
|
||||
// nodes need the key row to retain ephemerality. Other keys are removed along
|
||||
// with their nodes' auth_key_id references.
|
||||
// does not exist. Referenced ephemeral keys are revoked instead, so their
|
||||
// nodes keep the key they joined with. Other keys are removed along with their
|
||||
// nodes' auth_key_id references.
|
||||
func DestroyPreAuthKey(tx *gorm.DB, id uint64) error {
|
||||
return tx.Transaction(func(db *gorm.DB) error {
|
||||
res := db.Model(&types.Credential{}).
|
||||
@@ -421,8 +421,7 @@ func RevokePreAuthKey(tx *gorm.DB, id uint64) error {
|
||||
// DestroyRevokedPreAuthKeysBefore hard-deletes every key revoked before cutoff,
|
||||
// returning how many were removed. The background collector calls this to reap
|
||||
// soft-revoked keys after the retention window. Keys still referenced by a node
|
||||
// are kept: the node's ephemerality lives on its key, and revoking a key must
|
||||
// not change nodes already registered with it.
|
||||
// are kept, so revoking a key does not change nodes already registered with it.
|
||||
func (hsdb *HSDatabase) DestroyRevokedPreAuthKeysBefore(cutoff time.Time) (int, error) {
|
||||
var count int
|
||||
|
||||
|
||||
@@ -124,6 +124,7 @@ func TestDestroyPreAuthKeyKeepsReferencedEphemeralKey(t *testing.T) {
|
||||
|
||||
node := &types.Node{
|
||||
Hostname: "ephemeral-key-node", UserID: &user.ID, AuthKeyID: &pak.ID,
|
||||
Ephemeral: true,
|
||||
}
|
||||
require.NoError(t, db.DB.Create(node).Error)
|
||||
require.NoError(t, db.DeletePreAuthKey(pak.ID))
|
||||
|
||||
@@ -29,6 +29,7 @@ func TestDestroyUserKeepsTaggedNodeEphemeral(t *testing.T) {
|
||||
Hostname: "tagged-ephemeral-node",
|
||||
Tags: []string{"tag:server"},
|
||||
AuthKeyID: &pak.ID,
|
||||
Ephemeral: true,
|
||||
}
|
||||
require.NoError(t, db.DB.Create(node).Error)
|
||||
require.NoError(t, db.DestroyUser(types.UserID(user.ID)))
|
||||
|
||||
@@ -142,7 +142,7 @@ func TestHandleVerifyRequest_AdmitsByNodeKey(t *testing.T) {
|
||||
app.state.PutNodeInStoreForTest(*tagged)
|
||||
|
||||
ephemeral := app.state.CreateNodeForTest(user, "ephemeral")
|
||||
ephemeral.AuthKey = &types.Credential{Ephemeral: true}
|
||||
ephemeral.Ephemeral = true
|
||||
app.state.PutNodeInStoreForTest(*ephemeral)
|
||||
|
||||
expired := app.state.CreateNodeForTest(user, "expired")
|
||||
|
||||
+12
-4
@@ -501,9 +501,10 @@ func TestStaleGCTimerSparesPermanentNode(t *testing.T) {
|
||||
app.cfg.Node.Ephemeral.InactivityTimeout = time.Hour
|
||||
app.StartEphemeralGCForTest(t)
|
||||
user := app.state.CreateUserForTest("gc-permanent-user")
|
||||
ephemeral, err := app.state.CreatePreAuthKey(user.TypedID(), true, true, nil, nil)
|
||||
require.NoError(t, app.state.UpdatePolicyManagerUsersForTest())
|
||||
_, err := app.state.SetPolicy([]byte(`{"tagOwners":{"tag:server":["gc-permanent-user@"]}}`))
|
||||
require.NoError(t, err)
|
||||
permanent, err := app.state.CreatePreAuthKey(user.TypedID(), true, false, nil, nil)
|
||||
ephemeral, err := app.state.CreatePreAuthKey(user.TypedID(), true, true, nil, []string{"tag:server"})
|
||||
require.NoError(t, err)
|
||||
|
||||
machineKey := key.NewMachine().Public()
|
||||
@@ -523,8 +524,15 @@ func TestStaleGCTimerSparesPermanentNode(t *testing.T) {
|
||||
_, epoch := app.state.Connect(node.ID())
|
||||
app.ephemeralGC.Cancel(node.ID())
|
||||
|
||||
req.Auth.AuthKey = permanent.Key
|
||||
_, err = app.handleRegisterWithAuthKey(req, machineKey)
|
||||
// The user claiming the tagged node makes it permanent.
|
||||
authID := types.MustAuthID()
|
||||
require.NoError(t, app.state.SetAuthCacheEntry(authID, types.NewRegisterAuthRequest(&types.RegistrationData{
|
||||
MachineKey: machineKey,
|
||||
NodeKey: key.NewNode().Public(),
|
||||
Hostname: "gc-permanent-node",
|
||||
Hostinfo: &tailcfg.Hostinfo{Hostname: "gc-permanent-node"},
|
||||
})))
|
||||
_, _, err = app.state.HandleNodeFromAuthPath(authID, types.UserID(user.ID), nil, "webauth")
|
||||
require.NoError(t, err)
|
||||
|
||||
fresh, ok := app.state.GetNodeByID(node.ID())
|
||||
|
||||
@@ -42,23 +42,12 @@ var lifecycleSkipReasons = map[string]string{
|
||||
"ephemeral-lifecycle/E3-key-delete/2/keys": deletedKeyListed,
|
||||
"ephemeral-lifecycle/E3-key-delete/3/keys": deletedKeyListed,
|
||||
"ephemeral-lifecycle/E3-key-delete/4/status": deletedKeyDeleteNotFound,
|
||||
|
||||
"ephemeral-lifecycle/E4-reauth-other-key/eph-to-perm/ephemeral": ephemeralFollowsKey,
|
||||
"ephemeral-lifecycle/E4-reauth-other-key/perm-to-eph/ephemeral": ephemeralFollowsKey,
|
||||
|
||||
"ephemeral-lifecycle/collection/eph-to-perm/left": ephemeralFollowsKey,
|
||||
"ephemeral-lifecycle/collection/eph-to-perm/removed-observer": ephemeralFollowsKey,
|
||||
"ephemeral-lifecycle/collection/eph-to-perm/present-observer": ephemeralFollowsKey,
|
||||
"ephemeral-lifecycle/collection/perm-to-eph/left": ephemeralFollowsKey,
|
||||
"ephemeral-lifecycle/collection/perm-to-eph/removed-observer": ephemeralFollowsKey,
|
||||
"ephemeral-lifecycle/collection/perm-to-eph/present-observer": ephemeralFollowsKey,
|
||||
}
|
||||
|
||||
const (
|
||||
logoutAnswerUnauthorized = "LOGOUT_ANSWER_UNAUTHORIZED: headscale answers the logout that deletes an ephemeral node with MachineAuthorized false; SaaS answers true"
|
||||
deletedKeyListed = "DELETED_KEY_LISTED: headscale lists a deleted auth key; SaaS leaves it out of both key lists, while a read by ID still answers"
|
||||
deletedKeyDeleteNotFound = "DELETED_KEY_DELETE_NOT_FOUND: headscale answers 404 to a second delete of an auth key; SaaS answers 200"
|
||||
ephemeralFollowsKey = "EPHEMERAL_FOLLOWS_KEY: headscale takes a node's ephemeral flag from the key it last logged in with; SaaS keeps the flag the device was created with, so it is collected, or kept, as created"
|
||||
)
|
||||
|
||||
// TestRouteApprovalCompat replays a user-owned router through admin
|
||||
|
||||
@@ -814,6 +814,7 @@ func TestTaggedReauthUntagClearsEphemeralAuthKey(t *testing.T) {
|
||||
nd.RegisterMethod = util.RegisterMethodAuthKey
|
||||
nd.AuthKey = pak.AsCredential()
|
||||
nd.AuthKeyID = &pak.ID
|
||||
nd.Ephemeral = pak.Ephemeral
|
||||
})
|
||||
require.True(t, ok)
|
||||
require.True(t, seeded.IsEphemeral(), "precondition: node is ephemeral (tagged ephemeral key)")
|
||||
@@ -1552,17 +1553,15 @@ func newRetagTestState(t *testing.T) *State {
|
||||
return s
|
||||
}
|
||||
|
||||
// TestPreAuthKeyReauthPersistsAuthKeyID pins the root cause behind the
|
||||
// ephemeral-flag revert bug found while auditing #3370: on re-registration
|
||||
// with a *different* key, HandleNodeFromPreAuthKey updates node.AuthKeyID in
|
||||
// the NodeStore (state.go, in the in-place closure) but AuthKeyID is excluded
|
||||
// from nodeUpdateColumns (added for #2862 to avoid persisting a *deleted* key's
|
||||
// stale reference on MapRequest). The exclusion is correct for MapRequest, but
|
||||
// on the re-registration path the presented key is freshly loaded and valid, so
|
||||
// the new auth_key_id must be persisted. Otherwise a control-plane restart
|
||||
// reloads the OLD key, and any key-scoped property (notably Ephemeral) silently
|
||||
// reverts — an ephemeral->non-ephemeral re-auth leaves a node that looks
|
||||
// persistent in memory but is ephemeral again after restart and can be GC'd.
|
||||
// TestPreAuthKeyReauthPersistsAuthKeyID pins a bug found while working on #3370:
|
||||
// on re-registration with a *different* key, HandleNodeFromPreAuthKey updates
|
||||
// node.AuthKeyID in the NodeStore (state.go, in the in-place closure) but
|
||||
// AuthKeyID is excluded from nodeUpdateColumns (added for #2862 to avoid
|
||||
// persisting a *deleted* key's stale reference on MapRequest). The exclusion is
|
||||
// correct for MapRequest, but on the re-registration path the presented key is
|
||||
// freshly loaded and valid, so the new auth_key_id must be persisted. Otherwise
|
||||
// a control-plane restart reloads the OLD key. The ephemeral flag is the node's
|
||||
// own and stays as the node was created, before and after the restart.
|
||||
//
|
||||
// Drives the real handler twice, then reopens the DB to prove the association
|
||||
// survives the reload.
|
||||
@@ -1610,8 +1609,8 @@ func TestPreAuthKeyReauthPersistsAuthKeyID(t *testing.T) {
|
||||
require.NotNil(t, second.AuthKeyID().Get())
|
||||
require.Equal(t, keyB.ID, second.AuthKeyID().Get(),
|
||||
"NodeStore must reference the key just used to re-authenticate")
|
||||
require.False(t, second.IsEphemeral(),
|
||||
"re-auth with a non-ephemeral key must make the node non-ephemeral")
|
||||
require.True(t, second.IsEphemeral(),
|
||||
"re-auth with another key keeps the ephemeral flag the node was created with")
|
||||
|
||||
// Restart the control plane: reload state from the database only.
|
||||
require.NoError(t, s.Close())
|
||||
@@ -1623,14 +1622,11 @@ func TestPreAuthKeyReauthPersistsAuthKeyID(t *testing.T) {
|
||||
reloaded, ok := s2.GetNodeByID(nodeID)
|
||||
require.True(t, ok, "node must reload from DB after restart")
|
||||
|
||||
// This is the assertion that fails today: auth_key_id was never persisted on
|
||||
// re-registration, so the reload resurrects KEY A and the node is ephemeral
|
||||
// again — at risk of GC on the next disconnect.
|
||||
require.NotNil(t, reloaded.AuthKeyID().Get())
|
||||
require.Equal(t, keyB.ID, reloaded.AuthKeyID().Get(),
|
||||
"auth_key_id must persist across restart, not revert to the old key")
|
||||
require.False(t, reloaded.IsEphemeral(),
|
||||
"ephemerality must not silently revert after a control-plane restart")
|
||||
require.True(t, reloaded.IsEphemeral(),
|
||||
"the ephemeral flag must survive a control-plane restart")
|
||||
}
|
||||
|
||||
// TestTaggedNodeCanHaveKeyExpiry matches Tailscale: a tagged node has key
|
||||
@@ -2245,6 +2241,63 @@ func TestPAKReregisterRollbackKeepsPriorWrite(t *testing.T) {
|
||||
require.Equal(t, c.regReq.NodeKey, ns.NodeKey)
|
||||
}
|
||||
|
||||
// TestPAKClaimOfTaggedNodeClearsEphemeral: a user's key taking an
|
||||
// admin-tagged node back makes it a permanent device, as an untagging login
|
||||
// does. A claim the database rejects leaves the flag as it was.
|
||||
func TestPAKClaimOfTaggedNodeClearsEphemeral(t *testing.T) {
|
||||
s := newRetagTestState(t)
|
||||
user := s.CreateUserForTest("claim-user")
|
||||
|
||||
k0, err := s.CreatePreAuthKey(user.TypedID(), true, true, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
c := registerForPAKRereg(t, s, k0)
|
||||
_, err = s.SetPolicy([]byte(`{"tagOwners":{"tag:prod":["claim-user@"]}}`))
|
||||
require.NoError(t, err)
|
||||
_, _, err = s.SetNodeTags(c.node.ID(), []string{"tag:prod"})
|
||||
require.NoError(t, err)
|
||||
|
||||
k, err := s.CreatePreAuthKey(user.TypedID(), false, false, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
regReq := c.regReq
|
||||
regReq.Auth = &tailcfg.RegisterResponseAuth{AuthKey: k.Key}
|
||||
regReq.NodeKey = key.NewNode().Public()
|
||||
|
||||
view, pak := c.lookup(t, k.Key)
|
||||
|
||||
// Another registration consumes the key after the lookup read its row, so
|
||||
// the transaction's compare-and-set fails.
|
||||
err = s.db.Write(func(tx *gorm.DB) error {
|
||||
return db.UsePreAuthKey(tx, &types.PreAuthKey{ID: pak.ID})
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = c.reregister(view, pak, regReq)
|
||||
require.ErrorIs(t, err, types.PAKError("authkey already used"))
|
||||
|
||||
node, ok := s.GetNodeByID(c.node.ID())
|
||||
require.True(t, ok)
|
||||
require.True(t, node.IsTagged(), "rollback keeps the node tagged")
|
||||
require.True(t, node.IsEphemeral(), "rollback keeps the flag")
|
||||
|
||||
k2, err := s.CreatePreAuthKey(user.TypedID(), true, false, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
regReq.Auth = &tailcfg.RegisterResponseAuth{AuthKey: k2.Key}
|
||||
_, _, err = s.HandleNodeFromPreAuthKey(regReq, c.machineKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
node, ok = s.GetNodeByID(c.node.ID())
|
||||
require.True(t, ok)
|
||||
require.False(t, node.IsTagged())
|
||||
require.False(t, node.IsEphemeral())
|
||||
|
||||
row, err := s.db.GetNodeByID(c.node.ID())
|
||||
require.NoError(t, err)
|
||||
require.False(t, row.Ephemeral)
|
||||
}
|
||||
|
||||
// TestPAKReregisterConcurrentAdminExpire races a same-key restart with a spent
|
||||
// key against an admin expiry. Either serial order leaves the node expired.
|
||||
func TestPAKReregisterConcurrentAdminExpire(t *testing.T) {
|
||||
|
||||
@@ -314,7 +314,7 @@ func (s *State) DebugOverviewJSON() DebugOverviewInfo {
|
||||
info.Nodes.Expired++
|
||||
}
|
||||
|
||||
if node.AuthKey().Valid() && node.AuthKey().Ephemeral() {
|
||||
if node.IsEphemeral() {
|
||||
info.Nodes.Ephemeral++
|
||||
}
|
||||
}
|
||||
|
||||
+17
-11
@@ -119,6 +119,7 @@ var nodeUpdateColumns = []string{
|
||||
"UserID",
|
||||
"RegisterMethod",
|
||||
"Tags",
|
||||
"Ephemeral",
|
||||
"Expiry",
|
||||
"LastSeen",
|
||||
"ApprovedRoutes",
|
||||
@@ -1083,8 +1084,7 @@ func (s *State) ListEphemeralNodes() views.Slice[types.NodeView] {
|
||||
var ephemeralNodes []types.NodeView
|
||||
|
||||
for _, node := range allNodes.All() {
|
||||
// Check if node is ephemeral by checking its AuthKey
|
||||
if node.AuthKey().Valid() && node.AuthKey().Ephemeral() {
|
||||
if node.IsEphemeral() {
|
||||
ephemeralNodes = append(ephemeralNodes, node)
|
||||
}
|
||||
}
|
||||
@@ -2511,8 +2511,8 @@ func (s *State) applyAuthNodeUpdate(params authNodeUpdateParams) (types.NodeView
|
||||
// AuthKeyID is excluded from nodeUpdateColumns (#2862: never persist a
|
||||
// possibly-deleted key's stale reference on the shared update path). But
|
||||
// when a re-auth untags a node it clears AuthKeyID to nil (above), and that
|
||||
// must persist or the node reloads as tagged/ephemeral after a restart and
|
||||
// is garbage-collected. Writing NULL can never cause an FK error, so include
|
||||
// must persist or the node reloads bound to the tagged key after a
|
||||
// restart. Writing NULL can never cause an FK error, so include
|
||||
// the column only in that clearing case; the other transitions keep the
|
||||
// #2862-safe column set untouched.
|
||||
updateColumns := nodeUpdateColumns
|
||||
@@ -2602,6 +2602,7 @@ func (s *State) createAndSaveNewNode(params newNodeParams) (types.NodeView, erro
|
||||
|
||||
nodeToRegister.AuthKey = params.PreAuthKey.AsCredential()
|
||||
nodeToRegister.AuthKeyID = ¶ms.PreAuthKey.ID
|
||||
nodeToRegister.Ephemeral = params.PreAuthKey.Ephemeral
|
||||
} else {
|
||||
// Non-PreAuthKey registration (OIDC, CLI) - always user-owned
|
||||
nodeToRegister.UserID = ¶ms.User.ID
|
||||
@@ -2798,14 +2799,13 @@ func (s *State) processReauthTags(
|
||||
node.User = user
|
||||
|
||||
// The node is no longer tagged, so it must not keep a reference to
|
||||
// the tagged auth key. Leaving AuthKey set means a node created by a
|
||||
// tagged+ephemeral key stays IsEphemeral() after converting to
|
||||
// user-owned and is garbage-collected on its next disconnect,
|
||||
// silently deleting the user's just-claimed device. Clearing the
|
||||
// reference is persisted via the AuthKeyID column added to this
|
||||
// path's write below.
|
||||
// the tagged auth key, nor its ephemeral flag: an ephemeral node is
|
||||
// garbage-collected on its next disconnect, silently deleting the
|
||||
// user's just-claimed device. Clearing the reference is persisted
|
||||
// via the AuthKeyID column added to this path's write below.
|
||||
node.AuthKey = nil
|
||||
node.AuthKeyID = nil
|
||||
node.Ephemeral = false
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -3586,10 +3586,12 @@ func (s *State) reregisterNodeWithPAK(
|
||||
prior.NodeKey != regReq.NodeKey {
|
||||
// A user key that speaks for a tagged node, with a new node key,
|
||||
// takes it back as that user's device, as on Tailscale SaaS. A
|
||||
// relogin under the same node key keeps it tagged.
|
||||
// relogin under the same node key keeps it tagged. As at a login
|
||||
// that untags it, the claimed device is not ephemeral.
|
||||
node.Tags = nil
|
||||
node.UserID = &pak.User.ID
|
||||
node.User = pak.User
|
||||
node.Ephemeral = false
|
||||
}
|
||||
|
||||
node.AuthKey = pak.AsCredential()
|
||||
@@ -3717,6 +3719,10 @@ func (s *State) undoReregistration(prior, written *types.Node) {
|
||||
n.User = prior.User
|
||||
}
|
||||
|
||||
if n.Ephemeral == written.Ephemeral {
|
||||
n.Ephemeral = prior.Ephemeral
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(n.Expiry, written.Expiry) {
|
||||
n.Expiry = prior.Expiry
|
||||
}
|
||||
|
||||
+1
@@ -6,6 +6,7 @@
|
||||
"availableRoutes": [],
|
||||
"createdAt": "\u003ctimestamp\u003e",
|
||||
"discoKey": "\u003csecret\u003e",
|
||||
"ephemeral": false,
|
||||
"expiry": null,
|
||||
"givenName": "regnode",
|
||||
"id": "1",
|
||||
|
||||
+1
@@ -6,6 +6,7 @@
|
||||
"availableRoutes": [],
|
||||
"createdAt": "\u003ctimestamp\u003e",
|
||||
"discoKey": "\u003csecret\u003e",
|
||||
"ephemeral": false,
|
||||
"expiry": null,
|
||||
"givenName": "node-a",
|
||||
"id": "1",
|
||||
|
||||
+1
@@ -6,6 +6,7 @@
|
||||
"availableRoutes": [],
|
||||
"createdAt": "\u003ctimestamp\u003e",
|
||||
"discoKey": "\u003csecret\u003e",
|
||||
"ephemeral": false,
|
||||
"expiry": null,
|
||||
"givenName": "node-b",
|
||||
"id": "1",
|
||||
|
||||
+2
@@ -7,6 +7,7 @@
|
||||
"availableRoutes": [],
|
||||
"createdAt": "\u003ctimestamp\u003e",
|
||||
"discoKey": "\u003csecret\u003e",
|
||||
"ephemeral": false,
|
||||
"expiry": null,
|
||||
"givenName": "node-a",
|
||||
"id": "1",
|
||||
@@ -55,6 +56,7 @@
|
||||
"availableRoutes": [],
|
||||
"createdAt": "\u003ctimestamp\u003e",
|
||||
"discoKey": "\u003csecret\u003e",
|
||||
"ephemeral": false,
|
||||
"expiry": null,
|
||||
"givenName": "node-b",
|
||||
"id": "2",
|
||||
|
||||
+1
@@ -7,6 +7,7 @@
|
||||
"availableRoutes": [],
|
||||
"createdAt": "\u003ctimestamp\u003e",
|
||||
"discoKey": "\u003csecret\u003e",
|
||||
"ephemeral": false,
|
||||
"expiry": null,
|
||||
"givenName": "node-a",
|
||||
"id": "1",
|
||||
|
||||
+1
@@ -6,6 +6,7 @@
|
||||
"availableRoutes": [],
|
||||
"createdAt": "\u003ctimestamp\u003e",
|
||||
"discoKey": "\u003csecret\u003e",
|
||||
"ephemeral": false,
|
||||
"expiry": null,
|
||||
"givenName": "registered-node",
|
||||
"id": "1",
|
||||
|
||||
@@ -276,7 +276,7 @@ func (node *Node) ShouldBeOnline() bool {
|
||||
// IsEphemeral returns if the node is registered as an Ephemeral node.
|
||||
// https://tailscale.com/docs/features/ephemeral-nodes
|
||||
func (node *Node) IsEphemeral() bool {
|
||||
return node.AuthKey != nil && node.AuthKey.Ephemeral
|
||||
return node.Ephemeral
|
||||
}
|
||||
|
||||
// IPs returns the node's allocated Tailscale addresses. Order is
|
||||
|
||||
Reference in new issue
Block a user