mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-14 15:20:59 +00:00
fix(traffic): prevent phantom quota consumption from stale node data (#5412)
Three related bugs caused inflated traffic counters and spurious quota hits on multi-node setups, most visibly when a client email was renamed while a node was offline or its PostgreSQL deadlocked. **Fix 1 — phantom quota (root cause)** `setRemoteTrafficLocked` new-row path: when master had no `client_traffics` row for an email that a node reported, it seeded the row with `Up: cs.Up` — importing the node's full accumulated counter as if it were fresh quota usage. If the node retained stale data from a previously-deleted account (e.g. a failed deletion during an outage), the ghost 50 GB appeared on the new client immediately and triggered `disableInvalidClients` the same tick. Fixed by seeding at `Up: 0`; the current node value still becomes the baseline so only future increments count. **Fix 2 — PostgreSQL deadlock** `addClientTraffic` did a read-modify-write via `tx.Save(slice)`, issuing UPDATEs in slice order. Two concurrent goroutines locking the same rows in opposite order deadlock on PostgreSQL (SQLite avoids this with file-level serialisation). Replaced with atomic per-email `UPDATE SET up=up+?, down=down+?` statements. Also preserves the delayed-start ExpiryTime conversion that `adjustTraffics` computes in-memory but the old Save path persisted to the DB. **Fix 3 & 4 — stale `inbound_id` filters** `autoRenewClients` used `WHERE inbound_id NOT IN (node inbounds)` to skip node clients, but `client_traffics.inbound_id` is set once on INSERT and never refreshed. Replaced with an email-based subquery through `client_inbounds` (the authoritative source). Also added a safe type assertion for `settings["clients"].([]any)` that previously panicked on nil. **Fix 5 — stale `inbound_id` in reset** `resetAllClientTrafficsLocked` used `WHERE inbound_id = ?` to find which emails to reset; same staleness problem. Replaced with the `client_inbounds` join for email lookup; the `inbounds.last_traffic_reset_time` update still correctly uses the inbound ID directly on the `inbounds` table. Tests updated to reflect the new seeding-at-zero semantics and a new `TestGhostData_NoPhantomTraffic` test reproduces the exact 50 GB phantom scenario.
This commit is contained in:
@@ -559,8 +559,8 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
|
||||
Total: cs.Total,
|
||||
ExpiryTime: cs.ExpiryTime,
|
||||
Reset: cs.Reset,
|
||||
Up: canon.Up,
|
||||
Down: canon.Down,
|
||||
Up: 0,
|
||||
Down: 0,
|
||||
LastOnline: cs.LastOnline,
|
||||
}
|
||||
if err := tx.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "email"}}, DoNothing: true}).
|
||||
|
||||
Reference in New Issue
Block a user