Откриване на DDoS по IPFIX: малка машина до рутера, която различава атака от сваляне
Граничният ни рутер (AS47453) вече не пуска трафик от хиляди познати зловредни мрежи — това го върши наборът blackhole маршрути, за който писах по-рано. Но blackhole спира само онова, за което вече знаеш, че е лошо. Срещу внезапен флуд от свеж адрес, който до преди минута е бил чист, списък не помага. Трябва някой да гледа трафика в реално време и да каже „ето, сега нещо не е наред“. Точно това добавихме миналата седмица — откриване на обемни атаки по NetFlow, без да пипаме самия рутер.
Защо не на рутера
RouterOS може да експортира IPFIX (това е NetFlow версия 10) за целия трафик, който минава през него. Но самият анализ не му е работа — това е чист рутер, чиято единствена задача е да препраща пакети, без connection tracking и без firewall (защо точно така — в статията за blackhole по-долу).
Затова сложихме отделна малка виртуална машина до него: четири ядра, 8 GB памет, Ubuntu. На нея върви FastNetMon Community — безплатната версия на познат инструмент за детекция на DDoS. Рутерът праща IPFIX към машината, машината смята и не връща нищо обратно. Ако утре спре, рутерът продължава да работи все едно нищо не е станало.
Инсталиране
Community версията се слага с официалния инсталатор — един скрипт, който тегли пакета и вдига услугата:
wget https://install.fastnetmon.com/installer -O installer chmod +x installer sudo ./installer
Инсталаторът иска имейл (за маркетинг от производителя) и без подаден такъв се върти в безкраен цикъл — подава му се наведнъж и готово. След него услугата е вдигната, но по подразбиране не стартира сама при рестарт — това се включва с systemctl enable fastnetmon.
Конфигурацията
Три файла вършат цялата работа:
/etc/fastnetmon.conf— настройките;/etc/networks_list— твоите мрежи, тези, които се наблюдават;/etc/networks_whitelist— адреси, които никога да не влизат в списъка (сървъри, шлюзове, самата машина).
В networks_list се изреждат собствените префикси, по един на ред — това казва на FastNetMon кое е „наше“ и си струва да се пази:
203.0.113.0/24 198.51.100.0/23
А това е самият /etc/fastnetmon.conf — параметрите, които реално решават поведението. „По подразбиране“ е стойността във файла, който слага официалният инсталатор; удебеленото сме сменили ние.
Откъде идват данните
| Параметър | Какво прави | По подразбиране | При нас |
|---|---|---|---|
netflow |
Приема NetFlow/IPFIX от рутера | off | on |
netflow_host / netflow_port |
На кой адрес и порт слуша | 0.0.0.0 / 2055 | 0.0.0.0 / 2055 |
netflow_sampling_ratio |
Колко пакета представлява един отчетен (1 = всеки пакет) | 1 | 1 — рутерът праща без sampling |
average_calculation_time |
Прозорецът в секунди, за който се смята средната скорост на всеки адрес. Трябва да е по-дълъг от времената на рутера (виж по-долу) | 5 | 30 |
speed_calculation_delay |
През колко секунди се преизчисляват скоростите | 1 | 1 |
check_period |
През колко секунди се сравнява с праговете | 1 | 1 |
process_incoming_traffic / process_outgoing_traffic |
Гледа входящия / изходящия трафик | on / on | on / on |
networks_list_path |
Файлът с нашите мрежи — само те се следят | /etc/networks_list | същият |
white_list_path |
Адреси, които никога не се банват | /etc/networks_whitelist | същият — сървъри, рутери, статичните адреси на VIP клиент |
enable_connection_tracking |
Брои нови потоци в секунда за всеки адрес (нужно за прага по потоци) | on | on |
Кога е бан — общите прагове за един адрес
| Параметър | Какво прави | По подразбиране | При нас |
|---|---|---|---|
enable_ban |
Изобщо да банва (IPv4) | on | on — blackhole през GoBGP при дребни пакети |
enable_ban_ipv6 |
Същото за IPv6 (по адрес, не цяла мрежа) | on | on |
ban_for_pps + threshold_pps |
Бан над X пакета в секунда | on, 20 000 | on, 20 000 |
ban_for_bandwidth + threshold_mbps |
Бан над X мегабита в секунда | on, 1 000 | on, 1 000 |
ban_for_flows + threshold_flows |
Бан над X нови потока в секунда | off, 3 500 | off, 3 500 |
Прагове по протокол
| Параметър | Какво прави | По подразбиране | При нас |
|---|---|---|---|
ban_for_icmp_pps + threshold_icmp_pps |
Бан при ICMP над X пакета в секунда | off, 100 000 | on, 1 000 |
ban_for_tcp_pps / ban_for_udp_pps |
Бан при TCP / UDP над X пакета в секунда | off, 100 000 | off, 100 000 |
ban_for_tcp_bandwidth и сродните |
Същото, но в мегабити | off, 100 000 | off, 100 000 |
Какво става при бан
| Параметър | Какво прави | По подразбиране | При нас |
|---|---|---|---|
ban_time |
Колко секунди трае банът | 1 900 | 600 |
unban_only_if_attack_finished |
След изтичане отбанва само ако трафикът е паднал под прага, иначе удължава | on | on |
ban_details_records_count |
Колко записа на потоци събира за файла с подробностите | 20 | 20 |
notify_script_path |
Скрипт, който се вика при бан и отбан | /usr/local/bin/notify_about_attack.sh | същият — с нашата класификация |
collect_attack_pcap_dumps |
Пази pcap от атаката (при NetFlow пакети няма, безполезно) | off | off |
gobgp / exabgp |
Обявява бана по BGP — тоест реален blackhole | off / off | on / off |
gobgp_announce_host |
Обявява единичния адрес (/32) | on | off — обявява скриптът |
gobgp_community_host |
BGP community, по която рутерът разпознава бана | 65001:666 | 65001:666 |
gobgp_next_hop |
Next hop в обявата (рутерът така или иначе го прави blackhole) | 0.0.0.0 | адресът на машината |
gobgp_announce_host_ipv6 |
Същото за IPv6 | on | off — обявява скриптът |
Други
| Параметър | Какво прави | По подразбиране | При нас |
|---|---|---|---|
disable_usage_report |
Телеметрия към производителя | off | on — изключена |
monitor_local_ip_addresses |
Следи и собствените адреси на машината | on | on |
enable_api |
Локален API, през който работи конзолният клиент | on | on |
sort_parameter / max_ips_in_list |
Подредба и брой на най-натоварените адреси в конзолата | packets / 7 | packets / 7 |
Две неща, които таблицата не казва сама. Няма отделен праг за SYN — SYN flood се хваща от общия праг по пакети. И типът на атаката, който FastNetMon пише във файла си, не влияе на бана: той банва само по праговете отгоре, а типът е етикет.
Първите дни това беше само детекция: FastNetMon записваше кой е минал прага, без да блокира. От 11 октомври банът е истински blackhole на рутера през вградения GoBGP, а от 12 октомври blackhole има само при дребни пакети, тоест при атака. Как е вързано, е описано по-долу.
Рутерът праща NetFlow
Откъм рутера е буквално в два реда — включваш експорта и казваш накъде да го праща (адресът на машината, порт 2055, версия IPFIX):
/ip traffic-flow set enabled=yes active-flow-timeout=10s /ip traffic-flow target add dst-address=198.51.100.10 port=2055 version=ipfix

