概要

CoreDNS 的插件系统本质上使用了 Caddy 的插件系统。CoreDNS 在此基础上又定义了插件的接口和请求的格式,从而成为 CoreDNS 风格的插件。

CoreDNS 的代码量并不大,基本上 20% 的框架型代码 + 80% 的官方插件,所以还是很容易就能快速摸清楚大体的逻辑。相关的架构和使用文档还可以参考另外一篇博客:CoreDNS 使用与架构分析

备注:下文中的代码分析流程取自 CoreDNS v1.2.6 版本

插件加载的过程

编译过程

在讲 CoreDNS 插件加载的时候,必须先看看 CoreDNS 的编译过程 coredns/Makefile

这个 Makefile 相对比较简单,主 target coredns 没有太多依赖,所以逻辑还是很清晰,重点关注在编译 coredns 之前必须先完成 check 目标,而 check 目标则是:

1
2
.PHONY: check
check: presubmit core/zplugin.go core/dnsserver/zdirectives.go godeps

进行完 presubmit(就是执行 coredns/.presubmit 下的 Bash 脚本),需要生成两个关键的目标:coredns/core/zplugin.gocoredns/core/dnsserver/zdirectives.go,这两个目标都是用同一个方式生成:

1
2
core/zplugin.go core/dnsserver/zdirectives.go: plugin.cfg
	go generate coredns.go

coredns/coredns.go 中,有这么一句 Go generate 的注释:

1
2
3
...
//go:generate go run directives_generate.go
...

当执行 go generate coredns.go 这句命令的时候,将触发以 go:generate 为标记的命令(所谓的 go generate 就是代码中内嵌一些特殊的命令注释,执行特定命令时将触发命令的执行),即:go run directives_generate.go。该命令执行完之后将生成两个文件coredns/core/zplugin.gocoredns/core/dnsserver/zdirectives.go。自动代码的生成依赖于 plugin.cfg 配置文件,所以当配置文件更新时,对应的目标也需要被重新创建。

初始化注册插件

我们来分别看看 coredns/core/zplugin.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
// generated by directives_generate.go; DO NOT EDIT

package plugin

import (
	// Include all plugins.
	_ "github.com/coredns/coredns/plugin/auto"
	_ "github.com/coredns/coredns/plugin/autopath"
	_ "github.com/coredns/coredns/plugin/bind"
	_ "github.com/coredns/coredns/plugin/cache"
	_ "github.com/coredns/coredns/plugin/chaos"
	_ "github.com/coredns/coredns/plugin/debug"
	_ "github.com/coredns/coredns/plugin/dnssec"
	_ "github.com/coredns/coredns/plugin/dnstap"
	_ "github.com/coredns/coredns/plugin/erratic"
	_ "github.com/coredns/coredns/plugin/errors"
	_ "github.com/coredns/coredns/plugin/etcd"
	_ "github.com/coredns/coredns/plugin/federation"
	_ "github.com/coredns/coredns/plugin/file"
	_ "github.com/coredns/coredns/plugin/forward"
	_ "github.com/coredns/coredns/plugin/health"
	_ "github.com/coredns/coredns/plugin/hosts"
	_ "github.com/coredns/coredns/plugin/kubernetes"
	_ "github.com/coredns/coredns/plugin/loadbalance"
	_ "github.com/coredns/coredns/plugin/log"
	_ "github.com/coredns/coredns/plugin/loop"
	_ "github.com/coredns/coredns/plugin/metadata"
	_ "github.com/coredns/coredns/plugin/metrics"
	_ "github.com/coredns/coredns/plugin/nsid"
	_ "github.com/coredns/coredns/plugin/pprof"
	_ "github.com/coredns/coredns/plugin/proxy"
	_ "github.com/coredns/coredns/plugin/reload"
	_ "github.com/coredns/coredns/plugin/rewrite"
	_ "github.com/coredns/coredns/plugin/root"
	_ "github.com/coredns/coredns/plugin/route53"
	_ "github.com/coredns/coredns/plugin/secondary"
	_ "github.com/coredns/coredns/plugin/template"
	_ "github.com/coredns/coredns/plugin/tls"
	_ "github.com/coredns/coredns/plugin/trace"
	_ "github.com/coredns/coredns/plugin/whoami"
	_ "github.com/mholt/caddy/onevent"
)

