Привет, Хабр! Меня зовут Анатолий Кохан, я — DevOps-инженер в К2Тех.
Когда мы вводим в браузере имя сервера или доменное имя сайта, выполняем ping или запускаем любое удаленное приложение, операционная система должна преобразовать указанные имена в IP-адреса. Этот процесс называется разрешением доменного имени. На первый взгляд он может показаться весьма прозрачным, однако за ним скрывается многослойный механизм.
Данная статья — начало серии, посвященной низкоуровневой архитектуре разрешения имен. Поговорим о том, как устроен этот процесс в Linux на уровне ядра, различных библиотек C и системных вызовов.
---
Многие знают, что процесс разрешения имен в Linux — это не просто «вызов DNS», а цепочка из библиотек, конфигурационных записей и вызовов, зависящих от реализации конкретного приложения, используемых типов библиотек и системных настроек.
Однако у инженеров остаются вопросы. Например, требуется ли перезагрузка приложения после того, как изменился адрес DNS-сервера? Кроме того, чтобы диагностировать ошибки, таймауты, иные проблемы с разрешением имен в приложении и в системе, важно понимать, как работает вся эта цепочка — от getaddrinfo() до resolv.conf. В этой части постараемся разобрать все по слоям и собрать некую фундаментальную базу в кратком и доступном виде.
Вершина айсберга
Почти все современные приложения в Linux, от curl до systemd, используют функцию getaddrinfo() из стандартной библиотеки C (glibc или musl). Именно она выполняет основную работу по переводу доменного имени в IP-адрес (A, AAAA-записи) в зависимости от настроек и запроса.
При этом она не только выполняет DNS-запросы, но и обрабатывает другие типы данных, такие как имена сервисов, например, преобразует имя сетевого сервиса “http” в порт 80, используя /etc/services. Это делает ее универсальным инструментом для сетевых приложений.
Функция 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 (Name Service Switch).
NSS
NSS реализован на основе подгружаемых модулей — динамических библиотек, соответствующих API glibc, таких как libnss_dns.so, libnss_files.so, libnss_myhostname.so и других. Они функционируют как плагины и подгружаются библиотекой glibc во время выполнения, отвечая за конкретные методы разрешения IP-адресов. Порядок и набор источников, используемых для разрешения имен, задается в конфигурационном файле /etc/nsswitch.conf.
Пример содержания nsswitch.conf:
# /etc/nsswitch.conf
passwd: files systemd
group: files systemd
shadow: files
gshadow: files
hosts: files dns myhostname
networks: files
protocols: db files
services: db files
ethers: db files
rpc: db files
netgroup: nis
Например, строка в модулях с содержанием ``hosts: files dns
`` говорит, что сначала ищется соответствие в локальном файле /etc/hosts, и если модуль files возвращает результат, то последующие модули, такие как dns (делающий DNS-запрос) не будут вызваны.
Соответственно, если в nsswitch.conf строка hosts не включает упоминание модуля dns, то конфигурационный файл resolv.conf, содержащий настройки обращения к DNS-источникам, проигнорируется, и DNS-запрос не будет сформирован.
Также в NSS могут задействоваться модули mdns (для Zeroconf/Avahi), nis (в старых системах) и myhostname.
Модуль myhostname является частью systemd и используется для разрешения локального имени хоста. Он не всегда присутствует в минималистичных системах, таких как Alpine Linux.
Библиотеки
Нижеперечисленные библиотеки экосистемы Linux являются ключевыми и предоставляют приложениям определенный набор функций, включая разрешение доменных имен.
Glibc — наиболее распространённая реализация стандартной библиотеки языка C, реализует высокоуровневые функции, такие как getaddrinfo(). Она взаимодействует с NSS (Name Service Switch) для определения источников разрешения имён (например, /etc/hosts, DNS) и использует библиотеку libresolv для выполнения DNS-запросов.
Glibc может использовать системные вызовы, такие как sendto и recvfrom для отправки и получения DNS-запросов по UDP или TCP. Широко распространена в большинстве дистрибутивов Linux (Ubuntu, Debian, Fedora и др.)
Musl — альтернативная стандартная библиотека C, разработанная с упором на минимализм, производительность и совместимость со стандартами POSIX. Она используется в легковесных дистрибутивах, таких как Alpine Linux.
Musl реализует разрешение доменных имён напрямую, без использования NSS, самостоятельно читает /etc/hosts и /etc/resolv.conf и отправляет DNS-запросы, не используя внешние библиотеки вроде libresolv. Однако musl имеет ограничения в поддержке некоторых параметров resolv.conf, таких как rotate или сложные search.
Libresolv.so — является частью glibc, реализующая низкоуровневую работу с DNS, выполняя такие запросы, как res_query() и res_send(), но может использоваться независимо в некоторых приложениях вроде nslookup (что позволяет выполнять DNS-запросы напрямую, минуя стандартные механизмы разрешения имен).
Libresolv используется glibc для выполнения DNS-запросов, когда NSS указывает, что нужно обратиться к DNS. Она читает /etc/resolv.conf, формирует DNS-пакеты и отправляет их на указанные серверы по UDP или TCP.
Стоит отметить, что некоторые приложения, например, написанные на Go, могут полностью обходить glibc/musl и использовать собственные DNS-резолверы.
Как обрабатывается resolv.conf
Файл /etc/resolv.conf содержит основные настройки клиента DNS, а именно: список серверов, параметры, search-домены. Например:
nameserver 192.168.1.1
search dev.local
options timeout:2 attempts:3
Glibc и libresolv парсят его вручную при необходимости.
Важные моменты и ограничения:
- опции вроде rotate, ndots, timeout и attempts влияют на поведение запроса;
- опция rotate используется для циклического выбора серверов из списка nameserver, но она не поддерживается в musl;
- search используется для автодополнения, например, если имя db01 не является FQDN, к нему будут по очереди подставляться домены из директивы search.
Важно отметить, что файл resolv.conf может быть динамически изменён DHCP-клиентом, NetworkManager или утилитой resolvconf, что может вызывать путаницу при решении проблем с DNS. Об этом мы поговорим в одной из следующих частей.
Что делает res_query()
Это функция из libresolv, вызываемая внутренне в процессе разрешения имени. Она формирует DNS-пакет вручную и отправляет его на указанные в resolv.conf DNS-серверы. Её используют утилиты вроде nslookup, а также некоторые программы, которые обходят getaddrinfo().
Функция отправляет DNS-запросы с помощью res_send() по UDP, а при необходимости, например, при получении ответов, превышающих 512 байт, переключается на TCP.
Важно: при использовании 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. если первым указан модуль files, имя ищется в файле /etc/hosts;
4. если включён модуль dns, NSS вызывает libnss_dns.so, которая обращается к функциям из libresolv;
5. libresolv формирует DNS-запрос через res_query() и отправляет его с помощью res_send() на указанные в resolv.conf адреса DNS-серверов, затем получает и возвращает IP-адрес.

