Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions crates/integration-tests/src/tests/run_ephemeral_ssh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -358,3 +358,65 @@ 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<()> {
// Wait for network interface to be ready
let network_ready = run_bcvk(&[
"ephemeral",
"run-ssh",
"--label",
INTEGRATION_TEST_LABEL,
&get_test_image(),
"--",
"/bin/sh",
"-c",
r#"
for i in $(seq 1 30); do
ip -4 addr show | grep -q "inet " && break
sleep 1
done
"#,
])?;

assert!(
network_ready.success(),
"Network interface not ready: stdout: {}\nstderr: {}",
network_ready.stdout,
network_ready.stderr
);
// Use curl or wget, whichever is available
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 -sSf --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);
61 changes: 44 additions & 17 deletions crates/kit/src/qemu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,17 @@ pub enum NetworkMode {
User {
/// Port forwarding rules: "tcp::2222-:22" format
hostfwd: Vec<String>,
/// DNS servers to use (if None, QEMU's default 10.0.2.3 will be used)
dns_servers: Option<Vec<String>>,
},
}

impl Default for NetworkMode {
fn default() -> Self {
NetworkMode::User { hostfwd: vec![] }
NetworkMode::User {
hostfwd: vec![],
dns_servers: None,
}
}
}

Expand Down Expand Up @@ -322,8 +327,13 @@ impl QemuConfig {
pub fn enable_ssh_access(&mut self, host_port: Option<u16>) -> &mut Self {
let port = host_port.unwrap_or(2222); // Default to port 2222 on host
let hostfwd = format!("tcp::{}-:22", port); // Forward host port to guest port 22
// Preserve existing DNS servers if any
let dns_servers = match &self.network_mode {
NetworkMode::User { dns_servers, .. } => dns_servers.clone(),
};
self.network_mode = NetworkMode::User {
hostfwd: vec![hostfwd],
dns_servers,
};
self
}
Expand Down Expand Up @@ -522,23 +532,40 @@ 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",
]);
NetworkMode::User {
hostfwd,
dns_servers,
} => {
let mut netdev_parts = vec!["user".to_string(), "id=net0".to_string()];

// Add DNS server if specified
// QEMU's dns= parameter only accepts a single IP address, so use the first one
if let Some(dns_list) = dns_servers {
if let Some(first_dns) = dns_list.first() {
let dns_arg = format!("dns={}", first_dns);
netdev_parts.push(dns_arg);
if dns_list.len() > 1 {
debug!(
"QEMU dns= parameter only accepts a single IP, using first DNS server: {} (ignoring {} additional servers)",
first_dns,
dns_list.len() - 1
);
}
}
}

// 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",
]);
}
}

Expand Down
86 changes: 85 additions & 1 deletion crates/kit/src/run_ephemeral.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,50 @@ pub struct RunEphemeralOpts {

#[clap(long = "karg", help = "Additional kernel command line arguments")]
pub kernel_args: Vec<String>,

/// Host DNS servers (read on host, passed to container for QEMU configuration)
/// 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<Vec<String>>,
}

/// Parse DNS servers from resolv.conf format content
fn parse_resolv_conf(content: &str) -> Vec<String> {
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 /etc/resolv.conf
/// Returns a vector of DNS server IP addresses, or None if unable to read/parse
fn read_host_dns_servers() -> Option<Vec<String>> {
let resolv_conf = match std::fs::read_to_string("/etc/resolv.conf") {
Ok(content) => content,
Err(e) => {
debug!("Failed to read /etc/resolv.conf: {}", e);
return None;
}
};

let dns_servers = parse_resolv_conf(&resolv_conf);

if dns_servers.is_empty() {
debug!("No DNS servers found in /etc/resolv.conf");
None
} else {
debug!("Found DNS servers: {:?}", dns_servers);
Some(dns_servers)
}
}

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

// Read host DNS servers before entering container
// QEMU's slirp will use these instead of container's unreachable bridge DNS servers
let host_dns_servers = read_host_dns_servers();
if let Some(ref dns) = host_dns_servers {
debug!("Read host DNS servers: {:?}", dns);
} else {
debug!("No DNS servers found in host /etc/resolv.conf, QEMU will use default 10.0.2.3");
}

// 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
Expand Down Expand Up @@ -1229,6 +1285,34 @@ 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");

// Configure DNS servers from host's /etc/resolv.conf
// This fixes DNS resolution issues when QEMU runs inside containers.
// QEMU's slirp reads /etc/resolv.conf from the container's network namespace,
// which contains unreachable bridge DNS servers (e.g., 169.254.1.1, 10.x.y.z).
// By passing host DNS servers via QEMU's dns= parameter, we bypass slirp's
// resolv.conf reading and use the host's actual DNS servers.
let dns_servers = opts.host_dns_servers.clone();
if let Some(ref dns) = dns_servers {
debug!(
"Using host DNS servers (from host /etc/resolv.conf): {:?}",
dns
);
} else {
debug!("No host DNS servers available, QEMU will use default 10.0.2.3");
}

// Configure DNS servers in network mode
if let Some(ref dns) = dns_servers {
match &mut qemu_config.network_mode {
crate::qemu::NetworkMode::User {
dns_servers: dns_opt,
..
} => {
*dns_opt = Some(dns.clone());
}
}
}

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");
Expand Down
1 change: 1 addition & 0 deletions crates/kit/src/to_disk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading