Skip to main content

hashiverse_lib/transport/bootstrap_provider/
dnssec_bootstrap_provider.rs

1//! # DNSSEC-validated bootstrap provider
2//!
3//! Resolves the domains in [`crate::tools::config::BOOTSTRAP_DOMAINS`] via a
4//! `hickory-resolver` configured to require DNSSEC authentication. Successfully-resolved
5//! addresses are returned as the seed peer list.
6//!
7//! DNSSEC matters here because bootstrapping is the one moment a client hasn't yet
8//! authenticated anyone on the network — a hijacked A record from an unauthenticated
9//! resolver could redirect every new node to an attacker-controlled sybil cluster.
10//! Requiring DNSSEC-validated lookups pushes that attack surface back to the root
11//! trust anchor.
12
13use crate::tools::{config, tools};
14use crate::transport::bootstrap_provider::bootstrap_provider::BootstrapProvider;
15use hickory_resolver::{
16    config::{LookupIpStrategy, ResolverConfig, CLOUDFLARE, GOOGLE},
17    net::runtime::TokioRuntimeProvider,
18    Resolver, TokioResolver,
19};
20use log::warn;
21use std::collections::HashSet;
22
23pub struct DnssecBootstrapProvider {
24    resolvers: Vec<TokioResolver>,
25}
26
27impl Default for DnssecBootstrapProvider {
28    fn default() -> Self {
29        Self::new()
30    }
31}
32
33impl DnssecBootstrapProvider {
34    pub fn new() -> Self {
35        let resolver_configs = [
36            ResolverConfig::https(&CLOUDFLARE),
37            ResolverConfig::https(&GOOGLE),
38            // ResolverConfig::https(&QUAD9),   // seems to be too busy - many errors / timeouts
39        ];
40
41        let resolvers = resolver_configs
42            .into_iter()
43            .filter_map(|resolver_config| {
44                let mut builder = Resolver::builder_with_config(resolver_config, TokioRuntimeProvider::default());
45                builder.options_mut().validate = true;
46                builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4Only;
47                match builder.build() {
48                    Ok(resolver) => Some(resolver),
49                    Err(e) => {
50                        warn!("DNSSEC bootstrap resolver construction failed: {}", e);
51                        None
52                    }
53                }
54            })
55            .collect();
56
57        Self { resolvers }
58    }
59}
60
61#[async_trait::async_trait]
62impl BootstrapProvider for DnssecBootstrapProvider {
63    async fn get_bootstrap_addresses(&self) -> Vec<String> {
64        let mut addresses = HashSet::new();
65        for domain in config::BOOTSTRAP_DOMAINS {
66            for resolver in &self.resolvers {
67                match resolver.lookup_ip(*domain).await {
68                    Ok(response) => {
69                        for ip in response.iter() {
70                            addresses.insert(format!("{}:443", ip));
71                        }
72                    }
73                    Err(e) => {
74                        warn!("DNSSEC bootstrap lookup failed for {}: {}", domain, e);
75                    }
76                }
77            }
78        }
79        let mut addresses: Vec<String> = addresses.drain().collect();
80        tools::shuffle(&mut addresses);
81        addresses
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    /// Hits the live Cloudflare/Google DoH resolvers, so it is opt-in:
90    /// `cargo nextest run --cargo-profile profiling -p hashiverse-lib dnssec_bootstrap_resolves_live_domains --run-ignored only`
91    #[tokio::test]
92    #[ignore]
93    async fn dnssec_bootstrap_resolves_live_domains() {
94        let dnssec_bootstrap_provider = DnssecBootstrapProvider::new();
95        assert_eq!(dnssec_bootstrap_provider.resolvers.len(), 2, "every configured resolver should build");
96        let bootstrap_addresses = dnssec_bootstrap_provider.get_bootstrap_addresses().await;
97        assert!(!bootstrap_addresses.is_empty(), "expected at least one DNSSEC-validated bootstrap address");
98        assert!(bootstrap_addresses.iter().all(|bootstrap_address| bootstrap_address.ends_with(":443")));
99    }
100}