Translate CoreDNS Manual
什么是CoreDNS
CoreDNS是用go语言编写的DNS服务器。
CoreDNS是由插件驱动的。插件相当于CoreDNS的API,你可以编写自定义的API来添加功能。
但首先,你需要熟悉go语言和DNS的工作原理。
安装
源码安装
1 | $ mkdir -p $GOPATH/src/github.com/coredns |
检测版本
1 | $ ./coredns -version |
测试
在1053端口开启服务
1 | [root@A coredns]# ./coredns -dns.port=1053 |
客户端连接
1 | [root@A coredns]# dig @localhost -p 1053 a whoami.example.org |
插件
一旦CoreDNS解析配置并启动,它会运行各个服务器。每个服务器都有自己的服务域和监听端口,而且拥有自己的插件链。
CoreDNS处理处理一个DNS查询时,会经过以下步骤:
- 如果多个服务都在监听DNS查询端口,会选择服务域后缀最长匹配的服务器。比如有服务器A,服务域为example.com;服务器B,服务域为a.example.com,两者都监听53端口。现在有DNS在53端口查询www.a.example.org, 则会有B服务器处理该查询。
- 确认服务器后,会遍历服务器配置文件中配置的插件,默认的遍历顺序在plugin.cfg文件中设置。
- 每个插件会检查查询请求,并决定是否处理这个请求,此时可能有以下情况发生
- 插件处理该查询
- 插件不处理该查询
- 插件处理该查询,但在中途调用插件链中的下一个插件,处理落空
- 插件处理该查询,添加hint,后调用下一个插件
插件处理一个查询表示插件会对客户端做出响应。
查询被处理
插件处理查询,返回给客户端一个响应(或其他行为)。查询处理到此结束,不会调用下一个插件
查询不被处理
如果插件决定不处理该查询,会调用下一个插件,如果插件链中的最后一个插件也不处理该查询,CoreDNS返回 SERVFALL给 客户端。
查询处理落空
插件处理查询,但该插件的后端返回信息告知需要其他插件进行处理。如果处理落空,下一个插件会被调用。一个具有该行为的插件是 host,host插件首先检查/etc/hosts中的表项,如果它发现一个结果,那就返回该结果,如果没有,那就落空,由下一个插件处理该查询。
查询被处理,并添加hint
插件处理查询,并添加hint,之后调用下一个插件。下一个插件能看到给客户端的响应信息。
未注册插件
未注册拆件不处理DNS数据,但可能以其他方式影响CoreDNS的行为。
插件解析
一个插件包含 Setup、Registration、Handler 部分。
- Setup :解析插件配置和插件指令。
- Handler :包含处理请求的代码,并实现所有逻辑。
- Registration: 将插件注册到CoreDNS,该步骤发生在CoreDNS进行编译的时候。所有的注册插件都可以被服务器使用,至于在运行时调用哪个插件则在Corefile中配置。
插件文档
配置
CoreDNS中有许多模块可以配置。
首先,决定哪些插件可以编译到CoreDNS中,编译后的二进制文件包含 plugin.cfg中所有插件。添加或删除插件需要重新编译CoreDNS。
通常采用Corefile文件来配置CoreDNS。当CoreDNS启动时,如果-conf参数没有指定,它会在当前目录查找Corefile文件。Corefile文件中包含若干个服务器模块,每个服务器会列举它所用到的插件。
插件的在插件链中的顺序又plugin.cfg决定,与Corefile中配置的顺序无关。
Corefile中的注释以 # 开头。
环境变量
Corefile中支持环境变量,语法是{$ENV_VAR}
导入其他文件
参见import插件,该插件可以在Corefile中使用
可重用代码段(Snippet)
定义可重用代码段(Reusable Snippets),并通过import导入1
2
3
4
5
6
7
8
9
10
11# define a snippet
(snip) {
prometheus
log
errors
}
. {
whoami
import snip
}
服务器模块
每个服务器模块以服务器授权的服务域开头。在服务域或 一连串的服务域后(以空格分离),跟一个服务器模块,并用{}包围。
例:以下服务器配置中 . 表示所有服务域,故该服务器可以处理所有dns请求1
2
3. {
# Plugins defined here.
}
服务器模块可以选择一个监听端口,默认监听端口为53。端口号与服务域以:间隔。
例:以下服务器监听1053端口1
2
3.:1053 {
# Plugins defined here.
}
注:如果确定了监听端口,CoreDNS启动时的 -dns.port选项无效。
如果在一个服务域匹配两个以上端口,则开始时Corefile会报错1
2
3
4
5
6
7.:1054 {
}
.:1054 {
}
指定协议
在服务域前添加协议以指定协议1
2
3dns:// 标准的dns协议,为默认选项
tls:// for DNS over TLS.
grpc:// for DNS over gRPC.
插件
每个服务器模块指定一系列的插件供该服务器使用,最简单的用法是,直接在{}内添加插件名称。
例:添加chaos插件1
2
3. {
chaos
}
chaos以CH class响应查询,通过以上配置,CoreDNS会响应自己的版本号。1
2
3
4
5$ dig @localhost -p 1053 CH version.bind TXT
...
;; ANSWER SECTION:
version.bind. 0 CH TXT "CoreDNS-1.0.5"
...
多数插件允许以指定形式增加配置。
例:
Syntax
chaos [VERSION] [AUTHORS…]
VERSION is the version to return. Defaults to CoreDNS-, if not set.
AUTHORS is what authors to return. No default.
1 | . { |
也有插件可以将配置信息写到{}中
例:1
2
3
4
5. {
plugin {
# Plugin Block
}
}
我们可以结合以上配置,生成如下Corefile,该Corefile配置了4个服务域,监听2个端口1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21coredns.io:5300 {
file db.coredns.io
}
example.io:53 {
log
errors
file db.example.io
}
example.net:53 {
file db.example.net
}
.:53 {
kubernetes
forward . 8.8.8.8
log
errors
cache
}
外部插件
默认的CoreDNS不会编译外部插件,要激活外部插件,你需要自己编译CoreDNS。
可能的错误
health插件的文档中这样写道:这个插件只需要激活一次。你可能会认为下面的Corefile是合法的:1
2
3
4
5health
. {
whoami
}
但实际情况是,上述Corefile会报错:1
"Corefile:3 - Error during parsing: Unknown directive '.'".
这里发生什么了?health看上去像一个服务域,解析器想要一个插件名称,但下一个标签却是”.”, . 不是插件。实际上Corefile应该这样写:1
2
3
4. {
whoami
health
}
health文档的意思是,一旦health被检测到,它就是CoreDNS全局可用的,就算在某个服务器中你并没有声明health插件。
Setups
你可以在该章节找到一系列CoreDNS的配置。所有的设置都默认你不是root用户,因此不能监听53端口。在此我们以1053端口为代替(通过 -dns.port选项)。在每次设置中,都会默认读取Corefile中的配置。所以并不需要在启动时添加 -conf选项。因此下面两个命令是相同的:1
2./coredns -dns.port=1053 -conf Corefile
./coredns -dns.port=1053
所有的nds查询可以通过dig工具生成,dig是调试dns的黄金标准。完整的命令如下:1
dig -p 1053 @localhost +noall +answer <name> <type>
但我们在下面的配置简略了命令,所有 dig www.example.org A 的完整命令是1
dig -p 1053 @localhost +noall +answer www.example.org A
来自文件的权威服务
本次设置会使用file 插件,并请注意 redis插件能够从一个Redis数据库进行权威服务。让我们开始使用file插件进行设置吧!
我们建立一个称为DNS zone 的文件,你可以指定该文件为任意名称,file插件并不会在意,
我们放在文件中的数据将作用于 example.org服务域。
在当前目录新建一个名为 db.example.org 的文件,并写入以下内容:1
2
3
4
5
6
7
8
9
10
11
12
13
14$ORIGIN example.org.
@ 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. (
2017042745 ; serial
7200 ; refresh (2 hours)
3600 ; retry (1 hour)
1209600 ; expire (2 weeks)
3600 ; minimum (1 hour)
)
3600 IN NS a.iana-servers.net.
3600 IN NS b.iana-servers.net.
www IN A 127.0.0.1
IN AAAA ::1
最后两行定义www.example.org 拥有 127.0.0.1和 ::1(IPv6)两个地址。
下一步,新建一个最小Corefile文件,用于处理指向该服务域的dns查询,并添加log插件用于激活查询日志:1
2
3
4example.org {
file db.example.org
log
}
启动CoreDNS并利用dig 工具进行查询1
2
3$ dig www.example.org AAAA
www.example.org. 3600 IN AAAA ::1
可见设置生效了,因为log插件,我们可以看到查询被记录下来了:1
2
3
4::1 - [22/Feb/2018:10:21:01 +0000] "AAAA IN www.example.org. udp 45 false 4096" NOERROR qr,aa,rd,ra 121 170.195µs
#1.6.2 版本中日志如下:供参考
2019-08-28T09:30:07.315+08:00 [INFO] [::1]:51676 - 59518 "AAAA IN www.example.org. udp 44 false 4096" NOERROR qr,aa,rd 162 0.000241614s
以上日志显示返回地址的后端(::1),以及返回时间。其他记录的信息还有:查询类型(AAAA),查询类(IN),查询名称(www.example.org) ,使用的协议(udp), 请求的字节数,DO位状态以及UDP缓存大小。以上这些是从请求中解析的数据。NOERROR标志响应的开始,意为返回的Response Code,紧接的”qr,aa,rd,ra”是响应的标记集,响应的大小是121字节,最后是得到响应所用时间。
Forward(转发)
CoreDNS可以通过forward 插件转发通信到recursor(DNS服务器)。下面,我们会使用forward插件,并聚焦最基本的设置:转发通信到Google Public DNS(8.8.8.8)和Quad9 DNS(9.9.9.9)。
我们只需要在Corefile中进行配置即可,如果我们希望所有到CoreDNS的DNS查询转发到8.8.8.8或9.9.9.9,那可以这样配置:1
2
3
4. {
forward . 8.8.8.8 9.9.9.9
log
}
注意,你可以指定要向上转发的域名查询,例如1
forward example.com 8.8.8.8 9.9.9.9
只会向上转发在example.com域名中的子域名。
开启CoreDNS并用dig进行查询:1
2$ dig www.example.org AAAA
www.example.org. 25837 IN AAAA 2606:2800:220:1:248:1893:25c8:194
记录日志如下:1
:1 - [22/Feb/2018:10:34:39 +0000] 36325 "AAAA IN www.example.org. udp 45 false 4096" NOERROR qr,rd,ra,ad 73 1.859369ms
转发域名到不同的上游
如果你希望对example.com域名的解析转发到8.8.8.8,而其他域名由/etc/resolv.conf解析,可以这样配置Corefile:1
2
3
4
5
6
7
8
9example.org {
forward . 8.8.8.8
log
}
. {
forward . /etc/resolv.conf
log
}
Kubernetes
federation
Autopath
Metric
caching
Rrecursive Resolver
CoreDNS并没有一个本地的递归解析器,但libunbound具有这种功能。为了让这种配置生效,你需要激活unbound插件,并重新编译CoreDNS。具体命令和配置如下:
- 在plug.cfg文件添加 unbound:github.com/coredns/unbound
- $ go generate
- $ make
注意: unbound 插件需要编译cgo,这意味着coredns二进制文件现在链接了libunbound,不再是一个静态二进制文件。
之后,你可以激活unbound1
2
3
4
5. {
unbound
cache
log
}
cache插件被包含在里面,这样unbound内置的cache被禁用,而cache插件能正常工作。
编写插件
我们已经看过了一系列的插件配置,那如何写自己的插件呢?
你可以在plugin.md 和README.md找到一些有用信息。
通常创建一个最小插件你需要以下文件:
- setup.go和setup_test.go
该文件实现对Corefile配置的解析,一旦在Corefile中发现插件名称,就会调用该插件的setup函数。 - plugin_name.go 和plug_name_test.go:
该文件包含处理DNS查询的逻辑,test.go包含用于测试插件是否正常工作的单元测试代码 - README.md:
插件说明文档 - LICENSE 文件:
调用插件
当CoreDNS要调用一个插件,它会调用ServerDNS方法,ServerDNS有三个参数:
- context.Context
- dns.ResponseWriter : 客户端连接
- *dns.Msg : 客户端请求
ServerDNS返回两个值:一个响应code和一个error,error在errors插件被使用时会被记录到日志中。
响应code告诉CoreDNS,插件链是否写入应答,如果没写入,会由CoreDNS处。根据code的值,我们重利用DNS return codes(来自dns package)。
对于以下code:
SERVFAIL (dns.RcodeServerFailure)
REFUSED (dns.RcodeRefused)
FORMERR (dns.RcodeFormatError)
NOTIMP (dns.RcodeNotImplemented)
COreDNS会进行特殊处理,并假设没有数据写入给客户端。对于其他code,CoreDNS认为插件已经写入数据给客户端。
来自插件的日志(logging)
如果你希望插件也可以写入日志,你需要使用log package。Coredn并不实现log层。标准输出如下:1
2log.Printf("[LEVEL] ...")
# 其中LEVEL可以是:INFO、WARNING或ERROR
通常在返回错误时loggging应该留给更高层处理。但是。如果你希望消化错误并通知用户,在插件中写入日志也是一种选择。
Metrics (数据记录???)
文档
每个插件都应该有一个说明文档
Fallthrough
如何添加插件
一个插件以ServerDNS()方法进行定义。ServerDNS中传入请求(request),然后返回响应(responds)给客户端或者传递给下一插件。如果没有插件处理请求,返回SERVFAIL。
下面将以whoami插件为例讲解如何添加插件,whoami是CoreDNS自带插件,如果Corefile没有指定,whoami会默认加载。
首先一个问题是:我的插件要做什么呢? 。在这里whoami插件的目的是返回客户端的IP、使用协议(UDP,TCP)和请求端口给客户端。
第二个问题是:我的插件要叫什么名字?而这里我们的插件名称是whoami。
插件
一个插件包含以下部分:
- 插件注册(registration)
- 插件解析(setup)
- ServerDNS() 和 Name() 方法
有了这些部分,我们就可以加载并使用它。
Registration
setup.go文件复制处理注册,在setup.go中init函数长这样:1
2
3
4
5
6func init() {
caddy.RegisterPlugin("whoami", caddy.Plugin{
ServerType: "dns",
Action: setupWhoami,
})
}
CoreDNS 采用Caddy服务框架,在caddy.RegisterPlugin中传入的第一个参数是插件名称字符串。
ServerType为”dns”,这是因为CoreDNS是一个DNS服务器;Caddy也支持HTTP服务器。Action指定setup函数,用于解析Corefile。Its job is to return a type that implements the plugin.Handler interface.
通过以上init()函数,每当Corefile解析器读到”whoami”,setupWhoami函数就会被调用。
Setup 函数
由于whoami插件不需要参数,所以setup函数非常简单:1
2
3
4
5
6
7
8
9
10
11
12func 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的符号(tokens)并作用于这些符号。这里我们只检测whoami符号之后是否没有指定参数。如果你需要做更多设置,可能会用到c.Val()和c.Args()以及friends。
whoami完整的setup.go 文件如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50package whoami
import (
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/caddyserver/caddy"
)
func init() {
caddy.RegisterPlugin("whoami", caddy.Plugin{
ServerType: "dns",
Action: setup,
})
}
func setup(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{}
})
return nil
}
```
当然,你也需要编写setup.go的测试代码setup_test.go
```golang
package whoami
import (
"testing"
"github.com/caddyserver/caddy"
)
func TestSetup(t *testing.T) {
c := caddy.NewTestController("dns", `whoami`)
if err := setup(c); err != nil {
t.Fatalf("Expected no errors, but got: %v", err)
}
c = caddy.NewTestController("dns", `whoami example.org`)
if err := setup(c); err == nil {
t.Fatalf("Expected errors, but got: %v", err)
}
}
ServerDNS() 和 Name()
首先让我们看下不重要的Name()方法。Name()方法用来让其他插件确认一个指定插件是否被加载,该方法仅仅返回字符串”whoami”。1
2// Name implements the Handler interface.
func (wh Whoami) Name() string { return "whoami" }
接下来就是重头戏:ServerDNS方法,我们会一行一行解析该方法。1
2
3// 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是 client side ,在w中写入数据后会返回给client响应(response)。所有的客户端连接属性都可以从dns.ResponseWriter中恢复出来。
- r是进来的查询(query)。
ServerDNS方法返回一个int和一个error,int类型参数是DNS RCODE,其值可能是:dns.RcodeServerFailure, dns.RcodeNotImplemented, dns.RcodeSuccess, etc…。一个成功的返回值说明插件已经写入数据。
request.Request是一个辅助结构体,用于提取和缓存一些客户端信息,比如 EDNS0 records 和DNSSEC OK bit。
接下来我们配置返回信息:1
2
3
4a := &dns.Msg{}
a.SetReply(r)
a.Compress = true
a.Authoritative = true
我们创建了一个新的dns.Msg(一个message)称之为a,并复制r中内容,a将返回。此外对a进行一些处理之后,开启信息压缩。
之后我们通过state这个辅助结构体检测传入信息,查看可以返回什么。
1 | ip := state.IP() |
IP()返回客户端的IP地址。Family()返回使用的IP类型(IPv4,IPv6)。依据family我们可能产生一个A或AAAArecord和客户端地址。注意,如果我们没有指定一个TTL(超时时间)则其值为0,说明这些record不会被缓存。
接下来,编码客户端的源端口和使用协议:1
2
3
4
5
6srv := &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 records进行编码,在域名前添加”_tcp”或”_udp”前缀,SRV记录中的端口号重用了客户端端口号。
所以我们会产生这样的信息:
_tcp.example.org. 0 IN SRV 0 0
。
在ServerDNS方法的最后,我们创建完整的信息,并发送:1
2
3
4
5
6a.Extra = []dns.RR{rr, srv}
state.SizeAndDo(a)
w.WriteMsg(a)
return 0, nil
首先将rr和srv这两个资源记录(resource records)添加到应答信息a.Extra中。
之后调用state.SizeOnDo(a),用于在信息中设置正确的EDNS0记录并进行截断位处理。
最后调用w的WriteMsg方法,将信息写回给客户端。
ServerDNS处理成功,返回0和nil。
挂载插件
编辑plugin.cfg,并添加以下一行:1
whoami:whoami
在生成可执行二进制文件时:1
2go generate
go build
使用插件
参照前文