当前位置: 首页 > news >正文

Linux 中的 DNS 工作原理(一):​​从 getaddrinfo 到 resolv.conf

大家好!我是大聪明-PLUS

当我们在浏览器中输入服务器名称或网站域名、执行 ping 命令或启动任何远程应用程序时,操作系统必须将这些名称转换为 IP 地址。这个过程称为域名解析。乍一看,它可能很简单,但其背后却隐藏着一个多层机制。

本文是系列文章的第一篇,专门探讨名称解析的底层架构。我们将讨论 Linux 内核中名称解析过程的构成、各种 C 库以及系统调用。

许多人都知道,Linux 中的名称解析过程不仅仅是一次 DNS 调用,而是一系列库、配置条目和调用,这些库、配置条目和调用取决于特定应用程序的实现、所使用的库类型和系统设置。 

然而,工程师们仍然有一些疑问。例如,DNS 服务器地址更改后,应用程序是否需要重启?此外,为了诊断应用程序和系统中的错误、超时和其他名称解析问题,了解从 getaddrinfo() 到 resolv.conf 的整个流程至关重要。在本节中,我们将尝试逐层分解,并以简洁易懂的格式提供基础框架。

冰山一角

几乎所有现代 Linux 应用程序(从 curl 到 systemd)都使用标准 C 库(glibc 或 musl)中的getaddrinfo()函数。该函数的主要功能是根据设置和请求将域名转换为 IP 地址(A、AAAA 记录)。

它不仅执行 DNS 查询,还能处理其他类型的数据,例如服务名称。例如,它使用 /etc/services 将网络服务名称“http”转换为端口 80。这使得它成为一个多功能的网络应用工具。

getaddrinfo() 函数返回一个 addrinfo 结构列表,每个结构包含一个 IP 地址、套接字类型、协议和其他参数。这允许应用程序选择最合适的地址进行连接。

伪代码中使用 getaddrinfo() 的示例:

```
struct addrinfo hints, *res;
zero_memory(hints);
hints.ai_family = ANY_FAMILY;   
hints.ai_socktype = TCP;err = getaddrinfo("example.com", "http", hints, &res);
if (err == 0) {for each addr in res:use(addr)freeaddrinfo(res);
} else {print(gai_strerror(err));
}```

然而,getaddrinfo() 只是冰山一角。为了获取 IP 地址,它会调用系统配置数据中定义的一系列内部机制。其中之一就是 NSS(名称服务切换)。

NSS 是通过可加载模块(符合 glibc API 的动态库,例如 libnss_dns.so、libnss_files.so、libnss_myhostname.so 等)实现的。这些模块以插件的形式运行,由 glibc 库在运行时加载,负责特定的 IP 地址解析方法。用于名称解析的源顺序和集合在 /etc/nsswitch.conf 配置文件中指定。

nsswitch.conf内容示例:

# /etc/nsswitch.confpasswd:         files systemd
group:          files systemd
shadow:         files
gshadow:        fileshosts:          files dns myhostname
networks:       filesprotocols:      db files
services:       db files
ethers:         db files
rpc:            db filesnetgroup:       nis

例如,模块中包含“ hosts: files dns”的一行表示首先在本地 /etc/hosts 文件中查找匹配项,如果文件模块返回结果,则不会调用后续模块(例如 dns(执行 DNS 查找))。

因此,如果 nsswitch.conf 中的 hosts 行未提及 dns 模块,则包含访问 DNS 源设置的 resolv.conf 配置文件将被忽略,并且不会生成 DNS 查询。

NSS 还可以使用 mdns(用于 Zeroconf/Avahi)、nis(在旧系统中)和 myhostname 模块。

myhostname 模块是 systemd 的一部分,用于解析本地主机名。在 Alpine Linux 等极简系统上,它并不总是存在。

以下库是 Linux 生态系统的核心,为应用程序提供一组特定的功能,包括域名解析。

Glibc是 C 标准库最广泛使用的实现,它实现了诸如 getaddrinfo() 之类的高级函数。它与 NSS(名称服务交换机)交互以确定名称解析源(例如 /etc/hosts、DNS),并使用 libresolv 库执行 DNS 查询。

