feat: System DNS, Node Enrollment, and CDN Steering

- osds: Added system DNS forwarder on 127.0.0.1:53
  - SystemDnsManager for Windows/Linux DNS configuration
  - Auto-restore original DNS on exit
  - *.ospab.internal routing to master node
  - Encrypted DNS forwarding through OSTP tunnel

- oncp: Implemented node enrollment system
  - EnrollmentRegistry with state machine (Pending->Approved->Active)
  - SQLite-backed enrollment storage
  - Node PSK generation on approval
  - REST API endpoints for enrollment workflow

- oncp-master: Added enrollment CLI commands
  - 'node pending' - List pending enrollment requests
  - 'node approve <id>' - Approve and generate PSK
  - 'node reject <id>' - Reject enrollment

- ostp-server: Auto-registration on startup
  - Submits enrollment request to master node
  - Exits if PSK='AUTO' and awaits approval
  - Integrates with ONCP enrollment API

- oncp API: Enhanced CDN steering
  - Best nodes by country_code with fallback
  - Steering metadata (matched, fallback status)
  - Load-based node selection
This commit is contained in:
2026-01-01 23:45:24 +03:00
parent 7e1c87e70b
commit 5879344336
11 changed files with 1449 additions and 49 deletions

View File

@@ -87,6 +87,18 @@ enum NodeCommands {
/// Node ID
id: String,
},
/// List pending enrollment requests
Pending,
/// Approve enrollment request
Approve {
/// Node ID to approve
id: String,
},
/// Reject enrollment request
Reject {
/// Node ID to reject
id: String,
},
}
#[derive(Subcommand)]
@@ -328,6 +340,71 @@ async fn handle_node_command(state: Arc<AppState>, action: NodeCommands) -> Resu
}
}
}
NodeCommands::Pending => {
use oncp::EnrollmentState;
let pending = state.enrollment.list_by_state(EnrollmentState::Pending)?;
println!("{}", style("Pending Enrollment Requests").green().bold());
println!("{}", style("").dim().to_string().repeat(80));
if pending.is_empty() {
println!("{}", style("No pending requests").dim());
} else {
println!("{:<36} {:<15} {:<20} {:<8}",
style("Node ID").bold(),
style("Name").bold(),
style("Address").bold(),
style("Country").bold()
);
for node in pending {
println!("{:<36} {:<15} {:<20} {:<8}",
node.node_id,
node.name,
node.address,
node.country_code
);
}
println!();
println!("{}", style("Use 'node approve <id>' to approve").dim());
}
}
NodeCommands::Approve { id } => {
let uuid = uuid::Uuid::parse_str(&id)?;
match state.enrollment.approve(&uuid) {
Ok(node_psk) => {
println!("{} Node approved", style("").green().bold());
println!();
println!(" Node ID: {}", style(uuid).yellow());
println!(" Node PSK: {}", style(&node_psk).yellow().bold());
println!();
println!("{}", style("⚠ IMPORTANT: Save this PSK securely!").red().bold());
println!("{}", style(" Send it to the node operator via secure channel").dim());
println!("{}", style(" The node must use this PSK to connect").dim());
}
Err(e) => {
println!("{} Failed to approve: {}", style("").red().bold(), e);
}
}
}
NodeCommands::Reject { id } => {
let uuid = uuid::Uuid::parse_str(&id)?;
match state.enrollment.reject(&uuid) {
Ok(()) => {
println!("{} Node enrollment rejected", style("").green().bold());
}
Err(e) => {
println!("{} Failed to reject: {}", style("").red().bold(), e);
}
}
}
}
Ok(())