//! iroh P2P transport for Erlang distribution. //! //! Connections are QUIC with TLS 1.3 identity (ed25519 endpoint keys). The //! Erlang cookie handshake still runs on top for BEAM-compatible auth. use crate::error::{Error, Result}; use crate::stream::DistStream; use crate::ALPN; use iroh::endpoint::presets; use iroh::{Endpoint, EndpointAddr, EndpointId, SecretKey}; use std::str::FromStr; use std::sync::Arc; use tracing::{debug, info}; /// Manages an iroh [`Endpoint`] that speaks the `erl-iroh/1` ALPN. #[derive(Clone, Debug)] pub struct IrohTransport { endpoint: Endpoint, } impl IrohTransport { /// Bind a new endpoint with a random secret key (default N0 relays). pub async fn bind() -> Result { Self::bind_with_key(SecretKey::generate()).await } /// Bind with an explicit secret key (stable identity across restarts). pub async fn bind_with_key(secret: SecretKey) -> Result { let endpoint = Endpoint::builder(presets::N0) .secret_key(secret) .alpns(vec![ALPN.to_vec()]) .bind() .await .map_err(|e| Error::iroh(e))?; info!(id = %endpoint.id(), "iroh endpoint bound"); Ok(Self { endpoint }) } /// This endpoint's public id (share this so peers can dial you). pub fn endpoint_id(&self) -> EndpointId { self.endpoint.id() } /// Full addressing info (id + relay / direct addrs). pub fn endpoint_addr(&self) -> EndpointAddr { self.endpoint.addr() } /// Underlying iroh endpoint (for advanced use). pub fn endpoint(&self) -> &Endpoint { &self.endpoint } /// Dial a peer by endpoint id string (base32) and open a dist stream. pub async fn connect(&self, peer: &str) -> Result { let id = EndpointId::from_str(peer).map_err(|e| Error::InvalidName(e.to_string()))?; self.connect_id(id).await } /// Dial a peer by [`EndpointId`]. pub async fn connect_id(&self, id: EndpointId) -> Result { debug!(%id, "connecting over iroh"); let conn = self .endpoint .connect(id, ALPN) .await .map_err(|e| Error::iroh(e))?; let (send, recv) = conn.open_bi().await.map_err(|e| Error::iroh(e))?; Ok(DistStream::from_split(recv, send)) } /// Dial with a full [`EndpointAddr`] (id + relay/direct hints). pub async fn connect_addr(&self, addr: EndpointAddr) -> Result { debug!(id = %addr.id, "connecting over iroh (with addr hints)"); let conn = self .endpoint .connect(addr, ALPN) .await .map_err(|e| Error::iroh(e))?; let (send, recv) = conn.open_bi().await.map_err(|e| Error::iroh(e))?; Ok(DistStream::from_split(recv, send)) } /// Accept one inbound connection and return the dist stream + remote id. pub async fn accept(&self) -> Result<(DistStream, EndpointId)> { let incoming = self .endpoint .accept() .await .ok_or_else(|| Error::iroh("endpoint closed"))?; let conn = incoming.await.map_err(|e| Error::iroh(e))?; let remote = conn.remote_id(); let (send, recv) = conn.accept_bi().await.map_err(|e| Error::iroh(e))?; debug!(%remote, "accepted iroh connection"); Ok((DistStream::from_split(recv, send), remote)) } /// Gracefully close the endpoint. pub async fn close(self) { self.endpoint.close().await; } } /// Shared handle used by accept loops. pub type SharedIroh = Arc;