net: wait for async NIC attach instead of one-shot-exit (fixes no-eth0 boot)
driver-manager attaches NIC drivers asynchronously, so on a fresh boot the network.* scheme does not exist yet when smolnetd/dhcpd run (both gated only on the driver-manager service having *started*). smolnetd scanned /scheme once, found nothing, and exited -> /scheme/netcfg never came up -> dhcpd/netctl failed with "Can't open /scheme/netcfg/ifaces/eth0/mac" and DHCP timed out. - smolnetd: poll (bounded 20s) for a network.* adapter to appear before giving up; oneshot_async so this never blocks boot. Give-up log now distinguishes genuinely-NIC-less from a NIC-present-but-driver-failed (stale driver) case. - dhcpd: wait (bounded 20s) for /scheme/netcfg/ifaces/eth0/mac to appear before attempting DHCP, instead of one-shot "Can't open"; exit clean if no NIC.
This commit is contained in:
@@ -354,6 +354,22 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
// driver-manager attaches the NIC asynchronously, and smolnetd only brings up
|
||||
// /scheme/netcfg/ifaces/<iface> after it binds the adapter, so on a fresh boot
|
||||
// this path may not exist yet when dhcpd runs (10_dhcpd only requires_weak
|
||||
// that 10_smolnetd *started*). Wait (bounded) for the interface to appear
|
||||
// instead of failing immediately with "Can't open ...". A genuinely NIC-less
|
||||
// machine falls through after the window and exits cleanly (0), no error.
|
||||
let mac_path = format!("/scheme/netcfg/ifaces/{iface}/mac");
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20);
|
||||
while std::fs::File::open(&mac_path).is_err() {
|
||||
if std::time::Instant::now() >= deadline {
|
||||
eprintln!("dhcpd: {iface} never appeared in /scheme/netcfg (no NIC?); skipping DHCP");
|
||||
return;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(250));
|
||||
}
|
||||
|
||||
if let Err(err) = dhcp(iface, verbose) {
|
||||
eprintln!("dhcpd: {err}");
|
||||
process::exit(1);
|
||||
|
||||
+47
-11
@@ -35,22 +35,58 @@ fn get_all_network_adapters() -> Result<Vec<String>> {
|
||||
adapters.push(scheme);
|
||||
}
|
||||
|
||||
if adapters.is_empty() {
|
||||
// No NIC present is a normal state on a diskless/headless/bare machine,
|
||||
// or simply before an unsupported NIC's driver attaches. Report it as
|
||||
// info and let the caller exit cleanly instead of emitting a spurious
|
||||
// ERROR + non-zero exit on every boot of a NIC-less system.
|
||||
info!("no network adapter found; network stack idle");
|
||||
return Ok(adapters);
|
||||
}
|
||||
info!("Found {} network adapter(s): {:?}", adapters.len(), adapters);
|
||||
Ok(adapters)
|
||||
}
|
||||
|
||||
/// Wait for at least one NIC driver to register its `network.*` scheme.
|
||||
///
|
||||
/// driver-manager attaches drivers ASYNCHRONOUSLY, so on a real boot the NIC's
|
||||
/// `network.*` scheme does not exist yet when smolnetd starts (10_smolnetd only
|
||||
/// requires_weak that driver-manager has *started*). The previous code scanned
|
||||
/// `/scheme` exactly once and, finding nothing, exited — losing the race with
|
||||
/// virtio-netd and leaving `/scheme/netcfg` absent, so dhcpd/netctl failed with
|
||||
/// "Can't open /scheme/netcfg/ifaces/eth0/mac". Poll for a bounded window
|
||||
/// instead. smolnetd is oneshot_async, so this wait never blocks init/boot; a
|
||||
/// genuinely NIC-less machine simply gets an empty list after the window and
|
||||
/// exits cleanly.
|
||||
fn wait_for_network_adapters() -> Vec<String> {
|
||||
use std::time::{Duration, Instant};
|
||||
let deadline = Instant::now() + Duration::from_secs(20);
|
||||
let mut logged_wait = false;
|
||||
loop {
|
||||
match get_all_network_adapters() {
|
||||
Ok(adapters) if !adapters.is_empty() => {
|
||||
info!("Found {} network adapter(s): {:?}", adapters.len(), adapters);
|
||||
return adapters;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
return Vec::new();
|
||||
}
|
||||
if !logged_wait {
|
||||
info!("no network adapter yet; waiting for a NIC driver to attach...");
|
||||
logged_wait = true;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(250));
|
||||
}
|
||||
}
|
||||
|
||||
fn run(daemon: daemon::Daemon) -> Result<()> {
|
||||
let adapters = get_all_network_adapters()?;
|
||||
let adapters = wait_for_network_adapters();
|
||||
if adapters.is_empty() {
|
||||
// Nothing to service; exit cleanly (see get_all_network_adapters).
|
||||
// No `network.*` scheme registered within the wait window. Two cases:
|
||||
// (a) genuinely NIC-less machine -> expected, harmless;
|
||||
// (b) a NIC IS present but its driver never registered `network.*`
|
||||
// -> a driver bug or a STALE/broken NIC driver binary that no
|
||||
// longer speaks the current scheme protocol.
|
||||
// If this box has a NIC, this is case (b): rebuild the net driver
|
||||
// (virtio-netd / e1000d / rtl8168d). Exit cleanly either way (smolnetd
|
||||
// is oneshot_async, so waiting never blocked boot/login).
|
||||
warn!(
|
||||
"no network.* scheme after waiting; network stack idle. If a NIC is \
|
||||
present, its driver failed to register it (driver bug or stale driver binary)."
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
trace!("found {} network adapter(s)", adapters.len());
|
||||
|
||||
Reference in New Issue
Block a user