When Does iroh Hole-Punching Fall Back to Relay Servers?
iroh falls back to relay servers only after the remote-state actor conclusively determines that direct NAT traversal is impossible, specifically when all hole-punching candidates fail, symmetric NAT is detected, retry intervals exhaust without new address candidates, or direct paths exceed idle timeouts.
n0-computer/iroh is a Rust implementation of the Interplanetary Relay Overlay for Hole-punching that prioritizes direct peer-to-peer connections before resorting to relay infrastructure. Understanding precisely when iroh's hole-punching falls back to relay servers is essential for diagnosing connectivity failures and optimizing network performance in complex NAT environments.
Failure Conditions That Trigger Relay Fallback
The decision to abandon direct connection attempts occurs within iroh/src/socket/remote_map/remote_state.rs, where the remote-state actor continuously evaluates path viability.
Exhausted Direct Path Candidates
The RemotePathState logic tracks path viability through the FailedHolePunch status. When the prune_failed_holepunch handling determines that all direct paths have failed hole-punching attempts, the actor discards these unusable paths. The system then scans for remaining paths containing only relay addresses, triggering an immediate fallback to relay servers.
Symmetric NAT Detection
When the underlying NAT-traversal module returns a HolePunchResult::SymmetricNat, this result bubbles up to the remote-state actor. Symmetric NATs cannot be traversed via standard hole-punching techniques, so the actor immediately marks affected paths as relay-only, bypassing further direct connection attempts in favor of the configured relay infrastructure.
Retry Interval Timeouts
The constant HOLEPUNCH_ATTEMPTS_INTERVAL (set to 5 seconds) governs retry scheduling in remote_state.rs. If no new NAT address candidates appear between intervals, the actor skips the next hole-punch attempt and falls back to the relay path rather than attempting connections deemed likely to fail.
Path Idle Timeouts and Pruning
Direct paths that remain idle longer than RELAY_PATH_MAX_IDLE_TIMEOUT (default 30 seconds) are pruned by the remote-state actor. Once pruned or marked stale, the actor replaces the direct path with a relay path to maintain connectivity through the relay server.
Transport Layer Relay Establishment
When the remote-state actor selects a relay-only address, the transport layer creates a RelayPath as implemented in iroh/src/socket/transports.rs. The endpoint must be configured with RelayMode::Custom(relay_map) through the EndpointBuilder, typically initialized via test utilities like run_relay_server found in iroh/tests/patchbay/util.rs.
Implementation Code Examples
Configure an endpoint to support automatic relay fallback:
let relay_map = /* obtained from run_relay_server() */;
let builder = iroh::endpoint::Builder::default()
.relay_mode(iroh::relay::RelayMode::Custom(relay_map));
let endpoint = builder.bind().await?;
Remote-state actor logic that selects relay paths after failed attempts:
// Inside RemoteStateActor
if self.state.paths.iter().all(|p| p.is_failed_holepunch()) {
// No viable direct path → pick a relay‑only address
self.select_best_path(|p| p.is_relay());
}
Testing symmetric NAT fallback behavior:
let (lab, relay_map, _guard, _lab_guard) = lab_with_relay(testdir!()).await?;
let pair = Pair::new(relay_map);
let conn = pair.connect().await?;
assert!(is_relayed(&conn), "connection should be relayed");
Summary
- iroh attempts direct hole-punching before considering relay servers, as implemented in the n0-computer/iroh codebase.
- Fallback triggers when
FailedHolePunchstatus affects all direct paths iniroh/src/socket/remote_map/remote_state.rs. - Symmetric NAT detection immediately forces relay-only mode without retry attempts.
- The 5-second
HOLEPUNCH_ATTEMPTS_INTERVALand 30-secondRELAY_PATH_MAX_IDLE_TIMEOUTconstants control fallback timing. - The transport layer creates
RelayPathinstances only after the remote-state actor confirms direct traversal is impossible viaadd_failed_holepunchhandling and path pruning logic.
Frequently Asked Questions
How does iroh detect that hole-punching has failed?
The remote-state actor monitors path status through RemotePathState logic in iroh/src/socket/remote_map/remote_state.rs. When paths accumulate FailedHolePunch status through add_failed_holepunch handling and are pruned via the prune_failed_holepunch logic, the system recognizes that direct traversal is impossible and initiates relay fallback.
Can I configure the timeout before iroh falls back to relays?
Yes, the RELAY_PATH_MAX_IDLE_TIMEOUT constant defaults to 30 seconds, controlling when idle direct paths are pruned and replaced with relay connections. Additionally, the HOLEPUNCH_ATTEMPTS_INTERVAL of 5 seconds governs how long the system waits for new NAT candidates before skipping direct attempts.
Does symmetric NAT always force relay usage?
Yes, according to the source code, when HolePunchResult::SymmetricNat is returned by the NAT-traversal module, the remote-state actor immediately marks the path as relay-only because symmetric NATs prevent successful hole-punching, making relay servers the only viable connection method.
Where is the relay server configured in iroh?
Relay configuration occurs through iroh::relay::RelayMode::Custom(relay_map) passed to the iroh::endpoint::Builder, typically sourced from production relay maps or the test utility run_relay_server in iroh/tests/patchbay/util.rs (lines 493-521).
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →