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_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/// Complete configuration of the HOPR protocol stack.
26#[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    /// Libp2p-related transport configuration
34    #[validate(nested)]
35    #[cfg_attr(feature = "serde", serde(default))]
36    pub transport: TransportConfig,
37    /// HOPR packet pipeline configuration
38    #[validate(nested)]
39    #[cfg_attr(feature = "serde", serde(default))]
40    pub packet: HoprPacketPipelineConfig,
41    /// Probing protocol configuration
42    #[validate(nested)]
43    #[cfg_attr(feature = "serde", serde(default))]
44    pub probe: ProbeConfig,
45    /// Session protocol global configuration
46    #[validate(nested)]
47    #[cfg_attr(feature = "serde", serde(default))]
48    pub session: SessionGlobalConfig,
49    /// Path planner configuration
50    #[validate(nested)]
51    #[cfg_attr(feature = "serde", serde(skip))]
52    pub path_planner: hopr_transport_path::PathPlannerConfig,
53    /// Interval at which per-peer protocol conformance counters are flushed
54    /// into the network graph.
55    ///
56    /// Default is 15 seconds.
57    #[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/// Configuration of the HOPR packet pipeline.
66#[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    /// HOPR packet codec configuration
74    #[validate(nested)]
75    #[cfg_attr(feature = "serde", serde(default))]
76    pub codec: HoprCodecConfig,
77    /// Configuration of unacknowledged tickets processing.
78    #[validate(nested)]
79    #[cfg_attr(feature = "serde", serde(default))]
80    pub ack_processor: HoprUnacknowledgedTicketProcessorConfig,
81    /// Single Use Reply Block (SURB) handling configuration
82    #[validate(nested)]
83    #[cfg_attr(feature = "serde", serde(default))]
84    pub surb_store: SurbStoreConfig,
85    /// Packet pipeline configuration controlling output/input concurrency and acknowledgement processing
86    #[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/// Check whether the string looks like a valid domain.
94#[inline]
95pub fn looks_like_domain(s: &str) -> bool {
96    is_dns_address_regex(s)
97}
98
99/// Check whether the string is an actual reachable domain.
100pub 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/// Enumeration of possible host types.
105#[derive(Debug, Clone, PartialEq)]
106#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
107pub enum HostType {
108    /// IPv4 based host
109    IPv4(String),
110    /// DNS based host
111    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/// Configuration of the listening host.
138///
139/// This is used for the P2P and REST API listeners.
140///
141/// Intentionally has no default because it depends on the use case.
142#[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    /// Host on which to listen
150    #[cfg_attr(feature = "serde", serde(default))]
151    pub address: HostType,
152    /// Listening TCP or UDP port (mandatory).
153    #[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            // In case we run on a Dappnode-like device, presumably behind NAT, we fall back to TCP
195            // to circumvent issues with QUIC in such environments. To make this work reliably,
196            // we would need proper NAT traversal support.
197            let on_dappnode = std::env::var("DAPPNODE")
198                .map(|v| v.to_lowercase() == "true")
199                .unwrap_or(false);
200
201            // Using HOPRD_NAT a user can overwrite the default behaviour even on a Dappnode-like device
202            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/// Configuration of the physical transport mechanism.
259#[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    /// When true, assume that the node is running in an isolated network and does
267    /// not need any connection to nodes outside the subnet
268    #[cfg_attr(feature = "serde", serde(default))]
269    pub announce_local_addresses: bool,
270    /// When true, assume a testnet with multiple nodes running on the same machine
271    /// or in the same private IPv4 network
272    #[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/// Global configuration of Sessions and the Session manager.
333#[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    /// Maximum time before an idle Session is closed.
341    ///
342    /// Defaults to 3 minutes.
343    #[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    /// Maximum retries to attempt to establish the Session
352    /// Set 0 for no retries.
353    ///
354    /// Defaults to 3, maximum is 20.
355    #[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    /// Delay between Session establishment retries.
361    ///
362    /// Default is 2 seconds.
363    #[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    /// Sampling interval for SURB balancer in milliseconds.
371    ///
372    /// Default is 100 milliseconds.
373    #[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    /// Minimum runway of received SURBs in seconds.
382    ///
383    /// This applies to incoming Sessions on Exit nodes only and is the main indicator of how
384    /// the egress traffic will be shaped, unless the `NoRateControl` Session
385    /// capability is specified during initiation.
386    ///
387    /// Default is 5 seconds, minimum is 1 second.
388    #[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    /// Tag allocator partition configuration.
397    #[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    // --- HostConfig::FromStr tests ---
516
517    #[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    // --- TryFrom<&HostConfig> for Multiaddr tests ---
554
555    #[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    // --- SessionGlobalConfig validation tests ---
576
577    #[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}