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")]
|
#[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"]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user