基于 CoreDNS 搭建权威 DNS 服务器

2024-10-26 ⏳5.5分钟(2.2千字) 🕸️

权威 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 数据版本号。后面括号中的部分依次是:

以上记录中最开的名字,以及后续的 TTL、网络类型都可以省略。如果省略 TTL,默认会使用 $TTL 指定的过期时间。网络类型默认为 IN。如果名字也省略,则默认使用前面的名字。比如第二行的名字为@,即exaple.com,记录类型为 NS,也就是权威服务器域名,对应的值为ns。因为不是点结尾,所以是相对域名,实际对应ns1.example.com.。而第三行虽然没指定名字为@,但它的名字从上一行继承而来,也是example.com

TXT 类型的记录值需要用引号括起来,这个需要注意。

接下来的是给 ns1ns2子域名设置对应的 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 权威服务器来说都过于复杂,还是留给进阶读者自己研究吧。


  1. https://www.knot-dns.cz/↩︎

  2. https://www.nlnetlabs.nl/projects/nsd/about/↩︎

  3. https://coredns.io/↩︎