# Iroh JOnTheBeach Workshop
Clone https://github.com/n0-computer/iroh-workshop-jonthebeach
```
git clone https://github.com/n0-computer/iroh-workshop-jonthebeach
```
Join https://discord.gg/Ddc38zsW
WIFI: `on the beach`
# Agenda
In this workshop, we'll craft a ~~sleek~~, cross-platform chat application from scratch.
Bring your devices—whether Linux, Windows, macOS, ~~or iOS~~ —as we ensure compatibility across the board.
With a Rust toolchain and IDE in hand, we'll dive into the nitty-gritty details. ~~Fear not if you're not well-versed in Rust, as I'll provide minimal Python and Swift/iOS examples in a dedicated GitHub repository.~~
Throughout the session, you'll master the art of establishing direct connections using the iroh-net networking library, crafting custom application protocols via QUIC streams. Discover the joy o
f simplifying complex networking tasks and gain insights into iroh's utilization of the tailscale DERP protocol for direct connections. Don't miss this opportunity to unlock the secrets of peer-to-peer communication in a fun and interactive setting!
---
# Introduction
- Who wrote a simple TCP server before?
- Who has got experience with rust?
- Who has cargo installed?
---
# Exercise 1: Direct 1:1 connections
We will write a tool similar to https://www.dumbpipe.dev/ that connects two devices anywhere in the world.
## Project setup
Clone https://github.com/n0-computer/iroh-workshop-jonthebeach.
This will give you the code for all steps.
## Dependecies
[Project Setup](https://github.com/n0-computer/iroh-workshop-jonthebeach/commit/561d76bffc8b8494401238dc01d206281190af45)
## Connect
Creating an endpoint:
```rust
let endpoint = MagicEndpoint::builder()
.bind(0)
.await?;
```
Connecting
```rust
const JOTB_ALPN: &[u8] = b"JOTB2024";
let connection = endpoint.connect(addr, JOTB_ALPN).await?;
```
(Works only if the remote accepts MY_ALPN)
Opening a stream
```rust
let (send, recv) = connection.open_bi().await?;
```
Commit: [Minimal connect](https://github.com/n0-computer/iroh-workshop-jonthebeach/commit/264511f140df717ce2d8399c26b33faede092930)
Copy stdin to send and recv to stdout
```rust
tokio::spawn(copy_to_stdout(remote, recv));
copy_stdin_to(send).await?;
```
Commit: [Copy from stdin and to stdout](https://github.com/n0-computer/iroh-workshop-jonthebeach/commit/9616dcd56ab7a7b990845ae971072f7ac0783b39)
## Accept
Creating an endpoint:
For accept we must provide the set of ALPNs
```rust
const JOTB_ALPN: &[u8] = b"JOTB2024";
let endpoint = MagicEndpoint::builder()
.alpns(vec![PIPE_ALPN.to_vec()])
.bind(0)
.await?;
```
Print ticket:
```rust
let addr = endpoint.my_addr().await?;
println!("I am {}", addr.node_id);
println!("Listening on {:#?}", addr.info);
println!("ticket: {}", NodeTicket::new(addr)?);
```
Accept loop:
```rust
while let Some(connecting) = endpoint.accept().await {
// handle each incoming connection in separate tasks.
}
```
[Accept endpoint creation](https://github.com/n0-computer/iroh-workshop-jonthebeach/commit/559409e90ce3c171ff8fee4156373ebad2008860)
For each accept:
```rust
let (remote_node_id, alpn, connection) = magic_endpoint::accept_conn(connecting).await?;
let (send, recv) = connection.accept_bi().await?;
let author = remote_node_id.to_string();
// Send a greeting to the remote node.
send.write_all("hello\n".as_bytes()).await?;
// Spawn two tasks to copy data in both directions.
tokio::spawn(copy_stdin_to(send));
tokio::spawn(copy_to_stdout(author, recv));
```
[Handle accept](https://github.com/n0-computer/iroh-workshop-jonthebeach/commit/8c0c2d3c51ece0fe915e9cb5c1714151825a9677)
## Polish
```rust
let secret_key = get_or_create_secret()?;
```
Allows to specify the secret via an environment variable, to have a stable node id over multiple runs.
```rust
wait_for_relay(&endpoint).await?;
```
Wait for the node to figure out it's own relay URL
Commit: [Some polish](https://github.com/n0-computer/iroh-workshop-jonthebeach/commit/1c49836f6804ae5aeb8114e22db627e0fec6f0e4)
## Let's try it out
One terminal
```
cargo run -p pipe1
```
```
cargo run -p pipe1 <ticket>
```
## Use iroh DNS node discovery
https://twitter.com/iroh_n0/status/1787847932084928655
on the connect side, I want to *look up* node ids using the default iroh dns server
```rust
let discovery = DnsDiscovery::n0_dns();
let endpoint = MagicEndpoint::builder()
.secret_key(secret_key)
.discovery(Box::new(discovery))
...
```
on the accept side, I want to *publish* node ids to the default iroh dns server
```rust
let discovery = PkarrPublisher::n0_dns(secret_key.clone());
let endpoint = MagicEndpoint::builder()
.secret_key(secret_key)
.discovery(Box::new(discovery))
...
```
[Use iroh DNS discovery](https://github.com/n0-computer/iroh-workshop-jonthebeach/commit/8220d606409c3b92a40f7b1abf2e7d562ee224e4)
```
cargo run -p pipe2
```
https://www.diggui.com
## Use pkarr node discovery
both use `PkarrNodeDiscovery`.
on the connect side, we don't want to publish, so we don't need the secret key.
```rust
let discovery = PkarrNodeDiscovery::default();
let endpoint = MagicEndpoint::builder()
.secret_key(secret_key)
.discovery(Box::new(discovery))
...
```
on the accept side, we do want to publish, so we do need the secret key
```rust
let discovery = PkarrNodeDiscovery::builder()
.secret_key(secret_key.clone())
.build()?;
let endpoint = MagicEndpoint::builder()
.secret_key(secret_key)
.discovery(Box::new(discovery))
...
```
[PKARR discovery](https://github.com/n0-computer/iroh-workshop-jonthebeach/commit/3fbf7a270c6f4884a58ce9fbcf7d7758c44a59a1)
```
cargo run -p chat3
```
## Publish full addresses, not just relay URL
```rust
let discovery = PkarrNodeDiscovery::builder()
.secret_key(secret_key.clone())
.include_direct_addresses(true)
.build()?;
let endpoint = MagicEndpoint::builder()
.secret_key(secret_key)
.discovery(Box::new(discovery))
...
```
[Also publish addresses](https://github.com/n0-computer/iroh-workshop-jonthebeach/commit/e18807bfa89b9b8debb50b1cfc4de5c4fcdd2798)
```
cargo run -p chat4
```
# Exercise 2: Group chat
We will write a command line group chat.
## Project setup
We need an additional dependency
```toml
# iroh gossip protocol
iroh-gossip = { version = "0.15" }
```
## Create the endpoint
We need to allow the ALPN for the gossip protocol
```rust
let endpoint = MagicEndpoint::builder()
.secret_key(secret_key.clone())
.alpns(vec![iroh_gossip::net::GOSSIP_ALPN.to_vec()])
.bind(0)
.await?;
```
## Add the info from the addresses
```rust
// add all the info from the tickets to the endpoint
let mut ids = Vec::new();
for ticket in &args.tickets {
let addr = ticket.node_addr();
endpoint.add_node_addr(addr.clone())?;
ids.push(addr.node_id);
}
let gossip = Gossip::from_endpoint(...
```
## Handle incoming connections
We don't handle incoming connections manually. Instead we just feed them to the gossip. Since we are listening on only one ALPN we don't need to check the ALPN.
```rust
while let Some(connecting) = endpoint.accept().await {
let gossip = gossip.clone();
tokio::spawn(async move {
let (_, _, connection) = magic_endpoint::accept_conn(connecting).await?;
gossip.handle_connection(connection).await?;
anyhow::Ok(())
});
}
```
## Print incoming messages to stdout
Subscribe the topic and print everything that we get.
```rust
let mut stream = gossip.subscribe(topic).await?;
while let Ok(event) = stream.recv().await {
if let Event::Received(ev) = event {
let text = String::from_utf8_lossy(&ev.content);
println!("message {}", text);
}
}
```
## Read messages from stdin and send them
```rust
gossip.join(topic, ids).await?.await?;
let mut stdin = BufReader::new(tokio::io::stdin()).lines();
while let Some(line) = stdin.next_line().await? {
gossip.broadcast(topic, line.into_bytes().into()).await?;
}
```
We got a working chat!
```
cargo run -p chat1
```
## A proper protocol
We send around signed messages, so we know whom they are from!
```rust
#[derive(Debug, Serialize, Deserialize)]
enum Message {
Message { text: String },
// more message types will be added later
}
#[derive(Debug, Serialize, Deserialize)]
struct SignedMessage {
from: PublicKey,
data: Vec<u8>,
signature: Signature,
}
impl SignedMessage {
pub fn sign_and_encode(secret_key: &SecretKey, message: &Message) -> anyhow::Result<Vec<u8>> {
let data = postcard::to_stdvec(&message)?;
let signature = secret_key.sign(&data);
let from = secret_key.public();
let signed_message = Self {
from,
data,
signature,
};
let encoded = postcard::to_stdvec(&signed_message)?;
Ok(encoded)
}
pub fn verify_and_decode(bytes: &[u8]) -> anyhow::Result<(PublicKey, Message)> {
let signed_message: Self = postcard::from_bytes(bytes)?;
let key = signed_message.from;
key.verify(&signed_message.data, &signed_message.signature)?;
let message: Message = postcard::from_bytes(&signed_message.data)?;
Ok((signed_message.from, message))
}
}
```
## Wiring it up
Verify and decode incoming messages. Silently ignore non-verified messages
```rust
let Ok((from, msg)) = SignedMessage::verify_and_decode(&ev.content) else {
continue;
};
```
Handle the incoming messages (only one message type for now):
```rust=
match msg {
Message::Message { text } => { println!("{}> {}", from, text); }
// more message types will be added later
}
```
```
cargo run -p chat2
``````
cargo run -p chat1
```
## Encrypted direct messages
Extend the Message enum
```rust
enum Message {
Message { text: String },
Direct { to: PublicKey, encrypted: Vec<u8> },
}
```
Encryption:
Support `/for <publickey> <message>` syntax
```rust
let msg = if let Some(private) = line.strip_prefix("/for ") {
// yeah yeah, there are nicer ways to do this, sue me...
let mut parts = private.splitn(2, ' ');
let Some(to) = parts.next() else {
continue;
};
let Some(msg) = parts.next() else {
continue;
};
let Ok(to) = PublicKey::from_str(to) else {
continue;
};
let mut encrypted = msg.as_bytes().to_vec();
// encrypt the data in place
secret_key.shared(&to).seal(&mut encrypted);
Message::Direct { to, encrypted }
} else { ...
```
Decryption:
```rust
match msg {
...
Message::Direct { to, encrypted } => {
if to != secret_key.public() {
// not for us
return Ok(());
}
let mut buffer = encrypted;
secret_key.shared(&from).open(&mut buffer)?;
let message = std::str::from_utf8(&buffer)?;
println!("got encrypted message from {}: {}", from, message);
}
}
```
```
cargo run -p chat3
```
## Why do duplicate messages get swallowed?
https://docs.rs/iroh-gossip/latest/iroh_gossip/net/struct.Gossip.html#method.broadcast
Messages with the same content are only delivered once.
```rust
struct SignedMessage {
uid: u128, // just add a big random number. Crude, but works!
from: PublicKey,
data: Vec<u8>,
signature: Signature,
}
```
```
cargo run -p chat4
```
## Homework: support sending files
Syntax:
`/share <file>`
This involves using iroh-bytes and handling two different ALPNs, [GOSSIP_ALPN](https://docs.rs/iroh-gossip/latest/iroh_gossip/net/constant.GOSSIP_ALPN.html) and [iroh_bytes::protocol::ALPN](https://docs.rs/iroh-bytes/latest/iroh_bytes/protocol/constant.ALPN.html)
Depending on the incoming ALPN you have to dispatch to gossip or bytes.
## Homework: aliases
Syntax:
`/alias <alias>`
User can define an alias. All receivers of this alias from then on refer to the user just as `<alias>` instead of by node id.
This requires changing the code to have common mutable state between send and receive.