分析阅读约 3 分钟

为什么你的编程智能体会遇到 “CONNECT tunnel failed, response 403”

智能体的 curl 报出 CONNECT tunnel failed, response 403,而其他主机都正常。这是代理拒绝建立隧道。本文说明如何判断是谁拦截了它,以及该怎么修。

本页内容
code
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,消息如下:

code
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:

code
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 对测试代理的输出:

code
*   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 403

Trying 行显示的是代理,而不是网站。403 是对 CONNECT 的应答,之后没有任何 TLS 相关的行,所以网站从未参与。如果 403 出现在 CONNECT 成功并完成 TLS 握手之后,那它来自网站,问题不在代理。

接着,通过同一个代理比较一个允许的主机和一个被拦截的主机,用 write-out 把两个状态码分开:

sh
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.0curl 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)是它自己的;真实的沙箱代理或服务商代理会返回不同的文字。

相关阅读

ipvolt 正在为开发者和智能体构建代理基础设施。加入候补名单,开放访问时只发一封邮件。

参考来源

  1. RFC 9110: CONNECT
  2. anthropics/claude-code#56959: sandbox allowlist answering 403 to CONNECT
  3. SocketDev/socket-sdk-js#659: CI sandbox firewall blocking one host
  4. curl discussion #15718: proxy responses for http and https URLs
  5. Squid configuration: http_access suggested rules
  6. Bright Data: proxy error catalog
  7. Decodo: residential proxy restricted targets
  8. Node.js CLI: NODE_USE_ENV_PROXY

标签:ProxiesTroubleshooting