@snownstone

dig DNS - 2

Published on

关于 DNS,我本只是想知道我的电脑(Ubuntu 22.04)究竟是怎么处理这个问题的,具体的,如何指定本机所有 DNS 查询请求去到我设定的上游 DNS 服务器,对应 之前 DNS 查询流程的步骤 1 和 2

结果事到如今,发现这是一个远比我以为的复杂得多的问题,DNS 甚至都不是 the whole picture,整个问题域现在看下来至少要扩大到 名称解析(name resolution) 这一更大的范畴,而 DNS 只是其中一种重要的技术手段,其他还有如 LLMNR、mDNS、DNSSEC 验证等。此外,即使只聚焦于 DNS,问题也有很多,后面会触及到。

因为 Ubuntu、Fedora、Arch 等 Linux 发行包都默认使用 systemd-resolved 来为本地应用提供名称解析相关的系统服务,所以本篇内容基本就是以它为中心,关于它是什么、怎么工作、怎么设置、以及如何避开它使用其他技术。

不同于前一篇,这篇会涉及很多技术性的概念,没有办法,只有基于这些更底层的系统相关的细节,才能摸清其中的道道,进而实现灵活且有把握的控制。


目录


systemd, D-Bus 和 glibc

systemd

单从名称上看就知道 systemd-resolvedsystemd 肯定是有关系的。

systemd是 Ubuntu 等 Linux 系统采用的系统和服务管理器(System and Service Manager),是开机后 init 的一种具体实现,是用户空间第一个被启动的进程(PID 1)。systemd-resolved 是它的组成部分之一,主要负责名称解析。这里 可以看到 Ubuntu 版 systemd 源文件的构成。

D-Bus

D-Bus 即桌面总线,主要用于进程间通信(IPC,interprocess communication)。靠近用户端的不同桌面程序可以通过它相互沟通,更底层的 systemdNetworkManager 也都使用它通信。因此,systemd-resolved 为本机其他需要域名解析服务的应用提供的首要的 API 就是 D-Bus API。官方文档有清晰的说明,推荐所有应用首选 systemd-resolved D-Bus API。关于 NetworkManager,这里先简单说一下,很明显它是用于网络配置管理的,主要面向桌面用户,用户通过它配置网络,它再用相关信息对更底层、远离用户端的 systemd-resolved 进行配置。此外,说是用户配置,其实多数时候都是软件自动完成的,除非用户出于某些需要想要做一些特定的调整。

glibc

glibc 即 GNU 版的 C 标准库。

之所以要提及它,是因为很多时候,域名解析都是应用通过调用 glibc 函数实现的,如曾经的 gethostbyname(),现在的 getaddrinfo()getnameinfo()。于是,为了继承和兼容支持调用 glibc 域名解析函数的应用,systemd-resolved 同时提供 glibc API,这部分尤其纠缠,后面再重点梳理。

参考 1


glibc nss-dns

最早期甚至还没有 DNS 查询的时候,/etc/hosts 这个用户手写的文档就存在了,用于本机本地名称解析。现在各种复杂的技术都出现了,这个文档依然存在,而且,非常重要的是,各个应用实现自己名称解析的手段可能五花八门随心所欲,但是它们 全都尊重 /etc/hosts 的设定!

慢慢地网络出现了,NSS (Name Service Switch)就来了,主要是为了实现同一网络内不同机器之间动态地分享配置信息,glibc 中有一个专门的模块叫作 NSS 就负责这部分功能的实现,名称与对应 IP 也是其中一组配置信息。

关于 nss 的配置,可以查看 /etc/nsswitch.conf 文件:

nsswitch配置

如上图所示,hosts 这一行信息就规定了当应用调用 glibc nss 做域名解析时(如曾经的 gethostbyname() 或现在的 getaddrinfo()),它的行动步骤:

  1. files 表示会首先查询 /etc/hosts 文件中保存记录的域名和 IP 信息

  2. mdns4_minimal 表示 多播 DNS(Multicast DNS, mdns),只负责解析 .local 域名。多播就是在局域网内广播域名,同一网内的设备如果听到自己的域名被呼喊,就会立即报告 IP。后面紧跟的 [NOTFOUND=return] 表示如果前面的 mdns_minimal 没有找到域名对应 IP,就立即返回错误报告,不要再使用后面的技术继续解析,即不必要把本地的 .local 域名丢给外部的 DNS 服务器解析。

  3. dns,就是发送请求给外部的 DNS 服务器去查询了。至于访问外部的哪个 DNS 服务器,则是 /etc/resolv.conf 文件规定的。

