Merge pull request #13 from japaric/sebastian/gh7-docker-network

Use network to group all running containers
This commit is contained in:
Jorge Aparicio 2024-02-16 15:00:25 +01:00 committed by GitHub
commit 2abb8268f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 237 additions and 45 deletions

View File

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

View File

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

View File

@ -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<Self> {
pub fn new(network: &Network) -> Result<Self> {
Ok(Self {
inner: Container::run(Implementation::Unbound)?,
inner: Container::run(Implementation::Unbound, network)?,
})
}

View File

@ -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<Inner>,
}
@ -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<Self> {
pub fn run(implementation: Implementation, network: &Network) -> Result<Self> {
// 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";

View File

@ -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<NetworkInner>);
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<Self> {
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<Self> {
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<NetworkConfig> {
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(())
}
}

View File

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

View File

@ -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<Self> {
pub fn new(zone: FQDN<'a>, network: &Network) -> Result<Self> {
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"));

View File

@ -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<Self> {
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()?;

View File

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