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
9.7 KiB
9.7 KiB
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 конфиг с авто-дискавери
# 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
// 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 правила
# 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
# 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 ноды
# /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
// 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
Схема
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
// Не пишем на каждый пакет - накапливаем и сбрасываем раз в секунду
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
# 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
// Включаем 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
# Запуск 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'ятся