Public btest server: byte budget, multi-conn, web dashboard, quotas

- Inline byte budget in BandwidthState prevents quota overshoot at any
  link speed (TX/RX loops check per-packet, not per-interval)
- TCP multi-connection support for server-pro (session tokens, secondary
  connection joins, delegates to standard multi-conn handler)
- MD5 password verification against stored raw passwords in user DB
- Web dashboard: quota progress bars (daily/weekly/monthly), JSON export
  endpoint (/api/ip/{ip}/export), quota API (/api/ip/{ip}/quota)
- Landing page with usage instructions, UDP NAT warning, credentials
- Fix IP usage double-counting bug in QuotaManager::record_usage
- UserDb now stores DB path and raw passwords for MD5 auth
- 10 enforcer tests (4 new: budget calc, budget stop, budget exhausted,
  unlimited passthrough)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-04-01 18:43:09 +04:00
parent 7dd4820d2c
commit 4cdcc4e6c4
7 changed files with 763 additions and 98 deletions

View File

@@ -20,6 +20,9 @@ pub struct BandwidthState {
pub intervals: AtomicU32,
/// Remote peer's CPU usage (received via status messages)
pub remote_cpu: AtomicU8,
/// Remaining byte budget (TX + RX combined). When this reaches 0 the test
/// stops immediately. u64::MAX means unlimited (default for non-pro server).
pub byte_budget: AtomicU64,
}
impl BandwidthState {
@@ -38,6 +41,7 @@ impl BandwidthState {
total_lost_packets: AtomicU64::new(0),
intervals: AtomicU32::new(0),
remote_cpu: AtomicU8::new(0),
byte_budget: AtomicU64::new(u64::MAX),
})
}
@@ -50,6 +54,29 @@ impl BandwidthState {
self.intervals.fetch_add(1, Relaxed);
}
/// Try to spend `amount` bytes from the budget. Returns `true` if allowed,
/// `false` if the budget is exhausted (and sets `running = false`).
#[inline]
pub fn spend_budget(&self, amount: u64) -> bool {
use std::sync::atomic::Ordering::{Relaxed, SeqCst};
// Fast path: unlimited budget (non-pro server)
let current = self.byte_budget.load(Relaxed);
if current == u64::MAX {
return true;
}
if current < amount {
self.running.store(false, SeqCst);
return false;
}
self.byte_budget.fetch_sub(amount, Relaxed);
true
}
/// Set the byte budget (total bytes allowed for the entire test).
pub fn set_budget(&self, budget: u64) {
self.byte_budget.store(budget, std::sync::atomic::Ordering::SeqCst);
}
/// Get summary for syslog reporting.
pub fn summary(&self) -> (u64, u64, u64, u32) {
use std::sync::atomic::Ordering::Relaxed;