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

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

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

View file

@ -1,8 +1,10 @@
# API Reference - Rampart Manager
> REST API для управления Rampart.
> Base URL: `https://manager.rampart.internal/api/v1`
> Авторизация: Bearer JWT (получить через `/api/v1/auth/login`)
> REST API управления. Base URL: `http://MANAGER:8080`
> Авторизация: Bearer JWT (получить через `/api/v1/auth/login`).
Реализованные маршруты — см. src/bin/rampart-manager.rs. Это полный список:
ничего сверх перечисленного здесь API не предоставляет.
---
@ -10,408 +12,110 @@
### `POST /api/v1/auth/login`
Получение JWT токена.
```json
// Request
{
"password": "changeme"
}
{"password": "значение API_PASSWORD"}
// Response 200
{
"token": "eyJhbGciOiJIUzI1NiIs..."
}
{"token": "eyJhbGciOiJIUzI1NiIs..."}
// 401 → неверный пароль; 429 → больше 5 попыток за минуту с одного IP
```
Все последующие запросы:
Все защищённые запросы:
```
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Authorization: Bearer <token>
```
Токен живёт `JWT_EXPIRATION_SECS` секунд (дефолт 86400).
---
## Nodes
## Health (публичный)
### `GET /api/v1/nodes`
Список всех зарегистрированных нод.
### `GET /api/v1/health`
```json
// Response 200
{
"nodes": [
{
"id": "edge-eu-1",
"role": "edge",
"ip": "10.0.100.1",
"public_ip": "45.200.10.1",
"status": "online",
"version": "0.4.0",
"uptime_secs": 86400,
"metrics": {
"connections_per_sec": 1200,
"active_connections": 45000,
"cpu_percent": 45.2,
"memory_mb": 512
},
"last_heartbeat": "2026-07-19T10:30:00Z"
}
]
}
```
### `GET /api/v1/nodes/{id}`
Детальная информация о ноде.
### `POST /api/v1/nodes`
Регистрация новой ноды (или через авто-discovery).
```json
// Request
{
"name": "edge-us-2",
"role": "edge",
"public_ip": "45.200.20.5",
"wg_public_key": "<BASE64_KEY>"
}
// Response 201
{
"id": "edge-us-2",
"wg_config": "https://manager/api/v1/nodes/edge-us-2/wg-config",
"tls_cert": "https://manager/api/v1/nodes/edge-us-2/cert"
}
```
### `POST /api/v1/nodes/{id}/drain`
Вывести ноду из ротации (graceful shutdown).
```json
// Response 200
{
"status": "draining",
"active_connections_before": 45000,
"estimated_seconds": 30
}
```
---
## Blacklist
### `GET /api/v1/blacklist`
Список забаненных IP/ASN.
| Параметр | Тип | По умолчанию | Описание |
|----------|-----|-------------|----------|
| `page` | int | 1 | Пагинация |
| `per_page` | int | 100 | Элементов на странице |
| `reason` | string | - | Фильтр по причине |
| `search` | string | - | Поиск по IP/ASN |
```json
// Response 200
{
"items": [
{
"target": "1.2.3.4",
"type": "ip", // ip | asn | cidr
"reason": "rate_limit",
"created_by": "admin",
"created_at": "2026-07-19T10:00:00Z",
"expires_at": "2026-07-20T10:00:00Z",
"hits": 1500
}
],
"total": 42,
"page": 1,
"per_page": 100
}
```
### `POST /api/v1/blacklist`
Добавить IP/ASN/CIDR в блэклист.
```json
// Request
{
"target": "1.2.3.4",
"type": "ip", // ip | asn | cidr
"reason": "manual_ban",
"duration_secs": 3600 // null = навсегда
}
// Response 201
{
"status": "added",
"target": "1.2.3.4",
"propagated_to_nodes": 2,
"expires_at": "2026-07-19T11:00:00Z"
}
```
### `DELETE /api/v1/blacklist/{id}`
Удалить запись из блэклиста.
```json
// Response 200
{
"status": "removed",
"target": "1.2.3.4"
}
{"status": "healthy", "version": "0.3.0-dev"}
```
---
## Servers
### `GET /api/v1/servers`
### `GET /api/v1/servers` 🔒
Список зарегистрированных game серверов.
Список серверов из Redis-реестра (`rampart:servers:*`).
```json
// Response 200
{
"servers": [
{
"name": "survival-01",
"type": "survival",
"ip": "10.0.2.1",
"port": 25565,
"status": "online",
"proxy": "velocity-01",
"online": 42,
"max_players": 100,
"tps": 19.8,
"mspt": 25.3,
"ram_used_mb": 2048,
"ram_max_mb": 8192,
"last_heartbeat": "2026-07-19T10:30:00Z"
}
{"name": "app-01", "type": "tcp", "ip": "10.0.2.1", "port": 25566, "status": "online"}
]
}
```
### `GET /api/v1/servers/{name}`
Детальная информация о сервере.
### `DELETE /api/v1/servers/{name}`
Принудительно удалить сервер из registry.
Redis недоступен → пустой список.
---
## Challenges (v0.5+)
## Blacklist
### `GET /api/v1/challenges/status`
Статус challenge системы.
### `GET /api/v1/blacklist` 🔒
```json
// Response 200
{
"enabled": true,
"mode": "auto",
"active_challenges": 15,
"passed_last_hour": 1200,
"failed_last_hour": 45,
"current_type": "timing"
}
```
### `POST /api/v1/challenges/rotate`
Принудительно сменить тип challenge.
```json
// Request
{
"type": "map_captcha" // timing | map_captcha | behavioral | contextual
}
// Response 200
{
"status": "rotated",
"previous_type": "timing",
"new_type": "map_captcha",
"rotated_at": "2026-07-19T10:30:00Z"
}
```
---
## Metrics
### `GET /api/v1/metrics/summary`
Сводка метрик за период.
| Параметр | Тип | По умолчанию | Описание |
|----------|-----|-------------|----------|
| `since` | ISO8601 | -24h | Начало периода |
| `until` | ISO8601 | now | Конец периода |
```json
// Response 200
{
"total_connections": 5200000,
"blocked": 45000,
"allowed": 5155000,
"active_connections": 85000,
"top_attackers": [
{"ip": "5.5.5.5", "hits": 12000, "country": "NL"},
{"ip": "6.6.6.6", "hits": 8000, "country": "RU"}
"items": [
{"target": "1.2.3.4", "type": "ip", "reason": "manual_ban",
"created_at": "...", "expires_at": null}
],
"top_countries": [
{"country": "US", "connections": 2000000},
{"country": "DE", "connections": 1000000}
"total": 42
}
```
Источник: Redis set `rampart:blacklist`. Пагинации и фильтров нет.
### `POST /api/v1/blacklist` 🔒
```json
// Request
{"target": "1.2.3.4", "type": "ip", "reason": "manual_ban", "duration_secs": 3600}
// Response 200/201 — запись добавлена в Redis set
```
Edge-ноды подхватывают ban при следующей синхронизации блэклиста.
---
## Nodes
### `GET /api/v1/nodes` 🔒
Список зарегистрированных edge-нод из Redis (`rampart:nodes:*`, heartbeat).
```json
{
"nodes": [
{"id": "edge-eu-1", "role": "edge", "ip": "10.0.100.1",
"status": "online", "last_heartbeat": "..."}
]
}
```
---
## Health
## Не реализовано
### `GET /api/v1/health`
Честно, на текущий момент в API **нет**:
```json
// Response 200
{
"status": "healthy",
"version": "0.4.0",
"uptime_secs": 604800,
"components": {
"redis": "healthy",
"nats": "healthy",
"clickhouse": "healthy",
"edge_nodes": {"online": 2, "offline": 0},
"velocity_nodes": {"online": 3, "offline": 1},
"game_servers": {"online": 45, "offline": 2}
}
}
```
- удаления записей blacklist (только add/list);
- drain нод через API (есть только CLI `rampart-cli drain`);
- webhooks, metrics summary, challenge management;
- регистрации нод через `POST /api/v1/nodes`;
- rate limiting на сами запросы API (кроме login).
---
## Webhooks
### `POST /api/v1/webhooks`
Настройка webhook для событий.
```json
// Request
{
"url": "https://discord.com/api/webhooks/...",
"events": ["blacklist.added", "node.down", "attack.detected"],
"secret": "optional_hmac_secret"
}
// Response 201
{
"id": "wh_abc123",
"status": "active"
}
```
### Payload пример (blacklist.added)
```json
{
"event": "blacklist.added",
"timestamp": "2026-07-19T10:30:00Z",
"data": {
"target": "1.2.3.4",
"reason": "rate_limit",
"added_by": "auto"
}
}
```
---
## OpenAPI Spec
Полная OpenAPI 3.0 спецификация: `docs/api/openapi.yaml`
```yaml
openapi: "3.0.3"
info:
title: Rampart Manager API
version: "0.4.0"
servers:
- url: https://manager.rampart.internal/api/v1
paths:
/nodes:
get:
summary: List all nodes
security:
- bearerAuth: []
responses:
'200':
description: Node list
/blacklist:
post:
summary: Add to blacklist
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
target:
type: string
type:
type: string
enum: [ip, asn, cidr]
reason:
type: string
duration_secs:
type: integer
responses:
'201':
description: Added
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
```
---
## Rate Limiting
API имеет rate limiting: **60 запросов в минуту** на один JWT токен.
```json
// Response 429
{
"error": "rate_limit_exceeded",
"retry_after_secs": 30
}
```
Headers:
```
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1626688800
```
---
*Версия: 1.0 | Июль 2026*
*Версия: 2.0 | Август 2026*

View file

@ -1,30 +1,9 @@
# Configuration - Rampart
> Примеры конфигурационных файлов для всех компонентов.
> Единый конфиг edge-ноды (`config.toml`) и переменные окружения Manager.
> Схема конфига: `src/config/sections.rs` — единственный источник правды.
## Как конфиги связывают компоненты
```
Edge config.toml Manager (env)
bind.address JWT_SECRET
bind.port API_PASSWORD
backend.address ───→ REDIS_URL
hmac.secret ←──────┐ CLICKHOUSE_URL
store.redis_url ────┤
│
Velocity (env) │ Paper (env)
RAMPART_HMAC_SECRET┤ RAMPART_HMAC_SECRET
RAMPART_ALLOWED_ │ RAMPART_REDIS_URL
DOMAINS │ RAMPART_SERVER_NAME
RAMPART_REDIS_URL ─┘ RAMPART_SERVER_IP
```
HMAC secret должен быть ОДИНАКОВЫМ на Edge, Velocity и Paper.
Redis URL - одинаковым на всех компонентах.
---
## 1. Edge Node (`config.toml`)
## 1. Edge (`config.toml`, путь задаётся `RAMPART_CONFIG`)
```toml
[bind]
@ -32,249 +11,80 @@ address = "0.0.0.0"
port = 25565
[backend]
# Velocity нода или HAProxy
address = "10.0.0.2"
port = 25565
[hmac]
# Минимум 32 байта. Сгенерировать: openssl rand -hex 32
secret = "CHANGE_ME_32_BYTES_LONG_HERE_ABCDEF123456"
# Список TCP-бэкендов (addr:port). Обязателен непустой, адреса валидируются.
upstreams = ["127.0.0.1:25566"]
[workers]
# Количество воркеров = количество vCPU
count = 4
[xdp]
# Опционально, требует kernel 5.10+
enabled = false
interface = "eth0"
[pow]
# PoW Challenge (Layer 2). ВЫКЛЮЧЕН по умолчанию (P0-4):
# текстовый challenge отправляется до handshake и несовместим с ванильными
# MC-клиентами — они не умеют его решать, и при enabled=true никто не сможет
# зайти на сервер. Включать только после появления клиентского мода или PoW,
# совместимого с протоколом Minecraft.
enabled = false
difficulty = 4
[limits]
# Максимум времени на получение handshake (Slowloris защита)
handshake_timeout_secs = 5
# Максимум одновременных соединений с одного IP
max_connections_per_ip = 10
# Лимит коннектов в секунду с одного IP (Status ping)
rate_limit_status_pps = 2
# Лимит коннектов в секунду с одного IP (Login)
rate_limit_login_pps = 5
# Лимит burst
rate_limit_burst = 10
handshake_timeout_secs = 5 # таймаут чтения (slowloris-защита)
max_connections_per_ip = 10 # одновременные соединения с одного IP
rate_limit_pps = 5.0 # лимит коннектов/сек с одного IP
rate_limit_burst = 10.0 # burst
[ban]
ban_duration_secs = 3600
[store]
# v0.2+: Redis для синхронизации блэклиста
redis_url = "redis://:password@10.0.0.1:6379/0"
# TTL кэша блэклиста (локально)
redis_url = "" # пусто = синхронизация блэклиста выключена
blacklist_cache_ttl_secs = 300
clickhouse_url = "" # пусто = запись событий выключена
[xdp]
enabled = false # требует сборки --features xdp, kernel 5.10+
interface = "eth0"
[logging]
level = "info" # trace, debug, info, warn, error
format = "json" # json или text
level = "info" # trace, debug, info, warn, error
format = "text" # text или json
[metrics]
enabled = true
port = 9090
[quic]
# v0.4+: QUIC канал к Manager (опционально)
enabled = false
connect = "10.0.0.1:7777"
[pow]
enabled = false # PoW challenge, OFF по умолчанию
difficulty = 4
whitelist = ["127.0.0.1", "::1"] # только IP-адреса, валидируются при парсинге
```
---
Все секции опциональны — при отсутствии используются дефолты из таблицы выше.
## 2. Velocity Plugin
## 2. Manager (переменные окружения)
Плагин конфигурируется через переменные окружения (совпадают с edge node).
| Переменная | Обязательна | По умолчанию | Описание |
|------------|-------------|--------------|----------|
| `REDIS_URL` | нет | `redis://127.0.0.1:6379/0` | Хранилище состояния |
| `JWT_SECRET` | да | — | Минимум 32 байта |
| `JWT_AUDIENCE` | нет | `rampart` | Audience JWT |
| `JWT_EXPIRATION_SECS` | нет | `86400` | Время жизни токена |
| `API_PASSWORD` | да | — | Пароль для `/api/v1/auth/login`; значение `changeme` запрещено |
| `CORS_ORIGIN` | нет | `http://localhost:5173` | CORS; `*` или пусто — разрешить все |
Manager всегда слушает `0.0.0.0:8080`.
## 3. Feature flags сборки (Cargo.toml)
| Feature | По умолчанию | Что включает |
|---------|--------------|--------------|
| `store-redis` | ✅ | Redis-клиент и синхронизация блэклиста |
| `geoip` | ❌ | GeoIP lookup (maxminddb) |
| `xdp` | ❌ | Загрузка XDP-программы через libbpf-rs |
| `io-uring` | ❌ | splice/io_uring I/O |
| `protocol-http` | ❌ | Резерв под HTTP-плагин (пока пустой) |
Пример: `cargo build --release --features xdp --bin rampart`.
## 4. BPF/XDP программа
Исходник: `xdp/core/universal_filter.c`. Smoke-check компиляции:
```bash
# Обязательно: HMAC секрет (должен совпадать с edge нодой)
RAMPART_HMAC_SECRET="CHANGE_ME_32_BYTES_LONG_HERE_ABCDEF123456"
# Опционально: список разрешённых доменов (через запятую)
RAMPART_ALLOWED_DOMAINS="play.example.com,mc.example.com,example.com"
```
Установка:
```
# Сборка
cd plugins && ./gradlew :velocity:build
# Копирование в Velocity
cp velocity/build/libs/rampart-velocity-*.jar /opt/velocity/plugins/
# Рестарт
systemctl restart velocity
```
Плагин делает:
- **DomainCheck**: блокирует direct IP-коннекты, пропускает только домены из whitelist
- **HmacCheck**: верифицирует HMAC-SHA256 подпись в hostname (`\0shield\0<sig>`)
---
## 3. Paper Plugin
Конфигурация - через переменные окружения:
```bash
# Обязательно: HMAC секрет (должен совпадать с edge нодой)
RAMPART_HMAC_SECRET="CHANGE_ME_32_BYTES_LONG_HERE_ABCDEF123456"
```
Установка:
```
cd plugins && ./gradlew :paper:build
cp paper/build/libs/rampart-paper-*.jar /opt/paper/plugins/
```
Плагин делает:
- **HmacCheck**: резервная верификация HMAC-подписи на случай прямого коннекта (в обход Velocity)
---
## 4. Manager (`manager.toml`)
```toml
[bind]
address = "0.0.0.0"
port = 8080
[tls]
cert = "/etc/rampart/tls/manager.crt"
key = "/etc/rampart/tls/manager.key"
ca = "/etc/rampart/tls/ca.crt"
[auth]
jwt_secret = "CHANGE_ME_JWT_SECRET_HERE"
jwt_expiry_hours = 24
[redis]
url = "redis://:password@127.0.0.1:6379/0"
pool_size = 10
[nats]
# v0.4+: NATS для критических событий
urls = ["nats://127.0.0.1:4222"]
[clickhouse]
url = "http://127.0.0.1:8123"
db = "rampart"
batch_size = 1000
flush_interval_secs = 1
[quic]
# v0.4+: QUIC сервер для edge нод
bind = "0.0.0.0:7777"
[limits]
api_rate_per_minute = 60
clang -O2 -g -target bpf -c xdp/core/universal_filter.c -o /tmp/universal_filter.o
```
---
## 5. HAProxy (`haproxy.cfg`)
```haproxy
global
maxconn 100000
log /dev/log local0
defaults
mode tcp
timeout connect 3s
timeout client 30s
timeout server 30s
option tcplog
frontend minecraft_in
bind *:25565
mode tcp
# Только от edge нод
acl is_edge src 10.0.100.0/24
tcp-request connection reject if !is_edge
default_backend velocity_pool
backend velocity_pool
mode tcp
balance leastconn
option tcp-check
server vel1 10.0.0.2:25565 check inter 3s rise 2 fall 3
server vel2 10.0.0.3:25565 check inter 3s rise 2 fall 3
server vel3 10.0.0.4:25565 check inter 3s rise 2 fall 3
```
---
## 6. Prometheus (`prometheus.yml`)
```yaml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'rampart-edge'
static_configs:
- targets:
- '10.0.100.1:9090'
- '10.0.100.2:9090'
- job_name: 'rampart-manager'
static_configs:
- targets: ['10.0.0.1:9090']
- job_name: 'rampart-velocity'
static_configs:
- targets:
- '10.0.0.2:9091'
- '10.0.0.3:9091'
- job_name: 'paper-servers'
file_sd_configs:
- files: ['/etc/prometheus/game_servers.json']
refresh_interval: 30s
```
---
## 7. WireGuard (`wg0.conf`)
```ini
[Interface]
Address = 10.0.0.1/16
PrivateKey = <MANAGER_PRIVATE_KEY>
ListenPort = 51820
MTU = 1420
[Peer]
# Edge EU
PublicKey = <EDGE_EU_PUBLIC_KEY>
AllowedIPs = 10.0.100.1/32
[Peer]
# Edge US
PublicKey = <EDGE_US_PUBLIC_KEY>
AllowedIPs = 10.0.100.2/32
[Peer]
# Velocity 1
PublicKey = <VEL1_PUBLIC_KEY>
AllowedIPs = 10.0.0.2/32
```
---
*Версия: 1.0 | Июль 2026*
*Версия: 2.0 | Август 2026*

