feat!: universal redesign — drop Minecraft stack, single-crate architecture
- remove Java plugins (velocity/paper), dashboard, all MC-specific code
(handshake, death_code, varint, hostname-HMAC); available in history pre-v0.2
- merge crates/* into one package with src/bin/{rampart,rampart-manager,rampart-cli}
- ProtocolHandler trait + registry (no implementations yet), universal PoW kept
- XDP: universal L3/L4 filter (xdp/core/) + pluggable hook API (xdp/hooks/),
fix IPv6 saddr bug; clang build verified
- docs: bilingual knowledge base (docs/kb/: attacks x4, defense-levels,
practice x3), rewrite README/architecture for universal concept
- TODO.md v4.0: <=300-line module limit, competitor benchmark section (ref/)
- deploy/CI/docs cleanup: no MC references, new binary names
cargo build/clippy(-D warnings)/test green (55 tests)
This commit is contained in:
parent
0b53ed720b
commit
15f474486a
179 changed files with 5044 additions and 11519 deletions
104
src/filter/blacklist.rs
Normal file
104
src/filter/blacklist.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
use dashmap::DashMap;
|
||||
use std::net::IpAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
struct BanEntry {
|
||||
expires: Instant,
|
||||
_reason: String,
|
||||
}
|
||||
|
||||
pub struct Blacklist {
|
||||
entries: Arc<DashMap<IpAddr, BanEntry>>,
|
||||
}
|
||||
|
||||
impl Default for Blacklist {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Blacklist {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
entries: Arc::new(DashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_blocked(&self, ip: IpAddr) -> bool {
|
||||
if let Some(entry) = self.entries.get(&ip) {
|
||||
if entry.expires > Instant::now() {
|
||||
return true;
|
||||
}
|
||||
drop(entry);
|
||||
self.entries.remove(&ip);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn add(&self, ip: IpAddr, duration: Duration, reason: &str) {
|
||||
self.entries.insert(
|
||||
ip,
|
||||
BanEntry {
|
||||
expires: Instant::now() + duration,
|
||||
_reason: reason.to_string(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn remove(&self, ip: IpAddr) {
|
||||
self.entries.remove(&ip);
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.entries.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.entries.is_empty()
|
||||
}
|
||||
|
||||
pub fn clear_expired(&self) {
|
||||
self.entries.retain(|_, entry| entry.expires > Instant::now());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
|
||||
fn ip(octets: [u8; 4]) -> IpAddr {
|
||||
IpAddr::V4(Ipv4Addr::from(octets))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blacklist_block() {
|
||||
let bl = Blacklist::new();
|
||||
bl.add(ip([1, 2, 3, 4]), Duration::from_secs(60), "test");
|
||||
assert!(bl.is_blocked(ip([1, 2, 3, 4])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blacklist_not_blocked() {
|
||||
let bl = Blacklist::new();
|
||||
bl.add(ip([1, 2, 3, 4]), Duration::from_secs(60), "test");
|
||||
assert!(!bl.is_blocked(ip([5, 6, 7, 8])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blacklist_expired() {
|
||||
let bl = Blacklist::new();
|
||||
bl.add(ip([1, 2, 3, 4]), Duration::from_millis(1), "test");
|
||||
std::thread::sleep(Duration::from_millis(2));
|
||||
assert!(!bl.is_blocked(ip([1, 2, 3, 4])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blacklist_remove() {
|
||||
let bl = Blacklist::new();
|
||||
bl.add(ip([1, 2, 3, 4]), Duration::from_secs(60), "test");
|
||||
bl.remove(ip([1, 2, 3, 4]));
|
||||
assert!(!bl.is_blocked(ip([1, 2, 3, 4])));
|
||||
}
|
||||
}
|
||||
35
src/filter/geo.rs
Normal file
35
src/filter/geo.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#[cfg(feature = "geoip")]
|
||||
pub struct GeoIp {
|
||||
#[allow(dead_code)]
|
||||
reader: maxminddb::Reader<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "geoip")]
|
||||
impl GeoIp {
|
||||
pub fn new(db_path: &str) -> anyhow::Result<Self> {
|
||||
let reader = maxminddb::Reader::open_readfile(db_path)?;
|
||||
Ok(Self { reader })
|
||||
}
|
||||
}
|
||||
|
||||
pub enum IpCategory {
|
||||
Residential,
|
||||
Datacenter,
|
||||
Mobile,
|
||||
Vpn,
|
||||
Tor,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for IpCategory {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Residential => write!(f, "residential"),
|
||||
Self::Datacenter => write!(f, "datacenter"),
|
||||
Self::Mobile => write!(f, "mobile"),
|
||||
Self::Vpn => write!(f, "vpn"),
|
||||
Self::Tor => write!(f, "tor"),
|
||||
Self::Unknown => write!(f, "unknown"),
|
||||
}
|
||||
}
|
||||
}
|
||||
5
src/filter/mod.rs
Normal file
5
src/filter/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
//! Фильтры входящего трафика.
|
||||
|
||||
pub mod blacklist;
|
||||
pub mod geo;
|
||||
pub mod rate_limit;
|
||||
152
src/filter/rate_limit.rs
Normal file
152
src/filter/rate_limit.rs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
use dashmap::DashMap;
|
||||
use std::net::IpAddr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const MAX_BUCKETS: usize = 1_000_000;
|
||||
const EVICTION_IDLE: Duration = Duration::from_secs(600);
|
||||
const SWEEP_INTERVAL: Duration = Duration::from_secs(60);
|
||||
|
||||
struct Bucket {
|
||||
tokens: f64,
|
||||
last_refill: Instant,
|
||||
last_access: Instant,
|
||||
}
|
||||
|
||||
pub struct RateLimiter {
|
||||
buckets: Arc<DashMap<IpAddr, Bucket>>,
|
||||
max_tokens: f64,
|
||||
refill_rate: f64,
|
||||
_refill_interval: Duration,
|
||||
epoch: Instant,
|
||||
last_sweep_elapsed: AtomicU64,
|
||||
eviction_idle: Duration,
|
||||
}
|
||||
|
||||
impl RateLimiter {
|
||||
pub fn new(rate_per_sec: f64, burst: f64) -> Self {
|
||||
Self::with_eviction(rate_per_sec, burst, EVICTION_IDLE)
|
||||
}
|
||||
|
||||
fn with_eviction(rate_per_sec: f64, burst: f64, eviction_idle: Duration) -> Self {
|
||||
Self {
|
||||
buckets: Arc::new(DashMap::new()),
|
||||
max_tokens: burst,
|
||||
refill_rate: rate_per_sec,
|
||||
_refill_interval: Duration::from_secs(1),
|
||||
epoch: Instant::now(),
|
||||
last_sweep_elapsed: AtomicU64::new(0),
|
||||
eviction_idle,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check(&self, ip: IpAddr) -> bool {
|
||||
let now = Instant::now();
|
||||
let mut entry = self.buckets.entry(ip).or_insert_with(|| Bucket {
|
||||
tokens: self.max_tokens,
|
||||
last_refill: now,
|
||||
last_access: now,
|
||||
});
|
||||
|
||||
let elapsed = now.duration_since(entry.last_refill);
|
||||
let refill = elapsed.as_secs_f64() * self.refill_rate;
|
||||
entry.tokens = (entry.tokens + refill).min(self.max_tokens);
|
||||
entry.last_refill = now;
|
||||
entry.last_access = now;
|
||||
|
||||
if entry.tokens >= 1.0 {
|
||||
entry.tokens -= 1.0;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.buckets.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.buckets.is_empty()
|
||||
}
|
||||
|
||||
/// Эвиктит простаивающие бакеты. Запускается, когда бакетов больше
|
||||
/// MAX_BUCKETS либо по расписанию (раз в SWEEP_INTERVAL).
|
||||
pub fn sweep(&self) {
|
||||
let now = Instant::now();
|
||||
let elapsed_secs = now.duration_since(self.epoch).as_secs();
|
||||
let last = self.last_sweep_elapsed.load(Ordering::Relaxed);
|
||||
let due = last == 0 || elapsed_secs.saturating_sub(last) >= SWEEP_INTERVAL.as_secs();
|
||||
let over_cap = self.buckets.len() > MAX_BUCKETS;
|
||||
if !over_cap && !due {
|
||||
return;
|
||||
}
|
||||
self.buckets
|
||||
.retain(|_, b| now.duration_since(b.last_access) < self.eviction_idle);
|
||||
self.last_sweep_elapsed.store(elapsed_secs, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
|
||||
fn test_ip(octet: u8) -> IpAddr {
|
||||
IpAddr::V4(Ipv4Addr::new(10, 0, 0, octet))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rate_limit_under() {
|
||||
let limiter = RateLimiter::new(10.0, 10.0);
|
||||
assert!(limiter.check(test_ip(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rate_limit_over() {
|
||||
let limiter = RateLimiter::new(1.0, 1.0);
|
||||
assert!(limiter.check(test_ip(1)));
|
||||
assert!(!limiter.check(test_ip(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rate_limit_burst() {
|
||||
let limiter = RateLimiter::new(1.0, 5.0);
|
||||
for _ in 0..5 {
|
||||
assert!(limiter.check(test_ip(2)));
|
||||
}
|
||||
assert!(!limiter.check(test_ip(2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rate_limit_refill() {
|
||||
let limiter = RateLimiter::new(100.0, 1.0);
|
||||
assert!(limiter.check(test_ip(3)));
|
||||
assert!(!limiter.check(test_ip(3)));
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
assert!(limiter.check(test_ip(3)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sweep_removes_idle_keeps_active() {
|
||||
let limiter = RateLimiter::with_eviction(1.0, 10.0, Duration::from_millis(20));
|
||||
limiter.check(test_ip(1));
|
||||
limiter.check(test_ip(2));
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
limiter.check(test_ip(2));
|
||||
limiter.sweep();
|
||||
assert_eq!(limiter.len(), 1);
|
||||
assert!(!limiter.buckets.contains_key(&test_ip(1)));
|
||||
assert!(limiter.buckets.contains_key(&test_ip(2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sweep_does_not_remove_active() {
|
||||
let limiter = RateLimiter::with_eviction(1.0, 10.0, Duration::from_millis(50));
|
||||
limiter.check(test_ip(1));
|
||||
limiter.sweep();
|
||||
assert_eq!(limiter.len(), 1);
|
||||
assert!(limiter.buckets.contains_key(&test_ip(1)));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue