use std::error::Error as StdError;
use std::fmt;
use std::future::Future;
use std::net::IpAddr;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use ipnet::IpNet;
use parking_lot::RwLock;
use rand::rngs::StdRng;
use crate::gen_ip::probe_targets;
pub type DiscoveryError = Box<dyn StdError + Send + Sync>;
pub type DiscoverFuture<'a> =
Pin<Box<dyn Future<Output = Result<Vec<IpAddr>, DiscoveryError>> + Send + 'a>>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DiscoveryKind {
Authoritative,
Speculative,
}
pub trait Discovery: Send + Sync + 'static {
fn discover(&self) -> DiscoverFuture<'_>;
fn kind(&self) -> DiscoveryKind;
}
#[derive(Debug)]
pub struct RandomProbe {
nets: Arc<RwLock<Vec<IpNet>>>,
rng: Arc<RwLock<StdRng>>,
}
impl RandomProbe {
pub fn new(nets: Arc<RwLock<Vec<IpNet>>>, rng: Arc<RwLock<StdRng>>) -> Self {
RandomProbe { nets, rng }
}
}
impl Discovery for RandomProbe {
fn discover(&self) -> DiscoverFuture<'_> {
let nets = self.nets.read().clone();
let targets = probe_targets(&mut *self.rng.write(), &nets);
Box::pin(async move { Ok(targets) })
}
fn kind(&self) -> DiscoveryKind {
DiscoveryKind::Speculative
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum DnsDiscoveryError {
Resolve(std::io::Error),
Timeout(tokio::time::error::Elapsed),
}
impl fmt::Display for DnsDiscoveryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DnsDiscoveryError::Resolve(e) => write!(f, "DNS resolution failed: {e}"),
DnsDiscoveryError::Timeout(_) => write!(f, "DNS resolution timed out"),
}
}
}
impl StdError for DnsDiscoveryError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self {
DnsDiscoveryError::Resolve(e) => Some(e),
DnsDiscoveryError::Timeout(e) => Some(e),
}
}
}
pub const DEFAULT_DNS_DISCOVERY_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Debug)]
pub struct DnsDiscovery {
name: String,
port: u16,
timeout: Duration,
}
impl DnsDiscovery {
pub fn new(name: impl Into<String>, port: u16) -> Self {
DnsDiscovery {
name: name.into(),
port,
timeout: DEFAULT_DNS_DISCOVERY_TIMEOUT,
}
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
}
impl Discovery for DnsDiscovery {
fn discover(&self) -> DiscoverFuture<'_> {
let host = format!("{}:{}", self.name, self.port);
Box::pin(async move {
let addrs = tokio::time::timeout(self.timeout, tokio::net::lookup_host(host))
.await
.map_err(DnsDiscoveryError::Timeout)?
.map_err(DnsDiscoveryError::Resolve)?;
Ok(addrs.map(|sock_addr| sock_addr.ip()).collect())
})
}
fn kind(&self) -> DiscoveryKind {
DiscoveryKind::Authoritative
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn dns_discovery_resolves_loopback() {
let discovery = DnsDiscovery::new("localhost", 0);
let addrs = discovery
.discover()
.await
.expect("localhost should resolve");
assert!(
addrs.iter().any(|ip| ip.is_loopback()),
"expected a loopback address, got {addrs:?}"
);
}
#[tokio::test]
async fn dns_discovery_errors_on_unresolvable_name() {
let discovery = DnsDiscovery::new("this-name-should-not-resolve.invalid", 0);
assert!(discovery.discover().await.is_err());
}
#[test]
fn dns_discovery_with_timeout_is_a_builder() {
let discovery = DnsDiscovery::new("svc", 0).with_timeout(Duration::from_millis(1));
assert_eq!(discovery.timeout, Duration::from_millis(1));
}
#[tokio::test]
async fn dns_discovery_error_display_names_the_failure_kind() {
let resolve = DnsDiscoveryError::Resolve(std::io::Error::other("boom"));
assert_eq!(resolve.to_string(), "DNS resolution failed: boom");
let elapsed = tokio::time::timeout(Duration::from_nanos(1), std::future::pending::<()>())
.await
.unwrap_err();
let timeout_err = DnsDiscoveryError::Timeout(elapsed);
assert_eq!(timeout_err.to_string(), "DNS resolution timed out");
}
#[tokio::test]
async fn dns_discovery_error_source_chains_to_the_wrapped_error() {
let resolve = DnsDiscoveryError::Resolve(std::io::Error::other("boom"));
assert!(resolve.source().is_some());
let elapsed = tokio::time::timeout(Duration::from_nanos(1), std::future::pending::<()>())
.await
.unwrap_err();
let timeout_err = DnsDiscoveryError::Timeout(elapsed);
assert!(timeout_err.source().is_some());
}
#[test]
fn dns_discovery_is_authoritative() {
assert_eq!(
DnsDiscovery::new("svc", 0).kind(),
DiscoveryKind::Authoritative
);
}
#[tokio::test]
async fn random_probe_is_speculative_and_in_network() {
use rand::SeedableRng;
let net: IpNet = "127.0.0.0/8".parse().unwrap();
let probe = RandomProbe::new(
Arc::new(RwLock::new(vec![net])),
Arc::new(RwLock::new(StdRng::seed_from_u64(42))),
);
assert_eq!(probe.kind(), DiscoveryKind::Speculative);
let addrs = probe.discover().await.unwrap();
assert_eq!(addrs.len(), 1);
assert!(net.contains(&addrs[0]), "{net} should contain {}", addrs[0]);
}
}