Skip to main content

hopr_network_types/
types.rs

1use std::{
2    fmt::{Display, Formatter},
3    str::FromStr,
4};
5
6use libp2p_identity::PeerId;
7
8use crate::errors::NetworkTypeError;
9
10/// Lists some of the IP protocols.
11#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, strum::Display, strum::EnumString)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13#[strum(serialize_all = "lowercase", ascii_case_insensitive)]
14#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
15pub enum IpProtocol {
16    TCP,
17    UDP,
18}
19
20/// Implements a host name with port.
21/// This could be either a DNS name with port
22/// or an IP address with port represented by [`std::net::SocketAddr`].
23///
24/// This object implements [`std::net::ToSocketAddrs`] which performs automatic
25/// DNS name resolution in case this is a [`IpOrHost::Dns`] instance.
26#[derive(Debug, Clone, PartialEq, Eq, Hash)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28pub enum IpOrHost {
29    /// DNS name and port.
30    Dns(String, u16),
31    /// IP address with port.
32    Ip(std::net::SocketAddr),
33}
34
35impl Display for IpOrHost {
36    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
37        match &self {
38            IpOrHost::Dns(host, port) => write!(f, "{host}:{port}"),
39            IpOrHost::Ip(ip) => write!(f, "{ip}"),
40        }
41    }
42}
43
44impl FromStr for IpOrHost {
45    type Err = NetworkTypeError;
46
47    fn from_str(s: &str) -> Result<Self, Self::Err> {
48        if let Ok(addr) = std::net::SocketAddr::from_str(s) {
49            Ok(IpOrHost::Ip(addr))
50        } else {
51            s.split_once(":")
52                .ok_or(NetworkTypeError::Other("missing port delimiter".into()))
53                .and_then(|(host, port_str)| {
54                    u16::from_str(port_str)
55                        .map(|port| IpOrHost::Dns(host.to_string(), port))
56                        .map_err(|_| NetworkTypeError::Other("invalid port number".into()))
57                })
58        }
59    }
60}
61
62impl From<std::net::SocketAddr> for IpOrHost {
63    fn from(value: std::net::SocketAddr) -> Self {
64        IpOrHost::Ip(value)
65    }
66}
67
68impl IpOrHost {
69    /// Tries to resolve the DNS name and returns all IP addresses found.
70    /// If this enum is already an IP address and port, it will simply return it.
71    ///
72    /// Uses `tokio` resolver.
73    #[cfg(feature = "runtime-tokio")]
74    pub async fn resolve_tokio(self) -> std::io::Result<Vec<std::net::SocketAddr>> {
75        match self {
76            IpOrHost::Dns(name, port) => {
77                #[cfg(test)]
78                let resolver = hickory_resolver::Resolver::builder_with_config(
79                    hickory_resolver::config::ResolverConfig::default(),
80                    hickory_resolver::net::runtime::TokioRuntimeProvider::default(),
81                )
82                .build()
83                .map_err(std::io::Error::other)?;
84
85                #[cfg(not(test))]
86                let resolver = hickory_resolver::Resolver::builder_tokio()
87                    .map_err(std::io::Error::other)?
88                    .build()
89                    .map_err(std::io::Error::other)?;
90
91                let lookup = resolver.lookup_ip(&name).await.map_err(std::io::Error::other)?;
92                Ok(lookup.iter().map(|ip| std::net::SocketAddr::new(ip, port)).collect())
93            }
94            IpOrHost::Ip(addr) => Ok(vec![addr]),
95        }
96    }
97
98    /// Gets the port number.
99    pub fn port(&self) -> u16 {
100        match &self {
101            IpOrHost::Dns(_, port) => *port,
102            IpOrHost::Ip(addr) => addr.port(),
103        }
104    }
105
106    /// Gets the unresolved DNS name or IP address as string.
107    pub fn host(&self) -> String {
108        match &self {
109            IpOrHost::Dns(host, _) => host.clone(),
110            IpOrHost::Ip(addr) => addr.ip().to_string(),
111        }
112    }
113
114    /// Checks if this instance is a [DNS name](IpOrHost::Dns).
115    pub fn is_dns(&self) -> bool {
116        matches!(self, IpOrHost::Dns(..))
117    }
118
119    /// Checks if this instance is an [IP address](IpOrHost::Ip) and whether it is
120    /// an IPv4 address.
121    ///
122    /// Always returns `false` if this instance is a [DNS name](IpOrHost::Dns),
123    /// i.e.: it does not perform any DNS resolution.
124    pub fn is_ipv4(&self) -> bool {
125        matches!(self, IpOrHost::Ip(addr) if addr.is_ipv4())
126    }
127
128    /// Checks if this instance is an [IP address](IpOrHost::Ip) and whether it is
129    /// an IPv6 address.
130    ///
131    /// Always returns `false` if this instance is a [DNS name](IpOrHost::Dns),
132    /// i.e.: it does not perform any DNS resolution.
133    pub fn is_ipv6(&self) -> bool {
134        matches!(self, IpOrHost::Ip(addr) if addr.is_ipv6())
135    }
136
137    /// Checks if this instance is an [IP address](IpOrHost::Ip) and whether it is
138    /// a loopback address.
139    ///
140    /// Always returns `false` if this instance is a [DNS name](IpOrHost::Dns),
141    /// i.e.: it does not perform any DNS resolution.
142    pub fn is_loopback_ip(&self) -> bool {
143        matches!(self, IpOrHost::Ip(addr) if addr.ip().is_loopback())
144    }
145}
146
147/// Contains optionally encrypted [`IpOrHost`].
148///
149/// This is useful for hiding the [`IpOrHost`] instance from the Entry node.
150/// The client first encrypts the `IpOrHost` instance via [`SealedHost::seal`] using
151/// the Exit node's public key.
152/// Upon receiving the `SealedHost` instance by the Exit node, it can call
153/// [`SealedHost::unseal`] using its private key to get the original `IpOrHost` instance.
154///
155/// Sealing is fully randomized and therefore does not leak information about equal `IpOrHost`
156/// instances.
157///
158/// The length of the encrypted host is also obscured by the use of random padding before
159/// encryption.
160///
161/// ### Example
162/// ```no_run
163/// use hopr_network_types::prelude::{IpOrHost, SealedHost};
164/// use hopr_types::crypto::prelude::{Keypair, OffchainKeypair};
165/// use libp2p_identity::PeerId;
166///
167/// # fn main() -> anyhow::Result<()> {
168/// let keypair = OffchainKeypair::random();
169///
170/// let exit_node_peer_id: PeerId = keypair.public().into();
171/// let host: IpOrHost = "127.0.0.1:1000".parse()?;
172///
173/// // On the Client
174/// let encrypted = SealedHost::seal(host.clone(), keypair.public().into())?;
175///
176/// // On the Exit node
177/// let decrypted = encrypted.unseal(&keypair)?;
178/// assert_eq!(host, decrypted);
179///
180/// // Plain SealedHost unseals trivially
181/// let plain_sealed: SealedHost = host.clone().into();
182/// assert_eq!(host, plain_sealed.try_into()?);
183///
184/// // The same host sealing is randomized
185/// let encrypted_1 = SealedHost::seal(host.clone(), keypair.public().into())?;
186/// let encrypted_2 = SealedHost::seal(host.clone(), keypair.public().into())?;
187/// assert_ne!(encrypted_1, encrypted_2);
188///
189/// # Ok(())
190/// # }
191/// ```
192#[derive(Debug, Clone, PartialEq, Eq, Hash, strum::EnumTryAs)]
193#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
194pub enum SealedHost {
195    /// Plain (not sealed) [`IpOrHost`]
196    Plain(IpOrHost),
197    /// Encrypted [`IpOrHost`]
198    Sealed(Box<[u8]>),
199}
200
201impl SealedHost {
202    const MAX_LEN_WITH_PADDING: usize = 50;
203    /// Character that can be appended to the host to obscure its length.
204    ///
205    /// User can add as many of this character to the host, and it will be removed
206    /// during unsealing.
207    pub const PADDING_CHAR: char = '@';
208
209    /// Seals the given [`IpOrHost`] using the Exit node's peer ID.
210    pub fn seal(host: IpOrHost, peer_id: PeerId) -> crate::errors::Result<Self> {
211        let mut host_str = host.to_string();
212
213        // Add randomly long padding, so the length of the short hosts is obscured
214        if host_str.len() < Self::MAX_LEN_WITH_PADDING {
215            let pad_len = hopr_types::crypto_random::random_integer(0, (Self::MAX_LEN_WITH_PADDING as u64).into());
216            for _ in 0..pad_len {
217                host_str.push(Self::PADDING_CHAR);
218            }
219        }
220
221        hopr_types::crypto::seal::seal_data(host_str.as_bytes(), peer_id)
222            .map(Self::Sealed)
223            .map_err(|e| NetworkTypeError::Other(e.to_string()))
224    }
225
226    /// Tries to unseal the sealed [`IpOrHost`] using the private key as Exit node.
227    /// No-op, if the data is already unsealed.
228    pub fn unseal(self, key: &hopr_types::crypto::keypairs::OffchainKeypair) -> crate::errors::Result<IpOrHost> {
229        match self {
230            SealedHost::Plain(host) => Ok(host),
231            SealedHost::Sealed(enc) => hopr_types::crypto::seal::unseal_data(&enc, key)
232                .map_err(|e| NetworkTypeError::Other(e.to_string()))
233                .and_then(|data| {
234                    String::from_utf8(data.into_vec())
235                        .map_err(|e| NetworkTypeError::Other(e.to_string()))
236                        .and_then(|s| IpOrHost::from_str(s.trim_end_matches(Self::PADDING_CHAR)))
237                }),
238        }
239    }
240}
241
242impl From<IpOrHost> for SealedHost {
243    fn from(value: IpOrHost) -> Self {
244        Self::Plain(value)
245    }
246}
247
248impl TryFrom<SealedHost> for IpOrHost {
249    type Error = NetworkTypeError;
250
251    fn try_from(value: SealedHost) -> Result<Self, Self::Error> {
252        match value {
253            SealedHost::Plain(host) => Ok(host),
254            SealedHost::Sealed(_) => Err(NetworkTypeError::SealedTarget),
255        }
256    }
257}
258
259impl std::fmt::Display for SealedHost {
260    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
261        match self {
262            SealedHost::Plain(h) => write!(f, "{h}"),
263            SealedHost::Sealed(_) => write!(f, "<redacted host>"),
264        }
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use hopr_types::crypto::prelude::{Keypair, OffchainKeypair};
271    #[cfg(feature = "runtime-tokio")]
272    use {anyhow::anyhow, std::net::SocketAddr};
273
274    use super::*;
275
276    #[cfg(feature = "runtime-tokio")]
277    #[tokio::test]
278    async fn ip_or_host_must_resolve_dns_name() -> anyhow::Result<()> {
279        match IpOrHost::Dns("localhost".to_string(), 1000)
280            .resolve_tokio()
281            .await?
282            .first()
283            .ok_or(anyhow!("must resolve"))?
284        {
285            SocketAddr::V4(addr) => assert_eq!(*addr, "127.0.0.1:1000".parse()?),
286            SocketAddr::V6(addr) => assert_eq!(*addr, "[::1]:1000".parse()?),
287        }
288        Ok(())
289    }
290
291    #[cfg(feature = "runtime-tokio")]
292    #[tokio::test]
293    async fn ip_or_host_must_resolve_ip_address() -> anyhow::Result<()> {
294        let actual = IpOrHost::Ip("127.0.0.1:1000".parse()?).resolve_tokio().await?;
295
296        let actual = actual.first().ok_or(anyhow!("must resolve"))?;
297
298        let expected: SocketAddr = "127.0.0.1:1000".parse()?;
299
300        assert_eq!(*actual, expected);
301        Ok(())
302    }
303
304    #[test]
305    fn ip_or_host_should_parse_from_string() -> anyhow::Result<()> {
306        assert_eq!(
307            IpOrHost::Dns("some.dns.name.info".into(), 1234),
308            IpOrHost::from_str("some.dns.name.info:1234")?
309        );
310        assert_eq!(
311            IpOrHost::Ip("1.2.3.4:1234".parse()?),
312            IpOrHost::from_str("1.2.3.4:1234")?
313        );
314        Ok(())
315    }
316
317    #[ignore = "sealing is not implemented yet, see https://github.com/hoprnet/hoprnet/issues/7172"]
318    #[test]
319    fn sealing_adds_padding_to_hide_length() -> anyhow::Result<()> {
320        let peer_id: PeerId = OffchainKeypair::random().public().into();
321        let last_len = SealedHost::seal("127.0.0.1:1234".parse()?, peer_id)?
322            .try_as_sealed()
323            .unwrap()
324            .len();
325        for _ in 0..20 {
326            let current_len = SealedHost::seal("127.0.0.1:1234".parse()?, peer_id)?
327                .try_as_sealed()
328                .unwrap()
329                .len();
330            if current_len != last_len {
331                return Ok(());
332            }
333        }
334
335        anyhow::bail!("sealed length not randomized");
336    }
337}