Glibc 可以使用 sendto 和 recvfrom 等系统调用通过 UDP 或 TCP 发送和接收 DNS 查询。它在大多数 Linux 发行版(Ubuntu、Debian、Fedora 等)中被广泛使用。

Musl是一个替代的标准 C 库,其设计考虑了极简主义、性能和 POSIX 兼容性。它用于 Alpine Linux 等轻量级发行版。

Musl 直接实现域名解析,无需使用 NSS。它读取 /etc/hosts 和 /etc/resolv.conf 文件并发送 DNS 查询,无需使用 libresolv 等外部库。然而,musl 对某些 resolv.conf 参数(例如轮换或复杂搜索)的支持有限。

Libresolv.so是 glibc 的一部分,实现低级 DNS 处理,执行诸如 res_query() 和 res_send() 之类的查询,但可以在某些应用程序中独立使用,例如 nslookup(允许直接执行 DNS 查询,绕过标准名称解析机制)。

当 NSS 指定 DNS 访问时,glibc 使用 Libresolv 执行 DNS 查询。它读取 /etc/resolv.conf 文件,生成 DNS 数据包,并通过 UDP 或 TCP 将其发送到指定的服务器。

值得注意的是,某些应用程序(例如用 Go 编写的应用程序)可能会完全绕过 glibc/musl 并使用自己的 DNS 解析器。

如何处理 resolv.conf

/etc/resolv.conf 文件包含基本的 DNS 客户端设置,即服务器列表、参数和搜索域。例如:

nameserver 192.168.1.1
search dev.local
options timeout:2 attempts:3

如果有必要,Glibc 和 libresolv 会手动解析它。

要点和限制:

- rotate、ndots、timeout 和 attempt 等选项会影响请求的行为;

- rotate 选项用于从名称服务器列表中循环服务器,但 musl 不支持该选项;

- 搜索用于自动完成,例如,如果名称 db01 不是 FQDN,则搜索指令中的域将依次替换它。

需要注意的是,DHCP 客户端、NetworkManager 或 resolvconf 实用程序可能会动态修改 resolv.conf 文件,这在排除 DNS 问题时可能会造成混乱。我们将在以后的文章中讨论这个问题。

res_query() 做什么?

这是 libresolv 中的一个函数,在名称解析过程中内部调用。它手动构造一个 DNS 数据包并将其发送到 resolv.conf 中指定的 DNS 服务器。nslookup 等实用程序以及一些绕过 getaddrinfo() 的程序都会用到它。

该函数使用 res_send() 通过 UDP 发送 DNS 查询,并在必要时切换到 TCP,例如在接收到大于 512 字节的响应时。

重要提示:使用 res_query() 时,您不会从 /etc/hosts、NSS 或其他来源获取信息。这是一个纯 DNS 查询。因此,dig 或 nslookup 可能会返回一个结果,而 ping 或 curl 等则会返回完全不同的结果。

Res_query() 被视为已弃用函数,不建议使用。为了更方便、更安全地访问 DNS,最好使用 getaddrinfo() 或 c-ares 或 libdns 等库。

  • c-ares 是一个用于异步 DNS 查询的轻量级库,常用于高负载应用程序(例如 curl 和 Node.js)。

  • libunbound(来自 Unbound 项目)是一个更强大的库,具有 DNSSEC 支持和灵活的查询定制。

请求执行顺序和优先级

以下是Linux 上使用 glibc 和 NSS 的典型名称解析顺序:

1.应用程序调用getaddrinfo();

2. getaddrinfo() 访问NSS系统,并按照nsswitch.conf中指定的顺序进行;

3. 如果首先指定了文件模块,则在/etc/hosts文件中查找名称;

4. 如果启用了 dns 模块,NSS 将调用 libnss_dns.so,后者调用来自 libresolv 的函数;

5. libresolv通过res_query()生成DNS查询,并使用res_send()将其发送到resolv.conf中指定的DNS服务器地址,然后接收并返回IP地址。

Linux 中使用 glibc 进行名称解析的简化方案。此方案演示了基本路径,但也可以使用 NSS 中的其他源。源 (files/dns) 的顺序在 /etc/nsswitch.conf 中配置。现代系统也可能使用 DNS 缓存 (systemd-resolved、nscd)。

