Skip to content
This repository has been archived by the owner on Jun 7, 2024. It is now read-only.

use smaller Docker networks #40

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/dns-test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
143 changes: 103 additions & 40 deletions packages/dns-test/src/container/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use std::{
},
};

use rand::Rng;

use crate::Result;

/// Represents a network in which to put containers into.
Expand Down Expand Up @@ -50,31 +52,87 @@ impl Drop for NetworkInner {

impl NetworkInner {
pub fn new(pid: u32, network_name: &str) -> Result<Self> {
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")
}
}

Expand All @@ -84,27 +142,6 @@ pub struct NetworkConfig {
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);

Expand Down Expand Up @@ -157,4 +194,30 @@ mod tests {

Ok(())
}

#[test]
fn stress() {
let mut networks = vec![];
Copy link
Collaborator

@justahero justahero May 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test fails on my Macbook. There are 5 other Docker networks present unrelated to this project when I run this test.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are those networks created by default in a fresh macOS installation? what are their subnet masks? perhaps we should docker network inspect the output of docker network ls and use the subnet masks not shown there.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the collection of information on my Macbook.

$ docker network ls
NETWORK ID     NAME                             DRIVER    SCOPE
71ce00f0ea3c   bridge                           bridge    local
4ddc8f3c484c   customer-portal_cp_net           bridge    local
eb3ec8e5c769   download-server_criticalup_net   bridge    local
05a125eec965   host                             host      local
e40dd388a572   none                             null      local

Subnet masks for the first three entries are:

172.17.0.0/16
172.20.0.0/16
172.21.0.0/16

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);
}
}