feat!: universal redesign — drop Minecraft stack, single-crate architecture

- remove Java plugins (velocity/paper), dashboard, all MC-specific code
  (handshake, death_code, varint, hostname-HMAC); available in history pre-v0.2
- merge crates/* into one package with src/bin/{rampart,rampart-manager,rampart-cli}
- ProtocolHandler trait + registry (no implementations yet), universal PoW kept
- XDP: universal L3/L4 filter (xdp/core/) + pluggable hook API (xdp/hooks/),
  fix IPv6 saddr bug; clang build verified
- docs: bilingual knowledge base (docs/kb/: attacks x4, defense-levels,
  practice x3), rewrite README/architecture for universal concept
- TODO.md v4.0: <=300-line module limit, competitor benchmark section (ref/)
- deploy/CI/docs cleanup: no MC references, new binary names

cargo build/clippy(-D warnings)/test green (55 tests)
This commit is contained in:
loki5512344 2026-08-24 01:50:22 +02:00
parent 0b53ed720b
commit 15f474486a
Signed by: boba
GPG key ID: 253067914055423B
179 changed files with 5044 additions and 11519 deletions

44
docs/kb/README.md Normal file
View file

@ -0,0 +1,44 @@
# Rampart Knowledge Base
> Теоретическая база и практика защиты от DDoS, на которой построена платформа.
> Статьи двуязычные: в каждой есть разделы `## English` и `## Русский`.
## English
### Attacks — how they work
- [SYN Flood](./attacks/syn-flood.md) — the classic TCP half-open exhaustion attack; backlog mechanics, why one packet costs the server memory.
- [HTTP Flood](./attacks/http-flood.md) — application-layer floods with syntactically perfect requests; why L3/L4 defense is powerless.
- [Slowloris & Slow Attacks](./attacks/slowloris.md) — exhausting connection slots with connections that never finish; no bandwidth needed.
- [UDP Amplification](./attacks/udp-amplification.md) — reflection and amplification factors; DNS/NTP/Memcached abuse.
### Defense fundamentals
- [Defense Levels](./defense-levels.md) — where to filter a packet: kernel (XDP) vs userspace trade-offs, full packet path through the Linux stack, and why Rampart uses hybrid kernel fast-path + userspace smart-path.
### Practice
- [Kernel Tuning](./practice/kernel-tuning.md) — sysctl parameters that matter under flood (`tcp_max_syn_backlog`, somaxconn, backlog queues), ready-to-adapt config.
- [NIC Tuning](./practice/nic-tuning.md) — ring buffers, IRQ affinity, offloads, RPS/XPS for high pps workloads.
- [Stress Testing](./practice/stress-testing.md) — methodology and tooling for load/attack simulation against your own infra.
---
## Русский
### Атаки — как они работают
- [SYN Flood](./attacks/syn-flood.md) — классическая атака на полуоткрытые соединения; механика backlog'а, почему один пакет стоит серверу памяти.
- [HTTP Flood](./attacks/http-flood.md) — L7-флуд синтаксически корректными запросами; почему защита уровня L3/L4 бессильна.
- [Slowloris и медленные атаки](./attacks/slowloris.md) — исчерпание слотов соединений соединениями, которые никогда не завершаются; полоса не нужна.
- [UDP-амплификация](./attacks/udp-amplification.md) — рефлексия и коэффициенты усиления; злоупотребление DNS/NTP/Memcached.
### Основы защиты
- [Уровни фильтрации](./defense-levels.md) — где дропать пакет: компромиссы ядра (XDP) и userspace, полный путь пакета через сетевой стек Linux и почему Rampart использует гибрид kernel fast-path + userspace smart-path.
### Практика
- [Тюнинг ядра](./practice/kernel-tuning.md) — значимые под флудом параметры sysctl (`tcp_max_syn_backlog`, somaxconn, очереди), готовый конфиг для адаптации.
- [Тюнинг NIC](./practice/nic-tuning.md) — ring buffers, привязка прерываний, offload'ы, RPS/XPS для высоких pps.
- [Стресс-тестирование](./practice/stress-testing.md) — методика и инструменты нагрузочной/атакующей симуляции на своей инфраструктуре.

View file

@ -0,0 +1,233 @@
# HTTP Flood: Application-Layer Attack That Looks Like Traffic
> Knowledge Base · Rampart attack fundamentals · Related: [defense-levels.md](../defense-levels.md), [slowloris.md](./slowloris.md), PoW details in [anti-bot research](../../research/anti-bot.md)
## English
## 1. What an L7 flood is
An HTTP flood sends requests that are **syntactically perfect** — valid TCP, valid TLS, valid HTTP — at a rate or cost profile designed to exhaust application resources. There is nothing anomalous about any individual request; the anomaly is statistical (volume, distribution, cost) rather than protocol-level.
Common shapes:
- **GET flood on expensive endpoints** — hammering pages that trigger database joins, search, report generation, or uncached rendering. 100 req/s against `/search?q=...` hurts more than 10,000 req/s against a static asset.
- **POST flood** — form submissions, API writes: each request costs the backend not just CPU but writes, locks, and downstream calls.
- **Cache-busting** — appending a unique query string to every request (`/?t=<random>`, `?utm_<random>=1`) so every response misses the cache and hits origin. The same nominal traffic suddenly multiplies its backend load several-fold.
- **Legitimate-looking traffic** — full TLS with proper SNI, plausible User-Agent headers (or real headless browsers), cookies honored, human-like pacing. Distributed across residential proxies, it defeats naive "is this a datacenter IP" checks.
## 2. Why L3/L4 defense is powerless here
Everything below the application sees a stream of perfectly normal conversations:
```mermaid
flowchart TD
R[Requests arrive] --> X{XDP / kernel filters}
X -- "TCP handshake valid<br/>no flag anomalies<br/>rate may be under per-IP limits" --> P[Connections accepted]
P --> H{L7 engine:<br/>parse HTTP}
H -- "requests are VALID.<br/>The attack lives in semantics:<br/>which endpoint, what cost,<br/>what pattern" --> Q{Decision needs app context}
subgraph useless ["What L3/L4 can see"]
B1[src IP]
B2[packet rate]
B3[TCP flags]
end
subgraph needed ["What the decision actually requires"]
N1[endpoint cost model]
N2[per-session behavior]
N3[cross-request patterns]
N4[reputation history]
end
style Q fill:#f5e6c8
```
Concretely:
- Dropping by rate alone punishes legitimate bursts (a page load fires dozens of requests).
- The attacker's packets pass every sanity check because they *are* sane.
- Per-IP limiting helps only until the botnet spreads across thousands of residential-proxy IPs.
The only layer that can tell `/` from `/expensive-report` and a browser session from a script is one that has parsed the HTTP exchange and holds session state. That is why an L7 flood is decided in userspace — the kernel already lost the information needed for the verdict.
## 3. Defense
### 3.1 Challenge-response gating
Before expensive logic runs, the edge issues a cheap challenge that must be solved before the real request is served. Legitimate browsers solve it transparently; scripts pay a cost. This converts "free requests" into "paid requests" without touching honest users.
### 3.2 SHA256 hashcash proof-of-work with dynamic difficulty
Rampart's core anti-flood mechanism (Layer 2 of the stack). Principle:
1. Edge generates a random per-request challenge token + timestamp + current difficulty `d`.
2. Client must find a nonce such that `SHA256(token || nonce)` begins with characters from an allowed set — on average requiring `16^d` hash attempts (hex output). Difficulty 4 ≈ tens of milliseconds on a phone; difficulty 12 ≈ seconds even on a fast CPU.
3. Client returns `(token, nonce)`; edge verifies with **one** hash (~microseconds) and checks the timestamp (≤30 s window) — challenges are single-use, so replay is impossible.
4. Difficulty is dynamic: driven by global connections-per-second and attack mode.
```mermaid
sequenceDiagram
participant C as Client
participant E as Edge (PoW gate)
C->>E: request arrives during elevated load
E-->>C: {challenge token, difficulty d, allowed hex, timestamp}
Note over C: browser loops nonce:<br/>SHA256(token+nonce) starts with d allowed chars<br/>cost: O(16^d) hashes on the CLIENT
C-->>E: solution {token, nonce}
Note over E: verify = ONE sha256 + timestamp check<br/>FILTERING POINT: asymmetry —<br/>client pays seconds, server pays microseconds
E->>E: verified → forward to app logic
rect rgb(230, 240, 255)
Note over C,E: Dynamic difficulty: CPS > 500 → d=12, >100 → 10,<br/>>50 → 8, calm → 4. Attack raises everyone's price;<br/>verified/reputation-trusted clients get lower d or skip.
end
```
The economics are the point: verification is ~10⁶ times cheaper than solving, so the edge can demand work from millions of suspects while spending almost nothing itself. GPU farms don't rescue the attacker much — SHA256 isn't memory-hard but the bottleneck becomes orchestration of millions of solutions, and raising `d` scales their cost exponentially while ours stays flat.
### 3.3 Behavioral analysis
Per-session statistics distinguish humans from scripted floods:
- request pacing and inter-arrival variance (scripts are too regular; instant responses <200 ms are bots),
- endpoint mix (humans browse; bots hammer one URL),
- header order/fingerprint consistency,
- navigation coherence (referers, resource loading order).
Deviations feed a risk score instead of hard-blocking immediately — reducing false positives on NAT users behind one IP.
### 3.4 Reputation scoring
Every source accumulates a score (−100..+100):
| Signal | Effect |
|---|---|
| Passed PoW/challenges before | positive |
| Sent malformed/death-code packets | strong negative, auto-ban |
| ASN category (residential / datacenter / mobile / Tor) | baseline weighting of all limits |
| Anomaly vs 168-hour hourly baseline profile | negative drift |
Reputation multiplies rate limits and sets PoW difficulty: trusted residential client at calm time → difficulty 4, no friction; fresh datacenter IP mid-attack → strictest limits plus difficulty 12. Verified clients (fingerprint cache in Redis, HMAC-SHA256 based, TTL 24 h) skip re-challenges entirely — returning users don't pay twice.
### Summary
| Mechanism | What it stops |
|---|---|
| Challenge-response gate | Free unauthenticated access to expensive logic |
| Hashcash PoW, dynamic difficulty | Mass request generation — makes it economically irrational |
| Behavioral analysis | Bots that solve PoW but act non-human |
| Reputation scoring | Distributed floods via rented residential IPs |
## Русский
## 1. Что такое L7-флад
HTTP-флад шлёт запросы, **синтаксически безупречные** — валидный TCP, валидный TLS, валидный HTTP — с такой интенсивностью и стоимостным профилем, которые истощают ресурсы приложения. В отдельном запросе нет ничего аномального; аномалия статистическая (объём, распределение, стоимость), а не протокольная.
Типичные формы:
- **GET-флад по дорогим эндпоинтам** — долбление страниц, триггерящих джойны в БД, поиск, генерацию отчётов или некэшированный рендеринг. 100 req/s против `/search?q=...` больнее, чем 10 000 req/s по статике.
- **POST-флад** — отправка форм, запись в API: каждый запрос стоит бэкенду не только CPU, но и записей, локов и внешних вызовов.
- **Cache-busting** — добавление уникального query-параметра к каждому запросу (`/?t=<random>`, `?utm_<random>=1`), чтобы каждый ответ был cache-miss и уходил в origin. Тот же номинальный трафик внезапно умножает нагрузку на бэкенд в разы.
- **Маскировка под легитимный трафик** — полный TLS с корректным SNI, правдоподобные User-Agent (или реальные headless-браузеры), обработка cookies, человекоподобный темп. Распределение через резиденциальные прокси ломает наивные проверки «датацентровый ли это IP».
## 2. Почему защита L3/L4 здесь бессильна
Все слои ниже приложения видят поток абсолютно нормальных разговоров:
```mermaid
flowchart TD
R[Запросы приходят] --> X{XDP / фильтры ядра}
X -- "TCP-хендшейк валиден<br/>аномалий флагов нет<br/>rate может быть ниже per-IP лимитов" --> P[Соединения приняты]
P --> H{L7-движок:<br/>парсинг HTTP}
H -- "запросы ВАЛИДНЫ.<br/>Атака живёт в семантике:<br/>какой эндпоинт, какая цена,<br/>какой паттерн" --> Q{Для решения нужен контекст приложения}
subgraph useless ["Что видит L3/L4"]
B1[src IP]
B2[packet rate]
B3[TCP flags]
end
subgraph needed ["Что реально нужно для решения"]
N1[модель стоимости эндпоинтов]
N2[поведение сессии]
N3[кросс-запросные паттерны]
N4[история репутации]
end
style Q fill:#f5e6c8
```
Конкретно:
- Дроп по одному лишь rate наказывает легитимные всплески (загрузка одной страницы порождает десятки запросов).
- Пакеты атакующего проходят все санити-чеки, потому что они и есть санитарные.
- Per-IP лимиты помогают ровно до тех пор, пока ботнет не размажется по тысячам резиденциальных прокси-IP.
Единственный слой, отличающий `/` от `/expensive-report`, а браузерную сессию от скрипта, — слой, распарсивший HTTP-обмен и держащий состояние сессии. Поэтому L7-флад решается в userspace — ядро уже потеряло информацию, нужную для вердикта.
## 3. Защита
### 3.1 Гейтинг challenge-response
До запуска дорогой логики edge выдаёт дешёвый challenge, который надо решить, прежде чем реальный запрос будет обслужен. Легитимные браузеры решают его прозрачно; скрипты платят цену. «Бесплатные запросы» превращаются в «платные», не трогая честных пользователей.
### 3.2 SHA256 hashcash proof-of-work с динамической сложностью
Ключевой анти-флад механизм Rampart'а (слой 2 стека). Принцип:
1. Edge генерирует случайный per-request токен-challenge + timestamp + текущую сложность `d`.
2. Клиент должен найти nonce, при котором `SHA256(token || nonce)` начинается с символов из разрешённого набора — в среднем требуется `16^d` попыток хеширования (hex-вывод). Сложность 4 ≈ десятки миллисекунд даже на телефоне; сложность 12 ≈ секунды даже на быстром CPU.
3. Клиент возвращает `(token, nonce)`; edge проверяет **одним** хешем (~микросекунды) и проверяет timestamp (окно ≤30 с) — challenge одноразовый, replay невозможен.
4. Сложность динамическая: управляется глобальным CPS и режимом атаки.
```mermaid
sequenceDiagram
participant C as Клиент
participant E as Edge (PoW-гейт)
C->>E: запрос пришёл при повышенной нагрузке
E-->>C: {challenge token, difficulty d, allowed hex, timestamp}
Note over C: браузер крутит цикл nonce:<br/>SHA256(token+nonce) начинается с d разрешённых символов<br/>цена: O(16^d) хешей у КЛИЕНТА
C-->>E: решение {token, nonce}
Note over E: верификация = ОДИН sha256 + проверка timestamp<br/>ТОЧКА ФИЛЬТРАЦИИ: асимметрия —<br/>клиент платит секундами, сервер микросекундами
E->>E: верифицирован → вперёд к логике приложения
rect rgb(230, 240, 255)
Note over C,E: Динамическая сложность: CPS > 500 → d=12, >100 → 10,<br/>>50 → 8, спокойствие → 4. Атака повышает цену всем;<br/>верифицированные/доверенные клиенты получают низкую d или скип.
end
```
Экономика — сама суть механизма: верификация в ~10⁶ раз дешевле решения, поэтому edge может требовать работу от миллионов подозреваемых, почти ничего не тратя. GPU-фермы мало помогают атакующему — SHA256 не memory-hard, но узким местом становится оркестрация миллионов решений, а рост `d` масштабирует их цену экспоненциально, тогда как наша остаётся плоской.
### 3.3 Поведенческий анализ
Посессионная статистика отличает людей от скриптовых флудов:
- темп запросов и дисперсия интервалов (скрипты слишком ровные; мгновенные ответы <200 мс — боты),
- микс эндпоинтов (люди гуляют по сайту; боты долбят один URL),
- согласованность порядка заголовков/фингерпринта,
- связность навигации (referer'ы, порядок подгрузки ресурсов).
Отклонения капают в risk-score вместо немедленного жёсткого блока — снижая ложные срабатывания на NAT-пользователей за одним IP.
### 3.4 Reputation scoring
Каждый источник накапливает score (−100..+100):
| Сигнал | Эффект |
|---|---|
| Ранее проходил PoW/challenge | позитив |
| Слал мусорные/death-code пакеты | сильный негатив, авто-бан |
| Категория ASN (residential / datacenter / mobile / Tor) | базовое взвешивание всех лимитов |
| Аномалия против 168-часового почасового baseline | негативный дрейф |
Репутация умножает rate limit'ы и задаёт сложность PoW: доверенный residential-клиент в спокойное время → сложность 4, ноль трения; свежий датацентровый IP посреди атаки → самые строгие лимиты плюс сложность 12. Верифицированные клиенты (кэш fingerprint'ов в Redis на базе HMAC-SHA256, TTL 24 ч) вообще пропускают повторные challenge — вернувшиеся пользователи не платят дважды.
### Итог
| Механизм | Что останавливает |
|---|---|
| Гейт challenge-response | Бесплатный анонимный доступ к дорогой логике |
| Hashcash PoW, динамическая сложность | Массовую генерацию запросов — делает её экономически бессмысленной |
| Поведенческий анализ | Ботов, решивших PoW, но ведущих себя не по-человечески |
| Reputation scoring | Распределённые флуды через арендованные резиденциальные IP |

View file

@ -0,0 +1,197 @@
# Slowloris and Slow Attacks: Exhaustion Without Bandwidth
> Knowledge Base · Rampart attack fundamentals · Related: [defense-levels.md](../defense-levels.md), [http-flood.md](./http-flood.md)
## English
## 1. The opposite of a flood
A volumetric attack shouts; a slow attack whispers. Slowloris (named after the original 2009 tool) does not try to overwhelm bandwidth or packet rate — it tries to **occupy every available connection slot with connections that are technically alive but never finish**.
The mechanism exploits how HTTP servers parse requests:
1. Attacker opens a TCP connection and sends `GET / HTTP/1.1`.
2. Then it sends headers one at a time, dribbling out partial headers like `X-a: b\r\n` every few seconds.
3. The server cannot reject the request yet: the header block is incomplete, and by protocol it must keep waiting for `\r\n\r\n` that terminates the header section.
4. Just before the server's read timeout fires, the attacker sends another partial header — resetting the timer.
5. Repeat forever.
Each connection consumes a worker/thread/event-loop slot and a socket buffer while transferring essentially zero bytes. When all workers are busy waiting for "requests in progress", legitimate clients queue behind them and time out.
```mermaid
sequenceDiagram
participant A as Attacker
participant W as Server worker pool
participant L as Legitimate client
A->>W: TCP connect + "GET / HTTP/1.1"
loop every few seconds, forever
A->>W: "X-a: b" (partial header)
Note over W: read timer resets,<br/>worker stays occupied<br/>FILTERING POINT: timeout must be<br/>shorter than the dribble interval
end
Note over W: pool exhausted:<br/>all workers wait on dead requests
L->>W: (legitimate request)
W--)L: queued → connection timeout
```
Hundreds of such connections from a single IP can take down a default-configured web server. The attack traffic is measured in **bytes per minute** — it slips under any volumetric threshold.
## 2. How slow attacks differ from volumetric
| Property | Volumetric (SYN/UDP flood) | Slow (Slowloris family) |
|---|---|---|
| Resource attacked | Bandwidth, backlog, pps budget | Worker pool, sockets, memory per connection |
| Traffic volume | Huge | Tiny |
| Visible at L3/L4? | Yes — flag anomalies, rate spikes | No — packets look perfectly normal TCP |
| Source count needed | Often many IPs | Often one IP is enough |
| Right filter layer | Kernel (XDP) | Userspace, after parsing begins |
This last row matters most: no amount of kernel-level filtering can distinguish a Slowloris connection from a slow human client, because at the byte level they look identical. The decision requires understanding *application state* ("this request has been incomplete for 10 seconds"), which only exists above the kernel.
Variants of the same idea:
- **Slow Read** — complete request, but read the response 1 byte at a time, keeping the socket open.
- **Slow POST / R-U-Dead-Yet** — declare `Content-Length: 1000000`, then send body bytes at a trickle.
- **Slow connection holding** — open and just never send anything (idle hold).
## 3. Defense
### 3.1 Header/body read timeouts — the primary weapon
The server must enforce absolute deadlines independent of incoming bytes:
- **Header deadline**: total time to receive the full header block (e.g. 10 s). Not an idle timeout that the dribble resets — a hard cap from connection start to end-of-headers.
- **Body deadline**: total time to receive declared `Content-Length` bytes, and a sanity cap on the length itself.
- **Idle eviction**: a connection with no bytes for N seconds is closed regardless of state.
In Rampart's userspace engine this is exactly `tokio::time::timeout` around the handshake/read phase — if a full request isn't parsed within the window, the connection is dropped and the source gets a reputation penalty.
### 3.2 Per-IP connection limits
A single IP rarely needs more than a handful of concurrent connections. Enforce:
- max concurrent connections per IP,
- max new connections per second per IP (token bucket),
- global cap on half-parsed requests as a fraction of the pool.
With per-IP caps, one machine running Slowloris can occupy its own quota and nothing else; a distributed variant is then throttled by rate limits plus ASN reputation weighting (datacenter sources get smaller quotas than residential).
### 3.3 Reverse-proxy buffering — shrink the blast radius
Putting a buffering reverse proxy (or Rampart edge itself) in front of the application changes the game:
- The proxy reads the **complete** request before forwarding anything upstream.
- The application's workers only ever see fully-formed requests; slow clients stall proxy buffers, not app threads.
- Proxy buffers are cheap, sized for thousands of stalled connections, and paired with aggressive timeouts from §3.1.
### Why the userspace filter is the main defense here
```mermaid
flowchart TD
C[Connection established] --> X{XDP layer}
X -- "sees only valid TCP.<br/>Cannot tell slow attack<br/>from slow human" --> P[Bytes reach userspace]
P --> T{Userspace engine:<br/>header read timeout,<br/>conn-per-IP limit,<br/>request state tracking}
T -- "incomplete past deadline" --> D[Drop + reputation penalty]
T -- complete in time --> APP[Application logic]
style D fill:#f5d0d0
```
Kernel layers (XDP, nftables) operate on packets and flags; a slow attack produces perfectly valid packets. The discriminating information — "how long has this request been incomplete, how many such requests does this IP hold" — lives in application-level session state. Hence: cheap kernel layers still handle the volumetric shield, but **the userspace engine is where slow attacks actually get decided**, which is why Rampart places its timeout/reputation logic there rather than trying to push everything into eBPF.
## Русский
## 1. Полная противоположность фладу
Объёмная атака кричит; медленная атака шепчет. Slowloris (назван по одноимённому инструменту 2009 года) не пытается задавить полосу или pps — он пытается **занять все слоты соединений соединениями, которые формально живы, но никогда не завершаются**.
Механизм эксплуатирует то, как HTTP-серверы парсят запросы:
1. Атакующий открывает TCP-соединение и шлёт `GET / HTTP/1.1`.
2. Затем отправляет заголовки по одному, выпуская частичные заголовки вроде `X-a: b\r\n` раз в несколько секунд.
3. Сервер пока не может отклонить запрос: блок заголовков неполон, и по протоколу он обязан ждать завершающие `\r\n\r\n`.
4. За мгновение до срабатывания read-таймаута атакующий шлёт ещё один частичный заголовок — таймер сбрасывается.
5. И так бесконечно.
Каждое соединение занимает слот воркера/потока/события цикла и сокетный буфер, передавая фактически ноль байт. Когда все воркеры ждут «запросы в процессе», легитимные клиенты встают в очередь и отваливаются по таймауту.
```mermaid
sequenceDiagram
participant A as Атакующий
participant W as Пул воркеров сервера
participant L as Легитимный клиент
A->>W: TCP connect + "GET / HTTP/1.1"
loop каждые несколько секунд, вечно
A->>W: "X-a: b" (частичный заголовок)
Note over W: read-таймер сброшен,<br/>воркер занят<br/>ТОЧКА ФИЛЬТРАЦИИ: таймаут должен быть короче<br/>интервала «капельницы»
end
Note over W: пул исчерпан:<br/>все воркеры ждут мёртвые запросы
L->>W: (легитимный запрос)
W--)L: в очереди → connection timeout
```
Несколько сотен таких соединений с одного IP кладут веб-сервер с дефолтной конфигурацией. Трафик атаки измеряется в **байтах в минуту** — под любой объёмный порог не попадает.
## 2. Чем медленные атаки отличаются от объёмных
| Свойство | Объёмные (SYN/UDP flood) | Медленные (семейство Slowloris) |
|---|---|---|
| Атакуемый ресурс | Полоса, backlog, бюджет pps | Пул воркеров, сокеты, память на соединение |
| Объём трафика | Огромный | Крошечный |
| Видно на L3/L4? | Да — аномалии флагов, всплески rate | Нет — пакеты выглядят абсолютно нормальным TCP |
| Сколько нужно источников | Часто много IP | Часто хватает одного |
| Правильный слой фильтрации | Ядро (XDP) | Userspace, после начала парсинга |
Последняя строка важнее всего: никакая фильтрация уровня ядра не отличит Slowloris-соединение от медленного человеческого клиента — на уровне байтов они идентичны. Решение требует понимания *прикладного состояния* («этот запрос неполон уже 10 секунд»), которого у ядра просто нет.
Варианты той же идеи:
- **Slow Read** — запрос полный, но ответ читается по 1 байту, сокет держится открытым.
- **Slow POST / R-U-Dead-Yet** — объявляется `Content-Length: 1000000`, затем тело капает по байтам.
- **Удержание соединения** — открыть и вообще ничего не слать (idle-hold).
## 3. Защита
### 3.1 Таймауты на чтение заголовков/тела — главное оружие
Сервер обязан держать абсолютные дедлайны независимо от входящих байтов:
- **Дедлайн заголовков**: общее время на получение полного блока заголовков (например, 10 с). Не idle-таймаут, который «капельница» сбрасывает, а жёсткий лимит от начала соединения до конца заголовков.
- **Дедлайн тела**: общее время на получение заявленных `Content-Length` байт плюс санитарный потолок на саму длину.
- **Evict по простою**: соединение без единого байта N секунд закрывается независимо от состояния.
В userspace-движке Rampart это ровно `tokio::time::timeout` вокруг фазы хендшейка/чтения — если полный запрос не распарсен в окно, соединение дропается, источник получает штраф репутации.
### 3.2 Лимиты соединений per IP
Одному IP редко нужно больше горстки одновременных соединений. Вводим:
- максимум одновременных соединений на IP,
- максимум новых соединений в секунду на IP (token bucket),
- глобальный потолок на долю недопарсенных запросов в пуле.
При per-IP квотах одна машина со Slowloris занимает только свою квоту и больше ничего; распределённый вариант затем душится rate limit'ами и весами ASN-репутации (датацентровые источники получают меньшие квоты, чем residential).
### 3.3 Буферизация reverse-proxy — сузить радиус поражения
Буферизирующий reverse-proxy (или сам edge Rampart'а) перед приложением меняет правила игры:
- Прокси читает **полный** запрос до пересылки чего-либо наверх.
- Воркеры приложения видят только полностью сформированные запросы; медленные клиенты упираются в буферы прокси, а не в потоки приложения.
- Буферы прокси дёшевы, рассчитаны на тысячи зависших соединений и работают в паре с агрессивными таймаутами из §3.1.
### Почему userspace-фильтр здесь главный
```mermaid
flowchart TD
C[Соединение установлено] --> X{Слой XDP}
X -- "видит только валидный TCP.<br/>Не отличит медленную атаку<br/>от медленного человека" --> P[Байты доходят до userspace]
P --> T{Userspace-движок:<br/>header read timeout,<br/>лимит conn-per-IP,<br/>отслеживание состояния запроса}
T -- "неполон за дедлайном" --> D[Дроп + штраф репутации]
T -- "успел целиком" --> APP[Прикладная логика]
style D fill:#f5d0d0
```
Ядерные слои (XDP, nftables) оперируют пакетами и флагами; медленная атака производит идеально валидные пакеты. Различающая информация — «как долго этот запрос остаётся незавершённым и сколько их у этого IP» — живёт в прикладном состоянии сессии. Поэтому: дешёвые ядерные слои продолжают держать объёмный щит, но **медленные атаки реально решаются в userspace-движке** — именно поэтому Rampart размещает там логику таймаутов и репутации, а не пытается затолкать всё в eBPF.

View file

@ -0,0 +1,237 @@
# SYN Flood: Anatomy of the Classic TCP Attack
> Knowledge Base · Rampart attack fundamentals · Related: [kernel-tuning.md](../practice/kernel-tuning.md), [defense-levels.md](../defense-levels.md)
## English
## 1. The TCP handshake, briefly
Every TCP connection starts with a three-way handshake:
1. **SYN** — the client sends a packet with the SYN flag and its initial sequence number (ISN).
2. **SYN-ACK** — the server replies with SYN+ACK and its own ISN.
3. **ACK** — the client confirms; the connection moves to `ESTABLISHED`.
Between steps 1 and 3, the connection is **half-open**: the server has already allocated memory for it in a special queue called the **SYN backlog** (`tcp_max_syn_backlog`), waiting for the final ACK. A half-open connection lives until `tcp_synack_retries` retransmissions are exhausted (default ~1 minute).
This asymmetry is the whole problem: **the attacker spends one packet per half-open connection, the server spends memory + a timer + a potential retransmission.**
## 2. How the attack works
The attacker floods the target with SYN packets and never sends the final ACK (or spoofs an unreachable source IP, so SYN-ACKs go nowhere). The backlog fills up with dead half-open connections. When it is full, legitimate SYNs are dropped — the service becomes unreachable.
```mermaid
sequenceDiagram
participant B as Attacker / botnet
participant K as Kernel (listen socket)
participant L as Legitimate client
Note over K: SYN backlog capacity = tcp_max_syn_backlog
B->>K: SYN #1 (never completes)
K-->>B: SYN-ACK (retransmits up to tcp_synack_retries)
B->>K: SYN #2..#N (no ACK ever)
K-->>B: SYN-ACK...
Note over K: backlog full of half-open entries<br/>retransmit timers burning CPU
L->>K: SYN (legitimate)
K--)L: dropped — backlog overflow<br/>FILTERING POINT: this is what we must prevent
```
Key point: the victim never sees an error — connections simply time out. From the user's perspective the port is "down" even though the machine may be nearly idle.
## 3. Attack variants
| Variant | Source addresses | Difficulty | Why it works |
|---|---|---|---|
| **Single-IP flood** | One real IP | Trivial | Only viable against hosts without per-source limiting; one IP can still open tens of thousands of half-open sockets |
| **Distributed (botnet)** | Many real IPs | Low | Each bot opens a few hundred half-open connections; per-IP limits are diluted across thousands of sources |
| **Spoofed source IPs** | Random / unreachable IPs | Medium | SYN-ACK goes to a third party; the attacker pays 1 packet per connection, the server pays memory + retransmissions; per-IP limiting is useless because each "source" appears once |
Spoofed floods are the most dangerous variant: rate-limiting by source IP cannot help when every packet has a fresh fake address.
## 4. SYN cookies: stateless handshake
SYN cookies move the connection state out of server memory and into the wire. Instead of allocating a backlog entry, the server encodes everything it needs to know into the sequence number of its own SYN-ACK:
```
SYN-ACK seq = MD5/IP-hash(secret, src_ip, src_port, dst_ip, dst_port) ← top 24 bits
+ timestamp mod 2^6 ← middle 5 bits (rotating)
+ MSS encoding ← low 3 bits
```
When the final ACK arrives, the server recomputes the expected value from the ACK number. If it matches, the connection was legitimately completed — *and only then* does the kernel allocate a socket. No backlog entry was ever consumed by the attacker.
```mermaid
sequenceDiagram
participant B as Attacker (no ACK)
participant K as Kernel with syncookies
participant L as Legitimate client
B->>K: SYN
K-->>B: SYN-ACK with encoded seq (state NOT stored)
Note over B: attacker ignores it — nothing happened on the server
L->>K: SYN
K-->>L: SYN-ACK with encoded seq (state NOT stored)
L->>K: ACK (seq+1 matches cookie)
Note over K: cookie verified → NOW allocate socket
K-->>L: ESTABLISHED
rect rgb(230, 240, 255)
Note over K: FILTERING POINT: allocation happens only after<br/>cryptographic proof of round-trip capability
end
```
### Trade-offs
SYN cookies are not free:
- **TCP options are lost** — window scale, SACK, timestamps cannot be negotiated because there are no bits left in the sequence number (the Linux workaround stores a small MSS code only). Legitimate clients get degraded connections while cookies are active.
- **No retransmission bookkeeping** — the kernel does not remember outstanding SYN-ACKs, so behavior under packet loss differs slightly from the normal path.
- **CPU cost** — a hash computation per SYN instead of a table insert; negligible normally, measurable at millions of pps.
- They activate only under backlog pressure (`net.ipv4.tcp_syncookies = 1`, value `2` = always on), so they are a **fallback**, not the first line.
## 5. Defense layering in Rampart
The correct order of defense is cheapest-first: kill the flood before the kernel ever touches its backlog.
```mermaid
flowchart TD
A[SYN packets arrive at NIC] --> B{XDP program:<br/>per-src-IP SYN throttle}
B -- "over N SYNs/sec from one IP<br/>→ temporary ban entry in eBPF map" --> X[XDP_DROP<br/>~50-100 ns/packet]
B --> C{Invalid flags?<br/>SYN+FIN, SYN+RST}
C -- yes --> X
C -- no --> D[Kernel stack]
D --> E{Backlog full?}
E -- "yes → SYN cookies kick in<br/>(stateless, no memory spent)" --> F[Cookie-verified connections proceed]
E -- no --> G[Normal handshake]
F --> H[Userspace engine:<br/>rate limit, PoW challenge]
G --> H
style X fill:#f5d0d0
style F fill:#d0e8d0
```
Layer responsibilities:
1. **XDP SYN throttle (first line)** — a token-bucket or fixed-window counter per source IP in an eBPF LRU map. More than N SYNs/sec from one address → drop in the driver, before `sk_buff` allocation. This handles single-IP floods entirely and blunts distributed ones.
2. **Kernel sysctls (second line)** — `tcp_syncookies=1`, enlarged `tcp_max_syn_backlog` and `somaxconn`, reduced `tcp_synack_retries`. Full list and rationale: [kernel-tuning.md](../practice/kernel-tuning.md).
3. **Userspace engine (third line)** — connections that complete the handshake face per-IP connection rate limiting and a proof-of-work challenge before any application logic runs.
## Русский
## 1. TCP-хендшейк вкратце
Каждое TCP-соединение начинается с трёхстороннего рукопожатия:
1. **SYN** — клиент шлёт пакет с флагом SYN и своим начальным номером последовательности (ISN).
2. **SYN-ACK** — сервер отвечает SYN+ACK со своим ISN.
3. **ACK** — клиент подтверждает; соединение переходит в `ESTABLISHED`.
Между шагами 1 и 3 соединение является **полуоткрытым (half-open)**: сервер уже выделил под него память в специальной очереди — **SYN backlog** (`tcp_max_syn_backlog`) — и ждёт финальный ACK. Полуоткрытое соединение живёт до исчерпания ретрансмиссий `tcp_synack_retries` (по умолчанию около минуты).
В этой асимметрии и вся проблема: **атакующий тратит один пакет на каждое полуоткрытое соединение, сервер — память + таймер + потенциальную ретрансмиccию.**
## 2. Как работает атака
Атакующий заваливает цель SYN-пакетами и никогда не отправляет финальный ACK (или подделывает недостижимый исходный IP — тогда SYN-ACK уходят в никуда). Backlog заполняется мёртвыми полуоткрытыми соединениями. Когда он переполнен, легитимные SYN дропаются — сервис становится недоступен.
```mermaid
sequenceDiagram
participant B as Атакующий / ботнет
participant K as Ядро (listening socket)
participant L as Легитимный клиент
Note over K: ёмкость backlog = tcp_max_syn_backlog
B->>K: SYN #1 (никогда не завершится)
K-->>B: SYN-ACK (ретранслирует до tcp_synack_retries раз)
B->>K: SYN #2..#N (ACK не будет никогда)
K-->>B: SYN-ACK...
Note over K: backlog забит полуоткрытыми записями,<br/>таймеры ретрансляций жгут CPU
L->>K: SYN (легитимный)
K--)L: дроп — переполнение backlog<br/>ТОЧКА ФИЛЬТРАЦИИ: именно этого надо не допустить
```
Важный момент: жертва не видит никакой ошибки — соединения просто отваливаются по таймауту. Со стороны пользователя порт «лежит», хотя машина может быть почти простаивает.
## 3. Варианты атаки
| Вариант | Адреса источника | Сложность | Почему работает |
|---|---|---|---|
| **Флад с одного IP** | Один реальный IP | Тривиально | Работает только против хостов без per-source лимитов; один IP всё равно открывает десятки тысяч полусокетов |
| **Распределённый (ботнет)** | Много реальных IP | Низко | Каждый бот держит несколько сотен полуоткрытых соединений; per-IP лимиты размываются по тысячам источников |
| **Поддельные source IP** | Случайные / недостижимые IP | Средне | SYN-ACK уходит третьей стороне; атакующий платит 1 пакет за соединение, сервер — памятью и ретрансляциями; per-IP лимитирование бесполезно, ведь каждый «источник» появляется один раз |
Спуфинг-вариант самый опасный: ограничение по source IP бессмысленно, когда каждый пакет приходит с нового поддельного адреса.
## 4. SYN cookies: stateless-хендшейк
SYN cookies выносят состояние соединения из памяти сервера прямо в сеть. Вместо выделения записи в backlog сервер кодирует всю нужную информацию в номере последовательности собственного SYN-ACK:
```
SYN-ACK seq = hash(secret, src_ip, src_port, dst_ip, dst_port) ← старшие 24 бита
+ timestamp mod 2^6 ← средние 5 бит (ротация)
+ кодировка MSS ← младшие 3 бита
```
Когда приходит финальный ACK, сервер пересчитывает ожидаемое значение из номера ACK. Если совпало — соединение завершено легитимно, и *только тогда* ядро выделяет сокет. Атакующий не израсходовал ни одной записи backlog.
```mermaid
sequenceDiagram
participant B as Атакующий (без ACK)
participant K as Ядро с syncookies
participant L as Легитимный клиент
B->>K: SYN
K-->>B: SYN-ACK с закодированным seq (состояние НЕ хранится)
Note over B: атакующий игнорирует — на сервере ничего не произошло
L->>K: SYN
K-->>L: SYN-ACK с закодированным seq (состояние НЕ хранится)
L->>K: ACK (seq+1 совпадает с cookie)
Note over K: cookie верифицирован → ТОЛЬКО СЕЙЧАС выделяем сокет
K-->>L: ESTABLISHED
rect rgb(230, 240, 255)
Note over K: ТОЧКА ФИЛЬТРАЦИИ: аллокация происходит только после<br/>криптографического доказательства способности к round-trip
end
```
### Trade-offs
SYN cookies не бесплатны:
- **Потеря TCP-опций** — window scale, SACK, timestamps согласовать нельзя: в номере последовательности нет свободных бит (в Linux сохраняется только небольшой код MSS). Легитимные клиенты получают ухудшенные соединения, пока cookies активны.
- **Нет учёта ретрансляций** — ядро не помнит отправленные SYN-ACK, поэтому поведение при потерях немного отличается от нормального пути.
- **Стоимость CPU** — хеш на каждый SYN вместо вставки в таблицу; обычно незаметно, но измеримо при миллионах pps.
- Cookies включаются только при давлении на backlog (`net.ipv4.tcp_syncookies = 1`, значение `2` = всегда) — это **резервный механизм**, а не первая линия обороны.
## 5. Эшелонированная защита в Rampart
Правильный порядок защиты — от дешёвого к дорогому: убить флад до того, как ядро вообще тронет свой backlog.
```mermaid
flowchart TD
A[SYN-пакеты приходят на NIC] --> B{XDP-программа:<br/>per-src-IP SYN throttle}
B -- "больше N SYN/сек с одного IP<br/>→ временный бан в eBPF map" --> X[XDP_DROP<br/>~50-100 нс/пакет]
B --> C{Невалидные флаги?<br/>SYN+FIN, SYN+RST}
C -- да --> X
C -- нет --> D[Стек ядра]
D --> E{Backlog переполнен?}
E -- "да → включаются SYN cookies<br/>(stateless, память не тратится)" --> F[Соединения с верным cookie проходят дальше]
E -- нет --> G[Обычный хендшейк]
F --> H[Userspace-движок:<br/>rate limit, PoW-challenge]
G --> H
style X fill:#f5d0d0
style F fill:#d0e8d0
```
Ответственность слоёв:
1. **XDP SYN throttle (первая линия)** — token bucket или fixed-window счётчик на source IP в eBPF LRU map. Больше N SYN/сек с одного адреса → дроп в драйвере, до выделения `sk_buff`. Это полностью закрывает одно-IP флуды и ослабляет распределённые.
2. **Sysctls ядра (вторая линия)** — `tcp_syncookies=1`, увеличенные `tcp_max_syn_backlog` и `somaxconn`, сниженный `tcp_synack_retries`. Полный список и обоснование: [kernel-tuning.md](../practice/kernel-tuning.md).
3. **Userspace-движок (третья линия)** — соединения, прошедшие хендшейк, упираются в per-IP rate limit и proof-of-work challenge до запуска любой прикладной логики.

View file

@ -0,0 +1,177 @@
# UDP Amplification: Small Request, Giant Answer
> Knowledge Base · Rampart attack fundamentals · Related: [defense-levels.md](../defense-levels.md), [syn-flood.md](./syn-flood.md)
## English
## 1. Why UDP is the attacker's favorite protocol
UDP is connectionless: anyone can send a packet to any port without ever completing a handshake, and the receiving service will answer. This enables two abuses at once:
- **Reflection** — the attacker sets the *source* IP of their request packets to the victim's address. The open UDP service then sends its reply to the victim, not to the attacker. The victim sees traffic coming from thousands of legitimate DNS/NTP/Memcached servers around the world — attribution and blocking become hard.
- **Amplification** — if the response is much larger than the request, every attacker byte is multiplied. The attack bandwidth is no longer limited by the botnet's uplink but by the amplification factor of the abused protocol.
Combined: a botnet with 1 Gbps of egress can generate tens or hundreds of Gbps of inbound traffic.
```mermaid
sequenceDiagram
participant A as Attacker (spoofing src = Victim)
participant R as Open resolver / NTP / Memcached
participant V as Victim
A->>R: small UDP query<br/>(src IP forged = V)
Note over A: cost: ~60 bytes of upload per query
R-->>V: huge UDP response<br/>(sent to spoofed source!)
Note over V: receives 28x–10000x the bytes<br/>FILTERING POINT: this traffic must die<br/>before it consumes real bandwidth/CPU
V->>R: (victim cannot tell "real" servers from reflectors)
```
The attacker never sees the responses and doesn't care — the goal is the victim's pipe, not a conversation.
## 2. Amplification factors
Measured as `response size / request size` for a single well-formed query:
| Protocol | Request | Response | Amplification factor |
|---|---|---|---|
| **DNS** (open resolver, ANY/other records) | ~60 B query | up to ~3 KB response | **~28–54x** |
| **NTP** (`monlist` on old versions) | ~234 B command | up to ~130 KB list of peers | **~556x** |
| **Memcached** (exposed UDP port 11211) | ~15 B `get` command | megabytes of cached data, chunked into datagrams | **~10,000x+** |
Memcached deserves special mention: a single exposed instance with a few GB of cache turns a tiny botnet into a terabit-class event — the largest recorded volumetric attacks have used exactly this vector.
Other commonly abused protocols follow the same pattern: Chargen (~356x), SNMP (~6x), SSDP (~30x), CoAP (~10x), Portmapper (~28x).
Why does this work? Because these are legitimate services answering what looks like a legitimate question. The reflector is a victim too — it did nothing wrong except being open to the internet with UDP.
## 3. Defense
Defense against amplification has three distinct levels, each belonging to a different party:
### 3.1 BCP38 at the upstream — kill spoofing at the source
BCP38 ("Network Ingress Filtering") means the ISP verifies that packets leaving a customer network carry source addresses that actually belong to that customer. If every upstream performed BCP38 filtering (uRPF loose/strict mode), reflection attacks would be impossible by construction — you cannot spoof an address whose route points elsewhere.
This is outside the victim's control; it must be demanded from providers. When choosing hosting/upstream, BCP38 compliance is a real selection criterion.
### 3.2 UDP policy drop on the edge — your own first line
If your service speaks TCP only (as Rampart's protected services do), then **every incoming UDP packet to your public ports is noise by definition**. Drop it in XDP, before the kernel allocates a socket buffer:
```mermaid
flowchart TD
U[UDP packet arrives] --> P{XDP program:<br/>is UDP allowed on this port?}
P -- "no UDP listener expected<br/>→ policy drop" --> D[XDP_DROP<br/>~50 ns/packet, before skb]
P -- "UDP legitimately used<br/>(e.g. QUIC/DNS you host)" --> RL{Per-source rate limit<br/>in eBPF LRU map}
RL -- over limit --> D
RL -- ok --> K[Kernel stack]
style D fill:#f5d0d0
```
Rules of thumb:
- No UDP service on the port → drop all UDP there.
- Legitimate UDP service → strict per-source rate limiting + response-size caps; never run Memcached-style protocols on public ports.
- Fragmented UDP → drop first fragments with MF flag set unless fragmentation is genuinely needed.
Because XDP drops happen in the NIC driver (~tens of nanoseconds per packet), even multi-million-pps floods consume a fraction of one core instead of saturating the machine.
### 3.3 Rate limiting per source — for the unavoidable remainder
Traffic you cannot classify away (legitimate UDP protocols) gets token-bucket limits per source IP and per subnet in eBPF maps, plus ASN-based reputation weighting (datacenter sources get stricter budgets than residential). Persistent offenders graduate to a blacklist synced across edge nodes.
### Summary table
| Level | Who deploys | What it stops |
|---|---|---|
| BCP38 / uRPF upstream | Providers | Spoofing itself — removes the root cause |
| XDP UDP policy drop | You | The flood reaching kernel/userspace at all |
| Per-source rate limit | You | Abuse of UDP ports you actually need |
## Русский
## 1. Почему UDP — любимый протокол атакующего
UDP не требует соединения: любой может отправить пакет на любой порт, не завершая хендшейк, и сервис ответит. Это открывает сразу две возможности для злоупотребления:
- **Reflection (рефлексия)** — атакующий подменяет *source*-адрес своих запросов на адрес жертвы. Открытый UDP-сервис шлёт ответ жертве, а не атакующему. Жертва видит трафик с тысяч легитимных DNS/NTP/Memcached-серверов по всему миру — атрибуция и блокировка резко усложняются.
- **Amplification (усиление)** — если ответ сильно больше запроса, каждый байт атакующего умножается. Полоса атаки больше не ограничена аплинком ботнета, а определяется коэффициентом усиления эксплуатируемого протокола.
Вместе: ботнет с исходящими 1 Гбит/с генерирует десятки и сотни Гбит/с входящего трафика.
```mermaid
sequenceDiagram
participant A as Атакующий (spoofed src = Жертва)
participant R as Открытый резолвер / NTP / Memcached
participant V as Жертва
A->>R: маленький UDP-запрос<br/>(подделан src IP = V)
Note over A: цена: ~60 байт аплинка за запрос
R-->>V: огромный UDP-ответ<br/>(уходит на поддельный адрес!)
Note over V: получает в 28–10000 раз больше байт<br/>ТОЧКА ФИЛЬТРАЦИИ: этот трафик должен умереть,<br/>пока он не съел реальную полосу/CPU
V->>R: (жертва не может отличить «настоящие» серверы от рефлекторов)
```
Ответы атакующему не нужны и не интересны ему — цель полоса жертвы, а не диалог.
## 2. Коэффициенты усиления
Измеряется как `размер ответа / размер запроса` для одного корректного запроса:
| Протокол | Запрос | Ответ | Коэффициент усиления |
|---|---|---|---|
| **DNS** (открытый резолвер, ANY и др.) | запрос ~60 Б | ответ до ~3 КБ | **~28–54x** |
| **NTP** (`monlist` на старых версиях) | команда ~234 Б | список пиров до ~130 КБ | **~556x** |
| **Memcached** (открытый UDP-порт 11211) | команда `get` ~15 Б | мегабайты кэша, разбитые на датаграммы | **~10000x+** |
Memcached заслуживает отдельного упоминания: один открытый инстанс с парой гигабайт кэша превращает крошечный ботнет в терабитное событие — крупнейшие задокументированные объёмные атаки использовали именно этот вектор.
Другие часто эксплуатируемые протоколы работают так же: Chargen (~356x), SNMP (~6x), SSDP (~30x), CoAP (~10x), Portmapper (~28x).
Почему это работает? Потому что это легитимные сервисы, отвечающие на внешне легитимный вопрос. Рефлектор тоже жертва — он ничего плохого не сделал, просто был открыт в интернет по UDP.
## 3. Защита
Защита от амплификации существует на трёх уровнях, и каждый принадлежит разной стороне:
### 3.1 BCP38 на аплинке — убить спуфинг в зародыше
BCP38 («Network Ingress Filtering») означает, что провайдер проверяет: пакеты, покидающие клиентскую сеть, несут source-адреса, реально принадлежащие этой сети. Если бы все аплинки делали BCP38-фильтрацию (uRPF loose/strict), атаки отражением стали бы невозможны конструктивно — нельзя подделать адрес, чей маршрут ведёт в другое место.
Это вне контроля жертвы; этого нужно требовать от провайдеров. При выборе хостинга/аплинка соответствие BCP38 — реальный критерий отбора.
### 3.2 UDP policy drop на edge — ваша первая линия
Если ваш сервис говорит только по TCP (как защищаемые Rampart'ом сервисы), то **каждый входящий UDP-пакет на публичные порты — шум по определению**. Дропайте его в XDP, до того как ядро выделит сокет-буфер:
```mermaid
flowchart TD
U[Пришёл UDP-пакет] --> P{XDP-программа:<br/>разрешён ли UDP на этом порту?}
P -- "UDP-листенер не ожидается<br/>→ policy drop" --> D[XDP_DROP<br/>~50 нс/пакет, до skb]
P -- "UDP легитимен<br/>(например, свой QUIC/DNS)" --> RL{Per-source rate limit<br/>в eBPF LRU map}
RL -- превышен --> D
RL -- ок --> K[Стек ядра]
style D fill:#f5d0d0
```
Практические правила:
- На порту нет UDP-сервиса → дропать весь UDP там.
- Легитимный UDP-сервис есть → строгий per-source rate limit + ограничение размера ответа; Memcached-подобные протоколы наружу никогда не выставлять.
- Фрагментированный UDP → дропать первый фрагмент с флагом MF, если фрагментация действительно не нужна.
Поскольку XDP-дроп происходит в драйвере NIC (~десятки наносекунд на пакет), даже многомиллионный pps-флад съедает долю одного ядра вместо насыщения машины.
### 3.3 Rate limit per source — для неизбежного остатка
Трафик, который нельзя классифицировать прочь (легитимные UDP-протоколы), получает token-bucket лимиты на source IP и подсеть в eBPF-мапах плюс взвешивание по ASN-репутации (датацентровым источникам — более строгие бюджеты, чем residential). Устойчивые нарушители попадают в blacklist, синхронизируемый между edge-нодами.
### Сводная таблица
| Уровень | Кто внедряет | Что останавливает |
|---|---|---|
| BCP38 / uRPF у аплинка | Провайдеры | Сам спуфинг — устраняет первопричину |
| XDP UDP policy drop | Вы | Достижение фладом ядра/userspace вообще |
| Per-source rate limit | Вы | Злоупотребление нужными вам UDP-портами |

238
docs/kb/defense-levels.md Normal file
View file

@ -0,0 +1,238 @@
# Where to Filter Traffic: Comparing Defense Levels
> Knowledge Base · Rampart networking fundamentals
>
> The single most important architectural decision in a network protection system is **where** in the packet's journey you decide to drop it. Every level further down the stack costs more CPU per packet and gives you more information per packet. This document compares the levels, shows the full path of a packet through the Linux network stack, and explains why Rampart uses a hybrid kernel fast-path + userspace smart-path design.
## English
## 1. The core trade-off
There is an inverse relationship between **how early** you can inspect a packet and **how much** you can know about it:
- Early (kernel, pre-skb): you see raw bytes — Ethernet header, IP header, TCP header. Inspection is nearly free, but you cannot see application semantics.
- Late (userspace proxy): you see fully reassembled protocol streams — handshakes, requests, sessions. Inspection is expensive per byte, but the decision quality is much higher.
Good defense puts cheap decisions first and expensive decisions last.
## 2. Comparison of filtering levels
| Level | Hook point | When packet is dropped | Latency added per packet | CPU cost per packet | What can be checked | Development complexity |
|---|---|---|---|---|---|---|
| **XDP / eBPF** | NIC driver (or generic), before `skb` allocation | Before the kernel allocates `sk_buff`, before any socket work | ~tens of ns; drop happens right after DMA | Lowest: no skb alloc, no conntrack unless you opt in. Native mode: 15–20M pps dropped per core on modern hardware; generic mode (virtio): ~3–5M pps | Raw L2/L3/L4 headers: IP validation, TCP flags sanity (SYN+FIN, SYN+RST), port allowlists, per-IP rate limits, blacklist/whitelist maps, SYN cookies implemented manually, simple state machines (e.g., SYN → SYN-ACK seen?) | High: C with eBPF verifier constraints (no unbounded loops in older kernels, pointer arithmetic rules, map-based state only); hard to debug |
| **tc / eBPF** (`clsact` qdisc) | Traffic control layer, after skb exists, both ingress & egress | After `skb` allocation but before netfilter and before the socket lookup | Low, slightly above XDP-generic | Medium-low: skb already allocated; still cheaper than netfilter chains for pure header logic | Everything XDP sees, plus: packet marks, classification into qdiscs, egress shaping, works on all interfaces including those without native XDP support (bridges, bonds in some setups) | High-ish: same eBPF verifier, plus tc filter plumbing (`tc filter add ... bpf`) |
| **nftables / iptables** (netfilter hooks) | `PREROUTING`, `INPUT`, `FORWARD`, `OUTPUT`, `POSTROUTING` | Inside the kernel network stack, before delivery to the socket | Low-to-medium; each rule is evaluated linearly | Medium: full hook chain traversal, optional conntrack (which itself costs memory and CPU per connection) | L3/L4 matching (addr/port/proto), conntrack state (`ESTABLISHED` vs `NEW`), rate limiting (`hashlimit`, `recent`), set-based lookups (nftables sets/maps), SYN proxying (`synproxy`), basic string matching (expensive) | Low-medium: declarative config, well documented, no verifier; but limited to what matches exist |
| **Userspace reverse proxy** (nginx / HAProxy / Envoy / custom Rust engine like Rampart edge) | Socket receive queue → application event loop (epoll/io_uring) | After the kernel has done the full TCP handshake and delivered bytes to userspace | Highest: syscall overhead, context switches, copies; measured in microseconds | Highest: full TCP state machine per connection, socket buffers, epoll wakeups. But cost is **per connection**, not per packet | Full L7 semantics: protocol parsing, request validity, authentication, PoW challenges, session behavior, per-client rate limiting, reputation, dynamic difficulty, TLS termination | Low-medium: normal programming language, easy testing; but every attack connection consumes kernel socket resources until you close it |
Key numbers to internalize (order-of-magnitude, hardware-dependent):
```
XDP native drop: ~50–100 ns/packet, millions of pps per core
XDP generic drop: a few hundred ns/packet (extra copy driver→skb path)
nftables drop: hundreds of ns to µs (rule count dependent)
conntrack entry: ~300 bytes RAM per tracked connection
Userspace accept+drop: ~10–20 µs CPU per connection (epoll, Rust/C)
≈ 80k new conn/s per core for a real L7 handler
```
The practical consequence: if a 5M pps flood reaches your userspace proxy, your proxy dies doing `accept()` calls that produce nothing. If the same flood hits XDP_DROP, it burns ~one core at most.
## 3. Packet path through the Linux stack
Where each mechanism can intervene:
```mermaid
flowchart TD
A[Packet arrives at NIC] --> B{XDP program<br/>attached to driver?}
B -- "XDP_DROP<br/>(no skb allocated)" --> DROPPED1[Dropped, cheapest]
B -- XDP_PASS --> C[Driver builds sk_buff]
C --> E{tc ingress filter<br/>clsact qdisc}
E -- "tc drop / bpf verdict" --> DROPPED2[Dropped, no netfilter cost]
E -- accept --> F[Netfilter PREROUTING]
F --> G{Conntrack<br/>new connection?}
G --> H[nftables INPUT chain<br/>rules, hashlimit, synproxy]
H -- REJECT/DROP --> DROPPED3[Dropped inside stack]
H -- ACCEPT --> I[TCP/IP stack processing<br/>SYN queue → accept queue]
I -- "backlog overflow,<br/>no syncookies" --> DROPPED4[Dropped by kernel]
I --> J[Socket listen queue<br/>somaxconn limit]
J --> K[Application accepts<br/>epoll / io_uring]
K --> L{L7 logic:<br/>parse, auth, PoW, rate limit}
L -- reject --> DROPPED5[Connection closed<br/>most expensive drop]
L -- valid --> M[Proxied to backend]
style DROPPED1 fill:#2e7d32,color:#fff
style DROPPED2 fill:#43a047,color:#fff
style DROPPED3 fill:#7cb342,color:#000
style DROPPED4 fill:#c0ca33,color:#000
style DROPPED5 fill:#e53935,color:#fff
```
The color gradient is the point: green drops are almost free, red drops cost a full TCP handshake plus userspace scheduling. Every layer you let a bad packet traverse multiplies its cost to you by orders of magnitude.
## 4. Decision rule: drop as early as possible, decide as late as necessary
A useful way to split responsibilities:
### Filter at L3/L4 (kernel: XDP, tc, nftables)
Volume attacks where correctness does not require understanding the payload:
- SYN floods, ACK floods, RST/FIN floods with no matching connection state
- Invalid or nonsensical TCP flag combinations (SYN+FIN, SYN+RST, URG-only)
- IP fragmentation abuse (drop initial fragments when the protocol never needs them)
- Spoofed sources (uRPF-style checks, TTL heuristics)
- Known-bad IPs from a shared blacklist (BPF maps updated from userspace)
- Per-IP packet/connection rate limiting above a generous threshold
These checks are per-packet, stateless or near-stateless, and identical for every protocol — they do not need to know whether the traffic is HTTP, SSH, game traffic, or anything else. That makes them perfect candidates for compile-time-reusable kernel components.
### Decide at L7 (userspace)
Anything requiring session context or protocol semantics:
- Is this a syntactically and semantically valid request?
- Has this client proven computational effort (PoW challenge)?
- Does this client's behavior over time look human/legitimate (reputation)?
- Should difficulty rise because aggregate load crossed a threshold?
These questions cannot be answered from headers alone, so they must live where the stream is visible — the userspace engine.
### The boundary principle
If a check depends only on fields present in the first ~100 bytes of the frame and applies to every packet equally → push it toward XDP/tc/nftables.
If a check depends on history, reassembly, or protocol grammar → keep it in userspace.
Everything else lives somewhere between, typically nftables with conntrack or a tc BPF state machine.
## 5. Why hybrid architecture is the industry standard
No single level survives contact with real attacks:
- Kernel-only defense (firewall tuning alone): survives volumetric floods, but cannot tell a legitimate client from a bot once headers look fine.
- Userspace-only defense: excellent decision quality, but a modest pps flood exhausts the softirq/accept path before your smart logic ever runs.
Hence the pattern used by commercial scrubbing services and open-source stacks alike:
1. **Kernel fast-path**: XDP drops the obvious garbage at line rate; per-IP throttling keeps the connection rate bounded; blacklists are hot-updated from userspace via BPF maps.
2. **Userspace smart-path**: what survives is a small, semantically rich stream that justifies spending tens of microseconds per connection on parsing, PoW, and reputation.
The two halves communicate: userspace intelligence writes policy (block this IP, raise throttle for this prefix) into kernel maps; kernel telemetry (dropped counters, sampled offenders) flows back up to userspace for analysis. Neither half is useful alone; together they cover both axes of the trade-off.
This is exactly how Rampart structures its edge: eBPF/XDP programs own L3/L4 volume rejection, the Rust engine owns L7 verification (protocol plugins behind feature flags, universal SHA256 proof-of-work), and shared maps/counters are the contract between them.
---
---
## Русский
## 1. Ключевой компромисс
Между тем, **насколько рано** вы можете инспектировать пакет, и тем, **сколько** вы о нём знаете, существует обратная зависимость:
- Рано (ядро, до skb): видны сырые байты — заголовки Ethernet, IP, TCP. Инспекция почти бесплатна, но семантику приложения не видно.
- Поздно (userspace-прокси): видны полностью собранные потоки протокола — хендшейки, запросы, сессии. Инспекция дорогая в расчёте на байт, зато качество решения намного выше.
Хорошая защита сначала применяет дешёвые проверки, а дорогие оставляет напоследок.
## 2. Сравнение уровней фильтрации
| Уровень | Точка перехвата | Когда пакет дропается | Добавляемая задержка на пакет | Стоимость CPU на пакет | Что можно проверить | Сложность разработки |
|---|---|---|---|---|---|---|
| **XDP / eBPF** | Драйвер NIC (или generic), до аллокации `skb` | До того как ядро выделит `sk_buff`, до какой-либо работы с сокетами | Десятки нс; дроп сразу после DMA | Минимальная: нет аллокации skb, нет conntrack без явного включения. Native-режим: 15–20 млн pps дропа на ядро на современном железе; generic-режим (virtio): ~3–5 млн pps | Сырые заголовки L2/L3/L4: валидация IP, проверка флагов TCP (SYN+FIN, SYN+RST), порт-белые списки, per-IP rate limit, чёрные/белые списки через BPF-мапы, самописные SYN cookies, простые конечные автоматы (например: SYN → видели SYN-ACK?) | Высокая: язык C с ограничениями eBPF-верификатора (нет неограниченных циклов на старых ядрах, правила арифметики указателей, состояние только через мапы); тяжело отлаживать |
| **tc / eBPF** (`clsact` qdisc) | Уровень traffic control, после существования skb, ingress и egress | После аллокации `skb`, но до netfilter и до поиска сокета | Низкая, чуть выше XDP-generic | Средне-низкая: skb уже выделен; для чистой логики на заголовках всё ещё дешевле цепочек netfilter | Всё, что видит XDP, плюс: маркировка пакетов, классификация в qdisc, шейпинг на egress, работает на интерфейсах без нативного XDP (мосты, бонды в ряде конфигураций) | Повышенная: тот же верификатор eBPF плюс обвязка tc-фильтров (`tc filter add ... bpf`) |
| **nftables / iptables** (хуки netfilter) | `PREROUTING`, `INPUT`, `FORWARD`, `OUTPUT`, `POSTROUTING` | Внутри сетевого стека ядра, до доставки сокету | От низкой к средней; правила вычисляются линейно | Средняя: полный проход по цепочке хуков, опциональный conntrack (который сам стоит памяти и CPU на каждое соединение) | L3/L4-сопоставление (адрес/порт/протокол), состояние conntrack (`ESTABLISHED` vs `NEW`), rate limiting (`hashlimit`, `recent`), lookup по сетам (nftables sets/maps), SYN-проксирование (`synproxy`), базовый string match (дорогой) | Низко-средняя: декларативный конфиг, хорошая документация, нет верификатора; но ограничено набором существующих match'ей |
| **Userspace reverse proxy** (nginx / HAProxy / Envoy / собственный Rust-движок, как edge в Rampart) | Очередь приёма сокета → event loop приложения (epoll/io_uring) | После того как ядро полностью завершило TCP handshake и отдало байты в userspace | Наибольшая: накладные расходы syscall'ов, переключения контекста, копирования; микросекунды | Максимальная: полный TCP state machine на соединение, буферы сокетов, пробуждения epoll. Но стоимость считается **на соединение**, а не на пакет | Полная семантика L7: парсинг протокола, валидность запросов, аутентификация, PoW-челленджи, поведение сессий, per-client rate limit, репутация, динамическая сложность, терминирование TLS | Низко-средняя: обычный язык программирования, простое тестирование; но каждое атакующее соединение потребляет ресурсы сокетов ядра, пока его не закроешь |
Ключевые цифры, которые стоит запомнить (порядок величин, зависит от железа):
```
XDP native drop: ~50–100 нс/пакет, миллионы pps на ядро
XDP generic drop: несколько сотен нс/пакет (лишняя копия по пути driver→skb)
nftables drop: сотни нс — мкс (зависит от числа правил)
Запись conntrack: ~300 байт RAM на отслеживаемое соединение
Accept+drop в юзерспейсе: ~10–20 мкс CPU на соединение (epoll, Rust/C)
≈ 80 тыс. новых conn/s на ядро у реального L7-обработчика
```
Практический вывод: если флуд в 5 млн pps дойдёт до вашего userspace-прокси, прокси умрёт, выполняя пустые `accept()`. Если тот же флуд упирается в XDP_DROP, он сожжёт максимум одно ядро.
## 3. Путь пакета через стек Linux
Где каждый механизм может вмешаться:
```mermaid
flowchart TD
A[Пакет прибыл на NIC] --> B{XDP-программа<br/>прицеплена к драйверу?}
B -- "XDP_DROP<br/>(skb не выделялся)" --> DROPPED1[Дропнут, дешевле некуда]
B -- XDP_PASS --> C[Драйвер создаёт sk_buff]
C --> E{tc ingress фильтр<br/>qdisc clsact}
E -- "tc drop / вердикт bpf" --> DROPPED2[Дропнут, без затрат netfilter]
E -- accept --> F[Netfilter PREROUTING]
F --> G{Conntrack:<br/>новое соединение?}
G --> H[Цепочка nftables INPUT<br/>правила, hashlimit, synproxy]
H -- REJECT/DROP --> DROPPED3[Дропнут внутри стека]
H -- ACCEPT --> I[Обработка стека TCP/IP<br/>SYN-очередь → accept-очередь]
I -- "переполнение backlog,<br/>без syncookies" --> DROPPED4[Дропнут ядром]
I --> J[Очередь прослушивания сокета<br/>лимит somaxconn]
J --> K[Приложение принимает<br/>epoll / io_uring]
K --> L{Логика L7:<br/>парсинг, аутентификация, PoW, rate limit}
L -- reject --> DROPPED5[Соединение закрыто<br/>самый дорогой дроп]
L -- valid --> M[Проксировано на бэкенд]
style DROPPED1 fill:#2e7d32,color:#fff
style DROPPED2 fill:#43a047,color:#fff
style DROPPED3 fill:#7cb342,color:#000
style DROPPED4 fill:#c0ca33,color:#000
style DROPPED5 fill:#e53935,color:#fff
```
Цветовой градиент здесь и есть главная мысль: зелёные дропы почти бесплатны, красный стоит полного TCP handshake плюс планирования в userspace. Каждый уровень, который плохой пакет проходит насквозь, умножает его стоимость для вас на порядки.
## 4. Правило решения: дропай как можно раньше, решай как можно позже
Полезный способ разделить ответственность:
### Фильтровать на L3/L4 (ядро: XDP, tc, nftables)
Объёмные атаки, где корректность не требует понимания полезной нагрузки:
- SYN-флуды, ACK-флуды, RST/FIN-флуды без соответствующего состояния соединения
- Неверные или бессмысленные комбинации TCP-флагов (SYN+FIN, SYN+RST, только URG)
- Злоупотребление IP-фрагментацией (дроп первых фрагментов, когда протоколу они никогда не нужны)
- Поддельные источники (проверки в духе uRPF, эвристики по TTL)
- Известные плохие IP из общего блэклиста (BPF-мапы, обновляемые из userspace)
- Per-IP ограничение частоты пакетов/соединений выше щедрого порога
Эти проверки попакетные, без состояния или почти без состояния, и одинаковы для любого протокола — им не нужно знать, HTTP это, SSH, игровой трафик или что-то ещё. Это делает их идеальными кандидатами на переиспользуемые compile-time компоненты ядра.
### Решать на L7 (userspace)
Всё, что требует контекста сессии или семантики протокола:
- Является ли запрос синтаксически и семантически валидным?
- Доказал ли клиент вычислительные затраты (PoW-челлендж)?
- Похожа ли история поведения клиента на легитимную (репутация)?
- Нужно ли поднять сложность, потому что суммарная нагрузка пересекла порог?
На эти вопросы нельзя ответить по одним заголовкам, поэтому они живут там, где виден поток — в движке userspace.
### Принцип границы
Если проверка зависит только от полей в первых ~100 байтах кадра и одинаково применима к каждому пакету → двигайте её в XDP/tc/nftables.
Если проверка зависит от истории, сборки потока или грамматики протокола → держите её в userspace.
Всё остальное лежит посередине — обычно это nftables с conntrack или конечный автомат на tc BPF.
## 5. Почему гибридная архитектура — стандарт индустрии
Ни один отдельный уровень не выдерживает встречи с реальными атаками:
- Только ядро (один лишь тюнинг firewall'а): переживёт объёмный флуд, но не отличит легитимного клиента от бота, если заголовки выглядят нормально.
- Только userspace: отличное качество решений, но даже скромный флуд по pps исчерпает путь softirq/accept раньше, чем заработает ваша умная логика.
Поэтому и коммерческие сервисы чистки трафика, и open-source стеки используют одну схему:
1. **Kernel fast-path**: XDP сбрасывает очевидный мусор на линейной скорости; per-IP троттлинг ограничивает частоту соединений; блэклисты горячо обновляются из userspace через BPF-мапы.
2. **Userspace smart-path**: то, что выжило, — небольшой семантически насыщенный поток, ради которого оправданы десятки микросекунд CPU на соединение на парсинг, PoW и репутацию.
Половины общаются между собой: интеллект в userspace пишет политику (заблокировать этот IP, поднять троттлинг для этого префикса) в kernel-мапы; телеметрия ядра (счётчики дропов, сэмплы нарушителей) течёт наверх в userspace для анализа. Поодиночке ни одна половина не полезна; вместе они закрывают обе оси компромисса.
Именно так устроен edge в Rampart: программы eBPF/XDP отвечают за объёмное отсечение на L3/L4, Rust-движок — за верификацию на L7 (протокол-плагины за feature-флагами, универсальный SHA256 proof-of-work), а разделяемые мапы и счётчики — контракт между ними.

View file

@ -0,0 +1,290 @@
# Kernel Tuning Against DDoS (sysctl)
> Knowledge Base · Practice
>
> Before any smart defense runs, the Linux kernel itself decides how many half-open connections it will tolerate, how deep its queues are, and when it gives up. Misconfigured defaults turn a moderate flood into a total outage. This article walks through the sysctl parameters that matter, explains each one, and provides a ready-to-adapt config.
## English
## 1. How it works
All parameters below live under `/proc/sys/` and can be set either at runtime (`sysctl -w`, lost on reboot) or persistently via files in `/etc/sysctl.d/*.conf` (applied by `systemd-sysctl` at boot or `sysctl --system` manually). The `/proc/sys/net/ipv4/tcp_syncookies` file corresponds to the `net.ipv4.tcp_syncookies` key, and so on.
## 2. Parameters explained
### TCP SYN flood protection
**`net.ipv4.tcp_syncookies = 1`**
When the SYN accept queue for a listening socket overflows, the kernel stops storing half-open connections and instead encodes the connection state into cryptographically signed cookies inside the SYN-ACK sequence number. A real client returns the cookie in its ACK; a flooder usually does not. This is the single most important anti-SYN-flood knob in stock Linux.
- `0` — disabled
- `1` — enabled unconditionally
- `2` — always send cookies regardless of queue state (more aggressive; use only if you know what you are doing, since some TCP option negotiation degrades)
Trade-off: cookies cannot carry TCP options negotiated during handshake (window scaling is preserved on Linux via a syncookie extension, but SACK/Timestamps may be lost), so throughput per connection can drop slightly. Keep it `1` — the cost is negligible versus being SYN-flooded to death with default settings.
**`net.ipv4.tcp_max_syn_backlog = 65535`**
Maximum number of half-open (SYN received, ACK not yet) connections queued **per listening socket** before the kernel starts dropping SYNs or engaging syncookies. Default is often 128–1024 depending on kernel version and `net.core.somaxconn`. Raising it lets a burst of legitimate clients through without immediately triggering cookie mode. Cost: each queued SYN consumes a small amount of memory (request_sock structure), so this scales with RAM, not much else.
**`net.ipv4.tcp_synack_retries = 2`** and **`net.ipv4.tcp_syn_retries = 2`**
How many times the kernel retransmits SYN-ACK (server side) / SYN (client side) without an answer before giving up. Lower values clean up dead half-open connections faster, freeing backlog slots during floods. Default 5–6 means a dead connection lingers for over a minute; value `2` cuts that to seconds.
### Listen queues
**`net.core.somaxconn = 65535`**
Upper bound on the *accept* queue (fully established connections waiting for the application to call `accept()`) of every listening socket. Critically: the application must also ask for a large backlog (the second argument of `listen()`); the effective value is `min(somaxconn, app_backlog)`. If your proxy accepts slower than clients connect, a full accept queue causes silent SYN drops or resets — raising `somaxconn` alone does nothing if the app requests 128.
**`net.ipv4.tcp_abort_on_overflow = 0`**
Leave at `0` (default): when the accept queue overflows, the kernel silently drops SYNs, letting clients retry gracefully. Setting it to `1` sends RST immediately — useful for fail-fast load tests, harmful in production.
### Packet ingress
**`net.core.netdev_max_backlog = 65535`**
Queue of frames received by the NIC but not yet processed by the CPU's softirq (NET_RX) loop. Under a packet burst faster than the CPU can drain it, this queue absorbs the difference; overflow means dropped packets *before* any filtering logic sees them. Default is often 1000, which a multi-million-pps burst exhausts instantly. Trade-off: larger backlog adds latency under sustained overload and holds more memory; it buys time for the drainer but never replaces actual filtering.
**`net.core.rmem_max` / `net.core.wmem_max`**
Ceiling for socket receive/send buffer sizes that applications can request via SO_RCVBUF/SO_SNDBUF (and for autotuning limits via the `tcp_rmem`/`tcp_wmem` third values). For high-throughput proxies moving bulk data, 16–128 MB ceilings are common; for a pure drop/throttle edge they matter little.
### Ephemeral ports and connection churn
**`net.ipv4.ip_local_port_range = 1024 65535`**
Range of source ports used for outgoing connections. When your node acts as a client — e.g., an edge proxy opening upstream connections toward backends — each concurrent outgoing connection consumes one tuple (src IP, src port, dst IP, dst port). The old default `32768 60999` gives ~28k ports; widening to the full range gives ~64k per destination tuple. Combined with:
**`net.ipv4.tcp_fin_timeout = 15`** (default 60)
Seconds a socket sits in FIN-WAIT-2 after the peer closed. Long waits tie up tuples and fd entries during churn-heavy attacks (connect-flood patterns). Lowering to 10–30 accelerates cleanup. Note: this does **not** affect TIME_WAIT duration (that's fixed at ~60s, controlled indirectly by `tcp_tw_reuse`).
**`net.ipv4.tcp_tw_reuse = 1`**
Allows reuse of TIME_WAIT sockets for *outgoing* connections when timestamps make it safe. Helps when port exhaustion, not memory, is the constraint.
### Conntrack
**`net.netfilter.nf_conntrack_max`**
Maximum number of tracked connections in the conntrack table. Only relevant if you use stateful filtering (nftables/iptables with `-m conntrack`, NAT). A connection flood can fill this table; once full, new legitimate connections get dropped too. Size it as: expected peak concurrent connections × safety factor (2–4). Each entry costs roughly 300 bytes, so 1M entries ≈ 300 MB.
**`net.netfilter.nf_conntrack_buckets`**
Hash table buckets (set at module load time, read-only afterwards). Rule of thumb: `conntrack_max / 4`. If you don't need conntrack at all on a dedicated edge box, disabling the modules entirely saves both memory and per-packet CPU.
## 3. Ready config
`/etc/sysctl.d/ddos.conf`:
```ini
# /etc/sysctl.d/ddos.conf
# Kernel hardening against volumetric L3/L4 attacks.
# Apply with: sudo sysctl --system
# --- SYN flood ---
# Enable SYN cookies when half-open queue overflows.
net.ipv4.tcp_syncookies = 1
# Deeper half-open queue per listening socket.
net.ipv4.tcp_max_syn_backlog = 65535
# Give up on dead handshakes fast.
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_syn_retries = 2
# --- Accept queues ---
# Allow applications to request large listen backlogs.
net.core.somaxconn = 65535
# Do NOT reset clients on queue overflow (graceful retry).
net.ipv4.tcp_abort_on_overflow = 0
# --- Ingress buffering ---
# Absorb bursts between NIC IRQ and softirq processing.
net.core.netdev_max_backlog = 65535
# Socket buffer ceilings for bulk transfer.
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
# --- Outgoing connection churn (edge -> backend) ---
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
# --- Conntrack (only if using stateful rules) ---
# 1M tracked connections, ~300MB RAM.
net.netfilter.nf_conntrack_max = 1048576
# Reduce timeout of half-open tracked connections.
net.netfilter.nf_conntrack_tcp_timeout_syn_recv = 30
# --- Optional: silence ICMP flood ---
# net.ipv4.icmp_echo_ignore_all = 1
```
Apply and verify:
```bash
sudo sysctl --system
sysctl net.ipv4.tcp_syncookies net.core.somaxconn net.core.netdev_max_backlog
# Confirm the app-side backlog matches:
ss -lnt # Recv-Q column shows current accept queue usage vs limit on LISTEN sockets
```
## 4. Trade-offs and warnings
1. **Bigger queues ≠ protection.** Deep backlogs buy seconds of absorption. Without early dropping (XDP/nftables) they just delay the collapse and add memory pressure. Tuning raises the ceiling; it does not remove the attack.
2. **Memory accounting.** `tcp_max_syn_backlog=65535` × many listening ports × request_sock size, plus conntrack at ~300 B/entry — do the arithmetic for your VDS RAM. On a 2 GB box, don't set conntrack_max to 4M.
3. **Latency under overload.** Large `netdev_max_backlog` means packets wait longer in softirq queues exactly when the system is already overloaded — tail latency grows even though drops shrink.
4. **Application cooperation.** `somaxconn` is only a cap. The effective backlog is whatever the application passes to `listen()`. Check with `ss -lnt`.
5. **Conntrack is optional.** A dedicated edge doing stateless XDP drops + userspace verification may not need conntrack at all; loading it just to have big tables wastes CPU per packet.
6. **Test changes under load**, not just `sysctl --system`. A parameter that looks harmless can shift behavior dramatically at 100k pps (see stress-testing.md).
---
---
## Русский
# Тюнинг ядра Linux против DDoS (sysctl)
> Knowledge Base · Практика
>
> Прежде чем заработает какая-либо умная защита, само ядро Linux решает, сколько полуоткрытых соединений оно потерпит, насколько глубоки его очереди и когда сдаваться. Неоптимальные дефолты превращают умеренный флуд в полный отказ обслуживания. В этой статье разобраны значимые параметры sysctl, объяснён каждый и приведён готовый конфиг для адаптации.
## 1. Как это устроено
Все перечисленные параметры живут в `/proc/sys/` и задаются либо на лету (`sysctl -w`, сбрасывается при перезагрузке), либо постоянно через файлы в `/etc/sysctl.d/*.conf` (применяются `systemd-sysctl` при загрузке или вручную командой `sysctl --system`). Файл `/proc/sys/net/ipv4/tcp_syncookies` соответствует ключу `net.ipv4.tcp_syncookies` и так далее.
## 2. Разбор параметров
### Защита от SYN flood
**`net.ipv4.tcp_syncookies = 1`**
Когда очередь SYN для слушающего сокета переполняется, ядро перестаёт хранить полуоткрытые соединения и вместо этого кодирует состояние соединения в криптографически подписанные cookie внутри номера последовательности SYN-ACK. Реальный клиент вернёт cookie в своём ACK; флудер обычно нет. Это важнейший штатный регулятор против SYN-флуда в Linux.
- `0` — выключено
- `1` — включено по необходимости
- `2` — всегда слать cookie независимо от состояния очередей (агрессивнее; используйте, только если понимаете последствия, поскольку часть согласования TCP-опций деградирует)
Компромисс: cookie не могут переносить TCP-опции, согласовываемые в handshake (window scaling в Linux сохраняется через расширение syncookie, но SACK/Timestamps могут потеряться), поэтому пропускная способность соединения может слегка упасть. Держите `1` — цена ничтожна в сравнении с гибелью от SYN-флуда на дефолтах.
**`net.ipv4.tcp_max_syn_backlog = 65535`**
Максимум полуоткрытых (SYN получен, ACK ещё нет) соединений в очереди **на каждый слушающий сокет**, прежде чем ядро начнёт дропать SYN или включать syncookies. По умолчанию часто 128–1024 в зависимости от версии ядра и `net.core.somaxconn`. Увеличение позволяет всплеску легитимных клиентов пройти без немедленного перехода в cookie-режим. Цена: каждая SYN в очереди потребляет немного памяти (структура request_sock) — растёт расход RAM, больше почти ничего.
**`net.ipv4.tcp_synack_retries = 2`** и **`net.ipv4.tcp_syn_retries = 2`**
Сколько раз ядро ретранслирует SYN-ACK (сервер) / SYN (клиент) без ответа, прежде чем сдаться. Меньшие значения быстрее вычищают мёртвые полуоткрытые соединения, освобождая слоты backlog'а во время флуда. Дефолтные 5–6 означают, что мёртвое соединение висит больше минуты; значение `2` сокращает это до секунд.
### Очереди прослушивания
**`net.core.somaxconn = 65535`**
Верхняя граница *accept*-очереди (полностью установленные соединения, ожидающие вызова `accept()` приложением) каждого слушающего сокета. Критично: приложение тоже должно запросить большой backlog (второй аргумент `listen()`); эффективное значение — `min(somaxconn, backlog_приложения)`. Если прокси принимает медленнее, чем клиенты подключаются, переполненная accept-очередь вызывает тихие дропы SYN или reset'ы — поднятие одного лишь `somaxconn` ничего не даст, если приложение запрашивает 128.
**`net.ipv4.tcp_abort_on_overflow = 0`**
Оставьте `0` (дефолт): при переполнении accept-очереди ядро молча дропает SYN, позволяя клиентам корректно повторить попытку. Значение `1` шлёт RST немедленно — полезно для fail-fast нагрузочных тестов, вредно в продакшене.
### Приём пакетов
**`net.core.netdev_max_backlog = 65535`**
Очередь кадров, принятых NIC, но ещё не обработанных циклом softirq (NET_RX) на CPU. Когда всплеск пакетов быстрее, чем CPU успевает разгребать, эта очередь поглощает разницу; переполнение означает дроп пакетов *до* того, как их увидит хоть какая-то логика фильтрации. Дефолт часто 1000 — всплеск в миллионы pps исчерпывает его мгновенно. Компромисс: увеличенный backlog добавляет задержку при устойчивой перегрузке и держит больше памяти; он покупает время для разгребающего, но никогда не заменяет саму фильтрацию.
**`net.core.rmem_max` / `net.core.wmem_max`**
Потолок размеров буферов приёма/отправки сокетов, которые приложения могут запросить через SO_RCVBUF/SO_SNDBUF (и лимиты автотюнинга через третьи значения `tcp_rmem`/`tcp_wmem`). Для высокопроизводительных прокси, гоняющих объёмные данные, потолки 16–128 МБ обычны; для чистого drop/throttle-edge они мало что меняют.
### Эфемерные порты и churn соединений
**`net.ipv4.ip_local_port_range = 1024 65535`**
Диапазон исходящих портов для исходящих соединений. Когда нода выступает клиентом — например, edge-прокси открывает апстрим-соединения к бэкендам, — каждое одновременное исходящее соединение занимает один кортеж (src IP, src порт, dst IP, dst порт). Старый дефолт `32768 60999` даёт ~28 тыс. портов; расширение до полного диапазона — ~64 тыс. на кортеж назначения. В паре с:
**`net.ipv4.tcp_fin_timeout = 15`** (дефолт 60)
Секунды, которые сокет висит в FIN-WAIT-2 после закрытия пиром. Долгое ожидание связывает кортежи и fd во время churn-атак (паттерны connect-flood). Снижение до 10–30 ускоряет очистку. Заметьте: это **не влияет** на длительность TIME_WAIT (она фиксирована ~60 с, косвенно управляется через `tcp_tw_reuse`).
**`net.ipv4.tcp_tw_reuse = 1`**
Разрешает переиспользование сокетов в TIME_WAIT для *исходящих* соединений, когда timestamps делают это безопасным. Помогает, когда ограничением является нехватка портов, а не памяти.
### Conntrack
**`net.netfilter.nf_conntrack_max`**
Максимальное число отслеживаемых соединений в таблице conntrack. Актуально только при stateful-фильтрации (nftables/iptables с `-m conntrack`, NAT). Флуд соединений может заполнить таблицу; после этого дропаются уже и новые легитимные соединения. Размер: ожидаемый пик одновременных соединений × коэффициент запаса (2–4). Каждая запись стоит примерно 300 байт, значит 1 млн записей ≈ 300 МБ.
**`net.netfilter.nf_conntrack_buckets`**
Число корзин хеш-таблицы (задаётся при загрузке модуля, потом read-only). Правило большого пальца: `conntrack_max / 4`. Если conntrack на выделенном edge вообще не нужен — полное отключение модулей экономит и память, и CPU на каждом пакете.
## 3. Готовый конфиг
`/etc/sysctl.d/ddos.conf`:
```ini
# /etc/sysctl.d/ddos.conf
# Устойчивость ядра к объёмным атакам L3/L4.
# Применить: sudo sysctl --system
# --- SYN flood ---
# SYN cookies при переполнении очереди полуоткрытых.
net.ipv4.tcp_syncookies = 1
# Более глубокая очередь полуоткрытых на каждый слушающий сокет.
net.ipv4.tcp_max_syn_backlog = 65535
# Быстро отказываться от мёртвых handshake.
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_syn_retries = 2
# --- Очереди accept ---
# Разрешить приложениям большие listen-backlog.
net.core.somaxconn = 65535
# НЕ сбрасывать клиентов при переполнении очереди (мягкий retry).
net.ipv4.tcp_abort_on_overflow = 0
# --- Буферизация входа ---
# Поглощать всплески между IRQ сетевой карты и обработкой softirq.
net.core.netdev_max_backlog = 65535
# Потолки буферов сокетов для объёмной передачи.
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
# --- Churn исходящих соединений (edge → бэкенд) ---
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
# --- Conntrack (только при stateful-правилах) ---
# 1 млн отслеживаемых соединений, ~300 МБ RAM.
net.netfilter.nf_conntrack_max = 1048576
# Сократить таймаут полуоткрытых записей.
net.netfilter.nf_conntrack_tcp_timeout_syn_recv = 30
# --- Опционально: заглушить ICMP flood ---
# net.ipv4.icmp_echo_ignore_all = 1
```
Применить и проверить:
```bash
sudo sysctl --system
sysctl net.ipv4.tcp_syncookies net.core.somaxconn net.core.netdev_max_backlog
# Убедиться, что app-side backlog совпадает:
ss -lnt # колонка Recv-Q показывает текущее использование accept-очереди против лимита на LISTEN-сокетах
```
## 4. Компромиссы и предупреждения
1. **Большие очереди ≠ защита.** Глубокие backlog'и покупают секунды поглощения. Без раннего отсечения (XDP/nftables) они лишь откладывают коллапс и давят на память. Тюнинг поднимает потолок, но не устраняет атаку.
2. **Учёт памяти.** `tcp_max_syn_backlog=65535` × число слушающих портов × размер request_sock, плюс conntrack по ~300 Б/запись — посчитайте под свою VDS. На машине со 2 ГБ не ставьте conntrack_max в 4M.
3. **Задержка при перегрузке.** Большой `netdev_max_backlog` означает, что пакеты ждут дольше в softirq-очередях именно тогда, когда система уже перегружена — хвостовая задержка растёт, даже хотя дропов меньше.
4. **Кооперация приложения.** `somaxconn` — только потолок. Эффективный backlog — то, что приложение передаёт в `listen()`. Проверяйте через `ss -lnt`.
5. **Conntrack опционален.** Выделенный edge с stateless XDP-дропами и юзерспейс-верификацией может вообще обходиться без conntrack; грузить его ради больших таблиц — тратить CPU на каждом пакете.
6. **Тестируйте изменения под нагрузкой**, а не только `sysctl --system`. Параметр, выглядящий безобидно, может радикально поменять поведение на 100 тыс. pps (см. stress-testing.md).

View file

@ -0,0 +1,333 @@
# NIC Tuning: Queues, IRQ Affinity, Ring Buffers, Offloads
> Knowledge Base · Practice
>
> The network interface card is the first CPU consumer on the packet's path. A single-queue NIC feeding one core caps your entire defense at whatever that core can process; misconfigured offloads silently corrupt or slow traffic. This article covers multiqueue setup, receive-side scaling, interrupt distribution, ring buffers, and offload switches — with commands you can run to verify each step.
## English
## 1. Multiqueue NICs (ethtool -L)
Modern NICs expose multiple hardware RX/TX queues, each with its own interrupt. The kernel spreads packets across queues using a hash of header fields (RSS). If only 1 combined channel is active, all packets — including millions of flood packets — hit a single core.
```bash
# Show current queue count and max supported
ethtool -l eth0
# Example output:
# Channel parameters for eth0:
# Pre-set maximums:
# RX: 8
# TX: 8
# Other: 1
# Combined: 8
# Current hardware settings:
# RX: 1
# TX: 1
# Other: 1
# Combined: 1 <-- everything lands on one queue!
# Raise combined channels (RX+TX share) to maximum
sudo ethtool -L eth0 combined 8
# Or set RX/TX separately if the card distinguishes them
sudo ethtool -L eth0 rx 4 tx 4
```
On virtual machines (virtio), the number of queues is bounded by `queues` in the VM configuration and vCPUs available; match queues to vCPU count.
## 2. RSS / RPS / XPS
Three mechanisms with confusingly similar names:
| Mechanism | Layer | What it does | Configured via |
|---|---|---|---|
| **RSS** (Receive Side Scaling) | Hardware | NIC hashes packet headers → picks RX queue → raises that queue's IRQ | `ethtool -x eth0` (view), `ethtool -X eth0` (set indirection table) |
| **RPS** (Receive Packet Steering) | Software kernel | Same idea as RSS but done in software after the driver receives the packet — for NICs/queues without RSS or to spread further | sysfs per RX queue |
| **XPS** (Transmit Packet Steering) | Software kernel | Picks TX queue matching the transmitting CPU, keeping flow locality | sysfs per TX queue |
```bash
# View RSS indirection table and hash key
ethtool -x eth0
# Steer queues across CPUs evenly (example for 4 queues)
sudo ethtool -X eth0 equal 4
# Enable RPS on the first RX queue: point it at CPUs 2-3 (mask 0xc = bits 2,3)
echo c | sudo tee /sys/class/net/eth0/queues/rx-0/rps_cpus
# Check whether RPS is actually spreading (counters per CPU)
cat /proc/net/softnet_stat # column 2 = dropped at softirq, column 10+ depend on kernel version
# XPS: steer tx-0 to CPU 0 (mask 0x1)
echo 1 | sudo tee /sys/class/net/eth0/queues/tx-0/xps_cpus
```
Rule of thumb: prefer RSS when the hardware supports it; add RPS only where hardware queues < cores; XPS matters mainly for high-throughput *outgoing* paths.
## 3. IRQ affinity
Each RX queue generates interrupts. By default, the kernel may route them all to CPU0 — creating a hotspot exactly where your XDP program will also run.
```bash
# See how interrupts are currently distributed
grep eth0 /proc/interrupts
# List IRQ numbers of the NIC queues
grep -E 'eth0.*TxRx' /proc/interrupts | awk '{print $1}' | tr -d ':'
# Pin IRQ 41 to CPU 2 (mask is a hex bitmask)
echo 2 | sudo tee /proc/irq/41/smp_affinity
# Pin IRQ 42 to CPU 3, etc.
echo 4 | sudo tee /proc/irq/42/smp_affinity
```
Notes:
- `smp_affinity` is a hexadecimal bitmask: `1`=CPU0, `2`=CPU1, `4`=CPU2, `c`=CPU2+CPU3, `ff`=first 8 CPUs.
- On systems with irqbalance daemon running, it may override manual pinning — stop/mask it (`systemctl stop irqbalance`) or configure exclusions.
- For NAPI-driven high pps, interrupts coalesce into softirq processing; check `mpstat -P ALL 1` — `%soft` shows which CPUs are doing network work.
- Common strategy for an edge node: dedicate some cores to network softirq/XDP and others to userspace application threads, so a flood saturating softirq does not starve the proxy logic.
## 4. Ring buffer size
The RX ring is on-NIC memory where received descriptors wait for the driver. Too small → drops under bursts; too large → latency and memory waste.
```bash
# View current and maximum ring sizes
ethtool -g eth0
# Example output:
# Ring parameters for eth0:
# Pre-set maximums:
# RX: 4096
# TX: 4096
# Current hardware settings:
# RX: 256 <-- small, drops under burst
# TX: 256
# Set RX ring to max
sudo ethtool -G eth0 rx 4096
```
Verify drops directly:
```bash
ip -s link show eth0 # look at "dropped" counter
ethtool -S eth0 | grep -i drop # per-hardware-counter view
netstat -i # alternative overview
```
## 5. Offloads: GRO / TSO / LRO / checksumming
Offloads let the NIC or driver merge/split segments and compute checksums, cutting CPU per byte for bulk traffic:
- **GRO** (Generic Receive Offload): merges received segments into large skbs in software.
- **LRO** (Large Receive Offload): same in hardware — breaks forwarding/routing because merged skbs lose MAC/IP details; never enable on routers/proxies.
- **TSO** (TCP Segmentation Offload): NIC splits large outgoing buffers into MSS-sized segments.
- **RX/TX checksum offload**: NIC validates/computes checksums.
```bash
# View all offloads
ethtool -k eth0
# Toggle examples
sudo ethtool -K eth0 gro on
sudo ethtool -K eth0 tso on
sudo ethtool -K eth0 lro off # usually already off; keep it off on proxies
```
**When to turn GRO/TSO OFF:** when running XDP programs that must inspect individual packets, aggressive GRO merging changes what your eBPF sees (merged super-packets on the generic XDP path). Also consider disabling during packet-rate benchmarking, since merging hides the true pps cost. Keep them ON for normal bulk throughput workloads — they save substantial CPU.
## 6. Verifying load distribution
```bash
# Per-CPU softirq utilization, refresh every second
mpstat -P ALL 1
# Interrupt counters per CPU (watch eth0 lines move)
watch -n1 'grep eth0 /proc/interrupts'
# Softirq backlog / drops per CPU
cat /proc/net/softnet_stat
# columns: processed | dropped | time_squeeze ...
# Kernel-side packet drops summary
ip -s link
dropwatch -l 1 # if installed: live trace of where kernel drops packets
```
Healthy picture on a tuned edge: `%soft` spread over several cores rather than 100% on one; `dropped` in `/proc/net/softnet_stat` stays near zero except during deliberate overload tests; NIC-level `dropped` grows only when the attack exceeds what early filtering absorbs.
## 7. Persistence
All `ethtool` and sysfs settings are volatile. Persist them via a systemd unit, a network dispatcher script (`/etc/network/if-up.d/`), NetworkManager dispatcher, or netplan `set-link` options depending on your distro.
---
---
## Русский
# Тюнинг NIC: очереди, привязка IRQ, кольцевые буферы, offload'ы
> Knowledge Base · Практика
>
> Сетевая карта — первый потребитель CPU на пути пакета. Одноочередевой NIC, кормящий одно ядро, ограничивает всю вашу защиту тем, что успеет это ядро; неверно настроенные offload'ы молча портят или замедляют трафик. Статья охватывает настройку multiqueue, масштабирование приёма, распределение прерываний, кольцевые буферы и переключатели offload — с командами для проверки каждого шага.
## 1. Multiqueue NIC (ethtool -L)
Современные NIC предоставляют несколько аппаратных очередей RX/TX, у каждой своё прерывание. Ядро распределяет пакеты по очередям через хеш полей заголовков (RSS). Если активен только 1 combined-канал, все пакеты — включая миллионы пакетов флуда — попадают на одно ядро.
```bash
# Показать текущее число очередей и максимум
ethtool -l eth0
# Пример вывода:
# Channel parameters for eth0:
# Pre-set maximums:
# RX: 8
# TX: 8
# Other: 1
# Combined: 8
# Current hardware settings:
# RX: 1
# TX: 1
# Other: 1
# Combined: 1 <-- всё валится в одну очередь!
# Поднять combined-каналы (общие RX+TX) до максимума
sudo ethtool -L eth0 combined 8
# Или задать RX/TX по отдельности, если карта их различает
sudo ethtool -L eth0 rx 4 tx 4
```
На виртуальных машинах (virtio) число очередей ограничено параметром `queues` в конфигурации ВМ и числом vCPU; согласуйте количество очередей с числом vCPU.
## 2. RSS / RPS / XPS
Три механизма с путающими названиями:
| Механизм | Уровень | Что делает | Настраивается через |
|---|---|---|---|
| **RSS** (Receive Side Scaling) | Железо | NIC хеширует заголовки пакета → выбирает RX-очередь → поднимает её IRQ | `ethtool -x eth0` (просмотр), `ethtool -X eth0` (таблица indirection) |
| **RPS** (Receive Packet Steering) | Софт ядра | Та же идея, что RSS, но программно после приёма драйвером — для карт/очередей без RSS или для дальнейшего распределения | sysfs на каждую RX-очередь |
| **XPS** (Transmit Packet Steering) | Софт ядра | Выбирает TX-очередь, соответствующую передающему CPU, сохраняя локальность потока | sysfs на каждую TX-очередь |
```bash
# Посмотреть таблицу indirection и хеш-ключ RSS
ethtool -x eth0
# Равномерно раскидать очереди по CPU (пример для 4 очередей)
sudo ethtool -X eth0 equal 4
# Включить RPS на первой RX-очереди: направить на CPU 2-3 (маска 0xc = биты 2,3)
echo c | sudo tee /sys/class/net/eth0/queues/rx-0/rps_cpus
# Проверить, реально ли RPS распределяет (счётчики по CPU)
cat /proc/net/softnet_stat # колонка 2 = дропы в softirq
# XPS: направить tx-0 на CPU 0 (маска 0x1)
echo 1 | sudo tee /sys/class/net/eth0/queues/tx-0/xps_cpus
```
Правило большого пальца: предпочитайте RSS, если железо умеет; добавляйте RPS там, где аппаратных очередей меньше числа ядер; XPS важен прежде всего для высокопроизводительных *исходящих* путей.
## 3. Привязка IRQ (IRQ affinity)
Каждая RX-очередь генерирует прерывания. По умолчанию ядро может направить их все на CPU0 — создавая горячую точку ровно там, где будет работать и ваша XDP-программа.
```bash
# Как сейчас распределены прерывания
grep eth0 /proc/interrupts
# Номера IRQ очередей NIC
grep -E 'eth0.*TxRx' /proc/interrupts | awk '{print $1}' | tr -d ':'
# Привязать IRQ 41 к CPU 2 (маска — hex-битовая маска)
echo 2 | sudo tee /proc/irq/41/smp_affinity
# Привязать IRQ 42 к CPU 3 и т.д.
echo 4 | sudo tee /proc/irq/42/smp_affinity
```
Замечания:
- `smp_affinity` — шестнадцатеричная битовая маска: `1`=CPU0, `2`=CPU1, `4`=CPU2, `c`=CPU2+CPU3, `ff`=первые 8 CPU.
- Если работает демон irqbalance, он может перезаписать ручную привязку — остановите/замаскируйте его (`systemctl stop irqbalance`) или настройте исключения.
- При высоких pps обработка через NAPI сворачивает прерывания в обработку softirq; проверяйте `mpstat -P ALL 1` — колонка `%soft` показывает, какие CPU занимаются сетью.
- Частая стратегия для edge-ноды: выделить часть ядер под сетевой softirq/XDP, остальные — под потоки userspace-приложения, чтобы флуд, забивающий softirq, не душил логику прокси.
## 4. Размер кольцевого буфера
RX-ring — память на NIC, где принятые дескрипторы ждут драйвера. Слишком мал → дропы при всплесках; слишком велик → задержки и лишний расход памяти.
```bash
# Текущие и максимальные размеры ring
ethtool -g eth0
# Пример вывода:
# Ring parameters for eth0:
# Pre-set maximums:
# RX: 4096
# TX: 4096
# Current hardware settings:
# RX: 256 <-- мало, дропы при всплесках
# TX: 256
# Поставить RX ring на максимум
sudo ethtool -G eth0 rx 4096
```
Дропы проверяются напрямую:
```bash
ip -s link show eth0 # смотреть счётчик "dropped"
ethtool -S eth0 | grep -i drop # вид по аппаратным счётчикам
netstat -i # альтернативный обзор
```
## 5. Offload'ы: GRO / TSO / LRO / контрольные суммы
Offload'ы позволяют NIC или драйверу склеивать/делить сегменты и считать чексуммы, снижая расход CPU на байт для объёмного трафика:
- **GRO** (Generic Receive Offload): склеивает принятые сегменты в крупные skb программно.
- **LRO** (Large Receive Offload): то же аппаратно — ломает маршрутизацию/форвардинг, потому что склеенные skb теряют детали MAC/IP; никогда не включать на роутерах/прокси.
- **TSO** (TCP Segmentation Offload): NIC сам делит большие исходящие буферы на сегменты размера MSS.
- **RX/TX checksum offload**: NIC проверяет/вычисляет чексуммы.
```bash
# Посмотреть все offload'ы
ethtool -k eth0
# Примеры переключения
sudo ethtool -K eth0 gro on
sudo ethtool -K eth0 tso on
sudo ethtool -K eth0 lro off # обычно уже выключен; на прокси держать выключенным
```
**Когда выключать GRO/TSO:** когда запущены XDP-программы, обязанные видеть отдельные пакеты, агрессивное склеивание GRO меняет то, что видит ваш eBPF (склеенные супер-пакеты на generic-пути XDP). Также подумайте об отключении при бенчмарках частоты пакетов: склейка прячет настоящую стоимость pps. Для обычных объёмных нагрузок держите их включёнными — они заметно экономят CPU.
## 6. Проверка распределения нагрузки
```bash
# Загрузка softirq по CPU, обновление раз в секунду
mpstat -P ALL 1
# Счётчики прерываний по CPU (следим, как двигаются строки eth0)
watch -n1 'grep eth0 /proc/interrupts'
# Очередь softirq / дропы по CPU
cat /proc/net/softnet_stat
# Сводка дропов на стороне ядра
ip -s link
dropwatch -l 1 # если установлен: live-трейс мест дропа в ядре
```
Здоровая картина на настроенном edge: `%soft` распределён по нескольким ядрам, а не 100% на одном; `dropped` в `/proc/net/softnet_stat` держится около нуля вне специальных тестов перегрузки; аппаратный `dropped` растёт только когда атака превышает то, что поглощает раннее отсечение.
## 7. Сохранение настроек
Все настройки `ethtool` и sysfs летучи. Сохраняйте их через systemd unit, скрипт сетевого dispatcher'а (`/etc/network/if-up.d/`), dispatcher NetworkManager или опции `set-link` в netplan — в зависимости от дистрибутива.

View file

@ -0,0 +1,354 @@
# Stress Testing Your Own Infrastructure
> Knowledge Base · Practice
>
> A defense system that has never been attacked is a hypothesis, not a defense. This article covers the standard load-generation tools (hping3, iperf3, wrk/k6), a safe testing methodology, and the metrics that actually tell you whether your protection works. It ends with a real case study from this project.
## English
## 0. Ethics and legality — read first
- **Test only infrastructure you own or have explicit written permission to test.** Launching flood traffic against third-party systems is a crime in most jurisdictions (unauthorized impairment of computer systems), regardless of intent or duration.
- **Never test from cloud providers against targets outside their network without authorization** — most providers prohibit it in ToS and will terminate your account.
- **Use staging environments and loopback/bridge networks.** Everything in this article can be done on a single machine or a private two-node lab.
- Coordinate timing with your team: an unplanned "test" against production is indistinguishable from an outage caused by an attack.
- If you want to study attack traffic safely, generate it yourself against your own stub services — which is exactly what the tools below do.
## 1. The tools and what each one measures
| Tool | Attack/load type | Layer | What you learn |
|---|---|---|---|
| **hping3** | SYN flood, UDP flood, ICMP flood, flag-abuse | L3/L4 | How kernel + early filters behave under packet floods |
| **iperf3** | Bulk TCP/UDP throughput | L4 | Bandwidth ceiling of NIC/tunnel/kernel path |
| **wrk** | HTTP keep-alive requests | L7 | Request rate and latency of an HTTP endpoint under sustained load |
| **k6** | Scripted HTTP/API scenarios (JS) | L7 | Realistic user flows, thresholds, gradual ramp-up |
| **tcpkali** | Raw TCP connections/sec | L4/L7 | Connection-rate limits of a custom TCP service |
## 2. hping3 — L3/L4 floods
```bash
# Install
sudo apt install hping3
# SYN flood at full speed toward YOUR OWN server
# (run from a second machine or VM, not the target itself)
sudo hping3 -S --flood -p 443 TARGET_IP
# SYN flood with randomized source addresses — exercises
# anti-spoofing, syncookies, per-prefix throttling
sudo hping3 -S --flood -p 443 --rand-source TARGET_IP
# UDP flood (bandwidth-style)
sudo hping3 --udp --flood -p 53 TARGET_IP
# Invalid flag combination (SYN+FIN) — should be dropped by any sane filter
sudo hping3 -S -F -p 443 TARGET_IP
# Slower, controlled rate instead of --flood (packets per second)
sudo hping3 -S -p 443 --interval u1000 TARGET_IP # ~1000 pps
```
Observe the effect while it runs:
```bash
watch -n1 'ss -s' # socket summary: timewait/synrecv counts
watch -n1 'cat /proc/net/netstat | grep -A1 TcpExt:' # SYN cookies sent, etc.
nstat -az | grep -i syn # SynCookiesSent, ListenDrops counters
ip -s link # interface-level drops
```
`netstat -s` / `nstat` output worth knowing:
- `SYNs to LISTEN sockets dropped` — accept/SYN backlog overflowed
- `SYN cookies sent` — syncookie mode engaged (see kernel-tuning.md)
## 3. iperf3 — bandwidth
```bash
# Server side (target machine)
iperf3 -s
# Client side: TCP upload for 30 seconds
iperf3 -c TARGET_IP -t 30
# Parallel streams to saturate path
iperf3 -c TARGET_IP -t 30 -P 8
# UDP at fixed rate (e.g., 500 Mbit) with loss/jitter report
iperf3 -c TARGET_IP -u -b 500M -t 30
# Reverse direction (server sends to client)
iperf3 -c TARGET_IP -t 30 -R
```
What to look for: achieved throughput vs link capacity, UDP `lost/datagrams` ratio (packet loss under load), retransmits on TCP runs (`sender retransmits`). If a "protected" path shows far lower throughput than the bare one, your filter may be over-dropping legitimate bulk traffic.
## 4. wrk / k6 — L7 load
```bash
# wrk: 200 connections, 8 threads, 30 seconds against your own endpoint
wrk -t8 -c200 -d30s http://TARGET_IP/
# With a latency distribution summary
wrk -t8 -c200 -d30s --latency http://TARGET_IP/
# Custom request via Lua script file (POST example): wrk -s script.lua ...
```
```bash
# k6: scripted scenario with ramping virtual users
k6 run --vus 50 --duration 60s script.js
```
Minimal k6 script (`script.js`):
```javascript
import http from 'k6/http';
import { check } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 100 }, // ramp to 100 VUs
{ duration: '60s', target: 100 },
{ duration: '30s', target: 0 }, // ramp down
],
};
export default function () {
const res = http.get('http://TARGET_IP/');
check(res, { 'status is 200': (r) => r.status === 200 });
}
```
L7 generators answer a different question than hping3: not "does the pipe survive" but "does the application logic stay correct and fast when many clients hammer valid-looking requests".
## 5. Methodology
### Environment rules
1. **Staging or isolated lab first.** Two VMs/VDS on the same internal network, or Docker bridge networks — never production.
2. **Load source separate from target.** Generating flood on the same box as the defense skews every number (shared CPU, shared softirq).
3. **Baseline before defense**: measure how the unprotected service dies. Only then can you prove the defense changed anything.
4. **One variable at a time**, minimum 3 runs, take the median; warm up ~10 s before counting.
5. **Record the environment**: kernel version, CPU model/count, RAM, NIC type (virtio vs physical), queue settings from nic-tuning.md.
### Metrics that matter
| Metric | Command / source | Healthy signal |
|---|---|---|
| Packet rate (pps) | `sar -n DEV 1`, `ethtool -S` | Matches generator's intended rate |
| Bitrate | `sar -n DEV 1`, `iftop` | Within link budget |
| CPU softirq % | `mpstat -P ALL 1` (`%soft`) | Spread across cores, not pegged on one |
| Interface drops | `ip -s link` | Grows during attack *only* if that's your drop policy |
| Socket states | `ss -s` | No runaway TIME_WAIT/SYN-RECV accumulation |
| Kernel drop reasons | `nstat -az`, `dropwatch` | Drops attributable to intended filters |
| App-level allowed/blocked | Your service metrics (e.g., Prometheus `/metrics`) | Block ratio rises under attack; legit clients still pass |
| Legit-client latency | Separate probe client measuring RTT | Stays within SLO during the attack |
The last two rows are the crucial ones: a defense that blocks everything including real users has failed.
### Interpreting results
- **Blocked ratio high + legit RTT stable** → defense works.
- **Everything blocked** → filter too aggressive (check thresholds).
- **Nothing blocked, CPU melts** → attack traffic bypasses your layers; check whether it reaches userspace at all (`mpstat`, XDP drop counters).
- **Generator saturates first** → your numbers measure the generator, not the target. Scale out sources or lower rates.
## 6. Case study: edge-only loopback test on a small VDS (2026-08-04)
Full report: `docs/research/load-test-report.md`; scripts: `deploy/test/stress/`.
Setup: single 2 vCPU / 3.8 GB VDS, Ubuntu 22.04, Docker bridge `172.30.0.0/24`. One container ran the Rampart edge process plus a stub echo backend; another container acted as attacker holding 100 additional source IPs (`172.30.0.101–200`) and generating handshake-shaped L7 floods indistinguishable from real clients at the protocol level. XDP was disabled and PoW disabled — deliberately, so the test measured pure L7 rate limiting + reputation.
Phases:
- **Phase A (limits off)**: raw throughput measurement — ~121.5k handshakes in 30 s (~4.0k conn/s), all proxied; edge CPU peaked ~179% (both cores). Without limits, the same flood also throttled legitimate probe clients (2 of 5 succeeded).
- **Phase B (default per-IP limit 5 pps/IP, burst 10, reputation ban)**: identical flood — **119,376 blocked vs 528 allowed ≈ 99.6% blocked** at max ~32% CPU. All 5 legitimate probe clients passed during the attack with RTT 2.2–5.8 ms; abusive IPs got blacklisted after repeated violations.
- **Phase C (hping3 SYN flood, `--rand-source`)**: no impact on the L7 edge — without XDP, raw SYN handling belongs entirely to the kernel (syncookies/backlog). Confirms the layer separation discussed in defense-levels.md.
- **Phase D (300 kept-open connections)**: all proxied, negligible CPU — steady-state connection holding was limited by the backend stub, not the edge.
Lessons transferable to any project:
1. Always run both phases: **defense off** (find raw ceiling) and **defense on** (prove block ratio + legit availability). Phase B alone would hide the fact that unlimited mode harms real clients.
2. Masked L7 floods (valid protocol payloads from many IPs) are the honest adversary; simple SYN tests only validate the kernel layer.
3. A tiny 2-vCPU box handled the attack at 32% CPU once per-IP limiting engaged — good early-layer filtering buys enormous headroom.
4. Keep a legit probe client running throughout every phase; it is your false-positive alarm.
---
---
## Русский
# Стресс-тестирование собственной инфраструктуры
> Knowledge Base · Практика
>
> Система защиты, которую никогда не атаковали, — это гипотеза, а не защита. Статья охватывает стандартные инструменты генерации нагрузки (hping3, iperf3, wrk/k6), безопасную методику тестирования и метрики, которые реально показывают, работает ли защита. В конце — реальный кейс этого проекта.
## 0. Этика и законность — прочтите сначала
- **Тестируйте только инфраструктуру, которой владеете или на тестирование которой есть явное письменное разрешение.** Запуск флуд-трафика в чужие системы — уголовное преступление в большинстве юрисдикций (несанкционированное нарушение работы компьютерных систем), независимо от намерений и длительности.
- **Никогда не тестируйте из облачных провайдеров против целей вне их сети без разрешения** — большинство провайдеров запрещают это в ToS и блокируют аккаунт.
- **Используйте staging и loopback/bridge-сети.** Всё описанное ниже выполняется на одной машине или в приватной лаборатории из двух узлов.
- Согласуйте время с командой: незапланированный «тест» продакшена неотличим от аварии, вызванной атакой.
- Если хотите безопасно изучать атакующий трафик — генерируйте его сами против собственных заглушек; ровно это и делают инструменты ниже.
## 1. Инструменты и что каждый измеряет
| Инструмент | Тип атаки/нагрузки | Уровень | Что вы узнаёте |
|---|---|---|---|
| **hping3** | SYN flood, UDP flood, ICMP flood, злоупотребление флагами | L3/L4 | Как ведут себя ядро и ранние фильтры под пакетным флудом |
| **iperf3** | Объёмная пропускная способность TCP/UDP | L4 | Потолок пропускной способности пути NIC/туннель/ядро |
| **wrk** | HTTP-запросы поверх keep-alive | L7 | Частота запросов и задержки HTTP-эндпоинта под устойчивой нагрузкой |
| **k6** | Скриптовые HTTP/API-сценарии (JS) | L7 | Реалистичные пользовательские сценарии, пороги, плавный разгон |
| **tcpkali** | Сырые TCP-соединения/сек | L4/L7 | Предел частоты соединений кастомного TCP-сервиса |
## 2. hping3 — флуды L3/L4
```bash
# Установка
sudo apt install hping3
# SYN flood на полной скорости в СВОЙ сервер
# (запускать со второй машины или ВМ, не с самой цели)
sudo hping3 -S --flood -p 443 TARGET_IP
# SYN flood со случайными адресами источника — проверяет
# анти-спуфинг, syncookies, троттлинг по префиксам
sudo hping3 -S --flood -p 443 --rand-source TARGET_IP
# UDP flood (полосно-затратный)
sudo hping3 --udp --flood -p 53 TARGET_IP
# Неверная комбинация флагов (SYN+FIN) — любой адекватный фильтр должен дропнуть
sudo hping3 -S -F -p 443 TARGET_IP
# Медленнее, управляемая скорость вместо --flood (пакетов в секунду)
sudo hping3 -S -p 443 --interval u1000 TARGET_IP # ~1000 pps
```
Наблюдайте за эффектом во время запуска:
```bash
watch -n1 'ss -s' # сводка сокетов: счётчики timewait/synrecv
watch -n1 'cat /proc/net/netstat | grep -A1 TcpExt:' # отправленные SYN cookies и пр.
nstat -az | grep -i syn # счётчики SynCookiesSent, ListenDrops
ip -s link # дропы на уровне интерфейса
```
Полезные строки вывода `netstat -s` / `nstat`:
- `SYNs to LISTEN sockets dropped` — переполнение accept/SYN backlog'а
- `SYN cookies sent` — включился режим syncookies (см. kernel-tuning.md)
## 3. iperf3 — полоса пропускания
```bash
# Серверная сторона (целевая машина)
iperf3 -s
# Клиентская сторона: TCP-загрузка 30 секунд
iperf3 -c TARGET_IP -t 30
# Параллельные потоки для насыщения канала
iperf3 -c TARGET_IP -t 30 -P 8
# UDP на фиксированной скорости (например, 500 Мбит) с отчётом о потерях/джиттере
iperf3 -c TARGET_IP -u -b 500M -t 30
# Обратное направление (сервер шлёт клиенту)
iperf3 -c TARGET_IP -t 30 -R
```
На что смотреть: достигнутая полоса против ёмкости линка, отношение `lost/datagrams` по UDP (потери под нагрузкой), ретрансмиты в TCP-прогонах (`sender retransmits`). Если «защищённый» путь показывает заметно меньшую полосу, чем голый, ваш фильтр может перерезать и легитимный объёмный трафик.
## 4. wrk / k6 — нагрузка L7
```bash
# wrk: 200 соединений, 8 потоков, 30 секунд против своего эндпоинта
wrk -t8 -c200 -d30s http://TARGET_IP/
# С разбивкой задержек
wrk -t8 -c200 -d30s --latency http://TARGET_IP/
# Кастомный запрос через Lua-скрипт (пример POST): wrk -s script.lua ...
```
```bash
# k6: скриптовый сценарий с плавным числом виртуальных пользователей
k6 run --vus 50 --duration 60s script.js
```
Минимальный скрипт k6 (`script.js`):
```javascript
import http from 'k6/http';
import { check } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 100 }, // разгон до 100 VU
{ duration: '60s', target: 100 },
{ duration: '30s', target: 0 }, // спуск
],
};
export default function () {
const res = http.get('http://TARGET_IP/');
check(res, { 'status is 200': (r) => r.status === 200 });
}
```
L7-генераторы отвечают на другой вопрос, нежели hping3: не «выживет ли труба», а «остаётся ли логика приложения корректной и быстрой, когда множество клиентов долбят похожими на настоящие запросами».
## 5. Методика
### Правила окружения
1. **Сначала staging или изолированная лаборатория.** Две ВМ/VDS в одной внутренней сети или docker bridge-сети — никогда продакшен.
2. **Источник нагрузки отдельно от цели.** Генерация флуда на той же машине, где защита, искажает все цифры (общий CPU, общий softirq).
3. **Базлайн до защиты**: замерьте, как умирает незащищённый сервис. Только так можно доказать, что защита что-то изменила.
4. **По одной переменной за раз**, минимум 3 прогона, берём медиану; прогрев ~10 с до начала подсчёта.
5. **Фиксируйте окружение**: версия ядра, модель/число CPU, RAM, тип NIC (virtio против физического), настройки очередей из nic-tuning.md.
### Метрики, которые имеют значение
| Метрика | Команда / источник | Здоровый сигнал |
|---|---|---|
| Частота пакетов (pps) | `sar -n DEV 1`, `ethtool -S` | Совпадает с задуманной скоростью генератора |
| Битрейт | `sar -n DEV 1`, `iftop` | В пределах бюджета линка |
| CPU softirq % | `mpstat -P ALL 1` (`%soft`) | Распределено по ядрам, не упирается в одно |
| Дропы интерфейса | `ip -s link` | Растут при атаке *только* если это ваша политика дропа |
| Состояния сокетов | `ss -s` | Нет неконтролируемого накопления TIME_WAIT/SYN-RECV |
| Причины дропов в ядре | `nstat -az`, `dropwatch` | Дропы объясняются задуманными фильтрами |
| Allowed/blocked на уровне приложения | Метрики сервиса (например, Prometheus `/metrics`) | Доля блокировки растёт под атакой; легитимные клиенты проходят |
| Задержка легитимного клиента | Отдельный пробный клиент, меряющий RTT | Остается в рамках SLO во время атаки |
Последние две строки — решающие: защита, блокирующая всех подряд, включая реальных пользователей, провалилась.
### Интерпретация результатов
- **Высокая доля блокировки + стабильный RTT легитимных** → защита работает.
- **Заблокировано всё** → фильтр слишком агрессивен (проверьте пороги).
- **Ничего не заблокировано, CPU плавится** → атакующий трафик обходит ваши слои; проверьте, доходит ли он вообще до userspace (`mpstat`, счётчики XDP-дропов).
- **Генератор насыщается первым** → вы меряете генератор, а не цель. Масштабируйте источники или снизьте скорость.
## 6. Кейс: edge-only тест на loopback небольшой VDS (2026-08-04)
Полный отчёт: `docs/research/load-test-report.md`; скрипты: `deploy/test/stress/`.
Окружение: одна VDS 2 vCPU / 3.8 ГБ, Ubuntu 22.04, docker bridge `172.30.0.0/24`. Один контейнер запускал процесс Rampart edge плюс stub echo-бэкенд; второй контейнер выступал атакующим с сотней дополнительных IP-источников (`172.30.0.101–200`) и генерировал маскированные под протокол L7-флуды, неотличимые от реальных клиентов на уровне протокола. XDP был выключен и PoW выключен — сознательно, чтобы тест измерял чистый L7: rate limiting + репутацию.
Фазы:
- **Фаза A (лимиты сняты)**: измерение сырой пропускной способности — ~121,5 тыс. хендшейков за 30 с (~4.0 тыс. conn/s), всё проксировано; пик CPU edge ~179% (оба ядра). Без лимитов тот же флуд «душит» и легитимных пробных клиентов (успешны 2 из 5).
- **Фаза B (дефолтный лимит 5 pps/IP, burst 10, репутационный бан)**: идентичный флуд — **119 376 заблокировано против 528 пропущенных ≈ 99,6% blocked** при пике CPU ~32%. Все 5 легитимных пробных клиентов прошли во время атаки с RTT 2.2–5.8 мс; злоупотребляющие IP уходили в блэклист после повторных нарушений.
- **Фаза C (SYN flood через hping3, `--rand-source`)**: влияния на L7 edge нет — без XDP сырая обработка SYN целиком принадлежит ядру (syncookies/backlog). Подтверждает разделение слоёв из defense-levels.md.
- **Фаза D (300 удерживаемых соединений)**: все проксированы, CPU незначителен — удержание соединений в установившемся режиме ограничивал stub-бэкенд, а не edge.
Выводы, переносимые на любой проект:
1. Всегда гоняйте обе фазы: **защита выключена** (находим сырой потолок) и **защита включена** (доказываем долю блокировки и доступность легитимных клиентов). Одна лишь фаза B скрыла бы тот факт, что режим без лимитов вредит реальным клиентам.
2. Маскированные L7-флуды (валидные полезные нагрузки протокола с множества IP) — честный противник; простые SYN-тесты проверяют только слой ядра.
3. Маленькая VDS на 2 vCPU отбивала атаку при 32% CPU, как только включился per-IP лимит — хорошее раннее отсечение покупает огромный запас прочности.
4. Пробный легитимный клиент должен работать на протяжении каждой фазы — это ваш детектор ложных срабатываний.