From 169068d8fbe81d70c490cfd8af9ab866424a2edb Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Fri, 29 May 2026 01:41:52 +0200 Subject: [PATCH] fix(nodes): clean up orphaned client_inbounds on node inbound removal When a remote node disconnects or one of its inbounds vanishes from the traffic snapshot, setRemoteTrafficLocked deleted the central inbound row but left the client_inbounds join rows behind. Affected clients ended up linked to hundreds of phantom inbounds, and editing one then failed with "record not found" / "Load Old Data Error" because Update aborted on the first GetInbound miss. - Detach client_inbounds rows when deleting a vanished node inbound - Prune stale links during client Update instead of aborting the save - Drop orphaned client_inbounds rows on startup to heal existing DBs Closes #4636 --- database/db.go | 15 +++++++++++++++ web/service/client.go | 8 ++++++++ web/service/inbound.go | 3 +++ 3 files changed, 26 insertions(+) diff --git a/database/db.go b/database/db.go index 547d5e32..7aed0273 100644 --- a/database/db.go +++ b/database/db.go @@ -83,6 +83,21 @@ func initModels() error { return err } } + if err := pruneOrphanedClientInbounds(); err != nil { + return err + } + return nil +} + +func pruneOrphanedClientInbounds() error { + res := db.Exec("DELETE FROM client_inbounds WHERE inbound_id NOT IN (SELECT id FROM inbounds)") + if res.Error != nil { + log.Printf("Error pruning orphaned client_inbounds rows: %v", res.Error) + return res.Error + } + if res.RowsAffected > 0 { + log.Printf("Pruned %d orphaned client_inbounds row(s)", res.RowsAffected) + } return nil } diff --git a/web/service/client.go b/web/service/client.go index d8d01d4d..3307de42 100644 --- a/web/service/client.go +++ b/web/service/client.go @@ -629,6 +629,14 @@ func (s *ClientService) Update(inboundSvc *InboundService, id int, updated model for _, ibId := range inboundIds { inbound, getErr := inboundSvc.GetInbound(ibId) if getErr != nil { + if errors.Is(getErr, gorm.ErrRecordNotFound) { + if err := database.GetDB(). + Where("client_id = ? AND inbound_id = ?", id, ibId). + Delete(&model.ClientInbound{}).Error; err != nil { + return needRestart, err + } + continue + } return needRestart, getErr } oldKey := clientKeyForProtocol(inbound.Protocol, existing) diff --git a/web/service/inbound.go b/web/service/inbound.go index dba0504e..111eca39 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -1432,6 +1432,9 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi Delete(&xray.ClientTraffic{}).Error; err != nil { return false, err } + if err := s.clientService.DetachInbound(tx, c.Id); err != nil { + return false, err + } if err := tx.Where("id = ?", c.Id). Delete(&model.Inbound{}).Error; err != nil { return false, err