Автор статьи объясняет, как реализовать в HAProxy ограничение скорости обработки запросов (rate limiting) с определенных IP-адресов. Команда Mail.ru Cloud Solutions перевела его статью — надеемся, что с ней вам не придется тратить на это столько времени и усилий, сколько пришлось потратить ему.
Дело в том, что это один из самых популярных методов защиты сервера от DoS-атак, но в интернете трудно найти понятную инструкцию, как конкретно его настроить. Методом проб и ошибок автор заставил HAProxy ограничить частоту запросов по списку IP-адресов, который обновляется в реальном времени.
Для настройки HAProxy не требуется никаких предварительных знаний, поскольку ниже излагаются все необходимые шаги.
Опенсорсный и бесплатный HAProxy — высокодоступный балансировщик нагрузки и прокси-сервер. В последние годы он стал очень популярным, поскольку обеспечивает высокую производительность с минимумом ресурсов. В отличие от альтернативных программ, некоммерческая версия HAProxy Community Edition предлагает достаточное количество функций для надежной балансировки нагрузки.
В этой программе поначалу довольно сложно разобраться. Однако у нее очень скрупулезная и подробная техническая документация. Автор говорит, что это самая подробная документация среди всех опенсорсных программ, какие он когда-либо использовал.
Итак, вот пошаговая инструкция.
Настройка балансировщика нагрузки
Чтобы сэкономить время и не отвлекаться на настройку инфраструктуры, возьмем образы Docker и Docker Compose — и быстро запустим основные компоненты.
Первая задача — поднять рабочий инстанс балансировщика нагрузки HAProxy с несколькими бэкенд-серверами Apache.
Клонируем репозиторий
$ git clone git@github.com:stargazer/haproxy-ratelimiter.git
$ cd haproxy-ratelimiter
Можете посмотреть на Dockerfile
и docker-compose.yml
с параметрами установки. Их обсуждение выходит за рамки данной статьи, поэтому остановимся на том, что они создали рабочий инстанс HAProxy под названием loadbalancer
с двумя бэкенд-серверами api01
и api02
. Для конфигурации HAProxy изначально будем использовать файл haproxy-basic.cfg
, а затем переключимся на haproxy-ratelimiting.cfg
.
Для простоты конфигурационный файл haproxy-basic.cfg
сокращен до самого необходимого минимума и очищен от лишнего. Посмотрим на него:
defaults
mode http
timeout connect 5000ms
timeout client 50000ms
timeout server 50000ms
frontend proxy
bind *:80
use_backend api
backend api
balance roundrobin
server api01 api01:80
server api02 api02:80
Раздел frontend proxy
устанавливает HAProxy прослушивать порт 80 и пересылать все запросы в пул серверов api
на бэкенде.
Раздел backend api
определяет пул бэкенда api
с двумя бэкенд-серверами api01
и api02
и соответствующими адресами. Сервер для обслуживания каждого входящего запроса выбирается алгоритмом балансировки нагрузки roundrobin
, то есть, по сути, два доступных сервера используются по очереди.
Давайте запустим все три наши контейнера
$ sudo docker-compose up
Теперь у нас есть контейнер loadbalancer
, который перенаправляет запросы на два сервера бэкенда api01
и api02
. Мы получим ответ от одного из них, если введем в адресной строке http://localhost/
.
Интересно несколько раз обновить страницу и посмотреть логи docker-compose
.
api01_1 | 192.168.192.3 - - [08/Jan/2019:11:38:09 +0000] "GET / HTTP/1.1" 200 45 api02_1 | 192.168.192.3 - - [08/Jan/2019:11:38:10 +0000] "GET / HTTP/1.1" 304 - api01_1 | 192.168.192.3 - - [08/Jan/2019:11:38:10 +0000] "GET / HTTP/1.1" 304 - api02_1 | 192.168.192.3 - - [08/Jan/2019:11:38:11 +0000] "GET / HTTP/1.1" 304 - api01_1 | 192.168.192.3 - - [08/Jan/2019:11:38:11 +0000] "GET / HTTP/1.1" 304 - api02_1 | 192.168.192.3 - - [08/Jan/2019:11:38:11 +0000] "GET / HTTP/1.1" 304 -
Как видим, два сервера api
обрабатывают запросы по очереди.
Теперь у нас есть инстанс HAProxy с очень простой конфигурацией балансировки нагрузки и имеется некоторое представление о том, как он устроен.
Добавляем лимит на количество запросов
Чтобы поставить лимит на количество запросов в балансировщик нагрузки, нужно изменить файл конфигурации в инстансе HAProxy. Следует убедиться, что контейнер loadbalancer
использует конфигурационный файл haproxy-ratelimiter.cfg
.
Просто измените Dockerfile, чтобы заместить файл конфигурации.
FROM haproxy:1.7
COPY haproxy-ratelimiter.cfg /usr/local/etc/haproxy/haproxy.cfg
Установка лимитов
Все настройки прописываются в конфигурационном файле haproxy-ratelimiter.cfg
. Давайте внимательно его изучим.
defaults
mode http
timeout connect 5000ms
timeout client 50000ms
timeout server 50000ms
frontend proxy
bind *:80
# ACL function declarations
acl is_abuse src_http_req_rate(Abuse) ge 10
acl inc_abuse_cnt src_inc_gpc0(Abuse) gt 0
acl abuse_cnt src_get_gpc0(Abuse) gt 0
# Rules
tcp-request connection track-sc0 src table Abuse
tcp-request connection reject if abuse_cnt
http-request deny if abuse_cnt
http-request deny if is_abuse inc_abuse_cnt
use_backend api
backend api
balance roundrobin
server api01 api01:80
server api02 api02:80
backend Abuse
stick-table type ip size 100K expire 30m store gpc0,http_req_rate(10s)
HAProxy предлагает набор низкоуровневых примитивов, которые обеспечивают большую гибкость и подходят для различных вариантов использования. Его счетчики часто напоминают мне накапливающий регистр (сумматор) в CPU. Они хранят промежуточные результаты, принимают на вход различную семантику, но в итоге — это просто цифры. Чтобы хорошо во всем разобраться, есть смысл начать с самого конца конфигурационного файла.
Таблица Abuse
backend Abuse
stick-table type ip size 100K expire 30m store gpc0,http_req_rate(10s)
Здесь мы устанавливаем фиктивный бэкенд под названием Abuse
(«злоупотребления»). Фиктивный, потому что он используется только для таблицы stick-table, к которой остальная конфигурация может ссылаться по имени Abuse
. Stick-table — это таблица, хранящаяся в памяти процесса, где для каждой записи можно определить время жизни.
У нашей таблицы следующие характеристики:
type ip
: запросы сохраняются в таблице по IP-адресу в качестве ключа. Таким образом, запросы с одного и того же IP-адреса будут ссылаться на одну и ту же запись. По сути, это означает, что мы отслеживаем IP-адреса и связанные с ними данные.size 100K
: таблица содержит максимум 100 тыс. записей.expire 30m
: срок хранения записей составляет 30 минут бездействия.store gpc0,http_req_rate(10s)
: с записями хранится счетчикgpc0
и количество запросов IP-адреса за последние 10 секунд. С помощьюgpc0
мы будем отслеживать, сколько раз IP-адрес замечен в злоупотреблениях. По сути, положительное значение счетчика означает, что IP-адрес уже помечен как подозрительный. Назовем этот счетчикabuse indicator
.
В целом, таблица Abuse
позволяет отслеживать, помечен ли IP-адрес как подозрительный, а также текущую частоту запросов с этого адреса. Поэтому у нас появляется история записей, а также информация в реальном времени.
Теперь перейдем в раздел frontend proxy
и посмотрим, что там нового.
Функции и правила ACL
frontend proxy
bind *:80
# ACL function declarations
acl is_abuse src_http_req_rate(Abuse) ge 10
acl inc_abuse_cnt src_inc_gpc0(Abuse) gt 0
acl abuse_cnt src_get_gpc0(Abuse) gt 0
# Rules
tcp-request connection track-sc0 src table Abuse
tcp-request connection reject if abuse_cnt
http-request deny if abuse_cnt
http-request deny if is_abuse inc_abuse_cnt
use_backend api
Список управления доступом ACL (Access Control List) — это объявления функций, которые вызываются только при соответствии установленному правилу.
Давайте подробно рассмотрим все три записи ACL. Имейте в виду, что все они явно ссылаются на таблицу Abuse
, которая в качестве ключа использует IP-адреса, поэтому каждая функция применяется к IP-адресу запроса:
acl is_abuse src_http_req_rate(Abuse) ge 10
: функцияis_abuse
возвращаетTrue
, если текущая частота поступления запросов больше или равна десяти.acl inc_abuse_cnt src_inc_gpc0(Abuse) gt 0
: функцияinc_abuse_cnt
возвращаетTrue
, если инкрементируемое значениеgpc0
больше нуля. Поскольку начальное значениеgpc0
равно нулю, эта функция всегда возвращаетTrue
. Другими словами, она увеличивает значениеabuse indicator
, по сути, сигнализируя о злоупотреблениях с этого IP-адреса.acl abuse_cnt src_get_gpc0(Abuse) gt 0
: функцияabuse_cnt
возвращаетTrue
, если значениеgpc0
больше нуля. Другими словами, он говорит, был ли этот IP-адрес уже замечен в злоупотреблениях.
Как упоминалось ранее, ACL — это просто декларации, то есть объявления функций. Они не применяются к входящим запросам, пока не срабатывает какое-то правило.
Имеет смысл взглянуть на правила, определенные в том же разделе frontend
. Правила поочередно применяются к каждому входящему запросу — и запускают функции из списка ACL, которые мы только что определили.
Давайте посмотрим, что делает каждое правило:
tcp-request connection track-sc0 src table Abuse
: добавляет запрос в таблицуAbuse
. Поскольку в таблице ключом является IP-адрес, это правило пополняет список IP-адресов.tcp-request connection reject if abuse_cnt
: отклоняет новые TCP-соединения, если IP-адрес уже замечен в злоупотреблениях, то есть помечен как abuse. По сути, запрещает новые TCP-соединения с такими IP-адресами.http-request deny if abuse_cnt
: запрещает доступ, если IP-адрес уже замечен в злоупотреблениях. Это относится к уже установленным соединениям с IP-адресами, которые только что помечены как abuse.http-request deny if is_abuse inc_abuse_cnt
: запрещает доступ, еслиis_abuse
иinc_abuse_cnt
оба возвращаютTrue
. Другими словами, будет отказ в доступе, если с этого IP-адреса в настоящее время запросы поступают с высокой частотой, а затем этот IP-адрес вносится в черный список.
По существу, мы вводим два типа проверок: в реальном времени и по черному списку из истории запросов. Второе правило отвергает все новые TCP-соединения, если IP-адрес был замечен в злоупотреблениях. Третье правило запрещает обслуживание HTTP-запросов для IP-адреса из черного списка, независимо от текущей частоты запросов с этого адреса. Четвертое правило гарантирует, что HTTP-запросы с IP-адреса будут отклонены в тот самый момент, как только преодолен порог частоты запросов. Таким образом, второе правило в основном работает на новых TCP-соединениях, третье и четвертое — на уже установленных соединениях, причем первое — это историческая проверка, а второе — проверка в реальном времени.
Попробуем фильтр в деле!
Теперь можем снова собрать и запустить наши контейнеры.
$ sudo docker-compose down
$ sudo docker-compose build
$ sudo docker-compose up
Балансировщик нагрузки должен запуститься перед двумя серверами бэкенда.
Давайте направим наш браузер на http://localhost/
. Если быстро обновить страничку с десяток раз, мы превысим порог в десять запросов за десятисекундный интервал — и наши запросы будут отклонены. Если мы продолжим обновлять страницу, новые запросы будут отклоняться сразу — еще до того, как установлено TCP-соединение.
Вопросы
Почему лимит составляет десять запросов на десять секунд?
Таблица Abuse
определяет http_req_rate(10s)
, то есть частота запросов измеряется в окне в десять секунд. Функция is_abuse
из ACL возвращает True
при частоте запросов ≥10 в течение указанного интервала. Таким образом, злоупотреблением считается частота запросов в десять и более запросов за десять секунд.
В этой статье для примера мы решили установить низкий лимит, чтобы проще было проверить работоспособность ограничителя.
В чем разница между правилами http-request и tcp-request connection?
Из документации:
http-request: оператор http-request определяет набор правил, которые применяются на сетевом уровне 7 [по модели OSI]
Из документации:
tcp-request connection: выполнение действия над входящим соединением в зависимости от условия на сетевом уровне 4
Зачем отбрасывать HTTP-запросы, если мы в любом случае полностью отбрасываем TCP-запросы?
Представьте, что HTTP-запросы на сервер отправляют несколько TCP-соединений с одного IP-адреса. Частота HTTP-запросов быстро превысит пороговые значения. Именно тогда вступает в действие четвертое правило, которое отбрасывает запросы и вносит IP-адрес в черный список.
Теперь вполне возможно, что HTTP-соединения с того же IP-адреса остаются открытыми (см. постоянное HTTP-соединение), а частота HTTP-запросов упала ниже порогового значения. Третье правило гарантирует продолжение блокировки HTTP-запросов, поскольку abuse indicator
срабатывает на этот IP.
А теперь предположим, что через несколько минут тот же IP пытается установить TCP-соединения. Они отбрасываются немедленно, так как действует второе правило: оно видит помеченный IP-адрес — и сразу отбрасывает соединения.
Вывод
Поначалу может быть сложно разобраться с ограничением скорости обработки запросов с помощью HAProxy. Чтобы все сделать правильно, требуется довольно «низкоуровневое»
Что еще почитать:
- Как реализуется отказоустойчивая архитектура в платформе Mail.ru Cloud Solutions.
- Лучшие 10 хитростей и советов по Kubernetes.
- Наш канал в Телеграм о цифровой трансформации.
Автор: Андрей Пшеничнов