pub mod direct;
pub mod identity;
pub mod proto;
pub mod stats;
pub mod ticket;
use std::{
collections::HashMap,
sync::{Arc, Mutex, RwLock},
};
use anyhow::{Context, Result};
use iroh::{
Endpoint, EndpointAddr, EndpointId, RelayConfig, RelayMap, RelayMode, SecretKey,
address_lookup::memory::MemoryLookup,
endpoint::presets,
protocol::{DynProtocolHandler, Router},
};
use iroh_gossip::{
api::{Event as GossipEvent, GossipReceiver, GossipSender},
net::{GOSSIP_ALPN, Gossip},
proto::TopicId,
};
use n0_future::StreamExt;
use tokio::sync::mpsc;
pub use proto::{Audience, Kind, Payload};
pub use ticket::RoomTicket;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Via {
Gossip,
Direct,
}
#[derive(Debug, Clone, Default)]
pub enum Relays {
#[default]
N0,
Extend(Vec<RelayConfig>),
Only(Vec<RelayConfig>),
Disabled,
}
impl Relays {
fn mode(&self) -> RelayMode {
match self {
Relays::N0 => RelayMode::Default,
Relays::Disabled => RelayMode::Disabled,
Relays::Only(relays) => RelayMode::Custom(RelayMap::from_iter(relays.iter().cloned())),
Relays::Extend(relays) => {
let n0: Vec<Arc<RelayConfig>> = RelayMode::Default.relay_map().relays();
let all = n0
.iter()
.map(|c| RelayConfig::clone(c))
.chain(relays.iter().cloned());
RelayMode::Custom(RelayMap::from_iter(all))
}
}
}
}
#[derive(Debug, Clone)]
pub struct Frame {
pub topic: TopicId,
pub from: EndpointId,
pub kind: Kind,
pub body: Vec<u8>,
pub sent_at_ms: u64,
pub via: Via,
}
#[derive(Debug, Clone)]
pub enum Event {
Frame(Frame),
Neighbor {
topic: TopicId,
peer: EndpointId,
up: bool,
},
Notice {
topic: Option<TopicId>,
text: String,
},
}
pub struct Config {
pub secret_key: SecretKey,
pub relays: Relays,
pub protocols: Vec<(Vec<u8>, Box<dyn DynProtocolHandler>)>,
}
pub struct Ctx {
endpoint: Endpoint,
secret: SecretKey,
events: mpsc::UnboundedSender<Event>,
topics: RwLock<HashMap<TopicId, Arc<TopicState>>>,
addrs: RwLock<HashMap<EndpointId, EndpointAddr>>,
}
pub struct TopicState {
topic: TopicId,
sender: GossipSender,
task: Mutex<Option<n0_future::task::JoinHandle<()>>>,
}
impl std::fmt::Debug for Ctx {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Ctx").field("me", &self.me()).finish()
}
}
impl Ctx {
pub fn me(&self) -> EndpointId {
self.secret.public()
}
pub fn endpoint(&self) -> &Endpoint {
&self.endpoint
}
fn emit(&self, event: Event) {
let _ = self.events.send(event);
}
pub fn report(&self, topic: Option<TopicId>, text: impl Into<String>) {
self.emit(Event::Notice {
topic,
text: text.into(),
});
}
pub fn remember(&self, addr: EndpointAddr) {
self.addrs
.write()
.unwrap_or_else(|e| e.into_inner())
.insert(addr.id, addr);
}
pub fn addr_for(&self, id: EndpointId) -> EndpointAddr {
self.addrs
.read()
.unwrap_or_else(|e| e.into_inner())
.get(&id)
.cloned()
.unwrap_or_else(|| EndpointAddr::new(id))
}
fn topic(&self, topic: &TopicId) -> Option<Arc<TopicState>> {
self.topics
.read()
.unwrap_or_else(|e| e.into_inner())
.get(topic)
.cloned()
}
fn sign(
&self,
topic: TopicId,
audience: Audience,
kind: Kind,
body: Vec<u8>,
) -> Result<Vec<u8>> {
proto::encode_raw(&self.secret, topic, audience, kind, body)
}
pub async fn handle_frame(&self, bytes: &[u8], via: Via) -> Result<Option<Vec<u8>>> {
let (from, envelope) = proto::decode(bytes)?;
if from == self.me() {
return Ok(None);
}
let Some(state) = self.topic(&envelope.topic) else {
return Ok(None);
};
if !envelope.audience.includes(&self.me()) {
return Ok(None);
}
if envelope.kind == Kind::PING {
let ping = proto::Ping::from_body(&envelope.body)?;
let pong = proto::Pong {
nonce: ping.nonce,
ping_sent_at_ms: envelope.sent_at_ms,
};
let frame = self.sign(
state.topic,
Audience::Only(vec![from]),
Kind::PONG,
pong.to_body()?,
)?;
return match via {
Via::Direct => Ok(Some(frame)),
Via::Gossip => {
state.sender.broadcast(frame.into()).await?;
Ok(None)
}
};
}
self.emit(Event::Frame(Frame {
topic: state.topic,
from,
kind: envelope.kind,
body: envelope.body,
sent_at_ms: envelope.sent_at_ms,
via,
}));
Ok(None)
}
}
pub struct Node {
ctx: Arc<Ctx>,
relays_disabled: bool,
gossip: Gossip,
lookup: MemoryLookup,
router: Router,
events: Mutex<Option<mpsc::UnboundedReceiver<Event>>>,
}
impl Node {
pub async fn spawn(config: Config) -> Result<Self> {
let (events_tx, events_rx) = mpsc::unbounded_channel();
let lookup = MemoryLookup::new();
let endpoint = Endpoint::builder(presets::N0)
.secret_key(config.secret_key.clone())
.relay_mode(config.relays.mode())
.address_lookup(lookup.clone())
.bind()
.await
.context("bind endpoint")?;
let relays_disabled = matches!(config.relays, Relays::Disabled);
let gossip = Gossip::builder().spawn(endpoint.clone());
let ctx = Arc::new(Ctx {
endpoint: endpoint.clone(),
secret: config.secret_key,
events: events_tx,
topics: RwLock::new(HashMap::new()),
addrs: RwLock::new(HashMap::new()),
});
let mut router = Router::builder(endpoint)
.accept(GOSSIP_ALPN, gossip.clone())
.accept(direct::ALPN, direct::DirectHandler::new(ctx.clone()));
for (alpn, handler) in config.protocols {
router = router.accept(alpn, handler);
}
Ok(Node {
ctx,
relays_disabled,
gossip,
lookup,
router: router.spawn(),
events: Mutex::new(Some(events_rx)),
})
}
pub fn ctx(&self) -> Arc<Ctx> {
self.ctx.clone()
}
pub fn id(&self) -> EndpointId {
self.ctx.me()
}
pub fn endpoint(&self) -> &Endpoint {
&self.ctx.endpoint
}
pub fn addr(&self) -> EndpointAddr {
self.ctx.endpoint.addr()
}
pub async fn online(&self) {
if self.relays_disabled {
use iroh::Watcher;
let mut addr = self.ctx.endpoint.watch_addr();
loop {
if !addr.get().addrs.is_empty() {
return;
}
if addr.updated().await.is_err() {
return;
}
}
} else {
self.ctx.endpoint.online().await;
}
}
pub fn take_events(&self) -> Option<mpsc::UnboundedReceiver<Event>> {
self.events.lock().unwrap_or_else(|e| e.into_inner()).take()
}
pub async fn join(&self, topic: TopicId, bootstrap: Vec<EndpointAddr>) -> Result<Topic> {
let me = self.id();
for addr in &bootstrap {
if addr.id != me {
self.lookup.add_endpoint_info(addr.clone());
self.ctx.remember(addr.clone());
}
}
let peers: Vec<EndpointId> = bootstrap
.iter()
.map(|a| a.id)
.filter(|id| *id != me)
.collect();
if peers.is_empty() && !bootstrap.is_empty() {
anyhow::bail!(
"this ticket names only {}, which is you; two instances need two identities",
me.fmt_short()
);
}
let handle = self
.gossip
.subscribe(topic, peers)
.await
.context("subscribe to topic")?;
let (sender, receiver) = handle.split();
let state = Arc::new(TopicState {
topic,
sender,
task: Mutex::new(None),
});
self.ctx
.topics
.write()
.unwrap_or_else(|e| e.into_inner())
.insert(topic, state.clone());
let task = n0_future::task::spawn(gossip_loop(self.ctx.clone(), state.clone(), receiver));
*state.task.lock().unwrap_or_else(|e| e.into_inner()) = Some(task);
Ok(Topic {
ctx: self.ctx.clone(),
state,
})
}
pub async fn shutdown(&self) -> Result<()> {
let topics: Vec<_> = self
.ctx
.topics
.read()
.unwrap_or_else(|e| e.into_inner())
.values()
.cloned()
.collect();
for state in topics {
let _ = Topic {
ctx: self.ctx.clone(),
state,
}
.leave()
.await;
}
self.router
.shutdown()
.await
.map_err(|e| anyhow::anyhow!(e))?;
self.ctx.endpoint.close().await;
Ok(())
}
}
#[derive(Clone)]
pub struct Topic {
ctx: Arc<Ctx>,
state: Arc<TopicState>,
}
impl Topic {
pub fn id(&self) -> TopicId {
self.state.topic
}
pub fn ticket(&self, name: impl Into<String>) -> RoomTicket {
RoomTicket::new(self.state.topic, name, vec![self.ctx.endpoint.addr()])
}
pub async fn broadcast(&self, kind: Kind, body: Vec<u8>) -> Result<()> {
let frame = self
.ctx
.sign(self.state.topic, Audience::Everyone, kind, body)?;
self.state
.sender
.broadcast(frame.into())
.await
.context("broadcast")?;
Ok(())
}
pub async fn broadcast_payload<P: Payload>(&self, payload: &P) -> Result<()> {
self.broadcast(P::KIND, payload.to_body()?).await
}
pub async fn send_to(
&self,
to: EndpointId,
kind: Kind,
body: Vec<u8>,
) -> Result<Option<Vec<u8>>> {
let frame = self
.ctx
.sign(self.state.topic, Audience::Only(vec![to]), kind, body)?;
direct::request(&self.ctx.endpoint, self.ctx.addr_for(to), &frame).await
}
pub async fn ping(&self, to: EndpointId) -> Result<u64> {
let nonce: u64 = rand::random();
let sent_at = proto::now_ms();
let reply = self
.send_to(to, Kind::PING, proto::Ping { nonce }.to_body()?)
.await?
.context("peer accepted the ping but did not answer it")?;
let (_, envelope) = proto::decode(&reply)?;
let pong = proto::Pong::from_body(&envelope.body)?;
anyhow::ensure!(pong.nonce == nonce, "pong answers a different ping");
Ok(proto::now_ms().saturating_sub(sent_at))
}
pub async fn leave(&self) -> Result<()> {
let goodbye = self.broadcast_payload(&proto::Goodbye).await;
self.ctx
.topics
.write()
.unwrap_or_else(|e| e.into_inner())
.remove(&self.state.topic);
if let Some(task) = self
.state
.task
.lock()
.unwrap_or_else(|e| e.into_inner())
.take()
{
task.abort();
}
goodbye
}
}
async fn gossip_loop(ctx: Arc<Ctx>, state: Arc<TopicState>, mut receiver: GossipReceiver) {
loop {
match receiver.try_next().await {
Ok(Some(event)) => match event {
GossipEvent::NeighborUp(peer) => ctx.emit(Event::Neighbor {
topic: state.topic,
peer,
up: true,
}),
GossipEvent::NeighborDown(peer) => ctx.emit(Event::Neighbor {
topic: state.topic,
peer,
up: false,
}),
GossipEvent::Received(message) => {
if let Err(e) = ctx.handle_frame(&message.content, Via::Gossip).await {
ctx.report(
Some(state.topic),
format!("from {}: {e}", message.delivered_from.fmt_short()),
);
}
}
GossipEvent::Lagged => ctx.report(
Some(state.topic),
"fell behind on this topic; some messages were dropped",
),
},
Ok(None) => break,
Err(e) => {
ctx.report(Some(state.topic), format!("topic ended: {e}"));
break;
}
}
}
}