hopr_transport/
config.rs

1use proc_macro_regex::regex;
2use serde::{Deserialize, Serialize};
3use serde_with::serde_as;
4use std::fmt::{Display, Formatter};
5use std::net::ToSocketAddrs;
6use std::num::ParseIntError;
7use std::str::FromStr;
8use std::time::Duration;
9use validator::{Validate, ValidationError};
10
11use hopr_transport_identity::Multiaddr;
12pub use hopr_transport_network::{config::NetworkConfig, heartbeat::HeartbeatConfig};
13pub use hopr_transport_protocol::config::ProtocolConfig;
14
15use crate::errors::HoprTransportError;
16
17pub struct HoprTransportConfig {
18    pub transport: TransportConfig,
19    pub network: hopr_transport_network::config::NetworkConfig,
20    pub protocol: hopr_transport_protocol::config::ProtocolConfig,
21    pub heartbeat: hopr_transport_network::heartbeat::HeartbeatConfig,
22    pub session: SessionGlobalConfig,
23}
24
25regex!(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]$");
26
27/// Check whether the string looks like a valid domain.
28#[inline]
29pub fn looks_like_domain(s: &str) -> bool {
30    is_dns_address_regex(s)
31}
32
33/// Check whether the string is an actual reachable domain.
34pub fn is_reachable_domain(host: &str) -> bool {
35    host.to_socket_addrs().is_ok_and(|i| i.into_iter().next().is_some())
36}
37
38/// Enumeration of possible host types.
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40pub enum HostType {
41    /// IPv4 based host
42    IPv4(String),
43    /// DNS based host
44    Domain(String),
45}
46
47impl Default for HostType {
48    fn default() -> Self {
49        HostType::IPv4("127.0.0.1".to_owned())
50    }
51}
52
53/// Configuration of the listening host.
54///
55/// This is used for the P2P and REST API listeners.
56///
57/// Intentionally has no default because it depends on the use case.
58#[derive(Debug, Serialize, Deserialize, Validate, Clone, PartialEq)]
59#[serde(deny_unknown_fields)]
60pub struct HostConfig {
61    /// Host on which to listen
62    #[serde(default)] // must be defaulted to be mergeable from CLI args
63    pub address: HostType,
64    /// Listening TCP or UDP port (mandatory).
65    #[validate(range(min = 1u16))]
66    #[serde(default)] // must be defaulted to be mergeable from CLI args
67    pub port: u16,
68}
69
70impl FromStr for HostConfig {
71    type Err = String;
72
73    fn from_str(s: &str) -> Result<Self, Self::Err> {
74        let (ip_or_dns, str_port) = match s.split_once(':') {
75            None => return Err("Invalid host, is not in the '<host>:<port>' format".into()),
76            Some(split) => split,
77        };
78
79        let port = str_port.parse().map_err(|e: ParseIntError| e.to_string())?;
80
81        if validator::ValidateIp::validate_ipv4(&ip_or_dns) {
82            Ok(Self {
83                address: HostType::IPv4(ip_or_dns.to_owned()),
84                port,
85            })
86        } else if looks_like_domain(ip_or_dns) {
87            Ok(Self {
88                address: HostType::Domain(ip_or_dns.to_owned()),
89                port,
90            })
91        } else {
92            Err("Not a valid IPv4 or domain host".into())
93        }
94    }
95}
96
97impl Display for HostConfig {
98    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
99        write!(f, "{:?}:{}", self.address, self.port)
100    }
101}
102
103#[cfg(all(not(test), not(feature = "transport-quic")))]
104fn default_multiaddr_transport(port: u16) -> String {
105    format!("tcp/{port}")
106}
107
108#[cfg(any(test, feature = "transport-quic"))]
109fn default_multiaddr_transport(port: u16) -> String {
110    // In case we run on a Dappnode-like devices, presumably behind NAT, we fall back to TCP
111    // to circumvent issues with QUIC in such environments. To make this work reliably we'd need
112    // 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}
128
129impl TryFrom<&HostConfig> for Multiaddr {
130    type Error = HoprTransportError;
131
132    fn try_from(value: &HostConfig) -> Result<Self, Self::Error> {
133        match &value.address {
134            HostType::IPv4(ip) => Multiaddr::from_str(
135                format!("/ip4/{}/{}", ip.as_str(), default_multiaddr_transport(value.port)).as_str(),
136            )
137            .map_err(|e| HoprTransportError::Api(e.to_string())),
138            HostType::Domain(domain) => Multiaddr::from_str(
139                format!("/dns4/{}/{}", domain.as_str(), default_multiaddr_transport(value.port)).as_str(),
140            )
141            .map_err(|e| HoprTransportError::Api(e.to_string())),
142        }
143    }
144}
145
146fn validate_ipv4_address(s: &str) -> Result<(), ValidationError> {
147    if validator::ValidateIp::validate_ipv4(&s) {
148        let ipv4 = std::net::Ipv4Addr::from_str(s)
149            .map_err(|_| ValidationError::new("Failed to deserialize the string into an ipv4 address"))?;
150
151        if ipv4.is_private() || ipv4.is_multicast() || ipv4.is_unspecified() {
152            return Err(ValidationError::new(
153                "IPv4 cannot be private, multicast or unspecified (0.0.0.0)",
154            ))?;
155        }
156        Ok(())
157    } else {
158        Err(ValidationError::new("Invalid IPv4 address provided"))
159    }
160}
161
162fn validate_dns_address(s: &str) -> Result<(), ValidationError> {
163    if looks_like_domain(s) || is_reachable_domain(s) {
164        Ok(())
165    } else {
166        Err(ValidationError::new("Invalid DNS address provided"))
167    }
168}
169
170/// Validates the HostConfig to be used as an external host
171pub fn validate_external_host(host: &HostConfig) -> Result<(), ValidationError> {
172    match &host.address {
173        HostType::IPv4(ip4) => validate_ipv4_address(ip4),
174        HostType::Domain(domain) => validate_dns_address(domain),
175    }
176}
177
178/// Configuration of the physical transport mechanism.
179#[derive(Debug, Default, Serialize, Deserialize, Validate, Clone, PartialEq)]
180#[serde(deny_unknown_fields)]
181pub struct TransportConfig {
182    /// When true, assume that the node is running in an isolated network and does
183    /// not need any connection to nodes outside the subnet
184    #[serde(default)]
185    pub announce_local_addresses: bool,
186    /// When true, assume a testnet with multiple nodes running on the same machine
187    /// or in the same private IPv4 network
188    #[serde(default)]
189    pub prefer_local_addresses: bool,
190}
191
192const DEFAULT_SESSION_IDLE_TIMEOUT: Duration = Duration::from_secs(180);
193
194const SESSION_IDLE_MIN_TIMEOUT: Duration = Duration::from_secs(60);
195
196const DEFAULT_SESSION_ESTABLISH_RETRY_DELAY: Duration = Duration::from_secs(2);
197
198const DEFAULT_SESSION_ESTABLISH_MAX_RETRIES: u32 = 3;
199
200fn default_session_establish_max_retries() -> u32 {
201    DEFAULT_SESSION_ESTABLISH_MAX_RETRIES
202}
203
204fn default_session_idle_timeout() -> std::time::Duration {
205    DEFAULT_SESSION_IDLE_TIMEOUT
206}
207
208fn default_session_establish_retry_delay() -> std::time::Duration {
209    DEFAULT_SESSION_ESTABLISH_RETRY_DELAY
210}
211
212fn validate_session_idle_timeout(value: &std::time::Duration) -> Result<(), ValidationError> {
213    if SESSION_IDLE_MIN_TIMEOUT <= *value {
214        Ok(())
215    } else {
216        Err(ValidationError::new("session idle timeout is too low"))
217    }
218}
219
220/// Global configuration of Sessions.
221#[serde_as]
222#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Validate, smart_default::SmartDefault)]
223#[serde(deny_unknown_fields)]
224pub struct SessionGlobalConfig {
225    /// Maximum time before an idle Session is closed.
226    ///
227    /// Defaults to 3 minutes.
228    #[validate(custom(function = "validate_session_idle_timeout"))]
229    #[default(DEFAULT_SESSION_IDLE_TIMEOUT)]
230    #[serde(default = "default_session_idle_timeout")]
231    #[serde_as(as = "serde_with::DurationSeconds<u64>")]
232    pub idle_timeout: std::time::Duration,
233
234    /// Maximum retries to attempt to establish the Session
235    /// Set 0 for no retries.
236    ///
237    /// Defaults to 3, maximum is 20.
238    #[validate(range(min = 0, max = 20))]
239    #[default(DEFAULT_SESSION_ESTABLISH_MAX_RETRIES)]
240    #[serde(default = "default_session_establish_max_retries")]
241    pub establish_max_retries: u32,
242
243    /// Delay between Session establishment retries.
244    ///
245    /// Default is 2 seconds.
246    #[default(DEFAULT_SESSION_ESTABLISH_RETRY_DELAY)]
247    #[serde(default = "default_session_establish_retry_delay")]
248    #[serde_as(as = "serde_with::DurationSeconds<u64>")]
249    pub establish_retry_timeout: std::time::Duration,
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    #[test]
257    fn test_valid_domains_for_looks_like_a_domain() {
258        assert!(looks_like_domain("localhost"));
259        assert!(looks_like_domain("hoprnet.org"));
260        assert!(looks_like_domain("hub.hoprnet.org"));
261    }
262
263    #[test]
264    fn test_valid_domains_for_does_not_look_like_a_domain() {
265        assert!(!looks_like_domain(".org"));
266        assert!(!looks_like_domain("-hoprnet-.org"));
267    }
268
269    #[test]
270    fn test_valid_domains_should_be_reachable() {
271        assert!(!is_reachable_domain("google.com"));
272    }
273
274    #[test]
275    fn test_verify_valid_ip4_addresses() {
276        assert!(validate_ipv4_address("1.1.1.1").is_ok());
277        assert!(validate_ipv4_address("1.255.1.1").is_ok());
278        assert!(validate_ipv4_address("187.1.1.255").is_ok());
279        assert!(validate_ipv4_address("127.0.0.1").is_ok());
280    }
281
282    #[test]
283    fn test_verify_invalid_ip4_addresses() {
284        assert!(validate_ipv4_address("1.256.1.1").is_err());
285        assert!(validate_ipv4_address("-1.1.1.255").is_err());
286        assert!(validate_ipv4_address("127.0.0.256").is_err());
287        assert!(validate_ipv4_address("1").is_err());
288        assert!(validate_ipv4_address("1.1").is_err());
289        assert!(validate_ipv4_address("1.1.1").is_err());
290        assert!(validate_ipv4_address("1.1.1.1.1").is_err());
291    }
292
293    #[test]
294    fn test_verify_valid_dns_addresses() {
295        assert!(validate_dns_address("localhost").is_ok());
296        assert!(validate_dns_address("google.com").is_ok());
297        assert!(validate_dns_address("hub.hoprnet.org").is_ok());
298    }
299
300    #[test]
301    fn test_verify_invalid_dns_addresses() {
302        assert!(validate_dns_address("-hoprnet-.org").is_err());
303    }
304
305    #[test]
306    fn test_multiaddress_on_dappnode_default() {
307        temp_env::with_var("DAPPNODE", Some("true"), || {
308            assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
309        });
310    }
311
312    #[test]
313    fn test_multiaddress_on_non_dappnode_default() {
314        assert_eq!(default_multiaddr_transport(1234), "udp/1234/quic-v1");
315    }
316
317    #[test]
318    fn test_multiaddress_on_non_dappnode_uses_nat() {
319        temp_env::with_var("HOPRD_NAT", Some("true"), || {
320            assert_eq!(default_multiaddr_transport(1234), "tcp/1234");
321        });
322    }
323
324    #[test]
325    fn test_multiaddress_on_non_dappnode_not_uses_nat() {
326        temp_env::with_var("HOPRD_NAT", Some("false"), || {
327            assert_eq!(default_multiaddr_transport(1234), "udp/1234/quic-v1");
328        });
329    }
330
331    #[test]
332    fn test_multiaddress_on_dappnode_not_uses_nat() {
333        temp_env::with_vars([("DAPPNODE", Some("true")), ("HOPRD_NAT", Some("false"))], || {
334            assert_eq!(default_multiaddr_transport(1234), "udp/1234/quic-v1");
335        });
336    }
337}