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:
@@ -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))?;
|
||||
match std::fs::copy(&backup, resolv) {
|
||||
Ok(_) => {
|
||||
std::fs::remove_file(&backup).ok();
|
||||
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!(" 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"]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user