MikroTik + DNSBL: отказоустойчивая цепочка блокировки с fallback-зонами v.2.0
Артур Хайбуллин
Артур Хайбуллин 14 мая 2026, 17:56
mikrotik_dnsbl_scripts.jpg

В предыдущем материале я показывал базовый вариант ранней фильтрации входящего трафика на MikroTik через Spamhaus ZEN. Подход оказался рабочим - нагрузка на почтовые сервисы снизилась на 30-70%, а количество SMTP-сессий уменьшилось в несколько раз.

Но в процессе эксплуатации всплыли важные недостатки:

  • Единая точка отказа - если DNS Spamhaus недоступен, фильтрация полностью прекращается, и новые вредоносные IP перестают блокироваться до восстановления сервиса.

  • Ложное ощущение защиты - NXDOMAIN воспринимался скриптом как ошибка, из-за чего IP оставался в очереди навсегда.

  • Нет понимания причин блокировки - невозможно определить, какой DNSBL и по какой причине сработал.

Эти проблемы подтолкнули к переработке скрипта.

Нужно было добиться:

  • отказоустойчивости - если один DNSBL недоступен, автоматически переходить к следующему

  • прозрачности - каждый заблокированный IP должен иметь комментарий с указанием источника и причины

  • корректной очистки - IP должен удаляться из очереди при любом определённом ответе

  • устойчивости к race condition - скрипт не должен падать при параллельных запусках

Выбор fallback-зон

Помимо Spamhaus, я добавил несколько бесплатных DNSBL, работающих по тому же принципу (reversed IP + A-запись):

  • zen.spamhaus.org - основной источник (спам, вредоносное ПО, фишинг, ботнеты)

  • b.barracudacentral.org - Barracuda BRBL (репутационный список спамеров)

  • psbl.surriel.com - PSBL (пассивная фиксация спам-активности)

  • dnsbl.dronebl.org - DroneBL (ботнеты, прокси, заражённые устройства)

Приоритет задаётся порядком в массиве: сначала Spamhaus, затем остальные.

Критерии отбора

  • Бесплатность - все зоны доступны без регистрации

  • Единый DNS-механизм - запрос вида {reversed IP}.{zone} → 127.0.0.x

  • Актуальность - списки поддерживаются и обновляются

  • Релевантность - покрывают нужные типы угроз

Изменения в логике скрипта

Как было

Скрипт использовал только один DNSBL и не различал:

  • NXDOMAIN - IP чист

  • DNS timeout - сервер не ответил

Обе ситуации попадали в on-error{}, поэтому «чистые» IP оставались в очереди навсегда.

Как стало

Теперь используется цепочка зон:

dnszones = [spamhaus → barracuda → psbl → dronebl]

Для каждого IP:
  для каждой зоны (пока не заблокирован):
    ─ resolve(fqdn)
    ├─ 127.0.0.x → определить категорию, блокировать, blocked=true
    └─ NXDOMAIN  → checked=true, перейти к следующей зоне

  если blocked || checked:
    ─ удалить IP из spamhaus_check
  иначе:
    ─ оставить для повтора (все DNSBL молчат)

Для каждого IP:

  • для каждой зоны:

    • если ответ 127.0.0.x → блокируем, фиксируем категорию

    • если NXDOMAIN → считаем IP проверенным и переходим к следующей зоне

  • если IP либо заблокирован, либо хотя бы одна зона дала осмысленный ответ → удаляем из очереди

  • если все зоны молчат (таймауты) → оставляем IP до следующего запуска

Ключевое нововведение - разделение состояний:

  • blocked=true - IP найден в списке

  • checked=true - хотя бы одна зона ответила NXDOMAIN

Если ни blocked, ни checked не выставлены - значит, все DNSBL недоступны.

Категоризация по источнику

Теперь комментарий в списке блокировки содержит источник (Spamhaus, Barracuda, PSBL, DroneBL) и тип угрозы (spam, malware, phishing, proxy, botnet и т.д.).
Это позволяет сразу понимать, почему IP заблокирован.

Spamhaus ZEN:

  • 127.0.0.2 - spam

  • 127.0.0.3 - malware

  • 127.0.0.4 - phishing

  • 127.0.0.5 - spam+phishing

  • 127.0.0.6 - spam+malware

  • 127.0.0.7 - malware+phishing

  • 127.0.0.9 - spam+malware+phishing

Barracuda BRBL:

  • 127.0.0.2 - spam (Barracuda)

DroneBL:

  • 127.0.0.3 - IRC Drone

  • 127.0.0.5 - Bottler

  • 127.0.0.6 - spambot/drone

  • 127.0.0.7 - DDOS Drone

  • 127.0.0.8 - SOCKS Proxy

  • 127.0.0.9 - HTTP Proxy

  • 127.0.0.15 - compromised router

  • 127.0.0.17 - botnet (DroneBL)

Пример записи в address-list:

/ip firewall address-list add list=spamhaus_block address=185.xxx.xxx.xxx timeout=30d comment="spam (Barracuda)"

Расширение фильтра приватных диапазонов

Добавлены исключения для:

  • 100.64.0.0/10 - CGNAT

  • 0.x.x.x - нулевые адреса

Эти диапазоны не должны попадать в очередь.

Устойчивость к race condition

В первой версии при параллельных запусках возникала ошибка:

no such item (4) (/ip/firewall/address-list/remove; line 35)

Все операции удаления теперь обёрнуты в :do { remove } on-error={ }, что игнорирует попытку повторного удаления уже обработанного элемента.

Финальный код скрипта

