解密 | 一桩由数据洁癖引发的DNS悬案

域名系统(Domain Name System,以下简称DNS)是互联网的核心协议之一,不管是日常上网还是编程开发,都离不开它,说它是互联网的一项重要基础设施也不为过。

本文将通过追溯一段eBay DNS的陈年旧事,带你一起探究DNS中NS记录的工作原理。

引子

DNS中的记录存储在被称为“区域(zone)”的名字空间中(根据DNS实现的不同,可能具体表现为普通的文本文件,或是关系数据库中的表等)。 每个区域必须拥有类型为“NS”的同名记录,告诉记录消费者哪些服务器负责该区域的权威(authority,即记录数据的真正来源)。

通常来说,该NS记录也会用于该区域的父区域中,以实现父区域管理权向子区域的委托(delegation;见图1)。

在本文中,我们姑且称第二种类型的NS记录为“委托型NS”。

▲ 图1

DNS支撑着eBay私有云上规模巨大的应用流量。 据估计,eBay站点的DNS服务存储着逾四百万条记录,每秒钟处理的查询请求近百万级。 在推崇微服务化的今天,域名解析无疑是服务间相互调用的第一步,所以DNS的重要性不言而喻,任何短时间的服务不可用或性能降级都有可能酿成灾难后果。

而这一切正要从当年对某个DNS 区域的误删除说起。

追忆

同事W前辈是个有着严重数据洁癖的严谨工程师。有一天,他发现子区域corp.example.com已经被委托到了名称服务器DNS Slave 2,但这个区域同时又保留在父区域example.com 所在名称服务器DNS Slave 1 上(如图2所示)。

▲ 图2

此时,位于区域corp.example.com中的NS记录也都已指向新的名称服务器DNS Slave 2。

秉着“不把脏数据留给后来者,使之成为技术债“的原则,W前辈决定 将区域corp.ebay.com 从DNS Slave 1上删除。

作为一名老司机,规范行车(即遵守标准操作流程)早已融入工程师的血液中,所以W前辈在删除区域corp.ebay.com之前做了相当详尽的检查,以确保在删除操作后对corp.example.com域中记录的查询请求不会发送到DNS Slave 1。

检查可简单概括为以下三步:

01

找出通用顶级域(Generic Top Level Domain,gTLD) .com中的所有.com自己的NS记录,以获取.com的权威名称服务器(authoritative server)。

02

在第一步找到的.com的任一权威名称服务器上查询example.com 的NS记录,以获取example.com的权威名称服务器。

03

在第二步找到的example.com的任一名称服务器上查询corp.example.com的NS记录,确保没有任何NS记录指向DNS Slave 1。

预先的检查工作不可谓不到位,但意外总是突如其来。当删除操作执行后,公网上大量对corp.example.com域的DNS查询都失败了。

原因是example.com 区域中并不存在一条委托型NS记录 “corp.example.com NS ” 指向DNS Slave 2 。 所以事实上corp.example.com在DNS Slave 2并非一个被委托出去的区域,而是被配置为从属(slave)区域,而需要注意的是,从属区域所在的服务器一定是该区域的权威名称服务器。

在删除操作之前,递归(recursive)DNS查询通过 . → com.→ example.com. 的查询链将请求路由到example.com所在的名称服务器DNS Slave 1,而DNS Slave 1作为 corp.example.com区域的数据权威者,便直接返回了查询结果。

而在删除操作之后,递归查询任然按部就班的沿着 . → com. → example.com. 的查询链,最终路由到example.com所在的名称服务器DNS Slave 1,而此时DNS Slave 1上并不存在corp.example.com区域。

因为没有委托型NS记录,DNS Slave 1也无法回答发起查询的名称解析服务器(resolver)应该前往何处获取正确的结果。至此,DNS解析失败。

疑问

那么问题来了:

1.  为什么删除操作后,DNS Slave 2上仍然有到corp.example.com区域的查询流量?

2.  按上面的解释,岂不是区域本身的NS记录毫无作用,起效的都是父区域中指定的委托型NS?

为了找出真相,我们搭 建了简单的测 试环境。

实验

测试环境如图3.1所示:

▲ 图3.1

图中名称服务器运行的都是BIND 9.11,所有测试均在DNSSEC关闭的情况下进行。

