0887 371 498 support@itservice-bg.net
4642 blackhole маршрута вместо firewall: как рутерът пази fast path при 7 Gbps
09.09.2026 · Самуил Арсов · MikroTik, Киберсигурност, Рутери

4642 blackhole маршрута вместо firewall: как рутерът пази fast path при 7 Gbps

Граничният рутер на нашата мрежа (AS47453) държи три пълни BGP таблици — над 2,2 милиона IPv4 маршрута, от които около 1,07 милиона активни — и в пиковите часове през него минават 6–7 Gbps реален трафик при над половин милион пакета в секунда. Машината е CCR2116-12G-4S+ с 16 ядра и 16 GB RAM на RouterOS 7.24.

При този профил рутерът има една-единствена задача: да мести пакети. И точно затова на него няма connection tracking, няма filter, няма NAT, няма mangle. Тази статия е за това как при такава конфигурация се блокират над 4600 зловредни мрежи, без да се пипа firewall — и колко точно струва всяка операция, измерено в секунди на живата машина.

Защо conntrack е изключен и какво губиш с него

Connection tracking е това, което позволява на RouterOS да знае, че даден пакет принадлежи на вече установена сесия. От него зависят NAT, connection-state правилата, mangle таблицата и цялата логика „запомни този източник в address-list“.

На рутер, който прави чист рутинг и транзит, conntrack е чиста загуба — той иска памет и такт за всяка сесия, а никой не го пита. Изключен е и стои изключен. Резултатът се вижда: CPU средно 15% при седем гигабита и 14,7 от 16 GB свободна памет.

Изключва се с един ред и стои изключено:

/ip firewall connection tracking set enabled=no

# проверка
/ip firewall connection tracking print

    enabled: no
active-ipv4: no
active-ipv6: no

Оттук нататък обаче отпада цял пласт инструменти. Mangle таблицата не работи изобщо, матчърите по connection-state са безсмислени, а записването на нарушител в address-list с action=add-src-to-address-list е невъзможно — това действие е валидно само в mangle, а mangle без conntrack го няма.

Практическото следствие е по-широко, отколкото изглежда: на такъв рутер firewall-ът изобщо не е инструмент. Не можеш да запишеш източник на флуд в address-list, но не можеш и да го ограничиш по скорост — всяко правило в raw и всяка simple queue гасят fast path (виж следващата секция). Каквото и да сложиш в тези таблици, плащаш го с производителността на целия рутер, а не само на проблемния трафик.

Fast path — и защо едно-единствено правило го изключва

Fast path е бързият път през ядрото на RouterOS: пакетът се предава от входящия към изходящия интерфейс, без да минава през пълната обработка. Проверява се така:

/ip settings print

              allow-fast-path: yes
        ipv4-fast-path-active: yes
       ipv4-fast-path-packets: 88518666621
         ipv4-fast-path-bytes: 90952274398842

Условията да е активен са прости и безкомпромисни: allow-fast-path=yes, изключен conntrack, и нито едно активно правило във filter/raw/mangle. Simple queues също го гасят.

Измерихме точната цена на живата машина — включихме едно-единствено raw правило (drop по address-list) и веднага го върнахме на disabled:

Показател Без raw правила С ЕДНО активно raw правило
fast path пакети/сек 511 826 0
ipv4-fast-path-active true false
CPU 9–10% 23%

Това не е „малко по-скъпо“ — това е двоичен превключвател. Едно правило изключва fast path напълно, за целия трафик, а не само за пакетите, които то мачва. Оттам нататък firewall-ът е негоден инструмент на този рутер, независимо колко елегантно е написан.

Задачата: над 4600 мрежи за блокиране

Исканото беше просто по формулировка: рутерът да не си говори с известните зловредни и невалидни мрежи.

