
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/关闭,典型的防火墙/负载白名单拒绝特征。于是白名单整改开始:
-
加入 A 机构专线段 10.100.2.20/30 → 仍不通;
-
加入实际出口 10.100.4.40 → 仍不通;
-
补入其他出口 IP → 仍不通;
-
网络方复查发现:白名单掩码误配成了 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 报文它拆不出来。这本身就是一条线索------当抓包工具看不懂报文时,先怀疑协议栈不对,而不是网络不通。
复盘:这场仗掉下来的五件装备
-
分层定位能力:L4 / L6 / L7 三问,每层只回答一个问题;
-
证据链意识:堆栈 → telnet 截图 → gmcurl 输出 → 白名单变更记录,每步留时间戳,责任才说得清;
-
工具预置:离线工具包与验证手册,是平台方对客户的隐性服务水平;
-
协同边界:平台方管协议与白名单、网络方管专线与掩码、实施方管客户端改造与联调------三方 RACI 不清,就会重复定位;
-
复盘沉淀:把"29 位掩码误配"作为反例进变更检查清单,把"telnet 通 ≠ 握手通"进联调 FAQ。
附:国密接入与 SSL 握手排障 Checklist(可截图保存)
-
目标平台协议类型确认:国密 TLCP / 国际 TLS / 双栈?
-
客户端 TLS 实现确认:JDK / OpenSSL / BouncyCastle / 国密 SDK?
-
套件交集验证:国密客户端与 openssl s_client 各测一次;
-
L4 连通性用服务端真实 IP测,不是出口 IP;
-
白名单:出口 IP 是否加白,掩码位是否与实际网段一致;
-
证书链:fullchain 是否完整,SAN/CN 与访问域名是否匹配;
-
端口一致性:测试环境(如 444)与生产(443)不要混用对照;
-
握手证据:-vk 输出里的协议版本、套件、ServerHello 与证书链;
-
业务回执:握手成功不等于报文成功,必须业务侧确认落账;
-
沉淀:准入清单补项 + FAQ + 出口 IP 台账。
写在最后
上一篇写《curl 卡在 Client Hello》的复盘,这一篇算它的续集------同一个大类(TLS 层)里,两只长得不一样的怪:一只是卡在握手前半程 ,一只是协议根本不匹配,还捎带了一个白名单掩码。
金融生产环境的排障,很多时候考的不是单点技术,而是分层不慌、证据不断、协同不乱。把这三件事练熟,怪就越打越快。
你遇到过最离谱的"网络不通"是什么?评论区聊聊,下一篇可能就是你的案例。
关注不迷路,「生产事件打怪升级」系列持续更新。