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
290
docs/kb/practice/kernel-tuning.md
Normal file
290
docs/kb/practice/kernel-tuning.md
Normal 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).
|
||||
333
docs/kb/practice/nic-tuning.md
Normal file
333
docs/kb/practice/nic-tuning.md
Normal 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 — в зависимости от дистрибутива.
|
||||
354
docs/kb/practice/stress-testing.md
Normal file
354
docs/kb/practice/stress-testing.md
Normal 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. Пробный легитимный клиент должен работать на протяжении каждой фазы — это ваш детектор ложных срабатываний.
|
||||
Loading…
Add table
Add a link
Reference in a new issue