From 5630dd79e94373bb2d7f08e282b6817903e7da29 Mon Sep 17 00:00:00 2001 From: Sebastian Ziebell Date: Tue, 13 Feb 2024 10:55:39 +0100 Subject: [PATCH 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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(()) }