云计算百科
云计算领域专业知识百科平台

miekg/dns 库指南:Go 语言中构建 DNS 服务器、解析器与 DNSSEC 应用的完整实践

  • 云原生
  • 微服务
  • 容器编排
  • 运维

【免费下载链接】flynn

[UNMAINTAINED] A next generation open source platform as a service (PaaS)

项目地址:
https://gitcode.com/gh_mirrors/fl/flynn

点击查看 免费下载

导读

vendor/github.com/miekg/dns 是一个遵循“Less is more”哲学、只依赖 Go 标准库的完整 DNS 库,支持服务端与客户端编程,涵盖几乎全部常用资源记录(含 DNSSEC 类型)。本文以该库的 README 与源码(doc.go、server.go、client.go 等)为主线,结合其在 Flynn 中 discoverd 内置 DNS 服务(discoverd/server/dns.go)的真实落地场景,系统讲解 RR 构造、消息收发、服务器编写、DNSSEC、动态更新、TSIG、AXFR、EDNS0 与 SIG(0),让你读完即可用该库独立搭建解析器与权威服务器。

定位:为什么选择 miekg/dns

这个库的核心理念可以概括为三句话:

  • KISS(Keep It Simple, Stupid):保持最简单直接的设计;
  • Fast:追求高性能;
  • Small API:凡是能用 Go 原生代码简单实现的,就不额外提供封装函数。

README 中给出的基准数据(“Fast”特性一节)可供参考:回复速度约为每秒 8 万 QPS(更快的硬件可更高),RR 解析约每秒 10 万条,即 500 万条记录大约 50 秒解析完成。这些数值来自项目方自述,实际表现取决于硬件与负载。

从仓库目录可以看到,该库的源码组织非常精简,全部功能集中在几十个 .go 文件中:

文件职责
dns.go 核心常量、类型定义
msg.go DNS 消息的 Pack/Unpack 与消息结构
server.go 服务端实现(ServeMux、Handler、Server)
client.go 客户端查询实现
scan.go zone 文件解析器
dnssec.go DNSSEC 签名与验证
tsig.go TSIG 事务签名
xfr.go AXFR/IXFR 区传送
update.go RFC 2136 动态更新
edns.go EDNS0 扩展机制

在 Flynn 中的真实用途

本库并非孤立存在,而是 Flynn 中服务发现组件 discoverd 内置 DNS 服务器的底层引擎。在 discoverd/server/dns.go 中,discoverd 直接导入 github.com/miekg/dns,利用其 dns.Server、dns.NewServeMux、dns.HandlerFunc、dns.Client 等构件实现了一个集群内 DNS 服务:既能回答 discoverd. 域下的服务记录(A/AAAA/SRV/SOA),又能作为递归器转发其他域的查询。这是理解该库“服务端 + 客户端一体”能力的最佳参考实现。

构建与上手

编译安装

README 给出的构建方式(针对 Go 1.5/1.6 时代)如下:

go get github.com/miekg/dns
go build github.com/miekg/dns

在 Flynn 仓库中,该库以 vendor 方式固定版本引入,go.mod 中记录了依赖版本 github.com/miekg/dns v0.0.0-20160726032027-db96a2b759cd(见 go.mod),vendor/github.com/miekg/dns 目录即为该版本源码快照。这意味着 Flynn 的构建完全离线可复现,不受上游库演进影响。

文档入口

doc.go 是库的 API 使用文档(godoc github.com/miekg/dns 时会展示其内容),其开头的“how to use the API”示例是了解 API 的最佳起点。README 还提到更多示例程序位于 github.com/miekg/exdns 仓库。

资源记录(RR):三种构造方式

库的核心设计是:资源记录是原生 Go 类型,而非 wire 格式的字节串。这让你可以用类型安全的方式构造和读取 RR。

doc.go 展示了创建一个 MX 记录的基本模式:

