Мой первый DDoS
Статья пишется по горячим следам. DDoS не прекратился, и в данный момент атака продолжается.
До сегодняшнего дня я никогда не сталкивался с подобным типом атак. Слышать – слышал. Читал интересные статьи. Но не более того.
DDoS (Distributed Denial of Service) – это распределенная атака типа «отказ в обслуживании». Цель атаки – блокировать работу атакуемого сайта. DDoS атаки бывают разные по своей структуре и используемым технологиям. Но их объединяет одно: рядовые пользователи не могут получить доступ к сайту. Если на одном сервере находятся несколько сайтов, то все эти сайты становятся недоступными.
В ночь с 11 на 12 ноября одной из таких атак подвергся мой сайт lgnd.ru. Скажу пару слов о том, почему стали атаковать этот сайт. Для 99,9% пользователей Интернета сайт не представляет никакого интереса. Целевая аудитория – игроки браузерной стратегии heroeswm.ru. На моем сайте отображается различная статистическая информация по этой игре. Посещаемость 1000-5000 уников в сутки (в зависимости от событий в самой игре).
Сейчас идет голосование Премии Рунета. В прошлые годы конкуренты DDoS‘или сайт самой игры. В этом году досталось и сопутствующим ресурсам (справкам, газетам, сайтам со статистикой, клановым сайтам).
Начало
Итак. Я запустил компьютер в 6-00 мск. Зашел в блог, зашел на сайт статистики – ничего необычного не заметил. А вот в логах CRON’а обнаружились интересные строки:
Disk is full writing './lgnd@002eru/stat_radio.MYD' (Errcode: 122). Waiting for someone to free space... |
Кончилось место! А я ведь только на выходных чистил бэкапы и логи. Было занято 5Gb из 15Gb. За одну ночь пропало 10Gb!
Открываю консоль. Ищу растрату места.
Для этого я использую небольшой сниппет:
cd / du | sort -nr | head -30 |
Команда выведет 30 самых прожорливых мест (файлов и каталогов).
После выполнения этой команды я увидел, что nginx/error.log
занимает 9.5Gb. Но что же случилось?
top |
И вижу такую картину.
Load Average – 30!
Первая оптимизация
Сразу же удаляю все логи, чтобы очистить место. Перезапускаю apache, nginx, mysql, munin. Снова свободно 10Gb. Место начинает плавно забиваться. За 5 минут съедается около 1Gb места, а значит у меня есть менее часа, чтобы найти более радикальное решение (или снова очистить логи).
Основную нагрузку создают процессы apache, поэтому ограничиваю количество одновременных процессов:
Timeout 20 MaxKeepAliveRequests 30 |
Сервер немного отпустило. Могу работать с консолью без задержек. LA постепенно снижается. Но по-прежнему каждые 5 минут чищу логи nginx:
cat /dev/null > /var/log/nginx/access.log cat /dev/null > /var/log/nginx/error.log |
Параллельно ищу более грамотное решение.
Для начала вспоминаю про команду tail
, которая выводит последние 10 строк из указанного файла.
tail /var/log/nginx/error.log |
Вижу, что все запросы имеют вид:
client: 2.179.67.39, server: lgnd.ru, request: "GET /event/index HTTP/2.0", host: "lgnd.ru", referrer: "http://lgnd.ru/event/index" |
Бегло пробежавшись глазами по логу, замечаю что все запросы идут к одной странице! На мое счастье – это не главная страница, а значит её можно просто заблокировать на уровне nginx. Для этого в секции server необходимо добавить запрет для данного location:
server { listen 80; charset utf-8; server_name .lgnd.ru; ... location = /event/index { deny all; } ... } |
Теперь любой пользователь, запросив http://lgnd.ru/event/index
, получит 403 ошибку.
После этого нагрузка стала незначительной. Все сайты на этом сервере полностью возобновили свою работу. На этом можно было бы и остановиться, но я продолжил искать решение.
Бан по IP
На просторах Интернета обнаружилось одно интересное решение: анализируем лог с ошибками и самые активные IP адреса добавляем в бан-лист с помощью iptables.
Скрипт получился такой:
cat /var/log/nginx/error.log | grep "GET /event/index" | grep -oE '[0-9]{1,}\.[0-9]{1,}\.[0-9]{1,}\.[0-9]{1,}' |sort |uniq -c |sort -rn > /tmp/botnet.blacklist awk '{print "iptables -A INPUT -p tcp --dport 80 -s " $2 " -j DROP" }' /tmp/botnet.blacklist | head -n 50 > /tmp/iptables_ban.sh sh /tmp/iptables_ban.sh cat /dev/null > /var/log/nginx/error.log |
В 1 строчке мы выбираем все строчки из лога с ошибками, в которых есть запрос проблемной страницы /event/index
. Если бы запрашивали случайные страницы, можно было бы просто фильтровать по числу запросов. В моем случае все запросы идут к одной странице, что сильно упрощает мне работу.
Пример того, что будет содержаться в /tmp/botnet.blacklist
:
665 117.201.137.60 613 115.186.125.20 584 202.53.169.246 461 115.118.49.1 420 114.120.97.91 413 182.177.186.109 397 39.209.165.228 336 113.162.114.199 304 187.15.180.49 260 125.60.246.139 |
Во 2 строчке мы создаем bash скрипт, которым будем банить ботов по IP.
Пример созданного /tmp/iptables_ban.sh
:
iptables -A INPUT -p tcp --dport 80 -s 117.201.137.60 -j DROP iptables -A INPUT -p tcp --dport 80 -s 115.186.125.20 -j DROP iptables -A INPUT -p tcp --dport 80 -s 202.53.169.246 -j DROP iptables -A INPUT -p tcp --dport 80 -s 115.118.49.1 -j DROP iptables -A INPUT -p tcp --dport 80 -s 114.120.97.91 -j DROP |
В 3 и 4 строке мы выполняем этот скрипт и очищаем error.log
.
Этим способом нельзя защищаться постоянно. С ростом числа правил в iptables производительность всего сервера падает.
GeoIP Бан
Или бан по стране пользователя с помощью модуля geoip. Игра, которой посвящен мой сайт, создана для русскоязычных игроков. Пользователи СНГ составляют 98% всех посетителей lgnd.ru
. Заблокировав азиатские и африканские страны, я потеряю не более 0,1% пользователей.
Выборочно проверив местоположение самых активных ботов, я пришел к выводу, что это – азиатский ботнет. Основными странами были Индия, Китай, Филиппины, Малайзия и т.д.
Для реализации блокировки по стране я использовал модуль http_geoip_module. Кроме самого модуля потребовалось скачать geoip базу:
cd /etc/nginx/geoip_module wget http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz gunzip GeoIP.dat.gz |
После чего в конфиге nginx добавляем:
http { ... geoip_country /etc/nginx/geoip_module/GeoIP.dat; ... |
В конфиге с настройкой сервера:
server { listen 80; charset utf-8; server_name .lgnd.ru; set $a 1; if ($geoip_country_code = "CN") { set $a 0; } if ($geoip_country_code = "IN") { set $a 0; } if ($a = 0) { return 444; } ... |
Перезапускаем:
service nginx restart |
Теперь в ответ на запрос с китайского или индийского IP будет возвращен код 444. Это – нестандартный код, поэтому nginx просто закроет соединение. Если указать 403 или 404 код, то серверу придется отправить около 100 байт информации (заголовки, html страница с кодом ошибки и т. д.). Во время DDoS каждую секунду приходят сотни запросов. Закрывая соединение, мы экономим ресурсы своего сервера.
Стоит отметить, что для международных сайтов такой способ не подходит.
Бан по referer
Бан по стране позволил уменьшить число запросов в 3-4 раза. Оставшиеся боты создавали минимальную нагрузку. Но я не остановился на достигнутом, продолжил изучать логи. В какой-то момент я увидел общую закономерность: у всех DDoS запросов был один и тот же referer.
referrer: "http://lgnd.ru/event/index" |
Я быстро добавил правило в конфиг сервера:
server { listen 80; charset utf-8; server_name .lgnd.ru; if ($http_referer = "http://lgnd.ru/event/index") { return 444; } ... |
И на этом все закончилось. Размер лога ошибок замер на нулевой отметке. DDoS был побежден.
Графики Munin
Для мониторинга состояния серверов я использую Munin. К сожалению, когда кончилось место, графики перестали формироваться, поэтому ночью образовалась большая пропасть.
Как быстро закончилось место.
Мощность DDoS‘a составляла 10Мбит. 2500 запросов в секунду.
MySQL пришлось нелегко:
Ещё есть занятное значение $server_protocol
Можно было сразу прописать:
if ($server_protocol = «HTTP/2.0»)
{
return 444;
}