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
237 lines
6.2 KiB
Python
237 lines
6.2 KiB
Python
#!/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', '?')}")
|