Хабравчане, приветсвую!

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

По работе я веду SEO‑проекты, поэтому с DNS сталкиваюсь регулярно: переезды, новые домены, CDN, почта, странные редиректы и вечное «а почему сайт у меня уже работает, а у коллеги ещё нет». Обычно такие вопросы быстро уходят разработчикам или админам, а сам DNS остаётся где‑то в категории «ну, домен спросил адрес и получил ответ».

На днях как раз обсуждали с коллегами очередную проблему с DNS. Я полез чуть глубже и наткнулся на свежий доклад с IETF 126. Там в одном из экспериментов один DNS‑запрос породил 329 исходящих запросов. Сначала решил, что либо неправильно понял слайд, либо авторы специально собрали DNS‑матрёшку для конференции. Но оказалось, что всё гораздо интереснее.

В итоге полез разбираться, откуда берутся эти 329 запросов, и оказалось, что обычный DNS внутри устроен заметно веселее, чем выглядит из dig.

В DNS есть довольно уютная иллюзия.

Пишем:

dig example.com

Получаем IP.

В голове остается примерно такая схема:

я -> DNS -> 93.184.216.34

Максимум вспоминаются root‑сервер, .com, авторитетный сервер домена. Три‑четыре шага. Красиво.

А потом я увидел слайд с IETF 126.

На нем было одно число:

329.

Один PTR‑запрос. Один рекурсивный резолвер. Пустой кеш. BIND 9.18.

329 исходящих DNS‑запросов ради одного ответа.

Я сначала решил, что речь идет о каком‑то специально собранном DNS‑лабиринте. Оказалось интереснее: пример взят из вполне настоящего IPv6 reverse DNS.

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

Сначала выбросим кеш

Главная причина, почему мы почти никогда всего этого цирка вокруг себя не замечаем, — кеш.

Рекурсивный DNS‑сервер постоянно помнит уже найденные NS, адреса этих NS, результаты предыдущих запросов, DNSSEC‑данные и делегации.

Поэтому обычный запрос выглядит скучно.

Для эксперимента нужен резолвер, который проснулся минуту назад и про интернет знает примерно следующее:

Вот список root-серверов.
Удачи.

Именно такой режим использовался в измерениях Ondřej Surý из ISC: один BIND, один запрос, cold cache. В презентации отдельно указано, что все значения измерялись с холодным кешем.

Если хочется посмотреть механику самому, стенд собирается довольно просто.

Например, BIND:

sudo apt install bind9 dnsutils tcpdump

Слушаем его локально, включаем рекурсию и отправляем запрос прямо в свой resolver:

dig @127.0.0.1 example.com A

Перед новым прогоном очищаем кеш:

sudo rndc flush

А DNS‑трафик складываем в pcap:

sudo tcpdump -ni any port 53 -w dns.pcap

После запроса:

sudo pkill -INT tcpdump

Открываем dns.pcap в Wireshark.

Вот там DNS сразу перестает выглядеть как одна строчка из dig.

Самый скучный случай уже требует цепочки

Возьмем условный:

www.example.com

При действительно пустом состоянии резолверу приходится двигаться по дереву DNS.

Упрощенно:

.
└── com
    └── example.com
        └── www.example.com

Сначала нужен root.

Потом серверы .com.

Потом серверы example.com.

Потом уже ответ для www.example.com.

Документация BIND описывает именно такую итерацию: при отсутствии нужной делегации в локальном кеше resolver идет к ближайшей известной точке дерева, вплоть до root, получает referral и двигается дальше.

Получается примерно:

1. root -> где .com?
2. .com -> где example.com?
3. example.com -> где www.example.com?

На этом месте хочется закрыть статью со словами «ну ладно, три запроса».

Рано.

Потому что в каждом referral лежат имена других DNS‑серверов.

А резолверу нужны их IP‑адреса.

И вот с этого момента дерево начинает расти в стороны.

DNS‑сервер тоже имеет доменное имя

Допустим, .org говорит:

example.org NS katelyn.ns.cloudflare.com
example.org NS mitch.ns.cloudflare.com

Resolver получил названия серверов.

Для отправки UDP‑пакета название katelyn.ns.cloudflare.com почти бесполезно. Нужен адрес.

Значит внутри одного DNS‑разрешения появляется второе DNS‑разрешение:

Найди example.org
    |
    +-- сначала найди katelyn.ns.cloudflare.com

А cloudflare.com находится уже в .com.

То есть мы шли:

root
  -> org
      -> example.org

и внезапно прыгнули:

root
  -> com
      -> cloudflare.com
          -> katelyn.ns.cloudflare.com

