Skip to content

Commit eb89500

Browse files
committed
Fix DNS resolution in ephemeral guests
Configure QEMU user-mode networking to use host DNS servers from /etc/resolv.conf instead of the default 10.0.2.3, which doesn't work when QEMU runs inside containers. Signed-off-by: gursewak1997 <[email protected]>
1 parent b664ecb commit eb89500

File tree

4 files changed

+192
-18
lines changed

4 files changed

+192
-18
lines changed

crates/integration-tests/src/tests/run_ephemeral_ssh.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,3 +358,65 @@ fn test_run_ephemeral_ssh_broken_image_cleanup() -> Result<()> {
358358
Ok(())
359359
}
360360
integration_test!(test_run_ephemeral_ssh_broken_image_cleanup);
361+
362+
/// Test ephemeral VM network and DNS
363+
///
364+
/// Verifies that ephemeral bootc VMs can access the network and resolve DNS correctly.
365+
/// Uses HTTP request to quay.io to test both DNS resolution and network connectivity.
366+
fn test_run_ephemeral_dns_resolution() -> Result<()> {
367+
// Wait for network interface to be ready
368+
let network_ready = run_bcvk(&[
369+
"ephemeral",
370+
"run-ssh",
371+
"--label",
372+
INTEGRATION_TEST_LABEL,
373+
&get_test_image(),
374+
"--",
375+
"/bin/sh",
376+
"-c",
377+
r#"
378+
for i in $(seq 1 30); do
379+
ip -4 addr show | grep -q "inet " && break
380+
sleep 1
381+
done
382+
"#,
383+
])?;
384+
385+
assert!(
386+
network_ready.success(),
387+
"Network interface not ready: stdout: {}\nstderr: {}",
388+
network_ready.stdout,
389+
network_ready.stderr
390+
);
391+
// Use curl or wget, whichever is available
392+
let network_test = run_bcvk(&[
393+
"ephemeral",
394+
"run-ssh",
395+
"--label",
396+
INTEGRATION_TEST_LABEL,
397+
&get_test_image(),
398+
"--",
399+
"/bin/sh",
400+
"-c",
401+
r#"
402+
if command -v curl >/dev/null 2>&1; then
403+
curl -sSf --max-time 10 https://quay.io/v2/ >/dev/null
404+
elif command -v wget >/dev/null 2>&1; then
405+
wget -q --timeout=10 -O /dev/null https://quay.io/v2/
406+
else
407+
echo "Neither curl nor wget available"
408+
exit 1
409+
fi
410+
"#,
411+
])?;
412+
413+
assert!(
414+
network_test.success(),
415+
"Network connectivity test (HTTP request to quay.io) failed: stdout: {}\nstderr: {}",
416+
network_test.stdout,
417+
network_test.stderr
418+
);
419+
420+
Ok(())
421+
}
422+
integration_test!(test_run_ephemeral_dns_resolution);

crates/kit/src/qemu.rs

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,17 @@ pub enum NetworkMode {
8080
User {
8181
/// Port forwarding rules: "tcp::2222-:22" format
8282
hostfwd: Vec<String>,
83+
/// DNS servers to use (if None, QEMU's default 10.0.2.3 will be used)
84+
dns_servers: Option<Vec<String>>,
8385
},
8486
}
8587

8688
impl Default for NetworkMode {
8789
fn default() -> Self {
88-
NetworkMode::User { hostfwd: vec![] }
90+
NetworkMode::User {
91+
hostfwd: vec![],
92+
dns_servers: None,
93+
}
8994
}
9095
}
9196

@@ -322,8 +327,13 @@ impl QemuConfig {
322327
pub fn enable_ssh_access(&mut self, host_port: Option<u16>) -> &mut Self {
323328
let port = host_port.unwrap_or(2222); // Default to port 2222 on host
324329
let hostfwd = format!("tcp::{}-:22", port); // Forward host port to guest port 22
330+
// Preserve existing DNS servers if any
331+
let dns_servers = match &self.network_mode {
332+
NetworkMode::User { dns_servers, .. } => dns_servers.clone(),
333+
};
325334
self.network_mode = NetworkMode::User {
326335
hostfwd: vec![hostfwd],
336+
dns_servers,
327337
};
328338
self
329339
}
@@ -522,23 +532,40 @@ fn spawn(
522532

523533
// Configure network (only User mode supported now)
524534
match &config.network_mode {
525-
NetworkMode::User { hostfwd } => {
526-
if hostfwd.is_empty() {
527-
cmd.args([
528-
"-netdev",
529-
"user,id=net0",
530-
"-device",
531-
"virtio-net-pci,netdev=net0",
532-
]);
533-
} else {
534-
let hostfwd_arg = format!("user,id=net0,hostfwd={}", hostfwd.join(",hostfwd="));
535-
cmd.args([
536-
"-netdev",
537-
&hostfwd_arg,
538-
"-device",
539-
"virtio-net-pci,netdev=net0",
540-
]);
535+
NetworkMode::User {
536+
hostfwd,
537+
dns_servers,
538+
} => {
539+
let mut netdev_parts = vec!["user".to_string(), "id=net0".to_string()];
540+
541+
// Add DNS server if specified
542+
// QEMU's dns= parameter only accepts a single IP address, so use the first one
543+
if let Some(dns_list) = dns_servers {
544+
if let Some(first_dns) = dns_list.first() {
545+
let dns_arg = format!("dns={}", first_dns);
546+
netdev_parts.push(dns_arg);
547+
if dns_list.len() > 1 {
548+
debug!(
549+
"QEMU dns= parameter only accepts a single IP, using first DNS server: {} (ignoring {} additional servers)",
550+
first_dns,
551+
dns_list.len() - 1
552+
);
553+
}
554+
}
541555
}
556+
557+
// Add port forwarding rules
558+
for fwd in hostfwd {
559+
netdev_parts.push(format!("hostfwd={}", fwd));
560+
}
561+
562+
let netdev_arg = netdev_parts.join(",");
563+
cmd.args([
564+
"-netdev",
565+
&netdev_arg,
566+
"-device",
567+
"virtio-net-pci,netdev=net0",
568+
]);
542569
}
543570
}
544571

crates/kit/src/run_ephemeral.rs

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,50 @@ 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 /etc/resolv.conf
311+
/// Returns a vector of DNS server IP addresses, or None if unable to read/parse
312+
fn read_host_dns_servers() -> Option<Vec<String>> {
313+
let resolv_conf = match std::fs::read_to_string("/etc/resolv.conf") {
314+
Ok(content) => content,
315+
Err(e) => {
316+
debug!("Failed to read /etc/resolv.conf: {}", e);
317+
return None;
318+
}
319+
};
320+
321+
let dns_servers = parse_resolv_conf(&resolv_conf);
322+
323+
if dns_servers.is_empty() {
324+
debug!("No DNS servers found in /etc/resolv.conf");
325+
None
326+
} else {
327+
debug!("Found DNS servers: {:?}", dns_servers);
328+
Some(dns_servers)
329+
}
286330
}
287331

288332
/// Launch privileged container with QEMU+KVM for ephemeral VM, spawning as subprocess.
@@ -499,8 +543,20 @@ fn prepare_run_command_with_temp(
499543
cmd.args(["-v", &format!("{}:/run/systemd-units:ro", units_dir)]);
500544
}
501545

546+
// Read host DNS servers before entering container
547+
// QEMU's slirp will use these instead of container's unreachable bridge DNS servers
548+
let host_dns_servers = read_host_dns_servers();
549+
if let Some(ref dns) = host_dns_servers {
550+
debug!("Read host DNS servers: {:?}", dns);
551+
} else {
552+
debug!("No DNS servers found in host /etc/resolv.conf, QEMU will use default 10.0.2.3");
553+
}
554+
502555
// Pass configuration as JSON via BCK_CONFIG environment variable
503-
let config = serde_json::to_string(&opts).unwrap();
556+
// Include host DNS servers in the config so they're available inside the container
557+
let mut opts_with_dns = opts.clone();
558+
opts_with_dns.host_dns_servers = host_dns_servers;
559+
let config = serde_json::to_string(&opts_with_dns).unwrap();
504560
cmd.args(["-e", &format!("BCK_CONFIG={config}")]);
505561

506562
// Handle --execute output files and virtio-serial devices
@@ -1229,6 +1285,34 @@ Options=
12291285
qemu_config.add_virtio_serial_out("org.bcvk.journal", "/run/journal.log".to_string(), false);
12301286
debug!("Added virtio-serial device for journal streaming to /run/journal.log");
12311287

1288+
// Configure DNS servers from host's /etc/resolv.conf
1289+
// This fixes DNS resolution issues when QEMU runs inside containers.
1290+
// QEMU's slirp reads /etc/resolv.conf from the container's network namespace,
1291+
// which contains unreachable bridge DNS servers (e.g., 169.254.1.1, 10.x.y.z).
1292+
// By passing host DNS servers via QEMU's dns= parameter, we bypass slirp's
1293+
// resolv.conf reading and use the host's actual DNS servers.
1294+
let dns_servers = opts.host_dns_servers.clone();
1295+
if let Some(ref dns) = dns_servers {
1296+
debug!(
1297+
"Using host DNS servers (from host /etc/resolv.conf): {:?}",
1298+
dns
1299+
);
1300+
} else {
1301+
debug!("No host DNS servers available, QEMU will use default 10.0.2.3");
1302+
}
1303+
1304+
// Configure DNS servers in network mode
1305+
if let Some(ref dns) = dns_servers {
1306+
match &mut qemu_config.network_mode {
1307+
crate::qemu::NetworkMode::User {
1308+
dns_servers: dns_opt,
1309+
..
1310+
} => {
1311+
*dns_opt = Some(dns.clone());
1312+
}
1313+
}
1314+
}
1315+
12321316
if opts.common.ssh_keygen {
12331317
qemu_config.enable_ssh_access(None); // Use default port 2222
12341318
debug!("Enabled SSH port forwarding: host port 2222 -> guest port 22");

crates/kit/src/to_disk.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@ pub fn run(opts: ToDiskOpts) -> Result<()> {
430430
// - Attach target disk via virtio-blk
431431
// - Disable networking (using local storage only)
432432
let ephemeral_opts = RunEphemeralOpts {
433+
host_dns_servers: None,
433434
image: opts.get_installer_image().to_string(),
434435
common: common_opts,
435436
podman: crate::run_ephemeral::CommonPodmanOptions {

0 commit comments

Comments
 (0)