在上一篇文章中,我们了解了 CoreDNS 的基本概念、安装和配置文件的说明,与 CoreDNS 中的插件,本文将从零开始,深入学习 CoreDNS 插件开发的完整流程,让您能够开发出满足特定业务需求的高质量插件。在最后我们学习一下 CoreDNS 中的外部插件用以引入开发思路,来解决我们平台中的一些难点。

代码构成

在学习开发插件之前,我们先需要了解下 CoreDNS 的代码目录构成。

text
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
coredns/
├── core/                  # 核心组件
│   ├── dnsserver/         # DNS 服务器实现
│   └── plugin/            # 插件管理框架
├── plugin/                # 内置插件目录
│   ├── cache/              # 缓存插件
│   ├── forward/            # 转发插件
│   ├── kubernetes/         # Kubernetes插件
│   └── ...                 # 还有很多插件
├── request/              # 请求处理相关,封装了一些处理 DNS 请求的上下文信息,这部分在插件中用的比较多
└── test/                 # 测试工具和用例

插件开发核心概念

在 CoreDNS 中,插件开发核心概念是 “注册” [1] (Register) ,详细的步骤大致分为如下几步:

  1. Configration, 用户通过 CoreDNS 的默认 Corefile 完成配置加载,这个文件也可以按照第一章中提到的 -conf 参数来指定。
  2. Setup, 解析配置和内部数据结构,并完成初始化。
  3. Handler, 是 Plugin 的入口,用户必须对自己的插件继承这个 plugin.Handler 接口。
  4. Plugin Integration, 用户在 plugin.cfg 整合自己的插件代码,并完成编译。

注册 (Register) 和设置 (Setup) 你的插件

注册 (Register)

首先你的插件需要下面函数的调用才能完成注册的操作,每一个插件拥有一个 name(实例中叫 foo)

go
1
func init() { plugin.Register("foo", setup) }

在上面代码所示,setup 函数将会被调用以完成注册。

设置 (Setup)

Setup 函数的功能就是载入配置文件与填充内部结构,他的参数是 *caddy.Controller ,他的返回值是 error

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

  // various other code

  return nil
}

如果我们在配置中配置了下面的参数

text
1
foo gizmo

那么在获取这个参数可以使用下面代码

go
1
2
3
4
5
6
for c.Next() {              // Skip the plugin name, "foo" in this case.
    if !c.NextArg() {       // Expect at least one value.
        return c.ArgErr()   // Otherwise it's an error.
    }
    value := c.Val()        // Use the value.
}

用户可以使用 c.Next() 来迭代参数,如果 c.Next()True,因为参数可能存在多个,所以可以迭代多次来完成参数的获取。需要主义的是,第一个参数通常为 Plugin name。在大多数教程中都交给用户是跳过第一个参数。

编写插件 (Handler)

Handler 可以称之为你的插件的入口,定义插件的要求如下:

  1. 因为 plugin.Handler 接口的定义,定义 handler 必须要实现方法 ServeDNS ,ServeDNS 是处理每个 DNS 查询请求的方法。
  2. 以及至少一个属性 “next”,next 表示在 CoreDNS 的请求链 (Chain) 中的下一个插件。
  3. 必须包含 Name() 方法

实现自定义插件的伪代码如下:

go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type MyHandler struct {
  Next plugin.Handler
}

func (h MyHandler) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
  return h.Next.ServeDNS(ctx, w, r)
}

func (h MyHandler) Name() string { 
    return "foo"
}

在上面可以看到,ServeDNS() 返回两个参数,响应 (int) 和错误 (error),如果 error 不为空,则返回 SERVFAIL 到客户端。响应代码会告诉 CoreDNS 插件链 ”是否已经写入回复“,如果没有写入,CoreDNS会自行处理这个情况。

CoreDNS 会处理下面场景 [2]

  • SERVFAIL (dns.RcodeServerFailure)
  • REFUSED (dns.RcodeRefused)
  • FORMERR (dns.RcodeFormatError)
  • NOTIMP (dns.RcodeNotImplemented)

还有一些常用的返回码:

  • RcodeSuccess: No error
  • RcodeServerFailure: Server failure
  • RcodeNameError: Domain doesn’t exist
  • RcodeNotImplemented: Record type not implemented

将自己插件编译到CoreDNS中

