美文网首页
听说你想写个 DNS 服务器 - 数据包

听说你想写个 DNS 服务器 - 数据包

作者: 微微笑的蜗牛 | 来源:发表于2021-07-31 16:33 被阅读0次

    大家好,我是微微笑的蜗牛,🐌。

    今天将开始一个新的系列,动手写 DNS 服务器,使用 Swift 实现。

    我们知道,DNS 服务器是用来解析域名,返回对应的 IP 地址。它使用请求/应答模型,客户端发送请求包,服务器解析后返回应答的数据包。

    既然要自己动手写 DNS 服务器,那我们就得先了解下 DNS 数据包的结构是什么样的。

    数据包格式

    DNS 请求和应答的数据包格式是一样的,包括包头和包体数据两部分。

    先看看数据包的概览图:

    它包括如下部分:

    • Header,头部,长度为 12 字节
    • Question Section,查询列表
    • Answer Section,记录列表
    • Authority Section,权威服务器列表

    包含 Name Server 信息,也就是 NS 记录,用于逐级向下递归查询。

    NS 记录表示域名服务器信息,只能返回域名。

    举个例子,以下是一条 NS 记录:

    com.   6285 IN NS g.gtld-servers.net.
    

    它表明 g.gtld-servers.net 是一个 DNS 服务器,分管 com 域名。

    • Additional Section,附加信息列表

    这是比较有用的一部分,比如可包含 Name Server 对应的 IP 信息,也就是 A 记录。

    A 代表 Address,表示域名与 IP 的映射关系。

    举个例子,以下是一条 A 记录:

    a.gtld-servers.net. 1517    IN  A   192.5.6.30
    

    这就是 NS 对应的 A 记录,包含了 IP 信息。当拿到 IP 后,我们可以继续向该 DNS 服务器查询。

    Dig

    接着我们使用 dig 命令,查看服务器返回的数据,看是否能跟上述结构对应上。noedns 表示按原始数据展示。

    $ dig +noedns google.com
    
    ; <<>> DiG 9.10.6 <<>> +noedns google.com
    ;; global options: +cmd
    ;; Got answer:
    ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 7159
    ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 13, ADDITIONAL: 11
    
    ;; QUESTION SECTION:
    ;google.com.            IN  A
    
    ;; ANSWER SECTION:
    google.com.     127 IN  A   142.250.204.78
    
    ;; AUTHORITY SECTION:
    com.            6285    IN  NS  g.gtld-servers.net.
    com.            6285    IN  NS  k.gtld-servers.net.
    com.            6285    IN  NS  c.gtld-servers.net.
    com.            6285    IN  NS  d.gtld-servers.net.
    com.            6285    IN  NS  b.gtld-servers.net.
    com.            6285    IN  NS  f.gtld-servers.net.
    com.            6285    IN  NS  i.gtld-servers.net.
    com.            6285    IN  NS  m.gtld-servers.net.
    com.            6285    IN  NS  a.gtld-servers.net.
    com.            6285    IN  NS  l.gtld-servers.net.
    com.            6285    IN  NS  h.gtld-servers.net.
    com.            6285    IN  NS  j.gtld-servers.net.
    com.            6285    IN  NS  e.gtld-servers.net.
    
    ;; ADDITIONAL SECTION:
    a.gtld-servers.net. 1517    IN  A   192.5.6.30
    a.gtld-servers.net. 57320   IN  AAAA    2001:503:a83e::2:30
    b.gtld-servers.net. 438 IN  A   192.33.14.30
    b.gtld-servers.net. 32119   IN  AAAA    2001:503:231d::2:30
    c.gtld-servers.net. 3318    IN  A   192.26.92.30
    c.gtld-servers.net. 172527  IN  AAAA    2001:503:83eb::30
    d.gtld-servers.net. 1877    IN  A   192.31.80.30
    d.gtld-servers.net. 60201   IN  AAAA    2001:500:856e::30
    e.gtld-servers.net. 438 IN  A   192.12.94.30
    e.gtld-servers.net. 6198    IN  AAAA    2001:502:1ca1::30
    f.gtld-servers.net. 2957    IN  A   192.35.51.30
    
    ;; Query time: 0 msec
    ;; SERVER: 172.26.9.10#53(172.26.9.10)
    ;; WHEN: Sun Jul 04 16:26:53 CST 2021
    ;; MSG SIZE  rcvd: 504
    

    它包含了如下几部分:

    • ->>HEADER<<-:表示包头,后面的数据是包头字段对应的值。

      // 操作码
      opcode: QUERY, 
      
      // 状态
      status: NOERROR, 
      
      id: 7159
      
      // 设置的标记,qr 表示 response;rd 表示 recursion desire;ra 表示 Recursion available
      flags: qr rd ra;
      
      // 查询个数为 1 
      QUERY: 1, 
      
      // 记录个数为 1
      ANSWER: 1, 
      
      // ns 个数为 13
      AUTHORITY: 13, 
      
      // 附加信息个数为 11
      ADDITIONAL: 11
      
    • 之后是 QUESTION SECTION,有一条查询数据。IN 表示 ClassA 是记录类型。

      google.com.         IN  A
      
    • ANSWER SECTION,返回了一条记录。127TTL142.250.204.78 是 IP。

      google.com.     127 IN  A   142.250.204.78
      
    • AUTHORITY SECTION,有 13 条 NS 记录。

    • ADDITIONAL SECTION,有 11 条 NS 对应的 A 记录信息。

    我们可以看出,这些数据是跟包的结构是吻合的。

    DNS Header

    再来看看 Header 部分,照例先上结构图。

    image

    包头总共 12 字节,包括如下部分:

    • ID,包标识,16 bits。查询与应答包的 ID 一致。
    • QR,Query Response,1 bit。用于确定是查询还是应答,0 为查询,1 为应答。
    • OPCODE,Operation Code,4 bit。一般为 0。
    • AA,Authoritative Answer,1 bit。如果 DNS 服务器是权威的,则为 1。
    • TC,Truncated Message,消息是否截断,1 bit。如果数据包长度大于 512 字节,则为 1。
    • RD,Recursion Desired,1 bit,是否期望递归查询。
    • RA,Recursion Available,1 bit,服务器是否支持递归查询。
    • Z,保留字段,3 bits。
    • RCODE,Response Code,4 bits,返回码。
    • QDCOUNT,Question Count,16 bits,查询数量。
    • ANCOUNT,Answer Count,16 bits,结果数量。
    • NSCOUNT,Name Server Count,16 bits,ns 的数量。
    • ARCOUNT,Additional Count,16 bits,附加信息数量。

    DNS Question

    接着我们再来看看查询部分,可包含多条查询信息。

    查询的结构如下图所示:

    image

    它包括三部分:

    • domain:要查询的域名,比如 google.com
    • type:记录的类型,2 字节。
    • class:2 字节,一般为 1。

    DNS Answer

    同样,应答部分可包含多条记录。记录结构如下图所示:

    image

    它包括如下几部分:

    • domain:要查询的域名,比如 google.com
    • type:记录类型,2 字节。
    • class:2 字节,一般为 1。
    • ttl:Time To Live,存活时间,4 字节。
    • data_len:数据长度,2 字节。
    • ip:ip 地址,4 字节。

    Domain

    在查询和记录的结构中都包括了域名字段,那么域名在数据包中是如何表示的呢?

    1. 域名存储结构

    我们知道,域名是一串字符,以 "." 号分隔,比如 google.com

    一般情况下,如果需存储一段字符,我们会将各字符对应的 ASCII 写入。但是在 DNS 包数据中,. 是不存储的。

    它的做法是:

    • 将域名以 . 分隔后,将各部分进行存储。比如 google.com,只会存储 googlecom 两部分。

    • 在各部分之前还会加上数据长度,也就是 「数据长度+数据」 的格式。

    举个栗子,googlecom 的存储分别如下:

    image
    • 最后以 0x00 空字符结束。

    2. 举例说明

    还是拿 google.com 举例。以 . 分隔后,它会变成两部分:googlecom

    现在我们遵循「数据长度+数据」这个格式,来构造一下数据。

    • 先看 google,它的长度是 6,那么表示为 6+google
    • 再看 com,它的长度是 3,那么表示为 3+com
    • 最后加上结束符 0x00

    完整数据表示如下:

    image

    3. 例外情况

    但是存在一种例外情况,域名使用间接的方式来表示。

    比如响应包中包含了查询部分和记录部分,它们都包含了域名字段。既然在查询部分已经有域名数据了,那在记录部分就不需使用重复的数据来表示。只需使用某种间接方式,沿用查询部分的域名即可。

    这也是一种减少包大小的方式。如果有很多相同的域名,那么只需存储一份真正的域名即可,其余的都使用间接方式指向真正的域名。

    如下所示,间接表示时,通过某种方式指向到原查询部分的域名。

    image

    所以,只需要找到一种可以间接指向到真正域名的方式即可。

    上面说到常规的域名表示为:「数据长度+数据」。而间接表示时,在数据长度上做了些手脚,引出了跳转偏移的概念。

    跳转偏移是相对于整个数据包的偏移量。从数据长度中计算出偏移量后,再转到偏移处重新读取域名数据。

    正常情况下,数据长度是一字节。为了兼容间接表示的情况,有这样一个约定:如果长度的高两位都是 1,则表示是间接方式。这时候,数据长度之后的一个字节也将表示长度,也就是两个字节表示长度。

    跳转偏移量的计算方式是:去除第一个字节的高两位,余下的 14 位数据则为偏移量。

    如下图所示:

    image

    用数学公式表示如下,使用异或的方式清除高 2 位。

    // b1 是第一个字节,b2 是第二个字节
    b1b2 ^ 0xC000
    

    举个栗子,比如头两个字节是 0xC00C

    由于第一个字节 0xC0 → 11000000 高两位是 1,那么接下来的一个字节 0x0C 也表示长度。

    根据公式计算出偏移量:0xC00C ^ 0xC000 = 0x000C,接着去偏移量为 12 字节的地方去查找真实域名。

    偏移部分如下图所示:

    image

    其实上面这个栗子不是随便举的,是真实的数据。此时偏移量为 12,而 Header 的头部刚好也是 12 字节。哈哈,是不是有点巧合?

    由于头部后面跟着的是查询数据,而查询结构的第一个字段就是域名。这样就会恰好跳转到查询结构中的域名处,然后按正常情况读取该域名。一切都刚刚好~

    如下图所示:

    image

    数据包抓取

    在了解数据包的结构后,现在我们就来抓下 DNS 的数据包,解析出具体的数据。

    这里,我们使用 netcat 监听某端口的数据,然后执行 dig 命令,将查询数据发到同样的端口。

    步骤如下:

    1. 打开终端,执行如下命令:
    nc -u -l 1053 > query_packet.txt
    
    1. 新开另一个终端窗口,执行如下命令:
    dig +retry=0 -p 1053 @127.0.0.1 +noedns google.com
    

    这时,命令会执行失败。

    1. 在 netcat 命令窗口,按住 CTRL+C 终止进程,这样在 query_packet.txt 就有了查询的数据。

    2. 在 netcat 命令窗口,执行如下命令。使用 query_packet 中的包发送请求,同时记录响应数据。

    nc -u 8.8.8.8 53 < query_packet.txt > response_packet.txt
    
    1. 在 netcat 命令窗口,按住 CTRL+C 终止进程。同样响应数据会写入 response_packet.txt 中。

    这样我们就得到了查询数据和返回数据,接下来进行数据包的分析。

    数据包分析

    我们可使用 hexdump 查看包中的十六进制数据。

    hexdump -C query_packet.txt
    

    请求包

    query_packet.txt 中十六进制数据如下:

    00000000  19 59 01 20 00 01 00 00  00 00 00 00 06 67 6f 6f  |.Y. .........goo|
    00000010  67 6c 65 03 63 6f 6d 00  00 01 00 01              |gle.com.....|
    0000001c
    

    我们按照包结构一点点的分析。

    1. 首先是包头部分。

    包头有 12 个字节,将数据提取出来如下所示。注意:数据是大端字节序

    19 59 01 20 00 01 00 00  00 00 00 00
    

    根据包头中数据的属性归类,将其分为三部分:

    • ID 部分:2 字节,0x1959
    • flag 部分:2 字节,0x0120
    • count 部分:8 字节,0x01000000

    各字段的值就不一一分析了,如下所示:

    image
    1. 包体部分

    由于是请求包,只会有查询部分,数据就是剩余部分,如下所示:

    06 67 6f 6f 67 6c 65 03 63 6f 6d 00  00 01 00 01
    

    上面我们提到,查询数据的结构如下:

    image

    首先说下 domain 的解析。

    正常情况下,domain 的数据格式是「数据长度+数据」。规则很简单,解析过程如下所示:

    • 解析出一个字节的数据长度 len。
    • 读取后面 len 个字节的数据。因为分隔符 . 不存储,需手动加上。
    • 不断循环上面的步骤,直至碰到结束符 0x00 退出循环。

    参照上面的步骤,可解析出 domain 的值为:google.com

    接着就是解析 type、class 的值,按照它们所占字节数逐个解析就好。

    最后各部分的值,如下图所示:

    image

    响应包

    response_packet.txt 中十六进制数据如下:

    00000000  19 59 81 80 00 01 00 01  00 00 00 00 06 67 6f 6f  |.Y...........goo|
    00000010  67 6c 65 03 63 6f 6d 00  00 01 00 01 c0 0c 00 01  |gle.com.........|
    00000020  00 01 00 00 00 1c 00 04  8e fa cc 4e              |...........N|
    0000002c
    
    1. 包头部分

    跟请求包一样的分析方式,就不再赘述了,直接给出结果吧。

    image
    1. 包体部分

    响应的数据比请求数据要多一些,因为除了待查询数据,还包含了返回记录。查询数据跟请求包中的数据是一样的,解析就不再重复说了。

    这里主要说说返回记录数据的解析,数据从 c0 开始。

    我们先回顾下记录的数据结构,看看下图,第一个字段是域名。

    image

    再看看记录部分的数据,如下所示:

    // 记录部分的数据
    c0 0c 00 01 00 01 00 00 00 1c 00 04  8e fa cc 4e
    

    我们可以发现,第一个字节 0xc0 是以 0x11 开头的,说明 domain 是采用了间接的获取方式,这时候前两个字节 0xc00c 表示长度。

    按照前面提到过的公式,可计算出偏移量为 12:

    0xC00C ^ 0xC000 = 0x000C
    

    而包头大小刚好是 12 字节,这时候就会跳转到包体开始的位置,也就是查询数据部分,以正常方式读取域名。

    间接域名指向可看下图中标红部分:

    image

    在域名解析完成后,接下来是 type、class、ttl、data_len、ip 数据的解析。处理起来比较简单,按照它们各自所占字节数解析就好,各部分的值参照上图。

    代码实现

    完整代码实现可查看:https://github.com/silan-liu/dns-server

    工程结构分为如下几部分:

    • BytePacketBuffer,主要是对数据的操作,比如读取数据、移动指针等。
    • DNSHeader,包头数据的结构定义及数据解析。
    • DNSQuestion,查询数据的结构定义及数据解析。
    • DNSRecord,记录数据的结构定义及数据解析。
    • DNSPacket,数据包的结构定义及数据解析。
    • main,读取本地的响应包数据,解析出数据包。

    代码实现就不详细进行分析了,相信聪明的你可以轻易看懂~

    总结

    这篇文章主要介绍了 DNS 数据包的结构,各个部分的字段定义,布局信息以及如何进行相关数据的解析。

    下一节将讲述如何准备响应数据包,主要是关于数据包的写入操作。

    感谢阅读,希望能给你带来一点点收获~

    参考资料

    相关文章

      网友评论

          本文标题:听说你想写个 DNS 服务器 - 数据包

          本文链接:https://www.haomeiwen.com/subject/qdscpltx.html