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

@@ -21,3 +21,4 @@ hex.workspace = true
serde.workspace = true
serde_json.workspace = true
rand.workspace = true
reqwest = { version = "0.11", features = ["json"] }

View File

@@ -55,6 +55,13 @@ struct ConfigFile {
psk: String,
max_connections: Option<usize>,
log_level: Option<String>,
// Node enrollment settings
master_node_url: Option<String>,
node_name: Option<String>,
hardware_id: Option<String>,
region: Option<String>,
country_code: Option<String>,
}
fn setup_logging(level: &str) {
@@ -120,7 +127,7 @@ async fn main() -> Result<()> {
}
// Load config from file if specified
let (listen, psk, log_level) = if let Some(config_path) = cli.config {
let (listen, psk, log_level, config_path_opt) = if let Some(config_path) = cli.config.clone() {
let content = std::fs::read_to_string(&config_path)
.with_context(|| format!("Failed to read config file: {:?}", config_path))?;
let config: ConfigFile = serde_json::from_str(&content)
@@ -131,7 +138,7 @@ async fn main() -> Result<()> {
let psk = parse_psk(&config.psk)?;
let level = config.log_level.unwrap_or_else(|| cli.log_level.clone());
(addr, psk, level)
(addr, psk, level, Some(config_path))
} else {
// Use CLI args
let psk_str = cli.psk.ok_or_else(|| {
@@ -139,7 +146,7 @@ async fn main() -> Result<()> {
})?;
let psk = parse_psk(&psk_str)?;
(cli.listen, psk, cli.log_level)
(cli.listen, psk, cli.log_level, None)
};
setup_logging(&log_level);
@@ -153,6 +160,47 @@ async fn main() -> Result<()> {
tracing::info!(" PSK: {}...{}", &hex::encode(&psk[..4]), &hex::encode(&psk[28..]));
tracing::info!("");
// Check if node enrollment is configured
if let Some(config_path) = config_path_opt {
let content = std::fs::read_to_string(&config_path)?;
let config: ConfigFile = serde_json::from_str(&content)?;
if let Some(master_url) = config.master_node_url {
if config.psk == "AUTO" {
// Node needs to enroll - request PSK from master
tracing::info!("Node not enrolled - requesting to join network...");
let enrollment_result = request_enrollment(
&master_url,
config.node_name.as_deref().unwrap_or("ostp-node"),
&listen.to_string(),
config.country_code.as_deref().unwrap_or("US"),
config.hardware_id.as_deref().unwrap_or("unknown"),
config.region.as_deref().unwrap_or("default"),
).await;
match enrollment_result {
Ok(node_id) => {
tracing::info!("✓ Enrollment request submitted");
tracing::info!(" Node ID: {}", node_id);
tracing::info!("");
tracing::info!("Waiting for master node approval...");
tracing::info!("Contact the administrator to approve node: {}", node_id);
tracing::info!("Once approved, update config with provided PSK");
// Exit - node must wait for approval
return Ok(());
}
Err(e) => {
tracing::error!("Failed to submit enrollment: {}", e);
tracing::error!("Server will not start until enrolled");
return Err(e);
}
}
}
}
}
let config = ServerConfig::new(listen, psk);
let server = OstpServer::new(config);
@@ -161,3 +209,59 @@ async fn main() -> Result<()> {
Ok(())
}
/// Submit enrollment request to master node
async fn request_enrollment(
master_url: &str,
name: &str,
address: &str,
country_code: &str,
hardware_id: &str,
region: &str,
) -> Result<String> {
#[derive(serde::Serialize)]
struct EnrollmentRequest {
name: String,
address: String,
country_code: String,
hardware_id: String,
region: String,
}
#[derive(serde::Deserialize)]
#[allow(dead_code)]
struct EnrollmentResponse {
node_id: String,
state: String,
message: String,
}
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()?;
let request = EnrollmentRequest {
name: name.to_string(),
address: address.to_string(),
country_code: country_code.to_string(),
hardware_id: hardware_id.to_string(),
region: region.to_string(),
};
let url = format!("{}/api/v1/enrollment/request", master_url);
let response = client
.post(&url)
.json(&request)
.send()
.await
.context("Failed to connect to master node")?;
if !response.status().is_success() {
let status = response.status();
let text = response.text().await.unwrap_or_default();
anyhow::bail!("Enrollment request failed: {} - {}", status, text);
}
let enrollment: EnrollmentResponse = response.json().await?;
Ok(enrollment.node_id)
}