Add additional DNS servers and support DoH protocol additionally to regular DNS.

This commit is contained in:
2025-08-11 15:14:01 +02:00
parent e6666870b9
commit d6967fc521
6 changed files with 508 additions and 11 deletions
+3
View File
@@ -19,6 +19,9 @@ clap = { version = "4.4", features = ["derive"] }
tokio = { version = "1.0", features = ["full"] }
hickory-resolver = "0.24"
hickory-client = "0.24"
reqwest = { version = "0.12", features = ["json"] }
base64 = "0.22"
urlencoding = "2.1"
socket2 = "0.5"
pnet = "0.34"
anyhow = "1.0"
+14
View File
@@ -113,6 +113,20 @@ pub enum DnsCommands {
Comprehensive { domain: String },
#[command(about = "Test large DNS queries")]
Large { domain: String },
#[command(about = "Test DNS-over-HTTPS (DoH) providers")]
Doh {
domain: String,
#[arg(
short,
long,
help = "DoH provider name (google, google-json, cloudflare, cloudflare-family, cloudflare-security, cloudflare-json, cloudflare-family-json, cloudflare-security-json, quad9, quad9-unsecured, quad9-ecs, opendns, opendns-family, adguard, adguard-family, adguard-unfiltered)"
)]
provider: Option<String>,
#[arg(short = 'r', long, value_enum, default_value = "a")]
record_type: RecordTypeArg,
},
#[command(about = "List available DoH providers")]
DohProviders,
}
#[derive(Subcommand)]
+411
View File
@@ -0,0 +1,411 @@
use crate::utils::{measure_time, NetworkError, Result, TestResult};
use hickory_client::op::{Message, MessageType, OpCode, Query};
use hickory_client::rr::{Name, RecordType};
use reqwest::Client;
use serde_json::Value;
use std::str::FromStr;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct DohTest {
pub domain: String,
pub record_type: RecordType,
pub provider: DohProvider,
pub timeout: Duration,
}
#[derive(Debug, Clone)]
pub struct DohProvider {
pub name: &'static str,
pub url: &'static str,
pub description: &'static str,
pub format: DohFormat,
}
#[derive(Debug, Clone)]
pub enum DohFormat {
Json, // Google, Cloudflare style
WireFormat, // Quad9, AdGuard, OpenDNS style
}
// Comprehensive DoH providers list
pub const DOH_PROVIDERS: &[DohProvider] = &[
// Google DNS - Wire format (default/standard)
DohProvider {
name: "Google",
url: "https://dns.google/dns-query",
description: "Google Public DNS (8.8.8.8)",
format: DohFormat::WireFormat,
},
// Google DNS - JSON format (special variant)
DohProvider {
name: "Google-JSON",
url: "https://dns.google/resolve",
description: "Google Public DNS (8.8.8.8) - JSON variant",
format: DohFormat::Json,
},
// Cloudflare DNS - Wire format variants (default/standard)
DohProvider {
name: "Cloudflare",
url: "https://1.1.1.1/dns-query",
description: "Cloudflare DNS Primary (1.1.1.1)",
format: DohFormat::WireFormat,
},
DohProvider {
name: "Cloudflare-Family",
url: "https://1.1.1.2/dns-query",
description: "Cloudflare for Families (1.1.1.2) - Blocks malware/adult",
format: DohFormat::WireFormat,
},
DohProvider {
name: "Cloudflare-Security",
url: "https://1.1.1.3/dns-query",
description: "Cloudflare for Families (1.1.1.3) - Blocks malware only",
format: DohFormat::WireFormat,
},
// Cloudflare DNS - JSON format variants (special variants)
DohProvider {
name: "Cloudflare-JSON",
url: "https://1.1.1.1/dns-query",
description: "Cloudflare DNS Primary (1.1.1.1) - JSON variant",
format: DohFormat::Json,
},
DohProvider {
name: "Cloudflare-Family-JSON",
url: "https://1.1.1.2/dns-query",
description: "Cloudflare for Families (1.1.1.2) - Blocks malware/adult - JSON variant",
format: DohFormat::Json,
},
DohProvider {
name: "Cloudflare-Security-JSON",
url: "https://1.1.1.3/dns-query",
description: "Cloudflare for Families (1.1.1.3) - Blocks malware only - JSON variant",
format: DohFormat::Json,
},
// Quad9 DNS - All variants
DohProvider {
name: "Quad9",
url: "https://dns.quad9.net/dns-query",
description: "Quad9 Secure (9.9.9.9) - Blocks malicious domains",
format: DohFormat::WireFormat,
},
DohProvider {
name: "Quad9-Unsecured",
url: "https://dns10.quad9.net/dns-query",
description: "Quad9 Unsecured (9.9.9.10) - No domain blocking",
format: DohFormat::WireFormat,
},
DohProvider {
name: "Quad9-ECS",
url: "https://dns11.quad9.net/dns-query",
description: "Quad9 ECS (9.9.9.11) - Blocks malicious + EDNS Client Subnet",
format: DohFormat::WireFormat,
},
// OpenDNS
DohProvider {
name: "OpenDNS",
url: "https://doh.opendns.com/dns-query",
description: "OpenDNS Standard (208.67.222.222)",
format: DohFormat::WireFormat,
},
DohProvider {
name: "OpenDNS-Family",
url: "https://doh.familyshield.opendns.com/dns-query",
description: "OpenDNS FamilyShield (208.67.222.123) - Blocks adult content",
format: DohFormat::WireFormat,
},
// AdGuard DNS - All variants
DohProvider {
name: "AdGuard",
url: "https://dns.adguard.com/dns-query",
description: "AdGuard DNS (94.140.14.14) - Blocks ads and trackers",
format: DohFormat::WireFormat,
},
DohProvider {
name: "AdGuard-Family",
url: "https://dns-family.adguard.com/dns-query",
description: "AdGuard DNS Family (94.140.14.15) - Blocks ads, trackers, and adult content",
format: DohFormat::WireFormat,
},
DohProvider {
name: "AdGuard-Unfiltered",
url: "https://dns-unfiltered.adguard.com/dns-query",
description: "AdGuard DNS Unfiltered (94.140.14.140) - No filtering",
format: DohFormat::WireFormat,
},
];
impl DohTest {
pub fn new(domain: String, record_type: RecordType, provider: DohProvider) -> Self {
Self {
domain,
record_type,
provider,
timeout: Duration::from_secs(10),
}
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub async fn run(&self) -> TestResult {
let test_name = format!(
"DoH {:?} query for {} via {}",
self.record_type, self.domain, self.provider.name
);
let (duration, result) = measure_time(|| async { self.query_doh().await }).await;
match result {
Ok(response) => TestResult::new(test_name).success(duration, response),
Err(error) => TestResult::new(test_name).failure(duration, error),
}
}
async fn query_doh(&self) -> Result<String> {
let client = Client::builder()
.timeout(self.timeout)
.user_agent("NetTest/1.0")
.build()
.map_err(|e| NetworkError::Other(format!("Failed to create HTTP client: {}", e)))?;
match self.provider.format {
DohFormat::Json => self.query_doh_json(&client).await,
DohFormat::WireFormat => self.query_doh_wire_format(&client).await,
}
}
async fn query_doh_json(&self, client: &Client) -> Result<String> {
// Build URL with standard DoH parameters (Google/Cloudflare style)
let url = format!(
"{}?name={}&type={}",
self.provider.url,
self.domain,
self.record_type_to_number()
);
let response = client
.get(&url)
.header("Accept", "application/dns-json")
.send()
.await
.map_err(|e| NetworkError::Other(format!("DoH request failed: {}", e)))?;
if !response.status().is_success() {
return Err(NetworkError::Other(format!(
"DoH server returned status: {}",
response.status()
)));
}
let json_response: Value = response
.json()
.await
.map_err(|e| NetworkError::Other(format!("Failed to parse DoH response: {}", e)))?;
self.parse_doh_response(&json_response)
}
async fn query_doh_wire_format(&self, client: &Client) -> Result<String> {
// Create DNS query packet
let dns_packet = self.create_dns_query_packet()?;
let response = client
.post(self.provider.url)
.header("Content-Type", "application/dns-message")
.header("Accept", "application/dns-message")
.body(dns_packet)
.send()
.await
.map_err(|e| NetworkError::Other(format!("DoH request failed: {}", e)))?;
if !response.status().is_success() {
return Err(NetworkError::Other(format!(
"DoH server returned status: {}",
response.status()
)));
}
let response_bytes = response
.bytes()
.await
.map_err(|e| NetworkError::Other(format!("Failed to read DoH response: {}", e)))?;
self.parse_dns_response_packet(&response_bytes)
}
fn create_dns_query_packet(&self) -> Result<Vec<u8>> {
// Create a DNS query message
let name = Name::from_str(&self.domain)
.map_err(|e| NetworkError::Other(format!("Invalid domain name: {}", e)))?;
let query = Query::query(name, self.record_type);
let mut message = Message::new();
message
.set_message_type(MessageType::Query)
.set_op_code(OpCode::Query)
.set_recursion_desired(true)
.add_query(query);
// Serialize to bytes
message
.to_vec()
.map_err(|e| NetworkError::Other(format!("Failed to serialize DNS query: {}", e)))
}
fn parse_dns_response_packet(&self, response_bytes: &[u8]) -> Result<String> {
// Parse the DNS response packet
let message = Message::from_vec(response_bytes).map_err(|e| {
NetworkError::Other(format!("Failed to parse DNS response packet: {}", e))
})?;
// Check response code
if message.response_code() != hickory_client::op::ResponseCode::NoError {
return Err(NetworkError::DnsResolution(format!(
"DNS query failed with response code: {:?}",
message.response_code()
)));
}
let answers = message.answers();
if answers.is_empty() {
return Ok(format!("{:?} records: (none found)", self.record_type));
}
let mut records = Vec::new();
for answer in answers {
if answer.record_type() == self.record_type {
match answer.data() {
Some(rdata) => records.push(rdata.to_string()),
None => continue,
}
}
}
if records.is_empty() {
Ok(format!("{:?} records: (none found)", self.record_type))
} else {
Ok(format!(
"{:?} records: {}",
self.record_type,
records.join(", ")
))
}
}
fn record_type_to_number(&self) -> u16 {
match self.record_type {
RecordType::A => 1,
RecordType::NS => 2,
RecordType::CNAME => 5,
RecordType::SOA => 6,
RecordType::PTR => 12,
RecordType::MX => 15,
RecordType::TXT => 16,
RecordType::AAAA => 28,
_ => 1, // Default to A record
}
}
fn parse_doh_response(&self, response: &Value) -> Result<String> {
let status = response["Status"]
.as_u64()
.ok_or_else(|| NetworkError::Other("Invalid DoH response format".to_string()))?;
if status != 0 {
let status_text = match status {
1 => "Format Error",
2 => "Server Failure",
3 => "Name Error (NXDOMAIN)",
4 => "Not Implemented",
5 => "Refused",
_ => "Unknown Error",
};
return Err(NetworkError::DnsResolution(format!(
"DoH query failed with status {}: {}",
status, status_text
)));
}
let empty_vec = Vec::new();
let answers = response["Answer"].as_array().unwrap_or(&empty_vec);
if answers.is_empty() {
return Ok(format!("{:?} records: (none found)", self.record_type));
}
let mut records = Vec::new();
for answer in answers {
if let Some(data) = answer["data"].as_str() {
records.push(data.to_string());
}
}
if records.is_empty() {
Ok(format!("{:?} records: (none found)", self.record_type))
} else {
Ok(format!(
"{:?} records: {}",
self.record_type,
records.join(", ")
))
}
}
}
pub async fn test_doh_providers(domain: &str, record_type: RecordType) -> Vec<TestResult> {
let mut results = Vec::new();
for provider in DOH_PROVIDERS {
let test = DohTest::new(domain.to_string(), record_type, provider.clone());
results.push(test.run().await);
}
results
}
pub async fn test_doh_comprehensive(domain: &str) -> Vec<TestResult> {
let mut results = Vec::new();
// Test A records with JSON-compatible providers (Google and Cloudflare variants)
let json_compatible_providers = [
&DOH_PROVIDERS[0], // Google
&DOH_PROVIDERS[1], // Cloudflare Primary
&DOH_PROVIDERS[2], // Cloudflare-Family
&DOH_PROVIDERS[3], // Cloudflare-Security
];
for provider in json_compatible_providers {
let test = DohTest::new(domain.to_string(), RecordType::A, provider.clone());
results.push(test.run().await);
}
// Test AAAA records with the same JSON-compatible providers
for provider in json_compatible_providers {
let test = DohTest::new(domain.to_string(), RecordType::AAAA, provider.clone());
results.push(test.run().await);
}
results
}
pub fn get_provider_by_name(name: &str) -> Option<&'static DohProvider> {
DOH_PROVIDERS
.iter()
.find(|provider| provider.name.to_lowercase() == name.to_lowercase())
}
pub fn list_doh_providers() -> Vec<TestResult> {
let mut results = Vec::new();
for provider in DOH_PROVIDERS {
let details = format!("{} - {}", provider.url, provider.description);
let result = TestResult::new(format!("DoH Provider: {}", provider.name))
.success(Duration::from_millis(0), details);
results.push(result);
}
results
}
+30 -7
View File
@@ -10,9 +10,11 @@ use tokio::net::TcpStream;
use tokio::time::timeout;
pub mod categories;
pub mod doh;
pub mod queries;
pub use categories::*;
pub use doh::*;
pub use queries::*;
#[derive(Debug, Clone)]
@@ -614,18 +616,35 @@ pub async fn test_common_dns_servers(domain: &str, record_type: RecordType) -> V
);
results.push(system_result);
// Then test external DNS servers
// Test all traditional DNS servers (UDP/TCP)
let servers = [
// Google DNS
"8.8.8.8:53", // Google Primary
"8.8.4.4:53", // Google Secondary
"1.1.1.1:53", // Cloudflare Primary
"1.0.0.1:53", // Cloudflare Secondary
"9.9.9.9:53", // Quad9 Primary
"149.112.112.112:53", // Quad9 Secondary
// Cloudflare DNS - All variants
"1.1.1.1:53", // Cloudflare Primary (standard)
"1.0.0.1:53", // Cloudflare Secondary (standard)
"1.1.1.2:53", // Cloudflare Family (blocks malware/adult)
"1.1.1.3:53", // Cloudflare Family (blocks malware only)
// Quad9 DNS - All variants
"9.9.9.9:53", // Quad9 Primary (blocks malicious domains)
"149.112.112.112:53", // Quad9 Secondary (blocks malicious domains)
"9.9.9.10:53", // Quad9 Unsecured (no blocking)
"149.112.112.10:53", // Quad9 Unsecured Secondary (no blocking)
"9.9.9.11:53", // Quad9 Secured + ECS (blocks malicious + EDNS)
"149.112.112.11:53", // Quad9 Secured + ECS Secondary
// OpenDNS
"208.67.222.222:53", // OpenDNS Primary
"208.67.220.220:53", // OpenDNS Secondary
"1.1.1.2:53", // Cloudflare Family (blocks malware/adult)
"1.1.1.3:53", // Cloudflare Family (blocks malware)
"208.67.222.123:53", // OpenDNS FamilyShield Primary
"208.67.220.123:53", // OpenDNS FamilyShield Secondary
// AdGuard DNS - All variants
"94.140.14.14:53", // AdGuard DNS Primary (blocks ads/trackers)
"94.140.15.15:53", // AdGuard DNS Secondary (blocks ads/trackers)
"94.140.14.15:53", // AdGuard DNS Family Primary (blocks ads/trackers/adult)
"94.140.15.16:53", // AdGuard DNS Family Secondary (blocks ads/trackers/adult)
"94.140.14.140:53", // AdGuard DNS Unfiltered Primary
"94.140.14.141:53", // AdGuard DNS Unfiltered Secondary
];
for server_str in &servers {
@@ -635,6 +654,10 @@ pub async fn test_common_dns_servers(domain: &str, record_type: RecordType) -> V
}
}
// Add DNS-over-HTTPS tests (all available DoH providers)
let doh_results = crate::dns::doh::test_doh_providers(domain, record_type).await;
results.extend(doh_results);
results
}
+14
View File
@@ -15,11 +15,25 @@ pub async fn comprehensive_dns_test(domain: &str) -> Vec<TestResult> {
RecordType::SOA,
];
// Test all record types using system DNS resolver
// This focuses on comprehensive record type analysis rather than server testing
for record_type in &record_types {
let test = DnsTest::new(domain.to_string(), *record_type);
results.push(test.run().await);
}
// Add a few key DoH tests for comparison (JSON-compatible providers only)
let key_doh_providers = [
&crate::dns::doh::DOH_PROVIDERS[0], // Google
&crate::dns::doh::DOH_PROVIDERS[1], // Cloudflare Primary
];
for provider in key_doh_providers {
let test =
crate::dns::doh::DohTest::new(domain.to_string(), RecordType::A, provider.clone());
results.push(test.run().await);
}
results
}
+32
View File
@@ -184,6 +184,38 @@ async fn handle_dns_command(command: cli::DnsCommands, timeout: Duration) -> Vec
dns::queries::comprehensive_dns_test(&domain).await
}
cli::DnsCommands::Large { domain } => dns::queries::test_large_dns_queries(&domain).await,
cli::DnsCommands::Doh {
domain,
provider,
record_type,
} => {
let mut results = Vec::new();
for rt in record_type.to_record_type() {
match &provider {
Some(provider_name) => {
if let Some(provider) = dns::doh::get_provider_by_name(provider_name) {
let test = dns::doh::DohTest::new(domain.clone(), rt, provider.clone());
results.push(test.run().await);
} else {
results.push(TestResult::new("DoH Provider Error".to_string()).failure(
Duration::from_millis(0),
NetworkError::Other(format!(
"Unknown DoH provider: {}. Available providers: google, google-json, cloudflare, cloudflare-family, cloudflare-security, cloudflare-json, cloudflare-family-json, cloudflare-security-json, quad9, quad9-unsecured, quad9-ecs, opendns, opendns-family, adguard, adguard-family, adguard-unfiltered",
provider_name
)),
));
break; // Don't repeat error for each record type
}
}
None => {
let provider_results = dns::doh::test_doh_providers(&domain, rt).await;
results.extend(provider_results);
}
}
}
results
}
cli::DnsCommands::DohProviders => dns::doh::list_doh_providers(),
}
}