xray_config_inject_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. package service
  2. import (
  3. "encoding/json"
  4. "os"
  5. "testing"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  7. xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger"
  8. "github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
  9. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  10. "github.com/op/go-logging"
  11. )
  12. func TestMain(m *testing.M) {
  13. // A test binary re-executed with MTG_FAKE_CHILD=1 poses as an mtg child
  14. // process (see mtproto_fake_test.go) and never reaches the test runner.
  15. if os.Getenv("MTG_FAKE_CHILD") == "1" {
  16. fakeMtgChildMain()
  17. }
  18. // injectPanelEgress logs when it skips injection; the package logger must
  19. // exist before any test exercises a skipped path.
  20. xuilogger.InitLogger(logging.ERROR)
  21. os.Exit(m.Run())
  22. }
  23. func TestEnsureAPIServices(t *testing.T) {
  24. // legacy template without RoutingService gets it injected
  25. out := ensureAPIServices(json_util.RawMessage(`{"services":["HandlerService","LoggerService","StatsService"],"tag":"api"}`))
  26. var parsed struct {
  27. Services []string `json:"services"`
  28. Tag string `json:"tag"`
  29. }
  30. if err := json.Unmarshal(out, &parsed); err != nil {
  31. t.Fatal(err)
  32. }
  33. want := map[string]bool{"HandlerService": true, "StatsService": true, "RoutingService": true, "LoggerService": true}
  34. if len(parsed.Services) != 4 {
  35. t.Fatalf("expected 4 services, got %v", parsed.Services)
  36. }
  37. for _, svc := range parsed.Services {
  38. if !want[svc] {
  39. t.Fatalf("unexpected service %q", svc)
  40. }
  41. }
  42. if parsed.Tag != "api" {
  43. t.Fatalf("tag must be preserved, got %q", parsed.Tag)
  44. }
  45. // complete api block is returned unchanged (no marshal churn)
  46. full := json_util.RawMessage(`{"services":["HandlerService","StatsService","RoutingService"],"tag":"api"}`)
  47. if got := ensureAPIServices(full); string(got) != string(full) {
  48. t.Fatalf("complete api block must pass through untouched, got %s", got)
  49. }
  50. // absent api block stays absent
  51. if got := ensureAPIServices(nil); got != nil {
  52. t.Fatalf("nil api block must stay nil, got %s", got)
  53. }
  54. }
  55. func TestEnsureStatsPolicy(t *testing.T) {
  56. // default-template shape: level "0" exists with traffic flags — the online
  57. // flag is added and the siblings survive untouched
  58. out := ensureStatsPolicy(json_util.RawMessage(`{"levels":{"0":{"handshake":4,"statsUserUplink":true,"statsUserDownlink":true}},"system":{"statsInboundDownlink":true}}`))
  59. var parsed struct {
  60. Levels map[string]map[string]any `json:"levels"`
  61. System map[string]any `json:"system"`
  62. }
  63. if err := json.Unmarshal(out, &parsed); err != nil {
  64. t.Fatal(err)
  65. }
  66. level0 := parsed.Levels["0"]
  67. if level0["statsUserOnline"] != true {
  68. t.Fatalf("statsUserOnline must be injected into level 0, got %v", level0)
  69. }
  70. if level0["statsUserUplink"] != true || level0["statsUserDownlink"] != true || level0["handshake"] != float64(4) {
  71. t.Fatalf("sibling keys must be preserved, got %v", level0)
  72. }
  73. if parsed.System["statsInboundDownlink"] != true {
  74. t.Fatalf("system block must be preserved, got %v", parsed.System)
  75. }
  76. // missing levels block: level "0" is created with the flag
  77. out = ensureStatsPolicy(json_util.RawMessage(`{"system":{}}`))
  78. if err := json.Unmarshal(out, &parsed); err != nil {
  79. t.Fatal(err)
  80. }
  81. if parsed.Levels["0"]["statsUserOnline"] != true {
  82. t.Fatalf("level 0 must be created with statsUserOnline, got %s", out)
  83. }
  84. // every level gets the flag, an explicit false included — the flag is
  85. // panel infrastructure, like the api services
  86. out = ensureStatsPolicy(json_util.RawMessage(`{"levels":{"0":{"statsUserOnline":false},"1":{"connIdle":300}}}`))
  87. if err := json.Unmarshal(out, &parsed); err != nil {
  88. t.Fatal(err)
  89. }
  90. for _, key := range []string{"0", "1"} {
  91. if parsed.Levels[key]["statsUserOnline"] != true {
  92. t.Fatalf("level %s must have statsUserOnline forced on, got %s", key, out)
  93. }
  94. }
  95. if parsed.Levels["1"]["connIdle"] != float64(300) {
  96. t.Fatalf("level 1 siblings must be preserved, got %s", out)
  97. }
  98. // already-enabled input passes through byte-identical (no marshal churn,
  99. // no spurious restart)
  100. full := json_util.RawMessage(`{"levels":{"0":{"statsUserOnline":true}}}`)
  101. if got := ensureStatsPolicy(full); string(got) != string(full) {
  102. t.Fatalf("already-enabled policy must pass through untouched, got %s", got)
  103. }
  104. // absent policy block stays absent
  105. if got := ensureStatsPolicy(nil); got != nil {
  106. t.Fatalf("nil policy must stay nil, got %s", got)
  107. }
  108. // unparsable policy is left untouched
  109. bad := json_util.RawMessage(`{not json`)
  110. if got := ensureStatsPolicy(bad); string(got) != string(bad) {
  111. t.Fatalf("unparsable policy must be left untouched, got %s", got)
  112. }
  113. }
  114. func egressTestConfig() *xray.Config {
  115. return &xray.Config{
  116. RouterConfig: json_util.RawMessage(`{"domainStrategy":"AsIs","rules":[{"type":"field","inboundTag":["api"],"outboundTag":"api"}]}`),
  117. InboundConfigs: []xray.InboundConfig{
  118. {Port: 62789, Protocol: "tunnel", Tag: "api", Listen: json_util.RawMessage(`"127.0.0.1"`)},
  119. },
  120. }
  121. }
  122. type egressRouting struct {
  123. DomainStrategy string `json:"domainStrategy"`
  124. Rules []struct {
  125. InboundTag []string `json:"inboundTag"`
  126. OutboundTag string `json:"outboundTag"`
  127. Type string `json:"type"`
  128. } `json:"rules"`
  129. }
  130. func TestInjectPanelEgress(t *testing.T) {
  131. cfg := egressTestConfig()
  132. injectPanelEgress(cfg, "warp")
  133. if len(cfg.InboundConfigs) != 2 {
  134. t.Fatalf("expected the egress inbound to be appended, got %d inbounds", len(cfg.InboundConfigs))
  135. }
  136. ib := cfg.InboundConfigs[1]
  137. if ib.Tag != PanelEgressInboundTag || ib.Protocol != "socks" || ib.Port != panelEgressBasePort {
  138. t.Fatalf("unexpected egress inbound: %+v", ib)
  139. }
  140. if string(ib.Listen) != `"127.0.0.1"` {
  141. t.Fatalf("egress inbound must listen on loopback, got %s", ib.Listen)
  142. }
  143. var routing egressRouting
  144. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  145. t.Fatal(err)
  146. }
  147. if routing.DomainStrategy != "AsIs" {
  148. t.Fatalf("routing keys outside rules must be preserved, got %+v", routing)
  149. }
  150. if len(routing.Rules) != 2 {
  151. t.Fatalf("expected egress rule + existing rule, got %+v", routing.Rules)
  152. }
  153. first := routing.Rules[0]
  154. if first.Type != "field" || first.OutboundTag != "warp" ||
  155. len(first.InboundTag) != 1 || first.InboundTag[0] != PanelEgressInboundTag {
  156. t.Fatalf("egress rule must be prepended, got %+v", first)
  157. }
  158. }
  159. func TestInjectPanelEgress_BalancerTag(t *testing.T) {
  160. cfg := egressTestConfig()
  161. cfg.RouterConfig = json_util.RawMessage(`{"domainStrategy":"AsIs","rules":[],"balancers":[{"tag":"lb","selector":["warp"]}]}`)
  162. // A tag that names a balancer must be targeted via balancerTag so the
  163. // router resolves it; an outbound tag coexisting with balancers still uses
  164. // outboundTag.
  165. injectPanelEgress(cfg, "lb")
  166. var routing struct {
  167. Rules []struct {
  168. InboundTag []string `json:"inboundTag"`
  169. OutboundTag string `json:"outboundTag"`
  170. BalancerTag string `json:"balancerTag"`
  171. Type string `json:"type"`
  172. } `json:"rules"`
  173. }
  174. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  175. t.Fatal(err)
  176. }
  177. if len(routing.Rules) != 1 {
  178. t.Fatalf("expected the egress rule, got %+v", routing.Rules)
  179. }
  180. first := routing.Rules[0]
  181. if first.BalancerTag != "lb" || first.OutboundTag != "" {
  182. t.Fatalf("a balancer tag must target balancerTag, not outboundTag, got %+v", first)
  183. }
  184. if len(first.InboundTag) != 1 || first.InboundTag[0] != PanelEgressInboundTag {
  185. t.Fatalf("egress rule must bind the egress inbound, got %+v", first)
  186. }
  187. // A non-balancer tag alongside balancers keeps the plain outbound path.
  188. cfg2 := egressTestConfig()
  189. cfg2.RouterConfig = json_util.RawMessage(`{"rules":[],"balancers":[{"tag":"lb","selector":["warp"]}]}`)
  190. injectPanelEgress(cfg2, "warp")
  191. var routing2 struct {
  192. Rules []struct {
  193. OutboundTag string `json:"outboundTag"`
  194. BalancerTag string `json:"balancerTag"`
  195. } `json:"rules"`
  196. }
  197. if err := json.Unmarshal(cfg2.RouterConfig, &routing2); err != nil {
  198. t.Fatal(err)
  199. }
  200. if routing2.Rules[0].OutboundTag != "warp" || routing2.Rules[0].BalancerTag != "" {
  201. t.Fatalf("a concrete outbound must target outboundTag, got %+v", routing2.Rules[0])
  202. }
  203. }
  204. func TestInjectPanelEgress_PortCollision(t *testing.T) {
  205. cfg := egressTestConfig()
  206. cfg.InboundConfigs = append(cfg.InboundConfigs,
  207. xray.InboundConfig{Port: panelEgressBasePort, Protocol: "vless", Tag: "in-1"},
  208. xray.InboundConfig{Port: panelEgressBasePort + 1, Protocol: "vless", Tag: "in-2"},
  209. )
  210. injectPanelEgress(cfg, "direct")
  211. got := cfg.InboundConfigs[len(cfg.InboundConfigs)-1]
  212. if got.Tag != PanelEgressInboundTag || got.Port != panelEgressBasePort+2 {
  213. t.Fatalf("egress inbound must skip taken ports, got %+v", got)
  214. }
  215. }
  216. func TestInjectPanelEgress_TagCollisionSkips(t *testing.T) {
  217. cfg := egressTestConfig()
  218. cfg.InboundConfigs = append(cfg.InboundConfigs,
  219. xray.InboundConfig{Port: 1234, Protocol: "socks", Tag: PanelEgressInboundTag},
  220. )
  221. before := string(cfg.RouterConfig)
  222. injectPanelEgress(cfg, "direct")
  223. if len(cfg.InboundConfigs) != 2 || string(cfg.RouterConfig) != before {
  224. t.Fatal("a user inbound owning the egress tag must make injection a no-op")
  225. }
  226. }
  227. func TestInjectPanelEgress_NoRoutingSection(t *testing.T) {
  228. cfg := egressTestConfig()
  229. cfg.RouterConfig = nil
  230. injectPanelEgress(cfg, "direct")
  231. var routing egressRouting
  232. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  233. t.Fatal(err)
  234. }
  235. if len(routing.Rules) != 1 || routing.Rules[0].OutboundTag != "direct" {
  236. t.Fatalf("a routing section must be created with the egress rule, got %+v", routing)
  237. }
  238. if len(cfg.InboundConfigs) != 2 {
  239. t.Fatal("egress inbound must still be appended")
  240. }
  241. }
  242. func TestInjectPanelEgress_BadRoutingSkips(t *testing.T) {
  243. cfg := egressTestConfig()
  244. cfg.RouterConfig = json_util.RawMessage(`{not json`)
  245. injectPanelEgress(cfg, "direct")
  246. if len(cfg.InboundConfigs) != 1 {
  247. t.Fatal("unparsable routing must skip the whole injection, inbound included")
  248. }
  249. if string(cfg.RouterConfig) != `{not json` {
  250. t.Fatal("unparsable routing must be left untouched")
  251. }
  252. }
  253. func mtprotoInbound(tag string, settings string) *model.Inbound {
  254. return &model.Inbound{Tag: tag, Protocol: model.MTProto, Enable: true, Settings: settings}
  255. }
  256. func TestInjectMtprotoEgress_WithOutbound(t *testing.T) {
  257. cfg := egressTestConfig()
  258. injectMtprotoEgress(cfg, mtprotoInbound("inbound-443",
  259. `{"routeThroughXray":true,"routeXrayPort":50000,"outboundTag":"warp"}`))
  260. if len(cfg.InboundConfigs) != 2 {
  261. t.Fatalf("expected the bridge inbound to be appended, got %d", len(cfg.InboundConfigs))
  262. }
  263. ib := cfg.InboundConfigs[1]
  264. if ib.Tag != "inbound-443" || ib.Protocol != "socks" || ib.Port != 50000 {
  265. t.Fatalf("unexpected bridge inbound: %+v", ib)
  266. }
  267. if string(ib.Listen) != `"127.0.0.1"` {
  268. t.Fatalf("bridge must listen on loopback, got %s", ib.Listen)
  269. }
  270. var routing egressRouting
  271. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  272. t.Fatal(err)
  273. }
  274. if len(routing.Rules) != 2 {
  275. t.Fatalf("expected the egress rule prepended to the existing rule, got %+v", routing.Rules)
  276. }
  277. first := routing.Rules[0]
  278. if first.Type != "field" || first.OutboundTag != "warp" ||
  279. len(first.InboundTag) != 1 || first.InboundTag[0] != "inbound-443" {
  280. t.Fatalf("egress rule must bind the inbound tag to the outbound, got %+v", first)
  281. }
  282. }
  283. func TestInjectMtprotoEgress_NoOutboundLeavesRouting(t *testing.T) {
  284. cfg := egressTestConfig()
  285. before := string(cfg.RouterConfig)
  286. injectMtprotoEgress(cfg, mtprotoInbound("inbound-443",
  287. `{"routeThroughXray":true,"routeXrayPort":50001}`))
  288. if len(cfg.InboundConfigs) != 2 || cfg.InboundConfigs[1].Port != 50001 {
  289. t.Fatalf("bridge must still be appended without an outbound, got %+v", cfg.InboundConfigs)
  290. }
  291. if string(cfg.RouterConfig) != before {
  292. t.Fatalf("no outbound means no rule change, got %s", cfg.RouterConfig)
  293. }
  294. }
  295. func TestInjectMtprotoEgress_BalancerTag(t *testing.T) {
  296. cfg := egressTestConfig()
  297. cfg.RouterConfig = json_util.RawMessage(`{"rules":[],"balancers":[{"tag":"lb","selector":["warp"]}]}`)
  298. injectMtprotoEgress(cfg, mtprotoInbound("inbound-443",
  299. `{"routeThroughXray":true,"routeXrayPort":50002,"outboundTag":"lb"}`))
  300. var routing struct {
  301. Rules []struct {
  302. OutboundTag string `json:"outboundTag"`
  303. BalancerTag string `json:"balancerTag"`
  304. } `json:"rules"`
  305. }
  306. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  307. t.Fatal(err)
  308. }
  309. if len(routing.Rules) != 1 || routing.Rules[0].BalancerTag != "lb" || routing.Rules[0].OutboundTag != "" {
  310. t.Fatalf("a balancer tag must target balancerTag, got %+v", routing.Rules)
  311. }
  312. }
  313. func TestInjectMtprotoEgress_Disabled(t *testing.T) {
  314. // Not routed, and routed-but-portless, are both no-ops.
  315. for _, settings := range []string{
  316. `{"routeThroughXray":false,"routeXrayPort":50000}`,
  317. `{"routeThroughXray":true}`,
  318. `{"routeThroughXray":true,"routeXrayPort":0}`,
  319. } {
  320. cfg := egressTestConfig()
  321. before := string(cfg.RouterConfig)
  322. injectMtprotoEgress(cfg, mtprotoInbound("inbound-443", settings))
  323. if len(cfg.InboundConfigs) != 1 || string(cfg.RouterConfig) != before {
  324. t.Fatalf("settings %s must be a no-op, got %d inbounds", settings, len(cfg.InboundConfigs))
  325. }
  326. }
  327. }
  328. func TestInjectMtprotoEgress_TagCollisionSkips(t *testing.T) {
  329. cfg := egressTestConfig()
  330. cfg.InboundConfigs = append(cfg.InboundConfigs,
  331. xray.InboundConfig{Port: 443, Protocol: "vless", Tag: "inbound-443"})
  332. before := string(cfg.RouterConfig)
  333. injectMtprotoEgress(cfg, mtprotoInbound("inbound-443",
  334. `{"routeThroughXray":true,"routeXrayPort":50003,"outboundTag":"warp"}`))
  335. if len(cfg.InboundConfigs) != 2 || string(cfg.RouterConfig) != before {
  336. t.Fatal("a real inbound already owning the tag must make the bridge a no-op")
  337. }
  338. }