小菜学网络之 DNS 报文格式

本文转载自微信公众号「小菜学编程」,作者 fasionchan 。转载本文请联系小菜学编程公众号。

经过前面学习,我们知道查询一个域名,需要与 DNS 服务器进行通信。那么,DNS 通信过程大概是怎样的呢?

DNS 是一个典型的 Client-Server 应用,客户端发起域名查询请求,服务端对请求进行应答:

DNS 一般采用 UDP 作为传输层协议 (TCP 亦可),端口号是 53 。请求报文和应答报文均作为数据,搭载在 UDP 数据报中进行传输:

很显然,DNS 请求报文和应答报文均需要满足一定的格式,才能被通信双方所理解。这就是 DNS 协议负责的范畴,它位于传输层之上,属于 应用层 协议。

报文格式
DNS 报文分为 请求 和 应答 两种,结构是类似的,大致分为五部分:

头部 (header),描述报文类型,以及其下 4 个小节的情况;
问题节 (question),保存查询问题;
答案节 (answer),保存问题答案,也就是查询结果;
授权信息节 (authority),保存授权信息;
附加信息节 (additional),保存附加信息;
也有不少文献将 DNS 请求称为 DNS 查询 (query),两者是一个意思。

其中,头部是固定的,共 12 字节; 其他节不固定,记录数可多可少,数目保存在头部中。头部分为 6 个字段:

标识 (identifier),一个 16 位的 ID ,在应答中原样返回,以此匹配请求和应答;
标志 (flags),一些标志位,共 16 位;
问题记录数 (question count),一个 16 位整数,表示问题节中的记录个数;
答案记录数 (answer count),一个 16 位整数,表示答案节中的记录个数;
授权信息记录数 (authority record count),一个 16 位整数,表示授权信息节中的记录个数;
附加信息记录数 (additional record count),一个 16 位整数,表示附加信息节中的记录个数;
最后,我们来解释一下标志字段中的各个标志位:

QR 位标记报文是一个查询请求,还是查询应答;
0 表示查询请求;
1 表示查询应答;
操作码 (opcode) 占 4 位,表示操作类型:
0 代表标准查询;
1 代表反向查询;
2 代表服务器状态请求;
AA 位表示 权威回答 (authoritative answer),意味着当前查询结果是由域名的权威服务器给出的;
TC 位表示 截短 (truncated),使用 UDP 时,如果应答超过 512 字节,只返回前 512 个字节;
RD 位表示 期望递归 (recursion desired),在请求中设置,并在应答中返回;
该位为 1 时,服务器必须处理这个请求:如果服务器没有授权回答,它必须替客户端请求其他 DNS 服务器,这也是所谓的 递归查询 ;
该位为 0 时,如果服务器没有授权回答,它就返回一个能够处理该查询的服务器列表给客户端,由客户端自己进行 迭代查询 ;
RA 位表示可递归 (recursion available),如果服务器支持递归查询,就会在应答中设置该位,以告知客户端;
保留位,这 3 位目前未用,留作未来扩展;
响应码 (response code) 占 4 位,表示请求结果,常见的值包括:
0 表示没有差错;
3 表示名字差错,该差错由权威服务器返回,表示待查询的域名不存在;
问题记录
客户端查询域名时,需要向服务端发送请求报文; 待查询域名作为问题记录,保存在问题节中。

问题节支持保存多条问题记录,记录条数则保存在 DNS 头部中的问题记录数字段。这意味着,DNS 协议单个请求能够同时查询多个域名,虽然通常只查询一个。

一个问题记录由 3 个字段组成:

待查询域名 (Name),这个字段长度不固定,由具体域名决定;
查询类型 (Type),域名除了关联 IP 地址,还可以关联其他信息,常见类型包括 (下节详细介绍):
1 表示 A 记录,即 IP 地址;
28 表示 AAAA 记录,即 IPv6 地址;
etc
类 (Class) 通常为 1 ,表示 TCP/IP 互联网地址;
最后,我们回过头来考察域名字段,它的长度是不固定的。域名按 . 切分成若干部分,再依次保存。每个部分由一个前导计数字节开头,记录当前部分的字符数。

以域名 fasionchan.com. 为例,以 . 切分成 3 个部分,fasionchan 、com 以及空字符串 。请注意,空字符串 代表根域。因此,待查询域名字段依次为:

一个前导字节保存整数 10 ,然后 10 个字节保存 fasionchan 部分 (二级域);
一个前导字节保存整数 3 ,然后 3 个字节保存 com 部分 (一级域);
一个前导字节保存整数 0 ,然后 0 个字节保存 部分 (根域);
由此可见,每一级域名的长度理论上可以支持多达 255 个字符。

