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 | +--------+------------------+------------------+-------------------+--------------------+ |
Header
:
1 | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ |
ID
: 16bit,一个随机的标识符被分配给查询数据包。响应数据包必须以相同的ID进行回复。由于UDP的无状态性质,需要这样来区分不同响应QR
:1 bit,0 表示查询报文,1 表示响应报文OPCODE
:4 bit,通常值为 0(标准查询),其他值为 1(反向查询)和 2(服务器状态请求),更多细节参考RFC1035AA
:1 bit,授权回答(Authoritative Answer),如果响应服务器拥有被查询的域,则设置为 1TC
:1 bit,可截断(Truncated Message)。使用 UDP 时,表示当响应总长度超过 512 bytes 时,只返回 512 bytesRD
: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 | 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 |
QNAME
表示查询域名QTYPE
2 bytes,表示查询的协议类型QCLASS
2 bytes,表示查询的类。比如,IN
代表Internet
Answer Section
/Authority Section
/Additional Section
这三个字段格式一样
1 | 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 |
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 | $ dig +retry=0 -p 1053 @127.0.0.1 +noedns baidu.com |
由于这个端口不会有任何响应,所以这个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 | $ xxd -b query_packet.txt |
下面对请求和响应报文进行分析
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
80 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
EQR
是0,表示它是一个查询报文,OPCODE
也是0,因为它是一个标准查询,AA
、TC
和RA
标志与查询无关,而RD
为1,因为dig默认为请求递归查询。最后的RCODE
也和查询无关最后剩下的8 bytes表示:
1
2
300000000 00000001 00000000 00000000 00000000 00000000 00000000 00000000
-+-+-+-+-+-+-+-+- -+-+-+-+-+-+-+-+- -+-+-+-+-+-+-+-+- -+-+-+-+-+-+-+-+-
QDCOUNT ANCOUNT NSCOUNT ARCOUNTHeader
部分解析完成,之后来到了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
5query 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都以一个零长度的标签结束,也就是一个空字节Response Packet
由于query和response报文结构相同,所以前12位也是
Header
,可以看到,前16 bits(01110010 01101011
)和query中一致,这也印证了请求和响应的ID必须相同这一说法。再看之后16 bits的内容:1
2
3
4
5
6
7
81 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
300000000 00000001 00000000 00000010 00000000 00000000 00000000 00000000
-+-+-+-+-+-+-+-+- -+-+-+-+-+-+-+-+- -+-+-+-+-+-+-+-+- -+-+-+-+-+-+-+-+-
QDCOUNT ANCOUNT NSCOUNT ARCOUNT跳过和query packet中相同的
Question Section
部分,我们来到了Answer Section
1
2
3
4
5
611000000 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
)DNS Packet Compression
在上面的例子,对于一个查询有两个answer,如果每个answer都包含完整的Name信息,answer少的时候还好,但随着answer数量增加,很快就会超过DNS UDP报文512 bytes的长度限制。DNS压缩通过标签提供的跳转指令,消除了域名在
NAME
、QNAME
和RDATA
字段中的重复所以可以把前文提到的标签分为两类:数据标签(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
4name 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 1481
2
3
4name 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报文的功能