Initial commit: Rampart v0.2.0

Multi-layer DDoS protection for Minecraft servers.

- rampart-core: Edge node with XDP/eBPF + Rust L7 filtering
- rampart-manager: REST API with JWT auth, Redis sync
- rampart-cli: CLI tool for operators
- velocity-plugin: Domain check, HMAC verify, server registry, load balancer
- paper-plugin: Auto-registration, heartbeat, HMAC verify
- dashboard: React + Vite web UI for management
This commit is contained in:
loki5512344 2026-07-20 20:53:32 +02:00
commit cf9608ce5d
Signed by: boba
GPG key ID: 253067914055423B
159 changed files with 15341 additions and 0 deletions

68
docs/research/README.md Normal file
View file

@ -0,0 +1,68 @@
# Rampart - Research & Deep Dives
> Это исследовательская документация. Здесь живут глубокие разборы технологий,
> эксперименты и идеи для версий v0.4+.
>
> Для текущей архитектуры (v0.1-v0.3) смотри `ARCHITECTURE.md` в корне репо.
---
## Структура
| Файл | Что внутри | Актуально с |
|---|---|---|
| [architecture.md](./architecture.md) | Общая архитектура, компоненты, схемы C4 | v0.1 |
| [ddos.md](./ddos.md) | Векторы атак L3/L4/L7, методы защиты, AI-боты | v0.1 |
| [ebpf.md](./ebpf.md) | XDP/eBPF фильтр, BPF maps, ringbuf, verifier | v0.4 |
| [rust-performance.md](./rust-performance.md) | Zero-copy, io_uring, SO_REUSEPORT, NUMA, profiling | v0.3 |
| [io_uring.md](./io_uring.md) | **Объединено с rust-performance.md** | v0.4 |
| [haproxy.md](./haproxy.md) | HAProxy конфиг, mTLS, замена на Rust LB | v0.2 |
| [envoy.md](./envoy.md) | EWMA балансировка, Circuit Breaker, xDS API | v0.5 |
| [observability.md](./observability.md) | Prometheus, OpenTelemetry, ClickHouse, Parca | v0.3 |
| [minecraft-protocol.md](./minecraft-protocol.md) | Handshake парсинг, VarInt, Forge, fingerprinting | v0.1 |
| [anti-bot.md](./anti-bot.md) | Sonar, challenge системы, AI-обходы, fingerprint | v0.2 |
| [benchmark.md](./benchmark.md) | Инструменты, методология, ожидаемые результаты | v0.3 |
| [security.md](./security.md) | STRIDE, mTLS, Zero Trust, supply chain | v0.2 |
| [networking.md](./networking.md) | WireGuard, BGP Anycast, QUIC, MTU | v0.3 |
| [papers.md](./papers.md) | Ссылки на статьи, RFC, проекты для изучения | - |
---
## Как читать
```
Хочу написать первую версию (v0.1)
→ architecture.md + minecraft-protocol.md + ddos.md
Хочу добавить защиту от ботов
→ anti-bot.md
Хочу выжать максимум производительности
→ rust-performance.md + io_uring.md + ebpf.md
Хочу настроить мониторинг
→ observability.md
Хочу понять безопасность системы
→ security.md + networking.md
```
---
### Операционные документы (корень docs/)
| Файл | Описание |
|---|---|
| [deployment.md](../deployment.md) | Пошаговый деплой |
| [configuration.md](../configuration.md) | Примеры конфигов |
| [vds_compatibility.md](../vds_compatibility.md) | Таблица провайдеров |
| [disaster_recovery.md](../disaster_recovery.md) | Failover сценарии |
| [runbook.md](../runbook.md) | Инструкции для админа |
| [api.md](../api.md) | REST API спецификация |
| [testing.md](../testing.md) | Методология тестирования |
| [troubleshooting.md](../troubleshooting.md) | FAQ |
| [migration.md](../migration.md) | Обновление версий |
---
*Версия: 0.5-research | Июль 2026*

282
docs/research/anti-bot.md Normal file
View file

@ -0,0 +1,282 @@
# Anti-Bot - Sonar, Challenge системы, Fingerprinting
> Актуально: v0.2+
## Путь игрока через защиту
```
Новый игрок
|
v
┌──────────────────┐
│ Edge нода │ Rate limit, Blacklist, Death code
│ Rust │ Невалидные пакеты → бан IP
└────────┬─────────┘
v (валидный handshake)
┌──────────────────┐
│ Velocity │ DomainCheck, HmacCheck
│ Java │ Неизвестный домен → блок
└────────┬─────────┘
v (подписанный HMAC)
┌──────────────────┐
│ Sonar Limbo │ Гравитация, Vehicle, TCP timing
│ Java │ Не прошёл → блок IP на N мин
└────────┬─────────┘
v (прошёл физику)
┌──────────────────┐
│ Custom │ Timing challenge, Map CAPTCHA
│ Challenge │ Не прошёл → блок IP
└────────┬─────────┘
v
┌──────────────────┐
│ Hub / Game │ Игрок на сервере
│ Server │ Поведенческий анализ первые 30 сек
└──────────────────┘
```
Каждый слой может заблокировать игрока.
Verified DB на Redis - прошёл один раз, не проверяется снова (TTL 24h).
---
## Слои защиты от ботов
```
[1] XDP rate limit - ограничивает скорость SYN flood
[2] Rust rate limit - ограничивает connections/сек per IP
[3] HMAC verification - только через наш edge (криптография)
[4] ASN reputation - датацентровые IP = строже
[5] Sonar 3.0 (Limbo) - физическая проверка
[6] Custom challenge - кастомная механика (нет готового обхода)
[7] Behavioral analysis - паттерны поведения на хабе
```
---
## Sonar 3.0 - базовый слой (июль 2026)
GitHub: `jonesdevelopment/sonar`
Версия: 3.x, релиз 12 июля 2026
Поддержка: Velocity 3.4-3.5.x, MC 1.8-26.2
### Как работает
```
Игрок → Velocity → Sonar перехватывает
↓
Отправляет на Limbo (лёгкий фейковый сервер)
↓
Проверки на Limbo:
├─ Гравитация: игрок должен падать вниз
├─ Vehicle: правильные пакеты при взаимодействии с лодкой
├─ TCP timing: не слишком быстрые ответы
└─ Очередь: физически ограничивает число одновременных верификаций
↓
Прошёл → IP в verified DB → следующие подключения проходят мгновенно
```
### Конфиг
```yaml
# sonar/config.yml
general:
max-online-per-ip: 3
min-players-for-attack: 8 # при N+ новых conn/сек → режим атаки
verification:
timing:
first-packet: 3500 # мс на первый пакет
movement: 10000 # мс на проверку физики
gravity:
enabled: true
captcha-on-fail: true
vehicle:
enabled: true
database:
type: MYSQL # или POSTGRESQL, H2
host: "10.0.0.1"
database: "sonar"
expiration: 5 # verified IP живёт N дней
```
---
## Кастомный challenge (поверх Sonar)
### Почему нужен кастомный
```
Sonar открытый → атакующий читает код → пишет обход
Кастомный → нет готового обхода → атакующий тратит время
Меняем механику регулярно → обход устаревает
```
### Идеи challenge (от простого к сложному)
#### 1. Timing challenge
```java
// Игрок должен ответить МЕЖДУ 2 и 8 секундами
// Боты отвечают мгновенно или с постоянной задержкой
long sent = System.currentTimeMillis();
// ...ждём ответ...
long elapsed = System.currentTimeMillis() - sent;
if (elapsed < 2000) {
// Слишком быстро - скрипт
fail("Ответ слишком быстрый");
} else if (elapsed > 8000) {
// AFK/медленный скрипт
fail("Время вышло");
} else {
pass();
}
```
#### 2. Map CAPTCHA
```java
// Рендерим картинку на карте Minecraft
// Случайный шрифт из пула 50+ шрифтов
// Игрок вводит код в чате
MapRenderer renderer = new CaptchaMapRenderer(challenge.getCode());
ItemStack map = new ItemStack(Material.FILLED_MAP);
map.setItemMeta(mapMeta);
player.getInventory().setItemInMainHand(map);
player.sendMessage("§eВведи код с карты в чат:");
```
#### 3. Поведенческий анализ (первые 30 сек на хабе)
```java
// Смотрим на паттерны движения
// Реальный игрок: случайные повороты, ускорения, паузы
// Бот: линейное движение или полная неподвижность
@EventHandler
public void onPlayerMove(PlayerMoveEvent e) {
BehaviorProfile profile = profiles.get(e.getPlayer().getUniqueId());
profile.recordMovement(e.getTo());
if (profile.getSamples() >= 50) {
double score = profile.calculateBotProbability();
if (score > 0.85) {
triggerChallenge(e.getPlayer());
}
}
}
```
#### 4. Контекстный вопрос
```java
// Вопрос зависит от случайного события на сервере
// Бот не знает контекст
String[] events = {"Последний вошедший игрок", "Текущее время на сервере"};
// "Как зовут последнего игрока который зашёл перед тобой?"
// Бот не знает → провал
```
---
## Репутационная система IP
```rust
// Каждый IP получает score от -100 до +100
// Хранится в Redis с TTL
pub struct IpReputation {
score: i32,
last_updated: u64,
}
impl IpReputation {
pub fn apply_event(&mut self, event: ReputationEvent) {
let delta = match event {
ReputationEvent::SuccessfulLogin => +10,
ReputationEvent::HourWithoutIssues => +5,
ReputationEvent::RateLimitHit => -20,
ReputationEvent::InvalidPacket => -30,
ReputationEvent::BotChallengeFailed => -50,
ReputationEvent::BotChallengePass => +15,
};
self.score = (self.score + delta).clamp(-100, 100);
}
pub fn get_rate_multiplier(&self) -> f64 {
match self.score {
s if s >= 80 => 2.0, // доверенный - больше лимит
s if s >= 0 => 1.0, // нормальный
s if s >= -30 => 0.5, // подозрительный
s if s >= -60 => 0.2, // проблемный
_ => 0.05, // почти в бане
}
}
}
```
---
## Bloom Filter для блэклиста
```rust
// Для очень больших блэклистов (миллионы IP)
// Bloom filter: 1% false positive, но 100x меньше памяти
// HashSet<u32> на 1M IP: ~32 MB
// Bloom filter на 1M IP: ~2 MB при p=0.01
use bloomfilter::Bloom;
pub struct FastBlacklist {
bloom: Bloom<u32>, // быстрая предпроверка (может дать false positive)
exact: DashMap<u32, BanEntry>, // точная проверка (только если bloom сказал "да")
}
impl FastBlacklist {
pub fn is_blocked(&self, ip: u32) -> bool {
// Если bloom говорит "нет" - точно не в блэклисте (нет false negative)
if !self.bloom.check(&ip) { return false; }
// Bloom говорит "возможно да" - проверяем точно
self.exact.contains_key(&ip)
}
}
```
---
## VPN / Proxy детекция
```rust
pub struct VpnDetector {
// MaxMind GeoLite2-ASN + список известных VPN/proxy ASN
asn_reader: maxminddb::Reader<Vec<u8>>,
vpn_asns: HashSet<u32>,
datacenter_keywords: Vec<Regex>,
}
impl VpnDetector {
pub fn classify(&self, ip: IpAddr) -> IpCategory {
let Ok(record) = self.asn_reader.lookup::<Asn>(ip) else {
return IpCategory::Unknown;
};
if let Some(asn) = record.autonomous_system_number {
if self.vpn_asns.contains(&asn) {
return IpCategory::VPN;
}
}
if let Some(org) = record.autonomous_system_organization {
if self.datacenter_keywords.iter().any(|r| r.is_match(org)) {
return IpCategory::Datacenter;
}
}
IpCategory::Residential
}
}
```
> Список VPN ASN: https://github.com/X4BNet/lists_vpn (обновляется еженедельно)
> MaxMind GeoLite2-ASN: бесплатно при регистрации на maxmind.com

View file