查询类型 名称代码 描述
1 A IPv4 地址
2 NS 名称服务器
5 CNAME 规范名称
15 MX 电子邮件交互
16 TXT 文本信息
28 AAAA IPv6 地址
查询类型这里先不展开,下一小节会详细介绍。

资源记录
服务端处理查询请求后,需要向客户端发送应答报文; 域名查询结果作为资源记录,保存在答案以及其后两节中。

答案节、授权信息节和附加信息节均由一条或多条资源记录组成,记录数目保存在头部中的对应字段,不再赘述。

资源记录结构和问题记录非常相似,它总共有 6 个字段,前 3 个和问题记录完全一样:

被查询域名 (Name),与问题记录相同;
查询类型 (Type),与问题记录相同;
类 (Class),与问题记录相同;
有效期 (TTL),域名记录一般不会频繁改动,所以在有效期内可以将结果缓存起来,降低请求频率;
数据长度 (Resource Data Length),即查询结果的长度;
数据 (Resource Data),即查询结果;
如果查询类型是 A 记录,那查询结果就是一个 IP 地址,保存于资源记录中的数据字段; 而数据长度字段值为 4 ,因为 IP 地址的长度为 32 位,折合 4 字节。

报文实例
我们以 test.fasionchan.com 这个域名为例,来讲解 DNS 查询请求报文和应答报文。

执行 dig 命令即可查询该域名:

dig test.fasionchan.com
我们对查询 test.fasionchan.com 的一次通信过程进行抓包,结果保存在 Github 上,以供参考。童鞋们可以将抓包结果下载到本地,并用 WireShark 打开,并结合讲解进行分析。

请求报文

抓包结果请求报文只有头部、问题节和附加节,图解假设没有附加节。

先看头部,问题记录数为 1 ,其他记录数都是 0 。这意味着,请求报文只有问题节,而且问题节中只有一条问题记录,只查询一个域名。头部中的标志位分别如下:

QR=0 ,表示该报文是一个请求报文;
操作码为 0 ,表示这个 DNS 请求是一个标准请求;
TC=0 ,表示请求报文没有被截短;
RD=1 ,表示客户端希望服务器可以执行递归查询;
问题记录我们已经很熟悉了,不再赘述:

Type=1 ,表示客户端希望查询 A 记录,即与域名关联的 IP 地址;
Class=1 ,代表 TCP/IP 互联网;
应答报文

抓包结果应答报文只有头部、问题节和答案节。其中,问题节中的问题记录与请求报文一样,图中就不展开了。

先看头部,问题记录数为 1 ,答案记录数也是 1 ,其他记录数都是 0 。这意味着,应答报文只有问题节和答案节,而且它们各自只有一条记录。头部中的标志位分别如下:

QR=1 ,表示该报文是一个应答报文;
操作码为 0 ,表示这个 DNS 请求是一个标准请求;
AA=0 ,表示结果不是由域名的权威服务器返回的,因为查询对象是本地的 DNS 缓存服务器 (如果是向权威服务器发起查询,它返回的应答报文 AA=1);
TC=0 ,表示应答报文没有被截短;
RD=1 ,与请求报文保持一致,略;
RA=1 ,表示服务端支持递归查询;
响应码为 0 ,表示查询成功,没有出错;
答案节中的资源记录就是查询结果,前 3 个字段与问题记录一样,不再赘述。

TTL 字段是一个整数,表示有效期,单位是秒。例子中的查询结果,有效期是 752 秒,即 12 分 32 秒。也就是说,查询结果从现在开始算,12 分 32 秒内均有效,无须重新请求。

查询结果是一个 IP 地址,长度为 4 个字节,保存在资源数据字段中。

域名压缩
我们注意到,应答报文中,会将请求报文中的问题记录原样返回。由于问题记录和资源记录都会保存域名,这意味着域名会被重复保存,而报文尺寸是有限的!

为了节约报文空间,有必要解决域名重复保存问题,这也是所谓的信息压缩。具体做法如下:

域名在报文中第二次出现时,只用两个字节来保存。第一个字节最高两位都是 1 ,余下部分和第二个字节组合在一起,表示域名第一次出现时在报文中的偏移量。通过这个偏移量,就可以找到对应的域名。

由此一来,原来需要 21 个字节来保存的域名,现在只需区区两个字节即可搞定,数据量大大降低!

实际上,域名压缩机制还可以针对域名的某个部分进行。举个例子,假设一个请求报文同时查询两个域名:

fasionchan.com
test.fasionchan.com
请求报文中包含两个问题记录,分别对应域名 fasionchan.com 和 test.fasionchan.com 。这两个域名都有一个公共后缀 fasionchan.com ,无须重复保存。

如上图,第二个域名只需保存 test 部分,然后接两个字节特殊的压缩字节,指向第一个问题记录中的 fasionchan.com 。如果两条问题记录顺序颠倒,结果也是类似的,留待童鞋们自行思考。