fix circular reference: detect DHCP DNS when scutil shows loopback

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) <noreply@anthropic.com>
This commit is contained in:
Razvan Dimescu
2026-03-22 10:24:54 +02:00
parent 55ea49b003
commit 4a1c98b02d
2 changed files with 67 additions and 14 deletions

View File

@@ -86,7 +86,10 @@ async fn main() -> numa::Result<()> {
let system_dns = discover_system_dns(); let system_dns = discover_system_dns();
let upstream_addr = if config.upstream.address.is_empty() { let upstream_addr = if config.upstream.address.is_empty() {
system_dns.default_upstream.unwrap_or_else(|| { 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)"); info!("could not detect system DNS, falling back to 9.9.9.9 (Quad9)");
"9.9.9.9".to_string() "9.9.9.9".to_string()
}) })
@@ -296,7 +299,11 @@ async fn network_watch_loop(ctx: Arc<numa::ctx::ServerCtx>) {
// Check upstream change (only for auto-detected upstream) // Check upstream change (only for auto-detected upstream)
if ctx.upstream_auto { if ctx.upstream_auto {
let dns_info = numa::system_dns::discover_system_dns(); let dns_info = numa::system_dns::discover_system_dns();
if let Some(new_addr) = dns_info.default_upstream { // 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) = if let Ok(new_upstream) =
format!("{}:{}", new_addr, ctx.upstream_port).parse::<SocketAddr>() format!("{}:{}", new_addr, ctx.upstream_port).parse::<SocketAddr>()
{ {
@@ -308,7 +315,6 @@ async fn network_watch_loop(ctx: Arc<numa::ctx::ServerCtx>) {
} }
} }
} }
}
// Flush stale LAN peers on any network change // Flush stale LAN peers on any network change
if changed { if changed {

View File

@@ -205,6 +205,53 @@ fn read_upstream_from_file(path: &str) -> Option<String> {
None 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<String> {
#[cfg(target_os = "macos")]
{
detect_dhcp_dns_macos()
}
#[cfg(not(target_os = "macos"))]
{
None
}
}
#[cfg(target_os = "macos")]
fn detect_dhcp_dns_macos() -> Option<String> {
// 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::<std::net::Ipv4Addr>().is_ok()
{
log::info!("detected DHCP DNS: {}", addr);
return Some(addr.to_string());
}
}
}
}
}
}
None
}
// --- Windows implementation --- // --- Windows implementation ---
#[cfg(windows)] #[cfg(windows)]