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:
commit
cf9608ce5d
159 changed files with 15341 additions and 0 deletions
68
docs/research/README.md
Normal file
68
docs/research/README.md
Normal 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
282
docs/research/anti-bot.md
Normal 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
|
||||
127
docs/research/architecture.md
Normal file
127
docs/research/architecture.md
Normal 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
275
docs/research/benchmark.md
Normal 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
292
docs/research/ddos.md
Normal 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
340
docs/research/ebpf.md
Normal 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
240
docs/research/envoy.md
Normal 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
183
docs/research/haproxy.md
Normal 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
139
docs/research/io_uring.md
Normal 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
|
||||
```
|
||||
303
docs/research/minecraft-protocol.md
Normal file
303
docs/research/minecraft-protocol.md
Normal 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
337
docs/research/networking.md
Normal 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 сек
|
||||
```
|
||||
361
docs/research/observability.md
Normal file
361
docs/research/observability.md
Normal 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
184
docs/research/papers.md
Normal 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
|
||||
```
|
||||
312
docs/research/rust-performance.md
Normal file
312
docs/research/rust-performance.md
Normal 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
262
docs/research/security.md
Normal 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
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue