DNS到底怎么运作的
Tags: dns, ubuntu, resolve
Published: 2024-05-29 07:09:07

## 背景
[上文](https://dev.to/shouhua_57/ubunture-dian-wen-ti-lpf)讲到Ubuntu中热点遇到的一些问题，涉及到了Ubuntu环境中网络管理的各个组件，总是很模糊。本文主要通过梳理DNS管理来整理Ubuntu中网络管理组件分工，以及DNS管理细节，本文不会涉及DNS协议细节。

## 环境

    system：Ubuntu24.04
    libc: glibc2.39

## Ubuntu24.04网络管理

Linux环境中有多个网络管理组件，比如`Network Manager`, `systemd-networkd`等，不同组件有不同的上下文概念和配置方式，这对于网络管理和配置来说很有挑战性。后面推出了[netplan](https://netplan.io)，作为网络管理抽象层，使用YAML文件配置，使用上面列到的组建作为渲染器，一份配置文件适配多种渲染器，目前支持两种渲染器：`Network Manager`和`systemd-networkd`。  

Ubuntu上面默认使用前者作为网络管理工具，可以查看`man 5 NetworkManager.conf`查看相关配置，配置文件位于`/etc/NetworkManager/NetworkManager.conf`。

网络有很多部分，比如无线，有线，DNS, 蓝牙等，NetworkManager使用不同的软件管理这些组件。无线部分使用`wpa_supplicant`(见`wifi.backend`), dhcp client端也有多种选择，默认使用internal。

其中DNS配置很有意思，根据`manpage`文档，DNS管理使用`systemd-resolved`(`/etc/resolv.conf`文件是软连接至`/run/systemd/resolve/stub-resolv.conf`), 也可以设置使用`dnsmasq`。

## Ubuntu24.04 DNS管理

[DNS基础知识 Cloudflare](https://www.cloudflare.com/learning/dns/what-is-dns/)<br>
[DNS基础知识 ruanyifeng](http://www.ruanyifeng.com/blog/2016/06/dns.html)<br>


`resolvectl`作为`systemd-resolved`的命令行管理工具管理包括DNS等。

`systemd-resolved`会在本地53端口起个DNS服务，并且将`nameserver 127.0.0.53`写入`/etc/resolv.conf`文件，那其他软件DNS查找这个文件，都是访问本地53端口服务(这个成为`stub resolver`)，但是真正的DNS服务server会藏在`systemd-resolved`的相关配置文件中，可以使用如下命令查看：

```bash
resolvectl status
```
它会列出所有网络设备的resolve信息，比如无线网卡的信息
```
    Link 3 (wlp4s0)
        Current Scopes: DNS
             Protocols: +DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
    Current DNS Server: 192.168.0.1
           DNS Servers: 192.168.0.1
            DNS Domain: lan
```
上述的`192.168.0.1`作为DNS server一般是`DHCP`分配的，局域网内部的DNS server, 会去请求ISP的DNS server(DNS的**迭代模式(`Iterate`)**), 最后会进入**递归模式**，不断从`ROOT(.)`, `TLD nameserver(Top Level Domain)(.com)`, `Authoritative nameserver(baidu.com)`获取DNS信息，返回信息有各种所谓的`Record`(上述是全流程，当然可能存在缓存就不用走这些流程了，不然每次请求都消耗很多时间，另外服务器负载也太大了), 下面是常见的[Record](https://www.cloudflare.com/learning/dns/dns-records/dns-txt-record/)：
```
    A - IPV4
    AAAA - IPV6
    NS - Authoritative Name Server
    CNAME - 别名，比如www.baidu.com -> www.shifen.com
    MX - Mail
    TXT - Human readable document
    PTR - reverse DNS lookup, 由ip查域名
```

### 查看systemd-resolved的DNS缓存
使用`sudo resolvectl statistic`可以查看缓存情况，比如缓存个数，命中个数等。如果想查看缓存内容，使用`sudo pkill -USR1 systemd-resolve`, 然后在`syslog`中查看 `sudo journalctl -u systemd-resolved > ~/resolved.txt`。**注意上面打印的数据是unique的数据，记录中可能有多个相同域名指向多个IP**。

### 清空systemd-resolved的DNS缓存
清空所有cache

```bash
sudo pkill -USR2 systemd-resolve
# OR
sudo resolvectl flush-caches
sudo resolvectl statistic
```

### glibc的DNS服务nsswitch和ncsd
为什么提到了glibc的DNS服务呢？因为在程序中使用比如函数`getaddrinfo`进行域名解析时候就使用了glibc提供的域名解析服务。下面梳理glibc的DNS相关流程。  
glibc提供了各类名称解析服务，比如用户名->`USER ID`, 组名->`GROUP ID`, 域名->`IP`等，其中使用的模块是`NSS(Name Service Switch)`, **这个不要跟Firefox的NSS(Network Security Services)模块弄混淆了**。  
#### NSSwitch
可以使用`getent database key`从数据库中获取NSS支持类型的存储数据, 比如
```bash
getent ahosts # get A record host
getent group # get group resolve record
man 5 nss
man 5 nsswitch.conf
```
`getent ahost`先使用缓存没有就使用getaddrinfo开始网络请求，详情见`man getent`。  
`nsswitch`的配置文件见`/etc/nsswitch.conf`，我们关心DNS相关的是`hosts`
```bash
hosts:          files mdns4_minimal [NOTFOUND=return] dns
```
其中`hosts`值规定使用`DNS`查询的顺序，`files`代表`/etc/hosts`; `mdns4\_minimal`使用系统提供的`multicast DNS`解析服务，如果有这个服务，调用成功但是没有我们要的结果，就返回，如果没有这个服务，就使用系统提供的DNS服务查询(`/etc/resolv.conf`)。
使用`nsswitch`查询`DNS`整个过程，可以通过`strace`查看到上述描述细节
```bash
sudo strace -e trace=open,openat,connect -f ping -c1 www.baidu.com
     openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
     openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libcap.so.2", O_RDONLY|O_CLOEXEC) = 3
     openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libidn2.so.0", O_RDONLY|O_CLOEXEC) = 3
     openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
     openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libunistring.so.5", O_RDONLY|O_CLOEXEC) = 3
     openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
     openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache", O_RDONLY|O_CLOEXEC) = 5
# nscd缓存
     connect(5, {sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"}, 110) = -1 ENOENT (没有那个文件或目录)
     connect(5, {sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"}, 110) = -1 ENOENT (没有那个文件或目录)
# 使用了getaddrinfo, 所以会调用nsswitch查询dns
     openat(AT_FDCWD, "/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 5
# hosts中配置files第一顺序
     openat(AT_FDCWD, "/etc/host.conf", O_RDONLY|O_CLOEXEC) = 5
     openat(AT_FDCWD, "/etc/resolv.conf", O_RDONLY|O_CLOEXEC) = 5
     openat(AT_FDCWD, "/etc/hosts", O_RDONLY|O_CLOEXEC) = 5
     openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 5
# 使用multicast DNS本地局域网所有example.local机器咨询DNS, 没有这个机器服务，下一个dns系统查询
     openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libnss_mdns4_minimal.so.2", O_RDONLY|O_CLOEXEC) = 5
# hosts配置中的dns, 使用系统提供的dns服务, 即systemd-resolved
     connect(5, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.0.53")}, 16) = 0
     openat(AT_FDCWD, "/etc/gai.conf", O_RDONLY|O_CLOEXEC) = 5
     ...
     connect(5, {sa_family=AF_INET, sin_port=htons(1025), sin_addr=inet_addr("183.2.172.42")}, 16) = 0
     PING www.a.shifen.com (183.2.172.42) 56(84) bytes of data.
     openat(AT_FDCWD, "/etc/hosts", O_RDONLY|O_CLOEXEC) = 5
     connect(5, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.0.53")}, 16) = 0
     64 bytes from 183.2.172.42: icmp_seq=1 ttl=51 time=25.3 ms

     --- www.a.shifen.com ping statistics ---
     1 packets transmitted, 1 received, 0% packet loss, time 0ms
     rtt min/avg/max/mdev = 25.305/25.305/25.305/0.000 ms
     +++ exited with 0 +++
```
其中可以修改`/etc/nsswitch.conf`中的`hosts: file dns`就不会有`multicast DNS`请求了, **修改是自动刷新的**。  
**NOTICE：上述`strace`监听openat系统调用，open可能已经没有用了**

#### ncsd
其中`nscd(Name Service Cache Daemon)`是全局各种解析服务的缓存，数据库中存储了各种解析类型映射的缓存。[nscd manpage](https://man7.org/linux/man-pages/man8/nscd.8.html)<br>  
网上看到这篇[文章](https://cloud.tencent.com/developer/article/2242697)总结的不错。
注意，/etc/nscd.conf文件注释必须是首个字符为#，不然会服务可能启动失败。
主要提下调试，有两种方式，一种是使用日志文件，一种是在终端打印日志查看
1. 使用日志文件
修改`/etc/nscd.conf`中日志相关项, `logfile(/var/log/nscd.log)`和`debug-file(0-5, 可以选择5)`, 然后重启nscd, 就可以查看(`tail -f /var/log/nscd.log`), 当`ping`或者`getent hosts`就会更新日志。
3. 终端查看
先暂停`sudo systemctl stop nscd`, 然后`sudo nscd -g`, 就会在终端打印日志了，但是也要设置日志等级。

#### 总结
使用`getent`, `ping`这些命令都会走glibc提供的DNS服务，底层使用`getaddrinfo`函数; 其他命令如`host(man 1 host)`, `dig`, `nslookup`走的是全局DNS解析服务，但是也可以自己配置DNS服务器直接网络请求。查看manpage, 会发现都是`BIND 9`提供的命令，在找`BIND 9`你就会发现他们为什么不走glibc的了 :)

## 编程语言DNS服务
目前有很多库提供DNS服务，比如[libevent](http://www.wangafu.net/~nickm/libevent-book/Ref9_dns.html), [c-ares](https://github.com/c-ares/c-ares)。  
很多[语言](https://juejin.cn/post/7264949719276437538)如果底层使用使用glibc的函数也是走NSS那套，比如python，java等，但是也有的语言提供独立的DNS服务，比如自举后的golang。   
NodeJS底层使用`c-ares`提供DNS服务。
libcurl可以使用自己提供DNS server,但是需要编译时候添加c-ares库; 默认走glibc那套。

### Nodejs
[Nodejs文档](https://nodejs.org/docs/latest/api/dns.html#implementation-considerations)真心不错，最后说明了两种方式的特点和注意点。
默认情况(使用dns.lookup())，使用getaddrinfo走系统那套查询方式; 也可以使用 `dns.resolve()`, `dns.resolve*()`, and `dns.reverse()`，底层使用c-ares,直接网络请求DNS服务器获取解析结果。

## 小结
又整理了一篇无用知识，发现`strace`真是个好工具，解决问题都要想到她。