How to Dial Another Endpoint by Its Public Key in Iroh
To dial another Iroh endpoint by its public key, construct an EndpointAddr containing the remote node's EndpointId (public key) and optional RelayUrl, then call endpoint.connect(addr).await to initiate the cryptographic handshake and connection.
Iroh provides a peer-to-peer networking stack where every node is cryptographically identified by its public key. In the n0-computer/iroh codebase, this identifier is called the EndpointId, and dialing another endpoint by its public key requires packaging this ID into an EndpointAddr before invoking the connection logic. This guide walks through the source code implementation to show exactly how to open connections using public key identification.
Understanding Iroh Endpoint Identification
Every node in an Iroh network is identified by an EndpointId, which represents the cryptographic public key derived from the node's long-term secret key. This design eliminates the need for centralized address registries—knowing a node's public key is sufficient to initiate a connection.
To actually dial a remote node, you combine the EndpointId with an optional RelayUrl to create an EndpointAddr. According to iroh-base/src/endpoint_addr.rs, this struct bundles the identity and reachability information into a single address type that the networking stack can resolve.
Prerequisites: Binding a Local Endpoint
Before dialing remote nodes, your application must bind a local Endpoint. This initializes the QUIC sockets, timers, and cryptographic credentials needed to participate in the network.
use iroh::Endpoint;
// Bind a local endpoint using default configuration
let local_endpoint = Endpoint::bind(Default::default()).await?;
The Endpoint::bind method in iroh/src/endpoint.rs returns an Endpoint instance that serves as your main entry point for both accepting incoming connections and initiating outbound dials.
Constructing the Remote Address
To dial another endpoint by its public key, you must construct an EndpointAddr that encapsulates the target's identity and optional relay information.
Parsing the Public Key (EndpointId)
The EndpointId type in iroh-base/src/key.rs provides methods to parse public keys from various encodings. The most common format is base32 (z32):
use iroh_base::EndpointId;
// Parse a base32-encoded public key (e.g., from a QR code or configuration file)
let remote_id = EndpointId::from_z32("v4pdu5k2j...")?;
This remote_id represents the cryptographic identity of the peer you want to reach.
Adding a Relay URL (Optional)
While direct connections are possible, providing a RelayUrl significantly improves connectivity when peers are behind NATs. The relay server helps coordinate the initial connection before potentially upgrading to a direct path.
use iroh_base::RelayUrl;
use iroh::EndpointAddr;
// Create a relay URL (WebSocket secure is recommended)
let relay = RelayUrl::try_from("wss://relay.example.com")?;
// Build the address with both public key and relay
let remote_addr = EndpointAddr::new(remote_id).with_relay(relay);
If you omit the relay, the library will attempt direct hole-punching using IPv6/IPv4 as implemented in iroh-relay/src/client/tls.rs.
The Connection Process
When you call endpoint.connect(remote_addr).await, the Iroh runtime executes a multi-stage dialing process defined across several core files.
Address Resolution and Relay Discovery
If your EndpointAddr contains a RelayUrl, the Endpoint contacts the relay server to obtain a list of reachable socket addresses for the remote node. The ActiveRelayActor in iroh/src/socket/transports/relay/actor.rs handles this relay dialing logic with exponential backoff for resilience.
Transport Establishment and NAT Traversal
For direct connectivity, the library employs the dial_happy_eyeballs helper function found in iroh-relay/src/client/tls.rs. This implementation attempts IPv6 connections first, then falls back to IPv4, implementing the "happy eyeballs" algorithm to minimize connection latency while traversing NATs.
Cryptographic Handshake
Once the transport layer is established, iroh/src/endpoint/connection.rs manages the cryptographic handshake. This process authenticates both peers using their static keys (the EndpointId values), ensuring you are connected to the correct remote node and establishing encrypted channels for data transfer.
Complete Example: Dialing by Public Key
Here is a complete, runnable example demonstrating how to dial another Iroh endpoint using only its public key:
use iroh::Endpoint;
use iroh_base::{EndpointId, RelayUrl};
use iroh::EndpointAddr;
async fn connect_by_public_key() -> Result<(), Box<dyn std::error::Error>> {
// 1. Bind local endpoint
let local = Endpoint::bind(Default::default()).await?;
// 2. Parse the remote public key (EndpointId)
let remote_id = EndpointId::from_z32("v4pdu5k2j...")?;
// 3. Optional: Add a relay for NAT traversal
let relay = RelayUrl::try_from("wss://relay.example.com")?;
let remote_addr = EndpointAddr::new(remote_id).with_relay(relay);
// 4. Connect - this performs relay lookup, NAT traversal, and handshake
let conn = local.connect(remote_addr).await?;
// 5. Use the connection
println!("Connected to remote endpoint: {}", conn.remote_id());
Ok(())
}
Summary
- Every Iroh node is identified by an
EndpointId, which is the cryptographic public key of the node's long-term secret key. - Dialing requires constructing an
EndpointAddrcontaining the target'sEndpointIdand optionally aRelayUrlfor NAT traversal. - The
Endpoint::connectmethod iniroh/src/endpoint.rsorchestrates address resolution, relay discovery, transport establishment, and cryptographic handshake. - Source files including
iroh-base/src/endpoint_addr.rs,iroh/src/socket/transports/relay/actor.rs, andiroh-relay/src/client/tls.rsimplement the underlying dialing logic. - Direct connections are possible without relays when both peers are on the same LAN or have public IPv6 addresses, though relays improve reliability.
Frequently Asked Questions
What is an EndpointId in Iroh?
An EndpointId is the cryptographic public key that uniquely identifies an Iroh node. Defined in iroh-base/src/key.rs, this 32-byte value is derived from the node's long-term secret key and serves as the primary addressing mechanism for the peer-to-peer network. When you dial another endpoint by its public key, you are essentially specifying its EndpointId.
Do I need a relay URL to dial another Iroh node?
No, a relay URL is optional but highly recommended. If you provide only the EndpointId without a RelayUrl, the library attempts direct hole-punching using IPv6 and IPv4 as implemented in iroh-relay/src/client/tls.rs. However, without a relay, connections may fail if both peers are behind restrictive NATs or firewalls that prevent direct UDP communication.
How does Iroh handle NAT traversal when dialing?
Iroh implements several NAT traversal strategies. When a RelayUrl is provided, the ActiveRelayActor in iroh/src/socket/transports/relay/actor.rs coordinates with the relay server to facilitate the initial connection. For direct paths, the dial_happy_eyeballs function attempts IPv6 first, then IPv4, using QUIC and WebSocket transports to maximize connectivity chances.
What happens if the remote node is offline?
If the remote node is unreachable, the endpoint.connect call will return an error after exhausting retry attempts. The connection logic includes exponential backoff when attempting relay connections, and will eventually timeout if the peer cannot be contacted through either direct paths or relay infrastructure. Your application should handle these errors appropriately, as the connection attempt is asynchronous and may take several seconds to fail completely.
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 →