Distributed Erlang over iroh P2P + TLS, with a bot powers SDK
1//! iroh P2P transport for Erlang distribution.
2//!
3//! Connections are QUIC with TLS 1.3 identity (ed25519 endpoint keys). The
4//! Erlang cookie handshake still runs on top for BEAM-compatible auth.
5
6use crate::error::{Error, Result};
7use crate::stream::DistStream;
8use crate::ALPN;
9use iroh::endpoint::presets;
10use iroh::{Endpoint, EndpointAddr, EndpointId, SecretKey};
11use std::str::FromStr;
12use std::sync::Arc;
13use tracing::{debug, info};
14
15/// Manages an iroh [`Endpoint`] that speaks the `erl-iroh/1` ALPN.
16#[derive(Clone, Debug)]
17pub struct IrohTransport {
18 endpoint: Endpoint,
19}
20
21impl IrohTransport {
22 /// Bind a new endpoint with a random secret key (default N0 relays).
23 pub async fn bind() -> Result<Self> {
24 Self::bind_with_key(SecretKey::generate()).await
25 }
26
27 /// Bind with an explicit secret key (stable identity across restarts).
28 pub async fn bind_with_key(secret: SecretKey) -> Result<Self> {
29 let endpoint = Endpoint::builder(presets::N0)
30 .secret_key(secret)
31 .alpns(vec![ALPN.to_vec()])
32 .bind()
33 .await
34 .map_err(|e| Error::iroh(e))?;
35 info!(id = %endpoint.id(), "iroh endpoint bound");
36 Ok(Self { endpoint })
37 }
38
39 /// This endpoint's public id (share this so peers can dial you).
40 pub fn endpoint_id(&self) -> EndpointId {
41 self.endpoint.id()
42 }
43
44 /// Full addressing info (id + relay / direct addrs).
45 pub fn endpoint_addr(&self) -> EndpointAddr {
46 self.endpoint.addr()
47 }
48
49 /// Underlying iroh endpoint (for advanced use).
50 pub fn endpoint(&self) -> &Endpoint {
51 &self.endpoint
52 }
53
54 /// Dial a peer by endpoint id string (base32) and open a dist stream.
55 pub async fn connect(&self, peer: &str) -> Result<DistStream> {
56 let id = EndpointId::from_str(peer).map_err(|e| Error::InvalidName(e.to_string()))?;
57 self.connect_id(id).await
58 }
59
60 /// Dial a peer by [`EndpointId`].
61 pub async fn connect_id(&self, id: EndpointId) -> Result<DistStream> {
62 debug!(%id, "connecting over iroh");
63 let conn = self
64 .endpoint
65 .connect(id, ALPN)
66 .await
67 .map_err(|e| Error::iroh(e))?;
68 let (send, recv) = conn.open_bi().await.map_err(|e| Error::iroh(e))?;
69 Ok(DistStream::from_split(recv, send))
70 }
71
72 /// Dial with a full [`EndpointAddr`] (id + relay/direct hints).
73 pub async fn connect_addr(&self, addr: EndpointAddr) -> Result<DistStream> {
74 debug!(id = %addr.id, "connecting over iroh (with addr hints)");
75 let conn = self
76 .endpoint
77 .connect(addr, ALPN)
78 .await
79 .map_err(|e| Error::iroh(e))?;
80 let (send, recv) = conn.open_bi().await.map_err(|e| Error::iroh(e))?;
81 Ok(DistStream::from_split(recv, send))
82 }
83
84 /// Accept one inbound connection and return the dist stream + remote id.
85 pub async fn accept(&self) -> Result<(DistStream, EndpointId)> {
86 let incoming = self
87 .endpoint
88 .accept()
89 .await
90 .ok_or_else(|| Error::iroh("endpoint closed"))?;
91 let conn = incoming.await.map_err(|e| Error::iroh(e))?;
92 let remote = conn.remote_id();
93 let (send, recv) = conn.accept_bi().await.map_err(|e| Error::iroh(e))?;
94 debug!(%remote, "accepted iroh connection");
95 Ok((DistStream::from_split(recv, send), remote))
96 }
97
98 /// Gracefully close the endpoint.
99 pub async fn close(self) {
100 self.endpoint.close().await;
101 }
102}
103
104/// Shared handle used by accept loops.
105pub type SharedIroh = Arc<IrohTransport>;