Skip to main content
Let’s dive into iroh by building a simple peer-to-peer file transfer tool in rust!

Set up

We’ll assume you’ve set up rust and cargo on your machine. Initialize a new project by running:
cargo init ping-pong
cd ping-pong
Now, add the dependencies we’ll need for this example:
cargo add iroh iroh-ping iroh-tickets tokio anyhow
From here on we’ll be working inside the src/main.rs file.

Part 1: Simple Ping Protocol

Create an Endpoint

To start interacting with other iroh endpoints, we need to build an iroh::Endpoint. This is what manages the possibly changing network underneath, maintains a connection to the closest relay, and finds ways to address devices by EndpointId.
#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Create an endpoint, it allows creating and accepting
    // connections in the iroh p2p world
    let endpoint = Endpoint::bind().await?;

    // ...

    Ok(())
}
There we go, this is all we need to open connections or accept them.

Protocols

Now that we have an Endpoint, we can start using protocols over it. A protocol defines how two endpoints exchange messages. Just like HTTP defines how web browsers talk to servers, iroh protocols define how peers communicate over iroh connections. Each protocol is identified by an ALPN (Application-Layer Protocol Negotiation) string. When a connection arrives, the router uses this string to decide which handler processes the data. You can build custom protocol handlers or use existing ones like iroh-ping.
Learn more about writing your own protocol on the protocol documentation page.

Ping: Receive

iroh-ping is a diagnostic protocol bundled with iroh that lets two endpoints exchange lightweight ping/pong messages to prove connectivity, measure round-trip latency, or whatever else you want to build on top of it, without building a custom handler.
use anyhow::Result;
use iroh::{protocol::Router, Endpoint, Watcher};
use iroh_ping::Ping;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Create an endpoint, it allows creating and accepting
    // connections in the iroh p2p world
    let endpoint = Endpoint::builder().bind().await?;

    // bring the endpoint online before accepting connections
    endpoint.online().await;

    // Then we initialize a struct that can accept ping requests over iroh connections
    let ping = Ping::new();

    // receiving ping requests
    let recv_router = Router::builder(endpoint)
        .accept(iroh_ping::ALPN, ping)
        .spawn();

    // ...

    Ok(())
}
With these two lines, we’ve initialized iroh-blobs and gave it access to our Endpoint.

Ping: Send

At this point what we want to do depends on whether we want to accept incoming iroh connections from the network or create outbound iroh connections to other endpoints. Which one we want to do depends on if the executable was called with send as an argument or receive, so let’s parse these two options out from the CLI arguments and match on them:
use anyhow::Result;
use iroh::{protocol::Router, Endpoint, Watcher};
use iroh_ping::Ping;

#[tokio::main]
async fn main() -> Result<()> {
    // Create an endpoint, it allows creating and accepting
    // connections in the iroh p2p world
    let endpoint = Endpoint::builder().bind().await?;

    // Then we initialize a struct that can accept ping requests over iroh connections
    let ping = Ping::new();

    // receiving ping requests
    let recv_router = Router::builder(endpoint)
        .accept(iroh_ping::ALPN, ping)
        .spawn();

    // get the address of this endpoint to share with the sender
    let addr = recv_router.endpoint().addr();

    // create a send side & send a ping
    let send_ep = Endpoint::builder().bind().await?;
    let send_pinger = Ping::new();
    let rtt = send_pinger.ping(&send_ep, addr).await?;

    println!("ping took: {:?} to complete", rtt);
    Ok(())
}

Running it

Now that we’ve created both the sending and receiving sides of our ping program, we can run it!
cargo run
I   Compiling ping-pong v0.1.0 (/dev/ping-pong)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.62s
     Running `target/debug/ping-pong`
ping took: 1.189375ms to complete

Part 2: Discovering Peers with Tickets

Round-trip time isn’t very useful when both roles live in the same binary instance. Let’s split the app into two subcommands so you can run the receiver on one machine and the sender on another.

What is a ticket?

When an iroh endpoint comes online, it has an address containing its node ID, relay URL, and direct addresses. An EndpointTicket wraps this address into a serializable ticket — a short string you can copy and paste. Share this ticket with senders so they can dial the receiver without manually exchanging networking details. A ticket is made from an endpoint’s address like this:
use iroh_tickets::{Ticket, endpoint::EndpointTicket};

let ticket = EndpointTicket::new(endpoint.addr());
println!("{ticket}");
For more details on how it works, see Tickets and Discovery.

Receiver

The receiver creates an endpoint, brings it online, prints its ticket, then runs a router that accepts incoming ping requests indefinitely:
// filepath: src/main.rs
use anyhow::{anyhow, Result};
use iroh::{Endpoint, protocol::Router};
use iroh_ping::Ping;
use iroh_tickets::{Ticket, endpoint::EndpointTicket};
use std::env;

async fn run_receiver() -> Result<()> {
    let endpoint = Endpoint::builder().bind().await?;
    endpoint.online().await;

    let ping = Ping::new();

    let ticket = EndpointTicket::new(endpoint.addr());
    println!("{ticket}");

    Router::builder(endpoint)
        .accept(iroh_ping::ALPN, ping)
        .spawn();

    // Keep the receiver running indefinitely
    loop {
        tokio::time::sleep(tokio::time::Duration::from_secs(60)).await;
    }
}

Sender

The sender parses the ticket, creates its own endpoint, and pings the receiver’s address:
// filepath: src/main.rs
async fn run_sender(ticket: EndpointTicket) -> Result<()> {
    let send_ep = Endpoint::builder().bind().await?;
    let send_pinger = Ping::new();
    let rtt = send_pinger.ping(&send_ep, ticket.endpoint_addr().clone()).await?;
    println!("ping took: {:?} to complete", rtt);
    Ok(())
}

Wiring it together

Parse the command-line arguments to determine whether to run as receiver or sender:
// filepath: src/main.rs
#[tokio::main]
async fn main() -> Result<()> {
    let mut args = env::args().skip(1);
    let role = args
        .next()
        .ok_or_else(|| anyhow!("expected 'receiver' or 'sender' as the first argument"))?;

    match role.as_str() {
        "receiver" => run_receiver().await,
        "sender" => {
            let ticket_str = args
                .next()
                .ok_or_else(|| anyhow!("expected ticket as the second argument"))?;
            let ticket = EndpointTicket::deserialize(&ticket_str)
                .map_err(|e| anyhow!("failed to parse ticket: {}", e))?;

            run_sender(ticket).await
        }
        _ => Err(anyhow!("unknown role '{}'; use 'receiver' or 'sender'", role)),
    }
}

Running it

In one terminal, start the receiver:
cargo run -- receiver
It will print a ticket. Copy that ticket and run the sender in another terminal:
cargo run -- sender <TICKET>
You should see the round-trip time printed by the sender.

More Tutorials

You’ve now successfully built a small tool for sending messages over a peer to peer network! 🎉 The full example with the very latest version of iroh and iroh-ping can be viewed on github. If you’re hungry for more, check out