Linux上DNS客户端配置的西西弗斯式任务

  域名服务(Domain Name Service)发明于1983年。DNS是一个可以让你把名字转换成IP地址的系统,这样你的电脑就可以知道如何连接像google.com这样的网站。这是一个简单的服务,因此4.3 BSD的作者指定了一个名为/etc/resolv.conf的简单配置文件:

$ cat /etc/resolv.conf   
nameserver 100.100.2.136   
nameserver 100.100.2.138   

  在这种情况下,它告诉DNS解析函数使用192.168.122.1作为DNS服务器。这意味着当你查找像google.com这样的网站时,它会要求192.168.122.1为你查找:

$ nslookup google.com
Server:         100.100.2.136
Address:        100.100.2.136#53

Non-authoritative answer:
Name:   google.com
Address: 172.217.174.206
Name:   google.com
Address: 2404:6800:4005:809::200e 

  如果是在以前,我们可能只需要安全地修改/etc/resolv.conf,这样就结束了。
  然而,像DHCP这样的东西出现了,并且给这种对应增加了一些必要的复杂性。DHCP是一个协议,它让网络上的机器通过漫无目的地对网络上的每个设备大喊大叫,直到有人告诉他们应该使用什么配置。DHCP提供的东西之一是网络首选DNS服务器的IP地址。/etc/resolv.conf的内容需要由某个程序来管理,如果有分歧,不同意见的程序(如DHCP客户端和其他程序)需要竞争DNS控制权。大多数发行版和定制设置开始使用一个搜索不到的叫做resolvconf的程序来帮助实现这一点。
  resolvconf会在/etc/resolv.conf的开头添加一条注释,让你知道resolvconf正在管理它:

# Generated by resolvconf   

  resolvconf是一个松散的管理DNS公约,由多个程序以略微互不兼容的方式实现。最常见的两个是Debian的resolvconfopenresolv
  当一些东西对DNS配置有意见时,需要某种方法在它们之间进行仲裁。Debian的resolvconf采用了让每个人都赢的策略,并安装了一个混合了所有输入的配置。这在除了你实际上希望能够完全覆盖DNS配置(例如,因为一个管理员通过某程序设置了一个强制DNS配置)之外的情况是很好的。在其中某程序认为自己比其他程序更正确,但其他程序对自己的看法也是一样的,Debian resolvconf拒绝选出一个赢家。
  openresolv则允许指定DNS服务器的优先级顺序。此外,它允许程序指定一个“独占”模式,在这种模式下,总是倾向于选择一种选项,而其他选项将被丢弃。如果两个程序想要处于“独占”模式,最后一个提供配置的程序获胜,我们将回到DNS控制权的竞争中。
  之后FreeDesktop的人注意到这种持续不断的DNS控制权之争非常恼人(更不用说配置wifi连接更加恼人) ,他们一起创造了更好的路径并将之称为NetworkManager。它使用称之为D-Bus的协议允许其他程序告诉他该做什么。这是在resolvconf上的显著改进。要使用resolvconf更新/etc/resolv.conf,您需要将所需的配置通过管道传送到resolvconf,并希望您所希望的事情真正发生。NetworkManager的API有个模式且允许内省(自检?),从而使得程序一端更加容易。
  NetworkManager的目标是成为一个守护进程来制定Linux上所有的网络管理的规则。尽管它有自己的方式来管理/etc/resolv.conf,但有时可以将NetworkManager配置为使用resolvconf来管理/etc/resolv.conf。这发生在比想象更多的发行版上。网络管理器很好地隐藏了大量困难的部分,并允许用户使用GUI工具配置网络。
  NetworkManager在很长一段时间内都是DNS配置的标准和最好的选择(到今天仍有些发行版更喜欢它) ,但是随着事情变得越来越复杂而需要一些功能更强大的东西。systemd项目创建了一个名为systemd-resolved的解决方案,允许管理员对每个网络接口上如何解析DNS进行更多的控制。以下是一台Linux机器的解析状态:

$ resolvectl status
Global
           Protocols: +LLMNR +mDNS -DNSOverTLS DNSSEC=no/unsupported
    resolv.conf mode: stub
  Current DNS Server: 100.100.100.100
         DNS Servers: 100.100.100.100 8.8.8.8 1.1.1.1
Fallback DNS Servers: 100.100.100.100 8.8.8.8 1.1.1.1
          DNS Domain: akua.xeserv.us christine.website.beta.tailscale.net

Link 2 (enp5s0)
Current Scopes: LLMNR/IPv4 LLMNR/IPv6
     Protocols: -DefaultRoute +LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported

