数字时代,DNS(Domain Name System,域名系统)是最重要的战略资源之一。关注【融云全球互联网通信云】了解更多

就像我们访问好友需要知道他的地址一样,访问互联网也需要先获知“地址”。DNS 就是解析网址,将域名和 IP 地址相互映射的分布式数据库,能够使人更方便地访问互联网。所以,DNS 的运行效率和稳定性,极大影响我们的上网体验。

本文将详解 DNS 的工作流程,分析传统 DNS 解析面临的问题并分享 DoH 的破局实践。


详解 DNS

DNS 的层级结构

DNS 的层级结构必须支持高可用、高并发以及分布式,呈树形,自上而下分为四层,分别是根 DNS 服务器、顶级域 DNS 服务器、权威 DNS 服务器以及最贴近用户侧的本地 DNS 服务器(LocalDNS)。另外,还有一类比较特殊的 LocalDNS,被称为公共 DNS。

根 DNS 服务器作用是返回顶级域 DNS 服务器的地址。

顶级域 DNS 服务器作用是返回权威 DNS 服务器地址。

权威 DNS 服务器作用是返回对应主机的域名所解析的 IP 地址。

本地 DNS 服务器虽然没有域名解析结果的决定权,但它代理了用户向权威 DNS 服务器获取域名解析结果的过程,同时具备缓存解析结果的能力。在缓存有效期内,LocalDNS 不需要重复向权威 DNS 发起查询请求,可直接返回缓存结果。

DNS 的查询过程

以客户端访问 www.rongcloud.cn 为例,看域名的完整解析过程。

① 客户端向 LocalDNS 发出请求,询问域名的 IP 地址。

② LocalDNS 收到请求后,如发现缓存中有这个域名,则直接将对应结果返回给客户端;如没有,会向根 DNS 服务器发出询问请求。

③ 根 DNS 服务器收到请求后,反馈该域名由 .cn 顶级域负责,遂告知 LocalDNS 顶级域 DNS 服务器地址。

④ LocalDNS 向顶级域 DNS 服务器发出请求,询问域名的 IP 地址。

⑤ 顶级域 DNS 服务器反馈不知,让 LocalDNS 向权威 DNS 服务器询问,并告知其地址。

⑥ LocalDNS 向权威 DNS 服务器发出域名的 IP 地址查询需求。

⑦ 权威 DNS 服务器通过自己的配置查到对应的 IP 地址后,反馈给 LocalDNS。

⑧ 最终 LocalDNS 再将权威 DNS 服务器返回的 IP 地址发给客户端,同时记录到自己的缓存中。

⑨ 客户端通过这个 IP 地址和目标建立连接,发送业务数据。

这就是一次完整的域名解析过程。整个过程分为两部分,如下图示,左边是客户端与 LocalDNS 之间的交互,被称为递归查询;右边是 LocalDNS 与根/顶级域/权威 DNS 服务器之间的交互,被称为迭代查询

递归查询和迭代查询的区别是:递归查询是虽然我不知道,但原因我帮你问,你只需要等待最终结果就好了;而迭代查询则是虽然我不知道,但我可以告诉你谁可能知道,你需要自己去问它。

DNS 的缓存机制

执行一次完整的 DNS 解析请求一般耗时几百毫秒至几秒,如果每个客户端都通过这样的流程进行查询,用户体验将非常糟糕。

因此,在靠近客户端的位置,比如浏览器、客户端本地、LocalDNS 等位置都设计了 DNS 缓存机制,并设置相应的缓存生效时间,即 TTL(Time To Live,生存时间值)。在 TTL 时间范围内,当 DNS 解析请求命中某一级的缓存时会直接将缓存结果返回客户端。通过这种机制可以将解析时间缩减至数毫秒甚至微秒,减少解析域名的网络延迟,缓解权威 DNS 服务器的负载压力,但代价是一致性的牺牲。

因为客户端的缓存可以清理,但 LocalDNS 则完全不受用户及企业控制。

