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 FailedHolePunch status affects all direct paths in iroh/src/socket/remote_map/remote_state.rs.
  • Symmetric NAT detection immediately forces relay-only mode without retry attempts.
  • The 5-second HOLEPUNCH_ATTEMPTS_INTERVAL and 30-second RELAY_PATH_MAX_IDLE_TIMEOUT constants control fallback timing.
  • The transport layer creates RelayPath instances only after the remote-state actor confirms direct traversal is impossible via add_failed_holepunch handling 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →