mailcow отправляет письма с чужого IP, хотя SMTP_PORT привязан к нужному адресу: ищем SNAT
На сервере с несколькими IP-адресами администратор привязывает SMTP_PORT к нужному адресу и ожидает, что почта mailcow будет уходить именно с него — а письма продолжают отправляться с другого, обычно основного IP сервера. Дело в том, что привязка входящего порта и адрес, с которого сервер отправляет исходящий трафик, — это два разных механизма, и документация mailcow прямо это разделяет. Разбираю, чем IP bindings отличаются от SNAT, как правильно задать исходящий адрес через SNAT_TO_SOURCE и почему netfilter-mailcow — ключевой компонент в этой цепочке.
Симптом: привязали SMTP_PORT к нужному IP, а письма уходят с другого
Типичная ситуация на сервере с корпоративной почтой на mailcow, у которого несколько публичных IP-адресов: администратор хочет, чтобы исходящая почта уходила с определённого адреса — например, с того, на который провайдер настроил PTR-запись с именем почтового сервера, тогда как основной IP сервера занят сайтом или другими сервисами. Он находит в mailcow.conf переменную SMTP_PORT и привязывает её к нужному адресу вида SMTP_PORT=1.2.3.4:25, ожидая, что теперь вся почта Postfix будет отправляться именно с 1.2.3.4.
После применения настройки и перезапуска стека письма продолжают уходить с прежнего, обычно основного адреса сервера — это видно и по заголовкам Received в самом письме, и по тому, какой IP видит принимающий сервер. Возникает ощущение, что настройка не применилась или применилась не полностью, хотя формально всё сделано по инструкции: переменная задана, контейнеры перезапущены, конфиг обновился.
Особенно неприятно это выглядит, когда решение принималось не из абстрактного любопытства, а под конкретную задачу — например, нужно было соответствовать PTR-записи провайдера или увести почту с адреса, который засветился в чёрных списках из-за другого сервиса на том же сервере. Формально письма продолжают доставляться и жалоб от получателей нет, поэтому проблему часто откладывают на потом — а зря, потому что она напрямую влияет на доставляемость: если SPF/PTR настроены под один IP, а письма фактически уходят с другого, часть провайдеров может начать помечать такую почту подозрительной именно из-за расхождения ожидаемого и фактического адреса отправителя.
IP bindings и SNAT — два разных механизма, и документация mailcow это прямо разделяет
Здесь и кроется главная путаница: SMTP_PORT и подобные переменные (IMAP_PORT, HTTPS_PORT и другие из раздела IP bindings) отвечают за то, на каком адресе и порту сервис слушает входящие подключения — то есть на какой IP клиент или другой почтовый сервер должен постучаться, чтобы отдать письмо в mailcow. Это никак не связано с тем, какой исходящий адрес использует маршрутизация почты в Postfix, когда сам сервер отправляет письмо наружу. Документация mailcow формулирует это предельно прямо: «изменение привязки не влияет на source NAT» — то есть смена адреса прослушивания в IP bindings не меняет исходящий (source NAT) адрес ни при каких условиях.
За исходящий адрес отвечает отдельный, самостоятельный механизм — SNAT (Source NAT), который настраивается не через переменные IP bindings, а через собственные параметры SNAT_TO_SOURCE (для IPv4) и SNAT6_TO_SOURCE (для IPv6) в том же mailcow.conf. Это две независимые системы, которые администратор интуитивно ожидает связанными (раз порт слушает на этом IP — значит, и письма должны уходить с него), но на уровне реализации mailcow это разные конфигурационные блоки, обслуживаемые разными частями стека.
Логика такого разделения на самом деле объяснима, если смотреть на сетевой стек, а не на панель настроек: привязка порта — это про то, какой процесс на каком сокете слушает входящие TCP-соединения, а SNAT — это про то, как ядро (точнее, netfilter внутри контейнера) переписывает адрес источника у пакетов, уходящих наружу. Формально это разные уровни сетевого стека, и в большинстве других почтовых серверов (не только mailcow) они тоже настраиваются раздельно — просто в панели mailcow.conf оба параметра лежат рядом в одном файле, и это визуально создаёт иллюзию, что они как-то связаны друг с другом.
Как правильно настроить SNAT_TO_SOURCE
Чтобы задать конкретный исходящий IP-адрес для всего исходящего трафика mailcow, нужно прописать в mailcow.conf строки:
SNAT_TO_SOURCE=1.2.3.4
SNAT6_TO_SOURCE=dead:beef::1где 1.2.3.4 — нужный IPv4-адрес, а SNAT6_TO_SOURCE задаётся аналогично для IPv6, если он используется. После правки конфига изменения нужно применить пересозданием контейнеров:
docker compose up -dПросто перезапуска (docker compose restart) недостаточно: значения из mailcow.conf попадают в контейнер как переменные окружения при его создании, а restart не пересоздаёт контейнер. Документация mailcow указывает именно docker compose up -d — Compose увидит изменившееся окружение и пересоздаст netfilter-mailcow.
Технически за применение этих параметров отвечает контейнер netfilter-mailcow — именно он читает значения SNAT_TO_SOURCE/SNAT6_TO_SOURCE и создаёт правило в цепочке POSTROUTING таблицы nat. Важно, где именно: контейнер работает привилегированным и в сетевом пространстве хоста (network_mode: host), поэтому правило появляется в netfilter самого хоста (через iptables или nftables — в зависимости от бэкенда), а не внутри контейнера. Источником в правиле указана вся внутренняя сеть mailcow (172.22.1.0/24 по умолчанию, переменная IPV4_NETWORK), так что SNAT меняет адрес для всего исходящего трафика стека — Postfix, Rspamd, unbound, ACME, — а не для отдельного домена или отправителя. Развести почту разных доменов по разным IP одним SNAT_TO_SOURCE нельзя. Проверку он повторяет примерно раз в 10 секунд. Документация отдельно отмечает, что этот компонент удерживает свои правила SNAT на первой позиции в цепочке post-routing и пересоздаёт их, если что-то другое (например, ручное вмешательство администратора в netfilter на хосте) сдвигает их с этой позиции — то есть правила спроектированы устойчивыми к дрейфу порядка, но не защищёнными от ручного удаления или конфликта с другим ПО, которое тоже правит netfilter/iptables на том же хосте.
Как проверить, что SNAT реально применился
После применения docker compose up -d стоит не доверять факту отсутствия ошибок, а явно посмотреть, что увидел и сделал netfilter-mailcow. Документация mailcow рекомендует смотреть его лог:
docker compose logs --tail=200 netfilter-mailcowВ выводе должна быть строка вида Added POSTROUTING rule for source network 172.22.1.0/24 to SNAT target 1.2.3.4 — если её нет или есть ошибки, значит, либо переменная задана с опечаткой (например, неверный формат IP), либо контейнер не смог применить правило по другой причине (конфликт с уже существующими правилами на хосте, отсутствие нужных сетевых прав у контейнера).
Финальная проверка — отправить тестовое письмо на внешний ящик, до которого можно достучаться независимо (например, свой ящик на другом почтовом сервисе), и посмотреть заголовки Received в полученном письме либо просто проверить публичный IP, с которого пришло соединение, на принимающей стороне. Если после корректной настройки SNAT_TO_SOURCE письмо по-прежнему уходит со старого адреса, стоит проверить, не установлен ли на хосте (не в контейнере) собственный firewall или NAT-правила, которые перехватывают исходящий трафик раньше, чем успевает сработать SNAT-правило netfilter-mailcow — в таком случае конфликт нужно разрешать на уровне хостовых правил, а не в конфиге mailcow.
Кейс «Компьютерный класс Старт»: рассылка приглашений уходила не с того IP
Клиент — компьютерная школа «Компьютерный класс Старт», 11 рабочих мест, у которой на одном VPS с mailcow, установленным по нашему регламенту, арендованы два публичных IPv4-адреса: основной, на котором живёт сайт школы с формой записи на курсы, и дополнительный, взятый специально под почту — на него хостер прописал PTR-запись с именем почтового сервера, и на него же смотрят MX и SPF. Администратор настроил SMTP_PORT на дополнительный адрес, ожидая, что вся исходящая почта Postfix пойдёт именно с него, но спустя неделю выяснилось, что письма с приглашениями на курсы уходят получателям с основного IP, у которого PTR указывал на имя хостера. Часть писем крупные почтовые сервисы складывали в спам.
Диагностика заняла около получаса: сверили mailcow.conf — там действительно был задан только SMTP_PORT, а переменных SNAT_TO_SOURCE/SNAT6_TO_SOURCE не было вовсе, администратор просто не знал об их существовании и полагал, что привязка порта входящего сервиса автоматически определяет исходящий адрес. Добавили SNAT_TO_SOURCE с нужным дополнительным IP, применили docker compose up -d, проверили лог netfilter-mailcow — правило применилось без ошибок. Тестовое письмо на внешний ящик пришло уже с нужного адреса, отправили несколько реальных приглашений на курсы и убедились по заголовкам Received у получателей, что вся исходящая почта mailcow — и приглашения, и обычная переписка сотрудников — уходит с почтового IP с правильным PTR, а сайт продолжает работать на основном. Через две недели жалобы на попадание приглашений в спам прекратились.
Частая ошибка: SNAT настроили один раз и забыли про постоянство после обновлений
Второй по частоте источник путаницы — когда администратор один раз настроил SNAT_TO_SOURCE во время первичной установки mailcow (обычно по чужой инструкции, скопированной из статьи про установку с нуля), а затем при плановом обновлении mailcow через update-скрипт видит, что письма снова стали уходить с другого адреса. Причина почти всегда прозаичнее, чем кажется: сам mailcow.conf при штатном обновлении не перезаписывается и значения SNAT_TO_SOURCE сохраняются, но если администратор в какой-то момент вручную правил сетевые настройки хоста (например, ставил отдельный firewall или NAT-правила вне mailcow) — конфликт между хостовыми правилами и правилами netfilter-mailcow может проявиться заново после перезапуска стека, даже если конфигурация mailcow не менялась.
Отдельный практический момент: если на сервере параллельно с mailcow работают другие сервисы, которым тоже нужен исходящий трафик с определённого IP (например, отдельный веб-сервис или VPN-шлюз), стоит явно проверить, что их собственные правила SNAT/маршрутизации не пересекаются с диапазоном, который использует netfilter-mailcow. netfilter-mailcow удерживает свои правила на первой позиции в цепочке post-routing и пересоздаёт их при сдвиге — но это защита от собственного дрейфа порядка правил, а не от логического конфликта с другим ПО, которое одновременно пытается управлять NAT для того же трафика на одном и том же хосте. Похожий класс конфликтов — когда UFW не блокирует опубликованный порт Docker: правила самого Docker и правила хостового firewall живут в разных цепочках netfilter и могут противоречить друг другу неочевидным образом.
Чек-лист: разделение входящего и исходящего адреса в mailcow
Если цель — принимать почту на одном адресе, а слушать другие сервисы (веб-интерфейс, IMAP) на других адресах того же сервера, достаточно настроек IP bindings (SMTP_PORT, IMAP_PORT, HTTPS_PORT и аналогичных) — это вопрос входящих подключений, и SNAT здесь не требуется. Если же цель — чтобы исходящая почта уходила с конкретного адреса, правильный путь — SNAT_TO_SOURCE/SNAT6_TO_SOURCE, а не манипуляции с IP bindings, которые на исходящий трафик не влияют вовсе. Помните, что SNAT действует на весь стек mailcow целиком: если нужно отправлять разные домены с разных IP, это уже отдельная задача на уровне транспортов Postfix, и одной переменной в mailcow.conf её не решить.
После любой правки этих параметров — не полагаться на отсутствие ошибок при docker compose up -d, а явно проверить лог netfilter-mailcow и отправить тестовое письмо с проверкой заголовков на принимающей стороне. И держать в голове источник путаницы на будущее: интуитивно кажется, что адрес прослушивания порта и адрес отправки — это одно и то же для одного и того же сервиса, но в mailcow (как и во многих сетевых стеках с несколькими IP) это архитектурно разные вещи — вход и выход настраиваются раздельно.
Если в компании несколько потоков исходящей почты с разными требованиями к репутации — например, транзакционные уведомления клиентам, массовые рассылки и обычная деловая переписка сотрудников, — стоит сразу спланировать, какой поток с какого IP должен уходить (и нужен ли для массовых рассылок вообще отдельный сервер, раз SNAT в mailcow общий на весь стек), и зафиксировать это в документации на инсталляцию, а не держать в голове одного администратора. При смене администратора или при плановом аудите инфраструктуры это первое, что стоит сверить: соответствует ли фактический исходящий IP (по заголовкам реально отправленных писем) тому, что задокументировано, — расхождение здесь обнаруживается не сразу, а только когда получатели начинают жаловаться на доставляемость.
Частые вопросы
Почему письма mailcow уходят не с того IP, если SMTP_PORT привязан к нужному адресу?
SMTP_PORT настраивает только адрес, на котором сервис принимает входящие подключения. Исходящий (source NAT) адрес определяется отдельным механизмом — SNAT, и документация mailcow прямо указывает, что изменение привязки входящего порта не влияет на SNAT.
Какой параметр отвечает за исходящий IP-адрес почты в mailcow?
SNAT_TO_SOURCE для IPv4 и SNAT6_TO_SOURCE для IPv6 — оба задаются в mailcow.conf и применяются пересозданием контейнеров командой docker compose up -d.
Какой контейнер mailcow отвечает за применение SNAT-правил?
netfilter-mailcow. Он читает значения SNAT_TO_SOURCE/SNAT6_TO_SOURCE, формирует правила post-routing и удерживает их на первой позиции в цепочке, пересоздавая при сдвиге позиции.
Как проверить, что SNAT в mailcow применился?
Посмотреть лог docker compose logs --tail=200 netfilter-mailcow на предмет ошибок применения правил, затем отправить тестовое письмо на внешний ящик и сверить IP отправителя по заголовкам Received на принимающей стороне.
SNAT применился без ошибок в логе, но письма всё равно уходят со старого IP — что проверить дальше?
Проверить, нет ли на самом хосте (вне контейнеров) собственного firewall или NAT-правил, которые перехватывают исходящий трафик раньше, чем срабатывает правило netfilter-mailcow. Такой конфликт решается на уровне хостовых правил, а не в конфигурации mailcow.
Нужен ли SNAT, если у сервера всего один публичный IP?
Нет смысла — SNAT нужен именно на серверах с несколькими IP-адресами, когда требуется явно выбрать, с какого из них уходит исходящая почта. При одном IP исходящий адрес и так единственно возможный.
Источники
- mailcow docs — SNAT — Параметры SNAT_TO_SOURCE и SNAT6_TO_SOURCE в mailcow.conf, применение через docker compose up -d, роль netfilter-mailcow, удержание правил post-routing на первой позиции. https://docs.mailcow.email/post_installation/firststeps-snat/
- mailcow docs — IP bindings — Назначение переменных IP bindings (SMTP_PORT, IMAP_PORT, HTTPS_PORT и др.) как адресов прослушивания входящих подключений; прямое указание, что изменение привязки не влияет на source NAT. https://docs.mailcow.email/post_installation/firststeps-ip_bindings/
- mailcow community — Setting IP Bindings and SNAT — Практическое обсуждение разницы между настройкой IP bindings и SNAT на серверах с несколькими IP-адресами. https://community.mailcow.email/d/295-setting-ip-bindings-and-snat
- GitHub mailcow-dockerized — netfilter-mailcow (docker-compose.yml, main.py, modules) — netfilter-mailcow: privileged, network_mode host, переменные SNAT_TO_SOURCE/SNAT6_TO_SOURCE (по умолчанию n); SNAT применяется к сети IPV4_NETWORK.0/24 (172.22.1.0/24) в POSTROUTING, проверка каждые 10 с, строка лога «Added POSTROUTING rule for source network … to SNAT target …». https://github.com/mailcow/mailcow-dockerized/tree/master/data/Dockerfiles/netfilter



