基于 CoreDNS 搭建权威 DNS 服务器
涛叔权威 DNS 软件有很多,历史最悠久名的当属 BIND。比较新的软件有 cz.nic 开发的 KNOT1 ,还有 NLnet Lab 开发的 NSD2。但这些软件都是由 C 语言开发,虽然性能强劲,但是在灵活性方面确多有不变。比如它们基本都不太会支持很新的 DNS 协议。所以我想找一款Go 语言开改的权威 DNS 软件,方便后续扩展。转了一圈发现最靠谱非 CoreDNS3 莫属了。今天就为大家分享如何用 CoreDNS 搭建一组轻量化的权威 DNS 解析服务。
💡Tip如果读者还不了解 DNS 系统的工作原理,请先阅读我的这篇文章。
CoreDNS 最早是设计用来做云原生服务发现用的。但它是多面手,既能做递归解析服务器,又可以做权威解析服务器。CoreDNS 是一个裁剪过的 Caddy 服务器,所有功能都通过插件实现。CoreDNS 官方维护很多核心插件,同时社区也有很多三方插件可供选择。
本文基于 acl, file, transfer 和 secondary 插件搭建权威 DNS 解析服务。
首先我们到官网下载 CoreDNS 软件。官方提供了几乎各种平台和架构的二进制文件,如果不改代码,则可以直接下载使用。也可以从 GitHub 下载源码,再用 go 工具链编译。本文从略。
CoreDNS 启动命令如下:
coredns -conf /etc/coredns/corefile
这里的/etc/coredns/corefile
为 CoreDNS 的配置文件,格式参考 Caddy 服务器。配置的内容由具体的插件定义。我的服务器配置如下:
example.org {
file /var/lib/coredns/db.example.org
log
acl {
allow type AXFR net 2001:db8::/32
}
transfer {
to * 2001:db8::2
}
}
配置一开始就是具体的 DNS 域名,这里假设域名为 example.org。后面的大括号内表示跟当前域名相关的插件配置。
第一项 file 表示从文中加载 DNS 解析记录。后面接文件路径,文件内容需要符合 RFC1035 规范。这里先路过,我们会在最后面细说。
第二项 log 表示记录 DNS 解析请求,CoreDNS 默认会把日志输出到 stdout 设备。
第三项 acl 指定访问规则。这里限定只允许 2001:db8::/32 网段的设备发起类型为 AXFR 的查询。AXFR 表示 DNS Zone Transfer Protocol,用于备用权威服务器向主服务器同步解析记录。AXFR 查询因为要同步整个 zone 下的解析记录,可能会超出 UDP 报文长度,所以需要通过 TCP 协议传输。
最后一项 transfer 表示如果 DNS 记录有变更,则会主动通知备用 2001:db8::2 来同步变更数据。具体细节可以参考 RFC1996,本质上是主服务器向备服务发送一个特殊的 DNS 查询,查询的 op 字段置为 NOTIFY。备服务器收到后主动发起 AXFR 请求。
以上就是主权威服务器的配置。备用服务器的配置相对简单一些:
example.org {
secondary {
transfer from 2001:db8::1
}
log
}
这里的 secondary 表示当前为备用实例,数据需要从 transfer from 指定的主服务器同步。
以上就是所有配置的内容。现在说一下数据文件的格式。先附上一个示例:
$ORIGIN example.org
$TTL 300
@ 600 IN SOA ns1 dns 2024103100 3600 3600 1209600 600
@ 600 IN NS ns1
IN NS ns2
MX 5 mx1.example.org.
TXT "hello"
ns1 A 1.1.1.1
AAAA 2001:db8::1
ns2 A 1.1.1.2
AAAA 2001:db8::2
www.nic A 1.1.1.3
第一行$ORIGIN example.org
表示当前文件所属的域名为example.org
。第二行$TTL
表示默认的 TTL 时间,单位是秒。
接一下是 zone 文件的关键,SOA 记录。对于权威服务器而言,该记录的名字需要是@
,它代理域名本身,即example.org
。后面的的数字 600 表示该条记录的过期时间。再后面的 IN 表示 DNS 记录的分类(class)。IN 表示当前的因特网,其实还有像 Chaos (CH) 这样的类别,现在不常用了。大家日常见到的几乎都是 IN 类型。再后面是 DNS 记录的类型, SOA 全称 Start Of Authority record,它最主要的功能就是对外提供 DNS 数据版本号。后面括号中的部分依次是:
- MNAME 主服务器域名。这里的 ns1 表示相对域名,对应绝对域名是 ns1@exaple.com
- RNAME 系统管理员邮箱。dns 对应 dns.exaple.com。因为 @ 已经被占用,所以 dns@example.com 写为 dns.example.com
- SERIAL DNS 数据序列号,需要单调自增,每当有记录变更,都需要更新此字段
- REFRESH 备用服务全量同步时间间隔
- RETRY 如果备用服务器同步失败,下次同步需要等待的时长
- EXPIRE 如果主服务器一直失联,备服务器会在该字段指定的时长之后停止对外提供服务
- MINIMUM 如果 DNS 记录不存在,客户端也可以缓存这种负向结果,该字段指定负向结果 TTL
以上记录中最开的名字,以及后续的 TTL、网络类型都可以省略。如果省略 TTL,默认会使用 $TTL 指定的过期时间。网络类型默认为 IN。如果名字也省略,则默认使用前面的名字。比如第二行的名字为@
,即exaple.com
,记录类型为 NS,也就是权威服务器域名,对应的值为ns
。因为不是点结尾,所以是相对域名,实际对应ns1.example.com.
。而第三行虽然没指定名字为@
,但它的名字从上一行继承而来,也是example.com
。
TXT 类型的记录值需要用引号括起来,这个需要注意。
接下来的是给 ns1
和ns2
子域名设置对应的 A 和 AAAA 记录。最后一行是展示二级子域名指定 A 记录,实际对应的域名为www.nic.example.com.
。
DNS 分布式的核心是委托。所有的 DNS 查询都从根域.
开始。根域由 ICANN 管理,根域仅保存顶级域名的委托信息。比如 org.
域的 NS 记录为:
org. 3600 IN NS a0.org.afilias-nst.info.
org. 3600 IN NS a2.org.afilias-nst.info.
org. 3600 IN NS b0.org.afilias-nst.org.
org. 3600 IN NS b2.org.afilias-nst.org.
org. 3600 IN NS c0.org.afilias-nst.info.
org. 3600 IN NS d0.org.afilias-nst.org.
客户端尝试查询www.nic.example.org.
的 A 记录,它先会到根服务器上查询org.
对应的 NS 服务器列表。但根服务器返回的结果中居然还有像b0.org.afilias-nst.org.
这样的 org.
域名。这样岂不是死循环了吗?
假如org.
所有的 NS 服务器域名都是info.
,那就不会有这个问题。因为客户端会递归可以递归解析对应的地址。但 DNS 系统允许 NS 服务器使用当前域的子域名。在这种情况下,我们需要给上级服务器客户提供当前域名对应的 IP 地址。
如果你直接向根服务查询 org.
的 NS 服务器列表,会得到如下结果:
dig ns org. @a.root-servers.net.
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;org. IN NS
;; AUTHORITY SECTION:
org. 172800 IN NS a2.org.afilias-nst.info.
org. 172800 IN NS b2.org.afilias-nst.org.
org. 172800 IN NS d0.org.afilias-nst.org.
org. 172800 IN NS a0.org.afilias-nst.info.
org. 172800 IN NS b0.org.afilias-nst.org.
org. 172800 IN NS c0.org.afilias-nst.info.
;; ADDITIONAL SECTION:
a2.org.afilias-nst.info. 172800 IN A 199.249.112.1
a2.org.afilias-nst.info. 172800 IN AAAA 2001:500:40::1
b2.org.afilias-nst.org. 172800 IN A 199.249.120.1
b2.org.afilias-nst.org. 172800 IN AAAA 2001:500:48::1
d0.org.afilias-nst.org. 172800 IN A 199.19.57.1
d0.org.afilias-nst.org. 172800 IN AAAA 2001:500:f::1
a0.org.afilias-nst.info. 172800 IN A 199.19.56.1
a0.org.afilias-nst.info. 172800 IN AAAA 2001:500:e::1
b0.org.afilias-nst.org. 172800 IN A 199.19.54.1
b0.org.afilias-nst.org. 172800 IN AAAA 2001:500:c::1
c0.org.afilias-nst.info. 172800 IN A 199.19.53.1
c0.org.afilias-nst.info. 172800 IN AAAA 2001:500:b::1
这里根服务器通过所谓的 Additional Section 返回了 NS 服务器对应的 IP 地址。所以客户端不需要再额外发起新请求来查询它们的地址,也就消除了套娃的问题。
不过这种设计也会导致 DNSSEC 签名争议。虽然上级服务器可以通过 Additional Section 额外返回 NS 服务器的地址,但上级服务器不能为这些记录做 DNSSEC 签名,因为这些记录的所有权在当前服务器。如果没有签名,是不是网络中间节点可以任意劫持 NS 服务器列表呢?这确实是个问题。
回到我们的 zone 文件。我们为example.org.
指定了两台 NS 服务器,分别是ns1.example.org.
和ns2.example.org.
。显然,我们也需要把它们的 IP 地址一并录入上级权威服务器,也就是org.
的 NS 服务器列表。
好了,以上就是所有理论部分。最后我们用 systemd 来启动 CoreDNS 服务:
[Unit]
Description=CoreDNS
After=network.target
[Service]
Restart=always
RestartSec=1
User=coredns
ExecStart=/usr/local/sbin/coredns -conf /etc/coredns/corefile
[Install]
WantedBy=multi-user.target
启动之后不要忘记修改防火墙,开放 TCP 和 UDP 的 53 号端口。
每次修改 zone 文件之后不要忘记更新 SOA 记录的序列号,不然备用服务器不会自动同步。
到现在我们就拥有了自己的两台权威服务器。这种配置虽然简单,但是需要手工更新,并不能提供动态更新能力。不过问题也不大,毕竟 DNS 是典型的读多写少的系统。偶然改一下,不会造成很大负担。当然,你也可以开发外围系统来更新 zone 文件,再配合 auto 插件定时刷新。甚至你也可以使用诸如 mysql/redis 这类动态后端插件。不过这些对于理解 DNS 权威服务器来说都过于复杂,还是留给进阶读者自己研究吧。