Binding to a freshly-assigned IPv6 address fails with an opaque `Address not available` (tentative address / DAD), no retry or hint
Summary
Calling Endpoint::builder(...).bind_addr("[<ipv6>]:<port>") with an IPv6 address that was assigned to the interface moments earlier fails with:
Error: failed to bind iroh endpoint
0: Failed to bind sockets
1: Address not available (os error 99) // EADDRNOTAVAILThe address is present on the interface (scope global), but a freshly-added IPv6 address is tentative until Duplicate Address Detection (DAD) completes (~1–2 s on Linux), and bind(2) to a tentative address returns EADDRNOTAVAIL. Once DAD finishes, the same bind succeeds.
This is partly just how IPv6 works, so it may not be iroh's bug to "fix" outright — but the developer experience is rough: the failure is a bare OS error with no indication that the address is tentative or that retrying shortly would succeed, and iroh makes no attempt to tolerate a recoverable, common startup race.
Environment
- iroh
1.0.0 - Reproduced on Linux (armv7 container); the tentative-address behavior is standard Linux IPv6.
Repro
ip -6 addr add fd00:1234::5/64 dev eth0 # starts tentative; DAD pending
# bind immediately, before DAD completes:let ep = Endpoint::builder(presets::Minimal)
.relay_mode(RelayMode::Disabled)
.bind_addr("[fd00:1234::5]:45888".parse().unwrap())?
.bind()
.await?; // <-- Err: Failed to bind sockets / Address not available (os error 99)Bind a second or two later (after DAD) and it succeeds. Assigning the address nodad, or enabling optimistic DAD (net.ipv6.conf.<if>.optimistic_dad=1), also makes the immediate bind succeed.
Expected / nicer behaviors (any one would help)
- Clearer error — when a user-specified bind address fails with
EADDRNOTAVAIL, mention that the address may be tentative (IPv6 DAD) and a brief retry may succeed, instead of a bare "Address not available". This alone would save a lot of head-scratching. - Optional short retry — for an explicitly user-supplied
bind_addr, retry the bind a few times over ~1–2 s before giving up, so a node that self-assigns its bind address at startup doesn't lose a race with DAD. - Docs — note that an explicit IPv6
bind_addrmust already have passed DAD (or be addednodad/ optimistic).
Context
Hit while bringing up a relay-less LAN mesh where each node self-assigns its own address at startup and then binds the endpoint to it. The address assignment and the bind happen back-to-back in the same process, so the DAD window is almost always still open. (We've since moved to IPv4 for unrelated reasons, so this isn't blocking us — filing because the opaque error seems likely to trip up anyone self-assigning an IPv6 bind address.)
Source: n0-computer/iroh