How to Configure Custom Transports in iroh for Specialized Network Paths
You can configure custom transports in iroh by implementing the CustomTransport, CustomEndpoint, and CustomSender traits, registering them via Endpoint::builder().add_custom_transport(), and optionally controlling path selection with a custom PathSelector to prioritize specialized links like Bluetooth, LoRa, or Unix domain sockets.
The iroh networking stack supports pluggable transport abstractions that allow you to route QUIC traffic over non-standard network paths. While the library provides built-in transports for IP/UDP and relay connections, you can configure custom transports in iroh to leverage specialized hardware or logical channels. This guide walks through the trait implementations and builder configuration required to integrate alternative transports, referencing the actual source code in n0-computer/iroh.
Enable the Unstable Feature Flag
Custom transport support is currently gated behind the unstable-custom-transports Cargo feature because the API surface is still evolving. Add the feature to your Cargo.toml before implementing any transport traits.
[dependencies]
iroh = { version = "0.x", features = ["unstable-custom-transports"] }
Implement the Core Transport Traits
The transport system is built around three core traits defined in iroh/src/socket/transports/custom.rs. You must provide concrete implementations for each layer to create a functional transport.
CustomTransport: The Factory
The CustomTransport trait acts as a factory that produces endpoint instances. Implement the bind() method to initialize your underlying hardware or OS resources (e.g., opening a Bluetooth socket or LoRa radio).
use std::io;
use std::sync::Arc;
use iroh_base::CustomAddr;
use iroh::socket::transports::{CustomTransport, CustomEndpoint};
#[derive(Debug)]
struct MyTransport;
impl CustomTransport for MyTransport {
fn bind(&self) -> io::Result<Box<dyn CustomEndpoint>> {
// Initialize hardware resources here
Ok(Box::new(MyEndpoint::new()?))
}
}
CustomEndpoint: The Runtime Interface
The CustomEndpoint trait defined in iroh/src/socket/transports/custom.rs handles address discovery and packet reception. You must implement four key methods:
watch_local_addrs(): Returns the set of local addresses this transport exposes to the iroh endpointpoll_recv(): The async polling method that pulls packets from the underlying socket into iroh's bufferscreate_sender(): Returns aCustomSenderinstance for transmitting packetsmax_transmit_segments(): Indicates GSO (Generic Segmentation Offload) support level
use std::task::{Context, Poll};
use std::io::IoSliceMut;
use iroh::socket::transports::{CustomEndpoint, CustomSender, RecvInfo};
#[derive(Debug)]
struct MyEndpoint {
// Internal state, e.g., socket handle
}
impl MyEndpoint {
fn new() -> io::Result<Self> {
Ok(Self { /* ... */ })
}
}
impl CustomEndpoint for MyEndpoint {
fn watch_local_addrs(&self) -> n0_watcher::Direct<Vec<CustomAddr>> {
// Return local custom addresses available on this endpoint
n0_watcher::Direct::new(vec![
CustomAddr::new(0x42, vec![0x01, 0x02])
])
}
fn create_sender(&self) -> Arc<dyn CustomSender> {
Arc::new(MySender { /* ... */ })
}
fn poll_recv(
&mut self,
cx: &mut Context,
bufs: &mut [IoSliceMut<'_>],
metas: &mut [noq_udp::RecvMeta],
recv_infos: &mut [RecvInfo],
) -> Poll<io::Result<usize>> {
// Read from underlying socket and populate bufs
// For each packet, write RecvInfo::new(custom_addr, local_addr_opt)
unimplemented!()
}
fn max_transmit_segments(&self) -> std::num::NonZeroUsize {
// Return 1 for no GSO support, or higher for batching
std::num::NonZeroUsize::new(4).unwrap()
}
}
CustomSender: Packet Transmission
The CustomSender trait handles the egress path. Implement is_valid_send_addr() to filter which remote addresses your transport can handle, and poll_send() to encode and transmit packets to the underlying medium.
use iroh::socket::transports::{CustomSender, Transmit};
#[derive(Debug)]
struct MySender { /* ... */ }
impl CustomSender for MySender {
fn is_valid_send_addr(&self, addr: &CustomAddr) -> bool {
// Only accept addresses belonging to this transport type
addr.id() == 0x42
}
fn poll_send(
&self,
cx: &mut Context,
dst: &CustomAddr,
src: Option<&CustomAddr>,
transmit: &Transmit<'_>,
) -> Poll<io::Result<()>> {
// Encode packet and hand to OS/hardware socket
unimplemented!()
}
}
Register the Transport with the Endpoint Builder
Once your traits are implemented, register the transport using the builder API in iroh/src/endpoint.rs. The add_custom_transport() method accepts an Arc<dyn CustomTransport> and stores it in the builder's internal transports vector.
You can optionally disable built-in transports to create an endpoint that operates solely over your custom link.
use iroh::endpoint::{Builder, presets};
use std::sync::Arc;
let builder = Builder::new(presets::N0)
.add_custom_transport(Arc::new(MyTransport))
// Optional: remove default transports
.clear_ip_transports()
.clear_relay_transports();
When bind() is called, the builder iterates over all registered transports and invokes CustomTransport::bind() to instantiate the endpoints.
Implement Path Selection Strategy
By default, iroh uses an internal path selection algorithm that prefers IPv6, then IPv4, then relay connections. To prioritize your custom transport, implement the PathSelector trait and register it via path_selector() on the builder.
The select() method in iroh/src/endpoint.rs receives a PathSelectionContext containing all candidate paths and returns the chosen path.
use iroh::endpoint::transports::{Addr, PathSelection, PathSelectionContext, PathSelector};
struct PreferMyTransport;
impl PathSelector for PreferMyTransport {
fn select(&self, ctx: &PathSelectionContext<'_>) -> PathSelection {
// Prioritize custom transport paths with id 0x42
if let Some(p) = ctx.paths().find(|p| {
matches!(p.network_path().remote(), Addr::Custom(c) if c.id() == 0x42)
}) {
let mut sel = PathSelection::none();
sel.set(&p);
return sel;
}
// Fallback to lowest RTT path
ctx.paths()
.filter_map(|p| p.stats().map(|s| (p, s.rtt)))
.min_by_key(|(_, rtt)| *rtt)
.map_or_else(PathSelection::none, |(p, _)| {
let mut sel = PathSelection::none();
sel.set(p);
sel
})
}
}
// Apply the selector
let builder = builder.path_selector(Arc::new(PreferMyTransport));
Complete Working Example
Below is a minimal, self-contained example that demonstrates binding a custom transport and configuring the endpoint. This mirrors the official example in iroh/examples/custom-transport.rs but focuses on the essential configuration points.
use std::{sync::Arc, time::Duration};
use iroh::{
Endpoint, SecretKey, TransportAddr,
endpoint::{
Builder, presets,
transports::{Addr, PathSelection, PathSelectionContext, PathSelector},
},
};
use iroh_base::CustomAddr;
// Transport implementation
#[derive(Debug)]
struct MyTransport;
impl iroh::socket::transports::CustomTransport for MyTransport {
fn bind(&self) -> std::io::Result<Box<dyn iroh::socket::transports::CustomEndpoint>> {
Ok(Box::new(MyEndpoint {}))
}
}
#[derive(Debug)]
struct MyEndpoint;
impl iroh::socket::transports::CustomEndpoint for MyEndpoint {
fn watch_local_addrs(&self) -> n0_watcher::Direct<Vec<CustomAddr>> {
n0_watcher::Direct::new(vec![CustomAddr::new(0x99, vec![0x10, 0x20])])
}
fn create_sender(&self) -> Arc<dyn iroh::socket::transports::CustomSender> {
Arc::new(MySender {})
}
fn poll_recv(
&mut self,
_cx: &mut std::task::Context,
_bufs: &mut [std::io::IoSliceMut<'_>],
_metas: &mut [noq_udp::RecvMeta],
_recv_infos: &mut [iroh::socket::RecvInfo],
) -> std::task::Poll<std::io::Result<usize>> {
std::task::Poll::Pending
}
fn max_transmit_segments(&self) -> std::num::NonZeroUsize {
std::num::NonZeroUsize::new(4).unwrap()
}
}
#[derive(Debug)]
struct MySender;
impl iroh::socket::transports::CustomSender for MySender {
fn is_valid_send_addr(&self, addr: &CustomAddr) -> bool {
addr.id() == 0x99
}
fn poll_send(
&self,
_cx: &mut std::task::Context,
_dst: &CustomAddr,
_src: Option<&CustomAddr>,
_tx: &iroh::socket::transports::Transmit<'_>,
) -> std::task::Poll<std::io::Result<()>> {
std::task::Poll::Ready(Ok(()))
}
}
// Path selector
#[derive(Debug)]
struct PreferMyTransport;
impl PathSelector for PreferMyTransport {
fn select(&self, ctx: &PathSelectionContext<'_>) -> PathSelection {
if let Some(p) = ctx.paths().find(|p| {
matches!(p.network_path().remote(), Addr::Custom(c) if c.id() == 0x99)
}) {
let mut sel = PathSelection::none();
sel.set(&p);
return sel;
}
ctx.paths()
.filter_map(|p| p.stats().map(|s| (p, s.rtt)))
.min_by_key(|(_, rtt)| *rtt)
.map_or_else(PathSelection::none, |(p, _)| {
let mut sel = PathSelection::none();
sel.set(p);
sel
})
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let secret = SecretKey::from([0u8; 32]);
let builder = Endpoint::builder(presets::N0)
.secret_key(secret)
.add_custom_transport(Arc::new(MyTransport))
.clear_ip_transports()
.clear_relay_transports()
.path_selector(Arc::new(PreferMyTransport));
let endpoint = builder.bind().await?;
println!("Local addresses: {:?}", endpoint.addr());
Ok(())
}
Summary
- Enable the feature: Add
unstable-custom-transportsto yourCargo.tomlto access the trait definitions iniroh/src/socket/transports/custom.rs. - Implement three traits: Provide
CustomTransportas a factory,CustomEndpointfor receive logic and address discovery, andCustomSenderfor transmit operations. - Register with the builder: Use
add_custom_transport()on theEndpoint::builder()to register your implementation, optionally clearing default IP or relay transports. - Control path selection: Implement
PathSelectorto prioritize custom paths over standard network routes, usingpath_selector()on the builder. - Reference the example: Study
iroh/examples/custom-transport.rsfor a complete end-to-end implementation including test network simulation.
Frequently Asked Questions
What Cargo feature is required to configure custom transports in iroh?
You must enable the unstable-custom-transports feature in your Cargo.toml because the API is not yet stabilized. This feature exposes the CustomTransport, CustomEndpoint, and CustomSender traits in iroh/src/socket/transports/custom.rs.
How does iroh decide which transport to use for a connection?
Iroh evaluates all available paths (IP, relay, and custom) through the PathSelector trait. The default selector prefers IPv6, then IPv4, then relay. By providing a custom PathSelector implementation and registering it via path_selector(), you can prioritize custom transports based on address type, latency, or other application-specific criteria.
Can custom transports run alongside standard IP networking?
Yes. You can register custom transports alongside built-in transports by using add_custom_transport() without calling clear_ip_transports() or clear_relay_transports(). The endpoint will advertise all available addresses, and the path selector will choose the appropriate transport for each connection attempt.
What performance considerations apply to custom transport implementations?
Your CustomEndpoint should implement max_transmit_segments() to indicate GSO support if your underlying hardware supports batching. The poll_recv() and poll_send() methods integrate with iroh's async runtime, so they should be non-blocking and return Poll::Pending when waiting for I/O to avoid blocking the connection driver.
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 →