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:
Kristoffer Dalby committed 2026-11-02 17:46:53 +00:00
1 parent 5827e4b73c
commit bccfb46ab1
22 files changed
+183 -57

No files matched your search

+2
View File
@@ -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{},
+37
View File
@@ -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
View File
@@ -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")
+24
View File
@@ -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.
+1 -3
View File
@@ -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
}
+1
View File
@@ -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
+4 -5
View File
@@ -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
+1
View File
@@ -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))
+1
View File
@@ -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)))
+1 -1
View File
@@ -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
View File
@@ -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
+71 -18
View File
@@ -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) {
+1 -1
View File
@@ -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
View File
@@ -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 = &params.PreAuthKey.ID
nodeToRegister.Ephemeral = params.PreAuthKey.Ephemeral
} else {
// Non-PreAuthKey registration (OIDC, CLI) - always user-owned
nodeToRegister.UserID = &params.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
}
@@ -6,6 +6,7 @@
"availableRoutes": [],
"createdAt": "\u003ctimestamp\u003e",
"discoKey": "\u003csecret\u003e",
"ephemeral": false,
"expiry": null,
"givenName": "regnode",
"id": "1",
+1
View File
@@ -6,6 +6,7 @@
"availableRoutes": [],
"createdAt": "\u003ctimestamp\u003e",
"discoKey": "\u003csecret\u003e",
"ephemeral": false,
"expiry": null,
"givenName": "node-a",
"id": "1",
@@ -6,6 +6,7 @@
"availableRoutes": [],
"createdAt": "\u003ctimestamp\u003e",
"discoKey": "\u003csecret\u003e",
"ephemeral": false,
"expiry": null,
"givenName": "node-b",
"id": "1",
+2
View File
@@ -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",
@@ -7,6 +7,7 @@
"availableRoutes": [],
"createdAt": "\u003ctimestamp\u003e",
"discoKey": "\u003csecret\u003e",
"ephemeral": false,
"expiry": null,
"givenName": "node-a",
"id": "1",
@@ -6,6 +6,7 @@
"availableRoutes": [],
"createdAt": "\u003ctimestamp\u003e",
"discoKey": "\u003csecret\u003e",
"ephemeral": false,
"expiry": null,
"givenName": "registered-node",
"id": "1",
+1 -1
View File
@@ -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