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_probe::config::ProbeConfig;
12use hopr_transport_protocol::PacketPipelineConfig;
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;
18
19const DEFAULT_COUNTER_FLUSH_INTERVAL: Duration = Duration::from_secs(15);
20
21fn default_counter_flush_interval() -> Duration {
22 DEFAULT_COUNTER_FLUSH_INTERVAL
23}
24
25#[derive(Debug, smart_default::SmartDefault, Validate, Clone, PartialEq)]
27#[cfg_attr(
28 feature = "serde",
29 derive(serde::Serialize, serde::Deserialize),
30 serde(deny_unknown_fields)
31)]
32pub struct HoprProtocolConfig {
33 #[validate(nested)]
35 #[cfg_attr(feature = "serde", serde(default))]
36 pub transport: TransportConfig,
37 #[validate(nested)]
39 #[cfg_attr(feature = "serde", serde(default))]
40 pub packet: HoprPacketPipelineConfig,
41 #[validate(nested)]
43 #[cfg_attr(feature = "serde", serde(default))]
44 pub probe: ProbeConfig,
45 #[validate(nested)]
47 #[cfg_attr(feature = "serde", serde(default))]
48 pub session: SessionGlobalConfig,
49 #[cfg_attr(feature = "serde", serde(skip))]
51 pub path_planner: hopr_transport_path::PathPlannerConfig,
52 #[default(default_counter_flush_interval())]
57 #[cfg_attr(
58 feature = "serde",
59 serde(default = "default_counter_flush_interval", with = "humantime_serde")
60 )]
61 pub counter_flush_interval: Duration,
62}
63
64#[derive(Clone, Copy, Debug, PartialEq, Validate, smart_default::SmartDefault)]
66#[cfg_attr(
67 feature = "serde",
68 derive(serde::Serialize, serde::Deserialize),
69 serde(deny_unknown_fields)
70)]
71pub struct HoprPacketPipelineConfig {
72 #[validate(nested)]
74 #[cfg_attr(feature = "serde", serde(default))]
75 pub codec: HoprCodecConfig,
76 #[validate(nested)]
78 #[cfg_attr(feature = "serde", serde(default))]
79 pub ack_processor: HoprUnacknowledgedTicketProcessorConfig,
80 #[validate(nested)]
82 #[cfg_attr(feature = "serde", serde(default))]
83 pub surb_store: SurbStoreConfig,
84 #[validate(nested)]
86 #[cfg_attr(feature = "serde", serde(default))]
87 pub pipeline: PacketPipelineConfig,
88}
89
90regex!(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]$");
91
92#[inline]
94pub fn looks_like_domain(s: &str) -> bool {
95 is_dns_address_regex(s)
96}
97
98pub fn is_reachable_domain(host: &str) -> bool {
100 host.to_socket_addrs().is_ok_and(|i| i.into_iter().next().is_some())
101}
102
103#[derive(Debug, Clone, PartialEq)]
105#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
106pub enum HostType {
107 IPv4(String),
109 Domain(String),
111}
112
113impl validator::Validate for HostType {
114 fn validate(&self) -> Result<(), ValidationErrors> {
115 match &self {
116 HostType::IPv4(ip4) => validate_ipv4_address(ip4).map_err(|e| {
117 let mut errs = ValidationErrors::new();
118 errs.add("ipv4", e);
119 errs
120 }),
121 HostType::Domain(domain) => validate_dns_address(domain).map_err(|e| {
122 let mut errs = ValidationErrors::new();
123 errs.add("domain", e);
124 errs
125 }),
126 }
127 }
128}
129
130impl Default for HostType {
131 fn default() -> Self {
132 HostType::IPv4("127.0.0.1".to_owned())
133 }
134}
135
136#[derive(Debug, Validate, Clone, PartialEq)]
142#[cfg_attr(
143 feature = "serde",
144 derive(serde::Serialize, serde::Deserialize),
145 serde(deny_unknown_fields)
146)]
147pub struct HostConfig {
148 #[cfg_attr(feature = "serde", serde(default))]
150 pub address: HostType,
151 #[validate(range(min = 1u16))]
153 #[cfg_attr(feature = "serde", serde(default))]
154 pub port: u16,
155}
156
157impl FromStr for HostConfig {
158 type Err = String;
159
160 fn from_str(s: &str) -> Result<Self, Self::Err> {
161 let (ip_or_dns, str_port) = match s.split_once(':') {
162 None => return Err("Invalid host, is not in the '<host>:<port>' format".into()),
163 Some(split) => split,
164 };
165
166 let port = str_port.parse().map_err(|e: ParseIntError| e.to_string())?;
167
168 if validator::ValidateIp::validate_ipv4(&ip_or_dns) {
169 Ok(Self {
170 address: HostType::IPv4(ip_or_dns.to_owned()),
171 port,
172 })
173 } else if looks_like_domain(ip_or_dns) {
174 Ok(Self {
175 address: HostType::Domain(ip_or_dns.to_owned()),
176 port,
177 })
178 } else {
179 Err("Not a valid IPv4 or domain host".into())
180 }
181 }
182}
183
184impl Display for HostConfig {
185 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
186 write!(f, "{:?}:{}", self.address, self.port)
187 }
188}
189
190fn default_multiaddr_transport(port: u16) -> String {
191 cfg_if::cfg_if! {
192 if #[cfg(feature = "p2p-announce-quic")] {
193 let on_dappnode = std::env::var("DAPPNODE")
197 .map(|v| v.to_lowercase() == "true")
198 .unwrap_or(false);
199
200 let uses_nat = std::env::var("HOPRD_NAT")
202 .map(|v| v.to_lowercase() == "true")
203 .unwrap_or(on_dappnode);
204
205 if uses_nat {
206 format!("tcp/{port}")
207 } else {
208 format!("udp/{port}/quic-v1")
209 }
210 } else {
211 format!("tcp/{port}")
212 }
213 }
214}
215
216impl TryFrom<&HostConfig> for Multiaddr {
217 type Error = HoprTransportError;
218
219 fn try_from(value: &HostConfig) -> Result<Self, Self::Error> {
220 match &value.address {
221 HostType::IPv4(ip) => Multiaddr::from_str(
222 format!("/ip4/{}/{}", ip.as_str(), default_multiaddr_transport(value.port)).as_str(),
223 )
224 .map_err(|e| HoprTransportError::Api(e.to_string())),
225 HostType::Domain(domain) => Multiaddr::from_str(
226 format!("/dns4/{}/{}", domain.as_str(), default_multiaddr_transport(value.port)).as_str(),
227 )
228 .map_err(|e| HoprTransportError::Api(e.to_string())),
229 }
230 }
231}
232
233fn validate_ipv4_address(s: &str) -> Result<(), ValidationError> {
234 if validator::ValidateIp::validate_ipv4(&s) {
235 let ipv4 = std::net::Ipv4Addr::from_str(s)
236 .map_err(|_| ValidationError::new("Failed to deserialize the string into an ipv4 address"))?;
237
238 if ipv4.is_private() || ipv4.is_multicast() || ipv4.is_unspecified() {
239 return Err(ValidationError::new(
240 "IPv4 cannot be private, multicast or unspecified (0.0.0.0)",
241 ))?;
242 }
243 Ok(())
244 } else {
245 Err(ValidationError::new("Invalid IPv4 address provided"))
246 }
247}
248
249fn validate_dns_address(s: &str) -> Result<(), ValidationError> {
250 if looks_like_domain(s) || is_reachable_domain(s) {
251 Ok(())
252 } else {
253 Err(ValidationError::new("Invalid DNS address provided"))
254 }
255}
256
257#[derive(Debug, Default, Validate, Clone, Copy, PartialEq)]
259#[cfg_attr(
260 feature = "serde",
261 derive(serde::Serialize, serde::Deserialize),
262 serde(deny_unknown_fields)
263)]
264pub struct TransportConfig {
265 #[cfg_attr(feature = "serde", serde(default))]
268 pub announce_local_addresses: bool,
269 #[cfg_attr(feature = "serde", serde(default))]
272 pub prefer_local_addresses: bool,
273}
274
275const DEFAULT_SESSION_IDLE_TIMEOUT: Duration = Duration::from_mins(3);
276
277const SESSION_IDLE_MIN_TIMEOUT: Duration = Duration::from_secs(2);
278
279const DEFAULT_SESSION_ESTABLISH_RETRY_DELAY: Duration = Duration::from_secs(2);
280
281const DEFAULT_SESSION_ESTABLISH_MAX_RETRIES: u32 = 3;
282
283const DEFAULT_SESSION_BALANCER_SAMPLING: Duration = Duration::from_millis(100);
284
285const DEFAULT_SESSION_BALANCER_BUFFER_DURATION: Duration = Duration::from_secs(5);
286
287fn default_session_balancer_buffer_duration() -> Duration {
288 DEFAULT_SESSION_BALANCER_BUFFER_DURATION
289}
290
291fn default_session_establish_max_retries() -> u32 {
292 DEFAULT_SESSION_ESTABLISH_MAX_RETRIES
293}
294
295fn default_session_idle_timeout() -> Duration {
296 DEFAULT_SESSION_IDLE_TIMEOUT
297}
298
299fn default_session_establish_retry_delay() -> Duration {
300 DEFAULT_SESSION_ESTABLISH_RETRY_DELAY
301}
302
303fn default_session_balancer_sampling() -> Duration {
304 DEFAULT_SESSION_BALANCER_SAMPLING
305}
306
307fn validate_session_idle_timeout(value: &Duration) -> Result<(), ValidationError> {
308 if SESSION_IDLE_MIN_TIMEOUT <= *value {
309 Ok(())
310 } else {
311 Err(ValidationError::new("session idle timeout is too low"))
312 }
313}
314
315fn validate_balancer_sampling(value: &Duration) -> Result<(), ValidationError> {
316 if MIN_BALANCER_SAMPLING_INTERVAL <= *value {
317 Ok(())
318 } else {
319 Err(ValidationError::new("balancer sampling interval is too low"))
320 }
321}
322
323fn validate_balancer_buffer_duration(value: &Duration) -> Result<(), ValidationError> {
324 if MIN_SURB_BUFFER_DURATION <= *value {
325 Ok(())
326 } else {
327 Err(ValidationError::new("minmum SURB buffer duration is too low"))
328 }
329}
330
331#[derive(Clone, Copy, Debug, PartialEq, Eq, Validate, smart_default::SmartDefault)]
333#[cfg_attr(
334 feature = "serde",
335 derive(serde::Serialize, serde::Deserialize),
336 serde(deny_unknown_fields)
337)]
338pub struct SessionGlobalConfig {
339 #[validate(custom(function = "validate_session_idle_timeout"))]
343 #[default(default_session_idle_timeout())]
344 #[cfg_attr(
345 feature = "serde",
346 serde(default = "default_session_idle_timeout", with = "humantime_serde")
347 )]
348 pub idle_timeout: Duration,
349
350 #[validate(range(min = 0, max = 20))]
355 #[default(default_session_establish_max_retries())]
356 #[cfg_attr(feature = "serde", serde(default = "default_session_establish_max_retries"))]
357 pub establish_max_retries: u32,
358
359 #[default(default_session_establish_retry_delay())]
363 #[cfg_attr(
364 feature = "serde",
365 serde(default = "default_session_establish_retry_delay", with = "humantime_serde")
366 )]
367 pub establish_retry_timeout: Duration,
368
369 #[validate(custom(function = "validate_balancer_sampling"))]
373 #[default(default_session_balancer_sampling())]
374 #[cfg_attr(
375 feature = "serde",
376 serde(default = "default_session_balancer_sampling", with = "humantime_serde")
377 )]
378 pub balancer_sampling_interval: Duration,
379
380 #[validate(custom(function = "validate_balancer_buffer_duration"))]
388 #[default(default_session_balancer_buffer_duration())]
389 #[cfg_attr(
390 feature = "serde",
391 serde(default = "default_session_balancer_buffer_duration", with = "humantime_serde")
392 )]
393 pub balancer_minimum_surb_buffer_duration: Duration,
394
395 #[validate(nested)]
397 #[cfg_attr(feature = "serde", serde(default))]
398 pub tag_allocator: hopr_transport_tag_allocator::TagAllocatorConfig,
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 #[test]
406 fn test_valid_domains_for_looks_like_a_domain() {
407 assert!(looks_like_domain("localhost"));
408 assert!(looks_like_domain("hoprnet.org"));
409 assert!(looks_like_domain("hub.hoprnet.org"));
410 }
411
412 #[test]
413 fn test_valid_domains_for_does_not_look_like_a_domain() {
414 assert!(!looks_like_domain(".org"));
415 assert!(!looks_like_domain("-hoprnet-.org"));
416 }
417
418 #[test]
419 fn test_valid_domains_should_be_reachable() {
420 assert!(!is_reachable_domain("google.com"));
421 }
422
423 #[test]
424 fn test_verify_valid_ip4_addresses() {
425 assert!(validate_ipv4_address("1.1.1.1").is_ok());
426 assert!(validate_ipv4_address("1.255.1.1").is_ok());
427 assert!(validate_ipv4_address("187.1.1.255").is_ok());
428 assert!(validate_ipv4_address("127.0.0.1").is_ok());
429 }
430
431 #[test]
432 fn test_verify_invalid_ip4_addresses() {
433 assert!(validate_ipv4_address("1.256.1.1").is_err());
434 assert!(validate_ipv4_address("-1.1.1.255").is_err());
435 assert!(validate_ipv4_address("127.0.0.256").is_err());
436 assert!(validate_ipv4_address("1").is_err());
437 assert!(validate_ipv4_address("1.1").is_err());
438 assert!(validate_ipv4_address("1.1.1").is_err());
439 assert!(validate_ipv4_address("1.1.1.1.1").is_err());
440 }
441
442 #[test]
443 fn test_verify_valid_dns_addresses() {
444 assert!(validate_dns_address("localhost").is_ok());
445 assert!(validate_dns_address("google.com").is_ok());
446 assert!(validate_dns_address("hub.hoprnet.org").is_ok());
447 }
448
449 #[test]
450 fn test_verify_invalid_dns_addresses() {
451 assert!(validate_dns_address("-hoprnet-.org").is_err());
452 }
453
454 #[test]
455 fn test_multiaddress_on_dappnode_default() {
456 temp_env::with_var("DAPPNODE", Some("true"), || {
457 assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
458 });
459 }
460
461 #[cfg(feature = "p2p-announce-quic")]
462 #[test]
463 fn test_multiaddress_on_non_dappnode_default() {
464 temp_env::with_vars([("DAPPNODE", Some("false")), ("HOPRD_NAT", Some("false"))], || {
465 assert_eq!(default_multiaddr_transport(1234), "udp/1234/quic-v1");
466 });
467 }
468
469 #[cfg(not(feature = "p2p-announce-quic"))]
470 #[test]
471 fn test_multiaddress_on_non_dappnode_default() {
472 assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
473 }
474
475 #[test]
476 fn test_multiaddress_on_non_dappnode_uses_nat() {
477 temp_env::with_var("HOPRD_NAT", Some("true"), || {
478 assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
479 });
480 }
481
482 #[cfg(feature = "p2p-announce-quic")]
483 #[test]
484 fn test_multiaddress_on_non_dappnode_not_uses_nat() {
485 temp_env::with_var("HOPRD_NAT", Some("false"), || {
486 assert_eq!(default_multiaddr_transport(1234), "udp/1234/quic-v1");
487 });
488 }
489
490 #[cfg(not(feature = "p2p-announce-quic"))]
491 #[test]
492 fn test_multiaddress_on_non_dappnode_not_uses_nat() {
493 temp_env::with_var("HOPRD_NAT", Some("false"), || {
494 assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
495 });
496 }
497
498 #[cfg(feature = "p2p-announce-quic")]
499 #[test]
500 fn test_multiaddress_on_dappnode_not_uses_nat() {
501 temp_env::with_vars([("DAPPNODE", Some("true")), ("HOPRD_NAT", Some("false"))], || {
502 assert_eq!(default_multiaddr_transport(1234), "udp/1234/quic-v1");
503 });
504 }
505
506 #[cfg(not(feature = "p2p-announce-quic"))]
507 #[test]
508 fn test_multiaddress_on_dappnode_not_uses_nat() {
509 temp_env::with_vars([("DAPPNODE", Some("true")), ("HOPRD_NAT", Some("false"))], || {
510 assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
511 });
512 }
513
514 #[test]
517 fn host_config_parses_ipv4_address() {
518 let cfg = HostConfig::from_str("1.2.3.4:9091").unwrap();
519 insta::assert_debug_snapshot!(cfg);
520 }
521
522 #[test]
523 fn host_config_parses_domain() {
524 let cfg = HostConfig::from_str("example.com:443").unwrap();
525 insta::assert_debug_snapshot!(cfg);
526 }
527
528 #[test]
529 fn host_config_rejects_missing_port() {
530 assert!(HostConfig::from_str("1.2.3.4").is_err());
531 }
532
533 #[test]
534 fn host_config_rejects_invalid_port() {
535 assert!(HostConfig::from_str("1.2.3.4:abc").is_err());
536 }
537
538 #[test]
539 fn host_config_rejects_invalid_host() {
540 assert!(HostConfig::from_str("-invalid-.com:80").is_err());
541 }
542
543 #[test]
544 fn host_config_display_roundtrip() {
545 let cfg = HostConfig {
546 address: HostType::IPv4("10.0.0.1".into()),
547 port: 8080,
548 };
549 insta::assert_yaml_snapshot!(cfg.to_string());
550 }
551
552 #[test]
555 fn multiaddr_from_ipv4_host_config() {
556 let cfg = HostConfig {
557 address: HostType::IPv4("1.2.3.4".into()),
558 port: 9091,
559 };
560 let addr = Multiaddr::try_from(&cfg).unwrap();
561 insta::assert_yaml_snapshot!(addr.to_string());
562 }
563
564 #[test]
565 fn multiaddr_from_domain_host_config() {
566 let cfg = HostConfig {
567 address: HostType::Domain("example.com".into()),
568 port: 443,
569 };
570 let addr = Multiaddr::try_from(&cfg).unwrap();
571 insta::assert_yaml_snapshot!(addr.to_string());
572 }
573
574 #[test]
577 fn session_global_config_default_is_valid() {
578 let cfg = SessionGlobalConfig::default();
579 assert!(cfg.validate().is_ok());
580 }
581
582 #[test]
583 fn session_global_config_too_low_idle_timeout_is_rejected() {
584 let cfg = SessionGlobalConfig {
585 idle_timeout: Duration::from_millis(100),
586 ..Default::default()
587 };
588 assert!(cfg.validate().is_err());
589 }
590
591 #[test]
592 fn session_global_config_too_many_retries_is_rejected() {
593 let cfg = SessionGlobalConfig {
594 establish_max_retries: 21,
595 ..Default::default()
596 };
597 assert!(cfg.validate().is_err());
598 }
599}