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