DNS原理浅析:如何查询一个域名所有IP

  1. What’s DNS
  2. 域名
    1. 域名层级和结构
    2. TLD域名及分类
    3. 点.开头的域名
    4. 点.结尾的域名
  3. 资源记录
    1. RR的TYPE
    2. RR的CLASS
    3. 获取域名对应RR
  4. 域名协议
    1. Header段
    2. Question段
    3. Answer/Authority/Additional段
    4. EDNS0
  5. 域名服务器
    1. Zone
    2. Zone file
    3. Name Server的分类
    4. DNS 工作原理
  6. DNS Resolver
    1. 常见Resolver服务
  7. DNS智能解析
  8. 回到问题:如何获取域名对应的IP列表
  9. 参考

有一天老婆问我:

xxx.yyy.zzz.com 怎么查对应的ip?

我的第一反应是直接ping不就好了,然后又想了一下,如果域名配置了多IP(不同运营商),具体是返回哪个IP呢?是怎么根据自己的出口来决定的呢?又该如何获取一个域名对应的所有IP呢?一连串疑问,其实以前也有想过这个问题,但是一直没有深究,这里还是研究一下,把计算机网络相关的知识再学习一遍。

What’s DNS

DNS(Domain Name System)最开始就是为了解决域名到主机IP转换的分布式数据库系统,网络层是通过IP数据包进行传输的,所有计算机之间的通信的前提是需要对端的IP,其实再往下探究就是如何通过IP获取对端主机的MAC地址,链路层是通过MAC地址进行数据传输的,如下就是引用「Protocol structure of 6LoWPAN devices」中基于「TCP/IP」四层协议的经典数据包的结构示意图:

既然DNS是一个系统,而且是比较典型的C/S的架构系统,那么我们从服务器设计的角度来学习一些DNS系统是怎么设计的,它的基本数据结构,协议结构和服务器到底是怎么设计的,结合RFC和IANA相关的文档进行相关介绍。

按照「RFC1034 S2.4」对于DNS的介绍,DNS主要有三个部分组成:

  1. 域名空间(Domain Name Space)和资源记录(Resource Records):域名空间就是域名组成的层级树状结构,资源记录就是每个域名都对应一组资源描述数据,描述域名对应信息,例如A记录表示对应解析的IP。后面会详细介绍这两个部分。
  2. 名字服务器(Name Servers):分布式Server,持有域名子树和其对应的资源记录数据(Zone file),响应Resolvers的域名查询请求。后面会详细介绍。
  3. 解析器(Resolvers):客户端程序,根据客户端请求向Name Servers中查询信息。解析器必须能够访问至少一个名称服务器,并使用该名称服务器的信息直接回答查询,或者通过引用其他名称服务器来继续查询。解析器通常是用户程序可以直接访问的系统库;例如Linux中程序都是通过glibcgetaddrinfo(), gethostbyname2()来执行域名的解析。因此用户和resolver之间不需要用什么协议

这里主要从以下几个角度去学习:

  • DNS的基本数据结构:域名和资源记录;
  • DNS的协议;
  • DNS的架构;

域名

域名,从上网的第一天我们就接触到的概念,参考「wikipeida」解释如下:

由一串用点分隔的字符组成的互联网上某一台计算机或计算机组的名称,用于在数据传输时标识计算机的电子方位。域名可以说是一个IP地址的代称,目的是为了便于记忆后者。

域名的结构是分层的,域名中的域可以分为:顶级域,二级域,三级域,依次等到,每个域都是用.隔开Label组成的,「RFC 1035」关于「域名」的语法定义如下(最早定义其实是「RFC 952」):

1
2
3
4
5
6
7
8
<domain> ::= <subdomain> | " "
<subdomain> ::= <label> | <subdomain> "." <label>
<label> ::= <letter> [ [ <ldh-str> ] <let-dig> ]
<ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
<let-dig-hyp> ::= <let-dig> | "-"
<let-dig> ::= <letter> | <digit>
<letter> ::= any one of the 52 alphabetic characters A through Z in upper case and a through z in lower case
<digit> ::= any one of the ten digits 0 through 9

上面语法格式的定义,总结一下就是:

  • 域名由子域组成,子域由label和.组成;
  • label:由英文字母,数字,连字符(-)组成,以字母开头,字母或者数字结尾,所有连字符只能在中间;
  • 每个label最多不超过63个字符,整个域名的长度不超过255个字符。域名大小写不敏感。

其中关于以字母开头的描述已经是过时的了,因为我们见过很多存数字的域名,例如:

1
2
12306.cn
4399.com

RFC 952」和「RFC 1035」发布的草案,关于以字母开头的描述已经在「RFC 1123」被废弃,如下:

The syntax of a legal Internet host name was specified in RFC-952 [DNS:4]. One aspect of host name syntax is hereby changed: the restriction on the first character is relaxed to allow either a letter or a digit. Host software MUST support this more liberal syntax.

所以关于域名的Label的最新定义应该是:由英文字母,数字,连字符(-)组成,以字母或者数字开头和结尾,所有连字符只能在中间

RFC 2181」发布的草案,对DNS名字的限制更为宽松,只需要是Label和Full Name长度符合「RFC 1035」,对于任何二进制串都可以作为DNS的名字,但是实际系统并没有实现该RFC网上关于此的讨论。如下是关于「RFC 2181」的描述:

The DNS itself places only one restriction on the particular labels that can be used to identify resource records. That one restriction relates to the length of the label and the full name. The length of any one label is limited to between 1 and 63 octets. A full domain name is limited to 255 octets (including the separators).

Those restrictions aside, any binary string whatever can be used as the label of any resource record. Similarly, any binary string can serve as the value of any record that includes a domain name as some or all of its value (SOA, NS, MX, PTR, CNAME, and any others that may be added).

虽然DNS没有规定一个域名可以包含多少下级域名,但是根据上面的语法规定:理论上,域名最多可以有127层(每层一个字符,中间用点分隔)。可能你会问:不应该是128层吗?其实,域名的结尾还有一个根标签,一个空串,书写时通常省略,后面会介绍。

域名层级和结构

按照「RFC 1034」的对于域名空间的描述:

  • 域名空间是一种树形结构。树上的每个节点和叶子都对应一个资源集(可能为空)。域名系统不区分内部节点和叶子的用途,本备忘录使用“节点”一词来指代两者。
  • 每个节点都有一个标签,长度为0~63个八位字节。兄弟节点不能具有相同的标签,尽管可以在非兄弟节点之间使用相同的标签。有一个标签是保留的,即用于根的空(即零长度)标签,所以Domain Name中只能存在一个长度为0的标签:根标签
  • 节点的域名是从节点到树根路径上的标签列表。按照惯例,组成域名的标签从左到右打印或读取,从最具体(最低,离根最远)到最不具体(最高,离根最近)。

下面就是我画的一个域名树状结构的简要示意图:

在程序内部处理中,应将它们表示为标签序列,其中每个标签由一个字节的长度和一个字符串组成。由于所有域名都以根标签结尾,根标签是一个空字符串作为标签,因此程序内部表示可以使用零长度字节来终止域名。

如下是BIND(最流行的DNS服务器开源软件,使用BIND作为服务器软件的DNS服务器约占所有DNS服务器的九成),关于DOMAIN NAME的结构定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct dns_name {
unsigned int magic; // 域名结构体的魔数
unsigned char *ndata; // 域名原始数据(包含了每个label的长度)
unsigned int length; // 域名的ndata的长度
unsigned int labels; // 域名的label的个数
struct dns_name_attrs {
bool absolute : 1; /*%< Used by name.c */
bool readonly : 1; /*%< Used by name.c */
bool dynamic : 1; /*%< Used by name.c */
bool dynoffsets : 1; /*%< Used by name.c */
bool nocompress : 1; /*%< Used by name.c */
bool cache : 1; /*%< Used by resolver. */
bool answer : 1; /*%< Used by resolver. */
bool ncache : 1; /*%< Used by resolver. */
bool chaining : 1; /*%< Used by resolver. */
bool chase : 1; /*%< Used by resolver. */
bool wildcard : 1; /*%< Used by server. */
bool prerequisite : 1; /*%< Used by client. */
bool update : 1; /*%< Used by client. */
bool hasupdaterec : 1; /*%< Used by client. */
} attributes;
unsigned char *offsets; // 域名中每个label在ndata中的偏移
isc_buffer_t *buffer;
ISC_LINK(dns_name_t) link;
ISC_LIST(dns_rdataset_t) list;
};

关于BIND中DNS 域名结构的定义,我画了一个简要的结构描述示意图,如下:

RFC 1034」定义了所有的DNS Name都必须以root为截止,但是BIND中关于Name的实现说明:

