|
@@ -0,0 +1,454 @@
|
|
|
|
|
+package service
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "encoding/json"
|
|
|
|
|
+ "errors"
|
|
|
|
|
+ "net"
|
|
|
|
|
+ "os"
|
|
|
|
|
+ "path/filepath"
|
|
|
|
|
+ "testing"
|
|
|
|
|
+ "time"
|
|
|
|
|
+
|
|
|
|
|
+ "github.com/mhsanaei/3x-ui/v3/internal/xray"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+// Real-core tests: the bugs they pin live in how RoutingService rebuilds
|
|
|
|
|
+// balancers, which no fake reproduces faithfully.
|
|
|
|
|
+
|
|
|
|
|
+type balancerTestConfig struct {
|
|
|
|
|
+ outbounds []string
|
|
|
|
|
+ extraRule bool
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (c balancerTestConfig) build(t *testing.T, apiPort int) *xray.Config {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+ outbounds := []any{}
|
|
|
|
|
+ for _, tag := range c.outbounds {
|
|
|
|
|
+ protocol := "freedom"
|
|
|
|
|
+ if tag == "blocked" {
|
|
|
|
|
+ protocol = "blackhole"
|
|
|
|
|
+ }
|
|
|
|
|
+ outbounds = append(outbounds, map[string]any{"tag": tag, "protocol": protocol, "settings": map[string]any{}})
|
|
|
|
|
+ }
|
|
|
|
|
+ rules := []any{
|
|
|
|
|
+ map[string]any{"type": "field", "inboundTag": []string{"api"}, "outboundTag": "api"},
|
|
|
|
|
+ map[string]any{"type": "field", "port": "7777", "balancerTag": "b1"},
|
|
|
|
|
+ }
|
|
|
|
|
+ if c.extraRule {
|
|
|
|
|
+ rules = append(rules, map[string]any{"type": "field", "port": "8888", "outboundTag": "blocked"})
|
|
|
|
|
+ }
|
|
|
|
|
+ routing := map[string]any{
|
|
|
|
|
+ "domainStrategy": "AsIs",
|
|
|
|
|
+ "rules": rules,
|
|
|
|
|
+ "balancers": []any{map[string]any{"tag": "b1", "selector": []string{"proxy-"}}},
|
|
|
|
|
+ }
|
|
|
|
|
+ return &xray.Config{
|
|
|
|
|
+ LogConfig: mustJSON(t, map[string]any{"loglevel": "warning"}),
|
|
|
|
|
+ RouterConfig: mustJSON(t, routing),
|
|
|
|
|
+ OutboundConfigs: mustJSON(t, outbounds),
|
|
|
|
|
+ API: mustJSON(t, map[string]any{"tag": "api", "services": []string{"HandlerService", "RoutingService"}}),
|
|
|
|
|
+ InboundConfigs: []xray.InboundConfig{{
|
|
|
|
|
+ Listen: mustJSON(t, "127.0.0.1"),
|
|
|
|
|
+ Port: apiPort,
|
|
|
|
|
+ Protocol: "tunnel",
|
|
|
|
|
+ Settings: mustJSON(t, map[string]any{"rewriteAddress": "127.0.0.1"}),
|
|
|
|
|
+ Tag: "api",
|
|
|
|
|
+ }},
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+var allBalancerTestOutbounds = []string{"direct", "blocked", "proxy-a", "proxy-b"}
|
|
|
|
|
+
|
|
|
|
|
+func mustJSON(t *testing.T, v any) []byte {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+ data, err := json.Marshal(v)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ return data
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func freeLoopbackPort(t *testing.T) int {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+ l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ defer l.Close()
|
|
|
|
|
+ return l.Addr().(*net.TCPAddr).Port
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// startBalancerTestXray runs a real core and installs it as the panel's
|
|
|
|
|
+// current process, the way RestartXray does.
|
|
|
|
|
+func startBalancerTestXray(t *testing.T, spec balancerTestConfig) (*xray.Process, int) {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+ return startBalancerTestXrayOnPort(t, spec, freeLoopbackPort(t))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// startBalancerTestXrayOnPort keeps the API port, as a panel restart does,
|
|
|
|
|
+// so clients still holding the old core's port reach the new one.
|
|
|
|
|
+func startBalancerTestXrayOnPort(t *testing.T, spec balancerTestConfig, apiPort int) (*xray.Process, int) {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+ bin := os.Getenv("XRAY_E2E_BINARY")
|
|
|
|
|
+ if bin == "" {
|
|
|
|
|
+ t.Skip("set XRAY_E2E_BINARY to an xray built from go.mod's xray-core (go build github.com/xtls/xray-core/main)")
|
|
|
|
|
+ }
|
|
|
|
|
+ binDir := t.TempDir()
|
|
|
|
|
+ if err := os.Symlink(bin, filepath.Join(binDir, xray.GetBinaryName())); err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ t.Setenv("XUI_BIN_FOLDER", binDir)
|
|
|
|
|
+
|
|
|
|
|
+ process := xray.NewTestProcess(spec.build(t, apiPort), filepath.Join(binDir, "config.json"))
|
|
|
|
|
+ if err := process.Start(); err != nil {
|
|
|
|
|
+ t.Fatalf("start xray: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ t.Cleanup(func() { _ = process.Stop() })
|
|
|
|
|
+
|
|
|
|
|
+ previousProcess, previousResult := xrayState.snapshot()
|
|
|
|
|
+ xrayState.replace(process)
|
|
|
|
|
+ t.Cleanup(func() {
|
|
|
|
|
+ xrayState.mu.Lock()
|
|
|
|
|
+ xrayState.process = previousProcess
|
|
|
|
|
+ xrayState.result = previousResult
|
|
|
|
|
+ xrayState.mu.Unlock()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ deadline := time.Now().Add(15 * time.Second)
|
|
|
|
|
+ for {
|
|
|
|
|
+ if _, err := liveBalancerInfo(process, "b1"); err == nil {
|
|
|
|
|
+ return process, apiPort
|
|
|
|
|
+ } else if time.Now().After(deadline) {
|
|
|
|
|
+ t.Fatalf("xray api never became ready: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ time.Sleep(100 * time.Millisecond)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func liveBalancerInfo(process *xray.Process, tag string) (*xray.BalancerInfo, error) {
|
|
|
|
|
+ api := xray.XrayAPI{}
|
|
|
|
|
+ if err := api.Init(process.GetAPIPort()); err != nil {
|
|
|
|
|
+ return nil, err
|
|
|
|
|
+ }
|
|
|
|
|
+ defer api.Close()
|
|
|
|
|
+ return api.GetBalancerInfo(tag)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func liveBalancerOverride(t *testing.T, process *xray.Process, tag string) string {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+ info, err := liveBalancerInfo(process, tag)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("GetBalancerInfo(%s): %v", tag, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ return info.Override
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// routeFor is the outbound the running core picks for a TCP connection to port.
|
|
|
|
|
+func routeFor(process *xray.Process, port int) (string, error) {
|
|
|
|
|
+ api := xray.XrayAPI{}
|
|
|
|
|
+ if err := api.Init(process.GetAPIPort()); err != nil {
|
|
|
|
|
+ return "", err
|
|
|
|
|
+ }
|
|
|
|
|
+ defer api.Close()
|
|
|
|
|
+ result, err := api.TestRoute(xray.RouteTestRequest{IP: "203.0.113.1", Port: port})
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return "", err
|
|
|
|
|
+ }
|
|
|
|
|
+ return result.OutboundTag, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func liveRoute(t *testing.T, process *xray.Process, port int) string {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+ route, err := routeFor(process, port)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ t.Fatalf("TestRoute(port %d): %v", port, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ return route
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// saveDuringTest hot-applies cfg the way RestartXray does, holding the package lock.
|
|
|
|
|
+func saveDuringTest(t *testing.T, process *xray.Process, cfg *xray.Config) {
|
|
|
|
|
+ t.Helper()
|
|
|
|
|
+ lock.Lock()
|
|
|
|
|
+ defer lock.Unlock()
|
|
|
|
|
+ if !(&XrayService{}).tryHotApply(process, cfg) {
|
|
|
|
|
+ t.Fatal("tryHotApply = false, want the change applied live")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Every routing save is hot-applied with AddRule(shouldAppend=false), which
|
|
|
|
|
+// rebuilds all balancers and used to drop the operator's override silently.
|
|
|
|
|
+func TestBalancerOverrideSurvivesRoutingHotApply(t *testing.T) {
|
|
|
|
|
+ process, apiPort := startBalancerTestXray(t, balancerTestConfig{outbounds: allBalancerTestOutbounds})
|
|
|
|
|
+ svc := &XrayService{}
|
|
|
|
|
+ if err := svc.OverrideBalancer("b1", "proxy-b"); err != nil {
|
|
|
|
|
+ t.Fatalf("OverrideBalancer: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ newCfg := balancerTestConfig{outbounds: allBalancerTestOutbounds, extraRule: true}.build(t, apiPort)
|
|
|
|
|
+ if !svc.tryHotApply(process, newCfg) {
|
|
|
|
|
+ t.Fatal("tryHotApply = false, want the routing change applied live")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if got := liveBalancerOverride(t, process, "b1"); got != "proxy-b" {
|
|
|
|
|
+ t.Fatalf("override after routing hot apply = %q, want proxy-b", got)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// An override naming a removed outbound makes the core close every connection
|
|
|
|
|
+// the balancer routes ("non existing outTag"), so removing the target clears it.
|
|
|
|
|
+func TestBalancerOverrideClearedWhenTargetOutboundRemoved(t *testing.T) {
|
|
|
|
|
+ process, apiPort := startBalancerTestXray(t, balancerTestConfig{outbounds: allBalancerTestOutbounds})
|
|
|
|
|
+ svc := &XrayService{}
|
|
|
|
|
+ if err := svc.OverrideBalancer("b1", "proxy-b"); err != nil {
|
|
|
|
|
+ t.Fatalf("OverrideBalancer: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ newCfg := balancerTestConfig{outbounds: []string{"direct", "blocked", "proxy-a"}}.build(t, apiPort)
|
|
|
|
|
+ if !svc.tryHotApply(process, newCfg) {
|
|
|
|
|
+ t.Fatal("tryHotApply = false, want the outbound removal applied live")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if got := liveBalancerOverride(t, process, "b1"); got != "" {
|
|
|
|
|
+ t.Fatalf("override after its target was removed = %q, want cleared", got)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Testing a node through an outbound swaps the live routing in and back out;
|
|
|
|
|
+// the override must hold while the probe runs and after the restore.
|
|
|
|
|
+func TestBalancerOverrideSurvivesNodeOutboundBridge(t *testing.T) {
|
|
|
|
|
+ process, _ := startBalancerTestXray(t, balancerTestConfig{outbounds: allBalancerTestOutbounds})
|
|
|
|
|
+ if err := (&XrayService{}).OverrideBalancer("b1", "proxy-b"); err != nil {
|
|
|
|
|
+ t.Fatalf("OverrideBalancer: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var duringProbe string
|
|
|
|
|
+ (&NodeService{}).withOutboundBridge(1, "direct", func(proxyURL string) {
|
|
|
|
|
+ if proxyURL == "" {
|
|
|
|
|
+ t.Fatal("bridge was not built, so the routing swap under test never ran")
|
|
|
|
|
+ }
|
|
|
|
|
+ duringProbe = liveBalancerOverride(t, process, "b1")
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ if duringProbe != "proxy-b" {
|
|
|
|
|
+ t.Errorf("override while the node bridge was up = %q, want proxy-b", duringProbe)
|
|
|
|
|
+ }
|
|
|
|
|
+ if got := liveBalancerOverride(t, process, "b1"); got != "proxy-b" {
|
|
|
|
|
+ t.Errorf("override after the node bridge was torn down = %q, want proxy-b", got)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// A node probe can run for seconds; an override the operator sets meanwhile,
|
|
|
|
|
+// onto an outbound saved meanwhile, must outlive the bridge's teardown.
|
|
|
|
|
+func TestBalancerOverrideSetDuringNodeBridgeSurvivesTeardown(t *testing.T) {
|
|
|
|
|
+ process, apiPort := startBalancerTestXray(t, balancerTestConfig{outbounds: allBalancerTestOutbounds})
|
|
|
|
|
+ withProxyC := balancerTestConfig{outbounds: []string{"direct", "blocked", "proxy-a", "proxy-b", "proxy-c"}}.build(t, apiPort)
|
|
|
|
|
+
|
|
|
|
|
+ (&NodeService{}).withOutboundBridge(1, "direct", func(proxyURL string) {
|
|
|
|
|
+ if proxyURL == "" {
|
|
|
|
|
+ t.Fatal("bridge was not built, so the teardown under test never runs")
|
|
|
|
|
+ }
|
|
|
|
|
+ saveDuringTest(t, process, withProxyC)
|
|
|
|
|
+ if err := (&XrayService{}).OverrideBalancer("b1", "proxy-c"); err != nil {
|
|
|
|
|
+ t.Fatalf("OverrideBalancer: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ if got := liveBalancerOverride(t, process, "b1"); got != "proxy-c" {
|
|
|
|
|
+ t.Fatalf("override after the node bridge was torn down = %q, want proxy-c", got)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// The teardown used to restore the routing captured before the probe, silently
|
|
|
|
|
+// reverting a rule change saved while the probe ran.
|
|
|
|
|
+func TestNodeBridgeTeardownKeepsRoutingSavedDuringProbe(t *testing.T) {
|
|
|
|
|
+ process, apiPort := startBalancerTestXray(t, balancerTestConfig{outbounds: allBalancerTestOutbounds})
|
|
|
|
|
+ withPort8888Rule := balancerTestConfig{outbounds: allBalancerTestOutbounds, extraRule: true}.build(t, apiPort)
|
|
|
|
|
+
|
|
|
|
|
+ (&NodeService{}).withOutboundBridge(1, "direct", func(proxyURL string) {
|
|
|
|
|
+ if proxyURL == "" {
|
|
|
|
|
+ t.Fatal("bridge was not built, so the teardown under test never runs")
|
|
|
|
|
+ }
|
|
|
|
|
+ saveDuringTest(t, process, withPort8888Rule)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ if got := liveRoute(t, process, 8888); got != "blocked" {
|
|
|
|
|
+ t.Fatalf("route for port 8888 after the node bridge was torn down = %q, want blocked", got)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Mid-apply, the core already runs the saved rules but its config snapshot is
|
|
|
|
|
+// stale; a bridge opened then ran every connection on the stale rules.
|
|
|
|
|
+func TestNodeBridgeOpenedDuringConfigApplyRoutesByTheAppliedConfig(t *testing.T) {
|
|
|
|
|
+ process, apiPort := startBalancerTestXray(t, balancerTestConfig{outbounds: allBalancerTestOutbounds})
|
|
|
|
|
+ withPort8888Rule := balancerTestConfig{outbounds: allBalancerTestOutbounds, extraRule: true}.build(t, apiPort)
|
|
|
|
|
+ api := xray.XrayAPI{}
|
|
|
|
|
+ if err := api.Init(apiPort); err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ defer api.Close()
|
|
|
|
|
+
|
|
|
|
|
+ type probeView struct {
|
|
|
|
|
+ proxyURL, route string
|
|
|
|
|
+ err error
|
|
|
|
|
+ }
|
|
|
|
|
+ seen, torndown := make(chan probeView, 1), make(chan struct{})
|
|
|
|
|
+ lock.Lock()
|
|
|
|
|
+ if err := api.ApplyRoutingConfig(withPort8888Rule.RouterConfig); err != nil {
|
|
|
|
|
+ lock.Unlock()
|
|
|
|
|
+ t.Fatalf("ApplyRoutingConfig: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ go func() {
|
|
|
|
|
+ defer close(torndown)
|
|
|
|
|
+ (&NodeService{}).withOutboundBridge(1, "direct", func(proxyURL string) {
|
|
|
|
|
+ route, err := routeFor(process, 8888)
|
|
|
|
|
+ seen <- probeView{proxyURL, route, err}
|
|
|
|
|
+ })
|
|
|
|
|
+ }()
|
|
|
|
|
+ time.Sleep(300 * time.Millisecond) // room for an unserialized bridge to open mid-apply
|
|
|
|
|
+ process.SetConfig(withPort8888Rule)
|
|
|
|
|
+ lock.Unlock()
|
|
|
|
|
+ view := <-seen
|
|
|
|
|
+ <-torndown
|
|
|
|
|
+
|
|
|
|
|
+ if view.proxyURL == "" {
|
|
|
|
|
+ t.Fatal("bridge was not built, so the routing under test never ran")
|
|
|
|
|
+ }
|
|
|
|
|
+ if view.err != nil {
|
|
|
|
|
+ t.Fatalf("TestRoute while the bridge was up: %v", view.err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if view.route != "blocked" {
|
|
|
|
|
+ t.Fatalf("route for port 8888 while the node bridge was up = %q, want blocked", view.route)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Mid-apply, the core already runs the saved rules but its config snapshot is
|
|
|
|
|
+// stale; a teardown landing then restored the stale rules over the save.
|
|
|
|
|
+func TestNodeBridgeTeardownDuringConfigApplyRestoresTheAppliedRouting(t *testing.T) {
|
|
|
|
|
+ process, apiPort := startBalancerTestXray(t, balancerTestConfig{outbounds: allBalancerTestOutbounds})
|
|
|
|
|
+ withPort8888Rule := balancerTestConfig{outbounds: allBalancerTestOutbounds, extraRule: true}.build(t, apiPort)
|
|
|
|
|
+ api := xray.XrayAPI{}
|
|
|
|
|
+ if err := api.Init(apiPort); err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ defer api.Close()
|
|
|
|
|
+
|
|
|
|
|
+ probing, finishProbe, torndown := make(chan string), make(chan struct{}), make(chan struct{})
|
|
|
|
|
+ go func() {
|
|
|
|
|
+ defer close(torndown)
|
|
|
|
|
+ (&NodeService{}).withOutboundBridge(1, "direct", func(proxyURL string) {
|
|
|
|
|
+ probing <- proxyURL
|
|
|
|
|
+ <-finishProbe
|
|
|
|
|
+ })
|
|
|
|
|
+ }()
|
|
|
|
|
+ if proxyURL := <-probing; proxyURL == "" {
|
|
|
|
|
+ close(finishProbe)
|
|
|
|
|
+ t.Fatal("bridge was not built, so the teardown under test never runs")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ lock.Lock()
|
|
|
|
|
+ if err := api.ApplyRoutingConfig(withPort8888Rule.RouterConfig); err != nil {
|
|
|
|
|
+ lock.Unlock()
|
|
|
|
|
+ close(finishProbe)
|
|
|
|
|
+ t.Fatalf("ApplyRoutingConfig: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ close(finishProbe)
|
|
|
|
|
+ time.Sleep(300 * time.Millisecond) // room for an unserialized teardown to land mid-apply
|
|
|
|
|
+ process.SetConfig(withPort8888Rule)
|
|
|
|
|
+ lock.Unlock()
|
|
|
|
|
+ <-torndown
|
|
|
|
|
+
|
|
|
|
|
+ if got := liveRoute(t, process, 8888); got != "blocked" {
|
|
|
|
|
+ t.Fatalf("route for port 8888 after the node bridge was torn down = %q, want blocked", got)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// A core restarted during the probe never had the bridge; the teardown must not
|
|
|
|
|
+// push the old core's routing or overrides into it through the shared API port.
|
|
|
|
|
+func TestNodeBridgeTeardownLeavesRestartedCoreAlone(t *testing.T) {
|
|
|
|
|
+ first, apiPort := startBalancerTestXray(t, balancerTestConfig{outbounds: allBalancerTestOutbounds})
|
|
|
|
|
+ if err := (&XrayService{}).OverrideBalancer("b1", "proxy-b"); err != nil {
|
|
|
|
|
+ t.Fatalf("OverrideBalancer: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var restarted *xray.Process
|
|
|
|
|
+ (&NodeService{}).withOutboundBridge(1, "direct", func(proxyURL string) {
|
|
|
|
|
+ if proxyURL == "" {
|
|
|
|
|
+ t.Fatal("bridge was not built, so the teardown under test never runs")
|
|
|
|
|
+ }
|
|
|
|
|
+ lock.Lock()
|
|
|
|
|
+ defer lock.Unlock()
|
|
|
|
|
+ _ = first.Stop()
|
|
|
|
|
+ restarted, _ = startBalancerTestXrayOnPort(t, balancerTestConfig{outbounds: allBalancerTestOutbounds, extraRule: true}, apiPort)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ if got := liveRoute(t, restarted, 8888); got != "blocked" {
|
|
|
|
|
+ t.Errorf("route for port 8888 on the restarted core = %q, want blocked from its own config", got)
|
|
|
|
|
+ }
|
|
|
|
|
+ if got := liveBalancerOverride(t, restarted, "b1"); got != "" {
|
|
|
|
|
+ t.Errorf("override on the restarted core = %q, want none", got)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// The core accepts any string as a target and then closes every connection the
|
|
|
|
|
+// balancer routes, so the panel must refuse a target the running config lacks.
|
|
|
|
|
+func TestOverrideBalancerRejectsUnknownTarget(t *testing.T) {
|
|
|
|
|
+ process, _ := startBalancerTestXray(t, balancerTestConfig{outbounds: allBalancerTestOutbounds})
|
|
|
|
|
+
|
|
|
|
|
+ err := (&XrayService{}).OverrideBalancer("b1", "no-such-outbound")
|
|
|
|
|
+ if !errors.Is(err, errOverrideTargetUnknown) {
|
|
|
|
|
+ t.Fatalf("OverrideBalancer(unknown target) error = %v, want errOverrideTargetUnknown", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if got := liveBalancerOverride(t, process, "b1"); got != "" {
|
|
|
|
|
+ t.Fatalf("core override after a refused target = %q, want none", got)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Mid-apply, the core has already dropped an outbound its config snapshot still
|
|
|
|
|
+// lists; an override landing then pinned the balancer to that missing tag.
|
|
|
|
|
+func TestOverrideBalancerDuringConfigApplyChecksTheAppliedConfig(t *testing.T) {
|
|
|
|
|
+ process, apiPort := startBalancerTestXray(t, balancerTestConfig{outbounds: allBalancerTestOutbounds})
|
|
|
|
|
+ withoutProxyB := balancerTestConfig{outbounds: []string{"direct", "blocked", "proxy-a"}}.build(t, apiPort)
|
|
|
|
|
+ api := xray.XrayAPI{}
|
|
|
|
|
+ if err := api.Init(apiPort); err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ defer api.Close()
|
|
|
|
|
+
|
|
|
|
|
+ lock.Lock()
|
|
|
|
|
+ if err := api.DelOutbound("proxy-b"); err != nil {
|
|
|
|
|
+ lock.Unlock()
|
|
|
|
|
+ t.Fatalf("DelOutbound: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ result := make(chan error, 1)
|
|
|
|
|
+ go func() { result <- (&XrayService{}).OverrideBalancer("b1", "proxy-b") }()
|
|
|
|
|
+ time.Sleep(300 * time.Millisecond) // room for an unserialized override to land mid-apply
|
|
|
|
|
+ process.SetConfig(withoutProxyB)
|
|
|
|
|
+ lock.Unlock()
|
|
|
|
|
+
|
|
|
|
|
+ if err := <-result; !errors.Is(err, errOverrideTargetUnknown) {
|
|
|
|
|
+ t.Errorf("OverrideBalancer(removed outbound) error = %v, want errOverrideTargetUnknown", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if got := liveBalancerOverride(t, process, "b1"); got != "" {
|
|
|
|
|
+ t.Errorf("core override = %q, want none: proxy-b is gone from the core", got)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// A restart is documented to clear overrides; a routing save on the new core
|
|
|
|
|
+// must not resurrect one set on the previous process.
|
|
|
|
|
+func TestBalancerOverrideNotCarriedAcrossRestart(t *testing.T) {
|
|
|
|
|
+ _, _ = startBalancerTestXray(t, balancerTestConfig{outbounds: allBalancerTestOutbounds})
|
|
|
|
|
+ if err := (&XrayService{}).OverrideBalancer("b1", "proxy-b"); err != nil {
|
|
|
|
|
+ t.Fatalf("OverrideBalancer: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ restarted, apiPort := startBalancerTestXray(t, balancerTestConfig{outbounds: allBalancerTestOutbounds})
|
|
|
|
|
+ newCfg := balancerTestConfig{outbounds: allBalancerTestOutbounds, extraRule: true}.build(t, apiPort)
|
|
|
|
|
+ if !(&XrayService{}).tryHotApply(restarted, newCfg) {
|
|
|
|
|
+ t.Fatal("tryHotApply = false, want the routing change applied live")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if got := liveBalancerOverride(t, restarted, "b1"); got != "" {
|
|
|
|
|
+ t.Fatalf("override on the restarted core = %q, want none", got)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|