CoreDNS 指南

Translate CoreDNS Manual

什么是CoreDNS

CoreDNS是用go语言编写的DNS服务器。
CoreDNS是由插件驱动的。插件相当于CoreDNS的API,你可以编写自定义的API来添加功能。
但首先,你需要熟悉go语言和DNS的工作原理。

安装

源码安装

1
2
3
4
5
6
7
$ mkdir -p $GOPATH/src/github.com/coredns
$ cd $GOPATH/src/github.com/coredns/
$ wget https://github.com/coredns/coredns/archive/v1.6.2.tar.gz
$ tar -zxvf v1.6.2.tar.gz
$ mv coredns-1.6.2 coredns
$ cd coredns
$ make

检测版本

1
2
3
$ ./coredns -version
$ CoreDNS-1.6.2
$ linux/amd64, go1.12.5,

测试

在1053端口开启服务

1
2
3
4
5
6
[root@A coredns]# ./coredns -dns.port=1053
.:1053
2019-08-27T15:36:35.491+08:00 [INFO] CoreDNS-1.6.2
2019-08-27T15:36:35.491+08:00 [INFO] linux/amd64, go1.12.5,
CoreDNS-1.6.2
linux/amd64, go1.12.5,

客户端连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[root@A coredns]# dig @localhost -p 1053 a whoami.example.org

; <<>> DiG 9.9.4-RedHat-9.9.4-74.el7_6.1 <<>> @localhost -p 1053 a whoami.example.org
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 15693
;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 3
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;whoami.example.org. IN A

;; ADDITIONAL SECTION:
whoami.example.org. 0 IN AAAA ::1
_udp.whoami.example.org. 0 IN SRV 0 0 60562 .

;; Query time: 1 msec
;; SERVER: ::1#1053(::1)
;; WHEN: Tue Aug 27 16:08:21 CST 2019
;; MSG SIZE rcvd: 135

插件

一旦CoreDNS解析配置并启动,它会运行各个服务器。每个服务器都有自己的服务域和监听端口,而且拥有自己的插件链。
CoreDNS处理处理一个DNS查询时,会经过以下步骤:

  1. 如果多个服务都在监听DNS查询端口,会选择服务域后缀最长匹配的服务器。比如有服务器A,服务域为example.com;服务器B,服务域为a.example.com,两者都监听53端口。现在有DNS在53端口查询www.a.example.org, 则会有B服务器处理该查询。
  2. 确认服务器后,会遍历服务器配置文件中配置的插件,默认的遍历顺序在plugin.cfg文件中设置。
  3. 每个插件会检查查询请求,并决定是否处理这个请求,此时可能有以下情况发生
    • 插件处理该查询
    • 插件不处理该查询
    • 插件处理该查询,但在中途调用插件链中的下一个插件,处理落空
    • 插件处理该查询,添加hint,后调用下一个插件

插件处理一个查询表示插件会对客户端做出响应。

查询被处理

插件处理查询,返回给客户端一个响应(或其他行为)。查询处理到此结束,不会调用下一个插件

查询不被处理

如果插件决定不处理该查询,会调用下一个插件,如果插件链中的最后一个插件也不处理该查询,CoreDNS返回 SERVFALL给 客户端。

查询处理落空

插件处理查询,但该插件的后端返回信息告知需要其他插件进行处理。如果处理落空,下一个插件会被调用。一个具有该行为的插件是 host,host插件首先检查/etc/hosts中的表项,如果它发现一个结果,那就返回该结果,如果没有,那就落空,由下一个插件处理该查询。

查询被处理,并添加hint

插件处理查询,并添加hint,之后调用下一个插件。下一个插件能看到给客户端的响应信息。

未注册插件

未注册拆件不处理DNS数据,但可能以其他方式影响CoreDNS的行为。

插件解析

一个插件包含 Setup、Registration、Handler 部分。

  1. Setup :解析插件配置和插件指令。
  2. Handler :包含处理请求的代码,并实现所有逻辑。
  3. Registration: 将插件注册到CoreDNS,该步骤发生在CoreDNS进行编译的时候。所有的注册插件都可以被服务器使用,至于在运行时调用哪个插件则在Corefile中配置。

