v0.3: 6-layer architecture complete

Layers:
  Layer 1: XDP/eBPF — TCP state machine, SYN throttle, blacklist, ringbuf
  Layer 2: PoW Challenge — SHA-256 hashcash, dynamic difficulty, constant-time verify
  Layer 3: Rust Core — HMAC handshake, rate limit, death code (existing)
  Layer 4: Velocity — Physics check, CAPTCHA, protocol verification
  Layer 5: Paper — Heartbeat, auto-registration (existing)
  Layer 6: Traffic Intel — EWMA, 168h profiling, reputation, alerts

Infra: XDP→Prometheus metrics, ClickHouse + Grafana dashboard, Docker Compose
Testing: 100-IP DDoS simulation, MHDDoS ref analysis, load test report
Fixes: VarInt sign extension UB, pure ACK deadlock, RST/FIN cleanup
Ref: MHDDoS, Sonar, LimboFilter, AtomGuard, Infrarust, MC-XDP-eBPF, PowGo
This commit is contained in:
loki5512344 2026-07-21 15:47:36 +02:00
parent 78fc6e00c7
commit 269daa071f
Signed by: boba
GPG key ID: 253067914055423B
66 changed files with 4529 additions and 1003 deletions

120
deploy/test/README.md Normal file
View file

@ -0,0 +1,120 @@
# Локальное тестирование Rampart
Запускаем всё в Docker на одной машине, без выхода в интернет.
## Сеть
Все контейнеры в одной bridge-сети `rampart-test`:
```
attacker ──┐
├── rampart-edge ── backend
│ (XDP отключён в тестах,
│ используется userspace-only режим)
│
mclient ───┘ (Minecraft клиент для теста легитимных коннектов)
```
## Быстрый старт
```bash
# 1. Сеть
docker network create rampart-test
# 2. Backend (Minecraft сервер или заглушка)
docker run -d --name backend --network rampart-test itzg/minecraft-server
# 3. Rampart edge
docker run -d --name rampart --network rampart-test \
-e RAMPART_CONFIG=/etc/rampart/config.toml \
-v ./config.test.toml:/etc/rampart/config.toml \
rampart-core
# 4. Аттакер (MHDDoS)
docker run -d --name attacker --network rampart-test \
--cap-add=NET_RAW --cap-add=NET_ADMIN \
python:3.11 bash -c "while true; do sleep 10; done"
# 5. Легитимный клиент (mclient.py)
docker run -d --name mclient --network rampart-test \
python:3.11 python mclient.py --target rampart:25565
```
## Сценарии тестирования
### 1. SYN flood
```bash
docker exec attacker python3 /ref/MHDDoS/start.py SYN 172.x.x.x:25565 60 100
```
Ожидание: Rampart XDP дропает SYN-пакеты после превышения throttle.
Метрика: `rampart_xdp_syn_throttle` растёт, CPU < 30%.
### 2. TCP connection flood (CPS)
```bash
docker exec attacker python3 /ref/MHDDoS/start.py CPS 172.x.x.x:25565 60 100
```
Ожидание: Rampart rate-limiter блокирует >50 conn/s с одного IP.
Метрика: `rampart_rate_limit_hits` растёт.
### 3. Minecraft handshake flood
```bash
docker exec attacker python3 /ref/MHDDoS/start.py MINECRAFT 172.x.x.x:25565 60 100
```
Ожидание: Layer 2 PoW требует решения хэш-задачи.
Метрика: `rampart_pow_challenges_total{result="failed"}` растёт.
### 4. Сложный ботнет (MHDDoS MCBOT)
```bash
docker exec attacker python3 /ref/MHDDoS/start.py MCBOT 172.x.x.x:25565 60 50
```
Ожидание: Physics check детектирует неестественное движение.
Требует: PhysicsCheckListener активен.
### 5. DNS amplification
```bash
docker exec attacker python3 /ref/MHDDoS/start.py DNS 172.x.x.x:53 60 100
```
Ожидание: XDP дропает UDP не на порты 25565-25575.
Метрика: `rampart_xdp_dropped` растёт.
### 6. Slowloris (L7)
```bash
docker exec attacker python3 /ref/MHDDoS/start.py SLOW http://172.x.x.x:9090 60 100
```
Ожидание: Таймаут чтения закрывает соединение.
Метрика: `rampart_connections_total{result="blocked"}` растёт.
### 7. HTTP flood через cloudscraper (имитация CFB)
```bash
docker exec attacker python3 /ref/MHDDoS/start.py CFB http://172.x.x.x:9090 60 100
```
Ожидание: L7 rate-limiter блокирует >100 req/s с одного IP.
Метрика: `rampart_rate_limit_hits` растёт.
## Легитимный тест (mclient.py)
Тест должен проходить: Rampart пропускает нормальный Minecraft handshake.
```bash
python3 deploy/test/mclient.py --target rampart:25565 --username test_player
```
Ожидание: HMAC verified, соединение проксируется на backend.
## Метрики
Все метрики на http://localhost:9090/metrics:
```
rampart_xdp_total
rampart_xdp_passed
rampart_xdp_dropped
rampart_xdp_syn_throttle
rampart_xdp_verified
rampart_connections_total{result="allowed|blocked"}
rampart_rate_limit_hits{action="hit"}
rampart_pow_challenges_total{result="passed|failed|skipped"}
rampart_pow_current_difficulty
```
Grafana: http://localhost:3000 (admin/admin)
ClickHouse: http://localhost:8123 (для долгосрочных метрик)

