# 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 ```toml [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 ```rust #[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. ```rust impl RemoteConnection for IrohRemoteConnection { fn clone_boxed(&self) -> Box { ... } fn open_bi(&self) -> BoxFuture> { // Delegates to connection.open_bi() } fn zero_rtt_accepted(&self) -> BoxFuture { // Always true — fully authenticated connection } } ``` **Note:** This stops working when the underlying connection is closed. For automatic reconnection, use `IrohLazyRemoteConnection`. ### IrohZrttRemoteConnection ```rust #[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. ```rust impl RemoteConnection for IrohZrttRemoteConnection { fn open_bi(&self) -> BoxFuture> { // Delegates to the 0-RTT connection's open_bi() } fn zero_rtt_accepted(&self) -> BoxFuture { // 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 ```rust #[derive(Debug, Clone)] pub struct IrohLazyRemoteConnection(Arc); struct IrohRemoteConnectionInner { endpoint: iroh::Endpoint, addr: iroh::EndpointAddr, connection: tokio::sync::Mutex>, alpn: Vec, } ``` 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>` 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 ```rust pub fn client( endpoint: iroh::Endpoint, addr: impl Into, alpn: impl AsRef<[u8]>, ) -> irpc::Client ``` Convenience function to create a `Client` using iroh. Creates an `IrohLazyRemoteConnection` and wraps it with `Client::boxed()`. ## Server-Side: IrohProtocol ### IrohProtocol ```rust pub struct IrohProtocol { handler: Handler, request_id: AtomicU64, } ``` Implements `iroh::protocol::ProtocolHandler`, allowing it to be registered with iroh's `Router`: ```rust impl ProtocolHandler for IrohProtocol { 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:** ```rust let protocol = IrohProtocol::with_sender(local_sender); // or let protocol = IrohProtocol::new(handler); let router = Router::builder(endpoint) .accept(ALPN, protocol) .spawn(); ``` ### Iroh0RttProtocol ```rust pub struct Iroh0RttProtocol { ... } ``` Supports 0-RTT connections by implementing `ProtocolHandler::on_accepting()`: ```rust impl ProtocolHandler for Iroh0RttProtocol { async fn on_accepting(&self, accepting: Accepting) -> Result { 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 . ### IncomingRemoteConnection Trait ```rust pub trait IncomingRemoteConnection { fn accept_bi(&self) -> impl Future> + Send; fn close(&self, error_code: VarInt, reason: &[u8]); fn remote_id(&self) -> Result; } ``` 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) ```rust pub async fn handle_connection( connection: &impl IncomingRemoteConnection, handler: Handler, ) -> 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`: ```rust pub async fn read_request( connection: &impl IncomingRemoteConnection, ) -> io::Result> pub async fn read_request_raw( connection: &impl IncomingRemoteConnection, ) -> io::Result> ``` ## listen (iroh variant) ```rust pub async fn listen(endpoint: iroh::Endpoint, handler: Handler) ``` Accepts connections from an iroh `Endpoint` and handles them with the provided handler. Uses `n0_future::task::JoinSet` for task management. ## Example Usage ### Server ```rust 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)] Get(String), } async fn server() -> Result<()> { let (tx, rx) = tokio::sync::mpsc::channel(16); tokio::task::spawn(actor(rx)); let client = Client::::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 ```rust async fn connect(endpoint_id: EndpointId) -> Result> { 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> { let conn = endpoint.connect(addr, ALPN).await?; Ok(Client::boxed(IrohRemoteConnection::new(conn))) } ``` ### 0-RTT Client ```rust async fn connect_0rtt(endpoint: Endpoint, addr: EndpointAddr) -> Result> { 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))) } } } ```