feat: CDN Control Plane (ONCP) implementation

- Add REST API for node/user management (axum-based)
- Add NodeRegistry for server check-in and load balancing
- Add SniManager for dynamic SNI updates and emergency blocking
- Add CDN Dashboard CLI (oncp-master) with real-time monitoring
- Add ProbeDetector in ostp-guard for active probing detection
- Add iptables/nftables/Windows firewall ban integration
- Extend MimicryEngine with async SNI updates from control plane
- Fix all compilation warnings
- Update author to ospab.team
This commit is contained in:
2026-01-01 20:33:03 +03:00
parent fc00214b07
commit 6d4c06a013
19 changed files with 2671 additions and 15 deletions

25
oncp-master/Cargo.toml Normal file
View File

@@ -0,0 +1,25 @@
[package]
name = "oncp-master"
version.workspace = true
edition.workspace = true
[[bin]]
name = "oncp-master"
path = "src/main.rs"
[dependencies]
oncp = { path = "../oncp" }
ostp = { path = "../ostp" }
tokio.workspace = true
anyhow.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
clap.workspace = true
serde.workspace = true
serde_json.workspace = true
console = "0.15"
dialoguer = "0.11"
indicatif = "0.17"
chrono.workspace = true
uuid.workspace = true
base64.workspace = true

556
oncp-master/src/main.rs Normal file
View File

