Skip to main content

hopr_utils/network_types/
types.rs

1use std::{
2    fmt::{Display, Formatter},
3    str::FromStr,
4};
5
6use libp2p_identity::PeerId;
7
8use super::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 = "network-types-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                static RESOLVER: tokio::sync::OnceCell<hickory_resolver::TokioResolver> =
78                    tokio::sync::OnceCell::const_new();
79
80                let resolver = RESOLVER
81                    .get_or_try_init(|| async {
82                        hickory_resolver::Resolver::builder_tokio()
83                            .map_err(std::io::Error::other)?
84                            .build()
85                            .map_err(std::io::Error::other)
86                    })
87                    .await?;
88
89                let lookup = resolver.lookup_ip(&name).await.map_err(std::io::Error::other)?;
90                Ok(lookup.iter().map(|ip| std::net::SocketAddr::new(ip, port)).collect())
91            }
92            IpOrHost::Ip(addr) => Ok(vec![addr]),
93        }
94    }
95
96    /// Gets the port number.
97    pub fn port(&self) -> u16 {
98        match &self {
99            IpOrHost::Dns(_, port) => *port,
100            IpOrHost::Ip(addr) => addr.port(),
101        }
102    }
103
104    /// Gets the unresolved DNS name or IP address as string.
105    pub fn host(&self) -> String {
106        match &self {
107            IpOrHost::Dns(host, _) => host.clone(),
108            IpOrHost::Ip(addr) => addr.ip().to_string(),
109        }
110    }
111
112    /// Checks if this instance is a [DNS name](IpOrHost::Dns).
113    pub fn is_dns(&self) -> bool {
114        matches!(self, IpOrHost::Dns(..))
115    }
116
117    /// Checks if this instance is an [IP address](IpOrHost::Ip) and whether it is
118    /// an IPv4 address.
119    ///
120    /// Always returns `false` if this instance is a [DNS name](IpOrHost::Dns),
121    /// i.e.: it does not perform any DNS resolution.
122    pub fn is_ipv4(&self) -> bool {
123        matches!(self, IpOrHost::Ip(addr) if addr.is_ipv4())
124    }
125
126    /// Checks if this instance is an [IP address](IpOrHost::Ip) and whether it is
127    /// an IPv6 address.
128    ///
129    /// Always returns `false` if this instance is a [DNS name](IpOrHost::Dns),
130    /// i.e.: it does not perform any DNS resolution.
131    pub fn is_ipv6(&self) -> bool {
132        matches!(self, IpOrHost::Ip(addr) if addr.is_ipv6())
133    }
134
135    /// Checks if this instance is an [IP address](IpOrHost::Ip) and whether it is
136    /// a loopback 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_loopback_ip(&self) -> bool {
141        matches!(self, IpOrHost::Ip(addr) if addr.ip().is_loopback())
142    }
143}
144
145/// Contains optionally encrypted [`IpOrHost`].
146///
147/// This is useful for hiding the [`IpOrHost`] instance from the Entry node.
148/// The client first encrypts the `IpOrHost` instance via [`SealedHost::seal`] using
149/// the Exit node's public key.
150/// Upon receiving the `SealedHost` instance by the Exit node, it can call
151/// [`SealedHost::unseal`] using its private key to get the original `IpOrHost` instance.
152///
153/// Sealing is fully randomized and therefore does not leak information about equal `IpOrHost`
154/// instances.
155///
156/// The length of the encrypted host is also obscured by the use of random padding before
157/// encryption.
158///
159/// ### Example
160/// ```no_run
161/// use hopr_types::crypto::prelude::{Keypair, OffchainKeypair};
162/// use hopr_utils::network_types::prelude::{IpOrHost, SealedHost};
163/// use libp2p_identity::PeerId;
164///
165/// # fn main() -> anyhow::Result<()> {
166/// let keypair = OffchainKeypair::random();
167///
168/// let exit_node_peer_id: PeerId = keypair.public().into();
169/// let host: IpOrHost = "127.0.0.1:1000".parse()?;
170///
171/// // On the Client
172/// let encrypted = SealedHost::seal(host.clone(), keypair.public().into())?;
173///
174/// // On the Exit node
175/// let decrypted = encrypted.unseal(&keypair)?;
176/// assert_eq!(host, decrypted);
177///
178/// // Plain SealedHost unseals trivially
179/// let plain_sealed: SealedHost = host.clone().into();
180/// assert_eq!(host, plain_sealed.try_into()?);
181///
182/// // The same host sealing is randomized
183/// let encrypted_1 = SealedHost::seal(host.clone(), keypair.public().into())?;
184/// let encrypted_2 = SealedHost::seal(host.clone(), keypair.public().into())?;
185/// assert_ne!(encrypted_1, encrypted_2);
186///
187/// # Ok(())
188/// # }
189/// ```
190#[derive(Debug, Clone, PartialEq, Eq, Hash, strum::EnumTryAs)]
191#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
192pub enum SealedHost {
193    /// Plain (not sealed) [`IpOrHost`]
194    Plain(IpOrHost),
195    /// Encrypted [`IpOrHost`]
196    Sealed(Box<[u8]>),
197}
198
199impl SealedHost {
200    const MAX_LEN_WITH_PADDING: usize = 50;
201    /// Character that can be appended to the host to obscure its length.
202    ///
203    /// User can add as many of this character to the host, and it will be removed
204    /// during unsealing.
205    pub const PADDING_CHAR: char = '@';
206
207    /// Seals the given [`IpOrHost`] using the Exit node's peer ID.
208    pub fn seal(host: IpOrHost, peer_id: PeerId) -> super::errors::Result<Self> {
209        let mut host_str = host.to_string();
210
211        // Add randomly long padding, so the length of the short hosts is obscured
212        if host_str.len() < Self::MAX_LEN_WITH_PADDING {
213            let pad_len = hopr_types::crypto_random::random_integer(0, (Self::MAX_LEN_WITH_PADDING as u64).into());
214            for _ in 0..pad_len {
215                host_str.push(Self::PADDING_CHAR);
216            }
217        }
218
219        hopr_types::crypto::seal::seal_data(host_str.as_bytes(), peer_id)
220            .map(Self::Sealed)
221            .map_err(|e| NetworkTypeError::Other(e.to_string()))
222    }
223
224    /// Tries to unseal the sealed [`IpOrHost`] using the private key as Exit node.
225    /// No-op, if the data is already unsealed.
226    pub fn unseal(self, key: &hopr_types::crypto::keypairs::OffchainKeypair) -> super::errors::Result<IpOrHost> {
227        match self {
228            SealedHost::Plain(host) => Ok(host),
229            SealedHost::Sealed(enc) => hopr_types::crypto::seal::unseal_data(&enc, key)
230                .map_err(|e| NetworkTypeError::Other(e.to_string()))
231                .and_then(|data| {
232                    String::from_utf8(data.into_vec())
233                        .map_err(|e| NetworkTypeError::Other(e.to_string()))
234                        .and_then(|s| IpOrHost::from_str(s.trim_end_matches(Self::PADDING_CHAR)))
235                }),
236        }
237    }
238}
239
240impl From<IpOrHost> for SealedHost {
241    fn from(value: IpOrHost) -> Self {
242        Self::Plain(value)
243    }
244}
245
246impl TryFrom<SealedHost> for IpOrHost {
247    type Error = NetworkTypeError;
248
249    fn try_from(value: SealedHost) -> Result<Self, Self::Error> {
250        match value {
251            SealedHost::Plain(host) => Ok(host),
252            SealedHost::Sealed(_) => Err(NetworkTypeError::SealedTarget),
253        }
254    }
255}
256
257impl std::fmt::Display for SealedHost {
258    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
259        match self {
260            SealedHost::Plain(h) => write!(f, "{h}"),
261            SealedHost::Sealed(_) => write!(f, "<redacted host>"),
262        }
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use hopr_types::crypto::prelude::{Keypair, OffchainKeypair};
269    #[cfg(all(feature = "network-types", feature = "runtime-tokio"))]
270    use {anyhow::anyhow, std::net::SocketAddr};
271
272    use super::*;
273
274    #[cfg(all(feature = "network-types", feature = "runtime-tokio"))]
275    #[tokio::test]
276    async fn ip_or_host_must_resolve_ip_address() -> anyhow::Result<()> {
277        let actual = IpOrHost::Ip("127.0.0.1:1000".parse()?).resolve_tokio().await?;
278
279        let actual = actual.first().ok_or(anyhow!("must resolve"))?;
280
281        let expected: SocketAddr = "127.0.0.1:1000".parse()?;
282
283        assert_eq!(*actual, expected);
284        Ok(())
285    }
286
287    #[test]
288    fn ip_or_host_should_parse_from_string() -> anyhow::Result<()> {
289        assert_eq!(
290            IpOrHost::Dns("some.dns.name.info".into(), 1234),
291            IpOrHost::from_str("some.dns.name.info:1234")?
292        );
293        assert_eq!(
294            IpOrHost::Ip("1.2.3.4:1234".parse()?),
295            IpOrHost::from_str("1.2.3.4:1234")?
296        );
297        Ok(())
298    }
299
300    #[ignore = "sealing is not implemented yet, see https://github.com/hoprnet/hoprnet/issues/7172"]
301    #[test]
302    fn sealing_adds_padding_to_hide_length() -> anyhow::Result<()> {
303        let peer_id: PeerId = OffchainKeypair::random().public().into();
304        let last_len = SealedHost::seal("127.0.0.1:1234".parse()?, peer_id)?
305            .try_as_sealed()
306            .unwrap()
307            .len();
308        for _ in 0..20 {
309            let current_len = SealedHost::seal("127.0.0.1:1234".parse()?, peer_id)?
310                .try_as_sealed()
311                .unwrap()
312                .len();
313            if current_len != last_len {
314                return Ok(());
315            }
316        }
317
318        anyhow::bail!("sealed length not randomized");
319    }
320}