@ -0,0 +1,127 @@
# Architecture - Rampart
> Актуально: v0.1+
> Статус: основной документ
---
## Компоненты системы
```
┌─────────────────────────────────────────────────────────────────┐
│ EDGE LAYER │
│ XDP/eBPF (C) → Rust Core → mTLS/QUIC → Manager │
└─────────────────────────┬───────────────────────────────────────┘
│ чистый трафик
┌─────────────────────────▼───────────────────────────────────────┐
│ PROXY LAYER │
│ Rust Load Balancer → Velocity Cluster (x20) │
└─────────────────────────┬───────────────────────────────────────┘
│
┌─────────────────┼──────────────────┐
▼ ▼ ▼
Hub (x100) Game Servers Game Servers
лобби Survival (x100) Skyblock (x100)
разные VDS/дедики
```
## Типы нод и требования к хостингу
| Нода | Роль | CPU | RAM | Тип VDS | XDP нужен |
|---|---|---|---|---|---|
| **Edge** | Фильтрация DDoS | 2-4 vCPU | 2-4 GB | KVM / Bare Metal | ✅ |
| **Load Balancer** | L4 балансировка | 2 vCPU | 2 GB | KVM | ❌ |
| **Velocity** | MC Proxy | 4 vCPU | 4-8 GB | KVM | ❌ |
| **Manager** | API + Redis + NATS | 2-4 vCPU | 4-8 GB | KVM | ❌ |
| **Hub** | Лобби сервер | 4-8 vCPU | 8-16 GB | KVM / Bare Metal | ❌ |
| **Game Server** | Игровой процесс | 4-8 vCPU | 8-32 GB | KVM / Bare Metal | ❌ |
> ⚠️ **Важно:** XDP требует KVM или Bare Metal.
> OpenVZ / LXC контейнеры - XDP не работает вообще.
> Проверить тип виртуализации: `systemd-detect-virt`
## Sizing Guide
| Игроков онлайн | Edge нод | Velocity нод | Память Edge | Стоимость/мес (примерно) |
|---|---|---|---|---|
| до 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 |
> Цены ориентировочные для Hetzner/Contabo/Vultr. Bare Metal дешевле при большом трафике.
## Выбор WireGuard решения (для v0.1-v0.3)
**Используем hub-and-spoke + wg-quick.** Это просто, надёжно, понятно.
```
Manager нода = WireGuard Hub (10.0.0.1)
Все остальные ноды = Spoke, пиры с Hub
```
Headscale / Nebula / Tailscale - рассматриваем в v0.6+, когда нод станет 50+.
## Граница XDP / Rust (важно)
```
XDP делает: Rust делает:
L3: IP блэклист L7: MC handshake парсинг
L4: SYN flood drop HMAC подпись hostname
L4: rate limit (pps) rate limit (connections/sec)
L4: invalid TCP flags блэклист (сложные правила)
L4: UDP drop (MC=TCP) bot challenge
GeoIP/ASN lookup
```
XDP **не делает** HMAC, SHA256, GeoIP lookup - нет floating point до kernel 6.x,
нет доступа к heap, нет сложной логики. Всё L7 - только в Rust userspace.
## C4 - Container Diagram
```mermaid
graph TB
subgraph Edge["Edge Layer (VDS)"]
XDP[XDP Filter\nC/eBPF\nL3/L4 only]
Core[Rust Core\nL7 filter + HMAC]
end
subgraph Core_Infra["Core Infrastructure"]
LB[Rust Load Balancer]
Vel[Velocity Cluster\nJava x20]
Mgr[Manager API\nRust + Axum]
Redis[(Redis\nServer Registry\nBlacklist)]
NATS[NATS JetStream\nCritical Events]
CH[(ClickHouse\nAttack Log)]
end
subgraph Backends["Game Backends (WireGuard)"]
Hub[Hub x100]
Game[Game Servers x300]
end
XDP --> Core --> LB --> Vel --> Hub --> Game
Core -->|blacklist events| NATS
NATS --> Mgr
Mgr --> Redis
Mgr --> CH
Vel <-->|server registry| Redis
```
## ADR-001: Rust для Edge Core
**Решение:** Rust + tokio
**Альтернативы:** Go (GC паузы неприемлемы), C (небезопасен), Java (память)
**Причина:** Zero-cost abstractions, memory safety, нет GC, интеграция с libbpf-rs
## ADR-002: Redis как хранилище состояния
**Решение:** Redis + локальный кэш на edge нодах
**Оговорка:** При падении Redis - edge работает с кэшем блэклиста, Velocity с кэшем серверов
**Масштаб:** Redis Cluster при 1000+ серверов, Redis Sentinel для HA
## ADR-003: NATS для критических событий
**Решение:** NATS JetStream для blacklist updates, attack events, audit log
**Причина:** Redis Pub/Sub - fire-and-forget, NATS - at-least-once delivery
**Redis Pub/Sub оставляем для:** server registry updates, global chat (потеря допустима)

275
docs/research/benchmark.md Normal file
View file

@ -0,0 +1,275 @@
# Benchmark - Инструменты и методология
> Актуально: v0.3+
---
## Инструменты
| Инструмент | Что измеряет | Когда |
|---|---|---|
| **tcpkali** | TCP conn/sec, throughput | Основной benchmark |
| **SoulFire** | Реальные MC боты (Fabric код) | Anti-bot тест |
| **BotMark** | Быстрые MC handshake | Handshake throughput |
| **hping3** | SYN flood | XDP тест |
| **pktgen** | Max pps (kernel module) | XDP верхний предел |
| **iperf3** | Bandwidth | Throughput VDS |
| **cargo bench** | Rust unit benchmarks | Парсер, HMAC, rate limit |
---
## Методология
### Правила честного бенчмарка
```
1. Изолированная среда - никаких фоновых процессов
2. Прогрев (warm-up) - первые 10 сек не считаются
3. Несколько прогонов - минимум 3, берём медиану
4. Одна переменная - меняем одно за раз
5. Фиксируем конфигурацию - версия ядра, CPU, RAM, NIC
6. Не на той же машине - источник нагрузки на отдельном VDS
```
### Конфигурация тестового стенда
```
Тестируемый (edge нода):
VDS: Hetzner CX31 (4 vCPU, 8GB, 1Gbps, KVM)
OS: Ubuntu 22.04 LTS
Kernel: 5.15.x
NIC: virtio (XDP generic mode)
Источник нагрузки (отдельный VDS в той же сети):
VDS: Hetzner CX21 (2 vCPU, 4GB, 1Gbps)
Измеряем:
CPU edge ноды: htop / top
Память: /proc/meminfo
Connections: ss -s
Latency: tcpkali --latency-percentiles
```
---
## tcpkali - основной инструмент
```bash
# Установка
cargo install tcpkali # или apt install tcpkali
# Тест 1: новых соединений/сек
tcpkali \
--connections 1000 \
--connect-rate 5000 \ # 5000 новых conn/сек
--duration 30s \
--message-rate 0 \ # без данных - только коннект
TARGET_IP:25565
# Тест 2: активные соединения + трафик
tcpkali \
--connections 50000 \ # 50k одновременно
--connect-rate 1000 \
--duration 60s \
--message-rate 1 \ # 1 msg/сек от каждого
--message "$(cat mc_handshake.bin)" \
TARGET_IP:25565
# Тест 3: latency percentiles
tcpkali \
--connections 1000 \
--connect-rate 500 \
--duration 30s \
--latency-connect \ # измеряем latency до connect
--latency-percentiles 50,95,99,99.9 \
TARGET_IP:25565
```
---
## SoulFire - реальные MC боты
```bash
# SoulFire запускает настоящий Fabric MC клиент
# Боты ведут себя как реальные игроки на уровне протокола
# Скачать: github.com/AlexProgrammerDE/SoulFire
java -jar SoulFire.jar \
--target play.server.com:25565 \
--amount 500 \ # 500 ботов
--join-delay 100 \ # 100мс между подключениями
--protocol-version 765 # MC 1.20.4
```
---
## hping3 - SYN flood
```bash
# ТОЛЬКО для тестирования своих серверов!
# Запускать с отдельного VDS
# SYN flood
hping3 -S --flood -p 25565 TARGET_IP
# С рандомным src IP (проверяем uRPF)
hping3 -S --flood -p 25565 --rand-source TARGET_IP
# Смотрим на XDP счётчики
watch -n 1 'cat /sys/kernel/debug/tracing/trace_pipe'
# или через наш /metrics endpoint
curl http://TARGET_IP:9090/metrics | grep xdp_drops
```
---
## Rust unit benchmarks
```toml
# Cargo.toml
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
[[bench]]
name = "core_benchmarks"
harness = false
```
```rust
// benches/core_benchmarks.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId};
fn bench_handshake_parse(c: &mut Criterion) {
let mut group = c.benchmark_group("handshake_parse");
// Разные варианты hostname
let cases = vec![
("vanilla", build_handshake("play.server.com", 765, 2)),
("forge", build_handshake("play.server.com\0FML2\0", 765, 2)),
("hmac", build_handshake("play.server.com\0shield\0abcdef", 765, 2)),
];
for (name, packet) in &cases {
group.bench_with_input(BenchmarkId::new("parse", name), packet, |b, p| {
b.iter(|| McHandshake::parse(black_box(p)))
});
}
group.finish();
}
fn bench_hmac(c: &mut Criterion) {
let secret = b"test_secret_32_bytes_long_here!!";
let hostname = "play.server.com";
let signed = sign_hostname(hostname, secret);
let mut group = c.benchmark_group("hmac");
group.bench_function("sign", |b| {
b.iter(|| sign_hostname(black_box(hostname), secret))
});
group.bench_function("verify", |b| {
b.iter(|| verify_hostname(black_box(&signed), secret))
});
group.finish();
}
fn bench_rate_limiter(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap();
let limiter = RateLimiter::new(100, 10.0);
let ips: Vec<IpAddr> = (0..1000u32)
.map(|i| IpAddr::V4(Ipv4Addr::from(i)))
.collect();
c.bench_function("rate_limit_check", |b| {
b.to_async(&rt).iter(|| async {
let ip = ips[fastrand::usize(..ips.len())];
limiter.check(black_box(ip)).await
})
});
}
criterion_group!(benches, bench_handshake_parse, bench_hmac, bench_rate_limiter);
criterion_main!(benches);
```
```bash
# Запуск
cargo bench
# HTML отчёт в target/criterion/
open target/criterion/report/index.html
```
---
> ⚠️ **Важное уточнение:** Цифры 110k conn/s - для **synthetic echo benchmark** (простое прокси без L7 парсинга).
> Реальная производительность Rampart (handshake парсинг + HMAC + DashMap + rate limit) на 4 vCPU:
> - **~60-70k conn/s** (реалистично для v0.1-v0.3 на epoll)
> - **~85-95k conn/s** (с io_uring)
>
> Для простого TCP proxy без L7 логики - 110k+.
> Для точных цифр - прогони `cargo bench` на своём железе.
## Ожидаемые результаты (Hetzner CX31, 4 vCPU)
```
Unit benchmarks:
handshake_parse (vanilla): ~160 ns → 6.2M парсингов/сек
handshake_parse (forge): ~180 ns → 5.5M парсингов/сек
hmac_sign: ~820 ns → 1.2M подписей/сек
hmac_verify: ~840 ns → 1.2M верификаций/сек
rate_limit_check: ~220 ns → 4.5M проверок/сек
Системные (epoll / tokio):
Новых соединений/сек: ~80,000
Активных соединений: ~200,000
CPU при 80k conn/s: ~65%
Системные (io_uring):
Новых соединений/сек: ~110,000 (+37%)
Активных соединений: ~260,000
CPU при 110k conn/s: ~48%
XDP (generic mode на virtio):
Drop rate: ~3-5M pps
CPU при 3M pps: ~25%
XDP (native, bare metal):
Drop rate: ~15-20M pps
CPU при 10M pps: ~15%
```
### Таблица для README
```markdown
## Performance
Tested on Hetzner CX31 (4 vCPU, 8GB, KVM), Ubuntu 22.04, kernel 5.15
| Mode | New conn/s | Active conn | CPU |
|---|---|---|---|
| 1 core, epoll | 20k | 50k | ~100% |
| 4 core, epoll | 80k | 200k | ~65% |
| 4 core, io_uring | 110k | 260k | ~48% |
| XDP drop (generic) | 3-5M pps | - | ~25% |
| XDP drop (native) | 15-20M pps | - | ~15% |
```
---
## Профилирование под нагрузкой
```bash
# 1. Запускаем нагрузку
tcpkali --connections 50000 --connect-rate 5000 --duration 300s TARGET:25565 &
# 2. Пока идёт нагрузка - снимаем профиль CPU
perf record -g -p $(pgrep rampart-edge) -- sleep 30
perf report --stdio | head -100
# 3. Flamegraph
cargo flamegraph --pid $(pgrep rampart-edge) --output flamegraph.svg
open flamegraph.svg
# 4. tokio-console - смотрим какие async tasks тормозят
tokio-console http://TARGET:6669
```

292
docs/research/ddos.md Normal file
View file

