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 %}