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::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/// Lists some of the IP protocols.
21#[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/// Implements a host name with port.
31/// This could be either a DNS name with port
32/// or an IP address with port represented by [`std::net::SocketAddr`].
33///
34/// This object implements [`std::net::ToSocketAddrs`] which performs automatic
35/// DNS name resolution in case this is a [`IpOrHost::Dns`] instance.
36#[derive(Debug, Clone, PartialEq, Eq, Hash)]
37#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
38pub enum IpOrHost {
39    /// DNS name and port.
40    Dns(String, u16),
41    /// IP address with port.
42    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    /// Tries to resolve the DNS name and returns all IP addresses found.
80    /// If this enum is already an IP address and port, it will simply return it.
81    ///
82    /// Uses `tokio` resolver.
83    #[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                // This resolver setup is used in the tests to be executed in a sandbox environment
88                // which prevents IO access to system-level files.
89                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    /// Tries to resolve the DNS name and returns all IP addresses found.
101    /// If this enum is already an IP address and port, it will simply return it.
102    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    /// Gets the port number.
118    pub fn port(&self) -> u16 {
119        match &self {
120            IpOrHost::Dns(_, port) => *port,
121            IpOrHost::Ip(addr) => addr.port(),
122        }
123    }
124
125    /// Gets the unresolved DNS name or IP address as string.
126    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    /// Checks if this instance is a [DNS name](IpOrHost::Dns).
134    pub fn is_dns(&self) -> bool {
135        matches!(self, IpOrHost::Dns(..))
136    }
137
138    /// Checks if this instance is an [IP address](IpOrHost::Ip) and whether it is
139    /// an IPv4 address.
140    ///
141    /// Always returns `false` if this instance is a [DNS name](IpOrHost::Dns),
142    /// i.e.: it does not perform any DNS resolution.
143    pub fn is_ipv4(&self) -> bool {
144        matches!(self, IpOrHost::Ip(addr) if addr.is_ipv4())
145    }
146
147    /// Checks if this instance is an [IP address](IpOrHost::Ip) and whether it is
148    /// an IPv6 address.
149    ///
150    /// Always returns `false` if this instance is a [DNS name](IpOrHost::Dns),
151    /// i.e.: it does not perform any DNS resolution.
152    pub fn is_ipv6(&self) -> bool {
153        matches!(self, IpOrHost::Ip(addr) if addr.is_ipv6())
154    }
155
156    /// Checks if this instance is an [IP address](IpOrHost::Ip) and whether it is
157    /// a loopback address.
158    ///
159    /// Always returns `false` if this instance is a [DNS name](IpOrHost::Dns),
160    /// i.e.: it does not perform any DNS resolution.
161    pub fn is_loopback_ip(&self) -> bool {
162        matches!(self, IpOrHost::Ip(addr) if addr.ip().is_loopback())
163    }
164}
165
166/// Contains optionally encrypted [`IpOrHost`].
167///
168/// This is useful for hiding the [`IpOrHost`] instance from the Entry node.
169/// The client first encrypts the `IpOrHost` instance via [`SealedHost::seal`] using
170/// the Exit node's public key.
171/// Upon receiving the `SealedHost` instance by the Exit node, it can call
172/// [`SealedHost::unseal`] using its private key to get the original `IpOrHost` instance.
173///
174/// Sealing is fully randomized and therefore does not leak information about equal `IpOrHost`
175/// instances.
176///
177/// The length of the encrypted host is also obscured by the use of random padding before
178/// encryption.
179///
180/// ### Example
181/// ```rust
182/// use hopr_crypto_types::prelude::{Keypair, OffchainKeypair};
183/// use hopr_network_types::prelude::{IpOrHost, SealedHost};
184/// use libp2p_identity::PeerId;
185///
186/// # fn main() -> anyhow::Result<()> {
187/// let keypair = OffchainKeypair::random();
188///
189/// let exit_node_peer_id: PeerId = keypair.public().into();
190/// let host: IpOrHost = "127.0.0.1:1000".parse()?;
191///
192/// // On the Client
193/// let encrypted = SealedHost::seal(host.clone(), keypair.public().into())?;
194///
195/// // On the Exit node
196/// let decrypted = encrypted.unseal(&keypair)?;
197/// assert_eq!(host, decrypted);
198///
199/// // Plain SealedHost unseals trivially
200/// let plain_sealed: SealedHost = host.clone().into();
201/// assert_eq!(host, plain_sealed.try_into()?);
202///
203/// // The same host sealing is randomized
204/// let encrypted_1 = SealedHost::seal(host.clone(), keypair.public().into())?;
205/// let encrypted_2 = SealedHost::seal(host.clone(), keypair.public().into())?;
206/// assert_ne!(encrypted_1, encrypted_2);
207///
208/// # Ok(())
209/// # }
210/// ```
211#[derive(Debug, Clone, PartialEq, Eq, Hash, strum::EnumTryAs)]
212#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
213pub enum SealedHost {
214    /// Plain (not sealed) [`IpOrHost`]
215    Plain(IpOrHost),
216    /// Encrypted [`IpOrHost`]
217    Sealed(Box<[u8]>),
218}
219
220impl SealedHost {
221    const MAX_LEN_WITH_PADDING: usize = 50;
222    /// Character that can be appended to the host to obscure its length.
223    ///
224    /// User can add as many of this character to the host, and it will be removed
225    /// during unsealing.
226    pub const PADDING_CHAR: char = '@';
227
228    /// Seals the given [`IpOrHost`] using the Exit node's peer ID.
229    pub fn seal(host: IpOrHost, peer_id: PeerId) -> crate::errors::Result<Self> {
230        let mut host_str = host.to_string();
231
232        // Add randomly long padding, so the length of the short hosts is obscured
233        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    /// Tries to unseal the sealed [`IpOrHost`] using the private key as Exit node.
246    /// No-op, if the data is already unsealed.
247    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/// Represents routing options in a mixnet with a maximum number of hops.
288#[derive(Debug, Clone, PartialEq, Eq)]
289#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
290pub enum RoutingOptions {
291    /// A fixed intermediate path consisting of at most [`RoutingOptions::MAX_INTERMEDIATE_HOPS`] hops.
292    IntermediatePath(BoundedVec<Address, { RoutingOptions::MAX_INTERMEDIATE_HOPS }>),
293    /// Random intermediate path with at least the given number of hops,
294    /// but at most [`RoutingOptions::MAX_INTERMEDIATE_HOPS`].
295    Hops(BoundedSize<{ RoutingOptions::MAX_INTERMEDIATE_HOPS }>),
296}
297
298impl RoutingOptions {
299    /// The maximum number of hops this instance can represent.
300    pub const MAX_INTERMEDIATE_HOPS: usize = 3;
301
302    /// Inverts the intermediate path if this is an instance of [`RoutingOptions::IntermediatePath`].
303    /// Otherwise, does nothing.
304    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    /// Returns the number of hops this instance represents.
312    /// This value is guaranteed to be between 0 and [`RoutingOptions::MAX_INTERMEDIATE_HOPS`].
313    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/// Allows finding saved SURBs based on different criteria.
322#[derive(Clone, Debug, Copy, PartialEq, Eq)]
323#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
324pub enum SurbMatcher {
325    /// Try to find a SURB that has the exact given [`HoprSenderId`].
326    Exact(HoprSenderId),
327    /// Find any SURB with the given pseudonym.
328    Pseudonym(HoprPseudonym),
329}
330
331impl SurbMatcher {
332    /// Get the pseudonym part of the match.
333    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/// Routing information containing forward or return routing options.
354///
355/// Information in this object represents the minimum required basis
356/// to generate forward paths and return paths.
357///
358/// See also [`RoutingOptions`].
359#[derive(Debug, Clone, PartialEq, Eq, strum::EnumIs)]
360#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
361pub enum DestinationRouting {
362    /// Forward routing using the destination address and path,
363    /// with a possible return path.
364    Forward {
365        /// The destination address.
366        destination: Address,
367        /// Our pseudonym shown to the destination.
368        ///
369        /// If not given, it will be resolved as random.
370        pseudonym: Option<HoprPseudonym>,
371        /// The path to the destination.
372        forward_options: RoutingOptions,
373        /// Optional return path.
374        return_options: Option<RoutingOptions>,
375    },
376    /// Return routing using a SURB with the given pseudonym.
377    ///
378    /// Will fail if no SURB for this pseudonym is found.
379    Return(SurbMatcher),
380}
381
382impl DestinationRouting {
383    /// Shortcut for routing that does not create any SURBs for a return path.
384    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/// Contains the resolved routing information for the packet.
395///
396/// Instance of this object is typically constructed via some resolution of a
397/// [`DestinationRouting`] instance.
398///
399/// It contains the actual forward and return paths for forward packets,
400/// or an actual SURB for return (reply) packets.
401#[derive(Clone, Debug, strum::EnumIs)]
402pub enum ResolvedTransportRouting {
403    /// Concrete routing information for a forward packet.
404    Forward {
405        /// Pseudonym of the sender.
406        pseudonym: HoprPseudonym,
407        /// Forward path.
408        forward_path: ValidatedPath,
409        /// Optional list of return paths.
410        return_paths: Vec<ValidatedPath>,
411    },
412    /// Sender ID and the corresponding SURB.
413    Return(HoprSenderId, HoprSurb),
414}
415
416impl ResolvedTransportRouting {
417    /// Shortcut for routing that does not create any SURBs for a return path.
418    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    /// Returns the number of return paths (SURBs) on the [`ResolvedTransportRouting::Forward`]
427    /// variant, or always 0 on the [`ResolvedTransportRouting::Return`] variant.
428    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}