重要提示:如果在某个步骤中找到了名称(例如在主机中),则不会使用后续来源。

在带有 musl 的 Alpine Linux 等极简系统上,顺序可能会有所不同,因为 musl 不使用 NSS 而是通过读取 /etc/hosts 和 resolv.conf 本身直接实现 DNS 查询。

一些应用程序和语言(例如Go、Java、Node.js)可能会使用自己的DNS解析器,完全忽略系统设置。

作为示例,让我们分析一下 curl 实用程序的操作。

团队:

strace -f -e trace=network curl -s download.astralinux.ru > /dev/null
socket(AF_INET6, SOCK_DGRAM, IPPROTO_IP) = 3
socketpair(AF_UNIX, SOCK_STREAM, 0, [3, 4]) = 0
socketpair(AF_UNIX, SOCK_STREAM, 0, [5, 6]) = 0
strace: Process 283163 attached
[pid 283163] socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) = 7
[pid 283163] connect(7, {sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"}, 110) = -1 ENOENT (Нет такого файла или каталога)
[pid 283163] socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) = 7
[pid 283163] connect(7, {sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"}, 110) = -1 ENOENT (Нет такого файла или каталога)
[pid 283163] socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 7
[pid 283163] connect(7, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("172.24.31.107")}, 16) = 0
[pid 283163] sendmmsg(7, [{msg_hdr={msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="\250\207\1\0\0\1\0\0\0\0\0\0\10download\nastralinux"..., iov_len=40}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, msg_len=40}, {msg_hdr={msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="\240\215\1\0\0\1\0\0\0\0\0\0\10download\nastralinux"..., iov_len=40}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, msg_len=40}], 2, MSG_NOSIGNAL) = 2
[pid 283163] recvfrom(7, "\250\207\201\200\0\1\0\1\0\0\0\0\10download\nastralinux"..., 2048, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("172.24.31.107")}, [28->16]) = 56
[pid 283163] recvfrom(7, "\240\215\201\200\0\1\0\0\0\1\0\0\10download\nastralinux"..., 65536, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("172.24.31.107")}, [28->16]) = 114
[pid 283163] sendto(6, "\1", 1, MSG_NOSIGNAL, NULL, 0) = 1
[pid 283163] +++ exited with 0 +++
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 5
setsockopt(5, SOL_TCP, TCP_NODELAY, [1], 4) = 0
setsockopt(5, SOL_SOCKET, SO_KEEPALIVE, [1], 4) = 0
setsockopt(5, SOL_TCP, TCP_KEEPIDLE, [60], 4) = 0
setsockopt(5, SOL_TCP, TCP_KEEPINTVL, [60], 4) = 0
connect(5, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("130.193.50.59")}, 16) = -1 EINPROGRESS (Операция выполняется в данный момент)
getsockopt(5, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
getpeername(5, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("130.193.50.59")}, [128->16]) = 0
getsockname(5, {sa_family=AF_INET, sin_port=htons(48488), sin_addr=inet_addr("172.24.31.241")}, [128->16]) = 0
sendto(5, "GET / HTTP/1.1\r\nHost: download.a"..., 86, MSG_NOSIGNAL, NULL, 0) = 86
recvfrom(5, "HTTP/1.1 200 OK\r\nServer: nginx/1"..., 102400, 0, NULL, NULL) = 1617

我们在这条 strace 中看到了什么?

1. 尝试使用 NSCD(名称服务缓存守护进程)

connect(..., "/var/run/nscd/socket", ...) = -1 ENOENT

这意味着,glibc 首先会尝试使用 NSCD 中的名称缓存(如果 NSCD 正在运行)。如果系统中不存在该缓存,则请求将继续进行。

2. 调用socket()和connect()连接到DNS服务器

socket(AF_INET, SOCK_DGRAM|..., IPPROTO_IP) = 7
connect(7, ..., sin_addr=inet_addr("172.24.31.107")...)

这将创建一个 UDP 套接字来联系 /etc/resolv.conf 中指定的 DNS 服务器。

3. 调用 sendmmsg() – 发送 DNS 查询