View file

@ -1,368 +1,110 @@
# Deployment - Rampart
> Как поднять Rampart с нуля. v0.1-v0.7.
> Как поднять Rampart с нуля.
---
## 1. Требования
## 1. Компоненты
### Минимальные (v0.1)
- 2 × VDS (KVM): Edge нода + Manager/Redis
- Ubuntu 22.04+, kernel 5.10+
- Rust toolchain (rustup)
- Docker + docker compose (для Manager)
| Бинарь | Роль | Порт по умолчанию |
|--------|------|-------------------|
| `rampart` | Edge-нода: XDP + PoW + userspace-фильтр | 25565 (+ metrics 9090) |
| `rampart-manager` | REST API управления | 8080 |
| `rampart-cli` | Управление через Manager API | — |
### Полный стек (v0.4+)
- Edge: Debian 12 / Ubuntu 22.04, kernel 5.10+ (6.0+ для полного XDP)
- Manager: любая VDS с Docker
- Java 21 (для Velocity плагинов)
- WireGuard (между нодами)
## 2. Требования
## Схема деплоя
### Edge
- Ubuntu 22.04+ / Debian 12, kernel 5.10+
- KVM или bare metal (OpenVZ/LXC — XDP не работает)
- Rust toolchain для сборки
```
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Edge нода │────→│ Load │────→│ Velocity │
│ 25565/TCP │ │ Balancer │ │ кластер │
│ XDP + Rust │ │ HAProxy │ │ x20 нод │
└──────────────┘ └──────────────┘ └──────┬───────┘
│
┌────────────────────────────┤
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Hub x100 │ │ Game │
│ (лобби) │ │ серверы │
│ │ │ x300+ │
└──────────────┘ └──────────────┘
│ │
└──────────┬──────────────┘
▼
┌──────────────────┐
│ Manager нода │
│ API :8080 │
│ Redis │
│ WireGuard Hub │
└──────────────────┘
```
Для XDP дополнительно: `libelf-dev libbpf-dev clang linux-libc-dev`.
Все соединения через WireGuard (10.0.0.0/16).
Game серверы НЕ имеют публичных IP - только WG.
Edge - единственная точка входа из интернета.
### Manager
- Любая VDS с Docker (Redis обязателен)
---
## 2. Быстрый старт - v0.1 (локально)
### Шаг 1: Edge нода
## 3. Быстрый старт — Edge
```bash
# На свежей Ubuntu 22.04 VDS
# Сборка
git clone https://github.com/loki5512344/rampart.git && cd rampart
cargo build --release --bin rampart
# Установка Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
rustup default stable
# Установка
sudo cp target/release/rampart /usr/local/bin/
# Установка зависимостей
sudo apt-get update
sudo apt-get install -y build-essential pkg-config libssl-dev
# Клонирование и сборка
git clone https://github.com/yourname/rampart.git
cd rampart
# Сборка edge ноды
cargo build --release --bin rampart-core
# Создание конфига
mkdir -p /etc/rampart
cat > /etc/rampart/config.toml << 'EOF'
[bind]
address = "0.0.0.0"
port = 25565
[backend]
address = "127.0.0.1"
port = 25566
[hmac]
secret = "CHANGE_ME_32_BYTES_LONG_HERE"
[workers]
count = 4
[limits]
handshake_timeout_secs = 5
max_connections_per_ip = 10
EOF
# Конфиг
sudo mkdir -p /etc/rampart
sudo cp deploy/config/edge.toml /etc/rampart/config.toml
# Отредактируй backend.upstreams под свои бэкенды
# systemd unit
cat > /etc/systemd/system/rampart-edge.service << 'EOF'
cat > /etc/systemd/system/rampart.service << 'EOF'
[Unit]
Description=Rampart Edge Node
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/rampart-core --config /etc/rampart/config.toml
Environment=RAMPART_CONFIG=/etc/rampart/config.toml
ExecStart=/usr/local/bin/rampart
Restart=always
RestartSec=5
LimitNOFILE=65535
User=nobody
Group=nogroup
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now rampart-edge
systemctl enable --now rampart
# Проверка
journalctl -u rampart-edge -f
journalctl -u rampart -f
```
### Шаг 2: Manager (docker compose)
> ⚠️ В ядре нет ни одной реализации протокольного обработчика: реестр протоколов
> пуст, и `rampart` при старте требует feature `protocol-http` или внешний
> ProtocolHandler (см. src/bin/rampart.rs). До появления плагинов edge-нода
> запускается только в исследовательских целях.
## 4. Быстрый старт — Manager
```bash
# На отдельной VDS или той же
# Установка Docker
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# Клонирование
git clone https://github.com/yourname/rampart.git
cd rampart
# Запуск инфраструктуры
docker compose up -d
# Проверка
docker compose ps
curl http://localhost:9090/api/health
export REDIS_URL="redis://127.0.0.1:6379/0"
export JWT_SECRET="$(openssl rand -hex 32)" # минимум 32 байта
export API_PASSWORD="не-changeme" # дефолт запрещён
cargo run --release --bin rampart-manager
# API на 0.0.0.0:8080
```
### Шаг 3: Velocity плагин
Инфраструктура (Redis, ClickHouse, Grafana):
```bash
# На Velocity ноде
# Сборка плагина
cd plugins/velocity
mvn clean package
# Полученный JAR: target/rampart-velocity-*.jar
# Копируем в папку плагинов Velocity
cp target/rampart-velocity-*.jar /opt/velocity/plugins/
# Настройка
cat >> /opt/velocity/velocity.toml << 'EOF'
[rampart]
# Включаем HMAC проверку
hmac_secret = "CHANGE_ME_32_BYTES_LONG_HERE"
# Домены разрешённые для подключения
allowed_domains = ["play.example.com", "mc.example.com"]
# Redis (опционально, для v0.2+)
redis_url = "redis://:password@10.0.0.1:6379/0"
EOF
# Рестарт Velocity
systemctl restart velocity
docker compose up -d redis clickhouse grafana
```
### Шаг 4: Проверка
## 5. Docker
```bash
# Статус edge ноды
rampart status
# Образ edge
docker build -f deploy/docker/Dockerfile.edge -t rampart-edge .
# Диагностика
rampart doctor
# Полный стек edge + инфраструктура
docker compose -f deploy/docker-compose.yml up -d
```
# Проверка что порт слушается
ss -tlnp | grep 25565
## 6. Проверка после деплоя
# Тест подключения Minecraft клиента
# Открой MC → Multiplayer → play.example.com:25565
```bash
☐ systemctl status rampart
☐ ss -tlnp | grep 25565
☐ curl http://localhost:9090/metrics | grep rampart_
☐ rampart-cli status # через Manager API
☐ rampart-cli doctor # диагностика
```
---
## 3. WireGuard сеть
### Hub-and-Spoke на Manager
```bash
# На Manager ноде (WireGuard Hub)
# Установка
sudo apt-get install -y wireguard
# Генерация ключей
wg genkey | tee /etc/wireguard/manager.key | wg pubkey > /etc/wireguard/manager.pub
# Конфиг Hub
cat > /etc/wireguard/wg0.conf << 'EOF'
[Interface]
Address = 10.0.0.1/16
PrivateKey = <MANAGER_PRIVATE_KEY>
ListenPort = 51820
# Edge нода будет добавлена позже
EOF
systemctl enable --now wg-quick@wg0
```
### Добавление spoke ноды (через CLI)
```bash
# На Manager: генерируем конфиг для edge ноды
rampart wg add-node --role edge --name edge-eu-1 --public-ip 45.200.10.1
# Полученный конфиг:
# /etc/rampart/wg-configs/edge-eu-1/wg0.conf
# Копируем на edge ноду
scp /etc/rampart/wg-configs/edge-eu-1/wg0.conf root@45.200.10.1:/etc/wireguard/
# На edge ноде: запускаем
ssh root@45.200.10.1 'systemctl enable --now wg-quick@wg0'
# Проверка
ping 10.0.0.1 # Manager должен ответить
```
---
## 4. Полный production deploy (v0.4+)
### Edge нода с XDP
```bash
# Проверка совместимости
systemd-detect-virt # должно быть kvm или none
ethtool -i eth0 # драйвер: i40e, mlx5, virtio
# Установка XDP зависимостей
sudo apt-get install -y libbpf-dev clang llvm linux-headers-$(uname -r)
# Сборка с XDP
cargo build --release --features xdp
# Настройка sysctl для DDoS защиты
cat > /etc/sysctl.d/99-rampart.conf << 'EOF'
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_syn_retries = 2
net.ipv4.icmp_echo_ignore_all = 1
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.ip_local_port_range = 1024 65535
EOF
sysctl -p /etc/sysctl.d/99-rampart.conf
```
### Monitoring стек
```yaml
# /opt/rampart/docker-compose.monitoring.yml
# Дополнение к основному compose
services:
victoria-metrics:
image: victoriametrics/victoria-metrics:latest
ports:
- "8428:8428" # remote_write endpoint
command:
- '--storageDataPath=/storage'
- '--retentionPeriod=3'
volumes:
- vm-data:/storage
parca:
image: ghcr.io/parca-dev/parca:latest
ports:
- "7070:7070"
```
---
## 5. CI/CD pipeline
```yaml
# .github/workflows/deploy.yml
name: Deploy
on:
push:
tags:
- 'v*'
jobs:
build-edge:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: cargo build --release --features xdp
- uses: actions/upload-artifact@v4
with:
name: rampart-edge
path: target/release/rampart-core
deploy-edge:
needs: build-edge
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
- run: |
scp rampart-core root@${EDGE_HOST}:/usr/local/bin/
ssh root@${EDGE_HOST} 'systemctl restart rampart-edge'
```
---
## 6. Firewall (резюме)
```bash
# Быстрая настройка для edge ноды
sudo ./scripts/firewall.sh
# Проверка
sudo iptables -L -n -v
```
Полные правила в [networking.md](research/networking.md).
---
## 7. Checklist после деплоя
```
☐ Edge нода запущена: systemctl status rampart-edge
☐ Порты слушаются: ss -tlnp | grep 25565
☐ WireGuard работает: wg show
☐ Manager API отвечает: curl http://localhost:9090/api/health
☐ Redis доступен: redis-cli ping
☐ ClickHouse пишет: curl http://localhost:8123/ping
☐ Velocity плагин загружен: /plugins/rampart-velocity-*.jar
☐ Реальный MC клиент заходит
☐ Prometheus метрики: curl http://localhost:9090/metrics
```
---
## 8. Troubleshooting
| Симптом | Причина | Решение |
|---------|---------|---------|
| Edge не стартует | Порт занят | `ss -tlnp \| grep 25565`, смени порт |
| Velocity не подключается | Не совпадает HMAC secret | Проверь `config.toml` и `velocity.toml` |
| XDP не загружается | OpenVZ / old kernel | `systemd-detect-virt`, проверь `uname -r` |
| Redis connection refused | Не настроен firewall | `iptables -A INPUT -p tcp --dport 6379 -s 10.0.0.0/16 -j ACCEPT` |
| ClickHouse не пишет | Нет таблицы | Выполни CREATE TABLE из `observability.md` |
---
*Версия: 1.0 | Июль 2026*
*Версия: 2.0 | Август 2026*