Link 9 (tailscale0)
Current Scopes: LLMNR/IPv4 LLMNR/IPv6
     Protocols: -DefaultRoute +LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported

  systemd-resolved允许使用DNS over TLS,这是超出本文讨论中心的内容。不过systemd-resolved也允许第三方程序通过其D-Bus API(并非和 NetworkManager同样的API)可靠地配置它。
  当然,这里假设我们将DNS视为一个全球一致的名称空间,就像DNS最初被发明时的设想一样,但事实并非总是如此。
  一些网络或组织拥有处理无法通过互联网解析名称的自己的私有DNS服务器。这让事情变得更加复杂,由于缺乏更好的术语,可将这种设置称为“分裂DNS”。
  IP流量使用路由表在其他机器之间进行路由。这个路由表有一个网络和如何使用它们的说明列表。要正确处理“分裂DNS”设置,您需要一个DNS的路由表,按子域而非IP地址拆分。这就是Windows、macOS和Linux(使用systemd-resolved)处理这类配置的方式。例如,您可以有一个DNS路由表看起来像下面这样:

  •   如果域名以.akua结尾,查询10.77.2.2
  •   如果域名以.local结尾,查询Bonjour
  •   其他情况查询1.1.1.18.8.8.8

  这些设置比想象的更加普遍,几乎每个有Mac的家庭都在使用。这可以让你通过computername.local自动发现域计算机名的IP地址。大多数企业VPN也希望通过此将其内部服务(如git、数据库或IRC服务器)解析到VPN之后的IP地址。这可以防止将请求泄漏给公共DNS服务,而Linux(即在没有运行systemd-resolved时)缺乏这种开箱即用的支持一直是一个重大限制。
  /etc/resolv.conf不支持基于域名的DNS路由,因此第三方应用在最基本的配置中,可能需要守护进程中的过程中解析器内实现路由,并告诉操作系统将其所有DNS流量发送到其指定的DNS(如100.100.100.100)。这些流量通过机器上程序的进程在本地处理,并让基于resolv.conf的系统拥有“分裂DNS”。这仍然不得不偶尔竞争DNS控制权,这取决于试图编辑/etc/resolv.conf的其他程序。resolvconf是类似,可能在配置竞争上要少一些。
  然后,NetworkManager能够控制/etc/resolv.confresolvconf,可能还有一个名为dnsmasq的DNS服务器。唯一允许“分裂DNS”的是dnsmasq模式。这意味着第三方程序需要关心NetworkManager处于哪种模式,可能使用这段代码来实现这一点。这有一些额外的代码来处理应该使用NetworkManager的情况,但是它不能响应Ping(感谢$DEITY,标准的D-Bus方式是让每个对象实现一个“Ping”方法) ,这种情况下再次陷入困境。
  这一切的一个主要困难是Linux系统上的名称解析非常糟糕,这些方法中的每一个都会导致略微不同的行为。如果我们为go.akua做一个解析会发生什么?它会进入公共互联网的解析器吗?它会转到正确的“分裂DNS”吗?它会因为某种原因被送到Tor吗?它会被发送到你当地咖啡店的公共wifi热点上潜在的危险DNS服务器上吗?它会通过UDP,TCP或者DNS over HTTPS发送吗?我们不知道。这些东西没有被记录下来,因此你需要想尽办法来了解它做了什么。另外,glibc和musl的行为也有不同。
  该如何正确做的一个例子是systemd-resolved,它可以原生地做到现代“分裂DNS”VPN的一切,因此理论上不需要额外的工作。systemd团队细心地写下了他们所做的事情,并且明确地表明了你应该如何摆弄来得到你想要的东西,这是基础设施项目应该努力拥有的那类文档。
  现在,如果需要在Linux上提供一个DNS服务器,并且必须弄清楚应该如何配置系统的解析器,按照下面这样做:

  首先,检查/etc/resolv.conf是否存在,如果没有则可以覆盖。

  如果存在,那么需要检查文件的所有者(通过检查文件顶部的magic字段)例如:

# Generated by resolvconf
# This is /run/systemd/resolve/stub-resolv.conf managed by man:systemd-resolved(8).
# Do not edit.
# Generated by NetworkManager

  这将告诉我们哪个服务在管理/etc/resolv.conf文件,如果找不到任何所有者则需要替换掉/etc/resolv.conf并祈祷。
  如果resolvconf正在使用,那么你也应该使用,前提是resolvconf二进制文件在$PATH中:

  如果这个配置看起来属于NetworkManager,那么需要检查NetworkManager是否可以通过D-Bus访问,如果可以那么使用它,否则,需要重新覆盖resolv.conf。
  NetworkManager还给resolvconf的路径添加了一个问题: 如果resolvconf生成的配置来自NetworkManager,应当尝试使用NetworkManager而不是resolvconf。因此,需做一个额外的检测,看看是否resolvconf是由NetworkManager提供的,如果是则切换到NetworkManager。
  如果resolvconf似乎是由NetworkManager提供的,但是无法与NetworkManager通信,就应该退回到使用resolvconf。

  如果使用systemd-resolved,事情应该进行顺利… 但有一个问题,直到最近为止NetworkManager对systemd-resolved的配置有点不正确,从而使得如果自行与systemd-resolved进行对话不可能覆盖默认的解析程序。这点是在2020年12月通过NetworkManager 1.26.6修复的(相关的错误报告)。
  因此,如果正在使用systemd-resolved,需要检查NetworkManager是否也存在以及它是否将其配置推送到systemd-resolved中。如果是则必须使用NetworkManager来配置DNS,即使它的功能比systemd-resolved稍少一些。

  上面这样设置将允许以一致的方式在Linux系统上配置DNS。此外还需要为服务所需的DNS路由位实现一个“polyfill”,以应对每种没有路由感知DNS配置的情况(上图中的大多数情况)。
  如果你决定将来要提供一些新的DNS配置管理服务,请确保它有文档记录包含它与图中其余部分的相互作用。
  对于Linux发行版的维护者而言,可能会想知道应该给用户带来这些纷乱中的哪一部分。建议使用systemd-resolved,而如果需要用户友好的网络配置,则使用最新版本的NetworkManager(1.26.6或更好)。这将提供发行版最先进的DNS能力,并使网络软件的实现者更高兴。使用这种设置的DNS配置图像如下:

* 注:本文译自Tailscale的Blog,并在其基础上有所删改。文本的全部最终权利归原作者所有。