DNS:域名系统(二)

文章《DNS:域名系统(一)》从 DNS 系统的结构和工作原理的角度描绘了 DNS 系统的基本概貌,本文将继续深入探讨 DNS 的话题,以 DNS 客户端的视角窥探 DNS 协议的细节。

在 Internet 中,DNS 客户端与服务器之间使用 TCP 或 UDP 协议传送 DNS 消息,服务器端开放的端口号均为 53。在大多数情况下,客户端通过 UDP 协议向服务端发送 DNS 查询请求,服务端也通过 UDP 协议向客户端发送响应。在客户端或服务端认为一次查询或响应需要运载的数据量超过 512 字节时,需要改用 TCP 协议来传送请求或响应,后文将对这种情形有所解释。

DNS 请求与响应的消息均由消息头部与消息实体两部分组成,请求与响应的消息头部的格式是一致的,下面是 DNS 消息头部结构的图示[1]

                                    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 是一个 16 位的标识符,由客户端根据具体情况设置,当服务端回应这个请求时,会将同样的 ID 值填写在响应消息头部的 ID 字段中。
  • QR 是一个占 1 位的字段用于标记这个 DNS 消息是请求(Query)还是响应 (Response)。当 DNS 消息是一个请求消息时,这个字段被设置为 0,反之当 DNS 消息是一个响应消息时,这个字段被设置为 1。
  • Opcode 是一个 4 位的操作代码,它由客户端设置,服务器在响应中会填入与对应请求相同的 Opcode。目前,Opcode 有三种有效的取值,它们的值与含义如下:
    • 0 – 标准请求。
    • 1 – 反向解析请求,用于查询 PTR 记录。
    • 2 – 服务器状态查询。
  • 接下来是一个包含 4 位的 flag,从低到高依次是 AA、TC、RD 和 RA,每个 flag 的含义分别如下:
    • AA(Authoritative Answer)标记只在响应消息中有效,置位表示该响应来自请求域名对应的域中的授权服务器。
    • TC(Truncation)标记表示该消息因传输长度的限制而被截断。通常发生在客户端以 UDP 协议发起解析请求,而服务端的全部响应超过 512 字节时,服务端将响应在 512 字节处截断,并置位 TC,提示客户端收到的响应可能是不完整的。
    • RD(Recursion Desired)递归解析请求,要求 DNS 服务端以递归解析的方式处理请求。由客户端在请求中设置,服务端在响应中置位与对应的请求一致。值得注意的是,DNS 协议不强制要求所有 DNS 服务器提供递归解析的功能,因此即使客户端在请求中置位 RD,也有可能不会得到期望的响应,可以用下面提到的 RA 标记判断服务器是否支持递归解析。
    • RA(Recursion Available)递归可用。这个标记由服务端设置,在响应中有效。当 RA 置位时,表示该服务器支持递归解析,向它发起的递归解析请求都能得到期望的结果。
  • Z 所在的位置是一段 4 位的保留区域,必须填为 0。
  • RCODE(Response code)是由服务端在响应中设置的 4 位响应代码,不同的值含义分别如下:
    • 0 – 没有错误。
    • 1 – 格式错误。服务器无法解释请求。
    • 2 – 服务器失败。因为服务器的某些故障而不能完成解析请求。
    • 3 – 名字错误。这个错误代码只会出现在授权的域名服务器的响应中,含义为请求查询的域名不存在。
    • 4 – 未实现。当前服务器不支持这种查询。
    • 5 – 拒绝。服务器主动拒绝当前的查询请求。
    • 6 ~ 15 – 为其他失败保留的代码。
  • QDCOUNT 是一个 16 位的整数,表示消息实体中问题的数量。
  • ANCOUNT 是一个 16 位的整数,表示消息实体中答案的数量。
  • NSCOUNT 是一个 16 位的整数,表示消息实体中 NameServer 记录的数量。
  • ARCOUNT 是一个 16 位的整数,表示消息实体中授权记录的数量。

值得注意的一点是,当客户端程序通过 UDP 协议发起一次 DNS 解析请求,后又收到一个 TC 置位的服务器响应时,客户端程序一般要用 TCP 协议与服务器建立连接,重新发起一次解析请求以获得完整的解析结果。

消息实体紧随消息头部,消息实体由问题部分和资源记录部分组成,DNS 请求的消息实体只包含问题部分,DNS 响应的消息实体既有问题部分又有资源记录部分。其中资源记录又分三大块:DNS 回答资源记录、授权服务器资源记录和附加的资源记录。一个包含消息头部和完整消息实体的 DNS 消息如下所示。

    +---------------------+
    |        Header       |
    +---------------------+
    |       Question      | the question for the name server
    +---------------------+
    |        Answer       | RRs answering the question
    +---------------------+
    |      Authority      | RRs pointing toward an authority
    +---------------------+
    |      Additional     | RRs holding additional information
    +---------------------+

