add system DNS auto-configuration (install/uninstall)
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 <noreply@anthropic.com>
This commit is contained in:
38
src/main.rs
38
src/main.rs
@@ -13,7 +13,7 @@ use numa::ctx::{handle_query, ServerCtx};
|
|||||||
use numa::override_store::OverrideStore;
|
use numa::override_store::OverrideStore;
|
||||||
use numa::query_log::QueryLog;
|
use numa::query_log::QueryLog;
|
||||||
use numa::stats::ServerStats;
|
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]
|
#[tokio::main]
|
||||||
async fn main() -> numa::Result<()> {
|
async fn main() -> numa::Result<()> {
|
||||||
@@ -21,9 +21,39 @@ async fn main() -> numa::Result<()> {
|
|||||||
.format_timestamp_millis()
|
.format_timestamp_millis()
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
let config_path = std::env::args()
|
// Handle CLI subcommands
|
||||||
.nth(1)
|
let arg1 = std::env::args().nth(1).unwrap_or_default();
|
||||||
.unwrap_or_else(|| "numa.toml".to_string());
|
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 config = load_config(&config_path)?;
|
||||||
|
|
||||||
let upstream: SocketAddr =
|
let upstream: SocketAddr =
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
@@ -145,3 +147,184 @@ pub fn match_forwarding_rule(domain: &str, rules: &[ForwardingRule]) -> Option<S
|
|||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- System DNS configuration (install/uninstall) ---
|
||||||
|
|
||||||
|
fn numa_data_dir() -> 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<Vec<String>, 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<String> = 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<Vec<String>, 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<String, Vec<String>> = 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<String, Vec<String>> =
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user