Layers: Layer 1: XDP/eBPF — TCP state machine, SYN throttle, blacklist, ringbuf Layer 2: PoW Challenge — SHA-256 hashcash, dynamic difficulty, constant-time verify Layer 3: Rust Core — HMAC handshake, rate limit, death code (existing) Layer 4: Velocity — Physics check, CAPTCHA, protocol verification Layer 5: Paper — Heartbeat, auto-registration (existing) Layer 6: Traffic Intel — EWMA, 168h profiling, reputation, alerts Infra: XDP→Prometheus metrics, ClickHouse + Grafana dashboard, Docker Compose Testing: 100-IP DDoS simulation, MHDDoS ref analysis, load test report Fixes: VarInt sign extension UB, pure ACK deadlock, RST/FIN cleanup Ref: MHDDoS, Sonar, LimboFilter, AtomGuard, Infrarust, MC-XDP-eBPF, PowGo
334 lines
10 KiB
Markdown
334 lines
10 KiB
Markdown
# eBPF / XDP — Rampart 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% |
|
||
| **XDP (native)** | ~0.5 мкс | ~15% |
|
||
| **XDP (offload)** | ~0.1 мкс | ~0% |
|
||
|
||
---
|
||
|
||
## TCP State Machine
|
||
|
||
Rampart использует stateful подход из Minecraft-XDP-eBPF, с исправлениями:
|
||
|
||
```
|
||
┌──────────┐
|
||
│ SYN │
|
||
│ received │
|
||
└────┬─────┘
|
||
│
|
||
┌────▼─────┐
|
||
┌────────►│AWAIT_ACK │◄─────────┐
|
||
│ │ (SYN-ACK │ │
|
||
│ │ sent) │ │
|
||
│ └────┬─────┘ │
|
||
│ │ ACK received │
|
||
│ ┌────▼──────────┐ │
|
||
│ │AWAIT_MC_ │ │ retransmit
|
||
│ │HANDSHAKE ├─────┘ (если pure ACK)
|
||
│ └────┬──────────┘
|
||
│ │ MC handshake
|
||
│ ┌────▼──────────┐
|
||
│ │AWAIT_LOGIN │
|
||
│ └────┬──────────┘
|
||
│ │ LoginStart
|
||
│ ┌────▼──────────┐ ┌──────────────┐
|
||
│ │ VERIFIED │────►│Idle bpf_timer│
|
||
│ └────┬──────────┘ │ (60 sec) │
|
||
│ │ └──────┬───────┘
|
||
│ ┌────▼──────────┐ │ timeout
|
||
│ │ PING_SENT │ │
|
||
│ └────┬──────────┘ ┌──────▼───────┐
|
||
│ │ │ ENTRY_DELETED│
|
||
│ ┌────▼──────────┐ └──────────────┘
|
||
│ │PING_COMPLETE │
|
||
│ └────┬──────────┘
|
||
│ │
|
||
│ ┌────▼──────────┐
|
||
└─────────┤ CONN_DROP │
|
||
│ (RST/FIN) │
|
||
└───────────────┘
|
||
```
|
||
|
||
## Исправления относительно Minecraft-XDP-eBPF
|
||
|
||
### 1. Pure ACK deadlock (критический баг в MC-XDP-eBPF)
|
||
|
||
```
|
||
Оригинал (MC-XDP-eBPF):
|
||
AWAIT_ACK → pure ACK → DROP + переход в AWAIT_MC_HANDSHAKE
|
||
→ сервер не видит ACK → retransmit SYN-ACK → ~1-7 сек лага
|
||
|
||
Наш фикс:
|
||
AWAIT_ACK → pure ACK → PASS + переход в AWAIT_MC_HANDSHAKE
|
||
→ сервер видит ACK → TCP handshake завершён нормально
|
||
```
|
||
|
||
```c
|
||
// ОРИГИНАЛ (сломан):
|
||
if (state == AWAIT_ACK) {
|
||
initial_state->state = state = AWAIT_MC_HANDSHAKE;
|
||
if (tcp_payload >= tcp_payload_end) {
|
||
goto drop; // Pure ACK dropped → DEADLOCK
|
||
}
|
||
}
|
||
|
||
// ИСПРАВЛЕНИЕ:
|
||
if (state == AWAIT_ACK) {
|
||
initial_state->state = state = AWAIT_MC_HANDSHAKE;
|
||
if (tcp_payload >= tcp_payload_end) {
|
||
return XDP_PASS; // Pure ACK passed → handshake ok
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. Stale conntrack на RST/FIN
|
||
|
||
```c
|
||
// В AWAIT_MC_HANDSHAKE — RST/FIN должен чистить entry:
|
||
if ((tcp->fin || tcp->rst) && state != AWAIT_ACK) {
|
||
bpf_map_delete_elem(&conntrack_map, &flow_key);
|
||
return XDP_PASS;
|
||
}
|
||
```
|
||
|
||
### 3. Player map LRU
|
||
|
||
```c
|
||
// Оригинал: BPF_MAP_TYPE_HASH (plain) — при заполнении дропает новых игроков
|
||
// Исправление:
|
||
struct {
|
||
__uint(type, BPF_MAP_TYPE_LRU_HASH);
|
||
__uint(max_entries, 65535);
|
||
__type(key, struct flow_key);
|
||
__type(value, struct player_entry);
|
||
} player_connection_map SEC(".maps");
|
||
```
|
||
|
||
### 4. Idle timer на conntrack (не только на player)
|
||
|
||
```c
|
||
// Добавить bpf_timer в conntrack entry:
|
||
struct conntrack_entry {
|
||
__u32 state;
|
||
__u32 expected_seq;
|
||
__u32 src_ip;
|
||
__u16 src_port;
|
||
__u8 fails;
|
||
struct bpf_timer timer; // 30 sec idle timeout
|
||
};
|
||
```
|
||
|
||
### 5. IPv6 support
|
||
|
||
```c
|
||
// Оригинал: return XDP_PASS for non-IP (IPv6 bypass)
|
||
// Исправление:
|
||
if (eth->h_proto != bpf_htons(ETH_P_IP)
|
||
&& eth->h_proto != bpf_htons(ETH_P_IPV6)) {
|
||
return XDP_PASS;
|
||
}
|
||
|
||
// Отдельный flow key для IPv6:
|
||
struct flow_key_v6 {
|
||
struct in6_addr src_ip;
|
||
struct in6_addr dst_ip;
|
||
__u16 src_port;
|
||
__u16 dst_port;
|
||
};
|
||
```
|
||
|
||
### 6. IP/CIDR whitelist (issue #42 из MC-XDP-eBPF)
|
||
|
||
```c
|
||
struct whitelist_key {
|
||
__u32 prefixlen;
|
||
__u32 ip;
|
||
};
|
||
|
||
struct {
|
||
__uint(type, BPF_MAP_TYPE_LPM_TRIE);
|
||
__uint(max_entries, 10000);
|
||
__type(key, struct whitelist_key);
|
||
__type(value, __u8);
|
||
__uint(map_flags, BPF_F_NO_PREALLOC);
|
||
} whitelist_map SEC(".maps");
|
||
```
|
||
|
||
---
|
||
|
||
## BPF Maps
|
||
|
||
```c
|
||
// ── Blacklist (LPM_TRIE для CIDR) ──
|
||
struct {
|
||
__uint(type, BPF_MAP_TYPE_LPM_TRIE);
|
||
__uint(max_entries, 100000);
|
||
__type(key, struct lpm_key);
|
||
__type(value, __u64); // timestamp ban
|
||
__uint(map_flags, BPF_F_NO_PREALLOC);
|
||
} blacklist_map SEC(".maps");
|
||
|
||
// ── Whitelist (LPM_TRIE для CIDR) ──
|
||
struct {
|
||
__uint(type, BPF_MAP_TYPE_LPM_TRIE);
|
||
__uint(max_entries, 1000);
|
||
__type(key, struct lpm_key);
|
||
__type(value, __u8);
|
||
__uint(map_flags, BPF_F_NO_PREALLOC);
|
||
} whitelist_map SEC(".maps");
|
||
|
||
// ── Conntrack (unverified connections, LRU) ──
|
||
struct {
|
||
__uint(type, BPF_MAP_TYPE_LRU_HASH);
|
||
__uint(max_entries, 16384);
|
||
__type(key, struct flow_key);
|
||
__type(value, struct conntrack_entry);
|
||
} conntrack_map SEC(".maps");
|
||
|
||
// ── Verified players (LRU) ──
|
||
struct {
|
||
__uint(type, BPF_MAP_TYPE_LRU_HASH);
|
||
__uint(max_entries, 65535);
|
||
__type(key, struct flow_key);
|
||
__type(value, struct player_entry);
|
||
} player_connection_map SEC(".maps");
|
||
|
||
// ── SYN throttle per-IP ──
|
||
struct {
|
||
__uint(type, BPF_MAP_TYPE_LRU_HASH);
|
||
__uint(max_entries, 65535);
|
||
__type(key, __u32); // src IP
|
||
__type(value, struct throttle_entry);
|
||
} connection_throttle SEC(".maps");
|
||
|
||
// ── Statistics (per-CPU, для Prometheus) ──
|
||
struct {
|
||
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
|
||
__uint(max_entries, 16);
|
||
__type(key, __u32);
|
||
__type(value, __u64);
|
||
} stats_map SEC(".maps");
|
||
```
|
||
|
||
---
|
||
|
||
## Граница XDP / Rust
|
||
|
||
```
|
||
XDP делает (L3/L4): Rust делает (L7):
|
||
TCP state machine PoW Challenge (SHA256)
|
||
SYN throttle per-IP MC handshake парсинг
|
||
IP blacklist (LPM_TRIE) HMAC sign/verify
|
||
IP whitelist (CIDR) Rate limit per-IP
|
||
Invalid TCP flags drop Death code auto-ban
|
||
UDP drop GeoIP/ASN reputation
|
||
Per-connection seq tracking Blacklist (сложные правила)
|
||
bpf_timer idle cleanup Reassembly буфер
|
||
```
|
||
|
||
XDP **не может** (даже в kernel 6.x):
|
||
- SHA256 (нет heap, нет looping на достаточное время)
|
||
- Floating point (ограничен)
|
||
- Сложные строковые операции
|
||
- TLS инспекция
|
||
- GeoIP / DNS resolve
|
||
|
||
---
|
||
|
||
## Загрузка (Rust + libbpf-rs)
|
||
|
||
```rust
|
||
use libbpf_rs::{MapFlags, ObjectBuilder};
|
||
|
||
pub struct RampartXdp {
|
||
obj: Object,
|
||
interface: String,
|
||
}
|
||
|
||
impl RampartXdp {
|
||
pub fn load(interface: &str) -> Result<Self> {
|
||
let obj = ObjectBuilder::default()
|
||
.open_file("/etc/rampart/xdp_filter.o")?
|
||
.load()?;
|
||
|
||
let prog = obj.prog("rampart_xdp_filter").unwrap();
|
||
prog.attach_xdp(if_nametoindex(interface)?)?;
|
||
Ok(Self { obj, interface: interface.to_string() })
|
||
}
|
||
|
||
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 = now_nanos() + duration.as_nanos() as u64;
|
||
map.update(&key.to_bytes(), &ts.to_le_bytes(), MapFlags::ANY).unwrap();
|
||
}
|
||
|
||
pub fn get_stats(&self) -> XdpStats {
|
||
XdpStats {
|
||
total: self.read_percpu_sum("stats_map", 0),
|
||
passed: self.read_percpu_sum("stats_map", 1),
|
||
blocked: self.read_percpu_sum("stats_map", 2),
|
||
ratelimit: self.read_percpu_sum("stats_map", 3),
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Требования
|
||
|
||
```bash
|
||
# Проверка XDP
|
||
ethtool -i eth0 | grep driver # i40e, mlx5, virtio
|
||
|
||
# Тип виртуализации
|
||
systemd-detect-virt # kvm/none = XDP, openvz/lxc = нет
|
||
|
||
# Версия ядра
|
||
uname -r # >= 5.10
|
||
|
||
# Зависимости
|
||
apt-get install -y libbpf-dev clang llvm linux-headers-$(uname -r)
|
||
```
|
||
|
||
## ringbuf (вместо perfbuf)
|
||
|
||
| | perfbuf | ringbuf (kernel 5.8+) |
|
||
|---|---|---|
|
||
| Тип | Per-CPU | Один разделяемый |
|
||
| Копирование | Одно | Одно |
|
||
| Порядок | Не гарантирован | Гарантирован |
|
||
| Память | Per-CPU | Меньше |
|
||
| **Вывод** | Устаревший | **Используй ringbuf** |
|
||
|
||
## Лимиты BPF verifier
|
||
|
||
```
|
||
Максимум инструкций: 1M (kernel 5.2+)
|
||
Максимум стека: 512 байт
|
||
Максимум вложенности: 8
|
||
Циклы: разрешены с 5.3+
|
||
Dynamic alloc: нет (только BPF maps)
|
||
```
|