How to Enable Custom Transport Protocols in Iroh with the `unstable-custom-transports` Feature
Enable the unstable-custom-transports feature in your Cargo.toml, implement the CustomTransport, CustomEndpoint, and CustomSender traits from iroh::socket::transports::custom, then register your transport via Builder::add_custom_transport() before binding the endpoint.
Iroh is a Rust-based peer-to-peer networking stack that defaults to QUIC but allows you to plug in custom transport protocols for sending and receiving raw packets. This capability is gated behind the unstable-custom-transports Cargo feature, which exposes the trait interfaces necessary to integrate proprietary radio links, VPN tunnels, or simulation layers directly into Iroh's connection model according to the n0-computer/iroh source code.
Enable the unstable-custom-transports Feature
To compile Iroh with custom transport support, add the feature to your Cargo.toml dependency declaration:
[dependencies]
iroh = { version = "0.20", features = ["unstable-custom-transports"] }
Alternatively, build from the command line when working with the repository directly:
cargo build --features unstable-custom-transports
What the Feature Activates
Enabling this feature unlocks three critical components in the Iroh source:
- Trait definitions in
iroh/src/socket/transports/custom.rs— Defines theCustomTransport,CustomEndpoint, andCustomSendertraits that your implementation must satisfy. - Builder API extensions in
iroh/src/endpoint.rs(lines 800–814) — Adds theBuilder::add_custom_transport()method and the internalTransportConfig::Customvariant. - Public re-exports in
iroh/src/endpoint.rs(moduletransportsat line 29) — Makes the custom transport types available underiroh::endpoint::transportsfor downstream consumption.
Implement the Three Core Traits
Custom transports require implementing three hierarchical traits that handle binding, address advertisement, and packet transmission.
CustomTransport
The CustomTransport trait acts as a factory for creating endpoints. As defined in iroh/src/socket/transports/custom.rs, it requires a single method:
fn bind(&self) -> io::Result<Box<dyn CustomEndpoint>>;
This method is called during Builder::bind() to initialize your transport.
CustomEndpoint
The CustomEndpoint trait manages the local side of the connection. Key methods include:
watch_local_addrs()— Returns a watcher yieldingVec<CustomAddr>that are advertised to remote peers through Iroh's discovery mechanisms.create_sender()— Returns anArc<dyn CustomSender>used to transmit packets.poll_recv()— Polls for inbound packets, filling the provided buffers andRecvInfometadata for Iroh's path selection machinery.
CustomSender
The CustomSender trait handles actual packet transmission to remote addresses:
is_valid_send_addr()— Validates whether a givenCustomAddris reachable by this sender.poll_send()— Transmits a packet to the specified destination, returningPoll<io::Result<()>>.
Register Your Transport with the Endpoint Builder
After implementing the traits, instantiate your transport and register it using Builder::add_custom_transport() before calling bind():
use std::sync::Arc;
use iroh::Endpoint;
let my_transport = Arc::new(MyCustomTransport::new());
let endpoint = Endpoint::builder()
.add_custom_transport(my_transport)
.bind()
.await?;
The add_custom_transport method, implemented in iroh/src/endpoint.rs, stores your transport as an Arc<dyn CustomTransport> inside the builder's internal transports vector.
Control Path Selection with a Custom PathSelector
By default, Iroh selects paths based on measured latency. To prioritize your custom transport over standard IP or relay paths, implement the PathSelector trait and attach it via Builder::path_selector():
use iroh::endpoint::PathSelector;
struct PreferCustomTransport;
impl PathSelector for PreferTestTransport {
// Implementation that prefers TransportAddr::Custom
}
builder
.add_custom_transport(my_transport)
.path_selector(Arc::new(PreferCustomTransport));
For a complete reference implementation, see the PreferTestTransport example in iroh/examples/custom-transport.rs.
Complete Minimal Example
Below is a minimal echo transport demonstrating the structure required by the traits in iroh/src/socket/transports/custom.rs:
use std::io;
use std::sync::Arc;
use std::task::{Context, Poll};
use iroh::socket::transports::custom::{CustomTransport, CustomEndpoint, CustomSender};
use iroh_base::CustomAddr;
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>> {
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>> {
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<()>> {
Poll::Ready(Ok(()))
}
}
Reference Implementation and Architecture
For a production-ready example, examine the test transport in iroh/src/test_utils.rs and the complete working example in iroh/examples/custom-transport.rs. The latter demonstrates a TestNetwork that simulates packet flow between endpoints.
When you call Builder::bind(), Iroh executes the following sequence from iroh/src/endpoint.rs:
- Iterates over the
transportsvector containing yourTransportConfig::Custom. - Invokes
CustomTransport::bind()to create aBox<dyn CustomEndpoint>. - Polls
CustomEndpoint::poll_recv()in the runtime to receive packets viaRecvInfo. - Selects paths using your
PathSelector, preferringTransportAddr::Customwhen specified. - Transmits via
CustomSender::poll_send()when the custom path is chosen.
Custom addresses returned by watch_local_addrs() are advertised through standard discovery services (pkarr, DNS) as TransportAddr::Custom, allowing compatible peers to select them automatically.
Summary
- Enable the
unstable-custom-transportsfeature inCargo.tomlto unlock the custom transport API iniroh/src/socket/transports/custom.rs. - Implement three traits:
CustomTransport(factory),CustomEndpoint(receiving), andCustomSender(sending). - Register your transport via
Builder::add_custom_transport()defined iniroh/src/endpoint.rsbefore callingbind(). - Optionally provide a
PathSelectorviaBuilder::path_selector()to prioritize custom transports over IP or relay paths. - Custom addresses are automatically advertised and discovered through Iroh's standard address resolution mechanisms.
Frequently Asked Questions
Is the unstable-custom-transports feature safe for production use?
No. As indicated by the "unstable" prefix, this feature is explicitly excluded from semantic versioning guarantees. The trait definitions in iroh/src/socket/transports/custom.rs and the associated APIs in iroh/src/endpoint.rs may change in breaking ways without a major version bump, potentially requiring updates to your implementation when upgrading Iroh.
Can I use multiple custom transports simultaneously?
Yes. The Builder supports adding multiple transports via repeated calls to add_custom_transport(). Each transport is stored in the internal transports vector and polled independently during the binding process in iroh/src/endpoint.rs. This allows you to implement fallback strategies or support multiple underlying link types in a single endpoint.
How does Iroh prioritize between custom transports and standard QUIC paths?
Iroh uses the PathSelector trait to determine the optimal path for each connection. Without a custom selector, the default implementation typically prefers the lowest-latency available path. You can override this by providing your own selector via Builder::path_selector() that prioritizes TransportAddr::Custom variants when they are available for a given remote endpoint.
Do peers need to enable the same feature to communicate over a custom transport?
Yes. Both endpoints must enable unstable-custom-transports and implement compatible transport logic. The custom addresses are advertised as TransportAddr::Custom through the discovery system, and only peers with the feature enabled can recognize and select these paths during connection establishment. If a peer lacks the feature, it will ignore the custom address and use standard IP or relay paths instead.
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 →