support using BIND in the Resolver role

This commit is contained in:
Jorge Aparicio 2024-02-23 15:31:53 +01:00
parent b8605f7944
commit 2c4ef88a98
6 changed files with 122 additions and 14 deletions

View File

@ -21,8 +21,12 @@ fn edns_support() -> Result<()> {
let client = Client::new(network)?;
let settings = *DigSettings::default().authentic_data().recurse();
let ans = client.dig(settings, resolver.ipv4_addr(), RecordType::SOA, &FQDN::ROOT)?;
assert!(ans.status.is_servfail());
let _ans = client.dig(settings, resolver.ipv4_addr(), RecordType::SOA, &FQDN::ROOT)?;
// implementation-specific behavior
// unbound replies with SERVFAIL
// BIND replies with NOERROR
// assert!(_ans.status.is_servfail());
tshark.wait_for_capture()?;

View File

@ -22,6 +22,7 @@ const PACKAGE_NAME: &str = env!("CARGO_PKG_NAME");
#[derive(Clone)]
pub enum Image {
Bind,
Client,
Hickory(Repository<'static>),
Unbound,
@ -30,15 +31,21 @@ pub enum Image {
impl Image {
fn dockerfile(&self) -> &'static str {
match self {
Self::Unbound => include_str!("docker/unbound.Dockerfile"),
Self::Hickory { .. } => include_str!("docker/hickory.Dockerfile"),
Self::Bind => include_str!("docker/bind.Dockerfile"),
Self::Client => include_str!("docker/client.Dockerfile"),
Self::Hickory { .. } => include_str!("docker/hickory.Dockerfile"),
Self::Unbound => include_str!("docker/unbound.Dockerfile"),
}
}
fn once(&self) -> &'static Once {
match self {
Self::Client { .. } => {
Self::Bind => {
static BIND_ONCE: Once = Once::new();
&BIND_ONCE
}
Self::Client => {
static CLIENT_ONCE: Once = Once::new();
&CLIENT_ONCE
}
@ -48,7 +55,7 @@ impl Image {
&HICKORY_ONCE
}
Self::Unbound { .. } => {
Self::Unbound => {
static UNBOUND_ONCE: Once = Once::new();
&UNBOUND_ONCE
}
@ -59,6 +66,7 @@ impl Image {
impl From<Implementation> for Image {
fn from(implementation: Implementation) -> Self {
match implementation {
Implementation::Bind => Self::Bind,
Implementation::Unbound => Self::Unbound,
Implementation::Hickory(repo) => Self::Hickory(repo),
}
@ -69,6 +77,7 @@ impl fmt::Display for Image {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Client => "client",
Self::Bind => "bind",
Self::Hickory { .. } => "hickory",
Self::Unbound => "unbound",
};

View File

@ -0,0 +1,10 @@
FROM debian:bookworm-slim
# ldns-utils = ldns-{key2ds,keygen,signzone}
# rm = remove default configuration files
RUN apt-get update && \
apt-get install -y \
bind9 \
ldnsutils \
tshark && \
rm -f /etc/bind/*

View File

@ -28,8 +28,16 @@ const DEFAULT_TTL: u32 = 24 * 60 * 60; // 1 day
#[derive(Clone)]
pub enum Implementation {
Unbound,
Bind,
Hickory(Repository<'static>),
Unbound,
}
impl Implementation {
#[must_use]
pub fn is_bind(&self) -> bool {
matches!(self, Self::Bind)
}
}
#[derive(Clone)]
@ -70,6 +78,10 @@ pub fn subject() -> Implementation {
return Implementation::Unbound;
}
if subject == "bind" {
return Implementation::Bind;
}
if subject.starts_with("hickory") {
if let Some(url) = subject.strip_prefix("hickory ") {
Implementation::Hickory(Repository(url.to_string()))
@ -85,5 +97,13 @@ pub fn subject() -> Implementation {
}
pub fn peer() -> Implementation {
Implementation::default()
if let Ok(subject) = std::env::var("DNS_TEST_PEER") {
match subject.as_str() {
"unbound" => Implementation::Unbound,
"bind" => Implementation::Bind,
_ => panic!("`{subject}` is not supported as a test peer implementation"),
}
} else {
Implementation::default()
}
}

View File

@ -10,6 +10,7 @@ use crate::{Implementation, Result};
pub struct Resolver {
container: Container,
child: Child,
implementation: Implementation,
}
impl Resolver {
@ -26,8 +27,6 @@ impl Resolver {
trust_anchor: &TrustAnchor,
network: &Network,
) -> Result<Self> {
const TRUST_ANCHOR_FILE: &str = "/etc/trusted-key.key";
assert!(
!roots.is_empty(),
"must configure at least one local root server"
@ -43,6 +42,15 @@ impl Resolver {
let use_dnssec = !trust_anchor.is_empty();
match implementation {
Implementation::Bind => {
container.cp("/etc/bind/root.hints", &hints)?;
container.cp(
"/etc/bind/named.conf",
&named_conf(use_dnssec, network.netmask()),
)?;
}
Implementation::Unbound => {
container.cp("/etc/unbound/root.hints", &hints)?;
@ -62,16 +70,33 @@ impl Resolver {
}
if use_dnssec {
container.cp(TRUST_ANCHOR_FILE, &trust_anchor.to_string())?;
let path = if implementation.is_bind() {
"/etc/bind/bind.keys"
} else {
"/etc/trusted-key.key"
};
let contents = if implementation.is_bind() {
trust_anchor.delv()
} else {
trust_anchor.to_string()
};
container.cp(path, &contents)?;
}
let command: &[_] = match implementation {
Implementation::Bind => &["named", "-g", "-d5"],
Implementation::Unbound => &["unbound", "-d"],
Implementation::Hickory { .. } => &["hickory-dns", "-d"],
};
let child = container.spawn(command)?;
Ok(Self { child, container })
Ok(Self {
child,
container,
implementation: implementation.clone(),
})
}
pub fn eavesdrop(&self) -> Result<Tshark> {
@ -88,7 +113,11 @@ impl Resolver {
/// gracefully terminates the name server collecting all logs
pub fn terminate(self) -> Result<String> {
let pidfile = "/run/unbound.pid";
let pidfile = match self.implementation {
Implementation::Bind => "/tmp/named.pid",
Implementation::Unbound => "/run/unbound.pid",
Implementation::Hickory(..) => unimplemented!(),
};
let kill = format!(
"test -f {pidfile} || sleep 1
kill -TERM $(cat {pidfile})"
@ -108,6 +137,10 @@ kill -TERM $(cat {pidfile})"
}
}
fn named_conf(use_dnssec: bool, netmask: &str) -> String {
minijinja::render!(include_str!("templates/named.resolver.conf.jinja"), use_dnssec => use_dnssec, netmask => netmask)
}
fn unbound_conf(use_dnssec: bool, netmask: &str) -> String {
minijinja::render!(include_str!("templates/unbound.conf.jinja"), use_dnssec => use_dnssec, netmask => netmask)
}
@ -123,7 +156,7 @@ mod tests {
use super::*;
#[test]
fn terminate_works() -> Result<()> {
fn terminate_unbound_works() -> Result<()> {
let network = Network::new()?;
let ns = NameServer::new(&Implementation::Unbound, FQDN::ROOT, &network)?.start()?;
let resolver = Resolver::start(
@ -139,4 +172,22 @@ mod tests {
Ok(())
}
#[test]
fn terminate_bind_works() -> Result<()> {
let network = Network::new()?;
let ns = NameServer::new(&Implementation::Unbound, FQDN::ROOT, &network)?.start()?;
let resolver = Resolver::start(
&Implementation::Bind,
&[Root::new(ns.fqdn().clone(), ns.ipv4_addr())],
&TrustAnchor::empty(),
&network,
)?;
let logs = resolver.terminate()?;
eprintln!("{logs}");
assert!(logs.contains("starting BIND"));
Ok(())
}
}

View File

@ -0,0 +1,14 @@
options {
directory "/var/cache/bind";
pid-file "/tmp/named.pid";
recursion yes;
dnssec-validation {% if use_dnssec %} auto {% else %} no {% endif %};
allow-transfer { none; };
# significantly reduces noise in logs
empty-zones-enable no;
};
zone "." {
type hint;
file "/etc/bind/root.hints";
};