@@ -283,6 +283,70 @@ 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, passed to container for QEMU configuration)
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 addresses (127.0.0.53, ::1, etc.) which won't work in QEMU
330+ let filtered_servers: Vec < String > = dns_servers
331+ . into_iter ( )
332+ . filter ( |s| !s. starts_with ( "127." ) && s != "::1" )
333+ . collect ( ) ;
334+
335+ if !filtered_servers. is_empty ( ) {
336+ debug ! ( "Found DNS servers from {}: {:?}" , path, filtered_servers) ;
337+ return Some ( filtered_servers) ;
338+ } else {
339+ debug ! ( "No usable DNS servers in {}, trying next" , path) ;
340+ }
341+ }
342+ Err ( e) => {
343+ debug ! ( "Failed to read {}: {}, trying next" , path, e) ;
344+ }
345+ }
346+ }
347+
348+ debug ! ( "No DNS servers found in any resolv.conf file" ) ;
349+ None
286350}
287351
288352/// Launch privileged container with QEMU+KVM for ephemeral VM, spawning as subprocess.
@@ -499,8 +563,20 @@ fn prepare_run_command_with_temp(
499563 cmd. args ( [ "-v" , & format ! ( "{}:/run/systemd-units:ro" , units_dir) ] ) ;
500564 }
501565
566+ // Read host DNS servers before entering container
567+ // QEMU's slirp will use these instead of container's unreachable bridge DNS servers
568+ let host_dns_servers = read_host_dns_servers ( ) ;
569+ if let Some ( ref dns) = host_dns_servers {
570+ debug ! ( "Read host DNS servers: {:?}" , dns) ;
571+ } else {
572+ debug ! ( "No DNS servers found, QEMU will use container's resolv.conf" ) ;
573+ }
574+
502575 // Pass configuration as JSON via BCK_CONFIG environment variable
503- let config = serde_json:: to_string ( & opts) . unwrap ( ) ;
576+ // Include host DNS servers in the config so they're available inside the container
577+ let mut opts_with_dns = opts. clone ( ) ;
578+ opts_with_dns. host_dns_servers = host_dns_servers;
579+ let config = serde_json:: to_string ( & opts_with_dns) . unwrap ( ) ;
504580 cmd. args ( [ "-e" , & format ! ( "BCK_CONFIG={config}" ) ] ) ;
505581
506582 // Handle --execute output files and virtio-serial devices
@@ -1229,6 +1305,30 @@ Options=
12291305 qemu_config. add_virtio_serial_out ( "org.bcvk.journal" , "/run/journal.log" . to_string ( ) , false ) ;
12301306 debug ! ( "Added virtio-serial device for journal streaming to /run/journal.log" ) ;
12311307
1308+ // Configure DNS servers from host's /etc/resolv.conf
1309+ // This fixes DNS resolution issues when QEMU runs inside containers.
1310+ // QEMU's slirp reads /etc/resolv.conf from the container's network namespace,
1311+ // which contains unreachable bridge DNS servers (e.g., 169.254.1.1, 10.x.y.z).
1312+ // Solution: Create a custom resolv.conf with host's DNS servers that slirp can read.
1313+ let dns_servers = opts. host_dns_servers . clone ( ) ;
1314+ if let Some ( ref dns) = dns_servers {
1315+ // Create a resolv.conf that QEMU's slirp will read
1316+ let mut resolv_content = String :: new ( ) ;
1317+ for server in dns {
1318+ resolv_content. push_str ( & format ! ( "nameserver {}\n " , server) ) ;
1319+ }
1320+
1321+ // Write to /etc/resolv.conf so slirp can read it
1322+ // Note: This overwrites the container's resolv.conf, but that's okay since
1323+ // we're running in an isolated container that will be destroyed
1324+ std:: fs:: write ( "/etc/resolv.conf" , & resolv_content)
1325+ . context ( "Failed to write /etc/resolv.conf for QEMU slirp" ) ?;
1326+
1327+ debug ! ( "Configured DNS servers for QEMU slirp: {:?}" , dns) ;
1328+ } else {
1329+ debug ! ( "No host DNS servers available, QEMU slirp will use container's resolv.conf" ) ;
1330+ }
1331+
12321332 if opts. common . ssh_keygen {
12331333 qemu_config. enable_ssh_access ( None ) ; // Use default port 2222
12341334 debug ! ( "Enabled SSH port forwarding: host port 2222 -> guest port 22" ) ;
0 commit comments