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:
Red Bear OS
2026-07-25 16:02:50 +09:00
parent b906ad688a
commit 2e5e516587
2 changed files with 63 additions and 11 deletions
+47 -11
View File
@@ -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());