mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-22 07:48:26 +00:00
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:
74
.github/workflows/ci.yml
vendored
74
.github/workflows/ci.yml
vendored
@@ -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()
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -23,3 +23,9 @@ dist/
|
||||
*.ipk
|
||||
|
||||
sim-results/
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
*.egg
|
||||
@@ -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"]
|
||||
@@ -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"
|
||||
@@ -35,7 +35,7 @@
|
||||
scenario:
|
||||
name: "bottleneck-parent"
|
||||
seed: 42
|
||||
duration_secs: 120
|
||||
duration_secs: 60
|
||||
|
||||
topology:
|
||||
algorithm: explicit
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -22,7 +22,7 @@
|
||||
scenario:
|
||||
name: "cost-avoidance"
|
||||
seed: 42
|
||||
duration_secs: 120
|
||||
duration_secs: 45
|
||||
|
||||
topology:
|
||||
algorithm: explicit
|
||||
|
||||
@@ -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"
|
||||
@@ -27,7 +27,7 @@
|
||||
scenario:
|
||||
name: "depth-vs-cost"
|
||||
seed: 42
|
||||
duration_secs: 120
|
||||
duration_secs: 45
|
||||
|
||||
topology:
|
||||
algorithm: explicit
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
scenario:
|
||||
name: "ethernet-only"
|
||||
seed: 42
|
||||
duration_secs: 90
|
||||
duration_secs: 30
|
||||
|
||||
topology:
|
||||
algorithm: explicit
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
scenario:
|
||||
name: "mixed-technology"
|
||||
seed: 42
|
||||
duration_secs: 180
|
||||
duration_secs: 90
|
||||
|
||||
topology:
|
||||
algorithm: explicit
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
scenario:
|
||||
name: "smoke-10"
|
||||
seed: 42
|
||||
duration_secs: 60
|
||||
duration_secs: 30
|
||||
|
||||
topology:
|
||||
num_nodes: 10
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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}"
|
||||
@@ -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[@]}"
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
3
testing/docker/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
fips
|
||||
fipsctl
|
||||
fipstop
|
||||
49
testing/docker/Dockerfile
Normal file
49
testing/docker/Dockerfile
Normal 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"]
|
||||
@@ -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 && \
|
||||
214
testing/docker/entrypoint.sh
Normal file
214
testing/docker/entrypoint.sh
Normal 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
0
testing/lib/__init__.py
Normal file
205
testing/lib/log_analysis.py
Normal file
205
testing/lib/log_analysis.py
Normal 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()
|
||||
51
testing/lib/wait-converge.sh
Normal file
51
testing/lib/wait-converge.sh
Normal 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
73
testing/scripts/build.sh
Executable 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
|
||||
@@ -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"]
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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 ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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"]
|
||||
@@ -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 +0,0 @@
|
||||
nameserver 127.0.0.1
|
||||
@@ -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"
|
||||
@@ -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}")
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
@@ -1 +0,0 @@
|
||||
nameserver 127.0.0.1
|
||||
@@ -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"]
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
nameserver 127.0.0.1
|
||||
@@ -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 ─────────────────────
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 ─────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user