View file

@ -1,76 +1,55 @@
# Disaster Recovery - Rampart
> Что делать когда что-то пошло не так.
> Что делать, когда что-то пошло не так.
## Схема failover
```
Redis упал:
Edge: продолжает с локальным кэшем
Velocity: продолжает с последним кэшем серверов
Manager: API не работает → рестарт Redis, рестарт Manager
Edge: продолжает автономно на локальном кэше блэклиста
Manager: API отдаёт пустые списки / ошибки → рестарт Redis
Manager упал:
Edge: продолжает автономно
Velocity: читает Redis напрямую
Dashboard: недоступен → рестарт Manager
Edge: продолжает фильтрацию (фильтрация не зависит от Manager)
Управление (CLI/API) недоступно до восстановления
Edge нода упала:
Игроки на ней теряют коннект
При реконнекте → BGP/DNS → другая Edge нода
Если Edge одна → все офлайн
Клиенты за ней теряют соединение
При нескольких edge → DNS round-robin на живые
Если edge одна — сервис недоступен
Полный сбой дата-центра:
Edge ноды в других ДЦ продолжают работу
Игроки на живых серверах продолжают играть
Новые регистрации/баны не синхронизируются до восстановления
Edge в других ДЦ продолжают работу
Баны/ноды не синхронизируются до восстановления Redis
```
Все компоненты кроме Manager продолжают работать в degraded mode.
Manager - единственная single point of failure (без Redis Sentinel).
---
## 1. Redis упал
### Симптомы
- Velocity не видит новые серверы
- Edge не синхронизирует блэклист
- Manager API возвращает 500
- Edge перестал синхронизировать блэклист
- Manager API отдаёт пустые списки nodes/servers/blacklist
### Влияние
- **Edge:** Продолжает работать с локальным кэшем блэклиста. Новые баны не синхронизируются между нодами.
- **Velocity:** Продолжает работать с последним кэшем server registry. Новые серверы недоступны до восстановления Redis.
- **Manager:** API не работает.
- **Edge:** работает с локальным кэшем; новые баны не распространяются между нодами.
- **Manager:** данные из Redis недоступны.
### Действия
```bash
# 1. Проверка Redis
redis-cli ping
systemctl status redis
redis-cli ping || systemctl restart redis
# 2. Если Redis завис - рестарт
systemctl restart redis
# 3. Если Redis навсегда умер - поднять новый
# Убедись что пароль совпадает с конфигами
docker run -d --name rampart-redis \
-p 6379:6379 \
# Поднять новый (пароль должен совпадать):
docker run -d --name rampart-redis -p 6379:6379 \
redis:7-alpine redis-server --requirepass "$REDIS_PASSWORD"
# 4. Перезапустить Manager (он переподключится)
systemctl restart rampart-manager
# 5. Edge ноды переподключатся автоматически (retry в драйвере Redis)
# Если не переподключились - рестарт:
systemctl restart rampart-edge
# 6. Velocity переподключится с задержкой до 5 сек
systemctl restart rampart-manager # переподключится
# Edge переподключается сам (retry в драйвере); иначе:
systemctl restart rampart
```
### Предотвращение
- Redis Sentinel для HA (3 ноды)
- AOF + RDB persistence включены
- Регулярные бэкапы: `redis-cli SAVE`
@ -79,245 +58,92 @@ systemctl restart rampart-edge
## 2. Manager упал
### Симптомы
- API не отвечает
- Blacklist изменения не применяются
- Edge heartbeat пропадает
- `rampart-cli` команды падают с ошибкой соединения
- `/api/v1/health` не отвечает
### Влияние
- **Edge:** Продолжает работать автономно. Локальный блэклист активен.
- **Velocity:** Продолжает работать. Server registry из Redis доступен.
- **Dashboard:** Недоступен.
- Edge продолжает фильтровать трафик автономно.
### Действия
```bash
# 1. Проверка
systemctl status rampart-manager
journalctl -u rampart-manager -n 50 --no-pager
# 2. Рестарт
systemctl restart rampart-manager
# 3. Если не стартует - проверить логи
# Не стартует? Частая причина — env:
# JWT_SECRET < 32 байт или API_PASSWORD=changeme → процесс выходит с ошибкой.
journalctl -u rampart-manager -e | grep ERROR
# 4. Если проблема в конфиге
# Откатить последние изменения конфига
git checkout HEAD~1 -- config/manager.toml
systemctl restart rampart-manager
```
### Предотвращение
- systemd `Restart=always`
- Мониторинг: Prometheus alert `EdgeNodeDown`
- Два Manager в active/passive (v0.6+)
---
## 3. Edge нода упала
### Симптомы
- Игроки на этой ноде теряют соединение
- Prometheus alert: `EdgeNodeDown`
- Метрики перестали приходить
### Влияние
- Игроки, подключённые через эту ноду, дисконнектятся
- При переподключении → попадают на другую edge ноду
- Если edge нода одна → **все игроки офлайн**
- Клиенты теряют соединение, метрики :9090 пропали
### Действия
```bash
# 1. Проверка
systemctl status rampart-edge
journalctl -u rampart-edge -n 50 --no-pager
systemctl status rampart
journalctl -u rampart -n 50 --no-pager
# 2. Если OOM kill
dmesg | grep -i "oom\|rampart"
dmesg | grep -i "oom\|rampart" # OOM kill?
systemctl restart rampart
# 3. Рестарт
systemctl restart rampart-edge
# 4. Если не стартует - проверить конфиг
rampart doctor
# 5. Если аппаратная проблема - переключить DNS на другую edge ноду
# (при нескольких edge нодах)
# Не стартует — проверь конфиг и логи (см. runbook.md §2)
```
### Предотвращение
- Минимум 2 edge ноды
- DNS round-robin или BGP Anycast
- Минимум 2 edge ноды + DNS round-robin
- systemd `Restart=always`
- `rampart drain` для graceful maintenance
- Несколько edge нод за одним DNS-именем
---
## 4. ClickHouse упал
### Симптомы
- Attack log не пишется
- Dashboard по блокировкам пустой
### Влияние
- **Edge / Velocity / Manager:** Продолжают работать. Потеря аналитики.
- Данные не теряются (буферизация в Manager на 1 секунду с батчем до 1000).
Только потеря аналитики: события буферизуются и пишутся асинхронно,
фильтрация не зависит от ClickHouse.
### Действия
```bash
# 1. Проверка
systemctl status clickhouse-server
curl http://localhost:8123/ping
# 2. Рестарт
systemctl restart clickhouse-server
# 3. Если долго восстанавливается - проверить диск
df -h /var/lib/clickhouse
# 4. Если диск полон - почистить старые партиции
clickhouse-client --query "ALTER TABLE rampart.blocked DROP PARTITION '2025-01'"
curl http://localhost:8123/ping || systemctl restart clickhouse-server
df -h /var/lib/clickhouse # диск полон?
```
### Предотвращение
- TTL на таблицах (90 дней автоочистка)
- ClickHouse Cloud или Cluster (v0.6+)
- Alertmanager при заполнении диска > 80%
---
## 5. Root CA key скомпрометирован
### Симптомы
- Вы знаете что ключ утек
- Подозрительные сертификаты в сети
### Влияние
- **Полная компрометация mTLS:** Атакующий может выпустить сертификаты для любой ноды
### Действия
```bash
# 1. НЕМЕДЛЕННО: Сгенерировать новый Root CA
rampart pki init --root-ca rampart-ca-v2 --force
# 2. Выпустить новые сертификаты для ВСЕХ нод
for node in edge-eu-1 edge-us-1 vel-1 manager; do
rampart pki issue --ca edge-ca --name "$node" \
--ip "$(dig +short $node.rampart.internal)" \
--san "$node.rampart.internal" \
--output "/etc/rampart/pki/$node/"
done
# 3. Разослать новые сертификаты на все ноды
rampart pki sync --all-nodes
# 4. Перезапустить все сервисы (с новыми сертификатами)
rampart restart --all
# 5. Отозвать старый Root CA
rampart pki revoke --ca rampart-ca-v1
# 6. Расследовать утечку
# - Проверить кто имел доступ к ключу
# - Проверить логи доступа
# - Сменить все пароли
```
### Предотвращение
- Root CA ключ хранить **вне серверов** (на YubiKey или в Vault)
- Использовать Intermediate CA для повседневной работы
- Audit лог доступа к CA ключу
---
## 6. Полный сбой инфраструктуры
### Ситуация
Упали нода Manager + Redis + NATS одновременно (например, отключили дата-центр).
### Влияние
- Все edge ноды продолжают работать автономно
- Блэклист не синхронизируется
- Server registry не обновляется
- **Игроки продолжают играть на уже запущенных серверах**
### Восстановление
## 5. Полный сбой инфраструктуры
```bash
# 1. Поднять Manager на новой VDS
docker compose up -d
docker compose up -d redis && cargo run --release --bin rampart-manager
# 2. Восстановить Redis из бэкапа
redis-cli --pipe < /backup/rampart-redis-$(date +%Y-%m-%d).rdb
# 3. Edge ноды и Velocity переподключатся автоматически
# (они реконнектятся с экспоненциальной задержкой: 1s, 2s, 4s, 8s... max 60s)
# 3. Edge ноды переподключатся автоматически
# 4. Проверить что всё синхронизировалось
rampart doctor
```
### Предотвращение
- Бэкапы Redis: ежедневно, хранить 30 дней
- Terraform для быстрого поднятия инфраструктуры
- DNS записи с низким TTL (60 сек)
---
## 7. DDoS на Manager/Redis
### Симптомы
- Manager API не отвечает
- Redis latency > 1 секунды
- CPU Manager 100%
### Действия
```bash
# 1. Изолировать Manager - закрыть все порты кроме WireGuard
iptables -P INPUT DROP
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p udp --dport 51820 -j ACCEPT
iptables -A INPUT -j DROP
# 2. Если DDoS идёт на публичный IP - отключить его
# (Оставить только WireGuard туннель)
# 3. Edge ноды переживут без Manager несколько часов
# (они кэшируют блэклист локально)
# 4. Проверить
rampart-cli doctor
```
---
## 8. Cheatsheet быстрых команд
## 6. Cheatsheet быстрых команд
```bash
# Рестарт всего
systemctl restart rampart-edge rampart-manager redis clickhouse-server
# Проверка здоровья всей системы
rampart doctor
# Последние 50 строк логов edge
journalctl -u rampart-edge -n 50 -f
# CPU/memory edge
htop -p $(pgrep -d',' rampart-edge)
# Трафик на интерфейсе
systemctl restart rampart rampart-manager redis clickhouse-server
journalctl -u rampart -n 50 -f
htop -p $(pgrep -d',' rampart)
iftop -i eth0
# Статистика Redis
redis-cli info stats | grep -E "total_connections|total_commands|rejected"
# Активные соединения
ss -s | grep TCP
```
---
*Версия: 1.0 | Июль 2026*
*Версия: 2.0 | Август 2026*

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

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

