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_transport_identity::Multiaddr;
10pub use hopr_transport_network::config::NetworkConfig;
11pub use hopr_transport_probe::config::ProbeConfig;
12pub use hopr_transport_protocol::config::ProtocolConfig;
13use hopr_transport_session::{MIN_BALANCER_SAMPLING_INTERVAL, MIN_SURB_BUFFER_DURATION};
14use proc_macro_regex::regex;
15use serde::{Deserialize, Serialize};
16use serde_with::serde_as;
17use validator::{Validate, ValidationError};
18
19use crate::errors::HoprTransportError;
20
21pub struct HoprTransportConfig {
22    pub transport: TransportConfig,
23    pub network: NetworkConfig,
24    pub protocol: ProtocolConfig,
25    pub probe: ProbeConfig,
26    pub session: SessionGlobalConfig,
27}
28
29regex!(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]$");
30
31/// Check whether the string looks like a valid domain.
32#[inline]
33pub fn looks_like_domain(s: &str) -> bool {
34    is_dns_address_regex(s)
35}
36
37/// Check whether the string is an actual reachable domain.
38pub fn is_reachable_domain(host: &str) -> bool {
39    host.to_socket_addrs().is_ok_and(|i| i.into_iter().next().is_some())
40}
41
42/// Enumeration of possible host types.
43#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
44pub enum HostType {
45    /// IPv4 based host
46    IPv4(String),
47    /// DNS based host
48    Domain(String),
49}
50
51impl Default for HostType {
52    fn default() -> Self {
53        HostType::IPv4("127.0.0.1".to_owned())
54    }
55}
56
57/// Configuration of the listening host.
58///
59/// This is used for the P2P and REST API listeners.
60///
61/// Intentionally has no default because it depends on the use case.
62#[derive(Debug, Serialize, Deserialize, Validate, Clone, PartialEq)]
63#[serde(deny_unknown_fields)]
64pub struct HostConfig {
65    /// Host on which to listen
66    #[serde(default)] // must be defaulted to be mergeable from CLI args
67    pub address: HostType,
68    /// Listening TCP or UDP port (mandatory).
69    #[validate(range(min = 1u16))]
70    #[serde(default)] // must be defaulted to be mergeable from CLI args
71    pub port: u16,
72}
73
74impl FromStr for HostConfig {
75    type Err = String;
76
77    fn from_str(s: &str) -> Result<Self, Self::Err> {
78        let (ip_or_dns, str_port) = match s.split_once(':') {
79            None => return Err("Invalid host, is not in the '<host>:<port>' format".into()),
80            Some(split) => split,
81        };
82
83        let port = str_port.parse().map_err(|e: ParseIntError| e.to_string())?;
84
85        if validator::ValidateIp::validate_ipv4(&ip_or_dns) {
86            Ok(Self {
87                address: HostType::IPv4(ip_or_dns.to_owned()),
88                port,
89            })
90        } else if looks_like_domain(ip_or_dns) {
91            Ok(Self {
92                address: HostType::Domain(ip_or_dns.to_owned()),
93                port,
94            })
95        } else {
96            Err("Not a valid IPv4 or domain host".into())
97        }
98    }
99}
100
101impl Display for HostConfig {
102    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
103        write!(f, "{:?}:{}", self.address, self.port)
104    }
105}
106
107fn default_multiaddr_transport(port: u16) -> String {
108    cfg_if::cfg_if! {
109        if #[cfg(feature = "transport-quic")] {
110            // In case we run on a Dappnode-like device, presumably behind NAT, we fall back to TCP
111            // to circumvent issues with QUIC in such environments. To make this work reliably,
112            // we would need proper NAT traversal support.
113            let on_dappnode = std::env::var("DAPPNODE")
114                .map(|v| v.to_lowercase() == "true")
115                .unwrap_or(false);
116
117            // Using HOPRD_NAT a user can overwrite the default behaviour even on a Dappnode-like device
118            let uses_nat = std::env::var("HOPRD_NAT")
119                .map(|v| v.to_lowercase() == "true")
120                .unwrap_or(on_dappnode);
121
122            if uses_nat {
123                format!("tcp/{port}")
124            } else {
125                format!("udp/{port}/quic-v1")
126            }
127        } else {
128            format!("tcp/{port}")
129        }
130    }
131}
132
133impl TryFrom<&HostConfig> for Multiaddr {
134    type Error = HoprTransportError;
135
136    fn try_from(value: &HostConfig) -> Result<Self, Self::Error> {
137        match &value.address {
138            HostType::IPv4(ip) => Multiaddr::from_str(
139                format!("/ip4/{}/{}", ip.as_str(), default_multiaddr_transport(value.port)).as_str(),
140            )
141            .map_err(|e| HoprTransportError::Api(e.to_string())),
142            HostType::Domain(domain) => Multiaddr::from_str(
143                format!("/dns4/{}/{}", domain.as_str(), default_multiaddr_transport(value.port)).as_str(),
144            )
145            .map_err(|e| HoprTransportError::Api(e.to_string())),
146        }
147    }
148}
149
150fn validate_ipv4_address(s: &str) -> Result<(), ValidationError> {
151    if validator::ValidateIp::validate_ipv4(&s) {
152        let ipv4 = std::net::Ipv4Addr::from_str(s)
153            .map_err(|_| ValidationError::new("Failed to deserialize the string into an ipv4 address"))?;
154
155        if ipv4.is_private() || ipv4.is_multicast() || ipv4.is_unspecified() {
156            return Err(ValidationError::new(
157                "IPv4 cannot be private, multicast or unspecified (0.0.0.0)",
158            ))?;
159        }
160        Ok(())
161    } else {
162        Err(ValidationError::new("Invalid IPv4 address provided"))
163    }
164}
165
166fn validate_dns_address(s: &str) -> Result<(), ValidationError> {
167    if looks_like_domain(s) || is_reachable_domain(s) {
168        Ok(())
169    } else {
170        Err(ValidationError::new("Invalid DNS address provided"))
171    }
172}
173
174/// Validates the HostConfig to be used as an external host
175pub fn validate_external_host(host: &HostConfig) -> Result<(), ValidationError> {
176    match &host.address {
177        HostType::IPv4(ip4) => validate_ipv4_address(ip4),
178        HostType::Domain(domain) => validate_dns_address(domain),
179    }
180}
181
182/// Configuration of the physical transport mechanism.
183#[derive(Debug, Default, Serialize, Deserialize, Validate, Clone, PartialEq)]
184#[serde(deny_unknown_fields)]
185pub struct TransportConfig {
186    /// When true, assume that the node is running in an isolated network and does
187    /// not need any connection to nodes outside the subnet
188    #[serde(default)]
189    pub announce_local_addresses: bool,
190    /// When true, assume a testnet with multiple nodes running on the same machine
191    /// or in the same private IPv4 network
192    #[serde(default)]
193    pub prefer_local_addresses: bool,
194}
195
196const DEFAULT_SESSION_MAX_SESSIONS: u32 = 2048;
197
198const DEFAULT_SESSION_IDLE_TIMEOUT: Duration = Duration::from_secs(180);
199
200const SESSION_IDLE_MIN_TIMEOUT: Duration = Duration::from_secs(60);
201
202const DEFAULT_SESSION_ESTABLISH_RETRY_DELAY: Duration = Duration::from_secs(2);
203
204const DEFAULT_SESSION_ESTABLISH_MAX_RETRIES: u32 = 3;
205
206const DEFAULT_SESSION_BALANCER_SAMPLING: Duration = Duration::from_millis(500);
207
208const DEFAULT_SESSION_BALANCER_BUFFER_DURATION: Duration = Duration::from_millis(5000);
209
210fn default_session_max_sessions() -> u32 {
211    DEFAULT_SESSION_MAX_SESSIONS
212}
213
214fn default_session_balancer_buffer_duration() -> std::time::Duration {
215    DEFAULT_SESSION_BALANCER_BUFFER_DURATION
216}
217
218fn default_session_establish_max_retries() -> u32 {
219    DEFAULT_SESSION_ESTABLISH_MAX_RETRIES
220}
221
222fn default_session_idle_timeout() -> std::time::Duration {
223    DEFAULT_SESSION_IDLE_TIMEOUT
224}
225
226fn default_session_establish_retry_delay() -> std::time::Duration {
227    DEFAULT_SESSION_ESTABLISH_RETRY_DELAY
228}
229
230fn default_session_balancer_sampling() -> std::time::Duration {
231    DEFAULT_SESSION_BALANCER_SAMPLING
232}
233
234fn validate_session_idle_timeout(value: &std::time::Duration) -> Result<(), ValidationError> {
235    if SESSION_IDLE_MIN_TIMEOUT <= *value {
236        Ok(())
237    } else {
238        Err(ValidationError::new("session idle timeout is too low"))
239    }
240}
241
242fn validate_balancer_sampling(value: &std::time::Duration) -> Result<(), ValidationError> {
243    if MIN_BALANCER_SAMPLING_INTERVAL <= *value {
244        Ok(())
245    } else {
246        Err(ValidationError::new("balancer sampling interval is too low"))
247    }
248}
249
250fn validate_balancer_buffer_duration(value: &std::time::Duration) -> Result<(), ValidationError> {
251    if MIN_SURB_BUFFER_DURATION <= *value {
252        Ok(())
253    } else {
254        Err(ValidationError::new("minmum SURB buffer duration is too low"))
255    }
256}
257
258/// Global configuration of Sessions and the Session manager.
259#[serde_as]
260#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Validate, smart_default::SmartDefault)]
261#[serde(deny_unknown_fields)]
262pub struct SessionGlobalConfig {
263    /// Maximum time before an idle Session is closed.
264    ///
265    /// Defaults to 3 minutes.
266    #[validate(custom(function = "validate_session_idle_timeout"))]
267    #[default(DEFAULT_SESSION_IDLE_TIMEOUT)]
268    #[serde(default = "default_session_idle_timeout")]
269    #[serde_as(as = "serde_with::DurationSeconds<u64>")]
270    pub idle_timeout: std::time::Duration,
271
272    /// The maximum number of outgoing or incoming Sessions that
273    /// are allowed by the Session manager.
274    ///
275    /// Minimum is 1, the maximum is given by the Session tag range.
276    /// Default is 2048.
277    #[default(DEFAULT_SESSION_MAX_SESSIONS)]
278    #[serde(default = "default_session_max_sessions")]
279    #[validate(range(min = 1))]
280    pub maximum_sessions: u32,
281
282    /// Maximum retries to attempt to establish the Session
283    /// Set 0 for no retries.
284    ///
285    /// Defaults to 3, maximum is 20.
286    #[validate(range(min = 0, max = 20))]
287    #[default(DEFAULT_SESSION_ESTABLISH_MAX_RETRIES)]
288    #[serde(default = "default_session_establish_max_retries")]
289    pub establish_max_retries: u32,
290
291    /// Delay between Session establishment retries.
292    ///
293    /// Default is 2 seconds.
294    #[default(DEFAULT_SESSION_ESTABLISH_RETRY_DELAY)]
295    #[serde(default = "default_session_establish_retry_delay")]
296    #[serde_as(as = "serde_with::DurationSeconds<u64>")]
297    pub establish_retry_timeout: std::time::Duration,
298
299    /// Sampling interval for SURB balancer in milliseconds.
300    ///
301    /// Default is 500 milliseconds.
302    #[validate(custom(function = "validate_balancer_sampling"))]
303    #[default(DEFAULT_SESSION_BALANCER_SAMPLING)]
304    #[serde(default = "default_session_balancer_sampling")]
305    #[serde_as(as = "serde_with::DurationMilliSeconds<u64>")]
306    pub balancer_sampling_interval: std::time::Duration,
307
308    /// Minimum runway of received SURBs in seconds.
309    ///
310    /// This applies to incoming Sessions on Exit nodes only and is the main indicator of how
311    /// the egress traffic will be shaped, unless the `NoRateControl` Session
312    /// capability is specified during initiation.
313    ///
314    /// Default is 5 seconds, minimum is 1 second.
315    #[validate(custom(function = "validate_balancer_buffer_duration"))]
316    #[default(DEFAULT_SESSION_BALANCER_BUFFER_DURATION)]
317    #[serde(default = "default_session_balancer_buffer_duration")]
318    #[serde_as(as = "serde_with::DurationSeconds<u64>")]
319    pub balancer_minimum_surb_buffer_duration: std::time::Duration,
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    #[test]
327    fn test_valid_domains_for_looks_like_a_domain() {
328        assert!(looks_like_domain("localhost"));
329        assert!(looks_like_domain("hoprnet.org"));
330        assert!(looks_like_domain("hub.hoprnet.org"));
331    }
332
333    #[test]
334    fn test_valid_domains_for_does_not_look_like_a_domain() {
335        assert!(!looks_like_domain(".org"));
336        assert!(!looks_like_domain("-hoprnet-.org"));
337    }
338
339    #[test]
340    fn test_valid_domains_should_be_reachable() {
341        assert!(!is_reachable_domain("google.com"));
342    }
343
344    #[test]
345    fn test_verify_valid_ip4_addresses() {
346        assert!(validate_ipv4_address("1.1.1.1").is_ok());
347        assert!(validate_ipv4_address("1.255.1.1").is_ok());
348        assert!(validate_ipv4_address("187.1.1.255").is_ok());
349        assert!(validate_ipv4_address("127.0.0.1").is_ok());
350    }
351
352    #[test]
353    fn test_verify_invalid_ip4_addresses() {
354        assert!(validate_ipv4_address("1.256.1.1").is_err());
355        assert!(validate_ipv4_address("-1.1.1.255").is_err());
356        assert!(validate_ipv4_address("127.0.0.256").is_err());
357        assert!(validate_ipv4_address("1").is_err());
358        assert!(validate_ipv4_address("1.1").is_err());
359        assert!(validate_ipv4_address("1.1.1").is_err());
360        assert!(validate_ipv4_address("1.1.1.1.1").is_err());
361    }
362
363    #[test]
364    fn test_verify_valid_dns_addresses() {
365        assert!(validate_dns_address("localhost").is_ok());
366        assert!(validate_dns_address("google.com").is_ok());
367        assert!(validate_dns_address("hub.hoprnet.org").is_ok());
368    }
369
370    #[test]
371    fn test_verify_invalid_dns_addresses() {
372        assert!(validate_dns_address("-hoprnet-.org").is_err());
373    }
374
375    #[test]
376    fn test_multiaddress_on_dappnode_default() {
377        temp_env::with_var("DAPPNODE", Some("true"), || {
378            assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
379        });
380    }
381
382    #[cfg(feature = "transport-quic")]
383    #[test]
384    fn test_multiaddress_on_non_dappnode_default() {
385        temp_env::with_vars([("DAPPNODE", Some("false")), ("HOPRD_NAT", Some("false"))], || {
386            assert_eq!(default_multiaddr_transport(1234), "udp/1234/quic-v1");
387        });
388    }
389
390    #[cfg(not(feature = "transport-quic"))]
391    #[test]
392    fn test_multiaddress_on_non_dappnode_default() {
393        assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
394    }
395
396    #[test]
397    fn test_multiaddress_on_non_dappnode_uses_nat() {
398        temp_env::with_var("HOPRD_NAT", Some("true"), || {
399            assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
400        });
401    }
402
403    #[cfg(feature = "transport-quic")]
404    #[test]
405    fn test_multiaddress_on_non_dappnode_not_uses_nat() {
406        temp_env::with_var("HOPRD_NAT", Some("false"), || {
407            assert_eq!(default_multiaddr_transport(1234), "udp/1234/quic-v1");
408        });
409    }
410
411    #[cfg(not(feature = "transport-quic"))]
412    #[test]
413    fn test_multiaddress_on_non_dappnode_not_uses_nat() {
414        temp_env::with_var("HOPRD_NAT", Some("false"), || {
415            assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
416        });
417    }
418
419    #[cfg(feature = "transport-quic")]
420    #[test]
421    fn test_multiaddress_on_dappnode_not_uses_nat() {
422        temp_env::with_vars([("DAPPNODE", Some("true")), ("HOPRD_NAT", Some("false"))], || {
423            assert_eq!(default_multiaddr_transport(1234), "udp/1234/quic-v1");
424        });
425    }
426
427    #[cfg(not(feature = "transport-quic"))]
428    #[test]
429    fn test_multiaddress_on_dappnode_not_uses_nat() {
430        temp_env::with_vars([("DAPPNODE", Some("true")), ("HOPRD_NAT", Some("false"))], || {
431            assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
432        });
433    }
434}