Note that all names are not required to end with the root label, as they are in the actual DNS wire protocol.

针对用户输入的域名,就是将各个label的长度省略,label之间'.'分割,由于全Domain Name要求最后以root label为结尾,所以全Domain Name最后显示就是一个以'.'结尾的串,「RFC 1034」针对用户输入的Domain Name是否以root '.'为结尾,将域名分为两类:

  • absolute域名:以root '.'为结尾的域名,例如:poneria.ISI.EDU.。绝对域名目前又称之为FQDN(Fully Qualified Domain Name,完全限定域名),指从根域名开始一直到最低一级的所有域名标签。它提供了域名的完整路径,能够唯一地标识一个主机或者资源;
  • relative域名:只有部分前置labels表示的串,可能是一个不完整的domain name,可能需要本地软件进行域名补全的,例如:在ISI.EDU域中使用的的poneria域名;

所以,总结一下就是:根标签的作用是区分绝对域名和相对域名。绝对域名是完整的域名,它包括主机名和域名,以及根标签。相对域名则不包括根标签,它相对于当前搜索域进行解析。例如,在 example.com 域中,相对域名 www 会被解析为 www.example.com。在 DNS 解析中,使用绝对域名可以确保解析的准确性,避免因为相对域名而导致的解析错误。

TLD域名及分类

TLD(Top Level Domain)顶级域,前面介绍「域名层级和结构」的时候,域名树状结构的示意图,也列出来了一些TLD的域名。TLD域名经过这么多年的发展,现在主要有以下几个大类:

  • Infrastructure TLD(ARPA):基础设施顶级域,通常称之为ARPA域,因为里面只有一个.arpa域,预留给DNS服务器用来进行DNS反向域名解析功能。现在其中的两个二级域名,in-addr.arpaip6.arpa,用于对应 IPv4 和 IPv6 的 DNS 反向查询功能。

  • gTLD(generic TLD):通用顶级域,由IANA维护和管理,主要包含:.com.net.org等最开始一批顶级域,后面又新增了很多顶级域,例如:.xyz.vip等。gTLD的长度要求一定是至少3个字符

  • ccTLD(country code TLD):国家(地区)顶级域,长度固定2个字符,用来表示国家,主权国家,或附属领土的顶级域,由IANA分配并交由各个国家进行管理。中国的ccTLD为.cn,美国为.us,香港为.hk

  • IDN ccTLD(Internationalized Domain Name ccTLD):国际化顶级域,是上面说的ccTLD的一种,为什么要支持国际化ccTLD呢?其实就是为了能够让域名能够本地化,让使用者能通熟易懂,但是Domain Name的RFC规范和历史原因,很难彻底将Domain Name改为Unicode编码,所以最终采用了折衷的方案,于1990年在新加坡国立大学教授陈定炜指导下,通过Punycode算法,将Unicode字符集编码为符合Domain Name规范的有限ASCII码集合。IDN的顶级域有:.中国(编码 .xn--fiqs8s),.香港(编码 .xn--j6w193g),.한국(编码xn--3e0b707e)。下面是搜到的一些以.中国顶级域可访问的一些网站,如下:

1
2
3
4
qq.中国
中国移动.中国
华大基因.中国
小度.中国

当我们在浏览器输入qq.中国的时候,浏览器自动做了编码转换,如下:

  • Reserved TLD:保留顶级域,这些顶级域的预留要么是为了DNS内部自己使用,或者避免混淆和冲突,主要有以下一些:
1
2
3
4
5
6
example.		// 保留用于示例
invalid. // 保留用于无效域名
localhost. // 保留避免与传统localhost作为主机名发生冲突
test. // 保留用于测试
local. // 保留用于多播DNS名称解析协议解析的本地链路主机名
onion. // 保留用于Tor洋葱服务的自验证名称

其实上面的ARPA顶级域也可以归为Reserved TLD一类,RFC并没有对TLD进行很明确的分类。这里是IANA关于Reserved Domains的一些说明。

IANA负责所有TLD的管理,包括分配TLD的运营商,并维护其技术和管理详细信息。关于所有TLDs,IANA有一个Root Zone Database,包含了所有TLD的运营商记录,我们可以从Root Zone Database中看到所有的TLD,目前里面居然已经有了1951个TLD

顶级域名居然也可以向ICANN申请

https://www.zhihu.com/question/63552850

.开头的域名

不过曾经看到过域名开头有'.'开头的域名,那前导'.'有啥含义呢?例如在浏览器输入如下地址:

1
2
3
.www.google.com
.twitter.com
.www.baidu.com

经过Chrome抓包测试,会发现当输入http://.www.google.com的时候,Chrome会自动删除前导的点号,变成http://www.google.com,所以正如Bing给的解释一样:

又测试了一下curl命令,发现curl并没有做相关的兼容,如下:

1
2
3
4
5
6
$curl http://.www.baidu.com/
curl: (6) Could not resolve host: .www.baidu.com

$curl http://www.baidu.com/
<!DOCTYPE html>
<!--STATUS OK--><html> <head>...<title>百度一下,你就知道</title>...</head>...</html>

.结尾的域名

在介绍「域名层级和结构」一节中,我们知道了DNS的Domain Name规范最后要求有一个空串表示的root label,以此表示绝对域名,在书写的时候就会表现为域名最后有一个.。如下,在Chrome中输入www.google.com.后的请求表现:可以看到www.google.com.被重定向到了www.google.com

但是并不是所有的域名都可以通过这种绝对域名的方式来访问,很多常见的网站都不支持,如下几个知名站点都是无法通过绝对域名的方式访问,这是为什么呢?

1
2
https://www.baidu.com./
https://twitter.com./

资源记录

RFC 1034 S3.6」中介绍了RRs(Resource Records)的概念,在域名命名空间的树状结构中,每一个域名节点都有一组资源信息,有可能是空。这一组资源信息内每个资源都对应一个特殊名字,将特定名字和其关联的资源信息称之为一条RR,所以每个节点都包含一个RRs集合。

下面是我的walkerdu.com在腾讯云上配置的RRs列表:

RFC1035」RFC中定义了一个Resource Record包含以下结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
                                1  1  1  1  1  1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| |
/ /
/ NAME /
| |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| TYPE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| CLASS |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| TTL |
| |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| RDLENGTH |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
/ RDATA /
/ /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  • NAME:此资源记录对应的域名树状结构中的该Node的Name;
  • TYPE:记录类型,2字节的编码值,表示Resource Recode所关联的资源的类型,包含的具体类型后面会介绍。
  • CLASS:记录分类,2字节的编码值,表示Resource Recode所关联的资源所属的类别,后面会介绍。
  • TTL:记录过期时间,4字节长度,表示Resource Recode在再次查询信息源之前可以被缓存的时间间隔。零值表示该RR只能用于当前事务,并且不应被缓存。例如,SOA记录始终使用零TTL进行分发以禁止缓存。零值也可用于非常易变的数据。
  • RDLENGTH:RDATA字段的长度,2字节长度。
  • RDATA:此资源记录的值,是一个长度可变的字符串,值的格式取决于此记录的TYPE。该字符串最大64KB。

下面是BIND关于RR的结构的定义(BTW:按道理应该有Domain Name的,为啥没有呢?)。

1
2
3
4
5
6
7
8
struct dns_rdata {
unsigned char *data;
unsigned int length;
dns_rdataclass_t rdclass;
dns_rdatatype_t type;
unsigned int flags;
ISC_LINK(dns_rdata_t) link;
};

RR的TYPE

RFC1035」对于Resource Record所支持的TYPE的分类的定义,但是目前最全的RR的TYPE列表,全部有IANA的在线DNS参数列表维护,如下是一些比较重要的记录TYPE:

记录类型 详细说明
A 1 A 记录是最常用类型,表示主机地址,将域名指向一个 IPv4 地址,如 8.8.8.8
NS 2 指定该域名的权限域名服务器的域名名字
CNAME 5 域名别名,将此域名指向另一个域名地址,与其保持相同解析
SOA 6 起始授权记录,包含有关区域的管理信息,特别是关于区域传输的信息
NULL 10 空记录
WKS 11 该域名支持的已知服务描述
PTR 12 反向解析记录,用于将IP地址解析为域名
HINFO 13 对应的主机本身的物理信息,RFC1010列出了主机信息
MINFO 14 邮箱和邮件列表信息
MX 15 指定邮件服务器的优先级和域名
TXT 16 对域名进行标识和说明,可以存储任意文本信息,可用于:验证域名所有权,做 SPF 记录(反垃圾邮件)等
AAAA 28 和A记录一样表示主机地址,不过指向的IPV6地址

