How to Integrate iroh with Custom Transport Layers (Unstable Feature)
Integrating iroh with custom transport layers requires implementing the CustomTransport and CustomEndpoint traits, registering the transport via add_custom_transport on the Endpoint builder, and configuring a PathSelector to prioritize your transport when available, all gated behind the unstable-custom-transports feature flag.
The iroh networking library provides a pluggable transport architecture that allows developers to integrate custom networking layers beyond standard UDP and relay connections. This capability enables Bluetooth, LoRa, in-memory testing channels, or proprietary protocols to plug into iroh's QUIC-based API. By implementing the core traits defined in iroh/src/endpoint/transports/mod.rs and registering your transport with the endpoint builder, you can extend iroh's path selection logic to include your custom networking medium.
Enable the Unstable Feature
Custom transports are deliberately gated behind the unstable-custom-transports feature flag to allow API iteration without breaking stable releases. To use this functionality, enable the feature in your Cargo.toml:
[dependencies]
iroh = { version = "1.0.0", features = ["unstable-custom-transports"] }
For development and testing, you can also run examples directly:
cargo run --example custom-transport --features unstable-custom-transports
Architecture Overview
The custom transport integration follows a four-stage architecture:
- Transport Registration – Add your transport to the
Endpointbuilder usingadd_custom_transport, which stores the transport in the builder's internalPresetconfiguration. - Address Discovery – Custom transports expose
CustomAddrvalues (TransportAddr::Custom) that are included in the peer'sEndpointAddrduring discovery. - Path Selection – When establishing connections, iroh evaluates candidate paths via the
PathSelectortrait. You can implement a custom selector (likePreferTestTransport) to prioritize your transport over standard IP paths. - Sending and Receiving – The transport implements
CustomSenderfor packetization and transmission, whileCustomEndpointhandles local address monitoring and packet reception viapoll_recv().
Implementing the Transport Traits
A complete custom transport requires implementing three core traits defined in iroh/src/endpoint/transports/mod.rs.
CustomTransport and CustomEndpoint
Your transport struct must implement both CustomTransport (for binding) and CustomEndpoint (for runtime operations). The reference implementation in iroh/src/test_utils/test_transport.rs demonstrates the required methods:
pub const TEST_TRANSPORT_ID: u64 = 0x20;
pub struct TestTransport {
id_watchable: n0_watcher::Watchable<Vec<CustomAddr>>,
network: TestNetwork,
id: EndpointId,
}
impl CustomTransport for TestTransport {
fn bind(&self, _socket: Option<std::net::UdpSocket>) -> Result<Arc<dyn CustomEndpoint>> {
// Return self as the endpoint
Ok(Arc::new(self.clone()))
}
}
impl CustomEndpoint for TestTransport {
fn watch_local_addrs(&self) -> Watchable<Vec<CustomAddr>> {
self.id_watchable.clone()
}
fn poll_recv(&self, cx: &mut Context) -> Poll<Result<RecvMeta>> {
// Implement packet reception logic
// ...
}
}
CustomSender
The CustomSender trait handles packet transmission. According to the test transport implementation, you must implement poll_send() to handle actual packet dispatch:
impl CustomSender for TestSender {
fn poll_send(&self, cx: &mut Context, transmits: &[Transmit]) -> Poll<Result<usize>> {
// Packetize and send via your underlying channel
// Return number of packets sent
// ...
}
}
Address Handling
Custom transports use CustomAddr::from_parts() to construct addresses with a unique transport ID and endpoint identifier:
fn to_custom_addr(&self) -> CustomAddr {
CustomAddr::from_parts(TEST_TRANSPORT_ID, self.id.as_bytes())
}
This allows the transport to identify its own addresses during path selection.
Registering with the Endpoint Builder
Register your transport using the Endpoint builder API in iroh/src/endpoint/builder.rs. The example in iroh/examples/custom-transport.rs shows the complete registration pattern:
let network = TestNetwork::new();
let secret = SecretKey::generate();
let transport = network.create_transport(secret.public())?;
let builder = Endpoint::builder(presets::N0)
.secret_key(secret)
.add_custom_transport(transport.clone())
.path_selector(Arc::new(PreferTestTransport));
The Preset implementation (visible in test_transport.rs) automatically forwards the transport to add_custom_transport() when using the .preset() method:
impl Preset for Arc<TestTransport> {
fn apply(self, builder: Builder) -> Builder {
builder
.add_custom_transport(self.clone())
.address_lookup(self.network.address_lookup())
}
}
Path Selection Strategies
The PathSelector trait in iroh/src/endpoint/path_selector.rs determines which transport wins when multiple paths exist. The custom-transport.rs example provides a PreferTestTransport implementation that prioritizes custom transport addresses:
struct PreferTestTransport;
impl PathSelector for PreferTestTransport {
fn select(&self, ctx: &PathSelectionContext<'_>) -> PathSelection {
// First, try to find any candidate on our custom transport
if let Some(p) = ctx.paths().find(|p| {
matches!(p.network_path().remote(),
Addr::Custom(c) if c.id() == TEST_TRANSPORT_ID)
}) {
let mut sel = PathSelection::none();
sel.set(&p);
return sel;
}
// Fallback: pick the lowest-RTT path
ctx.paths()
.filter_map(|p| p.stats().map(|s| (p, s.rtt)))
.min_by_key(|(_, rtt)| *rtt)
.map(|(p, _)| {
let mut sel = PathSelection::none();
sel.set(p);
sel
})
.unwrap_or_default()
}
}
Alternatively, you can use the built-in BiasedRttPathSelector with a TransportBias configuration to prefer your custom transport's AddrKind without implementing a custom selector.
Complete Integration Example
The following end-to-end example from iroh/examples/custom-transport.rs demonstrates two endpoints communicating over an in-memory custom transport:
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
// Build two endpoints that talk over the test transport
let network = TestNetwork::new();
let s1 = SecretKey::from([0u8; 32]);
let s2 = SecretKey::from([1u8; 32]);
let t1 = network.create_transport(s1.public())?;
let t2 = network.create_transport(s2.public())?;
let ep1 = Endpoint::builder(presets::N0)
.secret_key(s1)
.preset(t1)
.path_selector(Arc::new(PreferTestTransport))
.bind().await?;
let ep2 = Endpoint::builder(presets::N0)
.secret_key(s2)
.preset(t2)
.bind().await?;
// Set up a simple echo protocol
let server = Router::builder(ep2)
.accept(ALPN, Echo)
.spawn();
// Connect using only the endpoint ID (discovery returns the custom address)
let conn = ep1.connect(s2.public(), ALPN).await?;
// Verify that the selected path is the custom transport
assert!(conn.paths().iter().any(|p|
p.is_selected() && matches!(p.remote_addr(),
TransportAddr::Custom(a) if a.id() == TEST_TRANSPORT_ID)));
// Exchange a message
let (mut send, mut recv) = conn.open_bi().await?;
send.write_all(b"hello").await?;
send.finish()?;
let reply = recv.read_to_end(100).await?;
assert_eq!(reply, b"hello");
conn.close(0u32.into(), b"bye");
server.shutdown().await?;
Ok(())
}
Summary
- Enable the feature using
unstable-custom-transportsin yourCargo.tomlor cargo commands. - Implement three traits:
CustomTransportfor binding,CustomEndpointfor address watching and reception, andCustomSenderfor transmission. - Register via the builder using
add_custom_transport()or thePresetpattern demonstrated iniroh/src/test_utils/test_transport.rs. - Control path selection by implementing
PathSelectoror usingBiasedRttPathSelectorwith transport bias to prioritize your custom transport. - Verify integration by checking that
TransportAddr::Customappears in the connection's selected paths.
Frequently Asked Questions
What is the stability status of custom transports in iroh?
Custom transports are marked as unstable and require the unstable-custom-transports feature flag. This allows the iroh team to iterate on the CustomTransport, CustomEndpoint, and CustomSender trait APIs without breaking semantic versioning guarantees for the stable crate surface.
How does path selection work with custom transports?
When a connection is established, iroh evaluates all available paths (IP, relay, and custom) through the PathSelector trait. The selector receives a PathSelectionContext containing all candidate paths and their statistics. You can implement a custom selector to prefer TransportAddr::Custom addresses, or use the built-in biased RTT selector with a TransportBias configuration.
Can I use custom transports alongside standard IP and relay transports?
Yes. The builder API supports hybrid configurations. You can call add_custom_transport() while retaining the default IP and relay transports from presets::N0, or explicitly clear them using clear_ip_transports() and clear_relay_transports() if you want an isolated custom transport network.
What methods must a custom transport implement?
According to iroh/src/endpoint/transports/mod.rs, you must implement bind() on CustomTransport, and watch_local_addrs(), create_sender(), and poll_recv() on CustomEndpoint. Additionally, the CustomSender returned by create_sender() must implement poll_send() to handle packet transmission.
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 →