Очень часто администраторы сталкиваются с ситуацией, когда какой-нибудь скрипт-кидди решит зафлудить сервер запросами прямо со своей домашней машины. Например в отместку за бан на форуме. Такие атаки, в отличие от настоящих DDoS-атак, отражать достаточно легко, но к сожалению не все администраторы умеют даже это. Поэтому попробуем поговорить о возможных способах защиты.
Сначала несколько случаев из личного опыта автора. Случай первый: шла долбёжка с одного адреса. Судя по всему работал скрипт, который в бесконечном цикле устанавливал tcp-соединение, запрашивал индексную страница сайта и рвал соединение.
Случай второй: так же с одного IP, так же судя по всему скрипт, устанавливал tcp-соединение и через секунду рвал.
Случай третий: чем-то напоминает первый, но после установки tcp-соединения посылался заголовок "Connection: Keep-Alive" и вместо одиночного запроса скрипт слал запросы по этому соединению в бесконечном цикле.
Во всех случаях скрипт был многопоточным и его целью было забить backlog на listen-сокете веб-сервера. Но в первом и третьем случае дополнительно страдали бэкенды, которые вынуждены были обрабатывать запросы (получать данные из кэша, бд, формировать страницу и т.д.).
Сразу напрашивается идея: ограничить число запросов, обрабатываемых для каждого IP-адреса в секунду, число одновременных соединений с каждого IP-адреса, и частоту установки новых tcp-соединений с каждого IP-адреса.
Поскольку комплекс серверов был построен аналогично описанному ранее, то есть состоял из фронтенда и пачки бэкендов, то разумеется защищать достаточно было фронтенд. В описываемом случае на фронтенде стоит Nginx, а сам фронтенд работает под управлением Ubuntu 12.04 LTS.
Небольшая но очень важная ремарка: статика отдавалась отдельным сервером, поскольку на статический контент приходилось около 70% всех запросов. Если отдавать статический контент вместе с динамическим то описываемые далее способы защиты могут нарушить отдачу статики, ошибочно приняв такие запросы за атаку. Потому настоятельно рекомендуется вынести статику на отдельный сервер.
Начнём с мелочей (хотя если говорить о проектах с высокой посещаемостью - мелочей там нет). Например если на фронтенде у вас несколько IP-адресов (скажем 1.1.1.1, 1.1.1.2 и 1.1.1.3) то первым делом стоит поменять параметр listen в конфигурации nginx:
# Было: #listen *:80; # Стало: listen 1.1.1.1:80; listen 1.1.1.2:80; listen 1.1.1.3:80;
Зачем? Да очень просто: в старом варианте создаётся один listen-сокет, а в новом - целых три. Поскольку backlog у сокета не резиновый, то надо либо делать несколько сокетов, либо увеличивать backlog. Но увеличение backlog не ускорит обработку очереди (к сожалению для каждого сокета она идёт в один поток), и клиенты из конца очереди могут не дождаться и отвалиться по тайм-ауту. Заодно такая настройка в целом положительно скажется на скорости обработки запросов клиентов, это особенно заметно на большом трафике.
Далее установим ограничения на число одновременных tcp-соединений и частоту установки новых.
В процессе эксплуатации решения выяснилось что трафик поисковых роботов временами воспринимался как попытка атаки, потому решено было адреса поисковых роботов добавить в исключения. Поисковики не публикуют адреса своих роботов, но любой администратор может провести анализ access-лога веб-сервера и вычленить оттуда адреса роботов.
Фрагмент обновлённой конфигурации скрипта iptables:
# Разрешаем http для google-бота for net in 66.249.0.0/16 74.125.0.0/16 72.14.0.0/16 173.194.0.0/16; do iptables -A INPUT -i ${IF_EXT} -s ${net} -m tcp -p tcp --dport 80 -j ACCEPT done # Разрешаем устанавливать новые tcp-соединения на 80-й порт не чаще 20 раз в секунду iptables -A INPUT -i ${IF_EXT} -m tcp -p tcp --dport 80 -m state --state NEW -m recent --name dpt80 --set iptables -A INPUT -i ${IF_EXT} -m tcp -p tcp --dport 80 -m state --state NEW -m recent --name dpt80 --update --seconds 1 --hitcount 20 -j DROP # Одновременно возможно не более 15 tcp-соединений на 80-й порт iptables -A INPUT -i ${IF_EXT} -m tcp -p tcp --syn --dport 80 -m connlimit --connlimit-above 15 -j REJECT --reject-with tcp-reset # А это правило уже было # Оно разрешает весь tcp-трафик на 80-й порт, который не был срезан предыдущими правилами iptables -A INPUT -i ${IF_EXT} -m tcp -p tcp --dport 80 -j ACCEPT
Хотя мы и рассматриваем защиту фронтенда на Linux, но всё-таки приведём аналогичные настройки и для pf на FreeBSD (фрагмент файла правил):
# Разрешаем трафик google-бота pass in quick on $if_ext inet proto tcp from 66.249.0.0/16 to ($if_ext) port 80 flags S/SA keep state pass in quick on $if_ext inet proto tcp from 74.125.0.0/16 to ($if_ext) port 80 flags S/SA keep state pass in quick on $if_ext inet proto tcp from 72.14.0.0/16 to ($if_ext) port 80 flags S/SA keep state pass in quick on $if_ext inet proto tcp from 173.194.0.0/16 to ($if_ext) port 80 flags S/SA keep state # Разрешаем не более 15 одновременных tcp-соединений на 80-й порт и установку со скоростью не чаще 20 раз в секунду pass in quick on $if_ext inet proto tcp from any to ($if_ext) port 80 flags S/SA keep state (max-src-conn 15, max-src-conn-rate 20/1) # Старое правило. Оно больше не нужно. #pass in quick on $if_ext inet proto tcp from any to ($if_ext) port 80 flags S/SA keep state
Переходим к настройкам nginx. Здесь мы будем использовать возможности модуля ngx_http_limit_req_module. Для начала опишем зону ограничений. Для этого создадим файл /etc/nginx/conf.d/req_zones.conf следующего содержания:
# Описываем зону с именем reqsglob и ограничением в 30 запросов в секунду limit_req_zone $binary_remote_addr zone=reqsglob:16m rate=30r/s; # Включаем ограничение # В данном случае будет выполняться не более 30 запросов в секунду, # Пропуская без задержек первые 9 запросов limit_req zone=reqsglob burst=9 nodelay;
Если вы всё-таки решили отдавать статический контент с основного фронтенда то имеет смысл в настройках виртуальных хостов описать два location'а. Один для "/", а другой для статики и ограничение вписывать не в глобальную конфигурацию, а в "location /".
Как проверить что получилось? Ну конечно же зафлудить сайт запросами самостоятельно:) Сделать это можно несложным скриптом. Например вот таким:
#!/usr/bin/perl use strict; use warnings; use diagnostics; use IO::Socket::INET; # IP-адрес вашего сервера my $conf_ip = '127.0.0.1'; # Имя вашего домена my $conf_host = 'www.example.com'; # Создаём 300 форков скрипта my $i = 0; for ($i = 0; $i < 299; $i++) { if (fork() == 0) { last; } } # В бесконечном цикле закидываем сайт запросами while (1) { eval { my $sock = IO::Socket::INET->new(PeerAddr => $conf_ip, PeerPort => '80', Proto => 'tcp', Timeout => '3') or print "Fail\n"; print $sock "GET / HTTP/1.1\nhost: $conf_host\n\n"; close $sock; } }
После запуска такого скрипта на своей машине вы вряд ли сможете зайти на тестируемый сайт с неё же. Потому либо запускайте скрипт на какой-то другой машине (с другим внешним адресом), либо найдите себе внешний прокси и попытайтесь зайти на сайт через него. Важно чтобы флуд шёл с одного адреса, а вы заходили с другого. Если зайти получилось - значит защита работает. Ни в коем случае не тестируйте чужие сайты: это уголовно-наказуемое деяние!
На этом всё. Приятной работы.
P.S. Эта статья ни в коем случае не претендует на звание серьёзного научного труда и охватывает лишь малую часть затронутой темы.
Vadim Bazilevich 2013-01-31 19:23:16 (#)
Если коротко механизм следующий:
клиенту присваиваться кука (боты в большей своей части с куками работать не умеют)
далее идет проверка наличия присвоенной куки - куки нет - запрос заворачивается опять на присвоение куки. Кука есть - запрос пропускается дальше.
На таком механизме сервер держал вполне сносно 800мб ддос на гигабитном канале. Далее проверить не удалось - ситуация не понравилась хостеру и из-за отсутствия у них механизмов фильтрации сервер был забанен. Пришлось переехать к более устойчивому к ддосу хостеру.
Есть еще один вариант самообучаемого фильтра на базе ipset. Но из-за не очень высокой скорости реакции на большом ддосе его хорошо использовать совместно с предыдущим фильтром.
Фильтр на nginx разгружает сервер, второй фильтр банит плохишей.
Вышеуказанная настройка nginx - вообще должна быть сделана по умолчанию. Еще неплохо при установке последнего проверять наличие модуля geo. Не последняя вещь при защите сервера.