CoreDNS全解—CoreDNS插件开发入门

前序

在之前的文章里提过,伴随云原生应用概念以及CNCF社区推广,CoreDNS 慢慢作为 K8S 集群的官方DNS服务,正在替代 KubeDNS 作为 K8S 集群的服务发现组件,但是,CoreDNS 因为 Golang 编写的高性能、“插件式”支持等特性,使得 CoreDNS 又不仅仅可以应用在 K8S 体系内,作为一款DNS应用,CoreDNS 可以借助众多插件,扩展其能力,进而应用在更多复杂的场景中,比如:整个公司级的基于服务发现和注册中心的DNS服务能力。

CoreDNS 自然不仅仅可以应用在这么大型的场景下,由于 CoreDNS 本身基于 Golang 编写,所以,利用其跨平台编译的能力,你也可以将其替换 DNSmasq、PowerDNS、SmartDNS 等家庭或个人的私有服务器上,比如,利用 CoreDNS 的 TLS based DNS 查询能力或 gRPC 查询能力,可以防止运营商的 DNS 污染,如果个人开发者具备一定能力,也可以编写插件,实现 SmartDNS(此DNS应用并不开源)的功能,是非常简单的一个事。

本文就主要聊一聊 CoreDNS 插件编写这个事情。CoreDNS 插件开发分为2个部分,CoreDNS 插件编写概述,以及 CoreDNS插件开发进阶。本文,便是第一篇。

其实,CoreDNS 要展开来讲东西还挺多,本文主要讲插件开发,后续,我会专门编写一个 《CoreDNS 全解析》的中文电子书,涵盖关于 CoreDNS 的更多内容。

CoreDNS 插件编写概述

概述篇主要包含2部分内容:

  1. 如何实现插件
  2. 如何注册插件

我相信在你读完整篇内容后,会有一个基本的认知。说的再多不如一次实战。等你自己写插件的时候,带着写插件的基本认知,再去看看其他插件怎么写的,就会有自己的一个体会了。

写插件的话,基本上来说,可以参考的插件推荐列表:

  1. hosts
  2. forward
  3. kubernetes

这几个插件的实现复杂度是由浅入深的。在有些文章里还在提 proxy 插件,我个人是不推荐再去看了。因为首先,从 v1.5.0(截止2019.04.23的最新稳定版) 版本开始,官方已经将其从默认加载的插件列表移除了。再一个,forward 插件很多功能和 forward 类似,完全可以用 forward 插件,替代 proxy 插件。

开始

CoreDNS的能力,和插件非常密切,有什么插件,就有什么样的能力,所以,我们如何编写自己的插件呢?

目前,CoreDNS 的插件机制,是借助 Caddy 项目来做的。所以,虽然本文在讲 CoreDNS 如何编写插件,但是其中有很多插件开发的内容,是和如何利用 Caddy 项目编写插件有相似之处的。

CoreDNS官方的介绍中提到,如果你想自己编写插件,并且希望这个插件能够被官方默认包含到 CoreDNS 项目中去的话,你需要先在 github 上提一个 issue 并在里边描述一下你的插件的设计。所以,打动官方开发人员也是蛮重要的。

如何注册插件

我们先不说怎么实现插件,先说一个插件,怎么把一个插件,注册到 CoreDNS 中。

插件的注册很简单,你需要在创建的插件包(package)中,写一个 init 函数。插件的注册,就放到 init 函数中(Golang的 init 函数会在包被使用的时候就自动执行)。

1
2
3
4
5
6
7
8
import "github.com/mholt/caddy"

func init() {
caddy.RegisterPlugin("foo", caddy.Plugin{
ServerType: "dns",
Action: setup,
})
}

每个插件都必须有一个名称,上面的例子中,插件名称是 fooServerType 必须且只能是 dns 。这里描述的 Action 就是说只要在 Corefile 中遇到指令 foo,CoreDNS 就会调用一个名为 setup 的函数。

setup:插件参数解析与插件注册

上面提到在插件包的 init 函数中,怎么做插件注册,其中,Action 字段的值,就是一个 setup 函数。而 add.RegisterPlugin 有2个参数,第一个是插件名称,第二个是插件的实体。这2个参数,保证了这个插件的唯一性。可以说,Action 字段的内容——setup函数,是为了 做CoreDNS解析和执行Corefile时运行的函数。也是我们马上要讲解的内容。

那么,setup 函数具体应该是什么样子的?我们看着例子来说:

1
2
3
4
5
6
7
func setup(c *caddy.Controller) error {
if err != nil {
return plugin.Error("foo", err)
}

return nil
}

简单来说,setup 这个函数,它接受一个类型为 caddy.Controller 的参数。返回值是一个 Golang 的 error 类型(在这个例子中,使用 plugin.Error 方法,做了一个对原有 error 的 wrap 封装,形成一个带有 plugin/foo 前缀的 error)。

setup 函数的主要职责,就是是解析指令的标记用的。我们看一下之 hosts 插件的语法:

1
2
3
4
5
6
7
hosts [FILE [ZONES...]] {
[INLINE]
ttl SECONDS
no_reverse
reload DURATION
fallthrough [ZONES...]
}

hosts 插件,怎么解析 ttl、no_reverse、reload 等指令呢?其实就是在 setup 函数里做的解析。如果我们自己手写这种解析,十分麻烦,但是,借助 setup 函数的参数: *caddy.Controller 就很容易做这样的解析了,这就是 setup 为什么需要这么一个参数的根本原因。

按照上面的例子,我们的 setup 方法怎么做插件参数的解析?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 下面的 c 指的其实就是  *caddy.Controller

