diff --git a/common/http-api-client/src/dns.rs b/common/http-api-client/src/dns.rs index 4b37dc41f51..db3424e8e61 100644 --- a/common/http-api-client/src/dns.rs +++ b/common/http-api-client/src/dns.rs @@ -181,6 +181,20 @@ async fn resolve( ) -> Result { let resolver = resolver.get_or_init(|| HickoryDnsResolver::new_resolver(independent)); + // try checking the static table to see if any of the addresses in the table have been + // looked up previously within the timeout to where we are not yet ready to try the + // default resolver yet again. + if let Some(ref static_resolver) = maybe_static { + let resolver = + static_resolver.get_or_init(|| HickoryDnsResolver::new_static_fallback(independent)); + + if let Some(addrs) = resolver.pre_resolve(name.as_str()) { + let addrs: Addrs = + Box::new(addrs.into_iter().map(|ip_addr| SocketAddr::new(ip_addr, 0))); + return Ok(addrs); + } + } + // Attempt a lookup using the primary resolver let resolve_fut = tokio::time::timeout(overall_dns_timeout, resolver.lookup_ip(name.as_str())); let primary_err = match resolve_fut.await { diff --git a/common/http-api-client/src/dns/static_resolver.rs b/common/http-api-client/src/dns/static_resolver.rs index 030ae4419da..86600447358 100644 --- a/common/http-api-client/src/dns/static_resolver.rs +++ b/common/http-api-client/src/dns/static_resolver.rs @@ -3,27 +3,114 @@ use crate::dns::ResolveError; use std::{ collections::HashMap, net::{IpAddr, SocketAddr}, - sync::{Arc, Mutex}, + sync::{Arc, Mutex, MutexGuard}, + time::{Duration, Instant}, }; use reqwest::dns::{Addrs, Name, Resolve, Resolving}; use tracing::*; +const DEFAULT_PRE_RESOLVE_TIMEOUT: Duration = super::DEFAULT_POSITIVE_LOOKUP_CACHE_TTL; + #[derive(Debug, Default, Clone)] pub struct StaticResolver { - static_addr_map: Arc>>>, + static_addr_map: Arc>>, + pre_resolve_timeout: Option, +} + +#[derive(Debug, Clone, Default)] +struct Entry { + valid_for_pre_resolve_until: Option, + addrs: Vec, +} + +impl Entry { + fn new(addrs: Vec) -> Self { + Self { + valid_for_pre_resolve_until: None, + addrs, + } + } } impl StaticResolver { pub fn new(static_entries: HashMap>) -> StaticResolver { debug!("building static resolver"); + let static_entries = static_entries + .into_iter() + .map(|(name, ips)| (name, Entry::new(ips))) + .collect(); Self { static_addr_map: Arc::new(Mutex::new(static_entries)), + pre_resolve_timeout: Some(DEFAULT_PRE_RESOLVE_TIMEOUT), } } + /// Return the full set of domain names and associated addresses stored in this static lookup table pub fn get_addrs(&self) -> HashMap> { - self.static_addr_map.lock().unwrap().clone() + let mut out = HashMap::new(); + self.static_addr_map + .lock() + .unwrap() + .iter() + .for_each(|(name, entry)| { + out.insert(name.clone(), entry.addrs.clone()); + }); + out + } + + /// Change the timeout for which domains can be pre-resolved after they are looked up in the + /// static lookup table. + #[allow(unused)] + pub fn with_pre_resolve_timeout(mut self, timeout: Duration) -> Self { + self.pre_resolve_timeout = Some(timeout); + self + } + + /// Try looking up the domain in the static table. If the domain is in the table AND we have + /// recently (within the configured timeout) looked it up previously in this static table using + /// a regular resolve. + pub fn pre_resolve(&self, name: &str) -> Option> { + debug!("found {name:?} in pre-resolve static table resolver"); + + self.pre_resolve_timeout?; + + self.static_addr_map + .lock() + .unwrap() + .get(name) + .filter(|e| { + e.valid_for_pre_resolve_until + .is_some_and(|t| t > Instant::now()) + }) + .map(|e| e.addrs.clone()) + } + + #[allow(unused)] + pub fn resolve_str(&self, name: &str) -> Option> { + Self::resolve_inner( + self.static_addr_map.lock().unwrap(), + name, + self.pre_resolve_timeout, + ) + .map(|e| e.addrs) + } + + fn resolve_inner( + mut table: MutexGuard<'_, HashMap>, + name: &str, + timeout: Option, + ) -> Option { + let resolved = table.get_mut(name)?; + + debug!("found {name:?} in static table resolver"); + + if let Some(pre_resolve_timeout) = timeout { + // We had to look this entry up and a pre-resolve duration is defined, so it will + // trigger in pre-resolve lookups for the next _timeout_ window. + resolved.valid_for_pre_resolve_until = Some(Instant::now() + pre_resolve_timeout); + } + Some(resolved.clone()) } } @@ -31,15 +118,15 @@ impl Resolve for StaticResolver { fn resolve(&self, name: Name) -> Resolving { debug!("looking up {name:?} in static resolver"); let addr_map = self.static_addr_map.clone(); + let timeout = self.pre_resolve_timeout; Box::pin(async move { let addr_map = addr_map.lock().unwrap(); - let lookup = match addr_map.get(name.as_str()) { + let lookup = match Self::resolve_inner(addr_map, name.as_str(), timeout) { None => return Err(ResolveError::StaticLookupMiss.into()), - Some(addrs) => addrs, + Some(entry) => entry.addrs, }; let addrs: Addrs = Box::new( lookup - .clone() .into_iter() .map(|ip_addr| SocketAddr::new(ip_addr, 0)), ); @@ -86,4 +173,45 @@ mod test { Ok(()) } + + #[test] + fn static_lookup_pre_resolve() { + let example_duration = Duration::from_secs(3); + let example_domain = String::from("static.nymvpn.com"); + let mut addr_map = HashMap::new(); + let example_ip4: IpAddr = "10.10.10.10".parse().unwrap(); + let example_ip6: IpAddr = "dead::beef".parse().unwrap(); + addr_map.insert(example_domain.clone(), vec![example_ip4, example_ip6]); + + let resolver = StaticResolver::new(addr_map).with_pre_resolve_timeout(example_duration); + + // ensure that attempting to pre-resolve without first resolving returns none + let result = resolver.pre_resolve(&example_domain); + assert!(result.is_none()); + + // resolving should now update the pre-resolve validity timeout for the entry + let entry = StaticResolver::resolve_inner( + resolver.static_addr_map.lock().unwrap(), + &example_domain, + Some(example_duration), + ) + .expect("missing entry???!!!!"); + assert!( + entry + .valid_for_pre_resolve_until + .is_some_and(|t| t < Instant::now() + example_duration) + ); + + // check that pre-resolve now returns the expected record + let addrs = resolver + .pre_resolve(&example_domain) + .expect("entry should be in pre-resolve now"); + assert!(addrs.contains(&example_ip4)); + + std::thread::sleep(example_duration); + + // check that after the timeout duration the pre-resolve no longer returns the address + let result = resolver.pre_resolve(&example_domain); + assert!(result.is_none()); + } } diff --git a/nym-registration-client/src/builder/config.rs b/nym-registration-client/src/builder/config.rs index d566c397f0f..a347cce87d2 100644 --- a/nym-registration-client/src/builder/config.rs +++ b/nym-registration-client/src/builder/config.rs @@ -113,9 +113,10 @@ impl BuilderConfig { RememberMe::new_mixnet() }; + let identity = self.entry_node.node.identity.to_string(); let builder = builder .with_user_agent(self.user_agent) - .request_gateway(self.entry_node.node.identity.to_string()) + .request_gateway(identity.clone()) .network_details(self.network_env) .debug_config(debug_config) .credentials_mode(true) @@ -128,9 +129,13 @@ impl BuilderConfig { builder .build() + .inspect(|_| tracing::debug!("successfully built reg client for {}", identity)) + .inspect_err(|e| tracing::debug!("failed to build reg client for {}: {e}", identity)) .map_err(|err| RegistrationClientError::BuildMixnetClient(Box::new(err)))? .connect_to_mixnet() .await + .inspect(|_| tracing::debug!("successfully connected reg client for {}", identity)) + .inspect_err(|e| tracing::debug!("failed to connect reg client for {}: {e}", identity)) .map_err(|err| RegistrationClientError::ConnectToMixnet(Box::new(err))) } } diff --git a/nym-registration-client/src/builder/mod.rs b/nym-registration-client/src/builder/mod.rs index 7993f922d27..8a1c2e59e2a 100644 --- a/nym-registration-client/src/builder/mod.rs +++ b/nym-registration-client/src/builder/mod.rs @@ -38,6 +38,7 @@ impl RegistrationClientBuilder { let (event_tx, event_rx) = mpsc::unbounded(); let nyxd_client = get_nyxd_client(&self.config.network_env)?; + let mixnet_client_startup_timeout = self.config.mixnet_client_startup_timeout; let (mixnet_client, bandwidth_controller): ( MixnetClient, @@ -46,20 +47,34 @@ impl RegistrationClientBuilder { let builder = MixnetClientBuilder::new_with_storage(mixnet_client_storage) .event_tx(EventSender(event_tx)); let mixnet_client = tokio::time::timeout( - self.config.mixnet_client_startup_timeout, + mixnet_client_startup_timeout, self.config.build_and_connect_mixnet_client(builder), ) - .await??; + .await + .inspect_err(|_| { + tracing::warn!( + "mixnet client connection timed out after {:?}", + mixnet_client_startup_timeout + ) + })? + .inspect_err(|e| tracing::warn!("mixnet build/connect error: {e}"))?; let bandwidth_controller = Box::new(BandwidthController::new(credential_storage, nyxd_client)); (mixnet_client, bandwidth_controller) } else { let builder = MixnetClientBuilder::new_ephemeral().event_tx(EventSender(event_tx)); let mixnet_client = tokio::time::timeout( - self.config.mixnet_client_startup_timeout, + mixnet_client_startup_timeout, self.config.build_and_connect_mixnet_client(builder), ) - .await??; + .await + .inspect_err(|_| { + tracing::warn!( + "mixnet client connection timed out after {:?}", + mixnet_client_startup_timeout + ) + })? + .inspect_err(|e| tracing::warn!("mixnet build/connect error: {e}"))?; let bandwidth_controller = Box::new(BandwidthController::new( EphemeralCredentialStorage::default(), nyxd_client,