From e7fba93c54ff704eaac6d4e630b1c639f5bb3155 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Tue, 27 Oct 2026 15:04:17 +0000 Subject: [PATCH] db, types: add a column for auto-approved routes It starts empty: the current policy cannot tell which earlier approvals were automatic, so all of them stay as an admin's. --- hscontrol/db/db.go | 12 ++++++ hscontrol/db/db_test.go | 5 +++ hscontrol/db/schema.sql | 1 + hscontrol/state/node_store_test.go | 3 +- hscontrol/types/node.go | 5 +++ hscontrol/types/types_clone.go | 56 ++++++++++++++------------- hscontrol/types/types_view.go | 62 +++++++++++++++++------------- 7 files changed, 89 insertions(+), 55 deletions(-) diff --git a/hscontrol/db/db.go b/hscontrol/db/db.go index c606b5e1c..655e1570e 100644 --- a/hscontrol/db/db.go +++ b/hscontrol/db/db.go @@ -282,6 +282,18 @@ WHERE tags IS NOT NULL AND tags != '[]' AND tags != '' AND tags != 'null' }, Rollback: func(db *gorm.DB) error { return nil }, }, + { + // Records which approved routes only auto-approval granted. It + // starts empty: the current policy cannot tell which earlier + // approvals were automatic, so all of them stay as an admin's. + ID: "202610211200-node-auto-approved-routes", + Migrate: func(tx *gorm.DB) error { + return tx.Transaction(func(tx *gorm.DB) error { + return tx.Exec(`ALTER TABLE nodes ADD COLUMN auto_approved_routes text`).Error + }) + }, + Rollback: func(db *gorm.DB) error { return nil }, + }, }, ) diff --git a/hscontrol/db/db_test.go b/hscontrol/db/db_test.go index d3cb2d1d0..5bab06c6a 100644 --- a/hscontrol/db/db_test.go +++ b/hscontrol/db/db_test.go @@ -117,6 +117,11 @@ func TestSQLiteMigrationAndDataValidation(t *testing.T) { require.NoError(t, err) require.Len(t, nodes, 5) + // Approvals from before the upgrade carry no auto-approval mark. + for _, n := range nodes { + assert.Empty(t, n.AutoApprovedRoutes, "node %d", n.ID) + } + // Pre-auth key ids are preserved, so every node keeps its key. for _, n := range nodes { require.NotNil(t, n.AuthKeyID, "node %d", n.ID) diff --git a/hscontrol/db/schema.sql b/hscontrol/db/schema.sql index f6fbd45ae..e340a1b51 100644 --- a/hscontrol/db/schema.sql +++ b/hscontrol/db/schema.sql @@ -89,6 +89,7 @@ CREATE TABLE nodes( last_seen datetime, expiry datetime, approved_routes text, + auto_approved_routes text, created_at datetime, updated_at datetime, diff --git a/hscontrol/state/node_store_test.go b/hscontrol/state/node_store_test.go index 56c3f47a5..96dc22913 100644 --- a/hscontrol/state/node_store_test.go +++ b/hscontrol/state/node_store_test.go @@ -1960,7 +1960,8 @@ var nodeFieldImpact = map[string]string{ "ID": "relation", "IPv4": "relation", "IPv6": "relation", "UserID": "relation", "User": "relation", "Tags": "relation", "ApprovedRoutes": "relation", "Hostinfo": "relation", - "IsOnline": "election", "Unhealthy": "election", + "AutoApprovedRoutes": "payload", + "IsOnline": "election", "Unhealthy": "election", "MachineKey": "payload", "NodeKey": "payload", "DiscoKey": "payload", "Endpoints": "payload", "Hostname": "payload", "GivenName": "payload", "RegisterMethod": "payload", "AuthKeyID": "payload", "AuthKey": "payload", diff --git a/hscontrol/types/node.go b/hscontrol/types/node.go index 756b906aa..553b56a84 100644 --- a/hscontrol/types/node.go +++ b/hscontrol/types/node.go @@ -179,6 +179,11 @@ type Node struct { // See [Node.Hostinfo] ApprovedRoutes Prefixes `gorm:"column:approved_routes;serializer:json"` + // AutoApprovedRoutes are the approved routes only auto-approval granted. + // The node loses one when it stops announcing it; a route an admin + // approved stays. + AutoApprovedRoutes Prefixes `gorm:"column:auto_approved_routes;serializer:json"` + CreatedAt time.Time UpdatedAt time.Time DeletedAt *time.Time diff --git a/hscontrol/types/types_clone.go b/hscontrol/types/types_clone.go index 9b64cc3bf..d8586f9b3 100644 --- a/hscontrol/types/types_clone.go +++ b/hscontrol/types/types_clone.go @@ -71,6 +71,7 @@ func (src *Node) Clone() *Node { dst.LastSeen = new(*src.LastSeen) } dst.ApprovedRoutes = append(src.ApprovedRoutes[:0:0], src.ApprovedRoutes...) + dst.AutoApprovedRoutes = append(src.AutoApprovedRoutes[:0:0], src.AutoApprovedRoutes...) if dst.DeletedAt != nil { dst.DeletedAt = new(*src.DeletedAt) } @@ -82,33 +83,34 @@ func (src *Node) Clone() *Node { // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _NodeCloneNeedsRegeneration = Node(struct { - ID NodeID - MachineKey key.MachinePublic - NodeKey key.NodePublic - DiscoKey key.DiscoPublic - Endpoints AddrPorts - Hostinfo *tailcfg.Hostinfo - IPv4 *netip.Addr - IPv6 *netip.Addr - Hostname string - GivenName string - UserID *uint - User *User - RegisterMethod string - Tags Strings - AuthKeyID *uint64 - AuthKey *Credential - Expiry *time.Time - LastSeen *time.Time - ApprovedRoutes Prefixes - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt *time.Time - IsOnline *bool - Unhealthy bool - ActiveSessions int - SessionEpoch uint64 - SessionStart time.Time + ID NodeID + MachineKey key.MachinePublic + NodeKey key.NodePublic + DiscoKey key.DiscoPublic + Endpoints AddrPorts + Hostinfo *tailcfg.Hostinfo + IPv4 *netip.Addr + IPv6 *netip.Addr + Hostname string + GivenName string + UserID *uint + User *User + RegisterMethod string + Tags Strings + AuthKeyID *uint64 + AuthKey *Credential + Expiry *time.Time + LastSeen *time.Time + ApprovedRoutes Prefixes + AutoApprovedRoutes Prefixes + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt *time.Time + IsOnline *bool + Unhealthy bool + ActiveSessions int + SessionEpoch uint64 + SessionStart time.Time }{}) // Clone makes a deep copy of PreAuthKey. diff --git a/hscontrol/types/types_view.go b/hscontrol/types/types_view.go index ee8d0163f..1696b3153 100644 --- a/hscontrol/types/types_view.go +++ b/hscontrol/types/types_view.go @@ -252,6 +252,13 @@ func (v NodeView) LastSeen() views.ValuePointer[time.Time] { func (v NodeView) ApprovedRoutes() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.ApprovedRoutes) } + +// AutoApprovedRoutes are the approved routes only auto-approval granted. +// The node loses one when it stops announcing it; a route an admin +// approved stays. +func (v NodeView) AutoApprovedRoutes() views.Slice[netip.Prefix] { + return views.SliceOf(v.ж.AutoApprovedRoutes) +} func (v NodeView) CreatedAt() time.Time { return v.ж.CreatedAt } func (v NodeView) UpdatedAt() time.Time { return v.ж.UpdatedAt } func (v NodeView) DeletedAt() views.ValuePointer[time.Time] { @@ -291,33 +298,34 @@ func (v NodeView) String() string { return v.ж.String() } // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _NodeViewNeedsRegeneration = Node(struct { - ID NodeID - MachineKey key.MachinePublic - NodeKey key.NodePublic - DiscoKey key.DiscoPublic - Endpoints AddrPorts - Hostinfo *tailcfg.Hostinfo - IPv4 *netip.Addr - IPv6 *netip.Addr - Hostname string - GivenName string - UserID *uint - User *User - RegisterMethod string - Tags Strings - AuthKeyID *uint64 - AuthKey *Credential - Expiry *time.Time - LastSeen *time.Time - ApprovedRoutes Prefixes - CreatedAt time.Time - UpdatedAt time.Time - DeletedAt *time.Time - IsOnline *bool - Unhealthy bool - ActiveSessions int - SessionEpoch uint64 - SessionStart time.Time + ID NodeID + MachineKey key.MachinePublic + NodeKey key.NodePublic + DiscoKey key.DiscoPublic + Endpoints AddrPorts + Hostinfo *tailcfg.Hostinfo + IPv4 *netip.Addr + IPv6 *netip.Addr + Hostname string + GivenName string + UserID *uint + User *User + RegisterMethod string + Tags Strings + AuthKeyID *uint64 + AuthKey *Credential + Expiry *time.Time + LastSeen *time.Time + ApprovedRoutes Prefixes + AutoApprovedRoutes Prefixes + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt *time.Time + IsOnline *bool + Unhealthy bool + ActiveSessions int + SessionEpoch uint64 + SessionStart time.Time }{}) // View returns a read-only view of PreAuthKey.