# 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,
worker stays occupied
FILTERING POINT: timeout must be
shorter than the dribble interval
end
Note over W: pool exhausted:
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.
Cannot tell slow attack
from slow human" --> P[Bytes reach userspace]
P --> T{Userspace engine:
header read timeout,
conn-per-IP limit,
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-таймер сброшен,
воркер занят
ТОЧКА ФИЛЬТРАЦИИ: таймаут должен быть короче
интервала «капельницы»
end
Note over W: пул исчерпан:
все воркеры ждут мёртвые запросы
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.
Не отличит медленную атаку
от медленного человека" --> P[Байты доходят до userspace]
P --> T{Userspace-движок:
header read timeout,
лимит conn-per-IP,
отслеживание состояния запроса}
T -- "неполон за дедлайном" --> D[Дроп + штраф репутации]
T -- "успел целиком" --> APP[Прикладная логика]
style D fill:#f5d0d0
```
Ядерные слои (XDP, nftables) оперируют пакетами и флагами; медленная атака производит идеально валидные пакеты. Различающая информация — «как долго этот запрос остаётся незавершённым и сколько их у этого IP» — живёт в прикладном состоянии сессии. Поэтому: дешёвые ядерные слои продолжают держать объёмный щит, но **медленные атаки реально решаются в userspace-движке** — именно поэтому Rampart размещает там логику таймаутов и репутации, а не пытается затолкать всё в eBPF.