因为Go标准库无DNS服务端实现,而miekg/dns封装协议细节、支持UDP/TCP监听与响应构造,是Go生态事实标准。

为什么用 miekg/dns 而不是标准库
Go 标准库没有提供 DNS 服务端实现,net 包只支持客户端解析(如 net.LookupHost),没法监听 UDP/TCP 53 端口、构造响应报文或处理查询类型。而 miekg/dns 是事实标准:它把 DNS 协议细节封装成 Go 结构体,让你专注逻辑,不用手搓二进制头字段。
注意:它不内置 HTTP 或 TLS 支持(比如 DoH/DoT),纯 DNS over UDP/TCP;也不带权威域名管理(如 zone 文件加载),得自己存记录。
常见错误现象:dns.Server.ListenAndServe() 启动后没反应,其实是没注册 dns.HandleFunc,或者监听地址写成 "127.0.0.1:53" 导致权限拒绝(Linux/macOS 下非 root 无法绑定 1–1023 端口)。
- 开发调试建议用
":8053"替代":53",避免 sudo - 必须在
dns.HandleFunc里显式调用w.WriteMsg(),否则客户端收不到响应 - 默认只处理 UDP;如需 TCP 支持,初始化
dns.Server时设Net: "tcp",且需单独启一个 TCP server 实例
最简响应:实现一个固定 A 记录的 DNS 服务器
核心是三步:注册处理器 → 构造响应 → 写回连接。下面这个例子返回 example.com 的 A 记录 192.0.2.1,其他域名一律 NXDOMAIN。
立即学习“go语言免费学习笔记(深入)”;
package main
import (
"log"
"net"
"github.com/miekg/dns"
)
func main() {
dns.HandleFunc(".", handleDNS)
server := &dns.Server{Addr: ":8053", Net: "udp"}
log.Printf("Starting DNS server on :8053")
log.Fatal(server.ListenAndServe())
}
func handleDNS(w dns.ResponseWriter, r *dns.Msg) {
m := new(dns.Msg)
m.SetReply(r)
m.Compress = true
if len(r.Question) == 0 {
w.WriteMsg(m)
return
}
q := r.Question[0]
if q.Qtype == dns.TypeA && q.Name == dns.Fqdn("example.com") {
a := &dns.A{
Hdr: dns.RR_Header{
Name: q.Name,
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: 300,
},
A: net.ParseIP("192.0.2.1").To4(),
}
m.Answer = append(m.Answer, a)
} else {
m.Rcode = dns.RcodeNameError // NXDOMAIN
}
w.WriteMsg(m)
}
关键点:
-
dns.Fqdn("example.com")必须加点转成全限定名("example.com."),否则匹配失败 -
dns.A.A字段要传net.IP.To4(),不能直接传"192.0.2.1"字符串 - 没匹配到时仅设
m.Rcode不够,还要清空m.Answer(否则可能残留旧数据)
如何支持多个域名和不同记录类型(CNAME、TXT)
硬编码 if-else 很快失控,建议用 map 存记录,键为 name + type 组合,值为 []dns.RR 切片。这样增删改只需操作 map,不碰协议逻辑。
使用场景:内网服务发现、本地开发环境 mock 域名解析(如把 api.dev 指向 10.0.0.5)。
参数差异:dns.CNAME 和 dns.TXT 的构造方式不同——CNAME 的 Target 必须是 FQDN(末尾带点),TXT 的 Txt 是字符串切片(哪怕只有一个字段也要写 []string{"hello"})。
- 不要用
strings.ToLower(q.Name)做匹配:DNS 名称比较应忽略大小写,但miekg/dns内部已处理,直接用原值比对即可 - 若需通配符(如
*.dev),得手动解析q.Name,miekg/dns不自动展开 - 大量记录时,map 查找快,但要注意并发安全:如果从 HTTP 接口动态更新 map,需加
sync.RWMutex
UDP vs TCP:什么时候必须开 TCP 服务
DNS 查询默认走 UDP,但响应超过 512 字节(未启用 EDNS0 时)或涉及区域传输(AXFR)、TSIG 签名、大 TXT 记录等,客户端会自动降级重试 TCP。所以只跑 UDP 服务器,在真实环境大概率丢响应。
性能影响:TCP 连接有握手开销,但现代 DNS 服务器通常复用连接;兼容性上,所有主流 resolver(dig、systemd-resolved、iOS/Android)都要求 TCP 可用。
- 启动两个 server:一个
Net: "udp",一个Net: "tcp",共用同一个 handler 函数 - 别在 handler 里做耗时操作(如 HTTP 请求、DB 查询),DNS 协议超时通常只有几秒,阻塞会导致整个连接卡住
- 测试 TCP 是否生效:用
dig @127.0.0.1 -p 8053 example.com +tcp,看是否返回;; SERVER: 127.0.0.1#8053(127.0.0.1)且无Truncated
真正容易被忽略的是:UDP server 和 TCP server 的 Addr 必须完全一致(包括端口),否则客户端无法正确关联两者;另外,防火墙或云厂商安全组常默认放行 UDP 53 但屏蔽 TCP 53,本地测通不代表线上可用。










