From fae2ca405b5157cee46fd21938f7cecae837cd06 Mon Sep 17 00:00:00 2001 From: gursewak1997 Date: Thu, 18 Dec 2025 14:30:41 -0800 Subject: [PATCH] Fix DNS resolution for VPN and private network configurations PR #167 introduced DNS filtering that excluded all private IP addresses (10.x, 172.16-31.x, 192.168.x, fc00::/7) assuming they would be unreachable from QEMU's slirp networking. However, this breaks VPN scenarios where private DNS servers are actually reachable. This change removes the overly aggressive private IP filtering, now only filtering out localhost and link-local addresses. Private network DNS servers are allowed through since they may be reachable (e.g., via VPN or air-gapped networks). If they're actually unreachable, DNS will fail naturally, which is better than prematurely filtering them out. Also downgraded the fallback warning from WARN to debug level since falling back to public DNS is a normal case, not an error condition. Assisted-by: Claude Code (Sonnet 4.5) Signed-off-by: gursewak1997 --- crates/kit/src/run_ephemeral.rs | 64 ++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/crates/kit/src/run_ephemeral.rs b/crates/kit/src/run_ephemeral.rs index d4277b0..3398e1f 100644 --- a/crates/kit/src/run_ephemeral.rs +++ b/crates/kit/src/run_ephemeral.rs @@ -326,23 +326,21 @@ fn read_host_dns_servers() -> Option> { Ok(content) => { let dns_servers = parse_resolv_conf(&content); - // Filter out localhost, link-local, and private network addresses - // QEMU runs in user networking mode (slirp) inside a container, which cannot - // reach private network addresses (10.x.x.x, 172.16-31.x.x, 192.168.x.x for IPv4, - // fc00::/7 ULA for IPv6). These are often VPN-only DNS servers that won't work. - // We'll fall back to public DNS (8.8.8.8, 1.1.1.1) which is more reliable. + // Filter out localhost and link-local addresses only + // Private network addresses (10.x, 172.16-31.x, 192.168.x, fc00::/7) are allowed + // because they may be reachable from the container/VM (e.g., VPN DNS servers). let filtered_servers: Vec = dns_servers .into_iter() .filter(|s| { // Try parsing as IPv4 first if let Ok(ip) = s.parse::() { - // Reject loopback, link-local, and private addresses - !ip.is_loopback() && !ip.is_link_local() && !ip.is_private() + // Reject loopback and link-local addresses only + !ip.is_loopback() && !ip.is_link_local() } else if let Ok(ip) = s.parse::() { - // Reject loopback (::1), link-local (fe80::/10), ULA (fc00::/7), and multicast - !ip.is_loopback() && !ip.is_multicast() + // Reject loopback (::1), link-local (fe80::/10), and multicast + !ip.is_loopback() + && !ip.is_multicast() && !(ip.segments()[0] & 0xffc0 == 0xfe80) // link-local fe80::/10 - && !(ip.segments()[0] & 0xfe00 == 0xfc00) // ULA fc00::/7 (private) } else { false // Reject invalid addresses } @@ -585,12 +583,7 @@ fn prepare_run_command_with_temp( // QEMU's slirp reads /etc/resolv.conf from the container's network namespace, // which would otherwise contain unreachable bridge DNS servers (e.g., 169.254.1.1). // Using --dns properly configures /etc/resolv.conf in the container. - let host_dns_servers = read_host_dns_servers().or_else(|| { - // Fallback to public DNS if no usable DNS found in system configuration - // This ensures DNS works even when host has broken/unreachable DNS config - warn!("No usable DNS servers found in system configuration, falling back to public DNS (8.8.8.8, 1.1.1.1). This may not work in air-gapped environments."); - Some(vec!["8.8.8.8".to_string(), "1.1.1.1".to_string()]) - }); + let host_dns_servers = read_host_dns_servers(); if let Some(ref dns) = host_dns_servers { debug!("Using DNS servers for ephemeral VM: {:?}", dns); @@ -1475,3 +1468,42 @@ Options= Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_resolv_conf() { + let cases = vec![ + // (input, expected) + ("nameserver 8.8.8.8\n", vec!["8.8.8.8"]), + ( + "nameserver 8.8.8.8\nnameserver 1.1.1.1\n", + vec!["8.8.8.8", "1.1.1.1"], + ), + ("# comment\nnameserver 8.8.8.8\n", vec!["8.8.8.8"]), + ("nameserver 127.0.0.1\n", vec!["127.0.0.1"]), + ("nameserver 169.254.1.1\n", vec!["169.254.1.1"]), + ("nameserver 10.0.0.1\n", vec!["10.0.0.1"]), + ( + "nameserver 2001:4860:4860::8888\n", + vec!["2001:4860:4860::8888"], + ), + ("nameserver ::1\n", vec!["::1"]), + ("nameserver fe80::1\n", vec!["fe80::1"]), + ("nameserver fc00::1\n", vec!["fc00::1"]), + ("# only comments\n", vec![]), + ("", vec![]), + ]; + + for (input, expected) in cases { + assert_eq!( + parse_resolv_conf(input), + expected, + "failed for input: {:?}", + input + ); + } + } +}