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
25pub 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#[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 #[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 #[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 #[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#[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 #[validate(nested)]
122 #[cfg_attr(feature = "serde", serde(default))]
123 pub transport: TransportConfig,
124 #[validate(nested)]
126 #[cfg_attr(feature = "serde", serde(default))]
127 pub packet: HoprPacketPipelineConfig,
128 #[validate(nested)]
130 #[cfg_attr(feature = "serde", serde(default))]
131 pub probe: ProbeConfig,
132 #[validate(nested)]
134 #[cfg_attr(feature = "serde", serde(default))]
135 pub session: SessionGlobalConfig,
136 #[cfg_attr(feature = "serde", serde(default))]
138 pub mixer: MixerConfig,
139 #[validate(nested)]
141 #[cfg_attr(feature = "serde", serde(default))]
142 pub stream: StreamProtocolConfig,
143 #[validate(nested)]
145 #[cfg_attr(feature = "serde", serde(skip))]
146 pub path_planner: crate::path::PathPlannerConfig,
147 #[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#[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 #[validate(nested)]
169 #[cfg_attr(feature = "serde", serde(default))]
170 pub codec: HoprCodecConfig,
171 #[validate(nested)]
173 #[cfg_attr(feature = "serde", serde(default))]
174 pub ack_processor: HoprUnacknowledgedTicketProcessorConfig,
175 #[validate(nested)]
177 #[cfg_attr(feature = "serde", serde(default))]
178 pub surb_store: SurbStoreConfig,
179 #[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#[inline]
189pub fn looks_like_domain(s: &str) -> bool {
190 is_dns_address_regex(s)
191}
192
193pub 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#[derive(Debug, Clone, PartialEq)]
200#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
201pub enum HostType {
202 IPv4(String),
204 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#[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 #[cfg_attr(feature = "serde", serde(default))]
245 pub address: HostType,
246 #[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 let on_dappnode = std::env::var("DAPPNODE")
292 .map(|v| v.to_lowercase() == "true")
293 .unwrap_or(false);
294
295 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#[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 #[cfg_attr(feature = "serde", serde(default))]
363 pub announce_local_addresses: bool,
364 #[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 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#[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}