feat: VDS stress test harness (edge-only) and load-test results

- deploy/test/stress/: multi-IP masked handshake flood (flood.py), legit client
  during attack (legit.py), full orchestration (run-stress.sh), edge Dockerfile
  with socat stub backend and high/defense configs
- docs/research/load-test-report.md: v3 VDS loopback results (2026-08-04)
- README.md: performance table update (4k conn/s raw, 99.6% blocked in defense,
  legit clients RTT 2-6ms during attack)
- docs/testing.md: multi-IP masked stress-test procedure
This commit is contained in:
loki5512344 2026-08-04 19:16:27 +02:00
parent d41bd6e815
commit 0b53ed720b
Signed by: boba
GPG key ID: 253067914055423B
11 changed files with 660 additions and 0 deletions

View file

@ -75,6 +75,20 @@ Tested on Hetzner CX31 (4 vCPU, 8GB, KVM), Ubuntu 22.04, kernel 5.15
Note: Real L7 throughput (handshake + HMAC + rate limit): ~60-70k conn/s (epoll), ~85-95k (io_uring). Note: Real L7 throughput (handshake + HMAC + rate limit): ~60-70k conn/s (epoll), ~85-95k (io_uring).
#### VDS stress test (2026-08-04) — edge-only, loopback
VDS 2 vCPU / 3.8GB / Ubuntu 22.04, Docker bridge. Edge-only (слои 1–3), без Redis/Velocity/Paper.
Атака маскировалась под обычный трафик: 100 source IP, валидные Minecraft handshake.
Подробности: [load-test-report.md](docs/research/load-test-report.md), скрипты: [deploy/test/stress](deploy/test/stress).
| Scenario | Result |
|----------|--------|
| Raw L7 throughput (valid handshake → HMAC → backend) | ~4k conn/s proxied, 100% (121.5k/30s; edge CPU ~179%, 2 cores) |
| Defense vs masked 100-IP flood (default 5 pps/IP) | **99.6% blocked** (528 allowed vs 119,376 blocked), CPU ~32% |
| Legit clients during attack | 5/5 OK, RTT 2.2–5.8ms |
| SYN flood (no XDP) | 0 impact — handled by kernel |
| Active connections | 300 held trivially (CPU ~0%, 7MB); limit is backend/fd, not edge |
### Quick Start ### Quick Start
```bash ```bash

View file

@ -0,0 +1,60 @@
# Стресс-тест Rampart на VDS (без Redis/Velocity/Paper)
Проверяет edge ноду (слои 1–3) на реальном VDS. Не требует Redis, Velocity, Paper или ClickHouse — только `rampart-core` и stub-бэкенд (socat echo) в одном контейнере.
## Топология
```
┌─────────────── host VDS ───────────────┐
│ docker bridge 172.30.0.0/24 │
│ │
│ rampart-edge (172.30.0.2) │
│ ├─ rampart-core :25565 (bind 0.0.0.0)│
│ ├─ socat echo :25566 (127.0.0.1) │ ← stub бэкенд
│ └─ metrics :9090 │
│ │
│ rampart-attacker (172.30.0.3) │
│ └─ 100 source IP (172.30.0.101-200) │ ← flood.py / hping3
└────────────────────────────────────────┘
```
- Метрики на хосте: `curl http://127.0.0.1:9090/metrics`
- Edge слушает на хосте: `127.0.0.1:25565`
## Быстрый старт
```bash
# 1. На VDS: клонировать репозиторий, собрать бинарь
git clone https://github.com/loki5512344/rampart.git && cd rampart
cargo build --release --bin rampart-core
# 2. Запустить весь цикл (setup + 4 фазы)
# run-stress.sh сам найдёт бинарь (target/release), а папку — по себе (переменная DIR опциональна)
cd deploy/test/stress
bash run-stress.sh
```
> Для запуска из другого места задай `DIR` (по умолчанию — папка скрипта).
## Фазы
| Фаза | Конфиг | Что делает | Проверяет |
|------|--------|-----------|-----------|
| A | `edge-high.toml` (лимиты 100k) | флуд валидными handshake с 100 IP | сырую пропускную способность L7 |
| B | `edge-defense.toml` (дефолт 5 pps/IP) | та же атака | rate limit + reputation ban, доступность легитимных клиентов |
| C | — | SYN flood hping3 (rand-source) | поведение без XDP (обрабатывает kernel) |
| D | `edge-high.toml` | 300 keepalive-коннектов | удержание активных соединений |
Во время фаз A и B параллельно подключается `legit.py` (легитимные MC клиенты), меряющий RTT — доказывает, что реальные игроки проходят во время атаки.
## Атака маскируется под обычный трафик
`flood.py` шлёт **валидные** Minecraft handshake (протокол 767, packet id 0x00) со случайными hostname из пула (`play.example.com`, `mc.example.com`, ...) и ждёт ответ бэкенда — на уровне L7 флуд неотличим от легитимного клиента. Различие даёт только per-IP rate limit и репутация.
## Метрики для сбора
```bash
curl -s http://127.0.0.1:9090/metrics | grep -E 'rampart_(connections_total|pow_challenges|attack_status)'
```
См. [load-test-report.md](../../research/load-test-report.md) — результаты прогона на VDS (2026-08-04).

View file

@ -0,0 +1,46 @@
[bind]
address = "0.0.0.0"
port = 25565
[backend]
address = "127.0.0.1"
port = 25566
[hmac]
secret = "test_secret_32_bytes_long_for_integration_test"
[workers]
count = 2
[limits]
handshake_timeout_secs = 5
max_connections_per_ip = 10
rate_limit_status_pps = 2
rate_limit_login_pps = 5
rate_limit_burst = 10
[store]
redis_url = ""
blacklist_cache_ttl_secs = 300
[xdp]
enabled = false
interface = "eth0"
[death_code]
enabled = true
ban_duration_secs = 3600
[metrics]
enabled = true
port = 9090
[logging]
level = "debug"
format = "text"
[pow]
enabled = false
difficulty = 4
whitelist = ["127.0.0.1", "::1"]

View file

@ -0,0 +1,46 @@
[bind]
address = "0.0.0.0"
port = 25565
[backend]
address = "127.0.0.1"
port = 25566
[hmac]
secret = "test_secret_32_bytes_long_for_integration_test"
[workers]
count = 2
[limits]
handshake_timeout_secs = 5
max_connections_per_ip = 100000
rate_limit_status_pps = 100000
rate_limit_login_pps = 100000
rate_limit_burst = 100000
[store]
redis_url = ""
blacklist_cache_ttl_secs = 300
[xdp]
enabled = false
interface = "eth0"
[death_code]
enabled = true
ban_duration_secs = 3600
[metrics]
enabled = true
port = 9090
[logging]
level = "debug"
format = "text"
[pow]
enabled = false
difficulty = 4
whitelist = ["127.0.0.1", "::1"]

View file

