Files
dns-server/.trae/documents/修复DNS服务器CNAME处理和NXDOMAIN错误问题.md
2026-01-14 23:08:46 +08:00

6.4 KiB
Raw Blame History

问题分析

通过深入分析代码,我找到了导致所有查询都显示同一个 NXDOMAIN 错误的根本原因:

核心问题mergeResponses 函数在合并多个 DNS 响应时,没有正确处理 Rcode 字段

具体原因

  1. 当使用并行查询模式时DNS 服务器会向多个上游服务器发送请求
  2. 函数使用第一个响应作为基础来合并其他响应
  3. 它清空了 AnswerNsExtra 字段,但保留了第一个响应的 Rcode
  4. 如果第一个响应返回 NXDOMAIN比如对于恶意域名 www.evilsnssdk.com那么合并后的响应也会保持 NXDOMAIN 状态,即使其他响应返回成功
  5. 这导致所有查询都显示同一个 NXDOMAIN 错误

修复方案

修复 mergeResponses 函数

关键修改点

  1. 重置 Rcode:在合并响应前,将 Rcode 重置为成功状态
  2. 处理 NXDOMAIN:只有当所有响应都是 NXDOMAIN 时,才返回 NXDOMAIN
  3. 优先使用成功响应:如果有任何响应返回成功,就使用成功的 Rcode

修复步骤

  1. 修改 mergeResponses 函数 (/root/dns/dns/server.go:842-933)

    • 在合并记录前,将 mergedResponse.Rcode 设置为 dns.RcodeSuccess
    • 添加变量 allNXDOMAIN 来跟踪是否所有响应都是 NXDOMAIN
    • 遍历所有响应,检查是否有成功响应
    • 如果所有响应都是 NXDOMAIN才将 mergedResponse.Rcode 设置为 dns.RcodeNameError
  2. 优化合并逻辑

    • 确保优先使用成功响应中的记录
    • 避免将 NXDOMAIN 响应的记录合并到成功响应中
    • 保持响应的一致性Rcode 与记录内容匹配

修复代码

// mergeResponses 合并多个DNS响应
func mergeResponses(responses []*dns.Msg) *dns.Msg {
    if len(responses) == 0 {
        return nil
    }

    // 如果只有一个响应,直接返回,避免不必要的合并操作
    if len(responses) == 1 {
        return responses[0].Copy()
    }

    // 使用第一个响应作为基础
    mergedResponse := responses[0].Copy()
    mergedResponse.Answer = []dns.RR{}
    mergedResponse.Ns = []dns.RR{}
    mergedResponse.Extra = []dns.RR{}
    
    // 重置Rcode为成功除非所有响应都是NXDOMAIN
    mergedResponse.Rcode = dns.RcodeSuccess
    
    // 检查是否所有响应都是NXDOMAIN
    allNXDOMAIN := true
    
    // 收集所有成功响应的记录
    for _, resp := range responses {
        if resp == nil {
            continue
        }
        
        // 如果有任何响应是成功的就不是allNXDOMAIN
        if resp.Rcode == dns.RcodeSuccess {
            allNXDOMAIN = false
        }
    }
    
    // 如果所有响应都是NXDOMAIN设置合并响应为NXDOMAIN
    if allNXDOMAIN {
        mergedResponse.Rcode = dns.RcodeNameError
    }

    // 使用map存储唯一记录选择最长TTL
    // 预分配map容量减少扩容开销
    answerMap := make(map[recordKey]dns.RR, len(responses[0].Answer)*len(responses))
    nsMap := make(map[recordKey]dns.RR, len(responses[0].Ns)*len(responses))
    extraMap := make(map[recordKey]dns.RR, len(responses[0].Extra)*len(responses))

    for _, resp := range responses {
        if resp == nil {
            continue
        }
        
        // 只合并与最终Rcode匹配的响应记录
        if (mergedResponse.Rcode == dns.RcodeSuccess && resp.Rcode == dns.RcodeSuccess) ||
           (mergedResponse.Rcode == dns.RcodeNameError && resp.Rcode == dns.RcodeNameError) {
            
            // 合并Answer部分
            for _, rr := range resp.Answer {
                key := getRecordKey(rr)
                if existing, exists := answerMap[key]; exists {
                    // 如果存在相同记录选择TTL更长的
                    if rr.Header().Ttl > existing.Header().Ttl {
                        answerMap[key] = rr
                    }
                } else {
                    answerMap[key] = rr
                }
            }

            // 合并Ns部分
            for _, rr := range resp.Ns {
                key := getRecordKey(rr)
                if existing, exists := nsMap[key]; exists {
                    // 如果存在相同记录选择TTL更长的
                    if rr.Header().Ttl > existing.Header().Ttl {
                        nsMap[key] = rr
                    }
                } else {
                    nsMap[key] = rr
                }
            }

            // 合并Extra部分
            for _, rr := range resp.Extra {
                // 跳过OPT记录避免重复
                if rr.Header().Rrtype == dns.TypeOPT {
                    continue
                }
                key := getRecordKey(rr)
                if existing, exists := extraMap[key]; exists {
                    // 如果存在相同记录选择TTL更长的
                    if rr.Header().Ttl > existing.Header().Ttl {
                        extraMap[key] = rr
                    }
                } else {
                    extraMap[key] = rr
                }
            }
        }
    }

    // 预分配切片容量,减少扩容开销
    mergedResponse.Answer = make([]dns.RR, 0, len(answerMap))
    mergedResponse.Ns = make([]dns.RR, 0, len(nsMap))
    mergedResponse.Extra = make([]dns.RR, 0, len(extraMap))

    // 将map转换回切片
    for _, rr := range answerMap {
        mergedResponse.Answer = append(mergedResponse.Answer, rr)
    }

    for _, rr := range nsMap {
        mergedResponse.Ns = append(mergedResponse.Ns, rr)
    }

    for _, rr := range extraMap {
        mergedResponse.Extra = append(mergedResponse.Extra, rr)
    }

    return mergedResponse
}```

## 预期效果

修复后DNS服务器将能够
- 正确合并多个 DNS 响应
- 确保 Rcode 与实际记录内容匹配
- 只有当所有响应都是 NXDOMAIN 时才返回 NXDOMAIN
- 避免单个恶意域名影响所有查询结果
- 正确显示各个域名的查询结果

## 修复代码位置

- **核心修改文件**`/root/dns/dns/server.go`
- **关键函数**`mergeResponses`
- **修改内容**修复响应合并逻辑正确处理 Rcode 字段

## 测试方法

1. 使用 nslookup 测试不同域名
2. 检查是否每个域名都显示正确的查询结果
3. 验证 www.evilsnssdk.com 返回 NXDOMAIN而其他域名返回成功
4. 检查日志中是否还有大量错误信息

这个修复将彻底解决所有查询都显示同一个 NXDOMAIN 错误的问题