不能只用 net.ListenUDP 启动 DNS 服务,因 DNS 协议需严格遵循 RFC 1035 解析 Header/Question/Answer 段,处理 TCP 长度前缀、EDNS0、TSIG、压缩指针及 TTL 校验等语义,而 net.ListenUDP 仅提供原始字节流,不封装协议逻辑。

Go 语言标准库不提供 DNS 服务端实现,想跑一个能响应查询的本地 DNS 服务器,必须用第三方库;miekg/dns 是当前最轻量、最稳定、文档最清晰的事实标准,纯 Go 实现,无 CGO 依赖,适合嵌入或调试。
为什么不能只用 net.ListenUDP 启动 DNS 服务
DNS 协议不是“收包→解析→回包”这么简单。UDP 报文需按 RFC 1035 解析 Header、Question、Answer 等段;TCP 连接要处理长度前缀(2 字节);EDNS0 扩展、TSIG 签名、压缩指针、TTL 校验等都得手动处理——net.ListenUDP 只管字节流,不负责协议语义。
直接手撕会踩这些坑:
-
dns.Msg解析失败时静默返回空响应,客户端看到SERVFAIL或超时,但日志里没线索 - 域名没加尾部点(如
"example.com"而非"example.com."),会被当成相对名拼接搜索域,导致记录不匹配 - UDP 响应超过 512 字节却没设
TC=1,客户端不会自动降级 TCP,直接丢弃 - 没检查
msg.RecursionDesired就转发,某些上游 DNS(如企业内网 DNS)会拒收非递归请求
如何用 miekg/dns 启动最小可用 DNS 服务
核心就三步:注册 dns.HandlerFunc、启动 dns.Server、监听 UDP(和可选 TCP)。开发阶段别硬绑 :53,用 :8053 避免 sudo。
免费 DNS 与邮件安全分析(IntoDNS.ai):包括 DNSSEC、SPF、DKIM、DMARC、MTA-STS、BIMI、SMTP STARTTLS、FCrDNS、黑名单、发件人要求及报告。
立即学习“go语言免费学习笔记(深入)”;
关键实操点:
- Handler 函数里必须调用
m.SetReply(r)初始化响应头,否则QR位为 0,客户端当垃圾包丢弃 - 构造记录用
dns.NewRR("example.com. IN A 192.0.2.1"),注意域名末尾的点、IN大写、TTL 显式写(如300),否则某些客户端(如 macOSdig)会报malformed response - UDP Server 和 TCP Server 必须分开启动,
Net字段不能混用;TCP Handler 里建议加if w.LocalAddr().Network() == "tcp" {...}防误判 - 响应前设
m.Compress = true,否则大响应可能因未压缩被截断
转发型 DNS 服务器最容易漏掉的细节
单纯返回静态 A 记录容易,但要做转发(比如查不到就问 8.8.8.8),以下几点不处理就会出线上问题:
-
dns.Exchange()默认无超时,上游卡住就 goroutine 永久阻塞;必须包装成带 context 的调用,例如ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) - 原始请求带 EDNS0(
r.IsEdns0() == true)时,转发请求也得带上相同 OPT 记录,否则上游可能降级或拒绝 - 原始请求带 TSIG(
r.IsTsig()返回 true),不能直接转发,需先验证签名或剥离,否则8.8.8.8等公共 DNS 会返回NOTAUTH - UDP handler 里加
if len(r.Bytes()) > 512 { return },防止恶意大包耗尽内存(dns.Msg不校验输入长度)
真正难的不是写通,而是让每个响应都经得起 dig +trace 和 tcpdump 检验——协议细节藏在 RFC 里,不在示例代码里。

















