From 2db44bd7d0f0c361f6831758393bbc1117b1ce1d Mon Sep 17 00:00:00 2001 From: Razvan Dimescu Date: Fri, 20 Mar 2026 11:39:30 +0200 Subject: [PATCH] add system DNS auto-configuration (install/uninstall) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit numa install — saves current DNS, sets all network services to 127.0.0.1 numa uninstall — restores original DNS from ~/.numa/original-dns.json numa help — shows usage macOS: uses networksetup to enumerate services and set/restore DNS. Linux: stubs with instructions for manual setup. Co-Authored-By: Claude Opus 4.6 --- src/main.rs | 38 +++++++++- src/system_dns.rs | 183 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 87c04c3..95d8719 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ use numa::ctx::{handle_query, ServerCtx}; use numa::override_store::OverrideStore; use numa::query_log::QueryLog; use numa::stats::ServerStats; -use numa::system_dns::discover_forwarding_rules; +use numa::system_dns::{discover_forwarding_rules, install_system_dns, uninstall_system_dns}; #[tokio::main] async fn main() -> numa::Result<()> { @@ -21,9 +21,39 @@ async fn main() -> numa::Result<()> { .format_timestamp_millis() .init(); - let config_path = std::env::args() - .nth(1) - .unwrap_or_else(|| "numa.toml".to_string()); + // Handle CLI subcommands + let arg1 = std::env::args().nth(1).unwrap_or_default(); + match arg1.as_str() { + "install" => { + eprintln!("\x1b[1;38;2;192;98;58mNuma\x1b[0m — configuring system DNS\n"); + return install_system_dns().map_err(|e| e.into()); + } + "uninstall" => { + eprintln!("\x1b[1;38;2;192;98;58mNuma\x1b[0m — restoring system DNS\n"); + return uninstall_system_dns().map_err(|e| e.into()); + } + "help" | "--help" | "-h" => { + eprintln!("Usage: numa [command] [config-path]"); + eprintln!(); + eprintln!("Commands:"); + eprintln!(" (none) Start the DNS server (default)"); + eprintln!(" install Set system DNS to 127.0.0.1 (requires sudo)"); + eprintln!(" uninstall Restore original system DNS settings"); + eprintln!(" help Show this help"); + eprintln!(); + eprintln!("Config path defaults to numa.toml"); + return Ok(()); + } + _ => {} + } + + let config_path = if arg1.is_empty() || arg1 == "run" { + std::env::args() + .nth(2) + .unwrap_or_else(|| "numa.toml".to_string()) + } else { + arg1 // treat as config path for backwards compatibility + }; let config = load_config(&config_path)?; let upstream: SocketAddr = diff --git a/src/system_dns.rs b/src/system_dns.rs index 1d356e2..8ca0de4 100644 --- a/src/system_dns.rs +++ b/src/system_dns.rs @@ -1,4 +1,6 @@ +use std::collections::HashMap; use std::net::SocketAddr; +use std::path::PathBuf; use log::info; @@ -145,3 +147,184 @@ pub fn match_forwarding_rule(domain: &str, rules: &[ForwardingRule]) -> Option PathBuf { + dirs_or_home().join(".numa") +} + +fn dirs_or_home() -> PathBuf { + std::env::var("HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from("/tmp")) +} + +fn backup_path() -> PathBuf { + numa_data_dir().join("original-dns.json") +} + +/// Set the system DNS to 127.0.0.1 so all queries go through Numa. +/// Saves the original DNS settings for later restoration. +pub fn install_system_dns() -> Result<(), String> { + #[cfg(target_os = "macos")] + { + install_macos() + } + #[cfg(target_os = "linux")] + { + install_linux() + } + #[cfg(not(any(target_os = "macos", target_os = "linux")))] + { + Err("system DNS configuration not supported on this OS".to_string()) + } +} + +/// Restore the original system DNS settings saved during install. +pub fn uninstall_system_dns() -> Result<(), String> { + #[cfg(target_os = "macos")] + { + uninstall_macos() + } + #[cfg(target_os = "linux")] + { + uninstall_linux() + } + #[cfg(not(any(target_os = "macos", target_os = "linux")))] + { + Err("system DNS configuration not supported on this OS".to_string()) + } +} + +// --- macOS implementation --- + +#[cfg(target_os = "macos")] +fn get_network_services() -> Result, String> { + let output = std::process::Command::new("networksetup") + .arg("-listallnetworkservices") + .output() + .map_err(|e| format!("failed to run networksetup: {}", e))?; + + let text = String::from_utf8_lossy(&output.stdout); + let services: Vec = text + .lines() + .skip(1) // first line is "An asterisk (*) denotes..." + .map(|l| l.trim_start_matches('*').trim().to_string()) + .filter(|l| !l.is_empty()) + .collect(); + + Ok(services) +} + +#[cfg(target_os = "macos")] +fn get_dns_servers(service: &str) -> Result, String> { + let output = std::process::Command::new("networksetup") + .args(["-getdnsservers", service]) + .output() + .map_err(|e| format!("failed to get DNS for {}: {}", service, e))?; + + let text = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if text.contains("aren't any DNS Servers") { + Ok(vec![]) // using DHCP defaults + } else { + Ok(text.lines().map(|l| l.trim().to_string()).collect()) + } +} + +#[cfg(target_os = "macos")] +fn install_macos() -> Result<(), String> { + let services = get_network_services()?; + let mut original: HashMap> = HashMap::new(); + + // Save current DNS for each service + for service in &services { + let servers = get_dns_servers(service)?; + original.insert(service.clone(), servers); + } + + // Save backup + let dir = numa_data_dir(); + std::fs::create_dir_all(&dir) + .map_err(|e| format!("failed to create {}: {}", dir.display(), e))?; + + let json = serde_json::to_string_pretty(&original) + .map_err(|e| format!("failed to serialize backup: {}", e))?; + std::fs::write(backup_path(), json).map_err(|e| format!("failed to write backup: {}", e))?; + + // Set DNS to 127.0.0.1 for each service + for service in &services { + let status = std::process::Command::new("networksetup") + .args(["-setdnsservers", service, "127.0.0.1"]) + .status() + .map_err(|e| format!("failed to set DNS for {}: {}", service, e))?; + + if status.success() { + eprintln!(" set DNS for \"{}\" -> 127.0.0.1", service); + } else { + eprintln!(" warning: failed to set DNS for \"{}\"", service); + } + } + + eprintln!("\n Original DNS saved to {}", backup_path().display()); + eprintln!(" Run 'sudo numa uninstall' to restore.\n"); + + Ok(()) +} + +#[cfg(target_os = "macos")] +fn uninstall_macos() -> Result<(), String> { + let path = backup_path(); + let json = std::fs::read_to_string(&path) + .map_err(|e| format!("no backup found at {}: {}", path.display(), e))?; + + let original: HashMap> = + serde_json::from_str(&json).map_err(|e| format!("invalid backup file: {}", e))?; + + for (service, servers) in &original { + let args = if servers.is_empty() { + // Restore to "empty" (DHCP default) by setting to "Empty" + vec!["-setdnsservers", service, "Empty"] + } else { + let mut a = vec!["-setdnsservers", service]; + a.extend(servers.iter().map(|s| s.as_str())); + a + }; + + let status = std::process::Command::new("networksetup") + .args(&args) + .status() + .map_err(|e| format!("failed to restore DNS for {}: {}", service, e))?; + + if status.success() { + let display = if servers.is_empty() { + "DHCP default".to_string() + } else { + servers.join(", ") + }; + eprintln!(" restored DNS for \"{}\" -> {}", service, display); + } else { + eprintln!(" warning: failed to restore DNS for \"{}\"", service); + } + } + + std::fs::remove_file(&path).ok(); + eprintln!("\n System DNS restored. Backup removed.\n"); + + Ok(()) +} + +// --- Linux stubs --- + +#[cfg(target_os = "linux")] +fn install_linux() -> Result<(), String> { + Err( + "Linux auto-configuration not yet implemented. Manually set your DNS to 127.0.0.1" + .to_string(), + ) +} + +#[cfg(target_os = "linux")] +fn uninstall_linux() -> Result<(), String> { + Err("Linux auto-configuration not yet implemented.".to_string()) +}