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:
loki5512344 2026-08-24 01:50:22 +02:00
parent 0b53ed720b
commit 15f474486a
Signed by: boba
GPG key ID: 253067914055423B
179 changed files with 5044 additions and 11519 deletions

104
src/filter/blacklist.rs Normal file
View 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
View 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
View file

@ -0,0 +1,5 @@
//! Фильтры входящего трафика.
pub mod blacklist;
pub mod geo;
pub mod rate_limit;

152
src/filter/rate_limit.rs Normal file
View 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)));
}
}