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