1use std::{
2 fmt::{Display, Formatter},
3 str::FromStr,
4};
5
6use libp2p_identity::PeerId;
7
8use crate::errors::NetworkTypeError;
9
10#[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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28pub enum IpOrHost {
29 Dns(String, u16),
31 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 #[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 pub fn port(&self) -> u16 {
100 match &self {
101 IpOrHost::Dns(_, port) => *port,
102 IpOrHost::Ip(addr) => addr.port(),
103 }
104 }
105
106 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 pub fn is_dns(&self) -> bool {
116 matches!(self, IpOrHost::Dns(..))
117 }
118
119 pub fn is_ipv4(&self) -> bool {
125 matches!(self, IpOrHost::Ip(addr) if addr.is_ipv4())
126 }
127
128 pub fn is_ipv6(&self) -> bool {
134 matches!(self, IpOrHost::Ip(addr) if addr.is_ipv6())
135 }
136
137 pub fn is_loopback_ip(&self) -> bool {
143 matches!(self, IpOrHost::Ip(addr) if addr.ip().is_loopback())
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Hash, strum::EnumTryAs)]
193#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
194pub enum SealedHost {
195 Plain(IpOrHost),
197 Sealed(Box<[u8]>),
199}
200
201impl SealedHost {
202 const MAX_LEN_WITH_PADDING: usize = 50;
203 pub const PADDING_CHAR: char = '@';
208
209 pub fn seal(host: IpOrHost, peer_id: PeerId) -> crate::errors::Result<Self> {
211 let mut host_str = host.to_string();
212
213 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 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}