xray_setting_test.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. package service
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "strings"
  6. "testing"
  7. )
  8. func TestShouldSkipLegacyUnencryptedOutboundRejection(t *testing.T) {
  9. prohibited := errors.New("vless without TLS or other encryption is prohibited unless the server address is a private IP or domain")
  10. tests := []struct {
  11. name string
  12. version string
  13. err error
  14. want bool
  15. }{
  16. {name: "older core", version: "26.4.25", err: prohibited, want: true},
  17. {name: "boundary version", version: "26.7.11", err: prohibited, want: false},
  18. {name: "newer core", version: "26.10.0", err: prohibited, want: false},
  19. {name: "unknown version", version: "Unknown", err: prohibited, want: false},
  20. {name: "unparseable version", version: "26.7", err: prohibited, want: false},
  21. {name: "empty version", version: "", err: prohibited, want: false},
  22. {name: "unrelated validation error", version: "26.4.25", err: errors.New("invalid outbound"), want: false},
  23. }
  24. for _, tt := range tests {
  25. t.Run(tt.name, func(t *testing.T) {
  26. if got := shouldSkipLegacyUnencryptedOutboundRejection(tt.version, tt.err); got != tt.want {
  27. t.Fatalf("shouldSkipLegacyUnencryptedOutboundRejection(%q, %v) = %v, want %v", tt.version, tt.err, got, tt.want)
  28. }
  29. })
  30. }
  31. }
  32. func TestCompareXrayCoreVersions(t *testing.T) {
  33. tests := []struct {
  34. a, b string
  35. want int
  36. ok bool
  37. }{
  38. {a: "26.7.11", b: "26.7.11", want: 0, ok: true},
  39. {a: "26.10.0", b: "26.7.11", want: 1, ok: true},
  40. {a: "v26.4.25", b: "26.7.11", want: -1, ok: true},
  41. {a: "Unknown", b: "26.7.11", want: 0, ok: false},
  42. }
  43. for _, tt := range tests {
  44. got, ok := compareXrayCoreVersions(tt.a, tt.b)
  45. if got != tt.want || ok != tt.ok {
  46. t.Errorf("compareXrayCoreVersions(%q, %q) = (%d, %v), want (%d, %v)", tt.a, tt.b, got, ok, tt.want, tt.ok)
  47. }
  48. }
  49. }
  50. func TestUnwrapXrayTemplateConfig(t *testing.T) {
  51. real := `{"log":{},"inbounds":[],"outbounds":[],"routing":{}}`
  52. t.Run("passes through a clean config", func(t *testing.T) {
  53. if got := UnwrapXrayTemplateConfig(real); got != real {
  54. t.Fatalf("clean config was modified: %s", got)
  55. }
  56. })
  57. t.Run("passes through invalid JSON unchanged", func(t *testing.T) {
  58. in := "not json at all"
  59. if got := UnwrapXrayTemplateConfig(in); got != in {
  60. t.Fatalf("invalid input was modified: %s", got)
  61. }
  62. })
  63. t.Run("unwraps one layer of response-shaped wrapper", func(t *testing.T) {
  64. wrapper := `{"inboundTags":["tag"],"outboundTestUrl":"x","xraySetting":` + real + `}`
  65. got := UnwrapXrayTemplateConfig(wrapper)
  66. if !equalJSON(t, got, real) {
  67. t.Fatalf("want %s, got %s", real, got)
  68. }
  69. })
  70. t.Run("unwraps multiple stacked layers", func(t *testing.T) {
  71. lvl1 := `{"xraySetting":` + real + `}`
  72. lvl2 := `{"xraySetting":` + lvl1 + `}`
  73. lvl3 := `{"xraySetting":` + lvl2 + `}`
  74. got := UnwrapXrayTemplateConfig(lvl3)
  75. if !equalJSON(t, got, real) {
  76. t.Fatalf("want %s, got %s", real, got)
  77. }
  78. })
  79. t.Run("handles an xraySetting stored as a JSON-encoded string", func(t *testing.T) {
  80. encoded, _ := json.Marshal(real) // becomes a quoted string
  81. wrapper := `{"xraySetting":` + string(encoded) + `}`
  82. got := UnwrapXrayTemplateConfig(wrapper)
  83. if !equalJSON(t, got, real) {
  84. t.Fatalf("want %s, got %s", real, got)
  85. }
  86. })
  87. t.Run("does not unwrap when top level already has real xray keys", func(t *testing.T) {
  88. // Pathological but defensible: if a user's actual config somehow
  89. // has both the real keys and an unrelated `xraySetting` key, we
  90. // must not strip it.
  91. in := `{"inbounds":[],"xraySetting":{"some":"thing"}}`
  92. got := UnwrapXrayTemplateConfig(in)
  93. if got != in {
  94. t.Fatalf("should have left real config alone, got %s", got)
  95. }
  96. })
  97. t.Run("stops at a reasonable depth", func(t *testing.T) {
  98. // Build a deeper-than-maxDepth chain that ends at something
  99. // non-wrapped, and confirm we end up at some valid JSON (we
  100. // don't loop forever and we don't blow the stack).
  101. s := real
  102. for range 16 {
  103. s = `{"xraySetting":` + s + `}`
  104. }
  105. got := UnwrapXrayTemplateConfig(s)
  106. if !strings.Contains(got, `"inbounds"`) && !strings.Contains(got, `"xraySetting"`) {
  107. t.Fatalf("unexpected tail: %s", got)
  108. }
  109. })
  110. }
  111. func equalJSON(t *testing.T, a, b string) bool {
  112. t.Helper()
  113. var va, vb any
  114. if err := json.Unmarshal([]byte(a), &va); err != nil {
  115. return false
  116. }
  117. if err := json.Unmarshal([]byte(b), &vb); err != nil {
  118. return false
  119. }
  120. ja, _ := json.Marshal(va)
  121. jb, _ := json.Marshal(vb)
  122. return string(ja) == string(jb)
  123. }
  124. // firstRuleOutbound parses the (post-hoisted) config and returns
  125. // routing.rules[0].outboundTag, or "" if anything is missing.
  126. func firstRuleOutbound(t *testing.T, raw string) string {
  127. t.Helper()
  128. var cfg map[string]any
  129. if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
  130. t.Fatalf("unmarshal cfg: %v", err)
  131. }
  132. routing, _ := cfg["routing"].(map[string]any)
  133. rules, _ := routing["rules"].([]any)
  134. if len(rules) == 0 {
  135. return ""
  136. }
  137. first, _ := rules[0].(map[string]any)
  138. tag, _ := first["outboundTag"].(string)
  139. return tag
  140. }
  141. func TestEnsureStatsRouting_HoistsApiRuleFromMiddle(t *testing.T) {
  142. // #4113 repro shape: admin added a cascade outbound and put a
  143. // catch-all routing rule above the api rule. stats query path
  144. // gets starved by the catch-all unless we hoist the api rule.
  145. in := `{
  146. "routing": {
  147. "rules": [
  148. {"type":"field","inboundTag":["inbound-vless"],"outboundTag":"vless-cascade"},
  149. {"type":"field","inboundTag":["api"],"outboundTag":"api"},
  150. {"type":"field","outboundTag":"blocked","ip":["geoip:private"]}
  151. ]
  152. }
  153. }`
  154. out, err := EnsureStatsRouting(in)
  155. if err != nil {
  156. t.Fatalf("unexpected err: %v", err)
  157. }
  158. if got := firstRuleOutbound(t, out); got != "api" {
  159. t.Fatalf("api rule should be at index 0 after hoist, got first outboundTag = %q\nfull: %s", got, out)
  160. }
  161. }
  162. func TestEnsureStatsRouting_NoOpWhenAlreadyFirst(t *testing.T) {
  163. // Don't churn the JSON when nothing needs fixing — same string in,
  164. // same string out. Lets the diff in the panel UI stay quiet for
  165. // well-formed configs.
  166. in := `{"routing":{"rules":[{"type":"field","inboundTag":["api"],"outboundTag":"api"},{"type":"field","outboundTag":"blocked","ip":["geoip:private"]}]}}`
  167. out, err := EnsureStatsRouting(in)
  168. if err != nil {
  169. t.Fatalf("unexpected err: %v", err)
  170. }
  171. if out != in {
  172. t.Fatalf("expected unchanged input, got: %s", out)
  173. }
  174. }
  175. func TestEnsureStatsRouting_InsertsDefaultWhenMissing(t *testing.T) {
  176. // Some admins delete the api rule by accident. Re-add a default
  177. // at the front so stats keep working after the next save.
  178. in := `{"routing":{"rules":[{"type":"field","outboundTag":"vless-cascade","inboundTag":["inbound-vless"]}]}}`
  179. out, err := EnsureStatsRouting(in)
  180. if err != nil {
  181. t.Fatalf("unexpected err: %v", err)
  182. }
  183. if got := firstRuleOutbound(t, out); got != "api" {
  184. t.Fatalf("default api rule should be inserted at index 0, got %q\nfull: %s", got, out)
  185. }
  186. // The original rule should still be there, just shifted.
  187. var cfg map[string]any
  188. json.Unmarshal([]byte(out), &cfg)
  189. rules := cfg["routing"].(map[string]any)["rules"].([]any)
  190. if len(rules) != 2 {
  191. t.Fatalf("expected 2 rules after insert, got %d: %v", len(rules), rules)
  192. }
  193. }
  194. func TestEnsureStatsRouting_NoRoutingBlock(t *testing.T) {
  195. // Pathological but possible: empty config or one without a routing
  196. // section. Don't crash, and create the section with the api rule.
  197. in := `{"log":{}}`
  198. out, err := EnsureStatsRouting(in)
  199. if err != nil {
  200. t.Fatalf("unexpected err: %v", err)
  201. }
  202. if got := firstRuleOutbound(t, out); got != "api" {
  203. t.Fatalf("api rule should be created when routing was missing, got %q\nfull: %s", got, out)
  204. }
  205. }
  206. func TestEnsureStatsRouting_InvalidJsonReturnsAsIs(t *testing.T) {
  207. // SaveXraySetting calls CheckXrayConfig before this helper, so
  208. // invalid JSON shouldn't reach us in practice — but be defensive
  209. // about garbage in (return same garbage out plus an error) so the
  210. // caller can choose to skip the hoist instead of corrupting input.
  211. in := "definitely not json"
  212. out, err := EnsureStatsRouting(in)
  213. if err == nil {
  214. t.Fatalf("expected error for invalid json, got none")
  215. }
  216. if out != in {
  217. t.Fatalf("expected raw passthrough on error, got %q", out)
  218. }
  219. }
  220. func TestEnsureStatsRouting_AcceptsInboundTagAsString(t *testing.T) {
  221. // Some manually-edited configs use a single string instead of an
  222. // array for inboundTag. Make sure we still recognize the api rule.
  223. in := `{"routing":{"rules":[{"type":"field","inboundTag":["other"],"outboundTag":"vless-cascade"},{"type":"field","inboundTag":"api","outboundTag":"api"}]}}`
  224. out, err := EnsureStatsRouting(in)
  225. if err != nil {
  226. t.Fatalf("unexpected err: %v", err)
  227. }
  228. if got := firstRuleOutbound(t, out); got != "api" {
  229. t.Fatalf("api rule with string-form inboundTag should hoist to front, got %q\nfull: %s", got, out)
  230. }
  231. }