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
112 lines
3.6 KiB
Rust
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"));
|
|
}
|