From b91195d22a01b6e20904fb9caf4ec7d2a578af3e Mon Sep 17 00:00:00 2001 From: gursewak1997 Date: Mon, 1 Dec 2025 15:06:03 -0800 Subject: [PATCH] Fix DNS resolution in ephemeral guests QEMU's slirp reads /etc/resolv.conf from the container namespace, which contains unreachable bridge DNS servers. On systemd-resolved hosts, it only has 127.0.0.53 (stub resolver). Read upstream DNS servers from host's /run/systemd/resolve/resolv.conf, pass them to the container, and write /etc/resolv.conf before starting QEMU. Signed-off-by: gursewak1997 --- .../src/tests/run_ephemeral_ssh.rs | 40 ++++++ crates/kit/scripts/entrypoint.sh | 6 + crates/kit/src/qemu.rs | 28 ++--- crates/kit/src/run_ephemeral.rs | 119 +++++++++++++++++- crates/kit/src/to_disk.rs | 1 + 5 files changed, 177 insertions(+), 17 deletions(-) diff --git a/crates/integration-tests/src/tests/run_ephemeral_ssh.rs b/crates/integration-tests/src/tests/run_ephemeral_ssh.rs index a1f6067..8a4d025 100644 --- a/crates/integration-tests/src/tests/run_ephemeral_ssh.rs +++ b/crates/integration-tests/src/tests/run_ephemeral_ssh.rs @@ -358,3 +358,43 @@ fn test_run_ephemeral_ssh_broken_image_cleanup() -> Result<()> { Ok(()) } integration_test!(test_run_ephemeral_ssh_broken_image_cleanup); + +/// Test ephemeral VM network and DNS +/// +/// Verifies that ephemeral bootc VMs can access the network and resolve DNS correctly. +/// Uses HTTP request to quay.io to test both DNS resolution and network connectivity. +fn test_run_ephemeral_dns_resolution() -> Result<()> { + // Test DNS + network by connecting to quay.io + // Use curl or wget, whichever is available + // Any HTTP response (including 401) proves DNS resolution and network connectivity work + let network_test = run_bcvk(&[ + "ephemeral", + "run-ssh", + "--label", + INTEGRATION_TEST_LABEL, + &get_test_image(), + "--", + "/bin/sh", + "-c", + r#" + if command -v curl >/dev/null 2>&1; then + curl -sS --max-time 10 https://quay.io/v2/ >/dev/null + elif command -v wget >/dev/null 2>&1; then + wget -q --timeout=10 -O /dev/null https://quay.io/v2/ + else + echo "Neither curl nor wget available" + exit 1 + fi + "#, + ])?; + + assert!( + network_test.success(), + "Network connectivity test (HTTP request to quay.io) failed: stdout: {}\nstderr: {}", + network_test.stdout, + network_test.stderr + ); + + Ok(()) +} +integration_test!(test_run_ephemeral_dns_resolution); diff --git a/crates/kit/scripts/entrypoint.sh b/crates/kit/scripts/entrypoint.sh index e4315e3..06474ff 100644 --- a/crates/kit/scripts/entrypoint.sh +++ b/crates/kit/scripts/entrypoint.sh @@ -25,6 +25,12 @@ init_tmproot() { # Ensure we have /etc/passwd as ssh-keygen wants it for bad reasons systemd-sysusers --root $(pwd) &>/dev/null + # Copy DNS configuration from container's /etc/resolv.conf (configured by podman --dns) + # into the bwrap namespace so QEMU's slirp can use it for DNS resolution + if [ -f /etc/resolv.conf ]; then + cp /etc/resolv.conf /run/tmproot/etc/resolv.conf + fi + # Shared directory between containers mkdir /run/inner-shared } diff --git a/crates/kit/src/qemu.rs b/crates/kit/src/qemu.rs index 39a69e7..a12207b 100644 --- a/crates/kit/src/qemu.rs +++ b/crates/kit/src/qemu.rs @@ -523,22 +523,20 @@ fn spawn( // Configure network (only User mode supported now) match &config.network_mode { NetworkMode::User { hostfwd } => { - if hostfwd.is_empty() { - cmd.args([ - "-netdev", - "user,id=net0", - "-device", - "virtio-net-pci,netdev=net0", - ]); - } else { - let hostfwd_arg = format!("user,id=net0,hostfwd={}", hostfwd.join(",hostfwd=")); - cmd.args([ - "-netdev", - &hostfwd_arg, - "-device", - "virtio-net-pci,netdev=net0", - ]); + let mut netdev_parts = vec!["user".to_string(), "id=net0".to_string()]; + + // Add port forwarding rules + for fwd in hostfwd { + netdev_parts.push(format!("hostfwd={}", fwd)); } + + let netdev_arg = netdev_parts.join(","); + cmd.args([ + "-netdev", + &netdev_arg, + "-device", + "virtio-net-pci,netdev=net0", + ]); } } diff --git a/crates/kit/src/run_ephemeral.rs b/crates/kit/src/run_ephemeral.rs index 13eaf7e..d4277b0 100644 --- a/crates/kit/src/run_ephemeral.rs +++ b/crates/kit/src/run_ephemeral.rs @@ -100,7 +100,7 @@ use color_eyre::Result; use rustix::path::Arg; use serde::{Deserialize, Serialize}; use tokio::io::AsyncReadExt; -use tracing::debug; +use tracing::{debug, warn}; const ENTRYPOINT: &str = "/var/lib/bcvk/entrypoint"; @@ -283,6 +283,87 @@ pub struct RunEphemeralOpts { #[clap(long = "karg", help = "Additional kernel command line arguments")] pub kernel_args: Vec, + + /// Host DNS servers (read on host, configured via podman --dns flags) + /// Not a CLI option - populated automatically from host's /etc/resolv.conf + #[clap(skip)] + #[serde(skip_serializing_if = "Option::is_none")] + pub host_dns_servers: Option>, +} + +/// Parse DNS servers from resolv.conf format content +fn parse_resolv_conf(content: &str) -> Vec { + let mut dns_servers = Vec::new(); + for line in content.lines() { + let line = line.trim(); + // Parse lines like "nameserver 8.8.8.8" or "nameserver 2001:4860:4860::8888" + if let Some(server) = line.strip_prefix("nameserver ") { + let server = server.trim(); + if !server.is_empty() { + dns_servers.push(server.to_string()); + } + } + } + dns_servers +} + +/// Read DNS servers from host's resolv.conf +/// Returns a vector of DNS server IP addresses, or None if unable to read/parse +/// +/// For systemd-resolved systems, reads from /run/systemd/resolve/resolv.conf +/// which contains actual upstream DNS servers, not the stub resolver (127.0.0.53). +/// Falls back to /etc/resolv.conf for non-systemd-resolved systems. +fn read_host_dns_servers() -> Option> { + // Try systemd-resolved's upstream DNS file first + // This avoids reading 127.0.0.53 (stub resolver) from /etc/resolv.conf + let paths = [ + "/run/systemd/resolve/resolv.conf", // systemd-resolved upstream servers + "/etc/resolv.conf", // traditional or fallback + ]; + + for path in &paths { + match std::fs::read_to_string(path) { + 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. + 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() + } 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() + && !(ip.segments()[0] & 0xffc0 == 0xfe80) // link-local fe80::/10 + && !(ip.segments()[0] & 0xfe00 == 0xfc00) // ULA fc00::/7 (private) + } else { + false // Reject invalid addresses + } + }) + .collect(); + + if !filtered_servers.is_empty() { + debug!("Found DNS servers from {}: {:?}", path, filtered_servers); + return Some(filtered_servers); + } else { + debug!("No usable DNS servers in {}, trying next", path); + } + } + Err(e) => { + debug!("Failed to read {}: {}, trying next", path, e); + } + } + } + + debug!("No DNS servers found in any resolv.conf file"); + None } /// Launch privileged container with QEMU+KVM for ephemeral VM, spawning as subprocess. @@ -499,8 +580,32 @@ fn prepare_run_command_with_temp( cmd.args(["-v", &format!("{}:/run/systemd-units:ro", units_dir)]); } + // Read host DNS servers and configure them via podman --dns flags + // This fixes DNS resolution issues when QEMU runs inside containers. + // 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()]) + }); + + if let Some(ref dns) = host_dns_servers { + debug!("Using DNS servers for ephemeral VM: {:?}", dns); + // Configure DNS servers for the container using --dns flags + // This properly sets up /etc/resolv.conf in the container's network namespace + for server in dns { + cmd.args(["--dns", server]); + } + } + // Pass configuration as JSON via BCK_CONFIG environment variable - let config = serde_json::to_string(&opts).unwrap(); + // Include host DNS servers in the config so they're available inside the container + let mut opts_with_dns = opts.clone(); + opts_with_dns.host_dns_servers = host_dns_servers; + let config = serde_json::to_string(&opts_with_dns).unwrap(); cmd.args(["-e", &format!("BCK_CONFIG={config}")]); // Handle --execute output files and virtio-serial devices @@ -1229,6 +1334,16 @@ Options= qemu_config.add_virtio_serial_out("org.bcvk.journal", "/run/journal.log".to_string(), false); debug!("Added virtio-serial device for journal streaming to /run/journal.log"); + // DNS is configured via podman --dns flags (see prepare_run_command_with_temp) + // This fixes DNS resolution issues when QEMU runs inside containers. + // QEMU's slirp reads /etc/resolv.conf from the container's network namespace, + // and podman properly sets it up using --dns instead of relying on bridge DNS. + if let Some(ref dns_servers) = opts.host_dns_servers { + debug!("DNS servers configured for QEMU slirp: {:?}", dns_servers); + } else { + warn!("No host DNS servers available, QEMU slirp will use container's resolv.conf which may not work"); + } + if opts.common.ssh_keygen { qemu_config.enable_ssh_access(None); // Use default port 2222 debug!("Enabled SSH port forwarding: host port 2222 -> guest port 22"); diff --git a/crates/kit/src/to_disk.rs b/crates/kit/src/to_disk.rs index 486d8a2..e33b492 100644 --- a/crates/kit/src/to_disk.rs +++ b/crates/kit/src/to_disk.rs @@ -430,6 +430,7 @@ pub fn run(opts: ToDiskOpts) -> Result<()> { // - Attach target disk via virtio-blk // - Disable networking (using local storage only) let ephemeral_opts = RunEphemeralOpts { + host_dns_servers: None, image: opts.get_installer_image().to_string(), common: common_opts, podman: crate::run_ephemeral::CommonPodmanOptions {