作者: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直连”。
自定义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); } }
`创建OkHttpClient 创建OkHttpClient对象,传入OkHttpDns对象代替默认Dns服务:
java private void okhttpDnsRequest() { OkHttpClient client = new OkHttpClient.Builder() .dns(OkHttpDns.getInstance(getApplicationContext())) .build(); ... ... }总结 相比于通用方案,OkHttp+HttpDns有以下两个主要优势:
实现简单,只需通过实现Dns接口即可接入HttpDns服务 通用性强,该方案在HTTPS,SNI以及设置Cookie等场景均适用。规避了证书校验,域名检查等环节 该实践对于Retrofit+OkHttp同样适用,将配置好的OkHttpClient作为Retrofit.Builder::client(OkHttpClient)参数传入即可。
OkHttp 不推荐使用HttpUrlConnection方式,即将URL中域名改IP,建议使用DNS接口
待补充
待补充webveiw的ip直连方式
参考
https://blog.csdn.net/leelit/article/details/77829196
https://help.aliyun.com/document_detail/30143.html?spm=a2c4g.11186623.6.565.552928b7YFUWYD





































苏公安网备 32011402010933号