use crate::dns_processor::DnsQueryProcessor;
use crate::query_manager::QueryManager;
use crate::stats::SharedStats;
use base64::{Engine as _, engine::general_purpose};
use bytes::Bytes;
use http_body_util::BodyExt;
use http_body_util::Full;
use hyper::{Method, Request, Response, StatusCode, header, server::conn::http1};
use hyper_util::rt::TokioIo;
use log::{debug, error, info, warn};
use std::convert::Infallible;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::net::TcpListener;
use tokio::sync::Semaphore;
async fn handle_doh_request(
req: Request<hyper::body::Incoming>,
query_manager: Arc<QueryManager>,
upstream_servers: Vec<String>,
server_timeout: u64,
dns_packet_len_max: usize,
stats: Arc<SharedStats>,
load_balancing_strategy: crate::load_balancer::LoadBalancingStrategy,
client_addr: SocketAddr,
) -> Result<Response<Full<Bytes>>, Infallible> {
match *req.method() {
Method::GET => {
handle_doh_get_request(
req,
query_manager,
upstream_servers,
server_timeout,
dns_packet_len_max,
stats,
load_balancing_strategy,
client_addr,
)
.await
}
Method::POST => {
handle_doh_post_request(
req,
query_manager,
upstream_servers,
server_timeout,
dns_packet_len_max,
stats,
load_balancing_strategy,
client_addr,
)
.await
}
_ => {
Ok(create_error_response(
"Method Not Allowed",
StatusCode::METHOD_NOT_ALLOWED,
))
}
}
}
async fn handle_doh_get_request(
req: Request<hyper::body::Incoming>,
query_manager: Arc<QueryManager>,
upstream_servers: Vec<String>,
server_timeout: u64,
dns_packet_len_max: usize,
stats: Arc<SharedStats>,
load_balancing_strategy: crate::load_balancer::LoadBalancingStrategy,
client_addr: SocketAddr,
) -> Result<Response<Full<Bytes>>, Infallible> {
let uri = req.uri();
let query = uri.query().unwrap_or("");
let params: Vec<(String, String)> = form_urlencoded::parse(query.as_bytes())
.into_owned()
.collect();
let dns_param = params.iter().find(|(name, _)| name == "dns");
match dns_param {
Some((_, value)) => {
match general_purpose::URL_SAFE_NO_PAD.decode(value) {
Ok(dns_message) => {
process_dns_message(
dns_message,
query_manager,
upstream_servers,
server_timeout,
dns_packet_len_max,
stats,
load_balancing_strategy,
&client_addr,
)
.await
}
Err(e) => {
error!("Failed to decode base64url DNS message: {e}");
Ok(create_error_response(
"Bad Request: Invalid DNS message encoding",
StatusCode::BAD_REQUEST,
))
}
}
}
None => {
Ok(create_error_response(
"Bad Request: Missing 'dns' parameter",
StatusCode::BAD_REQUEST,
))
}
}
}
async fn handle_doh_post_request(
req: Request<hyper::body::Incoming>,
query_manager: Arc<QueryManager>,
upstream_servers: Vec<String>,
server_timeout: u64,
dns_packet_len_max: usize,
stats: Arc<SharedStats>,
load_balancing_strategy: crate::load_balancer::LoadBalancingStrategy,
client_addr: SocketAddr,
) -> Result<Response<Full<Bytes>>, Infallible> {
let content_type = req
.headers()
.get(header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.unwrap_or("");
if content_type == "application/dns-message" {
match req.into_body().collect().await {
Ok(bytes) => {
process_dns_message(
bytes.to_bytes().to_vec(),
query_manager,
upstream_servers,
server_timeout,
dns_packet_len_max,
stats,
load_balancing_strategy,
&client_addr,
)
.await
}
Err(e) => {
error!("Failed to read request body: {e}");
Ok(create_error_response(
"Bad Request: Failed to read request body",
StatusCode::BAD_REQUEST,
))
}
}
} else {
Ok(create_error_response(
"Bad Request: Content-Type must be application/dns-message",
StatusCode::BAD_REQUEST,
))
}
}
fn create_error_response(message: &str, status: StatusCode) -> Response<Full<Bytes>> {
let mut response = Response::new(Full::new(Bytes::from(message.to_string())));
*response.status_mut() = status;
response
}
struct DoHHandler;
impl DnsQueryProcessor for DoHHandler {}
async fn process_dns_message(
dns_message: Vec<u8>,
query_manager: Arc<QueryManager>,
upstream_servers: Vec<String>,
server_timeout: u64,
dns_packet_len_max: usize,
stats: Arc<SharedStats>,
load_balancing_strategy: crate::load_balancer::LoadBalancingStrategy,
client_addr: &SocketAddr,
) -> Result<Response<Full<Bytes>>, Infallible> {
let handler = DoHHandler;
let client_addr_str = client_addr.to_string();
let result = handler
.process_dns_query(
&dns_message,
&client_addr_str,
"DoH",
&query_manager,
&upstream_servers,
server_timeout,
dns_packet_len_max,
Some(stats),
load_balancing_strategy,
)
.await;
match result {
Some((response_data, _)) => {
Ok(create_dns_response(&response_data))
}
None => {
Ok(create_error_response(
"Internal Server Error",
StatusCode::INTERNAL_SERVER_ERROR,
))
}
}
}
fn create_dns_response(response_data: &[u8]) -> Response<Full<Bytes>> {
let mut http_response = Response::new(Full::new(Bytes::from(response_data.to_vec())));
http_response.headers_mut().insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/dns-message"),
);
add_cache_headers(&mut http_response, response_data);
http_response
}
fn add_cache_headers(response: &mut Response<Full<Bytes>>, dns_data: &[u8]) {
let cache_ttl = match crate::dns_parser::extract_min_ttl(dns_data) {
Ok(Some(ttl)) => std::cmp::max(ttl, 1), _ => 10, };
let cache_control = format!("public, max-age={cache_ttl}");
if let Ok(value) = header::HeaderValue::from_str(&cache_control) {
response.headers_mut().insert(header::CACHE_CONTROL, value);
}
let now = std::time::UNIX_EPOCH
+ std::time::Duration::from_secs(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
);
let expiry_time = now + std::time::Duration::from_secs(cache_ttl as u64);
let expiry_http_date = httpdate::fmt_http_date(expiry_time);
if let Ok(value) = header::HeaderValue::from_str(&expiry_http_date) {
response.headers_mut().insert(header::EXPIRES, value);
}
debug!("DoH response with cache TTL: {cache_ttl} seconds");
}
pub async fn start_doh_server(
addr: SocketAddr,
query_manager: Arc<QueryManager>,
upstream_servers: Vec<String>,
server_timeout: u64,
dns_packet_len_max: usize,
stats: Arc<SharedStats>,
max_connections: usize,
rate_limiter: Option<Arc<crate::rate_limiter::RateLimiter>>,
load_balancing_strategy: crate::load_balancer::LoadBalancingStrategy,
ip_validator: Option<Arc<dyn std::any::Any + Send + Sync>>,
enable_strict_ip_validation: bool,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let listener = TcpListener::bind(addr).await?;
info!("DoH server listening on {addr}");
let semaphore = Arc::new(Semaphore::new(max_connections));
loop {
let (stream, client_addr) = listener.accept().await?;
let io = TokioIo::new(stream);
let query_manager = query_manager.clone();
let upstream_servers = upstream_servers.clone();
let stats = stats.clone();
let semaphore = semaphore.clone();
let rate_limiter = rate_limiter.clone();
let ip_validator = ip_validator.clone();
tokio::spawn(async move {
if !is_client_allowed(
client_addr,
&rate_limiter,
&ip_validator,
enable_strict_ip_validation,
)
.await
{
return;
}
let _permit = match semaphore.try_acquire() {
Ok(permit) => permit,
Err(_) => {
warn!("Too many DoH connections, rejecting connection from {client_addr}");
return;
}
};
debug!("Accepted DoH connection from {client_addr}");
handle_http_connection(
io,
client_addr,
query_manager,
upstream_servers,
server_timeout,
dns_packet_len_max,
stats,
load_balancing_strategy,
)
.await;
debug!("Closed DoH connection from {client_addr}");
});
}
}
async fn is_client_allowed(
client_addr: SocketAddr,
rate_limiter: &Option<Arc<crate::rate_limiter::RateLimiter>>,
ip_validator: &Option<Arc<dyn std::any::Any + Send + Sync>>,
enable_strict_ip_validation: bool,
) -> bool {
if enable_strict_ip_validation {
if let Some(_validator) = ip_validator {
debug!("Using provided IP validator for DoH client {client_addr}");
}
}
if let Some(limiter) = rate_limiter {
let client_ip = client_addr.ip();
if !limiter.is_allowed(client_ip).await {
warn!("Rate limit exceeded for DoH client {client_addr}, dropping connection");
return false;
}
}
true
}
async fn handle_http_connection(
io: TokioIo<tokio::net::TcpStream>,
client_addr: SocketAddr,
query_manager: Arc<QueryManager>,
upstream_servers: Vec<String>,
server_timeout: u64,
dns_packet_len_max: usize,
stats: Arc<SharedStats>,
load_balancing_strategy: crate::load_balancer::LoadBalancingStrategy,
) {
let client_addr_clone = client_addr;
let service = hyper::service::service_fn(move |req| {
let query_manager = query_manager.clone();
let upstream_servers = upstream_servers.clone();
let stats = stats.clone();
let client_addr = client_addr_clone;
async move {
handle_doh_request(
req,
query_manager,
upstream_servers,
server_timeout,
dns_packet_len_max,
stats,
load_balancing_strategy,
client_addr,
)
.await
}
});
let server_timeout_duration = std::time::Duration::from_secs(server_timeout);
let connection_future = http1::Builder::new().serve_connection(io, service);
match tokio::time::timeout(server_timeout_duration, connection_future).await {
Ok(result) => {
if let Err(err) = result {
error!("Error serving DoH connection from {client_addr}: {err}");
}
}
Err(_) => {
error!("DoH connection from {client_addr} timed out after {server_timeout} seconds");
}
}
}