
本文详解如何使用 Go + miekg/dns 库快速搭建轻量级本地 DNS 服务器,支持动态响应 A 和 SRV 查询(如 _etcd-client._tcp.test.com),专为单元测试、集成测试场景设计,无需修改系统 hosts 或 bind 配置。
本文详解如何使用 Go + miekg/dns 库快速搭建轻量级本地 DNS 服务器,支持动态响应 A 和 SRV 查询(如 `_etcd-client._tcp.test.com`),专为单元测试、集成测试场景设计,无需修改系统 hosts 或 bind 配置。
在微服务或分布式系统测试中,常需模拟真实 DNS 解析行为——尤其是依赖 SRV 记录的服务发现机制(如 etcd、Consul 客户端)。硬编码 /etc/hosts 或部署完整 BIND 既不灵活也不便自动化。Go 生态中,miekg/dns 是最成熟、RFC 兼容的 DNS 协议库,可让你完全编程化控制 DNS 响应逻辑,实现零外部依赖的本地 DNS 测试桩。
免费 DNS 与邮件安全分析(IntoDNS.ai):包括 DNSSEC、SPF、DKIM、DMARC、MTA-STS、BIMI、SMTP STARTTLS、FCrDNS、黑名单、发件人要求及报告。
以下是一个生产就绪的最小可行示例,支持 A 和 SRV 双类型查询,并适配测试环境常见需求:
✅ 核心功能说明
- 启动 UDP/TCP 双协议 DNS 服务(默认端口 :53,测试时建议改用 :5353 避免 root 权限)
- 对 test.com. 返回 127.0.0.1(A 记录)
- 对 _etcd-client._tcp.test.com. 返回 SRV 0 0 5000 my1.test.com.(目标指向同域 A 记录)
- 支持 IPv4(代码已简化,如需 IPv6 可扩展 AAAA 分支)
? 完整可运行代码
package main
import (
"log"
"net"
"time"
"github.com/miekg/dns"
)
const (
DOM = "test.com." // 主域名(注意末尾点号表示 FQDN)
SRVDom = "_etcd-client._tcp." // SRV 服务名前缀(也需 FQDN 格式)
)
func handleDNS(w dns.ResponseWriter, r *dns.Msg) {
m := new(dns.Msg)
m.SetReply(r)
// 检查请求是否含问题节
if len(r.Question) == 0 {
return
}
q := r.Question[0]
switch q.Qtype {
case dns.TypeA:
if dns.Name(q.Name).Equal(dns.Name(DOM)) {
rr := &dns.A{
Hdr: dns.RR_Header{
Name: DOM,
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: 30, // 缓存 30 秒,测试友好
},
A: net.ParseIP("127.0.0.1"),
}
m.Answer = append(m.Answer, rr)
}
case dns.TypeSRV:
if dns.Name(q.Name).Equal(dns.Name(SRVDom + DOM)) {
rr := &dns.SRV{
Hdr: dns.RR_Header{
Name: SRVDom + DOM,
Rrtype: dns.TypeSRV,
Class: dns.ClassINET,
Ttl: 30,
},
Priority: 0,
Weight: 0,
Port: 5000,
Target: DOM, // 指向主域名(自动解析为 127.0.0.1)
}
m.Answer = append(m.Answer, rr)
}
}
w.WriteMsg(m)
}
func startDNSServer(network, addr string) {
server := &dns.Server{
Addr: addr,
Net: network,
// 可选:启用日志便于调试
Handler: dns.HandlerFunc(handleDNS),
}
log.Printf("Starting %s DNS server on %s...", network, addr)
go func() {
if err := server.ListenAndServe(); err != nil {
log.Fatalf("Failed to start %s server: %v", network, err)
}
}()
}
func main() {
// 注册处理函数(实际由 Handler 字段接管,此处兼容旧版写法,可省略)
dns.HandleFunc(DOM, handleDNS)
dns.HandleFunc(SRVDom+DOM, handleDNS)
// 启动 UDP 和 TCP 服务(测试推荐非特权端口)
startDNSServer("udp", ":5353")
startDNSServer("tcp", ":5353")
// 保持进程运行(测试中建议用 sync.WaitGroup 或 context 控制生命周期)
select {}
}
? 验证方式(终端命令)
# 查询 SRV 记录(指定本地服务地址和端口) dig @127.0.0.1 -p 5353 _etcd-client._tcp.test.com. SRV +short # 输出应为: # 0 0 5000 test.com. # 查询 A 记录验证基础解析 dig @127.0.0.1 -p 5353 test.com. A +short # 输出应为: # 127.0.0.1
⚠️ 关键注意事项
- 权限与端口::53 需 root 权限;测试务必改用 :5353 等高位端口,启动时无需 sudo。
- FQDN 规范:所有域名(DOM, SRVDom)必须以 . 结尾,否则 DNS 库可能自动追加搜索域导致解析失败。
-
测试集成建议:
- 在 TestMain 中启动/关闭服务器,用 net.ListenTCP("tcp", &net.TCPAddr{Port: 0}) 动态分配端口避免冲突;
- 使用 net.Resolver 显式配置测试客户端指向该地址(而非系统 resolver):
resolver := &net.Resolver{ PreferGo: true, Dial: func(ctx context.Context, _, _ string) (net.Conn, error) { return net.DialTimeout("tcp", "127.0.0.1:5353", 2*time.Second) }, } addrs, _ := resolver.LookupSRV(context.Background(), "etcd-client", "tcp", "test.com.")
- 安全性:此服务无访问控制,仅限本地回环使用,切勿暴露到公网或容器外部网络。
通过以上方案,你可在 go test 流程中秒级启动一个可控、可断言、可销毁的 DNS 桩服务,彻底解耦对系统配置和外部 DNS 服务的依赖,大幅提升测试稳定性与可移植性。










