From 2130c405e0da72156d60446b033324973fa1c3d9 Mon Sep 17 00:00:00 2001 From: Bob Lee Date: Mon, 3 Aug 2026 08:58:32 -0700 Subject: [PATCH] fix(relay): route China deploys through mirrors --- src/apps/desktop/src/api/relay_deploy_api.rs | 17 ++- src/apps/relay-server/Dockerfile | 10 +- src/apps/relay-server/README.md | 8 +- src/apps/relay-server/mirror.sh | 16 ++- src/apps/relay-server/release-download.sh | 46 +++++- .../src/remote_ssh/relay_deploy.rs | 132 +++++++++++++++++- .../src/features/relay-deploy/README.md | 20 ++- .../relay-deploy/RelayDeployWizard.scss | 22 +++ .../relay-deploy/RelayDeployWizard.tsx | 27 +++- .../features/relay-deploy/relayDeployApi.ts | 15 +- src/web-ui/src/locales/en-US/common.json | 5 + src/web-ui/src/locales/zh-CN/common.json | 5 + src/web-ui/src/locales/zh-TW/common.json | 5 + 13 files changed, 297 insertions(+), 31 deletions(-) diff --git a/src/apps/desktop/src/api/relay_deploy_api.rs b/src/apps/desktop/src/api/relay_deploy_api.rs index 527e9bd58d..207c06d61d 100644 --- a/src/apps/desktop/src/api/relay_deploy_api.rs +++ b/src/apps/desktop/src/api/relay_deploy_api.rs @@ -11,7 +11,7 @@ //! `src/web-ui/src/features/relay-deploy/README.md`. use bitfun_core::service::remote_ssh::relay_deploy::{ - self, RelayDeployTask, RelayPreflight, RelayTaskPoll, RelayTaskStart, + self, RelayDeployTask, RelayMirrorMode, RelayPreflight, RelayTaskPoll, RelayTaskStart, }; use serde::Serialize; use tauri::State; @@ -39,14 +39,21 @@ pub async fn relay_deploy_preflight( pub async fn relay_deploy_install_docker( state: State<'_, AppState>, connection_id: String, + mirror_mode: Option, ) -> Result { let manager = state .get_ssh_manager_async() .await .map_err(|e| e.to_string())?; - relay_deploy::start_task(&manager, &connection_id, RelayDeployTask::InstallDocker, 0) - .await - .map_err(|e| e.to_string()) + relay_deploy::start_task( + &manager, + &connection_id, + RelayDeployTask::InstallDocker, + 0, + mirror_mode.unwrap_or_default(), + ) + .await + .map_err(|e| e.to_string()) } /// Stage the interactive deploy driver (run it in a remote PTY; poll via @@ -56,6 +63,7 @@ pub async fn relay_deploy_start( state: State<'_, AppState>, connection_id: String, port: Option, + mirror_mode: Option, ) -> Result { let manager = state .get_ssh_manager_async() @@ -66,6 +74,7 @@ pub async fn relay_deploy_start( &connection_id, RelayDeployTask::Deploy, port.unwrap_or(0), + mirror_mode.unwrap_or_default(), ) .await .map_err(|e| e.to_string()) diff --git a/src/apps/relay-server/Dockerfile b/src/apps/relay-server/Dockerfile index f0d185445e..254e1a343e 100644 --- a/src/apps/relay-server/Dockerfile +++ b/src/apps/relay-server/Dockerfile @@ -61,8 +61,9 @@ RUN set -eux; \ > /usr/local/cargo/config.toml; \ fi -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ +RUN apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=20 -o Acquire::https::Timeout=20 update \ + && apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=20 -o Acquire::https::Timeout=20 \ + install -y --no-install-recommends \ pkg-config \ build-essential \ ca-certificates \ @@ -145,8 +146,9 @@ RUN set -eux; \ /etc/apt/sources.list.d/debian.sources; \ fi; \ fi; \ - apt-get update \ - && apt-get install -y --no-install-recommends ca-certificates curl \ + apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=20 -o Acquire::https::Timeout=20 update \ + && apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=20 -o Acquire::https::Timeout=20 \ + install -y --no-install-recommends ca-certificates curl \ && rm -rf /var/lib/apt/lists/* WORKDIR /app diff --git a/src/apps/relay-server/README.md b/src/apps/relay-server/README.md index 2e7fb73a21..4ad46c7a1c 100644 --- a/src/apps/relay-server/README.md +++ b/src/apps/relay-server/README.md @@ -41,7 +41,9 @@ RELAY_CARGO_BUILD_JOBS=1 bash deploy.sh `deploy.sh` (and Desktop one-click deploy) auto-detects mainland China and configures host mirrors for apt, Docker Hub, and GitHub source retrieval plus a build-local Cargo/crates.io mirror. Docker Engine installation also uses a -mainland mirror. Override when needed: +mainland mirror. The Desktop wizard also offers **Auto / Mainland China / +Global** so an operator can override cloud IP geolocation without editing the +server environment. Override manual deploys when needed: ```bash BITFUN_MIRROR=cn bash deploy.sh # force China mirrors @@ -60,7 +62,9 @@ for the full list (`BITFUN_APT_MIRROR`, `BITFUN_DOCKER_REGISTRY_MIRRORS`, China mode does not modify the SSH user's global `~/.cargo/config.toml`; Cargo mirroring is scoped to the relay image build. Switching to `global` restores apt files disabled by BitFun and removes only Docker registry mirrors recorded -as BitFun additions. +as BitFun additions. The published-binary runtime image receives the same +choice as build args: Docker daemon mirrors cover the base-image pull, while +the apt mirror separately covers packages installed inside the image. `deploy.sh` enables Docker BuildKit so the Dockerfile can reuse Cargo registry/git/`target` cache mounts across redeploys. Keep BuildKit enabled diff --git a/src/apps/relay-server/mirror.sh b/src/apps/relay-server/mirror.sh index 47cbf58337..7f79c58131 100644 --- a/src/apps/relay-server/mirror.sh +++ b/src/apps/relay-server/mirror.sh @@ -18,6 +18,8 @@ # # Sets / exports (when mode=cn): # BITFUN_MIRROR_MODE=cn|global +# BITFUN_MIRROR_REQUESTED_MODE=auto|cn|global +# BITFUN_MIRROR_REASON= # BITFUN_USE_CN_MIRROR=0|1 # BITFUN_GITHUB_GIT_URL / BITFUN_GITHUB_TARBALL_URL # BITFUN_DOCKER_GET_URL @@ -217,16 +219,21 @@ bitfun_mirror_resolve_mode() { forced="$(echo "$forced" | tr '[:upper:]' '[:lower:]')" case "$forced" in cn|china|zh|zh-cn|zh_cn|1|true|yes) + export BITFUN_MIRROR_REQUESTED_MODE=cn export BITFUN_MIRROR_MODE=cn + export BITFUN_MIRROR_REASON=forced-cn export BITFUN_USE_CN_MIRROR=1 return 0 ;; global|intl|international|off|0|false|no|overseas) + export BITFUN_MIRROR_REQUESTED_MODE=global export BITFUN_MIRROR_MODE=global + export BITFUN_MIRROR_REASON=forced-global export BITFUN_USE_CN_MIRROR=0 return 0 ;; esac + export BITFUN_MIRROR_REQUESTED_MODE=auto # Already resolved in this shell (or exported by the caller). Detection costs # up to four HTTP lookups plus two 4s probes, and mirror.sh is initialised at @@ -234,10 +241,12 @@ bitfun_mirror_resolve_mode() { # because a network blip mid-deploy could flip the mode between steps. case "${BITFUN_MIRROR_MODE:-}" in cn) + export BITFUN_MIRROR_REASON="${BITFUN_MIRROR_REASON:-cached-cn}" export BITFUN_USE_CN_MIRROR=1 return 0 ;; global) + export BITFUN_MIRROR_REASON="${BITFUN_MIRROR_REASON:-cached-global}" export BITFUN_USE_CN_MIRROR=0 return 0 ;; @@ -248,6 +257,7 @@ bitfun_mirror_resolve_mode() { if [ "$country" = "CN" ]; then echo ">>> Region detect: public IP country=CN → China mirrors" export BITFUN_MIRROR_MODE=cn + export BITFUN_MIRROR_REASON=public-ip-cn export BITFUN_USE_CN_MIRROR=1 return 0 fi @@ -260,6 +270,7 @@ bitfun_mirror_resolve_mode() { if [ -n "$country" ]; then echo ">>> Region detect: public IP country=${country} → global mirrors" export BITFUN_MIRROR_MODE=global + export BITFUN_MIRROR_REASON="public-ip-${country}" export BITFUN_USE_CN_MIRROR=0 return 0 fi @@ -267,6 +278,7 @@ bitfun_mirror_resolve_mode() { if bitfun_mirror_timezone_suggests_cn; then echo ">>> Region detect: timezone suggests mainland China → China mirrors" export BITFUN_MIRROR_MODE=cn + export BITFUN_MIRROR_REASON=timezone-cn export BITFUN_USE_CN_MIRROR=1 return 0 fi @@ -274,12 +286,14 @@ bitfun_mirror_resolve_mode() { if bitfun_mirror_connectivity_suggests_cn; then echo ">>> Region detect: GitHub unreachable + Aliyun reachable → China mirrors" export BITFUN_MIRROR_MODE=cn + export BITFUN_MIRROR_REASON=connectivity-cn export BITFUN_USE_CN_MIRROR=1 return 0 fi echo ">>> Region detect: inconclusive → global mirrors" export BITFUN_MIRROR_MODE=global + export BITFUN_MIRROR_REASON=inconclusive-global export BITFUN_USE_CN_MIRROR=0 return 0 } @@ -1224,7 +1238,7 @@ bitfun_mirror_init() { bitfun_mirror_parse_args "$@" bitfun_mirror_resolve_mode bitfun_mirror_export_urls - echo ">>> Mirror mode: ${BITFUN_MIRROR_MODE} (BITFUN_USE_CN_MIRROR=${BITFUN_USE_CN_MIRROR})" + echo ">>> Mirror mode: ${BITFUN_MIRROR_MODE} (requested=${BITFUN_MIRROR_REQUESTED_MODE:-auto}, reason=${BITFUN_MIRROR_REASON:-unknown}, BITFUN_USE_CN_MIRROR=${BITFUN_USE_CN_MIRROR})" if [ "${BITFUN_MIRROR_MODE}" = "cn" ]; then echo ">>> GitHub git URL: ${BITFUN_GITHUB_GIT_URL}" echo ">>> GitHub tarball URL: ${BITFUN_GITHUB_TARBALL_URL}" diff --git a/src/apps/relay-server/release-download.sh b/src/apps/relay-server/release-download.sh index fd75ca2bf7..a726111bd2 100644 --- a/src/apps/relay-server/release-download.sh +++ b/src/apps/relay-server/release-download.sh @@ -19,6 +19,9 @@ # BITFUN_OPENBITFUN_RELEASE_BASE https://openbitfun.com/release # BITFUN_GITHUB_PROXY prefix-style proxy, set by mirror.sh in CN # BITFUN_MIRROR_MODE cn | global, set by mirror.sh +# BITFUN_USE_CN_MIRROR 1 switches apt inside the runtime image +# BITFUN_APT_MIRROR Debian mirror host (default mirrors.aliyun.com) +# BITFUN_RUNTIME_BASE runtime base image (default debian:trixie-slim) # RELAY_PORT published port (default 9700) # RELAY_HOST_BIND_IP bind address (default 0.0.0.0, as compose) # @@ -129,6 +132,24 @@ bitfun_canonical_checksum_url() { # source-build path that follows. bitfun_build_runtime_image() { local image="$1" context="$2" rc=1 + local use_cn_mirror="${BITFUN_USE_CN_MIRROR:-0}" + local apt_mirror="${BITFUN_APT_MIRROR:-mirrors.aliyun.com}" + local runtime_base="${BITFUN_RUNTIME_BASE:-debian:trixie-slim}" + + # The Docker daemon's registry-mirrors accelerate the FROM pull. These build + # args are a separate, equally necessary hop: apt runs *inside* the temporary + # runtime image and cannot see the host's /etc/apt sources. Losing these args + # made correctly detected CN hosts still contact deb.debian.org here. + local build_args=( + --build-arg "BITFUN_USE_CN_MIRROR=${use_cn_mirror}" + --build-arg "BITFUN_APT_MIRROR=${apt_mirror}" + --build-arg "BITFUN_RUNTIME_BASE=${runtime_base}" + ) + if [ "$use_cn_mirror" = "1" ]; then + echo ">>> Runtime image network route: China (apt=${apt_mirror}; Docker registry mirrors are daemon-managed)" + else + echo ">>> Runtime image network route: global (official Debian apt and image registry)" + fi # A config dir this user definitely owns. Empty if it cannot be created, in # which case the retries keep the inherited DOCKER_CONFIG. @@ -158,7 +179,7 @@ bitfun_build_runtime_image() { export DOCKER_BUILDKIT=0 ;; esac - bitfun_docker build -t "$image" "$context" + bitfun_docker build "${build_args[@]}" -t "$image" "$context" ); then rc=0 break @@ -406,10 +427,27 @@ bitfun_try_release_deploy() { # every release a client in the wild might still install. trixie-slim carries # glibc 2.41 and covers both 2.38 and 2.35. cat >"$context.new/Dockerfile" <<'DOCKERFILE' -FROM debian:trixie-slim +ARG BITFUN_RUNTIME_BASE=debian:trixie-slim +FROM ${BITFUN_RUNTIME_BASE} +ARG BITFUN_USE_CN_MIRROR=0 +ARG BITFUN_APT_MIRROR=mirrors.aliyun.com ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update \ - && apt-get install -y --no-install-recommends ca-certificates curl \ +RUN set -eux; \ + if [ "${BITFUN_USE_CN_MIRROR}" = "1" ]; then \ + sed -i \ + -e "s|deb.debian.org/debian|${BITFUN_APT_MIRROR}/debian|g" \ + -e "s|security.debian.org/debian-security|${BITFUN_APT_MIRROR}/debian-security|g" \ + /etc/apt/sources.list 2>/dev/null || true; \ + if [ -f /etc/apt/sources.list.d/debian.sources ]; then \ + sed -i \ + -e "s|deb.debian.org/debian|${BITFUN_APT_MIRROR}/debian|g" \ + -e "s|security.debian.org/debian-security|${BITFUN_APT_MIRROR}/debian-security|g" \ + /etc/apt/sources.list.d/debian.sources; \ + fi; \ + fi; \ + apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=20 -o Acquire::https::Timeout=20 update \ + && apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=20 -o Acquire::https::Timeout=20 \ + install -y --no-install-recommends ca-certificates curl \ && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY bitfun-relay-server relay-admin /app/ diff --git a/src/crates/services/services-integrations/src/remote_ssh/relay_deploy.rs b/src/crates/services/services-integrations/src/remote_ssh/relay_deploy.rs index b2f3ff8b4d..926dc6da5a 100644 --- a/src/crates/services/services-integrations/src/remote_ssh/relay_deploy.rs +++ b/src/crates/services/services-integrations/src/remote_ssh/relay_deploy.rs @@ -118,6 +118,30 @@ impl RelayDeployTask { } } +/// Network route used by relay deployment downloads and image builds. +/// +/// `Auto` keeps server-side detection as the default. The explicit variants +/// are a user-facing escape hatch for cloud IPs whose geolocation or outbound +/// routing does not reflect where the server is actually hosted. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RelayMirrorMode { + #[default] + Auto, + Cn, + Global, +} + +impl RelayMirrorMode { + fn as_str(self) -> &'static str { + match self { + Self::Auto => "auto", + Self::Cn => "cn", + Self::Global => "global", + } + } +} + /// Fine-grained Docker access classification for the current SSH session. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -426,6 +450,7 @@ pub async fn start_task( connection_id: &str, task: RelayDeployTask, port: u16, + mirror_mode: RelayMirrorMode, ) -> Result { let home = resolve_home(manager, connection_id).await?; let dir = format!("{home}/{DEPLOY_STATE_DIR}"); @@ -464,6 +489,7 @@ pub async fn start_task( let body_path = format!("{dir}/{stem}-body.sh"); let script_path = format!("{dir}/{stem}.sh"); let port_path = format!("{dir}/relay.port"); + let mirror_mode_path = format!("{dir}/relay.mirror-mode"); // Upload as LF-only: bash on the relay host runs a stray CR as a command. let body = to_unix_script(&body); let driver = to_unix_script(&driver); @@ -473,6 +499,13 @@ pub async fn start_task( manager .sftp_write(connection_id, &script_path, driver.as_bytes()) .await?; + manager + .sftp_write( + connection_id, + &mirror_mode_path, + format!("{}\n", mirror_mode.as_str()).as_bytes(), + ) + .await?; if matches!(task, RelayDeployTask::Deploy) { manager .sftp_write(connection_id, &port_path, format!("{port}\n").as_bytes()) @@ -1167,7 +1200,7 @@ bitfun_run_deploy_sh() { local dir="$1" local port="${RELAY_PORT:-9700}" # Prefer already-resolved mirror mode so deploy.sh does not re-probe. - local mirror_mode="${BITFUN_MIRROR:-${BITFUN_MIRROR_MODE:-auto}}" + local mirror_mode="${BITFUN_MIRROR_MODE:-${BITFUN_MIRROR:-auto}}" # Always --build-from-source: this function is reached ONLY after # bitfun_try_release_deploy already failed, and deploy.sh's own first step is # that same release-binary path. Without the flag it re-downloads, re-builds @@ -1258,6 +1291,7 @@ if [ -n "${{BITFUN_KEEP_HOME:-}}" ]; then DRIVER_PIDF="$D/$STEM.driver.pid" fi PREPARE_FLAG="$D/$STEM.preparing" +MIRROR_MODE_FILE="$D/relay.mirror-mode" # Re-claim the prepare phase: an elevated re-exec is a different process, and D # may have moved with HOME. echo $$ >"$DRIVER_PIDF" @@ -1272,6 +1306,13 @@ trap cleanup_prepare EXIT # Region/mirrors before apt tool install and Docker/GitHub downloads. export BITFUN_REPO_GIT_URL="{REPO_GIT_URL}" export BITFUN_REPO_TARBALL_URL="{REPO_TARBALL_URL}" +if [ -f "$MIRROR_MODE_FILE" ]; then + requested_mirror_mode="$(tr -d '[:space:]' < "$MIRROR_MODE_FILE")" + case "$requested_mirror_mode" in + auto|cn|global) export BITFUN_MIRROR="$requested_mirror_mode" ;; + *) echo "ERROR: invalid relay mirror mode: $requested_mirror_mode" >&2; exit 1 ;; + esac +fi bitfun_mirror_init bitfun_ensure_tools export DOCKER_CONFIG="${{DOCKER_CONFIG:-$HOME/.bitfun/docker-config}}" @@ -1312,11 +1353,15 @@ if [ "{kind}" = "install" ]; then set +e if command -v stdbuf >/dev/null 2>&1; then stdbuf -oL -eL env BITFUN_KEEP_HOME="$BITFUN_KEEP_HOME" \ - BITFUN_MIRROR="${{BITFUN_MIRROR:-${{BITFUN_MIRROR_MODE:-auto}}}}" \ + BITFUN_MIRROR="${{BITFUN_MIRROR:-auto}}" \ + BITFUN_MIRROR_MODE="${{BITFUN_MIRROR_MODE:-}}" \ + BITFUN_MIRROR_REASON="${{BITFUN_MIRROR_REASON:-}}" \ bash "$BODY" 2>&1 | tee -a "$LOG" else env BITFUN_KEEP_HOME="$BITFUN_KEEP_HOME" \ - BITFUN_MIRROR="${{BITFUN_MIRROR:-${{BITFUN_MIRROR_MODE:-auto}}}}" \ + BITFUN_MIRROR="${{BITFUN_MIRROR:-auto}}" \ + BITFUN_MIRROR_MODE="${{BITFUN_MIRROR_MODE:-}}" \ + BITFUN_MIRROR_REASON="${{BITFUN_MIRROR_REASON:-}}" \ bash "$BODY" 2>&1 | tee -a "$LOG" fi code=${{PIPESTATUS[0]}} @@ -1336,7 +1381,9 @@ echo ">>> Starting background task (log: $LOG)" | tee -a "$LOG" nohup env BITFUN_DOCKER_MODE="$BITFUN_DOCKER_MODE" DOCKER_CONFIG="$DOCKER_CONFIG" \ RELAY_CARGO_BUILD_JOBS="${{RELAY_CARGO_BUILD_JOBS:-}}" \ DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 BUILDKIT_PROGRESS=plain \ - BITFUN_MIRROR="${{BITFUN_MIRROR:-${{BITFUN_MIRROR_MODE:-auto}}}}" \ + BITFUN_MIRROR="${{BITFUN_MIRROR:-auto}}" \ + BITFUN_MIRROR_MODE="${{BITFUN_MIRROR_MODE:-}}" \ + BITFUN_MIRROR_REASON="${{BITFUN_MIRROR_REASON:-}}" \ BITFUN_USE_CN_MIRROR="${{BITFUN_USE_CN_MIRROR:-0}}" \ BITFUN_APT_MIRROR="${{BITFUN_APT_MIRROR:-}}" \ BITFUN_CARGO_SPARSE_URL="${{BITFUN_CARGO_SPARSE_URL:-}}" \ @@ -1802,8 +1849,8 @@ mod tests { install_docker_body_script, interactive_driver_script, parse_preflight, prepare_helpers_bash, release_binary_deploy_bash, release_tag_for_version, split_poll_stdout, stage_scripts_command, sync_source_bash, to_unix_script, - verified_checksum_exports, verify_minisign, DockerAccessMode, - RelayTaskStatus, RELAY_MIRROR_SH, RELAY_RELEASE_DOWNLOAD_SH, RELEASE_PUBKEY, + verified_checksum_exports, verify_minisign, DockerAccessMode, RelayTaskStatus, + RELAY_MIRROR_SH, RELAY_RELEASE_DOWNLOAD_SH, RELEASE_PUBKEY, }; #[test] @@ -1828,6 +1875,10 @@ mod tests { RELAY_MIRROR_SH.contains("bitfun_mirror_restore_host"), "mirror.sh must support switching a managed host back to global mode" ); + assert!( + RELAY_MIRROR_SH.contains("BITFUN_MIRROR_REASON"), + "mirror selection must log why auto detection chose its route" + ); assert!( !RELAY_MIRROR_SH.contains("data[\"bitfun-cn-mirror\"]"), "daemon.json must contain only dockerd-supported directives" @@ -1845,6 +1896,12 @@ mod tests { helpers.contains("bitfun_run_deploy_sh"), "prepare helpers must keep deploy runner" ); + let driver = interactive_driver_script("deploy", "deploy"); + assert!( + driver.contains("relay.mirror-mode") + && driver.contains("auto|cn|global) export BITFUN_MIRROR"), + "the wizard's explicit mirror choice must reach remote preparation" + ); } /// A CRLF checkout (Git for Windows' `core.autocrlf=true` default) used to @@ -2125,7 +2182,8 @@ sh -c "$(bitfun_shell_join printf '%s\n' 'a b' "it's" '{{{{.State.Running}}}}' ' "bookworm-slim (glibc 2.36) cannot load the arm64 relay (needs 2.38)" ); assert!( - script.contains("FROM debian:trixie-slim"), + script.contains("ARG BITFUN_RUNTIME_BASE=debian:trixie-slim") + && script.contains("FROM ${BITFUN_RUNTIME_BASE}"), "runtime base must provide a glibc at least as new as the release matrix" ); // `ldd` exits 0 even when it reports an unsatisfied symbol version, so @@ -2134,6 +2192,66 @@ sh -c "$(bitfun_shell_join printf '%s\n' 'a b' "it's" '{{{{.State.Running}}}}' ' script.contains(r#"*"not found"*)"#), "the runtime image must fail its build on an unloadable binary" ); + assert!( + script.contains("ARG BITFUN_USE_CN_MIRROR=0") + && script.contains("BITFUN_APT_MIRROR=mirrors.aliyun.com") + && script.contains("deb.debian.org/debian"), + "the generated runtime image must be able to rewrite its own apt sources" + ); + } + + /// Host apt configuration cannot affect `apt-get` inside a Docker build. + /// The release path therefore has to pass the resolved route as build args; + /// this is the exact propagation gap that made CN hosts hit deb.debian.org. + #[cfg(unix)] + #[test] + fn runtime_image_build_receives_resolved_mirror_args() { + use std::{fs, process::Command}; + + let dir = tempfile::tempdir().expect("temp dir"); + let script_path = dir.path().join("release-download.sh"); + let context = dir.path().join("runtime"); + let trace = dir.path().join("docker-args"); + fs::write(&script_path, release_binary_deploy_bash()).expect("write release script"); + fs::create_dir(&context).expect("create runtime context"); + + let output = Command::new("bash") + .arg("-c") + .arg( + r#" +set -euo pipefail +source "$1" +export TRACE="$3" +bitfun_docker() { printf '%s\n' "$@" > "$TRACE"; } +export BITFUN_USE_CN_MIRROR=1 +export BITFUN_APT_MIRROR=mirror.example +export BITFUN_RUNTIME_BASE=registry.example/library/debian:trixie-slim +bitfun_build_runtime_image bitfun-relay:test "$2" +"#, + ) + .arg("runtime-mirror-args") + .arg(&script_path) + .arg(&context) + .arg(&trace) + .output() + .expect("run runtime image build stub"); + assert!( + output.status.success(), + "runtime build stub failed:\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let args = fs::read_to_string(trace).expect("read docker args"); + for expected in [ + "BITFUN_USE_CN_MIRROR=1", + "BITFUN_APT_MIRROR=mirror.example", + "BITFUN_RUNTIME_BASE=registry.example/library/debian:trixie-slim", + ] { + assert!( + args.lines().any(|line| line == expected), + "docker build is missing {expected}:\n{args}" + ); + } } /// `docker logs` relays the container's stderr on its own stderr, and the diff --git a/src/web-ui/src/features/relay-deploy/README.md b/src/web-ui/src/features/relay-deploy/README.md index f60c4bebee..f5a1b8745b 100644 --- a/src/web-ui/src/features/relay-deploy/README.md +++ b/src/web-ui/src/features/relay-deploy/README.md @@ -93,10 +93,22 @@ Desktop Tauri surface: `src/apps/desktop/src/api/relay_deploy_api.rs` 15. **China mirrors before overseas downloads.** Desktop orchestration embeds `src/apps/relay-server/mirror.sh` and runs `bitfun_mirror_init` before apt tool install, Docker Engine install, and GitHub sync. `deploy.sh` sources - the same file so manual and one-click paths stay aligned. Force with - `BITFUN_MIRROR=cn|global`. Docker daemon metadata must stay outside - `daemon.json`; host Cargo config must remain untouched; global mode rolls - back only BitFun-managed apt and Docker entries. + the same file so manual and one-click paths stay aligned. The wizard exposes + `auto | cn | global`; the selected value is staged on the host and must reach + Docker install, published-binary deploy, and the source fallback unchanged. + Manual deploy can force the same choice with `BITFUN_MIRROR=cn|global`. + Docker daemon metadata must stay outside `daemon.json`; host Cargo config + must remain untouched; global mode rolls back only BitFun-managed apt and + Docker entries. + +15a. **A runtime-image build has two independent mirror hops.** Docker daemon + `registry-mirrors` accelerates `FROM debian:trixie-slim`; it does not affect + the `apt-get` that later runs inside that image. The generated release + Dockerfile must therefore receive `BITFUN_USE_CN_MIRROR` and + `BITFUN_APT_MIRROR` as build args, just like the source-build Dockerfile. + Keep bounded apt retries/timeouts on both paths. A log line that says + `Mirror mode: cn` followed by `deb.debian.org` is a propagation regression, + not a failed region detection. 16. **Scripts on the relay host are LF-only, in three independent layers.** `include_str!` and the `r#"..."#` remote templates both inherit the diff --git a/src/web-ui/src/features/relay-deploy/RelayDeployWizard.scss b/src/web-ui/src/features/relay-deploy/RelayDeployWizard.scss index b34fb976c5..ddc6fa7a9f 100644 --- a/src/web-ui/src/features/relay-deploy/RelayDeployWizard.scss +++ b/src/web-ui/src/features/relay-deploy/RelayDeployWizard.scss @@ -389,6 +389,28 @@ min-width: 160px; } + &__mirror-row { + display: flex; + align-items: flex-end; + gap: 12px; + margin: 0 16px 10px; + flex-wrap: wrap; + } + + &__field--mirror { + width: 180px; + flex-shrink: 0; + } + + &__mirror-hint { + margin: 0 0 6px; + font-size: 11px; + color: var(--bf-appearance-token-color-text-muted); + line-height: 1.4; + flex: 1; + min-width: 200px; + } + &__notice { display: flex; align-items: flex-start; diff --git a/src/web-ui/src/features/relay-deploy/RelayDeployWizard.tsx b/src/web-ui/src/features/relay-deploy/RelayDeployWizard.tsx index 136afe9b91..42708ee242 100644 --- a/src/web-ui/src/features/relay-deploy/RelayDeployWizard.tsx +++ b/src/web-ui/src/features/relay-deploy/RelayDeployWizard.tsx @@ -34,6 +34,7 @@ import { type RelayTaskStatus, type RelayVerifyResult, type DockerAccessMode, + type RelayMirrorMode, } from './relayDeployApi'; import { ConnectedTerminal, getTerminalService } from '@/tools/terminal'; import { createLogger } from '@/shared/utils/logger'; @@ -142,6 +143,7 @@ export const RelayDeployWizard: React.FC = ({ const [preflight, setPreflight] = useState(null); const [preflightLoading, setPreflightLoading] = useState(false); const [relayPortInput, setRelayPortInput] = useState(String(DEFAULT_RELAY_PORT)); + const [mirrorMode, setMirrorMode] = useState('auto'); // ── interactive PTY task (install docker / deploy) ─────────────────────── const [activeTask, setActiveTask] = useState(null); @@ -268,6 +270,7 @@ export const RelayDeployWizard: React.FC = ({ setPreflight(null); setPreflightLoading(false); setRelayPortInput(String(DEFAULT_RELAY_PORT)); + setMirrorMode('auto'); setActiveTask(null); setTaskStatus(null); setRegUsername(''); @@ -578,7 +581,7 @@ export const RelayDeployWizard: React.FC = ({ setTaskStatus('running'); setActiveTask('install_docker'); try { - const started = await relayDeployApi.installDocker(connectionId); + const started = await relayDeployApi.installDocker(connectionId, mirrorMode); await launchInteractiveTask('install_docker', connectionId, started.scriptPath); } catch (e) { setTaskStatus('failed'); @@ -598,7 +601,7 @@ export const RelayDeployWizard: React.FC = ({ setTaskStatus('running'); setActiveTask('deploy'); try { - const started = await relayDeployApi.startDeploy(connectionId, port); + const started = await relayDeployApi.startDeploy(connectionId, port, mirrorMode); await launchInteractiveTask('deploy', connectionId, started.scriptPath); } catch (e) { setTaskStatus('failed'); @@ -745,6 +748,12 @@ export const RelayDeployWizard: React.FC = ({ { label: t('ssh.remote.privateKey'), value: 'privateKey', icon: }, ]; + const mirrorModeOptions = [ + { label: t('relayDeploy.mirrorModeAuto'), value: 'auto' }, + { label: t('relayDeploy.mirrorModeCn'), value: 'cn' }, + { label: t('relayDeploy.mirrorModeGlobal'), value: 'global' }, + ]; + // ── step renderers ─────────────────────────────────────────────────────── const renderConnect = () => (
@@ -990,6 +999,20 @@ export const RelayDeployWizard: React.FC = ({

{t('relayDeploy.relayPortHint')}

+
+
+ +