---
title: Listen and dial
description: 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](/rust/traverse-nat). If you have not run a local connection yet, start with [Connect two peers](/rust/connect-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.

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

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

> **Warning**
>
> An address containing `/ip4/0.0.0.0` or `/ip6/::` describes where the socket is bound, not an address another machine can dial. Advertise a real interface, public, or relay circuit address to remote peers.

## Dial a known peer

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

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

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.

Prefer a readiness wait over a short-poll loop:

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

```rust
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](/rust/traverse-nat).

Next: [Register a protocol](/rust/register-a-protocol) or [Identity](/rust/identity).