RR的CLASS

RFC1035」对于Resource Record所支持的CLASS的分类的定义,但是目前最全的RR的CLASS列表,全部有IANA的在线DNS参数列表维护,如下常见的CLASS列表:

CLASS类型 说明
IN 1 最常用的类别,用于互联网上的资源记录。大多数DNS记录都属于IN类别,包括A记录、CNAME记录、MX记录等
CH 3 用于一些特殊目的的查询和记录,通常用于诊断和测试

获取域名对应RR

如何获取域名对应的RR的所有集合?可以使用DNS查询工具,如nslookupdig,来获取域名的资源记录(RR)集合。dig工具相对能够提供详细的输出信息,所以我们使用dig来进行域名相关的查询操作。

下面是dig命令的获取域名对应的RR集合的结果(先忽略dig工具的工作原理,后面还会详细介绍DNS通信协议和DNS系统):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
$ dig any walkerdu.com

; <<>> DiG 9.10.6 <<>> any walkerdu.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 44223
;; flags: qr rd ra; QUERY: 1, ANSWER: 6, AUTHORITY: 0, ADDITIONAL: 9

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4000
;; QUESTION SECTION:
;walkerdu.com. IN ANY

;; ANSWER SECTION:
walkerdu.com. 600 IN A 119.28.90.122
walkerdu.com. 600 IN A 43.153.61.41
walkerdu.com. 86400 IN NS f1g1ns1.dnspod.net.
walkerdu.com. 86400 IN NS f1g1ns2.dnspod.net.
walkerdu.com. 180 IN SOA f1g1ns1.dnspod.net. freednsadmin.dnspod.com. 1689244376 3600 180 1209600 180
walkerdu.com. 600 IN TXT "2017070408514434ciyclxlqemqo7yifzpqt9p33rkt1ss4arkowtnwfyckqgs8c"

;; ADDITIONAL SECTION:
f1g1ns1.dnspod.net. 55045 IN A 157.148.62.101
f1g1ns1.dnspod.net. 55045 IN A 36.155.149.233
f1g1ns1.dnspod.net. 55045 IN A 112.80.181.45
f1g1ns1.dnspod.net. 55045 IN A 117.89.178.173
f1g1ns1.dnspod.net. 55045 IN A 120.241.130.22
f1g1ns1.dnspod.net. 55045 IN A 129.211.176.187
f1g1ns1.dnspod.net. 55045 IN A 1.12.0.4
f1g1ns1.dnspod.net. 55045 IN A 183.47.126.178
f1g1ns1.dnspod.net. 3600 IN AAAA 2402:4e00:1430:15b:0:983e:2763:70a1
f1g1ns2.dnspod.net. 86400 IN A 117.89.178.184
f1g1ns2.dnspod.net. 86400 IN A 120.241.130.98
f1g1ns2.dnspod.net. 86400 IN A 129.211.176.239
f1g1ns2.dnspod.net. 86400 IN A 157.148.62.126
f1g1ns2.dnspod.net. 86400 IN A 1.12.0.1
f1g1ns2.dnspod.net. 86400 IN A 36.155.149.176
f1g1ns2.dnspod.net. 86400 IN A 112.80.181.111
f1g1ns2.dnspod.net. 86400 IN AAAA 2402:4e00:1020:1264:0:9136:29bc:87f9

;; Query time: 453 msec
;; SERVER: 10.85.61.21#53(10.85.61.21)
;; WHEN: Sat Jul 22 17:53:14 CST 2023
;; MSG SIZE rcvd: 524

我们先关注一下dig查询的返回结果的;; ANSWER SECTION:部分,里面返回了几条资源记录:

  • 两条A记录:是我给walkerdu.com配置的解析的host的地址,如下截图。
  • 一条TXT记录:这个TXT记录是域名服务商在我注册域名的时候填写的,前面部分看上去是一个创建时间,后面一串编码串,不知道是个啥?
  • 一条SOA记录和两条NS记录:这些是用来管理本地域名解析的,后面会介绍,不过这三条记录,在我们的配置记录里面并没有。

请注意,由于安全原因,许多DNS服务器不再支持“ANY”查询类型。因此,您可能需要针对每种类型的记录执行单独的查询。

下面我们调整一下资源记录:

  1. 删除walkerdu.com的所有A记录;
  2. 然后添加www.walkerdu.com的A记录,指向对应的host;
  3. 然后添加walkerdu.com的CNAME记录,指向www.walkerdu.com

我们再通过dig看一下walkerdu.com的资源记录列表:

1
2
3
4
5
6
;; ANSWER SECTION:
walkerdu.com. 600 IN CNAME www.walkerdu.com.
walkerdu.com. 600 IN TXT "2017070408514434ciyclxlqemqo7yifzpqt9p33rkt1ss4arkowtnwfyckqgs8c"
walkerdu.com. 86400 IN NS f1g1ns2.dnspod.net.
walkerdu.com. 86400 IN NS f1g1ns1.dnspod.net.
walkerdu.com. 180 IN SOA f1g1ns1.dnspod.net. freednsadmin.dnspod.com. 1689390763 3600 180 1209600 180

域名协议

RFC1035 S4」Section4介绍了DNS系统中通信协议的格式标准,如下,消息的结构分为5个部分:

1
2
3
4
5
6
7
8
9
10
11
+---------------------+
| Header |
+---------------------+
| Question | the question for the name server
+---------------------+
| Answer | RRs answering the question
+---------------------+
| Authority | RRs pointing toward an authority
+---------------------+
| Additional | RRs holding additional information
+---------------------+
  • Header:消息头,包含了后面消息体哪些部分是存在的,以及指定该消息是查询还是响应,查询是一个标准查询还是其他操作。
  • Question:请求字段,是一个数组,每个组包含三个字段,分别为:查询类型QTYPE,查询类别QCLASS,查询域名QNAME,数组长度为Header中QDCOUNT的数值。

后面三个部分具有相同的格式:都是RRs资源记录列表。

  • Answer:对应查询返回的RRs;
  • Authority:查询过程使用的权威名称服务器的RRs;
  • Additional:与查询相关但不严格属于问题答案的RRs;

Header段

域名协议消息中的Header部分的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
                                1  1  1  1  1  1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ID |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR| Opcode |AA|TC|RD|RA| Z | RCODE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QDCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ANCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| NSCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ARCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  • ID:查询程序分配的一个ID序号,2字节长,该字段在对应的查询回复中,必须带回。
  • QR:长度为1bit,0表示是查询消息,1表示响应消息。
  • Opcode:用来表示不同的查询消息类型,在响应消息中也必须带回,长度为4bit,「RFC1035」中有效的Opcode过时了,目前最新的有效值在IANA 的DNS OpCodes实时维护刷新,如下是一些常见的Opcode列表:
OpCode Name Reference
0 Query:标准查询 [RFC1035]
1 IQuery (反向查询,已废弃 ) [RFC3425]
2 Status(DNS服务器状态查询) [RFC1035]
3 Unassigned
4 Notify [RFC1996]
5 Update [RFC2136]
6 DNS Stateful Operations (DSO) [RFC8490]
7-15 Unassigned
  • AA:Authoritative Answer权威服务器回答,在响应中被设置。当 AA 位被设置为 1 时,表示 DNS 响应是一个权威答案。这意味着响应由一个权威的 DNS 服务器提供,该服务器负责管理查询的域名区域。权威答案是可信的,并且应该被客户端接受。当 AA 位被设置为 0 时,表示 DNS 响应不是一个权威答案。这意味着响应可能来自一个缓存服务器或者其他非权威的服务器。对于非权威答案,客户端可能需要进一步查询其他服务器以获取权威的答案。
  • TC:TrunCation,在响应中被设置,当 TC 位被设置为 1 时,表示 DNS 响应被截断。这意味着响应的大小超过了允许的最大长度,导致响应被截断为较短的长度。通常,这种情况发生在响应的大小超过传输协议(如 UDP)的最大限制时。这样的响应通常会提示客户端使用 TCP 进行重新查询。
  • RD:Recursion Desired,用于指示 DNS 查询是否要求递归解析,在消息查询中被设置,响应中带回。当 RD 位被设置为 1 时,表示 DNS 查询要求递归解析。递归解析是指 DNS 服务器在收到查询后,如果自己不具备查询所需的答案,会向其他 DNS 服务器继续发起查询,直到找到答案并返回给客户端。当 RD 位被设置为 0 时,表示 DNS 查询不要求递归解析。这意味着 DNS 服务器只能返回自己所持有的数据,如果没有所需的答案,将返回一个指示性的响应。客户端需要自行处理或发起新的查询以获取完整的答案。大多数 DNS 查询中,RD 位被设置为 1,以启用递归解析。
  • RA:Recursion Available,表示DNS服务器是否支持递归解析,在消息响应中被设置。
  • Z:保留字段,所有消息中必须置为0。
  • RCODE:Response code,响应码,4bit长度,表示响应消息的状态码。IANA 的DNS RCODES列出了最新的全量的RCODE列表。「RFC2671: Extension Mechanisms for DNS (EDNS0)」对RCODE的长度进行了扩展,目前RCODE长度为2字节,下面只列出「RFC1035」中有最开始定义的一些基础的RCODE:
