Skip to content

hellas-ai/tonic-iroh-transport

Repository files navigation

Crate License Documentation

tonic-iroh-transport

A transport layer that enables tonic gRPC services to run over iroh peer-to-peer connections.

Features

  • P2P gRPC: Run standard gRPC services over encrypted QUIC connections
  • NAT traversal: Automatic hole-punching with relay fallback
  • Service multiplexing: Multiple gRPC services over the same P2P connection
  • Type safety: Full integration with tonic's generated clients and servers
  • Feature split:
    • client: outbound connectors (IrohConnect, connect_alpn)
    • server: inbound transport runtime (TransportBuilder)
    • discovery: peer discovery and racing connects (swarm APIs)

How it Works

gRPC Client ←→ tonic-iroh-transport ←→ gRPC Server
                       ↓
               iroh P2P Network
            (QUIC + NAT traversal)

The transport layer bridges HTTP/2 gRPC streams with QUIC streams, enabling standard tonic applications to communicate directly peer-to-peer without requiring a centralized server.

Quick Start

This guide shows how to build a complete P2P gRPC service using the echo example.

1. Dependencies

Add to your Cargo.toml:

[dependencies]
# Default features are `client` + `server`.
tonic-iroh-transport = "0.5"
# If you use the swarm APIs in section 7, enable `discovery`:
# tonic-iroh-transport = { version = "0.5", features = ["discovery"] }
tonic = "0.14"
tonic-prost = "0.14"
prost = "0.14"
iroh = "0.97"
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
anyhow = "1.0"
tracing = "0.1"
tracing-subscriber = "0.3"

[build-dependencies]
tonic-prost-build = "0.14"

If you want a smaller build, disable defaults and opt into only what you need:

# client-only
tonic-iroh-transport = { version = "0.5", default-features = false, features = ["client"] }

2. Protocol Definition

Create proto/echo/v1/echo.proto to define your gRPC service:

syntax = "proto3";
package echo.v1;

service EchoService {
  rpc Echo(EchoRequest) returns (EchoResponse);
}

message EchoRequest {
  string message = 1;
}

message EchoResponse {
  string message = 1;
  string peer_id = 2;
}

This defines a simple service with one RPC method that echoes messages back with the peer ID.

3. Build Configuration

Create build.rs to generate Rust code from your protobuf:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    tonic_prost_build::configure()
        .out_dir("src/pb")
        .include_file("mod.rs")
        .build_transport(false)  // We provide our own transport
        .build_client(true)
        .build_server(true)
        .compile_protos(&["proto/echo/v1/echo.proto"], &["proto"])?;

    println!("cargo:rerun-if-changed=proto/echo/v1/echo.proto");
    Ok(())
}

This generates client and server code while disabling tonic's built-in transport.

4. Service Implementation

Implement your gRPC service handler:

use pb::echo::v1::{
    echo_service_server::EchoService,
    EchoRequest, EchoResponse,
};
use tonic_iroh_transport::IrohContext;

#[derive(Clone)]
struct EchoServiceImpl;

#[tonic::async_trait]
impl EchoService for EchoServiceImpl {
    async fn echo(&self, request: Request<EchoRequest>) -> Result<Response<EchoResponse>, Status> {
        // Extract peer info from the P2P connection
        let context = request.extensions().get::<IrohContext>().cloned();
        let peer_id = context
            .map(|ctx| ctx.node_id.to_string())
            .unwrap_or_else(|| "unknown".to_string());

        let req = request.into_inner();

        Ok(Response::new(EchoResponse {
            message: req.message,
            peer_id,
        }))
    }
}

The IrohContext extension provides details about the P2P connection.

5. Server Setup

Set up the P2P server with iroh transport:

use tonic_iroh_transport::TransportBuilder;
use pb::echo::v1::echo_service_server::EchoServiceServer;

