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
This commit is contained in:
parent
40bfe956e2
commit
aa787a558c
25 changed files with 1825 additions and 28 deletions
112
tests/traffic_intel.rs
Normal file
112
tests/traffic_intel.rs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
//! Интеграционные тесты 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"));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue