使用 Trust-DNS 建立权威 NS 服务器并启用 DNSSEC

最近白嫖了 $50 的 Vultr 代金券,可惜只能一个月用完,于是开始折腾一些需要多台服务器才能搞的东西,比如自建权威 DNS、负载均衡、K8S 和微服务等。

选择 Trust-DNS 的原因其实只有一个:它是用 Rust 写的。

自建权威服务器其实并不难,但是这方面的资料比较少。而一方面 Trust-DNS 作为一个小众的 DNS 服务器根本没有成体系的文档,另一方面有关权威 DNS 服务器启用 DNSSEC 的资料很少。因此,我在搭建的时候也踩了不少坑。

本文搭建了的两台服务器 hostname 为:

1
2
tns-yuezheng0
tns-luotian1

其中 tns 既是 Trust-DNS 的缩写,又表示 Test DNS Server。一般 DNS 服务器需要两台互为主备,于是正好拿阿绫(0)和天依(1)的名字当作序号。0=Master Sever, 1=Slave Server, 谁攻谁受一目了然。

下载并编译 Trust-DNS

Trust-DNS 的项目主页为:https://github.com/bluejekyll/trust-dns

下载和编译的方式,都在 README 文档 中叙述。本文仅作一个简单的总结。

本文假定您已安装 Rustup、Cargo、GCC、Git 等开发与编译工具。具体的安装方法请自行搜索。

第一步,获取源码并安装依赖。 Trust-DNS 并没有提供二进制包,作为一个 Rust 编写的程序,其依赖管理、编译步骤相较于 C/C++ 程序都简单很多,因此不必为“又要自行编译了”而焦虑。

1
2
git clone https://github.com/bluejekyll/trust-dns.git && cd trust-dns
sudo apt-get install openssl libssl-dev pkg-config

第二步,安装 cargo-make

文档中这一步并没有很明显的写出来,因此可能被忽略。

1
cargo install cargo-make

第三步,进行编译并测试。

1
cargo make all-features

值得注意的是,该测试需要可以访问 Google DNS 服务器的环境,否则会测试失败。事实上,如果实在没有这样的环境,这一步可以跳过。

笔者本来使用本地 Linux 虚拟机进行编译,但是即使设置代理也无法通过测试。随后转而使用一台香港 VPS,但编译速度实在太慢,于是就跳过了这一步。

第四步,以发布模式编译。

1
cargo build --release -p trust-dns

编译成功后,二进制程序位于 ./target/release/named

可以直接通过运行以下命令安装:

1
2
3
cp ./target/release/named /usr/bin
chown root:root /usr/bin/named
chmod +x /usr/bin/named

配置 named.toml

这里又要吐槽一下,开发者居然把实例配置文件放在测试数据目录底下。可以参看 ./tests/test-data/named_test_configs/example.toml 来进行配置。

配置文件默认应放置在 /etc/named.toml 下。我们自己创建它:

1
vi /etc/named.toml

我的配置文件全文如下:

1
2
3
4
5
6
7
8
9
10
11
listen_addrs_ipv4 = ["0.0.0.0"]
listen_addrs_ipv6 = ["2001::66:ccff"]
listen_port = 53

[[zones]]
zone = "luotianyi.dev"
zone_type = "Primary"
file = "/etc/zones/luotianyi.dev"
allow_update = false
allow_axfr = true
enable_dnssec = false

以上配置文件就是一个简化的版本,仅配置了侦听 IP 和端口及一个 ZONE。您可以照着这个 ZONE 配置,只需把 luotianyi.dev 改成自己的域名,并把侦听的 IPv6 该成自己的即可。

这里我们先设置 enable_dnssec = false,等测试成功后在启用 DNSSEC。

TOML 语法

TOML 的语法允许你配置多个 IP 地址,就像这样:

1
2
listen_addrs_ipv4 = ["192.0.2.1", "192.0.2.2"]
listen_addrs_ipv6 = ["2001::66:ccff", ""2001::ee:0"]

或者,不想启用 IPv6,可以注释掉:

1
# listen_addrs_ipv6 = ["2001::66:ccff"]

也可以给一个空数组:

1
# listen_addrs_ipv6 = []

坑1:systemd-resolved 冲突

一种办法是不要让 Trust-DNS 侦听所有 IPv4,而是指定一个 IP 地址:

1
listen_addrs_ipv4 = ["192.0.2.1", "192.0.2.2"]

systemd-resolved 只会使用 127.0.0.53 地址,只要改成与其不同的,就不冲突了。

另一种办法更简单粗暴,即禁用 systemd-resolved。

1
2
3
4
systemctl disable systemd-resolved.service
systemctl stop systemd-resolved
rm -rf /etc/resolv.conf
echo "nameserver 1.1.1.1" > /etc/resolv.conf

值得注意的有以下几点:

  1. 禁用 systemd-resolved 后,/etc/resolv.conf 就会变成失效软链接,因此要删掉重设。其中 nameserver 可以随意替换为喜欢的 DNS 服务器,但是要确保可以访问
  2. 可能导致内网镜像(腾讯云、阿里云一般有内网镜像,可以不走服务器流量)不可用,可以自己把内网镜像域名加入 hosts,也可以把 DHCP 获取的 DNS 服务器设为 nameserver。
  3. 请确保你的 hostname 和 hosts 里的 hostname 一致,否则会导致无法使用 sudo 等命令!

坑2:无法监听所有 IPv6 的 bug

截至本文成文时,最新版本的 Trust-DNS 存在 bug,可能无法绑定所有 IPv6 地址 ::0,此时需要人工指定一个 IPv6 地址。

1
listen_addrs_ipv6 = ["2001::66:ccff"]

或者,如果不需要 IPv6,可以注释掉不侦听 IPv6 DNS 请求。

配置 ZONE 文件

ZONE 文件的配置其实和 BIND 差不多,此处不做详述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
;subdoumain           TTL      type     value
@ IN 86400 SOA luotianyi.dev. joseph.josephcz.xyz. (
2021040401 ; Serial
300 ; Refresh
300 ; Retry
604800 ; Expire
30) ; Minimum TTL
86400 NS tns-yuezheng0.luotianyi.dev.
86400 NS tns-luotian1.luotianyi.dev.
3600 A 192.0.2.1
3600 A 192.0.2.2
3600 AAAA 2001::66:ccff
3600 AAAA 2001::ee:0
tns-yuezheng0 86400 A 192.0.2.1
86400 AAAA 2001::ee:0
tns-luotian1 86400 A 192.0.2.2
86400 CNAME luotianyi.dev
www

将 ZONE 文件保存到 named.toml 配置文件中 file 项所设的位置(本文中是 /etc/zones/luotianyi.dev)。

事实上一般 ZONE 文件以 .zone 作为后缀名,但是笔者偷懒没有设置,这是可行的但并不规范。

使用 dig 命令进行测试

首先开两个 ssh 窗口(或者用 screen),并直接运行程序。

1
named

另一个窗口运行 dig:

1
dig luotianyi.dev @127.0.0.1

正常的响应应当如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
; <<>> DiG 9.16.1-Ubuntu <<>> luotianyi.dev @127.0.0.1
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 16123
;; flags: qr aa; QUERY: 1, ANSWER: 2, AUTHORITY: 2, ADDITIONAL: 1

;; QUESTION SECTION:
;luotianyi.dev. IN A

;; ANSWER SECTION:
luotianyi.dev. 3600 IN A 192.0.2.1
luotianyi.dev. 3600 IN A 192.0.2.2

;; AUTHORITY SECTION:
luotianyi.dev. 86400 IN NS tns-yuezheng0.luotianyi.dev.
luotianyi.dev. 86400 IN NS tns-luotian1.luotianyi.dev.

同时,在另一个窗口中应该可以看到 Trust-DNS 的日志输出了。此时可以按 Ctrl-C 关闭 Trust-DNS 了。

配置 systemd 服务

创建 /etc/systemd/system/named.service 文件,写入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[Unit]
Description=Trust-DNS Named Service
Documentation=https://github.com/bluejekyll/trust-dns

[Service]
Type=simple
ExecStart=/usr/bin/named
Restart=always
RestartSec=5
KillSignal=SIGINT
SyslogIdentifier=trust-dns
User=root

[Install]
WantedBy=multi-user.target

然后启用它:

1
2
systemctl enable named
systemctl start named

设置 Glue 记录

测试完成后,就可以去域名注册商那里设置 Glue 记录,并修改 Nameserver 了。

1
2
Nameserver 1    tns-yuezheng0.luotianyi.dev    192.0.2.1
Nameserver 2 tns-luotian1.luotianyi.dev 192.0.2.2

DNSSEC 设置

这里只举 RSASHA256 加密方式的例子,其他的可以举一反三。

创建 DNSSEC 私钥

1
openssl genrsa -des3 -out /etc/ssl/private/luotianyi.dev.dnssec_key.pem 2048

这里有一个坑,如果私钥后缀不是 .pem,Trust-DNS 会无法识别它,然后报错。

启用 DNSSEC

编辑 /etc/named.toml 文件,在 ZONE 配置下加入以下几行(enable_dnssec 改成 true):

1
2
3
4
5
6
7
enable_dnssec = true
[[zones.keys]]
key_path = "/etc/ssl/private/luotianyi.dev.dnssec_key.pem"
algorithm = "RSASHA256"
password = "P@ssw0rd!"
is_zone_signing_key = true
create_if_absent = false

其中,algorithm 指定算法名称,所有可用算法列表在 这里password 是私钥密码,如果无密码填写 password=""

完整的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
listen_addrs_ipv4 = ["0.0.0.0"]
listen_addrs_ipv6 = ["2001::66:ccff"]
listen_port = 53

[[zones]]
zone = "luotianyi.dev"
zone_type = "Primary"
file = "/etc/zones/luotianyi.dev"
allow_update = false
allow_axfr = true
enable_dnssec = true
[[zones.keys]]
key_path = "/etc/ssl/private/luotianyi.dev.dnssec_key.pem"
algorithm = "RSASHA256"
password = "P@ssw0rd!"
is_zone_signing_key = true
create_if_absent = false

然后重启服务:

1
service named restart

将 DNSKEY 记录转换为 DS 记录

运行 dig ANY luotianyi.dev @127.0.0.1,然后找到 DNSKEY 记录:

1
luotianyi.dev.          30      IN      DNSKEY  257 3 8 AwEAAcpFsCsCz13nZXwiziKGRI5sDFUPTm5uAEqZiplxgdoXVWR9j3DQ Hc8iRRz9U5m71ctAnoDfowf6lT27rn6hUdNPHFS0vnLMSwWYZM7qTGQ3 9hZnF5rfKJPLs/Z6FXBqPkOfHOuBPKGgEMuflYZO8po5+/a9e/zxaNn5 sqCKFxGFo4BCLSBaBylbT8rEbYUZYEyecaN8geIW1WT8YxXzXVqyOOhw WUr/eEIleF7pOOlUJQ4TXw0Tn/GSAYhrF+LEWUG6kEnR6JbcQTVYq2BV PI4nBmm+zK1i2jtmA5zkskzozk58NmakcZs1CLNubR65O+48SNWfGqQX ABPo42Bl+wc=

然后打开 DNSKEY to DS record converter,把该记录贴进左边,复制转换出的值:

1
luotianyi.dev.	30	IN	DS	23384 8 2 81458BE3015CBD98DE3D14039203E5D7527344BA5BC94FC62EB728F8717A34B3

该记录就是设置的 DS 记录。在域名注册商处,找到类似“DNSSEC 设置”的地方,将该记录设置好即可。

检测

DNSSEC Analyzer 可以用来测试 DNSSEC 的启用情况。

我的测试报告可在 https://twitter.com/baobao1270/status/1379696124882157570 查看。