@ -1,287 +0,0 @@
# Migration - Rampart
> Как обновляться между версиями без даунтайма.
---
## Общие принципы
1. **Читай CHANGELOG** перед обновлением
2. **Бэкап** перед любой миграцией: Redis RDB, конфиги, сертификаты
3. **Одна нода** сначала - тестируй на одной edge, потом на всех
4. **Откат** - всегда сохраняй предыдущую версию бинарника
---
## v0.1 → v0.2 (Redis + Registry)
### Изменения
- Edge нода начинает использовать Redis для синхронизации блэклиста
- Paper плагин пишет в Redis вместо локального YAML
- Velocity получает server registry из Redis
### Шаги
```bash
# 1. Поднять Redis (если ещё нет)
docker compose up -d redis
# 2. Настроить Redis пароль
echo "requirepass НОВЫЙ_ПАРОЛЬ" >> /etc/redis/redis.conf
systemctl restart redis
# 3. Обновить конфиг edge ноды
cat >> /etc/rampart/config.toml << 'EOF'
[store]
redis_url = "redis://:НОВЫЙ_ПАРОЛЬ@10.0.0.1:6379/0"
blacklist_cache_ttl_secs = 300
EOF
# 4. Обновить Paper плагин (перейти с file → redis)
sed -i 's/registration_mode: "file"/registration_mode: "redis"/' paper-global.yml
sed -i 's|# redis_url:|redis_url: "redis://:НОВЫЙ_ПАРОЛЬ@10.0.0.1:6379/0"|' paper-global.yml
# 5. Обновить Velocity плагин
# Добавить redis_url в velocity.toml
# 6. Рестарт по очереди (no downtime)
systemctl restart rampart-edge # по одной edge ноде
systemctl restart velocity # по одной velocity
# Paper плагины - reload через /reload команду
```
### Откат
```bash
# Если что-то пошло не так:
# 1. Вернуть registration_mode: "file" в paper-global.yml
# 2. Убрать redis_url из всех конфигов
# 3. Рестартнуть всё в обратном порядке
```
---
## v0.2 → v0.3 (Observability)
### Изменения
- Добавляются Prometheus метрики на всех компонентах
- ClickHouse для хранения attack log
- Grafana дашборды
### Шаги
```bash
# 1. Поднять стек мониторинга
docker compose up -d clickhouse prometheus grafana
# 2. Создать таблицы ClickHouse
clickhouse-client --query "
CREATE DATABASE IF NOT EXISTS rampart;
CREATE TABLE IF NOT EXISTS rampart.blocked (
ts DateTime CODEC(Delta, ZSTD),
edge LowCardinality(String),
src_ip IPv4,
src_asn UInt32,
src_country LowCardinality(FixedString(2)),
reason LowCardinality(String),
proto_ver Int32,
hostname String CODEC(ZSTD)
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(ts)
ORDER BY (ts, edge, src_ip)
TTL ts + INTERVAL 90 DAY;
"
# 3. Настроить scrape targets в prometheus.yml
# 4. Импортировать Grafana dashboard
# 5. Ничего рестартить не нужно - метрики уже встроены
```
### Проверка
```bash
curl -s http://EDGE_IP:9090/metrics | grep rampart
```
---
## v0.3 → v0.4 (XDP)
### Изменения
- XDP программа на C
- libbpf-rs для загрузки в ядро
- Feature flag: `xdp`
### Шаги
```bash
# 1. Проверить совместимость
systemd-detect-virt # нужно kvm или none
uname -r # нужно 5.10+
ethtool -i eth0 # драйвер
# 2. Установить зависимости
sudo apt-get install -y libbpf-dev clang llvm linux-headers-$(uname -r)
# 3. Собрать с XDP
cargo build --release --features xdp
# 4. Обновить бинарник
cp /usr/local/bin/rampart-core /usr/local/bin/rampart-core.backup
cp target/release/rampart-core /usr/local/bin/
# 5. Включить XDP в конфиге
cat >> /etc/rampart/config.toml << 'EOF'
[xdp]
enabled = true
interface = "eth0"
EOF
# 6. Рестарт
systemctl restart rampart-edge
# 7. Проверить
journalctl -u rampart-edge | grep XDP
ip link show | grep xdp
```
### Откат
```bash
# Отключить XDP
rampart config set xdp_enabled false
systemctl restart rampart-edge
# Вернуть старый бинарник
cp /usr/local/bin/rampart-core.backup /usr/local/bin/rampart-core
```
---
## v0.4 → v0.5 (Anti-Bot)
### Изменения
- GeoIP (MaxMind GeoLite2)
- Sonar 3.0 интеграция
- Challenge API
### Шаги
```bash
# 1. Зарегистрироваться на maxmind.com, скачать GeoLite2-ASN
# 2. Разместить базу на Manager
mkdir -p /var/lib/rampart/geoip
cp GeoLite2-ASN.mmdb /var/lib/rampart/geoip/
# 3. Обновить конфиг edge
cat >> /etc/rampart/config.toml << 'EOF'
[geoip]
db_path = "/var/lib/rampart/geoip/GeoLite2-ASN.mmdb"
# ASN с повышенным скорингом
vpn_asns = [16276, 24940, 20473]
datacenter_asns = [16509, 14618, 8075]
EOF
# 4. Обновить Velocity плагин (с поддержкой Sonar)
cp plugins/velocity/target/rampart-velocity-*.jar /opt/velocity/plugins/
systemctl restart velocity
# 5. Проверить
rampart geoip lookup 1.2.3.4
```
---
## v0.5 → v0.6 (Scale + HA)
### Изменения
- Rust LB вместо HAProxy
- mTLS между всеми компонентами
- QUIC канал Edge ↔ Manager
- NATS JetStream
### Шаги
```bash
# 1. Развернуть NATS
docker compose up -d nats
# 2. Обновить конфиг Manager
cat >> /etc/rampart/manager.toml << 'EOF'
[nats]
urls = ["nats://127.0.0.1:4222"]
[quic]
bind = "0.0.0.0:7777"
EOF
# 3. Сгенерировать PKI
rampart pki init --root-ca rampart-ca
rampart pki issue --ca edge-ca --name edge-eu-1 --ip 10.0.100.1
rampart pki issue --ca infra-ca --name manager --ip 10.0.0.1
# 4. Развернуть сертификаты на все ноды
# 5. Включить mTLS в конфигах
# 6. Постепенно перевести трафик с HAProxy на Rust LB
```
### Миграция с HAProxy
```bash
# Фаза 1: Запустить Rust LB рядом с HAProxy
# (разные порты: HAProxy :25565, Rust LB :25566)
# Фаза 2: Переключить edge ноды на Rust LB
# (изменить backend.address в config.toml)
# Фаза 3: Остановить HAProxy
# (когда все edge переключены)
```
---
## v0.6 → v0.7 (Polish)
### Изменения
- io_uring runtime (feature flag)
- Zero-copy splice
- SLSA Level 3
### Шаги
```bash
# 1. Проверить io_uring доступность
cat /proc/sys/kernel/io_uring_disabled # 0 = OK
# 2. Собрать с io_uring
cargo build --release --features io-uring
# 3. Заменить бинарник
cp /usr/local/bin/rampart-core /usr/local/bin/rampart-core.epoll.backup
cp target/release/rampart-core /usr/local/bin/
systemctl restart rampart-edge
# 4. Проверить
journalctl -u rampart-edge | grep "io_uring"
# 5. Бенчмарк: сравнить производительность
tcpkali --connections 1000 --connect-rate 5000 --duration 30s EDGE_IP:25565
```
---
## Чеклист перед любой миграцией
```
☐ Прочитал CHANGELOG
☐ Сделал бэкап Redis: redis-cli SAVE
☐ Сделал бэкап конфигов: tar czf /backup/rampart-configs-$(date +%Y%m%d).tar.gz /etc/rampart/
☐ Сохранил старые бинарники
☐ Есть доступ к серверу через OOB/IPMI (на случай если сеть отвалится)
☐ Есть откат-план
☐ Предупредил команду в Discord
```
---
*Версия: 1.0 | Июль 2026*

View file

