diff --git a/Cargo.lock b/Cargo.lock index 35f0828..a9e81d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -157,6 +157,7 @@ dependencies = [ "ctrlc", "minijinja", "pretty_assertions", + "rand", "serde", "serde_json", "serde_with", @@ -201,6 +202,17 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -366,6 +378,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "pretty_assertions" version = "1.4.0" @@ -394,6 +412,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -591,6 +639,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "wasm-bindgen" version = "0.2.91" diff --git a/packages/dns-test/Cargo.toml b/packages/dns-test/Cargo.toml index eacbbf9..8ae4934 100644 --- a/packages/dns-test/Cargo.toml +++ b/packages/dns-test/Cargo.toml @@ -7,6 +7,7 @@ version = "0.1.0" [dependencies] minijinja = "1.0.12" +rand = "0.8.5" serde = { version = "1.0.196", features = ["derive"] } serde_json = "1.0.113" serde_with = "3.6.1" diff --git a/packages/dns-test/src/container/network.rs b/packages/dns-test/src/container/network.rs index 6c1751c..d630b58 100644 --- a/packages/dns-test/src/container/network.rs +++ b/packages/dns-test/src/container/network.rs @@ -6,6 +6,8 @@ use std::{ }, }; +use rand::Rng; + use crate::Result; /// Represents a network in which to put containers into. @@ -50,31 +52,87 @@ impl Drop for NetworkInner { impl NetworkInner { pub fn new(pid: u32, network_name: &str) -> Result { + const NUM_TRIES: usize = 3; + 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()); + let mut rng = rand::thread_rng(); + for _ in 0..NUM_TRIES { + // the probability this subnet collides with another network created by the framework is + // 1/3824 = ~2.6e-4 + // + // after 3 retries that probability of a collision is reduced to 1.78e-11 + // + // the probably will be bigger if more than one subnet has already been created by + // the framework but the base probability will only increase by an "N times" factor. 2 + // other networks make the probability 2/3824, 3 make it 3/3824, etc. + // + // creating a large Docker network _outside_ the framework can greatly increase the + // probability of a collision. for example, `docker create --subnet 172.18.0.0/16` + // increases the base probability to 256/3824 or 6.69e-2; after the 3 retries, the + // probability is still high at 3e-3 + // + // to prevent collisions with Docker networks created outside of the framework we could + // use the private address range 10.0.0.0/8 but that can then collide with other + // services like VPNs, wireguard, etc. + let subnet_pick = rng.gen_range(0..SUBNET_MAX); + let subnet = subnet(subnet_pick); + + let mut command = Command::new("docker"); + command + .args(["network", "create"]) + .args(["--internal", "--attachable", "--subnet", &subnet]) + .arg(&network_name); + + // create network + let output = command.output()?; + + if !output.status.success() { + continue; + } + + return Ok(Self { + name: network_name, + config: NetworkConfig { subnet }, + }); } - // inspect & parse network details - let config = get_network_config(&network_name)?; + Err(format!( + "failed to allocate a network in the address ranges +- 172.18.0.0/16 - 172.31.0.0/16 and +- 192.168.16.0/20 - 192.168.24.0/20 - Ok(Self { - name: network_name, - config, - }) +after {NUM_TRIES} tries" + ) + .into()) + } +} + +const SUBNET_SPLIT: u32 = (31 - 18 + 1) * 256; +const SUBNET_MAX: u32 = SUBNET_SPLIT + (255 - 16 + 1); + +fn subnet(n: u32) -> String { + assert!(n < SUBNET_MAX); + + // use subnets that `docker network create` would use like + // - 172.18.0.0/16 .. 172.31.0.0/16 + // - 192.168.16.0/20 .. 192.168.240.0/20 + // + // but split in smaller subnets + // + // on Linux, 172.17.0.0/16 is used as the default "bridge" network (see `docker network list`) + // so we don't use that subnet + if n < SUBNET_SPLIT { + let a = 18 + (n / 256); + let b = n % 256; + + format!("172.{a}.{b}.0/24") + } else { + let n = n - SUBNET_SPLIT; + let a = 16 + n; + + format!("192.168.{a}.0/24") } } @@ -84,27 +142,6 @@ pub struct NetworkConfig { 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); @@ -157,4 +194,30 @@ mod tests { Ok(()) } + + #[test] + fn stress() { + let mut networks = vec![]; + for index in 0..256 { + let network = Network::new().unwrap_or_else(|e| panic!("{}: {e}", index)); + eprintln!("{}", network.0.config.subnet); + networks.push(network); + } + } + + #[test] + fn subnet_works() { + assert_eq!("172.18.0.0/24", subnet(0)); + assert_eq!("172.18.1.0/24", subnet(1)); + assert_eq!("172.31.255.0/24", subnet(14 * 256 - 1)); + assert_eq!("192.168.16.0/24", subnet(14 * 256)); + assert_eq!("192.168.17.0/24", subnet(14 * 256 + 1)); + assert_eq!("192.168.255.0/24", subnet(14 * 256 + 239)); + } + + #[test] + #[should_panic] + fn subnet_overflows() { + let _boom = subnet(14 * 256 + 240); + } }