harden Linux DNS config and fix review findings

- Detect systemd-resolved: use drop-in config instead of overwriting
  /etc/resolv.conf (which gets regenerated)
- Warn if /etc/resolv.conf is a symlink (NetworkManager, etc.)
- Fix TOCTOU: attempt copy/remove directly, handle NotFound
- Remove side-effect from backup_path_linux (no eager mkdir)
- Fix macOS $HOME fallback: /var/root instead of /tmp
- Log warnings on launchctl/systemctl failures instead of silencing
- Delete plist before unloading (prevents zombie restarts)
- Extract ensure_binary_installed helper on Linux

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Razvan Dimescu
2026-03-20 12:32:20 +02:00
parent 4645df50e0
commit 57c4742f09

View File

@@ -186,8 +186,9 @@ pub fn uninstall_system_dns() -> Result<(), String> {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
fn numa_data_dir() -> std::path::PathBuf { fn numa_data_dir() -> std::path::PathBuf {
let home = std::env::var("HOME") let home = std::env::var("HOME")
.or_else(|_| std::env::var("SUDO_USER").map(|u| format!("/Users/{}", u)))
.map(std::path::PathBuf::from) .map(std::path::PathBuf::from)
.unwrap_or_else(|_| std::path::PathBuf::from("/tmp")); .unwrap_or_else(|_| std::path::PathBuf::from("/var/root"));
home.join(".numa") home.join(".numa")
} }
@@ -407,15 +408,23 @@ fn install_service_macos() -> Result<(), String> {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
fn uninstall_service_macos() -> Result<(), String> { fn uninstall_service_macos() -> Result<(), String> {
// Remove plist first so service won't restart on boot even if unload fails
if let Err(e) = std::fs::remove_file(PLIST_DEST) {
if e.kind() != std::io::ErrorKind::NotFound {
return Err(format!("failed to remove {}: {}", PLIST_DEST, e));
}
}
// Unload the service // Unload the service
let _ = std::process::Command::new("launchctl") let status = std::process::Command::new("launchctl")
.args(["unload", "-w", PLIST_DEST]) .args(["unload", "-w", PLIST_DEST])
.status(); .status();
if let Ok(s) = status {
// Remove plist if !s.success() {
if std::path::Path::new(PLIST_DEST).exists() { eprintln!(
std::fs::remove_file(PLIST_DEST) " warning: launchctl unload returned non-zero (service may still be running)"
.map_err(|e| format!("failed to remove {}: {}", PLIST_DEST, e))?; );
}
} }
eprintln!(" Service uninstalled. Numa will no longer auto-start.\n"); eprintln!(" Service uninstalled. Numa will no longer auto-start.\n");
@@ -449,24 +458,63 @@ fn backup_path_linux() -> std::path::PathBuf {
let home = std::env::var("HOME") let home = std::env::var("HOME")
.map(std::path::PathBuf::from) .map(std::path::PathBuf::from)
.unwrap_or_else(|_| std::path::PathBuf::from("/root")); .unwrap_or_else(|_| std::path::PathBuf::from("/root"));
let dir = home.join(".numa"); home.join(".numa").join("original-resolv.conf")
let _ = std::fs::create_dir_all(&dir); }
dir.join("original-resolv.conf")
#[cfg(target_os = "linux")]
fn is_systemd_resolved_active() -> bool {
std::process::Command::new("systemctl")
.args(["is-active", "--quiet", "systemd-resolved"])
.status()
.map(|s| s.success())
.unwrap_or(false)
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn install_linux() -> Result<(), String> { fn install_linux() -> Result<(), String> {
let backup = backup_path_linux(); // Detect systemd-resolved — direct resolv.conf manipulation won't persist
let resolv = std::path::Path::new("/etc/resolv.conf"); if is_systemd_resolved_active() {
let resolved_dir = std::path::Path::new("/etc/systemd/resolved.conf.d");
std::fs::create_dir_all(resolved_dir)
.map_err(|e| format!("failed to create {}: {}", resolved_dir.display(), e))?;
// Save current resolv.conf let drop_in = resolved_dir.join("numa.conf");
if resolv.exists() { std::fs::write(&drop_in, "[Resolve]\nDNS=127.0.0.1\nDomains=~.\n")
std::fs::copy(resolv, &backup) .map_err(|e| format!("failed to write {}: {}", drop_in.display(), e))?;
.map_err(|e| format!("failed to backup /etc/resolv.conf: {}", e))?;
eprintln!(" Saved /etc/resolv.conf to {}", backup.display()); let _ = run_systemctl(&["restart", "systemd-resolved"]);
eprintln!(" systemd-resolved detected.");
eprintln!(" Installed drop-in: {}", drop_in.display());
eprintln!(" Run 'sudo numa uninstall' to remove.\n");
return Ok(());
}
// Fallback: direct resolv.conf manipulation
let resolv = std::path::Path::new("/etc/resolv.conf");
let backup = backup_path_linux();
// Ensure backup directory exists
if let Some(parent) = backup.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("failed to create {}: {}", parent.display(), e))?;
}
// Back up current resolv.conf (ignore NotFound)
match std::fs::copy(resolv, &backup) {
Ok(_) => eprintln!(" Saved /etc/resolv.conf to {}", backup.display()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(format!("failed to backup /etc/resolv.conf: {}", e)),
}
if resolv
.symlink_metadata()
.map(|m| m.file_type().is_symlink())
.unwrap_or(false)
{
eprintln!(" warning: /etc/resolv.conf is a symlink — changes may not persist.");
eprintln!(" Consider using systemd-resolved or NetworkManager instead.\n");
} }
// Write new resolv.conf pointing to Numa
let content = let content =
"# Generated by Numa — run 'sudo numa uninstall' to restore\nnameserver 127.0.0.1\n"; "# Generated by Numa — run 'sudo numa uninstall' to restore\nnameserver 127.0.0.1\n";
std::fs::write(resolv, content) std::fs::write(resolv, content)
@@ -479,26 +527,45 @@ fn install_linux() -> Result<(), String> {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn uninstall_linux() -> Result<(), String> { fn uninstall_linux() -> Result<(), String> {
// Check for systemd-resolved drop-in first
let drop_in = std::path::Path::new("/etc/systemd/resolved.conf.d/numa.conf");
if drop_in.exists() {
std::fs::remove_file(drop_in)
.map_err(|e| format!("failed to remove {}: {}", drop_in.display(), e))?;
let _ = run_systemctl(&["restart", "systemd-resolved"]);
eprintln!(" Removed systemd-resolved drop-in. DNS restored.\n");
return Ok(());
}
// Fallback: restore resolv.conf from backup
let backup = backup_path_linux(); let backup = backup_path_linux();
let resolv = std::path::Path::new("/etc/resolv.conf"); let resolv = std::path::Path::new("/etc/resolv.conf");
if backup.exists() { match std::fs::copy(&backup, resolv) {
std::fs::copy(&backup, resolv) Ok(_) => {
.map_err(|e| format!("failed to restore /etc/resolv.conf: {}", e))?;
std::fs::remove_file(&backup).ok(); std::fs::remove_file(&backup).ok();
eprintln!(" Restored /etc/resolv.conf from backup. Backup removed.\n"); eprintln!(" Restored /etc/resolv.conf from backup. Backup removed.\n");
} else { }
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
eprintln!(" No backup found at {}.", backup.display()); eprintln!(" No backup found at {}.", backup.display());
eprintln!(" Manually edit /etc/resolv.conf to restore your DNS.\n"); eprintln!(" Manually edit /etc/resolv.conf to restore your DNS.\n");
} }
Err(e) => return Err(format!("failed to restore /etc/resolv.conf: {}", e)),
}
Ok(())
}
#[cfg(target_os = "linux")]
fn ensure_binary_installed() -> 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());
}
Ok(()) Ok(())
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn install_service_linux() -> Result<(), String> { fn install_service_linux() -> Result<(), String> {
if !std::path::Path::new("/usr/local/bin/numa").exists() { ensure_binary_installed()?;
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"); let unit = include_str!("../numa.service");
std::fs::write(SYSTEMD_UNIT, unit) std::fs::write(SYSTEMD_UNIT, unit)
@@ -517,12 +584,17 @@ fn install_service_linux() -> Result<(), String> {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn uninstall_service_linux() -> Result<(), String> { fn uninstall_service_linux() -> Result<(), String> {
let _ = run_systemctl(&["stop", "numa"]); if let Err(e) = run_systemctl(&["stop", "numa"]) {
let _ = run_systemctl(&["disable", "numa"]); eprintln!(" warning: {}", e);
}
if let Err(e) = run_systemctl(&["disable", "numa"]) {
eprintln!(" warning: {}", e);
}
if std::path::Path::new(SYSTEMD_UNIT).exists() { if let Err(e) = std::fs::remove_file(SYSTEMD_UNIT) {
std::fs::remove_file(SYSTEMD_UNIT) if e.kind() != std::io::ErrorKind::NotFound {
.map_err(|e| format!("failed to remove {}: {}", SYSTEMD_UNIT, e))?; return Err(format!("failed to remove {}: {}", SYSTEMD_UNIT, e));
}
} }
let _ = run_systemctl(&["daemon-reload"]); let _ = run_systemctl(&["daemon-reload"]);