diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 81a83828..4a852cff 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -35,12 +35,43 @@ 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 + 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 fi } @@ -167,6 +198,9 @@ 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 "配置加载流程执行完毕。" } diff --git a/docker/update.sh b/docker/update.sh index 1efd903c..f6f83ed4 100644 --- a/docker/update.sh +++ b/docker/update.sh @@ -36,12 +36,42 @@ 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 + + # 代理仅用于外网依赖下载,容器访问本机、局域网和内网服务时应保持直连。 + 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 + 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 fi } diff --git a/scripts/dev/simulate_docker_package_env.sh b/scripts/dev/simulate_docker_package_env.sh index 9d8f671b..c325eed7 100644 --- a/scripts/dev/simulate_docker_package_env.sh +++ b/scripts/dev/simulate_docker_package_env.sh @@ -12,6 +12,8 @@ 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}" @@ -56,6 +58,8 @@ 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 source "${UPDATE_FUNCS}" >/dev/null @@ -63,6 +67,9 @@ source "${UPDATE_FUNCS}" >/dev/null 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}" @@ -97,6 +104,7 @@ 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 ) @@ -113,7 +121,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 + unset PACKAGE_CACHE_ROOT PIP_CACHE_DIR UV_CACHE_DIR HTTP_PROXY HTTPS_PROXY http_proxy https_proxy NO_PROXY no_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 @@ -157,9 +165,11 @@ 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 + unset PACKAGE_CACHE_ROOT PIP_CACHE_DIR UV_CACHE_DIR NO_PROXY no_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 @@ -167,6 +177,8 @@ export MP_FAKE_PIP_LOG MP_FAKE_PYTHON_COUNT 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}" @@ -182,7 +194,7 @@ 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 PIP_PROXY PROXY_HOST + unset PACKAGE_CACHE_ROOT PIP_CACHE_DIR UV_CACHE_DIR PIP_PROXY PROXY_HOST NO_PROXY no_proxy source "${ENTRYPOINT_FUNCS}" load_config_from_app_env apply_package_cache_env diff --git a/tests/test_docker_proxy_env.py b/tests/test_docker_proxy_env.py new file mode 100644 index 00000000..9353f8b0 --- /dev/null +++ b/tests/test_docker_proxy_env.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +import os +import subprocess +from pathlib import Path + +import pytest + + +ROOT = Path(__file__).resolve().parents[1] + + +def _read_shell_prefix(path: Path, stop_marker: str) -> 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"]