Доступные методы борьбы с DDoS-атаками для владельцев vds/dedicated серверов с Linux

в 6:44, , рубрики: ddos, linux, vds, администрирование, Серверное администрирование, системное администрирование, метки: , ,

image

Начать свое присутствие на Хабре мы решили с материала, подготовленного для Конференции уральских веб-разработчиков, в котором описаны проверенные на собственной практике и оказавшиеся вполне успешными методы борьбы с 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 http://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: "http://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 раз – боты.

Теперь перед нами стоит две проблемы:

  1. Как выбрать из лога период «последние 5 минут»?
  2. Как отсортировать только тех, кто был заблокирован более 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 "%sn", 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 состоит из следующих логических блоков:

  1. Разрешаем трафик по уже установленным соединениям.
  2. Прописываем разрешения для «своих» ip-адресов.
  3. Таблица whitelist – это исключения.
  4. Таблица DDoS – это идентифицированные нами боты.
  5. Таблица friends – это сети РуНета, которым мы разрешаем доступ, если пакет дошел до этого уровня.
  6. Всем остальным – -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 http://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 http://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 "%sn", 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-соединений.

Мы надеемся, что эта статья будет полезна для владельцев VDS или Dedicated серверов. Просим оставлять свои комментарии и замечания.

Автор: NetAngels

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js