0

Как Linux врет вам про DNS

Предисловие: выложил эту статью на Хабр, но тамошние модераторы с синдромом вахтера решили, что раз я пишу грамотно и структурированно, то я нейросеть. Я человек двуязычный и, возможно, это сказывается, но апломб и бескомпромиссность, с которой модераторы вещают, меня взбесили. Мол, мы тут вообще-то в универах учились, в отличие от вас, и знаем что и как. Уверен, что всегда найдутся те, кто так скажет, но это лишь их мнение. Моя же задача рассказать о проблеме, на мой взгляд любопытной, и показать инструмент.

———

Однажды ты запускаешь dig домен_нейм, видишь исчерпываюший ответ, идешь дальше — а твой процесс резолвит что-то другое. Просто твой процесс и твой dig живут в двух параллельных вселенных, и обе вселенные “резолвинг имени”.

Если ты никогда с этим не сталкивался — либо у тебя мало продакшена за плечами, либо ты просто не заметил. Оно не падает с ошибкой. Оно просто отвечает не то, что ты ожидал, и ты лезешь в дебри, да еще и не в ту сторону.

Я на это наткнулся ровно так, как обычно натыкаются на подобное — не потому что курил мануалы, а потому что два часа дебажил то, что по всем признакам не должно было ломаться. И только на дне этого дебага понял: я вообще не знаю, что происходит между вызовом getaddrinfo() и ответом. Никто, по сути, толком не знает. Все просто верят, что DNS резолвит имена и живут с этим счастливым заблуждением, пока оно их не настигает.

Так что сядем и разберем, что там реально происходит. По пути будет несколько точек, в которых система может соврать, но не со зла, а просто потому что так устроена. И в конце будет маленький инструмент, который я написал, чтобы больше не гадать.

DNS — это не резолвинг. DNS — это один из вариантов

Вот с чего стоит начать разгребать: getaddrinfo() — это не функция “спроси DNS”. Это функция “спроси NSS”, а NSS (Name Service Switch) — это диспетчер, который решает, у кого спрашивать: у файла, у mDNS, у DNS, у LDAP, у самого черта, что тебе кто-то подсунул модулем. DNS там просто один из кандидатов, причем необязательно приоритетный.

Вся эта логика описана в одном скромном файле, который почти никто не читает после того, как система его замутила при установке:

$ cat /etc/nsswitch.conf hosts: files mdns4_minimal [NOTFOUND=return] dns

Вроде все просто, сначала спроси /etc/hosts, потом mDNS, потом DNS. Ну да, пока не обращаешь внимание на [NOTFOUND=return] (кому ведь интересно че там в скобках пишут?) — а ведь это уже маленькая бомбочка, о которой мы поговорим отдельно.

Первый источник вранья: /etc/hosts

Самый банальный и самый частый в реальности. Кто-то полгода назад добавил staging.internal в /etc/hosts, чтобы обойти DNS во время какого-то расследования, и забыл убрать. И из-за этого Пинкертона теперь этот хост “резолвится” на машине навсегда независимо от того, что говорит настоящий DNS, независимо от того, что там сейчас в зоне, независимо от того, помнит ли об этом кто-то живой.

files стоит первым в цепочке почти на любом дистрибутиве по умолчанию. И это значит: если имя есть в /etc/hosts — DNS вообще не спросят. Не DNS ответит неправильно, а DNS не спросят вовсе. Для человека, который смотрит на dig и видит правильный ответ, это выглядит как необъяснимая магия. Ведь как это может резолвиться в другое, если DNS говорит правильно? А очень просто — потому что digделает прямой DNS-запрос, а твой процесс до DNS в принципе не доходит.

Ловушка [NOTFOUND=return]

Вот эта конструкция в nsswitch.conf заслуживает отдельного разговора, потому что она реально может испортить тебе в лучшем случае вечер. [NOTFOUND=return] значит: если предыдущий источник явно ответил “такого имени нет” — остановить цепочку прямо здесь и вернуть “ниче не нашел”, не пытаясь спросить следующего.

