Платёжный шлюз отвечает по IP. DNS возвращает правильный адрес. Но приложение пишет Could not resolve host, и платежи не проходят. Если после успешного dig вычеркнуть разрешение имён из списка подозреваемых, расследование уйдёт в сторону.
В моём тренажёре Prod is Down есть кейс «Платёжный шлюз недоступен». Один из его вариантов построен именно на таком расхождении. Ниже - отдельный воспроизводимый эксперимент с настоящими dig, getent, curl и glibc. Это учебный стенд в изолированном контейнере.
Две команды, которые будто бы спорят
На стенде подняты локальный DNS и HTTP-шлюз. Имя gateway.pay.internal должно вести на 127.0.0.20. Шлюз слушает порт 8443, здесь на нём намеренно обычный HTTP, чтобы TLS не добавлял ещё одну переменную.
Сначала спрашиваем DNS:
dig +short gateway.pay.internal A
Ответ:
127.0.0.20
Затем обращаемся к шлюзу по этому же имени:
curl --noproxy '*' -4 -sS --max-time 2 http://gateway.pay.internal:8443/health
Получаем:
curl: (6) Could not resolve host: gateway.pay.internal
Обе команды запущены внутри одного контейнера. У них одинаковый /etc/resolv.conf, нет внешней сети, а прокси для запроса отключён. DNS всё время отдаёт один и тот же IP.
На снимке xterm виден весь эксперимент: сначала ошибка, затем изменение одной строки и успешный запрос.

dig проверил ровно то, о чём его попросили
dig послал DNS-запрос и получил A-запись. Это полезный результат: выбранный DNS доступен и знает нужное соответствие имени и адреса. Но команда не проверяла, каким способом имя ищет приложение.
В нашем окружении curl использует системный резолвер glibc. На его пути находится Name Service Switch — NSS. В /etc/nsswitch.conf задано, из каких источников и в каком порядке получать сведения об именах. Для этого служит строка hosts:.
На сломанном стенде там осталось:
hosts: files
Для поиска хоста разрешён только локальный файл /etc/hosts. Записи нашего шлюза в нём нет. Источника dns в порядке поиска тоже нет - поэтому наличие правильного DNS-сервера в /etc/resolv.conf ничего не меняет. До него этот поиск не доходит.
Пути получились такими:
dig → DNS → 127.0.0.20 curl → системный поиск → NSS → /etc/hosts → имя не найдено
Никакого противоречия: команды проверяли разные цепочки. dig не повторяет системный поиск имён через NSS, по умолчанию он выбирает DNS из /etc/resolv.conf и обращается к нему сам.
Как поймать расхождение без гадания
Следующая проверка - getent. Она обращается к базам NSS, а не заменяет их прямым DNS-запросом:
getent hosts gateway.pay.internal echo $?
На сломанном стенде вывода нет, код завершения - 2: ключ не найден. Вместе с успешным dig это уже указывает, что надо исследовать системный путь поиска имени, а не ограничиваться содержимым DNS-зоны.
Для более близкой проверки современного API полезен getent ahosts: он использует getaddrinfo(). У getent hosts другие функции поиска, поэтому считать его точной копией любого приложения нельзя. Различия описаны в getent. В эксперименте я дополнительно проверил IPv4-вызов socket.getaddrinfo() из отдельного процесса Python: до исправления он тоже возвращает ошибку, после - адрес шлюза.
Есть и независимая улика. У локального dnsmasq включён журнал запросов. Прямой вызов dig оставляет там запрос. Сломанные getent, Python и curl - ни одного. После исправления запросы от системного поиска появляются. Здесь проблема подтверждается на двух сторонах: приложение не получает адрес, а DNS даже не спрашивают.
Отделяем поиск имени от подключения
До исправления конфигурации полезно проверить сам шлюз, сохранив имя в URL:
curl --noproxy '*' -4 -sS --max-time 2 \ --resolve gateway.pay.internal:8443:127.0.0.20 \ http://gateway.pay.internal:8443/health
На стенде ответ - gateway: ok. Опция --resolve задаёт адрес для конкретной пары хост-порт внутри curl. Она позволяет отдельно проверить соединение с выбранным адресом и обработку запроса с исходным именем. Для HTTPS исходное имя сохраняется также для SNI и проверки сертификата.
Это диагностический обход: он показывает, что выбранный IP и HTTP-обработчик доступны. Он не исправляет системное разрешение имени и не доказывает исправность всех маршрутов или реплик. По этой же причине успешный запрос по IP нельзя считать проверкой полной пользовательской цепочки.
Исправление короче расследования
В нашем минимальном окружении ожидаемый порядок - сначала локальный файл, затем DNS:
hosts: files dns
После изменения повторяем обычные команды, уже без --resolve:
getent hosts gateway.pay.internal curl --noproxy '*' -4 -sS --max-time 2 http://gateway.pay.internal:8443/health
Теперь getent показывает 127.0.0.20 gateway.pay.internal, а шлюз отвечает gateway: ok. DNS-запись не менялась. /etc/hosts не менялся. HTTP-шлюз не перезапускался. Исправился выбор источников для системного поиска имени.
Строка files dns подходит этому стенду. На рабочем сервере нельзя вслепую заменять ею всю настройку: там могут использоваться resolve, myhostname, mDNS и условия досрочного завершения поиска. Восстанавливать нужно ожидаемую конфигурацию конкретной машины, затем проверять запрос из окружения приложения.
Перезапуск всей машины для нашего исправления не понадобился. Но отдельное приложение может держать собственный кеш или старое соединение. Проверка новым процессом и проверка работающего сервиса отвечают на разные вопросы — после правки нужны обе.
Что именно доказала каждая проверка
Проверка |
Что мы узнали на этом стенде |
|---|---|
|
DNS доступен и возвращает правильную A-запись |
|
Поиск через NSS не находит имя |
Python |
Системный API тоже не находит адрес |
|
С выбранным IP и исходным именем HTTP-запрос проходит |
Обычный |
Для этого запроса восстановлена вся проверяемая цепочка |
Оговорка про окружение существенна. У curl бывают разные resolver backend, включая c-ares, а приложение может использовать свой DNS-клиент или DoH. В контейнере нужно проверять его конфиги, а не только конфиги хоста. Поэтому результат getent - проверка системного пути на данной машине, а не универсальная модель любого приложения.