diff --git a/DockerInit.sh b/DockerInit.sh index 18fccad21..3ba18876e 100755 --- a/DockerInit.sh +++ b/DockerInit.sh @@ -31,12 +31,13 @@ if [ -z "$MTG_MULTI_VER" ]; then echo "DockerInit: could not resolve the latest mtg-multi release tag" >&2 exit 1 fi -mkdir -p build/bin +mkdir -p build/bin build/xray-image cd build/bin curl -sfLRO "https://github.com/XTLS/Xray-core/releases/download/v26.10.10/Xray-linux-${ARCH}.zip" unzip "Xray-linux-${ARCH}.zip" rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat -mv xray "xray-linux-${FNAME}" +mv xray "../xray-image/xray-linux-${FNAME}" +ln -s "../xray-image/xray-linux-${FNAME}" "xray-linux-${FNAME}" # mtg-multi (MTProto sidecar) ships prebuilt release binaries for every target # we package, so download and unpack the matching one instead of compiling. case $FNAME in diff --git a/Dockerfile b/Dockerfile index 13d41e9bf..aace5c411 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,7 +46,6 @@ RUN apk add --no-cache --update \ openssl COPY --from=builder /app/build/ /app/ -COPY --from=builder /app/build/bin/xray-* /app/xray-image/ COPY --from=builder /app/DockerEntrypoint.sh /app/ COPY --from=builder /app/x-ui.sh /usr/bin/x-ui COPY --from=builder /app/internal/web/translation /app/internal/web/translation diff --git a/docker_init_test.go b/docker_init_test.go new file mode 100644 index 000000000..f4964dc31 --- /dev/null +++ b/docker_init_test.go @@ -0,0 +1,129 @@ +package main + +import ( + "archive/tar" + "archive/zip" + "bytes" + "compress/gzip" + "io/fs" + "os" + "os/exec" + "path/filepath" + "runtime" + "slices" + "strings" + "testing" +) + +func TestDockerInitPackagesOneXrayBinaryWithRelocatableSymlink(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("DockerInit uses a Unix shell and symlinks") + } + script, err := filepath.Abs("DockerInit.sh") + if err != nil { + t.Fatal(err) + } + root := t.TempDir() + write := func(name string, data []byte, mode fs.FileMode) string { + t.Helper() + path := filepath.Join(root, name) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, data, mode); err != nil { + t.Fatal(err) + } + return path + } + var xrayArchive bytes.Buffer + zw := zip.NewWriter(&xrayArchive) + header := &zip.FileHeader{Name: "xray"} + header.SetMode(0o755) + entry, err := zw.CreateHeader(header) + if err != nil { + t.Fatal(err) + } + if _, err := entry.Write([]byte("#!/bin/sh\nprintf 'Xray 26.10.10 fixture\\n'\n")); err != nil { + t.Fatal(err) + } + if err := zw.Close(); err != nil { + t.Fatal(err) + } + xrayArchivePath := write("xray.zip", xrayArchive.Bytes(), 0o600) + var mtgArchive bytes.Buffer + gz := gzip.NewWriter(&mtgArchive) + tw := tar.NewWriter(gz) + const mtg = "#!/bin/sh\nexit 0\n" + if err := tw.WriteHeader(&tar.Header{Name: "mtg-multi-1.0.0-linux-amd64/mtg-multi", Mode: 0o755, Size: int64(len(mtg))}); err != nil { + t.Fatal(err) + } + if _, err := tw.Write([]byte(mtg)); err != nil { + t.Fatal(err) + } + if err := tw.Close(); err != nil { + t.Fatal(err) + } + if err := gz.Close(); err != nil { + t.Fatal(err) + } + mtgArchivePath := write("mtg.tar.gz", mtgArchive.Bytes(), 0o600) + write("tools/curl", []byte(`#!/bin/sh +for arg in "$@"; do url=$arg; done +case "$url" in + https://api.github.com/repos/mhsanaei/mtg-multi/releases/latest) + printf '{"tag_name":"v1.0.0"}\n' + ;; + */Xray-*.zip) + cp "$XRAY_ARCHIVE_FIXTURE" "${url##*/}" + ;; + */mtg-multi-*.tar.gz) + cp "$MTG_ARCHIVE_FIXTURE" "${url##*/}" + ;; + */geoip.dat|*/geosite.dat) + target=${url##*/} + if [ "$1" = '-sfLRo' ]; then target=$2; fi + printf 'geodata fixture\n' > "$target" + ;; + *) exit 1 ;; +esac +`), 0o755) + cmd := exec.Command("sh", script, "amd64") + cmd.Dir = root + cmd.Env = append(os.Environ(), "PATH="+filepath.Join(root, "tools")+string(os.PathListSeparator)+os.Getenv("PATH"), + "XRAY_ARCHIVE_FIXTURE="+xrayArchivePath, "MTG_ARCHIVE_FIXTURE="+mtgArchivePath) + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("DockerInit: %v\n%s", err, output) + } + app := filepath.Join(root, "app") + if err := os.Rename(filepath.Join(root, "build"), app); err != nil { + t.Fatal(err) + } + activePath := filepath.Join(app, "bin", "xray-linux-amd64") + target, err := os.Readlink(activePath) + if err != nil { + t.Fatalf("working Xray must be a symlink: %v", err) + } + if filepath.IsAbs(target) { + t.Fatalf("build symlink is tied to the builder path: %q", target) + } + output, err := exec.Command(activePath, "-version").CombinedOutput() + if err != nil || string(output) != "Xray 26.10.10 fixture\n" { + t.Fatalf("relocated Xray = %q, %v", output, err) + } + var binaries []string + if err := filepath.WalkDir(app, func(path string, entry fs.DirEntry, err error) error { + if err == nil && entry.Type().IsRegular() && strings.HasPrefix(entry.Name(), "xray") { + relative, relErr := filepath.Rel(app, path) + if relErr != nil { + return relErr + } + binaries = append(binaries, relative) + } + return err + }); err != nil { + t.Fatal(err) + } + if !slices.Equal(binaries, []string{filepath.Join("xray-image", "xray-linux-amd64")}) { + t.Fatalf("physical Xray binaries = %q, want only xray-image/xray-linux-amd64", binaries) + } +} diff --git a/internal/web/service/server_xray_version.go b/internal/web/service/server_xray_version.go index 3f711162f..224d7f03c 100644 --- a/internal/web/service/server_xray_version.go +++ b/internal/web/service/server_xray_version.go @@ -115,7 +115,7 @@ func localXrayBinaryPath(version string) string { if !supportedXrayVersion(version) { return "" } - for _, path := range []string{xray.GetBinaryPath(), bundledXrayBinaryPath()} { + for _, path := range []string{bundledXrayBinaryPath(), xray.GetBinaryPath()} { if xrayBinaryVersion(path) == strings.TrimPrefix(version, "v") { return path } @@ -146,12 +146,31 @@ func bundledXrayBinaryPath() string { } func restoreImageXray() error { - file, err := os.Open(bundledXrayBinaryPath()) + imagePath, err := filepath.Abs(bundledXrayBinaryPath()) if err != nil { - return fmt.Errorf("open bundled Xray: %w", err) + return err } - defer file.Close() - return replaceXrayBinary(file, xray.GetBinaryPath()) + image, err := os.Stat(imagePath) + if err != nil { + return fmt.Errorf("stat bundled Xray: %w", err) + } + target := xray.GetBinaryPath() + if active, err := os.Stat(target); err == nil && os.SameFile(image, active) { + return nil + } + if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { + return err + } + tmpDir, err := os.MkdirTemp(filepath.Dir(target), ".xray-link-*") + if err != nil { + return err + } + defer os.RemoveAll(tmpDir) + linkPath := filepath.Join(tmpDir, xray.GetBinaryName()) + if err := os.Symlink(imagePath, linkPath); err != nil { + return err + } + return os.Rename(linkPath, target) } func replaceXrayBinary(source io.Reader, target string) error { diff --git a/internal/web/service/server_xray_version_test.go b/internal/web/service/server_xray_version_test.go index fdb8da87c..a627fac24 100644 --- a/internal/web/service/server_xray_version_test.go +++ b/internal/web/service/server_xray_version_test.go @@ -134,6 +134,28 @@ func assertXrayVersionStarts(t *testing.T, root, want string) { } } +func assertXrayImageLink(t *testing.T, root string) { + t.Helper() + link, err := os.Lstat(xray.GetBinaryPath()) + if err != nil { + t.Fatal(err) + } + if link.Mode()&os.ModeSymlink == 0 { + t.Fatalf("working Xray mode = %v, want a symlink to the bundled binary", link.Mode()) + } + active, err := os.Stat(xray.GetBinaryPath()) + if err != nil { + t.Fatal(err) + } + bundled, err := os.Stat(filepath.Join(root, "xray-image", xray.GetBinaryName())) + if err != nil { + t.Fatal(err) + } + if !os.SameFile(active, bundled) { + t.Fatal("working and bundled Xray paths refer to different physical files") + } +} + func TestUpdateXrayPersistsOnlyContainerSelections(t *testing.T) { for _, container := range []string{"true", "false"} { t.Run(container, func(t *testing.T) { @@ -171,6 +193,7 @@ func TestContainerXrayUpdateUsesLocalVersionWithoutNetwork(t *testing.T) { name, installed, bundled string }{ {"bundled version", "26.10.9", "26.10.10"}, + {"duplicate image version", "26.10.10", "26.10.10"}, {"installed version", "26.10.10", "26.10.11"}, } { t.Run(tc.name, func(t *testing.T) { @@ -186,6 +209,9 @@ func TestContainerXrayUpdateUsesLocalVersionWithoutNetwork(t *testing.T) { } assertXrayVersionStarts(t, root, "26.10.10\n") assertStoredXrayVersion(t, "v26.10.10") + if tc.bundled == "26.10.10" { + assertXrayImageLink(t, root) + } }) } } @@ -229,23 +255,56 @@ func TestXrayVersionListKeepsImageChoiceWhenGitHubIsOffline(t *testing.T) { func TestXrayLatestRestoresImageOfflineAndDeletesPin(t *testing.T) { root := setupXrayVersionTest(t) requests := serveXrayVersion(t, "26.10.10") - writeXrayVersionBinary(t, filepath.Join(root, "xray-image", xray.GetBinaryName()), "26.10.11") + bundledPath := filepath.Join(root, "xray-image", xray.GetBinaryName()) + writeXrayVersionBinary(t, bundledPath, "26.10.11") + bundledBefore, err := os.ReadFile(bundledPath) + if err != nil { + t.Fatal(err) + } svc := &ServerService{} + if err := svc.UpdateXray("latest"); err != nil { + t.Fatal(err) + } + assertXrayImageLink(t, root) + assertXrayVersionStarts(t, root, "26.10.11\n") if err := svc.UpdateXray("v26.10.10"); err != nil { t.Fatal(err) } - assertXrayVersionStarts(t, root, "26.10.10\n") + assertXrayVersionStarts(t, root, "26.10.11\n26.10.10\n") + bundledAfter, err := os.ReadFile(bundledPath) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(bundledBefore, bundledAfter) { + t.Fatal("installing a pinned release overwrote the bundled symlink target") + } requests.Store(0) if err := svc.UpdateXray("latest"); err != nil { t.Fatal(err) } assertStoredXrayVersion(t, "") - assertXrayVersionStarts(t, root, "26.10.10\n26.10.11\n") + assertXrayImageLink(t, root) + assertXrayVersionStarts(t, root, "26.10.11\n26.10.10\n26.10.11\n") if got := requests.Load(); got != 0 { t.Fatalf("latest made %d network requests, want none", got) } } +func TestContainerXrayRestoreDoesNotLinkBundledBinaryToItself(t *testing.T) { + root := setupXrayVersionTest(t) + t.Setenv("XUI_BIN_FOLDER", filepath.Join(root, "xray-image")) + writeXrayVersionBinary(t, xray.GetBinaryPath(), "26.10.11") + svc := &ServerService{} + if err := svc.RestoreXrayVersion(); err != nil { + t.Fatal(err) + } + if err := svc.RestartXrayService(); err != nil { + t.Fatal(err) + } + assertXrayVersionStarts(t, root, "26.10.11\n") + assertStoredXrayVersion(t, "") +} + func TestContainerXrayPinSurvivesRecreationAndRestartBeforeFirstLaunch(t *testing.T) { root := setupXrayVersionTest(t) requests := serveXrayVersion(t, "26.10.10") @@ -333,6 +392,9 @@ func TestContainerXrayStartupFallbackPreservesPreferenceAndNativeBinary(t *testi } assertXrayVersionStarts(t, root, tc.wantVersion+"\n") assertStoredXrayVersion(t, tc.pin) + if tc.container == "true" { + assertXrayImageLink(t, root) + } if got := requests.Load(); got != tc.wantRequests { t.Fatalf("startup requests = %d, want %d", got, tc.wantRequests) } diff --git a/internal/xray/hot_diff_test.go b/internal/xray/hot_diff_test.go index fdb091973..56be26f94 100644 --- a/internal/xray/hot_diff_test.go +++ b/internal/xray/hot_diff_test.go @@ -13,6 +13,7 @@ import ( ) func TestMain(m *testing.M) { + runXrayAssetProcessHelper() // ComputeHotDiff logs the section that blocks a hot apply; the package // logger must exist before any test exercises a blocked path. xuilogger.InitLogger(logging.ERROR) diff --git a/internal/xray/process.go b/internal/xray/process.go index 9821d486d..10ad17a3f 100644 --- a/internal/xray/process.go +++ b/internal/xray/process.go @@ -320,7 +320,7 @@ func (p *Process) SetConfig(config *Config) { // keeping it in step after a hot apply (Start only writes it on a cold start). func (p *Process) PersistConfig() error { p.mu.RLock() - data, err := json.MarshalIndent(p.config, "", " ") + data, err := marshalProcessConfig(p.config) path := p.configPath p.mu.RUnlock() if err != nil { @@ -619,7 +619,7 @@ func (p *process) Start() (err error) { } }() - data, err := json.MarshalIndent(p.config, "", " ") + data, err := marshalProcessConfig(p.config) if err != nil { return common.NewErrorf("Failed to generate XRAY configuration files: %v", err) } @@ -639,6 +639,12 @@ func (p *process) Start() (err error) { } cmd := exec.CommandContext(context.Background(), GetBinaryPath(), "-c", configPath) + if os.Getenv("XUI_IN_DOCKER") == "true" { + cmd.Env, err = containerXrayEnv() + if err != nil { + return err + } + } cmd.Stdout = p.logWriter cmd.Stderr = p.logWriter diff --git a/internal/xray/process_assets_test.go b/internal/xray/process_assets_test.go new file mode 100644 index 000000000..528863bd2 --- /dev/null +++ b/internal/xray/process_assets_test.go @@ -0,0 +1,216 @@ +package xray + +import ( + "encoding/json" + "fmt" + "os" + "os/signal" + "path/filepath" + "runtime" + "syscall" + "testing" + "time" + + "github.com/xtls/xray-core/common/platform" + "github.com/xtls/xray-core/common/platform/filesystem" +) + +func runXrayAssetProcessHelper() { + if os.Getenv("XUI_TEST_XRAY_ASSET_HELPER") == "1" { + if len(os.Args) > 1 && os.Args[1] == "-version" { + fmt.Println("Xray 26.10.10 fixture") + os.Exit(0) + } + stopped := make(chan os.Signal, 1) + signal.Notify(stopped, syscall.SIGTERM) + data, err := readXrayProcessResources() + if err != nil { + data = fmt.Appendf(nil, "asset lookup failed: %v", err) + } + if writeErr := os.WriteFile(os.Getenv("XUI_TEST_XRAY_ASSET_RESULT"), data, 0o600); writeErr != nil { + fmt.Fprintln(os.Stderr, writeErr) + os.Exit(1) + } + if err != nil { + os.Exit(1) + } + <-stopped + os.Exit(0) + } +} + +func readXrayProcessResources() ([]byte, error) { + data, err := os.ReadFile(os.Args[2]) + if err != nil { + return nil, err + } + var c Config + if err := json.Unmarshal(data, &c); err != nil { + return nil, err + } + var env map[string]string + if len(c.Env) > 0 { + if err := json.Unmarshal(c.Env, &env); err != nil { + return nil, err + } + } + for key, value := range env { + if err := os.Setenv(key, value); err != nil { + return nil, err + } + } + data, err = os.ReadFile(platform.GetAssetLocation("geosite.dat")) + if err != nil { + return nil, err + } + cert, err := filesystem.ReadCert("certificate.pem") + if err != nil { + return nil, err + } + data = append(append(data, '\n'), cert...) + for _, section := range [][]byte{c.DNSConfig, c.RouterConfig} { + var cfg struct{ Script string } + if len(section) == 0 { + continue + } + if err := json.Unmarshal(section, &cfg); err != nil { + return nil, err + } + if cfg.Script == "" { + continue + } + path, err := platform.ResolveLuaFile(cfg.Script) + if err != nil { + return nil, err + } + script, err := os.ReadFile(path) + if err != nil { + return nil, err + } + data = append(append(data, '\n'), script...) + } + return data, nil +} + +func TestSymlinkedContainerXrayFindsConfiguredResources(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("test executable uses Unix signals and symlinks") + } + executable, err := os.Executable() + if err != nil { + t.Fatal(err) + } + binary, err := os.ReadFile(executable) + if err != nil { + t.Fatal(err) + } + for _, tc := range []struct { + name, container, override, want string + scriptLocation string + }{ + {name: "container default", container: "true", want: "geodata in bin\ncertificate in bin"}, + {name: "container explicit assets", container: "true", override: "XRAY_LOCATION_ASSET", want: "configured shared geodata\ncertificate in bin"}, + {name: "container dotted asset setting", container: "true", override: "xray.location.asset", want: "configured shared geodata\ncertificate in bin"}, + {name: "container explicit certificates", container: "true", override: "XRAY_LOCATION_CERT", want: "geodata in bin\nconfigured shared certificate"}, + {name: "container dotted certificate setting", container: "true", override: "xray.location.cert", want: "geodata in bin\nconfigured shared certificate"}, + {name: "native default", container: "false", want: "geodata in bin\ncertificate in bin"}, + {name: "container scripts beside working binary", container: "true", scriptLocation: "bin", want: "geodata in bin\ncertificate in bin\ndns in bin\nrouting in bin"}, + {name: "container scripts prefer working directory", container: "true", scriptLocation: "cwd", want: "geodata in bin\ncertificate in bin\ndns in cwd\nrouting in cwd"}, + {name: "container scripts prefer config environment", container: "true", scriptLocation: "env", want: "geodata in bin\ncertificate in bin\ndns in shared\nrouting in shared"}, + {name: "native scripts beside binary", container: "false", scriptLocation: "bin", want: "geodata in bin\ncertificate in bin\ndns in bin\nrouting in bin"}, + } { + t.Run(tc.name, func(t *testing.T) { + root := t.TempDir() + binDir := filepath.Join(root, "bin") + imageDir := filepath.Join(root, "xray-image") + sharedDir := filepath.Join(root, "shared-assets") + for _, dir := range []string{binDir, imageDir, sharedDir} { + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + } + for _, key := range []string{"XRAY_LOCATION_ASSET", "xray.location.asset", "XRAY_LOCATION_CERT", "xray.location.cert", "XRAY_LOCATION_CONFIG", "xray.location.config", "XRAY_LOCATION_CONFDIR", "xray.location.confdir"} { + t.Setenv(key, "") + if err := os.Unsetenv(key); err != nil { + t.Fatal(err) + } + } + t.Setenv("XUI_BIN_FOLDER", binDir) + t.Setenv("XUI_LOG_FOLDER", filepath.Join(root, "log")) + t.Setenv("XUI_IN_DOCKER", tc.container) + t.Setenv("XUI_TEST_XRAY_ASSET_HELPER", "1") + resultPath := filepath.Join(root, "asset-result") + t.Setenv("XUI_TEST_XRAY_ASSET_RESULT", resultPath) + if tc.override != "" { + t.Setenv(tc.override, sharedDir) + } + for path, content := range map[string]string{ + filepath.Join(binDir, "geosite.dat"): "geodata in bin", + filepath.Join(sharedDir, "geosite.dat"): "configured shared geodata", + filepath.Join(binDir, "certificate.pem"): "certificate in bin", + filepath.Join(sharedDir, "certificate.pem"): "configured shared certificate", + } { + if err := os.WriteFile(path, []byte(content), 0o600); err != nil { + t.Fatal(err) + } + } + physicalPath := GetBinaryPath() + if tc.container == "true" { + physicalPath = filepath.Join(imageDir, GetBinaryName()) + } + if err := os.WriteFile(physicalPath, binary, 0o755); err != nil { + t.Fatal(err) + } + if tc.container == "true" { + if err := os.Symlink(physicalPath, GetBinaryPath()); err != nil { + t.Fatal(err) + } + } + c := &Config{} + if tc.scriptLocation != "" { + t.Chdir(root) + c.DNSConfig = []byte(`{"script":"dns.lua"}`) + c.RouterConfig = []byte(`{"script":"routing.lua"}`) + locations := map[string]string{binDir: "bin"} + if tc.scriptLocation == "cwd" || tc.scriptLocation == "env" { + locations[root] = "cwd" + } + if tc.scriptLocation == "env" { + locations[sharedDir] = "shared" + t.Setenv("XRAY_LOCATION_CONFDIR", binDir) + c.Env, err = json.Marshal(map[string]string{"XRAY_LOCATION_CONFDIR": sharedDir}) + if err != nil { + t.Fatal(err) + } + } + for dir, label := range locations { + for _, name := range []string{"dns", "routing"} { + if err := os.WriteFile(filepath.Join(dir, name+".lua"), []byte(name+" in "+label), 0o600); err != nil { + t.Fatal(err) + } + } + } + } + p := NewProcess(c) + t.Cleanup(func() { _ = p.Stop() }) + if err := p.Start(); err != nil { + t.Fatal(err) + } + deadline := time.Now().Add(3 * time.Second) + var result []byte + for time.Now().Before(deadline) { + result, _ = os.ReadFile(resultPath) + if len(result) > 0 { + break + } + time.Sleep(10 * time.Millisecond) + } + if string(result) != tc.want { + t.Fatalf("Xray resource lookup = %q, want %q", result, tc.want) + } + if tc.scriptLocation != "" && (string(c.DNSConfig) != `{"script":"dns.lua"}` || string(c.RouterConfig) != `{"script":"routing.lua"}`) { + t.Fatalf("starting Xray mutated its config snapshot: dns=%s routing=%s", c.DNSConfig, c.RouterConfig) + } + }) + } +} diff --git a/internal/xray/process_container.go b/internal/xray/process_container.go new file mode 100644 index 000000000..8ac846b4f --- /dev/null +++ b/internal/xray/process_container.go @@ -0,0 +1,97 @@ +package xray + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + + "github.com/xtls/xray-core/common/platform" + + "github.com/mhsanaei/3x-ui/v3/internal/config" + "github.com/mhsanaei/3x-ui/v3/internal/util/json_util" +) + +func containerXrayEnv() ([]string, error) { + binDir, err := filepath.Abs(config.GetBinFolderPath()) + if err != nil { + return nil, err + } + env := os.Environ() + // A symlink changes os.Executable's directory; keep resource lookup beside the working path. + for _, name := range []string{platform.AssetLocation, platform.CertLocation} { + alternate := platform.NormalizeEnvName(name) + if _, exists := os.LookupEnv(name); exists { + continue + } + if _, exists := os.LookupEnv(alternate); !exists { + env = append(env, alternate+"="+binDir) + } + } + return env, nil +} + +func marshalProcessConfig(c *Config) ([]byte, error) { + if os.Getenv("XUI_IN_DOCKER") == "true" && c != nil { + var env map[string]string + if len(c.Env) == 0 || json.Unmarshal(c.Env, &env) == nil { + lookup := func(name string) string { + for _, key := range []string{name, platform.NormalizeEnvName(name)} { + if value, exists := env[key]; exists { + return value + } + if value, exists := os.LookupEnv(key); exists { + return value + } + } + return "" + } + dirs := []string{lookup(platform.ConfdirLocation), lookup(platform.ConfigLocation), ".", config.GetBinFolderPath()} + prepared := *c + prepared.DNSConfig = resolveContainerLuaScript(c.DNSConfig, dirs) + prepared.RouterConfig = resolveContainerLuaScript(c.RouterConfig, dirs) + c = &prepared + } + } + return json.MarshalIndent(c, "", " ") +} + +func resolveContainerLuaScript(section json_util.RawMessage, dirs []string) json_util.RawMessage { + var cfg struct{ Script string } + if len(section) == 0 || json.Unmarshal(section, &cfg) != nil || cfg.Script == "" || filepath.IsAbs(cfg.Script) { + return section + } + // Preserve Xray's lookup order, with the working binary directory as its final fallback. + for _, dir := range dirs { + if dir == "" { + continue + } + path, err := filepath.Abs(filepath.Join(dir, cfg.Script)) + if err != nil { + return section + } + info, err := os.Stat(path) + if os.IsNotExist(err) { + continue + } + if err != nil || !info.Mode().IsRegular() { + return section + } + var fields map[string]json.RawMessage + if json.Unmarshal(section, &fields) != nil { + return section + } + for key := range fields { + if strings.EqualFold(key, "script") { + delete(fields, key) + } + } + fields["script"], _ = json.Marshal(path) + data, err := json.Marshal(fields) + if err != nil { + return section + } + return data + } + return section +}