LocalDNS 主要由各级运营商自行管理与维护,当我们在权威 DNS 服务器控制台更新了一个域名的 IP 地址,没办法控制所有的 LocalDNS 缓存更新。所以,在缓存未超时的情况下,客户无法获取到正确的 IP 进行业务访问,只能等待 LocalDNS 服务器缓存超时后,重新去权威 DNS 服务器获取正确的 IP 地址。

除此之外,DNS 协议本身缺乏加密、认证、完整性保护的安全机制,这导致 DNS 经常被当做攻击对象,比如中间人攻击、缓存投毒、域名劫持等。

DNS 缓存投毒

缓存投毒攻击的最终目的,是通过一些手段使 LocalDNS 缓存的地址错误。在缓存有效期内的任何客户端来这个 LocalDNS 服务器解析域名,LocalDNS 都会直接将错误的地址发给客户端。

通常手段为:通过模拟权威 DNS 响应,并在真实的响应到达之前发送给 LocalDNS 实施攻击;也可以通过其他漏洞直接入侵到 LocalDNS 服务器进行篡改缓存。而客户端很难分辨网站是否有问题,容易造成个人信息泄露等风险。

DNS 负载均衡

如同一个人名下可以有多个手机号,一个域名也可以对应多个 IP 地址。不同客户端解析到的地址是多个 IP 地址中的一个,实现基于 DNS 的负载均衡。管理员可以为每个 IP 地址设置不同的权重,用来控制客户端得到这个 IP 地址的比例。

通过 DNS 的负载均衡能力,可以实现站点的同城多活。比如,同一个城市的不同机房 A 和 B 对外提供相同的服务, 将同一个域名同时解析到 A、B 机房的 IP 地址上,每个机房所承载的流量也可以通过设置权重来进行调整。

在实际使用中,服务器应该部署在不同的地区,这样可以让客户端直接访问距离更近的服务器,更快得到响应,提升用户体验。

正常情况下,客户端通过运营商提供的 LocalDNS 进行域名解析, 权威 DNS 则通过 LocalDNS 地址确认运营商及 LocalDNS 所在地区,实现基于运营商或地域的调度策略,为客户端下发最优地址。

以下情况会造成 LocalDNS 调度不准。
① 坐标干扰:客户端并没有使用自己所在地的运营商,或者自己所在地区的 LocalDNS。导致权威 DNS 服务器错误判断客户端的运营商与地域信息,从而将一个误认为最优但实际不是最优的地址返回给客户端。

② 缓存干扰:客户端使用了并非由运营商维护的公共 DNS,比如,北京联通客户端在公共 DNS 做了解析,会导致权威 DNS 服务器认为该公共 DNS 来的都是北京联通客户端。此时若上海电信的客户端也来这个集群请求该域名,会直接收到刚刚缓存北京联通的地址。

③ 解析转发:有一些运营商,虽然提供了 LocalDNS 服务,但不做迭代查询,而是将请求转发给其他运营商 LocalDNS,导致权威 DNS 服务器收到请求的 IP 地址和客户端实际所在的运营商 IP 地址不一致,从而错误地判断客户端的地理位置或运营商,给客户端返回了以为最优但并不是的地址。

那么,为什么不以客户端的地址作为判断运营商与地域的条件呢?

事实上,很多年前谷歌就针对这个问题提交了一份 DNS 扩展协议草案,并于 2016 年 5 月被正式纳入到 RFC 7871 中,即 Edns-Client-Subnet,简称为 ECS。

它的原理是基于原有的 DNS 扩展协议字段,将客户端真实地址带上,并通过 DNS 查询最终发给权威 DNS,这样在任何不正常情况下,权威 DNS 服务器都可以根据客户端真实地址来判断所在的运营商与地域。而要使用 ECS 能力,需要客户端或者 LocalDNS 的支持。在构造 DNS 请求的时候将客户端地址放到对应的字段上,同时也需要权威 DNS 的支持,在请求到达权威 DNS 后,将对应客户端地址解析出来,并按照这个地址进行调度逻辑的判断依据。

