背景
使用netty开发的tcp文件服务处于电信网络,发现手机端使用北京移动4G时,文件无法下载,但是上传正常,使用电信4G或联通4G,甚至其它地区的移动4G都可以正常下载。
疑点分析
初步认为是跨运营商时做了一些网络限制(内蒙古移动4G也存在这样的问题,有可能是地区政治因素影响)。可能的情况是移动4G入网口做了相关的安全拦截,不信任其它ISP发过来的数据报。因此排查方向暂定在端口以及加密通道。
排查过程
前提
4G手机开热点,使用笔记本连接4G热点运行测试客户端以便运行wireshark抓包。
尝试http
因为是网络问题,首先尝试http协议是否也存在类似问题,因此在排查过程中试验了不同环境下的http文件下载(包括分片),结果显示,不管是否加密http/https,都是能够正常下载的,因此http作为备用方案继续排查tcp问题。
针对端口拦截
考虑拦截规则可能是通过端口拦截或限速,之前文件服务所使用的端口为非常规端口,因此不断尝试其它端口,结论是确实某些特殊端口没有被拦截,比如8080、5222等,这些特殊端口下是可以正常下载,其它端口下表现为服务端正常接收到第一个报文,下行第一片文件数据时客户端接收不到(偶尔又能收到,但是速度非常缓慢1kb都不到),wilreshark抓包客户端存在大量tcp-out-of-order,服务端存在大量tcp-dup-ack、retransmission,即下行存在大量重传。
TLS加密
仅仅某些特殊端口可以使用无法满足需求,作为企业版软件运行在客户自己的机器上,特殊端口很可能被客户其它程序占用,继续排查,猜测既然是跨运营商拦截问题,那么很可能涉及加密问题,因此在netty基础上加了安全套接字,然而问题依旧。期间尝试修改各种netty参数,甚至放弃netty用原生nio以及原生socket加上tls层,同样无法通过。
python版本验证
在java版本服务端这无头绪的情况下,同事写了简易的python进行验证,尴尬的是python版本是可以的,但是也要加tls,非tls的python版本基本上也是不行的,因此明确非特殊端口需要加密。那么,接下来的问题是java版本服务端与python版本存在什么区别,通过交叉验证,结论如下:
服务端 | 客户端 | 结果 |
---|---|---|
java | java | × |
java | python | × |
python | python | √ |
python | java | 基本上可以,有时不行 |
可以看出,只要有一端是java就存在问题(目前使用jdk1.8),客户端是java的情况会好一些。
抓包对比python与java
怀疑问题还是在tls,因此抓包对比python与java的tls建立过程,发现协商得到的加密套件有差异,python协商出的是TLS_RSA_WITH_AES_256_GCM_SHA384,java协商出的是TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,尝试将java版本的加密套件直接设置为TLS_RSA_WITH_AES_256_GCM_SHA384,但是报错没找到该套件。通过以下代码可以查看当前所有加密套件,低版本的java8可能没有TLS_RSA_WITH_AES_256_GCM_SHA384套件,这时需要下载jce,http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html, 将解压后的jar包覆盖到/jre/lib/security,同时将/jre/lib/security/java.security中的crypto.policy=unlimited注释打开。
SSLServerSocketFactory ssf = (SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
ssf.getSupportedCipherSuites(); // 支持的加密套件
ssf.getDefaultCipherSuites(); // 默认的加密套件
搞定之后,使用相同的加密套件java版本仍然不行。。。。。。。。
最终解决之道
处理过程中偶然搜索到一篇文章:https://juejin.im/post/5b94d7b96fb9a05ce5767fe2, netty的ssl优化,其中提到jdk自带的某些加密套件算法性能低下,特别是gcm相关算法,由此得到启发,虽然之前的测试python与java使用相同加密套件,但是实现上可能存在差别,因此参照文章所说引入netty-tcnative-boringssl-static包,使用openSSL实现的算法替换jdk自带的,再次测试发现大部分情况下都是可以的,偶尔不行,再过滤掉gcm相关套件,重新测试稳定可行,并且速度也有所提升,到此问题解决!!!
public static SslContext initServer() throws Exception {
InputStream keyStream = null;
try {
keyStream = new FileInputStream("/var/run/security/sChat.jks");
char[] serverPass = PASS.toCharArray();
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(keyStream, serverPass);
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
keyManagerFactory.init(keyStore, serverPass);
return SslContextBuilder.forServer(keyManagerFactory).sslProvider(SslProvider.OPENSSL)
.ciphers(null,
(ciphers, defaultCiphers, supportedCiphers) ->
defaultCiphers.stream().filter(s -> !s.contains("GCM")).collect(Collectors.toSet()).toArray(new String[0]))
.build();
} finally {
try {
if (keyStream != null) {
keyStream.close();
}
} catch (Exception e) {
}
}
}
总结
- 跨运营商网络限制,按端口限制、加密限制;
- jdk实现的某些加密套件性能较低,在正常网络情况下没有问题,但是在本例跨运营商网络环境较不稳定的情况下可能存在因tls层某些步骤耗时较长而导致的超时重传等问题,具体哪些步骤会有影响有待深挖。