CoreDNS 提供了两种方式可以运行自定义插件,即 “编译时配置” (Compile-time Configuration) 和 “包装源码” 方式,

编译时配置

“编译时配置” (Compile-time Configuration) 也是官方在描述编译自定义插件的方式,就是在 CoreDNS 源码的 “plugin.cfg” 文件内增加你的插件然后运行 go generate。这个方式你必须 Clone CoreDNS 的源码进行修改和编译。

修改 plugin.cfg

text
1
2
3
etcd:etcd
# 编译按照下面的说明进行添加
{you-plugin-name}:github.com/{you-github-account}/{you-plugin-name}

Wrapping code

也可以根据 CoreDNS 官方的模式进行修改源码,在代码 core/plugin/zplugin.go 可以看到

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

import (
	// Include all plugins.
	_ "github.com/coredns/caddy/onevent"
	_ "github.com/coredns/coredns/plugin/acl"
	_ "github.com/coredns/coredns/plugin/any"
	_ "github.com/coredns/coredns/plugin/auto"
	_ "github.com/coredns/coredns/plugin/autopath"
	_ "github.com/coredns/coredns/plugin/azure"
	_ "github.com/coredns/coredns/plugin/bind"
	_ "github.com/coredns/coredns/plugin/bufsize"
	_ "github.com/coredns/coredns/plugin/cache"
	_ "github.com/coredns/coredns/plugin/cancel"

然后在代码 core/coredns.go 中可以看到,wrap 了 github.com/coredns/coredns/core/dnsserver

go
1
2
3
4
5
6
7
// Package core registers the server and all plugins we support.
package core

import (
	// plug in the server
	_ "github.com/coredns/coredns/core/dnsserver"
)

coredns.go 中可以看到是这样执行的

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

//go:generate go run directives_generate.go
//go:generate go run owners_generate.go

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

func main() {
	coremain.Run()
}

通过这个我们可以推理出两种方式来编译外部自定义插件

  1. 把插件放置到指定位置,修改代码,将其引入。
  2. 自行引入 coremain, dnsserver;接着运行。

方法2,如下代码所示 [3]

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

import (
    _ "github.com/you/foo"
    "github.com/coredns/coredns/coremain"
    "github.com/coredns/coredns/core/dnsserver"
)

var directives = []string{
    "foo",
    ...
    ...
    "whoami",
    "startup",
    "shutdown",
}

func init() {
    dnsserver.Directives = directives
}

func main() {
    coremain.Run()
}

最后编译自己的 CoreDNS Server 即可。

外部插件源码学习

通过前面部分我们掌握了自定义 CoreDNS 插件的 Specification,下面将以 mysql 插件来深入的学习开发自定义插件。

mysql 插件是允许用户使用 mysql 作为 zone 数据持久化的介质,通过配置可以预估他的代码内容。

text
1
2
3
4
5
6
7
8
9
mysql {
    dsn DSN
    [table_prefix TABLE_PREFIX]
    [max_lifetime MAX_LIFETIME]
    [max_open_connections MAX_OPEN_CONNECTIONS]
    [max_idle_connections MAX_IDLE_CONNECTIONS]
    [ttl DEFAULT_TTL]
    [zone_update_interval ZONE_UPDATE_INTERVAL]
}

通过上面配置可以知道,在 setup 中一定是迭代所有的参数解析为插件运行的配置,正如代码 coredns_mysql/setup.go 所示

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
func mysqlParse(c *caddy.Controller) (*CoreDNSMySql, error) {
	mysql := CoreDNSMySql{
		TablePrefix: "coredns_",
		Ttl:         300,
	}
	var err error

	c.Next()
	if c.NextBlock() {
		for {
			switch c.Val() {
			case "dsn":
				if !c.NextArg() {
					return &CoreDNSMySql{}, c.ArgErr()
				}
				mysql.Dsn = c.Val()
			case "table_prefix":
				if !c.NextArg() {
					return &CoreDNSMySql{}, c.ArgErr()
				}
				mysql.TablePrefix = c.Val()
			case "max_lifetime":
				if !c.NextArg() {
					return &CoreDNSMySql{}, c.ArgErr()
				}
				var val time.Duration
				val, err = time.ParseDuration(c.Val())
				if err != nil {
					val = defaultMaxLifeTime
				}
				mysql.MaxLifetime = val
			case "max_open_connections":
				if !c.NextArg() {
					return &CoreDNSMySql{}, c.ArgErr()
				}
				var val int
				val, err = strconv.Atoi(c.Val())
				if err != nil {
					val = defaultMaxOpenConnections
				}
				mysql.MaxOpenConnections = val
			case "max_idle_connections":
				if !c.NextArg() {
					return &CoreDNSMySql{}, c.ArgErr()
				}
				var val int
				val, err = strconv.Atoi(c.Val())
				if err != nil {
					val = defaultMaxIdleConnections
				}
				mysql.MaxIdleConnections = val
			case "zone_update_interval":
				if !c.NextArg() {
					return &CoreDNSMySql{}, c.ArgErr()
				}
				var val time.Duration
				val, err = time.ParseDuration(c.Val())
				if err != nil {
					val = defaultZoneUpdateTime
				}
				mysql.zoneUpdateTime = val
			case "ttl":
				if !c.NextArg() {
					return &CoreDNSMySql{}, c.ArgErr()
				}
				var val int
				val, err = strconv.Atoi(c.Val())
				if err != nil {
					val = defaultTtl
				}
				mysql.Ttl = uint32(val)
			default:
				if c.Val() != "}" {
					return &CoreDNSMySql{}, c.Errf("unknown property '%s'", c.Val())
				}
			}

			if !c.Next() {
				break
			}
		}

	}

	db, err := mysql.db()
	if err != nil {
		return nil, err
	}

	err = db.Ping()
	if err != nil {
		return nil, err
	}
	defer db.Close()

	mysql.tableName = mysql.TablePrefix + "records"

	return &mysql, nil
}

上面可以看到通过迭代 c.Next(),并遗弃第一个(第一个作为插件名称)然后为为每个配置赋值。

代码 handler.go 中有定义其配置的数据结构

go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
type CoreDNSMySql struct {
	Next               plugin.Handler
	Dsn                string
	TablePrefix        string
	MaxLifetime        time.Duration
	MaxOpenConnections int
	MaxIdleConnections int
	Ttl                uint32

	tableName      string
	lastZoneUpdate time.Time
	zoneUpdateTime time.Duration
	zones          []string
}

在大致了解了代码结构后,在了解下文件结构

文件作用
handler.go定义了 handler 的实现
mysql.gomodel 相关的操作
setup.go配置 setup函数,初始化配置,并填充到配置数据结构中
type.go用于定义数据库表类型和查询结果的内容

开发第一个提供静态 DNS 查询的插件

通过上面的学习,在这个任务中,我们来开发一个 “静态记录插件”。这个插件可以从配置文件中读取静态的 DNS 记录并提供查询服务。

实现基本的 DNS 记录响应

首先定义插件的数据结构

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
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
package staticdns

import (
    "context"
    "net"
    "strings"
    
    "github.com/coredns/coredns/plugin"
    "github.com/miekg/dns"
)

type StaticDNS struct {
    Next    plugin.Handler
    Records map[string][]Record
}

type Record struct {
    Type  uint16
    Value string
    TTL   uint32
}

func (s StaticDNS) Name() string {
    return "staticdns"
}

// 实现核心的查询处理逻辑
func (s StaticDNS) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
    if len(r.Question) == 0 {
        return plugin.NextOrFailure(s.Name(), s.Next, ctx, w, r)
    }

    q := r.Question[0]
    name := strings.ToLower(q.Name)
    
    // 查找对应的记录
    records, exists := s.Records[name]
    if !exists {
        return plugin.NextOrFailure(s.Name(), s.Next, ctx, w, r)
    }

    // 构建响应
    m := new(dns.Msg)
    m.SetReply(r)
    
    for _, record := range records {
        if record.Type == q.Qtype || q.Qtype == dns.TypeANY {
            rr := s.createResourceRecord(name, record)
            if rr != nil {
                m.Answer = append(m.Answer, rr)
            }
        }
    }
    
    if len(m.Answer) > 0 {
        w.WriteMsg(m)
        return dns.RcodeSuccess, nil
    }

    return plugin.NextOrFailure(s.Name(), s.Next, ctx, w, r)
}

