tuic_job_test.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. package job
  2. import (
  3. "crypto/rand"
  4. "crypto/rsa"
  5. "crypto/x509"
  6. "crypto/x509/pkix"
  7. "encoding/pem"
  8. "fmt"
  9. "math/big"
  10. "net"
  11. "path/filepath"
  12. "testing"
  13. "time"
  14. "github.com/mhsanaei/3x-ui/v3/internal/database"
  15. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  16. "github.com/mhsanaei/3x-ui/v3/internal/tuic"
  17. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  18. )
  19. func generateTestCertForJob(t *testing.T) (certPEM, keyPEM []byte) {
  20. t.Helper()
  21. priv, err := rsa.GenerateKey(rand.Reader, 2048)
  22. if err != nil {
  23. t.Fatalf("rsa.GenerateKey: %v", err)
  24. }
  25. template := x509.Certificate{
  26. SerialNumber: big.NewInt(1),
  27. Subject: pkix.Name{CommonName: "localhost"},
  28. NotBefore: time.Now().Add(-time.Hour),
  29. NotAfter: time.Now().Add(time.Hour),
  30. IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
  31. }
  32. der, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
  33. if err != nil {
  34. t.Fatalf("CreateCertificate: %v", err)
  35. }
  36. certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
  37. keyPEM = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
  38. return
  39. }
  40. func TestTuicJob_TrafficAccounting(t *testing.T) {
  41. if err := database.InitDB(filepath.Join(t.TempDir(), "tuic_job.db")); err != nil {
  42. t.Fatalf("database.InitDB failed: %v", err)
  43. }
  44. t.Cleanup(func() { _ = database.CloseDB() })
  45. certPEM, keyPEM := generateTestCertForJob(t)
  46. pc, err := net.ListenPacket("udp", "127.0.0.1:0")
  47. if err != nil {
  48. t.Fatal(err)
  49. }
  50. port := pc.LocalAddr().(*net.UDPAddr).Port
  51. _ = pc.Close()
  52. email := "[email protected]"
  53. settings := fmt.Sprintf(`{
  54. "certificate": %q,
  55. "private_key": %q,
  56. "congestion_control": "bbr",
  57. "alpn": ["h3"],
  58. "udp_relay_mode": "native",
  59. "zero_rtt_handshake": false,
  60. "clients": [
  61. {
  62. "uuid": "a0000000-0000-0000-0000-000000000001",
  63. "password": "password123",
  64. "email": %q,
  65. "enable": true
  66. }
  67. ]
  68. }`, string(certPEM), string(keyPEM), email)
  69. inbound := &model.Inbound{
  70. Id: 42,
  71. Tag: "tuic-in-42",
  72. Protocol: model.TUIC,
  73. Listen: "127.0.0.1",
  74. Port: port,
  75. Enable: true,
  76. Settings: settings,
  77. }
  78. if err := database.GetDB().Create(inbound).Error; err != nil {
  79. t.Fatalf("create inbound failed: %v", err)
  80. }
  81. clientTraffic := &xray.ClientTraffic{
  82. InboundId: inbound.Id,
  83. Email: email,
  84. Up: 0,
  85. Down: 0,
  86. Enable: true,
  87. }
  88. if err := database.GetDB().Create(clientTraffic).Error; err != nil {
  89. t.Fatalf("create clientTraffic failed: %v", err)
  90. }
  91. mgr := tuic.GetManager()
  92. t.Cleanup(mgr.StopAll)
  93. job := NewTuicJob()
  94. // Initial run reconciles desired instances and starts the server
  95. job.Run()
  96. // Add test traffic to the running client
  97. const wantUp = int64(1024)
  98. const wantDown = int64(2048)
  99. if !mgr.AddTestTraffic(inbound.Id, email, wantUp, wantDown) {
  100. t.Fatalf("failed to add test traffic for %s on inbound %d", email, inbound.Id)
  101. }
  102. // Second run collects and writes traffic to database
  103. job.Run()
  104. // Verify client traffic in database
  105. var dbClient xray.ClientTraffic
  106. if err := database.GetDB().Where("inbound_id = ? AND email = ?", inbound.Id, email).First(&dbClient).Error; err != nil {
  107. t.Fatalf("find client traffic in DB failed: %v", err)
  108. }
  109. if dbClient.Up != wantUp || dbClient.Down != wantDown {
  110. t.Fatalf("client traffic mismatch: got up=%d down=%d, want up=%d down=%d", dbClient.Up, dbClient.Down, wantUp, wantDown)
  111. }
  112. // Verify inbound total traffic in database: TuicJob leaves inbound total
  113. // accounting to xray_traffic_job (metered on the SOCKS relay tag, matching mtproto),
  114. // preventing double-counting.
  115. var dbInbound model.Inbound
  116. if err := database.GetDB().First(&dbInbound, inbound.Id).Error; err != nil {
  117. t.Fatalf("find inbound in DB failed: %v", err)
  118. }
  119. if dbInbound.Up != 0 || dbInbound.Down != 0 {
  120. t.Fatalf("expected inbound traffic to remain 0 in TuicJob (metered by Xray bridge), got up=%d down=%d", dbInbound.Up, dbInbound.Down)
  121. }
  122. }
  123. func TestAggregateTuicClientTrafficSumsAcrossInbounds(t *testing.T) {
  124. got := aggregateTuicClientTraffic([]tuic.ClientTrafficDelta{
  125. {Email: "[email protected]", Up: 100, Down: 200},
  126. {Email: "[email protected]", Up: 300, Down: 400},
  127. {Email: "[email protected]", Up: 5, Down: 6},
  128. }, []string{"[email protected]", "[email protected]"})
  129. byEmail := make(map[string]struct{ up, down int64 }, len(got))
  130. for _, traffic := range got {
  131. byEmail[traffic.Email] = struct{ up, down int64 }{traffic.Up, traffic.Down}
  132. }
  133. if shared := byEmail["[email protected]"]; shared.up != 400 || shared.down != 600 {
  134. t.Fatalf("shared client traffic = %+v, want (400, 600)", shared)
  135. }
  136. if other := byEmail["[email protected]"]; other.up != 5 || other.down != 6 {
  137. t.Fatalf("other client traffic = %+v, want (5, 6)", other)
  138. }
  139. if online, ok := byEmail["[email protected]"]; !ok || online.up != 0 || online.down != 0 {
  140. t.Fatalf("online-only client traffic = %+v, present=%v", online, ok)
  141. }
  142. if len(got) != 3 {
  143. t.Fatalf("got %d aggregated clients, want 3", len(got))
  144. }
  145. }
  146. func TestAggregateTuicClientTrafficPreservesStableIdentityAcrossEmailRename(t *testing.T) {
  147. const (
  148. inboundID = 82
  149. clientUUID = "a0000000-0000-0000-0000-000000000082"
  150. )
  151. got := aggregateTuicClientTraffic([]tuic.ClientTrafficDelta{
  152. {Email: "[email protected]", UUID: clientUUID, InboundID: inboundID, Up: 10, Down: 20},
  153. {Email: "[email protected]", UUID: clientUUID, InboundID: inboundID, Up: 30, Down: 40},
  154. }, nil)
  155. if len(got) != 1 {
  156. t.Fatalf("aggregate returned %d records, want 1", len(got))
  157. }
  158. if got[0].Email != "[email protected]" || got[0].TuicUUID != clientUUID || got[0].TuicInboundId != inboundID {
  159. t.Fatalf("aggregate lost retired TUIC identity: %+v", got[0])
  160. }
  161. if got[0].Up != 40 || got[0].Down != 60 {
  162. t.Fatalf("aggregate counters = (%d,%d), want (40,60)", got[0].Up, got[0].Down)
  163. }
  164. }