xray_config_inject_test.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  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_PortCollision(t *testing.T) {
  154. cfg := egressTestConfig()
  155. cfg.InboundConfigs = append(cfg.InboundConfigs,
  156. xray.InboundConfig{Port: panelEgressBasePort, Protocol: "vless", Tag: "in-1"},
  157. xray.InboundConfig{Port: panelEgressBasePort + 1, Protocol: "vless", Tag: "in-2"},
  158. )
  159. injectPanelEgress(cfg, "direct")
  160. got := cfg.InboundConfigs[len(cfg.InboundConfigs)-1]
  161. if got.Tag != PanelEgressInboundTag || got.Port != panelEgressBasePort+2 {
  162. t.Fatalf("egress inbound must skip taken ports, got %+v", got)
  163. }
  164. }
  165. func TestInjectPanelEgress_TagCollisionSkips(t *testing.T) {
  166. cfg := egressTestConfig()
  167. cfg.InboundConfigs = append(cfg.InboundConfigs,
  168. xray.InboundConfig{Port: 1234, Protocol: "socks", Tag: PanelEgressInboundTag},
  169. )
  170. before := string(cfg.RouterConfig)
  171. injectPanelEgress(cfg, "direct")
  172. if len(cfg.InboundConfigs) != 2 || string(cfg.RouterConfig) != before {
  173. t.Fatal("a user inbound owning the egress tag must make injection a no-op")
  174. }
  175. }
  176. func TestInjectPanelEgress_NoRoutingSection(t *testing.T) {
  177. cfg := egressTestConfig()
  178. cfg.RouterConfig = nil
  179. injectPanelEgress(cfg, "direct")
  180. var routing egressRouting
  181. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  182. t.Fatal(err)
  183. }
  184. if len(routing.Rules) != 1 || routing.Rules[0].OutboundTag != "direct" {
  185. t.Fatalf("a routing section must be created with the egress rule, got %+v", routing)
  186. }
  187. if len(cfg.InboundConfigs) != 2 {
  188. t.Fatal("egress inbound must still be appended")
  189. }
  190. }
  191. func TestInjectPanelEgress_BadRoutingSkips(t *testing.T) {
  192. cfg := egressTestConfig()
  193. cfg.RouterConfig = json_util.RawMessage(`{not json`)
  194. injectPanelEgress(cfg, "direct")
  195. if len(cfg.InboundConfigs) != 1 {
  196. t.Fatal("unparsable routing must skip the whole injection, inbound included")
  197. }
  198. if string(cfg.RouterConfig) != `{not json` {
  199. t.Fatal("unparsable routing must be left untouched")
  200. }
  201. }