代理提示 TLS 握手失败?先看 timeout、EOF 还是证书错误
简答:先看报错后缀再排查:timeout 优先检查端口与丢包,EOF 核对 SNI、传输层和代理协议,证书错误检查时间与证书链;若使用 Reality,还要逐项核对 serverName、fingerprint、PublicKey 或 shortId。
定义:TLS 握手失败,是客户端与远端在正式传输数据前无法完成 TLS 协商,原因可能位于代理协议、域名校验、证书、线路、TUN 路由或 MTU。
先按报错分流:不要一看到 TLS 就改证书
本文是第三方使用指南,与相关客户端项目及开发者无隶属关系。第一步不是关闭证书校验,而是确认报错发生在哪一层:代理端口没有连通、TLS ClientHello 没有得到完整响应、证书校验失败,还是 TUN 或容器网络没有正确转发。
| 日志关键词 | 优先怀疑 | 先做什么 | 不要先做什么 |
|---|---|---|---|
timeout、handshake timeout | 服务器端口不可达、线路丢包、代理地址或端口写错;也可能是 TUN、Docker 或 WSL2 路由没有生效 | 先用客户端关闭 TUN 或改为系统代理,再测试同一个节点;确认代理本地端口确实在监听 | 不要直接把它判断成证书错误 |
EOF、tls handshake eof | 对端提前关闭连接,可能与 SNI、传输方式、协议头或中间设备有关 | 核对 serverName、传输方式、路径、服务端口和节点协议是否成套 | 不要把 EOF 固定归因于某一个域名被阻断 |
bad certificate、unknown certificate | 证书链、SNI、系统时间或本地 HTTPS 解密设备不匹配 | 先检查时间和 serverName,再确认是否存在企业网关或安全软件的 HTTPS 解密 | 不要为了绕过报错长期开启跳过证书验证 |
alert handshake failure | 双方 TLS 参数没有交集,例如 ALPN、TLS 版本或传输层参数不匹配 | 恢复默认 TLS 参数,逐项对照服务端要求 | 不要同时修改 ALPN、指纹、TLS 版本和传输方式 |
这张表是排障分流,不是单一错误码的绝对结论。判断标准是:同一个节点在普通系统代理下能连接、开启 TUN 后失败,优先查 TUN 路由和 MTU;所有模式都失败,则优先查节点参数、端口和服务端状态。
最快验证:先确认本地代理端口和代理协议
如果使用的是 Clash、Mihomo、sing-box 或其他本地客户端,先确认客户端界面显示的 HTTP、SOCKS 或 mixed 端口正在监听。端口号必须替换为你自己的本地端口,下面的 example.com 也要替换为实际测试域名。
- 确认端口是否监听。Windows PowerShell 可执行
Get-NetTCPConnection -LocalPort 7890 -State Listen;Linux 或 macOS 可执行lsof -nP -iTCP:7890 -sTCP:LISTEN。如果没有输出,说明客户端没有监听该端口,或端口号写错;先回到客户端设置核对。 - 用 curl 测试 HTTP 代理。执行
curl -v --proxy http://127.0.0.1:7890 https://example.com/。这里的http://表示“连接到一个 HTTP 代理”,不代表目标网站只能使用 HTTP。curl 官方文档说明,没有协议前缀或使用http://时按 HTTP 代理处理;https://则表示代理本身也使用 HTTPS。 - 区分代理协议。如果客户端只提供 SOCKS5 端口,应改用
curl -v --proxy socks5h://127.0.0.1:7891 https://example.com/。看到 CONNECT、TLS ClientHello 以及 HTTP 响应头,说明本地代理链路至少完成了一段;若一开始就提示无法连接代理,问题仍在本地端口或代理协议。
curl 的代理协议前缀和端口行为可核对curl 官方命令文档。验证时建议一次只改一个变量:先固定域名和端口,再切换 HTTP、SOCKS5、TUN,避免把多个问题混在一起。
环境变量最容易写错:HTTPS_PROXY 不等于 HTTPS 代理
如果错误发生在终端、构建工具或 Docker 中,先看环境变量指向的到底是什么代理。许多本地代理软件监听的是 HTTP 代理端口,即使它负责转发 HTTPS 网站,代理地址仍然应写成 http://127.0.0.1:端口。只有代理服务本身确实提供 HTTPS 代理协议时,才使用 https://。
例如,本地 HTTP 代理端口为 7890 时,可以临时执行:
export HTTP_PROXY=http://127.0.0.1:7890export HTTPS_PROXY=http://127.0.0.1:7890export NO_PROXY=127.0.0.1,localhost
Windows PowerShell 可写为 $env:HTTP_PROXY="http://127.0.0.1:7890" 和 $env:HTTPS_PROXY="http://127.0.0.1:7890"。判定标准是:执行工具前查看变量值,确认协议头与本地监听服务一致;如果把本地 HTTP 代理写成 https://127.0.0.1:7890 后才出现 TLS 握手失败,先改回 http://,不要先关闭证书校验。
Docker 需要区分 Docker daemon、Docker CLI、构建过程和容器本身。Docker 官方文档给出的 daemon 示例中,https-proxy 的值可以是 http://proxy.example.com:3128;修改 daemon 配置后需要重新加载并重启 Docker。Docker Desktop 不读取 daemon 的 daemon.json 代理设置,应在 Docker Desktop 的代理设置中配置。相关路径和生效范围请以Docker daemon 代理配置文档及Docker CLI 代理配置文档为准。
如果使用 Docker Engine 23.0 或更高版本,可在 daemon 配置中使用 proxies 字段;版本更低时不要直接假定该字段可用,应改用 systemd 环境变量或先升级。检查是否生效可执行 systemctl show --property=Environment docker;看到实际的 HTTP_PROXY、HTTPS_PROXY 和 NO_PROXY 值,才说明 daemon 读取到了配置。
客户端节点参数:先核对 SNI、传输层、ALPN 和时间
普通 TLS 节点中,serverName 通常同时参与 SNI 和证书名称校验。Xray 官方文档说明,证书的 SAN 应包含该值;如果填写的是 IP 地址,则不会发送 SNI。若节点地址、serverName、传输方式或服务端证书不匹配,可能出现证书错误或握手被提前关闭。
- 核对地址与端口:地址是服务器连接目标,端口必须与服务端监听端口一致。不要把面板展示的域名、证书域名和连接地址随意互换。
- 核对 serverName:普通 TLS 节点中,优先使用服务端明确提供的域名;不要把节点 IP 当作域名填写。若修改后证书错误从“域名不匹配”变为连接超时,说明可能同时改变了路由或服务端选择,需恢复后单变量测试。
- 核对 ALPN:Xray 文档将 ALPN 定义为 TLS 握手中的应用层协议列表,常见默认值为
h2和http/1.1。WebSocket 或 HTTPUpgrade 场景不要凭经验强行填h2;先按服务端配置使用http/1.1,否则可能无法完成后续握手。 - 核对系统时间:先打开系统自动校时,再重新连接。证书校验依赖本机时间;REALITY 服务端也可能配置允许的最大时间差。没有服务端配置或日志时,不要把“相差多少秒一定失败”写成固定规则;能核实的做法是把客户端与系统标准时间同步后复测。
关于 serverName、ALPN、证书校验和 fingerprint 字段,可查看Project X TLS 官方文档。如果开启了本地安全软件或企业网关的 HTTPS 解密,代理设备可能重新签发证书;此时应检查该设备是否正确签发叶子证书和完整链,而不是盲目把根证书导入客户端或长期开启 allowInsecure。Xray 官方资料也提示,跳过证书验证会降低安全性,且该选项已被标记为弃用。
VLESS Reality 单独排查:PublicKey、shortId 和 dest 不要混填
Reality 不是普通 TLS 的“换一个域名”设置。客户端和服务端必须使用对应的 Reality 参数,且不同内核的字段名称可能不同。Xray 当前文档把客户端旧字段 publicKey 更名为 password;sing-box 文档则使用 public_key。因此,先看客户端导入后的实际字段,不要只按教程中的旧名称判断。
- 确认内核支持并实际读取 Reality。在客户端的节点详情或日志中确认安全类型为
reality,而不是tls。具体最低版本因客户端和内核而异,本文不写未核实的统一版本号;如果配置导入后字段消失、被改写或校验失败,应先升级到客户端支持的内核版本,并查看该客户端的版本说明。 - 核对 serverName 与服务端允许值。Xray 文档要求客户端的
serverName是服务端serverNames允许的值,通常应与 target,也就是旧字段名 dest,保持一致或符合其证书 SAN。客户端不要填写服务端专用的target或dest字段;官方文档明确提示,客户端填入该字段可能导致配置被误判为服务端。 - 核对 fingerprint。Xray Reality 客户端要求 fingerprint,常见值包括
chrome、firefox、safari等。Reality 不支持用unsafe关闭 uTLS;如果导入配置后 fingerprint 为空、拼写错误或被客户端改写,先恢复服务端提供的值。 - 核对 PublicKey 或 password。Xray 当前文档中,客户端使用服务端私钥对应的公钥,字段名为
password,旧配置可能仍显示publicKey。它不是普通 TLS 证书公钥,也不是服务器地址。sing-box 使用public_key,两者不要混成证书指纹。 - 核对 shortId 格式。Xray 要求 shortId 为十六进制字符,最多 16 个字符,字符数必须为偶数;sing-box 文档写的是 0 到 8 个十六进制字符。实际填写必须与服务端配置中的值一致,不能把带连字符的 UUID、普通密码或大写之外的非十六进制字符直接填入。
以上字段可逐项对照Xray REALITY 官方配置文档和sing-box TLS 与 Reality 官方文档。如果只有 Reality 节点失败而普通 TLS 节点正常,优先逐项核对这五类字段;如果所有节点都失败,则先回到本地代理端口、系统时间和网络连通性。
直连正常、开启 TUN 或进入容器后超时:检查 MTU 和路由
如果普通系统代理可以访问,开启 TUN 后出现 timeout,或者宿主机正常而 Docker、WSL2 内部连接卡在 ClientHello,先做单变量对照:同一域名、同一节点、同一代理端口,只切换 TUN 或容器网络。这样可以判断问题是否出在虚拟网卡、路由或分片。
- 先确认 TUN 是否真的接管流量。检查客户端日志是否出现 TUN 启动、默认路由或 DNS 接管信息。Mihomo 文档说明,TUN 用于捕获系统流量,
strict-route会强化路由规则;sing-box 文档也说明auto_route会把默认路由设置到 TUN。若日志显示 TUN 未启动,先不要改 MTU。 - 在 Linux 路径上做分片测试。将
example.com替换为实际目标,执行ping -M do -s 1372 example.com。该命令用于 IPv4 不允许分片测试;如果提示需要分片,可逐步降低载荷,例如 1360、1340、1300,并记录“最后一个不需要分片的值”。这只是路径测试,不代表代理节点支持的最终 MTU。 - 在 Mihomo TUN 中设置 mtu。配置示例为
tun: enable: true mtu: 1400。1400 只是测试起点,不是通用最佳值;修改后重启 TUN,再用同一个目标复测。Mihomo 官方文档说明,MTU 会影响极端条件下的传输速率,普通用户可以保留默认值。 - 在 sing-box TUN 中设置 mtu。配置示例为
{"type":"tun","interface_name":"tun0","mtu":1400,"auto_route":true}。如果使用非特权模式,sing-box 文档提示地址和 MTU 不会自动配置,必须确认配置准确并具备所需权限。 - 确认结果。只有在“降低 MTU 后同一节点、同一域名从超时变为成功,且普通系统代理仍正常”时,才可以把 MTU 视为可能因素;若结果不变,应恢复原值,继续检查路由、DNS、端口和节点参数。
MTU 字段和 TUN 行为可查看Mihomo TUN 官方文档与sing-box TUN 官方文档。不要把“1300 或 1400”写成所有网络都适用的固定答案,也不要同时修改 DNS、MTU、指纹和传输方式,否则无法判断真正原因。
证书报错与 CaUsedAsEndEntity:Rust 工具不要套用 Node.js 解法
如果日志出现 CaUsedAsEndEntity,不要直接把它等同于“缺少根证书”。它通常表示验证链中把 CA 证书当成了终端实体证书使用;在代理或安全软件进行 HTTPS 解密的场景,应检查其是否正确签发叶子证书、是否提供完整链,以及本地客户端是否信任正确的证书链。
对于使用 Rust/rustls 的工具,NODE_EXTRA_CA_CERTS 不适用,因为这是 Node.js 的环境变量。正确方向是关闭本地 HTTPS 解密、让企业网关签发正确的叶子证书,或按照该 Rust 工具自身文档配置证书信任。不要把 Node.js 环境变量当作通用 TLS 修复方法。
如果只是代理客户端的普通 TLS 节点报证书错误,先检查系统时间、serverName 与证书 SAN,再检查客户端是否开启了不安全选项。Xray 文档将 allowInsecure 定义为不验证远端证书有效性,并明确提示其存在中间人风险;因此它最多用于受控的短时定位,不应作为常规修复。
一套可复用的排查顺序
- 记录完整日志:保留报错前后 10 行,区分
timeout、EOF、证书错误和协议协商失败。 - 确认本地端口:检查 HTTP、SOCKS 或 mixed 端口是否监听,端口类型是否与命令或系统代理设置一致。
- 关闭 TUN 做对照:普通系统代理成功、TUN 失败时,转向路由、DNS、MTU 和虚拟网卡。
- 核对节点基本参数:地址、端口、协议、传输方式、路径、TLS 开关、serverName 和 ALPN。
- Reality 单独核对:确认内核实际支持,检查 fingerprint、serverName、PublicKey 或 password、shortId,不要把服务端 dest 填到客户端。
- 最后才检查证书链:确认系统时间、企业 HTTPS 解密、证书 SAN 和本地信任库;不要用关闭校验掩盖问题。
每次只修改一个字段,并记录“修改前日志、修改后日志、是否能复现”。如果同一节点在不同网络、不同客户端和不同模式下结果完全一致,才有必要向节点提供方反馈;仅凭一次 EOF 或 timeout,无法证明是某个域名、线路或服务商的固定问题。
常见问题
代理提示 TLS handshake timeout,先改证书还是改节点?
先不要改证书。确认本地代理端口在监听,再关闭 TUN 做对照;如果普通代理也超时,优先检查地址、端口、丢包和节点参数;如果只有 TUN 超时,再查路由与 MTU。
为什么 HTTPS_PROXY 写成 https://127.0.0.1:7890/ 可能失败?
因为 HTTPS_PROXY 的协议头描述的是“代理本身使用什么协议”。如果 7890 监听的是 HTTP 代理,应该写 http://127.0.0.1:7890;写成 https:// 后,客户端会尝试用 TLS 连接代理本身,可能在代理握手阶段失败。
TLS 握手失败时,系统时间相差几秒会出问题?
没有适用于所有客户端和节点的统一秒数阈值。几秒误差通常不应直接视为证书失败依据,但应先开启自动校时;Reality 还可能由服务端配置最大允许时间差。同步时间后再复测,比套用固定数字更可靠。
VLESS Reality 的 PublicKey、serverName 和 shortId 怎么核对?
PublicKey 或 password 应来自服务端 Reality 参数,不能当作证书公钥;serverName 必须是服务端允许的 serverNames 之一;shortId 必须与服务端值一致,并符合内核要求的十六进制格式。Xray 和 sing-box 的字段名称及长度规则可能不同,应按实际内核文档核对。
CaUsedAsEndEntity 能不能设置 NODE_EXTRA_CA_CERTS?
不能把它当作通用解法。NODE_EXTRA_CA_CERTS 是 Node.js 环境变量,对 Rust/rustls 工具不生效。应检查 HTTPS 解密设备是否签发了正确的叶子证书和完整链,并按具体 Rust 工具的证书信任文档处理。
MTU 调到 1300 或 1400 就一定能解决 TLS 超时吗?
不一定。1300 和 1400 只能作为测试值,最终应以路径分片测试和同一节点复测为准。只有降低 MTU 后问题稳定消失,才可以把 MTU 视为可能因素;否则应恢复原值继续查路由、DNS、端口和节点配置。