@@ -100,7 +100,7 @@ use color_eyre::Result;
100100use rustix:: path:: Arg ;
101101use serde:: { Deserialize , Serialize } ;
102102use tokio:: io:: AsyncReadExt ;
103- use tracing:: debug;
103+ use tracing:: { debug, warn } ;
104104
105105const ENTRYPOINT : & str = "/var/lib/bcvk/entrypoint" ;
106106
@@ -283,6 +283,87 @@ pub struct RunEphemeralOpts {
283283
284284 #[ clap( long = "karg" , help = "Additional kernel command line arguments" ) ]
285285 pub kernel_args : Vec < String > ,
286+
287+ /// Host DNS servers (read on host, configured via podman --dns flags)
288+ /// Not a CLI option - populated automatically from host's /etc/resolv.conf
289+ #[ clap( skip) ]
290+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
291+ pub host_dns_servers : Option < Vec < String > > ,
292+ }
293+
294+ /// Parse DNS servers from resolv.conf format content
295+ fn parse_resolv_conf ( content : & str ) -> Vec < String > {
296+ let mut dns_servers = Vec :: new ( ) ;
297+ for line in content. lines ( ) {
298+ let line = line. trim ( ) ;
299+ // Parse lines like "nameserver 8.8.8.8" or "nameserver 2001:4860:4860::8888"
300+ if let Some ( server) = line. strip_prefix ( "nameserver " ) {
301+ let server = server. trim ( ) ;
302+ if !server. is_empty ( ) {
303+ dns_servers. push ( server. to_string ( ) ) ;
304+ }
305+ }
306+ }
307+ dns_servers
308+ }
309+
310+ /// Read DNS servers from host's resolv.conf
311+ /// Returns a vector of DNS server IP addresses, or None if unable to read/parse
312+ ///
313+ /// For systemd-resolved systems, reads from /run/systemd/resolve/resolv.conf
314+ /// which contains actual upstream DNS servers, not the stub resolver (127.0.0.53).
315+ /// Falls back to /etc/resolv.conf for non-systemd-resolved systems.
316+ fn read_host_dns_servers ( ) -> Option < Vec < String > > {
317+ // Try systemd-resolved's upstream DNS file first
318+ // This avoids reading 127.0.0.53 (stub resolver) from /etc/resolv.conf
319+ let paths = [
320+ "/run/systemd/resolve/resolv.conf" , // systemd-resolved upstream servers
321+ "/etc/resolv.conf" , // traditional or fallback
322+ ] ;
323+
324+ for path in & paths {
325+ match std:: fs:: read_to_string ( path) {
326+ Ok ( content) => {
327+ let dns_servers = parse_resolv_conf ( & content) ;
328+
329+ // Filter out localhost, link-local, and private network addresses
330+ // QEMU runs in user networking mode (slirp) inside a container, which cannot
331+ // reach private network addresses (10.x.x.x, 172.16-31.x.x, 192.168.x.x for IPv4,
332+ // fc00::/7 ULA for IPv6). These are often VPN-only DNS servers that won't work.
333+ // We'll fall back to public DNS (8.8.8.8, 1.1.1.1) which is more reliable.
334+ let filtered_servers: Vec < String > = dns_servers
335+ . into_iter ( )
336+ . filter ( |s| {
337+ // Try parsing as IPv4 first
338+ if let Ok ( ip) = s. parse :: < std:: net:: Ipv4Addr > ( ) {
339+ // Reject loopback, link-local, and private addresses
340+ !ip. is_loopback ( ) && !ip. is_link_local ( ) && !ip. is_private ( )
341+ } else if let Ok ( ip) = s. parse :: < std:: net:: Ipv6Addr > ( ) {
342+ // Reject loopback (::1), link-local (fe80::/10), ULA (fc00::/7), and multicast
343+ !ip. is_loopback ( ) && !ip. is_multicast ( )
344+ && !( ip. segments ( ) [ 0 ] & 0xffc0 == 0xfe80 ) // link-local fe80::/10
345+ && !( ip. segments ( ) [ 0 ] & 0xfe00 == 0xfc00 ) // ULA fc00::/7 (private)
346+ } else {
347+ false // Reject invalid addresses
348+ }
349+ } )
350+ . collect ( ) ;
351+
352+ if !filtered_servers. is_empty ( ) {
353+ debug ! ( "Found DNS servers from {}: {:?}" , path, filtered_servers) ;
354+ return Some ( filtered_servers) ;
355+ } else {
356+ debug ! ( "No usable DNS servers in {}, trying next" , path) ;
357+ }
358+ }
359+ Err ( e) => {
360+ debug ! ( "Failed to read {}: {}, trying next" , path, e) ;
361+ }
362+ }
363+ }
364+
365+ debug ! ( "No DNS servers found in any resolv.conf file" ) ;
366+ None
286367}
287368
288369/// Launch privileged container with QEMU+KVM for ephemeral VM, spawning as subprocess.
@@ -499,8 +580,32 @@ fn prepare_run_command_with_temp(
499580 cmd. args ( [ "-v" , & format ! ( "{}:/run/systemd-units:ro" , units_dir) ] ) ;
500581 }
501582
583+ // Read host DNS servers and configure them via podman --dns flags
584+ // This fixes DNS resolution issues when QEMU runs inside containers.
585+ // QEMU's slirp reads /etc/resolv.conf from the container's network namespace,
586+ // which would otherwise contain unreachable bridge DNS servers (e.g., 169.254.1.1).
587+ // Using --dns properly configures /etc/resolv.conf in the container.
588+ let host_dns_servers = read_host_dns_servers ( ) . or_else ( || {
589+ // Fallback to public DNS if no usable DNS found in system configuration
590+ // This ensures DNS works even when host has broken/unreachable DNS config
591+ 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." ) ;
592+ Some ( vec ! [ "8.8.8.8" . to_string( ) , "1.1.1.1" . to_string( ) ] )
593+ } ) ;
594+
595+ if let Some ( ref dns) = host_dns_servers {
596+ debug ! ( "Using DNS servers for ephemeral VM: {:?}" , dns) ;
597+ // Configure DNS servers for the container using --dns flags
598+ // This properly sets up /etc/resolv.conf in the container's network namespace
599+ for server in dns {
600+ cmd. args ( [ "--dns" , server] ) ;
601+ }
602+ }
603+
502604 // Pass configuration as JSON via BCK_CONFIG environment variable
503- let config = serde_json:: to_string ( & opts) . unwrap ( ) ;
605+ // Include host DNS servers in the config so they're available inside the container
606+ let mut opts_with_dns = opts. clone ( ) ;
607+ opts_with_dns. host_dns_servers = host_dns_servers;
608+ let config = serde_json:: to_string ( & opts_with_dns) . unwrap ( ) ;
504609 cmd. args ( [ "-e" , & format ! ( "BCK_CONFIG={config}" ) ] ) ;
505610
506611 // Handle --execute output files and virtio-serial devices
@@ -1229,6 +1334,16 @@ Options=
12291334 qemu_config. add_virtio_serial_out ( "org.bcvk.journal" , "/run/journal.log" . to_string ( ) , false ) ;
12301335 debug ! ( "Added virtio-serial device for journal streaming to /run/journal.log" ) ;
12311336
1337+ // DNS is configured via podman --dns flags (see prepare_run_command_with_temp)
1338+ // This fixes DNS resolution issues when QEMU runs inside containers.
1339+ // QEMU's slirp reads /etc/resolv.conf from the container's network namespace,
1340+ // and podman properly sets it up using --dns instead of relying on bridge DNS.
1341+ if let Some ( ref dns_servers) = opts. host_dns_servers {
1342+ debug ! ( "DNS servers configured for QEMU slirp: {:?}" , dns_servers) ;
1343+ } else {
1344+ warn ! ( "No host DNS servers available, QEMU slirp will use container's resolv.conf which may not work" ) ;
1345+ }
1346+
12321347 if opts. common . ssh_keygen {
12331348 qemu_config. enable_ssh_access ( None ) ; // Use default port 2222
12341349 debug ! ( "Enabled SSH port forwarding: host port 2222 -> guest port 22" ) ;
0 commit comments