fix: resolve a batch of open bug-tagged issues (traffic accounting, share strategy, sub address, CPU) (#5477)

* fix(node): never re-add a node's full counter on reset/restart (#5456, #5476, #5390)

When a node's per-client counter dips below the master's stored baseline
(node reboot, xray restart, or a reset propagated to the node), the delta
accounting clamped delta to the node's whole current counter and re-added it
to the master total — double-counting a client's lifetime usage in a single
sync and often pushing them over quota. Treat a backward-moving counter as a
reset: add 0 and rebaseline to the reported value, so only genuine post-reset
usage accrues.

Resets also now clear the per-node NodeClientTraffic baseline (ResetClient
TrafficByEmail, resetClientTrafficLocked, BulkResetTraffic, resetAllClient
TrafficsLocked), mirroring the delete paths. Without this the node's pre-reset
cumulative — including traffic it had counted but not yet synced — leaks back
onto the master after a reset, which is the 'reset reverts after a while'
report. The next sync then takes the clean delta=0 + rebaseline path regardless
of node state.

Updates TestNodeCounterReset (was _Clamped, now _NoReAdd) to assert rebaseline
instead of re-add, and adds TestCentralResetClearsNodeBaseline_NoLeak.

* fix(inbound): keep persisted node share strategy on edit (#5375)

Opening the edit modal silently reverted shareAddrStrategy from 'node' to
'listen'. The downgrade effect fires before the form settles: availableNodes
is an empty placeholder until /nodes/list resolves, and Form.useWatch('protocol')
is briefly empty on the first edit render — both transiently make the node
option look unavailable, so the effect clobbered the saved value.

Gate the downgrade on availableNodesFetched (threaded from useNodesQuery through
InboundsPage) and on the protocol watch being settled, so a persisted strategy
is only downgraded when the node option is genuinely unavailable. Adds a
rerender-based regression test covering the nodes-loading race.

* <3

* perf(traffic): skip cross-panel quota subquery when no globals exist (#5392, #5389)

disableInvalidClients ran a correlated EXISTS against client_global_traffics
on the full client_traffics table every 5s. On a panel no master pushes to,
that table is empty so the subquery can never match — yet it forced a full
scan that pegged Postgres at 100% CPU on large client counts. Probe the table
first and drop the EXISTS branch when it's empty (the common case), and add an
idx_client_global_email index so the subquery is an index lookup when globals
are present. Cross-panel enforcement is unchanged (TestGlobalUsage_DisablesClient).

This also relieves #5389 ('traffic writer queue full' / panel freeze): the
heavy query runs inside the serialized traffic write, so a slow DB backs the
shared writer queue up until request handlers block.

* fix(sub): don't advertise a leaked client IP for local wildcard inbounds (#5425)

For a local inbound with no node, no custom share address, and a wildcard/blank
listen, resolveInboundAddress fell straight through to the subscriber's request
host. Behind NAT/proxy/CDN that Host can be the requesting client's own IP, so
the subscription wrote the client's address into the inbound instead of the
server's — while the panel's own share link (which doesn't use the request host)
stayed correct.

Prefer the admin's configured public host (Sub/Web domain) over the raw request
host for this last-resort fallback. With no configured host the request host
still stands, so existing single-domain setups are unaffected.
This commit is contained in:
Sanaei
2026-06-22 00:22:28 +02:00
committed by GitHub
parent 0b0b6250d6
commit 679d2e1cca
26 changed files with 248 additions and 44 deletions
@@ -169,7 +169,7 @@ func TestGhostData_NoPhantomTraffic(t *testing.T) {
assertUpDown(t, readTraffic(t, db, email), 1024, 2048, "only incremental traffic beyond baseline counts")
}
func TestNodeCounterReset_Clamped(t *testing.T) {
func TestNodeCounterReset_NoReAdd(t *testing.T) {
db := initTrafficTestDB(t)
createNodeInbound(t, db, 1, "n1-in", 41001)
svc := &InboundService{}
@@ -180,13 +180,19 @@ func TestNodeCounterReset_Clamped(t *testing.T) {
syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 950, Down: 950, Enable: true})
assertUpDown(t, readTraffic(t, db, email), 50, 50, "before node reset")
// Counter resets to 50 (Xray restart). delta=50-950=-900 → clamped → adds 50.
// Node reboot drops the counter to 50. delta=50-950=-900 is a counter reset,
// not new traffic: add 0 and rebaseline to 50, never re-add the node's full
// cumulative counter onto the master total (#5456).
syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 50, Down: 50, Enable: true})
ct := readTraffic(t, db, email)
if ct.Up < 0 || ct.Down < 0 {
t.Fatalf("row went negative after node reset: up=%d down=%d", ct.Up, ct.Down)
}
assertUpDown(t, ct, 100, 100, "after node counter reset (clamped)")
assertUpDown(t, ct, 50, 50, "after node counter reset: rebaselined, not re-added")
// Post-reset accrual resumes from the new baseline: 80-50=30.
syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 80, Down: 80, Enable: true})
assertUpDown(t, readTraffic(t, db, email), 80, 80, "post-reset delta accrues from rebaselined counter")
}
func TestCentralReset_NoReAdd(t *testing.T) {
@@ -212,6 +218,43 @@ func TestCentralReset_NoReAdd(t *testing.T) {
assertUpDown(t, readTraffic(t, db, email), 15, 15, "after central reset only increments accrue")
}
// A real reset (ResetClientTrafficByEmail) must clear the per-node baseline so
// the node's pre-reset cumulative — including traffic it counted but had not yet
// synced — cannot leak back onto the master after the reset (#5476, #5390).
func TestCentralResetClearsNodeBaseline_NoLeak(t *testing.T) {
db := initTrafficTestDB(t)
createNodeInbound(t, db, 1, "n1-in", 41001)
StartTrafficWriter()
svc := &InboundService{}
const email = "reset-revert"
syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 100, Down: 100, Enable: true})
syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 300, Down: 300, Enable: true})
assertUpDown(t, readTraffic(t, db, email), 200, 200, "before reset")
if err := svc.ResetClientTrafficByEmail(email); err != nil {
t.Fatalf("ResetClientTrafficByEmail: %v", err)
}
assertUpDown(t, readTraffic(t, db, email), 0, 0, "right after reset")
var baselines int64
if err := db.Model(&model.NodeClientTraffic{}).Where("email = ?", email).Count(&baselines).Error; err != nil {
t.Fatalf("count baselines: %v", err)
}
if baselines != 0 {
t.Fatalf("reset must clear node baseline rows, found %d", baselines)
}
// Node still reports its pre-reset cumulative (340 > last synced 300: usage it
// had not synced before the reset). It must not revert the reset.
syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 340, Down: 340, Enable: true})
assertUpDown(t, readTraffic(t, db, email), 0, 0, "stale node counter must not revert reset")
// Genuine post-reset usage accrues from the rebaselined counter: 370-340=30.
syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 370, Down: 370, Enable: true})
assertUpDown(t, readTraffic(t, db, email), 30, 30, "post-reset usage accrues")
}
func TestInboundRemoval_KeepsSharedEmailRow(t *testing.T) {
db := initTrafficTestDB(t)
createNodeInboundWithClient(t, db, 1, "n1-in", 41001, "shared")