很简单,就是 import 语句,但是并不真正使用对应的 package,这又是为什么呢 ?其实就是为了执行每个 package 的 init 方法。

我们仔细观察 coredns/plugin/ 下的每个插件,都有一个 setup.go,每个 setup.go 都有类似的 init()

1
2
3
4
5
6
func init() {
	caddy.RegisterPlugin("auto", caddy.Plugin{
		ServerType: "dns",
		Action:     setup,
	})
}

即调用 Caddy 的逻辑注册插件,其中 Action 是一个函数 setup()

1
2
3
4
func setup(c *caddy.Controller) error {
    // 注册插件的时候将会运行这个函数
    // 主要用于配置解析等一些初始化工作
}

综上,当引用了 coredns/zplugin.go 时,将按照 import 中给出的顺序依次支持每个插件的 init() 来注册插件,当 Caddy 服务器启动时,将按序执行已注册插件的 setup() 以此来完成插件的初始化动作

再来看看 coredns/core/dnsserver/zdirectives.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
// generated by directives_generate.go; DO NOT EDIT

package dnsserver

// Directives are registered in the order they should be
// executed.
//
// Ordering is VERY important. Every plugin will
// feel the effects of all other plugin below
// (after) them during a request, but they must not
// care what plugin above them are doing.
var Directives = []string{
	"metadata",
	"tls",
	"reload",
	"nsid",
	"root",
	"bind",
	"debug",
	"trace",
	"health",
	"pprof",
	"prometheus",
	"errors",
	"log",
	"dnstap",
	"chaos",
	"loadbalance",
	"cache",
	"rewrite",
	"dnssec",
	"autopath",
	"template",
	"hosts",
	"route53",
	"federation",
	"kubernetes",
	"file",
	"auto",
	"secondary",
	"etcd",
	"loop",
	"forward",
	"proxy",
	"erratic",
	"whoami",
	"on",
}

同样也很简单,只是定义了一个 Directives 的字符串数组。这个变量将在 coredns/core/dnsserver/register.go 中用到:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Any flags defined here, need to be namespaced to the serverType other
// wise they potentially clash with other server types.
func init() {
	flag.StringVar(&Port, serverType+".port", DefaultPort, "Default port")

	caddy.RegisterServerType(serverType, caddy.ServerType{
		Directives: func() []string { return Directives },
		DefaultInput: func() caddy.Input {
			return caddy.CaddyfileInput{
				Filepath:       "Corefile",
				Contents:       []byte(".:" + Port + " {\nwhoami\n}\n"),
				ServerTypeName: serverType,
			}
		},
		NewContext: newContext,
	})
}

其实就是将 Directives 作为一个参数传递给 caddy.ServerType{},并最终在 caddy/caddy.go 中的 executeDirectives() 使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func executeDirectives(inst *Instance, filename string,
	directives []string, sblocks []caddyfile.ServerBlock, justValidate bool) error {
    ...
	for _, dir := range directives {
		for i, sb := range sblocks {
                ...
			for j, key := range sb.Keys {
				// Execute directive if it is in the server block
				if tokens, ok := sb.Tokens[dir]; ok {
					...
					setup, err := DirectiveAction(inst.serverType, dir)
					if err != nil {
						return err
					}

					err = setup(controller)
					...
				}
			}
		}
		...
	}
	...
}

这段代码将Directives 中顺序执行对应插件之前注册的 setup()

zplugin.gozdirectives.go 都是由 coredns/directives_generate.go 生成,并依赖于 plugin.cfg