Только после этого можем вернуться к исходному example.org.

Получается DNS‑рекурсия внутри DNS‑рекурсии.

В терминологии DNS это один из самых интересных случаев — out‑of‑bailiwick nameserver.

Сервер домена живет за пределами зоны, которую сейчас делегируют.

В презентации Surý пример разобран буквально по шагам:

Want: A example.org

-> root
-> .org
-> example.org delegation

NS = katelyn.ns.cloudflare.com

-> root
-> .com
-> cloudflare.com
-> katelyn.ns.cloudflare.com

-> обратно к example.org

Один NS уже создал отдельную ветку.

А NS обычно несколько.

В какой‑то момент начинаешь подозревать, что DNS придумали люди, которым очень нравились квесты с побочными заданиями.

Glue спасает положение

Здесь DNS использует старый и очень практичный механизм — glue records.

Представим:

example.com NS ns1.example.com

Чтобы найти example.com, нужен ns1.example.com.

Чтобы найти ns1.example.com, сначала нужен example.com.

Красивый цикл.

Поэтому родительская зона может передать IP nameserver вместе с делегацией:

example.com.      NS   ns1.example.com.
ns1.example.com.  A    192.0.2.53

Вторая запись здесь работает как glue.

Resolver получает адрес сразу и продолжает путь.

У glue есть важная граница доверия. Современные резолверы осторожно относятся к адресам серверов, которые лежат за пределами соответствующей зоны. Такая осторожность напрямую связана с защитой кеша от подмены.

И тут появляется неприятная развилка для разработчика resolver:

Использовать больше полученных данных
        |
        +-> меньше запросов
        +-> больше поверхность для cache poisoning

Перепроверять данные самостоятельно
        |
        +-> больше запросов
        +-> более строгая модель доверия

DNS начинает выглядеть уже не как дерево имен, а как дерево доверия.

Теперь добавим CNAME

А теперь берем обычный современный сервис.

Например:

dig A teams.microsoft.com

В разборе DNS‑тем с IETF 126 от APNIC цепочка выглядела так:

teams.microsoft.com
  CNAME teams.office.com

teams.office.com
  CNAME tmc-g2.tm-4.office.com

tmc-g2.tm-4.office.com
  CNAME teams-office-com.s-0005.dual-s-msedge.net

teams-office-com.s-0005.dual-s-msedge.net
  CNAME s-0005.dual-s-msedge.net

s-0005.dual-s-msedge.net
  A 52.123.129.14
  A 52.123.128.14

Это уже четыре CNAME перед конечным A.

И каждый переход способен отправить resolver в новую зону.

microsoft.com
   |
   v
office.com
   |
   v
dual-s-msedge.net

А у каждой зоны свои NS.

У NS свои доменные имена.

Доменные имена NS снова требуют A/AAAA.

Те приводят к новым TLD и новым делегациям.

На бумаге CNAME выглядит как:

A -> B

Для cold‑cache resolver реальная картина ближе к такой:

A
├── NS зоны A
│   ├── A NS1
│   ├── AAAA NS1
│   ├── A NS2
│   └── AAAA NS2
│
└── CNAME B
    ├── NS зоны B
    │   ├── A NS1
    │   ├── AAAA NS1
    │   └── ...
    │
    └── CNAME C
        └── ...

Вот где количество запросов начинает расти очень быстро.

В измерениях с teams.microsoft.com BIND 9.18 делал от 141 до 180 запросов для A при выключенной DNSSEC‑валидации. Более новые ветки BIND тратили заметно меньше.

От одного dig.

180 DNS‑запросов.

И мы еще даже до IPv6 PTR из заголовка не дошли.

DNSSEC добавляет второе измерение

При DNSSEC resolverу мало получить запись.

Нужно проверить цепочку доверия.

В игре появляются:

DNSKEY
DS
RRSIG
NSEC / NSEC3

Условно resolver спрашивает:

Какой IP у example.com?

Получает ответ.

Следующий вопрос:

А кто подтверждает, что этот ответ настоящий?

Для этого строится цепочка:

root trust anchor
      |
      DS
      v
     TLD
      |
      DS
      v
example.com
      |
    DNSKEY
      |
    RRSIG

В официальном DNSSEC Guide BIND validating resolver описывается как рекурсивный сервер, выполняющий дополнительные действия для проверки подлинности полученных DNS‑данных.

С точки зрения безопасности это полезная работа.

С точки зрения нашего счетчика пакетов это еще несколько веток.

Особенно весело становится, когда зоны из основной цепочки имеют свои внешние NS, а те приводят в другие зоны.

И тут появляется PTR

Вот это место мне понравилось больше всего.

