Skip to main content
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 the iroh gossip protocol. The code in the above video differs slightly from the code presented below. We recommend watching the video and following along until you get comfortable. When you are ready for a deeper dive into the code, walk through this tutorial.

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:
This should print “hello world”, and you can see the code for that in the src/main.rs file, which is the file we will be building on for this tutorial. Next, let’s add the iroh dependencies, and a few others we will need to get the chat application working. tokio handles our async runtime, allowing us to do things like listen for incoming messages at the same time as we write outgoing messages. anyhow allows us to more easily handle errors, and is especially useful when writing binaries or when prototyping. rand generates randomness. Install the required dependencies:

Basic Endpoint Configuration

The first step is creating a basic endpoint configuration. Replace the following with the current contents of the src/main.rs file:

Adding Gossip Protocol Support

The iroh-gossip protocol is what we will use to not only send messages, but also coordinate who we are connected to in our chat application. Install the gossip protocol:
Then update the code to implement basic gossip functionality:

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:

Messages

The broadcast method will send bytes over the wire. To keep ourselves organized, we should create a Message enum that enumerates the kinds of messages we want folks to send. We can serialize those messages into bytes for the broadcast method to send. Let’s write a Message::AboutMe enum variant that allows someone who joins the chat to be called by a specific name. And let’s write a Message::Message that has a String with the actual chat messages. Also, we want each of those messages to include the EndpointId of the sender. In an actual application, we would encode and decode the messages with keypairs to ensure that everyone who sends a message is actually who they say they are. For more on that, check out our more robust chat example that exists in the iroh-gossip repo. In addition, the nature of the gossip protocol could potentially cause messages to be sent multiple times. This is done intentionally, to ensure at-least-once delivery of each message to all endpoints. This behavior is unexpected in most app contexts, so iroh will internally deduplicate messages based on the hash of their contents. In this case, if someone sends re-sends a message they already sent, it will be ignored by the other peers. To circumvent this, each message should include a piece of unique data to prevent this deduplication. This can be done in a number of ways - we will use a cryptographic nonce. We need to add crates that will allow us to serialize our new message types as bytes and deserialize bytes as our message type. serde stands for Serialize/Deserialize. serde-json lets us easily encode and decode to the json format, but we can choose other formats. E.g., in the iroh-gossip example, we use postcard. bytes is a utility library for working with bytes.
Then add the following to your main file:
Now, rather than broadcasting “sup”, let’s broadcast a message that let’s others in the chat know our name:

Implementing Message Reception

To handle incoming messages, we need to iterate over the stream of messages that come in on the receiver of the Topic. Dealing with streams in rust is complicated without additional help from crates that were designed to simplify using them. Knowing the best tools to use for this can also be complicated, since a certain crate may have a good trait implementation for some use cases, and a less good implementation for others. It can also feel a bit yucky to add a big beefy crate to your code, when you are only using a very small subset of its contents. In this application, we are using the StreamExt trait to make handling async streams easier. We’ve found that the best option right now is to use the futures-lite crate. It contains a subset of the futures crate. Install the futures-lite crate to handle async streams:
Then implement message reception. We are going to use a separate subscribe_loop function to keep the code more simple:
Let’s call this subscribe_loop function in our main function. Eventually, when we add the ability to send messages, we want this subscribe loop to be listening for incoming messages at the same time as we send outgoing messages. To do that, we are going to call the subscribe_loop function inside a tokio::spawn. That will spawn a task so that the subscribe loop is listening concurrently with our writing loop. Here are the imports and the main function so far:

The input loop

Now that we can receive messages, let’s code up how to send them. We are going to write an input_loop that reads from stdin. It’s going to take a Sender that can send Strings on a channel. Each time we get input from stdin, we will read it to a String buffer, and then send that string over the channel:
Much like we did for the subscribe_loop, we are going to spawn the input_loop on a thread. Tokio recommends that we actually spawn the loop listening to Stdin on a std thread rather than a tokio thread:
Okay, so we can read off of stdin. Now we need to broadcast what we are typing to the Topic (and also print our messages to ourselves so that we can read through the chat logically):
If you run the code now, you can finally see some signs of life! Type a message and you can see how we take the content from std and send it. It’s getting a bit lonely now, though. Let’s implement a way for other’s to join our Topic.

Implementing Signaling with Tickets

Let’s implement ticket-based signaling! This means we will turn the topic and our endpoint id information into a Ticket for others to use to join our Topic. We send the ticket by serializing the Ticket data and printing the serialized data to the terminal. We can then copy/paste for others to use.

Using the Ticket and Messages

Let’s update our main file to include printing a Ticket and broadcasting our name on the topic.
Now, when you run your code, you should see something like this:

Creating a Command-Line Interface

Here is where things get fun. We know how to create, join, and send and receive on a Topic. We also know how to get other to join that Topic. This has now created two different “roles” an endpoint can have: a topic “creator” and a topic “joiner”. One side “creates” the topic and the ticket, and the other side takes the ticket and uses it to join the topic and connect to the ticket creator. To “join”, we will need to pass in a Ticket as a command line argument. There is a great rust crate called clap that takes care of much of the CLI boiler plate for you. Install the clap crate for CLI argument parsing:
At this point, rather than show you a chunk of code, I’m going to lay out the entire file. Here are the bits to pay attention to: We now create a struct that handles the arguments for the CLI, and a struct that handles the commands for the CLI. We use clap to create a nice CLI. There are two commands open and join. join expects a `Ticket. We also now have a --name flag that we can optionally use as an identifier in the topic. If you use the open command, you create a topic. If you use the join command, you get a topic and a list of endpoint_ids from the ticket. In either case, we still print a ticket to the terminal. The smallest change, but a very important one, is that we go from using the subscribe method to the subscribe_and_join method. The subscribe method would return a Topic immediately. The subscribe_and_join method takes the given topic, joins it, and waits for someone else to join the topic before returning.

Running the Application

To join an existing chat room:
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.