From 58ac1356545ea0789f2d0b22e6112bb955ac2c94 Mon Sep 17 00:00:00 2001 From: Razvan Dimescu Date: Wed, 1 Apr 2026 06:15:10 +0300 Subject: [PATCH] feat: forward search domains to VPC resolver on Linux Parse search/domain lines from resolv.conf and create conditional forwarding rules to the original nameserver or AWS VPC resolver (169.254.169.253). Fixes internal hostname resolution on cloud VMs where recursive mode can't resolve private DNS zones. Co-Authored-By: Claude Opus 4.6 --- src/system_dns.rs | 82 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 76 insertions(+), 6 deletions(-) diff --git a/src/system_dns.rs b/src/system_dns.rs index 65e5adf..e8072cc 100644 --- a/src/system_dns.rs +++ b/src/system_dns.rs @@ -30,10 +30,7 @@ pub fn discover_system_dns() -> SystemDnsInfo { } #[cfg(target_os = "linux")] { - SystemDnsInfo { - default_upstream: detect_upstream_linux_or_backup(), - forwarding_rules: Vec::new(), - } + discover_linux() } #[cfg(windows)] { @@ -166,8 +163,81 @@ fn make_rule(domain: &str, nameserver: &str) -> Option { }) } -/// Detect upstream from /etc/resolv.conf, falling back to backup file if resolv.conf -/// only has loopback (meaning numa install already ran). +#[cfg(target_os = "linux")] +fn discover_linux() -> SystemDnsInfo { + let upstream = detect_upstream_linux_or_backup(); + + // Parse search domains and create forwarding rules to the original nameserver. + // On cloud VMs (AWS/GCP), internal domains need to reach the VPC resolver. + let forwarding_rules = detect_search_domain_rules(); + if !forwarding_rules.is_empty() { + info!( + "detected {} search domain forwarding rules", + forwarding_rules.len() + ); + } + + SystemDnsInfo { + default_upstream: upstream, + forwarding_rules, + } +} + +/// Parse search domains from resolv.conf and create forwarding rules to the +/// original nameserver or the AWS VPC resolver (169.254.169.253). +#[cfg(target_os = "linux")] +fn detect_search_domain_rules() -> Vec { + let mut rules = Vec::new(); + + // Find the original nameserver to forward internal domains to + let forwarder = find_original_nameserver().unwrap_or_else(|| "169.254.169.253".to_string()); + + // Parse search domains from resolv.conf + for path in &["/etc/resolv.conf"] { + if let Ok(text) = std::fs::read_to_string(path) { + for line in text.lines() { + let line = line.trim(); + if line.starts_with("search") || line.starts_with("domain") { + for domain in line.split_whitespace().skip(1) { + if let Some(rule) = make_rule(domain, &forwarder) { + info!("forwarding .{} to {}", domain, forwarder); + rules.push(rule); + } + } + } + } + } + } + rules +} + +/// Find the original (non-loopback) nameserver from resolv.conf or systemd-resolved config. +#[cfg(target_os = "linux")] +fn find_original_nameserver() -> Option { + // Try resolv.conf for a real nameserver + if let Some(ns) = read_upstream_from_file("/etc/resolv.conf") { + return Some(ns); + } + // Try systemd-resolved's actual upstream + if let Ok(output) = std::process::Command::new("resolvectl") + .args(["status", "--no-pager"]) + .output() + { + let text = String::from_utf8_lossy(&output.stdout); + for line in text.lines() { + if line.contains("DNS Servers") || line.contains("Current DNS Server") { + if let Some(ip) = line.split(':').next_back() { + let ip = ip.trim(); + if !is_loopback_or_stub(ip) && !ip.is_empty() { + return Some(ip.to_string()); + } + } + } + } + } + None +} + #[cfg(target_os = "linux")] fn detect_upstream_linux_or_backup() -> Option { // Try /etc/resolv.conf first