#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::uninlined_format_args
)]
use dnscrypt::{
HARDCODED_RESOLVERS, HardcodedResolver, establish_dnscrypt_session, resolve,
resolve_domain_via_dnscrypt_session,
};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
static CUSTOM_RESOLVERS: &[HardcodedResolver] = &[
HardcodedResolver {
ip: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(9, 9, 9, 9)), 8443),
provider_name: "2.dnscrypt-cert.quad9.net",
provider_pk: [
0x67, 0xc8, 0x47, 0xb8, 0xc8, 0x75, 0x8c, 0xd1, 0x20, 0x24, 0x55, 0x43, 0xbe, 0x75,
0x67, 0x46, 0xdf, 0x34, 0xdf, 0x1d, 0x84, 0xc0, 0x0b, 0x8c, 0x47, 0x03, 0x68, 0xdf,
0x82, 0x1d, 0x86, 0x3e,
],
},
HardcodedResolver {
ip: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(102, 214, 10, 82)), 8443),
provider_name: "2.dnscrypt-cert.jb1.cipherdns.co.za",
provider_pk: [
0x29, 0xfd, 0x92, 0xbc, 0x01, 0xbe, 0xfb, 0xc8, 0x85, 0xc5, 0x23, 0xb0, 0x08, 0x79,
0x2a, 0xe0, 0x8d, 0x98, 0xd4, 0x27, 0x6c, 0xc6, 0xf5, 0xa7, 0x0a, 0x0b, 0x45, 0xc0,
0x44, 0x96, 0x69, 0xad,
],
},
];
#[test]
fn test_sync_resolve_github() {
let res = resolve(HARDCODED_RESOLVERS, "github.com");
match res {
Ok(ips) => {
println!("Sync resolved github.com => {:?}", ips);
assert!(!ips.is_empty() && (ips[0].is_ipv4() || ips[0].is_ipv6()));
}
Err(e) => {
panic!("Sync resolve failed: {e:?}");
}
}
}
#[test]
fn test_sync_resolve_custom_resolvers() {
let res = resolve(CUSTOM_RESOLVERS, "github.com");
match res {
Ok(ips) => {
println!("Sync resolved via custom list github.com => {:?}", ips);
assert!(!ips.is_empty() && (ips[0].is_ipv4() || ips[0].is_ipv6()));
}
Err(e) => {
panic!("Sync custom resolve failed: {e:?}");
}
}
}
#[test]
fn test_sync_session_reuse() {
let sessions =
establish_dnscrypt_session(HARDCODED_RESOLVERS).expect("Failed to establish sync session");
let session = &sessions[0];
let ips1 = resolve_domain_via_dnscrypt_session(session, "github.com")
.expect("Failed to resolve github.com with session");
let ips2 = resolve_domain_via_dnscrypt_session(session, "quad9.net")
.expect("Failed to resolve quad9.net with session");
println!(
"Session reused: github.com => {:?}, quad9.net => {:?}",
ips1, ips2
);
assert!(!ips1.is_empty() && (ips1[0].is_ipv4() || ips1[0].is_ipv6()));
assert!(!ips2.is_empty() && (ips2[0].is_ipv4() || ips2[0].is_ipv6()));
}
#[test]
fn test_manual_udp_and_tcp_resolution() {
let mut udp_success = false;
let mut tcp_success = false;
for i in 0..CUSTOM_RESOLVERS.len() {
let Ok(sessions) = establish_dnscrypt_session(&CUSTOM_RESOLVERS[i..=i]) else {
continue;
};
let session = &sessions[0];
let build_query = || {
let mut txid = [0u8; 2];
getrandom::fill(&mut txid).expect("getrandom failed");
let client_query = dnscrypt::packet::build_a_record_query("github.com", txid);
let padded_query = dnscrypt::packet::pad_query(&client_query, 1024);
let mut client_nonce = [0u8; 12];
getrandom::fill(&mut client_nonce).expect("getrandom failed");
let mut query_nonce = [0u8; 24];
query_nonce[..12].copy_from_slice(&client_nonce);
let encrypted_query = dnscrypt::crypto::xchacha20_djb_poly1305_encrypt(
&session.shared_key,
&query_nonce,
&padded_query,
);
let mut request_packet = Vec::with_capacity(8 + 32 + 12 + encrypted_query.len());
request_packet.extend_from_slice(&session.client_magic);
request_packet.extend_from_slice(&session.client_pk_bytes);
request_packet.extend_from_slice(&client_nonce);
request_packet.extend_from_slice(&encrypted_query);
request_packet
};
let request_packet_udp = build_query();
if let Ok(resp_udp) =
dnscrypt::net::send_dns_query_udp(session.resolver_ip, &request_packet_udp)
{
if resp_udp.len() > 32 {
let mut resp_nonce = [0u8; 24];
resp_nonce.copy_from_slice(&resp_udp[8..32]);
if let Ok(decrypted) = dnscrypt::crypto::xchacha20_djb_poly1305_decrypt(
&session.shared_key,
&resp_nonce,
&resp_udp[32..],
) {
if let Ok(unpadded) = dnscrypt::packet::unpad_response(&decrypted) {
let ips_parsed = dnscrypt::packet::parse_dns_response(&unpadded);
if !ips_parsed.is_empty() && ips_parsed[0].is_ipv4() {
udp_success = true;
}
}
}
}
}
let request_packet_tcp = build_query();
if let Ok(resp_tcp) =
dnscrypt::net::send_dns_query_tcp(session.resolver_ip, &request_packet_tcp)
{
if resp_tcp.len() > 32 {
tcp_success = true;
}
}
if udp_success && tcp_success {
break;
}
}
assert!(udp_success, "UDP query failed across all custom resolvers");
assert!(tcp_success, "TCP query failed across all custom resolvers");
}
#[test]
fn test_all_hardcoded_providers_individually() {
let test_domains = ["github.com", "google.com", "proton.me", "mullvad.net"];
for domain in test_domains {
println!("Testing domain: {}", domain);
let mut results: Vec<(String, std::net::IpAddr)> = Vec::new();
for resolver in HARDCODED_RESOLVERS {
println!(
" -> Querying provider {} at IP {}",
resolver.provider_name, resolver.ip
);
let single_resolver_slice = std::slice::from_ref(resolver);
let sessions =
match dnscrypt::resolver::establish_dnscrypt_session(single_resolver_slice) {
Ok(s) => s,
Err(e) => {
let err_str = e.to_string();
if err_str.contains("os error 111")
|| err_str.contains("timeout")
|| err_str.contains("timed out")
{
println!(
" Skipping {} ({}) due to network block/timeout: {}",
resolver.provider_name, resolver.ip, err_str
);
continue;
}
panic!(
"Failed to establish session with {} ({}): {}",
resolver.provider_name, resolver.ip, e
);
}
};
assert_eq!(sessions.len(), 1, "Should have exactly 1 session");
let session = &sessions[0];
let ips = match dnscrypt::resolver::resolve_domain_via_dnscrypt_session(session, domain)
{
Ok(ips) => ips,
Err(e) => {
let err_str = e.to_string();
if err_str.contains("os error 111")
|| err_str.contains("timeout")
|| err_str.contains("timed out")
{
println!(
" Skipping resolution for {} ({}) due to network block/timeout: {}",
resolver.provider_name, resolver.ip, err_str
);
continue;
}
println!(
" WARNING: Failed to resolve {} via {} ({}): {}",
domain, resolver.provider_name, resolver.ip, e
);
continue;
}
};
assert!(!ips.is_empty() && (ips[0].is_ipv4() || ips[0].is_ipv6()));
println!(
" Successfully resolved {} to {:?} via {}",
domain, ips, resolver.ip
);
results.push((
format!("{} ({})", resolver.provider_name, resolver.ip),
ips[0],
));
}
if results.is_empty() {
continue;
}
let mut frequency: std::collections::HashMap<std::net::IpAddr, usize> =
std::collections::HashMap::new();
for (_, ip) in &results {
*frequency.entry(*ip).or_insert(0) += 1;
}
let mut majority_ip = results[0].1;
let mut max_count = 0;
for (ip, count) in &frequency {
if *count > max_count {
max_count = *count;
majority_ip = *ip;
}
}
for (provider_id, ip) in &results {
if *ip != majority_ip {
println!(
"WARNING (Geo-DNS / CDN): Provider {} returned IP {} for {}. Majority IP is {}.",
provider_id, ip, domain, majority_ip
);
}
}
}
}
#[cfg(feature = "max-security")]
#[test]
fn test_resolve_max_security_all_providers_fail_is_reported() {
let invalid_domain = format!("{}.com", "a".repeat(64));
let result = resolve(HARDCODED_RESOLVERS, &invalid_domain);
match result {
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("all providers failed"),
"unexpected error message: {msg}"
);
}
Ok(ips) => panic!("expected resolution to fail for an invalid domain, got {ips:?}"),
}
}
#[cfg(feature = "tokio")]
mod async_tests {
use super::*;
#[cfg(feature = "reqwest")]
use dnscrypt::DnscryptResolver;
use dnscrypt::{
establish_dnscrypt_session_async, resolve_async, resolve_domain_via_dnscrypt_session_async,
};
#[cfg(feature = "reqwest")]
use std::sync::Arc;
#[tokio::test]
async fn test_async_resolve_github() {
let res = resolve_async(HARDCODED_RESOLVERS, "github.com").await;
match res {
Ok(ips) => {
println!("Async resolved github.com => {:?}", ips);
assert!(!ips.is_empty() && (ips[0].is_ipv4() || ips[0].is_ipv6()));
}
Err(e) => {
panic!("Async resolve failed: {e:?}");
}
}
}
#[tokio::test]
async fn test_async_resolve_custom_resolvers() {
let res = resolve_async(CUSTOM_RESOLVERS, "github.com").await;
match res {
Ok(ips) => {
println!("Async resolved via custom list github.com => {:?}", ips);
assert!(!ips.is_empty() && (ips[0].is_ipv4() || ips[0].is_ipv6()));
}
Err(e) => {
panic!("Async custom resolve failed: {e:?}");
}
}
}
#[tokio::test]
async fn test_async_session_reuse() {
let sessions = establish_dnscrypt_session_async(HARDCODED_RESOLVERS)
.await
.expect("Failed to establish async session");
let session = &sessions[0];
let ips1 = resolve_domain_via_dnscrypt_session_async(session, "github.com")
.await
.expect("Failed to resolve github.com with async session");
let ips2 = resolve_domain_via_dnscrypt_session_async(session, "quad9.net")
.await
.expect("Failed to resolve quad9.net with async session");
println!(
"Async session reused: github.com => {:?}, quad9.net => {:?}",
ips1, ips2
);
assert!(!ips1.is_empty() && (ips1[0].is_ipv4() || ips1[0].is_ipv6()));
assert!(!ips2.is_empty() && (ips2[0].is_ipv4() || ips2[0].is_ipv6()));
}
#[cfg(feature = "max-security")]
#[tokio::test]
async fn test_resolve_async_max_security_all_providers_fail_is_reported() {
let invalid_domain = format!("{}.com", "a".repeat(64));
let result = resolve_async(HARDCODED_RESOLVERS, &invalid_domain).await;
match result {
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("all providers failed"),
"unexpected error message: {msg}"
);
}
Ok(ips) => panic!("expected resolution to fail for an invalid domain, got {ips:?}"),
}
}
#[cfg(feature = "reqwest")]
#[tokio::test]
async fn test_reqwest_resolver_integration() {
let resolver = Arc::new(DnscryptResolver::new());
let client = reqwest::Client::builder()
.dns_resolver(resolver)
.build()
.expect("Failed to build reqwest client");
let resp = client.get("http://www.github.com").send().await;
match resp {
Ok(r) => {
let status = r.status();
println!("Reqwest request succeeded with status: {status}");
assert!(status.is_success() || status.is_redirection());
}
Err(e) => {
println!("Reqwest request failed (expected if network outbound is limited): {e:?}");
}
}
}
#[cfg(feature = "reqwest")]
#[tokio::test]
async fn test_reqwest_resolver_custom_resolvers_integration() {
let resolver = Arc::new(DnscryptResolver::new_with_resolvers(CUSTOM_RESOLVERS));
let client = reqwest::Client::builder()
.dns_resolver(resolver)
.build()
.expect("Failed to build reqwest client");
let resp = client.get("http://www.github.com").send().await;
match resp {
Ok(r) => {
let status = r.status();
println!("Reqwest request with custom resolvers succeeded with status: {status}");
assert!(status.is_success() || status.is_redirection());
}
Err(e) => {
println!("Reqwest request with custom resolvers failed: {e:?}");
}
}
}
}