# DNS 查询 SERVFAIL 问题分析(已禁用 DNSSEC) ## 问题描述 客户端请求 DNS 解析时,需要请求两次才能得到正常结果: * 第一次请求:返回 SERVFAIL(服务器失败) * 第二次请求:返回正常解析结果 ## 重要信息 **DNSSEC 已禁用** - 因此问题与 DNSSEC 验证无关 ## 可能的原因分析 ### 1. 缓存初始化时机问题 **现象**:第一次查询时缓存为空,第二次查询命中缓存 **可能原因**: * 缓存加载逻辑可能存在问题 * 第一次查询的响应没有正确缓存 * 缓存键生成或查找逻辑有问题 **相关代码位置**: * `/root/dns-server/dns/cache.go` - `Get` 和 `Set` 方法 * `/root/dns-server/dns/server.go` - `handleCacheResponse` 方法 ### 2. DNSSEC 验证超时或失败 **现象**:第一次查询触发 DNSSEC 验证,验证超时导致 SERVFAIL;第二次直接使用缓存结果 **可能原因**: * DNSSEC 验证逻辑可能存在超时 * 验证失败后没有正确处理响应 * `verifyDNSSEC` 方法可能阻塞或失败 **相关代码位置**: * `/root/dns-server/dns/server.go` - `verifyDNSSEC` 方法 * 第 1563 行左右的 DNSSEC 验证逻辑 ### 3. 上游服务器选择逻辑问题 **现象**:第一次查询时服务器状态统计为空,导致选择了不可用的服务器 **可能原因**: * 服务器健康检查逻辑可能有问题 * 第一次查询时没有正确选择最快的服务器 * 并行查询逻辑可能存在竞态条件 **相关代码位置**: * `/root/dns-server/dns/server.go` - `forwardDNSRequestWithCache` 方法 * 第 1473-1600 行的并行查询逻辑 ### 4. 响应处理逻辑问题 **现象**:第一次查询返回的响应被错误地判断为失败 **可能原因**: * 响应代码判断逻辑可能有误 * `hasValidRecords` 判断可能过于严格 * 某些类型的响应被错误地拒绝 **相关代码位置**: * `/root/dns-server/dns/server.go` - 第 871-891 行的响应验证逻辑 ### 5. 并发锁竞争问题 **现象**:第一次查询时遇到锁竞争,导致超时返回 SERVFAIL **可能原因**: * 缓存访问锁可能与其他操作冲突 * 统计信息更新可能导致阻塞 * 日志写入可能阻塞查询流程 **相关代码位置**: * `/root/dns-server/dns/cache.go` - 各种锁操作 * `/root/dns-server/dns/server.go` - 统计更新操作 ## 调试建议 ### 方案 1:增强日志记录 在关键位置添加详细日志: * 缓存命中/未命中时 * DNSSEC 验证开始/结束/结果 * 上游服务器选择和响应 * 响应验证过程 ### 方案 2:检查时间戳 分析查询日志中的时间戳: * 第一次查询的实际处理时间 * 是否有超时现象 * DNSSEC 验证耗时 ### 方案 3:复现测试 使用 `dig` 命令测试: ```bash # 清除缓存后测试 dig @127.0.0.1 example.com +norecurse dig @127.0.0.1 example.com ``` ## 根本原因已确定 经过详细代码分析,发现问题出在 **并行请求模式下的响应处理逻辑缺陷**: ### 问题流程: 1. **第一次查询**: * 缓存未命中,向上游服务器发起并行查询 * 代码进入并行请求模式(第 1490-1777 行) * 收到上游返回的响应,`resp.response.Rcode == dns.RcodeSuccess` * **关键问题**:在 `noDNSSEC` 模式下(第 1572-1588 行),代码直接返回第一个成功响应 * **但是**:没有检查响应是否包含有效记录(Answer/Ns/Extra) * 如果上游返回的是 `Rcode=Success` 但 `Answer 为空` 的响应(例如某些 DNS 服务器的截断响应) * 该响应被直接发送给客户端 * 在 `handleUpstreamRequest` 第 870-891 行,由于 `hasValidRecords` 检查失败(Answer 为空),`response.Rcode` 没有被设置为 `RcodeSuccess` * 最终客户端收到 SERVFAIL 错误 2. **第二次查询**: * 第一次查询的响应已经被缓存(即使 Answer 为空,只要 Rcode=Success 就会被缓存) * 直接从缓存返回 * 但此时缓存的可能是完整响应(因为上游第二次返回了完整结果) * 客户端收到正常解析结果 ### 具体代码问题: **问题 1:并行模式下缺少 hasValidRecords 检查** 在 `/root/dns-server/dns/server.go` 第 1572-1588 行(并行模式,noDNSSEC 分支): ```go if noDNSSEC && !bestResponseSent { // 不验证 DNSSEC 的域名:直接返回第一个成功响应 if fastestResponse == nil || resp.rtt < fastestRtt { fastestResponse = resp.response fastestRtt = resp.rtt fastestServer = resp.server fastestDnssecServer = dnssecServerForResponse fastestHasDnssec = false // 立即发送结果,快速返回(只发送一次) resultChan <- struct { response: fastestResponse, // 可能 Answer 为空! ... } bestResponseSent = true } } ``` **对比**:在串行模式(第 1452-1469 行)中有 `hasValidRecords` 检查: ```go // 检查响应是否包含有效的记录,如果包含,将 Rcode 设置为成功 hasValidRecords := false if len(response.Answer) > 0 { hasValidRecords = true } else if len(response.Ns) > 0 { hasValidRecords = true } else if len(response.Extra) > 0 { for _, rr := range response.Extra { if rr.Header().Rrtype != dns.TypeOPT { hasValidRecords = true break } } } if hasValidRecords { response.Rcode = dns.RcodeSuccess } ``` **问题 2:Rcode 判断逻辑不准确** 第 1562 行只检查 `resp.response.Rcode == dns.RcodeSuccess`,但有些 DNS 服务器可能返回: * `Rcode=Success` 但 `Answer 为空`(例如需要 TCP 回退时) * 这种情况下应该继续等待其他服务器的响应,而不是立即返回 ## 建议的修复步骤 ### 方案 1:在并行模式下添加 hasValidRecords 检查(推荐) **问题**:并行模式(noDNSSEC 分支)缺少 hasValidRecords 检查 **解决方案**: 在并行模式的快速返回逻辑中添加 hasValidRecords 检查,确保只返回包含有效记录的响应 ```go // 修改前 if noDNSSEC && !bestResponseSent { // 不验证 DNSSEC 的域名:直接返回第一个成功响应 if fastestResponse == nil || resp.rtt < fastestRtt { fastestResponse = resp.response fastestRtt = resp.rtt fastestServer = resp.server fastestDnssecServer = dnssecServerForResponse fastestHasDnssec = false // 立即发送结果,快速返回(只发送一次) resultChan <- struct { response: fastestResponse, ... } bestResponseSent = true } } // 修改后 if noDNSSEC && !bestResponseSent { // 不验证 DNSSEC 的域名:返回第一个包含有效记录的成功响应 if fastestResponse == nil || resp.rtt < fastestRtt { // 检查响应是否包含有效记录 hasValidRecords := false if len(resp.response.Answer) > 0 { hasValidRecords = true } else if len(resp.response.Ns) > 0 { hasValidRecords = true } else if len(resp.response.Extra) > 0 { for _, rr := range resp.response.Extra { if rr.Header().Rrtype != dns.TypeOPT { hasValidRecords = true break } } } // 只有包含有效记录才返回 if hasValidRecords { fastestResponse = resp.response fastestRtt = resp.rtt fastestServer = resp.server fastestDnssecServer = dnssecServerForResponse fastestHasDnssec = false // 立即发送结果,快速返回(只发送一次) resultChan <- struct { response: fastestResponse, ... } bestResponseSent = true } } } ``` ### 方案 2:改进 Rcode 判断逻辑 **问题**:只检查 Rcode,不检查 Answer 内容 **解决方案**: 将 Rcode 检查和 Answer 检查结合起来,确保响应真正有效 ```go // 修改前 if resp.response.Rcode == dns.RcodeSuccess || resp.response.Rcode == dns.RcodeNameError { // 处理响应 } // 修改后 // 检查 Rcode 和 Answer 内容 isValidResponse := false if resp.response.Rcode == dns.RcodeSuccess { // 成功响应需要包含有效记录 if len(resp.response.Answer) > 0 || len(resp.response.Ns) > 0 { isValidResponse = true } } else if resp.response.Rcode == dns.RcodeNameError { // NXDOMAIN 响应可以没有 Answer isValidResponse = true } if isValidResponse { // 处理有效响应 } ``` ### 方案 3:增加重试机制 **问题**:第一次返回空 Answer 后没有重试 **解决方案**: 检测到空 Answer 时,继续等待其他服务器的响应或重试 ```go // 在并行模式中添加重试逻辑 if len(resp.response.Answer) == 0 && resp.response.Rcode == dns.RcodeSuccess { // Answer 为空,可能是截断响应,继续等待其他服务器 logger.Debug("响应 Answer 为空,继续等待其他服务器", "domain", domain) continue // 不立即返回,继续等待 } ``` ### 方案 4:调整上游 DNS 服务器配置 **问题**:某些上游 DNS 服务器可能返回截断响应 **解决方案**: 1. 检查上游 DNS 服务器配置 2. 尝试更换更可靠的上游 DNS 服务器 3. 增加 UDP 缓冲区大小,避免截断 ## 推荐修复顺序 1. **立即修复**:方案 1(在并行模式下添加 hasValidRecords 检查)- 直接解决问题 2. **优化改进**:方案 2(改进 Rcode 判断逻辑)- 提高健壮性 3. **可选优化**:方案 3(增加重试机制)- 进一步提高可靠性 ## 需要查看的关键代码 1. `verifyDNSSEC` 方法实现 2. `forwardDNSRequestWithCache` 中的 DNSSEC 验证逻辑 3. `handleUpstreamRequest` 中的响应处理 4. DNSSEC 相关配置和超时设置