Готовият списък за това е firehol_level1. FireHOL е отворен проект, който събира десетки публични репутационни списъци, обединява ги, премахва припокриванията и ги публикува в готов за машинна употреба вид, като ги обновява по няколко пъти на ден. Списъците са степенувани по агресивност и level 1 е най-консервативното ниво — в него влиза само това, с което една нормална мрежа няма никаква работа: bogon-и и невалидна адресация, блокове от Spamhaus DROP и EDROP (мрежи, отписани от собствениците си или изцяло в ръцете на злоупотребяващи) и адреси на известни командни сървъри на ботнети.

Идеята на това ниво е да може да се блокира без страх от фалшиви положителни — затова е и единственото, което си струва да седи директно на граничен рутер. Днес в него има 4657 префикса.

Класическият начин е address-list плюс drop правило в raw. От предната секция вече е ясно защо това е невъзможно тук — ще струва fast path-а, тоест ще качи CPU-то от 10 на 23% при седем гигабита, за да блокира трафик, който и без това е малък.

Остава другият лост: blackhole маршрути. Маршрут с type=blackhole дропва пакета в самата forwarding таблица. Първо трябваше да се докаже, че той не гаси fast path — и той не го гаси. Рутерът вече от години носи 22 системни blackhole (bogon набор и собствените ни мрежи) заедно с активен fast path, а тестът с още 500 не промени нищо: ipv4-fast-path-active=true през цялото време, CPU се качи временно до 19% само докато траеше добавянето.

Допълнителен ефект: на рутера е включен rp-filter=loose. Loose uRPF отхвърля пакети от източник, за който няма валиден маршрут — а blackhole маршрут не е валиден път. Тоест блокирането се очаква да работи и в двете посоки, не само на изхода.

Тестовете: колко струва всяка операция при 2,2 милиона маршрута

Преди да се пише скрипт, трябваше да се знае с какви времена се работи. Всички измервания са с вътрешен [:timestamp] на самата машина, върху 500 тестови /32 в документационните диапазони, при жив трафик:

Операция Време На единица
/ip route add × 500 1,88 s 3,76 ms
същото, но с коментар на всеки 2,47 s 4,94 ms
/ip route remove на събран списък 1,97 s 3,93 ms
find where blackhole 4,0 s
find where dst-address="X/32" 1,09 s
find where distance=10 (само) 7 min 40 s
find where comment="..." 7 min 17 s
get comment в цикъл × 522 1 min 51 s 213 ms на четене

Тук са двата урока, които определиха целия дизайн:

1. Флагът blackhole е евтин филтър, всичко останало не е. Разликата между find where blackhole (4 секунди) и find where comment= (437 секунди) е 113 пъти. При това търсенето по distance само по себе си е също толкова бавно, но blackhole and distance=10 е бързо — RouterOS първо стеснява по флага и после проверява останалото само върху намереното. Правилото е: условието задължително трябва да съдържа blackhole.

2. Никога не маркирай масови маршрути с коментар. Изглежда като най-естественото нещо на света — слагаш comment="firehol" и после ги намираш по него. На тази таблица това е седем минути и половина на всяко търсене, тоест скриптът никога не би завършил в разумно време. Същото важи и за четенето на свойства в цикъл: get по идентификатор също е линеен скан — 213 милисекунди на запис, две минути за петстотин.

Скритата цена: audit log-ът

Най-неприятното откритие не беше свързано с времето. RouterOS записва в системния лог всяко изпълнение на конфигурационна команда. 500 добавени маршрута дадоха 507 реда в лога за 2,5 секунди. При memory-lines=1000 един цикъл add + remove изяде 976 от 1000 реда — цялата история на BGP сесиите и логините просто изчезна.

С 4657 префикса логът се пренаписва около десет пъти при всяко пускане. Тоест ако не се внимава, защитата, която току-що си вдигнал, те ослепява за всичко останало на рутера. Същият урок излезе и по-рано при друг наш scheduler, който викаше enable/disable безусловно на стотици DHCP lease-а всеки час — поправката беше да чете текущото състояние и да пише само при разлика.

