commit
77150bbca6
20
.github/workflows/ci.yml
vendored
20
.github/workflows/ci.yml
vendored
|
@ -20,11 +20,23 @@ jobs:
|
|||
toolchain: stable
|
||||
components: clippy, rustfmt
|
||||
|
||||
- name: Run tests
|
||||
run: cargo t
|
||||
- name: Run dns-test tests
|
||||
run: cargo test -p dns-test -- --include-ignored
|
||||
|
||||
- name: Run tests against unbound
|
||||
run: cargo test -p conformance-tests -- --include-ignored
|
||||
|
||||
- name: Run tests against hickory
|
||||
run: DNS_TEST_SUBJECT=hickory cargo test -p conformance-tests
|
||||
|
||||
- name: Check that ignored tests fail with hickory
|
||||
run: |
|
||||
tmpfile="$(mktemp)"
|
||||
DNS_TEST_SUBJECT=hickory cargo test -p conformance-tests -- --ignored | tee "$tmpfile"
|
||||
grep 'test result: FAILED. 0 passed' "$tmpfile" || ( echo "expected ALL tests to fail but at least one passed; the passing tests must be un-#[ignore]-d" && exit 1 )
|
||||
|
||||
- name: Check that code is formatted
|
||||
run: cargo fmt -- --check
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
- name: Lint code
|
||||
run: cargo clippy -- -D warnings
|
||||
run: cargo clippy --workspace -- -D warnings
|
||||
|
|
9
Cargo.lock
generated
9
Cargo.lock
generated
|
@ -21,7 +21,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "dnssec-tests"
|
||||
name = "conformance-tests"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"dns-test",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dns-test"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"minijinja",
|
||||
|
|
15
Cargo.toml
15
Cargo.toml
|
@ -1,12 +1,3 @@
|
|||
[package]
|
||||
name = "dnssec-tests"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT or Apache 2.0"
|
||||
|
||||
[dependencies]
|
||||
minijinja = "1.0.12"
|
||||
tempfile = "3.9.0"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
[workspace]
|
||||
members = ["packages/*"]
|
||||
resolver = "2"
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
FROM ubuntu:22.04
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y dnsutils unbound nsd iputils-ping tshark vim ldnsutils
|
11
packages/conformance-tests/Cargo.toml
Normal file
11
packages/conformance-tests/Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
edition = "2021"
|
||||
name = "conformance-tests"
|
||||
publish = false
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies]
|
||||
dns-test.path = "../dns-test"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
3
packages/conformance-tests/src/lib.rs
Normal file
3
packages/conformance-tests/src/lib.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
#![cfg(test)]
|
||||
|
||||
mod resolver;
|
4
packages/conformance-tests/src/resolver.rs
Normal file
4
packages/conformance-tests/src/resolver.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
//! Recursive resolver role
|
||||
|
||||
mod dns;
|
||||
mod dnssec;
|
3
packages/conformance-tests/src/resolver/dns.rs
Normal file
3
packages/conformance-tests/src/resolver/dns.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
//! plain DNS functionality
|
||||
|
||||
mod scenarios;
|
104
packages/conformance-tests/src/resolver/dns/scenarios.rs
Normal file
104
packages/conformance-tests/src/resolver/dns/scenarios.rs
Normal file
|
@ -0,0 +1,104 @@
|
|||
use std::net::Ipv4Addr;
|
||||
|
||||
use dns_test::client::{Client, Dnssec, Recurse};
|
||||
use dns_test::name_server::NameServer;
|
||||
use dns_test::record::RecordType;
|
||||
use dns_test::zone_file::Root;
|
||||
use dns_test::{Resolver, Result, TrustAnchor, FQDN};
|
||||
|
||||
#[test]
|
||||
fn can_resolve() -> Result<()> {
|
||||
let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4);
|
||||
let needle_fqdn = FQDN("example.nameservers.com.")?;
|
||||
|
||||
let mut root_ns = NameServer::new(FQDN::ROOT)?;
|
||||
let mut com_ns = NameServer::new(FQDN::COM)?;
|
||||
|
||||
let mut nameservers_ns = NameServer::new(FQDN("nameservers.com.")?)?;
|
||||
nameservers_ns
|
||||
.a(root_ns.fqdn().clone(), root_ns.ipv4_addr())
|
||||
.a(com_ns.fqdn().clone(), com_ns.ipv4_addr())
|
||||
.a(needle_fqdn.clone(), expected_ipv4_addr);
|
||||
let nameservers_ns = nameservers_ns.start()?;
|
||||
|
||||
eprintln!("nameservers.com.zone:\n{}", nameservers_ns.zone_file());
|
||||
|
||||
com_ns.referral(
|
||||
nameservers_ns.zone().clone(),
|
||||
nameservers_ns.fqdn().clone(),
|
||||
nameservers_ns.ipv4_addr(),
|
||||
);
|
||||
let com_ns = com_ns.start()?;
|
||||
|
||||
eprintln!("com.zone:\n{}", com_ns.zone_file());
|
||||
|
||||
root_ns.referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr());
|
||||
let root_ns = root_ns.start()?;
|
||||
|
||||
eprintln!("root.zone:\n{}", root_ns.zone_file());
|
||||
|
||||
let roots = &[Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr())];
|
||||
let resolver = Resolver::start(dns_test::subject(), roots, &TrustAnchor::empty())?;
|
||||
let resolver_ip_addr = resolver.ipv4_addr();
|
||||
|
||||
let client = Client::new()?;
|
||||
let output = client.dig(
|
||||
Recurse::Yes,
|
||||
Dnssec::No,
|
||||
resolver_ip_addr,
|
||||
RecordType::A,
|
||||
&needle_fqdn,
|
||||
)?;
|
||||
|
||||
assert!(output.status.is_noerror());
|
||||
|
||||
let [answer] = output.answer.try_into().unwrap();
|
||||
let a = answer.try_into_a().unwrap();
|
||||
|
||||
assert_eq!(needle_fqdn, a.fqdn);
|
||||
assert_eq!(expected_ipv4_addr, a.ipv4_addr);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn nxdomain() -> Result<()> {
|
||||
let needle_fqdn = FQDN("unicorn.nameservers.com.")?;
|
||||
|
||||
let mut root_ns = NameServer::new(FQDN::ROOT)?;
|
||||
let mut com_ns = NameServer::new(FQDN::COM)?;
|
||||
|
||||
let mut nameservers_ns = NameServer::new(FQDN("nameservers.com.")?)?;
|
||||
nameservers_ns
|
||||
.a(root_ns.fqdn().clone(), root_ns.ipv4_addr())
|
||||
.a(com_ns.fqdn().clone(), com_ns.ipv4_addr());
|
||||
let nameservers_ns = nameservers_ns.start()?;
|
||||
|
||||
com_ns.referral(
|
||||
nameservers_ns.zone().clone(),
|
||||
nameservers_ns.fqdn().clone(),
|
||||
nameservers_ns.ipv4_addr(),
|
||||
);
|
||||
let com_ns = com_ns.start()?;
|
||||
|
||||
root_ns.referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr());
|
||||
let root_ns = root_ns.start()?;
|
||||
|
||||
let roots = &[Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr())];
|
||||
let resolver = Resolver::start(dns_test::subject(), roots, &TrustAnchor::empty())?;
|
||||
let resolver_ip_addr = resolver.ipv4_addr();
|
||||
|
||||
let client = Client::new()?;
|
||||
let output = client.dig(
|
||||
Recurse::Yes,
|
||||
Dnssec::No,
|
||||
resolver_ip_addr,
|
||||
RecordType::A,
|
||||
&needle_fqdn,
|
||||
)?;
|
||||
|
||||
assert!(dbg!(output).status.is_nxdomain());
|
||||
|
||||
Ok(())
|
||||
}
|
3
packages/conformance-tests/src/resolver/dnssec.rs
Normal file
3
packages/conformance-tests/src/resolver/dnssec.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
//! DNSSEC functionality
|
||||
|
||||
mod scenarios;
|
128
packages/conformance-tests/src/resolver/dnssec/scenarios.rs
Normal file
128
packages/conformance-tests/src/resolver/dnssec/scenarios.rs
Normal file
|
@ -0,0 +1,128 @@
|
|||
use std::net::Ipv4Addr;
|
||||
|
||||
use dns_test::client::{Client, Dnssec, Recurse};
|
||||
use dns_test::name_server::NameServer;
|
||||
use dns_test::record::RecordType;
|
||||
use dns_test::zone_file::Root;
|
||||
use dns_test::{Resolver, Result, TrustAnchor, FQDN};
|
||||
|
||||
// no DS records are involved; this is a single-link chain of trust
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn can_validate_without_delegation() -> Result<()> {
|
||||
let mut ns = NameServer::new(FQDN::ROOT)?;
|
||||
ns.a(ns.fqdn().clone(), ns.ipv4_addr());
|
||||
let ns = ns.sign()?;
|
||||
|
||||
let root_ksk = ns.key_signing_key().clone();
|
||||
let root_zsk = ns.zone_signing_key().clone();
|
||||
|
||||
eprintln!("root.zone.signed:\n{}", ns.signed_zone_file());
|
||||
|
||||
let ns = ns.start()?;
|
||||
|
||||
eprintln!("root.zone:\n{}", ns.zone_file());
|
||||
|
||||
let roots = &[Root::new(ns.fqdn().clone(), ns.ipv4_addr())];
|
||||
|
||||
let trust_anchor = TrustAnchor::from_iter([root_ksk.clone(), root_zsk.clone()]);
|
||||
let resolver = Resolver::start(dns_test::subject(), roots, &trust_anchor)?;
|
||||
let resolver_addr = resolver.ipv4_addr();
|
||||
|
||||
let client = Client::new()?;
|
||||
let output = client.dig(
|
||||
Recurse::Yes,
|
||||
Dnssec::Yes,
|
||||
resolver_addr,
|
||||
RecordType::SOA,
|
||||
&FQDN::ROOT,
|
||||
)?;
|
||||
|
||||
assert!(output.status.is_noerror());
|
||||
assert!(output.flags.authenticated_data);
|
||||
|
||||
let output = client.delv(resolver_addr, RecordType::SOA, &FQDN::ROOT, &trust_anchor)?;
|
||||
assert!(output.starts_with("; fully validated"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn can_validate_with_delegation() -> Result<()> {
|
||||
let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4);
|
||||
let needle_fqdn = FQDN("example.nameservers.com.")?;
|
||||
|
||||
let mut root_ns = NameServer::new(FQDN::ROOT)?;
|
||||
let mut com_ns = NameServer::new(FQDN::COM)?;
|
||||
|
||||
let mut nameservers_ns = NameServer::new(FQDN("nameservers.com.")?)?;
|
||||
nameservers_ns
|
||||
.a(root_ns.fqdn().clone(), root_ns.ipv4_addr())
|
||||
.a(com_ns.fqdn().clone(), com_ns.ipv4_addr())
|
||||
.a(needle_fqdn.clone(), expected_ipv4_addr);
|
||||
let nameservers_ns = nameservers_ns.sign()?;
|
||||
let nameservers_ds = nameservers_ns.ds().clone();
|
||||
let nameservers_ns = nameservers_ns.start()?;
|
||||
|
||||
eprintln!("nameservers.com.zone:\n{}", nameservers_ns.zone_file());
|
||||
|
||||
com_ns
|
||||
.referral(
|
||||
nameservers_ns.zone().clone(),
|
||||
nameservers_ns.fqdn().clone(),
|
||||
nameservers_ns.ipv4_addr(),
|
||||
)
|
||||
.ds(nameservers_ds);
|
||||
let com_ns = com_ns.sign()?;
|
||||
let com_ds = com_ns.ds().clone();
|
||||
let com_ns = com_ns.start()?;
|
||||
|
||||
eprintln!("com.zone:\n{}", com_ns.zone_file());
|
||||
|
||||
root_ns
|
||||
.referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr())
|
||||
.ds(com_ds);
|
||||
let root_ns = root_ns.sign()?;
|
||||
let root_ksk = root_ns.key_signing_key().clone();
|
||||
let root_zsk = root_ns.zone_signing_key().clone();
|
||||
|
||||
eprintln!("root.zone.signed:\n{}", root_ns.signed_zone_file());
|
||||
|
||||
let root_ns = root_ns.start()?;
|
||||
|
||||
eprintln!("root.zone:\n{}", root_ns.zone_file());
|
||||
|
||||
let roots = &[Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr())];
|
||||
|
||||
let trust_anchor = TrustAnchor::from_iter([root_ksk.clone(), root_zsk.clone()]);
|
||||
let resolver = Resolver::start(dns_test::subject(), roots, &trust_anchor)?;
|
||||
let resolver_addr = resolver.ipv4_addr();
|
||||
|
||||
let client = Client::new()?;
|
||||
let output = client.dig(
|
||||
Recurse::Yes,
|
||||
Dnssec::Yes,
|
||||
resolver_addr,
|
||||
RecordType::A,
|
||||
&needle_fqdn,
|
||||
)?;
|
||||
|
||||
assert!(output.status.is_noerror());
|
||||
|
||||
assert!(output.flags.authenticated_data);
|
||||
|
||||
let [a, _rrsig] = output.answer.try_into().unwrap();
|
||||
let a = a.try_into_a().unwrap();
|
||||
|
||||
assert_eq!(needle_fqdn, a.fqdn);
|
||||
assert_eq!(expected_ipv4_addr, a.ipv4_addr);
|
||||
|
||||
let output = client.delv(resolver_addr, RecordType::A, &needle_fqdn, &trust_anchor)?;
|
||||
assert!(output.starts_with("; fully validated"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO nxdomain with NSEC records
|
||||
// TODO nxdomain with NSEC3 records
|
13
packages/dns-test/Cargo.toml
Normal file
13
packages/dns-test/Cargo.toml
Normal file
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
name = "dns-test"
|
||||
publish = false
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies]
|
||||
minijinja = "1.0.12"
|
||||
tempfile = "3.9.0"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
|
@ -3,7 +3,8 @@ use std::net::Ipv4Addr;
|
|||
|
||||
use crate::container::Container;
|
||||
use crate::record::{Record, RecordType};
|
||||
use crate::{Error, Result, FQDN};
|
||||
use crate::trust_anchor::TrustAnchor;
|
||||
use crate::{Error, Implementation, Result, FQDN};
|
||||
|
||||
pub struct Client {
|
||||
inner: Container,
|
||||
|
@ -12,23 +13,33 @@ pub struct Client {
|
|||
impl Client {
|
||||
pub fn new() -> Result<Self> {
|
||||
Ok(Self {
|
||||
inner: Container::run()?,
|
||||
inner: Container::run(Implementation::Unbound)?,
|
||||
})
|
||||
}
|
||||
|
||||
// FIXME this needs to use the same trust anchor as `RecursiveResolver` or validation will fail
|
||||
pub fn delv(
|
||||
&self,
|
||||
server: Ipv4Addr,
|
||||
record_type: RecordType,
|
||||
fqdn: &FQDN<'_>,
|
||||
trust_anchor: &TrustAnchor,
|
||||
) -> Result<String> {
|
||||
const TRUST_ANCHOR_PATH: &str = "/etc/bind.keys";
|
||||
|
||||
assert!(
|
||||
!trust_anchor.is_empty(),
|
||||
"`delv` cannot be used with an empty trust anchor"
|
||||
);
|
||||
|
||||
self.inner.cp(TRUST_ANCHOR_PATH, &trust_anchor.delv())?;
|
||||
|
||||
self.inner.stdout(&[
|
||||
"delv",
|
||||
"+mtrace",
|
||||
&format!("@{server}"),
|
||||
record_type.as_str(),
|
||||
"-a",
|
||||
TRUST_ANCHOR_PATH,
|
||||
fqdn.as_str(),
|
||||
record_type.as_str(),
|
||||
])
|
||||
}
|
||||
|
||||
|
@ -209,6 +220,7 @@ pub enum DigStatus {
|
|||
NOERROR,
|
||||
NXDOMAIN,
|
||||
REFUSED,
|
||||
SERVFAIL,
|
||||
}
|
||||
|
||||
impl DigStatus {
|
||||
|
@ -216,6 +228,11 @@ impl DigStatus {
|
|||
pub fn is_noerror(&self) -> bool {
|
||||
matches!(self, Self::NOERROR)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_nxdomain(&self) -> bool {
|
||||
matches!(self, Self::NXDOMAIN)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for DigStatus {
|
||||
|
@ -226,6 +243,7 @@ impl FromStr for DigStatus {
|
|||
"NXDOMAIN" => Self::NXDOMAIN,
|
||||
"NOERROR" => Self::NOERROR,
|
||||
"REFUSED" => Self::REFUSED,
|
||||
"SERVFAIL" => Self::SERVFAIL,
|
||||
_ => return Err(format!("unknown status: {input}").into()),
|
||||
};
|
||||
|
|
@ -1,56 +1,52 @@
|
|||
use core::str;
|
||||
use std::fs;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::path::Path;
|
||||
use std::process::{self, Child, ExitStatus};
|
||||
use std::process::{self, ExitStatus};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::{atomic, Once};
|
||||
use std::sync::{atomic, Arc};
|
||||
|
||||
use tempfile::NamedTempFile;
|
||||
use tempfile::{NamedTempFile, TempDir};
|
||||
|
||||
use crate::{Error, Result};
|
||||
use crate::{Error, Implementation, Result};
|
||||
|
||||
pub struct Container {
|
||||
name: String,
|
||||
id: String,
|
||||
// TODO probably also want the IPv6 address
|
||||
ipv4_addr: Ipv4Addr,
|
||||
inner: Arc<Inner>,
|
||||
}
|
||||
|
||||
const PACKAGE_NAME: &str = env!("CARGO_PKG_NAME");
|
||||
|
||||
impl Container {
|
||||
/// Starts the container in a "parked" state
|
||||
pub fn run() -> Result<Self> {
|
||||
static ONCE: Once = Once::new();
|
||||
static COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
pub fn run(implementation: Implementation) -> Result<Self> {
|
||||
// TODO make this configurable and support hickory & bind
|
||||
let dockerfile = implementation.dockerfile();
|
||||
let docker_build_dir = TempDir::new()?;
|
||||
let docker_build_dir = docker_build_dir.path();
|
||||
fs::write(docker_build_dir.join("Dockerfile"), dockerfile)?;
|
||||
|
||||
// TODO configurable: hickory; bind
|
||||
let binary = "unbound";
|
||||
let image_tag = format!("dnssec-tests-{binary}");
|
||||
|
||||
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
let dockerfile_path = manifest_dir
|
||||
.join("docker")
|
||||
.join(format!("{binary}.Dockerfile"));
|
||||
let docker_dir_path = manifest_dir.join("docker");
|
||||
let image_tag = format!("{PACKAGE_NAME}-{implementation}");
|
||||
|
||||
let mut command = Command::new("docker");
|
||||
command
|
||||
.args(["build", "-t"])
|
||||
.arg(&image_tag)
|
||||
.arg("-f")
|
||||
.arg(dockerfile_path)
|
||||
.arg(docker_dir_path);
|
||||
.arg(docker_build_dir);
|
||||
|
||||
ONCE.call_once(|| {
|
||||
let status = command.status().unwrap();
|
||||
assert!(status.success());
|
||||
implementation.once().call_once(|| {
|
||||
let output = command.output().unwrap();
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"--- STDOUT ---\n{stdout}\n--- STDERR ---\n{stderr}"
|
||||
);
|
||||
});
|
||||
|
||||
let mut command = Command::new("docker");
|
||||
let pid = process::id();
|
||||
let count = COUNT.fetch_add(1, atomic::Ordering::Relaxed);
|
||||
let name = format!("{binary}-{pid}-{count}");
|
||||
let count = container_count();
|
||||
let name = format!("{PACKAGE_NAME}-{implementation}-{pid}-{count}");
|
||||
command
|
||||
.args(["run", "--rm", "--detach", "--name", &name])
|
||||
.arg("-it")
|
||||
|
@ -62,10 +58,13 @@ impl Container {
|
|||
|
||||
let ipv4_addr = get_ipv4_addr(&id)?;
|
||||
|
||||
Ok(Self {
|
||||
let inner = Inner {
|
||||
id,
|
||||
name,
|
||||
ipv4_addr,
|
||||
};
|
||||
Ok(Self {
|
||||
inner: Arc::new(inner),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -76,7 +75,7 @@ impl Container {
|
|||
fs::write(&mut temp_file, file_contents)?;
|
||||
|
||||
let src_path = temp_file.path().display().to_string();
|
||||
let dest_path = format!("{}:{path_in_container}", self.id);
|
||||
let dest_path = format!("{}:{path_in_container}", self.inner.id);
|
||||
|
||||
let mut command = Command::new("docker");
|
||||
command.args(["cp", &src_path, &dest_path]);
|
||||
|
@ -91,7 +90,7 @@ impl Container {
|
|||
pub fn output(&self, command_and_args: &[&str]) -> Result<Output> {
|
||||
let mut command = Command::new("docker");
|
||||
command
|
||||
.args(["exec", "-t", &self.id])
|
||||
.args(["exec", "-t", &self.inner.id])
|
||||
.args(command_and_args);
|
||||
|
||||
command.output()?.try_into()
|
||||
|
@ -100,12 +99,18 @@ impl Container {
|
|||
/// Similar to `Self::output` but checks `command_and_args` ran successfully and only
|
||||
/// returns the stdout
|
||||
pub fn stdout(&self, command_and_args: &[&str]) -> Result<String> {
|
||||
let output = self.output(command_and_args)?;
|
||||
let Output {
|
||||
status,
|
||||
stderr,
|
||||
stdout,
|
||||
} = self.output(command_and_args)?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(output.stdout)
|
||||
if status.success() {
|
||||
Ok(stdout)
|
||||
} else {
|
||||
Err(format!("[{}] `{command_and_args:?}` failed", self.name).into())
|
||||
eprintln!("STDOUT:\n{stdout}\nSTDERR:\n{stderr}");
|
||||
|
||||
Err(format!("[{}] `{command_and_args:?}` failed", self.inner.name).into())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -113,7 +118,7 @@ impl Container {
|
|||
pub fn status(&self, command_and_args: &[&str]) -> Result<ExitStatus> {
|
||||
let mut command = Command::new("docker");
|
||||
command
|
||||
.args(["exec", "-t", &self.id])
|
||||
.args(["exec", "-t", &self.inner.id])
|
||||
.args(command_and_args);
|
||||
|
||||
Ok(command.status()?)
|
||||
|
@ -126,19 +131,62 @@ impl Container {
|
|||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("[{}] `{command_and_args:?}` failed", self.name).into())
|
||||
Err(format!("[{}] `{command_and_args:?}` failed", self.inner.name).into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn(&self, cmd: &[&str]) -> Result<Child> {
|
||||
let mut command = Command::new("docker");
|
||||
command.args(["exec", "-t", &self.id]).args(cmd);
|
||||
command.stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||
command.args(["exec", "-t", &self.inner.id]).args(cmd);
|
||||
|
||||
Ok(command.spawn()?)
|
||||
let inner = command.spawn()?;
|
||||
Ok(Child {
|
||||
inner: Some(inner),
|
||||
_container: self.inner.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn ipv4_addr(&self) -> Ipv4Addr {
|
||||
self.ipv4_addr
|
||||
self.inner.ipv4_addr
|
||||
}
|
||||
}
|
||||
|
||||
fn container_count() -> usize {
|
||||
static COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
COUNT.fetch_add(1, atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
struct Inner {
|
||||
name: String,
|
||||
id: String,
|
||||
// TODO probably also want the IPv6 address
|
||||
ipv4_addr: Ipv4Addr,
|
||||
}
|
||||
|
||||
/// NOTE unlike `std::process::Child`, the drop implementation of this type will `kill` the
|
||||
/// child process
|
||||
// this wrapper over `std::process::Child` stores a reference to the container the child process
|
||||
// runs inside of, to prevent the scenario of the container being destroyed _before_
|
||||
// the child is killed
|
||||
pub struct Child {
|
||||
inner: Option<process::Child>,
|
||||
_container: Arc<Inner>,
|
||||
}
|
||||
|
||||
impl Child {
|
||||
pub fn wait(mut self) -> Result<Output> {
|
||||
let output = self.inner.take().expect("unreachable").wait_with_output()?;
|
||||
output.try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Child {
|
||||
fn drop(&mut self) {
|
||||
if let Some(mut inner) = self.inner.take() {
|
||||
let _ = inner.kill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -200,8 +248,8 @@ fn get_ipv4_addr(container_id: &str) -> Result<Ipv4Addr> {
|
|||
Ok(ipv4_addr.parse()?)
|
||||
}
|
||||
|
||||
// ensure the container gets deleted
|
||||
impl Drop for Container {
|
||||
// this ensures the container gets deleted and does not linger after the test runner process ends
|
||||
impl Drop for Inner {
|
||||
fn drop(&mut self) {
|
||||
// running this to completion would block the current thread for several seconds so just
|
||||
// fire and forget
|
||||
|
@ -219,7 +267,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn run_works() -> Result<()> {
|
||||
let container = Container::run()?;
|
||||
let container = Container::run(Implementation::Unbound)?;
|
||||
|
||||
let output = container.output(&["true"])?;
|
||||
assert!(output.status.success());
|
||||
|
@ -229,7 +277,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn ipv4_addr_works() -> Result<()> {
|
||||
let container = Container::run()?;
|
||||
let container = Container::run(Implementation::Unbound)?;
|
||||
let ipv4_addr = container.ipv4_addr();
|
||||
|
||||
let output = container.output(&["ping", "-c1", &format!("{ipv4_addr}")])?;
|
||||
|
@ -240,7 +288,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn cp_works() -> Result<()> {
|
||||
let container = Container::run()?;
|
||||
let container = Container::run(Implementation::Unbound)?;
|
||||
|
||||
let path = "/tmp/somefile";
|
||||
let contents = "hello";
|
8
packages/dns-test/src/docker/hickory.Dockerfile
Normal file
8
packages/dns-test/src/docker/hickory.Dockerfile
Normal file
|
@ -0,0 +1,8 @@
|
|||
FROM rust:1-slim-bookworm
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
tshark
|
||||
|
||||
RUN cargo install hickory-dns --version 0.24.0 --features recursor --debug
|
||||
env RUST_LOG=debug
|
13
packages/dns-test/src/docker/unbound.Dockerfile
Normal file
13
packages/dns-test/src/docker/unbound.Dockerfile
Normal file
|
@ -0,0 +1,13 @@
|
|||
FROM debian:bookworm-slim
|
||||
|
||||
# dnsutils = dig & delv
|
||||
# iputils-ping = ping
|
||||
# ldns-utils = ldns-{key2ds,keygen,signzone}
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
dnsutils \
|
||||
iputils-ping \
|
||||
ldnsutils \
|
||||
nsd \
|
||||
tshark \
|
||||
unbound
|
76
packages/dns-test/src/lib.rs
Normal file
76
packages/dns-test/src/lib.rs
Normal file
|
@ -0,0 +1,76 @@
|
|||
//! A test framework for all things DNS
|
||||
|
||||
use core::fmt;
|
||||
use std::sync::Once;
|
||||
|
||||
pub use crate::fqdn::FQDN;
|
||||
pub use crate::resolver::Resolver;
|
||||
pub use crate::trust_anchor::TrustAnchor;
|
||||
|
||||
pub type Error = Box<dyn std::error::Error>;
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
|
||||
pub mod client;
|
||||
mod container;
|
||||
mod fqdn;
|
||||
pub mod name_server;
|
||||
pub mod record;
|
||||
mod resolver;
|
||||
mod trust_anchor;
|
||||
pub mod zone_file;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Implementation {
|
||||
Unbound,
|
||||
Hickory,
|
||||
}
|
||||
|
||||
impl Implementation {
|
||||
fn dockerfile(&self) -> &'static str {
|
||||
match self {
|
||||
Implementation::Unbound => include_str!("docker/unbound.Dockerfile"),
|
||||
Implementation::Hickory => include_str!("docker/hickory.Dockerfile"),
|
||||
}
|
||||
}
|
||||
|
||||
fn once(&self) -> &'static Once {
|
||||
match self {
|
||||
Implementation::Unbound => {
|
||||
static UNBOUND_ONCE: Once = Once::new();
|
||||
&UNBOUND_ONCE
|
||||
}
|
||||
Implementation::Hickory => {
|
||||
static HICKORY_ONCE: Once = Once::new();
|
||||
&HICKORY_ONCE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Implementation {
|
||||
fn default() -> Self {
|
||||
Self::Unbound
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Implementation {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let s = match self {
|
||||
Implementation::Unbound => "unbound",
|
||||
Implementation::Hickory => "hickory",
|
||||
};
|
||||
f.write_str(s)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subject() -> Implementation {
|
||||
if let Ok(subject) = std::env::var("DNS_TEST_SUBJECT") {
|
||||
match subject.as_str() {
|
||||
"hickory" => Implementation::Hickory,
|
||||
"unbound" => Implementation::Unbound,
|
||||
_ => panic!("unknown implementation: {subject}"),
|
||||
}
|
||||
} else {
|
||||
Implementation::default()
|
||||
}
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
use core::sync::atomic::{self, AtomicUsize};
|
||||
use std::net::Ipv4Addr;
|
||||
use std::process::Child;
|
||||
|
||||
use crate::container::Container;
|
||||
use crate::container::{Child, Container};
|
||||
use crate::zone_file::{self, SoaSettings, ZoneFile, DNSKEY, DS};
|
||||
use crate::{Result, FQDN};
|
||||
use crate::{Implementation, Result, FQDN};
|
||||
|
||||
pub struct NameServer<'a, State> {
|
||||
container: Container,
|
||||
|
@ -43,7 +42,7 @@ impl<'a> NameServer<'a, Stopped> {
|
|||
});
|
||||
|
||||
Ok(Self {
|
||||
container: Container::run()?,
|
||||
container: Container::run(Implementation::Unbound)?,
|
||||
zone_file,
|
||||
state: Stopped,
|
||||
})
|
||||
|
@ -210,6 +209,31 @@ impl<'a> NameServer<'a, Signed> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> NameServer<'a, Running> {
|
||||
/// gracefully terminates the name server collecting all logs
|
||||
pub fn terminate(self) -> Result<String> {
|
||||
let pidfile = "/run/nsd/nsd.pid";
|
||||
// if `terminate` is called right after `start` NSD may not have had the chance to create
|
||||
// the PID file so if it doesn't exist wait for a bit before invoking `kill`
|
||||
let kill = format!(
|
||||
"test -f {pidfile} || sleep 1
|
||||
kill -TERM $(cat {pidfile})"
|
||||
);
|
||||
self.container.status_ok(&["sh", "-c", &kill])?;
|
||||
let output = self.state.child.wait()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err("could not terminate the `unbound` process".into());
|
||||
}
|
||||
|
||||
assert!(
|
||||
output.stderr.is_empty(),
|
||||
"stderr should be returned if not empty"
|
||||
);
|
||||
Ok(output.stdout)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, S> NameServer<'a, S> {
|
||||
pub fn ipv4_addr(&self) -> Ipv4Addr {
|
||||
self.container.ipv4_addr()
|
||||
|
@ -242,12 +266,6 @@ pub struct Running {
|
|||
child: Child,
|
||||
}
|
||||
|
||||
impl Drop for Running {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.child.kill();
|
||||
}
|
||||
}
|
||||
|
||||
fn primary_ns(ns_count: usize) -> FQDN<'static> {
|
||||
FQDN(format!("primary{ns_count}.nameservers.com.")).unwrap()
|
||||
}
|
||||
|
@ -352,4 +370,14 @@ mod tests {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminate_works() -> Result<()> {
|
||||
let ns = NameServer::new(FQDN::ROOT)?.start()?;
|
||||
let logs = ns.terminate()?;
|
||||
|
||||
assert!(logs.contains("nsd starting"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
107
packages/dns-test/src/resolver.rs
Normal file
107
packages/dns-test/src/resolver.rs
Normal file
|
@ -0,0 +1,107 @@
|
|||
use core::fmt::Write;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use crate::container::{Child, Container};
|
||||
use crate::trust_anchor::TrustAnchor;
|
||||
use crate::zone_file::Root;
|
||||
use crate::{Implementation, Result};
|
||||
|
||||
pub struct Resolver {
|
||||
container: Container,
|
||||
child: Child,
|
||||
}
|
||||
|
||||
impl Resolver {
|
||||
pub fn start(
|
||||
implementation: Implementation,
|
||||
roots: &[Root],
|
||||
trust_anchor: &TrustAnchor,
|
||||
) -> Result<Self> {
|
||||
const TRUST_ANCHOR_FILE: &str = "/etc/trusted-key.key";
|
||||
|
||||
let container = Container::run(implementation)?;
|
||||
|
||||
let mut hints = String::new();
|
||||
for root in roots {
|
||||
writeln!(hints, "{root}").unwrap();
|
||||
}
|
||||
|
||||
let use_dnssec = !trust_anchor.is_empty();
|
||||
match implementation {
|
||||
Implementation::Unbound => {
|
||||
container.cp("/etc/unbound/root.hints", &hints)?;
|
||||
|
||||
container.cp("/etc/unbound/unbound.conf", &unbound_conf(use_dnssec))?;
|
||||
}
|
||||
|
||||
Implementation::Hickory => {
|
||||
container.status_ok(&["mkdir", "-p", "/etc/hickory"])?;
|
||||
|
||||
container.cp("/etc/hickory/root.hints", &hints)?;
|
||||
|
||||
container.cp("/etc/named.toml", &hickory_conf(use_dnssec))?;
|
||||
}
|
||||
}
|
||||
|
||||
if use_dnssec {
|
||||
container.cp(TRUST_ANCHOR_FILE, &trust_anchor.to_string())?;
|
||||
}
|
||||
|
||||
let command: &[_] = match implementation {
|
||||
Implementation::Unbound => &["unbound", "-d"],
|
||||
Implementation::Hickory => &["hickory-dns", "-d"],
|
||||
};
|
||||
let child = container.spawn(command)?;
|
||||
|
||||
Ok(Self { child, container })
|
||||
}
|
||||
|
||||
pub fn ipv4_addr(&self) -> Ipv4Addr {
|
||||
self.container.ipv4_addr()
|
||||
}
|
||||
|
||||
/// gracefully terminates the name server collecting all logs
|
||||
pub fn terminate(self) -> Result<String> {
|
||||
let pidfile = "/run/unbound.pid";
|
||||
let kill = format!(
|
||||
"test -f {pidfile} || sleep 1
|
||||
kill -TERM $(cat {pidfile})"
|
||||
);
|
||||
self.container.status_ok(&["sh", "-c", &kill])?;
|
||||
let output = self.child.wait()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err("could not terminate the `unbound` process".into());
|
||||
}
|
||||
|
||||
assert!(
|
||||
output.stderr.is_empty(),
|
||||
"stderr should be returned if not empty"
|
||||
);
|
||||
Ok(output.stdout)
|
||||
}
|
||||
}
|
||||
|
||||
fn unbound_conf(use_dnssec: bool) -> String {
|
||||
minijinja::render!(include_str!("templates/unbound.conf.jinja"), use_dnssec => use_dnssec)
|
||||
}
|
||||
|
||||
fn hickory_conf(use_dnssec: bool) -> String {
|
||||
minijinja::render!(include_str!("templates/hickory.resolver.toml.jinja"), use_dnssec => use_dnssec)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn terminate_works() -> Result<()> {
|
||||
let resolver = Resolver::start(Implementation::Unbound, &[], &TrustAnchor::empty())?;
|
||||
let logs = resolver.terminate()?;
|
||||
|
||||
eprintln!("{logs}");
|
||||
assert!(logs.contains("start of service"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
[[zones]]
|
||||
zone = "."
|
||||
zone_type = "Hint"
|
||||
stores = { type = "recursor", roots = "/etc/hickory/root.hints", ns_cache_size = 1024, record_cache_size = 1048576 }
|
||||
enable_dnssec = {{ use_dnssec }}
|
51
packages/dns-test/src/trust_anchor.rs
Normal file
51
packages/dns-test/src/trust_anchor.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
use core::fmt;
|
||||
|
||||
use crate::zone_file::DNSKEY;
|
||||
|
||||
pub struct TrustAnchor {
|
||||
keys: Vec<DNSKEY>,
|
||||
}
|
||||
|
||||
impl TrustAnchor {
|
||||
pub fn empty() -> Self {
|
||||
Self { keys: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.keys.is_empty()
|
||||
}
|
||||
|
||||
pub fn add(&mut self, key: DNSKEY) -> &mut Self {
|
||||
self.keys.push(key);
|
||||
self
|
||||
}
|
||||
|
||||
/// formats the `TrustAnchor` in the format `delv` expects
|
||||
pub(super) fn delv(&self) -> String {
|
||||
let mut buf = "trust-anchors {".to_string();
|
||||
|
||||
for key in &self.keys {
|
||||
buf.push_str(&key.delv());
|
||||
}
|
||||
|
||||
buf.push_str("};");
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TrustAnchor {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for key in &self.keys {
|
||||
writeln!(f, "{key}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<DNSKEY> for TrustAnchor {
|
||||
fn from_iter<T: IntoIterator<Item = DNSKEY>>(iter: T) -> Self {
|
||||
Self {
|
||||
keys: iter.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -165,6 +165,20 @@ impl DNSKEY {
|
|||
pub fn key_tag(&self) -> u16 {
|
||||
self.key_tag
|
||||
}
|
||||
|
||||
/// formats the `DNSKEY` in the format `delv` expects
|
||||
pub(super) fn delv(&self) -> String {
|
||||
let Self {
|
||||
zone,
|
||||
flags,
|
||||
protocol,
|
||||
algorithm,
|
||||
public_key,
|
||||
..
|
||||
} = self;
|
||||
|
||||
format!("{zone} static-key {flags} {protocol} {algorithm} \"{public_key}\";\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for DNSKEY {
|
13
src/lib.rs
13
src/lib.rs
|
@ -1,13 +0,0 @@
|
|||
pub use crate::fqdn::FQDN;
|
||||
pub use crate::recursive_resolver::RecursiveResolver;
|
||||
|
||||
pub type Error = Box<dyn std::error::Error>;
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
|
||||
pub mod client;
|
||||
mod container;
|
||||
mod fqdn;
|
||||
pub mod name_server;
|
||||
pub mod record;
|
||||
mod recursive_resolver;
|
||||
pub mod zone_file;
|
|
@ -1,236 +0,0 @@
|
|||
use core::fmt::Write;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::process::Child;
|
||||
|
||||
use crate::container::Container;
|
||||
use crate::zone_file::{Root, DNSKEY};
|
||||
use crate::Result;
|
||||
|
||||
pub struct RecursiveResolver {
|
||||
container: Container,
|
||||
child: Child,
|
||||
}
|
||||
|
||||
impl RecursiveResolver {
|
||||
pub fn start(roots: &[Root], trust_anchors: &[DNSKEY]) -> Result<Self> {
|
||||
const TRUST_ANCHOR_FILE: &str = "/etc/trusted-key.key";
|
||||
|
||||
let container = Container::run()?;
|
||||
|
||||
let mut hints = String::new();
|
||||
for root in roots {
|
||||
writeln!(hints, "{root}").unwrap();
|
||||
}
|
||||
|
||||
container.cp("/etc/unbound/root.hints", &hints)?;
|
||||
|
||||
let use_dnssec = !trust_anchors.is_empty();
|
||||
container.cp("/etc/unbound/unbound.conf", &unbound_conf(use_dnssec))?;
|
||||
|
||||
if use_dnssec {
|
||||
let trust_anchor = trust_anchors.iter().fold(String::new(), |mut buf, ds| {
|
||||
writeln!(buf, "{ds}").expect("infallible");
|
||||
buf
|
||||
});
|
||||
|
||||
container.cp(TRUST_ANCHOR_FILE, &trust_anchor)?;
|
||||
}
|
||||
|
||||
let child = container.spawn(&["unbound", "-d"])?;
|
||||
|
||||
Ok(Self { child, container })
|
||||
}
|
||||
|
||||
pub fn ipv4_addr(&self) -> Ipv4Addr {
|
||||
self.container.ipv4_addr()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RecursiveResolver {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.child.kill();
|
||||
}
|
||||
}
|
||||
|
||||
fn unbound_conf(use_dnssec: bool) -> String {
|
||||
minijinja::render!(include_str!("templates/unbound.conf.jinja"), use_dnssec => use_dnssec)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use crate::{
|
||||
client::{Client, Dnssec, Recurse},
|
||||
name_server::NameServer,
|
||||
record::RecordType,
|
||||
FQDN,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn can_resolve() -> Result<()> {
|
||||
let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4);
|
||||
let needle = FQDN("example.nameservers.com.")?;
|
||||
|
||||
let mut root_ns = NameServer::new(FQDN::ROOT)?;
|
||||
let mut com_ns = NameServer::new(FQDN::COM)?;
|
||||
|
||||
let mut nameservers_ns = NameServer::new(FQDN("nameservers.com.")?)?;
|
||||
nameservers_ns
|
||||
.a(root_ns.fqdn().clone(), root_ns.ipv4_addr())
|
||||
.a(com_ns.fqdn().clone(), com_ns.ipv4_addr())
|
||||
.a(needle.clone(), expected_ipv4_addr);
|
||||
let nameservers_ns = nameservers_ns.start()?;
|
||||
|
||||
eprintln!("nameservers.com.zone:\n{}", nameservers_ns.zone_file());
|
||||
|
||||
com_ns.referral(
|
||||
nameservers_ns.zone().clone(),
|
||||
nameservers_ns.fqdn().clone(),
|
||||
nameservers_ns.ipv4_addr(),
|
||||
);
|
||||
let com_ns = com_ns.start()?;
|
||||
|
||||
eprintln!("com.zone:\n{}", com_ns.zone_file());
|
||||
|
||||
root_ns.referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr());
|
||||
let root_ns = root_ns.start()?;
|
||||
|
||||
eprintln!("root.zone:\n{}", root_ns.zone_file());
|
||||
|
||||
let roots = &[Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr())];
|
||||
let resolver = RecursiveResolver::start(roots, &[])?;
|
||||
let resolver_ip_addr = resolver.ipv4_addr();
|
||||
|
||||
let client = Client::new()?;
|
||||
let output = client.dig(
|
||||
Recurse::Yes,
|
||||
Dnssec::No,
|
||||
resolver_ip_addr,
|
||||
RecordType::A,
|
||||
&needle,
|
||||
)?;
|
||||
|
||||
assert!(output.status.is_noerror());
|
||||
|
||||
let [answer] = output.answer.try_into().unwrap();
|
||||
let a = answer.try_into_a().unwrap();
|
||||
|
||||
assert_eq!(needle, a.fqdn);
|
||||
assert_eq!(expected_ipv4_addr, a.ipv4_addr);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// no DS records are involved; this is a single-link chain of trust
|
||||
#[test]
|
||||
fn can_validate_without_delegation() -> Result<()> {
|
||||
let mut ns = NameServer::new(FQDN::ROOT)?;
|
||||
ns.a(ns.fqdn().clone(), ns.ipv4_addr());
|
||||
let ns = ns.sign()?;
|
||||
|
||||
let root_ksk = ns.key_signing_key().clone();
|
||||
let root_zsk = ns.zone_signing_key().clone();
|
||||
|
||||
eprintln!("root.zone.signed:\n{}", ns.signed_zone_file());
|
||||
|
||||
let ns = ns.start()?;
|
||||
|
||||
eprintln!("root.zone:\n{}", ns.zone_file());
|
||||
|
||||
let roots = &[Root::new(ns.fqdn().clone(), ns.ipv4_addr())];
|
||||
|
||||
let trust_anchor = [root_ksk.clone(), root_zsk.clone()];
|
||||
let resolver = RecursiveResolver::start(roots, &trust_anchor)?;
|
||||
let resolver_addr = resolver.ipv4_addr();
|
||||
|
||||
let client = Client::new()?;
|
||||
let output = client.dig(
|
||||
Recurse::Yes,
|
||||
Dnssec::Yes,
|
||||
resolver_addr,
|
||||
RecordType::SOA,
|
||||
&FQDN::ROOT,
|
||||
)?;
|
||||
|
||||
assert!(output.status.is_noerror());
|
||||
assert!(output.flags.authenticated_data);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_validate_with_delegation() -> Result<()> {
|
||||
let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4);
|
||||
let needle = FQDN("example.nameservers.com.")?;
|
||||
|
||||
let mut root_ns = NameServer::new(FQDN::ROOT)?;
|
||||
let mut com_ns = NameServer::new(FQDN::COM)?;
|
||||
|
||||
let mut nameservers_ns = NameServer::new(FQDN("nameservers.com.")?)?;
|
||||
nameservers_ns
|
||||
.a(root_ns.fqdn().clone(), root_ns.ipv4_addr())
|
||||
.a(com_ns.fqdn().clone(), com_ns.ipv4_addr())
|
||||
.a(needle.clone(), expected_ipv4_addr);
|
||||
let nameservers_ns = nameservers_ns.sign()?;
|
||||
let nameservers_ds = nameservers_ns.ds().clone();
|
||||
let nameservers_ns = nameservers_ns.start()?;
|
||||
|
||||
eprintln!("nameservers.com.zone:\n{}", nameservers_ns.zone_file());
|
||||
|
||||
com_ns
|
||||
.referral(
|
||||
nameservers_ns.zone().clone(),
|
||||
nameservers_ns.fqdn().clone(),
|
||||
nameservers_ns.ipv4_addr(),
|
||||
)
|
||||
.ds(nameservers_ds);
|
||||
let com_ns = com_ns.sign()?;
|
||||
let com_ds = com_ns.ds().clone();
|
||||
let com_ns = com_ns.start()?;
|
||||
|
||||
eprintln!("com.zone:\n{}", com_ns.zone_file());
|
||||
|
||||
root_ns
|
||||
.referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr())
|
||||
.ds(com_ds);
|
||||
let root_ns = root_ns.sign()?;
|
||||
let root_ksk = root_ns.key_signing_key().clone();
|
||||
let root_zsk = root_ns.zone_signing_key().clone();
|
||||
|
||||
eprintln!("root.zone.signed:\n{}", root_ns.signed_zone_file());
|
||||
|
||||
let root_ns = root_ns.start()?;
|
||||
|
||||
eprintln!("root.zone:\n{}", root_ns.zone_file());
|
||||
|
||||
let roots = &[Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr())];
|
||||
|
||||
let resolver = RecursiveResolver::start(roots, &[root_ksk.clone(), root_zsk.clone()])?;
|
||||
let resolver_ip_addr = resolver.ipv4_addr();
|
||||
|
||||
let client = Client::new()?;
|
||||
let output = client.dig(
|
||||
Recurse::Yes,
|
||||
Dnssec::Yes,
|
||||
resolver_ip_addr,
|
||||
RecordType::A,
|
||||
&needle,
|
||||
)?;
|
||||
|
||||
drop(resolver);
|
||||
|
||||
assert!(output.status.is_noerror());
|
||||
|
||||
assert!(output.flags.authenticated_data);
|
||||
|
||||
let [a, _rrsig] = output.answer.try_into().unwrap();
|
||||
let a = a.try_into_a().unwrap();
|
||||
|
||||
assert_eq!(needle, a.fqdn);
|
||||
assert_eq!(expected_ipv4_addr, a.ipv4_addr);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user