use std::net::{IpAddr, SocketAddr};
use std::sync::Arc;
use hickory_resolver::config::LookupIpStrategy;
use serde::{Deserialize, Serialize};
use url::Url;
use crate::client_proxy_chain::ClientChainGroup;
use crate::resolver::Resolver;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum IpStrategy {
Ipv4Only,
Ipv6Only,
Ipv4AndIpv6,
#[default]
Ipv4ThenIpv6,
Ipv6ThenIpv4,
}
impl IpStrategy {
pub fn to_hickory(self) -> LookupIpStrategy {
match self {
Self::Ipv4Only => LookupIpStrategy::Ipv4Only,
Self::Ipv6Only => LookupIpStrategy::Ipv6Only,
Self::Ipv4AndIpv6 => LookupIpStrategy::Ipv4AndIpv6,
Self::Ipv4ThenIpv6 => LookupIpStrategy::Ipv4thenIpv6,
Self::Ipv6ThenIpv4 => LookupIpStrategy::Ipv6thenIpv4,
}
}
}
#[derive(Debug)]
pub struct ParsedDnsServerEntry {
pub server: ParsedDnsServer,
pub client_chain: Arc<ClientChainGroup>,
pub bootstrap_resolver: Arc<dyn Resolver>,
pub ip_strategy: IpStrategy,
pub timeout_secs: u32,
pub connect_timeout_secs: u32,
pub attempts: usize,
}
impl ParsedDnsServerEntry {
pub fn new(
server: ParsedDnsServer,
chain: Arc<ClientChainGroup>,
bootstrap: Arc<dyn Resolver>,
ip_strategy: IpStrategy,
timeout_secs: u32,
connect_timeout_secs: u32,
attempts: usize,
) -> Self {
Self {
server,
client_chain: chain,
bootstrap_resolver: bootstrap,
ip_strategy,
timeout_secs,
connect_timeout_secs,
attempts,
}
}
}
#[derive(Debug, Clone)]
pub enum ParsedDnsServer {
System,
Udp { addr: SocketAddr },
Tcp { addr: SocketAddr },
Tls {
addr: SocketAddr,
server_name: Arc<str>,
},
Https {
addr: SocketAddr,
server_name: Arc<str>,
path: Arc<str>,
},
H3 {
addr: SocketAddr,
server_name: Arc<str>,
path: Arc<str>,
},
}
#[derive(Debug, Clone)]
pub enum DnsHost {
Ip(IpAddr),
Hostname(String),
}
impl DnsHost {
pub fn as_hostname(&self) -> Option<&str> {
match self {
Self::Hostname(s) => Some(s),
Self::Ip(_) => None,
}
}
}
#[derive(Debug, Clone)]
pub enum ParsedDnsUrl {
System,
Udp { host: DnsHost, port: u16 },
Tcp { host: DnsHost, port: u16 },
Tls {
host: DnsHost,
port: u16,
server_name: String,
},
Https {
host: DnsHost,
port: u16,
server_name: String,
path: String,
},
H3 {
host: DnsHost,
port: u16,
server_name: String,
path: String,
},
}
impl ParsedDnsUrl {
pub fn parse(url_str: &str) -> Result<Self, DnsConfigError> {
Self::parse_with_server_name(url_str, None)
}
pub fn parse_with_server_name(
url_str: &str,
server_name_override: Option<&str>,
) -> Result<Self, DnsConfigError> {
if url_str == "system" {
return Ok(Self::System);
}
let url = Url::parse(url_str)
.map_err(|e| DnsConfigError::InvalidUrl(url_str.to_string(), e.to_string()))?;
let scheme = url.scheme();
let host_str = url
.host_str()
.ok_or_else(|| DnsConfigError::MissingHost(url_str.to_string()))?;
let host = match host_str.parse::<IpAddr>() {
Ok(ip) => DnsHost::Ip(ip),
Err(_) => DnsHost::Hostname(host_str.to_string()),
};
let server_name = server_name_override
.map(String::from)
.unwrap_or_else(|| host_str.to_string());
match scheme {
"udp" => {
let port = url.port().unwrap_or(53);
Ok(Self::Udp { host, port })
}
"tcp" => {
let port = url.port().unwrap_or(53);
Ok(Self::Tcp { host, port })
}
"tls" => {
let port = url.port().unwrap_or(853);
Ok(Self::Tls {
host,
port,
server_name,
})
}
"https" => {
let port = url.port().unwrap_or(443);
let path = url.path().to_string();
Ok(Self::Https {
host,
port,
server_name,
path,
})
}
"h3" => {
let port = url.port().unwrap_or(443);
let path = url.path().to_string();
Ok(Self::H3 {
host,
port,
server_name,
path,
})
}
_ => Err(DnsConfigError::UnsupportedScheme(scheme.to_string())),
}
}
pub fn has_hostname(&self) -> bool {
self.hostname().is_some()
}
pub fn hostname(&self) -> Option<&str> {
match self {
Self::System => None,
Self::Udp { host, .. }
| Self::Tcp { host, .. }
| Self::Tls { host, .. }
| Self::Https { host, .. }
| Self::H3 { host, .. } => host.as_hostname(),
}
}
pub fn to_parsed_server(
&self,
resolved_ip: Option<IpAddr>,
) -> Result<ParsedDnsServer, DnsConfigError> {
match self {
Self::System => Ok(ParsedDnsServer::System),
Self::Udp { host, port } => {
let ip = Self::get_ip(host, resolved_ip)?;
Ok(ParsedDnsServer::Udp {
addr: SocketAddr::new(ip, *port),
})
}
Self::Tcp { host, port } => {
let ip = Self::get_ip(host, resolved_ip)?;
Ok(ParsedDnsServer::Tcp {
addr: SocketAddr::new(ip, *port),
})
}
Self::Tls {
host,
port,
server_name,
} => {
let ip = Self::get_ip(host, resolved_ip)?;
Ok(ParsedDnsServer::Tls {
addr: SocketAddr::new(ip, *port),
server_name: Arc::from(server_name.as_str()),
})
}
Self::Https {
host,
port,
server_name,
path,
} => {
let ip = Self::get_ip(host, resolved_ip)?;
Ok(ParsedDnsServer::Https {
addr: SocketAddr::new(ip, *port),
server_name: Arc::from(server_name.as_str()),
path: Arc::from(path.as_str()),
})
}
Self::H3 {
host,
port,
server_name,
path,
} => {
let ip = Self::get_ip(host, resolved_ip)?;
Ok(ParsedDnsServer::H3 {
addr: SocketAddr::new(ip, *port),
server_name: Arc::from(server_name.as_str()),
path: Arc::from(path.as_str()),
})
}
}
}
fn get_ip(host: &DnsHost, resolved_ip: Option<IpAddr>) -> Result<IpAddr, DnsConfigError> {
match host {
DnsHost::Ip(ip) => Ok(*ip),
DnsHost::Hostname(hostname) => {
resolved_ip.ok_or_else(|| DnsConfigError::HostnameNotResolved(hostname.clone()))
}
}
}
}
#[derive(Debug)]
pub enum DnsConfigError {
InvalidUrl(String, String),
MissingHost(String),
HostnameNotResolved(String),
UnsupportedScheme(String),
}
impl std::fmt::Display for DnsConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidUrl(url, err) => write!(f, "invalid DNS URL '{}': {}", url, err),
Self::MissingHost(url) => write!(f, "DNS URL missing host: {}", url),
Self::HostnameNotResolved(host) => {
write!(f, "hostname not resolved: {}", host)
}
Self::UnsupportedScheme(scheme) => write!(f, "unsupported DNS scheme: {}", scheme),
}
}
}
impl std::error::Error for DnsConfigError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_system() {
let url = ParsedDnsUrl::parse("system").unwrap();
assert!(matches!(url, ParsedDnsUrl::System));
let server = url.to_parsed_server(None).unwrap();
assert!(matches!(server, ParsedDnsServer::System));
}
#[test]
fn test_parse_udp_ip() {
let url = ParsedDnsUrl::parse("udp://8.8.8.8").unwrap();
match &url {
ParsedDnsUrl::Udp { host, port } => {
assert!(matches!(host, DnsHost::Ip(_)));
assert_eq!(*port, 53);
}
_ => panic!("expected Udp"),
}
assert!(!url.has_hostname());
let server = url.to_parsed_server(None).unwrap();
match server {
ParsedDnsServer::Udp { addr } => {
assert_eq!(addr.ip().to_string(), "8.8.8.8");
assert_eq!(addr.port(), 53);
}
_ => panic!("expected Udp"),
}
}
#[test]
fn test_parse_udp_custom_port() {
let url = ParsedDnsUrl::parse("udp://8.8.8.8:5353").unwrap();
let server = url.to_parsed_server(None).unwrap();
match server {
ParsedDnsServer::Udp { addr } => {
assert_eq!(addr.port(), 5353);
}
_ => panic!("expected Udp"),
}
}
#[test]
fn test_parse_tcp_ip() {
let url = ParsedDnsUrl::parse("tcp://1.1.1.1").unwrap();
let server = url.to_parsed_server(None).unwrap();
match server {
ParsedDnsServer::Tcp { addr } => {
assert_eq!(addr.ip().to_string(), "1.1.1.1");
assert_eq!(addr.port(), 53);
}
_ => panic!("expected Tcp"),
}
}
#[test]
fn test_parse_tls_ip() {
let url = ParsedDnsUrl::parse("tls://1.1.1.1").unwrap();
let server = url.to_parsed_server(None).unwrap();
match server {
ParsedDnsServer::Tls { addr, server_name } => {
assert_eq!(addr.ip().to_string(), "1.1.1.1");
assert_eq!(addr.port(), 853);
assert_eq!(&*server_name, "1.1.1.1");
}
_ => panic!("expected Tls"),
}
}
#[test]
fn test_parse_https_ip() {
let url = ParsedDnsUrl::parse("https://1.1.1.1/dns-query").unwrap();
let server = url.to_parsed_server(None).unwrap();
match server {
ParsedDnsServer::Https {
addr,
server_name,
path,
} => {
assert_eq!(addr.ip().to_string(), "1.1.1.1");
assert_eq!(addr.port(), 443);
assert_eq!(&*server_name, "1.1.1.1");
assert_eq!(&*path, "/dns-query");
}
_ => panic!("expected Https"),
}
}
#[test]
fn test_parse_https_root_path() {
let url = ParsedDnsUrl::parse("https://8.8.8.8").unwrap();
let server = url.to_parsed_server(None).unwrap();
match server {
ParsedDnsServer::Https { path, .. } => {
assert_eq!(&*path, "/");
}
_ => panic!("expected Https"),
}
}
#[test]
fn test_parse_h3_ip() {
let url = ParsedDnsUrl::parse("h3://1.1.1.1/dns-query").unwrap();
let server = url.to_parsed_server(None).unwrap();
match server {
ParsedDnsServer::H3 {
addr,
server_name,
path,
} => {
assert_eq!(addr.ip().to_string(), "1.1.1.1");
assert_eq!(addr.port(), 443);
assert_eq!(&*server_name, "1.1.1.1");
assert_eq!(&*path, "/dns-query");
}
_ => panic!("expected H3"),
}
}
#[test]
fn test_parse_h3_hostname() {
let url = ParsedDnsUrl::parse("h3://cloudflare-dns.com/dns-query").unwrap();
assert!(url.has_hostname());
match &url {
ParsedDnsUrl::H3 {
host,
port,
server_name,
path,
} => {
assert!(matches!(host, DnsHost::Hostname(h) if h == "cloudflare-dns.com"));
assert_eq!(*port, 443);
assert_eq!(server_name, "cloudflare-dns.com");
assert_eq!(path, "/dns-query");
}
_ => panic!("expected H3"),
}
}
#[test]
fn test_parse_hostname() {
let url = ParsedDnsUrl::parse("tls://dns.google").unwrap();
assert!(url.has_hostname());
match &url {
ParsedDnsUrl::Tls {
host, server_name, ..
} => {
assert!(matches!(host, DnsHost::Hostname(h) if h == "dns.google"));
assert_eq!(server_name, "dns.google");
}
_ => panic!("expected Tls"),
}
let result = url.to_parsed_server(None);
assert!(result.is_err());
let ip: IpAddr = "8.8.8.8".parse().unwrap();
let server = url.to_parsed_server(Some(ip)).unwrap();
match server {
ParsedDnsServer::Tls { addr, server_name } => {
assert_eq!(addr.ip().to_string(), "8.8.8.8");
assert_eq!(&*server_name, "dns.google");
}
_ => panic!("expected Tls"),
}
}
#[test]
fn test_parse_with_server_name_override() {
let url =
ParsedDnsUrl::parse_with_server_name("tls://1.2.3.4", Some("dns.example.com")).unwrap();
match url {
ParsedDnsUrl::Tls { server_name, .. } => {
assert_eq!(server_name, "dns.example.com");
}
_ => panic!("expected Tls"),
}
}
#[test]
fn test_parse_invalid_scheme() {
let result = ParsedDnsUrl::parse("quic://8.8.8.8");
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
DnsConfigError::UnsupportedScheme(_)
));
}
#[test]
fn test_ip_strategy_serde() {
assert_eq!(
serde_yaml::from_str::<IpStrategy>("ipv4_only").unwrap(),
IpStrategy::Ipv4Only
);
assert_eq!(
serde_yaml::from_str::<IpStrategy>("ipv6_only").unwrap(),
IpStrategy::Ipv6Only
);
assert_eq!(
serde_yaml::from_str::<IpStrategy>("ipv4_and_ipv6").unwrap(),
IpStrategy::Ipv4AndIpv6
);
assert_eq!(
serde_yaml::from_str::<IpStrategy>("ipv4_then_ipv6").unwrap(),
IpStrategy::Ipv4ThenIpv6
);
assert_eq!(
serde_yaml::from_str::<IpStrategy>("ipv6_then_ipv4").unwrap(),
IpStrategy::Ipv6ThenIpv4
);
}
#[test]
fn test_ip_strategy_serde_invalid() {
let result = serde_yaml::from_str::<IpStrategy>("invalid");
assert!(result.is_err());
}
#[test]
fn test_ip_strategy_default() {
assert_eq!(IpStrategy::default(), IpStrategy::Ipv4ThenIpv6);
}
#[test]
fn test_ip_strategy_to_hickory() {
assert!(matches!(
IpStrategy::Ipv4Only.to_hickory(),
LookupIpStrategy::Ipv4Only
));
assert!(matches!(
IpStrategy::Ipv6Only.to_hickory(),
LookupIpStrategy::Ipv6Only
));
assert!(matches!(
IpStrategy::Ipv4AndIpv6.to_hickory(),
LookupIpStrategy::Ipv4AndIpv6
));
assert!(matches!(
IpStrategy::Ipv4ThenIpv6.to_hickory(),
LookupIpStrategy::Ipv4thenIpv6
));
assert!(matches!(
IpStrategy::Ipv6ThenIpv4.to_hickory(),
LookupIpStrategy::Ipv6thenIpv4
));
}
}