你的机器上已经跑着一个带缓存的 DNS 解析器、一个 DoT 客户端、一个分流 DNS引擎、一个 mDNS 响应器——开机自启,而且你几乎肯定没亲手装过它。它就是 systemd-resolved,随 systemd 发行了整整十年的那个本地存根解析器——并且它只用一条你大概从没敲过的命令管理:resolvectl。
这里有个我在自己 Arch 主机上撞到的反转,也正是我写这篇的全部理由:systemd-resolved 正在运行,而且监听在 127.0.0.53,但我的应用从不走它。下面每一行都是那台机器的真实输出,只在一堆 Docker 网桥会变成噪音的地方做了裁剪。没有一处是编的,并且每一个需要 root 的命令我都标了出来——在这台 systemd 262 主机上,需要 root 的比我预想的还多。如果你想先补齐”递归是怎么一步步走的”那套背景,我的《DNS 究竟是怎么解析的》是配套阅读;这一篇只讲本地这一半。
127.0.0.53 上的本地 systemd-resolved 存根;命中直接走缓存,未命中转发给对应接口的上游——理想情况下走 DNS over TLS。可要是没人指向这个存根,那份缓存就只是死重量。systemd-resolved 到底是什么(以及它为什么早就在跑)
概念上,systemd-resolved 是四样东西拼在一起:
- 一个跑在
127.0.0.53的缓存存根解析器(127.0.0.54归 LLMNR)。每个通过 libc 解析域名的进程都能指向它,共享同一份缓存。 - 一个按接口的 DNS 调度器。它不保存”那台 DNS 服务器”,而是给每个网络接口各存一台,然后按名字决定问哪一台。这就是分流 DNS,也是最多人忽略的部分。
- 一个懂加密的转发器。它能把上游查询走 DNS over TLS(
DNSOverTLS=)发出去,并尝试校验 DNSSEC,于是一条恶意 LAN 就没法轻易偷看或伪造你的应答。 - 一个面向
LLMNR和 mDNS(.local)的链路本地名字引擎,你的机器靠它在没有服务器的情况下找到printer.local或某个 Windows 邻居。
它不是魔法,也不是那种”我自己从根服务器一路走下来”的递归解析器——它转发给配置好的上游并缓存答案。这个区别我放到最后再讲,因为它决定了你到底还要不要留着它。
resolvectl 就是全部操作面板。我这台机器上的 resolvectl --version:
$ resolvectl --version
systemd 262 (262-1-arch)
+PAM +AUDIT -SELINUX ... +GNUTLS +OPENSSL ...
服务本身健康、按 socket 激活:
$ systemctl status systemd-resolved
● systemd-resolved.service - Network Name Resolution
Loaded: loaded (/usr/lib/systemd/system/systemd-resolved.service; enabled; preset: enabled)
Active: active (running) since Mon 2026-10-12 04:12:29 CST; 1 day 3h ago
TriggeredBy: ● systemd-resolved-monitor.socket
● systemd-resolved-varlink.socket
Main PID: 360 (systemd-resolve)
Status: "Processing requests..."
这两个被触发的 socket 后面很关键——它们正是下面某条命令在没有 root 时失败的原因。
systemd-resolved 对比 nss-dns、dnsmasq 与 Unbound
大家总把这些混为一谈,其实它们不在同一层:
| 解析者 | 它是什么 | 缓存 | 按接口 | DoT/DoH | LLMNR/mDNS |
|---|---|---|---|---|---|
glibc nss-dns | libc 读 /etc/resolv.conf,明文发出 | 无 | 否 | 否 | 否 |
dnsmasq | 独立守护进程,转发器 + 缓存 + DHCP | 有 | 部分 | 仅转发 | 靠 avahi |
systemd-resolved | systemd 自带的解析器 | 有 | 原生支持 | 客户端,可选 | 内置 |
Unbound | 完整的递归 + 校验解析器 | 有 | 你自己接 | 走向上游 | 无 |
一句大实话:如果你要的从来只是”读 /etc/resolv.conf 然后解析”,那 nss-dns 至今干得挺好,resolvectl 给不了你什么。如果你要的是一台带缓存、还带 DHCP 给局域网的守护进程,dnsmasq 是经典。如果你已经有 systemd(你当然有)、又有多个接口、一个 VPN,或者想不多装一个包,那 systemd-resolved 早就在解决问题了。这就引到了人人都会踩的那一处:那个把应用指向它的文件。
127.0.0.53 存根与 /etc/resolv.conf 符号链接
应用够到存根的途径是 /etc/resolv.conf,而它在 systemd 机器上本来应该被接成一条指向 /run 的符号链接:
# 当 systemd-resolved 接管 /etc/resolv.conf(mode: stub)时,它是一个指向
# /run/systemd/resolve/ 的符号链接。以下是我机器上真实存在的两个文件:
$ ls -l /run/systemd/resolve/
-rw-r--r-- 1 systemd-resolve systemd-resolve 920 ... stub-resolv.conf # 存根
-rw-r--r-- 1 systemd-resolve systemd-resolve 808 ... resolv.conf # 直连上游
$ cat /run/systemd/resolve/stub-resolv.conf
nameserver 127.0.0.53
options edns0 trust-ad
search .
两个不同的文件,而 /etc/resolv.conf 指向哪一个,决定了一切:
stub-resolv.conf里是nameserver 127.0.0.53——应用打到缓存。用这个。resolv.conf里列的是真正的上游——应用绕过缓存,直接找223.5.5.5。这是”算了,干脆用上游”的逃生门。在把线接好的发行版上,你会ln -sf ../run/systemd/resolve/stub-resolv.conf /etc/resolv.conf。
现在说我实际面对的这台机器。我这里两条都没链接。NetworkManager 写自己的文件,于是存根在跑,却成了没人认领的孤儿:
$ ls -l /etc/resolv.conf
-rw-r--r-- 1 root root 75 10月12日 16:50 /etc/resolv.conf # 一个真实文件,不是符号链接
$ cat /etc/resolv.conf
# Generated by NetworkManager
nameserver 223.5.5.5
nameserver 119.29.29.29
$ ss -tuln | grep 53
udp UNCONN 0 0 127.0.0.54:53 0.0.0.0:* # LLMNR 存根
udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:* # DNS 存根,正在 LISTEN
tcp LISTEN 0 0 127.0.0.53%lo:53 0.0.0.0:*
tcp LISTEN 0 0 127.0.0.54:53 0.0.0.0:*
所以 127.0.0.53 上的存根是活的、健康的,而我的默认解析器却是 223.5.5.5。
resolvectl status 用一个词就点破了诊断——resolv.conf mode: foreign:
$ resolvectl status
Global
Protocols: +LLMNR +mDNS -DNSOverTLS DNSSEC=no/unsupported
resolv.conf mode: foreign
Current DNS Server: 223.5.5.5
DNS Servers: 223.5.5.5 119.29.29.29
Fallback DNS Servers: 9.9.9.9#dns.quad9.net 2620:fe::9#dns.quad9.net
1.1.1.1#cloudflare-dns.com ...
Link 3 (tun0) # VPN
Current Scopes: DNS
Protocols: -DefaultRoute +LLMNR +mDNS -DNSOverTLS DNSSEC=no/unsupported
Current DNS Server: 100.100.100.101
DNS Domain: ~internal
Default Route: no
Link 2 (wlp2s0) # Wi-Fi,默认路由
Current Scopes: DNS LLMNR/IPv4 LLMNR/IPv6 mDNS/IPv4 mDNS/IPv6
Protocols: +DefaultRoute +LLMNR +mDNS -DNSOverTLS DNSSEC=no/unsupported
Current DNS Server: 223.5.5.5
DNS Servers: 223.5.5.5 119.29.29.29
Default Route: yes
整篇的论点就浓缩在这一屏里。systemd-resolved 在跑,缓存在,但 foreign
模式意味着大多数应用根本看不到它——它们读 NetworkManager 那个文件,直奔阿里
DNS。而 resolvectl 以及任何显式打到存根的查询确实在用缓存。这就是”你遗忘
的那台存根解析器”,而在这样一台机器上第一件该修的事,就是把
/etc/resolv.conf 交还给存根。
缓存、resolvectl statistics,以及它为什么给我回 权限不够
缓存是回报:热点名字的重复查询在微秒级从内存答出,而且所有进程共享它。查看和 清空它的命令是:
$ resolvectl statistics # 当前缓存大小、命中/未命中
$ resolvectl flush-caches # 全部清空
唯独在我这台机器上,普通用户一条都跑不了:
$ resolvectl statistics
Failed to issue io.systemd.Resolve.Monitor.DumpStatistics() varlink call: 权限不够
$ resolvectl flush-caches
Failed to issue io.systemd.Resolve.Monitor.FlushCaches() varlink call: 权限不够
这是一个真实的 systemd-262 改动,我要白纸黑字记下来:缓存统计和清空从老的
D-Bus 方法(DumpStatistics 已经没了——直接调用会回 Unknown method)挪到了
受权限管控的 io.systemd.Resolve.Monitor varlink socket 上,它会用
权限不够(没有足够权限)回绝非特权调用方。所以在这台主机上,
resolvectl statistics 和 flush-caches 都需要 root,而下一节那些只读查询
不需要。我这儿没有 sudo,所以诚实的答案是:在 262 上、从一个非特权 shell,
我没法给你演示真实的命中/未命中计数。在更老的 systemd(≤ 252)上,它们过去是
以一张 Current Cache Size / Cache Hits / Cache Misses 的表返回的,没有这
种降级。
按接口的 DNS 与分流 DNS(resolvectl domain)
这是当你网络不止一个时,resolved 值得留下的那个功能。它把每个名字路由到正
确的链路。用 resolvectl domain 读这些域名:
$ resolvectl domain
Global:
Link 3 (tun0): ~internal
Link 60 (br-c9b53f86be7f):
...
Link 2 (wlp2s0):
tun0 上的 ~internal 是一个路由域名。开头的 ~ 表示”把 internal 之下
的名字发给这条链路的解析器,但不要把它当成搜索后缀追加。“再配上 VPN 的
Default Route: no 和 Wi-Fi 的 yes,这套策略就是一次教科书式的分流:
db.internal 走隧道的解析器(100.100.100.101,由跑着 tun0=10.126.126.3/24
的 Easytier 守护进程喂给它),其余全部走 Wi-Fi 的 223.5.5.5。如果你就是想
让 VPN 抢下所有 DNS,那条链路需要 Default Route: yes,用 resolvectl default-route tun0 yes 设置(需要 root)——上游的 ~. 哨兵域名起的作用等同
于一条路由条目。
为了用可控的方式演示这套机制,我在一个私有 Docker 网络上起了个一次性解析器
——不用拉镜像,coredns/coredns 本地就有。它只服务一个内网 zone:
# 容器里的 Corefile(裁剪过):
lab.internal.:53 {
hosts { 10.99.0.42 api.lab.internal
10.99.0.43 db.lab.internal
fallthrough }
whoami
log
}
docker run -d --name corednsdemo --net dnsdemo -p 1053:53/udp -p 1053:53/tcp \
coredns/coredns -conf /etc/coredns/Corefile
# 从同一网络上的第二个容器里:
$ nslookup api.lab.internal corednsdemo
Name: api.lab.internal
Address: 10.99.0.42
$ nslookup example.com corednsdemo
** server can't find example.com: REFUSED
*.lab.internal 解析到我埋进去的地址;一个公网名字直接 REFUSED。这正是每
链路解析器干的事:它权威地回答自己那一段后缀,其余一律拒绝——这也正是分流
DNS 按后缀而不是按”那台 DNS 服务器”来路由的原因。我还在容器网络之外、从主机经
发布端口验证过:
QUERY api.lab.internal -> 127.0.0.1:1053 answers=1 A=['10.99.0.42']
QUERY db.lab.internal -> 127.0.0.1:1053 answers=1 A=['10.99.0.43']
DNS over TLS(DNSOverTLS)与 DNSSEC
状态屏上写着 -DNSOverTLS DNSSEC=no/unsupported——DoT 关着,且在这套配置下
DNSSEC 不支持。你用一段 drop-in 打开 DoT(DNSOverTLS=yes 表示必须用,
opportunistic 表示尽力),再把某个链路的 DNS= 指向支持 DoT 的服务器,比如
1.1.1.1#cloudflare-dns.com。启用和重载都需要 root(改 drop-in、
systemctl restart systemd-resolved),所以我就用你也能用的、免 root 的办法
验证了传输层——向 Cloudflare 解析端口的裸 TLS 握手:
$ openssl s_client -connect 1.1.1.1:853
subject=CN=cloudflare-dns.com
issuer=CN=SSL.com SSL Intermediate CA ECC R2
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Verify return code: 0 (ok)
CN=cloudflare-dns.com 配上 Verify return code: 0 (ok),就是”这台解析器会说
DoT、并且出示的证书链能校验到你要配的那个 auth-name”的证据——不需要
TCP/853 抓包,也不需要 root。等 resolved 自己上了 DoT,resolvectl query 会
逐条应答告诉你;注意我这条明文查询带的是 acquired via local or encrypted transport: no:
$ resolvectl query blog.awin-x.top
blog.awin-x.top: 88.218.79.95 -- link: wlp2s0
-- Information acquired via protocol DNS in 55.4ms.
-- Data is authenticated: no; Data was acquired via local or encrypted transport: no
-- Data from: network
最后这行同时兼任 DNSSEC 的读数:今天 authenticated: no,等上游会校验、并设了
DNSSEC=yes 之后它才会翻成 yes。(注意这行不是你的证书检查——那件事交给
openssl 握手。)对于私有的、你不会公开的域名,DNSSEC 是杀鸡用牛刀;对公网域
名,挑一个会校验的上游、让 resolved 信任 AD 位就行,存根文件里那个
options trust-ad 干的就是这个。
三大坑,人人都会踩
**改 /etc/resolv.conf 会被覆盖。**它写着 # Generated by NetworkManager。你
在里面敲的任何东西,下次重连都会被重新生成。要配就配那个真正拥有它的东西——
NetworkManager,或者通过 resolvectl 和 resolved.conf.d/ drop-in 配
systemd-resolved——永远别去配那个文件。(比如我那个全局
DNS=223.5.5.5 119.29.29.29,来自
/etc/systemd/resolved.conf.d/99-easytier-dns.conf,而不是我改 resolv.conf 改
出来的。)
**VPN 的 DNS “没生效”。**VPN 拉起来不等于它的解析器就赢了。我的 tun0 是
Default Route: no,所以普通查询仍然走 Wi-Fi。只有 ~internal 之下的名字才用
隧道。你要是想让 VPN 兜住一切,就把它标成默认路由(需要 root)——否则这就是分
流 DNS 的正确行为,不是 bug。
.local 撞名。 *.local 是留给 mDNS/LLMNR 的,所以就把它当成一个本地
链路名字,永不转发到单播。后果:一个正经的、以 .local 结尾的公网 DNS zone永
远解析不了;一个多归属的 .local 名字还可能在多台主机之间飘。我的查询也显示,
这种失败不过是”没人答应”:
$ resolvectl query foo.local
foo.local: resolve call failed: All attempts to contact name servers or networks failed
链路上没有响应者,也没有单播兜底——这是设计如此。
什么时候该用 Unbound 或 dnsmasq 替换 systemd-resolved
这些情况下就留着 resolved:它本来就在跑、你想要按接口的分流 DNS、你用 VPN, 而且缓存加可选 DoT 就够。别跟它较劲。
而当你的需求超出了一个转发器,就该换家伙了。那条清晰的界线是:resolved 转发并 缓存;Unbound 自己递归并校验,带 qname 最小化;而当你想要一台缓存 + DHCP
- 广告拦截上游三合一的二进制时,dnsmasq 依旧是小盒子的冠军。如果你明确想
要一台自己从根服务器一路走下来、本地做 DNSSEC 校验、或者服务整个 LAN 的解析
器,那你就已经超出了一个存根的定位——那正是我的
《自建递归 DNS 解析器》那篇的主题。就我
这台机器而言,一个 VPN 在做分流,resolved 已经值回票价;我唯一会做的改动,是
把
/etc/resolv.conf重新指到127.0.0.53,好让我从没舍得关掉的那份缓存,终 于被用上。