Важно: если имя найдено на одном из шагов, например, в hosts, последующие источники не используются.
В минималистичных системах, таких как Alpine Linux с musl, порядок может отличаться, так как musl не использует NSS и реализует DNS-запросы напрямую, читая /etc/hosts и resolv.conf самостоятельно.
Некоторые приложения и языки (например, Go, Java, Node.js) могут использовать собственные DNS-резолверы, полностью игнорируя системные настройки.
Для примера проанализируем работу утилиты curl.
Команда:
strace -f -e trace=network curl -s download.astralinux.ru > /dev/null
Вывод strace:
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 (Name Service Cache Daemon)
connect(..., "/var/run/nscd/socket", ...) = -1 ENOENT
Это означает, что glibc сначала пытается использовать кеш имён из NSCD, если он запущен. В системе его нет, и запрос идёт дальше.
2. Вызов socket() и connect() к DNS-серверу
socket(AF_INET, SOCK_DGRAM|..., IPPROTO_IP) = 7
connect(7, ..., sin_addr=inet_addr("172.24.31.107")...)
Здесь создаётся UDP-сокет для обращения к DNS-серверу, указанному в /etc/resolv.conf.
3. Вызов sendmmsg() — отправка DNS-запросов
sendmmsg(7, [ { "download.astralinux.ru" }, { "download.astralinux.ru" } ], ...)
Здесь отправляются запросы на резолв имени.
4. Ответ от DNS
recvfrom(...) = 56
recvfrom(...) = 114
Теперь IP-адрес известен.
56 - это размер DNS-ответа в байтах, содержащего А-запись (IPv4-адрес)
114 - размер дополнительных данных, например CNAME, или авторитетные серверы в случае рекурсивного запроса.
5. TCP-соединение по IP
connect(5, ..., sin_addr=inet_addr("130.193.50.59"))
Здесь уже сам curl устанавливает TCP-соединение по IP-адресу, который ему вернула getaddrinfo().
Таким образом, когда мы вызываем curl, мы не видим DNS-запросов напрямую — их делает библиотека glibc внутри вызова getaddrinfo(). Но strace позволяет увидеть косвенные признаки:
Среди вызовов будет попытка подключиться к nscd, вызов connect() к DNS-серверу, отправка UDP-пакета через sendmmsg(), а затем — стандартное TCP-соединение по IP:
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 результаты могут кэшироваться, что влияет на производительность и актуальность данных.
Краткое резюме и ключевые моменты
DNS-запрос в Linux — это не обязательно запрос к DNS-серверу. Цепочка обращений может включать hosts, NSS, glibc и другие источники.
NSS и nsswitch.conf определяют порядок и источники разрешения имён.
glibc использует NSS и может кэшировать результаты; musl реализует DNS-резолвинг напрямую с ограниченной поддержкой опций resolv.conf.
Resolv.conf управляет настройками резолвера, но может быть изменен динамически.
Getaddrinfo() — основной интерфейс для разрешения имен, обрабатывает как DNS, так и другие источники.
В разных языках программирования (Go, Java, Python с dns.resolver, Node.js) могут использоваться собственные механизмы DNS-запросов.
В следующей части дополним картину общим представлением того, как устроено кэширование DNS записей — ключевой механизм, который напрямую влияет на производительность, надежность и поведение приложений при смене IP-адресов.
Комментарии (5)
Andrei9385
18.06.2025 11:28Здравствуйте. А вот допустим есть DNS сервер на Linux, BIND.
У него прописаны DNS гугла и яндекса.
Как я могу узнать, с помощью какого адреса я делаю разрешение в данный момент ?
rearranged Автор
18.06.2025 11:28Если коротко, то можно включить логирование на bind, или через tcpdump посмотреть. Более подробно планировал про дебаг описать в третьей части серии, поэтому не хотелось бы сразу спойлерить...
Andrei9385
18.06.2025 11:28Спасибо Вам большое. Обождем. Я смотрел в tcpdump, но не нашел, буду ждать следующую серию Ваших статей. Благодарю !
eaa
18.06.2025 11:28Прошли старые добрые времена, когда все было понятно в /etc/resolv.conf, теперь там отправляют в nameserver 127.0.0.53 и пишут "DO NOT EDIT THIS FILE BY HAND"
И про это ни слова в статье.
lrrr11
в Go вообще черная магия. Там есть 2 ресолвера - один написанный на Go, который парсит
/etc/resolv.conf
, и другой, который вызываетgetaddrinfo
через Cgo. Выбирается один из этих двух примерно так:как говорится, удачной отладки))