diff --git a/hscontrol/db/db.go b/hscontrol/db/db.go index 655e1570e..372ffe008 100644 --- a/hscontrol/db/db.go +++ b/hscontrol/db/db.go @@ -294,6 +294,43 @@ WHERE tags IS NOT NULL AND tags != '[]' AND tags != '' AND tags != 'null' }, Rollback: func(db *gorm.DB) error { return nil }, }, + { + // Store a node's ephemeral flag on the node, taken from the + // key it references, so a login with another key keeps it. + ID: "202610211300-node-ephemeral", + Migrate: func(tx *gorm.DB) error { + // The column and backfill commit together, before gormigrate + // records the migration; a stop in between leaves both done. + if tx.Migrator().HasColumn(&types.Node{}, "ephemeral") { + return nil + } + + return tx.Transaction(func(tx *gorm.DB) error { + colType := "numeric" + if tx.Name() != "sqlite" { + colType = "boolean" + } + + err := tx.Exec(`ALTER TABLE nodes ADD COLUMN ephemeral ` + colType + ` DEFAULT false`).Error + if err != nil { + return fmt.Errorf("adding ephemeral to nodes: %w", err) + } + + err = tx.Exec(` +UPDATE nodes SET ephemeral = true +WHERE auth_key_id IN ( + SELECT id FROM credentials WHERE kind = ? AND ephemeral = true +); + `, types.CredentialPreAuthKey).Error + if err != nil { + return fmt.Errorf("backfilling ephemeral on nodes: %w", err) + } + + return nil + }) + }, + Rollback: func(db *gorm.DB) error { return nil }, + }, }, ) diff --git a/hscontrol/db/schema.sql b/hscontrol/db/schema.sql index e340a1b51..fa96bf502 100644 --- a/hscontrol/db/schema.sql +++ b/hscontrol/db/schema.sql @@ -86,6 +86,7 @@ CREATE TABLE nodes( register_method text, tags text, auth_key_id integer, + ephemeral numeric DEFAULT false, last_seen datetime, expiry datetime, approved_routes text, diff --git a/hscontrol/state/node_store_test.go b/hscontrol/state/node_store_test.go index 96dc22913..6c505cdc3 100644 --- a/hscontrol/state/node_store_test.go +++ b/hscontrol/state/node_store_test.go @@ -1965,7 +1965,8 @@ var nodeFieldImpact = map[string]string{ "MachineKey": "payload", "NodeKey": "payload", "DiscoKey": "payload", "Endpoints": "payload", "Hostname": "payload", "GivenName": "payload", "RegisterMethod": "payload", "AuthKeyID": "payload", "AuthKey": "payload", - "Expiry": "election", "LastSeen": "payload", "CreatedAt": "payload", + "Ephemeral": "payload", + "Expiry": "election", "LastSeen": "payload", "CreatedAt": "payload", "UpdatedAt": "payload", "DeletedAt": "payload", "ActiveSessions": "payload", "SessionEpoch": "payload", "SessionStart": "election", } diff --git a/hscontrol/types/node.go b/hscontrol/types/node.go index d8439bacd..d3be272a7 100644 --- a/hscontrol/types/node.go +++ b/hscontrol/types/node.go @@ -175,6 +175,11 @@ type Node struct { AuthKeyID *uint64 `sql:"DEFAULT:NULL"` AuthKey *Credential + // Ephemeral is taken from the key the node was created with. Logging in + // again with another key keeps it, as Tailscale does; a user claiming a + // tagged node clears it. + Ephemeral bool `gorm:"default:false"` + Expiry *time.Time // LastSeen is when the node was last in contact with diff --git a/hscontrol/types/types_clone.go b/hscontrol/types/types_clone.go index d8586f9b3..2177c7444 100644 --- a/hscontrol/types/types_clone.go +++ b/hscontrol/types/types_clone.go @@ -99,6 +99,7 @@ var _NodeCloneNeedsRegeneration = Node(struct { Tags Strings AuthKeyID *uint64 AuthKey *Credential + Ephemeral bool Expiry *time.Time LastSeen *time.Time ApprovedRoutes Prefixes diff --git a/hscontrol/types/types_view.go b/hscontrol/types/types_view.go index 1696b3153..8b78b6c95 100644 --- a/hscontrol/types/types_view.go +++ b/hscontrol/types/types_view.go @@ -236,7 +236,12 @@ func (v NodeView) Tags() views.Slice[string] { return views.SliceOf(v.ж.Tags) } // authkey), which is what auth_key_id references. func (v NodeView) AuthKeyID() views.ValuePointer[uint64] { return views.ValuePointerOf(v.ж.AuthKeyID) } -func (v NodeView) AuthKey() CredentialView { return v.ж.AuthKey.View() } +func (v NodeView) AuthKey() CredentialView { return v.ж.AuthKey.View() } + +// Ephemeral is taken from the key the node was created with. Logging in +// again with another key keeps it, as Tailscale does; a user claiming a +// tagged node clears it. +func (v NodeView) Ephemeral() bool { return v.ж.Ephemeral } func (v NodeView) Expiry() views.ValuePointer[time.Time] { return views.ValuePointerOf(v.ж.Expiry) } // LastSeen is when the node was last in contact with @@ -314,6 +319,7 @@ var _NodeViewNeedsRegeneration = Node(struct { Tags Strings AuthKeyID *uint64 AuthKey *Credential + Ephemeral bool Expiry *time.Time LastSeen *time.Time ApprovedRoutes Prefixes