use std::net::SocketAddr;
use std::time::Duration;
use anyhow::Context;
use fedimint_core::util::SafeUrl;
use fedimint_logging::LOG_NET_IROH;
use iroh::defaults::DEFAULT_STUN_PORT;
use iroh::discovery::pkarr::{PkarrPublisher, PkarrResolver};
use iroh::endpoint::{Builder, TransportConfig};
use iroh::{Endpoint, RelayMode, RelayNode, RelayUrl, SecretKey};
use iroh_relay::RelayQuicConfig;
use tracing::{debug, info, warn};
use url::Url;
use crate::envs::{
FM_IROH_DHT_ENABLE_ENV, FM_IROH_N0_DISCOVERY_ENABLE_ENV, FM_IROH_PKARR_PUBLISHER_ENABLE_ENV,
FM_IROH_PKARR_RESOLVER_ENABLE_ENV, FM_IROH_RELAYS_ENABLE_ENV, is_env_var_set,
is_env_var_set_opt,
};
const DEFAULT_IROH_RELAYS: [&str; 2] = [
"https://euc1-1.relay.elsirion.fedimint.iroh.link/",
"https://use1-1.relay.elsirion.fedimint.iroh.link/",
];
pub const IROH_IDLE_TIMEOUT: Duration = Duration::from_secs(60);
pub const IROH_KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(30);
pub async fn build_iroh_endpoint(
secret_key: SecretKey,
bind_addr: SocketAddr,
iroh_dns: Option<SafeUrl>,
iroh_relays: Vec<SafeUrl>,
alpn: &[u8],
) -> Result<Endpoint, anyhow::Error> {
let relay_mode = if !is_env_var_set_opt(FM_IROH_RELAYS_ENABLE_ENV).unwrap_or(true) {
warn!(
target: LOG_NET_IROH,
"Iroh relays are disabled"
);
RelayMode::Disabled
} else if iroh_relays.is_empty() {
RelayMode::Custom(
DEFAULT_IROH_RELAYS
.into_iter()
.map(|url| {
relay_node_from_url(Url::parse(url).expect("default Iroh relay URL is valid"))
})
.collect(),
)
} else {
RelayMode::Custom(
iroh_relays
.into_iter()
.map(|url| relay_node_from_url(url.to_unsafe()))
.collect(),
)
};
let mut builder = Endpoint::builder();
if let Some(iroh_dns) = iroh_dns.map(SafeUrl::to_unsafe) {
if is_env_var_set_opt(FM_IROH_PKARR_PUBLISHER_ENABLE_ENV).unwrap_or(true) {
builder = builder.add_discovery({
let iroh_dns = iroh_dns.clone();
move |sk: &SecretKey| Some(PkarrPublisher::new(sk.clone(), iroh_dns))
});
} else {
warn!(
target: LOG_NET_IROH,
"Iroh pkarr publisher is disabled"
);
}
if is_env_var_set_opt(FM_IROH_PKARR_RESOLVER_ENABLE_ENV).unwrap_or(true) {
builder = builder.add_discovery(|_| Some(PkarrResolver::new(iroh_dns)));
} else {
warn!(
target: LOG_NET_IROH,
"Iroh pkarr resolver is disabled"
);
}
}
if is_env_var_set(FM_IROH_DHT_ENABLE_ENV) {
#[cfg(not(target_family = "wasm"))]
{
debug!(
target: LOG_NET_IROH,
"Iroh DHT is enabled"
);
builder = builder.discovery_dht();
}
} else {
info!(
target: LOG_NET_IROH,
"Iroh DHT is disabled"
);
}
builder = add_n0_discovery(builder);
let mut transport_config = TransportConfig::default();
transport_config.max_idle_timeout(Some(
IROH_IDLE_TIMEOUT
.try_into()
.expect("idle timeout fits in IdleTimeout"),
));
transport_config.keep_alive_interval(Some(IROH_KEEP_ALIVE_INTERVAL));
let builder = builder
.relay_mode(relay_mode)
.secret_key(secret_key)
.alpns(vec![alpn.to_vec()])
.transport_config(transport_config);
let builder = match bind_addr {
SocketAddr::V4(addr_v4) => builder.bind_addr_v4(addr_v4),
SocketAddr::V6(addr_v6) => builder.bind_addr_v6(addr_v6),
};
let endpoint = Box::pin(builder.bind())
.await
.context("Failed to bind Iroh endpoint")?;
info!(
target: LOG_NET_IROH,
%bind_addr,
node_id = %endpoint.node_id(),
node_id_pkarr = %z32::encode(endpoint.node_id().as_bytes()),
"Iroh p2p server endpoint"
);
Ok(endpoint)
}
fn relay_node_from_url(url: Url) -> RelayNode {
RelayNode {
url: RelayUrl::from(url),
stun_only: false,
stun_port: DEFAULT_STUN_PORT,
quic: Some(RelayQuicConfig::default()),
}
}
fn add_n0_discovery(builder: Builder) -> Builder {
if is_env_var_set_opt(FM_IROH_N0_DISCOVERY_ENABLE_ENV).unwrap_or(true) {
return add_n0_pkarr_resolver(builder.discovery_n0());
}
warn!(target: LOG_NET_IROH, "Iroh n0 discovery is disabled");
builder
}
#[cfg(not(target_family = "wasm"))]
fn add_n0_pkarr_resolver(builder: Builder) -> Builder {
if is_env_var_set_opt(FM_IROH_PKARR_RESOLVER_ENABLE_ENV).unwrap_or(true) {
return builder.add_discovery(|_| Some(PkarrResolver::n0_dns()));
}
warn!(
target: LOG_NET_IROH,
"Iroh pkarr resolver is disabled"
);
builder
}
#[cfg(target_family = "wasm")]
fn add_n0_pkarr_resolver(builder: Builder) -> Builder {
builder
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_iroh_relays_are_valid_urls() {
for relay in DEFAULT_IROH_RELAYS {
Url::parse(relay).expect("default Iroh relay URL is valid");
}
}
}