实现一个dns服务器 -- 1.协议概览

The Domain Name System (DNS) is the hierarchical and decentralized naming system used to identify computers reachable through the Internet or other Internet Protocol networks.

本文是实现一个dns服务器的第一章,实现一个dns服务器系列文章将从研究 DNS 协议开始,一步步实现一个简单的DNS server

一、协议

我们将从研究 DNS 协议开始,并使用我们的知识来实现一个简单的客户端。
DNS 的查询和响应具有相同的格式,数据包如下所示:

1
2
3
4
5
+--------+------------------+------------------+-------------------+--------------------+
|Header | Question Section | Answer Section | Authority Section | Additional Section |
+--------+------------------+------------------+-------------------+--------------------+
|12 bytes| Variable | Variable | Variable | Variable |
+--------+------------------+------------------+-------------------+--------------------+

Header

1
2
3
4
5
6
7
8
9
10
11
12
13
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ID |
+--+-----------+--+--+--+--+--+--+--+-----------+
|QR| Opcode |AA|TC|RD|RA| Z|AD|CD| RCODE |
+--+-----------+--+--+--+--+--+--+--+-----------+
| QDCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ANCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| NSCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ARCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  • ID: 16bit,一个随机的标识符被分配给查询数据包。响应数据包必须以相同的ID进行回复。由于UDP的无状态性质,需要这样来区分不同响应
  • QR:1 bit,0 表示查询报文,1 表示响应报文
  • OPCODE:4 bit,通常值为 0(标准查询),其他值为 1(反向查询)和 2(服务器状态请求),更多细节参考RFC1035
  • AA:1 bit,授权回答(Authoritative Answer),如果响应服务器拥有被查询的域,则设置为 1
  • TC:1 bit,可截断(Truncated Message)。使用 UDP 时,表示当响应总长度超过 512 bytes 时,只返回 512 bytes
  • RD:1 bit,期望递归(Recursion Desired)。如果为1,表示DNS服务器必须处理这个查询,即这是一个递归查询;如果设置为 0,说明这是一个迭代查询
  • RA:1 bit,可用递归(Recursion Available)。表示DNS服务器是否支持递归查询
  • Z:1 bit,保留字段(Zero),固定为0 ;
  • AD: 1 bit,认证数据(Authenticated Data),表示响应是否经过DNSSEC验证
  • CD: 1 bit,关闭检查(Checking Disable),同样和DNSSEC有关,表示是否禁用安全检查
  • RCODE:4 bit, 表示返回值,0表示没有错误

Question Section

1
2
3
4
5
6
7
8
  0  1  2  3  4  5  6  7  0  1  2  3  4  5  6  7
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QNAME |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QTYPE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QCLASS |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  • QNAME 表示查询域名
  • QTYPE 2 bytes,表示查询的协议类型
  • QCLASS 2 bytes,表示查询的类。比如,IN代表Internet

Answer Section/Authority Section/Additional Section

这三个字段格式一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  0  1  2  3  4  5  6  7  0  1  2  3  4  5  6  7
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| NAME |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| TYPE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| CLASS |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| TTL |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| RDLENGTH |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| RDATA |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  • NAME 资源记录包含的域名
  • TYPE 2 bytes,表示DNS协议的类型
  • CLASS 2 bytes,表示RDATA的类
  • TTL 4 bytes,表示资源记录可以缓存的时间。0代表只能被传输,但是不能被缓存
  • RDLENGTH 2 bytes,表示RDATA的长度
  • RDATA 不定长字符串,表示记录,格式和TYPE/CLASS有关。比如,TYPE是A,CLASS 是 IN,那么RDATA就是一个4个字节的ARPA网络地址

理论部分讲完,让我们使用dig工具在实际中感受一下DNS查询

我们可以使用netcat 命令监听一个端口:

1
$ nc -u -l 1053 > query_packet.txt

然后新建一个终端,使用dig命令往这个端口发送一个DNS查询

1
2
3
4
5
6
$ dig +retry=0 -p 1053 @127.0.0.1 +noedns baidu.com

; <<>> DiG 9.10.6 <<>> +retry=0 -p 1053 @127.0.0.1 +noedns baidu.com
; (1 server found)
;; global options: +cmd
;; connection timed out; no servers could be reached