Казалось бы, разумная оптимизация — mDNS не должен ходить в DNS за именами .local, которых там заведомо не будет. Но на практике это означает, что если что-то в цепочке до DNS решило, что имени не существует, DNS никогда не получит шанс сказать свое слово. Ты можешь идеально настроить зону, ты можешь трижды проверить записи и все равно получить NXDOMAIN на уровне приложения, потому что где-то раньше в цепочке был молчаливый шлагбаум.

Самое неприятное здесь — это происходит тихо. Просто отрицательный ответ, который выглядит абсолютно легитимно.

mDNS и иллюзия .local

Второй пункт в нашей цепочке — mDNS, тот самый multicast DNS, который отвечает за .local-имена в локальной сети. Штука сама по себе рабочая и полезная (Avahi, Bonjour и вот это все), но она добавляет еще один недетерминированный слой: ответ зависит от того, что прямо сейчас есть в сегменте сети, кто отозвался на multicast-запрос за отведенные ей несколько сотен миллисекунд, и повезло ли пакету дойти.

Тут вранья как такового меньше, но неопределенности — выше крыши. Один и тот же .local-хост может резолвиться по-разному в зависимости от того, кто именно ответил первым и насколько загружена сеть в этот момент.

systemd-resolved и обман через 127.0.0.53

А вот это, пожалуй, самый недооцененный источник путаницы на современных дистрибутивах. Открываешь /etc/resolv.conf, видишь:

nameserver 127.0.0.53

и на автомате читаешь это как “вот мой DNS-сервер”. Нет. Это заглушка, локальный прокси systemd-resolved. Реальные нейм-серверы, которые он спрашивает от твоего имени, спрятаны у него внутри в per-link конфиге, до которого обычный resolv.conf не дотягивается вообще. Чтобы узнать кто там реально отвечает за резолвинг, нужно спрашивать не файл, а сам systemd-resolved через D-Bus (org.freedesktop.resolve1.Manager).

Почему это важно? Потому что если ты хочешь сделать независимую проверку и наивно шлешь запрос на 127.0.0.53, ты снова спрашиваешь ту же самую цепочку, только другим путем. Ты сверяешься сам с собой. Ложное чувство независимой проверки, эдакая форма вранья, просто вежливая.

И вот тут есть нюанс, который удивляет: у systemd-resolved DNS-серверы не одни на всю систему, а per-link — свои на каждый сетевой интерфейс. Поднял VPN (тот же Tailscale) — получил отдельный резолвер для его зоны, который живет только на этом линке и в глобальный список серверов не попадает. Если имя относится к такой зоне, а ты сверяешься с глобальными ней серверами — ты гарантированно получишь расхождение, хотя формально все настроено правильно, просто ты спросил не того прохожего.

И тут всплывает со дна еще один источник лжи: gai.conf и сортировка.

DNS в ответе на запрос имени может вернуть сразу оба семейства адресов — и A-записи (IPv4), и AAAA-записи (IPv6). getaddrinfo() получает этот список и не отдает его как есть, а пересортировывает его по правилам RFC 6724, а конкретные веса этих правил живут в /etc/gai.conf. По умолчанию файл почти всегда пустой или отсутствует, что означает “используй дефолты RFC 6724 как есть”, и дефолты там говорят: предпочитай IPv6, если он вообще есть в ответе. Не если он работает. Просто если есть.

И вот тут расходится теория с продакшеном: IPv6 у хоста может быть объявлен в DNS, но реально не маршрутизируется, задроплен на файрволе или тупо висит на мертвом линке. getaddrinfo() про это ничего не знает — он смотрит только на RFC-таблицу приоритетов, а не на реальную доступность адреса. В итоге приложение честно берет “предпочтительный” IPv6-адрес первым, пытается на него достучаться, получает таймаут и только потом падает на IPv4-фолбэк — если он вообще предусмотрен. А dig тем временем как ни в чём не бывало показывает тебе чистый и рабочий IPv4-адрес, потому что dig никого не сортирует и никаких приоритетов не расставляет — он просто печатает, что вернул сервер.

