From 7adbf41f591c7017f21f26326d08e2acac065cf0 Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Wed, 28 Oct 2026 01:00:08 +0100 Subject: [PATCH] fix(rootfs): prefer the uplink resolver, drop loopback nameservers Copying /etc/resolv.conf verbatim gives the guest systemd-resolved's stub at 127.0.0.53, and loopback never crosses the engine's socket proxy: the name lookup dies inside the guest. Source the uplink config from /run/systemd/resolve first and strip 127.x nameserver lines from whatever is copied. --- crates/ecrou/src/rootfs.rs | 59 ++++++++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/crates/ecrou/src/rootfs.rs b/crates/ecrou/src/rootfs.rs index cc48e39..22102c6 100644 --- a/crates/ecrou/src/rootfs.rs +++ b/crates/ecrou/src/rootfs.rs @@ -303,20 +303,33 @@ fn extract_prepared( /// guest has whatever the image shipped, which is usually nothing. /// Best effort: a rootfs with its own resolver config keeps it. fn provision_resolver(rootfs: &Path) { - let host = match std::fs::read_to_string("/etc/resolv.conf") { - Ok(s) if !s.trim().is_empty() => s, + // The engine proxies guest sockets to the host's stack, so any + // host-reachable nameserver works in the machine - but loopback + // addresses do not cross: 127.0.0.53 stays inside the guest, where + // nothing listens. So the source of truth is systemd-resolved's + // uplink config when present, and loopback nameservers are dropped + // from whatever is copied. Best effort: failures leave the + // image's file. + let host = match std::fs::read_to_string("/run/systemd/resolve/resolv.conf") + .or_else(|_| std::fs::read_to_string("/etc/resolv.conf")) + { + Ok(s) if !s.trim().is_empty() => drop_loopback_nameservers(&s), Ok(_) => { - log::debug!("host /etc/resolv.conf is empty; leaving the image's in place"); + log::debug!("host resolver config is empty; leaving the image's in place"); return; } Err(e) => { log::debug!( - "no host /etc/resolv.conf ({}); leaving the image's in place", + "no host resolver config ({}); leaving the image's in place", e ); return; } }; + if !host.contains("nameserver ") { + log::debug!("host resolver config has no usable nameserver; leaving the image's"); + return; + } let target = rootfs.join("etc/resolv.conf"); if let Err(e) = std::fs::remove_file(&target) { if e.kind() != std::io::ErrorKind::NotFound { @@ -328,11 +341,30 @@ fn provision_resolver(rootfs: &Path) { let _ = std::fs::create_dir_all(parent); } match std::fs::write(&target, host) { - Ok(()) => log::debug!("provisioned the host resolver into {}", target.display()), + Ok(()) => log::debug!("provisioned a resolver into {}", target.display()), Err(e) => log::debug!("could not write {}: {}", target.display(), e), } } +/// Lines naming a loopback nameserver go: the guest's loopback is its +/// own, and the stub resolvers live on the host's. +fn drop_loopback_nameservers(config: &str) -> String { + config + .lines() + .filter(|line| { + let rest = match line.trim_start().strip_prefix("nameserver ") { + Some(r) => r.trim(), + None => return true, + }; + !rest.starts_with("127.") + }) + .fold(String::new(), |mut acc, line| { + acc.push_str(line); + acc.push('\n'); + acc + }) +} + /// Write a tarball of `rootfs` at `dest`, compressed according to dest's /// extension (.tar.gz, .tar.xz, .tar.zst, plain .tar), atomically via a /// `.partial` file. @@ -481,6 +513,23 @@ fn get_tarball_extension(url: &str) -> &str { #[cfg(test)] mod tests { + use super::drop_loopback_nameservers; + + #[test] + fn loopback_nameservers_dropped() { + let config = "# comment\nnameserver 127.0.0.53\noptions edns0\nnameserver 192.168.1.1\nnameserver 127.0.0.1\n"; + assert_eq!( + drop_loopback_nameservers(config), + "# comment\noptions edns0\nnameserver 192.168.1.1\n" + ); + // A search line whose address-ish token is untouched. + let config = "search 127.example\nnameserver 10.0.0.1\n"; + assert_eq!( + drop_loopback_nameservers(config), + "search 127.example\nnameserver 10.0.0.1\n" + ); + } + use super::*; use crate::progress::NoProgress; use std::os::unix::fs::PermissionsExt;