Skip to main content

fedimint_core/net/
iroh.rs

1use std::net::SocketAddr;
2use std::time::Duration;
3
4use fedimint_core::util::SafeUrl;
5use fedimint_logging::LOG_NET_IROH;
6use iroh::defaults::DEFAULT_STUN_PORT;
7use iroh::discovery::pkarr::{PkarrPublisher, PkarrResolver};
8use iroh::endpoint::{Builder, TransportConfig};
9use iroh::{Endpoint, RelayMode, RelayNode, RelayUrl, SecretKey};
10use iroh_relay::RelayQuicConfig;
11use thiserror::Error;
12use tracing::{debug, info, warn};
13use url::Url;
14
15use crate::envs::{
16    FM_IROH_DHT_ENABLE_ENV, FM_IROH_N0_DISCOVERY_ENABLE_ENV, FM_IROH_PKARR_PUBLISHER_ENABLE_ENV,
17    FM_IROH_PKARR_RESOLVER_ENABLE_ENV, FM_IROH_RELAYS_ENABLE_ENV, is_env_var_set,
18    is_env_var_set_opt,
19};
20
21const DEFAULT_IROH_RELAYS: [&str; 2] = [
22    "https://euc1-1.relay.elsirion.fedimint.iroh.link/",
23    "https://use1-1.relay.elsirion.fedimint.iroh.link/",
24];
25
26/// QUIC idle timeout for every iroh endpoint.
27///
28/// With [`IROH_KEEP_ALIVE_INTERVAL`] underneath it, a dead connection surfaces
29/// within ~5s instead of the 30s QUIC default. The negotiated idle timeout is
30/// the minimum of both sides', so a guardian setting this caps detection
31/// latency for clients whose endpoints keep iroh's defaults. The path-level
32/// settings stay untouched — iroh tunes those for hole punching.
33pub const IROH_IDLE_TIMEOUT: Duration = Duration::from_secs(5);
34
35/// QUIC keep-alive interval for every iroh endpoint, see
36/// [`IROH_IDLE_TIMEOUT`].
37pub const IROH_KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(1);
38
39pub async fn build_iroh_endpoint(
40    secret_key: SecretKey,
41    bind_addr: SocketAddr,
42    iroh_dns: Option<SafeUrl>,
43    iroh_relays: Vec<SafeUrl>,
44    alpn: &[u8],
45) -> Result<Endpoint, IrohEndpointError> {
46    let relay_mode = if !is_env_var_set_opt(FM_IROH_RELAYS_ENABLE_ENV).unwrap_or(true) {
47        warn!(
48            target: LOG_NET_IROH,
49            "Iroh relays are disabled"
50        );
51        RelayMode::Disabled
52    } else if iroh_relays.is_empty() {
53        RelayMode::Custom(
54            DEFAULT_IROH_RELAYS
55                .into_iter()
56                .map(|url| {
57                    relay_node_from_url(Url::parse(url).expect("default Iroh relay URL is valid"))
58                })
59                .collect(),
60        )
61    } else {
62        RelayMode::Custom(
63            iroh_relays
64                .into_iter()
65                .map(|url| relay_node_from_url(url.to_unsafe()))
66                .collect(),
67        )
68    };
69
70    let mut builder = Endpoint::builder();
71
72    if let Some(iroh_dns) = iroh_dns.map(SafeUrl::to_unsafe) {
73        if is_env_var_set_opt(FM_IROH_PKARR_PUBLISHER_ENABLE_ENV).unwrap_or(true) {
74            builder = builder.add_discovery({
75                let iroh_dns = iroh_dns.clone();
76                move |sk: &SecretKey| Some(PkarrPublisher::new(sk.clone(), iroh_dns))
77            });
78        } else {
79            warn!(
80                target: LOG_NET_IROH,
81                "Iroh pkarr publisher is disabled"
82            );
83        }
84
85        if is_env_var_set_opt(FM_IROH_PKARR_RESOLVER_ENABLE_ENV).unwrap_or(true) {
86            builder = builder.add_discovery(|_| Some(PkarrResolver::new(iroh_dns)));
87        } else {
88            warn!(
89                target: LOG_NET_IROH,
90                "Iroh pkarr resolver is disabled"
91            );
92        }
93    }
94
95    // See <https://github.com/fedimint/fedimint/issues/7811>
96    if is_env_var_set(FM_IROH_DHT_ENABLE_ENV) {
97        #[cfg(not(target_family = "wasm"))]
98        {
99            debug!(
100                target: LOG_NET_IROH,
101                "Iroh DHT is enabled"
102            );
103            builder = builder.discovery_dht();
104        }
105    } else {
106        info!(
107            target: LOG_NET_IROH,
108            "Iroh DHT is disabled"
109        );
110    }
111
112    builder = add_n0_discovery(builder);
113
114    let mut transport_config = TransportConfig::default();
115    transport_config.max_idle_timeout(Some(
116        IROH_IDLE_TIMEOUT
117            .try_into()
118            .expect("idle timeout fits in IdleTimeout"),
119    ));
120    // Iroh's default builder sets keep_alive_interval to 1s, but since we're
121    // providing a custom TransportConfig we need to set it explicitly.
122    transport_config.keep_alive_interval(Some(IROH_KEEP_ALIVE_INTERVAL));
123
124    let builder = builder
125        .relay_mode(relay_mode)
126        .secret_key(secret_key)
127        .alpns(vec![alpn.to_vec()])
128        .transport_config(transport_config);
129
130    let builder = match bind_addr {
131        SocketAddr::V4(addr_v4) => builder.bind_addr_v4(addr_v4),
132        SocketAddr::V6(addr_v6) => builder.bind_addr_v6(addr_v6),
133    };
134
135    let endpoint = Box::pin(builder.bind())
136        .await
137        .map_err(|e| IrohEndpointError::Bind(e.into()))?;
138
139    info!(
140        target: LOG_NET_IROH,
141        %bind_addr,
142        node_id = %endpoint.node_id(),
143        node_id_pkarr = %z32::encode(endpoint.node_id().as_bytes()),
144        "Iroh p2p server endpoint"
145    );
146
147    Ok(endpoint)
148}
149
150/// Failure to set up an iroh endpoint.
151#[derive(Debug, Error)]
152#[non_exhaustive]
153pub enum IrohEndpointError {
154    /// The endpoint could not bind its socket or reach its relays.
155    #[error("Failed to bind Iroh endpoint")]
156    Bind(#[source] Box<dyn std::error::Error + Send + Sync>),
157}
158
159fn relay_node_from_url(url: Url) -> RelayNode {
160    RelayNode {
161        url: RelayUrl::from(url),
162        stun_only: false,
163        stun_port: DEFAULT_STUN_PORT,
164        quic: Some(RelayQuicConfig::default()),
165    }
166}
167
168fn add_n0_discovery(builder: Builder) -> Builder {
169    if is_env_var_set_opt(FM_IROH_N0_DISCOVERY_ENABLE_ENV).unwrap_or(true) {
170        return add_n0_pkarr_resolver(builder.discovery_n0());
171    }
172
173    warn!(target: LOG_NET_IROH, "Iroh n0 discovery is disabled");
174    builder
175}
176
177#[cfg(not(target_family = "wasm"))]
178fn add_n0_pkarr_resolver(builder: Builder) -> Builder {
179    // Native discovery_n0 only uses DNS TXT; add HTTPS pkarr fallback.
180    if is_env_var_set_opt(FM_IROH_PKARR_RESOLVER_ENABLE_ENV).unwrap_or(true) {
181        return builder.add_discovery(|_| Some(PkarrResolver::n0_dns()));
182    }
183
184    warn!(
185        target: LOG_NET_IROH,
186        "Iroh pkarr resolver is disabled"
187    );
188    builder
189}
190
191#[cfg(target_family = "wasm")]
192fn add_n0_pkarr_resolver(builder: Builder) -> Builder {
193    builder
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn default_iroh_relays_are_valid_urls() {
202        for relay in DEFAULT_IROH_RELAYS {
203            Url::parse(relay).expect("default Iroh relay URL is valid");
204        }
205    }
206}