1pub mod error;
2pub mod http;
3pub mod iroh;
4pub mod metrics;
5#[cfg(all(feature = "tor", not(target_family = "wasm")))]
6pub mod tor;
7pub mod ws;
8
9use std::collections::{BTreeMap, BTreeSet, HashMap};
10use std::fmt::{self, Debug};
11use std::net::SocketAddr;
12use std::pin::Pin;
13use std::sync::Arc;
14use std::time::Duration;
15
16use anyhow::{anyhow, bail};
17use async_trait::async_trait;
18use fedimint_core::envs::{FM_WS_API_CONNECT_OVERRIDES_ENV, parse_kv_list_from_env};
19use fedimint_core::module::{ApiMethod, ApiRequestErased};
20use fedimint_core::util::backoff_util::{FibonacciBackoff, custom_backoff};
21use fedimint_core::util::{FmtCompact, FmtCompactAnyhow, SafeUrl};
22use fedimint_core::{apply, async_trait_maybe_send};
23use fedimint_logging::{LOG_CLIENT_NET_API, LOG_NET};
24use fedimint_metrics::HistogramExt as _;
25use reqwest::Method;
26use serde_json::Value;
27use tokio::sync::{OnceCell, SetOnce, broadcast, watch};
28use tracing::trace;
29
30use crate::error::ServerError;
31use crate::metrics::{CONNECTION_ATTEMPTS_TOTAL, CONNECTION_DURATION_SECONDS};
32use crate::ws::WebsocketConnector;
33
34pub type ServerResult<T> = Result<T, ServerError>;
35
36type ConnectorInitFn = Arc<
38 dyn Fn() -> Pin<Box<dyn Future<Output = anyhow::Result<DynConnector>> + Send>> + Send + Sync,
39>;
40
41#[derive(Debug, Clone)]
46#[allow(clippy::struct_excessive_bools)] pub struct ConnectorRegistryBuilder {
48 connection_overrides: BTreeMap<SafeUrl, SafeUrl>,
53
54 iroh_enable: bool,
56 iroh_dns: Option<SafeUrl>,
58 iroh_pkarr_dht: bool,
60
61 ws_enable: bool,
63 ws_force_tor: bool,
64
65 http_enable: bool,
67}
68
69impl ConnectorRegistryBuilder {
70 #[allow(clippy::unused_async)] pub async fn bind(self) -> anyhow::Result<ConnectorRegistry> {
72 let mut connectors_lazy: BTreeMap<String, (ConnectorInitFn, OnceCell<DynConnector>)> =
74 BTreeMap::new();
75
76 let path_change = Arc::new(watch::channel(0u64).0);
80
81 let builder_ws = self.clone();
83 let ws_connector_init = Arc::new(move || {
84 let builder = builder_ws.clone();
85 Box::pin(async move { builder.build_ws_connector().await })
86 as Pin<Box<dyn Future<Output = anyhow::Result<DynConnector>> + Send>>
87 });
88 connectors_lazy.insert("ws".into(), (ws_connector_init.clone(), OnceCell::new()));
89 connectors_lazy.insert("wss".into(), (ws_connector_init.clone(), OnceCell::new()));
90
91 let builder_iroh = self.clone();
93 let path_change_iroh = path_change.clone();
94 connectors_lazy.insert(
95 "iroh".into(),
96 (
97 Arc::new(move || {
98 let builder = builder_iroh.clone();
99 let path_change = path_change_iroh.clone();
100 Box::pin(async move { builder.build_iroh_connector(path_change).await })
101 as Pin<Box<dyn Future<Output = anyhow::Result<DynConnector>> + Send>>
102 }),
103 OnceCell::new(),
104 ),
105 );
106
107 let builder_http = self.clone();
108 let http_connector_init = Arc::new(move || {
109 let builder = builder_http.clone();
110 Box::pin(async move { builder.build_http_connector() })
111 as Pin<Box<dyn Future<Output = anyhow::Result<DynConnector>> + Send>>
112 });
113
114 connectors_lazy.insert(
115 "http".into(),
116 (http_connector_init.clone(), OnceCell::new()),
117 );
118 connectors_lazy.insert(
119 "https".into(),
120 (http_connector_init.clone(), OnceCell::new()),
121 );
122
123 Ok(ConnectorRegistry {
124 inner: ConnectorRegistryInner {
125 connectors_lazy,
126 connection_overrides: self.connection_overrides,
127 initialized: SetOnce::new(),
128 path_change,
129 }
130 .into(),
131 })
132 }
133
134 pub async fn build_iroh_connector(
135 &self,
136 path_change: Arc<watch::Sender<u64>>,
137 ) -> anyhow::Result<DynConnector> {
138 if !self.iroh_enable {
139 bail!("Iroh connector not enabled");
140 }
141 Ok(Arc::new(
142 iroh::IrohConnector::new(self.iroh_dns.clone(), self.iroh_pkarr_dht, path_change)
143 .await?,
144 ) as DynConnector)
145 }
146
147 pub async fn build_ws_connector(&self) -> anyhow::Result<DynConnector> {
148 if !self.ws_enable {
149 bail!("Websocket connector not enabled");
150 }
151
152 match self.ws_force_tor {
153 #[cfg(all(feature = "tor", not(target_family = "wasm")))]
154 true => {
155 use crate::tor::TorConnector;
156
157 Ok(Arc::new(TorConnector::bootstrap().await?) as DynConnector)
158 }
159
160 false => Ok(Arc::new(WebsocketConnector::new()) as DynConnector),
161 #[allow(unreachable_patterns)]
162 _ => bail!("Tor requested, but not support not compiled in"),
163 }
164 }
165
166 pub fn build_http_connector(&self) -> anyhow::Result<DynConnector> {
167 if !self.http_enable {
168 bail!("Http connector not enabled");
169 }
170
171 Ok(Arc::new(crate::http::HttpConnector::default()) as DynConnector)
172 }
173
174 pub fn iroh_pkarr_dht(self, enable: bool) -> Self {
175 Self {
176 iroh_pkarr_dht: enable,
177 ..self
178 }
179 }
180
181 pub fn ws_force_tor(self, enable: bool) -> Self {
182 Self {
183 ws_force_tor: enable,
184 ..self
185 }
186 }
187
188 pub fn http(self, enable: bool) -> Self {
189 Self {
190 http_enable: enable,
191 ..self
192 }
193 }
194
195 pub fn set_iroh_dns(self, url: SafeUrl) -> Self {
196 Self {
197 iroh_dns: Some(url),
198 ..self
199 }
200 }
201
202 pub fn with_env_var_overrides(mut self) -> anyhow::Result<Self> {
204 for (k, v) in parse_kv_list_from_env::<_, SafeUrl>(FM_WS_API_CONNECT_OVERRIDES_ENV)? {
206 self = self.with_connection_override(k, v);
207 }
208
209 Ok(Self { ..self })
210 }
211
212 pub fn with_connection_override(
213 mut self,
214 original_url: SafeUrl,
215 replacement_url: SafeUrl,
216 ) -> Self {
217 self.connection_overrides
218 .insert(original_url, replacement_url);
219 self
220 }
221}
222
223struct ConnectorRegistryInner {
225 connectors_lazy: BTreeMap<String, (ConnectorInitFn, OnceCell<DynConnector>)>,
227 connection_overrides: BTreeMap<SafeUrl, SafeUrl>,
229 initialized: tokio::sync::SetOnce<()>,
234 path_change: Arc<watch::Sender<u64>>,
237}
238
239#[derive(Clone)]
252pub struct ConnectorRegistry {
253 inner: Arc<ConnectorRegistryInner>,
254}
255
256impl fmt::Debug for ConnectorRegistry {
257 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258 f.debug_struct("ConnectorRegistry")
259 .field("connectors_lazy", &self.inner.connectors_lazy.len())
260 .field("connection_overrides", &self.inner.connection_overrides)
261 .finish()
262 }
263}
264
265impl ConnectorRegistry {
266 pub fn build_from_client_defaults() -> ConnectorRegistryBuilder {
271 ConnectorRegistryBuilder {
272 iroh_enable: true,
273 iroh_dns: None,
274 iroh_pkarr_dht: false,
275 ws_enable: true,
276 ws_force_tor: false,
277 http_enable: true,
278
279 connection_overrides: BTreeMap::default(),
280 }
281 }
282
283 pub fn build_from_server_defaults() -> ConnectorRegistryBuilder {
286 ConnectorRegistryBuilder {
287 iroh_enable: true,
288 iroh_dns: None,
289 iroh_pkarr_dht: true,
290 ws_enable: true,
291 ws_force_tor: false,
292 http_enable: false,
293
294 connection_overrides: BTreeMap::default(),
295 }
296 }
297
298 pub fn build_from_testing_defaults() -> ConnectorRegistryBuilder {
301 ConnectorRegistryBuilder {
302 iroh_enable: true,
303 iroh_dns: None,
304 iroh_pkarr_dht: false,
305 ws_enable: true,
306 ws_force_tor: false,
307 http_enable: true,
308
309 connection_overrides: BTreeMap::default(),
310 }
311 }
312
313 pub fn build_from_client_env() -> anyhow::Result<ConnectorRegistryBuilder> {
316 let builder = Self::build_from_client_defaults().with_env_var_overrides()?;
317 Ok(builder)
318 }
319
320 pub fn build_from_server_env() -> anyhow::Result<ConnectorRegistryBuilder> {
323 let builder = Self::build_from_server_defaults().with_env_var_overrides()?;
324 Ok(builder)
325 }
326
327 pub fn build_from_testing_env() -> anyhow::Result<ConnectorRegistryBuilder> {
330 let builder = Self::build_from_testing_defaults().with_env_var_overrides()?;
331 Ok(builder)
332 }
333
334 pub async fn wait_for_initialized_connections(&self) {
336 self.inner.initialized.wait().await;
337 }
338
339 pub async fn connect_guardian(
344 &self,
345 url: &SafeUrl,
346 api_secret: Option<&str>,
347 ) -> ServerResult<DynGuaridianConnection> {
348 trace!(
349 target: LOG_NET,
350 %url,
351 "Connection requested to guardian"
352 );
353 let _ = self.inner.initialized.set(());
354
355 let url = match self.inner.connection_overrides.get(url) {
356 Some(replacement) => {
357 trace!(
358 target: LOG_NET,
359 original_url = %url,
360 replacement_url = %replacement,
361 "Using a connectivity override for connection"
362 );
363
364 replacement
365 }
366 None => url,
367 };
368
369 let scheme = url.scheme().to_string();
370
371 let Some(connector_lazy) = self.inner.connectors_lazy.get(&scheme) else {
372 return Err(ServerError::InvalidEndpoint(anyhow!(
373 "Unsupported scheme: {}; missing endpoint handler",
374 url.scheme()
375 )));
376 };
377
378 let init_fn = connector_lazy.0.clone();
380
381 let timer = CONNECTION_DURATION_SECONDS
382 .with_label_values(&[&scheme])
383 .start_timer_ext();
384
385 let result = connector_lazy
386 .1
387 .get_or_try_init(|| async move { init_fn().await })
388 .await
389 .map_err(|e| {
390 ServerError::Transport(anyhow!(
391 "Connector failed to initialize: {}",
392 e.fmt_compact_anyhow()
393 ))
394 })?
395 .connect_guardian(url, api_secret)
396 .await;
397
398 timer.observe_duration();
399
400 let result_label = if result.is_ok() { "success" } else { "error" }.to_string();
401 CONNECTION_ATTEMPTS_TOTAL
402 .with_label_values(&[&scheme, &result_label])
403 .inc();
404
405 let conn = result.inspect_err(|err| {
406 trace!(
407 target: LOG_NET,
408 %url,
409 err = %err.fmt_compact(),
410 "Connection failed"
411 );
412 })?;
413
414 trace!(
415 target: LOG_NET,
416 %url,
417 "Connection returned"
418 );
419 Ok(conn)
420 }
421
422 pub async fn connect_gateway(&self, url: &SafeUrl) -> anyhow::Result<DynGatewayConnection> {
427 trace!(
428 target: LOG_NET,
429 %url,
430 "Connection requested to gateway"
431 );
432 let _ = self.inner.initialized.set(());
433
434 let url = match self.inner.connection_overrides.get(url) {
435 Some(replacement) => {
436 trace!(
437 target: LOG_NET,
438 original_url = %url,
439 replacement_url = %replacement,
440 "Using a connectivity override for connection"
441 );
442
443 replacement
444 }
445 None => url,
446 };
447
448 let scheme = url.scheme().to_string();
449
450 let Some(connector_lazy) = self.inner.connectors_lazy.get(&scheme) else {
451 return Err(anyhow!(
452 "Unsupported scheme: {}; missing endpoint handler",
453 url.scheme()
454 ));
455 };
456
457 let init_fn = connector_lazy.0.clone();
459
460 let timer = CONNECTION_DURATION_SECONDS
461 .with_label_values(&[&scheme])
462 .start_timer_ext();
463
464 let result = connector_lazy
465 .1
466 .get_or_try_init(|| async move { init_fn().await })
467 .await
468 .map_err(|e| {
469 ServerError::Transport(anyhow!(
470 "Connector failed to initialize: {}",
471 e.fmt_compact_anyhow()
472 ))
473 })?
474 .connect_gateway(url)
475 .await;
476
477 timer.observe_duration();
478
479 let result_label = if result.is_ok() { "success" } else { "error" }.to_string();
480 CONNECTION_ATTEMPTS_TOTAL
481 .with_label_values(&[&scheme, &result_label])
482 .inc();
483
484 result
485 }
486
487 pub fn connectivity(&self, url: &SafeUrl) -> Connectivity {
493 let url = match self.inner.connection_overrides.get(url) {
494 Some(replacement) => replacement,
495 None => url,
496 };
497
498 let Some((_, connector_cell)) = self.inner.connectors_lazy.get(url.scheme()) else {
499 return Connectivity::Unknown;
500 };
501
502 match connector_cell.get() {
503 Some(connector) => connector.connectivity(url),
504 None => Connectivity::Unknown,
505 }
506 }
507
508 pub async fn iroh_peer_info(
511 &self,
512 url: &SafeUrl,
513 path_timeout: Duration,
514 ) -> ServerResult<Option<IrohPeerInfo>> {
515 let url = match self.inner.connection_overrides.get(url) {
516 Some(replacement) => replacement,
517 None => url,
518 };
519
520 let Some((init_fn, connector_cell)) = self.inner.connectors_lazy.get(url.scheme()) else {
521 return Ok(None);
522 };
523
524 let init_fn = init_fn.clone();
525 connector_cell
526 .get_or_try_init(|| async move { init_fn().await })
527 .await
528 .map_err(|e| {
529 ServerError::Transport(anyhow!(
530 "Connector failed to initialize: {}",
531 e.fmt_compact_anyhow()
532 ))
533 })?
534 .iroh_peer_info(url, path_timeout)
535 .await
536 }
537
538 pub fn connectivity_change_notifier(&self) -> watch::Receiver<u64> {
547 self.inner.path_change.subscribe()
548 }
549}
550pub type DynConnector = Arc<dyn Connector>;
551
552#[async_trait]
553pub trait Connector: Send + Sync + 'static + Debug {
554 async fn connect_guardian(
555 &self,
556 url: &SafeUrl,
557 api_secret: Option<&str>,
558 ) -> ServerResult<DynGuaridianConnection>;
559
560 async fn connect_gateway(&self, url: &SafeUrl) -> anyhow::Result<DynGatewayConnection>;
561
562 fn connectivity(&self, url: &SafeUrl) -> Connectivity;
564
565 async fn iroh_peer_info(
567 &self,
568 _url: &SafeUrl,
569 _path_timeout: Duration,
570 ) -> ServerResult<Option<IrohPeerInfo>> {
571 Ok(None)
572 }
573}
574
575#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
584pub enum Connectivity {
585 Direct,
586 Relay,
587 Mixed,
588 Tor,
589 Unknown,
590}
591
592#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
599pub enum PeerStatus {
600 Disconnected,
601 Connected(Connectivity),
602}
603
604#[derive(Debug, Clone, PartialEq, Eq)]
606pub struct IrohPeerInfo {
607 pub node_id: String,
608 pub connectivity: Connectivity,
609 pub direct_addr: Option<SocketAddr>,
610 pub known_direct_addrs: Vec<SocketAddr>,
611 pub relay_url: Option<String>,
612}
613
614#[apply(async_trait_maybe_send!)]
617pub trait IConnection: Debug + Send + Sync + 'static {
618 fn is_connected(&self) -> bool;
619
620 async fn await_disconnection(&self);
621}
622
623pub type DynGuaridianConnection = Arc<dyn IGuardianConnection>;
625
626#[async_trait]
628pub trait IGuardianConnection: IConnection + Debug + Send + Sync + 'static {
629 async fn request(&self, method: ApiMethod, request: ApiRequestErased) -> ServerResult<Value>;
630
631 fn into_dyn(self) -> DynGuaridianConnection
632 where
633 Self: Sized,
634 {
635 Arc::new(self)
636 }
637}
638
639pub type DynGatewayConnection = Arc<dyn IGatewayConnection>;
641
642#[apply(async_trait_maybe_send!)]
644pub trait IGatewayConnection: IConnection + Debug + Send + Sync + 'static {
645 async fn request(
646 &self,
647 password: Option<String>,
648 method: Method,
649 route: &str,
650 payload: Option<Value>,
651 ) -> ServerResult<Value>;
652
653 fn into_dyn(self) -> DynGatewayConnection
654 where
655 Self: Sized,
656 {
657 Arc::new(self)
658 }
659}
660
661#[derive(Debug)]
662pub struct ConnectionPool<T: IConnection + ?Sized> {
663 connectors: ConnectorRegistry,
665
666 active_connections: watch::Sender<BTreeSet<SafeUrl>>,
667
668 #[allow(clippy::type_complexity)]
674 connections: Arc<tokio::sync::Mutex<HashMap<SafeUrl, Arc<ConnectionState<T>>>>>,
675}
676
677impl<T: IConnection + ?Sized> Clone for ConnectionPool<T> {
678 fn clone(&self) -> Self {
679 Self {
680 connectors: self.connectors.clone(),
681 connections: self.connections.clone(),
682 active_connections: self.active_connections.clone(),
683 }
684 }
685}
686
687impl<T: IConnection + ?Sized> ConnectionPool<T> {
688 pub fn new(connectors: ConnectorRegistry) -> Self {
689 Self {
690 connectors,
691 connections: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
692 active_connections: watch::channel(BTreeSet::new()).0,
693 }
694 }
695
696 async fn get_or_init_pool_entry(&self, url: &SafeUrl) -> Arc<ConnectionState<T>> {
697 let mut pool_locked = self.connections.lock().await;
698 pool_locked
699 .entry(url.to_owned())
700 .and_modify(|entry_arc| {
701 if let Some(existing_conn) = entry_arc.connection.get()
707 && !existing_conn.is_connected()
708 {
709 trace!(
710 target: LOG_CLIENT_NET_API,
711 %url,
712 "Existing connection is disconnected, removing from pool"
713 );
714 self.active_connections.send_modify(|v| {
715 v.remove(url);
716 });
717 *entry_arc = Arc::new(ConnectionState::new_reconnecting());
718 }
719 })
720 .or_insert_with(|| Arc::new(ConnectionState::new_initial()))
721 .clone()
722 }
723
724 pub async fn get_or_create_connection<F, Fut>(
725 &self,
726 url: &SafeUrl,
727 api_secret: Option<&str>,
728 create_connection: F,
729 ) -> ServerResult<Arc<T>>
730 where
731 F: Fn(SafeUrl, Option<String>, ConnectorRegistry) -> Fut + Clone + Send + Sync + 'static,
732 Fut: Future<Output = ServerResult<Arc<T>>> + Send + 'static,
733 {
734 let pool_entry_arc = self.get_or_init_pool_entry(url).await;
735
736 let leader_tx = loop {
737 let mut leader_rx = {
738 let mut chan_locked = pool_entry_arc
739 .merge_connection_attempts_chan
740 .lock()
741 .expect("locking error");
742
743 if chan_locked.is_closed() {
744 let (leader_tx, leader_rx) = broadcast::channel(1);
745 *chan_locked = leader_rx;
746 break leader_tx;
749 }
750
751 chan_locked.resubscribe()
753 };
754
755 if let Ok(res) = leader_rx.recv().await {
756 match res {
757 Ok(o) => return Ok(o),
758 Err(err) => {
759 return Err(ServerError::Connection(anyhow::format_err!("{}", err)));
760 }
761 }
762 }
763 };
764
765 let conn = pool_entry_arc
766 .connection
767 .get_or_try_init(|| async {
768 let retry_delay = pool_entry_arc.pre_reconnect_delay();
769 fedimint_core::runtime::sleep(retry_delay).await;
770
771 trace!(target: LOG_CLIENT_NET_API, %url, "Attempting to create a new connection");
772 let res = create_connection(
773 url.clone(),
774 api_secret.map(std::string::ToString::to_string),
775 self.connectors.clone(),
776 )
777 .await;
778
779 let _ = leader_tx.send(
784 res.as_ref()
785 .map(|o| o.clone())
786 .map_err(|err| err.to_string()),
787 );
788
789 let conn = res?;
790
791 self.active_connections.send_modify(|v| {
792 v.insert(url.clone());
793 });
794
795 fedimint_core::runtime::spawn("connection disconnect watch", {
796 let conn = conn.clone();
797 let s = self.clone();
798 let url = url.clone();
799 async move {
800 conn.await_disconnection().await;
802 s.get_or_init_pool_entry(&url).await;
808 }
809 });
810
811 Ok(conn)
812 })
813 .await?;
814
815 trace!(target: LOG_CLIENT_NET_API, %url, "Connection ready");
816 Ok(conn.clone())
817 }
818 pub fn get_active_connection_receiver(&self) -> watch::Receiver<BTreeSet<SafeUrl>> {
820 self.active_connections.subscribe()
821 }
822
823 pub async fn wait_for_initialized_connections(&self) {
824 self.connectors.wait_for_initialized_connections().await
825 }
826
827 pub fn connectivity(&self, url: &SafeUrl) -> Connectivity {
829 self.connectors.connectivity(url)
830 }
831
832 pub fn connectivity_change_notifier(&self) -> watch::Receiver<u64> {
835 self.connectors.connectivity_change_notifier()
836 }
837}
838
839#[derive(Debug)]
842struct ConnectionStateInner {
843 fresh: bool,
844 backoff: FibonacciBackoff,
845}
846
847#[derive(Debug)]
848pub struct ConnectionState<T: ?Sized> {
849 pub connection: tokio::sync::OnceCell<Arc<T>>,
851
852 merge_connection_attempts_chan:
856 std::sync::Mutex<broadcast::Receiver<std::result::Result<Arc<T>, String>>>,
857
858 inner: std::sync::Mutex<ConnectionStateInner>,
862}
863
864impl<T: ?Sized> ConnectionState<T> {
865 pub fn new_initial() -> Self {
867 Self {
868 connection: OnceCell::new(),
869 inner: std::sync::Mutex::new(ConnectionStateInner {
870 fresh: true,
871 backoff: custom_backoff(
872 Duration::from_millis(5),
874 Duration::from_secs(30),
875 None,
876 ),
877 }),
878 merge_connection_attempts_chan: std::sync::Mutex::new(broadcast::channel(1).1),
879 }
880 }
881
882 pub fn new_reconnecting() -> Self {
885 Self {
886 connection: OnceCell::new(),
887 inner: std::sync::Mutex::new(ConnectionStateInner {
888 fresh: false,
890 backoff: custom_backoff(
891 Duration::from_millis(500),
893 Duration::from_secs(30),
894 None,
895 ),
896 }),
897 merge_connection_attempts_chan: std::sync::Mutex::new(broadcast::channel(1).1),
898 }
899 }
900
901 pub fn pre_reconnect_delay(&self) -> Duration {
904 let mut backoff_locked = self.inner.lock().expect("Locking failed");
905 let fresh = backoff_locked.fresh;
906
907 backoff_locked.fresh = false;
908
909 if fresh {
910 Duration::default()
911 } else {
912 backoff_locked.backoff.next().expect("Keeps retrying")
913 }
914 }
915}