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;
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_IDLE_TIMEOUT: Duration = Duration::from_secs(180);
197
198const SESSION_IDLE_MIN_TIMEOUT: Duration = Duration::from_secs(60);
199
200const DEFAULT_SESSION_ESTABLISH_RETRY_DELAY: Duration = Duration::from_secs(2);
201
202const DEFAULT_SESSION_ESTABLISH_MAX_RETRIES: u32 = 3;
203
204const DEFAULT_SESSION_BALANCER_SAMPLING: Duration = Duration::from_secs(1);
205
206fn default_session_establish_max_retries() -> u32 {
207    DEFAULT_SESSION_ESTABLISH_MAX_RETRIES
208}
209
210fn default_session_idle_timeout() -> std::time::Duration {
211    DEFAULT_SESSION_IDLE_TIMEOUT
212}
213
214fn default_session_establish_retry_delay() -> std::time::Duration {
215    DEFAULT_SESSION_ESTABLISH_RETRY_DELAY
216}
217
218fn default_session_balancer_sampling() -> std::time::Duration {
219    DEFAULT_SESSION_BALANCER_SAMPLING
220}
221
222fn validate_session_idle_timeout(value: &std::time::Duration) -> Result<(), ValidationError> {
223    if SESSION_IDLE_MIN_TIMEOUT <= *value {
224        Ok(())
225    } else {
226        Err(ValidationError::new("session idle timeout is too low"))
227    }
228}
229
230fn validate_balancer_sampling(value: &std::time::Duration) -> Result<(), ValidationError> {
231    if MIN_BALANCER_SAMPLING_INTERVAL <= *value {
232        Ok(())
233    } else {
234        Err(ValidationError::new("balancer sampling interval is too low"))
235    }
236}
237
238/// Global configuration of Sessions.
239#[serde_as]
240#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Validate, smart_default::SmartDefault)]
241#[serde(deny_unknown_fields)]
242pub struct SessionGlobalConfig {
243    /// Maximum time before an idle Session is closed.
244    ///
245    /// Defaults to 3 minutes.
246    #[validate(custom(function = "validate_session_idle_timeout"))]
247    #[default(DEFAULT_SESSION_IDLE_TIMEOUT)]
248    #[serde(default = "default_session_idle_timeout")]
249    #[serde_as(as = "serde_with::DurationSeconds<u64>")]
250    pub idle_timeout: std::time::Duration,
251
252    /// Maximum retries to attempt to establish the Session
253    /// Set 0 for no retries.
254    ///
255    /// Defaults to 3, maximum is 20.
256    #[validate(range(min = 0, max = 20))]
257    #[default(DEFAULT_SESSION_ESTABLISH_MAX_RETRIES)]
258    #[serde(default = "default_session_establish_max_retries")]
259    pub establish_max_retries: u32,
260
261    /// Delay between Session establishment retries.
262    ///
263    /// Default is 2 seconds.
264    #[default(DEFAULT_SESSION_ESTABLISH_RETRY_DELAY)]
265    #[serde(default = "default_session_establish_retry_delay")]
266    #[serde_as(as = "serde_with::DurationSeconds<u64>")]
267    pub establish_retry_timeout: std::time::Duration,
268
269    /// Sampling interval for SURB balancer in milliseconds.
270    ///
271    /// Default is 1000 milliseconds.
272    #[validate(custom(function = "validate_balancer_sampling"))]
273    #[default(DEFAULT_SESSION_BALANCER_SAMPLING)]
274    #[serde(default = "default_session_balancer_sampling")]
275    #[serde_as(as = "serde_with::DurationMilliSeconds<u64>")]
276    pub balancer_sampling_interval: std::time::Duration,
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    #[test]
284    fn test_valid_domains_for_looks_like_a_domain() {
285        assert!(looks_like_domain("localhost"));
286        assert!(looks_like_domain("hoprnet.org"));
287        assert!(looks_like_domain("hub.hoprnet.org"));
288    }
289
290    #[test]
291    fn test_valid_domains_for_does_not_look_like_a_domain() {
292        assert!(!looks_like_domain(".org"));
293        assert!(!looks_like_domain("-hoprnet-.org"));
294    }
295
296    #[test]
297    fn test_valid_domains_should_be_reachable() {
298        assert!(!is_reachable_domain("google.com"));
299    }
300
301    #[test]
302    fn test_verify_valid_ip4_addresses() {
303        assert!(validate_ipv4_address("1.1.1.1").is_ok());
304        assert!(validate_ipv4_address("1.255.1.1").is_ok());
305        assert!(validate_ipv4_address("187.1.1.255").is_ok());
306        assert!(validate_ipv4_address("127.0.0.1").is_ok());
307    }
308
309    #[test]
310    fn test_verify_invalid_ip4_addresses() {
311        assert!(validate_ipv4_address("1.256.1.1").is_err());
312        assert!(validate_ipv4_address("-1.1.1.255").is_err());
313        assert!(validate_ipv4_address("127.0.0.256").is_err());
314        assert!(validate_ipv4_address("1").is_err());
315        assert!(validate_ipv4_address("1.1").is_err());
316        assert!(validate_ipv4_address("1.1.1").is_err());
317        assert!(validate_ipv4_address("1.1.1.1.1").is_err());
318    }
319
320    #[test]
321    fn test_verify_valid_dns_addresses() {
322        assert!(validate_dns_address("localhost").is_ok());
323        assert!(validate_dns_address("google.com").is_ok());
324        assert!(validate_dns_address("hub.hoprnet.org").is_ok());
325    }
326
327    #[test]
328    fn test_verify_invalid_dns_addresses() {
329        assert!(validate_dns_address("-hoprnet-.org").is_err());
330    }
331
332    #[test]
333    fn test_multiaddress_on_dappnode_default() {
334        temp_env::with_var("DAPPNODE", Some("true"), || {
335            assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
336        });
337    }
338
339    #[cfg(feature = "transport-quic")]
340    #[test]
341    fn test_multiaddress_on_non_dappnode_default() {
342        temp_env::with_vars([("DAPPNODE", Some("false")), ("HOPRD_NAT", Some("false"))], || {
343            assert_eq!(default_multiaddr_transport(1234), "udp/1234/quic-v1");
344        });
345    }
346
347    #[cfg(not(feature = "transport-quic"))]
348    #[test]
349    fn test_multiaddress_on_non_dappnode_default() {
350        assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
351    }
352
353    #[test]
354    fn test_multiaddress_on_non_dappnode_uses_nat() {
355        temp_env::with_var("HOPRD_NAT", Some("true"), || {
356            assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
357        });
358    }
359
360    #[cfg(feature = "transport-quic")]
361    #[test]
362    fn test_multiaddress_on_non_dappnode_not_uses_nat() {
363        temp_env::with_var("HOPRD_NAT", Some("false"), || {
364            assert_eq!(default_multiaddr_transport(1234), "udp/1234/quic-v1");
365        });
366    }
367
368    #[cfg(not(feature = "transport-quic"))]
369    #[test]
370    fn test_multiaddress_on_non_dappnode_not_uses_nat() {
371        temp_env::with_var("HOPRD_NAT", Some("false"), || {
372            assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
373        });
374    }
375
376    #[cfg(feature = "transport-quic")]
377    #[test]
378    fn test_multiaddress_on_dappnode_not_uses_nat() {
379        temp_env::with_vars([("DAPPNODE", Some("true")), ("HOPRD_NAT", Some("false"))], || {
380            assert_eq!(default_multiaddr_transport(1234), "udp/1234/quic-v1");
381        });
382    }
383
384    #[cfg(not(feature = "transport-quic"))]
385    #[test]
386    fn test_multiaddress_on_dappnode_not_uses_nat() {
387        temp_env::with_vars([("DAPPNODE", Some("true")), ("HOPRD_NAT", Some("false"))], || {
388            assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
389        });
390    }
391}