@ -0,0 +1,292 @@
# DDoS - Векторы атак и защита
> Актуально: v0.1+
> Это лучший раздел документации - глубокий разбор всех известных векторов.
---
## Как трафик проходит через защиту
```
Атакующий (ботнет)
|
v
┌──────────────────┐
│ 1. NIC / XDP │ L3/L4: SYN flood, UDP drop, IP blacklist
│ (kernel, C) │ CPU < 30%, дроп до 10M pps
└────────┬─────────┘
v (чистый TCP)
┌──────────────────┐
│ 2. Rust Core │ L7: парсинг handshake, HMAC, rate limit
│ (userspace) │ death code auto-ban, blacklist check
└────────┬─────────┘
v (валидный MC клиент)
┌──────────────────┐
│ 3. Load │ Round-robin, circuit breaker
│ Balancer/Proxy │ TPS < 12 = server out
└────────┬─────────┘
v
┌──────────────────┐
│ 4. Game Server │ Чистый трафик, без DDoS нагрузки
│ (Velocity/Hub) │
└──────────────────┘
```
Каждый слой отрабатывает и дропает до перехода к следующему.
XDP отсекает L3/L4 флуд, Rust - L7 атаки на протокол MC.
---
## L3/L4 атаки (объёмные)
| Атака | Механизм | Защита | Слой |
|---|---|---|---|
| **UDP Flood** | Миллионы UDP пакетов | MC = TCP, UDP дропается на уровне NIC | XDP |
| **SYN Flood** | Миллионы TCP SYN без ACK | SYN cookies в ядре Linux | XDP + sysctl |
| **ACK Flood** | Пакеты с ACK без SYN | Stateful connection tracking | XDP |
| **ICMP Flood** | Ping flood | Отключить ICMP ответы | sysctl |
| **Amplification** | DNS/NTP усиление | Фильтрация у провайдера (UDP) | Upstream |
| **Invalid flags** | TCP с мусорными флагами | XDP дроп по флагам | XDP |
| **IP Spoof** | Поддельный src IP | BPF map проверка + uRPF | XDP |
### sysctl для L3/L4 защиты
```bash
# SYN flood
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_syn_retries = 2
# ICMP
net.ipv4.icmp_echo_ignore_all = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
# Общие буферы
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.ip_local_port_range = 1024 65535
```
---
## L7 атаки (Minecraft-специфичные)
### Handshake Flood
Боты коннектятся тысячами, шлют валидный handshake, дропают.
```
Детект: connections/sec с одного IP > threshold
Защита: rate limit (token bucket) в Rust
Параметры: max 5 conn/IP/сек, burst 10
```
### Bot Join Flood
Тысячи фейковых логинов с разных IP.
```
Детект: LoginStart без предшествующего challenge
Защита: Sonar antibot (физика на limbo) + custom challenge
Параметры: очередь 100 одновременных верификаций
```
### Ping Flood (Status Request)
Тысячи пакетов с next_state=1 (не логин, просто пинг).
```
Детект: status requests/сек > threshold с IP
Защита: отдельный rate limit для status (next_state=1)
Параметры: max 2 status/IP/10сек
```
### Slow Loris (MC вариант)
Открывают TCP, шлют handshake по 1 байту каждые несколько секунд - занимают слоты.
```
Детект: время на handshake > 5 сек
Защита: connection timeout (5 сек на получение полного handshake)
Rust: tokio::time::timeout(Duration::from_secs(5), read_handshake())
```
### Fragmented Handshake
Handshake пакет разбит на несколько TCP сегментов - ломает парсеры.
```
Детект: невозможно, это нормальный TCP
Защита: robust парсер с reassembly буфером
читаем до N байт пока не получим полный пакет
timeout если слишком долго
```
### Fake Forge Flood
Бесконечный поток Forge handshake с мусорными mod list - ломает парсер.
```
Детект: mod list длиннее разумного (> 500 модов)
Защита: max_hostname_length = 4096, дроп при превышении
парсер с явными bounds check на каждый VarInt
```
### VarInt Overflow
Специально сформированные VarInt которые вызывают integer overflow.
```
Детект: VarInt > 5 байт (по MC протоколу)
Защита: строгий bounds check, паника = DROP не crash
// Правильный парсер с защитой
fn read_varint(buf: &[u8]) -> Result<(i32, usize), Error> {
let mut value: i32 = 0;
let mut position = 0;
for (i, &byte) in buf.iter().enumerate() {
if i >= 5 { return Err(Error::VarIntTooBig); } // MAX 5 байт
value |= ((byte & 0x7F) as i32) << position;
if (byte & 0x80) == 0 { return Ok((value, i + 1)); }
position += 7;
}
Err(Error::Incomplete)
}
```
---
## AI-боты (2026)
### Проблема
Современные attack frameworks используют AI и базы CAPTCHA решений:
- Боты проходят физику Sonar (реализован настоящий MC движок)
- Боты решают математические задачи в чате
- Боты кликают на блоки по описанию
- LimboFilter полностью обходится
### Что всё ещё работает
```
✓ HMAC верификация - только через наш edge (криптография)
✓ Rate limit на edge - физически ограничивает скорость
✓ ASN блокировка - датацентры не могут быть "жилыми" IP
✓ Репутационная система - долго строить репутацию
✓ Кастомный challenge - нет готового обхода
✓ Timing analysis - боты отвечают слишком быстро или паттернами
```
### Кастомный challenge - идеи которые сложно автоматизировать
```
1. Timing-based: игрок должен ответить МЕЖДУ 2 и 8 секундами
(слишком быстро = бот, слишком медленно = AFK скрипт)
2. Контекстный вопрос: вопрос зависит от случайного события
на сервере в последние 5 минут (бот не знает контекст)
3. Изменяющаяся механика: challenge меняется каждые 6 часов
(атакующий должен постоянно обновлять обход)
4. Map-based CAPTCHA: картинка рендерится на карте в инвентаре
случайным шрифтом из пула 50+ шрифтов
5. Поведенческий анализ: первые 30 сек на хабе - смотрим
на паттерны движения, мыши, взаимодействий
```
### Timing Analysis
```rust
// Боты часто отвечают с константной задержкой
// Реальные игроки - с нормальным распределением
pub struct TimingAnalyzer {
response_times: Vec<Duration>,
}
impl TimingAnalyzer {
pub fn is_bot_timing(&self, response_time: Duration) -> f64 {
let ms = response_time.as_millis() as f64;
// Слишком быстро - скрипт
if ms < 200.0 { return 0.9; }
// Слишком ровно - паттерн (variance < 10ms за 5 измерений)
if self.response_times.len() >= 5 {
let variance = self.calculate_variance();
if variance < 10.0 { return 0.85; }
}
// Нормальное распределение - человек
0.1
}
}
```
---
## Circuit Breaker для перегруженных серверов
```
CLOSED (нормально)
↓ TPS < 12 или timeout > 3 сек → OPEN
OPEN (сервер выведен)
↓ через 30 сек → HALF_OPEN (пробный трафик)
HALF_OPEN
↓ успешно → CLOSED
↓ снова плохо → OPEN
```
```rust
pub enum CircuitState { Closed, Open(Instant), HalfOpen }
impl CircuitBreaker {
pub fn should_route(&mut self, server: &ServerEntry) -> bool {
match &self.state {
CircuitState::Closed => {
if server.tps < 12.0 { self.trip(); false }
else { true }
}
CircuitState::Open(tripped_at) => {
if tripped_at.elapsed() > Duration::from_secs(30) {
self.state = CircuitState::HalfOpen;
true // пробуем
} else { false }
}
CircuitState::HalfOpen => true,
}
}
}
```
---
## ASN Reputation
Разные лимиты для разных типов сетей:
```rust
pub enum AsnReputation {
Residential, // обычный провайдер → стандартные лимиты
Datacenter, // AWS/OVH/Hetzner → строгие лимиты
Mobile, // мобильные сети → средние лимиты (NAT!)
Tor, // Tor exit node → максимальная строгость
Vpn, // известный VPN → настраивается
Unknown,
}
// rate limit множитель по типу ASN
fn rate_limit_multiplier(rep: &AsnReputation) -> f64 {
match rep {
AsnReputation::Residential => 1.0,
AsnReputation::Mobile => 0.5, // NAT - много игроков с 1 IP
AsnReputation::Datacenter => 0.2,
AsnReputation::Vpn => 0.3,
AsnReputation::Tor => 0.05,
AsnReputation::Unknown => 0.5,
}
}
```
> ⚠️ Мобильные сети используют NAT - один IP = много реальных игроков.
> Не блокируй мобильные ASN полностью, только снижай лимит.

340
docs/research/ebpf.md Normal file
View file

@ -0,0 +1,340 @@
# eBPF / XDP - Фильтрация уровня ядра
> Актуально: v0.4+
> Требует: Linux kernel 5.10+, KVM или Bare Metal (не OpenVZ/LXC)
---
## Почему XDP
```
Обычный путь пакета (без XDP):
NIC → driver → kernel TCP stack → socket buffer → userspace → решение
XDP путь:
NIC driver → XDP_DROP (ещё до kernel stack)
Никаких аллокаций, никаких копий, никаких syscall
```
| Метод | Задержка дропа | CPU на 5M pps | Требует |
|---|---|---|---|
| iptables | ~10 мкс | ~80% | - |
| nftables | ~8 мкс | ~70% | - |
| Rust userspace | ~5 мкс | ~50% | - |
| **XDP (generic)** | ~2 мкс | ~30% | любой kernel |
| **XDP (native)** | ~0.5 мкс | ~15% | поддержка в драйвере NIC |
| **XDP (offload)** | ~0.1 мкс | ~0% | SmartNIC |
Для большинства VDS - native XDP (Intel i40e, Mellanox ConnectX).
---
## Граница ответственности (критично)
```
XDP МОЖЕТ: XDP НЕ МОЖЕТ:
IP блэклист (LPM_TRIE) HMAC-SHA256 (нет floating point < kernel 6.x)
SYN flood rate limit GeoIP lookup (нет heap allocation)
Invalid TCP flags drop DNS resolve
UDP drop (MC = TCP only) Сложные строковые операции
Port whitelist Вызов userspace функций
Per-IP packet rate Блокировать по hostname
BPF map read/write TLS инспекция
```
Всё L7 (handshake парсинг, HMAC, hostname проверка) - **только в Rust userspace**.
---
## Структура BPF Maps
```c
// maps.h
// Блэклист IP (LPM - Longest Prefix Match, поддерживает CIDR)
struct {
__uint(type, BPF_MAP_TYPE_LPM_TRIE);
__uint(max_entries, 100000);
__type(key, struct lpm_key); // prefixlen + ip
__type(value, __u64); // timestamp бана
__uint(map_flags, BPF_F_NO_PREALLOC);
} blacklist_map SEC(".maps");
// Rate limit per IP (LRU - автоматически вытесняет старые)
struct {
__uint(type, BPF_MAP_TYPE_LRU_PERCPU_HASH);
__uint(max_entries, 500000);
__type(key, __u32); // src IP
__type(value, struct rate_entry);
} rate_map SEC(".maps");
// Whitelist доверенных IP (edge нод например)
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1000);
__type(key, __u32);
__type(value, __u8); // просто флаг
} trusted_map SEC(".maps");
// Статистика (для Prometheus)
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, 16);
__type(key, __u32); // индекс счётчика
__type(value, __u64);
} stats_map SEC(".maps");
// Ringbuf для передачи событий в userspace (быстрее perfbuf)
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 1 << 24); // 16 MB
} events SEC(".maps");
```
---
## XDP программа (C)
```c
// xdp_filter.c
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#include "maps.h"
#define MC_PORT 25565
#define RATE_LIMIT_PPS 20 // пакетов/сек с одного IP
#define BAN_DURATION_NS 60000000000ULL // 60 сек
// Статистические индексы
#define STAT_TOTAL 0
#define STAT_BLOCKED 1
#define STAT_RATELIM 2
static __always_inline void inc_stat(__u32 idx) {
__u64 *val = bpf_map_lookup_elem(&stats_map, &idx);
if (val) __sync_fetch_and_add(val, 1);
}
SEC("xdp")
int minecraft_xdp_filter(struct xdp_md *ctx) {
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
inc_stat(STAT_TOTAL);
// ── Парсим Ethernet ──
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end) return XDP_PASS;
if (eth->h_proto != bpf_htons(ETH_P_IP)) return XDP_PASS;
// ── Парсим IP ──
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end) return XDP_PASS;
if (ip->protocol != IPPROTO_TCP) return XDP_PASS; // UDP → дроп неявный (MC=TCP)
__u32 src_ip = ip->saddr;
// ── Whitelist (наши edge ноды, manager) ──
if (bpf_map_lookup_elem(&trusted_map, &src_ip)) return XDP_PASS;
// ── Парсим TCP ──
struct tcphdr *tcp = (void *)ip + (ip->ihl * 4);
if ((void *)(tcp + 1) > data_end) return XDP_PASS;
if (tcp->dest != bpf_htons(MC_PORT)) return XDP_PASS;
// ── Блэклист проверка ──
struct lpm_key key = { .prefixlen = 32, .ip = src_ip };
__u64 *ban_ts = bpf_map_lookup_elem(&blacklist_map, &key);
if (ban_ts) {
__u64 now = bpf_ktime_get_ns();
if (now - *ban_ts < BAN_DURATION_NS) {
inc_stat(STAT_BLOCKED);
return XDP_DROP;
}
bpf_map_delete_elem(&blacklist_map, &key);
}
// ── Invalid TCP flags ──
// Дропаем пакеты с мусорными флагами (не SYN, не ACK, не PSH+ACK)
__u8 flags = ((__u8 *)tcp)[13];
if ((flags & 0x3F) == 0) { // нет флагов вообще
inc_stat(STAT_BLOCKED);
return XDP_DROP;
}
// ── SYN rate limit ──
if (tcp->syn && !tcp->ack) {
struct rate_entry *entry = bpf_map_lookup_elem(&rate_map, &src_ip);
__u64 now = bpf_ktime_get_ns();
if (entry) {
// Простой sliding window
if (now - entry->window_start < 1000000000ULL) { // 1 сек
if (entry->count >= RATE_LIMIT_PPS) {
// Баним
__u64 ban_ts = now;
bpf_map_update_elem(&blacklist_map, &key, &ban_ts, BPF_ANY);
inc_stat(STAT_RATELIM);
inc_stat(STAT_BLOCKED);
return XDP_DROP;
}
__sync_fetch_and_add(&entry->count, 1);
} else {
// Новое окно
entry->window_start = now;
entry->count = 1;
}
} else {
struct rate_entry new_entry = { .window_start = now, .count = 1 };
bpf_map_update_elem(&rate_map, &src_ip, &new_entry, BPF_ANY);
}
}
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
```
---
## Rust loader (libbpf-rs)
```rust
// xdp/loader.rs
use libbpf_rs::{MapFlags, Object, ObjectBuilder};
pub struct XdpFilter {
obj: Object,
interface: String,
}
impl XdpFilter {
pub fn load(interface: &str) -> Result<Self> {
let obj = ObjectBuilder::default()
.open_file("/etc/rampart/xdp_filter.o")?
.load()?;
// Аттачим XDP программу к интерфейсу
let prog = obj.prog("minecraft_xdp_filter").unwrap();
prog.attach_xdp(if_nametoindex(interface)?)?;
Ok(Self { obj, interface: interface.to_string() })
}
// Добавляем IP в блэклист из Rust (обновляем BPF map)
pub fn ban_ip(&self, ip: Ipv4Addr, duration: Duration) {
let mut map = self.obj.map("blacklist_map").unwrap();
let key = LpmKey::new(32, ip);
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos() as u64;
map.update(&key.to_bytes(), &ts.to_le_bytes(), MapFlags::ANY).unwrap();
}
// Читаем статистику
pub fn get_stats(&self) -> XdpStats {
let map = self.obj.map("stats_map").unwrap();
XdpStats {
total: read_percpu_sum(&map, 0),
blocked: read_percpu_sum(&map, 1),
ratelim: read_percpu_sum(&map, 2),
}
}
// Читаем события из ringbuf (атаки, баны)
pub async fn read_events(&self, tx: mpsc::Sender<XdpEvent>) {
let mut ringbuf = RingBuffer::new();
ringbuf.add(self.obj.map("events").unwrap(), move |data| {
let event: XdpEvent = unsafe { *(data.as_ptr() as *const XdpEvent) };
let _ = tx.try_send(event);
0
}).unwrap();
loop {
ringbuf.poll(Duration::from_millis(10)).unwrap();
}
}
}
```
---
## Cargo.toml для XDP компонента
```toml
[dependencies]
libbpf-rs = "0.23"
libbpf-sys = "1.4"
[build-dependencies]
libbpf-cargo = "0.23" # автокомпиляция .c → .o в build.rs
```
```rust
// build.rs
use libbpf_cargo::SkeletonBuilder;
fn main() {
SkeletonBuilder::new()
.source("src/bpf/xdp_filter.c")
.build_and_generate("src/bpf/xdp_filter.skel.rs")
.unwrap();
}
```
---
## Требования к окружению
```bash
# Проверка что XDP поддерживается
ethtool -i eth0 | grep driver # должен быть i40e, mlx5, или virtio
# Проверка типа виртуализации
systemd-detect-virt
# kvm → XDP работает (native или generic)
# none → bare metal → XDP native
# openvz / lxc → XDP НЕ работает
# Проверка версии ядра
uname -r
# >= 5.10 - достаточно для нашего XDP
# >= 6.0 - полный функционал (float в eBPF, CO-RE стабильный)
# Установка зависимостей (Ubuntu 22.04+)
apt-get install -y libbpf-dev clang llvm linux-headers-$(uname -r)
```
---
## ringbuf vs perfbuf
| | perfbuf | ringbuf (kernel 5.8+) |
|---|---|---|
| Тип | Per-CPU кольцевой буфер | Один разделяемый буфер |
| Копирование | Одно | Одно |
| Порядок событий | Не гарантирован | Гарантирован |
| Потребление памяти | Per-CPU | Меньше |
| **Вывод** | Устаревший | **Используй ringbuf** |
---
## Известные лимиты BPF verifier
```
Максимум инструкций: 1M (kernel 5.2+, раньше 4096)
Максимум стека: 512 байт
Максимум вложенности: 8 уровней (loops разрешены с 5.3+)
Циклы: разрешены, но верификатор считает итерации
Динамический allocation: нет (только BPF maps)
```
Если программа не проходит верификатор - упрости логику или разбей на несколько программ в цепочке (TC + XDP).

