xray_config_inject_test.go 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. package service
  2. import (
  3. "encoding/json"
  4. "os"
  5. "testing"
  6. xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger"
  7. "github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
  8. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  9. "github.com/op/go-logging"
  10. )
  11. func TestMain(m *testing.M) {
  12. // injectPanelEgress logs when it skips injection; the package logger must
  13. // exist before any test exercises a skipped path.
  14. xuilogger.InitLogger(logging.ERROR)
  15. os.Exit(m.Run())
  16. }
  17. func TestEnsureAPIServices(t *testing.T) {
  18. // legacy template without RoutingService gets it injected
  19. out := ensureAPIServices(json_util.RawMessage(`{"services":["HandlerService","LoggerService","StatsService"],"tag":"api"}`))
  20. var parsed struct {
  21. Services []string `json:"services"`
  22. Tag string `json:"tag"`
  23. }
  24. if err := json.Unmarshal(out, &parsed); err != nil {
  25. t.Fatal(err)
  26. }
  27. want := map[string]bool{"HandlerService": true, "StatsService": true, "RoutingService": true, "LoggerService": true}
  28. if len(parsed.Services) != 4 {
  29. t.Fatalf("expected 4 services, got %v", parsed.Services)
  30. }
  31. for _, svc := range parsed.Services {
  32. if !want[svc] {
  33. t.Fatalf("unexpected service %q", svc)
  34. }
  35. }
  36. if parsed.Tag != "api" {
  37. t.Fatalf("tag must be preserved, got %q", parsed.Tag)
  38. }
  39. // complete api block is returned unchanged (no marshal churn)
  40. full := json_util.RawMessage(`{"services":["HandlerService","StatsService","RoutingService"],"tag":"api"}`)
  41. if got := ensureAPIServices(full); string(got) != string(full) {
  42. t.Fatalf("complete api block must pass through untouched, got %s", got)
  43. }
  44. // absent api block stays absent
  45. if got := ensureAPIServices(nil); got != nil {
  46. t.Fatalf("nil api block must stay nil, got %s", got)
  47. }
  48. }
  49. func TestEnsureStatsPolicy(t *testing.T) {
  50. // default-template shape: level "0" exists with traffic flags — the online
  51. // flag is added and the siblings survive untouched
  52. out := ensureStatsPolicy(json_util.RawMessage(`{"levels":{"0":{"handshake":4,"statsUserUplink":true,"statsUserDownlink":true}},"system":{"statsInboundDownlink":true}}`))
  53. var parsed struct {
  54. Levels map[string]map[string]any `json:"levels"`
  55. System map[string]any `json:"system"`
  56. }
  57. if err := json.Unmarshal(out, &parsed); err != nil {
  58. t.Fatal(err)
  59. }
  60. level0 := parsed.Levels["0"]
  61. if level0["statsUserOnline"] != true {
  62. t.Fatalf("statsUserOnline must be injected into level 0, got %v", level0)
  63. }
  64. if level0["statsUserUplink"] != true || level0["statsUserDownlink"] != true || level0["handshake"] != float64(4) {
  65. t.Fatalf("sibling keys must be preserved, got %v", level0)
  66. }
  67. if parsed.System["statsInboundDownlink"] != true {
  68. t.Fatalf("system block must be preserved, got %v", parsed.System)
  69. }
  70. // missing levels block: level "0" is created with the flag
  71. out = ensureStatsPolicy(json_util.RawMessage(`{"system":{}}`))
  72. if err := json.Unmarshal(out, &parsed); err != nil {
  73. t.Fatal(err)
  74. }
  75. if parsed.Levels["0"]["statsUserOnline"] != true {
  76. t.Fatalf("level 0 must be created with statsUserOnline, got %s", out)
  77. }
  78. // every level gets the flag, an explicit false included — the flag is
  79. // panel infrastructure, like the api services
  80. out = ensureStatsPolicy(json_util.RawMessage(`{"levels":{"0":{"statsUserOnline":false},"1":{"connIdle":300}}}`))
  81. if err := json.Unmarshal(out, &parsed); err != nil {
  82. t.Fatal(err)
  83. }
  84. for _, key := range []string{"0", "1"} {
  85. if parsed.Levels[key]["statsUserOnline"] != true {
  86. t.Fatalf("level %s must have statsUserOnline forced on, got %s", key, out)
  87. }
  88. }
  89. if parsed.Levels["1"]["connIdle"] != float64(300) {
  90. t.Fatalf("level 1 siblings must be preserved, got %s", out)
  91. }
  92. // already-enabled input passes through byte-identical (no marshal churn,
  93. // no spurious restart)
  94. full := json_util.RawMessage(`{"levels":{"0":{"statsUserOnline":true}}}`)
  95. if got := ensureStatsPolicy(full); string(got) != string(full) {
  96. t.Fatalf("already-enabled policy must pass through untouched, got %s", got)
  97. }
  98. // absent policy block stays absent
  99. if got := ensureStatsPolicy(nil); got != nil {
  100. t.Fatalf("nil policy must stay nil, got %s", got)
  101. }
  102. // unparsable policy is left untouched
  103. bad := json_util.RawMessage(`{not json`)
  104. if got := ensureStatsPolicy(bad); string(got) != string(bad) {
  105. t.Fatalf("unparsable policy must be left untouched, got %s", got)
  106. }
  107. }
  108. func egressTestConfig() *xray.Config {
  109. return &xray.Config{
  110. RouterConfig: json_util.RawMessage(`{"domainStrategy":"AsIs","rules":[{"type":"field","inboundTag":["api"],"outboundTag":"api"}]}`),
  111. InboundConfigs: []xray.InboundConfig{
  112. {Port: 62789, Protocol: "tunnel", Tag: "api", Listen: json_util.RawMessage(`"127.0.0.1"`)},
  113. },
  114. }
  115. }
  116. type egressRouting struct {
  117. DomainStrategy string `json:"domainStrategy"`
  118. Rules []struct {
  119. InboundTag []string `json:"inboundTag"`
  120. OutboundTag string `json:"outboundTag"`
  121. Type string `json:"type"`
  122. } `json:"rules"`
  123. }
  124. func TestInjectPanelEgress(t *testing.T) {
  125. cfg := egressTestConfig()
  126. injectPanelEgress(cfg, "warp")
  127. if len(cfg.InboundConfigs) != 2 {
  128. t.Fatalf("expected the egress inbound to be appended, got %d inbounds", len(cfg.InboundConfigs))
  129. }
  130. ib := cfg.InboundConfigs[1]
  131. if ib.Tag != PanelEgressInboundTag || ib.Protocol != "socks" || ib.Port != panelEgressBasePort {
  132. t.Fatalf("unexpected egress inbound: %+v", ib)
  133. }
  134. if string(ib.Listen) != `"127.0.0.1"` {
  135. t.Fatalf("egress inbound must listen on loopback, got %s", ib.Listen)
  136. }
  137. var routing egressRouting
  138. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  139. t.Fatal(err)
  140. }
  141. if routing.DomainStrategy != "AsIs" {
  142. t.Fatalf("routing keys outside rules must be preserved, got %+v", routing)
  143. }
  144. if len(routing.Rules) != 2 {
  145. t.Fatalf("expected egress rule + existing rule, got %+v", routing.Rules)
  146. }
  147. first := routing.Rules[0]
  148. if first.Type != "field" || first.OutboundTag != "warp" ||
  149. len(first.InboundTag) != 1 || first.InboundTag[0] != PanelEgressInboundTag {
  150. t.Fatalf("egress rule must be prepended, got %+v", first)
  151. }
  152. }
  153. func TestInjectPanelEgress_BalancerTag(t *testing.T) {
  154. cfg := egressTestConfig()
  155. cfg.RouterConfig = json_util.RawMessage(`{"domainStrategy":"AsIs","rules":[],"balancers":[{"tag":"lb","selector":["warp"]}]}`)
  156. // A tag that names a balancer must be targeted via balancerTag so the
  157. // router resolves it; an outbound tag coexisting with balancers still uses
  158. // outboundTag.
  159. injectPanelEgress(cfg, "lb")
  160. var routing struct {
  161. Rules []struct {
  162. InboundTag []string `json:"inboundTag"`
  163. OutboundTag string `json:"outboundTag"`
  164. BalancerTag string `json:"balancerTag"`
  165. Type string `json:"type"`
  166. } `json:"rules"`
  167. }
  168. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  169. t.Fatal(err)
  170. }
  171. if len(routing.Rules) != 1 {
  172. t.Fatalf("expected the egress rule, got %+v", routing.Rules)
  173. }
  174. first := routing.Rules[0]
  175. if first.BalancerTag != "lb" || first.OutboundTag != "" {
  176. t.Fatalf("a balancer tag must target balancerTag, not outboundTag, got %+v", first)
  177. }
  178. if len(first.InboundTag) != 1 || first.InboundTag[0] != PanelEgressInboundTag {
  179. t.Fatalf("egress rule must bind the egress inbound, got %+v", first)
  180. }
  181. // A non-balancer tag alongside balancers keeps the plain outbound path.
  182. cfg2 := egressTestConfig()
  183. cfg2.RouterConfig = json_util.RawMessage(`{"rules":[],"balancers":[{"tag":"lb","selector":["warp"]}]}`)
  184. injectPanelEgress(cfg2, "warp")
  185. var routing2 struct {
  186. Rules []struct {
  187. OutboundTag string `json:"outboundTag"`
  188. BalancerTag string `json:"balancerTag"`
  189. } `json:"rules"`
  190. }
  191. if err := json.Unmarshal(cfg2.RouterConfig, &routing2); err != nil {
  192. t.Fatal(err)
  193. }
  194. if routing2.Rules[0].OutboundTag != "warp" || routing2.Rules[0].BalancerTag != "" {
  195. t.Fatalf("a concrete outbound must target outboundTag, got %+v", routing2.Rules[0])
  196. }
  197. }
  198. func TestInjectPanelEgress_PortCollision(t *testing.T) {
  199. cfg := egressTestConfig()
  200. cfg.InboundConfigs = append(cfg.InboundConfigs,
  201. xray.InboundConfig{Port: panelEgressBasePort, Protocol: "vless", Tag: "in-1"},
  202. xray.InboundConfig{Port: panelEgressBasePort + 1, Protocol: "vless", Tag: "in-2"},
  203. )
  204. injectPanelEgress(cfg, "direct")
  205. got := cfg.InboundConfigs[len(cfg.InboundConfigs)-1]
  206. if got.Tag != PanelEgressInboundTag || got.Port != panelEgressBasePort+2 {
  207. t.Fatalf("egress inbound must skip taken ports, got %+v", got)
  208. }
  209. }
  210. func TestInjectPanelEgress_TagCollisionSkips(t *testing.T) {
  211. cfg := egressTestConfig()
  212. cfg.InboundConfigs = append(cfg.InboundConfigs,
  213. xray.InboundConfig{Port: 1234, Protocol: "socks", Tag: PanelEgressInboundTag},
  214. )
  215. before := string(cfg.RouterConfig)
  216. injectPanelEgress(cfg, "direct")
  217. if len(cfg.InboundConfigs) != 2 || string(cfg.RouterConfig) != before {
  218. t.Fatal("a user inbound owning the egress tag must make injection a no-op")
  219. }
  220. }
  221. func TestInjectPanelEgress_NoRoutingSection(t *testing.T) {
  222. cfg := egressTestConfig()
  223. cfg.RouterConfig = nil
  224. injectPanelEgress(cfg, "direct")
  225. var routing egressRouting
  226. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  227. t.Fatal(err)
  228. }
  229. if len(routing.Rules) != 1 || routing.Rules[0].OutboundTag != "direct" {
  230. t.Fatalf("a routing section must be created with the egress rule, got %+v", routing)
  231. }
  232. if len(cfg.InboundConfigs) != 2 {
  233. t.Fatal("egress inbound must still be appended")
  234. }
  235. }
  236. func TestInjectPanelEgress_BadRoutingSkips(t *testing.T) {
  237. cfg := egressTestConfig()
  238. cfg.RouterConfig = json_util.RawMessage(`{not json`)
  239. injectPanelEgress(cfg, "direct")
  240. if len(cfg.InboundConfigs) != 1 {
  241. t.Fatal("unparsable routing must skip the whole injection, inbound included")
  242. }
  243. if string(cfg.RouterConfig) != `{not json` {
  244. t.Fatal("unparsable routing must be left untouched")
  245. }
  246. }