文档开发者中心
我的

【综合】Android平台Https使用IP直连方案说明

作者:HW-LL


背景说明

一般而言,网络接入构造url时是使用域名方式,诸如如下: https://doamin_name/path?var=value 底层网络库处理请求时,先把域名通过DNS解析成IP,然后再与相应IP建链发送请求。当要采用httpdns或者容灾IP进行访问时,就需要对DNS解析的过程进行处理。OKhttp库对外提供DNS接口,可以优雅的实现其DNS接口完成DNS解析的定制逻辑,诸如直接使用容灾IP。但是Android平台原生的HttpUrlConnection是不支持DNS接口,当需要直接使用指定IP时,只能通过修改url的方式,诸如下 https://容灾IP/path?var=value

接入时后端常常都会存在转发层做一些诸如负载均衡的工作,由转发层将请求分发到真实服务器。HTTP接入的转发层一般是根据请求头中的HOST字段来转发到各个RS。所以对于HTTP接入,使用IP直连时需要设置一个HOST请求头,值为请求域名,在绝大部分情况下就可以获得IP直连的好处。 但是对于HTTPS接入,情况会变得稍微复杂一些。本文主要是对Android平台使用HttpURLConnection和OkHttp进行HTTPS IP直连时,遇到的一些问题及其解决方法的小结。

HttpUrlConnection最佳实践

HttpURLConnection因为把url中域名换成IP,导致跟域名相关的流程,诸如sni、证书校验都会有影响。本文将重点介绍这两个问题的解决思路,综合网上反馈,此种方式还有连接复用、TLS会话复用的问题,请参考附件。

TLS证书域名校验问题

针对“domain不匹配”问题,是因为证书中配置的是域名,但是IP直连请求时被改写成了IP,从而导致校验不通过。可以采用如下方案解决:hook证书校验过程中,将IP直接替换成原来的域名,再执行证书验证。

【注意】若报出SSL校验错误,javax.net.ssl.SSLPeerUnverifiedException: Hostname 117.78.31.127 not verified:,则是证书校验问题。

下面分别列出Android平台的示例代码。 `java public class IPConnectHostnameVerifier implements HostnameVerifier { private HostnameVerifier hostnameVerifier;

private String host;

/*
* host: 原始域名
*/
public IPConnectHostnameVerifier(HostnameVerifier hostnameVerifier, String host) {
    this.hostnameVerifier = hostnameVerifier;
    this.host = host;
}

/*
* 关于这个接口的说明,官方有文档描述:
* This is an extended verification option that implementers can provide.
* It is to be used during a handshake if the URL's hostname does not match the
* peer's identification hostname.
*
* 使用URL里设置的hostname不是远程的主机名(如:m.hwcloudtest.cn),与证书颁发的域不匹配,
* Android HttpsURLConnection提供了回调接口让用户来处理这种定制化场景。
* 在此处将校验的域名替换成原始域名
*
*/
@Override
public boolean verify(String hostname, SSLSession session) {
    return hostnameVerifier.verify(host, session);
}

}

`

SNI场景

定制SSLSocketFactory,在createSocket时替换为直连IP,并进行SNI配置。跟阿里HttpDNS推荐的方案相比,我们当前采用Wrapper的方式将aegis的SecureSSLSocketFactory给封了一层,仅仅是重写了createSocket接口,在此处设置SNI并触发TLS握手,确保原有的安全加密套件和tls协议版本号逻辑不丢失。

【注意】若报出SSL校验错误,System.err: javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.,请检查应用场景是否为SNI(单IP多HTTPS域名)。

