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.
* fix(node-traffic): restart remote xray after disabling clients to kill active sessions
When a client's traffic limit is reached on a remote node, the panel pushes
enable=false to that node via UpdateInbound. The node calls RemoveUser on its
local xray, which blocks new connections but leaves any already-established TCP
session alive. The user could continue browsing/downloading until they
disconnected voluntarily.
Fix: after successfully pushing a client disable to a remote node, call
RestartXray on that node. This mirrors what already happens for the local node
when the "Restart Xray on client disable" setting is enabled (default: on),
and ensures active sessions are terminated immediately on all nodes where the
client was disabled.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(node): restart remote xray after tx commit, not inside it
Move the remote RestartXray calls out of the addTraffic write
transaction. disableInvalidClients now returns the affected remote
node IDs instead of restarting their xray while the SQLite write lock
is held; AddTraffic performs the restart after the transaction commits
via restartRemoteNodesOnDisable. Avoids holding the serialized write
lock across slow per-node restart RPCs.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
When a remote node syncs traffic back to the panel, the UPDATE in
setRemoteTrafficLocked wrote cs.Enable directly into client_traffics.enable.
If a snapshot carrying enable=true arrived after the central panel had already
set enable=false (due to the client reaching their traffic limit), it silently
re-enabled the client — letting them consume 2-3x their allotted quota before
the next disable cycle caught up.
Fix: replace the unconditional SET enable = ? with a CASE expression that only
allows a disable (0->0), never a re-enable (0->1). The central panel remains
the sole authority for turning a client back on.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>