Files
alknet/docs/research/references/iroh/irpc/07-irpc-iroh.md

8.1 KiB

irpc: irpc-iroh — Iroh Transport Integration

The irpc-iroh crate provides transport integration for iroh, enabling irpc to work with iroh's QUIC connections that use endpoint IDs (rather than socket addresses) for routing.

Crate Overview

[package]
name = "irpc-iroh"
version = "0.13.0"
description = "Iroh transport for irpc"

Dependencies: iroh, irpc, tokio, tracing, serde, postcard, n0-error, n0-future

Key Types

IrohRemoteConnection

#[derive(Debug, Clone)]
pub struct IrohRemoteConnection(Connection);

Wraps an existing iroh Connection. Simplest way to use irpc with iroh — create a connection externally and wrap it.

impl RemoteConnection for IrohRemoteConnection {
    fn clone_boxed(&self) -> Box<dyn RemoteConnection> { ... }
    fn open_bi(&self) -> BoxFuture<Result<(SendStream, RecvStream), RequestError>> {
        // Delegates to connection.open_bi()
    }
    fn zero_rtt_accepted(&self) -> BoxFuture<bool> {
        // Always true — fully authenticated connection
    }
}

Note: This stops working when the underlying connection is closed. For automatic reconnection, use IrohLazyRemoteConnection.

IrohZrttRemoteConnection

#[derive(Debug, Clone)]
pub struct IrohZrttRemoteConnection(OutgoingZeroRttConnection);

Wraps an iroh 0-RTT (Zero Round Trip Time) connection. This enables sending data before the full handshake completes for reduced latency on reconnections.

impl RemoteConnection for IrohZrttRemoteConnection {
    fn open_bi(&self) -> BoxFuture<Result<(SendStream, RecvStream), RequestError>> {
        // Delegates to the 0-RTT connection's open_bi()
    }
    fn zero_rtt_accepted(&self) -> BoxFuture<bool> {
        // Actually checks handshake_completed() to determine
        // if 0-RTT data was accepted
    }
}

The zero_rtt_accepted() method:

  • Returns true if ZeroRttStatus::Accepted
  • Returns false if ZeroRttStatus::Rejected or on error
  • This allows the Client to decide whether to re-send data

IrohLazyRemoteConnection

#[derive(Debug, Clone)]
pub struct IrohLazyRemoteConnection(Arc<IrohRemoteConnectionInner>);

struct IrohRemoteConnectionInner {
    endpoint: iroh::Endpoint,
    addr: iroh::EndpointAddr,
    connection: tokio::sync::Mutex<Option<Connection>>,
    alpn: Vec<u8>,
}

The lazy connection caches the underlying iroh Connection and reconnects automatically:

  1. On first open_bi(), establishes a connection via endpoint.connect(addr, alpn)
  2. Caches the connection in a Mutex<Option<Connection>>
  3. On subsequent open_bi(), tries to reuse the cached connection
  4. If the cached connection fails, clears the cache and reconnects once

The alpn field is required because iroh connections need an ALPN protocol identifier.

client() Function

pub fn client<S: irpc::Service>(
    endpoint: iroh::Endpoint,
    addr: impl Into<iroh::EndpointAddr>,
    alpn: impl AsRef<[u8]>,
) -> irpc::Client<S>

Convenience function to create a Client<S> using iroh. Creates an IrohLazyRemoteConnection and wraps it with Client::boxed().

Server-Side: IrohProtocol

IrohProtocol

pub struct IrohProtocol<R> {
    handler: Handler<R>,
    request_id: AtomicU64,
}

Implements iroh::protocol::ProtocolHandler, allowing it to be registered with iroh's Router:

impl<R: DeserializeOwned + Send + 'static> ProtocolHandler for IrohProtocol<R> {
    async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
        // Handle the connection using irpc's handle_connection
        let handler = self.handler.clone();
        let fut = handle_connection(&connection, handler).map_err(AcceptError::from_err);
        fut.instrument(span).await
    }
}

Usage:

let protocol = IrohProtocol::with_sender(local_sender);
// or
let protocol = IrohProtocol::new(handler);

let router = Router::builder(endpoint)
    .accept(ALPN, protocol)
    .spawn();

Iroh0RttProtocol

pub struct Iroh0RttProtocol<R> { ... }

Supports 0-RTT connections by implementing ProtocolHandler::on_accepting():