`java public class TlsSniSocketFactory extends SSLSocketFactory { private static final String TAG = 'TlsSniSocketFactory';

private SSLSocketFactory secureSSLSocketFactory;

private HttpURLConnection conn;

public TlsSniSocketFactory(HttpURLConnection conn, SSLSocketFactory secureSSLSocketFactory) {
    this.conn = conn;
    this.secureSSLSocketFactory = secureSSLSocketFactory;
}

/**
 * {@inheritDoc} 此方法的实现存在问题,外部不能依赖使用。
 */
@Override
public String[] getDefaultCipherSuites() {
    return secureSSLSocketFactory.getDefaultCipherSuites();
}

@Override
public String[] getSupportedCipherSuites() {
    // 此方法目前无调用场景,未实现。
    return secureSSLSocketFactory.getSupportedCipherSuites();
}

/**
 * {@inheritDoc}
 */
@Override
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
    return enableSNI(secureSSLSocketFactory.createSocket(host, port), host);
}

/**
 * {@inheritDoc}
 */
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
    return enableSNI(secureSSLSocketFactory.createSocket(host, port), host.getHostName());
}

/**
 * {@inheritDoc}
 */
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort)
    throws IOException, UnknownHostException {
    return enableSNI(secureSSLSocketFactory.createSocket(host, port, localHost, localPort), host);
}

/**
 * {@inheritDoc}
 */
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort)
    throws IOException {
    return enableSNI(secureSSLSocketFactory.createSocket(address, port, localAddress, localPort),
        address.getHostName());
}

/**
 * {@inheritDoc}
 */
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
    return enableSNI(secureSSLSocketFactory.createSocket(s, host, port, autoClose), host);
}

private Socket enableSNI(Socket socket, String host) throws IOException {
    SSLSocket sslSocket = (SSLSocket) socket;
    String peerHost = conn.getRequestProperty('Host');
    if (peerHost == null) {
        peerHost = host;
    }
    Log.i(TAG, 'customized createSocket. host: ' + peerHost);

    // create and connect SSL socket, but don't do hostname/certificate verification yet
    SSLCertificateSocketFactory sslSocketFactory =
        (SSLCertificateSocketFactory) SSLCertificateSocketFactory.getDefault(0);
    // ssl = sslSocket;

    // set up SNI before the handshake
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
        Log.i(TAG, 'Setting SNI hostname');
        sslSocketFactory.setHostname(sslSocket, peerHost);
    } else {
        Log.d(TAG, 'No documented SNI support on Android <4.2, trying with reflection');
        try {
            java.lang.reflect.Method setHostnameMethod =
                sslSocket.getClass().getMethod('setHostname', String.class);
            setHostnameMethod.invoke(sslSocket, peerHost);
        } catch (Exception e) {
            Log.w(TAG, 'SNI not useable', e);
        }
    }

    // Force handshake, otherwise sni will be restore to ip by outer layer caller
    // out layer caller will set sni according to uri host
    // reference upgradeToTls function at
    // http://androidxref.com/4.3_r2.1/xref/external/okhttp/src/main/java/com/squareup/okhttp/Connection.java
    sslSocket.startHandshake();

    return sslSocket;
}

} ` [Tips]:

1、最开始我们只是尝试,在TlsSniSocketFactory#enableSNI接口设置SNI,并没调用sslSocket.startHandshake,我们发现sni并没有生效。查阅Android源码发现,在createSocket的调用者处其执行时序是

1、createSocket
2、根据url设置sni等扩展字段
3、startHandshake开始握手

所以,我们在createSocket里面去设置SNI又被系统第2步给还原了,所以当前的策略是,在createSocket中就把2,3都执行完。

HttpsURLConnection应用IP直连

`java String url = 'https://117.78.31.127/sni'; String host = 'caddy1.i-next.xyz'; // root ca is not trusted by ageis host = 'lfwisestacktest02.hwcloudtest.cn'; // root ca is trusted by ageis, but trusted by os try { URL mURL = new URL(url); HttpsURLConnection connection = (HttpsURLConnection) mURL.openConnection(); connection.setRequestProperty('Host', host);

// used ageis SecureSSLSocketFactory
TlsSniSocketFactory sslSocketFactory =
    new TlsSniSocketFactory(connection, SecureSSLSocketFactory.getInstance(context));

// used system default ssl factory 
// TlsSniSocketFactory sslSocketFactory =
// new TlsSniSocketFactory(connection, systemDefaultSslSocketFactory(systemDefaultTrustManager()));
connection.setSSLSocketFactory(sslSocketFactory);

// set hostverifier 
connection.setHostnameVerifier(
    new IPConnectHostnameVerifier(SecureSSLSocketFactory.STRICT_HOSTNAME_VERIFIER, host));
connection.connect();

Log.e(TAG, 'code: ' + connection.getResponseCode());
Log.e(TAG, 'response:' + connection.getResponseMessage());

} catch (Exception e) { e.printStackTrace(); }
`

