From 724b4a59d50e11d324c699d2a59ea3155b3239a0 Mon Sep 17 00:00:00 2001 From: InfinityPacer <160988576+InfinityPacer@users.noreply.github.com> Date: Tue, 23 Jun 2026 20:47:43 +0800 Subject: [PATCH] fix(docker): scope package proxy env (#5991) * Revert "Add NO_PROXY defaults to Docker proxy setup" This reverts commit 109be58abefc31c9e9cc50b195c8ea57432d2f3c. * fix(docker): scope package proxy env --- docker/entrypoint.sh | 52 +++----------- docker/update.sh | 54 ++++---------- scripts/dev/simulate_docker_package_env.sh | 82 +++++++++++++++++----- tests/test_docker_proxy_env.py | 81 --------------------- 4 files changed, 91 insertions(+), 178 deletions(-) delete mode 100644 tests/test_docker_proxy_env.py diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 4a852cff..27346d82 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -35,43 +35,15 @@ function apply_package_cache_env() { mkdir -p "${PIP_CACHE_DIR}" "${UV_CACHE_DIR}" } -function apply_no_proxy_env() { - local default_no_proxy="localhost,127.0.0.1,::1,0.0.0.0,10.0.0.0/8,100.64.0.0/10,169.254.0.0/16,172.16.0.0/12,192.168.0.0/16,fc00::/7,fe80::/10,host.docker.internal,host.containers.internal,gateway.docker.internal,.local,.lan,.internal,.home.arpa,.localdomain" - local merged="${NO_PROXY:-}" - local source_value item old_ifs - local -a no_proxy_items - - # Docker 内常见本机、局域网和内网服务必须直连,避免 PROXY_HOST 被映射到 - # HTTP_PROXY 后拦截 Telegram 回调、下载器、媒体服务器等本地请求。 - for source_value in "${no_proxy:-}" "${default_no_proxy}"; do - old_ifs="${IFS}" - IFS=',' - read -ra no_proxy_items <<< "${source_value}" - IFS="${old_ifs}" - for item in "${no_proxy_items[@]}"; do - item="${item#"${item%%[![:space:]]*}"}" - item="${item%"${item##*[![:space:]]}"}" - if [ -z "${item}" ]; then - continue - fi - case ",${merged}," in - *",${item},"*) ;; - *) merged="${merged:+${merged},}${item}" ;; - esac - done - done - - export NO_PROXY="${merged}" - export no_proxy="${merged}" -} - -function apply_package_proxy_env() { - if [ -n "${PROXY_HOST:-}" ]; then - export HTTP_PROXY="${PROXY_HOST}" - export HTTPS_PROXY="${PROXY_HOST}" - export http_proxy="${PROXY_HOST}" - export https_proxy="${PROXY_HOST}" - apply_no_proxy_env +function run_package_command() { + if [ -n "${PROXY_HOST}" ]; then + HTTP_PROXY="${PROXY_HOST}" \ + HTTPS_PROXY="${PROXY_HOST}" \ + http_proxy="${PROXY_HOST}" \ + https_proxy="${PROXY_HOST}" \ + "$@" + else + "$@" fi } @@ -198,9 +170,6 @@ function load_config_from_app_env() { done shopt -u extglob - if [ -n "${PROXY_HOST:-}${HTTP_PROXY:-}${HTTPS_PROXY:-}${http_proxy:-}${https_proxy:-}" ]; then - apply_no_proxy_env - fi INFO "配置加载流程执行完毕。" } @@ -328,13 +297,12 @@ function ensure_backend_runtime_dependencies() { fi WARN "→ 检测到后端核心依赖异常,开始尝试恢复主程序依赖..." - apply_package_proxy_env local -a pip_cmd=("${VENV_PATH}/bin/pip" "install" "-r" "/app/requirements.txt") if [ -n "${PIP_PROXY}" ]; then pip_cmd+=("-i" "${PIP_PROXY}") fi - if ! "${pip_cmd[@]}" > /dev/stdout 2> /dev/stderr; then + if ! run_package_command "${pip_cmd[@]}" > /dev/stdout 2> /dev/stderr; then ERROR "→ 自动恢复主程序依赖失败,后端无法启动。" diagnostic_keepalive 1 fi diff --git a/docker/update.sh b/docker/update.sh index f6f83ed4..19620720 100644 --- a/docker/update.sh +++ b/docker/update.sh @@ -36,42 +36,17 @@ function apply_package_cache_env() { apply_package_cache_env -function apply_no_proxy_env() { - local default_no_proxy="localhost,127.0.0.1,::1,0.0.0.0,10.0.0.0/8,100.64.0.0/10,169.254.0.0/16,172.16.0.0/12,192.168.0.0/16,fc00::/7,fe80::/10,host.docker.internal,host.containers.internal,gateway.docker.internal,.local,.lan,.internal,.home.arpa,.localdomain" - local merged="${NO_PROXY:-}" - local source_value item old_ifs - local -a no_proxy_items +PIP_ENV=() - # 代理仅用于外网依赖下载,容器访问本机、局域网和内网服务时应保持直连。 - for source_value in "${no_proxy:-}" "${default_no_proxy}"; do - old_ifs="${IFS}" - IFS=',' - read -ra no_proxy_items <<< "${source_value}" - IFS="${old_ifs}" - for item in "${no_proxy_items[@]}"; do - item="${item#"${item%%[![:space:]]*}"}" - item="${item%"${item##*[![:space:]]}"}" - if [[ -z "${item}" ]]; then - continue - fi - case ",${merged}," in - *",${item},"*) ;; - *) merged="${merged:+${merged},}${item}" ;; - esac - done - done - - export NO_PROXY="${merged}" - export no_proxy="${merged}" -} - -function apply_package_proxy_env() { - if [[ -n "${PROXY_HOST:-}" ]]; then - export HTTP_PROXY="${PROXY_HOST}" - export HTTPS_PROXY="${PROXY_HOST}" - export http_proxy="${PROXY_HOST}" - export https_proxy="${PROXY_HOST}" - apply_no_proxy_env +function set_package_proxy_env() { + PIP_ENV=() + if [[ -n "${PROXY_HOST}" ]]; then + PIP_ENV=( + "HTTP_PROXY=${PROXY_HOST}" + "HTTPS_PROXY=${PROXY_HOST}" + "http_proxy=${PROXY_HOST}" + "https_proxy=${PROXY_HOST}" + ) fi } @@ -120,13 +95,13 @@ function install_backend_and_download_resources() { # 复制新的requirements.in cp "${TMP_PATH}/App/requirements.in" /app/requirements.in # 重新编译依赖 - if ! ${VENV_PATH}/bin/pip-compile /app/requirements.in -o /app/requirements.txt; then + if ! env "${PIP_ENV[@]}" ${VENV_PATH}/bin/pip-compile /app/requirements.in -o /app/requirements.txt; then ERROR "依赖编译失败,恢复原依赖" cp /tmp/requirements.txt.backup /app/requirements.txt return 1 fi # 安装新依赖 - if ! ${VENV_PATH}/bin/pip install ${PIP_OPTIONS} -r /app/requirements.txt; then + if ! env "${PIP_ENV[@]}" ${VENV_PATH}/bin/pip install ${PIP_OPTIONS} -r /app/requirements.txt; then ERROR "依赖安装失败,恢复原依赖" cp /tmp/requirements.txt.backup /app/requirements.txt return 1 @@ -236,7 +211,7 @@ function test_connectivity_pip() { if [[ $? -eq 0 ]]; then PIP_OPTIONS="-i ${PIP_PROXY}" PIP_LOG="镜像代理模式" - apply_package_proxy_env + set_package_proxy_env return 0 fi fi @@ -248,13 +223,14 @@ function test_connectivity_pip() { ${VENV_PATH}/bin/pip install pip-hello-world > /dev/null 2>&1; then PIP_OPTIONS="" PIP_LOG="全局代理模式" - apply_package_proxy_env + set_package_proxy_env return 0 fi fi return 1 ;; 2) + PIP_ENV=() PIP_OPTIONS="" PIP_LOG="不使用代理" return 0 diff --git a/scripts/dev/simulate_docker_package_env.sh b/scripts/dev/simulate_docker_package_env.sh index c325eed7..940083e5 100644 --- a/scripts/dev/simulate_docker_package_env.sh +++ b/scripts/dev/simulate_docker_package_env.sh @@ -12,8 +12,6 @@ cat > "${TMP_DIR}/venv/bin/pip" <<'SH' printf 'argv=%s\n' "$*" >> "${MP_FAKE_PIP_LOG}" printf 'HTTP_PROXY=%s\n' "${HTTP_PROXY:-}" >> "${MP_FAKE_PIP_LOG}" printf 'HTTPS_PROXY=%s\n' "${HTTPS_PROXY:-}" >> "${MP_FAKE_PIP_LOG}" -printf 'NO_PROXY=%s\n' "${NO_PROXY:-}" >> "${MP_FAKE_PIP_LOG}" -printf 'no_proxy=%s\n' "${no_proxy:-}" >> "${MP_FAKE_PIP_LOG}" printf 'PACKAGE_CACHE_ROOT=%s\n' "${PACKAGE_CACHE_ROOT:-}" >> "${MP_FAKE_PIP_LOG}" printf 'PIP_CACHE_DIR=%s\n' "${PIP_CACHE_DIR:-}" >> "${MP_FAKE_PIP_LOG}" printf 'UV_CACHE_DIR=%s\n' "${UV_CACHE_DIR:-}" >> "${MP_FAKE_PIP_LOG}" @@ -58,18 +56,13 @@ export CONFIG_DIR="${TMP_DIR}/config" export MOVIEPILOT_AUTO_UPDATE=false export PIP_PROXY="https://mirror.example/simple" export PROXY_HOST="http://proxy.example:7890" -export NO_PROXY="custom.internal,127.0.0.1" -unset no_proxy -unset PACKAGE_CACHE_ROOT PIP_CACHE_DIR UV_CACHE_DIR +unset PACKAGE_CACHE_ROOT PIP_CACHE_DIR UV_CACHE_DIR HTTP_PROXY HTTPS_PROXY http_proxy https_proxy source "${UPDATE_FUNCS}" >/dev/null : > "${MP_FAKE_PIP_LOG}" test_connectivity_pip 0 assert_contains "argv=install -i https://mirror.example/simple pip-hello-world" "${MP_FAKE_PIP_LOG}" assert_contains "HTTPS_PROXY=http://proxy.example:7890" "${MP_FAKE_PIP_LOG}" -assert_contains "NO_PROXY=custom.internal,127.0.0.1,localhost,::1,0.0.0.0,10.0.0.0/8,100.64.0.0/10,169.254.0.0/16,172.16.0.0/12,192.168.0.0/16" "${MP_FAKE_PIP_LOG}" -assert_contains "host.docker.internal,host.containers.internal,gateway.docker.internal" "${MP_FAKE_PIP_LOG}" -assert_contains "no_proxy=custom.internal,127.0.0.1,localhost,::1,0.0.0.0,10.0.0.0/8,100.64.0.0/10" "${MP_FAKE_PIP_LOG}" assert_contains "PACKAGE_CACHE_ROOT=${TMP_DIR}/config/.cache" "${MP_FAKE_PIP_LOG}" assert_contains "PIP_CACHE_DIR=${TMP_DIR}/config/.cache/pip" "${MP_FAKE_PIP_LOG}" assert_contains "UV_CACHE_DIR=${TMP_DIR}/config/.cache/uv" "${MP_FAKE_PIP_LOG}" @@ -81,6 +74,11 @@ if [[ "${PIP_OPTIONS}" == *"--proxy"* ]]; then echo "PIP_OPTIONS must not contain --proxy: ${PIP_OPTIONS}" >&2 exit 1 fi +if [[ -n "${HTTP_PROXY:-}" || -n "${HTTPS_PROXY:-}" || -n "${http_proxy:-}" || -n "${https_proxy:-}" ]]; then + echo "pip connectivity must not leak PROXY_HOST into parent proxy env" >&2 + env | grep -E '^(HTTP_PROXY|HTTPS_PROXY|http_proxy|https_proxy)=' >&2 || true + exit 1 +fi assert_not_contains "user:pass" "${MP_FAKE_PIP_LOG}" : > "${MP_FAKE_PIP_LOG}" @@ -92,6 +90,33 @@ if [[ -n "${PIP_OPTIONS}" ]]; then echo "proxy branch must keep PIP_OPTIONS empty: ${PIP_OPTIONS}" >&2 exit 1 fi +if [[ -n "${HTTP_PROXY:-}" || -n "${HTTPS_PROXY:-}" || -n "${http_proxy:-}" || -n "${https_proxy:-}" ]]; then + echo "proxy connectivity must not leak PROXY_HOST into parent proxy env" >&2 + env | grep -E '^(HTTP_PROXY|HTTPS_PROXY|http_proxy|https_proxy)=' >&2 || true + exit 1 +fi + +MP_FAKE_PIP_LOG="${TMP_DIR}/update-explicit-standard-proxy.log" +export MP_FAKE_PIP_LOG +( + export VENV_PATH="${TMP_DIR}/venv" + export CONFIG_DIR="${TMP_DIR}/config" + export MOVIEPILOT_AUTO_UPDATE=false + export PIP_PROXY="" + export PROXY_HOST="http://proxy.example:7890" + export HTTP_PROXY="http://explicit.example:8080" + export HTTPS_PROXY="http://explicit.example:8080" + export http_proxy="http://explicit.example:8080" + export https_proxy="http://explicit.example:8080" + source "${UPDATE_FUNCS}" >/dev/null + test_connectivity_pip 1 + if [[ "${HTTP_PROXY}" != "http://explicit.example:8080" || "${HTTPS_PROXY}" != "http://explicit.example:8080" ]]; then + echo "explicit standard proxy env must be preserved" >&2 + env | grep -E '^(HTTP_PROXY|HTTPS_PROXY|http_proxy|https_proxy)=' >&2 || true + exit 1 + fi +) +assert_contains "HTTPS_PROXY=http://proxy.example:7890" "${MP_FAKE_PIP_LOG}" MP_FAKE_PIP_LOG="${TMP_DIR}/update-explicit-cache.log" export MP_FAKE_PIP_LOG @@ -104,7 +129,6 @@ export MP_FAKE_PIP_LOG export UV_CACHE_DIR="${TMP_DIR}/explicit-uv-cache" export PIP_PROXY="https://mirror.example/simple" export PROXY_HOST="http://proxy.example:7890" - unset NO_PROXY no_proxy source "${UPDATE_FUNCS}" >/dev/null test_connectivity_pip 0 ) @@ -121,7 +145,7 @@ export MP_FAKE_PIP_LOG export MOVIEPILOT_AUTO_UPDATE=false export PIP_PROXY="https://mirror.example/simple" export PROXY_HOST="http://proxy.example:7890" - unset PACKAGE_CACHE_ROOT PIP_CACHE_DIR UV_CACHE_DIR HTTP_PROXY HTTPS_PROXY http_proxy https_proxy NO_PROXY no_proxy + unset PACKAGE_CACHE_ROOT PIP_CACHE_DIR UV_CACHE_DIR HTTP_PROXY HTTPS_PROXY http_proxy https_proxy source "${UPDATE_FUNCS}" >/dev/null MP_FAKE_PIP_FAIL=1 test_connectivity_pip 0 && exit 1 if [[ -n "${HTTPS_PROXY:-}" || -n "${https_proxy:-}" ]]; then @@ -165,25 +189,51 @@ export MP_FAKE_PIP_LOG MP_FAKE_PYTHON_COUNT ( export VENV_PATH="${TMP_DIR}/venv" export CONFIG_DIR="${TMP_DIR}/config" - unset PACKAGE_CACHE_ROOT PIP_CACHE_DIR UV_CACHE_DIR NO_PROXY no_proxy + unset PACKAGE_CACHE_ROOT PIP_CACHE_DIR UV_CACHE_DIR HTTP_PROXY HTTPS_PROXY http_proxy https_proxy export PIP_PROXY="" export PROXY_HOST="http://proxy.example:7890" - export NO_PROXY="service.local" - export no_proxy="extra.lan" source "${ENTRYPOINT_FUNCS}" apply_package_cache_env ensure_backend_runtime_dependencies + if [[ -n "${HTTP_PROXY:-}" || -n "${HTTPS_PROXY:-}" || -n "${http_proxy:-}" || -n "${https_proxy:-}" ]]; then + echo "dependency recovery must not leak PROXY_HOST into parent proxy env" >&2 + env | grep -E '^(HTTP_PROXY|HTTPS_PROXY|http_proxy|https_proxy)=' >&2 || true + exit 1 + fi ) >/dev/null assert_contains "argv=install -r /app/requirements.txt" "${MP_FAKE_PIP_LOG}" assert_contains "HTTPS_PROXY=http://proxy.example:7890" "${MP_FAKE_PIP_LOG}" -assert_contains "NO_PROXY=service.local,extra.lan,localhost,127.0.0.1,::1,0.0.0.0,10.0.0.0/8" "${MP_FAKE_PIP_LOG}" -assert_contains "no_proxy=service.local,extra.lan,localhost,127.0.0.1,::1,0.0.0.0,10.0.0.0/8" "${MP_FAKE_PIP_LOG}" assert_contains "PACKAGE_CACHE_ROOT=${TMP_DIR}/config/.cache" "${MP_FAKE_PIP_LOG}" assert_contains "PIP_CACHE_DIR=${TMP_DIR}/config/.cache/pip" "${MP_FAKE_PIP_LOG}" assert_contains "UV_CACHE_DIR=${TMP_DIR}/config/.cache/uv" "${MP_FAKE_PIP_LOG}" assert_not_contains "--proxy" "${MP_FAKE_PIP_LOG}" +MP_FAKE_PIP_LOG="${TMP_DIR}/entrypoint-explicit-standard-proxy.log" +MP_FAKE_PYTHON_COUNT="${TMP_DIR}/python-count-explicit-standard-proxy" +export MP_FAKE_PIP_LOG MP_FAKE_PYTHON_COUNT +( + export VENV_PATH="${TMP_DIR}/venv" + export CONFIG_DIR="${TMP_DIR}/config" + unset PACKAGE_CACHE_ROOT PIP_CACHE_DIR UV_CACHE_DIR + export PIP_PROXY="" + export PROXY_HOST="http://proxy.example:7890" + export HTTP_PROXY="http://explicit.example:8080" + export HTTPS_PROXY="http://explicit.example:8080" + export http_proxy="http://explicit.example:8080" + export https_proxy="http://explicit.example:8080" + source "${ENTRYPOINT_FUNCS}" + apply_package_cache_env + ensure_backend_runtime_dependencies + if [[ "${HTTP_PROXY}" != "http://explicit.example:8080" || "${HTTPS_PROXY}" != "http://explicit.example:8080" ]]; then + echo "dependency recovery must preserve explicit standard proxy env" >&2 + env | grep -E '^(HTTP_PROXY|HTTPS_PROXY|http_proxy|https_proxy)=' >&2 || true + exit 1 + fi +) >/dev/null + +assert_contains "HTTPS_PROXY=http://proxy.example:7890" "${MP_FAKE_PIP_LOG}" + MP_FAKE_PIP_LOG="${TMP_DIR}/entrypoint-app-env.log" MP_FAKE_PYTHON_COUNT="${TMP_DIR}/python-count-app-env" cat > "${TMP_DIR}/config/app.env" < str: - """ - 读取 Docker 脚本中可独立执行的函数定义前缀。 - """ - lines = [] - for line in path.read_text(encoding="utf-8").splitlines(): - if line.startswith(stop_marker): - break - lines.append(line) - return "\n".join(lines) - - -@pytest.mark.parametrize( - ("script_path", "stop_marker"), - [ - (ROOT / "docker" / "entrypoint.sh", "# 环境变量补全"), - (ROOT / "docker" / "update.sh", "# 下载及解压"), - ], -) -def test_docker_proxy_env_adds_default_no_proxy_ranges( - tmp_path: Path, script_path: Path, stop_marker: str -) -> None: - """ - Docker 代理环境应默认绕过本机、局域网和常见容器内部地址。 - """ - shell_prefix = _read_shell_prefix(script_path, stop_marker) - config_dir = tmp_path / "config" - script = f""" -set -euo pipefail -CONFIG_DIR="{config_dir}" -PROXY_HOST="http://proxy.example:7890" -NO_PROXY="custom.internal,127.0.0.1" -no_proxy="extra.lan,127.0.0.1" -{shell_prefix} -apply_package_proxy_env -printf 'HTTP_PROXY=%s\\n' "${{HTTP_PROXY:-}}" -printf 'HTTPS_PROXY=%s\\n' "${{HTTPS_PROXY:-}}" -printf 'NO_PROXY=%s\\n' "${{NO_PROXY:-}}" -printf 'no_proxy=%s\\n' "${{no_proxy:-}}" -""" - - result = subprocess.run( - ["bash"], - input=script, - text=True, - capture_output=True, - check=True, - env={"PATH": os.environ.get("PATH", "")}, - ) - output = dict(line.split("=", 1) for line in result.stdout.splitlines()) - - assert output["HTTP_PROXY"] == "http://proxy.example:7890" - assert output["HTTPS_PROXY"] == "http://proxy.example:7890" - assert output["NO_PROXY"] == output["no_proxy"] - assert output["NO_PROXY"].count("127.0.0.1") == 1 - for item in ( - "localhost", - "::1", - "10.0.0.0/8", - "100.64.0.0/10", - "172.16.0.0/12", - "192.168.0.0/16", - "host.docker.internal", - "host.containers.internal", - "gateway.docker.internal", - "custom.internal", - "extra.lan", - ): - assert item in output["NO_PROXY"]