Последнее время стали довольно распространены высокоскоростные атаки, осуществляемые с использованием различных вариантов усиления (amplification) трафика.
Как же с ними бороться? Довольно сложно, это стоит понимать. Дешевые решения в виде ACL на 10GE свиче либо аппаратных правил на NIC (сетевой карте) - могут блокировать лишь часть паразитного трафика с очень ограниченным набором параметров - src/dst IP, src/dst port и протокол.
Более серьезные решения требуют привлечения либо высокопроизводительных роутеров (речь про роутеры класса Juniper MX 120+, и к слову, возможности фильтрации трафика у роутеров тоже весьма ограничены) либо аппаратных фаерволлов, например, от Arbor. Роутеры стоят огромных денег, но почти всегда есть в ДЦ - это плюс. А специализированное железо от Arbor стоит совершенно непомерные суммы.
Итак, наше решение софт бокс-фильтр, машина 10GE картами, кучей процессорных мощностей и Linux либо FreeBSD на борту.
Стоит понимать, что стандартными средствами того же Linux атаку в 10Mpps не отбить, в конце концов, он просто упадет, например, вот
так. Во FreeBSD ситуация во многом похожая и еще более усложняющаяся (для меня) не особо хорошим пониманием этой ОС.
Поэтому в качестве решения я обратился к системам прямого доступа к сетевой карте класса netmap, PF_RING, DPDK, а именно - мы будем использовать netmap, потому что он очень быстр, не требует лицензии и под него есть портированный фаерволл
ipwf
.
Итак, приготовим машину на Debian 7, с сетевыми картами ixgbe с по меньшей мере двумя интефейсами. Так как машина должна подключаться в разрез между атакуемой машиной и аплинком.
Сборка довольно проста, сначала нужно
собрать netmap со всеми драйверами и загрузить его.
После этого нужно установить в систему заголовочные файлы для netmap, чтобы собрать тулкит:
Стягиваем заголовки:
cd /usr/include/net
wget https://raw.githubusercontent.com/luigirizzo/netmap/master/sys/net/netmap_user.h
wget https://raw.githubusercontent.com/luigirizzo/netmap/master/sys/net/netmap.h
Собираем:
cd /usr/src
git clone https://github.com/luigirizzo/netmap-ipfw.git
cd netmap-ipfw
make
Запускаем:
./kipfw netmap:eth4 netmap:eth6
После этого можем загрузить тестовое правило в ipfw:
cd ipfw
./ipfw add 1 deny tcp from any to any 80 in
Для испытания насколько это решение хорошо в продакшене я запустил syn флуд в 7mpps с тестовой машины силами trafgen.
Метрики моей тесты машины (а это слабый CPU E5-2407 0 @ 2.20GHz x 4) на картинке:
top - 13:33:05 up 3 min, 2 users, load average: 0.44, 0.13, 0.05
Tasks: 78 total, 2 running, 76 sleeping, 0 stopped, 0 zombie
%Cpu(s): 10.0 us, 14.8 sy, 0.0 ni, 74.6 id, 0.0 wa, 0.0 hi, 0.7 si, 0.0 st
KiB Mem: 32980328 total, 546592 used, 32433736 free, 45320 buffers
KiB Swap: 8387580 total, 0 used, 8387580 free, 32204 cached
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3105 root 20 0 660m 9060 8908 R 99.5 0.0 0:25.36 kipfw
1 [|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||100.0%] Tasks: 26, 3 thr, 52 kthr; 2 running
2 [ 0.0%] Load average: 0.60 0.19 0.07
3 [ 0.0%] Uptime: 00:03:48
4 [| 0.7%]
Mem[||| 458/32207MB]
Swp[ 0/8190MB]
Как можно видеть, машина прогрузилась лишь на 1/5, но одно ядро выгружено полностью (об этом отдельно).
Вполне неплохой показатель для такого слабого железа!
Теперь попробуем ту же самую задачу простого бриджинга сделать на Linux.
Ставим бридж утилс:
apt-get install -y bridge-utils
Добавляем интерфейсы в него:
brctl addbr mybridge
brctl addif mybridge eth4
brctl addif mybridge eth6
ifconfig mybridge up
И после этого вливаем те же 7mpps:
1 [||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||99.1%] Tasks: 25, 3 thr, 48 kthr; 2 running
2 [|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||100.0%] Load average: 0.47 0.23 0.15
3 [|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||100.0%] Uptime: 1 day, 14:24:01
4 [||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||99.1%]
Mem[|||| 497/32207MB]
Swp[ 0/8190MB]
Шапка top выглядит еще более ужасающе:
top - 13:21:10 up 1 day, 14:24, 2 users, load average: 1.57, 0.53, 0.26
Tasks: 73 total, 5 running, 68 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.0 us, 0.0 sy, 0.0 ni, 0.4 id, 0.0 wa, 0.0 hi, 99.6 si, 0.0 st
KiB Mem: 32980328 total, 812976 used, 32167352 free, 40784 buffers
KiB Swap: 8387580 total, 0 used, 8387580 free, 263172 cached
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
15 root 20 0 0 0 0 R 93.5 0.0 57:08.71 ksoftirqd/2
10 root 20 0 0 0 0 R 90.9 0.0 39:51.09 ksoftirqd/1
3 root 20 0 0 0 0 R 65.8 0.0 71:59.15 ksoftirqd/0
19 root 20 0 0 0 0 R 64.8 0.0 59:43.95 ksoftirqd/3
2806 root 20 0 20520 824 548 S 1.3 0.0 0:12.81 irqbalance
4723 root 20 0 0 0 0 S 1.0 0.0 0:18.05 kworker/1:2
342 root 20 0 0 0 0 S 0.7 0.0 0:13.04 kworker/2:1
2866 ntp 20 0 39076 2404 1764 S 0.7 0.0 0:08.80 ntpd
5163 root 20 0 0 0 0 S 0.7 0.0 0:32.55 kworker/0:1
Как можете видеть - Linux с этой задачей не справился потому что в нем используется схема с многократным копированием пакета вместо одноразового. Кроме этого, сам по себе стек Linux довольно тяжел и плохо приспособлен к подобной нагрузке по умолчанию.
А теперь попробуем силами все того же netmap-ipfw отбить эту атаку по отпечатку. А именно используя ее снимок из tcpdump:
11:43:10.008418 IP 198.18.51.106.62978 > 10.10.10.248.http: Flags [S], seq 1137192741, win 16, length 0
11:43:10.008419 IP 198.18.51.93.34560 > 10.10.10.113.http: Flags [S], seq 4245548258, win 16, length 0
11:43:10.008419 IP 198.18.51.98.30252 > 10.10.10.89.http: Flags [S], seq 1765400726, win 16, length 0
11:43:10.008420 IP 198.18.51.144.19980 > 10.10.10.91.http: Flags [S], seq 1052340006, win 16, length 0
11:43:10.008420 IP 198.18.51.30.38034 > 10.10.10.38.http: Flags [S], seq 2198215300, win 16, length 0
11:43:10.008421 IP 198.18.51.140.60388 > 10.10.10.133.http: Flags [S], seq 2831049352, win 16, length 0
В Linux блокировка такого трафика была бы задачей нетривиальной, но тут мы можем попробовать использовать необычайно малый размер окна и отсутствие опций tcp пакета вообще (в реальной жизни это нонсенс):
./ipfw add 8 deny tcp from any to any tcpwin 16 tcpdatalen 0
Сразу убеждаемся, что в правило начал лететь трафик:
./ipfw -a list
connected to 127.0.0.1:5555
nalloc 2248 nbytes 124 ptr (nil)
00008 578904841 23156193640 deny tcp from any to any tcpwin 16 tcpdatalen 0
65535 18831554 753262160 allow ip from any to any
Нагрузка машины-фильтра при этом была в рамках разумного:
top - 13:46:15 up 16 min, 2 users, load average: 0.87, 0.65, 0.40
Tasks: 74 total, 2 running, 72 sleeping, 0 stopped, 0 zombie
%Cpu(s): 21.8 us, 3.0 sy, 0.0 ni, 74.7 id, 0.0 wa, 0.0 hi, 0.4 si, 0.0 st
KiB Mem: 32980328 total, 547800 used, 32432528 free, 45708 buffers
KiB Swap: 8387580 total, 0 used, 8387580 free, 32824 cached
Как я уже обращал внимание, процесс kipfw выедает одно ядро полностью и это будет (если уже не является) причиной провалов в трафике, потому что он не успевает отбрабатывать весь проходящий трафик корректно.
Что с этим делать? Нужно делать патчи для kipfw для параллельной обработки либо попробовать запустить число инстансов kipfw по числу аппаратных очередей на сетевой карте. Таким образом в самом худшем случае на каждый процессор выйдет нагрузка не более чем 14/4=3.5Mpps на ядро, что в принципе разумная цифра.
В принципе, тот же самый подход скорее всего решит проблемы с производительностью kipfw на FreeBSD, там он ровно также упирается в скорость одного ядра.
Если у кого есть желание присоединиться к проекту в качестве тестера или разработчика - мои контакты справа.