func setup(c *caddy.Controller) error {

// ...

// 参数解析
for c.Next() { // 跳过指令名称,也就是跳过 "hosts" 这个名称
args := c.RemainingArgs() // 获取所有参数,也就是 [FILE [ZONES...]] 对应的实际内容
for c.NextBlock() { // 获取下一个块内容,并遍历
switch c.Val() { // 获取指令名
case "fallthrough":
h.Fall.SetZonesFromArgs(c.RemainingArgs())
case "no_reverse":
options.autoReverse = false
case "ttl":
...
}
}
}

//...

}

最后,其实插件的注册,也是在 setup 函数内做的,插件的注册方式如下:

1
2
3
4
5
6
7
8
9
10
func setup(c *caddy.Controller) error {
// ....
// 参数解析
// ...
// 插件注册
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
k.Next = next
return k
})
}

我们看一个相对完成的 setup 函数示例:

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
func setup(c *caddy.Controller) error {
// See comment in the init function.
os.Stderr = os.Stdout

// 插件参数解析,并返回一个插件实体(先不用管插件实例怎么写,后边会介绍)
// 参数解析,放到了其他函数内,防止一大坨代码
k, err := paramsParse(c)
if err != nil {
return plugin.Error("multikube", err)
}
// 插件初始化
err = k.InitKubeCacheMulti()
if err != nil {
return plugin.Error("multikube", err)
}

k.RegisterKubeCache(c)

// 插件注册
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
k.Next = next
return k
})

return nil
}

将你的插件添加到CoreDNS

方式1:

1、把插件添加到CoreDNS,单纯从代码上来说,就是得把下面的一行(你的插件包)

1
import _ "your/plugin/package/path/you-package"

添加到 core/coredns.go 中,这一步可以保证你的包能编译到CoreDNS可执行文件中。

2、但是,只是编译进去了,并不表示这个插件在CoreDNS可以启用,所以,你还得把它添加到 directive.go,在这个文件中,你需要把你的插件名称,添加到 directives 这个 slice 里去。需要注意的是,slice 里插件的顺序非常的重要,这个顺序,决定了CoreDNS处理DNS请求时所执行的插件顺序(我们之前的文章提过,CoreDNS的插件执行顺序,并不是在Corefile中定义的。其实本质上,就是这个 slice 里定义的)。

方式2:

1、上面的2个操作,还是有点麻烦。有更简单的方式:修改 plugin.cfg ,添加你的插件(注意:在这个里边定义好插件顺序,就是 CoreDNS 编译后插件的执行顺序)

2、执行 go generate coredns.go 即可自动生成 directive.gocore/coredns.go。然后执行构建即可。

CoreDNS里的插件的实现逻辑

前面已经提到了如果自己写插件,插件怎么做参数解析(在 setup 函数内),怎么注册到CoreDNS(也是在 setup 函数内)、怎么定义插件顺序编译,没有提的是,我们自己的插件,到底应该如何实现具体的逻辑。先回顾一下 setup 函数的基本示例:

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
func setup(c *caddy.Controller) error {
// See comment in the init function.
os.Stderr = os.Stdout

// 1、插件参数解析,并获得具体插件的实例化对象
// 我们既要要讲的就是这个 k,怎么实现。
k, err := paramsParse(c)
if err != nil {
return plugin.Error("multikube", err)
}
// 2、插件初始化
err = k.InitKubeCacheMulti()
if err != nil {
return plugin.Error("multikube", err)
}

k.RegisterKubeCache(c)

// 3、插件注册
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
k.Next = next
return k
})

return nil
}

我们这次要讲的就是,插件实例化对象 怎么编写。

如果你去看 godoc for the plugin package 的话,里边有一个非常最重要的类型是 plugin.Handler。这个 Handler 是一个处理DNS请求的函数。虽然CoreDNS会为你设置启动DNS服务器的各项事宜,但是,插件的实例(也就是你插件的功能)你还得自己来实现(实现 plugin.Handler 接口)。

实现插件接口

plugin.Handler 是一个类似于 http.Handler 的接口(写过Golang的HTTP Server的同学一定不陌生),不同的是,它是处理 DNS 请求用的,这个接口里边,有一个 ServeDNS 方法(这个方法,返回 int,error)。 int 是 DNS rcode,也就是 DNS 响应状态码,error 是 处理 DNS 请求过程中的错误。有关这些返回值的更多详细信息,请阅读 plugin.md 文档。

前面提到,plugin.Handler 仅仅是一个接口,我们需要创建一个结构体类型来实现此接口,通常来说,实现这个接口的结构体大概是这样的:

1
2
3
type MyHandler struct {
Next plugin.Handler
}

然后,为这个接口体,添加 ServeDNS 方法,这个方法是真正处理DNS请求的方法:

1
2
3
4
5
6
7
8
func (h MyHandler) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {

// 此处是具体实现
// ...

// 最后,如果前面的逻辑未能找到DNS记录,可以将DNS请求转给下一个插件处理
return h.Next.ServeDNS(ctx, w, r)
}

plugin.Handler 还需要一个方法func Name()string,它主要返回当前的插件名称。

插件编写总结

  1. coredns/plugin 目录下创建自己的插件 package
  2. 在插件 package 下,创建 setup.go 在其中的 init 方法中,做插件配置工作(init 调用 setup 方法)
  3. setup 方法中做3个事情:参数解析、实例化插件实体、将插件实体注册到CoreDNS中。
  4. 实现插件实现:实现 ServeDNSName 方法。
  5. 更改 plugin.cfg 文件添加你的插件,执行 go generate coredns.go 生成代码,最后执行 go build (或者make)构建。
Donate comment here