通过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。