环境变量 HTTP_PROXY, HTTPS_PROXY 和 NO_PROXY 看起来非常简单:设置变量后,所有流量都通过代理。在实践中,正是在这一点上,开发人员花费了数小时进行调试:代理在终端中工作,但在Docker容器中却不工作;curl通过代理工作,而Python应用程序却不工作;CI运行器突然禁止内部注册表,因为NO_PROXY的语法不正确。我们将分析7个真实的陷阱,并展示如何通过具体代码来规避它们。
应用程序如何读取代理变量
重要的是要理解:HTTP_PROXY、HTTPS_PROXY和NO_PROXY并不是HTTP协议的标准,而是每个库以自己的方式实现的约定。Linux内核对这些变量一无所知——它们在网络客户端初始化阶段被特定的应用程序或库读取。
例如,curl和基于libcurl的工具在每次进程启动时读取变量。Python库requests通过模块urllib.request.getproxies()读取它们,该模块在创建会话时扫描进程的环境。而Node.js内置的fetch()在18版本之前根本不考虑这些变量,除非使用额外的包。
因此,第一个规则是:代理变量仅在特定库明确支持的地方有效。通过curl进行的检查并不能保证您的Go、Java或Node.js应用程序会表现得一样。
export HTTP_PROXY="http://user:[email protected]:8080"
export HTTPS_PROXY="http://user:[email protected]:8080"
export NO_PROXY="localhost,127.0.0.1,.internal.corp"
curl -v https://api.example.com/ping
# curl将在详细日志中显示:"Trying proxy.example.com..."
陷阱1:变量的大小写和httpoxy漏洞
大多数Linux控制台工具以小写读取变量:http_proxy、https_proxy、no_proxy。许多现代SDK则依赖于大写。这个差异看似微不足道,但正是因为它,"在本地工作,但在服务器上不工作"的错误在设置的第一天就会发生。
另一个故事是httpoxy漏洞(CVE-2016-5385及相关)。其本质在于,CGI环境(PHP、Python WSGI、某些Ruby框架)会自动将传入请求的HTTP头Proxy转换为环境变量HTTP_PROXY。攻击者发送头Proxy: http://attacker.com,您的服务器应用程序开始通过外部代理服务器发送出站请求,从而打开了MITM攻击和数据泄露的途径。
解决方案是始终同时在两个大小写中设置变量,并在Web服务器级别明确将HTTP_PROXY置为零,以处理传入请求:
# 为了兼容性同时设置两个大小写
export http_proxy="http://proxy.example.com:8080"
export HTTP_PROXY="http://proxy.example.com:8080"
export https_proxy="http://proxy.example.com:8080"
export HTTPS_PROXY="http://proxy.example.com:8080"
# 在nginx中——阻止来自客户端的Proxy头
proxy_set_header Proxy "";
陷阱2:NO_PROXY的语法在不同工具之间有所不同
NO_PROXY是一个例外列表,但域名和掩码的记录格式并没有标准化。一些工具将.example.com理解为"任何子域",其他工具则要求*.example.com,还有一些工具只理解完全匹配的主机,而根本不支持通配符。
例如,Go库net/http支持NO_PROXY中的CIDR表示法(10.0.0.0/8),而经典的curl在某些版本之前则不支持。这导致同一个NO_PROXY在一个CI步骤中正确排除了内部注册表,而在另一个用不同语言编写的步骤中却没有排除。
| 工具 | 子域的通配符 | CIDR掩码 |
|---|---|---|
| curl / libcurl | 通过.example.com | 不支持 |
| Go net/http | 通过.example.com | 支持 |
| Python requests | 通过.example.com | 不支持 |
| npm / Node.js | 需要*.example.com | 不支持 |
实用建议:同时列出NO_PROXY中的域名和通配符选项,以覆盖不同的解析器:
export NO_PROXY="localhost,127.0.0.1,.internal.corp,*.internal.corp,10.0.0.0/8"
陷阱3:Docker build与Docker run——不同的环境变量
这可能是Docker中最常见的陷阱。通过docker run -e HTTP_PROXY=...设置的变量仅在容器运行期间有效。它们不会自动传递到镜像构建阶段。如果您的Dockerfile内部调用apt-get install或pip install,并且构建在企业代理后进行,构建将会因为超时而挂起。
要进行构建,需要传递build-args,Docker从1.9版本开始"开箱即用"地理解这些参数,而无需在Dockerfile中声明:
docker build \
--build-arg HTTP_PROXY=http://proxy.example.com:8080 \
--build-arg HTTPS_PROXY=http://proxy.example.com:8080 \
--build-arg NO_PROXY=localhost,127.0.0.1 \
-t my-app:latest .
如果您希望变量在已启动的容器内仍然可用,则需要通过Dockerfile中的ENV明确复制它——ARG和ENV在Docker中是根本不同的机制,ARG在构建层后立即消失:
ARG HTTP_PROXY
ARG HTTPS_PROXY
ARG NO_PROXY
ENV HTTP_PROXY=${HTTP_PROXY}
ENV HTTPS_PROXY=${HTTPS_PROXY}
ENV NO_PROXY=${NO_PROXY}
RUN apt-get update && apt-get install -y curl
另一个细节:Docker守护进程本身可能会使用代理从注册表下载镜像——这不是在Dockerfile中配置的,而是在systemd单元的配置中/etc/systemd/system/docker.service.d/http-proxy.conf中设置的,这常常与容器内的变量混淆。
陷阱4:docker-compose和服务之间的变量继承
在docker-compose中,主机的环境变量不会自动传递到容器中,除非您在environment部分中明确列出它们,或者使用.env文件。一个常见的错误是在运行docker-compose up之前在终端中设置代理,并期望所有服务都能接收到它。
version: "3.9"
services:
scraper:
build:
context: .
args:
HTTP_PROXY: "${HTTP_PROXY}"
HTTPS_PROXY: "${HTTPS_PROXY}"
environment:
- HTTP_PROXY=${HTTP_PROXY}
- HTTPS_PROXY=${HTTPS_PROXY}
- NO_PROXY=localhost,127.0.0.1,db,redis
depends_on:
- db
- redis
请注意NO_PROXY:在docker-compose网络中,容器通过服务名称(db、redis)而不是localhost相互访问。如果忘记将服务名称添加到排除列表中,容器之间的所有内部流量也将通过外部代理,这将增加延迟,并可能在代理不允许内部协议(如PostgreSQL或Redis)时导致连接失败。
陷阱5:CI/CD运行器和代理秘密泄露
在GitHub Actions、GitLab CI和Jenkins中,代理变量通常在运行器或整个组织级别设置,这会产生两个问题。首先,如果代理的URL包含明文的用户名和密码,它将在任何打印环境的命令(env、printenv、脚本中的调试输出)中出现在构建日志中。其次,自托管的运行器有时会继承主机的系统代理,而该代理并不适合CI作业的流量。
# .github/workflows/build.yml
jobs:
build:
runs-on: ubuntu-latest
env:
HTTP_PROXY: ${{ secrets.PROXY_URL }}
HTTPS_PROXY: ${{ secrets.PROXY_URL }}
NO_PROXY: "localhost,127.0.0.1,github.com"
steps:
- uses: actions/checkout@v4
- name: 测试连接
run: curl -sS https://api.example.com/health
安全规则:仅在CI的秘密中存储代理的URL(GitHub Secrets、GitLab CI/CD Variables带有"masked"标志),永远不要在调试日志中完整输出环境变量,并限制NO_PROXY,以确保对CI平台本身(例如github.com、gitlab.com)的请求不通过代理——否则,如果在高峰时段代理不可用,构建可能会挂起。
陷阱6:代理的用户名和密码中的特殊字符
如果代理的密码包含字符@、:、/或#,URL解析器会在错误的位置将字符串拆分成多个部分。经典错误:密码P@ssw0rd在字符串http://user:P@[email protected]:8080中,解析器会将主机读取为"[email protected]",而不是"proxy.com"。
解决方案是始终在将用户名和密码插入变量之前进行URL编码:
# Python:在构建代理字符串之前编码密码
import urllib.parse
user = "login"
password = "P@ss:w0rd/1"
encoded_password = urllib.parse.quote(password, safe="")
proxy_url = f"http://{user}:{encoded_password}@proxy.example.com:8080"
import os
os.environ["HTTPS_PROXY"] = proxy_url
# Node.js:通过encodeURIComponent进行类似的构建
const user = "login";
const password = "P@ss:w0rd/1";
const encoded = encodeURIComponent(password);
const proxyUrl = `http://${user}:${encoded}@proxy.example.com:8080`;
process.env.HTTPS_PROXY = proxyUrl;
陷阱7:忽略env的应用程序
并非所有HTTP客户端都尊重系统代理变量。在Python中,库requests会通过requests.Session()自动读取它们,但如果您一次性明确传递了字典proxies={},这将覆盖环境变量,流量将直接通过,即使系统中设置了HTTP_PROXY。
import requests
# 空字典proxies会覆盖环境变量
response = requests.get("https://api.example.com", proxies={})
# 正确的做法是明确传递代理,或者根本不传递proxies
proxies = {
"http": "http://user:[email protected]:8080",
"https": "http://user:[email protected]:8080",
}
response = requests.get("https://api.example.com", proxies=proxies)
在Node.js中,内置的fetch在没有额外库的情况下根本不知道HTTP_PROXY。需要明确引入undici与ProxyAgent或经典的https-proxy-agent:
const { ProxyAgent, setGlobalDispatcher } = require("undici");
const proxyUrl = process.env.HTTPS_PROXY;
if (proxyUrl) {
setGlobalDispatcher(new ProxyAgent(proxyUrl));
}
fetch("https://api.example.com/data")
.then(res => res.json())
.then(console.log);
Java也需要单独的系统属性,而不是环境变量:-Dhttp.proxyHost=proxy.example.com -Dhttp.proxyPort=8080 -Dhttps.proxyHost=proxy.example.com -Dhttps.proxyPort=8080。JVM默认不读取HTTP_PROXY环境变量。
如何正确测试代理变量
在花时间调试应用程序之前,请先独立检查变量和代理。这可以节省数小时。
# 1. 检查变量在当前会话中是否真正设置
env | grep -i proxy
# 2. 直接通过curl检查代理的有效性
curl -x "$HTTP_PROXY" -v https://api.ipify.org
# 3. 从容器内部检查变量是否到达
docker exec -it my_container env | grep -i proxy
# 4. 检查NO_PROXY是否真正排除了所需的主机
curl -v --noproxy "internal.corp" https://service.internal.corp/health
如果任务是对外部API进行大量请求、解析市场或测试CI管道中的广告地理定位,选择合适类型的代理以应对负载非常重要。对于CI中的稳定高负载场景,通常只需使用数据中心代理——在大量构建和测试流量中,它们比住宅代理更快且更便宜。如果您的管道检查来自特定区域的真实用户IP的站点行为,最好连接住宅代理——它们降低了因"数据中心"IP而被封锁的风险。
部署前检查清单
- 变量同时在大写和小写中设置
- 代理URL中的用户名和密码通过
urllib.parse.quote或encodeURIComponent进行了URL编码 - NO_PROXY同时包含域名和通配符格式,以便在工具之间兼容
- 在docker-compose中,NO_PROXY包括来自Compose网络的服务名称,而不仅仅是localhost
- 对于Docker build,通过Dockerfile中的ENV将build-args与runtime env分开传递
- CI中的代理URL秘密被掩码,并且不会在调试日志中打印
- 对于Java应用程序,代理通过系统属性-D设置,而不仅仅是通过env
- 检查防止httpoxy的措施——在Web服务器级别将客户端的Proxy头置为零
结论
HTTP_PROXY、HTTPS_PROXY和NO_PROXY变量看似简单的约定,实际上却因为字符大小写、例外语法、Docker build与Docker run之间的界限、docker-compose中的环境继承、CI日志中的秘密泄露,以及并非每个库都读取这些变量而破裂。在寻找应用程序业务逻辑中的错误之前,请通过curl和docker exec独立检查每个变量。
如果您的基础设施依赖于解析外部网站、测试广告的地理定位或通过CI管道绕过IP封锁,正确传递环境变量并选择适合任务的代理类型至关重要。对于测试移动流量和检查移动运营商行为的应用程序,使用移动代理是合适的——它们提供真实移动网络的IP,并且更少受到反欺诈系统的过滤。