名称解析服务器会把相关查询转发给父域(parent domain)的名称服务器。通过从名称解析服务器上发起多次对corp.example.com 子域中A记录的查询,并在父域名称服务器和子域名称服务器上运行tcpdump(1)抓包来一探究竟。

几次DNS查询及抓包的数据如下:

1

在名称解析服务器上没有example.com域缓存的情况下

a.在名称解析服务器上运行dig(1)做 A 记录的查询 (摘录部分):

▲ 图3.2

b.在名称解析服务器上运行 `rndc dumpdb -cache`得到DNS缓存结果如下,可见BIND缓存了来自AUTHORITY SECTION的委托型NS和来自ADDITIONAL SECTION的NS所指向的IP。

▲ 图3.3

c.在父域所在服务器上运行tcpdump(1)得到结果如下,奇怪,为什么有两次相同 A记录查询请求?唯一区别是前一个请求的UDP段的大小为512字节而后者是4096字节。

▲ 图3.4

d.在子域所在服务器上运行tcpdump(1)得到结果如下,奇怪,从响应中的1/ 0 / 1可知此次返回的AUTHORITY SECTION包含0条记录,可是前面的dig(1)查询到的AUTHORITY SECTION明明返回了委托型NS?

▲ 图3.5

2

在名称解析服务器上有委托型NS缓存的情况下

a.在名称解析服务器上运行dig(1)做A记录的查询 (摘录部分):

▲ 图3.6

b.在名称解析服务器上运行 `rndc dumpdb -cache` 得到缓存结果如下,显示区域本身的权威NS记录覆盖了原来的委托型NS。

▲ 图3.7

c.在父域所在服务器上运行tcpdump(1)得到结果如下,奇怪,之前不是已经缓存了委托型NS吗,为什么又向父域所在服务器发了DNS查询请求?

▲ 图3.8

d.在子域所在服务器上运行tcpdump(1)得到结果如下,从返回中的 1 / 1 / 2 可知此次返回的AUTHORITY SECTION包含一条记录,即区域本身的权威NS。

▲ 图3.9

3

在名称解析服务器上有区域权威NS(corp.example.com. NS ns-in-child.corp.example.com.) 缓存的情况下

a.在名称解析服务器上运行dig(1)做A记录的查询,结果为 SERVFAIL。

现象

总结观察到的测试结果:

1.名称解析服务器在没有NS缓存时做DNS查询基本符合上图中 1 → 2 → 3 的路径,即名称解析服务器向父域所在的名称服务器发起A记录查询 ,由于父域的名称务器不支持递归查询,所以它并不直接返回最终的查询结果,而是通过返回到委托型NS的Referral来告诉名称解析服务器该去何处寻找最终结果。 第一次测试的c抓包结果0/1/2正表示0 ANSWERSECTION,1 AUTHORITY SECTION (包含委托型NS),2 ADDITIONAL SECTION 。然后名称解析服务器再次向委托型NS所指向的子域的名称服务器发起A记录查询,并得到A记录的查询结果。

2.名称解析服务器在第二次查询结束后缓存了优先级更高的区域权威NS(corp.example.com. NS ns-in-child.corp.example.com.),并在第三次查询中使用本条缓存记录,即 DNS查询请求到了ns-in-child.corp.example.com,而因为这台服务器并非真正的名称服务器, 所以第三次DNS查询失败,返回状态为SERVFAIL。

一波未平,一波又起,问题又来了:

第一次查询的c抓包显示名称解析服务器发起了两次相同的DNS查询,唯一区别是前一个请求的UDP段大小为512字节而后者是4096字节,这是为什么?

第一次查询的d抓包显示返回结果1 / 0 / 1,即此次返回的AUTHORITY SECTION 包含 0 条记录,可是前面的 dig(1) 查询到的AUTHORITY SECTION 明明返回了委托型NS,这条记录又是来自哪里 ?

第二次查询的d抓包显示返回结果1 / 1 / 2,为何相同的两次查询请求,返回结果却不一致呢?

探究

秉着刨根问底,一探究竟,明知山有虎,不向虎山行的工程师不是好司机精神,我们查阅了大量DNS RFC、BIND文档以及BIND源码,找到了上述问题的答案。

1.对于为什么发起两次相同的查询请求, 不单对问题1,事实上我们能看到测试2的c抓包显示: 即使在本地有委托型NS 缓存的情况下,名称解析服务器仍然向父域的名称服务器发起了一次查询,而不是直接利用缓存向子域的名称服务器发起查询。

