命令行工具

unexpected eof while reading:OpenSSL 3 连接被提前关闭

作者: · 最后核实

报错信息error:0A000126:SSL routines::unexpected eof while reading

error:0A000126:SSL routines::unexpected eof while reading 的意思是:对方没有先发送 TLS 的 close_notify 告警,就直接关闭了 TCP 连接。 关键是看它发生在什么时候:如果在握手阶段就断了,说明服务器或中间设备拒绝了你,这是连通性问题;如果服务器已经把响应发完才断,通常只是对方关连接的方式不规范,数据可能是完整的。

这条报错是什么意思

这条报错说明对方没按 TLS 规范发 close_notify 就关了连接,OpenSSL 3.0 起把这种情况报成错误。TLS 用“关闭告警”来结束连接。现行的 TLS 1.3 规范 RFC 9846 规定,除非已经发过错误告警,每一方在关闭写方向之前都必须发送 close_notify。没有这个告警,接收方就无法判断数据是全部到了,还是被攻击者中途截断了(截断攻击)。

OpenSSL 3.0 改变了“缺少 close_notify”的报告方式。3.0 之前,SSL_read() 返回 SSL_ERROR_SYSCALL,错误栈为空,errno 为 0,很多程序把它当成正常关闭。3.0 起改为返回 SSL_ERROR_SSL,原因是 SSL_R_UNEXPECTED_EOF_WHILE_READING(错误码 0A000126)。事情本身不新鲜,新的是这条报错。

它会以不同的样子出现:

