---
title: Connect two peers
description: 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](/docs/rust/install) with Rust 1.91 or newer.

## Create the project

1. **Create a binary package**

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

2. **Add the listener**

    Create `src/bin/listener.rs`:

    ```rust
    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/")
    }
    ```

3. **Add the dialer**

    Create `src/bin/dialer.rs`:

    ```rust
    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:

```console
$ 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:

```console
$ 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](/docs/rust/concepts) or [Register a protocol](/docs/rust/register-a-protocol).
