Split DNS

Dec 2, 2022



通过TUN/TAP接管主机流量的各种应用中不可避免的都需要接管DNS的解析功能,如果需要同时运行多个类似的应用,DNS的管理会变得很麻烦,这时就需要实现dns split功能。在了解如何实现split dns功能前,先得搞清楚Linux系统中是如何管理dns服务的,比较旧的系统中管理系统可以称为传统方案,现在的方案都是基于systemd-resolved的。

systemd-resolved


systemd-resolved算是Linux各种发行版系统中现代的dns解析系统,解决了很多传统dns方案的弊端。

nsswitch(Name Service Switch)


glibc在执行域名解析的时候会加载/etc/nsswitch.conf文件, 在ubuntu22.04中,nsswitch.conf文件中有这么一行配置:

    hosts: files mdns4_minimal [NOTFOUND=return] dns

上面配置的意思就是,在解析域名时,先查看nss-files, 也就是查看/etc/hosts文件是否存在hardcoded的域名映射项, 如果没有,则触发mdns4_minimal模块进行mDNS解析。

[NOTFOUND=return]的意思是如果mDNS解析失败,则解析流程立即结束返回,不再尝试后面的模块,也就是不再尝试后面的nss-dns模块。这是为了防止将xxx.local这样的查询送到public dns server去解析。当然只有.lcoal的域名才会触发mDNS解析。

最后这个dns模块是主角,在非systemd-resolved之前的系统里面,它代表的是nss-dns模块,而在systemd-resolved系统里面,nss-resolve模块替代了nss-dns模块主角的位置。Fedora 33和ubunut16.10之后已经默认是systemd-resolved模式了,但是为了保持兼容性,nss-dns模块需要加载的配置文件/etc/resolv.conf在ubuntu中依然存在,不过已经是一个指向/etc/run/resolvconf/resolv.conf的软连接。Fedora 33的/etc/nsswitch.conf文件内容如下:

    hosts: files mdns4_minimal [NOTFOUND=return] resolve [!UNAVAIL=return]  dns

上面最大的变化就是添加了[!UNAVAIL=return],nss-resolve模块通过varlink API或者D-Bus API调用systemd-resolved去解析域名,一旦systemd-resolved开始运行,glibc中的域名解析功能就会退出,[!UNAVAIL=return]就是表示,只要systemd-resolved工作正常,就不会再启动glibc去启动后面的nss-dns模块。

传统的dns方案中,/etc/resolv.conf文件内容大致如下:

    # Generated by NetworkManager
    nameserver 192.168.122.1

可以看出NetworkManager直接接管着nss-dns,这个配置文件的意思就是,所有的DNS请求都会发送到本地的router, 配置文件里面的地址更新来自于NetworkManager和Router直接的DHCP交互。


传统DNS解析的问题


由于NetworkManager直接接管了nss-dns的配置文件,在存在多个类似VPN的应用时,你只能通过NetworkManager中的checkbox去切换解析DNS请求的优先级,导致无法让特定的DNS请求发往它应该去的service。


systemd-resolved中的新方案


    nameserver 127.0.0.53

在新的方案下,/etc/resolv.conf所链接的文件中默认只有上面这一行,这个127.0.0.53就是systemd-resolved的local stub responder。

不过,虽然ubuntu在16.10就开始使用systemd-resolved了,但是ubuntu并没有从nss-dns切换到nss-resolve,所以在ubuntu中,还是glibc通过nss-dns模块将dns请求发送给127.0.0.53:53, 而不是之前的又DHCP获取到的本地dns server的地址。


Split DNS with systemd-resolved

DNS routing domains


routing domains就是配置的domain pattern会被systemd-resolved拿来和dns请求中的domain进行匹配,如果符合pattern,那么该dns请求就会发往配置了该routing domains的网口(interface)。

pattern 匹配意义 示例
example.com == example.com dns请求中name=example.com的包命中
~example.com like example.com dns请求中bname=www.example.com的命中
~ any 所有dns请求都命中


DNS search domains


当dns请求中的name不含有.时,这个请求会被append上所配置的search domain.

在systemd-resolved中,只要配置的routing domains中不含有~, 这个routing domain都会被视为search domain

routing domain treated as search domain
example.com yes
~example.com no
~ no



resolvectl


使用resolvectl可以查看和设置系统的dns和domain配置,

    $ resolvectl domain
    Global:
    Link 4 (wlp4s0): ~.
    Link 18 (hub0): 
    Link 26 (tun0): redhat.com


    $ resolvectl dns
    Global:
    Link 4 (wlp4s0): 192.168.1.1 8.8.4.4 8.8.8.8
    Link 18 (hub0):
    Link 26 (tun0): 10.45.248.15 10.38.5.26

上面第一行都是Global:,但是大部分情况下我们都会by interface的去设置规则,这个global其实没啥作用,只起到一个fallback的效果。

假设我们的一个内网穿透代理应用开启的tun口为tun0,我们要访问的内网服务为internal.example.service,负责解析这个域名的server地址为100.64.0.1,那么我们可以通过systemd-resolved中的工具resolvectl来设置dns和domain,实现我们的需求:

    $resolvectl dns tun0 100.64.0.1
    $resolvectl domain tun0 ~internal.example.service

上面resolvectl dns tun0 100.64.0.1的作用就是将tun0网口的dns server address 设置为100.64.0.1, 然后resolvectl domain tun0 ~internal.example.service是将tun0网口的routing domain设置为~internal.example.service, 因为含有~, 所以search domain并没有被同时设置。

这样当我们在浏览器中访问http://www.internal.example.service 时, dns请求的dst ip 将会是100.64.0.1


Differences with nss-dns


通过上面的介绍可以发现,在使用systemd-resolved后,dns的配置非常灵活,而且可以精确到每个interface去设置,这样像dns split这样的功能很容易实现。 那传统的nss-dns是怎样的呢?

传统的nss-dns方案下,/etc/resolv.conf文件中列出了所有的name servers,就相当于只有Global:这一行一样,所有的dns请求都先送给list中第一个name server,如果第一个server没有respond,那就再发给后面的name server。