SPF flattening: как обойти лимит в 10 DNS-запросов
У SPF есть архитектурное ограничение, о котором многие узнают в самый неподходящий момент: максимум 10 DNS-запросов на одну проверку. Подключили Google Workspace, SendGrid, HubSpot, Zendesk, Intercom - и лимит исчерпан. Одиннадцатый include ломает всю запись. Письма перестают проходить SPF-проверку. Flattening решает эту проблему - но создаёт новые.
Откуда взялся лимит в 10 запросов
RFC 7208 (спецификация SPF) устанавливает жёсткое правило: при обработке SPF-записи принимающий сервер не должен выполнять более 10 DNS-запросов с разрешением имён. Сюда входят механизмы include, a, mx, redirect и exists. Не входят ip4 и ip6 - они не требуют дополнительных DNS-обращений.
Цель ограничения - защита от amplification-атак. Без лимита злоумышленник мог бы создать SPF-запись со ста вложенными include, и каждая проверка письма превращалась бы в сотню DNS-запросов. Это открывает путь для DDoS через DNS.
Проблема в том, что ограничение писалось в эпоху, когда компания отправляла почту с одного-двух серверов. Сейчас средний бизнес использует 5-8 сервисов, каждый из которых требует свой include. А каждый include может содержать вложенные include.
Как считаются DNS-запросы
Распространённая ошибка - считать только верхнеуровневые include. На самом деле подсчёт рекурсивный. Рассмотрим запись:
v=spf1 include:_spf.google.com # 1 запрос include:sendgrid.net # 1 запрос include:spf.protection.outlook.com # 1 запрос -all
Три include - три запроса? Нет. Загляните внутрь _spf.google.com:
$ dig TXT _spf.google.com +short
"v=spf1 include:_netblocks.google.com
include:_netblocks2.google.com
include:_netblocks3.google.com ~all"Один include на Google превращается в четыре DNS-запроса (сам _spf.google.com плюс три вложенных netblock-а). SendGrid добавляет ещё 1-3. Outlook - ещё 2-4. Три строки в вашей SPF-записи легко съедают 8-11 из 10 допустимых запросов.
Проверить текущее количество можно командой или через онлайн-сервис:
# С помощью mxtoolbox # https://mxtoolbox.com/spf.aspx # Или через CLI-утилиту spfcheck (Python) pip install pyspf spfquery -ip 1.2.3.4 -sender user@yourdomain.com -helo yourdomain.com
Что происходит при превышении лимита
Результат SPF-проверки становится permerror. Не softfail, не neutral, а permanent error. Это означает, что принимающий сервер не может вынести решение по SPF. На практике большинство провайдеров трактуют permerror как провал проверки.
Последствия: DMARC alignment по SPF не пройдёт. Если DKIM тоже не настроен или сломан - письмо провалит DMARC целиком. Gmail, Yahoo, Outlook - все отреагируют негативно. В лучшем случае письмо уйдёт в спам. В худшем - будет отклонено на уровне SMTP.
По нашей статистике, около 15% доменов, которые проходят через uChecker, имеют SPF-записи с превышением лимита. Владельцы часто не подозревают о проблеме, потому что DKIM продолжает работать и частично компенсирует провал SPF.
Что такое SPF flattening
Идея простая: вместо вложенных include, которые требуют цепочку DNS-запросов, вы подставляете конечные IP-адреса напрямую. Механизмы ip4 и ip6 не считаются как DNS-запросы. Вы разворачиваете дерево include в плоский список IP-диапазонов - и лимит перестаёт быть проблемой.
До flattening:
v=spf1 include:_spf.google.com include:sendgrid.net
include:mail.zendesk.com include:spf.mandrillapp.com
include:servers.mcsv.net -all
# Итого: 12-14 DNS-запросов. SPF сломан.После flattening:
v=spf1 ip4:35.190.247.0/24 ip4:64.233.160.0/19
ip4:66.102.0.0/20 ip4:66.249.80.0/20
ip4:72.14.192.0/18 ip4:74.125.0.0/16
ip4:108.177.8.0/21 ip4:173.194.0.0/16
ip4:209.85.128.0/17 ip4:216.58.192.0/19
ip4:216.239.32.0/19
ip4:167.89.0.0/17 ip4:168.245.0.0/17
... -all
# Итого: 0 DNS-запросов. SPF валиден.Выглядит как решение всех проблем. Но есть нюансы.
Подводные камни flattening
1. IP-адреса меняются
Google, SendGrid, Mailgun и другие провайдеры периодически добавляют, убирают и перераспределяют IP-адреса своих почтовых серверов. Если вы «расплющили» запись один раз и забыли - через месяц-два часть IP может устареть, а новые адреса не будут включены в вашу запись. Письма, отправленные с новых IP, провалят SPF.
Это главный риск. Flattening превращает динамическую систему (include разрешается при каждой проверке) в статический снимок. Снимок протухает.
2. Длина TXT-записи
Одна TXT-запись в DNS ограничена 255 символами на строку. DNS-серверы позволяют объединять несколько строк в одну запись, но общая длина всё равно ограничена - обычно 4096 символов. Развёрнутая SPF-запись крупной компании может легко превысить этот порог. Решение - разбить на несколько TXT-записей через поддомены (об этом ниже).
3. Потеря прозрачности
Запись с include легко читать. Видно: Google, SendGrid, Zendesk. Запись с десятками CIDR-диапазонов - нет. Через полгода вы не вспомните, какие IP принадлежат Zendesk, а какие - SendGrid. Отладка превращается в ад.
Способ 1: ручной flattening
Подходит для малых команд с 2-3 сервисами отправки и если вы готовы раз в месяц проверять актуальность IP. Алгоритм:
- Для каждого include рекурсивно получите все IP-диапазоны.
- Замените include на набор
ip4/ip6в SPF-записи. - Если запись слишком длинная - разбейте через поддомены.
- Настройте напоминание проверять IP раз в 2-4 недели.
Bash-скрипт для извлечения IP из include:
#!/bin/bash
# resolve-spf.sh — рекурсивно извлекает IP из SPF-записи
# Использование: ./resolve-spf.sh _spf.google.com
resolve_spf() {
local domain="$1"
local record
record=$(dig TXT "$domain" +short | tr -d '"' | tr ' ' '\n')
for token in $record; do
case "$token" in
include:*)
resolve_spf "${token#include:}"
;;
ip4:*|ip6:*)
echo "$token"
;;
a|a:*)
# Механизм 'a' тоже потребляет DNS-запрос —
# здесь его нужно разрешить в IP
local host="${token#a:}"
[ "$host" = "a" ] && host="$domain"
dig A "$host" +short | while read -r ip; do
echo "ip4:$ip"
done
;;
esac
done
}
echo "# IP-адреса для: $1"
resolve_spf "$1" | sort -u$ ./resolve-spf.sh _spf.google.com # IP-адреса для: _spf.google.com ip4:35.190.247.0/24 ip4:64.233.160.0/19 ip4:66.102.0.0/20 ip4:66.249.80.0/20 ip4:72.14.192.0/18 ip4:74.125.0.0/16 ip4:108.177.8.0/21 ip4:130.211.0.0/22 ip4:172.217.0.0/19 ip4:173.194.0.0/16 ip4:209.85.128.0/17 ip4:216.58.192.0/19 ip4:216.239.32.0/19 ip6:2001:4860:4000::/36 ip6:2404:6800:4000::/36 ip6:2607:f8b0:4000::/36 ip6:2800:3f0:4000::/36 ip6:2a00:1450:4000::/36 ip6:2c0f:fb50:4000::/36
Проблема очевидна: один Google - это 19 записей. Добавьте SendGrid, Mailchimp, HubSpot - и TXT-запись разрастается до нечитаемого размера. Нужен более структурированный подход.
Способ 2: разделение через поддомены
Вместо одной гигантской записи вы создаёте несколько SPF-записей на поддоменах и ссылаетесь на них из основной. Каждый include на свой поддомен - это один DNS-запрос, но внутри поддомена вы используете только ip4/ ip6, которые не считаются.
# Основная запись — yourdomain.com
v=spf1 include:_spf1.yourdomain.com
include:_spf2.yourdomain.com -all
# 2 DNS-запроса
# _spf1.yourdomain.com — Google + SendGrid (только IP)
v=spf1 ip4:35.190.247.0/24 ip4:64.233.160.0/19
ip4:66.102.0.0/20 ip4:66.249.80.0/20
ip4:72.14.192.0/18 ip4:74.125.0.0/16
ip4:167.89.0.0/17 ip4:168.245.0.0/17 -all
# _spf2.yourdomain.com — HubSpot + Zendesk (только IP)
v=spf1 ip4:192.92.128.0/21 ip4:185.28.196.0/22
ip4:104.16.0.0/14 ip4:192.161.144.0/20 -allИтого: 2 DNS-запроса вместо 12-14. Каждый поддомен содержит только IP-диапазоны. Структура понятная: один поддомен на группу сервисов. Легко обновлять по отдельности.
Способ 3: автоматический flattening
Ручной подход не масштабируется. Если у вас больше трёх сервисов отправки, нужна автоматизация: скрипт или сервис, который регулярно разрешает все include, сравнивает с текущей записью и обновляет DNS при изменениях.
Python-скрипт для автоматического flattening через Cloudflare API:
import dns.resolver
import requests
import hashlib
CF_API = "https://api.cloudflare.com/client/v4"
CF_TOKEN = "YOUR_TOKEN"
ZONE_ID = "YOUR_ZONE_ID"
DOMAIN = "yourdomain.com"
# Сервисы, чьи include нужно «расплющить»
INCLUDES = [
"_spf.google.com",
"sendgrid.net",
"mail.zendesk.com",
"spf.mandrillapp.com",
]
def resolve_ips(domain: str, seen: set = None) -> list[str]:
"""Рекурсивно извлекает ip4/ip6 из SPF-записи."""
if seen is None:
seen = set()
if domain in seen:
return []
seen.add(domain)
ips = []
try:
answers = dns.resolver.resolve(domain, "TXT")
except Exception:
return []
for rdata in answers:
txt = rdata.to_text().strip('"')
for token in txt.split():
if token.startswith("ip4:") or token.startswith("ip6:"):
ips.append(token)
elif token.startswith("include:"):
ips.extend(resolve_ips(token[8:], seen))
return ips
def build_flattened_record() -> str:
all_ips = []
for inc in INCLUDES:
all_ips.extend(resolve_ips(inc))
unique = sorted(set(all_ips))
return f"v=spf1 {' '.join(unique)} -all"
def update_cloudflare(record_content: str):
headers = {
"Authorization": f"Bearer {CF_TOKEN}",
"Content-Type": "application/json",
}
# Получить текущую запись
r = requests.get(
f"{CF_API}/zones/{ZONE_ID}/dns_records"
f"?type=TXT&name={DOMAIN}",
headers=headers,
)
records = r.json()["result"]
spf_record = next(
(rec for rec in records if rec["content"].startswith("v=spf1")),
None,
)
if not spf_record:
print("SPF-запись не найдена")
return
if spf_record["content"] == record_content:
print("Изменений нет")
return
# Обновить
requests.put(
f"{CF_API}/zones/{ZONE_ID}/dns_records/{spf_record['id']}",
headers=headers,
json={
"type": "TXT",
"name": DOMAIN,
"content": record_content,
},
)
print(f"SPF обновлён: {record_content[:80]}...")
if __name__ == "__main__":
flat = build_flattened_record()
print(f"Собрано IP-диапазонов: {flat.count('ip4:') + flat.count('ip6:')}")
update_cloudflare(flat)Запускайте по cron раз в сутки. Скрипт сам обнаружит изменения и обновит запись. Добавьте отправку уведомления (Slack, email) при каждом обновлении - так вы будете знать, когда провайдер поменял свои IP.
# crontab -e 0 6 * * * /usr/bin/python3 /opt/scripts/spf-flatten.py >> /var/log/spf-flatten.log 2>&1
Готовые сервисы для SPF flattening
Если не хотите писать и поддерживать скрипты сами, есть специализированные решения. Они автоматически мониторят исходные include, обновляют DNS и уведомляют об изменениях.
- AutoSPF / SPF Optimizer - делегируете SPF на их поддомен, они автоматически поддерживают актуальный плоский список IP. Минимум усилий, но вы зависите от стороннего сервиса.
- dmarcian SPF Flattening - встроен в DMARC management. Если уже пользуетесь dmarcian для отчётов - удобно держать всё в одном месте.
- EasyDMARC - аналогичная функция плюс мониторинг и алерты.
- PowerSPF (от PowerDMARC) - динамический flattening с макросами SPF. Вместо IP использует
exists-механизм для проверки через свои серверы. Один DNS-запрос вместо десяти.
Выбор зависит от вашей толерантности к зависимости от третьих сторон. Для критичной инфраструктуры мы рекомендуем собственный скрипт с мониторингом. Для среднего бизнеса - платный сервис с SLA.
Альтернатива: SPF-макросы
Менее известный, но мощный подход. SPF поддерживает макросы - переменные, которые подставляются при проверке. Механизм exists позволяет вынести проверку на внешний DNS-сервер, который вы контролируете:
v=spf1 exists:%{i}._spf.yourdomain.com -allЗдесь %{i} подставляется IP-адресом отправителя. Принимающий сервер делает A-запрос к 1.2.3.4._spf.yourdomain.com. Если запись существует (возвращает любой A-ответ) - SPF пройден. Один DNS-запрос. Вы управляете зоной _spf.yourdomain.com и создаёте A-записи для каждого разрешённого IP.
Этот подход используют PowerDMARC, ValiMail и некоторые крупные компании с собственным DNS. Основной минус - требует отдельную DNS-зону с автоматическим управлением, что не каждый DNS-провайдер поддерживает удобно.
Чеклист: flattening без рисков
- Посчитайте текущее количество DNS-запросов. MXToolbox или
dmarcian.com/spf-survey. Если меньше 10 - flattening не нужен. Не чините то, что работает. - Составьте карту include. Запишите, какие сервисы стоят за каждым include. Это пригодится при отладке.
- Выберите подход: ручной, скрипт или сервис. Для 4+ сервисов ручной не подходит.
- Тестируйте перед деплоем. Отправьте тестовые письма через все каналы, проверьте SPF pass в заголовках.
- Настройте мониторинг. Проверяйте SPF-статус раз в сутки. DMARC-отчёты (rua) покажут провалы раньше, чем вы заметите падение open rate.
- Не убирайте DKIM. Flattening не заменяет DKIM. Даже если SPF идеален, DKIM остаётся вторым фактором для DMARC alignment и единственным способом подтвердить целостность письма.
- Документируйте изменения. Через год вы забудете, почему в SPF прописаны именно эти IP. Храните маппинг «IP-диапазон → сервис» рядом с конфигурацией.
Когда flattening не нужен
Прежде чем усложнять инфраструктуру, проверьте: может быть, проблема решается проще.
- Удалите лишние include. Отключили Mailchimp полгода назад, а include остался? Уберите. Каждый неиспользуемый include - бессмысленный расход лимита.
- Используйте поддомены для разных каналов. Маркетинг шлёт с
mail.yourdomain.com, транзакции - сnotify.yourdomain.com. Каждый поддомен - своя SPF-запись, свой лимит в 10 запросов. - Убедитесь, что DKIM настроен. Если DKIM работает корректно, провал SPF не фатален для DMARC. DMARC требует прохождения хотя бы одного из двух протоколов. Flattening ради flattening не имеет смысла, если DKIM покрывает все каналы.
Итого
SPF flattening - инструмент, а не серебряная пуля. Он решает конкретную техническую проблему: превышение лимита DNS-запросов. Но создаёт новую задачу: поддержание актуальности IP-адресов. Выбирайте подход по размеру компании: до 3 сервисов - обойдётесь поддоменами. 4-6 - скрипт с cron. Больше - платный сервис с автоматическим обновлением.
И помните: SPF - только один элемент доставляемости. Безупречная SPF-запись не спасёт, если база на треть состоит из невалидных адресов. Bounce rate уничтожит репутацию домена быстрее, чем любой permerror.
Настроили SPF - проверьте базу. Hard bounce, спам-ловушки, мёртвые ящики - всё это вредит репутации не меньше, чем сломанная SPF-запись. Загрузите список в uChecker - за минуты увидите реальное состояние базы и сможете убрать рискованные адреса до следующей рассылки.