curl: (56) OpenSSL SSL_read: error:0A000126:SSL routines::unexpected eof while reading, errno 0
ssl.SSLEOFError: [SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol (_ssl.c:1006)

Python 把这个原因映射成 SSLEOFError(见 EOF occurred in violation of protocol);PHP 会把它放在 “SSL operation failed” 警告里(见 Composer SSL operation failed)。如果 curl 是在握手阶段而不是读数据时遇到它,退出码会是 35(见 curl 35)。

常见原因(按出现频率排序)

最常见的是服务器响应完直接断开、不发 close_notify,这时数据通常是完整的。

1. 服务器响应完直接断开,不发 close_notify

有些服务器、框架、代理和负载均衡处理完请求就直接关闭 TCP,或者在空闲超时时发 RST。响应已经送达,只是少了“告别”。

  • 确认: curl -v https://example.com/ 在报错之前已经打印出完整的响应头和响应体,状态码和内容都正常。
  • 修复: 服务器归你管就升级服务器或负载均衡,让它正常关闭连接。客户端忽略这个告警,只在上层协议能检测截断时才可以(见下文)。

2. 握手被直接掐断,而不是正常拒绝

服务器不接受 ClientHello 时,本应回一个 protocol_version 或 handshake_failure 告警,有些服务器却直接关连接。常见原因:服务器只支持老旧的 TLS 版本或加密套件;客户端没发 SNI 而服务器要求 SNI;这个端口不是 TLS 服务。

  • 确认: 分别测试协议版本和 SNI:
openssl s_client -connect example.com:443 -servername example.com -tls1_3 </dev/null
openssl s_client -connect example.com:443 -servername example.com -tls1_2 </dev/null
openssl s_client -connect example.com:443 </dev/null      # 不带 SNI

某一种能完成握手(显示 Protocol 和 Cipher),另一种报这个错误,就找到了不匹配的地方。SSL 检测工具可以列出服务器接受的协议版本和加密套件。

  • 修复: 服务器开启 TLS 1.2 和 1.3;客户端要发送 SNI(用 openssl s_client 时要带 -servername)。

3. 防火墙、IPS 或代理切断了连接

做 HTTPS 检查、拦截特定目标或限制连接数的安全设备,经常直接发 TCP RST 或静默关闭。典型表现是换一个网络同样的命令就正常。

  • 确认: 换网络对比,并抓包看是谁关的连接:
sudo tcpdump -nn -i any host 203.0.113.10 and port 443

ClientHello 之后马上出现 RST(Flags [R]),说明是链路上的设备干的。

  • 修复: 请网络管理员放行目标地址,或者把它加入 HTTPS 检查的白名单。

4. 复用了已经被关闭的长连接

客户端在服务器空闲超时之后复用连接池里的旧连接,第一次读取就会遇到 EOF。

  • 确认: 报错是间歇性的,而且常出现在一段空闲之后。
  • 修复: 客户端的空闲超时设得比服务器、负载均衡更短,或者对幂等请求重试一次。

5. 服务器过载或限流

服务器工作进程、连接数用完时,可能一建立连接就关闭。对照报错时间查看服务器日志和连接数限制。

分平台修复命令

服务器端的修法是正常关闭连接;客户端只能在上层协议能检测截断时忽略这个错误。

服务器(Nginx): 当前 Nginx 源码会在 SSL 上下文里设置 SSL_OP_IGNORE_UNEXPECTED_EOF,升级老版本 Nginx 就能消除日志里的这类噪音。宝塔面板用户可以在软件商店里升级 Nginx。其他服务器则升级 TLS 库,并让应用、反向代理、负载均衡三者的空闲超时保持一致(客户端侧最短)。

Python 客户端(3.10+,OpenSSL 3),仅限上层协议能检测截断时:

import ssl
ctx = ssl.create_default_context()      # 证书校验保持开启
ctx.options |= ssl.OP_IGNORE_UNEXPECTED_EOF

任何 OpenSSL 3.2 及以上的程序: 用一个只给该进程用的配置文件(OPENSSL_CONF=/path/file.cnf),在 system_default 段里写 Options = IgnoreUnexpectedEOF。OpenSSL 3.0 和 3.1 没有这个配置项。

Windows: 这条报错只会出现在使用 OpenSSL 的程序里,比如 python.org 的 Python。使用 Windows 自带 Schannel 的程序会以别的报错表示连接被断开,见 Windows Schannel 报错。

国内服务器排查要点: 访问境外服务时,跨境链路丢包或被中间设备重置都会表现为这个错误,可以先在同地域的另一台云服务器上复现,排除本机问题;云厂商安全组、云防火墙的连接数限制也值得查一下。

不推荐:关闭证书校验

curl -k、verify=False 这类参数对这个错误没有作用,因为问题出在连接关闭而不是信任上。它们唯一的效果是让链路上的任何人都能冒充服务器、读走你的账号密码。

同样不要为了“让报错消失”而全局设置 SSL_OP_IGNORE_UNEXPECTED_EOF。OpenSSL 文档要求只在上层协议能检测截断、并且应用确实做了检查时才启用。更安全的做法是:修服务器或负载均衡、对齐超时设置、对幂等请求重试,只在使用定长协议(如 HTTP/1.1)的特定客户端里打开这个选项。

怎么确认已经修好

修好后,握手能完成并显示 Verify return code: 0 (ok),curl 也不再报 56 或 35 错误。

openssl s_client -connect example.com:443 -servername example.com -verify_return_error </dev/null
curl -sS -o /dev/null -w '%{http_code} %{ssl_verify_result}\n' https://example.com/

第一条命令应显示 Protocol、Cipher 和 Verify return code: 0 (ok);如果这些之后因为 s_client 关闭了自己的标准输入而多出一行 EOF,不算失败。第二条命令应输出 HTTP 状态码和 0,不再出现 curl 56 或 35 错误。更多连接测试命令见连接测试速查。

常见问题

这是证书问题吗?

不是。它说的是连接“怎么结束的”,与信任无关。证书问题会报 unable to get local issuer certificate、certificate has expired 这类校验错误。关闭证书校验不会让这个错误消失。

为什么升级到 OpenSSL 3 之后才出现?

对方不发 close_notify 就断开,这件事以前很可能就在发生。OpenSSL 1.1.1 把它报告为 SSL_ERROR_SYSCALL 且 errno 为 0,很多程序直接忽略了;OpenSSL 3.0 改为 SSL_ERROR_SSL 加 SSL_R_UNEXPECTED_EOF_WHILE_READING,同一件事现在就变成了报错。

设置 SSL_OP_IGNORE_UNEXPECTED_EOF 安全吗?

只有在 TLS 之上的协议能自己检测截断时才安全。带 Content-Length 或分块传输的 HTTP/1.1 可以;“一直读到连接关闭为止”的协议不行,攻击者可以悄悄把数据截短。OpenSSL 文档给出的条件也是这样。

为什么用 openssl s_client 加 </dev/null 测试时也会出现?

s_client 读到标准输入结束就会关闭自己这一侧,随后服务器断开连接,有时不发 close_notify。OpenSSL 手册建议非交互使用时加 -ign_eof,TLS 1.3 尤其如此。判断服务器是否正常,要看前面的 Protocol、Cipher、Verify return code 几行,而不是最后一行报错。

资料来源

  1. OpenSSL 3.0: SSL_get_error (unexpected EOF behaviour)
  2. OpenSSL 3.0: SSL_CTX_set_options (SSL_OP_IGNORE_UNEXPECTED_EOF)
  3. RFC 9846: TLS 1.3, section 6.1 Closure Alerts
  4. Python docs: ssl.SSLEOFError and OP_IGNORE_UNEXPECTED_EOF