Rust DNS 守护者终极版:铸就永不陨落的解析之盾

Rust DNS 守护者终极版:铸就永不陨落的解析之盾

Photos provided by Unsplash OR Pexels

引言:DNS 解析的永恒堡垒

在 Rust 生态中,DNS 解析不仅是网络应用的基石,更是面对复杂环境时的试金石。从基础的 Hickory-DNS 引入,到进阶的智能重试、缓存与故障转移,我们已构建了一个强大的dns_guardian crate。但为了适应多样化需求,我们进一步完善:将 Redis 持久化缓存转化为可选特征(feature),通过 Cargo.toml 启用,仅在需要时引入相关依赖和代码。这避免了不必要的膨胀,确保 crate 轻量且灵活。同时,保留 Builder 模式支持环境变量加载 NameServers、自定义缓存配置,并集成 DoH 探索的潜力。想象你的应用在启动时,预防性检查多级域名,智能重试规避瞬时故障,持久缓存加速查询,优雅降级提供修复指引——这一切,都在dns_guardian中实现。本指南呈现完整 crate 代码,助你打造 DNS 解析的永恒堡垒,无论生产还是开发,都如盾牌般坚不可摧。

第一部分:Crate 设计概述与完善点

  • Redis 特征化:通过[features]定义redis,依赖redisserdeserde_json。代码中使用#[cfg(feature = "redis")]条件编译 Redis 逻辑,提供配置选项但不强制。
  • Builder 模式:支持链式配置,包括 env 加载 NameServers、缓存容量/TTL、Redis URL(仅 feature 启用时)。
  • 核心功能:预防检查、智能重试(backoff)、Moka 缓存、系统 fallback、自定义错误与建议。
  • DoH 集成:作为可选配置,增强加密。
  • 完整性:包括所有依赖、错误处理、日志(tracing)。

以下是完整Cargo.tomlsrc/lib.rs代码。可直接复制创建 crate。

第二部分:完整 Cargo.toml

[package]
name = "dns_guardian"
version = "0.1.0"
edition = "2021"
description = "A robust DNS resolution guardian for Rust applications using Hickory-DNS."
authors = ["Your Name <your@email.com>"]
license = "MIT"
repository = "https://github.com/yourusername/dns_guardian"
keywords = ["dns", "resolution", "hickory", "rust", "async"]
categories = ["network-programming"]

[dependencies]
hickory-resolver = { version = "0.24.1", features = ["dns-over-https-rustls"] }
tokio = { version = "1", features = ["full"] }
moka = { version = "0.12", features = ["future"] }
backoff = "0.4"
thiserror = "1"
tracing = "0.1"

[features]
default = []
redis = ["dep:redis", "dep:serde", "dep:serde_json"]

[dependencies.redis]
version = "0.25"
features = ["tokio-comp", "connection-manager"]
optional = true

[dependencies.serde]
version = "1"
features = ["derive"]
optional = true

[dependencies.serde_json]
version = "1"
optional = true

[dev-dependencies]
mockall = "0.13"

第三部分:完整 src/lib.rs 代码

use std::collections::HashMap;
use std::net::{IpAddr, SocketAddr};
use std::sync::Arc;
use std::time::Duration;

use backoff::future::retry;
use backoff::ExponentialBackoff;
use hickory_resolver::config::{NameServerConfig, Protocol, ResolverConfig, ResolverOpts};
use hickory_resolver::error::ResolveError;
use hickory_resolver::lookup_ip::LookupIp;
use hickory_resolver::TokioAsyncResolver;
use moka::future::Cache;
use thiserror::Error;
use tracing::{error, info};

#[cfg(feature = "redis")]
use redis::{AsyncCommands, Client as RedisClient};
#[cfg(feature = "redis")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "redis")]
use serde_json;

/// 自定义错误类型,提供有意义的建议
#[derive(Error, Debug)]
pub enum DnsError {
    #[error("解析失败:{0}. 建议:检查网络连接或域名拼写。")]
    Resolve(ResolveError),
    #[error("重试耗尽:{0}. 建议:尝试备用 DNS 服务器或硬编码 IP。")]
    RetryExhausted(String),
    #[error("系统 fallback 失败:{0}. 建议:更新系统 resolv.conf。")]
    SystemFallback(std::io::Error),
    #[error("缓存失效:{0}. 建议:增加缓存容量。")]
    CacheError(String),
    #[cfg(feature = "redis")]
    #[error("Redis 操作失败:{0}. 建议:检查 Redis 连接 URL 或服务器状态。")]
    RedisError(redis::RedisError),
}

/// 持久化 IP 列表(仅 Redis 特征)
#[cfg(feature = "redis")]
#[derive(Serialize, Deserialize, Clone)]
struct CachedIps(Vec<IpAddr>);

/// DNS 守护者构建器:链式配置
pub struct DnsGuardianBuilder {
    domains: Vec<String>,
    config: ResolverConfig,
    opts: ResolverOpts,
    cache_capacity: u64,
    cache_ttl: Duration,
    #[cfg(feature = "redis")]
    redis_url: Option<String>,
}

impl DnsGuardianBuilder {
    /// 创建构建器
    pub fn new(domains: Vec<String>) -> Self {
        Self {
            domains,
            config: ResolverConfig::new(),
            opts: ResolverOpts::default(),
            cache_capacity: 100,
            cache_ttl: Duration::from_secs(300),
            #[cfg(feature = "redis")]
            redis_url: None,
        }
    }

    /// 从环境变量加载 NameServers(e.g., DNS_NAMESERVERS="8.8.8.8:53,1.1.1.1:53")
    pub fn with_env_nameservers(mut self) -> Self {
        if let Ok(env_servers) = std::env::var("DNS_NAMESERVERS") {
            for server_str in env_servers.split(',') {
                if let Ok(addr) = server_str.parse::<SocketAddr>() {
                    self.config.add_name_server(NameServerConfig::new(addr, Protocol::Udp));
                }
            }
        }
        self
    }

    /// 设置缓存容量
    pub fn cache_capacity(mut self, capacity: u64) -> Self {
        self.cache_capacity = capacity;
        self
    }

    /// 设置缓存 TTL
    pub fn cache_ttl(mut self, ttl: Duration) -> Self {
        self.cache_ttl = ttl;
        self
    }

    /// 添加 DoH 提供商(e.g., "google", "cloudflare")
    pub fn with_doh_provider(mut self, provider: &str) -> Self {
        match provider {
            "google" => self.config = ResolverConfig::google_https(),
            "cloudflare" => self.config = ResolverConfig::cloudflare_https(),
            "quad9" => self.config = ResolverConfig::quad9_https(),
            _ => {}
        }
        self
    }

    /// 添加自定义 DoH 服务器
    pub fn add_custom_doh(mut self, addr: SocketAddr, dns_name: &str) -> Self {
        let ns_config = NameServerConfig {
            socket_addr: addr,
            protocol: Protocol::Https,
            tls_dns_name: Some(dns_name.parse().expect("无效 DNS 名称")),
            trust_negative_responses: true,
            bind_addr: None,
        };
        self.config.add_name_server(ns_config);
        self
    }

    #[cfg(feature = "redis")]
    /// 设置 Redis URL(仅 Redis 特征启用)
    pub fn redis_url(mut self, url: String) -> Self {
        self.redis_url = Some(url);
        self
    }

    /// 构建 DnsGuardian
    pub fn build(self) -> Result<DnsGuardian, Box<dyn std::error::Error>> {
        let resolver = Arc::new(TokioAsyncResolver::tokio(self.config, self.opts));
        let cache = Cache::builder()
            .max_capacity(self.cache_capacity)
            .time_to_live(self.cache_ttl)
            .build();

        #[cfg(feature = "redis")]
        let redis = if let Some(url) = self.redis_url {
            Some(Arc::new(RedisClient::open(url)?))
        } else {
            None
        };

        #[cfg(not(feature = "redis"))]
        let redis = ();

        Ok(DnsGuardian {
            domains: self.domains,
            resolver,
            cache,
            redis,
        })
    }
}

