16.5 小时,我打掉了两只「SSL 握手失败」的怪

T 日 22:11,某央企财务公司(下称 A 机构)上线当天向某金融电子回单/查验平台(下称 P 平台)的 T2001 接口发消息,客户端直接抛异常:

javax.net.ssl.SSLHandshakeException: Remote host terminated the handshake

at sun.security.ssl.SSLSocketImpl.handleEOF(SSLSocketImpl.java:1379)

at sun.security.ssl.SSLSocketImpl.readHandshakeRecord(SSLSocketImpl.java:1107)

at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:400)

at sun.net.www.protocol.https.HttpsURLConnectionImpl.getOutputStream(...)

at cn.hutool.http.HttpConnection.getOutputStream(...)

at cn.hutool.http.HttpRequest.sendFormUrlEncoded(HttpRequest.java:1247)

Suppressed: java.net.SocketException: 断开的管道 (Write failed)

Caused by: java.io.EOFException: SSL peer shut down incorrectly

第二天 14:49 才验证通过,全程 16.5 小时。复盘时最大的体会是:这不是一个故障,是两个故障叠在一起------协议层不匹配 + 网络层白名单未就绪。它们互相打掩护,你修好一个,另一个还在装死。

下面按打怪进度写,每一关后面掉落一条经验值。你可以当 TLS 排障手册看,也可以当一次生产事件协同的样本看。

LV1 先拿到怪的身份证:读堆栈

三行异常,信息量很大:

  • Remote host terminated the handshake:对端主动断开,不是本地超时;
  • SSL peer shut down incorrectly:对端没有按 TLS 规范发 close_notify,是"无声关闭";
  • 断开的管道 (Write failed):写的时候连接已经没了。

再叠加调用栈位置------失败发生在 startHandshake 阶段,getOutputStream 之前。也就是说:握手刚开始就被对面掐了,连 ServerHello 都没正常拿到。

这就排掉了两类常见嫌疑:证书链不受信(那会是 PKIX path building failed)、超时或网络抖动(那会是 SocketTimeoutException)。剩下的最大嫌疑只有一个:协议或套件不兼容。

经验值 1:堆栈是怪的身份证。先读异常类型和抛出位置,再决定动谁------重启、换机器、抓包都不是第一步。

LV2 分层定界:telnet 通 ≠ 握手通

金融生产网络里,一次 HTTPS 调用至少要穿三层,每层只回答一个问题:

|-----------|---------------------------|-------------------|
| 层 | 工具 | 回答的问题 |
| L4 TCP | telnet / nc | 端口通不通(路由、防火墙、白名单) |
| L6 TLS 握手 | openssl s_client / gmcurl | 协议与套件匹不匹配、证书对不对 |
| L7 业务 | 真实业务报文 | 报文能不能正常返回并落账 |

本案里,A 机构内网机器(192.168.10.69,示意)telnet 服务端 443 是通的 ,但握手失败------问题定位到 TLS 层。可另一条路径上 telnet 又不通,这说明:还有第二只怪。

这里还藏着一个行业背景:P 平台服务端启用的是国密 SSL(TLCP,源自 GM/T 0024,现对应国标 GB/T 38636-2020),只接受 SM2/SM3/SM4 国密套件;而 A 机构用的是 hutool 封装的 JDK HttpsURLConnection,默认只发国际 TLS(TLSv1.2/1.3 + RSA/ECDHE 套件)。两边语言不通,服务端在 ServerHello 阶段直接关闭连接------客户端看到的就是那句 Remote host terminated the handshake。

经验值 2:把"通不通"拆成三层问。telnet 通 ≠ 握手通,握手通 ≠ 协议匹配,验证顺序不能倒。

判断国密还是不匹配,需要一把国密客户端。JDK 和原生 OpenSSL 默认都不支持 TLCP,P 平台侧惯用的是 GmSSL 生态的 gmcurl(--gmssl 参数)。

麻烦在于:目标服务器是离线的。于是排障变成了流水线作业------从 curl.gmssl.cn 下载 gmcurl_linux_x64 独立可执行文件 → 人工上传 → 重命名为 gmcurl → 加执行权限 → 才能跑第一条命令。这一段纯体力活,吃掉约 1 小时。

经验值 3:排障的"最后一公里"经常不是技术,而是包在哪。面向客户要预置离线工具包(各平台版本 + 校验值 + 一页手册),别让所有人现场下载。

LV4 方向纠偏:你到底在测谁

第一版验证命令长这样(示意):

./gmcurl -X POST -vk --gmssl \

--resolve p-gw.example.internal:443:10.100.4.40 \

https://p-gw.example.internal:443/.../T2001

--resolve 把域名指到了 10.100.4.40------那是 A 机构的专线出口 IP,不是 P 平台服务端(10.100.1.12)。网络方一句话把队伍拉回来:"客户端不应直连出口 IP,要验证服务端真实 IP。"

同期还有另一个岔路:有人提出"是不是要加回指路由"。这个猜测和本次故障无关,但它在群里挂了几十分钟,把注意力带走了一次。

经验值 4:动手前先画链路图,标清每个 IP 的身份(客户端 / 出口 / 负载均衡 / 服务端)。测错对象,所有结论都是噪音;每一个"要不要改一下路由"的提议,都先问一句它作用在哪一层。

LV5 第二只怪现身:白名单与 29 位掩码

纠正目标地址后,服务端视角 telnet 10.100.1.12 443 通了。但业务方向仍然不通,而且报错换了面孔:

telnet 10.100.4.40 443

Connection closed by foreign host

这不是 TLS 问题------TCP 层就被主动 RST/关闭,典型的防火墙/负载白名单拒绝特征。于是白名单整改开始:

  1. 加入 A 机构专线段 10.100.2.20/30 → 仍不通;

  2. 加入实际出口 10.100.4.40 → 仍不通;

  3. 补入其他出口 IP → 仍不通;

  4. 网络方复查发现:白名单掩码误配成了 29 位,网段覆盖范围与实际出口不匹配。修正后,telnet 立刻通了。

从 14:28 第一次加白,到 14:44 掩码修正,中间 16 分钟只因为一位掩码写错;而它叠在前一个问题身上,把整场排障拖成了 16.5 小时。

经验值 5:修好了还不通,说明还有第二个根因。"加白了还不通"不要重复加白,去查掩码位、生效对象、策略下发顺序。双根因事件的通用打法是:不假设"只有一个故障"。

LV6 击杀与掉落:握手成功 ≠ 战斗结束

14:49,国密验证通过:

./gmcurl -X POST -vk --gmssl \

--resolve p-gw.example.internal:443:10.100.1.12 \

https://p-gw.example.internal:443/.../T2001 \

-H 'Content-type:application/xml'

握手成功,网络链路恢复,业务侧按原配置重试。但请注意:这只是通道验证,不是终局。A 机构的应用栈(hutool + JDK HttpsURLConnection)默认发不出国密套件,生产要稳定跑,必须二选一:

  • 路径一:客户端改造。 集成国密 TLS 能力(BouncyCastle 的 GM TLS 支持、铜锁 Tongsuo 等国密 SDK),替换或扩展 SSLContext。改造成本在应用侧,链路少一跳。
  • 路径二:协议转换网关。 部署国密↔国际转换网关或加密机,业务系统零改造。代价是多出一个运维对象和一处性能点。

联调期间可以用 gmcurl 顶一下,生产不能长期靠命令行工具。

经验值 6:验证通过 ≠ 业务恢复。闭环的定义是业务报文回执 + 上线准入补项------把"目标平台协议类型(国密/国际/双栈)+ 客户端套件兼容性验证"写进准入清单,这只怪就不会再来第二次。

顺手带走:三条随手可用的验证命令

1) 看服务端到底接不接受国际套件(本案会直接失败)

openssl s_client -connect 10.100.1.12:443 -tls1_2 </dev/null

2) 只看 ClientHello:SNI 与客户端上报的套件列表

tshark -i any -f 'tcp port 443' -Y 'tls.handshake.type == 1' -T fields \

-e tls.handshake.extensions_server_name -e tls.handshake.ciphersuite

