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")]
fn numa_data_dir() -> std::path::PathBuf {
let home = std::env::var("HOME")
.or_else(|_| std::env::var("SUDO_USER").map(|u| format!("/Users/{}", u)))
.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")
}
@@ -407,15 +408,23 @@ fn install_service_macos() -> Result<(), String> {
#[cfg(target_os = "macos")]
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
let _ = std::process::Command::new("launchctl")
let status = 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))?;
if let Ok(s) = status {
if !s.success() {
eprintln!(
" warning: launchctl unload returned non-zero (service may still be running)"
);
}
}
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")
.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")
home.join(".numa").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")]
fn install_linux() -> Result<(), String> {
let backup = backup_path_linux();
let resolv = std::path::Path::new("/etc/resolv.conf");
// Detect systemd-resolved — direct resolv.conf manipulation won't persist
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
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());
let drop_in = resolved_dir.join("numa.conf");
std::fs::write(&drop_in, "[Resolve]\nDNS=127.0.0.1\nDomains=~.\n")
.map_err(|e| format!("failed to write {}: {}", drop_in.display(), e))?;
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 =
"# Generated by Numa — run 'sudo numa uninstall' to restore\nnameserver 127.0.0.1\n";
std::fs::write(resolv, content)
@@ -479,26 +527,45 @@ fn install_linux() -> Result<(), String> {
#[cfg(target_os = "linux")]
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 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");
match std::fs::copy(&backup, resolv) {
Ok(_) => {
std::fs::remove_file(&backup).ok();
eprintln!(" Restored /etc/resolv.conf from backup. Backup removed.\n");
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
eprintln!(" No backup found at {}.", backup.display());
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(())
}
#[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());
}
ensure_binary_installed()?;
let unit = include_str!("../numa.service");
std::fs::write(SYSTEMD_UNIT, unit)
@@ -517,12 +584,17 @@ fn install_service_linux() -> Result<(), String> {
#[cfg(target_os = "linux")]
fn uninstall_service_linux() -> Result<(), String> {
let _ = run_systemctl(&["stop", "numa"]);
let _ = run_systemctl(&["disable", "numa"]);
if let Err(e) = run_systemctl(&["stop", "numa"]) {
eprintln!(" warning: {}", e);
}
if let Err(e) = run_systemctl(&["disable", "numa"]) {
eprintln!(" warning: {}", e);
}
if std::path::Path::new(SYSTEMD_UNIT).exists() {
std::fs::remove_file(SYSTEMD_UNIT)
.map_err(|e| format!("failed to remove {}: {}", SYSTEMD_UNIT, e))?;
if let Err(e) = std::fs::remove_file(SYSTEMD_UNIT) {
if e.kind() != std::io::ErrorKind::NotFound {
return Err(format!("failed to remove {}: {}", SYSTEMD_UNIT, e));
}
}
let _ = run_systemctl(&["daemon-reload"]);