plugin.cfg 是一个很简单的配置文件:

1
2
3
4
5
6
metadata:metadata
tls:tls
reload:reload
nsid:nsid
root:root
...

每一行由冒号分割,第一部分是插件名,第二部分是插件的包名(可以是一个完整的外部地址,如 log:github.com/coredns/coredns/plugin/log),且插件在 plugin.cfg 中的顺序就是最终生成文件中对应的顺序

coredns/directives_generate.go 的逻辑也比较简单,基本上就是打开文件,按行解析文件并生成对应的 importDirectives

虽然 plugin.cfg 中定义了大量的默认插件,且编译的时候将其全部编译成一个二进制文件,但实际运行过程中并不会全部执行,CoreDNS 在处理请求过程中只会运行配置文件中所需要的插件

入口逻辑

CoreDNS 的入口在 coredns/coredns.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

//go:generate go run directives_generate.go

import (
	"github.com/coredns/coredns/coremain"

	// Plug in CoreDNS
	_ "github.com/coredns/coredns/core/plugin"
)

func main() {
	coremain.Run()
}

非常简单,直接调用一个 coremain.Run() 并开始运行。此处我们注意到 import 有一个引用了但是并没真正使用的 package github.com/coredns/coredns/core/plugin,而这个包下面只有一个文件,即 coredns/core/zplugin.go。经过上文的分析,这样将在 Run() 开始执行之前执行各个插件的 init() 动作,即调用 Caddy 相关的函数注册插件。

coremain.Run() 逻辑同样也很简单:

 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
package coremain

import (
    ...

	"github.com/coredns/coredns/core/dnsserver"
	
	...
)

func Run() {
	caddy.TrapSignals()
	...

	// 获取 Caddy 的配置,生成对应的配置文件结构 corefile
	corefile, err := caddy.LoadCaddyfile(serverType)
	...

	// 以 corefile 为配置启动 Caddy
	instance, err := caddy.Start(corefile)
	...
	
	// Execute instantiation events
	caddy.EmitEvent(caddy.InstanceStartupEvent, instance)

	// Twiddle your thumbs
	instance.Wait()
}

import 开头中将执行 dnsserver package 的 init(),即 register.go 中的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func init() {
	flag.StringVar(&Port, serverType+".port", DefaultPort, "Default port")

	caddy.RegisterServerType(serverType, caddy.ServerType{
		Directives: func() []string { return Directives },
		DefaultInput: func() caddy.Input {
			return caddy.CaddyfileInput{
				Filepath:       "Corefile",
				Contents:       []byte(".:" + Port + " {\nwhoami\n}\n"),
				ServerTypeName: serverType,
			}
		},
		NewContext: newContext,
	})
}

这是使用 Caddy 的接口注册一个服务器类型,即 DNS 服务器,其中 NewContext 字段是一个对应业务服务器的生成器:

1
2
3
func newContext(i *caddy.Instance) caddy.Context {
	return &dnsContext{keysToConfigs: make(map[string]*Config)}
}

即将生成 dnsContext{} 结构,该结构满足 caddy/plugins.goContext 的接口定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
type Context interface {
	// Called after the Caddyfile is parsed into server
	// blocks but before the directives are executed,
	// this method gives you an opportunity to inspect
	// the server blocks and prepare for the execution
	// of directives. Return the server blocks (which
	// you may modify, if desired) and an error, if any.
	// The first argument is the name or path to the
	// configuration file (Caddyfile).
	//
	// This function can be a no-op and simply return its
	// input if there is nothing to do here.
	InspectServerBlocks(string, []caddyfile.ServerBlock) ([]caddyfile.ServerBlock, error)

	// This is what Caddy calls to make server instances.
	// By this time, all directives have been executed and,
	// presumably, the context has enough state to produce
	// server instances for Caddy to start.
	MakeServers() ([]Server, error)
}

而在 dnsContext{} 中对应的 MakeServers() 方法将创建自定义的 Server 来处理服务器请求。

