Files
dns-server/.trae/documents/dns-servfail-issue-analysis.md
T
Alex Yang f9e2e5a6bc update
2026-04-12 21:40:22 +08:00

9.8 KiB
Raw Blame History

DNS 查询 SERVFAIL 问题分析(已禁用 DNSSEC

问题描述

客户端请求 DNS 解析时,需要请求两次才能得到正常结果:

  • 第一次请求:返回 SERVFAIL(服务器失败)

  • 第二次请求:返回正常解析结果

重要信息

DNSSEC 已禁用 - 因此问题与 DNSSEC 验证无关

可能的原因分析

1. 缓存初始化时机问题

现象:第一次查询时缓存为空,第二次查询命中缓存

可能原因

  • 缓存加载逻辑可能存在问题

  • 第一次查询的响应没有正确缓存

  • 缓存键生成或查找逻辑有问题

相关代码位置

  • /root/dns-server/dns/cache.go - GetSet 方法

  • /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 命令测试:

# 清除缓存后测试
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=SuccessAnswer 为空 的响应(例如某些 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 分支):

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 检查:

// 检查响应是否包含有效的记录,如果包含,将 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
}

问题 2Rcode 判断逻辑不准确

第 1562 行只检查 resp.response.Rcode == dns.RcodeSuccess,但有些 DNS 服务器可能返回:

  • Rcode=SuccessAnswer 为空(例如需要 TCP 回退时)

  • 这种情况下应该继续等待其他服务器的响应,而不是立即返回

建议的修复步骤

方案 1:在并行模式下添加 hasValidRecords 检查(推荐)

问题:并行模式(noDNSSEC 分支)缺少 hasValidRecords 检查

解决方案: 在并行模式的快速返回逻辑中添加 hasValidRecords 检查,确保只返回包含有效记录的响应

// 修改前
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 检查结合起来,确保响应真正有效

// 修改前
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 时,继续等待其他服务器的响应或重试

// 在并行模式中添加重试逻辑
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 相关配置和超时设置