240
docs/research/envoy.md Normal file
View file

@ -0,0 +1,240 @@
# Envoy - EWMA, Circuit Breaker, xDS API
> Актуально: v0.5+
> Envoy как референс для алгоритмов балансировки.
---
## EWMA балансировщик (как в Envoy)
### Почему LEAST_CONN недостаточно
```
Проблема:
Сервер A: 50 игроков, TPS 20 (быстрый)
Сервер B: 48 игроков, TPS 12 (лагающий)
LEAST_CONN выберет B → плохо
EWMA учитывает реальное время ответа:
Сервер A: быстро → высокий score → больше игроков
Сервер B: медленно → низкий score → меньше игроков
```
### Формула
```
effective_load = rtt_ewma × (active_requests + 1)
rtt_ewma_new = α × rtt_ewma_old + (1 - α) × rtt_sample
α = 0.95 (decay, параметр сглаживания)
```
### Реализация
```rust
// balancer/ewma.rs
use std::sync::atomic::{AtomicU64, Ordering};
pub struct EwmaBackend {
pub name: String,
rtt_ewma_us: AtomicU64, // EWMA в микросекундах
active: AtomicU64,
}
impl EwmaBackend {
pub fn new(name: String) -> Self {
Self {
name,
rtt_ewma_us: AtomicU64::new(1000), // стартовое значение 1мс
active: AtomicU64::new(0),
}
}
pub fn record_rtt(&self, rtt: Duration) {
let sample = rtt.as_micros() as u64;
let old = self.rtt_ewma_us.load(Ordering::Relaxed);
// EWMA: 95% старое + 5% новое измерение
let new = (old * 95 + sample * 5) / 100;
self.rtt_ewma_us.store(new, Ordering::Relaxed);
}
pub fn effective_load(&self) -> u64 {
let rtt = self.rtt_ewma_us.load(Ordering::Relaxed);
let active = self.active.load(Ordering::Relaxed);
rtt.saturating_mul(active + 1)
}
pub fn acquire(&self) { self.active.fetch_add(1, Ordering::Relaxed); }
pub fn release(&self) { self.active.fetch_sub(1, Ordering::Relaxed); }
}
pub struct EwmaBalancer {
backends: Vec<Arc<EwmaBackend>>,
}
impl EwmaBalancer {
pub fn select(&self) -> Option<Arc<EwmaBackend>> {
self.backends.iter()
.min_by_key(|b| b.effective_load())
.cloned()
}
}
```
---
## Circuit Breaker
```
CLOSED → нормальная работа, трафик идёт
↓ TPS < 12 или timeout > 3 сек подряд (N раз)
OPEN → сервер выведен из ротации
↓ через 30 сек (recovery timeout)
HALF_OPEN → пробный трафик (1 соединение)
↓ успешно → CLOSED
↓ снова ошибка → OPEN (увеличиваем timeout × 2)
```
```rust
// balancer/circuit_breaker.rs
pub enum State {
Closed,
Open { tripped_at: Instant, timeout: Duration },
HalfOpen,
}
pub struct CircuitBreaker {
state: State,
failure_count: u32,
failure_threshold: u32, // сколько ошибок до OPEN
}
impl CircuitBreaker {
pub fn should_route(&mut self) -> bool {
match &self.state {
State::Closed => true,
State::Open { tripped_at, timeout } => {
if tripped_at.elapsed() >= *timeout {
self.state = State::HalfOpen;
true
} else {
false
}
}
State::HalfOpen => true,
}
}
pub fn record_success(&mut self) {
self.failure_count = 0;
self.state = State::Closed;
}
pub fn record_failure(&mut self) {
self.failure_count += 1;
if self.failure_count >= self.failure_threshold {
let timeout = match &self.state {
State::Open { timeout, .. } => *timeout * 2, // exponential backoff
_ => Duration::from_secs(30),
};
self.state = State::Open {
tripped_at: Instant::now(),
timeout: timeout.min(Duration::from_secs(300)), // max 5 мин
};
}
}
}
```
---
## Health Scoring
```rust
pub fn calculate_health_score(server: &ServerEntry) -> f64 {
let tps_score = (server.tps / 20.0).min(1.0); // 0..1
let player_score = 1.0 - (server.online as f64 / server.max_players as f64);
let mspt_score = (1.0 - server.mspt / 50.0).max(0.0); // 50ms MSPT = 0 score
let ram_score = 1.0 - (server.ram_used as f64 / server.ram_max as f64);
// Взвешенная сумма
tps_score * 0.40
+ player_score * 0.30
+ mspt_score * 0.20
+ ram_score * 0.10
}
// Балансировщик выбирает по score вместо LEAST_CONN
pub fn select_by_score(servers: &[ServerEntry]) -> Option<&ServerEntry> {
servers.iter()
.filter(|s| calculate_health_score(s) > 0.3) // минимальный порог
.max_by(|a, b| {
calculate_health_score(a)
.partial_cmp(&calculate_health_score(b))
.unwrap()
})
}
```
---
## Consistent Hashing (друзья на одном Hub)
```rust
// Игроки с одной группой попадают на один Hub
// При добавлении новых Hubs - минимальная миграция игроков
use std::collections::BTreeMap;
pub struct ConsistentHash {
ring: BTreeMap<u64, String>, // hash → server_name
vnodes: u32, // виртуальные ноды (больше = равномернее)
}
impl ConsistentHash {
pub fn new(vnodes: u32) -> Self {
Self { ring: BTreeMap::new(), vnodes }
}
pub fn add_server(&mut self, name: &str) {
for i in 0..self.vnodes {
let key = hash(&format!("{}-{}", name, i));
self.ring.insert(key, name.to_string());
}
}
pub fn get_server(&self, player_uuid: &Uuid) -> Option<&str> {
if self.ring.is_empty() { return None; }
let hash = hash(&player_uuid.to_string());
// Идём по кольцу вправо от hash
self.ring.range(hash..)
.next()
.or_else(|| self.ring.iter().next())
.map(|(_, name)| name.as_str())
}
}
// Применение: для хабов, где важно чтобы друзья были рядом
// Для game серверов - EWMA (важна нагрузка, не стабильность)
```
---
## xDS API (Envoy паттерн для динамической конфигурации)
> Актуально v0.6+ - если нод станет 100+
xDS - протокол от Envoy/Istio для динамической доставки конфигурации нодам. Вместо того чтобы каждая нода поллила Redis - Manager пушит изменения через gRPC stream.
```
Manager (xDS сервер)
↓ gRPC stream (двунаправленный)
Edge ноды / LB ноды (xDS клиенты)
При изменении конфига (новый сервер, новое правило):
Manager → push → все ноды получают обновление мгновенно
Нет поллинга, нет задержки
```

183
docs/research/haproxy.md Normal file
View file