说白了,coremain.Run() 执行之后,我们将创建一个 Caddy 的服务器,服务器中接收到的请求将由我们自定义的 DNS 服务器来处理

Plugin 的设计

CoreDNS 采用的是插件链(Plugin chain) 的方式来执行插件的逻辑,也就是可以把多个插件的执行简单理解为以下的伪代码:

1
2
3
for _, plugin := range plugins {
    plugin()
}

其中插件链中的插件和顺序可从配置文件中配置。

一个请求在被插件处理时,大概有以下几种情况(可参考文章):

  • 请求被当前插件处理,处理完返回对应的响应,至此插件的执行逻辑结束,不会运行插件链的下一个插件

  • 请求被当前插件处理之后跳至下一个插件,即每个插件将维护一个 next 指针,指向下一个插件,转至下一个插件通过 NextOrFailure() 实现;

  • 请求被当前插件处理之后增加了新的信息,携带这些信息将请求交由下一个插件处理

很明显,写一个插件必须符合一定的接口要求,CoreDNS 在 coredns/plugin/plugin.go 中定义了:

1
2
3
4
5
6
7
8
type (
	Handler interface {
		// 每个插件处理请求的逻辑
		ServeDNS(context.Context, dns.ResponseWriter, *dns.Msg) (int, error)
		// 返回插件名
		Name() string
	}
)

每一个插件都会定义一个结构体(如果不需要对应数据结构就设置一个空的结构体),并为这个对象实现对应的接口,比如 whoami 插件的 whoami.go 是这样做的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 定义一个空的 struct
type Whoami struct{}

// 给 Whoami 对象实现 ServeDNS 方法
// w 是用来写入响应
// r 是接收到的 DNS 请求
func (wh Whoami) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
    ...
}

// 给 Whoami 对象实现 Name 方法
// 只需要简单返回插件名字的字符串即可
func (wh Whoami) Name() string { return "whoami" }

对于每一个插件,其 setup.go 中的 setup() 中都有这么个函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func setup(c *caddy.Controller) error {
    // 通常前面是做一些参数解析的逻辑
    
    // dnsserver 层添加插件
    // next 表示的是下一个插件
    dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
		l.Next = next
		return l
	})
	
	...  
}

setup()AddPlugin() 将一个函数对象添加到一个插件列表中:

1
2
3
func (c *Config) AddPlugin(m plugin.Plugin) {
	c.Plugin = append(c.Plugin, m)
}

对于每一个配置块,都有一个 c.Plugin 的列表,如果在配置块中插件顺序是 ABCD,那么对应到 c.Plugin 这个插件列表中位置也同样是 ABCD

AddPlugin() 添加的元素是 plugin.Plugin 类型:

1
2
3
func(next plugin.Handler) plugin.Handler {
    ...
}

其中 next 表示的是下一个插件对象。

当我们使用 NewServer() 创建对应的 Caddy 服务器之前,我们对每一个配置块会创建好一个插件列表(如上所示)c.Plugin,而在执行 NewServer() 时将根据 c.Plugin 的内容创建 c.PluginChain,即真正处理请求的插件链:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func NewServer(addr string, group []*Config) (*Server, error) {
    ...
    	var stack plugin.Handler
    	// 从插件列表的最后一个元素开始
		for i := len(site.Plugin) - 1; i >= 0; i-- {
			// stack 作为此时插件的 next 参数
			// 如果配置文件中的插件顺序是 A,B,C,D,首次初始化时添加到列表就会变成 D,C,B,A
			// 从最后一个元素 A,开始依次调用对应的 plugin.Handler,将有:
			// A: next=nil
			// B: next=A
			// C: next=B
			// D: next=C
			// 最终插件从 D 开始,即原来配置顺序的最后一个
			// 最终的执行顺序为配置文件插件顺序的逆序
			stack = site.Plugin[i](stack)

			// register the *handler* also
			site.registerHandler(stack)
			...
		}
		// 这时的插件是配置文件顺序中的最后一个
		site.pluginChain = stack
	...
}

