代理指南

ERR_TUNNEL_CONNECTION_FAILED 怎么解决?

核对 Chrome 当前代理、HTTPS CONNECT 响应、鉴权和目标限制,通过 curl 检查区分隧道失败与代理连接失败。

连接代理、接受 CONNECT 并与 HTTPS 目标建立 TLS 的顺序

概览

操作流程

  1. 确认实际线路

    记录发生故障的运行环境使用的代理主机、端口和协议。

  2. 检查 CONNECT 并重试

    读取 CONNECT 响应,修复对应问题,再访问原始目标。

ERR_TUNNEL_CONNECTION_FAILED 表示 Chromium 无法通过配置的代理建立隧道。先确认 Chrome 实际使用哪个代理,再检查代理对 HTTPS CONNECT 请求的响应。密码问题、不支持的目标端口与上游连接故障,需要采取不同的修复措施。

这个错误也可能出现在 Edge,或由 Playwright、Puppeteer 启动的 Chromium 浏览器中。请从实际发起请求的设备或容器里排查。

下一步

失败的是代理连接,还是 HTTPS 隧道?

连接代理、接受 CONNECT 并与 HTTPS 目标建立 TLS 的顺序
先连接代理并打开 CONNECT 隧道,再与目标进行 TLS 握手。

通过 HTTP 代理发起 HTTPS 请求时,客户端先连接代理,再请求代理连接目标主机和端口。隧道建立后,客户端才能通过这条线路与目标协商 TLS。

观察到的结果

优先排查

ERR_PROXY_CONNECTION_FAILED

客户端能否访问配置的代理端点

ERR_TUNNEL_CONNECTION_FAILED

CONNECT 交换、代理策略、鉴权和后续连接

代理返回 HTTP 407

要求的代理鉴权方式与所提供的凭据

隧道建立后出现证书校验错误

相关 TLS 连接的证书链和主机名

这些结果对应不同的失败阶段。Chromium 在网络错误定义中,将隧道错误与代理连接错误分开定义。仅凭这一错误,无法确定服务商的哪个组件发生故障,也不能证明目标服务本身正常。

1. 确认当前实际生效的代理

检查 Chrome 的系统代理设置入口、当前浏览器配置中启用的代理扩展,以及会写入系统代理的 VPN 或本地客户端。受管理设备还可能通过策略或 PAC 文件选择线路。修改前先记录实际使用的主机、端口和协议。

如果端点是 127.0.0.1,代理必须在同一个网络环境里运行。在容器中,这个地址指向容器自身。如果旧设置已不再需要,应通过拥有该设置的应用将其移除,再重新测试。组织要求保留的线路应由管理员管理。

使用 MIYAIP 时,应填写分配的连接端点,而不是界面显示的出口 IP。原生与中转连接可能需要不同的网络路径。静态代理接入教程介绍了连接字段和路由条件。

2. 通过同一个 HTTP 代理测试 HTTPS 请求

替换下面三个占位字段后,在 macOS 或 Linux 运行命令。只提供用户名时,curl 会提示输入代理密码。示例目标是 example.com;随后可以改用失败的 HTTPS 目标进行比较。代理地址应采用服务商实际提供的 HTTP 或 HTTPS 协议。

curl --proxy 'http://PROXY_HOST:PROXY_PORT' \
  --proxy-user 'PROXY_USERNAME' --noproxy '' \
  --connect-timeout 10 --max-time 30 \
  --silent --show-error --output /dev/null \
  --write-out 'connect_status=%{http_connect} target_status=%{http_code}\n' \
  'https://example.com/'

Windows 请使用 curl.exe,将命令写在一行,并把 /dev/null 替换为 NUL。检查实际运行环境中的 NO_PROXY 排除设置;较旧的 PowerShell 版本不一定按预期传递空的原生命令参数。

connect_status 表示 HTTP 代理的 CONNECT 响应。000 表示未记录到此类响应码,本身不说明原因。CONNECT 成功与目标响应成功是两个独立的观察结果。对于 SOCKS5 代理,请使用对应协议的连接示例,不要期待获得 HTTP CONNECT 状态。

3. 根据响应决定下一步检查

结果

下一步操作

CONNECT 407