由于这个端口不会有任何响应,所以这个DNS查询会以timed out告终,这是符合预期的。当收到这个失败信息后,就可以在第一个终端使用Ctrl+C退出netcat监听。通过这上面的操作,我们得到了 query_packet.txt 文件,可以通过这个文件来进行一次真实的DNS请求:

1
$ nc -u 8.8.8.8 53 < query_packet.txt > response_packet.txt

上面这条命令使用8.8.8.8:53进行DNS查询,8.8.8.8是Google提供的免费DNS服务器的IP地址
执行后等待一会儿输入Ctrl+C退出,这样就得到了response的报文,可以使用xxd命令将两份txt文件转为二进制格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ xxd -b query_packet.txt
00000000: 01110010 01101011 00000001 00100000 00000000 00000001 rk. ..
00000006: 00000000 00000000 00000000 00000000 00000000 00000000 ......
0000000c: 00000101 01100010 01100001 01101001 01100100 01110101 .baidu
00000012: 00000011 01100011 01101111 01101101 00000000 00000000 .com..
00000018: 00000001 00000000 00000001 ...

$ xxd -b response_packet.txt
00000000: 01110010 01101011 10000001 10000000 00000000 00000001 rk....
00000006: 00000000 00000010 00000000 00000000 00000000 00000000 ......
0000000c: 00000101 01100010 01100001 01101001 01100100 01110101 .baidu
00000012: 00000011 01100011 01101111 01101101 00000000 00000000 .com..
00000018: 00000001 00000000 00000001 11000000 00001100 00000000 ......
0000001e: 00000001 00000000 00000001 00000000 00000000 00000000 ......
00000024: 10110010 00000000 00000100 11011100 10110101 00100110 .....&
0000002a: 10010100 11000000 00001100 00000000 00000001 00000000 ......
00000030: 00000001 00000000 00000000 00000000 10110010 00000000 ......
00000036: 00000100 11011100 10110101 00100110 11111011 ...&.

