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:
parent
0b53ed720b
commit
15f474486a
179 changed files with 5044 additions and 11519 deletions
233
docs/kb/attacks/http-flood.md
Normal file
233
docs/kb/attacks/http-flood.md
Normal 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 |
|
||||
197
docs/kb/attacks/slowloris.md
Normal file
197
docs/kb/attacks/slowloris.md
Normal 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.
|
||||
237
docs/kb/attacks/syn-flood.md
Normal file
237
docs/kb/attacks/syn-flood.md
Normal 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 до запуска любой прикладной логики.
|
||||
177
docs/kb/attacks/udp-amplification.md
Normal file
177
docs/kb/attacks/udp-amplification.md
Normal 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-портами |
|
||||
Loading…
Add table
Add a link
Reference in a new issue