netfilter-mailcow: рестарты и DROP на SMTP
АйТи Фреш
Linux, Docker и DevOps

netfilter-mailcow перезапускается каждые 10–20 секунд и роняет SMTP: разбираем цепочки FORWARD и isolation

Автор: , директор ООО «АйТи-Фреш» · · ~13 мин чтения
Контейнер netfilter-mailcow перезапускается и дублирует правила блокировки перед цепочкой Docker, письма не проходят
Каждый рестарт добавляет ещё одно правило впереди DOCKER-USER — письма упираются в дубликаты, а не в реальную блокировку.

netfilter-mailcow перезапускается каждые 10–20 секунд, в цепочке FORWARD копятся дублирующиеся правила MAILCOW, а письма на порт 25 не доходят — при этом DISABLE_NETFILTER_ISOLATION_RULE=y в mailcow.conf уже стоит. Разбираю по конкретным issue проекта mailcow-dockerized, где именно ломается порядок цепочек Docker и что можно сделать, пока официального фикса нет.

Что происходит, когда netfilter-mailcow уходит в цикл рестартов

Контейнер netfilter-mailcow отвечает за автоматическую блокировку адресов, которые ведут себя подозрительно — перебор паролей, спам-паттерны — и делает это, вставляя правила в свою собственную цепочку MAILCOW внутри таблицы filter. При старте он проверяет положение этой цепочки в общем списке FORWARD и, если что-то не сходится с ожиданиями кода, завершает работу с ошибкой и рестартует — так задумана защита от несогласованного состояния firewall. Проблема в том, что в некоторых конфигурациях эта проверка триггерится не при реальном сбое, а из-за самого факта, что Docker уже создал свои системные цепочки DOCKER-USER и DOCKER-FORWARD до того, как стартовал netfilter-mailcow.

Внешне это выглядит пугающе: почтовый сервер вроде бы поднят, контейнеры мигают зелёным в docker compose ps через секунду после падения, но реальной стабильности нет — раз в 10–20 секунд процесс контейнера завершается и стартует заново, а в это время правила в FORWARD могут временно отсутствовать или дублироваться. Для внешнего наблюдателя, который просто пытается отправить письмо на 25 порт, это выглядит как случайные обрывы соединения без видимой закономерности.

Отдельная сложность в том, что мониторинг «жив ли контейнер» здесь не помогает: Docker честно отчитывается, что netfilter-mailcow работает, restart policy отрабатывает штатно, а health check самого mailcow может даже проходить, потому что состояние проверяется в момент, когда контейнер как раз недавно поднялся и успел ненадолго вставить правила. Проблема заметна только если явно смотреть на частоту событий restart в docker events или на количество строк с переходом MAILCOW в выводе iptables — обычные дашборды uptime её не покажут.

Схема правильного порядка цепочек FORWARD в Docker и того, как дубли MAILCOW нарушают этот порядок
Дубли MAILCOW встают впереди DOCKER-USER — все ваши кастомные правила firewall становятся бесполезны.

Разбор по Issue #6795: почему MAILCOW-правила оказываются не там

В Issue #6795 репозитория mailcow-dockerized описана именно эта ситуация на связке mailcow 2025-09b, Debian 13, Docker 28.4.0 и Docker Compose 2.39.4. Автор показал командой iptables -t filter -L FORWARD --line-numbers, что переход в цепочку MAILCOW добавляется дважды и оказывается перед DOCKER-USER, хотя по логике должно быть иначе. Корень проблемы, по разбору автора, находится в файле data/Dockerfiles/netfilter/modules/NFTables.py: код, который проверяет позицию цепочки MAILCOW, жёстко предполагает, что она должна находиться на позиции 0 в FORWARD. Но Docker всегда создаёт свои системные цепочки первыми при инициализации сети, поэтому на актуальных версиях Docker позиция 0 уже занята не MAILCOW, а DOCKER-USER — и каждая такая несостыковка заканчивается рестартом с ещё одной вставленной сверху копией правила MAILCOW.

