诊断由 TLS 的 SNI 引起的 https 服务调用握手失败

某一天, 开发人员说遇到一个奇怪的 bug: 某服务调用使用同步的 http 调用时是成功的, 最近他们迁到异步的 http 调用, 总是失败. 下面是出错的 error message 及出错栈:

19:25:53.183 [SocketConnectorIoProcessor-0.0] DEBUG org.apache.ahc.codec.HttpIoHandler - [partner-listing.thredup.com/104.18.23.236:443] Unexpected exception from SSLEngine.closeInbound().
javax.net.ssl.SSLException: Inbound closed before receiving peer's close_notify: possible truncation attack?
    at sun.security.ssl.Alerts.getSSLException(Alerts.java:208)
    at sun.security.ssl.SSLEngineImpl.fatal(SSLEngineImpl.java:1647)
    at sun.security.ssl.SSLEngineImpl.fatal(SSLEngineImpl.java:1615)
    at sun.security.ssl.SSLEngineImpl.closeInbound(SSLEngineImpl.java:1542)
    at org.apache.mina.filter.support.SSLHandler.destroy(SSLHandler.java:167)
    at org.apache.mina.filter.SSLFilter.sessionClosed(SSLFilter.java:367)
    ... 中间省略大部分 org.apache.mina.common 相关类
    at org.apache.mina.util.NamePreservingRunnable.run(NamePreservingRunnable.java:51)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)

看上去是一个 TLS 握手失败. 但是error message 里面并没有说明具体什么原因.

访问的网站是: https://partner-listing.thredup.com/api/v1.0/webhooks/xxxx, 同步访问是成功的, 使用各种浏览器访问也是成功的. 所以开发人员怀疑是平台部门提供的的异步访问 jar 包有问题. 于是他们找到平台部门帮忙.

单从这段出错 error message 看, 没有任何头绪, 于是 SRE 的下一步就是抓包看具体的tcp 内容. 下面就是这个尝试连接的 tcp dump:
tcpdump-sni.png

从tcp dump 可以看到这个连接很快失败:

  1. 首先, 前3行tcp握手建立;
  2. 第4行: client 发送 TLS client hello;
  3. 第5行: server ack client hello package;
  4. 第6行: server 发送 Fatal error: 握手失败;
  5. 第7行: server 发送 fin 表示要结束连接, 不玩了.

这里的最有提示的是第6行, 在下面的详细 tab 展示了, 具体出错的类型和描述:

Content type: alert (21)
Alert Message:
   Level: Fatal(2)
   Description: Handshake Failure (40)

Handshake failure 40 并没有告诉我们具体的是什么原因, 不过google还是针对这种出错给出了几种可能, 其中一种就是 server 那边需要 SNI(Server Name Indication). 我们这里就是 SNI 问题.

使用 openssl client, 我们很容易验证这个问题:

openssl s_client -tls1_2 -connect  partner-listing.thredup.com:443
openssl s_client -tls1_2 -connect  partner-listing.thredup.com:443 -servername partner-listing.thredup.com

后边的参数 -servername 就是制定 SNI. 前面一个命令会出错, 后面一个就是成功的.

什么是 SNI? 为什么需要 SNI?

SNI 是 Server Name Indication 的缩写. 假如一个服务器上部署了多个网站, 每个网站都有自己的域名, 并且这个服务器只有一个 IP 地址, 就会出现: 当客户端连接到这个服务器的时候, 服务器不知道你是要访问那个网站, 因为你的连接过来, 从 tcp 层来看, 连的都是同一个 IP, 它不做区分. 应用层, 比如 http 层, 就要区分不同的网站, 应用层不知道这个请求应该发送到那个网站. 所以 http 协议里面有个 header 是: Host, 它能让应用层知道不同的网站. 在应用层和 TCP 层的中间, TSL 也要区分你是访问那个网站, 所以它也需要一种机制, 去区分进来的请求是访问那个证书, 这里的 SNI 就是做这个事情的. SNI 是 TLS 的一个扩展字段, 在 2003 年 6 月份 加入 RFC 3546, 很多实现 TLS 客户端的实现都是在 2010 年之后才具体实现的这个 feature.

为什么我们的异步请求出错?

因为我们的异步客户端使用了一个很老的 jar 包, 它里面没有实现这个 SNI 的功能.

从一个连接成功的请求 tcp 数据来看, 这个里面是包含这个 SNI 扩展字段的:
sni-ext.png

使用MAT 分析heap dump

  1. 下载及设置
    官方下载: https://www.eclipse.org/mat/
    JDK 最好用最新的 JDK, 因为最新的基本优化最多.
    根据你分析的 heap dump 的大小, 有时候需要调整 MAT 的 heap 的大小. 这个参数在 MAT 根目录的 MemoryAnalyzer.ini 文件里面. 我经常分析 30G 以下的 heap, 基本设置为 27G, 是 JVM 使用压缩指针来加速.
    sjc-sreop-001.png

    另外, 对于 HPROF 的 dump 来说, 我经常设置为非严格 parse, 因为有时候有点错误, 不影响分析:
    sjc-sreop-002.png

    文档: MAT 自带文档在 Help -> Help Contents 菜单里面, 或者在线版本的文档

  2. 分析
    正常情况下当你打开一个 heap dump 之后, 它会问你是否自动诊断内存泄漏, 如果你不是为了诊断内存泄漏, 可以取消这步.
    Histogram: 是按照类的实例数量聚集, 能很快发现包含大量实例的类. 一般情况下 char[] 或者 String 都在最上面, 这基本没有问题.
    Dominator Tree: 对于诊断内存泄漏非常有用, 如果能抓到一个对象 dominate 很多实例, 基本你找到了问题所在.
    OQL: 就像查询数据库的 SQL 语言, 非常方便的查找任何对象, 实例;
    Threads: 查看当前heap的所有线程, 对于发现某个对象是怎么被创建, 或引用的非常有帮助.

更多关于如何使用 OQL: Java heap dump OQL samples - where
------ 未完待续 -------