json_service_test.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. package sub
  2. import (
  3. "encoding/json"
  4. "testing"
  5. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  6. )
  7. func hasDirectOutOutbound(svc *SubJsonService) bool {
  8. for _, raw := range svc.defaultOutbounds {
  9. var outbound map[string]any
  10. if err := json.Unmarshal(raw, &outbound); err != nil {
  11. continue
  12. }
  13. if outbound["tag"] == "direct_out" {
  14. return true
  15. }
  16. }
  17. return false
  18. }
  19. func outboundSettings(t *testing.T, raw []byte) map[string]any {
  20. t.Helper()
  21. var parsed map[string]any
  22. if err := json.Unmarshal(raw, &parsed); err != nil {
  23. t.Fatalf("failed to unmarshal outbound: %v", err)
  24. }
  25. settings, _ := parsed["settings"].(map[string]any)
  26. if settings == nil {
  27. t.Fatal("outbound has no settings")
  28. }
  29. return settings
  30. }
  31. func TestSubJsonServiceInjectsGlobalFinalMask(t *testing.T) {
  32. finalMask := `{"tcp":[{"type":"fragment","settings":{"packets":"tlshello","length":"100-200","delay":"10-20"}}],"udp":[{"type":"noise","settings":{"noise":[{"type":"base64","packet":"SGVsbG8="}]}}],"quicParams":{"congestion":"bbr"}}`
  33. svc := NewSubJsonService("", "", finalMask, nil)
  34. if hasDirectOutOutbound(svc) {
  35. t.Fatal("direct_out outbound must never be emitted")
  36. }
  37. stream := svc.streamData(`{"network":"tcp","security":"none","tcpSettings":{"header":{"type":"none"}}}`)
  38. if _, ok := stream["sockopt"]; ok {
  39. t.Fatal("legacy direct_out dialerProxy sockopt must never be set")
  40. }
  41. finalmask, _ := stream["finalmask"].(map[string]any)
  42. if finalmask == nil {
  43. t.Fatal("streamSettings is missing finalmask")
  44. }
  45. tcp, _ := finalmask["tcp"].([]any)
  46. if len(tcp) != 1 {
  47. t.Fatalf("tcp masks len = %d, want 1", len(tcp))
  48. }
  49. if first, _ := tcp[0].(map[string]any); first["type"] != "fragment" {
  50. t.Fatalf("tcp[0] type = %v, want fragment", first["type"])
  51. }
  52. udp, _ := finalmask["udp"].([]any)
  53. if len(udp) != 1 {
  54. t.Fatalf("udp masks len = %d, want 1", len(udp))
  55. }
  56. quic, _ := finalmask["quicParams"].(map[string]any)
  57. if quic == nil || quic["congestion"] != "bbr" {
  58. t.Fatalf("quicParams missing/wrong: %#v", finalmask["quicParams"])
  59. }
  60. }
  61. func TestSubJsonServiceMergesWithExistingFinalMask(t *testing.T) {
  62. finalMask := `{"tcp":[{"type":"fragment","settings":{"packets":"tlshello"}}]}`
  63. svc := NewSubJsonService("", "", finalMask, nil)
  64. stream := svc.streamData(`{
  65. "network":"tcp","security":"none","tcpSettings":{"header":{"type":"none"}},
  66. "finalmask":{"tcp":[{"type":"sudoku"}]}
  67. }`)
  68. finalmask, _ := stream["finalmask"].(map[string]any)
  69. tcp, _ := finalmask["tcp"].([]any)
  70. if len(tcp) != 2 {
  71. t.Fatalf("tcp masks len = %d, want 2 (existing + global)", len(tcp))
  72. }
  73. a, _ := tcp[0].(map[string]any)
  74. b, _ := tcp[1].(map[string]any)
  75. if a["type"] != "sudoku" || b["type"] != "fragment" {
  76. t.Fatalf("tcp masks = %#v, want existing sudoku then global fragment", tcp)
  77. }
  78. }
  79. func TestSubJsonServiceNoFinalMaskWhenEmpty(t *testing.T) {
  80. svc := NewSubJsonService("", "", "", nil)
  81. stream := svc.streamData(`{"network":"tcp","security":"none","tcpSettings":{"header":{"type":"none"}}}`)
  82. if _, ok := stream["finalmask"]; ok {
  83. t.Fatal("no finalmask should be emitted when subJsonFinalMask is empty")
  84. }
  85. if _, ok := stream["sockopt"]; ok {
  86. t.Fatal("legacy direct_out sockopt must never be set")
  87. }
  88. }
  89. // xray-core parses tlsSettings.pinnedPeerCertSha256 as a comma-separated string;
  90. // the JSON subscription must emit that form, not an array, or v2ray clients fail
  91. // to import the config (#5401).
  92. func TestSubJsonServicePinnedCertJoinedToString(t *testing.T) {
  93. svc := NewSubJsonService("", "", "", nil)
  94. stream := svc.streamData(`{"network":"tcp","security":"tls","tlsSettings":{"serverName":"a.example.com","settings":{"pinnedPeerCertSha256":["aa11","bb22"]}}}`)
  95. tls, _ := stream["tlsSettings"].(map[string]any)
  96. if tls == nil {
  97. t.Fatalf("tlsSettings missing: %#v", stream)
  98. }
  99. if got := tls["pinnedPeerCertSha256"]; got != "aa11,bb22" {
  100. t.Fatalf("pinnedPeerCertSha256 = %#v, want comma-separated string \"aa11,bb22\"", got)
  101. }
  102. }
  103. func TestSubJsonServiceVlessFlattened(t *testing.T) {
  104. inbound := &model.Inbound{Listen: "1.2.3.4", Port: 443, Protocol: model.VLESS, Settings: `{"encryption":"none"}`}
  105. client := model.Client{ID: "uuid-1", Flow: "xtls-rprx-vision"}
  106. settings := outboundSettings(t, NewSubJsonService("", "", "", nil).genVless(inbound, nil, client, ""))
  107. if _, ok := settings["vnext"]; ok {
  108. t.Fatal("vless outbound must not use vnext")
  109. }
  110. if settings["address"] != "1.2.3.4" || settings["id"] != "uuid-1" || settings["encryption"] != "none" || settings["flow"] != "xtls-rprx-vision" {
  111. t.Fatalf("flat vless settings wrong: %#v", settings)
  112. }
  113. }
  114. func TestSubJsonServiceVmessFlattened(t *testing.T) {
  115. inbound := &model.Inbound{Listen: "1.2.3.4", Port: 443, Protocol: model.VMESS, Settings: `{}`}
  116. client := model.Client{ID: "uuid-2"}
  117. settings := outboundSettings(t, NewSubJsonService("", "", "", nil).genVnext(inbound, nil, client, ""))
  118. if _, ok := settings["vnext"]; ok {
  119. t.Fatal("vmess outbound must not use vnext")
  120. }
  121. if settings["id"] != "uuid-2" || settings["security"] != "auto" {
  122. t.Fatalf("flat vmess settings wrong: %#v", settings)
  123. }
  124. }
  125. // Shadowsocks/Trojan outbounds must use the standard "servers" array so older
  126. // bundled xray-cores (e.g. v2rayN) parse them; the flat top-level form only
  127. // works on very recent xray-core.
  128. func TestSubJsonServiceServerUsesServersArray(t *testing.T) {
  129. trojan := &model.Inbound{Listen: "1.2.3.4", Port: 443, Protocol: model.Trojan, Settings: `{}`}
  130. client := model.Client{Password: "p4ss"}
  131. settings := outboundSettings(t, NewSubJsonService("", "", "", nil).genServer(trojan, nil, client, ""))
  132. server := firstServer(settings)
  133. if server == nil {
  134. t.Fatalf("trojan outbound must use a servers array, got: %#v", settings)
  135. }
  136. if server["password"] != "p4ss" || server["address"] != "1.2.3.4" {
  137. t.Fatalf("trojan server entry wrong: %#v", server)
  138. }
  139. if _, ok := server["method"]; ok {
  140. t.Fatalf("trojan must not carry method: %#v", server)
  141. }
  142. ss := &model.Inbound{Listen: "1.2.3.4", Port: 443, Protocol: model.Shadowsocks, Settings: `{"method":"aes-256-gcm"}`}
  143. ssSettings := outboundSettings(t, NewSubJsonService("", "", "", nil).genServer(ss, nil, client, ""))
  144. ssServer := firstServer(ssSettings)
  145. if ssServer == nil {
  146. t.Fatalf("shadowsocks outbound must use a servers array, got: %#v", ssSettings)
  147. }
  148. if ssServer["method"] != "aes-256-gcm" {
  149. t.Fatalf("shadowsocks server entry must carry method: %#v", ssServer)
  150. }
  151. }
  152. func TestSubJsonServiceXmuxSuppressesGlobalMux(t *testing.T) {
  153. globalMux := `{"enabled":true,"concurrency":8}`
  154. svc := NewSubJsonService(globalMux, "", "", nil)
  155. // When xmux is present in xhttpSettings, the per-inbound xmux handles
  156. // multiplexing and the legacy outbound.Mux must NOT be set.
  157. stream := `{"network":"xhttp","security":"tls","tlsSettings":{"serverName":"example.com"},"xhttpSettings":{"path":"/api","mode":"packet-up","xmux":{"maxConcurrency":"16-32"}}}`
  158. parsed := svc.streamData(stream)
  159. mux := globalMux
  160. if xhttp, ok := parsed["xhttpSettings"].(map[string]any); ok {
  161. if _, hasXmux := xhttp["xmux"]; hasXmux {
  162. mux = ""
  163. }
  164. }
  165. streamSettings, _ := json.Marshal(parsed)
  166. inbound := &model.Inbound{Listen: "1.2.3.4", Port: 443, Protocol: model.VLESS, Settings: `{"encryption":"none"}`}
  167. client := model.Client{ID: "uuid-1"}
  168. raw := svc.genVless(inbound, streamSettings, client, mux)
  169. var ob map[string]any
  170. if err := json.Unmarshal(raw, &ob); err != nil {
  171. t.Fatalf("unmarshal outbound: %v", err)
  172. }
  173. if _, has := ob["mux"]; has {
  174. t.Fatal("outbound.Mux must NOT be set when per-inbound xmux is present")
  175. }
  176. // Verify xmux is still inside xhttpSettings in streamSettings.
  177. ss, _ := ob["streamSettings"].(map[string]any)
  178. if ss == nil {
  179. t.Fatal("streamSettings missing from outbound")
  180. }
  181. xhttp, _ := ss["xhttpSettings"].(map[string]any)
  182. if xhttp == nil {
  183. t.Fatal("xhttpSettings missing from streamSettings")
  184. }
  185. xmux, _ := xhttp["xmux"].(map[string]any)
  186. if xmux == nil {
  187. t.Fatal("xmux missing from xhttpSettings — per-inbound xmux must survive streamData()")
  188. }
  189. if xmux["maxConcurrency"] != "16-32" {
  190. t.Fatalf("xmux.maxConcurrency = %v, want 16-32", xmux["maxConcurrency"])
  191. }
  192. }
  193. func TestSubJsonServiceGlobalMuxWhenNoXmux(t *testing.T) {
  194. globalMux := `{"enabled":true,"concurrency":8}`
  195. svc := NewSubJsonService(globalMux, "", "", nil)
  196. // When no xmux is present, the global subJsonMux should be used.
  197. stream := `{"network":"xhttp","security":"tls","tlsSettings":{"serverName":"example.com"},"xhttpSettings":{"path":"/api","mode":"packet-up"}}`
  198. parsed := svc.streamData(stream)
  199. mux := globalMux
  200. if xhttp, ok := parsed["xhttpSettings"].(map[string]any); ok {
  201. if _, hasXmux := xhttp["xmux"]; hasXmux {
  202. mux = ""
  203. }
  204. }
  205. streamSettings, _ := json.Marshal(parsed)
  206. inbound := &model.Inbound{Listen: "1.2.3.4", Port: 443, Protocol: model.VLESS, Settings: `{"encryption":"none"}`}
  207. client := model.Client{ID: "uuid-1"}
  208. raw := svc.genVless(inbound, streamSettings, client, mux)
  209. var ob map[string]any
  210. if err := json.Unmarshal(raw, &ob); err != nil {
  211. t.Fatalf("unmarshal outbound: %v", err)
  212. }
  213. m, has := ob["mux"]
  214. if !has {
  215. t.Fatal("outbound.Mux must be set when global subJsonMux is configured and no per-inbound xmux")
  216. }
  217. mm, _ := m.(map[string]any)
  218. if mm["enabled"] != true || mm["concurrency"] != float64(8) {
  219. t.Fatalf("mux payload wrong: %#v", m)
  220. }
  221. }