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 сесията.