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) // EADDRNOTAVAIL
The 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_addr must already have passed DAD (or be added nodad / 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.)
Summary
Calling
Endpoint::builder(...).bind_addr("[<ipv6>]:<port>")with an IPv6 address that was assigned to the interface moments earlier fails with:The 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), andbind(2)to a tentative address returnsEADDRNOTAVAIL. 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
1.0.0Repro
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)
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.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.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.)