inbound_tuic_test.go 11 KB

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