Skip to main content

hopr_utils_session/
lib.rs

1//! Session-related utilities for HOPR
2//!
3//! This module provides utility functions and structures for managing sessions,
4//! including session lifecycle management, session data handling, and common
5//! session operations.
6
7use std::{
8    collections::VecDeque, fmt::Formatter, future::Future, hash::Hash, net::SocketAddr, num::NonZeroUsize,
9    str::FromStr, sync::Arc,
10};
11
12use anyhow::anyhow;
13use base64::Engine;
14use bytesize::ByteSize;
15use dashmap::DashMap;
16use futures::{
17    FutureExt, StreamExt, TryStreamExt,
18    future::{AbortHandle, AbortRegistration},
19};
20use hopr_api::{
21    chain::HoprChainApi,
22    graph::{
23        NetworkGraphTraverse, NetworkGraphUpdate, NetworkGraphView, NetworkGraphWrite,
24        traits::{EdgeObservableRead, EdgeObservableWrite},
25    },
26    network::NetworkStreamControl,
27};
28#[cfg(not(feature = "explicit-path"))]
29use hopr_lib::HopRouting;
30#[cfg(feature = "explicit-path")]
31use hopr_lib::HoprSessionClientExplicitPathConfig;
32#[cfg(feature = "explicit-path")]
33use hopr_lib::api::types::internal::routing::RoutingOptions;
34use hopr_lib::{
35    Hopr, HoprSessionClientConfig,
36    api::{network::NetworkView, node::HoprSessionClientOperations, types::primitive::prelude::Address},
37    errors::HoprLibError,
38    exports::transport::{
39        HoprSession, HoprSessionConfigurator, OffchainPublicKey, SURB_SIZE, ServiceId, SessionId, SessionTarget,
40        transfer_session,
41    },
42};
43use hopr_utils::{
44    network_types::{
45        prelude::{ConnectedUdpStream, IpOrHost, IpProtocol, SealedHost, UdpStreamParallelism},
46        udp::ForeignDataMode,
47    },
48    runtime::Abortable,
49};
50use human_bandwidth::re::bandwidth::Bandwidth;
51use serde::{Deserialize, Serialize};
52use serde_with::serde_as;
53use tokio::net::TcpListener;
54use tracing::{debug, error, info};
55
56/// Size of the buffer for forwarding data to/from a TCP stream.
57pub const HOPR_TCP_BUFFER_SIZE: usize = 4096;
58
59/// Size of the buffer for forwarding data to/from a UDP stream.
60pub const HOPR_UDP_BUFFER_SIZE: usize = 16384;
61
62/// Size of the queue (back-pressure) for data incoming from a UDP stream.
63pub const HOPR_UDP_QUEUE_SIZE: usize = 8192;
64
65#[cfg(all(feature = "telemetry", not(test)))]
66lazy_static::lazy_static! {
67    static ref METRIC_ACTIVE_CLIENTS: hopr_api::types::telemetry::MultiGauge = hopr_api::types::telemetry::MultiGauge::new(
68        "hopr_session_hoprd_clients",
69        "Number of clients connected at this Entry node",
70        &["type"]
71    ).unwrap();
72}
73
74#[cfg(feature = "explicit-path")]
75/// Temporary compatibility alias while stored listener metadata is shared between
76/// hop-count and explicit-path session APIs.
77pub type Routing = RoutingOptions;
78
79#[cfg(not(feature = "explicit-path"))]
80pub type Routing = HopRouting;
81
82#[serde_as]
83#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
84/// Session target specification.
85pub enum SessionTargetSpec {
86    Plain(String),
87    Sealed(#[serde_as(as = "serde_with::base64::Base64")] Vec<u8>),
88    Service(ServiceId),
89}
90
91impl std::fmt::Display for SessionTargetSpec {
92    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
93        match self {
94            SessionTargetSpec::Plain(t) => write!(f, "{t}"),
95            SessionTargetSpec::Sealed(t) => write!(f, "$${}", base64::prelude::BASE64_URL_SAFE.encode(t)),
96            SessionTargetSpec::Service(t) => write!(f, "#{t}"),
97        }
98    }
99}
100
101impl FromStr for SessionTargetSpec {
102    type Err = HoprLibError;
103
104    fn from_str(s: &str) -> Result<Self, Self::Err> {
105        Ok(if let Some(stripped) = s.strip_prefix("$$") {
106            Self::Sealed(
107                base64::prelude::BASE64_URL_SAFE
108                    .decode(stripped)
109                    .map_err(|e| HoprLibError::Other(e.into()))?,
110            )
111        } else if let Some(stripped) = s.strip_prefix("#") {
112            Self::Service(
113                stripped
114                    .parse()
115                    .map_err(|_| HoprLibError::GeneralError("cannot parse service id".into()))?,
116            )
117        } else {
118            Self::Plain(s.to_owned())
119        })
120    }
121}
122
123impl SessionTargetSpec {
124    pub fn into_target(self, protocol: IpProtocol) -> Result<SessionTarget, HoprLibError> {
125        Ok(match (protocol, self) {
126            (IpProtocol::TCP, SessionTargetSpec::Plain(plain)) => {
127                SessionTarget::TcpStream(IpOrHost::from_str(&plain).map(SealedHost::from)?)
128            }
129            (IpProtocol::UDP, SessionTargetSpec::Plain(plain)) => {
130                SessionTarget::UdpStream(IpOrHost::from_str(&plain).map(SealedHost::from)?)
131            }
132            (IpProtocol::TCP, SessionTargetSpec::Sealed(enc)) => {
133                SessionTarget::TcpStream(SealedHost::Sealed(enc.into_boxed_slice()))
134            }
135            (IpProtocol::UDP, SessionTargetSpec::Sealed(enc)) => {
136                SessionTarget::UdpStream(SealedHost::Sealed(enc.into_boxed_slice()))
137            }
138            (_, SessionTargetSpec::Service(id)) => SessionTarget::ExitNode(id),
139        })
140    }
141}
142
143/// A single client connected to a session listener.
144#[derive(Debug)]
145pub struct ClientEntry {
146    /// The socket address of the connected client.
147    pub sock_addr: SocketAddr,
148    /// The abort handle for the client's session processing task.
149    pub abort_handle: AbortHandle,
150    /// The per-session configurator.
151    pub configurator: HoprSessionConfigurator,
152}
153
154/// Entry stored in the session registry table.
155#[derive(Debug)]
156pub struct StoredSessionEntry {
157    /// Destination address of the Session counterparty.
158    pub destination: Address,
159    /// Target of the Session.
160    pub target: SessionTargetSpec,
161    /// Forward routing options used for the Session.
162    pub forward_path: Routing,
163    /// Return routing options used for the Session.
164    pub return_path: Routing,
165    /// The maximum number of client sessions that the listener can spawn.
166    pub max_client_sessions: usize,
167    /// The maximum number of SURB packets that can be sent upstream.
168    pub max_surb_upstream: Option<human_bandwidth::re::bandwidth::Bandwidth>,
169    /// The amount of response data the Session counterparty can deliver back to us, without us
170    /// having to request it.
171    pub response_buffer: Option<bytesize::ByteSize>,
172    /// How many Sessions to pool for clients.
173    pub session_pool: Option<usize>,
174    /// The abort handle for the Session processing.
175    pub abort_handle: AbortHandle,
176
177    clients: Arc<DashMap<SessionId, ClientEntry>>,
178}
179
180impl StoredSessionEntry {
181    pub fn get_clients(&self) -> &Arc<DashMap<SessionId, ClientEntry>> {
182        &self.clients
183    }
184}
185
186/// This function first tries to parse `requested` as the `ip:port` host pair.
187/// If that does not work, it tries to parse `requested` as a single IP address
188/// and as a `:` prefixed port number. Whichever of those fails, is replaced by the corresponding
189/// part from the given `default`.
190pub fn build_binding_host(requested: Option<&str>, default: std::net::SocketAddr) -> std::net::SocketAddr {
191    match requested.map(|r| std::net::SocketAddr::from_str(r).map_err(|_| r)) {
192        Some(Err(requested)) => {
193            // If the requested host is not parseable as a whole as `SocketAddr`, try only its parts
194            debug!(requested, %default, "using partially default listen host");
195            std::net::SocketAddr::new(
196                requested.parse().unwrap_or(default.ip()),
197                requested
198                    .strip_prefix(":")
199                    .and_then(|p| u16::from_str(p).ok())
200                    .unwrap_or(default.port()),
201            )
202        }
203        Some(Ok(requested)) => {
204            debug!(%requested, "using requested listen host");
205            requested
206        }
207        None => {
208            debug!(%default, "using default listen host");
209            default
210        }
211    }
212}
213
214#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
215pub struct ListenerId(pub IpProtocol, pub std::net::SocketAddr);
216
217impl std::fmt::Display for ListenerId {
218    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219        write!(f, "{}://{}:{}", self.0, self.1.ip(), self.1.port())
220    }
221}
222
223#[derive(Default)]
224pub struct ListenerJoinHandles(pub DashMap<ListenerId, StoredSessionEntry>);
225
226impl ListenerJoinHandles {
227    /// Finds the [`HoprSessionConfigurator`] for the given session ID across all listeners.
228    pub fn find_configurator(&self, session_id: &SessionId) -> Option<HoprSessionConfigurator> {
229        self.0.iter().find_map(|entry| {
230            entry
231                .value()
232                .get_clients()
233                .get(session_id)
234                .map(|client| client.value().configurator.clone())
235        })
236    }
237
238    /// Returns all active session configurators for the given bound TCP host.
239    /// Intended for callers that only know the local TCP port they bound.
240    pub fn configurators_for(&self, bound_host: std::net::SocketAddr) -> Vec<HoprSessionConfigurator> {
241        self.0
242            .get(&ListenerId(IpProtocol::TCP, bound_host))
243            .map(|entry| {
244                entry
245                    .get_clients()
246                    .iter()
247                    .map(|client| client.value().configurator.clone())
248                    .collect()
249            })
250            .unwrap_or_default()
251    }
252}
253
254impl Abortable for ListenerJoinHandles {
255    fn abort_task(&self) {
256        self.0.alter_all(|_, v| {
257            v.abort_handle.abort();
258            v
259        });
260    }
261
262    fn was_aborted(&self) -> bool {
263        self.0.iter().all(|v| v.abort_handle.is_aborted())
264    }
265}
266
267// ---------------------------------------------------------------------------
268// Generic SessionFactory adapters
269// ---------------------------------------------------------------------------
270
271#[async_trait::async_trait]
272pub trait SessionFactory: Clone + Send + Sync + 'static {
273    type Cfg: Clone + Send + 'static;
274
275    /// Creates a new Session with the given destination, target and configuration.
276    async fn create_session(
277        &self,
278        dest: Address,
279        target: SessionTarget,
280        cfg: Self::Cfg,
281    ) -> Result<(HoprSession, HoprSessionConfigurator), anyhow::Error>;
282
283    /// Derives the forward and return routing options from the given configuration.
284    fn routing_from_cfg(&self, cfg: &Self::Cfg) -> Result<(Routing, Routing), anyhow::Error>;
285
286    /// Derives the listener limits (max SURB upstream and response buffer) from the given configuration.
287    fn listener_limits(&self, cfg: &Self::Cfg)
288    -> (Option<human_bandwidth::re::bandwidth::Bandwidth>, Option<ByteSize>);
289
290    /// Returns the idle timeout duration for sessions created by this factory, if any.
291    fn session_idle_timeout(&self) -> Option<std::time::Duration>;
292}
293
294pub struct HopSessionFactory<Chain, Graph, Net, TMgr> {
295    hopr: Arc<Hopr<Chain, Graph, Net, TMgr>>,
296}
297
298impl<Chain, Graph, Net, TMgr> HopSessionFactory<Chain, Graph, Net, TMgr> {
299    pub fn new(hopr: Arc<Hopr<Chain, Graph, Net, TMgr>>) -> Self {
300        Self { hopr }
301    }
302}
303
304impl<Chain, Graph, Net, TMgr> Clone for HopSessionFactory<Chain, Graph, Net, TMgr> {
305    fn clone(&self) -> Self {
306        Self {
307            hopr: self.hopr.clone(),
308        }
309    }
310}
311
312#[async_trait::async_trait]
313impl<Chain, Graph, Net, TMgr> SessionFactory for HopSessionFactory<Chain, Graph, Net, TMgr>
314where
315    Chain: HoprChainApi + Clone + Send + Sync + 'static,
316    Graph: NetworkGraphView<NodeId = OffchainPublicKey>
317        + NetworkGraphUpdate
318        + NetworkGraphWrite<NodeId = OffchainPublicKey>
319        + NetworkGraphTraverse<NodeId = OffchainPublicKey>
320        + Clone
321        + Send
322        + Sync
323        + 'static,
324    <Graph as NetworkGraphTraverse>::Observed: EdgeObservableRead + Send + 'static,
325    <Graph as NetworkGraphWrite>::Observed: EdgeObservableWrite + Send,
326    Net: NetworkView + NetworkStreamControl + Send + Sync + Clone + 'static,
327    TMgr: Send + Sync + 'static,
328{
329    type Cfg = HoprSessionClientConfig;
330
331    async fn create_session(
332        &self,
333        dest: Address,
334        target: SessionTarget,
335        cfg: Self::Cfg,
336    ) -> Result<(HoprSession, HoprSessionConfigurator), anyhow::Error> {
337        Ok(HoprSessionClientOperations::connect_to(self.hopr.as_ref(), dest, target, cfg).await?)
338    }
339
340    fn routing_from_cfg(&self, cfg: &Self::Cfg) -> Result<(Routing, Routing), anyhow::Error> {
341        // `.into()` is a no-op when explicit-path is off (Routing = HopRouting) but
342        // required when explicit-path is on (Routing = RoutingOptions).
343        #[allow(clippy::useless_conversion)]
344        Ok((cfg.forward_path.into(), cfg.return_path.into()))
345    }
346
347    fn listener_limits(
348        &self,
349        cfg: &Self::Cfg,
350    ) -> (Option<human_bandwidth::re::bandwidth::Bandwidth>, Option<ByteSize>) {
351        (
352            cfg.surb_management
353                .map(|v| Bandwidth::from_bps(v.max_surbs_per_sec * SURB_SIZE as u64)),
354            cfg.surb_management
355                .map(|v| ByteSize::b(v.target_surb_buffer_size * SURB_SIZE as u64)),
356        )
357    }
358
359    fn session_idle_timeout(&self) -> Option<std::time::Duration> {
360        Some(self.hopr.config().protocol.session.idle_timeout)
361    }
362}
363
364#[cfg(feature = "explicit-path")]
365pub struct ExplicitPathSessionFactory<Chain, Graph, Net, TMgr> {
366    hopr: Arc<Hopr<Chain, Graph, Net, TMgr>>,
367}
368
369#[cfg(feature = "explicit-path")]
370impl<Chain, Graph, Net, TMgr> ExplicitPathSessionFactory<Chain, Graph, Net, TMgr> {
371    pub fn new(hopr: Arc<Hopr<Chain, Graph, Net, TMgr>>) -> Self {
372        Self { hopr }
373    }
374}
375
376#[cfg(feature = "explicit-path")]
377impl<Chain, Graph, Net, TMgr> Clone for ExplicitPathSessionFactory<Chain, Graph, Net, TMgr> {
378    fn clone(&self) -> Self {
379        Self {
380            hopr: self.hopr.clone(),
381        }
382    }
383}
384
385#[cfg(feature = "explicit-path")]
386#[async_trait::async_trait]
387impl<Chain, Graph, Net, TMgr> SessionFactory for ExplicitPathSessionFactory<Chain, Graph, Net, TMgr>
388where
389    Chain: HoprChainApi + Clone + Send + Sync + 'static,
390    Graph: NetworkGraphView<NodeId = OffchainPublicKey>
391        + NetworkGraphUpdate
392        + NetworkGraphWrite<NodeId = OffchainPublicKey>
393        + NetworkGraphTraverse<NodeId = OffchainPublicKey>
394        + Clone
395        + Send
396        + Sync
397        + 'static,
398    <Graph as NetworkGraphTraverse>::Observed: EdgeObservableRead + Send + 'static,
399    <Graph as NetworkGraphWrite>::Observed: EdgeObservableWrite + Send,
400    Net: NetworkView + NetworkStreamControl + Send + Sync + Clone + 'static,
401    TMgr: Send + Sync + 'static,
402{
403    type Cfg = HoprSessionClientExplicitPathConfig;
404
405    async fn create_session(
406        &self,
407        dest: Address,
408        target: SessionTarget,
409        cfg: Self::Cfg,
410    ) -> Result<(HoprSession, HoprSessionConfigurator), anyhow::Error> {
411        Ok(self.hopr.connect_to_using_explicit_path(dest, target, cfg).await?)
412    }
413
414    fn routing_from_cfg(&self, cfg: &Self::Cfg) -> Result<(Routing, Routing), anyhow::Error> {
415        let forward_path = RoutingOptions::IntermediatePath(
416            cfg.forward_path
417                .clone()
418                .try_into()
419                .map_err(|e| anyhow!("invalid explicit forward path: {e}"))?,
420        );
421        let return_path = RoutingOptions::IntermediatePath(
422            cfg.return_path
423                .clone()
424                .try_into()
425                .map_err(|e| anyhow!("invalid explicit return path: {e}"))?,
426        );
427        Ok((forward_path, return_path))
428    }
429
430    fn listener_limits(
431        &self,
432        cfg: &Self::Cfg,
433    ) -> (Option<human_bandwidth::re::bandwidth::Bandwidth>, Option<ByteSize>) {
434        (
435            cfg.surb_management
436                .map(|v| Bandwidth::from_bps(v.max_surbs_per_sec * SURB_SIZE as u64)),
437            cfg.surb_management
438                .map(|v| ByteSize::b(v.target_surb_buffer_size * SURB_SIZE as u64)),
439        )
440    }
441
442    fn session_idle_timeout(&self) -> Option<std::time::Duration> {
443        Some(self.hopr.config().protocol.session.idle_timeout)
444    }
445}
446
447type SessionPoolInner = Arc<parking_lot::Mutex<VecDeque<(HoprSession, HoprSessionConfigurator)>>>;
448
449pub struct SessionPool {
450    pool: Option<SessionPoolInner>,
451    ah: Option<AbortHandle>,
452}
453
454impl SessionPool {
455    pub const MAX_SESSION_POOL_SIZE: usize = 5;
456
457    pub async fn new<T: SessionFactory>(
458        size: usize,
459        dst: Address,
460        target: SessionTarget,
461        cfg: T::Cfg,
462        factory: T,
463    ) -> Result<Self, anyhow::Error> {
464        let pool = Arc::new(parking_lot::Mutex::new(VecDeque::with_capacity(size)));
465        let factory_clone = factory.clone();
466        let pool_clone = pool.clone();
467        futures::stream::iter(0..size.min(Self::MAX_SESSION_POOL_SIZE))
468            .map(Ok)
469            .try_for_each_concurrent(Self::MAX_SESSION_POOL_SIZE, move |i| {
470                let pool = pool_clone.clone();
471                let factory = factory_clone.clone();
472                let target = target.clone();
473                let cfg = cfg.clone();
474                async move {
475                    match factory.create_session(dst, target.clone(), cfg.clone()).await {
476                        Ok((session, configurator)) => {
477                            debug!(session_id = %session.id(), num_session = i, "created a new session in pool");
478                            pool.lock().push_back((session, configurator));
479                            Ok(())
480                        }
481                        Err(error) => {
482                            error!(%error, num_session = i, "failed to establish session for pool");
483                            Err(anyhow!("failed to establish session #{i} in pool to {dst}: {error}"))
484                        }
485                    }
486                }
487            })
488            .await?;
489
490        if let Some(timeout) = factory.session_idle_timeout().filter(|_| !pool.lock().is_empty()) {
491            let pool_clone_1 = pool.clone();
492            let pool_clone_2 = pool.clone();
493            Ok(Self {
494                pool: Some(pool),
495                ah: Some(hopr_utils::spawn_as_abortable!(
496                    futures_time::stream::interval(futures_time::time::Duration::from(
497                        std::time::Duration::from_secs(1).max(timeout / 2)
498                    ))
499                    .take_while(move |_| futures::future::ready(!pool_clone_1.lock().is_empty()))
500                    .for_each(move |_| {
501                        let pool = pool_clone_2.clone();
502                        async move {
503                            let configurators: Vec<_> = pool.lock().iter().map(|(_, cfg)| cfg.clone()).collect();
504                            let mut dead_ids = Vec::new();
505                            for configurator in &configurators {
506                                if let Err(error) = configurator.ping().await {
507                                    let id = *configurator.id();
508                                    error!(%error, session_id = %id, "session in pool is not alive, will remove");
509                                    dead_ids.push(id);
510                                }
511                            }
512                            if !dead_ids.is_empty() {
513                                pool.lock().retain(|(_, cfg)| !dead_ids.contains(cfg.id()));
514                            }
515                        }
516                    })
517                )),
518            })
519        } else {
520            Ok(Self { pool: None, ah: None })
521        }
522    }
523
524    pub fn pop(&mut self) -> Option<(HoprSession, HoprSessionConfigurator)> {
525        self.pool.as_ref().and_then(|pool| pool.lock().pop_front())
526    }
527}
528
529impl Drop for SessionPool {
530    fn drop(&mut self) {
531        if let Some(ah) = self.ah.take() {
532            ah.abort();
533        }
534    }
535}
536
537#[allow(clippy::too_many_arguments)]
538pub async fn create_tcp_client_binding<T: SessionFactory>(
539    bind_host: std::net::SocketAddr,
540    port_range: Option<String>,
541    factory: T,
542    open_listeners: Arc<ListenerJoinHandles>,
543    destination: Address,
544    target_spec: SessionTargetSpec,
545    config: T::Cfg,
546    use_session_pool: Option<usize>,
547    max_client_sessions: Option<usize>,
548) -> Result<(std::net::SocketAddr, Option<SessionId>, usize), BindError> {
549    // Bind the TCP socket first
550    let (bound_host, tcp_listener) = tcp_listen_on(bind_host, port_range).await.map_err(|e| {
551        if e.kind() == std::io::ErrorKind::AddrInUse {
552            BindError::ListenHostAlreadyUsed
553        } else {
554            BindError::UnknownFailure(format!("failed to start TCP listener on {bind_host}: {e}"))
555        }
556    })?;
557    info!(%bound_host, "TCP session listener bound");
558
559    // For each new TCP connection coming to the listener,
560    // open a Session with the same parameters
561    let target = target_spec
562        .clone()
563        .into_target(IpProtocol::TCP)
564        .map_err(|e| BindError::UnknownFailure(e.to_string()))?;
565    let (forward_path, return_path) = factory
566        .routing_from_cfg(&config)
567        .map_err(|e| BindError::UnknownFailure(e.to_string()))?;
568    let (max_surb_upstream, response_buffer) = factory.listener_limits(&config);
569
570    // Create a session pool if requested
571    let session_pool_size = use_session_pool.unwrap_or(0);
572    let mut session_pool = SessionPool::new(
573        session_pool_size,
574        destination,
575        target.clone(),
576        config.clone(),
577        factory.clone(),
578    )
579    .await
580    .map_err(|e| BindError::UnknownFailure(e.to_string()))?;
581
582    let active_sessions = Arc::new(DashMap::new());
583    let mut max_clients = max_client_sessions.unwrap_or(5).max(1);
584
585    if max_clients < session_pool_size {
586        max_clients = session_pool_size;
587    }
588
589    let config_clone = config.clone();
590    // Create an abort handler for the listener
591    let (abort_handle, abort_reg) = AbortHandle::new_pair();
592    let active_sessions_clone = active_sessions.clone();
593    hopr_utils::runtime::prelude::spawn(async move {
594        let active_sessions_clone_2 = active_sessions_clone.clone();
595
596        hopr_utils::runtime::DropAbortable::new_with_registration(
597            tokio_stream::wrappers::TcpListenerStream::new(tcp_listener),
598            abort_reg,
599        )
600        .and_then(|sock| async { Ok((sock.peer_addr()?, sock)) })
601        .for_each(move |accepted_client| {
602            let data = config_clone.clone();
603            let target = target.clone();
604            let factory = factory.clone();
605            let active_sessions = active_sessions_clone_2.clone();
606            let has_capacity = accepted_client.is_ok() && active_sessions.len() < max_clients;
607            let maybe_pooled = has_capacity.then(|| session_pool.pop()).flatten();
608
609            async move {
610                match accepted_client {
611                    Ok((sock_addr, mut stream)) => {
612                        debug!(?sock_addr, "incoming TCP connection");
613
614                        // Check that we are still within the quota,
615                        // otherwise shutdown the new client immediately
616                        if active_sessions.len() >= max_clients {
617                            error!(?bind_host, "no more client slots available at listener");
618                            use tokio::io::AsyncWriteExt;
619                            if let Err(error) = stream.shutdown().await {
620                                error!(%error, ?sock_addr, "failed to shutdown TCP connection");
621                            }
622                            return;
623                        }
624
625                        // See if we still have some session pooled
626                        let (session, configurator) = match maybe_pooled {
627                            Some((s, c)) => {
628                                debug!(session_id = %s.id(), "using pooled session");
629                                (s, c)
630                            }
631                            None => {
632                                debug!("no more active sessions in the pool, creating a new one");
633                                match factory.create_session(destination, target, data).await {
634                                    Ok((s, c)) => (s, c),
635                                    Err(error) => {
636                                        error!(%error, "failed to establish session");
637                                        return;
638                                    }
639                                }
640                            }
641                        };
642
643                        let session_id = *session.id();
644                        debug!(?sock_addr, %session_id, "new session for incoming TCP connection");
645
646                        let (abort_handle, abort_reg) = AbortHandle::new_pair();
647                        active_sessions.insert(
648                            session_id,
649                            ClientEntry {
650                                sock_addr,
651                                abort_handle,
652                                configurator,
653                            },
654                        );
655
656                        #[cfg(all(feature = "telemetry", not(test)))]
657                        METRIC_ACTIVE_CLIENTS.increment(&["tcp"], 1.0);
658
659                        hopr_utils::runtime::prelude::spawn(
660                            // The stream either terminates naturally (by the client closing the TCP connection)
661                            // or is terminated via the abort handle.
662                            bind_session_to_stream(session, stream, HOPR_TCP_BUFFER_SIZE, Some(abort_reg)).then(
663                                move |_| async move {
664                                    // Regardless how the session ended, remove the abort handle
665                                    // from the map
666                                    active_sessions.remove(&session_id);
667
668                                    debug!(%session_id, "tcp session has ended");
669
670                                    #[cfg(all(feature = "telemetry", not(test)))]
671                                    METRIC_ACTIVE_CLIENTS.decrement(&["tcp"], 1.0);
672                                },
673                            ),
674                        );
675                    }
676                    Err(error) => error!(%error, "failed to accept connection"),
677                }
678            }
679        })
680        .await;
681
682        // Once the listener is done, abort all active sessions created by the listener
683        active_sessions_clone.iter().for_each(|entry| {
684            let client = entry.value();
685            debug!(session_id = %entry.key(), sock_addr = ?client.sock_addr, "aborting opened TCP session after listener has been closed");
686            client.abort_handle.abort()
687        });
688    });
689
690    open_listeners.0.insert(
691        ListenerId(hopr_utils::network_types::types::IpProtocol::TCP, bound_host),
692        StoredSessionEntry {
693            destination,
694            target: target_spec,
695            forward_path,
696            return_path,
697            clients: active_sessions,
698            max_client_sessions: max_clients,
699            max_surb_upstream,
700            response_buffer,
701            session_pool: Some(session_pool_size),
702            abort_handle,
703        },
704    );
705    Ok((bound_host, None, max_clients))
706}
707
708#[derive(Debug, thiserror::Error)]
709pub enum BindError {
710    #[error("conflict detected: listen host already in use")]
711    ListenHostAlreadyUsed,
712
713    #[error("unknown failure: {0}")]
714    UnknownFailure(String),
715}
716
717pub async fn create_udp_client_binding<T: SessionFactory>(
718    bind_host: std::net::SocketAddr,
719    port_range: Option<String>,
720    factory: T,
721    open_listeners: Arc<ListenerJoinHandles>,
722    destination: Address,
723    target_spec: SessionTargetSpec,
724    config: T::Cfg,
725) -> Result<(std::net::SocketAddr, Option<SessionId>, usize), BindError> {
726    // Bind the UDP socket first
727    let (bound_host, udp_socket) = udp_bind_to(bind_host, port_range).await.map_err(|e| {
728        if e.kind() == std::io::ErrorKind::AddrInUse {
729            BindError::ListenHostAlreadyUsed
730        } else {
731            BindError::UnknownFailure(format!("failed to start UDP listener on {bind_host}: {e}"))
732        }
733    })?;
734
735    info!(%bound_host, "UDP session listener bound");
736
737    let target = target_spec
738        .clone()
739        .into_target(IpProtocol::UDP)
740        .map_err(|e| BindError::UnknownFailure(e.to_string()))?;
741    let (forward_path, return_path) = factory
742        .routing_from_cfg(&config)
743        .map_err(|e| BindError::UnknownFailure(e.to_string()))?;
744    let (max_surb_upstream, response_buffer) = factory.listener_limits(&config);
745
746    // Create a single session for the UDP socket
747    let (session, configurator) = factory
748        .create_session(destination, target, config.clone())
749        .await
750        .map_err(|e| BindError::UnknownFailure(e.to_string()))?;
751
752    let open_listeners_clone = open_listeners.clone();
753    let listener_id = ListenerId(hopr_utils::network_types::types::IpProtocol::UDP, bound_host);
754
755    // Create an abort handle so that the Session can be terminated by aborting
756    // the UDP stream first. Because under the hood, the bind_session_to_stream uses
757    // `transfer_session` which in turn uses `copy_duplex_abortable`, aborting the
758    // `udp_socket` will:
759    //
760    // 1. Initiate graceful shutdown of `udp_socket`
761    // 2. Once done, initiate a graceful shutdown of `session`
762    // 3. Finally, return from the `bind_session_to_stream` which will terminate the spawned task
763    //
764    // This is needed because the `udp_socket` cannot terminate by itself.
765    let (abort_handle, abort_reg) = AbortHandle::new_pair();
766    let clients = Arc::new(DashMap::new());
767    let max_clients: usize = 1; // Maximum number of clients for this session. Currently always 1.
768
769    // TODO: add multiple client support to UDP sessions (#7370)
770    let session_id = *session.id();
771    clients.insert(
772        session_id,
773        ClientEntry {
774            sock_addr: bound_host,
775            abort_handle: abort_handle.clone(),
776            configurator,
777        },
778    );
779    hopr_utils::runtime::prelude::spawn(async move {
780        #[cfg(all(feature = "telemetry", not(test)))]
781        METRIC_ACTIVE_CLIENTS.increment(&["udp"], 1.0);
782
783        bind_session_to_stream(session, udp_socket, HOPR_UDP_BUFFER_SIZE, Some(abort_reg)).await;
784
785        #[cfg(all(feature = "telemetry", not(test)))]
786        METRIC_ACTIVE_CLIENTS.decrement(&["udp"], 1.0);
787
788        // Once the Session closes, remove it from the list
789        open_listeners_clone.0.remove(&listener_id);
790    });
791
792    open_listeners.0.insert(
793        listener_id,
794        StoredSessionEntry {
795            destination,
796            target: target_spec,
797            forward_path,
798            return_path,
799            max_client_sessions: max_clients,
800            max_surb_upstream,
801            response_buffer,
802            session_pool: None,
803            abort_handle,
804            clients,
805        },
806    );
807    Ok((bound_host, Some(session_id), max_clients))
808}
809
810async fn try_restricted_bind<F, S, Fut>(
811    addrs: Vec<std::net::SocketAddr>,
812    range_str: &str,
813    binder: F,
814) -> std::io::Result<S>
815where
816    F: Fn(Vec<std::net::SocketAddr>) -> Fut,
817    Fut: Future<Output = std::io::Result<S>>,
818{
819    if addrs.is_empty() {
820        return Err(std::io::Error::other("no valid socket addresses found"));
821    }
822
823    let range = range_str
824        .split_once(":")
825        .and_then(
826            |(a, b)| match u16::from_str(a).and_then(|a| Ok((a, u16::from_str(b)?))) {
827                Ok((a, b)) if a <= b => Some(a..=b),
828                _ => None,
829            },
830        )
831        .ok_or(std::io::Error::other(format!("invalid port range {range_str}")))?;
832
833    for port in range {
834        let addrs = addrs
835            .iter()
836            .map(|addr| std::net::SocketAddr::new(addr.ip(), port))
837            .collect::<Vec<_>>();
838        match binder(addrs).await {
839            Ok(listener) => return Ok(listener),
840            Err(error) => debug!(%error, "listen address not usable"),
841        }
842    }
843
844    Err(std::io::Error::new(
845        std::io::ErrorKind::AddrNotAvailable,
846        format!("no valid socket addresses found within range: {range_str}"),
847    ))
848}
849
850/// Listen on a specified address with a port from an optional port range for TCP connections.
851async fn tcp_listen_on<A: std::net::ToSocketAddrs>(
852    address: A,
853    port_range: Option<String>,
854) -> std::io::Result<(std::net::SocketAddr, TcpListener)> {
855    let addrs = address.to_socket_addrs()?.collect::<Vec<_>>();
856
857    // If automatic port allocation is requested and there's a restriction on the port range
858    // (via HOPRD_SESSION_PORT_RANGE), try to find an address within that range.
859    if addrs.iter().all(|a| a.port() == 0)
860        && let Some(range_str) = port_range
861    {
862        let tcp_listener = try_restricted_bind(
863            addrs,
864            &range_str,
865            |a| async move { TcpListener::bind(a.as_slice()).await },
866        )
867        .await?;
868        return Ok((tcp_listener.local_addr()?, tcp_listener));
869    }
870
871    let tcp_listener = TcpListener::bind(addrs.as_slice()).await?;
872    Ok((tcp_listener.local_addr()?, tcp_listener))
873}
874
875pub async fn udp_bind_to<A: std::net::ToSocketAddrs>(
876    address: A,
877    port_range: Option<String>,
878) -> std::io::Result<(std::net::SocketAddr, ConnectedUdpStream)> {
879    let addrs = address.to_socket_addrs()?.collect::<Vec<_>>();
880
881    let builder = ConnectedUdpStream::builder()
882        .with_buffer_size(HOPR_UDP_BUFFER_SIZE)
883        .with_foreign_data_mode(ForeignDataMode::Discard) // discard data from UDP clients other than the first one served
884        .with_queue_size(HOPR_UDP_QUEUE_SIZE)
885        .with_receiver_parallelism(
886            std::env::var("HOPRD_SESSION_ENTRY_UDP_RX_PARALLELISM")
887                .ok()
888                .and_then(|s| s.parse::<NonZeroUsize>().ok())
889                .map(UdpStreamParallelism::Specific)
890                .unwrap_or(UdpStreamParallelism::Auto),
891        );
892
893    // If automatic port allocation is requested and there's a restriction on the port range
894    // (via HOPRD_SESSION_PORT_RANGE), try to find an address within that range.
895    if addrs.iter().all(|a| a.port() == 0)
896        && let Some(range_str) = port_range
897    {
898        let udp_listener = try_restricted_bind(addrs, &range_str, |addrs| {
899            futures::future::ready(builder.clone().build(addrs.as_slice()))
900        })
901        .await?;
902
903        return Ok((*udp_listener.bound_address(), udp_listener));
904    }
905
906    let udp_socket = builder.build(address)?;
907    Ok((*udp_socket.bound_address(), udp_socket))
908}
909
910async fn bind_session_to_stream<T>(
911    mut session: HoprSession,
912    mut stream: T,
913    max_buf: usize,
914    abort_reg: Option<AbortRegistration>,
915) where
916    T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
917{
918    let session_id = *session.id();
919    match transfer_session(&mut session, &mut stream, max_buf, abort_reg).await {
920        Ok((session_to_stream_bytes, stream_to_session_bytes)) => info!(
921            session_id = ?session_id,
922            session_to_stream_bytes, stream_to_session_bytes, "client session ended",
923        ),
924        Err(error) => error!(
925            session_id = ?session_id,
926            %error,
927            "error during data transfer"
928        ),
929    }
930}
931
932#[cfg(test)]
933mod tests {
934    use anyhow::Context;
935    use futures::{
936        FutureExt, StreamExt,
937        channel::mpsc::{UnboundedReceiver, UnboundedSender},
938    };
939    use futures_time::future::FutureExt as TimeFutureExt;
940    use hopr_api::types::crypto::crypto_traits::Randomizable;
941    use hopr_lib::{
942        api::types::{
943            internal::{
944                prelude::HoprPseudonym,
945                routing::{DestinationRouting, RoutingOptions},
946            },
947            primitive::prelude::Address,
948        },
949        exports::transport::{ApplicationData, ApplicationDataIn, ApplicationDataOut, HoprSession},
950    };
951    use hopr_transport::session::HoprSessionConfig;
952    use tokio::io::{AsyncReadExt, AsyncWriteExt};
953
954    use super::*;
955
956    fn loopback_transport() -> (
957        UnboundedSender<(DestinationRouting, ApplicationDataOut)>,
958        UnboundedReceiver<ApplicationDataIn>,
959    ) {
960        let (input_tx, input_rx) = futures::channel::mpsc::unbounded::<(DestinationRouting, ApplicationDataOut)>();
961        let (output_tx, output_rx) = futures::channel::mpsc::unbounded::<ApplicationDataIn>();
962        tokio::task::spawn(
963            input_rx
964                .map(|(_, data)| {
965                    Ok(ApplicationDataIn {
966                        data: data.data,
967                        packet_info: Default::default(),
968                    })
969                })
970                .forward(output_tx)
971                .map(|e| tracing::debug!(?e, "loopback transport completed")),
972        );
973
974        (input_tx, output_rx)
975    }
976
977    #[tokio::test]
978    async fn hoprd_session_connection_should_create_a_working_tcp_socket_through_which_data_can_be_sent_and_received()
979    -> anyhow::Result<()> {
980        let session_id = HoprPseudonym::random();
981        let peer: Address = "0x5112D584a1C72Fc250176B57aEba5fFbbB287D8F".parse()?;
982        let cfg = HoprSessionConfig::default();
983        let session = HoprSession::new(
984            session_id,
985            DestinationRouting::forward_only(peer, RoutingOptions::IntermediatePath(Default::default())),
986            cfg,
987            loopback_transport(),
988            None,
989        )?;
990
991        let (bound_addr, tcp_listener) = tcp_listen_on(("127.0.0.1", 0), None)
992            .await
993            .context("listen_on failed")?;
994
995        tokio::task::spawn(async move {
996            match tcp_listener.accept().await {
997                Ok((stream, _)) => bind_session_to_stream(session, stream, HOPR_TCP_BUFFER_SIZE, None).await,
998                Err(e) => error!("failed to accept connection: {e}"),
999            }
1000        });
1001
1002        let mut tcp_stream = tokio::net::TcpStream::connect(bound_addr)
1003            .await
1004            .context("connect failed")?;
1005
1006        let data = vec![b"hello", b"world", b"this ", b"is   ", b"    a", b" test"];
1007
1008        for d in data.clone().into_iter() {
1009            tcp_stream.write_all(d).await.context("write failed")?;
1010        }
1011
1012        for d in data.iter() {
1013            let mut buf = vec![0; d.len()];
1014            tcp_stream.read_exact(&mut buf).await.context("read failed")?;
1015        }
1016
1017        Ok(())
1018    }
1019
1020    #[test_log::test(tokio::test)]
1021    async fn hoprd_session_connection_should_create_a_working_udp_socket_through_which_data_can_be_sent_and_received()
1022    -> anyhow::Result<()> {
1023        let session_id = HoprPseudonym::random();
1024        let peer: Address = "0x5112D584a1C72Fc250176B57aEba5fFbbB287D8F".parse()?;
1025        let cfg = HoprSessionConfig::default();
1026        let session = HoprSession::new(
1027            session_id,
1028            DestinationRouting::forward_only(peer, RoutingOptions::IntermediatePath(Default::default())),
1029            cfg,
1030            loopback_transport(),
1031            None,
1032        )?;
1033
1034        let (listen_addr, udp_listener) = udp_bind_to(("127.0.0.1", 0), None)
1035            .await
1036            .context("udp_bind_to failed")?;
1037
1038        let (abort_handle, abort_registration) = AbortHandle::new_pair();
1039        let jh = tokio::task::spawn(bind_session_to_stream(
1040            session,
1041            udp_listener,
1042            ApplicationData::PAYLOAD_SIZE,
1043            Some(abort_registration),
1044        ));
1045
1046        let mut udp_stream = ConnectedUdpStream::builder()
1047            .with_buffer_size(ApplicationData::PAYLOAD_SIZE)
1048            .with_queue_size(HOPR_UDP_QUEUE_SIZE)
1049            .with_counterparty(listen_addr)
1050            .build(("127.0.0.1", 0))
1051            .context("bind failed")?;
1052
1053        let data = vec![b"hello", b"world", b"this ", b"is   ", b"    a", b" test"];
1054
1055        for d in data.clone().into_iter() {
1056            udp_stream.write_all(d).await.context("write failed")?;
1057            // ConnectedUdpStream performs flush with each write
1058        }
1059
1060        for d in data.iter() {
1061            let mut buf = vec![0; d.len()];
1062            udp_stream.read_exact(&mut buf).await.context("read failed")?;
1063        }
1064
1065        // Once aborted, the bind_session_to_stream task must terminate too
1066        abort_handle.abort();
1067        jh.timeout(futures_time::time::Duration::from_millis(200)).await??;
1068
1069        Ok(())
1070    }
1071
1072    fn stub_stored_entry() -> StoredSessionEntry {
1073        let (abort_handle, _) = AbortHandle::new_pair();
1074        StoredSessionEntry {
1075            destination: Address::default(),
1076            target: SessionTargetSpec::Plain("localhost:8080".into()),
1077            forward_path: Default::default(),
1078            return_path: Default::default(),
1079            max_client_sessions: 5,
1080            max_surb_upstream: None,
1081            response_buffer: None,
1082            session_pool: None,
1083            abort_handle,
1084            clients: Arc::new(DashMap::new()),
1085        }
1086    }
1087
1088    #[test]
1089    fn find_configurator_should_return_none_when_no_listeners() {
1090        let handles = ListenerJoinHandles::default();
1091        let session_id = HoprPseudonym::random();
1092        assert!(handles.find_configurator(&session_id).is_none());
1093    }
1094
1095    #[test]
1096    fn find_configurator_should_return_none_when_session_not_tracked() {
1097        let handles = ListenerJoinHandles::default();
1098        let listener_id = ListenerId(IpProtocol::TCP, "127.0.0.1:9091".parse().unwrap());
1099        handles.0.insert(listener_id, stub_stored_entry());
1100
1101        let session_id = HoprPseudonym::random();
1102        assert!(handles.find_configurator(&session_id).is_none());
1103    }
1104
1105    #[test]
1106    fn configurators_for_returns_empty_for_unknown_host() {
1107        let handles = ListenerJoinHandles::default();
1108        let addr: std::net::SocketAddr = "127.0.0.1:9999".parse().unwrap();
1109        assert!(handles.configurators_for(addr).is_empty());
1110    }
1111
1112    #[test]
1113    fn configurators_for_returns_empty_vec_when_listener_has_no_clients() {
1114        let handles = ListenerJoinHandles::default();
1115        let addr: std::net::SocketAddr = "127.0.0.1:9091".parse().unwrap();
1116        handles.0.insert(ListenerId(IpProtocol::TCP, addr), stub_stored_entry());
1117        // Entry exists but has no clients, so the result must be an empty Vec.
1118        assert!(handles.configurators_for(addr).is_empty());
1119    }
1120
1121    #[test]
1122    fn configurators_for_ignores_udp_listener_on_same_port() {
1123        let handles = ListenerJoinHandles::default();
1124        let addr: std::net::SocketAddr = "127.0.0.1:9092".parse().unwrap();
1125        handles.0.insert(ListenerId(IpProtocol::UDP, addr), stub_stored_entry());
1126        // Only TCP listeners are looked up; a UDP entry on the same address must not match.
1127        assert!(handles.configurators_for(addr).is_empty());
1128    }
1129
1130    #[test]
1131    fn stored_session_entry_clients_should_start_empty() {
1132        let entry = stub_stored_entry();
1133        assert!(entry.get_clients().is_empty());
1134        assert_eq!(entry.max_client_sessions, 5);
1135    }
1136
1137    #[test]
1138    fn session_target_spec_plain_roundtrip() {
1139        let spec = SessionTargetSpec::Plain("localhost:8080".into());
1140        let s = spec.to_string();
1141        assert_eq!(s, "localhost:8080");
1142        assert_eq!(
1143            SessionTargetSpec::from_str(&s).unwrap(),
1144            SessionTargetSpec::Plain("localhost:8080".into())
1145        );
1146    }
1147
1148    #[test]
1149    fn session_target_spec_sealed_roundtrip() {
1150        let data = vec![0xde, 0xad, 0xbe, 0xef];
1151        let spec = SessionTargetSpec::Sealed(data.clone());
1152        let s = spec.to_string();
1153        assert!(s.starts_with("$$"));
1154        assert_eq!(
1155            SessionTargetSpec::from_str(&s).unwrap(),
1156            SessionTargetSpec::Sealed(data)
1157        );
1158    }
1159
1160    #[test]
1161    fn session_target_spec_service_roundtrip() {
1162        let spec = SessionTargetSpec::Service(42);
1163        let s = spec.to_string();
1164        assert_eq!(s, "#42");
1165        assert_eq!(SessionTargetSpec::from_str(&s).unwrap(), SessionTargetSpec::Service(42));
1166    }
1167
1168    #[test]
1169    fn build_binding_address() {
1170        let default = "10.0.0.1:10000".parse().unwrap();
1171
1172        let result = build_binding_host(Some("127.0.0.1:10000"), default);
1173        assert_eq!(result, "127.0.0.1:10000".parse::<std::net::SocketAddr>().unwrap());
1174
1175        let result = build_binding_host(None, default);
1176        assert_eq!(result, "10.0.0.1:10000".parse::<std::net::SocketAddr>().unwrap());
1177
1178        let result = build_binding_host(Some("127.0.0.1"), default);
1179        assert_eq!(result, "127.0.0.1:10000".parse::<std::net::SocketAddr>().unwrap());
1180
1181        let result = build_binding_host(Some(":1234"), default);
1182        assert_eq!(result, "10.0.0.1:1234".parse::<std::net::SocketAddr>().unwrap());
1183
1184        let result = build_binding_host(Some(":"), default);
1185        assert_eq!(result, "10.0.0.1:10000".parse::<std::net::SocketAddr>().unwrap());
1186
1187        let result = build_binding_host(Some(""), default);
1188        assert_eq!(result, "10.0.0.1:10000".parse::<std::net::SocketAddr>().unwrap());
1189    }
1190}