diff --git a/internal/web/service/panel/panel.go b/internal/web/service/panel/panel.go index f9ca5d9c3..0e2a9fa2d 100644 --- a/internal/web/service/panel/panel.go +++ b/internal/web/service/panel/panel.go @@ -248,18 +248,23 @@ 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 + proxyEnv := updateProxyEnvVars() if systemdRun, err := exec.LookPath("systemd-run"); err == nil { unitName := fmt.Sprintf("x-ui-web-update-%d", time.Now().Unix()) - cmd := exec.CommandContext(context.Background(), systemdRun, + args := []string{ "--unit", unitName, - "--setenv", "XUI_MAIN_FOLDER="+mainFolder, - "--setenv", "XUI_SERVICE="+serviceFolder, - "--setenv", "XUI_UPDATE_TAG="+updateTag, + "--setenv", "XUI_MAIN_FOLDER=" + mainFolder, + "--setenv", "XUI_SERVICE=" + serviceFolder, + "--setenv", "XUI_UPDATE_TAG=" + updateTag, "--setenv", runIDEnv, "--setenv", statusFileEnv, - bash, "-lc", updateScript, - ) + } + for _, kv := range proxyEnv { + args = append(args, "--setenv", kv) + } + args = append(args, bash, "-lc", updateScript) + cmd := exec.CommandContext(context.Background(), systemdRun, args...) out, err := cmd.CombinedOutput() if err != nil { output := strings.TrimSpace(string(out)) @@ -298,6 +303,18 @@ func (s *PanelService) startUpdate(useDev bool) (int64, error) { return runID, nil } +// updateProxyEnvVars forwards ambient proxy env vars to systemd-run's child, +// which (unlike the bash fallback) inherits nothing but --setenv. +func updateProxyEnvVars() []string { + var out []string + for _, key := range []string{"https_proxy", "HTTPS_PROXY", "all_proxy", "ALL_PROXY", "http_proxy", "HTTP_PROXY", "no_proxy", "NO_PROXY"} { + if v := os.Getenv(key); v != "" { + out = append(out, key+"="+v) + } + } + return out +} + // acquireUpdateSlot claims the single in-flight-update slot for runID. It // refuses while another run is genuinely still in flight, but grants the // slot immediately once that run's own status file reports a terminal diff --git a/internal/web/service/panel/panel_test.go b/internal/web/service/panel/panel_test.go index cd5e642bf..e51717452 100644 --- a/internal/web/service/panel/panel_test.go +++ b/internal/web/service/panel/panel_test.go @@ -51,6 +51,47 @@ func TestShellQuote(t *testing.T) { } } +// TestUpdateProxyEnvVars covers the bug this function fixes: ambient proxy +// vars must reach update.sh's systemd-run child, which inherits nothing. +func TestUpdateProxyEnvVars(t *testing.T) { + allKeys := []string{"https_proxy", "HTTPS_PROXY", "all_proxy", "ALL_PROXY", "http_proxy", "HTTP_PROXY", "no_proxy", "NO_PROXY"} + clearAll := func(t *testing.T) { + t.Helper() + for _, key := range allKeys { + t.Setenv(key, "") + } + } + + t.Run("nothing set returns nil", func(t *testing.T) { + clearAll(t) + if got := updateProxyEnvVars(); got != nil { + t.Fatalf("updateProxyEnvVars() = %v, want nil", got) + } + }) + + t.Run("forwards each set var under its own name", func(t *testing.T) { + clearAll(t) + t.Setenv("https_proxy", "socks5://127.0.0.1:10808") + t.Setenv("no_proxy", "10.0.0.0/8,localhost") + got := updateProxyEnvVars() + want := []string{"https_proxy=socks5://127.0.0.1:10808", "no_proxy=10.0.0.0/8,localhost"} + if len(got) != len(want) || got[0] != want[0] || got[1] != want[1] { + t.Fatalf("updateProxyEnvVars() = %v, want %v", got, want) + } + }) + + // A deliberately HTTP-only proxy config must not silently gain HTTPS traffic. + t.Run("http_proxy is not promoted to https_proxy", func(t *testing.T) { + clearAll(t) + t.Setenv("http_proxy", "http://127.0.0.1:8080") + got := updateProxyEnvVars() + want := []string{"http_proxy=http://127.0.0.1:8080"} + if len(got) != len(want) || got[0] != want[0] { + t.Fatalf("updateProxyEnvVars() = %v, want %v", got, want) + } + }) +} + func TestExtractReleaseCommit(t *testing.T) { full := "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b" cases := []struct {