From 2e5e51658763e949429511dcf5144c0fde8904b7 Mon Sep 17 00:00:00 2001 From: Red Bear OS Date: Sat, 25 Jul 2026 16:02:50 +0900 Subject: [PATCH] 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. --- dhcpd/src/main.rs | 16 ++++++++++++ netstack/src/main.rs | 58 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 63 insertions(+), 11 deletions(-) diff --git a/dhcpd/src/main.rs b/dhcpd/src/main.rs index e8b1707acf..27c3a8baae 100644 --- a/dhcpd/src/main.rs +++ b/dhcpd/src/main.rs @@ -354,6 +354,22 @@ fn main() { } } + // driver-manager attaches the NIC asynchronously, and smolnetd only brings up + // /scheme/netcfg/ifaces/ 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); diff --git a/netstack/src/main.rs b/netstack/src/main.rs index 905e679432..e614293329 100644 --- a/netstack/src/main.rs +++ b/netstack/src/main.rs @@ -35,22 +35,58 @@ fn get_all_network_adapters() -> Result> { 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 { + 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());