Browse Source

fix(panel): forward the panel's proxy to update.sh's own downloads (#6259)

* fix(panel): route update.sh's own downloads through the resolved proxy

startUpdate already fetches update.sh itself via a proxy-aware HTTP
client (NewProxiedHTTPClient), but the process that actually runs it
never got a proxy hint of its own -- so update.sh's own curl calls to
GitHub always went direct, even when the panel has a working proxy
path configured. This matters most for the systemd-run launch path,
which doesn't inherit the caller's environment at all (only --setenv
passes through), so a systemd host with a real ambient proxy would
silently lose it for this one hop.

curl already honors https_proxy/all_proxy natively, so no changes to
update.sh itself are needed -- only the launcher needs to forward a
proxy URL into the environment it hands to that detached process.

updateProxyEnvVars() prefers an already-set ambient proxy env var
(never silently overriding an admin's own proxy config) and only
falls back to the panel's own configured panel outbound
(PanelEgressProxyURL) when nothing is set, then forwards the result
to both launch paths.

* test(panel): cover updateProxyEnvVars' ambient-proxy path

Regression test for the fix in the previous commit -- an ambient
https_proxy must reach update.sh's own downloads, not just the
panel's own outbound requests. Scoped to the ambient-env branch only,
which never touches PanelEgressProxyURL/the database.

* fix: drop the panel-outbound fallback in updateProxyEnvVars

Per review: PanelEgressProxyURL() returns a loopback SOCKS bridge
living inside the panel's own Xray child. update.sh stops that child
partway through its run (systemctl stop x-ui, no KillMode override --
the default cgroup kill takes Xray with it) and removes the service
unit, but still needs curl afterwards for x-ui.sh and sometimes the
service unit itself. With the bridge dead, those downloads fail and
update.sh exits with no service unit installed and nothing to restart
it -- a host with a panel outbound configured and no ambient proxy
would be bricked by its next update.

Keep only the ambient-env-var forwarding, which is safe (an OS-level
var, not torn down when the panel dies), and fold in three smaller
fixes: forward no_proxy/NO_PROXY too, since install_base's apt/dnf
calls honor them; stop promoting a deliberately HTTP-only http_proxy
into https_proxy/all_proxy; and drop the now-redundant re-append on
the bash fallback path, which already inherits everything via
os.Environ().
Kuzz007 10 giờ trước cách đây
mục cha
commit
9408424959

+ 23 - 6
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

+ 41 - 0
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 {