@ -0,0 +1,183 @@
# HAProxy и свой Rust Load Balancer
> HAProxy - хорошее начало для v0.1-v0.2.
> Свой Rust LB - цель для v0.4 (убирает SPOF, добавляет MC-aware health check).
---
## Проблема с HAProxy
```
HAProxy как единственная точка входа = Single Point of Failure
Если HAProxy упал:
Все 20 Velocity нод недоступны
Все игроки дисконнектятся
Нет автоматического failover
Решение:
v0.1-v0.2: HAProxy + keepalived (VRRP failover)
v0.4+: Собственный Rust LB (несколько инстансов + SO_REUSEPORT)
```
---
## HAProxy конфиг (v0.1)
```
# /etc/haproxy/haproxy.cfg
global
maxconn 100000
log /dev/log local0
stats socket /run/haproxy/admin.sock mode 660 level admin
defaults
mode tcp
timeout connect 3s
timeout client 30s
timeout server 30s
option tcplog
# ── Входящие игроки (от edge нод) ──
frontend minecraft_in
bind *:25565
mode tcp
# Принимаем только от наших edge нод
acl is_edge_ip src 10.0.100.0/24
tcp-request connection reject if !is_edge_ip
default_backend velocity_pool
# ── Velocity кластер ──
backend velocity_pool
mode tcp
balance leastconn # наименьшее число активных соединений
option tcp-check # проверяем что порт открыт
# check inter 3s - проверяем каждые 3 сек
# rise 2 - нужно 2 успеха чтобы считать живым
# fall 3 - 3 неудачи → выводим из ротации
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
# ... до vel20
# ── Stats страница (для Prometheus) ──
frontend stats
bind 10.0.0.1:8404
stats enable
stats uri /stats
stats refresh 10s
stats auth admin:${HAPROXY_STATS_PASS}
```
## HAProxy + keepalived (устраняет SPOF)
```
# Два HAProxy сервера, один активный (MASTER), второй резервный (BACKUP)
# Виртуальный IP переключается автоматически при падении MASTER
# /etc/keepalived/keepalived.conf (на MASTER)
vrrp_instance VI_1 {
state MASTER
interface eth0
virtual_router_id 51
priority 100 # MASTER имеет высший приоритет
authentication {
auth_type PASS
auth_pass rampart
}
virtual_ipaddress {
10.0.0.1/24 # виртуальный IP, на него смотрят edge ноды
}
notify_master "/etc/keepalived/notify.sh MASTER"
notify_backup "/etc/keepalived/notify.sh BACKUP"
}
# На BACKUP: state BACKUP, priority 90
```
---
## Свой Rust Load Balancer (v0.4)
### Преимущества
```
✓ Нет SPOF - несколько инстансов на разных машинах
✓ SO_REUSEPORT - линейный scale по CPU
✓ MC-aware health check (не просто TCP, а настоящий MC ping)
✓ Hot reload без рестарта (добавить/убрать Velocity)
✓ Нативная интеграция с Redis/NATS
✓ Метрики в формате Prometheus из коробки
```
### MC-aware Health Check
```rust
// Не просто TCP connect, а настоящий MC Status ping
async fn check_velocity_health(addr: &SocketAddr) -> bool {
let mut stream = match tokio::time::timeout(
Duration::from_secs(2),
TcpStream::connect(addr)
).await {
Ok(Ok(s)) => s,
_ => return false,
};
// Шлём MC Handshake (next_state=1, status ping)
let handshake = build_mc_handshake("health.check", addr.port(), 1);
if stream.write_all(&handshake).await.is_err() { return false; }
// Шлём Status Request (0x00)
let status_req = vec![0x01, 0x00];
if stream.write_all(&status_req).await.is_err() { return false; }
// Ждём Status Response
let mut buf = vec![0u8; 1024];
match tokio::time::timeout(Duration::from_secs(1), stream.read(&mut buf)).await {
Ok(Ok(n)) if n > 5 => true,
_ => false,
}
}
```
### Hot Reload
```rust
pub struct RustLoadBalancer {
backends: Arc<ArcSwap<Vec<Backend>>>, // ArcSwap - lock-free swap
}
impl RustLoadBalancer {
// Атомарная замена списка бэкендов - без блокировки
pub async fn reload(&self, new_backends: Vec<Backend>) {
self.backends.store(Arc::new(new_backends));
// Текущие соединения не прерываются
// Новые соединения идут по новому списку
}
}
// ArcSwap из crates.io: arc-swap = "1"
```
### Несколько инстансов без SPOF
```
# На трёх разных машинах запускаем Rust LB
# Edge ноды видят все три через DNS round-robin или BGP anycast
DNS:
lb.internal A → 10.0.0.10 (LB1)
lb.internal A → 10.0.0.11 (LB2)
lb.internal A → 10.0.0.12 (LB3)
Если LB1 упал:
DNS TTL = 10 сек → edge ноды переключаются на LB2/LB3
Без keepalived, без VRRP, без единой точки отказа
```

139
docs/research/io_uring.md Normal file
View file

@ -0,0 +1,139 @@
# io_uring - Async I/O нового поколения
> Актуально: v0.4+
> Требует: Linux 5.10+ (стабильный), 6.0+ (полный функционал)
> Текущий код на tokio (epoll). io_uring - future optimization.
---
## epoll vs io_uring
```
epoll (tokio сейчас):
read() → syscall → копирование в userspace buf → возврат
На каждую операцию: минимум 1 syscall + 1 копия
io_uring:
Кладём запросы в submission queue (shared memory)
Ядро обрабатывает батчем, результаты в completion queue
Нет syscall per operation (только sq_enter раз в батч)
Нет копирования (registered buffers)
```
### Когда разница заметна
```
10k соединений: epoll ≈ io_uring (разница < 5%)
100k соединений: io_uring +15-20%
1M соединений: io_uring +35-40%
Для edge ноды с 50-200k активных соединений - заметно.
```
---
## Рантаймы сравнение
| Рантайм | Базируется на | Когда использовать |
|---|---|---|
| **tokio** (текущий) | epoll | v0.1-v0.3, универсально, стабильно |
| **tokio-uring** | io_uring | v0.4+, Linux only, edge ноды |
| **glommio** | io_uring, thread-per-core | v0.5+, высокая изоляция |
| **monoio** | io_uring, Tencent | v0.6+, максимальная пропускная способность |
> **Monoio** показывает лучшие числа на синтетических echo-бенчмарках,
> но для L7 (handshake парсинг, HMAC) разница с tokio-uring минимальна.
> Начинай с tokio, переходи на tokio-uring если профайлер покажет I/O bottleneck.
---
## Реализация через feature flag
```toml
# Cargo.toml
[features]
default = []
io-uring = ["dep:tokio-uring"]
[dependencies]
tokio = { version = "1", features = ["full"] }
tokio-uring = { version = "0.5", optional = true }
```
```rust
// src/runtime.rs
pub fn run(config: Config) {
#[cfg(feature = "io-uring")]
{
println!("Запуск с io_uring runtime");
tokio_uring::start(async { crate::edge::run(config).await });
}
#[cfg(not(feature = "io-uring"))]
{
println!("Запуск с epoll (tokio)");
tokio::runtime::Builder::new_multi_thread()
.worker_threads(num_cpus::get())
.enable_all()
.build()
.unwrap()
.block_on(crate::edge::run(config));
}
}
```
```bash
# Обычная сборка (epoll, работает везде)
cargo build --release
# С io_uring (Linux 5.10+)
cargo build --release --features io-uring
# Проверить версию ядра перед включением
uname -r # должно быть 5.10+
```
---
## Registered Buffers (продвинутый уровень)
```rust
// Регистрируем буферы один раз в ядре
// Потом read/write используют эти буферы без копирования
use tokio_uring::buf::IoBuf;
// При старте - регистрируем пул буферов
let buffers: Vec<Vec<u8>> = (0..1024)
.map(|_| vec![0u8; 4096])
.collect();
// io_uring читает прямо в зарегистрированный буфер
// Нет copy_to_user, нет дополнительной аллокации
let (result, buf) = stream.read(buf).await;
```
---
## Ограничения io_uring
```
✗ Только Linux (macOS/Windows → epoll fallback)
✗ Требует kernel 5.10+ (stable features)
✗ Некоторые VDS провайдеры блокируют io_uring
(security concerns, проверь: ls /proc/sys/kernel/io_uring_*)
✗ Не все операции имеют io_uring версии
✗ Сложнее debug (нет привычного strace для каждой операции)
```
### Проверка доступности на VDS
```bash
# Проверяем что io_uring не заблокирован
cat /proc/sys/kernel/io_uring_disabled
# 0 = разрешён, 1 = только root, 2 = запрещён
# Пробуем запустить простой io_uring тест
cargo run --example io_uring_test --features io-uring
```

View file

@ -0,0 +1,303 @@
# 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.

337
docs/research/networking.md Normal file
View file

