Files
btest-rs/src/server_pro/main.rs
Siavash Sameni d2fdc9c6ae Scaffold btest-server-pro: multi-user, quotas, LDAP
New binary `btest-server-pro` (build with --features pro):
  cargo build --release --features pro --bin btest-server-pro

Modules:
- server_pro/user_db.rs: SQLite user database with usage tracking
  - Users table (username, password_hash, quotas, enabled)
  - Usage table (daily bytes per user)
  - Sessions table (per-connection tracking)
- server_pro/quota.rs: bandwidth quota enforcement
  - Per-user daily/weekly limits
  - Per-IP connection limits
  - Max test duration
- server_pro/ldap_auth.rs: LDAP/AD authentication via ldap3
  - Simple bind authentication
  - Service account search for user DN

CLI flags: --users-db, --ldap-url, --ldap-base-dn, --ldap-bind-dn,
  --ldap-bind-pass, --daily-quota, --weekly-quota, --max-conn-per-ip,
  --max-duration

Binary sizes: btest=1.8MB, btest-server-pro=3.4MB (SQLite bundled)
Standard btest binary unchanged, 58 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 14:33:36 +04:00

159 lines
4.6 KiB
Rust

//! btest-server-pro: MikroTik Bandwidth Test server with multi-user, quotas, and LDAP.
//!
//! This is a superset of the standard `btest` server with additional features:
//! - SQLite user database (--users-db)
//! - Per-user and per-IP bandwidth quotas (daily/weekly)
//! - LDAP/Active Directory authentication (--ldap-url)
//! - Rate limiting for public server deployment
//!
//! Build with: cargo build --release --features pro --bin btest-server-pro
mod user_db;
mod quota;
mod ldap_auth;
use clap::Parser;
use tracing_subscriber::EnvFilter;
#[derive(Parser, Debug)]
#[command(
name = "btest-server-pro",
about = "btest-rs Pro Server: multi-user, quotas, LDAP",
version,
)]
struct Cli {
/// Listen port
#[arg(short = 'P', long = "port", default_value_t = 2000)]
port: u16,
/// IPv4 listen address
#[arg(long = "listen", default_value = "0.0.0.0")]
listen_addr: String,
/// IPv6 listen address (optional)
#[arg(long = "listen6")]
listen6_addr: Option<String>,
/// SQLite user database path
#[arg(long = "users-db", default_value = "btest-users.db")]
users_db: String,
/// LDAP server URL (e.g., ldap://dc.example.com)
#[arg(long = "ldap-url")]
ldap_url: Option<String>,
/// LDAP base DN for user search
#[arg(long = "ldap-base-dn")]
ldap_base_dn: Option<String>,
/// LDAP bind DN (for service account)
#[arg(long = "ldap-bind-dn")]
ldap_bind_dn: Option<String>,
/// LDAP bind password
#[arg(long = "ldap-bind-pass")]
ldap_bind_pass: Option<String>,
/// Default daily quota per user in bytes (0 = unlimited)
#[arg(long = "daily-quota", default_value_t = 0)]
daily_quota: u64,
/// Default weekly quota per user in bytes (0 = unlimited)
#[arg(long = "weekly-quota", default_value_t = 0)]
weekly_quota: u64,
/// Maximum concurrent connections per IP (0 = unlimited)
#[arg(long = "max-conn-per-ip", default_value_t = 5)]
max_conn_per_ip: u32,
/// Maximum test duration in seconds (0 = unlimited)
#[arg(long = "max-duration", default_value_t = 300)]
max_duration: u64,
/// Use EC-SRP5 authentication
#[arg(long = "ecsrp5")]
ecsrp5: bool,
/// Syslog server address
#[arg(long = "syslog")]
syslog: Option<String>,
/// CSV output file
#[arg(long = "csv")]
csv: Option<String>,
/// Verbose logging
#[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count)]
verbose: u8,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
let filter = match cli.verbose {
0 => "info",
1 => "debug",
_ => "trace",
};
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(filter)),
)
.with_target(false)
.init();
// Initialize subsystems
btest_rs::cpu::start_sampler();
if let Some(ref syslog_addr) = cli.syslog {
if let Err(e) = btest_rs::syslog_logger::init(syslog_addr) {
eprintln!("Warning: syslog init failed: {}", e);
}
}
if let Some(ref csv_path) = cli.csv {
if let Err(e) = btest_rs::csv_output::init(csv_path) {
eprintln!("Warning: CSV init failed: {}", e);
}
}
// Initialize user database
tracing::info!("Opening user database: {}", cli.users_db);
let db = user_db::UserDb::open(&cli.users_db)?;
db.ensure_tables()?;
tracing::info!("User database ready ({} users)", db.user_count()?);
// Initialize LDAP if configured
if let Some(ref url) = cli.ldap_url {
tracing::info!("LDAP configured: {}", url);
}
// Initialize quota manager
let quota_mgr = quota::QuotaManager::new(
db.clone(),
cli.daily_quota,
cli.weekly_quota,
cli.max_conn_per_ip,
cli.max_duration,
);
tracing::info!(
"Quotas: daily={}, weekly={}, max_conn_per_ip={}, max_duration={}s",
if cli.daily_quota == 0 { "unlimited".to_string() } else { format!("{}", cli.daily_quota) },
if cli.weekly_quota == 0 { "unlimited".to_string() } else { format!("{}", cli.weekly_quota) },
cli.max_conn_per_ip,
cli.max_duration,
);
tracing::info!("btest-server-pro starting on port {}", cli.port);
// TODO: Run the enhanced server loop with quota checks and multi-user auth
// For now, delegate to the standard server
let v4 = if cli.listen_addr.eq_ignore_ascii_case("none") { None } else { Some(cli.listen_addr) };
let v6 = cli.listen6_addr;
btest_rs::server::run_server(cli.port, None, None, cli.ecsrp5, v4, v6).await?;
Ok(())
}