9.8 KiB
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 命令测试:
# 清除缓存后测试
dig @127.0.0.1 example.com +norecurse
dig @127.0.0.1 example.com
根本原因已确定
经过详细代码分析,发现问题出在 并行请求模式下的响应处理逻辑缺陷:
问题流程:
-
第一次查询:
-
缓存未命中,向上游服务器发起并行查询
-
代码进入并行请求模式(第 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 错误
-
-
第二次查询:
-
第一次查询的响应已经被缓存(即使 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
}
问题 2:Rcode 判断逻辑不准确
第 1562 行只检查 resp.response.Rcode == dns.RcodeSuccess,但有些 DNS 服务器可能返回:
-
Rcode=Success但Answer 为空(例如需要 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 服务器可能返回截断响应
解决方案:
- 检查上游 DNS 服务器配置
- 尝试更换更可靠的上游 DNS 服务器
- 增加 UDP 缓冲区大小,避免截断
推荐修复顺序
- 立即修复:方案 1(在并行模式下添加 hasValidRecords 检查)- 直接解决问题
- 优化改进:方案 2(改进 Rcode 判断逻辑)- 提高健壮性
- 可选优化:方案 3(增加重试机制)- 进一步提高可靠性
需要查看的关键代码
verifyDNSSEC方法实现forwardDNSRequestWithCache中的 DNSSEC 验证逻辑handleUpstreamRequest中的响应处理- DNSSEC 相关配置和超时设置