RCODE Name Description Reference
0 NoError No Error [RFC1035]
1 FormErr Format Error,DNS无法解析该请求 [RFC1035]
2 ServFail Server Failure,DNS在处理该请求时出错 [RFC1035]
3 NXDomain Non-Existent Domain,权限域名服务器返回该查询的域名不存在 [RFC1035]
4 NotImp Not Implemented,DNS不支持的查询类型 [RFC1035]
5 Refused Query Refused,DNS由于政策原因,拒绝提供操作 [RFC1035]
  • QDCOUNT:消息查询部分的记录数,16bits,最多支持65535条记录的批量查询。通常会将一个或多个查询记录包含在查询部分中。每个查询记录描述了一个特定的查询,包括查询的域名、查询类型和查询类别。
  • ANCOUNT:消息中回答部分的记录数;
  • NSCOUNT:消息中的权限域名的记录数,用于指示哪些服务器负责管理特定域名的资源记录。授权回答部分的记录提供了查询的权威来源。
  • ARCOUNT:消息中额外信息部分的记录数;

Question段

前面介绍了DNS消息结构中的Header段,紧接着就是消息体的Question段,结构如下:

1
2
3
4
5
6
7
8
9
10
11
                                1  1  1  1  1  1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| |
/ QNAME /
/ /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QTYPE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QCLASS |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

Question段包含了QDCOUNT(Header头指定)个三元组,三元组包含的字段为QNAME,QTYPE和QCLASS。

  • QNAME:要查询的域名;
  • QTYPE:要查询的RR的TYPE类型,长度为2字节;
  • QCLASS:要查询的RR的CLASS,长度为2字节;

Question段在域名查询请求的响应中也会原样的带回来,以便做校验。

Answer/Authority/Additional段

前面说了,域名消息的这三个段都是RRs资源记录列表,具体的格式在「资源记录」一节有详细介绍,这里再列出来一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
                                1  1  1  1  1  1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| |
/ /
/ NAME /
| |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| TYPE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| CLASS |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| TTL |
| |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| RDLENGTH |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
/ RDATA /
/ /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

对于这三个段的NAME部分,大部分情况都是和Question段的QNAME都是同一个,所以为了减小消息的大小,DNS采用了一种NAME压缩方案,该方案消除了消息中NAME的重复。在这个方案中,整个NAME或者一个NAME末尾的标签列表会被替换成指向先前出现过的相同名称的指针。具体设计是NAME字段编码如下:

1
2
3
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 1 1| OFFSET |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

如果长度部分以len & 0xC0标记开始,就表示NAME活着标签出现重复,后面14bits部分,指向前面的完整NAME的offset,为什么这里是以高位11来标记呢?这就要回到最开始域名标签的定义了:每个label长度不超过63个字符,NAME中表示每个label长度的一个字节,最高位不可能是11开头的。

EDNS0

前面在介绍Header段的时候,我们知道由于标准协议设计过时的问题,Opcode的长度已经从4bit扩展为2字节。所以,为了解决标准协议中一些固定字段范围即将耗尽,以及客户端无法像DNS服务器透传一些字段,在「RFC2671: Extension Mechanisms for DNS (EDNS0)」中第一次对域名交互的协议进行了一些扩展性设计,⚠️「RFC2671」以及被废弃,最新的EDNS0已经被「RFC6891: Extension Mechanisms for DNS (EDNS0)」刷新。

该扩展性的设计是通过在Additional段添加格式化的结构来进行协议的扩展。上一节说了Answer/Authority/Additional这三个段的结构都是RR的标准结构定义。所以EDNS0针对扩展协议,在RR的结构基础上进行了特殊标记,如下:

1
2
3
4
5
6
7
8
9
10
+------------+--------------+------------------------------+
| Field Name | Field Type | Description |
+------------+--------------+------------------------------+
| NAME | domain name | MUST be 0 (root domain) |
| TYPE | u_int16_t | OPT (41) |
| CLASS | u_int16_t | requestor's UDP payload size |
| TTL | u_int32_t | extended RCODE and flags |
| RDLEN | u_int16_t | length of all RDATA |
| RDATA | octet stream | {attribute,value} pairs |
+------------+--------------+------------------------------+
  • 针对扩展的RR记录,RR TYPE为「OPT」,值为41

  • 扩展的RR记录中,TTL作为扩展的RCODE和flags使用,结构如下:

1
2
3
4
5
6
					+0 (MSB)                            +1 (LSB)
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
0: | EXTENDED-RCODE | VERSION |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
2: | DO| Z |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
  • 最后就是RR记录中的RDATA字段,RDATA内部是由多个{attribute,value}对组成的结构,对应的就是多个Option 这样扩展性就很好了,每个键值对的结构如下:
1
2
3
4
5
6
7
8
9
10
              +0 (MSB)                            +1 (LSB)
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
0: | OPTION-CODE |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
2: | OPTION-LENGTH |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
4: | |
/ OPTION-DATA /
/ /
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+

其中attribute部分,即OPTION-CODE由IANA分配,具体值可参考IANA DNS EDNS0 Option Codes。后面DATA部分就是根据不同的Option Code,有不同的定义,具体由各个扩展的RFC来进行定义。例如OPTION-CODE=8的edns-client-subnet,用来进行全球DNS加速(DNS智能解析)的,在「Client subnet in DNS requests:edns-client-subnet」最早提出,目前最新Subnet RFC为「RFC7871」,里面定义了对于ENDS0扩展部分的RDATA数据的结构定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
             +0 (MSB)                            +1 (LSB)
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
0: | OPTION-CODE |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
2: | OPTION-LENGTH |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
4: | FAMILY |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
6: | SOURCE NETMASK | SCOPE NETMASK |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
7: | ADDRESS... /
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+

域名服务器

前面介绍完了域名相关基础之后,我们回到正题,我们的DNS(域名服务器)是如何实现域名解析的。

RFC 1034」里面介绍了一下DNS的作用和设计:Name Server是由域名数据库信息组成的仓库,也就是最开始说的,一个分布式数据库。该数据库被划分为多个称之为:Zone区域的概念,每个Zone分布在不同的Name Server。Name Server可以具有多个可选功能和源数据,但其基本任务是使用其区域中的数据来回答查询请求

为了保证每个Zone提供高可用的解析服务,一个Zone需要分布在多个Name Server中,提供容灾的同时,还可以处理更多的查询请求。根据行政命令,要求每个Zone至少在两台服务器上可用,并且许多Zone具有更高的冗余性。

一个给定的Domain Server通常会支持一个或多个Zone,但这只能提供关于整个Domain Tree的一小部分的权威信息。它还可能对Domain Tree的其他部分有一些缓存的非权威数据。Domain Server会标记其对查询的响应,以便请求者可以判断响应是来自权威数据还是非权威数据。

Zone

前面说了,DNS的分布式数据库在逻辑上,由多个Zone组成。那Zone到底是一个什么概念呢?

我们知道,DNS的域名空间是一个分层的树状结构,DNS的Zone就是起始于该树中一个Node(或者称之域),并且还可向下扩展到其子域中,每个连接起来的名称空间群体就成为一个独立Zone。Zone被认为对连接区域中所有名称具有授权性质。

这些规则意味着每个Zone至少有一个节点,特定Zone中所有节点都是相互连接的,并且给定树结构下,每个Zone能够找到比该Zone内其他任何节点更接近根的最高节点。通常使用该节点的名称来标识此Zone。

虽然将名称空间分区,使每个域名位于单独的Zone或所有节点位于单一Zone是可能的,但并不特别有用。相反,数据库在特定组织希望接管子树的地方进行分区,一旦一个组织控制了自己的Zone,它可以单方面地更改Zone中的数据,增加与该Zone连接的新树节段,删除现有节点或委派新的子区域到其所属区域下。如果组织有子结构,可能希望进行进一步的内部分区,以实现名称空间控制的嵌套委派。在某些情况下,这些划分仅是为了使数据库维护更加方便。

