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