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, 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;
24
25/// Minimum accepted value for [`StreamProtocolConfig::stream_open_timeout`].
26pub const MIN_STREAM_OPEN_TIMEOUT: Duration = Duration::from_millis(1);
27
28fn default_per_peer_channel_capacity() -> usize {
29    DEFAULT_PER_PEER_CHANNEL_CAPACITY
30}
31
32fn default_stream_open_timeout() -> Duration {
33    DEFAULT_STREAM_OPEN_TIMEOUT
34}
35
36fn default_frame_writer_backpressure_bytes() -> usize {
37    DEFAULT_FRAME_WRITER_BACKPRESSURE_BYTES
38}
39
40fn validate_stream_open_timeout(value: &Duration) -> Result<(), ValidationError> {
41    if MIN_STREAM_OPEN_TIMEOUT <= *value {
42        Ok(())
43    } else {
44        Err(ValidationError::new("stream open timeout must be at least 1 ms"))
45    }
46}
47
48/// Configuration of the per-peer egress stream layer.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Validate, smart_default::SmartDefault)]
50#[cfg_attr(
51    feature = "serde",
52    derive(serde::Serialize, serde::Deserialize),
53    serde(deny_unknown_fields)
54)]
55pub struct StreamProtocolConfig {
56    /// Capacity of the per-peer drop-oldest ring buffer (in packets).
57    ///
58    /// The egress drain is fully non-blocking: it enqueues each outgoing packet
59    /// via `try_send`. When the ring is full the oldest buffered packet is evicted
60    /// and the newest enqueued (drop-oldest). The ring absorbs bursts while a
61    /// stream is being opened; once open the write pump continuously drains it,
62    /// so the ring stays near-empty under normal load.
63    ///
64    /// Sized to absorb a typical SURB pre-fill burst (default SurbBalancer:
65    /// target 7 000 / max 5 000/s). If the producer consistently outruns the
66    /// underlying transport, older packets are dropped as intentional transport
67    /// loss.
68    ///
69    /// Defaults to 5 000.
70    #[validate(range(min = 1))]
71    #[default(default_per_peer_channel_capacity())]
72    #[cfg_attr(feature = "serde", serde(default = "default_per_peer_channel_capacity"))]
73    pub per_peer_channel_capacity: usize,
74
75    /// Timeout for the `NetworkStreamControl::open` call when opening a new
76    /// outgoing stream to a peer.
77    ///
78    /// A timeout is mandatory: without it a permanently-unreachable peer would park
79    /// the opener task indefinitely. When the open attempt fails or times out the
80    /// buffered packets for that peer are dropped and a debug-level log entry is
81    /// emitted. The cache entry is then invalidated so the next send triggers a
82    /// fresh open attempt.
83    ///
84    /// Must be at least 1 ms. Defaults to 2 seconds.
85    #[validate(custom(function = "validate_stream_open_timeout"))]
86    #[default(default_stream_open_timeout())]
87    #[cfg_attr(
88        feature = "serde",
89        serde(default = "default_stream_open_timeout", with = "humantime_serde")
90    )]
91    pub stream_open_timeout: Duration,
92
93    /// Pending-write-buffer byte threshold on the framed writer before a flush is forced.
94    ///
95    /// A value of `1` flushes on every encoded frame (one syscall per message).
96    /// Larger values coalesce adjacent small frames into a single quinn write call,
97    /// reducing connection-mutex acquisitions and driver wake-ups on the hot path.
98    /// A HOPR packet is ~1 440 bytes; at the default 128 KiB threshold roughly 91
99    /// packets are coalesced per write, cutting driver wake frequency ~30×.
100    ///
101    /// Defaults to 131 072 bytes (128 KiB).
102    #[validate(range(min = 1))]
103    #[default(default_frame_writer_backpressure_bytes())]
104    #[cfg_attr(feature = "serde", serde(default = "default_frame_writer_backpressure_bytes"))]
105    pub frame_writer_backpressure_bytes: usize,
106}
107
108fn default_counter_flush_interval() -> Duration {
109    DEFAULT_COUNTER_FLUSH_INTERVAL
110}
111
112/// Complete configuration of the HOPR protocol stack.
113#[derive(Debug, smart_default::SmartDefault, Validate, Clone, PartialEq)]
114#[cfg_attr(
115    feature = "serde",
116    derive(serde::Serialize, serde::Deserialize),
117    serde(deny_unknown_fields)
118)]
119pub struct HoprProtocolConfig {
120    /// Libp2p-related transport configuration
121    #[validate(nested)]
122    #[cfg_attr(feature = "serde", serde(default))]
123    pub transport: TransportConfig,
124    /// HOPR packet pipeline configuration
125    #[validate(nested)]
126    #[cfg_attr(feature = "serde", serde(default))]
127    pub packet: HoprPacketPipelineConfig,
128    /// Probing protocol configuration
129    #[validate(nested)]
130    #[cfg_attr(feature = "serde", serde(default))]
131    pub probe: ProbeConfig,
132    /// Session protocol global configuration
133    #[validate(nested)]
134    #[cfg_attr(feature = "serde", serde(default))]
135    pub session: SessionGlobalConfig,
136    /// Mixer configuration.
137    #[cfg_attr(feature = "serde", serde(default))]
138    pub mixer: MixerConfig,
139    /// Per-peer egress stream configuration
140    #[validate(nested)]
141    #[cfg_attr(feature = "serde", serde(default))]
142    pub stream: StreamProtocolConfig,
143    /// Path planner configuration
144    #[validate(nested)]
145    #[cfg_attr(feature = "serde", serde(skip))]
146    pub path_planner: crate::path::PathPlannerConfig,
147    /// Interval at which per-peer protocol conformance counters are flushed
148    /// into the network graph.
149    ///
150    /// Default is 15 seconds.
151    #[default(default_counter_flush_interval())]
152    #[cfg_attr(
153        feature = "serde",
154        serde(default = "default_counter_flush_interval", with = "humantime_serde")
155    )]
156    pub counter_flush_interval: Duration,
157}
158
159/// Configuration of the HOPR packet pipeline.
160#[derive(Clone, Copy, Debug, PartialEq, Validate, smart_default::SmartDefault)]
161#[cfg_attr(
162    feature = "serde",
163    derive(serde::Serialize, serde::Deserialize),
164    serde(deny_unknown_fields)
165)]
166pub struct HoprPacketPipelineConfig {
167    /// HOPR packet codec configuration
168    #[validate(nested)]
169    #[cfg_attr(feature = "serde", serde(default))]
170    pub codec: HoprCodecConfig,
171    /// Configuration of unacknowledged tickets processing.
172    #[validate(nested)]
173    #[cfg_attr(feature = "serde", serde(default))]
174    pub ack_processor: HoprUnacknowledgedTicketProcessorConfig,
175    /// Single Use Reply Block (SURB) handling configuration
176    #[validate(nested)]
177    #[cfg_attr(feature = "serde", serde(default))]
178    pub surb_store: SurbStoreConfig,
179    /// Packet pipeline configuration controlling output/input concurrency and acknowledgement processing
180    #[validate(nested)]
181    #[cfg_attr(feature = "serde", serde(default))]
182    pub pipeline: PacketPipelineConfig,
183}
184
185regex!(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]$");
186
187/// Check whether the string looks like a valid domain.
188#[inline]
189pub fn looks_like_domain(s: &str) -> bool {
190    is_dns_address_regex(s)
191}
192
193/// Check whether the string is an actual reachable domain.
194pub fn is_reachable_domain(host: &str) -> bool {
195    host.to_socket_addrs().is_ok_and(|i| i.into_iter().next().is_some())
196}
197
198/// Enumeration of possible host types.
199#[derive(Debug, Clone, PartialEq)]
200#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
201pub enum HostType {
202    /// IPv4 based host
203    IPv4(String),
204    /// DNS based host
205    Domain(String),
206}
207
208impl validator::Validate for HostType {
209    fn validate(&self) -> Result<(), ValidationErrors> {
210        match &self {
211            HostType::IPv4(ip4) => validate_ipv4_address(ip4).map_err(|e| {
212                let mut errs = ValidationErrors::new();
213                errs.add("ipv4", e);
214                errs
215            }),
216            HostType::Domain(domain) => validate_dns_address(domain).map_err(|e| {
217                let mut errs = ValidationErrors::new();
218                errs.add("domain", e);
219                errs
220            }),
221        }
222    }
223}
224
225impl Default for HostType {
226    fn default() -> Self {
227        HostType::IPv4("127.0.0.1".to_owned())
228    }
229}
230
231/// Configuration of the listening host.
232///
233/// This is used for the P2P and REST API listeners.
234///
235/// Intentionally has no default because it depends on the use case.
236#[derive(Debug, Validate, Clone, PartialEq)]
237#[cfg_attr(
238    feature = "serde",
239    derive(serde::Serialize, serde::Deserialize),
240    serde(deny_unknown_fields)
241)]
242pub struct HostConfig {
243    /// Host on which to listen
244    #[cfg_attr(feature = "serde", serde(default))]
245    pub address: HostType,
246    /// Listening TCP or UDP port (mandatory).
247    #[validate(range(min = 1u16))]
248    #[cfg_attr(feature = "serde", serde(default))]
249    pub port: u16,
250}
251
252impl FromStr for HostConfig {
253    type Err = String;
254
255    fn from_str(s: &str) -> Result<Self, Self::Err> {
256        let (ip_or_dns, str_port) = match s.split_once(':') {
257            None => return Err("Invalid host, is not in the '<host>:<port>' format".into()),
258            Some(split) => split,
259        };
260
261        let port = str_port.parse().map_err(|e: ParseIntError| e.to_string())?;
262
263        if validator::ValidateIp::validate_ipv4(&ip_or_dns) {
264            Ok(Self {
265                address: HostType::IPv4(ip_or_dns.to_owned()),
266                port,
267            })
268        } else if looks_like_domain(ip_or_dns) {
269            Ok(Self {
270                address: HostType::Domain(ip_or_dns.to_owned()),
271                port,
272            })
273        } else {
274            Err("Not a valid IPv4 or domain host".into())
275        }
276    }
277}
278
279impl Display for HostConfig {
280    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
281        write!(f, "{:?}:{}", self.address, self.port)
282    }
283}
284
285fn default_multiaddr_transport(port: u16) -> String {
286    cfg_if::cfg_if! {
287        if #[cfg(feature = "p2p-announce-quic")] {
288            // In case we run on a Dappnode-like device, presumably behind NAT, we fall back to TCP
289            // to circumvent issues with QUIC in such environments. To make this work reliably,
290            // we would need proper NAT traversal support.
291            let on_dappnode = std::env::var("DAPPNODE")
292                .map(|v| v.to_lowercase() == "true")
293                .unwrap_or(false);
294
295            // Using HOPRD_NAT a user can overwrite the default behaviour even on a Dappnode-like device
296            let uses_nat = std::env::var("HOPRD_NAT")
297                .map(|v| v.to_lowercase() == "true")
298                .unwrap_or(on_dappnode);
299
300            if uses_nat {
301                format!("tcp/{port}")
302            } else {
303                format!("udp/{port}/quic-v1")
304            }
305        } else {
306            format!("tcp/{port}")
307        }
308    }
309}
310
311impl TryFrom<&HostConfig> for Multiaddr {
312    type Error = HoprTransportError;
313
314    fn try_from(value: &HostConfig) -> Result<Self, Self::Error> {
315        match &value.address {
316            HostType::IPv4(ip) => Multiaddr::from_str(
317                format!("/ip4/{}/{}", ip.as_str(), default_multiaddr_transport(value.port)).as_str(),
318            )
319            .map_err(|e| HoprTransportError::Api(e.to_string())),
320            HostType::Domain(domain) => Multiaddr::from_str(
321                format!("/dns4/{}/{}", domain.as_str(), default_multiaddr_transport(value.port)).as_str(),
322            )
323            .map_err(|e| HoprTransportError::Api(e.to_string())),
324        }
325    }
326}
327
328fn validate_ipv4_address(s: &str) -> Result<(), ValidationError> {
329    if validator::ValidateIp::validate_ipv4(&s) {
330        let ipv4 = std::net::Ipv4Addr::from_str(s)
331            .map_err(|_| ValidationError::new("Failed to deserialize the string into an ipv4 address"))?;
332
333        if ipv4.is_private() || ipv4.is_multicast() || ipv4.is_unspecified() {
334            return Err(ValidationError::new(
335                "IPv4 cannot be private, multicast or unspecified (0.0.0.0)",
336            ))?;
337        }
338        Ok(())
339    } else {
340        Err(ValidationError::new("Invalid IPv4 address provided"))
341    }
342}
343
344fn validate_dns_address(s: &str) -> Result<(), ValidationError> {
345    if looks_like_domain(s) || is_reachable_domain(s) {
346        Ok(())
347    } else {
348        Err(ValidationError::new("Invalid DNS address provided"))
349    }
350}
351
352/// Configuration of the physical transport mechanism.
353#[derive(Debug, Default, Validate, Clone, Copy, PartialEq)]
354#[cfg_attr(
355    feature = "serde",
356    derive(serde::Serialize, serde::Deserialize),
357    serde(deny_unknown_fields)
358)]
359pub struct TransportConfig {
360    /// When true, assume that the node is running in an isolated network and does
361    /// not need any connection to nodes outside the subnet
362    #[cfg_attr(feature = "serde", serde(default))]
363    pub announce_local_addresses: bool,
364    /// When true, assume a testnet with multiple nodes running on the same machine
365    /// or in the same private IPv4 network
366    #[cfg_attr(feature = "serde", serde(default))]
367    pub prefer_local_addresses: bool,
368}
369
370const DEFAULT_SESSION_IDLE_TIMEOUT: Duration = Duration::from_mins(3);
371
372const SESSION_IDLE_MIN_TIMEOUT: Duration = Duration::from_secs(2);
373
374const DEFAULT_SESSION_ESTABLISH_RETRY_DELAY: Duration = Duration::from_secs(2);
375
376const DEFAULT_SESSION_ESTABLISH_MAX_RETRIES: usize = 3;
377
378const DEFAULT_SESSION_BALANCER_SAMPLING: Duration = Duration::from_millis(100);
379
380const DEFAULT_SESSION_BALANCER_BUFFER_DURATION: Duration = Duration::from_secs(5);
381
382const DEFAULT_MAXIMUM_MANAGED_SESSIONS: usize = 100;
383
384fn default_session_balancer_buffer_duration() -> Duration {
385    DEFAULT_SESSION_BALANCER_BUFFER_DURATION
386}
387
388fn default_session_establish_max_retries() -> usize {
389    DEFAULT_SESSION_ESTABLISH_MAX_RETRIES
390}
391
392fn default_session_idle_timeout() -> Duration {
393    DEFAULT_SESSION_IDLE_TIMEOUT
394}
395
396fn default_session_establish_retry_delay() -> Duration {
397    DEFAULT_SESSION_ESTABLISH_RETRY_DELAY
398}
399
400fn default_session_balancer_sampling() -> Duration {
401    DEFAULT_SESSION_BALANCER_SAMPLING
402}
403
404fn default_max_managed_sessions() -> usize {
405    DEFAULT_MAXIMUM_MANAGED_SESSIONS
406}
407
408fn default_session_surb_balance_notify_period() -> Option<Duration> {
409    Some(Duration::from_secs(60))
410}
411
412fn validate_session_idle_timeout(value: &Duration) -> Result<(), ValidationError> {
413    if SESSION_IDLE_MIN_TIMEOUT <= *value {
414        Ok(())
415    } else {
416        Err(ValidationError::new("session idle timeout is too low"))
417    }
418}
419
420fn validate_balancer_sampling(value: &Duration) -> Result<(), ValidationError> {
421    if MIN_BALANCER_SAMPLING_INTERVAL <= *value {
422        Ok(())
423    } else {
424        Err(ValidationError::new("balancer sampling interval is too low"))
425    }
426}
427
428fn validate_balancer_buffer_duration(value: &Duration) -> Result<(), ValidationError> {
429    if MIN_SURB_BUFFER_DURATION <= *value {
430        Ok(())
431    } else {
432        Err(ValidationError::new("minmum SURB buffer duration is too low"))
433    }
434}
435
436fn validate_surb_balance_notify_period(value: &Duration) -> Result<(), ValidationError> {
437    // `custom` on an `Option` field skips `None` and passes the inner value on `Some`.
438    if *value >= Duration::from_secs(1) {
439        Ok(())
440    } else {
441        Err(ValidationError::new(
442            "SURB balance notify period must be at least 1 second",
443        ))
444    }
445}
446
447/// Global configuration of Sessions and the Session manager.
448#[derive(Clone, Copy, Debug, PartialEq, Eq, Validate, smart_default::SmartDefault)]
449#[cfg_attr(
450    feature = "serde",
451    derive(serde::Serialize, serde::Deserialize),
452    serde(deny_unknown_fields)
453)]
454pub struct SessionGlobalConfig {
455    /// Maximum time before an idle Session is closed.
456    ///
457    /// Defaults to 3 minutes.
458    #[validate(custom(function = "validate_session_idle_timeout"))]
459    #[default(default_session_idle_timeout())]
460    #[cfg_attr(
461        feature = "serde",
462        serde(default = "default_session_idle_timeout", with = "humantime_serde")
463    )]
464    pub idle_timeout: Duration,
465
466    /// Maximum number of Sessions that can be managed by the Session manager.
467    ///
468    /// Default is 1000, minimum is 2, maximum is 100 000.
469    #[validate(range(min = 2, max = 100_000))]
470    #[default(default_max_managed_sessions())]
471    #[cfg_attr(feature = "serde", serde(default = "default_max_managed_sessions"))]
472    pub maximum_managed_sessions: usize,
473
474    /// Maximum retries to attempt to establish the Session
475    /// Set 0 for no retries.
476    ///
477    /// Defaults to 3, maximum is 20.
478    #[validate(range(min = 0, max = 20))]
479    #[default(default_session_establish_max_retries())]
480    #[cfg_attr(feature = "serde", serde(default = "default_session_establish_max_retries"))]
481    pub establish_max_retries: usize,
482
483    /// Delay between Session establishment retries.
484    ///
485    /// Default is 2 seconds.
486    #[default(default_session_establish_retry_delay())]
487    #[cfg_attr(
488        feature = "serde",
489        serde(default = "default_session_establish_retry_delay", with = "humantime_serde")
490    )]
491    pub establish_retry_timeout: Duration,
492
493    /// Sampling interval for SURB balancer in milliseconds.
494    ///
495    /// Default is 100 milliseconds.
496    #[validate(custom(function = "validate_balancer_sampling"))]
497    #[default(default_session_balancer_sampling())]
498    #[cfg_attr(
499        feature = "serde",
500        serde(default = "default_session_balancer_sampling", with = "humantime_serde")
501    )]
502    pub balancer_sampling_interval: Duration,
503
504    /// Minimum runway of received SURBs in seconds.
505    ///
506    /// This applies to incoming Sessions on Exit nodes only and is the main indicator of how
507    /// the egress traffic will be shaped, unless the `NoRateControl` Session
508    /// capability is specified during initiation.
509    ///
510    /// Default is 5 seconds, minimum is 1 second.
511    #[validate(custom(function = "validate_balancer_buffer_duration"))]
512    #[default(default_session_balancer_buffer_duration())]
513    #[cfg_attr(
514        feature = "serde",
515        serde(default = "default_session_balancer_buffer_duration", with = "humantime_serde")
516    )]
517    pub balancer_minimum_surb_buffer_duration: Duration,
518
519    /// How often the Exit reports its true SURB buffer level to the Entry, as an absolute
520    /// correction of the Entry's dead-reckoned estimate. Without it, cumulative packet loss
521    /// silently inflates the estimate until the Exit runs out of SURBs and can no longer
522    /// send reply data.
523    ///
524    /// Default is 60 seconds. Set to `null` to disable; minimum effective period is 1 second.
525    #[validate(custom(function = "validate_surb_balance_notify_period"))]
526    #[default(default_session_surb_balance_notify_period())]
527    #[cfg_attr(
528        feature = "serde",
529        serde(
530            default = "default_session_surb_balance_notify_period",
531            with = "humantime_serde::option"
532        )
533    )]
534    pub surb_balance_notify_period: Option<Duration>,
535
536    /// Tag allocator partition configuration.
537    #[validate(nested)]
538    #[cfg_attr(feature = "serde", serde(default))]
539    pub tag_allocator: hopr_transport_tag_allocator::TagAllocatorConfig,
540}
541
542#[cfg(test)]
543mod tests {
544    use super::*;
545
546    #[test]
547    fn test_valid_domains_for_looks_like_a_domain() {
548        assert!(looks_like_domain("localhost"));
549        assert!(looks_like_domain("hoprnet.org"));
550        assert!(looks_like_domain("hub.hoprnet.org"));
551    }
552
553    #[test]
554    fn test_valid_domains_for_does_not_look_like_a_domain() {
555        assert!(!looks_like_domain(".org"));
556        assert!(!looks_like_domain("-hoprnet-.org"));
557    }
558
559    #[test]
560    fn test_valid_domains_should_be_reachable() {
561        assert!(!is_reachable_domain("google.com"));
562    }
563
564    #[test]
565    fn test_verify_valid_ip4_addresses() {
566        assert!(validate_ipv4_address("1.1.1.1").is_ok());
567        assert!(validate_ipv4_address("1.255.1.1").is_ok());
568        assert!(validate_ipv4_address("187.1.1.255").is_ok());
569        assert!(validate_ipv4_address("127.0.0.1").is_ok());
570    }
571
572    #[test]
573    fn test_verify_invalid_ip4_addresses() {
574        assert!(validate_ipv4_address("1.256.1.1").is_err());
575        assert!(validate_ipv4_address("-1.1.1.255").is_err());
576        assert!(validate_ipv4_address("127.0.0.256").is_err());
577        assert!(validate_ipv4_address("1").is_err());
578        assert!(validate_ipv4_address("1.1").is_err());
579        assert!(validate_ipv4_address("1.1.1").is_err());
580        assert!(validate_ipv4_address("1.1.1.1.1").is_err());
581    }
582
583    #[test]
584    fn test_verify_valid_dns_addresses() {
585        assert!(validate_dns_address("localhost").is_ok());
586        assert!(validate_dns_address("google.com").is_ok());
587        assert!(validate_dns_address("hub.hoprnet.org").is_ok());
588    }
589
590    #[test]
591    fn test_verify_invalid_dns_addresses() {
592        assert!(validate_dns_address("-hoprnet-.org").is_err());
593    }
594
595    #[test]
596    fn test_multiaddress_on_dappnode_default() {
597        temp_env::with_var("DAPPNODE", Some("true"), || {
598            assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
599        });
600    }
601
602    #[cfg(feature = "p2p-announce-quic")]
603    #[test]
604    fn test_multiaddress_on_non_dappnode_default() {
605        temp_env::with_vars([("DAPPNODE", Some("false")), ("HOPRD_NAT", Some("false"))], || {
606            assert_eq!(default_multiaddr_transport(1234), "udp/1234/quic-v1");
607        });
608    }
609
610    #[cfg(not(feature = "p2p-announce-quic"))]
611    #[test]
612    fn test_multiaddress_on_non_dappnode_default() {
613        assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
614    }
615
616    #[test]
617    fn test_multiaddress_on_non_dappnode_uses_nat() {
618        temp_env::with_var("HOPRD_NAT", Some("true"), || {
619            assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
620        });
621    }
622
623    #[cfg(feature = "p2p-announce-quic")]
624    #[test]
625    fn test_multiaddress_on_non_dappnode_not_uses_nat() {
626        temp_env::with_var("HOPRD_NAT", Some("false"), || {
627            assert_eq!(default_multiaddr_transport(1234), "udp/1234/quic-v1");
628        });
629    }
630
631    #[cfg(not(feature = "p2p-announce-quic"))]
632    #[test]
633    fn test_multiaddress_on_non_dappnode_not_uses_nat() {
634        temp_env::with_var("HOPRD_NAT", Some("false"), || {
635            assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
636        });
637    }
638
639    #[cfg(feature = "p2p-announce-quic")]
640    #[test]
641    fn test_multiaddress_on_dappnode_not_uses_nat() {
642        temp_env::with_vars([("DAPPNODE", Some("true")), ("HOPRD_NAT", Some("false"))], || {
643            assert_eq!(default_multiaddr_transport(1234), "udp/1234/quic-v1");
644        });
645    }
646
647    #[cfg(not(feature = "p2p-announce-quic"))]
648    #[test]
649    fn test_multiaddress_on_dappnode_not_uses_nat() {
650        temp_env::with_vars([("DAPPNODE", Some("true")), ("HOPRD_NAT", Some("false"))], || {
651            assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
652        });
653    }
654
655    // --- HostConfig::FromStr tests ---
656
657    #[test]
658    fn host_config_parses_ipv4_address() {
659        let cfg = HostConfig::from_str("1.2.3.4:9091").unwrap();
660        insta::assert_debug_snapshot!(cfg);
661    }
662
663    #[test]
664    fn host_config_parses_domain() {
665        let cfg = HostConfig::from_str("example.com:443").unwrap();
666        insta::assert_debug_snapshot!(cfg);
667    }
668
669    #[test]
670    fn host_config_rejects_missing_port() {
671        assert!(HostConfig::from_str("1.2.3.4").is_err());
672    }
673
674    #[test]
675    fn host_config_rejects_invalid_port() {
676        assert!(HostConfig::from_str("1.2.3.4:abc").is_err());
677    }
678
679    #[test]
680    fn host_config_rejects_invalid_host() {
681        assert!(HostConfig::from_str("-invalid-.com:80").is_err());
682    }
683
684    #[test]
685    fn host_config_display_roundtrip() {
686        let cfg = HostConfig {
687            address: HostType::IPv4("10.0.0.1".into()),
688            port: 8080,
689        };
690        insta::assert_yaml_snapshot!(cfg.to_string());
691    }
692
693    // --- TryFrom<&HostConfig> for Multiaddr tests ---
694
695    #[test]
696    fn multiaddr_from_ipv4_host_config() {
697        let cfg = HostConfig {
698            address: HostType::IPv4("1.2.3.4".into()),
699            port: 9091,
700        };
701        let addr = Multiaddr::try_from(&cfg).unwrap();
702        insta::assert_yaml_snapshot!(addr.to_string());
703    }
704
705    #[test]
706    fn multiaddr_from_domain_host_config() {
707        let cfg = HostConfig {
708            address: HostType::Domain("example.com".into()),
709            port: 443,
710        };
711        let addr = Multiaddr::try_from(&cfg).unwrap();
712        insta::assert_yaml_snapshot!(addr.to_string());
713    }
714
715    // --- SessionGlobalConfig validation tests ---
716
717    #[test]
718    fn session_global_config_default_is_valid() {
719        let cfg = SessionGlobalConfig::default();
720        assert!(cfg.validate().is_ok());
721    }
722
723    #[test]
724    fn session_global_config_too_low_idle_timeout_is_rejected() {
725        let cfg = SessionGlobalConfig {
726            idle_timeout: Duration::from_millis(100),
727            ..Default::default()
728        };
729        assert!(cfg.validate().is_err());
730    }
731
732    #[test]
733    fn session_global_config_too_many_retries_is_rejected() {
734        let cfg = SessionGlobalConfig {
735            establish_max_retries: 21,
736            ..Default::default()
737        };
738        assert!(cfg.validate().is_err());
739    }
740
741    #[test]
742    fn stream_protocol_config_default_has_expected_values() {
743        let cfg = StreamProtocolConfig::default();
744        assert_eq!(cfg.per_peer_channel_capacity, DEFAULT_PER_PEER_CHANNEL_CAPACITY);
745        assert_eq!(cfg.stream_open_timeout, DEFAULT_STREAM_OPEN_TIMEOUT);
746        assert_eq!(
747            cfg.frame_writer_backpressure_bytes,
748            DEFAULT_FRAME_WRITER_BACKPRESSURE_BYTES
749        );
750        cfg.validate().expect("default StreamProtocolConfig must be valid");
751    }
752
753    #[test]
754    fn stream_protocol_config_zero_capacity_is_rejected() {
755        let cfg = StreamProtocolConfig {
756            per_peer_channel_capacity: 0,
757            ..Default::default()
758        };
759        assert!(cfg.validate().is_err());
760    }
761
762    #[test]
763    fn stream_protocol_config_zero_backpressure_bytes_is_rejected() {
764        let cfg = StreamProtocolConfig {
765            frame_writer_backpressure_bytes: 0,
766            ..Default::default()
767        };
768        assert!(cfg.validate().is_err());
769    }
770
771    #[test]
772    fn stream_protocol_config_zero_stream_open_timeout_is_rejected() {
773        let cfg = StreamProtocolConfig {
774            stream_open_timeout: Duration::ZERO,
775            ..Default::default()
776        };
777        assert!(cfg.validate().is_err());
778    }
779}