Прямой DNS выглядит естественно:

habr.com -> IP

Reverse DNS делает обратное:

IP -> hostname

Для IPv4 адрес преобразуется в дерево in-addr.arpa.

Например:

212.132.99.84

становится чем‑то вроде:

84.99.132.212.in-addr.arpa

С IPv6 веселее.

Адрес содержит 32 шестнадцатеричные цифры.

В reverse DNS каждая цифра превращается в отдельную DNS label, порядок разворачивается, а в конце добавляется:

ip6.arpa

Получается монстр вроде:

7.8.6.0.0.0.1.c.0.0.0.0.0.0.0.0.
2.2.0.0.8.e.2.0.c.7.6.0.1.0.0.2.ip6.arpa.

И это уже 32 уровня потенциального пространства делегирования.

Каждый участок может обслуживаться своей организацией.

Примерно так:

IANA
 |
 RIR
 |
 LIR
 |
 ISP
 |
 клиент

У каждого уровня могут быть отдельные NS.

Эти NS могут находиться в других доменах.

И начинается знакомое:

PTR
 |
 +-- delegation
       |
       +-- NS
            |
            +-- где находится этот NS?
                  |
                  +-- другая зона
                        |
                        +-- ее NS

Вот мы и приехали к 329.

Те самые 329

Использовался IPv6 PTR для dnssec-stats.ant.isi.edu.

С холодным кешем результаты получились такими:

BIND 9.18: 295-329 запросов
BIND 9.20: 193-210
BIND 9.21: 102-128

То есть 329 — верхняя граница серии измерений на BIND 9.18, а не магическая константа DNS.

Это важнее самого красивого числа.

Одна и та же DNS‑конфигурация на разных поколениях resolver дала разницу примерно в три раза.

Протокол тот же.

Имя то же.

Ответ тот же.

Меняется стратегия resolver.

Почему BIND 9.21 тратит сильно меньше

Вот тут история из «забавного DNS‑факта» превращается в нормальную инженерную задачу.

Resolver постоянно выбирает, сколько данных подготовить заранее.

Есть два крайних подхода.

Вариант первый: разрешить всё

При каждой делегации получили пять NS:

ns1.provider-a.com
ns2.provider-a.net
ns3.provider-b.org
ns4.provider-c.io
ns5.provider-d.net

Можно сразу найти A и AAAA каждого.

Плюсы понятны: если первый сервер лежит, адреса остальных уже готовы.

Цена:

5 NS x A/AAAA x их собственные цепочки

Такой алгоритм прекрасно умеет превращать одно имя в сотни запросов.

Вариант второй: взять один NS

Получили пять серверов.

Выбрали один.

Разрешили только его.

Отправили запрос.

Работает быстро, пока выбранный NS отвечает.

При проблеме придется на ходу строить следующую ветку.

То есть у resolver есть настоящая оптимизационная задача:

сколько работы сделать заранее, чтобы сохранить устойчивость и одновременно ограничить DNS amplification внутри самой рекурсии.

В материалах IETF этот выбор сформулирован примерно как resolve everything против resolve bare minimum, а практическая стратегия лежит между ними.

В BIND 9.21.20 появился parent‑centric подход к обработке делегаций, который заметно уменьшил число cold‑cache запросов в приведенных тестах.

Результаты особенно красивые:

                    старый подход    parent-centric

google.com               24               7
facebook.com             25               7
chatgpt.com              32               8
x.com                   100              41
reddit.com               51              29
bing.com                 51              23
wikipedia.org            34               7

Разница огромная.

Причем x.com со своими 100 запросами здесь выглядит как человек, который зашел в магазин за хлебом и по дороге успел оформить ипотеку.

А зачем вообще считать DNS‑запросы

Поначалу 329 выглядит как спортивная статистика.

На практике у этой цифры есть несколько вполне физических последствий.

Каждый дополнительный запрос — это:

UDP/TCP пакет
+
работа resolver
+
состояние в памяти
+
ожидание ответа
+
возможный timeout
+
повторная попытка

А некоторые ветки идут последовательно.

Задержка одного сервера становится задержкой следующего шага.

Если один из внешних NS отвечает 800 мс, дерево начинает тормозить уже совсем по‑человечески.

В презентации есть хороший тезис: каждый такой переход на стороне resolver означает RTT. Кеш большую часть времени прячет эти расходы.

Именно поэтому DNS после рестарта resolver и DNS через пять минут его жизни — довольно разные системы.

Кеш здесь буквально держит интернет

После первого тяжелого разрешения большая часть промежуточных результатов остается в памяти:

