xray_config_inject_test.go 13 KB

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