集成阅读约 7 分钟

在 curl 中使用代理:-x、环境变量、SOCKS5 与认证

如何在 curl 中使用代理:-x 参数、http_proxy 与 https_proxy 变量、通过 socks5h 使用 SOCKS5、代理认证,以及如何解读 CONNECT 与 407 错误。

本页内容

起点

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 是刻意无法解析的;请替换为你提供商的网关。

curl -x · 经代理发送一个请求

sh
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 历史。应改为从变量读取,或使用下面诊断脚本中展示的 curl 8.3 的 --variable 与 --expand-proxy-user 形式,它在导入密钥时不会将其暴露为参数。

  • connect=407 或消息 Received HTTP code 407 from proxy after CONNECT 表示代理拒绝了凭据或根本没有收到凭据。检查 -U 的值、编码以及提供商的用户名格式。
  • 隧道已成功但目标站点返回 401,属于目标站点的问题;代理这一步已经正常工作。
  • 许多提供商把国家、会话或协议选项编码在用户名中。严格遵循提供商文档中的格式;curl 会原样传递该字符串。

curl --proxy-user · 从环境变量读取凭据

sh
: "${PROXY_USERNAME:?}" "${PROXY_PASSWORD:?}"
curl --disable --noproxy '' \
  -x http://proxy.example.invalid:8080 \
  --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 流量继续直连。

no_proxy 是以逗号分隔的主机名或域名后缀列表,其中的条目绕过代理;单独一个 * 则完全禁用代理。命令行上的 -x 始终优先于环境变量,命令行上的 --noproxy 始终优先于 no_proxy。

~/.curlrc 文件可以通过 proxy = "http://host:port" 为每次调用设置代理,这在工作站上很方便,但在容器或 CI 任务里会令人意外。当脚本必须忽略它时,运行 curl 时把 --disable(或其短形式 -q)作为第一个参数。

http_proxy 与 https_proxy · shell 会话

sh
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 没有 CONNECT 状态码,因此握手被拒绝时表现为 curl 退出码 97 并附带简短原因,而不是 407。

curl socks5h · 代理侧 DNS

sh
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。只有在代理回应 200 之后,curl 才在隧道内与目标站点开始 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 或更高版本以及 HTTP Basic 代理认证。让你的密钥管理器填充 PROXY_USERNAME 和 PROXY_PASSWORD,并把 PROXY_URL 设为提供商网关(含协议和端口);https://proxy.example.invalid:8443 是一个刻意无法连接的示例值。

curl 自行导入凭据,因此展开后的密码永远不会成为 shell 参数。该命令打印目标站点状态、CONNECT 状态和耗时,并丢弃响应体。在加入浏览器、并发或应用代码之前,先保留这个基线。

proxy-check.sh · curl 8.3+

sh
: "${PROXY_URL:?Set your provider proxy URL}"
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 做对比时,保持相同的目标站点和凭据。一次改动三项设置,会让一次成功的重试难以解释。向支持人员提供退出码、耗时和脱敏后的状态,而不是包含认证数据的详细跟踪。

  • 退出码 5,could not resolve proxy:网关主机名错误,或本地 DNS 无法解析它。
  • 退出码 7,failed to connect:代理主机已解析,但端口关闭、被过滤或从本网络不可达。
  • 退出码 28,operation timed out:先把 --connect-timeout 或 --max-time 调高一次以确认,再次出现则视为路由或容量问题。
  • 退出码 56 且附带 Received HTTP code 407 from proxy after CONNECT:代理认证失败;先修正 -U,再去动目标站点的请求头。
  • 退出码 97,proxy handshake error:SOCKS 握手被拒绝;检查 socks5 与 socks5h 协议的选择以及凭据。
  • 退出码 35 或 60,TLS 失败:检查主机名、信任链和系统时钟。保持证书验证开启。

上线之前

  • 使用提供商提供的真实网关,以及其文档中给出的协议和端口。
  • 以私密方式注入凭据;不要把密钥粘贴进 shell 历史。
  • 在测试并行流量之前,先记录一个有限制的基线。
  • 在改动设置之前,先记下故障来自 CONNECT 步骤还是目标站点。

参考来源与延伸阅读

本指南参考的技术资料。请以你所安装版本的文档以及代理服务商支持的配置为准。