NS для TLD
адреса NS
делегации
DNSKEY
DS
A
AAAA
CNAME

Следующий клиент приходит с похожим запросом, а половина дерева уже собрана.

Вместо:

root
 -> TLD
   -> provider
     -> NS provider
       -> другая зона
         -> ...

получаем:

cache -> почти готово

Поэтому эксперимент с cold cache одновременно искусственный и очень полезный.

Он снимает крышку.

В обычной работе кеш закрывает сложность DNS настолько хорошо, что мы забываем о ее существовании.

Можно увидеть это самому

Самая простая лаборатория выглядит так:

sudo apt install bind9 dnsutils tcpdump

Очищаем кеш:

sudo rndc flush

Запускаем запись:

sudo tcpdump -ni any port 53 -w cold.pcap

Делаем один запрос:

dig @127.0.0.1 teams.microsoft.com A

Останавливаем capture.

Затем повторяем тот же dig, уже сохранив трафик в:

warm.pcap

Дальше Wireshark:

dns

или:

dns.flags.response == 0

Получаем только DNS queries.

Еще полезнее:

tshark -r cold.pcap \
  -Y 'dns.flags.response == 0' \
  -T fields \
  -e ip.dst \
  -e ipv6.dst \
  -e dns.qry.name \
  -e dns.qry.type

Можно отсортировать имена:

tshark -r cold.pcap \
  -Y 'dns.flags.response == 0' \
  -T fields \
  -e dns.qry.name |
sort |
uniq -c |
sort -nr

И вот здесь хорошо видно, насколько итоговый адрес скрывает происходящее внутри resolver.

Для чистого эксперимента я бы прогнал минимум:

example.com
wikipedia.org
teams.microsoft.com
x.com
какой-нибудь IPv4 PTR
какой-нибудь IPv6 PTR

Каждый раз:

rndc flush
capture
один запрос
stop

Потом повторить без rndc flush.

Получатся две совершенно разные картины одного DNS.

Самая интересная часть тут вообще про дизайн систем

До этого я воспринимал recursive resolver как довольно прямолинейную программу.

Получил имя.

Пошел сверху вниз.

Вернул адрес.

После этой истории модель поменялась.

Resolver постоянно решает задачи доверия и стоимости:

Какие glue принять?

Какие адреса перепроверить?

Сколько NS подготовить?

Искать сразу IPv4 и IPv6?

Как далеко раскрывать соседние зависимости?

Когда использовать кеш?

Сколько работы разрешить одному клиентскому запросу?

Когда пора остановить рекурсию?

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

Сам DNS тоже удобнее представлять графом.

Запрос:

teams.microsoft.com

легко тянет за собой:

microsoft.com
azure-dns.org
azure-dns.info
azure-dns.com
azure-dns.net
office.com
dual-s-msedge.net
...

И это только ради ответа на один вопрос.

Что я вынес из этих 329 запросов

Само число 329 — хороший заголовок.

Гораздо интереснее причины его появления.

DNS строился как распределенная система делегаций. Потом в него пришли CDN, managed DNS, длинные CNAME‑цепочки, DNSSEC, IPv6, независимые DNS‑провайдеры и более строгие правила работы с glue.

Каждая технология по отдельности выглядит вполне разумно.

Вместе они образуют граф зависимостей, который cold‑cache resolver должен раскрутить перед тем, как вернуть несколько байт ответа.

И лучший инженерный трюк DNS здесь очень старый:

кешировать всё, что еще имеет TTL.

Пока кеш теплый, сотни внутренних действий превращаются для клиента в обычный:

Query time: 1 msec

А после rndc flush крышка снова открывается.

И один DNS‑запрос внезапно оказывается далеко не одним запросом.

Материалы

За исходную точку я взял доклад Ondřej Surý (ISC) “How many DNS queries does it take to resolve one name?”, представленный на IETF 126 в Вене в июле 2026 года. Именно там приведены измерения cold‑cache BIND 9.18/9.20/9.21, включая диапазон 295–329 запросов для IPv6 PTR.

Механику рекурсивной итерации сверял с актуальной документацией BIND 9: resolver при отсутствии данных в кеше двигается от ближайшей известной делегации, получает referral и продолжает итерацию к более конкретной зоне.

Для части про DNSSEC использовал официальный DNSSEC Guide BIND: validating resolver выполняет дополнительную проверку подлинности DNS‑ответов и работает с DNSSEC‑цепочкой доверия.

Отдельный разбор DNS‑сессий IETF 126 опубликовал Geoff Huston из APNIC; там хорошо показаны out‑of‑bailiwick NS, glue, CNAME и причина роста числа рекурсивных запросов.