# How iroh's Socket Layer Handles UDP and QUIC Packets: Transport Abstraction Deep Dive

> Discover how iroh's Socket layer unifies UDP and QUIC packet handling. Learn about its transport abstraction, datagram processing, and seamless coexistence of raw UDP and QUIC traffic.

- Repository: [number zero/iroh](https://github.com/n0-computer/iroh)
- Tags: deep-dive
- Published: 2026-07-11

---

**iroh's `Socket` struct aggregates multiple transport implementations (IP-UDP, relay-WebSocket, custom) into a unified interface, normalizes incoming UDP datagram metadata through `process_datagrams`, and forwards them to a `noq` QUIC endpoint while disabling the QUIC grease-bit to allow seamless coexistence of raw UDP and QUIC traffic.**

The iroh networking stack (n0-computer/iroh) separates raw UDP datagram handling from QUIC protocol logic through a sophisticated transport abstraction layer. By implementing a pluggable socket architecture in [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs), iroh enables packets to flow through direct IP sockets, relay connections, or custom transports while presenting a unified interface to the QUIC engine.

## The Socket Architecture: Abstracting UDP Transports

### Transport Aggregation Layer

When an endpoint is built via `EndpointInner::bind`, iroh creates a `Transport` object through `Transport::new` that aggregates concrete transport implementations. This design allows the socket to simultaneously handle IP-UDP sockets, relay-WebSocket connections, and custom user-defined transports.

The transport list is stored in `Socket.transports` and supplies a unified receive API for the socket actor. This abstraction enables the same socket instance to receive packets from multiple network paths without the higher-level code needing to distinguish between transport types.

### Transport Configuration and Binding

The socket supports three primary transport modes configured through the `TransportConfig` enum:

- **IP transport**: Direct UDP socket binding via `transports::Ip`
- **Relay transport**: WebSocket-based relay connections via `transports::Relay`
- **Custom transport**: User-provided transport implementations via `transports::Custom`

When binding, the socket iterates through the configured transports and initializes each according to its specific requirements, storing the resulting transport instances in the `Socket` struct.

## Receiving UDP Packets in iroh

### The process_datagrams Method

All transports forward incoming datagrams to the socket via the `process_datagrams` method (lines **276‑312** in [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs)). This function receives:

- A slice of `io::IoSliceMut` buffers (`bufs`) containing the raw packet data
- A slice of `noq_udp::RecvMeta` structures (`metas`) containing UDP metadata (source address, length, stride, ECN)
- A slice of `transports::RecvInfo` providing transport-specific context

The method iterates over received packets, normalizes length and stride values, updates metrics, and performs crucial address mapping to ensure the rest of the stack can process packets uniformly regardless of their transport origin.

### Address Mapping for Relay and Custom Transports

The socket converts transport-specific addresses (`transports::Addr`) into *mapped* addresses through three distinct paths:

**IP addresses** are recorded directly as IPv4 or IPv6 in the metrics without transformation.

**Relay addresses** (lines **87‑91** in [`iroh/src/socket/transports/relay.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/relay.rs)) resolve through `self.mapped_addrs.relay_addrs`. When a packet arrives via a relay connection, the socket looks up the private socket address using the relay URL and node ID as a key:

```rust
transports::Addr::Relay(src_url, src_node) => {
    let mapped = self.mapped_addrs.relay_addrs.get(&(src_url.clone(), *src_node));
    noq_meta.addr = mapped.private_socket_addr();
}

```

**Custom transport addresses** (lines **58‑62** in [`iroh/src/socket/transports/custom.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/custom.rs)) resolve similarly via `self.mapped_addrs.custom_addrs`:

```rust
transports::Addr::Custom(remote) => {
    let mapped = self.mapped_addrs.custom_addrs.get(remote);
    noq_meta.addr = mapped.private_socket_addr();
}

```

This mapping enables the socket to treat every packet as if it arrived on a normal UDP socket while preserving the original logical path (direct, relay, or custom).

### Metrics and Metadata Handling

Throughout the receive path, iroh collects detailed per-transport metrics via `self.metrics.socket.recv_*`. These statistics feed into the endpoint's runtime monitoring, exposing counters such as `udp_tx.bytes` and `udp_rx.bytes` that allow users to monitor both UDP and QUIC traffic separately.

## Integrating QUIC with Raw UDP

### The noq QUIC Endpoint

iroh relies on the **noq** library for QUIC protocol handling. The socket creates a `noq::Endpoint` during `EndpointInner::bind` (lines **262‑273** in [`socket.rs`](https://github.com/n0-computer/iroh/blob/main/socket.rs)) with an abstract UDP socket provided by the `Transport` layer.

The `noq::Endpoint` pulls datagrams from the transport through the same `process_datagrams` callback used for raw UDP traffic. This integration ensures that QUIC packets flow through the same normalization and address mapping pipeline as standard UDP datagrams.

### Bypassing the QUIC Grease Bit for Non-QUIC Traffic

To support forwarding *non-QUIC* UDP traffic unchanged, iroh disables the QUIC "grease-bit" check during endpoint configuration (lines **215‑219** in [`socket.rs`](https://github.com/n0-computer/iroh/blob/main/socket.rs)):

```rust
endpoint_config.grease_quic_bit(false);

```

This configuration tells the QUIC stack to ignore packets whose fixed QUIC bit is zero, allowing arbitrary UDP payloads to pass straight through to the application layer without triggering QUIC parsing errors. This mechanism enables seamless coexistence of QUIC connections and raw UDP datagrams within the same socket instance.

## Sending Packets Through the Abstract Socket

When higher-level code issues a QUIC send operation, the `noq` endpoint writes to the abstract socket, which routes the datagram through the appropriate transport (direct UDP socket, relay WebSocket, or custom transport).

The transport modules utilize the `noq_udp::Transmit` structure, which carries the destination address and ECN information. For example, in [`iroh/src/socket/transports/ip.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/ip.rs) (line **189**) and [`iroh/src/socket/transports/relay.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/relay.rs) (line **87**), the `RecvMeta` handling ensures that transmission metadata flows correctly through the abstraction layer.

```rust
// Creating a socket with both UDP and relay transports
let socket = Socket::bind(Options {
    transports: vec![
        TransportConfig::Ip { bind_addr: "0.0.0.0:0".parse().unwrap() },
        TransportConfig::Relay { url: relay_url, relay_map: default_map },
    ],
    // … other options …
}).await?;

```

```rust
// Sending a QUIC packet – the `noq` endpoint writes through the abstract socket
let quic_conn = endpoint.connect(remote_id).await?;
quic_conn.send_datagram(b"hello over QUIC").await?;

```

```rust
// Receiving a pure UDP packet (non-QUIC) – because grease_quic_bit is false,
// the packet bypasses the QUIC parser and appears as a regular datagram.
let mut buf = [0u8; 1500];
let (len, src) = socket.recv_from(&mut buf).await?;
println!("Got {} bytes from {}", len, src);

```

## Summary

- **Transport abstraction** in [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs) unifies IP-UDP, relay-WebSocket, and custom transports behind a single `Socket` interface through the `Transport` aggregation layer.
- **Address mapping** converts transport-specific addresses to normalized socket addresses using `mapped_addrs.relay_addrs` and `mapped_addrs.custom_addrs`, enabling uniform packet processing regardless of transport origin.
- **QUIC integration** occurs through the `noq` library, with the socket creating a `noq::Endpoint` that pulls datagrams from the abstract transport layer.
- **Non-QUIC UDP support** is achieved by disabling `grease_quic_bit` in the endpoint configuration, allowing raw UDP traffic to coexist with QUIC connections.
- **Bidirectional flow** handles both receiving (via `process_datagrams`) and sending (via `noq_udp::Transmit`) through the same transport abstraction, with comprehensive metrics collection throughout the path.

## Frequently Asked Questions

### How does iroh distinguish between QUIC and non-QUIC UDP packets?

iroh disables the QUIC grease-bit check by calling `endpoint_config.grease_quic_bit(false)` during endpoint creation in [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs). This configuration tells the underlying `noq` QUIC engine to ignore packets that don't have the fixed QUIC bit set, allowing them to pass through as raw UDP datagrams to the application layer rather than being rejected as invalid QUIC packets.

### What is the purpose of address mapping in iroh's socket layer?

Address mapping in `process_datagrams` resolves transport-specific addresses (`transports::Addr`) into private socket addresses that the rest of the stack can understand. For relay connections, the system looks up the private address in `mapped_addrs.relay_addrs` using the relay URL and node ID as keys. For custom transports, it queries `mapped_addrs.custom_addrs`. This normalization allows the QUIC engine and application code to treat all packets uniformly regardless of whether they arrived via direct IP, relay, or custom transport.

### How does iroh handle relayed packets versus direct UDP connections?

Both relayed and direct packets flow through the same `process_datagrams` method in [`iroh/src/socket.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs). The transport layer distinguishes them by their `transports::Addr` type—IP addresses pass through unchanged, while relay addresses (lines **87‑91** in [`iroh/src/socket/transports/relay.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/relay.rs)) undergo lookup in the relay address map. The socket then converts both types to a normalized `noq_udp::RecvMeta` structure before forwarding to the QUIC engine or application code.

### Can iroh support custom transport implementations beyond UDP and relay?

Yes, iroh's transport architecture supports pluggable custom transports through the `Transport` trait and `TransportConfig::Custom` variant. Custom transports implement the same interface as the built-in IP and relay transports, allowing them to participate in the unified receive pipeline handled by `process_datagrams`. Address resolution for custom transports occurs via `mapped_addrs.custom_addrs` (lines **58‑62** in [`iroh/src/socket/transports/custom.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket/transports/custom.rs)), enabling seamless integration with the existing socket abstraction.