但理想很丰满,现实很骨感。ECS 的普及程度并不高,因为目前市场上支持 ECS 的权威 DNS 不多,支持 ECS 的 LocalDNS 更是少之又少。而在中国,基本没有运营商的 LocalDNS 支持。


传统 DNS 解析面临的问题

总结而言,传统 DNS 解析面临以下几大问题:
首先,解析延迟高,因为首次需要迭代一级一级去查询。

其次,缓存生效不及时,因为 LocalDNS 是一个松散的管理模式,并且不受掌控。

再次,缓存投毒风险,因为 DNS 协议校验并不严格导致常被攻击。

以及,调度不准确,因为各种客户端和 LocalDNS 的错误使用或非标准使用,导致权威 DNS 服务器无法真实判断真实客户端所在地区和运营商。

我们可以基于 HTTP 协议搭建一套自己的 DNS 服务集群,并且让这套服务集群分布在各个地区与各个运营商,当客户端需要做域名解析时,直接通过 HTTP 协议与这套集群进行通信,得到对应的域名地址就可以了,这就是 DoH(DNS over HTTPS,也叫 HTTPDNS)。但不是所有业务都可以使用 DoH,因为默认的域名解析都要通过 DNS 协议,所以使用 DoH 的前提是客户端必须自己实现或集成已有的 DoH SDK,这样才可以绕过传统 DNS 进行域名解析,移动端应用使用较多。


DoH 如何破局

我们先来了解一下 DoH 的具体工作模式
客户端要访问 DoH 集群,首先要知道集群的地址,通常是在 SDK 中集成多个集群地址。

当客户端需要解析域名时,会先查询自己的本地缓存,当发现本地缓存存在,直接将对应结果返回给业务应用。

这个缓存和 LocalDNS 的缓存不一样,因为它在 DoH SDK 中自己实现,可以结合自己的实际业务情况来定义缓存策略,或者通过云控平台来控制,达到更新的动作。

如果客户端缓存中不存在域名解析结果,就发送一条 HTTP 请求,请求中包含要解析的域名,同时也会带有真实客户端的地址,这样在 DoH 服务接收到请求后可以根据真实的客户端地址进行解析,得到客户端所在的地区与运营商等信息,根据这些信息来调度适合该客户端的接入地址并返回。

当然,如果客户端访问 DoH 失败,也可以降级回到 LocalDNS 进行域名解析。

那么,DoH 如何解决上述 DNS 面临的问题呢?

这些问题大体可以分为三类:DNS 协议本身缺乏加密、认证、完整性保护的安全机制;解析速度与缓存更新速度的平衡;调度精准度问题。

第一类问题,DoH 基于 HTTP 协议实现,可以适用于几乎所有的网络环境,同时保留了鉴权、HTTPS 等更高安全性的扩展能力,避免恶意攻击劫持等行为。

第二类问题,DoH 将解析速度和更新速度全部掌控在自己手中。一方面,解析的过程,不需要本地 DNS 服务递归调用,一个 HTTP 请求可直接搞定且可以实时更新;另一方面,缓存在客户端 SDK 维护,过期时间、更新时间可自己控制。

第三类问题,通过 DoH 不需要经过 LocalDNS,也就不会出现调度问题。对于 HTTP 来说,透传客户端原地址是非常成熟的能力,可以确保将客户端真实原地址传递给 DoH 服务端,DoH 服务端可以根据真实的客户端地址进行运营商级别或地域级别的调度。

DoH 使用需要注意如下情况:
站点一般通过 Nginx 等反向代理软件进行统一接入,在该类软件上,通常以 HTTP Header 中的 Host 字段进行请求的路由匹配或者流量转发。

比如,左侧第一张图可以看到融云官网 www.rongcloud.cn 与融云的文档库站点 docs.rongcloud.cn 的解析地址是相同的。

当接入层服务收到对应的请求后,通过请求头中的 Host 字段的值来判断将这条请求转发给哪个后端服务。

当我们通过标准的网络库访问融云的官网域名时,实际发出的 HTTP 请求中 Header 的 Host 字段会自动补充为我们所访问的域名。实际发出的请求就像左侧第二张图一样,但使用 DoH 后我们需要将 HTTP 请求 URL 中的域名替换为解析获得的 IP 地址,这时由于标准的网络库会将 URL 中的 Host 域赋值给 HTTP 请求头中的 Host 头,发出的网络请求就会像左侧第三张图一样,Host 字段是个 IP 地址。

当这个请求发送到服务端后,会导致服务端无法正确判断应该将请求转发给哪个后端服务,这时服务端一般有两种策略来处理,一种是直接拒绝,一般会直接返回 403 HTTP 状态码;另一种则是发给默认的后端服务,但这个默认后端服务有可能并不是真实想要访问的后端服务,最终的结果就是会导致请求失败,或者无法得到预期的响应结果。

为了解决这个问题,我们需要在使用 DoH 时,主动设置 HTTP 请求 Host 头的值。

以 Android 官方网络库 HttpURLConnection 为例,代码如上图右。第三行通过 getHost 方法获取到要请求的域名,第五行通过 DoH SDK 获取到这个域名的地址,第九行通过 replaceFirst 方法将 URL 的域名换成 IP 地址,第十二行通过 setRequestProperty 方法设置 Host 头的值为要请求的域名。

当请求发出去后, 就会和未使用 DoH 所发出去的请求保持一致了。


基于 DoH 域名解析的优化实践

主要方式为预解析、智能缓存与懒加载。
预解析是在 App 应用初始化阶段的启动期进行预热,即针对业务的热点域名在后台发起异步的 DoH 解析请求。这部分预解析结果在后续的业务请求中可以直接使用,进而消除首次业务请求的 DNS 解析开销,提升 App 加载速度。

通过预解析获取域名的 IP 地址,同时也会有 TTL 缓存有效时间,这个 IP 地址需要合理缓存。通常操作系统本身的 DNS 缓存粒度比较粗,只是域名与地址的关系,但在客户端可以实现更细粒度的缓存管理来提升解析与使用效率。

比如在不同的网络运营商环境下,对域名的解析结果可能会发生变化,当我们使用电信 Wi-Fi 时,解析会返回就近的电信节点的 IP 地址,当我们使用联通 3G 时,解析会返回就近的联通节点的 IP 地址。我们可以针对不同运营商的解析结果进行缓存或者根据不同 Wi-Fi 的 SSID 进行缓存,确保我们在网络切换时能快速准确地获取到对应网络下的解析地址。甚至, 我们可以做本地的持久化缓存,当下一次 App 启动时直接读取缓存用于网络访问,提升用户体验。当然,这种持久化缓存一定要同时设计好缓存失效机制。

还有一种优化策略是懒加载
懒加载与预解析的配合使用可以真正实现 DNS 的零延迟解析,核心实现思路主要是两点:

① 业务层的域名解析请求只和本地缓存交互,不发生实际的网络解析请求,如果缓存存在,不管是否过期都直接返回缓存的记录。

② 若缓存过期,在返回结果的同时,发起一个异步后台网络请求与 DoH 服务器获取新的地址。

返回一个过期的 IP 地址,好像又变成了像 LocalDNS 一样更新不及时。但实际上不一样,因为 LocalDNS 的不及时是不可控的,而我们虽然返回了一个过期的 IP 地址,但同时也会异步地去更新缓存的内容,如果这个过期的 IP 地址在业务上无法正常使用,可以重新通过缓存获取到更新后的 IP 地址。

另外异步 HTTP 请求还可以将多个过期或即将过期的域名,通过一条请求发送给 DoH 服务端,从而减少网络的消耗以及服务器的压力。

当然,实际业务中,优化手段的选择需要根据业务情况进行适配选择,以更好地应对业务挑战及实现安全、可靠的网络访问。


融云RongCloud
82 声望1.2k 粉丝

因为专注,所以专业