MAATRIX / Блог / Что происходит, когда DNS-ответ не помещается в один пакет

Что происходит, когда DNS-ответ не помещается в один пакет

MAATRIX

DNS придумывали в начале 80-х под один короткий запрос и один короткий ответ по UDP — и почти сорок лет спустя это решение всё ещё аукается: домены с DNSSEC, множеством A/AAAA-записей или SPF-политикой в TXT легко упираются в старый лимит размера пакета. Дальше начинается механизм, который редко объясняют целиком: расширение EDNS0, флаг TC (truncated) и переключение на TCP. Разбираем, как это устроено внутри и почему файрвол, который «для простоты» блокирует DNS по TCP, тихо ломает резолвинг именно у сложных доменов — то есть у ваших.

Почему DNS изначально работает по UDP

DNS выбрал UDP не случайно и не по недосмотру. У запроса и ответа простая структура «вопрос → ответ», без необходимости в долгоживущем соединении, а UDP не тратит время на трёхстороннее рукопожатие TCP: пакет улетел — пакет пришёл, задержка на резолвинг одного имени близка к одному RTT. Для сервиса, который резолвер дёргает десятки раз на каждую загрузку страницы (домен сайта, домены картинок, CDN, аналитики, шрифтов), экономия на установлении соединения существенна.

Мы отдельно разбирали, чем вы платите за скорость UDP по сравнению с TCP в общем случае — здесь та же логика: DNS выбрал скорость там, где она обычно уместна, и подстраховался TCP там, где без него не обойтись. Обратная сторона UDP — отсутствие гарантий доставки и, что важнее в контексте этой статьи, ограничение на размер пакета, при котором он гарантированно не фрагментируется на пути от сервера к клиенту. Исторический стандарт DNS (RFC 1035) закладывал максимальный размер UDP-сообщения в 512 байт полезной нагрузки. Это очень мало: один ответ с несколькими A-записями, парой NS-записей в дополнительной секции и SOA ещё туда помещается, а вот ответ с DNSSEC-подписями (RRSIG), несколькими AAAA-записями и полным набором glue-записей для делегирования — уже нет.

Важно понимать: ограничение в 512 байт — это не потолок UDP как протокола (UDP-датаграмма теоретически может быть намного больше), а исторически согласованный между DNS-серверами и резолверами предел, гарантирующий, что пакет не будет фрагментирован на IP-уровне где-то по дороге. Фрагментированные UDP-пакеты — отдельная головная боль: если потеряется хотя бы один фрагмент, теряется весь ответ целиком, а некоторые файрволы и NAT-устройства фрагменты вообще режут. Поэтому DNS исторически предпочитает не фрагментировать, а либо ужиматься в 512 байт, либо честно сказать «этот ответ не влезает» и предложить клиенту способ получить его целиком.

Что происходит, когда ответ не помещается в 512 байт

Смоделируем ситуацию буквально. Авторитативный сервер собирает ответ на запрос — допустим, DNSKEY для зоны с DNSSEC, где в ответе нужно вернуть сразу несколько ключей и их подписи. Сериализованный ответ занимает, скажем, 1400 байт. Сервер обязан вернуться в рамки согласованного лимита UDP-пакета (о том, откуда он берётся — в следующем разделе), а раз ответ не влезает — у сервера ровно два пути.

Первый: если стороны заранее договорились о расширенном размере пакета через EDNS0, сервер просто отправляет более крупный UDP-пакет — в рамках согласованного лимита. Второй, крайний случай — сервер обрезает ответ до влезающего размера, ставит в заголовке DNS-сообщения флаг TC (TrunCated) и отправляет то, что поместилось. Это не ошибка и не отказ — сервер прямым текстом говорит клиенту: «то, что ты получил, неполное, если тебе нужен весь ответ — спроси меня снова, но уже по TCP».

Проверить это руками можно dig с принудительно маленьким буфером:

dig +bufsize=512 +noedns DNSKEY example.com

