mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-24 04:31:25 +00:00
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.
This commit is contained in:
parent
50b7763f07
commit
e8d445a045
9 changed files with 543 additions and 13 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
129
docker_init_test.go
Normal file
129
docker_init_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
216
internal/xray/process_assets_test.go
Normal file
216
internal/xray/process_assets_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
97
internal/xray/process_container.go
Normal file
97
internal/xray/process_container.go
Normal file
|
|
@ -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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue