@@ -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,80 @@ 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, IPv6, and link-local addresses (169.254.x.x)
330+ // Link-local addresses are container bridges that won't work from inside QEMU
331+ // Keep private network addresses (they may work in corporate/home networks)
332+ let filtered_servers: Vec < String > = dns_servers
333+ . into_iter ( )
334+ . filter ( |s| {
335+ // Parse as IPv4 address
336+ if let Ok ( ip) = s. parse :: < std:: net:: Ipv4Addr > ( ) {
337+ // Reject loopback (127.x.x.x) and link-local (169.254.x.x)
338+ !ip. is_loopback ( ) && !ip. is_link_local ( )
339+ } else {
340+ false // Reject IPv6 and invalid addresses
341+ }
342+ } )
343+ . collect ( ) ;
344+
345+ if !filtered_servers. is_empty ( ) {
346+ debug ! ( "Found DNS servers from {}: {:?}" , path, filtered_servers) ;
347+ return Some ( filtered_servers) ;
348+ } else {
349+ debug ! ( "No usable DNS servers in {}, trying next" , path) ;
350+ }
351+ }
352+ Err ( e) => {
353+ debug ! ( "Failed to read {}: {}, trying next" , path, e) ;
354+ }
355+ }
356+ }
357+
358+ debug ! ( "No DNS servers found in any resolv.conf file" ) ;
359+ None
286360}
287361
288362/// Launch privileged container with QEMU+KVM for ephemeral VM, spawning as subprocess.
@@ -499,8 +573,24 @@ fn prepare_run_command_with_temp(
499573 cmd. args ( [ "-v" , & format ! ( "{}:/run/systemd-units:ro" , units_dir) ] ) ;
500574 }
501575
576+ // Read host DNS servers before entering container
577+ // QEMU's slirp will use these instead of container's unreachable bridge DNS servers
578+ let host_dns_servers = read_host_dns_servers ( ) . or_else ( || {
579+ // Fallback to public DNS if no usable DNS found in system configuration
580+ // This ensures DNS works even when host has broken/unreachable DNS config
581+ 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." ) ;
582+ Some ( vec ! [ "8.8.8.8" . to_string( ) , "1.1.1.1" . to_string( ) ] )
583+ } ) ;
584+
585+ if let Some ( ref dns) = host_dns_servers {
586+ debug ! ( "Using DNS servers for ephemeral VM: {:?}" , dns) ;
587+ }
588+
502589 // Pass configuration as JSON via BCK_CONFIG environment variable
503- let config = serde_json:: to_string ( & opts) . unwrap ( ) ;
590+ // Include host DNS servers in the config so they're available inside the container
591+ let mut opts_with_dns = opts. clone ( ) ;
592+ opts_with_dns. host_dns_servers = host_dns_servers;
593+ let config = serde_json:: to_string ( & opts_with_dns) . unwrap ( ) ;
504594 cmd. args ( [ "-e" , & format ! ( "BCK_CONFIG={config}" ) ] ) ;
505595
506596 // Handle --execute output files and virtio-serial devices
@@ -1229,6 +1319,26 @@ Options=
12291319 qemu_config. add_virtio_serial_out ( "org.bcvk.journal" , "/run/journal.log" . to_string ( ) , false ) ;
12301320 debug ! ( "Added virtio-serial device for journal streaming to /run/journal.log" ) ;
12311321
1322+ // Configure DNS servers from host's /etc/resolv.conf
1323+ // This fixes DNS resolution issues when QEMU runs inside containers.
1324+ // QEMU's slirp reads /etc/resolv.conf from the container's network namespace,
1325+ // which contains unreachable bridge DNS servers (e.g., 169.254.1.1, 10.x.y.z).
1326+ // Solution: Write public DNS servers to /etc/resolv.conf in the bwrap namespace.
1327+ // Safe because we're in an ephemeral container that will be destroyed.
1328+ if let Some ( dns_servers) = opts. host_dns_servers . clone ( ) {
1329+ let mut resolv_content = String :: new ( ) ;
1330+ for server in & dns_servers {
1331+ resolv_content. push_str ( & format ! ( "nameserver {}\n " , server) ) ;
1332+ }
1333+
1334+ std:: fs:: write ( "/etc/resolv.conf" , & resolv_content)
1335+ . context ( "Failed to write /etc/resolv.conf for QEMU slirp" ) ?;
1336+
1337+ debug ! ( "Configured DNS servers for QEMU slirp: {:?}" , dns_servers) ;
1338+ } else {
1339+ debug ! ( "No host DNS servers available, QEMU slirp will use container's resolv.conf" ) ;
1340+ }
1341+
12321342 if opts. common . ssh_keygen {
12331343 qemu_config. enable_ssh_access ( None ) ; // Use default port 2222
12341344 debug ! ( "Enabled SSH port forwarding: host port 2222 -> guest port 22" ) ;
0 commit comments