Итог такого цикла — не только бесконечные рестарты контейнера, но и накопление дублирующихся переходов MAILCOW впереди DOCKER-USER, что на практике обесценивает любые пользовательские правила firewall в DOCKER-USER: пакет натыкается на более раннее правило MAILCOW прежде, чем вообще доходит до кастомной логики администратора. Issue закрыт со статусом not planned и меткой stale — предложенная в обсуждении правка NFTables.py не была подтверждена мейнтейнерами как официальное исправление, то есть на момент разбора это открытый риск, а не решённая история.

Важная деталь именно для эксплуатации: проблема воспроизводится не на всех хостах одинаково — она зависит от того, в каком порядке докер поднимает сети при старте демона и в какой момент запускается netfilter-mailcow относительно остальных сервисов compose-стека. Поэтому одна и та же версия mailcow может годами работать стабильно на одном сервере и сразу же зациклиться на другом, если там чуть другой порядок загрузки systemd-юнитов или другая версия самого Docker Engine — это не баг конкретной инсталляции, а гонка условий, которая по-разному проявляется в зависимости от окружения.

Почему DOCKER-USER вообще должен идти раньше правил mailcow

По документации Docker, цепочка DOCKER-USER — это специально оставленное место для пользовательских правил firewall, которые должны обрабатываться раньше внутренних цепочек Docker: DOCKER-FORWARD и DOCKER. В основной цепочке FORWARD Docker безусловно переходит сначала в DOCKER-USER, затем в DOCKER-FORWARD и далее в DOCKER-INGRESS — это фиксированный порядок, и он не предназначен для того, чтобы сторонние контейнеры вроде netfilter-mailcow подставляли собственные правила впереди него на постоянной основе.

Именно поэтому конфликт между NFTables.py и реальным порядком цепочек — это не мелочь конкретной инсталляции, а системное расхождение ожиданий: mailcow исторически рассчитывал управлять позицией 0 в FORWARD единолично, а начиная с определённой версии Docker это место уже занято системной инфраструктурой самого Docker Engine. Я видел этот же класс проблем и в других self-hosted проектах, которые пытаются жёстко контролировать позицию своих правил в firewall хоста, не учитывая, что Docker меняет эту модель от версии к версии.

Я подробно разбирал похожую перестройку правил на другом проекте в материале про перенос фильтрации Docker на nftables без DOCKER-USER — там та же логика: доверять позиции правила в списке нельзя, если её меняет сторонний процесс при каждом рестарте. Практический вывод из этого: любые собственные правила firewall для mailcow — allow-list по IP для админки, ограничение по гео, кастомные лимиты на подключение — я кладу именно в DOCKER-USER и никогда не рассчитываю, что они будут первыми в общей цепочке FORWARD. Если между DOCKER-USER и трафиком встревают лишние переходы MAILCOW, правило может формально существовать и быть правильным, но реально не применяться к части трафика, потому что пакет до него просто не доходит — уже отфильтрован раньше.

Второй симптом: SYN без ответа даже при DISABLE_NETFILTER_ISOLATION_RULE=y

Смежная, но отдельная проблема описана в Issue #7320: на связке mailcow 2026-05c, Debian 13 и Docker 29.5.3 входящие SMTP-пакеты доходили до хоста, DNAT отрабатывал корректно, но ответного SYN-ACK не было — соединение просто зависало. Автор нашёл в цепочке MAILCOW через iptables -L MAILCOW -n --line-numbers повторяющиеся правила DROP с комментарием mailcow isolation, а через tcpdump -i any port 25 -n подтвердил, что SYN действительно доходит до интерфейса, но ответа от сервиса не следует — то есть пакет режется уже после DNAT, на этапе фильтрации.

Самое неприятное в этом отчёте — переменная DISABLE_NETFILTER_ISOLATION_RULE=y в mailcow.conf, которая по идее должна отключать именно это изолирующее правило, в описанном случае не помогала: перезапуск netfilter-mailcow не убирал старые DROP-правила из цепочки, потому что MAILCOW-цепочка не флашится полностью перед повторной инициализацией, и правила изоляции просто накапливаются поверх старых. Обходной путь автора — iptables -F MAILCOW и затем docker compose restart netfilter-mailcow — давал эффект только до следующего автоматического рестарта контейнера, но подтверждённого исправления со стороны мейнтейнеров на момент разбора не появилось.

