Skip to main content

fedimint_core/net/
iroh.rs

1use std::net::SocketAddr;
2use std::time::Duration;
3
4use anyhow::Context;
5use fedimint_core::util::SafeUrl;
6use fedimint_logging::LOG_NET_IROH;
7use iroh::defaults::DEFAULT_STUN_PORT;
8use iroh::discovery::pkarr::{PkarrPublisher, PkarrResolver};
9use iroh::endpoint::{Builder, TransportConfig};
10use iroh::{Endpoint, RelayMode, RelayNode, RelayUrl, SecretKey};
11use iroh_relay::RelayQuicConfig;
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 used for iroh API endpoints.
27pub const IROH_IDLE_TIMEOUT: Duration = Duration::from_secs(60);
28
29/// QUIC keep-alive interval used for iroh API endpoints.
30pub const IROH_KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(30);
31
32pub async fn build_iroh_endpoint(
33    secret_key: SecretKey,
34    bind_addr: SocketAddr,
35    iroh_dns: Option<SafeUrl>,
36    iroh_relays: Vec<SafeUrl>,
37    alpn: &[u8],
38) -> Result<Endpoint, anyhow::Error> {
39    let relay_mode = if !is_env_var_set_opt(FM_IROH_RELAYS_ENABLE_ENV).unwrap_or(true) {
40        warn!(
41            target: LOG_NET_IROH,
42            "Iroh relays are disabled"
43        );
44        RelayMode::Disabled
45    } else if iroh_relays.is_empty() {
46        RelayMode::Custom(
47            DEFAULT_IROH_RELAYS
48                .into_iter()
49                .map(|url| {
50                    relay_node_from_url(Url::parse(url).expect("default Iroh relay URL is valid"))
51                })
52                .collect(),
53        )
54    } else {
55        RelayMode::Custom(
56            iroh_relays
57                .into_iter()
58                .map(|url| relay_node_from_url(url.to_unsafe()))
59                .collect(),
60        )
61    };
62
63    let mut builder = Endpoint::builder();
64
65    if let Some(iroh_dns) = iroh_dns.map(SafeUrl::to_unsafe) {
66        if is_env_var_set_opt(FM_IROH_PKARR_PUBLISHER_ENABLE_ENV).unwrap_or(true) {
67            builder = builder.add_discovery({
68                let iroh_dns = iroh_dns.clone();
69                move |sk: &SecretKey| Some(PkarrPublisher::new(sk.clone(), iroh_dns))
70            });
71        } else {
72            warn!(
73                target: LOG_NET_IROH,
74                "Iroh pkarr publisher is disabled"
75            );
76        }
77
78        if is_env_var_set_opt(FM_IROH_PKARR_RESOLVER_ENABLE_ENV).unwrap_or(true) {
79            builder = builder.add_discovery(|_| Some(PkarrResolver::new(iroh_dns)));
80        } else {
81            warn!(
82                target: LOG_NET_IROH,
83                "Iroh pkarr resolver is disabled"
84            );
85        }
86    }
87
88    // See <https://github.com/fedimint/fedimint/issues/7811>
89    if is_env_var_set(FM_IROH_DHT_ENABLE_ENV) {
90        #[cfg(not(target_family = "wasm"))]
91        {
92            debug!(
93                target: LOG_NET_IROH,
94                "Iroh DHT is enabled"
95            );
96            builder = builder.discovery_dht();
97        }
98    } else {
99        info!(
100            target: LOG_NET_IROH,
101            "Iroh DHT is disabled"
102        );
103    }
104
105    builder = add_n0_discovery(builder);
106
107    let mut transport_config = TransportConfig::default();
108    transport_config.max_idle_timeout(Some(
109        IROH_IDLE_TIMEOUT
110            .try_into()
111            .expect("idle timeout fits in IdleTimeout"),
112    ));
113    // Iroh's default builder sets keep_alive_interval to 1s, but since we're
114    // providing a custom TransportConfig we need to set it explicitly.
115    transport_config.keep_alive_interval(Some(IROH_KEEP_ALIVE_INTERVAL));
116
117    let builder = builder
118        .relay_mode(relay_mode)
119        .secret_key(secret_key)
120        .alpns(vec![alpn.to_vec()])
121        .transport_config(transport_config);
122
123    let builder = match bind_addr {
124        SocketAddr::V4(addr_v4) => builder.bind_addr_v4(addr_v4),
125        SocketAddr::V6(addr_v6) => builder.bind_addr_v6(addr_v6),
126    };
127
128    let endpoint = Box::pin(builder.bind())
129        .await
130        .context("Failed to bind Iroh endpoint")?;
131
132    info!(
133        target: LOG_NET_IROH,
134        %bind_addr,
135        node_id = %endpoint.node_id(),
136        node_id_pkarr = %z32::encode(endpoint.node_id().as_bytes()),
137        "Iroh p2p server endpoint"
138    );
139
140    Ok(endpoint)
141}
142
143fn relay_node_from_url(url: Url) -> RelayNode {
144    RelayNode {
145        url: RelayUrl::from(url),
146        stun_only: false,
147        stun_port: DEFAULT_STUN_PORT,
148        quic: Some(RelayQuicConfig::default()),
149    }
150}
151
152fn add_n0_discovery(builder: Builder) -> Builder {
153    if is_env_var_set_opt(FM_IROH_N0_DISCOVERY_ENABLE_ENV).unwrap_or(true) {
154        return add_n0_pkarr_resolver(builder.discovery_n0());
155    }
156
157    warn!(target: LOG_NET_IROH, "Iroh n0 discovery is disabled");
158    builder
159}
160
161#[cfg(not(target_family = "wasm"))]
162fn add_n0_pkarr_resolver(builder: Builder) -> Builder {
163    // Native discovery_n0 only uses DNS TXT; add HTTPS pkarr fallback.
164    if is_env_var_set_opt(FM_IROH_PKARR_RESOLVER_ENABLE_ENV).unwrap_or(true) {
165        return builder.add_discovery(|_| Some(PkarrResolver::n0_dns()));
166    }
167
168    warn!(
169        target: LOG_NET_IROH,
170        "Iroh pkarr resolver is disabled"
171    );
172    builder
173}
174
175#[cfg(target_family = "wasm")]
176fn add_n0_pkarr_resolver(builder: Builder) -> Builder {
177    builder
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn default_iroh_relays_are_valid_urls() {
186        for relay in DEFAULT_IROH_RELAYS {
187            Url::parse(relay).expect("default Iroh relay URL is valid");
188        }
189    }
190}