Самое обидное, что это не баг ни у кого. Ни у DNS, ни у приложения, ни у ядра. Это ровно то поведение, которое RFC и просил реализовать. Просто оно молчаливо предполагает мир, где если IPv6-адрес объявлен, то он работает. В 2026 году это предположение все еще сильно оптимистичнее, чем хотелось бы.

Ловушка, о которой почти никто не думает: статический Go-бинарник

И последняя, моя любимая, потому что она рвет шаблон у всех, кто искренне верит, что везде же NSS одна и та же.

Тут стоит сразу закрыть придирку, которая иначе прилетит в комментарии: Go в принципе умеет резолвить и через NSS, если собран с cgo и он доступен на платформе. Но статически слинкованный бинарь — а это ровно то, что обычно льют в прод (CGO_ENABLED=0, никакой libc-зависимости, один файл, копипаст на сервер) этой возможности лишен по построению. Со стандартной статической сборкой Go использует свой собственный, чистый резолвер, написанный на Go, который парсит resolv.conf сам и ходит в DNS сам, минуя NSS, glibc и все, о чем мы говорили выше, целиком.

Это значит следующее: ты можешь идеально настроить nsswitch.conf, добавить нужную запись в /etc/hosts, все перепроверить через getent, но твой Go-сервис все равно этого не увидит, потому что он вообще не спрашивает те источники, которые ты только что поправил. Он живет в своей отдельной реальности резолвинга, которая пересекается с системной только в одной точке — в resolv.conf, да и то не полностью.

Как я до этого докопался

Я это все, честно говоря, не вычитал заранее — я это выковырял руками, комбинацией dig, getent hosts, resolvectl status с легкой злостью. У каждой команды свой кусок правды, ни одна не показывает полную картину, и собрать их вместе в голове каждый раз совсем не кайф, особенно когда уже третий час ищешь глазами несуществующую опечатку. А пресловутый ИИ путается, несет пургу и лишь добавляет сомнений в адекватности происходящего.

В какой-то момент стало проще написать тулзу, которая проходит эту цепочку сама именно так, как ее проходил бы резолвер ОС и параллельно делает независимый DNS-запрос, чтобы явно показать: вот путь, вот его результат, а вот что говорит DNS напрямую, если их спросить в обход всего. Если расходится — вот тебе конкретная причина, а не “попробуй еще раз с флагом -v”.

Так появился gai — от getaddrinfo, а не то, что вы подумали.

$ gai doctor testhost.local

[gai] Simulating name resolution for "testhost.local"... (reality check via 212.227.123.16, 212.227.123.17, systemd-resolved stub: true) RESOLUTION PATH (simulated): 1. Files FOUND 10.0.0.1 DIAGNOSIS:

┌─ ISSUE ──────────────────────────────────────────────────────────────────┐ │ The OS chain and a direct DNS query disagree: 10.0.0.1 vs (none). │ │ Something earlier in the chain (files/mdns) is answering instead of DNS. │ └──────────────────────────────────────────────────────────────────────────┘

Никакого перехвата процессов, никакого LD_PRELOAD, никакого eBPF или ptrace — gai просто читает ту же конфигурацию, что читает сам резолвер ОС (nsswitch.conf, resolv.conf, gai.conf, /etc/hosts, D-Bus-состояние systemd-resolved, одноразовый mDNS-запрос), и честно моделирует путь, которым прошел бы NSS-диспетчер glibc. Отдельно он умеет заметить статический Go-бинарник и прямо сказать, что для этого процесса вся симуляция выше не имеет значения, у него свой резолвер.

Как поставить тулзу и что дальше

Ставится одной строкой, без Rust-тулчейна, без зависимостей вообще — статический бинарник, который просто лежит и работает.

Пойдет в /usr/local/bin/gai с обычными правами 0755 — как любой другой системный бинарник, ничего экзотического. Если хочется собрать самому — cargo install gai-inspector или cargo build --release из исходников, все через crates.io. Никакого рантайма с собой не тащит, ни Python, ни JVM, ни shared-либ, за которые надо переживать при переносе на голый сервер. Скачал, chmod не нужен даже, и можно спрашивать gai doctor <имя> хоть в контейнере, хоть на минимальном Alpine-образе.