3) 数一数服务端下发了几张证书(缺中间证书是另一类高频坑)

openssl s_client -connect 10.100.1.12:443 -showcerts </dev/null 2>/dev/null \

| grep -c 'BEGIN CERTIFICATE'

有个细节值得注意:tshark 的 tls.* 过滤器只认国际 TLS,国密 TLCP 报文它拆不出来。这本身就是一条线索------当抓包工具看不懂报文时,先怀疑协议栈不对,而不是网络不通。

复盘:这场仗掉下来的五件装备

  1. 分层定位能力:L4 / L6 / L7 三问,每层只回答一个问题;

  2. 证据链意识:堆栈 → telnet 截图 → gmcurl 输出 → 白名单变更记录,每步留时间戳,责任才说得清;

  3. 工具预置:离线工具包与验证手册,是平台方对客户的隐性服务水平;

  4. 协同边界:平台方管协议与白名单、网络方管专线与掩码、实施方管客户端改造与联调------三方 RACI 不清,就会重复定位;

  5. 复盘沉淀:把"29 位掩码误配"作为反例进变更检查清单,把"telnet 通 ≠ 握手通"进联调 FAQ。

附:国密接入与 SSL 握手排障 Checklist(可截图保存)

  1. 目标平台协议类型确认:国密 TLCP / 国际 TLS / 双栈?

  2. 客户端 TLS 实现确认:JDK / OpenSSL / BouncyCastle / 国密 SDK?

  3. 套件交集验证:国密客户端与 openssl s_client 各测一次;

  4. L4 连通性用服务端真实 IP测,不是出口 IP;

  5. 白名单:出口 IP 是否加白,掩码位是否与实际网段一致;

  6. 证书链:fullchain 是否完整,SAN/CN 与访问域名是否匹配;

  7. 端口一致性:测试环境(如 444)与生产(443)不要混用对照;

  8. 握手证据:-vk 输出里的协议版本、套件、ServerHello 与证书链;

  9. 业务回执:握手成功不等于报文成功,必须业务侧确认落账;

  10. 沉淀:准入清单补项 + FAQ + 出口 IP 台账。

写在最后

上一篇写《curl 卡在 Client Hello》的复盘,这一篇算它的续集------同一个大类(TLS 层)里,两只长得不一样的怪:一只是卡在握手前半程 ,一只是协议根本不匹配,还捎带了一个白名单掩码。

金融生产环境的排障,很多时候考的不是单点技术,而是分层不慌、证据不断、协同不乱。把这三件事练熟,怪就越打越快。

你遇到过最离谱的"网络不通"是什么?评论区聊聊,下一篇可能就是你的案例。

关注不迷路,「生产事件打怪升级」系列持续更新。

相关推荐
蜗牛互联网2 小时前
Python接入Gemini 3.8 Flash实现票据视觉抽取与规则校验
java·javascript·网络·人工智能·python
我就是不信2 小时前
TCP 原理详解:从三次握手到拥塞控制
网络·网络协议·tcp/ip
木白CPP2 小时前
USB(一): USB 概述&&介绍
linux·网络·嵌入式硬件
可乐鸡翅yeah_2 小时前
AES‑128 加密 M3U8,IV 初始化向量新手容易踩坑
前端·网络·数据库·ffmpeg·音视频·m3u8在线
HEJOO93 小时前
指针与结构体:C 语言进阶核心详解
c语言·开发语言·网络
en.en..3 小时前
IO 多路复用:select、poll、epoll 核心函数解析
网络
我就是不信3 小时前
TCP/IP 网络编程:从入门到实战
网络·网络协议·tcp/ip
沫璃染墨3 小时前
《Qt从零入门系列(十二):Qt文件操作详解——从QFile读写到QFileInfo与记事本实战》
开发语言·网络·c++·qt·交互·信号处理·文件
梦帮科技5 小时前
推测解码(Speculative Decoding)与 Draft-Target 双核协同:接受率判据、树状验证与两倍吞吐无损加速
网络·人工智能·深度学习·神经网络·线性代数·矩阵·架构