feat: subnet-level attack detection (prefix_stats)
- XDP: prefix_stats LRU map, per-/24 (v4) and /64 (v6) SYN/packet counters, incremented post-blacklist/throttle (xdp/core/prefix_stats.h) - userspace: SubnetDetector escalation ladder Monitor->StrictLimit->Challenge->Block with spoof-gate (Block requires >= min_unique_sources, CGNAT-safe) - config: [detect.prefix] section (enabled=false by default) - fallback without XDP: engine SubnetTracker aggregates connections per-prefix - fix: PrefixStatsVal::from_bytes for xdp feature build; prefix_len 24 vs 64 cargo build/clippy(-D warnings, all features)/test green: 83 tests
This commit is contained in:
parent
15f474486a
commit
40bfe956e2
14 changed files with 1044 additions and 11 deletions
|
|
@ -1,9 +1,11 @@
|
|||
use anyhow::{Context, Result, bail};
|
||||
use libbpf_rs::{MapCore, MapFlags, Object, ObjectBuilder, RingBuffer, RingBufferBuilder, Xdp, XdpFlags};
|
||||
use std::net::IpAddr;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::os::unix::io::AsFd;
|
||||
|
||||
use super::XdpStats;
|
||||
use crate::traffic::prefix::{PrefixKey, PrefixStatsVal};
|
||||
|
||||
pub struct XdpFilter {
|
||||
obj: Option<Object>,
|
||||
|
|
@ -78,17 +80,28 @@ impl XdpFilter {
|
|||
}
|
||||
|
||||
pub fn ban_ip(&self, ip: Ipv4Addr, duration_secs: u64) -> Result<()> {
|
||||
self.blacklist_update(32, &ip.octets(), duration_secs)
|
||||
}
|
||||
|
||||
/// Бан CIDR через `blacklist_map` (LPM trie): длина префикса в key[0].
|
||||
///
|
||||
/// # Errors
|
||||
/// XDP не загружен, IPv6-префиксы (ядро их пока не банит) или ошибка карты.
|
||||
pub fn ban_cidr(&self, prefix: IpAddr, prefix_len: u8, duration_secs: u64) -> Result<()> {
|
||||
let IpAddr::V4(net) = prefix else {
|
||||
bail!("ipv6 cidr bans not supported yet");
|
||||
};
|
||||
self.blacklist_update(prefix_len, &net.octets(), duration_secs)
|
||||
}
|
||||
|
||||
fn blacklist_update(&self, prefix_len: u8, v4: &[u8; 4], duration_secs: u64) -> Result<()> {
|
||||
let map = self.find_map("blacklist_map")?;
|
||||
let mut key = [0u8; 8];
|
||||
key[0] = 32;
|
||||
key[4..8].copy_from_slice(&ip.octets());
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_nanos() as u64;
|
||||
key[0] = prefix_len;
|
||||
key[4..8].copy_from_slice(v4);
|
||||
map.update(
|
||||
&key,
|
||||
&(now + duration_secs * 1_000_000_000).to_le_bytes(),
|
||||
&(unix_ns() + duration_secs * 1_000_000_000).to_le_bytes(),
|
||||
MapFlags::ANY,
|
||||
)?;
|
||||
Ok(())
|
||||
|
|
@ -103,6 +116,28 @@ impl XdpFilter {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Читает карту `prefix_stats` (агрегаты по префиксам /24 и /64).
|
||||
///
|
||||
/// # Errors
|
||||
/// XDP не загружен, карта отсутствует в объекте или ошибка BPF-чтения.
|
||||
pub fn read_prefix_stats(&self) -> Result<Vec<(PrefixKey, PrefixStatsVal)>> {
|
||||
let map = self.find_map("prefix_stats")?;
|
||||
let mut out = Vec::new();
|
||||
for key in map.keys() {
|
||||
let Ok(kb) = <[u8; 24]>::try_from(key.as_slice()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(val) = map.lookup(&kb, MapFlags::ANY).context("prefix_stats lookup failed")? else {
|
||||
continue;
|
||||
};
|
||||
let Ok(vb) = <[u8; 24]>::try_from(val.as_slice()) else {
|
||||
continue;
|
||||
};
|
||||
out.push((PrefixKey::from_bytes(kb), PrefixStatsVal::from_bytes(vb)));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn get_stats(&self) -> Result<XdpStats> {
|
||||
let map = self.find_map("stats_map")?;
|
||||
let sum = |idx: u32| -> u64 {
|
||||
|
|
@ -137,6 +172,13 @@ impl Drop for XdpFilter {
|
|||
}
|
||||
}
|
||||
|
||||
fn unix_ns() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_nanos() as u64
|
||||
}
|
||||
|
||||
fn build_ringbuf(obj: &Object) -> Result<RingBuffer<'static>> {
|
||||
let map = obj
|
||||
.maps()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
use anyhow::Result;
|
||||
use std::net::Ipv4Addr;
|
||||
use anyhow::{Ok, Result};
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
|
||||
use crate::traffic::prefix::{PrefixKey, PrefixStatsVal};
|
||||
|
||||
pub struct XdpFilter;
|
||||
|
||||
|
|
@ -17,9 +19,21 @@ impl XdpFilter {
|
|||
pub fn ban_ip(&self, _ip: Ipv4Addr, _duration_secs: u64) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
pub fn ban_cidr(&self, _prefix: IpAddr, _prefix_len: u8, _duration_secs: u64) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
pub fn unban_ip(&self, _ip: Ipv4Addr) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Честная ошибка: чтение `prefix_stats` не подключено без feature `xdp`.
|
||||
///
|
||||
/// # Errors
|
||||
/// Всегда — сборка без XDP не имеет доступа к картам ядра.
|
||||
pub fn read_prefix_stats(&self) -> Result<Vec<(PrefixKey, PrefixStatsVal)>> {
|
||||
anyhow::bail!("prefix_stats reading not wired yet: built without xdp feature")
|
||||
}
|
||||
|
||||
pub fn get_stats(&self) -> Result<super::XdpStats> {
|
||||
Ok(super::XdpStats::default())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue