How to Use ALPN in iroh QUIC Connections: Protocol Negotiation Explained

Application-Layer Protocol Negotiation (ALPN) is a TLS extension that allows iroh peers to agree on which application protocol to use over a QUIC connection, ensuring that both endpoints handle streams with the correct protocol handler.

In the n0-computer/iroh framework, ALPN serves as the foundation for protocol selection in QUIC connections. Understanding how to configure and use ALPN in iroh QUIC connections is essential for building robust peer-to-peer applications that can negotiate compatible protocols during the TLS handshake.

What is ALPN and Why It Matters in iroh

ALPN is a TLS extension that lets two endpoints agree on which application-specific protocol will run over a connection. In iroh, it serves three critical functions:

  1. Protocol selection – Both peers must present matching ALPN identifiers; otherwise, the connection is rejected.
  2. Endpoint configuration – You must declare which ALPN strings an endpoint accepts for inbound connections.
  3. Connection initiation – The client specifies which ALPN to request when connecting to a remote peer.

Protocol Selection and Validation

According to the iroh source code in iroh/src/lib.rs, the ALPN identifier acts as a gatekeeper. If the ALPN values do not match between connecting peers, the connection establishment fails immediately. This guarantees that streams opened later are interpreted by the correct handler.

The ALPN Data Format

ALPN values are opaque byte strings (&[u8]). While iroh does not enforce a naming scheme, the convention is to prefix identifiers with /iroh/, such as /iroh/echo/1.

Configuring ALPN on the Server Side

To accept incoming connections, an iroh endpoint must explicitly register the ALPN strings it supports. In iroh/src/endpoint.rs, the builder pattern provides the alpns() method for this purpose. If the ALPN list is empty, the endpoint can still initiate outbound connections, but it cannot accept any inbound ones.

use iroh::{endpoint, Endpoint};
use iroh::endpoint::presets;

// Define the protocol identifier for this service
const MY_ALPN: &[u8] = b"/iroh/my-protocol/1";

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Build an endpoint that will accept connections using MY_ALPN
    let ep = Endpoint::builder(presets::N0)
        .alpns(vec![MY_ALPN.to_vec()])   // <‑‑ register the ALPN we accept
        .bind()
        .await?;

    // Wait for an inbound connection
    let conn = ep.accept().await?.await?;

    // Verify that the negotiated ALPN matches what we expect
    assert_eq!(conn.alpn(), MY_ALPN);

    // Use the connection (e.g., open a stream) …
    let (mut send, mut recv) = conn.open_bi().await?;
    // …
    Ok(())
}

Initiating Connections with ALPN

When connecting to a remote peer, the client specifies the desired ALPN using the connect() method. As implemented in iroh/src/lib.rs, this ALPN is sent during the QUIC handshake and must match one advertised by the remote endpoint.

use iroh::{Endpoint, EndpointAddr};
use iroh::endpoint::presets;

const MY_ALPN: &[u8] = b"/iroh/my-protocol/1";

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create a client endpoint (no ALPN list needed for outgoing)
    let ep = Endpoint::builder(presets::N0).bind().await?;

    // The remote endpoint’s address (could be a relay URL or direct address)
    let remote: EndpointAddr = /* obtain address */;
    
    // Initiate the connection, passing the ALPN we want to use
    let conn = ep.connect(remote, MY_ALPN).await?;
    
    // The remote side will only accept this if it advertised MY_ALPN
    assert_eq!(conn.alpn(), MY_ALPN);

    // Use the connection …
    let (mut send, mut recv) = conn.open_bi().await?;
    // …
    Ok(())
}

Supporting Multiple Protocol Versions

For backward compatibility, iroh allows advertising additional ALPN identifiers beyond the primary one. The connect_with_opts() function accepts a ConnectOptions struct that can include extra ALPN strings. This is useful when supporting older clients that use a previous protocol identifier.

use iroh::{Endpoint, ConnectOptions};
use iroh::endpoint::presets;

const NEW_ALPN: &[u8] = b"/iroh/my-protocol/2";
const LEGACY_ALPN: &[u8] = b"/iroh/my-protocol/1";

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let ep = Endpoint::builder(presets::N0).bind().await?;
    let remote = /* obtain address */;

    let opts = ConnectOptions::new()
        .alpns(vec![LEGACY_ALPN.to_vec()]); // advertise the legacy identifier

    let conn = ep.connect_with_opts(remote, NEW_ALPN, opts).await?;
    
    // The remote endpoint will negotiate either NEW_ALPN or LEGACY_ALPN
    // conn.alpn() reveals which identifier was finally chosen
    Ok(())
}

The remote endpoint will negotiate either NEW_ALPN or LEGACY_ALPN; the resulting connection’s conn.alpn() method reveals which identifier was finally chosen.

Summary

  • ALPN prevents protocol mismatches by requiring both iroh peers to present identical protocol identifiers before a QUIC connection is established.
  • Server endpoints must register ALPNs using the alpns() builder method in iroh/src/endpoint.rs to accept inbound connections.
  • Clients specify ALPN during connection using connect() or connect_with_opts() as defined in iroh/src/lib.rs.
  • Use ConnectOptions to advertise multiple protocol versions for backward compatibility during the handshake.

Frequently Asked Questions

What happens if the ALPN doesn't match between iroh peers?

The connection is rejected during the TLS handshake. According to the implementation in iroh/src/lib.rs, both endpoints must agree on the same ALPN identifier; otherwise, the connection establishment fails immediately, preventing protocol confusion.

Can an iroh endpoint accept multiple different ALPNs?

Yes. The alpns() method in iroh/src/endpoint.rs accepts a Vec<Vec<u8>>, allowing you to register multiple protocol identifiers. The endpoint will accept connections that match any of the registered ALPNs.

Is the /iroh/ prefix required for ALPN identifiers in iroh?

No. ALPN values are opaque byte strings (&[u8]), and iroh does not impose any naming constraints. The /iroh/ prefix is merely a convention used within the ecosystem to avoid collisions.

How do I verify which ALPN was negotiated in an iroh connection?

Call the alpn() method on the Connection object after the handshake completes. This returns the byte string that was agreed upon, which you can assert against your expected constants to ensure the correct protocol handler is used.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →