记一次okhttp线上崩溃问题

崩溃日志如下

java.lang.IllegalArgumentException

Unexpected TLS version: NONE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java.lang.IllegalArgumentException
Unexpected TLS version: NONE
1 okhttp3.TlsVersion.forJavaName(SourceFile:3)
2 okhttp3.Handshake.get(SourceFile:4)
3 okhttp3.internal.connection.RealConnection.connectTls(SourceFile:14)
4 okhttp3.internal.connection.RealConnection.establishProtocol(SourceFile:5)
5 okhttp3.internal.connection.RealConnection.connect(SourceFile:14)
6 okhttp3.internal.connection.StreamAllocation.findConnection(SourceFile:36)
7 okhttp3.internal.connection.StreamAllocation.findHealthyConnection(SourceFile:1)
8 okhttp3.internal.connection.StreamAllocation.newStream(SourceFile:6)
9 okhttp3.internal.connection.ConnectInterceptor.intercept(SourceFile:5)
10 okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10)
11 okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1)
12 okhttp3.internal.cache.CacheInterceptor.intercept(SourceFile:22)
13 okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10)
14 okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1)
15 okhttp3.internal.http.BridgeInterceptor.intercept(SourceFile:22)
16 okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10)
17 okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(SourceFile:9)
18 okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10)
19 okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1)
20 com.zdwh.wwdz.util.z0.m.a.intercept(SourceFile:2)

在通过错误堆栈可以找到相关在okhttp-3.8.1中的相应代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public static TlsVersion forJavaName(String javaName) {
switch (javaName) {
case "TLSv1.3":
return TLS_1_3;
case "TLSv1.2":
return TLS_1_2;
case "TLSv1.1":
return TLS_1_1;
case "TLSv1":
return TLS_1_0;
case "SSLv3":
return SSL_3_0;
}
throw new IllegalArgumentException("Unexpected TLS version: " + javaName);
}
public static Handshake get(SSLSession session) {
String cipherSuiteString = session.getCipherSuite();
if (cipherSuiteString == null) throw new IllegalStateException("cipherSuite == null");
CipherSuite cipherSuite = CipherSuite.forJavaName(cipherSuiteString);
String tlsVersionString = session.getProtocol();
if (tlsVersionString == null) throw new IllegalStateException("tlsVersion == null");
TlsVersion tlsVersion = TlsVersion.forJavaName(tlsVersionString);
}
private void connectTls(ConnectionSpecSelector connectionSpecSelector) throws IOException {
Address address = route.address();
SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
boolean success = false;
SSLSocket sslSocket = null;
try {
// Create the wrapper over the connected socket.
sslSocket = (SSLSocket) sslSocketFactory.createSocket(
rawSocket, address.url().host(), address.url().port(), true /* autoClose */);
// Configure the socket's ciphers, TLS versions, and extensions.
ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
if (connectionSpec.supportsTlsExtensions()) {
Platform.get().configureTlsExtensions(
sslSocket, address.url().host(), address.protocols());
}
// Force handshake. This can throw!
sslSocket.startHandshake();
Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession());
}

通过关键代码可以找到tls version是session的protocol,而session是从sslSocket中获取的,sslSocket是通过sslSocketFactory.createSocket获取的。

后来谷歌了解到okhttp针对这个问题在3.10.0版本中解决了这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private void connectTls(ConnectionSpecSelector connectionSpecSelector) throws IOException {
Address address = route.address();
SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
boolean success = false;
SSLSocket sslSocket = null;
try {
// Create the wrapper over the connected socket.
sslSocket = (SSLSocket) sslSocketFactory.createSocket(
rawSocket, address.url().host(), address.url().port(), true /* autoClose */);
// Configure the socket's ciphers, TLS versions, and extensions.
ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
if (connectionSpec.supportsTlsExtensions()) {
Platform.get().configureTlsExtensions(
sslSocket, address.url().host(), address.protocols());
}
// Force handshake. This can throw!
sslSocket.startHandshake();
// block for session establishment
SSLSession sslSocketSession = sslSocket.getSession();
if (!isValid(sslSocketSession)) {
throw new IOException("a valid ssl session was not established");
}
Handshake unverifiedHandshake = Handshake.get(sslSocketSession);
}
private boolean isValid(SSLSession sslSocketSession) {
// don't use SslSocket.getSession since for failed results it returns SSL_NULL_WITH_NULL_NULL
return !"NONE".equals(sslSocketSession.getProtocol()) && !"SSL_NULL_WITH_NULL_NULL".equals(
sslSocketSession.getCipherSuite());
}

上面是3.10.0的代码,可以看到在connectTLS方法中已经将这个问题过滤了一遍,并且从报IllegalArgumentException改成IOException了。而网络框架能够catch住IOException,所以这个bug可以通过升级3.10.0解决,等升级之后观察一下线上的崩溃。

下面有两个问题,1是什么TLS,为什么会出现TLS的protocol为none,2是在bugly上的反馈,暴露这个问题的机型都是android9.0及以上的机子。

TLS其实就是标准版后的SSL,SSL3.0其实和TLS1.0没有什么差别。

为什么会出现NONE:这里我在OKHTTP别人的讨论纪录中找到了答案

NONE isn’t unknown or old. It’s that there is no SSL session anymore, or it failed initially. So changing TlsVersion lookup doesn’t seem right to me.

NONE并非未知或以前的协议。这是因为没有SSL会话了,或者它在最开始就失败了。因此,更改TlsVersion查找对我而言似乎不合适。

这里的意思是,NONE出现的原因是因为这个连接已经断开了,或者他根本就没有连接成功,所以这里应该是服务端和客户端的连接问题。

android9.0是有对TLS做一些改动,但是这些改动看起来和这次的问题没有关联性。暂时还找不到这个问题和9.0的关系