@ -1,183 +1,300 @@
# Architecture - Rampart
# Architecture — Rampart
> Актуально: v0.2+
> Статус: основной документ
> Relevant for: v0.3+ (universal redesign)
> Status: primary design document
> Language: English (research notes) — see [RU summary](#russian-summary) at the end
---
## 6-слойная архитектура защиты
## What changed
Rampart began as a 6-layer, Minecraft-specific protection stack: XDP filter with
hardcoded Minecraft handshake states → PoW → Rust L7 core → Velocity proxy (Java)
→ Paper agent (Java) → Traffic Intel. The big-bang redesign turns it into a
**universal L3/L4/L7 network protection platform** for any TCP service:
- VDS / dedicated servers,
- web services and APIs,
- game servers of any kind (Minecraft is now just a plugin, not the core).
The layer count drops from 6 to 4. Everything protocol-specific is extracted from
the core into compile-time protocol plugins; everything Java-based is removed.
## Layered architecture
```
┌──────────────────────────────────────────────────────────────────────┐
│ LAYER 1: XDP/eBPF (ядро) дроп L3/L4 до kernel TCP stack │
│ LAYER 1: XDP/eBPF (C) drop L3/L4 before kernel TCP stack │
│ ───────────────────────────── │
│ TCP state machine (minecraft_filter.c): │
│ AWAIT_ACK → AWAIT_MC_HANDSHAKE → AWAIT_LOGIN → verified │
│ + SYN throttle per-IP │
│ + IP blacklist (LPM_TRIE) │
│ + Invalid TCP flags drop (SYN+FIN, SYN+RST, URG, пустые) │
│ + UDP drop (MC = TCP only) │
│ + Per-connection seq tracking │
│ + bpf_timer idle cleanup │
│ + IP/CIDR whitelist │
│ ─────────────────────────────────────── │
│ Reference: Minecraft-XDP-eBPF (исправленный: нет pure ACK deadlock,│
│ LRU maps, IPv6, idle таймеры на conntrack) │
│ Universal (protocol-agnostic): │
│ + Generic TCP state machine (SYN → ESTABLISHED lifecycle, │
│ per-connection seq tracking, bpf_timer idle cleanup) │
│ + SYN throttle per-IP │
│ + IP/CIDR blacklist + whitelist (LPM_TRIE) │
│ + Invalid TCP flags drop (SYN+FIN, SYN+RST, URG, empty) │
│ + UDP policy (configurable: pass / drop / rate-limit) │
│ + Pluggable BPF protocol hooks (see ADR-002) │
├──────────────────────────────────────────────────────────────────────┤
│ LAYER 2: PoW Challenge (Rust) анти-handshake-flood │
│ LAYER 2: Universal PoW Challenge (Rust) anti-handshake-flood │
│ ───────────────────────────── │
│ SHA256 hashcash перед HMAC handshake: │
│ 1. Edge шлёт challenge (random + timestamp + difficulty) │
│ 2. Клиент решает PoW (nonce brute-force) │
│ 3. Edge верифицирует SHA256(data + nonce) prefix │
│ + Dynamic difficulty: повышается при CPS > threshold │
│ + Per-connection одноразовый challenge (nonce replay защита) │
│ ─────────────────────────────────────── │
│ Reference: PowGo (адаптирован: per-request challenge, timestamp, │
│ dynamic difficulty, без Redis, без IP+UA сессии) │
│ SHA256 hashcash over any TCP protocol (see ADR-004): │
│ 1. Edge sends challenge (random + timestamp + difficulty) │
│ 2. Client solves PoW (nonce brute-force) │
│ 3. Edge verifies SHA256(data + nonce) prefix │
│ + Dynamic difficulty: rises when CPS > threshold │
│ + Per-connection one-time challenge (nonce replay protection) │
├──────────────────────────────────────────────────────────────────────┤
│ LAYER 3: Rust Core (userspace) L7 фильтрация │
│ LAYER 3: Rust Userspace Core L7 filtering │
│ ───────────────────────────── │
│ + MC handshake парсинг (VarInt, bounds check) │
│ + HMAC-SHA256 hostname signature │
│ + Rate limit (token bucket per-IP) │
│ + Death code auto-ban (8 паттернов) │
│ + ASN/GeoIP reputation │
│ + Blacklist (Redis sync) │
│ + L7 handshake analysis (via active protocol plugin) │
│ + Rate limit (token bucket per-IP) │
│ + HMAC signatures (constant-time compare) │
│ + Death-code patterns (auto-ban on malicious payloads) │
├──────────────────────────────────────────────────────────────────────┤
│ LAYER 4: Velocity Proxy (Java) верификация игроков │
│ LAYER 4: Protocol Plugins (compile-time feature crates) │
│ ───────────────────────────── │
│ + Domain whitelist (блок прямых IP) │
│ + HMAC verification (constant-time compare) │
│ + Falling check (детерминированная физика: pre-computed кэш) │
│ + Protocol check (Transaction, SetHeldItem, ArmAnimation) │
│ + Vehicle check (Boat/Minecart gravity) │
│ + CAPTCHA challenge (Map item / PoW) │
│ + Redis server registry (delta-sync) │
│ + TPS-aware load balancer (circuit breaker < 12 TPS) │
│ ─────────────────────────────────────── │
│ Reference: Sonar pipeline + LimboFilter falling check │
│ (исправлено: HMAC fingerprint, idempotent finishVerification, │
│ без QuietDecoderException, без race в handler switching) │
├──────────────────────────────────────────────────────────────────────┤
│ LAYER 5: Paper Agent (Java) авто-регистрация │
│ ───────────────────────────── │
│ + Redis heartbeat (TPS, online игроки, память, CPU) │
│ + Auto-registration/unregistration │
│ + HMAC login check │
│ + Graceful shutdown │
├──────────────────────────────────────────────────────────────────────┤
│ LAYER 6: Traffic Intelligence (Rust + Redis) аналитика │
│ ───────────────────────────── │
│ + 168-hour traffic profiling (per-hour-slot baseline) │
│ + EWMA adaptive thresholds (правильная variance формула) │
│ + Z-Score anomaly detection (3 consecutive minutes для алерта) │
│ + Attack detection (CPS, PPS thresholds) │
│ + Reputation system (IP score -100..+100) │
│ + Discord webhook на события │
│ ─────────────────────────────────────── │
│ Reference: AtomGuard (исправлено: EWMA variance, Isolation Forest │
│ реально используется, без race в pipeline) │
│ + minecraft — first plugin (existing MC-handshake code moved in) │
│ + http, grpc — planned │
└──────────────────────────────────────────────────────────────────────┘
TRAFFIC INTEL (cross-cutting, Rust + Redis):
+ EWMA adaptive thresholds
+ Traffic profiling (per-slot baselines)
+ IP reputation system
```
## Схема прохождения трафика
### Traffic path
```
Атакующий (ботнет)
Attacker (botnet)
|
v
[1] XDP/eBPF ─── TCP state machine ─── blacklist ─── SYN throttle
| дроп: SYN flood, UDP, invalid flags, non-MC port
v (чистый TCP, прошёл state machine)
[2] PoW Challenge ─── SHA256 hashcash ─── dynamic difficulty
| дроп: не решил PoW за N секунд
v (валидный PoW)
[3] Rust Core ─── handshake parse ─── HMAC sign ─── rate limit ─── death code
| дроп: rate limit, invalid packet, bad HMAC
v (валидный MC handshake + HMAC)
[4] Velocity ─── domain check ─── HMAC verify ─── falling/physics check ─── CAPTCHA
| дроп: bad domain, bad HMAC, failed physics
v (верифицированный игрок)
[5] Game Server
| Чистый трафик, без DDoS нагрузки
[1] XDP/eBPF ─── universal TCP state machine ─── CIDR lists ─── SYN throttle
| drop: SYN flood, invalid flags, blacklisted CIDRs,
| UDP policy violations, failed BPF hook checks
v (clean TCP that passed state machine + hooks)
[2] PoW Challenge ─── SHA256 hashcash ─── dynamic difficulty [OFF by default]
| drop: did not solve PoW in time
v (valid PoW)
[3] Userspace Core ─── plugin handshake parse ─── rate limit ─── death codes
| drop: rate limit, malformed packets
v (validated application-layer client)
[4] Consumer service (game server, web backend, ...)
```
## Компоненты системы
## Component diagram
```
┌────────────────────────────────────────────────────────────────┐
│ EDGE NODE │
│ XDP/eBPF (C) → PoW (Rust) → Rust Core → Manager API │
│ ──────────────────────────────────────────────────────────── │
│ Требования: KVM/Bare Metal, 2-4 vCPU, 2-4 GB, kernel 5.10+ │
│ XDP native: Intel i40e, Mellanox ConnectX, virtio (generic) │
└────────────────────────┬───────────────────────────────────────┘
│ mTLS/QUIC
┌────────────────────────▼───────────────────────────────────────┐
│ VELOCITY CLUSTER │
│ Java 21, Velocity 3.4+, x20 нод │
│ Domain check → HMAC verify → Physics → CAPTCHA → Router │
└────────────────────────┬───────────────────────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
Hub (x100) Game Servers Game Servers
лобби Survival (x100) Skyblock (x100)
разные VDS/дедики
```mermaid
flowchart TB
subgraph Kernel["Layer 1 — Kernel"]
XDP["XDP/eBPF filter (C)<br/>TCP state machine · SYN throttle<br/>CIDR black/white · UDP policy"]
HOOKS["BPF protocol hooks<br/>(pluggable modules)"]
XDP --- HOOKS
end
subgraph Core["rampart-core (Rust)"]
LOADER["XDP loader<br/>(libbpf / aya)"]
POW["PoW challenge engine<br/>(SHA256 hashcash)"]
L7["L7 filtering core<br/>rate limit · HMAC · death codes"]
INTEL["Traffic Intel<br/>EWMA · profiling · reputation"]
LOADER --> XDP
POW --> L7 --> PLUGINAPI
INTEL -.->|thresholds & scores| L7
INTEL -.->|dynamic difficulty| POW
end
subgraph Plugins["Layer 4 — Protocol plugins (feature crates)"]
PLUGINAPI["Plugin trait API<br/>(stabilization in progress)"]
MC["minecraft plugin"]
HTTP["http plugin<br/>(planned)"]
GRPC["grpc plugin<br/>(planned)"]
PLUGINAPI --> MC
PLUGINAPI -.-> HTTP
PLUGINAPI -.-> GRPC
end
subgraph Control["Control plane"]
MANAGER["rampart-manager<br/>(axum API)"]
REDIS[(Redis)]
CLI["rampart-cli"]
MANAGER <--> REDIS
CLI --> MANAGER
end
L7 --> BACKEND["Protected service<br/>(game server / web backend / VDS)"]
style HTTP stroke-dasharray: 5 5
style GRPC stroke-dasharray: 5 5
```
## Требования к хостингу
## Architecture Decision Records
| Нода | Роль | CPU | RAM | Тип | XDP |
|------|------|-----|-----|-----|-----|
| **Edge** | XDP + PoW + фильтрация | 2-4 vCPU | 2-4 GB | KVM / Bare Metal | ✅ |
| **Velocity** | MC Proxy + верификация | 4 vCPU | 4-8 GB | KVM | ❌ |
| **Manager** | API + Redis | 2-4 vCPU | 4-8 GB | KVM | ❌ |
| **Hub** | Лобби | 4-8 vCPU | 8-16 GB | KVM / Bare Metal | ❌ |
| **Game** | Игровой процесс | 4-8 vCPU | 8-32 GB | KVM / Bare Metal | ❌ |
### ADR-004: Compile-time protocol plugins instead of hardcoded Minecraft
> ⚠️ XDP требует KVM или Bare Metal. OpenVZ/LXC контейнеры — XDP не работает.
> Проверить: `systemd-detect-virt`
**Context.** The Rust userspace core originally parsed only Minecraft handshakes
(VarInt decoding, hostname bounds checks). Every non-Minecraft use case required
forking the codebase.
## Sizing guide
**Decision.** Protocol-specific parsing moves behind an internal **plugin trait
API** implemented by separate feature crates (`rampart-plugin-minecraft`, later
`rampart-plugin-http`, `rampart-plugin-grpc`). Plugins are selected at compile
time via Cargo features — no dynamic loading, no ABI stability burden. The first
plugin, `minecraft`, receives the existing MC-handshake code as-is.
| Игроков | Edge нод | Velocity нод | Edge RAM | Стоимость/мес |
|---------|----------|--------------|----------|---------------|
| до 500 | 1 | 2 | 2 GB | ~$15-30 |
| до 2 000 | 2 | 4 | 4 GB | ~$40-80 |
| до 10 000 | 4-6 | 8-10 | 8 GB | ~$150-300 |
| до 50 000 | 10-15 | 15-20 | 16 GB | ~$600-1200 |
**Consequences.**
## Граница XDP / Rust (критично)
- (+) The core becomes protocol-neutral; one platform covers VDS, web, and games.
- (+) Compile-time selection keeps dispatch static and the hot path monomorphic.
- (−) Supporting multiple protocols simultaneously requires composing feature
sets per build; runtime protocol multiplexing is out of scope until the plugin
API stabilizes.
- (−) The trait API must stabilize before third-party plugins appear (roadmap).
```
XDP делает: Rust делает:
TCP state machine (stateful) PoW challenge (SHA256)
SYN throttle per-IP MC handshake парсинг
IP blacklist (LPM_TRIE) HMAC подпись hostname
Invalid TCP flags drop Rate limit (connections/sec)
UDP drop Death code auto-ban
Per-connection seq tracking GeoIP/ASN lookup
bpf_timer idle cleanup Blacklist (сложные правила)
```
### ADR-005: Pluggable BPF hooks instead of hardcoded Minecraft states
XDP **не может**: SHA256, HMAC, floating point, heap allocation, сложные строки.
Всё L7 — только в Rust userspace.
**Context.** The XDP program contained a Minecraft-specific TCP state machine:
`AWAIT_ACK → AWAIT_MC_HANDSHAKE → AWAIT_LOGIN → verified`. This made Layer 1
unusable for any non-Minecraft traffic.
## ADR-001: Rust для Edge Core
**Decision.** The XDP filter keeps only **generic, protocol-agnostic** logic:
a universal TCP lifecycle state machine, SYN throttle, CIDR black/white lists,
TCP-flag sanity checks, and configurable UDP policy. Deep protocol parsing moves
into **separate BPF hook modules** attached to the main filter at load time.
Each hook can inspect payload after the TCP header and return allow/drop/skip.
**Решение:** Rust + tokio
**Альтернативы:** Go (GC паузы), C (небезопасен), Java (память)
**Причина:** Zero-cost abstractions, memory safety, нет GC, libbpf-rs
**Consequences.**
## ADR-002: Redis как хранилище состояния
- (+) Layer 1 works for arbitrary TCP services out of the box.
- (+) Hook modules keep the main filter small, auditable, and verifiable.
- (−) Hook attachment adds a map-dispatch indirection on the hot path; measured
cost is acceptable, but the full benchmark suite is still in progress.
- (−) Hooks are more constrained than userspace parsing (no loops without
bounded verification); anything complex stays in Layer 3.
**Решение:** Redis + локальный кэш на edge нодах
**Оговорка:** При падении Redis — edge работает с кэшем, Velocity с кэшем серверов
**Масштаб:** Redis Cluster при 1000+ серверов, Redis Sentinel для HA
### ADR-006: Remove Java layers (Velocity/Paper) and React dashboard
## ADR-003: NATS для критических событий
**Context.** Layers 4–5 were Java services: a Velocity proxy performing domain
checks, HMAC verification, physics/CAPTCHA challenges, plus a Paper agent doing
Redis heartbeat and server auto-registration. A React dashboard covered the
control plane UI.
**Решение:** NATS JetStream для blacklist updates, attack events, audit log
**Причина:** Redis Pub/Sub — fire-and-forget, NATS — at-least-once delivery
**Decision.** All Java components and the dashboard are removed from the
repository. Consumers integrate with Rampart through the **Manager API**
(axum + Redis sync) instead of embedding proxy-side Java plugins. See
[Migration notes](#migration-notes).
**Consequences.**
- (+) Universality: Rampart no longer assumes Minecraft or the JVM at all;
it protects whatever sits behind it.
- (+) Dramatically smaller deployment surface: edge nodes are pure Rust + C.
- (−) Capabilities unique to the Java layers (physics checks, in-game CAPTCHA)
are gone; where needed they become responsibilities of consumer-side plugins
built against the Manager API.
- (−) Existing Velocity/Paper deployments must migrate or stay on pre-redesign
git history.
### ADR-007: Universal PoW stays, off by default
**Context.** The SHA256 hashcash challenge previously assumed a Minecraft
text-challenge flow, which vanilla clients could not solve. In the universal
platform the challenge is redefined to work over any TCP protocol.
**Decision.** Keep the PoW layer as a core capability, redesigned as a
protocol-independent challenge, but ship it **disabled by default**
(`pow.enabled = false`). Operators enable it when their client ecosystem can
answer the challenge (custom clients, modded protocols, HTTP integrations).
**Consequences.**
- (+) Anti-handshake-flood capability remains available platform-wide.
- (+) Default configuration never breaks clients that cannot solve PoW.
- (−) Out of the box, handshake floods are mitigated only by Layer 1 throttling
and Layer 3 rate limits until PoW is explicitly enabled.
### ADR-008: Bilingual knowledge base as documentation-first strategy
**Context.** Operational experience (attack anatomy, defense levels, practice
guides) was scattered across research notes, runbooks, and chat history. New
operators repeatedly rediscovered the same lessons.
**Decision.** Maintain `docs/kb/` as a curated, bilingual (EN/RU) knowledge
base: attack anatomy, defense level explanations, and practice guides.
Documentation-first: every new attack class or defense mechanism gets a KB
entry as part of the change, not after the fact.
**Consequences.**
- (+) Onboarding cost drops; operational decisions cite KB articles.
- (+) EN/RU mirroring serves both the international audience and the original
Russian-speaking operator community.
- (−) Bilingual maintenance doubles writing effort; entries may temporarily lag
in one language.
## Migration notes
**Removed from the repository** (recoverable from git history):
- `plugins/` — Java Velocity plugin (domain whitelist, physics checks, CAPTCHA,
TPS-aware routing) and Paper agent (Redis heartbeat, auto-registration).
- `velocity/`, `paper/` — build scaffolding for the Java layers.
- `dashboard/` — React + Vite + TypeScript control plane UI.
All removed sources remain accessible in git history
(`git log --follow -- plugins/ velocity/ paper/ dashboard/`).
**Moved into the plugin crate:**
- Minecraft handshake parsing (VarInt, bounds checks) from the Rust core →
`minecraft` protocol plugin crate.
- Minecraft-specific connection states in the XDP state machine → replaced by
the universal state machine; MC-specific deep parsing will reappear as a BPF
hook module (planned).
**Kept in place:**
- XDP/eBPF generic filtering (maps, LPM trie, throttle timers).
- SHA256 hashcash PoW engine (redesigned to be protocol-agnostic, off by default).
- Rate limiting, HMAC, death-code pattern matching in the userspace core.
- Traffic Intel (EWMA thresholds, profiling, reputation).
- Manager API (axum) + Redis sync, CLI.
**Integration path for former Java-layer consumers:** talk to rampart-manager's
HTTP API for server registration, blacklists, and event streams. There is no
in-process Java integration anymore.
## Requirements (edge node)
| Item | Requirement |
|------|-------------|
| Virtualization | KVM / Bare Metal (XDP does not work under OpenVZ/LXC; check `systemd-detect-virt`) |
| Kernel | 5.10+ |
| CPU/RAM | 2–4 vCPU, 2–4 GB |
| XDP drivers | native: Intel i40e, Mellanox ConnectX; virtio falls back to generic mode |
## Historical ADRs (pre-redesign, still valid)
- **ADR-001: Rust for the edge core.** Rust + tokio over Go (GC pauses),
C (memory safety), Java (footprint). Zero-cost abstractions + libbpf-rs.
- **ADR-002: Redis as state store.** Redis + local cache on edge nodes; edges
keep operating from cache if Redis is down; Redis Cluster/Sentinel for scale/HA.
- **ADR-003: NATS for critical events.** NATS JetStream for blacklist updates,
attack events, audit log — at-least-once delivery instead of Redis Pub/Sub's
fire-and-forget.
---
## RU summary (краткая выжимка)
Rampart переработан из 6-слойной Minecraft-специфичной защиты в универсальную
платформу сетевой защиты уровня L3/L4/L7 для любых сервисов. Слоёв стало четыре:
(1) XDP/eBPF в ядре — только протоколо-независимая логика (универсальный TCP
state machine, SYN throttle, CIDR-списки, UDP policy), глубокий парсинг вынесен
в подключаемые BPF hook-модули; (2) универсальный PoW-challenge (SHA256 hashcash)
поверх любого TCP-протокола, выключен по умолчанию; (3) userspace-ядро на Rust —
L7-анализ, rate limit, HMAC, death-code паттерны; (4) компайл-тайм протокол-плагины
(feature crates), первый — minecraft, далее http и grpc. Traffic Intel
(EWMA, профилирование, репутация) работает поперёк всех слоёв. Java-слои
(velocity/paper) и React dashboard удалены — потребители интегрируются через
Manager API (axum + Redis). Документация строится вокруг двуязычной базы знаний
docs/kb. Полные детали миграции и ссылки на git-историю — в Migration notes выше.

View file

@ -1,303 +0,0 @@
# Minecraft Protocol - Парсинг, VarInt, Fingerprinting
> Актуально: v0.1+
> Основа всей фильтрации - знание протокола.
---
## Handshake пакет (0x00) - структура
```
┌──────────────────────────────────────────────────────┐
│ VarInt │ Packet Length │
├──────────────────────────────────────────────────────┤
│ VarInt │ Packet ID = 0x00 │
├──────────────────────────────────────────────────────┤
│ VarInt │ Protocol Version │
│ │ 765 = 1.20.4, 769 = 1.21.4, 766 = 26.1 │
├──────────────────────────────────────────────────────┤
│ String │ Server Address (hostname) │
│ │ VarInt (length) + UTF-8 bytes │
├──────────────────────────────────────────────────────┤
│ UShort │ Server Port (big-endian, 2 bytes) │
├──────────────────────────────────────────────────────┤
│ VarInt │ Next State: 1 = Status, 2 = Login │
└──────────────────────────────────────────────────────┘
```
---
## VarInt - строгий парсер с bounds check
```rust
// minecraft/varint.rs
#[derive(Debug)]
pub enum VarIntError {
Incomplete, // данных меньше чем нужно
TooBig, // VarInt > 5 байт (не по спецификации)
Overflow, // значение выходит за i32
}
pub fn read_varint(buf: &[u8], start: usize) -> Result<(i32, usize), VarIntError> {
let mut value: i32 = 0;
let mut shift = 0;
for (i, &byte) in buf[start..].iter().enumerate() {
if i >= 5 {
// MC VarInt максимум 5 байт - всё что больше: атака
return Err(VarIntError::TooBig);
}
let segment = (byte & 0x7F) as i32;
// Проверяем overflow до сдвига
if shift >= 32 || (shift == 28 && segment > 0x0F) {
return Err(VarIntError::Overflow);
}
value |= segment << shift;
shift += 7;
if (byte & 0x80) == 0 {
return Ok((value, start + i + 1));
}
}
Err(VarIntError::Incomplete)
}
// VarString = VarInt (length) + UTF-8 bytes
pub fn read_string(buf: &[u8], start: usize) -> Result<(String, usize), ParseError> {
let (len, after_len) = read_varint(buf, start)?;
if len < 0 || len > 32767 {
return Err(ParseError::StringTooLong);
}
let end = after_len + len as usize;
if end > buf.len() {
return Err(ParseError::Incomplete);
}
let s = std::str::from_utf8(&buf[after_len..end])
.map_err(|_| ParseError::InvalidUtf8)?
.to_string();
Ok((s, end))
}
```
---
## Полный парсер handshake
```rust
// minecraft/handshake.rs
#[derive(Debug)]
pub struct McHandshake {
pub protocol_version: i32,
pub server_address: String,
pub server_port: u16,
pub next_state: NextState,
}
#[derive(Debug, PartialEq)]
pub enum NextState {
Status, // 1 - ping
Login, // 2 - игрок заходит
Unknown(i32),
}
impl McHandshake {
pub fn parse(buf: &[u8]) -> Result<Self, ParseError> {
let mut pos = 0;
// Packet length (игнорируем значение, просто двигаемся дальше)
let (_, after_len) = read_varint(buf, pos)?;
pos = after_len;
// Packet ID - должен быть 0x00
let (packet_id, after_id) = read_varint(buf, pos)?;
pos = after_id;
if packet_id != 0x00 {
return Err(ParseError::NotHandshake(packet_id));
}
// Protocol version (не валидируем - не хардкодим версии)
let (protocol_version, after_pv) = read_varint(buf, pos)?;
pos = after_pv;
// Server address
let (server_address, after_addr) = read_string(buf, pos)?;
pos = after_addr;
// Защита от слишком длинного hostname
if server_address.len() > 255 {
return Err(ParseError::HostnameTooLong);
}
// Server port (big-endian u16)
if pos + 2 > buf.len() {
return Err(ParseError::Incomplete);
}
let server_port = u16::from_be_bytes([buf[pos], buf[pos + 1]]);
pos += 2;
// Next state
let (next_state_raw, _) = read_varint(buf, pos)?;
let next_state = match next_state_raw {
1 => NextState::Status,
2 => NextState::Login,
n => NextState::Unknown(n),
};
Ok(McHandshake {
protocol_version,
server_address,
server_port,
next_state,
})
}
pub fn is_login(&self) -> bool {
self.next_state == NextState::Login
}
}
```
---
## Hostname суффиксы - Forge, FabricProxy, HMAC
```
Обычный клиент: "play.server.com"
Forge (старый): "play.server.com\0FML\0"
NeoForge/Forge: "play.server.com\0FML2\0"
FabricProxy-Lite: "play.server.com\0" + base64(data)
Наш HMAC: "play.server.com\0shield\0<hex_hmac>"
Комбинации:
Forge + HMAC: "play.server.com\0FML2\0\0shield\0<hex_hmac>"
```
### Правильный порядок разбора
```rust
// ВАЖНО: сначала убираем FML суффикс, потом проверяем HMAC
// Если делать наоборот - HMAC подпись не совпадёт
pub struct ParsedHostname {
pub domain: String, // "play.server.com"
pub forge_marker: Option<String>, // "FML2" если Forge
pub hmac: Option<String>, // hex HMAC если прошли через edge
}
pub fn parse_hostname(raw: &str) -> ParsedHostname {
let parts: Vec<&str> = raw.split('\0').collect();
// Ищем "shield" среди частей
let shield_pos = parts.iter().position(|&p| p == "shield");
// Forge маркер - обычно вторая часть
let forge_marker = parts.get(1)
.filter(|&&p| p == "FML" || p == "FML2" || p == "FML3")
.map(|&s| s.to_string());
ParsedHostname {
domain: parts[0].to_string(),
forge_marker,
hmac: shield_pos.and_then(|i| parts.get(i + 1)).map(|s| s.to_string()),
}
}
```
---
## Client Fingerprinting
### По handshake
```rust
pub enum ClientType {
Vanilla,
NeoForge, // \0FML2\0
Forge, // \0FML\0
FabricProxy, // специфичный base64 суффикс
Bot, // подозрительные паттерны
Unknown,
}
pub fn fingerprint_from_handshake(h: &McHandshake) -> ClientType {
let addr = &h.server_address;
if addr.contains("\0FML2\0") { return ClientType::NeoForge; }
if addr.contains("\0FML\0") { return ClientType::Forge; }
// Очень старый или нестандартный protocol_version
if h.protocol_version < 47 || h.protocol_version > 10000 {
return ClientType::Bot;
}
ClientType::Unknown
}
```
### По plugin channels (после Login)
```rust
// Lunar, Badlion, Feather регистрируют свои каналы через
// LoginPluginRequest / PluginChannels пакет
pub fn fingerprint_from_channels(channels: &[String]) -> Option<ClientType> {
for ch in channels {
if ch.starts_with("lunarclient:") { return Some(ClientType::LunarClient); }
if ch.starts_with("badlion:") { return Some(ClientType::BadlionClient); }
if ch.starts_with("feather:") { return Some(ClientType::FeatherClient); }
if ch.starts_with("pvplounge:") { return Some(ClientType::PvPLounge); }
}
None
}
```
---
## Важные нюансы протокола
```
1. Один TCP коннект = один игрок. MC не мультиплексирует.
2. После Handshake(next_state=2) → LoginStart пакет
Если LoginStart не пришёл за 5 сек → это бот. DROP.
3. Protocol version не хардкодить.
Mojang с 2025 использует новую схему (26.1, 26.2...).
Принимаем любой валидный VarInt в диапазоне 0..10000.
4. Hostname может прийти TCP-фрагментированным (несколько сегментов).
Парсер должен уметь работать с неполными данными - читать пока
не получим полный пакет или timeout.
5. Status ping (next_state=1) - не требует авторизации.
Боты часто используют для разведки (онлайн, версия сервера).
Rate limit status отдельно от login.
6. MC 1.20.2+ использует Configuration phase между Login и Play.
Velocity обрабатывает автоматически - нам не важно для edge.
```
---
## Совместимость версий (июль 2026)
| Версия MC | Protocol version | Схема |
|---|---|---|
| 1.20.4 | 765 | Старая |
| 1.21.1 | 767 | Старая |
| 1.21.4 | 769 | Старая |
| 26.1 | 8xx | Новая (Mojang) |
| 26.2 | 8xx | Новая (Mojang) |
Velocity 3.4+ поддерживает обе схемы прозрачно.
Sonar 3.x поддерживает 1.8 - 26.2.

View file

@ -4,281 +4,155 @@
---
## 1. DDoS атака - пошагово (3 ночи, вы сонный)
## 1. DDoS атака — пошагово
```bash
# ── ШАГ 1: Подтвердить атаку ──
# Открыть Grafana → посмотреть алерты
# Или в CLI:
curl -s http://localhost:9090/api/v1/alerts | jq '.data.alerts[] | select(.state=="firing")'
# Метрики edge ноды
curl -s http://EDGE_IP:9090/metrics | grep -E "rampart_(connections|rate_limit|attack_status)"
# Проверить метрики edge ноды
curl -s http://EDGE_IP:9090/metrics | grep -E "rampart_(connections|rate_limit|blocked)"
# rampart_attack_status: 0 = normal, 1 = suspicious, 2 = under_attack
# (детектор по порогу pps, см. src/traffic/detector.rs)
# ── ШАГ 2: Определить тип атаки ──
# Если CPU < 50% и много DROP → XDP работает, атака L3/L4
# Если CPU > 80% → атака L7 (handshake flood)
# Много SYN без завершения handshake → L3/L4, смотрим kernel/XDP
ss -s
# Проверка XDP счётчиков
cat /sys/kernel/debug/tracing/trace_pipe | head -20
# Много установленных коннектов с малым трафиком → slow-атака (L4)
# Много коннектов/сек с одних IP → connection flood
# ── ШАГ 3: Действия ──
# ── ШАГ 3: Действия через конфиг (правка + рестарт) ──
# A) SYN flood (XDP справляется)
# → просто наблюдаем, XDP дропает на уровне ядра
# → проверить CPU: должен быть < 30%
echo "Наблюдаем, XDP работает"
# A) Connection flood → ужесточить per-IP лимиты в /etc/rampart/config.toml:
# [limits]
# rate_limit_pps = 2.0
# rate_limit_burst = 5.0
# max_connections_per_ip = 5
systemctl restart rampart
# B) Handshake flood (L7)
# → Ужесточить rate limit на лету
rampart config set rate_limit_login_pps 2
rampart config set rate_limit_burst 5
# B) Атака с известных подсетей → внести в blacklist через Manager API или CLI:
rampart-cli blacklist add 1.2.3.4 "ddos source"
rampart-cli blacklist list
# → Включить emergency mode (только whitelist)
rampart emergency --enable
# Это блокирует все IP кроме whitelist (доверенные ASN, verified players)
# C) Доверенные IP (мониторинг, админы) → whitelist в config.toml
# (только IP-адреса), затем systemctl restart rampart
# C) Атака с датацентров
# → Заблокировать ASN
rampart blacklist add asn 16276 # OVH
rampart blacklist add asn 24940 # Hetzner
# → Включить GeoIP фильтр (блокировать страну)
rampart geoip block CN RU
# D) Атака на конкретный протокол
# → Временно заблокировать статус пинги
rampart config set rate_limit_status_pps 0.1
# → Заблокировать старые версии протокола
rampart config set min_protocol_version 765
# D) Повысить стоимость флуда → включить PoW:
# [pow]
# enabled = true
# difficulty = 5
systemctl restart rampart
# ── ШАГ 4: Если не помогает ──
# Включить challenge для ВСЕХ новых подключений
rampart challenge --mode all --type timing
# Emergency mode (через CLI):
rampart-cli emergency enable
# ... и обратно после атаки:
rampart-cli emergency disable
# В крайнем случае - отключить все не-WG порты на edge
systemctl stop rampart-edge
# В крайнем случае — закрыть порт на firewall и разбираться:
iptables -A INPUT -p tcp --dport 25565 -j DROP
# Игроки не заходят, но серверы в безопасности
# Проверить через провайдера: возможно у них есть tools для фильтрации
# ── ШАГ 5: После атаки ──
# Выключить emergency mode
rampart emergency --disable
# Проверить логи в ClickHouse
clickhouse-client --query "
SELECT src_country, count() as attacks
FROM rampart.blocked
WHERE ts > now() - INTERVAL 1 HOUR
GROUP BY src_country
ORDER BY attacks DESC
LIMIT 10
"
# События атак пишутся в ClickHouse (если настроен clickhouse_url):
clickhouse-client --query "SELECT count() FROM rampart.events WHERE event_type='attack'"
# Написать post-mortem
```
---
> Честно: hot-reload конфига не реализован — изменения применяются рестартом.
> ASN/CIDR-баны, GeoIP-блокировки стран и выбор типов challenge через CLI не
> реализованы; blacklist принимает цели в том виде, в котором их хранит Manager.
## 2. Edge нода не стартует
```bash
# 1. Проверить статус
systemctl status rampart-edge
systemctl status rampart
journalctl -u rampart -n 50 --no-pager
# 2. Логи
journalctl -u rampart-edge -n 50 --no-pager
# Типичные причины:
# 3. Типичные причины:
# A) Порт занят
# A) Порт занят
ss -tlnp | grep 25565
# Решение: сменить порт в /etc/rampart/config.toml
# B) Конфиг не валидный
rampart config validate /etc/rampart/config.toml
# B) Конфиг невалиден — проверь парсингом теста:
cargo test --test config_parse
# Или запусти вручную и прочитай ошибку:
RAMPART_CONFIG=/etc/rampart/config.toml /usr/local/bin/rampart
# C) libbpf не найден (если собрано с XDP)
ldd /usr/local/bin/rampart-core | grep bpf
# Решение: apt-get install libbpf-dev
# C) Нет ни одного protocol handler
# rampart требует зарегистрированный ProtocolHandler (feature protocol-http
# или внешний крейт). Пока плагинов нет — edge в исследовательском режиме.
# D) Нет прав на BPF
# Решение: sudo setcap cap_bpf+ep /usr/local/bin/rampart-core
# 4. Запуск вручную (для диагностики)
/usr/local/bin/rampart-core --config /etc/rampart/config.toml --verbose
# D) XDP не загрузился
journalctl -u rampart | grep -i xdp
# Временное решение: [xdp] enabled = false в config.toml
```
---
## 3. XDP не загружается
```bash
# 1. Проверить виртуализацию
systemd-detect-virt
# openvz/lxc → XDP не работает. Сменить провайдера.
# 2. Проверить версию ядра
uname -r
# < 5.10 → обновить ядро
# 3. Проверить драйвер
systemd-detect-virt # openvz/lxc → XDP не работает, нужен KVM
uname -r # нужно 5.10+
ethtool -i eth0 | grep driver
# virtio → только generic mode
# i40e/mlx5 → native mode
# 4. Проверить XDP поддержку
sudo ip link set dev eth0 xdp off 2>&1
# "Operation not supported" → XDP не поддерживается
# Проверить компиляцию BPF-программы:
clang -O2 -g -target bpf -c xdp/core/universal_filter.c -o /tmp/universal_filter.o
# 5. Решение: отключить XDP в config.toml
# [xdp]
# enabled = false
# И перезапустить edge
systemctl restart rampart-edge
# Отключить XDP:
# [xdp] enabled = false → systemctl restart rampart
```
---
## 4. Игроки не могут зайти
## 4. Клиенты не могут подключиться
```bash
# 1. Проверить edge ноду
curl -s http://EDGE_IP:9090/metrics | grep rampart_connections
# Если 0 → edge не принимает соединения
# 2. Проверить что порт открыт
nc -zv EDGE_IP 25565
# 3. Проверить HMAC
# На velocity: /logs/rampart-hmac.log
# "HMAC mismatch" → не совпадает secret
# "Direct IP blocked" → игрок подключился не через edge
# 4. Проверить firewall
nc -zv EDGE_IP 25565 # порт открыт?
iptables -L INPUT -n -v | grep 25565
dig +short your.domain # DNS указывает на edge?
# 5. Проверить DNS
dig +short play.example.com
# Должен показывать IP edge ноды
# 6. Проверить rate limit
# Если игроков много с одного IP (NAT) - превышают лимит
rampart config set max_connections_per_ip 50 # увеличить
# NAT: много клиентов за одним IP упираются в max_connections_per_ip — увеличь.
```
---
## 5. Высокая нагрузка на edge
```bash
# 1. Определить bottleneck
htop -p $(pgrep -d',' rampart)
iftop -i eth0 # pps/bandwidth
# CPU
htop -p $(pgrep -d',' rampart-core)
# Память
ps aux | grep rampart-core
# I/O (если много логов)
iotop
# Сеть (pps, bandwidth)
iftop -i eth0
# 2. Типичные причины:
# A) Не хватает воркеров
# → Увеличить workers = vCPU
rampart config set workers_count $(nproc)
systemctl restart rampart-edge
# B) CPU > 80% от L7 парсинга
# → Включить XDP чтобы разгрузить userspace
# → Уменьшить rate_limit до разумных пределов
# → Проверить что нет SQL injection или других атак (парсинг hostname!)
# C) Утечка памяти
# → Проверить RSS за последние часы
# → Если растёт - включить профилирование
rampart debug pprof
# 3. Временное решение
rampart config set max_connections 50000 # ограничить
# 4. Постоянное решение
# Добавить ещё одну edge ноду
rampart add-node --role edge --name edge-eu-2 --ip 45.200.10.2
# Не хватает воркеров → [workers] count = vCPU, рестарт.
# CPU > 80% от userspace → включи XDP ([xdp] enabled = true),
# чтобы дропать мусор раньше, и ужесточи rate limit.
```
---
## 6. ClickHouse переполнен
```bash
# 1. Проверить дисковое пространство
df -h /var/lib/clickhouse
# 2. Очистить старые партиции (> 90 дней)
clickhouse-client --query "
SELECT partition, formatReadableSize(bytes_on_disk)
FROM system.parts
WHERE table = 'blocked'
ORDER BY partition
"
# Удалить старые
clickhouse-client --query "
ALTER TABLE rampart.blocked DROP PARTITION '2025-01'
"
# 3. Настроить TTL если не сделано
clickhouse-client --query "
ALTER TABLE rampart.blocked
MODIFY TTL ts + INTERVAL 90 DAY
"
# 4. Отключить логирование на время (если совсем плохо)
rampart config set clickhouse_enabled false
# Данные складываются в буфер, не теряются
clickhouse-client --query "ALTER TABLE rampart.events MODIFY TTL ts + INTERVAL 90 DAY"
# Временное отключение записи: закомментируй clickhouse_url в config.toml, рестарт.
```
---
## 7. Краткий справочник команд
```bash
rampart status # Общее состояние системы
rampart doctor # Полная диагностика
rampart config get workers.count # Получить параметр
rampart config set workers.count 4 # Установить параметр (hot reload)
rampart blacklist add 1.2.3.4 # Забанить IP
rampart blacklist add asn 24940 # Забанить ASN
rampart blacklist list # Список забаненных
rampart blacklist remove 1.2.3.4 # Разбанить
rampart whitelist add 10.0.0.0/16 # Добавить в whitelist
rampart emergency --enable # Включить emergency mode
rampart emergency --disable # Выключить
rampart drain edge-eu-1 # Плавно вывести ноду
rampart reload backend # Перезагрузить список бэкендов
rampart pki rotate --role edge # Ротация сертификатов
rampart wg sync # Синхронизация WireGuard
rampart debug pprof # CPU профиль
rampart debug heap # Heap профиль
rampart debug metrics # Prometheus метрики в CLI
rampart-cli status # Общее состояние системы
rampart-cli doctor # Полная диагностика
rampart-cli config get <key> # Получить параметр
rampart-cli config set <key> <value> # Установить параметр
rampart-cli blacklist add <target> [reason]
rampart-cli blacklist remove <target>
rampart-cli blacklist list
rampart-cli emergency enable|disable # Emergency mode
rampart-cli drain <node> # Плавно вывести ноду
```
Полный список: `rampart-cli --help` (источник — src/bin/rampart-cli.rs).
---
*Версия: 1.0 | Июль 2026*
*Версия: 2.0 | Август 2026*

View file

@ -4,265 +4,90 @@
---
## 1. Unit тесты (Rust)
## 1. Unit и integration тесты (Rust)
```bash
# Все тесты
cargo test
# Конкретный модуль
cargo test handshake
cargo test hmac
cargo test rate_limiter
# С выводом
cargo test -- --nocapture
# С профилированием
cargo test --release
cargo test # все тесты
cargo test --test config_parse
cargo test -- --nocapture # с выводом
```
### Что тестировать
Существующие наборы (tests/):
| Модуль | Happy path | Error cases |
|--------|-----------|-------------|
| VarInt parser | обычный, короткий | overflow, incomplete, >5 байт |
| MC Handshake | vanilla, forge, hmac | truncated, invalid utf8, wrong packet id |
| HMAC sign/verify | правильный secret | wrong secret, empty hostname, timing |
| Rate limiter | under limit, reset | over limit, burst, concurrent |
| Blacklist | add/check/remove | expired entry, duplicate add |
| Тест | Что покрывает |
|------|---------------|
| `config_parse` | Парсинг и валидация единого конфига (src/config) |
| `filter_logic` | Blacklist, rate limit, geo-фильтры |
| `pow_roundtrip` | Выдача/решение/проверка PoW challenge |
| `protocol_registry` | Реестр протокольных плагинов |
### Пример: VarInt
---
```rust
#[test]
fn test_varint_normal() {
let buf = vec![0x00];
assert_eq!(read_varint(&buf, 0).unwrap(), (0, 1));
}
## 2. Статический анализ
#[test]
fn test_varint_max() {
let buf = vec![0xFF, 0xFF, 0xFF, 0xFF, 0x07];
assert_eq!(read_varint(&buf, 0).unwrap(), (i32::MAX, 5));
}
```bash
cargo fmt --all --check
cargo clippy --all-targets --all-features -- -D warnings
cargo deny check
```
#[test]
fn test_varint_overflow() {
let buf = vec![0xFF, 0xFF, 0xFF, 0xFF, 0x0F]; // > 5 байт
assert!(matches!(read_varint(&buf, 0), Err(VarIntError::TooBig)));
}
XDP: smoke-check компиляции BPF-программы:
#[test]
fn test_varint_incomplete() {
let buf = vec![0x80]; // ждём ещё байты
assert!(matches!(read_varint(&buf, 0), Err(VarIntError::Incomplete)));
}
```bash
clang -O2 -g -target bpf -c xdp/core/universal_filter.c -o /tmp/universal_filter.o
```
---
## 2. Интеграционные тесты
## 3. Локальный integration-стенд (Docker)
```bash
# Требуют: docker compose up (redis, clickhouse)
cargo test --test integration
bash deploy/test/run_test.sh
```
### Что тестируем
```rust
#[tokio::test]
async fn test_full_flow() {
// 1. Запускаем edge ноду (test config)
// 2. Подключаемся Minecraft клиентом (через tokio::net::TcpStream)
// 3. Шлём валидный handshake
// 4. Проверяем что HMAC добавлен
// 5. Проверяем что трафик проксирован до backend
}
#[tokio::test]
async fn test_blacklist_sync() {
// 1. Добавляем IP в блэклист через Redis
// 2. Проверяем что edge нода его подхватила
// 3. Пытаемся подключиться с забаненного IP
// 4. Проверяем что соединение отклонено
}
```
---
## 3. Fuzzing
```rust
// tests/fuzz/handshake.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
// Должен крашиться на любой вход
let _ = McHandshake::parse(data);
});
```
```bash
cargo install cargo-fuzz
cargo fuzz run handshake_parser
```
Поднимает backend-stub (TCP echo), edge (`rampart` с config.test.toml) и
attacker-контейнер. Сценарии — в [deploy/test/README.md](../deploy/test/README.md).
---
## 4. Нагрузочное тестирование
### Базовый тест (tcpkali)
### Много-IP TCP flood на VDS (edge-only)
`deploy/test/stress/` — полный цикл без Redis/ClickHouse: edge-контейнер с
stub-бэкендом (socat echo) + attacker-контейнер со 100 source IP.
`flood.py` шлёт чистые TCP-соединения (connect / slowloris / keepalive),
во время флуда параллельно подключаются generic TCP-клиенты (`legit.py`),
замеряющие RTT.
```bash
# Установка
cargo install tcpkali
# 50k новых соединений
tcpkali \
--connections 1000 \
--connect-rate 5000 \
--duration 60s \
EDGE_IP:25565
# 500 активных соединений с трафиком
tcpkali \
--connections 500 \
--connect-rate 100 \
--duration 120s \
--message-rate 1 \
--message "$(xxd mc_handshake.bin)" \
EDGE_IP:25565
```
### SYN flood (hping3)
```bash
# Только на свои серверы!
hping3 -S --flood -p 25565 EDGE_IP
# С рандомным src IP
hping3 -S --flood -p 25565 --rand-source EDGE_IP
```
### Реальные Minecraft боты (SoulFire)
```bash
java -jar SoulFire.jar \
--target play.example.com:25565 \
--amount 200 \
--join-delay 50 \
--protocol-version 765
```
---
## 5. DDoS simulation
```bash
# Сценарий 1: SYN flood
# Ожидание: XDP дропает, CPU < 30%
hping3 -S --flood -p 25565 EDGE_IP
# Сценарий 2: Handshake flood
# Ожидание: rate limit блокирует, CPU < 60%
for i in $(seq 1 1000); do
(echo -n "$MC_HANDSHAKE" | nc -w1 EDGE_IP 25565) &
done
# Сценарий 3: Slowloris
# Ожидание: timeout 5 сек, соединение закрывается
while true; do
echo -n -e '\x01' | nc -w 10 EDGE_IP 25565
done
# Сценарий 4: Fragmented handshake
# Ожидание: буферизация, успешный парсинг
# (отправляем handshake по 1 байту с задержкой 100ms)
```
### Готовый много-IP стресс-тест на VDS (edge-only)
`deploy/test/stress/` — полный цикл без Redis/Velocity/Paper: edge-контейнер с stub-бэкендом
(socat echo) + attacker-контейнер со 100 source IP. Атака **маскируется под обычный трафик**
(валидные handshake со случайными hostname), во время флуда параллельно заходят легитимные
клиенты (`legit.py`), замеряющие RTT.
```bash
# На VDS
git clone https://github.com/loki5512344/rampart.git && cd rampart
cargo build --release --bin rampart-core
cp target/release/rampart-core deploy/test/stress/edge-ctx/rampart-core
cargo build --release --bin rampart
cd deploy/test/stress && bash run-stress.sh
```
Фазы: A — сырая пропускная способность (лимиты 100k), B — защита (дефолт 5 pps/IP),
C — SYN flood, D — активные соединения. Результаты прогона 2026-08-04 —
в [load-test-report.md](research/load-test-report.md).
Фазы: A — сырая пропускная способность (лимиты сняты), B — защита (per-IP rate
limit + reputation bans), C — SYN flood hping3, D — активные соединения.
---
## 6. CI Pipeline
```yaml
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: cargo test
- run: cargo clippy -- -D warnings
- run: cargo fmt --check
integration:
runs-on: ubuntu-latest
services:
redis:
image: redis:7-alpine
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- run: cargo test --test integration
fuzz:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: cargo fuzz run handshake_parser -- -runs=100000
bench:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: cargo bench
```
---
## 7. Метрики качества
### tcpkali / hping3
```bash
# Покрытие кода
cargo install cargo-tarpaulin
cargo tarpaulin --out Html
open tarpaulin-report.html
# Цели:
# core/handshake.rs: > 95%
# core/hmac.rs: > 90%
# core/rate_limit: > 85%
# xdp/: тесты в изолированной среде
tcpkali --connections 1000 --connect-rate 5000 --duration 60s EDGE_IP:25565
hping3 -S --flood -p 25565 --rand-source EDGE_IP # только на свои серверы!
```
---
*Версия: 1.0 | Июль 2026*
## 5. Метрики для проверки во время теста
```bash
curl -s http://localhost:9090/metrics | grep rampart_
# rampart_connections_total{result="allowed|blocked"}
# rampart_rate_limit_hits
# rampart_pow_challenges_total{result=...}
# rampart_attack_status
```
---
*Версия: 2.0 | Август 2026*

View file

@ -7,181 +7,67 @@
### "XDP не загружается"
```bash
# Проверяем виртуализацию
systemd-detect-virt
# openvz / lxc -> XDP не работает, нужен KVM
systemd-detect-virt # openvz/lxc → XDP не работает, нужен KVM
uname -r # нужно 5.10+
dpkg -l | grep libbpf # libbpf-dev нужен при сборке
# Проверяем ядро
uname -r
# Нужно 5.10+
journalctl -u rampart | grep -i "xdp\|ebpf\|bpf"
# Проверяем зависимости
dpkg -l | grep libbpf
# libbpf-dev должен быть установлен
# Проверить компиляцию BPF-программы вручную:
clang -O2 -g -target bpf -c xdp/core/universal_filter.c -o /tmp/uf.o
# Смотрим ошибку загрузки
journalctl -u rampart-edge | grep -i "xdp\|ebpf\|bpf"
# Если драйвер не поддерживает native - fallback на generic
# В конфиге:
[xdp]
mode = "generic" # вместо "native" или "auto"
# Не помогло — отключи XDP в config.toml ([xdp] enabled = false) и рестарт.
```
### "Edge не коннектится к Manager"
### "Rate limit блокирует реальных клиентов"
```bash
# Проверяем WireGuard
ping 10.0.0.1
# Нет ответа -> WireGuard не работает
# Смотрим метрики
curl -s http://EDGE_IP:9090/metrics | grep rate_limit
wg show
# Смотрим peer Manager - есть ли last handshake?
# Нет handshake -> проблема с ключами или firewall у Manager
# Проверяем firewall на Manager
ssh root@MANAGER_IP 'iptables -L INPUT -n | grep 51820'
# Должно быть правило ACCEPT для UDP 51820
# Проверяем что Manager слушает
ssh root@MANAGER_IP 'ss -ulnp | grep 51820'
# Пересоздаём WireGuard handshake
wg set wg0 peer MANAGER_PUBKEY endpoint MANAGER_IP:51820
```
### "Rate limit блокирует реальных игроков"
```bash
# Симптом: игроки жалуются что не могут зайти
# Смотрим кого блокируем
journalctl -u rampart-edge | grep "RATE_LIMIT" | tail -50
# Если блокируем целые подсети мобильных операторов (NAT):
# Увеличиваем лимит для мобильных ASN
rampart config set rate_limit.mobile_multiplier 3.0
# Или поднимаем общий лимит
rampart config set rate_limit.max_connections_per_ip 10
rampart config reload
# Клиенты за NAT (один IP — много людей) → увеличь лимиты в config.toml:
# [limits] max_connections_per_ip = 50
# Или добавь их адрес в whitelist (только IP), рестарт.
```
### "Высокое CPU на edge ноде"
```bash
# Смотрим что жрёт CPU
top -p $(pgrep rampart-edge)
# Профилируем
perf top -p $(pgrep rampart-edge)
top -p $(pgrep rampart)
perf top -p $(pgrep rampart)
# Частые причины:
# 1. Слишком много активных соединений -> включить XDP чтобы дропать раньше
# 2. HMAC считается для каждого пакета -> норма, так и должно быть
# 3. GeoIP lookup медленный -> включить кэш
[geo]
cache_size = 100000
cache_ttl_secs = 3600
# 1. Много мусорных коннектов в userspace → включи XDP ([xdp] enabled = true)
# 2. Заниженные rate limit → соединения постоянно рвутся и пересоздаются
# 3. Не хватает воркеров → [workers] count = числу vCPU
```
### "Edge не стартует: no protocol plugins compiled"
Реестр протоколов пуст: `rampart` требует хотя бы один ProtocolHandler
(feature `protocol-http` или внешний крейт-плагин). Пока плагинов нет,
edge запускается только в исследовательских целях. См. src/bin/rampart.rs.
---
## Velocity плагин
## Manager
### "Velocity не видит серверы"
### "Manager не стартует: JWT_SECRET must be set"
```bash
# В логах Velocity ищем:
grep -i "rampart\|registry\|redis" /opt/velocity/logs/latest.log
# Частые причины:
# 1. Redis недоступен
redis-cli -h 10.0.0.1 -a $REDIS_PASSWORD ping
# Connection refused -> Redis не слушает на WireGuard IP
# 2. Неверный пароль Redis
# В config.yml проверяем redis.password
# 3. Velocity не в WireGuard сети
ping 10.0.0.1 # с ноды Velocity
# Нет ответа -> настраиваем WireGuard
# 4. Серверы не зарегистрированы (Paper агент не запущен)
redis-cli -h 10.0.0.1 -a $REDIS_PASSWORD keys "rampart:servers:*"
# Пустой ответ -> Paper агент не работает
export JWT_SECRET="$(openssl rand -hex 32)" # минимум 32 байта
export API_PASSWORD="сильный_пароль" # 'changeme' запрещён
export REDIS_URL="redis://127.0.0.1:6379/0" # опционально
rampart-manager
```
### "Игроков не пускает - 'Подключение по IP запрещено'"
### "Manager отдаёт 401"
Токен истёк (`JWT_EXPIRATION_SECS`, дефолт 86400) — перелогинься:
```bash
# Это нормально если игрок подключается по IP, а не домену
# Проверяем что DNS работает:
nslookup play.yourserver.com
# Должен вернуть IP edge ноды
# Если игрок подключается через домен и всё равно кикает:
# Проверяем что edge HMAC совпадает с Velocity
# На Velocity смотрим логи:
grep "HMAC\|shield" /opt/velocity/logs/latest.log
# Частые причины:
# 1. Разные HMAC секреты на edge и Velocity
# Сравниваем:
cat /etc/rampart/config.toml | grep hmac_secret
grep RAMPART_HMAC_SECRET /opt/velocity/velocity.conf
# 2. Edge нода не добавляет HMAC (add_hmac_header = false)
# В /etc/rampart/config.toml:
[shield]
add_hmac_header = true
```
### "Игрок попадает не на тот сервер"
```bash
# Проверяем стратегию балансировщика
grep "strategy" /opt/velocity/plugins/rampart/config.yml
# Смотрим онлайн по серверам
rampart server list
# Если сервер переполнен но всё равно получает игроков:
# Проверяем что Paper агент обновляет онлайн
redis-cli -h 10.0.0.1 -a $REDIS_PASSWORD \
GET rampart:servers:hub_1
# В JSON смотрим "online" - должно обновляться
```
---
## Paper агент
### "Агент не регистрирует сервер"
```bash
# В логах Minecraft сервера:
grep -i "rampart\|shield agent" /opt/minecraft/logs/latest.log
# Частые причины:
# 1. Redis недоступен с этой ноды
redis-cli -h 10.0.0.1 -a $REDIS_PASSWORD ping
# 2. Неверный IP в конфиге (указан публичный вместо WireGuard)
# Проверить env RAMPART_SERVER_IP
# Должен быть 10.0.x.x (WireGuard IP)
# 3. Дублирующееся имя сервера
redis-cli -h 10.0.0.1 -a $REDIS_PASSWORD \
keys "rampart:servers:*"
# Если имя уже есть - изменить RAMPART_SERVER_NAME
# 4. Агент не установлен
ls /opt/minecraft/plugins/ | grep rampart-paper
# Должен быть .jar файл
curl -s -X POST localhost:8080/api/v1/auth/login \
-H 'Content-Type: application/json' -d '{"password":"..."}'
```
---
@ -191,78 +77,20 @@ ls /opt/minecraft/plugins/ | grep rampart-paper
### "Redis падает с OOM"
```bash
# Проверяем использование памяти
redis-cli -a $REDIS_PASSWORD INFO memory | grep used_memory_human
# Настраиваем eviction policy
redis-cli -a $REDIS_PASSWORD CONFIG SET maxmemory 2gb
redis-cli -a $REDIS_PASSWORD CONFIG SET maxmemory-policy allkeys-lru
# Смотрим что занимает место
redis-cli -a $REDIS_PASSWORD --bigkeys
redis-cli INFO memory | grep used_memory_human
redis-cli CONFIG SET maxmemory 2gb
redis-cli CONFIG SET maxmemory-policy allkeys-lru
```
### "Redis медленно отвечает"
### "Edge не синхронизирует блэклист через Redis"
```bash
# Запускаем latency monitor
redis-cli -a $REDIS_PASSWORD --latency-history -i 1
# Смотрим slowlog
redis-cli -a $REDIS_PASSWORD SLOWLOG GET 10
# Частые причины:
# 1. KEYS команда (блокирует) -> заменить на SCAN
# 2. Нет persistent connection pool -> Jedis pool в Velocity плагине
# 3. Сеть: проверяем пинг от Velocity до Redis через WireGuard
ping 10.0.0.1
# redis_url задан и не пустой? Сборка с feature store-redis (дефолт)?
grep redis_url /etc/rampart/config.toml
journalctl -u rampart | grep -i redis
```
---
## WireGuard
### "Ноды не видят друг друга"
```bash
# На каждой ноде
wg show
# Смотрим:
# - есть ли peer с нужным PublicKey
# - есть ли "latest handshake" (должен быть свежий)
# - endpoint правильный
# Если нет handshake:
# 1. Проверяем что Manager слушает UDP 51820
ss -ulnp | grep 51820
# 2. Проверяем firewall на Manager
iptables -L INPUT -n | grep 51820
# 3. Проверяем что ключи правильные
wg pubkey < /etc/wireguard/private.key
# Должен совпасть с PublicKey у peer на Manager
# Форс рестарт
systemctl restart wg-quick@wg0
```
### "Высокий пинг через WireGuard"
```bash
# Измеряем
ping 10.0.0.1
# Нормально: < 5 мс внутри датацентра, < 50 мс между регионами
# Если > 200 мс -> проблема с маршрутизацией
traceroute 10.0.0.1
# MTU проблема (фрагментация):
ping -M do -s 1400 10.0.0.1
# Если drops -> MTU слишком большой
# В /etc/wireguard/wg0.conf добавить:
MTU = 1380
```
Без Redis edge работает автономно на локальном кэше блэклиста.
---
@ -271,57 +99,27 @@ MTU = 1380
### "ClickHouse не принимает данные"
```bash
# Проверяем что запущен
systemctl status clickhouse-server
# или
docker compose ps rampart-clickhouse
# Проверяем таблицы
curl http://localhost:8123/ping
clickhouse-client --query "SHOW TABLES FROM rampart"
# Проверяем ошибки вставки в логах Manager
journalctl -u rampart-manager | grep -i "clickhouse\|insert"
# Частые причины:
# 1. Таблица не создана -> запускаем миграции
rampart db migrate
# 2. Нет места на диске
df -h
# ClickHouse хранит в /var/lib/clickhouse/
# 3. Неверная схема (после обновления)
clickhouse-client --query "DESCRIBE TABLE rampart.blocked"
df -h /var/lib/clickhouse
```
Запись настраивается `store.clickhouse_url` в config.toml. Ошибки записи не
роняют edge — они уходят в debug-лог.
---
## Общая диагностика
```bash
# Полная проверка системы одной командой
rampart doctor
# Что проверяет:
# ✅ WireGuard туннели
# ✅ Redis доступность
# ✅ NATS доступность
# ✅ Manager API
# ✅ Все edge ноды online
# ✅ Все Velocity ноды online
# ✅ Хотя бы один Hub онлайн
# ✅ HMAC секрет одинаковый везде
# ✅ Сертификаты не истекают в ближайшие 30 дней
# ✅ Redis память < 80%
# ✅ Место на дисках > 20%
# Вывод:
# [OK] Redis: 10.0.0.1:6379
# [OK] Manager API: /api/health
# [WARN] Edge eu-1: last seen 45 sec ago (порог 30 сек)
# [FAIL] Hub_5: не зарегистрирован в Redis
rampart-cli doctor # полная диагностика через Manager API
rampart-cli status # краткий статус
```
> Честно: набор проверок `doctor` ограничен тем, что реализовано в CLI
> (см. src/cli/commands/doctor.rs) и доступно Manager API.
---
*Версия: 1.0 | Июль 2026*
*Версия: 2.0 | Август 2026*

View file

@ -1,7 +1,7 @@
# VDS Compatibility - Rampart
> Совместимость VDS провайдеров с XDP/eBPF, io_uring и WireGuard.
> Обновляется: Июль 2026
> Совместимость VDS провайдеров с XDP/eBPF.
> Обновляется: Август 2026
---
@ -9,60 +9,14 @@
Некоторые провайдеры используют виртуализацию, которая **не поддерживает XDP**:
| Тип виртуализации | XDP Native | XDP Generic | io_uring | Рекомендация |
|---|---|---|---|---|
| **KVM** | ✅ (зависит от драйвера) | ✅ | ✅ | Лучший выбор |
| **Bare Metal** | ✅ | ✅ | ✅ | Идеально для edge |
| **VMware** | ❌ | ✅ | ✅ | Приемлемо |
| **Hyper-V** | ❌ | ✅ | ✅ | Приемлемо |
| **OpenVZ / LXC** | ❌ | ❌ | ❌ | **НЕ ИСПОЛЬЗОВАТЬ** для edge |
| Тип виртуализации | XDP Native | XDP Generic | Рекомендация |
|---|---|---|---|
| **KVM** | ✅ (зависит от драйвера) | ✅ | Лучший выбор |
| **Bare Metal** | ✅ | ✅ | Идеально для edge |
| **VMware / Hyper-V** | ❌ | ✅ | Приемлемо |
| **OpenVZ / LXC** | ❌ | ❌ | **НЕ ИСПОЛЬЗОВАТЬ** для edge |
> ⚠️ **OpenVZ/LXC контейнеры не поддерживают XDP и io_uring.**
> Если купите VDS за $3 у OVH - XDP не заведётся.
---
## Таблица провайдеров
### Edge нода (требует XDP)
| Провайдер | План | Виртуализация | XDP Native | XDP Generic | Цена/мес | Примечание |
|---|---|---|---|---|---|---|
| **Hetzner** | CX22 (2vCPU, 4GB) | KVM | ❌ (virtio) | ✅ | €4.5 | Отличный entry-level |
| **Hetzner** | CPX21 (3vCPU, 4GB) | KVM | ✅ (i40e) | ✅ | €6.9 | Рекомендуется |
| **Hetzner** | AX102 (8vCPU, 32GB) | Bare Metal | ✅ | ✅ | €35 | Для крупных нод |
| **Contabo** | Cloud VPS S (4vCPU, 8GB) | KVM | ❌ | ✅ | €5.0 | Бюджетно, но CPU слабее |
| **Vultr** | High Frequency (2vCPU, 4GB) | KVM | ✅ | ✅ | $12 | Хорошая сеть |
| **Vultr** | Regular (2vCPU, 4GB) | KVM | ❌ (virtio) | ✅ | $6 | Базовый вариант |
| **OVHcloud** | VPS Value (2vCPU, 4GB) | KVM | ❌ | ✅ | €3.5 | Бюджетно |
| **OVHcloud** | VPS Elite (4vCPU, 8GB) | KVM | ✅ | ✅ | €15 | Рекомендуется |
| **OVHcloud** | Bare Metal Game (4vCPU, 32GB) | Bare Metal | ✅ | ✅ | €30 | Для game серверов |
| **DigitalOcean** | Premium (2vCPU, 4GB) | KVM | ❌ | ✅ | $12 | Стабильно, но дороже |
| **Linode** | Dedicated CPU (4vCPU, 8GB) | KVM | ✅ | ✅ | $36 | Дороговато для edge |
| **Scaleway** | DEV1-L (4vCPU, 8GB) | KVM | ❌ (virtio) | ✅ | €11 | - |
| **AWS** | c6i.large (2vCPU, 4GB) | Nitro KVM | ✅ (ena) | ✅ | ~$24 | Дорого, сложный network |
| **Google Cloud** | e2-standard-2 (2vCPU, 4GB) | KVM | ❌ | ✅ | ~$17 | - |
> ✅ = Подтверждено работает
> ❌ = Не поддерживается драйвером
### Manager / Load Balancer (XDP не нужен)
Для Manager, HAProxy, Rust LB подойдёт **любой KVM VDS** с 2 vCPU. XDP не требуется.
| Провайдер | План | Цена/мес |
|---|---|---|
| Hetzner CX22 | 2vCPU, 4GB | €4.5 |
| Contabo VPS S | 4vCPU, 8GB | €5.0 |
| OVH VPS Value | 2vCPU, 4GB | €3.5 |
### Game серверы (Minecraft)
| Провайдер | План | RAM | Цена/мес | Примечание |
|---|---|---|---|---|
| Hetzner AX102 | Bare Metal, 8vCPU | 32GB | €35 | Лучшее соотношение |
| OVH Game | 4vCPU | 32GB | €30 | Оптимизирован для игр |
| Localhost | Dedicated | 64GB+ | - | Лучшая производительность |
> ⚠️ OpenVZ/LXC не поддерживают XDP. Дешёвая VDS за $3 — XDP не заведётся.
---
@ -82,66 +36,37 @@ uname -r
# 4. XDP доступность
sudo ip link set dev eth0 xdp off 2>&1 || echo "XDP не поддерживается"
# 5. io_uring доступность
cat /proc/sys/kernel/io_uring_disabled
# 0 = OK, 1 = только root, 2 = заблокирован
```
---
## Рекомендуемые конфигурации
## Провайдеры
### Для старта (v0.1, до 500 игроков)
| Провайдер | План | Виртуализация | XDP Native | Цена/мес |
|---|---|---|---|---|
| Hetzner | CPX21 (3vCPU, 4GB) | KVM | ✅ (i40e на AX) | €6.9 |
| Hetzner | AX102 (bare metal) | — | ✅ | €35 |
| Vultr | High Frequency | KVM | ✅ | $12 |
| OVHcloud | VPS Elite | KVM | ✅ | €15 |
| Contabo | Cloud VPS S | KVM | ❌ (generic only) | €5.0 |
| DigitalOcean | Premium | KVM | ❌ (generic only) | $12 |
```
1 × Hetzner CX22 (€4.5) - Manager + Redis + NATS
1 × Hetzner CX22 (€4.5) - Edge нода (XDP Generic)
1 × Velocity на той же VDS что и Manager
N × Game серверы (ваши существующие)
Итого: ~€9/мес
```
Для Manager подойдёт любой KVM VDS с 2 vCPU — XDP там не нужен.
### Medium (v0.4+, до 5000 игроков)
```
1 × Hetzner CPX31 (€12) - Manager + Redis + NATS + ClickHouse
2 × Hetzner CPX21 (€6.9) - Edge ноды (XDP Native)
2 × Hetzner CX32 (€8) - Velocity
5 × Hetzner AX102 (€35) - Game серверы
Итого: ~€230/мес
```
### Large (v0.6+, до 50000 игроков)
```
1 × Hetzner AX102 (€35) - Manager + NATS + ClickHouse
4 × Hetzner CPX31 (€12) - Rust LB
6 × Hetzner CPX31 (€12) - Edge ноды (XDP Native)
15 × Hetzner CX32 (€8) - Velocity
20 × Hetzner AX102 (€35) - Game серверы
Итого: ~€1100/мес
```
> Таблица ориентировочная: перед покупкой проверь драйвер NIC и ядро командами выше.
---
## Лимиты провайдеров
### Hetzner
- **Traffic:** CX/CPX - 20TB включено, далее €1/TB
- **DDoS Protection:** Встроенная L3/L4 защита (10Gbps blackhole)
- **BGP:** Только на выделенных серверах (AX)
### Contabo
- **Traffic:** Неограничен (512Mbps)
- **DDoS Protection:** Есть, но слабая
- **CPU:** Старшие модели Intel Xeon, но shared
- Traffic: CX/CPX — 20TB включено, далее €1/TB
- DDoS Protection: встроенная L3/L4 защита (10Gbps blackhole)
### OVHcloud
- **VPS:** OpenVZ на старых тарифах - **проверяйте перед покупкой**
- **Game серверы:** Встроенная DDoS защита (up to 1Tbps)
- **BGP:** На Bare Metal
- VPS: старые тарифы бывают на OpenVZ — **проверяй перед покупкой**
- Game-линейка: встроенная DDoS защита до 1Tbps
---
*Версия: 1.0 | Июль 2026*
*Версия: 2.0 | Август 2026*