Listen and dial
Bind QUIC, TCP, or both; publish dialable peer addresses; and wait with readiness-driven helpers.
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
Prefer address-shaped listening: pass complete multiaddresses to listen_on (or listen_default for dual-stack QUIC) and call bind. The builder infers QUIC vs TCP from each address and groups compatible IPv4/IPv6 listeners onto one transport per shape.
| Method | Use it when |
|---|---|
.listen_on("/ip4/…/udp/…/quic-v1")?.bind() |
Configuration already uses complete multiaddresses. |
.listen_default()?.bind() |
Common dual-stack QUIC wildcards. |
bind_quic("127.0.0.1:0") |
Legacy one-socket helper (still supported). |
bind_quic_dual_stack() |
Legacy dual-stack QUIC helper (still supported). |
bind_tcp("0.0.0.0:0") |
Legacy TCP helper (tcp feature). |
.listen_on(quic)?.listen_on(tcp)?.bind() |
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")
.listen_default()?
.bind()?;
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 QUIC and TCP:
let mut node = Endpoint::builder()
.listen_on("/ip4/0.0.0.0/udp/4001/quic-v1")?
.listen_on("/ip4/0.0.0.0/tcp/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());
Prefer Endpoint::connect when one Connection identity and one terminal outcome is enough. connect races every candidate (after DNS expansion) and reports EndpointEvent::ConnectSettled. dial remains for raw Transport dials until the contraction ticket.
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 connection milestones
A successful dial still has two useful milestones:
Event::ConnectionEstablished: the transport is up and the peer identity is authenticated.Event::PeerReady: the first Identify exchange has completed, so the endpoint knows which protocols the peer supports.
Prefer a readiness wait over a short-poll loop:
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");
}
open_stream works once the peer is connected. You do not have to wait for PeerReady to open a protocol you already know the remote speaks. After Identify completes, open_stream can reject an unsupported protocol immediately with minip2p::Error::Swarm(minip2p::SwarmError::RemoteDoesNotSupport { .. }). Use wait_peer_ready when you want that early check or the advertised protocol list.
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 nat_wait_path (first usable path) or ConnectSettled (terminal). See Traverse NAT.
Next: Register a protocol or Identity.