sendmmsg(7, [ { "download.astralinux.ru" }, { "download.astralinux.ru" } ], ...)

名称解析请求发送到这里。 

4. DNS响应

recvfrom(...) = 56
recvfrom(...) = 114

现在 IP 地址已知了。

56 是包含 A 记录(IPv4 地址)的 DNS 响应的大小(以字节为单位)

114 - 附加数据的大小,例如 CNAME 或递归查询时的权威服务器。 

5. IP 上的 TCP 连接

connect(5, ..., sin_addr=inet_addr("130.193.50.59"))

这里 curl 本身与 getaddrinfo() 返回的 IP 地址建立了 TCP 连接。

因此,当我们调用 curl 时,我们无法直接看到 DNS 查询——它们是由 glibc 库在 getaddrinfo() 调用中执行的。但 strace 允许我们看到间接的迹象:

这些调用将包括尝试连接到 nscd、调用 DNS 服务器的 connect()、通过 sendmmsg() 发送 UDP 数据包,然后通过 IP 连接建立标准 TCP:

connect(7, {AF_INET, 172.24.31.107:53}) = 0
sendmmsg(7, [{ "download.astralinux.ru" }]) = 2
recvfrom(7, ...) = ...
connect(5, {130.193.50.59:80}) = 0

需要注意的是,getaddrinfo() 的行为可能取决于 libc 的实现。例如,在 glibc 中,结果可能会被缓存,这会影响性能和数据新鲜度。

简要总结和要点

  • Linux 中的 DNS 查询不一定是向 DNS 服务器发出的请求。查询链可以包括主机、NSS、glibc 和其他来源。

  • NSS 和 nsswitch.conf 定义名称解析的顺序和来源。

  • glibc 使用 NSS 并可以缓存结果;musl直接实现 DNS 解析,但对 resolv.conf 选项的支持有限。

  • Resolv.conf控制解析器设置,但可以动态更改。

  • Getaddrinfo()是名称解析的主要接口,处理 DNS 和其他来源。

  • 不同的编程语言(Go、Java、带有 dns.resolver 的 Python、Node.js)可能使用自己的 DNS 查询机制。

在下一节中,我们将概述DNS 记录缓存的工作原理——这是一种在 IP 地址发生变化时直接影响应用程序性能、可靠性和行为的关键机制。

http://www.dtcms.com/a/521808.html

相关文章:

  • 自己编程做网站骆诗网站建设
  • 在哪查找网站的建设者中文网站建设
  • python asyncio的各种用法与代码示例
  • 深圳网站营销型建设免费网络电话呼叫系统
  • Linux-基础IO(1)
  • 如何上传网站网站开发价格明细
  • 深圳英文网站制作定西建设厅网站
  • 面向边缘AI视觉系统的低成本硬件方案
  • 医疗网站建设市场网站维护中是怎么回事
  • 网站开发费如何入账课程培训网站建设
  • dw做的网站怎么放到服务器上网站设计应遵循的原则
  • 南宁工程建设网站有哪些网站建设中模板
  • php网站开发用什么工具wordpress 创建分类
  • xml的网站地图织梦制作网站建设7
  • 乌兰察布市建设工程造价网站网站注册域名
  • 北京网站备案要求吗运营实力 网站建设
  • 41.渗透-Kali Linux-工具-Xhydra(爆破攻击)
  • 公众号视频网站开发外国教程网站有哪些
  • seo网站页面f布局免费的网页设计代码模板
  • 制作商城版网站开发手机百度网页版登录入口
  • 搭建网站硬件要求外贸建站 知乎
  • 网站标准字体企业网站开发项目策划书基本框架
  • 从 0 到 1 团队落地仓颉语言:一份可复制的工程化改造与度量驱动实践!
  • 国外域名建站WordPress5分钟建站
  • [Java数据结构与算法] 哈希表(Hash Table)
  • 嘉兴模板建站代理网站广告模板代码
  • 济南网站建设认可搜点网络能容桂做pc端网站
  • 百度手机模板网站软文推广渠道
  • 国外特效网站汕头网站建设制作公司
  • 如何解决 pip install 安装报错 invalid command ‘bdist_wheel’(缺少 wheel)问题