В выводе, если ответ не влез, вы увидите в блоке флагов tc (сокращение от truncated) в строке ;; flags: qr rd ra tc;, а секция ANSWER может быть пустой или неполной, хотя запись точно существует.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

EDNS0: как договориться о большем пакете без перехода на TCP

Чистый исторический протокол DNS не оставлял пространства для манёвра — либо 512 байт, либо TCP. В 1999 году появилось расширение EDNS0 (RFC 2671, позже переопределено RFC 6891), которое добавляет в запрос псевдо-запись OPT. В ней клиент явно сообщает серверу, какой максимальный размер UDP-ответа он готов принять — обычно резолверы указывают значения в диапазоне от 1232 до 4096 байт, конкретное число зависит от софта и его настроек.

Если оба конца поддерживают EDNS0, сервер может собрать ответ размером, скажем, 1500 байт и отправить его одним UDP-пакетом — без перехода на TCP и без флага TC, при условии что итоговый пакет укладывается в согласованный размер и не рискует фрагментироваться на пути. На практике это закрывает подавляющее большинство случаев с DNSSEC и составными ответами: EDNS0 поддерживают все современные резолверы (BIND, PowerDNS, Unbound, resolver в systemd) и почти вся авторитативная инфраструктура.

Посмотреть, участвует ли EDNS0 в конкретном обмене, можно так:

dig +dnssec example.com

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 1232

Строка udp: 1232 — это как раз согласованный размер буфера. Если её нет вовсе, значит, запрос ушёл без EDNS0 и сервер ограничен классическими 512 байтами — риск усечения выше.

Здесь стоит сделать честную оговорку: EDNS0 расширяет лимит, но не снимает его полностью, и не отменяет проблему с фрагментацией пакетов, которые превышают MTU по дороге. Слишком агрессивно задранный размер буфера (скажем, 4096 байт при пути с маленьким MTU) может провоцировать фрагментацию IP-пакета — и тогда вы получаете ту самую ситуацию, где потеря одного фрагмента убивает весь ответ. Именно поэтому современные рекомендации (в частности, DNS Flag Day 2020) сместили практику в сторону более консервативного значения около 1232 байт — оно с запасом укладывается в типичный путевой MTU и снижает риск фрагментации, оставаясь при этом заметно больше исходных 512. Если тема фрагментации пакетов вам не до конца знакома, у нас есть отдельный разбор того, откуда берутся загадочные обрывы из-за MTU — механика там та же, просто на уровне IP, а не DNS.

Флаг TC и что делает клиент, получив усечённый ответ

Если EDNS0 недоступен или даже согласованного расширенного буфера не хватило, в дело вступает TC. Получив ответ с этим флагом, клиент (обычно это не конечное приложение, а резолвер — локальный кеширующий сервер вроде Unbound, systemd-resolved или резолвер провайдера) обязан по стандарту повторить тот же запрос, но уже по TCP на порт 53.

Механика простая: устанавливается полноценное TCP-соединение (три пакета рукопожатия), внутри него передаётся тот же DNS-запрос в формате, где перед сообщением добавляется двухбайтовая длина, сервер отвечает — и в TCP-сообщении лимита в 512 байт уже нет, максимальный размер там определяется тем же двухбайтовым полем длины, то есть до 65535 байт. Ответ приходит целиком, без усечения.

Плата за это — задержка. Вместо одного RTT на UDP-запрос клиент тратит RTT на установление TCP-соединения плюс ещё RTT на сам запрос-ответ (а если используется TLS поверх TCP, как в DNS-over-TLS, добавляется ещё и TLS-рукопожатие). Для одного резолвинга это обычно означает удвоение или больше времени ожидания по сравнению с обычным UDP-запросом — конкретный множитель у вас будет зависеть от RTT до сервера и от того, держит ли резолвер TCP-соединение открытым для последующих запросов через keep-alive (RFC 7766 явно рекомендует так делать, чтобы не платить за рукопожатие на каждый truncated-ответ заново).

Важная деталь: усечение и TC-флаг могут случиться и без DNSSEC — например, при большом количестве NS-записей у делегированной зоны, длинных TXT-записях (SPF с несколькими include, DKIM-ключи) или при ответах на запросы типа ANY (который сейчас многие серверы вообще перестали честно обслуживать по соображениям безопасности и производительности, отвечая на него минимальным набором или тем же TC).

Почему DNSSEC и объёмные зоны чаще упираются в этот лимит

DNSSEC — самый предсказуемый источник больших ответов, и вот почему. Обычный ответ на A-запрос — это, условно, одна-две записи типа A плюс, может быть, пара NS в дополнительной секции. С включённым DNSSEC к каждому такому набору записей добавляется RRSIG — криптографическая подпись, которая занимает заметно больше места, чем сама подписываемая запись, а для запросов DNSKEY или запросов «докажи, что такой записи нет» (NSEC/NSEC3, включая цепочки для отрицательных ответов) объём ответа растёт ещё сильнее.

Похожая картина у зон с большим числом делегированных поддоменов: если у зоны десяток NS-записей (например, при многоуровневом гео-DNS или при использовании нескольких провайдеров DNS одновременно для отказоустойчивости), ответ на запрос NS для этой зоны легко перевешивает 512 байт даже без всякого DNSSEC.

Практический вывод для тех, кто администрирует собственную зону: если вы включаете DNSSEC, разворачиваете широкий SPF/DKIM-набор в TXT или добавляете много NS-делегирований, стоит заранее убедиться, что и ваш авторитативный сервер, и резолверы ваших пользователей (а вы их не контролируете) корректно обрабатывают TCP-fallback. Мы уже разбирали похожий инцидент, где ротация ключей DNSSEC положила резолвинг у части пользователей — там причина была в другом (устаревший кеш подписи), но общая уязвимость DNSSEC к росту размера ответа заметна и в том разборе. Проверить свой авторитативный сервер можно напрямую:

dig +tcp DNSKEY example.com @ваш-ns-сервер

Если ответ пришёл — сервер обслуживает TCP-запросы штатно. Для PowerDNS и BIND это включено по умолчанию, но стоит явно свериться с конфигом, если он менялся под конкретную задачу (например, если кто-то раньше ограничивал слушающие сокеты только под UDP из соображений производительности).

Как проверить, что резолвер вашего сервера умеет TCP-fallback

Если сервер выступает не только авторитативным, но и резолвером (например, локальный кеширующий Unbound перед приложением), стоит убедиться, что исходящие запросы к вышестоящим DNS он тоже может переключать на TCP при получении TC. Для Unbound это поведение включено по умолчанию, но полезно явно посмотреть в конфиге:

# /etc/unbound/unbound.conf
server:
    do-tcp: yes
    tcp-upstream: no   # yes — принудительно всегда ходить по TCP наружу

do-tcp: yes разрешает Unbound принимать и отправлять запросы по TCP (в том числе fallback после TC), а tcp-upstream — это отдельная настройка «всегда ли ходить наружу по TCP», которую обычно включают только в сетях, где UDP DNS-трафик наружу заблокирован (например, во внутренней сети провайдера с жёстким периметром).

Проверить весь путь end-to-end проще всего смешанным тестом: сначала обычным UDP-запросом, затем принудительно по TCP, и сравнить, что резолвер получает одно и то же:

dig DNSKEY example.com
dig +tcp DNSKEY example.com

Если первый вариант возвращает пустую или усечённую секцию ANSWER с флагом tc, а второй — полный набор записей, значит механизм truncated/TCP работает штатно и проблема, если она есть, не в вашем сервере, а где-то дальше по сети — например, в файрволе на пути.

Файрвол, который «упрощает» правила и ломает резолвинг

Здесь и кроется самая частая практическая ошибка. DNS ассоциируется у многих администраторов исключительно с UDP/53 — и когда настраивают файрвол «с нуля» или ужесточают правила после аудита безопасности, разрешают только UDP 53 out/in, а TCP 53 блокируют или просто не открывают, считая его ненужным легаси. Логика понятна: раз обычный запрос помещается в UDP, зачем открывать ещё и TCP — это выглядит как лишняя поверхность атаки.

Проблема в том, что для доменов с DNSSEC, длинными TXT-записями или множеством NS-записей TCP — это не опция, а обязательная часть протокола. Заблокировав TCP/53, вы не «упрощаете» DNS, а ломаете резолвинг именно для сложных ответов, оставляя рабочими только простые — то есть поведение становится частичным и непредсказуемым: одни домены резолвятся нормально, другие подвисают и в итоге падают в таймаут после нескольких безуспешных попыток TCP-соединения, которое файрвол молча дропает.

Пример правила, которое стоит проверить в первую очередь, если у вас на сервере или на периметре есть кастомный iptables/nftables:

# так делать не стоит — открыт только UDP
iptables -A INPUT -p udp --dport 53 -j ACCEPT
iptables -A INPUT -p tcp --dport 53 -j DROP

# корректный вариант — оба протокола
iptables -A INPUT -p udp --dport 53 -j ACCEPT
iptables -A INPUT -p tcp --dport 53 -j ACCEPT

То же самое касается исходящих правил, если сервер сам выступает резолвером и ходит наружу к вышестоящим DNS или к корневым/TLD-серверам — исходящий TCP/53 тоже должен быть разрешён, иначе ваш резолвер не сможет достучаться до авторитативных серверов с DNSSEC.

Отдельно стоит проверить NAT-таблицы состояний (conntrack) — иногда UDP/53 разрешён и работает, а таймаут для TCP-сессий в NAT слишком короткий, и рукопожатие до DNS-сервера с высоким RTT не успевает завершиться. Диагностировать это можно дампом трафика прямо на интересующем порту:

tcpdump -i any port 53 -n -w dns_debug.pcap

и просмотром в Wireshark — видно, доходит ли SYN до сервера, приходит ли SYN-ACK назад и не обрывается ли соединение до конца передачи ответа.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Почему просто не увеличить лимит UDP-пакета для DNS раз и навсегда?

Потому что размер, который гарантированно не фрагментируется по пути, зависит от MTU на каждом промежуточном участке сети, а не только от возможностей конечных серверов. EDNS0 уже поднял практический лимит с 512 байт до типично 1232-4096, но полностью убрать риск фрагментации при очень больших ответах нельзя — поэтому TCP как запасной путь остаётся частью протокола.

Если я использую Cloudflare, Route 53 или другой managed DNS, мне вообще нужно об этом думать?

Managed DNS-провайдеры сами корректно обслуживают и EDNS0, и TCP-fallback на своей стороне. Думать об этом вам нужно там, где вы контролируете сеть между клиентом и сервером — то есть на файрволе собственного VPS или выделенного сервера, если он резолвер, или на периметре офисной/корпоративной сети, откуда сотрудники ходят в интернет.

Как понять, что у меня в проде именно эта проблема, а не что-то другое?

Характерный признак — избирательность: часть пользователей или часть локаций не могут открыть конкретные домены (особенно с DNSSEC), а простые домены резолвятся у всех одинаково. Проверка dig +tcp напрямую с проблемного узла и сравнение с обычным dig — самый быстрый способ подтвердить или исключить эту причину.

DNS-over-HTTPS и DNS-over-TLS решают эту проблему по-другому?

Они всегда работают поверх TCP (внутри TLS-сессии), поэтому усечение по 512-байтовому UDP-лимиту там в принципе невозможно — но взамен вы платите TCP- и TLS-рукопожатием на каждое новое соединение, если оно не переиспользуется. Это отдельный компромисс между надёжностью большого ответа и скоростью установления канала; мы разбирали DoH отдельно в статье про DNS-over-HTTPS и доступ к ИИ-сервисам.

Нужно ли мне вообще включать EDNS0 вручную?

Нет, в современных резолверах (Unbound, BIND, systemd-resolved, встроенные резолверы в Windows/macOS) EDNS0 включён по умолчанию. Ручная настройка обычно требуется, только если вы отлаживаете конкретную проблему или сознательно ограничиваете размер буфера под нестандартную сетевую топологию с маленьким MTU.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →