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#[inline]
29pub fn looks_like_domain(s: &str) -> bool {
30 is_dns_address_regex(s)
31}
32
33pub 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40pub enum HostType {
41 IPv4(String),
43 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#[derive(Debug, Serialize, Deserialize, Validate, Clone, PartialEq)]
59#[serde(deny_unknown_fields)]
60pub struct HostConfig {
61 #[serde(default)] pub address: HostType,
64 #[validate(range(min = 1u16))]
66 #[serde(default)] 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 let on_dappnode = std::env::var("DAPPNODE")
114 .map(|v| v.to_lowercase() == "true")
115 .unwrap_or(false);
116
117 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
170pub 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#[derive(Debug, Default, Serialize, Deserialize, Validate, Clone, PartialEq)]
180#[serde(deny_unknown_fields)]
181pub struct TransportConfig {
182 #[serde(default)]
185 pub announce_local_addresses: bool,
186 #[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#[serde_as]
222#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Validate, smart_default::SmartDefault)]
223#[serde(deny_unknown_fields)]
224pub struct SessionGlobalConfig {
225 #[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 #[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 #[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}