initial RecursiveResolver API

This commit is contained in:
Jorge Aparicio 2024-02-05 14:24:01 +01:00
parent 9101bb1046
commit 60ecfeca5e
6 changed files with 132 additions and 2 deletions

1
Cargo.lock generated
View File

@ -25,6 +25,7 @@ name = "dnssec-tests"
version = "0.1.0"
dependencies = [
"minijinja",
"serde",
"tempfile",
]

View File

@ -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"

View File

@ -114,6 +114,7 @@ impl Container {
Ok(child)
}
// TODO cache this to avoid calling `docker inspect` every time
pub fn ip_addr(&self) -> Result<String> {
let mut command = Command::new("docker");
command

View File

@ -1,4 +1,5 @@
pub use crate::authoritative_name_server::AuthoritativeNameServer;
pub use crate::recursive_resolver::RecursiveResolver;
pub type Error = Box<dyn std::error::Error>;
pub type Result<T> = core::result::Result<T, Error>;
@ -7,6 +8,7 @@ const CHMOD_RW_EVERYONE: &str = "666";
mod authoritative_name_server;
mod container;
mod recursive_resolver;
pub enum Domain<'a> {
Root,

123
src/recursive_resolver.rs Normal file
View File

@ -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<Self> {
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<String> {
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::<Vec<_>>();
let hints = root_hints(&roots);
eprintln!("{hints}");
let lines = hints.lines().collect::<Vec<_>>();
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::<Vec<_>>()
.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::<Vec<_>>()
.try_into()
.unwrap();
assert_eq!(expected_ns_name, ns_name);
assert_eq!("A", record_type);
assert_eq!(expected_ip_addr, ip_addr);
}
}
}

View File

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