Изводът е общ и си струва да се запомни: всяка set, add или remove в автоматичен скрипт е един ред в лога, независимо дали променя нещо. Или увеличаваш memory-lines, или изнасяш лога на syslog сървър, или сравняваш преди да пишеш.

Рутерът се справя сам — без Linux посредник

Първоначалното намерение беше списъкът да се тегли и обработва на сървър, а на рутера да се пращат готови команди. Очаквах стринговите операции в RouterOS да са неизползваемо бавни при близо пет хиляди реда. Сгреших — измерено на самата машина:

Стъпка Време
изтегляне на netset-а (72 KiB) 1,03 s
прочитане на файла 0,015 s
парсване на 4690 реда → 4657 префикса 0,455 s

Има обаче един капан, който струва време, ако не се знае:

# НЕ работи за голям файл — връща празно БЕЗ грешка
:local buf [/file get fh.txt contents]

# Работи: чете на парчета
:local buf ""
:local off 0
:local part
:do {
  :set part [/file read file=fh.txt chunk-size=32768 offset=$off as-value]
  :set buf ($buf . ($part->"data"))
  :set off ($off + 32768)
} while=([:len ($part->"data")] = 32768)

/file get contents за файл от 74 KB върна дължина 0 — мълчаливо, без съобщение за грешка. За малък файл работи безупречно, което прави засечката още по-коварна. Целият файл се събира на парчета за 15 милисекунди.

Скриптът

Логиката е дневен цикъл: изтегли, изчисти своите стари, сложи новите. Съкратен вариант на същественото:

:local url "https://iplists.firehol.org/files/firehol_level1.netset"

# 1. Изтегляне с fail-safe: при неуспех НЕ трием нищо
:do {
  /tool fetch url=$url dst-path=bh.txt check-certificate=no
} on-error={
  :log warning "bh-sync: fetch failed"
  :error "stop"
}

# 2. Четене на парчета (виж по-горе) и парсване ред по ред:
#    празни редове и коментари (#) се прескачат,
#    адрес без маска се допълва до /32

# 3. Изключения — проверка в hash, не в цикъл:
:if ([:typeof ($keep->$pfx)] != "nothing") do={ :set skipped ($skipped + 1) }

# 4. Триене САМО на своите — задължително през флага blackhole
:local old [/ip route find where blackhole and distance=10]
:if ([:len $old] > 0) do={ /ip route remove $old }

# 5. Добавяне
/ip route add dst-address=$pfx type=blackhole distance=10

:log info ("bh-sync: staro=" . $nold . " novo=" . $nnew . " propusnati=" . $skipped)

Няколко решения в този код не са очевидни:

Маркерът е distance=10, а не коментар. Комбинацията blackhole and distance=10 отделя нашите маршрути от системните за под пет секунди. Стойността обаче трябва да е в диапазона 3–19: BGP на този рутер е с distance 20, а системните blackhole са 1 и 2. Ако сложиш 200, blackhole маршрутът губи от BGP при точно съвпадение на префикса и стои неактивен — тиха, неработеща защита. По-специфичният префикс печели винаги, така че проблемът е само при пълно съвпадение — но именно Spamhaus DROP частта съдържа блокове, които някой анонсира точно така.

Списъкът с изключения е на две нива. Едните са точни префикси — системните bogon-и и собствените ни мрежи, които вече ги има и не бива да се дублират. Другите са цели блокове по начало на адреса: мрежите на съседите, клиентите и партньорите ни. Firehol не е чист bogon списък — в него влизат и напълно живи български мрежи, включително на наш пиър. Днес реално се изрязват 15 реда; останалите записи са застраховка за бъдещи версии на списъка.

Отделна routing-table не става за маркер. Пробвано — маршрут извън main просто не участва във forwarding-а и не блокира нищо.

Има и няколко чисто синтактични капана в RouterOS, платени в движение: масив не приема стрингов литерал за ключ ([:toarray "a,b,c"] плюс цикъл, който пълни hash-а); командите на един ред искат ;, не интервал; и в RouterOS 7 няма /system scheduler run — за ръчно пускане се изпълнява самият код.