这个 /etc/resolv.conf 文件也是复杂又纠缠,后面再细说。这里只说一点,某些应用可能不会调用 glibc nss-dns 来做域名解析,它们可能会直接读取 /etc/resolv.conf 内设定的 DNS 服务器 IP 信息(通常会是路由器默认使用的网络服务商的 DNS 服务器),自己独立完成域名解析工作。这些无疑都增加了整个名称解析问题的复杂性。

以上即 systemd-resolved 入场之前,通常的,使用或绕过 glibc nss-dns 如何实现域名解析。

参考 2

  • What Does It Take To Resolve A Hostname

    关于名称解析问题的复杂性,本文是很好的说明,特别是从历史的角度,做了更具体的梳理。但可能是演讲稿转化的缘故,虽然给出了重要线索和方向还有历史溯源,很多地方的表达我感觉不是很清晰,在对文中概念不熟悉的情况下,上来就看或许会很懵,当然技术背景强的人可能一看就懂。

  • the tragedy of gethostbyname

    这篇文章主要是在批评基于 glibc 调用 getaddrinfo(3) 解析域名是不可靠的,建议软件开发时使用专门的名称解析软件包。


systemd-resolved

本文的主角终于正式上场了!

前面已提及,systemd-resolvedsystemd 的构成部分,是 Ubuntu 等 Linux 系统自带的 Network Name Resolution 管理器,目的是为本机软件提供域名解析相关的服务,它会以 守护进程(daemon) 的方式在后台持续运行,可以承担缓存、DNS/DNSSEC 解析器、以及 LLMNR 与 MulticastDNS 解析器等工作。

三种接口

我当前的理解是,它试图通过提供多种接口把本机所有应用的名称解析请求全都聚拢到它这里来,由它自己统一负责。具体地:

  1. 官方文档首推的 D-Bus API,功能齐全,异步,且支持 DNSSEC 验证

  2. 针对那些习惯调用 glibc getaddrinfo(3),即,使用 glibc nss-dns 的应用,通过提供 glibc nss-resolve 这样一个插件,实现那些应用和 systemd-resolved 的通信,从而由后者接手负责名称解析;非异步,不支持 DNSSEC 验证

  3. 针对那些无论以上 1 和 2 都不使用而自己直接向外发送 DNS 查询请求的应用,systemd-resolved 另外在本地 127.0.0.53#53127.0.0.54 回环接口上布置了 DNS stub listener,试图以此将那些应用连接上 systemd-resolved,仍由后者负责 DNS 查询。

补充说明:

可能是因为没有软件开发的实操经验,我在 2 这个点上原地打转了好久才终于明白过来,以防有人和我类似,所以再啰嗦几句。我的理解就是,那些调用 glibc 函数的应用,看起来是在利用 glibc 原有的、它自己的 DNS 查询和解析功能(即 nss-dns),但其实 systemd-resolved 通过 glibc nss-resolve 这个插件,将 DNS 查询和解析这项工作给中途拦下并自己接手了!

3 也是类似道理。前面说过,有些“任性”的应用就是要自己负责 DNS 查询,所以它们会直接从 /etc/resolv.conf 文档中读走外部 DNS 服务器的 IP 地址。于是,systemd-resolved 就在 127.0.0.53#53 布置一个本地 resolver 监听着 53 端口,同时把本地这个 IP 地址写入 /etc/resolv.conf文档,这样那些“任性”的应用发出的查询请求不就又掉到它怀里去了嘛!

