guard/tests/traffic_intel.rs
loki5512344 aa787a558c
feat: traffic intel hot path, SYN RST-challenge, XDP environment diagnostics
Traffic Intel (was dead code, now wired):
- TrafficHook in listener accept path: cps/pps windows -> AttackDetector
- auto-ban IPs below reputation threshold under attack ([detect.autoban])
- AlertDispatcher: webhook on attack state transition only (dedup), metrics
  AUTO_BANS_TOTAL / INTEL_* ; [detect.alert].webhook_url

XDP SYN RST-challenge (Oubliette pattern, off by default):
- G_SYN_CHALLENGE_ENABLED=0: kernel replies bad-ACK SYN-ACK via XDP_TX,
  spoofed sources stay silent, live clients answer RST with secret echo ->
  challenge_verified (LRU, sliding TTL); brute-force of marker impossible
- maps challenge_verified/challenge_pending, STAT_CHALLENGE_*, all logic
  in xdp/core/syn_challenge.h (221 lines)

XDP diagnostics (src/xdp/diagnostics.rs):
- EnvironmentReport: kernel version/BTF/driver->AttachMode verdict,
  fail-fast before load on unsupported kernels; wired into  CLI
- SystemProbe trait for kernel-less testing

fix: .gitignore 'bin/' matched src/bin/ — rampart.rs was never committed

cargo build/clippy(-D warnings, --features xdp)/test green: 107 tests
2026-08-24 10:11:10 +02:00

112 lines
3.6 KiB
Rust

//! Интеграционные тесты Traffic Intelligence в hot path: авто-бан по
//! репутации, детектор атак на синтетическом всплеске, алерты на переходах.
use rampart::config::DetectAutobanConfig;
use rampart::filter::blacklist::Blacklist;
use rampart::traffic::alert::AlertDispatcher;
use rampart::traffic::detector::{AttackDetector, AttackStatus};
use rampart::traffic::hook::TrafficHook;
use rampart::traffic::reputation::IpReputation;
use std::net::{IpAddr, Ipv4Addr};
use std::sync::Arc;
use std::time::Duration;
fn ip(octets: [u8; 4]) -> IpAddr {
IpAddr::V4(Ipv4Addr::from(octets))
}
fn autoban_cfg(enabled: bool) -> DetectAutobanConfig {
DetectAutobanConfig {
enabled,
reputation_threshold: -50,
}
}
#[test]
fn reputation_fall_after_drops_triggers_autoban() {
let reputation = Arc::new(IpReputation::new());
let blacklist = Arc::new(Blacklist::new());
let hook = TrafficHook::new(Arc::clone(&reputation), Arc::clone(&blacklist), autoban_cfg(true), 60);
let attacker = ip([198, 51, 100, 7]);
for _ in 0..6 {
// Шесть дропов rate-limit'ом: -10 за каждый, порог auto-ban = -50.
reputation.record_bad(attacker);
hook.on_connection_end(attacker);
}
assert!(
reputation.score(attacker) <= -50,
"reputation must fall below threshold"
);
assert!(
blacklist.is_blocked(attacker),
"auto-ban must fire after repeated drops"
);
}
#[test]
fn autoban_ttl_expires_ban() {
let reputation = Arc::new(IpReputation::new());
let blacklist = Arc::new(Blacklist::new());
let hook = TrafficHook::new(
Arc::clone(&reputation),
Arc::clone(&blacklist),
autoban_cfg(true),
// Короткий TTL из «конфига» как мок времени.
0,
);
let attacker = ip([198, 51, 100, 8]);
for _ in 0..20 {
reputation.record_bad(attacker);
}
hook.on_connection_end(attacker);
std::thread::sleep(Duration::from_millis(5));
assert!(
!blacklist.is_blocked(attacker),
"zero TTL must not produce an active ban"
);
}
#[test]
fn detector_flags_synthetic_traffic_spike() {
let mut detector = AttackDetector::new();
// Базовая линия: тихий профиль.
for _ in 0..12 {
detector.analyze(500.0, 50.0);
}
// Синтетический всплеск: x200 pps и cps три окна подряд.
assert_eq!(
detector.analyze(100_000.0, 10_000.0),
AttackStatus::Suspicious,
"first anomalous window is suspicious"
);
assert_eq!(detector.analyze(100_000.0, 10_000.0), AttackStatus::Suspicious);
assert_eq!(
detector.analyze(100_000.0, 10_000.0),
AttackStatus::UnderAttack,
"three anomalous windows in a row must escalate to UnderAttack"
);
}
#[test]
fn alerts_fire_only_on_state_transitions() {
let dispatcher = AlertDispatcher::new();
let sequence = [
AttackStatus::Normal,
AttackStatus::Normal,
AttackStatus::Suspicious,
AttackStatus::UnderAttack,
AttackStatus::UnderAttack,
AttackStatus::UnderAttack,
AttackStatus::Suspicious,
AttackStatus::Normal,
AttackStatus::Normal,
];
let alerts: Vec<_> = sequence.iter().filter_map(|s| dispatcher.on_status(*s)).collect();
assert_eq!(alerts.len(), 2, "two transitions must yield exactly two alerts");
assert!(alerts[0].message.contains("attack started"));
assert!(alerts[1].message.contains("attack ended"));
}