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 006770b3..b15f0e14 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; @@ -10,6 +12,8 @@ use tempfile::{NamedTempFile, TempDir}; use crate::{Error, Implementation, Result}; +pub use crate::container::network::Network; + pub struct Container { inner: Arc, } @@ -18,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()?; @@ -50,6 +54,7 @@ impl Container { command .args(["run", "--rm", "--detach", "--name", &name]) .arg("-it") + .args(["--network", network.name()]) .arg(image_tag) .args(["sleep", "infinity"]); @@ -62,6 +67,7 @@ impl Container { id, name, ipv4_addr, + _network: network.clone(), }; Ok(Self { inner: Arc::new(inner), @@ -150,6 +156,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 { @@ -163,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 @@ -267,7 +278,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()); @@ -277,7 +289,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}")])?; @@ -288,7 +301,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 new file mode 100644 index 00000000..4199f9ee --- /dev/null +++ b/packages/dns-test/src/container/network.rs @@ -0,0 +1,160 @@ +use std::{ + process::{self, Command, Stdio}, + sync::{ + atomic::{self, AtomicUsize}, + Arc, + }, +}; + +use crate::Result; + +/// Represents a network in which to put containers into. +#[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 pid = process::id(); + let network_name = env!("CARGO_PKG_NAME"); + Ok(Self(Arc::new(NetworkInner::new(pid, network_name)?))) + } +} + +/// 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 + .args(["network", "create"]) + .args(["--internal", "--attachable"]) + .arg(&network_name); + + // create network + let output = command.output()?; + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.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)?; + + Ok(Self { + name: network_name, + config, + }) + } +} + +/// Collects all important configs. +pub struct NetworkConfig { + /// The CIDR subnet mask, e.g. "172.21.0.0/16" + subnet: String, +} + +/// Return network config +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 }) +} + +fn network_count() -> usize { + static COUNT: AtomicUsize = AtomicUsize::new(1); + + COUNT.fetch_add(1, atomic::Ordering::Relaxed) +} + +#[cfg(test)] +mod tests { + use crate::{container::Container, Implementation}; + + use super::*; + + fn exists_network(network_name: &str) -> bool { + let mut command = Command::new("docker"); + command.args(["network", "ls", "--format={{ .Name }}"]); + + 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() + } + + #[test] + fn create_works() -> Result<()> { + let network = Network::new(); + assert!(network.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 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(()) + } +} 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..1189b9cf 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, }) @@ -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"; @@ -290,10 +294,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 +314,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 +328,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 +344,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 +355,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 +380,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..3a56d11a 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 { @@ -43,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 => { @@ -94,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 { @@ -110,12 +114,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()?; 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