Лимит в 10 DNS-запросов SPF: flattening, оптимизация include и почему PermError убивает доставку
Один лишний include — и вся почта домена падает в спам, хотя в панели DNS «всё зелёное». Разбираем жёсткий потолок в 10 DNS-запросов из RFC 7208, почему счётчик считается на стороне получателя, чем реально помогает flattening и чем он опасен.
EvilMail Team17 июля 2026 г.11 мин чтения
Ваш SPF выглядит идеально: dig возвращает запись, онлайн-чекер рисует зелёную галочку, DKIM подписывается. А письма всё равно уходят в спам к части получателей, и логи отправляющего сервера чисты. Проверять в такой ситуации нужно не число символов в записи, а число DNS-запросов, которое она порождает на стороне получателя. Если их больше десяти, приёмник обязан вернуть PermError, и для Gmail, Outlook и Yahoo это равнозначно отсутствию SPF.
Это не теоретический риск. Один добавленный include: от новой CRM или тикет-системы переводит домен из рабочего состояния в тихо ломающееся — без единой ошибки в вашей инфраструктуре.
Что именно считается: анатомия лимита в 10 запросов
RFC 7208 §4.6.4 задаёт жёсткое правило: при вычислении SPF допускается не более 10 механизмов и модификаторов, требующих обращения к DNS. К ним относятся:
include
SPF flattening и лимит 10 DNS-запросов: как не поймать PermError | evilmail.pro — EvilMail Blog
a
mx
ptr
exists
модификатор redirect
Не считаются к лимиту ip4, ip6 и all — они не порождают резолв. Это ключ ко всей оптимизации: чем больше адресов вы выражаете напрямую через ip4/ip6, тем дешевле запись.
Три момента, о которые спотыкаются чаще всего.
Счётчик рекурсивный и глобальный. Каждый include раскрывается в чужую запись, и все её вложенные механизмы тратятся из вашего общего бюджета в 10, а не из какого-то локального. include:_spf.google.com — это не «1 запрос», а поддерево, которое само по себе съедает 3–4 запроса, раскрываясь в _netblocks.google.com, _netblocks2, _netblocks3.
`mx` дороже, чем кажется. Сам механизм mx стоит один запрос из десяти — но за ним прячется отдельный подлимит: раскрытие MX-хостов в A/AAAA не должно порождать больше 10 адресных запросов, иначе снова PermError. Добавьте сюда лишнюю латентность и зависимость от чужой MX-зоны, которую вы не всегда контролируете. Если у почтового сервера статический адрес, mx не нужен вовсе — прямой ip4 дешевле и предсказуемее.
Второй, забытый лимит — void lookups. Тот же §4.6.4 разрешает не более 2 запросов, вернувших NXDOMAIN или NODATA. Третий «пустой» ответ — снова PermError. Это убивает домены особенно коварно: старый include: на удалённый поддомен провайдера годами возвращает пустоту, счётчик void растёт, и запись ломается без всякого превышения основного лимита.
Почему PermError ломает доставку, а не «понижает балл»
У SPF-проверки семь исходов: none, neutral, pass, fail, softfail, temperror и permerror. Инженеры часто думают, что превышение лимита — это «немного хуже», как softfail. Это не так.
PermError означает «запись синтаксически или семантически невалидна, оценить SPF невозможно». Практически все крупные приёмники трактуют это как отсутствие проходящего SPF. Дальше включается DMARC. Если у вас p=reject или p=quarantine, а DKIM по какой-то причине не выровнен (не подписался, сломалось выравнивание, письмо переслали) — единственная опора, SPF, отсутствует. Результат: DMARC fail → письмо в спам или прямой отказ.
Цепочка выглядит так:
Коварство в том, что отправляющий сервер лимит не проверяет — SPF считает получатель. Поэтому в собственных логах Postfix или почтового шлюза вы не увидите ни строчки про PermError. Единственный источник правды — DMARC-агрегированные отчёты (rua=), где в поле spf начнёт массово появляться permerror. Без DMARC-репортинга домен может ломаться неделями незаметно.
Где утекают лимиты: аудит реальной записи
Разберём типичную «раздутую» запись компании с Google Workspace, маркетинговым ESP, тикет-системой и CRM:
Шесть include выглядят безобидно. Считаем по-настоящему, раскрывая дерево (состояние на 2026 год, цифры у провайдеров плавают):
_spf.google.com → 1 + вложенные _netblocks* = ~4
sendgrid.net → ~3
spf.protection.outlook.com → ~2
mailgun.org → ~2
_spf.salesforce.com → ~2
helpdesk.example.net → ~1
Итого около 14 запросов — запись давно в PermError, хотя любой чекер, который не раскрывает дерево, покажет её «валидной». Раскрыть include вручную можно тем же dig:
Правило простое: не доверяйте зелёной галочке без раскрытого дерева. Считайте не число ваших include, а сумму всех резолвов до листьев.
Include-оптимизация до того, как хвататься за flattening
Flattening — тяжёлая артиллерия с постоянными издержками. Сначала соберите дешёвые победы.
Уберите мёртвых отправителей. Самый частый случай — старый ESP, с которого уже год никто не шлёт. Каждый такой include — это 2–3 запроса впустую и потенциальный источник void lookups, когда провайдер удалит свою зону.
Замените `a`/`mx` на явные `ip4`/`ip6`. Если почтовый сервер имеет статический адрес, mx не нужен — впишите ip4:203.0.113.10 и сэкономьте резолв.
Выбросьте `ptr` целиком. Он официально deprecated (RFC 7208 §5.5): медленный, ненадёжный, зависит от reverse-зоны, которую вы часто не контролируете. Ни одна серьёзная запись в 2026 году не должна содержать ptr.
Делегируйте по поддоменам. Транзакционную почту вынесите на bounce.example.com, маркетинг — на mkt.example.com. Каждый поддомен получает собственный бюджет из 10. Это самый чистый способ масштабироваться: маркетинговый ESP с жирным include больше не конкурирует за лимит с основным доменом.
Часто одной чистки хватает, чтобы уйти с 14 запросов на комфортные 6–7, и flattening вообще не понадобится.
SPF flattening: как это работает и чем платишь
Механика прямолинейна: рекурсивно раскрыть все include в плоский список ip4/ip6 и опубликовать одну запись, где DNS-резолвов остаётся 1–2 (или ноль, если убрать вообще все механизмы с резолвом).
Честный разбор издержек, потому что flattening — это компромисс, а не серебряная пуля:
Вы берёте на себя чужие IP-диапазоны. Когда SendGrid или Google меняют пул, ваша плоская запись протухает. Их include обновится сам — ваш список нет. Почта с новых адресов начнёт получать fail.
Нужна автоматизация, иначе не беритесь. Flattening без cron-скрипта, который ежедневно переразворачивает диапазоны и сравнивает их с опубликованными, — это бомба замедленного действия.
Лимит длины записи. Одна TXT-строка ≤ 255 байт; запись можно собрать из нескольких строк, но ответ должен комфортно помещаться в UDP/EDNS-пакет — на практике держите её в пределах ~450 байт. Это всего пара десятков CIDR-блоков. Провайдер вроде Amazon SES с сотнями диапазонов в один плоский SPF просто не влезет.
Вывод: flattening оправдан для стабильных источников (собственные статические IP, провайдеры с редко меняющимися диапазонами) и опасен для SaaS с активной ротацией пула. Не флаттеньте Google «на всякий случай» — оставьте его как include, если бюджет позволяет.
Практика: собираем устойчивую запись
Порядок действий, который работает.
1.Инвентаризация всех реальных отправителей — кто фактически слал за последние 90 дней (смотрите DMARC-отчёты).
2.Делегирование по поддоменам для жирных ESP.
3.Замена a/mx/ptr на прямые ip4/ip6.
4.Flattening только для стабильных провайдеров.
5.Мониторинг счётчика и диапазонов.
Скрипт для аудита — грубо считает резолвы и сигналит при приближении к лимиту:
bash
#!/usr/bin/env bash
# spf-count.sh <domain> — грубый рекурсивный счётчик DNS-механизмов SPF
count=0
walk() {
local rec
rec=$(dig +short TXT "$1" | tr -d '"' | grep -m1 'v=spf1')
for tok in $rec; do
case "$tok" in
include:*) count=$((count+1)); walk "${tok#include:}";;
redirect=*) count=$((count+1)); walk "${tok#redirect=}";;
a|mx|ptr|a:*|mx:*|exists:*) count=$((count+1));;
esac
done
}
walk "$1"
echo "DNS-механизмов: $count"
[ "$count" -ge 10 ] && echo "ВНИМАНИЕ: >= 10 → риск PermError"
Пример устойчивой финальной записи с двумя lookups: собственная инфраструктура выражена через ip4, Google оставлен как include ради автообновления:
По поводу all: для устоявшегося домена используйте -all (hardfail). ~all (softfail) держите только на время отладки flattening, когда есть риск, что вы упустили диапазон. И два правила без исключений: никогда `+all` (он разрешает отправку с любого IP — открытая дверь для спуфинга) и никогда два механизма `all` в одной записи — второй игнорируется, но такая запись выдаёт непонимание того, что оценка идёт слева направо до первого совпадения.
Чек-лист перед публикацией SPF
Запись начинается ровно с v=spf1 и она единственная TXT-запись с этим префиксом на домене.
Сумма всех DNS-механизмов (с раскрытием вложенных include) ≤ 10 — проверено скриптом, а не чекером с галочкой.
Не более 2 механизмов, способных вернуть NXDOMAIN/NODATA (void lookups).
Нет ptr — удалён как deprecated.
Нет +all; ровно один all в конце; для прода -all, на отладке ~all.
Каждый include соответствует реально отправляющему сервису; мёртвые убраны.
Жирные ESP вынесены на поддомены со своим бюджетом.
Для flattened-диапазонов настроен cron, сверяющий актуальные IP провайдера с опубликованными.
Длина записи в разумных пределах (~450 байт), помещается в UDP/EDNS-ответ.
Настроен DMARC с rua= — единственный способ увидеть permerror глазами получателя.
Для временной почты и mail-инфраструктуры цена ошибки выше, чем для обычного корпоративного домена: PermError бьёт не по одному письму, а по репутации всего IP-пула. Поэтому на evilmail.pro мы держим SPF плоским для стабильных источников, отслеживаем счётчик резолвов в CI и мониторим DMARC-отчёты — не потому что это модно, а потому что тихо протухший include стоит дороже, чем скрипт, который его ловит.