#![allow(clippy::too_many_arguments)]
use async_trait::async_trait;
use clap::Parser;
use log::{debug, error, info, warn};
use rand::RngExt;
use serde::{Deserialize, Serialize};
use std::fs;
use std::net::SocketAddr;
use std::os::fd::AsRawFd;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpStream, UdpSocket};
use tokio::sync::Mutex;
use ip_validator::{IpRange, IpValidator};
mod allowed_zones;
mod cache;
mod connection_slots;
mod control;
mod dns_key;
mod dns_key_test;
mod dns_parser;
mod dns_processor;
mod doh;
mod errors;
mod hooks;
mod ip_validator;
mod load_balancer;
mod metrics;
mod net;
mod nx_zones;
mod probe;
mod query_logger;
mod query_manager;
mod rate_limiter;
mod resolver;
mod stats;
use connection_slots::{ClientSlots, ConnectionSlots};
use dns_processor::DnsQueryProcessor;
use errors::{DnsError, EtchDnsError, EtchDnsResult};
use probe::probe_server;
use query_manager::QueryManager;
use stats::SharedStats;
#[macro_use]
extern crate bpf;
#[derive(Parser, Debug)]
#[command(author, version, about = "An efficient caching DNS proxy")]
struct Args {
#[arg(short, long, value_name = "FILE")]
config: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct Config {
#[serde(default = "default_log_level")]
log_level: String,
#[serde(default = "default_listen_addresses")]
listen_addresses: Vec<String>,
#[serde(default = "default_doh_listen_addresses")]
doh_listen_addresses: Vec<String>,
#[serde(default = "default_packet_size")]
dns_packet_len_max: usize,
#[serde(default = "default_max_udp_clients")]
max_udp_clients: usize,
#[serde(default = "default_max_tcp_clients")]
max_tcp_clients: usize,
#[serde(default = "default_max_inflight_queries")]
max_inflight_queries: usize,
#[serde(default = "default_server_timeout")]
server_timeout: u64,
#[serde(default = "default_upstream_servers")]
upstream_servers: Vec<String>,
#[serde(default = "default_load_balancing_strategy")]
load_balancing_strategy: String,
#[serde(default)]
metrics_address: Option<String>,
#[serde(default = "default_metrics_path")]
metrics_path: String,
#[serde(default = "default_udp_rate_limit_window")]
udp_rate_limit_window: u64,
#[serde(default = "default_udp_rate_limit_count")]
udp_rate_limit_count: u32,
#[serde(default = "default_udp_rate_limit_max_clients")]
udp_rate_limit_max_clients: usize,
#[serde(default = "default_tcp_rate_limit_window")]
tcp_rate_limit_window: u64,
#[serde(default = "default_tcp_rate_limit_count")]
tcp_rate_limit_count: u32,
#[serde(default = "default_tcp_rate_limit_max_clients")]
tcp_rate_limit_max_clients: usize,
#[serde(default = "default_doh_rate_limit_window")]
doh_rate_limit_window: u64,
#[serde(default = "default_doh_rate_limit_count")]
doh_rate_limit_count: u32,
#[serde(default = "default_doh_rate_limit_max_clients")]
doh_rate_limit_max_clients: usize,
#[serde(default = "default_max_metrics_connections")]
max_metrics_connections: usize,
#[serde(default)]
enable_strict_ip_validation: bool,
#[serde(default)]
blocked_ip_ranges: Vec<String>,
#[serde(default = "default_block_private_ip_ranges")]
block_private_ips: bool,
#[serde(default = "default_block_loopback_ip_ranges")]
block_loopback_ips: bool,
#[serde(default = "default_min_client_port")]
min_client_port: u16,
#[serde(default = "default_max_doh_connections")]
max_doh_connections: usize,
#[serde(default = "default_control_listen_addresses")]
control_listen_addresses: Vec<String>,
#[serde(default = "default_max_control_connections")]
max_control_connections: usize,
#[serde(default = "default_control_path")]
control_path: String,
#[serde(default = "default_probe_interval")]
probe_interval: u64,
#[serde(default = "default_cache_size")]
cache_size: usize,
#[serde(default = "default_authoritative_dns")]
authoritative_dns: bool,
#[serde(default)]
allowed_zones_file: Option<String>,
#[serde(default)]
nx_zones_file: Option<String>,
#[serde(default = "default_serve_stale_grace_time")]
serve_stale_grace_time: u64,
#[serde(default = "default_serve_stale_ttl")]
serve_stale_ttl: u32,
#[serde(default = "default_negative_cache_ttl")]
negative_cache_ttl: u32,
#[serde(default = "default_min_cache_ttl")]
min_cache_ttl: u32,
#[serde(default)]
#[cfg(feature = "hooks")]
hooks_wasm_file: Option<String>,
#[serde(default, skip)]
#[cfg(not(feature = "hooks"))]
hooks_wasm_file: Option<String>,
#[serde(default)]
#[cfg(feature = "hooks")]
hooks_wasm_wasi: bool,
#[serde(default, skip)]
#[cfg(not(feature = "hooks"))]
hooks_wasm_wasi: bool,
#[serde(default)]
query_log_file: Option<String>,
#[serde(default = "default_query_log_include_timestamp")]
query_log_include_timestamp: bool,
#[serde(default = "default_query_log_include_client_addr")]
query_log_include_client_addr: bool,
#[serde(default = "default_query_log_include_query_type")]
query_log_include_query_type: bool,
#[serde(default = "default_query_log_include_query_class")]
query_log_include_query_class: bool,
#[serde(default = "default_query_log_rotation_size")]
query_log_rotation_size: usize,
#[serde(default = "default_query_log_rotation_interval")]
query_log_rotation_interval: String,
#[serde(default = "default_query_log_rotation_count")]
query_log_rotation_count: usize,
#[serde(default = "default_query_log_compression")]
query_log_compression: bool,
#[serde(default)]
user: Option<String>,
#[serde(default)]
group: Option<String>,
#[serde(default)]
chroot: Option<String>,
#[serde(default = "default_enable_ecs")]
enable_ecs: bool,
#[serde(default = "default_ecs_prefix_v4")]
ecs_prefix_v4: u8,
#[serde(default = "default_ecs_prefix_v6")]
ecs_prefix_v6: u8,
#[serde(default = "default_spoof_protection")]
spoof_protection: bool,
}
fn default_listen_addresses() -> Vec<String> {
vec!["0.0.0.0:10000".to_string()]
}
fn default_doh_listen_addresses() -> Vec<String> {
Vec::new() }
fn default_packet_size() -> usize {
4096
}
fn default_max_udp_clients() -> usize {
1000
}
fn default_max_tcp_clients() -> usize {
1000
}
fn default_max_inflight_queries() -> usize {
500
}
fn default_server_timeout() -> u64 {
5 }
fn default_upstream_servers() -> Vec<String> {
vec!["8.8.8.8:53".to_string(), "8.8.4.4:53".to_string()]
}
fn default_metrics_path() -> String {
"/metrics".to_string()
}
fn default_max_metrics_connections() -> usize {
5
}
fn default_max_doh_connections() -> usize {
10 }
fn default_control_listen_addresses() -> Vec<String> {
Vec::new() }
fn default_max_control_connections() -> usize {
5 }
fn default_control_path() -> String {
"/control".to_string() }
fn default_load_balancing_strategy() -> String {
"fastest".to_string()
}
fn default_probe_interval() -> u64 {
60 }
fn default_cache_size() -> usize {
10000 }
fn default_authoritative_dns() -> bool {
false }
fn default_serve_stale_grace_time() -> u64 {
0 }
fn default_serve_stale_ttl() -> u32 {
30 }
fn default_udp_rate_limit_window() -> u64 {
1 }
fn default_udp_rate_limit_count() -> u32 {
100 }
fn default_tcp_rate_limit_window() -> u64 {
5 }
fn default_tcp_rate_limit_count() -> u32 {
20 }
fn default_udp_rate_limit_max_clients() -> usize {
10000 }
fn default_tcp_rate_limit_max_clients() -> usize {
5000 }
fn default_doh_rate_limit_window() -> u64 {
10 }
fn default_doh_rate_limit_count() -> u32 {
30 }
fn default_doh_rate_limit_max_clients() -> usize {
5000 }
fn default_log_level() -> String {
"info".to_string() }
fn default_negative_cache_ttl() -> u32 {
1 }
fn default_min_cache_ttl() -> u32 {
1 }
fn default_query_log_include_timestamp() -> bool {
true }
fn default_query_log_include_client_addr() -> bool {
true }
fn default_query_log_include_query_type() -> bool {
true }
fn default_query_log_include_query_class() -> bool {
false }
fn default_query_log_rotation_size() -> usize {
0 }
fn default_query_log_rotation_interval() -> String {
"daily".to_string() }
fn default_query_log_rotation_count() -> usize {
7 }
fn default_query_log_compression() -> bool {
false }
fn default_enable_ecs() -> bool {
false }
fn default_ecs_prefix_v4() -> u8 {
24 }
fn default_ecs_prefix_v6() -> u8 {
56 }
fn default_spoof_protection() -> bool {
true }
fn default_block_private_ip_ranges() -> bool {
true }
fn default_block_loopback_ip_ranges() -> bool {
true }
fn default_min_client_port() -> u16 {
1024 }
impl Config {
fn from_file(path: &PathBuf) -> EtchDnsResult<Self> {
let content = fs::read_to_string(path).map_err(|e| {
EtchDnsError::ConfigReadError(format!(
"Failed to read config file {}: {}",
path.display(),
e
))
})?;
let config: Config = toml::from_str(&content).map_err(|e| {
EtchDnsError::ConfigParseError(format!(
"Failed to parse config file {}: {}",
path.display(),
e
))
})?;
config.validate()?;
Ok(config)
}
fn validate(&self) -> EtchDnsResult<()> {
match self.log_level.to_lowercase().as_str() {
"trace" | "debug" | "info" | "warn" | "error" => {}
_ => {
return Err(EtchDnsError::Other(format!(
"Invalid log level: {}. Must be one of: trace, debug, info, warn, error",
self.log_level
)));
}
}
if !(512..=dns_parser::DNS_MAX_PACKET_SIZE).contains(&self.dns_packet_len_max) {
return Err(EtchDnsError::Other(format!(
"Invalid DNS packet size: {}. Must be between 512 and {} bytes",
self.dns_packet_len_max,
dns_parser::DNS_MAX_PACKET_SIZE,
)));
}
if self.max_inflight_queries < 1 {
return Err(EtchDnsError::Other(format!(
"Invalid max_inflight_queries: {}. Must be at least 1",
self.max_inflight_queries
)));
}
if self.max_udp_clients == 0 || self.max_tcp_clients == 0 {
return Err(EtchDnsError::Other(
"max_udp_clients and max_tcp_clients must be greater than 0".to_string(),
));
}
if self.cache_size == 0 {
return Err(EtchDnsError::Other(
"cache_size must be greater than 0".to_string(),
));
}
if !self.doh_listen_addresses.is_empty() && self.max_doh_connections == 0 {
return Err(EtchDnsError::Other(
"max_doh_connections must be greater than 0 when DoH is enabled".to_string(),
));
}
if self.metrics_address.is_some() && self.max_metrics_connections == 0 {
return Err(EtchDnsError::Other(
"max_metrics_connections must be greater than 0 when metrics are enabled"
.to_string(),
));
}
if !self.control_listen_addresses.is_empty() && self.max_control_connections == 0 {
return Err(EtchDnsError::Other(
"max_control_connections must be greater than 0 when control is enabled"
.to_string(),
));
}
if self.server_timeout < 1 {
return Err(EtchDnsError::Other(format!(
"Invalid server_timeout: {}. Must be at least 1 second",
self.server_timeout
)));
}
for addr_str in &self.listen_addresses {
addr_str.parse::<SocketAddr>().map_err(|e| {
EtchDnsError::Other(format!("Invalid socket address {addr_str}: {e}"))
})?;
}
for addr_str in &self.doh_listen_addresses {
addr_str.parse::<SocketAddr>().map_err(|e| {
EtchDnsError::Other(format!("Invalid DoH socket address {addr_str}: {e}"))
})?;
}
for addr_str in &self.control_listen_addresses {
addr_str.parse::<SocketAddr>().map_err(|e| {
EtchDnsError::Other(format!(
"Invalid control server socket address {addr_str}: {e}"
))
})?;
}
if let Some(addr_str) = &self.metrics_address {
addr_str.parse::<SocketAddr>().map_err(|e| {
EtchDnsError::Other(format!("Invalid metrics socket address {addr_str}: {e}"))
})?;
}
if self.upstream_servers.is_empty() {
return Err(EtchDnsError::Other(
"At least one upstream server must be configured".to_string(),
));
}
for addr_str in &self.upstream_servers {
addr_str.parse::<SocketAddr>().map_err(|e| {
EtchDnsError::Other(format!("Invalid upstream socket address {addr_str}: {e}"))
})?;
}
for cidr in &self.blocked_ip_ranges {
IpRange::from_cidr(cidr).map_err(|e| {
EtchDnsError::Other(format!("Invalid blocked IP range {cidr}: {e}"))
})?;
}
if self.group.is_some() && self.user.is_none() {
return Err(EtchDnsError::Other(
"User must be specified when group is set".to_string(),
));
}
if !matches!(
self.query_log_rotation_interval.as_str(),
"hourly" | "daily" | "weekly" | "monthly" | "never"
) {
return Err(EtchDnsError::Other(format!(
"Invalid query_log_rotation_interval: {}",
self.query_log_rotation_interval
)));
}
if self.metrics_address.is_some() && !self.metrics_path.starts_with('/') {
return Err(EtchDnsError::Other(
"metrics_path must start with '/' when metrics are enabled".to_string(),
));
}
if !self.control_listen_addresses.is_empty() && !self.control_path.starts_with('/') {
return Err(EtchDnsError::Other(
"control_path must start with '/' when control is enabled".to_string(),
));
}
self.load_balancing_strategy
.parse::<load_balancer::LoadBalancingStrategy>()
.map_err(|e| EtchDnsError::Other(format!("Invalid load balancing strategy: {e}")))?;
if self.udp_rate_limit_window > 0 && self.udp_rate_limit_count == 0 {
return Err(EtchDnsError::Other(
"Invalid udp_rate_limit_count: must be greater than 0 when UDP rate limiting is enabled".to_string(),
));
}
if self.tcp_rate_limit_window > 0 && self.tcp_rate_limit_count == 0 {
return Err(EtchDnsError::Other(
"Invalid tcp_rate_limit_count: must be greater than 0 when TCP rate limiting is enabled".to_string(),
));
}
if self.udp_rate_limit_window > 0 && self.udp_rate_limit_max_clients == 0 {
return Err(EtchDnsError::Other(
"Invalid udp_rate_limit_max_clients: must be greater than 0 when UDP rate limiting is enabled".to_string(),
));
}
if self.tcp_rate_limit_window > 0 && self.tcp_rate_limit_max_clients == 0 {
return Err(EtchDnsError::Other(
"Invalid tcp_rate_limit_max_clients: must be greater than 0 when TCP rate limiting is enabled".to_string(),
));
}
if self.enable_ecs {
if self.ecs_prefix_v4 > 32 {
return Err(EtchDnsError::Other(format!(
"Invalid ecs_prefix_v4: {}. Must be between 0 and 32",
self.ecs_prefix_v4
)));
}
if self.ecs_prefix_v6 > 128 {
return Err(EtchDnsError::Other(format!(
"Invalid ecs_prefix_v6: {}. Must be between 0 and 128",
self.ecs_prefix_v6
)));
}
}
if self.doh_rate_limit_window > 0 && self.doh_rate_limit_count == 0 {
return Err(EtchDnsError::Other(
"Invalid doh_rate_limit_count: must be greater than 0 when DoH rate limiting is enabled".to_string(),
));
}
if self.doh_rate_limit_window > 0 && self.doh_rate_limit_max_clients == 0 {
return Err(EtchDnsError::Other(
"Invalid doh_rate_limit_max_clients: must be greater than 0 when DoH rate limiting is enabled".to_string(),
));
}
if let Some(chroot_dir) = &self.chroot {
if !std::path::Path::new(chroot_dir).exists() {
return Err(EtchDnsError::Other(format!(
"Chroot directory '{chroot_dir}' does not exist"
)));
}
if self.user.is_none() {
return Err(EtchDnsError::Other(
"User must be specified when chroot is enabled".to_string(),
));
}
}
Ok(())
}
fn socket_addrs(&self) -> EtchDnsResult<Vec<SocketAddr>> {
let mut addrs = Vec::new();
for addr_str in &self.listen_addresses {
let addr = addr_str.parse::<SocketAddr>().map_err(|e| {
EtchDnsError::Other(format!("Invalid socket address {addr_str}: {e}"))
})?;
addrs.push(addr);
}
Ok(addrs)
}
fn doh_socket_addrs(&self) -> EtchDnsResult<Vec<SocketAddr>> {
let mut addrs = Vec::new();
for addr_str in &self.doh_listen_addresses {
let addr = addr_str.parse::<SocketAddr>().map_err(|e| {
EtchDnsError::Other(format!("Invalid DoH socket address {addr_str}: {e}"))
})?;
addrs.push(addr);
}
Ok(addrs)
}
fn control_socket_addrs(&self) -> EtchDnsResult<Vec<SocketAddr>> {
let mut addrs = Vec::new();
for addr_str in &self.control_listen_addresses {
let addr = addr_str.parse::<SocketAddr>().map_err(|e| {
EtchDnsError::Other(format!(
"Invalid control server socket address {addr_str}: {e}"
))
})?;
addrs.push(addr);
}
Ok(addrs)
}
}
#[derive(Clone)]
struct ClientQuery {
data: Vec<u8>,
upstream_servers: Vec<String>,
server_timeout: u64,
dns_packet_len_max: usize,
max_udp_response_size: usize,
stats: Option<Arc<SharedStats>>,
load_balancing_strategy: load_balancer::LoadBalancingStrategy,
client_ip: Option<String>,
enable_ecs: bool,
ecs_prefix_v4: u8,
ecs_prefix_v6: u8,
spoof_protection: bool,
}
impl ClientQuery {
fn new(
data: Vec<u8>,
upstream_servers: Vec<String>,
server_timeout: u64,
dns_packet_len_max: usize,
stats: Arc<SharedStats>,
load_balancing_strategy: load_balancer::LoadBalancingStrategy,
) -> Self {
let max_udp_response_size = match dns_parser::extract_edns0_max_size(&data) {
Ok(Some(size)) => usize::from(size).max(dns_parser::DNS_MAX_UDP_PACKET_SIZE),
_ => dns_parser::DNS_MAX_UDP_PACKET_SIZE,
};
Self {
data,
upstream_servers,
server_timeout,
dns_packet_len_max,
max_udp_response_size,
stats: Some(stats),
load_balancing_strategy,
client_ip: None,
enable_ecs: false, ecs_prefix_v4: default_ecs_prefix_v4(),
ecs_prefix_v6: default_ecs_prefix_v6(),
spoof_protection: true, }
}
fn new_with_client_ip(
data: Vec<u8>,
upstream_servers: Vec<String>,
server_timeout: u64,
dns_packet_len_max: usize,
stats: Arc<SharedStats>,
load_balancing_strategy: load_balancer::LoadBalancingStrategy,
client_ip: String,
enable_ecs: bool,
ecs_prefix_v4: u8,
ecs_prefix_v6: u8,
spoof_protection: bool,
) -> Self {
let max_udp_response_size = match dns_parser::extract_edns0_max_size(&data) {
Ok(Some(size)) => usize::from(size).max(dns_parser::DNS_MAX_UDP_PACKET_SIZE),
_ => dns_parser::DNS_MAX_UDP_PACKET_SIZE,
};
Self {
data,
upstream_servers,
server_timeout,
dns_packet_len_max,
max_udp_response_size,
stats: Some(stats),
load_balancing_strategy,
client_ip: Some(client_ip),
enable_ecs,
ecs_prefix_v4,
ecs_prefix_v6,
spoof_protection,
}
}
async fn process(&self) -> EtchDnsResult<Vec<u8>> {
debug!("Processing query with data: {:?}", self.data);
if let Err(e) = dns_parser::validate_dns_packet(&self.data) {
debug!("Invalid DNS packet: {e}");
debug!("Dropping invalid DNS packet without response");
return Err(DnsError::InvalidPacket(format!("Invalid DNS packet: {e}")).into());
}
let upstream_server_str = self.select_upstream_server().await?;
debug!("Selected upstream server: {upstream_server_str}");
let upstream_addr = upstream_server_str.parse::<SocketAddr>().map_err(|e| {
DnsError::UpstreamError(format!(
"Failed to parse upstream server address {upstream_server_str}: {e}"
))
})?;
let bind_addr = if upstream_addr.is_ipv4() {
"0.0.0.0:0"
} else {
"[::]:0"
};
let upstream_socket = UdpSocket::bind(bind_addr).await.map_err(|e| {
DnsError::UpstreamError(format!(
"Failed to bind socket for upstream connection: {e}"
))
})?;
let mut query_data = self.data.clone();
if let Err(e) =
dns_parser::set_edns_max_payload_size(&mut query_data, self.dns_packet_len_max as u16)
{
error!("Failed to set EDNS maximum payload size: {e}");
debug!("Continuing with original query without EDNS");
query_data = self.data.clone(); } else {
debug!(
"Set EDNS maximum payload size to {} bytes",
self.dns_packet_len_max
);
if self.enable_ecs {
if let Some(client_ip) = &self.client_ip {
match dns_parser::add_edns_client_subnet(
&mut query_data,
client_ip,
self.ecs_prefix_v4,
self.ecs_prefix_v6,
self.dns_packet_len_max as u16,
) {
Ok(_) => {
debug!(
"Added EDNS-client-subnet for client IP {} with prefix lengths IPv4:{}/IPv6:{}",
client_ip, self.ecs_prefix_v4, self.ecs_prefix_v6
);
}
Err(e) => {
error!("Failed to add EDNS-client-subnet: {e}");
debug!("Continuing without EDNS-client-subnet");
}
}
} else {
debug!("EDNS-client-subnet is enabled but no client IP is available");
}
}
}
let random_tid: u16 = rand::rng().random();
let original_tid = dns_parser::tid(&query_data);
if let Err(e) = dns_parser::set_tid(&mut query_data, random_tid) {
error!("Failed to set transaction ID: {e}");
return Err(
DnsError::InvalidPacket(format!("Failed to set transaction ID: {e}")).into(),
);
}
debug!("Replaced query transaction ID {original_tid} with random ID {random_tid}");
let initial_timeout = std::cmp::max(1, self.server_timeout / 2);
debug!("Using initial timeout of {initial_timeout} seconds");
let mut buf = vec![0u8; self.dns_packet_len_max];
let start_time = std::time::Instant::now();
debug!("Sending query to upstream server (first attempt): {upstream_addr}");
upstream_socket
.send_to(&query_data, upstream_addr)
.await
.map_err(|e| {
DnsError::UpstreamError(format!(
"Failed to send query to upstream server {upstream_addr}: {e}"
))
})?;
let initial_timeout_duration = tokio::time::Duration::from_secs(initial_timeout);
let recv_future = resolver::recv_from_peer(&upstream_socket, &mut buf, upstream_addr);
match tokio::time::timeout(initial_timeout_duration, recv_future).await {
Ok(Ok(len)) => {
let response_time = start_time.elapsed();
debug!(
"Received response of size {len} bytes from upstream server (first attempt): {upstream_addr} in {response_time:?}"
);
let response = process_response(
&buf[..len],
random_tid,
upstream_addr,
self.spoof_protection,
&query_data,
)
.await;
if response.is_ok()
&& let Some(stats) = &self.stats
{
stats.record_success(upstream_addr, response_time).await;
}
response
}
Ok(Err(e)) => {
error!("Failed to receive response from upstream server {upstream_addr}: {e}");
if let Some(stats) = &self.stats {
stats.record_failure(upstream_addr).await;
}
Err(DnsError::UpstreamError(format!(
"Failed to receive response from upstream server {upstream_addr}: {e}"
))
.into())
}
Err(_) => {
debug!(
"Timeout waiting for response from upstream server after {initial_timeout} seconds (retrying): {upstream_addr}"
);
if let Some(stats) = &self.stats {
stats.record_timeout(upstream_addr).await;
}
debug!("Sending query to upstream server (retry attempt): {upstream_addr}");
upstream_socket
.send_to(&query_data, upstream_addr)
.await
.map_err(|e| {
DnsError::UpstreamError(format!(
"Failed to send retry query to upstream server {upstream_addr}: {e}"
))
})?;
let remaining_timeout = self.server_timeout - initial_timeout;
let remaining_timeout_duration =
tokio::time::Duration::from_secs(remaining_timeout);
let recv_future =
resolver::recv_from_peer(&upstream_socket, &mut buf, upstream_addr);
match tokio::time::timeout(remaining_timeout_duration, recv_future).await {
Ok(Ok(len)) => {
debug!(
"Received response of size {len} bytes from upstream server (retry attempt): {upstream_addr}"
);
let response = process_response(
&buf[..len],
random_tid,
upstream_addr,
self.spoof_protection,
&query_data,
)
.await;
if response.is_ok()
&& let Some(stats) = &self.stats
{
stats.clear_failures(upstream_addr).await;
}
response
}
Ok(Err(e)) => {
error!(
"Failed to receive response from upstream server on retry {upstream_addr}: {e}"
);
Err(DnsError::UpstreamError(format!(
"Failed to receive response from upstream server on retry {upstream_addr}: {e}"
))
.into())
}
Err(_) => {
debug!(
"Timeout waiting for response from upstream server on retry after {remaining_timeout} seconds: {upstream_addr}"
);
Err(DnsError::UpstreamTimeout.into())
}
}
}
}
}
async fn select_upstream_server(&self) -> EtchDnsResult<String> {
let upstream_servers = &self.upstream_servers;
if upstream_servers.is_empty() {
return Err(
DnsError::UpstreamError("No upstream servers configured".to_string()).into(),
);
}
load_balancer::select_upstream_server(
upstream_servers,
self.load_balancing_strategy,
self.stats.as_ref(),
)
.await
.map(|s| s.to_string()) .ok_or_else(|| {
EtchDnsError::DnsProcessingError(DnsError::UpstreamError(
"Failed to select upstream server".to_string(),
))
})
}
}
async fn process_response(
response_data: &[u8],
expected_tid: u16,
upstream_addr: SocketAddr,
spoof_protection: bool,
query_data: &[u8],
) -> EtchDnsResult<Vec<u8>> {
let response_tid = dns_parser::tid(response_data);
let should_fallback_to_tcp = if response_tid != expected_tid {
if spoof_protection {
warn!(
"Potential DNS spoofing detected over UDP from {}: expected transaction ID {}, got {}. Will retry with TCP.",
upstream_addr, expected_tid, response_tid
);
true } else {
debug!("Transaction ID mismatch: expected {expected_tid}, got {response_tid}");
return Err(
DnsError::UpstreamError("Transaction ID mismatch in response".to_string()).into(),
);
}
} else {
debug!("Verified response transaction ID: {response_tid}");
false
};
if !should_fallback_to_tcp {
if let Err(e) = dns_parser::validate_dns_response(response_data) {
debug!("Invalid DNS response: {e}");
return Err(DnsError::UpstreamError(format!("Invalid DNS response: {e}")).into());
}
if !dns_parser::questions_match(query_data, response_data)? {
return Err(
DnsError::UpstreamError("Question mismatch in UDP response".to_string()).into(),
);
}
if !dns_parser::edns_client_subnet_matches(query_data, response_data)? {
return Err(DnsError::UpstreamError("ECS mismatch in UDP response".to_string()).into());
}
}
if should_fallback_to_tcp || dns_parser::is_truncated(response_data) {
let reason = if should_fallback_to_tcp {
"potential spoofing detected"
} else {
"TC bit set"
};
debug!("Retrying with TCP ({reason})");
match retry_with_tcp(query_data, expected_tid, upstream_addr).await {
Ok(tcp_response) => {
debug!("Successfully received complete response via TCP");
return Ok(tcp_response);
}
Err(e) => {
if should_fallback_to_tcp {
warn!("Failed TCP retry after potential spoofing detected: {e}");
return Err(e);
}
warn!(
"Failed to get complete response via TCP: {e}, returning truncated UDP response"
);
}
}
}
Ok(response_data.to_vec())
}
async fn retry_with_tcp(
query_data: &[u8],
expected_tid: u16,
upstream_addr: SocketAddr,
) -> EtchDnsResult<Vec<u8>> {
let tcp_query_data = query_data.to_vec();
debug!("Connecting to upstream DNS server via TCP: {upstream_addr}");
let mut tcp_stream = match tokio::net::TcpStream::connect(upstream_addr).await {
Ok(stream) => stream,
Err(e) => {
return Err(DnsError::UpstreamError(format!(
"Failed to connect to upstream server via TCP: {e}"
))
.into());
}
};
let query_len = tcp_query_data.len() as u16;
let mut tcp_query = Vec::with_capacity(tcp_query_data.len() + 2);
tcp_query.push((query_len >> 8) as u8);
tcp_query.push(query_len as u8);
tcp_query.extend_from_slice(&tcp_query_data);
debug!("Sending DNS query via TCP to {upstream_addr}");
if let Err(e) = tcp_stream.write_all(&tcp_query).await {
return Err(
DnsError::UpstreamError(format!("Failed to send DNS query via TCP: {e}")).into(),
);
}
let mut length_buf = [0u8; 2];
if let Err(e) = tcp_stream.read_exact(&mut length_buf).await {
return Err(DnsError::UpstreamError(format!(
"Failed to read DNS response length via TCP: {e}"
))
.into());
}
let response_len = ((length_buf[0] as usize) << 8) | (length_buf[1] as usize);
if !(dns_parser::DNS_PACKET_LEN_MIN..=dns_parser::DNS_MAX_PACKET_SIZE).contains(&response_len) {
return Err(DnsError::UpstreamError(format!(
"Invalid DNS response length via TCP: {response_len}"
))
.into());
}
let mut response_buf = vec![0u8; response_len];
let mut total_bytes_read = 0;
let min_read_size = 512;
while total_bytes_read < response_len {
let bytes_remaining = response_len - total_bytes_read;
let read_result = tcp_stream.read(&mut response_buf[total_bytes_read..]).await;
match read_result {
Ok(bytes_read) => {
if bytes_read == 0 {
return Err(DnsError::UpstreamError(format!(
"TCP connection closed unexpectedly after reading {total_bytes_read} of {response_len} bytes"
))
.into());
}
if bytes_read < min_read_size && bytes_remaining > min_read_size {
debug!(
"Small TCP read detected ({bytes_read} bytes). This could be inefficient."
);
}
total_bytes_read += bytes_read;
debug!("Read {bytes_read} bytes, total so far: {total_bytes_read}/{response_len}");
}
Err(e) => {
return Err(DnsError::UpstreamError(format!(
"Failed to read DNS response via TCP after {total_bytes_read} of {response_len} bytes: {e}"
))
.into());
}
}
}
let response_tid = dns_parser::tid(&response_buf);
if response_tid != expected_tid {
debug!("TCP response transaction ID mismatch: expected {expected_tid}, got {response_tid}");
return Err(
DnsError::UpstreamError("Transaction ID mismatch in TCP response".to_string()).into(),
);
}
if let Err(e) = dns_parser::validate_dns_response(&response_buf) {
debug!("Invalid DNS TCP response: {e}");
return Err(DnsError::UpstreamError(format!("Invalid DNS TCP response: {e}")).into());
}
if !dns_parser::questions_match(&tcp_query_data, &response_buf)? {
return Err(
DnsError::UpstreamError("Question mismatch in TCP response".to_string()).into(),
);
}
if !dns_parser::edns_client_subnet_matches(&tcp_query_data, &response_buf)? {
return Err(DnsError::UpstreamError("ECS mismatch in TCP response".to_string()).into());
}
debug!("Successfully received DNS response via TCP, size: {response_len}");
Ok(response_buf)
}
async fn bind_tcp_listener(
addr: SocketAddr,
service: &str,
) -> EtchDnsResult<tokio::net::TcpListener> {
tokio::net::TcpListener::bind(addr).await.map_err(|e| {
EtchDnsError::SocketBindError(std::io::Error::new(
e.kind(),
format!("Failed to bind {service} listener to {addr}: {e}"),
))
})
}
fn spawn_in_slot(
slots: &ClientSlots,
addr: SocketAddr,
task: impl Future<Output = ()> + Send + 'static,
) -> bool {
let (cancel_tx, cancel_rx) = tokio::sync::oneshot::channel();
let Some(slot) = ConnectionSlots::register(slots, addr.ip(), cancel_tx) else {
return false;
};
tokio::spawn(async move {
let _slot = slot;
tokio::select! {
_ = task => {}
_ = cancel_rx => debug!("Client {addr} was replaced by a newer client"),
}
});
true
}
#[async_trait]
trait Client {
async fn process_query(&self);
}
struct UdpResponseSender {
socket: Arc<UdpSocket>,
#[cfg(target_os = "openbsd")]
send_lock: Mutex<()>,
}
impl UdpResponseSender {
fn new(socket: Arc<UdpSocket>) -> Self {
Self {
socket,
#[cfg(target_os = "openbsd")]
send_lock: Mutex::new(()),
}
}
async fn send_to(&self, data: &[u8], addr: SocketAddr) -> std::io::Result<usize> {
#[cfg(target_os = "openbsd")]
let _guard = self.send_lock.lock().await;
self.socket.send_to(data, addr).await
}
}
#[derive(Clone)]
struct UDPClient {
sender: Arc<UdpResponseSender>,
addr: SocketAddr,
query: ClientQuery,
query_manager: Arc<QueryManager>,
}
#[derive(Clone)]
struct TCPClient {
addr: SocketAddr,
query: ClientQuery,
query_manager: Arc<QueryManager>,
stream: Arc<Mutex<TcpStream>>,
}
impl UDPClient {
fn new(
sender: Arc<UdpResponseSender>,
data: Vec<u8>,
addr: SocketAddr,
upstream_servers: Vec<String>,
query_manager: Arc<QueryManager>,
) -> Self {
let server_timeout = query_manager.get_server_timeout();
let dns_packet_len_max = query_manager.get_dns_packet_len_max();
let stats = query_manager
.get_stats()
.expect("QueryManager should have stats");
let load_balancing_strategy = query_manager.get_load_balancing_strategy();
let enable_ecs = query_manager.get_enable_ecs();
let ecs_prefix_v4 = query_manager.get_ecs_prefix_v4();
let ecs_prefix_v6 = query_manager.get_ecs_prefix_v6();
let spoof_protection = query_manager.get_spoof_protection();
let client_ip = addr.ip().to_string();
let query = ClientQuery::new_with_client_ip(
data,
upstream_servers,
server_timeout,
dns_packet_len_max,
stats,
load_balancing_strategy,
client_ip,
enable_ecs,
ecs_prefix_v4,
ecs_prefix_v6,
spoof_protection,
);
Self {
sender,
addr,
query,
query_manager,
}
}
}
impl TCPClient {
fn new(
data: Vec<u8>,
addr: SocketAddr,
upstream_servers: Vec<String>,
query_manager: Arc<QueryManager>,
stream: Arc<Mutex<TcpStream>>,
) -> Self {
let server_timeout = query_manager.get_server_timeout();
let dns_packet_len_max = query_manager.get_dns_packet_len_max();
let stats = query_manager
.get_stats()
.expect("QueryManager should have stats");
let load_balancing_strategy = query_manager.get_load_balancing_strategy();
let enable_ecs = query_manager.get_enable_ecs();
let ecs_prefix_v4 = query_manager.get_ecs_prefix_v4();
let ecs_prefix_v6 = query_manager.get_ecs_prefix_v6();
let spoof_protection = query_manager.get_spoof_protection();
let client_ip = addr.ip().to_string();
let query = ClientQuery::new_with_client_ip(
data,
upstream_servers,
server_timeout,
dns_packet_len_max,
stats,
load_balancing_strategy,
client_ip,
enable_ecs,
ecs_prefix_v4,
ecs_prefix_v6,
spoof_protection,
);
Self {
addr,
query,
query_manager,
stream,
}
}
}
async fn process_tcp_connection(
stream: TcpStream,
addr: SocketAddr,
upstream_servers: Vec<String>,
query_manager: Arc<QueryManager>,
dns_packet_len_max: usize,
server_timeout: u64,
rate_limiter: Option<Arc<rate_limiter::RateLimiter>>,
) {
let stream_arc = Arc::new(Mutex::new(stream));
let mut len_buf = [0u8; 2];
let timeout_duration = Duration::from_secs(server_timeout);
loop {
let read_result = tokio::time::timeout(timeout_duration, async {
let mut stream = stream_arc.lock().await;
stream.read_exact(&mut len_buf).await
})
.await;
match read_result {
Ok(read_exact_result) => {
match read_exact_result {
Ok(_) => {
let len = ((len_buf[0] as u16) << 8) | (len_buf[1] as u16);
debug!("TCP client {addr} sent a query of length {len}");
if len as usize > dns_packet_len_max {
debug!(
"TCP client {addr} sent a query with length {len} which exceeds the maximum of {dns_packet_len_max}"
);
break;
}
let query_read_result = tokio::time::timeout(timeout_duration, async {
let mut stream = stream_arc.lock().await;
let mut query_buf = vec![0u8; len as usize];
let mut total_bytes_read = 0;
let min_read_size = 512;
while total_bytes_read < len as usize {
let bytes_remaining = len as usize - total_bytes_read;
match stream.read(&mut query_buf[total_bytes_read..]).await {
Ok(bytes_read) => {
if bytes_read == 0 {
return (Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
format!("TCP connection closed unexpectedly after reading {total_bytes_read} of {len} bytes")
)), query_buf);
}
if bytes_read < min_read_size && bytes_remaining > min_read_size {
debug!(
"Small TCP read detected from client {addr} ({bytes_read} bytes). This could be inefficient."
);
}
total_bytes_read += bytes_read;
debug!("Read {bytes_read} bytes from client {addr}, total so far: {total_bytes_read}/{len}");
},
Err(e) => {
return (Err(e), query_buf);
}
}
}
(Ok(()), query_buf)
})
.await;
match query_read_result {
Ok((read_result, query_buf)) => {
match read_result {
Ok(_) => {
debug!("Read {len} bytes from TCP client {addr}");
if let Some(limiter) = &rate_limiter
&& !limiter.is_allowed(addr.ip()).await
{
warn!(
"Rate limit exceeded for TCP client {addr}, closing connection"
);
break;
}
let client = TCPClient::new(
query_buf,
addr,
upstream_servers.clone(),
query_manager.clone(),
stream_arc.clone(),
);
client.process_query().await;
debug!("Completed processing task for TCP client {addr}");
}
Err(e) if net::is_peer_gone(&e) => {
debug!(
"TCP client {addr} went away while sending a query: {e}"
);
break;
}
Err(e) => {
error!("Failed to read query from TCP client {addr}: {e}");
break;
}
}
}
Err(_) => {
debug!(
"TCP connection from {addr} timed out after {server_timeout} seconds"
);
break;
}
}
}
Err(e) => {
if net::is_peer_gone(&e) {
debug!("TCP client {addr} closed the connection: {e}");
} else {
error!("Failed to read length from TCP client {addr}: {e}");
}
break;
}
}
}
Err(_) => {
debug!("TCP connection from {addr} timed out after {server_timeout} seconds");
break;
}
}
}
debug!("TCP connection handler for client {addr} exiting");
}
impl DnsQueryProcessor for TCPClient {}
impl DnsQueryProcessor for UDPClient {}
#[async_trait]
impl Client for TCPClient {
async fn process_query(&self) {
let client_addr = self.addr.to_string();
let query_future = self.process_dns_query(
&self.query.data,
&client_addr,
"TCP",
&self.query_manager,
&self.query.upstream_servers,
self.query.server_timeout,
self.query.dns_packet_len_max,
self.query.stats.clone(),
self.query.load_balancing_strategy,
);
let timeout_duration = std::time::Duration::from_secs(self.query.server_timeout * 2);
let response_opt = match tokio::time::timeout(timeout_duration, query_future).await {
Ok(response) => response,
Err(_) => {
debug!(
"TCP query for client {} timed out after {} seconds",
self.addr,
self.query.server_timeout * 2
);
None
}
};
if let Some((response_data, _)) = response_opt {
let response_len = response_data.len() as u16;
let mut tcp_response = Vec::with_capacity(response_data.len() + 2);
tcp_response.push((response_len >> 8) as u8);
tcp_response.push(response_len as u8);
tcp_response.extend_from_slice(&response_data);
let mut stream = self.stream.lock().await;
let write_timeout = Duration::from_secs(self.query.server_timeout);
match tokio::time::timeout(write_timeout, stream.write_all(&tcp_response)).await {
Ok(Ok(_)) => {
debug!(
"Sent {} bytes back to TCP client {}",
response_data.len(),
self.addr
);
debug!("Response successfully sent to client {}", self.addr);
}
Ok(Err(e)) if net::is_peer_gone(&e) => {
debug!(
"TCP client {} went away before getting its response: {e}",
self.addr
);
}
Ok(Err(e)) => {
error!("Failed to send response to TCP client {}: {}", self.addr, e);
debug!("Error details: {e:?}");
}
Err(_) => {
debug!("TCP client {} stopped reading its responses", self.addr);
let _ = stream.shutdown().await;
}
}
}
}
}
#[async_trait]
impl Client for UDPClient {
async fn process_query(&self) {
let client_addr = self.addr.to_string();
let query_future = self.process_dns_query(
&self.query.data,
&client_addr,
"UDP",
&self.query_manager,
&self.query.upstream_servers,
self.query.server_timeout,
self.query.dns_packet_len_max,
self.query.stats.clone(),
self.query.load_balancing_strategy,
);
let timeout_duration = std::time::Duration::from_secs(self.query.server_timeout * 2);
let response_opt = match tokio::time::timeout(timeout_duration, query_future).await {
Ok(response) => response,
Err(_) => {
debug!(
"UDP query for client {} timed out after {} seconds",
self.addr,
self.query.server_timeout * 2
);
None
}
};
if let Some((mut response_data, _)) = response_opt {
if response_data.len() > self.query.max_udp_response_size {
debug!(
"Response size ({} bytes) exceeds maximum UDP response size ({} bytes), truncating",
response_data.len(),
self.query.max_udp_response_size
);
match dns_parser::truncate_dns_packet(
&response_data,
Some(self.query.max_udp_response_size),
false,
) {
Ok(truncated_data) => {
debug!(
"Truncated response from {} bytes to {} bytes",
response_data.len(),
truncated_data.len()
);
response_data = truncated_data;
}
Err(e) => {
error!("Failed to truncate response: {e}");
debug!("Error details: {e:?}");
}
}
}
if let Err(e) = self.sender.send_to(&response_data, self.addr).await {
error!("Failed to send response to UDP client {}: {}", self.addr, e);
}
}
}
}
#[tokio::main]
async fn main() -> EtchDnsResult<()> {
let args = Args::parse();
let config = Config::from_file(&args.config)?;
let log_level = config.log_level.to_lowercase();
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(&log_level)).init();
debug!("Command line arguments: {args:?}");
debug!("Loaded configuration: {config:?}");
info!("Starting EtchDNS server");
info!("Log level: {}", config.log_level);
for addr in &config.listen_addresses {
info!("Listening on: {addr} (UDP/TCP)");
}
if !config.doh_listen_addresses.is_empty() {
for addr in &config.doh_listen_addresses {
info!("Listening on: {addr} (DoH)");
}
} else {
info!("DoH server is disabled");
}
if let Some(addr) = &config.metrics_address {
info!(
"Metrics server enabled on: {}, path: {}",
addr, config.metrics_path
);
} else {
info!("Metrics server is disabled");
}
info!("DNS cache size: {} entries", config.cache_size);
info!(
"Load balancing strategy: {}",
config.load_balancing_strategy
);
if config.spoof_protection {
info!("DNS spoofing protection: ENABLED (TCP fallback on UDP transaction ID mismatch)");
} else {
info!("DNS spoofing protection: DISABLED");
}
if let Some(file) = &config.allowed_zones_file {
info!("Domain filtering enabled with allowed zones file: {file}");
}
if let Some(file) = &config.nx_zones_file {
info!("NX domains filtering enabled with file: {file}");
}
if config.udp_rate_limit_window > 0 {
info!(
"UDP rate limiting: {} queries per {} seconds",
config.udp_rate_limit_count, config.udp_rate_limit_window
);
} else {
info!("UDP rate limiting is disabled");
}
if config.tcp_rate_limit_window > 0 {
info!(
"TCP rate limiting: {} queries per {} seconds",
config.tcp_rate_limit_count, config.tcp_rate_limit_window
);
} else {
info!("TCP rate limiting is disabled");
}
if !config.doh_listen_addresses.is_empty() && config.doh_rate_limit_window > 0 {
info!(
"DoH rate limiting: {} queries per {} seconds",
config.doh_rate_limit_count, config.doh_rate_limit_window
);
}
let socket_addrs = Arc::new(config.socket_addrs()?);
let mut udp_sockets = Vec::new();
let mut tcp_listeners = Vec::new();
for socket_addr in socket_addrs.iter() {
let udp_socket = UdpSocket::bind(socket_addr).await.map_err(|e| {
EtchDnsError::SocketBindError(std::io::Error::new(
e.kind(),
format!("Failed to bind UDP socket to {}: {}", socket_addr, e),
))
})?;
info!(
"Listening on UDP: {}",
udp_socket.local_addr().map_err(|e| {
EtchDnsError::SocketBindError(std::io::Error::new(
e.kind(),
format!(
"Failed to get local address for UDP socket bound to {}: {}",
socket_addr, e
),
))
})?
);
let upstream_socket_fd = udp_socket.as_raw_fd();
let filter = bpfprog!(8,72 0 0 4,53 0 5 17,72 0 0 12,21 0 3 1,72 0 0 18,37 1 0 1,6 0 0 262144,6 0 0 0);
bpf::attach_filter(upstream_socket_fd, filter).ok();
udp_socket.set_tos_v4(0x10).ok();
udp_sockets.push(Arc::new(udp_socket));
let tcp_listener = bind_tcp_listener(*socket_addr, "TCP").await?;
info!(
"Listening on TCP: {}",
tcp_listener.local_addr().map_err(|e| {
EtchDnsError::SocketBindError(std::io::Error::new(
e.kind(),
format!(
"Failed to get local address for TCP listener bound to {}: {}",
socket_addr, e
),
))
})?
);
tcp_listeners.push(Arc::new(tcp_listener));
}
debug!(
"Using buffer capacity of {} bytes for each socket",
config.dns_packet_len_max
);
debug!(
"Using a maximum of {} UDP clients and {} TCP clients",
config.max_udp_clients, config.max_tcp_clients
);
let udp_clients: ClientSlots = Arc::new(ConnectionSlots::new(config.max_udp_clients).into());
let tcp_clients: ClientSlots = Arc::new(ConnectionSlots::new(config.max_tcp_clients).into());
let global_stats = Arc::new(SharedStats::new());
debug!("Created global statistics tracker");
let mut hooks = Arc::new(hooks::SharedHooks::new());
debug!("Created hooks structure");
let mut validator = IpValidator::new();
for cidr in &config.blocked_ip_ranges {
match IpRange::from_cidr(cidr) {
Ok(range) => {
validator = validator.add_blocked_range(range);
}
Err(e) => {
warn!("Invalid IP range format {cidr}: {e}");
}
}
}
validator = validator.allow_ports(config.min_client_port, 65535);
if !config.block_private_ips {
validator = validator.allow_private();
}
if !config.block_loopback_ips {
validator = validator.allow_loopback();
}
let ip_validator = Arc::new(validator);
debug!(
"Created IP validator with {} blocked ranges",
config.blocked_ip_ranges.len()
);
let query_logger = Arc::new(query_logger::QueryLogger::new(
config.query_log_file.clone(),
config.query_log_include_timestamp,
config.query_log_include_client_addr,
config.query_log_include_query_type,
config.query_log_include_query_class,
config.query_log_rotation_size,
config.query_log_rotation_interval.clone(),
config.query_log_rotation_count,
config.query_log_compression,
));
if let Some(log_file) = &config.query_log_file {
info!("Query logging enabled to file: {log_file}");
}
if let Some(wasm_file) = &config.hooks_wasm_file {
info!("Loading WebAssembly hooks from file: {wasm_file}");
#[cfg(feature = "hooks")]
{
info!(
"WebAssembly WASI support is {}",
if config.hooks_wasm_wasi {
"enabled"
} else {
"disabled"
}
);
}
match hooks::Hooks::with_wasm_file(wasm_file, config.hooks_wasm_wasi) {
Ok(wasm_hooks) => {
hooks = Arc::new(hooks::SharedHooks::with_hooks(wasm_hooks));
debug!("Successfully loaded WebAssembly hooks");
}
Err(e) => {
error!("Failed to load WebAssembly hooks: {e}");
}
}
}
let cache_capacity = config.cache_size;
let dns_cache = Arc::new(cache::create_dns_cache(cache_capacity));
debug!("Created DNS cache with capacity of {cache_capacity} entries");
let udp_rate_limiter = if config.udp_rate_limit_window > 0 {
let limiter = Arc::new(rate_limiter::RateLimiter::new(
config.udp_rate_limit_window,
config.udp_rate_limit_count,
config.udp_rate_limit_max_clients,
));
debug!(
"Created UDP rate limiter with window of {} seconds, limit of {} queries per client IP, and max of {} client IPs",
config.udp_rate_limit_window,
config.udp_rate_limit_count,
config.udp_rate_limit_max_clients
);
Some(limiter)
} else {
debug!("UDP rate limiting is disabled");
None
};
let tcp_rate_limiter = if config.tcp_rate_limit_window > 0 {
let limiter = Arc::new(rate_limiter::RateLimiter::new(
config.tcp_rate_limit_window,
config.tcp_rate_limit_count,
config.tcp_rate_limit_max_clients,
));
debug!(
"Created TCP rate limiter with window of {} seconds, limit of {} queries per client IP, and max of {} client IPs",
config.tcp_rate_limit_window,
config.tcp_rate_limit_count,
config.tcp_rate_limit_max_clients
);
Some(limiter)
} else {
debug!("TCP rate limiting is disabled");
None
};
let doh_rate_limiter = if config.doh_rate_limit_window > 0 {
let limiter = Arc::new(rate_limiter::RateLimiter::new(
config.doh_rate_limit_window,
config.doh_rate_limit_count,
config.doh_rate_limit_max_clients,
));
debug!(
"Created DoH rate limiter with window of {} seconds, limit of {} queries per client IP, and max of {} client IPs",
config.doh_rate_limit_window,
config.doh_rate_limit_count,
config.doh_rate_limit_max_clients
);
Some(limiter)
} else {
debug!("DoH rate limiting is disabled");
None
};
let max_inflight_queries = config.max_inflight_queries;
let server_timeout = config.server_timeout;
let dns_packet_len_max = config.dns_packet_len_max;
let load_balancing_strategy = config
.load_balancing_strategy
.parse::<load_balancer::LoadBalancingStrategy>()
.unwrap_or_else(|_| {
warn!(
"Invalid load balancing strategy: {}, using fastest",
config.load_balancing_strategy
);
load_balancer::LoadBalancingStrategy::Fastest
});
let mut query_manager = QueryManager::new(
max_inflight_queries,
server_timeout,
dns_packet_len_max,
global_stats.clone(),
load_balancing_strategy,
config.authoritative_dns,
config.serve_stale_grace_time,
config.serve_stale_ttl,
config.negative_cache_ttl,
config.min_cache_ttl,
config.enable_ecs,
config.ecs_prefix_v4,
config.ecs_prefix_v6,
config.spoof_protection,
);
query_manager.set_cache((*dns_cache).clone());
query_manager.set_hooks(hooks.clone());
query_manager.set_query_logger(query_logger.clone());
if let Some(allowed_zones_file) = &config.allowed_zones_file {
match allowed_zones::AllowedZones::load_from_file(allowed_zones_file) {
Ok(zones) => {
let zone_count = zones.len();
query_manager.set_allowed_zones(zones);
info!("Loaded {zone_count} allowed zones from {allowed_zones_file}");
}
Err(e) => {
error!("Failed to load allowed zones from {allowed_zones_file}: {e}");
return Err(EtchDnsError::ConfigParseError(format!(
"Failed to load allowed zones file: {e}"
)));
}
}
}
if let Some(nx_zones_file) = &config.nx_zones_file {
match nx_zones::NxZones::load_from_file(nx_zones_file) {
Ok(zones) => {
let zone_count = zones.len();
query_manager.set_nx_zones(zones);
info!("Loaded {zone_count} nonexistent zones from {nx_zones_file}");
}
Err(e) => {
error!("Failed to load nonexistent zones from {nx_zones_file}: {e}");
return Err(EtchDnsError::ConfigParseError(format!(
"Failed to load nonexistent zones file: {e}"
)));
}
}
}
let query_manager = Arc::new(query_manager);
debug!(
"Created query manager with a maximum of {max_inflight_queries} in-flight queries, {server_timeout} second timeout, {dns_packet_len_max} byte packet size, and '{load_balancing_strategy}' load balancing strategy"
);
let metrics_listener = match &config.metrics_address {
Some(metrics_addr) => {
let metrics_addr = metrics_addr.parse::<SocketAddr>().map_err(|e| {
EtchDnsError::Other(format!("Invalid metrics address {metrics_addr}: {e}"))
})?;
Some(bind_tcp_listener(metrics_addr, "metrics").await?)
}
None => None,
};
let mut doh_listeners = Vec::new();
for doh_addr in config.doh_socket_addrs()? {
doh_listeners.push(bind_tcp_listener(doh_addr, "DoH").await?);
}
let mut control_listeners = Vec::new();
for control_addr in config.control_socket_addrs()? {
control_listeners.push(bind_tcp_listener(control_addr, "control").await?);
}
if let Some(username) = &config.user {
info!("Dropping privileges to user: {username}");
let pd = privdrop::PrivDrop::default();
let mut pd = pd.user(username);
if let Some(group) = &config.group {
info!("Using group: {group}");
pd = pd.group(group);
}
if let Some(chroot_dir) = &config.chroot {
info!("Using chroot directory: {chroot_dir}");
pd = pd.chroot(chroot_dir);
}
match pd.apply() {
Ok(_) => {
info!("Successfully dropped privileges");
}
Err(e) => {
let error_msg = format!("Failed to drop privileges: {e}");
error!("{error_msg}");
return Err(EtchDnsError::PrivilegeDropError(error_msg));
}
}
} else {
warn!(
"Running with full privileges. Consider setting 'user' in config.toml for better security."
);
}
let mut tasks = Vec::new();
if config.probe_interval > 0 {
info!(
"Starting server prober with interval of {} seconds and timeout of {} seconds",
config.probe_interval, config.server_timeout
);
let upstream_servers = config.upstream_servers.clone();
let stats = global_stats.clone();
let probe_interval = config.probe_interval;
let server_timeout = config.server_timeout;
let prober_task = tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(probe_interval));
loop {
interval.tick().await;
debug!("Probing all upstream DNS servers");
for server in &upstream_servers {
match server.parse::<SocketAddr>() {
Ok(addr) => {
match probe_server(addr, server_timeout).await {
Ok(response_time) => {
debug!("Probe to {addr} completed in {response_time:.2?}");
stats.record_success(addr, response_time).await;
}
Err(e) => {
warn!("Probe to {addr} failed: {e}");
stats.record_failure(addr).await;
}
}
}
Err(e) => {
error!("Failed to parse server address {server}: {e}");
}
}
}
}
});
tasks.push(prober_task);
}
if let Some(metrics_listener) = metrics_listener {
let metrics_path = config.metrics_path.clone();
let stats = global_stats.clone();
let max_metrics_connections = config.max_metrics_connections;
let metrics_query_manager = query_manager.clone();
let metrics_udp_clients = udp_clients.clone();
let metrics_tcp_clients = tcp_clients.clone();
let metrics_dns_cache = dns_cache.clone();
let metrics_task = tokio::spawn(async move {
if let Err(e) = metrics::start_metrics_server(
metrics_listener,
metrics_path,
stats,
max_metrics_connections,
Some(metrics_query_manager),
Some(metrics_udp_clients),
Some(metrics_tcp_clients),
Some(metrics_dns_cache),
)
.await
{
error!("Metrics server error: {e}");
}
});
tasks.push(metrics_task);
}
for doh_listener in doh_listeners {
let query_manager = query_manager.clone();
let upstream_servers = config.upstream_servers.clone();
let server_timeout = config.server_timeout;
let dns_packet_len_max = config.dns_packet_len_max;
let stats = global_stats.clone();
let max_connections = config.max_doh_connections;
let doh_rate_limiter = doh_rate_limiter.clone();
let load_balancing_strategy = config
.load_balancing_strategy
.parse::<load_balancer::LoadBalancingStrategy>()
.unwrap_or_else(|_| {
warn!(
"Invalid load balancing strategy: {}, using fastest",
config.load_balancing_strategy
);
load_balancer::LoadBalancingStrategy::Fastest
});
let doh_ip_validator = ip_validator.clone();
let doh_task = tokio::spawn(async move {
if let Err(e) = doh::start_doh_server(
doh_listener,
query_manager,
upstream_servers,
server_timeout,
dns_packet_len_max,
stats,
max_connections,
doh_rate_limiter,
load_balancing_strategy,
Some(doh_ip_validator),
config.enable_strict_ip_validation,
)
.await
{
error!("DoH server error: {e}");
}
});
tasks.push(doh_task);
}
for control_listener in control_listeners {
let control_path = config.control_path.clone();
let max_connections = config.max_control_connections;
let dns_cache = Some(dns_cache.clone());
let stats = Some(global_stats.clone());
let control_task = tokio::spawn(async move {
if let Err(e) = control::start_control_server(
control_listener,
control_path,
max_connections,
dns_cache,
stats,
)
.await
{
error!("Control server error: {e}");
}
});
tasks.push(control_task);
}
let upstream_servers = config.upstream_servers.clone();
let dns_packet_len_max = config.dns_packet_len_max;
for (i, socket) in udp_sockets.iter().enumerate() {
let socket = socket.clone();
let response_sender = Arc::new(UdpResponseSender::new(socket.clone()));
let all_socket_addrs = socket_addrs.clone();
let socket_addr = (*all_socket_addrs)[i];
let udp_clients = udp_clients.clone();
let upstream_servers = upstream_servers.clone();
let query_manager = query_manager.clone();
let udp_rate_limiter = udp_rate_limiter.clone();
let task_ip_validator = ip_validator.clone();
let stats = global_stats.clone();
let task = tokio::spawn(async move {
let mut buf = vec![0u8; dns_packet_len_max];
loop {
debug!("Waiting for incoming UDP packets on {socket_addr}...");
match socket.recv_from(&mut buf).await {
Ok((len, addr)) => {
debug!("Received packet of size {len} bytes from UDP client {addr}");
if config.enable_strict_ip_validation {
match task_ip_validator.validate_ip(addr.ip()) {
Ok(_) => {
if let Err(e) = task_ip_validator.validate_port(addr.port()) {
warn!("Disallowing query from UDP client {addr}: {e}");
continue;
}
}
Err(e) => {
warn!("Disallowing query from UDP client {addr}: {e}");
continue;
}
}
} else {
if addr.port() < 1024 {
warn!(
"Disallowing query from UDP client {addr} with reserved port {}, dropping query",
addr.port()
);
continue;
}
}
if let Some(rate_limiter) = &udp_rate_limiter {
let client_ip = addr.ip();
if !rate_limiter.is_allowed(client_ip).await {
warn!("Rate limit exceeded for UDP client {addr}, dropping query");
continue;
}
}
let data = buf[..len].to_vec();
debug!("Cloned packet data for processing");
let response_sender = response_sender.clone();
debug!("Spawning new task to handle UDP client {addr}");
let client = UDPClient::new(
response_sender,
data,
addr,
upstream_servers.clone(),
query_manager.clone(),
);
let task = async move {
debug!("Started processing task for UDP client {addr}");
client.process_query().await;
debug!("Completed processing task for UDP client {addr}");
};
if !spawn_in_slot(&udp_clients, addr, task) {
warn!("Maximum UDP client count reached, dropping query from {addr}");
}
}
Err(e) => {
error!("Failed to receive packet on UDP {socket_addr}: {e}");
tokio::time::sleep(Duration::from_millis(10)).await;
stats.increment_udp_receive_errors().await;
}
}
}
});
tasks.push(task);
}
let tcp_socket_addrs = socket_addrs.clone();
for (i, listener) in tcp_listeners.iter().enumerate() {
let listener = listener.clone();
let all_socket_addrs = tcp_socket_addrs.clone();
let socket_addr = (*all_socket_addrs)[i];
let tcp_clients = tcp_clients.clone();
let upstream_servers = upstream_servers.clone();
let query_manager = query_manager.clone();
let tcp_rate_limiter = tcp_rate_limiter.clone();
let task_ip_validator = ip_validator.clone();
let stats = global_stats.clone();
let task = tokio::spawn(async move {
loop {
debug!("Waiting for incoming TCP connections on {socket_addr}...");
match listener.accept().await {
Ok((stream, addr)) => {
debug!("Accepted TCP connection from {addr}");
if config.enable_strict_ip_validation {
match task_ip_validator.validate_ip(addr.ip()) {
Ok(_) => {
if let Err(e) = task_ip_validator.validate_port(addr.port()) {
warn!("Disallowing connection from TCP client {addr}: {e}");
continue;
}
}
Err(e) => {
warn!("Disallowing connection from TCP client {addr}: {e}");
continue;
}
}
} else {
if addr.port() < 1024 {
warn!(
"Disallowing connection from TCP client {addr} with reserved port {}, dropping connection",
addr.port()
);
continue;
}
}
let upstream_servers = upstream_servers.clone();
let query_manager = query_manager.clone();
let tcp_rate_limiter = tcp_rate_limiter.clone();
let task = async move {
debug!("Started TCP connection handler for client {addr}");
process_tcp_connection(
stream,
addr,
upstream_servers,
query_manager,
dns_packet_len_max,
server_timeout,
tcp_rate_limiter,
)
.await;
debug!("Completed TCP connection handler for client {addr}");
};
if !spawn_in_slot(&tcp_clients, addr, task) {
warn!(
"Maximum TCP client count reached, dropping connection from {addr}"
);
}
}
Err(e) => {
net::handle_accept_error(&e, "DNS TCP").await;
stats.increment_tcp_accept_errors().await;
}
}
}
});
tasks.push(task);
}
if tasks.is_empty() {
return Err(EtchDnsError::Other(
"No server or probe tasks were configured".to_string(),
));
}
let (result, _, _) = futures::future::select_all(tasks).await;
match result {
Ok(()) => Err(EtchDnsError::Other(
"A server task exited unexpectedly".to_string(),
)),
Err(e) => {
error!("Task error: {e}");
Err(EtchDnsError::Other(format!("Task error: {e}")))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
#[cfg(target_os = "openbsd")]
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn concurrent_udp_responses_keep_sending() {
let socket = Arc::new(UdpSocket::bind("127.0.0.1:0").await.unwrap());
let sender = Arc::new(UdpResponseSender::new(socket));
let receiver = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let destination = receiver.local_addr().unwrap();
let drain = tokio::spawn(async move {
let mut packet = [0; 4096];
loop {
receiver.recv_from(&mut packet).await.unwrap();
}
});
let barrier = Arc::new(tokio::sync::Barrier::new(8));
let mut sends = tokio::task::JoinSet::new();
for _ in 0..8 {
let sender = sender.clone();
let barrier = barrier.clone();
sends.spawn(async move {
barrier.wait().await;
for _ in 0..10_000 {
assert_eq!(sender.send_to(&[0; 4096], destination).await.unwrap(), 4096);
}
});
}
let result = tokio::time::timeout(Duration::from_secs(60), async {
while let Some(result) = sends.join_next().await {
result.unwrap();
}
})
.await;
sends.abort_all();
drain.abort();
let _ = drain.await;
result.expect("concurrent UDP replies stopped making progress");
}
fn query() -> Vec<u8> {
vec![
0x12, 0x34, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, b'e',
b'x', b'a', b'm', b'p', b'l', b'e', 0x03, b'c', b'o', b'm', 0x00, 0x00, 0x01, 0x00,
0x01,
]
}
#[test]
fn test_udp_port_validation() {
let valid_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 2053);
let invalid_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 53);
assert!(valid_addr.port() >= 1024);
assert!(invalid_addr.port() < 1024);
}
#[test]
fn test_example_configuration_is_valid() {
let config: Config = toml::from_str(include_str!("../etchdns.toml")).unwrap();
config.validate().unwrap();
}
#[test]
fn test_invalid_configuration_is_rejected() {
assert!(toml::from_str::<Config>("unknown_setting = true").is_err());
let mut config: Config = toml::from_str("").unwrap();
config.max_udp_clients = 0;
assert!(config.validate().is_err());
let mut config: Config = toml::from_str("").unwrap();
config.upstream_servers = vec!["not an address".to_string()];
assert!(config.validate().is_err());
let mut config: Config = toml::from_str("").unwrap();
config.blocked_ip_ranges = vec!["not a network".to_string()];
assert!(config.validate().is_err());
}
#[test]
fn test_configured_packet_limit_cannot_exceed_parser_limit() {
let mut config: Config = toml::from_str("").unwrap();
config.dns_packet_len_max = dns_parser::DNS_MAX_PACKET_SIZE;
assert!(config.validate().is_ok());
config.dns_packet_len_max += 1;
assert!(config.validate().is_err());
}
#[test]
fn test_small_edns_udp_size_is_treated_as_512() {
let mut packet = query();
dns_parser::add_edns_section(&mut packet, 1).unwrap();
let query = ClientQuery::new(
packet,
vec!["127.0.0.1:53".to_string()],
1,
4096,
Arc::new(SharedStats::new()),
load_balancer::LoadBalancingStrategy::Random,
);
assert_eq!(query.max_udp_response_size, 512);
}
#[tokio::test]
async fn test_upstream_response_question_must_match() {
let query = query();
let mut response = query.clone();
dns_parser::set_qr(&mut response, true).unwrap();
let upstream = "127.0.0.1:53".parse().unwrap();
assert!(
process_response(&response, 0x1234, upstream, true, &query)
.await
.is_ok()
);
let qtype_offset = response.len() - 4;
response[qtype_offset..qtype_offset + 2].copy_from_slice(&[0, 28]);
assert!(
process_response(&response, 0x1234, upstream, true, &query)
.await
.is_err()
);
}
#[tokio::test]
async fn test_tcp_fallback_preserves_ecs() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let upstream = listener.local_addr().unwrap();
let mut query = query();
dns_parser::add_edns_section(&mut query, 1232).unwrap();
dns_parser::add_edns_client_subnet(&mut query, "192.0.2.129", 24, 56, 1232).unwrap();
let server = tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
let mut length = [0u8; 2];
stream.read_exact(&mut length).await.unwrap();
let mut received = vec![0u8; u16::from_be_bytes(length) as usize];
stream.read_exact(&mut received).await.unwrap();
assert!(
dns_parser::extract_edns_client_subnet(&received)
.unwrap()
.is_some()
);
dns_parser::set_qr(&mut received, true).unwrap();
stream
.write_all(&(received.len() as u16).to_be_bytes())
.await
.unwrap();
stream.write_all(&received).await.unwrap();
});
let response = retry_with_tcp(&query, 0x1234, upstream).await.unwrap();
server.await.unwrap();
assert!(
dns_parser::extract_edns_client_subnet(&response)
.unwrap()
.is_some()
);
}
#[tokio::test]
async fn test_datagram_from_another_source_is_ignored() {
let upstream = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let upstream_addr = upstream.local_addr().unwrap();
let server = tokio::spawn(async move {
let mut packet = vec![0u8; 4096];
let (length, client) = upstream.recv_from(&mut packet).await.unwrap();
let stray = UdpSocket::bind("127.0.0.1:0").await.unwrap();
stray.send_to(b"junk", client).await.unwrap();
tokio::time::sleep(Duration::from_millis(50)).await;
packet.truncate(length);
dns_parser::set_qr(&mut packet, true).unwrap();
upstream.send_to(&packet, client).await.unwrap();
});
let client = ClientQuery::new(
query(),
vec![upstream_addr.to_string()],
2,
4096,
Arc::new(SharedStats::new()),
load_balancer::LoadBalancingStrategy::Random,
);
let response = client.process().await;
server.await.unwrap();
assert!(response.is_ok());
}
#[tokio::test]
async fn test_answered_retry_clears_the_failure_streak() {
let upstream = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let upstream_addr = upstream.local_addr().unwrap();
tokio::spawn(async move {
let mut packet = vec![0u8; 4096];
for datagram in 1u64.. {
let Ok((length, client)) = upstream.recv_from(&mut packet).await else {
break;
};
if datagram % 2 == 0 {
let mut response = packet[..length].to_vec();
dns_parser::set_qr(&mut response, true).unwrap();
upstream.send_to(&response, client).await.unwrap();
}
}
});
let stats = Arc::new(SharedStats::new());
for _ in 0..2 {
let client = ClientQuery::new(
query(),
vec![upstream_addr.to_string()],
2,
4096,
stats.clone(),
load_balancer::LoadBalancingStrategy::Random,
);
assert!(client.process().await.is_ok());
}
let snapshot = stats.get_stats().await;
let resolver = snapshot.get_resolver_stats(&upstream_addr).unwrap();
assert_eq!(resolver.timeout_count, 2);
assert_eq!(resolver.consecutive_failures, 0);
}
#[tokio::test]
async fn test_invalid_answer_does_not_unpark_a_resolver() {
let upstream = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let upstream_addr = upstream.local_addr().unwrap();
tokio::spawn(async move {
let mut packet = vec![0u8; 4096];
let (length, client) = upstream.recv_from(&mut packet).await.unwrap();
let mut response = packet[..length].to_vec();
dns_parser::set_qr(&mut response, true).unwrap();
response[13] = b'f';
upstream.send_to(&response, client).await.unwrap();
});
let stats = Arc::new(SharedStats::new());
for _ in 0..stats::PARK_AFTER_FAILURES {
stats.record_timeout(upstream_addr).await;
}
let client = ClientQuery::new(
query(),
vec![upstream_addr.to_string()],
2,
4096,
stats.clone(),
load_balancer::LoadBalancingStrategy::Random,
);
assert!(client.process().await.is_err());
let snapshot = stats.get_stats().await;
let resolver = snapshot.get_resolver_stats(&upstream_addr).unwrap();
assert_eq!(resolver.success_count, 0);
assert_eq!(resolver.consecutive_failures, stats::PARK_AFTER_FAILURES);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_tcp_client_that_never_reads_is_disconnected() {
let upstream = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let upstream_addr = upstream.local_addr().unwrap();
tokio::spawn(async move {
let mut packet = vec![0u8; 4096];
while let Ok((_, client)) = upstream.recv_from(&mut packet).await {
let mut response = packet[..query().len()].to_vec();
dns_parser::set_flags(&mut response, 0x8180).unwrap();
dns_parser::set_ancount(&mut response, 200).unwrap();
dns_parser::set_arcount(&mut response, 0).unwrap();
for i in 0..200u8 {
response.extend_from_slice(&[
0xc0, 0x0c, 0, 1, 0, 1, 0, 0, 0, 60, 0, 4, 192, 0, 2, i,
]);
}
upstream.send_to(&response, client).await.unwrap();
}
});
let listener_socket = tokio::net::TcpSocket::new_v4().unwrap();
listener_socket.set_send_buffer_size(4096).unwrap();
listener_socket
.bind("127.0.0.1:0".parse().unwrap())
.unwrap();
let listener = listener_socket.listen(1).unwrap();
let client_socket = tokio::net::TcpSocket::new_v4().unwrap();
client_socket.set_recv_buffer_size(4096).unwrap();
let mut client = client_socket
.connect(listener.local_addr().unwrap())
.await
.unwrap();
let (server_stream, peer) = listener.accept().await.unwrap();
let handler = tokio::spawn(process_tcp_connection(
server_stream,
peer,
vec![upstream_addr.to_string()],
Arc::new(QueryManager::for_tests(1, 4096, true)),
4096,
1,
None,
));
let mut pipelined = Vec::new();
for _ in 0..500 {
pipelined.extend_from_slice(&(query().len() as u16).to_be_bytes());
pipelined.extend_from_slice(&query());
}
client.write_all(&pipelined).await.unwrap();
assert!(
tokio::time::timeout(Duration::from_secs(20), handler)
.await
.is_ok()
);
}
}