V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
redford42
V2EX  ›  Java

给请求在代码里加上了 SNI 但是发布到服务器上 https 请求里没有带

  •  
  •   redford42 · 2021-04-10 20:39:30 +08:00 · 2170 次点击
    这是一个创建于 1353 天前的主题,其中的信息可能已经有所发展或是发生改变。

    服务器 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;
            }
        }
    
    
    }
    

    SNI 抓包对比

    ——————————————————————————————————————————————————

    拜托大家发散一下思路,想想还有什么方式能测试一下,以及还有哪里有可能限制

    12 条回复    2021-04-12 20:46:58 +08:00
    swiftg
        1
    swiftg  
       2021-04-10 21:58:47 +08:00
    域名被墙成关键词了
    dorothyREN
        2
    dorothyREN  
       2021-04-10 22:16:13 +08:00
    如果过墙的话 connection reset 一般都是被墙阻断了。
    xarthur
        3
    xarthur  
       2021-04-10 22:20:30 +08:00 via iPhone
    你是不是过墙了? SNI 已经被全部阻断了。
    redford42
        4
    redford42  
    OP
       2021-04-10 23:03:03 +08:00 via iPhone
    @swiftg @dorothyREN @xarthur
    我服务器出口的墙吗?
    截图的这个包都是在公司服务器上 tcpdump 的
    这时候抓包已经过了公司服务器的防火墙了吗?不太明白
    如果有墙阻断的话 springboot 这个应该也会没有 sni
    redford42
        5
    redford42  
    OP
       2021-04-10 23:07:55 +08:00 via iPhone
    @swiftg @dorothyREN @xarthur
    客户服务器那边是有防火墙的,有个云盾
    相当于一个云盾是一个服务器转发外来请求,然后会对应多个虚拟机,所以 https 请求必须带上这个 sni 才能定位转发到哪一台
    mytsing520
        6
    mytsing520  
       2021-04-10 23:16:34 +08:00
    限制了开启 SNI 的,如果客户端发过来没有携带 SNI 字段,肯定会返回 reset,这和墙不墙的没关系。
    如果你的业务前面有 WAF 或 nginx,那么需要在 WAF 或 nginx 上开启 SNI 才会携带信息。
    redford42
        7
    redford42  
    OP
       2021-04-10 23:29:18 +08:00 via iPhone
    @mytsing520 我是负责发请求的那个,我不明白我代码里加了 sni 为什么抓包发现发往客户的请求没有带上
    mytsing520
        8
    mytsing520  
       2021-04-11 08:41:37 +08:00
    @redford42 我在 6 楼回复中已经说明了。
    你在 5 楼回复中描述说有个云盾,可以和云盾核实一下有没有打开 SNI 识别。
    redford42
        9
    redford42  
    OP
       2021-04-11 23:17:21 +08:00 via iPhone
    @mytsing520
    客户服务器打开了 sni 识别
    现在问题是我 java 代码加了 sni,但是在我的服务器上抓包发往客户的请求里却没有带上 sni
    no1xsyzy
        10
    no1xsyzy  
       2021-04-12 09:48:56 +08:00
    你这 “客户” “服务器” 混乱得一批

    这么说:自己控制的服务器上的一个 HTTPS Client ( Java 写的)没能发出 SNI
    redford42
        11
    redford42  
    OP
       2021-04-12 11:03:30 +08:00 via iPhone
    @no1xsyzy 嗯,是这个情况
    打跟踪包把 httpsUrlConnection 里的 SSLSocketFactory 打印出来,ssl 对象的 sslparameters 是有 sni 的
    redford42
        12
    redford42  
    OP
       2021-04-12 20:46:58 +08:00 via iPhone
    破案了
    启动的 java options 里面有个 enable_sni=false
    浑身轻松
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1012 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 20:26 · PVG 04:26 · LAX 12:26 · JFK 15:26
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.