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

用 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.Server 的 Net 字段只支持单协议启动。混用会导致丢包、超时、客户端重试——尤其在转发 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(),设置Timeout和Net: "udp"(TCP 同理) - 重试逻辑写在 handler 内:最多 2 次,间隔 100ms,用
time.AfterFunc控制,别用for range time.Tick(goroutine 泄漏) - 注意
dns.Client的SingleInflight字段:设为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-resolved:sudo systemctl is-active systemd-resolved,若 active 则sudo systemctl stop systemd-resolved - Windows 用户直接改网卡 DNS 为
127.0.0.1即可,但记得测试完切回去,否则全网断 DNS
真正麻烦的是 EDNS0 选项透传和 TSIG 处理——这两项不手动解析和重建,上游会直接拒绝;而 miekg/dns 的 Msg.Copy() 不复制 EDNS0 记录,得自己遍历 msg.Extra 提取并附加到新消息里。










