How to Implement Custom Transports in iroh: A Complete Guide
Implement custom transports in iroh by implementing the CustomTransport trait and registering it via Endpoint::builder().with_custom_transport(), while using QuicTransportConfig to tune QUIC-level parameters.
iroh's networking layer is built around a flexible transport abstraction that decouples the protocol implementation from the underlying connectivity. Whether you need to tunnel traffic through a specialized network, implement a testing mock, or support a non-standard protocol, you can plug custom transports directly into the Endpoint builder. This guide covers the exact mechanisms for implementing custom transports in iroh using the source code from the n0-computer/iroh repository.
Understanding iroh's Transport Architecture
The transport system centers on two core abstractions defined in iroh/src/socket/transports.rs: the TransportConfig enum and the CustomTransport trait. These types allow the EndpointBuilder in iroh/src/endpoint.rs to compose multiple transport backends while maintaining a unified connection interface.
The TransportConfig Enum
The TransportConfig enum defines the set of transport types available to an iroh node. According to the source in iroh/src/socket/transports.rs, the variants include:
Ip { bind: SocketAddr, ... }— Standard UDP/QUIC sockets for IPv4/IPv6 connectivity.Relay { ... }— Relayed connections through iroh relay servers for NAT traversal.Custom(Arc<dyn CustomTransport>)— A placeholder for user-provided transport implementations.
When building an endpoint, you construct a Vec<TransportConfig> that determines which protocols the node will attempt to use for dialing and listening.
The CustomTransport Trait
To implement custom transports in iroh, you must satisfy the CustomTransport trait. As defined in iroh/src/socket/transports.rs, implementors must provide three asynchronous methods:
use iroh::socket::{TransportAddr, Connection, Listener};
#[async_trait::async_trait]
pub trait CustomTransport: Send + Sync {
async fn dial(&self, addr: TransportAddr) -> anyhow::Result<Connection>;
async fn listen(&self, bind: TransportAddr) -> anyhow::Result<Listener>;
fn local_addr(&self) -> TransportAddr;
}
The dial method initiates outgoing connections, listen creates a listener for incoming connections, and local_addr returns the transport address this implementation advertises to other nodes.
Configuring QUIC Transport Settings
Before adding custom transports, you often need to tune QUIC-level parameters that affect all connections. The QuicTransportConfig builder in iroh/src/endpoint/quic.rs exposes knobs like keep_alive_interval, max_idle_timeout, and congestion control settings.
use iroh::endpoint::QuicTransportConfig;
use std::time::Duration;
// Configure QUIC with aggressive keep-alives and a short idle timeout
let quic_cfg = QuicTransportConfig::builder()
.keep_alive_interval(Duration::from_secs(15))
.max_idle_timeout(Duration::from_secs(30))
.build();
The builder() method returns a QuicTransportConfigBuilder that validates parameters before construction. These settings apply to the underlying noq::TransportConfig used by the endpoint's QUIC implementation.
Implementing a Custom Transport
To create a working custom transport, define a struct and implement the CustomTransport trait using async_trait::async_trait. The reference implementation in iroh/examples/custom-transport.rs demonstrates the required boilerplate.
use std::sync::Arc;
use iroh::{
socket::{CustomTransport, TransportAddr, Connection, Listener},
SecretKey,
};
#[derive(Clone)]
struct MyCustomTransport;
#[async_trait::async_trait]
impl CustomTransport for MyCustomTransport {
async fn dial(&self, addr: TransportAddr) -> anyhow::Result<Connection> {
// Implement your dialing logic here
unimplemented!("Dial to {:?}", addr)
}
async fn listen(&self, bind: TransportAddr) -> anyhow::Result<Listener> {
// Implement your listening logic here
unimplemented!("Listen on {:?}", bind)
}
fn local_addr(&self) -> TransportAddr {
// Return a custom address identifier
TransportAddr::Custom(iroh::socket::CustomAddr::new(0xdeadbeef, b"mytransport"))
}
}
Your implementation must be Send + Sync because the endpoint holds the transport as an Arc<dyn CustomTransport> and uses it across asynchronous tasks.
Registering Custom Transports with the Endpoint
Once implemented, register your transport with the EndpointBuilder using with_custom_transport(). This method accepts an Arc<dyn CustomTransport> and appends it to the transport list.
use iroh::endpoint::Endpoint;
use std::sync::Arc;
let secret_key = SecretKey::generate();
let endpoint = Endpoint::builder(secret_key)
.with_custom_transport(Arc::new(MyCustomTransport))
.build()
.await?;
Combining QUIC Configuration and Custom Transports
You can simultaneously inject custom QUIC settings and custom transport implementations. The endpoint uses the QuicTransportConfig for protocol parameters while your custom transport provides the actual socket operations.
use iroh::endpoint::{Endpoint, QuicTransportConfig};
let quic_cfg = QuicTransportConfig::builder()
.keep_alive_interval(Duration::from_secs(10))
.max_idle_timeout(Duration::from_secs(20))
.build();
let secret_key = SecretKey::generate();
let endpoint = Endpoint::builder(secret_key)
.with_transport_config(quic_cfg) // Custom QUIC knobs
.with_custom_transport(Arc::new(MyCustomTransport)) // Custom transport
.build()
.await?;
The with_transport_config() method is defined in iroh/src/endpoint.rs and accepts a QuicTransportConfig constructed via the builder pattern shown above.
Using Only Custom Transports
To disable default IP transports and run exclusively with your custom implementation, call clear_ip_transports() before building. This removes the automatic IPv4 and IPv6 socket bindings.
let secret_key = SecretKey::generate();
let endpoint = Endpoint::builder(secret_key)
.clear_ip_transports() // Removes IPv4/IPv6 from the transport list
.with_custom_transport(Arc::new(MyCustomTransport))
.build()
.await?;
This configuration is useful when running iroh in restricted environments where UDP sockets are unavailable or when implementing proxy transports that handle all connectivity internally.
Summary
CustomTransporttrait — Implementdial(),listen(), andlocal_addr()iniroh/src/socket/transports.rsto define custom transport behavior.QuicTransportConfigbuilder — Useiroh/src/endpoint/quic.rsto tune keep-alive intervals, idle timeouts, and MTU discovery before injecting transports.- Endpoint registration — Pass custom transports to
Endpoint::builder().with_custom_transport()and combine withwith_transport_config()for full control. - Selective transport chains — Use
clear_ip_transports()to disable default UDP sockets when running purely on custom transports. - Reference implementation — Study
iroh/examples/custom-transport.rsfor a complete working example of the trait implementation.
Frequently Asked Questions
What methods must I implement for the CustomTransport trait?
You must implement three methods defined in iroh/src/socket/transports.rs: async fn dial() for initiating connections, async fn listen() for accepting them, and fn local_addr() to advertise your transport's address. All methods must be thread-safe because the endpoint shares the transport across asynchronous tasks using an Arc wrapper.
Can I use multiple custom transports simultaneously?
Yes. The EndpointBuilder stores transports in a Vec<TransportConfig>, and you can call with_custom_transport() multiple times with different implementations. The endpoint will attempt to use all registered transports when establishing connections, falling back through the list based on path availability.
How do I configure QUIC keep-alive intervals when using custom transports?
Use QuicTransportConfig::builder() from iroh/src/endpoint/quic.rs to set keep_alive_interval() and max_idle_timeout(), then pass the resulting config to Endpoint::builder().with_transport_config(). These settings apply to the QUIC protocol layer regardless of whether you use built-in IP transports or custom implementations.
Where can I find a complete working example of a custom transport?
The official iroh/examples/custom-transport.rs file in the n0-computer/iroh repository provides a full reference implementation. Additionally, iroh/tests/patchbay/util.rs (around line 464) demonstrates real-world usage of QuicTransportConfig::builder() inside the test suite, showing how custom transports integrate with the broader endpoint system.
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 →