global_traffic_test.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. package service
  2. import (
  3. "testing"
  4. "time"
  5. "github.com/mhsanaei/3x-ui/v3/internal/database"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  7. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  8. )
  9. func seedClientRow(t *testing.T, email string, inboundId int, up, down, total int64) {
  10. t.Helper()
  11. db := database.GetDB()
  12. if err := db.Create(&xray.ClientTraffic{InboundId: inboundId, Email: email, Enable: true, Up: up, Down: down, Total: total}).Error; err != nil {
  13. t.Fatalf("seed client_traffics %q: %v", email, err)
  14. }
  15. }
  16. func TestAcceptGlobalTraffic_SideTableOnly(t *testing.T) {
  17. db := initTrafficTestDB(t)
  18. svc := &InboundService{}
  19. seedClientRow(t, "alice", 1, 100, 100, 0)
  20. err := svc.AcceptGlobalTraffic("master-a", []*xray.ClientTraffic{
  21. {Email: "alice", Up: 900, Down: 800},
  22. {Email: "ghost", Up: 5, Down: 5}, // not hosted here — must be dropped
  23. })
  24. if err != nil {
  25. t.Fatalf("AcceptGlobalTraffic: %v", err)
  26. }
  27. local := readTraffic(t, db, "alice")
  28. if local.Up != 100 || local.Down != 100 {
  29. t.Errorf("local counters must stay pure, got up=%d down=%d", local.Up, local.Down)
  30. }
  31. var globals []model.ClientGlobalTraffic
  32. if err := db.Find(&globals).Error; err != nil {
  33. t.Fatalf("read globals: %v", err)
  34. }
  35. if len(globals) != 1 || globals[0].Email != "alice" || globals[0].Up != 900 || globals[0].Down != 800 {
  36. t.Errorf("unexpected globals: %+v", globals)
  37. }
  38. }
  39. func TestAcceptGlobalTraffic_OverwriteAndMultiMaster(t *testing.T) {
  40. db := initTrafficTestDB(t)
  41. svc := &InboundService{}
  42. seedClientRow(t, "alice", 1, 0, 0, 0)
  43. must := func(guid string, up, down int64) {
  44. t.Helper()
  45. if err := svc.AcceptGlobalTraffic(guid, []*xray.ClientTraffic{{Email: "alice", Up: up, Down: down}}); err != nil {
  46. t.Fatalf("AcceptGlobalTraffic(%s): %v", guid, err)
  47. }
  48. }
  49. must("master-a", 900, 900)
  50. must("master-a", 50, 50) // a master-side reset propagates by overwrite
  51. must("master-b", 500, 400)
  52. rows := []*xray.ClientTraffic{{Email: "alice", Up: 10, Down: 10}}
  53. overlayGlobalTraffic(db, rows)
  54. if rows[0].Up != 500 || rows[0].Down != 400 {
  55. t.Errorf("overlay should fold per-master max, got up=%d down=%d", rows[0].Up, rows[0].Down)
  56. }
  57. }
  58. func TestDepletedCond_ProbeGuard(t *testing.T) {
  59. db := initTrafficTestDB(t)
  60. svc := &InboundService{}
  61. // No global rows: the cross-panel EXISTS branch is skipped (#5392), but a
  62. // client over its local quota is still disabled.
  63. if got, _ := depletedCond(db); got != depletedClientsCondLocal {
  64. t.Fatalf("empty globals must use the local-only predicate")
  65. }
  66. seedClientRow(t, "local-cap", 1, 600, 600, 1000)
  67. if _, count, err := svc.disableInvalidClients(db); err != nil {
  68. t.Fatalf("disableInvalidClients: %v", err)
  69. } else if count != 1 {
  70. t.Fatalf("local over-quota client must be disabled, disabled %d", count)
  71. }
  72. // Once a master pushes a global row, the full predicate is used so combined
  73. // quota is enforced.
  74. if err := svc.AcceptGlobalTraffic("master-a", []*xray.ClientTraffic{{Email: "local-cap", Up: 1, Down: 1}}); err != nil {
  75. t.Fatalf("AcceptGlobalTraffic: %v", err)
  76. }
  77. if got, _ := depletedCond(db); got != depletedClientsCond {
  78. t.Fatalf("with globals present the cross-panel predicate must be used")
  79. }
  80. }
  81. // A master that stopped pushing leaves its last snapshot behind forever. Those
  82. // frozen counters must not keep enforcing quota, or a client well inside its
  83. // limit is disabled on every traffic poll with no way back (#6113).
  84. func TestStaleGlobalTraffic_Ignored(t *testing.T) {
  85. db := initTrafficTestDB(t)
  86. svc := &InboundService{}
  87. staleAt := time.Now().Add(-globalTrafficFreshWindow - time.Hour).UnixMilli()
  88. seedStaleGlobal := func(t *testing.T, guid, email string, up, down int64) {
  89. t.Helper()
  90. if err := svc.AcceptGlobalTraffic(guid, []*xray.ClientTraffic{{Email: email, Up: up, Down: down}}); err != nil {
  91. t.Fatalf("AcceptGlobalTraffic(%s): %v", guid, err)
  92. }
  93. if err := db.Model(&model.ClientGlobalTraffic{}).
  94. Where("master_guid = ? AND email = ?", guid, email).
  95. Update("updated_at", staleAt).Error; err != nil {
  96. t.Fatalf("age row: %v", err)
  97. }
  98. }
  99. t.Run("stale row alone neither enforces nor selects the cross-panel predicate", func(t *testing.T) {
  100. // 200 of 1000 used locally, 1900 reported by a master gone for a day.
  101. seedClientRow(t, "cap", 1, 100, 100, 1000)
  102. seedStaleGlobal(t, "dead-master", "cap", 1000, 900)
  103. if got, _ := depletedCond(db); got != depletedClientsCondLocal {
  104. t.Fatalf("only stale globals must fall back to the local-only predicate")
  105. }
  106. if _, count, err := svc.disableInvalidClients(db); err != nil {
  107. t.Fatalf("disableInvalidClients: %v", err)
  108. } else if count != 0 {
  109. t.Fatalf("stale global usage must not disable a client, disabled %d", count)
  110. }
  111. if got := readTraffic(t, db, "cap"); !got.Enable {
  112. t.Error("client within its local quota must stay enabled")
  113. }
  114. })
  115. t.Run("stale row does not inflate the displayed total", func(t *testing.T) {
  116. rows := []*xray.ClientTraffic{{Email: "cap", Up: 100, Down: 100}}
  117. overlayGlobalTraffic(db, rows)
  118. if rows[0].Up != 100 || rows[0].Down != 100 {
  119. t.Errorf("stale global must not overlay display counters, got up=%d down=%d", rows[0].Up, rows[0].Down)
  120. }
  121. })
  122. t.Run("a live master still enforces alongside the stale row", func(t *testing.T) {
  123. // This is the #6113 shape: one dead master frozen over quota, one live
  124. // master well under it. Only the live one may decide.
  125. if err := svc.AcceptGlobalTraffic("live-master", []*xray.ClientTraffic{{Email: "cap", Up: 1, Down: 1}}); err != nil {
  126. t.Fatalf("AcceptGlobalTraffic: %v", err)
  127. }
  128. if got, _ := depletedCond(db); got != depletedClientsCond {
  129. t.Fatalf("a fresh global row must select the cross-panel predicate")
  130. }
  131. if _, count, err := svc.disableInvalidClients(db); err != nil {
  132. t.Fatalf("disableInvalidClients: %v", err)
  133. } else if count != 0 {
  134. t.Fatalf("the live master reports usage well under quota, disabled %d", count)
  135. }
  136. // And once the live master reports real depletion, it takes effect.
  137. if err := svc.AcceptGlobalTraffic("live-master", []*xray.ClientTraffic{{Email: "cap", Up: 600, Down: 500}}); err != nil {
  138. t.Fatalf("AcceptGlobalTraffic: %v", err)
  139. }
  140. if _, count, err := svc.disableInvalidClients(db); err != nil {
  141. t.Fatalf("disableInvalidClients: %v", err)
  142. } else if count != 1 {
  143. t.Fatalf("fresh cross-panel depletion must disable the client, disabled %d", count)
  144. }
  145. })
  146. }
  147. func TestGlobalUsage_DisablesClient(t *testing.T) {
  148. db := initTrafficTestDB(t)
  149. svc := &InboundService{}
  150. // 200 of 1000 used locally — local check alone would never trip.
  151. seedClientRow(t, "cap", 1, 100, 100, 1000)
  152. if err := svc.AcceptGlobalTraffic("master-a", []*xray.ClientTraffic{{Email: "cap", Up: 800, Down: 700}}); err != nil {
  153. t.Fatalf("AcceptGlobalTraffic: %v", err)
  154. }
  155. if _, count, err := svc.disableInvalidClients(db); err != nil {
  156. t.Fatalf("disableInvalidClients: %v", err)
  157. } else if count != 1 {
  158. t.Fatalf("expected 1 client disabled, got %d", count)
  159. }
  160. if got := readTraffic(t, db, "cap"); got.Enable {
  161. t.Error("client should be disabled by global usage exceeding its quota")
  162. }
  163. }
  164. func TestGlobalRows_ClearedOnReset(t *testing.T) {
  165. db := initTrafficTestDB(t)
  166. svc := &InboundService{}
  167. seedClientRow(t, "alice", 1, 50, 50, 1000)
  168. if err := svc.AcceptGlobalTraffic("master-a", []*xray.ClientTraffic{{Email: "alice", Up: 999, Down: 999}}); err != nil {
  169. t.Fatalf("AcceptGlobalTraffic: %v", err)
  170. }
  171. if err := svc.ResetClientTrafficByEmail("alice"); err != nil {
  172. t.Fatalf("ResetClientTrafficByEmail: %v", err)
  173. }
  174. var cnt int64
  175. db.Model(&model.ClientGlobalTraffic{}).Count(&cnt)
  176. if cnt != 0 {
  177. t.Errorf("global rows should be cleared on reset, found %d", cnt)
  178. }
  179. }
  180. // The full inbound list doubles as the traffic snapshot masters poll, so it
  181. // must report pure local counters; the slim list only feeds this panel's UI,
  182. // so it carries the cross-panel overlay.
  183. func TestSnapshotListNotOverlaid_SlimOverlaid(t *testing.T) {
  184. db := initTrafficTestDB(t)
  185. svc := &InboundService{}
  186. settings := `{"clients": [{"email": "alice", "enable": true}]}`
  187. ib := &model.Inbound{UserId: 1, Tag: "in-a", Enable: true, Port: 42001, Protocol: model.VLESS, Settings: settings}
  188. if err := db.Create(ib).Error; err != nil {
  189. t.Fatalf("create inbound: %v", err)
  190. }
  191. seedClientRow(t, "alice", ib.Id, 100, 100, 0)
  192. if err := svc.AcceptGlobalTraffic("master-a", []*xray.ClientTraffic{{Email: "alice", Up: 900, Down: 900}}); err != nil {
  193. t.Fatalf("AcceptGlobalTraffic: %v", err)
  194. }
  195. full, err := svc.GetInbounds(1)
  196. if err != nil {
  197. t.Fatalf("GetInbounds: %v", err)
  198. }
  199. if len(full) != 1 || len(full[0].ClientStats) != 1 {
  200. t.Fatalf("unexpected full list shape: %d inbounds", len(full))
  201. }
  202. if full[0].ClientStats[0].Up != 100 {
  203. t.Errorf("full list (master snapshot) must stay un-overlaid, got up=%d", full[0].ClientStats[0].Up)
  204. }
  205. slim, err := svc.GetInboundsSlim(1)
  206. if err != nil {
  207. t.Fatalf("GetInboundsSlim: %v", err)
  208. }
  209. if len(slim) != 1 || len(slim[0].ClientStats) != 1 {
  210. t.Fatalf("unexpected slim list shape: %d inbounds", len(slim))
  211. }
  212. if slim[0].ClientStats[0].Up != 900 {
  213. t.Errorf("slim list should carry the global overlay, got up=%d", slim[0].ClientStats[0].Up)
  214. }
  215. }