起点
curl 选择代理的每一种方式:从单个 -x 参数到环境变量,以及决定请求能否成功的认证、SOCKS5 和隧道细节。
用 -x 为单个请求设置代理
-x 选项(长形式写作 --proxy)将单个 curl 请求经由代理转发。传给它代理的协议、主机和端口。省略协议时 curl 默认使用 http://,省略端口时默认使用 1080,因此两者都应显式写出。
协议描述的是到代理的连接,而不是目标站点。经普通 HTTP 代理访问 https:// 目标时仍然使用 -x http://host:port;curl 会通过它打开一条 CONNECT 隧道。只有当提供商在代理本身终止 TLS 时才写 -x https://host:port。curl 在此处还接受 socks4://、socks4a://、socks5:// 和 socks5h://。
下面的命令沿用了本指南其余部分的两个习惯:--disable 忽略可能自行添加代理的 curlrc,--noproxy '' 取消任何继承来的 no_proxy 排除项,确保请求真正经过你指定的网关。proxy.example.invalid 是刻意无法解析的;请替换为你提供商的网关。不加 --compressed 时,这条命令不会发送 Accept-Encoding 请求头,因此只在客户端请求时才压缩的服务器会经由代理返回未压缩的完整页面;如果你按 GB 付费,请参阅哪些客户端不请求压缩,以及如何核对字节数。
curl -x · 经代理发送一个请求
curl --disable --noproxy '' \
-x http://proxy.example.invalid:8080 \
https://example.com/代理认证:-U、--proxy-user 与 407
代理凭据与目标站点凭据是分开的。-U 或 --proxy-user 把凭据发给代理;-u 或 --user 把凭据发给目标站点。两者混用会导致代理返回 407 或目标站点返回 401,即使密码本身是正确的。
你也可以把凭据嵌入代理 URL,写成 http://user:password@host:port。先对保留字符做百分号编码:密码中的 @ 必须写成 %40,否则 curl 会把它当作主机部分的开始。Basic 是默认的代理认证方案;当提供商有文档说明时,可用 --proxy-digest、--proxy-ntlm 和 --proxy-anyauth 选择其他方案。
直接在命令中输入密码,可能会将其保存到 shell 历史,并使其出现在进程的初始参数中。如果通过私密方式提供变量值,可以避免在输入的命令中写出密码,但 --proxy-user "$PROXY_USERNAME:$PROXY_PASSWORD" 仍会在 curl 启动前将密码展开为参数。在支持的系统上,curl 启动后会隐藏该参数;稍后查看进程列表可能看不到此前的暴露。请参阅 curl 的 proxy-user 文档。
对于 curl 8.3.0 或更高版本的脚本,下面的示例在 curl 内部导入已导出的环境变量。请让密钥管理器提供变量,关闭 shell 跟踪,不要把含有密钥的 export 命令粘贴到历史记录中。这避免了密码出现在 curl 的参数中,但不会向有权检查的账户隐藏继承的环境或进程内存。交互式检查时,使用不带冒号的 --proxy-user username 会另外提示输入密码。
参数处理不会加密与代理的连接。发送给 http:// 代理的 HTTP Basic 凭据不会受到随后建立的目标 HTTPS 隧道保护。如果提供商支持,请使用 https:// 网关,并保持代理证书验证开启。
- connect=407 或消息 Received HTTP code 407 from proxy after CONNECT 表示代理拒绝了凭据或根本没有收到凭据。检查 -U 的值、编码以及提供商的用户名格式。
- 隧道已成功但目标站点返回 401,属于目标站点的问题;代理这一步已经正常工作。
- 如果提供商在用户名中编码国家、会话或协议选项,请遵循其文档格式。--proxy-user 接收以冒号分隔的用户名和密码;这一步不是 URL 编码。
curl 8.3+ · 在 curl 内部导入凭据
: "${PROXY_USERNAME:?}" "${PROXY_PASSWORD:?}"
curl --disable --noproxy '' \
-x http://proxy.example.invalid:8080 \
--variable %PROXY_USERNAME \
--variable %PROXY_PASSWORD \
--expand-proxy-user '{{PROXY_USERNAME}}:{{PROXY_PASSWORD}}' \
https://example.com/环境变量:http_proxy、https_proxy、no_proxy 与 .curlrc
不带 -x 时,curl 会在环境中查找代理。它只读取小写的 http_proxy,因为大写名称可能被 CGI 请求头设置;它接受 https_proxy 或 HTTPS_PROXY、all_proxy 或 ALL_PROXY,以及 no_proxy 或 NO_PROXY。变量按目标站点的协议选择:https:// 目标使用 https_proxy,因此仅设置 http_proxy 不能配置 HTTPS 路由;ALL_PROXY 或其他设置仍可能影响路由。curl_cffi 是 libcurl 的 Python 绑定,遵循同样的小写规则;curl_cffi 代理配置记录了这条规则,以及 CONNECT 被拒绝和 SOCKS 握手失败时抛出的异常。
no_proxy 是以逗号分隔的主机名或域名后缀列表,其中的条目绕过代理;单独一个 * 则完全禁用代理。-x 会覆盖代理环境变量选择的网关,但 no_proxy 仍可能让请求绕过该网关。--noproxy 会覆盖 no_proxy;空的 --noproxy 值会取消继承的排除项。
~/.curlrc 文件可以通过 proxy = "http://host:port" 为每次调用设置代理,这在工作站上很方便,但在容器或 CI 任务里会令人意外。当脚本必须忽略它时,运行 curl 时把 --disable(或其短形式 -q)作为第一个参数。
http_proxy 与 https_proxy · shell 会话
export http_proxy='http://proxy.example.invalid:8080'
export https_proxy='http://proxy.example.invalid:8080'
export no_proxy='localhost,127.0.0.1,.internal.example'
curl https://example.com/ # uses https_proxy
curl --noproxy '*' https://example.com/ # bypasses the proxy once在 curl 中使用 SOCKS5:socks5 与 socks5h 的区别
curl 通过同一个 -x 选项使用 SOCKS。socks5:// 在你的机器上解析目标主机名,并向代理发送 IP 地址。socks5h:// 发送主机名并让代理解析,当目标必须从代理所在网络解析、或本地 DNS 完全不应看到目标时,这正是你需要的。长选项 --socks5 和 --socks5-hostname 与之等价。
SOCKS5 用户名和密码认证使用同一个 --proxy-user 选项。SOCKS 没有 HTTP CONNECT 状态;被拒绝的 SOCKS 握手可能返回 curl 退出码 97 及原因,而不是 HTTP 407。这个兼容性示例仍会由 shell 将凭据展开到初始参数中;如需避免这种暴露,请使用上面的 curl 8.3+ 内部导入方式。
curl socks5h · 代理侧 DNS
curl --disable --noproxy '' \
-x socks5h://proxy.example.invalid:1080 \
--proxy-user "$PROXY_USERNAME:$PROXY_PASSWORD" \
https://example.com/HTTPS 目标:CONNECT 隧道与 -v
经 HTTP 代理访问 https:// 目标时,curl 首先向代理发送 CONNECT example.com:443。CONNECT 成功响应后,curl 在隧道内与目标建立 TLS。普通转发代理无法读取加密的 HTTP 交换;客户端信任的 TLS 拦截代理则是另一种情况。用 -v 可以观察两个步骤;CONNECT 响应先于目标响应头出现。-p 或 --proxytunnel 也会为 http:// 目标启用该隧道。
对普通 http:// 目标则没有隧道。curl 把完整 URL 发给代理,由代理去获取,你看到的响应头可能来自任何一方。当需要把某个状态码归属到某一层时,优先使用隧道的情形,并用 %{http_connect} 输出变量单独读取 CONNECT 状态。
使用 https:// 代理时,curl 会同时验证代理和目标站点的证书。--proxy-cacert 为代理提供私有信任包;保持两项验证都开启,而不是求助于 --proxy-insecure 或 -k。
先发送一个有限制的请求作为基线
把下面的内容保存为 proxy-check.sh 并用 sh proxy-check.sh 运行。它需要 curl 8.3.0 或更高版本以及 HTTP Basic 代理认证。让你的密钥管理器导出 PROXY_USERNAME 和 PROXY_PASSWORD,并将 PROXY_URL 导出为提供商网关(含协议和端口);https://proxy.example.invalid:8443 是一个刻意无法连接的示例值。
curl 自行导入凭据,因此密码不会作为 curl 的参数传入。前面的检查会拒绝缺失或空的值;环境变量和跟踪方面的限制仍然适用。该命令打印目标站点状态、CONNECT 状态和耗时,并丢弃响应体。在加入浏览器、并发或应用代码之前,先保留这个基线。
proxy-check.sh · curl 8.3+
: "${PROXY_URL:?Set your provider proxy URL}"
: "${PROXY_USERNAME:?}" "${PROXY_PASSWORD:?}"
curl --disable --silent --show-error --fail \
--noproxy '' \
--proxy "$PROXY_URL" \
--variable %PROXY_USERNAME \
--variable %PROXY_PASSWORD \
--expand-proxy-user '{{PROXY_USERNAME}}:{{PROXY_PASSWORD}}' \
--connect-timeout 10 --max-time 30 \
--output /dev/null \
--write-out 'http=%{http_code} connect=%{http_connect} seconds=%{time_total}\n' \
'https://example.com/'解读 curl 退出码并找出出问题的层
使用 curl 8.7.1 的本地合成测试比较了两种方式:shell 展开会把密码放入初始参数,而 curl 内部导入不会。两者发送了相同的测试凭据。稍后查看进程列表时,curl 已清除了 shell 展开的值,因此仅查看该列表会漏掉此前的暴露。这些检查说明的是本地命令行为,而不是提供商的安全性或代理性能。
可使用本地测试脚本复现,并查看合成测试结果。它仅使用虚构凭据测试 HTTP Basic 认证,没有测试外部代理服务。
一次成功的响应只证明这个请求完成了;它并不能确定具体的出口国家或运营商。要确认这些,请使用提供商文档中的诊断端点,或你自己控制的、能报告观测到的源地址的端点。
在把你的应用与 curl 做对比时,保持相同的目标站点和凭据。一次改动三项设置,会让一次成功的重试难以解释。向支持人员提供退出码、耗时和脱敏后的状态,而不是包含认证数据的详细跟踪。
- 退出码 5,could not resolve proxy:网关主机名错误,或本地 DNS 无法解析它。
- 退出码 7,failed to connect:代理主机已解析,但端口关闭、被过滤或从本网络不可达。
- 退出码 28,operation timed out:将连接和总时限与记录的耗时比较。重复超时本身不能证明原因是网关、目标站点还是所选时限。
- connect=407:代理要求认证。我们的 curl 8.7.1 本地测试在使用 --fail 时返回退出码 22,不使用时返回 56。请同时阅读 CONNECT 状态和退出码;不要等到出现 56 才检查代理凭据。
- 退出码 97,proxy handshake error:SOCKS 握手被拒绝;检查 socks5 与 socks5h 协议的选择以及凭据。
- 退出码 35 或 60,TLS 失败:检查主机名、信任链和系统时钟。保持证书验证开启。
- 退出码 35 且带有 wrong version number 并不是证书问题:TLS 握手收到了明文应答,要么来自代理端口,要么来自目标端口。SSL WRONG_VERSION_NUMBER:代理哪一跳出错演示了如何用 %{http_connect} 区分两者。
上线之前
- 使用提供商提供的真实网关,以及其文档中给出的协议和端口。
- 以私密方式注入凭据;不要把密钥粘贴进 shell 历史。
- 在测试并行流量之前,先记录一个有限制的基线。
- 在改动设置之前,先记下故障来自 CONNECT 步骤还是目标站点。
参考来源与延伸阅读
本指南参考的技术资料。请以你所安装版本的文档以及代理服务商支持的配置为准。