Skip to content
minip2p
Esc
navigateopen⌘Jpreview
On this page

Listen and dial

Bind QUIC, TCP, or both; publish dialable peer addresses; and wait for PeerReady.

This guide covers direct QUIC and TCP with the base Endpoint API. QUIC is on by default; TCP needs the tcp Cargo feature. For relay races and hole punching, use Traverse NAT. If you have not run a local connection yet, start with Connect two peers.

Bind an endpoint

Choose the bind method that matches the host:

Method Use it when
bind_quic("127.0.0.1:0") You want one socket address, commonly for local tests.
bind_quic_multiaddr(&addr) Configuration already uses a QUIC Multiaddr.
bind_quic_dual_stack() The application should bind separate IPv4 and IPv6 wildcard sockets.
bind_tcp("0.0.0.0:0") You want TCP only (tcp feature).
.quic_dual_stack().tcp(...).bind() You want one endpoint that speaks both.

Port 0 asks the operating system to pick a free UDP or TCP port.

use minip2p::{Endpoint, PeerAddr};

let mut node = Endpoint::builder()
    .agent_version("my-app/0.1.0")
    .bind_quic_dual_stack()?;

for address in node.listen_all()? {
    // Wildcards describe the bind socket; rewrite before sharing locally.
    println!("listen={}", local_dialable(&address));
}

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

listen() publishes the first already-bound address. listen_all() publishes every bound address, which is normally the right choice for a dual-stack endpoint.

To listen on both, chain the builder methods before bind:

let mut node = Endpoint::builder()
    .quic_dual_stack()
    .tcp("0.0.0.0:4001")
    .bind()?;

/udp/.../quic-v1 goes over QUIC; /tcp goes over TCP. Above that, swarm and app-protocol APIs look the same.

Dial a known peer

Parse a complete PeerAddr, including its terminal /p2p/<peer-id>:

use std::str::FromStr;

use minip2p::{Endpoint, PeerAddr};

let target = PeerAddr::from_str(
    "/dns/node.example.com/udp/4001/quic-v1/p2p/12D3KooW…",
)?;

let mut node = Endpoint::builder().bind_quic_dual_stack()?;
let connection_ids = node.dial(&target)?;
println!("started {} dial(s)", connection_ids.len());

dial starts every applicable local address family. For a /dns/... target on a dual-stack endpoint, that can mean both IPv4 and IPv6 connection IDs. /dns4/... and /dns6/... stay single-family. Use dial_ip4 or dial_ip6 when policy requires one family.

Wait for PeerReady

A successful dial still has two useful milestones:

  1. Event::ConnectionEstablished: the transport is up and the peer identity is authenticated.
  2. Event::PeerReady: the first Identify exchange has completed, so the endpoint knows which protocols the peer supports.
use std::time::Duration;

let ready = node.wait_peer_ready(
    target.peer_id(),
    Duration::from_secs(10),
)?;

if ready.is_none() {
    eprintln!("peer connected but did not become ready before the deadline");
}

Wait for PeerReady before opening an application protocol stream. After Identify completes, open_stream can reject an unsupported protocol immediately with minip2p::Error::Swarm(minip2p::SwarmError::RemoteDoesNotSupport { .. }).

Inspect and disconnect

if node.is_peer_ready(target.peer_id()) {
    if let Some(info) = node.peer_info(target.peer_id()) {
        println!(
            "agent={}",
            info.agent_version.as_deref().unwrap_or("<none>")
        );
    }
}

for peer in node.connected_peers() {
    println!("connected={peer}");
}

node.disconnect(target.peer_id())?;

disconnect closes the active connection. A later connection closure appears as Event::ConnectionClosed.

dial is direct-only

dial* does not reserve on a relay, establish a circuit, or start DCUtR. When the application has enabled and configured NAT traversal, use connect* plus wait_path instead. See Traverse NAT.

Next: Register a protocol or Identity.

Last updated on September 7, 2026

Was this page helpful?