Едно нещо изненада в началото: NetFlow пробите скачаха — ту 100 хиляди пакета в секунда, ту 800 хиляди, без това да е реално. Причината е, че рутерът праща потоците на вълни. Първо скъсихме времето, за което държи отворен поток (горният
active-flow-timeout=10s), и усреднихме за десет секунди. Общият трафик седна върху реалния. По отделен адрес обаче не съвсем: едно голямо сваляне беше отчетено като 465 Mbps и 39 000 пакета в секунда, а от самите записи на потоците излизаше около три пъти по-малко. Документацията на FastNetMon казва, че прозорецът за усредняване трябва да е по-дълъг и от active, и от inactive timeout на рутера (тук 10 и 15 секунди), иначе скоростите излизат завишени и на пикове. Същото описват и колеги във форума на MikroTik. На 11 октомври го вдигнахме на 30 секунди. Дали това слага край на фалшивите скокове — ще кажем след седмица наблюдение.
Как се гледа в терминала
Community версията няма уеб интерфейс, но има жив изглед в терминала — fastnetmon_client. Показва в реално време кои адреси колко теглят и колко качват, подредени по натоварване, а най-отдолу — списъка с адресите, минали прага:

Същият изглед за IPv6 е с fastnetmon_client --ipv6. Числата тук веднага се сверяват с броячите на рутера — ако се разминават силно, значи NetFlow не е настроен както трябва.
Прагът сам по себе си лъже
FastNetMon работи на праг: ако към един наш адрес влязат повече от определен брой пакети в секунда, обявява го за атака. Звучи просто, но има уловка. Голямо легитимно сваляне също праща десетки хиляди пакети в секунда. През първия ден машината „засече“ 97 събития над прага — и нито едно не беше атака. Всичките бяха хора, които теглят нещо по HTTPS, с пакети по 1100–1500 байта.
Разликата е в размера на пакета. При истинска атака пакетите са дребни — SYN flood е 60 байта, UDP flood също е малък, защото целта е да задръсти, не да пренесе нещо. При сваляне пакетите са пълни, близо до 1500. Затова не вярваме сляпо на прага. Всяко събитие над него минава през собствена класификация:
| Вид | По какво се познава |
|---|---|
| SYN flood | предимно SYN пакети и среден пакет под 200 байта |
| UDP amplification | UDP от отразяващи портове — DNS 53, NTP 123, memcached 11211 и подобни |
| UDP flood | UDP с дребни пакети |
| ICMP flood | предимно ICMP |
| Фрагменти | много фрагментирани пакети |
| Над прага, не атака | всичко друго — почти винаги сваляне |
Само първите пет броим за атака. Останалото се записва, но не вдига тревога. Между другото, FastNetMon има и собствено разпознаване на вида, но то гледа само какъв дял от пакетите е от даден тип, без размера — затова само то нарича голямото сваляне „SYN flood“. Не му вярваме, ползваме горната таблица.
Какво идва в Telegram
Известия по Telegram имаме отдавна от рутерите; закачихме и това за същия бот. Скриптът от конфига по-горе (notify_script_path) получава от FastNetMon адреса, посоката и подробностите за атаката, минава ги през класификацията и праща съобщение. Пращаме само когато клиент е блокиран, тоест има blackhole. Сваляне над прага отива само в лога. Видът е написан в съобщението:
🚨 FastNetMon: входяща атака — SYN flood IP: 203.0.113.66 85000 pps / 41 mbps, среден пакет 60 B Действие: blackhole анонсиран към R1
Когато банът изтече, идва второ съобщение, че адресът е свален и blackhole-ът е оттеглен. Така, ако клиент се обади, че няма интернет, отговорът е в групата.

