fix: macOS install health check, harden recursive probe
Verify numa is listening (API port) before redirecting system DNS on macOS — if the service fails to start (e.g. port 53 in use), unload the service and abort instead of breaking DNS. Probe up to 3 root hints before declaring recursive mode unavailable. Validate IPs from resolvectl to avoid IPv6 fragment extraction. Extract DEFAULT_API_PORT constant. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,7 @@ sudo numa # port 53 requires root
|
|||||||
|
|
||||||
Open the dashboard: **http://numa.numa** (or `http://localhost:5380`)
|
Open the dashboard: **http://numa.numa** (or `http://localhost:5380`)
|
||||||
|
|
||||||
Set as system DNS: `sudo numa install && sudo numa service start`
|
Set as system DNS: `sudo numa install`
|
||||||
|
|
||||||
## Local Services
|
## Local Services
|
||||||
|
|
||||||
|
|||||||
@@ -59,8 +59,10 @@ fn default_bind_addr() -> String {
|
|||||||
"0.0.0.0:53".to_string()
|
"0.0.0.0:53".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const DEFAULT_API_PORT: u16 = 5380;
|
||||||
|
|
||||||
fn default_api_port() -> u16 {
|
fn default_api_port() -> u16 {
|
||||||
5380
|
DEFAULT_API_PORT
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Default, PartialEq, Eq, Clone, Copy)]
|
#[derive(Deserialize, Default, PartialEq, Eq, Clone, Copy)]
|
||||||
|
|||||||
@@ -65,19 +65,20 @@ pub async fn probe_udp(root_hints: &[SocketAddr]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Probe whether recursive resolution works by querying a root server.
|
/// Probe whether recursive resolution works by querying root servers.
|
||||||
|
/// Tries up to 3 hints before declaring failure.
|
||||||
pub async fn probe_recursive(root_hints: &[SocketAddr]) -> bool {
|
pub async fn probe_recursive(root_hints: &[SocketAddr]) -> bool {
|
||||||
let hint = match root_hints.first() {
|
|
||||||
Some(h) => *h,
|
|
||||||
None => return false,
|
|
||||||
};
|
|
||||||
let mut probe = DnsPacket::query(next_id(), ".", QueryType::NS);
|
let mut probe = DnsPacket::query(next_id(), ".", QueryType::NS);
|
||||||
probe.header.recursion_desired = false;
|
probe.header.recursion_desired = false;
|
||||||
match forward_udp(&probe, hint, Duration::from_secs(3)).await {
|
for hint in root_hints.iter().take(3) {
|
||||||
Ok(resp) => !resp.answers.is_empty() || !resp.authorities.is_empty(),
|
if let Ok(resp) = forward_udp(&probe, *hint, Duration::from_secs(3)).await {
|
||||||
Err(_) => false,
|
if !resp.answers.is_empty() || !resp.authorities.is_empty() {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn prime_tld_cache(
|
pub async fn prime_tld_cache(
|
||||||
cache: &RwLock<DnsCache>,
|
cache: &RwLock<DnsCache>,
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ fn resolvectl_dns_server() -> Option<String> {
|
|||||||
if line.contains("DNS Servers") || line.contains("Current DNS Server") {
|
if line.contains("DNS Servers") || line.contains("Current DNS Server") {
|
||||||
if let Some(ip) = line.split(':').next_back() {
|
if let Some(ip) = line.split(':').next_back() {
|
||||||
let ip = ip.trim();
|
let ip = ip.trim();
|
||||||
if !is_loopback_or_stub(ip) {
|
if ip.parse::<std::net::IpAddr>().is_ok() && !is_loopback_or_stub(ip) {
|
||||||
return Some(ip.to_string());
|
return Some(ip.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -632,12 +632,7 @@ fn install_service_macos() -> Result<(), String> {
|
|||||||
std::fs::write(PLIST_DEST, plist)
|
std::fs::write(PLIST_DEST, plist)
|
||||||
.map_err(|e| format!("failed to write {}: {}", PLIST_DEST, e))?;
|
.map_err(|e| format!("failed to write {}: {}", PLIST_DEST, e))?;
|
||||||
|
|
||||||
// Configure system DNS before starting service
|
// Load the service first so numa is listening before DNS redirect
|
||||||
if let Err(e) = install_macos() {
|
|
||||||
eprintln!(" warning: failed to configure system DNS: {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the service
|
|
||||||
let status = std::process::Command::new("launchctl")
|
let status = std::process::Command::new("launchctl")
|
||||||
.args(["load", "-w", PLIST_DEST])
|
.args(["load", "-w", PLIST_DEST])
|
||||||
.status()
|
.status()
|
||||||
@@ -647,6 +642,27 @@ fn install_service_macos() -> Result<(), String> {
|
|||||||
return Err("launchctl load failed".to_string());
|
return Err("launchctl load failed".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait for numa to be ready before redirecting DNS
|
||||||
|
let api_up = (0..10).any(|i| {
|
||||||
|
if i > 0 {
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||||
|
}
|
||||||
|
std::net::TcpStream::connect(("127.0.0.1", crate::config::DEFAULT_API_PORT)).is_ok()
|
||||||
|
});
|
||||||
|
if !api_up {
|
||||||
|
// Service failed to start — don't redirect DNS to a dead endpoint
|
||||||
|
let _ = std::process::Command::new("launchctl")
|
||||||
|
.args(["unload", PLIST_DEST])
|
||||||
|
.status();
|
||||||
|
return Err(
|
||||||
|
"numa service did not start (port 53 may be in use). Service unloaded.".to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = install_macos() {
|
||||||
|
eprintln!(" warning: failed to configure system DNS: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
eprintln!(" Service installed and started.");
|
eprintln!(" Service installed and started.");
|
||||||
eprintln!(" Numa will auto-start on boot and restart if killed.");
|
eprintln!(" Numa will auto-start on boot and restart if killed.");
|
||||||
eprintln!(" Logs: /usr/local/var/log/numa.log");
|
eprintln!(" Logs: /usr/local/var/log/numa.log");
|
||||||
|
|||||||
Reference in New Issue
Block a user