找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2862

积分

0

好友

388

主题
发表于 3 天前 | 查看: 14| 回复: 0

之前为了给一个虚拟专用网客户端添加 SRV 记录查询功能,我尝试用了一个现成的 DNS 查询库,结果在 Windows 上编译时遇到了一堆麻烦。这让我下定决心,不如自己动手实现一套轻量的 DNS 查询工具。要自己写,第一步就得搞清楚 DNS 报文到底长什么样,特别是请求报文该怎么组装。这篇笔记就专门聊聊 DNS 请求报文的构成。

报文头部:固定的 12 字节

一个标准的 DNS 报文可以划分为五个主要部分。第一部分是一个固定长度为 12 字节的头部(Header),它包含了控制整个查询的核心信息。头部之后,是四个长度可变的数据段。

DNS报文结构示意图

这关键的 12 个字节,又可以细分为 6 个字段,其布局如下图所示:

DNS头部标志位字段布局图

我们来逐一拆解:

  • Transaction ID (标识符):由客户端发起请求时随机生成的一个 16 位无符号整数,用于匹配请求与对应的响应。
  • Flags (标志位):这是一个 16 位的复合字段,包含了多种控制标志,具体如下表:
标志位 作用 长度 (bit)
QR (Query/Response) 0表示查询请求,1表示响应 1
opcode 0: 标准查询 1: 反向查询 2: 服务器状态请求 4
Authoritative 表示是否为授权服务器的回答 1
Truncated 响应是否被截断,即使用UDP时,是否只返回前512字节内容 1
Recursion desired 是否期望进行递归查询 1
Recursion available 如果响应时,域名服务器可以递归查询,则将该位置为1 1
Z 预留字段 1
AD 应答服务器是否验证了DNSSEC数字签名 1
CD checkdisabled,是否接收没有验证的数据 1
rcode Response Code,返回码的字段,通常值为0或3,0表示没有出错,3表示域名不存在 4
  • Question Count (问题数):表示查询段(Question Section)中包含的查询条目数量。
  • Answer Record Count (资源记录数):表示回答段(Answer Section)中包含的资源记录数量。
  • Name Server (Authority Record) Count (授权域名记录数):表示授权段(Authority Section)中包含的记录数量。
  • Additional Record Count (额外资源记录):表示附加段(Additional Section)中包含的记录数量。

理论说完了,我们抓个包来看看实际数据。我本地发起了一次对 www.baidu.com 的 A 记录查询,其报文前 12 个字节的十六进制表示如下:

a0 fa 01 00 00 01 00 00 00 00 00 01
  • 前两个字节 0xa0fa 是随机生成的 Transaction ID。
  • 紧跟着的两个字节是 0x0100,这就是 Flags 字段。把它转成二进制是 0000000100000000

按照前面表格中每个标志位的长度来拆分这个二进制串:0 0000 0 0 1 0 0 0 0 0000。整理一下,这个查询请求的标志位含义就很清晰了:

  • QR 类型:0,表示这是一个查询请求。
  • opcode 查询方式:0000,表示标准查询。
  • Authoritative:0,查询请求中此位无关,置为0。
  • Truncated:0,表示内容未被截断。
  • Recursion Desired:1,期望服务器使用递归查询。
  • Recursion Available:0,查询请求中此位无关,置为0。
  • Z:预留位,固定为0。
  • AD:0,查询请求中此位无关,置为0。
  • CD:0,表示不接受未经验证的数据。
  • rcode:0000,查询请求中此位无关,置为0。

请求报文的构造

对于单纯的查询请求,头部 Flags 字段中实际需要关心的只有以下几项,其他无关位通常置0即可:

  • QR Type:查询请求固定为0。
  • Opcode:指定查询类型(如标准、反向查询)。
  • Truncated:查询时通常为0。
  • RD (Recursion Desired):是否启用递归查询,这是客户端可以控制的关键选项。
  • Z:预留位,固定为0。
  • CD:是否接受未验证的数据。

一个最常见的、只启用递归查询的标准请求,其 Flags 值组合起来就是 0x0100

头部之后,紧跟着的就是长度可变的 Question段。这里存放着我们真正要查询的内容:域名、记录类型和地址类型。那么,长度不定的域名信息是如何编码的呢?

www.baidu.com 为例,这个域名被点号.分割成三部分:wwwbaiducom。DNS 报文在传输时,采用 长度值 + 标签内容 的格式来编码。对于 www.baidu.com,其十六进制表示如下:

