node_client_traffic_sum_test.go 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691
  1. package service
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "maps"
  6. "path/filepath"
  7. "regexp"
  8. "slices"
  9. "strings"
  10. "sync"
  11. "testing"
  12. "gorm.io/gorm"
  13. "github.com/mhsanaei/3x-ui/v3/internal/database"
  14. "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
  15. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  16. "github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
  17. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  18. )
  19. func initTrafficTestDB(t *testing.T) *gorm.DB {
  20. t.Helper()
  21. dbDir := t.TempDir()
  22. t.Setenv("XUI_DB_FOLDER", dbDir)
  23. dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db"))
  24. return database.GetDB()
  25. }
  26. func createNodeInbound(t *testing.T, db *gorm.DB, nodeID int, tag string, port int) {
  27. t.Helper()
  28. nid := nodeID
  29. ib := &model.Inbound{UserId: 1, Tag: tag, Enable: true, Port: port, Protocol: model.VLESS, NodeID: &nid}
  30. if err := db.Create(ib).Error; err != nil {
  31. t.Fatalf("create node inbound %q: %v", tag, err)
  32. }
  33. }
  34. // createNodeInboundWithClient mirrors createNodeInbound but stores the client
  35. // in the settings JSON, which the node sync turns into a client_inbounds link.
  36. func createNodeInboundWithClient(t *testing.T, db *gorm.DB, nodeID int, tag string, port int, email string) {
  37. t.Helper()
  38. nid := nodeID
  39. settings := fmt.Sprintf(`{"clients": [{"email": %q, "enable": true}]}`, email)
  40. ib := &model.Inbound{UserId: 1, Tag: tag, Enable: true, Port: port, Protocol: model.VLESS, NodeID: &nid, Settings: settings}
  41. if err := db.Create(ib).Error; err != nil {
  42. t.Fatalf("create node inbound %q: %v", tag, err)
  43. }
  44. }
  45. func syncNode(t *testing.T, svc *InboundService, nodeID int, tag string, stats ...xray.ClientTraffic) {
  46. t.Helper()
  47. snap := &runtime.TrafficSnapshot{
  48. Inbounds: []*model.Inbound{{Tag: tag, ClientStats: stats}},
  49. }
  50. if _, err := svc.setRemoteTrafficLocked(nodeID, snap, false, false); err != nil {
  51. t.Fatalf("setRemoteTrafficLocked node %d: %v", nodeID, err)
  52. }
  53. }
  54. // syncNodeWithSettings mirrors syncNode but carries a real settings JSON on
  55. // the snapshot inbound, like production nodes do — the sync mirrors snapshot
  56. // settings onto the central row, and the shared-accumulator guard reads the
  57. // clients out of those settings.
  58. func syncNodeWithSettings(t *testing.T, svc *InboundService, nodeID int, tag, settings string, stats ...xray.ClientTraffic) {
  59. t.Helper()
  60. snap := &runtime.TrafficSnapshot{
  61. Inbounds: []*model.Inbound{{Tag: tag, Settings: settings, ClientStats: stats}},
  62. }
  63. if _, err := svc.setRemoteTrafficLocked(nodeID, snap, false, false); err != nil {
  64. t.Fatalf("setRemoteTrafficLocked node %d: %v", nodeID, err)
  65. }
  66. }
  67. func readTraffic(t *testing.T, db *gorm.DB, email string) xray.ClientTraffic {
  68. t.Helper()
  69. var ct xray.ClientTraffic
  70. if err := db.Model(xray.ClientTraffic{}).Where("email = ?", email).First(&ct).Error; err != nil {
  71. t.Fatalf("read client_traffics %q: %v", email, err)
  72. }
  73. return ct
  74. }
  75. func assertUpDown(t *testing.T, ct xray.ClientTraffic, wantUp, wantDown int64, when string) {
  76. t.Helper()
  77. if ct.Up != wantUp || ct.Down != wantDown {
  78. t.Errorf("%s: up=%d down=%d, want %d/%d", when, ct.Up, ct.Down, wantUp, wantDown)
  79. }
  80. }
  81. func TestTwoNodesShareEmail_SumsCorrectly(t *testing.T) {
  82. db := initTrafficTestDB(t)
  83. createNodeInbound(t, db, 1, "n1-in", 41001)
  84. createNodeInbound(t, db, 2, "n2-in", 41002)
  85. svc := &InboundService{}
  86. const email = "shared"
  87. // First-ever sync from each node: the row is created at 0 and the current
  88. // node counters become the baseline. Only deltas beyond those baselines count.
  89. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 100, Down: 100, Enable: true})
  90. syncNode(t, svc, 2, "n2-in", xray.ClientTraffic{Email: email, Up: 200, Down: 200, Enable: true})
  91. assertUpDown(t, readTraffic(t, db, email), 0, 0, "after baselines — historical node traffic not imported")
  92. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 150, Down: 150, Enable: true})
  93. syncNode(t, svc, 2, "n2-in", xray.ClientTraffic{Email: email, Up: 260, Down: 260, Enable: true})
  94. assertUpDown(t, readTraffic(t, db, email), 110, 110, "after both nodes grow — deltas (50+60) accrue")
  95. }
  96. func TestSingleNode_MirrorsCorrectly(t *testing.T) {
  97. db := initTrafficTestDB(t)
  98. createNodeInbound(t, db, 1, "n1-in", 41001)
  99. svc := &InboundService{}
  100. const email = "solo"
  101. // First sync: row seeded at 0, current counter becomes the baseline.
  102. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 500, Down: 600, Enable: true})
  103. assertUpDown(t, readTraffic(t, db, email), 0, 0, "first sync — historical traffic not imported")
  104. // Second sync: delta (700-500=200 / 800-600=200) accrues normally.
  105. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 700, Down: 800, Enable: true})
  106. assertUpDown(t, readTraffic(t, db, email), 200, 200, "second sync — delta accrues")
  107. }
  108. func TestNodeAdd_ImportsClientHistoryWithNewInbound(t *testing.T) {
  109. db := initTrafficTestDB(t)
  110. svc := &InboundService{}
  111. const email = "newnode-client"
  112. const histUp, histDown int64 = 6_000_000_000, 200_000_000_000
  113. syncNode(t, svc, 1, "fresh-in", xray.ClientTraffic{Email: email, Up: histUp, Down: histDown, Enable: true})
  114. assertUpDown(t, readTraffic(t, db, email), histUp, histDown, "node-add: client history imported with its brand-new inbound")
  115. syncNode(t, svc, 1, "fresh-in", xray.ClientTraffic{Email: email, Up: histUp + 1024, Down: histDown + 2048, Enable: true})
  116. assertUpDown(t, readTraffic(t, db, email), histUp+1024, histDown+2048, "post-import delta accrues, no double count")
  117. }
  118. // TestStaleSnapshot_DeletedClientNotResurrected reproduces #5739: a snapshot
  119. // fetched just before a client's deletion still names the email. Applying it
  120. // must neither recreate the client_traffics row (at zero) nor re-add the
  121. // client to the central inbound's settings while the delete tombstone lives.
  122. func TestStaleSnapshot_DeletedClientNotResurrected(t *testing.T) {
  123. db := initTrafficTestDB(t)
  124. createNodeInboundWithClient(t, db, 1, "n1-in", 41001, "victim-5739")
  125. svc := &InboundService{}
  126. const email = "victim-5739"
  127. withClient := fmt.Sprintf(`{"clients": [{"email": %q, "enable": true}]}`, email)
  128. syncNodeWithSettings(t, svc, 1, "n1-in", withClient, xray.ClientTraffic{Email: email, Up: 100, Down: 100, Enable: true})
  129. if err := db.Model(&model.Inbound{}).Where("tag = ?", "n1-in").
  130. Update("settings", `{"clients": []}`).Error; err != nil {
  131. t.Fatalf("clear central settings: %v", err)
  132. }
  133. if err := db.Where("email = ?", email).Delete(&xray.ClientTraffic{}).Error; err != nil {
  134. t.Fatalf("delete stats row: %v", err)
  135. }
  136. tombstoneClientEmail(email)
  137. syncNodeWithSettings(t, svc, 1, "n1-in", withClient, xray.ClientTraffic{Email: email, Up: 120, Down: 120, Enable: true})
  138. var rows int64
  139. if err := db.Model(xray.ClientTraffic{}).Where("email = ?", email).Count(&rows).Error; err != nil {
  140. t.Fatalf("count stats rows: %v", err)
  141. }
  142. if rows != 0 {
  143. t.Errorf("deleted client's stats row resurrected by stale snapshot (%d rows)", rows)
  144. }
  145. var ib model.Inbound
  146. if err := db.Where("tag = ?", "n1-in").First(&ib).Error; err != nil {
  147. t.Fatalf("load inbound: %v", err)
  148. }
  149. if strings.Contains(ib.Settings, email) {
  150. t.Errorf("deleted client re-added to central settings: %s", ib.Settings)
  151. }
  152. }
  153. func TestNodeAdd_TombstonedClientNotResurrected(t *testing.T) {
  154. db := initTrafficTestDB(t)
  155. svc := &InboundService{}
  156. const email = "deleted-ghost"
  157. const stale int64 = 50_000_000_000
  158. tombstoneClientEmail(email)
  159. syncNode(t, svc, 1, "fresh-in", xray.ClientTraffic{Email: email, Up: stale, Down: stale, Enable: true})
  160. assertUpDown(t, readTraffic(t, db, email), 0, 0, "tombstoned client must not resurrect via node-add seed")
  161. }
  162. func TestUpgrade_PreExistingRow_NoDoubleCount(t *testing.T) {
  163. db := initTrafficTestDB(t)
  164. createNodeInbound(t, db, 1, "n1-in", 41001)
  165. svc := &InboundService{}
  166. const email = "legacy"
  167. var ib model.Inbound
  168. if err := db.Where("tag = ?", "n1-in").First(&ib).Error; err != nil {
  169. t.Fatalf("load inbound: %v", err)
  170. }
  171. if err := db.Create(&xray.ClientTraffic{InboundId: ib.Id, Email: email, Up: 1000, Down: 2000, Enable: true}).Error; err != nil {
  172. t.Fatalf("seed pre-existing row: %v", err)
  173. }
  174. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 1000, Down: 2000, Enable: true})
  175. assertUpDown(t, readTraffic(t, db, email), 1000, 2000, "first snapshot must not double-count")
  176. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 1100, Down: 2100, Enable: true})
  177. assertUpDown(t, readTraffic(t, db, email), 1100, 2100, "growth after upgrade accrues")
  178. }
  179. // TestGhostData_NoPhantomTraffic reproduces the 50 GB phantom traffic bug:
  180. // a node retains stale data for a deleted client (e.g. de-1 was offline during
  181. // deletion). When the master first syncs this email again it must NOT import the
  182. // stale counter — it sets Up=0 and records the current node value as the
  183. // baseline so only future deltas count.
  184. func TestGhostData_NoPhantomTraffic(t *testing.T) {
  185. db := initTrafficTestDB(t)
  186. createNodeInbound(t, db, 1, "de1-in", 41001)
  187. svc := &InboundService{}
  188. const email = "pouya"
  189. const staleBytes int64 = 50 * 1024 * 1024 * 1024 // 50 GB stale on de-1
  190. // Node reports a client with a large pre-existing counter (ghost data from a
  191. // deleted account that survived on the node). Master has no row for this email.
  192. syncNode(t, svc, 1, "de1-in", xray.ClientTraffic{Email: email, Up: staleBytes, Down: staleBytes, Enable: true})
  193. ct := readTraffic(t, db, email)
  194. if ct.Up >= staleBytes || ct.Down >= staleBytes {
  195. t.Errorf("ghost data imported: up=%d down=%d; stale node counter must not count as new traffic", ct.Up, ct.Down)
  196. }
  197. assertUpDown(t, ct, 0, 0, "first sync of ghost email — imported as 0, not 50 GB")
  198. // Subsequent syncs only add incremental usage beyond the baseline.
  199. syncNode(t, svc, 1, "de1-in", xray.ClientTraffic{Email: email, Up: staleBytes + 1024, Down: staleBytes + 2048, Enable: true})
  200. assertUpDown(t, readTraffic(t, db, email), 1024, 2048, "only incremental traffic beyond baseline counts")
  201. }
  202. func TestNodeCounterReset_NoReAdd(t *testing.T) {
  203. db := initTrafficTestDB(t)
  204. createNodeInbound(t, db, 1, "n1-in", 41001)
  205. svc := &InboundService{}
  206. const email = "restart"
  207. // First sync seeds at 0 (baseline=900). Second sync adds delta 950-900=50.
  208. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 900, Down: 900, Enable: true})
  209. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 950, Down: 950, Enable: true})
  210. assertUpDown(t, readTraffic(t, db, email), 50, 50, "before node reset")
  211. // Node reboot drops the counter to 50. delta=50-950=-900 is a counter reset,
  212. // not new traffic: add 0 and rebaseline to 50, never re-add the node's full
  213. // cumulative counter onto the master total (#5456).
  214. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 50, Down: 50, Enable: true})
  215. ct := readTraffic(t, db, email)
  216. if ct.Up < 0 || ct.Down < 0 {
  217. t.Fatalf("row went negative after node reset: up=%d down=%d", ct.Up, ct.Down)
  218. }
  219. assertUpDown(t, ct, 50, 50, "after node counter reset: rebaselined, not re-added")
  220. // Post-reset accrual resumes from the new baseline: 80-50=30.
  221. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 80, Down: 80, Enable: true})
  222. assertUpDown(t, readTraffic(t, db, email), 80, 80, "post-reset delta accrues from rebaselined counter")
  223. }
  224. func TestCentralReset_NoReAdd(t *testing.T) {
  225. db := initTrafficTestDB(t)
  226. createNodeInbound(t, db, 1, "n1-in", 41001)
  227. createNodeInbound(t, db, 2, "n2-in", 41002)
  228. svc := &InboundService{}
  229. const email = "reset"
  230. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 100, Down: 100, Enable: true})
  231. syncNode(t, svc, 2, "n2-in", xray.ClientTraffic{Email: email, Up: 100, Down: 100, Enable: true})
  232. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 200, Down: 200, Enable: true})
  233. syncNode(t, svc, 2, "n2-in", xray.ClientTraffic{Email: email, Up: 200, Down: 200, Enable: true})
  234. if err := db.Model(xray.ClientTraffic{}).Where("email = ?", email).
  235. Updates(map[string]any{"up": 0, "down": 0}).Error; err != nil {
  236. t.Fatalf("simulate central reset: %v", err)
  237. }
  238. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 210, Down: 210, Enable: true})
  239. syncNode(t, svc, 2, "n2-in", xray.ClientTraffic{Email: email, Up: 205, Down: 205, Enable: true})
  240. assertUpDown(t, readTraffic(t, db, email), 15, 15, "after central reset only increments accrue")
  241. }
  242. // A real reset (ResetClientTrafficByEmail) must clear the per-node baseline so
  243. // the node's pre-reset cumulative — including traffic it counted but had not yet
  244. // synced — cannot leak back onto the master after the reset (#5476, #5390).
  245. func TestCentralResetClearsNodeBaseline_NoLeak(t *testing.T) {
  246. db := initTrafficTestDB(t)
  247. createNodeInbound(t, db, 1, "n1-in", 41001)
  248. StartTrafficWriter()
  249. svc := &InboundService{}
  250. const email = "reset-revert"
  251. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 100, Down: 100, Enable: true})
  252. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 300, Down: 300, Enable: true})
  253. assertUpDown(t, readTraffic(t, db, email), 200, 200, "before reset")
  254. if err := svc.ResetClientTrafficByEmail(email); err != nil {
  255. t.Fatalf("ResetClientTrafficByEmail: %v", err)
  256. }
  257. assertUpDown(t, readTraffic(t, db, email), 0, 0, "right after reset")
  258. var baselines int64
  259. if err := db.Model(&model.NodeClientTraffic{}).Where("email = ?", email).Count(&baselines).Error; err != nil {
  260. t.Fatalf("count baselines: %v", err)
  261. }
  262. if baselines != 0 {
  263. t.Fatalf("reset must clear node baseline rows, found %d", baselines)
  264. }
  265. // Node still reports its pre-reset cumulative (340 > last synced 300: usage it
  266. // had not synced before the reset). It must not revert the reset.
  267. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 340, Down: 340, Enable: true})
  268. assertUpDown(t, readTraffic(t, db, email), 0, 0, "stale node counter must not revert reset")
  269. // Genuine post-reset usage accrues from the rebaselined counter: 370-340=30.
  270. syncNode(t, svc, 1, "n1-in", xray.ClientTraffic{Email: email, Up: 370, Down: 370, Enable: true})
  271. assertUpDown(t, readTraffic(t, db, email), 30, 30, "post-reset usage accrues")
  272. }
  273. func TestInboundRemoval_KeepsSharedEmailRow(t *testing.T) {
  274. db := initTrafficTestDB(t)
  275. createNodeInboundWithClient(t, db, 1, "n1-in", 41001, "shared")
  276. createNodeInboundWithClient(t, db, 2, "n2-in", 41002, "shared")
  277. svc := &InboundService{}
  278. const email = "shared"
  279. settings := fmt.Sprintf(`{"clients": [{"email": %q, "enable": true}]}`, email)
  280. // Baselines set: node1=100, node2=200. Row seeded at 0.
  281. syncNodeWithSettings(t, svc, 1, "n1-in", settings, xray.ClientTraffic{Email: email, Up: 100, Down: 100, Enable: true})
  282. syncNodeWithSettings(t, svc, 2, "n2-in", settings, xray.ClientTraffic{Email: email, Up: 200, Down: 200, Enable: true})
  283. // node1 delta=50, node2 delta=60 → total=110.
  284. syncNodeWithSettings(t, svc, 1, "n1-in", settings, xray.ClientTraffic{Email: email, Up: 150, Down: 150, Enable: true})
  285. syncNodeWithSettings(t, svc, 2, "n2-in", settings, xray.ClientTraffic{Email: email, Up: 260, Down: 260, Enable: true})
  286. assertUpDown(t, readTraffic(t, db, email), 110, 110, "baseline sum")
  287. // Node 1 rebuilt (reinstall / another master's reconcile): its inbound
  288. // vanishes from the snapshot. The shared accumulator must survive — losing
  289. // it would let the next node sync re-seed the row with that node's counter
  290. // alone, showing only the last panel's number instead of the sum.
  291. if _, err := svc.setRemoteTrafficLocked(1, &runtime.TrafficSnapshot{}, false, false); err != nil {
  292. t.Fatalf("sync node 1 with empty snapshot: %v", err)
  293. }
  294. assertUpDown(t, readTraffic(t, db, email), 110, 110, "after node 1 inbound removal")
  295. // Node 2 keeps accruing onto the surviving row. node2 delta=300-260=40 → 150.
  296. syncNodeWithSettings(t, svc, 2, "n2-in", settings, xray.ClientTraffic{Email: email, Up: 300, Down: 300, Enable: true})
  297. assertUpDown(t, readTraffic(t, db, email), 150, 150, "after node 2 grows")
  298. }
  299. func TestClientGoneFromOneNode_KeepsSharedEmailRow(t *testing.T) {
  300. db := initTrafficTestDB(t)
  301. createNodeInboundWithClient(t, db, 1, "n1-in", 41001, "shared")
  302. createNodeInboundWithClient(t, db, 2, "n2-in", 41002, "shared")
  303. svc := &InboundService{}
  304. const email = "shared"
  305. settings := fmt.Sprintf(`{"clients": [{"email": %q, "enable": true}]}`, email)
  306. // First syncs set baselines (node1=100, node2=200). Row seeded at 0.
  307. syncNodeWithSettings(t, svc, 1, "n1-in", settings, xray.ClientTraffic{Email: email, Up: 100, Down: 100, Enable: true})
  308. syncNodeWithSettings(t, svc, 2, "n2-in", settings, xray.ClientTraffic{Email: email, Up: 200, Down: 200, Enable: true})
  309. // Client detached from node 1's inbound only: its stats vanish from that
  310. // inbound's snapshot while node 2 still hosts the email.
  311. syncNodeWithSettings(t, svc, 1, "n1-in", `{"clients": []}`)
  312. assertUpDown(t, readTraffic(t, db, email), 0, 0, "after client left node 1 — row unchanged at 0")
  313. // Node 2 delta: 240-200=40 → accrues to 40.
  314. syncNodeWithSettings(t, svc, 2, "n2-in", settings, xray.ClientTraffic{Email: email, Up: 240, Down: 240, Enable: true})
  315. assertUpDown(t, readTraffic(t, db, email), 40, 40, "node 2 keeps accruing")
  316. }
  317. // TestStatsUnderSiblingInbound_KeepsNodeBaseline reproduces the recurring
  318. // sweep bug behind #5202: the client is attached to two inbounds of the SAME
  319. // node, the node reports its stats under n1-a only, but the master-side row
  320. // is owned by n1-b's mirror. The per-email sweep for n1-b must not drop the
  321. // (node, email) baseline that n1-a's delta computation needs — doing so every
  322. // cycle froze the client's counter permanently.
  323. func TestStatsUnderSiblingInbound_KeepsNodeBaseline(t *testing.T) {
  324. db := initTrafficTestDB(t)
  325. createNodeInboundWithClient(t, db, 1, "n1-a", 41001, "fresh")
  326. createNodeInbound(t, db, 1, "n1-b", 41002)
  327. svc := &InboundService{}
  328. const email = "fresh"
  329. var ibB model.Inbound
  330. if err := db.Where("tag = ?", "n1-b").First(&ibB).Error; err != nil {
  331. t.Fatalf("load n1-b: %v", err)
  332. }
  333. // Master-side row created when the client was added on the panel, owned by
  334. // n1-b's mirror (e.g. the client form targeted that inbound).
  335. if err := db.Create(&xray.ClientTraffic{InboundId: ibB.Id, Email: email, Enable: true}).Error; err != nil {
  336. t.Fatalf("seed master row: %v", err)
  337. }
  338. settings := fmt.Sprintf(`{"clients": [{"email": %q, "enable": true}]}`, email)
  339. sync := func(up, down int64) {
  340. t.Helper()
  341. snap := &runtime.TrafficSnapshot{Inbounds: []*model.Inbound{
  342. {Tag: "n1-a", Settings: settings, ClientStats: []xray.ClientTraffic{{Email: email, Up: up, Down: down, Enable: true}}},
  343. {Tag: "n1-b", Settings: `{"clients": []}`},
  344. }}
  345. if _, err := svc.setRemoteTrafficLocked(1, snap, false, false); err != nil {
  346. t.Fatalf("sync: %v", err)
  347. }
  348. }
  349. sync(630, 630)
  350. var baselines int64
  351. if err := db.Model(&model.NodeClientTraffic{}).Where("node_id = ? AND email = ?", 1, email).Count(&baselines).Error; err != nil {
  352. t.Fatalf("count baselines: %v", err)
  353. }
  354. if baselines != 1 {
  355. t.Fatalf("baseline must survive the sibling-inbound sweep, found %d rows", baselines)
  356. }
  357. sync(700, 700)
  358. assertUpDown(t, readTraffic(t, db, email), 70, 70, "delta accrues once baseline survives")
  359. }
  360. // TestMultiAttach_SameNode_DivergentSiblings reproduces #5274: a client is
  361. // attached to several inbounds of the SAME node. Xray reports client traffic
  362. // globally per email, so the node's enriched inbound list copies one shared
  363. // counter onto every inbound the client is on. When those copies diverge — a
  364. // legacy per-inbound row surviving the v3.2.x→v3.3.x upgrade, or any drift —
  365. // the per-inbound delta loop used to treat the lower sibling as a node-counter
  366. // reset and re-add its full value, inflating the client far past real usage.
  367. // The merge must collapse the email to its node-wide total and count it once.
  368. func TestMultiAttach_SameNode_DivergentSiblings(t *testing.T) {
  369. db := initTrafficTestDB(t)
  370. createNodeInboundWithClient(t, db, 1, "n1-a", 41001, "multi")
  371. createNodeInboundWithClient(t, db, 1, "n1-b", 41002, "multi")
  372. createNodeInboundWithClient(t, db, 1, "n1-c", 41003, "multi")
  373. svc := &InboundService{}
  374. const email = "multi"
  375. settings := fmt.Sprintf(`{"clients": [{"email": %q, "enable": true}]}`, email)
  376. // The three inbounds report the same email with diverging values; the
  377. // node's true per-email total is the largest (the shared global counter).
  378. sync := func(a, b, c int64) {
  379. t.Helper()
  380. snap := &runtime.TrafficSnapshot{Inbounds: []*model.Inbound{
  381. {Tag: "n1-a", Settings: settings, ClientStats: []xray.ClientTraffic{{Email: email, Up: a, Down: a, Enable: true}}},
  382. {Tag: "n1-b", Settings: settings, ClientStats: []xray.ClientTraffic{{Email: email, Up: b, Down: b, Enable: true}}},
  383. {Tag: "n1-c", Settings: settings, ClientStats: []xray.ClientTraffic{{Email: email, Up: c, Down: c, Enable: true}}},
  384. }}
  385. if _, err := svc.setRemoteTrafficLocked(1, snap, false, false); err != nil {
  386. t.Fatalf("sync: %v", err)
  387. }
  388. }
  389. // First-ever sync: row seeded at 0, canon (max of siblings = 100) becomes the
  390. // baseline. The node total is NOT imported as historical traffic — this prevents
  391. // ghost data from a previously-deleted account being counted as real usage.
  392. sync(100, 50, 80)
  393. assertUpDown(t, readTraffic(t, db, email), 0, 0, "first sync seeds at 0 — not the sum (230) nor the max (100)")
  394. // Second sync: canon grew from 100→150, delta=50 accrues. Siblings 60 and 90
  395. // do not re-add their full values — the canon clamp prevents inflation.
  396. sync(150, 60, 90)
  397. assertUpDown(t, readTraffic(t, db, email), 50, 50, "second sync: only the 50-unit increment counts, not every sibling")
  398. // Equal siblings (the healthy current-schema case): canon grew from 150→200,
  399. // delta=50 accrues once.
  400. sync(200, 200, 200)
  401. assertUpDown(t, readTraffic(t, db, email), 100, 100, "equal siblings accrue the single increment")
  402. }
  403. var clientWriteSQL = regexp.MustCompile(`(?i)^\s*(?:INSERT\s+INTO|UPDATE|DELETE\s+FROM)\s+["` + "`" + `]?(clients|client_inbounds|client_traffics|node_client_traffics)\b`)
  404. // countClientRowWrites returns, per client table, how many rows fn's
  405. // INSERT/UPDATE/DELETE statements touched.
  406. func countClientRowWrites(t *testing.T, fn func()) map[string]int64 {
  407. t.Helper()
  408. db := database.GetDB()
  409. var mu sync.Mutex
  410. rows := map[string]int64{}
  411. record := func(tx *gorm.DB) {
  412. if tx.Error != nil || tx.RowsAffected == 0 || tx.Statement == nil {
  413. return
  414. }
  415. if m := clientWriteSQL.FindStringSubmatch(tx.Statement.SQL.String()); m != nil {
  416. mu.Lock()
  417. rows[m[1]] += tx.RowsAffected
  418. mu.Unlock()
  419. }
  420. }
  421. const name = "test:count_client_row_writes"
  422. cb := db.Callback()
  423. for _, err := range []error{
  424. cb.Create().After("gorm:create").Register(name, record),
  425. cb.Update().After("gorm:update").Register(name, record),
  426. cb.Delete().After("gorm:delete").Register(name, record),
  427. cb.Raw().After("gorm:raw").Register(name, record),
  428. } {
  429. if err != nil {
  430. t.Fatalf("register write callback: %v", err)
  431. }
  432. }
  433. defer func() {
  434. for _, err := range []error{
  435. cb.Create().Remove(name), cb.Update().Remove(name),
  436. cb.Delete().Remove(name), cb.Raw().Remove(name),
  437. } {
  438. if err != nil {
  439. t.Errorf("remove write callback: %v", err)
  440. }
  441. }
  442. }()
  443. fn()
  444. return rows
  445. }
  446. func TestNodeMerge_WritesOnlyChangedClients(t *testing.T) {
  447. const clientCount = 20
  448. type snapInbound struct {
  449. tag string
  450. protocol model.Protocol
  451. entry func(i int, email string) map[string]any
  452. }
  453. vlessEntry := func(flow string) func(int, string) map[string]any {
  454. return func(i int, email string) map[string]any {
  455. e := map[string]any{"email": email, "id": fmt.Sprintf("%08d-0000-4000-8000-000000000000", i), "subId": email, "enable": true}
  456. if flow != "" {
  457. e["flow"] = flow
  458. }
  459. return e
  460. }
  461. }
  462. wgEntry := func(tunnel int) func(int, string) map[string]any {
  463. return func(i int, email string) map[string]any {
  464. return map[string]any{
  465. "email": email, "subId": email, "enable": true,
  466. "privateKey": fmt.Sprintf("priv-%d-%d", tunnel, i),
  467. "publicKey": fmt.Sprintf("pub-%d-%d", tunnel, i),
  468. "allowedIPs": []string{fmt.Sprintf("10.%d.0.%d/32", tunnel, i+2)},
  469. "preSharedKey": fmt.Sprintf("psk-%d-%d", tunnel, i),
  470. "keepAlive": 25 * tunnel,
  471. }
  472. }
  473. }
  474. cases := []struct {
  475. name string
  476. inbounds []snapInbound
  477. }{
  478. {"vless with and without flow", []snapInbound{
  479. {"n1-reality", model.VLESS, vlessEntry("xtls-rprx-vision")},
  480. {"n1-ws", model.VLESS, vlessEntry("")},
  481. }},
  482. {"wireguard and vless", []snapInbound{
  483. {"n1-wg", model.WireGuard, wgEntry(1)},
  484. {"n1-vless", model.VLESS, vlessEntry("")},
  485. }},
  486. {"two wireguard tunnels", []snapInbound{
  487. {"n1-wg-a", model.WireGuard, wgEntry(1)},
  488. {"n1-wg-b", model.WireGuard, wgEntry(2)},
  489. }},
  490. }
  491. for _, tc := range cases {
  492. t.Run(tc.name, func(t *testing.T) {
  493. db := initTrafficTestDB(t)
  494. svc := &InboundService{}
  495. emails := make([]string, clientCount)
  496. stats := make([]xray.ClientTraffic, clientCount)
  497. for i := range emails {
  498. emails[i] = fmt.Sprintf("multi-%d", i)
  499. stats[i] = xray.ClientTraffic{Email: emails[i], Up: 1000, Down: 2000, Enable: true, LastOnline: 5000}
  500. }
  501. settings := make([]string, len(tc.inbounds))
  502. for n, ib := range tc.inbounds {
  503. entries := make([]map[string]any, clientCount)
  504. for i, email := range emails {
  505. entries[i] = ib.entry(i, email)
  506. }
  507. raw, err := json.Marshal(map[string]any{"clients": entries})
  508. if err != nil {
  509. t.Fatalf("marshal settings: %v", err)
  510. }
  511. settings[n] = string(raw)
  512. nodeID := 1
  513. if err := db.Create(&model.Inbound{
  514. UserId: 1, NodeID: &nodeID, Tag: ib.tag, Enable: true,
  515. Port: 42000 + n, Protocol: ib.protocol, Settings: settings[n],
  516. }).Error; err != nil {
  517. t.Fatalf("create inbound %q: %v", ib.tag, err)
  518. }
  519. }
  520. merge := func(stats []xray.ClientTraffic) {
  521. t.Helper()
  522. // A node copies a multi-attached client's one counter onto every inbound it is on.
  523. snap := &runtime.TrafficSnapshot{}
  524. for n, ib := range tc.inbounds {
  525. snap.Inbounds = append(snap.Inbounds, &model.Inbound{
  526. Tag: ib.tag, Enable: true, Port: 42000 + n, Protocol: ib.protocol,
  527. Settings: settings[n], ClientStats: slices.Clone(stats),
  528. })
  529. }
  530. if _, err := svc.setRemoteTrafficLocked(1, snap, false, false); err != nil {
  531. t.Fatalf("setRemoteTrafficLocked: %v", err)
  532. }
  533. }
  534. merge(stats)
  535. if got := countClientRowWrites(t, func() { merge(stats) }); len(got) != 0 {
  536. t.Errorf("repeating an unchanged snapshot wrote client rows %v, want none", got)
  537. }
  538. active := slices.Clone(stats)
  539. active[0].Up, active[0].Down, active[0].LastOnline = 1300, 2600, 6000
  540. got := countClientRowWrites(t, func() { merge(active) })
  541. want := map[string]int64{"client_traffics": 1, "node_client_traffics": 1}
  542. if !maps.Equal(got, want) {
  543. t.Errorf("one client's traffic tick wrote client rows %v, want %v", got, want)
  544. }
  545. ct := readTraffic(t, db, emails[0])
  546. assertUpDown(t, ct, 300, 600, "active client after its tick")
  547. if ct.LastOnline != 6000 {
  548. t.Errorf("last_online = %d, want 6000", ct.LastOnline)
  549. }
  550. })
  551. }
  552. }
  553. func TestDelClientStat_CleansNodeBaselines(t *testing.T) {
  554. db := initTrafficTestDB(t)
  555. svc := &InboundService{}
  556. const email = "gone"
  557. if err := db.Create(&xray.ClientTraffic{InboundId: 1, Email: email, Enable: true}).Error; err != nil {
  558. t.Fatalf("seed client_traffics: %v", err)
  559. }
  560. if err := db.Create(&model.NodeClientTraffic{NodeId: 1, Email: email, Up: 10, Down: 10}).Error; err != nil {
  561. t.Fatalf("seed node baseline 1: %v", err)
  562. }
  563. if err := db.Create(&model.NodeClientTraffic{NodeId: 2, Email: email, Up: 20, Down: 20}).Error; err != nil {
  564. t.Fatalf("seed node baseline 2: %v", err)
  565. }
  566. if err := svc.DelClientStat(db, email); err != nil {
  567. t.Fatalf("DelClientStat: %v", err)
  568. }
  569. var cnt int64
  570. if err := db.Model(&model.NodeClientTraffic{}).Where("email = ?", email).Count(&cnt).Error; err != nil {
  571. t.Fatalf("count baselines: %v", err)
  572. }
  573. if cnt != 0 {
  574. t.Errorf("expected node baselines cleaned, found %d", cnt)
  575. }
  576. }
  577. func TestNodeDelete_CleansNodeBaselines(t *testing.T) {
  578. db := initTrafficTestDB(t)
  579. nodeSvc := NodeService{}
  580. if err := db.Create(&model.NodeClientTraffic{NodeId: 7, Email: "a", Up: 1, Down: 1}).Error; err != nil {
  581. t.Fatalf("seed node 7 a: %v", err)
  582. }
  583. if err := db.Create(&model.NodeClientTraffic{NodeId: 7, Email: "b", Up: 2, Down: 2}).Error; err != nil {
  584. t.Fatalf("seed node 7 b: %v", err)
  585. }
  586. if err := db.Create(&model.NodeClientTraffic{NodeId: 8, Email: "c", Up: 3, Down: 3}).Error; err != nil {
  587. t.Fatalf("seed node 8 c: %v", err)
  588. }
  589. if err := nodeSvc.Delete(7); err != nil {
  590. t.Fatalf("NodeService.Delete(7): %v", err)
  591. }
  592. var sevenCnt, eightCnt int64
  593. db.Model(&model.NodeClientTraffic{}).Where("node_id = ?", 7).Count(&sevenCnt)
  594. db.Model(&model.NodeClientTraffic{}).Where("node_id = ?", 8).Count(&eightCnt)
  595. if sevenCnt != 0 {
  596. t.Errorf("node 7 baselines not cleaned: %d remain", sevenCnt)
  597. }
  598. if eightCnt != 1 {
  599. t.Errorf("node 8 baseline should survive, found %d", eightCnt)
  600. }
  601. }