总之就是你们不要自己搞,全都交给我来统一搞定!(也是煞费苦心有点可爱...

nss-resolve vs nss-dns

既然 systemd-resolved 如此积极抢工作,那它到底有啥好处呢,特别是相比通常使用的 glibc nss-dns

  • nss-resolve 支持根据不同的网络接口(如无线网卡接口、以太网接口等),分别设置 DNS 远程服务器,实现 DNS 分流;而 nss-dns,只支持设定一个全局的 DNS 服务器列表(最多设定 3 个)

  • nss-dns 会被调用加载到每个应用自己的进程中,各个进程分别各自访问远程 DNS 服务器。而 nss-resolve 被调用进入进程后,会通过 D-Bus 接口联系 systemd-resolved 守护进程,统一让后者去完成访问远程 DNS 服务器的工作。因为它都统一接下了,于是在自己内部可以创建一个大家共用的 缓存

  • 使用 D-Bus 通信,nss-resolve 的状态是动态变化的,可以和其他守护进程或图形界面有更强的整合,例如 NetworkManager

说明:从前面 /etc/nsswitch.conf 文件 hosts 部分截图可以看出,里面当前没有 resolve,说明我本机还没有启用这个 nss-resolve 插件。

DNS 服务器设定

以下这几种方式都可以设定 systemd-resolved 使用的 DNS 服务器

  • 全局设定遵从 /etc/systemd/resolved.conf 文档的内容。注意,不同于前面出现的 /etc/resolv.conf 文档

  • 除全局设定外,支持 per-link 静态文件设定

    • Ubuntu 服务器版使用 systemd-networkd 进行网络配置管理,依据不同的网络接口,对应的静态文件是 /etc/systemd/network/*.network

    • Ubuntu 桌面版使用 NetworkManager,因为 Ubuntu 现在用 netplan 作为 NetworkManagersystemd-networkd 的前端,对应文件是/etc/netplan/*.yaml

  • per-link DHCP 动态设定:NetworkManager 自动地通过 D-Bus 将网络接口信息推送给 systemd-resolved(它俩在系统内有很好的整合)

  • resolvectlsystemd-resolved D-Bus API 的 wrapper,在终端内调用这个命令可以实现多种功能,如域名解析、清理缓存、设定 DNS 服务器等,后面命令部分有具体示例

  • 其他服务的设定。后面会讲到,其实我本机因为安装了 AdGuard Home,实际上回避掉了 systemd-resolved 提供的服务,AdGuard Home 通过 /etc/systemd/resolved.conf.d drop-in 的方式设定了它自己作为 DNS 服务器。(装它是稀里糊涂装的,道理是后来才懂的,后面再细说。)

总之,关键就是 /etc/systemd/resolved.conf 的设定以及 per-link DHCP 动态设定。一般情况下,连接路由器后,它们会自动配置好网络以及对应文件保存的设定。只有当用户因为某些个性化的需求要做特别调整时,可能才会去查看相关信息,并手动修改。

使用 resolvectl status 命令查看,我系统内当前的 DNS 服务器设定如下,127.0.0.1#53 是 AdGuard Home 运行在我本机的 DNS 服务器:

resolvectl status

/etc/resolv.conf

前面已提及,当 systemd-resolved 出场想要全面接手本机所有 DNS 查询请求时,它一方面提供自己的 D-Bus API,另一方面还要兼容支持那些习惯性调用 glibc nss-dns、以及谁也不用就要自己独立解析域名的应用。

  • 为了支持(也可以说是拦截)那些要自己独立发送 DNS 查询请求的应用,可以将 /etc/resolv.conf 实际设置为一个指向 /run/systemd/resolve/stub-resolv.conf软链接(symbolic link),将 127.0.0.53 作为唯一的 DNS 服务器写入其中,对应 三种接口 的第三种。由此,应用还是会通过 systemd-resolved 进行域名解析。

  • 也可以将 /etc/resolv.conf 设置为指向静态文件 /usr/lib/systemd/resolv.conf 的软链接,类似上面,这个文件也将 127.0.0.53 作为唯一的 DNS 服务器,区别在于这个文件是静态的,上面那个 /run/systemd/resolve/stub-resolv.conf 是实时动态变化的,且包含 搜索域名(search domain)

  • 此外,还可以将 /etc/resolv.conf 设为指向 /run/systemd/resolve/resolv.conf 文件的软链接,后面这个文件也是动态变化的,保存的是 systemd-resolved 当前已知的系统全局性 DNS 服务器(非 per-link)。这个厉害了,这种模式下,读取 /etc/resolv.conf 数据的应用,就终于可以完全避开 systemd-resolved,自己直接向远程上游 DNS 服务器发送查询请求了!

  • 最后一种情况,其他软件接管控制了 /etc/resolv.conf,这种模式下,systemd-resolved 也只好乖乖读取其中的配置信息,听从安排了

如下图所示,在我当前的系统中,/etc/resolv.conf 作为软链接指向 /run/systemd/resolve/resolv.conf,属于上面第三种模式:

resolv.conf 软链接

AdGuard Home 也的确通过在我本地设置 DNS 服务器 127.0.0.1#53,避开了 systemd-resolved

/etc/resolv.conf

参考 3

前面两篇可以相互参照补充着看,写得非常好。内容都是关于 Fedora 启用 systemd-resolved 后,针对 nss-resolve 相比 nss-dns 的优点,特别是支持 per-link DNS 服务器设定这一特点,在 VPN 配置上的应用。


避开 systemd-resolved

回到我最开始关心的问题:如何操作可以强制本机所有 DNS 查询全都发往唯一/二自己指定的上游 DNS 服务器,下面这篇帖子的作者问了相似的问题,答案见得分最高的回答:

AdGuard Home 和 Pi-hole

我则是通过稀里糊涂地安装 AdGuard Home 做到的,相当于在本机另外装了一个 DNS 解析器,然后在控制界面手动设置了 DNS 服务器,后来通过 Wireshark 抓包,发现 UDP 数据包确实都发给了指定的上游 DNS 服务器。

安装过程中报了下面的错误,因为 AdGuard Home 的 DNS resolver 要和 systemd-resolved127.0.0.53#53 stub resolver 争用 53 端口,于是要禁用 DNSStubListener,如文中给出的解决办法所示。可以看出,改动的就是 /ect/systemd/resolved.conf 的配置(通过 drop in 方式),设定 127.0.0.1#53 为全局 DNS 服务器;此外,应用通过调用 nss-dns 或是自己直接访问 /etc/resolv.conf/run/systemd/resolve/resolv.conf的软链接)获取 DNS IP,如前面讲过的,避开了 systemd-resolved

注:关于 Wireshark 的安装,中间会弹出一个窗口需要对权限做一个选择,默认建议的是选否,实际应该选 Yes,不然后面没有权限访问网卡数据。此外,还会报一个"couldn't run /usr/bin/dumpcap in child process" 的错,针对这两个问题解决方法分别是:

sudo dpkg-reconfigure wireshark-common #修改权限,选 yes
sudo chmod +x /usr/bin/dumpcap #改为可执行文件,解决报错

关于我的稀里糊涂,一是不懂其中的机制,二是明明有更可爱的 Pi-hole 可选,但我当时不知道!叫 Pi 的怎么都这么可爱,全是可爱派...

BIND

至于 BIND,它是常用的构建 DNS 服务器的软件,从我安装 AdGuard Home 报的错来看,它应该也用了 BIND。具体地,可以参考下面的教程,这部分又会呼应上一篇 DNS 服务器的类型问题


附 1:命令

dignslookup

都可以做域名解析,默认使用 /etc/resolv.conf 的设定服务器,在我本机当前的配置下,用的都是 AdGuard Home 中我指定的。也可以以参数的方式在 nslookup 命令中指定某个具体的 DNS 服务器。dig +trace 可以看到 DNS 查询经过的各层级服务器。

dig +trace snownstone.com
dig +trace1
dig +trace2
dig +trace3
dig +trace4

参考:

注:顺带记一笔,ping 与它们不同,用的是 ICMP 协议

resolvectl

resolvectl query archlinux.org #域名解析
resolvectl query
resolvectl status #查看 global 和 per-link DNS 设定

resolvectl status

systemctl

systemctl is-active systemd-resolved #查看系统某服务是否在运行
systemctl
systemctl status system-resolved.service

netstatlsof

netstat 这个命令可以查看本机当前网络连接相关的数据,如协议类型、端口号、IP、状态等;lsof 则可以列出正在使用或监听端口的程序。


附 2:网络配置

路由器设置

  • 在浏览器中直接输入路由器在局域网内的 IP 192.168.31.1

  • 默认的是启用 DHCP 服务,动态分配 IP 给同一局域网内接入的其他设备,也可以手动设置给机器一个固定的 IP 地址

  • 可以手动设置 DNS 服务器 IP

  • 在路由器中做的配置修改会自动更新其他接入设备内的相关网络设置

Ubuntu 网络配置

服务器版系统用的是 Systemd-networkd,桌面版用的是 NetworkManager,同时新的版本开始用 Netplan(作为前端) 生成 yaml 格式的网络配置文件。

桌面版所有连接过的网络设置信息都保存在 /etc/NetworkManager/system-connections 路径下。

NetworkManager 默认的是 DHCP 配置,由路由器给接入网络的机器动态分配 IP 地址,DNS 服务器默认用的也是路由器的默认设置,通常就是网络运营商的 DNS 服务器。如果在路由器的配置页面(192.168.31.1)对 DNS 做手动修改,重启生效后,电脑的设置也会自动跟着修改。

AdGuard Home 将我的全局 DNS 服务器设置为了它自己(即 localhost:127.0.0.1#53),结果就是即使不对路由器的默认配置做手动修改,DNS 查询当前去的也是 AdGuard Home 中指定的服务器。