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:
25
oncp-master/Cargo.toml
Normal file
25
oncp-master/Cargo.toml
Normal 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
556
oncp-master/src/main.rs
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user