CoreDNS 是一个链接插件的 DNS 服务器。插件被定义为一个方法:ServeDNS()
获取一个请求,然后向客户端响应或将其传递到下一个插件。如果没有插件处理请求,则会返回 SERVFAIL 默认响应。
这篇博文详细介绍了如何向 CoreDNS 添加一个插件。我们使用的示例是 whoami 插件,它是一个 CoreDNS 插件,并且在没有指定 Corefile 的情况下默认加载。
请注意,这里的所有代码示例均为 Go,因为 CoreDNS 是用 Go 语言编写的。
第一个问题应该是:“我的插件应该做什么?”在 whoami 插件的情况下,其目的是回显客户端 IP(IPv4 或 IPv6)、使用的传输方法(“tcp”或“udp”)和请求端口号。
下一个问题是:“这个新插件叫什么?”尝试为其找到一个简短、有描述性的名称。在这种情况下,我们已经有一个(好)名称:whoami。
插件
插件包含多个部分
- 插件的注册
- 解析 whoami 插件以及 Corefile 中可能的参数的设置函数
ServeDNS()
和Name()
方法
定义好这些部分后,我们可以
- 连接它
- 使用它
1. 注册
通常情况下,插件有一个名为 setup.go
的文件来处理注册。在文件中,init
函数应如下所示
func init() { plugin.Register("whoami", setup) }
setup
是设置函数的名称,负责解析 Corefile。它的工作是返回实现 plugin.Handler
接口的类型。
因此,只要 Corefile 解析器看到“whoami”,就会调用 whoami.setup
。
2. 设置函数
由于 whoami 插件不允许任何选项,因此设置函数相对简单
func setupWhoami(c *caddy.Controller) error {
c.Next() // 'whoami'
if c.NextArg() {
return plugin.Error("whoami", c.ArgErr())
}
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
return Whoami{Next: next} // Set the Next field, so the plugin chaining works.
})
return nil
}
我们使用 *caddy.Controller
从 Corefile 接收令牌并对其进行操作。这里我们只检查令牌 whoami
之后是否未指定任何内容。如果您需要执行更多操作,请使用 c.Val()
、c.Args()
和 friends。
whoami 的完整 setup.go
在此处。
请注意,您还应该测试解析,请参见 setup_test.go。
3. ServeDNS() 和 Name()
我们从微不足道的 Name()
方法开始,其他插件使用该方法来检查已加载特定的插件。该方法只返回字符串 whoami
。
// Name implements the Handler interface.
func (wh Whoami) Name() string { return "whoami" }
然后,整件事情的核心,ServeDNS
方法。我们将逐行查看该方法。
// ServeDNS implements the plugin.Handler interface.
func (wh Whoami) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r}
如函数所示,它获取几个参数,其中 w
是客户端:写入 w
会向客户端返回一个响应。正如你将在下面所看到的,所有关于客户端连接的有趣属性都可以从 dns.ResponseWriter 中获取。r
是传入的查询。ServeDNS
方法返回一个整数和/或一个错误。整数可以取的值是 DNS RCODE,dns.RcodeServerFailure、dns.RcodeNotImplemented、dns.RcodeSuccess 等。一个成功返回的值表明插件已写入客户端。
request.Request
是一个帮助器结构,它抽象并缓存了一些客户端属性,例如 EDNS0 记录和 DNSSEC OK 位。
下面我们设置回复消息
a := &dns.Msg{}
a.SetReply(r)
a.Authoritative = true
我们创建一个新消息并将传入回复中的相关位复制到我们计划返回的消息中。我们摆弄了一些消息位并将其设置为权威性的。
然后我们将通过 state
帮助器结构检查传入的消息,看我们应该返回什么。
ip := state.IP()
var rr dns.RR
switch state.Family() {
case 1:
rr = &dns.A{}
rr.(*dns.A).Hdr = dns.RR_Header{Name: state.QName(),
Rrtype: dns.TypeA, Class: state.QClass()}
rr.(*dns.A).A = net.ParseIP(ip).To4()
case 2:
rr = &dns.AAAA{}
rr.(*dns.AAAA).Hdr = dns.RR_Header{Name: state.QName(),
Rrtype: dns.TypeAAAA, Class: state.QClass()}
rr.(*dns.AAAA).AAAA = net.ParseIP(ip)
}
IP()
返回客户端的 IP 地址。Family()
返回所使用的 IP 版本。根据 IP 版本,我们使用客户端地址创建 A 记录或 AAAA 记录。请注意,我们没有指定 TTL,这意味着它将为零;指示不应缓存这些记录。
接下来,我们要对客户端的源端口和所用传输协议进行编码。
srv := &dns.SRV{}
srv.Hdr = dns.RR_Header{Name: "_" + state.Proto() + "." + state.QName(),
Rrtype: dns.TypeSRV, Class: state.QClass()}
port, _ := strconv.Atoi(state.Port())
srv.Port = uint16(port)
srv.Target = "."
SRV 记录是理想选择。域名一个加了 _tcp
前缀,一个加了 _udp
前缀,并重新为 SRV 记录的端口号使用呼出回客户端端口号,即我们创建一些类似于以下内容:_tcp.example.org. 0 IN SRV 0 0 <portNr>
。
在这个方法的最后部分,我们创建完整的消息并发送它
a.Extra = []dns.RR{rr, srv}
w.WriteMsg(a)
return 0, nil
首先,我们将两个创建的资源记录(rr
和 srv
)添加到答复消息 a.Extra
的附加部分中。
然后,最后,我们调用 w
上的 WriteMsg
方法,将消息写回客户端。我们返回 0 和 nil
,表明写入成功(即使可能失败 - 我们没有检查 WriteMsg
的返回值)。
4. 连接它
接下来,我们需要告诉 CoreDNS 编译并使用这个新插件。最近简化了添加插件,只用编辑 plugin.cfg
并添加行
whoami:whoami
初始编号用于对插件进行排序(更多内容详见下文),然后是插件在注册中使用的名称,接着是 CoreDNS 插件目录内的软件包。
每个插件都在所有其他插件的列表中占有一席之地。例如,缓存或指标插件需要提早出现,以便它可以 “查看” 查询和响应并对其执行缓存或指标相关的操作。whoami 插件并没有那么特殊,可以放置在列表相对较后的位置(因此具有较大的编号)。
现在执行 `make`(或 `go generate && go build`)以获取可与我们闪亮的新插件配合使用的 `coredns` 二进制文件。在使用 `-plugins` 调用此二进制文件时,应包含 `dns.whoami`。
5. 使用方法
编写 Corefile
. {
whoami
}
使用文字表述,这意味着:对根 `.` 及其下一级拥有授权,这意味着所有可能的查询都将进入此段落。针对每个请求调用 whoami。
通过 `coredns -conf Corefile -dns.port 1053` 启动 CoreDNS。这里有几点需要注意。CoreDNS 将在当前目录中查找 Corefile,因此,这里仅出于完整性考虑而添加 `-conf Corefile`。`-dns.port` 将在端口 1053 上启动 CoreDNS,因此我们无需作为 root 用户运行。
CoreDNS 将输出以下内容
.:1053
CoreDNS-1.6.4
linux/amd64, go1.13.1, b139ba3
`.:1053` 表示它已经过分析我们的 Corefile,并且在端口 1053 侦听查询根 `.` 区域及其下一级的查询。
因此,让我们使用 `dig` 向其发送查询
% dig +nocmd @localhost mx example.org -p1053 +noall +additional
example.org. 0 IN A 127.0.0.1
_udp.example.org. 0 IN SRV 0 0 58359 .
非常好。它正在发挥作用。通过检查响应会发现,此请求是通过 UDP 从端口 58359 发送的 IPv4。
让我们尝试使用 TCP
% dig +nocmd @localhost a example.org -p1053 +noall +additional +tcp
example.org. 0 IN A 127.0.0.1
_tcp.example.org. 0 IN SRV 0 0 33435 .
是的,它正确检测到我们这次使用了 TCP(当然也使用了不同的端口)。
CoreDNS 中已经提供了许多不同的插件。如有新的精彩插件,欢迎随时提出问题。因此,如果您有什么想法,请在跟踪器上提出问题。
这是关于编写 CoreDNS 插件的 前一篇博客 的更新版本。
另请参见 插件示例,这是一个简单的插件,仅用作示例。