如下是Domain Name Space的树状拓扑结构通过Zone进行划分的简要示意图:主要分为几个部分:

  • Root Zone,由IANA负责维护和管理所有TLD域名。
  • TLDs Zone,由IANA分配的运营商负责管理,例如.com Zone是交由VeriSign负责维护和管理, .cn Zone是交友中国互联网信息中心负责维护。

Zone file

Zone的结构有四个主要部分组成:

  • Zone内所有节点的权威数据。
  • Zone内顶级节点的数据(可以看作是权威数据的一部分)。
  • Delegated Sub Zone的描述数据。
  • Sub Zone的Name Server的数据。

上面这些数据都以RR的形式表示,因此一个Zone可以完全通过一组RR集来描述,此描述组成的文本文件通常称之为Zone file。所有Zone可以通过在Name Server之间传输RRs来传递,或者可以通过一系列消息或通过FTP传输主文件。如下是引用维基「Domain Name Space」按照Zone划分的结构示意图:

Root Zone的Root Zone file是由IANA维护且公开的,在「IANA官网」可以查看,下面我们来看一下Root Zone file的部分内容,对我们理解DNS会比较有帮助。

下面是Root Zone file的开始部分,其中第一条RR如下:

1
.			86400	IN	SOA	a.root-servers.net. nstld.verisign-grs.com. 2023072500 1800 900 604800 86400

第一个RR是一个SOA记录:起始授权记录,Zone file必须从SOA记录开始,该记录包含了Zone的重要管理信息,SOA记录RDATA部分格式解析如下:

  • a.root-servers.net.:Root Zone的主要DNS。
  • nstld.verisign-grs.com.:负责Root Zone的管理员的电子邮箱,第一个.替换为@,即nstld@verisign-grs.com.,不用@直接表示邮箱的原因是,RR中@表示当前区域的Domain Name
  • 2023072500:序列号,用于指示区域文件的版本。
  • 1800:刷新时间,表示辅助服务器应在多长时间后向主服务器请求 SOA 记录以查看是否已更新。
  • 900:重试时间,表示服务器应在多长时间后再次向无响应的主域名服务器请求更新。
  • 604800:过期时间,表示如果辅助服务器在此时间内未收到主服务器的响应,则应停止对该区域的查询响应。
  • 86400:最小 TTL,表示区域中所有资源记录的最小 TTL。

SOA的格式也可以参考「RFC1034 Section6.1」关于C.ISI.EDU DNS的Zone file的配置说明。

SOA记录后面接着的RR记录为:

1
2
3
4
5
6
7
8
9
10
11
12
13
.			518400	IN	NS	a.root-servers.net.
. 518400 IN NS b.root-servers.net.
. 518400 IN NS c.root-servers.net.
. 518400 IN NS d.root-servers.net.
. 518400 IN NS e.root-servers.net.
. 518400 IN NS f.root-servers.net.
. 518400 IN NS g.root-servers.net.
. 518400 IN NS h.root-servers.net.
. 518400 IN NS i.root-servers.net.
. 518400 IN NS j.root-servers.net.
. 518400 IN NS k.root-servers.net.
. 518400 IN NS l.root-servers.net.
. 518400 IN NS m.root-servers.net.

这里一共有13条NS类型的记录,这个就是Root Zone的13台DNS的地址。我们在Root Zone file的后面可以看到这些根DNS对应的A记录,如下:

1
2
3
4
5
6
7
8
9
10
11
a.root-servers.net.	518400	IN	A	198.41.0.4
a.root-servers.net. 518400 IN AAAA 2001:503:ba3e:0:0:0:2:30
b.root-servers.net. 518400 IN A 199.9.14.201
b.root-servers.net. 518400 IN AAAA 2001:500:200:0:0:0:0:b
c.root-servers.net. 518400 IN A 192.33.4.12
c.root-servers.net. 518400 IN AAAA 2001:500:2:0:0:0:0:c
...
l.root-servers.net. 518400 IN A 199.7.83.42
l.root-servers.net. 518400 IN AAAA 2001:500:9f:0:0:0:0:42
m.root-servers.net. 518400 IN A 202.12.27.33
m.root-servers.net. 518400 IN AAAA 2001:dc3:0:0:0:0:0:35

会不会好奇,为什么每个根DNS只配置了一个A记录呢?其实,每个根 DNS 都有很多台分布在世界各地,同一个根DNS的所有服务器可以共享相同的 IP 地址,因为它们使用了一种称为Anycast 的技术。Anycast 路由可以将请求基于负载和接近度分发到不同的服务器。这意味着,尽管只有 13 个根服务器 IP 地址,但可以通过平行部署根DNS物理机来保证查询的速度。

另外,看到Zone file里面为什么还需要配置NS记录对应域名的A记录呢,不应该是交由对应的NS负责解析吗?其实这里涉及到DNS解析Circular dependencies「循环依赖」的问题,例如,root name配置的NS记录值为a.root-servers.net.,如果要按照标准解析流程,最终又会需要跑到根域名服务器解析root domain的地址,这就形成了一个死循环,所以这里为了解决这个问题,需要配置NS对应的A记录值。

下面我们看看Root Zone file中com.这个TLD对应的RR记录,如下,也配置了13个NS的记录,对应的域名是由VeriSign负责管理和维护的。

1
2
3
4
5
6
7
8
9
10
11
12
13
com.			172800	IN	NS	a.gtld-servers.net.
com. 172800 IN NS b.gtld-servers.net.
com. 172800 IN NS c.gtld-servers.net.
com. 172800 IN NS d.gtld-servers.net.
com. 172800 IN NS e.gtld-servers.net.
com. 172800 IN NS f.gtld-servers.net.
com. 172800 IN NS g.gtld-servers.net.
com. 172800 IN NS h.gtld-servers.net.
com. 172800 IN NS i.gtld-servers.net.
com. 172800 IN NS j.gtld-servers.net.
com. 172800 IN NS k.gtld-servers.net.
com. 172800 IN NS l.gtld-servers.net.
com. 172800 IN NS m.gtld-servers.net.

cn.这个ccTLD对应的RR记录如下,所有cn.结尾的域名都是由中国互联网络信息中心 (CNNIC)负责解析管理的。

1
2
3
4
5
6
cn.			172800	IN	NS	a.dns.cn.
cn. 172800 IN NS b.dns.cn.
cn. 172800 IN NS c.dns.cn.
cn. 172800 IN NS d.dns.cn.
cn. 172800 IN NS e.dns.cn.
cn. 172800 IN NS ns.cernet.net.

非根区域(Non-root Zone)的Zone file通常不是公开的。Zone文件的访问权限由域名所有者或管理员控制。他们可以选择将Zone file保持私有,只允许特定的DNS服务器或授权的用户访问。这可以通过配置DNS服务器的访问控制列表(ACL)或使用其他安全措施来实现。

对于由VeriSign负责管理的.com.net的gTLD,虽然没有公开Zone file,但是Verisign在其官网上公布了这两个TLD的二级域名的总数量。Verisign在官网中说明,该数据每天至少更新一次。截止2023-07-26,.com的结尾的子域名已经注册了1.6亿了,这么多。

Name Server的分类

介绍完Zone和Zone file的概念后,我们知道整个DNS系统是按照Zone进行逻辑划分和管理的,按照Zone的层级,可以将DNS服务器分为4大类:

  • 根域名服务器:负责所有TLD域名的解析,前面介绍Zone file的时候,分析了由IANA维护的root zone file的内容,以及采用Anycast技术实现的Root Zone的13个DNS地址的几百台Server,以支撑整个全球DNS系统的正常运行。
  • 顶级域名服务器:由各个TLD的运营商或者组织负责维护,主要负责该TLD域下的所有二级域名的解析。由于TLD的Zone file不对外公开,所以我们没有办法看到TLD Zone的所有数据,但是上面介绍Zone file的时候,Verisign在其官网上公布了它负责管理的.com.net的gTLD的二级域名的总数量。
  • 权威域名服务器(Authoritative name server):其实在RFC中并没有找到其定义,「Wikipedia」关于Authoritative name server的定义是:Name Server只从本地配置的Zone file获取数据进行域名查询请求的响应。相对应的是,Name Server提供查询的响应数据是从其他Name Server的Zone file的查询获得的Cache数据。为什么会有权威这个名字呢?因为一个Zone的Zone file包含的完整数据,被称之为权威数据。在之前介绍RR的时候,我们知道有一个NS记录,用来标志该域名对应的权威域名服务器,每个Zone都有一组权威域名服务器,配置在父Zone的Zone file中,通过RR类型为NS的记录来标记。
  • 本地域名服务器(Local name server):也称为本地递归域名服务器或本地缓存服务器,是位于本地网络或互联网服务提供商(ISP)网络中的DNS服务器。它是一个中间层的DNS服务器,负责处理终端用户计算机或设备发起的DNS查询请求。用户的所有请求都是由本地域名服务器负责查找和响应的。

