服务器 JDK 1.8u_112
Linux version 3.10.0-1062.9.1.el7.x86_64 ([email protected].org) (gcc version 4.8.5 20150623 (Red Hat 4.8.5-39) (GCC) )
服务器有 NGINX,但是仅针对进来的请求,对 80 和 443 端口做了转发,转发到服务所在的 81 端口
——————————————————————————————————————————————————
问题现象:
请求客户服务器 HTTPS 接口出现 connection reset
排查第一步:
先抓包,发现客户服务器在我的服务器 Client Hello 完就 reset 了我(第一次握手
在本地 windows 环境写了个小项目,模拟一模一样的请求代码
复现问题
挨个清理 https 请求的代码,发现去掉重写 hostVerifyName 就可以
然后上网查发现是 JDK8 早期版本的一个 bug
https://bugs.openjdk.java.net/browse/JDK-8159569
按照帖子
https://javabreaks.blogspot.com/2015/12/java-ssl-handshake-with-server-name.html
重写了 SSLSocketFactory
在本地解决了这个 connection reset 的问题
接下来打包代码发到服务器
这里要说明一下,这个项目是一个迭代了六年的项目,没有上 springboot,也没有上 maven
所以每次发布都是更新的 class
更新到服务器上后 tcpdump 抓包发现
Client Hello 的 Extension 里还是没有 server_name(SNI)
这里有怀疑自己代码更新出错,没有把代码更新上去
1.下载 class 与本地进行对比,是一样的
2.将服务器上的 tomcat 和 class 拷贝下来在本地环境( linux -》 win )启动,请求客户服务器抓包是有 SNI
这里已经感到很邪门了,觉得是不是 linux 服务器对出口请求有什么过滤
写了个 springboot 小程序,里面的 https 请求代码是和公司项目里的一样的(有加上 SNI 的代码
抓包,有 SNI
写了个纯 JAVA,只有 main 函数那种
抓包有 SNI
那这就可以排除 linux 服务对出口请求的过滤
不过我也不清楚会不会有单独针对某个程序的过滤???可能性不大
——————————————————————————————————————————————
截至这里我已经很绝望了,感觉像是鬼打墙。
查了 JDK 官方的记录说是 1.8u_152 解决了这个问题
先在本机验证,挂上 152 的 JDK,去掉添加 SNI 的代码,抓包有 SNI
所以连夜给服务器的项目升级了 JDK
这里根据 Catalina 里面记录可以保证 JDK 是用的 152
但是抓包,莫得!还是莫得???
——————————————————————————————————————————————
下面是涉案代码
服务器上的代码
public static JSONObject httpsRequest(String requestUrl, String requestMethod, String contentType, String outputStr) {
JSONObject jsonObject = null;
StringBuffer buffer = new StringBuffer();
HttpsURLConnection httpUrlConn = null;
OutputStream outputStream = null;
InputStream inputStream = null;
InputStreamReader inputStreamReader = null;
BufferedReader bufferedReader = null;
try {
URL url = new URL(requestUrl);
httpUrlConn = (HttpsURLConnection) url.openConnection();
if (contentType != null) {
httpUrlConn.setRequestProperty("Content-Type", contentType);
}
TrustManager[] tm = {new TrustManager()};
SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
sslContext.getServerSessionContext().setSessionCacheSize(1000);
sslContext.init(null, tm, new SecureRandom());
// SSLSocketFactory ssf = sslContext.getSocketFactory();
httpUrlConn.setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
SSLParameters sslParameters = new SSLParameters();
List sniHostNames = new ArrayList(1);
sniHostNames.add(new SNIHostName(url.getHost()));
sslParameters.setServerNames(sniHostNames);
SSLSocketFactoryWrapper ssf = new SSLSocketFactoryWrapper(sslContext.getSocketFactory(), sslParameters);
httpUrlConn.setSSLSocketFactory(ssf);
httpUrlConn.setDoOutput(true);
httpUrlConn.setDoInput(true);
httpUrlConn.setUseCaches(false);
httpUrlConn.setRequestMethod(requestMethod);
httpUrlConn.setConnectTimeout(20000);
httpUrlConn.setReadTimeout(20000);
if ("GET".equalsIgnoreCase(requestMethod)) {
httpUrlConn.connect();
}
if (outputStr != null) {
outputStream = httpUrlConn.getOutputStream();
outputStream.write(outputStr.getBytes("UTF-8"));
}
if ( httpUrlConn.getResponseCode() == HttpURLConnection.HTTP_OK || httpUrlConn.getResponseCode() == HttpURLConnection.HTTP_CREATED || httpUrlConn.getResponseCode() == HttpURLConnection.HTTP_ACCEPTED) {
inputStream = httpUrlConn.getInputStream();
} else {
inputStream = httpUrlConn.getErrorStream();
}
inputStreamReader = new InputStreamReader(inputStream, "UTF-8");
bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
try {
jsonObject = JSONObject.fromObject(buffer.toString());
} catch (Exception e1) {
try {
jsonObject = JSONObject.fromObject("{id:\"" + buffer.toString() + "\"}");
} catch (Exception e2) {
log.error("请求异常:" + requestUrl, e2);
return null;
}
}
} catch (Exception e) {
log.error("请求异常:" + requestUrl, e);
if (e.getMessage().contains("401 for URL")) {
return JSONObject.fromObject("{id:\"401\"}");
}
return null;
} finally {
closeConnection( httpUrlConn, outputStream, inputStream, inputStreamReader, bufferedReader);
}
return jsonObject;
}
可以看出除了 TrustManager 和 SSLSocketFactoryWrapper 其他的都是 JDK 自带的类
TrustManager
public class TrustManager implements X509TrustManager {
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}
SSLSocketFactoryWrapper
public class SSLSocketFactoryWrapper extends SSLSocketFactory {
private final SSLSocketFactory wrappedFactory;
private final SSLParameters sslParameters;
public SSLSocketFactoryWrapper(SSLSocketFactory factory, SSLParameters sslParameters) {
this.wrappedFactory = factory;
this.sslParameters = sslParameters;
}
@Override
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
SSLSocket socket = (SSLSocket) wrappedFactory.createSocket(host, port);
setParameters(socket);
return socket;
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort)
throws IOException, UnknownHostException {
SSLSocket socket = (SSLSocket) wrappedFactory.createSocket(host, port, localHost, localPort);
setParameters(socket);
return socket;
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
SSLSocket socket = (SSLSocket) wrappedFactory.createSocket(host, port);
setParameters(socket);
return socket;
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
SSLSocket socket = (SSLSocket) wrappedFactory.createSocket(address, port, localAddress, localPort);
setParameters(socket);
return socket;
}
@Override
public Socket createSocket() throws IOException {
SSLSocket socket = (SSLSocket) wrappedFactory.createSocket();
setParameters(socket);
return socket;
}
@Override
public String[] getDefaultCipherSuites() {
return wrappedFactory.getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return wrappedFactory.getSupportedCipherSuites();
}
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
SSLSocket socket = (SSLSocket) wrappedFactory.createSocket(s, host, port, autoClose);
setParameters(socket);
return socket;
}
private void setParameters(SSLSocket socket) {
socket.setSSLParameters(sslParameters);
}
}
我照抄的呀!我照着大佬抄的!!!
下面是 Springboot 和那个单类测试项目,都放到公司服务器上测试了,有 SNI
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
tryConnect();
}
public static void tryConnect() {
String requestUrl = "https://xhoa.xinhuamed.com.cn:443/seeyon/rest/token/wechat";
String contentType = null;
String requestMethod = "GET";
String outputStr = null;
StringBuffer buffer = new StringBuffer();
HttpsURLConnection httpUrlConn = null;
OutputStream outputStream = null;
InputStream inputStream = null;
InputStreamReader inputStreamReader = null;
BufferedReader bufferedReader = null;
try {
URL url = new URL(requestUrl);
httpUrlConn = (HttpsURLConnection) url.openConnection();
if (contentType != null) {
httpUrlConn.setRequestProperty("Content-Type", contentType);
}
TrustManager[] tm = {new TrustManager()};
SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
sslContext.getServerSessionContext().setSessionCacheSize(1000);
sslContext.init(null, tm, new SecureRandom());
// SSLSocketFactory ssf = sslContext.getSocketFactory();
SSLParameters sslParameters = new SSLParameters();
List sniHostNames = new ArrayList(1);
sniHostNames.add(new SNIHostName(url.getHost()));
sslParameters.setServerNames(sniHostNames);
SSLSocketFactoryWrapper ssf = new SSLSocketFactoryWrapper(sslContext.getSocketFactory(), sslParameters);
httpUrlConn.setSSLSocketFactory(ssf);
httpUrlConn.setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
httpUrlConn.setDoOutput(true);
httpUrlConn.setDoInput(true);
httpUrlConn.setUseCaches(false);
httpUrlConn.setRequestMethod(requestMethod);
httpUrlConn.setConnectTimeout(20000);
httpUrlConn.setReadTimeout(20000);
if ("GET".equalsIgnoreCase(requestMethod)) {
httpUrlConn.connect();
}
if (outputStr != null) {
outputStream = httpUrlConn.getOutputStream();
outputStream.write(outputStr.getBytes("UTF-8"));
}
if ( httpUrlConn.getResponseCode() == HttpURLConnection.HTTP_OK || httpUrlConn.getResponseCode() == HttpURLConnection.HTTP_CREATED || httpUrlConn.getResponseCode() == HttpURLConnection.HTTP_ACCEPTED) {
inputStream = httpUrlConn.getInputStream();
} else {
inputStream = httpUrlConn.getErrorStream();
}
inputStreamReader = new InputStreamReader(inputStream, "UTF-8");
bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
try {
System.out.println(buffer.toString());
} catch (Exception e1) {
System.out.println(e1);
}
} catch (Exception e) {
System.out.println(e);
} finally {
closeConnection( httpUrlConn, outputStream, inputStream, inputStreamReader, bufferedReader);
}
}
private static void closeConnection(HttpURLConnection httpUrlConn, OutputStream outputStream, InputStream inputStream, InputStreamReader inputStreamReader, BufferedReader bufferedReader) {
if (outputStream != null) {
try {
outputStream.close();
outputStream = null;
} catch (IOException e2) {
}
}
if (bufferedReader != null) {
try {
bufferedReader.close();
bufferedReader = null;
} catch (IOException e1) {
}
}
if (inputStreamReader != null) {
try {
inputStreamReader.close();
inputStreamReader = null;
} catch (IOException e1) {
}
}
if (inputStream != null) {
try {
inputStream.close();
inputStream = null;
} catch (IOException e) {
}
}
if ( httpUrlConn != null) {
httpUrlConn.disconnect();
httpUrlConn = null;
}
}
}
——————————————————————————————————————————————————
拜托大家发散一下思路,想想还有什么方式能测试一下,以及还有哪里有可能限制
1
swiftg 2021-04-10 21:58:47 +08:00
域名被墙成关键词了
|
2
dorothyREN 2021-04-10 22:16:13 +08:00
如果过墙的话 connection reset 一般都是被墙阻断了。
|
3
xarthur 2021-04-10 22:20:30 +08:00 via iPhone
你是不是过墙了? SNI 已经被全部阻断了。
|
4
redford42 OP @swiftg @dorothyREN @xarthur
我服务器出口的墙吗? 截图的这个包都是在公司服务器上 tcpdump 的 这时候抓包已经过了公司服务器的防火墙了吗?不太明白 如果有墙阻断的话 springboot 这个应该也会没有 sni |
5
redford42 OP @swiftg @dorothyREN @xarthur
客户服务器那边是有防火墙的,有个云盾 相当于一个云盾是一个服务器转发外来请求,然后会对应多个虚拟机,所以 https 请求必须带上这个 sni 才能定位转发到哪一台 |
6
mytsing520 2021-04-10 23:16:34 +08:00
限制了开启 SNI 的,如果客户端发过来没有携带 SNI 字段,肯定会返回 reset,这和墙不墙的没关系。
如果你的业务前面有 WAF 或 nginx,那么需要在 WAF 或 nginx 上开启 SNI 才会携带信息。 |
7
redford42 OP @mytsing520 我是负责发请求的那个,我不明白我代码里加了 sni 为什么抓包发现发往客户的请求没有带上
|
8
mytsing520 2021-04-11 08:41:37 +08:00
@redford42 我在 6 楼回复中已经说明了。
你在 5 楼回复中描述说有个云盾,可以和云盾核实一下有没有打开 SNI 识别。 |
9
redford42 OP |
10
no1xsyzy 2021-04-12 09:48:56 +08:00
你这 “客户” “服务器” 混乱得一批
这么说:自己控制的服务器上的一个 HTTPS Client ( Java 写的)没能发出 SNI |
11
redford42 OP @no1xsyzy 嗯,是这个情况
打跟踪包把 httpsUrlConnection 里的 SSLSocketFactory 打印出来,ssl 对象的 sslparameters 是有 sni 的 |
12
redford42 OP 破案了
启动的 java options 里面有个 enable_sni=false 浑身轻松 |