Маршрутизация почты через transport_maps в Postfix: домены, smart host и nexthop
Один Postfix, десяток доменов, и каждый должен уезжать по-своему: часть на локальный Dovecot по LMTP, часть на партнёрский релей на порту 2525, часть по голому IP без MX, а весь остальной исходящий — через smart host провайдера. Разбираем точный порядок разрешения nexthop, синтаксис transport:nexthop и главный практический инсайт: почему квадратные скобки решают 80% проблем «письмо ушло не туда».
EvilMail Team12 июля 2026 г.10 мин чтения
Одна очередь, разные пункты назначения
У вас один Postfix, а на нём висит example.com, partner.org, crm.internal и ещё восемь доменов. По умолчанию сервер обращается с каждым из них одинаково: делает MX-lookup и стучится на 25 порт. Но partner.org слушает 2525, crm.internal вообще нет в публичном DNS, а legacy.corp живёт на голом IP 10.0.5.20 без единой MX-записи. Первое, что все пробуют, — relayhost. И тут же всё ломается: relayhost
Postfix transport_maps: маршрутизация доменов, smart host и nexthop — EvilMail Blog
гонит вообще весь исходящий поток в одну трубу, включая ту почту, что должна была уйти напрямую.
Нужна не «одна труба», а таблица маршрутов: этот домен — по LMTP в локальный Dovecot, тот — на партнёрский релей по нестандартному порту, третий — по IP, а весь остальной исходящий — через smart host провайдера. Ровно это и делает transport_maps. По сути это маленький язык маршрутизации, который переопределяет и транспорт, и nexthop ещё до того, как Postfix посмотрит на MX.
Как Postfix решает, куда доставить письмо
Маршрутизацию считает trivial-rewrite(8). Для каждого получателя он вычисляет пару (транспорт, nexthop) в строго фиксированном порядке, и первое совпадение выигрывает:
`sender_dependent_transport_maps` — поиск по адресу отправителя. Если совпало, дальше можно не смотреть.
`transport_maps` — поиск по получателю: сначала полный user@domain, затем домен, затем родительские .домены.
`relay_domains` — если домен получателя входит в список, применяется relay_transport (по умолчанию relay), иначе — default_transport (по умолчанию smtp).
`relayhost` — самый последний fallback, и только для стандартного smtp-транспорта.
Ключевой момент, ради которого всё и затевается: transport_maps переопределяет и транспорт, и nexthop. Поэтому он сильнееrelayhost. Если для домена нашлась строка в transport-таблице, relayhost для него уже не сработает — письмо пойдёт туда, куда сказала таблица.
Синтаксис таблицы: transport:nexthop
Каждая строка таблицы — это ключ и транспорт:nexthop:
domain transport:nexthop
Обе половины правой части опциональны, и в этом вся гибкость:
`:[gateway]:587` — транспорт пустой (наследуется default_transport), но nexthop переопределён.
`lmtp:unix:private/dovecot-lmtp` — и то, и другое задано явно.
Встроенные транспорты, которые вам реально понадобятся: smtp, relay (тот же SMTP, но с раздельными настройками и лимитами для пересылки), lmtp, local, error (отбивает письмо с указанным DSN-кодом), discard (молча удаляет письмо), retry (всегда откладывает доставку до следующей попытки).
Порядок поиска ключа: сначала полный user@domain, потом domain, потом родительские домены с ведущей точкой. example.com матчит только сам example.com. Чтобы поймать поддомены, нужна отдельная строка .example.com — либо включённый parent_domain_matches_subdomains (по умолчанию в него transport_maps не входит, и это правильно: явные строки предсказуемее).
Разводим домены по разным хостам
Вот рабочий /etc/postfix/transport под весь наш зоопарк:
Обратите внимание на последнюю строку: полноадресный ключ [email protected] выигрывает у доменного example.com, так что письма конкретному получателю уедут через кастомный транспорт smart, а всё остальное по домену — на mx.example.com.
postmap обязателен после каждой правки текстового файла: Postfix читает бинарный .db, а не сам текст. Забудете — сервер продолжит работать по старой таблице, и вы полдня будете искать призрак.
nexthop и MX: почему квадратные скобки решают всё
Это причина примерно 80% тикетов «письмо ушло не туда». Разница:
smtp:mail.partner.org — Postfix делает MX-lookup по имени mail.partner.org. MX-записи у хоста обычно нет, Postfix откатывается на его A-запись и стучится на порт 25 — не туда и не на тот порт.
smtp:[mail.partner.org]:2525 — квадратные скобки говорят «это литеральный хост, MX не искать, резолвь только A/AAAA», а :2525 фиксирует порт.
Правило простое: для smart host, внутренних хостов и IP-адресов скобки обязательны. MX-резолв нужен только когда вы действительно пересылаете на чужой почтовый домен и хотите уважать его MX-приоритеты — тогда пишете relay:partner.org без скобок.
Заодно про relay vs smtp. Функционально это один и тот же SMTP-клиент, но relay имеет отдельные счётчики конкурентности и лимиты, и Postfix ждёт, что домены, которые вы пересылаете, перечислены в relay_domains. Это защита от превращения в открытый релей: без записи в relay_domains внешний домен получателя отвалится с Relay access denied.
Smart host: весь исходящий через один шлюз
Когда собственный IP без PTR, провайдер режет 25 порт, или нужен чистый IP ради доставляемости, весь исходящий заворачивают на шлюз провайдера. Простейший способ — relayhost (он же покрывает всё, что не перехватил transport_maps):
smtp_tls_security_level = encrypt здесь не косметика: без него SASL-логин и пароль уедут по plaintext, если сервер согласится на нешифрованную сессию. encrypt требует STARTTLS и отваливается, если TLS не поднялся.
Маршрутизация по отправителю
Отдельная суперспособность — разводить потоки по адресу From, а не получателя. Транзакционные письма (сбросы паролей, чеки) и маркетинговые рассылки должны уходить с разных IP, чтобы жалобы на рассылку не топили репутацию транзакционки.
sender_dependent_transport_maps проверяется первым, раньше transport_maps, поэтому sender-based правило выигрывает. Это ровно то, что нужно: решение «с какого IP уходить» принимается по отправителю, до того как отработает доменная маршрутизация получателя.
Свои транспорты в master.cf
Чтобы транспорт smart из примеров реально существовал, его определяют в master.cf поверх штатного smtp-агента:
smtp_bind_address прибивает исходящий IP — тот самый, для которого вы настроили PTR и SPF. syslog_name=postfix-smart — не мелочь: в логах вы будете грепать именно по нему, чтобы отделить этот поток от остальных. Лимиты задаются в main.cf по имени транспорта:
Так вы не сожжёте репутацию нового IP залпом — rate_delay = 1s держит паузу между доставками на один домен получателя. Пер-домейн TLS-политику при желании выносят отдельно:
`postmap -q "ключ" hash:/etc/postfix/transport` — прогоните по всем вариантам ключа: полный адрес, домен, .поддомен. Смотрите, что реально возвращает таблица, а не что вы думаете, что там написано.
`postconf transport_maps` — убедитесь, что Postfix вообще видит вашу таблицу и путь совпадает.
`grep 'relay=' /var/log/mail.log` — в строке доставки relay= показывает фактический хост и порт, status= — исход. Это истина в последней инстанции.
`grep postfix-smart /var/log/mail.log` — по syslog_name вытаскиваете один транспорт из общего шума.
`postqueue -p` и `postfix flush` — посмотреть застрявшее и пнуть очередь после исправления.
Топ граблей: забыли postmap после правки; написали хост без скобок и получили паразитный MX-lookup; ждали relayhost, а его перебил transport_maps; правили main.cf/master.cf, но сделали postmap вместо postfix reload (или наоборот). Помните: текстовые таблицы → `postmap`; параметры в `main.cf`/`master.cf` → `postfix reload`.
Чеклист внедрения
Сделайте бэкап: cp /etc/postfix/transport{,.bak}.
Впишите строки в /etc/postfix/transport, для хостов и IP — всегда в [скобках].
Соберите базу: postmap /etc/postfix/transport.
Проверьте каждый ключ через postmap -q — полный адрес, домен и родительский .домен.
Убедитесь, что все внешние домены, для которых вы принимаете и пересылаете почту, есть в relay_domains — иначе Relay access denied.
Примените: postfix reload.
Отправьте тестовое письмо на каждый маршрут.
Проверьте в mail.log поля relay= и status=, для кастомного транспорта — по syslog_name.
Для нового исходящего IP заранее проверьте PTR и SPF, иначе smart host не спасёт от спам-папки.
transport_maps перестаёт быть загадкой в тот момент, когда вы принимаете одну вещь: это не свалка исключений, а таблица маршрутизации с чётким приоритетом. Разберитесь один раз в порядке разрешения nexthop и в том, что делают квадратные скобки, — и десяток доменов на одном сервере превращается из головной боли в десять предсказуемых строк.