roughtime 0.1.0

A no_std-capable Roughtime secure time-sync client with pluggable crypto backends
Documentation
//! Secure Roughtime-server hostname resolution via [`dnscrypt`](https://docs.rs/dnscrypt).
//!
//! By default, this crate no longer requires a caller to resolve (and thereby trust the plain,
//! unauthenticated OS resolver for) a Roughtime server's hostname before querying it: this
//! module resolves [`RoughtimeServer::host`] over `DNSCrypt`, an authenticated and encrypted DNS
//! transport, closing the one remaining unauthenticated hop before the Roughtime protocol's own
//! Ed25519-verified response takes over.
//!
//! A hostname that fails to resolve (unreachable resolver, no valid certificate, no A record)
//! is silently skipped, consistent with how [`crate::client`] already tolerates individual
//! per-server query failures — callers end up with however many `(server, ip)` pairs resolved,
//! which feed straight into [`crate::client::TokioClient::query`] or
//! [`crate::client::BlockingClient::query`].

extern crate std;

use alloc::vec::Vec;
use std::net::IpAddr;

use dnscrypt::HARDCODED_RESOLVERS;

use crate::servers::RoughtimeServer;

/// Resolves `servers`' hostnames to IP addresses over `DNSCrypt`.
///
/// This is a blocking call (`dnscrypt::resolve` performs synchronous I/O); see
/// [`resolve_servers_async`] for an async equivalent.
#[must_use]
pub fn resolve_servers(servers: &'static [RoughtimeServer]) -> Vec<(RoughtimeServer, IpAddr)> {
    let mut resolved = Vec::with_capacity(servers.len());
    for &server in servers {
        if let Ok(ips) = dnscrypt::resolve(HARDCODED_RESOLVERS, server.host)
            && let Some(&ip) = ips.first()
        {
            resolved.push((server, ip));
        }
    }
    resolved
}

/// Async equivalent of [`resolve_servers`], resolving every hostname concurrently over
/// `DNSCrypt` via `tokio`.
#[cfg(feature = "tokio-client")]
pub async fn resolve_servers_async(
    servers: &'static [RoughtimeServer],
) -> Vec<(RoughtimeServer, IpAddr)> {
    let mut handles = Vec::with_capacity(servers.len());
    for &server in servers {
        handles.push(tokio::spawn(async move {
            dnscrypt::resolve_async(HARDCODED_RESOLVERS, server.host)
                .await
                .ok()
                .and_then(|ips| ips.first().copied())
                .map(|ip| (server, ip))
        }));
    }

    let mut resolved = Vec::with_capacity(handles.len());
    for handle in handles {
        if let Ok(Some(entry)) = handle.await {
            resolved.push(entry);
        }
    }
    resolved
}

#[cfg(test)]
mod tests {
    // Real DNSCrypt resolution needs live network access, so these only cover the parts of the
    // control flow that don't: an empty input never touches the network.
    use super::resolve_servers;

    #[test]
    fn empty_input_resolves_to_nothing() {
        assert!(resolve_servers(&[]).is_empty());
    }

    #[cfg(feature = "tokio-client")]
    #[tokio::test]
    async fn empty_input_resolves_to_nothing_async() {
        assert!(super::resolve_servers_async(&[]).await.is_empty());
    }
}