当执行完之后,site.pluginChain 指向原始配置文件中插件顺序的最后一个插件,也就是说,插件链中的插件顺序与配置文件中的插件顺序是相反的,后面我们也将看到,插件的执行顺序是按照插件链的顺序进行,即是插件配置顺序的逆序

CoreDNS 如何处理请求

在了解 CoreDNS 如何处理请求之前,我们需要重点看看 coredns/core/dnsserver 这个 package 的逻辑。

这个 package 定义一个 DNS 服务器,并将其注册到 Caddy 的运行逻辑中,从而接管请求的处理流程。

由上文可知,dnsContext{} 实现了 Caddy 的 Context 接口,其中比较关键的是 MakeServers() 的实现,即:

 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
// coredns/core/dnsserver/register.go
func (h *dnsContext) MakeServers() ([]caddy.Server, error) {
    ...
	var servers []caddy.Server
	// 由于我们可以定义多个 group 来对不同的域名做解析
	// 每个 group 都将创建一个不同的 DNS server 的实例
	for addr, group := range groups {
		// switch on addr
		switch tr, _ := parse.Transport(addr); tr {
		case transport.DNS:
			s, err := NewServer(addr, group)
			if err != nil {
				return nil, err
			}
			servers = append(servers, s)

		case transport.TLS:
			s, err := NewServerTLS(addr, group)
			if err != nil {
				return nil, err
			}
			servers = append(servers, s)

		case transport.GRPC:
			s, err := NewServergRPC(addr, group)
			if err != nil {
				return nil, err
			}
			servers = append(servers, s)

		case transport.HTTPS:
			s, err := NewServerHTTPS(addr, group)
			if err != nil {
				return nil, err
			}
			servers = append(servers, s)
		}

	}

	return servers, nil
}

MakeServers() 的逻辑可以看出,如果当前使用的是 DNS 协议,那么将会为每个 group 调用 NewServer() 创建一个 DNS Server。

不同协议的请求(DNS/TLS/gRPC/https)最终都会调用 ServeDNS() 来处理每一个请求,这也是最主要的核心逻辑:

 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
// coredns/core/dnsserver/server.go
func (s *Server) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) {
    ...
    // 如果请求落在对应的 zone,执行 zone 内的插件
    if h, ok := s.zones[string(b[:l])]; ok {
        ...
        if r.Question[0].Qtype != dns.TypeDS {
             // 如果没有过滤函数
        		if h.FilterFunc == nil {
        		   // 执行插件链上上的插件
        		   // 如果插件中有 NextOrFailure() 则将跳至下一个插件
        		   // 否则则直接返回
					rcode, _ := h.pluginChain.ServeDNS(ctx, w, r)
					if !plugin.ClientWrite(rcode) {
						DefaultErrorFunc(ctx, w, r, rcode)
					}
					return
				}
				// FilterFunc is set, call it to see if we should use this handler.
				// This is given to full query name.
				if h.FilterFunc(q) {
					rcode, _ := h.pluginChain.ServeDNS(ctx, w, r)
					if !plugin.ClientWrite(rcode) {
						DefaultErrorFunc(ctx, w, r, rcode)
					}
					return
				}
        } 
    }
    ...
}

至此,整个 CoreDNS 插件系统的基本逻辑告一段落。从整体来看,CoreDNS 不是一个复杂的系统,简简单单,但正是这种简洁的插件系统,才让 CoreDNS 渐渐演化出一个插件小生态,让 DNS 在原本的基础功能之上增加了更多扩展性功能,而最新的 CNCF 新闻,CoreDNS 已经从 CNCF 孵化项目中毕业,成为一个真正成熟的容器平台 DNS 方案的既定标准,可想而知,在不远的将来,CoreDNS 生态将枝繁叶茂