Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions common/http-api-client/src/dns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,20 @@ async fn resolve(
) -> Result<Addrs, ResolveError> {
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 {
Expand Down
140 changes: 134 additions & 6 deletions common/http-api-client/src/dns/static_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,130 @@ 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<Mutex<HashMap<String, Vec<IpAddr>>>>,
static_addr_map: Arc<Mutex<HashMap<String, Entry>>>,
pre_resolve_timeout: Option<Duration>,
}

#[derive(Debug, Clone, Default)]
struct Entry {
valid_for_pre_resolve_until: Option<Instant>,
addrs: Vec<IpAddr>,
}

impl Entry {
fn new(addrs: Vec<IpAddr>) -> Self {
Self {
valid_for_pre_resolve_until: None,
addrs,
}
}
}

impl StaticResolver {
pub fn new(static_entries: HashMap<String, Vec<IpAddr>>) -> 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<String, Vec<IpAddr>> {
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<Vec<IpAddr>> {
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<Vec<IpAddr>> {
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<String, Entry>>,
name: &str,
timeout: Option<Duration>,
) -> Option<Entry> {
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())
}
}

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)),
);
Expand Down Expand Up @@ -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());
}
}
7 changes: 6 additions & 1 deletion nym-registration-client/src/builder/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)))
}
}
Expand Down
23 changes: 19 additions & 4 deletions nym-registration-client/src/builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Loading