0x03, 0x77, 0x77, 0x77, 0x05, 0x62, 0x61, 0x69, 0x64, 0x75, 0x03, 0x63, 0x6f, 0x6d, 0x00

解析一下:

  • 0x03 表示接下来的标签有3个字符,然后是三个 0x77(字母‘w’的ASCII)。
  • 0x05 表示接下来的标签有5个字符,然后是 baidu 的 ASCII 码。
  • 0x03 表示接下来的标签有3个字符,然后是 com 的 ASCII 码。
  • 最后以 0x00 作为域名编码的结束符。

在结束符之后,完整的 Question 段还需要追加 2 字节的 Type(记录类型)和 2 字节的 Class(地址类型)。

  • 查询 A 记录时,Type 值为 1。
  • 使用 Internet 地址时,Class 值为 1。

现在,我们可以组装一个最简单的 DNS 请求负载了:

uint8_t qry[] = {
        0xde, 0xad, // Transaction Id 随机值
        0x01, 0x00, // Flags
        0x00, 0x01, // Questions 请求数量 1,即后面Query部分带有一个域名
        0x00, 0x00, // Anwser 数量,请求报文不关注这个字段
        0x00, 0x00, // 权威回答数量,请求报文不关注这个字段
        0x00, 0x00, // 额外信息数量,请求报文不关注这个字段
        0x03, 0x77, 0x77, 0x77, // 等价于 www
        0x05, 0x62, 0x61, 0x69, 0x64, 0x75, // 等价于 baidu
        0x03, 0x63, 0x6f, 0x6d, // 等价于 com
        0x00, // 域名截止符
        0x00, 0x01, // type 类型 A记录 2 字节
        0x00, 0x01  // class 类型 Internet 2 字节
};

我们只需要通过 UDP socket 将这个字节数组发送到任意 DNS 服务器的 53 端口,就能收到相应的回复,从而完成一次最基础的 DNS 查询。

Wireshark抓取的DNS请求报文C数组格式

看到头部里有一个“问题数量”的字段,我产生了一个大胆的想法:能不能在一个 DNS 请求包里,同时查询多个域名呢?初衷是为了提升效率,毕竟 12 字节的头部是固定开销,一次查询多个域名似乎能减少网络往返。

于是我构造了下面这个负载,试图同时查询 www.baidu.comwww.taobao.com 的 A 记录:

uint8_t qry[] = {
        0xde, 0xad, // Transaction Id 随机值
        0x01, 0x00, // Flags
        0x00, 0x02, // Questions 请求数量 2
        0x00, 0x00, // Anwser RRs
        0x00, 0x00, // Authority RRs
        0x00, 0x00, // Additional RRs
        0x03, 'w', 'w', 'w',
        0x05, 'b', 'a', 'i', 'd', 'u',
        0x03, 'c', 'o', 'm',
        0x00, // 域名截止符
        0x00, 0x01, // type 类型 A记录 2 字节
        0x00, 0x01, // class 类型 Internet 2 字节
        0x03, 'w', 'w', 'w',
        0x06, 't', 'a', 'o', 'b', 'a', 'o',
        0x03, 'c', 'o', 'm',
        0x00,
        0x00, 0x01, // type 类型 A记录 2 字节
        0x00, 0x01  // class 类型 Internet 2 字节
};

但实际抓包发现,DNS 服务器直接无视了我的这个“复合”请求。

Wireshark抓包显示多域名查询未获响应

我在 StackOverflow 上找到了一个相关讨论。其中一个回答我觉得很有道理:DNS 响应的头部 Flags 字段(如 AD、CD、rcode)是针对整个响应的状态标识。如果一次请求查询多个域名,而其中部分成功、部分失败,或者它们来自不同的权威服务器,这个统一的 Flags 字段将无法准确、无歧义地反映每个域名的具体查询状态。这很可能就是为什么现行 DNS 协议规范和实践都倾向于“一个请求,一个查询”。自己动手剖析协议细节,总能发现这些设计背后的权衡与考量。




上一篇:一位阿里P4员工的裁员视角:N+1、资产交接长队与职场思考
下一篇:动态公网IP消失后,如何通过TCP打洞与DNS技巧访问家庭内网服务
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-7 17:09 , Processed in 0.613115 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表