Дерево решений для диагностики недоставленного SMTP-трафика за netfilter-mailcow: три ветки причин и проверок
Три разных симптома — три разных команды диагностики, и все три встречаются на одном и том же хосте.

Кейс: автосервис «АвтоПричал» терял письма от системы записи на диагностику

У клиента — автосервиса «АвтоПричал», 21 рабочее место — после планового обновления хост-системы с обновлением Docker Engine до версии 28.x заявки из внешней системы записи клиентов на диагностику перестали стабильно доходить до ящика сервиса на mailcow: часть писем не доходила вовсе, часть приходила с задержкой в час-полтора, когда отправитель повторял попытку, а правила в DOCKER-USER, которыми клиент ограничивал доступ к админке, перестали срабатывать. В логе netfilter-mailcow обнаружился ровно тот паттерн из Issue #6795 — контейнер перезапускался каждые 10–20 секунд, а iptables -t filter -L FORWARD --line-numbers показывал две копии перехода в MAILCOW впереди DOCKER-USER.

Постоянного фикса от разработчиков mailcow на момент работ не было, поэтому мы применили обходной путь, который описал сам автор Issue #6795: пропатченный NFTables.py (в правиле вставки 'insert' заменён на 'add', проверка позиции цепочки закомментирована) подключили в контейнер через docker-compose.override.yml по пути /app/modules/NFTables.py в режиме только чтения, удалили дубли переходов MAILCOW командой iptables -t filter -D FORWARD <номер> и пересоздали стек. Плюс добавили мониторинг количества рестартов контейнера через docker events с алертом при частоте выше одного рестарта в минуту. Это не устранение первопричины — я прямо сказал клиенту, что решение временное и держится на конкретной комбинации версий, — а патч нужно сверять с исходником после каждого update.sh, — но оно убрало симптом и дало время дождаться апстрим-фикса или спланировать переход на альтернативную фильтрацию средствами внешнего firewall.

Отдельно мы предупредили клиента, что при следующем плановом обновлении Docker или mailcow ситуация может повториться на новой комбинации версий, и зафиксировали в регламенте обслуживания «АвтоПричала» обязательную проверку количества рестартов netfilter-mailcow сразу после любого апдейта хоста — до, а не после жалобы от системы записи клиентов.

Что дают опции Docker Engine 28 для похожих симптомов

Отдельно стоит учитывать изменение, которое принёс сам Docker Engine 28: по умолчанию неопубликованные порты контейнеров теперь блокируются для доступа с других хостов в локальной сети, даже если ip_forward включён, а filter-FORWARD настроен как ACCEPT. Это защита от нежелательного прямого доступа к внутренним IP контейнеров в обход публикации портов — полезная сама по себе, но она добавляет ещё один слой firewall-логики поверх и без того чувствительной комбинации netfilter-mailcow и Docker-цепочек, увеличивая пространство для похожих конфликтов на новых версиях Docker.

Похожий симптом я разбирал отдельно, когда опубликованный порт Docker оставался доступен вопреки UFW — там причина была обратная: Docker игнорировал правила хостового firewall, а не блокировал лишнее. Если после обновления Docker до версии 28 или новее внезапно перестаёт быть доступен неопубликованный явно сервис внутри контейнерной сети mailcow, а публикация портов через -p при этом настроена штатно, я в первую очередь смотрю именно на этот default-deny механизм, а не сразу на netfilter-mailcow. Тут часто путают две опции. "ip-forward-no-drop": true в /etc/docker/daemon.json лишь не даёт Docker выставить политику DROP для всей цепочки FORWARD — неопубликованные порты он при этом всё равно режет отдельными правилами. Вернуть прежний доступ к неопубликованным портам можно только для конкретной сети, создав её с опцией com.docker.network.bridge.gateway_mode_ipv4=nat-unprotected. Я использую это только как временную диагностическую меру, не как постоянное решение — держать почтовый хост, куда смотрит внешний интернет, в режиме до-28 небезопасно, и я возвращаю всё назад сразу после того, как подтвердил или опроверг гипотезу.

