feat(xdp): auto-ban from ringbuf events; fix feature gating, exit path, pow flake
Some checks are pending
CI / Rust — check & clippy (push) Waiting to run
CI / Rust — test (push) Waiting to run
CI / Repo — module size & default secrets (push) Waiting to run
CI / Rust — cargo-deny (push) Waiting to run
CI / Docker — build edge image (push) Blocked by required conditions

- xdp: real 24-byte xdp_event parsing (fixes LE byte-swap of src IP),
  opt-in [xdp] auto_ban (RATE_LIMIT always, CONN_DROP at fails>=threshold;
  EVENT_BAN/POLICY_DROP excluded by design), rampart_xdp_autobans_total;
  wired in app before load(); 10 tests in tests/xdp_events.rs
- build: --no-default-features compiles — redis paths cfg-gated behind
  store-redis, manager fail-fasts without it; CI gates the config now
- app: RunExit enum replaces process::exit in lib; single exit site in main
- tests: seed PoW roundtrip token (was ~1/16 flaky; 50 pre-fix fails -> 0)
- docs: TODO statuses refreshed (round 2)
This commit is contained in:
loki5512344 2026-09-16 00:49:41 +02:00
parent aa615a1141
commit bd40a44980
Signed by: boba
GPG key ID: 253067914055423B
23 changed files with 714 additions and 50 deletions

173
tests/xdp_events.rs Normal file
View file

