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}).