fix(node): import per-client traffic history on first sync of a node-hosted inbound

On the first sync of a node-hosted inbound, the central inbound adopted the
node's full lifetime counter but every client_traffics row was seeded at 0 (with
the delta baseline set to the node's current counter). So adding or migrating a
node that already had traffic kept the inbound total correct while every
per-client counter restarted from zero, and the master under-reported per-client
usage by the entire pre-attach history.

Seed a new client_traffics row from the node counter only when the inbound was
created during the same sync (a genuine node-add / inbound re-import); a client
reappearing under a pre-existing inbound still seeds 0, preserving the ghost
protection in TestGhostData_NoPhantomTraffic. The seed is additionally gated on
the delete tombstone so a just-deleted client cannot be resurrected if its
inbound is recreated. Baseline still equals the seeded value, so the next sync
delta is 0 and no traffic is double counted.

Adds TestNodeAdd_ImportsClientHistoryWithNewInbound and
TestNodeAdd_TombstonedClientNotResurrected.
This commit is contained in:
MHSanaei
2026-06-25 21:19:27 +02:00
parent 9dec15bd4b
commit b32837e523
2 changed files with 35 additions and 2 deletions
+9 -2
View File
@@ -380,6 +380,8 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
structuralChange := false
newInboundIDs := make(map[int]struct{})
snapTags := make(map[string]struct{}, len(snap.Inbounds))
for _, snapIb := range snap.Inbounds {
if snapIb == nil {
@@ -466,6 +468,7 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
if newIb.Tag != snapIb.Tag {
tagToCentral[newIb.Tag] = &newIb
}
newInboundIDs[newIb.Id] = struct{}{}
structuralChange = true
continue
}
@@ -620,6 +623,10 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
if dirty {
continue
}
var seedUp, seedDown int64
if _, isNewInbound := newInboundIDs[c.Id]; isNewInbound && !isClientEmailTombstoned(cs.Email) {
seedUp, seedDown = canon.Up, canon.Down
}
row := &xray.ClientTraffic{
InboundId: c.Id,
Email: cs.Email,
@@ -627,8 +634,8 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
Total: cs.Total,
ExpiryTime: cs.ExpiryTime,
Reset: cs.Reset,
Up: 0,
Down: 0,
Up: seedUp,
Down: seedDown,
LastOnline: cs.LastOnline,
}
if err := tx.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "email"}}, DoNothing: true}).
@@ -120,6 +120,32 @@ func TestSingleNode_MirrorsCorrectly(t *testing.T) {
assertUpDown(t, readTraffic(t, db, email), 200, 200, "second sync — delta accrues")
}
func TestNodeAdd_ImportsClientHistoryWithNewInbound(t *testing.T) {
db := initTrafficTestDB(t)
svc := &InboundService{}
const email = "newnode-client"
const histUp, histDown int64 = 6_000_000_000, 200_000_000_000
syncNode(t, svc, 1, "fresh-in", xray.ClientTraffic{Email: email, Up: histUp, Down: histDown, Enable: true})
assertUpDown(t, readTraffic(t, db, email), histUp, histDown, "node-add: client history imported with its brand-new inbound")
syncNode(t, svc, 1, "fresh-in", xray.ClientTraffic{Email: email, Up: histUp + 1024, Down: histDown + 2048, Enable: true})
assertUpDown(t, readTraffic(t, db, email), histUp+1024, histDown+2048, "post-import delta accrues, no double count")
}
func TestNodeAdd_TombstonedClientNotResurrected(t *testing.T) {
db := initTrafficTestDB(t)
svc := &InboundService{}
const email = "deleted-ghost"
const stale int64 = 50_000_000_000
tombstoneClientEmail(email)
syncNode(t, svc, 1, "fresh-in", xray.ClientTraffic{Email: email, Up: stale, Down: stale, Enable: true})
assertUpDown(t, readTraffic(t, db, email), 0, 0, "tombstoned client must not resurrect via node-add seed")
}
func TestUpgrade_PreExistingRow_NoDoubleCount(t *testing.T) {
db := initTrafficTestDB(t)
createNodeInbound(t, db, 1, "n1-in", 41001)