// 创建 DNS 资源记录的辅助方法
func (s StaticDNS) createResourceRecord(name string, record Record) dns.RR {
    switch record.Type {
    case dns.TypeA:
        ip := net.ParseIP(record.Value)
        if ip == nil || ip.To4() == nil {
            return nil
        }
        return &dns.A{
            Hdr: dns.RR_Header{
                Name:   name,
                Rrtype: dns.TypeA,
                Class:  dns.ClassINET,
                Ttl:    record.TTL,
            },
            A: ip.To4(),
        }
    case dns.TypeAAAA:
        ip := net.ParseIP(record.Value)
        if ip == nil || ip.To4() != nil {
            return nil
        }
        return &dns.AAAA{
            Hdr: dns.RR_Header{
                Name:   name,
                Rrtype: dns.TypeAAAA,
                Class:  dns.ClassINET,
                Ttl:    record.TTL,
            },
            AAAA: ip,
        }
    case dns.TypeCNAME:
        return &dns.CNAME{
            Hdr: dns.RR_Header{
                Name:   name,
                Rrtype: dns.TypeCNAME,
                Class:  dns.ClassINET,
                Ttl:    record.TTL,
            },
            Target: dns.Fqdn(record.Value),
        }
    }
    return nil
}

Setup: 将插件注册到 CoreDNS

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package staticdns

