httpdns是基于Http协议的域名解析服务,用于替代基于UDP协议向运营商Local DNS发起解析请求的传统方式,目标是解决域名劫持和跨网访问等问题。
传统的使用Local DNS的方式,我们只需要在url中指定hostname;网络库帮我们做了域名解析、ttl缓存管理,我们不需要关心底层的流程。
但当使用httpdns,就需要我们自己向httpdns发起http请求,获取到域名对应的ip。一般商业的httpdns sdk都会提供域名解析成ip的方法,一般的开发者接入httpdns还是需要做以下工作:
android端有三个网络基础库,具体的接入方式会有差异:
对于最后一个okhttp,由于它本身提供的设置外部dns的api,接入最简单,只需要简单的实现okhttp提供的lookup方法即可;但针对apache和urlconnection,就只能采取显式将url中的hostname替换成ip的方式了,然后添加Host头和HTTPS证书验证策略。但由于apache httpclient和urlconnection在设计之初,就没有考虑外部设置dns的策略,进行相关的接入时,总有各种异常case需要处理,比如apache http client在实现上,使用的是url中的ip信息作为cookie存储管理的key,而非请求头的host。
总之,现有的解法都不能完美的覆盖掉所有场景。如果想使用httpdns,那么建议开发者抛弃掉apache或urlconnection,引入okhttp这样的第三方网络库实现。
但的确会有那么一小撮仍在使用apache或者urlconnection的app,在不对现有网络库进行改造的情况下,有办法实现httpdns的低成本接入吗
我们先看下android的Netd的设计,
在android 4.3之后,DNS解析都向Netd代理的方式来管理,所有进程的dns解析都通过IPC向Netd进行来进行。 受限于权限问题,我们没法对Netd做修改,但也正因为Netd这个设计,我们对当前进程的dns解析结果的改动并不会影响设备上其他应用的dns解析。
顺着apache client/urlconnetion/okhttp的源码往下翻,我们会发现无论何种网络库,最终都需要使用libcore提供InetAddress.getByName() API来进行DNS解析,并最终调用android native层提供的DNS解析C函数。以android5.0为分界,出现了两个分支:
android 5.0以下系统将使用中的getaddrinfo;
int getaddrinfo( const char *hostname, const char *service, const struct addrinfo *hints, struct addrinfo **result )
android5.0及以上系统中android_getaddrinfoforget,相关的签名如下:
int android_getaddrinfoforget(const char *hostname, const char *service, const struct addrinfo *hints, struct addrinfo **result );
这两个函数的参数完全一致,都通过hostname参数来指定查询的域名, result参数来返回域名对应的IP列表。他们都会编译进libjavacore.so这个动态链接库。
相关的源码调用关系有些长,总之得出一个结论:所有网络库的dns解析流程入口都是同一个。
既然入口是同一个,我们可以针对这个入口做一些hook的事情。
如果围绕java层的InetAddress.getByName()找方案,由于它是普通的静态方法,插件化常见的代理方案都将失效;从虚拟机这层hook,比如说Andfix,又会有遇到虚拟机兼容以及方案过重的问题。
而从native层想办法成本则小了点。比如使用ptrace注入,又比如elfhook。
具体的流程是,在android 应用刚启动时,我们可以从libjavacore.so中,分别找到getaddrinfo和android_getaddrinfoforget两个函数的指针地址,将其指向我们自定义的get_httpdns_addrinfo。在get_httpdns_addrinfo函数内部,我们调用httpdns的域名解析,将ip填充到struct addrinfo **result结构体中。
利用这种方式,上层网络库是无感知的,仍然按照他本身InetAddress.getByName的逻辑获取域名解析结果,cookie、sni的问题也仍然按照网络库原始的策略管理,从而实现httpdns的无缝接入。
本文来自网易实践者社区,经作者郑文授权发布。