Public server: separate in/out IP quotas, web dashboard scaffold, test intervals

3 agents worked in parallel:

1. DB schema (user_db.rs):
   - ip_usage: inbound_bytes/outbound_bytes columns (renamed from tx/rx)
   - test_intervals table for per-second graphing data
   - Directional methods: get_ip_daily_inbound/outbound, record_ip_inbound/outbound
   - Query methods: get_session_intervals, get_ip_sessions, get_ip_stats
   - New structs: IntervalData, SessionSummary, IpStats

2. Quota (quota.rs):
   - Direction enum (Inbound/Outbound/Both)
   - 6 new directional IP limits (daily/weekly/monthly × in/out)
   - check_ip() now takes direction parameter
   - record_usage() takes (inbound_bytes, outbound_bytes)

3. Web dashboard (web/):
   - Stub router with axum (will be expanded)
   - Templates: index.html + dashboard.html with Chart.js
   - Dependencies: axum, tower-http, serde, serde_json, askama (optional, pro feature)

CLI additions:
  --ip-daily-in, --ip-daily-out, --web-port, --shared-password

64 tests, all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-04-01 16:30:18 +04:00
parent 9e3cd6d6d4
commit 2087e5a75f
10 changed files with 1992 additions and 47 deletions

View File

@@ -12,6 +12,7 @@ mod user_db;
mod quota;
mod enforcer;
mod server_loop;
mod web;
mod ldap_auth;
use clap::Parser;
@@ -88,10 +89,26 @@ struct Cli {
#[arg(long = "max-duration", default_value_t = 300)]
max_duration: u64,
/// Daily inbound (client→server) limit per IP in bytes (0 = unlimited)
#[arg(long = "ip-daily-in", default_value_t = 0)]
ip_daily_in: u64,
/// Daily outbound (server→client) limit per IP in bytes (0 = unlimited)
#[arg(long = "ip-daily-out", default_value_t = 0)]
ip_daily_out: u64,
/// How often to check quotas during a test in seconds
#[arg(long = "quota-check-interval", default_value_t = 10)]
quota_check_interval: u64,
/// Web dashboard port (0 = disabled)
#[arg(long = "web-port", default_value_t = 8080)]
web_port: u16,
/// Shared password for public mode (all users use this password)
#[arg(long = "shared-password")]
shared_password: Option<String>,
/// Use EC-SRP5 authentication
#[arg(long = "ecsrp5")]
ecsrp5: bool,
@@ -242,6 +259,8 @@ async fn main() -> anyhow::Result<()> {
}
// Initialize quota manager
// Directional IP quotas default to 0 (unlimited) unless the combined
// quota is set, in which case the same value is used for each direction.
let quota_mgr = quota::QuotaManager::new(
db.clone(),
cli.daily_quota,
@@ -250,6 +269,12 @@ async fn main() -> anyhow::Result<()> {
cli.ip_daily,
cli.ip_weekly,
cli.ip_monthly,
cli.ip_daily, // ip_daily_inbound
cli.ip_daily, // ip_daily_outbound
cli.ip_weekly, // ip_weekly_inbound
cli.ip_weekly, // ip_weekly_outbound
cli.ip_monthly, // ip_monthly_inbound
cli.ip_monthly, // ip_monthly_outbound
cli.max_conn_per_ip,
cli.max_duration,
);
@@ -268,6 +293,22 @@ async fn main() -> anyhow::Result<()> {
cli.max_conn_per_ip, cli.max_duration,
);
// Start web dashboard if port > 0
if cli.web_port > 0 {
let web_db = db.clone();
let web_port = cli.web_port;
tokio::spawn(async move {
tracing::info!("Web dashboard starting on http://0.0.0.0:{}", web_port);
let app = web::create_router(web_db);
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", web_port))
.await
.expect("Failed to bind web dashboard port");
if let Err(e) = axum::serve(listener, app).await {
tracing::error!("Web dashboard error: {}", e);
}
});
}
tracing::info!("btest-server-pro starting on port {}", cli.port);
let v4 = if cli.listen_addr.eq_ignore_ascii_case("none") { None } else { Some(cli.listen_addr) };