xray_balancer_override_e2e_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. package service
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "net"
  6. "os"
  7. "path/filepath"
  8. "testing"
  9. "time"
  10. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  11. )
  12. // Real-core tests: the bugs they pin live in how RoutingService rebuilds
  13. // balancers, which no fake reproduces faithfully.
  14. type balancerTestConfig struct {
  15. outbounds []string
  16. extraRule bool
  17. }
  18. func (c balancerTestConfig) build(t *testing.T, apiPort int) *xray.Config {
  19. t.Helper()
  20. outbounds := []any{}
  21. for _, tag := range c.outbounds {
  22. protocol := "freedom"
  23. if tag == "blocked" {
  24. protocol = "blackhole"
  25. }
  26. outbounds = append(outbounds, map[string]any{"tag": tag, "protocol": protocol, "settings": map[string]any{}})
  27. }
  28. rules := []any{
  29. map[string]any{"type": "field", "inboundTag": []string{"api"}, "outboundTag": "api"},
  30. map[string]any{"type": "field", "port": "7777", "balancerTag": "b1"},
  31. }
  32. if c.extraRule {
  33. rules = append(rules, map[string]any{"type": "field", "port": "8888", "outboundTag": "blocked"})
  34. }
  35. routing := map[string]any{
  36. "domainStrategy": "AsIs",
  37. "rules": rules,
  38. "balancers": []any{map[string]any{"tag": "b1", "selector": []string{"proxy-"}}},
  39. }
  40. return &xray.Config{
  41. LogConfig: mustJSON(t, map[string]any{"loglevel": "warning"}),
  42. RouterConfig: mustJSON(t, routing),
  43. OutboundConfigs: mustJSON(t, outbounds),
  44. API: mustJSON(t, map[string]any{"tag": "api", "services": []string{"HandlerService", "RoutingService"}}),
  45. InboundConfigs: []xray.InboundConfig{{
  46. Listen: mustJSON(t, "127.0.0.1"),
  47. Port: apiPort,
  48. Protocol: "tunnel",
  49. Settings: mustJSON(t, map[string]any{"rewriteAddress": "127.0.0.1"}),
  50. Tag: "api",
  51. }},
  52. }
  53. }
  54. var allBalancerTestOutbounds = []string{"direct", "blocked", "proxy-a", "proxy-b"}
  55. func mustJSON(t *testing.T, v any) []byte {
  56. t.Helper()
  57. data, err := json.Marshal(v)
  58. if err != nil {
  59. t.Fatal(err)
  60. }
  61. return data
  62. }
  63. func freeLoopbackPort(t *testing.T) int {
  64. t.Helper()
  65. l, err := net.Listen("tcp", "127.0.0.1:0")
  66. if err != nil {
  67. t.Fatal(err)
  68. }
  69. defer l.Close()
  70. return l.Addr().(*net.TCPAddr).Port
  71. }
  72. // startBalancerTestXray runs a real core and installs it as the panel's
  73. // current process, the way RestartXray does.
  74. func startBalancerTestXray(t *testing.T, spec balancerTestConfig) (*xray.Process, int) {
  75. t.Helper()
  76. return startBalancerTestXrayOnPort(t, spec, freeLoopbackPort(t))
  77. }
  78. // startBalancerTestXrayOnPort keeps the API port, as a panel restart does,
  79. // so clients still holding the old core's port reach the new one.
  80. func startBalancerTestXrayOnPort(t *testing.T, spec balancerTestConfig, apiPort int) (*xray.Process, int) {
  81. t.Helper()
  82. bin := os.Getenv("XRAY_E2E_BINARY")
  83. if bin == "" {
  84. t.Skip("set XRAY_E2E_BINARY to an xray built from go.mod's xray-core (go build github.com/xtls/xray-core/main)")
  85. }
  86. binDir := t.TempDir()
  87. if err := os.Symlink(bin, filepath.Join(binDir, xray.GetBinaryName())); err != nil {
  88. t.Fatal(err)
  89. }
  90. t.Setenv("XUI_BIN_FOLDER", binDir)
  91. process := xray.NewTestProcess(spec.build(t, apiPort), filepath.Join(binDir, "config.json"))
  92. if err := process.Start(); err != nil {
  93. t.Fatalf("start xray: %v", err)
  94. }
  95. t.Cleanup(func() { _ = process.Stop() })
  96. previousProcess, previousResult := xrayState.snapshot()
  97. xrayState.replace(process)
  98. t.Cleanup(func() {
  99. xrayState.mu.Lock()
  100. xrayState.process = previousProcess
  101. xrayState.result = previousResult
  102. xrayState.mu.Unlock()
  103. })
  104. deadline := time.Now().Add(15 * time.Second)
  105. for {
  106. if _, err := liveBalancerInfo(process, "b1"); err == nil {
  107. return process, apiPort
  108. } else if time.Now().After(deadline) {
  109. t.Fatalf("xray api never became ready: %v", err)
  110. }
  111. time.Sleep(100 * time.Millisecond)
  112. }
  113. }
  114. func liveBalancerInfo(process *xray.Process, tag string) (*xray.BalancerInfo, error) {
  115. api := xray.XrayAPI{}
  116. if err := api.Init(process.GetAPIPort()); err != nil {
  117. return nil, err
  118. }
  119. defer api.Close()
  120. return api.GetBalancerInfo(tag)
  121. }
  122. func liveBalancerOverride(t *testing.T, process *xray.Process, tag string) string {
  123. t.Helper()
  124. info, err := liveBalancerInfo(process, tag)
  125. if err != nil {
  126. t.Fatalf("GetBalancerInfo(%s): %v", tag, err)
  127. }
  128. return info.Override
  129. }
  130. // routeFor is the outbound the running core picks for a TCP connection to port.
  131. func routeFor(process *xray.Process, port int) (string, error) {
  132. api := xray.XrayAPI{}
  133. if err := api.Init(process.GetAPIPort()); err != nil {
  134. return "", err
  135. }
  136. defer api.Close()
  137. result, err := api.TestRoute(xray.RouteTestRequest{IP: "203.0.113.1", Port: port})
  138. if err != nil {
  139. return "", err
  140. }
  141. return result.OutboundTag, nil
  142. }
  143. func liveRoute(t *testing.T, process *xray.Process, port int) string {
  144. t.Helper()
  145. route, err := routeFor(process, port)
  146. if err != nil {
  147. t.Fatalf("TestRoute(port %d): %v", port, err)
  148. }
  149. return route
  150. }
  151. // saveDuringTest hot-applies cfg the way RestartXray does, holding the package lock.
  152. func saveDuringTest(t *testing.T, process *xray.Process, cfg *xray.Config) {
  153. t.Helper()
  154. lock.Lock()
  155. defer lock.Unlock()
  156. if !(&XrayService{}).tryHotApply(process, cfg) {
  157. t.Fatal("tryHotApply = false, want the change applied live")
  158. }
  159. }
  160. // Every routing save is hot-applied with AddRule(shouldAppend=false), which
  161. // rebuilds all balancers and used to drop the operator's override silently.
  162. func TestBalancerOverrideSurvivesRoutingHotApply(t *testing.T) {
  163. process, apiPort := startBalancerTestXray(t, balancerTestConfig{outbounds: allBalancerTestOutbounds})
  164. svc := &XrayService{}
  165. if err := svc.OverrideBalancer("b1", "proxy-b"); err != nil {
  166. t.Fatalf("OverrideBalancer: %v", err)
  167. }
  168. newCfg := balancerTestConfig{outbounds: allBalancerTestOutbounds, extraRule: true}.build(t, apiPort)
  169. if !svc.tryHotApply(process, newCfg) {
  170. t.Fatal("tryHotApply = false, want the routing change applied live")
  171. }
  172. if got := liveBalancerOverride(t, process, "b1"); got != "proxy-b" {
  173. t.Fatalf("override after routing hot apply = %q, want proxy-b", got)
  174. }
  175. }
  176. // An override naming a removed outbound makes the core close every connection
  177. // the balancer routes ("non existing outTag"), so removing the target clears it.
  178. func TestBalancerOverrideClearedWhenTargetOutboundRemoved(t *testing.T) {
  179. process, apiPort := startBalancerTestXray(t, balancerTestConfig{outbounds: allBalancerTestOutbounds})
  180. svc := &XrayService{}
  181. if err := svc.OverrideBalancer("b1", "proxy-b"); err != nil {
  182. t.Fatalf("OverrideBalancer: %v", err)
  183. }
  184. newCfg := balancerTestConfig{outbounds: []string{"direct", "blocked", "proxy-a"}}.build(t, apiPort)
  185. if !svc.tryHotApply(process, newCfg) {
  186. t.Fatal("tryHotApply = false, want the outbound removal applied live")
  187. }
  188. if got := liveBalancerOverride(t, process, "b1"); got != "" {
  189. t.Fatalf("override after its target was removed = %q, want cleared", got)
  190. }
  191. }
  192. // Testing a node through an outbound swaps the live routing in and back out;
  193. // the override must hold while the probe runs and after the restore.
  194. func TestBalancerOverrideSurvivesNodeOutboundBridge(t *testing.T) {
  195. process, _ := startBalancerTestXray(t, balancerTestConfig{outbounds: allBalancerTestOutbounds})
  196. if err := (&XrayService{}).OverrideBalancer("b1", "proxy-b"); err != nil {
  197. t.Fatalf("OverrideBalancer: %v", err)
  198. }
  199. var duringProbe string
  200. (&NodeService{}).withOutboundBridge(1, "direct", func(proxyURL string) {
  201. if proxyURL == "" {
  202. t.Fatal("bridge was not built, so the routing swap under test never ran")
  203. }
  204. duringProbe = liveBalancerOverride(t, process, "b1")
  205. })
  206. if duringProbe != "proxy-b" {
  207. t.Errorf("override while the node bridge was up = %q, want proxy-b", duringProbe)
  208. }
  209. if got := liveBalancerOverride(t, process, "b1"); got != "proxy-b" {
  210. t.Errorf("override after the node bridge was torn down = %q, want proxy-b", got)
  211. }
  212. }
  213. // A node probe can run for seconds; an override the operator sets meanwhile,
  214. // onto an outbound saved meanwhile, must outlive the bridge's teardown.
  215. func TestBalancerOverrideSetDuringNodeBridgeSurvivesTeardown(t *testing.T) {
  216. process, apiPort := startBalancerTestXray(t, balancerTestConfig{outbounds: allBalancerTestOutbounds})
  217. withProxyC := balancerTestConfig{outbounds: []string{"direct", "blocked", "proxy-a", "proxy-b", "proxy-c"}}.build(t, apiPort)
  218. (&NodeService{}).withOutboundBridge(1, "direct", func(proxyURL string) {
  219. if proxyURL == "" {
  220. t.Fatal("bridge was not built, so the teardown under test never runs")
  221. }
  222. saveDuringTest(t, process, withProxyC)
  223. if err := (&XrayService{}).OverrideBalancer("b1", "proxy-c"); err != nil {
  224. t.Fatalf("OverrideBalancer: %v", err)
  225. }
  226. })
  227. if got := liveBalancerOverride(t, process, "b1"); got != "proxy-c" {
  228. t.Fatalf("override after the node bridge was torn down = %q, want proxy-c", got)
  229. }
  230. }
  231. // The teardown used to restore the routing captured before the probe, silently
  232. // reverting a rule change saved while the probe ran.
  233. func TestNodeBridgeTeardownKeepsRoutingSavedDuringProbe(t *testing.T) {
  234. process, apiPort := startBalancerTestXray(t, balancerTestConfig{outbounds: allBalancerTestOutbounds})
  235. withPort8888Rule := balancerTestConfig{outbounds: allBalancerTestOutbounds, extraRule: true}.build(t, apiPort)
  236. (&NodeService{}).withOutboundBridge(1, "direct", func(proxyURL string) {
  237. if proxyURL == "" {
  238. t.Fatal("bridge was not built, so the teardown under test never runs")
  239. }
  240. saveDuringTest(t, process, withPort8888Rule)
  241. })
  242. if got := liveRoute(t, process, 8888); got != "blocked" {
  243. t.Fatalf("route for port 8888 after the node bridge was torn down = %q, want blocked", got)
  244. }
  245. }
  246. // Mid-apply, the core already runs the saved rules but its config snapshot is
  247. // stale; a bridge opened then ran every connection on the stale rules.
  248. func TestNodeBridgeOpenedDuringConfigApplyRoutesByTheAppliedConfig(t *testing.T) {
  249. process, apiPort := startBalancerTestXray(t, balancerTestConfig{outbounds: allBalancerTestOutbounds})
  250. withPort8888Rule := balancerTestConfig{outbounds: allBalancerTestOutbounds, extraRule: true}.build(t, apiPort)
  251. api := xray.XrayAPI{}
  252. if err := api.Init(apiPort); err != nil {
  253. t.Fatal(err)
  254. }
  255. defer api.Close()
  256. type probeView struct {
  257. proxyURL, route string
  258. err error
  259. }
  260. seen, torndown := make(chan probeView, 1), make(chan struct{})
  261. lock.Lock()
  262. if err := api.ApplyRoutingConfig(withPort8888Rule.RouterConfig); err != nil {
  263. lock.Unlock()
  264. t.Fatalf("ApplyRoutingConfig: %v", err)
  265. }
  266. go func() {
  267. defer close(torndown)
  268. (&NodeService{}).withOutboundBridge(1, "direct", func(proxyURL string) {
  269. route, err := routeFor(process, 8888)
  270. seen <- probeView{proxyURL, route, err}
  271. })
  272. }()
  273. time.Sleep(300 * time.Millisecond) // room for an unserialized bridge to open mid-apply
  274. process.SetConfig(withPort8888Rule)
  275. lock.Unlock()
  276. view := <-seen
  277. <-torndown
  278. if view.proxyURL == "" {
  279. t.Fatal("bridge was not built, so the routing under test never ran")
  280. }
  281. if view.err != nil {
  282. t.Fatalf("TestRoute while the bridge was up: %v", view.err)
  283. }
  284. if view.route != "blocked" {
  285. t.Fatalf("route for port 8888 while the node bridge was up = %q, want blocked", view.route)
  286. }
  287. }
  288. // Mid-apply, the core already runs the saved rules but its config snapshot is
  289. // stale; a teardown landing then restored the stale rules over the save.
  290. func TestNodeBridgeTeardownDuringConfigApplyRestoresTheAppliedRouting(t *testing.T) {
  291. process, apiPort := startBalancerTestXray(t, balancerTestConfig{outbounds: allBalancerTestOutbounds})
  292. withPort8888Rule := balancerTestConfig{outbounds: allBalancerTestOutbounds, extraRule: true}.build(t, apiPort)
  293. api := xray.XrayAPI{}
  294. if err := api.Init(apiPort); err != nil {
  295. t.Fatal(err)
  296. }
  297. defer api.Close()
  298. probing, finishProbe, torndown := make(chan string), make(chan struct{}), make(chan struct{})
  299. go func() {
  300. defer close(torndown)
  301. (&NodeService{}).withOutboundBridge(1, "direct", func(proxyURL string) {
  302. probing <- proxyURL
  303. <-finishProbe
  304. })
  305. }()
  306. if proxyURL := <-probing; proxyURL == "" {
  307. close(finishProbe)
  308. t.Fatal("bridge was not built, so the teardown under test never runs")
  309. }
  310. lock.Lock()
  311. if err := api.ApplyRoutingConfig(withPort8888Rule.RouterConfig); err != nil {
  312. lock.Unlock()
  313. close(finishProbe)
  314. t.Fatalf("ApplyRoutingConfig: %v", err)
  315. }
  316. close(finishProbe)
  317. time.Sleep(300 * time.Millisecond) // room for an unserialized teardown to land mid-apply
  318. process.SetConfig(withPort8888Rule)
  319. lock.Unlock()
  320. <-torndown
  321. if got := liveRoute(t, process, 8888); got != "blocked" {
  322. t.Fatalf("route for port 8888 after the node bridge was torn down = %q, want blocked", got)
  323. }
  324. }
  325. // A core restarted during the probe never had the bridge; the teardown must not
  326. // push the old core's routing or overrides into it through the shared API port.
  327. func TestNodeBridgeTeardownLeavesRestartedCoreAlone(t *testing.T) {
  328. first, apiPort := startBalancerTestXray(t, balancerTestConfig{outbounds: allBalancerTestOutbounds})
  329. if err := (&XrayService{}).OverrideBalancer("b1", "proxy-b"); err != nil {
  330. t.Fatalf("OverrideBalancer: %v", err)
  331. }
  332. var restarted *xray.Process
  333. (&NodeService{}).withOutboundBridge(1, "direct", func(proxyURL string) {
  334. if proxyURL == "" {
  335. t.Fatal("bridge was not built, so the teardown under test never runs")
  336. }
  337. lock.Lock()
  338. defer lock.Unlock()
  339. _ = first.Stop()
  340. restarted, _ = startBalancerTestXrayOnPort(t, balancerTestConfig{outbounds: allBalancerTestOutbounds, extraRule: true}, apiPort)
  341. })
  342. if got := liveRoute(t, restarted, 8888); got != "blocked" {
  343. t.Errorf("route for port 8888 on the restarted core = %q, want blocked from its own config", got)
  344. }
  345. if got := liveBalancerOverride(t, restarted, "b1"); got != "" {
  346. t.Errorf("override on the restarted core = %q, want none", got)
  347. }
  348. }
  349. // The core accepts any string as a target and then closes every connection the
  350. // balancer routes, so the panel must refuse a target the running config lacks.
  351. func TestOverrideBalancerRejectsUnknownTarget(t *testing.T) {
  352. process, _ := startBalancerTestXray(t, balancerTestConfig{outbounds: allBalancerTestOutbounds})
  353. err := (&XrayService{}).OverrideBalancer("b1", "no-such-outbound")
  354. if !errors.Is(err, errOverrideTargetUnknown) {
  355. t.Fatalf("OverrideBalancer(unknown target) error = %v, want errOverrideTargetUnknown", err)
  356. }
  357. if got := liveBalancerOverride(t, process, "b1"); got != "" {
  358. t.Fatalf("core override after a refused target = %q, want none", got)
  359. }
  360. }
  361. // Mid-apply, the core has already dropped an outbound its config snapshot still
  362. // lists; an override landing then pinned the balancer to that missing tag.
  363. func TestOverrideBalancerDuringConfigApplyChecksTheAppliedConfig(t *testing.T) {
  364. process, apiPort := startBalancerTestXray(t, balancerTestConfig{outbounds: allBalancerTestOutbounds})
  365. withoutProxyB := balancerTestConfig{outbounds: []string{"direct", "blocked", "proxy-a"}}.build(t, apiPort)
  366. api := xray.XrayAPI{}
  367. if err := api.Init(apiPort); err != nil {
  368. t.Fatal(err)
  369. }
  370. defer api.Close()
  371. lock.Lock()
  372. if err := api.DelOutbound("proxy-b"); err != nil {
  373. lock.Unlock()
  374. t.Fatalf("DelOutbound: %v", err)
  375. }
  376. result := make(chan error, 1)
  377. go func() { result <- (&XrayService{}).OverrideBalancer("b1", "proxy-b") }()
  378. time.Sleep(300 * time.Millisecond) // room for an unserialized override to land mid-apply
  379. process.SetConfig(withoutProxyB)
  380. lock.Unlock()
  381. if err := <-result; !errors.Is(err, errOverrideTargetUnknown) {
  382. t.Errorf("OverrideBalancer(removed outbound) error = %v, want errOverrideTargetUnknown", err)
  383. }
  384. if got := liveBalancerOverride(t, process, "b1"); got != "" {
  385. t.Errorf("core override = %q, want none: proxy-b is gone from the core", got)
  386. }
  387. }
  388. // A restart is documented to clear overrides; a routing save on the new core
  389. // must not resurrect one set on the previous process.
  390. func TestBalancerOverrideNotCarriedAcrossRestart(t *testing.T) {
  391. _, _ = startBalancerTestXray(t, balancerTestConfig{outbounds: allBalancerTestOutbounds})
  392. if err := (&XrayService{}).OverrideBalancer("b1", "proxy-b"); err != nil {
  393. t.Fatalf("OverrideBalancer: %v", err)
  394. }
  395. restarted, apiPort := startBalancerTestXray(t, balancerTestConfig{outbounds: allBalancerTestOutbounds})
  396. newCfg := balancerTestConfig{outbounds: allBalancerTestOutbounds, extraRule: true}.build(t, apiPort)
  397. if !(&XrayService{}).tryHotApply(restarted, newCfg) {
  398. t.Fatal("tryHotApply = false, want the routing change applied live")
  399. }
  400. if got := liveBalancerOverride(t, restarted, "b1"); got != "" {
  401. t.Fatalf("override on the restarted core = %q, want none", got)
  402. }
  403. }