Инструмент пока Linux-only и MVP:

  • Split-DNS/per-link-роутинг (VPN-зоны вроде Tailscale, о которых я говорил выше) он пока не умеет и честно репортит NOT FOUND вместо того чтобы соврать, но и не резолвит.

  • IPv6 mDNS — в процессе.

Репо: github.com/casablanque-code/gai. Там подробнее про возможности и флаги.

Если случится ситуация, где gai тоже наврал — тем более буду рад услышать. Код открыт, пулл реквесты принимаются. Интереснее разобрать баг, чем сделать вид, что такого не бывает.

Вы смотрите срез комментариев. Показать все
3
Автор поста оценил этот комментарий

Это значит следующее: ты можешь идеально настроить nsswitch.conf, добавить нужную запись в /etc/hosts, все перепроверить через getent, но твой Go-сервис все равно этого не увидит, потому что он вообще не спрашивает те источники, которые ты только что поправил. Он живет в своей отдельной реальности резолвинга, которая пересекается с системной только в одной точке — в resolv.conf

И какое такое зло мешает править СРАЗУ resolv.conf? Какое такое зло мешает поднять заранее свой ДНС на всю зону (и прописать его в собственно сам резолв конф?)? Зачем для этого ставить непонятный сторонний бинарник? Какую проблему это решает - кривые руки девопсов/админов? Так если руки кривые - так они еще много где накосячат же, да?


ИМХО - очередная никому ненужная, кроме того, кто её написал, хренота. Половину нужного резолвить не умеет, вторую половину резолвит как создателю видится правильно.

раскрыть ветку (13)
0
Автор поста оценил этот комментарий

Ох.. Как там дела в centos 6?

Ну поправил ты resolv.conf, хостс и мднс все равно раньше в цепочке нсссвич стоят. До твоего resolv.conf может и не дойти опрос. К тому же в новых дистрах он управляется демоном, ручные правки там до первого реконнекта живут.

Поднять дна не проблема. Проблема в том, что dig спросит твой идеальный днс напрямую, а системный вызов getaddrinfo() внутри приложения прогонит ответ через правила сортировки /etc/gai.conf (RFC 6724) или запнется о [NOTFOUND=return] в nsswitch.conf.

Ты можешь хоть трижды идеальную зону поднять, но если у приложения отвалился IPv6 на L2, а gai.conf приказал брать AAAA-запись первой, то твоё приложение упадет по таймауту, пока ты будешь радоваться «работающему днс».

Тулза это не резолвер, он ничего не резолвит в системе и ничего не подменяет. Это инструмент диагностики. Он показывает трассировку того, как именно система (через NSS, D-Bus, gai.conf и Go-netdns) будет резолвить имя для реального процесса. Чтобы не сидеть 2 часа с getent, dig, resolvectl status и парсингом конфигов, а получить ответ за 1 секунду.

Поскроллил по диагонали, код не посмотрел и критикуешь. С современным стеком твой староверский подход не проканает, старина)

раскрыть ветку (4)
1
Автор поста оценил этот комментарий

хостс и мднс все равно раньше в цепочке нсссвич стоят. До твоего resolv.conf может и не дойти опрос.

Справедливо, но не-справедливо. У меня всякие там разрабы руками на машины не ходят, и сие, опять-же, ручками не модифицируется. Так что если у вас ВНЕЗАПНО это кто-то, или некто руками трогает, то замечательный пассаж пр 6ю центось отправляется обратно автору.


RFC 6724

Как же мы аж с 2013 до 2026 без этого жили? М? Банальный вопрос - как люди до сих пор в проде вообще без ipv6 живут просто за скобками оставим.


С современным стеком твой староверский подход не проканает, старина)

(вздыхает) Вот понаберут по объявлениям. Они потом компиляторы мимо стандартов пишут, которые, в итоге, к ДНСу мимо стандартов обращаются, потом нихуя не работают, а проблемы, типа, у меня, да? Нормально делай - нормально будет. С описанной вами проблемой не сталкивался вообще ни разу. Ни с микросервисами на go, ни с монолитами на пыхе, ни с дотнетом на винде. Окружение, заранее, нормально готовьте и всё будет хорошо. Всё. А "найти проблем на ровном месте" по причине нетрадиционого угла выверта рук - это завсегда пожалста, хоть с ДНС, хоть с маршрутизацией, хоть "*" в ингресс-хост в траефике пишите.