#[tokio::main]
async fn main() -> Result<()> {
    // Create iroh endpoint for P2P networking
    let endpoint = iroh::Endpoint::bind(iroh::endpoint::presets::N0).await?;
    println!("Server Node ID: {}", endpoint.id());

    // Start transport with the Echo service
    let _guard = TransportBuilder::new(endpoint)
        .add_rpc(EchoServiceServer::new(EchoServiceImpl))
        .spawn()
        .await?;

    Ok(())
}

This creates an iroh endpoint, registers the service with the transport, and starts the gRPC server over iroh.

6. Client Usage

Connect to the P2P service and make calls:

use iroh::EndpointAddr;
use tonic_iroh_transport::IrohConnect;
use tonic::Request;
use pb::echo::v1::{
    echo_service_client::EchoServiceClient,
    echo_service_server::EchoServiceServer,
    EchoRequest,
};

// Create client endpoint
let client_endpoint = iroh::Endpoint::bind(iroh::endpoint::presets::N0).await?;
let server_addr = EndpointAddr::new(server_node_id);

// Connect using the IrohConnect trait
let channel = EchoServiceServer::<EchoServiceImpl>::connect(&client_endpoint, server_addr).await?;
let mut client = EchoServiceClient::new(channel);

// Make RPC calls
let request = Request::new(EchoRequest {
    message: "Hello, P2P gRPC!".to_string()
});

let response = client.echo(request).await?;
println!("Response: {}", response.into_inner().message);

The IrohConnect trait is automatically implemented for all tonic server types and handles protocol negotiation.

You can also configure connection options:

use std::time::Duration;

// With connection timeout
let channel = EchoServiceServer::<EchoServiceImpl>::connect(&client_endpoint, server_addr)
    .connect_timeout(Duration::from_secs(10))
    .await?;

7. Swarm discovery and racing connects

The DHT backend is a bootstrap mechanism, not a trust anchor. Advertisements are signed by the advertising iroh node ID, so peers cannot forge each other's ads, but the shared DHT buckets are still globally writable and can be spammed or overwritten.

use tonic_iroh_transport::swarm::{ServiceRegistry, DhtBackend};
use futures::StreamExt;
use std::time::Duration;

let dht = DhtBackend::new(&client_endpoint)?;
let mut registry = ServiceRegistry::new(&client_endpoint);
registry.add(dht);

// One-off peer lookup
let peer = registry.discover::<EchoServiceServer<EchoServiceImpl>>()
    .next()
    .await
    .ok_or(anyhow!("no peers found"))??;

// Race multiple connection attempts and take the first channel
let channel = registry
    .find::<EchoServiceServer<EchoServiceImpl>>()
    .timeout_each(Duration::from_secs(1))
    .max_inflight(8)
    .first()
    .await?;

// Or keep it as a stream of ready channels
let mut locator = registry.find::<EchoServiceServer<EchoServiceImpl>>().start();
if let Some(conn) = locator.next().await {
    let channel = conn?;
    // ...use channel...
}

Examples

  • echo - Simple echo service
  • chat - P2P chat application with multiple services

Performance

❯ ./benchmark.sh 5 32
=== P2P Chat Benchmark Test ===
Duration: 5s, Concurrency: 32

Building... OK
Starting receiver... OK
Running benchmark... OK
Shutting down receiver... OK

=== Results ===
  Duration: 5.00s
  Total messages: 204471
  Messages/sec: 40889.64
  Avg latency: 0.02ms

=== Resource Usage ===
  Receiver:         7.80 real         4.34 user        12.86 sys

Benchmark complete.

Development

# Check all crates compile
make check-all

# Run all tests
make test-all

# Format and lint
make fmt
make lint

# Build examples
make examples

License

MIT OR Apache-2.0

About

use tonic to codegen type-safe service traits and clients for e2e encrypted p2p rpcs

Resources

Stars

4 stars

Watchers

1 watching

Forks

Packages

 
 
 

Contributors