| Crates.io | tonic-iroh-transport |
| lib.rs | tonic-iroh-transport |
| version | 0.2.0 |
| created_at | 2025-06-14 00:35:51.787826+00 |
| updated_at | 2025-12-15 11:13:47.514915+00 |
| description | Transport layer for using tonic gRPC over iroh p2p connections |
| homepage | |
| repository | https://github.com/hellas-ai/tonic-iroh |
| max_upload_size | |
| id | 1712003 |
| size | 213,474 |
A transport layer that enables tonic gRPC services to run over iroh peer-to-peer connections.
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.
This guide shows how to build a complete P2P gRPC service using the echo example.
Add to your Cargo.toml:
[dependencies]
tonic-iroh-transport = "0.0.3" # or path = ".." in a workspace
tonic = { version = "0.13", features = ["prost"] }
prost = "0.13"
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
iroh = { version = "0.91" }
anyhow = "1.0"
tracing = "0.1"
tracing-subscriber = "0.3"
[build-dependencies]
tonic-build = "0.13"
Create proto/echo.proto to define your gRPC service:
syntax = "proto3";
package echo;
service Echo {
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.
Create build.rs to generate Rust code from your protobuf:
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_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.proto"], &["proto"])?;
println!("cargo:rerun-if-changed=proto/echo.proto");
Ok(())
}
This generates client and server code while disabling tonic's built-in transport.
Implement your gRPC service handler:
use pb::echo::{echo_server::Echo, EchoRequest, EchoResponse};
use tonic_iroh_transport::IrohContext;
#[derive(Clone)]
struct EchoService;
#[tonic::async_trait]
impl Echo for EchoService {
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.
Set up the P2P server with iroh transport:
use tonic_iroh_transport::TransportBuilder;
use pb::echo::echo_server::EchoServer;
#[tokio::main]
async fn main() -> Result<()> {
// Create iroh endpoint for P2P networking
let endpoint = iroh::Endpoint::builder().bind().await?;
println!("Server Node ID: {}", endpoint.id());
// Start transport with the Echo service
let _guard = TransportBuilder::new(endpoint)
.add_rpc(EchoServer::new(EchoService))
.spawn()
.await?;
Ok(())
}
This creates an iroh endpoint, registers the service with the transport, and starts the gRPC server over iroh.
Connect to the P2P service and make calls:
use tonic_iroh_transport::IrohConnect;
use pb::echo::{echo_client::EchoClient, echo_server::EchoServer, EchoRequest};
// Create client endpoint
let client_endpoint = iroh::Endpoint::builder().bind().await?;
let server_addr = EndpointAddr::new(server_node_id);
// Connect using the IrohConnect trait
let channel = EchoServer::<EchoService>::connect(&client_endpoint, server_addr).await?;
let mut client = EchoClient::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 = EchoServer::<EchoService>::connect(&client_endpoint, server_addr)
.connect_timeout(Duration::from_secs(10))
.await?;
❯ ./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.
# Check all crates compile
make check-all
# Run all tests
make test-all
# Format and lint
make fmt
make lint
# Build examples
make examples
MIT OR Apache-2.0