1use std::{
2 fmt::{Display, Formatter},
3 net::SocketAddr,
4 str::FromStr,
5};
6
7use hickory_resolver::name_server::ConnectionProvider;
8use hopr_crypto_packet::{HoprSurb, prelude::HoprSenderId};
9use hopr_crypto_random::Randomizable;
10use hopr_internal_types::prelude::HoprPseudonym;
11pub use hopr_path::ValidatedPath;
12use hopr_primitive_types::{
13 bounded::{BoundedSize, BoundedVec},
14 prelude::Address,
15};
16use libp2p_identity::PeerId;
17
18use crate::errors::NetworkTypeError;
19
20#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, strum::Display, strum::EnumString)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23#[strum(serialize_all = "lowercase", ascii_case_insensitive)]
24#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
25pub enum IpProtocol {
26 TCP,
27 UDP,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Hash)]
37#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
38pub enum IpOrHost {
39 Dns(String, u16),
41 Ip(std::net::SocketAddr),
43}
44
45impl Display for IpOrHost {
46 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
47 match &self {
48 IpOrHost::Dns(host, port) => write!(f, "{host}:{port}"),
49 IpOrHost::Ip(ip) => write!(f, "{ip}"),
50 }
51 }
52}
53
54impl FromStr for IpOrHost {
55 type Err = NetworkTypeError;
56
57 fn from_str(s: &str) -> Result<Self, Self::Err> {
58 if let Ok(addr) = std::net::SocketAddr::from_str(s) {
59 Ok(IpOrHost::Ip(addr))
60 } else {
61 s.split_once(":")
62 .ok_or(NetworkTypeError::Other("missing port delimiter".into()))
63 .and_then(|(host, port_str)| {
64 u16::from_str(port_str)
65 .map(|port| IpOrHost::Dns(host.to_string(), port))
66 .map_err(|_| NetworkTypeError::Other("invalid port number".into()))
67 })
68 }
69 }
70}
71
72impl From<std::net::SocketAddr> for IpOrHost {
73 fn from(value: std::net::SocketAddr) -> Self {
74 IpOrHost::Ip(value)
75 }
76}
77
78impl IpOrHost {
79 #[cfg(feature = "runtime-tokio")]
84 pub async fn resolve_tokio(self) -> std::io::Result<Vec<SocketAddr>> {
85 cfg_if::cfg_if! {
86 if #[cfg(test)] {
87 let config = hickory_resolver::config::ResolverConfig::new();
90 let options = hickory_resolver::config::ResolverOpts::default();
91 let resolver = hickory_resolver::Resolver::builder_with_config(config, hickory_resolver::name_server::TokioConnectionProvider::default()).with_options(options).build();
92 } else {
93 let resolver = hickory_resolver::Resolver::builder_tokio()?.build();
94 }
95 };
96
97 self.resolve(resolver).await
98 }
99
100 pub async fn resolve<P: ConnectionProvider>(
103 self,
104 resolver: hickory_resolver::Resolver<P>,
105 ) -> std::io::Result<Vec<SocketAddr>> {
106 match self {
107 IpOrHost::Dns(name, port) => Ok(resolver
108 .lookup_ip(name)
109 .await?
110 .into_iter()
111 .map(|ip| SocketAddr::new(ip, port))
112 .collect()),
113 IpOrHost::Ip(addr) => Ok(vec![addr]),
114 }
115 }
116
117 pub fn port(&self) -> u16 {
119 match &self {
120 IpOrHost::Dns(_, port) => *port,
121 IpOrHost::Ip(addr) => addr.port(),
122 }
123 }
124
125 pub fn host(&self) -> String {
127 match &self {
128 IpOrHost::Dns(host, _) => host.clone(),
129 IpOrHost::Ip(addr) => addr.ip().to_string(),
130 }
131 }
132
133 pub fn is_dns(&self) -> bool {
135 matches!(self, IpOrHost::Dns(..))
136 }
137
138 pub fn is_ipv4(&self) -> bool {
144 matches!(self, IpOrHost::Ip(addr) if addr.is_ipv4())
145 }
146
147 pub fn is_ipv6(&self) -> bool {
153 matches!(self, IpOrHost::Ip(addr) if addr.is_ipv6())
154 }
155
156 pub fn is_loopback_ip(&self) -> bool {
162 matches!(self, IpOrHost::Ip(addr) if addr.ip().is_loopback())
163 }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Hash, strum::EnumTryAs)]
212#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
213pub enum SealedHost {
214 Plain(IpOrHost),
216 Sealed(Box<[u8]>),
218}
219
220impl SealedHost {
221 const MAX_LEN_WITH_PADDING: usize = 50;
222 pub const PADDING_CHAR: char = '@';
227
228 pub fn seal(host: IpOrHost, peer_id: PeerId) -> crate::errors::Result<Self> {
230 let mut host_str = host.to_string();
231
232 if host_str.len() < Self::MAX_LEN_WITH_PADDING {
234 let pad_len = hopr_crypto_random::random_integer(0, (Self::MAX_LEN_WITH_PADDING as u64).into());
235 for _ in 0..pad_len {
236 host_str.push(Self::PADDING_CHAR);
237 }
238 }
239
240 hopr_crypto_types::seal::seal_data(host_str.as_bytes(), peer_id)
241 .map(Self::Sealed)
242 .map_err(|e| NetworkTypeError::Other(e.to_string()))
243 }
244
245 pub fn unseal(self, key: &hopr_crypto_types::keypairs::OffchainKeypair) -> crate::errors::Result<IpOrHost> {
248 match self {
249 SealedHost::Plain(host) => Ok(host),
250 SealedHost::Sealed(enc) => hopr_crypto_types::seal::unseal_data(&enc, key)
251 .map_err(|e| NetworkTypeError::Other(e.to_string()))
252 .and_then(|data| {
253 String::from_utf8(data.into_vec())
254 .map_err(|e| NetworkTypeError::Other(e.to_string()))
255 .and_then(|s| IpOrHost::from_str(s.trim_end_matches(Self::PADDING_CHAR)))
256 }),
257 }
258 }
259}
260
261impl From<IpOrHost> for SealedHost {
262 fn from(value: IpOrHost) -> Self {
263 Self::Plain(value)
264 }
265}
266
267impl TryFrom<SealedHost> for IpOrHost {
268 type Error = NetworkTypeError;
269
270 fn try_from(value: SealedHost) -> Result<Self, Self::Error> {
271 match value {
272 SealedHost::Plain(host) => Ok(host),
273 SealedHost::Sealed(_) => Err(NetworkTypeError::SealedTarget),
274 }
275 }
276}
277
278impl std::fmt::Display for SealedHost {
279 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
280 match self {
281 SealedHost::Plain(h) => write!(f, "{h}"),
282 SealedHost::Sealed(_) => write!(f, "<redacted host>"),
283 }
284 }
285}
286
287#[derive(Debug, Clone, PartialEq, Eq)]
289#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
290pub enum RoutingOptions {
291 IntermediatePath(BoundedVec<Address, { RoutingOptions::MAX_INTERMEDIATE_HOPS }>),
293 Hops(BoundedSize<{ RoutingOptions::MAX_INTERMEDIATE_HOPS }>),
296}
297
298impl RoutingOptions {
299 pub const MAX_INTERMEDIATE_HOPS: usize = 3;
301
302 pub fn invert(self) -> RoutingOptions {
305 match self {
306 RoutingOptions::IntermediatePath(v) => RoutingOptions::IntermediatePath(v.into_iter().rev().collect()),
307 _ => self,
308 }
309 }
310
311 pub fn count_hops(&self) -> usize {
314 match &self {
315 RoutingOptions::IntermediatePath(v) => v.as_ref().len(),
316 RoutingOptions::Hops(h) => (*h).into(),
317 }
318 }
319}
320
321#[derive(Clone, Debug, Copy, PartialEq, Eq)]
323#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
324pub enum SurbMatcher {
325 Exact(HoprSenderId),
327 Pseudonym(HoprPseudonym),
329}
330
331impl SurbMatcher {
332 pub fn pseudonym(&self) -> HoprPseudonym {
334 match self {
335 SurbMatcher::Exact(id) => id.pseudonym(),
336 SurbMatcher::Pseudonym(p) => *p,
337 }
338 }
339}
340
341impl From<HoprPseudonym> for SurbMatcher {
342 fn from(value: HoprPseudonym) -> Self {
343 Self::Pseudonym(value)
344 }
345}
346
347impl From<&HoprPseudonym> for SurbMatcher {
348 fn from(pseudonym: &HoprPseudonym) -> Self {
349 (*pseudonym).into()
350 }
351}
352
353#[derive(Debug, Clone, PartialEq, Eq, strum::EnumIs)]
360#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
361pub enum DestinationRouting {
362 Forward {
365 destination: Address,
367 pseudonym: Option<HoprPseudonym>,
371 forward_options: RoutingOptions,
373 return_options: Option<RoutingOptions>,
375 },
376 Return(SurbMatcher),
380}
381
382impl DestinationRouting {
383 pub fn forward_only(destination: Address, forward_options: RoutingOptions) -> Self {
385 Self::Forward {
386 destination,
387 pseudonym: None,
388 forward_options,
389 return_options: None,
390 }
391 }
392}
393
394#[derive(Clone, Debug, strum::EnumIs)]
402pub enum ResolvedTransportRouting {
403 Forward {
405 pseudonym: HoprPseudonym,
407 forward_path: ValidatedPath,
409 return_paths: Vec<ValidatedPath>,
411 },
412 Return(HoprSenderId, HoprSurb),
414}
415
416impl ResolvedTransportRouting {
417 pub fn forward_only(forward_path: ValidatedPath) -> Self {
419 Self::Forward {
420 pseudonym: HoprPseudonym::random(),
421 forward_path,
422 return_paths: vec![],
423 }
424 }
425
426 pub fn count_return_paths(&self) -> usize {
429 match self {
430 ResolvedTransportRouting::Forward { return_paths, .. } => return_paths.len(),
431 ResolvedTransportRouting::Return(..) => 0,
432 }
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use std::net::SocketAddr;
439
440 use anyhow::anyhow;
441 use hopr_crypto_types::prelude::{Keypair, OffchainKeypair};
442
443 use super::*;
444
445 #[tokio::test]
446 async fn ip_or_host_must_resolve_dns_name() -> anyhow::Result<()> {
447 match IpOrHost::Dns("localhost".to_string(), 1000)
448 .resolve_tokio()
449 .await?
450 .first()
451 .ok_or(anyhow!("must resolve"))?
452 {
453 SocketAddr::V4(addr) => assert_eq!(*addr, "127.0.0.1:1000".parse()?),
454 SocketAddr::V6(addr) => assert_eq!(*addr, "::1:1000".parse()?),
455 }
456 Ok(())
457 }
458
459 #[tokio::test]
460 async fn ip_or_host_must_resolve_ip_address() -> anyhow::Result<()> {
461 let actual = IpOrHost::Ip("127.0.0.1:1000".parse()?).resolve_tokio().await?;
462
463 let actual = actual.first().ok_or(anyhow!("must resolve"))?;
464
465 let expected: SocketAddr = "127.0.0.1:1000".parse()?;
466
467 assert_eq!(*actual, expected);
468 Ok(())
469 }
470
471 #[test]
472 fn ip_or_host_should_parse_from_string() -> anyhow::Result<()> {
473 assert_eq!(
474 IpOrHost::Dns("some.dns.name.info".into(), 1234),
475 IpOrHost::from_str("some.dns.name.info:1234")?
476 );
477 assert_eq!(
478 IpOrHost::Ip("1.2.3.4:1234".parse()?),
479 IpOrHost::from_str("1.2.3.4:1234")?
480 );
481 Ok(())
482 }
483
484 #[test]
485 fn sealing_adds_padding_to_hide_length() -> anyhow::Result<()> {
486 let peer_id: PeerId = OffchainKeypair::random().public().into();
487 let last_len = SealedHost::seal("127.0.0.1:1234".parse()?, peer_id)?
488 .try_as_sealed()
489 .unwrap()
490 .len();
491 for _ in 0..20 {
492 let current_len = SealedHost::seal("127.0.0.1:1234".parse()?, peer_id)?
493 .try_as_sealed()
494 .unwrap()
495 .len();
496 if current_len != last_len {
497 return Ok(());
498 }
499 }
500
501 anyhow::bail!("sealed length not randomized");
502 }
503}