@ -0,0 +1,337 @@
# Networking - WireGuard, BGP Anycast, QUIC, MTU
> Актуально: v0.1+ (WireGuard), v0.5+ (BGP), v0.4+ (QUIC)
---
## WireGuard - hub-and-spoke (v0.1-v0.3)
### Адресация
```
10.0.0.1 Manager + Redis + NATS (главный дедик)
10.0.0.2-21 Velocity 1-20
10.0.1.1-100 Hub 1-100
10.0.2.x Survival серверы
10.0.3.x Skyblock серверы
10.0.100.x Edge ноды (EU, US, AS...)
```
### Конфиг Manager ноды (Hub)
```ini
# /etc/wireguard/wg0.conf
[Interface]
Address = 10.0.0.1/16
PrivateKey = <MANAGER_PRIVATE_KEY>
ListenPort = 51820
# Edge нода EU
[Peer]
PublicKey = <EDGE_EU_PUBLIC_KEY>
AllowedIPs = 10.0.100.1/32
# Edge нода US
[Peer]
PublicKey = <EDGE_US_PUBLIC_KEY>
AllowedIPs = 10.0.100.2/32
# Velocity 1
[Peer]
PublicKey = <VEL1_PUBLIC_KEY>
AllowedIPs = 10.0.0.2/32
# Hub 1
[Peer]
PublicKey = <HUB1_PUBLIC_KEY>
AllowedIPs = 10.0.1.1/32
# ... и так для каждой ноды
```
### Конфиг Spoke ноды (edge, velocity, hub, game server)
```ini
# /etc/wireguard/wg0.conf на любой spoke ноде
[Interface]
Address = 10.0.100.1/32 # свой адрес в mesh
PrivateKey = <THIS_NODE_PRIVATE_KEY>
# Только один пир - Manager (Hub)
[Peer]
PublicKey = <MANAGER_PUBLIC_KEY>
Endpoint = <MANAGER_PUBLIC_IP>:51820
AllowedIPs = 10.0.0.0/16 # весь internal диапазон через hub
PersistentKeepalive = 25 # держим туннель через NAT
```
### Авто-генерация конфигов через CLI
```bash
# rampart CLI генерирует wg конфиги для всех нод
rampart wg init --network 10.0.0.0/16 --hub 185.200.100.1
rampart wg add-node --role edge --name edge-eu-1 --public-ip 45.200.10.1
rampart wg add-node --role velocity --name vel-1
rampart wg add-node --role hub --name hub-1
# Генерирует файлы:
# wg-configs/edge-eu-1/wg0.conf
# wg-configs/vel-1/wg0.conf
# ...
# Деплой на ноду
scp wg-configs/edge-eu-1/wg0.conf root@45.200.10.1:/etc/wireguard/
ssh root@45.200.10.1 'systemctl enable --now wg-quick@wg0'
```
---
## MTU - важный нюанс
```
Стандартный MTU Ethernet: 1500 байт
WireGuard overhead: ~80 байт (заголовок + шифрование)
Effective MTU в WG туннеле: 1420 байт
Если Minecraft пакет > 1420 байт → фрагментация → производительность падает.
Minecraft пакеты:
Handshake: ~50-300 байт ✅ (безопасно)
LoginStart: ~30-50 байт ✅
Chunk Data: может быть > 1420 байт ⚠️
Для chunk data: MC клиент и сервер обрабатывают фрагментацию на уровне TCP.
Для нашего edge проксирования: мы просто туннелируем TCP стрим,
фрагментация прозрачна. Проблем нет.
Настройка MTU:
```
```ini
# /etc/wireguard/wg0.conf
[Interface]
MTU = 1420 # явно указываем чтобы не было auto-discovery проблем
```
---
## Firewall - полный набор правил
```bash
#!/bin/bash
# /etc/rampart/firewall.sh
# ── Edge нода ──
setup_edge_firewall() {
iptables -F INPUT
iptables -F FORWARD
iptables -P INPUT DROP
iptables -P FORWARD DROP
# Localhost
iptables -A INPUT -i lo -j ACCEPT
# Established соединения
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# WireGuard
iptables -A INPUT -p udp --dport 51820 -j ACCEPT
# Minecraft от всех (мы принимаем атаки здесь и фильтруем)
iptables -A INPUT -p tcp --dport 25565 -j ACCEPT
# SSH (только с нашего IP управления)
iptables -A INPUT -p tcp --dport 22 -s ${MGMT_IP} -j ACCEPT
# Prometheus от Manager
iptables -A INPUT -p tcp --dport 9090 -s 10.0.0.1 -j ACCEPT
# HAProxy stats (для Prometheus)
iptables -A INPUT -p tcp --dport 8404 -s 10.0.0.0/16 -j ACCEPT
# Всё остальное - дроп
iptables -A INPUT -j DROP
}
# ── Velocity / HAProxy нода ──
setup_backend_firewall() {
iptables -F INPUT
iptables -P INPUT DROP
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# WireGuard
iptables -A INPUT -p udp --dport 51820 -j ACCEPT
# Minecraft только от edge нод через WireGuard
iptables -A INPUT -i wg0 -p tcp --dport 25565 -s 10.0.100.0/24 -j ACCEPT
# SSH
iptables -A INPUT -p tcp --dport 22 -s ${MGMT_IP} -j ACCEPT
# Prometheus сервисы (внутри WG)
iptables -A INPUT -p tcp --dport 9091 -s 10.0.0.0/16 -j ACCEPT
iptables -A INPUT -j DROP
}
# ── Game сервер ──
setup_game_firewall() {
iptables -F INPUT
iptables -P INPUT DROP
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# WireGuard
iptables -A INPUT -p udp --dport 51820 -j ACCEPT
# Minecraft только от Velocity нод
iptables -A INPUT -i wg0 -p tcp --dport 25565 -s 10.0.0.2/28 -j ACCEPT
# SSH
iptables -A INPUT -p tcp --dport 22 -s ${MGMT_IP} -j ACCEPT
# Prometheus метрики Paper
iptables -A INPUT -p tcp --dport 9092 -s 10.0.0.0/16 -j ACCEPT
iptables -A INPUT -j DROP
}
```
```
---
## QUIC - канал Edge ↔ Manager (v0.4+)
### Зачем для управляющего канала
```
TCP проблема: Head-of-line blocking
Большой blacklist update → блокирует heartbeat → edge думает что manager упал
QUIC решение: независимые streams
Stream 0: heartbeat (5 сек) - не блокируется
Stream 1: blacklist updates (push) - независимо
Stream 2: metrics (1 сек) - независимо
Stream 3: команды (drain/reload) - независимо
+ 0-RTT reconnect после разрыва (важно для мобильных VDS с нестабильным uplink)
+ Встроенный TLS 1.3 (не нужен отдельный слой)
```
### Реализация (quinn)
```toml
[dependencies]
quinn = "0.11"
```
```rust
// manager/src/quic.rs
pub async fn start_quic_server(config: Arc<Config>) -> Result<()> {
let tls = build_quic_server_tls(&config.tls);
let endpoint = quinn::Endpoint::server(tls, "0.0.0.0:7777".parse()?)?;
while let Some(incoming) = endpoint.accept().await {
let conn = incoming.await?;
// Получаем identity подключившейся edge ноды из сертификата
let node_id = extract_node_id(&conn);
tokio::spawn(handle_edge(conn, node_id));
}
Ok(())
}
async fn handle_edge(conn: quinn::Connection, node_id: String) {
// Открываем исходящие streams для push уведомлений
let blacklist_tx = conn.open_uni().await.unwrap();
// Слушаем входящие streams (heartbeat, metrics)
loop {
match conn.accept_bi().await {
Ok((tx, rx)) => {
tokio::spawn(handle_stream(tx, rx, node_id.clone()));
}
Err(_) => {
tracing::warn!("Edge нода {} отключилась", node_id);
break;
}
}
}
}
```
---
## BGP Anycast (v0.6+)
> Только если проект вырастет до 10+ edge нод и нужен настоящий anycast.
### Что нужно
```
1. Свой AS номер - получить через RIPE NCC (Европа) или ARIN (США)
Стоимость: ~500€/год членский взнос в RIPE
Плюс: купить через LIR (Local Internet Registry) - дешевле
2. Своя /24 подсеть - 256 IP адресов
Получить вместе с AS через RIPE
Стоимость: включено в RIPE членство
3. VDS с поддержкой BGP сессий
Vultr, Hetzner (не все локации), Leaseweb, OVH Premium
Проверять явно: "BGP sessions supported"
4. FRRouting на каждой edge ноде
```
### FRRouting конфиг
```ini
# /etc/frr/frr.conf на edge ноде
router bgp 65001
bgp router-id 185.200.100.1
# BGP сессия с upstream провайдером
neighbor 149.248.2.1 remote-as 20473
neighbor 149.248.2.1 description "Vultr upstream"
address-family ipv4 unicast
# Анонсируем свою подсеть с этой edge ноды
network 185.200.100.0/24
# NO_EXPORT - не распространяем анонс дальше (только к upstream)
neighbor 149.248.2.1 route-map SET_COMMUNITY out
exit-address-family
route-map SET_COMMUNITY permit 10
set community no-export
! Когда edge нода падает - FRRouting перестаёт анонсировать
! BGP withdraw → трафик автоматически идёт на другую ноду
! Время failover: ~30-60 сек (BGP convergence)
```
### Как это работает
```
play.server.com → 185.200.100.1 (один IP, твоя подсеть)
Игрок из Европы:
BGP → ближайшая нода которая анонсирует 185.200.100.0/24 → edge-eu-1
Игрок из США:
BGP → ближайшая нода → edge-us-1
edge-eu-1 упала → FRRouting делает withdraw →
Европейский трафик → автоматически → edge-us-1 или edge-as-1
Время: 30-60 сек
```

View file

@ -0,0 +1,361 @@
# Observability - Метрики, Трейсинг, Логи
> Актуально: v0.3+
---
## Стек
```
Метрики: Prometheus → VictoriaMetrics (долгосрочное хранение)
Трейсинг: OpenTelemetry → Grafana Tempo
Логи: трейсинг → Loki
Дашборды: Grafana
Атаки: ClickHouse (аналитика за месяцы)
Профайлинг: Parca (continuous)
Debug: tokio-console (async tasks)
```
---
## Что собираем с каждого компонента
### Edge нода (Rust) → порт 9090
```
rampart_connections_total{node, result} - total/blocked/allowed
rampart_active_connections{node}
rampart_bytes_proxied_total{node, dir} - in/out
rampart_handshake_parse_errors_total{node, reason}
rampart_rate_limit_hits_total{node}
rampart_blacklist_size{node}
rampart_xdp_drops_total{node, reason} - если XDP включён
# Гистограммы (важны для P99)
rampart_handshake_duration_seconds{node}
rampart_proxy_latency_seconds{node}
rampart_hmac_verify_duration_seconds{node}
```
### Velocity нода (Java) → порт 9091
```
velocity_players_online
velocity_domain_check_failures_total{reason}
velocity_hmac_check_failures_total
velocity_server_registry_size{type} - hub/survival/skyblock
velocity_balancer_decisions_total{strategy, server_type}
velocity_redis_latency_seconds - гистограмма
```
### Game сервер (Paper агент) → порт 9092
```
paper_tps{server, interval} - 1m/5m/15m
paper_mspt{server} - мс на тик
paper_players_online{server}
paper_chunks_loaded{server}
paper_entities_total{server}
paper_memory_used_bytes{server}
paper_memory_max_bytes{server}
paper_gc_pause_seconds{server} - GC паузы
```
---
## Prometheus конфиг с авто-дискавери
```yaml
# prometheus.yml
scrape_configs:
- job_name: 'rampart-edge'
static_configs:
- targets: ['10.0.100.1:9090', '10.0.100.2:9090']
- job_name: 'rampart-velocity'
static_configs:
- targets: ['10.0.0.2:9091', '10.0.0.3:9091']
# Game серверы - авто-дискавери (Manager генерирует файл из Redis)
- job_name: 'paper-servers'
file_sd_configs:
- files: ['/etc/prometheus/game_servers.json']
refresh_interval: 30s
- job_name: 'haproxy'
static_configs:
- targets: ['10.0.0.1:8404']
```
### Авто-генерация game_servers.json
```rust
// Manager генерирует файл каждые 30 сек
async fn generate_sd_file(redis: &Redis) {
let servers: Vec<serde_json::Value> = redis
.hgetall("rampart:servers").await
.values()
.map(|raw| {
let s: ServerEntry = serde_json::from_str(raw).unwrap();
serde_json::json!({
"targets": [format!("{}:9092", s.ip)],
"labels": { "server": s.name, "type": s.server_type }
})
})
.collect();
std::fs::write(
"/etc/prometheus/game_servers.json",
serde_json::to_string_pretty(&servers).unwrap()
).unwrap();
}
```
---
## Alerting правила
```yaml
# alerts.yml
groups:
- name: rampart-critical
rules:
- alert: DDoSAttack
expr: |
rate(rampart_connections_total{result="blocked"}[1m])
/ rate(rampart_connections_total[1m]) > 0.8
for: 30s
annotations:
summary: "DDoS атака на {{ $labels.node }}"
- alert: EdgeNodeDown
expr: up{job="rampart-edge"} == 0
for: 10s
annotations:
summary: "Edge нода {{ $labels.instance }} недоступна"
- alert: VelocityNodeDown
expr: up{job="rampart-velocity"} == 0
for: 15s
- alert: LowTPS
expr: paper_tps{interval="1m"} < 15
for: 2m
annotations:
summary: "Низкий TPS на {{ $labels.server }}: {{ $value }}"
- alert: HighMSPT
expr: paper_mspt > 45
for: 1m
annotations:
summary: "Высокий MSPT: {{ $value }}ms на {{ $labels.server }}"
```
### Алерт в Discord
```yaml
# alertmanager.yml
receivers:
- name: discord
webhook_configs:
- url: "${DISCORD_WEBHOOK}"
send_resolved: true
http_config:
headers:
Content-Type: application/json
title: '{{ .GroupLabels.alertname }}'
text: |
{{ range .Alerts }}
**{{ .Annotations.summary }}**
{{ end }}
```
---
## Push vs Pull
**Проблема:** Edge ноды - дешёвые VDS по всему миру, часто за NAT, с динамическими IP. Prometheus pull (scrape) не сработает если нода за NAT или firewall.
**Решение:**
```
Edge ноды → vmagent (push через remote_write) или OTel Collector
Причина: edge за NAT, динамические IP, firewall блокирует входящие
Manager / Velocity / HAProxy → Prometheus pull (статичные IP внутри WG сети)
```
### Схема
```
┌──────────────┐
│ VictoriaMetrics │
│ (remote_write) │
└───────┬──────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
vmagent Prometheus Prometheus
(edge-eu-1) (manager) (velocity)
push pull pull
```
### Конфиг vmagent для edge ноды
```yaml
# /etc/vmagent.yml
remote_write:
- url: "https://victoria.rampart.internal/api/v1/write"
scrape_configs:
- job_name: 'rampart-edge'
static_configs:
- targets: ['127.0.0.1:9090'] # localhost - не требует доступа извне
```
---
## OpenTelemetry - distributed tracing
```rust
// src/telemetry.rs
use opentelemetry_otlp::WithExportConfig;
use tracing_opentelemetry::OpenTelemetryLayer;
pub fn init(service: &str, otlp_endpoint: &str) {
let tracer = opentelemetry_otlp::new_pipeline()
.tracing()
.with_exporter(
opentelemetry_otlp::new_exporter()
.tonic()
.with_endpoint(otlp_endpoint)
)
.install_batch(opentelemetry_sdk::runtime::Tokio)
.unwrap();
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::from_default_env())
.with(OpenTelemetryLayer::new(tracer))
.init();
}
// Использование - автоматически создаёт spans
#[tracing::instrument(skip(stream, config))]
pub async fn handle_connection(stream: TcpStream, config: Arc<Config>) {
let handshake = parse_handshake(&stream).await; // child span
filter_request(&handshake).await; // child span
proxy_to_backend(stream).await; // child span
}
```
---
## ClickHouse - attack log
### Почему не PostgreSQL
```
SELECT count() WHERE country='CN' AND ts > now()-24h
PostgreSQL: ~2 сек на 100M строк
ClickHouse: ~50 мс на 100M строк
Сжатие: PostgreSQL ~3:1, ClickHouse ~10:1
```
### Схема
```sql
CREATE TABLE 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;
-- Materialized View для агрегатов (не пересчитываем каждый раз)
CREATE MATERIALIZED VIEW rampart.blocked_by_country_mv
ENGINE = SummingMergeTree()
ORDER BY (toDate(ts), src_country)
AS SELECT toDate(ts) as date, src_country, count() as hits
FROM rampart.blocked GROUP BY date, src_country;
```
### Батч запись из Rust
```rust
// Не пишем на каждый пакет - накапливаем и сбрасываем раз в секунду
pub struct ClickHouseWriter {
client: clickhouse::Client,
buffer: Mutex<Vec<BlockedEvent>>,
}
impl ClickHouseWriter {
pub async fn flush(&self) {
let records = { self.buffer.lock().await.drain(..).collect::<Vec<_>>() };
if records.is_empty() { return; }
let mut insert = self.client.insert("rampart.blocked").unwrap();
for r in &records { insert.write(r).await.unwrap(); }
insert.end().await.unwrap();
}
}
```
---
## Parca - continuous profiling
```yaml
# docker-compose.yml дополнение
parca:
image: ghcr.io/parca-dev/parca:latest
ports:
- "7070:7070"
volumes:
- ./parca.yaml:/etc/parca/parca.yaml
# parca.yaml
object_storage:
bucket:
type: FILESYSTEM
config:
directory: /tmp/parca
scrape_configs:
- job_name: 'rampart-edge'
scrape_interval: 10s
targets:
- targets: ['10.0.100.1:7071'] # pprof endpoint
```
```rust
// Включаем pprof endpoint в edge ноде
use pprof::ProfilerGuard;
// GET /debug/pprof/profile → CPU flame graph
// GET /debug/pprof/heap → heap allocation graph
```
---
## tokio-console - debug async tasks
```bash
# Запуск edge с поддержкой tokio-console
TOKIO_CONSOLE_BIND=10.0.100.1:6669 \
RUST_LOG=tokio=trace \
./rampart-edge
# Подключение (на своей машине)
tokio-console http://10.0.100.1:6669
# Видишь все async tasks, их состояние, сколько они poll'ятся
```

184
docs/research/papers.md Normal file
View file

@ -0,0 +1,184 @@
# Papers & References - Материалы для изучения
> Ссылки на статьи, RFC, проекты, инструменты которые легли в основу Rampart.
---
## Minecraft протокол
| Ресурс | Зачем |
|---|---|
| [wiki.vg/Protocol](https://wiki.vg/Protocol) | Официальная неофициальная документация MC протокола. Handshake, VarInt, все пакеты. |
| [wiki.vg/Handshaking_sequence](https://wiki.vg/Handshaking_sequence) | Полная последовательность handshake → login → play |
| [Velocity источник](https://github.com/PaperMC/Velocity) | Как PaperMC парсит MC протокол в Java - референс |
| [Pumpkin-MC](https://github.com/Snowiiii/Pumpkin) | MC сервер на Rust - референс для Rust парсинга протокола |
---
## eBPF / XDP
| Ресурс | Зачем |
|---|---|
| [Outfluencer/Minecraft-XDP-eBPF](https://github.com/Outfluencer/Minecraft-XDP-eBPF) | Референс: XDP фильтр специально для Minecraft (Rust + C, 190+ stars) |
| [xdp-project/xdp-tutorial](https://github.com/xdp-project/xdp-tutorial) | Лучший туториал по XDP - от простого к сложному |
| [libbpf-bootstrap](https://github.com/libbpf/libbpf-bootstrap) | Шаблоны eBPF программ с современным подходом (skeleton, CO-RE) |
| [aya-rs/aya](https://github.com/aya-rs/aya) | Альтернатива libbpf-rs - eBPF полностью на Rust (без C) |
| [BPF Performance Tools](https://www.brendangregg.com/bpf-performance-tools-book.html) | Книга Brendan Gregg - глубокий разбор BPF/eBPF |
| [Cloudflare: XDP введение](https://blog.cloudflare.com/l4drop-xdp-ebpf-based-ddos-mitigations/) | Как Cloudflare использует XDP для DDoS mitigation |
| [Facebook: XDP at scale](https://engineering.fb.com/2018/05/22/open-source/open-sourcing-katran-a-scalable-network-load-balancer/) | Katran - XDP load balancer от Facebook |
---
## Rust networking
| Ресурс | Зачем |
|---|---|
| [tokio-rs/tokio](https://github.com/tokio-rs/tokio) | Async runtime - основа edge ноды |
| [tokio-rs/tokio-uring](https://github.com/tokio-rs/tokio-uring) | io_uring runtime для tokio |
| [bytedance/monoio](https://github.com/bytedance/monoio) | Thread-per-core io_uring runtime от ByteDance |
| [glommio](https://github.com/DataDog/glommio) | io_uring runtime от DataDog |
| [rustls](https://github.com/rustls/rustls) | TLS на Rust - для mTLS |
| [quinn-rs/quinn](https://github.com/quinn-rs/quinn) | QUIC реализация на Rust |
| [zero-copy-paxos](https://www.usenix.org/conference/osdi14/technical-sessions/presentation/ports) | Статья о zero-copy в системных сервисах |
| [Uring и io_uring (LWN)](https://lwn.net/Articles/776703/) | Детальный разбор io_uring от автора |
---
## DDoS защита и сети
| Ресурс | Зачем |
|---|---|
| [Cloudflare Blog: DDoS](https://blog.cloudflare.com/tag/ddos/) | Статьи Cloudflare о реальных атаках и защите |
| [Path.net технический блог](https://path.net/blog/) | Как устроена игровая DDoS защита |
| [RFC 4271](https://datatracker.ietf.org/doc/html/rfc4271) | BGP - основа Anycast маршрутизации |
| [RFC 9000](https://datatracker.ietf.org/doc/html/rfc9000) | QUIC протокол (официальный RFC) |
| [WireGuard whitepaper](https://www.wireguard.com/papers/wireguard.pdf) | Технический документ WireGuard |
| [Hping3 man page](https://linux.die.net/man/8/hping3) | Инструмент для тестирования защиты |
| [tcpkali](https://github.com/satori-com/tcpkali) | Benchmark инструмент для TCP |
---
## Балансировка и прокси
| Ресурс | Зачем |
|---|---|
| [Envoy proxy docs](https://www.envoyproxy.io/docs/envoy/latest/) | EWMA, Circuit Breaker, xDS - референс архитектуры |
| [HAProxy конфигурация](https://www.haproxy.org/download/2.8/doc/configuration.txt) | Полная документация HAProxy |
| [Consistent Hashing paper](https://dl.acm.org/doi/10.1145/258533.258660) | Оригинальная статья Karger et al. 1997 |
| [EWMA в Envoy](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/load_balancing/load_balancers#weighted-least-request) | Как Envoy реализует EWMA балансировку |
| [Nginx SO_REUSEPORT](https://nginx.org/en/docs/http/ngx_http_upstream_module.html) | Как Nginx использует SO_REUSEPORT |
---
## Наблюдаемость
| Ресурс | Зачем |
|---|---|
| [OpenTelemetry](https://opentelemetry.io/docs/) | Официальная документация OTel |
| [ClickHouse docs](https://clickhouse.com/docs) | Документация ClickHouse - схемы, запросы |
| [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics) | Prometheus-совместимое хранилище для долгосрочных метрик |
| [Grafana Tempo](https://grafana.com/oss/tempo/) | Хранилище distributed traces |
| [Parca](https://github.com/parca-dev/parca) | Continuous profiling для production |
| [tokio-console](https://github.com/tokio-rs/console) | Debug async tokio tasks |
| [Brendan Gregg: Flame Graphs](https://www.brendangregg.com/flamegraphs.html) | Методология профилирования через flame graphs |
---
## Безопасность
| Ресурс | Зачем |
|---|---|
| [STRIDE модель](https://docs.microsoft.com/en-us/azure/security/develop/threat-modeling-tool-threats) | Методология threat modeling |
| [subtle crate](https://docs.rs/subtle/) | Constant-time операции в Rust |
| [HMAC RFC 2104](https://datatracker.ietf.org/doc/html/rfc2104) | Оригинальный HMAC RFC |
| [cargo-audit](https://github.com/rustsec/rustsec) | CVE проверка Rust зависимостей |
| [cargo-deny](https://github.com/EmbarkStudios/cargo-deny) | Политики лицензий и зависимостей |
| [SLSA framework](https://slsa.dev/) | Supply chain security уровни |
| [cosign](https://github.com/sigstore/cosign) | Подпись Docker образов |
---
## Смежные open-source проекты
| Проект | Язык | Что взять |
|---|---|---|
| [Velocity](https://github.com/PaperMC/Velocity) | Java | MC proxy - основа нашего плагина |
| [Gate (Minekube)](https://github.com/minekube/gate) | Go | Высокопроизводительный MC proxy - архитектурный референс |
| [Minecraft-XDP-eBPF](https://github.com/Outfluencer/Minecraft-XDP-eBPF) | Rust+C | XDP для Minecraft - брать за основу XDP компонента |
| [Sonar](https://github.com/jonesdevelopment/sonar) | Java | Antibot для Velocity - интегрируем как слой |
| [RedisBungee-Reloaded](https://github.com/ProxioDev/RedisBungee) | Java | Cross-proxy синхронизация - референс |
| [VeloFlame](https://github.com/) | Java | Velocity форк с встроенным антиботом (июль 2026) |
| [Pumpkin-MC](https://github.com/Snowiiii/Pumpkin) | Rust | MC сервер на Rust - референс протокола |
| [Katran](https://github.com/facebookincubator/katran) | C++ | XDP load balancer от Facebook - архитектурный референс |
| [NATS](https://github.com/nats-io/nats-server) | Go | Event bus - используем для критических событий |
| [FRRouting](https://github.com/FRRouting/frr) | C | BGP routing - для Anycast в v0.6+ |
| [headscale](https://github.com/juanfont/headscale) | Go | Self-hosted WireGuard координатор - для v0.6+ |
---
## Статьи и блоги по теме
| Статья | Почему стоит прочитать |
|---|---|
| [How TCPShield works](https://tcpshield.com/blog/) | Понять конкурента изнутри |
| [Cloudflare: Lessons from protecting 26M HTTP RPS](https://blog.cloudflare.com/ddos-threat-report-for-2024-q4/) | Реальная статистика DDoS атак |
| [Linux networking performance](https://talawah.io/blog/linux-kernel-vs-dpdk-http-performance-showdown/) | Kernel vs DPDK vs XDP сравнение |
| [Tokio internals](https://tokio.rs/blog/2019-10-scheduler) | Как работает tokio scheduler |
| [io_uring в production](https://developers.mattermost.com/blog/hands-on-iouring-go/) | Реальный опыт io_uring |
| [eBPF maps deep dive](https://prototype-kernel.readthedocs.io/en/latest/bpf/ebpf_maps.html) | BPF map типы, когда что использовать |
---
## RFC для изучения
| RFC | Тема |
|---|---|
| RFC 793 | TCP - основа всего |
| RFC 4271 | BGP-4 |
| RFC 4786 | Anycast через BGP |
| RFC 7413 | TCP Fast Open |
| RFC 9000 | QUIC Transport |
| RFC 9001 | QUIC + TLS 1.3 |
| RFC 8446 | TLS 1.3 |
| RFC 2104 | HMAC |
| RFC 5246 | TLS 1.2 (для совместимости) |
---
## Инструменты для разработки
```bash
# Анализ трафика
wireshark # GUI пакетный анализатор
tshark # CLI версия wireshark
tcpdump # быстрый захват пакетов
# Benchmark
tcpkali # TCP нагрузочное тестирование
iperf3 # bandwidth тест
hping3 # генерация специфических пакетов
wrk # HTTP benchmark (для Manager API)
# eBPF отладка
bpftool # управление BPF программами и картами
bpftrace # скриптовый язык для eBPF
strace # системные вызовы (для userspace)
# Rust
cargo-flamegraph # flame graphs
cargo-criterion # benchmark с HTML отчётами
cargo-audit # CVE проверка
cargo-deny # политики зависимостей
tokio-console # async tasks debug
# Сеть
wireguard-tools # wg, wg-quick
frr # FRRouting (BGP)
iptables/nftables # firewall
# Мониторинг
prometheus # метрики
grafana # дашборды
clickhouse # attack log аналитика
parca # continuous profiling
```

View file

@ -0,0 +1,312 @@
# Rust Performance - Zero-Copy, SO_REUSEPORT, NUMA
> Актуально: v0.3+
---
## Zero-Copy проксирование
```
Обычный proxy (2 копии):
NIC → kernel buf → copy → userspace buf → copy → kernel buf → NIC
splice(2) zero-copy (0 копий в userspace):
NIC → kernel pipe → NIC
Данные никогда не покидают kernel
```
### Когда применять
```
Handshake фаза → обычный read() (нужно видеть байты, парсить, ставить HMAC)
После handshake → zero-copy splice (просто проксируем стрим)
```
```rust
// src/proxy/tunnel.rs
use tokio_splice::zero_copy_bidirectional;
pub async fn tunnel(mut client: TcpStream, mut backend: TcpStream) {
// После того как handshake прочитан и HMAC добавлен -
// всё остальное идёт через splice(2) без копий в userspace
let _ = zero_copy_bidirectional(&mut client, &mut backend).await;
}
```
---
## SO_REUSEPORT - линейный scale по CPU
```rust
// main.rs - N воркеров, каждый слушает тот же порт
// Ядро само балансирует входящие SYN между воркерами
use socket2::{Domain, Socket, Type};
fn build_listener(addr: SocketAddr) -> TcpListener {
let socket = Socket::new(Domain::IPV4, Type::STREAM, None).unwrap();
socket.set_reuse_port(true).unwrap(); // SO_REUSEPORT
socket.set_reuse_address(true).unwrap();
socket.set_nonblocking(true).unwrap();
socket.bind(&addr.into()).unwrap();
socket.listen(65535).unwrap();
TcpListener::from_std(socket.into()).unwrap()
}
#[tokio::main]
async fn main() {
let addr: SocketAddr = "0.0.0.0:25565".parse().unwrap();
let cpus = num_cpus::get();
let handles: Vec<_> = (0..cpus)
.map(|_| tokio::spawn(accept_loop(build_listener(addr))))
.collect();
futures::future::join_all(handles).await;
}
```
### Ожидаемый прирост
| Ядра | Без SO_REUSEPORT | С SO_REUSEPORT |
|---|---|---|
| 1 | 20k conn/s | 20k conn/s |
| 4 | 22k conn/s | 78k conn/s |
| 8 | 23k conn/s | 155k conn/s |
---
## Buffer Pool - без heap allocation на каждый пакет
```rust
// src/pool.rs - пул буферов, переиспользуем вместо Vec::new()
// ⚠ ВАЖНО: tokio::sync::Mutex блокирует async runtime в hot path.
// Используем crossbeam::ArrayQueue - lock-free, не блокирует.
use crossbeam::queue::ArrayQueue;
use std::sync::Arc;
pub struct BufferPool {
pool: Arc<ArrayQueue<Vec<u8>>>,
buf_size: usize,
}
impl BufferPool {
pub fn new(capacity: usize, buf_size: usize) -> Self {
let pool = ArrayQueue::new(capacity);
for _ in 0..capacity {
pool.push(vec![0u8; buf_size]).ok();
}
Self { pool: Arc::new(pool), buf_size }
}
// Не async! Не блокирует runtime.
pub fn acquire(&self) -> Vec<u8> {
self.pool.pop().unwrap_or_else(|| vec![0u8; self.buf_size])
}
// Не async! Не блокирует runtime.
pub fn release(&self, mut buf: Vec<u8>) {
buf.clear();
let _ = self.pool.push(buf); // игнорируем если полон
}
}
```
---
## DashMap - lock-free concurrent HashMap
```rust
// Блэклист и rate limit - читаются на каждый пакет
// RwLock<HashMap> создаёт contention под нагрузкой
// DashMap решает это через шарды
use dashmap::DashMap;
pub struct Blacklist {
// 64 шарда, каждый со своим RwLock
// Разные IP попадают в разные шарды → нет contention
ips: DashMap<Ipv4Addr, BanEntry>,
}
impl Blacklist {
pub fn is_blocked(&self, ip: Ipv4Addr) -> bool {
if let Some(entry) = self.ips.get(&ip) {
if entry.expires > Instant::now() {
return true;
}
drop(entry);
self.ips.remove(&ip); // expired
}
false
}
}
```
---
## io_uring - async I/O нового поколения (v0.4+, future optimization)
> Текущий код на tokio (epoll). io_uring - future optimization для edge нод.
### epoll vs io_uring
```
epoll (tokio сейчас):
read() → syscall → копирование в userspace buf → возврат
На каждую операцию: минимум 1 syscall + 1 копия
io_uring:
Кладём запросы в submission queue (shared memory)
Ядро обрабатывает батчем, результаты в completion queue
Нет syscall per operation (только sq_enter раз в батч)
Нет копирования (registered buffers)
```
### Когда разница заметна
```
10k соединений: epoll ≈ io_uring (разница < 5%)
100k соединений: io_uring +15-20%
1M соединений: io_uring +35-40%
```
### Рантаймы сравнение
| Рантайм | Базируется на | Когда использовать |
|---|---|---|
| **tokio** (текущий) | epoll | v0.1-v0.3, универсально |
| **tokio-uring** | io_uring | v0.4+, Linux only |
| **glommio** | io_uring, thread-per-core | v0.5+, высокая изоляция |
| **monoio** | io_uring, Tencent | v0.6+, максимальная пропускная способность |
### Реализация через feature flag
```toml
# Cargo.toml
[features]
default = []
io-uring = ["dep:tokio-uring"]
[dependencies]
tokio = { version = "1", features = ["full"] }
tokio-uring = { version = "0.5", optional = true }
```
```rust
// src/runtime.rs
pub fn run(config: Config) {
#[cfg(feature = "io-uring")]
{
tracing::info!("Запуск с io_uring runtime");
tokio_uring::start(async { crate::edge::run(config).await });
}
#[cfg(not(feature = "io-uring"))]
{
tracing::info!("Запуск с epoll (tokio)");
tokio::runtime::Builder::new_multi_thread()
.worker_threads(num_cpus::get())
.enable_all()
.build()
.unwrap()
.block_on(crate::edge::run(config));
}
}
```
```bash
# Обычная сборка (epoll, работает везде)
cargo build --release
# С io_uring (Linux 5.10+)
cargo build --release --features io-uring
```
### Registered Buffers
```rust
// Регистрируем буферы один раз в ядре
// Потом read/write используют эти буферы без копирования
use tokio_uring::buf::IoBuf;
let buffers: Vec<Vec<u8>> = (0..1024)
.map(|_| vec![0u8; 4096])
.collect();
// io_uring читает прямо в зарегистрированный буфер
// Нет copy_to_user, нет дополнительной аллокации
let (result, buf) = stream.read(buf).await;
```
### Ограничения io_uring
```
✗ Только Linux (macOS/Windows → epoll fallback)
✗ Требует kernel 5.10+ (stable features)
✗ Некоторые VDS провайдеры блокируют io_uring
(проверь: cat /proc/sys/kernel/io_uring_disabled)
```
---
## NUMA-aware allocation (для 2-сокетных серверов)
> Актуально для bare metal с 2 физическими CPU (NUMA topology)
```rust
// Привязываем воркеры к NUMA нодам
// Память аллоцируется близко к CPU который её использует
use nix::sched::{sched_setaffinity, CpuSet};
fn pin_to_numa_node(worker_id: usize, numa_node: usize) {
let mut cpuset = CpuSet::new();
// NUMA node 0: CPU 0-7, NUMA node 1: CPU 8-15 (пример)
let cpu_start = numa_node * 8;
let cpu_for_worker = cpu_start + (worker_id % 8);
cpuset.set(cpu_for_worker).unwrap();
sched_setaffinity(Pid::from_raw(0), &cpuset).unwrap();
}
```
Для обычных VDS (1 NUMA нода) - не нужно.
---
## Profiling в production
```bash
# tokio-console - live view async tasks
# Запускаем edge с поддержкой tokio-console
TOKIO_CONSOLE_BIND=10.0.100.1:6669 ./rampart-edge
# На своей машине
tokio-console http://10.0.100.1:6669
# Parca - continuous profiling (CPU flame graphs)
docker run -p 7070:7070 ghcr.io/parca-dev/parca:latest
# Смотрим в браузере: http://localhost:7070
# perf (Linux)
perf record -g -p $(pgrep rampart-edge) -- sleep 30
perf report --stdio | head -50
# Flamegraph
cargo flamegraph --bin rampart-edge
```
---
## Сводная таблица оптимизаций
| Техника | Прирост | Версия | Сложность |
|---|---|---|---|
| SO_REUSEPORT | 4x на 4 ядрах | v0.1 | Низкая |
| DashMap вместо RwLock | 2x при contention | v0.1 | Низкая |
| Buffer pool | -30% alloc | v0.2 | Средняя |
| Zero-copy splice | -50% CPU на трафик | v0.2 | Средняя |
| io_uring | +30-40% conn/s | v0.4 | Высокая |
| XDP | 10x дроп rate | v0.4 | Высокая |
| NUMA pinning | +10-20% на 2P сервере | v0.6 | Высокая |

262
docs/research/security.md Normal file
View file

@ -0,0 +1,262 @@
# Security - STRIDE, mTLS, Zero Trust, Supply Chain
> Актуально: v0.2+
---
## STRIDE Threat Model
| Угроза | Конкретно | Защита |
|---|---|---|
| **S**poofing | Атакующий подделывает IP edge ноды | mTLS (сертификат не подделать) + WireGuard |
| **T**ampering | Подмена HMAC в hostname | HMAC-SHA256 + constant-time compare |
| **R**epudiation | Нет доказательств кто добавил IP в блэклист | Аудит лог (user, ts, action, IP) в ClickHouse |
| **I**nfo Disclosure | Утечка реального IP backend | Всё за WireGuard + iptables DROP |
| **D**oS | Перегрузка edge ноды | XDP + rate limit + challenge |
| **E**scalation | Доступ к Manager API без авторизации | JWT + mTLS + IP whitelist + rate limit |
---
## Zero Trust - принципы
```
1. Никому не доверяй по умолчанию - даже внутри WireGuard сети
2. Проверяй каждый компонент - mTLS между всеми сервисами
3. Минимальные привилегии - каждый компонент видит только нужное
4. Логируй всё - аудит лог каждого действия
Применение в Rampart:
Edge нода → HAProxy/LB: mTLS (сертификат edge ноды)
LB → Velocity: mTLS (сертификат LB)
Velocity → Redis: пароль + только WireGuard IP
Manager API: JWT + mTLS + IP whitelist
```
---
## mTLS - схема сертификатов
```
Root CA (rampart-ca)
├── Intermediate CA (edge-ca)
│ ├── edge-eu-1.crt
│ ├── edge-us-1.crt
│ └── edge-as-1.crt
├── Intermediate CA (infra-ca)
│ ├── haproxy.crt
│ ├── velocity-1.crt ... velocity-20.crt
│ ├── manager.crt
│ └── dashboard.crt
└── Intermediate CA (game-ca)
├── hub-1.crt ... hub-100.crt
└── (game серверам не нужен mTLS - они за Velocity)
```
### Генерация через CLI
```bash
# Инициализация PKI (один раз)
rampart pki init \
--root-ca rampart-ca \
--output /etc/rampart/pki/
# Выпуск сертификата для новой edge ноды
rampart pki issue \
--ca edge-ca \
--name edge-us-2 \
--ip 10.0.100.5 \
--san "edge-us-2.rampart.internal" \
--output /etc/rampart/pki/edge-us-2/
# Ротация (раз в год, автоматически через cron)
rampart pki rotate --role edge --days-before-expiry 30
```
### Реализация в Rust (rustls)
```rust
// tls.rs
use rustls::{ServerConfig, ClientConfig, RootCertStore};
use tokio_rustls::{TlsAcceptor, TlsConnector};
pub fn server_config(cert: &str, key: &str, ca: &str) -> Arc<ServerConfig> {
let mut root_store = RootCertStore::empty();
root_store.add(load_cert(ca)).unwrap();
Arc::new(ServerConfig::builder()
// Требуем клиентский сертификат (mutual)
.with_client_cert_verifier(
WebPkiClientVerifier::builder(Arc::new(root_store))
.build().unwrap()
)
.with_single_cert(load_certs(cert), load_key(key))
.unwrap())
}
pub fn client_config(cert: &str, key: &str, ca: &str) -> Arc<ClientConfig> {
let mut root_store = RootCertStore::empty();
root_store.add(load_cert(ca)).unwrap();
Arc::new(ClientConfig::builder()
.with_root_certificates(root_store)
.with_client_auth_cert(load_certs(cert), load_key(key))
.unwrap())
}
```
---
## HMAC - правильная реализация
```rust
// hmac/signer.rs
use hmac::{Hmac, Mac};
use sha2::Sha256;
// ВАЖНО: subtle для constant-time сравнения (защита от timing атак)
use subtle::ConstantTimeEq;
type HmacSha256 = Hmac<Sha256>;
pub fn sign(hostname: &str, secret: &[u8]) -> String {
let mut mac = HmacSha256::new_from_slice(secret)
.expect("HMAC accepts any key length");
mac.update(hostname.as_bytes());
hex::encode(mac.finalize().into_bytes())
}
pub fn verify(hostname: &str, provided_sig: &str, secret: &[u8]) -> bool {
let expected = sign(hostname, secret);
// constant_time_eq - время сравнения не зависит от содержимого
// Без этого атакующий может угадать HMAC по времени ответа
expected.as_bytes().ct_eq(provided_sig.as_bytes()).into()
}
// Добавляем к hostname: "play.server.com\0shield\0<hex_hmac>"
pub fn sign_hostname(raw: &str, secret: &[u8]) -> String {
// Берём только domain часть (без Forge суффиксов)
let domain = raw.split('\0').next().unwrap_or(raw);
let sig = sign(domain, secret);
format!("{}\0shield\0{}", raw, sig) // сохраняем Forge суффикс
}
```
---
## Аудит лог
```rust
// Каждое административное действие записывается
#[derive(Serialize, Deserialize, Clickhouse)]
pub struct AuditEntry {
pub ts: DateTime<Utc>,
pub user: String, // кто сделал
pub action: String, // "blacklist.add" / "server.remove" / "config.change"
pub target: String, // "1.2.3.4" / "survival_47"
pub details: String, // JSON с деталями
pub src_ip: String, // откуда был запрос
pub success: bool,
}
// Вставляем в ClickHouse (не в Redis - нужна долгосрочная история)
pub async fn audit(entry: AuditEntry) {
clickhouse_client
.insert("rampart.audit_log")
.write(&entry)
.await
.ok(); // не прерываем основной флоу если аудит упал
}
```
---
## Защита Redis
```bash
# redis.conf
bind 10.0.0.1 # только WireGuard IP (не 0.0.0.0!)
requirepass "LONG_RANDOM_PASSWORD_HERE"
protected-mode yes
rename-command FLUSHALL "" # запрещаем опасные команды
rename-command FLUSHDB ""
rename-command DEBUG ""
rename-command CONFIG "CONFIG_RESTRICTED_CMD"
# Firewall - дополнительный слой
iptables -A INPUT -p tcp --dport 6379 -s 10.0.0.0/16 -j ACCEPT
iptables -A INPUT -p tcp --dport 6379 -j DROP
```
---
## Supply Chain Security (v0.5+)
```yaml
# .github/workflows/supply-chain.yml
cargo-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: cargo install cargo-audit
- run: cargo audit # проверяем CVE в зависимостях
cargo-deny:
runs-on: ubuntu-latest
steps:
- uses: EmbarkStudios/cargo-deny-action@v1
with:
command: check all # лицензии, дублирования, CVE
sbom:
runs-on: ubuntu-latest
steps:
- uses: anchore/sbom-action@v0 # генерируем SBOM
with:
format: spdx-json
sign-release:
runs-on: ubuntu-latest
steps:
- uses: sigstore/cosign-installer@v3
- run: |
cosign sign --yes \
ghcr.io/yourname/rampart-core:${{ github.sha }}
```
### Совместимость лицензий
```
Наш код: MIT или Apache-2.0
Ключевые зависимости:
tokio: MIT ✅
rustls: MIT/Apache ✅
libbpf-rs: LGPL-2.1 ✅ (динамическая линковка)
libbpf-sys: LGPL-2.1 ✅
XDP C код: GPL-2.0 ✅ (kernel module, отдельная сборка)
Потенциальная проблема:
XDP .c файлы компилируются в eBPF bytecode и загружаются в ядро.
Сам .c файл под GPL - это нормально для kernel interaction.
Rust loader (userspace) - MIT, не загрязняется GPL.
```
---
## Утечка реального IP - чеклист
```
☐ DNS история очищена (проверь через SecurityTrails, Shodan)
☐ Reverse DNS не раскрывает хостинг
☐ Старые firewall правила удалены
☐ game серверы не пингуют внешние ресурсы со своего IP
(обновления плагинов, curl запросы - через proxy или не извне)
☐ Email заголовки (если сервер шлёт письма) - проверить что не раскрывают IP
☐ Error pages, краш репорты - не выводить IP
☐ MC команды типа /ip - отключить или ограничить
☐ Доступ членов команды - минимальный, только нужные люди знают IP
☐ Pterodactyl/панель управления - закрыта за VPN или IP whitelist
```