本地域名服务器处于较近的网络位置,它能够更快地响应DNS查询请求,减少网络延迟,提高解析速度。同时,通过在本地缓存解析结果,重复查询相同的域名可以直接返回缓存的结果,避免重复向外部DNS服务器发起相同的查询请求,减少对外部DNS服务器的负担。

关于上面四大类的Name Server如何协调工作的,在后面「DNS工作原理」一节中会详细介绍;

DNS 工作原理

在文章最开始的时候,介绍了「RFC1034 S2.4」对于DNS主要有三个部分组成:

  1. 域名空间(Domain Name Space)和资源记录(Resource Records)
  2. 名字服务器(Name Servers)
  3. 解析器(Resolvers)

DNS的这个设计结构将用户接口、故障恢复和分发等问题隔离在Resolver中,并将数据库更新和刷新问题隔离在Name Server中。

为了提高性能,实现可以将这些功能耦合在一起。例如,通常Name Server所在的服务器上,还会存在一个Resolver,目的是为了能够查询由其他Name Servers管理的域名,并将其RR数据进行缓存,以提高DNS整个系统的查询效率。

在经过前面Zone&Zone file的介绍后,我们知道Name Servers持有对应域名子树(Zone)的完整数据:Zone file,并负责响应Resolvers的域名查询请求。其实Name Servers管理的数据有两部分:

  • 第一部分:该Zone的完整数据,即Zone file:域子树上所有Name及其RRs数据;此数据也被称之为:权威数据;Name Server本身会周期性的保证其Zone file的刷新。前面介绍了Root Zone file也介绍了其SOA记录里面描述其Zone file数据的刷新和过期时间的配置。
  • 第二部分:是由Name Server自己的Resolver向其他NS获取的缓存数据。这些数据可能不完整,但在重复访问非本地数据(非本Zone file的数据)时可以提高检索过程的性能。缓存数据最终会被超时机制丢弃。

RFC1035 S2.2」列出了一个简单的,最典型的DNS的结构示意图,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
             Local Host                        |  Foreign
|
+---------+ +----------+ | +--------+
| | user queries | |queries | | |
| User |-------------->| |---------|->|Foreign |
| Program | | Resolver | | | Name |
| |<--------------| |<--------|--| Server |
| | user responses| |responses| | |
+---------+ +----------+ | +--------+
| A |
cache additions | | references |
V | |
+----------+ |
| cache | |
+----------+ |

上面这个Local Host表示用户的本地,用户程序向Resolver,一般是操作系统提供的API,例如Linux内glibc的getaddrinfo() or gethostbyname2()函数,发起域名解析的请求,该Resolver负责通过标准的DNS协议向外部的Name Servers发起域名查询,并解析结果返回给用户程序。Resovler可能会使用本地Cache进行查询,以加速请求。

如下是我的机器上采用ltrace跟踪ping在执行ping之前执行域名解析的过程中涉及的glibc库调用和系统调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ ltrace -S -s 1024 ping github.com
...
142 inet_aton("github.com", { 0 }) = 0
...
144 gethostbyname2(0x7f42c67930a0, 2, 0xffffffff, 0x7f42c6793000 <unfinished ...>
...
146 open@SYS("/etc/resolv.conf", 524288, 0666) = 4
...
149 read@SYS(4, "; generated by /usr/sbin/dhclient-script\nnameserver 10.85.61.21\nnameserver 10.123.119.98\nnameserver 10.123.120.11 0\n", 4096) = 115
...
162 socket@SYS(1, 0x80801, 0, 3) = 4
163 connect@SYS(4, 0x7fff7bc8c860, 110, 3) = 0
164 sendto@SYS(4, 0x7fff7bc8c800, 23, 0x4000) = 23
165 poll@SYS(0x7fff7bc8c920, 1, 5000) = 1
166 read@SYS(4, "\002", 32) = 32
167 readv@SYS(4, 0x7fff7bc8ca20, 2) = 15
168 close@SYS(4) = 0
169 <... gethostbyname2 resumed> ) = 0x7f42c5c9fe20
...
200 inet_ntoa({ 0xa6f3cd14 }) = "20.205.243.166"

可以看到ping命令拿到域名后,会调用库函数gethostbyname2(),交由其执行google.com的解析,gethostbyname2()内部会根据/etc/resolv.conf配置的nameserver的信息通过UDP发送DNS查询请求,最终获取到google.com对应的主机IP:20.205.243.166

下面是「RFC1035 S2.2」提供的从Name Server的角度,一个简单的DNS的交互流程图:Name Server从本机的Master files(等同于 Zone files)中读取该Zone内的RR信息,返回给外部的Resolver的查询。

1
2
3
4
5
6
7
8
9
10
11
             Local Host                        |  Foreign
|
+---------+ |
/ /| |
+---------+ | +----------+ | +--------+
| | | | |responses| | |
| | | | Name |---------|->|Foreign |
| Master |-------------->| Server | | |Resolver|
| files | | | |<--------|--| |
| |/ | | queries | +--------+
+---------+ +----------+ |

DNS要求所有Zone都必须有多个名称服务器进行冗余提供服务。所以,该Zone内指定的辅助Name Server可以定时使用DNS的区域传输协议从主Name Server获取Zone file并检查更新。以下是该配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
             Local Host                        |  Foreign
|
+---------+ |
/ /| |
+---------+ | +----------+ | +--------+
| | | | |responses| | |
| | | | Name |---------|->|Foreign |
| Master |-------------->| Server | | |Resolver|
| files | | | |<--------|--| |
| |/ | | queries | +--------+
+---------+ +----------+ |
A |maintenance | +--------+
| +------------|->| |
| queries | |Foreign |
| | | Name |
+------------------|--| Server |
maintenance responses | +--------+

下面是是「RFC1035 S2.2」提供的一个DNS系统完整的信息流示意图,这里Shared database是一个Cache,对于Resolver来说,它缓存的是用户最近DNS查询请求的缓存数据;对于Name Server它缓存了该Zone的权威RR数据,定时的会从主Name Server获取全新的数据,进行刷新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
               Local Host                        |  Foreign
|
+---------+ +----------+ | +--------+
| | user queries | |queries | | |
| User |-------------->| |---------|->|Foreign |
| Program | | Resolver | | | Name |
| |<--------------| |<--------|--| Server |
| | user responses| |responses| | |
+---------+ +----------+ | +--------+
| A |
cache additions | | references |
V | |
+----------+ |
| Shared | |
----------------------------| database |---------|------------
+----------+ |
A | |
+---------+ refreshes | | references |
/ /| | V |
+---------+ | +----------+ | +--------+
| | | | |responses| | |
| | | | Name |---------|->|Foreign |
| Master |-------------->| Server | | |Resolver|
| files | | | |<--------|--| |
| |/ | | queries | +--------+
+---------+ +----------+ |
A |maintenance | +--------+
| +------------|->| |
| queries | |Foreign |
| | | Name |
+------------------|--| Server |
maintenance responses | +--------+

结合上一节「Name Server的分类」,我们将域名解析的流程结合四大类的域名服务器的具体例子「引用wikipedia」来看一下:

上面这个例子,我们假使没有本地域名服务器,域名查询请求只涉及到三大类权威NS:根域名服务器,TLD域名服务器,权威域名服务器。那么我们Resolver会通过iterative的方式进行www.wikipedia.org的域名解析流程:

  • 首先,Resolver向根域名服务器请求.org顶级域名服务器的地址;
  • 然后,Resolver得到根域名服务器的响应:.org域名服务器的IP地址后,继续向.org域名服务器请求wikipedia.org二级权威域名服务器的地址;
  • 然后,Resovler得到.org域名服务器的响应:wikipedia.org二级权威域名服务器的IP地址,最终向wikipedia.org权威域名服务器请求www.wikipedia.org的地址;
  • 最后,Resolver从wikipedia.org二级权威域名服务器获得了www.wikipedia.org的IP地址,任务完成。

这种机制如果每个互联网解析都需要从根服务器开始,将给根服务器带来巨大的流量负担。实际上,在DNS服务器中使用缓存以减轻根服务器的压力,因此,根名称服务器实际上只参与了相对较小比例的请求。其中本地域名服务器起到一个很关键的作用,下面是一个加了本地域名服务器的域名解析图示「引用wikipedia」,如下:

