因为Go标准库无DNS服务端实现,net包仅支持客户端解析,无法监听53端口、构造响应报文或处理查询类型;必须使用miekg/dns等第三方库,它封装协议细节、支持UDP/TCP双栈监听与响应构造,是Go生态事实标准。

为什么不能只用 Go 标准库启动 DNS 服务器
Go 标准库没有 DNS 服务端实现,net 包只提供客户端能力(如 net.LookupHost),无法监听 53 端口、解析查询报文或构造合法响应。试图用标准库手搓 DNS 报文极易出错——Header 字段错一位、QR 位没设成 1、ID 不匹配,客户端就直接丢包。必须用第三方库,miekg/dns 是当前最稳定、纯 Go、零 cgo 依赖的事实标准,CoreDNS 底层也在用它。
如何用 miekg/dns 启动一个能响应 A 记录的最小服务
核心就三步:注册处理器、构造响应、写回连接。下面这段代码返回 example.com. 的 A 记录,其他域名一律 NXDOMAIN:
package main
import (
"log"
"net"
"github.com/miekg/dns"
)
func main() {
server := &dns.Server{Addr: ":8053", Net: "udp"}
dns.HandleFunc(".", func(w dns.ResponseWriter, r *dns.Msg) {
m := new(dns.Msg)
m.SetReply(r) // 必须调用,否则 QR=0、ID 错乱,客户端拒收
m.Authoritative = true
if len(r.Question) == 0 {
w.WriteMsg(m)
return
}
q := r.Question[0]
if q.Qtype == dns.TypeA && q.Name == "example.com." {
rr, _ := dns.NewRR("example.com. 300 IN A 192.0.2.1")
m.Answer = append(m.Answer, rr)
} else {
m.Rcode = dns.RcodeNameError // NXDOMAIN
}
w.WriteMsg(m)
})
log.Println("DNS server listening on :8053")
log.Fatal(server.ListenAndServe())
}
-
:8053是开发时安全端口,避免 sudo;上线需改:53并以 root 运行 - 域名必须带尾部点(
"example.com."),否则被当相对名拼接搜索域 -
TTL显式写成300,不写可能默认为 0,导致缓存失效或客户端拒绝 - 必须调用
m.SetReply(r),不能只 new Msg 后直接填字段
为什么只监听 UDP 会导致部分查询失败
DNS 协议规定:响应超过 512 字节、区域传输(AXFR)、或某些递归场景必须走 TCP。只开 UDP 时,客户端在超时后会重试 TCP,但你的服务没监听,最终查不到记录或降级失败。
正确做法是同时启两个 server 实例:
立即学习“go语言免费学习笔记(深入)”;
- UDP 实例:
&dns.Server{Addr: ":53", Net: "udp"} - TCP 实例:
&dns.Server{Addr: ":53", Net: "tcp"}(注意:不能复用同一个dns.Server对象) - 两个实例共用同一个
dns.HandleFunc注册逻辑,无需重复写处理函数
实际部署中漏掉 TCP 监听,Wireshark 里能看到客户端发了 SYN,但无响应,接着 fallback 到公共 DNS,你以为代码没问题,其实是协议层面被截断。
如何支持泛域名(如 *.example.com)解析
dns.ServeMux 不支持通配符注册,"*.example.com" 这种字符串根本不会被匹配到。真正可行的是注册兜底 handler("."),然后手动判断:
- 取
r.Question[0].Name,检查是否以"example.com."结尾(注意末尾点) - 区分
Qtype:A/AAAA/CNAME 处理逻辑不同,别混用 - 响应里的
RR_Header.Name必须和问题中的Question[0].Name完全一致(大小写+点) - 不要用
dns.NewRR("*.example.com. ...")—— 那只是字面量,不是通配符语义
泛解析容易忽略全限定名末尾点,比如把 api.example.com 当成相对名,结果查到 api.example.com.localdomain.,查不到记录还找不到原因。


















