Initial commit: Rampart v0.2.0

Multi-layer DDoS protection for Minecraft servers.

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

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

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