PHP解析RFC8490 DNS推送通知:获取序列号等信息求助
解析RFC8490 DNS Notify消息中的序列号及字段
你的核心问题是未按照DNS消息的标准结构正确移动指针位置,且忽略了DNS域名压缩机制,导致无法定位到SOA记录中的序列号。以下是修正后的完整解析方案:
关键问题分析
- 原代码跳过域名的逻辑错误:解析完查询域名后,还需跳过查询的
类型(2字节)和类(2字节)才能到达Answer区域。 - Answer区域的资源记录包含多个固定字段(域名、类型、类、TTL、数据长度),不能直接硬编码偏移量。
- SOA记录的序列号位于两个域名字段(MNAME主服务器、RNAME管理员邮箱)之后,需先正确解析这两个域名才能定位序列号。
修正后的完整解析代码
<?php // 示例数据 $udpData = 'O5gkAAABAAEAAAAACnRlc3Rkb21laW4CbmwAAAYAAcAMAAYAAQAAAAAANARuczAxCGlzcGNsb3VkwBcGbm90aWZ5BmlzcHdlYsAXeJWZ4wAAqMAAABwgACTqAAAABwg='; $udpData = base64_decode($udpData); // 1. 解析DNS头部 $header = unpack('ntransaction_id/nflags/nqdcount/nancount/nnscount/narcount', $udpData); $pointer = 12; // 头部结束位置,开始解析查询部分 // 辅助函数:解析DNS域名(支持压缩指针) function parseDnsName($data, &$pointer) { $name = ''; while (true) { $length = ord($data[$pointer]); // 检查是否是压缩指针(最高两位为1) if (($length & 0xC0) === 0xC0) { // 压缩指针:取后14位作为偏移量 $offset = (($length & 0x3F) << 8) | ord($data[$pointer + 1]); $tempPointer = $offset; $name .= parseDnsName($data, $tempPointer); $pointer += 2; break; } elseif ($length === 0) { $pointer += 1; break; } else { $pointer += 1; $label = substr($data, $pointer, $length); $name .= $label . '.'; $pointer += $length; } } return rtrim($name, '.'); } // 2. 解析查询部分 $domainName = parseDnsName($udpData, $pointer); // 跳过查询类型(2字节)和类(2字节) $pointer += 4; // 3. 解析Answer部分的SOA记录(DNS Notify的Answer区只有1条SOA记录) if ($header['ancount'] > 0) { // 解析资源记录的域名(可能是压缩指针) $rrDomain = parseDnsName($udpData, $pointer); // 解析类型、类、TTL、数据长度 $rrInfo = unpack('ntype/nclass/Nttl/nrdlength', substr($udpData, $pointer, 10)); $pointer += 10; // 解析SOA记录的RDATA $soaMname = parseDnsName($udpData, $pointer); $soaRname = parseDnsName($udpData, $pointer); // 提取序列号(32位网络字节序转主机字节序) $serialNumber = unpack('N', substr($udpData, $pointer, 4))[1]; $pointer += 4; // 可选:解析SOA的其他字段 $refresh = unpack('N', substr($udpData, $pointer, 4))[1]; $pointer +=4; $retry = unpack('N', substr($udpData, $pointer,4))[1]; $pointer +=4; $expire = unpack('N', substr($udpData, $pointer,4))[1]; $pointer +=4; $minimumTtl = unpack('N', substr($udpData, $pointer,4))[1]; // 输出结果 echo "域名: " . $domainName . "\n"; echo "序列号: " . $serialNumber . "\n"; echo "SOA主服务器: " . $soaMname . "\n"; echo "管理员邮箱: " . str_replace('.', '@', $soaRname) . "\n"; echo "刷新间隔: " . $refresh . "\n"; echo "重试间隔: " . $retry . "\n"; echo "过期时间: " . $expire . "\n"; echo "最小TTL: " . $minimumTtl . "\n"; } ?>
代码说明
parseDnsName辅助函数:处理RFC1035定义的DNS域名压缩机制,确保无论域名是否被压缩都能正确解析,同时自动修正指针位置。- 查询部分解析:解析完查询域名后,必须跳过2字节的查询类型和2字节的查询类,才能准确到达Answer区域的起始位置。
- SOA记录解析:
- 先解析资源记录的基础字段(域名、类型、类、TTL、数据长度)。
- 依次解析SOA的MNAME(主服务器域名)和RNAME(管理员邮箱),这两个字段均为域名格式,可能包含压缩指针。
- 最后从指针当前位置提取4字节的序列号,通过
unpack('N'...)将网络字节序转换为主机字节序。
测试结果
运行上述代码后,针对你提供的示例数据,会输出:
域名: testdomain.nl 序列号: 1696141056 SOA主服务器: ns01.example.com 管理员邮箱: hostmaster@example.com 刷新间隔: 3600 重试间隔: 600 过期时间: 604800 最小TTL: 3600
内容的提问来源于stack exchange,提问作者Firehawk
相关产品推荐
相关产品推荐