View file

@ -0,0 +1,45 @@
[bind]
address = "0.0.0.0"
port = 25565
[backend]
address = "backend"
port = 25565
[hmac]
secret = "test-secret-for-local-dev-only"
[worker]
count = 4
[limits]
rate_limit_login_pps = 50
rate_limit_burst = 100
[store]
redis_url = ""
clickhouse_url = "http://clickhouse:8123"
[xdp]
enabled = false
interface = "eth0"
[death_code]
enabled = true
[minecraft]
ping_timeout_ms = 5000
handshake_timeout_ms = 10000
[metrics]
enabled = true
port = 9090
[logging]
level = "debug"
[pow]
enabled = true
difficulty = 4
whitelist = ["localhost", "127.0.0.1"]

73
deploy/test/mclient.py Normal file
View file

@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""Minecraft handshake client for testing Rampart."""
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, port, protocol=767):
packet = bytearray()
packet.extend(pack_varint(protocol))
packet.extend(pack_varint(len(host)))
packet.extend(host.encode())
packet.extend(struct.pack(">H", port))
packet.extend(pack_varint(2))
length = pack_varint(len(packet))
return length + bytes(packet)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--target", default="localhost:25565")
parser.add_argument("--username", default="test_bot")
args = parser.parse_args()
host, port_str = args.target.split(":")
port = int(port_str)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(10)
sock.connect((host, port))
sock.sendall(make_handshake(host, port))
data = sock.recv(4096)
if data:
print(f"Got response: {data.hex()}")
# If PoW challenge -> receive challenge, solve, send nonce
if b"challenge" in data:
print("PoW challenge received")
challenge = data.decode().strip()
for nonce in range(1000000):
import hashlib
h = hashlib.sha256(f"{challenge}{nonce}".encode()).hexdigest()
if h.startswith("0000"):
sock.sendall(str(nonce).encode())
resp = sock.recv(4096)
print(f"PoW ok, handshake: {resp.hex()}")
break
else:
print(f"Handshake response: {data.hex()}")
else:
print("No response (blocked)")
sock.close()
if __name__ == "__main__":
main()

64
deploy/test/run_test.sh Executable file
View file

@ -0,0 +1,64 @@
#!/usr/bin/env bash
set -euo pipefail
NET="rampart-test"
DIR="$(cd "$(dirname "$0")" && pwd)"
echo "=== Создание сети ==="
docker network create "$NET" 2>/dev/null || true
echo "=== ClickHouse ==="
docker rm -f clickhouse 2>/dev/null || true
docker run -d --name clickhouse --network "$NET" \
-v "$DIR/../clickhouse/schema.sql:/docker-entrypoint-initdb.d/schema.sql" \
-p 8123:8123 \
clickhouse/clickhouse-server:latest
echo "=== Grafana ==="
docker rm -f grafana 2>/dev/null || true
docker run -d --name grafana --network "$NET" \
-p 3000:3000 \
-e GF_INSTALL_PLUGINS=grafana-clickhouse-datasource \
grafana/grafana:latest
echo "=== Backend (Minecraft stub) ==="
docker rm -f backend 2>/dev/null || true
# simple TCP echo server as placeholder
docker run -d --name backend --network "$NET" \
alpine sh -c "apk add socat && socat TCP-LISTEN:25565,fork EXEC:'cat'"
echo "=== Rampart Edge ==="
docker rm -f rampart 2>/dev/null || true
TMPDIR=$(mktemp -d)
cp "$DIR/../../target/release/rampart-core" "$TMPDIR/"
cp "$DIR/../docker/Dockerfile.test" "$TMPDIR/Dockerfile"
docker build -t rampart-core "$TMPDIR"
rm -rf "$TMPDIR"
docker run -d --name rampart --network "$NET" \
--cap-add=NET_ADMIN \
-p 25565:25565 -p 9090:9090 \
-e RAMPART_CONFIG=/etc/rampart/config.toml \
-v "$DIR/config.test.toml:/etc/rampart/config.toml" \
rampart-core
echo "=== Attacker (MHDDoS) ==="
docker rm -f attacker 2>/dev/null || true
docker run -d --name attacker --network "$NET" \
--cap-add=NET_RAW --cap-add=NET_ADMIN \
-v "$DIR/../../ref/MHDDoS:/ref/MHDDoS" \
python:3.11 bash -c "
cd /ref/MHDDoS && pip install -r requirements.txt -q && \
while true; do sleep 10; done
"
echo ""
echo "=== Готово ==="
echo "Rampart edge: localhost:25565"
echo "Metrics: http://localhost:9090/metrics"
echo "Grafana: http://localhost:3000 (admin/admin)"
echo "ClickHouse: http://localhost:8123"
echo ""
echo "Пример атаки:"
echo " docker exec attacker python3 /ref/MHDDoS/start.py TCP rampart:25565 60 100"
echo ""
echo "Для остановки: docker rm -f rampart backend attacker clickhouse grafana"

View file

@ -0,0 +1,237 @@
#!/usr/bin/env python3
"""DDoS simulation: 100 IPs from attacker container, 3 legit clients from host."""
import subprocess, time, re, sys
TARGET_IP = "172.18.0.6"
TARGET_PORT = 25565
METRICS_URL = "http://localhost:9090/metrics"
DURATION = 30
NUM_IPS = 100
def metrics():
try:
import urllib.request
data = urllib.request.urlopen(METRICS_URL, timeout=5).read().decode()
result = {}
for line in data.splitlines():
if line.startswith("rampart_"):
parts = line.split()
if len(parts) >= 2:
result[parts[0]] = parts[-1]
return result
except:
return {}
def print_metrics(label, m):
print(f" [{label}]", end="")
for k, v in sorted(m.items()):
print(f" {k}={v}", end="")
print()
ALLOWED = set("0123")
def solve_pow(challenge, difficulty):
import hashlib
t0 = time.time()
for n in range(20_000_000):
h = hashlib.sha256(f"{challenge}{n}".encode()).hexdigest()
if all(c in ALLOWED for c in h[:difficulty]):
return n, time.time() - t0
return None, time.time() - t0
def legit_client(client_id, delay):
import socket, hashlib, struct
time.sleep(delay)
m = metrics()
diff = int(m.get("rampart_pow_current_difficulty", "4"))
try:
s = socket.socket()
s.settimeout(10)
s.connect(("localhost", TARGET_PORT))
data = s.recv(4096).decode().strip()
nonce, solve_t = solve_pow(data, diff)
if nonce is None:
print(f" [legit#{client_id}] FAILED to solve PoW (diff={diff})")
s.close()
return
s.sendall(f"{nonce}\n".encode())
time.sleep(0.1)
# MC handshake
def wv(v):
b = bytearray()
while True:
byte = v & 0x7F
v >>= 7
if v:
byte |= 0x80
b.append(byte)
if not v:
break
return bytes(b)
host = "localhost"
hs = bytearray()
hs.extend(wv(0))
hs.extend(wv(767))
hs.extend(wv(len(host)))
hs.extend(host.encode())
hs.extend(struct.pack(">H", 25565))
hs.extend(wv(2))
s.sendall(wv(len(hs)) + bytes(hs))
time.sleep(0.1)
name = f"test_{client_id}"
login = bytearray()
login.extend(wv(0))
login.extend(wv(len(name)))
login.extend(name.encode())
s.sendall(wv(len(login)) + bytes(login))
resp = s.recv(4096)
status = "OK" if resp else "no_resp"
print(
f" [legit#{client_id}] ✅ diff={diff} solve={solve_t:.3f}s status={status}"
)
except Exception as e:
print(f" [legit#{client_id}] ❌ diff={diff} error={e}")
finally:
try:
s.close()
except:
pass
def run_flood_in_attacker():
"""Run the 100-IP flood inside the attacker container."""
print("[setup] Launching flood inside attacker container...")
import os, tempfile
script = """
import socket, threading, time, struct
TARGET = ("172.18.0.6", 25565)
DURATION = 30
NUM_IPS = 100
sent = 0
lock = threading.Lock()
def wv(v):
b = bytearray()
while True:
byte = v & 0x7F
v >>= 7
if v: byte |= 0x80
b.append(byte)
if not v: break
return bytes(b)
host = "localhost"
handshake = wv(0) + wv(767) + wv(len(host)) + host.encode() + struct.pack(">H", 25565) + wv(2)
end = time.time() + DURATION
def flood():
global sent
while time.time() < end:
try:
s = socket.socket()
s.settimeout(5)
s.connect(TARGET)
s.sendall(wv(len(handshake)) + handshake)
with lock: sent += 1
s.close()
except: pass
threads = [threading.Thread(target=flood) for _ in range(NUM_IPS)]
for t in threads: t.start()
for t in threads: t.join()
print(f"FLOOD_DONE:{sent}")
"""
tmp = tempfile.mktemp(suffix=".py")
with open(tmp, "w") as f:
f.write(script)
subprocess.run(f'docker cp "{tmp}" attacker:/tmp/flood.py', shell=True, check=True)
os.unlink(tmp)
result = subprocess.run(
f"docker exec attacker python3 /tmp/flood.py",
shell=True,
capture_output=True,
text=True,
timeout=DURATION + 20,
)
for line in result.stdout.splitlines():
if "FLOOD_DONE" in line:
return int(line.split(":")[1])
print(" [flood] stdout:", result.stdout[-300:])
print(" [flood] stderr:", result.stderr[-300:])
return 0
# ── Main ──
print("=" * 60)
print("Rampart DDoS Simulation — 100 IP Handshake Flood + Legit Clients")
print("=" * 60)
before = metrics()
print_metrics("BEFORE", before)
# Launch flood in attacker container
flood_total = run_flood_in_attacker()
# Launch legit clients during flood
import threading
legit3 = threading.Thread(target=legit_client, args=(3, 25))
legit2 = threading.Thread(target=legit_client, args=(2, 15))
legit1 = threading.Thread(target=legit_client, args=(1, 5))
legit1.start()
time.sleep(0.1)
legit2.start()
time.sleep(0.1)
legit3.start()
# Poll metrics during attack
for i in range(DURATION // 5):
time.sleep(5)
m = metrics()
print_metrics(f"t={(i + 1) * 5}s", m)
legit1.join()
legit2.join()
legit3.join()
time.sleep(2)
after = metrics()
print_metrics("AFTER", after)
# Summary
print()
print("=" * 60)
print("SUMMARY")
print("=" * 60)
diff = lambda k: int(after.get(k, "0")) - int(before.get(k, "0"))
print(f" Total handshakes sent: {flood_total}")
print(f" CPS: {flood_total // DURATION}")
print(
f" PoW challenges failed: +{diff('rampart_pow_challenges_total{result="failed"}')}"
)
print(
f" PoW challenges passed: +{diff('rampart_pow_challenges_total{result="passed"}')}"
)
print(
f" Connections allowed: +{diff('rampart_connections_total{result="allowed"}')}"
)
print(
f" Connections blocked: +{diff('rampart_connections_total{result="blocked"}')}"
)
print(
f" PoW difficulty (start): {before.get('rampart_pow_current_difficulty', '?')}"
)
print(f" PoW difficulty (end): {after.get('rampart_pow_current_difficulty', '?')}")