From 06850de728cc44dce6ee8acdfac6c6e26dd681c2 Mon Sep 17 00:00:00 2001 From: Razvan Dimescu Date: Sun, 22 Mar 2026 10:24:54 +0200 Subject: [PATCH] fix circular reference: detect DHCP DNS when scutil shows loopback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When numa install is active, scutil --dns only returns 127.0.0.1. Previously fell back to 9.9.9.9 (Quad9) which fails on networks that block external DNS. Now reads DHCP-provided DNS from ipconfig getpacket en0/en1 as intermediate fallback before Quad9. Tested on a network that blocks 8.8.8.8, 9.9.9.9, 1.1.1.1 but allows ISP DNS (213.154.124.25) — Numa now auto-detects and uses it. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/main.rs | 34 ++++++++++++++++++++-------------- src/system_dns.rs | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 14 deletions(-) diff --git a/src/main.rs b/src/main.rs index 3934d61..5de9a15 100644 --- a/src/main.rs +++ b/src/main.rs @@ -86,10 +86,13 @@ async fn main() -> numa::Result<()> { let system_dns = discover_system_dns(); let upstream_addr = if config.upstream.address.is_empty() { - system_dns.default_upstream.unwrap_or_else(|| { - info!("could not detect system DNS, falling back to 9.9.9.9 (Quad9)"); - "9.9.9.9".to_string() - }) + system_dns + .default_upstream + .or_else(numa::system_dns::detect_dhcp_dns) + .unwrap_or_else(|| { + info!("could not detect system DNS, falling back to 9.9.9.9 (Quad9)"); + "9.9.9.9".to_string() + }) } else { config.upstream.address.clone() }; @@ -296,16 +299,19 @@ async fn network_watch_loop(ctx: Arc) { // Check upstream change (only for auto-detected upstream) if ctx.upstream_auto { let dns_info = numa::system_dns::discover_system_dns(); - if let Some(new_addr) = dns_info.default_upstream { - if let Ok(new_upstream) = - format!("{}:{}", new_addr, ctx.upstream_port).parse::() - { - let mut upstream = ctx.upstream.lock().unwrap(); - if new_upstream != *upstream { - info!("upstream changed: {} → {}", *upstream, new_upstream); - *upstream = new_upstream; - changed = true; - } + // Use detected upstream, or try DHCP-provided DNS, or fall back to Quad9 + let new_addr = dns_info + .default_upstream + .or_else(numa::system_dns::detect_dhcp_dns) + .unwrap_or_else(|| "9.9.9.9".to_string()); + if let Ok(new_upstream) = + format!("{}:{}", new_addr, ctx.upstream_port).parse::() + { + let mut upstream = ctx.upstream.lock().unwrap(); + if new_upstream != *upstream { + info!("upstream changed: {} → {}", *upstream, new_upstream); + *upstream = new_upstream; + changed = true; } } } diff --git a/src/system_dns.rs b/src/system_dns.rs index 9d46ea3..57559b5 100644 --- a/src/system_dns.rs +++ b/src/system_dns.rs @@ -205,6 +205,53 @@ fn read_upstream_from_file(path: &str) -> Option { None } +/// Detect DNS server from DHCP lease — fallback when scutil/resolv.conf only shows 127.0.0.1. +/// On macOS: parses `ipconfig getpacket en0` for domain_name_server. +/// On Linux/Windows: returns None (not implemented yet). +pub fn detect_dhcp_dns() -> Option { + #[cfg(target_os = "macos")] + { + detect_dhcp_dns_macos() + } + #[cfg(not(target_os = "macos"))] + { + None + } +} + +#[cfg(target_os = "macos")] +fn detect_dhcp_dns_macos() -> Option { + // Try common interfaces + for iface in &["en0", "en1"] { + let output = std::process::Command::new("ipconfig") + .args(["getpacket", iface]) + .output() + .ok()?; + let text = String::from_utf8_lossy(&output.stdout); + for line in text.lines() { + if line.contains("domain_name_server") { + // Format: "domain_name_server (ip_mult): {213.154.124.25, 1.0.0.1}" + if let Some(braces) = line.split('{').nth(1) { + let inner = braces.trim_end_matches('}').trim(); + // Take the first non-loopback DNS server + for addr in inner.split(',') { + let addr = addr.trim(); + if !addr.is_empty() + && addr != "127.0.0.1" + && addr != "0.0.0.0" + && addr.parse::().is_ok() + { + log::info!("detected DHCP DNS: {}", addr); + return Some(addr.to_string()); + } + } + } + } + } + } + None +} + // --- Windows implementation --- #[cfg(windows)]