Skip to main content

hopr_transport/
config.rs

1use std::{
2    fmt::{Display, Formatter},
3    net::ToSocketAddrs,
4    num::ParseIntError,
5    str::FromStr,
6    time::Duration,
7};
8
9use hopr_api::Multiaddr;
10pub use hopr_protocol_hopr::{HoprCodecConfig, HoprUnacknowledgedTicketProcessorConfig, SurbPopOrder, SurbStoreConfig};
11pub use hopr_transport_mixer::config::MixerConfig;
12pub use hopr_transport_probe::config::ProbeConfig;
13use hopr_transport_session::{MIN_BALANCER_SAMPLING_INTERVAL, MIN_SURB_BUFFER_DURATION};
14use proc_macro_regex::regex;
15use validator::{Validate, ValidationError, ValidationErrors};
16
17use crate::{errors::HoprTransportError, protocol::PacketPipelineConfig};
18
19const DEFAULT_COUNTER_FLUSH_INTERVAL: Duration = Duration::from_secs(15);
20
21const DEFAULT_PER_PEER_CHANNEL_CAPACITY: usize = 5_000;
22const DEFAULT_STREAM_OPEN_TIMEOUT: Duration = Duration::from_secs(2);
23const DEFAULT_FRAME_WRITER_BACKPRESSURE_BYTES: usize = 131_072;
24const DEFAULT_EGRESS_BACKPRESSURE_TIMEOUT: Duration = Duration::from_secs(2);
25
26/// Minimum accepted value for [`StreamProtocolConfig::stream_open_timeout`].
27pub const MIN_STREAM_OPEN_TIMEOUT: Duration = Duration::from_millis(1);
28
29/// Minimum accepted value for [`StreamProtocolConfig::egress_backpressure_timeout`].
30pub const MIN_EGRESS_BACKPRESSURE_TIMEOUT: Duration = Duration::from_millis(1);
31
32fn default_per_peer_channel_capacity() -> usize {
33    DEFAULT_PER_PEER_CHANNEL_CAPACITY
34}
35
36fn default_stream_open_timeout() -> Duration {
37    DEFAULT_STREAM_OPEN_TIMEOUT
38}
39
40fn default_frame_writer_backpressure_bytes() -> usize {
41    DEFAULT_FRAME_WRITER_BACKPRESSURE_BYTES
42}
43
44#[inline]
45fn default_egress_backpressure_timeout() -> Duration {
46    DEFAULT_EGRESS_BACKPRESSURE_TIMEOUT
47}
48
49fn validate_stream_open_timeout(value: &Duration) -> Result<(), ValidationError> {
50    if MIN_STREAM_OPEN_TIMEOUT <= *value {
51        Ok(())
52    } else {
53        Err(ValidationError::new("stream open timeout must be at least 1 ms"))
54    }
55}
56
57fn validate_egress_backpressure_timeout(value: &Duration) -> Result<(), ValidationError> {
58    if MIN_EGRESS_BACKPRESSURE_TIMEOUT <= *value {
59        Ok(())
60    } else {
61        // A zero (or sub-millisecond) timeout would make every full channel fall straight into
62        // drop-newest, silently defeating the backpressure feature — reject it at config time.
63        Err(ValidationError::new(
64            "egress backpressure timeout must be at least 1 ms",
65        ))
66    }
67}
68
69/// Configuration of the per-peer egress stream layer.
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Validate, smart_default::SmartDefault)]
71#[cfg_attr(
72    feature = "serde",
73    derive(serde::Serialize, serde::Deserialize),
74    serde(deny_unknown_fields)
75)]
76pub struct StreamProtocolConfig {
77    /// Capacity of the per-peer egress channel (in packets).
78    ///
79    /// The egress drain enqueues each outgoing packet via `try_send`. When the
80    /// channel is full the behaviour depends on the stream state: while the stream
81    /// is still opening it drops the newest packet (a slow open for one peer must
82    /// not head-of-line-block others); once the stream is open and its write pump
83    /// is draining, it instead applies bounded backpressure — waiting up to
84    /// `EGRESS_BACKPRESSURE_TIMEOUT` for space so wire-rate backpressure propagates
85    /// upstream — and only drops the newest packet if the peer stays full past that
86    /// timeout. The channel absorbs bursts while a stream is being opened; once open
87    /// the write pump continuously drains it, so it stays near-empty under normal load.
88    ///
89    /// Sized to absorb a typical SURB pre-fill burst (default SurbBalancer:
90    /// target 7 000 / max 5 000/s).
91    ///
92    /// Defaults to 5 000.
93    #[validate(range(min = 1))]
94    #[default(default_per_peer_channel_capacity())]
95    #[cfg_attr(feature = "serde", serde(default = "default_per_peer_channel_capacity"))]
96    pub per_peer_channel_capacity: usize,
97
98    /// Timeout for the `NetworkStreamControl::open` call when opening a new
99    /// outgoing stream to a peer.
100    ///
101    /// A timeout is mandatory: without it a permanently-unreachable peer would park
102    /// the opener task indefinitely. When the open attempt fails or times out the
103    /// buffered packets for that peer are dropped and a debug-level log entry is
104    /// emitted. The cache entry is then invalidated so the next send triggers a
105    /// fresh open attempt.
106    ///
107    /// Must be at least 1 ms. Defaults to 2 seconds.
108    #[validate(custom(function = "validate_stream_open_timeout"))]
109    #[default(default_stream_open_timeout())]
110    #[cfg_attr(
111        feature = "serde",
112        serde(default = "default_stream_open_timeout", with = "humantime_serde")
113    )]
114    pub stream_open_timeout: Duration,
115
116    /// Pending-write-buffer byte threshold on the framed writer before a flush is forced.
117    ///
118    /// A value of `1` flushes on every encoded frame (one syscall per message).
119    /// Larger values coalesce adjacent small frames into a single quinn write call,
120    /// reducing connection-mutex acquisitions and driver wake-ups on the hot path.
121    /// A HOPR packet is ~1 440 bytes; at the default 128 KiB threshold roughly 91
122    /// packets are coalesced per write, cutting driver wake frequency ~30×.
123    ///
124    /// Defaults to 131 072 bytes (128 KiB).
125    #[validate(range(min = 1))]
126    #[default(default_frame_writer_backpressure_bytes())]
127    #[cfg_attr(feature = "serde", serde(default = "default_frame_writer_backpressure_bytes"))]
128    pub frame_writer_backpressure_bytes: usize,
129
130    /// Maximum time the egress drain waits on a full — but open and draining — per-peer channel
131    /// before falling back to drop-newest.
132    ///
133    /// While the stream is open, a full channel means the wire is slower than the producer, so
134    /// waiting here propagates wire-rate backpressure up through the mixer and session socket to the
135    /// application writer (no packet loss). The bound ensures a single permanently-stalled peer cannot
136    /// head-of-line-block delivery to other peers indefinitely: after this timeout the packet is
137    /// dropped and the drain moves on. Healthy peers drain far faster than this, so the timeout is not
138    /// hit in normal operation.
139    ///
140    /// It doubles as the per-peer write-pump stall timeout: if the pump makes no progress writing to
141    /// a peer's stream for this long (a remote that stopped reading parks quinn's `poll_write`
142    /// forever), the pump fails and its cache entry is evicted so the next send reopens the stream,
143    /// rather than the pump parking indefinitely and the drain serialising on it.
144    ///
145    /// Defaults to 2 seconds. Must be at least 1 ms — a zero value would defeat the feature.
146    #[validate(custom(function = "validate_egress_backpressure_timeout"))]
147    #[default(default_egress_backpressure_timeout())]
148    #[cfg_attr(feature = "serde", serde(default = "default_egress_backpressure_timeout"))]
149    pub egress_backpressure_timeout: Duration,
150}
151
152fn default_counter_flush_interval() -> Duration {
153    DEFAULT_COUNTER_FLUSH_INTERVAL
154}
155
156/// How often SURB round-trip counts reach the network graph.
157///
158/// Much shorter than the protocol counter flush, which is the same order as the recovery window
159/// this signal exists to shorten -- evidence about a dead relayer sitting unreported for 15 s would
160/// defeat the point. Still well inside the graph's own bucket width, so batching adds no
161/// distortion while cutting graph write locks by orders of magnitude.
162const DEFAULT_SURB_FLUSH_INTERVAL: Duration = Duration::from_secs(1);
163
164fn default_surb_flush_interval() -> Duration {
165    DEFAULT_SURB_FLUSH_INTERVAL
166}
167
168/// Simulated per-packet transit latency inserted between the mixer and the wire.
169///
170/// When set on a node's config, every packet emitted by the mixer is held for a
171/// Gaussian-jittered delay before being forwarded to the transport layer.  The delay
172/// is **FIFO** (packets are never reordered): the release deadline is `max(prev_deadline,
173/// now) + sample`, so back-to-back bursts accumulate a monotonically non-decreasing
174/// offset rather than reordering.
175///
176/// **Intended for testing only** — simulates WAN-link transit latency (e.g. ~50 ms) in
177/// a local cluster.  Defaults to `None` (disabled; zero production overhead).
178#[derive(Debug, Clone, Copy, PartialEq, Eq, smart_default::SmartDefault)]
179#[cfg_attr(
180    feature = "serde",
181    derive(serde::Serialize, serde::Deserialize),
182    serde(deny_unknown_fields)
183)]
184pub struct TransitLatencyConfig {
185    /// Mean transit latency per packet.
186    #[default(Duration::from_millis(50))]
187    #[cfg_attr(feature = "serde", serde(with = "humantime_serde"))]
188    pub mean: Duration,
189    /// Standard deviation of the transit latency.
190    ///
191    /// Set to zero for a deterministic (fixed) delay equal to `mean`.
192    #[default(Duration::from_millis(5))]
193    #[cfg_attr(feature = "serde", serde(with = "humantime_serde"))]
194    pub std_dev: Duration,
195}
196
197/// Complete configuration of the HOPR protocol stack.
198#[derive(Debug, smart_default::SmartDefault, Validate, Clone, PartialEq)]
199#[cfg_attr(
200    feature = "serde",
201    derive(serde::Serialize, serde::Deserialize),
202    serde(deny_unknown_fields)
203)]
204pub struct HoprProtocolConfig {
205    /// Libp2p-related transport configuration
206    #[validate(nested)]
207    #[cfg_attr(feature = "serde", serde(default))]
208    pub transport: TransportConfig,
209    /// HOPR packet pipeline configuration
210    #[validate(nested)]
211    #[cfg_attr(feature = "serde", serde(default))]
212    pub packet: HoprPacketPipelineConfig,
213    /// Probing protocol configuration
214    #[validate(nested)]
215    #[cfg_attr(feature = "serde", serde(default))]
216    pub probe: ProbeConfig,
217    /// Session protocol global configuration
218    #[validate(nested)]
219    #[cfg_attr(feature = "serde", serde(default))]
220    pub session: SessionGlobalConfig,
221    /// Mixer configuration.
222    #[cfg_attr(feature = "serde", serde(default))]
223    pub mixer: MixerConfig,
224    /// Simulated transit latency shim between the mixer output and the wire.
225    ///
226    /// When `Some`, a Gaussian-jittered FIFO delay is inserted before every forwarded
227    /// packet — simulating WAN-link transit time in a local cluster test run.
228    /// Set `None` (the default) in production: zero overhead.
229    #[cfg_attr(feature = "serde", serde(default))]
230    pub transit_latency: Option<TransitLatencyConfig>,
231    /// Per-peer egress stream configuration
232    #[validate(nested)]
233    #[cfg_attr(feature = "serde", serde(default))]
234    pub stream: StreamProtocolConfig,
235    /// Path planner configuration
236    #[validate(nested)]
237    #[cfg_attr(feature = "serde", serde(skip))]
238    pub path_planner: crate::path::PathPlannerConfig,
239    /// Interval at which per-peer protocol conformance counters are flushed
240    /// into the network graph.
241    ///
242    /// Default is 15 seconds.
243    #[default(default_counter_flush_interval())]
244    #[cfg_attr(
245        feature = "serde",
246        serde(default = "default_counter_flush_interval", with = "humantime_serde")
247    )]
248    pub counter_flush_interval: Duration,
249    /// Interval at which SURB round-trip counts are flushed into the network graph.
250    ///
251    /// Default is 1 second.
252    #[default(default_surb_flush_interval())]
253    #[cfg_attr(
254        feature = "serde",
255        serde(default = "default_surb_flush_interval", with = "humantime_serde")
256    )]
257    pub surb_flush_interval: Duration,
258}
259
260/// Configuration of the HOPR packet pipeline.
261#[derive(Clone, Copy, Debug, PartialEq, Validate, smart_default::SmartDefault)]
262#[cfg_attr(
263    feature = "serde",
264    derive(serde::Serialize, serde::Deserialize),
265    serde(deny_unknown_fields)
266)]
267pub struct HoprPacketPipelineConfig {
268    /// HOPR packet codec configuration
269    #[validate(nested)]
270    #[cfg_attr(feature = "serde", serde(default))]
271    pub codec: HoprCodecConfig,
272    /// Configuration of unacknowledged tickets processing.
273    #[validate(nested)]
274    #[cfg_attr(feature = "serde", serde(default))]
275    pub ack_processor: HoprUnacknowledgedTicketProcessorConfig,
276    /// Single Use Reply Block (SURB) handling configuration
277    #[validate(nested)]
278    #[cfg_attr(feature = "serde", serde(default))]
279    pub surb_store: SurbStoreConfig,
280    /// Packet pipeline configuration controlling output/input concurrency and acknowledgement processing
281    #[validate(nested)]
282    #[cfg_attr(feature = "serde", serde(default))]
283    pub pipeline: PacketPipelineConfig,
284}
285
286regex!(is_dns_address_regex "^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)*[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$");
287
288/// Check whether the string looks like a valid domain.
289#[inline]
290pub fn looks_like_domain(s: &str) -> bool {
291    is_dns_address_regex(s)
292}
293
294/// Check whether the string is an actual reachable domain.
295pub fn is_reachable_domain(host: &str) -> bool {
296    host.to_socket_addrs().is_ok_and(|i| i.into_iter().next().is_some())
297}
298
299/// Enumeration of possible host types.
300#[derive(Debug, Clone, PartialEq)]
301#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
302pub enum HostType {
303    /// IPv4 based host
304    IPv4(String),
305    /// DNS based host
306    Domain(String),
307}
308
309impl validator::Validate for HostType {
310    fn validate(&self) -> Result<(), ValidationErrors> {
311        match &self {
312            HostType::IPv4(ip4) => validate_ipv4_address(ip4).map_err(|e| {
313                let mut errs = ValidationErrors::new();
314                errs.add("ipv4", e);
315                errs
316            }),
317            HostType::Domain(domain) => validate_dns_address(domain).map_err(|e| {
318                let mut errs = ValidationErrors::new();
319                errs.add("domain", e);
320                errs
321            }),
322        }
323    }
324}
325
326impl Default for HostType {
327    fn default() -> Self {
328        HostType::IPv4("127.0.0.1".to_owned())
329    }
330}
331
332/// Configuration of the listening host.
333///
334/// This is used for the P2P and REST API listeners.
335///
336/// Intentionally has no default because it depends on the use case.
337#[derive(Debug, Validate, Clone, PartialEq)]
338#[cfg_attr(
339    feature = "serde",
340    derive(serde::Serialize, serde::Deserialize),
341    serde(deny_unknown_fields)
342)]
343pub struct HostConfig {
344    /// Host on which to listen
345    #[cfg_attr(feature = "serde", serde(default))]
346    pub address: HostType,
347    /// Listening TCP or UDP port (mandatory).
348    #[validate(range(min = 1u16))]
349    #[cfg_attr(feature = "serde", serde(default))]
350    pub port: u16,
351}
352
353impl FromStr for HostConfig {
354    type Err = String;
355
356    fn from_str(s: &str) -> Result<Self, Self::Err> {
357        let (ip_or_dns, str_port) = match s.split_once(':') {
358            None => return Err("Invalid host, is not in the '<host>:<port>' format".into()),
359            Some(split) => split,
360        };
361
362        let port = str_port.parse().map_err(|e: ParseIntError| e.to_string())?;
363
364        if validator::ValidateIp::validate_ipv4(&ip_or_dns) {
365            Ok(Self {
366                address: HostType::IPv4(ip_or_dns.to_owned()),
367                port,
368            })
369        } else if looks_like_domain(ip_or_dns) {
370            Ok(Self {
371                address: HostType::Domain(ip_or_dns.to_owned()),
372                port,
373            })
374        } else {
375            Err("Not a valid IPv4 or domain host".into())
376        }
377    }
378}
379
380impl Display for HostConfig {
381    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
382        write!(f, "{:?}:{}", self.address, self.port)
383    }
384}
385
386fn default_multiaddr_transport(port: u16) -> String {
387    cfg_if::cfg_if! {
388        if #[cfg(feature = "p2p-announce-quic")] {
389            // In case we run on a Dappnode-like device, presumably behind NAT, we fall back to TCP
390            // to circumvent issues with QUIC in such environments. To make this work reliably,
391            // we would need proper NAT traversal support.
392            let on_dappnode = std::env::var("DAPPNODE")
393                .map(|v| v.to_lowercase() == "true")
394                .unwrap_or(false);
395
396            // Using HOPRD_NAT a user can overwrite the default behaviour even on a Dappnode-like device
397            let uses_nat = std::env::var("HOPRD_NAT")
398                .map(|v| v.to_lowercase() == "true")
399                .unwrap_or(on_dappnode);
400
401            if uses_nat {
402                format!("tcp/{port}")
403            } else {
404                format!("udp/{port}/quic-v1")
405            }
406        } else {
407            format!("tcp/{port}")
408        }
409    }
410}
411
412impl TryFrom<&HostConfig> for Multiaddr {
413    type Error = HoprTransportError;
414
415    fn try_from(value: &HostConfig) -> Result<Self, Self::Error> {
416        match &value.address {
417            HostType::IPv4(ip) => Multiaddr::from_str(
418                format!("/ip4/{}/{}", ip.as_str(), default_multiaddr_transport(value.port)).as_str(),
419            )
420            .map_err(|e| HoprTransportError::Api(e.to_string())),
421            HostType::Domain(domain) => Multiaddr::from_str(
422                format!("/dns4/{}/{}", domain.as_str(), default_multiaddr_transport(value.port)).as_str(),
423            )
424            .map_err(|e| HoprTransportError::Api(e.to_string())),
425        }
426    }
427}
428
429fn validate_ipv4_address(s: &str) -> Result<(), ValidationError> {
430    if validator::ValidateIp::validate_ipv4(&s) {
431        let ipv4 = std::net::Ipv4Addr::from_str(s)
432            .map_err(|_| ValidationError::new("Failed to deserialize the string into an ipv4 address"))?;
433
434        if ipv4.is_private() || ipv4.is_multicast() || ipv4.is_unspecified() {
435            return Err(ValidationError::new(
436                "IPv4 cannot be private, multicast or unspecified (0.0.0.0)",
437            ))?;
438        }
439        Ok(())
440    } else {
441        Err(ValidationError::new("Invalid IPv4 address provided"))
442    }
443}
444
445fn validate_dns_address(s: &str) -> Result<(), ValidationError> {
446    if looks_like_domain(s) || is_reachable_domain(s) {
447        Ok(())
448    } else {
449        Err(ValidationError::new("Invalid DNS address provided"))
450    }
451}
452
453/// Configuration of the physical transport mechanism.
454#[derive(Debug, Default, Validate, Clone, Copy, PartialEq)]
455#[cfg_attr(
456    feature = "serde",
457    derive(serde::Serialize, serde::Deserialize),
458    serde(deny_unknown_fields)
459)]
460pub struct TransportConfig {
461    /// When true, assume that the node is running in an isolated network and does
462    /// not need any connection to nodes outside the subnet
463    #[cfg_attr(feature = "serde", serde(default))]
464    pub announce_local_addresses: bool,
465    /// When true, assume a testnet with multiple nodes running on the same machine
466    /// or in the same private IPv4 network
467    #[cfg_attr(feature = "serde", serde(default))]
468    pub prefer_local_addresses: bool,
469}
470
471const DEFAULT_SESSION_IDLE_TIMEOUT: Duration = Duration::from_mins(3);
472
473const SESSION_IDLE_MIN_TIMEOUT: Duration = Duration::from_secs(2);
474
475const DEFAULT_SESSION_ESTABLISH_RETRY_DELAY: Duration = Duration::from_secs(2);
476
477const DEFAULT_SESSION_ESTABLISH_MAX_RETRIES: usize = 3;
478
479const DEFAULT_SESSION_BALANCER_SAMPLING: Duration = Duration::from_millis(100);
480
481const DEFAULT_SESSION_BALANCER_BUFFER_DURATION: Duration = Duration::from_secs(5);
482
483const DEFAULT_MAXIMUM_MANAGED_SESSIONS: usize = 100;
484
485fn default_session_balancer_buffer_duration() -> Duration {
486    DEFAULT_SESSION_BALANCER_BUFFER_DURATION
487}
488
489fn default_session_establish_max_retries() -> usize {
490    DEFAULT_SESSION_ESTABLISH_MAX_RETRIES
491}
492
493fn default_session_idle_timeout() -> Duration {
494    DEFAULT_SESSION_IDLE_TIMEOUT
495}
496
497fn default_session_establish_retry_delay() -> Duration {
498    DEFAULT_SESSION_ESTABLISH_RETRY_DELAY
499}
500
501fn default_session_balancer_sampling() -> Duration {
502    DEFAULT_SESSION_BALANCER_SAMPLING
503}
504
505fn default_max_managed_sessions() -> usize {
506    DEFAULT_MAXIMUM_MANAGED_SESSIONS
507}
508
509/// Transport-layer default for the SURB balance notification period; this is the effective default
510/// for [`SessionGlobalConfig::surb_balance_notify_period`] (15s). It deliberately overrides the
511/// lower-level fallback in `SessionManagerConfig` (whose own field default is 60s) with a tighter
512/// 15s cadence, so the Entry's dead-reckoned estimate of the Exit's SURB buffer is corrected often
513/// enough to keep the SURB balancer from under-producing (and starving the Exit) under drift,
514/// without the per-session keep-alive overhead of the previous 2s cadence. The 1s floor is enforced
515/// downstream by `SessionManager::new` (`MIN_SURB_BUFFER_NOTIFICATION_PERIOD`).
516fn default_session_surb_balance_notify_period() -> Option<Duration> {
517    Some(Duration::from_secs(15))
518}
519
520fn default_session_max_frames_behind_gap() -> Option<usize> {
521    Some(256)
522}
523
524fn validate_session_idle_timeout(value: &Duration) -> Result<(), ValidationError> {
525    if SESSION_IDLE_MIN_TIMEOUT <= *value {
526        Ok(())
527    } else {
528        Err(ValidationError::new("session idle timeout is too low"))
529    }
530}
531
532fn validate_balancer_sampling(value: &Duration) -> Result<(), ValidationError> {
533    if MIN_BALANCER_SAMPLING_INTERVAL <= *value {
534        Ok(())
535    } else {
536        Err(ValidationError::new("balancer sampling interval is too low"))
537    }
538}
539
540fn validate_balancer_buffer_duration(value: &Duration) -> Result<(), ValidationError> {
541    if MIN_SURB_BUFFER_DURATION <= *value {
542        Ok(())
543    } else {
544        Err(ValidationError::new("minmum SURB buffer duration is too low"))
545    }
546}
547
548fn validate_surb_balance_notify_period(value: &Duration) -> Result<(), ValidationError> {
549    // `custom` on an `Option` field skips `None` and passes the inner value on `Some`.
550    if *value >= Duration::from_secs(1) {
551        Ok(())
552    } else {
553        Err(ValidationError::new(
554            "SURB balance notify period must be at least 1 second",
555        ))
556    }
557}
558
559/// Global configuration of Sessions and the Session manager.
560#[derive(Clone, Copy, Debug, PartialEq, Eq, Validate, smart_default::SmartDefault)]
561#[cfg_attr(
562    feature = "serde",
563    derive(serde::Serialize, serde::Deserialize),
564    serde(deny_unknown_fields)
565)]
566pub struct SessionGlobalConfig {
567    /// Maximum time before an idle Session is closed.
568    ///
569    /// Defaults to 3 minutes.
570    #[validate(custom(function = "validate_session_idle_timeout"))]
571    #[default(default_session_idle_timeout())]
572    #[cfg_attr(
573        feature = "serde",
574        serde(default = "default_session_idle_timeout", with = "humantime_serde")
575    )]
576    pub idle_timeout: Duration,
577
578    /// Maximum number of Sessions that can be managed by the Session manager.
579    ///
580    /// Default is 1000, minimum is 2, maximum is 100 000.
581    #[validate(range(min = 2, max = 100_000))]
582    #[default(default_max_managed_sessions())]
583    #[cfg_attr(feature = "serde", serde(default = "default_max_managed_sessions"))]
584    pub maximum_managed_sessions: usize,
585
586    /// Maximum retries to attempt to establish the Session
587    /// Set 0 for no retries.
588    ///
589    /// Defaults to 3, maximum is 20.
590    #[validate(range(min = 0, max = 20))]
591    #[default(default_session_establish_max_retries())]
592    #[cfg_attr(feature = "serde", serde(default = "default_session_establish_max_retries"))]
593    pub establish_max_retries: usize,
594
595    /// Delay between Session establishment retries.
596    ///
597    /// Default is 2 seconds.
598    #[default(default_session_establish_retry_delay())]
599    #[cfg_attr(
600        feature = "serde",
601        serde(default = "default_session_establish_retry_delay", with = "humantime_serde")
602    )]
603    pub establish_retry_timeout: Duration,
604
605    /// Sampling interval for SURB balancer in milliseconds.
606    ///
607    /// Default is 100 milliseconds.
608    #[validate(custom(function = "validate_balancer_sampling"))]
609    #[default(default_session_balancer_sampling())]
610    #[cfg_attr(
611        feature = "serde",
612        serde(default = "default_session_balancer_sampling", with = "humantime_serde")
613    )]
614    pub balancer_sampling_interval: Duration,
615
616    /// Minimum runway of received SURBs in seconds.
617    ///
618    /// This applies to incoming Sessions on Exit nodes only and is the main indicator of how
619    /// the egress traffic will be shaped, unless the `NoRateControl` Session
620    /// capability is specified during initiation.
621    ///
622    /// Default is 5 seconds, minimum is 1 second.
623    #[validate(custom(function = "validate_balancer_buffer_duration"))]
624    #[default(default_session_balancer_buffer_duration())]
625    #[cfg_attr(
626        feature = "serde",
627        serde(default = "default_session_balancer_buffer_duration", with = "humantime_serde")
628    )]
629    pub balancer_minimum_surb_buffer_duration: Duration,
630
631    /// How often the Exit reports its true SURB buffer level to the Entry, as an absolute
632    /// correction of the Entry's dead-reckoned estimate. Without it, cumulative packet loss
633    /// silently inflates the estimate until the Exit runs out of SURBs and can no longer
634    /// send reply data.
635    ///
636    /// Default is 15 seconds. Set to `null` to disable; minimum effective period is 1 second.
637    #[validate(custom(function = "validate_surb_balance_notify_period"))]
638    #[default(default_session_surb_balance_notify_period())]
639    #[cfg_attr(
640        feature = "serde",
641        serde(
642            default = "default_session_surb_balance_notify_period",
643            with = "humantime_serde::option"
644        )
645    )]
646    pub surb_balance_notify_period: Option<Duration>,
647
648    /// How many later frames may queue behind a missing one before the reassembler gives up on it
649    /// and releases what it already has.
650    ///
651    /// Only applies to Sessions without retransmission, where a missing frame is never coming and
652    /// waiting out the frame timeout cannot change the outcome — it only holds everything behind
653    /// it. The right value tracks reordering depth (throughput × latency spread ÷ frame size), so
654    /// a bulk-data Session and a control Session on the same node differ by orders of magnitude;
655    /// an individual Session may override it.
656    ///
657    /// Default is 256. Set to `null` to disable the bound and wait out the frame timeout instead.
658    #[default(default_session_max_frames_behind_gap())]
659    #[cfg_attr(feature = "serde", serde(default = "default_session_max_frames_behind_gap"))]
660    pub max_frames_behind_gap: Option<usize>,
661
662    /// Tag allocator partition configuration.
663    #[validate(nested)]
664    #[cfg_attr(feature = "serde", serde(default))]
665    pub tag_allocator: hopr_transport_tag_allocator::TagAllocatorConfig,
666}
667
668#[cfg(test)]
669mod tests {
670    use super::*;
671
672    #[test]
673    fn egress_backpressure_timeout_rejects_sub_minimum_values() {
674        assert!(validate_egress_backpressure_timeout(&Duration::ZERO).is_err());
675        assert!(validate_egress_backpressure_timeout(&Duration::from_micros(500)).is_err());
676        assert!(validate_egress_backpressure_timeout(&MIN_EGRESS_BACKPRESSURE_TIMEOUT).is_ok());
677        assert!(validate_egress_backpressure_timeout(&DEFAULT_EGRESS_BACKPRESSURE_TIMEOUT).is_ok());
678    }
679
680    #[test]
681    fn stream_protocol_config_default_is_valid() {
682        assert!(StreamProtocolConfig::default().validate().is_ok());
683    }
684
685    #[test]
686    fn test_valid_domains_for_looks_like_a_domain() {
687        assert!(looks_like_domain("localhost"));
688        assert!(looks_like_domain("hoprnet.org"));
689        assert!(looks_like_domain("hub.hoprnet.org"));
690    }
691
692    #[test]
693    fn test_valid_domains_for_does_not_look_like_a_domain() {
694        assert!(!looks_like_domain(".org"));
695        assert!(!looks_like_domain("-hoprnet-.org"));
696    }
697
698    #[test]
699    fn test_valid_domains_should_be_reachable() {
700        assert!(!is_reachable_domain("google.com"));
701    }
702
703    #[test]
704    fn test_verify_valid_ip4_addresses() {
705        assert!(validate_ipv4_address("1.1.1.1").is_ok());
706        assert!(validate_ipv4_address("1.255.1.1").is_ok());
707        assert!(validate_ipv4_address("187.1.1.255").is_ok());
708        assert!(validate_ipv4_address("127.0.0.1").is_ok());
709    }
710
711    #[test]
712    fn test_verify_invalid_ip4_addresses() {
713        assert!(validate_ipv4_address("1.256.1.1").is_err());
714        assert!(validate_ipv4_address("-1.1.1.255").is_err());
715        assert!(validate_ipv4_address("127.0.0.256").is_err());
716        assert!(validate_ipv4_address("1").is_err());
717        assert!(validate_ipv4_address("1.1").is_err());
718        assert!(validate_ipv4_address("1.1.1").is_err());
719        assert!(validate_ipv4_address("1.1.1.1.1").is_err());
720    }
721
722    #[test]
723    fn test_verify_valid_dns_addresses() {
724        assert!(validate_dns_address("localhost").is_ok());
725        assert!(validate_dns_address("google.com").is_ok());
726        assert!(validate_dns_address("hub.hoprnet.org").is_ok());
727    }
728
729    #[test]
730    fn test_verify_invalid_dns_addresses() {
731        assert!(validate_dns_address("-hoprnet-.org").is_err());
732    }
733
734    #[test]
735    fn test_multiaddress_on_dappnode_default() {
736        temp_env::with_var("DAPPNODE", Some("true"), || {
737            assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
738        });
739    }
740
741    #[cfg(feature = "p2p-announce-quic")]
742    #[test]
743    fn test_multiaddress_on_non_dappnode_default() {
744        temp_env::with_vars([("DAPPNODE", Some("false")), ("HOPRD_NAT", Some("false"))], || {
745            assert_eq!(default_multiaddr_transport(1234), "udp/1234/quic-v1");
746        });
747    }
748
749    #[cfg(not(feature = "p2p-announce-quic"))]
750    #[test]
751    fn test_multiaddress_on_non_dappnode_default() {
752        assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
753    }
754
755    #[test]
756    fn test_multiaddress_on_non_dappnode_uses_nat() {
757        temp_env::with_var("HOPRD_NAT", Some("true"), || {
758            assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
759        });
760    }
761
762    #[cfg(feature = "p2p-announce-quic")]
763    #[test]
764    fn test_multiaddress_on_non_dappnode_not_uses_nat() {
765        temp_env::with_var("HOPRD_NAT", Some("false"), || {
766            assert_eq!(default_multiaddr_transport(1234), "udp/1234/quic-v1");
767        });
768    }
769
770    #[cfg(not(feature = "p2p-announce-quic"))]
771    #[test]
772    fn test_multiaddress_on_non_dappnode_not_uses_nat() {
773        temp_env::with_var("HOPRD_NAT", Some("false"), || {
774            assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
775        });
776    }
777
778    #[cfg(feature = "p2p-announce-quic")]
779    #[test]
780    fn test_multiaddress_on_dappnode_not_uses_nat() {
781        temp_env::with_vars([("DAPPNODE", Some("true")), ("HOPRD_NAT", Some("false"))], || {
782            assert_eq!(default_multiaddr_transport(1234), "udp/1234/quic-v1");
783        });
784    }
785
786    #[cfg(not(feature = "p2p-announce-quic"))]
787    #[test]
788    fn test_multiaddress_on_dappnode_not_uses_nat() {
789        temp_env::with_vars([("DAPPNODE", Some("true")), ("HOPRD_NAT", Some("false"))], || {
790            assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
791        });
792    }
793
794    // --- HostConfig::FromStr tests ---
795
796    #[test]
797    fn host_config_parses_ipv4_address() {
798        let cfg = HostConfig::from_str("1.2.3.4:9091").unwrap();
799        insta::assert_debug_snapshot!(cfg);
800    }
801
802    #[test]
803    fn host_config_parses_domain() {
804        let cfg = HostConfig::from_str("example.com:443").unwrap();
805        insta::assert_debug_snapshot!(cfg);
806    }
807
808    #[test]
809    fn host_config_rejects_missing_port() {
810        assert!(HostConfig::from_str("1.2.3.4").is_err());
811    }
812
813    #[test]
814    fn host_config_rejects_invalid_port() {
815        assert!(HostConfig::from_str("1.2.3.4:abc").is_err());
816    }
817
818    #[test]
819    fn host_config_rejects_invalid_host() {
820        assert!(HostConfig::from_str("-invalid-.com:80").is_err());
821    }
822
823    #[test]
824    fn host_config_display_roundtrip() {
825        let cfg = HostConfig {
826            address: HostType::IPv4("10.0.0.1".into()),
827            port: 8080,
828        };
829        insta::assert_yaml_snapshot!(cfg.to_string());
830    }
831
832    // --- TryFrom<&HostConfig> for Multiaddr tests ---
833
834    #[test]
835    fn multiaddr_from_ipv4_host_config() {
836        let cfg = HostConfig {
837            address: HostType::IPv4("1.2.3.4".into()),
838            port: 9091,
839        };
840        let addr = Multiaddr::try_from(&cfg).unwrap();
841        insta::assert_yaml_snapshot!(addr.to_string());
842    }
843
844    #[test]
845    fn multiaddr_from_domain_host_config() {
846        let cfg = HostConfig {
847            address: HostType::Domain("example.com".into()),
848            port: 443,
849        };
850        let addr = Multiaddr::try_from(&cfg).unwrap();
851        insta::assert_yaml_snapshot!(addr.to_string());
852    }
853
854    // --- SessionGlobalConfig validation tests ---
855
856    #[test]
857    fn session_global_config_default_is_valid() {
858        let cfg = SessionGlobalConfig::default();
859        assert!(cfg.validate().is_ok());
860    }
861
862    #[test]
863    fn session_global_config_too_low_idle_timeout_is_rejected() {
864        let cfg = SessionGlobalConfig {
865            idle_timeout: Duration::from_millis(100),
866            ..Default::default()
867        };
868        assert!(cfg.validate().is_err());
869    }
870
871    #[test]
872    fn session_global_config_too_many_retries_is_rejected() {
873        let cfg = SessionGlobalConfig {
874            establish_max_retries: 21,
875            ..Default::default()
876        };
877        assert!(cfg.validate().is_err());
878    }
879
880    #[test]
881    fn stream_protocol_config_default_has_expected_values() {
882        let cfg = StreamProtocolConfig::default();
883        assert_eq!(cfg.per_peer_channel_capacity, DEFAULT_PER_PEER_CHANNEL_CAPACITY);
884        assert_eq!(cfg.stream_open_timeout, DEFAULT_STREAM_OPEN_TIMEOUT);
885        assert_eq!(
886            cfg.frame_writer_backpressure_bytes,
887            DEFAULT_FRAME_WRITER_BACKPRESSURE_BYTES
888        );
889        cfg.validate().expect("default StreamProtocolConfig must be valid");
890    }
891
892    #[test]
893    fn stream_protocol_config_zero_capacity_is_rejected() {
894        let cfg = StreamProtocolConfig {
895            per_peer_channel_capacity: 0,
896            ..Default::default()
897        };
898        assert!(cfg.validate().is_err());
899    }
900
901    #[test]
902    fn stream_protocol_config_zero_backpressure_bytes_is_rejected() {
903        let cfg = StreamProtocolConfig {
904            frame_writer_backpressure_bytes: 0,
905            ..Default::default()
906        };
907        assert!(cfg.validate().is_err());
908    }
909
910    #[test]
911    fn stream_protocol_config_zero_stream_open_timeout_is_rejected() {
912        let cfg = StreamProtocolConfig {
913            stream_open_timeout: Duration::ZERO,
914            ..Default::default()
915        };
916        assert!(cfg.validate().is_err());
917    }
918
919    #[test]
920    fn stream_protocol_config_zero_backpressure_timeout_is_rejected() {
921        // Proves the field `#[validate(custom)]` attribute is actually wired into the derived
922        // `StreamProtocolConfig::validate()` — which the parent `HoprProtocolConfig`/`HoprLibConfig`
923        // invoke via `#[validate(nested)]` at node build time (builder.rs `cfg.validate()?`).
924        let cfg = StreamProtocolConfig {
925            egress_backpressure_timeout: Duration::ZERO,
926            ..Default::default()
927        };
928        assert!(cfg.validate().is_err());
929    }
930}