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

Source: https://ipvolt.com/zh/blog/connect-tunnel-failed-403
Markdown: https://ipvolt.com/zh/blog/connect-tunnel-failed-403.md
Language: zh-CN

[ipvolt 首页](https://ipvolt.com/zh.md) / [博客](https://ipvolt.com/zh/blog.md) / CONNECT tunnel failed 403：智能体为何遇到

分析
发布于: 2026-10-02
更新于: 2026-10-02
作者： ipvolt
阅读约 3 分钟

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

```text
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`，消息如下：

```text
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`：

```text
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](https://www.rfc-editor.org/rfc/rfc9110.html#name-connect)

由此可以得出两点：

- **这不是凭据问题。** 需要凭据的代理会返回 407 并附带 `Proxy-Authenticate` 质询；那种情况见 [407 指南](/zh/guides/fix-proxy-error-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](https://github.com/anthropics/claude-code/issues/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](https://github.com/SocketDev/socket-sdk-js/issues/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 对测试代理的输出：

```text
*   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` 中的主机会绕过代理直接连接，而沙箱可能以另一种方式拦截直连。[代理环境变量指南](/zh/guides/proxy-environment-variables) 解释了客户端如何在这些变量之间做选择，[NO_PROXY matching, tested](/zh/blog/no-proxy-matching-tested) 则表明各个客户端对 `NO_PROXY` 条目匹配什么并没有一致的理解。Node 内置的 `fetch` 会忽略这些变量，除非设置了 `NODE_USE_ENV_PROXY=1` 或 `--use-env-proxy`，所以同一个 shell 里的 Node 脚本和 curl 命令可能走不同的路线。[Node.js CLI 文档](https://nodejs.org/api/cli.html#node_use_env_proxy1)

## 不绕开代理的修复方法

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](http://www.squid-cache.org/Doc/config/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 错误目录](https://docs.brightdata.com/proxy-networks/errorCatalog) Decodo 列出了默认限制的目标类别，例如银行和政府网站。[Decodo 受限目标](https://help.decodo.com/docs/residential-proxy-restricted-targets)

不要绕过沙箱的出口规则。为了避开白名单而取消 `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](https://github.com/curl/curl/discussions/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 日用 [下载目录](/downloads/connect-tunnel-failed-403/README.md) 中的实验环境记录的：一个运行在 `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](/downloads/connect-tunnel-failed-403/results.json)。测试代理的 403 响应体（`403 Forbidden: lab proxy policy`）是它自己的；真实的沙箱代理或服务商代理会返回不同的文字。

## 相关阅读

- [修复代理错误 407](/zh/guides/fix-proxy-error-407)：代理要求提供凭据时。
- [修复 ERR_TUNNEL_CONNECTION_FAILED](/zh/guides/fix-err-tunnel-connection-failed)：Chromium、Playwright 和 Puppeteer 中的同一种拒绝。
- [代理环境变量](/zh/guides/proxy-environment-variables)：客户端实际读取的是哪个变量。
- [NO_PROXY matching, tested](/zh/blog/no-proxy-matching-tested)：为什么同一条绕过条目在一个客户端中匹配、在另一个中不匹配。
- [AI 智能体的代理与算力](/zh/blog/ai-agent-proxies-and-compute)：智能体的哪些部分真正需要代理。

ipvolt 正在为开发者和智能体构建代理基础设施。[加入候补名单](https://ipvolt.com/#waitlist-hero)，开放访问时只发一封邮件。

## 参考来源

- [RFC 9110: CONNECT](https://www.rfc-editor.org/rfc/rfc9110.html#name-connect)
- [anthropics/claude-code#56959: sandbox allowlist answering 403 to CONNECT](https://github.com/anthropics/claude-code/issues/56959)
- [SocketDev/socket-sdk-js#659: CI sandbox firewall blocking one host](https://github.com/SocketDev/socket-sdk-js/issues/659)
- [curl discussion #15718: proxy responses for http and https URLs](https://github.com/curl/curl/discussions/15718)
- [Squid configuration: http_access suggested rules](http://www.squid-cache.org/Doc/config/http_access/)
- [Bright Data: proxy error catalog](https://docs.brightdata.com/proxy-networks/errorCatalog)
- [Decodo: residential proxy restricted targets](https://help.decodo.com/docs/residential-proxy-restricted-targets)
- [Node.js CLI: NODE_USE_ENV_PROXY](https://nodejs.org/api/cli.html#node_use_env_proxy1)

## 第一时间了解 ipvolt 开放体验。

顺便一提

ipvolt 仍在开发中。留下邮箱，开放体验时我们只会通知你一次。

开放体验时仅发一封通知邮件，不发送其他邮件。

[申请抢先体验](https://ipvolt.com/zh/blog/connect-tunnel-failed-403#waitlist-blog-end)

[隐私政策](https://ipvolt.com/privacy)


## 相关文章

- [日本价格解析：日元、全角字符与税込/税抜标签](https://ipvolt.com/zh/blog/japanese-price-parsing.md) (分析, 2026年10月6日, 阅读约 2 分钟): 解析日本价格字段，同时不丢失币种和计税基础。运行一套经过测试的 Python 夹具，覆盖全角日元、混合价格以及刻意设计的待复核用例。
- [SOCKS5 与 SOCKS5h 的区别：6 个客户端实测](https://ipvolt.com/zh/blog/socks5-vs-socks5h.md) (对比, 2026年10月4日, 阅读约 3 分钟): socks5:// 并不是在每个客户端里都表示本地 DNS。我们记录了 curl、Requests、HTTPX、aiohttp、Playwright 和 Node 在每种协议写法下发给 SOCKS5 代理的地址。
- [博彩赔率 API 与抓取对比：成本工作表](https://ipvolt.com/zh/blog/betting-odds-api-vs-scraping.md) (对比, 2026年9月28日, 阅读约 2 分钟): 用一份可编辑的工作表，按覆盖范围、数据新鲜度和建模采集成本比较博彩赔率 API 与经授权的网页抓取，并清楚写明全部工作量假设和计算方法。

## 相关指南

- [不靠猜测修复代理错误 407](https://ipvolt.com/zh/guides/fix-proxy-error-407.md): 按可重复的顺序诊断 Proxy Authentication Required：网关、认证方式、凭据编码、账户范围和客户端配置。
- [修复 ERR_TUNNEL_CONNECTION_FAILED 错误](https://ipvolt.com/zh/guides/fix-err-tunnel-connection-failed.md): 解释 ERR_TUNNEL_CONNECTION_FAILED 与 ERR_PROXY_CONNECTION_FAILED，附经验证的失败对照表，并说明如何找回 Chromium 隐藏的 CONNECT 状态码。
- [代理环境变量：HTTP_PROXY 与 NO_PROXY](https://ipvolt.com/zh/guides/proxy-environment-variables.md): 通过一个隔离的本地检查，诊断 HTTP_PROXY、HTTPS_PROXY、ALL_PROXY 和 NO_PROXY 在 curl、Python Requests 和 Node.js 中的路由差异。

## 关于 ipvolt

来自 ipvolt 团队的技术分析。

ipvolt 尚未开放使用。

[阅读英文原文](https://ipvolt.com/blog/connect-tunnel-failed-403.md)
