From e8d445a04533497cdd7ecc0ab3105d06d5ea72b1 Mon Sep 17 00:00:00 2001 From: ergolyam Date: Thu, 15 Oct 2026 23:41:33 +0300 Subject: [PATCH] fix(xray): reuse bundled container binary through symlinks Package the image's Xray executable once and make the working path a relocatable symlink. Reuse that executable for latest and matching explicit pins, replacing duplicate working copies instead of copying it again. Restore image selections with an atomic symlink replacement. Downloads replace the working symlink without overwriting its target, preserving the image binary for fallback and unpinning. Avoid self-referencing links when the working and bundled paths identify the same file. Keep container resource lookup compatible with the former executable directory, including geodata, relative certificates, Lua script search precedence, and environment overrides. Leave native processes unchanged. Exercise the real packaging script after relocation, real child-process resource lookup, and version-switching lifecycles without nested containers. --- DockerInit.sh | 5 +- Dockerfile | 1 - docker_init_test.go | 129 +++++++++++ internal/web/service/server_xray_version.go | 29 ++- .../web/service/server_xray_version_test.go | 68 +++++- internal/xray/hot_diff_test.go | 1 + internal/xray/process.go | 10 +- internal/xray/process_assets_test.go | 216 ++++++++++++++++++ internal/xray/process_container.go | 97 ++++++++ 9 files changed, 543 insertions(+), 13 deletions(-) create mode 100644 docker_init_test.go create mode 100644 internal/xray/process_assets_test.go create mode 100644 internal/xray/process_container.go 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 +}