fedimint_core/net/
iroh.rs1use 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
26pub const IROH_IDLE_TIMEOUT: Duration = Duration::from_secs(60);
28
29pub 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 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 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 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}