Przeglądaj źródła

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.
MHSanaei 1 dzień temu
rodzic
commit
b32837e523

+ 9 - 2
internal/web/service/inbound_node.go

@@ -380,6 +380,8 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
 
 
 	structuralChange := false
 	structuralChange := false
 
 
+	newInboundIDs := make(map[int]struct{})
+
 	snapTags := make(map[string]struct{}, len(snap.Inbounds))
 	snapTags := make(map[string]struct{}, len(snap.Inbounds))
 	for _, snapIb := range snap.Inbounds {
 	for _, snapIb := range snap.Inbounds {
 		if snapIb == nil {
 		if snapIb == nil {
@@ -466,6 +468,7 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
 			if newIb.Tag != snapIb.Tag {
 			if newIb.Tag != snapIb.Tag {
 				tagToCentral[newIb.Tag] = &newIb
 				tagToCentral[newIb.Tag] = &newIb
 			}
 			}
+			newInboundIDs[newIb.Id] = struct{}{}
 			structuralChange = true
 			structuralChange = true
 			continue
 			continue
 		}
 		}
@@ -620,6 +623,10 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
 				if dirty {
 				if dirty {
 					continue
 					continue
 				}
 				}
+				var seedUp, seedDown int64
+				if _, isNewInbound := newInboundIDs[c.Id]; isNewInbound && !isClientEmailTombstoned(cs.Email) {
+					seedUp, seedDown = canon.Up, canon.Down
+				}
 				row := &xray.ClientTraffic{
 				row := &xray.ClientTraffic{
 					InboundId:  c.Id,
 					InboundId:  c.Id,
 					Email:      cs.Email,
 					Email:      cs.Email,
@@ -627,8 +634,8 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
 					Total:      cs.Total,
 					Total:      cs.Total,
 					ExpiryTime: cs.ExpiryTime,
 					ExpiryTime: cs.ExpiryTime,
 					Reset:      cs.Reset,
 					Reset:      cs.Reset,
-					Up:         0,
-					Down:       0,
+					Up:         seedUp,
+					Down:       seedDown,
 					LastOnline: cs.LastOnline,
 					LastOnline: cs.LastOnline,
 				}
 				}
 				if err := tx.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "email"}}, DoNothing: true}).
 				if err := tx.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "email"}}, DoNothing: true}).

+ 26 - 0
internal/web/service/node_client_traffic_sum_test.go

@@ -120,6 +120,32 @@ func TestSingleNode_MirrorsCorrectly(t *testing.T) {
 	assertUpDown(t, readTraffic(t, db, email), 200, 200, "second sync — delta accrues")
 	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) {
 func TestUpgrade_PreExistingRow_NoDoubleCount(t *testing.T) {
 	db := initTrafficTestDB(t)
 	db := initTrafficTestDB(t)
 	createNodeInbound(t, db, 1, "n1-in", 41001)
 	createNodeInbound(t, db, 1, "n1-in", 41001)