引言:DNS 解析的永恒堡垒
在 Rust 生态中,DNS 解析不仅是网络应用的基石,更是面对复杂环境时的试金石。从基础的 Hickory-DNS 引入,到进阶的智能重试、缓存与故障转移,我们已构建了一个强大的dns_guardian
crate。但为了适应多样化需求,我们进一步完善:将 Redis 持久化缓存转化为可选特征(feature),通过 Cargo.toml 启用,仅在需要时引入相关依赖和代码。这避免了不必要的膨胀,确保 crate 轻量且灵活。同时,保留 Builder 模式支持环境变量加载 NameServers、自定义缓存配置,并集成 DoH 探索的潜力。想象你的应用在启动时,预防性检查多级域名,智能重试规避瞬时故障,持久缓存加速查询,优雅降级提供修复指引——这一切,都在dns_guardian
中实现。本指南呈现完整 crate 代码,助你打造 DNS 解析的永恒堡垒,无论生产还是开发,都如盾牌般坚不可摧。
第一部分:Crate 设计概述与完善点
- Redis 特征化:通过
[features]
定义redis
,依赖redis
、serde
、serde_json
。代码中使用#[cfg(feature = "redis")]
条件编译 Redis 逻辑,提供配置选项但不强制。 - Builder 模式:支持链式配置,包括 env 加载 NameServers、缓存容量/TTL、Redis URL(仅 feature 启用时)。
- 核心功能:预防检查、智能重试(backoff)、Moka 缓存、系统 fallback、自定义错误与建议。
- DoH 集成:作为可选配置,增强加密。
- 完整性:包括所有依赖、错误处理、日志(tracing)。
以下是完整Cargo.toml
和src/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 项目从此无惧域名挑战。
参考资料
- Hickory-DNS 官方文档:https://docs.rs/hickory-resolver/latest/hickory_resolver/ (解析器配置、DoH 支持详解)。
- Redis-Rs Crate:https://docs.rs/redis/latest/redis/ (异步连接与命令示例)。
- Moka 缓存文档:https://docs.rs/moka/latest/moka/ (未来异步 LRU 实现指南)。
- Backoff Crate:https://docs.rs/backoff/latest/backoff/ (指数退避重试算法)。
- Thiserror Crate:https://docs.rs/thiserror/latest/thiserror/ (自定义错误类型最佳实践)。
- Tracing Crate:https://docs.rs/tracing/latest/tracing/ (日志记录与监控)。
- Rust 特征与条件编译:Rust 官方书籍 - Conditional Compilation (https://doc.rust-lang.org/book/ch14-03-conditional-compilation.html)。
- DoH RFC:RFC 8484 - DNS Queries over HTTPS (https://datatracker.ietf.org/doc/html/rfc8484)。
- 社区案例:GitHub - Hickory-DNS issues (https://github.com/hickory-dns/hickory-dns/issues),多级域名处理讨论。
- 书籍推荐:《Programming Rust》by Jim Blandy (O’Reilly),深入异步与 crate 设计。
版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)