redbear-netctl: wait for interface to appear before waiting for DHCP address

wait_for_address assumed eth0 already existed and polled for an address with a
1s window -> on a fresh boot (smolnetd/driver-manager bring the interface up
async) it spun on a non-existent interface and logged "timed out waiting for
DHCP address on eth0". Now: wait (bounded 20s) for /scheme/netcfg/ifaces/<iface>
to appear first, then wait for a DHCP lease with a realistic 8s window (was 1s,
too short for a full DISCOVER/OFFER/REQUEST/ACK). Both bounded + oneshot_async so
boot never blocks. Pairs with the smolnetd/dhcpd async-NIC-attach fix.
This commit is contained in:
2026-07-25 16:04:04 +09:00
parent 41c5926790
commit c3f8c89856
@@ -542,7 +542,10 @@ fn dhcp_wait_timeout() -> Duration {
.ok()
.and_then(|value| value.parse::<u64>().ok())
.map(Duration::from_millis)
.unwrap_or_else(|| Duration::from_millis(1000))
// 8s: a real DHCP DISCOVER/OFFER/REQUEST/ACK handshake over a freshly
// brought-up link needs more than the old 1s (which expired before the
// lease ever arrived).
.unwrap_or_else(|| Duration::from_millis(8000))
}
fn dhcp_poll_interval() -> Duration {
@@ -553,10 +556,42 @@ fn dhcp_poll_interval() -> Duration {
.unwrap_or_else(|| Duration::from_millis(50))
}
fn iface_appear_timeout() -> Duration {
env::var("REDBEAR_NETCTL_IFACE_WAIT_MS")
.ok()
.and_then(|value| value.parse::<u64>().ok())
.map(Duration::from_millis)
.unwrap_or_else(|| Duration::from_millis(20_000))
}
fn interface_exists(interface: &str) -> bool {
netcfg_root()
.join("ifaces")
.join(interface)
.join("mac")
.exists()
}
fn wait_for_address(interface: &str) -> Result<(), String> {
let deadline = Instant::now() + dhcp_wait_timeout();
let poll = dhcp_poll_interval();
// The NIC driver + smolnetd bring /scheme/netcfg/ifaces/<iface> up
// ASYNCHRONOUSLY (driver-manager attaches drivers after this service
// starts), so on a fresh boot the interface may not exist yet. Wait for it
// to appear FIRST, then for a DHCP-assigned address. Both bounded so a
// NIC-less / no-DHCP machine still returns (this runs oneshot_async, so the
// wait never blocks boot/login).
let iface_deadline = Instant::now() + iface_appear_timeout();
while !interface_exists(interface) {
if Instant::now() >= iface_deadline {
return Err(format!(
"interface {interface} never appeared in /scheme/netcfg (no NIC, or its driver failed to attach)"
));
}
thread::sleep(poll);
}
let deadline = Instant::now() + dhcp_wait_timeout();
loop {
match current_addr(interface).as_deref() {
Some(addr) if addr != "Not configured" && !addr.is_empty() => return Ok(()),