mirror of
https://github.com/logos-co/logos-libp2p-module.git
synced 2026-08-27 16:01:12 +00:00
244 lines
8.3 KiB
C++
244 lines
8.3 KiB
C++
/// # Tutorial 11: Circuit Relay – Connecting Through Firewalls
|
||
///
|
||
/// Not all peers are directly reachable. Some are behind NAT, firewalls,
|
||
/// or have no public IP. Circuit Relay solves this by having a
|
||
/// **relay node** forward traffic between peers.
|
||
///
|
||
/// ## The Three-Node Pattern
|
||
///
|
||
/// Circuit relay uses three roles:
|
||
///
|
||
/// ```
|
||
/// Client <--> Relay <--> Destination
|
||
/// (B) (R) (A)
|
||
/// ```
|
||
///
|
||
/// - **Destination** (A) — The peer behind NAT that wants to be reachable.
|
||
/// It connects to the relay and reserves a slot.
|
||
/// - **Relay** (R) — A publicly reachable node that forwards traffic.
|
||
/// - **Client** (B) — A peer that wants to connect to A through R.
|
||
///
|
||
/// ## How it works
|
||
///
|
||
/// 1. The Destination connects to the Relay and calls
|
||
/// `circuitRelayReserve()` to request a relay slot.
|
||
/// 2. The Relay confirms the reservation and returns relay addresses.
|
||
/// 3. The Client dials the Destination via the Relay using
|
||
/// `dialCircuitRelay()`.
|
||
/// 4. The Relay transparently forwards stream data between them.
|
||
///
|
||
/// > **Note**: Circuit relay adds latency and bandwidth overhead on the
|
||
/// > relay node. Use it only when direct connections are impossible.
|
||
///
|
||
/// -----------
|
||
|
||
#include <cstdio>
|
||
#include <chrono>
|
||
#include <cstdint>
|
||
#include <thread>
|
||
#include <string>
|
||
#include <vector>
|
||
#include "plugin.h"
|
||
|
||
int main()
|
||
{
|
||
printf("=== Tutorial 11: Circuit Relay ===\n\n");
|
||
|
||
setLogLevel("fatal");
|
||
|
||
/// ## Step 1: Create three nodes
|
||
///
|
||
/// - **Relay** (port 9890): publicly reachable, `circuitRelay: true`
|
||
/// - **Destination** (port 9891): behind NAT
|
||
/// - **Client** (port 9892): wants to connect to Destination
|
||
///
|
||
/// The Relay and Destination must be connected before the reservation.
|
||
/// The Client connects to the Relay before dialing the Destination.
|
||
|
||
// Relay node — must have circuitRelay enabled
|
||
Libp2pModuleOptions optsRelay;
|
||
optsRelay.addrs = {"/ip4/127.0.0.1/tcp/9890"};
|
||
optsRelay.circuitRelay = true;
|
||
|
||
// Destination node (behind NAT)
|
||
Libp2pModuleOptions optsDest;
|
||
optsDest.addrs = {"/ip4/127.0.0.1/tcp/9891"};
|
||
optsDest.circuitRelayClient = true;
|
||
|
||
// Client node
|
||
Libp2pModuleOptions optsClient;
|
||
optsClient.addrs = {"/ip4/127.0.0.1/tcp/9892"};
|
||
optsClient.circuitRelayClient = true;
|
||
|
||
Libp2pModuleImpl relay(optsRelay);
|
||
Libp2pModuleImpl dest(optsDest);
|
||
Libp2pModuleImpl client(optsClient);
|
||
|
||
printf("Starting nodes...\n");
|
||
|
||
StdLogosResult relayStartRes = relay.start();
|
||
if (!relayStartRes.success) {
|
||
fprintf(stderr, "Relay failed: %s\n", relayStartRes.error.c_str());
|
||
return 1;
|
||
}
|
||
|
||
StdLogosResult destStartRes = dest.start();
|
||
if (!destStartRes.success) {
|
||
fprintf(stderr, "Destination failed: %s\n",
|
||
destStartRes.error.c_str());
|
||
return 1;
|
||
}
|
||
|
||
StdLogosResult clientStartRes = client.start();
|
||
if (!clientStartRes.success) {
|
||
fprintf(stderr, "Client failed: %s\n",
|
||
clientStartRes.error.c_str());
|
||
return 1;
|
||
}
|
||
printf("All three nodes started\n");
|
||
|
||
/// ## Step 2: Get node addresses
|
||
StdLogosResult infoRelayRes = relay.peerInfo();
|
||
if (!infoRelayRes.success) {
|
||
fprintf(stderr, "Failed to get relay info: %s\n",
|
||
infoRelayRes.error.c_str());
|
||
return 1;
|
||
}
|
||
auto infoRelay = infoRelayRes.value;
|
||
std::string relayPeerId = infoRelay["peerId"].get<std::string>();
|
||
std::vector<std::string> relayAddrs;
|
||
for (const auto& a : infoRelay["addrs"])
|
||
relayAddrs.push_back(a.get<std::string>());
|
||
|
||
StdLogosResult infoDestRes = dest.peerInfo();
|
||
if (!infoDestRes.success) {
|
||
fprintf(stderr, "Failed to get destination info: %s\n",
|
||
infoDestRes.error.c_str());
|
||
return 1;
|
||
}
|
||
auto infoDest = infoDestRes.value;
|
||
std::string destPeerId = infoDest["peerId"].get<std::string>();
|
||
|
||
printf("Relay peer ID: %s\n", relayPeerId.c_str());
|
||
printf("Destination peer ID: %s\n", destPeerId.c_str());
|
||
|
||
/// ## Step 3: Destination connects to the Relay and reserves a slot
|
||
///
|
||
/// The Destination must connect to the Relay first, then call
|
||
/// `circuitRelayReserve()` to request a reservation. The relay
|
||
/// returns the addresses the Destination can be reached at (via relay).
|
||
printf("\nDestination connecting to relay...\n");
|
||
StdLogosResult destConnectRes =
|
||
dest.connectPeer(relayPeerId, relayAddrs, 5000);
|
||
if (!destConnectRes.success) {
|
||
fprintf(stderr, "Destination failed to connect to relay: %s\n",
|
||
destConnectRes.error.c_str());
|
||
return 1;
|
||
}
|
||
printf("Destination connected to relay\n");
|
||
|
||
printf("Destination requesting relay reservation...\n");
|
||
StdLogosResult reserveRes = dest.circuitRelayReserve(relayPeerId, relayAddrs);
|
||
if (!reserveRes.success) {
|
||
fprintf(stderr, "Relay reservation failed: %s\n",
|
||
reserveRes.error.c_str());
|
||
return 1;
|
||
}
|
||
if (reserveRes.value.empty()) {
|
||
fprintf(stderr, "Reservation did not return relay addresses\n");
|
||
return 1;
|
||
}
|
||
|
||
printf("Relay reservation successful!\n");
|
||
printf("Relay addresses:\n");
|
||
for (const auto& addr : reserveRes.value) {
|
||
printf(" %s\n", addr.get<std::string>().c_str());
|
||
}
|
||
|
||
/// ## Step 4: Client connects to the Relay, then dials Destination
|
||
///
|
||
/// The Client connects to the Relay (same as any other peer), then
|
||
/// uses `dialCircuitRelay()` to reach the Destination through the Relay.
|
||
printf("\nClient connecting to relay...\n");
|
||
StdLogosResult clientConnectRes =
|
||
client.connectPeer(relayPeerId, relayAddrs, 5000);
|
||
if (!clientConnectRes.success) {
|
||
fprintf(stderr, "Client failed to connect to relay: %s\n",
|
||
clientConnectRes.error.c_str());
|
||
return 1;
|
||
}
|
||
printf("Client connected to relay\n");
|
||
|
||
/// Now the Client dials the Destination's peer ID through the relay.
|
||
/// The multiaddr comes from the reservation response, with `/p2p-circuit`
|
||
/// appended so libp2p routes the stream through the Relay.
|
||
printf("Client dialing destination through relay...\n");
|
||
|
||
std::string relayDialAddr;
|
||
relayDialAddr = reserveRes.value[0].get<std::string>() + "/p2p-circuit";
|
||
|
||
/// For circuit relay, we use a well-known protocol to test connectivity.
|
||
/// The ping protocol works well for this.
|
||
StdLogosResult dialRes = client.dialCircuitRelay(
|
||
destPeerId,
|
||
relayDialAddr,
|
||
"/ipfs/ping/1.0.0");
|
||
|
||
if (!dialRes.success) {
|
||
fprintf(stderr, "Circuit relay dial failed: %s\n",
|
||
dialRes.error.c_str());
|
||
fprintf(stderr, "Circuit relay may need configuration tuning.\n");
|
||
fprintf(stderr, "Ensure the relay node has circuitRelay=true and the\n");
|
||
fprintf(stderr, "destination has connected and reserved a slot.\n");
|
||
return 1;
|
||
}
|
||
uint64_t streamId = dialRes.value.get<uint64_t>();
|
||
printf("Circuit relay stream opened! id: %llu\n",
|
||
(unsigned long long)streamId);
|
||
|
||
// Send a ping through the relay:
|
||
std::string payload(32, '\0');
|
||
for (int i = 0; i < 32; ++i) payload[i] = static_cast<char>(i);
|
||
|
||
StdLogosResult writeRes = client.streamWrite(streamId, payload);
|
||
if (!writeRes.success) {
|
||
fprintf(stderr, "Write failed: %s\n", writeRes.error.c_str());
|
||
return 1;
|
||
}
|
||
printf("Sent %zu bytes through relay\n", payload.size());
|
||
|
||
client.streamClose(streamId);
|
||
client.streamRelease(streamId);
|
||
|
||
/// ## Step 5: Clean up
|
||
relay.stop();
|
||
dest.stop();
|
||
client.stop();
|
||
|
||
printf("\n=== Tutorial 11 Complete ===\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/// ## Key Takeaways
|
||
///
|
||
/// - Circuit Relay allows connecting peers behind NAT/firewalls
|
||
/// - Three roles: Destination (relayed), Relay (forwarder), Client
|
||
/// - `circuitRelay: true` enables relay mode on a node
|
||
/// - Destination calls `circuitRelayReserve()` to request a slot
|
||
/// - Client calls `dialCircuitRelay()` with dest peer ID + address
|
||
/// - Relay overhead should be considered; prefer direct connections
|
||
/// when possible
|
||
/// - The relay address returned by reserve may be needed by the
|
||
/// client for some configurations
|
||
///
|
||
/// This concludes the tutorial series! You now have a solid
|
||
/// foundation for building peer-to-peer applications with
|
||
/// `logos-libp2p-module`.
|
||
|
||
/// ## Run tutorial
|
||
///
|
||
/// ```bash
|
||
/// ./build/tutorial/tutorial_11_circuit_relay
|
||
/// ```
|