/// DNS 守护者:处理域名列表的解析
pub struct DnsGuardian {
    domains: Vec<String>,
    resolver: Arc<TokioAsyncResolver>,
    cache: Cache<String, Vec<IpAddr>>,
    #[cfg(feature = "redis")]
    redis: Option<Arc<RedisClient>>,
    #[cfg(not(feature = "redis"))]
    redis: (),
}

impl DnsGuardian {
    /// 启动时初始化:预防检查、智能重试、缓存、降级、转移
    pub async fn init(&self) -> Result<HashMap<String, Vec<SocketAddr>>, DnsError> {
        info!("启动 DNS 预防性检查...");

        let mut results = HashMap::new();
        let mut futures = Vec::new();

        for domain in &self.domains {
            let domain_clone = domain.clone();
            let resolver_clone = self.resolver.clone();
            let cache_clone = self.cache.clone();
            #[cfg(feature = "redis")]
            let redis_clone = self.redis.clone();
            #[cfg(not(feature = "redis"))]
            let redis_clone = ();

            futures.push(tokio::spawn(async move {
                Self::resolve_with_retry_and_cache(&domain_clone, resolver_clone, cache_clone, redis_clone).await
            }));
        }

        for future in futures {
            let (domain, ips) = future.await.unwrap()?;
            results.insert(domain, ips.iter().map(|ip| SocketAddr::new(*ip, 80)).collect());
        }

        info!("DNS 检查完成,所有域名解析成功。");
        Ok(results)
    }

    /// 核心解析逻辑:重试 + 缓存 + 转移
    async fn resolve_with_retry_and_cache(
        domain: &str,
        resolver: Arc<TokioAsyncResolver>,
        cache: Cache<String, Vec<IpAddr>>,
        #[cfg(feature = "redis")]
        redis: Option<Arc<RedisClient>>,
        #[cfg(not(feature = "redis"))]
        _redis: (),
    ) -> Result<(String, Vec<IpAddr>), DnsError> {
        // 先查 Moka 缓存
        if let Some(ips) = cache.get(domain).await {
            info!("Moka 缓存命中:{}", domain);
            return Ok((domain.to_string(), ips));
        }

        #[cfg(feature = "redis")]
        // 若启用 Redis,查 Redis
        let mut ips_from_redis = Vec::new();
        #[cfg(feature = "redis")]
        if let Some(redis_client) = redis {
            let mut conn = redis_client.get_multiplexed_async_connection().await.map_err(DnsError::RedisError)?;
            if let Some(json): Option<String> = conn.get(format!("dns:{}", domain)).await.map_err(DnsError::RedisError)? {
                if let Ok(cached) = serde_json::from_str::<CachedIps>(&json) {
                    ips_from_redis = cached.0;
                    info!("Redis 缓存命中:{}", domain);
                }
            }
        }

        #[cfg(feature = "redis")]
        if !ips_from_redis.is_empty() {
            cache.insert(domain.to_string(), ips_from_redis.clone()).await;
            return Ok((domain.to_string(), ips_from_redis));
        }

        // 指数退避重试
        let backoff = ExponentialBackoff {
            initial_interval: Duration::from_secs(1),
            max_interval: Duration::from_secs(10),
            multiplier: 2.0,
            max_elapsed_time: Some(Duration::from_secs(30)),
            ..Default::default()
        };

        let result = retry(backoff, || async {
            match resolver.lookup_ip(domain).await {
                Ok(lookup) => Ok(lookup.iter().collect::<Vec<_>>()),
                Err(e) => {
                    error!("解析 {} 失败:{}", domain, e);
                    // 故障转移:尝试系统 resolver
                    match Self::system_fallback(domain).await {
                        Ok(ips) => Ok(ips),
                        Err(fallback_e) => Err(backoff::Error::Permanent(DnsError::Resolve(e))),
                    }
                }
            }
        })
        .await
        .map_err(|e| DnsError::RetryExhausted(e.to_string()))?;

        // 存入 Moka 缓存
        cache.insert(domain.to_string(), result.clone()).await;

        #[cfg(feature = "redis")]
        // 若启用 Redis,存入(TTL 与缓存一致)
        if let Some(redis_client) = redis {
            let mut conn = redis_client.get_multiplexed_async_connection().await.map_err(DnsError::RedisError)?;
            let json = serde_json::to_string(&CachedIps(result.clone())).map_err(|e| DnsError::CacheError(e.to_string()))?;
            conn.set_ex(format!("dns:{}", domain), json, self.cache_ttl.as_secs() as usize).await.map_err(DnsError::RedisError)?;
        }

        Ok((domain.to_string(), result))
    }