:local dnszones {
    "zen.spamhaus.org";
    "b.barracudacentral.org";
    "psbl.surriel.com";
    "dnsbl.dronebl.org"
}
:local blockTimeout "30d"

:foreach i in=[/ip firewall address-list find list=spamhaus_check] do={
    :local srcIP [/ip firewall address-list get $i address]

    :if ([:len $srcIP] > 15 || [:find $srcIP "/"] >= 0) do={
        :do { /ip firewall address-list remove $i } on-error={ }
        :continue
    }

    :local p1 [:find $srcIP "."]
    :local p2 [:find $srcIP "." ($p1 + 1)]
    :local p3 [:find $srcIP "." ($p2 + 1)]

    :local o1 [:pick $srcIP 0 $p1]
    :local o2 [:pick $srcIP ($p1 + 1) $p2]
    :local o3 [:pick $srcIP ($p2 + 1) $p3]
    :local o4 [:pick $srcIP ($p3 + 1) [:len $srcIP]]

    :if ($o1 = "10" || \
         ($o1 = "127") || \
         ($o1 = "169" && $o2 = "254") || \
         ($o1 = "172" && (($o2 >= 16 && $o2 <= 31))) || \
         ($o1 = "192" && $o2 = "168") || \
         ($o1 = "100" && (($o2 >= 64 && $o2 <= 127))) || \
         [:tonum $o1] < 1) do={
        :do { /ip firewall address-list remove $i } on-error={ }
        :continue
    }

    :local revIP ($o4.".".$o3.".".$o2.".".$o1)
    :local blocked false
    :local checked false

    :foreach zone in=$dnszones do={
        :if (!$blocked) do={
            :local fqdn ($revIP . "." . $zone)
            :do {
                :local res [:resolve $fqdn]
                :local respStr [:tostr $res]

                :if ([:pick $respStr 0 8] = "127.0.0.") do={
                    :local classCode [:pick $respStr 8 [:len $respStr]]
                    :local comment ""

                    :if ($zone = "zen.spamhaus.org") do={
                        :if ($classCode = "2") do={ :set comment "spam" }
                        :if ($classCode = "3") do={ :set comment "malware" }
                        :if ($classCode = "4") do={ :set comment "phishing" }
                        :if ($classCode = "5") do={ :set comment "spam+phishing" }
                        :if ($classCode = "6") do={ :set comment "spam+malware" }
                        :if ($classCode = "7") do={ :set comment "malware+phishing" }
                        :if ($classCode = "9") do={ :set comment "spam+malware+phishing" }
                    }
                    :if ($zone = "b.barracudacentral.org") do={
                        :if ($classCode = "2") do={ :set comment "spam (Barracuda)" }
                    }
                    :if ($zone = "psbl.surriel.com") do={
                        :set comment "listed (PSBL)"
                    }
                    :if ($zone = "dnsbl.dronebl.org") do={
                        :if ($classCode = "3") do={ :set comment "IRC Drone" }
                        :if ($classCode = "5") do={ :set comment "Bottler" }
                        :if ($classCode = "6") do={ :set comment "spambot/drone" }
                        :if ($classCode = "7") do={ :set comment "DDOS Drone" }
                        :if ($classCode = "8") do={ :set comment "SOCKS Proxy" }
                        :if ($classCode = "9") do={ :set comment "HTTP Proxy" }
                        :if ($classCode = "15") do={ :set comment "compromised router" }
                        :if ($classCode = "17") do={ :set comment "botnet (DroneBL)" }
                    }

                    :if ($comment = "") do={
                        :set comment ("blocked by " . $zone)
                    }

                    /ip firewall address-list add \
                        list=spamhaus_block address=$srcIP \
                        timeout=$blockTimeout comment=$comment
                    :log info ("Block " . $srcIP . " (" . $comment . ") by " . $zone)
                    :set blocked true
                }
                :set checked true
            } on-error={
                :set checked true
            }
        }
    }

    :if ($blocked || $checked) do={
        :do { /ip firewall address-list remove $i } on-error={ }
    }
}

Что ещё изменилось

  • Таймаут блокировки увеличен с 1 дня до 30 дней

  • Проверка префикса стала явной, без регулярных выражений

  • Архитектура взаимодействия (RAW → Filter → Scheduler) осталась прежней

Ограничения

  • PSBL не даёт детальных кодов

  • Barracuda может ограничивать частоту запросов без регистрации

  • DroneBL ориентирован на ботнеты, а не на спам

Результат

После внедрения:

  • отказоустойчивость выросла - при падении Spamhaus работают остальные зоны

  • прозрачность улучшилась - комментарии показывают источник и тип угрозы

  • стабильность повысилась - ошибок в планировщике больше нет

  • очередь очищается корректно - «чистые» IP не висят бесконечно

Заключение

Переход от одного DNSBL к цепочке fallback-зон - логичный шаг для продакшена.
Spamhaus остаётся лучшим источником, но полагаться только на него рискованно.

Добавление Barracuda, PSBL и DroneBL:

  • не усложнило конфигурацию

  • значительно повысило надёжность фильтрации

Следующие шаги:

  • геолокационная фильтрация

  • сбор статистики по зонам

  • репутационное взвешивание

DNSBL-фильтрация - это не магия, а работа с данными. Чем больше независимых источников Вы используете - тем стабильнее результат.

Спасибо, что дочитали до конца. DNSBL-фильтрация --- это не магия, а методичная работа с данными. И чем больше независимых источников данных Вы используете, тем стабильнее результат. 😉

0
25
Комментарии (0)
Комментировать
Пока нет комментариев

Станьте первым, кто прокомментирует эту запись