可以看到这里DNS链路上的每层都存在Cache:

  • 应用程序自己的Cache。参考「引用wikipedia」的介绍,Internet Explorer 代表了一个值得注意的例外:默认情况下,IE 3.x 之前的版本会将 DNS 记录缓存 24 小时。Internet Explorer 4.x 及更高版本(最高为 IE 8)将默认超时值减少到半小时,可以通过修改默认配置来更改该值。这种做法可能会在调试 DNS 问题时增加额外的难度,因为它会掩盖此类数据的历史记录。但是按道理RR记录都是有TTL的过期了,还存着?
  • 用户本机的DNS Cache。本机的大部分DNS解析都会从Local Resovler,以递归查询的方式进行域名解析和结果cache,以此来加速域名解析;
  • 企业或ISP提供商的Cache。该层就是很重要的本地域名服务器,支持Resovler向自己发起递归查询,然后此域名服务器通过迭代查询的方式将最终的结果返回给用户,并对查询的DNS记录进行Cache,这很重要,本地域名服务器承载了互联网上大部分的DNS请求。

下面是在DNSPod上面添加域名对应的A记录的提示,虽然实际上并没有延迟那么久,但是还是那个问题:按道理RR记录都是有TTL的过期时间,为什么不按规范,还存着呢?虽然RR记录变更的不难么频繁,Cache久一点确实可以加速访问,但是准确性不需要考虑的吗?

DNS Resolver

前面介绍整个DNS工作流程中,Resolver扮演者Client的角色,它通过前面介绍的域名交互协议和各个域名服务器之间进行交互。在整个域名查询过程中存在各个节点,例如用户本机有常用的Local Resolver,例如NSCD,systemd-resolved等,本地递归域名服务器作为重要节点,也都有自己的Resolver负责向权威域名服务器进行域名请求。

这里需要说明一下Resolver进行域名查询的三种方式,参考「wikipedia」

  • 迭代查询(Iterative Query):Resolver查询www.walkerdu.com时,会向根域名服务器发送一个查询请求,根域名服务器会返回指向顶级域名服务器(TLD,Top-Level Domain)的地址。然后,DNS resolver再向TLD服务器发送查询请求,TLD服务器会返回指向权威域名服务器(Authoritative Name Server)的地址。最后,DNS resolver再向权威域名服务器发送查询请求,并从权威域名服务器获取域名的IP地址。
  • 递归查询(Recursive Query):在递归查询中,DNS resolver向本地域名服务器(通常由互联网服务提供商(ISP)或企业提供)发送查询请求,本地递归域名服务器会负责迭代查询的所有步骤,并将最终的解析结果返回给DNS resolver。这里本地域名服务器通常称为本地递归域名服务器,因为最终域名查询请求是由本地域名服务器通过迭代查询不同层级的域名服务器获得的结果,即本地域名服务器收敛了查询过程,对于Resovler而言,它是递归查询了结果,最终返回给调用方
  • 非递归查询(Non Recursive Query):DNS resolver向其他DNS服务器发送一个查询请求。被查询的DNS服务器会立即返回查询结果,无论它是否拥有所需的域名解析信息。如果所查询的DNS服务器没有直接拥有答案,它会返回一个指向可能包含答案的其他DNS服务器(通常是权威域名服务器)的引用。DNS resolver不会继续向其他服务器发送进一步的查询请求,而是直接接收到查询结果。

RFC1035 S4」Section4中,指出了域名查询协议中的Header字段中的「RD:Recursion Desired」用于指示 DNS 查询是否要求递归解析,而「RA:Recursion Available」字段,表示DNS服务器是否支持递归解析。

大多数情况下,我们使用的DNS resolver会使用递归查询方式,因为递归查询能够提供更好的性能和用户体验。递归查询由本地递归域名服务器(通常由互联网服务提供商(ISP)或企业提供)处理所有的迭代查询步骤,直到获得最终的解析结果,并将结果返回给终端用户的DNS resolver。这样的方式能够减少网络延迟,加快解析速度,并降低对外部DNS服务器的负担。

权威域名服务器通常不会支持递归查询。权威域名服务器是负责管理特定域名区域的服务器。这里包括根域名服务器和顶级域名服务器。因为递归查询会影响这些权威域名服务器的性能,它们是DNS系统的关键路径。

下图是基于「wikipedia」DNS解析流程画的一个示意图,旨在说明Resovler的递归查询和迭代查询在DNS解析过程中是如何进行的,从这个流程图中我也可以得出本地域名服务器为什么也称为本地递归域名服务器,因为他们之间都是进行的递归查询,只有到权威服务器的时候才变成迭代查询。

常见Resolver服务

发行版常见的客户端的DNS Resolver有:NSCD,systems-resolve,dnsmasq

  1. systemd-resolved:这是一个由 systemd 提供的系统级 DNS 解析器,可以在基于 systemd 的发行版(如 Ubuntu、Fedora)中找到。
  2. dnsmasq:这是一个小巧且易于配置的 DNS 解析器和 DHCP 服务器。它在许多发行版中被用作本地解析器(Local resolver)和缓存服务,例如 Raspberry Pi 上的 Raspbian 发行版。
  3. NetworkManager:这是一个常用的网络管理工具,许多发行版使用 NetworkManager 来管理网络连接。NetworkManager 在某些配置中可以用作 DNS 解析器,以供系统中的应用程序使用。
  4. Unbound:这是一个快速、轻量级的递归 DNS 解析器,它在某些发行版中作为默认解析器的选择。Unbound 可以作为本地解析器并提供 DNS 缓存功能。
  5. nscd 是一个在某些 Linux 发行版中常见的守护进程,用于缓存系统的名称解析服务,包括 DNS 解析。它可以显著提高解析性能,减少对外部 DNS 服务器的依赖。nscd 的可用性和使用可能因不同的发行版而异。在一些现代的发行版中,如 Ubuntu 20.04 和 CentOS 8,nscd 已被 systemd-resolved 或其他解析器取代。因此,在特定的系统上,nscd 可能不被使用或被弃用。

前面介绍「DNS工作原理」一节中简单的参数了库函数级别的resovler,例如Linux内glibc的getaddrinfo() or gethostbyname2()函数,发起域名解析的请求,该Resolver负责通过标准的DNS协议向外部的Name Servers发起域名查询,并解析结果返回给用户程序。Resovler可能会使用本地Cache进行查询,以加速请求。

那库函数和本地的Cache服务,例如systemd-resolved之间是如何协作的呢?GPT-4给的解释是:

当一个应用程序通过库函数进行DNS解析时,如果系统正在运行systemd-resolved服务,那么这个服务可能会被用来执行实际的DNS查询。也就是说,库函数可能会将DNS查询请求发送给systemd-resolved,然后由它来与DNS服务器进行通信并获取查询结果。这样可以利用systemd-resolved的缓存功能,提高DNS解析的速度。

这个「可能与否」取决于/etc/nsswitch.conf的配置,后面有时间可以详细研究一下。

DNS智能解析

通过前面的学习,我们知道,域名对应的A记录(AAAA记录)决定了其解析对应的主机的IP地址。每一个域名可以配置多个A记录,按照RFC标准,多A记录在查询的时候是随机返回的。也即:在传统的DNS解析过程中,DNS服务器会根据请求DNS解析的服务器IP地址返回相应的结果,而不是实际发起请求的客户端IP地址。

但是在网络环境比较复杂的地区,例如中国,南北地区,跨运营商之间的访问质量是比较差的,所以为了能够让业务能够提供更好的服务,在2011年9月由OpenDNS和Google联合发起了「The Global Internet Speedup」项目(目前该项目的主站已经关闭,只能从web archive里面看到了),该项目介绍中说:

为了保证用户访问网站快速,我们尽可能地将内容靠近用户。这样可以减少对高带宽内容(如视频)的延迟,并实现全球范围内对互联网容量的更有效利用。

为了达到这个目的,DNS在确保用户访问正确资源方面起着关键作用。通常情况下,热门资源存在于多个地方。这样更多的人可以同时访问内容,并且速度更快。全球互联网加速是我们给那些同意并实施了提议标准以改善全球用户互联网体验的众多领先互联网公司所取的名称,该标准增强了DNS功能,提升了全球用户的网络体验。

在「The Global Internet Speedup」的项目参与公司一栏中,还可以看到:EdgeCast,CDNetworks,BitGravity,Comodo,CloudFlare。

可以认为The Global Internet Speedup是DNS智能解析的具体技术实现,它通过起草了「Client subnet in DNS requests draft-vandergaast-edns-client-subnet-00 」来标准化DNS系统如何通过EDNS的扩展来进行DNS的智能解析。

