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:
@@ -62,6 +62,8 @@ pub struct StealthDnsForwarder {
|
||||
listen_addr: SocketAddr,
|
||||
/// Upstream resolver (will be tunneled through OSTP)
|
||||
upstream: SocketAddr,
|
||||
/// Master node internal IP for *.ospab.internal queries
|
||||
master_node_ip: std::net::IpAddr,
|
||||
}
|
||||
|
||||
impl StealthDnsForwarder {
|
||||
@@ -69,9 +71,15 @@ impl StealthDnsForwarder {
|
||||
Self {
|
||||
listen_addr: listen,
|
||||
upstream,
|
||||
master_node_ip: std::net::IpAddr::V4(std::net::Ipv4Addr::new(10, 8, 0, 1)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_master_ip(mut self, master_ip: std::net::IpAddr) -> Self {
|
||||
self.master_node_ip = master_ip;
|
||||
self
|
||||
}
|
||||
|
||||
/// Start DNS listener (intercepts local queries)
|
||||
pub async fn run(&self) -> Result<(), DnsError> {
|
||||
let socket = UdpSocket::bind(self.listen_addr).await?;
|
||||
@@ -82,6 +90,12 @@ impl StealthDnsForwarder {
|
||||
let (len, src) = socket.recv_from(&mut buf).await?;
|
||||
let query = &buf[..len];
|
||||
|
||||
// Check if query is for *.ospab.internal
|
||||
if let Some(response) = self.handle_internal_query(query) {
|
||||
let _ = socket.send_to(&response, src).await;
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Encrypt and forward through OSTP tunnel instead of direct
|
||||
// For now, direct forward (to be replaced with tunnel)
|
||||
match self.forward_query(query).await {
|
||||
@@ -95,6 +109,87 @@ impl StealthDnsForwarder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle *.ospab.internal queries - resolve to master node IP
|
||||
fn handle_internal_query(&self, query: &[u8]) -> Option<Vec<u8>> {
|
||||
if query.len() < 12 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Parse domain from query (simplified)
|
||||
let qname_start = 12;
|
||||
let mut pos = qname_start;
|
||||
let mut domain = String::new();
|
||||
|
||||
while pos < query.len() && query[pos] != 0 {
|
||||
let len = query[pos] as usize;
|
||||
if len == 0 {
|
||||
break;
|
||||
}
|
||||
if !domain.is_empty() {
|
||||
domain.push('.');
|
||||
}
|
||||
pos += 1;
|
||||
if pos + len > query.len() {
|
||||
return None;
|
||||
}
|
||||
domain.push_str(&String::from_utf8_lossy(&query[pos..pos + len]));
|
||||
pos += len;
|
||||
}
|
||||
|
||||
// Check if domain ends with .ospab.internal
|
||||
if !domain.ends_with(".ospab.internal") {
|
||||
return None;
|
||||
}
|
||||
|
||||
tracing::debug!("Resolving internal domain: {}", domain);
|
||||
|
||||
// Build DNS response
|
||||
let mut response = Vec::from(query);
|
||||
|
||||
// Set response flags (QR=1, AA=1, RCODE=0)
|
||||
response[2] = 0x84; // QR + AA
|
||||
response[3] = 0x00; // No error
|
||||
|
||||
// Set answer count to 1
|
||||
response[6] = 0x00;
|
||||
response[7] = 0x01;
|
||||
|
||||
// Add answer section (pointer to question + A record)
|
||||
response.push(0xC0); // Pointer to question name
|
||||
response.push(0x0C); // Offset 12
|
||||
|
||||
// Type A
|
||||
response.push(0x00);
|
||||
response.push(0x01);
|
||||
|
||||
// Class IN
|
||||
response.push(0x00);
|
||||
response.push(0x01);
|
||||
|
||||
// TTL (300 seconds)
|
||||
response.push(0x00);
|
||||
response.push(0x00);
|
||||
response.push(0x01);
|
||||
response.push(0x2C);
|
||||
|
||||
// Data length (4 bytes for IPv4)
|
||||
response.push(0x00);
|
||||
response.push(0x04);
|
||||
|
||||
// IP address
|
||||
match self.master_node_ip {
|
||||
std::net::IpAddr::V4(ip) => {
|
||||
response.extend_from_slice(&ip.octets());
|
||||
}
|
||||
std::net::IpAddr::V6(_) => {
|
||||
// For IPv6, would need AAAA record - fallback to IPv4 loopback
|
||||
response.extend_from_slice(&[127, 0, 0, 1]);
|
||||
}
|
||||
}
|
||||
|
||||
Some(response)
|
||||
}
|
||||
|
||||
async fn forward_query(&self, query: &[u8]) -> Result<Vec<u8>, DnsError> {
|
||||
let socket = UdpSocket::bind("0.0.0.0:0").await?;
|
||||
socket.send_to(query, self.upstream).await?;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod dns;
|
||||
pub mod system;
|
||||
|
||||
pub use dns::{build_dns_query, detect_hijack, DnsError, QueryType, StealthDnsForwarder};
|
||||
pub use system::{DnsBackup, SystemDnsError, SystemDnsManager};
|
||||
|
||||
260
osds/src/system.rs
Normal file
260
osds/src/system.rs
Normal file
@@ -0,0 +1,260 @@
|
||||
//! System DNS configuration management for Windows/Linux
|
||||
|
||||
use std::process::Command;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SystemDnsError {
|
||||
#[error("io error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("command failed: {0}")]
|
||||
CommandFailed(String),
|
||||
#[error("unsupported platform")]
|
||||
UnsupportedPlatform,
|
||||
#[error("permission denied")]
|
||||
PermissionDenied,
|
||||
}
|
||||
|
||||
/// Stores original DNS settings for restoration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DnsBackup {
|
||||
#[cfg(target_os = "windows")]
|
||||
pub interface: String,
|
||||
#[cfg(target_os = "windows")]
|
||||
pub original_dns: Vec<String>,
|
||||
#[cfg(target_os = "linux")]
|
||||
pub resolv_conf_backup: String,
|
||||
}
|
||||
|
||||
/// System DNS manager - sets DNS to 127.0.0.1 and restores on drop
|
||||
pub struct SystemDnsManager {
|
||||
backup: Option<DnsBackup>,
|
||||
}
|
||||
|
||||
impl SystemDnsManager {
|
||||
pub fn new() -> Self {
|
||||
Self { backup: None }
|
||||
}
|
||||
|
||||
/// Set system DNS to 127.0.0.1 (requires admin/root)
|
||||
pub async fn set_local_dns(&mut self) -> Result<(), SystemDnsError> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
self.set_windows_dns().await
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
self.set_linux_dns().await
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
|
||||
{
|
||||
Err(SystemDnsError::UnsupportedPlatform)
|
||||
}
|
||||
}
|
||||
|
||||
/// Restore original DNS settings
|
||||
pub async fn restore(&self) -> Result<(), SystemDnsError> {
|
||||
if self.backup.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
self.restore_windows_dns().await
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
self.restore_linux_dns().await
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
|
||||
{
|
||||
Err(SystemDnsError::UnsupportedPlatform)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn set_windows_dns(&mut self) -> Result<(), SystemDnsError> {
|
||||
// Get active network interface
|
||||
let output = Command::new("netsh")
|
||||
.args(&["interface", "show", "interface"])
|
||||
.output()?;
|
||||
|
||||
let interfaces = String::from_utf8_lossy(&output.stdout);
|
||||
let active_interface = interfaces
|
||||
.lines()
|
||||
.find(|line| line.contains("Connected") && !line.contains("Disconnected"))
|
||||
.and_then(|line| line.split_whitespace().last())
|
||||
.ok_or_else(|| SystemDnsError::CommandFailed("No active interface found".into()))?;
|
||||
|
||||
// Backup current DNS settings
|
||||
let output = Command::new("netsh")
|
||||
.args(&[
|
||||
"interface",
|
||||
"ipv4",
|
||||
"show",
|
||||
"dnsservers",
|
||||
active_interface,
|
||||
])
|
||||
.output()?;
|
||||
|
||||
let dns_output = String::from_utf8_lossy(&output.stdout);
|
||||
let original_dns: Vec<String> = dns_output
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
if line.trim().starts_with(|c: char| c.is_numeric()) {
|
||||
Some(line.trim().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.backup = Some(DnsBackup {
|
||||
interface: active_interface.to_string(),
|
||||
original_dns,
|
||||
});
|
||||
|
||||
// Set DNS to 127.0.0.1
|
||||
let status = Command::new("netsh")
|
||||
.args(&[
|
||||
"interface",
|
||||
"ipv4",
|
||||
"set",
|
||||
"dnsservers",
|
||||
active_interface,
|
||||
"static",
|
||||
"127.0.0.1",
|
||||
"primary",
|
||||
])
|
||||
.status()?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(SystemDnsError::CommandFailed(
|
||||
"Failed to set DNS".into(),
|
||||
));
|
||||
}
|
||||
|
||||
tracing::info!("System DNS set to 127.0.0.1 on interface {}", active_interface);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn restore_windows_dns(&self) -> Result<(), SystemDnsError> {
|
||||
let backup = self.backup.as_ref().unwrap();
|
||||
|
||||
if backup.original_dns.is_empty() {
|
||||
// Set to DHCP
|
||||
let status = Command::new("netsh")
|
||||
.args(&[
|
||||
"interface",
|
||||
"ipv4",
|
||||
"set",
|
||||
"dnsservers",
|
||||
&backup.interface,
|
||||
"dhcp",
|
||||
])
|
||||
.status()?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(SystemDnsError::CommandFailed(
|
||||
"Failed to restore DNS to DHCP".into(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
// Restore first DNS as primary
|
||||
let status = Command::new("netsh")
|
||||
.args(&[
|
||||
"interface",
|
||||
"ipv4",
|
||||
"set",
|
||||
"dnsservers",
|
||||
&backup.interface,
|
||||
"static",
|
||||
&backup.original_dns[0],
|
||||
"primary",
|
||||
])
|
||||
.status()?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(SystemDnsError::CommandFailed(
|
||||
"Failed to restore primary DNS".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// Add additional DNS servers
|
||||
for dns in backup.original_dns.iter().skip(1) {
|
||||
let _ = Command::new("netsh")
|
||||
.args(&[
|
||||
"interface",
|
||||
"ipv4",
|
||||
"add",
|
||||
"dnsservers",
|
||||
&backup.interface,
|
||||
dns,
|
||||
])
|
||||
.status();
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("System DNS restored on interface {}", backup.interface);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn set_linux_dns(&mut self) -> Result<(), SystemDnsError> {
|
||||
use tokio::fs;
|
||||
|
||||
// Backup /etc/resolv.conf
|
||||
let resolv_conf = fs::read_to_string("/etc/resolv.conf").await?;
|
||||
self.backup = Some(DnsBackup {
|
||||
resolv_conf_backup: resolv_conf,
|
||||
});
|
||||
|
||||
// Write new resolv.conf
|
||||
let new_conf = "# Managed by OSTP\nnameserver 127.0.0.1\n";
|
||||
fs::write("/etc/resolv.conf", new_conf).await?;
|
||||
|
||||
tracing::info!("System DNS set to 127.0.0.1 in /etc/resolv.conf");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn restore_linux_dns(&self) -> Result<(), SystemDnsError> {
|
||||
use tokio::fs;
|
||||
|
||||
let backup = self.backup.as_ref().unwrap();
|
||||
fs::write("/etc/resolv.conf", &backup.resolv_conf_backup).await?;
|
||||
|
||||
tracing::info!("System DNS restored from backup");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SystemDnsManager {
|
||||
fn drop(&mut self) {
|
||||
if self.backup.is_some() {
|
||||
// Best effort restore - use blocking runtime
|
||||
let rt = tokio::runtime::Handle::try_current();
|
||||
if let Ok(handle) = rt {
|
||||
let _ = handle.block_on(self.restore());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Requires admin/root
|
||||
async fn test_set_and_restore_dns() {
|
||||
let mut manager = SystemDnsManager::new();
|
||||
manager.set_local_dns().await.unwrap();
|
||||
manager.restore().await.unwrap();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user