# How to Integrate iroh with Existing QUIC Implementations and Custom Socket Factories

> Integrate iroh with custom QUIC implementations and socket factories by enabling the unstable-custom-transports feature and implementing core traits for seamless networking.

- Repository: [number zero/iroh](https://github.com/n0-computer/iroh)
- Tags: how-to-guide
- Published: 2026-06-24

---

**Enable the `unstable-custom-transports` feature and implement the `CustomTransport`, `CustomEndpoint`, and `CustomSender` traits to plug any QUIC stack or socket factory into iroh's networking layer.**

The iroh networking stack builds its QUIC layer on top of the `noq` library, but exposes an extension point for swapping the underlying transport. When you need to integrate iroh with existing QUIC implementations—such as a fork of Quinn with custom TLS settings—or use a custom socket factory for constrained environments like WebAssembly or kernel-bypass networking, you can leverage the unstable custom transport API to retain iroh's protocol features while controlling the transport layer.

## Enabling the Custom Transport Feature

Access to the transport layer is gated behind the `unstable-custom-transports` Cargo feature. Add the following to your [`Cargo.toml`](https://github.com/n0-computer/iroh/blob/main/Cargo.toml):

```toml
[dependencies]
iroh = { version = "0.8", features = ["unstable-custom-transports"] }

```

This unlocks the `custom_transport` method on the endpoint builder and the trait definitions required to bridge your own networking code.

## Implementing the Three Core Traits

To integrate iroh with existing QUIC implementations, you must implement three traits defined in [`iroh/src/socket/transports/custom.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/custom.rs). These traits abstract the transport lifecycle, packet reception, and transmission.

### CustomTransport

The `CustomTransport` trait acts as a factory that produces `CustomEndpoint` instances. It defines a single method:

- `bind(&self) -> io::Result<Box<dyn CustomEndpoint>>`: Creates and returns the endpoint instance that iroh will drive.

### CustomEndpoint

The `CustomEndpoint` trait represents the local socket or QUIC endpoint. It must implement:

- `watch_local_addrs(&self) -> n0_watcher::Direct<Vec<CustomAddr>>`: Returns the local addresses the endpoint is bound to.
- `create_sender(&self) -> Arc<dyn CustomSender>`: Produces a sender handle for outgoing packets.
- `poll_recv(...) -> Poll<io::Result<usize>>`: Yields received datagrams to the `noq` state machine. This method drives all inbound traffic.
- `max_transmit_segments(&self) -> NonZeroUsize`: Indicates how many packets the transport can batch per send operation.

### CustomSender

The `CustomSender` trait handles outbound transmission:

- `is_valid_send_addr(&self, addr: &CustomAddr) -> bool`: Filters whether a destination address is supported by this transport.
- `poll_send(...) -> Poll<io::Result<()>>`: Transmits a single datagram. `noq` calls this repeatedly during its send loop.

## Registering Your Transport with the Endpoint Builder

Once your traits are implemented, register the transport using the `Builder` in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs). The `custom_transport` method accepts any `Box<dyn CustomTransport>`:

```rust
use iroh::{endpoint::Builder, socket::transports::custom::CustomTransport};

struct MyQuicTransport;
impl CustomTransport for MyQuicTransport { /* ... */ }

let endpoint = Builder::default()
    .custom_transport(Box::new(MyQuicTransport))
    .bind()
    .await?;

```

After calling `bind()`, iroh treats your implementation as the exclusive transport, disabling the default UDP socket creation.

## How the Integration Works Under the Hood

When you call `Builder::custom_transport`, the endpoint builder stores your implementation in the `Transport` enum defined in [`iroh/src/socket/transports.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports.rs). During the bind sequence:

1. The builder invokes `CustomTransport::bind` to create your `CustomEndpoint`.
2. The endpoint is wrapped in the `Transport::Custom` variant and held by the iroh endpoint.
3. Internally, iroh drives your transport through the `noq::AsyncUdpSocket` trait, calling `poll_recv` to ingest packets and `poll_send` via the `CustomSender` to egress them.
4. All QUIC-level operations—connection establishment, stream multiplexing, and datagram support—continue to be handled by `noq`, ensuring that features like hole-punching, relay fallback, and path selection remain functional regardless of the underlying socket type.

This architecture means you can swap the transport layer without altering any application-level code that uses iroh for blob transfer, gossip, or document synchronization.

## Use Cases for Custom Transports

Implementing a custom transport is useful in several scenarios where the default UDP socket is insufficient:

- **Alternative QUIC implementations**: Bridge iroh to a fork of Quinn that includes specialized TLS configuration or congestion control algorithms, while retaining iroh's protocol stack.
- **Sandboxed or restricted environments**: Use kernel-bypass sockets (DPDK, netmap) or WebAssembly networking APIs that do not expose standard UDP sockets.
- **Testing and simulation**: Inject mock transports that record traffic, simulate packet loss, or run entirely in-process for deterministic integration tests.

## Complete Implementation Example

The following example demonstrates wrapping an existing Quinn endpoint to use as a custom transport. It shows the required trait implementations and wiring:

```rust
use std::{io, net::SocketAddr, sync::Arc, task::{Context, Poll}};
use iroh::{
    endpoint::{Builder, Endpoint},
    socket::{
        transports::custom::{CustomTransport, CustomEndpoint, CustomSender},
        RecvInfo, Transmit,
    },
    base::CustomAddr,
};

/// Wraps an existing quinn::Endpoint as an iroh transport.
struct QuinnTransport(quinn::Endpoint);

impl CustomTransport for QuinnTransport {
    fn bind(&self) -> io::Result<Box<dyn CustomEndpoint>> {
        Ok(Box::new(QuinnEndpoint {
            inner: self.0.clone(),
        }))
    }
}

struct QuinnEndpoint {
    inner: quinn::Endpoint,
}

impl CustomEndpoint for QuinnEndpoint {
    fn watch_local_addrs(&self) -> n0_watcher::Direct<Vec<CustomAddr>> {
        let addr = self.inner.local_addr().unwrap();
        n0_watcher::Direct::new(vec![CustomAddr::Udp(addr)])
    }

    fn create_sender(&self) -> Arc<dyn CustomSender> {
        Arc::new(QuinnSender {
            inner: self.inner.clone(),
        })
    }

    fn poll_recv(
        &mut self,
        cx: &mut Context<'_>,
        bufs: &mut [io::IoSliceMut<'_>],
        metas: &mut [noq_udp::RecvMeta],
        recv_infos: &mut [RecvInfo],
    ) -> Poll<io::Result<usize>> {
        // Forward received datagrams from the QUIC endpoint to noq.
        // Implementation depends on how you expose raw UDP from quinn.
        Poll::Pending
    }

    fn max_transmit_segments(&self) -> std::num::NonZeroUsize {
        std::num::NonZeroUsize::new(16).unwrap()
    }
}

struct QuinnSender {
    inner: quinn::Endpoint,
}

impl CustomSender for QuinnSender {
    fn is_valid_send_addr(&self, addr: &CustomAddr) -> bool {
        matches!(addr, CustomAddr::Udp(_))
    }

    fn poll_send(
        &self,
        _cx: &mut Context<'_>,
        dst: &CustomAddr,
        _src: Option<&CustomAddr>,
        transmit: &Transmit<'_>,
    ) -> Poll<io::Result<()>> {
        match dst {
            CustomAddr::Udp(sockaddr) => {
                // Convert transmit to raw UDP send via your QUIC stack
                // Example: self.inner.send_datagram(*sockaddr, transmit.contents);
                Poll::Ready(Ok(()))
            }
            _ => Poll::Ready(Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "unsupported address",
            ))),
        }
    }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Initialize your existing QUIC endpoint
    let quinn_endpoint = quinn::Endpoint::client("[::]:0".parse::<SocketAddr>()?)?;

    // Wire it into iroh
    let iroh_endpoint: Endpoint = Builder::default()
        .custom_transport(Box::new(QuinnTransport(quinn_endpoint)))
        .bind()
        .await?;

    // Use iroh_endpoint normally for dialing, accepting, and streaming
    Ok(())
}

```

Replace the stubbed `poll_recv` and `poll_send` bodies with actual calls into your QUIC library or raw socket to complete the integration.

## Summary

- **Enable** the `unstable-custom-transports` feature in your [`Cargo.toml`](https://github.com/n0-computer/iroh/blob/main/Cargo.toml) to access the custom transport API.
- **Implement** the `CustomTransport`, `CustomEndpoint`, and `CustomSender` traits defined in [`iroh/src/socket/transports/custom.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/custom.rs) to bridge your socket factory.
- **Register** your implementation with `Builder::custom_transport` before calling `bind()` in [`iroh/src/endpoint.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/endpoint.rs).
- **Retain** all iroh features—hole-punching, relay fallback, and protocol handlers—because the QUIC state machine remains in `noq` while only the packet I/O is delegated to your code.
- **Reference** the built-in transports in [`iroh/src/socket/transports/ip.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/ip.rs) and [`relay.rs`](https://github.com/n0-computer/iroh/blob/main/relay.rs) as templates for your implementation.

## Frequently Asked Questions

### What is the performance impact of using a custom transport?

The custom transport abstraction adds one layer of trait object dispatch per packet, but the overhead is minimal compared to the system calls for network I/O. The `max_transmit_segments` method allows you to batch sends (typically up to 16 or 64 datagrams) to amortize dispatch costs, matching the performance characteristics of the built-in UDP transport.

### Can I use multiple transports simultaneously with iroh?

Currently, the `Builder::custom_transport` method accepts a single transport implementation that replaces the default UDP socket entirely. To use multiple transports (for example, a custom socket for specific interfaces and standard UDP for others), you would need to implement a composite `CustomTransport` that internally multiplexes between your desired backends.

### Which iroh features continue to work when using a custom transport?

All high-level features—including **NAT hole-punching**, **relay fallback**, **path selection**, and **protocol handlers** (blobs, gossip, docs)—continue to function unchanged. These features operate above the QUIC layer managed by `noq`, which still performs congestion control, encryption, and stream multiplexing. Your custom transport only replaces the raw packet ingress and egress.

### Where can I find reference implementations to use as templates?

The iroh source code includes two built-in transport implementations that serve as canonical examples: [`iroh/src/socket/transports/ip.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/ip.rs) implements standard UDP socket handling, and [`iroh/src/socket/transports/relay.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/relay.rs) implements relay-over-HTTP fallback. Both demonstrate proper handling of `poll_recv`, `poll_send`, and address watching in production code.