导致这些的原因正是我们在名称解析服务器的BIND中将父域的名称服务器配成了转发器(forwarder),而其默认行为是转发优先(Forward First),也就是优先使用转发器指定的DNS服务器做域名解析,如果查询不到再使用本机DNS服务器做域名解析。

体而言,当名称解析服务器本地存有A记录a.corp.example.com. 10 A 1.2.3.4 的缓存时名称解析服务器会直接返回结果,若没有,即便本地存在委托型NS的缓存,名称解析服务器仍然会向转发器指定的DNS服务器发起查询,而作为转发器的父域名称服务器不支持递归查询,第一次查询没有A记录结果,此时名称解析服务器才会再次尝试按照递归查询的方式(即图中的 1 → 2 → 3) 进行DNS查询,这也就是为什么我们能看到两次查询请求。

2. 对于发起的两次查询请求UDP段大小不同 已知扩展DNS(Extension mechanisms for DNS,EDNS)支持的最大UDP段大小为4096字节,而具体的大小仍然可以有不同的实现,只要在512 – 4096字节的区间内。由以下RFC 摘录不难推测,对同一个查询报文的目标服务器的首个查询请求为UDP段大小512字节,而后续请求UDP段大小为4096字节 正是BIND在探测防火墙或者其他网络限制之后尝试使用EDNS支持的最大负载(payload)尺寸。 之后的测试也显示后续到同一个目标服务器的请求UDP段大小都为4096字节,不再出现512字节的情况。

The requestor’s UDP payload size (encoded in the RR CLASS field) is the number of octets of the largest UDP payload that can be reassembled and delivered in the requestor’s network stack. Note that path MTU, with or without fragmentation, could be smaller than this. 

The requestor SHOULD place a value in this field that it can actually receive.  For example, if a requestor sits behind a firewall that will block fragmented IP packets, a requestor SHOULD NOT choose a value that will cause fragmentation. Doing so will prevent large responses from being received and can cause fallback to occur. This knowledge may be auto-detected by the implementation or provided by a human administrator. 

Where fragmentation is not a concern, use of bigger values SHOULD be considered by implementers.  Implementations SHOULD use their largest configured or implemented values as a starting point in an EDNS transaction in the absence of previous knowledge about the destination server.

▲ RFC6891 6.2.3. Requestor’s Payload Size

3.对于问题2,第一次dig(1)查询到的AUTHORITY SECTION 中委托型NS的由来, 通过抓包分析发现子域所在名服务器的返回中并不包含这条记录,但在名称解析服务器(本机BIND)返回给dig(1)时在AUTHORITY SECTION中加上了委托型NS,即 此时AUTHORITY SECTION 中的记录来自运行于名称解析服务器上的BIND而非父域名称服务器。

4.对于问题3,子域名称服务器对两次A记录查询的返回结果不一致, 我们找到了如下BIND源码。可见,当第一次查询请求UDP段大小为512字节时,BIND将打开最小返回(Minimal Responses)模式,即不返回AUTHORITY SECTION和 ADDITINAL SECTION中NS的相关记录,而ADDITINAL SECTION中默认包含一条EDNS的伪资源记录(OPT pseudo-RR),故返回结果为 1 / 0 / 1。

▲ 图4

结语

了解了NS的工作原理和许多BIND的实现细节,再回头看最初的两个问题,答案也就显而易见了。

为什么删除操作后,DNS Slave 2上仍然有到corp.example.com区域的查询流量?

原因是存在于corp.example.com区域本身的NS指向DNS Slave 2,这条NS在区域删除操作前已经被缓存在了部分名称解析服务器上,又因为NS的TTL通常较长,在TTL过期前,这些名称解析服务器的查询会直接到达 DNS Slave 2。

按上面的解释,岂不是区域本身的NS记录毫无作用,用到的都是父域中指定的委托型NS?

在首次DNS的递归查询中用到的是父域中指定的委托型NS,而发起查询的名称解析服务器会同时缓存区域本身的NS(区域本身的NS 缓存优先级高于委托型NS),而在后续的查询中名称解析服务器会优先使用缓存中的NS,直到该NS的TTL过期再重复之前的递归查询。

至此总算拨开云雾见青天,感觉自己又成长了呢。