
## 一、背景原因

Goto解析Google的域名时不稳定，经与Google沟通，对方建议开启DNS ECS。
> ECS指的是EDNS Client Subnet, 定义在RFC7871，作用是让权威DNS服务器感知最终用户大概位置，从而返回最近的IP

Goto希望腾讯云的PrivateDNS支持此功能，考虑到对用户的业务至关重要且属于标准DNS协议的部分，域名团队接受这个需求并开发。

其它背景信息：
1. GCP不支持此功能，猜测是Google会对GCP的来源访问做稳定性保障
2. AWS、阿里云均不支持此功能
## 二、用户访问路径

具体GoTo的解析场景如下：

* 用户配置了一个内网域名 M，其 CNAME 指向另一个内网域名 N；
* 用户未使用ipv6，域名 N 仅配置了 A 记录，未配置 AAAA 记录；
* 当客户端请求域名 M 的 AAAA 记录时，PrivateDNS 会尝试递归查询域名 N 的 AAAA 记录；
* 由于域名 N 在公网侧不存在对应的 AAAA 解析结果，该版本在组装携带 ECS 信息的返回报文时产生异常，形成格式错误的响应包。

![[Pasted image 20260115100609.png]]
## 三、问题原因

域名产品开发人员在实现这个需求时，引入了一个bug。即当ipv6的AAAA记录不存在时，回包的addtional info里的内容填写错误。用户所在容器集群的CoreDNS服务器检测到这个错误之后反复重试，导致客户端超时，从而严重影响用户业务。

为什么这个bug在CoreDNS上才会出现？类比来看，如果把DNS比作HTTP服务，这次的回包应该是"404 Not Found"，但是因为字段填错，变成了"404 Not Found (xxxx)", Go语言的底层DNS协议实现会严格校验并报错，然后CoreDNS处理不当，在程序中无限循环请求，导致问题出现。在其它场景下，大部分请求方看到404就正常处理了，或者是报错之后给客户端返回错误，客户端同时获得了ipv4的解析结果，不会影响正常业务。

> CoreDNS相关的问题较多，历史上也曾出现过其它问题导致的超时，在社区实践中一般把max_connect_attempts设置为1到3，防止无限循环
## 四、如何解决问题

短期在CoreDNS里禁用ipv6或者设置max_connect_attempts即可解决问题，同时PrivateDNS版本回滚。

长期有以下几个工作：

1. 修复PrivateDNS中ECS实现的问题
2. 提供CoreDNS使用的建议
3. 增加PrivateDNS后面对接CoreDNS的测试

## 五、附录-CoreDNS分析


 在connect.go:136-159中，当ReadMsg()遇到错误时：

  ret, err = pc.c.ReadMsg()
  if err != nil {
      if ret != nil && ... && shouldTruncateResponse(err) {
          // 只有overflow错误才会截断处理
          ret = truncateResponse(ret)
          break
      }

      pc.c.Close() // 关闭连接
      if ret != nil {
          ret.Id = originId
      }
      return ret, err  // 返回错误！
  }


