前提条件说明
本文讨论基于以下前提:
- 你的系统使用了 systemd-resolved
- 网络由 NetworkManager 或 Netplan 接管
- /etc/resolv.conf 为动态生成的符号链接
- 注意:不同发行版默认配置可能略有差异(如 nsswitch.conf 或 NetworkManager 设置)。
在默认使用 systemd-resolved + NetworkManager / Netplan 的现代 Linux 发行版中,/etc/resolv.conf 往往不是配置源,而是一个动态生成的解析接口。手动修改的内容通常无法跨越下一次网络重连或系统重启,因为经理人们随时准备根据最新的网络环境背刺你的修改。
0. DNS 解析的三层结构(核心心智模型)
需要明确的是,systemd-resolved 只是 glibc NSS 解析链条中的一个可选后端。在现代 Linux 系统中,一次 DNS 查询是一个严密的分层架构:
A. 应用层:应用程序(浏览器、ping、curl)并不直接查询 DNS 服务器,而是调用 glibc 提供的解析函数(如 getaddrinfo())。
B. glibc 解析器(Stub Resolver):它的行为取决于 /etc/nsswitch.conf 的配置链:
- 现代 nss-resolve 路径:glibc 通过 IPC 直接与 systemd-resolved 通信,完全不经过 libc 默认的 UDP stub 路径。
- 传统 nss-dns 路径:它读取 /etc/resolv.conf 中指定的 nameserver(通常是 127.0.0.53)并发送 UDP 请求。
- 注意:非 glibc 程序(如 Go 的纯 Go 解析器)仍可能绕过此逻辑直接读取 /etc/resolv.conf。
C. 本地解析服务(systemd-resolved):接收到请求后,根据 per-link DNS 配置和路由策略选择上游 DNS。
D. 执行层(上游 DNS):最终将请求发往真正的 ISP、公共 DNS 或内网 DNS。
1. 为什么 resolv.conf 设计成“瞬态报告”?
这不是反人类,而是为了移动化适配。在 Wi-Fi、热点、VPN 频繁切换的今天,系统需要一个“调解员”实时处理链路冲突。如果 resolv.conf 是死文件,当你从公司 VPN 切换到手机热点时,由于 DNS 无法自动更新,你的网络会立刻瘫痪。
2. 权力架构:谁才是真正的老板?
在采用 systemd 体系的发行版中,DNS 解析通常由 systemd-resolved 负责聚合和管理。其生成的解析配置(位于 /run/systemd/resolve/ 下),通常由安装脚本或管理员手动通过符号链接映射到 /etc/resolv.conf。
核心契约:当该链接指向 systemd-resolved 时,网络管理器不再直接写入该文件,而是通过 D-Bus 将配置提交给它。
3. 核心痛点:DHCP “全家桶”污染
当你连上公共 Wi-Fi,DHCP 协议会强推一个 “全家桶”给你:包含 IP、网关和运营商提供的默认 DNS。更阴险的是 IPv6 后门:很多人只堵住了 IPv4,结果系统又通过 DHCPv6 或是 IPv6 的路由器广播(RA)偷偷塞进一个 IPv6 DNS 地址,导致解析再次被覆盖。
4. 三大对策、各自缺点及 VPN 影响
正统派:经理人覆盖 (Overrides)
Ubuntu Server(使用 systemd-networkd 后端)可以在 Netplan YAML 中将 dhcp4-overrides 和 dhcp6-overrides 的 use-dns 设为 false。
⚠️ 生产环境警告:如果你使用的是 NetworkManager 后端,Netplan 的配置不会生效。你必须通过 nmcli 显式关闭自动 DNS 获取。如果你通过 SSH 远程操作,建议使用链式执行以缩短操作窗口,减少因连接中断导致的配置不一致风险:
# 链式执行:修改并立即激活。注意:&& 不能防御错误的配置,如果配置本身有误,你仍会失联。 nmcli con modify <connection-name> ipv4.ignore-auto-dns yes ipv6.ignore-auto-dns yes && nmcli con up <connection-name>
专家建议:对于极其关键的远程服务器,最稳妥的做法是在执行前预设一个 5 分钟后的自动回滚任务(如 echo "nmcli con up <old-connection-name>" | at now + 5 min),确保即使配置错误也能自动恢复连接。
挂钩派:resolvconf 前置挂钩 (Prepending Hook)
- 机制:通过修改 /etc/resolvconf/resolv.conf.d/head 文件,将配置强行拼接到生成的 resolv.conf 最顶部。
- 实施前提:必须手动停用并禁用 systemd-resolved 服务,并安装传统替代包(如 openresolv)。
- “咖啡店陷阱”:在需要网页认证的 Wi-Fi 下,强制的 Public DNS 无法解析局域网内的本地跳转地址,会导致你永远打不开登录页面。
- VPN 破坏者:会破坏依赖 systemd-resolved 注入 DNS 的 VPN(如 WireGuard 或 Tailscale),导致内网解析失败。
暴力派:chattr 焊死 (The Nuclear Option)
删掉软链接改实体文件,写死 8.8.8.8 后执行 chattr +i /etc/resolv.conf。
- 排障噩梦:NetworkManager 会将其标记为 unmanaged;而 systemd-resolved 会进入“消费者模式”(Consumer Mode),仅仅将其作为向下兼容的参考输入。
- 分歧路径:绕过 libc 直接读文件的程序(如 Go/Node.js),其最终查询目标将不可避免地退化为你锁死的 8.8.8.8,导致 VPN 的 per-link DNS 路由报废。
5. 那个消失的 127.0.0.53
之所以称之为“消失”,是因为在现代 Linux 配置下,你能在 /etc/resolv.conf 中直视这个地址,却无法在任何物理网络接口上捕获它的流量。虽然 netstat 显示 127.0.0.53 处于监听状态,但由于 nss-resolve 优先走内存级 IPC 通信,绝大多数解析流量根本不会经过 lo 回环接口。
- 流量路径:
- 前半程 (App → Resolver):如果是现代 App,走 IPC 路径,lo 接口上抓不到包;如果是传统 App(如 dig),走 UDP 路径,你在 lo 上能抓到包。
- 后半程 (Resolver → 上游):一旦本地缓存不命中,systemd-resolved 必须向外求助。此时报文会通过路由表从物理网卡(如 wlo1)发出。
实际上,systemd 官方强烈建议程序通过 nss-resolve 直接通信。而 127.0.0.53 仅仅是为那些直接发送 UDP 裸请求的程序提供的兼容性 Stub Listener(官方从 v252 开始还在 127.0.0.54 提供了一个跳过本地合成逻辑、偏向纯转发语义的 Proxy 模式)。
6. 真正的入口:nsswitch.conf
在 Linux 中,解析顺序取决于 /etc/nsswitch.conf 的 hosts: 行。现代发行版常见的配置如下:
hosts: files resolve [!UNAVAIL=return] dns
- 顺序决定论:glibc 严格从左到右按序调用模块。尽管官方建议将 resolve 排在 files 前以利用缓存,但多数发行版仍优先保障 /etc/hosts 的权威。
- 断路器机制:[!UNAVAIL=return] 意味着只有当 systemd-resolved 进程彻底死亡或不可达时,才会向右回退。如果只是单纯的域名不存在(NXDOMAIN),解析会直接终止,不会触发回退。
7. 短路逻辑与敏感泄露
在许多被旧版部署工具、自动化脚本或人工干预过的系统中,/etc/nsswitch.conf 往往会变成一个充满陷阱的乱序版本。问题并不在于解析模块本身有毒,而在于 NSS 的链式顺序与断路器语法被错误地拼接成了如下模样:
hosts: files mdns4_minimal [NOTFOUND=return] resolve [!UNAVAIL=return] dns mymachines
第一道封锁线:.local 与断路器引发的解析车祸
nss-mdns 模块的职责本就是通过 mDNS 解析 .local,这无可厚非。但如果你公司的 AD 域为 corp.contoso.local,在上述带有断路器的顺序下将演变成灾难。mdns4_minimal 优先在物理局域网发送组播,找不到后,紧随其后的 [NOTFOUND=return] 指令会瞬间截断后续的查询链。这意味着你的 VPN DNS(即使配置在了 resolve 或 dns 层面)根本收不到请求,内网身份验证直接熔断。这是一种极其典型的配置顺序陷阱。
队尾的刺客:乱序 mymachines 的隐私泄露
nss-mymachines 用于解析注册到本地 systemd-machined 的容器名。系统官方标准建议将其放置在 resolve 和 dns 之前,以保障本地容器名优先命中,这在正常配置的系统下是完全安全的。然而,在被篡改或错误配置的乱序系统中(如上述配置中它掉到了 dns 之后),它就成了“队尾的刺客”。如果你 ping 一个本地容器(如 test-db),系统会先让 dns 模块去查一圈公网报错后,才轮到它。这意味着这种配置级的 Bug,会将你的本地容器命名架构明文泄露给配置的上游 DNS 服务器(即便上游最终返回 NXDOMAIN,带有你容器名的查询包也已经出网被记录)。
8. Netplan 的秩序 vs. 恶意的破坏: Netplan 的逻辑秩序与版本敏感性
许多人试图在 YAML 中配置 dhcp4-overrides: use-dns: false 来一劳永逸地拒绝 DHCP 污染。这确实确立了一种逻辑秩序,但请务必注意后端与版本的敏感性:Netplan 官方文档指出,use-dns 设定主要针对 networkd 后端生效。
尽管现代 Netplan 确实会尝试将 dhcp4-overrides 翻译并应用到 NetworkManager 的 DHCP 流程中(转化为 NM 的 ipv4.ignore-auto-dns=yes 属性),但这种跨后端的映射高度依赖版本,在诸如 Ubuntu 20.04 LTS 等仍被广泛使用的旧版系统中极易失效。因此,在极度关键的生产场景中,切勿盲目依赖 YAML 的抽象翻译,更稳妥的做法是直接核实 nmcli 的实际执行状态或底层生成的 keyfile。
秩序防不住“破门者”
即使你完美锁定了底层配置,如果有流氓 VPN 客户端直接以 root 权限执行 write() 强覆写 /etc/resolv.conf,或者恶意进程强占了与 systemd-resolved 冲突的 53 端口(例如试图绑定 0.0.0.0:53 而非避开 127.0.0.53:53),你的 nslookup 依然会发生泄露或引发服务死锁。秩序约束的是遵循系统调用的合法组件;要防备真正的破坏者,你依然需要求助于 AppArmor 等强制访问控制或底层的包过滤机制。
9. 隐私的终极形态:DoT 与 DNSSEC 的正交防护
纯靠锁死 8.8.8.8 无法防止基于明文 DNS(UDP/TCP 53)的链路劫持或透明代理。systemd-resolved 原生支持 DNS-over-TLS (DoT)(用于传输链路加密)以及 DNSSEC(用于记录完整性校验)。通过修改 /etc/systemd/resolved.conf 启用 DNSOverTLS=yes,在上游解析器支持 DoT 的前提下,让解析请求在 TLS 加密通道中传输。这才是比单纯修改文件更深层的防护机制(可防止链路监听与篡改,但不改变对上游解析器的信任假设)。
10. 软链接的“狸猫换太子”
在 Ubuntu 中,/etc/resolv.conf 的指向决定了你的“抗干扰能力”:
- 模式 A(默认):指向 ../run/systemd/resolve/stub-resolv.conf。解析经过管理员过滤,支持 Split-DNS。
- 模式 B(底层):指向 ../run/systemd/resolve/resolv.conf。上游 DNS 直接暴露在文件里,不经过 127.0.0.53 转发。
11. 终极验证:让理论成为可见的数据
- 查看当前真实使用的 DNS:resolvectl status
- 定位 Split-DNS 链路污染的神器:resolvectl query google.com (它会绕过 glibc 告诉你最终选用了哪个网卡接口和哪个具体服务器)
12. 观测与旧时代的遗产:没 nscd 导致的缓存缺失
古代 Linux 依靠 nscd 在 libc 层面拦截并提供独立缓存。现代发行版已默认不再预装该组件,并在很大程度上将其边缘化(尽管部分 LDAP/NIS 企业环境仍在苟延残喘)。因此,只要你的程序(如纯 Go 解析器)或挂钩策略绕过了 systemd-resolved 的 IPC 接口,你的查询就会走传统的 nss-dns 路径。由于该路径默认不带缓存,你将跌落回“零缓存”状态,每次查询都会触发真实的物理发包。
法证观测法 (resolvectl monitor)
你可以使用此命令实时监听 D-Bus 总线上的解析请求。法证级盲点警告:它只能看到走 IPC 隧道的查询。如果你的程序绕过了缓存直接读取 /etc/resolv.conf(如 dig 或纯 Go 程序),monitor 的界面上将是一片死寂。这种“看不见”,正是底层规则被绕过的最强铁证。
13. DNSCrypt 与 Dnsmasq 的乱入
当你试图通过 /head 引入 127.0.0.1 来对接 dnscrypt-proxy 或 dnsmasq 时,极易引发 53 端口启动死锁。默认情况下,systemd-resolved 牢牢绑定在 127.0.0.53:53。虽然这与 127.0.0.1 是独立的 IP,但大多数第三方 DNS 缓存工具在出厂配置下会默认尝试全局监听(绑定 0.0.0.0:53)。在 Linux 内核机制下,当具体 IP 的端口已被占用时,后续的全局通配符绑定(Wildcard Bind)会被内核直接以 Address already in use 拒绝。这就是为什么许多极客配置了本地代理却莫名启动失败——除非你懂得去深入修改代理工具的底层配置(如在 dnsmasq 中强制开启 bind-interfaces),否则最终往往不得不退回硬编码 8.8.8.8。
14. 服务器(Server)环境下的生死法则
- 云厂商的 VPC 结界:云架构完全依赖云厂商提供的“内网专属 DNS”(如 AWS 的 169.254.169.253)。在云上,必须优先使用云内网 DNS,否则会导致内网服务解析熔断。
- 代理中转税:在极高并发场景下(如单机数万 QPS),本地 stub 转发路径会引入额外的内核态与用户态切换(Context Switch)及潜在的锁竞争。
15. Docker/K8s 的云原生结界
为什么运行在系统上的容器不会被宿主机的 127.0.0.53 搞崩溃?因为容器引擎拥有一套独立且强悍的旁路法则:
- Docker 的条件触发回退(Conditional Fallback):在默认的 bridge 网络中,Docker 会参考并继承宿主机的 /etc/resolv.conf(并经过引擎的过滤与重写)。法证级细节:如果 Docker(底层 libnetwork 源码)发现宿主机的配置中包含回环地址(如 127.0.0.53),为了防止容器内发生不可逾越的路由死循环,会强制将其剔除。更暗黑的是,如果剔除后没有留下任何有效的上游 DNS,在部分实现与运行环境中,可能会触发兜底机制,注入预设的公共 DNS 服务器(如 8.8.8.8 和 8.8.4.4)。而在自定义网络(User-defined networks)中,Docker 通常会启用内嵌的 DNS 引擎(固定为 127.0.0.11)(除非被显式 DNS 参数或特殊网络模式如 host 覆盖),负责容器内的服务发现与上游转发。
- K8s 的集群级接管:在 Kubernetes 世界里,DNS 解析被彻底拔高到了集群控制面。kubelet 会根据 dnsPolicy 策略接管并重写容器内的 /etc/resolv.conf 的内容。在默认的 ClusterFirst 策略族(及相关变体)下,这意味着容器的解析路径不再依赖宿主机的 systemd-resolved 守护进程或其宿主级 NSS 解析链,而是通过容器内部的 libc Resolver 指向集群 DNS Service(通常为 CoreDNS 的 ClusterIP,底层经由 kube-proxy 或 IPVS 完成 VIP 流量转发),将解析路径控制权完全收敛至集群控制面(对于集群外部域名,解析请求最终仍可能由 CoreDNS 根据配置转发至外部上游)。(注意:如果你手贱设置了 dnsPolicy: Default,Pod 将直接继承宿主机的解析配置,从而可能再次暴露于宿主机解析机制引发的异常,例如回环地址不可达)。
16. Android 与系统的分化: 移动端的终极形态(DnsResolver APEX)
在 Android 系统中,/etc/resolv.conf 早就被抛弃。在 Android 9 及以前,解析从 Bionic libc 走向 netd 的 Socket;而从 Android 10 开始,为了追求更独立的模块化升级,DNS Resolver 被独立为 com.android.resolv APEX 模块。现代 Android 架构通过底层专有 API 与这个独立的系统模块进行高性能 IPC 通信。这印证了现代操作系统在网络切换频繁的场景下,“废弃静态文件,拥抱模块化进程通信”是不可逆的演进方向。