Почему сама блокировка становится нагрузкой
«Мы блокируем атаку» — недостаточная фраза. Вопрос другой: на каком уровне выполняется блокировка и во что она вам обходится?
«Мы блокируем атаку» — недостаточная фраза. Вопрос другой: на каком уровне выполняется блокировка и во что она вам обходится?
Запрос, заблокированный на уровне приложения, проходит весь путь: принимается TCP-соединение, выполняется рукопожатие TLS, читаются заголовки HTTP, срабатывает правило и пишется ответ 403.
То есть на каждый запрос, который вы видите как «заблокирован», сервер потратил соединение, шифрование и ответ. Для одного запроса это ничтожно. Для ста тысяч в секунду это и есть причина падения.
Самая дорогая часть — TLS. При рукопожатии с ключом RSA-2048 операция подписи на сервере в разы дороже, чем работа клиента: эта асимметрия не злой умысел, а природа протокола. ECDSA P-256 заметно дешевле, но всё равно на порядки дороже простого приёма пакета.
Атакующий это знает. Поэтому часть самых эффективных L7-атак вообще не отправляет запрос: соединение открывается, TLS начинается и на этом останавливается. С вашей стороны это даже не «заблокированный запрос» — в статистику он не попадает, а память и файловый дескриптор занимает.
Во время атаки на панели видно «заблокировано запросов: 2,4 млн», и кажется, что защита работает. А сайт при этом не открывается.
Причина простая: считается статистика блокировок, а каждая блокировка означает выполненную работу. Чем больше число, тем больше сервер поработал. Рост графика — это рост расхода, а не защиты.
Правильная панель показывает на одном графике две линии: заблокированные запросы и загрузку процессора в тот же момент. Полезна и третья — число открытых соединений, потому что во многих случаях первым заканчивается не процессор, а именно оно.
Экзотических инструментов для этого не нужно. `ss -s` показывает открытые сокеты одной строкой, `netstat -s` даёт счётчики повторных передач и отброшенных пакетов. Если во время атаки растут `TCPBacklogDrop` или `ListenOverflows`, вы уже упёрлись в очередь, и правило блокировки на это никак не влияет.
После того как источник определён, решение не должно подниматься наверх. Пакет отбрасывается на самой нижней ступени сетевого стека — до приложения он не доходит, TLS не открывается, ответ не пишется.
На практике уровней несколько. Самый нижний — сама сетевая карта или XDP: пакет отбрасывается на уровне драйвера, без создания сокета и без `sk_buff`. Выше — nftables или iptables, то есть уже после `conntrack` и маршрутизации. Самый верхний — правило внутри приложения.
Разница не теоретическая. Каждая ступень требует выделения дополнительных структур на пакет, блокировок и переключений контекста; для пакета, отброшенного в XDP, ничего из этого не выполняется. На практике это означает на порядок больше пакетов на том же железе.
Второй выигрыш незаметен, но важнее: отброшенный пакет не занимает места в таблице соединений. Когда переполняется `nf_conntrack`, новые соединения начинают отклоняться, и это касается всего сервера — и атакуемого сайта, и соседнего. Падение чаще всего начинается именно отсюда.
Ядро видит пакет, а не сессию. Оно не сделает вывод «этот пользователь восемнадцать раз добавил один и тот же товар в корзину» — для такого вывода нужен контекст, а контекст живёт наверху.
Программа XDP вообще работает в жёстко ограниченной среде: верификатор eBPF не разрешает циклы, динамическую память и долгое исполнение. Это не недостаток, а условие — именно благодаря этим ограничениям она успевает работать на скорости пакета.
Поэтому правильная архитектура двухуровневая. Решение принимается наверху, в контексте поведения; исполнение выполняется внизу, там, где дёшево. Обнаружение должно быть умным, отсечение — грубым и быстрым.
Связывает их список: логика наверху определяет источник и передаёт его вниз со сроком. Технически это обычно eBPF map или `ipset`, потому что запись туда не требует перезагрузки набора правил. Срок обязателен — вечный чёрный список через год включает и настоящих пользователей.
Срок блокировки источника задаёт две разные ошибки. Слишком короткий — атакующий возвращается снова и снова, а логика обнаружения работает непрерывно. Слишком длинный — тысяча пользователей за NAT мобильного оператора часами не может зайти.
Практическое решение — растущий срок: короткий при первом срабатывании, длиннее при повторе. Случайно попавший пользователь выходит быстро, а возвращающийся источник отодвигается всё дальше.
В IPv6 правило другое. Одному хосту обычно выделяется /64, то есть блокировка одного адреса не даёт ничего — атакующий переходит на соседний. Поэтому в IPv6 блок применяется по /64, а иногда и по /48, и это решение должно быть записано отдельно.
И каждая запись должна хранить причину: какое правило, в какое время, по какому признаку. Список без причин через месяц превращается в чёрный ящик, к которому никто не решается притронуться: на вопрос «почему этот IP здесь?» ответа нет, а удалять страшно.
Объёмная атака забивает канал, и поглощается она на уровне оператора связи. Речь здесь не о процессоре, а о ширине канала — такую атаку невозможно остановить рядом с сервером: канал уже забит, а ваше правило стоит на другом его конце.
Классические примеры — усиление через DNS или NTP и SYN-флуд. Ответ на них тоже другой: центр очистки на аплинке, увод трафика по BGP или `SYN cookies` (в Linux `net.ipv4.tcp_syncookies`), позволяющие отвечать без хранения состояния при переполнении очереди.
Атака на уровне приложения выглядит иначе: объём трафика небольшой, запросы корректные, TLS настоящий, User-Agent убедительный. Но каждый из них просит дорогую страницу — поиск, отчёт, каталог с фильтрами. Здесь падает не канал, а база данных, и сетевой график при этом выглядит совершенно спокойным.
Одной меры на оба случая не бывает. Фраза «у нас есть защита от DDoS» бессмысленна, пока не сказано, о каком из двух идёт речь, — а во многих предложениях подразумевается только первый.
Блокировать каждый подозрительный запрос необязательно. Иногда дешевле замедлить его или поставить проверку, требующую вычислений: настоящий браузер этого не заметит, а скрипт, шлющий тысячи запросов, потеряет скорость.
Это особенно полезно там, где уверенности нет. Если ошиблась блокировка, пользователь теряется целиком; если ошиблось замедление, он ждёт несколько сотен миллисекунд. Цена ошибки падает — а значит, порог можно поставить строже.
На уровне HTTP это делается через 429 и заголовок `Retry-After` — механизм стандартный, и хорошо написанные клиенты ему подчиняются. Плохо написанные не подчиняются, и именно эта разница становится полезным сигналом: клиент, игнорирующий `Retry-After`, не браузер.
Важное условие: замедление тоже должно выполняться там, где дёшево. Держать соединение открытым и тянуть с ответом — значит занимать собственные дескрипторы. Ожидание должно быть без состояния: ответить сразу и отклонить следующий запрос дешевле, чем удерживать открытое соединение.
Спросите у поставщика одно: меняется ли загрузка процессора сервера для заблокированного источника? Если вместе с числом «заблокированных запросов» растёт и процессор — блокировка выполняется на уровне приложения.
Второй вопрос: выполняется ли рукопожатие TLS для запроса с заблокированного адреса? Это легко проверить — запустите `openssl s_client -connect host:443` с заблокированного адреса. Если сертификат вернулся, самую дорогую часть вы всё равно оплачиваете, а блокировка лишь меняет текст ответа.
Третий вопрос про срок: сколько живёт запись в чёрном списке и что его продлевает? Если ответа нет, значит список чистят вручную — то есть не чистят.
Четвёртый раскрывает больше всех: где считаются отброшенные пакеты во время нагрузочного теста? Если поставщик вообще не может показать это число, значит ниже прикладного уровня отбрасывания не происходит.
Покажите домен — дадим первичный анализ вашего текущего внешнего состояния. На этом этапе ничего не меняется.