раскрыть ветку (3)
Автор поста оценил этот комментарий

Ну риторика ясна. «Я не сталкивался, значит этого не существует». Ок, мистер безупречный) Как я и сказал особо ты не вникал, насыпал терминов из разных сфер чтобы мы оценили масштаб фигуры и ширину размаха) Остановимся на том, что ты слишком хорош для этого инструмента

раскрыть ветку (2)
0
Автор поста оценил этот комментарий

«Я не сталкивался, значит этого не существует»

Вопрос максимально простой, был и остаётся, "ЗАЧЕМ?".


Как я и сказал особо ты не вникал, насыпал терминов из разных сфер чтобы мы оценили масштаб фигуры и ширину размаха)

Как я и обозначил - ты их особо и не прокомментировал. Можно, а зачем?


Остановимся на том, что ты слишком хорош для этого инструмента

Не, я нихрена не понимаю зачем и когда он нужен. Т.к. из псто - решительно непонятно. Из комментов - я понял что кгода у тебя там есть нечто на ipv6, или мб (хз) просто на тачке ipv6 поднят, еще когда программеры по продакшону с ручными правками бегают (но тут, на мой скромный, не твой инструмент нужен, а пиздюлей раздать), но в общем и цлом - ниша применения непонятна. Да, местами я тут иронизирую (возможно неуместно), но в целом - один хрен непонятно "зачем" и "когда применять". На "йет анотхре рэндом сервер" на котором ipv4 и больше всё - как-бы вроде и не надо (или надо, а почему?) На чистом ipv6 серваке - тоже вроде не надо - ибо и так всё понятно (или нет)? Непонятно, короч - зачем, почему, когда и в чём смысл и преимущество.

раскрыть ветку (1)
1
Автор поста оценил этот комментарий

Я понял твой скепсис. В мире с одним IPv4-интерфейсом и статичным /etc/resolv.conf этот инструмент действительно не нужен. Как и strace не нужен, пока printf показывает правду.

Проблема в том, что мир линукс за последние 10 лет усложнился до невозможности и «понятно» там больше не бывает.

Моя тулза это не резолвер. Это симулятор-трассировщик. Он не чинит проблемы, он показывает причину за секунду, чтобы ты не тратил 2 часа на ручной сбор пазла. Пока это mvp, но будет лучше.

Ты на админской тачке, поднял Tailscale. У тебя в системе появился интерфейс tailscale0 и сплит-днс. Тебе нужно зарезолвить внутренний хост db.prod.tailscale.

dig спросит глобальный неймсервер из resolv.conf и не получит ответа (т.к. неймсервер не знает про Tailscale-зону). Приложение должно зарезолвить через per-link днс, который systemd-resolved повесил только на интерфейс tailscale0. Понять руками какой днс-сервер и почему сейчас отвечает за домен .tailscale, используя только стандартные утилиты типа dig или resolvectl это квест на 15 минут. gai doctor db.prod.tailscale за секунду показывает, что имя резолвится только через неймсервер х на интерфейсе tailscale0.

Еще пример: на сервере поднят systemd-resolved. Открываешь resolv.conf, видишь nameserver 127.0.0.53. Ты делаешь dig domain.local. Диг спрашивает 127.0.0.53, тот проксирует запрос в upstream, все работает, ты видишь IP 10.0.0.1.

Твой процесс делает getaddrinfo("domain.local"). Он идёт в libc, libc идёт в /etc/nsswitch.conf, видит там модуль mdns4_minimal [NOTFOUND=return]. mDNS отправляет мультикаст -запрос в сеть, какая-то железка отвечает, что имя domain.local резолвится в 192.168.1.1. libс получает этот ответ, и из-за [NOTFOUND=return] DNS вообще не спрашивается. Твой процесс получает неправильный IP. Ты смотришь в dig, видишь правильный IP. У тебя диссонанс.

Я могу еще десяток примеров написать. Не все работает так, как мы хотим и раздать пиздюлей не всегда помогает.

