inbound_settings_stale_write_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. package service
  2. import (
  3. "context"
  4. "encoding/json"
  5. "net/http"
  6. "net/http/httptest"
  7. "net/url"
  8. "reflect"
  9. "strconv"
  10. "testing"
  11. "time"
  12. "github.com/mhsanaei/3x-ui/v3/internal/database"
  13. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  14. "github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
  15. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  16. "gorm.io/gorm"
  17. )
  18. // commitTickBetweenReadAndWrite parks the serial writer, lets op read the
  19. // inbound and queue its transaction, then commits tick ahead of that transaction.
  20. func commitTickBetweenReadAndWrite(t *testing.T, tick func(tx *gorm.DB) error, op func()) {
  21. t.Helper()
  22. resetTrafficWriterForTest(t)
  23. StartTrafficWriter()
  24. parked := make(chan struct{})
  25. release := make(chan struct{})
  26. tickErr := make(chan error, 1)
  27. go func() {
  28. tickErr <- submitTrafficWrite(func() error {
  29. close(parked)
  30. <-release
  31. return database.GetDB().Transaction(tick)
  32. })
  33. }()
  34. <-parked
  35. opDone := make(chan struct{})
  36. go func() {
  37. defer close(opDone)
  38. op()
  39. }()
  40. waitTrafficWriterQueued(t)
  41. close(release)
  42. if err := <-tickErr; err != nil {
  43. t.Fatalf("tick: %v", err)
  44. }
  45. <-opDone
  46. }
  47. // seedRenewableNeighbour builds an inbound holding a healthy client X and a
  48. // quota-disabled client Y whose auto-renew is due, as the traffic job sees them.
  49. func seedRenewableNeighbour(t *testing.T, port int, nodeID *int) *model.Inbound {
  50. t.Helper()
  51. past := time.Now().Add(-time.Hour).UnixMilli()
  52. future := time.Now().Add(30 * 24 * time.Hour).UnixMilli()
  53. clients := []model.Client{
  54. {Email: "x@stale", ID: "aaaaaaaa-0000-0000-0000-00000000000a", SubID: "sub-x", Enable: true, ExpiryTime: future},
  55. {Email: "y@stale", ID: "aaaaaaaa-0000-0000-0000-00000000000b", SubID: "sub-y", Enable: false, Reset: 30, ExpiryTime: past, TotalGB: 1000},
  56. }
  57. ib := &model.Inbound{
  58. Tag: "stale-" + strconv.Itoa(port), Enable: true, Port: port, Protocol: model.VLESS,
  59. Settings: clientsSettings(t, clients), NodeID: nodeID,
  60. }
  61. if err := database.GetDB().Create(ib).Error; err != nil {
  62. t.Fatalf("create inbound: %v", err)
  63. }
  64. if err := (&ClientService{}).SyncInbound(nil, ib.Id, clients); err != nil {
  65. t.Fatalf("SyncInbound: %v", err)
  66. }
  67. rows := []xray.ClientTraffic{
  68. {InboundId: ib.Id, Email: "x@stale", Enable: true, ExpiryTime: future},
  69. {InboundId: ib.Id, Email: "y@stale", Enable: false, Up: 600, Down: 400, Total: 1000, Reset: 30, ExpiryTime: past},
  70. }
  71. if err := database.GetDB().Create(&rows).Error; err != nil {
  72. t.Fatalf("seed client_traffics: %v", err)
  73. }
  74. return ib
  75. }
  76. func autoRenewTick(tx *gorm.DB) error {
  77. _, _, err := (&InboundService{}).autoRenewClients(tx, newTrafficMutationBatch())
  78. return err
  79. }
  80. // renewYTick writes the renewal autoRenewClients would commit for y@stale; it
  81. // skips clients hosted only on a node, so the node case applies it directly.
  82. func renewYTick(inboundId int) func(tx *gorm.DB) error {
  83. return func(tx *gorm.DB) error {
  84. var ib model.Inbound
  85. if err := tx.First(&ib, inboundId).Error; err != nil {
  86. return err
  87. }
  88. var settings map[string]any
  89. if err := json.Unmarshal([]byte(ib.Settings), &settings); err != nil {
  90. return err
  91. }
  92. for _, c := range settings["clients"].([]any) {
  93. if m := c.(map[string]any); m["email"] == "y@stale" {
  94. m["enable"] = true
  95. m["expiryTime"] = time.Now().Add(30 * 24 * time.Hour).UnixMilli()
  96. }
  97. }
  98. b, err := json.MarshalIndent(settings, "", " ")
  99. if err != nil {
  100. return err
  101. }
  102. return tx.Model(&model.Inbound{}).Where("id = ?", inboundId).Update("settings", string(b)).Error
  103. }
  104. }
  105. func settingsClient(t *testing.T, inboundId int, email string) (model.Client, bool) {
  106. t.Helper()
  107. ib, err := (&InboundService{}).GetInbound(inboundId)
  108. if err != nil {
  109. t.Fatalf("GetInbound: %v", err)
  110. }
  111. clients, err := (&InboundService{}).GetClients(ib)
  112. if err != nil {
  113. t.Fatalf("GetClients: %v", err)
  114. }
  115. for _, c := range clients {
  116. if c.Email == email {
  117. return c, true
  118. }
  119. }
  120. return model.Client{}, false
  121. }
  122. func requireNeighbourRenewed(t *testing.T, inboundId int) model.Client {
  123. t.Helper()
  124. y, ok := settingsClient(t, inboundId, "y@stale")
  125. if !ok {
  126. t.Fatal("neighbour y@stale missing from settings")
  127. }
  128. if now := time.Now().UnixMilli(); !y.Enable || y.ExpiryTime <= now {
  129. t.Fatalf("renewed neighbour rolled back in settings: enable=%v expiryTime=%d (now %d)", y.Enable, y.ExpiryTime, now)
  130. }
  131. return y
  132. }
  133. type staleClientOp struct {
  134. name string
  135. // advancesNodeFingerprint: on a node inbound the op pushes per client and
  136. // then advances the reconcile-skip fingerprint.
  137. advancesNodeFingerprint bool
  138. run func(t *testing.T, ib *model.Inbound) error
  139. }
  140. var staleClientOps = []staleClientOp{
  141. {"edit", true, func(t *testing.T, ib *model.Inbound) error {
  142. rec := lookupClientRecord(t, "x@stale")
  143. edited := rec.ToClient()
  144. edited.Comment = "edited"
  145. _, err := (&ClientService{}).UpdateInboundClient(&InboundService{}, &model.Inbound{
  146. Id: ib.Id, Settings: clientsSettings(t, []model.Client{*edited}),
  147. }, "x@stale")
  148. return err
  149. }},
  150. {"add", true, func(t *testing.T, ib *model.Inbound) error {
  151. _, err := (&ClientService{}).AddInboundClient(&InboundService{}, &model.Inbound{
  152. Id: ib.Id, Settings: clientsSettings(t, []model.Client{{Email: "z@stale", ID: "aaaaaaaa-0000-0000-0000-00000000000c", Enable: true}}),
  153. })
  154. return err
  155. }},
  156. {"delete", true, func(t *testing.T, ib *model.Inbound) error {
  157. _, err := (&ClientService{}).DelInboundClientByEmail(&InboundService{}, ib.Id, "x@stale", false, true)
  158. return err
  159. }},
  160. {"bulk detach", true, func(t *testing.T, ib *model.Inbound) error {
  161. _, _, err := (&ClientService{}).BulkDetach(&InboundService{}, []string{"x@stale"}, []int{ib.Id})
  162. return err
  163. }},
  164. {"bulk adjust", false, func(t *testing.T, ib *model.Inbound) error {
  165. _, _, err := (&ClientService{}).BulkAdjust(&InboundService{}, []string{"x@stale"}, 1, 0, "", nil, "")
  166. return err
  167. }},
  168. {"bulk delete", false, func(t *testing.T, ib *model.Inbound) error {
  169. _, _, err := (&ClientService{}).BulkDelete(&InboundService{}, []string{"x@stale"}, false)
  170. return err
  171. }},
  172. {"bulk set enable", true, func(t *testing.T, ib *model.Inbound) error {
  173. _, _, err := (&ClientService{}).BulkSetEnable(&InboundService{}, []string{"x@stale"}, false)
  174. return err
  175. }},
  176. }
  177. // Each client op reads the inbound before queueing its write; a renewal the
  178. // traffic writer commits in between must not be reverted to enable=false.
  179. func TestClientOpsKeepNeighbourRenewedMidOp(t *testing.T) {
  180. for i, op := range staleClientOps {
  181. t.Run(op.name, func(t *testing.T) {
  182. setupBulkDB(t)
  183. ib := seedRenewableNeighbour(t, 23101+i, nil)
  184. commitTickBetweenReadAndWrite(t, autoRenewTick, func() {
  185. if err := op.run(t, ib); err != nil {
  186. t.Errorf("%s: %v", op.name, err)
  187. }
  188. })
  189. requireNeighbourRenewed(t, ib.Id)
  190. })
  191. }
  192. }
  193. // An op on the renewed client itself keeps the fields it did not change.
  194. func TestBulkAdjustOnRenewedClientKeepsRenewal(t *testing.T) {
  195. setupBulkDB(t)
  196. ib := seedRenewableNeighbour(t, 23120, nil)
  197. commitTickBetweenReadAndWrite(t, autoRenewTick, func() {
  198. if _, _, err := (&ClientService{}).BulkAdjust(&InboundService{}, []string{"y@stale"}, 0, 500, "", nil, ""); err != nil {
  199. t.Errorf("BulkAdjust: %v", err)
  200. }
  201. })
  202. if y := requireNeighbourRenewed(t, ib.Id); y.TotalGB != 1500 {
  203. t.Fatalf("y@stale totalGB = %d, want 1500 (the adjust itself was lost)", y.TotalGB)
  204. }
  205. }
  206. // The node got only the per-client push, so the skip fingerprint must not claim
  207. // it also holds the renewal the traffic writer committed mid-op.
  208. func TestNodeClientOpsMidRenewalStillReconcileRenewal(t *testing.T) {
  209. for i, op := range staleClientOps {
  210. if !op.advancesNodeFingerprint {
  211. continue
  212. }
  213. t.Run(op.name, func(t *testing.T) {
  214. setupBulkDB(t)
  215. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
  216. w.Header().Set("Content-Type", "application/json")
  217. _, _ = w.Write([]byte(`{"success":true}`))
  218. }))
  219. t.Cleanup(srv.Close)
  220. u, err := url.Parse(srv.URL)
  221. if err != nil {
  222. t.Fatalf("parse url: %v", err)
  223. }
  224. port, _ := strconv.Atoi(u.Port())
  225. node := &model.Node{
  226. Name: "stale-node", Scheme: "http", Address: u.Hostname(), Port: port, BasePath: "/",
  227. ApiToken: "tok", Enable: true, Status: "online", AllowPrivateAddress: true,
  228. }
  229. if err := database.GetDB().Create(node).Error; err != nil {
  230. t.Fatalf("create node: %v", err)
  231. }
  232. remote := runtime.NewRemote(node, nil)
  233. useTestRuntimeManager(t).SetRuntimeOverride(node.Id, remote)
  234. ib := seedRenewableNeighbour(t, 23131+i, &node.Id)
  235. remote.AdoptInboundAlias(ib, runtime.RemoteInboundOption{Id: 7, Tag: ib.Tag})
  236. commitTickBetweenReadAndWrite(t, renewYTick(ib.Id), func() {
  237. if err := op.run(t, ib); err != nil {
  238. t.Errorf("%s: %v", op.name, err)
  239. }
  240. })
  241. requireNeighbourRenewed(t, ib.Id)
  242. saved, err := (&InboundService{}).GetInbound(ib.Id)
  243. if err != nil {
  244. t.Fatalf("GetInbound: %v", err)
  245. }
  246. pushed, err := remote.ReconcileInbound(context.Background(), saved, true)
  247. if err != nil {
  248. t.Fatalf("ReconcileInbound: %v", err)
  249. }
  250. if !pushed {
  251. t.Fatal("reconcile skipped the inbound: the node never receives y@stale's renewal")
  252. }
  253. })
  254. }
  255. }
  256. func TestRebaseClientSettings(t *testing.T) {
  257. const base = `{"decryption":"none","clients":[{"email":"a","enable":true,"totalGB":1},{"email":"b","enable":false,"expiryTime":1}]}`
  258. cases := []struct {
  259. name, ours, current, want string
  260. }{
  261. {
  262. name: "untouched client takes the committed version",
  263. ours: `{"decryption":"none","clients":[{"email":"a","enable":true,"totalGB":2},{"email":"b","enable":false,"expiryTime":1}]}`,
  264. current: `{"decryption":"none","clients":[{"email":"a","enable":true,"totalGB":1},{"email":"b","enable":true,"expiryTime":9}]}`,
  265. want: `{"decryption":"none","clients":[{"email":"a","enable":true,"totalGB":2},{"email":"b","enable":true,"expiryTime":9}]}`,
  266. },
  267. {
  268. name: "edited client keeps committed changes to fields the op left alone",
  269. ours: `{"decryption":"none","clients":[{"email":"a","enable":true,"totalGB":1},{"email":"b","enable":false,"expiryTime":1,"comment":"x"}]}`,
  270. current: `{"decryption":"none","clients":[{"email":"a","enable":true,"totalGB":1},{"email":"b","enable":true,"expiryTime":9}]}`,
  271. want: `{"decryption":"none","clients":[{"email":"a","enable":true,"totalGB":1},{"email":"b","enable":true,"expiryTime":9,"comment":"x"}]}`,
  272. },
  273. {
  274. name: "client the op removed stays removed",
  275. ours: `{"decryption":"none","clients":[{"email":"a","enable":true,"totalGB":1}]}`,
  276. current: `{"decryption":"none","clients":[{"email":"a","enable":true,"totalGB":1},{"email":"b","enable":true,"expiryTime":9}]}`,
  277. want: `{"decryption":"none","clients":[{"email":"a","enable":true,"totalGB":1}]}`,
  278. },
  279. {
  280. name: "client committed after the read is kept",
  281. ours: `{"decryption":"none","clients":[{"email":"a","enable":false,"totalGB":1},{"email":"b","enable":false,"expiryTime":1}]}`,
  282. current: `{"decryption":"none","clients":[{"email":"a","enable":true,"totalGB":1},{"email":"b","enable":false,"expiryTime":1},{"email":"c"}]}`,
  283. want: `{"decryption":"none","clients":[{"email":"a","enable":false,"totalGB":1},{"email":"b","enable":false,"expiryTime":1},{"email":"c"}]}`,
  284. },
  285. {
  286. name: "untouched client removed after the read stays removed",
  287. ours: `{"decryption":"none","clients":[{"email":"a","enable":false,"totalGB":1},{"email":"b","enable":false,"expiryTime":1}]}`,
  288. current: `{"decryption":"none","clients":[{"email":"a","enable":true,"totalGB":1}]}`,
  289. want: `{"decryption":"none","clients":[{"email":"a","enable":false,"totalGB":1}]}`,
  290. },
  291. {
  292. name: "top-level key follows whichever side changed it",
  293. ours: `{"decryption":"none","testseed":[1],"clients":[{"email":"a","enable":true,"totalGB":1},{"email":"b","enable":false,"expiryTime":1}]}`,
  294. current: `{"decryption":"mlkem","clients":[{"email":"a","enable":true,"totalGB":1},{"email":"b","enable":false,"expiryTime":1}]}`,
  295. want: `{"decryption":"mlkem","testseed":[1],"clients":[{"email":"a","enable":true,"totalGB":1},{"email":"b","enable":false,"expiryTime":1}]}`,
  296. },
  297. }
  298. for _, tc := range cases {
  299. t.Run(tc.name, func(t *testing.T) {
  300. got, err := rebaseClientSettings(base, tc.ours, tc.current)
  301. if err != nil {
  302. t.Fatalf("rebaseClientSettings: %v", err)
  303. }
  304. var gotV, wantV any
  305. if err := json.Unmarshal([]byte(got), &gotV); err != nil {
  306. t.Fatalf("unmarshal got: %v", err)
  307. }
  308. if err := json.Unmarshal([]byte(tc.want), &wantV); err != nil {
  309. t.Fatalf("unmarshal want: %v", err)
  310. }
  311. if !reflect.DeepEqual(gotV, wantV) {
  312. t.Fatalf("rebase = %s\nwant %s", got, tc.want)
  313. }
  314. })
  315. }
  316. }