import (
    "strconv"
    "strings"
    
    "github.com/coredns/caddy"
    "github.com/coredns/coredns/core/dnsserver"
    "github.com/coredns/coredns/plugin"
    "github.com/miekg/dns"
)

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

func setup(c *caddy.Controller) error {
    records := make(map[string][]Record)
    
    for c.Next() {
        args := c.RemainingArgs()
        if len(args) > 0 {
            return c.ArgErr()
        }
        
        for c.NextBlock() {
            if err := parseRecord(c, records); err != nil {
                return err
            }
        }
    }

    plugin := &StaticDNS{Records: records}

    dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
        plugin.Next = next
        return plugin
    })

    return nil
}

func parseRecord(c *caddy.Controller, records map[string][]Record) error {
    args := c.RemainingArgs()
    if len(args) < 3 {
        return c.Errf("invalid record format, expected: domain type value [ttl]")
    }
    
    domain := strings.ToLower(args[0])
    if !strings.HasSuffix(domain, ".") {
        domain += "."
    }
    
    recordType := parseRecordType(args[1])
    if recordType == 0 {
        return c.Errf("unsupported record type: %s", args[1])
    }
    
    value := args[2]
    
    ttl := uint32(3600) // 默认 TTL
    if len(args) > 3 {
        if t, err := strconv.ParseUint(args[3], 10, 32); err == nil {
            ttl = uint32(t)
        }
    }
    
    record := Record{
        Type:  recordType,
        Value: value,
        TTL:   ttl,
    }
    
    records[domain] = append(records[domain], record)
    return nil
}

func parseRecordType(typeStr string) uint16 {
    switch strings.ToUpper(typeStr) {
    case "A":
        return dns.TypeA
    case "AAAA":
        return dns.TypeAAAA
    case "CNAME":
        return dns.TypeCNAME
    default:
        return 0
    }
}

Configration - 配置插件可以解析域

text
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
.:53 {
    staticdns {
        example.com A 10.0.0.110 300
        example.com AAAA 2001:db8::1 300
        www.example.com CNAME example.com 600
        mail.example.com A 10.0.0.111
        blog.example.com A 10.0.0.112
    }
    forward . 8.8.8.8
    log
    errors
}

上面配置定义了几条静态 DNS 记录,当客户端查询对应域名时,插件会返回配置的记录。

Compile - 将插件整合到 CoreDNS 中

因为这里我们不是作为一个公共仓库,也不是使用载入 coredns 主包的方式进行的,所以我们需要把代码放置于 coredns plugins/staticdns 中

然后在 core/plugin/zplugin.go 中增加下面一行

go
1
_ "github.com/coredns/coredns/plugin/staticdns"

然后编译代码

bash
1
go build

总结

在本章节中,详细的展开对 coredns 插件的编写部分,通过 CoreDNS 目录结构,代码构成 以及 CoreDNS 自定义插件编写规范,简单学习了 CoreDNS的源码;通过外部插件 mysql 学习了插件定义的思路,以及自定义了一个 staticdns 插件三个部分完成学习 coredns 自定义插件。

在下一篇文章中,将从系统需求开始,一步步学习完成系统设计,以为后面实高性能 DNS 域名平台做好准备。

Reference

[1] How to Register a CoreDNS Plugin?

[2] Writing Plugins

[3] Developing Custom Plugins for CoreDNS