From 4645df50e0c4cb201576390a2b979137b4932f11 Mon Sep 17 00:00:00 2001 From: Razvan Dimescu Date: Fri, 20 Mar 2026 12:24:03 +0200 Subject: [PATCH] add Linux systemd service and DNS configuration Linux: - numa install: backs up /etc/resolv.conf, sets nameserver to 127.0.0.1 - numa uninstall: restores original /etc/resolv.conf from backup - numa service start: installs systemd unit, enables + starts - numa service stop: stops, disables, removes unit file - numa service status: shows systemctl status macOS: launchd plist (already working) Both platforms: Restart=always / KeepAlive=true for crash recovery. Co-Authored-By: Claude Opus 4.6 --- com.numa.dns.plist | 20 ++++ numa.service | 16 +++ src/system_dns.rs | 248 +++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 278 insertions(+), 6 deletions(-) create mode 100644 com.numa.dns.plist create mode 100644 numa.service diff --git a/com.numa.dns.plist b/com.numa.dns.plist new file mode 100644 index 0000000..4ef561c --- /dev/null +++ b/com.numa.dns.plist @@ -0,0 +1,20 @@ + + + + + Label + com.numa.dns + ProgramArguments + + /usr/local/bin/numa + + RunAtLoad + + KeepAlive + + StandardOutPath + /usr/local/var/log/numa.log + StandardErrorPath + /usr/local/var/log/numa.log + + diff --git a/numa.service b/numa.service new file mode 100644 index 0000000..50b0909 --- /dev/null +++ b/numa.service @@ -0,0 +1,16 @@ +[Unit] +Description=Numa DNS — DNS you own, everywhere you go +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +ExecStart=/usr/local/bin/numa +Restart=always +RestartSec=2 +StandardOutput=journal +StandardError=journal +SyslogIdentifier=numa + +[Install] +WantedBy=multi-user.target diff --git a/src/system_dns.rs b/src/system_dns.rs index 3e9d4e6..564dca4 100644 --- a/src/system_dns.rs +++ b/src/system_dns.rs @@ -315,17 +315,253 @@ fn uninstall_macos() -> Result<(), String> { Ok(()) } -// --- Linux stubs --- +// --- Service management --- + +#[cfg(target_os = "macos")] +const PLIST_LABEL: &str = "com.numa.dns"; +#[cfg(target_os = "macos")] +const PLIST_DEST: &str = "/Library/LaunchDaemons/com.numa.dns.plist"; +#[cfg(target_os = "linux")] +const SYSTEMD_UNIT: &str = "/etc/systemd/system/numa.service"; + +/// Install Numa as a system service that starts on boot and auto-restarts. +pub fn install_service() -> Result<(), String> { + #[cfg(target_os = "macos")] + { + install_service_macos() + } + #[cfg(target_os = "linux")] + { + install_service_linux() + } + #[cfg(not(any(target_os = "macos", target_os = "linux")))] + { + Err("service installation not supported on this OS".to_string()) + } +} + +/// Uninstall the Numa system service. +pub fn uninstall_service() -> Result<(), String> { + #[cfg(target_os = "macos")] + { + uninstall_service_macos() + } + #[cfg(target_os = "linux")] + { + uninstall_service_linux() + } + #[cfg(not(any(target_os = "macos", target_os = "linux")))] + { + Err("service uninstallation not supported on this OS".to_string()) + } +} + +/// Show the service status. +pub fn service_status() -> Result<(), String> { + #[cfg(target_os = "macos")] + { + service_status_macos() + } + #[cfg(target_os = "linux")] + { + service_status_linux() + } + #[cfg(not(any(target_os = "macos", target_os = "linux")))] + { + Err("service status not supported on this OS".to_string()) + } +} + +#[cfg(target_os = "macos")] +fn install_service_macos() -> Result<(), String> { + // Check binary exists + if !std::path::Path::new("/usr/local/bin/numa").exists() { + return Err("numa binary not found at /usr/local/bin/numa. Run: sudo cp target/release/numa /usr/local/bin/numa".to_string()); + } + + // Create log directory + std::fs::create_dir_all("/usr/local/var/log") + .map_err(|e| format!("failed to create log dir: {}", e))?; + + // Write plist + let plist = include_str!("../com.numa.dns.plist"); + std::fs::write(PLIST_DEST, plist) + .map_err(|e| format!("failed to write {}: {}", PLIST_DEST, e))?; + + // Load the service + let status = std::process::Command::new("launchctl") + .args(["load", "-w", PLIST_DEST]) + .status() + .map_err(|e| format!("failed to run launchctl: {}", e))?; + + if status.success() { + eprintln!(" Service installed and started."); + eprintln!(" Numa will auto-start on boot and restart if killed."); + eprintln!(" Logs: /usr/local/var/log/numa.log"); + eprintln!(" Run 'sudo numa service stop' to uninstall.\n"); + Ok(()) + } else { + Err("launchctl load failed".to_string()) + } +} + +#[cfg(target_os = "macos")] +fn uninstall_service_macos() -> Result<(), String> { + // Unload the service + let _ = std::process::Command::new("launchctl") + .args(["unload", "-w", PLIST_DEST]) + .status(); + + // Remove plist + if std::path::Path::new(PLIST_DEST).exists() { + std::fs::remove_file(PLIST_DEST) + .map_err(|e| format!("failed to remove {}: {}", PLIST_DEST, e))?; + } + + eprintln!(" Service uninstalled. Numa will no longer auto-start.\n"); + Ok(()) +} + +#[cfg(target_os = "macos")] +fn service_status_macos() -> Result<(), String> { + let output = std::process::Command::new("launchctl") + .args(["list", PLIST_LABEL]) + .output() + .map_err(|e| format!("failed to run launchctl: {}", e))?; + + if output.status.success() { + let text = String::from_utf8_lossy(&output.stdout); + eprintln!(" Numa service is loaded.\n"); + for line in text.lines() { + eprintln!(" {}", line); + } + eprintln!(); + } else { + eprintln!(" Numa service is not installed.\n"); + } + Ok(()) +} + +// --- Linux implementation --- + +#[cfg(target_os = "linux")] +fn backup_path_linux() -> std::path::PathBuf { + let home = std::env::var("HOME") + .map(std::path::PathBuf::from) + .unwrap_or_else(|_| std::path::PathBuf::from("/root")); + let dir = home.join(".numa"); + let _ = std::fs::create_dir_all(&dir); + dir.join("original-resolv.conf") +} #[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(), - ) + let backup = backup_path_linux(); + let resolv = std::path::Path::new("/etc/resolv.conf"); + + // Save current resolv.conf + if resolv.exists() { + std::fs::copy(resolv, &backup) + .map_err(|e| format!("failed to backup /etc/resolv.conf: {}", e))?; + eprintln!(" Saved /etc/resolv.conf to {}", backup.display()); + } + + // Write new resolv.conf pointing to Numa + let content = + "# Generated by Numa — run 'sudo numa uninstall' to restore\nnameserver 127.0.0.1\n"; + std::fs::write(resolv, content) + .map_err(|e| format!("failed to write /etc/resolv.conf: {}", e))?; + + eprintln!(" Set /etc/resolv.conf -> nameserver 127.0.0.1"); + eprintln!(" Run 'sudo numa uninstall' to restore.\n"); + Ok(()) } #[cfg(target_os = "linux")] fn uninstall_linux() -> Result<(), String> { - Err("Linux auto-configuration not yet implemented.".to_string()) + let backup = backup_path_linux(); + let resolv = std::path::Path::new("/etc/resolv.conf"); + + if backup.exists() { + std::fs::copy(&backup, resolv) + .map_err(|e| format!("failed to restore /etc/resolv.conf: {}", e))?; + std::fs::remove_file(&backup).ok(); + eprintln!(" Restored /etc/resolv.conf from backup. Backup removed.\n"); + } else { + eprintln!(" No backup found at {}.", backup.display()); + eprintln!(" Manually edit /etc/resolv.conf to restore your DNS.\n"); + } + Ok(()) +} + +#[cfg(target_os = "linux")] +fn install_service_linux() -> Result<(), String> { + if !std::path::Path::new("/usr/local/bin/numa").exists() { + return Err("numa binary not found at /usr/local/bin/numa. Run: sudo cp target/release/numa /usr/local/bin/numa".to_string()); + } + + let unit = include_str!("../numa.service"); + std::fs::write(SYSTEMD_UNIT, unit) + .map_err(|e| format!("failed to write {}: {}", SYSTEMD_UNIT, e))?; + + run_systemctl(&["daemon-reload"])?; + run_systemctl(&["enable", "numa"])?; + run_systemctl(&["start", "numa"])?; + + eprintln!(" Service installed and started."); + eprintln!(" Numa will auto-start on boot and restart if killed."); + eprintln!(" Logs: journalctl -u numa -f"); + eprintln!(" Run 'sudo numa service stop' to uninstall.\n"); + Ok(()) +} + +#[cfg(target_os = "linux")] +fn uninstall_service_linux() -> Result<(), String> { + let _ = run_systemctl(&["stop", "numa"]); + let _ = run_systemctl(&["disable", "numa"]); + + if std::path::Path::new(SYSTEMD_UNIT).exists() { + std::fs::remove_file(SYSTEMD_UNIT) + .map_err(|e| format!("failed to remove {}: {}", SYSTEMD_UNIT, e))?; + } + let _ = run_systemctl(&["daemon-reload"]); + + eprintln!(" Service uninstalled. Numa will no longer auto-start.\n"); + Ok(()) +} + +#[cfg(target_os = "linux")] +fn service_status_linux() -> Result<(), String> { + let output = std::process::Command::new("systemctl") + .args(["status", "numa"]) + .output() + .map_err(|e| format!("failed to run systemctl: {}", e))?; + + let text = String::from_utf8_lossy(&output.stdout); + if text.is_empty() { + eprintln!(" Numa service is not installed.\n"); + } else { + for line in text.lines() { + eprintln!(" {}", line); + } + eprintln!(); + } + Ok(()) +} + +#[cfg(target_os = "linux")] +fn run_systemctl(args: &[&str]) -> Result<(), String> { + let status = std::process::Command::new("systemctl") + .args(args) + .status() + .map_err(|e| format!("systemctl {} failed: {}", args.join(" "), e))?; + if status.success() { + Ok(()) + } else { + Err(format!( + "systemctl {} exited with {}", + args.join(" "), + status + )) + } }