Skip to content

Peer to Peer

irrld edited this page Jul 30, 2026 · 4 revisions

Peer-to-Peer

Two peers behind NAT cannot dial each other directly. znet's P2P module solves this the usual way: both connect out to a rendezvous server, learn each other's external address, then punch simultaneously so each one's NAT sees the other's traffic as a reply to something it already sent.

The result is an ordinary PeerSession. Once it exists, everything else in this wiki applies unchanged.

The pieces

PeerLocator Talks to the rendezvous server, exchanges addresses, drives the punch
p2p::PunchSync The punch itself. Called by the locator; usable directly if you have addresses already
p2p::IsInitiator Decides which peer takes the server role
rendezvous-server A ready-to-run rendezvous server in the repo

Usage

// the locator's Wait() returns once punching resolves, so hold the session
// yourself if the connection should outlive it
std::shared_ptr<PeerSession> g_session;
std::unique_ptr<p2p::PeerLocator> g_locator;

bool OnPeerReady(p2p::PeerLocatorReadyEvent& event) {
  // registered with the rendezvous server under event.peer_name(); ask for
  // whoever you want to reach
  g_locator->AskPeer("the-other-peer");
  return false;
}

bool OnPeerConnected(p2p::PeerConnectedEvent& event) {
  g_session = event.session();
  g_session->SetCodec(std::make_shared<Codec>());

  // both peers run the same code, so one has to take the server role. this
  // derives it from the punch id so the two sides never both pick the same one.
  bool acts_as_server = p2p::IsInitiator(event.punch_id(), event.self_peer_name(),
                                         event.target_peer_name());
  return false;
}

void OnEvent(Event& event) {
  EventDispatcher dispatcher{event};
  dispatcher.Dispatch<p2p::PeerLocatorReadyEvent>(ZNET_BIND_GLOBAL_FN(OnPeerReady));
  dispatcher.Dispatch<p2p::PeerConnectedEvent>(ZNET_BIND_GLOBAL_FN(OnPeerConnected));
}

int RunPeer() {
  p2p::PeerLocatorConfig config{"rendezvous.example.com", 25000};
  g_locator.reset(new p2p::PeerLocator(config));
  g_locator->SetEventCallback(ZNET_BIND_GLOBAL_FN(OnEvent));
  if (g_locator->Connect() != Result::Success) {
    return 1;
  }
  g_locator->Wait();  // returns once punching resolves, either way
  return 0;
}

Events

Event When Accessors
PeerLocatorReadyEvent Registered with the rendezvous server peer_name(), endpoint()
PeerConnectedEvent The punch succeeded and a session exists session(), punch_id(), self_peer_name(), target_peer_name()
PeerLocatorCloseEvent The locator shut down None

Two things that catch people out

Wait() returns when punching resolves, not when you are done. The locator's job ends once it has produced a session. If you let the locator go out of scope without holding the session, the connection goes with it. Keep the shared_ptr somewhere that outlives the locator, as the example does.

To reuse a locator for another peer, call Connect() again and handle the events again.

Somebody has to be the server. Both peers run identical code, so without a tiebreak both would wait for the other to speak first. p2p::IsInitiator derives the answer from the punch id and the two names, so the two sides always disagree: exactly one gets true.

This also decides encryption: the initiator is the accepting side, so its options are what the session adopts. See Encryption and Compression.

Transport

PeerLocatorConfig::connection_type picks the transport for the punched connection. ZDT is the better fit: it is UDP underneath, and UDP hole punching works through more NATs than TCP's does.

Note that this one defaults to ConnectionType::TCP, unlike ClientConfig and ServerConfig, which default to ZDT. If you want ZDT for a punched connection, say so:

p2p::PeerLocatorConfig config{"rendezvous.example.com", 25000};
config.connection_type = ConnectionType::ZDT;

Limits

Hole punching does not always work. Symmetric NATs assign a different external port per destination, so the address learned from the rendezvous server is not the one your peer must send to. There is no fallback relay in znet: if the punch fails, it fails, and your application decides what to do next.

Running a rendezvous server

The repo ships one:

cmake --build build --target rendezvous-server
./build/rendezvous-server/rendezvous-server

It needs a public address both peers can reach. It only brokers introductions, and no traffic flows through it once the punch succeeds.

Status

The P2P module is younger than the rest of the library and less exercised. Known gaps as of this writing:

  • StartPunchRequestPacket::connection_type_ is never serialized, so the transport choice does not survive the wire and PunchSync dispatches on an indeterminate value
  • PeerLocator's is_running_ and session_ are read across threads without synchronization
  • ~PeerLocator does not join its task

Treat it as usable but not yet hardened. The p2p example is the working reference.

Clone this wiki locally