这里再解释一下什么是智能解析:简单来说,DNS智能解析就是根据用户的地理位置、网络条件、服务器负载等信息,选择最佳的目标主机来响应用户的请求。这种方式可以确保用户能够访问到延迟最低、网络性能最好、负载最轻的目标主机,从而提供更好的用户体验和服务质量。智能解析还可以结合其他策略,如负载均衡、容灾备份等,以提高系统的可靠性和性能。如下是一个简要的DNS智能解析示例:

参考DNSPod 智能解析,其支持解析按多种方式划分线路,主要有:

  • 按照运营商划分;
  • 按照省份+运营商划分;
  • 按照国内大区划分:华北,华东等;
  • 按照国家;

并不是所有的DNS都支持EDNS Client Subnet 扩展的,参考「 Using dig for testing EDNS client-subnet」我们知道,可以通过dig来进行EDNS的功能验证;参考「Public DNS Server List」列出了公共且免费可用的DNS服务器,然后通过dig命令验证:

1
$ dig xxx.yyy.zzz.com @8.8.8.8 +subnet=27.128.190.0/24

得到了一些Public DNS对于Client Subnet的支持,如下:

Provider Primary DNS Server Secondary DNS Server Client Subnet
Google Public DNS 8.8.8.8 8.8.4.4 支持
Cloudflare 1.1.1.1 1.0.0.1 不支持
OpenDNS 208.67.222.222 208.67.220.220 不支持
Level 3 209.244.0.3 209.244.0.4 不支持
Verisign 64.6.64.6 64.6.65.6 支持
Quad9 9.9.9.9 149.112.112.112 不支持
AliDNS 223.5.5.5 223.6.6.6 支持
Baidu Public DNS 180.76.76.76 不支持
DNSPod Public DNS+ 119.29.29.29 119.28.28.28 不支持,网络不通

按照「RFC7871 Client subnet in DNS requests:edns-client-subnet」对Subnet扩展的Cache要求,如下:

If FAMILY, SOURCE PREFIX-LENGTH, and SOURCE PREFIX-LENGTH bits of ADDRESS in the response don’t match the non-zero fields in the corresponding query, the full response MUST be dropped,

即,如果ADDRESS不同,一定不能进行Cache,但是实际测试发现目前在上面三个可用的Public DNS中只有Google Public DNS是可以保证及时清除Cache的,其他的Public DNS根本不能在切换subnet的时候,重新进行解析

回到问题:如何获取域名对应的IP列表

经过上面这么多的学习,我们再回头看这个问题:怎么知道xxx.yyy.zzz.com 对应解析的ip?

标准答案就是没有办法获取

原因一:我们前面说了,虽然IANA公开了root zone file,也就是所有顶级域的NS及其A记录的列表,但是二级域名以及以下的权威域名服务提供商都是不公开其Zone file的,所以我们没有办法获取到原始的Zone file,也就没有办法获取域名配置的所有的A记录。

原因二:因为域名的A记录可以配置很多,然后如果其配置了DNS智能解析,在域名服务商上配置了全球各个地方对应的解析线路。我们基本没有办法获取其全量的IP的。

唯一有的可能就是,全球各地都有主机,然后通过域名请求协议去查询该域名对应的A记录,最终把所有的A记录汇总,可能就是其A记录的全集。有没有这个可能呢?

通过站长工具进行DNS A解析,如下:可以看到站长工具在全国各地都有一些主机部署,但是这就是该域名所有的A记录吗?

我用了一台硅谷的服务器,通过dig测试的结果如下,可见上面站长工具的解析并不全:

1
2
3
4
5
6
7
8
9

$ dig A xxx.yyy.zzz.com
...
;; ANSWER SECTION:
xxx.yyy.zzz.com. 60 IN A 13.225.131.37
xxx.yyy.zzz.com. 60 IN A 13.225.131.23
xxx.yyy.zzz.com. 60 IN A 13.225.131.50
xxx.yyy.zzz.com. 60 IN A 13.225.131.2
...

我们可以看一下这个域名配置的NS记录是啥:

1
2
3
4
5
6
7
8
$ dig NS xxx.yyy.zzz.com
...
;; QUESTION SECTION:
;xxx.yyy.zzz.com. IN NS

;; AUTHORITY SECTION:
we-api.com. 900 IN SOA ns-711.awsdns-24.net. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400
...

这个域名是通过aws来托管的。下面我们通过EDNS Client Subnet选项来传递客户端的subnet,如下使用河北电信的一个subnet测试,可以看到结果和本地直接dig获得的是不同的解析结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ dig xxx.yyy.zzz.com @8.8.8.8 +subnet=27.128.190.0/24

; <<>> DiG 9.18.1-1ubuntu1.2-Ubuntu <<>> xxx.yyy.zzz.com @8.8.8.8 +subnet=27.128.190.0/24
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 16431
;; flags: qr rd ra; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
; CLIENT-SUBNET: 27.128.190.0/24/24
;; QUESTION SECTION:
;xxx.yyy.zzz.com. IN A

;; ANSWER SECTION:
xxx.yyy.zzz.com. 60 IN A 18.66.122.46
xxx.yyy.zzz.com. 60 IN A 18.66.122.78
xxx.yyy.zzz.com. 60 IN A 18.66.122.67
xxx.yyy.zzz.com. 60 IN A 18.66.122.38

;; Query time: 39 msec
;; SERVER: 8.8.8.8#53(8.8.8.8) (UDP)
;; WHEN: Wed Aug 09 15:15:20 CST 2023
;; MSG SIZE rcvd: 121

所以基于EDNS Client Subnet的扩展,针对如何获取一个域名对应的IP列表,我们可以有一个曲线救国的解决方案:用地域和ISP上足够多的subnet,向DNS发起域名解析请求,然后将最终的结果汇总,就可以得到一个最大化的域名对应的A记录列表,只要Client Subnet足够多,就可以覆盖所有的A记录。

为此我写了一个开源项目super-dig,基于EDNS Client Subnet的扩展向支持EDNS的DNS发起域名解析请求,覆盖的Client Subnet包含了国内所有省份的三大运营商,以及国外所有的国家。

执行的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ bin/super-dig xxx.yyy.zzz.com --ns_file=configs/ns.json -f configs/ip_region.json
|------------------------------------------------------------------------------------------------|
|Local Subnet | ISP | Records A |
|------------------------------------------------------------------------------------------------|
|中国 上海 | 电信 | 52.84.251.28 |
| | | 52.84.251.57 |
| | | 52.84.251.65 |
| | | 52.84.251.67 |
|----------------------------------------------------------------| |
|中国 云南省 | 联通 | |
|----------------------------------------------------------------| |
|阿联酋 | 0 | |
|印度尼西亚 | | |
|不丹 | | |
|孟加拉 | | |
|东帝汶 | | |
|南乔治亚岛和南桑威奇群岛 | | |
|图瓦卢 | | |
|新加坡 | | |
|------------------------------------------------------------------------------------------------|
|泰国 | 0 | 65.9.181.101 |

至此,DNS的学习也算告一段落了,这次给我的感受就是,我们平常接触的很多知识,看似很简单,但对其背景,整个底层设计,以及实现,原理,了解的都不够,所以在需要的时候,还是多花一点时间去了解其背后的设计和实现,对于业务设计也是很有帮助的。

参考

https://github.com/gaoyifan/china-operator-ip

http://ip.yqie.com/china.aspx

https://superuser.com/questions/847575/how-do-i-find-all-known-ips-for-a-given-domain-in-linux

https://superuser.com/questions/1299678/how-to-find-all-ip-addresses-and-or-subnets-associated-with-a-fqdn-hostname

https://support.opendns.com/hc/en-us/articles/227987687-Using-dig-for-testing-EDNS-client-subnet

https://support.opendns.com/hc/en-us/articles/227987647-EDNS-Client-Subnet-FAQ

https://support.umbrella.com/hc/en-us/articles/360021857552-Umbrella-and-EDNS-Client-Subnet-ECS-

nscd如何查看cache的域名解析记录
strings /var/db/nscd/hosts

https://unix.stackexchange.com/questions/28553/how-to-read-the-local-dns-cache-contents

systems-resolved如何查看cache的域名解析记录

https://askubuntu.com/questions/1257831/how-can-i-see-the-systemd-resolve-dns-cache

https://jvns.ca/blog/2022/02/23/getaddrinfo-is-kind-of-weird/

https://stackoverflow.com/questions/2157592/how-does-getaddrinfo-do-dns-lookup

https://stackoverflow.com/questions/16741732/using-getaddrinfo-only-checks-nscd-cache-first-time-if-dns-times-out

github.com/bminor/glibc/tree/master/sysdeps/posix/getaddrinfo.c

这里说库函数会每次都重新执行域名解析,而不是有任何缓存:

https://jameshfisher.com/2018/02/03/what-does-getaddrinfo-do/