node_traffic_sync_ip_push_test.go 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. package job
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net/http"
  6. "net/http/httptest"
  7. "path/filepath"
  8. "slices"
  9. "strconv"
  10. "strings"
  11. "sync"
  12. "testing"
  13. "time"
  14. "github.com/op/go-logging"
  15. "github.com/mhsanaei/3x-ui/v3/internal/database"
  16. "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
  17. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  18. xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger"
  19. "github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
  20. "github.com/mhsanaei/3x-ui/v3/internal/web/service"
  21. )
  22. // A node's IP-limit job only reads rows for its own clients, so pushing the
  23. // whole table made every node store and echo back the entire fleet's IPs.
  24. func TestNodeTrafficSyncPushesOnlyHostedClientIps(t *testing.T) {
  25. xuilogger.InitLogger(logging.ERROR)
  26. dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db"))
  27. service.StartTrafficWriter()
  28. t.Cleanup(service.StopTrafficWriter)
  29. runtime.SetManager(runtime.NewManager(runtime.LocalDeps{APIPort: func() int { return 0 }, SetNeedRestart: func() {}}))
  30. t.Cleanup(func() { runtime.SetManager(nil) })
  31. var mu sync.Mutex
  32. pushed := map[string][]string{}
  33. now := time.Now().Unix()
  34. for i, email := range []string{"a@node", "b@node"} {
  35. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  36. w.Header().Set("Content-Type", "application/json")
  37. switch {
  38. case strings.HasSuffix(r.URL.Path, "inbounds/list"):
  39. settings := fmt.Sprintf(`{"clients":[{"email":%q,"id":"0000000%d-0000-4000-8000-000000000000","enable":true}],"decryption":"none"}`, email, i)
  40. ib, _ := json.Marshal([]map[string]any{{
  41. "id": 1, "tag": fmt.Sprintf("in-%d", 20000+i), "port": 20000 + i, "protocol": "vless", "enable": true,
  42. "settings": settings, "streamSettings": `{"network":"tcp"}`, "sniffing": `{}`,
  43. "clientStats": []map[string]any{{"email": email, "enable": true}},
  44. }})
  45. _, _ = w.Write([]byte(`{"success":true,"obj":` + string(ib) + `}`))
  46. return
  47. case strings.HasSuffix(r.URL.Path, "server/clientIps") && r.Method == http.MethodPost:
  48. var rows []model.InboundClientIps
  49. _ = json.NewDecoder(r.Body).Decode(&rows)
  50. mu.Lock()
  51. for _, row := range rows {
  52. pushed[email] = append(pushed[email], row.ClientEmail)
  53. }
  54. mu.Unlock()
  55. }
  56. _, _ = w.Write([]byte(`{"success":true}`))
  57. }))
  58. t.Cleanup(srv.Close)
  59. host, port, _ := strings.Cut(strings.TrimPrefix(srv.URL, "http://"), ":")
  60. portNum, _ := strconv.Atoi(port)
  61. if err := database.GetDB().Create(&model.Node{
  62. Name: email, Scheme: "http", Address: host, Port: portNum, BasePath: "/", ApiToken: "tok",
  63. Enable: true, Status: "online", AllowPrivateAddress: true, TlsVerifyMode: "verify",
  64. }).Error; err != nil {
  65. t.Fatalf("create node: %v", err)
  66. }
  67. if err := database.GetDB().Create(&model.InboundClientIps{
  68. ClientEmail: email, Ips: fmt.Sprintf(`[{"ip":"10.0.0.%d","timestamp":%d}]`, i+1, now),
  69. }).Error; err != nil {
  70. t.Fatalf("seed client ips: %v", err)
  71. }
  72. }
  73. NewNodeTrafficSyncJob().Run()
  74. for _, email := range []string{"a@node", "b@node"} {
  75. if got := pushed[email]; !slices.Equal(got, []string{email}) {
  76. t.Errorf("node hosting %s received IP rows for %v, want only [%s]", email, got, email)
  77. }
  78. }
  79. }