Введение
Целевая аудитория данной статьи — это программисты, имеющие в распоряжении vds или dedicated. Эта статья не претендует на полноценное руководство по борьбе с DDoS-атаками и многие сисадминские нюансы здесь намеренно опущены. Мы рассматриваем только ddos типа http flood как наиболее распространенный тип ddos и наиболее дешевый для заказчика. Суть таких атак сводится к частому запрашиванию каких-то страниц сайта ботами, что приводит к исчерпанию ресурсов сервера и невозможности посещения атакуемого сайта обычными посетителями.
Связка nginx — apache — fastcgi/wsgi. Узкие места
Типовая схема организации работы веб-приложения состоит из 3х уровней: это reverse proxy-сервер (например nginx), apache (web сервер), и какое-то fastcgi/wsgi/… приложение. Могут быть вырожденные случаи, когда нет apache, или при использовании mod_php/mod_python, когда нет выделенного приложения (оно встроено в веб-сервер), но суть работы схемы при этом не меняется, меняется только количество уровней в ней.
Fcgi сервер может запустить несколько десятков процессов, параллельно обрабатывающих входящие запросы. Увеличить это значение можно только до определенного предела, пока процессы помещаются в памяти. Дальнейшее увеличение приведет к swapping’у. При DDoS-атаке или при высокой посещаемости, когда все текущие процессы fcgi уже заняты обработкой поступивших запросов, вновь поступающие запросы apache ставит в очередь, пока либо не освободится какой-то из fcgi процессов, либо не возникнет таймаут нахождения в очереди (в этом случае возникает ошибка 503).
Apache точно так же имеет лимит на кол-во коннектов, как правило несколько сотен (на порядок больше, чем fcgi). После того, как все коннекты к apache исчерпаны, запросы в очередь уже ставит nginx.
Nginx, в силу своей асинхронной архитектуры может спокойно держать несколько тысяч коннектов при очень скромном расходе памяти, поэтому типовые DDoS-атаки не доходят до такого уровня, когда nginx не в состоянии принимать новые коннекты, если nginx настроен соответствующим образом.
Фильтрация трафика на nginx. Разбор логов nginx
Предлагаемая нами методика сводится к тому, чтобы лимитировать общее кол-во запросов к сайту определенным значением (напр. 1500 в минуту, в зависимости от того, сколько максимум хитов может выдержать движок сайта при текущих серверных мощностях). Все, что будет превышать это значение, мы первоначально будем фильтровать с помощью nginx (limit_req_zone $host zone=hostreqlimit:20m rate=1500r/m;
). Затем мы будем смотреть в логи nginx и вычислять там те IP-адреса, которые были отфильтрованы более определенного количества раз за определенный промежуток времени (например более 100 раз за 5 минут) и запрещать доступ к нам этим IP-адресам с помощью firewall.
Почему мы не используем традиционный и часто рекомендуемый лимит по подключениям с одного и того же ip адреса (limit_req_zone $binary_remote_addr ...
)? Во-первых, под этот лимит попадут клиенты провайдеров, сидящие за nat’ом. Во-вторых, этот порог установить как-то универсально не возможно, потому что есть сайты с ajax и большим кол-вом js/css/картинок и у них в принципе на загрузку одной страницы может быть несколько десятков хитов, поэтому такой порог использовать можно, но только для каждого сайта индивидуально. В третьих, для т.н. «вялотекущих» DDoS-атак боты вообще не будут попадать под этот порог, их просто будет много, но каждый в отдельности бот будет делать не много запросов за короткий период времени, в результате мы ничего не сможем отфильтровать, а сайт при этом работать не будет.
Для того, чтобы воспользоваться нашим методом, конфигурационный файл nginx, при работе nginx в качестве reverse proxy для apache, должен выглядеть примерно следующим образом:
http {
limit_req_zone $host zone=hostreqlimit:20m rate=1500r/m;
...
server {
listen 1.2.3.4;
server_name domain.ru www.domain.ru;
limit_req zone=hostreqlimit burst=2500 nodelay;
location /
{
proxy_pass https://127.0.0.1:80;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
}
}
}
В этом конфиге так же подразумевается, что apache у нас слушает на loopback интерфейсе на 127.0.0.1:80, а nginx на 80м порту на нашем внешнем ip-адресе (1.2.3.4) и на порту 8080 на 127.0.0.1.
Отфильтрованные nginx’ом хиты будут сопровождаться такой записью в error.log nginx’а:
2012/01/30 17:11:48 [error] 16862#0: *247484 limiting requests, excess: 2500.200 by zone "hostreqlimit", client: 92.255.185.237,
server: domain.ru, request: "GET / HTTP/1.1", host: "domain.ru", referrer: "https://www.yahoo.com/"
Чтобы получить из error.log список всех блокировавшихся ip-адресов мы можем выполнить следующее:
cat error.log | awk '/hostreqlimit/ { gsub(", ", " "); print $14}' | sort | uniq -c | sort -n
Но мы с вами помним, что мы тут блокируем просто всех, кто обратился к сайту после того, как счетчик обращений насчитал 1500 раз в минуту, поэтому не все заблокированные — боты. Ботов же можно выделить, если провести какую-то условную черту по количеству блокировок. Как правило для такой черты выбирается значение в несколько сотен раз за 5-15 минут. Например, мы пополняем список ботов раз в 5 минут и считаем что все, кого nginx заблокировал более 200 раз — боты.
Теперь перед нами стоит две проблемы:
- Как выбрать из лога период «последние 5 минут»
- Как отсортировать только тех, кто был заблокирован более N раз
Первую проблему решаем при помощи
tail -c +OFFSET
. Идея сводится к тому, что после разбора error.log мы записываем во вспомогательный файл его текущий размер в байтах (
stat -c '%s' error.log > offset
), а при следующем разборе отматываем error.log на последнюю просмотренную позицию (
tail -c +$(cat offset)
). Таким образом, запуская разбор логов раз в 5 минут, мы будем просматривать только ту часть лога, которая относится к последним 5 минутам.
Вторую проблему решаем при помощи скрипта на awk. В итоге получим (THRESHOLD — это тот самый лимит по количеству блокировок, после которого соответствующий IP-адрес считается принадлежащим атакующему нас боту):
touch offset; (test $(stat -c '%s' error.log) -lt $(cat offset) 2>/dev/null && echo 0 > offset) || echo 0 > offset; \
tail -c +$(cat offset) error.log | awk -v THRESHOLD=200 '/hostreqlimit/ { gsub(", ", " "); a[$14]++; } \
END { for (i in a) if (a[i]>THRESHOLD) printf "%s\n", i; }' ; stat -c '%s' error.log > offset
Подразумевается, что этот набор команд выполняется в той директории, где лежит error.log от nginx, то есть как правило это /var/log/nginx
. Полученный в результате список мы можем отправить в firewall на блокировку (об этом ниже).
Как просто можно построить список сетей для бана.
Еще одна стоящая перед нами задача при DDoS — это максимально ограничить доступ к нашему сайту для тех, кто не является его потенциальным посетителем, потому что ботнеты могут содержать десятки тысяч компьютеров и зачастую отсечь лишние ip-адреса целыми посетями гораздо проще, чем вылавливая каждого бота в отдельности.
Первое, что нам может помочь, это список сетей Рунета на сайте NOC masterhost. В настоящий момент в этом списке почти 5000 сетей. Большинство российских сайтов ориентированы на посетителей из России, поэтому отсечь всех заграничных посетителей, а вместе с этим и всех заграничных ботов, выглядит вполне логичным решением. Однако, в последнее время внутри Российских сетей возникает все больше и больше самостоятельных ботнетов, поэтому такое решение хоть и обосновано, но очень часто отнюдь не спасает от атаки.
Если сайт имеет устоявшееся community (ядро), то мы можем выбрать список IP адресов постоянных посетителей из логов веб-сервера за последние 3-4 недели. Хотя новые посетители на время атаки на сайт попадать не смогут, но зато старые активные пользователи скорее всего даже не заметят никакой атаки. Кроме того, среди постоянных посетителей врядли будут боты, поэтому такой метод может в принципе сам по себе остановить атаку на какое-то время.
Если это сайт местного значения, можно забанить на firewall всех, кроме сетей местных провайдеров и сетей поисковых систем (Яндекс).
Введение в iptables, пример простейшего firewall
В ОС Linux firewall работает на базе iptables. Фактически суть работы iptables сводится к тому, что для каждого пакета трафика, принимаемого снаружи или отправляемого с сервера, применяется определенный набор правил, которые могут повлиять на судьбу данного пакета. В самом простом случае правила просто говорят, что пакет нужно либо принять (ACCEPT), либо отбросить (DROP). Правила подразделяются на цепочки (chains). Например, принимаемые сервером из Интернета пакеты попадают в цепочку INPUT, где для каждого пакета с самого начала правил в цепочке проверяется, подходит ли данный пакет под описанные в правиле условия и если подходит, то к пакету применяется это правило, а если нет, то пакет передается следующему правилу. Если ни одно из правил для пакета не было применено, то к пакету применяется политика по-умолчанию (policy).
В качестве простого примера напишем правила firewall, которые разрешают подключение к серверу по ssh только из нашего офиса (с ip-адреса 1.2.3.4), а всем остальным доступ по ssh блокируют:
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
-A INPUT -s 1.2.3.4/32 -m comment --comment "our office" -j ACCEPT
-A INPUT -p tcp -m tcp --dport 22 -j DROP
COMMIT
Эти строки можно записать в текстовый файл и загрузить в firewall с помощью:
iptables-restore < firewall.txt
, а сохранить текущее состояние firewall в файл:
iptables-save > firewall.txt
.
Работают эти правила следующим образом. Первая строка — разрешаем весь трафик для всех соединений, которые уже открыты (процедура handshake пройдена). Вторая строка — разрешаем любой трафик с ip-адреса 1.2.3.4 и помечаем комментарием, что это наш офис. На самом деле сюда доходят только пакеты, устанавливающие какое-либо соединение, то есть пакеты типа syn и ack, все остальные пакеты проходят только первую строку. Третья строка — запрещаем всем подключение по tcp на 22й порт. Сюда доходят попытки подключения (syn, ack) по ssh от всех, кроме нашего офиса.
Интересно, что первую строчку можно смело удалить. Плюс наличия такой строки: для уже открытых соединений в firewall отработает всего одно правило, а пакеты в рамках уже открытых соединений — это подавляющее большинство принимаемых нами пакетов, то есть firewall с такой строкой в самом начале практически не будет вносить никаких дополнительных задержек в работу сетевого стека сервера. Минус: эта строка приводит к активации модуля conntrack, который держит в памяти копию таблицы всех установленных соединений. Что затратнее: держать копию таблицы соединений или необходимость обрабатывать несколько правил firewall на каждый пакет — это индивидуальный нюанс каждого сервера. Если firewall содержит всего несколько правил, на наш взгляд правильнее строить его правила так, чтобы модуль conntrack не активизировался.
В iptables можно создавать дополнительные цепочки, задаваемые пользователем. В каком-то смысле это выглядит как аналог вызова функций в языках программирования. Создаются новые цепочки просто: iptables -N chain_name
. Используются создаваемые таким образом цепочки для того, чтобы разделять firewall на разные логически блоки. Об этом подробнее далее.
Рекомендуемая структура firewall для противодействия ddos
Рекомендуемая нами структура для противодействия DDoS состоит из следующих логических блоков:
- Разрешаем трафик по уже установленным соединениям
- Прописываем разрешения для своих ip-адресов
- Таблица whitelist — это исключения
- Таблица ddos — это идентифицированные нами боты
- Таблица friends — это сети РуНета, которым мы разрешаем доступ, если пакет дошел до сюда
- Всем остальным — -j DROP
В терминах iptables это выглядит так:
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:ddos - [0:0]
:friends - [0:0]
:whitelist - [0:0]
-A INPUT -i lo -j ACCEPT
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
-A INPUT -s 1.2.3.4/32 -m comment --comment "our office" -j ACCEPT
-A INPUT -p tcp -m tcp --dport 22 -j DROP
-A INPUT -j whitelist
-A INPUT -j ddos
-A INPUT -j friends
-A INPUT -j DROP
-A whitelist -s 222.222.222.222 -j ACCEPT
-A whitelist -s 111.111.111.111 -j ACCEPT
-A ddos -s 4.3.2.0/24 -j DROP
-A friends -s 91.201.52.0/22 -j ACCEPT
COMMIT
Опять же, целесообразность наличия второй строки под вопросом и в зависимости от полного размера firewall она может как ускорять его работу, так и тормозить.
Заполняем таблицу friends:
for net in $(curl -s https://noc.masterhost.ru/allrunet/runet); do iptables -A friends -s $net -j ACCEPT; done
Проблема такого firewall в его монстроидальности: таблица friends в случае Рунета будет содержать порядка 5000 правил. Таблица ddos в случае более-менее среднего ddos’а будет содержать еще 1-2 тысячи записей. Итого firewall будет состоять из 5-7 тысяч строк. При этом все пакеты, прилетающие от заграничных отправителей, которые должны быть просто отброшены, на самом деле будут проходить все 5-7 тысяч правил, пока не доберутся до последнего: -A INPUT -j DROP Сам по себе такой firewall будет отъедать огромное количество ресурсов.
Ipset — решение для монстроидальных firewall’ов.
Ipset полностью решает проблему с монстроидальными firewall, в которых присутствуют тысячи строк с описанием того, что делать с пакетами с разными адресами отправителей или получателей. Ipset представляет собой утилиту по управлению специальными set’ами (наборами однотипных данных), где для нескольких заранее определенных типов данных сделаны специальные hash-таблицы, позволяющие очень быстро устанавливать факт наличия или отсутствия определенного ключа в этой таблице. В каком-то смысле это аналог memcached, но только гораздо более быстрый и позволяющий при этом хранить только несколько конкретных типов данных. Создадим новый набор данных для хранения информации об ip-адресах ddos-ботов:
ipset -N ddos iphash
Здесь последним параметром указывается тип создаваемой таблицы: nethash — это set для списка сетей, iphash — для отдельных ip адресов. Есть разные варианты таблиц, подробности в man ipset. Соответственно whitelist и friends — это таблицы типа nethash, а ddos — iphash.
Чтобы воспользоваться созданной таблицей ipset в firewall, достаточно одного правила (строки firewall), например:
-A INPUT -m set --match-set whitelist src -j ACCEPT
-A INPUT -m set --match-set ddos src -j DROP
Добвить какой-то ip-адрес во вновь созданную таблицу можно так:
ipset -A ddos 1.2.3.4
таким образом, весь наш firewall при использовании ipset сводится к:
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -i lo -j ACCEPT
-A INPUT -s 1.2.3.4/32 -m comment --comment "our office" -j ACCEPT
-A INPUT -p tcp -m tcp --dport 22 -j DROP
-A INPUT -m set --match-set whitelist src -j ACCEPT
-A INPUT -m set --match-set ddos src -j DROP
-A INPUT -m set --match-set friends src -j ACCEPT
-A INPUT -j DROP
COMMIT
Заполняем set friends (тип nethash):
for net in $(curl -s https://noc.masterhost.ru/allrunet/runet); do ipset -A friends $net; done
заполняем set ddos из показанной ранее команды:
touch offset; (test $(stat -c '%s' error.log) -lt $(cat offset) 2>/dev/null && echo 0 > offset) || echo 0 > offset; \
for ip in $(tail -c +$(cat offset) error.log | awk -v THRESHOLD=300 \
'/hostreqlimit/ { gsub(", ", " "); a[$14]++; } END { for (i in a) if (a[i]>THRESHOLD) printf "%s\n", i; }' ; \
stat -c '%s' error.log > offset); do ipset -A ddos $ip; done
Используем модуль TARPIT:
Модуль iptables под названием tarpit представляет собой т.н. «ловушку». Принцип работы tarpit такой: клиент присылает syn-пакет для попытки установки handshake (начало установки tcp-соединения). Tarpit отвечает ему syn/ack пакетом, о котором тут же забывает. При этом никакое соединение на самом деле не открывается и никакие ресурсы не выделяются. Когда от бота приходит конечный ACK-пакет, модуль tarpit отправляет назад пакет, устанавливающий размер окна для передачи данных на сервер равным нулю. После этого любые попытки закрыть это соединение со стороны бота tarpit’ом игнорируются. Клиент (бот) считает, что соединение открыто, но «залипло» (размер окна 0 байт) и пытается закрыть это соединение, но он ничего не может сделать вплоть до истечения таймаута, а таймаут, в зависимости от настроек — это порядка 12-24 минут.
Использовать tarpit в firewall можно следующим образом:
-A INPUT -p tcp -m set --match-set ddos src -j TARPIT --tarpit
-A INPUT -m set --match-set ddos src -j DROP
Собираем xtables-addons
К сожалению, модули ipset и tarpit в стандартном наборе современных дистрибутивов отсутствуют. Их нужно установить дополнительно. Для более-менее свежих дистрибутивов Debian и Ubuntu это делается достаточно просто:
apt-get install module-assistant xtables-addons-source
m-a a-i xtables-addons
После этого система сама скачает все нужное для сборки ПО, сама все соберет и сама все установит. Для других дистрибутивов Linux действия аналогичные, но за конкретикой мы предлагаем обратиться к справочному руководству.
Тюнинг ядра.
Как правило, разговоры о борьбе с ddos-атаками начинаются с рекомендаций по тюнингу ядра ОС. Однако, на наш взгляд, если ресурсов в принципе мало (например, в наличии менее одного Гб памяти), то тюнинг ядра смысла не имеет, т.к почти ничего не даст. Максимум полезного в этом случае будет — включить т.н. syncookies. Включение syncookies позволяет эффективно бороться с атаками типа syn flood, когда сервер забрасывается большим количеством syn-пакетов. Получая syn-пакет сервер должен выделить ресурсы на открытие нового соединения. Если за syn-пакетом не последует продолжение процедуры установки соединения, сервер выделит ресурсы и будет ждать, пока не произойдет таймаут (несколько минут). В конечном итоге, без syncookies, при достаточном количестве отправленных серверу syn-пакетов он не сможет более принимать соединения, потому что система израсходует на хранение информации о полуоткрытых соединениях все свои ресурсы.
Параметры ядра, о которых пойдет речь, правятся с помощью команды sysctl:
sysctl [-w] option
Опция -w
означает, что вы хотите записать новое значение в какой-то параметр, а ее отсутствие, что вы хотите прочитать текущее значение этого параметра. Рекомендуется поправить следующие параметры:
net.ipv4.tcp_syncookies=1
net.ipv4.ip_local_port_range = 1024 65535
net.core.netdev_max_backlog = 30000
net.ipv4.tcp_max_syn_backlog = 4096
net.core.somaxconn = 4096
net.core.rmem_default = 124928
net.core.rmem_max = 124928
net.core.wmem_max = 124928
net.ipv4.tcp_rmem = 4096 8192 87380
net.ipv4.tcp_wmem = 4096 8192 65536
Параметр net.ipv4.tcp_syncookies отвечает за включение механизма syncookies; net.core.netdev_max_backlog определяет максимальное количество пакетов в очереди на обработку, если интерфейс получает пакеты быстрее, чем ядро может их обработать; net.ipv4.tcp_max_syn_backlog определяет максимальное число запоминаемых запросов на соединение, для которых не было получено подтверждения от подключающегося клиента; net.core.somaxconn максимальное число открытых сокетов, ждущих соединения. Последние 5 строк — это различные буферы для tcp-соединений.
Оригинал: https://www.netangels.ru/support/ddos/
Спасибо, Антону Халикову.
Просмотров: 1118