如何在Golang中制作一个简单的本地DNS转发服务器 Go语言DNS协议处理

3次阅读

用 miekg/dns 实现 dns 转发需分别启动 udp/tcp 两个 dns.server,handler 中检查 rd 标志、手动处理 edns0/tsig、用 dns.client 控制超时重试,并开发时监听非特权端口(如 :5353)避权限与系统 resolver 干扰。

如何在Golang中制作一个简单的本地DNS转发服务器 Go语言DNS协议处理

miekg/dns 库解析和转发 DNS 请求最简路径

go 本身不带标准 DNS 服务器实现,得靠第三方库;miekg/dns 是事实标准,轻量、稳定、协议层控制力强。别碰 net/http 那套思路——DNS 是 UDP/TCP 混合协议,不是 HTTP 请求-响应模型。

实操建议:

立即学习go语言免费学习笔记(深入)”;

  • dns.Server 启动监听,显式指定 Net: "udp""tcp",别依赖默认值(UDP 默认端口 53,但开发时通常要 sudo)
  • 注册 HandlerFunc 处理 *dns.Msg,核心逻辑就三步:解析 msg.Question[0].Name → 构造新查询 → 调用 dns.Exchange() 转发
  • 务必检查 msg.RecursionDesired:客户端没设 RD=1,你强行转发可能被上游拒绝(比如某些企业 DNS 只响应递归请求)
  • 示例关键片段:
    srv := &dns.Server{Addr: ":5353", Net: "udp", Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {     if len(r.Question) == 0 { return }     q := r.Question[0]     upstream := new(dns.Msg)     upstream.SetQuestion(q.Name, q.Qtype)     upstream.RecursionDesired = true     resp, err := dns.Exchange(upstream, "8.8.8.8:53")     if err != nil { w.WriteMsg(new(dns.Msg)); return }     w.WriteMsg(resp) })}

DNS UDP 和 TCP 转发必须分开处理

DNS 查询超 512 字节或涉及区域传输时会降级到 TCP;但 dns.ServerNet 字段只支持单协议启动。混用会导致丢包、超时、客户端重试——尤其在转发 EDNS0 扩展(如 DNSSEC、大型 TXT 记录)时更明显。

实操建议:

立即学习go语言免费学习笔记(深入)”;

  • UDP 服务监听 :5353,TCP 服务另起一个 dns.Server 监听相同地址但 Net: "tcp"
  • TCP handler 中必须调用 w.LocalAddr() 判断是否真走 TCP(有些系统 UDP 会伪造 TCP 连接),避免误判
  • 转发前检查 r.IsTsig():带 TSIG 签名的请求不能直接透传,需先验证或剥离(否则上游拒绝)
  • UDP handler 里加 if len(r.Bytes()) > 512 { /* drop or log */ },防止缓冲区溢出(dns.Msg 解析不校验长度)

dns.Exchange() 超时和重试必须手动控制

默认 dns.Exchange() 没超时,卡死在上游无响应时整个 handler goroutine 就挂住;而且它不自动重试,一次失败就返回空响应,客户端看到 SERVFAIL。

实操建议:

立即学习go语言免费学习笔记(深入)”;

  • dns.Client 替代裸 dns.Exchange(),设置 TimeoutNet: "udp"(TCP 同理)
  • 重试逻辑写在 handler 内:最多 2 次,间隔 100ms,用 time.AfterFunc 控制,别用 for range time.Tick(goroutine 泄漏)
  • 注意 dns.ClientSingleInflight 字段:设为 true 可防同一域名并发重复查询(适合缓存场景,但转发服务器通常关掉)
  • 错误判断优先看 err != nil,其次看 resp.Rcode == dns.RcodeServerFailure,别只依赖 err

本地测试时绕过系统 DNS 缓存和权限限制

linux/macos 下绑定 53 端口需 root,但开发调试不该总开 sudo;另外 macOS 的 mDNSResponder、systemd-resolved 会劫持 53 流量,导致你 localhost:5353 根本收不到包。

实操建议:

立即学习go语言免费学习笔记(深入)”;

  • 开发时监听 :5353,用 dig @127.0.0.1 -p 5353 google.com 显式指定端口,避开系统 resolver
  • macOS 上临时停用 mDNSResponder:sudo killall -9 mDNSResponder(重启后恢复)
  • Linux 上确认没启用 systemd-resolvedsudo systemctl is-active systemd-resolved,若 active 则 sudo systemctl stop systemd-resolved
  • Windows 用户直接改网卡 DNS 为 127.0.0.1 即可,但记得测试完切回去,否则全网断 DNS

真正麻烦的是 EDNS0 选项透传和 TSIG 处理——这两项不手动解析和重建,上游会直接拒绝;而 miekg/dnsMsg.copy() 不复制 EDNS0 记录,得自己遍历 msg.Extra 提取并附加到新消息里。

text=ZqhQzanResources