dns响应解析需手动逐字段提取并转换字节序,不可直接memcpy;qname需按label链表和压缩指针(0xc0开头)规则解析;udp包长以recvfrom返回值为准,非固定512字节。

DNS响应包结构不匹配导致解析失败
直接用 memcpy 或 reinterpret_cast 把 UDP 数据包丢进结构体,十有八九会崩——不是字段错位,就是字节序翻车。DNS 协议头前 12 字节是固定格式,但 C++ 结构体默认对齐、填充不可控,且 x86 是小端,DNS 所有整数字段(如 id、flags、qdcount)都是网络字节序(大端)。必须手动逐字段提取,不能依赖内存布局。
- 用
ntohs()/ntohl()转换所有 16/32 位整数字段,id、flags、ancount等一个都不能漏 - 跳过结构体定义,直接用指针偏移 +
const uint8_t*遍历:头 12 字节后是问题部分(QNAME),它不是固定长度,需按 DNS 压缩格式边读边跳 - 别信“包长=512”的说法,EDNS0 可能扩展到 4096,UDP 包实际长度得看
recvfrom返回值,不是硬编码
QNAME 解析时遇到压缩指针就卡住
DNS 名称字段(QNAME、NAME)用 label 链表表示,每个 label 以长度字节开头,以 0 结尾;但若某处出现 0xc0 开头的二字节(如 0xc0 0x0c),这就是压缩指针,指向前面某处的 label 起始位置。很多人只处理了普通 label,一见 0xc0 就当非法字符跳过,结果后续字段全乱。
- 解析 name 时,每读一个字节,先判断是否
(b & 0xc0) == 0xc0:若是,取下一位组成 offset(((b & 0x3f) ),然后跳回该 offset 继续读 - 必须维护原始 buffer 起始地址和当前读位置,压缩指针是相对于整个 packet 起始的绝对偏移,不是相对当前位置
- 递归解析要设深度限制(比如 10 层),避免恶意构造的循环指针导致栈溢出或无限循环
RR(资源记录)解析中 RDATA 长度与类型强相关
RDLENGTH 字段告诉你 RDATA 占多少字节,但光靠这个不够——RDATA 内容格式完全由 TYPE 决定。比如 TYPE=A 是 4 字节 IPv4 地址,TYPE=AAAA 是 16 字节,TYPE=CNAME 或 TYPE=PTR 又是变长 domain name。直接 memcpy 出来当字符串用,遇到 A 记录就崩。
- 先用
ntohs()读TYPE,再分支处理:if (type == 1) { /* A */ }、else if (type == 28) { /* AAAA */ } - 对 name 类型(CNAME、MX、SRV 的 target 字段等),必须再次调用 QNAME 解析逻辑,不能假设以 \0 结尾——DNS name 本身不含 \0,结尾是长度字节 0
- MX 记录的
preference字段在 RDATA 开头,2 字节 network byte order,必须先ntohs(),再跳过这 2 字节去解析后续 domain name
std::string 存 domain name 容易丢数据
把 DNS name(比如 \x03www\x06github\x03com\x00)塞进 std::string,遇到中间的 \x00 就被截断,因为 std::string 构造函数默认按 C 字符串处理。更隐蔽的问题是:有些 name 结尾没 \x00(比如压缩指针结尾),但你用 c_str() 输出日志,就会把后面内存全打出来。
- 存 name 用
std::vector<uint8_t></uint8_t>或std::string_view(C++17),构造时显式传 length,例如std::string_view(data, len) - 转可读域名时,写个专用函数:遍历每个 label,跳过长度字节,拷贝 label 内容,label 间加 '.',最后去掉末尾多余的 '.'
- 调试时用
printf("name len=%zu, hex=", name.size()); for (auto b : name) printf("%02x ", b);,别依赖cout
解析 DNS 响应真正的麻烦不在解包,而在 name 的嵌套跳转和 type-specific RDATA 处理——一个没处理好的压缩指针,能让整个 RR 解析链偏移错位,后续所有字段都不可信。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











