ldap_sync_hung_node_test.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. package job
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "io"
  6. "net/http"
  7. "net/http/httptest"
  8. "strconv"
  9. "strings"
  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. )
  17. // ldapHungNodeInbound seeds an online node whose client writes hang until the gate
  18. // opens, plus one inbound on it holding the given enabled clients.
  19. func ldapHungNodeInbound(t *testing.T, emails []string) (*resetGate, *model.Inbound) {
  20. t.Helper()
  21. initLdapJobDB(t)
  22. runtime.SetManager(runtime.NewManager(runtime.LocalDeps{APIPort: func() int { return 0 }, SetNeedRestart: func() {}}))
  23. t.Cleanup(func() { runtime.SetManager(nil) })
  24. gate := &resetGate{release: make(chan struct{})}
  25. const tag = "ldap-node-in"
  26. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  27. _, _ = io.Copy(io.Discard, r.Body)
  28. w.Header().Set("Content-Type", "application/json")
  29. if r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "inbounds/list") {
  30. _, _ = w.Write([]byte(`{"success":true,"obj":[{"id":1,"tag":"` + tag + `"}]}`))
  31. return
  32. }
  33. if r.Method == http.MethodPost {
  34. gate.entered.Add(1)
  35. select {
  36. case <-r.Context().Done():
  37. case <-gate.release:
  38. }
  39. }
  40. _, _ = w.Write([]byte(`{"success":true}`))
  41. }))
  42. t.Cleanup(srv.Close)
  43. t.Cleanup(gate.open)
  44. host, port, _ := strings.Cut(strings.TrimPrefix(srv.URL, "http://"), ":")
  45. portNum, _ := strconv.Atoi(port)
  46. db := database.GetDB()
  47. node := &model.Node{
  48. Name: "ldap-node", Scheme: "http", Address: host, Port: portNum, BasePath: "/", ApiToken: "tok",
  49. Enable: true, Status: "online", AllowPrivateAddress: true, TlsVerifyMode: "verify",
  50. }
  51. if err := db.Create(node).Error; err != nil {
  52. t.Fatalf("create node: %v", err)
  53. }
  54. clients := make([]model.Client, 0, len(emails))
  55. for i, email := range emails {
  56. clients = append(clients, model.Client{Email: email, ID: fmt.Sprintf("00000000-0000-4000-8000-%012d", i), Enable: true})
  57. }
  58. settings, _ := json.Marshal(map[string]any{"clients": clients, "decryption": "none"})
  59. ib := &model.Inbound{
  60. UserId: 1, Enable: true, Port: 47200, Protocol: model.VLESS, NodeID: &node.Id,
  61. Tag: tag, Settings: string(settings), StreamSettings: `{"network":"tcp"}`,
  62. }
  63. if err := db.Create(ib).Error; err != nil {
  64. t.Fatalf("create inbound: %v", err)
  65. }
  66. for _, c := range clients {
  67. rec := model.ClientRecord{Email: c.Email, UUID: c.ID, Enable: true}
  68. if err := db.Create(&rec).Error; err != nil {
  69. t.Fatalf("create client record: %v", err)
  70. }
  71. if err := db.Create(&model.ClientInbound{ClientId: rec.Id, InboundId: ib.Id}).Error; err != nil {
  72. t.Fatalf("link client: %v", err)
  73. }
  74. if err := db.Create(&xray.ClientTraffic{InboundId: ib.Id, Email: c.Email, Enable: true}).Error; err != nil {
  75. t.Fatalf("create client traffic: %v", err)
  76. }
  77. }
  78. return gate, ib
  79. }
  80. func inboundClientEnables(t *testing.T, inboundID int) map[string]bool {
  81. t.Helper()
  82. var ib model.Inbound
  83. if err := database.GetDB().First(&ib, inboundID).Error; err != nil {
  84. t.Fatalf("reload inbound: %v", err)
  85. }
  86. var settings struct {
  87. Clients []model.Client `json:"clients"`
  88. }
  89. if err := json.Unmarshal([]byte(ib.Settings), &settings); err != nil {
  90. t.Fatalf("parse settings: %v", err)
  91. }
  92. out := make(map[string]bool, len(settings.Clients))
  93. for _, c := range settings.Clients {
  94. out[c.Email] = c.Enable
  95. }
  96. return out
  97. }
  98. // Each LDAP user was disabled on its own, and every per-user push held the inbound
  99. // lock for the push timeout, so users sharing a hung node inbound queued behind it.
  100. func TestLdapBatchSetEnableDoesNotQueueUsersOnHungNode(t *testing.T) {
  101. emails := []string{"u1@ldap", "u2@ldap", "u3@ldap", "u4@ldap", "u5@ldap"}
  102. gate, ib := ldapHungNodeInbound(t, emails)
  103. done := make(chan struct{})
  104. go func() {
  105. defer close(done)
  106. NewLdapSyncJob().batchSetEnable(emails, false)
  107. }()
  108. t.Cleanup(func() { gate.open(); <-done })
  109. select {
  110. case <-done:
  111. case <-time.After(9 * time.Second):
  112. t.Fatalf("disabling %d LDAP users on one hung node inbound took over 9s (%d pushes started)", len(emails), gate.entered.Load())
  113. }
  114. for email, enabled := range inboundClientEnables(t, ib.Id) {
  115. if enabled {
  116. t.Errorf("client %s still enabled after the LDAP disable", email)
  117. }
  118. }
  119. }
  120. // Clients missing from LDAP were detached one at a time, each waiting out the push
  121. // timeout on a hung node inbound, so a directory cleanup could run for hours.
  122. func TestLdapDeleteDoesNotQueueClientsOnHungNode(t *testing.T) {
  123. emails := []string{"gone1@ldap", "gone2@ldap", "gone3@ldap", "gone4@ldap", "gone5@ldap"}
  124. gate, ib := ldapHungNodeInbound(t, emails)
  125. done := make(chan struct{})
  126. go func() {
  127. defer close(done)
  128. NewLdapSyncJob().deleteClientsNotInLDAP(ib.Tag, map[string]struct{}{})
  129. }()
  130. t.Cleanup(func() { gate.open(); <-done })
  131. select {
  132. case <-done:
  133. case <-time.After(9 * time.Second):
  134. t.Fatalf("detaching %d clients from one hung node inbound took over 9s (%d pushes started)", len(emails), gate.entered.Load())
  135. }
  136. if left := inboundClientEnables(t, ib.Id); len(left) != 0 {
  137. t.Errorf("clients still on the inbound after the LDAP cleanup: %v", left)
  138. }
  139. }