impl<R: DeserializeOwned + Send + 'static> ProtocolHandler for Iroh0RttProtocol<R> {
    async fn on_accepting(&self, accepting: Accepting) -> Result<Connection, AcceptError> {
        let zrtt_conn = accepting.into_0rtt();
        // Handle 0-RTT data immediately
        handle_connection(&zrtt_conn, handler).await?;
        // Wait for handshake completion
        let conn = zrtt_conn.handshake_completed().await?;
        Ok(conn)
    }

    async fn accept(&self, _connection: Connection) -> Result<(), AcceptError> {
        // Noop — handled in on_accepting
        Ok(())
    }
}

Warning: 0-RTT data is replayable. Only use for idempotent operations. See https://www.iroh.computer/blog/0rtt-api.

IncomingRemoteConnection Trait

pub trait IncomingRemoteConnection {
    fn accept_bi(&self) -> impl Future<Output = Result<(SendStream, RecvStream), ConnectionError>> + Send;
    fn close(&self, error_code: VarInt, reason: &[u8]);
    fn remote_id(&self) -> Result<EndpointId, RemoteEndpointIdError>;
}

Abstraction over Connection and IncomingZeroRttConnection, enabling handle_connection and read_request to work with both regular and 0-RTT connections.

Implemented for:

  • Connection — regular iroh connection
  • IncomingZeroRttConnection — 0-RTT connection

handle_connection (iroh variant)

pub async fn handle_connection<R: DeserializeOwned + 'static>(
    connection: &impl IncomingRemoteConnection,
    handler: Handler<R>,
) -> io::Result<()>

Similar to the noq version but works with iroh's IncomingRemoteConnection trait. Records the remote endpoint ID in the tracing span.

read_request and read_request_raw (iroh variants)

Same logic as the noq versions but using IncomingRemoteConnection instead of noq::Connection:

pub async fn read_request<S: RemoteService>(
    connection: &impl IncomingRemoteConnection,
) -> io::Result<Option<S::Message>>

pub async fn read_request_raw<R: DeserializeOwned + 'static>(
    connection: &impl IncomingRemoteConnection,
) -> io::Result<Option<(R, RecvStream, SendStream)>>

listen (iroh variant)

pub async fn listen<R: DeserializeOwned + 'static>(endpoint: iroh::Endpoint, handler: Handler<R>)

Accepts connections from an iroh Endpoint and handles them with the provided handler. Uses n0_future::task::JoinSet for task management.

Example Usage

Server

use irpc::{rpc_requests, channel::oneshot, Client, WithChannels};
use irpc_iroh::IrohProtocol;
use iroh::{endpoint::presets, protocol::Router, Endpoint};

#[rpc_requests(message = FooMessage)]
#[derive(Debug, Serialize, Deserialize)]
enum FooProtocol {
    #[rpc(tx=oneshot::Sender<String>)]
    Get(String),
}

async fn server() -> Result<()> {
    let (tx, rx) = tokio::sync::mpsc::channel(16);
    tokio::task::spawn(actor(rx));
    let client = Client::<FooProtocol>::local(tx);

    let endpoint = Endpoint::bind(presets::N0).await?;
    let protocol = IrohProtocol::with_sender(client.as_local().unwrap());
    let router = Router::builder(endpoint).accept(ALPN, protocol).spawn();
    // ... keep running
}

Client

async fn connect(endpoint_id: EndpointId) -> Result<Client<FooProtocol>> {
    let endpoint = Endpoint::bind(presets::N0).await?;
    let client = irpc_iroh::client(endpoint, endpoint_id, ALPN);
    Ok(client)
}

// Or with direct connection:
async fn connect_direct(endpoint: Endpoint, addr: EndpointAddr) -> Result<Client<FooProtocol>> {
    let conn = endpoint.connect(addr, ALPN).await?;
    Ok(Client::boxed(IrohRemoteConnection::new(conn)))
}

0-RTT Client

async fn connect_0rtt(endpoint: Endpoint, addr: EndpointAddr) -> Result<Client<EchoProtocol>> {
    let connecting = endpoint.connect_with_opts(addr, ALPN, Default::default()).await?;
    match connecting.into_0rtt() {
        Ok(conn) => Ok(Client::boxed(IrohZrttRemoteConnection::new(conn))),
        Err(connecting) => {
            let conn = connecting.await?;
            Ok(Client::boxed(IrohRemoteConnection::new(conn)))
        }
    }
}