freedom_domain_strategy_migration_test.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. package database
  2. import (
  3. "encoding/json"
  4. "strings"
  5. "testing"
  6. corelog "github.com/xtls/xray-core/common/log"
  7. "github.com/mhsanaei/3x-ui/v3/internal/config"
  8. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  9. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  10. )
  11. func TestRewriteFreedomDomainStrategy(t *testing.T) {
  12. tests := []struct {
  13. name string
  14. raw string
  15. wantChanged bool
  16. wantOutbound map[string]any
  17. }{
  18. {
  19. name: "the deprecated settings key moves to sockopt",
  20. raw: `{"outbounds":[{"protocol":"freedom","tag":"direct","settings":{"domainStrategy":"UseIPv4","finalRules":[{"action":"allow"}]}}]}`,
  21. wantChanged: true,
  22. wantOutbound: map[string]any{
  23. "protocol": "freedom", "tag": "direct",
  24. "settings": map[string]any{"finalRules": []any{map[string]any{"action": "allow"}}},
  25. "streamSettings": map[string]any{"sockopt": map[string]any{"domainStrategy": "UseIPv4"}},
  26. },
  27. },
  28. {
  29. name: "the outbound-root targetStrategy moves to sockopt and is dropped",
  30. raw: `{"outbounds":[{"protocol":"freedom","tag":"direct","targetStrategy":"ForceIPv6","settings":{}}]}`,
  31. wantChanged: true,
  32. wantOutbound: map[string]any{
  33. "protocol": "freedom", "tag": "direct", "settings": map[string]any{},
  34. "streamSettings": map[string]any{"sockopt": map[string]any{"domainStrategy": "ForceIPv6"}},
  35. },
  36. },
  37. {
  38. name: "the root key wins over the settings key, as in the core",
  39. raw: `{"outbounds":[{"protocol":"freedom","tag":"direct","targetStrategy":"UseIPv4","settings":{"domainStrategy":"UseIPv6"}}]}`,
  40. wantChanged: true,
  41. wantOutbound: map[string]any{
  42. "protocol": "freedom", "tag": "direct", "settings": map[string]any{},
  43. "streamSettings": map[string]any{"sockopt": map[string]any{"domainStrategy": "UseIPv4"}},
  44. },
  45. },
  46. {
  47. name: "the settings targetStrategy wins over domainStrategy",
  48. raw: `{"outbounds":[{"protocol":"freedom","tag":"direct","settings":{"targetStrategy":"UseIPv6","domainStrategy":"UseIPv4"}}]}`,
  49. wantChanged: true,
  50. wantOutbound: map[string]any{
  51. "protocol": "freedom", "tag": "direct", "settings": map[string]any{},
  52. "streamSettings": map[string]any{"sockopt": map[string]any{"domainStrategy": "UseIPv6"}},
  53. },
  54. },
  55. {
  56. name: "an AsIs alias is dropped and leaves the sockopt value alone",
  57. raw: `{"outbounds":[{"protocol":"freedom","tag":"direct","settings":{"domainStrategy":"AsIs"},"streamSettings":{"sockopt":{"domainStrategy":"UseIPv6","tcpFastOpen":true}}}]}`,
  58. wantChanged: true,
  59. wantOutbound: map[string]any{
  60. "protocol": "freedom", "tag": "direct", "settings": map[string]any{},
  61. "streamSettings": map[string]any{"sockopt": map[string]any{"domainStrategy": "UseIPv6", "tcpFastOpen": true}},
  62. },
  63. },
  64. {
  65. name: "the existing sockopt spelling is preserved",
  66. raw: `{"outbounds":[{"protocol":"freedom","tag":"direct","settings":{"domainStrategy":"useipv4v6"}}]}`,
  67. wantChanged: true,
  68. wantOutbound: map[string]any{
  69. "protocol": "freedom", "tag": "direct", "settings": map[string]any{},
  70. "streamSettings": map[string]any{"sockopt": map[string]any{"domainStrategy": "useipv4v6"}},
  71. },
  72. },
  73. {
  74. name: "a strategy the core refuses is dropped rather than moved",
  75. raw: `{"outbounds":[{"protocol":"freedom","tag":"direct","settings":{"domainStrategy":"UseIPv5"}}]}`,
  76. wantChanged: true,
  77. wantOutbound: map[string]any{
  78. "protocol": "freedom", "tag": "direct", "settings": map[string]any{},
  79. },
  80. },
  81. {
  82. name: "other protocols keep their outbound-root targetStrategy",
  83. raw: `{"outbounds":[{"protocol":"vless","tag":"proxy","targetStrategy":"UseIPv4","settings":{}}]}`,
  84. wantChanged: false,
  85. wantOutbound: map[string]any{
  86. "protocol": "vless", "tag": "proxy", "targetStrategy": "UseIPv4",
  87. "settings": map[string]any{},
  88. },
  89. },
  90. {
  91. name: "a freedom outbound without a strategy is left untouched",
  92. raw: `{"outbounds":[{"protocol":"freedom","tag":"direct","settings":{"finalRules":[{"action":"allow"}]}}]}`,
  93. wantChanged: false,
  94. wantOutbound: map[string]any{
  95. "protocol": "freedom", "tag": "direct",
  96. "settings": map[string]any{"finalRules": []any{map[string]any{"action": "allow"}}},
  97. },
  98. },
  99. }
  100. for _, tc := range tests {
  101. t.Run(tc.name, func(t *testing.T) {
  102. updated, changed, err := rewriteFreedomDomainStrategy(tc.raw)
  103. if err != nil {
  104. t.Fatalf("unexpected error: %v", err)
  105. }
  106. if changed != tc.wantChanged {
  107. t.Fatalf("changed = %v, want %v", changed, tc.wantChanged)
  108. }
  109. var cfg struct {
  110. Outbounds []map[string]any `json:"outbounds"`
  111. }
  112. if err := json.Unmarshal([]byte(updated), &cfg); err != nil {
  113. t.Fatalf("rewritten template is not JSON: %v", err)
  114. }
  115. if len(cfg.Outbounds) != 1 {
  116. t.Fatalf("got %d outbounds, want 1", len(cfg.Outbounds))
  117. }
  118. got, _ := json.Marshal(cfg.Outbounds[0])
  119. want, _ := json.Marshal(tc.wantOutbound)
  120. if string(got) != string(want) {
  121. t.Fatalf("outbound = %s, want %s", got, want)
  122. }
  123. })
  124. }
  125. }
  126. type coreLogCapture struct{ msgs []string }
  127. func (c *coreLogCapture) Handle(msg corelog.Message) { c.msgs = append(c.msgs, msg.String()) }
  128. func (c *coreLogCapture) has(sub string) bool {
  129. return strings.Contains(strings.Join(c.msgs, "\n"), sub)
  130. }
  131. type discardLogHandler struct{}
  132. func (discardLogHandler) Handle(corelog.Message) {}
  133. // captureCoreLogs takes over the vendored core's log sink for the duration of
  134. // one test, which is the only way to observe a config-load warning.
  135. func captureCoreLogs(t *testing.T) *coreLogCapture {
  136. t.Helper()
  137. capture := new(coreLogCapture)
  138. corelog.RegisterHandler(capture)
  139. t.Cleanup(func() { corelog.RegisterHandler(discardLogHandler{}) })
  140. return capture
  141. }
  142. // Drives the real core: a rewrite that dropped the value instead of moving it
  143. // would leave the config warning on every load and fail here.
  144. func TestRewriteFreedomDomainStrategySatisfiesCore(t *testing.T) {
  145. for _, tc := range []struct {
  146. name string
  147. raw string
  148. wantValue string
  149. }{
  150. {
  151. name: "deprecated settings key",
  152. raw: `{"protocol":"freedom","tag":"direct","settings":{"domainStrategy":"UseIPv4","finalRules":[{"action":"allow"}]}}`,
  153. wantValue: `"domainStrategy": "UseIPv4"`,
  154. },
  155. {
  156. name: "outbound-root targetStrategy",
  157. raw: `{"protocol":"freedom","tag":"direct","targetStrategy":"ForceIPv6","settings":{}}`,
  158. wantValue: `"domainStrategy": "ForceIPv6"`,
  159. },
  160. } {
  161. t.Run(tc.name, func(t *testing.T) {
  162. capture := captureCoreLogs(t)
  163. if err := xray.ValidateOutboundConfig([]byte(tc.raw)); err != nil {
  164. t.Fatalf("xray-core must accept the legacy outbound: %v", err)
  165. }
  166. if !capture.has("sockopt.domainStrategy") {
  167. t.Fatal("expected the core to warn about the legacy strategy placement")
  168. }
  169. updated, changed, err := rewriteFreedomDomainStrategy(
  170. `{"outbounds":[` + tc.raw + `]}`,
  171. )
  172. if err != nil || !changed {
  173. t.Fatalf("rewrite: changed=%v err=%v", changed, err)
  174. }
  175. var after struct {
  176. Outbounds []json.RawMessage `json:"outbounds"`
  177. }
  178. if err := json.Unmarshal([]byte(updated), &after); err != nil {
  179. t.Fatal(err)
  180. }
  181. if !strings.Contains(string(after.Outbounds[0]), tc.wantValue) {
  182. t.Fatalf("rewritten outbound = %s, want it to carry %s", after.Outbounds[0], tc.wantValue)
  183. }
  184. capture.msgs = nil
  185. if err := xray.ValidateOutboundConfig(after.Outbounds[0]); err != nil {
  186. t.Fatalf("xray-core refused the rewritten outbound: %v", err)
  187. }
  188. if capture.has("sockopt.domainStrategy") {
  189. t.Fatalf("rewritten outbound still warns on load: %v", capture.msgs)
  190. }
  191. })
  192. }
  193. }
  194. func TestRewriteFreedomDomainStrategyInvalidJSON(t *testing.T) {
  195. _, changed, err := rewriteFreedomDomainStrategy("{not json")
  196. if err == nil {
  197. t.Fatal("expected an error for invalid JSON")
  198. }
  199. if changed {
  200. t.Fatal("invalid JSON must not report a change")
  201. }
  202. }
  203. func TestMigrateFreedomDomainStrategyRewritesStoredTemplate(t *testing.T) {
  204. t.Setenv("XUI_DB_FOLDER", t.TempDir())
  205. // A CGO_ENABLED=0 build links a stubbed driver, so this test needs the same
  206. // C compiler the rest of the package's DB tests do.
  207. if err := InitDB(config.GetDBPath()); err != nil {
  208. if strings.Contains(err.Error(), "CGO_ENABLED=0") {
  209. t.Skipf("sqlite needs cgo: %v", err)
  210. }
  211. t.Fatalf("init db: %v", err)
  212. }
  213. t.Cleanup(func() { _ = CloseDB() })
  214. legacy := `{"outbounds":[{"protocol":"freedom","tag":"direct","settings":{"domainStrategy":"UseIPv4"}}]}`
  215. seedTemplate(t, legacy)
  216. if err := db.Where("seeder_name = ?", "FreedomDomainStrategyFix").
  217. Delete(&model.HistoryOfSeeders{}).Error; err != nil {
  218. t.Fatalf("clear seeder history: %v", err)
  219. }
  220. if err := migrateFreedomDomainStrategy(); err != nil {
  221. t.Fatalf("migrate: %v", err)
  222. }
  223. got := storedTemplate(t)
  224. var cfg struct {
  225. Outbounds []map[string]any `json:"outbounds"`
  226. }
  227. if err := json.Unmarshal([]byte(got), &cfg); err != nil {
  228. t.Fatalf("stored template is not JSON: %v", err)
  229. }
  230. if len(cfg.Outbounds) != 1 {
  231. t.Fatalf("stored outbounds = %d, want 1", len(cfg.Outbounds))
  232. }
  233. outbound := cfg.Outbounds[0]
  234. if _, present := outbound["targetStrategy"]; present {
  235. t.Errorf("stored outbound kept the root targetStrategy: %s", got)
  236. }
  237. settings, _ := outbound["settings"].(map[string]any)
  238. if _, present := settings["domainStrategy"]; present {
  239. t.Errorf("stored outbound kept the deprecated settings key: %s", got)
  240. }
  241. stream, _ := outbound["streamSettings"].(map[string]any)
  242. sockopt, _ := stream["sockopt"].(map[string]any)
  243. if sockopt["domainStrategy"] != "UseIPv4" {
  244. t.Errorf("stored sockopt strategy = %v, want UseIPv4", sockopt["domainStrategy"])
  245. }
  246. // The history gate is what keeps a hand-edited template from being rewritten
  247. // again on every restart, so run the real seeder list over a fresh legacy one.
  248. seedTemplate(t, legacy)
  249. if err := runSeeders(false); err != nil {
  250. t.Fatalf("runSeeders: %v", err)
  251. }
  252. if got := storedTemplate(t); got != legacy {
  253. t.Errorf("a completed seeder rewrote the template again: %s", got)
  254. }
  255. }
  256. func seedTemplate(t *testing.T, value string) {
  257. t.Helper()
  258. if err := db.Where("key = ?", "xrayTemplateConfig").Delete(&model.Setting{}).Error; err != nil {
  259. t.Fatalf("clear template: %v", err)
  260. }
  261. if err := db.Create(&model.Setting{Key: "xrayTemplateConfig", Value: value}).Error; err != nil {
  262. t.Fatalf("seed template: %v", err)
  263. }
  264. }
  265. func storedTemplate(t *testing.T) string {
  266. t.Helper()
  267. var setting model.Setting
  268. if err := db.Where("key = ?", "xrayTemplateConfig").First(&setting).Error; err != nil {
  269. t.Fatalf("reload template: %v", err)
  270. }
  271. return setting.Value
  272. }