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

View File

@@ -125,8 +125,8 @@ jobs:
# Runs only when both build and test succeed. Each topology / scenario is a
# separate matrix entry so they run in parallel.
#
# Static topologies → build Docker images, start containers, ping-test
# Chaos scenarios → build sim image, run stochastic simulation
# All harnesses share a single Docker image (fips-test:latest) built once
# in the setup step from testing/docker/.
# ─────────────────────────────────────────────────────────────────────────────
integration:
name: Integration (${{ matrix.suite }})
@@ -153,24 +153,25 @@ jobs:
- suite: chaos-smoke-10
type: chaos
scenario: smoke-10
- suite: chaos-10
- suite: churn-mixed-10
type: chaos
scenario: chaos-10
scenario: churn-mixed
chaos_flags: "--nodes 10 --duration 120"
- suite: ethernet-mesh
type: chaos
scenario: ethernet-mesh
- suite: ethernet-only
type: chaos
scenario: ethernet-only
- suite: tcp-mesh
type: chaos
scenario: tcp-mesh
- suite: bottleneck-parent
type: chaos
scenario: bottleneck-parent
- suite: cost-avoidance
type: chaos
scenario: cost-avoidance
- suite: cost-mixed-7node
type: chaos
scenario: cost-mixed-7node
- suite: cost-reeval
type: chaos
scenario: cost-reeval
@@ -183,6 +184,9 @@ jobs:
- suite: mixed-technology
type: chaos
scenario: mixed-technology
- suite: congestion-stress
type: chaos
scenario: congestion-stress
# ── Sidecar deployment ──────────────────────────────────────────
- suite: sidecar
type: sidecar
@@ -197,24 +201,22 @@ jobs:
name: fips-linux
path: _bin
# ── Static topology ────────────────────────────────────────────────────
- name: Install binary (static)
if: matrix.type == 'static'
# Install binaries to unified docker context and build shared image
- name: Install binaries and build Docker image
run: |
chmod +x _bin/fips _bin/fipsctl
cp _bin/fips testing/static/fips
cp _bin/fipsctl testing/static/fipsctl
[ -f _bin/fipstop ] && chmod +x _bin/fipstop || true
cp _bin/fips testing/docker/fips
cp _bin/fipsctl testing/docker/fipsctl
[ -f _bin/fipstop ] && cp _bin/fipstop testing/docker/fipstop || true
docker build -t fips-test:latest testing/docker
docker build -t fips-test-app:latest -f testing/docker/Dockerfile.app testing/docker
# ── Static topology ────────────────────────────────────────────────────
- name: Generate configs (static)
if: matrix.type == 'static'
run: bash testing/static/scripts/generate-configs.sh ${{ matrix.topology }}
- name: Build Docker images (static)
if: matrix.type == 'static'
run: |
docker compose -f testing/static/docker-compose.yml \
--profile ${{ matrix.topology }} build
- name: Start containers (static)
if: matrix.type == 'static'
run: |
@@ -238,25 +240,12 @@ jobs:
--profile ${{ matrix.topology }} down --volumes --remove-orphans
# ── Rekey integration test ──────────────────────────────────────────────
- name: Install binary (rekey)
if: matrix.type == 'rekey'
run: |
chmod +x _bin/fips _bin/fipsctl
cp _bin/fips testing/static/fips
cp _bin/fipsctl testing/static/fipsctl
- name: Generate and inject configs (rekey)
if: matrix.type == 'rekey'
run: |
bash testing/static/scripts/generate-configs.sh rekey
bash testing/static/scripts/rekey-test.sh inject-config
- name: Build Docker images (rekey)
if: matrix.type == 'rekey'
run: |
docker compose -f testing/static/docker-compose.yml \
--profile rekey build
- name: Start containers (rekey)
if: matrix.type == 'rekey'
run: |
@@ -284,20 +273,9 @@ jobs:
if: matrix.type == 'chaos'
run: pip3 install --quiet pyyaml jinja2
- name: Install binary (chaos)
if: matrix.type == 'chaos'
run: |
chmod +x _bin/fips _bin/fipsctl
cp _bin/fips testing/chaos/fips
cp _bin/fipsctl testing/chaos/fipsctl
- name: Build chaos Docker image
if: matrix.type == 'chaos'
run: docker build -t fips-chaos:latest testing/chaos
- name: Run chaos scenario
if: matrix.type == 'chaos'
run: bash testing/chaos/scripts/chaos.sh ${{ matrix.scenario }}
run: bash testing/chaos/scripts/chaos.sh ${{ matrix.scenario }} ${{ matrix.chaos_flags }}
- name: Upload sim results on failure (chaos)
if: matrix.type == 'chaos' && failure()
@@ -308,17 +286,9 @@ jobs:
retention-days: 7
# ── Sidecar deployment ──────────────────────────────────────────────
- name: Install binary (sidecar)
if: matrix.type == 'sidecar'
run: |
chmod +x _bin/fips _bin/fipsctl _bin/fipstop
cp _bin/fips testing/sidecar/fips
cp _bin/fipsctl testing/sidecar/fipsctl
cp _bin/fipstop testing/sidecar/fipstop
- name: Run sidecar integration test
if: matrix.type == 'sidecar'
run: bash testing/sidecar/scripts/test-sidecar.sh
run: bash testing/sidecar/scripts/test-sidecar.sh --skip-build
- name: Collect logs on failure (sidecar)
if: matrix.type == 'sidecar' && failure()
@@ -327,4 +297,4 @@ jobs:
echo "--- sidecar-${node} logs ---"
docker logs "sidecar-${node}-fips-1" 2>&1 || true
echo ""
done
done

8
.gitignore vendored
View File

@@ -22,4 +22,10 @@ reference/
dist/
*.ipk
sim-results/
sim-results/
# Python
__pycache__/
*.py[cod]
*.egg-info/
*.egg

View File

@@ -1,36 +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
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
# Static web page served via Python HTTP server
RUN printf 'Fuck IPs!\n' > /root/index.html
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

View File

@@ -1,57 +0,0 @@
#!/bin/bash
# Container entrypoint: start services and wait for Ethernet interfaces.
#
# If the FIPS config references Ethernet transports, wait for the
# interfaces to appear before starting the FIPS daemon. This handles
# the case where veth pairs are created from the host after the
# container starts.
set -e
# Enable TCP ECN negotiation for both IPv4 and IPv6 connections.
# Despite the "ipv4" name, this sysctl controls ECN for all TCP.
# Without this, IPv6 packets traversing the FIPS mesh carry ECN=0b00
# (Not-ECT), and mark_ipv6_ecn_ce() is a no-op per RFC 3168.
sysctl -w net.ipv4.tcp_ecn=1 >/dev/null 2>&1 || true
# Start background services
dnsmasq
/usr/sbin/sshd
iperf3 -s -D
python3 -m http.server 8000 -d /root -b :: &>/dev/null &
CONFIG="/etc/fips/fips.yaml"
# Extract Ethernet interface names from the config file.
# Matches "interface: <name>" lines that appear under transports.ethernet.
# The sed strips the key prefix and any whitespace.
ETH_IFACES=""
if grep -q 'ethernet:' "$CONFIG" 2>/dev/null; then
ETH_IFACES=$(grep '^\s*interface:' "$CONFIG" \
| sed 's/.*interface:\s*//' \
| tr -d ' ' || true)
fi
if [ -n "$ETH_IFACES" ]; then
echo "Waiting for Ethernet interfaces: $ETH_IFACES"
DEADLINE=$((SECONDS + 30))
while [ $SECONDS -lt $DEADLINE ]; do
ALL_FOUND=true
for iface in $ETH_IFACES; do
if [ ! -e "/sys/class/net/$iface" ]; then
ALL_FOUND=false
break
fi
done
if $ALL_FOUND; then
echo "All Ethernet interfaces ready"
break
fi
sleep 0.2
done
if ! $ALL_FOUND; then
echo "WARNING: Timed out waiting for Ethernet interfaces"
fi
fi
exec fips --config "$CONFIG"

View File

@@ -35,7 +35,7 @@
scenario:
name: "bottleneck-parent"
seed: 42
duration_secs: 120
duration_secs: 60
topology:
algorithm: explicit

View File

@@ -1,53 +0,0 @@
scenario:
name: "chaos-10"
seed: 42
duration_secs: 120
topology:
num_nodes: 10
algorithm: random_geometric
params:
radius: 0.5
ensure_connected: true
subnet: "172.20.0.0/24"
ip_start: 10
netem:
enabled: true
default_policy:
delay_ms: { min: 5, max: 50 }
jitter_ms: { min: 1, max: 10 }
loss_pct: { min: 0, max: 2 }
mutation:
interval_secs: { min: 15, max: 30 }
fraction: 0.3
policies:
normal:
delay_ms: [5, 20]
loss_pct: [0, 1]
degraded:
delay_ms: [50, 100]
jitter_ms: [10, 30]
loss_pct: [3, 8]
terrible:
delay_ms: [100, 200]
jitter_ms: [30, 60]
loss_pct: [10, 20]
link_flaps:
enabled: true
interval_secs: { min: 20, max: 60 }
max_down_links: 2
down_duration_secs: { min: 10, max: 30 }
protect_connectivity: true
traffic:
enabled: true
max_concurrent: 3
interval_secs: { min: 10, max: 30 }
duration_secs: { min: 5, max: 15 }
parallel_streams: 4
logging:
rust_log: "info"
output_dir: "./sim-results"

View File

@@ -1,56 +0,0 @@
scenario:
name: "churn-10"
seed: 42
duration_secs: 600
topology:
num_nodes: 10
algorithm: random_geometric
params:
radius: 0.5
ensure_connected: true
subnet: "172.20.0.0/24"
ip_start: 10
netem:
enabled: true
default_policy:
delay_ms: { min: 5, max: 50 }
jitter_ms: { min: 1, max: 10 }
loss_pct: { min: 0, max: 2 }
mutation:
interval_secs: { min: 20, max: 45 }
fraction: 0.3
policies:
normal:
delay_ms: [5, 20]
loss_pct: [0, 1]
degraded:
delay_ms: [50, 100]
jitter_ms: [10, 30]
loss_pct: [3, 8]
link_flaps:
enabled: true
interval_secs: { min: 30, max: 60 }
max_down_links: 2
down_duration_secs: { min: 10, max: 30 }
protect_connectivity: true
traffic:
enabled: true
max_concurrent: 3
interval_secs: { min: 10, max: 30 }
duration_secs: { min: 5, max: 15 }
parallel_streams: 4
node_churn:
enabled: true
interval_secs: { min: 60, max: 180 }
max_down_nodes: 1
down_duration_secs: { min: 30, max: 90 }
protect_connectivity: true
logging:
rust_log: "debug"
output_dir: "./sim-results"

View File

@@ -1,60 +0,0 @@
scenario:
name: "churn-20"
seed: 42
duration_secs: 600
topology:
num_nodes: 20
algorithm: erdos_renyi
params:
p: 0.3
ensure_connected: true
subnet: "172.20.0.0/23"
ip_start: 10
netem:
enabled: true
default_policy:
delay_ms: { min: 5, max: 50 }
jitter_ms: { min: 1, max: 10 }
loss_pct: { min: 0, max: 2 }
mutation:
interval_secs: { min: 20, max: 45 }
fraction: 0.3
policies:
normal:
delay_ms: [5, 20]
loss_pct: [0, 1]
degraded:
delay_ms: [50, 100]
jitter_ms: [10, 30]
loss_pct: [3, 8]
link_flaps:
enabled: true
interval_secs: { min: 30, max: 60 }
max_down_links: 3
down_duration_secs: { min: 10, max: 30 }
protect_connectivity: true
traffic:
enabled: true
max_concurrent: 10
interval_secs: { min: 0, max: 30 }
duration_secs: { min: 5, max: 90 }
parallel_streams: 4
node_churn:
enabled: true
interval_secs: { min: 60, max: 90 }
max_down_nodes: 5
down_duration_secs: { min: 30, max: 90 }
protect_connectivity: false
bandwidth:
enabled: true
tiers_mbps: [1, 10, 100, 1000]
logging:
rust_log: "debug"
output_dir: "./sim-results"

View File

@@ -1,11 +1,13 @@
# 20-node churn test with mixed transports (UDP, Ethernet, TCP)
# Parameterized churn test with mixed transports (UDP, Ethernet, TCP)
#
# Based on churn-20 but with transport_mix assigning each edge a
# random transport type by weight. Tests spanning tree convergence,
# link flaps, node churn, and traffic across heterogeneous transports.
# Default: 20 nodes. Override num_nodes with --nodes flag in chaos.sh,
# or copy this file and adjust topology.num_nodes directly.
#
# Tests spanning tree convergence, link flaps, node churn, and traffic
# across heterogeneous transports at configurable scale.
scenario:
name: "churn-20-mixed"
name: "churn-mixed"
seed: 42
duration_secs: 600
@@ -15,7 +17,7 @@ topology:
params:
p: 0.3
ensure_connected: true
subnet: "172.20.0.0/23"
subnet: "172.20.0.0/16"
ip_start: 10
transport_mix:
udp: 0.6

View File

@@ -22,7 +22,7 @@
scenario:
name: "cost-avoidance"
seed: 42
duration_secs: 120
duration_secs: 45
topology:
algorithm: explicit

View File

@@ -1,78 +0,0 @@
# Cost-Based Parent Selection: Mixed Technology 7-Node Test
#
# Topology (explicit, multiple link types):
#
# n01 (root)
# / | \
# fiber | fiber BT
# / | \
# n02 n03 n04
# | | \ |
# fiber fiber \ fiber
# | | wifi |
# n05 n06 n07
#
# Cross-links: n03-n05 (fiber), n04-n06 (wifi)
#
# Test subjects:
# - n06 has edges to n03 (fiber) and n04 (wifi) — should prefer n03
# - n04's link to root is Bluetooth (L2CAP), so n04 has higher-cost parent link
# - n07 connects only to n04 (no choice, stuck with Bluetooth upstream)
#
# Validation: tree snapshot shows n06's parent is n03 (not n04).
scenario:
name: "cost-mixed-7node"
seed: 42
duration_secs: 180
topology:
algorithm: explicit
num_nodes: 7
params:
adjacency:
- [n01, n02]
- [n01, n03]
- [n01, n04]
- [n02, n05]
- [n03, n06]
- [n04, n07]
- [n03, n05]
- [n04, n06]
subnet: "172.20.0.0/24"
ip_start: 10
netem:
enabled: true
default_policy:
# Fiber-like
delay_ms: [1, 5]
jitter_ms: [0, 1]
loss_pct: [0, 0.5]
link_policies:
# Bluetooth (L2CAP) link from n01 to n04
- edges: ["n01-n04"]
policy:
delay_ms: [15, 40]
jitter_ms: [5, 15]
loss_pct: [2, 8]
# WiFi link from n04 to n06
- edges: ["n04-n06"]
policy:
delay_ms: [5, 20]
jitter_ms: [2, 5]
loss_pct: [1, 3]
link_flaps:
enabled: false
traffic:
enabled: true
max_concurrent: 3
interval_secs: {min: 10, max: 25}
duration_secs: {min: 5, max: 15}
parallel_streams: 4
logging:
rust_log: "info"
output_dir: "./sim-results"

View File

@@ -27,7 +27,7 @@
scenario:
name: "depth-vs-cost"
seed: 42
duration_secs: 120
duration_secs: 45
topology:
algorithm: explicit

View File

@@ -15,7 +15,7 @@
scenario:
name: "ethernet-only"
seed: 42
duration_secs: 90
duration_secs: 30
topology:
algorithm: explicit

View File

@@ -34,7 +34,7 @@
scenario:
name: "mixed-technology"
seed: 42
duration_secs: 180
duration_secs: 90
topology:
algorithm: explicit

View File

@@ -1,7 +1,7 @@
scenario:
name: "smoke-10"
seed: 42
duration_secs: 60
duration_secs: 30
topology:
num_nodes: 10

View File

@@ -1,40 +0,0 @@
# TCP chain: 4-node linear topology, all TCP transport
#
# Tests basic TCP transport connectivity, spanning tree convergence,
# and multi-hop routing over TCP links.
#
# Topology:
#
# n01 ---tcp--- n02 ---tcp--- n03 ---tcp--- n04
scenario:
name: "tcp-chain"
seed: 42
duration_secs: 90
topology:
algorithm: explicit
num_nodes: 4
default_transport: tcp
params:
adjacency:
- [n01, n02]
- [n02, n03]
- [n03, n04]
netem:
enabled: true
default_policy:
delay_ms: [1, 5]
jitter_ms: [0, 1]
loss_pct: [0, 0.5]
link_flaps:
enabled: false
traffic:
enabled: false
logging:
rust_log: "info"
output_dir: "./sim-results"

View File

@@ -1,45 +0,0 @@
# TCP-only: 4-node ring, all TCP transport
#
# Tests pure-TCP mesh with netem impairment. Nodes have no UDP
# transport — only TCP with static peer config.
#
# Topology:
#
# n01 ---tcp--- n02
# | |
# tcp tcp
# | |
# n04 ---tcp--- n03
scenario:
name: "tcp-only"
seed: 42
duration_secs: 90
topology:
algorithm: explicit
num_nodes: 4
default_transport: tcp
params:
adjacency:
- [n01, n02]
- [n02, n03]
- [n03, n04]
- [n04, n01]
netem:
enabled: true
default_policy:
delay_ms: [1, 10]
jitter_ms: [0, 2]
loss_pct: [0, 1]
link_flaps:
enabled: false
traffic:
enabled: false
logging:
rust_log: "info"
output_dir: "./sim-results"

View File

