#[cfg(feature = "__tls")]
use std::ffi::OsStr;
#[cfg(feature = "prometheus-metrics")]
use std::net::SocketAddr;
use std::{
fmt, fs, io,
marker::PhantomData,
net::{Ipv4Addr, Ipv6Addr},
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
#[cfg(feature = "sqlite")]
use cfg_if::cfg_if;
use ipnet::IpNet;
#[cfg(feature = "__tls")]
use rustls::{
pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject},
server::ResolvesServerCert,
sign::{CertifiedKey, SingleCertAndKey},
};
use serde::de::{self, MapAccess, SeqAccess, Visitor};
use serde::{self, Deserialize, Deserializer};
use thiserror::Error;
use tracing::{debug, info};
#[cfg(feature = "__dnssec")]
use crate::dnssec;
#[cfg(feature = "__https")]
use hickory_net::http::DEFAULT_DNS_QUERY_PATH;
#[cfg(feature = "__tls")]
use hickory_net::tls::default_provider;
use hickory_proto::{ProtoError, rr::Name, serialize::txt::ParseError};
#[cfg(feature = "recursor")]
use hickory_resolver::recursor::RecursiveConfig;
#[cfg(feature = "__dnssec")]
use hickory_server::dnssec::NxProofKind;
#[cfg(any(feature = "recursor", feature = "sqlite"))]
use hickory_server::net::runtime::TokioRuntimeProvider;
#[cfg(feature = "blocklist")]
use hickory_server::store::blocklist::{BlocklistConfig, BlocklistZoneHandler};
#[cfg(feature = "resolver")]
use hickory_server::store::forwarder::{ForwardConfig, ForwardZoneHandler};
#[cfg(feature = "recursor")]
use hickory_server::store::recursor::RecursiveZoneHandler;
#[cfg(feature = "sqlite")]
use hickory_server::store::sqlite::{SqliteConfig, SqliteZoneHandler};
use hickory_server::{
store::file::{FileConfig, FileZoneHandler},
zone_handler::{AxfrPolicy, ZoneHandler, ZoneType},
};
#[cfg(test)]
mod tests;
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub(crate) struct Config {
#[serde(default)]
pub(crate) listen_addrs_ipv4: Vec<Ipv4Addr>,
#[serde(default)]
pub(crate) listen_addrs_ipv6: Vec<Ipv6Addr>,
#[serde(default = "default_port")]
pub(crate) listen_port: u16,
#[cfg(feature = "__tls")]
#[serde(default = "default_tls_port")]
pub(crate) tls_listen_port: u16,
#[cfg(feature = "__https")]
#[serde(default = "default_https_port")]
pub(crate) https_listen_port: u16,
#[cfg(feature = "__quic")]
#[serde(default = "default_tls_port")]
pub(crate) quic_listen_port: u16,
#[cfg(feature = "prometheus-metrics")]
pub(crate) prometheus_listen_addr: Option<SocketAddr>,
#[serde(default)]
pub(crate) disable_tcp: bool,
#[serde(default)]
pub(crate) disable_udp: bool,
#[cfg(feature = "__tls")]
#[serde(default)]
pub(crate) disable_tls: bool,
#[cfg(feature = "__https")]
#[serde(default)]
pub(crate) disable_https: bool,
#[cfg(feature = "__quic")]
#[serde(default)]
pub(crate) disable_quic: bool,
#[cfg(feature = "prometheus-metrics")]
#[serde(default)]
pub(crate) disable_prometheus: bool,
#[serde(
deserialize_with = "parse_request_timeout",
default = "default_request_timeout"
)]
pub(crate) tcp_request_timeout: Duration,
#[cfg(feature = "__tls")]
#[serde(default)]
pub(crate) ssl_keylog_enabled: bool,
#[serde(default = "default_directory")]
pub(crate) directory: PathBuf,
pub user: Option<String>,
pub group: Option<String>,
#[serde(default)]
#[serde(deserialize_with = "deserialize_with_file")]
pub(crate) zones: Vec<ZoneConfig>,
#[cfg(feature = "__tls")]
pub(crate) tls_cert: Option<TlsCertConfig>,
#[cfg(feature = "__https")]
#[serde(default = "default_http_endpoint")]
pub(crate) http_endpoint: String,
#[serde(default)]
pub(crate) deny_networks: Vec<IpNet>,
#[serde(default)]
pub(crate) allow_networks: Vec<IpNet>,
#[serde(default)]
pub(crate) udp_socket: UdpSocketConfig,
#[serde(default)]
pub(crate) tcp_socket: TcpSocketConfig,
}
#[derive(Debug, Default, Clone, Copy, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct UdpSocketConfig {
pub(crate) recv_buffer_size: Option<usize>,
pub(crate) send_buffer_size: Option<usize>,
#[cfg(unix)]
pub(crate) sockets: Option<usize>,
}
#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct TcpSocketConfig {
#[serde(default = "default_tcp_listen_backlog")]
pub(crate) listen_backlog: i32,
#[serde(default = "default_tcp_response_buffer_size")]
pub(crate) response_buffer_size: usize,
}
impl Default for TcpSocketConfig {
fn default() -> Self {
Self {
listen_backlog: default_tcp_listen_backlog(),
response_buffer_size: default_tcp_response_buffer_size(),
}
}
}
fn default_tcp_listen_backlog() -> i32 {
128
}
fn default_tcp_response_buffer_size() -> usize {
32
}
impl Config {
pub(crate) fn read_config(path: &Path) -> Result<Self, ConfigError> {
Self::from_toml(&fs::read_to_string(path)?)
}
fn from_toml(toml: &str) -> Result<Self, ConfigError> {
Ok(toml::from_str(toml)?)
}
}
#[derive(Deserialize, Debug)]
struct ZoneConfigWithFile {
file: Option<PathBuf>,
#[serde(flatten)]
config: ZoneConfig,
}
fn deserialize_with_file<'de, D>(deserializer: D) -> Result<Vec<ZoneConfig>, D::Error>
where
D: Deserializer<'de>,
D::Error: de::Error,
{
Vec::<ZoneConfigWithFile>::deserialize(deserializer)?
.into_iter()
.map(|ZoneConfigWithFile { file, mut config }| match file {
Some(file) => match &mut config.zone_type_config {
ZoneTypeConfig::Primary(server_config)
| ZoneTypeConfig::Secondary(server_config) => {
if server_config
.stores
.iter()
.any(|store| matches!(store, ServerStoreConfig::File(_)))
{
Err(<D::Error as de::Error>::custom(
"having `file` and `[zones.store]` item with type `file` is ambiguous",
))
} else {
let store = ServerStoreConfig::File(FileConfig { zone_path: file });
if server_config.stores.len() == 1
&& matches!(&server_config.stores[0], ServerStoreConfig::Default)
{
server_config.stores[0] = store;
} else {
server_config.stores.push(store);
}
Ok(config)
}
}
_ => Err(<D::Error as de::Error>::custom(
"cannot use `file` on a zone that is not primary or secondary",
)),
},
_ => Ok(config),
})
.collect::<Result<Vec<_>, _>>()
}
#[derive(Deserialize, Debug)]
pub(crate) struct ZoneConfig {
pub zone: String, #[serde(flatten)]
pub zone_type_config: ZoneTypeConfig,
}
impl ZoneConfig {
pub(crate) async fn load(
self,
zone_dir: &Path,
) -> Result<Vec<Arc<dyn ZoneHandler>>, ProtoError> {
debug!("loading zone with config: {self:#?}");
let zone_name = self
.zone()
.map_err(|err| format!("failed to read zone name: {err}"))?;
let zone_type = self.zone_type();
let mut handlers: Vec<Arc<dyn ZoneHandler>> = vec![];
match self.zone_type_config {
ZoneTypeConfig::Primary(server_config) | ZoneTypeConfig::Secondary(server_config) => {
debug!(
"loading zone handlers for {zone_name} with stores {:?}",
server_config.stores
);
let axfr_policy = server_config.axfr_policy();
for store in &server_config.stores {
let handler: Arc<dyn ZoneHandler> = match store {
#[cfg(feature = "sqlite")]
ServerStoreConfig::Sqlite(config) => {
#[cfg_attr(not(feature = "__dnssec"), allow(unused_mut))]
let mut handler =
SqliteZoneHandler::<TokioRuntimeProvider>::try_from_config(
zone_name.clone(),
zone_type,
axfr_policy,
server_config.is_dnssec_enabled(),
Some(zone_dir),
config,
#[cfg(feature = "__dnssec")]
server_config.nx_proof_kind.clone(),
)
.await?;
#[cfg(feature = "__dnssec")]
dnssec::load_keys(&mut handler, &zone_name, &server_config.keys)
.await?;
Arc::new(handler)
}
ServerStoreConfig::File(config) => {
#[cfg_attr(not(feature = "__dnssec"), allow(unused_mut))]
let mut handler = FileZoneHandler::try_from_config(
zone_name.clone(),
zone_type,
axfr_policy,
Some(zone_dir),
config,
#[cfg(feature = "__dnssec")]
server_config.nx_proof_kind.clone(),
)?;
#[cfg(feature = "__dnssec")]
dnssec::load_keys(&mut handler, &zone_name, &server_config.keys)
.await?;
Arc::new(handler)
}
_ => return Err(ProtoError::from(EMPTY_STORES)),
};
handlers.push(handler);
}
}
ZoneTypeConfig::External { stores } => {
debug!(
"loading zone handlers for {zone_name} with stores {:?}",
stores
);
#[cfg_attr(
not(any(feature = "blocklist", feature = "resolver")),
allow(unreachable_code, unused_variables, clippy::never_loop)
)]
for store in stores {
let handler: Arc<dyn ZoneHandler> = match store {
#[cfg(feature = "blocklist")]
ExternalStoreConfig::Blocklist(config) => {
Arc::new(BlocklistZoneHandler::try_from_config(
zone_name.clone(),
config,
Some(zone_dir),
)?)
}
#[cfg(feature = "resolver")]
ExternalStoreConfig::Forward(config) => {
let forwarder = ForwardZoneHandler::builder_tokio(config)
.with_origin(zone_name.clone())
.build()?;
Arc::new(forwarder)
}
#[cfg(feature = "recursor")]
ExternalStoreConfig::Recursor(config) => {
let recursor = RecursiveZoneHandler::try_from_config(
zone_name.clone(),
zone_type,
*config,
Some(zone_dir),
TokioRuntimeProvider::default(),
)
.await?;
Arc::new(recursor)
}
_ => return Err(ProtoError::from(EMPTY_STORES)),
};
handlers.push(handler);
}
}
}
info!("zone successfully loaded: {zone_name}");
Ok(handlers)
}
pub(crate) fn zone(&self) -> Result<Name, ProtoError> {
Name::parse(&self.zone, Some(&Name::new()))
}
fn zone_type(&self) -> ZoneType {
match &self.zone_type_config {
ZoneTypeConfig::Primary { .. } => ZoneType::Primary,
ZoneTypeConfig::Secondary { .. } => ZoneType::Secondary,
ZoneTypeConfig::External { .. } => ZoneType::External,
}
}
}
const EMPTY_STORES: &str = "empty [[zones.stores]] in config";
#[derive(Deserialize, Debug)]
#[serde(tag = "zone_type")]
#[serde(deny_unknown_fields)]
pub(crate) enum ZoneTypeConfig {
Primary(ServerZoneConfig),
Secondary(ServerZoneConfig),
External {
#[serde(default = "store_config_default")]
#[serde(deserialize_with = "store_config_visitor")]
stores: Vec<ExternalStoreConfig>,
},
}
impl ZoneTypeConfig {
#[cfg(test)]
fn as_server(&self) -> Option<&ServerZoneConfig> {
match self {
Self::Primary(c) | Self::Secondary(c) => Some(c),
_ => None,
}
}
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub(crate) struct ServerZoneConfig {
#[serde(default)]
pub axfr_policy: AxfrPolicy,
#[cfg(feature = "__dnssec")]
#[serde(default)]
pub keys: Vec<dnssec::KeyConfig>,
#[cfg(feature = "__dnssec")]
pub nx_proof_kind: Option<NxProofKind>,
#[serde(default = "store_config_default")]
#[serde(deserialize_with = "store_config_visitor")]
pub stores: Vec<ServerStoreConfig>,
}
impl ServerZoneConfig {
#[cfg(test)]
fn file(&self) -> Option<&Path> {
self.stores.iter().find_map(|store| match store {
ServerStoreConfig::File(file_config) => Some(&*file_config.zone_path),
#[cfg(feature = "sqlite")]
ServerStoreConfig::Sqlite(sqlite_config) => Some(&*sqlite_config.zone_path),
ServerStoreConfig::Default => None,
})
}
fn axfr_policy(&self) -> AxfrPolicy {
self.axfr_policy
}
#[cfg(feature = "sqlite")]
fn is_dnssec_enabled(&self) -> bool {
cfg_if! {
if #[cfg(feature = "__dnssec")] {
!self.keys.is_empty()
} else {
false
}
}
}
}
#[derive(Deserialize, Debug, Default)]
#[serde(tag = "type")]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub(crate) enum ServerStoreConfig {
File(FileConfig),
#[cfg(feature = "sqlite")]
Sqlite(SqliteConfig),
#[default]
Default,
}
#[allow(clippy::large_enum_variant)]
#[derive(Deserialize, Debug, Default)]
#[serde(rename_all = "lowercase", tag = "type")]
#[non_exhaustive]
pub(crate) enum ExternalStoreConfig {
#[cfg(feature = "blocklist")]
Blocklist(BlocklistConfig),
#[cfg(feature = "resolver")]
Forward(ForwardConfig),
#[cfg(feature = "recursor")]
Recursor(Box<RecursiveConfig>),
#[default]
Default,
}
fn store_config_default<S: Default>() -> Vec<S> {
vec![Default::default()]
}
fn store_config_visitor<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de>,
{
struct MapOrSequence<T>(PhantomData<T>);
impl<'de, T: Deserialize<'de>> Visitor<'de> for MapOrSequence<T> {
type Value = Vec<T>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("map or sequence")
}
fn visit_seq<S>(self, seq: S) -> Result<Vec<T>, S::Error>
where
S: SeqAccess<'de>,
{
Deserialize::deserialize(de::value::SeqAccessDeserializer::new(seq))
}
fn visit_map<M>(self, map: M) -> Result<Vec<T>, M::Error>
where
M: MapAccess<'de>,
{
match Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)) {
Ok(seq) => Ok(vec![seq]),
Err(e) => Err(e),
}
}
}
deserializer.deserialize_any(MapOrSequence::<T>(PhantomData))
}
#[cfg(any(feature = "__tls", feature = "__https", feature = "__quic"))]
#[derive(Deserialize, PartialEq, Eq, Debug)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub(crate) struct TlsCertConfig {
pub(crate) path: PathBuf,
pub(crate) endpoint_name: Option<String>,
pub(crate) private_key: PathBuf,
}
#[cfg(any(feature = "__tls", feature = "__https", feature = "__quic"))]
impl TlsCertConfig {
pub(crate) fn load(&self, zone_dir: &Path) -> Result<Arc<dyn ResolvesServerCert>, String> {
if let Some(endpoint_name) = &self.endpoint_name {
info!("loading TLS cert for {endpoint_name} from {:?}", self.path);
} else {
info!("loading TLS cert from {:?}", self.path);
}
if self.path.extension().and_then(OsStr::to_str) != Some("pem") {
return Err(format!(
"unsupported certificate file format (expected `.pem` extension): {}",
self.path.display()
));
}
let cert_path = zone_dir.join(&self.path);
info!(
"loading TLS PEM certificate chain from: {}",
cert_path.display()
);
let cert_chain = CertificateDer::pem_file_iter(&cert_path)
.map_err(|e| {
format!(
"failed to read cert chain from {}: {e}",
cert_path.display()
)
})?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| {
format!(
"failed to parse cert chain from {}: {e}",
cert_path.display()
)
})?;
let key_extension = self.private_key.extension();
let key = if key_extension.is_some_and(|ext| ext == "pem") {
let key_path = zone_dir.join(&self.private_key);
info!("loading TLS PKCS8 key from PEM: {}", key_path.display());
PrivateKeyDer::from_pem_file(&key_path)
.map_err(|e| format!("failed to read key from {}: {e}", key_path.display()))?
} else if key_extension.is_some_and(|ext| ext == "der" || ext == "key") {
let key_path = zone_dir.join(&self.private_key);
info!("loading TLS PKCS8 key from DER: {}", key_path.display());
let buf =
fs::read(&key_path).map_err(|e| format!("error reading key from file: {e}"))?;
PrivateKeyDer::try_from(buf).map_err(|e| format!("error parsing key DER: {e}"))?
} else {
return Err(format!(
"unsupported private key file format (expected `.pem` or `.der` extension): {}",
self.private_key.display()
));
};
let certified_key = CertifiedKey::from_der(cert_chain, key, &default_provider())
.map_err(|err| format!("failed to read certificate and keys: {err:?}"))?;
Ok(Arc::new(SingleCertAndKey::from(certified_key)))
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub(crate) enum ConfigError {
#[error("io error: {0}")]
Io(#[from] io::Error),
#[error("toml decode error: {0}")]
TomlDecode(#[from] toml::de::Error),
#[error("failed to parse the zone file: {0}")]
ZoneParse(#[from] ParseError),
}
fn parse_request_timeout<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Duration, D::Error> {
Ok(Duration::from_secs(u64::deserialize(deserializer)?))
}
fn default_request_timeout() -> Duration {
Duration::from_secs(5)
}
#[cfg(feature = "__https")]
fn default_http_endpoint() -> String {
DEFAULT_DNS_QUERY_PATH.to_string()
}
fn default_directory() -> PathBuf {
PathBuf::from("/var/named") }
fn default_port() -> u16 {
53
}
#[cfg(any(feature = "__tls", feature = "__quic"))]
fn default_tls_port() -> u16 {
853
}
#[cfg(feature = "__https")]
fn default_https_port() -> u16 {
443
}