插件文档

https://coredns.io/plugins

配置

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
3
dns://    标准的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
2
3
. {
chaos CoreDNS-001 info@coredns.io
}

也有插件可以将配置信息写到{}中
例:

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
21
coredns.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
5
health

. {
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
4
example.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
9
example.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,不再是一个静态二进制文件。
    之后,你可以激活unbound
    1
    2
    3
    4
    5
    . {
    unbound
    cache
    log
    }

cache插件被包含在里面,这样unbound内置的cache被禁用,而cache插件能正常工作。

编写插件

我们已经看过了一系列的插件配置,那如何写自己的插件呢?
你可以在plugin.md 和README.md找到一些有用信息。
通常创建一个最小插件你需要以下文件:

  1. setup.go和setup_test.go
    该文件实现对Corefile配置的解析,一旦在Corefile中发现插件名称,就会调用该插件的setup函数。
  2. plugin_name.go 和plug_name_test.go:
    该文件包含处理DNS查询的逻辑,test.go包含用于测试插件是否正常工作的单元测试代码
  3. README.md:
    插件说明文档
  4. LICENSE 文件:

调用插件

当CoreDNS要调用一个插件,它会调用ServerDNS方法,ServerDNS有三个参数:

  1. context.Context
  2. dns.ResponseWriter : 客户端连接
  3. *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
2
log.Printf("[LEVEL] ...")
# 其中LEVEL可以是:INFO、WARNING或ERROR

通常在返回错误时loggging应该留给更高层处理。但是。如果你希望消化错误并通知用户,在插件中写入日志也是一种选择。

Metrics (数据记录???)

文档

每个插件都应该有一个说明文档

Fallthrough

如何添加插件

一个插件以ServerDNS()方法进行定义。ServerDNS中传入请求(request),然后返回响应(responds)给客户端或者传递给下一插件。如果没有插件处理请求,返回SERVFAIL。
下面将以whoami插件为例讲解如何添加插件,whoami是CoreDNS自带插件,如果Corefile没有指定,whoami会默认加载。
首先一个问题是:我的插件要做什么呢? 。在这里whoami插件的目的是返回客户端的IP、使用协议(UDP,TCP)和请求端口给客户端。
第二个问题是:我的插件要叫什么名字?而这里我们的插件名称是whoami。

插件

一个插件包含以下部分:

  1. 插件注册(registration)
  2. 插件解析(setup)
  3. ServerDNS() 和 Name() 方法
    有了这些部分,我们就可以加载并使用它。

Registration

setup.go文件复制处理注册,在setup.go中init函数长这样:

1
2
3
4
5
6
func 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
12
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的符号(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
50
package 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}

该函数传入了三个参数:

  1. w是 client side ,在w中写入数据后会返回给client响应(response)。所有的客户端连接属性都可以从dns.ResponseWriter中恢复出来。
  2. 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
    4
    a := &dns.Msg{}
    a.SetReply(r)
    a.Compress = true
    a.Authoritative = true

我们创建了一个新的dns.Msg(一个message)称之为a,并复制r中内容,a将返回。此外对a进行一些处理之后,开启信息压缩。
之后我们通过state这个辅助结构体检测传入信息,查看可以返回什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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类型(IPv4,IPv6)。依据family我们可能产生一个A或AAAArecord和客户端地址。注意,如果我们没有指定一个TTL(超时时间)则其值为0,说明这些record不会被缓存。
接下来,编码客户端的源端口和使用协议:

1
2
3
4
5
6
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 records进行编码,在域名前添加”_tcp”或”_udp”前缀,SRV记录中的端口号重用了客户端端口号。
所以我们会产生这样的信息:

_tcp.example.org. 0 IN SRV 0 0

在ServerDNS方法的最后,我们创建完整的信息,并发送:

1
2
3
4
5
6
a.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
2
go generate
go build

使用插件

参照前文