    /// 故障转移:fallback 到系统标准库
    async fn system_fallback(domain: &str) -> Result<Vec<IpAddr>, DnsError> {
        info!("转移到系统解析:{}", domain);
        let addrs = tokio::net::lookup_host((domain, 80))
            .await
            .map_err(DnsError::SystemFallback)?
            .map(|addr| addr.ip())
            .collect::<Vec<_>>();
        if addrs.is_empty() {
            Err(DnsError::SystemFallback(std::io::Error::new(
                std::io::ErrorKind::NotFound,
                "无 IP 返回",
            )))
        } else {
            Ok(addrs)
        }
    }
}

第四部分:使用示例与注意事项

在你的应用中:

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing::subscriber::set_global_default(tracing::fmt::Subscriber::new())?;

    let domains = vec!["example.com".to_string(), "sub.sub.example.com".to_string()];
    let builder = DnsGuardianBuilder::new(domains)
        .with_env_nameservers()
        .with_doh_provider("cloudflare")
        .cache_capacity(200)
        #[cfg(feature = "redis")]
        .redis_url("redis://localhost:6379".to_string())
        .build()?;
    let guardian = builder;

    match guardian.init().await {
        Ok(results) => println!("解析结果:{:?}", results),
        Err(e) => eprintln!("失败:{}. 降级启动。", e),
    }
    Ok(())
}

注意:启用 Redis 需在 Cargo.toml 添加dns_guardian = { path = "...", features = ["redis"] }。测试时用cargo test --features redis

第五部分:总结

此终极版dns_guardian 以特征化 Redis 实现模块化,结合 Builder 灵活配置,确保高效、可靠的 DNS 解析。你的 Rust 项目从此无惧域名挑战。

参考资料

  1. Hickory-DNS 官方文档https://docs.rs/hickory-resolver/latest/hickory_resolver/ (解析器配置、DoH 支持详解)。
  2. Redis-Rs Cratehttps://docs.rs/redis/latest/redis/ (异步连接与命令示例)。
  3. Moka 缓存文档https://docs.rs/moka/latest/moka/ (未来异步 LRU 实现指南)。
  4. Backoff Cratehttps://docs.rs/backoff/latest/backoff/ (指数退避重试算法)。
  5. Thiserror Cratehttps://docs.rs/thiserror/latest/thiserror/ (自定义错误类型最佳实践)。
  6. Tracing Cratehttps://docs.rs/tracing/latest/tracing/ (日志记录与监控)。
  7. Rust 特征与条件编译:Rust 官方书籍 - Conditional Compilation (https://doc.rust-lang.org/book/ch14-03-conditional-compilation.html)。
  8. DoH RFC:RFC 8484 - DNS Queries over HTTPS (https://datatracker.ietf.org/doc/html/rfc8484)。
  9. 社区案例:GitHub - Hickory-DNS issues (https://github.com/hickory-dns/hickory-dns/issues),多级域名处理讨论。
  10. 书籍推荐:《Programming Rust》by Jim Blandy (O’Reilly),深入异步与 crate 设计。

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)