fedimint_core/net/
iroh.rs1use 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
26pub const IROH_IDLE_TIMEOUT: Duration = Duration::from_secs(5);
34
35pub 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 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 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#[derive(Debug, Error)]
152#[non_exhaustive]
153pub enum IrohEndpointError {
154 #[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 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}