0
Автор поста оценил этот комментарий

В отличие от вас, человек заметил проблему, и попытался разобраться, и не просто попытался разобраться, а дал очень полезную информацию к размышлению и инструмент тестирования. Это называется одним словом - расти. В отличие от вас, с опеннета, где "лишь бы вбросить" доступно без регистрации.


В дополнение, странно, что на Хабре к ней отнеслись не как полагается сообществу их уровня. Хотя, Хабр же... )

раскрыть ветку (7)
3
Автор поста оценил этот комментарий

Слушай, у меня тоже проблема. Я вот взял и внёс в resolv.conf адрес 123.123.123.123. И у меня сразу перестал работать ДНС. Проблема? Проблема! Мож мне свою утилиту написать, которая будет проверять конфиги на ебанутые значения? Сколько проблем сразу решу!


Для меня - это выглядит именно так. Вы абсолютно в своём праве со мной не согласиться.


P.s. Не Хабр - а "Вестник Минцифры", ну типа последние пару лет. Чего там от них ждать.

раскрыть ветку (2)
0
Автор поста оценил этот комментарий

Ну для описанного рода проблемы это оверкилл будет, но опять же если заранее знать, что проблема именно в этом. Почему-то все видят лишь тулзу, а я в большей степени хотел просто подсветить определенную сторону. Развитие происходит именно так.


Про хабр верно, но все равно немного жаль)

раскрыть ветку (1)
0
Автор поста оценил этот комментарий

Я в соседнем треде чуть более подобно отписался по поводу того что в целом "по ситуации" думаю, просто на мой взгляд (а я последние лет 6 девопесов лидую) - ситуация экзотическая. И если ты в это (ipv6) лезешь то ты со старта должен ткие штуки знать, иначе - нечего лезть, оно тебя сожрётЪ.


Хабр уже давно оскотинился, к сожалению. С, примерно, 2015 там ничего не публиковал. Даже не уверен что акк жив еще. (так-же как тут только без двух r на конце)

1
Автор поста оценил этот комментарий

Сам себе придумал проблему, сам её героически решил. Молодец!

раскрыть ветку (1)
1
Автор поста оценил этот комментарий

Пусть так. Но в дискуссиях порой что-то рождается. Может я не заметил, а кто-то увидит. Суть ведь не в тулзе вовсе

Автор поста оценил этот комментарий

Людям в целом свойственно критиковать новое и то, что они не до конца понимают, увы. Спасибо, что верно заметил намерение. На мой взгляд это и есть фундамент опен сорса)

раскрыть ветку (1)
0
Автор поста оценил этот комментарий

Это новое, звучит как костыль над костылем над костылем.

Вы смотрите срез комментариев. Чтобы написать комментарий, перейдите к общему списку

Темы

Политика

Теги

Популярные авторы

Сообщества

18+

Теги

Популярные авторы

Сообщества

Игры

Теги

Популярные авторы

Сообщества

Юмор

Теги

Популярные авторы

Сообщества

Отношения

Теги

Популярные авторы

Сообщества

Здоровье

Теги

Популярные авторы

Сообщества

Путешествия

Теги

Популярные авторы

Сообщества

Спорт

Теги

Популярные авторы

Сообщества

Хобби

Теги

Популярные авторы

Сообщества

Сервис

Теги

Популярные авторы

Сообщества

Природа

Теги

Популярные авторы

Сообщества

Бизнес

Теги

Популярные авторы

Сообщества

Транспорт

Теги

Популярные авторы

Сообщества

Общение

Теги

Популярные авторы

Сообщества

Юриспруденция

Теги

Популярные авторы

Сообщества

Наука

Теги

Популярные авторы

Сообщества

IT

Теги

Популярные авторы

Сообщества

Животные

Теги

Популярные авторы

Сообщества

Кино и сериалы

Теги

Популярные авторы

Сообщества

Экономика

Теги

Популярные авторы

Сообщества

Кулинария

Теги

Популярные авторы

Сообщества

История

Теги

Популярные авторы

Сообщества

Недвижимость и ремонт

Теги

Популярные авторы

Сообщества