【免费下载链接】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 的服务:
在 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)用到的函数:
| 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)用到的函数:
| 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 列出了完整支持清单(括号内为本仓库可核实的对应实现文件):
| 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),仅供参考
网硕互联帮助中心






评论前必须登录!
注册