@ -0,0 +1,7 @@
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends socat ca-certificates && rm -rf /var/lib/apt/lists/*
COPY rampart-core /usr/local/bin/rampart-core
COPY start.sh /start.sh
RUN chmod +x /start.sh
EXPOSE 25565 9090
CMD ["/start.sh"]

151
deploy/test/stress/flood.py Normal file
View file

@ -0,0 +1,151 @@
#!/usr/bin/env python3
"""Много-IP Minecraft stress generator, маскирующийся под обычный трафик.
Отправляет валидные MC handshake с рандомными hostname из разных source IP
(--ips-start..--ips-end должны быть назначены на eth0 контейнера).
Modes:
connect - TCP connect + close (conn/s flood)
handshake - валидный MC handshake + ждём ответ backend (маскировка под клиента)
status - status-ping handshake (state 1)
slowloris - connect + 1 байт + hold
keepalive - connect + handshake + держим соединение
"""
import argparse
import random
import socket
import struct
import threading
import time
from datetime import datetime
HOSTS = [
"play.example.com",
"mc.example.com",
"lobby.example.com",
"hub.example.com",
"survival.example.com",
"skyblock.example.com",
"bedwars.example.com",
"minigames.example.com",
"vip.example.com",
]
PROTOCOL = 767
def pack_varint(value):
buf = []
while True:
byte = value & 0x7F
value >>= 7
if value:
byte |= 0x80
buf.append(byte)
if not value:
break
return bytes(buf)
def make_handshake(state=2, host=None):
host = host or random.choice(HOSTS)
pkt = bytearray()
pkt.extend(pack_varint(0)) # packet ID 0x00
pkt.extend(pack_varint(PROTOCOL))
pkt.extend(pack_varint(len(host)))
pkt.extend(host.encode())
pkt.extend(struct.pack(">H", 25565))
pkt.extend(pack_varint(state))
return pack_varint(len(pkt)) + bytes(pkt)
def parse_args():
p = argparse.ArgumentParser()
p.add_argument("--target", default="172.30.0.2")
p.add_argument("--port", type=int, default=25565)
p.add_argument(
"--mode",
choices=["connect", "handshake", "status", "slowloris", "keepalive"],
default="handshake",
)
p.add_argument("--duration", type=int, default=30)
p.add_argument("--threads", type=int, default=100)
p.add_argument("--ips-start", type=int, default=101)
p.add_argument("--ips-end", type=int, default=200)
p.add_argument("--ips-base", default="172.30.0.")
p.add_argument("--timeout", type=float, default=5.0)
return p.parse_args()
def main():
args = parse_args()
src_ips = [f"{args.ips_base}{i}" for i in range(args.ips_start, args.ips_end + 1)]
stop = threading.Event()
stats = {"sent": 0, "lock": threading.Lock()}
def worker():
while not stop.is_set():
src = random.choice(src_ips)
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
s.bind((src, 0))
s.settimeout(args.timeout)
s.connect((args.target, args.port))
if args.mode == "handshake":
s.sendall(make_handshake())
try:
s.recv(1)
except socket.timeout:
pass
elif args.mode == "status":
s.sendall(make_handshake(state=1))
try:
s.recv(1)
except socket.timeout:
pass
elif args.mode == "slowloris":
s.sendall(b"\x01")
time.sleep(30)
elif args.mode == "keepalive":
s.sendall(make_handshake())
time.sleep(30)
with stats["lock"]:
stats["sent"] += 1
s.close()
except OSError:
pass
except Exception:
pass
threads = [
threading.Thread(target=worker, daemon=True) for _ in range(args.threads)
]
for t in threads:
t.start()
t0 = time.time()
try:
while time.time() - t0 < args.duration:
time.sleep(1)
with stats["lock"]:
cur = stats["sent"]
print(
f" [{datetime.now():%H:%M:%S}] src_ips={len(src_ips)} sent={cur:8d} rate={cur / max(time.time() - t0, 1):9.1f}/s"
)
except KeyboardInterrupt:
pass
finally:
stop.set()
time.sleep(0.5)
elapsed = max(time.time() - t0, 0.001)
with stats["lock"]:
total = stats["sent"]
print(
f"DONE total={total} avg={total / elapsed:.1f}/s elapsed={elapsed:.1f}s src_ips={len(src_ips)}"
)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""Легитимный Minecraft клиент: подключается ВО ВРЕМЯ DDoS и меряет RTT."""
import argparse
import socket
import struct
import time
def pack_varint(value):
buf = []
while True:
byte = value & 0x7F
value >>= 7
if value:
byte |= 0x80
buf.append(byte)
if not value:
break
return bytes(buf)
def make_handshake(host="play.example.com", port=25565, state=2):
pkt = bytearray()
pkt.extend(pack_varint(0))
pkt.extend(pack_varint(767))
pkt.extend(pack_varint(len(host)))
pkt.extend(host.encode())
pkt.extend(struct.pack(">H", port))
pkt.extend(pack_varint(state))
return pack_varint(len(pkt)) + bytes(pkt)
def main():
p = argparse.ArgumentParser()
p.add_argument("--target", default="127.0.0.1")
p.add_argument("--port", type=int, default=25565)
p.add_argument("--count", type=int, default=6)
p.add_argument("--interval", type=float, default=5.0)
p.add_argument("--timeout", type=float, default=10.0)
p.add_argument("--name", default="legit")
args = p.parse_args()
for i in range(args.count):
t0 = time.time()
s = None
try:
s = socket.create_connection((args.target, args.port), timeout=args.timeout)
s.settimeout(args.timeout)
s.sendall(make_handshake())
resp = s.recv(1)
rtt = (time.time() - t0) * 1000
ok = len(resp) > 0
print(f"[{args.name}#{i}] {'OK' if ok else 'NO_RESP'} rtt={rtt:.1f}ms")
except Exception as e:
rtt = (time.time() - t0) * 1000
print(f"[{args.name}#{i}] FAIL {type(e).__name__}: {e} rtt={rtt:.1f}ms")
finally:
if s:
try:
s.close()
except Exception:
pass
time.sleep(args.interval)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,172 @@
#!/usr/bin/env bash
set -euo pipefail
NET="rampart-stress-net"
SUB="172.30.0.0/24"
EDGE_IP="172.30.0.2"
ATT_IP="172.30.0.3"
DIR="${DIR:-$(cd "$(dirname "$0")" && pwd)}"
IPS_START=101
IPS_END=200
edge_metrics() {
curl -s --max-time 5 http://127.0.0.1:9090/metrics 2>/dev/null | grep -E '^rampart_' || true
}
mget() {
local label="$1"
local val
val=$(edge_metrics | grep -F "$label" | awk '{print $NF}' | head -1)
echo "${val:-0}"
}
edge_cpu() {
docker stats --no-stream --format '{{.CPUPerc}}' rampart-edge 2>/dev/null || echo "?"
}
start_edge() {
local cfg="$1"
docker rm -f rampart-edge 2>/dev/null || true
docker run -d --name rampart-edge --network "$NET" --ip "$EDGE_IP" \
-p 127.0.0.1:25565:25565 -p 127.0.0.1:9090:9090 \
-v "$DIR/configs/$cfg:/etc/rampart/config.toml:ro" \
rampart-edge >/dev/null
for _ in $(seq 1 30); do
if edge_metrics | grep -q rampart_; then return 0; fi
sleep 2
done
echo "ERROR: edge не поднялся" >&2
return 1
}
run_flood_phase() {
local phase="$1" cfg="$2" duration="$3" mode="$4" threads="$5" ips="$6" name="$7"
echo ""
echo "=============================================================="
echo "ФАЗА $phase: $name (cfg=$cfg, duration=${duration}s, mode=$mode, ips=$ips, threads=$threads)"
echo "=============================================================="
start_edge "$cfg"
local ba bb bp
ba=$(mget 'rampart_connections_total{result="allowed"}')
bb=$(mget 'rampart_connections_total{result="blocked"}')
bp=$(mget 'rampart_pow_challenges_total{result="failed"}')
echo "baseline: allowed=$ba blocked=$bb pow_fail=$bp"
docker exec rampart-attacker python3 /flood.py \
--target "$EDGE_IP" --port 25565 --mode "$mode" \
--duration "$duration" --threads "$threads" \
--ips-start "$IPS_START" --ips-end "$ips" \
> /tmp/flood_$phase.log 2>&1 &
local flood_pid=$!
python3 "$DIR/legit.py" --target 127.0.0.1 --port 25565 \
--count 5 --interval $((duration / 5)) --name "phase$phase" \
> /tmp/legit_$phase.log 2>&1 &
local legit_pid=$!
for i in $(seq 1 $((duration / 5))); do
sleep 5
local a bl p s cpu
a=$(mget 'rampart_connections_total{result="allowed"}')
bl=$(mget 'rampart_connections_total{result="blocked"}')
p=$(mget 'rampart_pow_challenges_total{result="failed"}')
s=$(mget 'rampart_attack_status ')
cpu=$(edge_cpu)
echo " [t=${i}x5s] status=$s cpu=$cpu allowed=+$((a - ba)) blocked=+$((bl - bb)) pow_fail=+$((p - bp))"
done
wait "$flood_pid" || true
wait "$legit_pid" || true
local ea eb ep es ec
ea=$(mget 'rampart_connections_total{result="allowed"}')
eb=$(mget 'rampart_connections_total{result="blocked"}')
ep=$(mget 'rampart_pow_challenges_total{result="failed"}')
es=$(mget 'rampart_attack_status ')
ec=$(edge_cpu)
echo "--- итог фазы $phase ---"
echo " attack_status=$es cpu=$ec"
echo " allowed: $((ea - ba)) (+$(( (ea - ba) / duration ))/s)"
echo " blocked: $((eb - bb)) (+$(( (eb - bb) / duration ))/s)"
echo " pow_fail: $((ep - bp))"
echo "--- легитимные клиенты во время фазы ---"
grep -E '^\[phase' /tmp/legit_$phase.log || true
echo ""
}
echo "=== [setup] сеть + образы ==="
docker rm -f rampart-edge rampart-attacker 2>/dev/null || true
docker network rm -f "$NET" 2>/dev/null || true
docker network create --subnet "$SUB" "$NET" >/dev/null
# Подготовка контекстов: бинарь ищем в target/release репозитория
mkdir -p "$DIR/edge-ctx" "$DIR/attacker-ctx"
cp "$DIR/flood.py" "$DIR/attacker-ctx/flood.py" 2>/dev/null || true
if [ ! -f "$DIR/edge-ctx/rampart-core" ]; then
for p in "$DIR/rampart-core" "$DIR/repo/target/release/rampart-core" "$DIR/../target/release/rampart-core"; do
if [ -f "$p" ]; then cp "$p" "$DIR/edge-ctx/rampart-core"; break; fi
done
fi
[ -f "$DIR/edge-ctx/rampart-core" ] || { echo "ERROR: rampart-core не найден. Собери: cargo build --release --bin rampart-core" >&2; exit 1; }
docker build -q -f "$DIR/edge.Dockerfile" -t rampart-edge "$DIR/edge-ctx"
docker build -q -f "$DIR/attacker.Dockerfile" -t rampart-attacker "$DIR/attacker-ctx"
echo "=== [setup] attacker + 100 source IP ==="
docker rm -f rampart-attacker 2>/dev/null || true
docker run -d --name rampart-attacker --network "$NET" --ip "$ATT_IP" \
--cap-add=NET_RAW --cap-add=NET_ADMIN rampart-attacker >/dev/null
docker exec rampart-attacker sh -c '
for i in $(seq 101 200); do
ip addr add 172.30.0.$i/24 dev eth0 2>/dev/null || true
done
echo "source IPs on eth0: $(ip addr show eth0 | grep -c "inet ")"
'
echo ""
echo "############### PHASE A: СЫРАЯ ПРОПУСКНАЯ СПОСОБНОСТЬ ###############"
echo "############### (лимиты сняты: 100k pps, 100 src IP, валидные handshake) ###############"
run_flood_phase A edge-high.toml 30 handshake 100 "$IPS_END" "raw throughput, 100 IP flood, valid handshake"
echo ""
echo "############### PHASE B: ЗАЩИТА (дефолтные лимиты 5 pps/IP) ###############"
echo "############### (та же атака, но теперь edge режет по IP; легитимные клиенты заходят) ###############"
run_flood_phase B edge-defense.toml 30 handshake 100 "$IPS_END" "defense, rate limit 5pps/IP + reputation bans"
echo ""
echo "############### PHASE C: SYN flood ###############"
echo "=============================================================="
echo "ФАЗА C: SYN flood hping3 (rand-source, 20s)"
echo "=============================================================="
ba=$(mget 'rampart_connections_total{result="allowed"}')
bb=$(mget 'rampart_connections_total{result="blocked"}')
timeout 20 docker exec rampart-attacker hping3 -S --flood --rand-source -p 25565 "$EDGE_IP" || true
sleep 2
ea=$(mget 'rampart_connections_total{result="allowed"}')
eb=$(mget 'rampart_connections_total{result="blocked"}')
es=$(mget 'rampart_attack_status ')
echo " attack_status=$es cpu=$(edge_cpu) allowed=+$((ea - ba)) blocked=+$((eb - bb))"
echo " (SYN flood обрабатывается kernel'ом/XDP, L7 edge почти не задет)"
echo ""
echo ""
echo "############### PHASE D: активные соединения (keepalive) ###############"
echo "=============================================================="
echo "ФАЗА D: 300 keepalive коннектов (валидный handshake, держим открытым)"
echo "=============================================================="
start_edge edge-high.toml
ba=$(mget 'rampart_connections_total{result="allowed"}')
docker exec rampart-attacker python3 /flood.py \
--target "$EDGE_IP" --port 25565 --mode keepalive \
--duration 20 --threads 300 --ips-start "$IPS_START" --ips-end "$IPS_END" || true
sleep 2
ea=$(mget 'rampart_connections_total{result="allowed"}')
echo " allowed=+$((ea - ba)) cpu=$(edge_cpu)"
echo ""
echo "=============================================================="
echo "ИТОГОВЫЙ СВОД"
echo "=============================================================="
edge_metrics | grep -E 'connections_total|pow_challenges|attack_status'
echo ""
echo "CPU/память контейнеров:"
docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}' rampart-edge rampart-attacker

View file

@ -0,0 +1,4 @@
#!/bin/sh
# Stub backend (TCP echo) на 127.0.0.1:25566 для стресс-теста edge ноды.
socat TCP-LISTEN:25566,fork,reuseaddr,bind=127.0.0.1 EXEC:'cat' &
exec rampart-core --config /etc/rampart/config.toml

View file

@ -262,3 +262,76 @@ Before this test, the difficulty adjuster was untested under load. The code anal
a. Attack connections were being dropped before reaching the tunnel handler (kernel SYN backlog or XDP) a. Attack connections were being dropped before reaching the tunnel handler (kernel SYN backlog or XDP)
b. Or the Metrics endpoint polling interval (5s) wasn't capturing the escalation before difficulty reset b. Or the Metrics endpoint polling interval (5s) wasn't capturing the escalation before difficulty reset
- In v2, with explicit metrics reads at each poll interval, the escalation should be visible - In v2, with explicit metrics reads at each poll interval, the escalation should be visible
---
## v3 Test — VDS Loopback, Edge-only (2026-08-04)
> Date: 2026-08-04
> Scripts: `deploy/test/stress/` (flood.py, legit.py, run-stress.sh)
> Environment: VDS 2 vCPU / 3.8 GB RAM / Ubuntu 22.04, Docker bridge 172.30.0.0/24
> Target: Rampart v0.2.0, **edge-only** (слои 1–3) — без Redis, Velocity, Paper, ClickHouse
> Backend: socat TCP echo (stub) в том же контейнере на 127.0.0.1:25566
> XDP: disabled, PoW: disabled, workers: 2
### Setup
| Container | Role | IP |
|---|---|---|
| `rampart-edge` | rampart-core + socat stub backend | 172.30.0.2 |
| `rampart-attacker` | load generator, 100 source IP (172.30.0.101–200) | 172.30.0.3 |
Атака **маскируется под обычный трафик**: `flood.py` отправляет валидные Minecraft handshake
(protocol 767, packet id 0x00) со случайными hostname и ждёт ответа бэкенда — на уровне L7
ботнет неотличим от легитимных клиентов. Различение даёт только per-IP rate limit + репутация.
### Phase A — Raw throughput (лимиты сняты: 100k pps/IP)
| Measure | Value |
|---|---|
| Flood sent (100 IP, 30s) | ~121.5k handshake |
| Peak flood rate | ~4.0k conn/s |
| Edge allowed (proxied to backend) | 121,484 → **100%** |
| Edge CPU (peak) | ~179% (оба ядра) |
| Attack detector | `attack_status=1` (Suspicious) на старте, затем 0 (baseline-EWMA адаптируется) |
Легитимные клиенты во время флуда: 2/5 OK (RTT 3–43ms), 3 NO_RESP — без лимитов
флуд «душит» и легитимных клиентов.
### Phase B — Defense (дефолтные лимиты: 5 pps/IP, burst 10, reputation ban)
| Measure | Value |
|---|---|
| Edge blocked | 119,376 |
| Edge allowed | 528 |
| Block ratio | **~99.6%** |
| Edge CPU (max) | ~32% |
Легитимные клиенты во время атаки (тот же флуд, 100 IP): **5/5 OK, RTT 2.2–5.8ms**.
Rate limit срезает каждый IP до ~5 conn/s, после ~10–20 злоупотреблений IP уходит в
blacklist (reputation < -40) на 3600s. Реальный клиент (1 conn каждые 5s) не затронут.
### Phase C — SYN flood (hping3 --rand-source)
Edge не пострадал (allowed/blocked не изменились): без XDP SYN-флуд обрабатывает kernel.
L7 edge задет только при установленных TCP-соединениях.
### Phase D — Active connections
300 keepalive-соединений (валидный handshake, держим открытым) — все проксированы.
CPU ~0%, память ~7 MB. Удержание соединений упирается в backend (socat fork) и лимит fd,
не в edge.
### Выводы v3
1. **Полный L7-путь (parse → HMAC sign → backend → relay)**: ~4.0k conn/s на 2-ядерном VDS
при 100% прохождении (121.5k за 30s). Узкое место на этой конфигурации — сам генератор
(RTT round-trip до echo-бэкенда), не edge.
2. **Rate limit + reputation работают**: та же маскированная атака режется до ~0.4% прохода
(528 vs 119,376 blocked) при CPU ~32%.
3. **Легитимные клиенты доступны во время атаки**: RTT 2.2–5.8ms, 100% успех в defense-режиме.
4. **Детектор** отмечает Suspicious на старте флуда, но baseline-EWMA быстро адаптируется —
UnderAttack требует устойчивого превышения >3× базового уровня.
5. **SYN flood без XDP** — вне зоны L7 edge; на этой конфигурации защиту от него даёт
только XDP/eBPF или ядро (syncookies).

View file

@ -184,6 +184,25 @@ done
# (отправляем handshake по 1 байту с задержкой 100ms) # (отправляем 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
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).
--- ---
## 6. CI Pipeline ## 6. CI Pipeline