hopr_network_types/
types.rs

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::*;
11use hopr_primitive_types::bounded::{BoundedSize, BoundedVec};
12use libp2p_identity::PeerId;
13
14use crate::errors::NetworkTypeError;
15
16/// Lists some of the IP protocols.
17#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, strum::Display, strum::EnumString)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19#[strum(serialize_all = "lowercase", ascii_case_insensitive)]
20#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
21pub enum IpProtocol {
22    TCP,
23    UDP,
24}
25
26/// Implements a host name with port.
27/// This could be either a DNS name with port
28/// or an IP address with port represented by [`std::net::SocketAddr`].
29///
30/// This object implements [`std::net::ToSocketAddrs`] which performs automatic
31/// DNS name resolution in case this is a [`IpOrHost::Dns`] instance.
32#[derive(Debug, Clone, PartialEq, Eq, Hash)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34pub enum IpOrHost {
35    /// DNS name and port.
36    Dns(String, u16),
37    /// IP address with port.
38    Ip(std::net::SocketAddr),
39}
40
41impl Display for IpOrHost {
42    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
43        match &self {
44            IpOrHost::Dns(host, port) => write!(f, "{host}:{port}"),
45            IpOrHost::Ip(ip) => write!(f, "{ip}"),
46        }
47    }
48}
49
50impl FromStr for IpOrHost {
51    type Err = NetworkTypeError;
52
53    fn from_str(s: &str) -> Result<Self, Self::Err> {
54        if let Ok(addr) = std::net::SocketAddr::from_str(s) {
55            Ok(IpOrHost::Ip(addr))
56        } else {
57            s.split_once(":")
58                .ok_or(NetworkTypeError::Other("missing port delimiter".into()))
59                .and_then(|(host, port_str)| {
60                    u16::from_str(port_str)
61                        .map(|port| IpOrHost::Dns(host.to_string(), port))
62                        .map_err(|_| NetworkTypeError::Other("invalid port number".into()))
63                })
64        }
65    }
66}
67
68impl From<std::net::SocketAddr> for IpOrHost {
69    fn from(value: std::net::SocketAddr) -> Self {
70        IpOrHost::Ip(value)
71    }
72}
73
74impl IpOrHost {
75    /// Tries to resolve the DNS name and returns all IP addresses found.
76    /// If this enum is already an IP address and port, it will simply return it.
77    ///
78    /// Uses `tokio` resolver.
79    #[cfg(feature = "runtime-tokio")]
80    pub async fn resolve_tokio(self) -> std::io::Result<Vec<SocketAddr>> {
81        cfg_if::cfg_if! {
82            if #[cfg(test)] {
83                // This resolver setup is used in the tests to be executed in a sandbox environment
84                // which prevents IO access to system-level files.
85                let config = hickory_resolver::config::ResolverConfig::new();
86                let options = hickory_resolver::config::ResolverOpts::default();
87                let resolver = hickory_resolver::Resolver::builder_with_config(config, hickory_resolver::name_server::TokioConnectionProvider::default()).with_options(options).build();
88            } else {
89                let resolver = hickory_resolver::Resolver::builder_tokio()?.build();
90            }
91        };
92
93        self.resolve(resolver).await
94    }
95
96    /// Tries to resolve the DNS name and returns all IP addresses found.
97    /// If this enum is already an IP address and port, it will simply return it.
98    pub async fn resolve<P: ConnectionProvider>(
99        self,
100        resolver: hickory_resolver::Resolver<P>,
101    ) -> std::io::Result<Vec<SocketAddr>> {
102        match self {
103            IpOrHost::Dns(name, port) => Ok(resolver
104                .lookup_ip(name)
105                .await?
106                .into_iter()
107                .map(|ip| SocketAddr::new(ip, port))
108                .collect()),
109            IpOrHost::Ip(addr) => Ok(vec![addr]),
110        }
111    }
112
113    /// Gets the port number.
114    pub fn port(&self) -> u16 {
115        match &self {
116            IpOrHost::Dns(_, port) => *port,
117            IpOrHost::Ip(addr) => addr.port(),
118        }
119    }
120
121    /// Gets the unresolved DNS name or IP address as string.
122    pub fn host(&self) -> String {
123        match &self {
124            IpOrHost::Dns(host, _) => host.clone(),
125            IpOrHost::Ip(addr) => addr.ip().to_string(),
126        }
127    }
128
129    /// Checks if this instance is a [DNS name](IpOrHost::Dns).
130    pub fn is_dns(&self) -> bool {
131        matches!(self, IpOrHost::Dns(..))
132    }
133
134    /// Checks if this instance is an [IP address](IpOrHost::Ip) and whether it is
135    /// an IPv4 address.
136    ///
137    /// Always returns `false` if this instance is a [DNS name](IpOrHost::Dns),
138    /// i.e.: it does not perform any DNS resolution.
139    pub fn is_ipv4(&self) -> bool {
140        matches!(self, IpOrHost::Ip(addr) if addr.is_ipv4())
141    }
142
143    /// Checks if this instance is an [IP address](IpOrHost::Ip) and whether it is
144    /// an IPv6 address.
145    ///
146    /// Always returns `false` if this instance is a [DNS name](IpOrHost::Dns),
147    /// i.e.: it does not perform any DNS resolution.
148    pub fn is_ipv6(&self) -> bool {
149        matches!(self, IpOrHost::Ip(addr) if addr.is_ipv6())
150    }
151
152    /// Checks if this instance is an [IP address](IpOrHost::Ip) and whether it is
153    /// a loopback address.
154    ///
155    /// Always returns `false` if this instance is a [DNS name](IpOrHost::Dns),
156    /// i.e.: it does not perform any DNS resolution.
157    pub fn is_loopback_ip(&self) -> bool {
158        matches!(self, IpOrHost::Ip(addr) if addr.ip().is_loopback())
159    }
160}
161
162/// Contains optionally encrypted [`IpOrHost`].
163///
164/// This is useful for hiding the [`IpOrHost`] instance from the Entry node.
165/// The client first encrypts the `IpOrHost` instance via [`SealedHost::seal`] using
166/// the Exit node's public key.
167/// Upon receiving the `SealedHost` instance by the Exit node, it can call
168/// [`SealedHost::unseal`] using its private key to get the original `IpOrHost` instance.
169///
170/// Sealing is fully randomized and therefore does not leak information about equal `IpOrHost`
171/// instances.
172///
173/// The length of the encrypted host is also obscured by the use of random padding before
174/// encryption.
175///
176/// ### Example
177/// ```rust
178/// use hopr_crypto_types::prelude::{Keypair, OffchainKeypair};
179/// use hopr_network_types::prelude::{IpOrHost, SealedHost};
180/// use libp2p_identity::PeerId;
181///
182/// # fn main() -> anyhow::Result<()> {
183/// let keypair = OffchainKeypair::random();
184///
185/// let exit_node_peer_id: PeerId = keypair.public().into();
186/// let host: IpOrHost = "127.0.0.1:1000".parse()?;
187///
188/// // On the Client
189/// let encrypted = SealedHost::seal(host.clone(), keypair.public().into())?;
190///
191/// // On the Exit node
192/// let decrypted = encrypted.unseal(&keypair)?;
193/// assert_eq!(host, decrypted);
194///
195/// // Plain SealedHost unseals trivially
196/// let plain_sealed: SealedHost = host.clone().into();
197/// assert_eq!(host, plain_sealed.try_into()?);
198///
199/// // The same host sealing is randomized
200/// let encrypted_1 = SealedHost::seal(host.clone(), keypair.public().into())?;
201/// let encrypted_2 = SealedHost::seal(host.clone(), keypair.public().into())?;
202/// assert_ne!(encrypted_1, encrypted_2);
203///
204/// # Ok(())
205/// # }
206/// ```
207#[derive(Debug, Clone, PartialEq, Eq, Hash, strum::EnumTryAs)]
208#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
209pub enum SealedHost {
210    /// Plain (not sealed) [`IpOrHost`]
211    Plain(IpOrHost),
212    /// Encrypted [`IpOrHost`]
213    Sealed(Box<[u8]>),
214}
215
216impl SealedHost {
217    const MAX_LEN_WITH_PADDING: usize = 50;
218    /// Character that can be appended to the host to obscure its length.
219    ///
220    /// User can add as many of this character to the host, and it will be removed
221    /// during unsealing.
222    pub const PADDING_CHAR: char = '@';
223
224    /// Seals the given [`IpOrHost`] using the Exit node's peer ID.
225    pub fn seal(host: IpOrHost, peer_id: PeerId) -> crate::errors::Result<Self> {
226        let mut host_str = host.to_string();
227
228        // Add randomly long padding, so the length of the short hosts is obscured
229        if host_str.len() < Self::MAX_LEN_WITH_PADDING {
230            let pad_len = hopr_crypto_random::random_integer(0, (Self::MAX_LEN_WITH_PADDING as u64).into());
231            for _ in 0..pad_len {
232                host_str.push(Self::PADDING_CHAR);
233            }
234        }
235
236        hopr_crypto_types::seal::seal_data(host_str.as_bytes(), peer_id)
237            .map(Self::Sealed)
238            .map_err(|e| NetworkTypeError::Other(e.to_string()))
239    }
240
241    /// Tries to unseal the sealed [`IpOrHost`] using the private key as Exit node.
242    /// No-op, if the data is already unsealed.
243    pub fn unseal(self, key: &hopr_crypto_types::keypairs::OffchainKeypair) -> crate::errors::Result<IpOrHost> {
244        match self {
245            SealedHost::Plain(host) => Ok(host),
246            SealedHost::Sealed(enc) => hopr_crypto_types::seal::unseal_data(&enc, key)
247                .map_err(|e| NetworkTypeError::Other(e.to_string()))
248                .and_then(|data| {
249                    String::from_utf8(data.into_vec())
250                        .map_err(|e| NetworkTypeError::Other(e.to_string()))
251                        .and_then(|s| IpOrHost::from_str(s.trim_end_matches(Self::PADDING_CHAR)))
252                }),
253        }
254    }
255}
256
257impl From<IpOrHost> for SealedHost {
258    fn from(value: IpOrHost) -> Self {
259        Self::Plain(value)
260    }
261}
262
263impl TryFrom<SealedHost> for IpOrHost {
264    type Error = NetworkTypeError;
265
266    fn try_from(value: SealedHost) -> Result<Self, Self::Error> {
267        match value {
268            SealedHost::Plain(host) => Ok(host),
269            SealedHost::Sealed(_) => Err(NetworkTypeError::SealedTarget),
270        }
271    }
272}
273
274impl std::fmt::Display for SealedHost {
275    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
276        match self {
277            SealedHost::Plain(h) => write!(f, "{h}"),
278            SealedHost::Sealed(_) => write!(f, "<redacted host>"),
279        }
280    }
281}
282
283/// Represents routing options in a mixnet with a maximum number of hops.
284#[derive(Debug, Clone, PartialEq, Eq)]
285#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
286pub enum RoutingOptions {
287    /// A fixed intermediate path consisting of at most [`RoutingOptions::MAX_INTERMEDIATE_HOPS`] hops.
288    IntermediatePath(BoundedVec<NodeId, { RoutingOptions::MAX_INTERMEDIATE_HOPS }>),
289    /// Random intermediate path with at least the given number of hops,
290    /// but at most [`RoutingOptions::MAX_INTERMEDIATE_HOPS`].
291    Hops(BoundedSize<{ RoutingOptions::MAX_INTERMEDIATE_HOPS }>),
292}
293
294impl RoutingOptions {
295    /// The maximum number of hops this instance can represent.
296    pub const MAX_INTERMEDIATE_HOPS: usize = 3;
297
298    /// Inverts the intermediate path if this is an instance of [`RoutingOptions::IntermediatePath`].
299    /// Otherwise, does nothing.
300    pub fn invert(self) -> RoutingOptions {
301        match self {
302            RoutingOptions::IntermediatePath(v) => RoutingOptions::IntermediatePath(v.into_iter().rev().collect()),
303            _ => self,
304        }
305    }
306
307    /// Returns the number of hops this instance represents.
308    /// This value is guaranteed to be between 0 and [`RoutingOptions::MAX_INTERMEDIATE_HOPS`].
309    pub fn count_hops(&self) -> usize {
310        match &self {
311            RoutingOptions::IntermediatePath(v) => v.as_ref().len(),
312            RoutingOptions::Hops(h) => (*h).into(),
313        }
314    }
315}
316
317/// Allows finding saved SURBs based on different criteria.
318#[derive(Clone, Debug, Copy, PartialEq, Eq)]
319#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
320pub enum SurbMatcher {
321    /// Try to find a SURB that has the exact given [`HoprSenderId`].
322    Exact(HoprSenderId),
323    /// Find any SURB with the given pseudonym.
324    Pseudonym(HoprPseudonym),
325}
326
327impl SurbMatcher {
328    /// Get the pseudonym part of the match.
329    pub fn pseudonym(&self) -> HoprPseudonym {
330        match self {
331            SurbMatcher::Exact(id) => id.pseudonym(),
332            SurbMatcher::Pseudonym(p) => *p,
333        }
334    }
335}
336
337impl From<HoprPseudonym> for SurbMatcher {
338    fn from(value: HoprPseudonym) -> Self {
339        Self::Pseudonym(value)
340    }
341}
342
343impl From<&HoprPseudonym> for SurbMatcher {
344    fn from(pseudonym: &HoprPseudonym) -> Self {
345        (*pseudonym).into()
346    }
347}
348
349/// Routing information containing forward or return routing options.
350///
351/// Information in this object represents the minimum required basis
352/// to generate forward paths and return paths.
353///
354/// See also [`RoutingOptions`].
355#[derive(Debug, Clone, PartialEq, Eq, strum::EnumIs)]
356#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
357pub enum DestinationRouting {
358    /// Forward routing using the destination node and path,
359    /// with a possible return path.
360    Forward {
361        /// The destination node.
362        destination: Box<NodeId>,
363        /// Our pseudonym shown to the destination.
364        ///
365        /// If not given, it will be resolved as random.
366        pseudonym: Option<HoprPseudonym>,
367        /// The path to the destination.
368        forward_options: RoutingOptions,
369        /// Optional return path.
370        return_options: Option<RoutingOptions>,
371    },
372    /// Return routing using a SURB with the given pseudonym.
373    ///
374    /// Will fail if no SURB for this pseudonym is found.
375    Return(SurbMatcher),
376}
377
378impl DestinationRouting {
379    /// Shortcut for routing that does not create any SURBs for a return path.
380    pub fn forward_only<T: Into<NodeId>>(destination: T, forward_options: RoutingOptions) -> Self {
381        Self::Forward {
382            destination: Box::new(destination.into()),
383            pseudonym: None,
384            forward_options,
385            return_options: None,
386        }
387    }
388}
389
390/// Contains the resolved routing information for the packet.
391///
392/// Instance of this object is typically constructed via some resolution of a
393/// [`DestinationRouting`] instance.
394///
395/// It contains the actual forward and return paths for forward packets,
396/// or an actual SURB for return (reply) packets.
397#[derive(Clone, Debug, strum::EnumIs)]
398pub enum ResolvedTransportRouting {
399    /// Concrete routing information for a forward packet.
400    Forward {
401        /// Pseudonym of the sender.
402        pseudonym: HoprPseudonym,
403        /// Forward path.
404        forward_path: ValidatedPath,
405        /// Optional list of return paths.
406        return_paths: Vec<ValidatedPath>,
407    },
408    /// Sender ID and the corresponding SURB.
409    Return(HoprSenderId, HoprSurb),
410}
411
412impl ResolvedTransportRouting {
413    /// Shortcut for routing that does not create any SURBs for a return path.
414    pub fn forward_only(forward_path: ValidatedPath) -> Self {
415        Self::Forward {
416            pseudonym: HoprPseudonym::random(),
417            forward_path,
418            return_paths: vec![],
419        }
420    }
421
422    /// Returns the number of return paths (SURBs) on the [`ResolvedTransportRouting::Forward`]
423    /// variant, or always 0 on the [`ResolvedTransportRouting::Return`] variant.
424    pub fn count_return_paths(&self) -> usize {
425        match self {
426            ResolvedTransportRouting::Forward { return_paths, .. } => return_paths.len(),
427            ResolvedTransportRouting::Return(..) => 0,
428        }
429    }
430}
431
432#[cfg(test)]
433mod tests {
434    use hopr_crypto_types::prelude::{Keypair, OffchainKeypair};
435    #[cfg(feature = "runtime-tokio")]
436    use {anyhow::anyhow, std::net::SocketAddr};
437
438    use super::*;
439
440    #[cfg(feature = "runtime-tokio")]
441    #[tokio::test]
442    async fn ip_or_host_must_resolve_dns_name() -> anyhow::Result<()> {
443        match IpOrHost::Dns("localhost".to_string(), 1000)
444            .resolve_tokio()
445            .await?
446            .first()
447            .ok_or(anyhow!("must resolve"))?
448        {
449            SocketAddr::V4(addr) => assert_eq!(*addr, "127.0.0.1:1000".parse()?),
450            SocketAddr::V6(addr) => assert_eq!(*addr, "::1:1000".parse()?),
451        }
452        Ok(())
453    }
454
455    #[cfg(feature = "runtime-tokio")]
456    #[tokio::test]
457    async fn ip_or_host_must_resolve_ip_address() -> anyhow::Result<()> {
458        let actual = IpOrHost::Ip("127.0.0.1:1000".parse()?).resolve_tokio().await?;
459
460        let actual = actual.first().ok_or(anyhow!("must resolve"))?;
461
462        let expected: SocketAddr = "127.0.0.1:1000".parse()?;
463
464        assert_eq!(*actual, expected);
465        Ok(())
466    }
467
468    #[test]
469    fn ip_or_host_should_parse_from_string() -> anyhow::Result<()> {
470        assert_eq!(
471            IpOrHost::Dns("some.dns.name.info".into(), 1234),
472            IpOrHost::from_str("some.dns.name.info:1234")?
473        );
474        assert_eq!(
475            IpOrHost::Ip("1.2.3.4:1234".parse()?),
476            IpOrHost::from_str("1.2.3.4:1234")?
477        );
478        Ok(())
479    }
480
481    #[test]
482    fn sealing_adds_padding_to_hide_length() -> anyhow::Result<()> {
483        let peer_id: PeerId = OffchainKeypair::random().public().into();
484        let last_len = SealedHost::seal("127.0.0.1:1234".parse()?, peer_id)?
485            .try_as_sealed()
486            .unwrap()
487            .len();
488        for _ in 0..20 {
489            let current_len = SealedHost::seal("127.0.0.1:1234".parse()?, peer_id)?
490                .try_as_sealed()
491                .unwrap()
492                .len();
493            if current_len != last_len {
494                return Ok(());
495            }
496        }
497
498        anyhow::bail!("sealed length not randomized");
499    }
500}