Същото и публично, но без адресите
Тези данни показваме и на публичната страница с инфраструктурата — колко атаки е имало, кога и от какъв вид, седем дни назад по часове. Адресите не ги показваме. Те са наши и няма смисъл публична страница да подсказва на нападателя дали ударът му е минал. По същата причина числата там са с половин час закъснение.
Страницата показва и свалянията, но с друг цвят. В графиката по часове жълтото е „над прага, сваляне“: голям трафик с едри пакети, който не е атака и не се блокира. Червеното е атака по класификацията. В плочката „Вид на атаките“ има и ред „Над прага (pps)“. Той показва колко от всички събития за 30 дни са просто превишен праг. През първите дни бяха 99%. Таблицата отдолу остава само за истинските атаки.
Адресът не излиза, но дали е IPv4 или IPv6 се вижда. В подсказката на всяко квадратче и в таблицата с атаките пише „IPv4“ или „IPv6“, а в „Вид на атаките“ има ред „IPv4 / IPv6“ с бройките за 30 дни. Тук се натъкнахме на дребен бъг във FastNetMon Community. Във файла с подробностите за IPv6 бан полето IP: е 0.0.0.0. Истинският адрес е само в името на файла, затова го взимаме от там.

Как се тества, без да удряш собствената мрежа
Остава един въпрос: откъде знаеш, че цялата верига работи, ако още никой не те е нападнал? Да пуснеш истински флуд по собствения си uplink само за да се самопровериш е глупаво — товариш мрежата и може да събориш точно това, което пазиш.
Затова написах малък скрипт, който праща изкуствен IPFIX поток право към машината по локалния интерфейс — описание на атака, но без нито един реален пакет по мрежата. За FastNetMon изглежда като 90 000 пакета в секунда SYN към тестов адрес. Пуснахме го и проследихме цялата верига: засече го, класифицира го като SYN flood със среден пакет 56 байта, прати 🚨 в Telegram, появи се и на страницата.
Едно нещо ни отне време. Кратък изблик само „засича“ и спира дотам. FastNetMon първо събира проба от около двайсет записа за дъмпа на атаката и чак тогава финализира. Ако потокът спре преди това, тревогата увисва и съобщение не тръгва. Трябва да тече непрекъснато поне половин минута — което всъщност обяснява защо реалните сваляния минават докрай, а първият ни кратък тест заседна.
Blackhole през GoBGP
Дотук машината само гледаше. На 11 октомври банът стана истински. Когато FastNetMon реши, че адрес е над прага, и пакетите са дребни, рутерът спира целия трафик към него за десет минути. После го пуска сам, ако трафикът е паднал.
Връзката между двете е BGP. Във FastNetMon Community има вграден GoBGP. Това е BGP демон, който не пипа маршрутите на самия Linux. Само говори BGP с рутера: обявява „този адрес, с такава community“ и после го оттегля. Хубавото на BGP е, че го разбира всеки рутер, без скриптове, които влизат по SSH и пишат команди.
На машината
GoBGP идва с инсталатора, в /opt/fastnetmon-community/libraries/gobgp_*. Услуга за него няма, затова я пишем сами. Конфигът е кратък: нашият частен AS, адресът на машината и рутерът като единствен съсед:
# /etc/gobgpd.conf
[global.config]
as = 65001
router-id = "198.51.100.10"
local-address-list = ["198.51.100.10"]
[[neighbors]]
[neighbors.config]
neighbor-address = "198.51.100.1"
peer-as = 47453
[[neighbors.afi-safis]]
[neighbors.afi-safis.config]
afi-safi-name = "ipv4-unicast"
[[neighbors.afi-safis]]
[neighbors.afi-safis.config]
afi-safi-name = "ipv6-unicast"
В systemd услугата има един важен ред. BindsTo=fastnetmon.service връзва GoBGP към FastNetMon. Ако FastNetMon спре или се срине, спира и GoBGP. BGP сесията пада и рутерът сам маха всички blackhole маршрути. Няма как да остане забравен бан на клиент, защото детекторът е умрял.
# /etc/systemd/system/gobgpd.service [Unit] Description=GoBGP for FastNetMon After=network-online.target BindsTo=fastnetmon.service Before=fastnetmon.service [Service] ExecStart=/opt/fastnetmon-community/libraries/gobgp_3_12_0/gobgpd -f /etc/gobgpd.conf --api-hosts=127.0.0.1:50051 Restart=on-failure [Install] WantedBy=fastnetmon.service
API-то слуша само локално. Порт 179 е отворен в защитната стена само за адреса на рутера. Във fastnetmon.conf остава да се включи модулът:
gobgp = on gobgp_next_hop = 198.51.100.10 gobgp_next_hop_host_ipv4 = 198.51.100.10 gobgp_announce_host = off # обявява скриптът, по размера на пакета gobgp_community_host = 65001:666 gobgp_announce_host_ipv6 = off # и IPv6 го обявява скриптът
На рутера
Откъм MikroTik е един BGP съсед и един филтър. Филтърът е по-важната част. Той пази рутера не от атаките, а от самата машина. Ако тя по грешка или защото е пробита обяви нещо друго, примерно default маршрут или мрежата със сървърите, рутерът трябва да го откаже:
/routing bgp connection add name=fastnetmon as=47453 \
local.address=198.51.100.1 local.role=ebgp \
remote.address=198.51.100.10 remote.as=65001 \
afi=ip,ipv6 input.filter=fnm-in \
input.limit-process-routes-ipv4=100 input.limit-process-routes-ipv6=100 \
output.filter-chain=fnm-out output.redistribute=""
/routing filter rule add chain=fnm-out rule="reject;"
/routing filter rule add chain=fnm-in rule="\
if (dst in 203.0.113.0/25 || dst in 2001:db8:0:2::/64) { reject; }\n\
if (dst-len == 32 && bgp-communities includes 65001:666 \
&& (dst in 203.0.113.0/24 || dst in 198.51.100.0/23)) {\n\
set blackhole yes;\n\
append bgp-communities no-export;\n\
accept;\n\
}\n\
if (dst-len == 128 && bgp-communities includes 65001:666 \
&& dst in 2001:db8::/48) {\n\
set blackhole yes;\n\
append bgp-communities no-export;\n\
accept;\n\
}\n\
reject;"
Редовете на филтъра по ред:
- Първият е бялата листа: сървъри, рутери и статичните адреси на един VIP клиент. Те законно правят повече от праговете и blackhole за тях не се приема никога. За IPv6 това е мрежата на сървърите. Същата листа я има и във FastNetMon, тук е втора преграда.
- Вторият приема само единичен адрес (
/32) от нашите мрежи, с нашата community. Слага го като blackhole и го маркираno-export, за да не излезе към доставчиците. - Третият е същото за IPv6: единичен адрес (
/128) от нашия IPv6 блок. - Всичко останало се отказва.
IPv6 по същата сесия
За IPv6 не вдигнахме втора BGP сесия. BGP е многопротоколен (MP-BGP): една сесия, вдигната върху IPv4, носи и ipv6-unicast, стига двете страни да го обявят. В GoBGP това е второ afi-safi към съседа, а в RouterOS 7 е afi=ip,ipv6 на връзката. Параметърът там се казва afi, не address-families.
Едно нещо все пак трябваше: машината да има собствен IPv6 адрес в мрежата на рутера. Иначе GoBGP праща IPv6 маршрута с next-hop от вида ::ffff:198.51.100.10, а с истински адрес няма какво да се гадае как рутерът ще го приеме. Сложихме статичен адрес и по същия случай записахме статично IPv6 съседите в сървърната мрежа на рутера, с коментар кой е кой. В таблицата със съседи имаше много неуспешни записи за адреси, които никой не ползва: някой сканира мрежата.
Проверката беше като за IPv4. Ръчно обявен тестов адрес излезе на рутера като blackhole. Адрес от сървърната мрежа беше отказан. Към доставчиците не тръгна нищо. Скриптът мина бан и unban с IPv6 адрес. Изкуственият IPFIX е само за IPv4, затова пълен тест с трафик за IPv6 още няма.
Към машината рутерът не праща нищо (fnm-out), тя няма нужда от маршрути. Таванът от 100 префикса пази, ако нещо започне да обявява масово.
Две неща проверихме на живо, защото ни тревожеха от blackhole статията. Blackhole маршрутите от BGP не гасят fast path на рутера, той остава активен. И не пишат по ред в лога за всеки маршрут, както правят ръчно добавените. Тогава точно това ни беше изтрило историята на лога.
Тестът
Първо ръчно, без FastNetMon. С gobgp global rib add обявихме свободен адрес от нашите мрежи и адрес от бялата листа. Първият се появи на рутера като blackhole, вторият беше отказан. Към доставчиците не тръгна нищо. После с изкуствения IPFIX от предишната секция: от засичането до blackhole на рутера минаха около четири секунди, а при изтичане на бана маршрутът изчезна сам.
Решението, което не е техническо
Праговете не различават атака от много бързо сваляне. Още първия час след включването blackhole получи клиент, който сваляше игра от Steam: кеш сървърите им в София дават над 240 Mbps на една машина. Десет минути без интернет за него. Първо избрахме всеки бан да е blackhole, с праг 30 000 пакета в секунда, за да не може един клиент да вземе ресурса на хилядата около него.
На следващия ден го направихме по-точно. FastNetMon Community не комбинира прагове: банва, ако мине pps или Mbps, условие „и“ няма. Затова FastNetMon вече само казва „над прага“, а blackhole решава скриптът по средния размер на пакета. Атаките, които товарят рутери и сървъри, са с дребни пакети: SYN около 60 байта, ICMP около 50. Свалянията са с пакети от 1200 до 1500 байта. Границата е 400 байта, а прагът се върна на 20 000 пакета в секунда.
| Към един адрес | Резултат |
|---|---|
| над 20 000 pps, среден пакет под 400 B | blackhole + съобщение |
| над 20 000 pps, среден пакет над 400 B | само запис, трафикът минава |
| UDP amplification, фрагменти | blackhole и при едри пакети |
| ICMP над 1 000 pps | blackhole |
if [ "$AVG" -lt 400 ] || [ "$KIND" = amp ] || [ "$KIND" = frag ] || [ "$KIND" = icmp ]; then
gobgp global rib -a ipv4 add "$IP/32" nexthop 198.51.100.10 community 65001:666
fi
Размерът на пакета е по-добър от праг по Mbps, защото расте заедно с атаката. При 30 000 pps границата от 400 байта е около 100 Mbps. Но атака от 80 000 pps и 150 Mbps е с пакети от 234 байта. Праг „под 100 Mbps“ би я пропуснал, а размерът я хваща.
В Telegram идва само блокиран клиент. Ако някой се обади, че няма интернет, отговорът е там.
Едно ограничение остава. Доставчиците ни не приемат blackhole от нас, така че атаката пак стига до граничния рутер. Пазим вътрешната мрежа и останалите клиенти, но не и самия uplink.
Какво следва
Няколко дни наблюдение, преди да се пипа нещо друго. Дали усредняването за 30 секунди наистина маха фалшивите скокове. Дали границата от 400 байта пуска свалянията и хваща атаките. И първата истинска IPv6 атака, за да видим веригата докрай.
По темата: Blackhole маршрути вместо firewall — как същият граничен рутер блокира познатите зловредни мрежи, без да пипа firewall.