Капанът с правата на scheduler-а

Скриптът беше тестван парче по парче през SSH като admin и работеше. Първото пускане през самия scheduler обаче даде:

bh-sync: fetch failed

Причината е, че policy=read,write,test не стига за /tool fetch. Fetch записва файл, а файловите операции се контролират от ftp policy. Правилното е policy=ftp,read,write,test.

Това щеше да излезе чак на първото автоматично пускане в 5:30 сутринта, при това като тиха повреда — старите маршрути щяха да стоят, никой нямаше да забележи месеци наред. Хвана се само защото scheduler-ът беше пуснат нарочно с преместен старт. Изпълнението през SSH не доказва, че scheduler-ът ще проработи — правата са различни. Тествай през самия scheduler.

Същият този провал потвърди и че fail-safe-ът работи както трябва: при неуспешно изтегляне блокът on-error спира скрипта преди триенето и маршрутите остават непокътнати. Провалът е fail-closed за данните и fail-open за защитата — точно в тази посока, в която го искаш.

Резултатът

Scheduler-ът работи всяка сутрин в 5:30. Пълният цикъл — изтегляне, парсване, триене на старите, добавяне на новите — отнема между 27 и 42 секунди според натоварването и скоростта на изтеглянето. Ето засеченото първо пускане, което направи и двете операции:

21:40:48  старт
21:41:30  bh-sync: staro=4642 novo=4642 propusnati=15

# 42 секунди общо

Триенето на 4642 маршрута на реален мащаб се раздели така: find where blackhole and distance=10 = 4,67 s, самото remove на събрания списък = 12,42 s (2,68 ms на маршрут — по-ефективно, отколкото на пакети от по 500). Времето на find не зависи от броя намерени: то е един пълен скан на 2,2 милиона маршрута, независимо дали ще върне 20 или 5000 записа.

Състоянието след внедряването:

Показател Стойност
blackhole маршрути 4664 (4642 от списъка + 22 системни)
ipv4-fast-path-active true
CPU при 7 Gbps 15–18%
свободна памет 14,7 от 16 GB
BGP сесии всички established, нула флапове

Връщането назад е два реда и отнема под 20 секунди — системните 22 не се засягат, защото те са с distance 1 и 2:

/ip route remove [find where blackhole and distance=10]
/system scheduler disable blackhole-sync

Какво остава да се направи по-добре

Този вариант е прост и работи, но не е върхът. По-чистото решение при такъв мащаб е RTBH през BGP — отделна машина със сесия към рутера, която анонсира префиксите с blackhole next-hop. Тогава на рутера няма нито една конфигурационна команда, нула реда в audit log-а, а обновяването е инкрементално по природа. Рутерът и без това дъвче 2,2 милиона префикса — още пет хиляди не му тежат.

При такава схема задължително се слагат ограничения от страната на рутера: входящ филтър само за дължини /24 до /32, изричен reject на собствените префикси и на подразбиращия се маршрут, лимит на броя префикси, TCP MD5 и TTL security. Компрометирана машина тогава може само да дропва трафик, но не и да го отклонява.

Какво си струва да се вземе от този случай

  • Fast path е двоичен. Едно активно правило в raw го изключва за целия трафик. Ако рутерът е чист транзитен, firewall-ът просто не е инструмент на тази машина.
  • Blackhole маршрутите са единственият лост, който не го гаси. Проверено с 500 и после с 4642 записа при жив трафик.
  • Измервай, преди да проектираш. Разликата между работещ и невъзможен скрипт тук беше избор на филтър: 4 секунди срещу 7 минути.
  • Audit log-ът е реален ресурс. Масовите операции го изяждат и те ослепяват точно когато трябва да виждаш какво става.
  • Тествай автоматиката през самата автоматика. Правата на scheduler-а не са правата на SSH сесията.