use std::fmt::Debug;
use std::future::Future;
use std::hash::Hash;
use std::net::SocketAddr;
use std::num::NonZeroUsize;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::{Duration, Instant};
use futures::future::FutureExt;
use log::debug;
use lru::LruCache;
use parking_lot::Mutex;
use tokio::sync::{Mutex as AsyncMutex, RwLock};
use crate::address::{NetLocation, ResolvedLocation};
type ResolveFuture = Pin<Box<dyn Future<Output = std::io::Result<Vec<SocketAddr>>> + Send>>;
const CACHING_NATIVE_RESOLVER_CACHE_CAPACITY: usize = 10_000;
const RESOLVER_CACHE_CAPACITY: usize = 1_024;
fn bounded_lru<K: Hash + Eq, V>(capacity: usize) -> LruCache<K, V> {
let mut cache = LruCache::unbounded();
cache.resize(NonZeroUsize::new(capacity).unwrap());
cache
}
pub trait Resolver: Send + Sync + Debug {
fn resolve_location(&self, location: &NetLocation) -> ResolveFuture;
}
pub struct TimeoutResolver<T> {
inner: T,
timeout: Duration,
}
impl<T: Resolver> TimeoutResolver<T> {
#[allow(dead_code)]
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
#[allow(dead_code)]
pub fn new(inner: T) -> Self {
Self {
inner,
timeout: Self::DEFAULT_TIMEOUT,
}
}
pub fn with_timeout(inner: T, timeout: Duration) -> Self {
Self { inner, timeout }
}
}
impl<T: Resolver> Debug for TimeoutResolver<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TimeoutResolver")
.field("inner", &self.inner)
.field("timeout", &self.timeout)
.finish()
}
}
impl<T: Resolver> Resolver for TimeoutResolver<T> {
fn resolve_location(&self, location: &NetLocation) -> ResolveFuture {
if location.to_socket_addr_nonblocking().is_some() {
let loc = location.clone();
return Box::pin(async move { Ok(vec![loc.to_socket_addr_nonblocking().unwrap()]) });
}
let inner_future = self.inner.resolve_location(location);
let timeout_duration = self.timeout;
let location_str = location.to_string();
Box::pin(async move {
match tokio::time::timeout(timeout_duration, inner_future).await {
Ok(result) => result,
Err(_) => Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!(
"DNS resolution for {} timed out after {:?}",
location_str, timeout_duration
),
)),
}
})
}
}
type ResolverFactoryFuture =
Pin<Box<dyn Future<Output = std::io::Result<Arc<dyn Resolver>>> + Send>>;
pub type ResolverFactory = Arc<dyn Fn() -> ResolverFactoryFuture + Send + Sync>;
#[derive(Debug, Clone, Copy)]
pub struct RefreshPolicy {
pub max_idle: Duration,
pub retry_once_after_refresh: bool,
}
pub struct RefreshingResolver {
factory: ResolverFactory,
inner: Arc<RwLock<Arc<dyn Resolver>>>,
refresh_lock: Arc<AsyncMutex<()>>,
refresh_state: Arc<Mutex<RefreshState>>,
policy: RefreshPolicy,
description: String,
}
struct RefreshState {
last_activity_at: Option<Instant>,
generation: u64,
}
impl Debug for RefreshingResolver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RefreshingResolver")
.field("description", &self.description)
.field("max_idle", &self.policy.max_idle)
.finish()
}
}
impl RefreshingResolver {
pub async fn new(
factory: ResolverFactory,
policy: RefreshPolicy,
description: String,
) -> std::io::Result<Self> {
let inner = factory().await?;
Ok(Self {
factory,
inner: Arc::new(RwLock::new(inner)),
refresh_lock: Arc::new(AsyncMutex::new(())),
refresh_state: Arc::new(Mutex::new(RefreshState {
last_activity_at: None,
generation: 0,
})),
policy,
description,
})
}
fn should_refresh_for_error(err: &std::io::Error) -> bool {
matches!(
err.kind(),
std::io::ErrorKind::TimedOut
| std::io::ErrorKind::ConnectionReset
| std::io::ErrorKind::BrokenPipe
| std::io::ErrorKind::ConnectionAborted
| std::io::ErrorKind::UnexpectedEof
| std::io::ErrorKind::NotConnected
)
}
}
impl Resolver for RefreshingResolver {
fn resolve_location(&self, location: &NetLocation) -> ResolveFuture {
if let Some(socket_addr) = location.to_socket_addr_nonblocking() {
return Box::pin(async move { Ok(vec![socket_addr]) });
}
let location = location.clone();
let inner = self.inner.clone();
let refresh_lock = self.refresh_lock.clone();
let factory = self.factory.clone();
let refresh_state = self.refresh_state.clone();
let policy = self.policy;
let description = self.description.clone();
Box::pin(async move {
if matches!(refresh_state.lock().last_activity_at, Some(last) if last.elapsed() > policy.max_idle)
{
let _guard = refresh_lock.lock().await;
if matches!(refresh_state.lock().last_activity_at, Some(last) if last.elapsed() > policy.max_idle)
{
log::info!(
"RefreshingResolver ({}): rebuilding after idle timeout ({:?})",
description,
policy.max_idle
);
match factory().await {
Ok(fresh) => {
*inner.write().await = fresh;
let mut state = refresh_state.lock();
state.last_activity_at = Some(Instant::now());
state.generation = state.generation.wrapping_add(1);
}
Err(e) => {
log::warn!(
"RefreshingResolver ({}): idle refresh failed: {}",
description,
e
);
}
}
}
}
let current_generation = refresh_state.lock().generation;
let current = inner.read().await.clone();
match current.resolve_location(&location).await {
Ok(addrs) => {
refresh_state.lock().last_activity_at = Some(Instant::now());
Ok(addrs)
}
Err(err)
if policy.retry_once_after_refresh
&& RefreshingResolver::should_refresh_for_error(&err) =>
{
let _guard = refresh_lock.lock().await;
if refresh_state.lock().generation != current_generation {
let current = inner.read().await.clone();
let addrs = current.resolve_location(&location).await?;
refresh_state.lock().last_activity_at = Some(Instant::now());
return Ok(addrs);
}
log::info!(
"RefreshingResolver ({}): refresh-on-error ({}) for {}",
description,
err.kind(),
location
);
match factory().await {
Ok(fresh) => {
*inner.write().await = fresh.clone();
{
let mut state = refresh_state.lock();
state.generation = state.generation.wrapping_add(1);
}
let addrs = fresh.resolve_location(&location).await?;
refresh_state.lock().last_activity_at = Some(Instant::now());
Ok(addrs)
}
Err(factory_err) => {
log::warn!(
"RefreshingResolver ({}): error-refresh factory failed: {}",
description,
factory_err
);
Err(err)
}
}
}
Err(err) => Err(err),
}
})
}
}
#[derive(Debug, Default)]
pub struct NativeResolver;
impl NativeResolver {
pub fn new() -> Self {
NativeResolver {}
}
}
impl Resolver for NativeResolver {
fn resolve_location(&self, location: &NetLocation) -> ResolveFuture {
let address = location.address().clone();
let port = location.port();
Box::pin(
tokio::net::lookup_host((address.to_string(), port)).map(move |result| {
let ret = result.map(|r| {
r.filter(|addr| !addr.ip().is_unspecified())
.collect::<Vec<_>>()
});
debug!("NativeResolver resolved {address}:{port} -> {ret:?}");
ret
}),
)
}
}
pub async fn resolve_single_address(
resolver: &Arc<dyn Resolver>,
location: &NetLocation,
) -> std::io::Result<SocketAddr> {
if let Some(socket_addr) = location.to_socket_addr_nonblocking() {
return Ok(socket_addr);
}
let resolve_results = resolver.resolve_location(location).await?;
if resolve_results.is_empty() {
return Err(std::io::Error::other(format!(
"could not resolve location: {location}"
)));
}
Ok(resolve_results[0])
}
pub async fn resolve_addresses(
resolver: &Arc<dyn Resolver>,
location: &NetLocation,
) -> std::io::Result<Vec<SocketAddr>> {
if let Some(socket_addr) = location.to_socket_addr_nonblocking() {
return Ok(vec![socket_addr]);
}
let addrs = resolver.resolve_location(location).await?;
if addrs.is_empty() {
return Err(std::io::Error::other(format!(
"could not resolve location: {location}"
)));
}
Ok(addrs)
}
pub async fn resolve_location(
location: &mut ResolvedLocation,
resolver: &Arc<dyn Resolver>,
) -> std::io::Result<SocketAddr> {
if let Some(addr) = location.resolved_addr() {
return Ok(addr);
}
let addr = resolve_single_address(resolver, location.location()).await?;
location.set_resolved(addr);
Ok(addr)
}
pub struct CachingNativeResolver {
cache: Arc<parking_lot::Mutex<LruCache<NetLocation, CachedResolveResult>>>,
result_timeout_secs: u64,
}
struct CachedResolveResult {
timestamp: Instant,
addr: SocketAddr,
}
impl std::fmt::Debug for CachingNativeResolver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CachingNativeResolver")
.field("result_timeout_secs", &self.result_timeout_secs)
.finish()
}
}
impl CachingNativeResolver {
pub const DEFAULT_RESULT_TIMEOUT_SECS: u64 = 60 * 60;
pub fn new() -> Self {
Self::with_timeout(Self::DEFAULT_RESULT_TIMEOUT_SECS)
}
pub fn with_timeout(result_timeout_secs: u64) -> Self {
Self {
cache: Arc::new(parking_lot::Mutex::new(bounded_lru(
CACHING_NATIVE_RESOLVER_CACHE_CAPACITY,
))),
result_timeout_secs,
}
}
}
impl Default for CachingNativeResolver {
fn default() -> Self {
Self::new()
}
}
impl Resolver for CachingNativeResolver {
fn resolve_location(&self, location: &NetLocation) -> ResolveFuture {
{
let mut cache = self.cache.lock();
if let Some(cached) = cache.get(location)
&& Instant::now().duration_since(cached.timestamp)
<= Duration::from_secs(self.result_timeout_secs)
{
let addr = cached.addr;
return Box::pin(async move { Ok(vec![addr]) });
}
cache.pop(location);
}
let location = location.clone();
let cache = self.cache.clone();
Box::pin(async move {
let address = location.address().to_string();
let port = location.port();
let result = tokio::net::lookup_host((address.clone(), port)).await?;
let addrs: Vec<SocketAddr> =
result.filter(|addr| !addr.ip().is_unspecified()).collect();
if addrs.is_empty() {
return Err(std::io::Error::other(format!(
"DNS lookup returned no addresses for {address}"
)));
}
cache.lock().put(
location,
CachedResolveResult {
timestamp: Instant::now(),
addr: addrs[0],
},
);
debug!("CachingNativeResolver resolved {address}:{port} -> {addrs:?}");
Ok(addrs)
})
}
}
pub struct ResolverCache {
resolver: Arc<dyn Resolver>,
cache: LruCache<NetLocation, (Instant, SocketAddr)>,
pending: Option<(NetLocation, ResolveFuture)>,
result_timeout_secs: u64,
}
impl ResolverCache {
pub const DEFAULT_RESULT_TIMEOUT_SECS: u64 = 60 * 60;
pub fn new(resolver: Arc<dyn Resolver>) -> Self {
Self::new_with_timeout(resolver, Self::DEFAULT_RESULT_TIMEOUT_SECS)
}
pub fn new_with_timeout(resolver: Arc<dyn Resolver>, result_timeout_secs: u64) -> Self {
Self {
resolver,
cache: bounded_lru(RESOLVER_CACHE_CAPACITY),
pending: None,
result_timeout_secs,
}
}
#[allow(
dead_code,
reason = "Public library API; the binary uses poll_resolve_location"
)]
pub async fn resolve_location(&mut self, target: &NetLocation) -> std::io::Result<SocketAddr> {
self.pending = None;
if let Some(socket_addr) = target.to_socket_addr_nonblocking() {
return Ok(socket_addr);
}
if let Some((ts, addr)) = self.cache.get(target) {
if Instant::now().duration_since(*ts) <= Duration::from_secs(self.result_timeout_secs) {
return Ok(*addr);
}
self.cache.pop(target);
}
let addrs = self.resolver.resolve_location(target).await?;
if addrs.is_empty() {
return Err(std::io::Error::other(format!(
"DNS lookup returned no addresses for {target}"
)));
}
let addr = addrs[0];
self.cache.put(target.clone(), (Instant::now(), addr));
Ok(addr)
}
pub fn poll_resolve_location(
&mut self,
cx: &mut Context<'_>,
target: &NetLocation,
) -> Poll<std::io::Result<SocketAddr>> {
if self
.pending
.as_ref()
.is_some_and(|(pending_target, _)| pending_target != target)
{
self.pending = None;
}
if let Some(socket_addr) = target.to_socket_addr_nonblocking() {
return Poll::Ready(Ok(socket_addr));
}
if let Some((ts, addr)) = self.cache.get(target) {
if Instant::now().duration_since(*ts) <= Duration::from_secs(self.result_timeout_secs) {
return Poll::Ready(Ok(*addr));
}
self.cache.pop(target);
}
if self.pending.is_none() {
self.pending = Some((target.clone(), self.resolver.resolve_location(target)));
}
let result = self.pending.as_mut().unwrap().1.as_mut().poll(cx);
match result {
Poll::Pending => Poll::Pending,
Poll::Ready(Ok(addrs)) => {
self.pending = None;
if addrs.is_empty() {
return Poll::Ready(Err(std::io::Error::other(format!(
"DNS lookup returned no addresses for {target}"
))));
}
let addr = addrs[0];
self.cache.put(target.clone(), (Instant::now(), addr));
Poll::Ready(Ok(addr))
}
Poll::Ready(Err(e)) => {
self.pending = None;
Poll::Ready(Err(e))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::address::Address;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::sync::Barrier;
#[derive(Debug)]
struct MockResolver {
addrs: Vec<SocketAddr>,
call_count: AtomicUsize,
error_kind: Option<std::io::ErrorKind>,
}
impl MockResolver {
fn with_addrs(addrs: Vec<SocketAddr>) -> Self {
Self {
addrs,
call_count: AtomicUsize::new(0),
error_kind: None,
}
}
fn with_error(kind: std::io::ErrorKind) -> Self {
Self {
addrs: vec![],
call_count: AtomicUsize::new(0),
error_kind: Some(kind),
}
}
}
impl Resolver for MockResolver {
fn resolve_location(&self, _location: &NetLocation) -> ResolveFuture {
self.call_count.fetch_add(1, Ordering::Relaxed);
let addrs = self.addrs.clone();
let error_kind = self.error_kind;
Box::pin(async move {
if let Some(kind) = error_kind {
Err(std::io::Error::new(kind, "mock error"))
} else {
Ok(addrs)
}
})
}
}
#[derive(Debug, Default)]
struct PendingResolver {
active: Arc<AtomicUsize>,
calls: AtomicUsize,
}
struct PendingFuture {
active: Arc<AtomicUsize>,
}
impl Future for PendingFuture {
type Output = std::io::Result<Vec<SocketAddr>>;
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
Poll::Pending
}
}
impl Drop for PendingFuture {
fn drop(&mut self) {
self.active.fetch_sub(1, Ordering::Relaxed);
}
}
impl Resolver for PendingResolver {
fn resolve_location(&self, _location: &NetLocation) -> ResolveFuture {
self.calls.fetch_add(1, Ordering::Relaxed);
self.active.fetch_add(1, Ordering::Relaxed);
Box::pin(PendingFuture {
active: self.active.clone(),
})
}
}
fn test_location() -> NetLocation {
NetLocation::new(Address::Hostname("example.com".to_string()), 80)
}
fn test_addrs() -> Vec<SocketAddr> {
vec!["127.0.0.1:80".parse().unwrap()]
}
fn unique_location(index: usize) -> NetLocation {
NetLocation::new(Address::Hostname(format!("host-{index}.example")), 443)
}
#[test]
fn test_caching_native_resolver_cache_is_bounded() {
let resolver = CachingNativeResolver::new();
let mut cache = resolver.cache.lock();
for index in 0..CACHING_NATIVE_RESOLVER_CACHE_CAPACITY + 1 {
cache.put(
unique_location(index),
CachedResolveResult {
timestamp: Instant::now(),
addr: "127.0.0.1:443".parse().unwrap(),
},
);
}
assert_eq!(cache.len(), CACHING_NATIVE_RESOLVER_CACHE_CAPACITY);
assert!(cache.peek(&unique_location(0)).is_none());
}
#[tokio::test]
async fn test_resolver_cache_is_bounded() {
let resolver: Arc<dyn Resolver> = Arc::new(MockResolver::with_addrs(test_addrs()));
let mut cache = ResolverCache::new(resolver);
for index in 0..RESOLVER_CACHE_CAPACITY + 1 {
cache
.resolve_location(&unique_location(index))
.await
.unwrap();
}
assert_eq!(cache.cache.len(), RESOLVER_CACHE_CAPACITY);
assert!(cache.cache.peek(&unique_location(0)).is_none());
}
#[test]
fn test_resolver_cache_cancels_abandoned_pending_result() {
let resolver = Arc::new(PendingResolver::default());
let mut cache = ResolverCache::new(resolver.clone());
let mut cx = Context::from_waker(futures::task::noop_waker_ref());
let first = unique_location(0);
let second = unique_location(1);
assert!(matches!(
cache.poll_resolve_location(&mut cx, &first),
Poll::Pending
));
assert_eq!(resolver.active.load(Ordering::Relaxed), 1);
assert!(matches!(
cache.poll_resolve_location(&mut cx, &second),
Poll::Pending
));
assert_eq!(resolver.active.load(Ordering::Relaxed), 1);
assert_eq!(resolver.calls.load(Ordering::Relaxed), 2);
assert!(matches!(
cache.poll_resolve_location(&mut cx, &second),
Poll::Pending
));
assert_eq!(resolver.calls.load(Ordering::Relaxed), 2);
let literal = NetLocation::new(Address::Ipv4(std::net::Ipv4Addr::LOCALHOST), 443);
assert!(matches!(
cache.poll_resolve_location(&mut cx, &literal),
Poll::Ready(Ok(_))
));
assert_eq!(resolver.active.load(Ordering::Relaxed), 0);
}
#[tokio::test]
async fn test_refreshing_resolver_retries_after_timeout() {
let success_addrs = test_addrs();
let call_count = Arc::new(AtomicUsize::new(0));
let call_count_clone = call_count.clone();
let addrs = success_addrs.clone();
let factory: ResolverFactory = Arc::new(move || {
let n = call_count_clone.fetch_add(1, Ordering::Relaxed);
let addrs = addrs.clone();
Box::pin(async move {
if n == 0 {
Ok(
Arc::new(MockResolver::with_error(std::io::ErrorKind::TimedOut))
as Arc<dyn Resolver>,
)
} else {
Ok(Arc::new(MockResolver::with_addrs(addrs)) as Arc<dyn Resolver>)
}
})
});
let policy = RefreshPolicy {
max_idle: Duration::from_secs(60),
retry_once_after_refresh: true,
};
let resolver = RefreshingResolver::new(factory, policy, "test".to_string())
.await
.unwrap();
let result = resolver.resolve_location(&test_location()).await.unwrap();
assert_eq!(result, success_addrs);
assert_eq!(call_count.load(Ordering::Relaxed), 2);
}
#[tokio::test]
async fn test_refreshing_resolver_no_retry_on_non_refreshable_error() {
let call_count = Arc::new(AtomicUsize::new(0));
let call_count_clone = call_count.clone();
let factory: ResolverFactory = Arc::new(move || {
call_count_clone.fetch_add(1, Ordering::Relaxed);
Box::pin(async move {
Ok(
Arc::new(MockResolver::with_error(std::io::ErrorKind::Other))
as Arc<dyn Resolver>,
)
})
});
let policy = RefreshPolicy {
max_idle: Duration::from_secs(60),
retry_once_after_refresh: true,
};
let resolver = RefreshingResolver::new(factory, policy, "test".to_string())
.await
.unwrap();
let result = resolver.resolve_location(&test_location()).await;
assert!(result.is_err());
assert_eq!(call_count.load(Ordering::Relaxed), 1);
}
#[tokio::test]
async fn test_refreshing_resolver_idle_refresh() {
let call_count = Arc::new(AtomicUsize::new(0));
let addrs = test_addrs();
let call_count_clone = call_count.clone();
let addrs_clone = addrs.clone();
let factory: ResolverFactory = Arc::new(move || {
call_count_clone.fetch_add(1, Ordering::Relaxed);
let addrs = addrs_clone.clone();
Box::pin(
async move { Ok(Arc::new(MockResolver::with_addrs(addrs)) as Arc<dyn Resolver>) },
)
});
let policy = RefreshPolicy {
max_idle: Duration::from_millis(50),
retry_once_after_refresh: true,
};
let resolver = RefreshingResolver::new(factory, policy, "test".to_string())
.await
.unwrap();
let result = resolver.resolve_location(&test_location()).await.unwrap();
assert_eq!(result, addrs);
assert_eq!(call_count.load(Ordering::Relaxed), 1);
tokio::time::sleep(Duration::from_millis(100)).await;
let result = resolver.resolve_location(&test_location()).await.unwrap();
assert_eq!(result, addrs);
assert_eq!(call_count.load(Ordering::Relaxed), 2);
}
#[tokio::test]
async fn test_refreshing_resolver_coalesces_concurrent_idle_refresh() {
let call_count = Arc::new(AtomicUsize::new(0));
let addrs = test_addrs();
let call_count_clone = call_count.clone();
let addrs_clone = addrs.clone();
let factory: ResolverFactory = Arc::new(move || {
call_count_clone.fetch_add(1, Ordering::Relaxed);
let addrs = addrs_clone.clone();
Box::pin(async move {
tokio::time::sleep(Duration::from_millis(10)).await;
Ok(Arc::new(MockResolver::with_addrs(addrs)) as Arc<dyn Resolver>)
})
});
let policy = RefreshPolicy {
max_idle: Duration::from_millis(20),
retry_once_after_refresh: true,
};
let resolver = Arc::new(
RefreshingResolver::new(factory, policy, "test".to_string())
.await
.unwrap(),
);
let result = resolver.resolve_location(&test_location()).await.unwrap();
assert_eq!(result, addrs);
assert_eq!(call_count.load(Ordering::Relaxed), 1);
tokio::time::sleep(Duration::from_millis(50)).await;
let concurrency = 32;
let barrier = Arc::new(Barrier::new(concurrency));
let tasks = (0..concurrency).map(|_| {
let resolver = resolver.clone();
let barrier = barrier.clone();
let location = test_location();
tokio::spawn(async move {
barrier.wait().await;
resolver.resolve_location(&location).await
})
});
for result in futures::future::join_all(tasks).await {
assert_eq!(result.unwrap().unwrap(), addrs);
}
assert_eq!(call_count.load(Ordering::Relaxed), 2);
}
#[tokio::test]
async fn test_resolve_addresses_returns_all() {
let addrs: Vec<SocketAddr> = vec![
"1.1.1.1:80".parse().unwrap(),
"2.2.2.2:80".parse().unwrap(),
"3.3.3.3:80".parse().unwrap(),
];
let inner: Arc<dyn Resolver> = Arc::new(MockResolver::with_addrs(addrs.clone()));
let loc = test_location();
let result = resolve_addresses(&inner, &loc).await.unwrap();
assert_eq!(result, addrs);
}
#[tokio::test]
async fn test_resolve_addresses_ip_literal() {
let inner: Arc<dyn Resolver> = Arc::new(MockResolver::with_addrs(vec![]));
let loc = NetLocation::new(Address::Ipv4("1.2.3.4".parse().unwrap()), 443);
let result = resolve_addresses(&inner, &loc).await.unwrap();
assert_eq!(result, vec!["1.2.3.4:443".parse::<SocketAddr>().unwrap()]);
}
#[tokio::test]
async fn test_timeout_resolver_ip_bypass() {
let inner = MockResolver::with_addrs(test_addrs());
let resolver = TimeoutResolver::with_timeout(inner, Duration::from_millis(1));
let loc = NetLocation::new(Address::Ipv4("1.2.3.4".parse().unwrap()), 80);
let result = resolver.resolve_location(&loc).await.unwrap();
assert_eq!(result[0], "1.2.3.4:80".parse::<SocketAddr>().unwrap());
}
}