Skip to content
minip2p
Esc
↑↓navigate↵open⌘Jpreview
On this page

Connect two peers

Print a dialable PeerAddr and measure a libp2p Ping RTT between two local processes.

This quickstart creates one small Cargo project with two binaries. The listener prints a paste-ready peer address; the dialer connects to it and measures a Ping round trip.

Before starting, install minip2p with Rust 1.91 or newer.

Create the project

Create a binary package

cargo new minip2p-hello
cd minip2p-hello
cargo add minip2p-rs
mkdir -p src/bin

Add the listener

Create src/bin/listener.rs:

use minip2p::{Endpoint, EndpointWaitOutcome, PeerAddr, WaitDeadline};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut node = Endpoint::builder()
        .agent_version("minip2p-hello/listener")
        .listen_default()?
        .bind()?;

    println!("peer={}", node.peer_id());
    for address in node.listen_all()? {
        println!("listen={}", local_dialable(&address));
    }

    loop {
        if let EndpointWaitOutcome::Event(event) = node.wait(WaitDeadline::NEVER)? {
            println!("{event:?}");
        }
    }
}

fn local_dialable(address: &PeerAddr) -> String {
    address
        .to_string()
        .replace("/ip4/0.0.0.0/", "/ip4/127.0.0.1/")
        .replace("/ip6/::/", "/ip6/::1/")
}

Add the dialer

Create src/bin/dialer.rs:

use std::time::{Duration, Instant};

use minip2p::{ConnectOutcome, Endpoint, EndpointEvent, EndpointWaitOutcome, PeerAddr};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // A complete peer address, e.g. /ip4/127.0.0.1/udp/4001/quic-v1/p2p/12D3KooW...
    let target: PeerAddr = std::env::args()
        .nth(1)
        .ok_or("usage: dialer <peer-address>")?
        .parse()?;
    let peer = target.peer_id().clone();

    let mut node = Endpoint::builder()
        .agent_version("minip2p-hello/dialer")
        .listen_default()?
        .bind()?;

    // One Connection attempt, one Connect ID, one terminal event.
    let connect_id = node.connect(target)?;
    let mut deadline = Instant::now() + Duration::from_secs(10);
    loop {
        let event = match node.wait(deadline)? {
            EndpointWaitOutcome::Event(event) => event,
            // Another thread woke the wait; nothing to service here.
            EndpointWaitOutcome::Deadline => {
                return Err("peer did not answer in time".into());
            }
            _ => continue, // interrupted by a WaitHandle
        };
        match event {
            EndpointEvent::ConnectSettled {
                connect_id: id,
                outcome,
                ..
            } if id == connect_id => {
                if !matches!(outcome, ConnectOutcome::Connected { .. }) {
                    return Err(format!("connect failed: {outcome:?}").into());
                }
            }
            EndpointEvent::PeerReady { peer_id, .. } if peer_id == peer => {
                node.ping(&peer)?;
                // The ping gets its own budget, however long setup took.
                deadline = Instant::now() + Duration::from_secs(5);
            }
            EndpointEvent::PingRttMeasured { peer_id, rtt_ms, .. } if peer_id == peer => {
                println!("peer={peer} rtt={rtt_ms}ms");
                return Ok(());
            }
            // Everything else is unrelated to this exchange.
            _ => {}
        }
    }
}

Run it

In terminal one:

$ cargo run --bin listener
peer=12D3KooW…
listen=/ip4/127.0.0.1/udp/54321/quic-v1/p2p/12D3KooW…
listen=/ip6/::1/udp/54322/quic-v1/p2p/12D3KooW…

Keep the listener running. In terminal two, paste one complete listen= value:

$ cargo run --bin dialer -- /ip4/127.0.0.1/udp/54321/quic-v1/p2p/12D3KooW…
peer=12D3KooW… rtt=2ms

connect returns a Connect ID immediately; the attempt ends with exactly one ConnectSettled event carrying that ID. The dialer’s loop is the canonical pattern: match the events it is waiting for by ID or peer, and let everything else pass. It pings after PeerReady (Identify finished) only because that is a convenient milestone for this demo; open_stream does not require Identify for a protocol both peers already registered.

listen_default binds dual-stack QUIC wildcard sockets. The listener rewrites only the printed local-demo addresses to loopback; the endpoint itself continues listening on both wildcard sockets.

If it does not connect

The address fails to parse

Copy the entire value after listen=. A PeerAddr needs the terminal /p2p/<peer-id> component as well as the QUIC transport address.

The peer never becomes ready

Keep the listener process running. minip2p is caller-driven, so the listener must continue calling wait to accept the connection, complete Identify, and answer Ping.

The IPv6 address does not work

Use the printed IPv4 loopback address instead. listen_default binds separate IPv4 and IPv6 sockets, but the host still needs the corresponding address family available.

Next: Concepts or Register a protocol.

Last updated on October 22, 2026

Was this page helpful?