How Iroh Handles Relay vs Direct Connections: NAT Traversal Implementation Guide
Iroh's networking layer establishes connections directly when possible but automatically falls back to relay servers when NAT or firewall restrictions prevent peer-to-peer paths.
The n0-computer/iroh framework provides robust NAT traversal by attempting direct connections first, then seamlessly switching to relayed communication when necessary. This hybrid approach ensures connectivity across diverse network conditions while optimizing for low-latency direct paths whenever available.
Understanding Relay and Direct Connection Modes
Iroh distinguishes between two fundamental transport modes: direct peer-to-peer connections and relayed connections through intermediary servers.
Direct Peer-to-Peer Connections
Direct connections represent the optimal path where iroh establishes a UDP-based QUIC connection between two endpoints without intermediary hops. These connections offer lower latency and higher throughput since data flows directly between peers.
Relayed Connections via Relay Servers
When NAT devices, firewalls, or asymmetric routing prevent direct connectivity, iroh utilizes relay servers as intermediaries. The relay server forwards encrypted traffic between peers without decrypting the contents, maintaining end-to-end security while enabling connectivity in restrictive network environments.
Configuring Relay Support in Iroh
Relay functionality centers around the RelayMap, a configuration structure that tells endpoints which relay servers are available for fallback connections.
Setting Up the Relay Map
When constructing an Endpoint, you supply a relay map through the builder pattern. According to the implementation in iroh/src/endpoint.rs at line 452, the builder stores this configuration via:
.relay_mode(RelayMode::Custom(relay_map))
The default test relay uses the URL https://relay.test, which the framework recognizes as a valid relay endpoint during connection establishment.
Spawning a Test Relay Server
For integration testing, iroh can spin up an in-process relay server. As implemented in iroh-relay/src/lib.rs at lines 515-521, the server binds to [::]:80 and registers the test URL:
let url: RelayUrl = "https://relay.test".parse().expect("valid relay url");
let relay_map: RelayMap = RelayConfig::new(url, quic).into();
The run_relay_server() function returns both the running server instance and the RelayMap required by connecting endpoints.
Connection Establishment and Path Selection
Iroh implements sophisticated path discovery that evaluates multiple potential routes simultaneously, preferring direct connectivity while maintaining relay options as fallback.
Address Discovery and EndpointAddr
Each discovered address resolves to an EndpointAddr structure that carries metadata about the path type. The is_relay() method, defined in iroh/src/socket/transports.rs at lines 1086-1087, distinguishes between direct and relay addresses:
- Direct addresses: Return
falsefromis_relay(), representing actual IP:port combinations reachable through the public internet - Relay addresses: Return
truefromis_relay(), representing paths through the configured relay infrastructure
Path Preference Logic
During the connection handshake, the endpoint gathers candidate addresses from multiple sources including DNS resolution and the relay map. The implementation in iroh/src/endpoint.rs (lines 2468-2688) filters and sorts these paths, attempting direct addresses first before falling back to relay options.
If all direct connection attempts fail due to NAT traversal timeouts or ICMP unreachable errors, the framework automatically retries using the first available relay address, ensuring connection reliability even in challenging network topologies.
Detecting the Active Connection Type
Once established, connections expose their transport characteristics through the path inspection API, allowing applications to adapt behavior based on latency and bandwidth constraints.
Runtime Path Inspection
You can determine whether an active connection traverses a relay by checking the path state. As shown in the test utilities at iroh/tests/patchbay/util.rs line 382, the detection function appears as:
pub(crate) fn is_relayed(conn: &iroh::endpoint::Connection) -> bool {
conn.path().is_relay()
}
This returns true when the current path flows through a relay server, and false for direct peer-to-peer connections.
Forcing Relay-Only Mode
Test utilities sometimes require simulating worst-case network conditions by restricting endpoints to relay-only communication. The helper function at iroh/tests/patchbay/util.rs line 474 demonstrates stripping non-relay addresses:
fn addr_relay_only(addr: EndpointAddr) -> EndpointAddr {
EndpointAddr::from_parts(
addr.id,
addr.addrs.into_iter().filter(|a| a.is_relay()).collect()
)
}
This filtering ensures the endpoint attempts only relay-based connections, useful for validating relay infrastructure behavior or simulating symmetric NAT scenarios.
Connection Lifecycle and Fallback Behavior
Iroh connections are dynamic, capable of migrating between transport modes as network conditions change without requiring application-level reconnection.
From Relay to Direct Path Migration
The framework continuously monitors path quality through the connection lifecycle. While the initial handshake might establish a relayed connection due to aggressive connection timeouts, ongoing NAT hole-punching attempts in the background can discover direct paths post-establishment.
When a direct path becomes available, iroh can migrate the connection from relayed to direct mode transparently. Conversely, if a direct path degrades or becomes unavailable, the connection may fall back to the relay. This behavior is extensively tested in the patchbay integration tests, demonstrating that conn.path().is_relay() returns different values at different points in the connection lifetime based on real-time path availability.
Summary
- Relay configuration relies on the
RelayMapstructure passed throughEndpoint::builder().relay_mode(RelayMode::Custom(relay_map))at initialization time - Path discovery generates both direct and relay
EndpointAddrinstances, with theis_relay()method distinguishing transport types - Connection preference prioritizes direct addresses during handshake, automatically falling back to relay addresses when NAT traversal fails
- Runtime introspection uses
conn.path().is_relay()to determine the current transport mode of established connections - Dynamic migration allows connections to transition between relay and direct modes as network paths become available or unavailable
Frequently Asked Questions
How does iroh decide between relay and direct connections?
Iroh attempts direct connections first during the initial handshake, trying public IP addresses discovered through DNS or previous connections. Only if these attempts timeout or receive unreachable errors does the framework fall back to addresses marked with is_relay() == true. This logic resides in the path selection implementation within iroh/src/endpoint.rs around lines 2468-2688.
Can I force an iroh endpoint to use only relay connections?
Yes, by filtering the endpoint's address list to include only relay addresses before establishing connections. The test utility addr_relay_only in iroh/tests/patchbay/util.rs demonstrates this by filtering with .filter(|a| a.is_relay()), effectively preventing direct connection attempts while maintaining relay functionality.
How do I check if an active connection is using a relay?
Call conn.path().is_relay() on the connection object, which returns a boolean indicating whether the active path traverses a relay server. This mechanism, used in the patchbay test suite at line 382, allows applications to monitor transport quality and adjust timeouts or buffer sizes accordingly.
What is the default relay URL for testing in iroh?
The standard test relay uses https://relay.test, which resolves to the in-process relay server spawned by iroh_relay::run_relay_server(). This URL is hardcoded in test configurations within iroh-relay/src/lib.rs and binds to [::]:80 when running the local relay infrastructure.
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 →