xray_config_inject_test.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  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. OutboundConfigs: json_util.RawMessage(`[{"protocol":"freedom","tag":"direct"},{"protocol":"socks","tag":"warp"}]`),
  118. InboundConfigs: []xray.InboundConfig{
  119. {Port: 62789, Protocol: "tunnel", Tag: "api", Listen: json_util.RawMessage(`"127.0.0.1"`)},
  120. },
  121. }
  122. }
  123. type egressRouting struct {
  124. DomainStrategy string `json:"domainStrategy"`
  125. Rules []struct {
  126. InboundTag []string `json:"inboundTag"`
  127. OutboundTag string `json:"outboundTag"`
  128. Type string `json:"type"`
  129. } `json:"rules"`
  130. }
  131. func TestInjectPanelEgress(t *testing.T) {
  132. cfg := egressTestConfig()
  133. injectPanelEgress(cfg, "warp")
  134. if len(cfg.InboundConfigs) != 2 {
  135. t.Fatalf("expected the egress inbound to be appended, got %d inbounds", len(cfg.InboundConfigs))
  136. }
  137. ib := cfg.InboundConfigs[1]
  138. if ib.Tag != PanelEgressInboundTag || ib.Protocol != "socks" || ib.Port != panelEgressBasePort {
  139. t.Fatalf("unexpected egress inbound: %+v", ib)
  140. }
  141. if string(ib.Listen) != `"127.0.0.1"` {
  142. t.Fatalf("egress inbound must listen on loopback, got %s", ib.Listen)
  143. }
  144. var routing egressRouting
  145. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  146. t.Fatal(err)
  147. }
  148. if routing.DomainStrategy != "AsIs" {
  149. t.Fatalf("routing keys outside rules must be preserved, got %+v", routing)
  150. }
  151. if len(routing.Rules) != 2 {
  152. t.Fatalf("expected egress rule + existing rule, got %+v", routing.Rules)
  153. }
  154. first := routing.Rules[0]
  155. if first.Type != "field" || first.OutboundTag != "warp" ||
  156. len(first.InboundTag) != 1 || first.InboundTag[0] != PanelEgressInboundTag {
  157. t.Fatalf("egress rule must be prepended, got %+v", first)
  158. }
  159. }
  160. func TestInjectPanelEgress_BalancerTag(t *testing.T) {
  161. cfg := egressTestConfig()
  162. cfg.RouterConfig = json_util.RawMessage(`{"domainStrategy":"AsIs","rules":[],"balancers":[{"tag":"lb","selector":["warp"]}]}`)
  163. // A tag that names a balancer must be targeted via balancerTag so the
  164. // router resolves it; an outbound tag coexisting with balancers still uses
  165. // outboundTag.
  166. injectPanelEgress(cfg, "lb")
  167. var routing struct {
  168. Rules []struct {
  169. InboundTag []string `json:"inboundTag"`
  170. OutboundTag string `json:"outboundTag"`
  171. BalancerTag string `json:"balancerTag"`
  172. Type string `json:"type"`
  173. } `json:"rules"`
  174. }
  175. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  176. t.Fatal(err)
  177. }
  178. if len(routing.Rules) != 1 {
  179. t.Fatalf("expected the egress rule, got %+v", routing.Rules)
  180. }
  181. first := routing.Rules[0]
  182. if first.BalancerTag != "lb" || first.OutboundTag != "" {
  183. t.Fatalf("a balancer tag must target balancerTag, not outboundTag, got %+v", first)
  184. }
  185. if len(first.InboundTag) != 1 || first.InboundTag[0] != PanelEgressInboundTag {
  186. t.Fatalf("egress rule must bind the egress inbound, got %+v", first)
  187. }
  188. // A non-balancer tag alongside balancers keeps the plain outbound path.
  189. cfg2 := egressTestConfig()
  190. cfg2.RouterConfig = json_util.RawMessage(`{"rules":[],"balancers":[{"tag":"lb","selector":["warp"]}]}`)
  191. injectPanelEgress(cfg2, "warp")
  192. var routing2 struct {
  193. Rules []struct {
  194. OutboundTag string `json:"outboundTag"`
  195. BalancerTag string `json:"balancerTag"`
  196. } `json:"rules"`
  197. }
  198. if err := json.Unmarshal(cfg2.RouterConfig, &routing2); err != nil {
  199. t.Fatal(err)
  200. }
  201. if routing2.Rules[0].OutboundTag != "warp" || routing2.Rules[0].BalancerTag != "" {
  202. t.Fatalf("a concrete outbound must target outboundTag, got %+v", routing2.Rules[0])
  203. }
  204. }
  205. func TestInjectPanelEgress_PortCollision(t *testing.T) {
  206. cfg := egressTestConfig()
  207. cfg.InboundConfigs = append(cfg.InboundConfigs,
  208. xray.InboundConfig{Port: panelEgressBasePort, Protocol: "vless", Tag: "in-1"},
  209. xray.InboundConfig{Port: panelEgressBasePort + 1, Protocol: "vless", Tag: "in-2"},
  210. )
  211. injectPanelEgress(cfg, "direct")
  212. got := cfg.InboundConfigs[len(cfg.InboundConfigs)-1]
  213. if got.Tag != PanelEgressInboundTag || got.Port != panelEgressBasePort+2 {
  214. t.Fatalf("egress inbound must skip taken ports, got %+v", got)
  215. }
  216. }
  217. func TestInjectPanelEgress_TagCollisionSkips(t *testing.T) {
  218. cfg := egressTestConfig()
  219. cfg.InboundConfigs = append(cfg.InboundConfigs,
  220. xray.InboundConfig{Port: 1234, Protocol: "socks", Tag: PanelEgressInboundTag},
  221. )
  222. before := string(cfg.RouterConfig)
  223. injectPanelEgress(cfg, "direct")
  224. if len(cfg.InboundConfigs) != 2 || string(cfg.RouterConfig) != before {
  225. t.Fatal("a user inbound owning the egress tag must make injection a no-op")
  226. }
  227. }
  228. func TestInjectPanelEgress_NoRoutingSection(t *testing.T) {
  229. cfg := egressTestConfig()
  230. cfg.RouterConfig = nil
  231. injectPanelEgress(cfg, "direct")
  232. var routing egressRouting
  233. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  234. t.Fatal(err)
  235. }
  236. if len(routing.Rules) != 1 || routing.Rules[0].OutboundTag != "direct" {
  237. t.Fatalf("a routing section must be created with the egress rule, got %+v", routing)
  238. }
  239. if len(cfg.InboundConfigs) != 2 {
  240. t.Fatal("egress inbound must still be appended")
  241. }
  242. }
  243. func TestInjectPanelEgress_BadRoutingSkips(t *testing.T) {
  244. cfg := egressTestConfig()
  245. cfg.RouterConfig = json_util.RawMessage(`{not json`)
  246. injectPanelEgress(cfg, "direct")
  247. if len(cfg.InboundConfigs) != 1 {
  248. t.Fatal("unparsable routing must skip the whole injection, inbound included")
  249. }
  250. if string(cfg.RouterConfig) != `{not json` {
  251. t.Fatal("unparsable routing must be left untouched")
  252. }
  253. }
  254. func TestInjectPanelEgress_MissingTargetSkips(t *testing.T) {
  255. cfg := egressTestConfig()
  256. before := string(cfg.RouterConfig)
  257. injectPanelEgress(cfg, "removed-subscription-outbound")
  258. if len(cfg.InboundConfigs) != 1 {
  259. t.Fatalf("a missing target must not expose the panel bridge, got %+v", cfg.InboundConfigs)
  260. }
  261. if string(cfg.RouterConfig) != before {
  262. t.Fatalf("a missing target must leave routing untouched, got %s", cfg.RouterConfig)
  263. }
  264. }
  265. func TestInjectPanelEgress_BadOutboundsSkips(t *testing.T) {
  266. cfg := egressTestConfig()
  267. cfg.OutboundConfigs = json_util.RawMessage(`{not json`)
  268. before := string(cfg.RouterConfig)
  269. injectPanelEgress(cfg, "direct")
  270. if len(cfg.InboundConfigs) != 1 {
  271. t.Fatalf("unparsable outbounds must not expose the panel bridge, got %+v", cfg.InboundConfigs)
  272. }
  273. if string(cfg.RouterConfig) != before {
  274. t.Fatalf("unparsable outbounds must leave routing untouched, got %s", cfg.RouterConfig)
  275. }
  276. }
  277. func TestInjectNodeEgresses_MissingTargetSkips(t *testing.T) {
  278. cfg := egressTestConfig()
  279. injectNodeEgresses(cfg, []*model.Node{
  280. {Id: 1, Enable: true, OutboundTag: "removed-subscription-outbound"},
  281. {Id: 2, Enable: true, OutboundTag: "warp"},
  282. })
  283. if len(cfg.InboundConfigs) != 2 {
  284. t.Fatalf("only the node with a valid target should get a bridge, got %+v", cfg.InboundConfigs)
  285. }
  286. bridge := cfg.InboundConfigs[1]
  287. if bridge.Tag != NodeEgressInboundTag(2) || bridge.Port != nodeEgressBasePort+2 {
  288. t.Fatalf("unexpected node egress bridge: %+v", bridge)
  289. }
  290. var routing egressRouting
  291. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  292. t.Fatal(err)
  293. }
  294. if len(routing.Rules) != 2 || routing.Rules[0].OutboundTag != "warp" ||
  295. len(routing.Rules[0].InboundTag) != 1 || routing.Rules[0].InboundTag[0] != NodeEgressInboundTag(2) {
  296. t.Fatalf("only the valid node egress rule should be prepended, got %+v", routing.Rules)
  297. }
  298. }
  299. func TestInjectNodeEgresses_BadOutboundsSkips(t *testing.T) {
  300. cfg := egressTestConfig()
  301. cfg.OutboundConfigs = json_util.RawMessage(`{not json`)
  302. before := string(cfg.RouterConfig)
  303. injectNodeEgresses(cfg, []*model.Node{{Id: 1, Enable: true, OutboundTag: "direct"}})
  304. if len(cfg.InboundConfigs) != 1 {
  305. t.Fatalf("unparsable outbounds must not expose a node bridge, got %+v", cfg.InboundConfigs)
  306. }
  307. if string(cfg.RouterConfig) != before {
  308. t.Fatalf("unparsable outbounds must leave routing untouched, got %s", cfg.RouterConfig)
  309. }
  310. }
  311. func TestInjectNodeEgresses_BalancerTarget(t *testing.T) {
  312. cfg := egressTestConfig()
  313. cfg.RouterConfig = json_util.RawMessage(`{"rules":[],"balancers":[{"tag":"lb","selector":["warp"]}]}`)
  314. injectNodeEgresses(cfg, []*model.Node{{Id: 1, Enable: true, OutboundTag: "lb"}})
  315. var routing struct {
  316. Rules []struct {
  317. OutboundTag string `json:"outboundTag"`
  318. BalancerTag string `json:"balancerTag"`
  319. } `json:"rules"`
  320. }
  321. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  322. t.Fatal(err)
  323. }
  324. if len(cfg.InboundConfigs) != 2 || len(routing.Rules) != 1 ||
  325. routing.Rules[0].BalancerTag != "lb" || routing.Rules[0].OutboundTag != "" {
  326. t.Fatalf("a valid balancer target must create the node bridge and rule, got %+v", routing.Rules)
  327. }
  328. }
  329. func TestInjectNodeEgresses_TagCollisionSkips(t *testing.T) {
  330. cfg := egressTestConfig()
  331. cfg.InboundConfigs = append(cfg.InboundConfigs,
  332. xray.InboundConfig{Port: 1234, Protocol: "socks", Tag: NodeEgressInboundTag(1)},
  333. )
  334. before := string(cfg.RouterConfig)
  335. injectNodeEgresses(cfg, []*model.Node{{Id: 1, Enable: true, OutboundTag: "direct"}})
  336. if len(cfg.InboundConfigs) != 2 || string(cfg.RouterConfig) != before {
  337. t.Fatal("an existing node egress tag must make that node injection a no-op")
  338. }
  339. }
  340. func TestInjectNodeEgresses_PortCollision(t *testing.T) {
  341. cfg := egressTestConfig()
  342. cfg.InboundConfigs = append(cfg.InboundConfigs,
  343. xray.InboundConfig{Port: nodeEgressBasePort + 1, Protocol: "vless", Tag: "in-1"},
  344. xray.InboundConfig{Port: nodeEgressBasePort + 2, Protocol: "vless", Tag: "in-2"},
  345. )
  346. injectNodeEgresses(cfg, []*model.Node{{Id: 1, Enable: true, OutboundTag: "direct"}})
  347. bridge := cfg.InboundConfigs[len(cfg.InboundConfigs)-1]
  348. if bridge.Tag != NodeEgressInboundTag(1) || bridge.Port != nodeEgressBasePort+3 {
  349. t.Fatalf("node egress must skip taken ports, got %+v", bridge)
  350. }
  351. }
  352. func TestInjectNodeEgresses_NoRoutingSection(t *testing.T) {
  353. cfg := egressTestConfig()
  354. cfg.RouterConfig = nil
  355. injectNodeEgresses(cfg, []*model.Node{{Id: 1, Enable: true, OutboundTag: "direct"}})
  356. var routing egressRouting
  357. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  358. t.Fatal(err)
  359. }
  360. if len(cfg.InboundConfigs) != 2 || len(routing.Rules) != 1 ||
  361. routing.Rules[0].OutboundTag != "direct" ||
  362. len(routing.Rules[0].InboundTag) != 1 || routing.Rules[0].InboundTag[0] != NodeEgressInboundTag(1) {
  363. t.Fatalf("a routing section must be created with the node egress rule, got %+v", routing.Rules)
  364. }
  365. }
  366. func TestInjectNodeEgresses_BadRoutingSkips(t *testing.T) {
  367. cfg := egressTestConfig()
  368. cfg.RouterConfig = json_util.RawMessage(`{not json`)
  369. injectNodeEgresses(cfg, []*model.Node{{Id: 1, Enable: true, OutboundTag: "direct"}})
  370. if len(cfg.InboundConfigs) != 1 {
  371. t.Fatalf("unparsable routing must not expose a node bridge, got %+v", cfg.InboundConfigs)
  372. }
  373. if string(cfg.RouterConfig) != `{not json` {
  374. t.Fatalf("unparsable routing must be left untouched, got %s", cfg.RouterConfig)
  375. }
  376. }
  377. func mtprotoInbound(tag string, settings string) *model.Inbound {
  378. return &model.Inbound{Tag: tag, Protocol: model.MTProto, Enable: true, Settings: settings}
  379. }
  380. func TestInjectMtprotoEgress_WithOutbound(t *testing.T) {
  381. cfg := egressTestConfig()
  382. injectMtprotoEgress(cfg, mtprotoInbound("inbound-443",
  383. `{"routeThroughXray":true,"routeXrayPort":50000,"outboundTag":"warp"}`))
  384. if len(cfg.InboundConfigs) != 2 {
  385. t.Fatalf("expected the bridge inbound to be appended, got %d", len(cfg.InboundConfigs))
  386. }
  387. ib := cfg.InboundConfigs[1]
  388. if ib.Tag != "inbound-443" || ib.Protocol != "socks" || ib.Port != 50000 {
  389. t.Fatalf("unexpected bridge inbound: %+v", ib)
  390. }
  391. if string(ib.Listen) != `"127.0.0.1"` {
  392. t.Fatalf("bridge must listen on loopback, got %s", ib.Listen)
  393. }
  394. var routing egressRouting
  395. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  396. t.Fatal(err)
  397. }
  398. if len(routing.Rules) != 2 {
  399. t.Fatalf("expected the egress rule prepended to the existing rule, got %+v", routing.Rules)
  400. }
  401. first := routing.Rules[0]
  402. if first.Type != "field" || first.OutboundTag != "warp" ||
  403. len(first.InboundTag) != 1 || first.InboundTag[0] != "inbound-443" {
  404. t.Fatalf("egress rule must bind the inbound tag to the outbound, got %+v", first)
  405. }
  406. }
  407. func TestInjectMtprotoEgress_NoOutboundLeavesRouting(t *testing.T) {
  408. cfg := egressTestConfig()
  409. before := string(cfg.RouterConfig)
  410. injectMtprotoEgress(cfg, mtprotoInbound("inbound-443",
  411. `{"routeThroughXray":true,"routeXrayPort":50001}`))
  412. if len(cfg.InboundConfigs) != 2 || cfg.InboundConfigs[1].Port != 50001 {
  413. t.Fatalf("bridge must still be appended without an outbound, got %+v", cfg.InboundConfigs)
  414. }
  415. if string(cfg.RouterConfig) != before {
  416. t.Fatalf("no outbound means no rule change, got %s", cfg.RouterConfig)
  417. }
  418. }
  419. func TestInjectMtprotoEgress_BalancerTag(t *testing.T) {
  420. cfg := egressTestConfig()
  421. cfg.RouterConfig = json_util.RawMessage(`{"rules":[],"balancers":[{"tag":"lb","selector":["warp"]}]}`)
  422. injectMtprotoEgress(cfg, mtprotoInbound("inbound-443",
  423. `{"routeThroughXray":true,"routeXrayPort":50002,"outboundTag":"lb"}`))
  424. var routing struct {
  425. Rules []struct {
  426. OutboundTag string `json:"outboundTag"`
  427. BalancerTag string `json:"balancerTag"`
  428. } `json:"rules"`
  429. }
  430. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  431. t.Fatal(err)
  432. }
  433. if len(routing.Rules) != 1 || routing.Rules[0].BalancerTag != "lb" || routing.Rules[0].OutboundTag != "" {
  434. t.Fatalf("a balancer tag must target balancerTag, got %+v", routing.Rules)
  435. }
  436. }
  437. func TestInjectMtprotoEgress_Disabled(t *testing.T) {
  438. // Not routed, and routed-but-portless, are both no-ops.
  439. for _, settings := range []string{
  440. `{"routeThroughXray":false,"routeXrayPort":50000}`,
  441. `{"routeThroughXray":true}`,
  442. `{"routeThroughXray":true,"routeXrayPort":0}`,
  443. } {
  444. cfg := egressTestConfig()
  445. before := string(cfg.RouterConfig)
  446. injectMtprotoEgress(cfg, mtprotoInbound("inbound-443", settings))
  447. if len(cfg.InboundConfigs) != 1 || string(cfg.RouterConfig) != before {
  448. t.Fatalf("settings %s must be a no-op, got %d inbounds", settings, len(cfg.InboundConfigs))
  449. }
  450. }
  451. }
  452. func TestInjectMtprotoEgress_TagCollisionSkips(t *testing.T) {
  453. cfg := egressTestConfig()
  454. cfg.InboundConfigs = append(cfg.InboundConfigs,
  455. xray.InboundConfig{Port: 443, Protocol: "vless", Tag: "inbound-443"})
  456. before := string(cfg.RouterConfig)
  457. injectMtprotoEgress(cfg, mtprotoInbound("inbound-443",
  458. `{"routeThroughXray":true,"routeXrayPort":50003,"outboundTag":"warp"}`))
  459. if len(cfg.InboundConfigs) != 2 || string(cfg.RouterConfig) != before {
  460. t.Fatal("a real inbound already owning the tag must make the bridge a no-op")
  461. }
  462. }
  463. func TestInjectMtprotoEgress_MissingTargetSkips(t *testing.T) {
  464. cfg := egressTestConfig()
  465. before := string(cfg.RouterConfig)
  466. injectMtprotoEgress(cfg, mtprotoInbound("inbound-443",
  467. `{"routeThroughXray":true,"routeXrayPort":50004,"outboundTag":"removed-subscription-outbound"}`))
  468. if len(cfg.InboundConfigs) != 1 {
  469. t.Fatalf("a missing target must not expose the mtproto bridge, got %+v", cfg.InboundConfigs)
  470. }
  471. if string(cfg.RouterConfig) != before {
  472. t.Fatalf("a missing target must leave routing untouched, got %s", cfg.RouterConfig)
  473. }
  474. }
  475. func TestInjectMtprotoEgress_BadOutboundsSkips(t *testing.T) {
  476. cfg := egressTestConfig()
  477. cfg.OutboundConfigs = json_util.RawMessage(`{not json`)
  478. before := string(cfg.RouterConfig)
  479. injectMtprotoEgress(cfg, mtprotoInbound("inbound-443",
  480. `{"routeThroughXray":true,"routeXrayPort":50005,"outboundTag":"direct"}`))
  481. if len(cfg.InboundConfigs) != 1 {
  482. t.Fatalf("unparsable outbounds must not expose the mtproto bridge, got %+v", cfg.InboundConfigs)
  483. }
  484. if string(cfg.RouterConfig) != before {
  485. t.Fatalf("unparsable outbounds must leave routing untouched, got %s", cfg.RouterConfig)
  486. }
  487. }
  488. func TestInjectMtprotoEgress_BadRoutingSkips(t *testing.T) {
  489. cfg := egressTestConfig()
  490. cfg.RouterConfig = json_util.RawMessage(`{not json`)
  491. injectMtprotoEgress(cfg, mtprotoInbound("inbound-443",
  492. `{"routeThroughXray":true,"routeXrayPort":50006,"outboundTag":"direct"}`))
  493. if len(cfg.InboundConfigs) != 1 {
  494. t.Fatalf("unparsable routing must not expose the mtproto bridge, got %+v", cfg.InboundConfigs)
  495. }
  496. if string(cfg.RouterConfig) != `{not json` {
  497. t.Fatalf("unparsable routing must be left untouched, got %s", cfg.RouterConfig)
  498. }
  499. }