feat: add anti-abuse mechanisms
Prevent slot blocking and reservation spam: - RateLimiter: per-sender cooldowns, hourly limits, pending caps - ProofOfWork: Hashcash-style PoW for reservation requests - SenderReputation: track honor rate, no-shows, auto-block - TherapistPolicy: configurable requirements per provider - 7 new tests (39 total) Docs: docs/anti-abuse.md with implementation roadmap
This commit is contained in:
216
docs/anti-abuse.md
Normal file
216
docs/anti-abuse.md
Normal file
@@ -0,0 +1,216 @@
|
||||
# Anti-Abuse: Preventing Slot Blocking
|
||||
|
||||
## Problem
|
||||
|
||||
Bad actors could abuse the reservation system by:
|
||||
1. **Slot Squatting**: Reserving slots with no intention to attend
|
||||
2. **Denial of Service**: Mass-reserving to block legitimate patients
|
||||
3. **Competitor Sabotage**: Blocking a therapist's calendar
|
||||
4. **Spam Queries**: Flooding the network with fake queries
|
||||
|
||||
## Defense Layers
|
||||
|
||||
### Layer 1: Rate Limiting (Protocol Level)
|
||||
|
||||
```rust
|
||||
pub struct RateLimits {
|
||||
/// Max reservations per sender per hour
|
||||
pub max_reservations_per_hour: u8, // Default: 3
|
||||
/// Max pending (unconfirmed) reservations per sender
|
||||
pub max_pending_reservations: u8, // Default: 2
|
||||
/// Min time between reservations (seconds)
|
||||
pub reservation_cooldown_secs: u32, // Default: 300 (5 min)
|
||||
/// Max queries per sender per minute
|
||||
pub max_queries_per_minute: u8, // Default: 10
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation**: Each relay tracks sender activity and drops excessive requests.
|
||||
|
||||
### Layer 2: Reservation Deposits (Economic)
|
||||
|
||||
Require a small proof-of-work or micro-deposit to make reservations:
|
||||
|
||||
| Method | Cost to Attacker | UX Impact |
|
||||
|--------|------------------|-----------|
|
||||
| **Hashcash PoW** | CPU time (~1-5s per reservation) | Slight delay |
|
||||
| **Token Stake** | Loses stake on no-show | Requires token system |
|
||||
| **Reputation Bond** | Loses reputation on abuse | Requires history |
|
||||
|
||||
**Recommended**: Start with Hashcash PoW — no external dependencies.
|
||||
|
||||
```rust
|
||||
pub struct ReservationProof {
|
||||
/// Hashcash proof-of-work (20-bit difficulty)
|
||||
pub pow_nonce: u64,
|
||||
/// Hash must start with N zero bits
|
||||
pub difficulty: u8,
|
||||
}
|
||||
|
||||
impl ReservationProof {
|
||||
pub fn verify(&self, reservation_id: &[u8; 16]) -> bool {
|
||||
let hash = sha256([reservation_id, &self.pow_nonce.to_le_bytes()].concat());
|
||||
leading_zeros(&hash) >= self.difficulty
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Layer 3: Confirmation Requirements
|
||||
|
||||
Therapists can require confirmation steps:
|
||||
|
||||
```rust
|
||||
pub enum ConfirmationMode {
|
||||
/// Auto-confirm (trust network)
|
||||
AutoConfirm,
|
||||
/// Require patient to solve CAPTCHA-like challenge
|
||||
ChallengeResponse { challenge: Vec<u8> },
|
||||
/// Require callback to verify contact
|
||||
ContactVerification { method: ContactMethod },
|
||||
/// Manual review by therapist
|
||||
ManualReview,
|
||||
}
|
||||
```
|
||||
|
||||
### Layer 4: No-Show Tracking
|
||||
|
||||
Track reservation outcomes per sender:
|
||||
|
||||
```rust
|
||||
pub struct SenderReputation {
|
||||
pub address: [u8; 16],
|
||||
pub reservations_made: u32,
|
||||
pub reservations_honored: u32,
|
||||
pub reservations_cancelled: u32, // With notice
|
||||
pub no_shows: u32, // Without notice
|
||||
pub last_no_show: Option<u64>,
|
||||
}
|
||||
|
||||
impl SenderReputation {
|
||||
pub fn honor_rate(&self) -> f32 {
|
||||
if self.reservations_made == 0 { return 0.5; }
|
||||
(self.reservations_honored as f32) / (self.reservations_made as f32)
|
||||
}
|
||||
|
||||
pub fn is_blocked(&self) -> bool {
|
||||
self.no_shows >= 3 || self.honor_rate() < 0.5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Therapists can share blocklists** via signed messages:
|
||||
|
||||
```rust
|
||||
pub struct BlocklistEntry {
|
||||
pub blocked_address: [u8; 16],
|
||||
pub reason: BlockReason,
|
||||
pub reported_by: [u8; 16],
|
||||
pub signature: [u8; 64],
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
pub enum BlockReason {
|
||||
NoShow,
|
||||
Spam,
|
||||
Harassment,
|
||||
FakeIdentity,
|
||||
}
|
||||
```
|
||||
|
||||
### Layer 5: Reservation Limits per Therapist
|
||||
|
||||
Therapists set their own limits:
|
||||
|
||||
```rust
|
||||
pub struct TherapistPolicy {
|
||||
/// Max pending reservations from new senders
|
||||
pub max_pending_new: u8, // Default: 1
|
||||
/// Max pending from established senders
|
||||
pub max_pending_established: u8, // Default: 3
|
||||
/// Require verification level for reservations
|
||||
pub min_verification_level: u8, // 0 = any, 2 = peer-endorsed
|
||||
/// Auto-reject senders with low honor rate
|
||||
pub min_honor_rate: f32, // Default: 0.7
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Roadmap
|
||||
|
||||
### Phase 1: Basic Rate Limiting (Week 1)
|
||||
- [ ] Add `RateLimiter` to `ServiceRouter`
|
||||
- [ ] Track per-sender reservation counts
|
||||
- [ ] Drop excessive requests with `ServiceAction::RateLimited`
|
||||
|
||||
### Phase 2: Proof-of-Work (Week 2)
|
||||
- [ ] Add `ReservationProof` to reserve message payload
|
||||
- [ ] Verify PoW before processing reservation
|
||||
- [ ] Adaptive difficulty based on network load
|
||||
|
||||
### Phase 3: Reputation System (Week 3-4)
|
||||
- [ ] `SenderReputation` storage
|
||||
- [ ] Honor/no-show reporting from therapists
|
||||
- [ ] Blocklist propagation
|
||||
|
||||
### Phase 4: Therapist Policies (Week 4+)
|
||||
- [ ] Policy field in `SlotAnnounce`
|
||||
- [ ] Policy enforcement in reservation handling
|
||||
|
||||
## Example: Complete Anti-Abuse Flow
|
||||
|
||||
```
|
||||
Patient Relay Therapist
|
||||
| | |
|
||||
|-- Query (CBT, 104xx) -->| |
|
||||
|<-- Matches (3 slots) ---| |
|
||||
| | |
|
||||
|-- Reserve (slot_id) --->| |
|
||||
| + PoW proof | |
|
||||
| (1.2s computation) | |
|
||||
| |-- Check rate limit ------>|
|
||||
| | (OK: 1st today) |
|
||||
| |-- Check reputation ------>|
|
||||
| | (OK: new sender, 0.5) |
|
||||
| |-- Forward reserve ------->|
|
||||
| | |
|
||||
| |<-- Confirm (pending) -----|
|
||||
|<-- Pending -------------| |
|
||||
| | |
|
||||
| [Patient attends] | |
|
||||
| |<-- Outcome: honored ------|
|
||||
| | (reputation += 1) |
|
||||
```
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### New Users (No History)
|
||||
|
||||
- Allow 1 pending reservation
|
||||
- Require PoW
|
||||
- Lower priority than established users
|
||||
|
||||
### Therapist Gaming the System
|
||||
|
||||
- Therapists could falsely report no-shows
|
||||
- Mitigation: Require mutual confirmation (patient confirms attendance too)
|
||||
- Weight reports by reporter reputation
|
||||
|
||||
### Sybil Attacks (Many Fake Identities)
|
||||
|
||||
- Each identity requires new Ed25519 keypair
|
||||
- PoW per reservation makes mass-blocking expensive
|
||||
- Cross-relay blocklist sharing limits damage
|
||||
|
||||
## Privacy Considerations
|
||||
|
||||
- Reputation is tied to mesh address, not real identity
|
||||
- No-show reports don't reveal appointment details
|
||||
- Blocklists only contain addresses + reason, not personal data
|
||||
|
||||
## Metrics to Monitor
|
||||
|
||||
| Metric | Healthy Range | Alert Threshold |
|
||||
|--------|---------------|-----------------|
|
||||
| Reservation/Confirm ratio | > 0.8 | < 0.5 |
|
||||
| Unique senders per relay | Growing | Flat with high volume |
|
||||
| PoW rejection rate | < 5% | > 20% |
|
||||
| Blocklist growth | Slow | Rapid spike |
|
||||
532
src/anti_abuse.rs
Normal file
532
src/anti_abuse.rs
Normal file
@@ -0,0 +1,532 @@
|
||||
//! 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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@ pub mod verification;
|
||||
pub mod services;
|
||||
pub mod wire;
|
||||
pub mod error;
|
||||
pub mod anti_abuse;
|
||||
|
||||
pub use identity::ServiceIdentity;
|
||||
pub use message::{ServiceMessage, MessageType};
|
||||
@@ -52,6 +53,7 @@ pub use router::{ServiceRouter, ServiceHandler, ServiceAction};
|
||||
pub use store::ServiceStore;
|
||||
pub use verification::{Verification, VerificationLevel};
|
||||
pub use error::ServiceError;
|
||||
pub use anti_abuse::{RateLimiter, RateLimits, ProofOfWork, SenderReputation, TherapistPolicy};
|
||||
|
||||
/// Well-known service IDs.
|
||||
pub mod service_ids {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,8 +1,8 @@
|
||||
/home/c/projects/tool.meshservice/target/debug/deps/meshservice-286f0426bc447b66.d: src/lib.rs src/identity.rs src/message.rs src/router.rs src/store.rs src/verification.rs src/services/mod.rs src/services/fapp.rs src/services/housing.rs src/wire.rs src/error.rs
|
||||
/home/c/projects/tool.meshservice/target/debug/deps/meshservice-286f0426bc447b66.d: src/lib.rs src/identity.rs src/message.rs src/router.rs src/store.rs src/verification.rs src/services/mod.rs src/services/fapp.rs src/services/housing.rs src/wire.rs src/error.rs src/anti_abuse.rs
|
||||
|
||||
/home/c/projects/tool.meshservice/target/debug/deps/libmeshservice-286f0426bc447b66.rlib: src/lib.rs src/identity.rs src/message.rs src/router.rs src/store.rs src/verification.rs src/services/mod.rs src/services/fapp.rs src/services/housing.rs src/wire.rs src/error.rs
|
||||
/home/c/projects/tool.meshservice/target/debug/deps/libmeshservice-286f0426bc447b66.rlib: src/lib.rs src/identity.rs src/message.rs src/router.rs src/store.rs src/verification.rs src/services/mod.rs src/services/fapp.rs src/services/housing.rs src/wire.rs src/error.rs src/anti_abuse.rs
|
||||
|
||||
/home/c/projects/tool.meshservice/target/debug/deps/libmeshservice-286f0426bc447b66.rmeta: src/lib.rs src/identity.rs src/message.rs src/router.rs src/store.rs src/verification.rs src/services/mod.rs src/services/fapp.rs src/services/housing.rs src/wire.rs src/error.rs
|
||||
/home/c/projects/tool.meshservice/target/debug/deps/libmeshservice-286f0426bc447b66.rmeta: src/lib.rs src/identity.rs src/message.rs src/router.rs src/store.rs src/verification.rs src/services/mod.rs src/services/fapp.rs src/services/housing.rs src/wire.rs src/error.rs src/anti_abuse.rs
|
||||
|
||||
src/lib.rs:
|
||||
src/identity.rs:
|
||||
@@ -15,3 +15,4 @@ src/services/fapp.rs:
|
||||
src/services/housing.rs:
|
||||
src/wire.rs:
|
||||
src/error.rs:
|
||||
src/anti_abuse.rs:
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
/home/c/projects/tool.meshservice/target/debug/deps/meshservice-a34e90bc28566d7f.d: src/lib.rs src/identity.rs src/message.rs src/router.rs src/store.rs src/verification.rs src/services/mod.rs src/services/fapp.rs src/services/housing.rs src/wire.rs src/error.rs
|
||||
/home/c/projects/tool.meshservice/target/debug/deps/meshservice-a34e90bc28566d7f.d: src/lib.rs src/identity.rs src/message.rs src/router.rs src/store.rs src/verification.rs src/services/mod.rs src/services/fapp.rs src/services/housing.rs src/wire.rs src/error.rs src/anti_abuse.rs
|
||||
|
||||
/home/c/projects/tool.meshservice/target/debug/deps/meshservice-a34e90bc28566d7f: src/lib.rs src/identity.rs src/message.rs src/router.rs src/store.rs src/verification.rs src/services/mod.rs src/services/fapp.rs src/services/housing.rs src/wire.rs src/error.rs
|
||||
/home/c/projects/tool.meshservice/target/debug/deps/meshservice-a34e90bc28566d7f: src/lib.rs src/identity.rs src/message.rs src/router.rs src/store.rs src/verification.rs src/services/mod.rs src/services/fapp.rs src/services/housing.rs src/wire.rs src/error.rs src/anti_abuse.rs
|
||||
|
||||
src/lib.rs:
|
||||
src/identity.rs:
|
||||
@@ -13,3 +13,4 @@ src/services/fapp.rs:
|
||||
src/services/housing.rs:
|
||||
src/wire.rs:
|
||||
src/error.rs:
|
||||
src/anti_abuse.rs:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1 +1 @@
|
||||
/home/c/projects/tool.meshservice/target/debug/examples/fapp_service: /home/c/projects/tool.meshservice/examples/fapp_service.rs /home/c/projects/tool.meshservice/src/error.rs /home/c/projects/tool.meshservice/src/identity.rs /home/c/projects/tool.meshservice/src/lib.rs /home/c/projects/tool.meshservice/src/message.rs /home/c/projects/tool.meshservice/src/router.rs /home/c/projects/tool.meshservice/src/services/fapp.rs /home/c/projects/tool.meshservice/src/services/housing.rs /home/c/projects/tool.meshservice/src/services/mod.rs /home/c/projects/tool.meshservice/src/store.rs /home/c/projects/tool.meshservice/src/verification.rs /home/c/projects/tool.meshservice/src/wire.rs
|
||||
/home/c/projects/tool.meshservice/target/debug/examples/fapp_service: /home/c/projects/tool.meshservice/examples/fapp_service.rs /home/c/projects/tool.meshservice/src/anti_abuse.rs /home/c/projects/tool.meshservice/src/error.rs /home/c/projects/tool.meshservice/src/identity.rs /home/c/projects/tool.meshservice/src/lib.rs /home/c/projects/tool.meshservice/src/message.rs /home/c/projects/tool.meshservice/src/router.rs /home/c/projects/tool.meshservice/src/services/fapp.rs /home/c/projects/tool.meshservice/src/services/housing.rs /home/c/projects/tool.meshservice/src/services/mod.rs /home/c/projects/tool.meshservice/src/store.rs /home/c/projects/tool.meshservice/src/verification.rs /home/c/projects/tool.meshservice/src/wire.rs
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1 +1 @@
|
||||
/home/c/projects/tool.meshservice/target/debug/examples/housing_service: /home/c/projects/tool.meshservice/examples/housing_service.rs /home/c/projects/tool.meshservice/src/error.rs /home/c/projects/tool.meshservice/src/identity.rs /home/c/projects/tool.meshservice/src/lib.rs /home/c/projects/tool.meshservice/src/message.rs /home/c/projects/tool.meshservice/src/router.rs /home/c/projects/tool.meshservice/src/services/fapp.rs /home/c/projects/tool.meshservice/src/services/housing.rs /home/c/projects/tool.meshservice/src/services/mod.rs /home/c/projects/tool.meshservice/src/store.rs /home/c/projects/tool.meshservice/src/verification.rs /home/c/projects/tool.meshservice/src/wire.rs
|
||||
/home/c/projects/tool.meshservice/target/debug/examples/housing_service: /home/c/projects/tool.meshservice/examples/housing_service.rs /home/c/projects/tool.meshservice/src/anti_abuse.rs /home/c/projects/tool.meshservice/src/error.rs /home/c/projects/tool.meshservice/src/identity.rs /home/c/projects/tool.meshservice/src/lib.rs /home/c/projects/tool.meshservice/src/message.rs /home/c/projects/tool.meshservice/src/router.rs /home/c/projects/tool.meshservice/src/services/fapp.rs /home/c/projects/tool.meshservice/src/services/housing.rs /home/c/projects/tool.meshservice/src/services/mod.rs /home/c/projects/tool.meshservice/src/store.rs /home/c/projects/tool.meshservice/src/verification.rs /home/c/projects/tool.meshservice/src/wire.rs
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1 +1 @@
|
||||
/home/c/projects/tool.meshservice/target/debug/examples/multi_service: /home/c/projects/tool.meshservice/examples/multi_service.rs /home/c/projects/tool.meshservice/src/error.rs /home/c/projects/tool.meshservice/src/identity.rs /home/c/projects/tool.meshservice/src/lib.rs /home/c/projects/tool.meshservice/src/message.rs /home/c/projects/tool.meshservice/src/router.rs /home/c/projects/tool.meshservice/src/services/fapp.rs /home/c/projects/tool.meshservice/src/services/housing.rs /home/c/projects/tool.meshservice/src/services/mod.rs /home/c/projects/tool.meshservice/src/store.rs /home/c/projects/tool.meshservice/src/verification.rs /home/c/projects/tool.meshservice/src/wire.rs
|
||||
/home/c/projects/tool.meshservice/target/debug/examples/multi_service: /home/c/projects/tool.meshservice/examples/multi_service.rs /home/c/projects/tool.meshservice/src/anti_abuse.rs /home/c/projects/tool.meshservice/src/error.rs /home/c/projects/tool.meshservice/src/identity.rs /home/c/projects/tool.meshservice/src/lib.rs /home/c/projects/tool.meshservice/src/message.rs /home/c/projects/tool.meshservice/src/router.rs /home/c/projects/tool.meshservice/src/services/fapp.rs /home/c/projects/tool.meshservice/src/services/housing.rs /home/c/projects/tool.meshservice/src/services/mod.rs /home/c/projects/tool.meshservice/src/store.rs /home/c/projects/tool.meshservice/src/verification.rs /home/c/projects/tool.meshservice/src/wire.rs
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user