Consolidate Docker test harness infrastructure

Replace 4 near-identical per-harness Docker setups with unified shared
infrastructure. Net result: -463 lines across 55 files, faster CI
(8.5 min vs ~13.5 min), 14 scenarios (down from 21).

Unified Docker image (testing/docker/):
- Single Dockerfile (trixie-slim) with FIPS_TEST_MODE env var for
  mode dispatch: default, chaos, sidecar, tor-socks5, tor-directory
- Single entrypoint.sh with conditional logic per mode
- Replaces 5 Dockerfiles, 3 entrypoints, 4 resolv.conf copies

Shared build and libraries (testing/scripts/, testing/lib/):
- testing/scripts/build.sh: single build script with macOS zigbuild
  support, replaces 4 per-harness copies
- testing/lib/derive_keys.py: shared key derivation module, replaces
  3 copies of derive-keys.py
- testing/lib/log_analysis.py: shared log analysis extracted from
  chaos sim/logs.py, with CLI interface and rekey cutover tracking
- testing/lib/wait-converge.sh: shared convergence wait helpers
  (wait_for_links, wait_for_peers) using fipsctl JSON polling

Scenario consolidation:
- Remove 7 redundant scenarios: tcp-chain (subsumed by tcp-mesh),
  tcp-only (subsumed by tcp-mesh), chaos-10 (replaced by
  churn-mixed --nodes 10), churn-10/churn-20/churn-20-mixed
  (subsumed by parameterized churn-mixed), cost-mixed-7node
  (overlaps mixed-technology)
- Add churn-mixed scenario with --nodes flag for scale testing
- Reduce idle scenario durations: smoke-10 60s→30s,
  ethernet-only 90s→30s, cost-avoidance 120s→45s,
  depth-vs-cost 120s→45s, bottleneck-parent 120s→60s,
  mixed-technology 180s→90s

CI updates:
- ci-local.sh: unified image build, structured chaos suite entries
  with per-scenario flags, --skip-build for sidecar
- ci.yml: shared binary install + image build step, updated scenario
  matrix, chaos_flags support for parameterized scenarios
- Add tcp-mesh and congestion-stress to CI matrix
- Static ping test uses active peer convergence detection instead
  of hardcoded 5s sleep

