From a438d7c5e12f41323101d3f326d854bcb565c41e Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 29 Jan 2024 17:08:18 +0100 Subject: [PATCH 001/124] initial commit --- .gitignore | 1 + Cargo.lock | 7 +++ Cargo.toml | 9 ++++ README.md | 89 +++++++++++++++++++++++++++++++++++++++ docker/nsd.Dockerfile | 4 ++ docker/unbound.Dockerfile | 4 ++ src/lib.rs | 14 ++++++ 7 files changed, 128 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 docker/nsd.Dockerfile create mode 100644 docker/unbound.Dockerfile create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ea8c4bf7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..426b416d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "dnssec-tests" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..2b8c6086 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "dnssec-tests" +version = "0.1.0" +edition = "2021" +license = "MIT or Apache 2.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/README.md b/README.md new file mode 100644 index 00000000..01565049 --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +# `dnssec-tests` + +Test infrastructure for DNSSEC conformance tests. + +## Design goals + +- Test MUST not depend on external services like `1.1.1.1` or `8.8.8.8` + - rationale: it must be possible to run tests locally, without internet access +- All nodes in the network must not be the subject under test. + - rationale: test inter-operability with other software like `unbound` and `nsd` +- All test input must be local files or constants + - rationale: tests are self-contained +- + +## Minimally working DNSSEC-enabled network + +- `.` domain + - name server: `nsd` (`my.root-server.com`) +- TLD domain (`com.`) + - name server: `nsd` (`ns.com`) +- target domain (`example.com.`) + - name server: `nsd` (`ns.example.com`) +- recursive resolver: `unbound` + - configured to use `my.root-server.com` as root server + - configured with a trust anchor: the public key of `my.root-server.com` + +each name server has +- a zone signing key pair +- a key signing key pair +- signed zone files + +### exploration + +#### `nsd` for root name server + +run: `nsd -d` + +- `/etc/nsd/nsd.conf` + +``` text +remote-control: + control-enable: no + +zone: + name: . + zonefile: /etc/nsd/zones/root.zone +``` + +- `/etc/nsd/zones/root.zone` + +``` text +$ORIGIN . +$TTL 1800 +@ IN SOA primary.root-server.com. admin.root-server.com. ( + 2014080301 + 3600 + 900 + 1209600 + 1800 + ) + +``` + +#### `unbound` + +run `unbound -d` + +- `/etc/unbound/unbound.conf` + +ideally instead of `0.0.0.0`, it should only cover the `docker0` network interface. or disable docker containers' access to the internet + +``` text +server: + verbosity: 4 + use-syslog: no + interface: 0.0.0.0 + access-control: 172.17.0.0/16 allow + root-hints: /etc/unbound/root.hints + +remote-control: + control-enable: no +``` + +- `/etc/unbound/root.hints`. NOTE IP address of docker container + +``` text +. 3600000 NS MY.ROOT-SERVERS.NET. +MY.ROOT-SERVERS.NET. 3600000 A 172.17.0.2 +``` diff --git a/docker/nsd.Dockerfile b/docker/nsd.Dockerfile new file mode 100644 index 00000000..a758bdeb --- /dev/null +++ b/docker/nsd.Dockerfile @@ -0,0 +1,4 @@ +FROM ubuntu:22.04 + +RUN apt-get update && \ + apt-get install -y nsd \ No newline at end of file diff --git a/docker/unbound.Dockerfile b/docker/unbound.Dockerfile new file mode 100644 index 00000000..45a11b85 --- /dev/null +++ b/docker/unbound.Dockerfile @@ -0,0 +1,4 @@ +FROM ubuntu:22.04 + +RUN apt-get update && \ + apt-get install -y unbound \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..7d12d9af --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} From e5c373b6dacaf3562813442b3c0f0b91d207faf7 Mon Sep 17 00:00:00 2001 From: Sebastian Ziebell Date: Thu, 1 Feb 2024 15:29:35 +0100 Subject: [PATCH 002/124] Expand test setup to run on OSX as well A client based `Dockerfile` is added to run `dig` & `delv` in, to make the setup work on OSX. * set up client container * install additional tools * expand Readme with setup instructions --- README.md | 22 ++++++++++++++++++++-- docker/client.Dockerfile | 4 ++++ docker/nsd.Dockerfile | 2 +- docker/unbound.Dockerfile | 2 +- 4 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 docker/client.Dockerfile diff --git a/README.md b/README.md index 01565049..082fc186 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,24 @@ remote-control: - `/etc/unbound/root.hints`. NOTE IP address of docker container ``` text -. 3600000 NS MY.ROOT-SERVERS.NET. -MY.ROOT-SERVERS.NET. 3600000 A 172.17.0.2 +. 3600000 NS primary.root-server.com. +primary.root-server.com. 3600000 A 172.17.0.2 ``` + +#### `client` + +Container is `docker/client.Dockerfile`, build with: `docker build -t dnssec-tests-client -f docker/client.Dockerfile docker`, with `tshark`. + +Run the client container with extra capabilities + +```shell +docker run --rm -it --cap-add=NET_RAW --cap-add=NET_ADMIN dnssec-tests-client /bin/bash +``` + +Then run `tshark` inside the container: + +```shell +tshark -f 'host 172.17.0.3' -O dns +``` + +to filter DNS messages for host `172.17.0.3` (`unbound`). diff --git a/docker/client.Dockerfile b/docker/client.Dockerfile new file mode 100644 index 00000000..be9c228a --- /dev/null +++ b/docker/client.Dockerfile @@ -0,0 +1,4 @@ +FROM ubuntu:22.04 + +RUN apt-get update && \ + apt-get install -y dnsutils iputils-ping tshark \ No newline at end of file diff --git a/docker/nsd.Dockerfile b/docker/nsd.Dockerfile index a758bdeb..216b0a2b 100644 --- a/docker/nsd.Dockerfile +++ b/docker/nsd.Dockerfile @@ -1,4 +1,4 @@ FROM ubuntu:22.04 RUN apt-get update && \ - apt-get install -y nsd \ No newline at end of file + apt-get install -y nsd iputils-ping \ No newline at end of file diff --git a/docker/unbound.Dockerfile b/docker/unbound.Dockerfile index 45a11b85..e0473477 100644 --- a/docker/unbound.Dockerfile +++ b/docker/unbound.Dockerfile @@ -1,4 +1,4 @@ FROM ubuntu:22.04 RUN apt-get update && \ - apt-get install -y unbound \ No newline at end of file + apt-get install -y unbound iputils \ No newline at end of file From 88afa403a4764d944c303403f6e35d4124d39404 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 1 Feb 2024 16:24:34 +0100 Subject: [PATCH 003/124] add TLD name server instructions --- README.md | 44 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 082fc186..08c23578 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,11 @@ each name server has ### exploration +Notes: + +- run all containers with ` --cap-add=NET_RAW --cap-add=NET_ADMIN` +- use `docker exec` to run `tshark` on network nodes ( containers ) of interest + #### `nsd` for root name server run: `nsd -d` @@ -43,10 +48,10 @@ remote-control: zone: name: . - zonefile: /etc/nsd/zones/root.zone + zonefile: /etc/nsd/zones/main.zone ``` -- `/etc/nsd/zones/root.zone` +- `/etc/nsd/zones/main.zone` ``` text $ORIGIN . @@ -58,9 +63,42 @@ $TTL 1800 1209600 1800 ) +@ IN NS primary.root-server.com. +; referral +com. IN NS primary.tld-server.com. +primary.tld-server.com. IN A 172.17.0.$TLD_NS_IP_ADDRESS ``` +#### `nsd` for the TLD name server + +run: `nsd -d` + +- `/etc/nsd/nsd.conf` + +``` text +remote-control: + control-enable: no + +zone: + name: . + zonefile: /etc/nsd/zones/main.zone +``` + +- `/etc/nsd/zones/main.zone` + +``` text +$ORIGIN com. +$TTL 1800 +@ IN SOA primary.tld-server.com. admin.tld-server.com. ( + 2014080301 + 3600 + 900 + 1209600 + 1800 + ) +@ IN NS primary.tld-server.com. +``` #### `unbound` run `unbound -d` @@ -85,7 +123,7 @@ remote-control: ``` text . 3600000 NS primary.root-server.com. -primary.root-server.com. 3600000 A 172.17.0.2 +primary.root-server.com. 3600000 A 172.17.0.$ROOT_NS_IP_ADDRESS ``` #### `client` From 907f40923dbac797d6d1a0503695d9ee7e00f5b1 Mon Sep 17 00:00:00 2001 From: Sebastian Ziebell Date: Thu, 1 Feb 2024 16:24:39 +0100 Subject: [PATCH 004/124] Prepare containers to work on OSX * copy config files into unbound container * install vim & tshark in all containers --- docker/client.Dockerfile | 2 +- docker/files/etc/unbound/root.hints | 2 ++ docker/files/etc/unbound/unbound.conf | 9 +++++++++ docker/nsd.Dockerfile | 2 +- docker/unbound.Dockerfile | 5 ++++- 5 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 docker/files/etc/unbound/root.hints create mode 100644 docker/files/etc/unbound/unbound.conf diff --git a/docker/client.Dockerfile b/docker/client.Dockerfile index be9c228a..e05b7249 100644 --- a/docker/client.Dockerfile +++ b/docker/client.Dockerfile @@ -1,4 +1,4 @@ FROM ubuntu:22.04 RUN apt-get update && \ - apt-get install -y dnsutils iputils-ping tshark \ No newline at end of file + apt-get install -y dnsutils iputils-ping tshark vim \ No newline at end of file diff --git a/docker/files/etc/unbound/root.hints b/docker/files/etc/unbound/root.hints new file mode 100644 index 00000000..5516fe71 --- /dev/null +++ b/docker/files/etc/unbound/root.hints @@ -0,0 +1,2 @@ +. 3600000 NS primary.root-server.com. +primary.root-server.com. 3600000 A 172.17.0.2 diff --git a/docker/files/etc/unbound/unbound.conf b/docker/files/etc/unbound/unbound.conf new file mode 100644 index 00000000..ad446203 --- /dev/null +++ b/docker/files/etc/unbound/unbound.conf @@ -0,0 +1,9 @@ +server: + verbosity: 4 + use-syslog: no + interface: 0.0.0.0 + access-control: 172.17.0.0/16 allow + root-hints: /etc/unbound/root.hints + +remote-control: + control-enable: no diff --git a/docker/nsd.Dockerfile b/docker/nsd.Dockerfile index 216b0a2b..d9441e5a 100644 --- a/docker/nsd.Dockerfile +++ b/docker/nsd.Dockerfile @@ -1,4 +1,4 @@ FROM ubuntu:22.04 RUN apt-get update && \ - apt-get install -y nsd iputils-ping \ No newline at end of file + apt-get install -y nsd iputils-ping tshark vim \ No newline at end of file diff --git a/docker/unbound.Dockerfile b/docker/unbound.Dockerfile index e0473477..78c14dcb 100644 --- a/docker/unbound.Dockerfile +++ b/docker/unbound.Dockerfile @@ -1,4 +1,7 @@ FROM ubuntu:22.04 RUN apt-get update && \ - apt-get install -y unbound iputils \ No newline at end of file + apt-get install -y unbound iputils-ping tshark vim + +COPY ./files/etc/unbound/unbound.conf /etc/unbound/unbound.conf +COPY ./files/etc/unbound/root.hints /etc/unbound/root.hints From 3c50ca911a99067ff49a46c7993c1e64b53f0c32 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 1 Feb 2024 17:19:01 +0100 Subject: [PATCH 005/124] initial Container API --- Cargo.lock | 150 +++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/lib.rs | 203 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 349 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 426b416d..a2e24963 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,156 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "dnssec-tests" version = "0.1.0" +dependencies = [ + "tempfile", +] + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "rustix" +version = "0.38.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +dependencies = [ + "bitflags 2.4.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "tempfile" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" diff --git a/Cargo.toml b/Cargo.toml index 2b8c6086..b1f7ab22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,3 +7,4 @@ license = "MIT or Apache 2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +tempfile = "3.9.0" diff --git a/src/lib.rs b/src/lib.rs index 7d12d9af..2422ee27 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,207 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right +use core::fmt; +use std::process::Output; +use std::sync::atomic; +use std::{ + fs, + path::Path, + process::{Command, ExitStatus, Stdio}, + sync::atomic::AtomicUsize, +}; + +use tempfile::NamedTempFile; + +pub type Error = Box; +pub type Result = core::result::Result; + +pub struct Container { + id: String, + name: String, +} + +impl Container { + /// Starts the container in a "parked" state + pub fn run(image: Image) -> Result { + static COUNT: AtomicUsize = AtomicUsize::new(0); + + let image_tag = format!("dnssec-tests-{image}"); + + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let dockerfile_path = manifest_dir + .join("docker") + .join(format!("{image}.Dockerfile")); + let docker_dir_path = manifest_dir.join("docker"); + dbg!(&image_tag); + + let mut command = Command::new("docker"); + command + .args(&["build", "-t"]) + .arg(&image_tag) + .arg("-f") + .arg(dockerfile_path) + .arg(docker_dir_path); + let status = command.status()?; + + if !status.success() { + return Err(format!("`{command:?}` failed").into()); + } + + // run container based on image + // `docker run --rm -it $IMAGE sleep infinity` + + let mut command = Command::new("docker"); + let container_name = format!("{image}-{}", COUNT.fetch_add(1, atomic::Ordering::Relaxed)); + command.args(&["run", "--rm", "--detach", "--name", &container_name]); + let output = command + .arg("-it") + .arg(image_tag) + .args(["sleep", "infinity"]) + .output()?; + + if !output.status.success() { + return Err(format!("`{command:?}` failed").into()); + } + + let id = core::str::from_utf8(&output.stdout)?.trim().to_string(); + dbg!(&id); + + Ok(Self { + id, + name: container_name, + }) + } + + pub fn cp(&self, path_in_container: &str, file_contents: &str) -> Result<()> { + let mut temp_file = NamedTempFile::new()?; + 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 mut command = Command::new("docker"); + command.args(["cp", &src_path, &dest_path]); + + let status = command.status()?; + if !status.success() { + return Err(format!("`{command:?}` failed").into()); + } + + Ok(()) + } + + pub fn exec(&self, cmd: &[&str]) -> Result { + let mut command = Command::new("docker"); + command.args(&["exec", "-t", &self.id]).args(cmd); + + let output = command.output()?; + + Ok(output) + } + + pub fn ip_addr(&self) -> Result { + let mut command = Command::new("docker"); + command + .args(&[ + "inspect", + "-f", + "{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}", + ]) + .arg(&self.id); + + let output = command.output()?; + if !output.status.success() { + return Err(format!("`{command:?}` failed").into()); + } + + let ip_addr = core::str::from_utf8(&output.stdout)?.trim().to_string(); + dbg!(&ip_addr); + + Ok(ip_addr) + } +} + +// ensure the container gets deleted +impl Drop for Container { + fn drop(&mut self) { + // running this to completion would block the current thread for several seconds so just + // fire and forget + let _ = Command::new("docker") + .args(["rm", "-f", &self.id]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + } +} + +pub enum Image { + Nsd, // for ROOT, TLD, DOMAIN + Unbound, + Client, +} + +impl fmt::Display for Image { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match self { + Image::Nsd => "nsd", + Image::Unbound => "unbound", + Image::Client => "client", + }; + f.write_str(name) + } } #[cfg(test)] mod tests { + use std::net::Ipv4Addr; + use super::*; #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); + fn run_works() -> Result<()> { + let container = Container::run(Image::Client)?; + + let output = container.exec(&["true"])?; + assert!(output.status.success()); + + Ok(()) + } + + #[test] + fn ip_addr_works() -> Result<()> { + let container = Container::run(Image::Client)?; + + let ip_addr = container.ip_addr()?; + assert!(ip_addr.parse::().is_ok()); + + Ok(()) + } + + #[test] + fn cp_works() -> Result<()> { + let container = Container::run(Image::Client)?; + + let path = "/tmp/somefile"; + let contents = "hello"; + container.cp(path, contents)?; + + let output = container.exec(&["cat", path])?; + dbg!(&output); + + assert!(output.status.success()); + + assert_eq!(contents, core::str::from_utf8(&output.stdout)?); + + Ok(()) + } + + #[ignore = "TODO"] + #[test] + fn tld_setup() -> Result<()> { + let container = Container::run(Image::Nsd)?; + + container.cp("/etc/nsd/zones/main.zone", "TODO")?; + + container.exec(&["nsd", "-d"])?; + + Ok(()) } } From bc10cda9cc353bdbff06f7511c95248ce7c2834c Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 1 Feb 2024 18:06:05 +0100 Subject: [PATCH 006/124] WIP root & tld name server setup --- Cargo.lock | 65 +++++++++++++++++++ Cargo.toml | 1 + README.md | 12 ++-- docker/nsd.Dockerfile | 2 +- docker/unbound.Dockerfile | 1 - src/lib.rs | 65 +++++++++++++++++-- src/templates/nsd.conf.jinja | 6 ++ .../templates/root.hints.jinja | 2 +- src/templates/root.zone.jinja | 12 ++++ src/templates/tld.zone.jinja | 10 +++ 10 files changed, 160 insertions(+), 16 deletions(-) create mode 100644 src/templates/nsd.conf.jinja rename docker/files/etc/unbound/root.hints => src/templates/root.hints.jinja (50%) create mode 100644 src/templates/root.zone.jinja create mode 100644 src/templates/tld.zone.jinja diff --git a/Cargo.lock b/Cargo.lock index a2e24963..9b6bb82d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" name = "dnssec-tests" version = "0.1.0" dependencies = [ + "minijinja", "tempfile", ] @@ -55,6 +56,33 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +[[package]] +name = "minijinja" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe0ff215195a22884d867b547c70a0c4815cbbcc70991f281dca604b20d10ce" +dependencies = [ + "serde", +] + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -77,6 +105,37 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "serde" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "tempfile" version = "3.9.0" @@ -90,6 +149,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index b1f7ab22..91297a19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,4 +7,5 @@ license = "MIT or Apache 2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +minijinja = "1.0.12" tempfile = "3.9.0" diff --git a/README.md b/README.md index 08c23578..8e320a06 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ remote-control: control-enable: no zone: - name: . + name: main zonefile: /etc/nsd/zones/main.zone ``` @@ -91,11 +91,11 @@ zone: $ORIGIN com. $TTL 1800 @ IN SOA primary.tld-server.com. admin.tld-server.com. ( - 2014080301 - 3600 - 900 - 1209600 - 1800 + 2014010100 ; Serial + 10800 ; Refresh (3 hours) + 900 ; Retry (15 minutes) + 604800 ; Expire (1 week) + 86400 ; Minimum (1 day) ) @ IN NS primary.tld-server.com. ``` diff --git a/docker/nsd.Dockerfile b/docker/nsd.Dockerfile index d9441e5a..2284ebb8 100644 --- a/docker/nsd.Dockerfile +++ b/docker/nsd.Dockerfile @@ -1,4 +1,4 @@ FROM ubuntu:22.04 RUN apt-get update && \ - apt-get install -y nsd iputils-ping tshark vim \ No newline at end of file + apt-get install -y nsd iputils-ping tshark vim diff --git a/docker/unbound.Dockerfile b/docker/unbound.Dockerfile index 78c14dcb..67e89404 100644 --- a/docker/unbound.Dockerfile +++ b/docker/unbound.Dockerfile @@ -4,4 +4,3 @@ RUN apt-get update && \ apt-get install -y unbound iputils-ping tshark vim COPY ./files/etc/unbound/unbound.conf /etc/unbound/unbound.conf -COPY ./files/etc/unbound/root.hints /etc/unbound/root.hints diff --git a/src/lib.rs b/src/lib.rs index 2422ee27..f26d7399 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,10 @@ use core::fmt; -use std::process::Output; +use std::process::{self, ExitStatus, Output}; use std::sync::atomic; use std::{ fs, path::Path, - process::{Command, ExitStatus, Stdio}, + process::{Command, Stdio}, sync::atomic::AtomicUsize, }; @@ -49,7 +49,11 @@ impl Container { // `docker run --rm -it $IMAGE sleep infinity` let mut command = Command::new("docker"); - let container_name = format!("{image}-{}", COUNT.fetch_add(1, atomic::Ordering::Relaxed)); + let pid = process::id(); + let container_name = format!( + "{image}-{pid}-{}", + COUNT.fetch_add(1, atomic::Ordering::Relaxed) + ); command.args(&["run", "--rm", "--detach", "--name", &container_name]); let output = command .arg("-it") @@ -97,6 +101,16 @@ impl Container { Ok(output) } + // FIXME + pub fn exec2(&self, cmd: &[&str]) -> Result { + let mut command = Command::new("docker"); + command.args(&["exec", "-t", &self.id]).args(cmd); + + let status = command.status()?; + + Ok(status) + } + pub fn ip_addr(&self) -> Result { let mut command = Command::new("docker"); command @@ -193,14 +207,51 @@ mod tests { Ok(()) } - #[ignore = "TODO"] + use minijinja::{context, Environment}; + + fn tld_zone(domain: &str) -> String { + assert!(domain.ends_with(".")); + + let mut env = Environment::new(); + let name = "main.zone"; + env.add_template(name, include_str!("templates/tld.zone.jinja")) + .unwrap(); + let template = env.get_template(name).unwrap(); + template.render(context! { tld => domain }).unwrap() + } + + fn root_zone() -> String { + let mut env = Environment::new(); + let name = "main.zone"; + env.add_template(name, include_str!("templates/root.zone.jinja")) + .unwrap(); + let template = env.get_template(name).unwrap(); + template.render(context! {}).unwrap() + } + + // TODO create `nsd.conf` file at runtime #[test] fn tld_setup() -> Result<()> { - let container = Container::run(Image::Nsd)?; + let tld_ns = Container::run(Image::Nsd)?; - container.cp("/etc/nsd/zones/main.zone", "TODO")?; + tld_ns.exec(&["mkdir", "-p", "/etc/nsd/zones"])?; + tld_ns.cp("/etc/nsd/zones/main.zone", &tld_zone("."))?; - container.exec(&["nsd", "-d"])?; + tld_ns.exec(&["nsd", "-d"])?; + + Ok(()) + } + + #[test] + fn root_setup() -> Result<()> { + let tld_ns = Container::run(Image::Nsd)?; + + tld_ns.exec(&["mkdir", "-p", "/etc/nsd/zones"])?; + let zone_path = "/etc/nsd/zones/main.zone"; + tld_ns.cp(zone_path, &root_zone())?; + tld_ns.exec(&["chmod", "666", zone_path])?; + + tld_ns.exec2(&["nsd", "-d"])?; Ok(()) } diff --git a/src/templates/nsd.conf.jinja b/src/templates/nsd.conf.jinja new file mode 100644 index 00000000..d3af5808 --- /dev/null +++ b/src/templates/nsd.conf.jinja @@ -0,0 +1,6 @@ +remote-control: + control-enable: no + +zone: + name: {{ domain }} + zonefile: /etc/nsd/zones/main.zone diff --git a/docker/files/etc/unbound/root.hints b/src/templates/root.hints.jinja similarity index 50% rename from docker/files/etc/unbound/root.hints rename to src/templates/root.hints.jinja index 5516fe71..d8e436aa 100644 --- a/docker/files/etc/unbound/root.hints +++ b/src/templates/root.hints.jinja @@ -1,2 +1,2 @@ . 3600000 NS primary.root-server.com. -primary.root-server.com. 3600000 A 172.17.0.2 +primary.root-server.com. 3600000 A {{ root_ns_ip_addr }} diff --git a/src/templates/root.zone.jinja b/src/templates/root.zone.jinja new file mode 100644 index 00000000..e5712ffd --- /dev/null +++ b/src/templates/root.zone.jinja @@ -0,0 +1,12 @@ +$ORIGIN . +$TTL 1800 +@ IN SOA primary.root-server.com admin.root-server.com ( + 2014010100 ; Serial + 10800 ; Refresh (3 hours) + 900 ; Retry (15 minutes) + 604800 ; Expire (1 week) + 86400 ; Minimum (1 day) + ) +@ IN NS primary.root-server.com + +; TODO referral diff --git a/src/templates/tld.zone.jinja b/src/templates/tld.zone.jinja new file mode 100644 index 00000000..f6766615 --- /dev/null +++ b/src/templates/tld.zone.jinja @@ -0,0 +1,10 @@ +$ORIGIN {{ tld }} +$TTL 1800 +@ IN SOA primary.tld-server.{{ tld }} admin.tld-server.{{ tld }} ( + 2014010100 ; Serial + 10800 ; Refresh (3 hours) + 900 ; Retry (15 minutes) + 604800 ; Expire (1 week) + 86400 ; Minimum (1 day) + ) +@ IN NS primary.tld-server.{{ tld }} From 6026caf25dd060a152a9cf5742535ef2f0beab6e Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 2 Feb 2024 14:48:26 +0100 Subject: [PATCH 007/124] make nameserver setup work --- src/lib.rs | 54 ++++++++++++++++++++++++++++-------- src/templates/tld.zone.jinja | 2 ++ 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f26d7399..f6fdf58d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,14 +67,16 @@ impl Container { let id = core::str::from_utf8(&output.stdout)?.trim().to_string(); dbg!(&id); - - Ok(Self { + let container = Self { id, name: container_name, - }) + }; + dbg!(container.ip_addr()?); + + Ok(container) } - pub fn cp(&self, path_in_container: &str, file_contents: &str) -> Result<()> { + pub fn cp(&self, path_in_container: &str, file_contents: &str, chmod: &str) -> Result<()> { let mut temp_file = NamedTempFile::new()?; fs::write(&mut temp_file, file_contents)?; @@ -89,6 +91,12 @@ impl Container { return Err(format!("`{command:?}` failed").into()); } + let command = &["chmod", chmod, path_in_container]; + let output = self.exec(command)?; + if !output.status.success() { + return Err(format!("`{command:?}` failed").into()); + } + Ok(()) } @@ -195,7 +203,7 @@ mod tests { let path = "/tmp/somefile"; let contents = "hello"; - container.cp(path, contents)?; + container.cp(path, contents, CHMOD_RW_EVERYONE)?; let output = container.exec(&["cat", path])?; dbg!(&output); @@ -211,6 +219,7 @@ mod tests { fn tld_zone(domain: &str) -> String { assert!(domain.ends_with(".")); + assert!(!domain.starts_with(".")); let mut env = Environment::new(); let name = "main.zone"; @@ -220,6 +229,17 @@ mod tests { template.render(context! { tld => domain }).unwrap() } + fn nsd_conf(domain: &str) -> String { + assert!(domain.ends_with(".")); + + let mut env = Environment::new(); + let name = "nsd.conf"; + env.add_template(name, include_str!("templates/nsd.conf.jinja")) + .unwrap(); + let template = env.get_template(name).unwrap(); + template.render(context! { domain => domain }).unwrap() + } + fn root_zone() -> String { let mut env = Environment::new(); let name = "main.zone"; @@ -229,29 +249,39 @@ mod tests { template.render(context! {}).unwrap() } + const CHMOD_RW_EVERYONE: &str = "666"; + // TODO create `nsd.conf` file at runtime #[test] fn tld_setup() -> Result<()> { let tld_ns = Container::run(Image::Nsd)?; tld_ns.exec(&["mkdir", "-p", "/etc/nsd/zones"])?; - tld_ns.cp("/etc/nsd/zones/main.zone", &tld_zone("."))?; + tld_ns.cp( + "/etc/nsd/zones/main.zone", + &tld_zone("com."), + CHMOD_RW_EVERYONE, + )?; + tld_ns.cp("/etc/nsd/nsd.conf", &nsd_conf("com."), CHMOD_RW_EVERYONE)?; - tld_ns.exec(&["nsd", "-d"])?; + let status = tld_ns.exec2(&["nsd", "-d"])?; + // println!("stdout: {}", core::str::from_utf8(&output.stdout).unwrap()); + // println!("stderr: {}", core::str::from_utf8(&output.stderr).unwrap()); + assert!(status.success()); Ok(()) } #[test] fn root_setup() -> Result<()> { - let tld_ns = Container::run(Image::Nsd)?; + let root_ns = Container::run(Image::Nsd)?; - tld_ns.exec(&["mkdir", "-p", "/etc/nsd/zones"])?; + root_ns.exec(&["mkdir", "-p", "/etc/nsd/zones"])?; let zone_path = "/etc/nsd/zones/main.zone"; - tld_ns.cp(zone_path, &root_zone())?; - tld_ns.exec(&["chmod", "666", zone_path])?; + root_ns.cp("/etc/nsd/nsd.conf", &nsd_conf("."), CHMOD_RW_EVERYONE)?; + root_ns.cp(zone_path, &root_zone(), CHMOD_RW_EVERYONE)?; - tld_ns.exec2(&["nsd", "-d"])?; + root_ns.exec2(&["nsd", "-d"])?; Ok(()) } diff --git a/src/templates/tld.zone.jinja b/src/templates/tld.zone.jinja index f6766615..f16feb2a 100644 --- a/src/templates/tld.zone.jinja +++ b/src/templates/tld.zone.jinja @@ -8,3 +8,5 @@ $TTL 1800 86400 ; Minimum (1 day) ) @ IN NS primary.tld-server.{{ tld }} + +; intentionally blank From cc753de77cee6d691910b6326f3f22c426d27ff1 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 2 Feb 2024 14:59:13 +0100 Subject: [PATCH 008/124] add NsdContainer --- src/lib.rs | 148 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 92 insertions(+), 56 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f6fdf58d..79e43bb7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,11 +8,92 @@ use std::{ sync::atomic::AtomicUsize, }; +use minijinja::{context, Environment}; use tempfile::NamedTempFile; pub type Error = Box; pub type Result = core::result::Result; +const CHMOD_RW_EVERYONE: &str = "666"; + +fn tld_zone(domain: &str) -> String { + assert!(domain.ends_with(".")); + assert!(!domain.starts_with(".")); + + let mut env = Environment::new(); + let name = "main.zone"; + env.add_template(name, include_str!("templates/tld.zone.jinja")) + .unwrap(); + let template = env.get_template(name).unwrap(); + template.render(context! { tld => domain }).unwrap() +} + +fn root_zone() -> String { + let mut env = Environment::new(); + let name = "main.zone"; + env.add_template(name, include_str!("templates/root.zone.jinja")) + .unwrap(); + let template = env.get_template(name).unwrap(); + template.render(context! {}).unwrap() +} + +fn nsd_conf(domain: &str) -> String { + assert!(domain.ends_with(".")); + + let mut env = Environment::new(); + let name = "nsd.conf"; + env.add_template(name, include_str!("templates/nsd.conf.jinja")) + .unwrap(); + let template = env.get_template(name).unwrap(); + template.render(context! { domain => domain }).unwrap() +} + +enum Domain<'a> { + Root, + Tld { domain: &'a str }, +} + +impl Domain<'_> { + fn fqdn(&self) -> &str { + match self { + Domain::Root => ".", + Domain::Tld { domain } => domain, + } + } +} + +pub struct NsdContainer { + inner: Container, +} + +impl NsdContainer { + pub fn new(domain: Domain) -> Result { + let container = Container::run(Image::Nsd)?; + + container.exec(&["mkdir", "-p", "/etc/nsd/zones"])?; + let zone_path = "/etc/nsd/zones/main.zone"; + + container.cp( + "/etc/nsd/nsd.conf", + &nsd_conf(domain.fqdn()), + CHMOD_RW_EVERYONE, + )?; + + let zone_file_contents = match domain { + Domain::Root => root_zone(), + Domain::Tld { domain } => tld_zone(domain), + }; + + container.cp(zone_path, &zone_file_contents, CHMOD_RW_EVERYONE)?; + + Ok(Self { inner: container }) + } + + pub fn start(&self) -> Result { + self.inner.exec2(&["nsd", "-d"]) + } +} + pub struct Container { id: String, name: String, @@ -215,73 +296,28 @@ mod tests { Ok(()) } - use minijinja::{context, Environment}; - - fn tld_zone(domain: &str) -> String { - assert!(domain.ends_with(".")); - assert!(!domain.starts_with(".")); - - let mut env = Environment::new(); - let name = "main.zone"; - env.add_template(name, include_str!("templates/tld.zone.jinja")) - .unwrap(); - let template = env.get_template(name).unwrap(); - template.render(context! { tld => domain }).unwrap() - } - - fn nsd_conf(domain: &str) -> String { - assert!(domain.ends_with(".")); - - let mut env = Environment::new(); - let name = "nsd.conf"; - env.add_template(name, include_str!("templates/nsd.conf.jinja")) - .unwrap(); - let template = env.get_template(name).unwrap(); - template.render(context! { domain => domain }).unwrap() - } - - fn root_zone() -> String { - let mut env = Environment::new(); - let name = "main.zone"; - env.add_template(name, include_str!("templates/root.zone.jinja")) - .unwrap(); - let template = env.get_template(name).unwrap(); - template.render(context! {}).unwrap() - } - - const CHMOD_RW_EVERYONE: &str = "666"; - // TODO create `nsd.conf` file at runtime #[test] fn tld_setup() -> Result<()> { - let tld_ns = Container::run(Image::Nsd)?; - - tld_ns.exec(&["mkdir", "-p", "/etc/nsd/zones"])?; - tld_ns.cp( - "/etc/nsd/zones/main.zone", - &tld_zone("com."), - CHMOD_RW_EVERYONE, - )?; - tld_ns.cp("/etc/nsd/nsd.conf", &nsd_conf("com."), CHMOD_RW_EVERYONE)?; - - let status = tld_ns.exec2(&["nsd", "-d"])?; - // println!("stdout: {}", core::str::from_utf8(&output.stdout).unwrap()); - // println!("stderr: {}", core::str::from_utf8(&output.stderr).unwrap()); - assert!(status.success()); + let tld_ns = NsdContainer::new(Domain::Tld { domain: "com." })?; + tld_ns.start()?; Ok(()) } #[test] fn root_setup() -> Result<()> { - let root_ns = Container::run(Image::Nsd)?; + let root_ns = NsdContainer::new(Domain::Root)?; + root_ns.start()?; - root_ns.exec(&["mkdir", "-p", "/etc/nsd/zones"])?; - let zone_path = "/etc/nsd/zones/main.zone"; - root_ns.cp("/etc/nsd/nsd.conf", &nsd_conf("."), CHMOD_RW_EVERYONE)?; - root_ns.cp(zone_path, &root_zone(), CHMOD_RW_EVERYONE)?; + // let root_ns = Container::run(Image::Nsd)?; - root_ns.exec2(&["nsd", "-d"])?; + // root_ns.exec(&["mkdir", "-p", "/etc/nsd/zones"])?; + // let zone_path = "/etc/nsd/zones/main.zone"; + // root_ns.cp("/etc/nsd/nsd.conf", &nsd_conf("."), CHMOD_RW_EVERYONE)?; + // root_ns.cp(zone_path, &root_zone(), CHMOD_RW_EVERYONE)?; + + // root_ns.exec2(&["nsd", "-d"])?; Ok(()) } From 61bb4bb3155ee2e938d74fc19e5ad1669ccc214a Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 2 Feb 2024 15:09:45 +0100 Subject: [PATCH 009/124] test nameserver with dig --- src/lib.rs | 60 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 79e43bb7..80bc2a5a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ use core::fmt; -use std::process::{self, ExitStatus, Output}; +use std::process::{self, Child, Output}; use std::sync::atomic; use std::{ fs, @@ -48,7 +48,7 @@ fn nsd_conf(domain: &str) -> String { template.render(context! { domain => domain }).unwrap() } -enum Domain<'a> { +pub enum Domain<'a> { Root, Tld { domain: &'a str }, } @@ -63,11 +63,12 @@ impl Domain<'_> { } pub struct NsdContainer { - inner: Container, + child: Child, + container: Container, } impl NsdContainer { - pub fn new(domain: Domain) -> Result { + pub fn start(domain: Domain) -> Result { let container = Container::run(Image::Nsd)?; container.exec(&["mkdir", "-p", "/etc/nsd/zones"])?; @@ -86,11 +87,19 @@ impl NsdContainer { container.cp(zone_path, &zone_file_contents, CHMOD_RW_EVERYONE)?; - Ok(Self { inner: container }) + let child = container.spawn(&["nsd", "-d"])?; + + Ok(Self { child, container }) } - pub fn start(&self) -> Result { - self.inner.exec2(&["nsd", "-d"]) + pub fn ip_addr(&self) -> Result { + self.container.ip_addr() + } +} + +impl Drop for NsdContainer { + fn drop(&mut self) { + let _ = self.child.kill(); } } @@ -190,14 +199,13 @@ impl Container { Ok(output) } - // FIXME - pub fn exec2(&self, cmd: &[&str]) -> Result { + pub fn spawn(&self, cmd: &[&str]) -> Result { let mut command = Command::new("docker"); command.args(&["exec", "-t", &self.id]).args(cmd); - let status = command.status()?; + let child = command.spawn()?; - Ok(status) + Ok(child) } pub fn ip_addr(&self) -> Result { @@ -296,28 +304,34 @@ mod tests { Ok(()) } - // TODO create `nsd.conf` file at runtime #[test] fn tld_setup() -> Result<()> { - let tld_ns = NsdContainer::new(Domain::Tld { domain: "com." })?; - tld_ns.start()?; + let tld_ns = NsdContainer::start(Domain::Tld { domain: "com." })?; + let ip_addr = tld_ns.ip_addr()?; + + let client = Container::run(Image::Client)?; + let output = client.exec(&["dig", &format!("@{ip_addr}"), "SOA", "com."])?; + + assert!(output.status.success()); + let stdout = core::str::from_utf8(&output.stdout)?; + println!("{stdout}"); + assert!(stdout.contains("status: NOERROR")); Ok(()) } #[test] fn root_setup() -> Result<()> { - let root_ns = NsdContainer::new(Domain::Root)?; - root_ns.start()?; + let root_ns = NsdContainer::start(Domain::Root)?; + let ip_addr = root_ns.ip_addr()?; - // let root_ns = Container::run(Image::Nsd)?; + let client = Container::run(Image::Client)?; + let output = client.exec(&["dig", &format!("@{ip_addr}"), "SOA", "."])?; - // root_ns.exec(&["mkdir", "-p", "/etc/nsd/zones"])?; - // let zone_path = "/etc/nsd/zones/main.zone"; - // root_ns.cp("/etc/nsd/nsd.conf", &nsd_conf("."), CHMOD_RW_EVERYONE)?; - // root_ns.cp(zone_path, &root_zone(), CHMOD_RW_EVERYONE)?; - - // root_ns.exec2(&["nsd", "-d"])?; + assert!(output.status.success()); + let stdout = core::str::from_utf8(&output.stdout)?; + println!("{stdout}"); + assert!(stdout.contains("status: NOERROR")); Ok(()) } From e997c8cff5deed2b98b4efda78a1609720486c50 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 2 Feb 2024 15:15:42 +0100 Subject: [PATCH 010/124] move Container into its own module --- src/container.rs | 194 +++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 191 +--------------------------------------------- 2 files changed, 197 insertions(+), 188 deletions(-) create mode 100644 src/container.rs diff --git a/src/container.rs b/src/container.rs new file mode 100644 index 00000000..9c16cfe2 --- /dev/null +++ b/src/container.rs @@ -0,0 +1,194 @@ +use std::fs; +use std::path::Path; +use std::process::{self, Child, Output}; +use std::process::{Command, Stdio}; +use std::sync::atomic; +use std::sync::atomic::AtomicUsize; + +use tempfile::NamedTempFile; + +use crate::{Image, Result}; + +pub struct Container { + id: String, + name: String, +} + +impl Container { + /// Starts the container in a "parked" state + pub fn run(image: Image) -> Result { + static COUNT: AtomicUsize = AtomicUsize::new(0); + + let image_tag = format!("dnssec-tests-{image}"); + + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let dockerfile_path = manifest_dir + .join("docker") + .join(format!("{image}.Dockerfile")); + let docker_dir_path = manifest_dir.join("docker"); + dbg!(&image_tag); + + let mut command = Command::new("docker"); + command + .args(&["build", "-t"]) + .arg(&image_tag) + .arg("-f") + .arg(dockerfile_path) + .arg(docker_dir_path); + let status = command.status()?; + + if !status.success() { + return Err(format!("`{command:?}` failed").into()); + } + + let mut command = Command::new("docker"); + let pid = process::id(); + let container_name = format!( + "{image}-{pid}-{}", + COUNT.fetch_add(1, atomic::Ordering::Relaxed) + ); + command.args(&["run", "--rm", "--detach", "--name", &container_name]); + let output = command + .arg("-it") + .arg(image_tag) + .args(["sleep", "infinity"]) + .output()?; + + if !output.status.success() { + return Err(format!("`{command:?}` failed").into()); + } + + let id = core::str::from_utf8(&output.stdout)?.trim().to_string(); + dbg!(&id); + let container = Self { + id, + name: container_name, + }; + dbg!(container.ip_addr()?); + + Ok(container) + } + + pub fn cp(&self, path_in_container: &str, file_contents: &str, chmod: &str) -> Result<()> { + let mut temp_file = NamedTempFile::new()?; + 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 mut command = Command::new("docker"); + command.args(["cp", &src_path, &dest_path]); + + let status = command.status()?; + if !status.success() { + return Err(format!("`{command:?}` failed").into()); + } + + let command = &["chmod", chmod, path_in_container]; + let output = self.exec(command)?; + if !output.status.success() { + return Err(format!("`{command:?}` failed").into()); + } + + Ok(()) + } + + pub fn exec(&self, cmd: &[&str]) -> Result { + let mut command = Command::new("docker"); + command.args(&["exec", "-t", &self.id]).args(cmd); + + let output = command.output()?; + + Ok(output) + } + + pub fn spawn(&self, cmd: &[&str]) -> Result { + let mut command = Command::new("docker"); + command.args(&["exec", "-t", &self.id]).args(cmd); + + let child = command.spawn()?; + + Ok(child) + } + + pub fn ip_addr(&self) -> Result { + let mut command = Command::new("docker"); + command + .args(&[ + "inspect", + "-f", + "{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}", + ]) + .arg(&self.id); + + let output = command.output()?; + if !output.status.success() { + return Err(format!("`{command:?}` failed").into()); + } + + let ip_addr = core::str::from_utf8(&output.stdout)?.trim().to_string(); + dbg!(&ip_addr); + + Ok(ip_addr) + } +} + +// ensure the container gets deleted +impl Drop for Container { + fn drop(&mut self) { + // running this to completion would block the current thread for several seconds so just + // fire and forget + let _ = Command::new("docker") + .args(["rm", "-f", &self.id]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + } +} + +#[cfg(test)] +mod tests { + use std::net::Ipv4Addr; + + use crate::CHMOD_RW_EVERYONE; + + use super::*; + + #[test] + fn run_works() -> Result<()> { + let container = Container::run(Image::Client)?; + + let output = container.exec(&["true"])?; + assert!(output.status.success()); + + Ok(()) + } + + #[test] + fn ip_addr_works() -> Result<()> { + let container = Container::run(Image::Client)?; + + let ip_addr = container.ip_addr()?; + assert!(ip_addr.parse::().is_ok()); + + Ok(()) + } + + #[test] + fn cp_works() -> Result<()> { + let container = Container::run(Image::Client)?; + + let path = "/tmp/somefile"; + let contents = "hello"; + container.cp(path, contents, CHMOD_RW_EVERYONE)?; + + let output = container.exec(&["cat", path])?; + dbg!(&output); + + assert!(output.status.success()); + + assert_eq!(contents, core::str::from_utf8(&output.stdout)?); + + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 80bc2a5a..40113604 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,15 +1,8 @@ use core::fmt; -use std::process::{self, Child, Output}; -use std::sync::atomic; -use std::{ - fs, - path::Path, - process::{Command, Stdio}, - sync::atomic::AtomicUsize, -}; +use std::process::Child; +use container::Container; use minijinja::{context, Environment}; -use tempfile::NamedTempFile; pub type Error = Box; pub type Result = core::result::Result; @@ -103,145 +96,7 @@ impl Drop for NsdContainer { } } -pub struct Container { - id: String, - name: String, -} - -impl Container { - /// Starts the container in a "parked" state - pub fn run(image: Image) -> Result { - static COUNT: AtomicUsize = AtomicUsize::new(0); - - let image_tag = format!("dnssec-tests-{image}"); - - let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); - let dockerfile_path = manifest_dir - .join("docker") - .join(format!("{image}.Dockerfile")); - let docker_dir_path = manifest_dir.join("docker"); - dbg!(&image_tag); - - let mut command = Command::new("docker"); - command - .args(&["build", "-t"]) - .arg(&image_tag) - .arg("-f") - .arg(dockerfile_path) - .arg(docker_dir_path); - let status = command.status()?; - - if !status.success() { - return Err(format!("`{command:?}` failed").into()); - } - - // run container based on image - // `docker run --rm -it $IMAGE sleep infinity` - - let mut command = Command::new("docker"); - let pid = process::id(); - let container_name = format!( - "{image}-{pid}-{}", - COUNT.fetch_add(1, atomic::Ordering::Relaxed) - ); - command.args(&["run", "--rm", "--detach", "--name", &container_name]); - let output = command - .arg("-it") - .arg(image_tag) - .args(["sleep", "infinity"]) - .output()?; - - if !output.status.success() { - return Err(format!("`{command:?}` failed").into()); - } - - let id = core::str::from_utf8(&output.stdout)?.trim().to_string(); - dbg!(&id); - let container = Self { - id, - name: container_name, - }; - dbg!(container.ip_addr()?); - - Ok(container) - } - - pub fn cp(&self, path_in_container: &str, file_contents: &str, chmod: &str) -> Result<()> { - let mut temp_file = NamedTempFile::new()?; - 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 mut command = Command::new("docker"); - command.args(["cp", &src_path, &dest_path]); - - let status = command.status()?; - if !status.success() { - return Err(format!("`{command:?}` failed").into()); - } - - let command = &["chmod", chmod, path_in_container]; - let output = self.exec(command)?; - if !output.status.success() { - return Err(format!("`{command:?}` failed").into()); - } - - Ok(()) - } - - pub fn exec(&self, cmd: &[&str]) -> Result { - let mut command = Command::new("docker"); - command.args(&["exec", "-t", &self.id]).args(cmd); - - let output = command.output()?; - - Ok(output) - } - - pub fn spawn(&self, cmd: &[&str]) -> Result { - let mut command = Command::new("docker"); - command.args(&["exec", "-t", &self.id]).args(cmd); - - let child = command.spawn()?; - - Ok(child) - } - - pub fn ip_addr(&self) -> Result { - let mut command = Command::new("docker"); - command - .args(&[ - "inspect", - "-f", - "{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}", - ]) - .arg(&self.id); - - let output = command.output()?; - if !output.status.success() { - return Err(format!("`{command:?}` failed").into()); - } - - let ip_addr = core::str::from_utf8(&output.stdout)?.trim().to_string(); - dbg!(&ip_addr); - - Ok(ip_addr) - } -} - -// ensure the container gets deleted -impl Drop for Container { - fn drop(&mut self) { - // running this to completion would block the current thread for several seconds so just - // fire and forget - let _ = Command::new("docker") - .args(["rm", "-f", &self.id]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); - } -} +mod container; pub enum Image { Nsd, // for ROOT, TLD, DOMAIN @@ -262,48 +117,8 @@ impl fmt::Display for Image { #[cfg(test)] mod tests { - use std::net::Ipv4Addr; - use super::*; - #[test] - fn run_works() -> Result<()> { - let container = Container::run(Image::Client)?; - - let output = container.exec(&["true"])?; - assert!(output.status.success()); - - Ok(()) - } - - #[test] - fn ip_addr_works() -> Result<()> { - let container = Container::run(Image::Client)?; - - let ip_addr = container.ip_addr()?; - assert!(ip_addr.parse::().is_ok()); - - Ok(()) - } - - #[test] - fn cp_works() -> Result<()> { - let container = Container::run(Image::Client)?; - - let path = "/tmp/somefile"; - let contents = "hello"; - container.cp(path, contents, CHMOD_RW_EVERYONE)?; - - let output = container.exec(&["cat", path])?; - dbg!(&output); - - assert!(output.status.success()); - - assert_eq!(contents, core::str::from_utf8(&output.stdout)?); - - Ok(()) - } - #[test] fn tld_setup() -> Result<()> { let tld_ns = NsdContainer::start(Domain::Tld { domain: "com." })?; From 42de7c3a92bdedbd02e1fd5c77a3089c4b5ccf32 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 2 Feb 2024 15:19:29 +0100 Subject: [PATCH 011/124] move NsdContainer into its own module --- src/lib.rs | 117 ++--------------------------------------------------- src/nsd.rs | 115 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 114 deletions(-) create mode 100644 src/nsd.rs diff --git a/src/lib.rs b/src/lib.rs index 40113604..a6eab726 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,45 +1,14 @@ use core::fmt; -use std::process::Child; -use container::Container; -use minijinja::{context, Environment}; +pub use crate::nsd::NsdContainer; pub type Error = Box; pub type Result = core::result::Result; const CHMOD_RW_EVERYONE: &str = "666"; -fn tld_zone(domain: &str) -> String { - assert!(domain.ends_with(".")); - assert!(!domain.starts_with(".")); - - let mut env = Environment::new(); - let name = "main.zone"; - env.add_template(name, include_str!("templates/tld.zone.jinja")) - .unwrap(); - let template = env.get_template(name).unwrap(); - template.render(context! { tld => domain }).unwrap() -} - -fn root_zone() -> String { - let mut env = Environment::new(); - let name = "main.zone"; - env.add_template(name, include_str!("templates/root.zone.jinja")) - .unwrap(); - let template = env.get_template(name).unwrap(); - template.render(context! {}).unwrap() -} - -fn nsd_conf(domain: &str) -> String { - assert!(domain.ends_with(".")); - - let mut env = Environment::new(); - let name = "nsd.conf"; - env.add_template(name, include_str!("templates/nsd.conf.jinja")) - .unwrap(); - let template = env.get_template(name).unwrap(); - template.render(context! { domain => domain }).unwrap() -} +mod container; +mod nsd; pub enum Domain<'a> { Root, @@ -55,49 +24,6 @@ impl Domain<'_> { } } -pub struct NsdContainer { - child: Child, - container: Container, -} - -impl NsdContainer { - pub fn start(domain: Domain) -> Result { - let container = Container::run(Image::Nsd)?; - - container.exec(&["mkdir", "-p", "/etc/nsd/zones"])?; - let zone_path = "/etc/nsd/zones/main.zone"; - - container.cp( - "/etc/nsd/nsd.conf", - &nsd_conf(domain.fqdn()), - CHMOD_RW_EVERYONE, - )?; - - let zone_file_contents = match domain { - Domain::Root => root_zone(), - Domain::Tld { domain } => tld_zone(domain), - }; - - container.cp(zone_path, &zone_file_contents, CHMOD_RW_EVERYONE)?; - - let child = container.spawn(&["nsd", "-d"])?; - - Ok(Self { child, container }) - } - - pub fn ip_addr(&self) -> Result { - self.container.ip_addr() - } -} - -impl Drop for NsdContainer { - fn drop(&mut self) { - let _ = self.child.kill(); - } -} - -mod container; - pub enum Image { Nsd, // for ROOT, TLD, DOMAIN Unbound, @@ -114,40 +40,3 @@ impl fmt::Display for Image { f.write_str(name) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn tld_setup() -> Result<()> { - let tld_ns = NsdContainer::start(Domain::Tld { domain: "com." })?; - let ip_addr = tld_ns.ip_addr()?; - - let client = Container::run(Image::Client)?; - let output = client.exec(&["dig", &format!("@{ip_addr}"), "SOA", "com."])?; - - assert!(output.status.success()); - let stdout = core::str::from_utf8(&output.stdout)?; - println!("{stdout}"); - assert!(stdout.contains("status: NOERROR")); - - Ok(()) - } - - #[test] - fn root_setup() -> Result<()> { - let root_ns = NsdContainer::start(Domain::Root)?; - let ip_addr = root_ns.ip_addr()?; - - let client = Container::run(Image::Client)?; - let output = client.exec(&["dig", &format!("@{ip_addr}"), "SOA", "."])?; - - assert!(output.status.success()); - let stdout = core::str::from_utf8(&output.stdout)?; - println!("{stdout}"); - assert!(stdout.contains("status: NOERROR")); - - Ok(()) - } -} diff --git a/src/nsd.rs b/src/nsd.rs new file mode 100644 index 00000000..4d434214 --- /dev/null +++ b/src/nsd.rs @@ -0,0 +1,115 @@ +use std::process::Child; + +use minijinja::{context, Environment}; + +use crate::{container::Container, Domain, Image, Result, CHMOD_RW_EVERYONE}; + +pub struct NsdContainer { + child: Child, + container: Container, +} + +impl NsdContainer { + pub fn start(domain: Domain) -> Result { + let container = Container::run(Image::Nsd)?; + + container.exec(&["mkdir", "-p", "/etc/nsd/zones"])?; + let zone_path = "/etc/nsd/zones/main.zone"; + + container.cp( + "/etc/nsd/nsd.conf", + &nsd_conf(domain.fqdn()), + CHMOD_RW_EVERYONE, + )?; + + let zone_file_contents = match domain { + Domain::Root => root_zone(), + Domain::Tld { domain } => tld_zone(domain), + }; + + container.cp(zone_path, &zone_file_contents, CHMOD_RW_EVERYONE)?; + + let child = container.spawn(&["nsd", "-d"])?; + + Ok(Self { child, container }) + } + + pub fn ip_addr(&self) -> Result { + self.container.ip_addr() + } +} + +impl Drop for NsdContainer { + fn drop(&mut self) { + let _ = self.child.kill(); + } +} + +fn tld_zone(domain: &str) -> String { + assert!(domain.ends_with(".")); + assert!(!domain.starts_with(".")); + + let mut env = Environment::new(); + let name = "main.zone"; + env.add_template(name, include_str!("templates/tld.zone.jinja")) + .unwrap(); + let template = env.get_template(name).unwrap(); + template.render(context! { tld => domain }).unwrap() +} + +fn root_zone() -> String { + let mut env = Environment::new(); + let name = "main.zone"; + env.add_template(name, include_str!("templates/root.zone.jinja")) + .unwrap(); + let template = env.get_template(name).unwrap(); + template.render(context! {}).unwrap() +} + +fn nsd_conf(domain: &str) -> String { + assert!(domain.ends_with(".")); + + let mut env = Environment::new(); + let name = "nsd.conf"; + env.add_template(name, include_str!("templates/nsd.conf.jinja")) + .unwrap(); + let template = env.get_template(name).unwrap(); + template.render(context! { domain => domain }).unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tld_setup() -> Result<()> { + let tld_ns = NsdContainer::start(Domain::Tld { domain: "com." })?; + let ip_addr = tld_ns.ip_addr()?; + + let client = Container::run(Image::Client)?; + let output = client.exec(&["dig", &format!("@{ip_addr}"), "SOA", "com."])?; + + assert!(output.status.success()); + let stdout = core::str::from_utf8(&output.stdout)?; + println!("{stdout}"); + assert!(stdout.contains("status: NOERROR")); + + Ok(()) + } + + #[test] + fn root_setup() -> Result<()> { + let root_ns = NsdContainer::start(Domain::Root)?; + let ip_addr = root_ns.ip_addr()?; + + let client = Container::run(Image::Client)?; + let output = client.exec(&["dig", &format!("@{ip_addr}"), "SOA", "."])?; + + assert!(output.status.success()); + let stdout = core::str::from_utf8(&output.stdout)?; + println!("{stdout}"); + assert!(stdout.contains("status: NOERROR")); + + Ok(()) + } +} From f4ded488ce32d189c242ce532bd344f265c2f0cb Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 2 Feb 2024 15:39:38 +0100 Subject: [PATCH 012/124] merge all docker images into one --- docker/client.Dockerfile | 4 ---- docker/nsd.Dockerfile | 4 ---- docker/unbound.Dockerfile | 2 +- src/container.rs | 18 ++++++++++-------- src/lib.rs | 17 ----------------- src/nsd.rs | 8 ++++---- 6 files changed, 15 insertions(+), 38 deletions(-) delete mode 100644 docker/client.Dockerfile delete mode 100644 docker/nsd.Dockerfile diff --git a/docker/client.Dockerfile b/docker/client.Dockerfile deleted file mode 100644 index e05b7249..00000000 --- a/docker/client.Dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -FROM ubuntu:22.04 - -RUN apt-get update && \ - apt-get install -y dnsutils iputils-ping tshark vim \ No newline at end of file diff --git a/docker/nsd.Dockerfile b/docker/nsd.Dockerfile deleted file mode 100644 index 2284ebb8..00000000 --- a/docker/nsd.Dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -FROM ubuntu:22.04 - -RUN apt-get update && \ - apt-get install -y nsd iputils-ping tshark vim diff --git a/docker/unbound.Dockerfile b/docker/unbound.Dockerfile index 67e89404..51c75d45 100644 --- a/docker/unbound.Dockerfile +++ b/docker/unbound.Dockerfile @@ -1,6 +1,6 @@ FROM ubuntu:22.04 RUN apt-get update && \ - apt-get install -y unbound iputils-ping tshark vim + apt-get install -y dnsutils unbound nsd iputils-ping tshark vim COPY ./files/etc/unbound/unbound.conf /etc/unbound/unbound.conf diff --git a/src/container.rs b/src/container.rs index 9c16cfe2..dbdfc0b8 100644 --- a/src/container.rs +++ b/src/container.rs @@ -7,7 +7,7 @@ use std::sync::atomic::AtomicUsize; use tempfile::NamedTempFile; -use crate::{Image, Result}; +use crate::Result; pub struct Container { id: String, @@ -16,15 +16,17 @@ pub struct Container { impl Container { /// Starts the container in a "parked" state - pub fn run(image: Image) -> Result { + pub fn run() -> Result { static COUNT: AtomicUsize = AtomicUsize::new(0); - let image_tag = format!("dnssec-tests-{image}"); + // 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!("{image}.Dockerfile")); + .join(format!("{binary}.Dockerfile")); let docker_dir_path = manifest_dir.join("docker"); dbg!(&image_tag); @@ -44,7 +46,7 @@ impl Container { let mut command = Command::new("docker"); let pid = process::id(); let container_name = format!( - "{image}-{pid}-{}", + "{binary}-{pid}-{}", COUNT.fetch_add(1, atomic::Ordering::Relaxed) ); command.args(&["run", "--rm", "--detach", "--name", &container_name]); @@ -156,7 +158,7 @@ mod tests { #[test] fn run_works() -> Result<()> { - let container = Container::run(Image::Client)?; + let container = Container::run()?; let output = container.exec(&["true"])?; assert!(output.status.success()); @@ -166,7 +168,7 @@ mod tests { #[test] fn ip_addr_works() -> Result<()> { - let container = Container::run(Image::Client)?; + let container = Container::run()?; let ip_addr = container.ip_addr()?; assert!(ip_addr.parse::().is_ok()); @@ -176,7 +178,7 @@ mod tests { #[test] fn cp_works() -> Result<()> { - let container = Container::run(Image::Client)?; + let container = Container::run()?; let path = "/tmp/somefile"; let contents = "hello"; diff --git a/src/lib.rs b/src/lib.rs index a6eab726..c10f90e0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,20 +23,3 @@ impl Domain<'_> { } } } - -pub enum Image { - Nsd, // for ROOT, TLD, DOMAIN - Unbound, - Client, -} - -impl fmt::Display for Image { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let name = match self { - Image::Nsd => "nsd", - Image::Unbound => "unbound", - Image::Client => "client", - }; - f.write_str(name) - } -} diff --git a/src/nsd.rs b/src/nsd.rs index 4d434214..9ddedc3d 100644 --- a/src/nsd.rs +++ b/src/nsd.rs @@ -2,7 +2,7 @@ use std::process::Child; use minijinja::{context, Environment}; -use crate::{container::Container, Domain, Image, Result, CHMOD_RW_EVERYONE}; +use crate::{container::Container, Domain, Result, CHMOD_RW_EVERYONE}; pub struct NsdContainer { child: Child, @@ -11,7 +11,7 @@ pub struct NsdContainer { impl NsdContainer { pub fn start(domain: Domain) -> Result { - let container = Container::run(Image::Nsd)?; + let container = Container::run()?; container.exec(&["mkdir", "-p", "/etc/nsd/zones"])?; let zone_path = "/etc/nsd/zones/main.zone"; @@ -86,7 +86,7 @@ mod tests { let tld_ns = NsdContainer::start(Domain::Tld { domain: "com." })?; let ip_addr = tld_ns.ip_addr()?; - let client = Container::run(Image::Client)?; + let client = Container::run()?; let output = client.exec(&["dig", &format!("@{ip_addr}"), "SOA", "com."])?; assert!(output.status.success()); @@ -102,7 +102,7 @@ mod tests { let root_ns = NsdContainer::start(Domain::Root)?; let ip_addr = root_ns.ip_addr()?; - let client = Container::run(Image::Client)?; + let client = Container::run()?; let output = client.exec(&["dig", &format!("@{ip_addr}"), "SOA", "."])?; assert!(output.status.success()); From 1d616e822db96ebe8e84b3e86d8daadad79e5b68 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 2 Feb 2024 15:40:43 +0100 Subject: [PATCH 013/124] rename nsd -> AuthoritativeNameServer --- src/{nsd.rs => authoritative_name_server.rs} | 10 +++++----- src/lib.rs | 6 ++---- 2 files changed, 7 insertions(+), 9 deletions(-) rename src/{nsd.rs => authoritative_name_server.rs} (91%) diff --git a/src/nsd.rs b/src/authoritative_name_server.rs similarity index 91% rename from src/nsd.rs rename to src/authoritative_name_server.rs index 9ddedc3d..edbb4b57 100644 --- a/src/nsd.rs +++ b/src/authoritative_name_server.rs @@ -4,12 +4,12 @@ use minijinja::{context, Environment}; use crate::{container::Container, Domain, Result, CHMOD_RW_EVERYONE}; -pub struct NsdContainer { +pub struct AuthoritativeNameServer { child: Child, container: Container, } -impl NsdContainer { +impl AuthoritativeNameServer { pub fn start(domain: Domain) -> Result { let container = Container::run()?; @@ -39,7 +39,7 @@ impl NsdContainer { } } -impl Drop for NsdContainer { +impl Drop for AuthoritativeNameServer { fn drop(&mut self) { let _ = self.child.kill(); } @@ -83,7 +83,7 @@ mod tests { #[test] fn tld_setup() -> Result<()> { - let tld_ns = NsdContainer::start(Domain::Tld { domain: "com." })?; + let tld_ns = AuthoritativeNameServer::start(Domain::Tld { domain: "com." })?; let ip_addr = tld_ns.ip_addr()?; let client = Container::run()?; @@ -99,7 +99,7 @@ mod tests { #[test] fn root_setup() -> Result<()> { - let root_ns = NsdContainer::start(Domain::Root)?; + let root_ns = AuthoritativeNameServer::start(Domain::Root)?; let ip_addr = root_ns.ip_addr()?; let client = Container::run()?; diff --git a/src/lib.rs b/src/lib.rs index c10f90e0..699f1338 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,12 @@ -use core::fmt; - -pub use crate::nsd::NsdContainer; +pub use crate::authoritative_name_server::AuthoritativeNameServer; pub type Error = Box; pub type Result = core::result::Result; const CHMOD_RW_EVERYONE: &str = "666"; +mod authoritative_name_server; mod container; -mod nsd; pub enum Domain<'a> { Root, From 9101bb10468b4c4e9c94609383693b2fedcb96be Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 2 Feb 2024 15:45:12 +0100 Subject: [PATCH 014/124] only build image once per test runner run --- src/container.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/container.rs b/src/container.rs index dbdfc0b8..08c7c988 100644 --- a/src/container.rs +++ b/src/container.rs @@ -2,8 +2,8 @@ use std::fs; use std::path::Path; use std::process::{self, Child, Output}; use std::process::{Command, Stdio}; -use std::sync::atomic; use std::sync::atomic::AtomicUsize; +use std::sync::{atomic, Once}; use tempfile::NamedTempFile; @@ -17,6 +17,7 @@ pub struct Container { impl Container { /// Starts the container in a "parked" state pub fn run() -> Result { + static ONCE: Once = Once::new(); static COUNT: AtomicUsize = AtomicUsize::new(0); // TODO configurable: hickory; bind @@ -37,11 +38,11 @@ impl Container { .arg("-f") .arg(dockerfile_path) .arg(docker_dir_path); - let status = command.status()?; - if !status.success() { - return Err(format!("`{command:?}` failed").into()); - } + ONCE.call_once(|| { + let status = command.status().unwrap(); + assert!(status.success()); + }); let mut command = Command::new("docker"); let pid = process::id(); From 60ecfeca5ea6b1322b9e4b862f02b205d3fb4e12 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 5 Feb 2024 14:24:01 +0100 Subject: [PATCH 015/124] initial RecursiveResolver API --- Cargo.lock | 1 + Cargo.toml | 1 + src/container.rs | 1 + src/lib.rs | 2 + src/recursive_resolver.rs | 123 +++++++++++++++++++++++++++++++++ src/templates/root.hints.jinja | 6 +- 6 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 src/recursive_resolver.rs diff --git a/Cargo.lock b/Cargo.lock index 9b6bb82d..4ae8393f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,6 +25,7 @@ name = "dnssec-tests" version = "0.1.0" dependencies = [ "minijinja", + "serde", "tempfile", ] diff --git a/Cargo.toml b/Cargo.toml index 91297a19..03007d0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,5 @@ license = "MIT or Apache 2.0" [dependencies] minijinja = "1.0.12" +serde = { version = "1.0.196", features = ["derive"] } tempfile = "3.9.0" diff --git a/src/container.rs b/src/container.rs index 08c7c988..9753106f 100644 --- a/src/container.rs +++ b/src/container.rs @@ -114,6 +114,7 @@ impl Container { Ok(child) } + // TODO cache this to avoid calling `docker inspect` every time pub fn ip_addr(&self) -> Result { let mut command = Command::new("docker"); command diff --git a/src/lib.rs b/src/lib.rs index 699f1338..3d526062 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub use crate::authoritative_name_server::AuthoritativeNameServer; +pub use crate::recursive_resolver::RecursiveResolver; pub type Error = Box; pub type Result = core::result::Result; @@ -7,6 +8,7 @@ const CHMOD_RW_EVERYONE: &str = "666"; mod authoritative_name_server; mod container; +mod recursive_resolver; pub enum Domain<'a> { Root, diff --git a/src/recursive_resolver.rs b/src/recursive_resolver.rs new file mode 100644 index 00000000..7ab0c9fa --- /dev/null +++ b/src/recursive_resolver.rs @@ -0,0 +1,123 @@ +use std::process::Child; + +use serde::Serialize; + +use crate::container::Container; +use crate::{Result, CHMOD_RW_EVERYONE}; + +pub struct RecursiveResolver { + container: Container, + child: Child, +} + +#[derive(Serialize)] +pub struct RootServer { + name: String, + ip_addr: String, +} + +fn root_hints(roots: &[RootServer]) -> String { + minijinja::render!( + include_str!("templates/root.hints.jinja"), + roots => roots + ) +} + +impl RecursiveResolver { + pub fn start(root_servers: &[RootServer]) -> Result { + let container = Container::run()?; + + container.cp( + "/etc/unbound/root.hints", + &root_hints(root_servers), + CHMOD_RW_EVERYONE, + )?; + + let child = container.spawn(&["unbound", "-d"])?; + + Ok(Self { child, container }) + } + + pub fn ip_addr(&self) -> Result { + self.container.ip_addr() + } +} + +impl Drop for RecursiveResolver { + fn drop(&mut self) { + let _ = self.child.kill(); + } +} + +#[cfg(test)] +mod tests { + use crate::AuthoritativeNameServer; + + use super::*; + + #[test] + fn can_resolve() -> Result<()> { + let root_ns = AuthoritativeNameServer::start(crate::Domain::Root)?; + let roots = &[RootServer { + name: "my.root-server.com".to_string(), + ip_addr: root_ns.ip_addr()?, + }]; + let resolver = RecursiveResolver::start(roots)?; + let resolver_ip_addr = resolver.ip_addr()?; + + let container = Container::run()?; + let output = container.exec(&["dig", &format!("@{}", resolver_ip_addr), "example.com"])?; + + let stdout = core::str::from_utf8(&output.stdout)?; + assert!(stdout.contains("status: NOERROR")); + + Ok(()) + } + + #[test] + fn root_hints_template_works() { + let expected = [ + ("a.root-server.com", "172.17.0.1"), + ("b.root-server.com", "172.17.0.2"), + ]; + + let roots = expected + .iter() + .map(|(ns_name, ip_addr)| RootServer { + name: ns_name.to_string(), + ip_addr: ip_addr.to_string(), + }) + .collect::>(); + + let hints = root_hints(&roots); + + eprintln!("{hints}"); + let lines = hints.lines().collect::>(); + + for (lines, (expected_ns_name, expected_ip_addr)) in lines.chunks(2).zip(expected) { + let [ns_record, a_record] = lines.try_into().unwrap(); + + // block to avoid shadowing + { + let [domain, _ttl, record_type, ns_name] = ns_record + .split_whitespace() + .collect::>() + .try_into() + .unwrap(); + + assert_eq!(".", domain); + assert_eq!("NS", record_type); + assert_eq!(expected_ns_name, ns_name); + } + + let [ns_name, _ttl, record_type, ip_addr] = a_record + .split_whitespace() + .collect::>() + .try_into() + .unwrap(); + assert_eq!(expected_ns_name, ns_name); + assert_eq!("A", record_type); + assert_eq!(expected_ip_addr, ip_addr); + } + } +} diff --git a/src/templates/root.hints.jinja b/src/templates/root.hints.jinja index d8e436aa..180fe2c5 100644 --- a/src/templates/root.hints.jinja +++ b/src/templates/root.hints.jinja @@ -1,2 +1,4 @@ -. 3600000 NS primary.root-server.com. -primary.root-server.com. 3600000 A {{ root_ns_ip_addr }} +{%- for root in roots -%} +. 3600000 NS {{ root.name }} +{{ root.name }} 3600000 A {{ root.ip_addr }} +{% endfor %} From d79581bdcc01b458cf24d846cb56275f1b55f8db Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 5 Feb 2024 14:27:10 +0100 Subject: [PATCH 016/124] fix warnings --- src/authoritative_name_server.rs | 6 +++--- src/container.rs | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/authoritative_name_server.rs b/src/authoritative_name_server.rs index edbb4b57..6b6209d6 100644 --- a/src/authoritative_name_server.rs +++ b/src/authoritative_name_server.rs @@ -46,8 +46,8 @@ impl Drop for AuthoritativeNameServer { } fn tld_zone(domain: &str) -> String { - assert!(domain.ends_with(".")); - assert!(!domain.starts_with(".")); + assert!(domain.ends_with('.')); + assert!(!domain.starts_with('.')); let mut env = Environment::new(); let name = "main.zone"; @@ -67,7 +67,7 @@ fn root_zone() -> String { } fn nsd_conf(domain: &str) -> String { - assert!(domain.ends_with(".")); + assert!(domain.ends_with('.')); let mut env = Environment::new(); let name = "nsd.conf"; diff --git a/src/container.rs b/src/container.rs index 9753106f..e52ca0ef 100644 --- a/src/container.rs +++ b/src/container.rs @@ -11,7 +11,7 @@ use crate::Result; pub struct Container { id: String, - name: String, + _name: String, } impl Container { @@ -33,7 +33,7 @@ impl Container { let mut command = Command::new("docker"); command - .args(&["build", "-t"]) + .args(["build", "-t"]) .arg(&image_tag) .arg("-f") .arg(dockerfile_path) @@ -50,7 +50,7 @@ impl Container { "{binary}-{pid}-{}", COUNT.fetch_add(1, atomic::Ordering::Relaxed) ); - command.args(&["run", "--rm", "--detach", "--name", &container_name]); + command.args(["run", "--rm", "--detach", "--name", &container_name]); let output = command .arg("-it") .arg(image_tag) @@ -65,7 +65,7 @@ impl Container { dbg!(&id); let container = Self { id, - name: container_name, + _name: container_name, }; dbg!(container.ip_addr()?); @@ -98,7 +98,7 @@ impl Container { pub fn exec(&self, cmd: &[&str]) -> Result { let mut command = Command::new("docker"); - command.args(&["exec", "-t", &self.id]).args(cmd); + command.args(["exec", "-t", &self.id]).args(cmd); let output = command.output()?; @@ -107,7 +107,7 @@ impl Container { pub fn spawn(&self, cmd: &[&str]) -> Result { let mut command = Command::new("docker"); - command.args(&["exec", "-t", &self.id]).args(cmd); + command.args(["exec", "-t", &self.id]).args(cmd); let child = command.spawn()?; @@ -118,7 +118,7 @@ impl Container { pub fn ip_addr(&self) -> Result { let mut command = Command::new("docker"); command - .args(&[ + .args([ "inspect", "-f", "{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}", From c7e0580c7ae9a04321235af83029d42b4b17e8fa Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 5 Feb 2024 14:42:20 +0100 Subject: [PATCH 017/124] use Ipv4Addr type for IP addresses --- src/authoritative_name_server.rs | 10 ++--- src/container.rs | 72 ++++++++++++++++++-------------- src/recursive_resolver.rs | 20 +++++---- 3 files changed, 56 insertions(+), 46 deletions(-) diff --git a/src/authoritative_name_server.rs b/src/authoritative_name_server.rs index 6b6209d6..a8b50baa 100644 --- a/src/authoritative_name_server.rs +++ b/src/authoritative_name_server.rs @@ -1,4 +1,4 @@ -use std::process::Child; +use std::{net::Ipv4Addr, process::Child}; use minijinja::{context, Environment}; @@ -34,8 +34,8 @@ impl AuthoritativeNameServer { Ok(Self { child, container }) } - pub fn ip_addr(&self) -> Result { - self.container.ip_addr() + pub fn ipv4_addr(&self) -> Ipv4Addr { + self.container.ipv4_addr() } } @@ -84,7 +84,7 @@ mod tests { #[test] fn tld_setup() -> Result<()> { let tld_ns = AuthoritativeNameServer::start(Domain::Tld { domain: "com." })?; - let ip_addr = tld_ns.ip_addr()?; + let ip_addr = tld_ns.ipv4_addr(); let client = Container::run()?; let output = client.exec(&["dig", &format!("@{ip_addr}"), "SOA", "com."])?; @@ -100,7 +100,7 @@ mod tests { #[test] fn root_setup() -> Result<()> { let root_ns = AuthoritativeNameServer::start(Domain::Root)?; - let ip_addr = root_ns.ip_addr()?; + let ip_addr = root_ns.ipv4_addr(); let client = Container::run()?; let output = client.exec(&["dig", &format!("@{ip_addr}"), "SOA", "."])?; diff --git a/src/container.rs b/src/container.rs index e52ca0ef..d9a64937 100644 --- a/src/container.rs +++ b/src/container.rs @@ -1,4 +1,6 @@ +use core::str; use std::fs; +use std::net::Ipv4Addr; use std::path::Path; use std::process::{self, Child, Output}; use std::process::{Command, Stdio}; @@ -10,8 +12,10 @@ use tempfile::NamedTempFile; use crate::Result; pub struct Container { - id: String, _name: String, + id: String, + // TODO probably also want the IPv6 address + ipv4_addr: Ipv4Addr, } impl Container { @@ -61,15 +65,16 @@ impl Container { return Err(format!("`{command:?}` failed").into()); } - let id = core::str::from_utf8(&output.stdout)?.trim().to_string(); + let id = str::from_utf8(&output.stdout)?.trim().to_string(); dbg!(&id); - let container = Self { + + let ipv4_addr = get_ipv4_addr(&id)?; + + Ok(Self { id, _name: container_name, - }; - dbg!(container.ip_addr()?); - - Ok(container) + ipv4_addr, + }) } pub fn cp(&self, path_in_container: &str, file_contents: &str, chmod: &str) -> Result<()> { @@ -114,29 +119,33 @@ impl Container { Ok(child) } - // TODO cache this to avoid calling `docker inspect` every time - pub fn ip_addr(&self) -> Result { - let mut command = Command::new("docker"); - command - .args([ - "inspect", - "-f", - "{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}", - ]) - .arg(&self.id); - - let output = command.output()?; - if !output.status.success() { - return Err(format!("`{command:?}` failed").into()); - } - - let ip_addr = core::str::from_utf8(&output.stdout)?.trim().to_string(); - dbg!(&ip_addr); - - Ok(ip_addr) + pub fn ipv4_addr(&self) -> Ipv4Addr { + self.ipv4_addr } } +// TODO cache this to avoid calling `docker inspect` every time +fn get_ipv4_addr(container_id: &str) -> Result { + let mut command = Command::new("docker"); + command + .args([ + "inspect", + "-f", + "{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}", + ]) + .arg(container_id); + + let output = command.output()?; + if !output.status.success() { + return Err(format!("`{command:?}` failed").into()); + } + + let ipv4_addr = str::from_utf8(&output.stdout)?.trim().to_string(); + dbg!(&ipv4_addr); + + Ok(ipv4_addr.parse()?) +} + // ensure the container gets deleted impl Drop for Container { fn drop(&mut self) { @@ -152,8 +161,6 @@ impl Drop for Container { #[cfg(test)] mod tests { - use std::net::Ipv4Addr; - use crate::CHMOD_RW_EVERYONE; use super::*; @@ -169,11 +176,12 @@ mod tests { } #[test] - fn ip_addr_works() -> Result<()> { + fn ipv4_addr_works() -> Result<()> { let container = Container::run()?; + let ipv4_addr = container.ipv4_addr(); - let ip_addr = container.ip_addr()?; - assert!(ip_addr.parse::().is_ok()); + let output = container.exec(&["ping", "-c1", &format!("{ipv4_addr}")])?; + assert!(output.status.success()); Ok(()) } diff --git a/src/recursive_resolver.rs b/src/recursive_resolver.rs index 7ab0c9fa..8259f150 100644 --- a/src/recursive_resolver.rs +++ b/src/recursive_resolver.rs @@ -1,3 +1,4 @@ +use std::net::Ipv4Addr; use std::process::Child; use serde::Serialize; @@ -13,7 +14,7 @@ pub struct RecursiveResolver { #[derive(Serialize)] pub struct RootServer { name: String, - ip_addr: String, + ip_addr: Ipv4Addr, } fn root_hints(roots: &[RootServer]) -> String { @@ -38,8 +39,8 @@ impl RecursiveResolver { Ok(Self { child, container }) } - pub fn ip_addr(&self) -> Result { - self.container.ip_addr() + pub fn ipv4_addr(&self) -> Ipv4Addr { + self.container.ipv4_addr() } } @@ -56,14 +57,15 @@ mod tests { use super::*; #[test] + #[ignore = "FIXME"] fn can_resolve() -> Result<()> { let root_ns = AuthoritativeNameServer::start(crate::Domain::Root)?; let roots = &[RootServer { name: "my.root-server.com".to_string(), - ip_addr: root_ns.ip_addr()?, + ip_addr: root_ns.ipv4_addr(), }]; let resolver = RecursiveResolver::start(roots)?; - let resolver_ip_addr = resolver.ip_addr()?; + let resolver_ip_addr = resolver.ipv4_addr(); let container = Container::run()?; let output = container.exec(&["dig", &format!("@{}", resolver_ip_addr), "example.com"])?; @@ -77,15 +79,15 @@ mod tests { #[test] fn root_hints_template_works() { let expected = [ - ("a.root-server.com", "172.17.0.1"), - ("b.root-server.com", "172.17.0.2"), + ("a.root-server.com", Ipv4Addr::new(172, 17, 0, 1)), + ("b.root-server.com", Ipv4Addr::new(172, 17, 0, 2)), ]; let roots = expected .iter() .map(|(ns_name, ip_addr)| RootServer { name: ns_name.to_string(), - ip_addr: ip_addr.to_string(), + ip_addr: *ip_addr, }) .collect::>(); @@ -117,7 +119,7 @@ mod tests { .unwrap(); assert_eq!(expected_ns_name, ns_name); assert_eq!("A", record_type); - assert_eq!(expected_ip_addr, ip_addr); + assert_eq!(expected_ip_addr.to_string(), ip_addr); } } } From cbbb12b3b55c864f81edb7419a119b4054b9e449 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 5 Feb 2024 15:03:57 +0100 Subject: [PATCH 018/124] refactor Container methods --- src/authoritative_name_server.rs | 18 ++--- src/container.rs | 123 ++++++++++++++++++++----------- src/lib.rs | 2 +- src/recursive_resolver.rs | 7 +- 4 files changed, 95 insertions(+), 55 deletions(-) diff --git a/src/authoritative_name_server.rs b/src/authoritative_name_server.rs index a8b50baa..4a02639a 100644 --- a/src/authoritative_name_server.rs +++ b/src/authoritative_name_server.rs @@ -13,9 +13,9 @@ impl AuthoritativeNameServer { pub fn start(domain: Domain) -> Result { let container = Container::run()?; - container.exec(&["mkdir", "-p", "/etc/nsd/zones"])?; - let zone_path = "/etc/nsd/zones/main.zone"; + container.status_ok(&["mkdir", "-p", "/etc/nsd/zones"])?; + let zone_path = "/etc/nsd/zones/main.zone"; container.cp( "/etc/nsd/nsd.conf", &nsd_conf(domain.fqdn()), @@ -87,12 +87,11 @@ mod tests { let ip_addr = tld_ns.ipv4_addr(); let client = Container::run()?; - let output = client.exec(&["dig", &format!("@{ip_addr}"), "SOA", "com."])?; + let output = client.output(&["dig", &format!("@{ip_addr}"), "SOA", "com."])?; assert!(output.status.success()); - let stdout = core::str::from_utf8(&output.stdout)?; - println!("{stdout}"); - assert!(stdout.contains("status: NOERROR")); + eprintln!("{}", output.stdout); + assert!(output.stdout.contains("status: NOERROR")); Ok(()) } @@ -103,12 +102,11 @@ mod tests { let ip_addr = root_ns.ipv4_addr(); let client = Container::run()?; - let output = client.exec(&["dig", &format!("@{ip_addr}"), "SOA", "."])?; + let output = client.output(&["dig", &format!("@{ip_addr}"), "SOA", "."])?; assert!(output.status.success()); - let stdout = core::str::from_utf8(&output.stdout)?; - println!("{stdout}"); - assert!(stdout.contains("status: NOERROR")); + eprintln!("{}", output.stdout); + assert!(output.stdout.contains("status: NOERROR")); Ok(()) } diff --git a/src/container.rs b/src/container.rs index d9a64937..2c49a247 100644 --- a/src/container.rs +++ b/src/container.rs @@ -2,17 +2,17 @@ use core::str; use std::fs; use std::net::Ipv4Addr; use std::path::Path; -use std::process::{self, Child, Output}; +use std::process::{self, Child, ExitStatus}; use std::process::{Command, Stdio}; use std::sync::atomic::AtomicUsize; use std::sync::{atomic, Once}; use tempfile::NamedTempFile; -use crate::Result; +use crate::{Error, Result}; pub struct Container { - _name: String, + name: String, id: String, // TODO probably also want the IPv6 address ipv4_addr: Ipv4Addr, @@ -33,7 +33,6 @@ impl Container { .join("docker") .join(format!("{binary}.Dockerfile")); let docker_dir_path = manifest_dir.join("docker"); - dbg!(&image_tag); let mut command = Command::new("docker"); command @@ -50,29 +49,23 @@ impl Container { let mut command = Command::new("docker"); let pid = process::id(); - let container_name = format!( - "{binary}-{pid}-{}", - COUNT.fetch_add(1, atomic::Ordering::Relaxed) - ); - command.args(["run", "--rm", "--detach", "--name", &container_name]); - let output = command + let count = COUNT.fetch_add(1, atomic::Ordering::Relaxed); + let name = format!("{binary}-{pid}-{count}"); + command + .args(["run", "--rm", "--detach", "--name", &name]) .arg("-it") .arg(image_tag) - .args(["sleep", "infinity"]) - .output()?; + .args(["sleep", "infinity"]); - if !output.status.success() { - return Err(format!("`{command:?}` failed").into()); - } - - let id = str::from_utf8(&output.stdout)?.trim().to_string(); + let output: Output = checked_output(&mut command)?.try_into()?; + let id = output.stdout; dbg!(&id); let ipv4_addr = get_ipv4_addr(&id)?; Ok(Self { id, - _name: container_name, + name, ipv4_addr, }) } @@ -86,37 +79,49 @@ impl Container { let mut command = Command::new("docker"); command.args(["cp", &src_path, &dest_path]); + checked_output(&mut command)?; - let status = command.status()?; - if !status.success() { - return Err(format!("`{command:?}` failed").into()); - } - - let command = &["chmod", chmod, path_in_container]; - let output = self.exec(command)?; - if !output.status.success() { - return Err(format!("`{command:?}` failed").into()); - } + self.status_ok(&["chmod", chmod, path_in_container])?; Ok(()) } - pub fn exec(&self, cmd: &[&str]) -> Result { + /// Similar to `std::process::Command::output` but runs `command_and_args` in the container + pub fn output(&self, command_and_args: &[&str]) -> Result { let mut command = Command::new("docker"); - command.args(["exec", "-t", &self.id]).args(cmd); + command + .args(["exec", "-t", &self.id]) + .args(command_and_args); - let output = command.output()?; + command.output()?.try_into() + } - Ok(output) + /// Similar to `std::process::Command::status` but runs `command_and_args` in the container + pub fn status(&self, command_and_args: &[&str]) -> Result { + let mut command = Command::new("docker"); + command + .args(["exec", "-t", &self.id]) + .args(command_and_args); + + Ok(command.status()?) + } + + /// Like `Self::status` but checks that `command_and_args` executed successfully + pub fn status_ok(&self, command_and_args: &[&str]) -> Result<()> { + let status = self.status(command_and_args)?; + + if status.success() { + Ok(()) + } else { + Err(format!("[{}] `{command_and_args:?}` failed", self.name).into()) + } } pub fn spawn(&self, cmd: &[&str]) -> Result { let mut command = Command::new("docker"); command.args(["exec", "-t", &self.id]).args(cmd); - let child = command.spawn()?; - - Ok(child) + Ok(command.spawn()?) } pub fn ipv4_addr(&self) -> Ipv4Addr { @@ -124,7 +129,44 @@ impl Container { } } -// TODO cache this to avoid calling `docker inspect` every time +#[derive(Debug)] +pub struct Output { + pub status: ExitStatus, + pub stderr: String, + pub stdout: String, +} + +impl TryFrom for Output { + type Error = Error; + + fn try_from(output: process::Output) -> Result { + let mut stderr = String::from_utf8(output.stderr)?; + while stderr.ends_with('\n') { + stderr.pop(); + } + + let mut stdout = String::from_utf8(output.stdout)?; + while stdout.ends_with('\n') { + stdout.pop(); + } + + Ok(Self { + status: output.status, + stderr, + stdout, + }) + } +} + +fn checked_output(command: &mut Command) -> Result { + let output = command.output()?; + if output.status.success() { + Ok(output) + } else { + Err(format!("`{command:?}` failed").into()) + } +} + fn get_ipv4_addr(container_id: &str) -> Result { let mut command = Command::new("docker"); command @@ -169,7 +211,7 @@ mod tests { fn run_works() -> Result<()> { let container = Container::run()?; - let output = container.exec(&["true"])?; + let output = container.output(&["true"])?; assert!(output.status.success()); Ok(()) @@ -180,7 +222,7 @@ mod tests { let container = Container::run()?; let ipv4_addr = container.ipv4_addr(); - let output = container.exec(&["ping", "-c1", &format!("{ipv4_addr}")])?; + let output = container.output(&["ping", "-c1", &format!("{ipv4_addr}")])?; assert!(output.status.success()); Ok(()) @@ -194,12 +236,11 @@ mod tests { let contents = "hello"; container.cp(path, contents, CHMOD_RW_EVERYONE)?; - let output = container.exec(&["cat", path])?; + let output = container.output(&["cat", path])?; dbg!(&output); assert!(output.status.success()); - - assert_eq!(contents, core::str::from_utf8(&output.stdout)?); + assert_eq!(contents, output.stdout); Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 3d526062..3ea4b81b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,7 @@ pub type Result = core::result::Result; const CHMOD_RW_EVERYONE: &str = "666"; mod authoritative_name_server; -mod container; +pub mod container; mod recursive_resolver; pub enum Domain<'a> { diff --git a/src/recursive_resolver.rs b/src/recursive_resolver.rs index 8259f150..1d27ee48 100644 --- a/src/recursive_resolver.rs +++ b/src/recursive_resolver.rs @@ -68,10 +68,11 @@ mod tests { let resolver_ip_addr = resolver.ipv4_addr(); let container = Container::run()?; - let output = container.exec(&["dig", &format!("@{}", resolver_ip_addr), "example.com"])?; + let output = + container.output(&["dig", &format!("@{}", resolver_ip_addr), "example.com"])?; - let stdout = core::str::from_utf8(&output.stdout)?; - assert!(stdout.contains("status: NOERROR")); + assert!(output.status.success()); + assert!(output.stdout.contains("status: NOERROR")); Ok(()) } From bab595a412126f49198093a5864f637d4e7f30ab Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 5 Feb 2024 15:44:25 +0100 Subject: [PATCH 019/124] simplify code with minijinja::render! --- src/authoritative_name_server.rs | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/src/authoritative_name_server.rs b/src/authoritative_name_server.rs index 4a02639a..a0b16859 100644 --- a/src/authoritative_name_server.rs +++ b/src/authoritative_name_server.rs @@ -1,7 +1,5 @@ use std::{net::Ipv4Addr, process::Child}; -use minijinja::{context, Environment}; - use crate::{container::Container, Domain, Result, CHMOD_RW_EVERYONE}; pub struct AuthoritativeNameServer { @@ -49,32 +47,23 @@ fn tld_zone(domain: &str) -> String { assert!(domain.ends_with('.')); assert!(!domain.starts_with('.')); - let mut env = Environment::new(); - let name = "main.zone"; - env.add_template(name, include_str!("templates/tld.zone.jinja")) - .unwrap(); - let template = env.get_template(name).unwrap(); - template.render(context! { tld => domain }).unwrap() + minijinja::render!( + include_str!("templates/tld.zone.jinja"), + tld => domain, + ) } fn root_zone() -> String { - let mut env = Environment::new(); - let name = "main.zone"; - env.add_template(name, include_str!("templates/root.zone.jinja")) - .unwrap(); - let template = env.get_template(name).unwrap(); - template.render(context! {}).unwrap() + minijinja::render!(include_str!("templates/root.zone.jinja"),) } fn nsd_conf(domain: &str) -> String { assert!(domain.ends_with('.')); - let mut env = Environment::new(); - let name = "nsd.conf"; - env.add_template(name, include_str!("templates/nsd.conf.jinja")) - .unwrap(); - let template = env.get_template(name).unwrap(); - template.render(context! { domain => domain }).unwrap() + minijinja::render!( + include_str!("templates/nsd.conf.jinja"), + domain => domain + ) } #[cfg(test)] From 1b0f1ef59f83677261278230a67c0aed9bf042b2 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 5 Feb 2024 15:53:48 +0100 Subject: [PATCH 020/124] move validation to Domain ctor --- src/authoritative_name_server.rs | 33 ++++++++++++++------------------ src/domain.rs | 32 +++++++++++++++++++++++++++++++ src/lib.rs | 16 ++-------------- src/recursive_resolver.rs | 4 ++-- 4 files changed, 50 insertions(+), 35 deletions(-) create mode 100644 src/domain.rs diff --git a/src/authoritative_name_server.rs b/src/authoritative_name_server.rs index a0b16859..17bd813e 100644 --- a/src/authoritative_name_server.rs +++ b/src/authoritative_name_server.rs @@ -1,4 +1,5 @@ -use std::{net::Ipv4Addr, process::Child}; +use std::net::Ipv4Addr; +use std::process::Child; use crate::{container::Container, Domain, Result, CHMOD_RW_EVERYONE}; @@ -14,15 +15,12 @@ impl AuthoritativeNameServer { container.status_ok(&["mkdir", "-p", "/etc/nsd/zones"])?; let zone_path = "/etc/nsd/zones/main.zone"; - container.cp( - "/etc/nsd/nsd.conf", - &nsd_conf(domain.fqdn()), - CHMOD_RW_EVERYONE, - )?; + container.cp("/etc/nsd/nsd.conf", &nsd_conf(domain), CHMOD_RW_EVERYONE)?; - let zone_file_contents = match domain { - Domain::Root => root_zone(), - Domain::Tld { domain } => tld_zone(domain), + let zone_file_contents = if domain.is_root() { + root_zone() + } else { + tld_zone(domain) }; container.cp(zone_path, &zone_file_contents, CHMOD_RW_EVERYONE)?; @@ -43,13 +41,12 @@ impl Drop for AuthoritativeNameServer { } } -fn tld_zone(domain: &str) -> String { - assert!(domain.ends_with('.')); - assert!(!domain.starts_with('.')); +fn tld_zone(domain: Domain) -> String { + assert!(!domain.is_root()); minijinja::render!( include_str!("templates/tld.zone.jinja"), - tld => domain, + tld => domain.as_str() ) } @@ -57,12 +54,10 @@ fn root_zone() -> String { minijinja::render!(include_str!("templates/root.zone.jinja"),) } -fn nsd_conf(domain: &str) -> String { - assert!(domain.ends_with('.')); - +fn nsd_conf(domain: Domain) -> String { minijinja::render!( include_str!("templates/nsd.conf.jinja"), - domain => domain + domain => domain.as_str() ) } @@ -72,7 +67,7 @@ mod tests { #[test] fn tld_setup() -> Result<()> { - let tld_ns = AuthoritativeNameServer::start(Domain::Tld { domain: "com." })?; + let tld_ns = AuthoritativeNameServer::start(Domain("com.")?)?; let ip_addr = tld_ns.ipv4_addr(); let client = Container::run()?; @@ -87,7 +82,7 @@ mod tests { #[test] fn root_setup() -> Result<()> { - let root_ns = AuthoritativeNameServer::start(Domain::Root)?; + let root_ns = AuthoritativeNameServer::start(Domain::ROOT)?; let ip_addr = root_ns.ipv4_addr(); let client = Container::run()?; diff --git a/src/domain.rs b/src/domain.rs new file mode 100644 index 00000000..2d05689e --- /dev/null +++ b/src/domain.rs @@ -0,0 +1,32 @@ +use crate::Result; + +#[derive(Clone, Copy)] +pub struct Domain<'a> { + inner: &'a str, +} + +// TODO likely needs further validation +#[allow(non_snake_case)] +pub fn Domain(input: &str) -> Result> { + if !input.ends_with('.') { + return Err("domain must end with a `.`".into()); + } + + if input != "." && input.starts_with('.') { + return Err("non-root domain cannot start with a `.`".into()); + } + + Ok(Domain { inner: input }) +} + +impl<'a> Domain<'a> { + pub const ROOT: Domain<'static> = Domain { inner: "." }; + + pub fn is_root(&self) -> bool { + self.inner == "." + } + + pub fn as_str(&self) -> &'a str { + self.inner + } +} diff --git a/src/lib.rs b/src/lib.rs index 3ea4b81b..4b74a65f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub use crate::authoritative_name_server::AuthoritativeNameServer; +pub use crate::domain::Domain; pub use crate::recursive_resolver::RecursiveResolver; pub type Error = Box; @@ -8,18 +9,5 @@ const CHMOD_RW_EVERYONE: &str = "666"; mod authoritative_name_server; pub mod container; +mod domain; mod recursive_resolver; - -pub enum Domain<'a> { - Root, - Tld { domain: &'a str }, -} - -impl Domain<'_> { - fn fqdn(&self) -> &str { - match self { - Domain::Root => ".", - Domain::Tld { domain } => domain, - } - } -} diff --git a/src/recursive_resolver.rs b/src/recursive_resolver.rs index 1d27ee48..14efbc47 100644 --- a/src/recursive_resolver.rs +++ b/src/recursive_resolver.rs @@ -52,14 +52,14 @@ impl Drop for RecursiveResolver { #[cfg(test)] mod tests { - use crate::AuthoritativeNameServer; + use crate::{AuthoritativeNameServer, Domain}; use super::*; #[test] #[ignore = "FIXME"] fn can_resolve() -> Result<()> { - let root_ns = AuthoritativeNameServer::start(crate::Domain::Root)?; + let root_ns = AuthoritativeNameServer::start(Domain::ROOT)?; let roots = &[RootServer { name: "my.root-server.com".to_string(), ip_addr: root_ns.ipv4_addr(), From 7e9f63d85ee7ab62992f5ff09ba3408588888735 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 5 Feb 2024 15:55:01 +0100 Subject: [PATCH 021/124] fix non-fatal NSD error about PID file --- src/authoritative_name_server.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/authoritative_name_server.rs b/src/authoritative_name_server.rs index 17bd813e..61b1d5b3 100644 --- a/src/authoritative_name_server.rs +++ b/src/authoritative_name_server.rs @@ -12,8 +12,10 @@ impl AuthoritativeNameServer { pub fn start(domain: Domain) -> Result { let container = Container::run()?; - container.status_ok(&["mkdir", "-p", "/etc/nsd/zones"])?; + // for PID file + container.status_ok(&["mkdir", "-p", "/run/nsd/"])?; + container.status_ok(&["mkdir", "-p", "/etc/nsd/zones"])?; let zone_path = "/etc/nsd/zones/main.zone"; container.cp("/etc/nsd/nsd.conf", &nsd_conf(domain), CHMOD_RW_EVERYONE)?; From 984a05e873d6b7c4c0c4b128b627faca83ddbea6 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 5 Feb 2024 18:33:04 +0100 Subject: [PATCH 022/124] revamp zone file generation --- Cargo.lock | 1 - Cargo.toml | 3 - src/authoritative_name_server.rs | 88 ++++++---- src/container.rs | 2 - src/domain.rs | 35 +++- src/lib.rs | 8 + src/record.rs | 293 +++++++++++++++++++++++++++++++ src/recursive_resolver.rs | 99 +++-------- src/templates/root.hints.jinja | 4 - src/templates/root.zone.jinja | 12 -- src/templates/tld.zone.jinja | 12 -- 11 files changed, 412 insertions(+), 145 deletions(-) create mode 100644 src/record.rs delete mode 100644 src/templates/root.hints.jinja delete mode 100644 src/templates/root.zone.jinja delete mode 100644 src/templates/tld.zone.jinja diff --git a/Cargo.lock b/Cargo.lock index 4ae8393f..9b6bb82d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,7 +25,6 @@ name = "dnssec-tests" version = "0.1.0" dependencies = [ "minijinja", - "serde", "tempfile", ] diff --git a/Cargo.toml b/Cargo.toml index 03007d0b..50ccdc0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,9 +4,6 @@ version = "0.1.0" edition = "2021" license = "MIT or Apache 2.0" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] minijinja = "1.0.12" -serde = { version = "1.0.196", features = ["derive"] } tempfile = "3.9.0" diff --git a/src/authoritative_name_server.rs b/src/authoritative_name_server.rs index 61b1d5b3..bde2b3b5 100644 --- a/src/authoritative_name_server.rs +++ b/src/authoritative_name_server.rs @@ -1,15 +1,18 @@ use std::net::Ipv4Addr; use std::process::Child; -use crate::{container::Container, Domain, Result, CHMOD_RW_EVERYONE}; +use crate::container::Container; +use crate::record::{self, Referral, SoaSettings, Zone}; +use crate::{Domain, Result, CHMOD_RW_EVERYONE}; -pub struct AuthoritativeNameServer { +pub struct AuthoritativeNameServer<'a> { child: Child, container: Container, + zone: Zone<'a>, } -impl AuthoritativeNameServer { - pub fn start(domain: Domain) -> Result { +impl<'a> AuthoritativeNameServer<'a> { + pub fn start(domain: Domain<'a>, referrals: &[Referral<'a>]) -> Result { let container = Container::run()?; // for PID file @@ -17,46 +20,48 @@ impl AuthoritativeNameServer { container.status_ok(&["mkdir", "-p", "/etc/nsd/zones"])?; let zone_path = "/etc/nsd/zones/main.zone"; - container.cp("/etc/nsd/nsd.conf", &nsd_conf(domain), CHMOD_RW_EVERYONE)?; + container.cp("/etc/nsd/nsd.conf", &nsd_conf(&domain), CHMOD_RW_EVERYONE)?; - let zone_file_contents = if domain.is_root() { - root_zone() - } else { - tld_zone(domain) + let ns_count = crate::nameserver_count(); + let ns = Domain(format!("primary.ns{ns_count}.com."))?; + let soa = record::Soa { + domain: domain.clone(), + ns, + admin: Domain(format!("admin.ns{ns_count}.com."))?, + settings: SoaSettings::default(), }; + let mut zone = Zone::new(domain, soa); + for referral in referrals { + zone.referral(referral) + } - container.cp(zone_path, &zone_file_contents, CHMOD_RW_EVERYONE)?; + container.cp(zone_path, &zone.to_string(), CHMOD_RW_EVERYONE)?; let child = container.spawn(&["nsd", "-d"])?; - Ok(Self { child, container }) + Ok(Self { + child, + container, + zone, + }) } pub fn ipv4_addr(&self) -> Ipv4Addr { self.container.ipv4_addr() } + + pub fn nameserver(&self) -> &Domain<'a> { + &self.zone.soa.ns + } } -impl Drop for AuthoritativeNameServer { +impl Drop for AuthoritativeNameServer<'_> { fn drop(&mut self) { let _ = self.child.kill(); } } -fn tld_zone(domain: Domain) -> String { - assert!(!domain.is_root()); - - minijinja::render!( - include_str!("templates/tld.zone.jinja"), - tld => domain.as_str() - ) -} - -fn root_zone() -> String { - minijinja::render!(include_str!("templates/root.zone.jinja"),) -} - -fn nsd_conf(domain: Domain) -> String { +fn nsd_conf(domain: &Domain) -> String { minijinja::render!( include_str!("templates/nsd.conf.jinja"), domain => domain.as_str() @@ -68,8 +73,8 @@ mod tests { use super::*; #[test] - fn tld_setup() -> Result<()> { - let tld_ns = AuthoritativeNameServer::start(Domain("com.")?)?; + fn tld_ns() -> Result<()> { + let tld_ns = AuthoritativeNameServer::start(Domain("com.")?, &[])?; let ip_addr = tld_ns.ipv4_addr(); let client = Container::run()?; @@ -83,8 +88,8 @@ mod tests { } #[test] - fn root_setup() -> Result<()> { - let root_ns = AuthoritativeNameServer::start(Domain::ROOT)?; + fn root_ns() -> Result<()> { + let root_ns = AuthoritativeNameServer::start(Domain::ROOT, &[])?; let ip_addr = root_ns.ipv4_addr(); let client = Container::run()?; @@ -96,4 +101,27 @@ mod tests { Ok(()) } + + #[test] + fn root_ns_with_referral() -> Result<()> { + let expected_ip_addr = Ipv4Addr::new(172, 17, 200, 1); + let root_ns = AuthoritativeNameServer::start( + Domain::ROOT, + &[Referral { + domain: Domain("com.")?, + ipv4_addr: expected_ip_addr, + ns: Domain("primary.tld-server.com.")?, + }], + )?; + let ip_addr = root_ns.ipv4_addr(); + + let client = Container::run()?; + let output = client.output(&["dig", &format!("@{ip_addr}"), "NS", "com."])?; + + assert!(output.status.success()); + eprintln!("{}", output.stdout); + assert!(output.stdout.contains("status: NOERROR")); + + Ok(()) + } } diff --git a/src/container.rs b/src/container.rs index 2c49a247..e7f12759 100644 --- a/src/container.rs +++ b/src/container.rs @@ -59,7 +59,6 @@ impl Container { let output: Output = checked_output(&mut command)?.try_into()?; let id = output.stdout; - dbg!(&id); let ipv4_addr = get_ipv4_addr(&id)?; @@ -183,7 +182,6 @@ fn get_ipv4_addr(container_id: &str) -> Result { } let ipv4_addr = str::from_utf8(&output.stdout)?.trim().to_string(); - dbg!(&ipv4_addr); Ok(ipv4_addr.parse()?) } diff --git a/src/domain.rs b/src/domain.rs index 2d05689e..922347cd 100644 --- a/src/domain.rs +++ b/src/domain.rs @@ -1,13 +1,17 @@ +use core::fmt; +use std::borrow::Cow; + use crate::Result; -#[derive(Clone, Copy)] +#[derive(Clone)] pub struct Domain<'a> { - inner: &'a str, + inner: Cow<'a, str>, } // TODO likely needs further validation #[allow(non_snake_case)] -pub fn Domain(input: &str) -> Result> { +pub fn Domain<'a>(input: impl Into>) -> Result> { + let input = input.into(); if !input.ends_with('.') { return Err("domain must end with a `.`".into()); } @@ -20,13 +24,32 @@ pub fn Domain(input: &str) -> Result> { } impl<'a> Domain<'a> { - pub const ROOT: Domain<'static> = Domain { inner: "." }; + pub const ROOT: Domain<'static> = Domain { + inner: Cow::Borrowed("."), + }; pub fn is_root(&self) -> bool { self.inner == "." } - pub fn as_str(&self) -> &'a str { - self.inner + pub fn as_str(&self) -> &str { + &self.inner + } + + pub fn into_owned(self) -> Domain<'static> { + let owned = match self.inner { + Cow::Borrowed(borrowed) => borrowed.to_string(), + Cow::Owned(owned) => owned, + }; + + Domain { + inner: Cow::Owned(owned), + } + } +} + +impl fmt::Display for Domain<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.inner) } } diff --git a/src/lib.rs b/src/lib.rs index 4b74a65f..82c2a386 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +use std::sync::atomic::{self, AtomicUsize}; + pub use crate::authoritative_name_server::AuthoritativeNameServer; pub use crate::domain::Domain; pub use crate::recursive_resolver::RecursiveResolver; @@ -10,4 +12,10 @@ const CHMOD_RW_EVERYONE: &str = "666"; mod authoritative_name_server; pub mod container; mod domain; +pub mod record; mod recursive_resolver; + +fn nameserver_count() -> usize { + static COUNT: AtomicUsize = AtomicUsize::new(0); + COUNT.fetch_add(1, atomic::Ordering::Relaxed) +} diff --git a/src/record.rs b/src/record.rs new file mode 100644 index 00000000..aaafbb9d --- /dev/null +++ b/src/record.rs @@ -0,0 +1,293 @@ +//! DNS records in BIND syntax +//! +//! Note that the `@` syntax is not used to avoid relying on the order of the records + +use core::fmt; +use std::net::Ipv4Addr; + +use crate::Domain; + +pub struct Zone<'a> { + pub origin: Domain<'a>, + pub ttl: u32, + pub soa: Soa<'a>, + pub records: Vec>, +} + +impl<'a> Zone<'a> { + /// Convenience constructor that uses "reasonable" defaults + pub fn new(origin: Domain<'a>, soa: Soa<'a>) -> Self { + Self { + origin, + ttl: 1800, + soa, + records: Vec::new(), + } + } + + /// Appends a record + pub fn record(&mut self, record: impl Into>) { + self.records.push(record.into()) + } + + /// Appends a NS + A record pair + pub fn referral(&mut self, referral: &Referral<'a>) { + let Referral { + domain, + ipv4_addr, + ns, + } = referral; + + self.record(Ns { + domain: domain.clone(), + ns: ns.clone(), + }); + self.record(A { + domain: domain.clone(), + ipv4_addr: *ipv4_addr, + }); + } +} + +impl fmt::Display for Zone<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + origin, + ttl, + soa, + records, + } = self; + + writeln!(f, "$ORIGIN {origin}")?; + writeln!(f, "$TTL {ttl}")?; + writeln!(f, "{soa}")?; + + for record in records { + writeln!(f, "{record}")?; + } + + Ok(()) + } +} + +pub struct Referral<'a> { + pub domain: Domain<'a>, + pub ipv4_addr: Ipv4Addr, + pub ns: Domain<'a>, +} + +pub struct Root<'a> { + pub ipv4_addr: Ipv4Addr, + pub ns: Domain<'a>, + pub ttl: u32, +} + +impl<'a> Root<'a> { + /// Convenience constructor that uses "reasonable" defaults + pub fn new(ns: Domain<'a>, ipv4_addr: Ipv4Addr) -> Self { + Self { + ipv4_addr, + ns, + ttl: 3600000, // 1000 hours + } + } +} + +impl fmt::Display for Root<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { ipv4_addr, ns, ttl } = self; + + writeln!(f, ".\t{ttl}\tNS\t{ns}")?; + write!(f, "{ns}\t{ttl}\tA\t{ipv4_addr}") + } +} + +pub enum Record<'a> { + A(A<'a>), + Ns(Ns<'a>), +} + +impl<'a> From> for Record<'a> { + fn from(v: A<'a>) -> Self { + Self::A(v) + } +} + +impl<'a> From> for Record<'a> { + fn from(v: Ns<'a>) -> Self { + Self::Ns(v) + } +} + +impl fmt::Display for Record<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Record::A(a) => a.fmt(f), + Record::Ns(ns) => ns.fmt(f), + } + } +} + +pub struct A<'a> { + pub domain: Domain<'a>, + pub ipv4_addr: Ipv4Addr, +} + +impl fmt::Display for A<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { domain, ipv4_addr } = self; + + write!(f, "{domain}\tIN\tA\t{ipv4_addr}") + } +} + +pub struct Ns<'a> { + pub domain: Domain<'a>, + pub ns: Domain<'a>, +} + +impl fmt::Display for Ns<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { domain, ns } = self; + + write!(f, "{domain}\tIN\tNS\t{ns}") + } +} + +pub struct Soa<'a> { + pub domain: Domain<'a>, + pub ns: Domain<'a>, + pub admin: Domain<'a>, + pub settings: SoaSettings, +} + +impl fmt::Display for Soa<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + domain, + ns, + admin, + settings, + } = self; + + write!(f, "{domain}\tIN\tSOA\t{ns}\t{admin}\t{settings}") + } +} + +pub struct SoaSettings { + pub serial: u32, + pub refresh: u32, + pub retry: u32, + pub expire: u32, + pub minimum: u32, +} + +impl Default for SoaSettings { + fn default() -> Self { + Self { + serial: 2024010101, + refresh: 1800, // 30 minutes + retry: 900, // 15 minutes + expire: 604800, // 1 week + minimum: 86400, // 1 day + } + } +} + +impl fmt::Display for SoaSettings { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + serial, + refresh, + retry, + expire, + minimum, + } = self; + + write!(f, "( {serial} {refresh} {retry} {expire} {minimum} )") + } +} + +#[cfg(test)] +mod tests { + use crate::Result; + + use super::*; + + #[test] + fn a_to_string() -> Result<()> { + let expected = "e.gtld-servers.net. IN A 192.12.94.30"; + let a = example_a()?; + assert_eq!(expected, a.to_string()); + + Ok(()) + } + + #[test] + fn ns_to_string() -> Result<()> { + let expected = "com. IN NS e.gtld-servers.net."; + let ns = example_ns()?; + assert_eq!(expected, ns.to_string()); + + Ok(()) + } + + #[test] + fn root_to_string() -> Result<()> { + let expected = ". 3600000 NS a.root-servers.net. +a.root-servers.net. 3600000 A 198.41.0.4"; + let root = Root::new(Domain("a.root-servers.net.")?, Ipv4Addr::new(198, 41, 0, 4)); + assert_eq!(expected, root.to_string()); + Ok(()) + } + + #[test] + fn soa_to_string() -> Result<()> { + let expected = + ". IN SOA a.root-servers.net. nstld.verisign-grs.com. ( 2024010101 1800 900 604800 86400 )"; + let soa = example_soa()?; + assert_eq!(expected, soa.to_string()); + + Ok(()) + } + + #[test] + fn zone_file_to_string() -> Result<()> { + let expected = "$ORIGIN . +$TTL 1800 +. IN SOA a.root-servers.net. nstld.verisign-grs.com. ( 2024010101 1800 900 604800 86400 ) +com. IN NS e.gtld-servers.net. +e.gtld-servers.net. IN A 192.12.94.30 +"; + let mut zone = Zone::new(Domain::ROOT, example_soa()?); + zone.record(example_ns()?); + zone.record(example_a()?); + + assert_eq!(expected, zone.to_string()); + + Ok(()) + } + + fn example_a() -> Result> { + Ok(A { + domain: Domain("e.gtld-servers.net.")?, + ipv4_addr: Ipv4Addr::new(192, 12, 94, 30), + }) + } + + fn example_ns() -> Result> { + Ok(Ns { + domain: Domain("com.")?, + ns: Domain("e.gtld-servers.net.")?, + }) + } + + fn example_soa() -> Result> { + Ok(Soa { + domain: Domain(".")?, + ns: Domain("a.root-servers.net.")?, + admin: Domain("nstld.verisign-grs.com.")?, + settings: SoaSettings::default(), + }) + } +} diff --git a/src/recursive_resolver.rs b/src/recursive_resolver.rs index 14efbc47..ed823bc5 100644 --- a/src/recursive_resolver.rs +++ b/src/recursive_resolver.rs @@ -1,9 +1,9 @@ +use core::fmt::Write; use std::net::Ipv4Addr; use std::process::Child; -use serde::Serialize; - use crate::container::Container; +use crate::record::Root; use crate::{Result, CHMOD_RW_EVERYONE}; pub struct RecursiveResolver { @@ -11,28 +11,16 @@ pub struct RecursiveResolver { child: Child, } -#[derive(Serialize)] -pub struct RootServer { - name: String, - ip_addr: Ipv4Addr, -} - -fn root_hints(roots: &[RootServer]) -> String { - minijinja::render!( - include_str!("templates/root.hints.jinja"), - roots => roots - ) -} - impl RecursiveResolver { - pub fn start(root_servers: &[RootServer]) -> Result { + pub fn start(roots: &[Root]) -> Result { let container = Container::run()?; - container.cp( - "/etc/unbound/root.hints", - &root_hints(root_servers), - CHMOD_RW_EVERYONE, - )?; + let mut hints = String::new(); + for root in roots { + writeln!(hints, "{root}").unwrap(); + } + + container.cp("/etc/unbound/root.hints", &hints, CHMOD_RW_EVERYONE)?; let child = container.spawn(&["unbound", "-d"])?; @@ -52,18 +40,24 @@ impl Drop for RecursiveResolver { #[cfg(test)] mod tests { - use crate::{AuthoritativeNameServer, Domain}; + use crate::{record::Referral, AuthoritativeNameServer, Domain}; use super::*; #[test] - #[ignore = "FIXME"] fn can_resolve() -> Result<()> { - let root_ns = AuthoritativeNameServer::start(Domain::ROOT)?; - let roots = &[RootServer { - name: "my.root-server.com".to_string(), - ip_addr: root_ns.ipv4_addr(), - }]; + let tld_ns = AuthoritativeNameServer::start(Domain("com.")?, &[])?; + + let root_ns = AuthoritativeNameServer::start( + Domain::ROOT, + &[Referral { + domain: Domain("com.")?, + ipv4_addr: tld_ns.ipv4_addr(), + ns: tld_ns.nameserver().clone(), + }], + )?; + + let roots = &[Root::new(root_ns.nameserver().clone(), root_ns.ipv4_addr())]; let resolver = RecursiveResolver::start(roots)?; let resolver_ip_addr = resolver.ipv4_addr(); @@ -71,56 +65,11 @@ mod tests { let output = container.output(&["dig", &format!("@{}", resolver_ip_addr), "example.com"])?; + eprintln!("{}", output.stdout); + assert!(output.status.success()); assert!(output.stdout.contains("status: NOERROR")); Ok(()) } - - #[test] - fn root_hints_template_works() { - let expected = [ - ("a.root-server.com", Ipv4Addr::new(172, 17, 0, 1)), - ("b.root-server.com", Ipv4Addr::new(172, 17, 0, 2)), - ]; - - let roots = expected - .iter() - .map(|(ns_name, ip_addr)| RootServer { - name: ns_name.to_string(), - ip_addr: *ip_addr, - }) - .collect::>(); - - let hints = root_hints(&roots); - - eprintln!("{hints}"); - let lines = hints.lines().collect::>(); - - for (lines, (expected_ns_name, expected_ip_addr)) in lines.chunks(2).zip(expected) { - let [ns_record, a_record] = lines.try_into().unwrap(); - - // block to avoid shadowing - { - let [domain, _ttl, record_type, ns_name] = ns_record - .split_whitespace() - .collect::>() - .try_into() - .unwrap(); - - assert_eq!(".", domain); - assert_eq!("NS", record_type); - assert_eq!(expected_ns_name, ns_name); - } - - let [ns_name, _ttl, record_type, ip_addr] = a_record - .split_whitespace() - .collect::>() - .try_into() - .unwrap(); - assert_eq!(expected_ns_name, ns_name); - assert_eq!("A", record_type); - assert_eq!(expected_ip_addr.to_string(), ip_addr); - } - } } diff --git a/src/templates/root.hints.jinja b/src/templates/root.hints.jinja deleted file mode 100644 index 180fe2c5..00000000 --- a/src/templates/root.hints.jinja +++ /dev/null @@ -1,4 +0,0 @@ -{%- for root in roots -%} -. 3600000 NS {{ root.name }} -{{ root.name }} 3600000 A {{ root.ip_addr }} -{% endfor %} diff --git a/src/templates/root.zone.jinja b/src/templates/root.zone.jinja deleted file mode 100644 index e5712ffd..00000000 --- a/src/templates/root.zone.jinja +++ /dev/null @@ -1,12 +0,0 @@ -$ORIGIN . -$TTL 1800 -@ IN SOA primary.root-server.com admin.root-server.com ( - 2014010100 ; Serial - 10800 ; Refresh (3 hours) - 900 ; Retry (15 minutes) - 604800 ; Expire (1 week) - 86400 ; Minimum (1 day) - ) -@ IN NS primary.root-server.com - -; TODO referral diff --git a/src/templates/tld.zone.jinja b/src/templates/tld.zone.jinja deleted file mode 100644 index f16feb2a..00000000 --- a/src/templates/tld.zone.jinja +++ /dev/null @@ -1,12 +0,0 @@ -$ORIGIN {{ tld }} -$TTL 1800 -@ IN SOA primary.tld-server.{{ tld }} admin.tld-server.{{ tld }} ( - 2014010100 ; Serial - 10800 ; Refresh (3 hours) - 900 ; Retry (15 minutes) - 604800 ; Expire (1 week) - 86400 ; Minimum (1 day) - ) -@ IN NS primary.tld-server.{{ tld }} - -; intentionally blank From d13186e4049202e9f6eb268929a695aec833efe2 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 5 Feb 2024 19:21:52 +0100 Subject: [PATCH 023/124] make resolution test work --- Cargo.toml | 3 + src/authoritative_name_server.rs | 132 +++++++++++++++++++++++-------- src/record.rs | 3 +- src/recursive_resolver.rs | 74 +++++++++++++++-- 4 files changed, 172 insertions(+), 40 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 50ccdc0a..72d5349d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,3 +7,6 @@ license = "MIT or Apache 2.0" [dependencies] minijinja = "1.0.12" tempfile = "3.9.0" + +[lib] +doctest = false diff --git a/src/authoritative_name_server.rs b/src/authoritative_name_server.rs index bde2b3b5..1e2a9563 100644 --- a/src/authoritative_name_server.rs +++ b/src/authoritative_name_server.rs @@ -12,40 +12,26 @@ pub struct AuthoritativeNameServer<'a> { } impl<'a> AuthoritativeNameServer<'a> { - pub fn start(domain: Domain<'a>, referrals: &[Referral<'a>]) -> Result { - let container = Container::run()?; - - // for PID file - container.status_ok(&["mkdir", "-p", "/run/nsd/"])?; - - container.status_ok(&["mkdir", "-p", "/etc/nsd/zones"])?; - let zone_path = "/etc/nsd/zones/main.zone"; - container.cp("/etc/nsd/nsd.conf", &nsd_conf(&domain), CHMOD_RW_EVERYONE)?; - + /// Spins up a container in a parked state where the name server is not running yet + pub fn reserve() -> Result { let ns_count = crate::nameserver_count(); - let ns = Domain(format!("primary.ns{ns_count}.com."))?; - let soa = record::Soa { - domain: domain.clone(), - ns, - admin: Domain(format!("admin.ns{ns_count}.com."))?, - settings: SoaSettings::default(), - }; - let mut zone = Zone::new(domain, soa); - for referral in referrals { - zone.referral(referral) - } + let nameserver = primary_ns(ns_count); - container.cp(zone_path, &zone.to_string(), CHMOD_RW_EVERYONE)?; - - let child = container.spawn(&["nsd", "-d"])?; - - Ok(Self { - child, - container, - zone, + Ok(StoppedAuthoritativeNameServer { + container: Container::run()?, + nameserver, + ns_count, }) } + pub fn start( + domain: Domain<'a>, + referrals: &[Referral<'a>], + a_records: &[record::A<'a>], + ) -> Result { + Self::reserve()?.start(domain, referrals, a_records) + } + pub fn ipv4_addr(&self) -> Ipv4Addr { self.container.ipv4_addr() } @@ -53,6 +39,10 @@ impl<'a> AuthoritativeNameServer<'a> { pub fn nameserver(&self) -> &Domain<'a> { &self.zone.soa.ns } + + pub fn zone(&self) -> &Zone<'a> { + &self.zone + } } impl Drop for AuthoritativeNameServer<'_> { @@ -61,6 +51,85 @@ impl Drop for AuthoritativeNameServer<'_> { } } +fn primary_ns(ns_count: usize) -> Domain<'static> { + Domain(format!("primary{ns_count}.nameservers.com.")).unwrap() +} + +fn admin_ns(ns_count: usize) -> Domain<'static> { + Domain(format!("admin{ns_count}.nameservers.com.")).unwrap() +} + +pub struct StoppedAuthoritativeNameServer { + container: Container, + nameserver: Domain<'static>, + ns_count: usize, +} + +impl StoppedAuthoritativeNameServer { + pub fn ipv4_addr(&self) -> Ipv4Addr { + self.container.ipv4_addr() + } + + pub fn nameserver(&self) -> &Domain<'static> { + &self.nameserver + } + + pub fn start<'a>( + self, + domain: Domain<'a>, + referrals: &[Referral<'a>], + a_records: &[record::A<'a>], + ) -> Result> { + let Self { + container, + nameserver, + ns_count, + } = self; + + // for PID file + container.status_ok(&["mkdir", "-p", "/run/nsd/"])?; + + container.status_ok(&["mkdir", "-p", "/etc/nsd/zones"])?; + let zone_path = "/etc/nsd/zones/main.zone"; + container.cp("/etc/nsd/nsd.conf", &nsd_conf(&domain), CHMOD_RW_EVERYONE)?; + + let soa = record::Soa { + domain: domain.clone(), + ns: nameserver.clone(), + admin: admin_ns(ns_count), + settings: SoaSettings::default(), + }; + let mut zone = Zone::new(domain.clone(), soa); + + zone.record(record::Ns { + domain: domain.clone(), + ns: nameserver, + }); + zone.record(record::A { + domain, + ipv4_addr: container.ipv4_addr(), + }); + + for referral in referrals { + zone.referral(referral) + } + + for a in a_records { + zone.record(a.clone()) + } + + container.cp(zone_path, &zone.to_string(), CHMOD_RW_EVERYONE)?; + + let child = container.spawn(&["nsd", "-d"])?; + + Ok(AuthoritativeNameServer { + child, + container, + zone, + }) + } +} + fn nsd_conf(domain: &Domain) -> String { minijinja::render!( include_str!("templates/nsd.conf.jinja"), @@ -74,7 +143,7 @@ mod tests { #[test] fn tld_ns() -> Result<()> { - let tld_ns = AuthoritativeNameServer::start(Domain("com.")?, &[])?; + let tld_ns = AuthoritativeNameServer::start(Domain("com.")?, &[], &[])?; let ip_addr = tld_ns.ipv4_addr(); let client = Container::run()?; @@ -89,7 +158,7 @@ mod tests { #[test] fn root_ns() -> Result<()> { - let root_ns = AuthoritativeNameServer::start(Domain::ROOT, &[])?; + let root_ns = AuthoritativeNameServer::start(Domain::ROOT, &[], &[])?; let ip_addr = root_ns.ipv4_addr(); let client = Container::run()?; @@ -112,6 +181,7 @@ mod tests { ipv4_addr: expected_ip_addr, ns: Domain("primary.tld-server.com.")?, }], + &[], )?; let ip_addr = root_ns.ipv4_addr(); diff --git a/src/record.rs b/src/record.rs index aaafbb9d..c659e93a 100644 --- a/src/record.rs +++ b/src/record.rs @@ -43,7 +43,7 @@ impl<'a> Zone<'a> { ns: ns.clone(), }); self.record(A { - domain: domain.clone(), + domain: ns.clone(), ipv4_addr: *ipv4_addr, }); } @@ -128,6 +128,7 @@ impl fmt::Display for Record<'_> { } } +#[derive(Clone)] pub struct A<'a> { pub domain: Domain<'a>, pub ipv4_addr: Ipv4Addr, diff --git a/src/recursive_resolver.rs b/src/recursive_resolver.rs index ed823bc5..c246fadc 100644 --- a/src/recursive_resolver.rs +++ b/src/recursive_resolver.rs @@ -40,36 +40,94 @@ impl Drop for RecursiveResolver { #[cfg(test)] mod tests { - use crate::{record::Referral, AuthoritativeNameServer, Domain}; + use crate::{ + record::{self, Referral}, + AuthoritativeNameServer, Domain, + }; use super::*; #[test] fn can_resolve() -> Result<()> { - let tld_ns = AuthoritativeNameServer::start(Domain("com.")?, &[])?; + let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4); + let needle = Domain("example.nameservers.com.")?; + let root_ns = AuthoritativeNameServer::reserve()?; + let com_ns = AuthoritativeNameServer::reserve()?; - let root_ns = AuthoritativeNameServer::start( + let nameservers_domain = Domain("nameservers.com.")?; + let nameservers_ns = AuthoritativeNameServer::start( + nameservers_domain.clone(), + &[], + &[ + record::A { + domain: root_ns.nameserver().clone(), + ipv4_addr: root_ns.ipv4_addr(), + }, + record::A { + domain: com_ns.nameserver().clone(), + ipv4_addr: com_ns.ipv4_addr(), + }, + record::A { + domain: needle.clone(), + ipv4_addr: expected_ipv4_addr, + }, + ], + )?; + + eprintln!("nameservers.com.zone:\n{}", nameservers_ns.zone()); + + let com_domain = Domain("com.")?; + let com_ns = com_ns.start( + com_domain.clone(), + &[Referral { + domain: nameservers_domain, + ipv4_addr: nameservers_ns.ipv4_addr(), + ns: nameservers_ns.nameserver().clone(), + }], + &[], + )?; + + eprintln!("com.zone:\n{}", com_ns.zone()); + + let root_ns = root_ns.start( Domain::ROOT, &[Referral { - domain: Domain("com.")?, - ipv4_addr: tld_ns.ipv4_addr(), - ns: tld_ns.nameserver().clone(), + domain: com_domain, + ipv4_addr: com_ns.ipv4_addr(), + ns: com_ns.nameserver().clone(), }], + &[], )?; + eprintln!("root.zone:\n{}", root_ns.zone()); + let roots = &[Root::new(root_ns.nameserver().clone(), root_ns.ipv4_addr())]; let resolver = RecursiveResolver::start(roots)?; let resolver_ip_addr = resolver.ipv4_addr(); let container = Container::run()?; - let output = - container.output(&["dig", &format!("@{}", resolver_ip_addr), "example.com"])?; + let output = container.output(&[ + "dig", + &format!("@{}", resolver_ip_addr), + &needle.to_string(), + ])?; eprintln!("{}", output.stdout); assert!(output.status.success()); assert!(output.stdout.contains("status: NOERROR")); + let mut found = false; + let needle = needle.to_string(); + for line in output.stdout.lines() { + if line.starts_with(&needle) { + found = true; + assert!(line.ends_with(&expected_ipv4_addr.to_string())); + } + } + + assert!(found); + Ok(()) } } From fc7cf970a59ab8c4ed4735012381d38175c29f7f Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 5 Feb 2024 19:51:02 +0100 Subject: [PATCH 024/124] fix nameserver's A record & add some docs --- src/authoritative_name_server.rs | 52 ++++++++++++++++++++------------ src/record.rs | 8 ++--- src/recursive_resolver.rs | 6 ++-- 3 files changed, 40 insertions(+), 26 deletions(-) diff --git a/src/authoritative_name_server.rs b/src/authoritative_name_server.rs index 1e2a9563..8a911da7 100644 --- a/src/authoritative_name_server.rs +++ b/src/authoritative_name_server.rs @@ -2,13 +2,13 @@ use std::net::Ipv4Addr; use std::process::Child; use crate::container::Container; -use crate::record::{self, Referral, SoaSettings, Zone}; +use crate::record::{self, Referral, SoaSettings, ZoneFile}; use crate::{Domain, Result, CHMOD_RW_EVERYONE}; pub struct AuthoritativeNameServer<'a> { child: Child, container: Container, - zone: Zone<'a>, + zone_file: ZoneFile<'a>, } impl<'a> AuthoritativeNameServer<'a> { @@ -24,6 +24,7 @@ impl<'a> AuthoritativeNameServer<'a> { }) } + /// This is short-hand for `Self::reserve().start(/* .. */)` pub fn start( domain: Domain<'a>, referrals: &[Referral<'a>], @@ -37,11 +38,11 @@ impl<'a> AuthoritativeNameServer<'a> { } pub fn nameserver(&self) -> &Domain<'a> { - &self.zone.soa.ns + &self.zone_file.soa.ns } - pub fn zone(&self) -> &Zone<'a> { - &self.zone + pub fn zone_file(&self) -> &ZoneFile<'a> { + &self.zone_file } } @@ -74,9 +75,22 @@ impl StoppedAuthoritativeNameServer { &self.nameserver } + /// Starts a primary name server that has authority over the given `zone` + /// + /// The domain of the name server will have the form `primary{count}.nameservers.com.` where + /// `{count}` is a unique, monotonically increasing integer + /// + /// The zone will contain these records + /// + /// - one SOA record, with the primary name server set to the name server domain + /// - one NS record, with the name server domain set as the only available name server for + /// `zone` + /// - one A record, that maps the name server domain to its IPv4 address + /// - one NS + A record pair, for each referral in the `referrals` list + /// - the A records in the `a_records` list pub fn start<'a>( self, - domain: Domain<'a>, + zone: Domain<'a>, referrals: &[Referral<'a>], a_records: &[record::A<'a>], ) -> Result> { @@ -90,42 +104,42 @@ impl StoppedAuthoritativeNameServer { container.status_ok(&["mkdir", "-p", "/run/nsd/"])?; container.status_ok(&["mkdir", "-p", "/etc/nsd/zones"])?; - let zone_path = "/etc/nsd/zones/main.zone"; - container.cp("/etc/nsd/nsd.conf", &nsd_conf(&domain), CHMOD_RW_EVERYONE)?; + let zone_file_path = "/etc/nsd/zones/main.zone"; + container.cp("/etc/nsd/nsd.conf", &nsd_conf(&zone), CHMOD_RW_EVERYONE)?; let soa = record::Soa { - domain: domain.clone(), + domain: zone.clone(), ns: nameserver.clone(), admin: admin_ns(ns_count), settings: SoaSettings::default(), }; - let mut zone = Zone::new(domain.clone(), soa); + let mut zone_file = ZoneFile::new(zone.clone(), soa); - zone.record(record::Ns { - domain: domain.clone(), - ns: nameserver, + zone_file.record(record::Ns { + domain: zone.clone(), + ns: nameserver.clone(), }); - zone.record(record::A { - domain, + zone_file.record(record::A { + domain: nameserver, ipv4_addr: container.ipv4_addr(), }); for referral in referrals { - zone.referral(referral) + zone_file.referral(referral) } for a in a_records { - zone.record(a.clone()) + zone_file.record(a.clone()) } - container.cp(zone_path, &zone.to_string(), CHMOD_RW_EVERYONE)?; + container.cp(zone_file_path, &zone_file.to_string(), CHMOD_RW_EVERYONE)?; let child = container.spawn(&["nsd", "-d"])?; Ok(AuthoritativeNameServer { child, container, - zone, + zone_file, }) } } diff --git a/src/record.rs b/src/record.rs index c659e93a..0c3b7eb0 100644 --- a/src/record.rs +++ b/src/record.rs @@ -7,14 +7,14 @@ use std::net::Ipv4Addr; use crate::Domain; -pub struct Zone<'a> { +pub struct ZoneFile<'a> { pub origin: Domain<'a>, pub ttl: u32, pub soa: Soa<'a>, pub records: Vec>, } -impl<'a> Zone<'a> { +impl<'a> ZoneFile<'a> { /// Convenience constructor that uses "reasonable" defaults pub fn new(origin: Domain<'a>, soa: Soa<'a>) -> Self { Self { @@ -49,7 +49,7 @@ impl<'a> Zone<'a> { } } -impl fmt::Display for Zone<'_> { +impl fmt::Display for ZoneFile<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let Self { origin, @@ -260,7 +260,7 @@ $TTL 1800 com. IN NS e.gtld-servers.net. e.gtld-servers.net. IN A 192.12.94.30 "; - let mut zone = Zone::new(Domain::ROOT, example_soa()?); + let mut zone = ZoneFile::new(Domain::ROOT, example_soa()?); zone.record(example_ns()?); zone.record(example_a()?); diff --git a/src/recursive_resolver.rs b/src/recursive_resolver.rs index c246fadc..c54afda8 100644 --- a/src/recursive_resolver.rs +++ b/src/recursive_resolver.rs @@ -74,7 +74,7 @@ mod tests { ], )?; - eprintln!("nameservers.com.zone:\n{}", nameservers_ns.zone()); + eprintln!("nameservers.com.zone:\n{}", nameservers_ns.zone_file()); let com_domain = Domain("com.")?; let com_ns = com_ns.start( @@ -87,7 +87,7 @@ mod tests { &[], )?; - eprintln!("com.zone:\n{}", com_ns.zone()); + eprintln!("com.zone:\n{}", com_ns.zone_file()); let root_ns = root_ns.start( Domain::ROOT, @@ -99,7 +99,7 @@ mod tests { &[], )?; - eprintln!("root.zone:\n{}", root_ns.zone()); + eprintln!("root.zone:\n{}", root_ns.zone_file()); let roots = &[Root::new(root_ns.nameserver().clone(), root_ns.ipv4_addr())]; let resolver = RecursiveResolver::start(roots)?; From 7ad5bacbdccfaef34911b067f14ecbfc07ff1bc3 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 6 Feb 2024 16:47:18 +0100 Subject: [PATCH 025/124] parse dig's output --- src/authoritative_name_server.rs | 51 ++-- src/client.rs | 408 +++++++++++++++++++++++++++++++ src/container.rs | 12 + src/domain.rs | 19 +- src/lib.rs | 2 + src/recursive_resolver.rs | 30 +-- 6 files changed, 466 insertions(+), 56 deletions(-) create mode 100644 src/client.rs diff --git a/src/authoritative_name_server.rs b/src/authoritative_name_server.rs index 8a911da7..7aafdb7a 100644 --- a/src/authoritative_name_server.rs +++ b/src/authoritative_name_server.rs @@ -85,7 +85,6 @@ impl StoppedAuthoritativeNameServer { /// - one SOA record, with the primary name server set to the name server domain /// - one NS record, with the name server domain set as the only available name server for /// `zone` - /// - one A record, that maps the name server domain to its IPv4 address /// - one NS + A record pair, for each referral in the `referrals` list /// - the A records in the `a_records` list pub fn start<'a>( @@ -119,10 +118,6 @@ impl StoppedAuthoritativeNameServer { domain: zone.clone(), ns: nameserver.clone(), }); - zone_file.record(record::A { - domain: nameserver, - ipv4_addr: container.ipv4_addr(), - }); for referral in referrals { zone_file.referral(referral) @@ -153,45 +148,35 @@ fn nsd_conf(domain: &Domain) -> String { #[cfg(test)] mod tests { + use crate::{ + client::{RecordType, Recurse}, + Client, + }; + use super::*; #[test] - fn tld_ns() -> Result<()> { - let tld_ns = AuthoritativeNameServer::start(Domain("com.")?, &[], &[])?; + fn simplest() -> Result<()> { + let com_domain = Domain("com.")?; + let tld_ns = AuthoritativeNameServer::start(com_domain.clone(), &[], &[])?; let ip_addr = tld_ns.ipv4_addr(); - let client = Container::run()?; - let output = client.output(&["dig", &format!("@{ip_addr}"), "SOA", "com."])?; + let client = Client::new()?; + let output = client.dig(Recurse::No, ip_addr, RecordType::SOA, &com_domain)?; - assert!(output.status.success()); - eprintln!("{}", output.stdout); - assert!(output.stdout.contains("status: NOERROR")); + assert!(output.status.is_noerror()); Ok(()) } #[test] - fn root_ns() -> Result<()> { - let root_ns = AuthoritativeNameServer::start(Domain::ROOT, &[], &[])?; - let ip_addr = root_ns.ipv4_addr(); - - let client = Container::run()?; - let output = client.output(&["dig", &format!("@{ip_addr}"), "SOA", "."])?; - - assert!(output.status.success()); - eprintln!("{}", output.stdout); - assert!(output.stdout.contains("status: NOERROR")); - - Ok(()) - } - - #[test] - fn root_ns_with_referral() -> Result<()> { + fn with_referral() -> Result<()> { let expected_ip_addr = Ipv4Addr::new(172, 17, 200, 1); + let com_domain = Domain("com.")?; let root_ns = AuthoritativeNameServer::start( Domain::ROOT, &[Referral { - domain: Domain("com.")?, + domain: com_domain.clone(), ipv4_addr: expected_ip_addr, ns: Domain("primary.tld-server.com.")?, }], @@ -199,12 +184,10 @@ mod tests { )?; let ip_addr = root_ns.ipv4_addr(); - let client = Container::run()?; - let output = client.output(&["dig", &format!("@{ip_addr}"), "NS", "com."])?; + let client = Client::new()?; + let output = client.dig(Recurse::No, ip_addr, RecordType::NS, &com_domain)?; - assert!(output.status.success()); - eprintln!("{}", output.stdout); - assert!(output.stdout.contains("status: NOERROR")); + assert!(output.status.is_noerror()); Ok(()) } diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 00000000..2a7190e9 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,408 @@ +use core::array; +use core::result::Result as CoreResult; +use core::str::FromStr; +use std::net::Ipv4Addr; + +use crate::container::Container; +use crate::{Domain, Error, Result}; + +pub struct Client { + inner: Container, +} + +impl Client { + pub fn new() -> Result { + Ok(Self { + inner: Container::run()?, + }) + } + + pub fn dig( + &self, + recurse: Recurse, + server: Ipv4Addr, + record_type: RecordType, + domain: &Domain<'_>, + ) -> Result { + let output = self.inner.stdout(&[ + "dig", + recurse.as_str(), + &format!("@{server}"), + record_type.as_str(), + domain.as_str(), + ])?; + + output.parse() + } +} + +#[allow(clippy::upper_case_acronyms)] +pub enum RecordType { + A, + NS, + SOA, +} + +impl RecordType { + fn as_str(&self) -> &'static str { + match self { + RecordType::A => "A", + RecordType::SOA => "SOA", + RecordType::NS => "NS", + } + } +} + +#[derive(Clone, Copy)] +pub enum Recurse { + Yes, + No, +} + +impl Recurse { + fn as_str(&self) -> &'static str { + match self { + Recurse::Yes => "+recurse", + Recurse::No => "+norecurse", + } + } +} + +pub struct DigOutput { + pub flags: DigFlags, + pub status: DigStatus, + pub answer: Vec, + // TODO(if needed) other sections +} + +impl FromStr for DigOutput { + type Err = Error; + + fn from_str(input: &str) -> Result { + const FLAGS_PREFIX: &str = ";; flags: "; + const STATUS_PREFIX: &str = ";; ->>HEADER<<- opcode: QUERY, status: "; + const ANSWER_HEADER: &str = ";; ANSWER SECTION:"; + + fn not_found(prefix: &str) -> String { + format!("`{prefix}` line was not found") + } + + fn more_than_once(prefix: &str) -> String { + format!("`{prefix}` line was found more than once") + } + + fn missing(prefix: &str, delimiter: &str) -> String { + format!("`{prefix}` line is missing a {delimiter}") + } + + let mut flags = None; + let mut status = None; + let mut answer = None; + + let mut lines = input.lines(); + while let Some(line) = lines.next() { + if let Some(unprefixed) = line.strip_prefix(FLAGS_PREFIX) { + let (flags_text, _rest) = unprefixed + .split_once(';') + .ok_or_else(|| missing(FLAGS_PREFIX, "semicolon (;)"))?; + + if flags.is_some() { + return Err(more_than_once(FLAGS_PREFIX).into()); + } + + flags = Some(flags_text.parse()?); + } else if let Some(unprefixed) = line.strip_prefix(STATUS_PREFIX) { + let (status_text, _rest) = unprefixed + .split_once(',') + .ok_or_else(|| missing(STATUS_PREFIX, "comma (,)"))?; + + if status.is_some() { + return Err(more_than_once(STATUS_PREFIX).into()); + } + + status = Some(status_text.parse()?); + } else if line.starts_with(ANSWER_HEADER) { + if answer.is_some() { + return Err(more_than_once(ANSWER_HEADER).into()); + } + + let mut records = vec![]; + for line in lines.by_ref() { + if line.is_empty() { + break; + } + + records.push(line.parse()?); + } + + answer = Some(records); + } + } + + Ok(Self { + flags: flags.ok_or_else(|| not_found(FLAGS_PREFIX))?, + status: status.ok_or_else(|| not_found(STATUS_PREFIX))?, + answer: answer.unwrap_or_default(), + }) + } +} + +#[derive(Debug, Default, PartialEq)] +pub struct DigFlags { + pub qr: bool, + pub recursion_desired: bool, + pub recursion_available: bool, + pub authoritative_answer: bool, +} + +impl FromStr for DigFlags { + type Err = Error; + + fn from_str(input: &str) -> std::prelude::v1::Result { + let mut qr = false; + let mut recursion_desired = false; + let mut recursion_available = false; + let mut authoritative_answer = false; + + for flag in input.split_whitespace() { + match flag { + "qr" => qr = true, + "rd" => recursion_desired = true, + "ra" => recursion_available = true, + "aa" => authoritative_answer = true, + _ => return Err(format!("unknown flag: {flag}").into()), + } + } + + Ok(Self { + qr, + recursion_desired, + recursion_available, + authoritative_answer, + }) + } +} + +#[allow(clippy::upper_case_acronyms)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum DigStatus { + NOERROR, + NXDOMAIN, + REFUSED, +} + +impl DigStatus { + #[must_use] + pub fn is_noerror(&self) -> bool { + matches!(self, Self::NOERROR) + } +} + +impl FromStr for DigStatus { + type Err = Error; + + fn from_str(input: &str) -> Result { + let status = match input { + "NXDOMAIN" => Self::NXDOMAIN, + "NOERROR" => Self::NOERROR, + "REFUSED" => Self::REFUSED, + _ => return Err(format!("unknown status: {input}").into()), + }; + + Ok(status) + } +} + +#[derive(Debug)] +#[allow(clippy::upper_case_acronyms)] +pub enum Record { + A(A), + SOA(SOA), +} + +impl Record { + pub fn try_into_a(self) -> CoreResult { + if let Self::A(v) = self { + Ok(v) + } else { + Err(self) + } + } +} + +impl FromStr for Record { + type Err = Error; + + fn from_str(input: &str) -> Result { + let record_type = input + .split_whitespace() + .nth(3) + .ok_or("record is missing the type column")?; + + let record = match record_type { + "A" => Record::A(input.parse()?), + "NS" => todo!(), + "SOA" => Record::SOA(input.parse()?), + _ => return Err(format!("unknown record type: {record_type}").into()), + }; + + Ok(record) + } +} + +#[derive(Debug)] +pub struct A { + pub domain: Domain<'static>, + pub ttl: u32, + pub ipv4_addr: Ipv4Addr, +} + +impl FromStr for A { + type Err = Error; + + fn from_str(input: &str) -> Result { + let mut columns = input.split_whitespace(); + + let [Some(domain), Some(ttl), Some(class), Some(record_type), Some(ipv4_addr), None] = + array::from_fn(|_| columns.next()) + else { + return Err("expected 5 columns".into()); + }; + + if record_type != "A" { + return Err(format!("tried to parse `{record_type}` record as an A record").into()); + } + + if class != "IN" { + return Err(format!("unknown class: {class}").into()); + } + + Ok(Self { + domain: domain.parse()?, + ttl: ttl.parse()?, + ipv4_addr: ipv4_addr.parse()?, + }) + } +} + +#[allow(clippy::upper_case_acronyms)] +#[derive(Debug)] +pub struct SOA { + pub domain: Domain<'static>, + pub ttl: u32, + pub nameserver: Domain<'static>, + pub admin: Domain<'static>, + pub serial: u32, + pub refresh: u32, + pub retry: u32, + pub expire: u32, + pub minimum: u32, +} + +impl FromStr for SOA { + type Err = Error; + + fn from_str(input: &str) -> Result { + let mut columns = input.split_whitespace(); + + let [Some(domain), Some(ttl), Some(class), Some(record_type), Some(nameserver), Some(admin), Some(serial), Some(refresh), Some(retry), Some(expire), Some(minimum), None] = + array::from_fn(|_| columns.next()) + else { + return Err("expected 11 columns".into()); + }; + + if record_type != "SOA" { + return Err(format!("tried to parse `{record_type}` record as a SOA record").into()); + } + + if class != "IN" { + return Err(format!("unknown class: {class}").into()); + } + + Ok(Self { + domain: domain.parse()?, + ttl: ttl.parse()?, + nameserver: nameserver.parse()?, + admin: admin.parse()?, + serial: serial.parse()?, + refresh: refresh.parse()?, + retry: retry.parse()?, + expire: expire.parse()?, + minimum: minimum.parse()?, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn nxdomain() -> Result<()> { + // $ dig nonexistent.domain. + let input = " +; <<>> DiG 9.18.18-0ubuntu0.22.04.1-Ubuntu <<>> nonexistent.domain. +;; global options: +cmd +;; Got answer: +;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 45583 +;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1 + +;; OPT PSEUDOSECTION: +; EDNS: version: 0, flags:; udp: 1232 +;; QUESTION SECTION: +;nonexistent.domain. IN A + +;; Query time: 3 msec +;; SERVER: 192.168.1.1#53(192.168.1.1) (UDP) +;; WHEN: Tue Feb 06 15:00:12 UTC 2024 +;; MSG SIZE rcvd: 47 +"; + + let output: DigOutput = input.parse()?; + + assert_eq!(DigStatus::NXDOMAIN, output.status); + assert_eq!( + DigFlags { + qr: true, + recursion_desired: true, + recursion_available: true, + ..DigFlags::default() + }, + output.flags + ); + assert!(output.answer.is_empty()); + + Ok(()) + } + + #[test] + fn can_parse_a_record() -> Result<()> { + let input = "a.root-servers.net. 3600000 IN A 198.41.0.4"; + let a: A = input.parse()?; + + assert_eq!("a.root-servers.net.", a.domain.as_str()); + assert_eq!(3600000, a.ttl); + assert_eq!(Ipv4Addr::new(198, 41, 0, 4), a.ipv4_addr); + + Ok(()) + } + + #[test] + fn can_parse_soa_record() -> Result<()> { + let input = ". 15633 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2024020501 1800 900 604800 86400"; + + let soa: SOA = input.parse()?; + + assert_eq!(".", soa.domain.as_str()); + assert_eq!(15633, soa.ttl); + assert_eq!("a.root-servers.net.", soa.nameserver.as_str()); + assert_eq!("nstld.verisign-grs.com.", soa.admin.as_str()); + assert_eq!(2024020501, soa.serial); + assert_eq!(1800, soa.refresh); + assert_eq!(900, soa.retry); + assert_eq!(604800, soa.expire); + assert_eq!(86400, soa.minimum); + + Ok(()) + } +} diff --git a/src/container.rs b/src/container.rs index e7f12759..095e591b 100644 --- a/src/container.rs +++ b/src/container.rs @@ -95,6 +95,18 @@ impl Container { command.output()?.try_into() } + /// 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 { + let output = self.output(command_and_args)?; + + if output.status.success() { + Ok(output.stdout) + } else { + Err(format!("[{}] `{command_and_args:?}` failed", self.name).into()) + } + } + /// Similar to `std::process::Command::status` but runs `command_and_args` in the container pub fn status(&self, command_and_args: &[&str]) -> Result { let mut command = Command::new("docker"); diff --git a/src/domain.rs b/src/domain.rs index 922347cd..159d4623 100644 --- a/src/domain.rs +++ b/src/domain.rs @@ -1,9 +1,10 @@ use core::fmt; +use core::str::FromStr; use std::borrow::Cow; -use crate::Result; +use crate::{Error, Result}; -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub struct Domain<'a> { inner: Cow<'a, str>, } @@ -48,6 +49,20 @@ impl<'a> Domain<'a> { } } +impl FromStr for Domain<'static> { + type Err = Error; + + fn from_str(input: &str) -> Result { + Ok(Domain(input)?.into_owned()) + } +} + +impl fmt::Debug for Domain<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + impl fmt::Display for Domain<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.inner) diff --git a/src/lib.rs b/src/lib.rs index 82c2a386..95b95cde 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ use std::sync::atomic::{self, AtomicUsize}; pub use crate::authoritative_name_server::AuthoritativeNameServer; +pub use crate::client::Client; pub use crate::domain::Domain; pub use crate::recursive_resolver::RecursiveResolver; @@ -10,6 +11,7 @@ pub type Result = core::result::Result; const CHMOD_RW_EVERYONE: &str = "666"; mod authoritative_name_server; +mod client; pub mod container; mod domain; pub mod record; diff --git a/src/recursive_resolver.rs b/src/recursive_resolver.rs index c54afda8..22fcea0e 100644 --- a/src/recursive_resolver.rs +++ b/src/recursive_resolver.rs @@ -41,8 +41,9 @@ impl Drop for RecursiveResolver { #[cfg(test)] mod tests { use crate::{ + client::{RecordType, Recurse}, record::{self, Referral}, - AuthoritativeNameServer, Domain, + AuthoritativeNameServer, Client, Domain, }; use super::*; @@ -51,6 +52,7 @@ mod tests { fn can_resolve() -> Result<()> { let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4); let needle = Domain("example.nameservers.com.")?; + let root_ns = AuthoritativeNameServer::reserve()?; let com_ns = AuthoritativeNameServer::reserve()?; @@ -105,28 +107,16 @@ mod tests { let resolver = RecursiveResolver::start(roots)?; let resolver_ip_addr = resolver.ipv4_addr(); - let container = Container::run()?; - let output = container.output(&[ - "dig", - &format!("@{}", resolver_ip_addr), - &needle.to_string(), - ])?; + let client = Client::new()?; + let output = client.dig(Recurse::Yes, resolver_ip_addr, RecordType::A, &needle)?; - eprintln!("{}", output.stdout); + assert!(output.status.is_noerror()); - assert!(output.status.success()); - assert!(output.stdout.contains("status: NOERROR")); + let [answer] = output.answer.try_into().unwrap(); + let a = answer.try_into_a().unwrap(); - let mut found = false; - let needle = needle.to_string(); - for line in output.stdout.lines() { - if line.starts_with(&needle) { - found = true; - assert!(line.ends_with(&expected_ipv4_addr.to_string())); - } - } - - assert!(found); + assert_eq!(needle, a.domain); + assert_eq!(expected_ipv4_addr, a.ipv4_addr); Ok(()) } From 3e5ef300ce2d03265a8e6c82f7c597ad29285d97 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 6 Feb 2024 18:11:31 +0100 Subject: [PATCH 026/124] refactor/ns: build pattern + type state --- src/authoritative_name_server.rs | 215 +++++++++++++++---------------- src/domain.rs | 4 + src/recursive_resolver.rs | 61 +++------ 3 files changed, 126 insertions(+), 154 deletions(-) diff --git a/src/authoritative_name_server.rs b/src/authoritative_name_server.rs index 7aafdb7a..c3b85162 100644 --- a/src/authoritative_name_server.rs +++ b/src/authoritative_name_server.rs @@ -5,48 +5,115 @@ use crate::container::Container; use crate::record::{self, Referral, SoaSettings, ZoneFile}; use crate::{Domain, Result, CHMOD_RW_EVERYONE}; -pub struct AuthoritativeNameServer<'a> { - child: Child, +pub struct AuthoritativeNameServer<'a, State> { container: Container, zone_file: ZoneFile<'a>, + _state: State, } -impl<'a> AuthoritativeNameServer<'a> { - /// Spins up a container in a parked state where the name server is not running yet - pub fn reserve() -> Result { +impl<'a> AuthoritativeNameServer<'a, Stopped> { + /// Spins up a primary name server that has authority over the given `zone` + /// + /// The initial state of the server is the "Stopped" state where it won't answer any query. + /// + /// The FQDN of the name server will have the form `primary{count}.nameservers.com.` where + /// `{count}` is a (process-wide) unique, monotonically increasing integer + /// + /// The zone file will contain these records + /// + /// - one SOA record, with the primary name server set to the name server domain + /// - one NS record, with the name server domain set as the only available name server for + pub fn new(zone: Domain<'a>) -> Result { let ns_count = crate::nameserver_count(); let nameserver = primary_ns(ns_count); - Ok(StoppedAuthoritativeNameServer { + let soa = record::Soa { + domain: zone.clone(), + ns: nameserver.clone(), + admin: admin_ns(ns_count), + settings: SoaSettings::default(), + }; + let mut zone_file = ZoneFile::new(zone.clone(), soa); + + zone_file.record(record::Ns { + domain: zone, + ns: nameserver.clone(), + }); + + Ok(Self { container: Container::run()?, - nameserver, - ns_count, + zone_file, + _state: Stopped, }) } - /// This is short-hand for `Self::reserve().start(/* .. */)` - pub fn start( - domain: Domain<'a>, - referrals: &[Referral<'a>], - a_records: &[record::A<'a>], - ) -> Result { - Self::reserve()?.start(domain, referrals, a_records) + /// Adds a NS + A record pair to the zone file + pub fn referral(&mut self, referral: &Referral<'a>) -> &mut Self { + self.zone_file.referral(referral); + self } + /// Adds an A record pair to the zone file + pub fn a(&mut self, domain: Domain<'a>, ipv4_addr: Ipv4Addr) -> &mut Self { + self.zone_file.record(record::A { domain, ipv4_addr }); + self + } + + /// Moves the server to the "Start" state where it can answer client queries + pub fn start(self) -> Result> { + let Self { + container, + zone_file, + _state: _, + } = self; + + // for PID file + container.status_ok(&["mkdir", "-p", "/run/nsd/"])?; + + container.cp( + "/etc/nsd/nsd.conf", + &nsd_conf(&zone_file.origin), + CHMOD_RW_EVERYONE, + )?; + + container.status_ok(&["mkdir", "-p", "/etc/nsd/zones"])?; + container.cp( + "/etc/nsd/zones/main.zone", + &zone_file.to_string(), + CHMOD_RW_EVERYONE, + )?; + + let child = container.spawn(&["nsd", "-d"])?; + + Ok(AuthoritativeNameServer { + container, + zone_file, + _state: Running { child }, + }) + } +} + +impl<'a, S> AuthoritativeNameServer<'a, S> { pub fn ipv4_addr(&self) -> Ipv4Addr { self.container.ipv4_addr() } - pub fn nameserver(&self) -> &Domain<'a> { - &self.zone_file.soa.ns - } - pub fn zone_file(&self) -> &ZoneFile<'a> { &self.zone_file } + + pub fn nameserver(&self) -> &Domain<'a> { + &self.zone_file.soa.ns + } } -impl Drop for AuthoritativeNameServer<'_> { +pub struct Stopped; + +pub struct Running { + child: Child, +} + +impl Drop for Running { fn drop(&mut self) { let _ = self.child.kill(); } @@ -60,85 +127,6 @@ fn admin_ns(ns_count: usize) -> Domain<'static> { Domain(format!("admin{ns_count}.nameservers.com.")).unwrap() } -pub struct StoppedAuthoritativeNameServer { - container: Container, - nameserver: Domain<'static>, - ns_count: usize, -} - -impl StoppedAuthoritativeNameServer { - pub fn ipv4_addr(&self) -> Ipv4Addr { - self.container.ipv4_addr() - } - - pub fn nameserver(&self) -> &Domain<'static> { - &self.nameserver - } - - /// Starts a primary name server that has authority over the given `zone` - /// - /// The domain of the name server will have the form `primary{count}.nameservers.com.` where - /// `{count}` is a unique, monotonically increasing integer - /// - /// The zone will contain these records - /// - /// - one SOA record, with the primary name server set to the name server domain - /// - one NS record, with the name server domain set as the only available name server for - /// `zone` - /// - one NS + A record pair, for each referral in the `referrals` list - /// - the A records in the `a_records` list - pub fn start<'a>( - self, - zone: Domain<'a>, - referrals: &[Referral<'a>], - a_records: &[record::A<'a>], - ) -> Result> { - let Self { - container, - nameserver, - ns_count, - } = self; - - // for PID file - container.status_ok(&["mkdir", "-p", "/run/nsd/"])?; - - container.status_ok(&["mkdir", "-p", "/etc/nsd/zones"])?; - let zone_file_path = "/etc/nsd/zones/main.zone"; - container.cp("/etc/nsd/nsd.conf", &nsd_conf(&zone), CHMOD_RW_EVERYONE)?; - - let soa = record::Soa { - domain: zone.clone(), - ns: nameserver.clone(), - admin: admin_ns(ns_count), - settings: SoaSettings::default(), - }; - let mut zone_file = ZoneFile::new(zone.clone(), soa); - - zone_file.record(record::Ns { - domain: zone.clone(), - ns: nameserver.clone(), - }); - - for referral in referrals { - zone_file.referral(referral) - } - - for a in a_records { - zone_file.record(a.clone()) - } - - container.cp(zone_file_path, &zone_file.to_string(), CHMOD_RW_EVERYONE)?; - - let child = container.spawn(&["nsd", "-d"])?; - - Ok(AuthoritativeNameServer { - child, - container, - zone_file, - }) - } -} - fn nsd_conf(domain: &Domain) -> String { minijinja::render!( include_str!("templates/nsd.conf.jinja"), @@ -157,12 +145,11 @@ mod tests { #[test] fn simplest() -> Result<()> { - let com_domain = Domain("com.")?; - let tld_ns = AuthoritativeNameServer::start(com_domain.clone(), &[], &[])?; + let tld_ns = AuthoritativeNameServer::new(Domain::COM)?.start()?; let ip_addr = tld_ns.ipv4_addr(); let client = Client::new()?; - let output = client.dig(Recurse::No, ip_addr, RecordType::SOA, &com_domain)?; + let output = client.dig(Recurse::No, ip_addr, RecordType::SOA, &Domain::COM)?; assert!(output.status.is_noerror()); @@ -172,20 +159,20 @@ mod tests { #[test] fn with_referral() -> Result<()> { let expected_ip_addr = Ipv4Addr::new(172, 17, 200, 1); - let com_domain = Domain("com.")?; - let root_ns = AuthoritativeNameServer::start( - Domain::ROOT, - &[Referral { - domain: com_domain.clone(), - ipv4_addr: expected_ip_addr, - ns: Domain("primary.tld-server.com.")?, - }], - &[], - )?; - let ip_addr = root_ns.ipv4_addr(); + let mut root_ns = AuthoritativeNameServer::new(Domain::ROOT)?; + root_ns.referral(&Referral { + domain: Domain::COM, + ipv4_addr: expected_ip_addr, + ns: Domain("primary.tld-server.com.")?, + }); + let root_ns = root_ns.start()?; + + eprintln!("root.zone:\n{}", root_ns.zone_file()); + + let ipv4_addr = root_ns.ipv4_addr(); let client = Client::new()?; - let output = client.dig(Recurse::No, ip_addr, RecordType::NS, &com_domain)?; + let output = client.dig(Recurse::No, ipv4_addr, RecordType::NS, &Domain::COM)?; assert!(output.status.is_noerror()); diff --git a/src/domain.rs b/src/domain.rs index 159d4623..bb74ce58 100644 --- a/src/domain.rs +++ b/src/domain.rs @@ -29,6 +29,10 @@ impl<'a> Domain<'a> { inner: Cow::Borrowed("."), }; + pub const COM: Domain<'static> = Domain { + inner: Cow::Borrowed("com."), + }; + pub fn is_root(&self) -> bool { self.inner == "." } diff --git a/src/recursive_resolver.rs b/src/recursive_resolver.rs index 22fcea0e..7e725714 100644 --- a/src/recursive_resolver.rs +++ b/src/recursive_resolver.rs @@ -42,7 +42,7 @@ impl Drop for RecursiveResolver { mod tests { use crate::{ client::{RecordType, Recurse}, - record::{self, Referral}, + record::Referral, AuthoritativeNameServer, Client, Domain, }; @@ -53,53 +53,34 @@ mod tests { let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4); let needle = Domain("example.nameservers.com.")?; - let root_ns = AuthoritativeNameServer::reserve()?; - let com_ns = AuthoritativeNameServer::reserve()?; + let mut root_ns = AuthoritativeNameServer::new(Domain::ROOT)?; + let mut com_ns = AuthoritativeNameServer::new(Domain::COM)?; let nameservers_domain = Domain("nameservers.com.")?; - let nameservers_ns = AuthoritativeNameServer::start( - nameservers_domain.clone(), - &[], - &[ - record::A { - domain: root_ns.nameserver().clone(), - ipv4_addr: root_ns.ipv4_addr(), - }, - record::A { - domain: com_ns.nameserver().clone(), - ipv4_addr: com_ns.ipv4_addr(), - }, - record::A { - domain: needle.clone(), - ipv4_addr: expected_ipv4_addr, - }, - ], - )?; + let mut nameservers_ns = AuthoritativeNameServer::new(nameservers_domain.clone())?; + nameservers_ns + .a(root_ns.nameserver().clone(), root_ns.ipv4_addr()) + .a(com_ns.nameserver().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()); - let com_domain = Domain("com.")?; - let com_ns = com_ns.start( - com_domain.clone(), - &[Referral { - domain: nameservers_domain, - ipv4_addr: nameservers_ns.ipv4_addr(), - ns: nameservers_ns.nameserver().clone(), - }], - &[], - )?; + com_ns.referral(&Referral { + domain: nameservers_domain, + ipv4_addr: nameservers_ns.ipv4_addr(), + ns: nameservers_ns.nameserver().clone(), + }); + let com_ns = com_ns.start()?; eprintln!("com.zone:\n{}", com_ns.zone_file()); - let root_ns = root_ns.start( - Domain::ROOT, - &[Referral { - domain: com_domain, - ipv4_addr: com_ns.ipv4_addr(), - ns: com_ns.nameserver().clone(), - }], - &[], - )?; + root_ns.referral(&Referral { + domain: Domain::COM, + ipv4_addr: com_ns.ipv4_addr(), + ns: com_ns.nameserver().clone(), + }); + let root_ns = root_ns.start()?; eprintln!("root.zone:\n{}", root_ns.zone_file()); From e29b901bc18735ee4a321063a54d7966be1cb15b Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 6 Feb 2024 18:12:22 +0100 Subject: [PATCH 027/124] shorten AuthoritativeNameServer -> NameServer --- src/lib.rs | 4 ++-- ...authoritative_name_server.rs => name_server.rs} | 14 +++++++------- src/recursive_resolver.rs | 8 ++++---- 3 files changed, 13 insertions(+), 13 deletions(-) rename src/{authoritative_name_server.rs => name_server.rs} (91%) diff --git a/src/lib.rs b/src/lib.rs index 95b95cde..22481203 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,8 @@ use std::sync::atomic::{self, AtomicUsize}; -pub use crate::authoritative_name_server::AuthoritativeNameServer; pub use crate::client::Client; pub use crate::domain::Domain; +pub use crate::name_server::NameServer; pub use crate::recursive_resolver::RecursiveResolver; pub type Error = Box; @@ -10,10 +10,10 @@ pub type Result = core::result::Result; const CHMOD_RW_EVERYONE: &str = "666"; -mod authoritative_name_server; mod client; pub mod container; mod domain; +mod name_server; pub mod record; mod recursive_resolver; diff --git a/src/authoritative_name_server.rs b/src/name_server.rs similarity index 91% rename from src/authoritative_name_server.rs rename to src/name_server.rs index c3b85162..327b5455 100644 --- a/src/authoritative_name_server.rs +++ b/src/name_server.rs @@ -5,13 +5,13 @@ use crate::container::Container; use crate::record::{self, Referral, SoaSettings, ZoneFile}; use crate::{Domain, Result, CHMOD_RW_EVERYONE}; -pub struct AuthoritativeNameServer<'a, State> { +pub struct NameServer<'a, State> { container: Container, zone_file: ZoneFile<'a>, _state: State, } -impl<'a> AuthoritativeNameServer<'a, Stopped> { +impl<'a> NameServer<'a, Stopped> { /// Spins up a primary name server that has authority over the given `zone` /// /// The initial state of the server is the "Stopped" state where it won't answer any query. @@ -60,7 +60,7 @@ impl<'a> AuthoritativeNameServer<'a, Stopped> { } /// Moves the server to the "Start" state where it can answer client queries - pub fn start(self) -> Result> { + pub fn start(self) -> Result> { let Self { container, zone_file, @@ -85,7 +85,7 @@ impl<'a> AuthoritativeNameServer<'a, Stopped> { let child = container.spawn(&["nsd", "-d"])?; - Ok(AuthoritativeNameServer { + Ok(NameServer { container, zone_file, _state: Running { child }, @@ -93,7 +93,7 @@ impl<'a> AuthoritativeNameServer<'a, Stopped> { } } -impl<'a, S> AuthoritativeNameServer<'a, S> { +impl<'a, S> NameServer<'a, S> { pub fn ipv4_addr(&self) -> Ipv4Addr { self.container.ipv4_addr() } @@ -145,7 +145,7 @@ mod tests { #[test] fn simplest() -> Result<()> { - let tld_ns = AuthoritativeNameServer::new(Domain::COM)?.start()?; + let tld_ns = NameServer::new(Domain::COM)?.start()?; let ip_addr = tld_ns.ipv4_addr(); let client = Client::new()?; @@ -159,7 +159,7 @@ mod tests { #[test] fn with_referral() -> Result<()> { let expected_ip_addr = Ipv4Addr::new(172, 17, 200, 1); - let mut root_ns = AuthoritativeNameServer::new(Domain::ROOT)?; + let mut root_ns = NameServer::new(Domain::ROOT)?; root_ns.referral(&Referral { domain: Domain::COM, ipv4_addr: expected_ip_addr, diff --git a/src/recursive_resolver.rs b/src/recursive_resolver.rs index 7e725714..d7386afb 100644 --- a/src/recursive_resolver.rs +++ b/src/recursive_resolver.rs @@ -43,7 +43,7 @@ mod tests { use crate::{ client::{RecordType, Recurse}, record::Referral, - AuthoritativeNameServer, Client, Domain, + Client, Domain, NameServer, }; use super::*; @@ -53,11 +53,11 @@ mod tests { let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4); let needle = Domain("example.nameservers.com.")?; - let mut root_ns = AuthoritativeNameServer::new(Domain::ROOT)?; - let mut com_ns = AuthoritativeNameServer::new(Domain::COM)?; + let mut root_ns = NameServer::new(Domain::ROOT)?; + let mut com_ns = NameServer::new(Domain::COM)?; let nameservers_domain = Domain("nameservers.com.")?; - let mut nameservers_ns = AuthoritativeNameServer::new(nameservers_domain.clone())?; + let mut nameservers_ns = NameServer::new(nameservers_domain.clone())?; nameservers_ns .a(root_ns.nameserver().clone(), root_ns.ipv4_addr()) .a(com_ns.nameserver().clone(), com_ns.ipv4_addr()) From 7f7d9f7ccfa2a17563e39129ea163829b3fad1c5 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 6 Feb 2024 18:15:05 +0100 Subject: [PATCH 028/124] rename Domain -> FQDN --- src/client.rs | 12 +++++------ src/{domain.rs => fqdn.rs} | 24 +++++++++++----------- src/lib.rs | 4 ++-- src/name_server.rs | 30 +++++++++++++-------------- src/record.rs | 42 +++++++++++++++++++------------------- src/recursive_resolver.rs | 12 +++++------ 6 files changed, 62 insertions(+), 62 deletions(-) rename src/{domain.rs => fqdn.rs} (72%) diff --git a/src/client.rs b/src/client.rs index 2a7190e9..38a01ebf 100644 --- a/src/client.rs +++ b/src/client.rs @@ -4,7 +4,7 @@ use core::str::FromStr; use std::net::Ipv4Addr; use crate::container::Container; -use crate::{Domain, Error, Result}; +use crate::{Error, Result, FQDN}; pub struct Client { inner: Container, @@ -22,7 +22,7 @@ impl Client { recurse: Recurse, server: Ipv4Addr, record_type: RecordType, - domain: &Domain<'_>, + domain: &FQDN<'_>, ) -> Result { let output = self.inner.stdout(&[ "dig", @@ -252,7 +252,7 @@ impl FromStr for Record { #[derive(Debug)] pub struct A { - pub domain: Domain<'static>, + pub domain: FQDN<'static>, pub ttl: u32, pub ipv4_addr: Ipv4Addr, } @@ -288,10 +288,10 @@ impl FromStr for A { #[allow(clippy::upper_case_acronyms)] #[derive(Debug)] pub struct SOA { - pub domain: Domain<'static>, + pub domain: FQDN<'static>, pub ttl: u32, - pub nameserver: Domain<'static>, - pub admin: Domain<'static>, + pub nameserver: FQDN<'static>, + pub admin: FQDN<'static>, pub serial: u32, pub refresh: u32, pub retry: u32, diff --git a/src/domain.rs b/src/fqdn.rs similarity index 72% rename from src/domain.rs rename to src/fqdn.rs index bb74ce58..fb76e80d 100644 --- a/src/domain.rs +++ b/src/fqdn.rs @@ -5,13 +5,13 @@ use std::borrow::Cow; use crate::{Error, Result}; #[derive(Clone, PartialEq)] -pub struct Domain<'a> { +pub struct FQDN<'a> { inner: Cow<'a, str>, } // TODO likely needs further validation #[allow(non_snake_case)] -pub fn Domain<'a>(input: impl Into>) -> Result> { +pub fn FQDN<'a>(input: impl Into>) -> Result> { let input = input.into(); if !input.ends_with('.') { return Err("domain must end with a `.`".into()); @@ -21,15 +21,15 @@ pub fn Domain<'a>(input: impl Into>) -> Result> { return Err("non-root domain cannot start with a `.`".into()); } - Ok(Domain { inner: input }) + Ok(FQDN { inner: input }) } -impl<'a> Domain<'a> { - pub const ROOT: Domain<'static> = Domain { +impl<'a> FQDN<'a> { + pub const ROOT: FQDN<'static> = FQDN { inner: Cow::Borrowed("."), }; - pub const COM: Domain<'static> = Domain { + pub const COM: FQDN<'static> = FQDN { inner: Cow::Borrowed("com."), }; @@ -41,33 +41,33 @@ impl<'a> Domain<'a> { &self.inner } - pub fn into_owned(self) -> Domain<'static> { + pub fn into_owned(self) -> FQDN<'static> { let owned = match self.inner { Cow::Borrowed(borrowed) => borrowed.to_string(), Cow::Owned(owned) => owned, }; - Domain { + FQDN { inner: Cow::Owned(owned), } } } -impl FromStr for Domain<'static> { +impl FromStr for FQDN<'static> { type Err = Error; fn from_str(input: &str) -> Result { - Ok(Domain(input)?.into_owned()) + Ok(FQDN(input)?.into_owned()) } } -impl fmt::Debug for Domain<'_> { +impl fmt::Debug for FQDN<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(self, f) } } -impl fmt::Display for Domain<'_> { +impl fmt::Display for FQDN<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.inner) } diff --git a/src/lib.rs b/src/lib.rs index 22481203..d312149a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ use std::sync::atomic::{self, AtomicUsize}; pub use crate::client::Client; -pub use crate::domain::Domain; +pub use crate::fqdn::FQDN; pub use crate::name_server::NameServer; pub use crate::recursive_resolver::RecursiveResolver; @@ -12,7 +12,7 @@ const CHMOD_RW_EVERYONE: &str = "666"; mod client; pub mod container; -mod domain; +mod fqdn; mod name_server; pub mod record; mod recursive_resolver; diff --git a/src/name_server.rs b/src/name_server.rs index 327b5455..c6466bbf 100644 --- a/src/name_server.rs +++ b/src/name_server.rs @@ -3,7 +3,7 @@ use std::process::Child; use crate::container::Container; use crate::record::{self, Referral, SoaSettings, ZoneFile}; -use crate::{Domain, Result, CHMOD_RW_EVERYONE}; +use crate::{Result, CHMOD_RW_EVERYONE, FQDN}; pub struct NameServer<'a, State> { container: Container, @@ -23,7 +23,7 @@ impl<'a> NameServer<'a, Stopped> { /// /// - one SOA record, with the primary name server set to the name server domain /// - one NS record, with the name server domain set as the only available name server for - pub fn new(zone: Domain<'a>) -> Result { + pub fn new(zone: FQDN<'a>) -> Result { let ns_count = crate::nameserver_count(); let nameserver = primary_ns(ns_count); @@ -54,7 +54,7 @@ impl<'a> NameServer<'a, Stopped> { } /// Adds an A record pair to the zone file - pub fn a(&mut self, domain: Domain<'a>, ipv4_addr: Ipv4Addr) -> &mut Self { + pub fn a(&mut self, domain: FQDN<'a>, ipv4_addr: Ipv4Addr) -> &mut Self { self.zone_file.record(record::A { domain, ipv4_addr }); self } @@ -102,7 +102,7 @@ impl<'a, S> NameServer<'a, S> { &self.zone_file } - pub fn nameserver(&self) -> &Domain<'a> { + pub fn nameserver(&self) -> &FQDN<'a> { &self.zone_file.soa.ns } } @@ -119,15 +119,15 @@ impl Drop for Running { } } -fn primary_ns(ns_count: usize) -> Domain<'static> { - Domain(format!("primary{ns_count}.nameservers.com.")).unwrap() +fn primary_ns(ns_count: usize) -> FQDN<'static> { + FQDN(format!("primary{ns_count}.nameservers.com.")).unwrap() } -fn admin_ns(ns_count: usize) -> Domain<'static> { - Domain(format!("admin{ns_count}.nameservers.com.")).unwrap() +fn admin_ns(ns_count: usize) -> FQDN<'static> { + FQDN(format!("admin{ns_count}.nameservers.com.")).unwrap() } -fn nsd_conf(domain: &Domain) -> String { +fn nsd_conf(domain: &FQDN) -> String { minijinja::render!( include_str!("templates/nsd.conf.jinja"), domain => domain.as_str() @@ -145,11 +145,11 @@ mod tests { #[test] fn simplest() -> Result<()> { - let tld_ns = NameServer::new(Domain::COM)?.start()?; + let tld_ns = NameServer::new(FQDN::COM)?.start()?; let ip_addr = tld_ns.ipv4_addr(); let client = Client::new()?; - let output = client.dig(Recurse::No, ip_addr, RecordType::SOA, &Domain::COM)?; + let output = client.dig(Recurse::No, ip_addr, RecordType::SOA, &FQDN::COM)?; assert!(output.status.is_noerror()); @@ -159,11 +159,11 @@ mod tests { #[test] fn with_referral() -> Result<()> { let expected_ip_addr = Ipv4Addr::new(172, 17, 200, 1); - let mut root_ns = NameServer::new(Domain::ROOT)?; + let mut root_ns = NameServer::new(FQDN::ROOT)?; root_ns.referral(&Referral { - domain: Domain::COM, + domain: FQDN::COM, ipv4_addr: expected_ip_addr, - ns: Domain("primary.tld-server.com.")?, + ns: FQDN("primary.tld-server.com.")?, }); let root_ns = root_ns.start()?; @@ -172,7 +172,7 @@ mod tests { let ipv4_addr = root_ns.ipv4_addr(); let client = Client::new()?; - let output = client.dig(Recurse::No, ipv4_addr, RecordType::NS, &Domain::COM)?; + let output = client.dig(Recurse::No, ipv4_addr, RecordType::NS, &FQDN::COM)?; assert!(output.status.is_noerror()); diff --git a/src/record.rs b/src/record.rs index 0c3b7eb0..04c42cc5 100644 --- a/src/record.rs +++ b/src/record.rs @@ -5,10 +5,10 @@ use core::fmt; use std::net::Ipv4Addr; -use crate::Domain; +use crate::FQDN; pub struct ZoneFile<'a> { - pub origin: Domain<'a>, + pub origin: FQDN<'a>, pub ttl: u32, pub soa: Soa<'a>, pub records: Vec>, @@ -16,7 +16,7 @@ pub struct ZoneFile<'a> { impl<'a> ZoneFile<'a> { /// Convenience constructor that uses "reasonable" defaults - pub fn new(origin: Domain<'a>, soa: Soa<'a>) -> Self { + pub fn new(origin: FQDN<'a>, soa: Soa<'a>) -> Self { Self { origin, ttl: 1800, @@ -71,20 +71,20 @@ impl fmt::Display for ZoneFile<'_> { } pub struct Referral<'a> { - pub domain: Domain<'a>, + pub domain: FQDN<'a>, pub ipv4_addr: Ipv4Addr, - pub ns: Domain<'a>, + pub ns: FQDN<'a>, } pub struct Root<'a> { pub ipv4_addr: Ipv4Addr, - pub ns: Domain<'a>, + pub ns: FQDN<'a>, pub ttl: u32, } impl<'a> Root<'a> { /// Convenience constructor that uses "reasonable" defaults - pub fn new(ns: Domain<'a>, ipv4_addr: Ipv4Addr) -> Self { + pub fn new(ns: FQDN<'a>, ipv4_addr: Ipv4Addr) -> Self { Self { ipv4_addr, ns, @@ -130,7 +130,7 @@ impl fmt::Display for Record<'_> { #[derive(Clone)] pub struct A<'a> { - pub domain: Domain<'a>, + pub domain: FQDN<'a>, pub ipv4_addr: Ipv4Addr, } @@ -143,8 +143,8 @@ impl fmt::Display for A<'_> { } pub struct Ns<'a> { - pub domain: Domain<'a>, - pub ns: Domain<'a>, + pub domain: FQDN<'a>, + pub ns: FQDN<'a>, } impl fmt::Display for Ns<'_> { @@ -156,9 +156,9 @@ impl fmt::Display for Ns<'_> { } pub struct Soa<'a> { - pub domain: Domain<'a>, - pub ns: Domain<'a>, - pub admin: Domain<'a>, + pub domain: FQDN<'a>, + pub ns: FQDN<'a>, + pub admin: FQDN<'a>, pub settings: SoaSettings, } @@ -237,7 +237,7 @@ mod tests { fn root_to_string() -> Result<()> { let expected = ". 3600000 NS a.root-servers.net. a.root-servers.net. 3600000 A 198.41.0.4"; - let root = Root::new(Domain("a.root-servers.net.")?, Ipv4Addr::new(198, 41, 0, 4)); + let root = Root::new(FQDN("a.root-servers.net.")?, Ipv4Addr::new(198, 41, 0, 4)); assert_eq!(expected, root.to_string()); Ok(()) } @@ -260,7 +260,7 @@ $TTL 1800 com. IN NS e.gtld-servers.net. e.gtld-servers.net. IN A 192.12.94.30 "; - let mut zone = ZoneFile::new(Domain::ROOT, example_soa()?); + let mut zone = ZoneFile::new(FQDN::ROOT, example_soa()?); zone.record(example_ns()?); zone.record(example_a()?); @@ -271,23 +271,23 @@ e.gtld-servers.net. IN A 192.12.94.30 fn example_a() -> Result> { Ok(A { - domain: Domain("e.gtld-servers.net.")?, + domain: FQDN("e.gtld-servers.net.")?, ipv4_addr: Ipv4Addr::new(192, 12, 94, 30), }) } fn example_ns() -> Result> { Ok(Ns { - domain: Domain("com.")?, - ns: Domain("e.gtld-servers.net.")?, + domain: FQDN::COM, + ns: FQDN("e.gtld-servers.net.")?, }) } fn example_soa() -> Result> { Ok(Soa { - domain: Domain(".")?, - ns: Domain("a.root-servers.net.")?, - admin: Domain("nstld.verisign-grs.com.")?, + domain: FQDN::ROOT, + ns: FQDN("a.root-servers.net.")?, + admin: FQDN("nstld.verisign-grs.com.")?, settings: SoaSettings::default(), }) } diff --git a/src/recursive_resolver.rs b/src/recursive_resolver.rs index d7386afb..eb1b7f0b 100644 --- a/src/recursive_resolver.rs +++ b/src/recursive_resolver.rs @@ -43,7 +43,7 @@ mod tests { use crate::{ client::{RecordType, Recurse}, record::Referral, - Client, Domain, NameServer, + Client, NameServer, FQDN, }; use super::*; @@ -51,12 +51,12 @@ mod tests { #[test] fn can_resolve() -> Result<()> { let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4); - let needle = Domain("example.nameservers.com.")?; + let needle = FQDN("example.nameservers.com.")?; - let mut root_ns = NameServer::new(Domain::ROOT)?; - let mut com_ns = NameServer::new(Domain::COM)?; + let mut root_ns = NameServer::new(FQDN::ROOT)?; + let mut com_ns = NameServer::new(FQDN::COM)?; - let nameservers_domain = Domain("nameservers.com.")?; + let nameservers_domain = FQDN("nameservers.com.")?; let mut nameservers_ns = NameServer::new(nameservers_domain.clone())?; nameservers_ns .a(root_ns.nameserver().clone(), root_ns.ipv4_addr()) @@ -76,7 +76,7 @@ mod tests { eprintln!("com.zone:\n{}", com_ns.zone_file()); root_ns.referral(&Referral { - domain: Domain::COM, + domain: FQDN::COM, ipv4_addr: com_ns.ipv4_addr(), ns: com_ns.nameserver().clone(), }); From 5858309bfa530baa0c0be2be8b690a10b2e4fd33 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 6 Feb 2024 18:52:30 +0100 Subject: [PATCH 029/124] revise names and module organization --- src/client.rs | 177 +--------------- src/fqdn.rs | 4 +- src/lib.rs | 16 +- src/name_server.rs | 68 ++++--- src/record.rs | 385 +++++++++++++---------------------- src/recursive_resolver.rs | 36 ++-- src/templates/nsd.conf.jinja | 2 +- src/zone_file.rs | 287 ++++++++++++++++++++++++++ 8 files changed, 491 insertions(+), 484 deletions(-) create mode 100644 src/zone_file.rs diff --git a/src/client.rs b/src/client.rs index 38a01ebf..b66522a2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,9 +1,8 @@ -use core::array; -use core::result::Result as CoreResult; use core::str::FromStr; use std::net::Ipv4Addr; use crate::container::Container; +use crate::record::{Record, RecordType}; use crate::{Error, Result, FQDN}; pub struct Client { @@ -22,37 +21,20 @@ impl Client { recurse: Recurse, server: Ipv4Addr, record_type: RecordType, - domain: &FQDN<'_>, + fqdn: &FQDN<'_>, ) -> Result { let output = self.inner.stdout(&[ "dig", recurse.as_str(), &format!("@{server}"), record_type.as_str(), - domain.as_str(), + fqdn.as_str(), ])?; output.parse() } } -#[allow(clippy::upper_case_acronyms)] -pub enum RecordType { - A, - NS, - SOA, -} - -impl RecordType { - fn as_str(&self) -> &'static str { - match self { - RecordType::A => "A", - RecordType::SOA => "SOA", - RecordType::NS => "NS", - } - } -} - #[derive(Clone, Copy)] pub enum Recurse { Yes, @@ -213,132 +195,12 @@ impl FromStr for DigStatus { } } -#[derive(Debug)] -#[allow(clippy::upper_case_acronyms)] -pub enum Record { - A(A), - SOA(SOA), -} - -impl Record { - pub fn try_into_a(self) -> CoreResult { - if let Self::A(v) = self { - Ok(v) - } else { - Err(self) - } - } -} - -impl FromStr for Record { - type Err = Error; - - fn from_str(input: &str) -> Result { - let record_type = input - .split_whitespace() - .nth(3) - .ok_or("record is missing the type column")?; - - let record = match record_type { - "A" => Record::A(input.parse()?), - "NS" => todo!(), - "SOA" => Record::SOA(input.parse()?), - _ => return Err(format!("unknown record type: {record_type}").into()), - }; - - Ok(record) - } -} - -#[derive(Debug)] -pub struct A { - pub domain: FQDN<'static>, - pub ttl: u32, - pub ipv4_addr: Ipv4Addr, -} - -impl FromStr for A { - type Err = Error; - - fn from_str(input: &str) -> Result { - let mut columns = input.split_whitespace(); - - let [Some(domain), Some(ttl), Some(class), Some(record_type), Some(ipv4_addr), None] = - array::from_fn(|_| columns.next()) - else { - return Err("expected 5 columns".into()); - }; - - if record_type != "A" { - return Err(format!("tried to parse `{record_type}` record as an A record").into()); - } - - if class != "IN" { - return Err(format!("unknown class: {class}").into()); - } - - Ok(Self { - domain: domain.parse()?, - ttl: ttl.parse()?, - ipv4_addr: ipv4_addr.parse()?, - }) - } -} - -#[allow(clippy::upper_case_acronyms)] -#[derive(Debug)] -pub struct SOA { - pub domain: FQDN<'static>, - pub ttl: u32, - pub nameserver: FQDN<'static>, - pub admin: FQDN<'static>, - pub serial: u32, - pub refresh: u32, - pub retry: u32, - pub expire: u32, - pub minimum: u32, -} - -impl FromStr for SOA { - type Err = Error; - - fn from_str(input: &str) -> Result { - let mut columns = input.split_whitespace(); - - let [Some(domain), Some(ttl), Some(class), Some(record_type), Some(nameserver), Some(admin), Some(serial), Some(refresh), Some(retry), Some(expire), Some(minimum), None] = - array::from_fn(|_| columns.next()) - else { - return Err("expected 11 columns".into()); - }; - - if record_type != "SOA" { - return Err(format!("tried to parse `{record_type}` record as a SOA record").into()); - } - - if class != "IN" { - return Err(format!("unknown class: {class}").into()); - } - - Ok(Self { - domain: domain.parse()?, - ttl: ttl.parse()?, - nameserver: nameserver.parse()?, - admin: admin.parse()?, - serial: serial.parse()?, - refresh: refresh.parse()?, - retry: retry.parse()?, - expire: expire.parse()?, - minimum: minimum.parse()?, - }) - } -} - #[cfg(test)] mod tests { use super::*; #[test] - fn nxdomain() -> Result<()> { + fn dig_nxdomain() -> Result<()> { // $ dig nonexistent.domain. let input = " ; <<>> DiG 9.18.18-0ubuntu0.22.04.1-Ubuntu <<>> nonexistent.domain. @@ -374,35 +236,4 @@ mod tests { Ok(()) } - - #[test] - fn can_parse_a_record() -> Result<()> { - let input = "a.root-servers.net. 3600000 IN A 198.41.0.4"; - let a: A = input.parse()?; - - assert_eq!("a.root-servers.net.", a.domain.as_str()); - assert_eq!(3600000, a.ttl); - assert_eq!(Ipv4Addr::new(198, 41, 0, 4), a.ipv4_addr); - - Ok(()) - } - - #[test] - fn can_parse_soa_record() -> Result<()> { - let input = ". 15633 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2024020501 1800 900 604800 86400"; - - let soa: SOA = input.parse()?; - - assert_eq!(".", soa.domain.as_str()); - assert_eq!(15633, soa.ttl); - assert_eq!("a.root-servers.net.", soa.nameserver.as_str()); - assert_eq!("nstld.verisign-grs.com.", soa.admin.as_str()); - assert_eq!(2024020501, soa.serial); - assert_eq!(1800, soa.refresh); - assert_eq!(900, soa.retry); - assert_eq!(604800, soa.expire); - assert_eq!(86400, soa.minimum); - - Ok(()) - } } diff --git a/src/fqdn.rs b/src/fqdn.rs index fb76e80d..18e08a8b 100644 --- a/src/fqdn.rs +++ b/src/fqdn.rs @@ -14,11 +14,11 @@ pub struct FQDN<'a> { pub fn FQDN<'a>(input: impl Into>) -> Result> { let input = input.into(); if !input.ends_with('.') { - return Err("domain must end with a `.`".into()); + return Err("FQDN must end with a `.`".into()); } if input != "." && input.starts_with('.') { - return Err("non-root domain cannot start with a `.`".into()); + return Err("non-root FQDN cannot start with a `.`".into()); } Ok(FQDN { inner: input }) diff --git a/src/lib.rs b/src/lib.rs index d312149a..20d12ed7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,4 @@ -use std::sync::atomic::{self, AtomicUsize}; - -pub use crate::client::Client; pub use crate::fqdn::FQDN; -pub use crate::name_server::NameServer; pub use crate::recursive_resolver::RecursiveResolver; pub type Error = Box; @@ -10,14 +6,10 @@ pub type Result = core::result::Result; const CHMOD_RW_EVERYONE: &str = "666"; -mod client; -pub mod container; +pub mod client; +mod container; mod fqdn; -mod name_server; +pub mod name_server; pub mod record; mod recursive_resolver; - -fn nameserver_count() -> usize { - static COUNT: AtomicUsize = AtomicUsize::new(0); - COUNT.fetch_add(1, atomic::Ordering::Relaxed) -} +pub mod zone_file; diff --git a/src/name_server.rs b/src/name_server.rs index c6466bbf..8c4c3aa2 100644 --- a/src/name_server.rs +++ b/src/name_server.rs @@ -1,8 +1,9 @@ +use core::sync::atomic::{self, AtomicUsize}; use std::net::Ipv4Addr; use std::process::Child; use crate::container::Container; -use crate::record::{self, Referral, SoaSettings, ZoneFile}; +use crate::zone_file::{self, SoaSettings, ZoneFile}; use crate::{Result, CHMOD_RW_EVERYONE, FQDN}; pub struct NameServer<'a, State> { @@ -21,23 +22,24 @@ impl<'a> NameServer<'a, Stopped> { /// /// The zone file will contain these records /// - /// - one SOA record, with the primary name server set to the name server domain - /// - one NS record, with the name server domain set as the only available name server for + /// - one SOA record, with the primary name server field set to this name server's FQDN + /// - one NS record, with this name server's FQDN set as the only available name server for + /// the zone pub fn new(zone: FQDN<'a>) -> Result { - let ns_count = crate::nameserver_count(); + let ns_count = ns_count(); let nameserver = primary_ns(ns_count); - let soa = record::Soa { - domain: zone.clone(), - ns: nameserver.clone(), + let soa = zone_file::SOA { + zone: zone.clone(), + nameserver: nameserver.clone(), admin: admin_ns(ns_count), settings: SoaSettings::default(), }; let mut zone_file = ZoneFile::new(zone.clone(), soa); - zone_file.record(record::Ns { - domain: zone, - ns: nameserver.clone(), + zone_file.entry(zone_file::NS { + zone, + nameserver: nameserver.clone(), }); Ok(Self { @@ -48,14 +50,19 @@ impl<'a> NameServer<'a, Stopped> { } /// Adds a NS + A record pair to the zone file - pub fn referral(&mut self, referral: &Referral<'a>) -> &mut Self { - self.zone_file.referral(referral); + pub fn referral( + &mut self, + zone: FQDN<'a>, + nameserver: FQDN<'a>, + ipv4_addr: Ipv4Addr, + ) -> &mut Self { + self.zone_file.referral(zone, nameserver, ipv4_addr); self } /// Adds an A record pair to the zone file - pub fn a(&mut self, domain: FQDN<'a>, ipv4_addr: Ipv4Addr) -> &mut Self { - self.zone_file.record(record::A { domain, ipv4_addr }); + pub fn a(&mut self, fqdn: FQDN<'a>, ipv4_addr: Ipv4Addr) -> &mut Self { + self.zone_file.entry(zone_file::A { fqdn, ipv4_addr }); self } @@ -93,6 +100,11 @@ impl<'a> NameServer<'a, Stopped> { } } +fn ns_count() -> usize { + static COUNT: AtomicUsize = AtomicUsize::new(0); + COUNT.fetch_add(1, atomic::Ordering::Relaxed) +} + impl<'a, S> NameServer<'a, S> { pub fn ipv4_addr(&self) -> Ipv4Addr { self.container.ipv4_addr() @@ -102,8 +114,12 @@ impl<'a, S> NameServer<'a, S> { &self.zone_file } - pub fn nameserver(&self) -> &FQDN<'a> { - &self.zone_file.soa.ns + pub fn zone(&self) -> &FQDN<'a> { + &self.zone_file.origin + } + + pub fn fqdn(&self) -> &FQDN<'a> { + &self.zone_file.soa.nameserver } } @@ -127,19 +143,17 @@ fn admin_ns(ns_count: usize) -> FQDN<'static> { FQDN(format!("admin{ns_count}.nameservers.com.")).unwrap() } -fn nsd_conf(domain: &FQDN) -> String { +fn nsd_conf(fqdn: &FQDN) -> String { minijinja::render!( include_str!("templates/nsd.conf.jinja"), - domain => domain.as_str() + fqdn => fqdn.as_str() ) } #[cfg(test)] mod tests { - use crate::{ - client::{RecordType, Recurse}, - Client, - }; + use crate::client::{Client, Recurse}; + use crate::record::RecordType; use super::*; @@ -160,11 +174,11 @@ mod tests { fn with_referral() -> Result<()> { let expected_ip_addr = Ipv4Addr::new(172, 17, 200, 1); let mut root_ns = NameServer::new(FQDN::ROOT)?; - root_ns.referral(&Referral { - domain: FQDN::COM, - ipv4_addr: expected_ip_addr, - ns: FQDN("primary.tld-server.com.")?, - }); + root_ns.referral( + FQDN::COM, + FQDN("primary.tld-server.com.")?, + expected_ip_addr, + ); let root_ns = root_ns.start()?; eprintln!("root.zone:\n{}", root_ns.zone_file()); diff --git a/src/record.rs b/src/record.rs index 04c42cc5..cc4edbfe 100644 --- a/src/record.rs +++ b/src/record.rs @@ -1,181 +1,108 @@ -//! DNS records in BIND syntax -//! -//! Note that the `@` syntax is not used to avoid relying on the order of the records +//! Text representation of DNS records -use core::fmt; +use core::array; +use core::result::Result as CoreResult; +use core::str::FromStr; use std::net::Ipv4Addr; -use crate::FQDN; +use crate::{Error, Result, FQDN}; -pub struct ZoneFile<'a> { - pub origin: FQDN<'a>, - pub ttl: u32, - pub soa: Soa<'a>, - pub records: Vec>, +#[allow(clippy::upper_case_acronyms)] +pub enum RecordType { + A, + NS, + SOA, } -impl<'a> ZoneFile<'a> { - /// Convenience constructor that uses "reasonable" defaults - pub fn new(origin: FQDN<'a>, soa: Soa<'a>) -> Self { - Self { - origin, - ttl: 1800, - soa, - records: Vec::new(), - } - } - - /// Appends a record - pub fn record(&mut self, record: impl Into>) { - self.records.push(record.into()) - } - - /// Appends a NS + A record pair - pub fn referral(&mut self, referral: &Referral<'a>) { - let Referral { - domain, - ipv4_addr, - ns, - } = referral; - - self.record(Ns { - domain: domain.clone(), - ns: ns.clone(), - }); - self.record(A { - domain: ns.clone(), - ipv4_addr: *ipv4_addr, - }); - } -} - -impl fmt::Display for ZoneFile<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { - origin, - ttl, - soa, - records, - } = self; - - writeln!(f, "$ORIGIN {origin}")?; - writeln!(f, "$TTL {ttl}")?; - writeln!(f, "{soa}")?; - - for record in records { - writeln!(f, "{record}")?; - } - - Ok(()) - } -} - -pub struct Referral<'a> { - pub domain: FQDN<'a>, - pub ipv4_addr: Ipv4Addr, - pub ns: FQDN<'a>, -} - -pub struct Root<'a> { - pub ipv4_addr: Ipv4Addr, - pub ns: FQDN<'a>, - pub ttl: u32, -} - -impl<'a> Root<'a> { - /// Convenience constructor that uses "reasonable" defaults - pub fn new(ns: FQDN<'a>, ipv4_addr: Ipv4Addr) -> Self { - Self { - ipv4_addr, - ns, - ttl: 3600000, // 1000 hours - } - } -} - -impl fmt::Display for Root<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { ipv4_addr, ns, ttl } = self; - - writeln!(f, ".\t{ttl}\tNS\t{ns}")?; - write!(f, "{ns}\t{ttl}\tA\t{ipv4_addr}") - } -} - -pub enum Record<'a> { - A(A<'a>), - Ns(Ns<'a>), -} - -impl<'a> From> for Record<'a> { - fn from(v: A<'a>) -> Self { - Self::A(v) - } -} - -impl<'a> From> for Record<'a> { - fn from(v: Ns<'a>) -> Self { - Self::Ns(v) - } -} - -impl fmt::Display for Record<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl RecordType { + pub fn as_str(&self) -> &'static str { match self { - Record::A(a) => a.fmt(f), - Record::Ns(ns) => ns.fmt(f), + RecordType::A => "A", + RecordType::SOA => "SOA", + RecordType::NS => "NS", } } } -#[derive(Clone)] -pub struct A<'a> { - pub domain: FQDN<'a>, +#[derive(Debug)] +#[allow(clippy::upper_case_acronyms)] +pub enum Record { + A(A), + SOA(SOA), +} + +impl Record { + pub fn try_into_a(self) -> CoreResult { + if let Self::A(v) = self { + Ok(v) + } else { + Err(self) + } + } +} + +impl FromStr for Record { + type Err = Error; + + fn from_str(input: &str) -> Result { + let record_type = input + .split_whitespace() + .nth(3) + .ok_or("record is missing the type column")?; + + let record = match record_type { + "A" => Record::A(input.parse()?), + "NS" => todo!(), + "SOA" => Record::SOA(input.parse()?), + _ => return Err(format!("unknown record type: {record_type}").into()), + }; + + Ok(record) + } +} + +#[derive(Debug)] +pub struct A { + pub fqdn: FQDN<'static>, + pub ttl: u32, pub ipv4_addr: Ipv4Addr, } -impl fmt::Display for A<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { domain, ipv4_addr } = self; +impl FromStr for A { + type Err = Error; - write!(f, "{domain}\tIN\tA\t{ipv4_addr}") + fn from_str(input: &str) -> Result { + let mut columns = input.split_whitespace(); + + let [Some(fqdn), Some(ttl), Some(class), Some(record_type), Some(ipv4_addr), None] = + array::from_fn(|_| columns.next()) + else { + return Err("expected 5 columns".into()); + }; + + if record_type != "A" { + return Err(format!("tried to parse `{record_type}` record as an A record").into()); + } + + if class != "IN" { + return Err(format!("unknown class: {class}").into()); + } + + Ok(Self { + fqdn: fqdn.parse()?, + ttl: ttl.parse()?, + ipv4_addr: ipv4_addr.parse()?, + }) } } -pub struct Ns<'a> { - pub domain: FQDN<'a>, - pub ns: FQDN<'a>, -} - -impl fmt::Display for Ns<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { domain, ns } = self; - - write!(f, "{domain}\tIN\tNS\t{ns}") - } -} - -pub struct Soa<'a> { - pub domain: FQDN<'a>, - pub ns: FQDN<'a>, - pub admin: FQDN<'a>, - pub settings: SoaSettings, -} - -impl fmt::Display for Soa<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { - domain, - ns, - admin, - settings, - } = self; - - write!(f, "{domain}\tIN\tSOA\t{ns}\t{admin}\t{settings}") - } -} - -pub struct SoaSettings { +#[allow(clippy::upper_case_acronyms)] +#[derive(Debug)] +pub struct SOA { + pub zone: FQDN<'static>, + pub ttl: u32, + pub nameserver: FQDN<'static>, + pub admin: FQDN<'static>, pub serial: u32, pub refresh: u32, pub retry: u32, @@ -183,112 +110,72 @@ pub struct SoaSettings { pub minimum: u32, } -impl Default for SoaSettings { - fn default() -> Self { - Self { - serial: 2024010101, - refresh: 1800, // 30 minutes - retry: 900, // 15 minutes - expire: 604800, // 1 week - minimum: 86400, // 1 day +impl FromStr for SOA { + type Err = Error; + + fn from_str(input: &str) -> Result { + let mut columns = input.split_whitespace(); + + let [Some(zone), Some(ttl), Some(class), Some(record_type), Some(nameserver), Some(admin), Some(serial), Some(refresh), Some(retry), Some(expire), Some(minimum), None] = + array::from_fn(|_| columns.next()) + else { + return Err("expected 11 columns".into()); + }; + + if record_type != "SOA" { + return Err(format!("tried to parse `{record_type}` record as a SOA record").into()); } - } -} -impl fmt::Display for SoaSettings { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { - serial, - refresh, - retry, - expire, - minimum, - } = self; + if class != "IN" { + return Err(format!("unknown class: {class}").into()); + } - write!(f, "( {serial} {refresh} {retry} {expire} {minimum} )") + Ok(Self { + zone: zone.parse()?, + ttl: ttl.parse()?, + nameserver: nameserver.parse()?, + admin: admin.parse()?, + serial: serial.parse()?, + refresh: refresh.parse()?, + retry: retry.parse()?, + expire: expire.parse()?, + minimum: minimum.parse()?, + }) } } #[cfg(test)] mod tests { - use crate::Result; - use super::*; #[test] - fn a_to_string() -> Result<()> { - let expected = "e.gtld-servers.net. IN A 192.12.94.30"; - let a = example_a()?; - assert_eq!(expected, a.to_string()); + fn can_parse_a_record() -> Result<()> { + let input = "a.root-servers.net. 3600000 IN A 198.41.0.4"; + let a: A = input.parse()?; + + assert_eq!("a.root-servers.net.", a.fqdn.as_str()); + assert_eq!(3600000, a.ttl); + assert_eq!(Ipv4Addr::new(198, 41, 0, 4), a.ipv4_addr); Ok(()) } #[test] - fn ns_to_string() -> Result<()> { - let expected = "com. IN NS e.gtld-servers.net."; - let ns = example_ns()?; - assert_eq!(expected, ns.to_string()); + fn can_parse_soa_record() -> Result<()> { + let input = ". 15633 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2024020501 1800 900 604800 86400"; + + let soa: SOA = input.parse()?; + + assert_eq!(".", soa.zone.as_str()); + assert_eq!(15633, soa.ttl); + assert_eq!("a.root-servers.net.", soa.nameserver.as_str()); + assert_eq!("nstld.verisign-grs.com.", soa.admin.as_str()); + assert_eq!(2024020501, soa.serial); + assert_eq!(1800, soa.refresh); + assert_eq!(900, soa.retry); + assert_eq!(604800, soa.expire); + assert_eq!(86400, soa.minimum); Ok(()) } - - #[test] - fn root_to_string() -> Result<()> { - let expected = ". 3600000 NS a.root-servers.net. -a.root-servers.net. 3600000 A 198.41.0.4"; - let root = Root::new(FQDN("a.root-servers.net.")?, Ipv4Addr::new(198, 41, 0, 4)); - assert_eq!(expected, root.to_string()); - Ok(()) - } - - #[test] - fn soa_to_string() -> Result<()> { - let expected = - ". IN SOA a.root-servers.net. nstld.verisign-grs.com. ( 2024010101 1800 900 604800 86400 )"; - let soa = example_soa()?; - assert_eq!(expected, soa.to_string()); - - Ok(()) - } - - #[test] - fn zone_file_to_string() -> Result<()> { - let expected = "$ORIGIN . -$TTL 1800 -. IN SOA a.root-servers.net. nstld.verisign-grs.com. ( 2024010101 1800 900 604800 86400 ) -com. IN NS e.gtld-servers.net. -e.gtld-servers.net. IN A 192.12.94.30 -"; - let mut zone = ZoneFile::new(FQDN::ROOT, example_soa()?); - zone.record(example_ns()?); - zone.record(example_a()?); - - assert_eq!(expected, zone.to_string()); - - Ok(()) - } - - fn example_a() -> Result> { - Ok(A { - domain: FQDN("e.gtld-servers.net.")?, - ipv4_addr: Ipv4Addr::new(192, 12, 94, 30), - }) - } - - fn example_ns() -> Result> { - Ok(Ns { - domain: FQDN::COM, - ns: FQDN("e.gtld-servers.net.")?, - }) - } - - fn example_soa() -> Result> { - Ok(Soa { - domain: FQDN::ROOT, - ns: FQDN("a.root-servers.net.")?, - admin: FQDN("nstld.verisign-grs.com.")?, - settings: SoaSettings::default(), - }) - } } diff --git a/src/recursive_resolver.rs b/src/recursive_resolver.rs index eb1b7f0b..3d7b6431 100644 --- a/src/recursive_resolver.rs +++ b/src/recursive_resolver.rs @@ -3,7 +3,7 @@ use std::net::Ipv4Addr; use std::process::Child; use crate::container::Container; -use crate::record::Root; +use crate::zone_file::Root; use crate::{Result, CHMOD_RW_EVERYONE}; pub struct RecursiveResolver { @@ -41,9 +41,10 @@ impl Drop for RecursiveResolver { #[cfg(test)] mod tests { use crate::{ - client::{RecordType, Recurse}, - record::Referral, - Client, NameServer, FQDN, + client::{Client, Recurse}, + name_server::NameServer, + record::RecordType, + FQDN, }; use super::*; @@ -56,35 +57,30 @@ mod tests { let mut root_ns = NameServer::new(FQDN::ROOT)?; let mut com_ns = NameServer::new(FQDN::COM)?; - let nameservers_domain = FQDN("nameservers.com.")?; - let mut nameservers_ns = NameServer::new(nameservers_domain.clone())?; + let mut nameservers_ns = NameServer::new(FQDN("nameservers.com.")?)?; nameservers_ns - .a(root_ns.nameserver().clone(), root_ns.ipv4_addr()) - .a(com_ns.nameserver().clone(), com_ns.ipv4_addr()) + .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(&Referral { - domain: nameservers_domain, - ipv4_addr: nameservers_ns.ipv4_addr(), - ns: nameservers_ns.nameserver().clone(), - }); + 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(&Referral { - domain: FQDN::COM, - ipv4_addr: com_ns.ipv4_addr(), - ns: com_ns.nameserver().clone(), - }); + 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.nameserver().clone(), root_ns.ipv4_addr())]; + let roots = &[Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr())]; let resolver = RecursiveResolver::start(roots)?; let resolver_ip_addr = resolver.ipv4_addr(); @@ -96,7 +92,7 @@ mod tests { let [answer] = output.answer.try_into().unwrap(); let a = answer.try_into_a().unwrap(); - assert_eq!(needle, a.domain); + assert_eq!(needle, a.fqdn); assert_eq!(expected_ipv4_addr, a.ipv4_addr); Ok(()) diff --git a/src/templates/nsd.conf.jinja b/src/templates/nsd.conf.jinja index d3af5808..2a7541af 100644 --- a/src/templates/nsd.conf.jinja +++ b/src/templates/nsd.conf.jinja @@ -2,5 +2,5 @@ remote-control: control-enable: no zone: - name: {{ domain }} + name: {{ fqdn }} zonefile: /etc/nsd/zones/main.zone diff --git a/src/zone_file.rs b/src/zone_file.rs new file mode 100644 index 00000000..524f26db --- /dev/null +++ b/src/zone_file.rs @@ -0,0 +1,287 @@ +//! BIND-style zone file +//! +//! Note that +//! - the `@` syntax is not used to avoid relying on the order of the entries +//! - relative domain names are not used; all domain names must be in fully-qualified form + +use core::fmt; +use std::net::Ipv4Addr; + +use crate::FQDN; + +pub struct ZoneFile<'a> { + pub origin: FQDN<'a>, + pub ttl: u32, + pub soa: SOA<'a>, + pub entries: Vec>, +} + +impl<'a> ZoneFile<'a> { + /// Convenience constructor that uses "reasonable" defaults + pub fn new(origin: FQDN<'a>, soa: SOA<'a>) -> Self { + Self { + origin, + ttl: 1800, + soa, + entries: Vec::new(), + } + } + + /// Appends an entry + pub fn entry(&mut self, entry: impl Into>) { + self.entries.push(entry.into()) + } + + /// Appends a NS + A entry pair + pub fn referral(&mut self, zone: FQDN<'a>, nameserver: FQDN<'a>, ipv4_addr: Ipv4Addr) { + self.entry(NS { + zone: zone.clone(), + nameserver: nameserver.clone(), + }); + self.entry(A { + fqdn: nameserver, + ipv4_addr, + }); + } +} + +impl fmt::Display for ZoneFile<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + origin, + ttl, + soa, + entries, + } = self; + + writeln!(f, "$ORIGIN {origin}")?; + writeln!(f, "$TTL {ttl}")?; + writeln!(f, "{soa}")?; + + for entry in entries { + writeln!(f, "{entry}")?; + } + + Ok(()) + } +} + +pub struct Root<'a> { + pub ipv4_addr: Ipv4Addr, + pub ns: FQDN<'a>, + pub ttl: u32, +} + +impl<'a> Root<'a> { + /// Convenience constructor that uses "reasonable" defaults + pub fn new(ns: FQDN<'a>, ipv4_addr: Ipv4Addr) -> Self { + Self { + ipv4_addr, + ns, + ttl: 3600000, // 1000 hours + } + } +} + +impl fmt::Display for Root<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { ipv4_addr, ns, ttl } = self; + + writeln!(f, ".\t{ttl}\tNS\t{ns}")?; + write!(f, "{ns}\t{ttl}\tA\t{ipv4_addr}") + } +} + +pub enum Entry<'a> { + A(A<'a>), + NS(NS<'a>), +} + +impl<'a> From> for Entry<'a> { + fn from(v: A<'a>) -> Self { + Self::A(v) + } +} + +impl<'a> From> for Entry<'a> { + fn from(v: NS<'a>) -> Self { + Self::NS(v) + } +} + +impl fmt::Display for Entry<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Entry::A(a) => a.fmt(f), + Entry::NS(ns) => ns.fmt(f), + } + } +} + +#[derive(Clone)] +pub struct A<'a> { + pub fqdn: FQDN<'a>, + pub ipv4_addr: Ipv4Addr, +} + +impl fmt::Display for A<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { fqdn, ipv4_addr } = self; + + write!(f, "{fqdn}\tIN\tA\t{ipv4_addr}") + } +} + +pub struct NS<'a> { + pub zone: FQDN<'a>, + pub nameserver: FQDN<'a>, +} + +impl fmt::Display for NS<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + zone, + nameserver: ns, + } = self; + + write!(f, "{zone}\tIN\tNS\t{ns}") + } +} + +pub struct SOA<'a> { + pub zone: FQDN<'a>, + pub nameserver: FQDN<'a>, + pub admin: FQDN<'a>, + pub settings: SoaSettings, +} + +impl fmt::Display for SOA<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + zone, + nameserver: ns, + admin, + settings, + } = self; + + write!(f, "{zone}\tIN\tSOA\t{ns}\t{admin}\t{settings}") + } +} + +pub struct SoaSettings { + pub serial: u32, + pub refresh: u32, + pub retry: u32, + pub expire: u32, + pub minimum: u32, +} + +impl Default for SoaSettings { + fn default() -> Self { + Self { + serial: 2024010101, + refresh: 1800, // 30 minutes + retry: 900, // 15 minutes + expire: 604800, // 1 week + minimum: 86400, // 1 day + } + } +} + +impl fmt::Display for SoaSettings { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + serial, + refresh, + retry, + expire, + minimum, + } = self; + + write!(f, "( {serial} {refresh} {retry} {expire} {minimum} )") + } +} + +#[cfg(test)] +mod tests { + use crate::Result; + + use super::*; + + #[test] + fn a_to_string() -> Result<()> { + let expected = "e.gtld-servers.net. IN A 192.12.94.30"; + let a = example_a()?; + assert_eq!(expected, a.to_string()); + + Ok(()) + } + + #[test] + fn ns_to_string() -> Result<()> { + let expected = "com. IN NS e.gtld-servers.net."; + let ns = example_ns()?; + assert_eq!(expected, ns.to_string()); + + Ok(()) + } + + #[test] + fn root_to_string() -> Result<()> { + let expected = ". 3600000 NS a.root-servers.net. +a.root-servers.net. 3600000 A 198.41.0.4"; + let root = Root::new(FQDN("a.root-servers.net.")?, Ipv4Addr::new(198, 41, 0, 4)); + assert_eq!(expected, root.to_string()); + Ok(()) + } + + #[test] + fn soa_to_string() -> Result<()> { + let expected = + ". IN SOA a.root-servers.net. nstld.verisign-grs.com. ( 2024010101 1800 900 604800 86400 )"; + let soa = example_soa()?; + assert_eq!(expected, soa.to_string()); + + Ok(()) + } + + #[test] + fn zone_file_to_string() -> Result<()> { + let expected = "$ORIGIN . +$TTL 1800 +. IN SOA a.root-servers.net. nstld.verisign-grs.com. ( 2024010101 1800 900 604800 86400 ) +com. IN NS e.gtld-servers.net. +e.gtld-servers.net. IN A 192.12.94.30 +"; + let mut zone = ZoneFile::new(FQDN::ROOT, example_soa()?); + zone.entry(example_ns()?); + zone.entry(example_a()?); + + assert_eq!(expected, zone.to_string()); + + Ok(()) + } + + fn example_a() -> Result> { + Ok(A { + fqdn: FQDN("e.gtld-servers.net.")?, + ipv4_addr: Ipv4Addr::new(192, 12, 94, 30), + }) + } + + fn example_ns() -> Result> { + Ok(NS { + zone: FQDN::COM, + nameserver: FQDN("e.gtld-servers.net.")?, + }) + } + + fn example_soa() -> Result> { + Ok(SOA { + zone: FQDN::ROOT, + nameserver: FQDN("a.root-servers.net.")?, + admin: FQDN("nstld.verisign-grs.com.")?, + settings: SoaSettings::default(), + }) + } +} From a527ed62181e307d2ee286b3e22fd4838d77e3eb Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 6 Feb 2024 18:53:40 +0100 Subject: [PATCH 030/124] hardcode chmod used in Container::cp everything uses the same value --- src/container.rs | 10 +++++----- src/lib.rs | 2 -- src/name_server.rs | 14 +++----------- src/recursive_resolver.rs | 4 ++-- 4 files changed, 10 insertions(+), 20 deletions(-) diff --git a/src/container.rs b/src/container.rs index 095e591b..0848c85b 100644 --- a/src/container.rs +++ b/src/container.rs @@ -69,7 +69,9 @@ impl Container { }) } - pub fn cp(&self, path_in_container: &str, file_contents: &str, chmod: &str) -> Result<()> { + pub fn cp(&self, path_in_container: &str, file_contents: &str) -> Result<()> { + const CHMOD_RW_EVERYONE: &str = "666"; + let mut temp_file = NamedTempFile::new()?; fs::write(&mut temp_file, file_contents)?; @@ -80,7 +82,7 @@ impl Container { command.args(["cp", &src_path, &dest_path]); checked_output(&mut command)?; - self.status_ok(&["chmod", chmod, path_in_container])?; + self.status_ok(&["chmod", CHMOD_RW_EVERYONE, path_in_container])?; Ok(()) } @@ -213,8 +215,6 @@ impl Drop for Container { #[cfg(test)] mod tests { - use crate::CHMOD_RW_EVERYONE; - use super::*; #[test] @@ -244,7 +244,7 @@ mod tests { let path = "/tmp/somefile"; let contents = "hello"; - container.cp(path, contents, CHMOD_RW_EVERYONE)?; + container.cp(path, contents)?; let output = container.output(&["cat", path])?; dbg!(&output); diff --git a/src/lib.rs b/src/lib.rs index 20d12ed7..60f55948 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,8 +4,6 @@ pub use crate::recursive_resolver::RecursiveResolver; pub type Error = Box; pub type Result = core::result::Result; -const CHMOD_RW_EVERYONE: &str = "666"; - pub mod client; mod container; mod fqdn; diff --git a/src/name_server.rs b/src/name_server.rs index 8c4c3aa2..f1b36bff 100644 --- a/src/name_server.rs +++ b/src/name_server.rs @@ -4,7 +4,7 @@ use std::process::Child; use crate::container::Container; use crate::zone_file::{self, SoaSettings, ZoneFile}; -use crate::{Result, CHMOD_RW_EVERYONE, FQDN}; +use crate::{Result, FQDN}; pub struct NameServer<'a, State> { container: Container, @@ -77,18 +77,10 @@ impl<'a> NameServer<'a, Stopped> { // for PID file container.status_ok(&["mkdir", "-p", "/run/nsd/"])?; - container.cp( - "/etc/nsd/nsd.conf", - &nsd_conf(&zone_file.origin), - CHMOD_RW_EVERYONE, - )?; + container.cp("/etc/nsd/nsd.conf", &nsd_conf(&zone_file.origin))?; container.status_ok(&["mkdir", "-p", "/etc/nsd/zones"])?; - container.cp( - "/etc/nsd/zones/main.zone", - &zone_file.to_string(), - CHMOD_RW_EVERYONE, - )?; + container.cp("/etc/nsd/zones/main.zone", &zone_file.to_string())?; let child = container.spawn(&["nsd", "-d"])?; diff --git a/src/recursive_resolver.rs b/src/recursive_resolver.rs index 3d7b6431..7bd8506a 100644 --- a/src/recursive_resolver.rs +++ b/src/recursive_resolver.rs @@ -4,7 +4,7 @@ use std::process::Child; use crate::container::Container; use crate::zone_file::Root; -use crate::{Result, CHMOD_RW_EVERYONE}; +use crate::Result; pub struct RecursiveResolver { container: Container, @@ -20,7 +20,7 @@ impl RecursiveResolver { writeln!(hints, "{root}").unwrap(); } - container.cp("/etc/unbound/root.hints", &hints, CHMOD_RW_EVERYONE)?; + container.cp("/etc/unbound/root.hints", &hints)?; let child = container.spawn(&["unbound", "-d"])?; From 037cf4f698e545e072a004c77e8f4726dcfad273 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 6 Feb 2024 20:05:21 +0100 Subject: [PATCH 031/124] ns: sign zone file --- docker/unbound.Dockerfile | 2 +- src/client.rs | 21 +++- src/container.rs | 4 +- src/name_server.rs | 233 ++++++++++++++++++++++++++++++++++++-- src/recursive_resolver.rs | 10 +- 5 files changed, 253 insertions(+), 17 deletions(-) diff --git a/docker/unbound.Dockerfile b/docker/unbound.Dockerfile index 51c75d45..138d7abe 100644 --- a/docker/unbound.Dockerfile +++ b/docker/unbound.Dockerfile @@ -1,6 +1,6 @@ FROM ubuntu:22.04 RUN apt-get update && \ - apt-get install -y dnsutils unbound nsd iputils-ping tshark vim + apt-get install -y dnsutils unbound nsd iputils-ping tshark vim ldnsutils COPY ./files/etc/unbound/unbound.conf /etc/unbound/unbound.conf diff --git a/src/client.rs b/src/client.rs index b66522a2..0a9fb1e2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -19,6 +19,7 @@ impl Client { pub fn dig( &self, recurse: Recurse, + dnssec: Dnssec, server: Ipv4Addr, record_type: RecordType, fqdn: &FQDN<'_>, @@ -26,6 +27,7 @@ impl Client { let output = self.inner.stdout(&[ "dig", recurse.as_str(), + dnssec.as_str(), &format!("@{server}"), record_type.as_str(), fqdn.as_str(), @@ -35,6 +37,21 @@ impl Client { } } +#[derive(Clone, Copy)] +pub enum Dnssec { + Yes, + No, +} + +impl Dnssec { + fn as_str(&self) -> &'static str { + match self { + Self::Yes => "+dnssec", + Self::No => "+nodnssec", + } + } +} + #[derive(Clone, Copy)] pub enum Recurse { Yes, @@ -44,8 +61,8 @@ pub enum Recurse { impl Recurse { fn as_str(&self) -> &'static str { match self { - Recurse::Yes => "+recurse", - Recurse::No => "+norecurse", + Self::Yes => "+recurse", + Self::No => "+norecurse", } } } diff --git a/src/container.rs b/src/container.rs index 0848c85b..6690bed5 100644 --- a/src/container.rs +++ b/src/container.rs @@ -154,12 +154,12 @@ impl TryFrom for Output { fn try_from(output: process::Output) -> Result { let mut stderr = String::from_utf8(output.stderr)?; - while stderr.ends_with('\n') { + while stderr.ends_with(|c| matches!(c, '\n' | '\r')) { stderr.pop(); } let mut stdout = String::from_utf8(output.stdout)?; - while stdout.ends_with('\n') { + while stdout.ends_with(|c| matches!(c, '\n' | '\r')) { stdout.pop(); } diff --git a/src/name_server.rs b/src/name_server.rs index f1b36bff..61401158 100644 --- a/src/name_server.rs +++ b/src/name_server.rs @@ -1,15 +1,17 @@ +use core::array; +use core::str::FromStr; use core::sync::atomic::{self, AtomicUsize}; use std::net::Ipv4Addr; use std::process::Child; use crate::container::Container; use crate::zone_file::{self, SoaSettings, ZoneFile}; -use crate::{Result, FQDN}; +use crate::{Error, Result, FQDN}; pub struct NameServer<'a, State> { container: Container, zone_file: ZoneFile<'a>, - _state: State, + state: State, } impl<'a> NameServer<'a, Stopped> { @@ -45,7 +47,7 @@ impl<'a> NameServer<'a, Stopped> { Ok(Self { container: Container::run()?, zone_file, - _state: Stopped, + state: Stopped, }) } @@ -66,12 +68,66 @@ impl<'a> NameServer<'a, Stopped> { self } + /// Freezes and signs the name server's zone file + pub fn sign(self) -> Result> { + // TODO do we want to make these settings configurable? + const ZSK_BITS: usize = 1024; + const KSK_BITS: usize = 2048; + const ALGORITHM: &str = "RSASHA1-NSEC3-SHA1"; + + let Self { + container, + zone_file, + state: _, + } = self; + + container.status_ok(&["mkdir", "-p", ZONES_DIR])?; + container.cp("/etc/nsd/zones/main.zone", &zone_file.to_string())?; + + let zone = &zone_file.origin; + + let zsk_keygen = + format!("cd {ZONES_DIR} && ldns-keygen -a {ALGORITHM} -b {ZSK_BITS} {zone}"); + let zsk_filename = container.stdout(&["sh", "-c", &zsk_keygen])?; + let zsk_path = format!("{ZONES_DIR}/{zsk_filename}.key"); + let zsk: Key = container.stdout(&["cat", &zsk_path])?.parse()?; + + let ksk_keygen = + format!("cd {ZONES_DIR} && ldns-keygen -k -a {ALGORITHM} -b {KSK_BITS} {zone}"); + let ksk_filename = container.stdout(&["sh", "-c", &ksk_keygen])?; + let ksk_path = format!("{ZONES_DIR}/{ksk_filename}.key"); + let ksk: Key = container.stdout(&["cat", &ksk_path])?.parse()?; + + // -n = use NSEC3 instead of NSEC + // -p = set the opt-out flag on all nsec3 rrs + let signzone = format!( + "cd {ZONES_DIR} && ldns-signzone -n -p {ZONE_FILENAME} {zsk_filename} {ksk_filename}" + ); + container.status_ok(&["sh", "-c", &signzone])?; + + // we have an in-memory representation of the zone file so we just delete the on-disk version + let zone_file_path = zone_file_path(); + container.status_ok(&["mv", &format!("{zone_file_path}.signed"), &zone_file_path])?; + + let signed_zone_file = container.stdout(&["cat", &zone_file_path])?; + + Ok(NameServer { + container, + zone_file, + state: Signed { + zsk, + ksk, + signed_zone_file, + }, + }) + } + /// Moves the server to the "Start" state where it can answer client queries pub fn start(self) -> Result> { let Self { container, zone_file, - _state: _, + state: _, } = self; // for PID file @@ -79,24 +135,123 @@ impl<'a> NameServer<'a, Stopped> { container.cp("/etc/nsd/nsd.conf", &nsd_conf(&zone_file.origin))?; - container.status_ok(&["mkdir", "-p", "/etc/nsd/zones"])?; - container.cp("/etc/nsd/zones/main.zone", &zone_file.to_string())?; + container.status_ok(&["mkdir", "-p", ZONES_DIR])?; + container.cp(&zone_file_path(), &zone_file.to_string())?; let child = container.spawn(&["nsd", "-d"])?; Ok(NameServer { container, zone_file, - _state: Running { child }, + state: Running { child }, }) } } +#[derive(Debug)] +pub struct Key { + pub bits: u16, + pub encoded: String, + pub id: u32, +} + +impl FromStr for Key { + type Err = Error; + + fn from_str(input: &str) -> Result { + let (before, after) = input.split_once(';').ok_or("comment was not found")?; + let mut columns = before.split_whitespace(); + + let [Some(_zone), Some(class), Some(record_type), Some(_flags), Some(_protocol), Some(_algorithm), Some(encoded), None] = + array::from_fn(|_| columns.next()) + else { + return Err("expected 7 columns".into()); + }; + + if record_type != "DNSKEY" { + return Err(format!("tried to parse `{record_type}` record as a DNSKEY record").into()); + } + + if class != "IN" { + return Err(format!("unknown class: {class}").into()); + } + + // {id = 24975 (zsk), size = 1024b} + let error = "invalid comment syntax"; + let (id_expr, size_expr) = after.split_once(',').ok_or(error)?; + + // {id = 24975 (zsk) + let (id_lhs, id_rhs) = id_expr.split_once('=').ok_or(error)?; + if id_lhs.trim() != "{id" { + return Err(error.into()); + } + + // 24975 (zsk) + let (id, _key_type) = id_rhs.trim().split_once(' ').ok_or(error)?; + + // size = 1024b} + let (size_lhs, size_rhs) = size_expr.split_once('=').ok_or(error)?; + if size_lhs.trim() != "size" { + return Err(error.into()); + } + let bits = size_rhs.trim().strip_suffix("b}").ok_or(error)?.parse()?; + + Ok(Self { + bits, + encoded: encoded.to_string(), + id: id.parse()?, + }) + } +} + +const ZONES_DIR: &str = "/etc/nsd/zones"; +const ZONE_FILENAME: &str = "main.zone"; + +fn zone_file_path() -> String { + format!("{ZONES_DIR}/{ZONE_FILENAME}") +} + fn ns_count() -> usize { static COUNT: AtomicUsize = AtomicUsize::new(0); COUNT.fetch_add(1, atomic::Ordering::Relaxed) } +impl<'a> NameServer<'a, Signed> { + /// Moves the server to the "Start" state where it can answer client queries + pub fn start(self) -> Result> { + let Self { + container, + zone_file, + state: _, + } = self; + + // for PID file + container.status_ok(&["mkdir", "-p", "/run/nsd/"])?; + + container.cp("/etc/nsd/nsd.conf", &nsd_conf(&zone_file.origin))?; + + let child = container.spawn(&["nsd", "-d"])?; + + Ok(NameServer { + container, + zone_file, + state: Running { child }, + }) + } + + pub fn key_signing_key(&self) -> &Key { + &self.state.ksk + } + + pub fn zone_signing_key(&self) -> &Key { + &self.state.zsk + } + + pub fn signed_zone_file(&self) -> &str { + &self.state.signed_zone_file + } +} + impl<'a, S> NameServer<'a, S> { pub fn ipv4_addr(&self) -> Ipv4Addr { self.container.ipv4_addr() @@ -117,6 +272,12 @@ impl<'a, S> NameServer<'a, S> { pub struct Stopped; +pub struct Signed { + zsk: Key, + ksk: Key, + signed_zone_file: String, +} + pub struct Running { child: Child, } @@ -144,7 +305,7 @@ fn nsd_conf(fqdn: &FQDN) -> String { #[cfg(test)] mod tests { - use crate::client::{Client, Recurse}; + use crate::client::{Client, Dnssec, Recurse}; use crate::record::RecordType; use super::*; @@ -155,7 +316,13 @@ mod tests { let ip_addr = tld_ns.ipv4_addr(); let client = Client::new()?; - let output = client.dig(Recurse::No, ip_addr, RecordType::SOA, &FQDN::COM)?; + let output = client.dig( + Recurse::No, + Dnssec::No, + ip_addr, + RecordType::SOA, + &FQDN::COM, + )?; assert!(output.status.is_noerror()); @@ -178,10 +345,56 @@ mod tests { let ipv4_addr = root_ns.ipv4_addr(); let client = Client::new()?; - let output = client.dig(Recurse::No, ipv4_addr, RecordType::NS, &FQDN::COM)?; + let output = client.dig( + Recurse::No, + Dnssec::No, + ipv4_addr, + RecordType::NS, + &FQDN::COM, + )?; assert!(output.status.is_noerror()); Ok(()) } + + #[test] + #[ignore = "FIXME need to parse RRSIG record in dig's output"] + fn signed() -> Result<()> { + let tld_ns = NameServer::new(FQDN::ROOT)?.sign()?; + + eprintln!("KSK: {:?}", tld_ns.key_signing_key()); + eprintln!("ZSK: {:?}", tld_ns.zone_signing_key()); + eprintln!("root.zone.signed:\n{}", tld_ns.signed_zone_file()); + + let tld_ns = tld_ns.start()?; + + let ipv4_addr = tld_ns.ipv4_addr(); + + let client = Client::new()?; + let output = client.dig( + Recurse::No, + Dnssec::Yes, + ipv4_addr, + RecordType::SOA, + &FQDN::ROOT, + )?; + + assert!(output.status.is_noerror()); + + Ok(()) + } + + #[test] + fn can_parse_ldns_keygen_output() -> Result<()> { + let input = "example.com. IN DNSKEY 256 3 7 AwEAAdIpMlio4GJas7GbIZ9xRpzpB2pf4SxBJcsquN/0yNBPGNE2rzcFykqMAKmLwypk1/1q/EdHVa4tQ5RlK0w09CRhgSXfCaph+yLNJKpiPyuVcXKl2k0RnO4p835sgVEUIvx8qGTDo7c7DA9UBje+/3ViFKqVhOBaWyT6gHAmNVpb ;{id = 24975 (zsk), size = 1024b}"; + + let key: Key = input.parse()?; + + assert_eq!(1024, key.bits); + let expected = "AwEAAdIpMlio4GJas7GbIZ9xRpzpB2pf4SxBJcsquN/0yNBPGNE2rzcFykqMAKmLwypk1/1q/EdHVa4tQ5RlK0w09CRhgSXfCaph+yLNJKpiPyuVcXKl2k0RnO4p835sgVEUIvx8qGTDo7c7DA9UBje+/3ViFKqVhOBaWyT6gHAmNVpb"; + assert_eq!(expected, key.encoded); + + Ok(()) + } } diff --git a/src/recursive_resolver.rs b/src/recursive_resolver.rs index 7bd8506a..4879e321 100644 --- a/src/recursive_resolver.rs +++ b/src/recursive_resolver.rs @@ -41,7 +41,7 @@ impl Drop for RecursiveResolver { #[cfg(test)] mod tests { use crate::{ - client::{Client, Recurse}, + client::{Client, Dnssec, Recurse}, name_server::NameServer, record::RecordType, FQDN, @@ -85,7 +85,13 @@ mod tests { let resolver_ip_addr = resolver.ipv4_addr(); let client = Client::new()?; - let output = client.dig(Recurse::Yes, resolver_ip_addr, RecordType::A, &needle)?; + let output = client.dig( + Recurse::Yes, + Dnssec::No, + resolver_ip_addr, + RecordType::A, + &needle, + )?; assert!(output.status.is_noerror()); From 2bcad2a25c1027a90b179e7402a5f0afdb7c39aa Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Wed, 7 Feb 2024 14:59:15 +0100 Subject: [PATCH 032/124] parse RRSIG record & complete signed NS test --- src/client.rs | 1 + src/name_server.rs | 11 ++++- src/record.rs | 119 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 128 insertions(+), 3 deletions(-) diff --git a/src/client.rs b/src/client.rs index 0a9fb1e2..c40eb33d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -67,6 +67,7 @@ impl Recurse { } } +#[derive(Debug)] pub struct DigOutput { pub flags: DigFlags, pub status: DigStatus, diff --git a/src/name_server.rs b/src/name_server.rs index 61401158..c757cba5 100644 --- a/src/name_server.rs +++ b/src/name_server.rs @@ -359,7 +359,6 @@ mod tests { } #[test] - #[ignore = "FIXME need to parse RRSIG record in dig's output"] fn signed() -> Result<()> { let tld_ns = NameServer::new(FQDN::ROOT)?.sign()?; @@ -382,6 +381,15 @@ mod tests { assert!(output.status.is_noerror()); + let [soa, rrsig] = output + .answer + .try_into() + .expect("two records in answer section"); + + assert!(soa.is_soa()); + let rrsig = rrsig.try_into_rrsig().unwrap(); + assert_eq!(RecordType::SOA, rrsig.type_covered); + Ok(()) } @@ -392,6 +400,7 @@ mod tests { let key: Key = input.parse()?; assert_eq!(1024, key.bits); + assert_eq!(24975, key.id); let expected = "AwEAAdIpMlio4GJas7GbIZ9xRpzpB2pf4SxBJcsquN/0yNBPGNE2rzcFykqMAKmLwypk1/1q/EdHVa4tQ5RlK0w09CRhgSXfCaph+yLNJKpiPyuVcXKl2k0RnO4p835sgVEUIvx8qGTDo7c7DA9UBje+/3ViFKqVhOBaWyT6gHAmNVpb"; assert_eq!(expected, key.encoded); diff --git a/src/record.rs b/src/record.rs index cc4edbfe..0f6885ac 100644 --- a/src/record.rs +++ b/src/record.rs @@ -8,6 +8,7 @@ use std::net::Ipv4Addr; use crate::{Error, Result, FQDN}; #[allow(clippy::upper_case_acronyms)] +#[derive(Debug, PartialEq)] pub enum RecordType { A, NS, @@ -24,10 +25,26 @@ impl RecordType { } } +impl FromStr for RecordType { + type Err = Error; + + fn from_str(input: &str) -> CoreResult { + let record_type = match input { + "A" => Self::A, + "SOA" => Self::SOA, + "NS" => Self::NS, + _ => return Err(format!("unknown record type: {input}").into()), + }; + + Ok(record_type) + } +} + #[derive(Debug)] #[allow(clippy::upper_case_acronyms)] pub enum Record { A(A), + RRSIG(RRSIG), SOA(SOA), } @@ -39,6 +56,18 @@ impl Record { Err(self) } } + + pub fn try_into_rrsig(self) -> CoreResult { + if let Self::RRSIG(v) = self { + Ok(v) + } else { + Err(self) + } + } + + pub fn is_soa(&self) -> bool { + matches!(self, Self::SOA(..)) + } } impl FromStr for Record { @@ -53,6 +82,7 @@ impl FromStr for Record { let record = match record_type { "A" => Record::A(input.parse()?), "NS" => todo!(), + "RRSIG" => Record::RRSIG(input.parse()?), "SOA" => Record::SOA(input.parse()?), _ => return Err(format!("unknown record type: {record_type}").into()), }; @@ -80,8 +110,11 @@ impl FromStr for A { return Err("expected 5 columns".into()); }; - if record_type != "A" { - return Err(format!("tried to parse `{record_type}` record as an A record").into()); + let expected = "A"; + if record_type != expected { + return Err( + format!("tried to parse `{record_type}` record as an {expected} record").into(), + ); } if class != "IN" { @@ -96,6 +129,67 @@ impl FromStr for A { } } +#[allow(clippy::upper_case_acronyms)] +#[derive(Debug)] +pub struct RRSIG { + pub fqdn: FQDN<'static>, + pub ttl: u32, + pub type_covered: RecordType, + pub algorithm: u32, + pub labels: u32, + pub original_ttl: u32, + pub signature_expiration: u64, + pub signature_inception: u64, + pub key_tag: u32, + pub signer_name: FQDN<'static>, + /// base64 encoded + pub signature: String, +} + +impl FromStr for RRSIG { + type Err = Error; + + fn from_str(input: &str) -> CoreResult { + let mut columns = input.split_whitespace(); + + let [Some(fqdn), Some(ttl), Some(class), Some(record_type), Some(type_covered), Some(algorithm), Some(labels), Some(original_ttl), Some(signature_expiration), Some(signature_inception), Some(key_tag), Some(signer_name)] = + array::from_fn(|_| columns.next()) + else { + return Err("expected at least 12 columns".into()); + }; + + let expected = "RRSIG"; + if record_type != expected { + return Err( + format!("tried to parse `{record_type}` record as a {expected} record").into(), + ); + } + + if class != "IN" { + return Err(format!("unknown class: {class}").into()); + } + + let mut signature = String::new(); + for column in columns { + signature.push_str(column); + } + + Ok(Self { + fqdn: fqdn.parse()?, + ttl: ttl.parse()?, + type_covered: type_covered.parse()?, + algorithm: algorithm.parse()?, + labels: labels.parse()?, + original_ttl: original_ttl.parse()?, + signature_expiration: signature_expiration.parse()?, + signature_inception: signature_inception.parse()?, + key_tag: key_tag.parse()?, + signer_name: signer_name.parse()?, + signature, + }) + } +} + #[allow(clippy::upper_case_acronyms)] #[derive(Debug)] pub struct SOA { @@ -178,4 +272,25 @@ mod tests { Ok(()) } + + #[test] + fn can_parse_rrsig_record() -> Result<()> { + let input = ". 1800 IN RRSIG SOA 7 0 1800 20240306132701 20240207132701 11264 . wXpRU4elJPGYm2kgVVsIwGf1IkYJcQ3UE4mwmItWdxj0XWSWY07MO4Ll DMJgsE0u64Q/345Ck7+aQ904uLebwCvpFnsmkyCxk82XIAfHN9FiwzSy qoR/zZEvBONaej3vrvsqPwh8q/pvypLft9647HcFdwY0juzZsbrAaDAX 8WY="; + + let rrsig: RRSIG = input.parse()?; + + assert_eq!(FQDN::ROOT, rrsig.fqdn); + assert_eq!(1800, rrsig.ttl); + assert_eq!(RecordType::SOA, rrsig.type_covered); + assert_eq!(7, rrsig.algorithm); + assert_eq!(0, rrsig.labels); + assert_eq!(20240306132701, rrsig.signature_expiration); + assert_eq!(20240207132701, rrsig.signature_inception); + assert_eq!(11264, rrsig.key_tag); + assert_eq!(FQDN::ROOT, rrsig.signer_name); + let expected = "wXpRU4elJPGYm2kgVVsIwGf1IkYJcQ3UE4mwmItWdxj0XWSWY07MO4LlDMJgsE0u64Q/345Ck7+aQ904uLebwCvpFnsmkyCxk82XIAfHN9FiwzSyqoR/zZEvBONaej3vrvsqPwh8q/pvypLft9647HcFdwY0juzZsbrAaDAX8WY="; + assert_eq!(expected, rrsig.signature); + + Ok(()) + } } From 306ce7a32b9a4579961ef7636391ff37bde20106 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Wed, 7 Feb 2024 20:11:09 +0100 Subject: [PATCH 033/124] set up DS records and trust anchor to make DNSSEC work --- docker/unbound.Dockerfile | 2 - src/client.rs | 20 ++ src/name_server.rs | 122 +++------- src/recursive_resolver.rs | 136 +++++++++++- .../templates/unbound.conf.jinja | 3 + src/zone_file.rs | 210 +++++++++++++++++- 6 files changed, 398 insertions(+), 95 deletions(-) rename docker/files/etc/unbound/unbound.conf => src/templates/unbound.conf.jinja (71%) diff --git a/docker/unbound.Dockerfile b/docker/unbound.Dockerfile index 138d7abe..ce8e2a62 100644 --- a/docker/unbound.Dockerfile +++ b/docker/unbound.Dockerfile @@ -2,5 +2,3 @@ FROM ubuntu:22.04 RUN apt-get update && \ apt-get install -y dnsutils unbound nsd iputils-ping tshark vim ldnsutils - -COPY ./files/etc/unbound/unbound.conf /etc/unbound/unbound.conf diff --git a/src/client.rs b/src/client.rs index c40eb33d..1902ac7d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -16,6 +16,22 @@ impl Client { }) } + // 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<'_>, + ) -> Result { + self.inner.stdout(&[ + "delv", + "+mtrace", + &format!("@{server}"), + record_type.as_str(), + fqdn.as_str(), + ]) + } + pub fn dig( &self, recurse: Recurse, @@ -153,6 +169,7 @@ pub struct DigFlags { pub recursion_desired: bool, pub recursion_available: bool, pub authoritative_answer: bool, + pub authenticated_data: bool, } impl FromStr for DigFlags { @@ -163,6 +180,7 @@ impl FromStr for DigFlags { let mut recursion_desired = false; let mut recursion_available = false; let mut authoritative_answer = false; + let mut authenticated_data = false; for flag in input.split_whitespace() { match flag { @@ -170,6 +188,7 @@ impl FromStr for DigFlags { "rd" => recursion_desired = true, "ra" => recursion_available = true, "aa" => authoritative_answer = true, + "ad" => authenticated_data = true, _ => return Err(format!("unknown flag: {flag}").into()), } } @@ -179,6 +198,7 @@ impl FromStr for DigFlags { recursion_desired, recursion_available, authoritative_answer, + authenticated_data, }) } } diff --git a/src/name_server.rs b/src/name_server.rs index c757cba5..c6c8fb6e 100644 --- a/src/name_server.rs +++ b/src/name_server.rs @@ -1,12 +1,10 @@ -use core::array; -use core::str::FromStr; use core::sync::atomic::{self, AtomicUsize}; use std::net::Ipv4Addr; use std::process::Child; use crate::container::Container; -use crate::zone_file::{self, SoaSettings, ZoneFile}; -use crate::{Error, Result, FQDN}; +use crate::zone_file::{self, SoaSettings, ZoneFile, DNSKEY, DS}; +use crate::{Result, FQDN}; pub struct NameServer<'a, State> { container: Container, @@ -68,6 +66,12 @@ impl<'a> NameServer<'a, Stopped> { self } + /// Adds a DS record to the zone file + pub fn ds(&mut self, ds: DS) -> &mut Self { + self.zone_file.entry(ds); + self + } + /// Freezes and signs the name server's zone file pub fn sign(self) -> Result> { // TODO do we want to make these settings configurable? @@ -90,13 +94,13 @@ impl<'a> NameServer<'a, Stopped> { format!("cd {ZONES_DIR} && ldns-keygen -a {ALGORITHM} -b {ZSK_BITS} {zone}"); let zsk_filename = container.stdout(&["sh", "-c", &zsk_keygen])?; let zsk_path = format!("{ZONES_DIR}/{zsk_filename}.key"); - let zsk: Key = container.stdout(&["cat", &zsk_path])?.parse()?; + let zsk: DNSKEY = container.stdout(&["cat", &zsk_path])?.parse()?; let ksk_keygen = format!("cd {ZONES_DIR} && ldns-keygen -k -a {ALGORITHM} -b {KSK_BITS} {zone}"); let ksk_filename = container.stdout(&["sh", "-c", &ksk_keygen])?; let ksk_path = format!("{ZONES_DIR}/{ksk_filename}.key"); - let ksk: Key = container.stdout(&["cat", &ksk_path])?.parse()?; + let ksk: DNSKEY = container.stdout(&["cat", &ksk_path])?.parse()?; // -n = use NSEC3 instead of NSEC // -p = set the opt-out flag on all nsec3 rrs @@ -105,6 +109,11 @@ impl<'a> NameServer<'a, Stopped> { ); container.status_ok(&["sh", "-c", &signzone])?; + // TODO do we want to make the hashing algorithm configurable? + // -2 = use SHA256 for the DS hash + let key2ds = format!("cd {ZONES_DIR} && ldns-key2ds -n -2 {ZONE_FILENAME}.signed"); + let ds: DS = container.stdout(&["sh", "-c", &key2ds])?.parse()?; + // we have an in-memory representation of the zone file so we just delete the on-disk version let zone_file_path = zone_file_path(); container.status_ok(&["mv", &format!("{zone_file_path}.signed"), &zone_file_path])?; @@ -115,9 +124,10 @@ impl<'a> NameServer<'a, Stopped> { container, zone_file, state: Signed { - zsk, + ds, ksk, signed_zone_file, + zsk, }, }) } @@ -148,62 +158,6 @@ impl<'a> NameServer<'a, Stopped> { } } -#[derive(Debug)] -pub struct Key { - pub bits: u16, - pub encoded: String, - pub id: u32, -} - -impl FromStr for Key { - type Err = Error; - - fn from_str(input: &str) -> Result { - let (before, after) = input.split_once(';').ok_or("comment was not found")?; - let mut columns = before.split_whitespace(); - - let [Some(_zone), Some(class), Some(record_type), Some(_flags), Some(_protocol), Some(_algorithm), Some(encoded), None] = - array::from_fn(|_| columns.next()) - else { - return Err("expected 7 columns".into()); - }; - - if record_type != "DNSKEY" { - return Err(format!("tried to parse `{record_type}` record as a DNSKEY record").into()); - } - - if class != "IN" { - return Err(format!("unknown class: {class}").into()); - } - - // {id = 24975 (zsk), size = 1024b} - let error = "invalid comment syntax"; - let (id_expr, size_expr) = after.split_once(',').ok_or(error)?; - - // {id = 24975 (zsk) - let (id_lhs, id_rhs) = id_expr.split_once('=').ok_or(error)?; - if id_lhs.trim() != "{id" { - return Err(error.into()); - } - - // 24975 (zsk) - let (id, _key_type) = id_rhs.trim().split_once(' ').ok_or(error)?; - - // size = 1024b} - let (size_lhs, size_rhs) = size_expr.split_once('=').ok_or(error)?; - if size_lhs.trim() != "size" { - return Err(error.into()); - } - let bits = size_rhs.trim().strip_suffix("b}").ok_or(error)?.parse()?; - - Ok(Self { - bits, - encoded: encoded.to_string(), - id: id.parse()?, - }) - } -} - const ZONES_DIR: &str = "/etc/nsd/zones"; const ZONE_FILENAME: &str = "main.zone"; @@ -239,17 +193,21 @@ impl<'a> NameServer<'a, Signed> { }) } - pub fn key_signing_key(&self) -> &Key { + pub fn key_signing_key(&self) -> &DNSKEY { &self.state.ksk } - pub fn zone_signing_key(&self) -> &Key { + pub fn zone_signing_key(&self) -> &DNSKEY { &self.state.zsk } pub fn signed_zone_file(&self) -> &str { &self.state.signed_zone_file } + + pub fn ds(&self) -> &DS { + &self.state.ds + } } impl<'a, S> NameServer<'a, S> { @@ -257,6 +215,7 @@ impl<'a, S> NameServer<'a, S> { self.container.ipv4_addr() } + /// Zone file BEFORE signing pub fn zone_file(&self) -> &ZoneFile<'a> { &self.zone_file } @@ -273,8 +232,9 @@ impl<'a, S> NameServer<'a, S> { pub struct Stopped; pub struct Signed { - zsk: Key, - ksk: Key, + ds: DS, + zsk: DNSKEY, + ksk: DNSKEY, signed_zone_file: String, } @@ -360,21 +320,21 @@ mod tests { #[test] fn signed() -> Result<()> { - let tld_ns = NameServer::new(FQDN::ROOT)?.sign()?; + let ns = NameServer::new(FQDN::ROOT)?.sign()?; - eprintln!("KSK: {:?}", tld_ns.key_signing_key()); - eprintln!("ZSK: {:?}", tld_ns.zone_signing_key()); - eprintln!("root.zone.signed:\n{}", tld_ns.signed_zone_file()); + eprintln!("KSK:\n{}", ns.key_signing_key()); + eprintln!("ZSK:\n{}", ns.zone_signing_key()); + eprintln!("root.zone.signed:\n{}", ns.signed_zone_file()); - let tld_ns = tld_ns.start()?; + let tld_ns = ns.start()?; - let ipv4_addr = tld_ns.ipv4_addr(); + let ns_addr = tld_ns.ipv4_addr(); let client = Client::new()?; let output = client.dig( Recurse::No, Dnssec::Yes, - ipv4_addr, + ns_addr, RecordType::SOA, &FQDN::ROOT, )?; @@ -392,18 +352,4 @@ mod tests { Ok(()) } - - #[test] - fn can_parse_ldns_keygen_output() -> Result<()> { - let input = "example.com. IN DNSKEY 256 3 7 AwEAAdIpMlio4GJas7GbIZ9xRpzpB2pf4SxBJcsquN/0yNBPGNE2rzcFykqMAKmLwypk1/1q/EdHVa4tQ5RlK0w09CRhgSXfCaph+yLNJKpiPyuVcXKl2k0RnO4p835sgVEUIvx8qGTDo7c7DA9UBje+/3ViFKqVhOBaWyT6gHAmNVpb ;{id = 24975 (zsk), size = 1024b}"; - - let key: Key = input.parse()?; - - assert_eq!(1024, key.bits); - assert_eq!(24975, key.id); - let expected = "AwEAAdIpMlio4GJas7GbIZ9xRpzpB2pf4SxBJcsquN/0yNBPGNE2rzcFykqMAKmLwypk1/1q/EdHVa4tQ5RlK0w09CRhgSXfCaph+yLNJKpiPyuVcXKl2k0RnO4p835sgVEUIvx8qGTDo7c7DA9UBje+/3ViFKqVhOBaWyT6gHAmNVpb"; - assert_eq!(expected, key.encoded); - - Ok(()) - } } diff --git a/src/recursive_resolver.rs b/src/recursive_resolver.rs index 4879e321..5e369e69 100644 --- a/src/recursive_resolver.rs +++ b/src/recursive_resolver.rs @@ -3,7 +3,7 @@ use std::net::Ipv4Addr; use std::process::Child; use crate::container::Container; -use crate::zone_file::Root; +use crate::zone_file::{Root, DNSKEY}; use crate::Result; pub struct RecursiveResolver { @@ -12,7 +12,9 @@ pub struct RecursiveResolver { } impl RecursiveResolver { - pub fn start(roots: &[Root]) -> Result { + pub fn start(roots: &[Root], trust_anchors: &[DNSKEY]) -> Result { + const TRUST_ANCHOR_FILE: &str = "/etc/trusted-key.key"; + let container = Container::run()?; let mut hints = String::new(); @@ -22,6 +24,18 @@ impl RecursiveResolver { 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 }) @@ -38,8 +52,13 @@ impl Drop for RecursiveResolver { } } +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, @@ -81,7 +100,7 @@ mod tests { 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 = RecursiveResolver::start(roots, &[])?; let resolver_ip_addr = resolver.ipv4_addr(); let client = Client::new()?; @@ -103,4 +122,115 @@ mod tests { 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(()) + } } diff --git a/docker/files/etc/unbound/unbound.conf b/src/templates/unbound.conf.jinja similarity index 71% rename from docker/files/etc/unbound/unbound.conf rename to src/templates/unbound.conf.jinja index ad446203..fe74a6cf 100644 --- a/docker/files/etc/unbound/unbound.conf +++ b/src/templates/unbound.conf.jinja @@ -4,6 +4,9 @@ server: interface: 0.0.0.0 access-control: 172.17.0.0/16 allow root-hints: /etc/unbound/root.hints +{% if use_dnssec %} + trust-anchor-file: /etc/trusted-key.key +{% endif %} remote-control: control-enable: no diff --git a/src/zone_file.rs b/src/zone_file.rs index 524f26db..71a7b9c6 100644 --- a/src/zone_file.rs +++ b/src/zone_file.rs @@ -4,10 +4,11 @@ //! - the `@` syntax is not used to avoid relying on the order of the entries //! - relative domain names are not used; all domain names must be in fully-qualified form -use core::fmt; +use core::{array, fmt}; use std::net::Ipv4Addr; +use std::str::FromStr; -use crate::FQDN; +use crate::{Error, FQDN}; pub struct ZoneFile<'a> { pub origin: FQDN<'a>, @@ -94,9 +95,17 @@ impl fmt::Display for Root<'_> { pub enum Entry<'a> { A(A<'a>), + DNSKEY(DNSKEY), + DS(DS), NS(NS<'a>), } +impl<'a> From for Entry<'a> { + fn from(v: DS) -> Self { + Self::DS(v) + } +} + impl<'a> From> for Entry<'a> { fn from(v: A<'a>) -> Self { Self::A(v) @@ -113,6 +122,8 @@ impl fmt::Display for Entry<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Entry::A(a) => a.fmt(f), + Entry::DNSKEY(dnskey) => dnskey.fmt(f), + Entry::DS(ds) => ds.fmt(f), Entry::NS(ns) => ns.fmt(f), } } @@ -132,6 +143,166 @@ impl fmt::Display for A<'_> { } } +// integer types chosen based on bit sizes in section 2.1 of RFC4034 +#[derive(Clone, Debug)] +pub struct DNSKEY { + zone: FQDN<'static>, + flags: u16, + protocol: u8, + algorithm: u8, + public_key: String, + + // extra information in `+multiline` format and `ldns-keygen`'s output + bits: u16, + key_tag: u16, +} + +impl DNSKEY { + pub fn bits(&self) -> u16 { + self.bits + } + + pub fn key_tag(&self) -> u16 { + self.key_tag + } +} + +impl FromStr for DNSKEY { + type Err = Error; + + fn from_str(input: &str) -> Result { + let (before, after) = input.split_once(';').ok_or("comment was not found")?; + let mut columns = before.split_whitespace(); + + let [Some(zone), Some(class), Some(record_type), Some(flags), Some(protocol), Some(algorithm), Some(public_key), None] = + array::from_fn(|_| columns.next()) + else { + return Err("expected 7 columns".into()); + }; + + if record_type != "DNSKEY" { + return Err(format!("tried to parse `{record_type}` record as a DNSKEY record").into()); + } + + if class != "IN" { + return Err(format!("unknown class: {class}").into()); + } + + // {id = 24975 (zsk), size = 1024b} + let error = "invalid comment syntax"; + let (id_expr, size_expr) = after.split_once(',').ok_or(error)?; + + // {id = 24975 (zsk) + let (id_lhs, id_rhs) = id_expr.split_once('=').ok_or(error)?; + if id_lhs.trim() != "{id" { + return Err(error.into()); + } + + // 24975 (zsk) + let (key_tag, _key_type) = id_rhs.trim().split_once(' ').ok_or(error)?; + + // size = 1024b} + let (size_lhs, size_rhs) = size_expr.split_once('=').ok_or(error)?; + if size_lhs.trim() != "size" { + return Err(error.into()); + } + let bits = size_rhs.trim().strip_suffix("b}").ok_or(error)?.parse()?; + + Ok(Self { + zone: zone.parse()?, + flags: flags.parse()?, + protocol: protocol.parse()?, + algorithm: algorithm.parse()?, + public_key: public_key.to_string(), + + key_tag: key_tag.parse()?, + bits, + }) + } +} + +impl fmt::Display for DNSKEY { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + zone, + flags, + protocol, + algorithm, + public_key, + bits: _, + key_tag: _, + } = self; + + write!( + f, + "{zone}\tIN\tDNSKEY\t{flags}\t{protocol}\t{algorithm}\t{public_key}" + ) + } +} + +#[derive(Clone)] +pub struct DS { + zone: FQDN<'static>, + _ttl: u32, + key_tag: u16, + algorithm: u8, + digest_type: u8, + digest: String, +} + +impl FromStr for DS { + type Err = Error; + + fn from_str(input: &str) -> Result { + let mut columns = input.split_whitespace(); + + let [Some(zone), Some(ttl), Some(class), Some(record_type), Some(key_tag), Some(algorithm), Some(digest_type), Some(digest), None] = + array::from_fn(|_| columns.next()) + else { + return Err("expected 8 columns".into()); + }; + + let expected = "DS"; + if record_type != expected { + return Err( + format!("tried to parse `{record_type}` entry as a {expected} entry").into(), + ); + } + + if class != "IN" { + return Err(format!("unknown class: {class}").into()); + } + + Ok(Self { + zone: zone.parse()?, + _ttl: ttl.parse()?, + key_tag: key_tag.parse()?, + algorithm: algorithm.parse()?, + digest_type: digest_type.parse()?, + digest: digest.to_string(), + }) + } +} + +/// NOTE does NOT include the TTL field +impl fmt::Display for DS { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + zone, + _ttl, + key_tag, + algorithm, + digest_type, + digest, + } = self; + + write!( + f, + "{zone}\tIN\tDS\t{key_tag}\t{algorithm}\t{digest_type}\t{digest}" + ) + } +} + pub struct NS<'a> { pub zone: FQDN<'a>, pub nameserver: FQDN<'a>, @@ -262,6 +433,41 @@ e.gtld-servers.net. IN A 192.12.94.30 Ok(()) } + // not quite roundtrip because we drop the TTL field when doing `to_string` + #[test] + fn ds_roundtrip() -> Result<()> { + let input = + ". 1800 IN DS 31153 7 2 7846338aaacde9cc9518f1f450082adc015a207c45a1e69d6e660e6836f4ef3b"; + let ds: DS = input.parse()?; + let output = ds.to_string(); + + let expected = + ". IN DS 31153 7 2 7846338aaacde9cc9518f1f450082adc015a207c45a1e69d6e660e6836f4ef3b"; + assert_eq!(expected, output); + + Ok(()) + } + + #[test] + fn dnskey_roundtrip() -> Result<()> { + let input = "example.com. IN DNSKEY 256 3 7 AwEAAdIpMlio4GJas7GbIZ9xRpzpB2pf4SxBJcsquN/0yNBPGNE2rzcFykqMAKmLwypk1/1q/EdHVa4tQ5RlK0w09CRhgSXfCaph+yLNJKpiPyuVcXKl2k0RnO4p835sgVEUIvx8qGTDo7c7DA9UBje+/3ViFKqVhOBaWyT6gHAmNVpb ;{id = 24975 (zsk), size = 1024b}"; + + let dnskey: DNSKEY = input.parse()?; + + assert_eq!(256, dnskey.flags); + assert_eq!(3, dnskey.protocol); + assert_eq!(7, dnskey.algorithm); + let expected = "AwEAAdIpMlio4GJas7GbIZ9xRpzpB2pf4SxBJcsquN/0yNBPGNE2rzcFykqMAKmLwypk1/1q/EdHVa4tQ5RlK0w09CRhgSXfCaph+yLNJKpiPyuVcXKl2k0RnO4p835sgVEUIvx8qGTDo7c7DA9UBje+/3ViFKqVhOBaWyT6gHAmNVpb"; + assert_eq!(expected, dnskey.public_key); + assert_eq!(1024, dnskey.bits()); + assert_eq!(24975, dnskey.key_tag()); + + let output = dnskey.to_string(); + assert!(input.starts_with(&output)); + + Ok(()) + } + fn example_a() -> Result> { Ok(A { fqdn: FQDN("e.gtld-servers.net.")?, From c50568c70921faa732d147b489750c3e2d10265f Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 8 Feb 2024 15:09:03 +0100 Subject: [PATCH 034/124] enable CI --- .github/workflows/ci.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..ea123563 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI + +on: + pull_request: + branches: [main] + merge_group: + +jobs: + ci: + name: Continuous Integration + runs-on: ubuntu-latest + + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Install ${{ matrix.rust }} toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + components: clippy, rustfmt + + - name: Run tests + run: cargo t + + - name: Check that code is formatted + run: cargo fmt -- --check + + - name: Lint code + run: cargo clippy -- -D warnings From 02f5307056823caa6d10fee9806b385edeb9229d Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 8 Feb 2024 15:14:54 +0100 Subject: [PATCH 035/124] make `docker build` less noisy --- src/container.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/container.rs b/src/container.rs index 6690bed5..1e827568 100644 --- a/src/container.rs +++ b/src/container.rs @@ -43,8 +43,13 @@ impl Container { .arg(docker_dir_path); ONCE.call_once(|| { - let status = command.status().unwrap(); - assert!(status.success()); + 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"); From 3e78cfa30ec32d3c87353640f08a454a86aa08cf Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 8 Feb 2024 15:30:32 +0100 Subject: [PATCH 036/124] ensure child process does not outlive its container --- src/container.rs | 61 ++++++++++++++++++++++++++++----------- src/name_server.rs | 15 +++------- src/recursive_resolver.rs | 16 ++++------ 3 files changed, 54 insertions(+), 38 deletions(-) diff --git a/src/container.rs b/src/container.rs index 1e827568..a27e2df5 100644 --- a/src/container.rs +++ b/src/container.rs @@ -2,20 +2,17 @@ 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, Once}; use tempfile::NamedTempFile; use crate::{Error, Result}; pub struct Container { - name: String, - id: String, - // TODO probably also want the IPv6 address - ipv4_addr: Ipv4Addr, + inner: Arc, } impl Container { @@ -67,10 +64,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), }) } @@ -81,7 +81,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]); @@ -96,7 +96,7 @@ impl Container { pub fn output(&self, command_and_args: &[&str]) -> Result { 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() @@ -110,7 +110,7 @@ impl Container { if output.status.success() { Ok(output.stdout) } else { - Err(format!("[{}] `{command_and_args:?}` failed", self.name).into()) + Err(format!("[{}] `{command_and_args:?}` failed", self.inner.name).into()) } } @@ -118,7 +118,7 @@ impl Container { pub fn status(&self, command_and_args: &[&str]) -> Result { 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()?) @@ -131,19 +131,46 @@ 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 { let mut command = Command::new("docker"); - command.args(["exec", "-t", &self.id]).args(cmd); + command.args(["exec", "-t", &self.inner.id]).args(cmd); - Ok(command.spawn()?) + let inner = command.spawn()?; + Ok(Child { + inner, + _container: self.inner.clone(), + }) } pub fn ipv4_addr(&self) -> Ipv4Addr { - self.ipv4_addr + self.inner.ipv4_addr + } +} + +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: process::Child, + _container: Arc, +} + +impl Drop for Child { + fn drop(&mut self) { + let _ = self.inner.kill(); } } @@ -205,8 +232,8 @@ fn get_ipv4_addr(container_id: &str) -> Result { 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 diff --git a/src/name_server.rs b/src/name_server.rs index c6c8fb6e..cb56a620 100644 --- a/src/name_server.rs +++ b/src/name_server.rs @@ -1,8 +1,7 @@ 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}; @@ -153,7 +152,7 @@ impl<'a> NameServer<'a, Stopped> { Ok(NameServer { container, zone_file, - state: Running { child }, + state: Running { _child: child }, }) } } @@ -189,7 +188,7 @@ impl<'a> NameServer<'a, Signed> { Ok(NameServer { container, zone_file, - state: Running { child }, + state: Running { _child: child }, }) } @@ -239,13 +238,7 @@ pub struct Signed { } pub struct Running { - child: Child, -} - -impl Drop for Running { - fn drop(&mut self) { - let _ = self.child.kill(); - } + _child: Child, } fn primary_ns(ns_count: usize) -> FQDN<'static> { diff --git a/src/recursive_resolver.rs b/src/recursive_resolver.rs index 5e369e69..2b282973 100644 --- a/src/recursive_resolver.rs +++ b/src/recursive_resolver.rs @@ -1,14 +1,13 @@ use core::fmt::Write; use std::net::Ipv4Addr; -use std::process::Child; -use crate::container::Container; +use crate::container::{Child, Container}; use crate::zone_file::{Root, DNSKEY}; use crate::Result; pub struct RecursiveResolver { container: Container, - child: Child, + _child: Child, } impl RecursiveResolver { @@ -38,7 +37,10 @@ impl RecursiveResolver { let child = container.spawn(&["unbound", "-d"])?; - Ok(Self { child, container }) + Ok(Self { + _child: child, + container, + }) } pub fn ipv4_addr(&self) -> Ipv4Addr { @@ -46,12 +48,6 @@ impl RecursiveResolver { } } -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) } From 095b68b88790e1bcace886ad7decdd653a84f694 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 8 Feb 2024 17:54:35 +0100 Subject: [PATCH 037/124] add API to gracefully terminate name server & resolver --- src/container.rs | 16 ++++++++++++--- src/name_server.rs | 41 ++++++++++++++++++++++++++++++++++++--- src/recursive_resolver.rs | 39 ++++++++++++++++++++++++++++++++----- 3 files changed, 85 insertions(+), 11 deletions(-) diff --git a/src/container.rs b/src/container.rs index a27e2df5..13a4164e 100644 --- a/src/container.rs +++ b/src/container.rs @@ -137,11 +137,12 @@ impl Container { pub fn spawn(&self, cmd: &[&str]) -> Result { let mut command = Command::new("docker"); + command.stdout(Stdio::piped()).stderr(Stdio::piped()); command.args(["exec", "-t", &self.inner.id]).args(cmd); let inner = command.spawn()?; Ok(Child { - inner, + inner: Some(inner), _container: self.inner.clone(), }) } @@ -164,13 +165,22 @@ struct Inner { // runs inside of, to prevent the scenario of the container being destroyed _before_ // the child is killed pub struct Child { - inner: process::Child, + inner: Option, _container: Arc, } +impl Child { + pub fn wait(mut self) -> Result { + let output = self.inner.take().expect("unreachable").wait_with_output()?; + output.try_into() + } +} + impl Drop for Child { fn drop(&mut self) { - let _ = self.inner.kill(); + if let Some(mut inner) = self.inner.take() { + let _ = inner.kill(); + } } } diff --git a/src/name_server.rs b/src/name_server.rs index cb56a620..97c45967 100644 --- a/src/name_server.rs +++ b/src/name_server.rs @@ -152,7 +152,7 @@ impl<'a> NameServer<'a, Stopped> { Ok(NameServer { container, zone_file, - state: Running { _child: child }, + state: Running { child }, }) } } @@ -188,7 +188,7 @@ impl<'a> NameServer<'a, Signed> { Ok(NameServer { container, zone_file, - state: Running { _child: child }, + state: Running { child }, }) } @@ -209,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 { + 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() @@ -238,7 +263,7 @@ pub struct Signed { } pub struct Running { - _child: Child, + child: Child, } fn primary_ns(ns_count: usize) -> FQDN<'static> { @@ -345,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(()) + } } diff --git a/src/recursive_resolver.rs b/src/recursive_resolver.rs index 2b282973..6713dcb9 100644 --- a/src/recursive_resolver.rs +++ b/src/recursive_resolver.rs @@ -7,7 +7,7 @@ use crate::Result; pub struct RecursiveResolver { container: Container, - _child: Child, + child: Child, } impl RecursiveResolver { @@ -37,15 +37,33 @@ impl RecursiveResolver { let child = container.spawn(&["unbound", "-d"])?; - Ok(Self { - _child: child, - container, - }) + 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 { + 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 { @@ -229,4 +247,15 @@ mod tests { Ok(()) } + + #[test] + fn terminate_works() -> Result<()> { + let resolver = RecursiveResolver::start(&[], &[])?; + let logs = resolver.terminate()?; + + eprintln!("{logs}"); + assert!(logs.contains("start of service")); + + Ok(()) + } } From 1e5aac55e74e861ed099ce266fc457b25e4a77a8 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 8 Feb 2024 18:10:49 +0100 Subject: [PATCH 038/124] bake Dockerfile into the crate to avoid the problem of changes in the `docker` directory affecting the outcomes of tests. IOW, so that all downstream uses of the crate use the same docker image --- docker/unbound.Dockerfile | 4 ---- src/container.rs | 35 +++++++++++++++++++---------------- src/docker/unbound.Dockerfile | 13 +++++++++++++ 3 files changed, 32 insertions(+), 20 deletions(-) delete mode 100644 docker/unbound.Dockerfile create mode 100644 src/docker/unbound.Dockerfile diff --git a/docker/unbound.Dockerfile b/docker/unbound.Dockerfile deleted file mode 100644 index ce8e2a62..00000000 --- a/docker/unbound.Dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -FROM ubuntu:22.04 - -RUN apt-get update && \ - apt-get install -y dnsutils unbound nsd iputils-ping tshark vim ldnsutils diff --git a/src/container.rs b/src/container.rs index 13a4164e..50bd12ff 100644 --- a/src/container.rs +++ b/src/container.rs @@ -1,13 +1,12 @@ use core::str; use std::fs; use std::net::Ipv4Addr; -use std::path::Path; use std::process::{self, ExitStatus}; use std::process::{Command, Stdio}; use std::sync::atomic::AtomicUsize; use std::sync::{atomic, Arc, Once}; -use tempfile::NamedTempFile; +use tempfile::{NamedTempFile, TempDir}; use crate::{Error, Result}; @@ -15,29 +14,27 @@ pub struct Container { inner: Arc, } +const PACKAGE_NAME: &str = env!("CARGO_PKG_NAME"); + impl Container { /// Starts the container in a "parked" state pub fn run() -> Result { static ONCE: Once = Once::new(); - static COUNT: AtomicUsize = AtomicUsize::new(0); - // TODO configurable: hickory; bind - let binary = "unbound"; - let image_tag = format!("dnssec-tests-{binary}"); + // TODO make this configurable and support hickory & bind + let implementation = "unbound"; + let dockerfile = include_str!("docker/unbound.Dockerfile"); + let docker_build_dir = TempDir::new()?; + let docker_build_dir = docker_build_dir.path(); + fs::write(docker_build_dir.join("Dockerfile"), dockerfile)?; - 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 output = command.output().unwrap(); @@ -51,8 +48,8 @@ impl Container { 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") @@ -152,6 +149,12 @@ impl Container { } } +fn container_count() -> usize { + static COUNT: AtomicUsize = AtomicUsize::new(0); + + COUNT.fetch_add(1, atomic::Ordering::Relaxed) +} + struct Inner { name: String, id: String, diff --git a/src/docker/unbound.Dockerfile b/src/docker/unbound.Dockerfile new file mode 100644 index 00000000..234edf49 --- /dev/null +++ b/src/docker/unbound.Dockerfile @@ -0,0 +1,13 @@ +FROM ubuntu:22.04 + +# 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 From 1c2d9ec4dc8c6d7e423e42d252838c73b963c3fc Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 8 Feb 2024 18:15:41 +0100 Subject: [PATCH 039/124] restructure into a workspace --- Cargo.lock | 2 +- Cargo.toml | 15 +++------------ packages/dns-test/Cargo.toml | 13 +++++++++++++ {src => packages/dns-test/src}/client.rs | 0 {src => packages/dns-test/src}/container.rs | 0 .../dns-test/src}/docker/unbound.Dockerfile | 0 {src => packages/dns-test/src}/fqdn.rs | 0 {src => packages/dns-test/src}/lib.rs | 2 ++ {src => packages/dns-test/src}/name_server.rs | 0 {src => packages/dns-test/src}/record.rs | 0 .../dns-test/src}/recursive_resolver.rs | 0 .../dns-test/src}/templates/nsd.conf.jinja | 0 .../dns-test/src}/templates/unbound.conf.jinja | 0 {src => packages/dns-test/src}/zone_file.rs | 0 14 files changed, 19 insertions(+), 13 deletions(-) create mode 100644 packages/dns-test/Cargo.toml rename {src => packages/dns-test/src}/client.rs (100%) rename {src => packages/dns-test/src}/container.rs (100%) rename {src => packages/dns-test/src}/docker/unbound.Dockerfile (100%) rename {src => packages/dns-test/src}/fqdn.rs (100%) rename {src => packages/dns-test/src}/lib.rs (88%) rename {src => packages/dns-test/src}/name_server.rs (100%) rename {src => packages/dns-test/src}/record.rs (100%) rename {src => packages/dns-test/src}/recursive_resolver.rs (100%) rename {src => packages/dns-test/src}/templates/nsd.conf.jinja (100%) rename {src => packages/dns-test/src}/templates/unbound.conf.jinja (100%) rename {src => packages/dns-test/src}/zone_file.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 9b6bb82d..c3039e0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,7 +21,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "dnssec-tests" +name = "dns-test" version = "0.1.0" dependencies = [ "minijinja", diff --git a/Cargo.toml b/Cargo.toml index 72d5349d..225effa9 100644 --- a/Cargo.toml +++ b/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" diff --git a/packages/dns-test/Cargo.toml b/packages/dns-test/Cargo.toml new file mode 100644 index 00000000..a9c5e601 --- /dev/null +++ b/packages/dns-test/Cargo.toml @@ -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 diff --git a/src/client.rs b/packages/dns-test/src/client.rs similarity index 100% rename from src/client.rs rename to packages/dns-test/src/client.rs diff --git a/src/container.rs b/packages/dns-test/src/container.rs similarity index 100% rename from src/container.rs rename to packages/dns-test/src/container.rs diff --git a/src/docker/unbound.Dockerfile b/packages/dns-test/src/docker/unbound.Dockerfile similarity index 100% rename from src/docker/unbound.Dockerfile rename to packages/dns-test/src/docker/unbound.Dockerfile diff --git a/src/fqdn.rs b/packages/dns-test/src/fqdn.rs similarity index 100% rename from src/fqdn.rs rename to packages/dns-test/src/fqdn.rs diff --git a/src/lib.rs b/packages/dns-test/src/lib.rs similarity index 88% rename from src/lib.rs rename to packages/dns-test/src/lib.rs index 60f55948..05fa9d41 100644 --- a/src/lib.rs +++ b/packages/dns-test/src/lib.rs @@ -1,3 +1,5 @@ +//! A test framework for all things DNS + pub use crate::fqdn::FQDN; pub use crate::recursive_resolver::RecursiveResolver; diff --git a/src/name_server.rs b/packages/dns-test/src/name_server.rs similarity index 100% rename from src/name_server.rs rename to packages/dns-test/src/name_server.rs diff --git a/src/record.rs b/packages/dns-test/src/record.rs similarity index 100% rename from src/record.rs rename to packages/dns-test/src/record.rs diff --git a/src/recursive_resolver.rs b/packages/dns-test/src/recursive_resolver.rs similarity index 100% rename from src/recursive_resolver.rs rename to packages/dns-test/src/recursive_resolver.rs diff --git a/src/templates/nsd.conf.jinja b/packages/dns-test/src/templates/nsd.conf.jinja similarity index 100% rename from src/templates/nsd.conf.jinja rename to packages/dns-test/src/templates/nsd.conf.jinja diff --git a/src/templates/unbound.conf.jinja b/packages/dns-test/src/templates/unbound.conf.jinja similarity index 100% rename from src/templates/unbound.conf.jinja rename to packages/dns-test/src/templates/unbound.conf.jinja diff --git a/src/zone_file.rs b/packages/dns-test/src/zone_file.rs similarity index 100% rename from src/zone_file.rs rename to packages/dns-test/src/zone_file.rs From edd6eebe1a6b9d4c91e4c62e082ca12567e69d78 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 8 Feb 2024 18:28:05 +0100 Subject: [PATCH 040/124] mv tests into conformance-tests package --- Cargo.lock | 7 + packages/conformance-tests/Cargo.toml | 11 ++ packages/conformance-tests/src/lib.rs | 3 + packages/conformance-tests/src/resolver.rs | 4 + .../conformance-tests/src/resolver/dns.rs | 3 + .../src/resolver/dns/scenarios.rs | 62 +++++++ .../conformance-tests/src/resolver/dnssec.rs | 3 + .../src/resolver/dnssec/scenarios.rs | 118 ++++++++++++ packages/dns-test/src/recursive_resolver.rs | 174 ------------------ 9 files changed, 211 insertions(+), 174 deletions(-) create mode 100644 packages/conformance-tests/Cargo.toml create mode 100644 packages/conformance-tests/src/lib.rs create mode 100644 packages/conformance-tests/src/resolver.rs create mode 100644 packages/conformance-tests/src/resolver/dns.rs create mode 100644 packages/conformance-tests/src/resolver/dns/scenarios.rs create mode 100644 packages/conformance-tests/src/resolver/dnssec.rs create mode 100644 packages/conformance-tests/src/resolver/dnssec/scenarios.rs diff --git a/Cargo.lock b/Cargo.lock index c3039e0c..46f2b24b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,13 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "conformance-tests" +version = "0.1.0" +dependencies = [ + "dns-test", +] + [[package]] name = "dns-test" version = "0.1.0" diff --git a/packages/conformance-tests/Cargo.toml b/packages/conformance-tests/Cargo.toml new file mode 100644 index 00000000..548b378d --- /dev/null +++ b/packages/conformance-tests/Cargo.toml @@ -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 diff --git a/packages/conformance-tests/src/lib.rs b/packages/conformance-tests/src/lib.rs new file mode 100644 index 00000000..dd939657 --- /dev/null +++ b/packages/conformance-tests/src/lib.rs @@ -0,0 +1,3 @@ +#![cfg(test)] + +mod resolver; diff --git a/packages/conformance-tests/src/resolver.rs b/packages/conformance-tests/src/resolver.rs new file mode 100644 index 00000000..85e4d364 --- /dev/null +++ b/packages/conformance-tests/src/resolver.rs @@ -0,0 +1,4 @@ +//! Recursive resolver role + +mod dns; +mod dnssec; diff --git a/packages/conformance-tests/src/resolver/dns.rs b/packages/conformance-tests/src/resolver/dns.rs new file mode 100644 index 00000000..4902447e --- /dev/null +++ b/packages/conformance-tests/src/resolver/dns.rs @@ -0,0 +1,3 @@ +//! plain DNS functionality + +mod scenarios; diff --git a/packages/conformance-tests/src/resolver/dns/scenarios.rs b/packages/conformance-tests/src/resolver/dns/scenarios.rs new file mode 100644 index 00000000..d76e9aee --- /dev/null +++ b/packages/conformance-tests/src/resolver/dns/scenarios.rs @@ -0,0 +1,62 @@ +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::{RecursiveResolver, Result, 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 = 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_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(()) +} diff --git a/packages/conformance-tests/src/resolver/dnssec.rs b/packages/conformance-tests/src/resolver/dnssec.rs new file mode 100644 index 00000000..63400356 --- /dev/null +++ b/packages/conformance-tests/src/resolver/dnssec.rs @@ -0,0 +1,3 @@ +//! DNSSEC functionality + +mod scenarios; diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs new file mode 100644 index 00000000..62edb1f7 --- /dev/null +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs @@ -0,0 +1,118 @@ +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::{RecursiveResolver, Result, FQDN}; + +// 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(()) +} diff --git a/packages/dns-test/src/recursive_resolver.rs b/packages/dns-test/src/recursive_resolver.rs index 6713dcb9..8c318cd4 100644 --- a/packages/dns-test/src/recursive_resolver.rs +++ b/packages/dns-test/src/recursive_resolver.rs @@ -72,182 +72,8 @@ fn unbound_conf(use_dnssec: bool) -> String { #[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(()) - } - #[test] fn terminate_works() -> Result<()> { let resolver = RecursiveResolver::start(&[], &[])?; From 5c53ba0899ff451bd87608254ea238656e9e4fb8 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 8 Feb 2024 19:23:50 +0100 Subject: [PATCH 041/124] make `Client::delv` work & use it in dnssec tests --- .../src/resolver/dns/scenarios.rs | 4 +- .../src/resolver/dnssec/scenarios.rs | 27 ++++++---- packages/dns-test/src/client.rs | 17 +++++-- packages/dns-test/src/container.rs | 12 +++-- packages/dns-test/src/lib.rs | 2 + packages/dns-test/src/recursive_resolver.rs | 16 +++--- packages/dns-test/src/trust_anchor.rs | 51 +++++++++++++++++++ packages/dns-test/src/zone_file.rs | 14 +++++ 8 files changed, 114 insertions(+), 29 deletions(-) create mode 100644 packages/dns-test/src/trust_anchor.rs diff --git a/packages/conformance-tests/src/resolver/dns/scenarios.rs b/packages/conformance-tests/src/resolver/dns/scenarios.rs index d76e9aee..e229d1b8 100644 --- a/packages/conformance-tests/src/resolver/dns/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dns/scenarios.rs @@ -4,7 +4,7 @@ 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::{RecursiveResolver, Result, FQDN}; +use dns_test::{RecursiveResolver, Result, TrustAnchor, FQDN}; #[test] fn can_resolve() -> Result<()> { @@ -38,7 +38,7 @@ fn can_resolve() -> Result<()> { 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 = RecursiveResolver::start(roots, &TrustAnchor::empty())?; let resolver_ip_addr = resolver.ipv4_addr(); let client = Client::new()?; diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs index 62edb1f7..2a9d0846 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs @@ -4,7 +4,7 @@ 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::{RecursiveResolver, Result, FQDN}; +use dns_test::{RecursiveResolver, Result, TrustAnchor, FQDN}; // no DS records are involved; this is a single-link chain of trust #[test] @@ -24,7 +24,7 @@ fn can_validate_without_delegation() -> Result<()> { let roots = &[Root::new(ns.fqdn().clone(), ns.ipv4_addr())]; - let trust_anchor = [root_ksk.clone(), root_zsk.clone()]; + let trust_anchor = TrustAnchor::from_iter([root_ksk.clone(), root_zsk.clone()]); let resolver = RecursiveResolver::start(roots, &trust_anchor)?; let resolver_addr = resolver.ipv4_addr(); @@ -40,13 +40,16 @@ fn can_validate_without_delegation() -> Result<()> { 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(()) } #[test] fn can_validate_with_delegation() -> Result<()> { let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4); - let needle = FQDN("example.nameservers.com.")?; + let needle_fqdn = FQDN("example.nameservers.com.")?; let mut root_ns = NameServer::new(FQDN::ROOT)?; let mut com_ns = NameServer::new(FQDN::COM)?; @@ -55,7 +58,7 @@ fn can_validate_with_delegation() -> Result<()> { 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); + .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()?; @@ -90,20 +93,19 @@ fn can_validate_with_delegation() -> Result<()> { 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 trust_anchor = TrustAnchor::from_iter([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_ip_addr, + resolver_addr, RecordType::A, - &needle, + &needle_fqdn, )?; - drop(resolver); - assert!(output.status.is_noerror()); assert!(output.flags.authenticated_data); @@ -111,8 +113,11 @@ fn can_validate_with_delegation() -> Result<()> { let [a, _rrsig] = output.answer.try_into().unwrap(); let a = a.try_into_a().unwrap(); - assert_eq!(needle, a.fqdn); + 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(()) } diff --git a/packages/dns-test/src/client.rs b/packages/dns-test/src/client.rs index 1902ac7d..d7b90144 100644 --- a/packages/dns-test/src/client.rs +++ b/packages/dns-test/src/client.rs @@ -3,6 +3,7 @@ use std::net::Ipv4Addr; use crate::container::Container; use crate::record::{Record, RecordType}; +use crate::trust_anchor::TrustAnchor; use crate::{Error, Result, FQDN}; pub struct Client { @@ -16,19 +17,29 @@ impl Client { }) } - // 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 { + 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(), ]) } diff --git a/packages/dns-test/src/container.rs b/packages/dns-test/src/container.rs index 50bd12ff..545c7385 100644 --- a/packages/dns-test/src/container.rs +++ b/packages/dns-test/src/container.rs @@ -102,11 +102,17 @@ 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 { - 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 { + eprintln!("STDOUT:\n{stdout}\nSTDERR:\n{stderr}"); + Err(format!("[{}] `{command_and_args:?}` failed", self.inner.name).into()) } } diff --git a/packages/dns-test/src/lib.rs b/packages/dns-test/src/lib.rs index 05fa9d41..f6232bfc 100644 --- a/packages/dns-test/src/lib.rs +++ b/packages/dns-test/src/lib.rs @@ -2,6 +2,7 @@ pub use crate::fqdn::FQDN; pub use crate::recursive_resolver::RecursiveResolver; +pub use crate::trust_anchor::TrustAnchor; pub type Error = Box; pub type Result = core::result::Result; @@ -12,4 +13,5 @@ mod fqdn; pub mod name_server; pub mod record; mod recursive_resolver; +mod trust_anchor; pub mod zone_file; diff --git a/packages/dns-test/src/recursive_resolver.rs b/packages/dns-test/src/recursive_resolver.rs index 8c318cd4..f88d8f8d 100644 --- a/packages/dns-test/src/recursive_resolver.rs +++ b/packages/dns-test/src/recursive_resolver.rs @@ -2,7 +2,8 @@ use core::fmt::Write; use std::net::Ipv4Addr; use crate::container::{Child, Container}; -use crate::zone_file::{Root, DNSKEY}; +use crate::trust_anchor::TrustAnchor; +use crate::zone_file::Root; use crate::Result; pub struct RecursiveResolver { @@ -11,7 +12,7 @@ pub struct RecursiveResolver { } impl RecursiveResolver { - pub fn start(roots: &[Root], trust_anchors: &[DNSKEY]) -> Result { + pub fn start(roots: &[Root], trust_anchor: &TrustAnchor) -> Result { const TRUST_ANCHOR_FILE: &str = "/etc/trusted-key.key"; let container = Container::run()?; @@ -23,16 +24,11 @@ impl RecursiveResolver { container.cp("/etc/unbound/root.hints", &hints)?; - let use_dnssec = !trust_anchors.is_empty(); + let use_dnssec = !trust_anchor.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)?; + container.cp(TRUST_ANCHOR_FILE, &trust_anchor.to_string())?; } let child = container.spawn(&["unbound", "-d"])?; @@ -76,7 +72,7 @@ mod tests { #[test] fn terminate_works() -> Result<()> { - let resolver = RecursiveResolver::start(&[], &[])?; + let resolver = RecursiveResolver::start(&[], &TrustAnchor::empty())?; let logs = resolver.terminate()?; eprintln!("{logs}"); diff --git a/packages/dns-test/src/trust_anchor.rs b/packages/dns-test/src/trust_anchor.rs new file mode 100644 index 00000000..b14173e2 --- /dev/null +++ b/packages/dns-test/src/trust_anchor.rs @@ -0,0 +1,51 @@ +use core::fmt; + +use crate::zone_file::DNSKEY; + +pub struct TrustAnchor { + keys: Vec, +} + +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 for TrustAnchor { + fn from_iter>(iter: T) -> Self { + Self { + keys: iter.into_iter().collect(), + } + } +} diff --git a/packages/dns-test/src/zone_file.rs b/packages/dns-test/src/zone_file.rs index 71a7b9c6..8dc153c9 100644 --- a/packages/dns-test/src/zone_file.rs +++ b/packages/dns-test/src/zone_file.rs @@ -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 { From 362838b41f55c9854836e5b271bccf376612fde2 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 8 Feb 2024 19:28:05 +0100 Subject: [PATCH 042/124] update CI config --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea123563..b5e61a7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,10 +21,10 @@ jobs: components: clippy, rustfmt - name: Run tests - run: cargo t + run: cargo test --workspace - 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 From 3c95b851503e4f30f52d988a4b3d120b0ed609b3 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 9 Feb 2024 14:55:03 +0100 Subject: [PATCH 043/124] introduce Implementation enum --- .../src/resolver/dns/scenarios.rs | 2 +- .../src/resolver/dnssec/scenarios.rs | 4 +- packages/dns-test/src/client.rs | 4 +- packages/dns-test/src/container.rs | 13 +++--- .../dns-test/src/docker/hickory.Dockerfile | 7 +++ .../dns-test/src/docker/unbound.Dockerfile | 2 +- packages/dns-test/src/lib.rs | 45 +++++++++++++++++++ packages/dns-test/src/name_server.rs | 4 +- packages/dns-test/src/recursive_resolver.rs | 13 ++++-- 9 files changed, 75 insertions(+), 19 deletions(-) create mode 100644 packages/dns-test/src/docker/hickory.Dockerfile diff --git a/packages/conformance-tests/src/resolver/dns/scenarios.rs b/packages/conformance-tests/src/resolver/dns/scenarios.rs index e229d1b8..84da3b31 100644 --- a/packages/conformance-tests/src/resolver/dns/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dns/scenarios.rs @@ -38,7 +38,7 @@ fn can_resolve() -> Result<()> { 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, &TrustAnchor::empty())?; + let resolver = RecursiveResolver::start(dns_test::subject(), roots, &TrustAnchor::empty())?; let resolver_ip_addr = resolver.ipv4_addr(); let client = Client::new()?; diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs index 2a9d0846..7838800f 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs @@ -25,7 +25,7 @@ fn can_validate_without_delegation() -> Result<()> { let roots = &[Root::new(ns.fqdn().clone(), ns.ipv4_addr())]; let trust_anchor = TrustAnchor::from_iter([root_ksk.clone(), root_zsk.clone()]); - let resolver = RecursiveResolver::start(roots, &trust_anchor)?; + let resolver = RecursiveResolver::start(dns_test::subject(), roots, &trust_anchor)?; let resolver_addr = resolver.ipv4_addr(); let client = Client::new()?; @@ -94,7 +94,7 @@ fn can_validate_with_delegation() -> Result<()> { 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 = RecursiveResolver::start(roots, &trust_anchor)?; + let resolver = RecursiveResolver::start(dns_test::subject(), roots, &trust_anchor)?; let resolver_addr = resolver.ipv4_addr(); let client = Client::new()?; diff --git a/packages/dns-test/src/client.rs b/packages/dns-test/src/client.rs index d7b90144..a4102d67 100644 --- a/packages/dns-test/src/client.rs +++ b/packages/dns-test/src/client.rs @@ -4,7 +4,7 @@ use std::net::Ipv4Addr; use crate::container::Container; use crate::record::{Record, RecordType}; use crate::trust_anchor::TrustAnchor; -use crate::{Error, Result, FQDN}; +use crate::{Error, Implementation, Result, FQDN}; pub struct Client { inner: Container, @@ -13,7 +13,7 @@ pub struct Client { impl Client { pub fn new() -> Result { Ok(Self { - inner: Container::run()?, + inner: Container::run(Implementation::Unbound)?, }) } diff --git a/packages/dns-test/src/container.rs b/packages/dns-test/src/container.rs index 545c7385..3c51c70e 100644 --- a/packages/dns-test/src/container.rs +++ b/packages/dns-test/src/container.rs @@ -8,7 +8,7 @@ use std::sync::{atomic, Arc, Once}; use tempfile::{NamedTempFile, TempDir}; -use crate::{Error, Result}; +use crate::{Error, Implementation, Result}; pub struct Container { inner: Arc, @@ -18,12 +18,11 @@ const PACKAGE_NAME: &str = env!("CARGO_PKG_NAME"); impl Container { /// Starts the container in a "parked" state - pub fn run() -> Result { + pub fn run(implementation: Implementation) -> Result { static ONCE: Once = Once::new(); // TODO make this configurable and support hickory & bind - let implementation = "unbound"; - let dockerfile = include_str!("docker/unbound.Dockerfile"); + 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)?; @@ -270,7 +269,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()); @@ -280,7 +279,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}")])?; @@ -291,7 +290,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"; diff --git a/packages/dns-test/src/docker/hickory.Dockerfile b/packages/dns-test/src/docker/hickory.Dockerfile new file mode 100644 index 00000000..0fe0137f --- /dev/null +++ b/packages/dns-test/src/docker/hickory.Dockerfile @@ -0,0 +1,7 @@ +FROM rust:1-slim-bookworm + +RUN apt-get update && \ + apt-get install -y \ + tshark + +RUN cargo install hickory-dns --version 0.24.0 diff --git a/packages/dns-test/src/docker/unbound.Dockerfile b/packages/dns-test/src/docker/unbound.Dockerfile index 234edf49..b42777ef 100644 --- a/packages/dns-test/src/docker/unbound.Dockerfile +++ b/packages/dns-test/src/docker/unbound.Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:22.04 +FROM debian:bookworm-slim # dnsutils = dig & delv # iputils-ping = ping diff --git a/packages/dns-test/src/lib.rs b/packages/dns-test/src/lib.rs index f6232bfc..b6747262 100644 --- a/packages/dns-test/src/lib.rs +++ b/packages/dns-test/src/lib.rs @@ -1,5 +1,7 @@ //! A test framework for all things DNS +use core::fmt; + pub use crate::fqdn::FQDN; pub use crate::recursive_resolver::RecursiveResolver; pub use crate::trust_anchor::TrustAnchor; @@ -15,3 +17,46 @@ pub mod record; mod recursive_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"), + } + } +} + +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() + } +} diff --git a/packages/dns-test/src/name_server.rs b/packages/dns-test/src/name_server.rs index 97c45967..5de5a2e9 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -3,7 +3,7 @@ use std::net::Ipv4Addr; 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, @@ -42,7 +42,7 @@ impl<'a> NameServer<'a, Stopped> { }); Ok(Self { - container: Container::run()?, + container: Container::run(Implementation::Unbound)?, zone_file, state: Stopped, }) diff --git a/packages/dns-test/src/recursive_resolver.rs b/packages/dns-test/src/recursive_resolver.rs index f88d8f8d..7393b1ca 100644 --- a/packages/dns-test/src/recursive_resolver.rs +++ b/packages/dns-test/src/recursive_resolver.rs @@ -4,7 +4,7 @@ use std::net::Ipv4Addr; use crate::container::{Child, Container}; use crate::trust_anchor::TrustAnchor; use crate::zone_file::Root; -use crate::Result; +use crate::{Implementation, Result}; pub struct RecursiveResolver { container: Container, @@ -12,10 +12,14 @@ pub struct RecursiveResolver { } impl RecursiveResolver { - pub fn start(roots: &[Root], trust_anchor: &TrustAnchor) -> Result { + pub fn start( + implementation: Implementation, + roots: &[Root], + trust_anchor: &TrustAnchor, + ) -> Result { const TRUST_ANCHOR_FILE: &str = "/etc/trusted-key.key"; - let container = Container::run()?; + let container = Container::run(implementation)?; let mut hints = String::new(); for root in roots { @@ -72,7 +76,8 @@ mod tests { #[test] fn terminate_works() -> Result<()> { - let resolver = RecursiveResolver::start(&[], &TrustAnchor::empty())?; + let resolver = + RecursiveResolver::start(Implementation::Unbound, &[], &TrustAnchor::empty())?; let logs = resolver.terminate()?; eprintln!("{logs}"); From 04a7190e616f7c4d206d3c73045e776bf9586f2a Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 9 Feb 2024 15:37:58 +0100 Subject: [PATCH 044/124] supporty hickory-based Resolver --- packages/dns-test/src/container.rs | 6 ++-- .../dns-test/src/docker/hickory.Dockerfile | 3 +- packages/dns-test/src/lib.rs | 14 ++++++++++ packages/dns-test/src/recursive_resolver.rs | 28 ++++++++++++++++--- .../src/templates/hickory.resolver.toml.jinja | 5 ++++ 5 files changed, 47 insertions(+), 9 deletions(-) create mode 100644 packages/dns-test/src/templates/hickory.resolver.toml.jinja diff --git a/packages/dns-test/src/container.rs b/packages/dns-test/src/container.rs index 3c51c70e..006770b3 100644 --- a/packages/dns-test/src/container.rs +++ b/packages/dns-test/src/container.rs @@ -4,7 +4,7 @@ use std::net::Ipv4Addr; use std::process::{self, ExitStatus}; use std::process::{Command, Stdio}; use std::sync::atomic::AtomicUsize; -use std::sync::{atomic, Arc, Once}; +use std::sync::{atomic, Arc}; use tempfile::{NamedTempFile, TempDir}; @@ -19,8 +19,6 @@ const PACKAGE_NAME: &str = env!("CARGO_PKG_NAME"); impl Container { /// Starts the container in a "parked" state pub fn run(implementation: Implementation) -> Result { - static ONCE: Once = Once::new(); - // TODO make this configurable and support hickory & bind let dockerfile = implementation.dockerfile(); let docker_build_dir = TempDir::new()?; @@ -35,7 +33,7 @@ impl Container { .arg(&image_tag) .arg(docker_build_dir); - ONCE.call_once(|| { + 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); diff --git a/packages/dns-test/src/docker/hickory.Dockerfile b/packages/dns-test/src/docker/hickory.Dockerfile index 0fe0137f..1f6efac9 100644 --- a/packages/dns-test/src/docker/hickory.Dockerfile +++ b/packages/dns-test/src/docker/hickory.Dockerfile @@ -4,4 +4,5 @@ RUN apt-get update && \ apt-get install -y \ tshark -RUN cargo install hickory-dns --version 0.24.0 +RUN cargo install hickory-dns --version 0.24.0 --features recursor +env RUST_LOG=debug diff --git a/packages/dns-test/src/lib.rs b/packages/dns-test/src/lib.rs index b6747262..aeccd5fe 100644 --- a/packages/dns-test/src/lib.rs +++ b/packages/dns-test/src/lib.rs @@ -1,6 +1,7 @@ //! A test framework for all things DNS use core::fmt; +use std::sync::Once; pub use crate::fqdn::FQDN; pub use crate::recursive_resolver::RecursiveResolver; @@ -31,6 +32,19 @@ impl Implementation { 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 { diff --git a/packages/dns-test/src/recursive_resolver.rs b/packages/dns-test/src/recursive_resolver.rs index 7393b1ca..f95ee696 100644 --- a/packages/dns-test/src/recursive_resolver.rs +++ b/packages/dns-test/src/recursive_resolver.rs @@ -26,16 +26,32 @@ impl RecursiveResolver { writeln!(hints, "{root}").unwrap(); } - container.cp("/etc/unbound/root.hints", &hints)?; - let use_dnssec = !trust_anchor.is_empty(); - container.cp("/etc/unbound/unbound.conf", &unbound_conf(use_dnssec))?; + 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 child = container.spawn(&["unbound", "-d"])?; + let command: &[_] = match implementation { + Implementation::Unbound => &["unbound", "-d"], + Implementation::Hickory => &["hickory-dns", "-d"], + }; + let child = container.spawn(command)?; Ok(Self { child, container }) } @@ -70,6 +86,10 @@ 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::*; diff --git a/packages/dns-test/src/templates/hickory.resolver.toml.jinja b/packages/dns-test/src/templates/hickory.resolver.toml.jinja new file mode 100644 index 00000000..d3da6496 --- /dev/null +++ b/packages/dns-test/src/templates/hickory.resolver.toml.jinja @@ -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 }} From 9526338ca76b334b22628c6798f67c43e86406e9 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 9 Feb 2024 16:03:09 +0100 Subject: [PATCH 045/124] shorten RecursiveResolver -> Resolver --- packages/conformance-tests/src/resolver/dns/scenarios.rs | 4 ++-- .../conformance-tests/src/resolver/dnssec/scenarios.rs | 6 +++--- packages/dns-test/src/lib.rs | 4 ++-- .../dns-test/src/{recursive_resolver.rs => resolver.rs} | 7 +++---- 4 files changed, 10 insertions(+), 11 deletions(-) rename packages/dns-test/src/{recursive_resolver.rs => resolver.rs} (94%) diff --git a/packages/conformance-tests/src/resolver/dns/scenarios.rs b/packages/conformance-tests/src/resolver/dns/scenarios.rs index 84da3b31..3c965295 100644 --- a/packages/conformance-tests/src/resolver/dns/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dns/scenarios.rs @@ -4,7 +4,7 @@ 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::{RecursiveResolver, Result, TrustAnchor, FQDN}; +use dns_test::{Resolver, Result, TrustAnchor, FQDN}; #[test] fn can_resolve() -> Result<()> { @@ -38,7 +38,7 @@ fn can_resolve() -> Result<()> { eprintln!("root.zone:\n{}", root_ns.zone_file()); let roots = &[Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr())]; - let resolver = RecursiveResolver::start(dns_test::subject(), roots, &TrustAnchor::empty())?; + let resolver = Resolver::start(dns_test::subject(), roots, &TrustAnchor::empty())?; let resolver_ip_addr = resolver.ipv4_addr(); let client = Client::new()?; diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs index 7838800f..c4e969de 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs @@ -4,7 +4,7 @@ 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::{RecursiveResolver, Result, TrustAnchor, FQDN}; +use dns_test::{Resolver, Result, TrustAnchor, FQDN}; // no DS records are involved; this is a single-link chain of trust #[test] @@ -25,7 +25,7 @@ fn can_validate_without_delegation() -> Result<()> { let roots = &[Root::new(ns.fqdn().clone(), ns.ipv4_addr())]; let trust_anchor = TrustAnchor::from_iter([root_ksk.clone(), root_zsk.clone()]); - let resolver = RecursiveResolver::start(dns_test::subject(), roots, &trust_anchor)?; + let resolver = Resolver::start(dns_test::subject(), roots, &trust_anchor)?; let resolver_addr = resolver.ipv4_addr(); let client = Client::new()?; @@ -94,7 +94,7 @@ fn can_validate_with_delegation() -> Result<()> { 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 = RecursiveResolver::start(dns_test::subject(), roots, &trust_anchor)?; + let resolver = Resolver::start(dns_test::subject(), roots, &trust_anchor)?; let resolver_addr = resolver.ipv4_addr(); let client = Client::new()?; diff --git a/packages/dns-test/src/lib.rs b/packages/dns-test/src/lib.rs index aeccd5fe..c88e1209 100644 --- a/packages/dns-test/src/lib.rs +++ b/packages/dns-test/src/lib.rs @@ -4,7 +4,7 @@ use core::fmt; use std::sync::Once; pub use crate::fqdn::FQDN; -pub use crate::recursive_resolver::RecursiveResolver; +pub use crate::resolver::Resolver; pub use crate::trust_anchor::TrustAnchor; pub type Error = Box; @@ -15,7 +15,7 @@ mod container; mod fqdn; pub mod name_server; pub mod record; -mod recursive_resolver; +mod resolver; mod trust_anchor; pub mod zone_file; diff --git a/packages/dns-test/src/recursive_resolver.rs b/packages/dns-test/src/resolver.rs similarity index 94% rename from packages/dns-test/src/recursive_resolver.rs rename to packages/dns-test/src/resolver.rs index f95ee696..12d94193 100644 --- a/packages/dns-test/src/recursive_resolver.rs +++ b/packages/dns-test/src/resolver.rs @@ -6,12 +6,12 @@ use crate::trust_anchor::TrustAnchor; use crate::zone_file::Root; use crate::{Implementation, Result}; -pub struct RecursiveResolver { +pub struct Resolver { container: Container, child: Child, } -impl RecursiveResolver { +impl Resolver { pub fn start( implementation: Implementation, roots: &[Root], @@ -96,8 +96,7 @@ mod tests { #[test] fn terminate_works() -> Result<()> { - let resolver = - RecursiveResolver::start(Implementation::Unbound, &[], &TrustAnchor::empty())?; + let resolver = Resolver::start(Implementation::Unbound, &[], &TrustAnchor::empty())?; let logs = resolver.terminate()?; eprintln!("{logs}"); From e52980a82f175a9c194af52cdda600d3cf86c5af Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 9 Feb 2024 16:35:29 +0100 Subject: [PATCH 046/124] test NXDOMAIN scenario --- .../src/resolver/dns/scenarios.rs | 41 +++++++++++++++++++ .../src/resolver/dnssec/scenarios.rs | 3 ++ packages/dns-test/src/client.rs | 7 ++++ 3 files changed, 51 insertions(+) diff --git a/packages/conformance-tests/src/resolver/dns/scenarios.rs b/packages/conformance-tests/src/resolver/dns/scenarios.rs index 3c965295..c6274950 100644 --- a/packages/conformance-tests/src/resolver/dns/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dns/scenarios.rs @@ -60,3 +60,44 @@ fn can_resolve() -> Result<()> { Ok(()) } + +#[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(()) +} diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs index c4e969de..a3f07af8 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs @@ -121,3 +121,6 @@ fn can_validate_with_delegation() -> Result<()> { Ok(()) } + +// TODO nxdomain with NSEC records +// TODO nxdomain with NSEC3 records diff --git a/packages/dns-test/src/client.rs b/packages/dns-test/src/client.rs index a4102d67..5cce1e4e 100644 --- a/packages/dns-test/src/client.rs +++ b/packages/dns-test/src/client.rs @@ -220,6 +220,7 @@ pub enum DigStatus { NOERROR, NXDOMAIN, REFUSED, + SERVFAIL, } impl DigStatus { @@ -227,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 { @@ -237,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()), }; From d17e42a679445c88b90886c66828db144e471f20 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 9 Feb 2024 16:37:53 +0100 Subject: [PATCH 047/124] update CI to run tests against unbound & hickory --- .github/workflows/ci.yml | 7 +++++-- packages/conformance-tests/src/resolver/dns/scenarios.rs | 1 + .../conformance-tests/src/resolver/dnssec/scenarios.rs | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5e61a7a..8f19df18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,8 +20,11 @@ jobs: toolchain: stable components: clippy, rustfmt - - name: Run tests - run: cargo test --workspace + - name: Run tests against unbound + run: cargo test --workspace --include-ignored + + - name: Run tests against hickory + run: DNS_TEST_SUBJECT=hickory cargo test --workspace - name: Check that code is formatted run: cargo fmt --all -- --check diff --git a/packages/conformance-tests/src/resolver/dns/scenarios.rs b/packages/conformance-tests/src/resolver/dns/scenarios.rs index c6274950..a25d3bce 100644 --- a/packages/conformance-tests/src/resolver/dns/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dns/scenarios.rs @@ -61,6 +61,7 @@ fn can_resolve() -> Result<()> { Ok(()) } +#[ignore] #[test] fn nxdomain() -> Result<()> { let needle_fqdn = FQDN("unicorn.nameservers.com.")?; diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs index a3f07af8..31c5df7b 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs @@ -7,6 +7,7 @@ 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)?; @@ -46,6 +47,7 @@ fn can_validate_without_delegation() -> Result<()> { Ok(()) } +#[ignore] #[test] fn can_validate_with_delegation() -> Result<()> { let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4); From 3d73fe3da1ed2c6916559f541767a97bafd6aacf Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 9 Feb 2024 16:38:45 +0100 Subject: [PATCH 048/124] fix CI workflow --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f19df18..68cfb642 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: components: clippy, rustfmt - name: Run tests against unbound - run: cargo test --workspace --include-ignored + run: cargo test --workspace -- --include-ignored - name: Run tests against hickory run: DNS_TEST_SUBJECT=hickory cargo test --workspace From 952c346e9e3d23797d68055da46e399eb9a696e9 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 9 Feb 2024 16:42:52 +0100 Subject: [PATCH 049/124] build hickory in debug mode --- packages/dns-test/src/docker/hickory.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dns-test/src/docker/hickory.Dockerfile b/packages/dns-test/src/docker/hickory.Dockerfile index 1f6efac9..d2edd1a9 100644 --- a/packages/dns-test/src/docker/hickory.Dockerfile +++ b/packages/dns-test/src/docker/hickory.Dockerfile @@ -4,5 +4,5 @@ RUN apt-get update && \ apt-get install -y \ tshark -RUN cargo install hickory-dns --version 0.24.0 --features recursor +RUN cargo install hickory-dns --version 0.24.0 --features recursor --debug env RUST_LOG=debug From 33509f44846f19fb8950550cd28a7dc743f7b730 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 9 Feb 2024 16:48:59 +0100 Subject: [PATCH 050/124] check that ignored tests fail with hickory --- .github/workflows/ci.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68cfb642..963da5c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,11 +20,20 @@ jobs: toolchain: stable components: clippy, rustfmt + - name: Run dns-test tests + run: cargo test -p dns-test -- --include-ignored + - name: Run tests against unbound - run: cargo test --workspace -- --include-ignored + run: cargo test -p conformance-tests -- --include-ignored - name: Run tests against hickory - run: DNS_TEST_SUBJECT=hickory cargo test --workspace + 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 --all -- --check From 36f93252a2cda50dea6881a4aa256a1b91438e22 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 12 Feb 2024 19:11:59 +0100 Subject: [PATCH 051/124] resolver: check that the root servers list is not empty --- packages/dns-test/src/resolver.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/dns-test/src/resolver.rs b/packages/dns-test/src/resolver.rs index 12d94193..3a084f7a 100644 --- a/packages/dns-test/src/resolver.rs +++ b/packages/dns-test/src/resolver.rs @@ -12,6 +12,13 @@ pub struct Resolver { } impl Resolver { + /// Starts a DNS server in the recursive resolver role + /// + /// This server is not an authoritative name server; it does not server a zone file to clients + /// + /// # Panics + /// + /// This constructor panics if `roots` is an empty slice pub fn start( implementation: Implementation, roots: &[Root], @@ -19,6 +26,11 @@ impl Resolver { ) -> Result { const TRUST_ANCHOR_FILE: &str = "/etc/trusted-key.key"; + assert!( + !roots.is_empty(), + "must configure at least one local root server" + ); + let container = Container::run(implementation)?; let mut hints = String::new(); @@ -92,11 +104,19 @@ fn hickory_conf(use_dnssec: bool) -> String { #[cfg(test)] mod tests { + use crate::{name_server::NameServer, FQDN}; + use super::*; #[test] fn terminate_works() -> Result<()> { - let resolver = Resolver::start(Implementation::Unbound, &[], &TrustAnchor::empty())?; + let ns = NameServer::new(FQDN::ROOT)?.start()?; + + let resolver = Resolver::start( + Implementation::Unbound, + &[Root::new(ns.fqdn().clone(), ns.ipv4_addr())], + &TrustAnchor::empty(), + )?; let logs = resolver.terminate()?; eprintln!("{logs}"); From 5630dd79e94373bb2d7f08e282b6817903e7da29 Mon Sep 17 00:00:00 2001 From: Sebastian Ziebell Date: Tue, 13 Feb 2024 10:55:39 +0100 Subject: [PATCH 052/124] Add Network types Creates & removes the Docker network & reads in the allocated subnet mask. --- packages/dns-test/src/container.rs | 2 + packages/dns-test/src/container/network.rs | 109 +++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 packages/dns-test/src/container/network.rs diff --git a/packages/dns-test/src/container.rs b/packages/dns-test/src/container.rs index 006770b3..61bf0182 100644 --- a/packages/dns-test/src/container.rs +++ b/packages/dns-test/src/container.rs @@ -1,3 +1,5 @@ +mod network; + use core::str; use std::fs; use std::net::Ipv4Addr; diff --git a/packages/dns-test/src/container/network.rs b/packages/dns-test/src/container/network.rs new file mode 100644 index 00000000..fdbe5ee7 --- /dev/null +++ b/packages/dns-test/src/container/network.rs @@ -0,0 +1,109 @@ +use std::{ + process::{Command, Stdio}, + sync::atomic::{self, AtomicUsize}, +}; + +use crate::Result; + +const NETWORK_NAME: &str = "dnssec-network"; + +/// Represents a network in which to put containers into. +pub struct Network { + name: String, +} + +impl Network { + pub fn new() -> Result { + let id = network_count(); + let network_name = format!("{NETWORK_NAME}-{id}"); + + let mut command = Command::new("docker"); + command + .args(["network", "create"]) + .args(["--internal"]) + .arg(&network_name); + + // create network + 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}" + ); + + // inspect & parse network details + + Ok(Self { name: network_name }) + } + + /// Returns the name of the network. + pub fn name(&self) -> &str { + self.name.as_str() + } +} + +/// Collects all important configs. +pub struct NetworkConfig { + /// The CIDR subnet mask, e.g. "172.21.0.0/16" + subnet: String, +} + +/// +fn get_network_config(network_name: &str) -> Result { + let mut command = Command::new("docker"); + command + .args([ + "network", + "inspect", + "-f", + "{{range .IPAM.Config}}{{.Subnet}}{{end}}", + ]) + .arg(network_name); + + let output = command.output()?; + if !output.status.success() { + return Err(format!("{command:?} failed").into()); + } + + let subnet = std::str::from_utf8(&output.stdout)?.trim().to_string(); + Ok(NetworkConfig { subnet }) +} + +/// This ensure the Docket network is deleted after the test runner process ends. +impl Drop for Network { + fn drop(&mut self) { + // Remove the network + // TODO check if all containers need to disconnect first + let _ = Command::new("docker") + .args(["network", "rm", "--force", self.name.as_str()]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + } +} + +fn network_count() -> usize { + static COUNT: AtomicUsize = AtomicUsize::new(1); + + COUNT.fetch_add(1, atomic::Ordering::Relaxed) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn create_works() -> Result<()> { + assert!(Network::new().is_ok()); + Ok(()) + } + + #[test] + fn network_subnet_works() -> Result<()> { + let network = Network::new().expect("Failed to create network"); + let config = get_network_config(network.name()); + assert!(config.is_ok()); + Ok(()) + } +} From 820f1c3447a798dcf3086a6fa942a8952a289ec2 Mon Sep 17 00:00:00 2001 From: Sebastian Ziebell Date: Tue, 13 Feb 2024 11:25:52 +0100 Subject: [PATCH 053/124] Pass in Network to containers --- .../src/resolver/dns/scenarios.rs | 24 +++++++------- .../src/resolver/dnssec/scenarios.rs | 20 ++++++----- packages/dns-test/src/client.rs | 6 ++-- packages/dns-test/src/container.rs | 13 +++++--- packages/dns-test/src/container/network.rs | 33 ++++++++++++------- packages/dns-test/src/lib.rs | 1 + packages/dns-test/src/name_server.rs | 24 ++++++++------ packages/dns-test/src/resolver.rs | 10 +++--- 8 files changed, 79 insertions(+), 52 deletions(-) diff --git a/packages/conformance-tests/src/resolver/dns/scenarios.rs b/packages/conformance-tests/src/resolver/dns/scenarios.rs index a25d3bce..9dcdcbd2 100644 --- a/packages/conformance-tests/src/resolver/dns/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dns/scenarios.rs @@ -4,17 +4,18 @@ 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}; +use dns_test::{Network, 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 network = Network::new()?; + let mut root_ns = NameServer::new(FQDN::ROOT, &network)?; + let mut com_ns = NameServer::new(FQDN::COM, &network)?; - let mut nameservers_ns = NameServer::new(FQDN("nameservers.com.")?)?; + let mut nameservers_ns = NameServer::new(FQDN("nameservers.com.")?, &network)?; nameservers_ns .a(root_ns.fqdn().clone(), root_ns.ipv4_addr()) .a(com_ns.fqdn().clone(), com_ns.ipv4_addr()) @@ -38,10 +39,10 @@ fn can_resolve() -> Result<()> { 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 = Resolver::start(dns_test::subject(), roots, &TrustAnchor::empty(), &network)?; let resolver_ip_addr = resolver.ipv4_addr(); - let client = Client::new()?; + let client = Client::new(&network)?; let output = client.dig( Recurse::Yes, Dnssec::No, @@ -66,10 +67,11 @@ fn can_resolve() -> Result<()> { 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 network = Network::new()?; + let mut root_ns = NameServer::new(FQDN::ROOT, &network)?; + let mut com_ns = NameServer::new(FQDN::COM, &network)?; - let mut nameservers_ns = NameServer::new(FQDN("nameservers.com.")?)?; + let mut nameservers_ns = NameServer::new(FQDN("nameservers.com.")?, &network)?; nameservers_ns .a(root_ns.fqdn().clone(), root_ns.ipv4_addr()) .a(com_ns.fqdn().clone(), com_ns.ipv4_addr()); @@ -86,10 +88,10 @@ fn nxdomain() -> Result<()> { 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 = Resolver::start(dns_test::subject(), roots, &TrustAnchor::empty(), &network)?; let resolver_ip_addr = resolver.ipv4_addr(); - let client = Client::new()?; + let client = Client::new(&network)?; let output = client.dig( Recurse::Yes, Dnssec::No, diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs index 31c5df7b..1300d9b4 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs @@ -4,13 +4,14 @@ 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}; +use dns_test::{Network, 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)?; + let network = Network::new()?; + let mut ns = NameServer::new(FQDN::ROOT, &network)?; ns.a(ns.fqdn().clone(), ns.ipv4_addr()); let ns = ns.sign()?; @@ -26,10 +27,10 @@ fn can_validate_without_delegation() -> Result<()> { 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 = Resolver::start(dns_test::subject(), roots, &trust_anchor, &network)?; let resolver_addr = resolver.ipv4_addr(); - let client = Client::new()?; + let client = Client::new(&network)?; let output = client.dig( Recurse::Yes, Dnssec::Yes, @@ -53,10 +54,11 @@ 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 network = Network::new()?; + let mut root_ns = NameServer::new(FQDN::ROOT, &network)?; + let mut com_ns = NameServer::new(FQDN::COM, &network)?; - let mut nameservers_ns = NameServer::new(FQDN("nameservers.com.")?)?; + let mut nameservers_ns = NameServer::new(FQDN("nameservers.com.")?, &network)?; nameservers_ns .a(root_ns.fqdn().clone(), root_ns.ipv4_addr()) .a(com_ns.fqdn().clone(), com_ns.ipv4_addr()) @@ -96,10 +98,10 @@ fn can_validate_with_delegation() -> Result<()> { 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 = Resolver::start(dns_test::subject(), roots, &trust_anchor, &network)?; let resolver_addr = resolver.ipv4_addr(); - let client = Client::new()?; + let client = Client::new(&network)?; let output = client.dig( Recurse::Yes, Dnssec::Yes, diff --git a/packages/dns-test/src/client.rs b/packages/dns-test/src/client.rs index 5cce1e4e..bfc57b7b 100644 --- a/packages/dns-test/src/client.rs +++ b/packages/dns-test/src/client.rs @@ -1,7 +1,7 @@ use core::str::FromStr; use std::net::Ipv4Addr; -use crate::container::Container; +use crate::container::{Container, Network}; use crate::record::{Record, RecordType}; use crate::trust_anchor::TrustAnchor; use crate::{Error, Implementation, Result, FQDN}; @@ -11,9 +11,9 @@ pub struct Client { } impl Client { - pub fn new() -> Result { + pub fn new(network: &Network) -> Result { Ok(Self { - inner: Container::run(Implementation::Unbound)?, + inner: Container::run(Implementation::Unbound, network)?, }) } diff --git a/packages/dns-test/src/container.rs b/packages/dns-test/src/container.rs index 61bf0182..21d621ee 100644 --- a/packages/dns-test/src/container.rs +++ b/packages/dns-test/src/container.rs @@ -12,6 +12,8 @@ use tempfile::{NamedTempFile, TempDir}; use crate::{Error, Implementation, Result}; +pub use crate::container::network::Network; + pub struct Container { inner: Arc, } @@ -20,7 +22,7 @@ const PACKAGE_NAME: &str = env!("CARGO_PKG_NAME"); impl Container { /// Starts the container in a "parked" state - pub fn run(implementation: Implementation) -> Result { + pub fn run(implementation: Implementation, network: &Network) -> Result { // TODO make this configurable and support hickory & bind let dockerfile = implementation.dockerfile(); let docker_build_dir = TempDir::new()?; @@ -269,7 +271,8 @@ mod tests { #[test] fn run_works() -> Result<()> { - let container = Container::run(Implementation::Unbound)?; + let network = Network::new()?; + let container = Container::run(Implementation::Unbound, &network)?; let output = container.output(&["true"])?; assert!(output.status.success()); @@ -279,7 +282,8 @@ mod tests { #[test] fn ipv4_addr_works() -> Result<()> { - let container = Container::run(Implementation::Unbound)?; + let network = Network::new()?; + let container = Container::run(Implementation::Unbound, &network)?; let ipv4_addr = container.ipv4_addr(); let output = container.output(&["ping", "-c1", &format!("{ipv4_addr}")])?; @@ -290,7 +294,8 @@ mod tests { #[test] fn cp_works() -> Result<()> { - let container = Container::run(Implementation::Unbound)?; + let network = Network::new()?; + let container = Container::run(Implementation::Unbound, &network)?; let path = "/tmp/somefile"; let contents = "hello"; diff --git a/packages/dns-test/src/container/network.rs b/packages/dns-test/src/container/network.rs index fdbe5ee7..7d7036ab 100644 --- a/packages/dns-test/src/container/network.rs +++ b/packages/dns-test/src/container/network.rs @@ -1,5 +1,5 @@ use std::{ - process::{Command, Stdio}, + process::{Command, ExitStatus, Stdio}, sync::atomic::{self, AtomicUsize}, }; @@ -10,6 +10,7 @@ const NETWORK_NAME: &str = "dnssec-network"; /// Represents a network in which to put containers into. pub struct Network { name: String, + config: NetworkConfig, } impl Network { @@ -17,6 +18,9 @@ impl Network { let id = network_count(); let network_name = format!("{NETWORK_NAME}-{id}"); + // A network can exist, for example when a test panics + let _ = remove_network(network_name.as_str())?; + let mut command = Command::new("docker"); command .args(["network", "create"]) @@ -33,8 +37,12 @@ impl Network { ); // inspect & parse network details + let config = get_network_config(&network_name)?; - Ok(Self { name: network_name }) + Ok(Self { + name: network_name, + config, + }) } /// Returns the name of the network. @@ -49,7 +57,7 @@ pub struct NetworkConfig { subnet: String, } -/// +/// Return network config fn get_network_config(network_name: &str) -> Result { let mut command = Command::new("docker"); command @@ -70,19 +78,22 @@ fn get_network_config(network_name: &str) -> Result { Ok(NetworkConfig { subnet }) } -/// This ensure the Docket network is deleted after the test runner process ends. +/// This ensure the Docker network is deleted after the test runner process ends. impl Drop for Network { fn drop(&mut self) { - // Remove the network - // TODO check if all containers need to disconnect first - let _ = Command::new("docker") - .args(["network", "rm", "--force", self.name.as_str()]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status(); + let _ = remove_network(&self.name); } } +fn remove_network(network_name: &str) -> Result { + let mut command = Command::new("docker"); + command + .args(["network", "rm", "--force", network_name]) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + Ok(command.status()?) +} + fn network_count() -> usize { static COUNT: AtomicUsize = AtomicUsize::new(1); diff --git a/packages/dns-test/src/lib.rs b/packages/dns-test/src/lib.rs index c88e1209..18a625d7 100644 --- a/packages/dns-test/src/lib.rs +++ b/packages/dns-test/src/lib.rs @@ -3,6 +3,7 @@ use core::fmt; use std::sync::Once; +pub use crate::container::Network; pub use crate::fqdn::FQDN; pub use crate::resolver::Resolver; pub use crate::trust_anchor::TrustAnchor; diff --git a/packages/dns-test/src/name_server.rs b/packages/dns-test/src/name_server.rs index 5de5a2e9..42d0919c 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -1,7 +1,7 @@ use core::sync::atomic::{self, AtomicUsize}; use std::net::Ipv4Addr; -use crate::container::{Child, Container}; +use crate::container::{Child, Container, Network}; use crate::zone_file::{self, SoaSettings, ZoneFile, DNSKEY, DS}; use crate::{Implementation, Result, FQDN}; @@ -24,7 +24,7 @@ impl<'a> NameServer<'a, Stopped> { /// - one SOA record, with the primary name server field set to this name server's FQDN /// - one NS record, with this name server's FQDN set as the only available name server for /// the zone - pub fn new(zone: FQDN<'a>) -> Result { + pub fn new(zone: FQDN<'a>, network: &Network) -> Result { let ns_count = ns_count(); let nameserver = primary_ns(ns_count); @@ -42,7 +42,7 @@ impl<'a> NameServer<'a, Stopped> { }); Ok(Self { - container: Container::run(Implementation::Unbound)?, + container: Container::run(Implementation::Unbound, network)?, zone_file, state: Stopped, }) @@ -290,10 +290,11 @@ mod tests { #[test] fn simplest() -> Result<()> { - let tld_ns = NameServer::new(FQDN::COM)?.start()?; + let network = Network::new()?; + let tld_ns = NameServer::new(FQDN::COM, &network)?.start()?; let ip_addr = tld_ns.ipv4_addr(); - let client = Client::new()?; + let client = Client::new(&network)?; let output = client.dig( Recurse::No, Dnssec::No, @@ -309,8 +310,9 @@ mod tests { #[test] fn with_referral() -> Result<()> { + let network = Network::new()?; let expected_ip_addr = Ipv4Addr::new(172, 17, 200, 1); - let mut root_ns = NameServer::new(FQDN::ROOT)?; + let mut root_ns = NameServer::new(FQDN::ROOT, &network)?; root_ns.referral( FQDN::COM, FQDN("primary.tld-server.com.")?, @@ -322,7 +324,7 @@ mod tests { let ipv4_addr = root_ns.ipv4_addr(); - let client = Client::new()?; + let client = Client::new(&network)?; let output = client.dig( Recurse::No, Dnssec::No, @@ -338,7 +340,8 @@ mod tests { #[test] fn signed() -> Result<()> { - let ns = NameServer::new(FQDN::ROOT)?.sign()?; + let network = Network::new()?; + let ns = NameServer::new(FQDN::ROOT, &network)?.sign()?; eprintln!("KSK:\n{}", ns.key_signing_key()); eprintln!("ZSK:\n{}", ns.zone_signing_key()); @@ -348,7 +351,7 @@ mod tests { let ns_addr = tld_ns.ipv4_addr(); - let client = Client::new()?; + let client = Client::new(&network)?; let output = client.dig( Recurse::No, Dnssec::Yes, @@ -373,7 +376,8 @@ mod tests { #[test] fn terminate_works() -> Result<()> { - let ns = NameServer::new(FQDN::ROOT)?.start()?; + let network = Network::new()?; + let ns = NameServer::new(FQDN::ROOT, &network)?.start()?; let logs = ns.terminate()?; assert!(logs.contains("nsd starting")); diff --git a/packages/dns-test/src/resolver.rs b/packages/dns-test/src/resolver.rs index 3a084f7a..1e15adeb 100644 --- a/packages/dns-test/src/resolver.rs +++ b/packages/dns-test/src/resolver.rs @@ -1,7 +1,7 @@ use core::fmt::Write; use std::net::Ipv4Addr; -use crate::container::{Child, Container}; +use crate::container::{Child, Container, Network}; use crate::trust_anchor::TrustAnchor; use crate::zone_file::Root; use crate::{Implementation, Result}; @@ -23,6 +23,7 @@ impl Resolver { implementation: Implementation, roots: &[Root], trust_anchor: &TrustAnchor, + network: &Network, ) -> Result { const TRUST_ANCHOR_FILE: &str = "/etc/trusted-key.key"; @@ -31,7 +32,7 @@ impl Resolver { "must configure at least one local root server" ); - let container = Container::run(implementation)?; + let container = Container::run(implementation, network)?; let mut hints = String::new(); for root in roots { @@ -110,12 +111,13 @@ mod tests { #[test] fn terminate_works() -> Result<()> { - let ns = NameServer::new(FQDN::ROOT)?.start()?; - + let network = Network::new()?; + let ns = NameServer::new(FQDN::ROOT, &network)?.start()?; let resolver = Resolver::start( Implementation::Unbound, &[Root::new(ns.fqdn().clone(), ns.ipv4_addr())], &TrustAnchor::empty(), + &network, )?; let logs = resolver.terminate()?; From 2289567998fb169a2fc129c6a12b0f3e396d417e Mon Sep 17 00:00:00 2001 From: Sebastian Ziebell Date: Tue, 13 Feb 2024 12:15:01 +0100 Subject: [PATCH 054/124] Disconnect all containers before removing network The list of attached containers is determined, all of them are disconnected from the network, then the network is deleted. * set net mask in unbound conf template * expose container id --- packages/dns-test/src/container.rs | 5 ++ packages/dns-test/src/container/network.rs | 69 ++++++++++++++++++- packages/dns-test/src/name_server.rs | 4 ++ packages/dns-test/src/resolver.rs | 9 ++- .../dns-test/src/templates/unbound.conf.jinja | 2 +- 5 files changed, 84 insertions(+), 5 deletions(-) diff --git a/packages/dns-test/src/container.rs b/packages/dns-test/src/container.rs index 21d621ee..288d7357 100644 --- a/packages/dns-test/src/container.rs +++ b/packages/dns-test/src/container.rs @@ -54,6 +54,7 @@ impl Container { command .args(["run", "--rm", "--detach", "--name", &name]) .arg("-it") + .args(["--network", network.name()]) .arg(image_tag) .args(["sleep", "infinity"]); @@ -154,6 +155,10 @@ impl Container { pub fn ipv4_addr(&self) -> Ipv4Addr { self.inner.ipv4_addr } + + pub fn id(&self) -> &str { + &self.inner.id + } } fn container_count() -> usize { diff --git a/packages/dns-test/src/container/network.rs b/packages/dns-test/src/container/network.rs index 7d7036ab..730e8f79 100644 --- a/packages/dns-test/src/container/network.rs +++ b/packages/dns-test/src/container/network.rs @@ -24,7 +24,7 @@ impl Network { let mut command = Command::new("docker"); command .args(["network", "create"]) - .args(["--internal"]) + .args(["--internal", "--attachable"]) .arg(&network_name); // create network @@ -49,6 +49,11 @@ impl Network { pub fn name(&self) -> &str { self.name.as_str() } + + /// Returns the subnet mask + pub fn netmask(&self) -> &str { + &self.config.subnet + } } /// Collects all important configs. @@ -85,7 +90,20 @@ impl Drop for Network { } } +/// Removes the given network. fn remove_network(network_name: &str) -> Result { + // Disconnects all attached containers + for container_id in get_attached_containers(network_name)? { + let mut command = Command::new("docker"); + let _ = command + .args(["network", "disconnect", "--force"]) + .args([network_name, container_id.as_str()]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status()?; + } + + // Remove the network let mut command = Command::new("docker"); command .args(["network", "rm", "--force", network_name]) @@ -94,6 +112,35 @@ fn remove_network(network_name: &str) -> Result { Ok(command.status()?) } +/// Finds the list of connected containers +fn get_attached_containers(network_name: &str) -> Result> { + let mut command = Command::new("docker"); + command.args([ + "network", + "inspect", + network_name, + "-f", + r#"{{ range $k, $v := .Containers }}{{ printf "%s\n" $k }}{{ end }}"#, + ]); + + let output = command.output()?; + let container_ids = match output.status.success() { + true => { + let container_ids = std::str::from_utf8(&output.stdout)? + .trim() + .to_string() + .lines() + .filter(|line| !line.trim().is_empty()) + .map(|line| line.to_string()) + .collect::>(); + container_ids + } + false => vec![], + }; + + Ok(container_ids) +} + fn network_count() -> usize { static COUNT: AtomicUsize = AtomicUsize::new(1); @@ -102,6 +149,8 @@ fn network_count() -> usize { #[cfg(test)] mod tests { + use crate::{name_server::NameServer, FQDN}; + use super::*; #[test] @@ -117,4 +166,22 @@ mod tests { assert!(config.is_ok()); Ok(()) } + + #[test] + fn remove_network_works() -> Result<()> { + let network = Network::new().expect("Failed to create network"); + let network_name = network.name().to_string(); + let nameserver = NameServer::new(FQDN::ROOT, &network)?; + + let container_ids = get_attached_containers(network.name())?; + assert_eq!(1, container_ids.len()); + assert_eq!(&[nameserver.container_id().to_string()], &container_ids[..]); + + drop(network); + + let container_ids = get_attached_containers(&network_name)?; + assert!(container_ids.is_empty()); + + Ok(()) + } } diff --git a/packages/dns-test/src/name_server.rs b/packages/dns-test/src/name_server.rs index 42d0919c..1189b9cf 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -155,6 +155,10 @@ impl<'a> NameServer<'a, Stopped> { state: Running { child }, }) } + + pub fn container_id(&self) -> &str { + self.container.id() + } } const ZONES_DIR: &str = "/etc/nsd/zones"; diff --git a/packages/dns-test/src/resolver.rs b/packages/dns-test/src/resolver.rs index 1e15adeb..3a56d11a 100644 --- a/packages/dns-test/src/resolver.rs +++ b/packages/dns-test/src/resolver.rs @@ -44,7 +44,10 @@ impl Resolver { Implementation::Unbound => { container.cp("/etc/unbound/root.hints", &hints)?; - container.cp("/etc/unbound/unbound.conf", &unbound_conf(use_dnssec))?; + container.cp( + "/etc/unbound/unbound.conf", + &unbound_conf(use_dnssec, network.netmask()), + )?; } Implementation::Hickory => { @@ -95,8 +98,8 @@ kill -TERM $(cat {pidfile})" } } -fn unbound_conf(use_dnssec: bool) -> String { - minijinja::render!(include_str!("templates/unbound.conf.jinja"), use_dnssec => use_dnssec) +fn unbound_conf(use_dnssec: bool, netmask: &str) -> String { + minijinja::render!(include_str!("templates/unbound.conf.jinja"), use_dnssec => use_dnssec, netmask => netmask) } fn hickory_conf(use_dnssec: bool) -> String { diff --git a/packages/dns-test/src/templates/unbound.conf.jinja b/packages/dns-test/src/templates/unbound.conf.jinja index fe74a6cf..ca5e54d3 100644 --- a/packages/dns-test/src/templates/unbound.conf.jinja +++ b/packages/dns-test/src/templates/unbound.conf.jinja @@ -2,7 +2,7 @@ server: verbosity: 4 use-syslog: no interface: 0.0.0.0 - access-control: 172.17.0.0/16 allow + access-control: {{ netmask }} allow root-hints: /etc/unbound/root.hints {% if use_dnssec %} trust-anchor-file: /etc/trusted-key.key From a4ca3d64231c0a4cba911a5645023f83bdc3ef8f Mon Sep 17 00:00:00 2001 From: Sebastian Ziebell Date: Fri, 16 Feb 2024 13:49:58 +0100 Subject: [PATCH 055/124] Incorporate feedback * add new type `Network` that holds `Arc` * adjust network name to use `CARGO_PKG_NAME` env var, process id and counter * remove function to remove network * clone Network in container * refactor Network tests --- packages/dns-test/src/container.rs | 2 + packages/dns-test/src/container/network.rs | 166 ++++++++++----------- 2 files changed, 82 insertions(+), 86 deletions(-) diff --git a/packages/dns-test/src/container.rs b/packages/dns-test/src/container.rs index 288d7357..b15f0e14 100644 --- a/packages/dns-test/src/container.rs +++ b/packages/dns-test/src/container.rs @@ -67,6 +67,7 @@ impl Container { id, name, ipv4_addr, + _network: network.clone(), }; Ok(Self { inner: Arc::new(inner), @@ -172,6 +173,7 @@ struct Inner { id: String, // TODO probably also want the IPv6 address ipv4_addr: Ipv4Addr, + _network: Network, } /// NOTE unlike `std::process::Child`, the drop implementation of this type will `kill` the diff --git a/packages/dns-test/src/container/network.rs b/packages/dns-test/src/container/network.rs index 730e8f79..0625c6dd 100644 --- a/packages/dns-test/src/container/network.rs +++ b/packages/dns-test/src/container/network.rs @@ -1,25 +1,57 @@ use std::{ - process::{Command, ExitStatus, Stdio}, - sync::atomic::{self, AtomicUsize}, + process::{self, Command, Stdio}, + sync::{ + atomic::{self, AtomicUsize}, + Arc, + }, }; use crate::Result; -const NETWORK_NAME: &str = "dnssec-network"; - /// Represents a network in which to put containers into. -pub struct Network { +#[derive(Clone)] +pub struct Network(Arc); + +impl Network { + /// Returns the name of the network. + pub fn name(&self) -> &str { + self.0.name.as_str() + } + + /// Returns the subnet mask + pub fn netmask(&self) -> &str { + &self.0.config.subnet + } +} + +struct NetworkInner { name: String, config: NetworkConfig, } impl Network { pub fn new() -> Result { - let id = network_count(); - let network_name = format!("{NETWORK_NAME}-{id}"); + let pid = process::id(); + let network_name = env!("CARGO_PKG_NAME"); + Ok(Self(Arc::new(NetworkInner::new(pid, network_name)?))) + } +} - // A network can exist, for example when a test panics - let _ = remove_network(network_name.as_str())?; +/// This ensure the Docker network is deleted after the test runner process ends. +impl Drop for NetworkInner { + fn drop(&mut self) { + let _ = Command::new("docker") + .args(["network", "rm", "--force", self.name.as_str()]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + } +} + +impl NetworkInner { + pub fn new(pid: u32, network_name: &str) -> Result { + let count = network_count(); + let network_name = format!("{network_name}-{pid}-{count}"); let mut command = Command::new("docker"); command @@ -28,13 +60,13 @@ impl Network { .arg(&network_name); // create network - let output = command.output().unwrap(); + let output = command.output()?; 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}" - ); + + if !output.status.success() { + return Err(format!("--- STDOUT ---\n{stdout}\n--- STDERR ---\n{stderr}").into()); + } // inspect & parse network details let config = get_network_config(&network_name)?; @@ -44,16 +76,6 @@ impl Network { config, }) } - - /// Returns the name of the network. - pub fn name(&self) -> &str { - self.name.as_str() - } - - /// Returns the subnet mask - pub fn netmask(&self) -> &str { - &self.config.subnet - } } /// Collects all important configs. @@ -83,64 +105,6 @@ fn get_network_config(network_name: &str) -> Result { Ok(NetworkConfig { subnet }) } -/// This ensure the Docker network is deleted after the test runner process ends. -impl Drop for Network { - fn drop(&mut self) { - let _ = remove_network(&self.name); - } -} - -/// Removes the given network. -fn remove_network(network_name: &str) -> Result { - // Disconnects all attached containers - for container_id in get_attached_containers(network_name)? { - let mut command = Command::new("docker"); - let _ = command - .args(["network", "disconnect", "--force"]) - .args([network_name, container_id.as_str()]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status()?; - } - - // Remove the network - let mut command = Command::new("docker"); - command - .args(["network", "rm", "--force", network_name]) - .stdout(Stdio::null()) - .stderr(Stdio::null()); - Ok(command.status()?) -} - -/// Finds the list of connected containers -fn get_attached_containers(network_name: &str) -> Result> { - let mut command = Command::new("docker"); - command.args([ - "network", - "inspect", - network_name, - "-f", - r#"{{ range $k, $v := .Containers }}{{ printf "%s\n" $k }}{{ end }}"#, - ]); - - let output = command.output()?; - let container_ids = match output.status.success() { - true => { - let container_ids = std::str::from_utf8(&output.stdout)? - .trim() - .to_string() - .lines() - .filter(|line| !line.trim().is_empty()) - .map(|line| line.to_string()) - .collect::>(); - container_ids - } - false => vec![], - }; - - Ok(container_ids) -} - fn network_count() -> usize { static COUNT: AtomicUsize = AtomicUsize::new(1); @@ -153,6 +117,35 @@ mod tests { use super::*; + /// Finds the list of connected containers + fn get_attached_containers(network_name: &str) -> Result> { + let mut command = Command::new("docker"); + command.args([ + "network", + "inspect", + network_name, + "-f", + r#"{{ range $k, $v := .Containers }}{{ printf "%s\n" $k }}{{ end }}"#, + ]); + + let output = command.output()?; + let container_ids = match output.status.success() { + true => { + let container_ids = std::str::from_utf8(&output.stdout)? + .trim() + .to_string() + .lines() + .filter(|line| !line.trim().is_empty()) + .map(|line| line.to_string()) + .collect::>(); + container_ids + } + false => vec![], + }; + + Ok(container_ids) + } + #[test] fn create_works() -> Result<()> { assert!(Network::new().is_ok()); @@ -170,17 +163,18 @@ mod tests { #[test] fn remove_network_works() -> Result<()> { let network = Network::new().expect("Failed to create network"); - let network_name = network.name().to_string(); let nameserver = NameServer::new(FQDN::ROOT, &network)?; let container_ids = get_attached_containers(network.name())?; assert_eq!(1, container_ids.len()); assert_eq!(&[nameserver.container_id().to_string()], &container_ids[..]); - drop(network); + drop(nameserver); - let container_ids = get_attached_containers(&network_name)?; - assert!(container_ids.is_empty()); + let container_ids = get_attached_containers(network.name())?; + assert_eq!(0, container_ids.len()); + + drop(network); Ok(()) } From 014662d21887696a20cc8829229d740b8bb89d5c Mon Sep 17 00:00:00 2001 From: Sebastian Ziebell Date: Fri, 16 Feb 2024 14:31:30 +0100 Subject: [PATCH 056/124] Refactor tests to check network state --- packages/dns-test/src/container/network.rs | 65 ++++++++-------------- 1 file changed, 22 insertions(+), 43 deletions(-) diff --git a/packages/dns-test/src/container/network.rs b/packages/dns-test/src/container/network.rs index 0625c6dd..4199f9ee 100644 --- a/packages/dns-test/src/container/network.rs +++ b/packages/dns-test/src/container/network.rs @@ -113,68 +113,47 @@ fn network_count() -> usize { #[cfg(test)] mod tests { - use crate::{name_server::NameServer, FQDN}; + use crate::{container::Container, Implementation}; use super::*; - /// Finds the list of connected containers - fn get_attached_containers(network_name: &str) -> Result> { + fn exists_network(network_name: &str) -> bool { let mut command = Command::new("docker"); - command.args([ - "network", - "inspect", - network_name, - "-f", - r#"{{ range $k, $v := .Containers }}{{ printf "%s\n" $k }}{{ end }}"#, - ]); + command.args(["network", "ls", "--format={{ .Name }}"]); - let output = command.output()?; - let container_ids = match output.status.success() { - true => { - let container_ids = std::str::from_utf8(&output.stdout)? - .trim() - .to_string() - .lines() - .filter(|line| !line.trim().is_empty()) - .map(|line| line.to_string()) - .collect::>(); - container_ids - } - false => vec![], - }; + let output = command.output().expect("Failed to get output"); + let stdout = String::from_utf8_lossy(&output.stdout); - Ok(container_ids) + stdout + .trim() + .lines() + .find(|line| line == &network_name) + .is_some() } #[test] fn create_works() -> Result<()> { - assert!(Network::new().is_ok()); - Ok(()) - } + let network = Network::new(); + assert!(network.is_ok()); - #[test] - fn network_subnet_works() -> Result<()> { - let network = Network::new().expect("Failed to create network"); - let config = get_network_config(network.name()); - assert!(config.is_ok()); + let network = network.expect("Failed to construct network"); + assert!(exists_network(network.name())); Ok(()) } #[test] fn remove_network_works() -> Result<()> { let network = Network::new().expect("Failed to create network"); - let nameserver = NameServer::new(FQDN::ROOT, &network)?; - - let container_ids = get_attached_containers(network.name())?; - assert_eq!(1, container_ids.len()); - assert_eq!(&[nameserver.container_id().to_string()], &container_ids[..]); - - drop(nameserver); - - let container_ids = get_attached_containers(network.name())?; - assert_eq!(0, container_ids.len()); + let network_name = network.name().to_string(); + let container = + Container::run(Implementation::Unbound, &network).expect("Failed to start container"); + assert!(exists_network(&network_name)); drop(network); + assert!(exists_network(&network_name)); + + drop(container); + assert!(!exists_network(&network_name)); Ok(()) } From e77fd41635da728aa90d0238b66759d35a9d6cf0 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 12 Feb 2024 17:57:39 +0100 Subject: [PATCH 057/124] add eavesdrop API closes #9 --- Cargo.lock | 385 +++++++++++++++++++++++++++ packages/dns-test/Cargo.toml | 3 + packages/dns-test/src/client.rs | 4 + packages/dns-test/src/container.rs | 32 ++- packages/dns-test/src/lib.rs | 1 + packages/dns-test/src/name_server.rs | 6 + packages/dns-test/src/resolver.rs | 5 + packages/dns-test/src/tshark.rs | 344 ++++++++++++++++++++++++ 8 files changed, 774 insertions(+), 6 deletions(-) create mode 100644 packages/dns-test/src/tshark.rs diff --git a/Cargo.lock b/Cargo.lock index 46f2b24b..20877964 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,33 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "bitflags" version = "1.3.2" @@ -14,12 +41,40 @@ version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets", +] + [[package]] name = "conformance-tests" version = "0.1.0" @@ -27,14 +82,74 @@ dependencies = [ "dns-test", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "darling" +version = "0.20.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc5d6b04b3fd0ba9926f945895de7d806260a2d7431ba82e7edaecb043c4c6b8" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e48a959bcd5c761246f5d090ebc2fbf7b9cd527a492b07a67510c108f1e7e3" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1545d67a2149e1d93b7e5c7752dce5a7426eb5d1357ddcfd89336b94444f77" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + [[package]] name = "dns-test" version = "0.1.0" dependencies = [ "minijinja", + "serde", + "serde_json", + "serde_with", "tempfile", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.8" @@ -51,6 +166,96 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "js-sys" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "libc" version = "0.2.153" @@ -63,6 +268,12 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + [[package]] name = "minijinja" version = "1.0.12" @@ -72,6 +283,33 @@ dependencies = [ "serde", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.78" @@ -112,6 +350,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + [[package]] name = "serde" version = "1.0.196" @@ -132,6 +376,53 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15d167997bd841ec232f5b2b8e0e26606df2e7caa4c31b95ea9ca52b200bd270" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.2.3", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865f9743393e638991566a8b7a479043c2c8da94a33e0a31f18214c9cae0a64d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "2.0.48" @@ -156,12 +447,106 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "time" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "wasm-bindgen" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/packages/dns-test/Cargo.toml b/packages/dns-test/Cargo.toml index a9c5e601..0851b3e4 100644 --- a/packages/dns-test/Cargo.toml +++ b/packages/dns-test/Cargo.toml @@ -7,6 +7,9 @@ version = "0.1.0" [dependencies] minijinja = "1.0.12" +serde = { version = "1.0.196", features = ["derive"] } +serde_json = "1.0.113" +serde_with = "3.6.1" tempfile = "3.9.0" [lib] diff --git a/packages/dns-test/src/client.rs b/packages/dns-test/src/client.rs index bfc57b7b..f991b9f0 100644 --- a/packages/dns-test/src/client.rs +++ b/packages/dns-test/src/client.rs @@ -17,6 +17,10 @@ impl Client { }) } + pub fn ipv4_addr(&self) -> Ipv4Addr { + self.inner.ipv4_addr() + } + pub fn delv( &self, server: Ipv4Addr, diff --git a/packages/dns-test/src/container.rs b/packages/dns-test/src/container.rs index b15f0e14..149f6e89 100644 --- a/packages/dns-test/src/container.rs +++ b/packages/dns-test/src/container.rs @@ -3,17 +3,17 @@ mod network; use core::str; use std::fs; use std::net::Ipv4Addr; -use std::process::{self, ExitStatus}; +use std::process::{self, ChildStdout, ExitStatus}; use std::process::{Command, Stdio}; use std::sync::atomic::AtomicUsize; use std::sync::{atomic, Arc}; use tempfile::{NamedTempFile, TempDir}; +pub use crate::container::network::Network; use crate::{Error, Implementation, Result}; -pub use crate::container::network::Network; - +#[derive(Clone)] pub struct Container { inner: Arc, } @@ -52,9 +52,18 @@ impl Container { let count = container_count(); let name = format!("{PACKAGE_NAME}-{implementation}-{pid}-{count}"); command - .args(["run", "--rm", "--detach", "--name", &name]) - .arg("-it") - .args(["--network", network.name()]) + .args([ + "run", + "--rm", + "--detach", + "--cap-add=NET_RAW", + "--cap-add=NET_ADMIN", + "--network", + network.name(), + "--name", + &name, + "-it", + ]) .arg(image_tag) .args(["sleep", "infinity"]); @@ -187,6 +196,17 @@ pub struct Child { } impl Child { + /// Returns a handle to the child's stdout + /// + /// This method will succeed at most once + pub fn stdout(&mut self) -> Result { + Ok(self + .inner + .as_mut() + .and_then(|child| child.stdout.take()) + .ok_or("could not retrieve child's stdout")?) + } + pub fn wait(mut self) -> Result { let output = self.inner.take().expect("unreachable").wait_with_output()?; output.try_into() diff --git a/packages/dns-test/src/lib.rs b/packages/dns-test/src/lib.rs index 18a625d7..6c9d32e8 100644 --- a/packages/dns-test/src/lib.rs +++ b/packages/dns-test/src/lib.rs @@ -18,6 +18,7 @@ pub mod name_server; pub mod record; mod resolver; mod trust_anchor; +pub mod tshark; pub mod zone_file; #[derive(Clone, Copy)] diff --git a/packages/dns-test/src/name_server.rs b/packages/dns-test/src/name_server.rs index 1189b9cf..66bbb1bf 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -2,6 +2,7 @@ use core::sync::atomic::{self, AtomicUsize}; use std::net::Ipv4Addr; use crate::container::{Child, Container, Network}; +use crate::tshark::Tshark; use crate::zone_file::{self, SoaSettings, ZoneFile, DNSKEY, DS}; use crate::{Implementation, Result, FQDN}; @@ -214,6 +215,11 @@ impl<'a> NameServer<'a, Signed> { } impl<'a> NameServer<'a, Running> { + /// Starts a `tshark` instance that captures DNS messages flowing through this network node + pub fn eavesdrop(&self) -> Result { + self.container.eavesdrop() + } + /// gracefully terminates the name server collecting all logs pub fn terminate(self) -> Result { let pidfile = "/run/nsd/nsd.pid"; diff --git a/packages/dns-test/src/resolver.rs b/packages/dns-test/src/resolver.rs index 3a56d11a..02e40721 100644 --- a/packages/dns-test/src/resolver.rs +++ b/packages/dns-test/src/resolver.rs @@ -3,6 +3,7 @@ use std::net::Ipv4Addr; use crate::container::{Child, Container, Network}; use crate::trust_anchor::TrustAnchor; +use crate::tshark::Tshark; use crate::zone_file::Root; use crate::{Implementation, Result}; @@ -72,6 +73,10 @@ impl Resolver { Ok(Self { child, container }) } + pub fn eavesdrop(&self) -> Result { + self.container.eavesdrop() + } + pub fn ipv4_addr(&self) -> Ipv4Addr { self.container.ipv4_addr() } diff --git a/packages/dns-test/src/tshark.rs b/packages/dns-test/src/tshark.rs new file mode 100644 index 00000000..ee61b1df --- /dev/null +++ b/packages/dns-test/src/tshark.rs @@ -0,0 +1,344 @@ +//! `tshark` JSON output parser + +use core::result::Result as CoreResult; +use std::io::{BufRead, BufReader, Lines}; +use std::net::Ipv4Addr; +use std::process::ChildStdout; +use std::sync::atomic::{self, AtomicUsize}; + +use serde::Deserialize; +use serde_with::{serde_as, DisplayFromStr}; + +use crate::container::{Child, Container}; +use crate::Result; + +static ID: AtomicUsize = AtomicUsize::new(0); + +const UDP_PORT: u16 = 53; + +impl Container { + pub fn eavesdrop(&self) -> Result { + let id = ID.fetch_add(1, atomic::Ordering::Relaxed); + let pidfile = pid_file(id); + let capture_file = capture_file(id); + + // `docker exec $child` merges the child's stderr and stdout streams and pipes them into + // its stdout. as we cannot tell stdout (JSON) from stderr (log message) from the host side, + // we'll redirect the JSON output to a file inside the container and read the log messages + // from the host side + // --log-level info --log-domain main + let tshark = format!( + "echo $$ > {pidfile} +exec tshark --log-level debug --log-domain main,capture -l -i eth0 -T json -O dns -f 'udp port {UDP_PORT}' > {capture_file}" + ); + let mut child = self.spawn(&["sh", "-c", &tshark])?; + + let stdout = child.stdout()?; + let mut stdout = BufReader::new(stdout).lines(); + + for res in stdout.by_ref() { + let line = res?; + + if line.contains("Capture started") { + break; + } + } + + Ok(Tshark { + container: self.clone(), + child, + stdout, + id, + }) + } +} + +fn pid_file(id: usize) -> String { + format!("/tmp/tshark{id}.pid") +} + +fn capture_file(id: usize) -> String { + format!("/tmp/tshark{id}.json") +} + +pub struct Tshark { + child: Child, + container: Container, + id: usize, + stdout: Lines>, +} + +impl Tshark { + /// Blocks until `tshark` reports that it has captured new DNS messages + /// + /// This method returns the number of newly captured messages + // XXX maybe do this automatically / always in `terminate`? + pub fn wait_for_capture(&mut self) -> Result { + // sync_pipe_input_cb(): new packets NN + for res in self.stdout.by_ref() { + let line = res?; + + if line.contains(": new packets ") { + let (_rest, count) = line.rsplit_once(' ').unwrap(); + return Ok(count.parse()?); + } + } + + Err("unexpected EOF".into()) + } + + pub fn terminate(self) -> Result> { + let pidfile = pid_file(self.id); + let kill = format!("test -f {pidfile} || sleep 1; kill $(cat {pidfile})"); + + self.container.status_ok(&["sh", "-c", &kill])?; + let output = self.child.wait()?; + + if !output.status.success() { + return Err("could not terminate the `tshark` process".into()); + } + + // wait until the message "NN packets captured" appears + // wireshark will close stderr after printing that so exhausting + // the file descriptor produces the same result + for res in self.stdout { + res?; + } + + let capture_file = capture_file(self.id); + let output = self.container.stdout(&["cat", &capture_file])?; + + let mut messages = vec![]; + let entries: Vec = serde_json::from_str(&output)?; + + let own_addr = self.container.ipv4_addr(); + for entry in entries { + let Layers { ip, dns } = entry._source.layers; + + let direction = if ip.dst == own_addr { + Direction::Incoming { source: ip.src } + } else if ip.src == own_addr { + Direction::Outgoing { + destination: ip.dst, + } + } else { + return Err( + format!("unexpected IP packet found in wireshark trace: {ip:?}").into(), + ); + }; + + messages.push(Message { + contents: dns, + direction, + }); + } + + Ok(messages) + } +} + +#[derive(Debug)] +pub struct Message { + // TODO this should be more "cooked", i.e. be deserialized into a `struct` + pub contents: serde_json::Value, + pub direction: Direction, +} + +#[derive(Clone, Copy, Debug)] +pub enum Direction { + Incoming { source: Ipv4Addr }, + Outgoing { destination: Ipv4Addr }, +} + +impl Direction { + pub fn try_into_incoming(self) -> CoreResult { + if let Self::Incoming { source } = self { + Ok(source) + } else { + Err(self) + } + } + + pub fn try_into_outgoing(self) -> CoreResult { + if let Self::Outgoing { destination } = self { + Ok(destination) + } else { + Err(self) + } + } +} + +#[derive(Deserialize)] +struct Entry { + _source: Source, +} + +#[derive(Deserialize)] +struct Source { + layers: Layers, +} + +#[derive(Deserialize)] +struct Layers { + ip: Ip, + dns: serde_json::Value, +} + +#[serde_as] +#[derive(Debug, Deserialize)] +struct Ip { + #[serde(rename = "ip.src")] + #[serde_as(as = "DisplayFromStr")] + src: Ipv4Addr, + + #[serde(rename = "ip.dst")] + #[serde_as(as = "DisplayFromStr")] + dst: Ipv4Addr, +} + +#[cfg(test)] +mod tests { + use crate::client::{Client, Dnssec, Recurse}; + use crate::name_server::NameServer; + use crate::record::RecordType; + use crate::zone_file::Root; + use crate::{Implementation, Network, Resolver, TrustAnchor, FQDN}; + + use super::*; + + #[test] + fn nameserver() -> Result<()> { + let network = &Network::new()?; + let ns = NameServer::new(FQDN::ROOT, network)?.start()?; + let mut tshark = ns.eavesdrop()?; + + let client = Client::new(network)?; + let resp = client.dig( + Recurse::No, + Dnssec::No, + ns.ipv4_addr(), + RecordType::SOA, + &FQDN::ROOT, + )?; + + assert!(resp.status.is_noerror()); + + let captured = tshark.wait_for_capture()?; + assert_eq!(2, captured); + + let messages = tshark.terminate()?; + + let [first, second] = messages.try_into().expect("2 DNS messages"); + assert_eq!( + client.ipv4_addr(), + first.direction.try_into_incoming().unwrap() + ); + + assert_eq!( + client.ipv4_addr(), + second.direction.try_into_outgoing().unwrap() + ); + + Ok(()) + } + + #[test] + fn resolver() -> Result<()> { + let network = &Network::new()?; + let mut root_ns = NameServer::new(FQDN::ROOT, network)?; + let mut com_ns = NameServer::new(FQDN::COM, network)?; + + let mut nameservers_ns = NameServer::new(FQDN("nameservers.com.")?, network)?; + 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( + Implementation::Unbound, + roots, + &TrustAnchor::empty(), + network, + )?; + let mut tshark = resolver.eavesdrop()?; + let resolver_addr = resolver.ipv4_addr(); + + let client = Client::new(network)?; + let output = client.dig( + Recurse::Yes, + Dnssec::No, + dbg!(resolver_addr), + RecordType::A, + root_ns.fqdn(), + )?; + + assert!(output.status.is_noerror()); + + let count = tshark.wait_for_capture()?; + dbg!(count); + + let messages = tshark.terminate()?; + assert!(messages.len() > 2); + + let ns_addrs = dbg!([ + root_ns.ipv4_addr(), + com_ns.ipv4_addr(), + nameservers_ns.ipv4_addr(), + ]); + let client_addr = dbg!(client.ipv4_addr()); + + let mut from_client_count = 0; + let mut to_client_count = 0; + let mut to_ns_count = 0; + let mut from_ns_count = 0; + for message in messages { + match message.direction { + Direction::Incoming { source } => { + if source == client_addr { + from_client_count += 1; + } else if ns_addrs.contains(&source) { + from_ns_count += 1; + } else { + panic!( + "found packet coming from {source} which is outside the network graph" + ) + } + } + + Direction::Outgoing { destination } => { + if destination == client_addr { + to_client_count += 1; + } else if ns_addrs.contains(&destination) { + to_ns_count += 1; + } else { + panic!( + "found packet going to {destination} which is outside the network graph" + ) + } + } + } + } + + // query from client (dig) + assert_eq!(1, from_client_count); + + // answer to client (dig) + assert_eq!(1, to_client_count); + + // check that all queries sent to nameservers were answered + assert_eq!(to_ns_count, from_ns_count); + + Ok(()) + } +} From 438af313404e1faa201a6ff3497acb8a79026bf7 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 12 Feb 2024 18:53:39 +0100 Subject: [PATCH 058/124] Tshark: newtype the json::Value & add some getters --- packages/dns-test/src/tshark.rs | 56 ++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/packages/dns-test/src/tshark.rs b/packages/dns-test/src/tshark.rs index ee61b1df..d07fe7eb 100644 --- a/packages/dns-test/src/tshark.rs +++ b/packages/dns-test/src/tshark.rs @@ -87,7 +87,7 @@ impl Tshark { Err("unexpected EOF".into()) } - pub fn terminate(self) -> Result> { + pub fn terminate(self) -> Result> { let pidfile = pid_file(self.id); let kill = format!("test -f {pidfile} || sleep 1; kill $(cat {pidfile})"); @@ -127,8 +127,8 @@ impl Tshark { ); }; - messages.push(Message { - contents: dns, + messages.push(Capture { + message: Message { inner: dns }, direction, }); } @@ -137,11 +137,57 @@ impl Tshark { } } +#[derive(Debug)] +pub struct Capture { + pub message: Message, + pub direction: Direction, +} + #[derive(Debug)] pub struct Message { // TODO this should be more "cooked", i.e. be deserialized into a `struct` - pub contents: serde_json::Value, - pub direction: Direction, + inner: serde_json::Value, +} + +impl Message { + /// Returns `true` if the DO bit is set + /// + /// Returns `None` if there's no OPT pseudo-RR + pub fn is_do_bit_set(&self) -> Option { + let do_bit = match self + .opt_record()? + .get("dns.resp.z_tree")? + .get("dns.resp.z.do")? + .as_str()? + { + "1" => true, + "0" => false, + _ => return None, + }; + + Some(do_bit) + } + + /// Returns the "sender's UDP payload size" field in the OPT pseudo-RR + /// + /// Returns `None` if there's no OPT record present + pub fn udp_payload_size(&self) -> Option { + self.opt_record()? + .get("dns.rr.udp_payload_size")? + .as_str()? + .parse() + .ok() + } + + fn opt_record(&self) -> Option<&serde_json::Value> { + for (key, value) in self.inner.get("Additional records")?.as_object()? { + if key.ends_with(": type OPT") { + return Some(value); + } + } + + None + } } #[derive(Clone, Copy, Debug)] From 156e005ff2c9f768e06a1a610fde9ee3ae54bc9b Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 12 Feb 2024 18:57:44 +0100 Subject: [PATCH 059/124] RFC4035: test EDNS support --- .../conformance-tests/src/resolver/dnssec.rs | 1 + .../src/resolver/dnssec/rfc4035.rs | 1 + .../src/resolver/dnssec/rfc4035/section_4.rs | 1 + .../dnssec/rfc4035/section_4/section_4_1.rs | 54 +++++++++++++++++++ packages/dns-test/src/client.rs | 5 ++ 5 files changed, 62 insertions(+) create mode 100644 packages/conformance-tests/src/resolver/dnssec/rfc4035.rs create mode 100644 packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4.rs create mode 100644 packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs diff --git a/packages/conformance-tests/src/resolver/dnssec.rs b/packages/conformance-tests/src/resolver/dnssec.rs index 63400356..f408ef38 100644 --- a/packages/conformance-tests/src/resolver/dnssec.rs +++ b/packages/conformance-tests/src/resolver/dnssec.rs @@ -1,3 +1,4 @@ //! DNSSEC functionality +mod rfc4035; mod scenarios; diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035.rs new file mode 100644 index 00000000..289eace0 --- /dev/null +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035.rs @@ -0,0 +1 @@ +mod section_4; diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4.rs new file mode 100644 index 00000000..5779d78f --- /dev/null +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4.rs @@ -0,0 +1 @@ +mod section_4_1; diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs new file mode 100644 index 00000000..533e31b3 --- /dev/null +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs @@ -0,0 +1,54 @@ +use dns_test::client::{Client, Dnssec, Recurse}; +use dns_test::name_server::NameServer; +use dns_test::record::RecordType; +use dns_test::tshark::{Capture, Direction}; +use dns_test::zone_file::Root; +use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; + +#[test] +#[ignore] +fn edns_support() -> Result<()> { + let network = &Network::new()?; + let ns = NameServer::new(FQDN::ROOT, network)?.start()?; + let resolver = Resolver::start( + dns_test::subject(), + &[Root::new(ns.fqdn().clone(), ns.ipv4_addr())], + &TrustAnchor::empty(), + network, + )?; + + let mut tshark = resolver.eavesdrop()?; + + let client = Client::new(network)?; + let ans = client.dig( + Recurse::Yes, + Dnssec::Yes, + resolver.ipv4_addr(), + RecordType::SOA, + &FQDN::ROOT, + )?; + assert!(ans.status.is_servfail()); + + tshark.wait_for_capture()?; + + let captures = tshark.terminate()?; + + let ns_addr = ns.ipv4_addr(); + for Capture { message, direction } in captures { + if let Direction::Outgoing { destination } = direction { + if destination == client.ipv4_addr() { + continue; + } + + // sanity check + assert_eq!(ns_addr, destination); + + if destination == ns_addr { + assert_eq!(Some(true), message.is_do_bit_set()); + assert!(message.udp_payload_size().unwrap() >= 1220); + } + } + } + + Ok(()) +} diff --git a/packages/dns-test/src/client.rs b/packages/dns-test/src/client.rs index f991b9f0..2119f24b 100644 --- a/packages/dns-test/src/client.rs +++ b/packages/dns-test/src/client.rs @@ -237,6 +237,11 @@ impl DigStatus { pub fn is_nxdomain(&self) -> bool { matches!(self, Self::NXDOMAIN) } + + #[must_use] + pub fn is_servfail(&self) -> bool { + matches!(self, Self::SERVFAIL) + } } impl FromStr for DigStatus { From 0e2b35699ccf7b69f16db596712a25f4dbc41f66 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 16 Feb 2024 13:38:27 +0100 Subject: [PATCH 060/124] add mechanism to make `docker build` visible set the DNS_TEST_VERBOSE_DOCKER_BUILD env var to 1 to make the `docker build` command print its output to the console --- packages/dns-test/src/container.rs | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/dns-test/src/container.rs b/packages/dns-test/src/container.rs index 149f6e89..0008344b 100644 --- a/packages/dns-test/src/container.rs +++ b/packages/dns-test/src/container.rs @@ -1,12 +1,12 @@ mod network; use core::str; -use std::fs; use std::net::Ipv4Addr; use std::process::{self, ChildStdout, ExitStatus}; use std::process::{Command, Stdio}; use std::sync::atomic::AtomicUsize; use std::sync::{atomic, Arc}; +use std::{env, fs}; use tempfile::{NamedTempFile, TempDir}; @@ -38,13 +38,7 @@ impl Container { .arg(docker_build_dir); 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}" - ); + exec_or_panic(&mut command, verbose_docker_build()); }); let mut command = Command::new("docker"); @@ -171,6 +165,25 @@ impl Container { } } +fn verbose_docker_build() -> bool { + env::var("DNS_TEST_VERBOSE_DOCKER_BUILD").as_deref() == Ok("1") +} + +fn exec_or_panic(command: &mut Command, verbose: bool) { + if verbose { + let status = command.status().unwrap(); + assert!(status.success()); + } else { + 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}" + ); + } +} + fn container_count() -> usize { static COUNT: AtomicUsize = AtomicUsize::new(0); From 55184172e3e9b7e8f1326515c7dfe1478275eee8 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 16 Feb 2024 13:40:07 +0100 Subject: [PATCH 061/124] build hickory from a local/remote git source DNS_TEST_SUBJECT now needs to contain the URL to the hickory source code --- packages/dns-test/src/client.rs | 2 +- packages/dns-test/src/container.rs | 27 ++++++++++++++--- packages/dns-test/src/container/network.rs | 2 +- .../dns-test/src/docker/hickory.Dockerfile | 3 +- packages/dns-test/src/lib.rs | 29 +++++++++++++------ packages/dns-test/src/name_server.rs | 2 +- packages/dns-test/src/resolver.rs | 6 ++-- 7 files changed, 51 insertions(+), 20 deletions(-) diff --git a/packages/dns-test/src/client.rs b/packages/dns-test/src/client.rs index 2119f24b..b66b73ca 100644 --- a/packages/dns-test/src/client.rs +++ b/packages/dns-test/src/client.rs @@ -13,7 +13,7 @@ pub struct Client { impl Client { pub fn new(network: &Network) -> Result { Ok(Self { - inner: Container::run(Implementation::Unbound, network)?, + inner: Container::run(&Implementation::Unbound, network)?, }) } diff --git a/packages/dns-test/src/container.rs b/packages/dns-test/src/container.rs index 0008344b..f1ef2277 100644 --- a/packages/dns-test/src/container.rs +++ b/packages/dns-test/src/container.rs @@ -22,7 +22,7 @@ const PACKAGE_NAME: &str = env!("CARGO_PKG_NAME"); impl Container { /// Starts the container in a "parked" state - pub fn run(implementation: Implementation, network: &Network) -> Result { + pub fn run(implementation: &Implementation, network: &Network) -> Result { // TODO make this configurable and support hickory & bind let dockerfile = implementation.dockerfile(); let docker_build_dir = TempDir::new()?; @@ -37,7 +37,26 @@ impl Container { .arg(&image_tag) .arg(docker_build_dir); + let srcdir = if let Implementation::Hickory { url } = implementation { + Some(url) + } else { + None + }; + implementation.once().call_once(|| { + if let Some(srcdir) = srcdir { + let mut cp_r = Command::new("git"); + cp_r.args([ + "clone", + "--depth", + "1", + srcdir, + &docker_build_dir.join("src").display().to_string(), + ]); + + exec_or_panic(&mut cp_r, false); + } + exec_or_panic(&mut command, verbose_docker_build()); }); @@ -312,7 +331,7 @@ mod tests { #[test] fn run_works() -> Result<()> { let network = Network::new()?; - let container = Container::run(Implementation::Unbound, &network)?; + let container = Container::run(&Implementation::Unbound, &network)?; let output = container.output(&["true"])?; assert!(output.status.success()); @@ -323,7 +342,7 @@ mod tests { #[test] fn ipv4_addr_works() -> Result<()> { let network = Network::new()?; - let container = Container::run(Implementation::Unbound, &network)?; + let container = Container::run(&Implementation::Unbound, &network)?; let ipv4_addr = container.ipv4_addr(); let output = container.output(&["ping", "-c1", &format!("{ipv4_addr}")])?; @@ -335,7 +354,7 @@ mod tests { #[test] fn cp_works() -> Result<()> { let network = Network::new()?; - let container = Container::run(Implementation::Unbound, &network)?; + let container = Container::run(&Implementation::Unbound, &network)?; let path = "/tmp/somefile"; let contents = "hello"; diff --git a/packages/dns-test/src/container/network.rs b/packages/dns-test/src/container/network.rs index 4199f9ee..7e9f81b1 100644 --- a/packages/dns-test/src/container/network.rs +++ b/packages/dns-test/src/container/network.rs @@ -146,7 +146,7 @@ mod tests { let network = Network::new().expect("Failed to create network"); let network_name = network.name().to_string(); let container = - Container::run(Implementation::Unbound, &network).expect("Failed to start container"); + Container::run(&Implementation::Unbound, &network).expect("Failed to start container"); assert!(exists_network(&network_name)); drop(network); diff --git a/packages/dns-test/src/docker/hickory.Dockerfile b/packages/dns-test/src/docker/hickory.Dockerfile index d2edd1a9..4cedaf46 100644 --- a/packages/dns-test/src/docker/hickory.Dockerfile +++ b/packages/dns-test/src/docker/hickory.Dockerfile @@ -4,5 +4,6 @@ RUN apt-get update && \ apt-get install -y \ tshark -RUN cargo install hickory-dns --version 0.24.0 --features recursor --debug +COPY ./src /usr/src/hickory +RUN cargo install --path /usr/src/hickory/bin --features recursor --debug env RUST_LOG=debug diff --git a/packages/dns-test/src/lib.rs b/packages/dns-test/src/lib.rs index 6c9d32e8..82d90851 100644 --- a/packages/dns-test/src/lib.rs +++ b/packages/dns-test/src/lib.rs @@ -21,17 +21,17 @@ mod trust_anchor; pub mod tshark; pub mod zone_file; -#[derive(Clone, Copy)] +#[derive(Clone)] pub enum Implementation { Unbound, - Hickory, + Hickory { url: String }, } impl Implementation { fn dockerfile(&self) -> &'static str { match self { Implementation::Unbound => include_str!("docker/unbound.Dockerfile"), - Implementation::Hickory => include_str!("docker/hickory.Dockerfile"), + Implementation::Hickory { .. } => include_str!("docker/hickory.Dockerfile"), } } @@ -41,7 +41,8 @@ impl Implementation { static UNBOUND_ONCE: Once = Once::new(); &UNBOUND_ONCE } - Implementation::Hickory => { + + Implementation::Hickory { .. } => { static HICKORY_ONCE: Once = Once::new(); &HICKORY_ONCE } @@ -59,7 +60,7 @@ impl fmt::Display for Implementation { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s = match self { Implementation::Unbound => "unbound", - Implementation::Hickory => "hickory", + Implementation::Hickory { .. } => "hickory", }; f.write_str(s) } @@ -67,10 +68,20 @@ impl fmt::Display for Implementation { 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}"), + if subject == "unbound" { + return Implementation::Unbound; + } + + if subject.starts_with("hickory") { + if let Some(url) = subject.strip_prefix("hickory ") { + Implementation::Hickory { + url: url.to_string(), + } + } else { + panic!("the syntax of DNS_TEST_SUBJECT is 'hickory $URL', e.g. 'hickory /tmp/hickory' or 'hickory https://github.com/owner/repo'") + } + } else { + panic!("unknown implementation: {subject}") } } else { Implementation::default() diff --git a/packages/dns-test/src/name_server.rs b/packages/dns-test/src/name_server.rs index 66bbb1bf..8033b676 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -43,7 +43,7 @@ impl<'a> NameServer<'a, Stopped> { }); Ok(Self { - container: Container::run(Implementation::Unbound, network)?, + container: Container::run(&Implementation::Unbound, network)?, zone_file, state: Stopped, }) diff --git a/packages/dns-test/src/resolver.rs b/packages/dns-test/src/resolver.rs index 02e40721..091e59b1 100644 --- a/packages/dns-test/src/resolver.rs +++ b/packages/dns-test/src/resolver.rs @@ -33,7 +33,7 @@ impl Resolver { "must configure at least one local root server" ); - let container = Container::run(implementation, network)?; + let container = Container::run(&implementation, network)?; let mut hints = String::new(); for root in roots { @@ -51,7 +51,7 @@ impl Resolver { )?; } - Implementation::Hickory => { + Implementation::Hickory { .. } => { container.status_ok(&["mkdir", "-p", "/etc/hickory"])?; container.cp("/etc/hickory/root.hints", &hints)?; @@ -66,7 +66,7 @@ impl Resolver { let command: &[_] = match implementation { Implementation::Unbound => &["unbound", "-d"], - Implementation::Hickory => &["hickory-dns", "-d"], + Implementation::Hickory { .. } => &["hickory-dns", "-d"], }; let child = container.spawn(command)?; From 10351a70211e3c785300d47d15e9a932abdd2e07 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 16 Feb 2024 13:43:57 +0100 Subject: [PATCH 062/124] ci: build hickory from a pinned git commit --- .github/workflows/ci.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 963da5c2..9307cf05 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,9 @@ on: branches: [main] merge_group: +env: + HICKORY_REV: a3669bd80f3f7b97f0c301c15f1cba6368d97b63 + jobs: ci: name: Continuous Integration @@ -27,7 +30,10 @@ jobs: run: cargo test -p conformance-tests -- --include-ignored - name: Run tests against hickory - run: DNS_TEST_SUBJECT=hickory cargo test -p conformance-tests + run: | + git clone https://github.com/hickory-dns/hickory-dns /tmp/hickory + ( cd /tmp/hickory && git reset --hard ${{ env.HICKORY_REV }} ) + DNS_TEST_SUBJECT="hickory /tmp/hickory" cargo test -p conformance-tests - name: Check that ignored tests fail with hickory run: | From 9bfa4c5c71d11c04da821beb3bddbce96201f54d Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 16 Feb 2024 13:50:32 +0100 Subject: [PATCH 063/124] ci: make docker build verbose --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9307cf05..5b47ca70 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,7 @@ on: env: HICKORY_REV: a3669bd80f3f7b97f0c301c15f1cba6368d97b63 + DNS_TEST_VERBOSE_DOCKER_BUILD: 1 jobs: ci: From 0afae4f0425515985505674fa651f8568e8c923f Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 16 Feb 2024 13:56:49 +0100 Subject: [PATCH 064/124] don't include the .git directory in the context send to `docker build` without this `DNS_TEST_SUBJECT="hickory $URL" cargo t` was re-building hickory from scratch even when its source had not change not including the `.git` directory in the hickory repository lets Docker see that the source code has not changed --- packages/dns-test/src/container.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/dns-test/src/container.rs b/packages/dns-test/src/container.rs index f1ef2277..449f81d8 100644 --- a/packages/dns-test/src/container.rs +++ b/packages/dns-test/src/container.rs @@ -57,6 +57,9 @@ impl Container { exec_or_panic(&mut cp_r, false); } + fs::write(docker_build_dir.join(".dockerignore"), "src/.git") + .expect("could not create .dockerignore file"); + exec_or_panic(&mut command, verbose_docker_build()); }); From 1429b6bedf438804a8c55eb32fb9d3120ba3c0ef Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 16 Feb 2024 14:05:16 +0100 Subject: [PATCH 065/124] ci: fix syntax of DNS_TEST_SUBJECT --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b47ca70..f6e9e86c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: - name: Check that ignored tests fail with hickory run: | tmpfile="$(mktemp)" - DNS_TEST_SUBJECT=hickory cargo test -p conformance-tests -- --ignored | tee "$tmpfile" + DNS_TEST_SUBJECT="hickory /tmp/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 From 1cb7ee40fe247a4c92307e3046ed11ac9348c28a Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 16 Feb 2024 15:00:49 +0100 Subject: [PATCH 066/124] perform some validation of the repository argument --- Cargo.lock | 67 ++++++++++++++++++++++++++++++ packages/dns-test/Cargo.toml | 1 + packages/dns-test/src/container.rs | 8 ++-- packages/dns-test/src/lib.rs | 36 ++++++++++++++-- 4 files changed, 104 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 20877964..35d020f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,6 +142,7 @@ dependencies = [ "serde_json", "serde_with", "tempfile", + "url", ] [[package]] @@ -172,6 +173,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -219,6 +229,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -304,6 +324,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + [[package]] name = "powerfmt" version = "0.2.0" @@ -478,12 +504,53 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "wasm-bindgen" version = "0.2.91" diff --git a/packages/dns-test/Cargo.toml b/packages/dns-test/Cargo.toml index 0851b3e4..20b13d33 100644 --- a/packages/dns-test/Cargo.toml +++ b/packages/dns-test/Cargo.toml @@ -11,6 +11,7 @@ serde = { version = "1.0.196", features = ["derive"] } serde_json = "1.0.113" serde_with = "3.6.1" tempfile = "3.9.0" +url = "2.5.0" [lib] doctest = false diff --git a/packages/dns-test/src/container.rs b/packages/dns-test/src/container.rs index 449f81d8..92550114 100644 --- a/packages/dns-test/src/container.rs +++ b/packages/dns-test/src/container.rs @@ -37,20 +37,20 @@ impl Container { .arg(&image_tag) .arg(docker_build_dir); - let srcdir = if let Implementation::Hickory { url } = implementation { - Some(url) + let repo = if let Implementation::Hickory(repo) = implementation { + Some(repo) } else { None }; implementation.once().call_once(|| { - if let Some(srcdir) = srcdir { + if let Some(repo) = repo { let mut cp_r = Command::new("git"); cp_r.args([ "clone", "--depth", "1", - srcdir, + repo.as_str(), &docker_build_dir.join("src").display().to_string(), ]); diff --git a/packages/dns-test/src/lib.rs b/packages/dns-test/src/lib.rs index 82d90851..75ea0f58 100644 --- a/packages/dns-test/src/lib.rs +++ b/packages/dns-test/src/lib.rs @@ -1,8 +1,12 @@ //! A test framework for all things DNS use core::fmt; +use std::borrow::Cow; +use std::path::Path; use std::sync::Once; +use url::Url; + pub use crate::container::Network; pub use crate::fqdn::FQDN; pub use crate::resolver::Resolver; @@ -24,7 +28,33 @@ pub mod zone_file; #[derive(Clone)] pub enum Implementation { Unbound, - Hickory { url: String }, + Hickory(Repository<'static>), +} + +#[derive(Clone)] +pub struct Repository<'a> { + inner: Cow<'a, str>, +} + +impl Repository<'_> { + fn as_str(&self) -> &str { + &self.inner + } +} + +/// checks that `input` looks like a valid repository which can be either local or remote +/// +/// # Panics +/// +/// this function panics if `input` is not a local `Path` that exists or a well-formed URL +#[allow(non_snake_case)] +pub fn Repository(input: impl Into>) -> Repository<'static> { + let input = input.into(); + assert!( + Path::new(&*input).exists() || Url::parse(&input).is_ok(), + "{input} is not a valid repository" + ); + Repository { inner: input } } impl Implementation { @@ -74,9 +104,7 @@ pub fn subject() -> Implementation { if subject.starts_with("hickory") { if let Some(url) = subject.strip_prefix("hickory ") { - Implementation::Hickory { - url: url.to_string(), - } + Implementation::Hickory(Repository(url.to_string())) } else { panic!("the syntax of DNS_TEST_SUBJECT is 'hickory $URL', e.g. 'hickory /tmp/hickory' or 'hickory https://github.com/owner/repo'") } From 709a8406b1163507dc0792d7b6a56014ff3d4ad2 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 16 Feb 2024 15:01:06 +0100 Subject: [PATCH 067/124] document what ./src refers to in hickory.Dockerfile --- packages/dns-test/src/docker/hickory.Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/dns-test/src/docker/hickory.Dockerfile b/packages/dns-test/src/docker/hickory.Dockerfile index 4cedaf46..358b7a93 100644 --- a/packages/dns-test/src/docker/hickory.Dockerfile +++ b/packages/dns-test/src/docker/hickory.Dockerfile @@ -4,6 +4,9 @@ RUN apt-get update && \ apt-get install -y \ tshark +# `dns-test` will invoke `docker build` from a temporary directory that contains +# a clone of the hickory repository. `./src` here refers to that clone; not to +# any directory inside the `dns-test` repository COPY ./src /usr/src/hickory RUN cargo install --path /usr/src/hickory/bin --features recursor --debug env RUST_LOG=debug From 86284cce4b5aa39ca3b0c761c002fc45334c7a80 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 20 Feb 2024 15:59:52 +0100 Subject: [PATCH 068/124] add `explore` example --- Cargo.lock | 22 +++++ packages/dns-test/Cargo.toml | 3 + packages/dns-test/examples/explore.rs | 116 ++++++++++++++++++++++++++ packages/dns-test/src/client.rs | 4 + packages/dns-test/src/name_server.rs | 8 +- packages/dns-test/src/resolver.rs | 4 + 6 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 packages/dns-test/examples/explore.rs diff --git a/Cargo.lock b/Cargo.lock index 35d020f5..2a3639b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,6 +88,16 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "ctrlc" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b467862cc8610ca6fc9a1532d7777cee0804e678ab45410897b9396495994a0b" +dependencies = [ + "nix", + "windows-sys", +] + [[package]] name = "darling" version = "0.20.5" @@ -137,6 +147,7 @@ dependencies = [ name = "dns-test" version = "0.1.0" dependencies = [ + "ctrlc", "minijinja", "serde", "serde_json", @@ -303,6 +314,17 @@ dependencies = [ "serde", ] +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.4.2", + "cfg-if", + "libc", +] + [[package]] name = "num-conv" version = "0.1.0" diff --git a/packages/dns-test/Cargo.toml b/packages/dns-test/Cargo.toml index 20b13d33..7dc0924a 100644 --- a/packages/dns-test/Cargo.toml +++ b/packages/dns-test/Cargo.toml @@ -15,3 +15,6 @@ url = "2.5.0" [lib] doctest = false + +[dev-dependencies] +ctrlc = "3.4.2" diff --git a/packages/dns-test/examples/explore.rs b/packages/dns-test/examples/explore.rs new file mode 100644 index 00000000..112c211d --- /dev/null +++ b/packages/dns-test/examples/explore.rs @@ -0,0 +1,116 @@ +use std::sync::mpsc; + +use dns_test::client::Client; +use dns_test::name_server::NameServer; +use dns_test::record::RecordType; +use dns_test::zone_file::Root; +use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; + +fn main() -> Result<()> { + let network = Network::new()?; + + println!("building docker image..."); + let mut root_ns = NameServer::new(FQDN::ROOT, &network)?; + println!("DONE"); + + println!("setting up name servers..."); + let mut com_ns = NameServer::new(FQDN::COM, &network)?; + + let mut nameservers_ns = NameServer::new(FQDN("nameservers.com.")?, &network)?; + 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.sign()?; + let nameservers_ds = nameservers_ns.ds().clone(); + let nameservers_ns = nameservers_ns.start()?; + + 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()?; + + 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(); + + let root_ns = root_ns.start()?; + + let roots = &[Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr())]; + println!("DONE"); + + let trust_anchor = TrustAnchor::from_iter([root_ksk.clone(), root_zsk.clone()]); + println!("building docker image..."); + let resolver = Resolver::start(dns_test::subject(), roots, &trust_anchor, &network)?; + println!("DONE\n\n"); + + let resolver_addr = resolver.ipv4_addr(); + let client = Client::new(&network)?; + // generate `/etc/bind.keys` + client.delv(resolver_addr, RecordType::SOA, &FQDN::ROOT, &trust_anchor)?; + + let (tx, rx) = mpsc::channel(); + + ctrlc::set_handler(move || tx.send(()).expect("could not forward signal"))?; + + println!(". (root) name server's IP address: {}", root_ns.ipv4_addr()); + println!( + "attach to this container with: `docker exec -it {} bash`\n", + root_ns.container_id() + ); + + println!("com. name server's IP address: {}", com_ns.ipv4_addr()); + println!( + "attach to this container with: `docker exec -it {} bash`\n", + com_ns.container_id() + ); + + println!( + "nameservers.com. name server's IP address: {}", + nameservers_ns.ipv4_addr() + ); + println!( + "attach to this container with: `docker exec -it {} bash`\n", + nameservers_ns.container_id() + ); + + println!("resolver's IP address: {resolver_addr}"); + println!( + "attach to this container with: `docker exec -it {} bash`\n", + resolver.container_id() + ); + + println!("client's IP address: {}", client.ipv4_addr()); + println!( + "attach to this container with: `docker exec -it {} bash`\n\n", + client.container_id() + ); + + println!("example queries (run these in the client container):\n"); + println!("`dig @{resolver_addr} SOA .`\n"); + println!( + "`delv -a /etc/bind.keys @{resolver_addr} SOA .` (you MUST use the `-a` flag with delv)\n\n" + ); + + println!( + "to print the DNS traffic flowing through the resolver run this command in +the resolver container before performing queries:\n" + ); + println!("`tshark -f 'udp port 53' -O dns`\n\n"); + + println!("press Ctrl+C to take down the network"); + + rx.recv()?; + + println!("\ntaking down network..."); + + Ok(()) +} diff --git a/packages/dns-test/src/client.rs b/packages/dns-test/src/client.rs index b66b73ca..e283498d 100644 --- a/packages/dns-test/src/client.rs +++ b/packages/dns-test/src/client.rs @@ -17,6 +17,10 @@ impl Client { }) } + pub fn container_id(&self) -> &str { + self.inner.id() + } + pub fn ipv4_addr(&self) -> Ipv4Addr { self.inner.ipv4_addr() } diff --git a/packages/dns-test/src/name_server.rs b/packages/dns-test/src/name_server.rs index 8033b676..dc97b89f 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -156,10 +156,6 @@ impl<'a> NameServer<'a, Stopped> { state: Running { child }, }) } - - pub fn container_id(&self) -> &str { - self.container.id() - } } const ZONES_DIR: &str = "/etc/nsd/zones"; @@ -245,6 +241,10 @@ kill -TERM $(cat {pidfile})" } impl<'a, S> NameServer<'a, S> { + pub fn container_id(&self) -> &str { + self.container.id() + } + pub fn ipv4_addr(&self) -> Ipv4Addr { self.container.ipv4_addr() } diff --git a/packages/dns-test/src/resolver.rs b/packages/dns-test/src/resolver.rs index 091e59b1..c6773ab7 100644 --- a/packages/dns-test/src/resolver.rs +++ b/packages/dns-test/src/resolver.rs @@ -77,6 +77,10 @@ impl Resolver { self.container.eavesdrop() } + pub fn container_id(&self) -> &str { + self.container.id() + } + pub fn ipv4_addr(&self) -> Ipv4Addr { self.container.ipv4_addr() } From 59dc60c5c41ed69932abb9e1c2229f0a090ce43f Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 20 Feb 2024 16:36:52 +0100 Subject: [PATCH 069/124] enable verbose `docker build` with any value of DNS_TEST_VERBOSE_DOCKER_BUILD --- packages/dns-test/src/container.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dns-test/src/container.rs b/packages/dns-test/src/container.rs index 92550114..2ada6ca6 100644 --- a/packages/dns-test/src/container.rs +++ b/packages/dns-test/src/container.rs @@ -188,7 +188,7 @@ impl Container { } fn verbose_docker_build() -> bool { - env::var("DNS_TEST_VERBOSE_DOCKER_BUILD").as_deref() == Ok("1") + env::var("DNS_TEST_VERBOSE_DOCKER_BUILD").as_deref().is_ok() } fn exec_or_panic(command: &mut Command, verbose: bool) { From acca2d4f0f49181a9f5727e268be7d1186f7f4a5 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 20 Feb 2024 17:00:30 +0100 Subject: [PATCH 070/124] update README --- README.md | 172 ++++++++++++++++++++---------------------------------- 1 file changed, 64 insertions(+), 108 deletions(-) diff --git a/README.md b/README.md index 8e320a06..2c88f800 100644 --- a/README.md +++ b/README.md @@ -1,145 +1,101 @@ # `dnssec-tests` -Test infrastructure for DNSSEC conformance tests. +This repository contains two packages: -## Design goals +- `dns-test`. This is a test framework (library) for testing DNS implementations. +- `conformance-tests`. This is a collection of DNS, mainly DNSSEC, tests. -- Test MUST not depend on external services like `1.1.1.1` or `8.8.8.8` - - rationale: it must be possible to run tests locally, without internet access -- All nodes in the network must not be the subject under test. - - rationale: test inter-operability with other software like `unbound` and `nsd` -- All test input must be local files or constants - - rationale: tests are self-contained -- +## Requirements -## Minimally working DNSSEC-enabled network +To use the code in this repository you need: -- `.` domain - - name server: `nsd` (`my.root-server.com`) -- TLD domain (`com.`) - - name server: `nsd` (`ns.com`) -- target domain (`example.com.`) - - name server: `nsd` (`ns.example.com`) -- recursive resolver: `unbound` - - configured to use `my.root-server.com` as root server - - configured with a trust anchor: the public key of `my.root-server.com` +- a stable Rust toolchain to build the code +- a working Docker setup that can run *Linux* containers -- the host OS does not need to be Linux -each name server has -- a zone signing key pair -- a key signing key pair -- signed zone files +## `dns-test` -### exploration +This test framework was built with the following design goals and constraints in mind: -Notes: +- Tests must work without access to the internet. That is, tests cannot rely on external services like `1.1.1.1`, `8.8.8.8`, `a.root-servers.net.`, etc. To this effect, each test runs into its own ephemeral network isolated from the internet and from the networks of other tests running concurrently. -- run all containers with ` --cap-add=NET_RAW --cap-add=NET_ADMIN` -- use `docker exec` to run `tshark` on network nodes ( containers ) of interest +- Test code must be decoupled from the API of any DNS implementation. That is, DNS implementation specific details (library/FFI calls, configuration files) must not appear in test code. To this end, interaction with DNS implementations is done at the network level using tools like `dig`, `delv` and `tshark`. -#### `nsd` for root name server +- It must be possible to switch the 'implementation under test' at runtime. In other words, one should not need to recompile the tests to switch the DNS implementation being tested. To this end, the `DNS_TEST_SUBJECT` environment variable is used to switch the DNS implementation that'll be tested. -run: `nsd -d` +### Test drive -- `/etc/nsd/nsd.conf` +To start a small DNS network using the `dns-test` framework run this command and follow the instructions to interact with the DNS network. -``` text -remote-control: - control-enable: no - -zone: - name: . - zonefile: /etc/nsd/zones/main.zone +``` console +$ cargo run --example explore ``` -- `/etc/nsd/zones/main.zone` +By default, this will use `unbound` as the resolver. You can switch the resolver to `hickory-dns` using the `DNS_TEST_SUBJECT` environment variable: -``` text -$ORIGIN . -$TTL 1800 -@ IN SOA primary.root-server.com. admin.root-server.com. ( - 2014080301 - 3600 - 900 - 1209600 - 1800 - ) -@ IN NS primary.root-server.com. - -; referral -com. IN NS primary.tld-server.com. -primary.tld-server.com. IN A 172.17.0.$TLD_NS_IP_ADDRESS +``` shell +$ DNS_TEST_SUBJECT="hickory https://github.com/hickory-dns/hickory-dns" cargo run --example explore ``` -#### `nsd` for the TLD name server +### Environment variables -run: `nsd -d` +- `DNS_TEST_SUBJECT`. This variable controls what the `dns_test::subject` function returns. The variable can contain one of these values: + - `unbound` + - `hickory $REPOSITORY`. where `$REPOSITORY` is a placeholder for git repository. Examples values for `$REPOSITORY`: `https://github.com/hickory-dns/hickory-dns`; `/home/user/git-repos/hickory-dns`. NOTE: when using a local repository, changes that have not been committed, regardless of whether they are staged or not, will **not** be included in the `hickory-dns` build. + +- `DNS_TEST_VERBOSE_DOCKER_BUILD`. Setting this variable prints the output of the `docker build` invocations that the framework does to the console. This is useful to verify that image caching is working; for example if you set `DNS_TEST_SUBJECT` to a local `hickory-dns` repository then consecutively running the `explore` example and/or `conformance-tests` test suite **must** not rebuild `hickory-dns` provided that you have not *committed* any new change to the local repository. -- `/etc/nsd/nsd.conf` +## `conformance-tests` -``` text -remote-control: - control-enable: no +This is a collection of tests that check the conformance of a DNS implementation to the different RFCs around DNS and DNSSEC. -zone: - name: main - zonefile: /etc/nsd/zones/main.zone +### Running the test suite + +To run the conformance tests against `unbound` run: + +``` console +$ cargo test -p conformance-tests -- --include-ignored ``` -- `/etc/nsd/zones/main.zone` +To run the conformance tests against `hickory-dns` run: -``` text -$ORIGIN com. -$TTL 1800 -@ IN SOA primary.tld-server.com. admin.tld-server.com. ( - 2014010100 ; Serial - 10800 ; Refresh (3 hours) - 900 ; Retry (15 minutes) - 604800 ; Expire (1 week) - 86400 ; Minimum (1 day) - ) -@ IN NS primary.tld-server.com. -``` -#### `unbound` - -run `unbound -d` - -- `/etc/unbound/unbound.conf` - -ideally instead of `0.0.0.0`, it should only cover the `docker0` network interface. or disable docker containers' access to the internet - -``` text -server: - verbosity: 4 - use-syslog: no - interface: 0.0.0.0 - access-control: 172.17.0.0/16 allow - root-hints: /etc/unbound/root.hints - -remote-control: - control-enable: no +``` console +$ DNS_TEST_SUBJECT="hickory /path/to/repository" cargo test -p conformance-tests ``` -- `/etc/unbound/root.hints`. NOTE IP address of docker container +### Test organization -``` text -. 3600000 NS primary.root-server.com. -primary.root-server.com. 3600000 A 172.17.0.$ROOT_NS_IP_ADDRESS +The module organization is not yet set in stone but currently uses the following structure: + +``` console +packages/conformance-tests/src +├── lib.rs +├── resolver +│ ├── dns +│ │ └── scenarios.rs +│ ├── dns.rs +│ ├── dnssec +│ │ ├── rfc4035 +│ │ │ ├── section_4 +│ │ │ │ └── section_4_1.rs +│ │ │ └── section_4.rs +│ │ ├── rfc4035.rs +│ │ └── scenarios.rs +│ └── dnssec.rs +└── resolver.rs ``` -#### `client` +The modules in the root correspond to the *role* being tested: `resolver` (recursive resolver), `name-server` (authoritative-only name server), etc. -Container is `docker/client.Dockerfile`, build with: `docker build -t dnssec-tests-client -f docker/client.Dockerfile docker`, with `tshark`. +The next module level contains the *functionality* being tested: (plain) DNS, DNSSEC, NSEC3, etc. -Run the client container with extra capabilities +The next module level contains the RFC documents, whose requirements are being tested: RFC4035, etc. -```shell -docker run --rm -it --cap-add=NET_RAW --cap-add=NET_ADMIN dnssec-tests-client /bin/bash -``` +The next module levels contain sections, subsections and any other subdivision that may be relevant. -Then run `tshark` inside the container: +At the RFC module level there's a special module called `scenarios`. This module contains tests that map to representative use cases of the parent functionality. Each use case can be tested in successful and failure scenarios, hence the name. The organization within this module will be ad hoc. -```shell -tshark -f 'host 172.17.0.3' -O dns -``` +### Adding tests and the use of `#[ignore]` -to filter DNS messages for host `172.17.0.3` (`unbound`). +When adding a new test to the test suite, it must pass with the `unbound` implementation, which is treated as the *reference* implementation. The CI workflow will check that *all* tests, including the ones that have the `#[ignore]` attribute, pass with the `unbound` implementation. + +New tests that don't pass with the `hickory-dns` implementation must be marked as `#[ignore]`-d. The CI workflow will check that non-`#[ignore]`-d tests pass with the `hickory-dns` implementation. Additionally, the CI workflow will check that all `#[ignore]`-d tests *fail* with the `hickory-dns` implementation; this is to ensure that fixed tests get un-`#[ignore]`-d. From c0b681e0a363c9062f9d14f39c277065eb35c3cd Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 23 Feb 2024 12:48:34 +0100 Subject: [PATCH 071/124] add an `Implementation` parameter to `NameServer::new` use `dns_test::peer` for name servers in conformance tests --- .../src/resolver/dns/scenarios.rs | 14 ++++++++------ .../dnssec/rfc4035/section_4/section_4_1.rs | 2 +- .../src/resolver/dnssec/scenarios.rs | 9 +++++---- packages/dns-test/src/lib.rs | 4 ++++ packages/dns-test/src/name_server.rs | 15 ++++++++++----- packages/dns-test/src/resolver.rs | 2 +- packages/dns-test/src/tshark.rs | 9 +++++---- 7 files changed, 34 insertions(+), 21 deletions(-) diff --git a/packages/conformance-tests/src/resolver/dns/scenarios.rs b/packages/conformance-tests/src/resolver/dns/scenarios.rs index 9dcdcbd2..b3c4da7f 100644 --- a/packages/conformance-tests/src/resolver/dns/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dns/scenarios.rs @@ -12,10 +12,11 @@ fn can_resolve() -> Result<()> { let needle_fqdn = FQDN("example.nameservers.com.")?; let network = Network::new()?; - let mut root_ns = NameServer::new(FQDN::ROOT, &network)?; - let mut com_ns = NameServer::new(FQDN::COM, &network)?; + let mut root_ns = NameServer::new(dns_test::peer(), FQDN::ROOT, &network)?; + let mut com_ns = NameServer::new(dns_test::peer(), FQDN::COM, &network)?; - let mut nameservers_ns = NameServer::new(FQDN("nameservers.com.")?, &network)?; + let mut nameservers_ns = + NameServer::new(dns_test::peer(), FQDN("nameservers.com.")?, &network)?; nameservers_ns .a(root_ns.fqdn().clone(), root_ns.ipv4_addr()) .a(com_ns.fqdn().clone(), com_ns.ipv4_addr()) @@ -68,10 +69,11 @@ fn nxdomain() -> Result<()> { let needle_fqdn = FQDN("unicorn.nameservers.com.")?; let network = Network::new()?; - let mut root_ns = NameServer::new(FQDN::ROOT, &network)?; - let mut com_ns = NameServer::new(FQDN::COM, &network)?; + let mut root_ns = NameServer::new(dns_test::peer(), FQDN::ROOT, &network)?; + let mut com_ns = NameServer::new(dns_test::peer(), FQDN::COM, &network)?; - let mut nameservers_ns = NameServer::new(FQDN("nameservers.com.")?, &network)?; + let mut nameservers_ns = + NameServer::new(dns_test::peer(), FQDN("nameservers.com.")?, &network)?; nameservers_ns .a(root_ns.fqdn().clone(), root_ns.ipv4_addr()) .a(com_ns.fqdn().clone(), com_ns.ipv4_addr()); diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs index 533e31b3..9772e309 100644 --- a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs @@ -9,7 +9,7 @@ use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; #[ignore] fn edns_support() -> Result<()> { let network = &Network::new()?; - let ns = NameServer::new(FQDN::ROOT, network)?.start()?; + let ns = NameServer::new(dns_test::peer(), FQDN::ROOT, network)?.start()?; let resolver = Resolver::start( dns_test::subject(), &[Root::new(ns.fqdn().clone(), ns.ipv4_addr())], diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs index 1300d9b4..554e7417 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs @@ -11,7 +11,7 @@ use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; #[test] fn can_validate_without_delegation() -> Result<()> { let network = Network::new()?; - let mut ns = NameServer::new(FQDN::ROOT, &network)?; + let mut ns = NameServer::new(dns_test::peer(), FQDN::ROOT, &network)?; ns.a(ns.fqdn().clone(), ns.ipv4_addr()); let ns = ns.sign()?; @@ -55,10 +55,11 @@ fn can_validate_with_delegation() -> Result<()> { let needle_fqdn = FQDN("example.nameservers.com.")?; let network = Network::new()?; - let mut root_ns = NameServer::new(FQDN::ROOT, &network)?; - let mut com_ns = NameServer::new(FQDN::COM, &network)?; + let mut root_ns = NameServer::new(dns_test::peer(), FQDN::ROOT, &network)?; + let mut com_ns = NameServer::new(dns_test::peer(), FQDN::COM, &network)?; - let mut nameservers_ns = NameServer::new(FQDN("nameservers.com.")?, &network)?; + let mut nameservers_ns = + NameServer::new(dns_test::peer(), FQDN("nameservers.com.")?, &network)?; nameservers_ns .a(root_ns.fqdn().clone(), root_ns.ipv4_addr()) .a(com_ns.fqdn().clone(), com_ns.ipv4_addr()) diff --git a/packages/dns-test/src/lib.rs b/packages/dns-test/src/lib.rs index 75ea0f58..8c06730c 100644 --- a/packages/dns-test/src/lib.rs +++ b/packages/dns-test/src/lib.rs @@ -115,3 +115,7 @@ pub fn subject() -> Implementation { Implementation::default() } } + +pub fn peer() -> Implementation { + Implementation::default() +} diff --git a/packages/dns-test/src/name_server.rs b/packages/dns-test/src/name_server.rs index 8033b676..7cc942b0 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -25,7 +25,12 @@ impl<'a> NameServer<'a, Stopped> { /// - one SOA record, with the primary name server field set to this name server's FQDN /// - one NS record, with this name server's FQDN set as the only available name server for /// the zone - pub fn new(zone: FQDN<'a>, network: &Network) -> Result { + pub fn new(implementation: Implementation, zone: FQDN<'a>, network: &Network) -> Result { + assert!( + matches!(implementation, Implementation::Unbound), + "currently only `unbound` (`nsd`) can be used as a `NameServer`" + ); + let ns_count = ns_count(); let nameserver = primary_ns(ns_count); @@ -301,7 +306,7 @@ mod tests { #[test] fn simplest() -> Result<()> { let network = Network::new()?; - let tld_ns = NameServer::new(FQDN::COM, &network)?.start()?; + let tld_ns = NameServer::new(Implementation::Unbound, FQDN::COM, &network)?.start()?; let ip_addr = tld_ns.ipv4_addr(); let client = Client::new(&network)?; @@ -322,7 +327,7 @@ mod tests { fn with_referral() -> Result<()> { let network = Network::new()?; let expected_ip_addr = Ipv4Addr::new(172, 17, 200, 1); - let mut root_ns = NameServer::new(FQDN::ROOT, &network)?; + let mut root_ns = NameServer::new(Implementation::Unbound, FQDN::ROOT, &network)?; root_ns.referral( FQDN::COM, FQDN("primary.tld-server.com.")?, @@ -351,7 +356,7 @@ mod tests { #[test] fn signed() -> Result<()> { let network = Network::new()?; - let ns = NameServer::new(FQDN::ROOT, &network)?.sign()?; + let ns = NameServer::new(Implementation::Unbound, FQDN::ROOT, &network)?.sign()?; eprintln!("KSK:\n{}", ns.key_signing_key()); eprintln!("ZSK:\n{}", ns.zone_signing_key()); @@ -387,7 +392,7 @@ mod tests { #[test] fn terminate_works() -> Result<()> { let network = Network::new()?; - let ns = NameServer::new(FQDN::ROOT, &network)?.start()?; + let ns = NameServer::new(Implementation::Unbound, FQDN::ROOT, &network)?.start()?; let logs = ns.terminate()?; assert!(logs.contains("nsd starting")); diff --git a/packages/dns-test/src/resolver.rs b/packages/dns-test/src/resolver.rs index 091e59b1..b8d59c22 100644 --- a/packages/dns-test/src/resolver.rs +++ b/packages/dns-test/src/resolver.rs @@ -120,7 +120,7 @@ mod tests { #[test] fn terminate_works() -> Result<()> { let network = Network::new()?; - let ns = NameServer::new(FQDN::ROOT, &network)?.start()?; + let ns = NameServer::new(Implementation::Unbound, FQDN::ROOT, &network)?.start()?; let resolver = Resolver::start( Implementation::Unbound, &[Root::new(ns.fqdn().clone(), ns.ipv4_addr())], diff --git a/packages/dns-test/src/tshark.rs b/packages/dns-test/src/tshark.rs index d07fe7eb..f230d31d 100644 --- a/packages/dns-test/src/tshark.rs +++ b/packages/dns-test/src/tshark.rs @@ -255,7 +255,7 @@ mod tests { #[test] fn nameserver() -> Result<()> { let network = &Network::new()?; - let ns = NameServer::new(FQDN::ROOT, network)?.start()?; + let ns = NameServer::new(Implementation::Unbound, FQDN::ROOT, network)?.start()?; let mut tshark = ns.eavesdrop()?; let client = Client::new(network)?; @@ -291,10 +291,11 @@ mod tests { #[test] fn resolver() -> Result<()> { let network = &Network::new()?; - let mut root_ns = NameServer::new(FQDN::ROOT, network)?; - let mut com_ns = NameServer::new(FQDN::COM, network)?; + let mut root_ns = NameServer::new(Implementation::Unbound, FQDN::ROOT, network)?; + let mut com_ns = NameServer::new(Implementation::Unbound, FQDN::COM, network)?; - let mut nameservers_ns = NameServer::new(FQDN("nameservers.com.")?, network)?; + let mut nameservers_ns = + NameServer::new(Implementation::Unbound, FQDN("nameservers.com.")?, network)?; nameservers_ns .a(root_ns.fqdn().clone(), root_ns.ipv4_addr()) .a(com_ns.fqdn().clone(), com_ns.ipv4_addr()); From 98cb9ddaae405f1f9842470d27d4c4938da0db6a Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 23 Feb 2024 13:21:07 +0100 Subject: [PATCH 072/124] add a Client image --- packages/dns-test/src/client.rs | 6 +- packages/dns-test/src/container.rs | 80 ++++++++++++++++--- packages/dns-test/src/container/network.rs | 4 +- .../dns-test/src/docker/client.Dockerfile | 8 ++ .../dns-test/src/docker/unbound.Dockerfile | 4 - packages/dns-test/src/lib.rs | 35 -------- packages/dns-test/src/name_server.rs | 3 +- packages/dns-test/src/resolver.rs | 3 +- 8 files changed, 85 insertions(+), 58 deletions(-) create mode 100644 packages/dns-test/src/docker/client.Dockerfile diff --git a/packages/dns-test/src/client.rs b/packages/dns-test/src/client.rs index b66b73ca..af6861b5 100644 --- a/packages/dns-test/src/client.rs +++ b/packages/dns-test/src/client.rs @@ -1,10 +1,10 @@ use core::str::FromStr; use std::net::Ipv4Addr; -use crate::container::{Container, Network}; +use crate::container::{Container, Image, Network}; use crate::record::{Record, RecordType}; use crate::trust_anchor::TrustAnchor; -use crate::{Error, Implementation, Result, FQDN}; +use crate::{Error, Result, FQDN}; pub struct Client { inner: Container, @@ -13,7 +13,7 @@ pub struct Client { impl Client { pub fn new(network: &Network) -> Result { Ok(Self { - inner: Container::run(&Implementation::Unbound, network)?, + inner: Container::run(&Image::Client, network)?, }) } diff --git a/packages/dns-test/src/container.rs b/packages/dns-test/src/container.rs index 92550114..d3651d93 100644 --- a/packages/dns-test/src/container.rs +++ b/packages/dns-test/src/container.rs @@ -1,17 +1,17 @@ mod network; -use core::str; +use core::{fmt, str}; use std::net::Ipv4Addr; use std::process::{self, ChildStdout, ExitStatus}; use std::process::{Command, Stdio}; use std::sync::atomic::AtomicUsize; -use std::sync::{atomic, Arc}; +use std::sync::{atomic, Arc, Once}; use std::{env, fs}; use tempfile::{NamedTempFile, TempDir}; pub use crate::container::network::Network; -use crate::{Error, Implementation, Result}; +use crate::{Error, Implementation, Repository, Result}; #[derive(Clone)] pub struct Container { @@ -20,16 +20,72 @@ pub struct Container { const PACKAGE_NAME: &str = env!("CARGO_PKG_NAME"); +#[derive(Clone)] +pub enum Image { + Client, + Hickory(Repository<'static>), + Unbound, +} + +impl Image { + fn dockerfile(&self) -> &'static str { + match self { + Self::Unbound => include_str!("docker/unbound.Dockerfile"), + Self::Hickory { .. } => include_str!("docker/hickory.Dockerfile"), + Self::Client => include_str!("docker/client.Dockerfile"), + } + } + + fn once(&self) -> &'static Once { + match self { + Self::Client { .. } => { + static CLIENT_ONCE: Once = Once::new(); + &CLIENT_ONCE + } + + Self::Hickory { .. } => { + static HICKORY_ONCE: Once = Once::new(); + &HICKORY_ONCE + } + + Self::Unbound { .. } => { + static UNBOUND_ONCE: Once = Once::new(); + &UNBOUND_ONCE + } + } + } +} + +impl From for Image { + fn from(implementation: Implementation) -> Self { + match implementation { + Implementation::Unbound => Self::Unbound, + Implementation::Hickory(repo) => Self::Hickory(repo), + } + } +} + +impl fmt::Display for Image { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + Self::Client => "client", + Self::Hickory { .. } => "hickory", + Self::Unbound => "unbound", + }; + f.write_str(s) + } +} + impl Container { /// Starts the container in a "parked" state - pub fn run(implementation: &Implementation, network: &Network) -> Result { + pub fn run(image: &Image, network: &Network) -> Result { // TODO make this configurable and support hickory & bind - let dockerfile = implementation.dockerfile(); + let dockerfile = image.dockerfile(); let docker_build_dir = TempDir::new()?; let docker_build_dir = docker_build_dir.path(); fs::write(docker_build_dir.join("Dockerfile"), dockerfile)?; - let image_tag = format!("{PACKAGE_NAME}-{implementation}"); + let image_tag = format!("{PACKAGE_NAME}-{image}"); let mut command = Command::new("docker"); command @@ -37,13 +93,13 @@ impl Container { .arg(&image_tag) .arg(docker_build_dir); - let repo = if let Implementation::Hickory(repo) = implementation { + let repo = if let Image::Hickory(repo) = image { Some(repo) } else { None }; - implementation.once().call_once(|| { + image.once().call_once(|| { if let Some(repo) = repo { let mut cp_r = Command::new("git"); cp_r.args([ @@ -66,7 +122,7 @@ impl Container { let mut command = Command::new("docker"); let pid = process::id(); let count = container_count(); - let name = format!("{PACKAGE_NAME}-{implementation}-{pid}-{count}"); + let name = format!("{PACKAGE_NAME}-{image}-{pid}-{count}"); command .args([ "run", @@ -334,7 +390,7 @@ mod tests { #[test] fn run_works() -> Result<()> { let network = Network::new()?; - let container = Container::run(&Implementation::Unbound, &network)?; + let container = Container::run(&Image::Client, &network)?; let output = container.output(&["true"])?; assert!(output.status.success()); @@ -345,7 +401,7 @@ mod tests { #[test] fn ipv4_addr_works() -> Result<()> { let network = Network::new()?; - let container = Container::run(&Implementation::Unbound, &network)?; + let container = Container::run(&Image::Client, &network)?; let ipv4_addr = container.ipv4_addr(); let output = container.output(&["ping", "-c1", &format!("{ipv4_addr}")])?; @@ -357,7 +413,7 @@ mod tests { #[test] fn cp_works() -> Result<()> { let network = Network::new()?; - let container = Container::run(&Implementation::Unbound, &network)?; + let container = Container::run(&Image::Client, &network)?; let path = "/tmp/somefile"; let contents = "hello"; diff --git a/packages/dns-test/src/container/network.rs b/packages/dns-test/src/container/network.rs index 7e9f81b1..6c1751ca 100644 --- a/packages/dns-test/src/container/network.rs +++ b/packages/dns-test/src/container/network.rs @@ -113,7 +113,7 @@ fn network_count() -> usize { #[cfg(test)] mod tests { - use crate::{container::Container, Implementation}; + use crate::container::{Container, Image}; use super::*; @@ -146,7 +146,7 @@ mod tests { let network = Network::new().expect("Failed to create network"); let network_name = network.name().to_string(); let container = - Container::run(&Implementation::Unbound, &network).expect("Failed to start container"); + Container::run(&Image::Client, &network).expect("Failed to start container"); assert!(exists_network(&network_name)); drop(network); diff --git a/packages/dns-test/src/docker/client.Dockerfile b/packages/dns-test/src/docker/client.Dockerfile new file mode 100644 index 00000000..9ab60a70 --- /dev/null +++ b/packages/dns-test/src/docker/client.Dockerfile @@ -0,0 +1,8 @@ +FROM debian:bookworm-slim + +# dnsutils = dig & delv +# iputils-ping = ping +RUN apt-get update && \ + apt-get install -y \ + dnsutils \ + iputils-ping diff --git a/packages/dns-test/src/docker/unbound.Dockerfile b/packages/dns-test/src/docker/unbound.Dockerfile index b42777ef..4b8139f0 100644 --- a/packages/dns-test/src/docker/unbound.Dockerfile +++ b/packages/dns-test/src/docker/unbound.Dockerfile @@ -1,12 +1,8 @@ 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 \ diff --git a/packages/dns-test/src/lib.rs b/packages/dns-test/src/lib.rs index 8c06730c..4f8b5c01 100644 --- a/packages/dns-test/src/lib.rs +++ b/packages/dns-test/src/lib.rs @@ -1,9 +1,7 @@ //! A test framework for all things DNS -use core::fmt; use std::borrow::Cow; use std::path::Path; -use std::sync::Once; use url::Url; @@ -57,45 +55,12 @@ pub fn Repository(input: impl Into>) -> Repository<'static> { Repository { inner: input } } -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") { if subject == "unbound" { diff --git a/packages/dns-test/src/name_server.rs b/packages/dns-test/src/name_server.rs index 7cc942b0..724a4499 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -47,8 +47,9 @@ impl<'a> NameServer<'a, Stopped> { nameserver: nameserver.clone(), }); + let image = implementation.into(); Ok(Self { - container: Container::run(&Implementation::Unbound, network)?, + container: Container::run(&image, network)?, zone_file, state: Stopped, }) diff --git a/packages/dns-test/src/resolver.rs b/packages/dns-test/src/resolver.rs index b8d59c22..948f41f9 100644 --- a/packages/dns-test/src/resolver.rs +++ b/packages/dns-test/src/resolver.rs @@ -33,7 +33,8 @@ impl Resolver { "must configure at least one local root server" ); - let container = Container::run(&implementation, network)?; + let image = implementation.clone().into(); + let container = Container::run(&image, network)?; let mut hints = String::new(); for root in roots { From 56a9613615b323e81e4f5447cd28ad4ff2b4c77d Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 23 Feb 2024 15:35:51 +0100 Subject: [PATCH 073/124] fix the explore example --- packages/dns-test/examples/explore.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/dns-test/examples/explore.rs b/packages/dns-test/examples/explore.rs index 112c211d..3ed9b9d5 100644 --- a/packages/dns-test/examples/explore.rs +++ b/packages/dns-test/examples/explore.rs @@ -8,15 +8,16 @@ use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; fn main() -> Result<()> { let network = Network::new()?; + let peer = dns_test::peer(); println!("building docker image..."); - let mut root_ns = NameServer::new(FQDN::ROOT, &network)?; + let mut root_ns = NameServer::new(peer.clone(), FQDN::ROOT, &network)?; println!("DONE"); println!("setting up name servers..."); - let mut com_ns = NameServer::new(FQDN::COM, &network)?; + let mut com_ns = NameServer::new(peer.clone(), FQDN::COM, &network)?; - let mut nameservers_ns = NameServer::new(FQDN("nameservers.com.")?, &network)?; + let mut nameservers_ns = NameServer::new(peer.clone(), FQDN("nameservers.com.")?, &network)?; nameservers_ns .a(root_ns.fqdn().clone(), root_ns.ipv4_addr()) .a(com_ns.fqdn().clone(), com_ns.ipv4_addr()); From a83b6629a3f6a112783a7102a31d16294a7a7b20 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 16 Feb 2024 15:53:40 +0100 Subject: [PATCH 074/124] drop generic lifetime parameters from types none of the parsing we are doing requires a non-static lifetime --- packages/dns-test/src/client.rs | 4 +- packages/dns-test/src/fqdn.rs | 22 ++++---- packages/dns-test/src/name_server.rs | 39 ++++++-------- packages/dns-test/src/record.rs | 12 ++--- packages/dns-test/src/zone_file.rs | 80 ++++++++++++++-------------- 5 files changed, 76 insertions(+), 81 deletions(-) diff --git a/packages/dns-test/src/client.rs b/packages/dns-test/src/client.rs index 069f2562..484fb2c7 100644 --- a/packages/dns-test/src/client.rs +++ b/packages/dns-test/src/client.rs @@ -29,7 +29,7 @@ impl Client { &self, server: Ipv4Addr, record_type: RecordType, - fqdn: &FQDN<'_>, + fqdn: &FQDN, trust_anchor: &TrustAnchor, ) -> Result { const TRUST_ANCHOR_PATH: &str = "/etc/bind.keys"; @@ -57,7 +57,7 @@ impl Client { dnssec: Dnssec, server: Ipv4Addr, record_type: RecordType, - fqdn: &FQDN<'_>, + fqdn: &FQDN, ) -> Result { let output = self.inner.stdout(&[ "dig", diff --git a/packages/dns-test/src/fqdn.rs b/packages/dns-test/src/fqdn.rs index 18e08a8b..5c3d8678 100644 --- a/packages/dns-test/src/fqdn.rs +++ b/packages/dns-test/src/fqdn.rs @@ -5,13 +5,13 @@ use std::borrow::Cow; use crate::{Error, Result}; #[derive(Clone, PartialEq)] -pub struct FQDN<'a> { - inner: Cow<'a, str>, +pub struct FQDN { + inner: Cow<'static, str>, } // TODO likely needs further validation #[allow(non_snake_case)] -pub fn FQDN<'a>(input: impl Into>) -> Result> { +pub fn FQDN(input: impl Into>) -> Result { let input = input.into(); if !input.ends_with('.') { return Err("FQDN must end with a `.`".into()); @@ -24,12 +24,12 @@ pub fn FQDN<'a>(input: impl Into>) -> Result> { Ok(FQDN { inner: input }) } -impl<'a> FQDN<'a> { - pub const ROOT: FQDN<'static> = FQDN { +impl FQDN { + pub const ROOT: FQDN = FQDN { inner: Cow::Borrowed("."), }; - pub const COM: FQDN<'static> = FQDN { + pub const COM: FQDN = FQDN { inner: Cow::Borrowed("com."), }; @@ -41,7 +41,7 @@ impl<'a> FQDN<'a> { &self.inner } - pub fn into_owned(self) -> FQDN<'static> { + pub fn into_owned(self) -> FQDN { let owned = match self.inner { Cow::Borrowed(borrowed) => borrowed.to_string(), Cow::Owned(owned) => owned, @@ -53,21 +53,21 @@ impl<'a> FQDN<'a> { } } -impl FromStr for FQDN<'static> { +impl FromStr for FQDN { type Err = Error; fn from_str(input: &str) -> Result { - Ok(FQDN(input)?.into_owned()) + FQDN(input.to_string()) } } -impl fmt::Debug for FQDN<'_> { +impl fmt::Debug for FQDN { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(self, f) } } -impl fmt::Display for FQDN<'_> { +impl fmt::Display for FQDN { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.inner) } diff --git a/packages/dns-test/src/name_server.rs b/packages/dns-test/src/name_server.rs index db9aab0a..f907c8b6 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -6,13 +6,13 @@ use crate::tshark::Tshark; use crate::zone_file::{self, SoaSettings, ZoneFile, DNSKEY, DS}; use crate::{Implementation, Result, FQDN}; -pub struct NameServer<'a, State> { +pub struct NameServer { container: Container, - zone_file: ZoneFile<'a>, + zone_file: ZoneFile, state: State, } -impl<'a> NameServer<'a, Stopped> { +impl NameServer { /// Spins up a primary name server that has authority over the given `zone` /// /// The initial state of the server is the "Stopped" state where it won't answer any query. @@ -25,7 +25,7 @@ impl<'a> NameServer<'a, Stopped> { /// - one SOA record, with the primary name server field set to this name server's FQDN /// - one NS record, with this name server's FQDN set as the only available name server for /// the zone - pub fn new(implementation: Implementation, zone: FQDN<'a>, network: &Network) -> Result { + pub fn new(implementation: Implementation, zone: FQDN, network: &Network) -> Result { assert!( matches!(implementation, Implementation::Unbound), "currently only `unbound` (`nsd`) can be used as a `NameServer`" @@ -56,18 +56,13 @@ impl<'a> NameServer<'a, Stopped> { } /// Adds a NS + A record pair to the zone file - pub fn referral( - &mut self, - zone: FQDN<'a>, - nameserver: FQDN<'a>, - ipv4_addr: Ipv4Addr, - ) -> &mut Self { + pub fn referral(&mut self, zone: FQDN, nameserver: FQDN, ipv4_addr: Ipv4Addr) -> &mut Self { self.zone_file.referral(zone, nameserver, ipv4_addr); self } /// Adds an A record pair to the zone file - pub fn a(&mut self, fqdn: FQDN<'a>, ipv4_addr: Ipv4Addr) -> &mut Self { + pub fn a(&mut self, fqdn: FQDN, ipv4_addr: Ipv4Addr) -> &mut Self { self.zone_file.entry(zone_file::A { fqdn, ipv4_addr }); self } @@ -79,7 +74,7 @@ impl<'a> NameServer<'a, Stopped> { } /// Freezes and signs the name server's zone file - pub fn sign(self) -> Result> { + pub fn sign(self) -> Result> { // TODO do we want to make these settings configurable? const ZSK_BITS: usize = 1024; const KSK_BITS: usize = 2048; @@ -139,7 +134,7 @@ impl<'a> NameServer<'a, Stopped> { } /// Moves the server to the "Start" state where it can answer client queries - pub fn start(self) -> Result> { + pub fn start(self) -> Result> { let Self { container, zone_file, @@ -176,9 +171,9 @@ fn ns_count() -> usize { COUNT.fetch_add(1, atomic::Ordering::Relaxed) } -impl<'a> NameServer<'a, Signed> { +impl NameServer { /// Moves the server to the "Start" state where it can answer client queries - pub fn start(self) -> Result> { + pub fn start(self) -> Result> { let Self { container, zone_file, @@ -216,7 +211,7 @@ impl<'a> NameServer<'a, Signed> { } } -impl<'a> NameServer<'a, Running> { +impl NameServer { /// Starts a `tshark` instance that captures DNS messages flowing through this network node pub fn eavesdrop(&self) -> Result { self.container.eavesdrop() @@ -246,7 +241,7 @@ kill -TERM $(cat {pidfile})" } } -impl<'a, S> NameServer<'a, S> { +impl NameServer { pub fn container_id(&self) -> &str { self.container.id() } @@ -256,15 +251,15 @@ impl<'a, S> NameServer<'a, S> { } /// Zone file BEFORE signing - pub fn zone_file(&self) -> &ZoneFile<'a> { + pub fn zone_file(&self) -> &ZoneFile { &self.zone_file } - pub fn zone(&self) -> &FQDN<'a> { + pub fn zone(&self) -> &FQDN { &self.zone_file.origin } - pub fn fqdn(&self) -> &FQDN<'a> { + pub fn fqdn(&self) -> &FQDN { &self.zone_file.soa.nameserver } } @@ -282,11 +277,11 @@ pub struct Running { child: Child, } -fn primary_ns(ns_count: usize) -> FQDN<'static> { +fn primary_ns(ns_count: usize) -> FQDN { FQDN(format!("primary{ns_count}.nameservers.com.")).unwrap() } -fn admin_ns(ns_count: usize) -> FQDN<'static> { +fn admin_ns(ns_count: usize) -> FQDN { FQDN(format!("admin{ns_count}.nameservers.com.")).unwrap() } diff --git a/packages/dns-test/src/record.rs b/packages/dns-test/src/record.rs index 0f6885ac..e8086916 100644 --- a/packages/dns-test/src/record.rs +++ b/packages/dns-test/src/record.rs @@ -93,7 +93,7 @@ impl FromStr for Record { #[derive(Debug)] pub struct A { - pub fqdn: FQDN<'static>, + pub fqdn: FQDN, pub ttl: u32, pub ipv4_addr: Ipv4Addr, } @@ -132,7 +132,7 @@ impl FromStr for A { #[allow(clippy::upper_case_acronyms)] #[derive(Debug)] pub struct RRSIG { - pub fqdn: FQDN<'static>, + pub fqdn: FQDN, pub ttl: u32, pub type_covered: RecordType, pub algorithm: u32, @@ -141,7 +141,7 @@ pub struct RRSIG { pub signature_expiration: u64, pub signature_inception: u64, pub key_tag: u32, - pub signer_name: FQDN<'static>, + pub signer_name: FQDN, /// base64 encoded pub signature: String, } @@ -193,10 +193,10 @@ impl FromStr for RRSIG { #[allow(clippy::upper_case_acronyms)] #[derive(Debug)] pub struct SOA { - pub zone: FQDN<'static>, + pub zone: FQDN, pub ttl: u32, - pub nameserver: FQDN<'static>, - pub admin: FQDN<'static>, + pub nameserver: FQDN, + pub admin: FQDN, pub serial: u32, pub refresh: u32, pub retry: u32, diff --git a/packages/dns-test/src/zone_file.rs b/packages/dns-test/src/zone_file.rs index 8dc153c9..53c498df 100644 --- a/packages/dns-test/src/zone_file.rs +++ b/packages/dns-test/src/zone_file.rs @@ -10,16 +10,16 @@ use std::str::FromStr; use crate::{Error, FQDN}; -pub struct ZoneFile<'a> { - pub origin: FQDN<'a>, +pub struct ZoneFile { + pub origin: FQDN, pub ttl: u32, - pub soa: SOA<'a>, - pub entries: Vec>, + pub soa: SOA, + pub entries: Vec, } -impl<'a> ZoneFile<'a> { +impl ZoneFile { /// Convenience constructor that uses "reasonable" defaults - pub fn new(origin: FQDN<'a>, soa: SOA<'a>) -> Self { + pub fn new(origin: FQDN, soa: SOA) -> Self { Self { origin, ttl: 1800, @@ -29,12 +29,12 @@ impl<'a> ZoneFile<'a> { } /// Appends an entry - pub fn entry(&mut self, entry: impl Into>) { + pub fn entry(&mut self, entry: impl Into) { self.entries.push(entry.into()) } /// Appends a NS + A entry pair - pub fn referral(&mut self, zone: FQDN<'a>, nameserver: FQDN<'a>, ipv4_addr: Ipv4Addr) { + pub fn referral(&mut self, zone: FQDN, nameserver: FQDN, ipv4_addr: Ipv4Addr) { self.entry(NS { zone: zone.clone(), nameserver: nameserver.clone(), @@ -46,7 +46,7 @@ impl<'a> ZoneFile<'a> { } } -impl fmt::Display for ZoneFile<'_> { +impl fmt::Display for ZoneFile { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let Self { origin, @@ -67,15 +67,15 @@ impl fmt::Display for ZoneFile<'_> { } } -pub struct Root<'a> { +pub struct Root { pub ipv4_addr: Ipv4Addr, - pub ns: FQDN<'a>, + pub ns: FQDN, pub ttl: u32, } -impl<'a> Root<'a> { +impl Root { /// Convenience constructor that uses "reasonable" defaults - pub fn new(ns: FQDN<'a>, ipv4_addr: Ipv4Addr) -> Self { + pub fn new(ns: FQDN, ipv4_addr: Ipv4Addr) -> Self { Self { ipv4_addr, ns, @@ -84,7 +84,7 @@ impl<'a> Root<'a> { } } -impl fmt::Display for Root<'_> { +impl fmt::Display for Root { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let Self { ipv4_addr, ns, ttl } = self; @@ -93,32 +93,32 @@ impl fmt::Display for Root<'_> { } } -pub enum Entry<'a> { - A(A<'a>), +pub enum Entry { + A(A), DNSKEY(DNSKEY), DS(DS), - NS(NS<'a>), + NS(NS), } -impl<'a> From for Entry<'a> { +impl From for Entry { fn from(v: DS) -> Self { Self::DS(v) } } -impl<'a> From> for Entry<'a> { - fn from(v: A<'a>) -> Self { +impl From for Entry { + fn from(v: A) -> Self { Self::A(v) } } -impl<'a> From> for Entry<'a> { - fn from(v: NS<'a>) -> Self { +impl From for Entry { + fn from(v: NS) -> Self { Self::NS(v) } } -impl fmt::Display for Entry<'_> { +impl fmt::Display for Entry { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Entry::A(a) => a.fmt(f), @@ -130,12 +130,12 @@ impl fmt::Display for Entry<'_> { } #[derive(Clone)] -pub struct A<'a> { - pub fqdn: FQDN<'a>, +pub struct A { + pub fqdn: FQDN, pub ipv4_addr: Ipv4Addr, } -impl fmt::Display for A<'_> { +impl fmt::Display for A { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let Self { fqdn, ipv4_addr } = self; @@ -146,7 +146,7 @@ impl fmt::Display for A<'_> { // integer types chosen based on bit sizes in section 2.1 of RFC4034 #[derive(Clone, Debug)] pub struct DNSKEY { - zone: FQDN<'static>, + zone: FQDN, flags: u16, protocol: u8, algorithm: u8, @@ -256,7 +256,7 @@ impl fmt::Display for DNSKEY { #[derive(Clone)] pub struct DS { - zone: FQDN<'static>, + zone: FQDN, _ttl: u32, key_tag: u16, algorithm: u8, @@ -317,12 +317,12 @@ impl fmt::Display for DS { } } -pub struct NS<'a> { - pub zone: FQDN<'a>, - pub nameserver: FQDN<'a>, +pub struct NS { + pub zone: FQDN, + pub nameserver: FQDN, } -impl fmt::Display for NS<'_> { +impl fmt::Display for NS { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let Self { zone, @@ -333,14 +333,14 @@ impl fmt::Display for NS<'_> { } } -pub struct SOA<'a> { - pub zone: FQDN<'a>, - pub nameserver: FQDN<'a>, - pub admin: FQDN<'a>, +pub struct SOA { + pub zone: FQDN, + pub nameserver: FQDN, + pub admin: FQDN, pub settings: SoaSettings, } -impl fmt::Display for SOA<'_> { +impl fmt::Display for SOA { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let Self { zone, @@ -482,21 +482,21 @@ e.gtld-servers.net. IN A 192.12.94.30 Ok(()) } - fn example_a() -> Result> { + fn example_a() -> Result { Ok(A { fqdn: FQDN("e.gtld-servers.net.")?, ipv4_addr: Ipv4Addr::new(192, 12, 94, 30), }) } - fn example_ns() -> Result> { + fn example_ns() -> Result { Ok(NS { zone: FQDN::COM, nameserver: FQDN("e.gtld-servers.net.")?, }) } - fn example_soa() -> Result> { + fn example_soa() -> Result { Ok(SOA { zone: FQDN::ROOT, nameserver: FQDN("a.root-servers.net.")?, From 66d6061ffce6633bc4f534b9907f22610398c1c4 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 19 Feb 2024 11:51:51 +0100 Subject: [PATCH 075/124] drop most zone_file entry types instead use the record types in zone files the main difference between e.g. zone_file::A and record::A was that the latter had a TTL filed and the former didn't to eliminate code duplication we make the `ZoneFile` API use the `record` types and discard the zone_file entry types --- .../src/resolver/dns/scenarios.rs | 12 +- .../src/resolver/dnssec/scenarios.rs | 14 +- packages/dns-test/examples/explore.rs | 10 +- packages/dns-test/src/lib.rs | 9 +- packages/dns-test/src/name_server.rs | 53 +- packages/dns-test/src/record.rs | 519 ++++++++++++++++-- packages/dns-test/src/trust_anchor.rs | 2 +- packages/dns-test/src/tshark.rs | 6 +- packages/dns-test/src/zone_file.rs | 451 ++------------- 9 files changed, 587 insertions(+), 489 deletions(-) diff --git a/packages/conformance-tests/src/resolver/dns/scenarios.rs b/packages/conformance-tests/src/resolver/dns/scenarios.rs index b3c4da7f..f696691a 100644 --- a/packages/conformance-tests/src/resolver/dns/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dns/scenarios.rs @@ -2,7 +2,7 @@ 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::record::{Record, RecordType}; use dns_test::zone_file::Root; use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; @@ -18,9 +18,9 @@ fn can_resolve() -> Result<()> { let mut nameservers_ns = NameServer::new(dns_test::peer(), FQDN("nameservers.com.")?, &network)?; 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); + .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) + .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())) + .add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); let nameservers_ns = nameservers_ns.start()?; eprintln!("nameservers.com.zone:\n{}", nameservers_ns.zone_file()); @@ -75,8 +75,8 @@ fn nxdomain() -> Result<()> { let mut nameservers_ns = NameServer::new(dns_test::peer(), FQDN("nameservers.com.")?, &network)?; nameservers_ns - .a(root_ns.fqdn().clone(), root_ns.ipv4_addr()) - .a(com_ns.fqdn().clone(), com_ns.ipv4_addr()); + .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) + .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())); let nameservers_ns = nameservers_ns.start()?; com_ns.referral( diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs index 554e7417..fd9d2676 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs @@ -2,7 +2,7 @@ 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::record::{Record, RecordType}; use dns_test::zone_file::Root; use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; @@ -12,7 +12,7 @@ use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; fn can_validate_without_delegation() -> Result<()> { let network = Network::new()?; let mut ns = NameServer::new(dns_test::peer(), FQDN::ROOT, &network)?; - ns.a(ns.fqdn().clone(), ns.ipv4_addr()); + ns.add(Record::a(ns.fqdn().clone(), ns.ipv4_addr())); let ns = ns.sign()?; let root_ksk = ns.key_signing_key().clone(); @@ -61,9 +61,9 @@ fn can_validate_with_delegation() -> Result<()> { let mut nameservers_ns = NameServer::new(dns_test::peer(), FQDN("nameservers.com.")?, &network)?; 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); + .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) + .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())) + .add(Record::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()?; @@ -76,7 +76,7 @@ fn can_validate_with_delegation() -> Result<()> { nameservers_ns.fqdn().clone(), nameservers_ns.ipv4_addr(), ) - .ds(nameservers_ds); + .add(nameservers_ds); let com_ns = com_ns.sign()?; let com_ds = com_ns.ds().clone(); let com_ns = com_ns.start()?; @@ -85,7 +85,7 @@ fn can_validate_with_delegation() -> Result<()> { root_ns .referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr()) - .ds(com_ds); + .add(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(); diff --git a/packages/dns-test/examples/explore.rs b/packages/dns-test/examples/explore.rs index 3ed9b9d5..e6d9919e 100644 --- a/packages/dns-test/examples/explore.rs +++ b/packages/dns-test/examples/explore.rs @@ -2,7 +2,7 @@ use std::sync::mpsc; use dns_test::client::Client; use dns_test::name_server::NameServer; -use dns_test::record::RecordType; +use dns_test::record::{Record, RecordType}; use dns_test::zone_file::Root; use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; @@ -19,8 +19,8 @@ fn main() -> Result<()> { let mut nameservers_ns = NameServer::new(peer.clone(), FQDN("nameservers.com.")?, &network)?; nameservers_ns - .a(root_ns.fqdn().clone(), root_ns.ipv4_addr()) - .a(com_ns.fqdn().clone(), com_ns.ipv4_addr()); + .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) + .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())); let nameservers_ns = nameservers_ns.sign()?; let nameservers_ds = nameservers_ns.ds().clone(); let nameservers_ns = nameservers_ns.start()?; @@ -31,14 +31,14 @@ fn main() -> Result<()> { nameservers_ns.fqdn().clone(), nameservers_ns.ipv4_addr(), ) - .ds(nameservers_ds); + .add(nameservers_ds); let com_ns = com_ns.sign()?; let com_ds = com_ns.ds().clone(); let com_ns = com_ns.start()?; root_ns .referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr()) - .ds(com_ds); + .add(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(); diff --git a/packages/dns-test/src/lib.rs b/packages/dns-test/src/lib.rs index 4f8b5c01..48c81c00 100644 --- a/packages/dns-test/src/lib.rs +++ b/packages/dns-test/src/lib.rs @@ -10,9 +10,6 @@ pub use crate::fqdn::FQDN; pub use crate::resolver::Resolver; pub use crate::trust_anchor::TrustAnchor; -pub type Error = Box; -pub type Result = core::result::Result; - pub mod client; mod container; mod fqdn; @@ -23,6 +20,12 @@ mod trust_anchor; pub mod tshark; pub mod zone_file; +pub type Error = Box; +pub type Result = core::result::Result; + +// TODO maybe this should be a TLS variable that each unit test (thread) can override +const DEFAULT_TTL: u32 = 24 * 60 * 60; // 1 day + #[derive(Clone)] pub enum Implementation { Unbound, diff --git a/packages/dns-test/src/name_server.rs b/packages/dns-test/src/name_server.rs index f907c8b6..fd8bec05 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -2,9 +2,10 @@ use core::sync::atomic::{self, AtomicUsize}; use std::net::Ipv4Addr; use crate::container::{Child, Container, Network}; +use crate::record::{self, Record, SoaSettings, DS, SOA}; use crate::tshark::Tshark; -use crate::zone_file::{self, SoaSettings, ZoneFile, DNSKEY, DS}; -use crate::{Implementation, Result, FQDN}; +use crate::zone_file::{self, ZoneFile}; +use crate::{Implementation, Result, DEFAULT_TTL, FQDN}; pub struct NameServer { container: Container, @@ -34,18 +35,16 @@ impl NameServer { let ns_count = ns_count(); let nameserver = primary_ns(ns_count); - let soa = zone_file::SOA { + let soa = SOA { zone: zone.clone(), + ttl: DEFAULT_TTL, nameserver: nameserver.clone(), admin: admin_ns(ns_count), settings: SoaSettings::default(), }; - let mut zone_file = ZoneFile::new(zone.clone(), soa); + let mut zone_file = ZoneFile::new(soa); - zone_file.entry(zone_file::NS { - zone, - nameserver: nameserver.clone(), - }); + zone_file.add(Record::ns(zone, nameserver.clone())); let image = implementation.into(); Ok(Self { @@ -61,15 +60,9 @@ impl NameServer { self } - /// Adds an A record pair to the zone file - pub fn a(&mut self, fqdn: FQDN, ipv4_addr: Ipv4Addr) -> &mut Self { - self.zone_file.entry(zone_file::A { fqdn, ipv4_addr }); - self - } - - /// Adds a DS record to the zone file - pub fn ds(&mut self, ds: DS) -> &mut Self { - self.zone_file.entry(ds); + /// Adds a record to the name server's zone file + pub fn add(&mut self, record: impl Into) -> &mut Self { + self.zone_file.add(record); self } @@ -89,19 +82,19 @@ impl NameServer { container.status_ok(&["mkdir", "-p", ZONES_DIR])?; container.cp("/etc/nsd/zones/main.zone", &zone_file.to_string())?; - let zone = &zone_file.origin; + let zone = zone_file.origin(); let zsk_keygen = format!("cd {ZONES_DIR} && ldns-keygen -a {ALGORITHM} -b {ZSK_BITS} {zone}"); let zsk_filename = container.stdout(&["sh", "-c", &zsk_keygen])?; let zsk_path = format!("{ZONES_DIR}/{zsk_filename}.key"); - let zsk: DNSKEY = container.stdout(&["cat", &zsk_path])?.parse()?; + let zsk: zone_file::DNSKEY = container.stdout(&["cat", &zsk_path])?.parse()?; let ksk_keygen = format!("cd {ZONES_DIR} && ldns-keygen -k -a {ALGORITHM} -b {KSK_BITS} {zone}"); let ksk_filename = container.stdout(&["sh", "-c", &ksk_keygen])?; let ksk_path = format!("{ZONES_DIR}/{ksk_filename}.key"); - let ksk: DNSKEY = container.stdout(&["cat", &ksk_path])?.parse()?; + let ksk: zone_file::DNSKEY = container.stdout(&["cat", &ksk_path])?.parse()?; // -n = use NSEC3 instead of NSEC // -p = set the opt-out flag on all nsec3 rrs @@ -120,15 +113,17 @@ impl NameServer { container.status_ok(&["mv", &format!("{zone_file_path}.signed"), &zone_file_path])?; let signed_zone_file = container.stdout(&["cat", &zone_file_path])?; + let ttl = zone_file.soa.ttl; Ok(NameServer { container, zone_file, state: Signed { ds, - ksk, signed_zone_file, - zsk, + // inherit SOA's TTL value + ksk: ksk.with_ttl(ttl), + zsk: zsk.with_ttl(ttl), }, }) } @@ -144,7 +139,7 @@ impl NameServer { // for PID file container.status_ok(&["mkdir", "-p", "/run/nsd/"])?; - container.cp("/etc/nsd/nsd.conf", &nsd_conf(&zone_file.origin))?; + container.cp("/etc/nsd/nsd.conf", &nsd_conf(zone_file.origin()))?; container.status_ok(&["mkdir", "-p", ZONES_DIR])?; container.cp(&zone_file_path(), &zone_file.to_string())?; @@ -183,7 +178,7 @@ impl NameServer { // for PID file container.status_ok(&["mkdir", "-p", "/run/nsd/"])?; - container.cp("/etc/nsd/nsd.conf", &nsd_conf(&zone_file.origin))?; + container.cp("/etc/nsd/nsd.conf", &nsd_conf(zone_file.origin()))?; let child = container.spawn(&["nsd", "-d"])?; @@ -194,11 +189,11 @@ impl NameServer { }) } - pub fn key_signing_key(&self) -> &DNSKEY { + pub fn key_signing_key(&self) -> &record::DNSKEY { &self.state.ksk } - pub fn zone_signing_key(&self) -> &DNSKEY { + pub fn zone_signing_key(&self) -> &record::DNSKEY { &self.state.zsk } @@ -256,7 +251,7 @@ impl NameServer { } pub fn zone(&self) -> &FQDN { - &self.zone_file.origin + self.zone_file.origin() } pub fn fqdn(&self) -> &FQDN { @@ -268,8 +263,8 @@ pub struct Stopped; pub struct Signed { ds: DS, - zsk: DNSKEY, - ksk: DNSKEY, + zsk: record::DNSKEY, + ksk: record::DNSKEY, signed_zone_file: String, } diff --git a/packages/dns-test/src/record.rs b/packages/dns-test/src/record.rs index e8086916..c16a49f8 100644 --- a/packages/dns-test/src/record.rs +++ b/packages/dns-test/src/record.rs @@ -1,24 +1,29 @@ //! Text representation of DNS records -use core::array; use core::result::Result as CoreResult; use core::str::FromStr; +use core::{array, fmt}; +use std::fmt::Write; use std::net::Ipv4Addr; -use crate::{Error, Result, FQDN}; +use crate::{Error, Result, DEFAULT_TTL, FQDN}; #[allow(clippy::upper_case_acronyms)] #[derive(Debug, PartialEq)] pub enum RecordType { A, + DS, NS, SOA, + // excluded because cannot appear in RRSIG.type_covered + // RRSIG, } impl RecordType { pub fn as_str(&self) -> &'static str { match self { RecordType::A => "A", + RecordType::DS => "DS", RecordType::SOA => "SOA", RecordType::NS => "NS", } @@ -31,6 +36,7 @@ impl FromStr for RecordType { fn from_str(input: &str) -> CoreResult { let record_type = match input { "A" => Self::A, + "DS" => Self::DS, "SOA" => Self::SOA, "NS" => Self::NS, _ => return Err(format!("unknown record type: {input}").into()), @@ -40,14 +46,59 @@ impl FromStr for RecordType { } } +impl fmt::Display for RecordType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + RecordType::A => "A", + RecordType::DS => "DS", + RecordType::NS => "NS", + RecordType::SOA => "SOA", + }; + + f.write_str(s) + } +} + #[derive(Debug)] #[allow(clippy::upper_case_acronyms)] pub enum Record { A(A), + DS(DS), + NS(NS), RRSIG(RRSIG), SOA(SOA), } +impl From for Record { + fn from(v: DS) -> Self { + Self::DS(v) + } +} + +impl From for Record { + fn from(v: A) -> Self { + Self::A(v) + } +} + +impl From for Record { + fn from(v: NS) -> Self { + Self::NS(v) + } +} + +impl From for Record { + fn from(v: RRSIG) -> Self { + Self::RRSIG(v) + } +} + +impl From for Record { + fn from(v: SOA) -> Self { + Self::SOA(v) + } +} + impl Record { pub fn try_into_a(self) -> CoreResult { if let Self::A(v) = self { @@ -68,6 +119,24 @@ impl Record { pub fn is_soa(&self) -> bool { matches!(self, Self::SOA(..)) } + + pub fn a(fqdn: FQDN, ipv4_addr: Ipv4Addr) -> Self { + A { + fqdn, + ttl: DEFAULT_TTL, + ipv4_addr, + } + .into() + } + + pub fn ns(zone: FQDN, nameserver: FQDN) -> Self { + NS { + zone, + ttl: DEFAULT_TTL, + nameserver, + } + .into() + } } impl FromStr for Record { @@ -91,6 +160,18 @@ impl FromStr for Record { } } +impl fmt::Display for Record { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Record::A(a) => write!(f, "{a}"), + Record::DS(ds) => write!(f, "{ds}"), + Record::NS(ns) => write!(f, "{ns}"), + Record::RRSIG(rrsig) => write!(f, "{rrsig}"), + Record::SOA(soa) => write!(f, "{soa}"), + } + } +} + #[derive(Debug)] pub struct A { pub fqdn: FQDN, @@ -129,6 +210,189 @@ impl FromStr for A { } } +impl fmt::Display for A { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + fqdn, + ttl, + ipv4_addr, + } = self; + + write!(f, "{fqdn}\t{ttl}\tIN\tA\t{ipv4_addr}") + } +} + +// integer types chosen based on bit sizes in section 2.1 of RFC4034 +#[derive(Clone, Debug)] +pub struct DNSKEY { + pub zone: FQDN, + pub ttl: u32, + pub flags: u16, + pub protocol: u8, + pub algorithm: u8, + pub public_key: String, +} + +impl DNSKEY { + /// 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 { + type Err = Error; + + fn from_str(input: &str) -> CoreResult { + let mut columns = input.split_whitespace(); + + let [Some(zone), Some(ttl), Some(class), Some(record_type), Some(flags), Some(protocol), Some(algorithm)] = + array::from_fn(|_| columns.next()) + else { + return Err("expected at least 7 columns".into()); + }; + + if record_type != "DNSKEY" { + return Err(format!("tried to parse `{record_type}` record as a DNSKEY record").into()); + } + + if class != "IN" { + return Err(format!("unknown class: {class}").into()); + } + + let mut public_key = String::new(); + for column in columns { + public_key.push_str(column); + } + + Ok(Self { + zone: zone.parse()?, + ttl: ttl.parse()?, + flags: flags.parse()?, + protocol: protocol.parse()?, + algorithm: algorithm.parse()?, + public_key, + }) + } +} + +impl fmt::Display for DNSKEY { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + zone, + ttl, + flags, + protocol, + algorithm, + public_key, + } = self; + + write!( + f, + "{zone}\t{ttl}\tIN\tDNSKEY\t{flags} {protocol} {algorithm}" + )?; + + write_split_long_string(f, public_key) + } +} + +#[derive(Clone, Debug)] +pub struct DS { + zone: FQDN, + ttl: u32, + key_tag: u16, + algorithm: u8, + digest_type: u8, + digest: String, +} + +impl FromStr for DS { + type Err = Error; + + fn from_str(input: &str) -> CoreResult { + let mut columns = input.split_whitespace(); + + let [Some(zone), Some(ttl), Some(class), Some(record_type), Some(key_tag), Some(algorithm), Some(digest_type)] = + array::from_fn(|_| columns.next()) + else { + return Err("expected at least 7 columns".into()); + }; + + let expected = "DS"; + if record_type != expected { + return Err( + format!("tried to parse `{record_type}` entry as a {expected} entry").into(), + ); + } + + if class != "IN" { + return Err(format!("unknown class: {class}").into()); + } + + let mut digest = String::new(); + for column in columns { + digest.push_str(column); + } + + Ok(Self { + zone: zone.parse()?, + ttl: ttl.parse()?, + key_tag: key_tag.parse()?, + algorithm: algorithm.parse()?, + digest_type: digest_type.parse()?, + digest, + }) + } +} + +impl fmt::Display for DS { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + zone, + ttl, + key_tag, + algorithm, + digest_type, + digest, + } = self; + + write!( + f, + "{zone}\t{ttl}\tIN\tDS\t{key_tag} {algorithm} {digest_type}" + )?; + + write_split_long_string(f, digest) + } +} + +#[derive(Debug)] +pub struct NS { + pub zone: FQDN, + pub ttl: u32, + pub nameserver: FQDN, +} + +impl fmt::Display for NS { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + zone, + ttl, + nameserver, + } = self; + + write!(f, "{zone}\t{ttl}\tIN\tNS {nameserver}") + } +} + #[allow(clippy::upper_case_acronyms)] #[derive(Debug)] pub struct RRSIG { @@ -190,6 +454,28 @@ impl FromStr for RRSIG { } } +impl fmt::Display for RRSIG { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + fqdn, + ttl, + type_covered, + algorithm, + labels, + original_ttl, + signature_expiration, + signature_inception, + key_tag, + signer_name, + signature, + } = self; + + write!(f, "{fqdn}\t{ttl}\tIN\tRRSIG\t{type_covered} {algorithm} {labels} {original_ttl} {signature_expiration} {signature_inception} {key_tag} {signer_name}")?; + + write_split_long_string(f, signature) + } +} + #[allow(clippy::upper_case_acronyms)] #[derive(Debug)] pub struct SOA { @@ -197,11 +483,7 @@ pub struct SOA { pub ttl: u32, pub nameserver: FQDN, pub admin: FQDN, - pub serial: u32, - pub refresh: u32, - pub retry: u32, - pub expire: u32, - pub minimum: u32, + pub settings: SoaSettings, } impl FromStr for SOA { @@ -229,34 +511,198 @@ impl FromStr for SOA { ttl: ttl.parse()?, nameserver: nameserver.parse()?, admin: admin.parse()?, - serial: serial.parse()?, - refresh: refresh.parse()?, - retry: retry.parse()?, - expire: expire.parse()?, - minimum: minimum.parse()?, + settings: SoaSettings { + serial: serial.parse()?, + refresh: refresh.parse()?, + retry: retry.parse()?, + expire: expire.parse()?, + minimum: minimum.parse()?, + }, }) } } +impl fmt::Display for SOA { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + zone, + ttl, + nameserver, + admin, + settings, + } = self; + + write!(f, "{zone}\t{ttl}\tIN\tSOA\t{nameserver} {admin} {settings}") + } +} + +#[derive(Debug)] +pub struct SoaSettings { + pub serial: u32, + pub refresh: u32, + pub retry: u32, + pub expire: u32, + pub minimum: u32, +} + +impl Default for SoaSettings { + fn default() -> Self { + Self { + serial: 2024010101, + refresh: 1800, // 30 minutes + retry: 900, // 15 minutes + expire: 604800, // 1 week + minimum: 86400, // 1 day + } + } +} + +impl fmt::Display for SoaSettings { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + serial, + refresh, + retry, + expire, + minimum, + } = self; + + write!(f, "{serial} {refresh} {retry} {expire} {minimum}") + } +} + +fn write_split_long_string(f: &mut fmt::Formatter<'_>, field: &str) -> fmt::Result { + for (index, c) in field.chars().enumerate() { + if index % 56 == 0 { + f.write_char(' ')?; + } + f.write_char(c)?; + } + Ok(()) +} + #[cfg(test)] mod tests { use super::*; #[test] - fn can_parse_a_record() -> Result<()> { - let input = "a.root-servers.net. 3600000 IN A 198.41.0.4"; - let a: A = input.parse()?; + fn a() -> Result<()> { + // dig A a.root-servers.net + let input = "a.root-servers.net. 77859 IN A 198.41.0.4"; + let a @ A { + fqdn, + ttl, + ipv4_addr, + } = &input.parse()?; - assert_eq!("a.root-servers.net.", a.fqdn.as_str()); - assert_eq!(3600000, a.ttl); - assert_eq!(Ipv4Addr::new(198, 41, 0, 4), a.ipv4_addr); + assert_eq!("a.root-servers.net.", fqdn.as_str()); + assert_eq!(77859, *ttl); + assert_eq!(Ipv4Addr::new(198, 41, 0, 4), *ipv4_addr); + + let output = a.to_string(); + assert_eq!(output, input); Ok(()) } #[test] - fn can_parse_soa_record() -> Result<()> { - let input = ". 15633 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2024020501 1800 900 604800 86400"; + fn dnskey() -> Result<()> { + // dig DNSKEY . + let input = ". 1116 IN DNSKEY 257 3 8 AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3 +/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kv ArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF 0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+e oZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfd RUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwN R1AkUTV74bU="; + + let dnskey @ DNSKEY { + zone, + ttl, + flags, + protocol, + algorithm, + public_key, + } = &input.parse()?; + + assert_eq!(FQDN::ROOT, *zone); + assert_eq!(1116, *ttl); + assert_eq!(257, *flags); + assert_eq!(3, *protocol); + assert_eq!(8, *algorithm); + let expected = "AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3+/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kvArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+eoZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfdRUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwNR1AkUTV74bU="; + assert_eq!(expected, public_key); + + let output = dnskey.to_string(); + assert_eq!(output, input); + + Ok(()) + } + + #[test] + fn ds() -> Result<()> { + // dig DS com. + let input = "com. 7612 IN DS 19718 13 2 8ACBB0CD28F41250A80A491389424D341522D946B0DA0C0291F2D3D7 71D7805A"; + + let ds @ DS { + zone, + ttl, + key_tag, + algorithm, + digest_type, + digest, + } = &input.parse()?; + + assert_eq!(FQDN::COM, *zone); + assert_eq!(7612, *ttl); + assert_eq!(19718, *key_tag); + assert_eq!(13, *algorithm); + assert_eq!(2, *digest_type); + let expected = "8ACBB0CD28F41250A80A491389424D341522D946B0DA0C0291F2D3D771D7805A"; + assert_eq!(expected, digest); + + let output = ds.to_string(); + assert_eq!(output, input); + + Ok(()) + } + + #[test] + fn rrsig() -> Result<()> { + // dig +dnssec SOA . + let input = ". 1800 IN RRSIG SOA 7 0 1800 20240306132701 20240207132701 11264 . wXpRU4elJPGYm2kgVVsIwGf1IkYJcQ3UE4mwmItWdxj0XWSWY07MO4Ll DMJgsE0u64Q/345Ck7+aQ904uLebwCvpFnsmkyCxk82XIAfHN9FiwzSy qoR/zZEvBONaej3vrvsqPwh8q/pvypLft9647HcFdwY0juzZsbrAaDAX 8WY="; + + let rrsig @ RRSIG { + fqdn, + ttl, + type_covered, + algorithm, + labels, + original_ttl, + signature_expiration, + signature_inception, + key_tag, + signer_name, + signature, + } = &input.parse()?; + + assert_eq!(FQDN::ROOT, *fqdn); + assert_eq!(1800, *ttl); + assert_eq!(RecordType::SOA, *type_covered); + assert_eq!(7, *algorithm); + assert_eq!(0, *labels); + assert_eq!(1800, *original_ttl); + assert_eq!(20240306132701, *signature_expiration); + assert_eq!(20240207132701, *signature_inception); + assert_eq!(11264, *key_tag); + assert_eq!(FQDN::ROOT, *signer_name); + let expected = "wXpRU4elJPGYm2kgVVsIwGf1IkYJcQ3UE4mwmItWdxj0XWSWY07MO4LlDMJgsE0u64Q/345Ck7+aQ904uLebwCvpFnsmkyCxk82XIAfHN9FiwzSyqoR/zZEvBONaej3vrvsqPwh8q/pvypLft9647HcFdwY0juzZsbrAaDAX8WY="; + assert_eq!(expected, signature); + + let output = rrsig.to_string(); + assert_eq!(input, output); + + Ok(()) + } + + #[test] + fn soa() -> Result<()> { + // dig SOA . + let input = ". 15633 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2024020501 1800 900 604800 86400"; let soa: SOA = input.parse()?; @@ -264,32 +710,15 @@ mod tests { assert_eq!(15633, soa.ttl); assert_eq!("a.root-servers.net.", soa.nameserver.as_str()); assert_eq!("nstld.verisign-grs.com.", soa.admin.as_str()); - assert_eq!(2024020501, soa.serial); - assert_eq!(1800, soa.refresh); - assert_eq!(900, soa.retry); - assert_eq!(604800, soa.expire); - assert_eq!(86400, soa.minimum); + let settings = &soa.settings; + assert_eq!(2024020501, settings.serial); + assert_eq!(1800, settings.refresh); + assert_eq!(900, settings.retry); + assert_eq!(604800, settings.expire); + assert_eq!(86400, settings.minimum); - Ok(()) - } - - #[test] - fn can_parse_rrsig_record() -> Result<()> { - let input = ". 1800 IN RRSIG SOA 7 0 1800 20240306132701 20240207132701 11264 . wXpRU4elJPGYm2kgVVsIwGf1IkYJcQ3UE4mwmItWdxj0XWSWY07MO4Ll DMJgsE0u64Q/345Ck7+aQ904uLebwCvpFnsmkyCxk82XIAfHN9FiwzSy qoR/zZEvBONaej3vrvsqPwh8q/pvypLft9647HcFdwY0juzZsbrAaDAX 8WY="; - - let rrsig: RRSIG = input.parse()?; - - assert_eq!(FQDN::ROOT, rrsig.fqdn); - assert_eq!(1800, rrsig.ttl); - assert_eq!(RecordType::SOA, rrsig.type_covered); - assert_eq!(7, rrsig.algorithm); - assert_eq!(0, rrsig.labels); - assert_eq!(20240306132701, rrsig.signature_expiration); - assert_eq!(20240207132701, rrsig.signature_inception); - assert_eq!(11264, rrsig.key_tag); - assert_eq!(FQDN::ROOT, rrsig.signer_name); - let expected = "wXpRU4elJPGYm2kgVVsIwGf1IkYJcQ3UE4mwmItWdxj0XWSWY07MO4LlDMJgsE0u64Q/345Ck7+aQ904uLebwCvpFnsmkyCxk82XIAfHN9FiwzSyqoR/zZEvBONaej3vrvsqPwh8q/pvypLft9647HcFdwY0juzZsbrAaDAX8WY="; - assert_eq!(expected, rrsig.signature); + let output = soa.to_string(); + assert_eq!(output, input); Ok(()) } diff --git a/packages/dns-test/src/trust_anchor.rs b/packages/dns-test/src/trust_anchor.rs index b14173e2..e6333bbe 100644 --- a/packages/dns-test/src/trust_anchor.rs +++ b/packages/dns-test/src/trust_anchor.rs @@ -1,6 +1,6 @@ use core::fmt; -use crate::zone_file::DNSKEY; +use crate::record::DNSKEY; pub struct TrustAnchor { keys: Vec, diff --git a/packages/dns-test/src/tshark.rs b/packages/dns-test/src/tshark.rs index f230d31d..ecc39740 100644 --- a/packages/dns-test/src/tshark.rs +++ b/packages/dns-test/src/tshark.rs @@ -246,7 +246,7 @@ struct Ip { mod tests { use crate::client::{Client, Dnssec, Recurse}; use crate::name_server::NameServer; - use crate::record::RecordType; + use crate::record::{Record, RecordType}; use crate::zone_file::Root; use crate::{Implementation, Network, Resolver, TrustAnchor, FQDN}; @@ -297,8 +297,8 @@ mod tests { let mut nameservers_ns = NameServer::new(Implementation::Unbound, FQDN("nameservers.com.")?, network)?; nameservers_ns - .a(root_ns.fqdn().clone(), root_ns.ipv4_addr()) - .a(com_ns.fqdn().clone(), com_ns.ipv4_addr()); + .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) + .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())); let nameservers_ns = nameservers_ns.start()?; com_ns.referral( diff --git a/packages/dns-test/src/zone_file.rs b/packages/dns-test/src/zone_file.rs index 53c498df..e4bb46c5 100644 --- a/packages/dns-test/src/zone_file.rs +++ b/packages/dns-test/src/zone_file.rs @@ -1,72 +1,63 @@ -//! BIND-style zone file +//! BIND-style zone files //! //! Note that //! - the `@` syntax is not used to avoid relying on the order of the entries //! - relative domain names are not used; all domain names must be in fully-qualified form -use core::{array, fmt}; +use core::fmt; +use std::array; use std::net::Ipv4Addr; use std::str::FromStr; -use crate::{Error, FQDN}; +use crate::record::{self, Record, SOA}; +use crate::{Error, Result, DEFAULT_TTL, FQDN}; pub struct ZoneFile { - pub origin: FQDN, - pub ttl: u32, + origin: FQDN, pub soa: SOA, - pub entries: Vec, + pub records: Vec, } impl ZoneFile { /// Convenience constructor that uses "reasonable" defaults - pub fn new(origin: FQDN, soa: SOA) -> Self { + pub fn new(soa: SOA) -> Self { Self { - origin, - ttl: 1800, + origin: soa.zone.clone(), soa, - entries: Vec::new(), + records: Vec::new(), } } - /// Appends an entry - pub fn entry(&mut self, entry: impl Into) { - self.entries.push(entry.into()) + /// Adds the given `record` to the zone file + pub fn add(&mut self, record: impl Into) { + self.records.push(record.into()) } - /// Appends a NS + A entry pair + /// Shortcut method for adding a referral (NS + A record pair) pub fn referral(&mut self, zone: FQDN, nameserver: FQDN, ipv4_addr: Ipv4Addr) { - self.entry(NS { - zone: zone.clone(), - nameserver: nameserver.clone(), - }); - self.entry(A { - fqdn: nameserver, - ipv4_addr, - }); + self.add(Record::ns(zone, nameserver.clone())); + self.add(Record::a(nameserver, ipv4_addr)); + } + + pub(crate) fn origin(&self) -> &FQDN { + &self.origin } } impl fmt::Display for ZoneFile { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { - origin, - ttl, - soa, - entries, - } = self; + let Self { soa, records, .. } = self; - writeln!(f, "$ORIGIN {origin}")?; - writeln!(f, "$TTL {ttl}")?; writeln!(f, "{soa}")?; - - for entry in entries { - writeln!(f, "{entry}")?; + for record in records { + writeln!(f, "{record}")?; } Ok(()) } } +/// A root (server) hint pub struct Root { pub ipv4_addr: Ipv4Addr, pub ns: FQDN, @@ -79,7 +70,7 @@ impl Root { Self { ipv4_addr, ns, - ttl: 3600000, // 1000 hours + ttl: DEFAULT_TTL, } } } @@ -93,100 +84,47 @@ impl fmt::Display for Root { } } -pub enum Entry { - A(A), - DNSKEY(DNSKEY), - DS(DS), - NS(NS), -} - -impl From for Entry { - fn from(v: DS) -> Self { - Self::DS(v) - } -} - -impl From for Entry { - fn from(v: A) -> Self { - Self::A(v) - } -} - -impl From for Entry { - fn from(v: NS) -> Self { - Self::NS(v) - } -} - -impl fmt::Display for Entry { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Entry::A(a) => a.fmt(f), - Entry::DNSKEY(dnskey) => dnskey.fmt(f), - Entry::DS(ds) => ds.fmt(f), - Entry::NS(ns) => ns.fmt(f), - } - } -} - -#[derive(Clone)] -pub struct A { - pub fqdn: FQDN, - pub ipv4_addr: Ipv4Addr, -} - -impl fmt::Display for A { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { fqdn, ipv4_addr } = self; - - write!(f, "{fqdn}\tIN\tA\t{ipv4_addr}") - } -} - -// integer types chosen based on bit sizes in section 2.1 of RFC4034 -#[derive(Clone, Debug)] -pub struct DNSKEY { +// NOTE compared to `record::DNSKEY`, this zone file entry lacks the TTL field +#[allow(clippy::upper_case_acronyms)] +pub(crate) struct DNSKEY { zone: FQDN, flags: u16, protocol: u8, algorithm: u8, public_key: String, - - // extra information in `+multiline` format and `ldns-keygen`'s output - bits: u16, - key_tag: u16, } impl DNSKEY { - pub fn bits(&self) -> u16 { - self.bits - } - - pub fn key_tag(&self) -> u16 { - self.key_tag - } - - /// formats the `DNSKEY` in the format `delv` expects - pub(super) fn delv(&self) -> String { + pub fn with_ttl(self, ttl: u32) -> record::DNSKEY { let Self { zone, flags, protocol, algorithm, public_key, - .. } = self; - format!("{zone} static-key {flags} {protocol} {algorithm} \"{public_key}\";\n") + record::DNSKEY { + zone, + ttl, + flags, + protocol, + algorithm, + public_key, + } } } impl FromStr for DNSKEY { type Err = Error; - fn from_str(input: &str) -> Result { - let (before, after) = input.split_once(';').ok_or("comment was not found")?; - let mut columns = before.split_whitespace(); + fn from_str(mut input: &str) -> Result { + // discard trailing comment + if let Some((before, _after)) = input.split_once(';') { + input = before.trim(); + } + + let mut columns = input.split_whitespace(); let [Some(zone), Some(class), Some(record_type), Some(flags), Some(protocol), Some(algorithm), Some(public_key), None] = array::from_fn(|_| columns.next()) @@ -202,306 +140,39 @@ impl FromStr for DNSKEY { return Err(format!("unknown class: {class}").into()); } - // {id = 24975 (zsk), size = 1024b} - let error = "invalid comment syntax"; - let (id_expr, size_expr) = after.split_once(',').ok_or(error)?; - - // {id = 24975 (zsk) - let (id_lhs, id_rhs) = id_expr.split_once('=').ok_or(error)?; - if id_lhs.trim() != "{id" { - return Err(error.into()); - } - - // 24975 (zsk) - let (key_tag, _key_type) = id_rhs.trim().split_once(' ').ok_or(error)?; - - // size = 1024b} - let (size_lhs, size_rhs) = size_expr.split_once('=').ok_or(error)?; - if size_lhs.trim() != "size" { - return Err(error.into()); - } - let bits = size_rhs.trim().strip_suffix("b}").ok_or(error)?.parse()?; - Ok(Self { zone: zone.parse()?, flags: flags.parse()?, protocol: protocol.parse()?, algorithm: algorithm.parse()?, public_key: public_key.to_string(), - - key_tag: key_tag.parse()?, - bits, }) } } -impl fmt::Display for DNSKEY { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { - zone, - flags, - protocol, - algorithm, - public_key, - bits: _, - key_tag: _, - } = self; - - write!( - f, - "{zone}\tIN\tDNSKEY\t{flags}\t{protocol}\t{algorithm}\t{public_key}" - ) - } -} - -#[derive(Clone)] -pub struct DS { - zone: FQDN, - _ttl: u32, - key_tag: u16, - algorithm: u8, - digest_type: u8, - digest: String, -} - -impl FromStr for DS { - type Err = Error; - - fn from_str(input: &str) -> Result { - let mut columns = input.split_whitespace(); - - let [Some(zone), Some(ttl), Some(class), Some(record_type), Some(key_tag), Some(algorithm), Some(digest_type), Some(digest), None] = - array::from_fn(|_| columns.next()) - else { - return Err("expected 8 columns".into()); - }; - - let expected = "DS"; - if record_type != expected { - return Err( - format!("tried to parse `{record_type}` entry as a {expected} entry").into(), - ); - } - - if class != "IN" { - return Err(format!("unknown class: {class}").into()); - } - - Ok(Self { - zone: zone.parse()?, - _ttl: ttl.parse()?, - key_tag: key_tag.parse()?, - algorithm: algorithm.parse()?, - digest_type: digest_type.parse()?, - digest: digest.to_string(), - }) - } -} - -/// NOTE does NOT include the TTL field -impl fmt::Display for DS { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { - zone, - _ttl, - key_tag, - algorithm, - digest_type, - digest, - } = self; - - write!( - f, - "{zone}\tIN\tDS\t{key_tag}\t{algorithm}\t{digest_type}\t{digest}" - ) - } -} - -pub struct NS { - pub zone: FQDN, - pub nameserver: FQDN, -} - -impl fmt::Display for NS { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { - zone, - nameserver: ns, - } = self; - - write!(f, "{zone}\tIN\tNS\t{ns}") - } -} - -pub struct SOA { - pub zone: FQDN, - pub nameserver: FQDN, - pub admin: FQDN, - pub settings: SoaSettings, -} - -impl fmt::Display for SOA { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { - zone, - nameserver: ns, - admin, - settings, - } = self; - - write!(f, "{zone}\tIN\tSOA\t{ns}\t{admin}\t{settings}") - } -} - -pub struct SoaSettings { - pub serial: u32, - pub refresh: u32, - pub retry: u32, - pub expire: u32, - pub minimum: u32, -} - -impl Default for SoaSettings { - fn default() -> Self { - Self { - serial: 2024010101, - refresh: 1800, // 30 minutes - retry: 900, // 15 minutes - expire: 604800, // 1 week - minimum: 86400, // 1 day - } - } -} - -impl fmt::Display for SoaSettings { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Self { - serial, - refresh, - retry, - expire, - minimum, - } = self; - - write!(f, "( {serial} {refresh} {retry} {expire} {minimum} )") - } -} - #[cfg(test)] mod tests { - use crate::Result; - use super::*; #[test] - fn a_to_string() -> Result<()> { - let expected = "e.gtld-servers.net. IN A 192.12.94.30"; - let a = example_a()?; - assert_eq!(expected, a.to_string()); + fn dnskey() -> Result<()> { + let input = ". IN DNSKEY 256 3 7 AwEAAaCUpg+5lH7vart4WiMw4lbbkTNKfkvoyXWsAj09Cc5lT1bFo6sS7o4evhzXU9+iDGZkWZnnkwWg2thXfGgNdfQNTKW/Owz9UMDGv5yjkANKI3fI4jHn7Xp1qIZAwZG0W3RU26s7vkKWVcmA3mrKlDIX9r4BRIZrBVOtNgiHydbB ;{id = 42933 (zsk), size = 1024b}"; + + let DNSKEY { + zone, + flags, + protocol, + algorithm, + public_key, + } = input.parse()?; + + assert_eq!(FQDN::ROOT, zone); + assert_eq!(256, flags); + assert_eq!(3, protocol); + assert_eq!(7, algorithm); + let expected = "AwEAAaCUpg+5lH7vart4WiMw4lbbkTNKfkvoyXWsAj09Cc5lT1bFo6sS7o4evhzXU9+iDGZkWZnnkwWg2thXfGgNdfQNTKW/Owz9UMDGv5yjkANKI3fI4jHn7Xp1qIZAwZG0W3RU26s7vkKWVcmA3mrKlDIX9r4BRIZrBVOtNgiHydbB"; + assert_eq!(expected, public_key); Ok(()) } - - #[test] - fn ns_to_string() -> Result<()> { - let expected = "com. IN NS e.gtld-servers.net."; - let ns = example_ns()?; - assert_eq!(expected, ns.to_string()); - - Ok(()) - } - - #[test] - fn root_to_string() -> Result<()> { - let expected = ". 3600000 NS a.root-servers.net. -a.root-servers.net. 3600000 A 198.41.0.4"; - let root = Root::new(FQDN("a.root-servers.net.")?, Ipv4Addr::new(198, 41, 0, 4)); - assert_eq!(expected, root.to_string()); - Ok(()) - } - - #[test] - fn soa_to_string() -> Result<()> { - let expected = - ". IN SOA a.root-servers.net. nstld.verisign-grs.com. ( 2024010101 1800 900 604800 86400 )"; - let soa = example_soa()?; - assert_eq!(expected, soa.to_string()); - - Ok(()) - } - - #[test] - fn zone_file_to_string() -> Result<()> { - let expected = "$ORIGIN . -$TTL 1800 -. IN SOA a.root-servers.net. nstld.verisign-grs.com. ( 2024010101 1800 900 604800 86400 ) -com. IN NS e.gtld-servers.net. -e.gtld-servers.net. IN A 192.12.94.30 -"; - let mut zone = ZoneFile::new(FQDN::ROOT, example_soa()?); - zone.entry(example_ns()?); - zone.entry(example_a()?); - - assert_eq!(expected, zone.to_string()); - - Ok(()) - } - - // not quite roundtrip because we drop the TTL field when doing `to_string` - #[test] - fn ds_roundtrip() -> Result<()> { - let input = - ". 1800 IN DS 31153 7 2 7846338aaacde9cc9518f1f450082adc015a207c45a1e69d6e660e6836f4ef3b"; - let ds: DS = input.parse()?; - let output = ds.to_string(); - - let expected = - ". IN DS 31153 7 2 7846338aaacde9cc9518f1f450082adc015a207c45a1e69d6e660e6836f4ef3b"; - assert_eq!(expected, output); - - Ok(()) - } - - #[test] - fn dnskey_roundtrip() -> Result<()> { - let input = "example.com. IN DNSKEY 256 3 7 AwEAAdIpMlio4GJas7GbIZ9xRpzpB2pf4SxBJcsquN/0yNBPGNE2rzcFykqMAKmLwypk1/1q/EdHVa4tQ5RlK0w09CRhgSXfCaph+yLNJKpiPyuVcXKl2k0RnO4p835sgVEUIvx8qGTDo7c7DA9UBje+/3ViFKqVhOBaWyT6gHAmNVpb ;{id = 24975 (zsk), size = 1024b}"; - - let dnskey: DNSKEY = input.parse()?; - - assert_eq!(256, dnskey.flags); - assert_eq!(3, dnskey.protocol); - assert_eq!(7, dnskey.algorithm); - let expected = "AwEAAdIpMlio4GJas7GbIZ9xRpzpB2pf4SxBJcsquN/0yNBPGNE2rzcFykqMAKmLwypk1/1q/EdHVa4tQ5RlK0w09CRhgSXfCaph+yLNJKpiPyuVcXKl2k0RnO4p835sgVEUIvx8qGTDo7c7DA9UBje+/3ViFKqVhOBaWyT6gHAmNVpb"; - assert_eq!(expected, dnskey.public_key); - assert_eq!(1024, dnskey.bits()); - assert_eq!(24975, dnskey.key_tag()); - - let output = dnskey.to_string(); - assert!(input.starts_with(&output)); - - Ok(()) - } - - fn example_a() -> Result { - Ok(A { - fqdn: FQDN("e.gtld-servers.net.")?, - ipv4_addr: Ipv4Addr::new(192, 12, 94, 30), - }) - } - - fn example_ns() -> Result { - Ok(NS { - zone: FQDN::COM, - nameserver: FQDN("e.gtld-servers.net.")?, - }) - } - - fn example_soa() -> Result { - Ok(SOA { - zone: FQDN::ROOT, - nameserver: FQDN("a.root-servers.net.")?, - admin: FQDN("nstld.verisign-grs.com.")?, - settings: SoaSettings::default(), - }) - } } From 57a1fc923177e0ce1608e6859a1e4384ebcead96 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 20 Feb 2024 15:10:10 +0100 Subject: [PATCH 076/124] parse more record types --- Cargo.lock | 23 + packages/dns-test/Cargo.toml | 1 + packages/dns-test/src/record.rs | 538 ++++++++++++++---- .../src/{zone_file.rs => zone_file/mod.rs} | 47 ++ packages/dns-test/src/zone_file/muster.zone | 11 + 5 files changed, 497 insertions(+), 123 deletions(-) rename packages/dns-test/src/{zone_file.rs => zone_file/mod.rs} (79%) create mode 100644 packages/dns-test/src/zone_file/muster.zone diff --git a/Cargo.lock b/Cargo.lock index 2a3639b3..23e77b6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -143,12 +143,19 @@ dependencies = [ "serde", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "dns-test" version = "0.1.0" dependencies = [ "ctrlc", "minijinja", + "pretty_assertions", "serde", "serde_json", "serde_with", @@ -358,6 +365,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro2" version = "1.0.78" @@ -701,3 +718,9 @@ name = "windows_x86_64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" diff --git a/packages/dns-test/Cargo.toml b/packages/dns-test/Cargo.toml index 7dc0924a..eacbbf9c 100644 --- a/packages/dns-test/Cargo.toml +++ b/packages/dns-test/Cargo.toml @@ -18,3 +18,4 @@ doctest = false [dev-dependencies] ctrlc = "3.4.2" +pretty_assertions = "1.4.0" diff --git a/packages/dns-test/src/record.rs b/packages/dns-test/src/record.rs index c16a49f8..a6e1aef3 100644 --- a/packages/dns-test/src/record.rs +++ b/packages/dns-test/src/record.rs @@ -3,72 +3,77 @@ use core::result::Result as CoreResult; use core::str::FromStr; use core::{array, fmt}; +use std::any; use std::fmt::Write; use std::net::Ipv4Addr; use crate::{Error, Result, DEFAULT_TTL, FQDN}; -#[allow(clippy::upper_case_acronyms)] -#[derive(Debug, PartialEq)] -pub enum RecordType { - A, - DS, - NS, - SOA, - // excluded because cannot appear in RRSIG.type_covered - // RRSIG, -} +const CLASS: &str = "IN"; // "internet" -impl RecordType { - pub fn as_str(&self) -> &'static str { - match self { - RecordType::A => "A", - RecordType::DS => "DS", - RecordType::SOA => "SOA", - RecordType::NS => "NS", +macro_rules! record_types { + ($($variant:ident),*) => { + #[allow(clippy::upper_case_acronyms)] + #[derive(Debug, PartialEq)] + pub enum RecordType { + $($variant),* } - } + + impl RecordType { + pub fn as_str(&self) -> &'static str { + match self { + $(Self::$variant => stringify!($variant)),* + } + } + } + + impl FromStr for RecordType { + type Err = Error; + + fn from_str(input: &str) -> Result { + $(if input == stringify!($variant) { + return Ok(Self::$variant); + })* + + Err(format!("unknown record type: {input}").into()) + } + } + + impl fmt::Display for RecordType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } + } + }; } -impl FromStr for RecordType { - type Err = Error; - - fn from_str(input: &str) -> CoreResult { - let record_type = match input { - "A" => Self::A, - "DS" => Self::DS, - "SOA" => Self::SOA, - "NS" => Self::NS, - _ => return Err(format!("unknown record type: {input}").into()), - }; - - Ok(record_type) - } -} - -impl fmt::Display for RecordType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match self { - RecordType::A => "A", - RecordType::DS => "DS", - RecordType::NS => "NS", - RecordType::SOA => "SOA", - }; - - f.write_str(s) - } -} +record_types!(A, AAAA, DNSKEY, DS, MX, NS, NSEC3, NSEC3PARAM, RRSIG, SOA, TXT); #[derive(Debug)] #[allow(clippy::upper_case_acronyms)] pub enum Record { A(A), + DNSKEY(DNSKEY), DS(DS), NS(NS), + NSEC3(NSEC3), + NSEC3PARAM(NSEC3PARAM), RRSIG(RRSIG), SOA(SOA), } +impl From for Record { + fn from(v: NSEC3) -> Self { + Self::NSEC3(v) + } +} + +impl From for Record { + fn from(v: DNSKEY) -> Self { + Self::DNSKEY(v) + } +} + impl From for Record { fn from(v: DS) -> Self { Self::DS(v) @@ -150,7 +155,11 @@ impl FromStr for Record { let record = match record_type { "A" => Record::A(input.parse()?), - "NS" => todo!(), + "DNSKEY" => Record::DNSKEY(input.parse()?), + "DS" => Record::DS(input.parse()?), + "NS" => Record::NS(input.parse()?), + "NSEC3" => Record::NSEC3(input.parse()?), + "NSEC3PARAM" => Record::NSEC3PARAM(input.parse()?), "RRSIG" => Record::RRSIG(input.parse()?), "SOA" => Record::SOA(input.parse()?), _ => return Err(format!("unknown record type: {record_type}").into()), @@ -165,7 +174,10 @@ impl fmt::Display for Record { match self { Record::A(a) => write!(f, "{a}"), Record::DS(ds) => write!(f, "{ds}"), + Record::DNSKEY(dnskey) => write!(f, "{dnskey}"), Record::NS(ns) => write!(f, "{ns}"), + Record::NSEC3(nsec3) => write!(f, "{nsec3}"), + Record::NSEC3PARAM(nsec3param) => write!(f, "{nsec3param}"), Record::RRSIG(rrsig) => write!(f, "{rrsig}"), Record::SOA(soa) => write!(f, "{soa}"), } @@ -191,16 +203,8 @@ impl FromStr for A { return Err("expected 5 columns".into()); }; - let expected = "A"; - if record_type != expected { - return Err( - format!("tried to parse `{record_type}` record as an {expected} record").into(), - ); - } - - if class != "IN" { - return Err(format!("unknown class: {class}").into()); - } + check_record_type::(record_type)?; + check_class(class)?; Ok(Self { fqdn: fqdn.parse()?, @@ -218,7 +222,8 @@ impl fmt::Display for A { ipv4_addr, } = self; - write!(f, "{fqdn}\t{ttl}\tIN\tA\t{ipv4_addr}") + let record_type = unqualified_type_name::(); + write!(f, "{fqdn}\t{ttl}\t{CLASS}\t{record_type}\t{ipv4_addr}") } } @@ -252,7 +257,11 @@ impl DNSKEY { impl FromStr for DNSKEY { type Err = Error; - fn from_str(input: &str) -> CoreResult { + fn from_str(mut input: &str) -> Result { + if let Some((rr, _comment)) = input.rsplit_once(" ;") { + input = rr.trim_end(); + } + let mut columns = input.split_whitespace(); let [Some(zone), Some(ttl), Some(class), Some(record_type), Some(flags), Some(protocol), Some(algorithm)] = @@ -261,13 +270,8 @@ impl FromStr for DNSKEY { return Err("expected at least 7 columns".into()); }; - if record_type != "DNSKEY" { - return Err(format!("tried to parse `{record_type}` record as a DNSKEY record").into()); - } - - if class != "IN" { - return Err(format!("unknown class: {class}").into()); - } + check_record_type::(record_type)?; + check_class(class)?; let mut public_key = String::new(); for column in columns { @@ -296,9 +300,10 @@ impl fmt::Display for DNSKEY { public_key, } = self; + let record_type = unqualified_type_name::(); write!( f, - "{zone}\t{ttl}\tIN\tDNSKEY\t{flags} {protocol} {algorithm}" + "{zone}\t{ttl}\t{CLASS}\t{record_type}\t{flags} {protocol} {algorithm}" )?; write_split_long_string(f, public_key) @@ -318,7 +323,7 @@ pub struct DS { impl FromStr for DS { type Err = Error; - fn from_str(input: &str) -> CoreResult { + fn from_str(input: &str) -> Result { let mut columns = input.split_whitespace(); let [Some(zone), Some(ttl), Some(class), Some(record_type), Some(key_tag), Some(algorithm), Some(digest_type)] = @@ -327,16 +332,8 @@ impl FromStr for DS { return Err("expected at least 7 columns".into()); }; - let expected = "DS"; - if record_type != expected { - return Err( - format!("tried to parse `{record_type}` entry as a {expected} entry").into(), - ); - } - - if class != "IN" { - return Err(format!("unknown class: {class}").into()); - } + check_record_type::(record_type)?; + check_class(class)?; let mut digest = String::new(); for column in columns { @@ -365,9 +362,10 @@ impl fmt::Display for DS { digest, } = self; + let record_type = unqualified_type_name::(); write!( f, - "{zone}\t{ttl}\tIN\tDS\t{key_tag} {algorithm} {digest_type}" + "{zone}\t{ttl}\t{CLASS}\t{record_type}\t{key_tag} {algorithm} {digest_type}" )?; write_split_long_string(f, digest) @@ -389,7 +387,158 @@ impl fmt::Display for NS { nameserver, } = self; - write!(f, "{zone}\t{ttl}\tIN\tNS {nameserver}") + let record_type = unqualified_type_name::(); + write!(f, "{zone}\t{ttl}\t{CLASS}\t{record_type}\t{nameserver}") + } +} + +impl FromStr for NS { + type Err = Error; + + fn from_str(input: &str) -> Result { + let mut columns = input.split_whitespace(); + + let [Some(zone), Some(ttl), Some(class), Some(record_type), Some(nameserver), None] = + array::from_fn(|_| columns.next()) + else { + return Err("expected 5 columns".into()); + }; + + check_record_type::(record_type)?; + check_class(class)?; + + Ok(Self { + zone: zone.parse()?, + ttl: ttl.parse()?, + nameserver: nameserver.parse()?, + }) + } +} + +// integer types chosen based on bit sizes in section 3.2 of RFC5155 +#[derive(Debug)] +pub struct NSEC3 { + pub fqdn: FQDN, + pub ttl: u32, + pub hash_alg: u8, + pub flags: u8, + pub iterations: u16, + pub salt: String, + pub next_hashed_owner_name: String, + pub record_types: Vec, +} + +impl FromStr for NSEC3 { + type Err = Error; + + fn from_str(input: &str) -> Result { + let mut columns = input.split_whitespace(); + + let [Some(fqdn), Some(ttl), Some(class), Some(record_type), Some(hash_alg), Some(flags), Some(iterations), Some(salt), Some(next_hashed_owner_name)] = + array::from_fn(|_| columns.next()) + else { + return Err("expected at least 9 columns".into()); + }; + + check_record_type::(record_type)?; + check_class(class)?; + + let mut record_types = vec![]; + for column in columns { + record_types.push(column.parse()?); + } + + Ok(Self { + fqdn: fqdn.parse()?, + ttl: ttl.parse()?, + hash_alg: hash_alg.parse()?, + flags: flags.parse()?, + iterations: iterations.parse()?, + salt: salt.to_string(), + next_hashed_owner_name: next_hashed_owner_name.to_string(), + record_types, + }) + } +} + +impl fmt::Display for NSEC3 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + fqdn, + ttl, + hash_alg, + flags, + iterations, + salt, + next_hashed_owner_name, + record_types, + } = self; + + let record_type = unqualified_type_name::(); + write!(f, "{fqdn}\t{ttl}\t{CLASS}\t{record_type}\t{hash_alg} {flags} {iterations} {salt} {next_hashed_owner_name}")?; + + for record_type in record_types { + write!(f, " {record_type}")?; + } + + Ok(()) + } +} + +// integer types chosen based on bit sizes in section 4.2 of RFC5155 +#[derive(Debug)] +pub struct NSEC3PARAM { + pub zone: FQDN, + pub ttl: u32, + pub hash_alg: u8, + pub flags: u8, + pub iterations: u16, +} + +impl FromStr for NSEC3PARAM { + type Err = Error; + + fn from_str(input: &str) -> Result { + let mut columns = input.split_whitespace(); + + let [Some(zone), Some(ttl), Some(class), Some(record_type), Some(hash_alg), Some(flags), Some(iterations), Some(dash), None] = + array::from_fn(|_| columns.next()) + else { + return Err("expected 8 columns".into()); + }; + + check_record_type::(record_type)?; + check_class(class)?; + + if dash != "-" { + todo!("salt is not implemented") + } + + Ok(Self { + zone: zone.parse()?, + ttl: ttl.parse()?, + hash_alg: hash_alg.parse()?, + flags: flags.parse()?, + iterations: iterations.parse()?, + }) + } +} + +impl fmt::Display for NSEC3PARAM { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + zone, + ttl, + hash_alg, + flags, + iterations, + } = self; + + let record_type = unqualified_type_name::(); + write!( + f, + "{zone}\t{ttl}\t{CLASS}\t{record_type}\t{hash_alg} {flags} {iterations} -" + ) } } @@ -422,16 +571,8 @@ impl FromStr for RRSIG { return Err("expected at least 12 columns".into()); }; - let expected = "RRSIG"; - if record_type != expected { - return Err( - format!("tried to parse `{record_type}` record as a {expected} record").into(), - ); - } - - if class != "IN" { - return Err(format!("unknown class: {class}").into()); - } + check_record_type::(record_type)?; + check_class(class)?; let mut signature = String::new(); for column in columns { @@ -470,7 +611,8 @@ impl fmt::Display for RRSIG { signature, } = self; - write!(f, "{fqdn}\t{ttl}\tIN\tRRSIG\t{type_covered} {algorithm} {labels} {original_ttl} {signature_expiration} {signature_inception} {key_tag} {signer_name}")?; + let record_type = unqualified_type_name::(); + write!(f, "{fqdn}\t{ttl}\t{CLASS}\t{record_type}\t{type_covered} {algorithm} {labels} {original_ttl} {signature_expiration} {signature_inception} {key_tag} {signer_name}")?; write_split_long_string(f, signature) } @@ -498,13 +640,8 @@ impl FromStr for SOA { return Err("expected 11 columns".into()); }; - if record_type != "SOA" { - return Err(format!("tried to parse `{record_type}` record as a SOA record").into()); - } - - if class != "IN" { - return Err(format!("unknown class: {class}").into()); - } + check_record_type::(record_type)?; + check_class(class)?; Ok(Self { zone: zone.parse()?, @@ -532,7 +669,11 @@ impl fmt::Display for SOA { settings, } = self; - write!(f, "{zone}\t{ttl}\tIN\tSOA\t{nameserver} {admin} {settings}") + let record_type = unqualified_type_name::(); + write!( + f, + "{zone}\t{ttl}\t{CLASS}\t{record_type}\t{nameserver} {admin} {settings}" + ) } } @@ -571,6 +712,32 @@ impl fmt::Display for SoaSettings { } } +fn check_class(class: &str) -> Result<()> { + if class != "IN" { + return Err(format!("unknown class: {class}").into()); + } + + Ok(()) +} + +fn check_record_type(record_type: &str) -> Result<()> { + let expected = unqualified_type_name::(); + if record_type == expected { + Ok(()) + } else { + Err(format!("tried to parse `{record_type}` record as an {expected} record").into()) + } +} + +fn unqualified_type_name() -> &'static str { + let name = any::type_name::(); + if let Some((_rest, component)) = name.rsplit_once(':') { + component + } else { + name + } +} + fn write_split_long_string(f: &mut fmt::Formatter<'_>, field: &str) -> fmt::Result { for (index, c) in field.chars().enumerate() { if index % 56 == 0 { @@ -585,31 +752,34 @@ fn write_split_long_string(f: &mut fmt::Formatter<'_>, field: &str) -> fmt::Resu mod tests { use super::*; + use pretty_assertions::assert_eq; + + // dig A a.root-servers.net + const A_INPUT: &str = "a.root-servers.net. 77859 IN A 198.41.0.4"; + #[test] fn a() -> Result<()> { - // dig A a.root-servers.net - let input = "a.root-servers.net. 77859 IN A 198.41.0.4"; let a @ A { fqdn, ttl, ipv4_addr, - } = &input.parse()?; + } = &A_INPUT.parse()?; assert_eq!("a.root-servers.net.", fqdn.as_str()); assert_eq!(77859, *ttl); assert_eq!(Ipv4Addr::new(198, 41, 0, 4), *ipv4_addr); let output = a.to_string(); - assert_eq!(output, input); + assert_eq!(A_INPUT, output); Ok(()) } + // dig DNSKEY . + const DNSKEY_INPUT: &str = ". 1116 IN DNSKEY 257 3 8 AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3 +/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kv ArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF 0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+e oZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfd RUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwN R1AkUTV74bU="; + #[test] fn dnskey() -> Result<()> { - // dig DNSKEY . - let input = ". 1116 IN DNSKEY 257 3 8 AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3 +/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kv ArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF 0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+e oZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfd RUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwN R1AkUTV74bU="; - let dnskey @ DNSKEY { zone, ttl, @@ -617,7 +787,7 @@ mod tests { protocol, algorithm, public_key, - } = &input.parse()?; + } = &DNSKEY_INPUT.parse()?; assert_eq!(FQDN::ROOT, *zone); assert_eq!(1116, *ttl); @@ -628,16 +798,30 @@ mod tests { assert_eq!(expected, public_key); let output = dnskey.to_string(); - assert_eq!(output, input); + assert_eq!(DNSKEY_INPUT, output); Ok(()) } #[test] - fn ds() -> Result<()> { - // dig DS com. - let input = "com. 7612 IN DS 19718 13 2 8ACBB0CD28F41250A80A491389424D341522D946B0DA0C0291F2D3D7 71D7805A"; + fn parsing_dnskey_ignores_trailing_comment() -> Result<()> { + // `ldns-signzone`'s output + const DNSKEY_INPUT2: &str = ". 86400 IN DNSKEY 256 3 7 AwEAAbEzD/uB2WK89f+PJ1Lyg5xvdt9mXge/R5tiQl8SEAUh/kfbn8jQiakH3HbBnBtdNXpjYrsmM7AxMmJLrp75dFMVnl5693/cY5k4dSk0BFJPQtBsZDn/7Q1rviQn0gqKNjaUfISuRpgCIWFKdRtTdq1VRDf3qIn7S/nuhfWE4w15 ;{id = 11387 (zsk), size = 1024b}"; + let DNSKEY { public_key, .. } = DNSKEY_INPUT2.parse()?; + + let expected = "AwEAAbEzD/uB2WK89f+PJ1Lyg5xvdt9mXge/R5tiQl8SEAUh/kfbn8jQiakH3HbBnBtdNXpjYrsmM7AxMmJLrp75dFMVnl5693/cY5k4dSk0BFJPQtBsZDn/7Q1rviQn0gqKNjaUfISuRpgCIWFKdRtTdq1VRDf3qIn7S/nuhfWE4w15"; + assert_eq!(expected, public_key); + + Ok(()) + } + + // dig DS com. + const DS_INPUT: &str = + "com. 7612 IN DS 19718 13 2 8ACBB0CD28F41250A80A491389424D341522D946B0DA0C0291F2D3D7 71D7805A"; + + #[test] + fn ds() -> Result<()> { let ds @ DS { zone, ttl, @@ -645,7 +829,7 @@ mod tests { algorithm, digest_type, digest, - } = &input.parse()?; + } = &DS_INPUT.parse()?; assert_eq!(FQDN::COM, *zone); assert_eq!(7612, *ttl); @@ -656,16 +840,109 @@ mod tests { assert_eq!(expected, digest); let output = ds.to_string(); - assert_eq!(output, input); + assert_eq!(DS_INPUT, output); Ok(()) } + // dig NS . + const NS_INPUT: &str = ". 86400 IN NS f.root-servers.net."; + + #[test] + fn ns() -> Result<()> { + let ns @ NS { + zone, + ttl, + nameserver, + } = &NS_INPUT.parse()?; + + assert_eq!(FQDN::ROOT, *zone); + assert_eq!(86400, *ttl); + assert_eq!("f.root-servers.net.", nameserver.as_str()); + + let output = ns.to_string(); + assert_eq!(NS_INPUT, output); + + Ok(()) + } + + // dig +dnssec A unicorn.example.com. + const NSEC3_INPUT: &str = "abhif1b25fhcda5amfk5hnrsh6jid2ki.example.com. 3571 IN NSEC3 1 0 5 53BCBC5805D2B761 GVPMD82B8ER38VUEGP72I721LIH19RGR A NS SOA MX TXT AAAA RRSIG DNSKEY NSEC3PARAM"; + + #[test] + fn nsec3() -> Result<()> { + let nsec3 @ NSEC3 { + fqdn, + ttl, + hash_alg, + flags, + iterations, + salt, + next_hashed_owner_name, + record_types, + } = &NSEC3_INPUT.parse()?; + + assert_eq!( + "abhif1b25fhcda5amfk5hnrsh6jid2ki.example.com.", + fqdn.as_str() + ); + assert_eq!(3571, *ttl); + assert_eq!(1, *hash_alg); + assert_eq!(0, *flags); + assert_eq!(5, *iterations); + assert_eq!("53BCBC5805D2B761", salt); + assert_eq!("GVPMD82B8ER38VUEGP72I721LIH19RGR", next_hashed_owner_name); + assert_eq!( + [ + RecordType::A, + RecordType::NS, + RecordType::SOA, + RecordType::MX, + RecordType::TXT, + RecordType::AAAA, + RecordType::RRSIG, + RecordType::DNSKEY, + RecordType::NSEC3PARAM + ], + record_types.as_slice() + ); + + let output = nsec3.to_string(); + assert_eq!(NSEC3_INPUT, output); + + Ok(()) + } + + // dig NSEC3PARAM com. + const NSEC3PARAM_INPUT: &str = "com. 86238 IN NSEC3PARAM 1 0 0 -"; + + #[test] + fn nsec3param() -> Result<()> { + let nsec3param @ NSEC3PARAM { + zone, + ttl, + hash_alg, + flags, + iterations, + } = &NSEC3PARAM_INPUT.parse()?; + + assert_eq!(FQDN::COM, *zone); + assert_eq!(86238, *ttl); + assert_eq!(1, *hash_alg); + assert_eq!(0, *flags); + assert_eq!(0, *iterations); + + let output = nsec3param.to_string(); + assert_eq!(NSEC3PARAM_INPUT, output); + + Ok(()) + } + + // dig +dnssec SOA . + const RRSIG_INPUT: &str = ". 1800 IN RRSIG SOA 7 0 1800 20240306132701 20240207132701 11264 . wXpRU4elJPGYm2kgVVsIwGf1IkYJcQ3UE4mwmItWdxj0XWSWY07MO4Ll DMJgsE0u64Q/345Ck7+aQ904uLebwCvpFnsmkyCxk82XIAfHN9FiwzSy qoR/zZEvBONaej3vrvsqPwh8q/pvypLft9647HcFdwY0juzZsbrAaDAX 8WY="; + #[test] fn rrsig() -> Result<()> { - // dig +dnssec SOA . - let input = ". 1800 IN RRSIG SOA 7 0 1800 20240306132701 20240207132701 11264 . wXpRU4elJPGYm2kgVVsIwGf1IkYJcQ3UE4mwmItWdxj0XWSWY07MO4Ll DMJgsE0u64Q/345Ck7+aQ904uLebwCvpFnsmkyCxk82XIAfHN9FiwzSy qoR/zZEvBONaej3vrvsqPwh8q/pvypLft9647HcFdwY0juzZsbrAaDAX 8WY="; - let rrsig @ RRSIG { fqdn, ttl, @@ -678,7 +955,7 @@ mod tests { key_tag, signer_name, signature, - } = &input.parse()?; + } = &RRSIG_INPUT.parse()?; assert_eq!(FQDN::ROOT, *fqdn); assert_eq!(1800, *ttl); @@ -694,17 +971,18 @@ mod tests { assert_eq!(expected, signature); let output = rrsig.to_string(); - assert_eq!(input, output); + assert_eq!(RRSIG_INPUT, output); Ok(()) } + // dig SOA . + const SOA_INPUT: &str = + ". 15633 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2024020501 1800 900 604800 86400"; + #[test] fn soa() -> Result<()> { - // dig SOA . - let input = ". 15633 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2024020501 1800 900 604800 86400"; - - let soa: SOA = input.parse()?; + let soa: SOA = SOA_INPUT.parse()?; assert_eq!(".", soa.zone.as_str()); assert_eq!(15633, soa.ttl); @@ -718,7 +996,21 @@ mod tests { assert_eq!(86400, settings.minimum); let output = soa.to_string(); - assert_eq!(output, input); + assert_eq!(SOA_INPUT, output); + + Ok(()) + } + + #[test] + fn any() -> Result<()> { + assert!(matches!(A_INPUT.parse()?, Record::A(..))); + assert!(matches!(DNSKEY_INPUT.parse()?, Record::DNSKEY(..))); + assert!(matches!(DS_INPUT.parse()?, Record::DS(..))); + assert!(matches!(NS_INPUT.parse()?, Record::NS(..))); + assert!(matches!(NSEC3_INPUT.parse()?, Record::NSEC3(..))); + assert!(matches!(NSEC3PARAM_INPUT.parse()?, Record::NSEC3PARAM(..))); + assert!(matches!(RRSIG_INPUT.parse()?, Record::RRSIG(..))); + assert!(matches!(SOA_INPUT.parse()?, Record::SOA(..))); Ok(()) } diff --git a/packages/dns-test/src/zone_file.rs b/packages/dns-test/src/zone_file/mod.rs similarity index 79% rename from packages/dns-test/src/zone_file.rs rename to packages/dns-test/src/zone_file/mod.rs index e4bb46c5..57a6c29a 100644 --- a/packages/dns-test/src/zone_file.rs +++ b/packages/dns-test/src/zone_file/mod.rs @@ -57,6 +57,40 @@ impl fmt::Display for ZoneFile { } } +impl FromStr for ZoneFile { + type Err = Error; + + fn from_str(input: &str) -> Result { + let mut records = vec![]; + let mut maybe_soa = None; + for line in input.lines() { + let line = line.trim(); + + if line.is_empty() { + continue; + } + + let record: Record = line.parse()?; + if let Record::SOA(soa) = record { + if maybe_soa.is_some() { + return Err("found more than one SOA record".into()); + } + + maybe_soa = Some(soa); + } else { + records.push(record) + } + } + + let soa = maybe_soa.ok_or("no SOA record found in zone file")?; + Ok(Self { + origin: soa.zone.clone(), + soa, + records, + }) + } +} + /// A root (server) hint pub struct Root { pub ipv4_addr: Ipv4Addr, @@ -154,6 +188,8 @@ impl FromStr for DNSKEY { mod tests { use super::*; + use pretty_assertions::assert_eq; + #[test] fn dnskey() -> Result<()> { let input = ". IN DNSKEY 256 3 7 AwEAAaCUpg+5lH7vart4WiMw4lbbkTNKfkvoyXWsAj09Cc5lT1bFo6sS7o4evhzXU9+iDGZkWZnnkwWg2thXfGgNdfQNTKW/Owz9UMDGv5yjkANKI3fI4jHn7Xp1qIZAwZG0W3RU26s7vkKWVcmA3mrKlDIX9r4BRIZrBVOtNgiHydbB ;{id = 42933 (zsk), size = 1024b}"; @@ -175,4 +211,15 @@ mod tests { Ok(()) } + + #[test] + fn roundtrip() -> Result<()> { + // `ldns-signzone`'s output minus trailing comments; long trailing fields have been split as well + let input = include_str!("muster.zone"); + let zone: ZoneFile = input.parse()?; + let output = zone.to_string(); + assert_eq!(input, output); + + Ok(()) + } } diff --git a/packages/dns-test/src/zone_file/muster.zone b/packages/dns-test/src/zone_file/muster.zone new file mode 100644 index 00000000..7fab9f0e --- /dev/null +++ b/packages/dns-test/src/zone_file/muster.zone @@ -0,0 +1,11 @@ +. 86400 IN SOA primary0.nameservers.com. admin0.nameservers.com. 2024022028 1800 900 604800 86400 +. 86400 IN RRSIG SOA 7 0 86400 20240319104519 20240220104519 11387 . Ks9b5tMyNxxrvw3JkgGkR2H5NPqTDwAwmwh3B7iNC0UHAYGU4B01ZJHj DIsJqDoJ2hsKG5oq0hQuwBSKv2nSBA1oSQcNrBDzOk105gu6tsXg2O8V ZCpAtEColco5ziOX8AWRqRMM5adSfA4xyj5H3NToMjRVDLpVpZsU4BAa 4dU= +. 86400 IN NS primary0.nameservers.com. +. 86400 IN RRSIG NS 7 0 86400 20240319104519 20240220104519 11387 . rZpACeVX3m2CwI/gY/rVYNOAs6ge4h+M74yV+CoAZYJaJLjeHd+jY0YV ixU3hap9bbFCZqhXKU5WSpJSsc/9PrgxEt2XycpbvAJwvIwdqWLUW741 /AOwnyrgv+7PLp4vkDdeLI9tcsY5V/ABpQrYW2i8Gtz90OEpvEEd5+4C LyU= +. 86400 IN DNSKEY 256 3 7 AwEAAbEzD/uB2WK89f+PJ1Lyg5xvdt9mXge/R5tiQl8SEAUh/kfbn8jQ iakH3HbBnBtdNXpjYrsmM7AxMmJLrp75dFMVnl5693/cY5k4dSk0BFJP QtBsZDn/7Q1rviQn0gqKNjaUfISuRpgCIWFKdRtTdq1VRDf3qIn7S/nu hfWE4w15 +. 86400 IN DNSKEY 257 3 7 AwEAAco2Ck4XM5M4RO+QiwZhMFW9Hf8s0cOWH6QZ8OUQisjP6n+gYsbE pOOHhRiABN+QuVhRK9BN+Mt0LqMSBjSy53t5P3NerckqUTQ4HlZkn2QK bhc+TOgvHN5iDj0RBMkTaJ09y5vYmeNv5npFk6hV+VsbBoFRLXTuPSms 8LsH72W6y1HEHNzvAd5H3ro1d2awp66CXRTOcbXbFAIELpTgAU6ZJjEo RBMASZ3Ug4oZ96yvegy2OZnAyFsxBGdOvecs+zoYKeezqaq21YMpnZkf eE7RYexGPm1p8/7smQjBph/uoVDp5k5DuPkTmzpafVOn2YHGB395vT37 uLi9B5Oef9c= +. 86400 IN RRSIG DNSKEY 7 0 86400 20240319104519 20240220104519 11245 . yH/aEcWQhgfmf8RjByMYDDuglaquWsHECA+nRmedIA4Kz7Vc74f77JLi QrhvFFSIFkQNyixNsTugLmTZunphbLrbQNKTWw8gpgd/8u6Oc9OdTYJu T+ADL+Rrgge7mDkPjDRKNhQ6VkIiRzwLBFhoYTA1LZF98CAnJGQcpw4W 1YCkqPbXIzsa3hq2OajC8NzZMEgeI95N1CJ/o5AmhwLtWVuv04q+seGX roSiTlWIQRKGsbCR2v97UjMG4l9XIbijzZbY4dK2/4WrCIw9mjp/cSE8 r/AdfegTi1oqOM9i4QebKvyU9c3rnJRVbFMhEXL1e0M/5bZNytXp43ex VTTHcA== +. 3600 IN NSEC3PARAM 1 0 1 - +. 3600 IN RRSIG NSEC3PARAM 7 0 3600 20240319104519 20240220104519 11387 . IhM+g5s6DwlFKbj6+zd+f/CqN1I4/QtF0aTOMvf0c+s5l+emx/yZEVCT 8LdX4cmz72eYeC4w/dM2btrhhHohhb/hdK1v7ukxtBVgvk6pOmuye2/E cuGkll7B59l+wlRaSmeXAQjiCUX6gyg9tlvmtcnomWVgtjIgMKJpggy8 B6k= +fasdp12mo9fh69ahu5bseugoh3np33tc. 86400 IN NSEC3 1 1 1 - fasdp12mo9fh69ahu5bseugoh3np33tc NS SOA RRSIG DNSKEY NSEC3PARAM +fasdp12mo9fh69ahu5bseugoh3np33tc. 86400 IN RRSIG NSEC3 7 1 86400 20240319104519 20240220104519 11387 . dsdwsTOGL5BvrC1v/5bmDy5Bz8wnN/IG3XRAg6RqKVMK0fLPMsd5uhXm U2gPJ5xUg1RkBQ5+etlBRm2p7vSDjbMa/hjRbvUJgP+c4dL68g+FcHv4 v9fb1Jaao9Goy/ZxZ1dbwXAdxhi+pyvikCdNcKsdiCtFD9pX7V5Nh2Cc GQQ= From 1592454395c7c2b33cf93ed17c80698b15d03a62 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 20 Feb 2024 15:11:00 +0100 Subject: [PATCH 077/124] allow mutation of signed zone file --- packages/dns-test/src/name_server.rs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/dns-test/src/name_server.rs b/packages/dns-test/src/name_server.rs index fd8bec05..449ff210 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -108,11 +108,11 @@ impl NameServer { let key2ds = format!("cd {ZONES_DIR} && ldns-key2ds -n -2 {ZONE_FILENAME}.signed"); let ds: DS = container.stdout(&["sh", "-c", &key2ds])?.parse()?; - // we have an in-memory representation of the zone file so we just delete the on-disk version let zone_file_path = zone_file_path(); - container.status_ok(&["mv", &format!("{zone_file_path}.signed"), &zone_file_path])?; + let signed: ZoneFile = container + .stdout(&["cat", &format!("{zone_file_path}.signed")])? + .parse()?; - let signed_zone_file = container.stdout(&["cat", &zone_file_path])?; let ttl = zone_file.soa.ttl; Ok(NameServer { @@ -120,7 +120,7 @@ impl NameServer { zone_file, state: Signed { ds, - signed_zone_file, + signed, // inherit SOA's TTL value ksk: ksk.with_ttl(ttl), zsk: zsk.with_ttl(ttl), @@ -172,7 +172,7 @@ impl NameServer { let Self { container, zone_file, - state: _, + state, } = self; // for PID file @@ -180,6 +180,8 @@ impl NameServer { container.cp("/etc/nsd/nsd.conf", &nsd_conf(zone_file.origin()))?; + container.cp(&zone_file_path(), &state.signed.to_string())?; + let child = container.spawn(&["nsd", "-d"])?; Ok(NameServer { @@ -197,8 +199,12 @@ impl NameServer { &self.state.zsk } - pub fn signed_zone_file(&self) -> &str { - &self.state.signed_zone_file + pub fn signed_zone_file(&self) -> &ZoneFile { + &self.state.signed + } + + pub fn signed_zone_file_mut(&mut self) -> &mut ZoneFile { + &mut self.state.signed } pub fn ds(&self) -> &DS { @@ -265,7 +271,7 @@ pub struct Signed { ds: DS, zsk: record::DNSKEY, ksk: record::DNSKEY, - signed_zone_file: String, + signed: ZoneFile, } pub struct Running { From 16e83b1d6b8a1930dfa98b115115b9a45a86f37d Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 20 Feb 2024 12:10:40 +0100 Subject: [PATCH 078/124] move secure DNSSEC scenarios into a module --- .../src/resolver/dnssec/scenarios.rs | 132 +----------------- .../src/resolver/dnssec/scenarios/secure.rs | 131 +++++++++++++++++ 2 files changed, 132 insertions(+), 131 deletions(-) create mode 100644 packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs index fd9d2676..46b4ddeb 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs @@ -1,131 +1 @@ -use std::net::Ipv4Addr; - -use dns_test::client::{Client, Dnssec, Recurse}; -use dns_test::name_server::NameServer; -use dns_test::record::{Record, RecordType}; -use dns_test::zone_file::Root; -use dns_test::{Network, 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 network = Network::new()?; - let mut ns = NameServer::new(dns_test::peer(), FQDN::ROOT, &network)?; - ns.add(Record::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, &network)?; - let resolver_addr = resolver.ipv4_addr(); - - let client = Client::new(&network)?; - 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 network = Network::new()?; - let mut root_ns = NameServer::new(dns_test::peer(), FQDN::ROOT, &network)?; - let mut com_ns = NameServer::new(dns_test::peer(), FQDN::COM, &network)?; - - let mut nameservers_ns = - NameServer::new(dns_test::peer(), FQDN("nameservers.com.")?, &network)?; - nameservers_ns - .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) - .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())) - .add(Record::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(), - ) - .add(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()) - .add(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, &network)?; - let resolver_addr = resolver.ipv4_addr(); - - let client = Client::new(&network)?; - 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 +mod secure; diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs new file mode 100644 index 00000000..7ae7e3cc --- /dev/null +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs @@ -0,0 +1,131 @@ +use std::net::Ipv4Addr; + +use dns_test::client::{Client, Dnssec, Recurse}; +use dns_test::name_server::NameServer; +use dns_test::record::{Record, RecordType}; +use dns_test::zone_file::Root; +use dns_test::{Network, 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 network = Network::new()?; + let mut ns = NameServer::new(dns_test::peer(), FQDN::ROOT, &network)?; + ns.add(Record::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, &network)?; + let resolver_addr = resolver.ipv4_addr(); + + let client = Client::new(&network)?; + 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 peer = dns_test::peer(); + let network = Network::new()?; + let mut root_ns = NameServer::new(peer.clone(), FQDN::ROOT, &network)?; + let mut com_ns = NameServer::new(peer.clone(), FQDN::COM, &network)?; + + let mut nameservers_ns = NameServer::new(peer, FQDN("nameservers.com.")?, &network)?; + nameservers_ns + .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) + .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())) + .add(Record::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(), + ) + .add(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()) + .add(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, &network)?; + let resolver_addr = resolver.ipv4_addr(); + + let client = Client::new(&network)?; + 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 From df344e57b1297debcd6d3ea23d1684d7b58452ea Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 20 Feb 2024 12:38:27 +0100 Subject: [PATCH 079/124] fix integer types in RRSIG --- packages/dns-test/src/record.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/dns-test/src/record.rs b/packages/dns-test/src/record.rs index a6e1aef3..0abc7df8 100644 --- a/packages/dns-test/src/record.rs +++ b/packages/dns-test/src/record.rs @@ -542,18 +542,22 @@ impl fmt::Display for NSEC3PARAM { } } +// integer types chosen based on bit sizes in section 3.1 of RFC4034 #[allow(clippy::upper_case_acronyms)] #[derive(Debug)] pub struct RRSIG { pub fqdn: FQDN, pub ttl: u32, pub type_covered: RecordType, - pub algorithm: u32, - pub labels: u32, + pub algorithm: u8, + pub labels: u8, pub original_ttl: u32, + // NOTE on the wire these are 32-bit UNIX timestamps but in text representation they are + // `strftime` formatted + // TODO switch these to `chrono::DateTime`? pub signature_expiration: u64, pub signature_inception: u64, - pub key_tag: u32, + pub key_tag: u16, pub signer_name: FQDN, /// base64 encoded pub signature: String, From b87ae21d2ae260935b49889c874d8fad69d7b391 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 20 Feb 2024 15:06:59 +0100 Subject: [PATCH 080/124] make `dig` queries more configurable switch from enum arguments like `Recurse` and `Dnssec` to a build-pattern-based `Settings` struct --- .../src/resolver/dns/scenarios.rs | 21 +--- .../dnssec/rfc4035/section_4/section_4_1.rs | 11 +- .../src/resolver/dnssec/scenarios/secure.rs | 22 +--- packages/dns-test/src/client.rs | 105 ++++++++++++------ packages/dns-test/src/name_server.rs | 22 +--- packages/dns-test/src/tshark.rs | 14 +-- 6 files changed, 97 insertions(+), 98 deletions(-) diff --git a/packages/conformance-tests/src/resolver/dns/scenarios.rs b/packages/conformance-tests/src/resolver/dns/scenarios.rs index f696691a..39470b83 100644 --- a/packages/conformance-tests/src/resolver/dns/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dns/scenarios.rs @@ -1,6 +1,6 @@ use std::net::Ipv4Addr; -use dns_test::client::{Client, Dnssec, Recurse}; +use dns_test::client::{Client, DigSettings}; use dns_test::name_server::NameServer; use dns_test::record::{Record, RecordType}; use dns_test::zone_file::Root; @@ -44,13 +44,9 @@ fn can_resolve() -> Result<()> { let resolver_ip_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; - let output = client.dig( - Recurse::Yes, - Dnssec::No, - resolver_ip_addr, - RecordType::A, - &needle_fqdn, - )?; + + let settings = *DigSettings::default().recurse(); + let output = client.dig(settings, resolver_ip_addr, RecordType::A, &needle_fqdn)?; assert!(output.status.is_noerror()); @@ -94,13 +90,8 @@ fn nxdomain() -> Result<()> { let resolver_ip_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; - let output = client.dig( - Recurse::Yes, - Dnssec::No, - resolver_ip_addr, - RecordType::A, - &needle_fqdn, - )?; + let settings = *DigSettings::default().recurse(); + let output = client.dig(settings, resolver_ip_addr, RecordType::A, &needle_fqdn)?; assert!(dbg!(output).status.is_nxdomain()); diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs index 9772e309..233ea95c 100644 --- a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs @@ -1,4 +1,4 @@ -use dns_test::client::{Client, Dnssec, Recurse}; +use dns_test::client::{Client, DigSettings}; use dns_test::name_server::NameServer; use dns_test::record::RecordType; use dns_test::tshark::{Capture, Direction}; @@ -20,13 +20,8 @@ fn edns_support() -> Result<()> { let mut tshark = resolver.eavesdrop()?; let client = Client::new(network)?; - let ans = client.dig( - Recurse::Yes, - Dnssec::Yes, - resolver.ipv4_addr(), - RecordType::SOA, - &FQDN::ROOT, - )?; + let settings = *DigSettings::default().authentic_data().recurse(); + let ans = client.dig(settings, resolver.ipv4_addr(), RecordType::SOA, &FQDN::ROOT)?; assert!(ans.status.is_servfail()); tshark.wait_for_capture()?; diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs index 7ae7e3cc..b3571ce2 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs @@ -1,6 +1,6 @@ use std::net::Ipv4Addr; -use dns_test::client::{Client, Dnssec, Recurse}; +use dns_test::client::{Client, DigSettings}; use dns_test::name_server::NameServer; use dns_test::record::{Record, RecordType}; use dns_test::zone_file::Root; @@ -31,13 +31,8 @@ fn can_validate_without_delegation() -> Result<()> { let resolver_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; - let output = client.dig( - Recurse::Yes, - Dnssec::Yes, - resolver_addr, - RecordType::SOA, - &FQDN::ROOT, - )?; + let settings = *DigSettings::default().recurse().authentic_data(); + let output = client.dig(settings, resolver_addr, RecordType::SOA, &FQDN::ROOT)?; assert!(output.status.is_noerror()); assert!(output.flags.authenticated_data); @@ -103,19 +98,14 @@ fn can_validate_with_delegation() -> Result<()> { let resolver_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; - let output = client.dig( - Recurse::Yes, - Dnssec::Yes, - resolver_addr, - RecordType::A, - &needle_fqdn, - )?; + let settings = *DigSettings::default().recurse().authentic_data(); + let output = client.dig(settings, 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] = output.answer.try_into().unwrap(); let a = a.try_into_a().unwrap(); assert_eq!(needle_fqdn, a.fqdn); diff --git a/packages/dns-test/src/client.rs b/packages/dns-test/src/client.rs index 484fb2c7..85e99eb1 100644 --- a/packages/dns-test/src/client.rs +++ b/packages/dns-test/src/client.rs @@ -53,16 +53,17 @@ impl Client { pub fn dig( &self, - recurse: Recurse, - dnssec: Dnssec, + settings: DigSettings, server: Ipv4Addr, record_type: RecordType, fqdn: &FQDN, ) -> Result { let output = self.inner.stdout(&[ "dig", - recurse.as_str(), - dnssec.as_str(), + settings.rdflag(), + settings.do_bit(), + settings.adflag(), + settings.cdflag(), &format!("@{server}"), record_type.as_str(), fqdn.as_str(), @@ -72,32 +73,68 @@ impl Client { } } -#[derive(Clone, Copy)] -pub enum Dnssec { - Yes, - No, +#[derive(Clone, Copy, Default)] +pub struct DigSettings { + adflag: bool, + cdflag: bool, + dnssec: bool, + recurse: bool, } -impl Dnssec { - fn as_str(&self) -> &'static str { - match self { - Self::Yes => "+dnssec", - Self::No => "+nodnssec", +impl DigSettings { + /// Sets the AD bit in the query + pub fn authentic_data(&mut self) -> &mut Self { + self.adflag = true; + self + } + + fn adflag(&self) -> &'static str { + if self.adflag { + "+adflag" + } else { + "+noadflag" } } -} -#[derive(Clone, Copy)] -pub enum Recurse { - Yes, - No, -} + /// Sets the CD bit in the query + pub fn checking_disabled(&mut self) -> &mut Self { + self.cdflag = true; + self + } -impl Recurse { - fn as_str(&self) -> &'static str { - match self { - Self::Yes => "+recurse", - Self::No => "+norecurse", + fn cdflag(&self) -> &'static str { + if self.cdflag { + "+cdflag" + } else { + "+nocdflag" + } + } + + /// Sets the DO bit in the query + pub fn dnssec(&mut self) -> &mut Self { + self.dnssec = true; + self + } + + fn do_bit(&self) -> &'static str { + if self.dnssec { + "+dnssec" + } else { + "+nodnssec" + } + } + + /// Sets the RD bit in the query + pub fn recurse(&mut self) -> &mut Self { + self.recurse = true; + self + } + + fn rdflag(&self) -> &'static str { + if self.recurse { + "+recurse" + } else { + "+norecurse" } } } @@ -184,11 +221,12 @@ impl FromStr for DigOutput { #[derive(Debug, Default, PartialEq)] pub struct DigFlags { - pub qr: bool, - pub recursion_desired: bool, - pub recursion_available: bool, - pub authoritative_answer: bool, pub authenticated_data: bool, + pub authoritative_answer: bool, + pub checking_disabled: bool, + pub qr: bool, + pub recursion_available: bool, + pub recursion_desired: bool, } impl FromStr for DigFlags { @@ -200,6 +238,7 @@ impl FromStr for DigFlags { let mut recursion_available = false; let mut authoritative_answer = false; let mut authenticated_data = false; + let mut checking_disabled = false; for flag in input.split_whitespace() { match flag { @@ -208,16 +247,18 @@ impl FromStr for DigFlags { "ra" => recursion_available = true, "aa" => authoritative_answer = true, "ad" => authenticated_data = true, + "cd" => checking_disabled = true, _ => return Err(format!("unknown flag: {flag}").into()), } } Ok(Self { - qr, - recursion_desired, - recursion_available, - authoritative_answer, authenticated_data, + authoritative_answer, + checking_disabled, + qr, + recursion_available, + recursion_desired, }) } } diff --git a/packages/dns-test/src/name_server.rs b/packages/dns-test/src/name_server.rs index 449ff210..5015ab74 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -295,7 +295,7 @@ fn nsd_conf(fqdn: &FQDN) -> String { #[cfg(test)] mod tests { - use crate::client::{Client, Dnssec, Recurse}; + use crate::client::{Client, DigSettings}; use crate::record::RecordType; use super::*; @@ -307,13 +307,7 @@ mod tests { let ip_addr = tld_ns.ipv4_addr(); let client = Client::new(&network)?; - let output = client.dig( - Recurse::No, - Dnssec::No, - ip_addr, - RecordType::SOA, - &FQDN::COM, - )?; + let output = client.dig(DigSettings::default(), ip_addr, RecordType::SOA, &FQDN::COM)?; assert!(output.status.is_noerror()); @@ -338,8 +332,7 @@ mod tests { let client = Client::new(&network)?; let output = client.dig( - Recurse::No, - Dnssec::No, + DigSettings::default(), ipv4_addr, RecordType::NS, &FQDN::COM, @@ -364,13 +357,8 @@ mod tests { let ns_addr = tld_ns.ipv4_addr(); let client = Client::new(&network)?; - let output = client.dig( - Recurse::No, - Dnssec::Yes, - ns_addr, - RecordType::SOA, - &FQDN::ROOT, - )?; + let settings = *DigSettings::default().dnssec(); + let output = client.dig(settings, ns_addr, RecordType::SOA, &FQDN::ROOT)?; assert!(output.status.is_noerror()); diff --git a/packages/dns-test/src/tshark.rs b/packages/dns-test/src/tshark.rs index ecc39740..0aef4397 100644 --- a/packages/dns-test/src/tshark.rs +++ b/packages/dns-test/src/tshark.rs @@ -244,7 +244,7 @@ struct Ip { #[cfg(test)] mod tests { - use crate::client::{Client, Dnssec, Recurse}; + use crate::client::{Client, DigSettings}; use crate::name_server::NameServer; use crate::record::{Record, RecordType}; use crate::zone_file::Root; @@ -260,8 +260,7 @@ mod tests { let client = Client::new(network)?; let resp = client.dig( - Recurse::No, - Dnssec::No, + DigSettings::default(), ns.ipv4_addr(), RecordType::SOA, &FQDN::ROOT, @@ -322,13 +321,8 @@ mod tests { let resolver_addr = resolver.ipv4_addr(); let client = Client::new(network)?; - let output = client.dig( - Recurse::Yes, - Dnssec::No, - dbg!(resolver_addr), - RecordType::A, - root_ns.fqdn(), - )?; + let settings = *DigSettings::default().recurse(); + let output = client.dig(settings, dbg!(resolver_addr), RecordType::A, root_ns.fqdn())?; assert!(output.status.is_noerror()); From a39afe6412930faa5c392c02d9b04d22a253f0e6 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 20 Feb 2024 15:04:10 +0100 Subject: [PATCH 081/124] test a bogus DNSSEC scenario --- Cargo.lock | 1 + packages/conformance-tests/Cargo.toml | 1 + .../src/resolver/dnssec/scenarios.rs | 1 + .../src/resolver/dnssec/scenarios/bogus.rs | 93 +++++++++++++++++++ 4 files changed, 96 insertions(+) create mode 100644 packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs diff --git a/Cargo.lock b/Cargo.lock index 23e77b6b..35f08284 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,6 +79,7 @@ dependencies = [ name = "conformance-tests" version = "0.1.0" dependencies = [ + "base64", "dns-test", ] diff --git a/packages/conformance-tests/Cargo.toml b/packages/conformance-tests/Cargo.toml index 548b378d..b39d0b61 100644 --- a/packages/conformance-tests/Cargo.toml +++ b/packages/conformance-tests/Cargo.toml @@ -5,6 +5,7 @@ publish = false version = "0.1.0" [dependencies] +base64 = "0.21.7" dns-test.path = "../dns-test" [lib] diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs index 46b4ddeb..416fff70 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs @@ -1 +1,2 @@ +mod bogus; mod secure; diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs new file mode 100644 index 00000000..40011547 --- /dev/null +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs @@ -0,0 +1,93 @@ +use std::net::Ipv4Addr; + +use base64::prelude::*; +use dns_test::client::{Client, DigSettings}; +use dns_test::name_server::NameServer; +use dns_test::record::{Record, RecordType}; +use dns_test::zone_file::Root; +use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; + +#[ignore] +#[test] +fn bad_signature_in_leaf_nameserver() -> Result<()> { + let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4); + let needle_fqdn = FQDN("example.nameservers.com.")?; + + let network = Network::new()?; + let peer = dns_test::peer(); + let mut root_ns = NameServer::new(peer.clone(), FQDN::ROOT, &network)?; + let mut com_ns = NameServer::new(peer.clone(), FQDN::COM, &network)?; + + let mut nameservers_ns = NameServer::new(peer, FQDN("nameservers.com.")?, &network)?; + nameservers_ns + .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) + .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())) + .add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); + let mut nameservers_ns = nameservers_ns.sign()?; + + // fault injection: change the signature field of the RRSIG that covers the A record we'll query + let mut modified = 0; + for record in &mut nameservers_ns.signed_zone_file_mut().records { + if let Record::RRSIG(rrsig) = record { + if rrsig.fqdn == needle_fqdn { + let mut signature = BASE64_STANDARD.decode(&rrsig.signature)?; + let last = signature.last_mut().expect("empty signature"); + *last = !*last; + + rrsig.signature = BASE64_STANDARD.encode(&signature); + modified += 1; + } + } + } + assert_eq!(modified, 1, "sanity check"); + + let nameservers_ds = nameservers_ns.ds().clone(); + let nameservers_ns = nameservers_ns.start()?; + + com_ns + .referral( + nameservers_ns.zone().clone(), + nameservers_ns.fqdn().clone(), + nameservers_ns.ipv4_addr(), + ) + .add(nameservers_ds); + let com_ns = com_ns.sign()?; + let com_ds = com_ns.ds().clone(); + let com_ns = com_ns.start()?; + + root_ns + .referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr()) + .add(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(); + + let root_ns = root_ns.start()?; + + 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, &network)?; + let resolver_addr = resolver.ipv4_addr(); + + let client = Client::new(&network)?; + + let mut settings = *DigSettings::default().recurse().authentic_data(); + let output = client.dig(settings, resolver_addr, RecordType::A, &needle_fqdn)?; + + // the resolver will try to validate the chain of trust; the validation fails so it responds + // with SERVFAIL + assert!(output.status.is_servfail()); + + // avoids a SERVFAIL response + settings.checking_disabled(); + + let output = client.dig(settings, resolver_addr, RecordType::A, &needle_fqdn)?; + + // when the CD (Checking Disabled) bit is set the server won't respond with SERVFAIL on + // validation errors. the outcome of the validation process is reported in the AD bit + assert!(output.status.is_noerror()); + assert!(!output.flags.authenticated_data); + + Ok(()) +} From 5f26698a51e853f606903ab3862b6978b6194378 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 26 Feb 2024 12:28:42 +0100 Subject: [PATCH 082/124] make license more visible it was already in the Cargo.toml metadata but add the files and mention it in the README --- LICENSE-APACHE | 202 ++++++++++++++++++++++++++ LICENSE-MIT | 17 +++ README.md | 9 ++ packages/conformance-tests/Cargo.toml | 1 + 4 files changed, 229 insertions(+) create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 00000000..d5dd862b --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 00000000..969d061e --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,17 @@ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 2c88f800..01422668 100644 --- a/README.md +++ b/README.md @@ -99,3 +99,12 @@ At the RFC module level there's a special module called `scenarios`. This module When adding a new test to the test suite, it must pass with the `unbound` implementation, which is treated as the *reference* implementation. The CI workflow will check that *all* tests, including the ones that have the `#[ignore]` attribute, pass with the `unbound` implementation. New tests that don't pass with the `hickory-dns` implementation must be marked as `#[ignore]`-d. The CI workflow will check that non-`#[ignore]`-d tests pass with the `hickory-dns` implementation. Additionally, the CI workflow will check that all `#[ignore]`-d tests *fail* with the `hickory-dns` implementation; this is to ensure that fixed tests get un-`#[ignore]`-d. + +## License + +Licensed under either of + +- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or ) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or ) + +at your option. diff --git a/packages/conformance-tests/Cargo.toml b/packages/conformance-tests/Cargo.toml index 548b378d..87fcffb9 100644 --- a/packages/conformance-tests/Cargo.toml +++ b/packages/conformance-tests/Cargo.toml @@ -1,5 +1,6 @@ [package] edition = "2021" +license = "MIT OR Apache-2.0" name = "conformance-tests" publish = false version = "0.1.0" From 6fda01af0047c0699034a3fe412561c06f7b60ef Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 26 Feb 2024 11:58:43 +0100 Subject: [PATCH 083/124] parse authority section from dig's output --- packages/dns-test/src/client.rs | 52 +++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/packages/dns-test/src/client.rs b/packages/dns-test/src/client.rs index 85e99eb1..0f9e4aed 100644 --- a/packages/dns-test/src/client.rs +++ b/packages/dns-test/src/client.rs @@ -144,6 +144,7 @@ pub struct DigOutput { pub flags: DigFlags, pub status: DigStatus, pub answer: Vec, + pub authority: Vec, // TODO(if needed) other sections } @@ -154,6 +155,7 @@ impl FromStr for DigOutput { const FLAGS_PREFIX: &str = ";; flags: "; const STATUS_PREFIX: &str = ";; ->>HEADER<<- opcode: QUERY, status: "; const ANSWER_HEADER: &str = ";; ANSWER SECTION:"; + const AUTHORITY_HEADER: &str = ";; AUTHORITY SECTION:"; fn not_found(prefix: &str) -> String { format!("`{prefix}` line was not found") @@ -170,6 +172,7 @@ impl FromStr for DigOutput { let mut flags = None; let mut status = None; let mut answer = None; + let mut authority = None; let mut lines = input.lines(); while let Some(line) = lines.next() { @@ -208,6 +211,21 @@ impl FromStr for DigOutput { } answer = Some(records); + } else if line.starts_with(AUTHORITY_HEADER) { + if authority.is_some() { + return Err(more_than_once(AUTHORITY_HEADER).into()); + } + + let mut records = vec![]; + for line in lines.by_ref() { + if line.is_empty() { + break; + } + + records.push(line.parse()?); + } + + authority = Some(records); } } @@ -215,6 +233,7 @@ impl FromStr for DigOutput { flags: flags.ok_or_else(|| not_found(FLAGS_PREFIX))?, status: status.ok_or_else(|| not_found(STATUS_PREFIX))?, answer: answer.unwrap_or_default(), + authority: authority.unwrap_or_default(), }) } } @@ -346,4 +365,37 @@ mod tests { Ok(()) } + + #[test] + fn authority_section() -> Result<()> { + // $ dig A . + let input = " +; <<>> DiG 9.18.24 <<>> A . +;; global options: +cmd +;; Got answer: +;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 39670 +;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1 + +;; OPT PSEUDOSECTION: +; EDNS: version: 0, flags:; udp: 1232 +;; QUESTION SECTION: +;. IN A + +;; AUTHORITY SECTION: +. 2910 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2024022600 1800 900 604800 86400 + +;; Query time: 43 msec +;; SERVER: 192.168.1.1#53(192.168.1.1) (UDP) +;; WHEN: Mon Feb 26 11:55:50 CET 2024 +;; MSG SIZE rcvd: 103 +"; + + let output: DigOutput = input.parse()?; + + let [record] = output.authority.try_into().expect("exactly one record"); + + matches!(record, Record::SOA(..)); + + Ok(()) + } } From 49b2abc6bea1bb1694c11b97b2c7c85304bacb5f Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 26 Feb 2024 12:09:12 +0100 Subject: [PATCH 084/124] take Impl by ref in NameServer::new this eliminates the need for cloning in tests that use more than one name server, making the code more succint / less noisy --- .../src/resolver/dns/scenarios.rs | 16 ++++++++-------- .../dnssec/rfc4035/section_4/section_4_1.rs | 2 +- .../src/resolver/dnssec/scenarios/bogus.rs | 6 +++--- .../src/resolver/dnssec/scenarios/secure.rs | 8 ++++---- packages/dns-test/examples/explore.rs | 6 +++--- packages/dns-test/src/name_server.rs | 12 ++++++------ packages/dns-test/src/resolver.rs | 2 +- packages/dns-test/src/tshark.rs | 8 ++++---- 8 files changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/conformance-tests/src/resolver/dns/scenarios.rs b/packages/conformance-tests/src/resolver/dns/scenarios.rs index 39470b83..f43e0018 100644 --- a/packages/conformance-tests/src/resolver/dns/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dns/scenarios.rs @@ -12,11 +12,11 @@ fn can_resolve() -> Result<()> { let needle_fqdn = FQDN("example.nameservers.com.")?; let network = Network::new()?; - let mut root_ns = NameServer::new(dns_test::peer(), FQDN::ROOT, &network)?; - let mut com_ns = NameServer::new(dns_test::peer(), FQDN::COM, &network)?; + let peer = dns_test::peer(); + let mut root_ns = NameServer::new(&peer, FQDN::ROOT, &network)?; + let mut com_ns = NameServer::new(&peer, FQDN::COM, &network)?; - let mut nameservers_ns = - NameServer::new(dns_test::peer(), FQDN("nameservers.com.")?, &network)?; + let mut nameservers_ns = NameServer::new(&peer, FQDN("nameservers.com.")?, &network)?; nameservers_ns .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())) @@ -65,11 +65,11 @@ fn nxdomain() -> Result<()> { let needle_fqdn = FQDN("unicorn.nameservers.com.")?; let network = Network::new()?; - let mut root_ns = NameServer::new(dns_test::peer(), FQDN::ROOT, &network)?; - let mut com_ns = NameServer::new(dns_test::peer(), FQDN::COM, &network)?; + let peer = dns_test::peer(); + let mut root_ns = NameServer::new(&peer, FQDN::ROOT, &network)?; + let mut com_ns = NameServer::new(&peer, FQDN::COM, &network)?; - let mut nameservers_ns = - NameServer::new(dns_test::peer(), FQDN("nameservers.com.")?, &network)?; + let mut nameservers_ns = NameServer::new(&peer, FQDN("nameservers.com.")?, &network)?; nameservers_ns .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())); diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs index 233ea95c..e4a1c393 100644 --- a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs @@ -9,7 +9,7 @@ use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; #[ignore] fn edns_support() -> Result<()> { let network = &Network::new()?; - let ns = NameServer::new(dns_test::peer(), FQDN::ROOT, network)?.start()?; + let ns = NameServer::new(&dns_test::peer(), FQDN::ROOT, network)?.start()?; let resolver = Resolver::start( dns_test::subject(), &[Root::new(ns.fqdn().clone(), ns.ipv4_addr())], diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs index 40011547..869fca6b 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs @@ -15,10 +15,10 @@ fn bad_signature_in_leaf_nameserver() -> Result<()> { let network = Network::new()?; let peer = dns_test::peer(); - let mut root_ns = NameServer::new(peer.clone(), FQDN::ROOT, &network)?; - let mut com_ns = NameServer::new(peer.clone(), FQDN::COM, &network)?; + let mut root_ns = NameServer::new(&peer, FQDN::ROOT, &network)?; + let mut com_ns = NameServer::new(&peer, FQDN::COM, &network)?; - let mut nameservers_ns = NameServer::new(peer, FQDN("nameservers.com.")?, &network)?; + let mut nameservers_ns = NameServer::new(&peer, FQDN("nameservers.com.")?, &network)?; nameservers_ns .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())) diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs index b3571ce2..7a74b176 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs @@ -11,7 +11,7 @@ use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; #[test] fn can_validate_without_delegation() -> Result<()> { let network = Network::new()?; - let mut ns = NameServer::new(dns_test::peer(), FQDN::ROOT, &network)?; + let mut ns = NameServer::new(&dns_test::peer(), FQDN::ROOT, &network)?; ns.add(Record::a(ns.fqdn().clone(), ns.ipv4_addr())); let ns = ns.sign()?; @@ -51,10 +51,10 @@ fn can_validate_with_delegation() -> Result<()> { let peer = dns_test::peer(); let network = Network::new()?; - let mut root_ns = NameServer::new(peer.clone(), FQDN::ROOT, &network)?; - let mut com_ns = NameServer::new(peer.clone(), FQDN::COM, &network)?; + let mut root_ns = NameServer::new(&peer, FQDN::ROOT, &network)?; + let mut com_ns = NameServer::new(&peer, FQDN::COM, &network)?; - let mut nameservers_ns = NameServer::new(peer, FQDN("nameservers.com.")?, &network)?; + let mut nameservers_ns = NameServer::new(&peer, FQDN("nameservers.com.")?, &network)?; nameservers_ns .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())) diff --git a/packages/dns-test/examples/explore.rs b/packages/dns-test/examples/explore.rs index e6d9919e..54da47b8 100644 --- a/packages/dns-test/examples/explore.rs +++ b/packages/dns-test/examples/explore.rs @@ -11,13 +11,13 @@ fn main() -> Result<()> { let peer = dns_test::peer(); println!("building docker image..."); - let mut root_ns = NameServer::new(peer.clone(), FQDN::ROOT, &network)?; + let mut root_ns = NameServer::new(&peer, FQDN::ROOT, &network)?; println!("DONE"); println!("setting up name servers..."); - let mut com_ns = NameServer::new(peer.clone(), FQDN::COM, &network)?; + let mut com_ns = NameServer::new(&peer, FQDN::COM, &network)?; - let mut nameservers_ns = NameServer::new(peer.clone(), FQDN("nameservers.com.")?, &network)?; + let mut nameservers_ns = NameServer::new(&peer, FQDN("nameservers.com.")?, &network)?; nameservers_ns .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())); diff --git a/packages/dns-test/src/name_server.rs b/packages/dns-test/src/name_server.rs index 5015ab74..e5732c2b 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -26,7 +26,7 @@ impl NameServer { /// - one SOA record, with the primary name server field set to this name server's FQDN /// - one NS record, with this name server's FQDN set as the only available name server for /// the zone - pub fn new(implementation: Implementation, zone: FQDN, network: &Network) -> Result { + pub fn new(implementation: &Implementation, zone: FQDN, network: &Network) -> Result { assert!( matches!(implementation, Implementation::Unbound), "currently only `unbound` (`nsd`) can be used as a `NameServer`" @@ -46,7 +46,7 @@ impl NameServer { zone_file.add(Record::ns(zone, nameserver.clone())); - let image = implementation.into(); + let image = implementation.clone().into(); Ok(Self { container: Container::run(&image, network)?, zone_file, @@ -303,7 +303,7 @@ mod tests { #[test] fn simplest() -> Result<()> { let network = Network::new()?; - let tld_ns = NameServer::new(Implementation::Unbound, FQDN::COM, &network)?.start()?; + let tld_ns = NameServer::new(&Implementation::Unbound, FQDN::COM, &network)?.start()?; let ip_addr = tld_ns.ipv4_addr(); let client = Client::new(&network)?; @@ -318,7 +318,7 @@ mod tests { fn with_referral() -> Result<()> { let network = Network::new()?; let expected_ip_addr = Ipv4Addr::new(172, 17, 200, 1); - let mut root_ns = NameServer::new(Implementation::Unbound, FQDN::ROOT, &network)?; + let mut root_ns = NameServer::new(&Implementation::Unbound, FQDN::ROOT, &network)?; root_ns.referral( FQDN::COM, FQDN("primary.tld-server.com.")?, @@ -346,7 +346,7 @@ mod tests { #[test] fn signed() -> Result<()> { let network = Network::new()?; - let ns = NameServer::new(Implementation::Unbound, FQDN::ROOT, &network)?.sign()?; + let ns = NameServer::new(&Implementation::Unbound, FQDN::ROOT, &network)?.sign()?; eprintln!("KSK:\n{}", ns.key_signing_key()); eprintln!("ZSK:\n{}", ns.zone_signing_key()); @@ -377,7 +377,7 @@ mod tests { #[test] fn terminate_works() -> Result<()> { let network = Network::new()?; - let ns = NameServer::new(Implementation::Unbound, FQDN::ROOT, &network)?.start()?; + let ns = NameServer::new(&Implementation::Unbound, FQDN::ROOT, &network)?.start()?; let logs = ns.terminate()?; assert!(logs.contains("nsd starting")); diff --git a/packages/dns-test/src/resolver.rs b/packages/dns-test/src/resolver.rs index 22c5e55a..d26062ed 100644 --- a/packages/dns-test/src/resolver.rs +++ b/packages/dns-test/src/resolver.rs @@ -125,7 +125,7 @@ mod tests { #[test] fn terminate_works() -> Result<()> { let network = Network::new()?; - let ns = NameServer::new(Implementation::Unbound, FQDN::ROOT, &network)?.start()?; + let ns = NameServer::new(&Implementation::Unbound, FQDN::ROOT, &network)?.start()?; let resolver = Resolver::start( Implementation::Unbound, &[Root::new(ns.fqdn().clone(), ns.ipv4_addr())], diff --git a/packages/dns-test/src/tshark.rs b/packages/dns-test/src/tshark.rs index 0aef4397..762d7591 100644 --- a/packages/dns-test/src/tshark.rs +++ b/packages/dns-test/src/tshark.rs @@ -255,7 +255,7 @@ mod tests { #[test] fn nameserver() -> Result<()> { let network = &Network::new()?; - let ns = NameServer::new(Implementation::Unbound, FQDN::ROOT, network)?.start()?; + let ns = NameServer::new(&Implementation::Unbound, FQDN::ROOT, network)?.start()?; let mut tshark = ns.eavesdrop()?; let client = Client::new(network)?; @@ -290,11 +290,11 @@ mod tests { #[test] fn resolver() -> Result<()> { let network = &Network::new()?; - let mut root_ns = NameServer::new(Implementation::Unbound, FQDN::ROOT, network)?; - let mut com_ns = NameServer::new(Implementation::Unbound, FQDN::COM, network)?; + let mut root_ns = NameServer::new(&Implementation::Unbound, FQDN::ROOT, network)?; + let mut com_ns = NameServer::new(&Implementation::Unbound, FQDN::COM, network)?; let mut nameservers_ns = - NameServer::new(Implementation::Unbound, FQDN("nameservers.com.")?, network)?; + NameServer::new(&Implementation::Unbound, FQDN("nameservers.com.")?, network)?; nameservers_ns .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())); From ab9b1e68cc39aec6397d32b6e0bc8e4056040c86 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 26 Feb 2024 12:10:00 +0100 Subject: [PATCH 085/124] also take Impl by ref in Resolver::new mainly for symmetry. the existing tests don't clone the subject so the code does not become any shorter --- packages/conformance-tests/src/resolver/dns/scenarios.rs | 4 ++-- .../src/resolver/dnssec/rfc4035/section_4/section_4_1.rs | 2 +- .../conformance-tests/src/resolver/dnssec/scenarios/bogus.rs | 2 +- .../conformance-tests/src/resolver/dnssec/scenarios/secure.rs | 4 ++-- packages/dns-test/examples/explore.rs | 2 +- packages/dns-test/src/resolver.rs | 4 ++-- packages/dns-test/src/tshark.rs | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/conformance-tests/src/resolver/dns/scenarios.rs b/packages/conformance-tests/src/resolver/dns/scenarios.rs index f43e0018..20a90d07 100644 --- a/packages/conformance-tests/src/resolver/dns/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dns/scenarios.rs @@ -40,7 +40,7 @@ fn can_resolve() -> Result<()> { 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(), &network)?; + let resolver = Resolver::start(&dns_test::subject(), roots, &TrustAnchor::empty(), &network)?; let resolver_ip_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; @@ -86,7 +86,7 @@ fn nxdomain() -> Result<()> { 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(), &network)?; + let resolver = Resolver::start(&dns_test::subject(), roots, &TrustAnchor::empty(), &network)?; let resolver_ip_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs index e4a1c393..12a3e7c4 100644 --- a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs @@ -11,7 +11,7 @@ fn edns_support() -> Result<()> { let network = &Network::new()?; let ns = NameServer::new(&dns_test::peer(), FQDN::ROOT, network)?.start()?; let resolver = Resolver::start( - dns_test::subject(), + &dns_test::subject(), &[Root::new(ns.fqdn().clone(), ns.ipv4_addr())], &TrustAnchor::empty(), network, diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs index 869fca6b..0dd99b51 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs @@ -67,7 +67,7 @@ fn bad_signature_in_leaf_nameserver() -> Result<()> { 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, &network)?; + let resolver = Resolver::start(&dns_test::subject(), roots, &trust_anchor, &network)?; let resolver_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs index 7a74b176..6df21009 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs @@ -27,7 +27,7 @@ fn can_validate_without_delegation() -> Result<()> { 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, &network)?; + let resolver = Resolver::start(&dns_test::subject(), roots, &trust_anchor, &network)?; let resolver_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; @@ -94,7 +94,7 @@ fn can_validate_with_delegation() -> Result<()> { 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, &network)?; + let resolver = Resolver::start(&dns_test::subject(), roots, &trust_anchor, &network)?; let resolver_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; diff --git a/packages/dns-test/examples/explore.rs b/packages/dns-test/examples/explore.rs index 54da47b8..038870a1 100644 --- a/packages/dns-test/examples/explore.rs +++ b/packages/dns-test/examples/explore.rs @@ -50,7 +50,7 @@ fn main() -> Result<()> { let trust_anchor = TrustAnchor::from_iter([root_ksk.clone(), root_zsk.clone()]); println!("building docker image..."); - let resolver = Resolver::start(dns_test::subject(), roots, &trust_anchor, &network)?; + let resolver = Resolver::start(&dns_test::subject(), roots, &trust_anchor, &network)?; println!("DONE\n\n"); let resolver_addr = resolver.ipv4_addr(); diff --git a/packages/dns-test/src/resolver.rs b/packages/dns-test/src/resolver.rs index d26062ed..813e3bc3 100644 --- a/packages/dns-test/src/resolver.rs +++ b/packages/dns-test/src/resolver.rs @@ -21,7 +21,7 @@ impl Resolver { /// /// This constructor panics if `roots` is an empty slice pub fn start( - implementation: Implementation, + implementation: &Implementation, roots: &[Root], trust_anchor: &TrustAnchor, network: &Network, @@ -127,7 +127,7 @@ mod tests { let network = Network::new()?; let ns = NameServer::new(&Implementation::Unbound, FQDN::ROOT, &network)?.start()?; let resolver = Resolver::start( - Implementation::Unbound, + &Implementation::Unbound, &[Root::new(ns.fqdn().clone(), ns.ipv4_addr())], &TrustAnchor::empty(), &network, diff --git a/packages/dns-test/src/tshark.rs b/packages/dns-test/src/tshark.rs index 762d7591..e736ea9d 100644 --- a/packages/dns-test/src/tshark.rs +++ b/packages/dns-test/src/tshark.rs @@ -312,7 +312,7 @@ mod tests { let roots = &[Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr())]; let resolver = Resolver::start( - Implementation::Unbound, + &Implementation::Unbound, roots, &TrustAnchor::empty(), network, From 30ffd3882bb2b81e7e5c76a84de7f4858d60ddf1 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 1 Mar 2024 19:47:43 +0100 Subject: [PATCH 086/124] README: add some tips on writing tests --- README.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/README.md b/README.md index 01422668..d5d0c523 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,51 @@ $ DNS_TEST_SUBJECT="hickory https://github.com/hickory-dns/hickory-dns" cargo ru - `DNS_TEST_VERBOSE_DOCKER_BUILD`. Setting this variable prints the output of the `docker build` invocations that the framework does to the console. This is useful to verify that image caching is working; for example if you set `DNS_TEST_SUBJECT` to a local `hickory-dns` repository then consecutively running the `explore` example and/or `conformance-tests` test suite **must** not rebuild `hickory-dns` provided that you have not *committed* any new change to the local repository. +### Writing tests + +Here are some considerations when writing tests. + +- Both `unbound` and BIND, in the resolver role, will initially query for the A record of their configured root server's FQDN as well as the A records of all the name servers covering the zones required to resolve the root server's FQDN. As of [49c89f7], All the name servers have a FQDN of the form `primaryNNN.nameservers.com.`, where `NNN` is a non-negative integer. These initial `A primaryNNN.nameservers.com.` queries will be sent to the name server that covers the `nameservers.com.` zone. What all this means in practice, is that you'll need to add these A records -- the root server's, `com.`'s name server and `nameservers.com.`'s name server -- to the `nameservers.com.` zone file; if you don't, most queries (expect perhaps for `SOA .`) will fail to resolve with return code SERVFAIL. + +[49c89f7]: https://github.com/ferrous-systems/dnssec-tests/commit/49c89f764ede89aefe578b799e7766f051a600cc + +``` rust +let root_ns: NameServer; // for `.` zone +let com_ns: NameServer; // for `com.` zone +let nameservers_ns: NameServer; // for `nameservers.com.` zone + +nameservers_ns + .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) + .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())); + +// each `NameServer` will start out with an A record of its FQDN to its own IPv4 address in its +// zone file so NO need to add that one in the preceding statement +``` + +- To get resolution to work, you need referrals -- in the form of NS and A record pairs -- from parent zones to child zones. Check the [`dns::scenarios::can_resolve`] for an example of how to set up referrals. + +[`dns::scenarios::can_resolve`]: https://github.com/ferrous-systems/dnssec-tests/blob/49c89f764ede89aefe578b799e7766f051a600cc/packages/conformance-tests/src/resolver/dns/scenarios.rs#L10 + +- To get DNSSEC validation to work, you need the DS record of the child zone in the parent zone. Furthermore, the DS record needs to be signed using parent zone's key. Check the [`dnssec::scenarios::secure::can_validate_with_delegation`] for an example of how to set up the DS records. + +[`dnssec::scenarios::secure::can_validate_with_delegation`]: https://github.com/ferrous-systems/dnssec-tests/blob/49c89f764ede89aefe578b799e7766f051a600cc/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs#L48 + +- You can get the logs of both a `Resolver` and `NameServer` using the `terminate` method. This method terminates the server and returns all the logs. This can be useful when trying to figure out why a query is not producing the expected results. + +``` rust +let resolver: Resolver; + +let ans = client.dig(/* .. */); + +let logs = resolver.terminate()?; + +// print the logs to figure out ... +eprintln!("{logs}"); + +// ... why this assertion is not working +assert!(ans.status.is_noerror()); +``` + ## `conformance-tests` This is a collection of tests that check the conformance of a DNS implementation to the different RFCs around DNS and DNSSEC. From f3bd5a1c5557ea0400340bbf487a21aa6afe3e10 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 7 Mar 2024 16:03:38 +0100 Subject: [PATCH 087/124] README: cover dns-test automatic clean-up and advise against running tests with cargo-watch --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index d5d0c523..34c594e1 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,18 @@ $ DNS_TEST_SUBJECT="hickory https://github.com/hickory-dns/hickory-dns" cargo ru - `DNS_TEST_VERBOSE_DOCKER_BUILD`. Setting this variable prints the output of the `docker build` invocations that the framework does to the console. This is useful to verify that image caching is working; for example if you set `DNS_TEST_SUBJECT` to a local `hickory-dns` repository then consecutively running the `explore` example and/or `conformance-tests` test suite **must** not rebuild `hickory-dns` provided that you have not *committed* any new change to the local repository. +### Automatic clean-up + +`dns-test` has been designed to clean up, that is remove, the Docker containers and Docker networks that it creates. +If you use `dns-test` and it does not clean up Docker resources, that's a bug that should be reported. + +`dns-test` uses destructors (the `Drop` trait) to clean up resources. +If you forcefully terminate a process, e.g. using Ctrl+C or a signal like SIGINT, that uses `dns-test` then the destructors won't run and Docker resources won't be cleaned up. + +Note that `cargo watch` terminates the last process using signals before starting a new instance of it. +Therefore we advise against using `cargo watch` to *run* tests that use the `dns-test` framework; +using `cargo-watch` to `check` such tests is perfectly fine, however. + ### Writing tests Here are some considerations when writing tests. From 2c4ef88a983cfd3a32402f4decb48b55c0e1f155 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 23 Feb 2024 15:31:53 +0100 Subject: [PATCH 088/124] support using BIND in the Resolver role --- .../dnssec/rfc4035/section_4/section_4_1.rs | 8 ++- packages/dns-test/src/container.rs | 17 +++-- packages/dns-test/src/docker/bind.Dockerfile | 10 +++ packages/dns-test/src/lib.rs | 24 ++++++- packages/dns-test/src/resolver.rs | 63 +++++++++++++++++-- .../src/templates/named.resolver.conf.jinja | 14 +++++ 6 files changed, 122 insertions(+), 14 deletions(-) create mode 100644 packages/dns-test/src/docker/bind.Dockerfile create mode 100644 packages/dns-test/src/templates/named.resolver.conf.jinja diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs index 12a3e7c4..88a65eb8 100644 --- a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs @@ -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()?; diff --git a/packages/dns-test/src/container.rs b/packages/dns-test/src/container.rs index 5bdfe19b..fb44c4ad 100644 --- a/packages/dns-test/src/container.rs +++ b/packages/dns-test/src/container.rs @@ -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 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", }; diff --git a/packages/dns-test/src/docker/bind.Dockerfile b/packages/dns-test/src/docker/bind.Dockerfile new file mode 100644 index 00000000..c6a7e7dd --- /dev/null +++ b/packages/dns-test/src/docker/bind.Dockerfile @@ -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/* diff --git a/packages/dns-test/src/lib.rs b/packages/dns-test/src/lib.rs index 48c81c00..eb7fe63a 100644 --- a/packages/dns-test/src/lib.rs +++ b/packages/dns-test/src/lib.rs @@ -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() + } } diff --git a/packages/dns-test/src/resolver.rs b/packages/dns-test/src/resolver.rs index 813e3bc3..efa6da29 100644 --- a/packages/dns-test/src/resolver.rs +++ b/packages/dns-test/src/resolver.rs @@ -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 { - 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 { @@ -88,7 +113,11 @@ impl Resolver { /// gracefully terminates the name server collecting all logs pub fn terminate(self) -> Result { - 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(()) + } } diff --git a/packages/dns-test/src/templates/named.resolver.conf.jinja b/packages/dns-test/src/templates/named.resolver.conf.jinja new file mode 100644 index 00000000..4b57908d --- /dev/null +++ b/packages/dns-test/src/templates/named.resolver.conf.jinja @@ -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"; +}; From 90ee7b30f63ecd9c4e6754c2300eb76317962c1e Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 29 Feb 2024 16:53:07 +0100 Subject: [PATCH 089/124] CI: run conformance tests against BIND --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6e9e86c..2a2ab11c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,9 @@ jobs: - name: Run tests against unbound run: cargo test -p conformance-tests -- --include-ignored + - name: Run tests against BIND + run: DNS_TEST_SUBJECT=bind cargo test -p conformance-tests -- --include-ignored + - name: Run tests against hickory run: | git clone https://github.com/hickory-dns/hickory-dns /tmp/hickory From 7aa9d543b4bcd3fe5de7e5023dc1ec9eb0434474 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 29 Feb 2024 17:38:06 +0100 Subject: [PATCH 090/124] support BIND in the NameServer role --- packages/dns-test/src/name_server.rs | 108 +++++++++++++++--- .../templates/named.name-server.conf.jinja | 14 +++ .../dns-test/src/templates/nsd.conf.jinja | 2 +- 3 files changed, 105 insertions(+), 19 deletions(-) create mode 100644 packages/dns-test/src/templates/named.name-server.conf.jinja diff --git a/packages/dns-test/src/name_server.rs b/packages/dns-test/src/name_server.rs index e5732c2b..81ce4d70 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -9,8 +9,9 @@ use crate::{Implementation, Result, DEFAULT_TTL, FQDN}; pub struct NameServer { container: Container, - zone_file: ZoneFile, + implementation: Implementation, state: State, + zone_file: ZoneFile, } impl NameServer { @@ -28,12 +29,17 @@ impl NameServer { /// the zone pub fn new(implementation: &Implementation, zone: FQDN, network: &Network) -> Result { assert!( - matches!(implementation, Implementation::Unbound), - "currently only `unbound` (`nsd`) can be used as a `NameServer`" + matches!( + implementation, + Implementation::Unbound | Implementation::Bind + ), + "currently only `unbound` (`nsd`) and BIND can be used as a `NameServer`" ); let ns_count = ns_count(); let nameserver = primary_ns(ns_count); + let image = implementation.clone().into(); + let container = Container::run(&image, network)?; let soa = SOA { zone: zone.clone(), @@ -45,10 +51,12 @@ impl NameServer { let mut zone_file = ZoneFile::new(soa); zone_file.add(Record::ns(zone, nameserver.clone())); + // BIND requires that `nameserver` has an A record + zone_file.add(Record::a(nameserver.clone(), container.ipv4_addr())); - let image = implementation.clone().into(); Ok(Self { - container: Container::run(&image, network)?, + container, + implementation: implementation.clone(), zone_file, state: Stopped, }) @@ -76,11 +84,13 @@ impl NameServer { let Self { container, zone_file, + implementation, state: _, } = self; container.status_ok(&["mkdir", "-p", ZONES_DIR])?; - container.cp("/etc/nsd/zones/main.zone", &zone_file.to_string())?; + let zone_file_path = zone_file_path(); + container.cp(&zone_file_path, &zone_file.to_string())?; let zone = zone_file.origin(); @@ -108,7 +118,6 @@ impl NameServer { let key2ds = format!("cd {ZONES_DIR} && ldns-key2ds -n -2 {ZONE_FILENAME}.signed"); let ds: DS = container.stdout(&["sh", "-c", &key2ds])?.parse()?; - let zone_file_path = zone_file_path(); let signed: ZoneFile = container .stdout(&["cat", &format!("{zone_file_path}.signed")])? .parse()?; @@ -117,6 +126,7 @@ impl NameServer { Ok(NameServer { container, + implementation, zone_file, state: Signed { ds, @@ -133,28 +143,45 @@ impl NameServer { let Self { container, zone_file, + implementation, state: _, } = self; - // for PID file - container.status_ok(&["mkdir", "-p", "/run/nsd/"])?; + let origin = zone_file.origin(); + let (path, contents, cmd_args) = match &implementation { + Implementation::Bind => ( + "/etc/bind/named.conf", + named_conf(origin), + &["named", "-g", "-d5"][..], + ), - container.cp("/etc/nsd/nsd.conf", &nsd_conf(zone_file.origin()))?; + Implementation::Unbound => { + // for PID file + container.status_ok(&["mkdir", "-p", "/run/nsd/"])?; + + ("/etc/nsd/nsd.conf", nsd_conf(origin), &["nsd", "-d"][..]) + } + + Implementation::Hickory(_) => unreachable!(), + }; + + container.cp(path, &contents)?; container.status_ok(&["mkdir", "-p", ZONES_DIR])?; container.cp(&zone_file_path(), &zone_file.to_string())?; - let child = container.spawn(&["nsd", "-d"])?; + let child = container.spawn(cmd_args)?; Ok(NameServer { container, + implementation, zone_file, state: Running { child }, }) } } -const ZONES_DIR: &str = "/etc/nsd/zones"; +const ZONES_DIR: &str = "/etc/zones"; const ZONE_FILENAME: &str = "main.zone"; fn zone_file_path() -> String { @@ -172,20 +199,40 @@ impl NameServer { let Self { container, zone_file, + implementation, state, } = self; - // for PID file - container.status_ok(&["mkdir", "-p", "/run/nsd/"])?; + let (conf_path, conf_contents, cmd_args) = match implementation { + Implementation::Bind => ( + "/etc/bind/named.conf", + named_conf(zone_file.origin()), + &["named", "-g", "-d5"][..], + ), - container.cp("/etc/nsd/nsd.conf", &nsd_conf(zone_file.origin()))?; + Implementation::Unbound => { + // for PID file + container.status_ok(&["mkdir", "-p", "/run/nsd/"])?; + + ( + "/etc/nsd/nsd.conf", + nsd_conf(zone_file.origin()), + &["nsd", "-d"][..], + ) + } + + Implementation::Hickory(..) => unreachable!(), + }; + + container.cp(conf_path, &conf_contents)?; container.cp(&zone_file_path(), &state.signed.to_string())?; - let child = container.spawn(&["nsd", "-d"])?; + let child = container.spawn(cmd_args)?; Ok(NameServer { container, + implementation, zone_file, state: Running { child }, }) @@ -220,7 +267,13 @@ impl NameServer { /// gracefully terminates the name server collecting all logs pub fn terminate(self) -> Result { - let pidfile = "/run/nsd/nsd.pid"; + let pidfile = match &self.implementation { + Implementation::Bind => "/tmp/named.pid", + + Implementation::Unbound => "/run/nsd/nsd.pid", + + Implementation::Hickory(_) => unreachable!(), + }; // 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!( @@ -286,6 +339,13 @@ fn admin_ns(ns_count: usize) -> FQDN { FQDN(format!("admin{ns_count}.nameservers.com.")).unwrap() } +fn named_conf(fqdn: &FQDN) -> String { + minijinja::render!( + include_str!("templates/named.name-server.conf.jinja"), + fqdn => fqdn.as_str() + ) +} + fn nsd_conf(fqdn: &FQDN) -> String { minijinja::render!( include_str!("templates/nsd.conf.jinja"), @@ -375,7 +435,7 @@ mod tests { } #[test] - fn terminate_works() -> Result<()> { + fn terminate_nsd_works() -> Result<()> { let network = Network::new()?; let ns = NameServer::new(&Implementation::Unbound, FQDN::ROOT, &network)?.start()?; let logs = ns.terminate()?; @@ -384,4 +444,16 @@ mod tests { Ok(()) } + + #[test] + fn terminate_named_works() -> Result<()> { + let network = Network::new()?; + let ns = NameServer::new(&Implementation::Bind, FQDN::ROOT, &network)?.start()?; + let logs = ns.terminate()?; + + eprintln!("{logs}"); + assert!(logs.contains("starting BIND")); + + Ok(()) + } } diff --git a/packages/dns-test/src/templates/named.name-server.conf.jinja b/packages/dns-test/src/templates/named.name-server.conf.jinja new file mode 100644 index 00000000..3a829ef5 --- /dev/null +++ b/packages/dns-test/src/templates/named.name-server.conf.jinja @@ -0,0 +1,14 @@ +options { + directory "/var/cache/bind"; + pid-file "/tmp/named.pid"; + recursion no; + dnssec-validation no; + allow-transfer { none; }; + # significantly reduces noise in logs + empty-zones-enable no; +}; + +zone "{{ fqdn }}" IN { + type primary; + file "/etc/zones/main.zone"; +}; diff --git a/packages/dns-test/src/templates/nsd.conf.jinja b/packages/dns-test/src/templates/nsd.conf.jinja index 2a7541af..36dea4fc 100644 --- a/packages/dns-test/src/templates/nsd.conf.jinja +++ b/packages/dns-test/src/templates/nsd.conf.jinja @@ -3,4 +3,4 @@ remote-control: zone: name: {{ fqdn }} - zonefile: /etc/nsd/zones/main.zone + zonefile: /etc/zones/main.zone From 4f024887e01e95816395f6d51d14b0dd11970a2a Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 29 Feb 2024 18:22:07 +0100 Subject: [PATCH 091/124] CI: test unbound (subject) against BIND (peers) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a2ab11c..e8f7942b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: run: cargo test -p dns-test -- --include-ignored - name: Run tests against unbound - run: cargo test -p conformance-tests -- --include-ignored + run: DNS_TEST_PEER=bind cargo test -p conformance-tests -- --include-ignored - name: Run tests against BIND run: DNS_TEST_SUBJECT=bind cargo test -p conformance-tests -- --include-ignored From 9689568974ef88253932e9b2f942bda3315ae7cc Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 29 Feb 2024 18:42:15 +0100 Subject: [PATCH 092/124] refactor Implementation branching into its own module --- .../dns-test/src/docker/hickory.Dockerfile | 3 +- packages/dns-test/src/implementation.rs | 190 ++++++++++++++++++ packages/dns-test/src/lib.rs | 53 +---- packages/dns-test/src/name_server.rs | 81 ++------ packages/dns-test/src/resolver.rs | 63 ++---- .../src/templates/hickory.resolver.toml.jinja | 2 +- .../src/templates/named.resolver.conf.jinja | 2 +- .../dns-test/src/templates/nsd.conf.jinja | 3 + .../dns-test/src/templates/unbound.conf.jinja | 3 +- 9 files changed, 233 insertions(+), 167 deletions(-) create mode 100644 packages/dns-test/src/implementation.rs diff --git a/packages/dns-test/src/docker/hickory.Dockerfile b/packages/dns-test/src/docker/hickory.Dockerfile index 358b7a93..77206244 100644 --- a/packages/dns-test/src/docker/hickory.Dockerfile +++ b/packages/dns-test/src/docker/hickory.Dockerfile @@ -8,5 +8,6 @@ RUN apt-get update && \ # a clone of the hickory repository. `./src` here refers to that clone; not to # any directory inside the `dns-test` repository COPY ./src /usr/src/hickory -RUN cargo install --path /usr/src/hickory/bin --features recursor --debug +RUN cargo install --path /usr/src/hickory/bin --features recursor --debug && \ + mkdir /etc/hickory env RUST_LOG=debug diff --git a/packages/dns-test/src/implementation.rs b/packages/dns-test/src/implementation.rs new file mode 100644 index 00000000..256ad247 --- /dev/null +++ b/packages/dns-test/src/implementation.rs @@ -0,0 +1,190 @@ +use core::fmt; +use std::borrow::Cow; +use std::path::Path; + +use url::Url; + +use crate::FQDN; + +#[derive(Clone, Copy)] +pub enum Config<'a> { + NameServer { origin: &'a FQDN }, + Resolver { use_dnssec: bool, netmask: &'a str }, +} + +impl Config<'_> { + pub fn role(&self) -> Role { + match self { + Config::NameServer { .. } => Role::NameServer, + Config::Resolver { .. } => Role::Resolver, + } + } +} + +#[derive(Clone, Copy)] +pub enum Role { + NameServer, + Resolver, +} + +impl Role { + #[must_use] + pub fn is_resolver(&self) -> bool { + matches!(self, Self::Resolver) + } +} + +#[derive(Clone)] +pub enum Implementation { + Bind, + Hickory(Repository<'static>), + Unbound, +} + +impl Implementation { + #[must_use] + pub fn is_bind(&self) -> bool { + matches!(self, Self::Bind) + } + + pub(crate) fn format_config(&self, config: Config) -> String { + match config { + Config::Resolver { + use_dnssec, + netmask, + } => match self { + Self::Bind => { + minijinja::render!( + include_str!("templates/named.resolver.conf.jinja"), + use_dnssec => use_dnssec, + netmask => netmask, + ) + } + + Self::Hickory(_) => { + minijinja::render!( + include_str!("templates/hickory.resolver.toml.jinja"), + use_dnssec => use_dnssec, + ) + } + + Self::Unbound => { + minijinja::render!( + include_str!("templates/unbound.conf.jinja"), + use_dnssec => use_dnssec, + netmask => netmask, + ) + } + }, + + Config::NameServer { origin } => match self { + Self::Bind => { + minijinja::render!( + include_str!("templates/named.name-server.conf.jinja"), + fqdn => origin.as_str() + ) + } + + Self::Unbound => { + minijinja::render!( + include_str!("templates/nsd.conf.jinja"), + fqdn => origin.as_str() + ) + } + + Self::Hickory(_) => unimplemented!(), + }, + } + } + + pub(crate) fn conf_file_path(&self, role: Role) -> &'static str { + match self { + Self::Bind => "/etc/bind/named.conf", + + Self::Hickory(_) => "/etc/named.toml", + + Self::Unbound => match role { + Role::NameServer => "/etc/nsd/nsd.conf", + Role::Resolver => "/etc/unbound/unbound.conf", + }, + } + } + + pub(crate) fn cmd_args(&self, role: Role) -> &[&'static str] { + match self { + Implementation::Bind => &["named", "-g", "-d5"], + + Implementation::Hickory(_) => { + assert!( + role.is_resolver(), + "hickory acting in `NameServer` role is currently not supported" + ); + + &["hickory-dns", "-d"] + } + + Implementation::Unbound => match role { + Role::NameServer => &["nsd", "-d"], + + Role::Resolver => &["unbound", "-d"], + }, + } + } + + pub(crate) fn pidfile(&self, role: Role) -> &'static str { + match self { + Implementation::Bind => "/tmp/named.pid", + + Implementation::Hickory(_) => unimplemented!(), + + Implementation::Unbound => match role { + Role::NameServer => "/tmp/nsd.pid", + Role::Resolver => "/tmp/unbound.pid", + }, + } + } +} + +impl fmt::Display for Implementation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + Implementation::Bind => "bind", + Implementation::Hickory(_) => "hickory", + Implementation::Unbound => "unbound", + }; + + f.write_str(s) + } +} + +#[derive(Clone)] +pub struct Repository<'a> { + inner: Cow<'a, str>, +} + +impl Repository<'_> { + pub(crate) fn as_str(&self) -> &str { + &self.inner + } +} + +/// checks that `input` looks like a valid repository which can be either local or remote +/// +/// # Panics +/// +/// this function panics if `input` is not a local `Path` that exists or a well-formed URL +#[allow(non_snake_case)] +pub fn Repository(input: impl Into>) -> Repository<'static> { + let input = input.into(); + assert!( + Path::new(&*input).exists() || Url::parse(&input).is_ok(), + "{input} is not a valid repository" + ); + Repository { inner: input } +} + +impl Default for Implementation { + fn default() -> Self { + Self::Unbound + } +} diff --git a/packages/dns-test/src/lib.rs b/packages/dns-test/src/lib.rs index eb7fe63a..cfbcb96f 100644 --- a/packages/dns-test/src/lib.rs +++ b/packages/dns-test/src/lib.rs @@ -1,18 +1,15 @@ //! A test framework for all things DNS -use std::borrow::Cow; -use std::path::Path; - -use url::Url; - pub use crate::container::Network; pub use crate::fqdn::FQDN; +pub use crate::implementation::{Implementation, Repository}; pub use crate::resolver::Resolver; pub use crate::trust_anchor::TrustAnchor; pub mod client; mod container; mod fqdn; +mod implementation; pub mod name_server; pub mod record; mod resolver; @@ -26,52 +23,6 @@ pub type Result = core::result::Result; // TODO maybe this should be a TLS variable that each unit test (thread) can override const DEFAULT_TTL: u32 = 24 * 60 * 60; // 1 day -#[derive(Clone)] -pub enum Implementation { - Bind, - Hickory(Repository<'static>), - Unbound, -} - -impl Implementation { - #[must_use] - pub fn is_bind(&self) -> bool { - matches!(self, Self::Bind) - } -} - -#[derive(Clone)] -pub struct Repository<'a> { - inner: Cow<'a, str>, -} - -impl Repository<'_> { - fn as_str(&self) -> &str { - &self.inner - } -} - -/// checks that `input` looks like a valid repository which can be either local or remote -/// -/// # Panics -/// -/// this function panics if `input` is not a local `Path` that exists or a well-formed URL -#[allow(non_snake_case)] -pub fn Repository(input: impl Into>) -> Repository<'static> { - let input = input.into(); - assert!( - Path::new(&*input).exists() || Url::parse(&input).is_ok(), - "{input} is not a valid repository" - ); - Repository { inner: input } -} - -impl Default for Implementation { - fn default() -> Self { - Self::Unbound - } -} - pub fn subject() -> Implementation { if let Ok(subject) = std::env::var("DNS_TEST_SUBJECT") { if subject == "unbound" { diff --git a/packages/dns-test/src/name_server.rs b/packages/dns-test/src/name_server.rs index 81ce4d70..eea06b21 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -2,6 +2,7 @@ use core::sync::atomic::{self, AtomicUsize}; use std::net::Ipv4Addr; use crate::container::{Child, Container, Network}; +use crate::implementation::{Config, Role}; use crate::record::{self, Record, SoaSettings, DS, SOA}; use crate::tshark::Tshark; use crate::zone_file::{self, ZoneFile}; @@ -147,30 +148,19 @@ impl NameServer { state: _, } = self; - let origin = zone_file.origin(); - let (path, contents, cmd_args) = match &implementation { - Implementation::Bind => ( - "/etc/bind/named.conf", - named_conf(origin), - &["named", "-g", "-d5"][..], - ), - - Implementation::Unbound => { - // for PID file - container.status_ok(&["mkdir", "-p", "/run/nsd/"])?; - - ("/etc/nsd/nsd.conf", nsd_conf(origin), &["nsd", "-d"][..]) - } - - Implementation::Hickory(_) => unreachable!(), + let config = Config::NameServer { + origin: zone_file.origin(), }; - container.cp(path, &contents)?; + container.cp( + implementation.conf_file_path(config.role()), + &implementation.format_config(config), + )?; container.status_ok(&["mkdir", "-p", ZONES_DIR])?; container.cp(&zone_file_path(), &zone_file.to_string())?; - let child = container.spawn(cmd_args)?; + let child = container.spawn(implementation.cmd_args(config.role()))?; Ok(NameServer { container, @@ -203,32 +193,16 @@ impl NameServer { state, } = self; - let (conf_path, conf_contents, cmd_args) = match implementation { - Implementation::Bind => ( - "/etc/bind/named.conf", - named_conf(zone_file.origin()), - &["named", "-g", "-d5"][..], - ), - - Implementation::Unbound => { - // for PID file - container.status_ok(&["mkdir", "-p", "/run/nsd/"])?; - - ( - "/etc/nsd/nsd.conf", - nsd_conf(zone_file.origin()), - &["nsd", "-d"][..], - ) - } - - Implementation::Hickory(..) => unreachable!(), + let config = Config::NameServer { + origin: zone_file.origin(), }; - - container.cp(conf_path, &conf_contents)?; - + container.cp( + implementation.conf_file_path(config.role()), + &implementation.format_config(config), + )?; container.cp(&zone_file_path(), &state.signed.to_string())?; - let child = container.spawn(cmd_args)?; + let child = container.spawn(implementation.cmd_args(config.role()))?; Ok(NameServer { container, @@ -267,13 +241,8 @@ impl NameServer { /// gracefully terminates the name server collecting all logs pub fn terminate(self) -> Result { - let pidfile = match &self.implementation { - Implementation::Bind => "/tmp/named.pid", + let pidfile = self.implementation.pidfile(Role::NameServer); - Implementation::Unbound => "/run/nsd/nsd.pid", - - Implementation::Hickory(_) => unreachable!(), - }; // 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!( @@ -284,7 +253,9 @@ kill -TERM $(cat {pidfile})" let output = self.state.child.wait()?; if !output.status.success() { - return Err("could not terminate the `unbound` process".into()); + return Err( + format!("could not terminate the `{}` process", self.implementation).into(), + ); } assert!( @@ -339,20 +310,6 @@ fn admin_ns(ns_count: usize) -> FQDN { FQDN(format!("admin{ns_count}.nameservers.com.")).unwrap() } -fn named_conf(fqdn: &FQDN) -> String { - minijinja::render!( - include_str!("templates/named.name-server.conf.jinja"), - fqdn => fqdn.as_str() - ) -} - -fn nsd_conf(fqdn: &FQDN) -> String { - minijinja::render!( - include_str!("templates/nsd.conf.jinja"), - fqdn => fqdn.as_str() - ) -} - #[cfg(test)] mod tests { use crate::client::{Client, DigSettings}; diff --git a/packages/dns-test/src/resolver.rs b/packages/dns-test/src/resolver.rs index efa6da29..4460a198 100644 --- a/packages/dns-test/src/resolver.rs +++ b/packages/dns-test/src/resolver.rs @@ -2,6 +2,7 @@ use core::fmt::Write; use std::net::Ipv4Addr; use crate::container::{Child, Container, Network}; +use crate::implementation::{Config, Role}; use crate::trust_anchor::TrustAnchor; use crate::tshark::Tshark; use crate::zone_file::Root; @@ -40,34 +41,17 @@ impl Resolver { writeln!(hints, "{root}").unwrap(); } + container.cp("/etc/root.hints", &hints)?; + 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)?; - - container.cp( - "/etc/unbound/unbound.conf", - &unbound_conf(use_dnssec, network.netmask()), - )?; - } - - 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))?; - } - } + let config = Config::Resolver { + use_dnssec, + netmask: network.netmask(), + }; + container.cp( + implementation.conf_file_path(config.role()), + &implementation.format_config(config), + )?; if use_dnssec { let path = if implementation.is_bind() { @@ -85,12 +69,7 @@ impl Resolver { 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)?; + let child = container.spawn(implementation.cmd_args(config.role()))?; Ok(Self { child, @@ -113,11 +92,7 @@ impl Resolver { /// gracefully terminates the name server collecting all logs pub fn terminate(self) -> Result { - let pidfile = match self.implementation { - Implementation::Bind => "/tmp/named.pid", - Implementation::Unbound => "/run/unbound.pid", - Implementation::Hickory(..) => unimplemented!(), - }; + let pidfile = self.implementation.pidfile(Role::Resolver); let kill = format!( "test -f {pidfile} || sleep 1 kill -TERM $(cat {pidfile})" @@ -137,18 +112,6 @@ 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) -} - -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 crate::{name_server::NameServer, FQDN}; diff --git a/packages/dns-test/src/templates/hickory.resolver.toml.jinja b/packages/dns-test/src/templates/hickory.resolver.toml.jinja index d3da6496..418db8fd 100644 --- a/packages/dns-test/src/templates/hickory.resolver.toml.jinja +++ b/packages/dns-test/src/templates/hickory.resolver.toml.jinja @@ -1,5 +1,5 @@ [[zones]] zone = "." zone_type = "Hint" -stores = { type = "recursor", roots = "/etc/hickory/root.hints", ns_cache_size = 1024, record_cache_size = 1048576 } +stores = { type = "recursor", roots = "/etc/root.hints", ns_cache_size = 1024, record_cache_size = 1048576 } enable_dnssec = {{ use_dnssec }} diff --git a/packages/dns-test/src/templates/named.resolver.conf.jinja b/packages/dns-test/src/templates/named.resolver.conf.jinja index 4b57908d..cc609492 100644 --- a/packages/dns-test/src/templates/named.resolver.conf.jinja +++ b/packages/dns-test/src/templates/named.resolver.conf.jinja @@ -10,5 +10,5 @@ options { zone "." { type hint; - file "/etc/bind/root.hints"; + file "/etc/root.hints"; }; diff --git a/packages/dns-test/src/templates/nsd.conf.jinja b/packages/dns-test/src/templates/nsd.conf.jinja index 36dea4fc..7a5b0ec3 100644 --- a/packages/dns-test/src/templates/nsd.conf.jinja +++ b/packages/dns-test/src/templates/nsd.conf.jinja @@ -1,3 +1,6 @@ +server: + pidfile: /tmp/nsd.pid + remote-control: control-enable: no diff --git a/packages/dns-test/src/templates/unbound.conf.jinja b/packages/dns-test/src/templates/unbound.conf.jinja index ca5e54d3..212078aa 100644 --- a/packages/dns-test/src/templates/unbound.conf.jinja +++ b/packages/dns-test/src/templates/unbound.conf.jinja @@ -3,7 +3,8 @@ server: use-syslog: no interface: 0.0.0.0 access-control: {{ netmask }} allow - root-hints: /etc/unbound/root.hints + root-hints: /etc/root.hints + pidfile: /tmp/unbound.pid {% if use_dnssec %} trust-anchor-file: /etc/trusted-key.key {% endif %} From 70245e7ff8e75dcb053d62b0a106f2bfd69a4991 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 5 Mar 2024 14:41:45 +0100 Subject: [PATCH 093/124] refactor: use builder pattern in Resolver ctor the `start` constructor's parameter list was getting long and we want to add even more configuration options, like EDE, in the future. using the builder pattern lets us introduce new settings without breaking changes --- .../src/resolver/dns/scenarios.rs | 16 +- .../dnssec/rfc4035/section_4/section_4_1.rs | 10 +- .../src/resolver/dnssec/scenarios/bogus.rs | 13 +- .../src/resolver/dnssec/scenarios/secure.rs | 23 +-- packages/dns-test/examples/explore.rs | 9 +- packages/dns-test/src/resolver.rs | 160 ++++++++++-------- packages/dns-test/src/trust_anchor.rs | 4 + packages/dns-test/src/tshark.rs | 12 +- 8 files changed, 138 insertions(+), 109 deletions(-) diff --git a/packages/conformance-tests/src/resolver/dns/scenarios.rs b/packages/conformance-tests/src/resolver/dns/scenarios.rs index 20a90d07..121936cd 100644 --- a/packages/conformance-tests/src/resolver/dns/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dns/scenarios.rs @@ -4,7 +4,7 @@ use dns_test::client::{Client, DigSettings}; use dns_test::name_server::NameServer; use dns_test::record::{Record, RecordType}; use dns_test::zone_file::Root; -use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; +use dns_test::{Network, Resolver, Result, FQDN}; #[test] fn can_resolve() -> Result<()> { @@ -39,8 +39,11 @@ fn can_resolve() -> Result<()> { 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(), &network)?; + let resolver = Resolver::new( + &network, + Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr()), + ) + .start(&dns_test::subject())?; let resolver_ip_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; @@ -85,8 +88,11 @@ fn nxdomain() -> Result<()> { 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(), &network)?; + let resolver = Resolver::new( + &network, + Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr()), + ) + .start(&dns_test::subject())?; let resolver_ip_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs index 88a65eb8..6bfbacf2 100644 --- a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs @@ -3,19 +3,15 @@ use dns_test::name_server::NameServer; use dns_test::record::RecordType; use dns_test::tshark::{Capture, Direction}; use dns_test::zone_file::Root; -use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; +use dns_test::{Network, Resolver, Result, FQDN}; #[test] #[ignore] fn edns_support() -> Result<()> { let network = &Network::new()?; let ns = NameServer::new(&dns_test::peer(), FQDN::ROOT, network)?.start()?; - let resolver = Resolver::start( - &dns_test::subject(), - &[Root::new(ns.fqdn().clone(), ns.ipv4_addr())], - &TrustAnchor::empty(), - network, - )?; + let resolver = Resolver::new(network, Root::new(ns.fqdn().clone(), ns.ipv4_addr())) + .start(&dns_test::subject())?; let mut tshark = resolver.eavesdrop()?; diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs index 0dd99b51..5f51d7dc 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs @@ -5,7 +5,7 @@ use dns_test::client::{Client, DigSettings}; use dns_test::name_server::NameServer; use dns_test::record::{Record, RecordType}; use dns_test::zone_file::Root; -use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; +use dns_test::{Network, Resolver, Result, FQDN}; #[ignore] #[test] @@ -64,10 +64,13 @@ fn bad_signature_in_leaf_nameserver() -> Result<()> { let root_ns = root_ns.start()?; - 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, &network)?; + let resolver = Resolver::new( + &network, + Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr()), + ) + .trust_anchor_key(root_ksk) + .trust_anchor_key(root_zsk) + .start(&dns_test::subject())?; let resolver_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs index 6df21009..3ed3439b 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs @@ -24,10 +24,10 @@ fn can_validate_without_delegation() -> Result<()> { 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, &network)?; + let trust_anchor = &TrustAnchor::from_iter([root_ksk.clone(), root_zsk.clone()]); + let resolver = Resolver::new(&network, Root::new(ns.fqdn().clone(), ns.ipv4_addr())) + .trust_anchor(trust_anchor) + .start(&dns_test::subject())?; let resolver_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; @@ -37,7 +37,7 @@ fn can_validate_without_delegation() -> Result<()> { assert!(output.status.is_noerror()); assert!(output.flags.authenticated_data); - let output = client.delv(resolver_addr, RecordType::SOA, &FQDN::ROOT, &trust_anchor)?; + let output = client.delv(resolver_addr, RecordType::SOA, &FQDN::ROOT, trust_anchor)?; assert!(output.starts_with("; fully validated")); Ok(()) @@ -91,10 +91,13 @@ fn can_validate_with_delegation() -> Result<()> { 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, &network)?; + let trust_anchor = &TrustAnchor::from_iter([root_ksk, root_zsk]); + let resolver = Resolver::new( + &network, + Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr()), + ) + .trust_anchor(trust_anchor) + .start(&dns_test::subject())?; let resolver_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; @@ -111,7 +114,7 @@ fn can_validate_with_delegation() -> Result<()> { 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)?; + let output = client.delv(resolver_addr, RecordType::A, &needle_fqdn, trust_anchor)?; assert!(output.starts_with("; fully validated")); Ok(()) diff --git a/packages/dns-test/examples/explore.rs b/packages/dns-test/examples/explore.rs index 038870a1..6e8de222 100644 --- a/packages/dns-test/examples/explore.rs +++ b/packages/dns-test/examples/explore.rs @@ -44,13 +44,16 @@ fn main() -> Result<()> { let root_zsk = root_ns.zone_signing_key().clone(); let root_ns = root_ns.start()?; - - let roots = &[Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr())]; println!("DONE"); let trust_anchor = TrustAnchor::from_iter([root_ksk.clone(), root_zsk.clone()]); println!("building docker image..."); - let resolver = Resolver::start(&dns_test::subject(), roots, &trust_anchor, &network)?; + let resolver = Resolver::new( + &network, + Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr()), + ) + .trust_anchor(&trust_anchor) + .start(&dns_test::subject())?; println!("DONE\n\n"); let resolver_addr = resolver.ipv4_addr(); diff --git a/packages/dns-test/src/resolver.rs b/packages/dns-test/src/resolver.rs index 4460a198..93d17f3a 100644 --- a/packages/dns-test/src/resolver.rs +++ b/packages/dns-test/src/resolver.rs @@ -3,6 +3,7 @@ use std::net::Ipv4Addr; use crate::container::{Child, Container, Network}; use crate::implementation::{Config, Role}; +use crate::record::DNSKEY; use crate::trust_anchor::TrustAnchor; use crate::tshark::Tshark; use crate::zone_file::Root; @@ -15,67 +16,13 @@ pub struct Resolver { } impl Resolver { - /// Starts a DNS server in the recursive resolver role - /// - /// This server is not an authoritative name server; it does not server a zone file to clients - /// - /// # Panics - /// - /// This constructor panics if `roots` is an empty slice - pub fn start( - implementation: &Implementation, - roots: &[Root], - trust_anchor: &TrustAnchor, - network: &Network, - ) -> Result { - assert!( - !roots.is_empty(), - "must configure at least one local root server" - ); - - let image = implementation.clone().into(); - let container = Container::run(&image, network)?; - - let mut hints = String::new(); - for root in roots { - writeln!(hints, "{root}").unwrap(); + #[allow(clippy::new_ret_no_self)] + pub fn new(network: &Network, root: Root) -> ResolverSettings { + ResolverSettings { + network: network.clone(), + roots: vec![root], + trust_anchor: TrustAnchor::empty(), } - - container.cp("/etc/root.hints", &hints)?; - - let use_dnssec = !trust_anchor.is_empty(); - let config = Config::Resolver { - use_dnssec, - netmask: network.netmask(), - }; - container.cp( - implementation.conf_file_path(config.role()), - &implementation.format_config(config), - )?; - - if use_dnssec { - 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 child = container.spawn(implementation.cmd_args(config.role()))?; - - Ok(Self { - child, - container, - implementation: implementation.clone(), - }) } pub fn eavesdrop(&self) -> Result { @@ -112,6 +59,83 @@ kill -TERM $(cat {pidfile})" } } +pub struct ResolverSettings { + network: Network, + roots: Vec, + trust_anchor: TrustAnchor, +} + +impl ResolverSettings { + /// Starts a DNS server in the recursive resolver role + /// + /// This server is not an authoritative name server; it does not serve a zone file to clients + pub fn start(&self, implementation: &Implementation) -> Result { + let image = implementation.clone().into(); + let container = Container::run(&image, &self.network)?; + + let mut hints = String::new(); + for root in &self.roots { + writeln!(hints, "{root}").unwrap(); + } + + container.cp("/etc/root.hints", &hints)?; + + let use_dnssec = !self.trust_anchor.is_empty(); + let config = Config::Resolver { + use_dnssec, + netmask: self.network.netmask(), + }; + container.cp( + implementation.conf_file_path(config.role()), + &implementation.format_config(config), + )?; + + if use_dnssec { + let path = if implementation.is_bind() { + "/etc/bind/bind.keys" + } else { + "/etc/trusted-key.key" + }; + + let contents = if implementation.is_bind() { + self.trust_anchor.delv() + } else { + self.trust_anchor.to_string() + }; + + container.cp(path, &contents)?; + } + + let child = container.spawn(implementation.cmd_args(config.role()))?; + + Ok(Resolver { + child, + container, + implementation: implementation.clone(), + }) + } + + /// Adds a root hint + pub fn root(&mut self, root: Root) -> &mut Self { + self.roots.push(root); + self + } + + /// Adds a DNSKEY record to the trust anchor + pub fn trust_anchor_key(&mut self, key: DNSKEY) -> &mut Self { + self.trust_anchor.add(key.clone()); + self + } + + /// Adds all the keys in the `other` trust anchor to ours + pub fn trust_anchor(&mut self, other: &TrustAnchor) -> &mut Self { + for key in other.keys() { + self.trust_anchor.add(key.clone()); + } + self + } +} + #[cfg(test)] mod tests { use crate::{name_server::NameServer, FQDN}; @@ -122,12 +146,8 @@ mod tests { fn terminate_unbound_works() -> Result<()> { let network = Network::new()?; let ns = NameServer::new(&Implementation::Unbound, FQDN::ROOT, &network)?.start()?; - let resolver = Resolver::start( - &Implementation::Unbound, - &[Root::new(ns.fqdn().clone(), ns.ipv4_addr())], - &TrustAnchor::empty(), - &network, - )?; + let resolver = Resolver::new(&network, Root::new(ns.fqdn().clone(), ns.ipv4_addr())) + .start(&Implementation::Unbound)?; let logs = resolver.terminate()?; eprintln!("{logs}"); @@ -140,12 +160,8 @@ mod tests { 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 resolver = Resolver::new(&network, Root::new(ns.fqdn().clone(), ns.ipv4_addr())) + .start(&Implementation::Bind)?; let logs = resolver.terminate()?; eprintln!("{logs}"); diff --git a/packages/dns-test/src/trust_anchor.rs b/packages/dns-test/src/trust_anchor.rs index e6333bbe..01e1f631 100644 --- a/packages/dns-test/src/trust_anchor.rs +++ b/packages/dns-test/src/trust_anchor.rs @@ -20,6 +20,10 @@ impl TrustAnchor { self } + pub(crate) fn keys(&self) -> &[DNSKEY] { + &self.keys + } + /// formats the `TrustAnchor` in the format `delv` expects pub(super) fn delv(&self) -> String { let mut buf = "trust-anchors {".to_string(); diff --git a/packages/dns-test/src/tshark.rs b/packages/dns-test/src/tshark.rs index e736ea9d..8b850019 100644 --- a/packages/dns-test/src/tshark.rs +++ b/packages/dns-test/src/tshark.rs @@ -248,7 +248,7 @@ mod tests { use crate::name_server::NameServer; use crate::record::{Record, RecordType}; use crate::zone_file::Root; - use crate::{Implementation, Network, Resolver, TrustAnchor, FQDN}; + use crate::{Implementation, Network, Resolver, FQDN}; use super::*; @@ -310,13 +310,11 @@ mod tests { 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( - &Implementation::Unbound, - roots, - &TrustAnchor::empty(), + let resolver = Resolver::new( network, - )?; + Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr()), + ) + .start(&Implementation::Unbound)?; let mut tshark = resolver.eavesdrop()?; let resolver_addr = resolver.ipv4_addr(); From 166863bcc4a315442f1d49a2d61ecbec8de5d6cd Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 5 Mar 2024 14:10:20 +0100 Subject: [PATCH 094/124] allow enabling Extended DNS Errors (EDE) --- packages/dns-test/src/implementation.rs | 24 +++++++++++++++++-- packages/dns-test/src/resolver.rs | 10 ++++++++ .../dns-test/src/templates/unbound.conf.jinja | 1 + 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/dns-test/src/implementation.rs b/packages/dns-test/src/implementation.rs index 256ad247..1ec81e15 100644 --- a/packages/dns-test/src/implementation.rs +++ b/packages/dns-test/src/implementation.rs @@ -8,8 +8,15 @@ use crate::FQDN; #[derive(Clone, Copy)] pub enum Config<'a> { - NameServer { origin: &'a FQDN }, - Resolver { use_dnssec: bool, netmask: &'a str }, + NameServer { + origin: &'a FQDN, + }, + Resolver { + use_dnssec: bool, + netmask: &'a str, + /// Extended DNS error (RFC8914) + ede: bool, + }, } impl Config<'_> { @@ -42,6 +49,14 @@ pub enum Implementation { } impl Implementation { + pub fn supports_ede(&self) -> bool { + match self { + Implementation::Bind => false, + Implementation::Hickory(_) => true, + Implementation::Unbound => true, + } + } + #[must_use] pub fn is_bind(&self) -> bool { matches!(self, Self::Bind) @@ -52,8 +67,11 @@ impl Implementation { Config::Resolver { use_dnssec, netmask, + ede, } => match self { Self::Bind => { + assert!(!ede, "the BIND resolver does not support EDE (RFC8914)"); + minijinja::render!( include_str!("templates/named.resolver.conf.jinja"), use_dnssec => use_dnssec, @@ -62,6 +80,7 @@ impl Implementation { } Self::Hickory(_) => { + // TODO enable EDE in Hickory when supported minijinja::render!( include_str!("templates/hickory.resolver.toml.jinja"), use_dnssec => use_dnssec, @@ -73,6 +92,7 @@ impl Implementation { include_str!("templates/unbound.conf.jinja"), use_dnssec => use_dnssec, netmask => netmask, + ede => ede, ) } }, diff --git a/packages/dns-test/src/resolver.rs b/packages/dns-test/src/resolver.rs index 93d17f3a..4a5ddf43 100644 --- a/packages/dns-test/src/resolver.rs +++ b/packages/dns-test/src/resolver.rs @@ -19,6 +19,7 @@ impl Resolver { #[allow(clippy::new_ret_no_self)] pub fn new(network: &Network, root: Root) -> ResolverSettings { ResolverSettings { + ede: false, network: network.clone(), roots: vec![root], trust_anchor: TrustAnchor::empty(), @@ -60,6 +61,8 @@ kill -TERM $(cat {pidfile})" } pub struct ResolverSettings { + /// Extended DNS Errors (RFC8914) + ede: bool, network: Network, roots: Vec, trust_anchor: TrustAnchor, @@ -84,6 +87,7 @@ impl ResolverSettings { let config = Config::Resolver { use_dnssec, netmask: self.network.netmask(), + ede: self.ede, }; container.cp( implementation.conf_file_path(config.role()), @@ -115,6 +119,12 @@ impl ResolverSettings { }) } + /// Enables the Extended DNS Errors (RFC8914) feature + pub fn extended_dns_errors(&mut self) -> &mut Self { + self.ede = true; + self + } + /// Adds a root hint pub fn root(&mut self, root: Root) -> &mut Self { self.roots.push(root); diff --git a/packages/dns-test/src/templates/unbound.conf.jinja b/packages/dns-test/src/templates/unbound.conf.jinja index 212078aa..13eeb758 100644 --- a/packages/dns-test/src/templates/unbound.conf.jinja +++ b/packages/dns-test/src/templates/unbound.conf.jinja @@ -5,6 +5,7 @@ server: access-control: {{ netmask }} allow root-hints: /etc/root.hints pidfile: /tmp/unbound.pid + ede: {% if ede %} yes {% else %} no {% endif %} {% if use_dnssec %} trust-anchor-file: /etc/trusted-key.key {% endif %} From dc197761075a50ad6953ec11473874e7df310ea6 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 5 Mar 2024 18:47:50 +0100 Subject: [PATCH 095/124] parse EDE info from dig's output --- packages/dns-test/src/client.rs | 69 ++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/packages/dns-test/src/client.rs b/packages/dns-test/src/client.rs index 0f9e4aed..3dd2a652 100644 --- a/packages/dns-test/src/client.rs +++ b/packages/dns-test/src/client.rs @@ -141,6 +141,7 @@ impl DigSettings { #[derive(Debug)] pub struct DigOutput { + pub ede: Option, pub flags: DigFlags, pub status: DigStatus, pub answer: Vec, @@ -154,6 +155,7 @@ impl FromStr for DigOutput { fn from_str(input: &str) -> Result { const FLAGS_PREFIX: &str = ";; flags: "; const STATUS_PREFIX: &str = ";; ->>HEADER<<- opcode: QUERY, status: "; + const EDE_PREFIX: &str = "; EDE: "; const ANSWER_HEADER: &str = ";; ANSWER SECTION:"; const AUTHORITY_HEADER: &str = ";; AUTHORITY SECTION:"; @@ -173,6 +175,7 @@ impl FromStr for DigOutput { let mut status = None; let mut answer = None; let mut authority = None; + let mut ede = None; let mut lines = input.lines(); while let Some(line) = lines.next() { @@ -196,6 +199,17 @@ impl FromStr for DigOutput { } status = Some(status_text.parse()?); + } else if let Some(unprefixed) = line.strip_prefix(EDE_PREFIX) { + let code = unprefixed + .split_once(' ') + .map(|(code, _rest)| code) + .unwrap_or(unprefixed); + + if ede.is_some() { + return Err(more_than_once(EDE_PREFIX).into()); + } + + ede = Some(code.parse()?); } else if line.starts_with(ANSWER_HEADER) { if answer.is_some() { return Err(more_than_once(ANSWER_HEADER).into()); @@ -230,14 +244,37 @@ impl FromStr for DigOutput { } Ok(Self { - flags: flags.ok_or_else(|| not_found(FLAGS_PREFIX))?, - status: status.ok_or_else(|| not_found(STATUS_PREFIX))?, answer: answer.unwrap_or_default(), authority: authority.unwrap_or_default(), + ede, + flags: flags.ok_or_else(|| not_found(FLAGS_PREFIX))?, + status: status.ok_or_else(|| not_found(STATUS_PREFIX))?, }) } } +#[derive(Debug, PartialEq)] +pub enum ExtendedDnsError { + DnssecBogus, + DnskeyMissing, +} + +impl FromStr for ExtendedDnsError { + type Err = Error; + + fn from_str(input: &str) -> std::prelude::v1::Result { + let code: u16 = input.parse()?; + + let code = match code { + 6 => Self::DnssecBogus, + 9 => Self::DnskeyMissing, + _ => todo!("EDE {code} has not yet been implemented"), + }; + + Ok(code) + } +} + #[derive(Debug, Default, PartialEq)] pub struct DigFlags { pub authenticated_data: bool, @@ -398,4 +435,32 @@ mod tests { Ok(()) } + + #[test] + fn ede() -> Result<()> { + let input = "; <<>> DiG 9.18.24-1-Debian <<>> +recurse +nodnssec +adflag +nocdflag @192.168.176.5 A example.nameservers.com. +; (1 server found) +;; global options: +cmd +;; Got answer: +;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 49801 +;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1 + +;; OPT PSEUDOSECTION: +; EDNS: version: 0, flags:; udp: 1232 +; EDE: 9 (DNSKEY Missing) +;; QUESTION SECTION: +;example.nameservers.com. IN A + +;; Query time: 26 msec +;; SERVER: 192.168.176.5#53(192.168.176.5) (UDP) +;; WHEN: Tue Mar 05 17:45:29 UTC 2024 +;; MSG SIZE rcvd: 58 +"; + + let output: DigOutput = input.parse()?; + + assert_eq!(Some(ExtendedDnsError::DnskeyMissing), output.ede); + + Ok(()) + } } From 31048f5cd036708bbfdd9f4f80c38e4d8408ce16 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 5 Mar 2024 18:48:32 +0100 Subject: [PATCH 096/124] extend DNSKEY API --- packages/dns-test/src/record.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/dns-test/src/record.rs b/packages/dns-test/src/record.rs index 0abc7df8..aaaf446a 100644 --- a/packages/dns-test/src/record.rs +++ b/packages/dns-test/src/record.rs @@ -239,6 +239,8 @@ pub struct DNSKEY { } impl DNSKEY { + const KSK_BIT: u16 = 1; + /// formats the `DNSKEY` in the format `delv` expects pub(super) fn delv(&self) -> String { let Self { @@ -252,6 +254,19 @@ impl DNSKEY { format!("{zone} static-key {flags} {protocol} {algorithm} \"{public_key}\";\n") } + + pub fn clear_key_signing_key_bit(&mut self) { + self.flags &= !Self::KSK_BIT; + } + + pub fn is_key_signing_key(&self) -> bool { + let mask = Self::KSK_BIT; + self.flags & mask == mask + } + + pub fn is_zone_signing_key(&self) -> bool { + !self.is_key_signing_key() + } } impl FromStr for DNSKEY { From 95f94e2c7b5c9a7c73b6c3540549a2a4d7c4af98 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 5 Mar 2024 18:48:48 +0100 Subject: [PATCH 097/124] add first EDE test --- .../src/resolver/dnssec/scenarios.rs | 1 + .../src/resolver/dnssec/scenarios/ede.rs | 101 ++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs index 416fff70..6dd90eea 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios.rs @@ -1,2 +1,3 @@ mod bogus; +mod ede; mod secure; diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs new file mode 100644 index 00000000..95d1dcda --- /dev/null +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs @@ -0,0 +1,101 @@ +use std::net::Ipv4Addr; + +use dns_test::client::{Client, DigSettings, ExtendedDnsError}; +use dns_test::name_server::NameServer; +use dns_test::record::{Record, RecordType}; +use dns_test::zone_file::Root; +use dns_test::{Network, Resolver, Result, FQDN}; + +#[ignore] +#[test] +fn dnskey_missing() -> Result<()> { + let subject = dns_test::subject(); + let supports_ede = subject.supports_ede(); + + let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4); + let needle_fqdn = FQDN("example.nameservers.com.")?; + + let network = Network::new()?; + let peer = dns_test::peer(); + let mut root_ns = NameServer::new(&peer, FQDN::ROOT, &network)?; + let mut com_ns = NameServer::new(&peer, FQDN::COM, &network)?; + + let mut nameservers_ns = NameServer::new(&peer, FQDN("nameservers.com.")?, &network)?; + nameservers_ns + .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) + .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())) + .add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); + let mut nameservers_ns = nameservers_ns.sign()?; + + // remove the ZSK DNSKEY record + let records = &mut nameservers_ns.signed_zone_file_mut().records; + let mut remove_count = 0; + *records = records + .drain(..) + .filter(|record| { + let remove = if let Record::DNSKEY(dnskey) = record { + dnskey.is_zone_signing_key() + } else { + false + }; + + if remove { + remove_count += 1; + } + + !remove + }) + .collect(); + assert_eq!(1, remove_count); + + let nameservers_ds = nameservers_ns.ds().clone(); + let nameservers_ns = nameservers_ns.start()?; + + com_ns + .referral( + nameservers_ns.zone().clone(), + nameservers_ns.fqdn().clone(), + nameservers_ns.ipv4_addr(), + ) + .add(nameservers_ds); + let com_ns = com_ns.sign()?; + let com_ds = com_ns.ds().clone(); + let com_ns = com_ns.start()?; + + root_ns + .referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr()) + .add(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(); + + let root_ns = root_ns.start()?; + + let mut resolver = Resolver::new( + &network, + Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr()), + ); + + if supports_ede { + resolver.extended_dns_errors(); + } + + let resolver = resolver + .trust_anchor_key(root_ksk) + .trust_anchor_key(root_zsk) + .start(&subject)?; + let resolver_addr = resolver.ipv4_addr(); + + let client = Client::new(&network)?; + + let settings = *DigSettings::default().recurse().authentic_data(); + let output = client.dig(settings, resolver_addr, RecordType::A, &needle_fqdn)?; + + assert!(output.status.is_servfail()); + + if supports_ede { + assert_eq!(Some(ExtendedDnsError::DnskeyMissing), output.ede); + } + + Ok(()) +} From 7bb6b9439cb4cb2d6939c8783ca320206e1ce398 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Wed, 6 Mar 2024 20:31:14 +0100 Subject: [PATCH 098/124] add name_server::Graph --- packages/dns-test/src/container.rs | 8 +- packages/dns-test/src/fqdn.rs | 60 ++++++++++++ packages/dns-test/src/name_server.rs | 131 ++++++++++++++++++++++++++- 3 files changed, 195 insertions(+), 4 deletions(-) diff --git a/packages/dns-test/src/container.rs b/packages/dns-test/src/container.rs index fb44c4ad..e19bd50a 100644 --- a/packages/dns-test/src/container.rs +++ b/packages/dns-test/src/container.rs @@ -157,7 +157,7 @@ impl Container { id, name, ipv4_addr, - _network: network.clone(), + network: network.clone(), }; Ok(Self { inner: Arc::new(inner), @@ -250,6 +250,10 @@ impl Container { pub fn id(&self) -> &str { &self.inner.id } + + pub(crate) fn network(&self) -> &Network { + &self.inner.network + } } fn verbose_docker_build() -> bool { @@ -282,7 +286,7 @@ struct Inner { id: String, // TODO probably also want the IPv6 address ipv4_addr: Ipv4Addr, - _network: Network, + network: Network, } /// NOTE unlike `std::process::Child`, the drop implementation of this type will `kill` the diff --git a/packages/dns-test/src/fqdn.rs b/packages/dns-test/src/fqdn.rs index 5c3d8678..b0ae757c 100644 --- a/packages/dns-test/src/fqdn.rs +++ b/packages/dns-test/src/fqdn.rs @@ -33,6 +33,10 @@ impl FQDN { inner: Cow::Borrowed("com."), }; + pub const NAMESERVERS: FQDN = FQDN { + inner: Cow::Borrowed("nameservers.com."), + }; + pub fn is_root(&self) -> bool { self.inner == "." } @@ -51,6 +55,28 @@ impl FQDN { inner: Cow::Owned(owned), } } + + pub fn parent(&self) -> Option { + let (fragment, parent) = self.inner.split_once('.').unwrap(); + + if fragment.is_empty() { + None + } else { + let parent = if parent.is_empty() { + FQDN::ROOT + } else { + FQDN(parent.to_string()).unwrap() + }; + Some(parent) + } + } + + pub fn num_labels(&self) -> usize { + self.inner + .split('.') + .filter(|label| !label.is_empty()) + .count() + } } impl FromStr for FQDN { @@ -72,3 +98,37 @@ impl fmt::Display for FQDN { f.write_str(&self.inner) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parent() -> Result<()> { + let mut fqdn = FQDN("example.nameservers.com.")?; + assert_eq!(3, fqdn.num_labels()); + + let parent = fqdn.parent(); + assert_eq!( + Some("nameservers.com."), + parent.as_ref().map(|fqdn| fqdn.as_str()) + ); + fqdn = parent.unwrap(); + assert_eq!(2, fqdn.num_labels()); + + let parent = fqdn.parent(); + assert_eq!(Some(FQDN::COM), parent); + fqdn = parent.unwrap(); + assert_eq!(1, fqdn.num_labels()); + + let parent = fqdn.parent(); + assert_eq!(Some(FQDN::ROOT), parent); + fqdn = parent.unwrap(); + assert_eq!(0, fqdn.num_labels()); + + let parent = fqdn.parent(); + assert!(parent.is_none()); + + Ok(()) + } +} diff --git a/packages/dns-test/src/name_server.rs b/packages/dns-test/src/name_server.rs index eea06b21..3bef3fc3 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -5,8 +5,135 @@ use crate::container::{Child, Container, Network}; use crate::implementation::{Config, Role}; use crate::record::{self, Record, SoaSettings, DS, SOA}; use crate::tshark::Tshark; -use crate::zone_file::{self, ZoneFile}; -use crate::{Implementation, Result, DEFAULT_TTL, FQDN}; +use crate::zone_file::{self, Root, ZoneFile}; +use crate::{Implementation, Result, TrustAnchor, DEFAULT_TTL, FQDN}; + +pub struct Graph { + pub nameservers: Vec>, + pub root: Root, + pub trust_anchor: Option, +} + +/// Whether to sign the zone files +pub enum Sign<'a> { + No, + Yes, + /// Signs the zone files and then modifies the records produced by the signing process + // XXX if captures are needed use `&dyn Fn(..)` instead of a function pointer + AndAmend(&'a dyn Fn(&FQDN, &mut Vec)), +} + +impl Graph { + /// Builds up a minimal DNS graph from `leaf` up to a root name server and returns all the + /// name servers in the graph + /// + /// All new name servers will share the `Implementation` of `leaf`. + /// + /// The returned name servers are sorted from leaf zone to root zone. + /// + /// both `Sign::Yes` and `Sign::AndAmend` will add a DS record with the hash of the child's + /// key to the parent's zone file + /// + /// a non-empty `TrustAnchor` is returned only when `Sign::Yes` or `Sign::AndAmend` is used + pub fn build(leaf: NameServer, sign: Sign) -> Result { + // TODO if `leaf` is not authoritative over `nameservers.com.`, we would need two "lines" to + // root. for example, if `leaf` is authoritative over `example.net.` we would need these two + // lines: + // - `nameservers.com.`, `com.`, `.` to cover the `primaryNNN.nameservers.com.` domains that + // `NameServer` implicitly uses + // - `example.net.`, `net.`, `.` to cover the requested `leaf` name server + assert_eq!(&FQDN::NAMESERVERS, leaf.zone(), "not yet implemented"); + + // first pass: create nameservers for parent zones + let mut zone = leaf.zone().clone(); + let mut nameservers = vec![leaf]; + while let Some(parent) = zone.parent() { + let leaf = &mut nameservers[0]; + let nameserver = NameServer::new( + &leaf.implementation, + parent.clone(), + leaf.container.network(), + )?; + + leaf.add(Record::a(nameserver.fqdn().clone(), nameserver.ipv4_addr())); + nameservers.push(nameserver); + + zone = parent; + } + + // XXX will not hold when `leaf` is not authoritative over `nameservers.com.` + assert_eq!(3, nameservers.len()); + + // second pass: add referrals from parent to child + // `windows_mut` is not a thing in `core::iter` so use indexing as a workaround + for index in 0..nameservers.len() - 1 { + let [child, parent] = &mut nameservers[index..][..2] else { + unreachable!() + }; + + parent.referral( + child.zone().clone(), + child.fqdn().clone(), + child.ipv4_addr(), + ); + } + + let root = nameservers.last().unwrap(); + let root = Root::new(root.fqdn().clone(), root.ipv4_addr()); + + // start name servers + let (nameservers, trust_anchor) = match sign { + Sign::No => ( + nameservers + .into_iter() + .map(|nameserver| nameserver.start()) + .collect::>()?, + None, + ), + + _ => { + let mut trust_anchor = TrustAnchor::empty(); + let maybe_mutate = match sign { + Sign::No => unreachable!(), + Sign::Yes => None, + Sign::AndAmend(f) => Some(f), + }; + + let mut running = vec![]; + let mut child_ds = None; + let len = nameservers.len(); + for (index, mut nameserver) in nameservers.into_iter().enumerate() { + if let Some(ds) = child_ds.take() { + nameserver.add(ds); + } + + let mut nameserver = nameserver.sign()?; + child_ds = Some(nameserver.ds().clone()); + if let Some(mutate) = maybe_mutate { + let zone = nameserver.zone().clone(); + mutate(&zone, &mut nameserver.signed_zone_file_mut().records); + } + + if index == len - 1 { + // the last nameserver covers `.` + trust_anchor.add(nameserver.key_signing_key().clone()); + trust_anchor.add(nameserver.zone_signing_key().clone()); + } + + running.push(nameserver.start()?); + } + + (running, Some(trust_anchor)) + } + }; + + Ok(Graph { + nameservers, + root, + trust_anchor, + }) + } +} pub struct NameServer { container: Container, From 4d31eca533c2b3d90dba7ff813ee51b7ee53aaf2 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Wed, 6 Mar 2024 20:31:45 +0100 Subject: [PATCH 099/124] use Graph to simplify tests --- .../src/resolver/dns/scenarios.rs | 67 ++++--------- .../src/resolver/dnssec/scenarios/bogus.rs | 84 +++++++---------- .../src/resolver/dnssec/scenarios/ede.rs | 93 +++++++------------ .../src/resolver/dnssec/scenarios/secure.rs | 57 +++--------- 4 files changed, 92 insertions(+), 209 deletions(-) diff --git a/packages/conformance-tests/src/resolver/dns/scenarios.rs b/packages/conformance-tests/src/resolver/dns/scenarios.rs index 121936cd..2481bbd4 100644 --- a/packages/conformance-tests/src/resolver/dns/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dns/scenarios.rs @@ -1,9 +1,8 @@ use std::net::Ipv4Addr; use dns_test::client::{Client, DigSettings}; -use dns_test::name_server::NameServer; +use dns_test::name_server::{Graph, NameServer, Sign}; use dns_test::record::{Record, RecordType}; -use dns_test::zone_file::Root; use dns_test::{Network, Resolver, Result, FQDN}; #[test] @@ -13,37 +12,17 @@ fn can_resolve() -> Result<()> { let network = Network::new()?; let peer = dns_test::peer(); - let mut root_ns = NameServer::new(&peer, FQDN::ROOT, &network)?; - let mut com_ns = NameServer::new(&peer, FQDN::COM, &network)?; - let mut nameservers_ns = NameServer::new(&peer, FQDN("nameservers.com.")?, &network)?; - nameservers_ns - .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) - .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())) - .add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); - let nameservers_ns = nameservers_ns.start()?; + let mut leaf_ns = NameServer::new(&peer, FQDN::NAMESERVERS, &network)?; + leaf_ns.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); - eprintln!("nameservers.com.zone:\n{}", nameservers_ns.zone_file()); + let Graph { + nameservers: _nameservers, + root, + .. + } = Graph::build(leaf_ns, Sign::No)?; - 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 resolver = Resolver::new( - &network, - Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr()), - ) - .start(&dns_test::subject())?; + let resolver = Resolver::new(&network, root).start(&dns_test::subject())?; let resolver_ip_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; @@ -69,30 +48,16 @@ fn nxdomain() -> Result<()> { let network = Network::new()?; let peer = dns_test::peer(); - let mut root_ns = NameServer::new(&peer, FQDN::ROOT, &network)?; - let mut com_ns = NameServer::new(&peer, FQDN::COM, &network)?; - let mut nameservers_ns = NameServer::new(&peer, FQDN("nameservers.com.")?, &network)?; - nameservers_ns - .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) - .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())); - let nameservers_ns = nameservers_ns.start()?; + let leaf_ns = NameServer::new(&peer, FQDN::NAMESERVERS, &network)?; - com_ns.referral( - nameservers_ns.zone().clone(), - nameservers_ns.fqdn().clone(), - nameservers_ns.ipv4_addr(), - ); - let com_ns = com_ns.start()?; + let Graph { + nameservers: _nameservers, + root, + .. + } = Graph::build(leaf_ns, Sign::No)?; - root_ns.referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr()); - let root_ns = root_ns.start()?; - - let resolver = Resolver::new( - &network, - Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr()), - ) - .start(&dns_test::subject())?; + let resolver = Resolver::new(&network, root).start(&dns_test::subject())?; let resolver_ip_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs index 5f51d7dc..3e33ed90 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs @@ -2,9 +2,8 @@ use std::net::Ipv4Addr; use base64::prelude::*; use dns_test::client::{Client, DigSettings}; -use dns_test::name_server::NameServer; +use dns_test::name_server::{Graph, NameServer, Sign}; use dns_test::record::{Record, RecordType}; -use dns_test::zone_file::Root; use dns_test::{Network, Resolver, Result, FQDN}; #[ignore] @@ -15,62 +14,41 @@ fn bad_signature_in_leaf_nameserver() -> Result<()> { let network = Network::new()?; let peer = dns_test::peer(); - let mut root_ns = NameServer::new(&peer, FQDN::ROOT, &network)?; - let mut com_ns = NameServer::new(&peer, FQDN::COM, &network)?; - let mut nameservers_ns = NameServer::new(&peer, FQDN("nameservers.com.")?, &network)?; - nameservers_ns - .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) - .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())) - .add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); - let mut nameservers_ns = nameservers_ns.sign()?; + let mut leaf_ns = NameServer::new(&peer, FQDN::NAMESERVERS, &network)?; + leaf_ns.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); - // fault injection: change the signature field of the RRSIG that covers the A record we'll query - let mut modified = 0; - for record in &mut nameservers_ns.signed_zone_file_mut().records { - if let Record::RRSIG(rrsig) = record { - if rrsig.fqdn == needle_fqdn { - let mut signature = BASE64_STANDARD.decode(&rrsig.signature)?; - let last = signature.last_mut().expect("empty signature"); - *last = !*last; + let Graph { + nameservers: _nameservers, + root, + trust_anchor, + } = Graph::build( + leaf_ns, + Sign::AndAmend(&|zone, records| { + if zone == &FQDN::NAMESERVERS { + let mut modified = 0; + for record in records { + if let Record::RRSIG(rrsig) = record { + if rrsig.fqdn == needle_fqdn { + let mut signature = BASE64_STANDARD.decode(&rrsig.signature).unwrap(); + let last = signature.last_mut().expect("empty signature"); + *last = !*last; - rrsig.signature = BASE64_STANDARD.encode(&signature); - modified += 1; + rrsig.signature = BASE64_STANDARD.encode(&signature); + modified += 1; + } + } + } + + assert_eq!(modified, 1, "sanity check"); } - } - } - assert_eq!(modified, 1, "sanity check"); + }), + )?; - let nameservers_ds = nameservers_ns.ds().clone(); - let nameservers_ns = nameservers_ns.start()?; - - com_ns - .referral( - nameservers_ns.zone().clone(), - nameservers_ns.fqdn().clone(), - nameservers_ns.ipv4_addr(), - ) - .add(nameservers_ds); - let com_ns = com_ns.sign()?; - let com_ds = com_ns.ds().clone(); - let com_ns = com_ns.start()?; - - root_ns - .referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr()) - .add(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(); - - let root_ns = root_ns.start()?; - - let resolver = Resolver::new( - &network, - Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr()), - ) - .trust_anchor_key(root_ksk) - .trust_anchor_key(root_zsk) - .start(&dns_test::subject())?; + let trust_anchor = &trust_anchor.unwrap(); + let resolver = Resolver::new(&network, root) + .trust_anchor(trust_anchor) + .start(&dns_test::subject())?; let resolver_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs index 95d1dcda..1c30c7b3 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs @@ -1,9 +1,8 @@ use std::net::Ipv4Addr; use dns_test::client::{Client, DigSettings, ExtendedDnsError}; -use dns_test::name_server::NameServer; +use dns_test::name_server::{Graph, NameServer, Sign}; use dns_test::record::{Record, RecordType}; -use dns_test::zone_file::Root; use dns_test::{Network, Resolver, Result, FQDN}; #[ignore] @@ -16,74 +15,48 @@ fn dnskey_missing() -> Result<()> { let needle_fqdn = FQDN("example.nameservers.com.")?; let network = Network::new()?; - let peer = dns_test::peer(); - let mut root_ns = NameServer::new(&peer, FQDN::ROOT, &network)?; - let mut com_ns = NameServer::new(&peer, FQDN::COM, &network)?; + let mut leaf_ns = NameServer::new(&dns_test::peer(), FQDN::NAMESERVERS, &network)?; + leaf_ns.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); - let mut nameservers_ns = NameServer::new(&peer, FQDN("nameservers.com.")?, &network)?; - nameservers_ns - .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) - .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())) - .add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); - let mut nameservers_ns = nameservers_ns.sign()?; + let Graph { + nameservers: _nameservers, + root, + trust_anchor, + } = Graph::build( + leaf_ns, + Sign::AndAmend(&|zone, records| { + // remove the ZSK DNSKEY record + if zone == &FQDN::NAMESERVERS { + let mut remove_count = 0; + *records = records + .drain(..) + .filter(|record| { + let remove = if let Record::DNSKEY(dnskey) = record { + dnskey.is_zone_signing_key() + } else { + false + }; - // remove the ZSK DNSKEY record - let records = &mut nameservers_ns.signed_zone_file_mut().records; - let mut remove_count = 0; - *records = records - .drain(..) - .filter(|record| { - let remove = if let Record::DNSKEY(dnskey) = record { - dnskey.is_zone_signing_key() - } else { - false - }; + if remove { + remove_count += 1; + } - if remove { - remove_count += 1; + !remove + }) + .collect(); + assert_eq!(1, remove_count); } + }), + )?; - !remove - }) - .collect(); - assert_eq!(1, remove_count); - - let nameservers_ds = nameservers_ns.ds().clone(); - let nameservers_ns = nameservers_ns.start()?; - - com_ns - .referral( - nameservers_ns.zone().clone(), - nameservers_ns.fqdn().clone(), - nameservers_ns.ipv4_addr(), - ) - .add(nameservers_ds); - let com_ns = com_ns.sign()?; - let com_ds = com_ns.ds().clone(); - let com_ns = com_ns.start()?; - - root_ns - .referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr()) - .add(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(); - - let root_ns = root_ns.start()?; - - let mut resolver = Resolver::new( - &network, - Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr()), - ); + let mut resolver = Resolver::new(&network, root); if supports_ede { resolver.extended_dns_errors(); } - let resolver = resolver - .trust_anchor_key(root_ksk) - .trust_anchor_key(root_zsk) - .start(&subject)?; + let trust_anchor = &trust_anchor.unwrap(); + let resolver = resolver.trust_anchor(trust_anchor).start(&subject)?; let resolver_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs index 3ed3439b..9a0b2515 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs @@ -1,7 +1,7 @@ use std::net::Ipv4Addr; use dns_test::client::{Client, DigSettings}; -use dns_test::name_server::NameServer; +use dns_test::name_server::{Graph, NameServer, Sign}; use dns_test::record::{Record, RecordType}; use dns_test::zone_file::Root; use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; @@ -51,53 +51,20 @@ fn can_validate_with_delegation() -> Result<()> { let peer = dns_test::peer(); let network = Network::new()?; - let mut root_ns = NameServer::new(&peer, FQDN::ROOT, &network)?; - let mut com_ns = NameServer::new(&peer, FQDN::COM, &network)?; - let mut nameservers_ns = NameServer::new(&peer, FQDN("nameservers.com.")?, &network)?; - nameservers_ns - .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) - .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())) - .add(Record::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()?; + let mut leaf_ns = NameServer::new(&peer, FQDN::NAMESERVERS, &network)?; + leaf_ns.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); - eprintln!("nameservers.com.zone:\n{}", nameservers_ns.zone_file()); + let Graph { + nameservers: _nameservers, + root, + trust_anchor, + } = Graph::build(leaf_ns, Sign::Yes)?; - com_ns - .referral( - nameservers_ns.zone().clone(), - nameservers_ns.fqdn().clone(), - nameservers_ns.ipv4_addr(), - ) - .add(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()) - .add(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 trust_anchor = &TrustAnchor::from_iter([root_ksk, root_zsk]); - let resolver = Resolver::new( - &network, - Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr()), - ) - .trust_anchor(trust_anchor) - .start(&dns_test::subject())?; + let trust_anchor = &trust_anchor.unwrap(); + let resolver = Resolver::new(&network, root) + .trust_anchor(trust_anchor) + .start(&dns_test::subject())?; let resolver_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; From b96aa89da9ceb2fe591f1609953e652889a14847 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 7 Mar 2024 14:15:10 +0100 Subject: [PATCH 100/124] add more EDE tests --- .../src/resolver/dnssec/scenarios/ede.rs | 137 +++++++++++++++--- packages/dns-test/src/client.rs | 6 +- 2 files changed, 122 insertions(+), 21 deletions(-) diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs index 1c30c7b3..ccae7178 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs @@ -8,25 +8,11 @@ use dns_test::{Network, Resolver, Result, FQDN}; #[ignore] #[test] fn dnskey_missing() -> Result<()> { - let subject = dns_test::subject(); - let supports_ede = subject.supports_ede(); - - let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4); - let needle_fqdn = FQDN("example.nameservers.com.")?; - - let network = Network::new()?; - let mut leaf_ns = NameServer::new(&dns_test::peer(), FQDN::NAMESERVERS, &network)?; - leaf_ns.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); - - let Graph { - nameservers: _nameservers, - root, - trust_anchor, - } = Graph::build( - leaf_ns, - Sign::AndAmend(&|zone, records| { - // remove the ZSK DNSKEY record + fixture( + ExtendedDnsError::DnskeyMissing, + |_needle_fqdn, zone, records| { if zone == &FQDN::NAMESERVERS { + // remove the DNSKEY record that contains the ZSK let mut remove_count = 0; *records = records .drain(..) @@ -44,8 +30,119 @@ fn dnskey_missing() -> Result<()> { !remove }) .collect(); - assert_eq!(1, remove_count); + assert_eq!(1, remove_count, "sanity check"); } + }, + ) +} + +#[ignore] +#[test] +fn rrsigs_missing() -> Result<()> { + fixture( + ExtendedDnsError::RrsigsMissing, + |needle_fqdn, zone, records| { + if zone == &FQDN::NAMESERVERS { + // remove the RRSIG records that covers the needle record + let mut remove_count = 0; + *records = records + .drain(..) + .filter(|record| { + let remove = if let Record::RRSIG(rrsig) = record { + rrsig.type_covered == RecordType::A && rrsig.fqdn == *needle_fqdn + } else { + false + }; + + if remove { + remove_count += 1; + } + + !remove + }) + .collect(); + assert_eq!(1, remove_count, "sanity check"); + } + }, + ) +} + +#[ignore] +#[test] +fn unsupported_dnskey_algorithm() -> Result<()> { + fixture( + ExtendedDnsError::UnsupportedDnskeyAlgorithm, + |needle_fqdn, zone, records| { + if zone == &FQDN::NAMESERVERS { + // lie about the algorithm that was used to sign the needle record + let mut modified_count = 0; + for record in records { + if let Record::RRSIG(rrsig) = record { + if rrsig.type_covered == RecordType::A && rrsig.fqdn == *needle_fqdn { + assert_ne!(1, rrsig.algorithm, "modify the value below"); + rrsig.algorithm = 1; + modified_count += 1; + } + } + } + assert_eq!(1, modified_count, "sanity check"); + } + }, + ) +} + +#[ignore] +#[test] +fn dnssec_bogus() -> Result<()> { + fixture( + ExtendedDnsError::DnssecBogus, + |needle_fqdn, zone, records| { + if zone == &FQDN::NAMESERVERS { + // corrupt the RRSIG record that covers the needle record + let mut modified_count = 0; + for record in records { + if let Record::RRSIG(rrsig) = record { + if rrsig.type_covered == RecordType::A && rrsig.fqdn == *needle_fqdn { + rrsig.signature_expiration = rrsig.signature_inception - 1; + modified_count += 1; + } + } + } + assert_eq!(1, modified_count, "sanity check"); + } + }, + ) +} + +// Sets up a minimal, DNSSEC-enabled DNS graph where the leaf zone contains a "needle" A record +// that we'll search for +// +// `amend` can be used to modify zone files *after* they have been signed. it's used to introduce +// errors in the signed zone files +// +// the query for the needle record is expected to fail with the `expected` Extended DNS Error +fn fixture( + expected: ExtendedDnsError, + amend: fn(needle_fqdn: &FQDN, zone: &FQDN, records: &mut Vec), +) -> Result<()> { + let subject = dns_test::subject(); + let supports_ede = subject.supports_ede(); + + let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4); + let needle_fqdn = FQDN("example.nameservers.com.")?; + + let network = Network::new()?; + let mut leaf_ns = NameServer::new(&dns_test::peer(), FQDN::NAMESERVERS, &network)?; + leaf_ns.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); + + let Graph { + nameservers: _nameservers, + root, + trust_anchor, + } = Graph::build( + leaf_ns, + Sign::AndAmend(&|zone, records| { + amend(&needle_fqdn, zone, records); }), )?; @@ -67,7 +164,7 @@ fn dnskey_missing() -> Result<()> { assert!(output.status.is_servfail()); if supports_ede { - assert_eq!(Some(ExtendedDnsError::DnskeyMissing), output.ede); + assert_eq!(Some(expected), output.ede); } Ok(()) diff --git a/packages/dns-test/src/client.rs b/packages/dns-test/src/client.rs index 3dd2a652..281eab82 100644 --- a/packages/dns-test/src/client.rs +++ b/packages/dns-test/src/client.rs @@ -255,8 +255,10 @@ impl FromStr for DigOutput { #[derive(Debug, PartialEq)] pub enum ExtendedDnsError { - DnssecBogus, DnskeyMissing, + DnssecBogus, + RrsigsMissing, + UnsupportedDnskeyAlgorithm, } impl FromStr for ExtendedDnsError { @@ -266,8 +268,10 @@ impl FromStr for ExtendedDnsError { let code: u16 = input.parse()?; let code = match code { + 1 => Self::UnsupportedDnskeyAlgorithm, 6 => Self::DnssecBogus, 9 => Self::DnskeyMissing, + 10 => Self::RrsigsMissing, _ => todo!("EDE {code} has not yet been implemented"), }; From 05ffecec45d8098fd29e8ac1d1bf4f47abc1f4e6 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 11 Mar 2024 14:15:44 +0100 Subject: [PATCH 101/124] add a few NameServer role DNSSEC tests --- packages/conformance-tests/src/lib.rs | 1 + packages/conformance-tests/src/name_server.rs | 1 + .../src/name_server/rfc4035.rs | 1 + .../src/name_server/rfc4035/section_3.rs | 1 + .../rfc4035/section_3/section_3_1.rs | 1 + .../section_3/section_3_1/section_3_1_1.rs | 61 +++++++++++++++++++ 6 files changed, 66 insertions(+) create mode 100644 packages/conformance-tests/src/name_server.rs create mode 100644 packages/conformance-tests/src/name_server/rfc4035.rs create mode 100644 packages/conformance-tests/src/name_server/rfc4035/section_3.rs create mode 100644 packages/conformance-tests/src/name_server/rfc4035/section_3/section_3_1.rs create mode 100644 packages/conformance-tests/src/name_server/rfc4035/section_3/section_3_1/section_3_1_1.rs diff --git a/packages/conformance-tests/src/lib.rs b/packages/conformance-tests/src/lib.rs index dd939657..a92b491a 100644 --- a/packages/conformance-tests/src/lib.rs +++ b/packages/conformance-tests/src/lib.rs @@ -1,3 +1,4 @@ #![cfg(test)] +mod name_server; mod resolver; diff --git a/packages/conformance-tests/src/name_server.rs b/packages/conformance-tests/src/name_server.rs new file mode 100644 index 00000000..c7c01a74 --- /dev/null +++ b/packages/conformance-tests/src/name_server.rs @@ -0,0 +1 @@ +mod rfc4035; diff --git a/packages/conformance-tests/src/name_server/rfc4035.rs b/packages/conformance-tests/src/name_server/rfc4035.rs new file mode 100644 index 00000000..10712e9e --- /dev/null +++ b/packages/conformance-tests/src/name_server/rfc4035.rs @@ -0,0 +1 @@ +mod section_3; diff --git a/packages/conformance-tests/src/name_server/rfc4035/section_3.rs b/packages/conformance-tests/src/name_server/rfc4035/section_3.rs new file mode 100644 index 00000000..137bed61 --- /dev/null +++ b/packages/conformance-tests/src/name_server/rfc4035/section_3.rs @@ -0,0 +1 @@ +mod section_3_1; diff --git a/packages/conformance-tests/src/name_server/rfc4035/section_3/section_3_1.rs b/packages/conformance-tests/src/name_server/rfc4035/section_3/section_3_1.rs new file mode 100644 index 00000000..5656f46d --- /dev/null +++ b/packages/conformance-tests/src/name_server/rfc4035/section_3/section_3_1.rs @@ -0,0 +1 @@ +mod section_3_1_1; diff --git a/packages/conformance-tests/src/name_server/rfc4035/section_3/section_3_1/section_3_1_1.rs b/packages/conformance-tests/src/name_server/rfc4035/section_3/section_3_1/section_3_1_1.rs new file mode 100644 index 00000000..8477b76a --- /dev/null +++ b/packages/conformance-tests/src/name_server/rfc4035/section_3/section_3_1/section_3_1_1.rs @@ -0,0 +1,61 @@ +use dns_test::client::{Client, DigSettings}; +use dns_test::name_server::NameServer; +use dns_test::record::{Record, RecordType}; +use dns_test::{Network, Result, FQDN}; + +#[test] +fn rrsig_in_answer_section() -> Result<()> { + let network = Network::new()?; + + let ns = NameServer::new(&dns_test::subject(), FQDN::ROOT, &network)? + .sign()? + .start()?; + + let client = Client::new(&network)?; + let ns_fqdn = ns.fqdn(); + let ans = client.dig( + *DigSettings::default().dnssec(), + ns.ipv4_addr(), + RecordType::A, + ns_fqdn, + )?; + + assert!(ans.status.is_noerror()); + let [a, rrsig] = ans.answer.try_into().unwrap(); + + assert!(matches!(a, Record::A(..))); + let rrsig = rrsig.try_into_rrsig().unwrap(); + assert_eq!(RecordType::A, rrsig.type_covered); + assert_eq!(ns_fqdn, &rrsig.fqdn); + + Ok(()) +} + +#[test] +fn rrsig_in_authority_section() -> Result<()> { + let network = Network::new()?; + + let ns = NameServer::new(&dns_test::subject(), FQDN::ROOT, &network)? + .sign()? + .start()?; + + let client = Client::new(&network)?; + let ans = client.dig( + *DigSettings::default().dnssec(), + ns.ipv4_addr(), + RecordType::SOA, + &FQDN::ROOT, + )?; + + assert!(ans.status.is_noerror()); + let [ns, rrsig] = ans.authority.try_into().unwrap(); + + assert!(matches!(ns, Record::NS(..))); + let rrsig = rrsig.try_into_rrsig().unwrap(); + assert_eq!(RecordType::NS, rrsig.type_covered); + assert_eq!(FQDN::ROOT, rrsig.fqdn); + + Ok(()) +} + +// TODO Additional section From ce222b3de17d40b702f4b19a075241670b189164 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 11 Mar 2024 15:50:00 +0100 Subject: [PATCH 102/124] support Hickory in NameServer role note that because Hickory does not support pre-signed zone files all the DNSSEC tests fail with it --- .../section_3/section_3_1/section_3_1_1.rs | 3 +++ .../dns-test/src/docker/hickory.Dockerfile | 2 ++ packages/dns-test/src/implementation.rs | 23 ++++++------------- packages/dns-test/src/name_server.rs | 8 ------- .../templates/hickory.name-server.toml.jinja | 4 ++++ 5 files changed, 16 insertions(+), 24 deletions(-) create mode 100644 packages/dns-test/src/templates/hickory.name-server.toml.jinja diff --git a/packages/conformance-tests/src/name_server/rfc4035/section_3/section_3_1/section_3_1_1.rs b/packages/conformance-tests/src/name_server/rfc4035/section_3/section_3_1/section_3_1_1.rs index 8477b76a..20a1a9e3 100644 --- a/packages/conformance-tests/src/name_server/rfc4035/section_3/section_3_1/section_3_1_1.rs +++ b/packages/conformance-tests/src/name_server/rfc4035/section_3/section_3_1/section_3_1_1.rs @@ -4,6 +4,7 @@ use dns_test::record::{Record, RecordType}; use dns_test::{Network, Result, FQDN}; #[test] +#[ignore] fn rrsig_in_answer_section() -> Result<()> { let network = Network::new()?; @@ -32,6 +33,7 @@ fn rrsig_in_answer_section() -> Result<()> { } #[test] +#[ignore] fn rrsig_in_authority_section() -> Result<()> { let network = Network::new()?; @@ -59,3 +61,4 @@ fn rrsig_in_authority_section() -> Result<()> { } // TODO Additional section +// TODO TC bit diff --git a/packages/dns-test/src/docker/hickory.Dockerfile b/packages/dns-test/src/docker/hickory.Dockerfile index 77206244..18cd7555 100644 --- a/packages/dns-test/src/docker/hickory.Dockerfile +++ b/packages/dns-test/src/docker/hickory.Dockerfile @@ -1,7 +1,9 @@ FROM rust:1-slim-bookworm +# ldns-utils = ldns-{key2ds,keygen,signzone} RUN apt-get update && \ apt-get install -y \ + ldnsutils \ tshark # `dns-test` will invoke `docker build` from a temporary directory that contains diff --git a/packages/dns-test/src/implementation.rs b/packages/dns-test/src/implementation.rs index 1ec81e15..b22e0ae1 100644 --- a/packages/dns-test/src/implementation.rs +++ b/packages/dns-test/src/implementation.rs @@ -34,13 +34,6 @@ pub enum Role { Resolver, } -impl Role { - #[must_use] - pub fn is_resolver(&self) -> bool { - matches!(self, Self::Resolver) - } -} - #[derive(Clone)] pub enum Implementation { Bind, @@ -112,7 +105,12 @@ impl Implementation { ) } - Self::Hickory(_) => unimplemented!(), + Self::Hickory(_) => { + minijinja::render!( + include_str!("templates/hickory.name-server.toml.jinja"), + fqdn => origin.as_str() + ) + } }, } } @@ -134,14 +132,7 @@ impl Implementation { match self { Implementation::Bind => &["named", "-g", "-d5"], - Implementation::Hickory(_) => { - assert!( - role.is_resolver(), - "hickory acting in `NameServer` role is currently not supported" - ); - - &["hickory-dns", "-d"] - } + Implementation::Hickory(_) => &["hickory-dns", "-d"], Implementation::Unbound => match role { Role::NameServer => &["nsd", "-d"], diff --git a/packages/dns-test/src/name_server.rs b/packages/dns-test/src/name_server.rs index 3bef3fc3..37b1f160 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -156,14 +156,6 @@ impl NameServer { /// - one NS record, with this name server's FQDN set as the only available name server for /// the zone pub fn new(implementation: &Implementation, zone: FQDN, network: &Network) -> Result { - assert!( - matches!( - implementation, - Implementation::Unbound | Implementation::Bind - ), - "currently only `unbound` (`nsd`) and BIND can be used as a `NameServer`" - ); - let ns_count = ns_count(); let nameserver = primary_ns(ns_count); let image = implementation.clone().into(); diff --git a/packages/dns-test/src/templates/hickory.name-server.toml.jinja b/packages/dns-test/src/templates/hickory.name-server.toml.jinja new file mode 100644 index 00000000..1d784c86 --- /dev/null +++ b/packages/dns-test/src/templates/hickory.name-server.toml.jinja @@ -0,0 +1,4 @@ +[[zones]] +zone = "{{ fqdn }}" +zone_type = "Primary" +file = "/etc/zones/main.zone" From 74e479706138ae9fd66c57e3cd9cd17ca5ec82f4 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 11 Mar 2024 16:04:35 +0100 Subject: [PATCH 103/124] add a DNSSEC-disabled NameServer scenario test to ensure that hickory-dns is correctly configured to work in NameServer mode --- packages/conformance-tests/src/name_server.rs | 1 + .../src/name_server/scenarios.rs | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 packages/conformance-tests/src/name_server/scenarios.rs diff --git a/packages/conformance-tests/src/name_server.rs b/packages/conformance-tests/src/name_server.rs index c7c01a74..a2e4ef2b 100644 --- a/packages/conformance-tests/src/name_server.rs +++ b/packages/conformance-tests/src/name_server.rs @@ -1 +1,2 @@ mod rfc4035; +mod scenarios; diff --git a/packages/conformance-tests/src/name_server/scenarios.rs b/packages/conformance-tests/src/name_server/scenarios.rs new file mode 100644 index 00000000..17578ad7 --- /dev/null +++ b/packages/conformance-tests/src/name_server/scenarios.rs @@ -0,0 +1,23 @@ +use dns_test::client::{Client, DigSettings}; +use dns_test::name_server::NameServer; +use dns_test::record::RecordType; +use dns_test::{Network, Result, FQDN}; + +#[test] +fn authoritative_answer() -> Result<()> { + let network = &Network::new()?; + let ns = NameServer::new(&dns_test::subject(), FQDN::ROOT, network)?.start()?; + + let client = Client::new(network)?; + let ans = client.dig( + DigSettings::default(), + ns.ipv4_addr(), + RecordType::SOA, + &FQDN::ROOT, + )?; + + assert!(ans.status.is_noerror()); + assert!(ans.flags.authoritative_answer); + + Ok(()) +} From 4f277c1dbb702be0134246689b2909ec75090ced Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 11 Mar 2024 16:41:59 +0100 Subject: [PATCH 104/124] implement terminate for Hickory both in the NameServer and Resolver roles --- packages/dns-test/src/implementation.rs | 9 ++++-- packages/dns-test/src/name_server.rs | 37 ++++++++++++++++++++++++- packages/dns-test/src/resolver.rs | 33 ++++++++++++++++++++-- 3 files changed, 73 insertions(+), 6 deletions(-) diff --git a/packages/dns-test/src/implementation.rs b/packages/dns-test/src/implementation.rs index b22e0ae1..9a3dadac 100644 --- a/packages/dns-test/src/implementation.rs +++ b/packages/dns-test/src/implementation.rs @@ -132,7 +132,12 @@ impl Implementation { match self { Implementation::Bind => &["named", "-g", "-d5"], - Implementation::Hickory(_) => &["hickory-dns", "-d"], + Implementation::Hickory(_) => &[ + "sh", + "-c", + "echo $$ > /tmp/hickory.pid +exec hickory-dns -d", + ], Implementation::Unbound => match role { Role::NameServer => &["nsd", "-d"], @@ -146,7 +151,7 @@ impl Implementation { match self { Implementation::Bind => "/tmp/named.pid", - Implementation::Hickory(_) => unimplemented!(), + Implementation::Hickory(_) => "/tmp/hickory.pid", Implementation::Unbound => match role { Role::NameServer => "/tmp/nsd.pid", diff --git a/packages/dns-test/src/name_server.rs b/packages/dns-test/src/name_server.rs index 37b1f160..32488f40 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -371,7 +371,10 @@ kill -TERM $(cat {pidfile})" self.container.status_ok(&["sh", "-c", &kill])?; let output = self.state.child.wait()?; - if !output.status.success() { + // the hickory-dns binary does not do signal handling so it won't shut down gracefully; we + // will still get some logs so we'll ignore the fact that it fails to shut down ... + let is_hickory = matches!(self.implementation, Implementation::Hickory(_)); + if !is_hickory && !output.status.success() { return Err( format!("could not terminate the `{}` process", self.implementation).into(), ); @@ -431,8 +434,12 @@ fn admin_ns(ns_count: usize) -> FQDN { #[cfg(test)] mod tests { + use std::thread; + use std::time::Duration; + use crate::client::{Client, DigSettings}; use crate::record::RecordType; + use crate::Repository; use super::*; @@ -532,4 +539,32 @@ mod tests { Ok(()) } + + #[test] + fn terminate_hickory_works() -> Result<()> { + let network = Network::new()?; + let ns = NameServer::new( + &Implementation::Hickory(Repository("https://github.com/hickory-dns/hickory-dns")), + FQDN::ROOT, + &network, + )? + .start()?; + + // hickory-dns does not do signal handling so we need to wait until it prints something to + // the console + thread::sleep(Duration::from_millis(500)); + + let logs = ns.terminate()?; + + eprintln!("{logs}"); + let mut found = false; + for line in logs.lines() { + if line.contains("Hickory DNS") && line.contains("starting") { + found = true; + } + } + assert!(found); + + Ok(()) + } } diff --git a/packages/dns-test/src/resolver.rs b/packages/dns-test/src/resolver.rs index 4a5ddf43..57644291 100644 --- a/packages/dns-test/src/resolver.rs +++ b/packages/dns-test/src/resolver.rs @@ -48,8 +48,13 @@ 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()); + // the hickory-dns binary does not do signal handling so it won't shut down gracefully; we + // will still get some logs so we'll ignore the fact that it fails to shut down ... + let is_hickory = matches!(self.implementation, Implementation::Hickory(_)); + if !is_hickory && !output.status.success() { + return Err( + format!("could not terminate the `{}` process", self.implementation).into(), + ); } assert!( @@ -148,7 +153,7 @@ impl ResolverSettings { #[cfg(test)] mod tests { - use crate::{name_server::NameServer, FQDN}; + use crate::{name_server::NameServer, Repository, FQDN}; use super::*; @@ -179,4 +184,26 @@ mod tests { Ok(()) } + + #[test] + fn terminate_hickory_works() -> Result<()> { + let network = Network::new()?; + let ns = NameServer::new(&Implementation::Unbound, FQDN::ROOT, &network)?.start()?; + let resolver = Resolver::new(&network, Root::new(ns.fqdn().clone(), ns.ipv4_addr())) + .start(&Implementation::Hickory(Repository( + "https://github.com/hickory-dns/hickory-dns", + )))?; + let logs = resolver.terminate()?; + + eprintln!("{logs}"); + let mut found = false; + for line in logs.lines() { + if line.contains("Hickory DNS") && line.contains("starting") { + found = true; + } + } + assert!(found); + + Ok(()) + } } From 2e4642192715b1bc6ba4d05b3a2376f81c1c4948 Mon Sep 17 00:00:00 2001 From: Sebastian Ziebell Date: Tue, 14 May 2024 14:17:44 +0200 Subject: [PATCH 105/124] Check hickory-dns is fully started When starting `hickory-dns` there is no easy way to check the start sequence has finished & its fully ready to accept connections. Other tools, e.g. unbound, are designed as services, they will correctly manage their `pidfile`. They also could be queried by the `servicectl` inside the Docker container. --- packages/dns-test/src/implementation.rs | 5 +++ packages/dns-test/src/resolver.rs | 48 +++++++++++++++---------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/packages/dns-test/src/implementation.rs b/packages/dns-test/src/implementation.rs index 9a3dadac..08465d45 100644 --- a/packages/dns-test/src/implementation.rs +++ b/packages/dns-test/src/implementation.rs @@ -55,6 +55,11 @@ impl Implementation { matches!(self, Self::Bind) } + #[must_use] + pub fn is_hickory(&self) -> bool { + matches!(self, Self::Hickory(_)) + } + pub(crate) fn format_config(&self, config: Config) -> String { match config { Config::Resolver { diff --git a/packages/dns-test/src/resolver.rs b/packages/dns-test/src/resolver.rs index 57644291..678c7b94 100644 --- a/packages/dns-test/src/resolver.rs +++ b/packages/dns-test/src/resolver.rs @@ -1,4 +1,5 @@ use core::fmt::Write; +use std::io::{BufRead, BufReader}; use std::net::Ipv4Addr; use crate::container::{Child, Container, Network}; @@ -38,23 +39,26 @@ impl Resolver { self.container.ipv4_addr() } - /// gracefully terminates the name server collecting all logs + /// Gracefully terminates the name server collecting all logs pub fn terminate(self) -> Result { - let pidfile = self.implementation.pidfile(Role::Resolver); + let Resolver { + implementation, + container, + child, + } = self; + + let pidfile = implementation.pidfile(Role::Resolver); let kill = format!( "test -f {pidfile} || sleep 1 kill -TERM $(cat {pidfile})" ); - self.container.status_ok(&["sh", "-c", &kill])?; - let output = self.child.wait()?; + container.status_ok(&["sh", "-c", &kill])?; + let output = child.wait()?; // the hickory-dns binary does not do signal handling so it won't shut down gracefully; we // will still get some logs so we'll ignore the fact that it fails to shut down ... - let is_hickory = matches!(self.implementation, Implementation::Hickory(_)); - if !is_hickory && !output.status.success() { - return Err( - format!("could not terminate the `{}` process", self.implementation).into(), - ); + if !implementation.is_hickory() && !output.status.success() { + return Err(format!("could not terminate the `{}` process", implementation).into()); } assert!( @@ -115,7 +119,21 @@ impl ResolverSettings { container.cp(path, &contents)?; } - let child = container.spawn(implementation.cmd_args(config.role()))?; + let mut child = container.spawn(implementation.cmd_args(config.role()))?; + + // For HickoryDNS we need to wait until its start sequence finished. Only then the server is able + // to accept connections. The start sequence logs are consumed here. + if implementation.is_hickory() { + let stdout = child.stdout()?; + let lines = BufReader::new(stdout).lines(); + + for line in lines { + let line = line?; + if line.contains("Server starting up") { + break; + } + } + } Ok(Resolver { child, @@ -195,14 +213,8 @@ mod tests { )))?; let logs = resolver.terminate()?; - eprintln!("{logs}"); - let mut found = false; - for line in logs.lines() { - if line.contains("Hickory DNS") && line.contains("starting") { - found = true; - } - } - assert!(found); + // Hickory-DNS start sequence log has been consumed in `ResolverSettings.start`. + assert!(logs.is_empty()); Ok(()) } From 58239028f4c94495f11b7427abec733059dc7960 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 11 Mar 2024 17:22:28 +0100 Subject: [PATCH 106/124] turn dns_test::{subject,peer} into immutable statics using `std::env::set_var` to set or change the value of either DNS_TEST_SUBJECT or DNS_TEST_PEER is A Bad Idea, specially so when tests are running in parallel we can't forbid the use of `env::set_var` _but_ at least we can ensure that even in its presence the return value of `dns_test::{subject,peer}` will not change this is accomplished using a "lazy" static variable that gets initialized at most once during the lifetime of the process instead of reading the env var each time `{subject,peer}` is called to better convey the fact that the return value of `{subject,peer}` won't change, we present them as static variables instead --- Cargo.lock | 7 ++ .../section_3/section_3_1/section_3_1_1.rs | 4 +- .../src/name_server/scenarios.rs | 2 +- .../src/resolver/dns/scenarios.rs | 10 ++- .../dnssec/rfc4035/section_4/section_4_1.rs | 4 +- .../src/resolver/dnssec/scenarios/bogus.rs | 5 +- .../src/resolver/dnssec/scenarios/ede.rs | 6 +- .../src/resolver/dnssec/scenarios/secure.rs | 9 ++- packages/dns-test/Cargo.toml | 1 + packages/dns-test/examples/explore.rs | 10 +-- packages/dns-test/src/implementation.rs | 4 +- packages/dns-test/src/lib.rs | 65 +++++++++++++++++-- 12 files changed, 92 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 35f08284..5927e0b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -155,6 +155,7 @@ name = "dns-test" version = "0.1.0" dependencies = [ "ctrlc", + "lazy_static", "minijinja", "pretty_assertions", "serde", @@ -295,6 +296,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.153" diff --git a/packages/conformance-tests/src/name_server/rfc4035/section_3/section_3_1/section_3_1_1.rs b/packages/conformance-tests/src/name_server/rfc4035/section_3/section_3_1/section_3_1_1.rs index 20a1a9e3..02a9d6ac 100644 --- a/packages/conformance-tests/src/name_server/rfc4035/section_3/section_3_1/section_3_1_1.rs +++ b/packages/conformance-tests/src/name_server/rfc4035/section_3/section_3_1/section_3_1_1.rs @@ -8,7 +8,7 @@ use dns_test::{Network, Result, FQDN}; fn rrsig_in_answer_section() -> Result<()> { let network = Network::new()?; - let ns = NameServer::new(&dns_test::subject(), FQDN::ROOT, &network)? + let ns = NameServer::new(&dns_test::SUBJECT, FQDN::ROOT, &network)? .sign()? .start()?; @@ -37,7 +37,7 @@ fn rrsig_in_answer_section() -> Result<()> { fn rrsig_in_authority_section() -> Result<()> { let network = Network::new()?; - let ns = NameServer::new(&dns_test::subject(), FQDN::ROOT, &network)? + let ns = NameServer::new(&dns_test::SUBJECT, FQDN::ROOT, &network)? .sign()? .start()?; diff --git a/packages/conformance-tests/src/name_server/scenarios.rs b/packages/conformance-tests/src/name_server/scenarios.rs index 17578ad7..ee26a788 100644 --- a/packages/conformance-tests/src/name_server/scenarios.rs +++ b/packages/conformance-tests/src/name_server/scenarios.rs @@ -6,7 +6,7 @@ use dns_test::{Network, Result, FQDN}; #[test] fn authoritative_answer() -> Result<()> { let network = &Network::new()?; - let ns = NameServer::new(&dns_test::subject(), FQDN::ROOT, network)?.start()?; + let ns = NameServer::new(&dns_test::SUBJECT, FQDN::ROOT, network)?.start()?; let client = Client::new(network)?; let ans = client.dig( diff --git a/packages/conformance-tests/src/resolver/dns/scenarios.rs b/packages/conformance-tests/src/resolver/dns/scenarios.rs index 2481bbd4..d3d7fc5c 100644 --- a/packages/conformance-tests/src/resolver/dns/scenarios.rs +++ b/packages/conformance-tests/src/resolver/dns/scenarios.rs @@ -11,9 +11,8 @@ fn can_resolve() -> Result<()> { let needle_fqdn = FQDN("example.nameservers.com.")?; let network = Network::new()?; - let peer = dns_test::peer(); - let mut leaf_ns = NameServer::new(&peer, FQDN::NAMESERVERS, &network)?; + let mut leaf_ns = NameServer::new(&dns_test::PEER, FQDN::NAMESERVERS, &network)?; leaf_ns.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); let Graph { @@ -22,7 +21,7 @@ fn can_resolve() -> Result<()> { .. } = Graph::build(leaf_ns, Sign::No)?; - let resolver = Resolver::new(&network, root).start(&dns_test::subject())?; + let resolver = Resolver::new(&network, root).start(&dns_test::SUBJECT)?; let resolver_ip_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; @@ -47,9 +46,8 @@ fn nxdomain() -> Result<()> { let needle_fqdn = FQDN("unicorn.nameservers.com.")?; let network = Network::new()?; - let peer = dns_test::peer(); - let leaf_ns = NameServer::new(&peer, FQDN::NAMESERVERS, &network)?; + let leaf_ns = NameServer::new(&dns_test::PEER, FQDN::NAMESERVERS, &network)?; let Graph { nameservers: _nameservers, @@ -57,7 +55,7 @@ fn nxdomain() -> Result<()> { .. } = Graph::build(leaf_ns, Sign::No)?; - let resolver = Resolver::new(&network, root).start(&dns_test::subject())?; + let resolver = Resolver::new(&network, root).start(&dns_test::SUBJECT)?; let resolver_ip_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs index 6bfbacf2..61244953 100644 --- a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs @@ -9,9 +9,9 @@ use dns_test::{Network, Resolver, Result, FQDN}; #[ignore] fn edns_support() -> Result<()> { let network = &Network::new()?; - let ns = NameServer::new(&dns_test::peer(), FQDN::ROOT, network)?.start()?; + let ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)?.start()?; let resolver = Resolver::new(network, Root::new(ns.fqdn().clone(), ns.ipv4_addr())) - .start(&dns_test::subject())?; + .start(&dns_test::SUBJECT)?; let mut tshark = resolver.eavesdrop()?; diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs index 3e33ed90..b41db3a3 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs @@ -13,9 +13,8 @@ fn bad_signature_in_leaf_nameserver() -> Result<()> { let needle_fqdn = FQDN("example.nameservers.com.")?; let network = Network::new()?; - let peer = dns_test::peer(); - let mut leaf_ns = NameServer::new(&peer, FQDN::NAMESERVERS, &network)?; + let mut leaf_ns = NameServer::new(&dns_test::PEER, FQDN::NAMESERVERS, &network)?; leaf_ns.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); let Graph { @@ -48,7 +47,7 @@ fn bad_signature_in_leaf_nameserver() -> Result<()> { let trust_anchor = &trust_anchor.unwrap(); let resolver = Resolver::new(&network, root) .trust_anchor(trust_anchor) - .start(&dns_test::subject())?; + .start(&dns_test::SUBJECT)?; let resolver_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs index ccae7178..8f9cdc2d 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/ede.rs @@ -125,14 +125,14 @@ fn fixture( expected: ExtendedDnsError, amend: fn(needle_fqdn: &FQDN, zone: &FQDN, records: &mut Vec), ) -> Result<()> { - let subject = dns_test::subject(); + let subject = &dns_test::SUBJECT; let supports_ede = subject.supports_ede(); let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4); let needle_fqdn = FQDN("example.nameservers.com.")?; let network = Network::new()?; - let mut leaf_ns = NameServer::new(&dns_test::peer(), FQDN::NAMESERVERS, &network)?; + let mut leaf_ns = NameServer::new(&dns_test::PEER, FQDN::NAMESERVERS, &network)?; leaf_ns.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); let Graph { @@ -153,7 +153,7 @@ fn fixture( } let trust_anchor = &trust_anchor.unwrap(); - let resolver = resolver.trust_anchor(trust_anchor).start(&subject)?; + let resolver = resolver.trust_anchor(trust_anchor).start(subject)?; let resolver_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs index 9a0b2515..bf076c0b 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs @@ -11,7 +11,7 @@ use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; #[test] fn can_validate_without_delegation() -> Result<()> { let network = Network::new()?; - let mut ns = NameServer::new(&dns_test::peer(), FQDN::ROOT, &network)?; + let mut ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, &network)?; ns.add(Record::a(ns.fqdn().clone(), ns.ipv4_addr())); let ns = ns.sign()?; @@ -27,7 +27,7 @@ fn can_validate_without_delegation() -> Result<()> { let trust_anchor = &TrustAnchor::from_iter([root_ksk.clone(), root_zsk.clone()]); let resolver = Resolver::new(&network, Root::new(ns.fqdn().clone(), ns.ipv4_addr())) .trust_anchor(trust_anchor) - .start(&dns_test::subject())?; + .start(&dns_test::SUBJECT)?; let resolver_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; @@ -49,10 +49,9 @@ fn can_validate_with_delegation() -> Result<()> { let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4); let needle_fqdn = FQDN("example.nameservers.com.")?; - let peer = dns_test::peer(); let network = Network::new()?; - let mut leaf_ns = NameServer::new(&peer, FQDN::NAMESERVERS, &network)?; + let mut leaf_ns = NameServer::new(&dns_test::PEER, FQDN::NAMESERVERS, &network)?; leaf_ns.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); let Graph { @@ -64,7 +63,7 @@ fn can_validate_with_delegation() -> Result<()> { let trust_anchor = &trust_anchor.unwrap(); let resolver = Resolver::new(&network, root) .trust_anchor(trust_anchor) - .start(&dns_test::subject())?; + .start(&dns_test::SUBJECT)?; let resolver_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; diff --git a/packages/dns-test/Cargo.toml b/packages/dns-test/Cargo.toml index eacbbf9c..2d595812 100644 --- a/packages/dns-test/Cargo.toml +++ b/packages/dns-test/Cargo.toml @@ -6,6 +6,7 @@ publish = false version = "0.1.0" [dependencies] +lazy_static = "1.4.0" minijinja = "1.0.12" serde = { version = "1.0.196", features = ["derive"] } serde_json = "1.0.113" diff --git a/packages/dns-test/examples/explore.rs b/packages/dns-test/examples/explore.rs index 6e8de222..277e16b9 100644 --- a/packages/dns-test/examples/explore.rs +++ b/packages/dns-test/examples/explore.rs @@ -8,16 +8,16 @@ use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; fn main() -> Result<()> { let network = Network::new()?; - let peer = dns_test::peer(); + let peer = &dns_test::PEER; println!("building docker image..."); - let mut root_ns = NameServer::new(&peer, FQDN::ROOT, &network)?; + let mut root_ns = NameServer::new(peer, FQDN::ROOT, &network)?; println!("DONE"); println!("setting up name servers..."); - let mut com_ns = NameServer::new(&peer, FQDN::COM, &network)?; + let mut com_ns = NameServer::new(peer, FQDN::COM, &network)?; - let mut nameservers_ns = NameServer::new(&peer, FQDN("nameservers.com.")?, &network)?; + let mut nameservers_ns = NameServer::new(peer, FQDN("nameservers.com.")?, &network)?; nameservers_ns .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())); @@ -53,7 +53,7 @@ fn main() -> Result<()> { Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr()), ) .trust_anchor(&trust_anchor) - .start(&dns_test::subject())?; + .start(&dns_test::SUBJECT)?; println!("DONE\n\n"); let resolver_addr = resolver.ipv4_addr(); diff --git a/packages/dns-test/src/implementation.rs b/packages/dns-test/src/implementation.rs index 08465d45..9cac8786 100644 --- a/packages/dns-test/src/implementation.rs +++ b/packages/dns-test/src/implementation.rs @@ -34,7 +34,7 @@ pub enum Role { Resolver, } -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum Implementation { Bind, Hickory(Repository<'static>), @@ -178,7 +178,7 @@ impl fmt::Display for Implementation { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Repository<'a> { inner: Cow<'a, str>, } diff --git a/packages/dns-test/src/lib.rs b/packages/dns-test/src/lib.rs index cfbcb96f..afc3773d 100644 --- a/packages/dns-test/src/lib.rs +++ b/packages/dns-test/src/lib.rs @@ -1,5 +1,9 @@ //! A test framework for all things DNS +use std::env; + +use lazy_static::lazy_static; + pub use crate::container::Network; pub use crate::fqdn::FQDN; pub use crate::implementation::{Implementation, Repository}; @@ -23,8 +27,13 @@ pub type Result = core::result::Result; // TODO maybe this should be a TLS variable that each unit test (thread) can override const DEFAULT_TTL: u32 = 24 * 60 * 60; // 1 day -pub fn subject() -> Implementation { - if let Ok(subject) = std::env::var("DNS_TEST_SUBJECT") { +lazy_static! { + pub static ref SUBJECT: Implementation = parse_subject(); + pub static ref PEER: Implementation = parse_peer(); +} + +fn parse_subject() -> Implementation { + if let Ok(subject) = env::var("DNS_TEST_SUBJECT") { if subject == "unbound" { return Implementation::Unbound; } @@ -47,14 +56,58 @@ pub fn subject() -> Implementation { } } -pub fn peer() -> Implementation { - if let Ok(subject) = std::env::var("DNS_TEST_PEER") { - match subject.as_str() { +fn parse_peer() -> Implementation { + if let Ok(peer) = env::var("DNS_TEST_PEER") { + match peer.as_str() { "unbound" => Implementation::Unbound, "bind" => Implementation::Bind, - _ => panic!("`{subject}` is not supported as a test peer implementation"), + _ => panic!("`{peer}` is not supported as a test peer implementation"), } } else { Implementation::default() } } + +#[cfg(test)] +mod tests { + use std::env; + + use super::*; + + impl PartialEq for Implementation { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Hickory(_), Self::Hickory(_)) => true, + _ => core::mem::discriminant(self) == core::mem::discriminant(other), + } + } + } + + #[test] + fn immutable_subject() { + let before = super::SUBJECT.clone(); + let newval = if before == Implementation::Unbound { + "bind" + } else { + "unbound" + }; + env::set_var("DNS_TEST_SUBJECT", newval); + + let after = super::SUBJECT.clone(); + assert_eq!(before, after); + } + + #[test] + fn immutable_peer() { + let before = super::PEER.clone(); + let newval = if before == Implementation::Unbound { + "bind" + } else { + "unbound" + }; + env::set_var("DNS_TEST_PEER", newval); + + let after = super::PEER.clone(); + assert_eq!(before, after); + } +} From cd2895a1681a0ee5a956c19816163d7def812704 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 11 Mar 2024 17:45:06 +0100 Subject: [PATCH 107/124] CI: make clippy check cfg(test) code, tests & examples --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8f7942b..62f05ddc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,4 +49,4 @@ jobs: run: cargo fmt --all -- --check - name: Lint code - run: cargo clippy --workspace -- -D warnings + run: cargo clippy --workspace --all-targets -- -D warnings From 49990d2530c7609ebfec7f9f8211c59b74e21842 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 11 Mar 2024 17:45:34 +0100 Subject: [PATCH 108/124] fix previously undetected clippy warning --- packages/dns-test/src/container/network.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/dns-test/src/container/network.rs b/packages/dns-test/src/container/network.rs index 6c1751ca..ebe9c6d9 100644 --- a/packages/dns-test/src/container/network.rs +++ b/packages/dns-test/src/container/network.rs @@ -124,11 +124,7 @@ mod tests { let output = command.output().expect("Failed to get output"); let stdout = String::from_utf8_lossy(&output.stdout); - stdout - .trim() - .lines() - .find(|line| line == &network_name) - .is_some() + stdout.trim().lines().any(|line| line == network_name) } #[test] From b8408199981020561bc7cc991d9bc963a9799041 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 23 Apr 2024 18:34:43 +0200 Subject: [PATCH 109/124] hickory/resolver: drop _cache_size settings they are not required as default values exist --- packages/dns-test/src/templates/hickory.resolver.toml.jinja | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dns-test/src/templates/hickory.resolver.toml.jinja b/packages/dns-test/src/templates/hickory.resolver.toml.jinja index 418db8fd..bd4be69c 100644 --- a/packages/dns-test/src/templates/hickory.resolver.toml.jinja +++ b/packages/dns-test/src/templates/hickory.resolver.toml.jinja @@ -1,5 +1,5 @@ [[zones]] zone = "." zone_type = "Hint" -stores = { type = "recursor", roots = "/etc/root.hints", ns_cache_size = 1024, record_cache_size = 1048576 } +stores = { type = "recursor", roots = "/etc/root.hints" } enable_dnssec = {{ use_dnssec }} From 1aab8812df518978b27bec5768df764b3ccf0749 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 1 Mar 2024 19:08:28 +0100 Subject: [PATCH 110/124] `explore`: make DNSSEC opt-in --- packages/dns-test/examples/explore.rs | 113 ++++++++++++++++++++------ 1 file changed, 86 insertions(+), 27 deletions(-) diff --git a/packages/dns-test/examples/explore.rs b/packages/dns-test/examples/explore.rs index 277e16b9..0047bf7e 100644 --- a/packages/dns-test/examples/explore.rs +++ b/packages/dns-test/examples/explore.rs @@ -1,3 +1,4 @@ +use std::env; use std::sync::mpsc; use dns_test::client::Client; @@ -7,6 +8,8 @@ use dns_test::zone_file::Root; use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; fn main() -> Result<()> { + let args = Args::from_env()?; + let network = Network::new()?; let peer = &dns_test::PEER; @@ -21,32 +24,47 @@ fn main() -> Result<()> { nameservers_ns .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())); - let nameservers_ns = nameservers_ns.sign()?; - let nameservers_ds = nameservers_ns.ds().clone(); - let nameservers_ns = nameservers_ns.start()?; - com_ns - .referral( - nameservers_ns.zone().clone(), - nameservers_ns.fqdn().clone(), - nameservers_ns.ipv4_addr(), - ) - .add(nameservers_ds); - let com_ns = com_ns.sign()?; - let com_ds = com_ns.ds().clone(); - let com_ns = com_ns.start()?; + let nameservers_ns = if args.dnssec { + let nameservers_ns = nameservers_ns.sign()?; + com_ns.add(nameservers_ns.ds().clone()); + nameservers_ns.start()? + } else { + nameservers_ns.start()? + }; - root_ns - .referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr()) - .add(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(); + com_ns.referral( + nameservers_ns.zone().clone(), + nameservers_ns.fqdn().clone(), + nameservers_ns.ipv4_addr(), + ); + + let com_ns = if args.dnssec { + let com_ns = com_ns.sign()?; + root_ns.add(com_ns.ds().clone()); + com_ns.start()? + } else { + com_ns.start()? + }; + + root_ns.referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr()); + + let mut trust_anchor = TrustAnchor::empty(); + let root_ns = if args.dnssec { + let root_ns = root_ns.sign()?; + let root_ksk = root_ns.key_signing_key(); + let root_zsk = root_ns.zone_signing_key(); + + trust_anchor.add(root_ksk.clone()); + trust_anchor.add(root_zsk.clone()); + + root_ns.start()? + } else { + root_ns.start()? + }; - let root_ns = root_ns.start()?; println!("DONE"); - let trust_anchor = TrustAnchor::from_iter([root_ksk.clone(), root_zsk.clone()]); println!("building docker image..."); let resolver = Resolver::new( &network, @@ -58,8 +76,11 @@ fn main() -> Result<()> { let resolver_addr = resolver.ipv4_addr(); let client = Client::new(&network)?; - // generate `/etc/bind.keys` - client.delv(resolver_addr, RecordType::SOA, &FQDN::ROOT, &trust_anchor)?; + + if args.dnssec { + // generate `/etc/bind.keys` + client.delv(resolver_addr, RecordType::SOA, &FQDN::ROOT, &trust_anchor)?; + } let (tx, rx) = mpsc::channel(); @@ -99,10 +120,13 @@ fn main() -> Result<()> { ); println!("example queries (run these in the client container):\n"); - println!("`dig @{resolver_addr} SOA .`\n"); - println!( - "`delv -a /etc/bind.keys @{resolver_addr} SOA .` (you MUST use the `-a` flag with delv)\n\n" - ); + let adflag = if args.dnssec { "+adflag" } else { "+noadflag" }; + println!("`dig @{resolver_addr} {adflag} SOA .`\n"); + if args.dnssec { + println!( + "`delv -a /etc/bind.keys @{resolver_addr} SOA .` (you MUST use the `-a` flag with delv)\n\n" + ); + } println!( "to print the DNS traffic flowing through the resolver run this command in @@ -118,3 +142,38 @@ the resolver container before performing queries:\n" Ok(()) } + +struct Args { + dnssec: bool, +} + +impl Args { + fn from_env() -> Result { + let args: Vec<_> = env::args().skip(1).collect(); + let num_args = args.len(); + + let dnssec = if num_args == 0 { + false + } else if num_args == 1 { + if args[0] == "--dnssec" { + true + } else { + return cli_error(); + } + } else { + return cli_error(); + }; + + Ok(Self { dnssec }) + } +} + +fn cli_error() -> Result { + eprintln!( + "usage: explore [--dnssec] +Options: + --dnssec sign zone files to enable DNSSEC" + ); + + Err("CLI error".into()) +} From 5d15aa222820309894ee78ac8885a38df594dcaa Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 1 Mar 2024 19:13:58 +0100 Subject: [PATCH 111/124] `explore`: generate `bind.keys` w/o querying resolver this avoids the resolver caching any query. that way `tshark` can observe all the messages involved in DNSSEC validating a query "from scratch" --- packages/dns-test/examples/explore.rs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/dns-test/examples/explore.rs b/packages/dns-test/examples/explore.rs index 0047bf7e..bc13d63a 100644 --- a/packages/dns-test/examples/explore.rs +++ b/packages/dns-test/examples/explore.rs @@ -1,4 +1,5 @@ use std::env; +use std::net::Ipv4Addr; use std::sync::mpsc; use dns_test::client::Client; @@ -65,6 +66,19 @@ fn main() -> Result<()> { println!("DONE"); + let client = Client::new(&network)?; + if args.dnssec { + // this will send queries to the loopback address and fail because there's no resolver + // but as a side-effect it will generate the `/etc/bind.keys` file we want + // ignore the expected error + let _ = client.delv( + Ipv4Addr::new(127, 0, 0, 1), + RecordType::SOA, + &FQDN::ROOT, + &trust_anchor, + )?; + } + println!("building docker image..."); let resolver = Resolver::new( &network, @@ -74,14 +88,6 @@ fn main() -> Result<()> { .start(&dns_test::SUBJECT)?; println!("DONE\n\n"); - let resolver_addr = resolver.ipv4_addr(); - let client = Client::new(&network)?; - - if args.dnssec { - // generate `/etc/bind.keys` - client.delv(resolver_addr, RecordType::SOA, &FQDN::ROOT, &trust_anchor)?; - } - let (tx, rx) = mpsc::channel(); ctrlc::set_handler(move || tx.send(()).expect("could not forward signal"))?; @@ -107,7 +113,8 @@ fn main() -> Result<()> { nameservers_ns.container_id() ); - println!("resolver's IP address: {resolver_addr}"); + let resolver_addr = resolver.ipv4_addr(); + println!("resolver's IP address: {resolver_addr}",); println!( "attach to this container with: `docker exec -it {} bash`\n", resolver.container_id() From fef26b7139efc38b422844b25225f9ffd4e20087 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 29 Apr 2024 18:48:11 +0200 Subject: [PATCH 112/124] dns-test: add getters & make some fields public --- packages/dns-test/src/record.rs | 20 ++++++++++++++------ packages/dns-test/src/tshark.rs | 4 ++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/dns-test/src/record.rs b/packages/dns-test/src/record.rs index aaaf446a..dc9d4423 100644 --- a/packages/dns-test/src/record.rs +++ b/packages/dns-test/src/record.rs @@ -142,6 +142,14 @@ impl Record { } .into() } + + pub fn try_into_ds(self) -> CoreResult { + if let Self::DS(v) = self { + Ok(v) + } else { + Err(self) + } + } } impl FromStr for Record { @@ -327,12 +335,12 @@ impl fmt::Display for DNSKEY { #[derive(Clone, Debug)] pub struct DS { - zone: FQDN, - ttl: u32, - key_tag: u16, - algorithm: u8, - digest_type: u8, - digest: String, + pub zone: FQDN, + pub ttl: u32, + pub key_tag: u16, + pub algorithm: u8, + pub digest_type: u8, + pub digest: String, } impl FromStr for DS { diff --git a/packages/dns-test/src/tshark.rs b/packages/dns-test/src/tshark.rs index 8b850019..c78e1903 100644 --- a/packages/dns-test/src/tshark.rs +++ b/packages/dns-test/src/tshark.rs @@ -179,6 +179,10 @@ impl Message { .ok() } + pub fn as_value(&self) -> &serde_json::Value { + &self.inner + } + fn opt_record(&self) -> Option<&serde_json::Value> { for (key, value) in self.inner.get("Additional records")?.as_object()? { if key.ends_with(": type OPT") { From 261b9f4428ff72e01401edf2c19a6bf8d024918b Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Mon, 29 Apr 2024 18:48:37 +0200 Subject: [PATCH 113/124] resolver: test that DS query is sent to parent zone --- .../src/resolver/dnssec/rfc4035.rs | 1 + .../src/resolver/dnssec/rfc4035/section_3.rs | 1 + .../dnssec/rfc4035/section_3/section_3_1.rs | 1 + .../section_3/section_3_1/section_3_1_4.rs | 77 +++++++++++++++++++ 4 files changed, 80 insertions(+) create mode 100644 packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3.rs create mode 100644 packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_1.rs create mode 100644 packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_1/section_3_1_4.rs diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035.rs index 289eace0..5ee74792 100644 --- a/packages/conformance-tests/src/resolver/dnssec/rfc4035.rs +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035.rs @@ -1 +1,2 @@ +mod section_3; mod section_4; diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3.rs new file mode 100644 index 00000000..137bed61 --- /dev/null +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3.rs @@ -0,0 +1 @@ +mod section_3_1; diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_1.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_1.rs new file mode 100644 index 00000000..b2554b53 --- /dev/null +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_1.rs @@ -0,0 +1 @@ +mod section_3_1_4; diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_1/section_3_1_4.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_1/section_3_1_4.rs new file mode 100644 index 00000000..c3489208 --- /dev/null +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_1/section_3_1_4.rs @@ -0,0 +1,77 @@ +use dns_test::{ + client::{Client, DigSettings}, + name_server::{Graph, NameServer, Sign}, + record::RecordType, + tshark::{Capture, Direction}, + Network, Resolver, Result, FQDN, +}; + +#[test] +#[ignore] +fn on_clients_ds_query_it_queries_the_parent_zone() -> Result<()> { + let network = Network::new()?; + + let leaf_ns = NameServer::new(&dns_test::PEER, FQDN::NAMESERVERS, &network)?; + + let Graph { + nameservers, + root, + trust_anchor, + } = Graph::build(leaf_ns, Sign::Yes)?; + + let mut com_ns_addr = None; + for nameserver in &nameservers { + if nameserver.zone() == &FQDN::COM { + com_ns_addr = Some(nameserver.ipv4_addr()); + } + } + let com_ns_addr = com_ns_addr.expect("com. NS not found"); + + let trust_anchor = &trust_anchor.unwrap(); + let resolver = Resolver::new(&network, root) + .trust_anchor(trust_anchor) + .start(&dns_test::SUBJECT)?; + + let mut tshark = resolver.eavesdrop()?; + + let resolver_addr = resolver.ipv4_addr(); + + let client = Client::new(&network)?; + let settings = *DigSettings::default().recurse(); + let output = client.dig(settings, resolver_addr, RecordType::DS, &FQDN::NAMESERVERS)?; + + tshark.wait_for_capture()?; + + let captures = tshark.terminate()?; + + // check that we were able to retrieve the DS record + assert!(output.status.is_noerror()); + let [record] = output.answer.try_into().unwrap(); + let ds = record.try_into_ds().unwrap(); + assert_eq!(ds.zone, FQDN::NAMESERVERS); + + // check that DS query was forwarded to the `com.` (parent zone) nameserver + let client_addr = client.ipv4_addr(); + let mut outgoing_ds_query_count = 0; + for Capture { message, direction } in captures { + if let Direction::Outgoing { destination } = direction { + if destination != client_addr { + let queries = message.as_value()["Queries"] + .as_object() + .expect("expected Object"); + for query in queries.keys() { + if query.contains("type DS") { + assert!(query.contains("nameservers.com")); + assert_eq!(com_ns_addr, destination); + + outgoing_ds_query_count += 1; + } + } + } + } + } + + assert_eq!(1, outgoing_ds_query_count); + + Ok(()) +} From 8b582baffc0b4c3be982a7d3279002af3b037752 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 25 Apr 2024 17:14:05 +0200 Subject: [PATCH 114/124] resolver: test DO bit handling this corresponds to section 3.2.1 of RFC4035 --- .../src/resolver/dnssec/rfc4035/section_3.rs | 1 + .../dnssec/rfc4035/section_3/section_3_2.rs | 126 ++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2.rs diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3.rs index 137bed61..44662807 100644 --- a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3.rs +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3.rs @@ -1 +1,2 @@ mod section_3_1; +mod section_3_2; diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2.rs new file mode 100644 index 00000000..f7c0f0df --- /dev/null +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2.rs @@ -0,0 +1,126 @@ +use dns_test::{ + client::{Client, DigSettings}, + name_server::NameServer, + record::{Record, RecordType}, + tshark::{Capture, Direction}, + zone_file::Root, + Network, Resolver, Result, FQDN, +}; + +#[test] +#[ignore] +fn do_bit_not_set_in_request() -> Result<()> { + let network = &Network::new()?; + let ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)? + .sign()? + .start()?; + let resolver = Resolver::new(network, Root::new(ns.fqdn().clone(), ns.ipv4_addr())) + .start(&dns_test::SUBJECT)?; + + let mut tshark = resolver.eavesdrop()?; + + let client = Client::new(network)?; + let settings = *DigSettings::default().recurse(); + let ans = client.dig(settings, resolver.ipv4_addr(), RecordType::SOA, &FQDN::ROOT)?; + + // "the name server side MUST strip any authenticating DNSSEC RRs from the response" + let [answer] = ans.answer.try_into().unwrap(); + + assert!(matches!(answer, Record::SOA(_))); + + tshark.wait_for_capture()?; + + let captures = tshark.terminate()?; + + let ns_addr = ns.ipv4_addr(); + for Capture { message, direction } in captures { + if let Direction::Outgoing { destination } = direction { + if destination == client.ipv4_addr() { + continue; + } + + // sanity check + assert_eq!(ns_addr, destination); + + // "The resolver side of a security-aware recursive name server MUST set the DO bit + // when sending requests" + if destination == ns_addr { + assert_eq!(Some(true), message.is_do_bit_set()); + } + } + } + + Ok(()) +} + +#[test] +fn if_do_bit_not_set_in_request_then_requested_dnssec_record_is_not_stripped() -> Result<()> { + let network = &Network::new()?; + let ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)? + .sign()? + .start()?; + let resolver = Resolver::new(network, Root::new(ns.fqdn().clone(), ns.ipv4_addr())) + .start(&dns_test::SUBJECT)?; + + let client = Client::new(network)?; + let settings = *DigSettings::default().recurse(); + let ans = client.dig( + settings, + resolver.ipv4_addr(), + RecordType::DNSKEY, + &FQDN::ROOT, + )?; + + // "MUST NOT strip any DNSSEC RR types that the initiating query explicitly requested" + for record in &ans.answer { + assert!(matches!(record, Record::DNSKEY(_))) + } + + Ok(()) +} + +#[test] +#[ignore] +fn do_bit_set_in_request() -> Result<()> { + let network = &Network::new()?; + let ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)? + .sign()? + .start()?; + let resolver = Resolver::new(network, Root::new(ns.fqdn().clone(), ns.ipv4_addr())) + .start(&dns_test::SUBJECT)?; + + let mut tshark = resolver.eavesdrop()?; + + let client = Client::new(network)?; + let settings = *DigSettings::default().dnssec().recurse(); + let ans = client.dig(settings, resolver.ipv4_addr(), RecordType::SOA, &FQDN::ROOT)?; + + let [answer, rrsig] = ans.answer.try_into().unwrap(); + + assert!(matches!(answer, Record::SOA(_))); + assert!(matches!(rrsig, Record::RRSIG(_))); + + tshark.wait_for_capture()?; + + let captures = tshark.terminate()?; + + let ns_addr = ns.ipv4_addr(); + for Capture { message, direction } in captures { + if let Direction::Outgoing { destination } = direction { + if destination == client.ipv4_addr() { + continue; + } + + // sanity check + assert_eq!(ns_addr, destination); + + // "The resolver side of a security-aware recursive name server MUST set the DO bit + // when sending requests" + if destination == ns_addr { + assert_eq!(Some(true), message.is_do_bit_set()); + } + } + } + + Ok(()) +} From 8f414879b8f8345b829ab4cfa66f43c6728704f1 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 10 May 2024 14:19:27 +0200 Subject: [PATCH 115/124] test CD bit forwarding --- .../dnssec/rfc4035/section_3/section_3_2.rs | 2 ++ .../section_3/section_3_2/section_3_2_2.rs | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2/section_3_2_2.rs diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2.rs index f7c0f0df..7d2bd623 100644 --- a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2.rs +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2.rs @@ -1,3 +1,5 @@ +mod section_3_2_2; + use dns_test::{ client::{Client, DigSettings}, name_server::NameServer, diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2/section_3_2_2.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2/section_3_2_2.rs new file mode 100644 index 00000000..de88b4e4 --- /dev/null +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2/section_3_2_2.rs @@ -0,0 +1,23 @@ +use dns_test::{ + client::{Client, DigSettings}, + name_server::NameServer, + record::RecordType, + zone_file::Root, + Network, Resolver, Result, FQDN, +}; + +#[test] +fn copies_cd_bit_from_query_to_response() -> Result<()> { + let network = &Network::new()?; + let ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)?.start()?; + let resolver = Resolver::new(network, Root::new(ns.fqdn().clone(), ns.ipv4_addr())) + .start(&dns_test::SUBJECT)?; + + let client = Client::new(network)?; + let settings = *DigSettings::default().checking_disabled().recurse(); + let ans = client.dig(settings, resolver.ipv4_addr(), RecordType::SOA, &FQDN::ROOT)?; + + assert!(ans.flags.checking_disabled); + + Ok(()) +} From b6e97bf3b677a384f61b22115538eb88a95d4be6 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 10 May 2024 15:07:54 +0200 Subject: [PATCH 116/124] move existing test into RFC4035 section 3.2.2 --- .../conformance-tests/src/resolver/dnssec.rs | 1 + .../src/resolver/dnssec/fixtures.rs | 50 ++++++++++++++ .../section_3/section_3_2/section_3_2_2.rs | 34 ++++++++++ .../src/resolver/dnssec/scenarios/bogus.rs | 66 ++++--------------- packages/dns-test/src/resolver.rs | 4 ++ packages/dns-test/src/zone_file/mod.rs | 1 + 6 files changed, 101 insertions(+), 55 deletions(-) create mode 100644 packages/conformance-tests/src/resolver/dnssec/fixtures.rs diff --git a/packages/conformance-tests/src/resolver/dnssec.rs b/packages/conformance-tests/src/resolver/dnssec.rs index f408ef38..f813ae4b 100644 --- a/packages/conformance-tests/src/resolver/dnssec.rs +++ b/packages/conformance-tests/src/resolver/dnssec.rs @@ -1,4 +1,5 @@ //! DNSSEC functionality +mod fixtures; mod rfc4035; mod scenarios; diff --git a/packages/conformance-tests/src/resolver/dnssec/fixtures.rs b/packages/conformance-tests/src/resolver/dnssec/fixtures.rs new file mode 100644 index 00000000..eb326de9 --- /dev/null +++ b/packages/conformance-tests/src/resolver/dnssec/fixtures.rs @@ -0,0 +1,50 @@ +use std::net::Ipv4Addr; + +use base64::prelude::*; +use dns_test::{ + name_server::{Graph, NameServer, Sign}, + record::Record, + Network, Resolver, Result, FQDN, +}; + +pub fn bad_signature_in_leaf_nameserver( + leaf_fqdn: &FQDN, + leaf_ipv4_addr: Ipv4Addr, +) -> Result<(Resolver, Graph)> { + assert_eq!(Some(FQDN::NAMESERVERS), leaf_fqdn.parent()); + + let network = Network::new()?; + + let mut leaf_ns = NameServer::new(&dns_test::PEER, FQDN::NAMESERVERS, &network)?; + leaf_ns.add(Record::a(leaf_fqdn.clone(), leaf_ipv4_addr)); + + let graph = Graph::build( + leaf_ns, + Sign::AndAmend(&|zone, records| { + if zone == &FQDN::NAMESERVERS { + let mut modified = 0; + for record in records { + if let Record::RRSIG(rrsig) = record { + if rrsig.fqdn == *leaf_fqdn { + let mut signature = BASE64_STANDARD.decode(&rrsig.signature).unwrap(); + let last = signature.last_mut().expect("empty signature"); + *last = !*last; + + rrsig.signature = BASE64_STANDARD.encode(&signature); + modified += 1; + } + } + } + + assert_eq!(modified, 1, "sanity check"); + } + }), + )?; + + let trust_anchor = graph.trust_anchor.as_ref().unwrap(); + let resolver = Resolver::new(&network, graph.root.clone()) + .trust_anchor(trust_anchor) + .start(&dns_test::SUBJECT)?; + + Ok((resolver, graph)) +} diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2/section_3_2_2.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2/section_3_2_2.rs index de88b4e4..ce9b6ab5 100644 --- a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2/section_3_2_2.rs +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2/section_3_2_2.rs @@ -1,3 +1,5 @@ +use std::net::Ipv4Addr; + use dns_test::{ client::{Client, DigSettings}, name_server::NameServer, @@ -6,6 +8,8 @@ use dns_test::{ Network, Resolver, Result, FQDN, }; +use crate::resolver::dnssec::fixtures; + #[test] fn copies_cd_bit_from_query_to_response() -> Result<()> { let network = &Network::new()?; @@ -21,3 +25,33 @@ fn copies_cd_bit_from_query_to_response() -> Result<()> { Ok(()) } + +#[test] +fn if_cd_bit_is_set_then_respond_with_data_that_fails_authentication() -> Result<()> { + let needle_fqdn = FQDN("example.nameservers.com.")?; + let needle_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4); + + let (resolver, _graph) = + fixtures::bad_signature_in_leaf_nameserver(&needle_fqdn, needle_ipv4_addr)?; + + let resolver_addr = resolver.ipv4_addr(); + + let client = Client::new(resolver.network())?; + + let settings = *DigSettings::default() + .recurse() + .authentic_data() + .checking_disabled(); + let output = client.dig(settings, resolver_addr, RecordType::A, &needle_fqdn)?; + + assert!(output.status.is_noerror()); + assert!(!output.flags.authenticated_data); + + let [record] = output.answer.try_into().unwrap(); + let record = record.try_into_a().unwrap(); + + assert_eq!(needle_fqdn, record.fqdn); + assert_eq!(needle_ipv4_addr, record.ipv4_addr); + + Ok(()) +} diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs index b41db3a3..c84ec51a 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/bogus.rs @@ -1,73 +1,29 @@ use std::net::Ipv4Addr; -use base64::prelude::*; use dns_test::client::{Client, DigSettings}; -use dns_test::name_server::{Graph, NameServer, Sign}; -use dns_test::record::{Record, RecordType}; -use dns_test::{Network, Resolver, Result, FQDN}; +use dns_test::record::RecordType; +use dns_test::{Result, FQDN}; +use crate::resolver::dnssec::fixtures; + +// TODO find out which RFC section states this #[ignore] #[test] -fn bad_signature_in_leaf_nameserver() -> Result<()> { - let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4); +fn if_cd_bit_is_clear_and_data_is_not_authentic_then_respond_with_servfail() -> Result<()> { let needle_fqdn = FQDN("example.nameservers.com.")?; + let needle_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4); - let network = Network::new()?; + let (resolver, _graph) = + fixtures::bad_signature_in_leaf_nameserver(&needle_fqdn, needle_ipv4_addr)?; - let mut leaf_ns = NameServer::new(&dns_test::PEER, FQDN::NAMESERVERS, &network)?; - leaf_ns.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); - - let Graph { - nameservers: _nameservers, - root, - trust_anchor, - } = Graph::build( - leaf_ns, - Sign::AndAmend(&|zone, records| { - if zone == &FQDN::NAMESERVERS { - let mut modified = 0; - for record in records { - if let Record::RRSIG(rrsig) = record { - if rrsig.fqdn == needle_fqdn { - let mut signature = BASE64_STANDARD.decode(&rrsig.signature).unwrap(); - let last = signature.last_mut().expect("empty signature"); - *last = !*last; - - rrsig.signature = BASE64_STANDARD.encode(&signature); - modified += 1; - } - } - } - - assert_eq!(modified, 1, "sanity check"); - } - }), - )?; - - let trust_anchor = &trust_anchor.unwrap(); - let resolver = Resolver::new(&network, root) - .trust_anchor(trust_anchor) - .start(&dns_test::SUBJECT)?; let resolver_addr = resolver.ipv4_addr(); - let client = Client::new(&network)?; + let client = Client::new(resolver.network())?; - let mut settings = *DigSettings::default().recurse().authentic_data(); + let settings = *DigSettings::default().recurse().authentic_data(); let output = client.dig(settings, resolver_addr, RecordType::A, &needle_fqdn)?; - // the resolver will try to validate the chain of trust; the validation fails so it responds - // with SERVFAIL assert!(output.status.is_servfail()); - // avoids a SERVFAIL response - settings.checking_disabled(); - - let output = client.dig(settings, resolver_addr, RecordType::A, &needle_fqdn)?; - - // when the CD (Checking Disabled) bit is set the server won't respond with SERVFAIL on - // validation errors. the outcome of the validation process is reported in the AD bit - assert!(output.status.is_noerror()); - assert!(!output.flags.authenticated_data); - Ok(()) } diff --git a/packages/dns-test/src/resolver.rs b/packages/dns-test/src/resolver.rs index 678c7b94..4abf28f8 100644 --- a/packages/dns-test/src/resolver.rs +++ b/packages/dns-test/src/resolver.rs @@ -31,6 +31,10 @@ impl Resolver { self.container.eavesdrop() } + pub fn network(&self) -> &Network { + self.container.network() + } + pub fn container_id(&self) -> &str { self.container.id() } diff --git a/packages/dns-test/src/zone_file/mod.rs b/packages/dns-test/src/zone_file/mod.rs index 57a6c29a..2c8c02b4 100644 --- a/packages/dns-test/src/zone_file/mod.rs +++ b/packages/dns-test/src/zone_file/mod.rs @@ -92,6 +92,7 @@ impl FromStr for ZoneFile { } /// A root (server) hint +#[derive(Clone)] pub struct Root { pub ipv4_addr: Ipv4Addr, pub ns: FQDN, From 2de7139bc61be7874b7e9bd67aedbb936628be9a Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Fri, 10 May 2024 15:56:47 +0200 Subject: [PATCH 117/124] resolver: check that out queries don't have the AD bit --- .../src/resolver/dnssec/fixtures.rs | 29 +++++++- .../src/resolver/dnssec/rfc4035/section_4.rs | 1 + .../dnssec/rfc4035/section_4/section_4_6.rs | 70 +++++++++++++++++++ .../src/resolver/dnssec/scenarios/secure.rs | 24 ++----- packages/dns-test/src/tshark.rs | 14 ++++ 5 files changed, 119 insertions(+), 19 deletions(-) create mode 100644 packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_6.rs diff --git a/packages/conformance-tests/src/resolver/dnssec/fixtures.rs b/packages/conformance-tests/src/resolver/dnssec/fixtures.rs index eb326de9..5167773b 100644 --- a/packages/conformance-tests/src/resolver/dnssec/fixtures.rs +++ b/packages/conformance-tests/src/resolver/dnssec/fixtures.rs @@ -2,9 +2,9 @@ use std::net::Ipv4Addr; use base64::prelude::*; use dns_test::{ - name_server::{Graph, NameServer, Sign}, + name_server::{Graph, NameServer, Running, Sign}, record::Record, - Network, Resolver, Result, FQDN, + Network, Resolver, Result, TrustAnchor, FQDN, }; pub fn bad_signature_in_leaf_nameserver( @@ -48,3 +48,28 @@ pub fn bad_signature_in_leaf_nameserver( Ok((resolver, graph)) } + +pub fn minimally_secure( + leaf_fqdn: FQDN, + leaf_ipv4_addr: Ipv4Addr, +) -> Result<(Resolver, Vec>, TrustAnchor)> { + assert_eq!(Some(FQDN::NAMESERVERS), leaf_fqdn.parent()); + + let network = Network::new()?; + + let mut leaf_ns = NameServer::new(&dns_test::PEER, FQDN::NAMESERVERS, &network)?; + leaf_ns.add(Record::a(leaf_fqdn.clone(), leaf_ipv4_addr)); + + let Graph { + nameservers, + root, + trust_anchor, + } = Graph::build(leaf_ns, Sign::Yes)?; + + let trust_anchor = trust_anchor.unwrap(); + let resolver = Resolver::new(&network, root) + .trust_anchor(&trust_anchor) + .start(&dns_test::SUBJECT)?; + + Ok((resolver, nameservers, trust_anchor)) +} diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4.rs index 5779d78f..52bb840b 100644 --- a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4.rs +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4.rs @@ -1 +1,2 @@ mod section_4_1; +mod section_4_6; diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_6.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_6.rs new file mode 100644 index 00000000..392a4623 --- /dev/null +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_6.rs @@ -0,0 +1,70 @@ +use std::net::Ipv4Addr; + +use dns_test::{ + client::{Client, DigSettings}, + record::RecordType, + tshark::{Capture, Direction}, + Result, FQDN, +}; + +use crate::resolver::dnssec::fixtures; + +#[test] +fn clears_ad_bit_in_outgoing_queries() -> Result<()> { + let leaf_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4); + let leaf_fqdn = FQDN("example.nameservers.com.")?; + + let (resolver, nameservers, _trust_anchor) = + fixtures::minimally_secure(leaf_fqdn.clone(), leaf_ipv4_addr)?; + + let mut tshark = resolver.eavesdrop()?; + + let resolver_addr = resolver.ipv4_addr(); + + let client = Client::new(resolver.network())?; + let settings = *DigSettings::default().recurse().authentic_data(); + let _output = client.dig(settings, resolver_addr, RecordType::A, &leaf_fqdn)?; + + tshark.wait_for_capture()?; + let captures = tshark.terminate()?; + + let client_addr = client.ipv4_addr(); + let mut ns_checks_count = 0; + let mut client_checks_count = 0; + let ns_addrs = nameservers + .iter() + .map(|ns| ns.ipv4_addr()) + .collect::>(); + for Capture { message, direction } in captures { + match direction { + Direction::Incoming { source } => { + if source == client_addr { + // sanity check + assert!(message.is_ad_flag_set()); + + client_checks_count += 1; + } + } + + Direction::Outgoing { destination } => { + if destination == client_addr { + // skip response to client + continue; + } + + // sanity check + assert!(ns_addrs.contains(&destination)); + + assert!(!message.is_ad_flag_set()); + + ns_checks_count += 1; + } + } + } + + // sanity checks + assert_eq!(1, client_checks_count); + assert_ne!(0, dbg!(ns_checks_count)); + + Ok(()) +} diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs index bf076c0b..4f2d5579 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs @@ -1,11 +1,13 @@ use std::net::Ipv4Addr; use dns_test::client::{Client, DigSettings}; -use dns_test::name_server::{Graph, NameServer, Sign}; +use dns_test::name_server::NameServer; use dns_test::record::{Record, RecordType}; use dns_test::zone_file::Root; use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; +use crate::resolver::dnssec::fixtures; + // no DS records are involved; this is a single-link chain of trust #[ignore] #[test] @@ -49,24 +51,12 @@ fn can_validate_with_delegation() -> Result<()> { let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4); let needle_fqdn = FQDN("example.nameservers.com.")?; - let network = Network::new()?; + let (resolver, _nameservers, trust_anchor) = + fixtures::minimally_secure(needle_fqdn.clone(), expected_ipv4_addr)?; - let mut leaf_ns = NameServer::new(&dns_test::PEER, FQDN::NAMESERVERS, &network)?; - leaf_ns.add(Record::a(needle_fqdn.clone(), expected_ipv4_addr)); - - let Graph { - nameservers: _nameservers, - root, - trust_anchor, - } = Graph::build(leaf_ns, Sign::Yes)?; - - let trust_anchor = &trust_anchor.unwrap(); - let resolver = Resolver::new(&network, root) - .trust_anchor(trust_anchor) - .start(&dns_test::SUBJECT)?; let resolver_addr = resolver.ipv4_addr(); - let client = Client::new(&network)?; + let client = Client::new(resolver.network())?; let settings = *DigSettings::default().recurse().authentic_data(); let output = client.dig(settings, resolver_addr, RecordType::A, &needle_fqdn)?; @@ -80,7 +70,7 @@ fn can_validate_with_delegation() -> Result<()> { 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)?; + let output = client.delv(resolver_addr, RecordType::A, &needle_fqdn, &trust_anchor)?; assert!(output.starts_with("; fully validated")); Ok(()) diff --git a/packages/dns-test/src/tshark.rs b/packages/dns-test/src/tshark.rs index c78e1903..1307f4f7 100644 --- a/packages/dns-test/src/tshark.rs +++ b/packages/dns-test/src/tshark.rs @@ -183,6 +183,20 @@ impl Message { &self.inner } + pub fn is_ad_flag_set(&self) -> bool { + let Some(authenticated) = self.inner["dns.flags_tree"] + .as_object() + .unwrap() + .get("dns.flags.authenticated") + else { + return false; + }; + + let authenticated = authenticated.as_str().unwrap(); + assert_eq!("1", authenticated); + true + } + fn opt_record(&self) -> Option<&serde_json::Value> { for (key, value) in self.inner.get("Additional records")?.as_object()? { if key.ends_with(": type OPT") { From 96b3b6650dd7585b7203f6089d49cbe2fb5f1df5 Mon Sep 17 00:00:00 2001 From: Sebastian Ziebell Date: Mon, 13 May 2024 16:52:08 +0200 Subject: [PATCH 118/124] Add function to return A record A NameServer can now return the `Record::A` entry for itself. --- README.md | 4 ++-- .../src/resolver/dnssec/scenarios/secure.rs | 4 ++-- packages/dns-test/examples/explore.rs | 6 ++---- packages/dns-test/src/name_server.rs | 7 ++++++- packages/dns-test/src/tshark.rs | 6 ++---- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 34c594e1..1de72e41 100644 --- a/README.md +++ b/README.md @@ -70,8 +70,8 @@ let com_ns: NameServer; // for `com.` zone let nameservers_ns: NameServer; // for `nameservers.com.` zone nameservers_ns - .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) - .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())); + .add(root_ns.a()) + .add(com_ns.a()); // each `NameServer` will start out with an A record of its FQDN to its own IPv4 address in its // zone file so NO need to add that one in the preceding statement diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs index 4f2d5579..373ca82f 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs @@ -2,7 +2,7 @@ use std::net::Ipv4Addr; use dns_test::client::{Client, DigSettings}; use dns_test::name_server::NameServer; -use dns_test::record::{Record, RecordType}; +use dns_test::record::RecordType; use dns_test::zone_file::Root; use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; @@ -14,7 +14,7 @@ use crate::resolver::dnssec::fixtures; fn can_validate_without_delegation() -> Result<()> { let network = Network::new()?; let mut ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, &network)?; - ns.add(Record::a(ns.fqdn().clone(), ns.ipv4_addr())); + ns.add(ns.a()); let ns = ns.sign()?; let root_ksk = ns.key_signing_key().clone(); diff --git a/packages/dns-test/examples/explore.rs b/packages/dns-test/examples/explore.rs index bc13d63a..73a43a86 100644 --- a/packages/dns-test/examples/explore.rs +++ b/packages/dns-test/examples/explore.rs @@ -4,7 +4,7 @@ use std::sync::mpsc; use dns_test::client::Client; use dns_test::name_server::NameServer; -use dns_test::record::{Record, RecordType}; +use dns_test::record::RecordType; use dns_test::zone_file::Root; use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; @@ -22,9 +22,7 @@ fn main() -> Result<()> { let mut com_ns = NameServer::new(peer, FQDN::COM, &network)?; let mut nameservers_ns = NameServer::new(peer, FQDN("nameservers.com.")?, &network)?; - nameservers_ns - .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) - .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())); + nameservers_ns.add(root_ns.a()).add(com_ns.a()); let nameservers_ns = if args.dnssec { let nameservers_ns = nameservers_ns.sign()?; diff --git a/packages/dns-test/src/name_server.rs b/packages/dns-test/src/name_server.rs index 32488f40..6e599a9d 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -55,7 +55,7 @@ impl Graph { leaf.container.network(), )?; - leaf.add(Record::a(nameserver.fqdn().clone(), nameserver.ipv4_addr())); + leaf.add(nameserver.a()); nameservers.push(nameserver); zone = parent; @@ -409,6 +409,11 @@ impl NameServer { pub fn fqdn(&self) -> &FQDN { &self.zone_file.soa.nameserver } + + /// Returns the [`Record::A`] record for this server. + pub fn a(&self) -> Record { + Record::a(self.fqdn().clone(), self.ipv4_addr()) + } } pub struct Stopped; diff --git a/packages/dns-test/src/tshark.rs b/packages/dns-test/src/tshark.rs index 1307f4f7..6364d437 100644 --- a/packages/dns-test/src/tshark.rs +++ b/packages/dns-test/src/tshark.rs @@ -264,7 +264,7 @@ struct Ip { mod tests { use crate::client::{Client, DigSettings}; use crate::name_server::NameServer; - use crate::record::{Record, RecordType}; + use crate::record::RecordType; use crate::zone_file::Root; use crate::{Implementation, Network, Resolver, FQDN}; @@ -313,9 +313,7 @@ mod tests { let mut nameservers_ns = NameServer::new(&Implementation::Unbound, FQDN("nameservers.com.")?, network)?; - nameservers_ns - .add(Record::a(root_ns.fqdn().clone(), root_ns.ipv4_addr())) - .add(Record::a(com_ns.fqdn().clone(), com_ns.ipv4_addr())); + nameservers_ns.add(root_ns.a()).add(com_ns.a()); let nameservers_ns = nameservers_ns.start()?; com_ns.referral( From 0bcf74961796181b5d1d4903496afd087d85384c Mon Sep 17 00:00:00 2001 From: Sebastian Ziebell Date: Mon, 13 May 2024 16:54:57 +0200 Subject: [PATCH 119/124] Add function to set referral via other nameserver * fix clippy lints --- packages/dns-test/examples/explore.rs | 8 ++------ packages/dns-test/src/name_server.rs | 15 ++++++++++----- packages/dns-test/src/tshark.rs | 8 ++------ 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/packages/dns-test/examples/explore.rs b/packages/dns-test/examples/explore.rs index 73a43a86..dc080c30 100644 --- a/packages/dns-test/examples/explore.rs +++ b/packages/dns-test/examples/explore.rs @@ -32,11 +32,7 @@ fn main() -> Result<()> { nameservers_ns.start()? }; - com_ns.referral( - nameservers_ns.zone().clone(), - nameservers_ns.fqdn().clone(), - nameservers_ns.ipv4_addr(), - ); + com_ns.referral_nameserver(&nameservers_ns); let com_ns = if args.dnssec { let com_ns = com_ns.sign()?; @@ -46,7 +42,7 @@ fn main() -> Result<()> { com_ns.start()? }; - root_ns.referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr()); + root_ns.referral_nameserver(&com_ns); let mut trust_anchor = TrustAnchor::empty(); let root_ns = if args.dnssec { diff --git a/packages/dns-test/src/name_server.rs b/packages/dns-test/src/name_server.rs index 6e599a9d..d8549303 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -71,11 +71,7 @@ impl Graph { unreachable!() }; - parent.referral( - child.zone().clone(), - child.fqdn().clone(), - child.ipv4_addr(), - ); + parent.referral_nameserver(child); } let root = nameservers.last().unwrap(); @@ -188,6 +184,15 @@ impl NameServer { self } + /// Adds a NS + A record pair to the zone file from another NameServer + pub fn referral_nameserver(&mut self, nameserver: &NameServer) -> &mut Self { + self.referral( + nameserver.zone().clone(), + nameserver.fqdn().clone(), + nameserver.ipv4_addr(), + ) + } + /// Adds a record to the name server's zone file pub fn add(&mut self, record: impl Into) -> &mut Self { self.zone_file.add(record); diff --git a/packages/dns-test/src/tshark.rs b/packages/dns-test/src/tshark.rs index 6364d437..6af22c17 100644 --- a/packages/dns-test/src/tshark.rs +++ b/packages/dns-test/src/tshark.rs @@ -316,14 +316,10 @@ mod tests { nameservers_ns.add(root_ns.a()).add(com_ns.a()); let nameservers_ns = nameservers_ns.start()?; - com_ns.referral( - nameservers_ns.zone().clone(), - nameservers_ns.fqdn().clone(), - nameservers_ns.ipv4_addr(), - ); + com_ns.referral_nameserver(&nameservers_ns); let com_ns = com_ns.start()?; - root_ns.referral(FQDN::COM, com_ns.fqdn().clone(), com_ns.ipv4_addr()); + root_ns.referral_nameserver(&com_ns); let root_ns = root_ns.start()?; let resolver = Resolver::new( From 0ea13974809bf053aec3bdac09dc8f5a51794277 Mon Sep 17 00:00:00 2001 From: Sebastian Ziebell Date: Mon, 13 May 2024 17:52:26 +0200 Subject: [PATCH 120/124] Add function to return root hint This adds a function to `NameServer` to return root hints. --- .../dnssec/rfc4035/section_3/section_3_2.rs | 10 +++------- .../rfc4035/section_3/section_3_2/section_3_2_2.rs | 4 +--- .../dnssec/rfc4035/section_4/section_4_1.rs | 4 +--- .../src/resolver/dnssec/scenarios/secure.rs | 3 +-- packages/dns-test/examples/explore.rs | 10 +++------- packages/dns-test/src/name_server.rs | 5 +++++ packages/dns-test/src/resolver.rs | 13 +++++-------- packages/dns-test/src/tshark.rs | 8 ++------ 8 files changed, 21 insertions(+), 36 deletions(-) diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2.rs index 7d2bd623..8d947214 100644 --- a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2.rs +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2.rs @@ -5,7 +5,6 @@ use dns_test::{ name_server::NameServer, record::{Record, RecordType}, tshark::{Capture, Direction}, - zone_file::Root, Network, Resolver, Result, FQDN, }; @@ -16,8 +15,7 @@ fn do_bit_not_set_in_request() -> Result<()> { let ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)? .sign()? .start()?; - let resolver = Resolver::new(network, Root::new(ns.fqdn().clone(), ns.ipv4_addr())) - .start(&dns_test::SUBJECT)?; + let resolver = Resolver::new(network, ns.root_hint()).start(&dns_test::SUBJECT)?; let mut tshark = resolver.eavesdrop()?; @@ -61,8 +59,7 @@ fn if_do_bit_not_set_in_request_then_requested_dnssec_record_is_not_stripped() - let ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)? .sign()? .start()?; - let resolver = Resolver::new(network, Root::new(ns.fqdn().clone(), ns.ipv4_addr())) - .start(&dns_test::SUBJECT)?; + let resolver = Resolver::new(network, ns.root_hint()).start(&dns_test::SUBJECT)?; let client = Client::new(network)?; let settings = *DigSettings::default().recurse(); @@ -88,8 +85,7 @@ fn do_bit_set_in_request() -> Result<()> { let ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)? .sign()? .start()?; - let resolver = Resolver::new(network, Root::new(ns.fqdn().clone(), ns.ipv4_addr())) - .start(&dns_test::SUBJECT)?; + let resolver = Resolver::new(network, ns.root_hint()).start(&dns_test::SUBJECT)?; let mut tshark = resolver.eavesdrop()?; diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2/section_3_2_2.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2/section_3_2_2.rs index ce9b6ab5..13e13fa0 100644 --- a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2/section_3_2_2.rs +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2/section_3_2_2.rs @@ -4,7 +4,6 @@ use dns_test::{ client::{Client, DigSettings}, name_server::NameServer, record::RecordType, - zone_file::Root, Network, Resolver, Result, FQDN, }; @@ -14,8 +13,7 @@ use crate::resolver::dnssec::fixtures; fn copies_cd_bit_from_query_to_response() -> Result<()> { let network = &Network::new()?; let ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)?.start()?; - let resolver = Resolver::new(network, Root::new(ns.fqdn().clone(), ns.ipv4_addr())) - .start(&dns_test::SUBJECT)?; + let resolver = Resolver::new(network, ns.root_hint()).start(&dns_test::SUBJECT)?; let client = Client::new(network)?; let settings = *DigSettings::default().checking_disabled().recurse(); diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs index 61244953..56c4d8da 100644 --- a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs @@ -2,7 +2,6 @@ use dns_test::client::{Client, DigSettings}; use dns_test::name_server::NameServer; use dns_test::record::RecordType; use dns_test::tshark::{Capture, Direction}; -use dns_test::zone_file::Root; use dns_test::{Network, Resolver, Result, FQDN}; #[test] @@ -10,8 +9,7 @@ use dns_test::{Network, Resolver, Result, FQDN}; fn edns_support() -> Result<()> { let network = &Network::new()?; let ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)?.start()?; - let resolver = Resolver::new(network, Root::new(ns.fqdn().clone(), ns.ipv4_addr())) - .start(&dns_test::SUBJECT)?; + let resolver = Resolver::new(network, ns.root_hint()).start(&dns_test::SUBJECT)?; let mut tshark = resolver.eavesdrop()?; diff --git a/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs b/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs index 373ca82f..7778d53e 100644 --- a/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs +++ b/packages/conformance-tests/src/resolver/dnssec/scenarios/secure.rs @@ -3,7 +3,6 @@ use std::net::Ipv4Addr; use dns_test::client::{Client, DigSettings}; use dns_test::name_server::NameServer; use dns_test::record::RecordType; -use dns_test::zone_file::Root; use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; use crate::resolver::dnssec::fixtures; @@ -27,7 +26,7 @@ fn can_validate_without_delegation() -> Result<()> { eprintln!("root.zone:\n{}", ns.zone_file()); let trust_anchor = &TrustAnchor::from_iter([root_ksk.clone(), root_zsk.clone()]); - let resolver = Resolver::new(&network, Root::new(ns.fqdn().clone(), ns.ipv4_addr())) + let resolver = Resolver::new(&network, ns.root_hint()) .trust_anchor(trust_anchor) .start(&dns_test::SUBJECT)?; let resolver_addr = resolver.ipv4_addr(); diff --git a/packages/dns-test/examples/explore.rs b/packages/dns-test/examples/explore.rs index dc080c30..f92dd4fc 100644 --- a/packages/dns-test/examples/explore.rs +++ b/packages/dns-test/examples/explore.rs @@ -5,7 +5,6 @@ use std::sync::mpsc; use dns_test::client::Client; use dns_test::name_server::NameServer; use dns_test::record::RecordType; -use dns_test::zone_file::Root; use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; fn main() -> Result<()> { @@ -74,12 +73,9 @@ fn main() -> Result<()> { } println!("building docker image..."); - let resolver = Resolver::new( - &network, - Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr()), - ) - .trust_anchor(&trust_anchor) - .start(&dns_test::SUBJECT)?; + let resolver = Resolver::new(&network, root_ns.root_hint()) + .trust_anchor(&trust_anchor) + .start(&dns_test::SUBJECT)?; println!("DONE\n\n"); let (tx, rx) = mpsc::channel(); diff --git a/packages/dns-test/src/name_server.rs b/packages/dns-test/src/name_server.rs index d8549303..9ac28dc5 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -419,6 +419,11 @@ impl NameServer { pub fn a(&self) -> Record { Record::a(self.fqdn().clone(), self.ipv4_addr()) } + + /// Returns the [`Root`] hint for this server. + pub fn root_hint(&self) -> Root { + Root::new(self.fqdn().clone(), self.ipv4_addr()) + } } pub struct Stopped; diff --git a/packages/dns-test/src/resolver.rs b/packages/dns-test/src/resolver.rs index 4abf28f8..40352159 100644 --- a/packages/dns-test/src/resolver.rs +++ b/packages/dns-test/src/resolver.rs @@ -183,8 +183,7 @@ mod tests { fn terminate_unbound_works() -> Result<()> { let network = Network::new()?; let ns = NameServer::new(&Implementation::Unbound, FQDN::ROOT, &network)?.start()?; - let resolver = Resolver::new(&network, Root::new(ns.fqdn().clone(), ns.ipv4_addr())) - .start(&Implementation::Unbound)?; + let resolver = Resolver::new(&network, ns.root_hint()).start(&Implementation::Unbound)?; let logs = resolver.terminate()?; eprintln!("{logs}"); @@ -197,8 +196,7 @@ mod tests { fn terminate_bind_works() -> Result<()> { let network = Network::new()?; let ns = NameServer::new(&Implementation::Unbound, FQDN::ROOT, &network)?.start()?; - let resolver = Resolver::new(&network, Root::new(ns.fqdn().clone(), ns.ipv4_addr())) - .start(&Implementation::Bind)?; + let resolver = Resolver::new(&network, ns.root_hint()).start(&Implementation::Bind)?; let logs = resolver.terminate()?; eprintln!("{logs}"); @@ -211,10 +209,9 @@ mod tests { fn terminate_hickory_works() -> Result<()> { let network = Network::new()?; let ns = NameServer::new(&Implementation::Unbound, FQDN::ROOT, &network)?.start()?; - let resolver = Resolver::new(&network, Root::new(ns.fqdn().clone(), ns.ipv4_addr())) - .start(&Implementation::Hickory(Repository( - "https://github.com/hickory-dns/hickory-dns", - )))?; + let resolver = Resolver::new(&network, ns.root_hint()).start(&Implementation::Hickory( + Repository("https://github.com/hickory-dns/hickory-dns"), + ))?; let logs = resolver.terminate()?; // Hickory-DNS start sequence log has been consumed in `ResolverSettings.start`. diff --git a/packages/dns-test/src/tshark.rs b/packages/dns-test/src/tshark.rs index 6af22c17..ba40a6cb 100644 --- a/packages/dns-test/src/tshark.rs +++ b/packages/dns-test/src/tshark.rs @@ -265,7 +265,6 @@ mod tests { use crate::client::{Client, DigSettings}; use crate::name_server::NameServer; use crate::record::RecordType; - use crate::zone_file::Root; use crate::{Implementation, Network, Resolver, FQDN}; use super::*; @@ -322,11 +321,8 @@ mod tests { root_ns.referral_nameserver(&com_ns); let root_ns = root_ns.start()?; - let resolver = Resolver::new( - network, - Root::new(root_ns.fqdn().clone(), root_ns.ipv4_addr()), - ) - .start(&Implementation::Unbound)?; + let resolver = + Resolver::new(network, root_ns.root_hint()).start(&Implementation::Unbound)?; let mut tshark = resolver.eavesdrop()?; let resolver_addr = resolver.ipv4_addr(); From ab2aa39b063d833b347b85370ba5dbace551ef5d Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 23 May 2024 13:08:01 +0200 Subject: [PATCH 121/124] NameServer: fix generation of SOA record `unbound` requires that the MNAME lies underneath the zone. That is `primaryNN.nameservers.com.` is not a valid MNAME for a nameserver authoritative over `mydomain.com.`. For that zone, `primaryNN.mydomain.com.` would be a valid MNAME. --- packages/dns-test/src/name_server.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/dns-test/src/name_server.rs b/packages/dns-test/src/name_server.rs index 9ac28dc5..14b5b891 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -153,7 +153,7 @@ impl NameServer { /// the zone pub fn new(implementation: &Implementation, zone: FQDN, network: &Network) -> Result { let ns_count = ns_count(); - let nameserver = primary_ns(ns_count); + let nameserver = primary_ns(ns_count, &zone); let image = implementation.clone().into(); let container = Container::run(&image, network)?; @@ -161,7 +161,7 @@ impl NameServer { zone: zone.clone(), ttl: DEFAULT_TTL, nameserver: nameserver.clone(), - admin: admin_ns(ns_count), + admin: admin_ns(ns_count, &zone), settings: SoaSettings::default(), }; let mut zone_file = ZoneFile::new(soa); @@ -439,12 +439,22 @@ pub struct Running { child: Child, } -fn primary_ns(ns_count: usize) -> FQDN { - FQDN(format!("primary{ns_count}.nameservers.com.")).unwrap() +fn primary_ns(ns_count: usize, zone: &FQDN) -> FQDN { + FQDN(format!("primary{ns_count}.{}", expand_zone(zone))).unwrap() } -fn admin_ns(ns_count: usize) -> FQDN { - FQDN(format!("admin{ns_count}.nameservers.com.")).unwrap() +fn admin_ns(ns_count: usize, zone: &FQDN) -> FQDN { + FQDN(format!("admin{ns_count}.{}", expand_zone(zone))).unwrap() +} + +fn expand_zone(zone: &FQDN) -> String { + if zone == &FQDN::ROOT { + "nameservers.com.".to_string() + } else if zone.num_labels() == 1 { + format!("nameservers.{}", zone.as_str()) + } else { + zone.to_string() + } } #[cfg(test)] From a0909f3c046db843c3c118df2d85899caabc2945 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 23 May 2024 11:32:43 +0200 Subject: [PATCH 122/124] tweak `Graph::build` to support anydomain.com. as leaf zone --- packages/dns-test/src/name_server.rs | 78 ++++++++++++++++------------ 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/packages/dns-test/src/name_server.rs b/packages/dns-test/src/name_server.rs index 14b5b891..4db1c0c3 100644 --- a/packages/dns-test/src/name_server.rs +++ b/packages/dns-test/src/name_server.rs @@ -19,7 +19,6 @@ pub enum Sign<'a> { No, Yes, /// Signs the zone files and then modifies the records produced by the signing process - // XXX if captures are needed use `&dyn Fn(..)` instead of a function pointer AndAmend(&'a dyn Fn(&FQDN, &mut Vec)), } @@ -36,46 +35,54 @@ impl Graph { /// /// a non-empty `TrustAnchor` is returned only when `Sign::Yes` or `Sign::AndAmend` is used pub fn build(leaf: NameServer, sign: Sign) -> Result { - // TODO if `leaf` is not authoritative over `nameservers.com.`, we would need two "lines" to - // root. for example, if `leaf` is authoritative over `example.net.` we would need these two - // lines: - // - `nameservers.com.`, `com.`, `.` to cover the `primaryNNN.nameservers.com.` domains that - // `NameServer` implicitly uses - // - `example.net.`, `net.`, `.` to cover the requested `leaf` name server - assert_eq!(&FQDN::NAMESERVERS, leaf.zone(), "not yet implemented"); + assert_eq!(2, leaf.zone().num_labels(), "not yet implemented"); + assert_eq!(Some(FQDN::COM), leaf.zone().parent(), "not yet implemented"); // first pass: create nameservers for parent zones let mut zone = leaf.zone().clone(); - let mut nameservers = vec![leaf]; - while let Some(parent) = zone.parent() { - let leaf = &mut nameservers[0]; - let nameserver = NameServer::new( - &leaf.implementation, - parent.clone(), - leaf.container.network(), - )?; + let network = leaf.container.network().clone(); + let implementation = leaf.implementation.clone(); - leaf.add(nameserver.a()); + let (mut nameservers_ns, leaf) = if leaf.zone() != &FQDN::NAMESERVERS { + let nameservers_ns = NameServer::new(&implementation, FQDN::NAMESERVERS, &network)?; + (nameservers_ns, Some(leaf)) + } else { + (leaf, None) + }; + + // the nameserver covering `FQDN::NAMESERVERS` needs A records about all the nameservers in the graph + let mut nameservers = vec![]; + while let Some(parent) = zone.parent() { + let nameserver = NameServer::new(&implementation, parent.clone(), &network)?; + + nameservers_ns.add(nameserver.a()); nameservers.push(nameserver); zone = parent; } + drop((network, implementation)); - // XXX will not hold when `leaf` is not authoritative over `nameservers.com.` - assert_eq!(3, nameservers.len()); + if let Some(leaf) = leaf { + nameservers.insert(0, leaf); + } + nameservers.insert(0, nameservers_ns); // second pass: add referrals from parent to child - // `windows_mut` is not a thing in `core::iter` so use indexing as a workaround - for index in 0..nameservers.len() - 1 { - let [child, parent] = &mut nameservers[index..][..2] else { - unreachable!() - }; - - parent.referral_nameserver(child); + // the nameservers are sorted leaf-most zone first but siblings may be next to each other + // for each child (e.g. `nameservers.com.`), do a linear search for its parent (`com.`) + for index in 1..nameservers.len() { + let (left, right) = nameservers.split_at_mut(index); + let child = left.last_mut().unwrap(); + for maybe_parent in right { + if Some(maybe_parent.zone()) == child.zone().parent().as_ref() { + let parent = maybe_parent; + parent.referral_nameserver(child); + break; + } + } } - let root = nameservers.last().unwrap(); - let root = Root::new(root.fqdn().clone(), root.ipv4_addr()); + let root = nameservers.last().unwrap().root_hint(); // start name servers let (nameservers, trust_anchor) = match sign { @@ -96,15 +103,22 @@ impl Graph { }; let mut running = vec![]; - let mut child_ds = None; + let mut children_ds = vec![]; + let mut children_num_labels = 0; let len = nameservers.len(); for (index, mut nameserver) in nameservers.into_iter().enumerate() { - if let Some(ds) = child_ds.take() { - nameserver.add(ds); + if !children_ds.is_empty() { + let is_parent = nameserver.zone().num_labels() + 1 == children_num_labels; + if is_parent { + for ds in children_ds.drain(..) { + nameserver.add(ds); + } + } } let mut nameserver = nameserver.sign()?; - child_ds = Some(nameserver.ds().clone()); + children_ds.push(nameserver.ds().clone()); + children_num_labels = nameserver.zone().num_labels(); if let Some(mutate) = maybe_mutate { let zone = nameserver.zone().clone(); mutate(&zone, &mut nameserver.signed_zone_file_mut().records); From f87d98eaaf42d15bbf4797a0ed6a828ab38002cf Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Thu, 23 May 2024 13:10:23 +0200 Subject: [PATCH 123/124] explore: use `Graph` API and use `mydomain.com.` as leaf zone --- packages/dns-test/examples/explore.rs | 88 +++++++-------------------- 1 file changed, 22 insertions(+), 66 deletions(-) diff --git a/packages/dns-test/examples/explore.rs b/packages/dns-test/examples/explore.rs index f92dd4fc..6d4c0427 100644 --- a/packages/dns-test/examples/explore.rs +++ b/packages/dns-test/examples/explore.rs @@ -3,9 +3,9 @@ use std::net::Ipv4Addr; use std::sync::mpsc; use dns_test::client::Client; -use dns_test::name_server::NameServer; +use dns_test::name_server::{Graph, NameServer, Sign}; use dns_test::record::RecordType; -use dns_test::{Network, Resolver, Result, TrustAnchor, FQDN}; +use dns_test::{Network, Resolver, Result, FQDN}; fn main() -> Result<()> { let args = Args::from_env()?; @@ -14,49 +14,16 @@ fn main() -> Result<()> { let peer = &dns_test::PEER; println!("building docker image..."); - let mut root_ns = NameServer::new(peer, FQDN::ROOT, &network)?; + let leaf_ns = NameServer::new(peer, FQDN("mydomain.com.")?, &network)?; println!("DONE"); println!("setting up name servers..."); - let mut com_ns = NameServer::new(peer, FQDN::COM, &network)?; - - let mut nameservers_ns = NameServer::new(peer, FQDN("nameservers.com.")?, &network)?; - nameservers_ns.add(root_ns.a()).add(com_ns.a()); - - let nameservers_ns = if args.dnssec { - let nameservers_ns = nameservers_ns.sign()?; - com_ns.add(nameservers_ns.ds().clone()); - nameservers_ns.start()? - } else { - nameservers_ns.start()? - }; - - com_ns.referral_nameserver(&nameservers_ns); - - let com_ns = if args.dnssec { - let com_ns = com_ns.sign()?; - root_ns.add(com_ns.ds().clone()); - com_ns.start()? - } else { - com_ns.start()? - }; - - root_ns.referral_nameserver(&com_ns); - - let mut trust_anchor = TrustAnchor::empty(); - let root_ns = if args.dnssec { - let root_ns = root_ns.sign()?; - let root_ksk = root_ns.key_signing_key(); - let root_zsk = root_ns.zone_signing_key(); - - trust_anchor.add(root_ksk.clone()); - trust_anchor.add(root_zsk.clone()); - - root_ns.start()? - } else { - root_ns.start()? - }; - + let sign = if args.dnssec { Sign::Yes } else { Sign::No }; + let Graph { + root, + trust_anchor, + nameservers, + } = Graph::build(leaf_ns, sign)?; println!("DONE"); let client = Client::new(&network)?; @@ -68,40 +35,29 @@ fn main() -> Result<()> { Ipv4Addr::new(127, 0, 0, 1), RecordType::SOA, &FQDN::ROOT, - &trust_anchor, + trust_anchor.as_ref().unwrap(), )?; } println!("building docker image..."); - let resolver = Resolver::new(&network, root_ns.root_hint()) - .trust_anchor(&trust_anchor) - .start(&dns_test::SUBJECT)?; + let mut builder = Resolver::new(&network, root); + if let Some(trust_anchor) = trust_anchor { + builder.trust_anchor(&trust_anchor); + } + let resolver = builder.start(&dns_test::SUBJECT)?; println!("DONE\n\n"); let (tx, rx) = mpsc::channel(); ctrlc::set_handler(move || tx.send(()).expect("could not forward signal"))?; - println!(". (root) name server's IP address: {}", root_ns.ipv4_addr()); - println!( - "attach to this container with: `docker exec -it {} bash`\n", - root_ns.container_id() - ); - - println!("com. name server's IP address: {}", com_ns.ipv4_addr()); - println!( - "attach to this container with: `docker exec -it {} bash`\n", - com_ns.container_id() - ); - - println!( - "nameservers.com. name server's IP address: {}", - nameservers_ns.ipv4_addr() - ); - println!( - "attach to this container with: `docker exec -it {} bash`\n", - nameservers_ns.container_id() - ); + for ns in &nameservers { + println!("{} name server's IP address: {}", ns.zone(), ns.ipv4_addr()); + println!( + "attach to this container with: `docker exec -it {} bash`\n", + ns.container_id() + ); + } let resolver_addr = resolver.ipv4_addr(); println!("resolver's IP address: {resolver_addr}",); From 0d529d4f41f694a531252a29b7e032e83ad3551c Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Wed, 29 May 2024 12:46:32 +0200 Subject: [PATCH 124/124] bump hickory-dns and unignore fixed tests also build hickory-dns with dnssec support and enable security awareness --- .github/workflows/ci.yml | 2 +- .../dnssec/rfc4035/section_3/section_3_1/section_3_1_4.rs | 1 - .../src/resolver/dnssec/rfc4035/section_3/section_3_2.rs | 2 -- .../src/resolver/dnssec/rfc4035/section_4/section_4_1.rs | 1 - packages/dns-test/src/docker/hickory.Dockerfile | 2 +- packages/dns-test/src/templates/hickory.resolver.toml.jinja | 2 +- 6 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 62f05ddc..15fd455f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: merge_group: env: - HICKORY_REV: a3669bd80f3f7b97f0c301c15f1cba6368d97b63 + HICKORY_REV: 107635c6c5934524894736f1b141198d0fa62fec DNS_TEST_VERBOSE_DOCKER_BUILD: 1 jobs: diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_1/section_3_1_4.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_1/section_3_1_4.rs index c3489208..8fca7308 100644 --- a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_1/section_3_1_4.rs +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_1/section_3_1_4.rs @@ -7,7 +7,6 @@ use dns_test::{ }; #[test] -#[ignore] fn on_clients_ds_query_it_queries_the_parent_zone() -> Result<()> { let network = Network::new()?; diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2.rs index 8d947214..e9a1fc53 100644 --- a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2.rs +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_3/section_3_2.rs @@ -9,7 +9,6 @@ use dns_test::{ }; #[test] -#[ignore] fn do_bit_not_set_in_request() -> Result<()> { let network = &Network::new()?; let ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)? @@ -79,7 +78,6 @@ fn if_do_bit_not_set_in_request_then_requested_dnssec_record_is_not_stripped() - } #[test] -#[ignore] fn do_bit_set_in_request() -> Result<()> { let network = &Network::new()?; let ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)? diff --git a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs index 56c4d8da..deec6fe6 100644 --- a/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs +++ b/packages/conformance-tests/src/resolver/dnssec/rfc4035/section_4/section_4_1.rs @@ -5,7 +5,6 @@ use dns_test::tshark::{Capture, Direction}; use dns_test::{Network, Resolver, Result, FQDN}; #[test] -#[ignore] fn edns_support() -> Result<()> { let network = &Network::new()?; let ns = NameServer::new(&dns_test::PEER, FQDN::ROOT, network)?.start()?; diff --git a/packages/dns-test/src/docker/hickory.Dockerfile b/packages/dns-test/src/docker/hickory.Dockerfile index 18cd7555..9844d115 100644 --- a/packages/dns-test/src/docker/hickory.Dockerfile +++ b/packages/dns-test/src/docker/hickory.Dockerfile @@ -10,6 +10,6 @@ RUN apt-get update && \ # a clone of the hickory repository. `./src` here refers to that clone; not to # any directory inside the `dns-test` repository COPY ./src /usr/src/hickory -RUN cargo install --path /usr/src/hickory/bin --features recursor --debug && \ +RUN cargo install --path /usr/src/hickory/bin --features recursor,dnssec-ring --debug && \ mkdir /etc/hickory env RUST_LOG=debug diff --git a/packages/dns-test/src/templates/hickory.resolver.toml.jinja b/packages/dns-test/src/templates/hickory.resolver.toml.jinja index bd4be69c..d32d8e4a 100644 --- a/packages/dns-test/src/templates/hickory.resolver.toml.jinja +++ b/packages/dns-test/src/templates/hickory.resolver.toml.jinja @@ -1,5 +1,5 @@ [[zones]] zone = "." zone_type = "Hint" -stores = { type = "recursor", roots = "/etc/root.hints" } +stores = { type = "recursor", roots = "/etc/root.hints", security_aware = true } enable_dnssec = {{ use_dnssec }}