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