179 lines
6.6 KiB
Rust
179 lines
6.6 KiB
Rust
//! Bridge-side glue over the `iroh-gossip-rendezvous` crate.
|
|
//!
|
|
//! Starts the rendezvous, spawns a task that decodes incoming gossip events
|
|
//! into [`GossipEvent`]s the router understands, and a broadcaster task that
|
|
//! encodes outgoing [`GossipFrame`]s onto the iroh-gossip sender.
|
|
|
|
use std::collections::HashSet;
|
|
use std::sync::Arc;
|
|
|
|
use anyhow::{Context, Result};
|
|
use bytes::Bytes;
|
|
use iroh::{PublicKey, SecretKey};
|
|
use iroh_gossip::api::Event;
|
|
use iroh_gossip_rendezvous::Rendezvous;
|
|
pub use iroh_gossip_rendezvous::{DhtStatus, RendezvousState};
|
|
use tokio::sync::mpsc;
|
|
use tokio_util::sync::CancellationToken;
|
|
use tracing::{debug, error, warn};
|
|
|
|
use crate::packet::GossipFrame;
|
|
|
|
/// App salt for the donglora-bridge rendezvous surface. Versioned so a
|
|
/// future breaking change gets a fresh DHT meeting place — v1 and v2
|
|
/// bridges do not see each other.
|
|
pub const APP_SALT: &str = "donglora-bridge/v2";
|
|
|
|
/// Events from the gossip swarm to the router.
|
|
pub enum GossipEvent {
|
|
/// A gossip frame received from the swarm.
|
|
Frame(GossipFrame),
|
|
/// Neighbor count changed.
|
|
NeighborChanged(usize),
|
|
}
|
|
|
|
/// Handle held by the rest of the bridge. Owns the `Rendezvous` that in turn
|
|
/// owns all DHT + gossip background work. Dropping this stops everything.
|
|
pub struct Gossip {
|
|
rendezvous: Rendezvous,
|
|
cancel: CancellationToken,
|
|
}
|
|
|
|
impl Gossip {
|
|
/// Spin up the gossip swarm + DHT rendezvous.
|
|
///
|
|
/// Returns `(handle, event_rx, frame_tx)`:
|
|
/// - `event_rx`: channel of events from the swarm to the router
|
|
/// - `frame_tx`: channel for the router to send frames to broadcast
|
|
pub async fn new(
|
|
secret_key: SecretKey,
|
|
passphrase: &str,
|
|
) -> Result<(Self, mpsc::Receiver<GossipEvent>, mpsc::Sender<GossipFrame>)> {
|
|
let cancel = CancellationToken::new();
|
|
|
|
let rendezvous = Rendezvous::builder()
|
|
.passphrase(passphrase)
|
|
.app_salt(APP_SALT)
|
|
.secret_key(secret_key)
|
|
.build()
|
|
.await
|
|
.context("starting iroh-gossip rendezvous")?;
|
|
|
|
let my_id = rendezvous.node_id();
|
|
tracing::info!("iroh endpoint bound, id: {}", my_id.fmt_short());
|
|
tracing::info!("topic: {}", hex::encode(&rendezvous.topic_id().as_bytes()[..8]));
|
|
|
|
let (event_tx, event_rx) = mpsc::channel::<GossipEvent>(256);
|
|
let (frame_tx, frame_rx) = mpsc::channel::<GossipFrame>(256);
|
|
|
|
// Receiver task: drain the rendezvous event stream, translate into
|
|
// router-side `GossipEvent`s.
|
|
spawn_receiver(rendezvous.subscribe(), event_tx, cancel.clone());
|
|
|
|
// Broadcaster task: pull encoded frames from the router, hand them
|
|
// to the iroh-gossip sender.
|
|
spawn_broadcaster(rendezvous.sender().clone(), frame_rx, cancel.clone());
|
|
|
|
Ok((Self { rendezvous, cancel }, event_rx, frame_tx))
|
|
}
|
|
|
|
/// Snapshot of rendezvous state for the TUI.
|
|
#[must_use]
|
|
pub fn state(&self) -> RendezvousState {
|
|
self.rendezvous.state()
|
|
}
|
|
|
|
/// Graceful shutdown. Cancels our own glue tasks, then the rendezvous.
|
|
/// Idempotent.
|
|
pub async fn shutdown(&self) {
|
|
self.cancel.cancel();
|
|
self.rendezvous.shutdown().await;
|
|
}
|
|
}
|
|
|
|
// ── Internal tasks ──────────────────────────────────────────────────────
|
|
|
|
fn spawn_receiver(
|
|
mut rx: tokio::sync::broadcast::Receiver<Event>,
|
|
event_tx: mpsc::Sender<GossipEvent>,
|
|
cancel: CancellationToken,
|
|
) {
|
|
tokio::spawn(async move {
|
|
// Track our own neighbor set so we can send accurate counts to the
|
|
// router (the rendezvous crate tracks the same set internally for
|
|
// its `state()`; we duplicate here to avoid a cross-crate handle).
|
|
let mut neighbors: HashSet<PublicKey> = HashSet::new();
|
|
let exit_reason: &'static str = loop {
|
|
let event = tokio::select! {
|
|
ev = rx.recv() => ev,
|
|
() = cancel.cancelled() => break "cancelled",
|
|
};
|
|
let event = match event {
|
|
Ok(ev) => ev,
|
|
Err(tokio::sync::broadcast::error::RecvError::Closed) => break "broadcast closed",
|
|
Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
|
|
warn!("gossip receiver lagged — dropped {n} events");
|
|
continue;
|
|
}
|
|
};
|
|
match event {
|
|
Event::Received(msg) => {
|
|
if let Some(frame) = GossipFrame::decode(&msg.content) {
|
|
let _ = event_tx.send(GossipEvent::Frame(frame)).await;
|
|
} else {
|
|
debug!("received malformed gossip frame ({} bytes)", msg.content.len());
|
|
}
|
|
}
|
|
Event::NeighborUp(id) => {
|
|
neighbors.insert(id);
|
|
tracing::info!("neighbor up: {} (total: {})", id.fmt_short(), neighbors.len());
|
|
let _ = event_tx.send(GossipEvent::NeighborChanged(neighbors.len())).await;
|
|
}
|
|
Event::NeighborDown(id) => {
|
|
neighbors.remove(&id);
|
|
tracing::info!("neighbor down: {} (total: {})", id.fmt_short(), neighbors.len());
|
|
let _ = event_tx.send(GossipEvent::NeighborChanged(neighbors.len())).await;
|
|
}
|
|
Event::Lagged => {
|
|
warn!("gossip receiver lagged — some messages were dropped");
|
|
}
|
|
}
|
|
};
|
|
if exit_reason == "cancelled" {
|
|
debug!("gossip receiver stopped ({exit_reason})");
|
|
} else {
|
|
error!("gossip receiver STOPPED ({exit_reason}) — this bridge will no longer receive from the swarm",);
|
|
}
|
|
});
|
|
}
|
|
|
|
fn spawn_broadcaster(
|
|
sender: iroh_gossip::api::GossipSender,
|
|
mut frame_rx: mpsc::Receiver<GossipFrame>,
|
|
cancel: CancellationToken,
|
|
) {
|
|
tokio::spawn(async move {
|
|
loop {
|
|
let frame = tokio::select! {
|
|
frame = frame_rx.recv() => frame,
|
|
() = cancel.cancelled() => break,
|
|
};
|
|
let Some(frame) = frame else { break };
|
|
let encoded = Bytes::from(frame.encode());
|
|
if let Err(e) = sender.broadcast(encoded).await {
|
|
debug!("gossip broadcast error: {e:#}");
|
|
}
|
|
}
|
|
debug!("gossip broadcaster stopped");
|
|
});
|
|
}
|
|
|
|
// Shared via Arc<Gossip> for the TUI.
|
|
impl Gossip {
|
|
/// Wrap in `Arc` for sharing with the TUI task.
|
|
#[must_use]
|
|
pub fn into_arc(self) -> Arc<Self> {
|
|
Arc::new(self)
|
|
}
|
|
}
|