为什么个人站长值得自建一个递归 DNS 解析器
先说一个很多人不知道的事实:你 VPS 上默认那个 resolv.conf,写的往往是云厂商的内网 DNS,比如 100.100.100.100 或者 183.60.83.19 之类。它是能用,但你完全不知道它在背后做了什么——有没有记录你的查询、有没有过滤某些域名、有没有在解析失败时返回一个广告导航页(DNS 劫持)。对个人站长来说,服务器上跑的每一个 curl、每一次 apt update、每一个 API 调用,都要走这个解析器。
这篇文章讲的是自建递归解析器 Unbound 的完整落地:它和 BIND 的区别在哪、为什么个人站更适合 Unbound、完整配置怎么写、DNSSEC 验证怎么开、和 systemd-resolved 打架怎么解决,以及我自己踩过的几个坑。
一、递归解析器和权威 DNS 是两码事
这是最容易搞混的地方。很多人看到「自建 DNS」就以为要像 BIND 那样配 zone 文件、写 A 记录——那是权威服务器(authoritative),作用是「回答别人关于我域名的查询」。而 递归解析器(recursive resolver) 干的是完全相反的事:它代替你的服务器去问别人。
完整的一次递归查询是这样的:
- 你的机器问 Unbound:「
www.example.com的 IP 是多少?」 - Unbound 先问根服务器(
.),根服务器回答:「.com的权威服务器在 a.gtld-servers.net」 - Unbound 再问
.com的服务器,回答:「example.com的 NS 在 ns1.example.com」 - Unbound 再问
ns1.example.com,终于拿到真正的 A 记录 - Unbound 把结果缓存起来(按 TTL),返回给你的机器
云厂商的 DNS 就是这样一个解析器,只不过它是公共的、你不知道它怎么配置。自建之后,好处是:
| 项目 | 云厂商 DNS | 自建 Unbound |
|---|---|---|
| 查询日志 | 不透明,可能被记录 | 完全自己掌控,可关可开 |
| DNS 劫持/广告跳转 | 部分厂商有 | 无,返回真实 NXDOMAIN |
| DNSSEC 验证 | 通常不验证 | 可强验证,防投毒 |
| 缓存命中率 | 共享缓存,可能被污染 | 私有缓存,干净 |
| 查询延迟 | 看厂商节点分布 | 首次稍慢,命中后极快 |
| 可用性 | 厂商偶发故障就全站解析失败 | 自己可控,可配多个上游兜底 |
二、Unbound 和 BIND9 怎么选
个人站长场景下,我的建议很明确:权威用 BIND 或 PowerDNS,递归用 Unbound。原因是 Unbound 在设计上就是「只做递归解析器」这一件事,代码量小、默认权限模型严格(可以 chroot、可以降权到 nobody)、内置 DNSSEC 验证,而且配置文件比 BIND 的 zone 语法清爽太多。
反过来说,如果你想用一台 Unbound 既做递归又做权威,那是在给自己找麻烦——Unbound 虽然支持 local-zone,但不适合当生产权威服务器。
安装(Debian 12 / Ubuntu 22.04)
apt update
apt install -y unbound unbound-host dnsutils
# Debian 12 上装完是自动启动的,先停掉改配置
systemctl stop unboundUbuntu 上装 Unbound 有个坑:它会顺手帮你改 /etc/resolv.conf 指向 127.0.0.1,但如果 resolv.conf 是 systemd-resolved 托管的软链接,你这个改动会被覆盖回去。后面第三节专门讲这个。
三、最小可用配置:监听本地就行
99% 的个人站长只需要本机使用这个解析器,不需要给公网提供递归服务。这非常重要——给公网开放递归解析器,会被当成 DNS 放大攻击的跳板,几小时内就会收到云厂商的滥用告警甚至停机。
配置文件在 /etc/unbound/unbound.conf.d/local.conf(Debian 系把主配置拆成了多个片段,别去改 /etc/unbound/unbound.conf 主文件,升级时容易被覆盖):
server:
# 只监听本机,绝不监听 0.0.0.0
interface: 127.0.0.1
interface: ::1
port: 53
# 只允许本机查询,这是关键的安全边界
access-control: 127.0.0.0/8 allow
access-control: ::1 allow
# 其余全部拒绝(默认就是拒绝,但显式写出来更清楚)
access-control: 0.0.0.0/0 refuse
access-control: ::/0 refuse
# 降权运行,别用 root
username: "unbound"
directory: "/etc/unbound"
chroot: "/etc/unbound"
# 记录日志(排查完可以关掉,减少磁盘写入)
logfile: "/var/log/unbound/unbound.log"
verbosity: 1
log-queries: no
log-replies: no
# 隐藏版本号,别给人指纹
hide-identity: yes
hide-version: yes
# 关键:强制 DNSSEC 验证
auto-trust-anchor-file: "/var/lib/unbound/root.key"
val-log-level: 2
# 缓存大小:个人站 64MB 足够,换来极高命中率
msg-cache-size: 64m
rrset-cache-size: 128m
cache-min-ttl: 60
cache-max-ttl: 86400
# 隐私:不发送客户端子网、最小化查询
qname-minimisation: yes
qname-minimisation-strict: yes
do-not-query-localhost: yes
private-address: 10.0.0.0/8
private-address: 172.16.0.0/12
private-address: 192.168.0.0/16
# 防投毒:固化 nameserver 端口和目标
harden-glue: yes
harden-dnssec-stripped: yes
harden-below-nxdomain: yes
harden-referral-path: yes
use-caps-for-id: yes
# 预取热门域名,压缩首次查询延迟
prefetch: yes
prefetch-key: yes配置里几个参数值得单独解释,因为它们经常被抄错:
access-control:这是最核心的安全设置。写allow的范围才能查询,其他一律拒绝。有人图省事写access-control: 0.0.0.0/0 allow,那等于开了一个公开递归解析器,是事故级别的错误。auto-trust-anchor-file:指向根信任锚(root.key),DNSSEC 验证的起点。Unbound 会自动通过 RFC 5011 机制更新它,前提是这个文件所在目录可写。Debian 包会自动装unbound-anchor,如果看到root.key为空或报错,手动执行一次unbound-anchor -a /var/lib/unbound/root.key。harden-dnssec-stripped:如果上游把 DNSSEC 记录剥掉了却声称验证通过,直接拒绝。这是防降级攻击的关键。qname-minimisation:查询逐步缩减域名前缀,减少向根服务器泄露完整域名,是 RFC 7816 的隐私增强。
改完检查语法并启动:
unbound-checkconf
# 输出 "unbound-checkconf: no errors in /etc/unbound/unbound.conf" 才算过
systemctl enable --now unbound
systemctl status unbound --no-pager四、让系统真的用上它:systemd-resolved 的冲突
这是自建解析器最常翻车的地方。你配置好了 Unbound,结果发现 dig 的结果还是走的老 DNS——因为 Ubuntu/Debian 新版都用 systemd-resolved 在 127.0.0.53:53 上占着 53 端口,你的 Unbound 报「address already in use」或者干脆被忽略。
先确认状态:
# 看 53 端口被谁占了
ss -lunp | grep ':53'
# 如果看到 127.0.0.53:53 是 systemd-resolve,就是它
# 看当前实际使用的上游
resolvectl status | head -30有两种处理方式,我推荐第一种。
方案一:停掉 resolved,直接让 Unbound 独占 53(推荐)
systemctl disable --now systemd-resolved
# 删掉托管软链接,换成真实文件
rm -f /etc/resolv.conf
cat > /etc/resolv.conf <<'EOF'
nameserver 127.0.0.1
nameserver ::1
options edns0 trust-ad
search .
EOF
# 防止被 NetworkManager / cloud-init 再次覆盖
chattr +i /etc/resolv.conf
systemctl restart unbound注意 options trust-ad 这一行——没有它,glibc 在 getaddrinfo 时不会向应用传递 AD(Authenticated Data)标志,你就「验证了 DNSSEC 但应用不知道」。这是一个非常隐蔽的细节。
chattr +i 上锁很有效,但记住以后要改 resolv.conf 必须先 chattr -i,否则会报「Operation not permitted」,很多人在这卡半天。
方案二:保留 resolved,让它转发给 Unbound
如果你不想停 resolved(比如还在用它的 mDNS 功能),就改它的上游指向 Unbound 的另一个端口:
# Unbound 监听 5335(避开 resolved 占用的 53)
# /etc/unbound/unbound.conf.d/local.conf 里改 port: 5335
# 然后告诉 resolved 转发给它
mkdir -p /etc/systemd/resolved.conf.d
cat > /etc/systemd/resolved.conf.d/unbound.conf <<'EOF'
[Resolve]
DNS=127.0.0.1:5335
DNSStubListener=no
DNSSEC=allow-downgrade
EOF
systemctl restart systemd-resolved unbound两种方案我实际都用过,方案一更干净、少一层转发、延迟更低,个人 VPS 上没有任何理由留着 resolved。
五、验证:别只看「服务起来了」
服务 active 不代表解析真的走了你的 Unbound。按这个顺序实测:
# 1. 直接指定用 Unbound 查询,确认能解析
dig @127.0.0.1 example.com +short
# 2. 确认响应来自本地 Unbound(看 SERVER 行应该是 127.0.0.1#53)
dig example.com | grep -A1 'SERVER:'
# 3. DNSSEC 验证:这个域名有 DNSSEC,应该看到 ad 标志
dig @127.0.0.1 cloudflare.com +dnssec | grep flags
# 期望:flags: qr rd ra ad; <-- 有 ad 就说明验证通过
# 4. 验证一个「DNSSEC 故意配错」的域名,必须返回 SERVFAIL
dig @127.0.0.1 dnssec-failed.org
# 期望:status: SERVFAIL
# 5. 缓存是否生效:第二次查询的 Query time 应该降到 1ms 以内
dig @127.0.0.1 www.qq.com | grep 'Query time'第 4 步是判断 DNSSEC 到底有没有真正生效的唯一可靠方法。如果 dnssec-failed.org 返回正常解析结果,说明你的验证被绕过了——很可能是 auto-trust-anchor-file 路径写错或 root.key 是空的。
查 Unbound 的运行统计
unbound-control stats_noreset | grep -E 'total.num|cache.hits|cache.miss|num.query.type.A|num.answer.secure|num.answer.bogus'几个关键指标:
total.num.cachehits / total.num.queries→ 缓存命中率。稳定运行一天后,个人站应该看到 80% 以上。低于 50% 说明 TTL 设置有问题或缓存被打爆。num.answer.secure→ 通过 DNSSEC 验证的响应数,应该持续增长。num.answer.bogus→ 验证失败的响应数。偶尔有是正常的(遇到真的配错 DNSSEC 的站点),但如果持续高速增长,说明你的根信任锚坏了,所有验证都失败。
六、我踩过的四个坑
坑一:日志把磁盘写满
开了 verbosity: 2 加 log-queries: yes 之后,一个访问量稍大的站一天能写出几个 G 的日志。修法是 verbosity 保持 1、关掉 log-queries,并加一条 logrotate:
cat > /etc/logrotate.d/unbound <<'EOF'
/var/log/unbound/unbound.log {
weekly
rotate 4
compress
delaycompress
missingok
notifempty
create 0640 unbound unbound
postrotate
/usr/sbin/unbound-control log_reopen 2>/dev/null || true
endscript
}
EOF注意 postrotate 里的 unbound-control log_reopen——只是 rotate 文件不通知进程的话,Unbound 会继续往已经改名(甚至已删除)的旧 inode 写,新文件永远是空的。这个坑和 Nginx 的 nginx -s reopen 是同一类。
坑二:chroot 之后 unbound 起不来
Debian 包默认在 systemd unit 里带了 chroot 和 ProtectSystem=strict 之类的硬化参数。如果你把日志目录或 root.key 放在 chroot 之外,进程会静默失败。排查方法:
journalctl -u unbound -n 50 --no-pager
# 找 "failed to open" / "permission denied" 之类的行要么把所有路径都放进 chroot 目录(/etc/unbound/)之下,要么在 /etc/systemd/system/unbound.service.d/override.conf 里覆盖掉硬化参数。我选前者,更安全。
坑三:私有地址被拒绝,内网域名解析失败
private-address 列表里的网段,Unbound 会认为「公网 DNS 不该返回这些地址」而拒绝。这本来是防投毒的,但如果你在服务器上要解析内网的域名(比如数据库走内网域名 db.internal),转发给上游后会被这条规则拦掉。修法是给内网域名单独配 domain-insecure 和 local-zone,或者用 forward-zone 指定内网 DNS。
坑四:DNS 解析慢导致 SSH 登录卡 5 秒
配完之后发现 SSH 登录要卡好几秒才出密码提示。原因是 sshd 默认开了 UseDNS yes,登录时反查客户端 IP 的 PTR 记录,而这个反查走递归要好几跳。修法:
# /etc/ssh/sshd_config
UseDNS no
GSSAPIAuthentication no这不是 Unbound 的 bug,但自建递归后「首次查询慢」的特性会把这类问题的症状放大,值得顺手一起改掉。
七、什么时候不该自建递归解析器
说点劝退的。以下情况别折腾:
- 只有一两台机器、业务不敏感:云厂商 DNS 省事,收益抵不过维护成本。
- 服务器到根服务器的网络质量差:有些机房对 53 端口 UDP 有 QoS 或丢包,递归查询会非常慢,这种情况用 DNS over TLS 的公共解析器(
1.1.1.1、9.9.9.9)更好。 - 想做的是「给公网提供 DNS 服务」:那你要的是权威服务器,别用 Unbound。
- 服务器内存极小(不足 256MB):Unbound 基础占用约 40-60MB,加上缓存,低配机上要慎重。可以把
rrset-cache-size降到 16m 试试。
八、小结
自建 Unbound 递归解析器的核心收益有三点:解析链路透明可控(知道每个域名去哪问的)、DNSSEC 真正生效(防缓存投毒)、缓存私有化(不受公共 DNS 污染影响)。代价是要处理 systemd-resolved 冲突、chroot 权限、日志轮转这几件琐事,一次性配好之后基本不用管。
如果你只做一件事,那就做把 access-control 收紧到 127.0.0.0/8——开放递归解析器被滥用导致云厂商停机,是这类部署里最常见、后果最严重的失误。