@@ -0,0 +1,556 @@
//! ONCP Master Node - CDN Control Plane CLI & API Server
//!
//! Provides:
//! - REST API for node and user management
//! - CLI dashboard for monitoring
//! - Dynamic SNI distribution
//! - Network health monitoring
use anyhow::Result;
use clap::{Parser, Subcommand};
use console::{style, Term};
use indicatif::{ProgressBar, ProgressStyle};
use oncp::{AppState, Node, User, run_server};
use std::sync::Arc;
use std::time::Duration;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[derive(Parser)]
#[command(name = "oncp-master")]
#[command(about = "OSTP CDN Control Plane - Master Node", long_about = None)]
#[command(version)]
struct Cli {
#[command(subcommand)]
command: Commands,
/// Database path
#[arg(short, long, default_value = "oncp.db", global = true)]
database: String,
/// Log level (error, warn, info, debug, trace)
#[arg(long, default_value = "info", global = true)]
log_level: String,
}
#[derive(Subcommand)]
enum Commands {
/// Start the API server
Serve {
/// Listen address
#[arg(short, long, default_value = "0.0.0.0:8080")]
listen: String,
},
/// Interactive dashboard
Dashboard,
/// Node management commands
Node {
#[command(subcommand)]
action: NodeCommands,
},
/// User management commands
User {
#[command(subcommand)]
action: UserCommands,
},
/// SNI management commands
Sni {
#[command(subcommand)]
action: SniCommands,
},
/// Show network statistics
Stats,
}
#[derive(Subcommand)]
enum NodeCommands {
/// List all nodes
List,
/// Add a new node
Add {
/// Node name
#[arg(short, long)]
name: String,
/// Node address (ip:port)
#[arg(short, long)]
address: String,
/// Country code
#[arg(short, long)]
country: String,
},
/// Remove a node
Remove {
/// Node ID
id: String,
},
}
#[derive(Subcommand)]
enum UserCommands {
/// List all users
List,
/// Create a new user
Create {
/// Bandwidth quota in GB
#[arg(short, long, default_value = "100")]
quota: u64,
/// Validity in days
#[arg(short, long, default_value = "30")]
days: i64,
},
/// Show user config (for client setup)
Config {
/// User UUID
id: String,
},
/// Delete a user
Delete {
/// User UUID
id: String,
},
}
#[derive(Subcommand)]
enum SniCommands {
/// List active SNIs
List,
/// Block a domain
Block {
/// Domain to block
domain: String,
},
/// Add a domain
Add {
/// Domain to add
domain: String,
/// Country code
#[arg(short, long, default_value = "GLOBAL")]
country: String,
},
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
// Initialize logging
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::new(&cli.log_level))
.with(tracing_subscriber::fmt::layer())
.init();
let state = Arc::new(AppState::new(&cli.database)?);
// Initialize default SNIs
state.sni_manager.init_defaults().await;
match cli.command {
Commands::Serve { listen } => {
run_api_server(state, &listen).await
}
Commands::Dashboard => {
run_dashboard(state).await
}
Commands::Node { action } => {
handle_node_command(state, action).await
}
Commands::User { action } => {
handle_user_command(state, action).await
}
Commands::Sni { action } => {
handle_sni_command(state, action).await
}
Commands::Stats => {
show_stats(state).await
}
}
}
async fn run_api_server(state: Arc<AppState>, listen: &str) -> Result<()> {
println!("{}", style("🚀 Starting ONCP Master Node").green().bold());
println!(" {} API listening on {}", style("").cyan(), style(listen).yellow());
println!(" {} Database: {}", style("").cyan(), style("oncp.db").yellow());
println!();
// Start stale cleanup task
let cleanup_state = state.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(30)).await;
let stale = cleanup_state.nodes.cleanup_stale().await;
if !stale.is_empty() {
tracing::info!("Marked {} nodes as offline (stale)", stale.len());
}
}
});
run_server(state, listen).await
}
async fn run_dashboard(state: Arc<AppState>) -> Result<()> {
let term = Term::stdout();
loop {
term.clear_screen()?;
// Header
println!("{}", style("╔════════════════════════════════════════════════════════════════╗").cyan());
println!("{}", style("║ OSTP CDN Control Plane - Live Dashboard ║").cyan().bold());
println!("{}", style("╚════════════════════════════════════════════════════════════════╝").cyan());
println!();
// Network stats
let stats = state.nodes.network_stats().await;
println!("{}", style("📊 Network Status").green().bold());
println!(" ├─ Nodes: {} / {} online",
style(stats.online_nodes).green().bold(),
style(stats.total_nodes).dim());
println!(" ├─ Connections: {}", style(stats.total_connections).yellow().bold());
println!(" ├─ Traffic TX: {:.2} MB", stats.total_bytes_tx as f64 / 1024.0 / 1024.0);
println!(" ├─ Traffic RX: {:.2} MB", stats.total_bytes_rx as f64 / 1024.0 / 1024.0);
println!(" └─ Avg Load: {:.1}%", stats.avg_load * 100.0);
println!();
// Node list
let nodes = state.nodes.list().await;
println!("{}", style("🖥️ Nodes").green().bold());
if nodes.is_empty() {
println!(" └─ {}", style("No nodes registered").dim());
} else {
for (i, node) in nodes.iter().enumerate() {
let status_icon = match node.status {
oncp::NodeStatus::Online => style("").green(),
oncp::NodeStatus::Offline => style("").red(),
oncp::NodeStatus::Maintenance => style("").yellow(),
oncp::NodeStatus::Overloaded => style("").magenta(),
};
let prefix = if i == nodes.len() - 1 { "└─" } else { "├─" };
println!(" {} {} {} ({}) - {} conns, {:.0}% load",
prefix,
status_icon,
style(&node.name).bold(),
node.country_code,
node.active_connections,
node.cpu_load * 100.0
);
}
}
println!();
// SNI stats
let sni_stats = state.sni_manager.stats().await;
println!("{}", style("🔗 SNI Configuration").green().bold());
println!(" ├─ Active Domains: {}", sni_stats.total_domains);
println!(" ├─ Blocked Domains: {}", style(sni_stats.blocked_domains).red());
println!(" ├─ Countries: {}", sni_stats.countries);
println!(" └─ Pending Updates: {}", sni_stats.pending_updates);
println!();
// Controls
println!("{}", style("─────────────────────────────────────────────").dim());
println!("Press {} to refresh, {} to quit",
style("Enter").cyan().bold(),
style("Ctrl+C").red().bold());
// Wait for input or timeout
tokio::select! {
_ = tokio::time::sleep(Duration::from_secs(5)) => {}
_ = tokio::signal::ctrl_c() => {
println!("\n{}", style("Goodbye!").green());
return Ok(());
}
}
}
}
async fn handle_node_command(state: Arc<AppState>, action: NodeCommands) -> Result<()> {
match action {
NodeCommands::List => {
let nodes = state.nodes.list().await;
println!("{}", style("Registered Nodes").green().bold());
println!("{}", style("").dim().to_string().repeat(70));
if nodes.is_empty() {
println!("{}", style("No nodes registered").dim());
} else {
println!("{:<36} {:<15} {:<8} {:<6} {:<8}",
style("ID").bold(),
style("Name").bold(),
style("Country").bold(),
style("Status").bold(),
style("Load").bold()
);
for node in nodes {
let status = match node.status {
oncp::NodeStatus::Online => style("ONLINE").green(),
oncp::NodeStatus::Offline => style("OFFLINE").red(),
oncp::NodeStatus::Maintenance => style("MAINT").yellow(),
oncp::NodeStatus::Overloaded => style("OVERLOAD").magenta(),
};
println!("{:<36} {:<15} {:<8} {:<6} {:.1}%",
node.node_id,
node.name,
node.country_code,
status,
node.cpu_load * 100.0
);
}
}
}
NodeCommands::Add { name, address, country } => {
let node = Node::new(&name, &address, &country);
let id = state.nodes.register(node).await;
println!("{} Node registered", style("").green().bold());
println!(" ID: {}", style(id).yellow());
println!(" Name: {}", name);
println!(" Address: {}", address);
println!(" Country: {}", country);
}
NodeCommands::Remove { id } => {
let uuid = uuid::Uuid::parse_str(&id)?;
match state.nodes.remove(&uuid).await {
Some(node) => {
println!("{} Removed node: {}", style("").green().bold(), node.name);
}
None => {
println!("{} Node not found", style("").red().bold());
}
}
}
}
Ok(())
}
async fn handle_user_command(state: Arc<AppState>, action: UserCommands) -> Result<()> {
match action {
UserCommands::List => {
println!("{}", style("Registered Users").green().bold());
println!("{}", style("").dim().to_string().repeat(80));
// Query directly since we need list functionality
let conn = state.users.conn.lock().unwrap();
let mut stmt = conn.prepare(
"SELECT uuid, bandwidth_quota, bandwidth_used, expires_at, active FROM users"
)?;
let users = stmt.query_map([], |row| {
let uuid_str: String = row.get(0)?;
Ok((
uuid_str,
row.get::<_, i64>(1)?,
row.get::<_, i64>(2)?,
row.get::<_, String>(3)?,
row.get::<_, i32>(4)? == 1,
))
})?;
println!("{:<36} {:<10} {:<10} {:<20} {:<8}",
style("UUID").bold(),
style("Quota").bold(),
style("Used").bold(),
style("Expires").bold(),
style("Active").bold()
);
for user in users.flatten() {
let quota_gb = user.1 as f64 / 1024.0 / 1024.0 / 1024.0;
let used_gb = user.2 as f64 / 1024.0 / 1024.0 / 1024.0;
let active = if user.4 { style("Yes").green() } else { style("No").red() };
println!("{:<36} {:.1} GB {:.1} GB {:<20} {}",
user.0,
quota_gb,
used_gb,
&user.3[..19], // Trim datetime
active
);
}
}
UserCommands::Create { quota, days } => {
use oncp::UserRegistry;
let pb = ProgressBar::new_spinner();
pb.set_style(ProgressStyle::default_spinner()
.template("{spinner:.green} {msg}")
.unwrap());
pb.set_message("Creating user...");
let user = User::new(quota, days);
state.users.create_user(&user)?;
pb.finish_with_message("User created!");
println!();
println!("{} User Created", style("").green().bold());
println!();
println!(" {} {}", style("UUID:").bold(), style(&user.uuid).yellow());
println!(" {} {} GB", style("Quota:").bold(), quota);
println!(" {} {} days", style("Valid:").bold(), days);
println!(" {} {}", style("Expires:").bold(), user.expires_at.format("%Y-%m-%d %H:%M UTC"));
println!();
// Generate config string for QR/sharing
let config = serde_json::json!({
"uuid": user.uuid.to_string(),
"expires": user.expires_at.to_rfc3339(),
});
let config_str = base64_encode(config.to_string());
println!(" {} {}", style("Config:").bold(), style(&config_str).dim());
println!();
println!(" Share this config string with the user for client setup.");
}
UserCommands::Config { id } => {
use oncp::UserRegistry;
let uuid = uuid::Uuid::parse_str(&id)?;
match state.users.get_user(&uuid)? {
Some(user) => {
println!("{}", style("User Configuration").green().bold());
println!();
// Get best servers
let servers = state.nodes.best_global(3).await;
let config = serde_json::json!({
"uuid": user.uuid.to_string(),
"servers": servers.iter().map(|s| &s.address).collect::<Vec<_>>(),
"expires": user.expires_at.to_rfc3339(),
});
println!("{}", style("JSON Config:").bold());
println!("{}", serde_json::to_string_pretty(&config)?);
println!();
let encoded = base64_encode(config.to_string());
println!("{} {}", style("Base64:").bold(), encoded);
// QR code hint
println!();
println!("{}", style("Tip: Use 'qrencode' to generate QR code from Base64 string").dim());
}
None => {
println!("{} User not found", style("").red().bold());
}
}
}
UserCommands::Delete { id } => {
let uuid = uuid::Uuid::parse_str(&id)?;
let conn = state.users.conn.lock().unwrap();
let deleted = conn.execute("DELETE FROM users WHERE uuid = ?", [id])?;
if deleted > 0 {
println!("{} User deleted: {}", style("").green().bold(), uuid);
} else {
println!("{} User not found", style("").red().bold());
}
}
}
Ok(())
}
async fn handle_sni_command(state: Arc<AppState>, action: SniCommands) -> Result<()> {
match action {
SniCommands::List => {
let snis = state.sni_manager.get_active_snis().await;
println!("{}", style("Active SNI Domains").green().bold());
println!("{}", style("").dim().to_string().repeat(50));
for sni in snis {
println!("{}", sni);
}
let stats = state.sni_manager.stats().await;
println!();
println!("{} total, {} blocked",
style(stats.total_domains).bold(),
style(stats.blocked_domains).red().bold()
);
}
SniCommands::Block { domain } => {
state.sni_manager.block_domain(domain.clone()).await;
println!("{} Blocked domain: {}", style("").green().bold(), style(&domain).red());
println!(" Emergency update queued for all nodes.");
}
SniCommands::Add { domain, country } => {
use oncp::SniUpdate;
let update = SniUpdate {
remove: vec![],
add: vec![domain.clone()],
country_code: Some(country.clone()),
emergency: false,
};
state.sni_manager.apply_update(update).await;
println!("{} Added domain: {} ({})",
style("").green().bold(),
style(&domain).cyan(),
country
);
}
}
Ok(())
}
async fn show_stats(state: Arc<AppState>) -> Result<()> {
let stats = state.nodes.network_stats().await;
let sni_stats = state.sni_manager.stats().await;
println!("{}", style("═══════════════════════════════════════════════").cyan());
println!("{}", style(" OSTP CDN Network Statistics").cyan().bold());
println!("{}", style("═══════════════════════════════════════════════").cyan());
println!();
println!("{}", style("Nodes").green().bold());
println!(" Total: {}", stats.total_nodes);
println!(" Online: {}", style(stats.online_nodes).green());
println!(" Offline: {}", style(stats.total_nodes - stats.online_nodes).red());
println!();
println!("{}", style("Traffic").green().bold());
println!(" TX: {:.2} MB", stats.total_bytes_tx as f64 / 1024.0 / 1024.0);
println!(" RX: {:.2} MB", stats.total_bytes_rx as f64 / 1024.0 / 1024.0);
println!(" Total: {:.2} MB", (stats.total_bytes_tx + stats.total_bytes_rx) as f64 / 1024.0 / 1024.0);
println!();
println!("{}", style("Connections").green().bold());
println!(" Active: {}", stats.total_connections);
println!(" Avg Load: {:.1}%", stats.avg_load * 100.0);
println!();
println!("{}", style("SNI").green().bold());
println!(" Domains: {}", sni_stats.total_domains);
println!(" Blocked: {}", style(sni_stats.blocked_domains).red());
println!(" Countries: {}", sni_stats.countries);
println!();
Ok(())
}
// Helper for base64 encoding
fn base64_encode(input: impl AsRef<[u8]>) -> String {
use base64::{Engine as _, engine::general_purpose};
general_purpose::STANDARD.encode(input)
}