Skip to main content
In this tutorial, we will build a ping program that connects two endpoints using a ticket over DNS. The full example of this code can be viewed on github.

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 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::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(())
}

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::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::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
Having trouble? If your ping isn’t working or you want to see what’s happening under the hood, check out our troubleshooting guide to enable logging and diagnose issues.

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 address is a structured representation of this information that can be consumed by iroh endpoints to dial each other. An EndpointTicket wraps this address into a serializable format — a short string you can copy and paste. Share this string with senders so they can dial the receiver without manually exchanging networking details. This out of band information must be sent between the endpoints so that they can discover and connect to each other, while maintaining security bootstrapping the end-to-end encrypted connection. In this example, we will just use a string for users to copy/paste, but in your app, you could publish it to a server or send it as a QR code or as a query string at the end of a URL. It’s up to you. 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<()> {
     // Create an endpoint, it allows creating and accepting
    // connections in the iroh p2p world
    let endpoint = Endpoint::bind().await?;

    // Wait for the endpoint to be accessible by others on the internet
    endpoint.online().await;

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

    // get the address of this endpoint to share with the sender
    let ticket = EndpointTicket::new(endpoint.addr());
    println!("{ticket}");

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

    // Keep the receiver running until Ctrl+C
    tokio::signal::ctrl_c().await?;
    Ok(())
}

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::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.
Connection issues? If the sender can’t reach the receiver or you’re seeing errors, visit our troubleshooting guide to enable detailed logging and use iroh-doctor to diagnose network problems.

More Tutorials

You’ve now successfully built a small tool for sending messages over a peer to peer network! 🎉 The full example of this code can be viewed on github. If you’re hungry for more, check out