mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-25 15:25:01 +03:00
fix(sub): close a deleted host binding only on the inbound it served
Deleting the only host a client was bound to blanked that client on every inbound. Once the host row was gone, the binding could no longer say which inbound it guarded, so it failed closed everywhere: a client on inbounds A and B, bound to one host on A, lost B's link the moment that host was deleted, and nothing in the panel hinted at it. Every host delete (host groups, the externalProxy sync, inbound deletion, the depleted-inbound sweep and the orphan prune) now goes through database.DeleteHosts, which keeps each deleted host's inbound in host_tombstones in the same transaction. A binding to a deleted host closes only that inbound; ids that resolve to no host at all still fail closed everywhere. The Postgres Host step now runs the deletion cases as well.
This commit is contained in:
+35
-7
@@ -32,6 +32,7 @@ import (
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
@@ -78,6 +79,7 @@ func allModels() []any {
|
||||
&model.ClientGroup{},
|
||||
&model.InboundFallback{},
|
||||
&model.Host{},
|
||||
&model.HostTombstone{},
|
||||
&model.NodeClientTraffic{},
|
||||
&model.NodeClientIp{},
|
||||
&model.ClientGlobalTraffic{},
|
||||
@@ -873,7 +875,7 @@ func SyncHostsFromExternalProxy(tx *gorm.DB, inboundId int, oldStreamSettings, n
|
||||
continue
|
||||
}
|
||||
if i >= len(added) {
|
||||
if err := tx.Delete(host).Error; err != nil {
|
||||
if _, err := DeleteHosts(tx, "id = ?", host.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
@@ -1048,17 +1050,43 @@ func anyToNonEmptyStrings(v any) []string {
|
||||
}
|
||||
|
||||
func pruneOrphanedHosts() error {
|
||||
res := db.Exec("DELETE FROM hosts WHERE inbound_id NOT IN (SELECT id FROM inbounds)")
|
||||
if res.Error != nil {
|
||||
log.Printf("Error pruning orphaned hosts rows: %v", res.Error)
|
||||
return res.Error
|
||||
var pruned int64
|
||||
err := db.Transaction(func(tx *gorm.DB) error {
|
||||
var err error
|
||||
pruned, err = DeleteHosts(tx, "inbound_id NOT IN (SELECT id FROM inbounds)")
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Error pruning orphaned hosts rows: %v", err)
|
||||
return err
|
||||
}
|
||||
if res.RowsAffected > 0 {
|
||||
log.Printf("Pruned %d orphaned hosts row(s)", res.RowsAffected)
|
||||
if pruned > 0 {
|
||||
log.Printf("Pruned %d orphaned hosts row(s)", pruned)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteHosts deletes the hosts matching query and keeps each one's inbound in host_tombstones, in
|
||||
// the caller's transaction, so a client still bound to one fails closed on that inbound alone.
|
||||
func DeleteHosts(tx *gorm.DB, query any, args ...any) (int64, error) {
|
||||
var tombstones []model.HostTombstone
|
||||
if err := tx.Model(&model.Host{}).Select("id AS host_id, inbound_id").Where(query, args...).Scan(&tombstones).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(tombstones) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
// migrate-db resyncs the hosts sequence to MAX(id), so a deleted id can return; keep its latest inbound.
|
||||
if err := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "host_id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"inbound_id"}),
|
||||
}).CreateInBatches(&tombstones, 500).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
res := tx.Where(query, args...).Delete(&model.Host{})
|
||||
return res.RowsAffected, res.Error
|
||||
}
|
||||
|
||||
func pruneOrphanedClientInbounds() error {
|
||||
res := db.Exec("DELETE FROM client_inbounds WHERE inbound_id NOT IN (SELECT id FROM inbounds)")
|
||||
if res.Error != nil {
|
||||
|
||||
@@ -53,6 +53,7 @@ func migrationModels() []any {
|
||||
&model.ClientGroup{},
|
||||
&model.InboundFallback{},
|
||||
&model.Host{},
|
||||
&model.HostTombstone{},
|
||||
&model.NodeClientTraffic{},
|
||||
&model.NodeClientIp{},
|
||||
&model.ClientGlobalTraffic{},
|
||||
|
||||
@@ -1221,6 +1221,15 @@ type Host struct {
|
||||
|
||||
func (Host) TableName() string { return "hosts" }
|
||||
|
||||
// HostTombstone keeps the inbound a deleted host served, so a client still bound to that host
|
||||
// fails closed on that inbound alone.
|
||||
type HostTombstone struct {
|
||||
HostId int `json:"hostId" gorm:"primaryKey;autoIncrement:false;column:host_id"`
|
||||
InboundId int `json:"inboundId" gorm:"not null;column:inbound_id"`
|
||||
}
|
||||
|
||||
func (HostTombstone) TableName() string { return "host_tombstones" }
|
||||
|
||||
// KeepAliveSeconds returns a scalar value; ranges are emitted by native AWG config.
|
||||
func (c Client) KeepAliveSeconds() int {
|
||||
if c.KeepAlive == nil {
|
||||
|
||||
Reference in New Issue
Block a user