//! iroh transport scaffolding for every.channel. use anyhow::{Context, Result}; use bytes::Bytes; use ec_core::{StreamCatalogEntry, StreamControlAnnouncement}; use futures_lite::StreamExt; use iroh::address_lookup::{ DhtAddressLookup, DiscoveryEvent, DnsAddressLookup, MdnsAddressLookup, PkarrPublisher, UserData, }; use iroh::endpoint::RelayMode; use iroh::{ address_lookup::memory::MemoryLookup, protocol::Router, Endpoint, EndpointAddr, PublicKey, SecretKey, }; use iroh_gossip::{ api::{Event, GossipReceiver, GossipSender}, net::{Gossip, GOSSIP_ALPN}, proto::TopicId, }; use std::collections::BTreeMap; use std::env; use std::time::{Duration, Instant}; pub const ALPN_MOQ: &[u8] = b"every.channel/moq/0"; pub const DEFAULT_CATALOG_TOPIC: &str = "every.channel/catalog/v1"; pub const DEFAULT_CONTROL_TOPIC: &str = "every.channel/control/v1"; pub const MDNS_USER_DATA: &str = "every.channel"; #[derive(Debug, Clone)] pub struct TokenBucket { capacity: u64, tokens: f64, refill_per_sec: f64, last_refill: Instant, } impl TokenBucket { pub fn new(capacity: u64, refill_per_sec: u64) -> Self { let capacity = capacity.max(1); let refill_per_sec = refill_per_sec.max(1) as f64; Self { capacity, tokens: capacity as f64, refill_per_sec, last_refill: Instant::now(), } } pub fn allow(&mut self, amount: u64) -> bool { self.refill(); let amount = amount as f64; if amount <= self.tokens { self.tokens -= amount; true } else { false } } fn refill(&mut self) { let now = Instant::now(); let elapsed = now.duration_since(self.last_refill).as_secs_f64(); if elapsed <= 0.0 { return; } self.tokens = (self.tokens + elapsed * self.refill_per_sec).min(self.capacity as f64); self.last_refill = now; } } #[cfg(test)] mod tests { use super::*; #[test] fn token_bucket_allows_and_refills() { let mut bucket = TokenBucket::new(10, 10); assert!(bucket.allow(7)); assert!(bucket.allow(3)); assert!(!bucket.allow(1)); // Force a refill without sleeping. bucket.last_refill = Instant::now() - Duration::from_secs(1); assert!(bucket.allow(1)); } } #[derive(Debug, Clone, Copy, Default)] pub struct DiscoveryConfig { pub dht: bool, pub mdns: bool, pub dns: bool, } impl DiscoveryConfig { pub fn from_env() -> Result { match env::var("EVERY_CHANNEL_IROH_DISCOVERY") { Ok(value) => Self::from_list(&value), Err(env::VarError::NotPresent) => Ok(Self::default()), Err(err) => Err(err.into()), } } pub fn from_list(value: &str) -> Result { let mut config = DiscoveryConfig::default(); for raw in value.split(|c: char| c == ',' || c == ';' || c.is_whitespace()) { let token = raw.trim().to_ascii_lowercase(); if token.is_empty() { continue; } match token.as_str() { "dht" => config.dht = true, "mdns" => config.mdns = true, "dns" => config.dns = true, "all" => { config.dht = true; config.mdns = true; config.dns = true; } "none" | "off" => { config = DiscoveryConfig::default(); } _ => { return Err(anyhow::anyhow!("unknown discovery mode: {token}")); } } } Ok(config) } } pub async fn build_endpoint( secret: Option, discovery: DiscoveryConfig, ) -> Result { let relay_mode = relay_mode_from_env().unwrap_or(RelayMode::Default); let mut builder = Endpoint::empty_builder(relay_mode); if let Some(secret) = secret { builder = builder.secret_key(secret); } if discovery.dns { builder = builder .address_lookup(PkarrPublisher::n0_dns()) .address_lookup(DnsAddressLookup::n0_dns()); } if discovery.dht { builder = builder.address_lookup(DhtAddressLookup::builder()); } if discovery.mdns { builder = builder.address_lookup(MdnsAddressLookup::builder()); } let endpoint = builder.bind().await?; endpoint.set_alpns(vec![ALPN_MOQ.to_vec()]); Ok(endpoint) } fn relay_mode_from_env() -> Result { let value = match env::var("EVERY_CHANNEL_IROH_RELAY") { Ok(value) => value, Err(env::VarError::NotPresent) => return Ok(RelayMode::Default), Err(err) => return Err(err.into()), }; match value.trim().to_ascii_lowercase().as_str() { "" | "default" => Ok(RelayMode::Default), "disabled" | "off" => Ok(RelayMode::Disabled), other => Err(anyhow::anyhow!("unknown relay mode: {other}")), } } pub async fn start_endpoint() -> Result { let discovery = DiscoveryConfig::from_env()?; build_endpoint(None, discovery).await } pub fn catalog_topic() -> TopicId { let hash = blake3::hash(DEFAULT_CATALOG_TOPIC.as_bytes()); TopicId::from_bytes(*hash.as_bytes()) } pub fn control_topic() -> TopicId { let hash = blake3::hash(DEFAULT_CONTROL_TOPIC.as_bytes()); TopicId::from_bytes(*hash.as_bytes()) } pub fn parse_endpoint_addr(value: &str) -> Result { let value = value.trim(); if value.starts_with('{') { let addr = serde_json::from_str::(value).context("invalid EndpointAddr json")?; return Ok(addr); } let id = value.parse::().context("invalid endpoint id")?; Ok(EndpointAddr::new(id)) } #[derive(Debug, Clone)] pub struct MdnsDiscovery { mdns: MdnsAddressLookup, endpoint_id: PublicKey, user_data: Option, } impl MdnsDiscovery { pub async fn start( endpoint: &Endpoint, user_data: Option<&str>, advertise: bool, ) -> Result { let mdns = MdnsAddressLookup::builder() .advertise(advertise) .build(endpoint.id()) .context("mdns address lookup failed")?; endpoint.address_lookup().add(mdns.clone()); let user_data = if let Some(value) = user_data { let data = UserData::try_from(value.to_string()).context("invalid mdns user data")?; endpoint.set_user_data_for_address_lookup(Some(data.clone())); Some(data) } else { None }; Ok(Self { mdns, endpoint_id: endpoint.id(), user_data, }) } pub async fn discover_peers(&self, timeout: Duration) -> Result> { let mut stream = self.mdns.subscribe().await; let deadline = Instant::now() + timeout; let mut peers: BTreeMap = BTreeMap::new(); loop { let now = Instant::now(); if now >= deadline { break; } let remaining = deadline - now; match tokio::time::timeout(remaining, stream.next()).await { Ok(Some(DiscoveryEvent::Discovered { endpoint_info, .. })) => { if endpoint_info.endpoint_id == self.endpoint_id { continue; } if let Some(expected) = self.user_data.as_ref() { if endpoint_info.data.user_data() != Some(expected) { continue; } } let addr = EndpointAddr::from(endpoint_info); peers.insert(addr.id, addr); } Ok(Some(DiscoveryEvent::Expired { .. })) => {} Ok(None) => break, Err(_) => break, } } Ok(peers.into_values().collect()) } } #[derive(Debug)] pub struct CatalogGossip { sender: GossipSender, receiver: GossipReceiver, _router: Router, _gossip: Gossip, _memory_lookup: MemoryLookup, } impl CatalogGossip { pub async fn join(endpoint: Endpoint, peers: &[String]) -> Result { let memory_lookup = MemoryLookup::new(); endpoint.address_lookup().add(memory_lookup.clone()); let gossip = Gossip::builder().spawn(endpoint.clone()); let router = Router::builder(endpoint.clone()) .accept(GOSSIP_ALPN, gossip.clone()) .spawn(); let peer_addrs = peers .iter() .map(|peer| parse_endpoint_addr(peer)) .collect::, _>>() .context("failed to parse gossip peer addr")?; for peer in &peer_addrs { memory_lookup.add_endpoint_info(peer.clone()); } let peer_ids = peer_addrs .iter() .map(|addr| addr.id) .collect::>(); // Allow local-only startup with no explicit bootstrap peers. In that mode we still // subscribe to the topic immediately and can add peers later. let topic = if peer_ids.is_empty() { gossip.subscribe(catalog_topic(), Vec::new()).await? } else { gossip.subscribe_and_join(catalog_topic(), peer_ids).await? }; let (sender, receiver) = topic.split(); Ok(Self { sender, receiver, _router: router, _gossip: gossip, _memory_lookup: memory_lookup, }) } pub async fn announce(&mut self, entry: StreamCatalogEntry) -> Result<()> { let bytes = serde_json::to_vec(&entry)?; self.sender.broadcast(Bytes::from(bytes)).await?; Ok(()) } pub async fn next_entry(&mut self) -> Result> { while let Some(event) = self.receiver.try_next().await? { if let Event::Received(msg) = event { if let Ok(entry) = serde_json::from_slice::(&msg.content) { return Ok(Some(entry)); } } } Ok(None) } /// Add peers after the gossip topic has already been joined. This enables /// "nearby" discovery to continuously contribute new peers over time. pub fn add_peers(&self, peers: Vec) { for peer in peers { self._memory_lookup.add_endpoint_info(peer); } } } #[derive(Debug)] pub struct ControlGossip { sender: GossipSender, receiver: GossipReceiver, _router: Router, _gossip: Gossip, _memory_lookup: MemoryLookup, } impl ControlGossip { pub async fn join(endpoint: Endpoint, peers: &[String]) -> Result { let memory_lookup = MemoryLookup::new(); endpoint.address_lookup().add(memory_lookup.clone()); let gossip = Gossip::builder().spawn(endpoint.clone()); let router = Router::builder(endpoint.clone()) .accept(GOSSIP_ALPN, gossip.clone()) .spawn(); let peer_addrs = peers .iter() .map(|peer| parse_endpoint_addr(peer)) .collect::, _>>() .context("failed to parse control peer addr")?; for peer in &peer_addrs { memory_lookup.add_endpoint_info(peer.clone()); } let peer_ids = peer_addrs .iter() .map(|addr| addr.id) .collect::>(); // Allow local-only startup with no explicit bootstrap peers. In that mode we still // subscribe to the topic immediately and can add peers later. let topic = if peer_ids.is_empty() { gossip.subscribe(control_topic(), Vec::new()).await? } else { gossip.subscribe_and_join(control_topic(), peer_ids).await? }; let (sender, receiver) = topic.split(); Ok(Self { sender, receiver, _router: router, _gossip: gossip, _memory_lookup: memory_lookup, }) } pub async fn announce(&mut self, announcement: StreamControlAnnouncement) -> Result<()> { let bytes = serde_json::to_vec(&announcement)?; self.sender.broadcast(Bytes::from(bytes)).await?; Ok(()) } pub async fn next_announcement(&mut self) -> Result> { while let Some(event) = self.receiver.try_next().await? { if let Event::Received(msg) = event { if let Ok(announcement) = serde_json::from_slice::(&msg.content) { return Ok(Some(announcement)); } } } Ok(None) } pub fn add_peers(&self, peers: Vec) { for peer in peers { self._memory_lookup.add_endpoint_info(peer); } } }