inbound_tuic_test.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. package service
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "strings"
  6. "testing"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database"
  8. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  9. "github.com/mhsanaei/3x-ui/v3/internal/tuic"
  10. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  11. )
  12. func TestInjectTuicSocks(t *testing.T) {
  13. cfg := &xray.Config{}
  14. inbounds := []*model.Inbound{
  15. {
  16. Id: 5,
  17. Tag: "tuic-in-5",
  18. Protocol: model.TUIC,
  19. Enable: true,
  20. Settings: `{
  21. "certificate": "dummy-cert",
  22. "private_key": "dummy-key",
  23. "clients": [
  24. {"uuid": "a0000000-0000-0000-0000-000000000001", "password": "pass1", "email": "[email protected]", "enable": true}
  25. ]
  26. }`,
  27. },
  28. }
  29. injectTuicSocks(cfg, inbounds)
  30. if len(cfg.InboundConfigs) != 1 {
  31. t.Fatalf("expected 1 injected SOCKS inbound, got %d", len(cfg.InboundConfigs))
  32. }
  33. sc := cfg.InboundConfigs[0]
  34. if sc.Tag != "tuic-in-5" {
  35. t.Fatalf("expected tag tuic-in-5, got %s", sc.Tag)
  36. }
  37. if sc.Protocol != "socks" {
  38. t.Fatalf("expected protocol socks, got %s", sc.Protocol)
  39. }
  40. expectedPort := tuic.SOCKSPortForInbound(5)
  41. if sc.Port != expectedPort {
  42. t.Fatalf("expected port %d, got %d", expectedPort, sc.Port)
  43. }
  44. if string(sc.Listen) != `"127.0.0.1"` {
  45. t.Fatalf("expected listen 127.0.0.1, got %s", sc.Listen)
  46. }
  47. if string(sc.Sniffing) != tuicEgressSniffingSettings {
  48. t.Fatalf("expected sniffing settings %s, got %s", tuicEgressSniffingSettings, sc.Sniffing)
  49. }
  50. var parsedSettings struct {
  51. Auth string `json:"auth"`
  52. UDP bool `json:"udp"`
  53. }
  54. if err := json.Unmarshal(sc.Settings, &parsedSettings); err != nil {
  55. t.Fatalf("failed to unmarshal settings: %v", err)
  56. }
  57. if parsedSettings.Auth != "noauth" || !parsedSettings.UDP {
  58. t.Fatalf("expected auth=noauth, udp=true, got %+v", parsedSettings)
  59. }
  60. }
  61. func TestCheckTuicSocksConflict(t *testing.T) {
  62. setupConflictDB(t)
  63. // Seed TUIC inbound with ID 10
  64. tuicIb := &model.Inbound{
  65. Id: 10,
  66. Tag: "tuic-10",
  67. Protocol: model.TUIC,
  68. Enable: true,
  69. Listen: "0.0.0.0",
  70. Port: 8443,
  71. Settings: `{"clients":[{"uuid":"a0000000-0000-0000-0000-000000000001","password":"p","email":"[email protected]"}]}`,
  72. }
  73. if err := database.GetDB().Create(tuicIb).Error; err != nil {
  74. t.Fatalf("failed to seed TUIC inbound: %v", err)
  75. }
  76. relayPort := tuic.SOCKSPortForInbound(10)
  77. // Try to create a new TCP inbound on that relayPort on 127.0.0.1
  78. newIb := &model.Inbound{
  79. Tag: "colliding-inbound",
  80. Protocol: model.Mixed,
  81. Enable: true,
  82. Listen: "127.0.0.1",
  83. Port: relayPort,
  84. }
  85. detail, err := checkTuicSocksConflict(database.GetDB(), newIb, 0, transportTCP)
  86. if err != nil {
  87. t.Fatalf("checkTuicSocksConflict error: %v", err)
  88. }
  89. if detail == nil {
  90. t.Fatalf("expected conflict on port %d, got none", relayPort)
  91. }
  92. if detail.Tag != "tuic-10" {
  93. t.Fatalf("expected conflict tag tuic-10, got %s", detail.Tag)
  94. }
  95. }
  96. func TestCheckTuicSocksReverseConflict(t *testing.T) {
  97. setupConflictDB(t)
  98. targetPort := tuic.SOCKSPortForInbound(20)
  99. // Seed existing inbound on targetPort on 127.0.0.1
  100. existing := &model.Inbound{
  101. Id: 99,
  102. Tag: "existing-on-relay-port",
  103. Protocol: model.Mixed,
  104. Enable: true,
  105. Listen: "127.0.0.1",
  106. Port: targetPort,
  107. }
  108. if err := database.GetDB().Create(existing).Error; err != nil {
  109. t.Fatalf("failed to seed existing inbound: %v", err)
  110. }
  111. detail, err := checkTuicSocksReverseConflict(database.GetDB(), 20)
  112. if err != nil {
  113. t.Fatalf("checkTuicSocksReverseConflict error: %v", err)
  114. }
  115. if detail == nil {
  116. t.Fatalf("expected reverse conflict for id 20 on port %d, got none", targetPort)
  117. }
  118. if detail.Tag != "existing-on-relay-port" {
  119. t.Fatalf("expected tag existing-on-relay-port, got %s", detail.Tag)
  120. }
  121. }
  122. func TestDesiredTuicInstances(t *testing.T) {
  123. setupConflictDB(t)
  124. ib := &model.Inbound{
  125. Id: 30,
  126. Tag: "tuic-desired-test",
  127. Protocol: model.TUIC,
  128. Enable: true,
  129. Listen: "0.0.0.0",
  130. Port: 9443,
  131. Settings: `{
  132. "certificate": "cert",
  133. "private_key": "key",
  134. "clients": [
  135. {"uuid": "a0000000-0000-0000-0000-000000000001", "password": "p1", "email": "[email protected]", "enable": true},
  136. {"uuid": "a0000000-0000-0000-0000-000000000002", "password": "p2", "email": "[email protected]", "enable": true}
  137. ]
  138. }`,
  139. }
  140. if err := database.GetDB().Create(ib).Error; err != nil {
  141. t.Fatalf("failed to seed inbound: %v", err)
  142. }
  143. // Add client traffic entry disabling [email protected]
  144. ct := &xray.ClientTraffic{
  145. InboundId: 30,
  146. Email: "[email protected]",
  147. Enable: false,
  148. }
  149. if err := database.GetDB().Create(ct).Error; err != nil {
  150. t.Fatalf("failed to seed client traffic: %v", err)
  151. }
  152. svc := &InboundService{}
  153. instances, err := svc.DesiredTuicInstances()
  154. if err != nil {
  155. t.Fatalf("DesiredTuicInstances failed: %v", err)
  156. }
  157. found := false
  158. for _, inst := range instances {
  159. if inst.Id == 30 {
  160. found = true
  161. if len(inst.Clients) != 1 || inst.Clients[0].Email != "[email protected]" {
  162. t.Fatalf("expected only [email protected], got %+v", inst.Clients)
  163. }
  164. }
  165. }
  166. if !found {
  167. t.Fatal("expected to find instance for inbound 30")
  168. }
  169. }
  170. func TestCheckForwardedPortsConflict_CollidesWithTuicSocksPort(t *testing.T) {
  171. setupConflictDB(t)
  172. seedInboundConflict(t, "tuic-1", "0.0.0.0", 8443, model.TUIC, ``, `{"clients":[{"uuid":"u","password":"p","email":"e"}]}`)
  173. var tuicInbound model.Inbound
  174. if err := database.GetDB().Where("tag = ?", "tuic-1").First(&tuicInbound).Error; err != nil {
  175. t.Fatalf("read seeded row: %v", err)
  176. }
  177. relayPort := tuic.SOCKSPortForInbound(tuicInbound.Id)
  178. svc := &InboundService{}
  179. ctx, err := svc.loadPortConflictContext(database.GetDB(), nil)
  180. if err != nil {
  181. t.Fatalf("loadPortConflictContext: %v", err)
  182. }
  183. hit := svc.checkForwardedPortsConflict(ctx, fmt.Sprintf("%d", relayPort))
  184. if !strings.Contains(hit, "SOCKS5") {
  185. t.Fatalf("expected a collision naming the TUIC inbound's SOCKS5 relay port, got %q", hit)
  186. }
  187. }
  188. func TestCheckTuicSocksConflict_DisabledInboundRetainsReservation(t *testing.T) {
  189. setupConflictDB(t)
  190. seedInboundConflict(t, "tuic-disabled", "0.0.0.0", 8443, model.TUIC, ``, `{"clients":[{"uuid":"u","password":"p","email":"e"}]}`)
  191. var tuicIb model.Inbound
  192. if err := database.GetDB().Where("tag = ?", "tuic-disabled").First(&tuicIb).Error; err != nil {
  193. t.Fatalf("read seeded row: %v", err)
  194. }
  195. if err := database.GetDB().Model(&tuicIb).Update("enable", false).Error; err != nil {
  196. t.Fatalf("disable inbound: %v", err)
  197. }
  198. relayPort := tuic.SOCKSPortForInbound(tuicIb.Id)
  199. testIb := &model.Inbound{
  200. Tag: "conflict-test",
  201. Protocol: model.VLESS,
  202. Listen: "127.0.0.1",
  203. Port: relayPort,
  204. Enable: true,
  205. }
  206. conflict, err := checkTuicSocksConflict(database.GetDB(), testIb, 0, transportTCP)
  207. if err != nil {
  208. t.Fatalf("checkTuicSocksConflict: %v", err)
  209. }
  210. if conflict == nil {
  211. t.Fatal("expected conflict on disabled TUIC inbound's SOCKS port, got nil")
  212. }
  213. if conflict.InboundID != tuicIb.Id {
  214. t.Fatalf("expected conflict with inbound %d, got %d", tuicIb.Id, conflict.InboundID)
  215. }
  216. }
  217. func TestCheckTuicSocksRelayCollision(t *testing.T) {
  218. setupConflictDB(t)
  219. // Seed first TUIC inbound with ID 1
  220. ib1 := &model.Inbound{
  221. Id: 1,
  222. Tag: "tuic-1",
  223. Protocol: model.TUIC,
  224. Enable: true,
  225. Listen: "0.0.0.0",
  226. Port: 8443,
  227. Settings: `{"clients":[{"uuid":"a0000000-0000-0000-0000-000000000001","password":"p","email":"[email protected]"}]}`,
  228. }
  229. if err := database.GetDB().Create(ib1).Error; err != nil {
  230. t.Fatalf("seed ib1: %v", err)
  231. }
  232. // ID 1001 wraps to the same relay port (64001) as ID 1
  233. conflict, err := checkTuicSocksRelayCollision(database.GetDB(), 1001)
  234. if err != nil {
  235. t.Fatalf("checkTuicSocksRelayCollision: %v", err)
  236. }
  237. if conflict == nil {
  238. t.Fatal("expected collision between ID 1001 and ID 1, got nil")
  239. }
  240. if conflict.InboundID != 1 {
  241. t.Fatalf("expected collision with inbound 1, got %d", conflict.InboundID)
  242. }
  243. }
  244. func TestTuicSocksSelfConflict(t *testing.T) {
  245. ib := &model.Inbound{
  246. Protocol: model.TUIC,
  247. Listen: "127.0.0.1",
  248. Port: tuic.SOCKSPortForInbound(5),
  249. }
  250. errStr := tuicSocksSelfConflict(ib, 5)
  251. if errStr == "" {
  252. t.Fatal("expected self conflict error string, got empty")
  253. }
  254. if !strings.Contains(errStr, "own SOCKS5 relay port") {
  255. t.Fatalf("unexpected error string: %s", errStr)
  256. }
  257. // Different port should not conflict
  258. ib.Port = 9999
  259. if diff := tuicSocksSelfConflict(ib, 5); diff != "" {
  260. t.Fatalf("expected no conflict for different port, got %s", diff)
  261. }
  262. }
  263. func TestInboundTuicServerParsesLegacyFlatSettings(t *testing.T) {
  264. server := inboundTuicServer(string(model.TUIC), `{"certificate":"/cert.pem","private_key":"/secret-key.pem","congestion_control":" CuBiC ","udp_relay_mode":"quic","sni":"profile.example"}`)
  265. if server == nil {
  266. t.Fatal("expected legacy flat TUIC settings")
  267. }
  268. if server.CongestionControl != "cubic" || server.UDPRelayMode != "quic" || server.SNI != "profile.example" {
  269. t.Fatalf("legacy flat fields were not normalized: %+v", server)
  270. }
  271. if server.PrivateKey != "" {
  272. t.Fatal("client preview exposed the inbound private key")
  273. }
  274. }
  275. func TestNormalizeTuicSettingsCanonicalizesCongestionAndPacketLimit(t *testing.T) {
  276. ib := &model.Inbound{Protocol: model.TUIC, Settings: `{"congestion_control":"RENO","max_udp_relay_packet_size":65507,"server":{"congestion_control":" CuBiC ","max_udp_relay_packet_size":65500}}`}
  277. if err := normalizeTuicSettings(ib); err != nil {
  278. t.Fatalf("normalizeTuicSettings: %v", err)
  279. }
  280. var got struct {
  281. CongestionControl string `json:"congestion_control"`
  282. MaxPacketSize int `json:"max_udp_relay_packet_size"`
  283. Server struct {
  284. CongestionControl string `json:"congestion_control"`
  285. MaxPacketSize int `json:"max_udp_relay_packet_size"`
  286. } `json:"server"`
  287. }
  288. if err := json.Unmarshal([]byte(ib.Settings), &got); err != nil {
  289. t.Fatalf("unmarshal normalized settings: %v", err)
  290. }
  291. if got.CongestionControl != "new_reno" || got.Server.CongestionControl != "cubic" {
  292. t.Fatalf("congestion controllers were not canonicalized: %+v", got)
  293. }
  294. if got.MaxPacketSize != 65245 || got.Server.MaxPacketSize != 65245 {
  295. t.Fatalf("packet limits were not clamped: %+v", got)
  296. }
  297. ib.Settings = `{"server":{"congestion_control":"experimental"}}`
  298. if err := normalizeTuicSettings(ib); err == nil {
  299. t.Fatal("unsupported congestion controller was accepted")
  300. }
  301. }