DNS报文格式

闻道有先后,术业有专攻。

—— 唐·韩愈·《师说》

经过前面学习,我们知道查询一个域名,需要与 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 个部分,fasionchancom 以及空字符串 。请注意,空字符串 代表根域。因此,待查询域名字段依次为:

  • 一个前导字节保存整数 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.comtest.fasionchan.com 。这两个域名都有一个公共后缀 fasionchan.com ,无须重复保存。

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

小菜学网络】系列文章首发于公众号【小菜学编程】,敬请关注:

【小菜学网络】系列文章首发于公众号【小菜学编程】,敬请关注: