SPF permerror: лимит в 10 DNS-запросов и как его исправить
Почему SPF возвращает permerror после 10 DNS-запросов, какие механизмы считаются, как посчитать свою запись вручную и какие исправления живут дольше уплощения.
Опубликовано: · 7 мин чтения
При одной проверке SPF-записи может быть вычислено не более десяти механизмов, требующих DNS-запроса. Считаются include, a, mx, exists, redirect и ptr, а также всё, что находится внутри вложенных include. ip4, ip6 и all ничего не стоят. Одиннадцатый запрос завершает проверку результатом permerror, а permerror — это не pass: с точки зрения DMARC SPF просто не пройден. Долговечное решение — оставить в записи меньше сервисов, а не прятать их изобретательнее.
Правило установлено в RFC 7208, раздел 4.6.4.
Что считается, а что нет
| Механизм | Идёт в счёт десяти? | Примечание |
|---|---|---|
include: |
Да | Плюс каждый считаемый механизм внутри включённой записи, рекурсивно |
a |
Да | В том числе a:host.example.com |
mx |
Да | Имеет дополнительный собственный лимит, см. ниже |
exists: |
Да | Используется в основном с макросами |
redirect= |
Да | Это модификатор, но он вызывает запрос |
ptr |
Да | Признан устаревшим самим RFC; удалите его |
ip4:, ip6: |
Нет | Адреса записаны явно, DNS не нужен |
all |
Нет | |
exp= |
Нет | Запрашивается только для построения текста пояснения после отказа |
Лимит относится к одной проверке SPF целиком. Первоначальное получение вашей собственной TXT-записи не считается; считается всё, что эта запись затем вызывает.
Ещё два лимита из того же раздела
- Разрастание
mxиptr. Вычисление одного механизмаmxне должно приводить более чем к десяти запросам адресов для возвращённых MX-имён (тот же предел действует для имён, найденных черезptr). Домен с более чем десятью MX-хостами получает permerror через эту дверь, даже если общее число механизмов невелико. - Пустые запросы. Запрос, который возвращает NXDOMAIN или пустой ответ, называется «пустым» (void lookup). RFC говорит, что получатели SHOULD допускать не больше двух таких запросов на проверку и возвращать permerror сверх того. Двух мёртвых include, оставшихся от отключённых сервисов, может оказаться достаточно.
И классика, не связанная со счётом
Две TXT-записи, начинающиеся с v=spf1, на одном имени — тоже permerror. Так бывает, когда инструкция нового поставщика говорит «добавьте эту SPF-запись», и кто-то делает ровно это. Объедините механизмы в одну запись.
Почему сбои выглядят случайными
Permerror означает «эту запись невозможно интерпретировать». Что с этим делать, каждый получатель решает сам: одни трактуют его как fail, другие как neutral. DMARC спрашивает только одно: дал ли SPF выровненный pass, а permerror — не pass. Если письмо несёт ещё и выровненную подпись DKIM, DMARC всё равно проходит, и сломанную SPF-запись месяцами никто не замечает. Потом письмо без DKIM (забытый сервер приложения, сканер) начинает возвращаться у одного провайдера и доходить у другого.
Порядок механизмов добавляет путаницы. SPF вычисляется слева направо и останавливается на первом совпадении. Отправитель, совпавший со вторым механизмом, никогда не доходит до одиннадцатого запроса; отправитель, указанный в конце записи, доходит. Поэтому одна и та же запись может проходить для вашего почтового провайдера и давать permerror для системы выставления счетов.
Посчитайте свою запись вручную
Начните с самой записи:
$ dig +short TXT example.com | grep spf1
"v=spf1 mx a include:_spf.mailprovider.example include:spf.newsletter.example include:spf.crm.example include:spf.helpdesk.example ip4:192.0.2.10 -all"
Затем пройдите по каждому include и по каждому include внутри них:
$ dig +short TXT _spf.mailprovider.example
"v=spf1 include:_netblocks1.mailprovider.example include:_netblocks2.mailprovider.example include:_netblocks3.mailprovider.example ~all"
$ dig +short TXT _netblocks1.mailprovider.example
"v=spf1 ip4:198.51.100.0/24 ip6:2001:db8:10::/48 ~all"
Выпишите по строке на каждый считаемый механизм:
Механизм в example.com |
Своя стоимость | Вложенная стоимость | Итого |
|---|---|---|---|
mx |
1 | 0 | 1 |
a |
1 | 0 | 1 |
include:_spf.mailprovider.example |
1 | 3 include | 4 |
include:spf.newsletter.example |
1 | 1 include | 2 |
include:spf.crm.example |
1 | 1 include + 1 a |
3 |
include:spf.helpdesk.example |
1 | 0 | 1 |
ip4:192.0.2.10 |
0 | 0 | 0 |
| Всего | 12 |
Двенадцать: permerror для любого отправителя, который не совпал раньше. Обратите внимание, что all внутри включённой записи не завершает вашу проверку; include имеет значение только тогда, когда даёт pass.
Исправления в том порядке, в каком их стоит пробовать
1. Уберите то, чем больше не пользуетесь
Большинство записей сверх лимита содержат хотя бы один сервис, от которого отказались годы назад. Каждый удалённый поставщик обычно освобождает от одного до четырёх запросов. Заодно закрывается дыра: include разрешает отправляющим адресам платформы слать почту от вашего домена, а на общей инфраструктуре те же адреса несут почту других клиентов.
2. Замените a и mx адресами
mx и a — удобные значения по умолчанию, которые добавляют многие генераторы. Если ваш веб-сервер не отправляет почту, a бесполезен. Если хосты из ваших MX-записей не отправляют вашу исходящую почту (обычная ситуация при почте у провайдера, где отправку покрывает его include), mx тоже бесполезен. Там, где сервер действительно отправляет и его адрес стабилен, пишите ip4:192.0.2.10 или ip6:2001:db8::25: ноль запросов.
3. Проверьте, использует ли сервис ваш домен в конверте вообще
SPF проверяет домен отправителя конверта (MAIL FROM, позже видимый как Return-Path), а не заголовок From, который видят люди. Многие сервисы рассылок ставят туда собственный домен для возвратов. В этом случае получатель запрашивает их SPF-запись, а вашу — никогда, и их include в вашей записи тратит запросы впустую.
Откройте письмо, которое сервис действительно отправил, и прочитайте заголовки:
Return-Path: <bounce-7f3a@bounces.newsletter.example>
From: Example Shop <news@example.com>
Return-Path не относится к example.com, значит include:spf.newsletter.example из записи корневого домена можно убрать. Такое письмо проходит DMARC благодаря DKIM-подписи с d=example.com или благодаря собственному return-path (следующий шаг).
4. Дайте каждому отправляющему сервису свой поддомен
Лимит действует на одну проверку, а каждая проверка начинается с домена конверта. Отправляйте рассылки с news.example.com, а транзакционные письма с billing.example.com, и у каждого имени будет своя запись и свой бюджет в десять запросов:
news.example.com. TXT "v=spf1 include:spf.newsletter.example -all"
billing.example.com. TXT "v=spf1 include:spf.invoicing.example -all"
Большинство платформ реализуют это как «собственный return-path» или «собственный домен возвратов»: вы создаёте CNAME вроде bounce.news.example.com, указывающий на провайдера, и SPF-запись целиком живёт на его стороне. При нестрогом выравнивании (по умолчанию в DMARC) bounce.news.example.com выровнен с адресом From на example.com. SPF-записи не наследуются, так что у поддомена без собственной записи её нет вовсе.
5. Опирайтесь на выровненный DKIM
DMARC нужен один выровненный pass, SPF или DKIM. Для каждого сервиса, способного подписывать письма с вашим доменом в d=, достаточно одного DKIM, и он переживает пересылку, чего SPF не умеет (см. почему пересылка почты ломает SPF). Именно это делает шаг 3 безопасным.
6. Уплощение, только с автоматизацией
«Уплощение» (flattening) заменяет каждый include теми диапазонами IP-адресов, в которые он разрешается сейчас. Число запросов падает до нуля, и запись работает сегодня. Беда придёт завтра: провайдеры добавляют и выводят из оборота диапазоны, не предупреждая вас, и в тот день часть вашей легитимной почты начнёт проваливать SPF с записью, которая выглядит совершенно корректной. Если уплощать, это должен быть процесс, который регулярно заново разрешает include и публикует запись, и за этим процессом нужно следить. Уплощённая вручную запись — это отложенный сбой.
Уплощённые записи к тому же становятся длинными. Одна символьная строка TXT вмещает не больше 255 байт; более длинные записи разбиваются на несколько строк, которые получатели склеивают без добавления пробелов, так что разрыв в неудачном месте сливает два механизма в один. Держите небольшим и весь ответ целиком: ответы длиннее традиционных 512 байт UDP зависят от EDNS0 или перехода на TCP, и RFC советует оставаться в этих пределах.
7. Макросы
Макросы SPF (exists:%{i}._spf.example.com) могут свернуть множество отправителей в один запрос. Им нужен DNS-бэкенд, построенный специально под это, и их трудно отлаживать: не средство первой линии.
~all против -all ко всему этому не относится: квалификатор у all решает, что происходит с несовпавшими отправителями, и в любом случае не стоит ни одного запроса.
Частые ошибки
- Считать только include, видимые в записи верхнего уровня, и забывать о вложенных.
- Добавлять
include:для сервиса, чей Return-Path находится на его собственном домене. - Оставлять include отключённых сервисов: они тратят запросы, а когда поставщик удалит имя, превращаются в пустые запросы.
- Публиковать вторую запись
v=spf1вместо правки первой. - Разбивать длинную запись на строки по 255 байт и терять пробел между двумя механизмами на стыке.
Если вы составляете запись с нуля, генератор SPF соберёт синтаксис; считать запросы всё равно придётся по живым include.
Проверьте в OrbitProbe
Проверка SPF в OrbitProbe запрашивает SPF-запись домена и отмечает проблемы, которые чаще всего её ломают: больше одной записи, +all, отсутствующий механизм all и слишком много DNS-запросов. Запустите её для корневого домена, а затем отдельно для каждого поддомена, с которого уходит почта, потому что каждое имя проверяется само по себе. DNS-запрос показывает, что опубликовано в этот момент; принимают ли ваши письма, он показать не может, так что продолжайте читать и отчёты DMARC. Общая картина того, как четыре почтовые записи зависят друг от друга, есть в статье MX, SPF, DKIM и DMARC: как они работают вместе, а полный разбор доставки — в чек-листе проверки DNS для писем, попадающих в спам.