When to Use Custom Transports in Iroh: 5 Scenarios and Implementation Guide
Use custom transports in iroh when you need to operate over non-IP networks like Bluetooth or Tor, require specialized hardware offloading, or need deterministic testing environments while retaining iroh's high-level endpoint and connection management.
The n0-computer/iroh networking stack abstracts packet-level I/O behind modular transports. While the default configuration uses UDP-based QUIC with optional relay support for NAT traversal, implementing custom transports in iroh allows you to integrate alternative packet-level mediums such as Bluetooth Low Energy, Tor, or proprietary radio links.
When to Use Custom Transports in Iroh
The iroh codebase provides specific extension points for custom transports through the unstable-custom-transport feature flag. Consider implementing a custom transport when you encounter these five scenarios:
-
Non-IP environments: Bluetooth Low Energy, LoRa, or other radio links that cannot use the built-in UDP stack. Custom transports expose a
CustomAddrtype for these links. -
Privacy-oriented routing: Tor or I2P tunnels where you want the transport to handle its own encryption and obfuscation. Iroh treats the entire tunnel as a black-box address.
-
Performance-specific needs: Zero-copy batch sending (GSO) or hardware offloading that requires implementing
max_transmit_segmentsto enable larger batch sizes than the regular UDP transport provides. -
Testing and simulation: In-memory mock networks that replace physical networking with deterministic channels, as demonstrated in the test utilities.
-
Special address semantics: When you need to attach extra metadata to addresses, such as device IDs or encryption keys.
CustomAddrcan carry arbitrary opaque data, and the transport exposes this viaRecvInfo.
When you add a custom transport, you also gain access to a path selector that can prioritize custom paths over standard IP paths. This is essential when the custom path offers lower latency, higher reliability, or stronger security guarantees.
Architecture of Custom Transports
The custom transport system in iroh follows a trait-based architecture that integrates with the endpoint builder and path selection logic.
Transport Registration and Builder API
Custom transports are registered through the endpoint builder using add_custom_transport, which is gated behind the unstable-custom-transport feature. In iroh/src/endpoint.rs, this method stores a TransportConfig::Custom inside the endpoint configuration:
#[cfg(feature = "unstable-custom-transports")]
pub fn add_custom_transport(mut self, factory: Arc<dyn CustomTransport>) -> Self {
self.transports.push(TransportConfig::Custom(factory));
self
}
This registration pattern allows the endpoint to instantiate your transport during the binding phase.
Core Traits: CustomTransport and CustomEndpoint
A custom transport implements the CustomTransport trait, which creates a CustomEndpoint instance. The endpoint provides a local address watcher, a sender factory, and a poll-based receive loop. The trait definitions in iroh/src/socket/transports/custom.rs specify:
pub trait CustomTransport: std::fmt::Debug + Send + Sync + 'static {
fn bind(&self) -> io::Result<Box<dyn CustomEndpoint>>;
}
pub trait CustomEndpoint: std::fmt::Debug + Send + Sync + 'static {
fn watch_local_addrs(&self) -> n0_watcher::Direct<Vec<CustomAddr>>;
fn create_sender(&self) -> Arc<dyn CustomSender>;
fn poll_recv(&mut self, …) -> Poll<io::Result<usize>>;
fn max_transmit_segments(&self) -> NonZeroUsize { … }
}
Address Representation with CustomAddr
Custom transports use CustomAddr to represent network addresses. Defined in iroh-base/src/endpoint_addr.rs, this struct embeds a transport ID (registered in TRANSPORTS.md) and opaque address bytes:
pub struct CustomAddr { /* … */ }
This design allows custom transports to carry arbitrary addressing information while maintaining type safety within the iroh networking stack.
Path Selection and Prioritization
The default path selector prefers IPv6, then IPv4, then relays. By supplying your own PathSelector implementation, you can force the custom transport to win whenever a viable path exists. The example in iroh/examples/custom-transport.rs demonstrates a PreferTestTransport selector:
impl PathSelector for PreferTestTransport {
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() == TEST_TRANSPORT_ID)
) {
selection.set(&p);
return selection;
}
// fall back to lowest‑RTT
…
}
}
Performance Optimization with Segment Offloading
Custom transports can implement max_transmit_segments to expose kernel-level GSO (Generic Segmentation Offload) capabilities. The default implementation in iroh/src/socket/transports/custom.rs returns NonZeroUsize::MIN, but overriding this allows higher throughput through larger batch sizes:
fn max_transmit_segments(&self) -> NonZeroUsize { NonZeroUsize::MIN }
Implementation Example
Below is a minimal implementation that plugs in a dummy in-memory transport. This example uses the test utilities from iroh/src/test_utils/test_transport.rs and demonstrates custom path selection:
use std::{sync::Arc, time::Duration};
use iroh::{
Endpoint, SecretKey, TransportAddr,
endpoint::{
Builder, Connection, presets,
transports::{Addr, PathSelection, PathSelectionContext, PathSelector},
},
protocol::{AcceptError, ProtocolHandler, Router},
test_utils::test_transport::{TEST_TRANSPORT_ID, TestNetwork, TestTransport},
};
use n0_error::Result;
/// Prefer the test custom transport above all others.
#[derive(Debug)]
struct PreferTestTransport;
impl PathSelector for PreferTestTransport {
fn select(&self, ctx: &PathSelectionContext<'_>) -> PathSelection {
let mut sel = PathSelection::none();
if let Some(p) = ctx.paths().find(|p|
matches!(p.network_path().remote(), Addr::Custom(c) if c.id() == TEST_TRANSPORT_ID)
) {
sel.set(&p);
return sel;
}
// fallback to the lowest‑RTT path
if let Some(p) = ctx.paths()
.filter_map(|p| p.stats().map(|s| (p, s.rtt)))
.min_by_key(|(_, rtt)| *rtt)
.map(|(p, _)| p)
{
sel.set(&p);
}
sel
}
}
#[tokio::main]
async fn main() -> Result<()> {
// Create a simulated network that supplies the custom transport.
let net = TestNetwork::new();
let secret_a = SecretKey::from([0u8; 32]);
let secret_b = SecretKey::from([1u8; 32]);
// Build two endpoints, each equipped with the test transport.
let t_a = net.create_transport(secret_a.public())?;
let ep_a = Endpoint::builder(presets::N0)
.secret_key(secret_a.clone())
.preset(t_a)
.path_selector(Arc::new(PreferTestTransport))
.bind()
.await?;
let t_b = net.create_transport(secret_b.public())?;
let ep_b = Endpoint::builder(presets::N0)
.secret_key(secret_b.clone())
.preset(t_b)
.path_selector(Arc::new(PreferTestTransport))
.bind()
.await?;
// Simple echo protocol.
#[derive(Debug, Clone)]
struct Echo;
impl ProtocolHandler for Echo {
async fn accept(&self, conn: Connection) -> Result<(), AcceptError> {
let (mut send, mut recv) = conn.accept_bi().await?;
tokio::io::copy(&mut recv, &mut send).await?;
send.finish()?;
Ok(())
}
}
// Server side
let _router = Router::builder(ep_b).accept(b"iroh-example/echo/0", Echo).spawn();
// Client side – connect using only the remote endpoint id.
let conn = ep_a.connect(secret_b.public(), b"iroh-example/echo/0").await?;
// Verify that the selected path is our custom transport.
let selected = conn.paths().iter().find(|p| p.is_selected()).unwrap();
assert!(matches!(selected.remote_addr(),
TransportAddr::Custom(c) if c.id() == TEST_TRANSPORT_ID));
// Perform a round‑trip.
let (mut send, mut recv) = conn.open_bi().await?;
send.write_all(b"hello").await?;
send.finish().await?;
let mut buf = Vec::new();
recv.read_to_end(&mut buf).await?;
assert_eq!(buf, b"hello");
Ok(())
}
Custom addresses are advertised via the address-lookup service. The test transport registers its address in TestAddrLookup::resolve, ensuring peers can discover the custom path just like any other transport:
TransportAddr::Custom(CustomAddr::from_parts(TEST_TRANSPORT_ID, endpoint_id.as_bytes()))
Summary
- Custom transports in iroh enable non-IP networking through the
unstable-custom-transportfeature flag and theadd_custom_transportmethod. - Implement
CustomTransportandCustomEndpointfromiroh/src/socket/transports/custom.rsto create alternative packet-level mediums. - Use
CustomAddrfor transport-specific addressing with arbitrary metadata. - Override
max_transmit_segmentsto enable hardware offloading and GSO for performance-critical applications. - Provide a custom
PathSelectorto prioritize custom transports over default IP paths based on latency, reliability, or security requirements.
Frequently Asked Questions
How do I enable custom transport support in iroh?
Enable the unstable-custom-transports feature in your Cargo.toml when depending on iroh. This exposes the add_custom_transport method on the endpoint builder and the necessary trait definitions in iroh/src/socket/transports/custom.rs.
What is the difference between a custom transport and the default QUIC transport?
The default transport uses UDP-based QUIC with optional relay support for NAT traversal. A custom transport replaces the underlying packet I/O layer entirely, allowing you to use Bluetooth, Tor, in-memory channels, or proprietary hardware while retaining iroh's high-level connection management, encryption, and protocol handling.
Can I use multiple custom transports simultaneously?
Yes. The endpoint builder accepts multiple transports, and you can register several custom transports alongside the default IP transport. Use a custom PathSelector implementation to prioritize between them based on availability, latency, or specific transport IDs.
How does path selection work with custom transports?
The default selector prefers IPv6, then IPv4, then relays. When you implement a custom PathSelector, you can inspect the PathSelectionContext to find paths using your custom transport via Addr::Custom matching, and force selection of those paths when they provide better connectivity or security characteristics than standard IP routes.
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 →