Чек-лист диагностики зависшего SMTP за netfilter-mailcow

Порядок, которым я пользуюсь, когда клиент жалуется на нестабильную доставку почты сразу после обновления хоста или mailcow:

1. Проверить частоту рестартов: docker compose logs --since 5m netfilter-mailcow | grep -ci 'restarting container' (именно такой строкой netfilter сообщает о перезапуске) или счётчик docker inspect -f '{{.RestartCount}}' $(docker compose ps -q netfilter-mailcow). 2. Посмотреть реальный порядок цепочек:

iptables -t filter -L FORWARD --line-numbers
iptables -L MAILCOW -n --line-numbers

3. Проверить, накапливаются ли дубликаты DROP mailcow isolation, даже если DISABLE_NETFILTER_ISOLATION_RULE=y выставлен в mailcow.conf. 4. Снять трафик на 25/465/587 портах: tcpdump -i any port 25 -n, чтобы отличить «пакет не дошёл» от «пакет дошёл, но ответа нет». 5. Если версия Docker 28 или новее — проверить, не работает ли default-deny для непубликуемых портов независимо от netfilter-mailcow. Все пять пунктов укладываются в десять минут и почти всегда сразу показывают, с каким именно из трёх сценариев вы имеете дело.

DISABLE_NETFILTER_ISOLATION_RULE=y не гарантирует чистую цепочку MAILCOW — по Issue #7320 старые DROP-правила изоляции могут не флашиться при рестарте и накапливаться поверх новых.
Чек-лист из пяти шагов диагностики недоставленной почты за контейнером netfilter-mailcow
Пока апстрим не выпустил фикс, единственная стабильная стратегия — держать проверенный обходной патч и мониторить рестарты.

Частые вопросы

Есть ли официальный фикс от mailcow для конфликта netfilter-mailcow с цепочками Docker?

На момент проверки нет: Issue #6795 и #7320 в репозитории mailcow-dockerized закрыты со статусом not planned, предложенные правки NFTables.py не подтверждены мейнтейнерами как официальное решение.

Помогает ли DISABLE_NETFILTER_ISOLATION_RULE=y в mailcow.conf?

Иногда нет. По Issue #7320 переменная должна отключать изолирующее DROP-правило, но при неполном флаше цепочки MAILCOW при рестарте старые DROP-правила остаются, и почта продолжает теряться.

Почему проблема проявляется сильнее после обновления Docker до версии 28?

В актуальных версиях Docker в начале FORWARD стоят безусловные переходы в DOCKER-USER и DOCKER-FORWARD, а Docker Engine 28 вдобавок включил default-deny для неопубликованных портов. Проверка в NFTables.py ждёт MAILCOW на позиции 0, которая уже занята цепочками Docker, — отсюда цикл рестартов из Issue #6795.

Можно ли просто отключить netfilter-mailcow целиком?

Технически да, но тогда пропадает автоблокировка перебора паролей и спам-источников. Я использую это как крайнюю временную меру и сразу компенсирую фильтрацией на внешнем firewall перед хостом mailcow.

Как быстро отличить дубликаты правил MAILCOW от нормального состояния?

Командой iptables -t filter -L FORWARD --line-numbers: в норме переход в MAILCOW встречается один раз. Если видите его дважды и подряд перезапускается контейнер netfilter-mailcow — это тот самый конфликт из Issue #6795.

Столкнулись с похожей задачей? Обращайтесь — решим

Если у вас происходит что-то из описанного в этой статье — или любая другая проблема с ИТ-инфраструктурой, — обращайтесь в любое время. Мои специалисты и я лично разберём ситуацию, найдём настоящую причину и доведём до решения.

Возьмёмся и за разовую задачу, и за постоянное обслуживание. Первичная консультация — бесплатно и без обязательств.

📞 +7 903 729-62-41 ✈ Telegram @ITfresh_Boss

С уважением, Семёнов Евгений Сергеевич, директор «АйТи Фреш» — IT-аутсорсинг для компаний до 50 рабочих мест, 15+ лет практики

Источники

© ООО «АйТи-Фреш» · Москва · Все статьи