Question 是 DNS 解析请求的问题部分,在 DNS 响应消息中也包含这个部分,内容与对应的请求消息中的 Question 一致。Question 中有请求解析的域名、请求的资源类型和网络类型。Question 的结构如下所示。

                                    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                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

QNAME 是被查询的域的域名,它的长度是不定长的。在《DNS:域名系统(一)》中提到,域名系统树中任意一个结点域名是由该结点到根结点的路径上经过的所有结点的标签一起组成的,书写上用圆点分隔这些标签(label),如 work.luodichen.com 等。域名在 QNAME 中的格式与书写格式稍有不同,同样是从枝叶到树根的标签依次排列,但每个标签之前还有一个字节表示该标签的长度。注意,树根的 NULL 结点也要包含在其中,树根总是以一个值为 0 的字节表示,同时也是 QNAME 字段结束的标识。例如域名 work.luodichen.com 在 QNAME 中存储形式的 C 语言表示如下:

特别地,当解析的目标域名是根域名时,QNAME 的格式同样遵循上面所说的规则,向 QNAME 中填入根域名的长度 0,其后不再有任何字符。

QTYPE 是一个 2 字节的查询类型字段,表示希望解析域名的资源记录的类型,DNS 协议规定的资源记录类型较多,有些是已经废弃不用的,有些是为了适应互联网的发展新加的。下面的代码列出了几种常见的资源记录类型的缩写、数值和说明。

QCLASS 字段表示网络类型,一般设置为 1(Internet)。

DNS 响应消息中包含 Answer、Authority 和 Additional 等三组资源记录,每一组资源记录都有 0 个或多个资源记录,上文已经提到,每组资源记录的数目在协议头部对应的字段中。每个资源记录的格式都是一致的,它们的结构如下所示:

                                    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 是变长字段,表示该资源记录对应的域名,它的格式可能是压缩形式的,后文中将讨论 DNS 消息中压缩名字的格式。

TYPE 与 CLASS 与上文所说的 QTYPE 和 QCLASS 类似,表示这个资源记录的类型与网络类型。

TTL 是一个 32 位的整数,表示这个资源记录的生存时间(秒),TTL 是一个资源记录能在 DNS 缓存中生存的最长时间。

RDLENGTH 是一个 16 位的整数,它表示 RDATA 字段的长度(字节)。

RDATA 是一个变长的字段,根据资源记录类型(TYPE)的不同而有不同的格式,例如在 A(IP 地址)记录中,RDATA 是一个 32 位的 IP 地址。更多资源记录类型对应的 RDATA 格式请参考文本参考文献中提到的 RFC 1035 文档和其他相关的 RFC 文档。

为了节省 DNS 查询和响应的网络流量,DNS 协议使用一种压缩的策略来消除 DNS 消息中域名相关字段可能产生的重复内容,从而减小 DNS 消息的长度,这对于一种主要使用 UDP 来传送消息的协议来说是很重要的。前文中提到,一个域名可以表示为多个标签连续排列,并在每个标签前用一个字节表示标签长度的方式来表示,为了下文描述方便,将这个字节称为该标签的“标签头”。此外,DNS 协议还允许使用一种指针的方式引用消息实体中当前标签所在的位置之前任意位置出现过的域名或域名的一部分。DNS 规定,当一个“标签头”的高 2 位都为 1 时,这个标签头余下的 6 位以及标签头后一个字节的 8 位共 14 位构成一个指针。指针的整数数值是一个偏移量,偏移的起始位置是消息实体的第一个字节,指针指向的是另一个标签头,因为指针的目标可能是一个普通的标签头也可能是另一个带指针的标签头,因此需要递归地处理新的标签头,直到遇到根结点标签。下面是一个在 Answer 资源记录中 NAME 字段以指针表示的实例,可以看到 Answer 资源记录的 Name 字段是 0xc00c,指针偏移值是 12,指向 Queries 中 Name 字段的一个域名 luodichen.com。

dns-compress图 1 解析域名 luodichen.com 的响应消息

在编写文本的同时,还用 C++ 开发了一个同时支持 Windows 和 Linux 平台的 DNS 客户端库,源码托管在 GitHub,查看

(允许转载本文,转载请注明出处 http://luodichen.com/blog/?p=367 )

参考文献
[1] P. Mockapetris, DOMAIN NAMES – IMPLEMENTATION AND SPECIFICATION, RFC 1035, November 1987; www.ietf.org/rfc/rfc1035.txt