@@ -1,50 +0,0 @@
#!/bin/bash
# Build the FIPS binary for the chaos simulation Docker image.
# Usage: ./scripts/build.sh
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CHAOS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
# Find project root (directory containing Cargo.toml)
PROJECT_ROOT="$(cd "$CHAOS_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/chaos/scripts/build.sh" >&2
exit 1
fi
# Detect host OS
UNAME_S=$(uname -s)
CARGO_TARGET="x86_64-unknown-linux-musl"
if [ "$UNAME_S" = "Darwin" ]; then
echo "Detected macOS host - using cross-compilation for Linux..."
if ! command -v cargo-zigbuild &> /dev/null; then
echo "Error: cargo-zigbuild not found." >&2
echo "Please install it: cargo install cargo-zigbuild" >&2
exit 1
fi
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" "$CHAOS_DIR/fips"
cp "$PROJECT_ROOT/target/$CARGO_TARGET/release/fipsctl" "$CHAOS_DIR/fipsctl"
else
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" "$CHAOS_DIR/fips"
cp "$PROJECT_ROOT/target/release/fipsctl" "$CHAOS_DIR/fipsctl"
fi
echo "Done. Binaries at $CHAOS_DIR/{fips,fipsctl}"

View File

@@ -8,6 +8,7 @@
# -v, --verbose Enable debug logging
# --seed <N> Override scenario seed
# --duration <secs> Override scenario duration
# --nodes <N> Override topology.num_nodes
# --list List available scenarios
#
# Examples:
@@ -33,6 +34,7 @@ usage() {
echo " -v, --verbose Enable debug logging"
echo " --seed <N> Override scenario seed"
echo " --duration <secs> Override scenario duration"
echo " --nodes <N> Override topology.num_nodes"
echo " --list List available scenarios"
exit 1
}
@@ -62,12 +64,14 @@ SCENARIO_ARG=""
VERBOSE=""
SEED=""
DURATION=""
NODES=""
while [ $# -gt 0 ]; do
case "$1" in
-v|--verbose) VERBOSE="--verbose"; shift ;;
--seed) SEED="$2"; shift 2 ;;
--duration) DURATION="$2"; shift 2 ;;
--nodes) NODES="$2"; shift 2 ;;
--list) list_scenarios ;;
-*) echo "Error: Unknown option '$1'" >&2; usage ;;
*)
@@ -119,9 +123,10 @@ if ! docker info &> /dev/null; then
exit 1
fi
if [ ! -f "$CHAOS_DIR/fips" ]; then
echo "Error: FIPS binary not found at $CHAOS_DIR/fips" >&2
echo "Run testing/chaos/scripts/build.sh first" >&2
DOCKER_DIR="$CHAOS_DIR/../docker"
if [ ! -f "$DOCKER_DIR/fips" ]; then
echo "Error: FIPS binary not found at $DOCKER_DIR/fips" >&2
echo "Run testing/scripts/build.sh first" >&2
exit 1
fi
@@ -137,8 +142,17 @@ echo " Scenario: $(basename "$SCENARIO_FILE" .yaml)"
echo " File: $SCENARIO_FILE"
[ -n "$SEED" ] && echo " Seed: $SEED (override)"
[ -n "$DURATION" ] && echo " Duration: ${DURATION}s (override)"
[ -n "$NODES" ] && echo " Nodes: $NODES (override)"
echo ""
# If --nodes is specified, create a patched copy of the scenario file
if [ -n "$NODES" ]; then
PATCHED=$(mktemp /tmp/chaos-scenario-XXXXXX.yaml)
sed "s/^\( num_nodes:\).*/\1 $NODES/" "$SCENARIO_FILE" > "$PATCHED"
PYTHON_ARGS[0]="$PATCHED"
trap 'rm -f "$PATCHED"' EXIT
fi
# Run from testing/chaos directory (sim expects relative paths)
cd "$CHAOS_DIR"
python3 -m sim "${PYTHON_ARGS[@]}"

View File

@@ -9,8 +9,12 @@ from jinja2 import Template
from .scenario import Scenario
from .topology import SimTopology
# Image name for the pre-built FIPS test image.
# The runner builds this once before starting containers.
FIPS_SIM_IMAGE = "fips-test:latest"
# Jinja2 template for the compose file.
# build context points back to the testing/chaos root where the Dockerfile lives.
# Uses a pre-built image instead of per-service build to support large topologies.
_COMPOSE_TEMPLATE = Template(
"""\
networks:
@@ -21,8 +25,7 @@ networks:
- subnet: {{ subnet }}
x-fips-common: &fips-common
build:
context: ../..
image: {{ image }}
cap_add:
- NET_ADMIN
- NET_RAW
@@ -36,6 +39,7 @@ x-fips-common: &fips-common
environment:
- RUST_LOG={{ rust_log }}
- RUST_BACKTRACE=1
- FIPS_TEST_MODE=chaos
services:
{% for node in nodes %}
@@ -44,7 +48,6 @@ services:
container_name: fips-node-{{ node.node_id }}
hostname: {{ node.node_id }}
volumes:
- ../../resolv.conf:/etc/resolv.conf:ro
- ./{{ node.node_id }}.yaml:/etc/fips/fips.yaml:ro
networks:
fips-net:
@@ -67,6 +70,7 @@ def generate_compose(
content = _COMPOSE_TEMPLATE.render(
subnet=scenario.topology.subnet,
rust_log=scenario.logging.rust_log,
image=FIPS_SIM_IMAGE,
nodes=nodes,
)

View File

@@ -1,14 +1,11 @@
"""Key derivation wrapper, reusing logic from scripts/derive-keys.py."""
"""Key derivation wrapper, importing from shared testing library."""
import importlib.util
import os
import sys
# Import derive() from scripts/derive-keys.py
_SCRIPTS_DIR = os.path.join(os.path.dirname(__file__), "..", "scripts")
_DERIVE_KEYS_PATH = os.path.join(_SCRIPTS_DIR, "derive-keys.py")
# Add testing/ to sys.path so we can import testing.lib.derive_keys
_TESTING_DIR = os.path.join(os.path.dirname(__file__), "..", "..")
if _TESTING_DIR not in sys.path:
sys.path.insert(0, _TESTING_DIR)
_spec = importlib.util.spec_from_file_location("derive_keys", _DERIVE_KEYS_PATH)
_mod = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_mod)
derive = _mod.derive # derive(mesh_name, node_name) -> (nsec_hex, npub_bech32)
from lib.derive_keys import derive # noqa: E402

View File

@@ -1,72 +1,31 @@
"""Log collection and post-run analysis."""
"""Log collection and post-run analysis for chaos simulations.
Delegates core analysis to the shared testing.lib.log_analysis module.
Adds chaos-specific collection (Docker container logs, sim metadata).
"""
from __future__ import annotations
import os
import re
import subprocess
import logging
from dataclasses import dataclass, field
import os
import subprocess
# Import shared analysis from testing/lib/
import sys
_TESTING_DIR = os.path.join(os.path.dirname(__file__), "..", "..")
if _TESTING_DIR not in sys.path:
sys.path.insert(0, _TESTING_DIR)
from lib.log_analysis import ( # noqa: E402
AnalysisResult,
analyze_logs,
strip_ansi,
)
log = logging.getLogger(__name__)
# Regex to strip ANSI escape codes from tracing output
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
@dataclass
class AnalysisResult:
errors: list[tuple[str, str]] = field(default_factory=list)
warnings: list[tuple[str, str]] = field(default_factory=list)
sessions_established: list[tuple[str, str]] = field(default_factory=list)
peers_promoted: list[tuple[str, str]] = field(default_factory=list)
peer_removals: list[tuple[str, str]] = field(default_factory=list)
parent_switches: list[tuple[str, str]] = field(default_factory=list)
mmp_link_metrics: list[tuple[str, str]] = field(default_factory=list)
mmp_session_metrics: list[tuple[str, str]] = field(default_factory=list)
handshake_timeouts: list[tuple[str, str]] = field(default_factory=list)
panics: list[tuple[str, str]] = field(default_factory=list)
congestion_detected: list[tuple[str, str]] = field(default_factory=list)
kernel_drop_events: list[tuple[str, str]] = field(default_factory=list)
def summary(self) -> str:
lines = [
"=== Simulation Analysis ===",
"",
f"Panics: {len(self.panics)}",
f"Errors: {len(self.errors)}",
f"Warnings: {len(self.warnings)}",
f"Sessions established: {len(self.sessions_established)}",
f"Peers promoted: {len(self.peers_promoted)}",
f"Peer removals: {len(self.peer_removals)}",
f"Parent switches: {len(self.parent_switches)}",
f"Handshake timeouts: {len(self.handshake_timeouts)}",
f"MMP link samples: {len(self.mmp_link_metrics)}",
f"MMP session samples: {len(self.mmp_session_metrics)}",
f"Congestion events: {len(self.congestion_detected)}",
f"Kernel drop events: {len(self.kernel_drop_events)}",
]
if self.panics:
lines.append("")
lines.append("--- PANICS ---")
for container, line in self.panics[:10]:
lines.append(f" [{container}] {line.strip()}")
if self.errors:
lines.append("")
lines.append("--- ERRORS (first 20) ---")
for container, line in self.errors[:20]:
lines.append(f" [{container}] {line.strip()}")
if self.handshake_timeouts:
lines.append("")
lines.append("--- HANDSHAKE TIMEOUTS (first 10) ---")
for container, line in self.handshake_timeouts[:10]:
lines.append(f" [{container}] {line.strip()}")
lines.append("")
return "\n".join(lines)
# Re-export for existing callers
__all__ = ["AnalysisResult", "analyze_logs", "collect_logs", "write_sim_metadata"]
def collect_logs(container_names: list[str], output_dir: str) -> dict[str, str]:
@@ -82,11 +41,8 @@ def collect_logs(container_names: list[str], output_dir: str) -> dict[str, str]:
text=True,
timeout=30,
)
# Combine stdout and stderr — tracing may go to either
# depending on the subscriber configuration.
# Strip ANSI escape codes for clean log files.
raw = result.stdout + result.stderr
log_text = _ANSI_RE.sub("", raw)
log_text = strip_ansi(raw)
logs[name] = log_text
path = os.path.join(output_dir, f"{name}.log")
@@ -100,53 +56,6 @@ def collect_logs(container_names: list[str], output_dir: str) -> dict[str, str]:
return logs
def analyze_logs(logs: dict[str, str]) -> AnalysisResult:
"""Parse structured tracing output and categorize events."""
result = AnalysisResult()
for container, log_text in logs.items():
for raw_line in log_text.splitlines():
# Strip ANSI escape codes for reliable matching
line = _ANSI_RE.sub("", raw_line)
# Panics
if "panicked" in line or "PANIC" in line:
result.panics.append((container, line))
# Errors and warnings
elif " ERROR " in line:
result.errors.append((container, line))
elif " WARN " in line:
result.warnings.append((container, line))
# Session establishment
if "Session established" in line:
result.sessions_established.append((container, line))
# Peer promotion
if "Inbound peer promoted" in line or "Outbound handshake completed" in line:
result.peers_promoted.append((container, line))
# Peer removal
if "Peer removed" in line:
result.peer_removals.append((container, line))
# Parent switches
if "Parent switched" in line:
result.parent_switches.append((container, line))
# Handshake timeouts
if "timed out" in line and ("handshake" in line.lower() or "Handshake" in line):
result.handshake_timeouts.append((container, line))
# MMP metrics
if "MMP link metrics" in line:
result.mmp_link_metrics.append((container, line))
if "MMP session metrics" in line:
result.mmp_session_metrics.append((container, line))
# Congestion events
if "Congestion detected" in line:
result.congestion_detected.append((container, line))
if "Kernel recv drops first observed" in line:
result.kernel_drop_events.append((container, line))
return result
def write_sim_metadata(
output_dir: str,
scenario_name: str,

View File

@@ -7,6 +7,7 @@ import logging
import os
import random
import signal
import subprocess
import sys
import time
@@ -119,9 +120,14 @@ class SimRunner:
self.compose_file = generate_compose(self.topology, self.scenario, config_dir)
log.info("Wrote %s", self.compose_file)
# 4. Build images (reuses Docker cache)
log.info("Building Docker images...")
docker_compose(self.compose_file, ["build"])
# 4. Build the test image once (avoids per-service build at scale)
log.info("Building Docker image...")
from .compose import FIPS_SIM_IMAGE
docker_dir = os.path.join(os.path.dirname(__file__), "..", "..", "docker")
subprocess.run(
["docker", "build", "-t", FIPS_SIM_IMAGE, docker_dir],
check=True,
)
# 5. Start containers
log.info("Starting %d containers...", len(self.topology.nodes))

View File

@@ -15,9 +15,10 @@
#
# Integration suites:
# static-mesh, static-chain, rekey,
# chaos-smoke-10, chaos-10, ethernet-mesh, ethernet-only,
# bottleneck-parent, cost-avoidance, cost-mixed-7node,
# cost-reeval, cost-stability, depth-vs-cost, mixed-technology,
# chaos-smoke-10, chaos-churn-mixed-10, chaos-ethernet-mesh,
# chaos-ethernet-only, chaos-tcp-mesh, chaos-bottleneck-parent,
# chaos-cost-avoidance, chaos-cost-reeval, chaos-cost-stability,
# chaos-depth-vs-cost, chaos-mixed-technology, chaos-congestion-stress,
# sidecar
#
# Exit codes:
@@ -47,11 +48,20 @@ ONLY_SUITE=""
# All integration suites matching ci.yml
STATIC_SUITES=(static-mesh static-chain)
REKEY_SUITES=(rekey)
# Each entry: "display-name scenario [--flag value ...]"
CHAOS_SUITES=(
chaos-smoke-10 chaos-10
ethernet-mesh ethernet-only
bottleneck-parent cost-avoidance cost-mixed-7node
cost-reeval cost-stability depth-vs-cost mixed-technology
"smoke-10 smoke-10"
"churn-mixed-10 churn-mixed --nodes 10 --duration 120"
"ethernet-mesh ethernet-mesh"
"ethernet-only ethernet-only"
"tcp-mesh tcp-mesh"
"bottleneck-parent bottleneck-parent"
"cost-avoidance cost-avoidance"
"cost-reeval cost-reeval"
"cost-stability cost-stability"
"depth-vs-cost depth-vs-cost"
"mixed-technology mixed-technology"
"congestion-stress congestion-stress"
)
SIDECAR_SUITES=(sidecar)
@@ -83,7 +93,10 @@ list_suites() {
for s in "${REKEY_SUITES[@]}"; do echo " $s"; done
echo ""
echo " Chaos scenarios:"
for s in "${CHAOS_SUITES[@]}"; do echo " $s"; done
for entry in "${CHAOS_SUITES[@]}"; do
read -ra parts <<< "$entry"
echo " chaos-${parts[0]} (${parts[*]:1})"
done
echo ""
echo " Sidecar:"
for s in "${SIDECAR_SUITES[@]}"; do echo " $s"; done
@@ -195,9 +208,6 @@ run_static() {
info "[$topology] Generating configs"
bash testing/static/scripts/generate-configs.sh "$topology" || { record "static-$topology" 1; return; }
info "[$topology] Building Docker images"
docker compose -f "$compose" --profile "$topology" build --quiet || { record "static-$topology" 1; return; }
info "[$topology] Starting containers"
docker compose -f "$compose" --profile "$topology" up -d || { record "static-$topology" 1; return; }
@@ -223,9 +233,6 @@ run_rekey() {
bash testing/static/scripts/generate-configs.sh rekey || { record "rekey" 1; return; }
bash testing/static/scripts/rekey-test.sh inject-config || { record "rekey" 1; return; }
info "[rekey] Building Docker images"
docker compose -f "$compose" --profile rekey build --quiet || { record "rekey" 1; return; }
info "[rekey] Starting containers"
docker compose -f "$compose" --profile rekey up -d || { record "rekey" 1; return; }
@@ -244,17 +251,18 @@ run_rekey() {
# Run a chaos scenario
run_chaos() {
local scenario="$1"
local name="$1"
shift
local rc=0
info "[chaos/$scenario] Running simulation"
if bash testing/chaos/scripts/chaos.sh "$scenario" 2>&1; then
info "[chaos/$name] Running simulation"
if bash testing/chaos/scripts/chaos.sh "$@" 2>&1; then
rc=0
else
rc=1
fi
record "chaos-$scenario" $rc
record "chaos-$name" $rc
}
# Run sidecar test
@@ -262,7 +270,7 @@ run_sidecar() {
local rc=0
info "[sidecar] Running integration test"
if bash testing/sidecar/scripts/test-sidecar.sh 2>&1; then
if bash testing/sidecar/scripts/test-sidecar.sh --skip-build 2>&1; then
rc=0
else
rc=1
@@ -275,24 +283,14 @@ run_sidecar() {
run_integration() {
stage "Stage 3: Integration Tests"
# Install binaries to test directories
info "Installing release binaries to test directories"
install_binaries testing/static
install_binaries testing/chaos
install_binaries testing/sidecar
# Install binaries to shared docker context
info "Installing release binaries"
install_binaries testing/docker
# Build chaos Docker image once (shared by all chaos scenarios)
local need_chaos=false
if [[ -z "$ONLY_SUITE" && "$SKIP_CHAOS" != true ]]; then
need_chaos=true
elif [[ "$ONLY_SUITE" == chaos-* ]]; then
need_chaos=true
fi
if [[ "$need_chaos" == true ]]; then
info "Building chaos Docker image"
docker build -t fips-chaos:latest testing/chaos --quiet || { record "chaos-build" 1; return; }
fi
# Build unified test image once (used by all harnesses)
info "Building fips-test Docker image"
docker build -t fips-test:latest testing/docker --quiet || { record "docker-build" 1; return; }
docker build -t fips-test-app:latest -f testing/docker/Dockerfile.app testing/docker --quiet || { record "docker-build-app" 1; return; }
# Single suite mode
if [[ -n "$ONLY_SUITE" ]]; then
@@ -300,7 +298,7 @@ run_integration() {
return
fi
# Static topologies (sequential — they share the docker-compose)
# Static topologies (sequential — profiles share container names)
for topo in "${STATIC_SUITES[@]}"; do
local topology="${topo#static-}"
run_static "$topology"
@@ -316,8 +314,11 @@ run_integration() {
local suite_names=()
local running=0
for suite in "${CHAOS_SUITES[@]}"; do
local scenario="${suite#chaos-}"
for entry in "${CHAOS_SUITES[@]}"; do
# Parse: "display-name scenario [flags...]"
read -ra parts <<< "$entry"
local name="${parts[0]}"
local args=("${parts[@]:1}")
# Throttle: wait for a slot
while [[ $running -ge $PARALLEL_JOBS ]]; do
@@ -327,12 +328,12 @@ run_integration() {
# Run in background, capture output to temp file
local logfile
logfile=$(mktemp "/tmp/ci-chaos-${scenario}.XXXXXX")
logfile=$(mktemp "/tmp/ci-chaos-${name}.XXXXXX")
(
run_chaos "$scenario" >"$logfile" 2>&1
run_chaos "$name" "${args[@]}" >"$logfile" 2>&1
) &
pids+=($!)
suite_names+=("$scenario:$logfile")
suite_names+=("$name:$logfile")
running=$((running + 1))
done
@@ -369,7 +370,21 @@ run_suite() {
rekey)
run_rekey ;;
chaos-*)
run_chaos "${suite#chaos-}" ;;
local chaos_name="${suite#chaos-}"
local found=false
for entry in "${CHAOS_SUITES[@]}"; do
read -ra parts <<< "$entry"
if [[ "${parts[0]}" == "$chaos_name" ]]; then
run_chaos "$chaos_name" "${parts[@]:1}"
found=true
break
fi
done
if [[ "$found" != true ]]; then
# Fall back to using the name as the scenario directly
run_chaos "$chaos_name" "$chaos_name"
fi
;;
sidecar)
run_sidecar ;;
*)

3
testing/docker/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
fips
fipsctl
fipstop

49
testing/docker/Dockerfile Normal file
View File

@@ -0,0 +1,49 @@
# Unified test image for all FIPS integration test harnesses.
#
# Supports multiple modes via FIPS_TEST_MODE environment variable:
# default — static/basic: dnsmasq + sshd + iperf3 + http + fips
# chaos — above + ethernet interface wait loop + TCP ECN
# sidecar — config from env vars + iptables isolation + fips
# tor-socks5 — dnsmasq + sshd + fips (tor daemon is separate container)
# tor-directory — dnsmasq + tor (directory mode) + wait for .onion + fips
FROM debian:trixie-slim
RUN apt-get update && \
apt-get install -y --no-install-recommends \
iproute2 iputils-ping dnsutils \
openssh-client openssh-server \
dnsmasq iptables tor \
iperf3 curl python3 \
tcpdump netcat-openbsd rsync && \
rm -rf /var/lib/apt/lists/*
# 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 fipstop /usr/local/bin/
RUN chmod +x /usr/local/bin/fips /usr/local/bin/fipsctl /usr/local/bin/fipstop
# Static web page for HTTP server (chaos/static modes)
RUN printf 'Fuck IPs!\n' > /root/index.html
COPY resolv.conf /etc/resolv.conf
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

View File

@@ -1,3 +1,6 @@
# Sidecar companion container: shares the FIPS container's network namespace.
# Provides basic networking tools for testing mesh connectivity.
FROM debian:trixie-slim
RUN apt-get update && \
@@ -7,7 +10,7 @@ RUN apt-get update && \
tcpdump netcat-openbsd curl iperf3 && \
rm -rf /var/lib/apt/lists/*
# Setup SSH server with no authentication (test only!)
# 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 && \

View File

@@ -0,0 +1,214 @@
#!/bin/bash
# Unified entrypoint for FIPS test containers.
#
# Mode is selected via FIPS_TEST_MODE environment variable:
# default — dnsmasq + sshd + iperf3 + http server + fips
# chaos — above + TCP ECN + ethernet interface wait
# sidecar — generate config from env + iptables isolation + fips
# tor-socks5 — dnsmasq + sshd + fips (tor daemon is separate)
# tor-directory — dnsmasq + tor + wait for .onion hostname + fips
set -e
MODE="${FIPS_TEST_MODE:-default}"
CONFIG="/etc/fips/fips.yaml"
# ── Common: dnsmasq ──────────────────────────────────────────────────────
start_dnsmasq() {
dnsmasq
}
# ── Common: background services (sshd, iperf3, http) ────────────────────
start_services() {
/usr/sbin/sshd
iperf3 -s -D
python3 -m http.server 8000 -d /root -b :: &>/dev/null &
}
# ── Chaos: TCP ECN + ethernet wait ──────────────────────────────────────
enable_ecn() {
sysctl -w net.ipv4.tcp_ecn=1 >/dev/null 2>&1 || true
}
wait_for_ethernet() {
# If config references ethernet transports, wait for interfaces to appear.
# Veth pairs are created from the host after the container starts.
local eth_ifaces=""
if grep -q 'ethernet:' "$CONFIG" 2>/dev/null; then
eth_ifaces=$(grep '^\s*interface:' "$CONFIG" \
| sed 's/.*interface:\s*//' \
| tr -d ' ' || true)
fi
if [ -n "$eth_ifaces" ]; then
echo "Waiting for Ethernet interfaces: $eth_ifaces"
local deadline=$((SECONDS + 30))
local all_found=false
while [ $SECONDS -lt $deadline ]; do
all_found=true
for iface in $eth_ifaces; do
if [ ! -e "/sys/class/net/$iface" ]; then
all_found=false
break
fi
done
if $all_found; then
echo "All Ethernet interfaces ready"
break
fi
sleep 0.2
done
if ! $all_found; then
echo "WARNING: Timed out waiting for Ethernet interfaces"
fi
fi
}
# ── Sidecar: config generation + iptables isolation ─────────────────────
generate_sidecar_config() {
FIPS_NSEC="${FIPS_NSEC:?FIPS_NSEC is required}"
FIPS_UDP_BIND="${FIPS_UDP_BIND:-0.0.0.0:2121}"
FIPS_TUN_MTU="${FIPS_TUN_MTU:-1280}"
FIPS_PEER_TRANSPORT="${FIPS_PEER_TRANSPORT:-udp}"
mkdir -p /etc/fips
local peers_section=""
if [ -n "$FIPS_PEER_NPUB" ] && [ -n "$FIPS_PEER_ADDR" ]; then
FIPS_PEER_ALIAS="${FIPS_PEER_ALIAS:-peer}"
peers_section=" - npub: \"${FIPS_PEER_NPUB}\"
alias: \"${FIPS_PEER_ALIAS}\"
addresses:
- transport: ${FIPS_PEER_TRANSPORT}
addr: \"${FIPS_PEER_ADDR}\"
connect_policy: auto_connect"
fi
cat > "$CONFIG" <<EOF
node:
identity:
nsec: "${FIPS_NSEC}"
tun:
enabled: true
name: fips0
mtu: ${FIPS_TUN_MTU}
dns:
enabled: true
bind_addr: "127.0.0.1"
transports:
udp:
bind_addr: "${FIPS_UDP_BIND}"
mtu: 1472
tcp: {}
peers:
${peers_section:- []}
EOF
echo "Generated $CONFIG"
}
apply_iptables_isolation() {
# Only FIPS transport (UDP 2121, TCP 443) may use eth0.
# All other eth0 traffic is dropped. fips0 and loopback unrestricted.
iptables -A OUTPUT -o lo -j ACCEPT
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o eth0 -p udp --dport 2121 -j ACCEPT
iptables -A OUTPUT -o eth0 -p udp --sport 2121 -j ACCEPT
iptables -A INPUT -i eth0 -p udp --dport 2121 -j ACCEPT
iptables -A INPUT -i eth0 -p udp --sport 2121 -j ACCEPT
iptables -A OUTPUT -o eth0 -p tcp --dport 443 -j ACCEPT
iptables -A INPUT -i eth0 -p tcp --sport 443 -j ACCEPT
iptables -A OUTPUT -o eth0 -j DROP
iptables -A INPUT -i eth0 -j DROP
ip6tables -A OUTPUT -o lo -j ACCEPT
ip6tables -A INPUT -i lo -j ACCEPT
ip6tables -A OUTPUT -o fips0 -j ACCEPT
ip6tables -A INPUT -i fips0 -j ACCEPT
ip6tables -A OUTPUT -o eth0 -j DROP
ip6tables -A INPUT -i eth0 -j DROP
echo "iptables isolation rules applied"
}
# ── Tor directory mode: start tor + wait for hostname ────────────────────
start_tor_directory() {
local hidden_service_dir="/var/lib/tor/fips_onion_service"
local is_directory=false
if grep -qE '^\s+mode:\s+"directory"' "$CONFIG" 2>/dev/null; then
is_directory=true
fi
if [ "$is_directory" = true ]; then
mkdir -p "$hidden_service_dir"
chmod 700 "$hidden_service_dir"
fi
echo "Starting Tor daemon..."
tor -f /etc/tor/torrc &
if [ "$is_directory" = true ]; then
local hostname_file="${hidden_service_dir}/hostname"
echo "Waiting for Tor to create ${hostname_file}..."
for i in $(seq 1 120); do
if [ -f "$hostname_file" ]; then
echo "Tor hostname file ready after ${i}s: $(cat "$hostname_file")"
break
fi
sleep 1
done
if [ ! -f "$hostname_file" ]; then
echo "FATAL: Tor did not create hostname file within 120s"
exit 1
fi
fi
}
# ── Mode dispatch ────────────────────────────────────────────────────────
case "$MODE" in
default)
start_dnsmasq
start_services
exec fips --config "$CONFIG"
;;
chaos)
enable_ecn
start_dnsmasq
start_services
wait_for_ethernet
exec fips --config "$CONFIG"
;;
sidecar)
generate_sidecar_config
apply_iptables_isolation
start_dnsmasq
exec fips --config "$CONFIG"
;;
tor-socks5)
start_dnsmasq
/usr/sbin/sshd
exec fips --config "$CONFIG"
;;
tor-directory)
start_dnsmasq
start_tor_directory
echo "Starting FIPS daemon..."
exec fips --config "$CONFIG"
;;
*)
echo "Unknown FIPS_TEST_MODE: $MODE"
echo "Valid modes: default, chaos, sidecar, tor-socks5, tor-directory"
exit 1
;;
esac

0
testing/lib/__init__.py Normal file
View File

205
testing/lib/log_analysis.py Normal file
View File

@@ -0,0 +1,205 @@
#!/usr/bin/env python3
"""Shared log analysis for FIPS integration tests.
Parses structured tracing output from FIPS daemons and categorizes
events (panics, errors, sessions, parent switches, etc.).
CLI usage:
python3 -m lib.log_analysis <logfile> [<logfile> ...]
python3 -m lib.log_analysis --from-docker <container> [<container> ...]
Exit codes:
0 — no panics detected
2 — panics detected
"""
from __future__ import annotations
import re
import subprocess
import sys
from dataclasses import dataclass, field
# Regex to strip ANSI escape codes from tracing output
ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
def strip_ansi(text: str) -> str:
"""Remove ANSI escape codes from text."""
return ANSI_RE.sub("", text)
@dataclass
class AnalysisResult:
"""Categorized events extracted from FIPS daemon logs."""
errors: list[tuple[str, str]] = field(default_factory=list)
warnings: list[tuple[str, str]] = field(default_factory=list)
sessions_established: list[tuple[str, str]] = field(default_factory=list)
peers_promoted: list[tuple[str, str]] = field(default_factory=list)
peer_removals: list[tuple[str, str]] = field(default_factory=list)
parent_switches: list[tuple[str, str]] = field(default_factory=list)
mmp_link_metrics: list[tuple[str, str]] = field(default_factory=list)
mmp_session_metrics: list[tuple[str, str]] = field(default_factory=list)
handshake_timeouts: list[tuple[str, str]] = field(default_factory=list)
panics: list[tuple[str, str]] = field(default_factory=list)
congestion_detected: list[tuple[str, str]] = field(default_factory=list)
kernel_drop_events: list[tuple[str, str]] = field(default_factory=list)
rekey_cutovers: list[tuple[str, str]] = field(default_factory=list)
def summary(self) -> str:
"""Format a human-readable summary of the analysis."""
lines = [
"=== Log Analysis ===",
"",
f"Panics: {len(self.panics)}",
f"Errors: {len(self.errors)}",
f"Warnings: {len(self.warnings)}",
f"Sessions established: {len(self.sessions_established)}",
f"Peers promoted: {len(self.peers_promoted)}",
f"Peer removals: {len(self.peer_removals)}",
f"Parent switches: {len(self.parent_switches)}",
f"Handshake timeouts: {len(self.handshake_timeouts)}",
f"MMP link samples: {len(self.mmp_link_metrics)}",
f"MMP session samples: {len(self.mmp_session_metrics)}",
f"Congestion events: {len(self.congestion_detected)}",
f"Kernel drop events: {len(self.kernel_drop_events)}",
f"Rekey cutovers: {len(self.rekey_cutovers)}",
]
if self.panics:
lines.append("")
lines.append("--- PANICS ---")
for source, line in self.panics[:10]:
lines.append(f" [{source}] {line.strip()}")
if self.errors:
lines.append("")
lines.append("--- ERRORS (first 20) ---")
for source, line in self.errors[:20]:
lines.append(f" [{source}] {line.strip()}")
if self.handshake_timeouts:
lines.append("")
lines.append("--- HANDSHAKE TIMEOUTS (first 10) ---")
for source, line in self.handshake_timeouts[:10]:
lines.append(f" [{source}] {line.strip()}")
lines.append("")
return "\n".join(lines)
@property
def has_panics(self) -> bool:
return len(self.panics) > 0
def analyze_text(log_text: str, source: str = "") -> AnalysisResult:
"""Analyze a single log text and return categorized events."""
result = AnalysisResult()
_analyze_lines(result, source, log_text)
return result
def analyze_logs(logs: dict[str, str]) -> AnalysisResult:
"""Analyze logs from multiple sources (keyed by source name)."""
result = AnalysisResult()
for source, log_text in logs.items():
_analyze_lines(result, source, log_text)
return result
def _analyze_lines(result: AnalysisResult, source: str, log_text: str):
"""Parse log lines and append to result."""
for raw_line in log_text.splitlines():
line = strip_ansi(raw_line)
# Panics
if "panicked" in line or "PANIC" in line:
result.panics.append((source, line))
# Errors and warnings
elif " ERROR " in line:
result.errors.append((source, line))
elif " WARN " in line:
result.warnings.append((source, line))
# Session establishment
if "Session established" in line:
result.sessions_established.append((source, line))
# Peer promotion
if "Inbound peer promoted" in line or "Outbound handshake completed" in line:
result.peers_promoted.append((source, line))
# Peer removal
if "Peer removed" in line:
result.peer_removals.append((source, line))
# Parent switches
if "Parent switched" in line:
result.parent_switches.append((source, line))
# Handshake timeouts
if "timed out" in line and ("handshake" in line.lower() or "Handshake" in line):
result.handshake_timeouts.append((source, line))
# MMP metrics
if "MMP link metrics" in line:
result.mmp_link_metrics.append((source, line))
if "MMP session metrics" in line:
result.mmp_session_metrics.append((source, line))
# Congestion events
if "Congestion detected" in line:
result.congestion_detected.append((source, line))
if "Kernel recv drops first observed" in line:
result.kernel_drop_events.append((source, line))
# Rekey cutovers
if "Rekey cutover complete" in line or "FSP rekey cutover complete" in line:
result.rekey_cutovers.append((source, line))
def collect_docker_logs(containers: list[str]) -> dict[str, str]:
"""Collect logs from Docker containers, stripping ANSI codes."""
logs = {}
for name in containers:
try:
result = subprocess.run(
["docker", "logs", name],
capture_output=True,
text=True,
timeout=30,
)
raw = result.stdout + result.stderr
logs[name] = strip_ansi(raw)
except (subprocess.TimeoutExpired, Exception):
logs[name] = ""
return logs
def main():
"""CLI entry point."""
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} [--from-docker] <source> [<source> ...]",
file=sys.stderr)
sys.exit(1)
from_docker = False
args = sys.argv[1:]
if args[0] == "--from-docker":
from_docker = True
args = args[1:]
if not args:
print("Error: no sources specified", file=sys.stderr)
sys.exit(1)
if from_docker:
logs = collect_docker_logs(args)
else:
logs = {}
for path in args:
with open(path) as f:
logs[path] = f.read()
result = analyze_logs(logs)
print(result.summary())
sys.exit(2 if result.has_panics else 0)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,51 @@
#!/bin/bash
# Shared convergence wait helpers for FIPS integration tests.
#
# Source this file to get wait_for_links() and wait_for_peers().
#
# Usage:
# source "$(dirname "$0")/../../lib/wait-converge.sh"
# wait_for_links <container> <min_links> [timeout_secs]
# wait_for_peers <container> <min_peers> [timeout_secs]
# Wait until a container has at least min_links active links.
# Returns 0 on success, 1 on timeout.
wait_for_links() {
local container="$1"
local min_links="$2"
local timeout="${3:-30}"
for i in $(seq 1 "$timeout"); do
local count
count=$(docker exec "$container" fipsctl show links 2>/dev/null \
| python3 -c "import sys,json; print(len(json.load(sys.stdin).get('links',[])))" 2>/dev/null || echo 0)
if [ "$count" -ge "$min_links" ]; then
echo " $container: $count link(s) after ${i}s"
return 0
fi
sleep 1
done
echo " $container: TIMEOUT waiting for $min_links link(s) after ${timeout}s"
return 1
}
# Wait until a container has at least min_peers connected peers.
# Returns 0 on success, 1 on timeout.
wait_for_peers() {
local container="$1"
local min_peers="$2"
local timeout="${3:-30}"
for i in $(seq 1 "$timeout"); do
local count
count=$(docker exec "$container" fipsctl show peers 2>/dev/null \
| python3 -c "import sys,json; print(sum(1 for p in json.load(sys.stdin).get('peers',[]) if p.get('connectivity')=='connected'))" 2>/dev/null || echo 0)
if [ "$count" -ge "$min_peers" ]; then
echo " $container: $count peer(s) after ${i}s"
return 0
fi
sleep 1
done
echo " $container: TIMEOUT waiting for $min_peers peer(s) after ${timeout}s"
return 1
}

73
testing/scripts/build.sh Executable file
View File

@@ -0,0 +1,73 @@
#!/bin/bash
# Build FIPS binaries and the unified test Docker image.
#
# Supports cross-compilation from macOS to Linux using cargo-zigbuild.
#
# Usage: ./build.sh [--no-docker]
# --no-docker Skip Docker image build (just compile and copy binaries)
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
TESTING_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
DOCKER_DIR="$TESTING_DIR/docker"
# Find project root (directory containing Cargo.toml)
PROJECT_ROOT="$(cd "$TESTING_DIR/.." && pwd)"
if [ ! -f "$PROJECT_ROOT/Cargo.toml" ]; then
echo "Error: Cannot find Cargo.toml at $PROJECT_ROOT" >&2
exit 1
fi
BUILD_DOCKER=true
while [ $# -gt 0 ]; do
case "$1" in
--no-docker) BUILD_DOCKER=false; shift ;;
*) echo "Unknown option: $1" >&2; exit 1 ;;
esac
done
# Detect host OS
UNAME_S=$(uname -s)
CARGO_TARGET="x86_64-unknown-linux-musl"
if [ "$UNAME_S" = "Darwin" ]; then
echo "Detected macOS host — using cross-compilation for Linux..."
if ! command -v cargo-zigbuild &> /dev/null; then
echo "Error: cargo-zigbuild not found." >&2
echo "Please install it: cargo install cargo-zigbuild" >&2
exit 1
fi
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"
TARGET_DIR="$PROJECT_ROOT/target/$CARGO_TARGET/release"
else
echo "Building FIPS (release)..."
cargo build --release --manifest-path="$PROJECT_ROOT/Cargo.toml"
TARGET_DIR="$PROJECT_ROOT/target/release"
fi
echo "Copying binaries to $DOCKER_DIR/"
cp "$TARGET_DIR/fips" "$DOCKER_DIR/fips"
cp "$TARGET_DIR/fipsctl" "$DOCKER_DIR/fipsctl"
[ -f "$TARGET_DIR/fipstop" ] && cp "$TARGET_DIR/fipstop" "$DOCKER_DIR/fipstop" || true
chmod +x "$DOCKER_DIR/fips" "$DOCKER_DIR/fipsctl"
[ -f "$DOCKER_DIR/fipstop" ] && chmod +x "$DOCKER_DIR/fipstop" || true
echo "Done. Binaries at $DOCKER_DIR/{fips,fipsctl,fipstop}"
if [ "$BUILD_DOCKER" = true ]; then
echo ""
echo "Building Docker images..."
docker build -t fips-test:latest "$DOCKER_DIR"
docker build -t fips-test-app:latest -f "$DOCKER_DIR/Dockerfile.app" "$DOCKER_DIR"
echo "Done. Images: fips-test:latest, fips-test-app:latest"
fi

View File

@@ -1,33 +0,0 @@
FROM debian:trixie-slim
RUN apt-get update && \
apt-get install -y --no-install-recommends \
iproute2 iputils-ping dnsutils dnsmasq iptables \
openssh-client openssh-server python3 \
tcpdump netcat-openbsd curl iperf3 && \
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 fipstop /usr/local/bin/
RUN chmod +x /usr/local/bin/fips /usr/local/bin/fipsctl /usr/local/bin/fipstop
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -8,8 +8,7 @@ networks:
services:
fips:
build:
context: .
image: fips-test:latest
hostname: fips-sidecar
cap_add:
- NET_ADMIN
@@ -20,6 +19,7 @@ services:
restart: "no"
environment:
- RUST_LOG=${RUST_LOG:-info}
- FIPS_TEST_MODE=sidecar
- FIPS_NSEC=${FIPS_NSEC}
- FIPS_PEER_NPUB=${FIPS_PEER_NPUB:-}
- FIPS_PEER_ADDR=${FIPS_PEER_ADDR:-}
@@ -27,19 +27,15 @@ services:
- FIPS_UDP_BIND=${FIPS_UDP_BIND:-0.0.0.0:2121}
- FIPS_TUN_MTU=${FIPS_TUN_MTU:-1280}
- FIPS_PEER_TRANSPORT=${FIPS_PEER_TRANSPORT:-udp}
volumes:
- ./resolv.conf:/etc/resolv.conf:ro
networks:
fips-net:
ipv4_address: ${FIPS_IPV4:-172.20.1.20}
app:
build:
context: .
dockerfile: Dockerfile.app
image: fips-test-app:latest
network_mode: "service:fips"
depends_on:
- fips
volumes:
- ./resolv.conf:/etc/resolv.conf:ro
- ../docker/resolv.conf:/etc/resolv.conf:ro
command: ["sleep", "infinity"]

View File

@@ -1,84 +0,0 @@
#!/bin/bash
# FIPS sidecar entrypoint: generate config, apply iptables isolation, launch FIPS.
set -e
# --- Generate FIPS config from environment variables ---
FIPS_NSEC="${FIPS_NSEC:?FIPS_NSEC is required}"
FIPS_UDP_BIND="${FIPS_UDP_BIND:-0.0.0.0:2121}"
FIPS_TUN_MTU="${FIPS_TUN_MTU:-1280}"
FIPS_PEER_TRANSPORT="${FIPS_PEER_TRANSPORT:-udp}"
mkdir -p /etc/fips
# Build peers section
PEERS_SECTION=""
if [ -n "$FIPS_PEER_NPUB" ] && [ -n "$FIPS_PEER_ADDR" ]; then
FIPS_PEER_ALIAS="${FIPS_PEER_ALIAS:-peer}"
PEERS_SECTION=" - npub: \"${FIPS_PEER_NPUB}\"
alias: \"${FIPS_PEER_ALIAS}\"
addresses:
- transport: ${FIPS_PEER_TRANSPORT}
addr: \"${FIPS_PEER_ADDR}\"
connect_policy: auto_connect"
fi
cat > /etc/fips/fips.yaml <<EOF
node:
identity:
nsec: "${FIPS_NSEC}"
tun:
enabled: true
name: fips0
mtu: ${FIPS_TUN_MTU}
dns:
enabled: true
bind_addr: "127.0.0.1"
transports:
udp:
bind_addr: "${FIPS_UDP_BIND}"
mtu: 1472
tcp: {}
peers:
${PEERS_SECTION:- []}
EOF
echo "Generated /etc/fips/fips.yaml"
# --- Apply iptables rules for strict network isolation ---
#
# Goal: only FIPS UDP transport (port 2121) may use eth0.
# All other eth0 traffic is dropped. fips0 and loopback are unrestricted.
# This ensures the app container (sharing this network namespace) can only
# communicate over the FIPS mesh.
# IPv4: allow only FIPS transport on eth0
iptables -A OUTPUT -o lo -j ACCEPT
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o eth0 -p udp --dport 2121 -j ACCEPT
iptables -A OUTPUT -o eth0 -p udp --sport 2121 -j ACCEPT
iptables -A INPUT -i eth0 -p udp --dport 2121 -j ACCEPT
iptables -A INPUT -i eth0 -p udp --sport 2121 -j ACCEPT
iptables -A OUTPUT -o eth0 -p tcp --dport 443 -j ACCEPT
iptables -A INPUT -i eth0 -p tcp --sport 443 -j ACCEPT
iptables -A OUTPUT -o eth0 -j DROP
iptables -A INPUT -i eth0 -j DROP
# IPv6: allow fips0 and loopback, block eth0
ip6tables -A OUTPUT -o lo -j ACCEPT
ip6tables -A INPUT -i lo -j ACCEPT
ip6tables -A OUTPUT -o fips0 -j ACCEPT
ip6tables -A INPUT -i fips0 -j ACCEPT
ip6tables -A OUTPUT -o eth0 -j DROP
ip6tables -A INPUT -i eth0 -j DROP
echo "iptables isolation rules applied"
# --- Start dnsmasq and launch FIPS ---
dnsmasq
exec fips --config /etc/fips/fips.yaml

View File

@@ -1,57 +0,0 @@
#!/bin/bash
# Build the FIPS binary and Docker sidecar image.
# Usage: ./build.sh
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
DOCKER_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
# 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/sidecar/scripts/build.sh" >&2
exit 1
fi
# Detect host OS
UNAME_S=$(uname -s)
CARGO_TARGET="x86_64-unknown-linux-musl"
if [ "$UNAME_S" = "Darwin" ]; then
echo "Detected macOS host — using cross-compilation for Linux..."
if ! command -v cargo-zigbuild &> /dev/null; then
echo "Error: cargo-zigbuild not found." >&2
echo "Please install it: cargo install cargo-zigbuild" >&2
exit 1
fi
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"
cp "$PROJECT_ROOT/target/$CARGO_TARGET/release/fipstop" "$DOCKER_DIR/fipstop"
else
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"
cp "$PROJECT_ROOT/target/release/fipstop" "$DOCKER_DIR/fipstop"
fi
echo "Done. Binaries at $DOCKER_DIR/{fips,fipsctl,fipstop}"
echo ""
echo "Building Docker image..."
docker compose -f "$DOCKER_DIR/docker-compose.yml" build
echo ""
echo "Ready: cd testing/sidecar && docker compose up -d"

View File

@@ -58,8 +58,10 @@ trap cleanup EXIT
# ── Build ──────────────────────────────────────────────────────────────────
if [[ "${1:-}" != "--skip-build" ]]; then
log "Building sidecar images..."
docker compose $COMPOSE_BASE build
log "Building test images..."
DOCKER_DIR="$(cd "$SIDECAR_DIR/../docker" && pwd)"
docker build -t fips-test:latest "$DOCKER_DIR"
docker build -t fips-test-app:latest -f "$DOCKER_DIR/Dockerfile.app" "$DOCKER_DIR"
fi
# ── Start nodes ────────────────────────────────────────────────────────────

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"]

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:

View File

@@ -1 +0,0 @@
nameserver 127.0.0.1

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"

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

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>

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

View File

@@ -1,31 +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
# Start dnsmasq, SSH server, and run FIPS
ENTRYPOINT ["/bin/bash", "-c", "dnsmasq && /usr/sbin/sshd && exec fips --config /etc/fips/fips.yaml"]

View File

@@ -1 +0,0 @@
nameserver 127.0.0.1

View File

@@ -1,37 +0,0 @@
# Dockerfile for directory-mode test: Tor + FIPS co-located in one container.
#
# Tor manages the onion service via HiddenServiceDir. FIPS reads the
# .onion hostname from /var/lib/tor/fips_onion_service/hostname at startup.
# This requires Tor to bootstrap and create the hostname file before FIPS
# starts, so the entrypoint script waits for it.
FROM debian:bookworm-slim
ARG TORRC=torrc
RUN apt-get update && \
apt-get install -y --no-install-recommends \
tor iproute2 iputils-ping dnsutils \
dnsmasq python3 && \
rm -rf /var/lib/apt/lists/*
# 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
COPY ${TORRC} /etc/tor/torrc
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
COPY resolv.conf /etc/resolv.conf
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -5,8 +5,8 @@
# [fips-b] — Tor + FIPS co-located, socks5-only, connects to fips-a's .onion
#
# Both nodes run Tor and FIPS in the same container. Node A's Tor manages
# the onion service via HiddenServiceDir with Sandbox 1. Node B connects
# outbound through its local Tor's SOCKS5 proxy.
# the onion service via HiddenServiceDir. Node B connects outbound through
# its local Tor's SOCKS5 proxy.
networks:
dir-test:
@@ -14,9 +14,7 @@ networks:
services:
fips-a:
build:
context: .
dockerfile: Dockerfile.colocated
image: fips-test:latest
cap_add:
- NET_ADMIN
devices:
@@ -28,17 +26,15 @@ services:
hostname: fips-dir-a
volumes:
- ./configs/node-a.yaml:/etc/fips/fips.yaml:ro
- ./torrc:/etc/tor/torrc:ro
environment:
- RUST_LOG=info,fips::transport::tor=debug
- FIPS_TEST_MODE=tor-directory
networks:
dir-test:
fips-b:
build:
context: .
dockerfile: Dockerfile.colocated
args:
TORRC: torrc.socks5
image: fips-test:latest
cap_add:
- NET_ADMIN
devices:
@@ -50,7 +46,9 @@ services:
hostname: fips-dir-b
volumes:
- ./configs/node-b.yaml:/etc/fips/fips.yaml:ro
- ./torrc.socks5:/etc/tor/torrc:ro
environment:
- RUST_LOG=info,fips::transport::tor=debug
- FIPS_TEST_MODE=tor-directory
networks:
dir-test:

View File

@@ -1,46 +0,0 @@
#!/bin/bash
# Entrypoint for directory-mode test container.
# Starts Tor, optionally waits for hostname file, then starts FIPS.
set -e
echo "Starting dnsmasq..."
dnsmasq
# Check if this node uses directory mode (match the YAML value, not comments)
IS_DIRECTORY_MODE=false
if grep -qE '^\s+mode:\s+"directory"' /etc/fips/fips.yaml 2>/dev/null; then
IS_DIRECTORY_MODE=true
fi
# Pre-create HiddenServiceDir with correct permissions.
# Tor requires 0700 on the directory.
HIDDEN_SERVICE_DIR="/var/lib/tor/fips_onion_service"
if [ "$IS_DIRECTORY_MODE" = true ]; then
mkdir -p "$HIDDEN_SERVICE_DIR"
chmod 700 "$HIDDEN_SERVICE_DIR"
fi
echo "Starting Tor daemon..."
tor -f /etc/tor/torrc &
# If this node uses directory mode, wait for Tor to create the hostname file
if [ "$IS_DIRECTORY_MODE" = true ]; then
HOSTNAME_FILE="${HIDDEN_SERVICE_DIR}/hostname"
echo "Waiting for Tor to create ${HOSTNAME_FILE}..."
for i in $(seq 1 120); do
if [ -f "$HOSTNAME_FILE" ]; then
echo "Tor hostname file ready after ${i}s: $(cat "$HOSTNAME_FILE")"
break
fi
sleep 1
done
if [ ! -f "$HOSTNAME_FILE" ]; then
echo "FATAL: Tor did not create hostname file within 120s"
exit 1
fi
fi
echo "Starting FIPS daemon..."
exec fips --config /etc/fips/fips.yaml

View File

@@ -1 +0,0 @@
nameserver 127.0.0.1

View File

@@ -19,7 +19,7 @@ trap 'echo ""; echo "Test interrupted — cleaning up..."; docker compose down 2
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEST_DIR="$SCRIPT_DIR/.."
DERIVE_KEYS="$SCRIPT_DIR/../../../static/scripts/derive-keys.py"
DERIVE_KEYS="$SCRIPT_DIR/../../../lib/derive_keys.py"
cd "$TEST_DIR"
PASSED=0
@@ -84,7 +84,7 @@ echo ""
# ── Phase 1: Start node A (Tor + FIPS co-located) ────────────────
echo "Phase 1: Starting node A (Tor+FIPS, directory-mode onion service)..."
docker compose down 2>/dev/null || true
docker compose up -d --build fips-a
docker compose up -d fips-a
echo ""
# ── Phase 2: Wait for onion service creation ─────────────────────

View File

@@ -22,8 +22,7 @@ services:
tor-test:
fips-a:
build:
context: ../common
image: fips-test:latest
cap_add:
- NET_ADMIN
devices:
@@ -36,16 +35,15 @@ services:
depends_on:
- tor-daemon
volumes:
- ../common/resolv.conf:/etc/resolv.conf:ro
- ./configs/node-a.yaml:/etc/fips/fips.yaml:ro
environment:
- RUST_LOG=info,fips::transport::tor=debug
- FIPS_TEST_MODE=tor-socks5
networks:
tor-test:
fips-b:
build:
context: ../common
image: fips-test:latest
cap_add:
- NET_ADMIN
devices:
@@ -58,9 +56,9 @@ services:
depends_on:
- tor-daemon
volumes:
- ../common/resolv.conf:/etc/resolv.conf:ro
- ./configs/node-b.yaml:/etc/fips/fips.yaml:ro
environment:
- RUST_LOG=info,fips::transport::tor=debug
- FIPS_TEST_MODE=tor-socks5
networks:
tor-test:

View File

@@ -28,7 +28,7 @@ trap 'echo ""; echo "Test interrupted — cleaning up..."; docker compose down 2
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TOR_DIR="$SCRIPT_DIR/.."
DERIVE_KEYS="$SCRIPT_DIR/../../../static/scripts/derive-keys.py"
DERIVE_KEYS="$SCRIPT_DIR/../../../lib/derive_keys.py"
cd "$TOR_DIR"
PASSED=0
@@ -80,7 +80,7 @@ echo ""
# ── Phase 1: Build and start ─────────────────────────────────────
echo "Phase 1: Starting Tor daemon and FIPS nodes..."
docker compose down 2>/dev/null || true
docker compose up -d --build
docker compose up -d
echo ""
# ── Phase 2: Wait for Tor bootstrap ─────────────────────────────