New mesh_node.rs providing a production-ready node: - MeshNodeBuilder for fluent configuration - MeshConfig integration for all settings - MeshMetrics tracking for all operations - Rate limiting on incoming messages - Backpressure controller - Graceful shutdown via ShutdownCoordinator - Optional FappRouter based on capabilities - MeshRouter for envelope routing - TransportManager for multi-transport support Key APIs: - MeshNodeBuilder::new().fapp_relay().build() - node.process_incoming() with rate limiting + metrics - node.gc() for store/routing table cleanup - node.shutdown() for graceful termination 222 tests passing (203 lib + 3 fapp_flow + 16 multi_node)
533 lines
14 KiB
Rust
533 lines
14 KiB
Rust
//! Anti-abuse mechanisms for preventing slot blocking and spam.
|
|
|
|
use std::collections::HashMap;
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
use sha2::{Digest, Sha256};
|
|
|
|
/// Rate limiting configuration.
|
|
#[derive(Debug, Clone)]
|
|
pub struct RateLimits {
|
|
/// Max reservations per sender per hour.
|
|
pub max_reservations_per_hour: u8,
|
|
/// Max pending (unconfirmed) reservations per sender.
|
|
pub max_pending_reservations: u8,
|
|
/// Min time between reservations (seconds).
|
|
pub reservation_cooldown_secs: u32,
|
|
/// Max queries per sender per minute.
|
|
pub max_queries_per_minute: u8,
|
|
}
|
|
|
|
impl Default for RateLimits {
|
|
fn default() -> Self {
|
|
Self {
|
|
max_reservations_per_hour: 3,
|
|
max_pending_reservations: 2,
|
|
reservation_cooldown_secs: 300,
|
|
max_queries_per_minute: 10,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Tracks sender activity for rate limiting.
|
|
#[derive(Debug, Default)]
|
|
pub struct RateLimiter {
|
|
limits: RateLimits,
|
|
/// sender_address -> activity
|
|
activity: HashMap<[u8; 16], SenderActivity>,
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
struct SenderActivity {
|
|
/// Timestamps of reservations in last hour.
|
|
reservation_times: Vec<u64>,
|
|
/// Count of pending reservations.
|
|
pending_count: u8,
|
|
/// Timestamp of last reservation.
|
|
last_reservation: u64,
|
|
/// Query timestamps in last minute.
|
|
query_times: Vec<u64>,
|
|
}
|
|
|
|
impl RateLimiter {
|
|
/// Create with default limits.
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Create with custom limits.
|
|
pub fn with_limits(limits: RateLimits) -> Self {
|
|
Self {
|
|
limits,
|
|
activity: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
/// Check if a reservation is allowed.
|
|
pub fn check_reservation(&mut self, sender: &[u8; 16]) -> RateLimitResult {
|
|
let now = now();
|
|
let activity = self.activity.entry(*sender).or_default();
|
|
|
|
// Clean old entries
|
|
activity.reservation_times.retain(|&t| now - t < 3600);
|
|
|
|
// Check cooldown
|
|
if now - activity.last_reservation < u64::from(self.limits.reservation_cooldown_secs) {
|
|
return RateLimitResult::Cooldown {
|
|
wait_secs: self.limits.reservation_cooldown_secs - (now - activity.last_reservation) as u32,
|
|
};
|
|
}
|
|
|
|
// Check hourly limit
|
|
if activity.reservation_times.len() >= self.limits.max_reservations_per_hour as usize {
|
|
return RateLimitResult::HourlyLimitReached;
|
|
}
|
|
|
|
// Check pending limit
|
|
if activity.pending_count >= self.limits.max_pending_reservations {
|
|
return RateLimitResult::TooManyPending;
|
|
}
|
|
|
|
RateLimitResult::Allowed
|
|
}
|
|
|
|
/// Record a reservation attempt.
|
|
pub fn record_reservation(&mut self, sender: &[u8; 16]) {
|
|
let now = now();
|
|
let activity = self.activity.entry(*sender).or_default();
|
|
activity.reservation_times.push(now);
|
|
activity.last_reservation = now;
|
|
activity.pending_count = activity.pending_count.saturating_add(1);
|
|
}
|
|
|
|
/// Record reservation confirmed/completed (reduce pending).
|
|
pub fn record_reservation_resolved(&mut self, sender: &[u8; 16]) {
|
|
if let Some(activity) = self.activity.get_mut(sender) {
|
|
activity.pending_count = activity.pending_count.saturating_sub(1);
|
|
}
|
|
}
|
|
|
|
/// Check if a query is allowed.
|
|
pub fn check_query(&mut self, sender: &[u8; 16]) -> RateLimitResult {
|
|
let now = now();
|
|
let activity = self.activity.entry(*sender).or_default();
|
|
|
|
// Clean old entries
|
|
activity.query_times.retain(|&t| now - t < 60);
|
|
|
|
if activity.query_times.len() >= self.limits.max_queries_per_minute as usize {
|
|
return RateLimitResult::QueryLimitReached;
|
|
}
|
|
|
|
RateLimitResult::Allowed
|
|
}
|
|
|
|
/// Record a query.
|
|
pub fn record_query(&mut self, sender: &[u8; 16]) {
|
|
let now = now();
|
|
let activity = self.activity.entry(*sender).or_default();
|
|
activity.query_times.push(now);
|
|
}
|
|
|
|
/// Prune old activity data.
|
|
pub fn prune(&mut self) {
|
|
let now = now();
|
|
self.activity.retain(|_, a| {
|
|
a.reservation_times.retain(|&t| now - t < 3600);
|
|
a.query_times.retain(|&t| now - t < 60);
|
|
!a.reservation_times.is_empty() || !a.query_times.is_empty() || a.pending_count > 0
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Result of rate limit check.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum RateLimitResult {
|
|
/// Request allowed.
|
|
Allowed,
|
|
/// Must wait before next reservation.
|
|
Cooldown { wait_secs: u32 },
|
|
/// Hourly reservation limit reached.
|
|
HourlyLimitReached,
|
|
/// Too many pending reservations.
|
|
TooManyPending,
|
|
/// Query rate limit reached.
|
|
QueryLimitReached,
|
|
}
|
|
|
|
impl RateLimitResult {
|
|
pub fn is_allowed(&self) -> bool {
|
|
matches!(self, RateLimitResult::Allowed)
|
|
}
|
|
}
|
|
|
|
/// Proof-of-work for reservation requests.
|
|
#[derive(Debug, Clone)]
|
|
pub struct ProofOfWork {
|
|
/// Nonce that produces valid hash.
|
|
pub nonce: u64,
|
|
/// Required difficulty (leading zero bits).
|
|
pub difficulty: u8,
|
|
}
|
|
|
|
impl ProofOfWork {
|
|
/// Default difficulty (20 bits ≈ 1-2 seconds on modern CPU).
|
|
pub const DEFAULT_DIFFICULTY: u8 = 20;
|
|
|
|
/// Generate proof-of-work for a reservation.
|
|
pub fn generate(reservation_id: &[u8; 16], difficulty: u8) -> Self {
|
|
let mut nonce = 0u64;
|
|
loop {
|
|
if Self::check_hash(reservation_id, nonce, difficulty) {
|
|
return Self { nonce, difficulty };
|
|
}
|
|
nonce = nonce.wrapping_add(1);
|
|
}
|
|
}
|
|
|
|
/// Verify proof-of-work.
|
|
pub fn verify(&self, reservation_id: &[u8; 16]) -> bool {
|
|
Self::check_hash(reservation_id, self.nonce, self.difficulty)
|
|
}
|
|
|
|
fn check_hash(reservation_id: &[u8; 16], nonce: u64, difficulty: u8) -> bool {
|
|
let mut hasher = Sha256::new();
|
|
hasher.update(reservation_id);
|
|
hasher.update(&nonce.to_le_bytes());
|
|
let hash = hasher.finalize();
|
|
leading_zero_bits(&hash) >= difficulty
|
|
}
|
|
}
|
|
|
|
/// Count leading zero bits in a byte slice.
|
|
fn leading_zero_bits(data: &[u8]) -> u8 {
|
|
let mut count = 0u8;
|
|
for byte in data {
|
|
if *byte == 0 {
|
|
count += 8;
|
|
} else {
|
|
count += byte.leading_zeros() as u8;
|
|
break;
|
|
}
|
|
}
|
|
count
|
|
}
|
|
|
|
/// Sender reputation tracking.
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct SenderReputation {
|
|
pub address: [u8; 16],
|
|
pub reservations_made: u32,
|
|
pub reservations_honored: u32,
|
|
pub reservations_cancelled: u32,
|
|
pub no_shows: u32,
|
|
pub last_no_show: Option<u64>,
|
|
}
|
|
|
|
impl SenderReputation {
|
|
/// Create for a new sender.
|
|
pub fn new(address: [u8; 16]) -> Self {
|
|
Self {
|
|
address,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Calculate honor rate (0.0 to 1.0).
|
|
pub fn honor_rate(&self) -> f32 {
|
|
if self.reservations_made == 0 {
|
|
return 0.5; // Neutral for new users
|
|
}
|
|
(self.reservations_honored as f32) / (self.reservations_made as f32)
|
|
}
|
|
|
|
/// Check if sender should be blocked.
|
|
pub fn is_blocked(&self) -> bool {
|
|
self.no_shows >= 3 || (self.reservations_made >= 5 && self.honor_rate() < 0.5)
|
|
}
|
|
|
|
/// Record a completed reservation.
|
|
pub fn record_honored(&mut self) {
|
|
self.reservations_made += 1;
|
|
self.reservations_honored += 1;
|
|
}
|
|
|
|
/// Record a cancelled reservation (with notice).
|
|
pub fn record_cancelled(&mut self) {
|
|
self.reservations_made += 1;
|
|
self.reservations_cancelled += 1;
|
|
}
|
|
|
|
/// Record a no-show.
|
|
pub fn record_no_show(&mut self) {
|
|
self.reservations_made += 1;
|
|
self.no_shows += 1;
|
|
self.last_no_show = Some(now());
|
|
}
|
|
}
|
|
|
|
/// Reputation store.
|
|
#[derive(Debug, Default)]
|
|
pub struct ReputationStore {
|
|
reputations: HashMap<[u8; 16], SenderReputation>,
|
|
}
|
|
|
|
impl ReputationStore {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Get or create reputation for a sender.
|
|
pub fn get_or_create(&mut self, address: [u8; 16]) -> &mut SenderReputation {
|
|
self.reputations
|
|
.entry(address)
|
|
.or_insert_with(|| SenderReputation::new(address))
|
|
}
|
|
|
|
/// Get reputation (read-only).
|
|
pub fn get(&self, address: &[u8; 16]) -> Option<&SenderReputation> {
|
|
self.reputations.get(address)
|
|
}
|
|
|
|
/// Check if sender is blocked.
|
|
pub fn is_blocked(&self, address: &[u8; 16]) -> bool {
|
|
self.reputations
|
|
.get(address)
|
|
.map(|r| r.is_blocked())
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
/// Get honor rate (0.5 for unknown).
|
|
pub fn honor_rate(&self, address: &[u8; 16]) -> f32 {
|
|
self.reputations
|
|
.get(address)
|
|
.map(|r| r.honor_rate())
|
|
.unwrap_or(0.5)
|
|
}
|
|
}
|
|
|
|
/// Blocklist entry.
|
|
#[derive(Debug, Clone)]
|
|
pub struct BlocklistEntry {
|
|
pub blocked_address: [u8; 16],
|
|
pub reason: BlockReason,
|
|
pub reported_by: [u8; 16],
|
|
pub signature: Vec<u8>,
|
|
pub timestamp: u64,
|
|
}
|
|
|
|
/// Reason for blocking.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
#[repr(u8)]
|
|
pub enum BlockReason {
|
|
NoShow = 1,
|
|
Spam = 2,
|
|
Harassment = 3,
|
|
FakeIdentity = 4,
|
|
}
|
|
|
|
/// Therapist-defined reservation policy.
|
|
#[derive(Debug, Clone)]
|
|
pub struct TherapistPolicy {
|
|
/// Max pending reservations from new senders.
|
|
pub max_pending_new: u8,
|
|
/// Max pending from established senders.
|
|
pub max_pending_established: u8,
|
|
/// Require this verification level for reservations.
|
|
pub min_verification_level: u8,
|
|
/// Auto-reject senders with honor rate below this.
|
|
pub min_honor_rate: f32,
|
|
/// Require proof-of-work.
|
|
pub require_pow: bool,
|
|
/// PoW difficulty (if required).
|
|
pub pow_difficulty: u8,
|
|
}
|
|
|
|
impl Default for TherapistPolicy {
|
|
fn default() -> Self {
|
|
Self {
|
|
max_pending_new: 1,
|
|
max_pending_established: 3,
|
|
min_verification_level: 0,
|
|
min_honor_rate: 0.5,
|
|
require_pow: true,
|
|
pow_difficulty: ProofOfWork::DEFAULT_DIFFICULTY,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TherapistPolicy {
|
|
/// Check if a reservation request meets policy.
|
|
pub fn check(
|
|
&self,
|
|
sender_reputation: &SenderReputation,
|
|
sender_verification_level: u8,
|
|
pow: Option<&ProofOfWork>,
|
|
reservation_id: &[u8; 16],
|
|
) -> PolicyResult {
|
|
// Check verification level
|
|
if sender_verification_level < self.min_verification_level {
|
|
return PolicyResult::InsufficientVerification;
|
|
}
|
|
|
|
// Check honor rate
|
|
if sender_reputation.honor_rate() < self.min_honor_rate {
|
|
return PolicyResult::LowReputation;
|
|
}
|
|
|
|
// Check blocked
|
|
if sender_reputation.is_blocked() {
|
|
return PolicyResult::Blocked;
|
|
}
|
|
|
|
// Check proof-of-work
|
|
if self.require_pow {
|
|
match pow {
|
|
Some(p) if p.difficulty >= self.pow_difficulty && p.verify(reservation_id) => {}
|
|
Some(_) => return PolicyResult::InvalidPoW,
|
|
None => return PolicyResult::MissingPoW,
|
|
}
|
|
}
|
|
|
|
PolicyResult::Allowed
|
|
}
|
|
}
|
|
|
|
/// Result of policy check.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum PolicyResult {
|
|
Allowed,
|
|
InsufficientVerification,
|
|
LowReputation,
|
|
Blocked,
|
|
MissingPoW,
|
|
InvalidPoW,
|
|
}
|
|
|
|
impl PolicyResult {
|
|
pub fn is_allowed(&self) -> bool {
|
|
matches!(self, PolicyResult::Allowed)
|
|
}
|
|
}
|
|
|
|
fn now() -> u64 {
|
|
SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_secs()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn rate_limiter_allows_first_reservation() {
|
|
let mut limiter = RateLimiter::new();
|
|
let sender = [1u8; 16];
|
|
|
|
assert!(limiter.check_reservation(&sender).is_allowed());
|
|
}
|
|
|
|
#[test]
|
|
fn rate_limiter_enforces_cooldown() {
|
|
let mut limiter = RateLimiter::with_limits(RateLimits {
|
|
reservation_cooldown_secs: 300,
|
|
..Default::default()
|
|
});
|
|
let sender = [2u8; 16];
|
|
|
|
limiter.record_reservation(&sender);
|
|
let result = limiter.check_reservation(&sender);
|
|
|
|
assert!(matches!(result, RateLimitResult::Cooldown { .. }));
|
|
}
|
|
|
|
#[test]
|
|
fn rate_limiter_enforces_hourly_limit() {
|
|
let mut limiter = RateLimiter::with_limits(RateLimits {
|
|
max_reservations_per_hour: 2,
|
|
reservation_cooldown_secs: 0,
|
|
..Default::default()
|
|
});
|
|
let sender = [3u8; 16];
|
|
|
|
limiter.record_reservation(&sender);
|
|
limiter.record_reservation(&sender);
|
|
|
|
assert_eq!(limiter.check_reservation(&sender), RateLimitResult::HourlyLimitReached);
|
|
}
|
|
|
|
#[test]
|
|
fn pow_generation_and_verification() {
|
|
let reservation_id = [42u8; 16];
|
|
let pow = ProofOfWork::generate(&reservation_id, 8); // Low difficulty for test
|
|
|
|
assert!(pow.verify(&reservation_id));
|
|
assert!(!pow.verify(&[0u8; 16])); // Wrong ID
|
|
}
|
|
|
|
#[test]
|
|
fn reputation_tracking() {
|
|
let mut rep = SenderReputation::new([5u8; 16]);
|
|
|
|
rep.record_honored();
|
|
rep.record_honored();
|
|
rep.record_no_show();
|
|
|
|
assert_eq!(rep.reservations_made, 3);
|
|
assert_eq!(rep.honor_rate(), 2.0 / 3.0);
|
|
assert!(!rep.is_blocked());
|
|
|
|
rep.record_no_show();
|
|
rep.record_no_show();
|
|
|
|
assert!(rep.is_blocked()); // 3 no-shows
|
|
}
|
|
|
|
#[test]
|
|
fn policy_check_pow() {
|
|
let policy = TherapistPolicy {
|
|
require_pow: true,
|
|
pow_difficulty: 8,
|
|
..Default::default()
|
|
};
|
|
let rep = SenderReputation::new([6u8; 16]);
|
|
let reservation_id = [7u8; 16];
|
|
|
|
// No PoW
|
|
assert_eq!(
|
|
policy.check(&rep, 0, None, &reservation_id),
|
|
PolicyResult::MissingPoW
|
|
);
|
|
|
|
// Valid PoW
|
|
let pow = ProofOfWork::generate(&reservation_id, 8);
|
|
assert_eq!(
|
|
policy.check(&rep, 0, Some(&pow), &reservation_id),
|
|
PolicyResult::Allowed
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn policy_check_verification_level() {
|
|
let policy = TherapistPolicy {
|
|
min_verification_level: 2,
|
|
require_pow: false,
|
|
..Default::default()
|
|
};
|
|
let rep = SenderReputation::new([8u8; 16]);
|
|
let reservation_id = [9u8; 16];
|
|
|
|
assert_eq!(
|
|
policy.check(&rep, 1, None, &reservation_id),
|
|
PolicyResult::InsufficientVerification
|
|
);
|
|
|
|
assert_eq!(
|
|
policy.check(&rep, 2, None, &reservation_id),
|
|
PolicyResult::Allowed
|
|
);
|
|
}
|
|
}
|