mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 19:25:05 +03:00
fix(update): keep Xray-core running until update downloads finish (#7002)
UpdateXray stopped the core before downloading, which also stopped the Panel Traffic Outbound bridge the download needed on servers that reach GitHub only through it, leaving every client offline. The new core is now downloaded, staged and checked with run -test against the current config while the old one serves; only the rename and restart swap it (Windows still stops first). update.sh gets the bridge as XUI_UPDATE_PROXY, installs x-ui.sh from the release archive, and fetches Alpine's x-ui.rc before stopping x-ui.
This commit is contained in:
committed by
GitHub
parent
4d583cebf3
commit
6d9e836d61
@@ -250,6 +250,9 @@ func (s *PanelService) startUpdate(useDev bool) (int64, error) {
|
||||
updateScript := fmt.Sprintf("set -e; trap 'rm -f %s' EXIT; %s %s", shellQuote(scriptPath), shellQuote(bash), shellQuote(scriptPath))
|
||||
runIDEnv := "XUI_UPDATE_RUN_ID=" + strconv.FormatInt(runID, 10)
|
||||
statusFileEnv := "XUI_UPDATE_STATUS_FILE=" + statusFile
|
||||
// update.sh sends its curl downloads through the panel egress bridge. With socks5h the
|
||||
// proxy resolves GitHub's hostnames, since this server's own DNS may be filtered.
|
||||
egressEnv := "XUI_UPDATE_PROXY=" + strings.Replace((&service.SettingService{}).PanelEgressProxyURL(), "socks5://", "socks5h://", 1)
|
||||
proxyEnv := updateProxyEnvVars()
|
||||
|
||||
if systemdRun, err := exec.LookPath("systemd-run"); err == nil {
|
||||
@@ -261,6 +264,7 @@ func (s *PanelService) startUpdate(useDev bool) (int64, error) {
|
||||
"--setenv", "XUI_UPDATE_TAG=" + updateTag,
|
||||
"--setenv", runIDEnv,
|
||||
"--setenv", statusFileEnv,
|
||||
"--setenv", egressEnv,
|
||||
}
|
||||
for _, kv := range proxyEnv {
|
||||
args = append(args, "--setenv", kv)
|
||||
@@ -290,6 +294,7 @@ func (s *PanelService) startUpdate(useDev bool) (int64, error) {
|
||||
"XUI_UPDATE_TAG="+updateTag,
|
||||
runIDEnv,
|
||||
statusFileEnv,
|
||||
egressEnv,
|
||||
)
|
||||
setDetachedProcess(cmd)
|
||||
if err := cmd.Start(); err != nil {
|
||||
|
||||
@@ -933,13 +933,16 @@ const (
|
||||
maxXrayReleasesBytes = 32 << 20
|
||||
)
|
||||
|
||||
func (s *ServerService) GetXrayVersions() ([]string, error) {
|
||||
const (
|
||||
XrayURL = "https://api.github.com/repos/XTLS/Xray-core/releases"
|
||||
bufferSize = 8192
|
||||
)
|
||||
// Tests point these at a local server that stands in for GitHub.
|
||||
var (
|
||||
xrayReleasesURL = "https://api.github.com/repos/XTLS/Xray-core/releases"
|
||||
xrayDownloadURL = "https://github.com/XTLS/Xray-core/releases/download"
|
||||
)
|
||||
|
||||
req, reqErr := http.NewRequestWithContext(context.Background(), http.MethodGet, XrayURL, nil)
|
||||
func (s *ServerService) GetXrayVersions() ([]string, error) {
|
||||
const bufferSize = 8192
|
||||
|
||||
req, reqErr := http.NewRequestWithContext(context.Background(), http.MethodGet, xrayReleasesURL, nil)
|
||||
if reqErr != nil {
|
||||
return nil, reqErr
|
||||
}
|
||||
@@ -1041,7 +1044,7 @@ func (s *ServerService) downloadXRay(version string) (string, error) {
|
||||
}
|
||||
|
||||
fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch)
|
||||
url := fmt.Sprintf("https://github.com/XTLS/Xray-core/releases/download/%s/%s", version, fileName)
|
||||
url := fmt.Sprintf("%s/%s/%s", xrayDownloadURL, version, fileName)
|
||||
client := s.settingService.NewProxiedHTTPClient(60 * time.Second)
|
||||
req, reqErr := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
|
||||
if reqErr != nil {
|
||||
@@ -1154,12 +1157,8 @@ func (s *ServerService) UpdateXray(version string) error {
|
||||
return fmt.Errorf("xray version %q is not in the fetched release list", version)
|
||||
}
|
||||
|
||||
// 1. Stop xray before doing anything
|
||||
if err := s.StopXrayService(); err != nil {
|
||||
logger.Warning("failed to stop xray before update:", err)
|
||||
}
|
||||
|
||||
// 2. Download the zip
|
||||
// Keep the running core up until the swap. The download may need its panel egress
|
||||
// bridge, and if the candidate fails the config check, clients stay on the old core.
|
||||
zipFileName, err := s.downloadXRay(version)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1181,19 +1180,20 @@ func (s *ServerService) UpdateXray(version string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 3. Helper to extract files
|
||||
copyZipFile := func(zipName string, fileName string) error {
|
||||
// Stage next to the target so the swap is an atomic rename on one filesystem, and
|
||||
// keep the target's extension so Windows will run the staged file for the check.
|
||||
stageZipFile := func(zipName string, fileName string) (string, error) {
|
||||
zipFile, err := reader.Open(zipName)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
defer zipFile.Close()
|
||||
if err := os.MkdirAll(filepath.Dir(fileName), 0o755); err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
tmpFile, err := os.CreateTemp(filepath.Dir(fileName), ".xray-*")
|
||||
tmpFile, err := os.CreateTemp(filepath.Dir(fileName), ".xray-*"+filepath.Ext(fileName))
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
tmpPath := tmpFile.Name()
|
||||
ok := false
|
||||
@@ -1205,42 +1205,53 @@ func (s *ServerService) UpdateXray(version string) error {
|
||||
}()
|
||||
n, err := io.Copy(tmpFile, io.LimitReader(zipFile, maxXrayBinaryBytes+1))
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
if n > maxXrayBinaryBytes {
|
||||
return fmt.Errorf("xray binary exceeds %d bytes", maxXrayBinaryBytes)
|
||||
return "", fmt.Errorf("xray binary exceeds %d bytes", maxXrayBinaryBytes)
|
||||
}
|
||||
if err := tmpFile.Chmod(0o755); err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
_ = os.Remove(fileName)
|
||||
}
|
||||
if err := os.Rename(tmpPath, fileName); err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
ok = true
|
||||
return nil
|
||||
return tmpPath, nil
|
||||
}
|
||||
|
||||
// 4. Extract correct binary
|
||||
zipName, target := "xray", xray.GetBinaryPath()
|
||||
if runtime.GOOS == "windows" {
|
||||
targetBinary := filepath.Join(config.GetBinFolderPath(), "xray-windows-amd64.exe")
|
||||
err = copyZipFile("xray.exe", targetBinary)
|
||||
} else {
|
||||
err = copyZipFile("xray", xray.GetBinaryPath())
|
||||
zipName, target = "xray.exe", filepath.Join(config.GetBinFolderPath(), "xray-windows-amd64.exe")
|
||||
}
|
||||
staged, err := stageZipFile(zipName, target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(staged)
|
||||
|
||||
xrayConfig, err := s.xrayService.GetXrayConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := xray.CheckConfig(staged, xrayConfig); err != nil {
|
||||
return fmt.Errorf("xray %s failed the config check and was not installed: %w", version, err)
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows cannot replace a running executable.
|
||||
if err := s.StopXrayService(); err != nil {
|
||||
logger.Warning("failed to stop xray before update:", err)
|
||||
}
|
||||
_ = os.Remove(target)
|
||||
}
|
||||
if err := os.Rename(staged, target); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 5. Restart xray
|
||||
if err := s.xrayService.RestartXray(true); err != nil {
|
||||
logger.Error("start xray failed:", err)
|
||||
return err
|
||||
return fmt.Errorf("xray %s is installed, but the restart failed: %w", version, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
||||
)
|
||||
|
||||
// fakeXrayCore handles only the command lines the panel uses to run a core. It exits 99
|
||||
// on any other, so a changed invocation fails the test.
|
||||
func fakeXrayCore(version string, acceptsConfig bool) string {
|
||||
check := "exit 0"
|
||||
if !acceptsConfig {
|
||||
check = "echo 'Failed to start: fake core refuses this config'; exit 23"
|
||||
}
|
||||
return "#!/bin/sh\n" +
|
||||
"case \"$*\" in\n" +
|
||||
"-version) echo 'Xray " + version + " (fake)' ;;\n" +
|
||||
"'run -test -c '*) [ -s \"$4\" ] || exit 98; " + check + " ;;\n" +
|
||||
"'-c '*) exec sleep 60 ;;\n" +
|
||||
"*) exit 99 ;;\n" +
|
||||
"esac\n"
|
||||
}
|
||||
|
||||
func xrayReleaseZip(t *testing.T, core string) []byte {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
zw := zip.NewWriter(&buf)
|
||||
w, err := zw.Create("xray")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := w.Write([]byte(core)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := zw.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// xrayReleaseServer stands in for GitHub. It lists one version and serves asset with a
|
||||
// matching .dgst, or a 404 when asset is nil.
|
||||
func xrayReleaseServer(t *testing.T, version string, asset []byte) {
|
||||
t.Helper()
|
||||
sum := sha256.Sum256(asset)
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/releases", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, `[{"tag_name":%q}]`, version)
|
||||
})
|
||||
mux.HandleFunc("/download/"+version+"/", func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case asset == nil:
|
||||
http.NotFound(w, r)
|
||||
case strings.HasSuffix(r.URL.Path, ".zip.dgst"):
|
||||
fmt.Fprintf(w, "SHA2-256= %s\n", hex.EncodeToString(sum[:]))
|
||||
case strings.HasSuffix(r.URL.Path, ".zip"):
|
||||
_, _ = w.Write(asset)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
srv := httptest.NewServer(mux)
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
releases, download := xrayReleasesURL, xrayDownloadURL
|
||||
xrayReleasesURL, xrayDownloadURL = srv.URL+"/releases", srv.URL+"/download"
|
||||
t.Cleanup(func() { xrayReleasesURL, xrayDownloadURL = releases, download })
|
||||
}
|
||||
|
||||
// startFakeCore runs the installed core on the panel's own config, as RestartXray
|
||||
// would, and stops whichever core is current when the test ends.
|
||||
func startFakeCore(t *testing.T) *xray.Process {
|
||||
t.Helper()
|
||||
cfg, err := (&XrayService{}).GetXrayConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("GetXrayConfig: %v", err)
|
||||
}
|
||||
p := xray.NewProcess(cfg)
|
||||
if err := p.Start(); err != nil {
|
||||
t.Fatalf("start fake core: %v", err)
|
||||
}
|
||||
restore := SetXrayProcessForTest(p)
|
||||
t.Cleanup(func() {
|
||||
if current := XrayProcess(); current != nil {
|
||||
_ = current.Stop()
|
||||
}
|
||||
_ = p.Stop()
|
||||
restore()
|
||||
// restore() keeps the hold-back reason that a refused restart sets.
|
||||
xrayState.holdBack("")
|
||||
})
|
||||
return p
|
||||
}
|
||||
|
||||
func TestUpdateXray(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("the fake cores are shell scripts")
|
||||
}
|
||||
const current, next = "26.6.27", "26.7.1"
|
||||
installed := fakeXrayCore(current, true)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
candidate string // the release's core; empty makes the download 404
|
||||
conflict bool // two inbounds on one port, added after the old core starts
|
||||
wantErr string
|
||||
wantBinary string
|
||||
wantVersion string
|
||||
}{
|
||||
{
|
||||
// The running core hosts the panel egress bridge. Stopping it first made the
|
||||
// download bypass the bridge, and when that failed every client stayed offline.
|
||||
name: "failed download leaves the running core alone",
|
||||
wantErr: "unexpected HTTP 404",
|
||||
wantBinary: installed,
|
||||
wantVersion: current,
|
||||
},
|
||||
{
|
||||
name: "core that refuses the config is not installed",
|
||||
candidate: fakeXrayCore(next, false),
|
||||
wantErr: "fake core refuses this config",
|
||||
wantBinary: installed,
|
||||
wantVersion: current,
|
||||
},
|
||||
{
|
||||
name: "accepted core replaces the running one",
|
||||
candidate: fakeXrayCore(next, true),
|
||||
wantBinary: fakeXrayCore(next, true),
|
||||
wantVersion: next,
|
||||
},
|
||||
{
|
||||
// A refused restart keeps the old core running, but the new binary is
|
||||
// already on disk, so the error has to say the update is installed.
|
||||
name: "refused restart reports the installed core",
|
||||
candidate: fakeXrayCore(next, true),
|
||||
conflict: true,
|
||||
wantErr: "is installed, but the restart failed: xray config refused",
|
||||
wantBinary: fakeXrayCore(next, true),
|
||||
wantVersion: current,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
dbDir := t.TempDir()
|
||||
t.Setenv("XUI_DB_FOLDER", dbDir)
|
||||
dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db"))
|
||||
binFolder := t.TempDir()
|
||||
t.Setenv("XUI_BIN_FOLDER", binFolder)
|
||||
t.Setenv("XUI_LOG_FOLDER", t.TempDir())
|
||||
|
||||
binary := filepath.Join(binFolder, xray.GetBinaryName())
|
||||
if err := os.WriteFile(binary, []byte(installed), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
running := startFakeCore(t)
|
||||
if tc.conflict {
|
||||
for _, tag := range []string{"first", "second"} {
|
||||
seedInboundConflict(t, tag, "0.0.0.0", 4443, model.VLESS, `{"network":"tcp"}`, `{}`)
|
||||
}
|
||||
}
|
||||
|
||||
var asset []byte
|
||||
if tc.candidate != "" {
|
||||
asset = xrayReleaseZip(t, tc.candidate)
|
||||
}
|
||||
xrayReleaseServer(t, "v"+next, asset)
|
||||
|
||||
err := (&ServerService{}).UpdateXray("v" + next)
|
||||
if tc.wantErr == "" && err != nil {
|
||||
t.Fatalf("UpdateXray: %v", err)
|
||||
}
|
||||
if tc.wantErr != "" && (err == nil || !strings.Contains(err.Error(), tc.wantErr)) {
|
||||
t.Fatalf("UpdateXray error = %v, want it to contain %q", err, tc.wantErr)
|
||||
}
|
||||
if tc.wantErr != "" && (XrayProcess() != running || !running.IsRunning()) {
|
||||
t.Fatal("a failed update stopped or replaced the running core")
|
||||
}
|
||||
if got := XrayProcess().GetXrayVersion(); got != tc.wantVersion {
|
||||
t.Fatalf("running core version = %q, want %q", got, tc.wantVersion)
|
||||
}
|
||||
got, err := os.ReadFile(binary)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != tc.wantBinary {
|
||||
t.Fatalf("installed core:\n%s\nwant:\n%s", got, tc.wantBinary)
|
||||
}
|
||||
leftovers, err := filepath.Glob(filepath.Join(binFolder, ".*"))
|
||||
if err != nil || len(leftovers) > 0 {
|
||||
t.Fatalf("temp files left in the bin folder: %v (glob err %v)", leftovers, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2003,37 +2003,12 @@ func normalizeStreamUDPHop(stream map[string]any) bool {
|
||||
return maskcompat.NormalizeUDPHop(stream["finalmask"])
|
||||
}
|
||||
|
||||
// forceUserspaceWireGuardTun moves every WireGuard outbound to the gVisor TUN (#6796). Values
|
||||
// stay raw so the template's other keys reach the core byte for byte.
|
||||
// forceUserspaceWireGuardTun moves every WireGuard outbound to the gVisor TUN (#6796) and
|
||||
// warns for each one it moves off a kernel TUN.
|
||||
func forceUserspaceWireGuardTun(raw json_util.RawMessage) json_util.RawMessage {
|
||||
var outbounds []map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &outbounds); err != nil {
|
||||
return raw
|
||||
}
|
||||
for _, ob := range outbounds {
|
||||
var protocol, tag string
|
||||
if json.Unmarshal(ob["protocol"], &protocol) != nil || !strings.EqualFold(protocol, "wireguard") {
|
||||
continue
|
||||
}
|
||||
var settings map[string]json.RawMessage
|
||||
if json.Unmarshal(ob["settings"], &settings) != nil || settings == nil {
|
||||
continue
|
||||
}
|
||||
wasUserspace := string(settings["noKernelTun"]) == "true"
|
||||
settings["noKernelTun"] = json.RawMessage("true")
|
||||
rewritten, err := json.Marshal(settings)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ob["settings"] = rewritten
|
||||
if !wasUserspace {
|
||||
_ = json.Unmarshal(ob["tag"], &tag)
|
||||
logger.Warningf("Outbound %q: kernel TUN cannot be set up here, running WireGuard on the userspace TUN instead (#6796)", tag)
|
||||
}
|
||||
}
|
||||
rewritten, err := json.Marshal(outbounds)
|
||||
if err != nil {
|
||||
return raw
|
||||
rewritten, moved := xray.ForceUserspaceWireGuardTun(raw)
|
||||
for _, tag := range moved {
|
||||
logger.Warningf("Outbound %q: kernel TUN cannot be set up here, running WireGuard on the userspace TUN instead (#6796)", tag)
|
||||
}
|
||||
return rewritten
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package xray
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
|
||||
)
|
||||
|
||||
// ForceUserspaceWireGuardTun sets noKernelTun on every WireGuard outbound and returns the tags
|
||||
// it changed. Values stay raw so the template's other keys reach the core byte for byte.
|
||||
func ForceUserspaceWireGuardTun(raw json_util.RawMessage) (json_util.RawMessage, []string) {
|
||||
var outbounds []map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &outbounds); err != nil {
|
||||
return raw, nil
|
||||
}
|
||||
var moved []string
|
||||
for _, ob := range outbounds {
|
||||
var protocol string
|
||||
if json.Unmarshal(ob["protocol"], &protocol) != nil || !strings.EqualFold(protocol, "wireguard") {
|
||||
continue
|
||||
}
|
||||
var settings map[string]json.RawMessage
|
||||
if json.Unmarshal(ob["settings"], &settings) != nil || settings == nil {
|
||||
continue
|
||||
}
|
||||
wasUserspace := string(settings["noKernelTun"]) == "true"
|
||||
settings["noKernelTun"] = json.RawMessage("true")
|
||||
rewritten, err := json.Marshal(settings)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ob["settings"] = rewritten
|
||||
if !wasUserspace {
|
||||
var tag string
|
||||
_ = json.Unmarshal(ob["tag"], &tag)
|
||||
moved = append(moved, tag)
|
||||
}
|
||||
}
|
||||
rewritten, err := json.Marshal(outbounds)
|
||||
if err != nil {
|
||||
return raw, nil
|
||||
}
|
||||
return rewritten, moved
|
||||
}
|
||||
@@ -595,6 +595,43 @@ func (p *process) refreshAPIPort() {
|
||||
p.mu.Unlock()
|
||||
}
|
||||
|
||||
// xrayCheckTimeout bounds `run -test`, which loads the config and its geo files but opens
|
||||
// no listeners, so it can run while the live core holds the ports.
|
||||
const xrayCheckTimeout = 30 * time.Second
|
||||
|
||||
// CheckConfig runs the core at binaryPath in test mode against cfg. Updates call it first,
|
||||
// so a core that rejects cfg never replaces the running one.
|
||||
func CheckConfig(binaryPath string, cfg *Config) error {
|
||||
// The test run builds every outbound. A kernel-TUN WireGuard one would add a second wg
|
||||
// device and IPv6 policy rules to the host, and the run exits without removing the rules.
|
||||
checked := *cfg
|
||||
checked.OutboundConfigs, _ = ForceUserspaceWireGuardTun(cfg.OutboundConfigs)
|
||||
data, err := json.Marshal(&checked)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// CreateTemp creates the file as 0600; the config holds client credentials.
|
||||
file, err := os.CreateTemp(filepath.Dir(binaryPath), ".config-check-*.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(file.Name())
|
||||
_, err = file.Write(data)
|
||||
if closeErr := file.Close(); err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), xrayCheckTimeout)
|
||||
defer cancel()
|
||||
out, err := exec.CommandContext(ctx, binaryPath, "run", "-test", "-c", file.Name()).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %s", err, bytes.TrimSpace(out))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// refreshVersion updates the version string by running the Xray binary with -version.
|
||||
func (p *process) refreshVersion() {
|
||||
version := "Unknown"
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package xray
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
|
||||
)
|
||||
|
||||
// A kernel-TUN WireGuard outbound in `run -test` adds a second wg device and IPv6 policy
|
||||
// rules to the host, and the check exits without removing the rules.
|
||||
func TestCheckConfigKeepsWireGuardOffKernelTun(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("shell fixture is Unix-only")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
checked := filepath.Join(dir, "checked.json")
|
||||
core := filepath.Join(dir, "xray")
|
||||
script := "#!/bin/sh\n[ \"$1 $2 $3\" = 'run -test -c' ] || exit 99\nexec cp \"$4\" '" + checked + "'\n"
|
||||
if err := os.WriteFile(core, []byte(script), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cfg := &Config{OutboundConfigs: json_util.RawMessage(
|
||||
`[{"protocol":"wireguard","tag":"warp","settings":{"noKernelTun":false,"mtu":1420}}]`)}
|
||||
if err := CheckConfig(core, cfg); err != nil {
|
||||
t.Fatalf("CheckConfig: %v", err)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(checked)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got struct {
|
||||
Outbounds []struct {
|
||||
Settings map[string]any `json:"settings"`
|
||||
} `json:"outbounds"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &got); err != nil {
|
||||
t.Fatalf("checked config: %v\n%s", err, data)
|
||||
}
|
||||
if len(got.Outbounds) != 1 || got.Outbounds[0].Settings["noKernelTun"] != true {
|
||||
t.Fatalf("checked outbounds = %+v, want the wireguard outbound on noKernelTun", got.Outbounds)
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,9 @@ if _command_exists curl; then
|
||||
else
|
||||
_fail "ERROR: Command 'curl' not found."
|
||||
fi
|
||||
# The panel sets XUI_UPDATE_PROXY to its egress bridge's socks5h URL for the GitHub downloads.
|
||||
# The IP lookups use plain curl so they still report this server's own address.
|
||||
[[ -n "${XUI_UPDATE_PROXY}" ]] && curl_bin="${curl_bin} --proxy ${XUI_UPDATE_PROXY}"
|
||||
|
||||
# Check OS and set release variable
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
@@ -524,17 +527,15 @@ update_x-ui() {
|
||||
fi
|
||||
fi
|
||||
echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..."
|
||||
# x-ui.sh, x-ui.rc and the unit files must come from the same release as
|
||||
# the binary; only the rolling dev build tracks main.
|
||||
# x-ui.rc and the unit files must come from the binary's own release; only the
|
||||
# rolling dev build tracks main. x-ui.sh ships in the archive and needs no GitHub check.
|
||||
script_ref="${tag_version}"
|
||||
if [[ "${tag_version}" == "dev-latest" ]]; then
|
||||
script_ref="main"
|
||||
fi
|
||||
# The unit files are only fetched when the release tarball lacks them, so
|
||||
# they are checked at that point instead of here.
|
||||
local required_files=("x-ui.sh")
|
||||
[[ $release == "alpine" ]] && required_files+=("x-ui.rc")
|
||||
require_repo_files "${script_ref}" "${required_files[@]}"
|
||||
[[ $release == "alpine" ]] && require_repo_files "${script_ref}" "x-ui.rc"
|
||||
${curl_bin} -fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2> /dev/null
|
||||
if [[ $? -ne 0 ]]; then
|
||||
_fail "ERROR: Failed to download x-ui, please be sure that your server can access GitHub"
|
||||
@@ -566,6 +567,23 @@ update_x-ui() {
|
||||
_fail "ERROR: Failed to download the checksum for x-ui-linux-$(arch).tar.gz (HTTP ${sidecar_code})"
|
||||
fi
|
||||
|
||||
# Download everything before x-ui stops, because its Xray and any panel egress bridge
|
||||
# stop with it. Alpine's init script is the only file the archive lacks.
|
||||
if [[ $release == "alpine" ]]; then
|
||||
echo -e "${green}Downloading startup unit x-ui.rc...${plain}"
|
||||
xui_rc_temp="/etc/init.d/x-ui.tmp.$$"
|
||||
rm -f "${xui_rc_temp}"
|
||||
${curl_bin} -fLRo "${xui_rc_temp}" "https://raw.githubusercontent.com/MHSanaei/3x-ui/${script_ref}/x-ui.rc" > /dev/null 2>&1
|
||||
if [[ $? -ne 0 ]]; then
|
||||
rm -f "${xui_rc_temp}" "${archive}"
|
||||
_fail "ERROR: Failed to download startup unit x-ui.rc, please be sure that your server can access GitHub"
|
||||
fi
|
||||
if [[ ! -s "${xui_rc_temp}" ]]; then
|
||||
rm -f "${xui_rc_temp}" "${archive}"
|
||||
_fail "ERROR: Downloaded startup unit x-ui.rc is empty, please be sure that your server can access GitHub"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -e ${xui_folder}/ ]]; then
|
||||
echo -e "${green}Stopping x-ui...${plain}"
|
||||
if [[ $release == "alpine" ]]; then
|
||||
@@ -650,20 +668,10 @@ update_x-ui() {
|
||||
chmod +x bin/mtg-linux-$(arch) > /dev/null 2>&1
|
||||
fi
|
||||
|
||||
echo -e "${green}Downloading and installing x-ui.sh script...${plain}"
|
||||
# The archive's x-ui.sh is built from the same commit as the binary.
|
||||
echo -e "${green}Installing x-ui.sh script...${plain}"
|
||||
local xui_script_temp="/usr/bin/x-ui-temp.$$"
|
||||
rm -f "${xui_script_temp}"
|
||||
${curl_bin} -fLRo "${xui_script_temp}" "https://raw.githubusercontent.com/MHSanaei/3x-ui/${script_ref}/x-ui.sh" > /dev/null 2>&1
|
||||
if [[ $? -ne 0 ]]; then
|
||||
rm -f "${xui_script_temp}"
|
||||
_fail "ERROR: Failed to download x-ui.sh script, please be sure that your server can access GitHub"
|
||||
fi
|
||||
if [[ ! -s "${xui_script_temp}" ]]; then
|
||||
rm -f "${xui_script_temp}"
|
||||
_fail "ERROR: Downloaded x-ui.sh script is empty, please be sure that your server can access GitHub"
|
||||
fi
|
||||
mv -f "${xui_script_temp}" /usr/bin/x-ui
|
||||
if [[ $? -ne 0 ]]; then
|
||||
if ! cp -f x-ui.sh "${xui_script_temp}" || ! mv -f "${xui_script_temp}" /usr/bin/x-ui; then
|
||||
rm -f "${xui_script_temp}"
|
||||
_fail "ERROR: Failed to install x-ui.sh script"
|
||||
fi
|
||||
@@ -686,18 +694,7 @@ update_x-ui() {
|
||||
"${xui_folder}/x-ui" migrate
|
||||
|
||||
if [[ $release == "alpine" ]]; then
|
||||
echo -e "${green}Downloading and installing startup unit x-ui.rc...${plain}"
|
||||
xui_rc_temp="/etc/init.d/x-ui.tmp.$$"
|
||||
rm -f "${xui_rc_temp}"
|
||||
${curl_bin} -fLRo "${xui_rc_temp}" "https://raw.githubusercontent.com/MHSanaei/3x-ui/${script_ref}/x-ui.rc" > /dev/null 2>&1
|
||||
if [[ $? -ne 0 ]]; then
|
||||
rm -f "${xui_rc_temp}"
|
||||
_fail "ERROR: Failed to download startup unit x-ui.rc, please be sure that your server can access GitHub"
|
||||
fi
|
||||
if [[ ! -s "${xui_rc_temp}" ]]; then
|
||||
rm -f "${xui_rc_temp}"
|
||||
_fail "ERROR: Downloaded startup unit x-ui.rc is empty, please be sure that your server can access GitHub"
|
||||
fi
|
||||
echo -e "${green}Installing startup unit x-ui.rc...${plain}"
|
||||
mv -f "${xui_rc_temp}" /etc/init.d/x-ui
|
||||
if [[ $? -ne 0 ]]; then
|
||||
rm -f "${xui_rc_temp}"
|
||||
|
||||
Reference in New Issue
Block a user