OkHttp最佳实践

OkHttp是一个处理网络请求的开源项目,是Android端最火热的轻量级框架,由移动支付Square公司贡献用于替代HttpUrlConnection和Apache HttpClient。随着OkHttp的不断成熟,越来越多的Android开发者使用OkHttp作为网络框架。

OkHttp默认使用系统DNS服务InetAddress进行域名解析,但同时也暴露了自定义DNS服务的接口,通过该接口我们可以优雅地使用“IP直连”。

  1. 自定义DNS接口 OkHttp暴露了一个Dns接口,通过实现该接口,我们可以自定义Dns服务: `java public class OkHttpDns implements Dns { private static final Dns SYSTEM = Dns.SYSTEM; HttpDnsService httpdns;//httpdns 解析服务 private static OkHttpDns instance = null; private OkHttpDns(Context context) { this.httpdns = HttpDns.getService(context, 'account id'); } public static OkHttpDns getInstance(Context context) { if(instance == null) { instance = new OkHttpDns(context); } return instance; }

    @Override public List<InetAddress> lookup(String hostname) throws UnknownHostException { // 自定义IP直连的逻辑 String ip = httpdns.getIpByHostAsync(hostname); if(ip != null) { //如果ip不为null,直接使用该ip进行网络请求 List<InetAddress> inetAddresses = Arrays.asList(InetAddress.getAllByName(ip)); Log.e('OkHttpDns', 'inetAddresses:' + inetAddresses); return inetAddresses; } //如果返回null,走系统DNS服务解析域名 return Dns.SYSTEM.lookup(hostname); } } `

  2. 创建OkHttpClient 创建OkHttpClient对象,传入OkHttpDns对象代替默认Dns服务: java private void okhttpDnsRequest() { OkHttpClient client = new OkHttpClient.Builder() .dns(OkHttpDns.getInstance(getApplicationContext())) .build(); ... ... }

  3. 总结 相比于通用方案,OkHttp+HttpDns有以下两个主要优势:

实现简单,只需通过实现Dns接口即可接入HttpDns服务 通用性强,该方案在HTTPS,SNI以及设置Cookie等场景均适用。规避了证书校验,域名检查等环节 该实践对于Retrofit+OkHttp同样适用,将配置好的OkHttpClient作为Retrofit.Builder::client(OkHttpClient)参数传入即可。

OkHttp 不推荐使用HttpUrlConnection方式,即将URL中域名改IP,建议使用DNS接口

待补充

待补充webveiw的ip直连方式

参考

  1. https://blog.csdn.net/leelit/article/details/77829196

  2. https://help.aliyun.com/document_detail/30143.html?spm=a2c4g.11186623.6.565.552928b7YFUWYD

点赞
收藏
回复
4
分享
举报
浏览3066 发布于2019-11-18 02:07未知归属地
全部评论
最多点赞
最新发布
最早发布

作者的其他文章推荐:Quic特性1:连接迁移

4楼回复于2019-11-18 09:05 未知归属地

作者的其他文章推荐:Android手机修改/etc/hosts的方法

3楼回复于2019-11-18 09:04 未知归属地

赞一个赞一个

2楼回复于2019-11-18 09:03 未知归属地

很详细的帖子,不错不错

1楼回复于2019-11-18 09:03 未知归属地
写回答
新增插入模板功能
一键使用模板,快速填写内容,轻松发帖~
知道了
  • 为了保障您的信息安全,请勿上传您的敏感个人信息(如您的密码等信息)和您的敏感资产信息(如关键源代码、签名私钥、调试安装包、业务日志等信息),且您需自行承担由此产生的信息泄露等安全风险。
  • 如您发布的内容为转载内容,请注明内容来源。

我要发帖子

目录 收起
背景说明
HttpUrlConnection最佳实践
TLS证书域名校验问题
SNI场景
HttpsURLConnection应用IP直连
OkHttp最佳实践
待补充
参考
了解社区公约,与您携手共创和谐专业的开发者社区。