下面对请求和响应报文进行分析

  1. Query Packet

    之前提到过,Header长度为12 bytes,对应01110010 01101011 00000001 00100000 00000000 00000001 00000000 00000000 00000000 00000000 00000000 00000000,前16 bits表示ID,对应十进制为29291,这是DNS请求的唯一标识别,请求和响应的ID应该相同,之后的16 bits对应以下内容:

    1
    2
    3
    4
    5
    6
    7
    8
    0 0 0 0 0 0 0 1  0 0 1 0 0 0 0 0
    - -+-+-+- - - - - -+-+- -+-+-+-
    Q O A T R R Z A C R
    R P A C D A D D C
    C O
    O D
    D E
    E

    QR是0,表示它是一个查询报文,OPCODE也是0,因为它是一个标准查询,AATCRA标志与查询无关,而RD为1,因为dig默认为请求递归查询。最后的RCODE也和查询无关

    最后剩下的8 bytes表示:

    1
    2
    3
    00000000 00000001 00000000 00000000 00000000 00000000 00000000 00000000
    -+-+-+-+-+-+-+-+- -+-+-+-+-+-+-+-+- -+-+-+-+-+-+-+-+- -+-+-+-+-+-+-+-+-
    QDCOUNT ANCOUNT NSCOUNT ARCOUNT

    Header部分解析完成,之后来到了Question Section,为方便查看,先将00000101 01100010 01100001 01101001 01100100 01110101 00000011 01100011 01101111 01101101 00000000 00000000 00000001 00000000 00000001 转为16进制表示 05 62 61 69 64 75 03 63 6f 6d 00 00 01 00 01

    1
    2
    3
    4
    5
                        query name           type   class
    -------------------------------- ----- -----
    HEX 05 62 61 69 64 75 03 63 6f 6d 00 00 01 00 01
    ASCII b a i d u c o m
    DEC 5 3 0 1 1

    正如前文所述,Question Section由三部分组成:query name、type和class。query name的编码方式并没有通过.进行分隔,而是将每个名称编码为一串标签(label),每个标签前有一个表示其长度的单字节。在上面的例子中,baidu是5个字节,因此前面是00000101,而 com 是3个字节,前面是00000011。最后,所有的query name都以一个零长度的标签结束,也就是一个空字节

  2. Response Packet

    由于query和response报文结构相同,所以前12位也是Header,可以看到,前16 bits(01110010 01101011)和query中一致,这也印证了请求和响应的ID必须相同这一说法。再看之后16 bits的内容:

    1
    2
    3
    4
    5
    6
    7
    8
    1 0 0 0 0 0 0 1  1 0 0 0 0 0 0 0
    - -+-+-+- - - - - -+-+- -+-+-+-
    Q O A T R R Z A C R
    R P A C D A D D C
    C O
    O D
    D E
    E

    由于这是一个响应,所以QR被设置为1,RA表示服务器支持递归,再看看之后的8 bytes数据

    1
    2
    3
    00000000 00000001 00000000 00000010 00000000 00000000 00000000 00000000
    -+-+-+-+-+-+-+-+- -+-+-+-+-+-+-+-+- -+-+-+-+-+-+-+-+- -+-+-+-+-+-+-+-+-
    QDCOUNT ANCOUNT NSCOUNT ARCOUNT

    跳过和query packet中相同的Question Section部分,我们来到了Answer Section

    1
    2
    3
    4
    5
    6
    11000000 00001100 00000000 00000001 00000000 00000001 
    00000000 00000000 00000000 10110010 00000000 00000100
    11011100 10110101 00100110 10010100
    11000000 00001100 00000000 00000001 00000000 00000001
    00000000 00000000 00000000 10110010 00000000 00000100
    11011100 10110101 00100110 11111011

    按照我们之前解析Question Section的经验,name会将每个名称编码为一串标签,每个标签前有一个表示其长度的单字节,11000000转为10进制之后是192,但显然之后数据长度不足192 bytes。那么是出了什么问题呢?这就涉及到了DNS压缩技术(DNS Packet Compression

  3. DNS Packet Compression

    在上面的例子,对于一个查询有两个answer,如果每个answer都包含完整的Name信息,answer少的时候还好,但随着answer数量增加,很快就会超过DNS UDP报文512 bytes的长度限制。DNS压缩通过标签提供的跳转指令,消除了域名在NAMEQNAMERDATA字段中的重复

    所以可以把前文提到的标签分为两类:数据标签(data label)和压缩标签(compression label)。数据标签在解析Query Packet部分已做过说明,压缩标签则相当于一个指针,

    实现方式为:原本计数字节高两位设置为 1,剩余位与随后的一个 byte 组成一个 14 位的指针,即偏移量,给出距离距离 DNS 消息开始出的字节数,在那可找到一个用于替代压缩标签的数据标签,即压缩标签能够指向一个距离开始处多达 16384(2^14)个字节的位置

    具体到我们的例子,首先将Answer Section中的二进制转为16进制方便查看

    c0 0c 00 01 00 01 00 00 00 b2 00 04 dc b5 26 94 c0 0c 00 01 00 01 00 00 00 b2 00 04 dc b5 26 fb

    注意到0xc00c出现了两次,结合之前解析出的ANCOUNT为2,我们可以得出两个ANSWER都使用了压缩标签

    计数字节高两位设置为1后的16进制表示是0xc000(二进制11000000 00000000),所以我们可以通过用这个掩码对我们的两个字节进行xor来找到跳转的位置,0xc00c ^ 0xc000 = 12。因此,我们应该跳到数据包的第12 bytes,并从那里读取。回顾DNS头的长度恰好是12 bytes,可以推出我们需要从数据包的Question Section开始读取,因为这个例子中question是以domain开始( baidu.com)。一旦我们完成了对name的读取,我们就在我们离开的地方继续解析,并进入到记录类型

    1
    2
    3
    4
          name     type   class         ttl        len      ip
    ------ ------ ------ -------------- ------ --------------
    HEX c0 0c 00 01 00 01 00 00 00 b2 00 04 dc b5 26 94
    DEC 192 12 1 1 178 4 220 181 38 148
    1
    2
    3
    4
          name     type   class         ttl        len      ip
    ------ ------ ------ -------------- ------ --------------
    HEX c0 0c 00 01 00 01 00 00 00 b2 00 04 dc b5 26 fb
    DEC 192 12 1 1 178 4 220 181 38 251

    这样我们也就顺利完成了Response Packet的解析

二、总结

这一章节主要介绍了DNS协议的报文结构,并通过dig工具分析了一次完整的DNS请求过程。下一章将会开始编码,实现通过代码解析DNS报文的功能