HttpDNS功能说明及实现

功能说明

HTTPDNS使用HTTP协议进行域名解析,代替现有基于UDP的DNS协议,域名解析请求直接发送到阿里云的HTTPDNS服务器,从而绕过运营商的Local DNS,能够避免Local DNS造成的域名劫持问题和调度不精准问题。

功能 说明
防劫持 绕过运营商Local DNS,避免域名劫持,让每一次访问都畅通无阻。
精准调度 基于访问的来源IP,获得最精准的解析结果,让客户端就近接入业务节点。
0ms解析延迟 通过热点域名预解析、缓存DNS解析结果、解析结果懒更新策略等方式实现0解析延迟
快速生效 避免Local DNS不遵循权威TTL,解析结果长时间无法更新的问题
降低解析失败率 有效降低无线场景下解析失败的比率

防劫持

HTTPDNS代替了传统的LocalDNS完成递归解析的功能,基于HTTP协议的设计可以适用于几乎所有的网络环境,同时保留了鉴权、HTTPS等更高安全性的扩展能力,避免恶意攻击劫持行为。

精准调度

传统域名解析的调度精准性问题,本质根源在于Local DNS的部署和分配机制上。由于碎片化的管理方式,这些环节的服务质量同样很难得到保障。HTTPDNS在递归解析实现上优化了与权威DNS的交互,通过<a name="https://datatracker.ietf.org/doc/rfc7871">edns-client-subnet</a>协议将终端用户的IP信息直接交付给权威DNS,这样权威DNS就可以忽略Local DNS IP信息,根据终端用户的IP信息进行精准调度,避免Local DNS的坐标干扰

DNS解析0延迟:

  • 构建客户端DNS缓存;

      通过合理的DNS缓存,我们确保每次网络交互的DNS解析都是从内存中获取IP信息,从而大幅降低DNS解析开销。根据业务的不同,我们可以
      制订更丰富的缓存策略,如根据运营商缓存,可以在网络切换的场景下复用已缓存的不同运营商线路的域名IP信息,避免网络切换后进行链
      路重选择引入的DNS网络解析开销。另外,我们还可以引入IP本地化离线存储,在客户端重启时快速从本地读取域名IP信息,大幅提升首页
      载入效率。
    
  • 热点域名预解析;

      在客户端启动过程中,我们可以通过热点域名的预解析完成热点域名的缓存载入。当真正的业务请求发生时,直接由内存中读取目标域名的IP
      信息,避免传统DNS的网络开销。
    
  • 懒更新策略;

      绝大多数场景下业务域名的IP信息变更并不频繁,特别是在单次APP的使用周期内,域名解析获取的IP往往是相同的(特殊业务场景除外)。
      因此我们可以利用DNS懒更新策略来实现TTL过期后的DNS快速解析。所谓DNS懒更新策略即客户端不主动探测域名对应IP的TTL时间,当业务
      请求需要访问某个业务域名时,查询内存缓存并返回该业务域名对应的IP解析结果。如果IP解析结果的TTL已过期,则在后台进行异步DNS网
      络解析与缓存结果更新。通过上述策略,用户的所有DNS解析都在与内存交互,避免了网络交互引入的延迟。
    

实现方案

服务端:

服务端提供API接口,app端直接通过ip地址访问,ip地址可以有多个

请求方式:HTTP GET

URL参数说明:

名称 是否必须 描述
host 必须 要解析的域名
ip 可选 用户的来源IP,如果没指定这个参数,默认使用请求连接的源IP

请求示例:

考虑到服务IP防攻击之类的安全风险,为保障服务可用性,HTTPDNS同时提供多个服务IP,当某个服务IP在异常情况下不可用时,可以使用其它服务IP进行重试。

请求成功时,HTTP响应的状态码为200,响应结果用JSON格式表示,示例如下:
{
  "host": "www.suning.com",
  "ips": [
    "112.84.104.48"
  ],
  "ttl": 57,
  "origin_ttl": 120
}
请求失败的响应示例:
{
  "code": "MissingArgument"
}

错误码列表如下:

错误码 HTTP状态码 描述
MissingArgument 400 缺少必要参数
InvalidHost 400 域名格式不合法
MethodNotAllowed 405 不支持的HTTP方法
InternalError 500 服务端内部错误
错误处理

异常下的出错兼容逻辑,主要包括异步请求,重试,降级

异步请求

访问HTTPDNS服务时,应该使用异步请求的策略,避免解析延迟太大而对业务造成影响,特别是在网络环境异常或HTTPDNS服务IP异常不可
用时,如果用同步访问,需要等待网络超时后才会返回解析失败,这个超时时间较大,可能对业务的使用体验造成很大影响。

异步请求策略:解析域名时,如果当前缓存中有TTL未过期的IP,可直接使用;如果没有,则立刻让此次请求降级走原生LocalDNS解析,同
时另起线程异步地发起HTTPDNS请求进行解析,更新缓存,这样后续解析域名时就能命中缓存。

重试

访问HTTPDNS服务解析域名时,如果请求HTTPDNS服务端失败,即HTTP请求没有返回,可以进行重试。

大部分情况下,这种访问失败是由于网络原因引起的,重试可以解决。

降级

不管是因为什么原因,当通过HTTPDNS服务无法获得域名对应的IP时,都必须降级:使用标准的DNS解析,通过Local DNS去解析域名。

Android端:

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

  • 自定义DNS接口

OkHttp暴露了一个Dns接口,通过实现该接口,我们可以自定义Dns服务:

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服务:

private void okhttpDnsRequest() {
    OkHttpClient client = new OkHttpClient.Builder()
    .dns(OkHttpDns.getInstance(getApplicationContext()))
    .build();
    Request request = new Request.Builder()
    .url("http://www.aliyun.com")
    .build();
    Response response = null;
    client.newCall(request).enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) {
            e.printStackTrace();
        }
        @Override
        public void onResponse(Call call, Response response) throws IOException {
            if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
                DataInputStream dis = new DataInputStream(response.body().byteStream());
                int len;
                byte[] buff = new byte[4096];
                StringBuilder result = new StringBuilder();
                while ((len = dis.read(buff)) != -1) {
                    result.append(new String(buff, 0, len));
                }
                Log.d("OkHttpDns", "Response: " + result.toString());
            }
        });
}
  • 总结

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

实现简单,只需通过实现Dns接口即可接入HttpDns服务

通用性强,该方案在HTTPS,SNI以及设置Cookie等场景均适用。规避了证书校验,域名检查等环节

IOS端:

基于NSURLProtocol可拦截iOS系统上基于上层网络库NSURLConnection/NSURLSession发出的网络请求;

通过以下接口注册自定义NSURLProtocol,用于拦截上层网络请求,并创建新的网络请求接管数据发送、接收、重定向等处理逻辑,将结果反馈给原始请求。

[NSURLProtocol registerClass:[CustomProtocol class]];

自定义NSURLProtocol处理过程概述:

  • 在canInitWithRequest中过滤要需要做HTTPDNS域名解析的请求;
  • 请求拦截后,做HTTPDNS域名解析;
  • 解析完成后,同普通请求一样,替换URL.host字段,替换HTTP Header Host域,并接管该请求的数据发送、接收、重定向等处理;

推荐阅读更多精彩内容