r := new(dns.MX)
r.Hdr = dns.RR_Header{Name: "miek.nl.", Rrtype: dns.TypeMX,
Class: dns.ClassINET, Ttl: 3600}
r.Preference = 10
r.Mx = "mx.miek.nl."

也可以直接从字符串解析(推荐用于配置驱动的场景):

mx, err := dns.NewRR("miek.nl. 3600 IN MX 10 mx.miek.nl.")

当默认 TTL(3600)和 class(IN)恰好合适时还可以更简洁:

mx, err := dns.NewRR("miek.nl. MX 10 mx.miek.nl.")

甚至可以直接解析带 $ORIGIN 指令的 zone 片段:

mx, err := dns.NewRR("$ORIGIN nl.\\nmiek 1H IN MX 10 mx.miek")

注意 doc.go 中的一条关键约定:发送消息前域名必须是完全限定名(FQDN),未限定的名字在打包(pack)时会失败。库提供了 dns.Fqdn("miek.nl") 这类工具来补全末尾的点。

消息构造与四段结构

DNS 消息 dns.Msg(定义见 msg.go)由四个区段构成,与协议本身一一对应:

  • Question:问题区;
  • Answer:回答区;
  • Ns:授权区(authority);
  • Extra:附加区(additional)。

其中除 Question 外均为 []RR。消息头 MsgHdr 则暴露了 Id、Response、Opcode、Truncated、RecursionDesired、RecursionAvailable、AuthenticatedData、CheckingDisabled、Rcode 等字段(msg.go)。

构造一个查询 MX 记录的消息,最简洁的写法是:

m := new(dns.Msg)
m.SetQuestion("miek.nl.", dns.TypeMX)

如果对域名是否已是 FQDN 不确定:

m.SetQuestion(dns.Fqdn("miek.nl"), dns.TypeMX)

更灵活但稍显冗长的等价写法(手动填充各字段,便于逐项控制):

m1 := new(dns.Msg)
m1.Id = dns.Id() // 16 位随机 ID
m1.RecursionDesired = true
m1.Question = make([]dns.Question, 1)
m1.Question[0] = dns.Question{"miek.nl.", dns.TypeMX, dns.ClassINET}

dns.Id 是一个可重赋值的变量,默认返回 16 位随机数;你甚至可以为了测试把它改成固定值(见 msg.go)。

读取响应时,按类型断言访问 RR 数据是标准姿势。例如取出 Answer 区第一个 RR,判断它是否为 TXT 记录:

if t, ok := in.Answer[0].(*dns.TXT); ok {
// do something with t.Txt
}

名称与 TXT 字符串的展示形式

doc.go 强调:域名与 TXT 字符字符串在解包(unpack)和转字符串时都会统一转换为展示形式(presentation form):

  • TXT 字符串中的 tab、回车、换行分别转义为 \\t、\\r、\\n;反斜杠与引号被转义;小于 32 或大于 127 的字节转为 \\DDD 形式;
  • 域名在此基础上还会对括号、句点、空格、分号与 @ 符号进行转义。

这保证了打印、日志与配置文件中的一致性。

客户端编程:同步查询与高级选项

最简查询

不需要高级特性时,一条 UDP 查询即可完成:

in, err := dns.Exchange(m1, "127.0.0.1:53")

该包级函数(client.go)内部以 2 秒超时拨号,发送消息并等待回复,还会校验响应消息的 Id 是否与请求一致(不一致返回 ErrId)。

使用 dns.Client 获得 RTT 与更多控制

c := new(dns.Client)
in, rtt, err := c.Exchange(m1, "127.0.0.1:53")

dns.Client(client.go)支持以下参数:

字段说明
Net "tcp" 或 "tcp-tls"(DNS over TLS)时走 TCP,默认空串走 UDP
UDPSize UDP 最小接收缓冲区
TLSConfig TLS 连接配置(用于 DoT)
Timeout 拨号 + 读写累计超时,非零时覆盖下面三个超时
DialTimeout / ReadTimeout / WriteTimeout 默认均为 2 秒
TsigSecret TSIG 密钥表,map[完全限定域名]base64密钥
SingleInflight 为 true 时合并相同 Qname/Qtype/Qclass 的并发查询