关键点：如果Additional section格式错误导致的解析错误不是overflow类型，会直接返回错误。

  现在查看forward plugin如何处理这个错误。

  问题根源

  当privatedns返回Additional section格式错误的包时，会触发以下流程：

  1. 错误返回路径（connect.go:136-159）
  ret, err = pc.c.ReadMsg()
  if err != nil {
      // Additional section格式错误不是overflow，不会被截断处理
      if shouldTruncateResponse(err) { ... } // 不满足
      pc.c.Close()// 关闭连接
      return ret, err  // 返回错误到forward plugin
  }

  2. 重试循环陷阱（forward.go:192-210）
  if err != nil {
      proxy.Healthcheck()  // 触发健康检查（异步）

      if f.maxConnectAttempts > 0 {
          connectAttempts++
          // 默认为0，不会break}

      if fails < len(f.proxies) {// 0 < 1 = true
          continue  // 重试！
      }
      break
  }

  3. 循环重置机制（forward.go:127-131）
  if i >= len(list) {
      i = 0
      fails = 0  // 重置fails计数！
  }

  为什么会不断重试

  在您的场景中（只有一个privatedns）：

  4. 第1次请求：privatedns返回格式错误 → err != nil
  5. 检查重试条件：fails < len(f.proxies) → 0 < 1 = true
  6. 继续循环：i++ → i=1 → i >= len(list) → 重置i=0, fails=0
  7. 第2次请求：再次请求privatedns → 再次失败
  8. 无限循环：重复步骤2-4，直到5秒deadline

  关键问题

  fails计数器永远不会增加，因为：
  - 只有在proxy.Down()为true时才增加fails（line 136）
  - 但健康检查是异步的，在当前请求循环中可能还没生效
  - 默认maxfails=2，需要2次失败才标记为Down
  - 在此期间，会持续重试

  重试持续时间

  - 默认deadline：5秒（line 122: defaultTimeout）
  - 实际重试次数：取决于每次请求的延迟
  - 如果每次请求100ms，5秒内可能重试50次！

CoreDNS社区讨论汇总

  第一部分：核心问题识别

  社区已经识别出多个类似的问题：

  1. Issue #5953 - 非合规上游的弹性问题（最相关）

  问题描述：
  - 当上游DNS服务器返回超过512字节的UDP响应但不设置截断标志时
  - CoreDNS遇到"dns: overflowing header size"错误
  - 关键发现：CoreDNS会"无助地重试5秒钟"

  影响场景：
  - Windows 2016/2019 AD DC集成DNS服务器
  - 任何不符合RFC规范的DNS服务器

  社区讨论焦点：
  - 是否应该在UDP负载失败后自动使用TCP重试
  - 如何提高对非合规上游的容错能力

  状态：2023年3月提出，CoreDNS 1.10.0版本

  来源：https://github.com/coredns/coredns/issues/5953

  ---
  2. Issue #4183 - 截断响应处理问题

  问题描述：
  - 当客户端通过UDP发送请求且未配置force_tcp或prefer_udp时
  - 上游返回截断响应，CoreDNS不会自动通过TCP重试
  - 也不会在响应中设置TC标志

  根本原因：
  - 默认配置下缺少自动TCP重试机制
  - 需要显式配置prefer_udp才能启用UDP→TCP重试

  解决方案：
  - 使用prefer_udp配置选项
  - 或使用force_tcp强制所有请求使用TCP

  来源：https://github.com/coredns/coredns/issues/4183

  ---
  第二部分：重试机制的设计问题

  3. max_connect_attempts的默认值问题

  官方文档说明：
  - max_connect_attempts限制单个请求的上游连接尝试总数
  - 默认值为0，表示无限制

  问题所在：
  - 单上游场景下，默认值0会导致无限重试
  - 在5秒deadline内可能重试数十次
  - 浪费资源且导致客户端超时

  社区认知：
  - 文档中提到此参数，但未强调默认值的风险
  - 缺少针对单上游场景的最佳实践建议

  来源：https://coredns.io/plugins/forward/、https://github.com/coredns/coredns/blob/master/plugin/forward/README.md

  ---
  4. Issue #1899 & #2636 - FORMERR错误处理

  问题描述：
  - Forward插件在高查询速率下返回FORMERR
  - 特别是当传入QPS高于上游服务器总QPS时

  相关发现：
  - EDNS查询格式问题导致FORMERR
  - 某些上游无法正确处理EDNS扩展

  解决方案：
  - Issue #2636通过PR #2637修复
  - Kuma项目的修复：确保FORMERR和NOTIMP响应会重试到原始DNS服务器

  来源：https://github.com/coredns/coredns/issues/1899、https://github.com/coredns/coredns/issues/2636

  ---
  第三部分：社区的解决方案和建议

  生产环境的常见解决方案

  1. Red Hat/OpenShift的建议：
  - 在Azure Red Hat OpenShift集群中遇到CoreDNS超时问题
  - 建议配置合理的超时和重试参数
  - 监控DNS查询延迟和失败率

  来源：https://access.redhat.com/solutions/7015390

  2. AWS EKS的最佳实践：
  - 排查DNS故障时建议检查CoreDNS配置
  - 确保上游DNS服务器的可用性
  - 考虑使用多个上游服务器

  来源：https://repost.aws/knowledge-center/eks-dns-failure

  3. Alibaba Cloud的DNS最佳实践：
  - 建议配置多个上游DNS服务器
  - 使用健康检查机制
  - 合理设置超时参数

  来源：https://www.alibabacloud.com/help/en/ack/ack-managed-and-ack-dedicated/user-guide/dns-best-practice

  ---
  第四部分：社区共识与未解决的问题

  社区共识

  1. 重试逻辑存在设计缺陷：
    - 单上游场景下的行为不够优雅
    - 默认值（max_connect_attempts=0）过于激进
    - 缺少对错误类型的区分
  2. 需要显式配置才能获得合理行为：
    - 必须手动设置max_connect_attempts
    - 必须配置prefer_udp或force_tcp处理截断
    - 默认配置不适合生产环境
  3. 非合规上游是常见问题：
    - Windows AD DNS服务器
    - 某些企业内部DNS实现
    - 需要更好的容错机制

  仍未完全解决的问题

  4. 格式错误的快速失败机制：
    - 社区尚未实现对持久格式错误的快速失败
    - 仍然依赖5秒deadline和健康检查
  5. 单上游场景的优化：
    - 没有针对单上游场景的特殊处理
    - fails计数器逻辑不适合单上游
  6. 错误类型区分：
    - 未区分临时网络错误和持久格式错误
    - 所有错误都触发相同的重试逻辑

  