Chaos infrastructure improvements (from discovery-rework branch):
- Pre-built Docker image instead of per-service build at scale
- --nodes flag in chaos.sh for runtime topology size override
This commit is contained in:
Johnathan Corgan
2026-03-19 18:08:36 +00:00
parent c8b7459fbc
commit 9e63b42bd9
55 changed files with 823 additions and 1286 deletions
-34
View File
@@ -1,34 +0,0 @@
FROM debian:bookworm-slim
RUN apt-get update && \
apt-get install -y --no-install-recommends \
iproute2 iputils-ping dnsutils openssh-client openssh-server iperf3 \
dnsmasq curl python3 rsync && \
rm -rf /var/lib/apt/lists/*
# Setup SSH server with no authentication (test only!)
RUN mkdir -p /var/run/sshd && \
ssh-keygen -A && \
sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config && \
sed -i 's/#PermitEmptyPasswords no/PermitEmptyPasswords yes/' /etc/ssh/sshd_config && \
sed -i 's/UsePAM yes/UsePAM no/' /etc/ssh/sshd_config && \
passwd -d root
# dnsmasq: forward .fips to FIPS daemon, everything else to Docker DNS
RUN printf '%s\n' \
'port=53' \
'listen-address=127.0.0.1' \
'bind-interfaces' \
'server=/fips/127.0.0.1#5354' \
'server=127.0.0.11' \
'no-resolv' \
>> /etc/dnsmasq.conf
COPY fips fipsctl /usr/local/bin/
RUN chmod +x /usr/local/bin/fips /usr/local/bin/fipsctl
# Static web page served via Python HTTP server
RUN printf 'Fuck IPs!\n' > /root/index.html
# Start dnsmasq, SSH server, iperf3, and HTTP server in background, then run FIPS
ENTRYPOINT ["/bin/bash", "-c", "dnsmasq && /usr/sbin/sshd && iperf3 -s -D && python3 -m http.server 8000 -d /root -b :: &>/dev/null & exec fips --config /etc/fips/fips.yaml"]
+24 -25
View File
@@ -6,8 +6,7 @@ networks:
- subnet: 172.20.0.0/24
x-fips-common: &fips-common
build:
context: .
image: fips-test:latest
cap_add:
- NET_ADMIN
devices:
@@ -28,7 +27,7 @@ services:
container_name: fips-node-a
hostname: node-a
volumes:
- ./resolv.conf:/etc/resolv.conf:ro
- ../docker/resolv.conf:/etc/resolv.conf:ro
- ./generated-configs/mesh/node-a.yaml:/etc/fips/fips.yaml:ro
networks:
fips-net:
@@ -40,7 +39,7 @@ services:
container_name: fips-node-b
hostname: node-b
volumes:
- ./resolv.conf:/etc/resolv.conf:ro
- ../docker/resolv.conf:/etc/resolv.conf:ro
- ./generated-configs/mesh/node-b.yaml:/etc/fips/fips.yaml:ro
networks:
fips-net:
@@ -52,7 +51,7 @@ services:
container_name: fips-node-c
hostname: node-c
volumes:
- ./resolv.conf:/etc/resolv.conf:ro
- ../docker/resolv.conf:/etc/resolv.conf:ro
- ./generated-configs/mesh/node-c.yaml:/etc/fips/fips.yaml:ro
networks:
fips-net:
@@ -64,7 +63,7 @@ services:
container_name: fips-node-d
hostname: node-d
volumes:
- ./resolv.conf:/etc/resolv.conf:ro
- ../docker/resolv.conf:/etc/resolv.conf:ro
- ./generated-configs/mesh/node-d.yaml:/etc/fips/fips.yaml:ro
networks:
fips-net:
@@ -76,7 +75,7 @@ services:
container_name: fips-node-e
hostname: node-e
volumes:
- ./resolv.conf:/etc/resolv.conf:ro
- ../docker/resolv.conf:/etc/resolv.conf:ro
- ./generated-configs/mesh/node-e.yaml:/etc/fips/fips.yaml:ro
networks:
fips-net:
@@ -89,7 +88,7 @@ services:
container_name: fips-node-a
hostname: node-a
volumes:
- ./resolv.conf:/etc/resolv.conf:ro
- ../docker/resolv.conf:/etc/resolv.conf:ro
- ./generated-configs/mesh-public/node-a.yaml:/etc/fips/fips.yaml:ro
networks:
fips-net:
@@ -101,7 +100,7 @@ services:
container_name: fips-node-b
hostname: node-b
volumes:
- ./resolv.conf:/etc/resolv.conf:ro
- ../docker/resolv.conf:/etc/resolv.conf:ro
- ./generated-configs/mesh-public/node-b.yaml:/etc/fips/fips.yaml:ro
networks:
fips-net:
@@ -113,7 +112,7 @@ services:
container_name: fips-node-c
hostname: node-c
volumes:
- ./resolv.conf:/etc/resolv.conf:ro
- ../docker/resolv.conf:/etc/resolv.conf:ro
- ./generated-configs/mesh-public/node-c.yaml:/etc/fips/fips.yaml:ro
networks:
fips-net:
@@ -125,7 +124,7 @@ services:
container_name: fips-node-d
hostname: node-d
volumes:
- ./resolv.conf:/etc/resolv.conf:ro
- ../docker/resolv.conf:/etc/resolv.conf:ro
- ./generated-configs/mesh-public/node-d.yaml:/etc/fips/fips.yaml:ro
networks:
fips-net:
@@ -137,7 +136,7 @@ services:
container_name: fips-node-e
hostname: node-e
volumes:
- ./resolv.conf:/etc/resolv.conf:ro
- ../docker/resolv.conf:/etc/resolv.conf:ro
- ./generated-configs/mesh-public/node-e.yaml:/etc/fips/fips.yaml:ro
networks:
fips-net:
@@ -150,7 +149,7 @@ services:
container_name: fips-node-a
hostname: node-a
volumes:
- ./resolv.conf:/etc/resolv.conf:ro
- ../docker/resolv.conf:/etc/resolv.conf:ro
- ./generated-configs/chain/node-a.yaml:/etc/fips/fips.yaml:ro
networks:
fips-net:
@@ -162,7 +161,7 @@ services:
container_name: fips-node-b
hostname: node-b
volumes:
- ./resolv.conf:/etc/resolv.conf:ro
- ../docker/resolv.conf:/etc/resolv.conf:ro
- ./generated-configs/chain/node-b.yaml:/etc/fips/fips.yaml:ro
networks:
fips-net:
@@ -174,7 +173,7 @@ services:
container_name: fips-node-c
hostname: node-c
volumes:
- ./resolv.conf:/etc/resolv.conf:ro
- ../docker/resolv.conf:/etc/resolv.conf:ro
- ./generated-configs/chain/node-c.yaml:/etc/fips/fips.yaml:ro
networks:
fips-net:
@@ -186,7 +185,7 @@ services:
container_name: fips-node-d
hostname: node-d
volumes:
- ./resolv.conf:/etc/resolv.conf:ro
- ../docker/resolv.conf:/etc/resolv.conf:ro
- ./generated-configs/chain/node-d.yaml:/etc/fips/fips.yaml:ro
networks:
fips-net:
@@ -198,7 +197,7 @@ services:
container_name: fips-node-e
hostname: node-e
volumes:
- ./resolv.conf:/etc/resolv.conf:ro
- ../docker/resolv.conf:/etc/resolv.conf:ro
- ./generated-configs/chain/node-e.yaml:/etc/fips/fips.yaml:ro
networks:
fips-net:
@@ -211,7 +210,7 @@ services:
container_name: fips-node-a
hostname: node-a
volumes:
- ./resolv.conf:/etc/resolv.conf:ro
- ../docker/resolv.conf:/etc/resolv.conf:ro
- ./generated-configs/rekey/node-a.yaml:/etc/fips/fips.yaml:ro
networks:
fips-net:
@@ -223,7 +222,7 @@ services:
container_name: fips-node-b
hostname: node-b
volumes:
- ./resolv.conf:/etc/resolv.conf:ro
- ../docker/resolv.conf:/etc/resolv.conf:ro
- ./generated-configs/rekey/node-b.yaml:/etc/fips/fips.yaml:ro
networks:
fips-net:
@@ -235,7 +234,7 @@ services:
container_name: fips-node-c
hostname: node-c
volumes:
- ./resolv.conf:/etc/resolv.conf:ro
- ../docker/resolv.conf:/etc/resolv.conf:ro
- ./generated-configs/rekey/node-c.yaml:/etc/fips/fips.yaml:ro
networks:
fips-net:
@@ -247,7 +246,7 @@ services:
container_name: fips-node-d
hostname: node-d
volumes:
- ./resolv.conf:/etc/resolv.conf:ro
- ../docker/resolv.conf:/etc/resolv.conf:ro
- ./generated-configs/rekey/node-d.yaml:/etc/fips/fips.yaml:ro
networks:
fips-net:
@@ -259,7 +258,7 @@ services:
container_name: fips-node-e
hostname: node-e
volumes:
- ./resolv.conf:/etc/resolv.conf:ro
- ../docker/resolv.conf:/etc/resolv.conf:ro
- ./generated-configs/rekey/node-e.yaml:/etc/fips/fips.yaml:ro
networks:
fips-net:
@@ -272,7 +271,7 @@ services:
container_name: fips-node-a
hostname: node-a
volumes:
- ./resolv.conf:/etc/resolv.conf:ro
- ../docker/resolv.conf:/etc/resolv.conf:ro
- ./generated-configs/tcp-chain/node-a.yaml:/etc/fips/fips.yaml:ro
networks:
fips-net:
@@ -284,7 +283,7 @@ services:
container_name: fips-node-b
hostname: node-b
volumes:
- ./resolv.conf:/etc/resolv.conf:ro
- ../docker/resolv.conf:/etc/resolv.conf:ro
- ./generated-configs/tcp-chain/node-b.yaml:/etc/fips/fips.yaml:ro
networks:
fips-net:
@@ -296,7 +295,7 @@ services:
container_name: fips-node-c
hostname: node-c
volumes:
- ./resolv.conf:/etc/resolv.conf:ro
- ../docker/resolv.conf:/etc/resolv.conf:ro
- ./generated-configs/tcp-chain/node-c.yaml:/etc/fips/fips.yaml:ro
networks:
fips-net:
-1
View File
@@ -1 +0,0 @@
nameserver 127.0.0.1
-73
View File
@@ -1,73 +0,0 @@
#!/bin/bash
# Build the FIPS binary, generate configs, and build Docker images.
# Supports cross-compilation from macOS to Linux using cargo-zigbuild.
# Usage: ./build.sh [topology] [mesh-name]
# topology: mesh, mesh-public, chain, etc. (default: mesh)
# mesh-name: optional; derives unique node identities via sha256(mesh-name|node-id)
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
DOCKER_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
# Topology to use (default: mesh)
TOPOLOGY="${1:-mesh}"
MESH_NAME="${2:-}"
# Find project root (directory containing Cargo.toml)
PROJECT_ROOT="$(cd "$DOCKER_DIR/../.." && pwd)"
if [ ! -f "$PROJECT_ROOT/Cargo.toml" ]; then
echo "Error: Cannot find Cargo.toml at $PROJECT_ROOT" >&2
echo "Expected layout: <project-root>/testing/static/scripts/build.sh" >&2
exit 1
fi
echo "Using topology: $TOPOLOGY"
# Detect host OS
UNAME_S=$(uname -s)
CARGO_TARGET="x86_64-unknown-linux-musl"
# Check for cross-compilation tooling on macOS
if [ "$UNAME_S" = "Darwin" ]; then
echo "Detected macOS host - using cross-compilation for Linux..."
# Check if cargo-zigbuild is installed
if ! command -v cargo-zigbuild &> /dev/null; then
echo "Error: cargo-zigbuild not found." >&2
echo "Please install it: cargo install cargo-zigbuild" >&2
echo "" >&2
echo "Or install zig directly: brew install zig" >&2
exit 1
fi
# Check if target is installed
if ! rustup target list --installed | grep -q "$CARGO_TARGET"; then
echo "Installing Rust target $CARGO_TARGET..."
rustup target add "$CARGO_TARGET"
fi
echo "Building FIPS for Linux (release) using cargo-zigbuild..."
cargo zigbuild --release --target "$CARGO_TARGET" --manifest-path="$PROJECT_ROOT/Cargo.toml"
echo "Copying binaries to docker context..."
cp "$PROJECT_ROOT/target/$CARGO_TARGET/release/fips" "$DOCKER_DIR/fips"
cp "$PROJECT_ROOT/target/$CARGO_TARGET/release/fipsctl" "$DOCKER_DIR/fipsctl"
else
# Native Linux build
echo "Building FIPS (release)..."
cargo build --release --manifest-path="$PROJECT_ROOT/Cargo.toml"
echo "Copying binaries to docker context..."
cp "$PROJECT_ROOT/target/release/fips" "$DOCKER_DIR/fips"
cp "$PROJECT_ROOT/target/release/fipsctl" "$DOCKER_DIR/fipsctl"
fi
echo "Done. Binaries at $DOCKER_DIR/{fips,fipsctl}"
echo ""
echo "Generating node configurations from templates..."
"$SCRIPT_DIR/generate-configs.sh" "$TOPOLOGY" $MESH_NAME
echo ""
echo "Building Docker images..."
docker compose -f "$DOCKER_DIR/docker-compose.yml" --profile "$TOPOLOGY" build
echo ""
echo "Ready: docker compose -f testing/static/docker-compose.yml --profile $TOPOLOGY up -d"
-111
View File
@@ -1,111 +0,0 @@
#!/usr/bin/env python3
"""Derive deterministic nostr nsec/npub from mesh-name and node-name.
Usage: derive-keys.py <mesh-name> <node-name>
Output: nsec=<hex>\nnpub=<bech32>
Derivation: nsec = sha256(mesh_name + "|" + node_name)
npub = bech32("npub", secp256k1_pubkey_x(nsec))
Pure Python, no external dependencies.
"""
import hashlib
import sys
# --- secp256k1 ---
P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
def _modinv(a, m):
return pow(a, m - 2, m)
def _point_add(p1, p2):
if p1 is None:
return p2
if p2 is None:
return p1
x1, y1 = p1
x2, y2 = p2
if x1 == x2 and y1 != y2:
return None
if x1 == x2:
lam = (3 * x1 * x1) * _modinv(2 * y1, P) % P
else:
lam = (y2 - y1) * _modinv(x2 - x1, P) % P
x3 = (lam * lam - x1 - x2) % P
y3 = (lam * (x1 - x3) - y1) % P
return (x3, y3)
def _scalar_mult(k, point):
result = None
addend = point
while k:
if k & 1:
result = _point_add(result, addend)
addend = _point_add(addend, addend)
k >>= 1
return result
# --- bech32 (BIP-173) ---
_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
def _bech32_polymod(values):
gen = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3]
chk = 1
for v in values:
b = chk >> 25
chk = (chk & 0x1FFFFFF) << 5 ^ v
for i in range(5):
chk ^= gen[i] if ((b >> i) & 1) else 0
return chk
def _bech32_encode(hrp, data_5bit):
hrp_expand = [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
polymod = _bech32_polymod(hrp_expand + data_5bit + [0] * 6) ^ 1
checksum = [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
return hrp + "1" + "".join(_CHARSET[d] for d in data_5bit + checksum)
def _convertbits(data, frombits, tobits):
acc, bits, ret = 0, 0, []
maxv = (1 << tobits) - 1
for value in data:
acc = (acc << frombits) | value
bits += frombits
while bits >= tobits:
bits -= tobits
ret.append((acc >> bits) & maxv)
if bits:
ret.append((acc << (tobits - bits)) & maxv)
return ret
# --- public API ---
def derive(mesh_name, node_name):
nsec_hex = hashlib.sha256(f"{mesh_name}|{node_name}".encode()).hexdigest()
k = int(nsec_hex, 16)
pub = _scalar_mult(k, (Gx, Gy))
x_hex = format(pub[0], "064x")
data_5bit = _convertbits(list(bytes.fromhex(x_hex)), 8, 5)
npub = _bech32_encode("npub", data_5bit)
return nsec_hex, npub
if __name__ == "__main__":
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <mesh-name> <node-name>", file=sys.stderr)
sys.exit(1)
nsec, npub = derive(sys.argv[1], sys.argv[2])
print(f"nsec={nsec}")
print(f"npub={npub}")
+1 -1
View File
@@ -12,7 +12,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_DIR="$SCRIPT_DIR/../configs"
GENERATED_DIR="$SCRIPT_DIR/../generated-configs"
TEMPLATE_FILE="$CONFIG_DIR/node.template.yaml"
DERIVE_KEYS="$SCRIPT_DIR/derive-keys.py"
DERIVE_KEYS="$SCRIPT_DIR/../../lib/derive_keys.py"
# Parse topology YAML to extract node attributes
# Usage: get_node_attr <topology_file> <node_id> <attr_name>
+10 -3
View File
@@ -18,6 +18,7 @@ FAILED=0
# Node identities (from generated env file)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../../lib/wait-converge.sh"
ENV_FILE="$SCRIPT_DIR/../generated-configs/npubs.env"
if [ ! -f "$ENV_FILE" ]; then
echo "Error: $ENV_FILE not found. Run generate-configs.sh first." >&2
@@ -51,9 +52,15 @@ ping_test() {
echo "=== FIPS Ping Test ($PROFILE topology) ==="
echo ""
# Wait for nodes to converge
echo "Waiting 5s for mesh convergence..."
sleep 5
# Wait for nodes to converge — peers + discovery propagation
echo "Waiting for mesh convergence..."
if [ "$PROFILE" = "chain" ]; then
wait_for_peers fips-node-b 2 15 || true
else
wait_for_peers fips-node-a 2 15 || true
fi
# Allow extra time for discovery to propagate across the mesh
sleep 3
if [ "$PROFILE" = "mesh" ] || [ "$PROFILE" = "mesh-public" ]; then
# Sparse mesh topology: A-B, B-C, C-D, D-E, E-A, A-D