Building a Peer-to-Peer Chat Application in Rust

Building a P2P Chat Application with Rust and Iroh

This tutorial demonstrates how to build a peer-to-peer chat application from scratch using Rust and the Iroh library. While this implementation is simplified, it illustrates core concepts of P2P networking and the Iroh gossip protocol.

Prerequisites

The tutorial assumes basic programming knowledge but no prior Rust experience. To begin, install Rust by following the instructions at rust-lang.org.

Project Setup

First, initialize a new Rust project:

cargo init iroh-gossip-chat
cd iroh-gossip-chat
cargo run

Install the required dependencies:

cargo add iroh tokio anyhow rand

Basic Endpoint Configuration

The first step is creating a basic endpoint configuration:

use anyhow::Result;
use iroh::{SecretKey, Endpoint};
use iroh::protocol::Router;

#[tokio::main]
async fn main() -> Result<()> {
    let secret_key = SecretKey::generate(rand::rngs::OsRng);
    println!("> our secret key: {secret_key}");

    let endpoint = Endpoint::builder()
        .discovery_n0()
        .bind()
        .await?;

    println!("> our node id: {}", endpoint.node_id());

    Ok(())
}

Adding Gossip Protocol Support

Install the gossip protocol:

cargo add iroh-gossip

Then update the code to implement basic gossip functionality:

use anyhow::Result;
use iroh::protocol::Router;
use iroh::{Endpoint, SecretKey};
use iroh_gossip::net::Gossip;

#[tokio::main]
async fn main() -> Result<()> {
    let secret_key = SecretKey::generate(rand::rngs::OsRng);
    println!("> our secret key: {secret_key}");

    let endpoint = Endpoint::builder()
        .secret_key(secret_key)
        .discovery_n0()
        .bind()
        .await?;

    println!("> our node id: {}", endpoint.node_id());
    let gossip = Gossip::builder().spawn(endpoint.clone()).await?;

    let router = Router::builder(endpoint.clone())
        .accept(iroh_gossip::ALPN, gossip.clone())
        .spawn()
        .await?;

    router.shutdown().await?;

    Ok(())
}

Creating and Broadcasting to a Topic

Topics are the fundamental unit of communication in the gossip protocol. Here's how to create a topic and broadcast a message:

use anyhow::Result;
use iroh::protocol::Router;
use iroh::{Endpoint, SecretKey};
use iroh_gossip::net::Gossip;
use iroh_gossip::proto::TopicId;

#[tokio::main]
async fn main() -> Result<()> {
    let secret_key = SecretKey::generate(rand::rngs::OsRng);
    println!("> our secret key: {secret_key}");

    let endpoint = Endpoint::builder().discovery_n0().bind().await?;

    println!("> our node id: {}", endpoint.node_id());
    let gossip = Gossip::builder().spawn(endpoint.clone()).await?;

    let router = Router::builder(endpoint.clone())
        .accept(iroh_gossip::ALPN, gossip.clone())
        .spawn()
        .await?;

    let id = TopicId::from_bytes(rand::random());
    let peer_ids = vec![];
    let (sender, _receiver) = gossip.subscribe(id, peer_ids)?.split();
    sender.broadcast("sup".into()).await?;

    router.shutdown().await?;

    Ok(())
}

Implementing Message Reception

Install the futures-lite crate to handle async streams:

cargo add futures-lite

Then implement message reception:

use anyhow::Result;
use iroh::{SecretKey, Endpoint};
use iroh::protocol::Router;
use futures_lite::StreamExt;
use iroh_gossip::{Gossip, Event, TopicId};

#[tokio::main]
async fn main() -> Result<()> {
    // Previous endpoint setup code...

    let (sender, mut receiver) = gossip.subscribe_and_join(id, peer_ids).await?.split();

    tokio::spawn(async move || {
        while let Some(event) = receiver.try_next().await? {
            if let Event::Gossip(gossip_event) = event {
                match gossip_event {
                    GossipEvent::Received(message) => println!("got message: {:?}", &message),
                    _ => {}
                }
            }
        }
    });

    sender.broadcast(b"sup").await?;

    router.shutdown().await?;

    Ok(())
}

Implementing Signaling with Tickets

To enable nodes to discover and join each other, implement ticket-based signaling:

cargo add serde data_encoding

Add the ticket implementation:

#[derive(Debug, Serialize, Deserialize)]
struct Ticket {
    topic: TopicId,
    peers: Vec<NodeAddr>,
}

impl Ticket {
    fn from_bytes(bytes: &[u8]) -> Result<Self> {
        serde_json::from_slice(bytes).map_err(Into::into)
    }

    pub fn to_bytes(&self) -> Vec<u8> {
        serde_json::to_vec(self).expect("serde_json::to_vec is infallible")
    }
}

impl fmt::Display for Ticket {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let mut text = data_encoding::BASE32_NOPAD.encode(&self.to_bytes()[..]);
        text.make_ascii_lowercase();
        write!(f, "{}", text)
    }
}

impl FromStr for Ticket {
    type Err = anyhow::Error;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let bytes = data_encoding::BASE32_NOPAD.decode(s.to_ascii_uppercase().as_bytes())?;
        Self::from_bytes(&bytes)
    }
}

Creating a Command-Line Interface

Install the clap crate for CLI argument parsing:

cargo add clap --features derive

The final implementation includes a full command-line interface with commands for creating and joining chat rooms:

use std::{
    collections::HashMap,
    fmt,
    net::{Ipv4Addr, SocketAddrV4},
    str::FromStr,
};

#[derive(Parser, Debug)]
struct Args {
    #[clap(long)]
    no_relay: bool,
    #[clap(short, long)]
    name: Option<String>,
    #[clap(subcommand)]
    command: Command,
}

#[derive(Parser, Debug)]
enum Command {
    Open,
    Join {
        ticket: String,
    },
}

// Main function implementation with CLI command handling...

Running the Application

To create a new chat room:

cargo run -- --name user1 open

To join an existing chat room:

cargo run -- --name user2 join <ticket>

The application will now support basic chat functionality between connected peers, with messages broadcast to all participants in the room.

Notes on Security

While this implementation demonstrates the basic concepts, a production system would need additional security measures. For example, the example in the Iroh gossip protocol repository includes message signing to prevent impersonation attacks.

For more sophisticated implementations and security features, refer to the examples in the Iroh gossip protocol repository.