fix(node): let a node change only the clients it alone hosts

A node snapshot was merged into the master's client records by email, and
the only ownership check filtered one derived copy of it: the node's
settings were still stored and re-synced later, its stats still merged,
and a record linked to the master as well as the node took every value
the node reported, including credentials rendered into the master's own
Xray config.

Each snapshot now passes one gate before anything is written. A record
also linked to the master or another node keeps the master's values
apart from per-link fields and the lifecycle the merge already settles;
a record linked only elsewhere, a detached record, a padded or
case-variant email and a new client holding another client's subId are
dropped from the stored settings as well. A soft-orphaned record can be
taken back only by the node whose merge orphaned it, which the new
clients.sync_orphaned_by column records. A node tunnel no longer owns
the peer keys of a client a master-local tunnel also serves.
This commit is contained in:
MHSanaei
2026-11-01 00:02:12 +01:00
parent f66e369e41
commit 7907843941
11 changed files with 623 additions and 96 deletions
+4 -1
View File
@@ -405,7 +405,10 @@ func migrateSyncOrphanColumns() error {
if !db.Migrator().HasColumn(&model.ClientRecord{}, "sync_orphaned_at") {
return nil
}
return db.Exec("UPDATE clients SET sync_orphaned_at = 0 WHERE sync_orphaned_at IS NULL").Error
if err := db.Exec("UPDATE clients SET sync_orphaned_at = 0 WHERE sync_orphaned_at IS NULL").Error; err != nil {
return err
}
return db.Exec("UPDATE clients SET sync_orphaned_by = 0 WHERE sync_orphaned_by IS NULL").Error
}
// The client identity checks match emails case-insensitively; without an
+2
View File
@@ -1018,6 +1018,8 @@ type ClientRecord struct {
// Owned solely by the node-snapshot sweep, which soft-orphans instead of
// deleting; orphans from any other cause stay at zero and are never reaped.
SyncOrphanedAt int64 `json:"-" gorm:"column:sync_orphaned_at;default:0"`
// The node whose merge orphaned the record: only that node may take it back.
SyncOrphanedBy int `json:"-" gorm:"column:sync_orphaned_by;default:0"`
}
func (ClientRecord) TableName() string { return "clients" }