How to Implement Custom Transport Protocols with Iroh's unstable-custom-transports Feature
Enable the unstable-custom-transports Cargo feature, implement the CustomTransport, CustomEndpoint, and CustomSender traits, and register your transport via Builder::add_custom_transport to route packets through your custom protocol.
Iroh's networking stack extends beyond QUIC to support user-defined packet transport mechanisms. By enabling the unstable-custom-transports feature in the iroh crate, you can inject bespoke protocols—such as specialized radio links, VPN tunnels, or simulation layers—directly into the connection lifecycle.
Enabling the Feature Flag
To compile with custom transport support, add the feature to your Cargo.toml:
iroh = { version = "...", features = ["unstable-custom-transports"] }
This activates the trait definitions in iroh/src/socket/transports/custom.rs and exposes the Builder::add_custom_transport method in iroh/src/endpoint.rs at lines 800-814.
Understanding the Custom Transport Architecture
The architecture revolves around three core traits that define how raw packets flow through your custom implementation.
The Three Core Traits
Located in iroh/src/socket/transports/custom.rs, these traits form the contract between your code and Iroh's path selection machinery:
CustomTransport: The entry point trait. Itsbind()method returns aBox<dyn CustomEndpoint>and is called once during endpoint construction.CustomEndpoint: Represents a bound transport instance. It manageswatch_local_addrs()for address advertisement,poll_recv()for inbound packet processing, andcreate_sender()for outbound transmission handles.CustomSender: Handles actual packet transmission viapoll_send()and validates destination addresses withis_valid_send_addr().
Lifecycle and Integration
When you call Builder::add_custom_transport, the builder stores an Arc<dyn CustomTransport> inside its internal transports vector. During Builder::bind(), the runtime invokes CustomTransport::bind() to create the endpoint. The runtime then polls CustomEndpoint::poll_recv to inject incoming packets into the path-selection logic via RecvInfo structures. For outbound traffic, create_sender provides an Arc<dyn CustomSender> that the runtime invokes when the path selector chooses your transport over IP or relay paths.
Implementing a Minimal Custom Transport
The following example demonstrates a basic echo transport that implements all required traits. This pattern references the trait definitions in iroh/src/socket/transports/custom.rs:
use std::io;
use std::sync::Arc;
use std::task::{Context, Poll};
use iroh_base::CustomAddr;
use iroh::endpoint::transports::custom::{CustomTransport, CustomEndpoint, CustomSender};
struct EchoTransport;
impl CustomTransport for EchoTransport {
fn bind(&self) -> io::Result<Box<dyn CustomEndpoint>> {
Ok(Box::new(EchoEndpoint))
}
}
struct EchoEndpoint;
impl CustomEndpoint for EchoEndpoint {
fn watch_local_addrs(&self) -> n0_watcher::Direct<Vec<CustomAddr>> {
// Advertise a single dummy address for discovery
n0_watcher::Direct::new(vec![CustomAddr::new(0, vec![0])])
}
fn create_sender(&self) -> Arc<dyn CustomSender> {
Arc::new(EchoSender)
}
fn poll_recv(
&mut self,
_cx: &mut Context,
_bufs: &mut [io::IoSliceMut<'_>],
_metas: &mut [noq_udp::RecvMeta],
_infos: &mut [iroh::socket::transports::RecvInfo],
) -> Poll<io::Result<usize>> {
// Return 0 packets in this toy example
Poll::Ready(Ok(0))
}
}
struct EchoSender;
impl CustomSender for EchoSender {
fn is_valid_send_addr(&self, _addr: &CustomAddr) -> bool {
true
}
fn poll_send(
&self,
_cx: &mut Context,
_dst: &CustomAddr,
_src: Option<&CustomAddr>,
_transmit: &iroh::socket::transports::Transmit<'_>,
) -> Poll<io::Result<()>> {
// Drop packets in this example implementation
Poll::Ready(Ok(()))
}
}
Registering Your Transport with the Endpoint
To activate your implementation, wrap it in an Arc and register it during endpoint construction:
let my_transport = Arc::new(EchoTransport);
let endpoint = Endpoint::builder(presets::N0)
.add_custom_transport(my_transport)
.bind()
.await?;
As implemented in iroh/src/endpoint.rs, add_custom_transport stores your transport configuration. When the endpoint binds, it automatically invokes your bind() method and integrates the resulting endpoint into the runtime's packet polling loop.
Path Selection and Production Patterns
By default, Iroh uses an internal path selector that prioritizes low-latency IP or relay paths. To prefer your custom transport, implement a PathSelector and attach it via Builder::path_selector. The repository's example in iroh/examples/custom-transport.rs demonstrates a PreferTestTransport selector that chooses custom addresses when available, falling back to standard paths otherwise.
For a complete reference implementation, examine the test transport in iroh/src/test_utils.rs. This implementation provides a full in-memory network simulation using TestTransport and TestNetwork, showing how to handle CustomAddr serialization, packet buffering, and async runtime integration.
Summary
- Enable the
unstable-custom-transportsfeature in yourCargo.tomlto access the custom transport API. - Implement
CustomTransport,CustomEndpoint, andCustomSenderfromiroh/src/socket/transports/custom.rsto define your protocol's behavior. - Register your transport via
Builder::add_custom_transportbefore callingbind(). - Use
watch_local_addrs()to advertise custom addresses andpoll_recv()to handle inbound packets from the network. - Optional: Provide a custom
PathSelectorto prioritize your transport over IP or relay paths when establishing connections.
Frequently Asked Questions
Is the unstable-custom-transports feature production-ready?
No. As indicated by the "unstable" prefix, this feature is not covered by semantic versioning guarantees. The API may change between minor releases without a major version bump, making it suitable for experimentation and controlled deployments but not for stable production systems requiring long-term API stability.
How does path selection work with custom transports?
The PathSelector implementation evaluates all available paths—including IP, relay, and custom addresses—for each remote peer. When your custom transport advertises addresses via watch_local_addrs, the selector can choose them based on your priority logic. If you do not provide a custom selector, Iroh defaults to standard latency-based selection, which may bypass your transport if IP paths offer lower round-trip times.
Can I use multiple custom transports simultaneously?
Yes. The Builder supports multiple transports via repeated calls to add_custom_transport. Each transport is stored in the internal transports vector and bound during Builder::bind(). The path selector then chooses between all available transport options based on your selection criteria and the remote peer's advertised addresses.
Where can I find a complete working example?
The iroh repository includes a working demonstration in iroh/examples/custom-transport.rs. This example wires a TestTransport from iroh/src/test_utils.rs, implements a custom path selector, and verifies that connections route through the custom transport layer. It serves as the definitive reference for implementing custom transports in real-world scenarios.
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 →