curl: (56) CONNECT tunnel failed, response 403这是 curl 8.15 至 8.17 请求代理为一个 https:// 主机建立隧道却被拒绝时的输出。curl 8.18.0 和 8.22.0 打印同样的消息,但退出码是 7 而不是 56。通过同一个本地测试代理记录下来的同一次拒绝,在 Python Requests 2.34.2 中表现为抛出 requests.exceptions.ProxyError,消息如下:
HTTPSConnectionPool(host='example.org', port=443): Max retries exceeded with url: / (Caused by ProxyError('Unable to connect to proxy', OSError('Tunnel connection failed: 403 Forbidden')))在 Node.js 24.20.0 内置的 fetch 中(设置了 NODE_USE_ENV_PROXY=1),逐行打印每一层 err.cause:
depth 0: TypeError: fetch failed
depth 1: DOMException: Request was cancelled.
depth 2: RequestAbortedError [UND_ERR_ABORTED]: Proxy response (403) !== 200 when HTTP Tunneling三种情况下,都是代理在你的请求离开它之前就说了“不”。通过同一个代理,https://example.com/ 在每个客户端中都返回 200。变的只是主机。
这个 403 意味着什么
对于 https:// URL,客户端会向代理发送 CONNECT example.org:443,等待允许建立隧道。RFC 9110 规定,任何 2xx 响应都会让连接切换到隧道模式,而“任何非成功的响应都表示隧道尚未建立”。403 就是代理拒绝建立这条隧道。RFC 9110
由此可以得出两点:
- 这不是凭据问题。 需要凭据的代理会返回 407 并附带
Proxy-Authenticate质询;那种情况见 407 指南。403 表示代理决定不转发这个请求,所以换密码没有用。 - 目标网站根本没有被访问到。 没有隧道,就没有 TLS 握手,也没有发往
example.org的请求。curl 的 write-out 能说明这一点:connect=403是代理的应答,http=000表示网站从未回应。
为什么编程智能体会遇到它
沙箱和 CI 运行器常常让所有出站流量经过一个带白名单的出口代理。代理地址通过 HTTPS_PROXY 传入,所以 curl、pip、npm 和 fetch 都会走它。白名单里的主机能建立隧道,其他一律得到 403。这就是为什么在同一个 shell 里,一个主机能访问,下一个却失败。
两份公开报告展示了这种模式:
- 在 anthropics/claude-code#56959 中,使用 Bedrock 运行的 Claude Code 把
HTTPS_PROXY指向了沙箱自己在 localhost 上的代理。该代理只允许四个主机(bedrock-runtime.us-east-1.amazonaws.com、api.anthropic.com和两个 Sentry 通配模式),按报告者的说法,“任何其他主机都会从本地代理收到 HTTP 403”。curl https://github.com以curl: (56) CONNECT tunnel failed, response 403失败。报告者表示,在该模式下,用户设置和托管设置中的sandbox.network.allowedDomains条目都被忽略了。该 issue 被作为 #37970 的重复项关闭。 - 在 SocketDev/socket-sdk-js#659 中,一个在 CI 中执行每周依赖更新的智能体未能完成任务。更新工具 taze 去
npm.antfu.dev查询版本,而该主机“被 CI 沙箱防火墙拦截(403 CONNECT tunnel failed)”。对该主机执行 curl 返回 HTTP 000 和 curl 错误 56,而curl https://registry.npmjs.org/semver返回 200。维护者的修复方法是让查询留在已在白名单中的registry.npmjs.org上。
如何判断是谁拒绝了
通过同一个代理,用 curl -v 运行一次失败的 URL。以 > 开头的行是 curl 发出的内容;在 CONNECT tunnel failed 之前以 < 开头的行是代理的应答。下面是 curl 8.16.0 对测试代理的输出:
* Trying 127.0.0.1:41745...
* CONNECT: no ALPN negotiated
* allocate connect buffer
* Establish HTTP proxy tunnel to example.org:443
> CONNECT example.org:443 HTTP/1.1
> Host: example.org:443
> User-Agent: curl/8.16.0
> Proxy-Connection: Keep-Alive
>
< HTTP/1.1 403 Forbidden
< Content-Type: text/plain
< Content-Length: 32
< Connection: close
<
* CONNECT tunnel failed, response 403
* closing connection #0
curl: (56) CONNECT tunnel failed, response 403Trying 行显示的是代理,而不是网站。403 是对 CONNECT 的应答,之后没有任何 TLS 相关的行,所以网站从未参与。如果 403 出现在 CONNECT 成功并完成 TLS 握手之后,那它来自网站,问题不在代理。
接着,通过同一个代理比较一个允许的主机和一个被拦截的主机,用 write-out 把两个状态码分开:
curl --disable -sS -o /dev/null -x http://127.0.0.1:41745 -w 'http=%{http_code} connect=%{http_connect} exit=%{exitcode}\n' https://example.com/
curl --disable -sS -o /dev/null -x http://127.0.0.1:41745 -w 'http=%{http_code} connect=%{http_connect} exit=%{exitcode}\n' https://example.org/用 curl 8.18.0 时,第一条打印 http=200 connect=200 exit=0。第二条打印 http=000 connect=403 exit=7,外加 curl: (7) CONNECT tunnel failed, response 403。同一个客户端、同一个代理、不同的主机:说明代理有按主机划分的规则。用你自己的允许主机和被拦截主机重复这个比较。在智能体的沙箱里,选一个确定能访问的主机,例如智能体已经成功访问过的包仓库。
最后,检查进程实际使用的是哪个代理。在失败的同一个 shell 或任务步骤中打印 HTTPS_PROXY、HTTP_PROXY、NO_PROXY 以及它们的小写形式。列在 NO_PROXY 中的主机会绕过代理直接连接,而沙箱可能以另一种方式拦截直连。代理环境变量指南 解释了客户端如何在这些变量之间做选择,NO_PROXY matching, tested 则表明各个客户端对 NO_PROXY 条目匹配什么并没有一致的理解。Node 内置的 fetch 会忽略这些变量,除非设置了 NODE_USE_ENV_PROXY=1 或 --use-env-proxy,所以同一个 shell 里的 Node 脚本和 curl 命令可能走不同的路线。Node.js CLI 文档
不绕开代理的修复方法
403 是一个策略决定,所以修复要在策略一侧或来源一侧进行:
- 放行该主机。 把工具需要的确切域名加入沙箱或 CI 的出口白名单。域名要从
curl -v的CONNECT行或工具自身的报错中取,而不是从它的配置里找:在 Socket 的报告中,taze 是去npm.antfu.dev查询版本,而不是 npm 仓库。 - 改用已被允许的来源。 镜像、内部仓库,或策略已经放行的官方仓库,常常能省去修改策略。Socket 的修复就是这样做的。
- 检查端口。 有些代理只为 443 端口建立隧道。Squid 的建议配置包含
http_access deny CONNECT !SSL_ports,其中SSL_ports设为 443 端口。Squid http_access 在只允许example.com走 443 端口的测试代理中,https://example.com:8443/得到了与被拦截主机相同的CONNECT tunnel failed, response 403。 - 检查服务商的目标规则。 商业代理服务商也可能拒绝某些目标。Bright Data 的错误目录把策略类错误列在 HTTP 403 之下,例如 “You tried to target %HOST% which is blocked by Bright Data policy”。Bright Data 错误目录 Decodo 列出了默认限制的目标类别,例如银行和政府网站。Decodo 受限目标
不要绕过沙箱的出口规则。为了避开白名单而取消 HTTPS_PROXY、把主机加进 NO_PROXY,或改走另一个代理,都会破坏别人有意设置的控制,而且可能根本行不通:#56959 的报告者发现,沙箱在代理之下同样封锁了直接的网络访问。请让管理沙箱或 CI 运行器的人放行该主机,并把 curl -v 中确切的 CONNECT 行交给他们。Socket 报告中的智能体正是这么做的:它停了下来,报告了被拦截的主机,没有试图绕过防火墙。
为什么 curl 对 http:// 的报告方式不同
对于 http:// URL,不存在 CONNECT。curl 把整个请求发给代理,代理返回的 403 就是对这个请求的响应。测试代理对两种情况都回了 403,但 curl 的报告方式不同:
| 通过同一个代理 | curl 8.15.0 至 8.17.0 | curl 8.18.0 和 8.22.0 |
|---|---|---|
https://example.org/(被拦截) | 退出码 56,curl: (56) CONNECT tunnel failed, response 403 | 退出码 7,curl: (7) CONNECT tunnel failed, response 403 |
http://example.org/(被拦截) | 退出码 0,代理的 403 页面作为响应体打印出来 | 退出码 0,代理的 403 页面作为响应体打印出来 |
http://example.org/ 加 --fail | 退出码 22,curl: (22) The requested URL returned error: 403 | 退出码 22,curl: (22) The requested URL returned error: 403 |
https://example.com/(允许) | 退出码 0,http=200 connect=200 | 退出码 0,http=200 connect=200 |
在 http:// 的情况下,curl -v 显示 > GET http://example.org/ HTTP/1.1,随后是 < HTTP/1.1 403 Forbidden,curl 会打印响应体。curl 维护者 Daniel Stenberg 在 curl 讨论 #15718 中说明了这种差别。该讨论针对的是 407,但适用于任何拒绝:隧道建立失败意味着传输没有执行,因此是失败;而对代理发出的 GET 得到了响应,则“是一次包含 4xx 状态码的成功传输”。所以脚本里不加 --fail 的 http:// URL 可能看起来像是成功了。
其他客户端也是同样的分化,只有一个例外。Python Requests 对 http://example.org/ 返回了一个状态为 403 的普通 Response。Node.js 24.20.0 的 fetch(设置 NODE_USE_ENV_PROXY=1)即使对 http:// URL 也建立了 CONNECT example.org:80 隧道,因此以同样的 Proxy response (403) !== 200 when HTTP Tunneling 原因失败。
还有一个版本细节:我们使用的 curl 8.22.0 静态构建包含 HTTP/3。当主机声明支持 HTTP/3 时,它会先尝试建立 CONNECT-UDP 隧道,所以 -v 在最终的 curl: (7) CONNECT tunnel failed, response 403 之前显示了两次 403 应答。
复现
以上内容都是在 2026 年 10 月 2 日用 下载目录 中的实验环境记录的:一个运行在 127.0.0.1 上的 Node.js 代理,只为 example.com 的 443 端口建立隧道,对其他所有 CONNECT 和普通 HTTP 请求返回 403;所用版本为 curl 8.15.0、8.16.0、8.17.0、8.18.0 和 8.22.0,Python 3.14.4 搭配 Requests 2.34.2 与 urllib3 2.8.0,以及 Node.js 24.20.0。原始输出见 results.json。测试代理的 403 响应体(403 Forbidden: lab proxy policy)是它自己的;真实的沙箱代理或服务商代理会返回不同的文字。
相关阅读
- 修复代理错误 407:代理要求提供凭据时。
- 修复 ERR_TUNNEL_CONNECTION_FAILED:Chromium、Playwright 和 Puppeteer 中的同一种拒绝。
- 代理环境变量:客户端实际读取的是哪个变量。
- NO_PROXY matching, tested:为什么同一条绕过条目在一个客户端中匹配、在另一个中不匹配。
- AI 智能体的代理与算力:智能体的哪些部分真正需要代理。
ipvolt 正在为开发者和智能体构建代理基础设施。加入候补名单,开放访问时只发一封邮件。
参考来源
- RFC 9110: CONNECT
- anthropics/claude-code#56959: sandbox allowlist answering 403 to CONNECT
- SocketDev/socket-sdk-js#659: CI sandbox firewall blocking one host
- curl discussion #15718: proxy responses for http and https URLs
- Squid configuration: http_access suggested rules
- Bright Data: proxy error catalog
- Decodo: residential proxy restricted targets
- Node.js CLI: NODE_USE_ENV_PROXY