1
0

rewriter_protocol_case_test.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. package database
  2. import (
  3. "encoding/json"
  4. "strings"
  5. "testing"
  6. "github.com/mhsanaei/3x-ui/v3/internal/config"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  8. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  9. )
  10. func TestRewriteRemovedOutboundKeysSeesAnUppercaseFreedom(t *testing.T) {
  11. raw := `{"outbounds":[{"protocol":"Freedom","tag":"direct","settings":{},"streamSettings":{"sockopt":{"addressPortStrategy":"SrvPortOnly"}}}]}`
  12. var before struct {
  13. Outbounds []json.RawMessage `json:"outbounds"`
  14. }
  15. if err := json.Unmarshal([]byte(raw), &before); err != nil {
  16. t.Fatal(err)
  17. }
  18. // A refusal here is the proof the core treated it as freedom, not as unknown.
  19. if err := xray.ValidateOutboundConfig(before.Outbounds[0]); err == nil {
  20. t.Fatal("expected the vendored core to refuse the legacy addressPortStrategy")
  21. }
  22. updated, changed, err := rewriteRemovedOutboundKeys(raw)
  23. if err != nil {
  24. t.Fatalf("rewrite: %v", err)
  25. }
  26. if !changed {
  27. t.Fatal(`an outbound spelled "Freedom" was left with the key the core refuses`)
  28. }
  29. var after struct {
  30. Outbounds []json.RawMessage `json:"outbounds"`
  31. }
  32. if err := json.Unmarshal([]byte(updated), &after); err != nil {
  33. t.Fatal(err)
  34. }
  35. if err := xray.ValidateOutboundConfig(after.Outbounds[0]); err != nil {
  36. t.Fatalf("rewritten outbound still refused by xray-core: %v", err)
  37. }
  38. }
  39. func TestRewriteUppercaseFreedomFinalRules(t *testing.T) {
  40. hardened := []any{
  41. map[string]any{"action": "block", "ip": []any{"geoip:private"}},
  42. map[string]any{"action": "allow"},
  43. }
  44. tests := []struct {
  45. name string
  46. raw string
  47. wantChanged bool
  48. wantRules []any
  49. }{
  50. {
  51. name: "stock allow-only rules are hardened",
  52. raw: `{"outbounds":[{"protocol":"Freedom","tag":"direct","settings":{"finalRules":[{"action":"allow"}]}}]}`,
  53. wantChanged: true,
  54. wantRules: hardened,
  55. },
  56. {
  57. name: "legacy private-only allow is hardened",
  58. raw: `{"outbounds":[{"protocol":"FREEDOM","tag":"direct","settings":{"finalRules":[{"action":"allow","ip":["geoip:private"]}]}}]}`,
  59. wantChanged: true,
  60. wantRules: hardened,
  61. },
  62. {
  63. name: "missing finalRules is hardened",
  64. raw: `{"outbounds":[{"protocol":"Freedom","tag":"direct","settings":{}}]}`,
  65. wantChanged: true,
  66. wantRules: hardened,
  67. },
  68. {
  69. name: "customized rules are preserved",
  70. raw: `{"outbounds":[{"protocol":"Freedom","tag":"direct","settings":{"finalRules":[{"action":"block","ip":["1.2.3.4"]},{"action":"allow"}]}}]}`,
  71. wantChanged: false,
  72. },
  73. {
  74. name: "a canonical spelling was already handled by its own seeder",
  75. raw: `{"outbounds":[{"protocol":"freedom","tag":"direct","settings":{"finalRules":[{"action":"allow"}]}}]}`,
  76. wantChanged: false,
  77. },
  78. {
  79. name: "another protocol is ignored",
  80. raw: `{"outbounds":[{"protocol":"blackhole","tag":"blocked","settings":{}}]}`,
  81. wantChanged: false,
  82. },
  83. }
  84. for _, tc := range tests {
  85. t.Run(tc.name, func(t *testing.T) {
  86. updated, changed, err := rewriteUppercaseFreedomFinalRules(tc.raw)
  87. if err != nil {
  88. t.Fatalf("rewrite: %v", err)
  89. }
  90. if changed != tc.wantChanged {
  91. t.Fatalf("changed = %v, want %v (out: %s)", changed, tc.wantChanged, updated)
  92. }
  93. if !tc.wantChanged {
  94. if updated != tc.raw {
  95. t.Fatalf("raw config mutated without change flag:\n%s", updated)
  96. }
  97. return
  98. }
  99. var cfg map[string]any
  100. if err := json.Unmarshal([]byte(updated), &cfg); err != nil {
  101. t.Fatalf("updated config is not valid json: %v", err)
  102. }
  103. outbounds := cfg["outbounds"].([]any)
  104. settings := outbounds[0].(map[string]any)["settings"].(map[string]any)
  105. gotRules, _ := json.Marshal(settings["finalRules"])
  106. wantRules, _ := json.Marshal(tc.wantRules)
  107. if string(gotRules) != string(wantRules) {
  108. t.Fatalf("finalRules = %s, want %s", gotRules, wantRules)
  109. }
  110. })
  111. }
  112. }
  113. // The two earlier seeders recorded their rows before this predicate existed, so
  114. // this pins the re-run reaching a panel whose history already has both.
  115. func TestUppercaseFreedomFinalRulesFixReachesHistoryGatedPanels(t *testing.T) {
  116. t.Setenv("XUI_DB_FOLDER", t.TempDir())
  117. if err := InitDB(config.GetDBPath()); err != nil {
  118. if strings.Contains(err.Error(), "CGO_ENABLED=0") {
  119. t.Skipf("sqlite needs cgo: %v", err)
  120. }
  121. t.Fatalf("init db: %v", err)
  122. }
  123. t.Cleanup(func() { _ = CloseDB() })
  124. stock := `{"outbounds":[{"protocol":"Freedom","tag":"direct","settings":{"finalRules":[{"action":"allow"}]}}]}`
  125. seedTemplate(t, stock)
  126. // InitDB pre-seeds a fresh install's rows, so the earlier two are already
  127. // recorded and this seeder has to look unapplied for the run to reach it.
  128. for _, name := range []string{"FreedomFinalRulesReverseFix", "FreedomFinalRulesPrivateEgressBlock"} {
  129. var count int64
  130. if err := db.Model(&model.HistoryOfSeeders{}).
  131. Where("seeder_name = ?", name).Count(&count).Error; err != nil {
  132. t.Fatalf("count %s: %v", name, err)
  133. }
  134. if count == 0 {
  135. t.Fatalf("%s is no longer pre-seeded on a fresh install", name)
  136. }
  137. }
  138. if err := db.Where("seeder_name = ?", "UppercaseFreedomFinalRulesFix").
  139. Delete(&model.HistoryOfSeeders{}).Error; err != nil {
  140. t.Fatalf("clear seeder history: %v", err)
  141. }
  142. if err := runSeeders(false); err != nil {
  143. t.Fatalf("runSeeders: %v", err)
  144. }
  145. var cfg struct {
  146. Outbounds []map[string]any `json:"outbounds"`
  147. }
  148. if err := json.Unmarshal([]byte(storedTemplate(t)), &cfg); err != nil {
  149. t.Fatalf("stored template is not JSON: %v", err)
  150. }
  151. settings, _ := cfg.Outbounds[0]["settings"].(map[string]any)
  152. if settings["finalRules"] == nil {
  153. t.Fatal("the hardening never reached an outbound spelled Freedom")
  154. }
  155. if proto, _ := cfg.Outbounds[0]["protocol"].(string); proto != "Freedom" {
  156. t.Fatalf("the seeder rewrote the protocol id: %q", proto)
  157. }
  158. if hardened := storedTemplate(t); !strings.Contains(hardened, `"geoip:private"`) {
  159. t.Fatalf("stored finalRules are not the hardened pair: %s", hardened)
  160. }
  161. // A second pass must leave the template alone: the row it just wrote gates it.
  162. after := storedTemplate(t)
  163. if err := runSeeders(false); err != nil {
  164. t.Fatalf("second runSeeders: %v", err)
  165. }
  166. if got := storedTemplate(t); got != after {
  167. t.Fatalf("the re-run seed rewrote the template again:\n got %s\nwant %s", got, after)
  168. }
  169. }
  170. // The core lowercases a protocol id before looking up its handler, so a config
  171. // that runs as freedom must migrate the same whether it says Freedom or freedom.
  172. func TestRewritersTreatProtocolCaseAlike(t *testing.T) {
  173. tests := []struct {
  174. name string
  175. rewrite func(string) (string, bool, error)
  176. raw string
  177. }{
  178. {
  179. name: "removed outbound keys",
  180. rewrite: rewriteRemovedOutboundKeys,
  181. raw: `{"outbounds":[{"protocol":"freedom","tag":"direct","settings":{},"streamSettings":{"sockopt":{"addressPortStrategy":"SrvPortOnly"}}}]}`,
  182. },
  183. {
  184. name: "freedom final rules reverse",
  185. rewrite: rewriteFreedomFinalRules,
  186. raw: `{"outbounds":[{"protocol":"freedom","tag":"direct","settings":{"finalRules":[{"action":"allow","ip":["geoip:private"]}]}}]}`,
  187. },
  188. {
  189. name: "freedom private egress block",
  190. rewrite: rewriteFreedomFinalRulesPrivateEgress,
  191. raw: `{"outbounds":[{"protocol":"freedom","tag":"direct","settings":{"finalRules":[]}}]}`,
  192. },
  193. }
  194. for _, tt := range tests {
  195. t.Run(tt.name, func(t *testing.T) {
  196. want, wantChanged, err := tt.rewrite(tt.raw)
  197. if err != nil || !wantChanged {
  198. t.Fatalf("lowercase rewrite: changed=%v err=%v", wantChanged, err)
  199. }
  200. upper := strings.Replace(tt.raw, `"protocol":"freedom"`, `"protocol":"Freedom"`, 1)
  201. got, changed, err := tt.rewrite(upper)
  202. if err != nil {
  203. t.Fatalf("uppercase rewrite: %v", err)
  204. }
  205. if !changed {
  206. t.Fatalf(`the rewrite skipped an outbound spelled "Freedom"`)
  207. }
  208. if loweredGot, loweredWant := lowercaseProtocol(t, got), lowercaseProtocol(t, want); loweredGot != loweredWant {
  209. t.Fatalf("uppercase result differs from lowercase:\n got %s\nwant %s", loweredGot, loweredWant)
  210. }
  211. })
  212. }
  213. }
  214. // The rewriters keep the spelling they were given; only the migrated keys may
  215. // differ, so the comparison ignores the case of the protocol id itself.
  216. func lowercaseProtocol(t *testing.T, raw string) string {
  217. t.Helper()
  218. var cfg map[string]any
  219. if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
  220. t.Fatal(err)
  221. }
  222. outbounds, _ := cfg["outbounds"].([]any)
  223. for _, ob := range outbounds {
  224. obj, ok := ob.(map[string]any)
  225. if !ok {
  226. continue
  227. }
  228. if proto, ok := obj["protocol"].(string); ok {
  229. obj["protocol"] = strings.ToLower(proto)
  230. }
  231. }
  232. out, err := json.Marshal(cfg)
  233. if err != nil {
  234. t.Fatal(err)
  235. }
  236. return string(out)
  237. }