确认代理凭据、账户状态和鉴权模式。账户登录密码或 API token 并不自动等同于代理密码

CONNECT 403

检查代理对目标地址和端口的策略。如果这条线路本应允许访问,向支持人员提供目标主机、端口和时间戳

CONNECT 429

降低请求并发,并遵循服务商的重试建议。反复重发相同请求可能延长问题持续时间

CONNECT 502/503/504

与另一个允许访问的目标比较,并检查服务商状态。记录问题只影响一个目标,还是影响所有目标

CONNECT 成功,但下一步失败

检查实际报告的 TLS 或目标错误;此时隧道本身已经打开

curl 成功,但 Chrome 失败

将浏览器配置、扩展、PAC 规则和鉴权方式与成功测试所用配置进行比较

在线代理检查器可以提供另一个观察位置。它的服务器与笔记本、办公网络或容器可能使用不同线路,因此远程检查成功不能替代同设备测试。

验证原始目标

回到原先的 Chrome 配置,重新打开失败的 URL。然后使用 IP 与浏览器环境检查,查看实际观察到的出口。请同时保留目标访问结果和出口结果;只有其中一项,还不足以验证整条线路。

常见问题

为什么 HTTP 页面可以访问,HTTPS 页面却失败?

HTTP 代理可能接受普通 HTTP 请求,却拒绝 CONNECT 请求或某些目标端口。修改 DNS 或清理浏览器存储前,应先比较 CONNECT 响应。

应该先清除 Cookie 吗?

Cookie 无法修复被拒绝的 CONNECT 请求。代理和 HTTPS 路径正常后,再检查网站会话问题。

更换代理 IP 能解决这个错误吗?

取决于原因。错误的密码或被禁止的目标端口,可能在任何出口上都失败。应先纠正已经识别的原因,再考虑更换产品或地区。

确认端点和浏览器线路

测试分配的连接字段,再从实际执行任务的浏览器中检查出口和目标。

下一步

运行本地演示

下载 network_demo.py,或将以下完整代码保存为同名文件。使用 Python 3 运行 python network_demo.py。服务仅监听 127.0.0.1:8766,使用完按 Ctrl+C 退出。

"""Local HTTP demonstration. No external requests, credentials or MIYAIP service calls."""
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlsplit
import json
import time


class DemoHandler(BaseHTTPRequestHandler):
    def log_message(self, *_):
        pass

    def reply(self, code, body=b"", headers=None):
        self.send_response(code)
        for key, value in (headers or {}).items():
            self.send_header(key, value)
        self.send_header("Content-Length", str(len(body)))
        try:
            self.end_headers()
            self.wfile.write(body)
        except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
            pass

    def do_CONNECT(self):
        self.reply(407, headers={"Proxy-Authenticate": 'Basic realm="local-demo"'})

    def do_GET(self):
        path = urlsplit(self.path).path
        if path == "/redirect":
            return self.reply(302, headers={"Location": "/headers"})
        if path == "/loop":
            return self.reply(302, headers={"Location": "/loop"})
        if path == "/slow":
            time.sleep(2)
        if path in {"/headers", "/slow"}:
            payload = {"method": self.command, "headers": dict(self.headers)}
            return self.reply(200, json.dumps(payload, indent=2).encode(),
                              {"Content-Type": "application/json"})
        self.reply(404, b"Use /headers, /redirect, /loop or /slow")

    def do_POST(self):
        body = self.rfile.read(int(self.headers.get("Content-Length", "0")))
        if urlsplit(self.path).path == "/redirect307":
            return self.reply(307, headers={"Location": "/headers"})
        if urlsplit(self.path).path == "/redirect302":
            return self.reply(302, headers={"Location": "/headers"})
        payload = {"method": self.command, "headers": dict(self.headers),
                   "body": body.decode("utf-8", errors="replace")}
        self.reply(200, json.dumps(payload, indent=2).encode(),
                   {"Content-Type": "application/json"})


if __name__ == "__main__":
    print("Local demonstration: http://127.0.0.1:8766 (Ctrl+C to stop)", flush=True)
    ThreadingHTTPServer(("127.0.0.1", 8766), DemoHandler).serve_forever()

来源