@ -0,0 +1,173 @@
//! Интеграционные тесты wire-формата и политики авто-бана XDP-событий.
//! Ядро/Redis не нужны: проверяем чистые парсер и решение + дефолты конфига.
#![cfg(feature = "xdp")]
use rampart::config::Config;
use rampart::xdp::{AutoBanConfig, XDP_EVENT_LEN, XdpEventKind, auto_ban_decision, parse_xdp_event};
use std::net::Ipv4Addr;
const IP: [u8; 4] = [203, 0, 113, 7];
/// Собирает кадр ровно как ядро (`push_event`, common.h:87-97): скаляры — native
/// (host) order, `src_ip` — сетевой порядок октетов (raw из заголовка пакета).
fn frame(kind: u32, octets: [u8; 4], metadata: u32, ts: u64) -> [u8; XDP_EVENT_LEN] {
let mut b = [0u8; XDP_EVENT_LEN];
b[0..4].copy_from_slice(&kind.to_ne_bytes());
b[4..8].copy_from_slice(&octets);
b[8..12].copy_from_slice(&metadata.to_ne_bytes());
// b[12..16] — выравнивающий padding под __u64: ядро его не инициализирует.
b[16..24].copy_from_slice(&ts.to_ne_bytes());
b
}
#[test]
fn struct_size_is_24_bytes() {
// Раскладка common.h: __u32×3 + pad(4) + __u64 = 24, НЕ 16 как парсил код ранее.
assert_eq!(XDP_EVENT_LEN, 24);
}
#[test]
fn round_trip_recovers_all_fields() {
let ev = parse_xdp_event(&frame(1, IP, 100, 0xDEAD_BEEF)).expect("valid frame parses");
assert_eq!(ev.kind, XdpEventKind::RateLimit);
assert_eq!(ev.src_ip, Ipv4Addr::new(203, 0, 113, 7));
assert_eq!(ev.metadata, 100);
assert_eq!(ev.timestamp_ns, 0xDEAD_BEEF);
}
#[test]
fn dirty_padding_does_not_corrupt_parse() {
let mut f = frame(3, IP, 5, 0x1_0000_0000);
f[12..16].copy_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD]);
let ev = parse_xdp_event(&f).expect("padding must be ignored");
assert_eq!(ev.kind, XdpEventKind::ConnDrop);
assert_eq!(ev.metadata, 5);
assert_eq!(ev.timestamp_ns, 0x1_0000_0000);
}
#[test]
fn src_ip_is_network_order_octets_not_native_u32() {
// КЛЮЧЕВОЙ вывод по endianness: октеты лежат в точковом порядке напрямую.
let f = frame(2, [198, 51, 100, 23], 0, 0);
let ev = parse_xdp_event(&f).expect("parses");
assert_eq!(ev.src_ip, Ipv4Addr::new(198, 51, 100, 23));
// Старый баг (`u32::from_ne_bytes` → Ipv4Addr::from(u32)) на little-endian
// давал бы переёрнутый адрес — фиксируем, что мы так НЕ делаем.
if cfg!(target_endian = "little") {
let naive = u32::from_ne_bytes(<[u8; 4]>::try_from(&f[4..8]).expect("4 octets"));
assert_eq!(Ipv4Addr::from(naive), Ipv4Addr::new(23, 100, 51, 198));
assert_ne!(Ipv4Addr::from(naive), ev.src_ip);
}
}
#[test]
fn truncated_frames_are_rejected() {
let full = frame(1, IP, 0, 0);
assert!(parse_xdp_event(&full[..16]).is_none(), "старая длина 16 — не валидна");
assert!(parse_xdp_event(&full[..23]).is_none(), "на 1 байт короче — не валидна");
assert!(parse_xdp_event(&full).is_some(), "ровно 24 — валидна");
assert!(parse_xdp_event(&[]).is_none());
}
#[test]
fn unknown_event_type_is_rejected() {
for kind in [4u32, 9, 255, u32::MAX] {
assert!(parse_xdp_event(&frame(kind, IP, 0, 0)).is_none(), "type {kind} unknown");
}
}
#[test]
fn decision_matrix_by_type_and_enablement() {
let enabled = AutoBanConfig {
enabled: true,
..AutoBanConfig::default()
};
let disabled = AutoBanConfig {
enabled: false,
..AutoBanConfig::default()
};
// (kind, metadata=fails/хиты, банить ли при enabled)
let rows = [
(0u32, 255u32, false), // EVENT_BAN — ядро уже забанило: никогда
(1, 100, true), // EVENT_RATE_LIMIT — всегда баним
(2, 0, false), // EVENT_POLICY_DROP — политика по дефолту не банит
(3, 10, true), // EVENT_CONN_DROP fails=10 >= 3 — баним
(3, 0, false), // EVENT_CONN_DROP fails=0 (hook-drop) — не баним
(3, 2, false), // EVENT_CONN_DROP fails=2 < 3 — не баним
];
for (kind, meta, should_ban_when_on) in rows {
let ev = parse_xdp_event(&frame(kind, IP, meta, 0)).expect("known type");
assert!(
auto_ban_decision(&ev, &disabled).is_none(),
"disabled: kind {kind} meta {meta} must never ban"
);
let decided = auto_ban_decision(&ev, &enabled).is_some();
assert_eq!(
decided, should_ban_when_on,
"enabled: kind {kind} meta {meta} -> ban={decided}, expected {should_ban_when_on}"
);
}
}
#[test]
fn ban_carries_configured_duration_and_reason() {
let cfg = AutoBanConfig {
enabled: true,
duration_secs: 120,
conn_drop_fails: 3,
};
let rl = parse_xdp_event(&frame(1, IP, 0, 0)).expect("rate limit frame");
let ban = auto_ban_decision(&rl, &cfg).expect("rate limit bans");
assert_eq!(ban.ip, Ipv4Addr::new(203, 0, 113, 7));
assert_eq!(ban.duration_secs, 120);
assert_eq!(ban.reason, "xdp-rate-limit");
let cd = parse_xdp_event(&frame(3, IP, 4, 0)).expect("conn drop frame");
assert_eq!(
auto_ban_decision(&cd, &cfg).expect("conn drop bans").reason,
"xdp-conn-drop"
);
}
#[test]
fn defaults_disable_autoban_without_xdp_keys() {
let cfg = Config::parse_str("").expect("empty config parses");
assert!(
!cfg.xdp.auto_ban_enabled,
"авто-бан обязан быть выключен по умолчанию (safe-by-default)"
);
assert_eq!(cfg.xdp.auto_ban_duration_secs, 60);
assert_eq!(cfg.xdp.auto_ban_conn_drop_fails, 3);
let policy = AutoBanConfig::from_xdp(&cfg.xdp);
let rl = parse_xdp_event(&frame(1, IP, 0, 0)).expect("frame parses");
assert!(auto_ban_decision(&rl, &policy).is_none(), "default policy bans nothing");
}
#[test]
fn xdp_section_enables_autoban_and_maps_fields() {
let cfg = Config::parse_str(
r#"
[xdp]
auto_ban_enabled = true
auto_ban_duration_secs = 45
auto_ban_conn_drop_fails = 5
"#,
)
.expect("[xdp] auto-ban keys parse");
let policy = AutoBanConfig::from_xdp(&cfg.xdp);
assert!(policy.enabled);
assert_eq!(policy.duration_secs, 45);
assert_eq!(policy.conn_drop_fails, 5);
// fails=4 ниже нового порога 5 — не бан; fails=5 — бан.
let below = parse_xdp_event(&frame(3, IP, 4, 0)).expect("frame parses");
assert!(auto_ban_decision(&below, &policy).is_none());
let at = parse_xdp_event(&frame(3, IP, 5, 0)).expect("frame parses");
assert_eq!(
auto_ban_decision(&at, &policy)
.expect("at-threshold bans")
.duration_secs,
45
);
}