合并并发查询:SingleInflight

当有大量并发请求指向同一问题时,可以开启查询合并:

c.SingleInflight = true

设置后,库会使用单飞(singleflight)机制,多个相同查询共享一次网络往返,显著降低上游压力(实现位于 singleinflight.go)。

另外注意:Exchange 不会自动重试失败查询,也不会在截断(truncation)时自动回退 TCP;如需大响应,应在消息上附加 EDNS0 OPT RR 来通告更大的缓冲区(见下文 EDNS0 一节),否则回退到历史 512 字节限制。

服务端编程:模仿 net/http 的 Handler 模型

服务端 API 刻意模仿 net/http:注册 handler、启动监听、并发处理。核心接口见 server.go:

  • Handler:实现 ServeDNS(w ResponseWriter, r *Msg) 的类型;
  • ResponseWriter:提供 LocalAddr()、RemoteAddr()、WriteMsg(*Msg)、Write([]byte)、Close()、TsigStatus()、TsigTimersOnly(bool)、Hijack() 等能力;
  • ServeMux:DNS 请求多路复用器,按 zone 名匹配最贴近的已注册模式,且对 DNSSEC 感知(DS 记录查询会被导向父 zone),并发访问安全。

一个最简单的服务器,几乎可以照抄 net/http 的写法:

server := &dns.Server{Addr: ":53", Net: "udp"}
server.TsigSecret = map[string]string{"axfr.": "so6ZGir4GPAqINNh9U5c3A=="}
go server.ListenAndServe()
dns.HandleFunc(".", handleRequest)

func handleRequest(w dns.ResponseWriter, r *dns.Msg) {
m := new(dns.Msg)
m.SetReply(r)
w.WriteMsg(m)
}

包级函数 dns.ListenAndServe(addr, network, handler) 和 dns.ListenAndServeTLS(addr, certFile, keyFile, handler) 也都有提供(server.go),后者对应 DNS over TLS 服务端。

生产案例:discoverd 的双栈 DNS 服务器

Flynn 的 discoverd/server/dns.go 演示了如何用该库搭建可同时监听 UDP/TCP 的服务:

  • 用 dns.NewServeMux() 创建 mux;
  • 用 mux.HandleFunc(srv.Domain, api.ServiceLookup) 注册 discoverd. 域的服务查询 handler,有递归器时再用 mux.HandleFunc(".", api.Recurse) 注册兜底 handler;
  • 分别构造 dns.Server{Net: "udp", PacketConn: l, Handler: mux, …} 与 dns.Server{Net: "tcp", Listener: l, Handler: mux, …},各自 ActivateAndServe() 启动;
  • 关闭时调用每个 Server 的 Shutdown()。
  • 在 handler 内部,discoverd 用 dns.SplitDomainName 拆分查询名,解析 _service._tcp 这类 RFC 2782 SRV 风格请求、leader.service 主节点查询和 instance.service._i 实例地址查询;对 SRV 响应还会在 Extra 区追加实例 A/AAAA 记录(discoverd/server/dns.go)。响应构造上,SOA 记录、SRV 记录和 A/AAAA 记录分别用 dns.SOA、dns.SRV、dns.A/dns.AAAA 原生类型填充(discoverd/server/dns.go)。

    其递归器 Recurse 则体现了客户端 API 的用法:构造 dns.Client,按入站连接协议选择 client.Net = "tcp",依次对每个上游 recursor 执行 client.Exchange(req, recursor),成功即 w.WriteMsg(res) 转发,全部失败则返回 SERVFAIL(discoverd/server/dns.go)。

    该逻辑有完整的测试覆盖,见 discoverd/server/dns_test.go,其中同时启动了 dns.Server(UDP 与 TCP),并通过 dns.Client 的 Exchange 校验压缩长响应、服务查询与 SERVFAIL 场景。

    其他服务端要点

    • 同一 TCP 连接上默认最多处理 128 个查询后关闭连接(maxTCPQueries,见 server.go);
    • handler 可通过 w.Hijack() 接管连接,之后库不再对该连接做任何处理;
    • 包级 DefaultServeMux 可用于全局注册,HandleFailed 返回一个对任何请求都回 SERVFAIL 的 handler。

    Zone 文件解析

    RFC 1035 的 zone 文件解析由 scan.go(及其辅助 scanner.go、scan_rr.go)实现。README 明确支持:

    • $INCLUDE:包含其他文件;
    • $ORIGIN:修改当前源点;
    • $TTL:设置默认 TTL;
    • $GENERATE:为所有记录类型生成序列化记录。

    配合 dns.NewRR 与 dns.NewZoneParser 等入口,可以方便地把 zone 文件加载进内存,作为权威服务器的数据源。

    DNSSEC:签名、验证与密钥生成

    DNSSEC(DNS Security Extensions)用公钥密码为资源记录签名:公钥放在 DNSKEY 记录中,签名放在 RRSIG 记录中。本库支持:

    • 签名(dnssec.go);
    • 验证(dnssec.go);
    • 密钥生成(dnssec_keygen.go、dnssec_keyscan.go 负责扫描与私钥处理);
    • 算法覆盖 DSA、RSA、ECDSA。

    请求一个 zone 的 DNSSEC 信息,只需在消息中设置 DO(DNSSEC OK)位:

    m := new(dns.Msg)
    m.SetEdns0(4096, true)

    SetEdns0(4096, true) 同时完成了两件事:附加一个通告 4096 字节缓冲区的 OPT RR,并设置 DO 位,要求响应中包含 DNSSEC 记录。NSEC3 的支持见 nsecx.go,TLSA 记录(DANE)见 tlsa.go。

    动态更新(RFC 2136)

    动态更新复用 DNS 消息格式,但语义上重命名了三个区段(见 update.go):

    • Question → Zone;
    • Answer → Prerequisite(前提条件);
    • Authority → Update;
    • Additional 名称不变。

    你可以用前提条件规定“某记录存在/不存在”来决定后续增删是否执行。doc.go 给出两张来自 RFC 2136 的完整对应表:

    前提区(Prerequisite)用到的函数:

    CLASSTYPERDATA含义函数
    ANY ANY 空 名字被使用 dns.NameUsed
    ANY rrset 空 RRset 存在(与值无关) dns.RRsetUsed
    NONE ANY 空 名字未被使用 dns.NameNotUsed
    NONE rrset 空 RRset 不存在 dns.RRsetNotUsed
    zone rrset rr RRset 存在(依赖值) dns.Used

    前提区也可以留空。

    更新区(Update)用到的函数:

    CLASSTYPERDATA含义函数
    ANY ANY 空 删除名字下所有 RRset dns.RemoveName
    ANY rrset 空 删除一个 RRset dns.RemoveRRset
    NONE rrset rr 从 RRset 中删除一条 RR dns.Remove
    zone rrset rr 向 RRset 添加一条 RR dns.Insert

    TSIG 事务签名

    TSIG(Transaction Signature)为每条消息附加一个 HMAC 签名记录。支持的算法包括 HmacMD5、HmacSHA1、HmacSHA256、HmacSHA512(tsig.go)。

    查询场景

    c := new(dns.Client)
    c.TsigSecret = map[string]string{"axfr.": "so6ZGir4GPAqINNh9U5c3A=="}
    m := new(dns.Msg)
    m.SetQuestion("miek.nl.", dns.TypeMX)
    m.SetTsig("axfr.", dns.HmacMD5, 300, time.Now().Unix())
    // 发送时 TSIG RR 会被计算并自动填入

    注意:TSIG 的 key 名是域名,必须完全限定(带末尾点)。

    服务端验证

    server := &dns.Server{Addr: ":53", Net: "udp"}
    server.TsigSecret = map[string]string{"axfr.": "so6ZGir4GPAqINNh9U5c3A=="}
    go server.ListenAndServe()
    dns.HandleFunc(".", handleRequest)

    func handleRequest(w dns.ResponseWriter, r *dns.Msg) {
    m := new(dns.Msg)
    m.SetReply(r)
    if r.IsTsig() != nil {
    if w.TsigStatus() == nil {
    // r 带 TSIG 且验证通过
    m.SetTsig("axfr.", dns.HmacMD5, 300, time.Now().Unix())
    } else {
    // r 带 TSIG 但验证失败
    }
    }
    w.WriteMsg(m)
    }

    ResponseWriter.TsigStatus() 返回 TSIG 验证状态,为 nil 表示验证成功;TsigTimersOnly(bool) 可切换到仅校验时间戳模式。

    AXFR / IXFR 区传送

    区传送(zone transfer)是 TSIG 最主要的应用场景。用 dns.Transfer 请求 AXFR 的完整模式如下(xfr.go):

    t := new(dns.Transfer)
    m := new(dns.Msg)
    t.TsigSecret = map[string]string{"axfr.": "so6ZGir4GPAqINNh9U5c3A=="}
    m.SetAxfr("miek.nl.")
    m.SetTsig("axfr.", dns.HmacMD5, 300, time.Now().Unix())
    c, err := t.In(m, "176.58.119.54:53")
    for r := range c {
    // 逐个处理传送回来的记录
    }

    每收到一个消息信封都会做 TSIG 校验,任何异常都会返回错误。IXFR(增量传送,RFC 1995)同样支持。

    EDNS0 扩展机制

    EDNS0 由 RFC 2671 定义、RFC 6891 更新,引入一种被“滥用”的新 RR 类型 OPT。构造一个空的 OPT RR:

    o := new(dns.OPT)
    o.Hdr.Name = "." // 按定义必须是根域
    o.Hdr.Rrtype = dns.TypeOPT

    OPT 的 rdata 是一组 EDNS0 接口(edns.go),目前已标准化的包括 EDNS0_NSID(RFC 5001)与 EDNS0_SUBNET(client subnet,draft-vandergaast-edns-client-subnet-02),且可在同一个 OPT 中组合。

    服务端检查客户端带上了哪些选项:

    // o 是一个 dns.OPT
    for _, s := range o.Option {
    switch e := s.(type) {
    case *dns.EDNS0_NSID:
    // 处理 e.Nsid
    case *dns.EDNS0_SUBNET:
    // 访问 e.Family、e.Address 等
    }
    }

    相关能力还包括:DNS Cookies(RFC 7873,draft-ietf-dnsop-cookies)、NSID(RFC 5001)、EDNS0 EXPIRE 选项(RFC 7314)以及 EDNS0 DNS Update Lease(draft,尚在草案阶段)。此外,启用消息压缩后库会自动进行 DNS 名称压缩(见 msg.go 中的 maxCompressionOffset 常量,压缩指针可用 14 位)。

    SIG(0)(RFC 2931)

    RFC 2931 的 SIG(0) 为 DNS 事务提供保护:保护 glue 记录、请求、请求与响应中的消息头以及响应整体完整性。它工作方式类似 TSIG,但使用公钥密码而非共享密钥。

    支持的算法:DSA、ECDSAP256SHA256、ECDSAP384SHA384、RSASHA1、RSASHA256、RSASHA512(实现见 sig0.go)。

    一个已知限制:多消息会话中的连续签名(signing subsequent messages in multi-message sessions)尚未实现。

    私有 RR 类型

    RFC 6895 为私有用途划出了一段类型码区间:65,280 – 65,534(0xFF00 – 0xFFFE)。在向 IANA 申请正式类型码之前,可以用这些码实验新的 RR 类型(相关工具函数见 types.go 与 ztypes.go 的类型映射)。

    支持的 RFC 全景

    README 列出了完整支持清单(括号内为本仓库可核实的对应实现文件):

    RFC内容对应源码
    1034/1035 DNS 标准 msg.go、scan.go
    1982 序列号算术 各序列号处理处
    1876 LOC 记录 types.go
    1995 / 5936 IXFR / AXFR xfr.go
    1996 DNS notify 消息 Opcode 支持
    2136 DNS 动态更新 update.go
    2782 SRV 记录 类型定义与 discoverd 用法(discoverd/server/dns.go)
    2845 / 4635 TSIG / HMAC SHA TSIG tsig.go
    2915 / 3401-3403 NAPTR 记录 类型定义
    3225 DO 位(DNSSEC OK) SetEdns0
    3597 未知 RR 通用 RR 处理
    4033/4034/4035 DNSSEC 与验证函数 dnssec.go
    4255 SSHFP 记录 类型定义
    4408 SPF 记录 类型定义
    4509 DS 中的 SHA256 哈希 DNSSEC 验证
    4592 DNS 通配符 zone 解析
    4701 DHCID 类型定义
    5001 NSID edns.go
    5155 NSEC3 记录 nsecx.go
    5205 HIP 记录 类型定义
    5702 DNS 中的 SHA2 DNSSEC 算法
    5966 TCP 实现建议 server.go
    6605 ECDSA DNSSEC 算法
    6844 CAA 记录 类型定义
    6891 EDNS0 更新 edns.go
    6975 DNSSEC 算法理解 DNSSEC 支持
    7043 EUI48/EUI64 记录 类型定义
    7314 EDNS EXPIRE 选项 EDNS 选项
    7553 URI 记录 类型定义
    7858 DNS over TLS(draft) client.go 的 tcp-tls 与 server.go 的 ListenAndServeTLS
    7873 DNS Cookies(draft-ietf-dnsop-cookies) EDNS 选项

    按 README 的说法,这份清单几乎覆盖了“所有”相关 RFC;同时它也强调 ldns、NSD、Net::DNS、GRONG 等项目是设计上的参考来源。

    在项目中使用时的建议

    • 依赖固定:参照 Flynn 的做法,把该库放入 vendor 目录并记录精确版本(go.mod),保证构建可复现;
    • 注意 FQDN 约定:所有发送的域名与 TSIG key 名必须完全限定,否则打包失败;
    • 正确设置 EDNS0 缓冲区:需要大于 512 字节响应时,用 SetEdns0 通告缓冲区大小;
    • 区分传输:大响应与区传送场景优先 TCP;UDP 响应注意 512 字节 / EDNS0 缓冲区的限制;
    • 复用 discoverd 的模式:需要同时服务 UDP 与 TCP 时,可仿照 discoverd/server/dns.go 分别启动两个 dns.Server 共享同一个 ServeMux。

    结语

    从 README 的“Less is more”哲学出发,本文把 miekg/dns 的完整能力面——RR 构造、消息四段结构、客户端同步查询与 singleflight 优化、net/http 风格的服务端模型、zone 解析、DNSSEC、RFC 2136 动态更新、TSIG、AXFR/IXFR、EDNS0 与 SIG(0)——逐项落地为可直接运行的代码模式,并结合 Flynn discoverd 的 discoverd/server/dns.go 展示了其在真实 PaaS 服务发现中的工程用法。无论是编写权威服务器、递归解析器,还是为内部基础设施增加安全 DNS 能力,这个库都提供了小而完整的工具箱。

    赞

    分享

    • 云原生
    • 微服务
    • 容器编排
    • 运维

    【免费下载链接】flynn

    [UNMAINTAINED] A next generation open source platform as a service (PaaS)

    项目地址:
    https://gitcode.com/gh_mirrors/fl/flynn

    点击查看 免费下载

    上一篇:
    LyricsX macOS 歌词同步指南:自动搜词、桌面卡拉OK、十秒调准偏移

    下一篇:
    md2pptx:3大优势让你5分钟完成Markdown转PPT的革命性方案

    创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » miekg/dns 库指南:Go 语言中构建 DNS 服务器、解析器与 DNSSEC 应用的完整实践
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!