基本概念
中国互联网经过这么多年的沉浮, 地下黑色产业链已经有了很大的变化. 随着免费杀毒软件的流行, 中国互联网发生了一些比较明显的变化, 比如曾经盗号木马横行, 现在就很少见了. 但是黑色产业并没有消失, 而是转型做起来其他的买卖, 比如买卖流量等.
运营商劫持
运营商是指那些提供宽带服务的 ISP, 包括三大运营商中国电信, 中国移动, 中国联通, 还有一些小运营商, 比如长城宽带, 歌华有线宽带. 运营商提供最最基础的网络服务, 掌握着通往用户物理大门的钥匙.
网络运营商为了卖广告或者其他经济利益, 有时候会直接劫持用户的访问, 目前, 运营商比较常见的作恶方式有两种, 分别是 DNS 劫持和 HTTP 劫持.
DNS 劫持
DNS 是 Domain Name System 的简写, 即域名系统, 它作为可以将域名和 IP 地址相互映射的一个分布式数据库, 能够使人更方便的访问互联网. 通俗的说, 当你在浏览器中输入网站的域名时, DNS 服务器会将域名转为具体的 IP 地址.
DNS 劫持主要有以下几种表现:
弹出的迷你浏览器直接跳转到某个导航网站;
内置浏览器被跳转到某个宣传赚钱的网页, 诱导消费;
而避免 DNS 劫持的尽量不要使用运营商默认的 DNS.
Http 劫持
在使用者与其目的网络服务所建立的专用数据通道中, 监视特定数据信息, 提示当满足设定的条件时, 就会在正常的数据流中插入精心设计的网络数据报文, 目的是让用户端程序解释 "错误" 的数据, 并以弹出新窗口的形式在使用者界面展示宣传性广告或者直接显示某网站的内容.
上面问题造成的原因, 根本上是运营商的问题, 所以尽量不要使用运营商提供的默认的东西
HttpDns
HttpDns 服务则是基于 HTTP 协议自建 DNS 服务, 或者选择更加可靠的 DNS 服务提供商来完成 DNS 服务, 以降低发生安全问题的风险. HttpDns 还可以为精准调度提供支持.
通常大公司都有自己的 HttpDns 服务器, 例如微博团队开源的 HttpDns 方案 https://www.oschina.net/p/httpdnslib , 腾讯有开放自己的 HttpDns 服务 https://github.com/tencentyun/httpdns-android-sdk . 阿里云 https://help.aliyun.com/product/30100.html?spm=5176.doc30140.3.1.hkjg6L 和 DNSPod https://www.dnspod.cn/httpdns/guide 还推出了商业化的产品. 当然, 如果有需要可以自己搭建一套 HtppDns 服务.
Android 接入 HttpDns
在 Android 开发中, 我们通常不会关心 Http 请求的详细执行过程, 因为具体的网络请求会使用一些第三方库, 如 okHttp,retrofit 等.
在 Android 开发中, 使用 HttpDns 将获得的 IP 地址应用请求的最简单方式是, 将域名替换为 IP, 然后用新的 URL 发起 HTTP 请求. 这样就能有效的防止 DNS 劫持的行为.
然而, 标准的 HTTP 协议中服务端会将 HTTP 请求头中 HOST 字段的值作为请求的域名, 在我们没有主动设置 HOST 字段的值时, 网络库也会自动地从 URL 中提取域名, 并为请求做设置. 但使用 HttpDns 后, URL 中的域名信息丢失, 这时候就需要设置 HOST 字段值. 例如:
- String originalUrl = "http://www.sina.com/";
- URL url = new URL(originalURL);
- String originalHost = url.getHost();
- // 同步接口获取 IP
- String ip = httpdns.getIpByHost(originalHost);
- HttpURLConnection conn;
- if (ip != null) {
- // 通过 HTTPDNS 获取 IP 成功, 进行 URL 替换和 HOST 头设置
- url = new URL(originalUrl.replaceFirst(originalHost, ip));
- conn = (HttpURLConnection) url.openConnection();
- // 设置请求 HOST 字段
- conn.setRequestProperty("Host", originHost);
- } else {
- conn = (HttpURLConnection) url.openConnection();
- }
当然, 进行上面的修改后, 需要通知其他的使用方, 具体的, 在客户端的网络库中, 有以下几个地方需要修改.
COOKIE 存取. 支持 COOKIE 存取的网络库, 在存取 COOKIE 时, 从 URL 中提取的域名通常是 key 的重要部分.
连接管理. 连接的 Keep-Alive 参数, 可以让执行 HTTP 请求的 TCP 连接在请求结束后不会被立即关闭, 而是先保持一段时间. 为新发起的请求查找可用连接时, 主要的依据也是 URL 中的域名. 针对相同域名同时执行的 HTTP 请求的最大个数 6 个的限制, 也需要借助于 URL 中的域名来完成.
HTTPS 的 SNI 及证书验证. SSL/TLS 的 SNI 扩展用于支持虚拟主机托管. 在 SSL/TLS 握手期间, 客户端通过该扩展将要请求的域名发送给服务器, 以便可以取到适当的证书. SNI 信息也来源于 URL 中的域名.
常见问题
HTTPS 域名证书验证问题
许多服务并不是多服务 (域名) 共用一个物理 IP 的, 因而丢失 SNI 信息并不是特别的要紧, 针对以上的情况, 解决掉域名证书的验证问题即可.
HttpsURLConnection 方式
如果针对传统的 HttpsURLConnection 请求方式, 可以使用下面的方式来解决证书验证问题.
- try {
- String url = "https://140.225.164.59/?sprefer=sypc00";
- final String originHostname = "www.wolfcstech.com";
- HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection();
- connection.setRequestProperty("Host", originHostname);
- connection.setHostnameVerifier(new HostnameVerifier() {
- @Override
- public boolean verify(String hostname, SSLSession session) {
- return HttpsURLConnection.getDefaultHostnameVerifier().verify(originHostname, session);
- }
- });
- connection.connect();
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- }
主要思路即是自定义证书验证的逻辑. HostnameVerifier 的 verify() 传回来的域名是 url 中的 ip 地址, 但我们可以在定制的域名证书验证逻辑中, 使用原始的真实的域名与服务器返回的证书一起做验证.
SNI 问题解决方案
对于多个域名部署在相同 IP 地址的主机上的场景, 除了要处理域名证书验证外, SNI 的设置也是必须的. 阿里云给出的解决方案是, 自定义 SSLSocketFactory, 控制 SSLSocket 的创建过程, 在 SSLSocket 被创建成功之后, 立即设置 SNI 信息进去. 例如, 下面是 SSLSocketFactory 的实现方式:
- public class TlsSniSocketFactory extends SSLSocketFactory {
- private final String TAG = TlsSniSocketFactory.class.getSimpleName();
- HostnameVerifier hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
- private HttpsURLConnection conn;
- public TlsSniSocketFactory(HttpsURLConnection conn) {
- this.conn = conn;
- }
- @Override
- public Socket createSocket() throws IOException {
- return null;
- }
- @Override
- public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
- return null;
- }
- @Override
- public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
- return null;
- }
- @Override
- public Socket createSocket(InetAddress host, int port) throws IOException {
- return null;
- }
- @Override
- public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
- return null;
- }
- // TLS layer
- @Override
- public String[] getDefaultCipherSuites() {
- return new String[0];
- }
- @Override
- public String[] getSupportedCipherSuites() {
- return new String[0];
- }
- @Override
- public Socket createSocket(Socket plainSocket, String host, int port, boolean autoClose) throws IOException {
- String peerHost = this.conn.getRequestProperty("Host");
- if (peerHost == null)
- peerHost = host;
- Log.i(TAG, "customized createSocket. host:" + peerHost);
- InetAddress address = plainSocket.getInetAddress();
- if (autoClose) {
- // we don't need the plainSocket
- plainSocket.close();
- }
- // create and connect SSL socket, but don't do hostname/certificate verification yet
- SSLCertificateSocketFactory sslSocketFactory = (SSLCertificateSocketFactory) SSLCertificateSocketFactory.getDefault(0);
- SSLSocket ssl = (SSLSocket) sslSocketFactory.createSocket(address, port);
- // enable TLSv1.1/1.2 if available
- ssl.setEnabledProtocols(ssl.getSupportedProtocols());
- // 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(ssl, peerHost);
- } else {
- Log.d(TAG, "No documented SNI support on Android <4.2, trying with reflection");
- try {
- java.lang.reflect.Method setHostnameMethod = ssl.getClass().getMethod("setHostname", String.class);
- setHostnameMethod.invoke(ssl, peerHost);
- } catch (Exception e) {
- Log.w(TAG, "SNI not useable", e);
- }
- }
- // verify hostname and certificate
- SSLSession session = ssl.getSession();
- if (!hostnameVerifier.verify(peerHost, session))
- throw new SSLPeerUnverifiedException("Cannot verify hostname:" + peerHost);
- Log.i(TAG, "Established" + session.getProtocol() + "connection with" + session.getPeerHost() +
- "using" + session.getCipherSuite());
- return ssl;
- }
- }
只定制 SSLSocketFactory 的方法, 看起来是比较难以达成目的了, 有人就想通过更深层的定制, 即同时自定义 SSLSocket 来实现, 如 GitHub 中的 https://github.com/guardianproject/NetCipher/blob/master/libnetcipher/src/info/guardianproject/netcipher/client/TlsOnlySocketFactory.java . 但是此种方案也不能解决解决问题, 因为支持 SSL 扩展的许多接口, 都不是标准的 SSLSocket 接口, 比如用于支持 SNI 的 setHostname()接口, 用于支持 ALPN 的 setAlpnProtocols() 和 getAlpnSelectedProtocol() 接口等. 这样的接口还会随着 SSL/TLS 协议的发展而不断增加.
到目前为止, 接入 HttpDns 的最好方法是, 不要替换请求的 URL 中的域名部分, 只在需要 Dns 的时候才使用 HttpDns. 具体而实现上, 使用那些可以定制 Dns 逻辑的网络库, 比如 OkHttp, 或者使用 Chromium 的网络库基础上做的库, 实现域名解析的接口, 并在该接口的实现中通过 HttpDns 模块来执行域名解析. 例如:
- private static class MyDns implements Dns {
- @Override
- public List<InetAddress> lookup(String hostname) throws UnknownHostException {
- List<String> strIps = HttpDns.getInstance().getIpByHost(hostname);
- List<InetAddress> ipList;
- if (strIps != null && strIps.size()> 0) {
- ipList = new ArrayList<>();
- for (String ip : strIps) {
- ipList.add(InetAddress.getByName(ip));
- }
- } else {
- ipList = Dns.SYSTEM.lookup(hostname);
- }
- return ipList;
- }
- }
- private OkHttp3Utils() {
- okhttp3.OkHttpClient.Builder builder = new okhttp3.OkHttpClient.Builder();
- builder.dns(new MyDns());
- mOkHttpClient = builder.build();
- }
来源: https://blog.csdn.net/xiangzhihong8/article/details/79957454