mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +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:
+23
-53
@@ -125,8 +125,8 @@ jobs:
|
|||||||
# Runs only when both build and test succeed. Each topology / scenario is a
|
# Runs only when both build and test succeed. Each topology / scenario is a
|
||||||
# separate matrix entry so they run in parallel.
|
# separate matrix entry so they run in parallel.
|
||||||
#
|
#
|
||||||
# Static topologies → build Docker images, start containers, ping-test
|
# All harnesses share a single Docker image (fips-test:latest) built once
|
||||||
# Chaos scenarios → build sim image, run stochastic simulation
|
# in the setup step from testing/docker/.
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
integration:
|
integration:
|
||||||
name: Integration (${{ matrix.suite }})
|
name: Integration (${{ matrix.suite }})
|
||||||
@@ -153,24 +153,25 @@ jobs:
|
|||||||
- suite: chaos-smoke-10
|
- suite: chaos-smoke-10
|
||||||
type: chaos
|
type: chaos
|
||||||
scenario: smoke-10
|
scenario: smoke-10
|
||||||
- suite: chaos-10
|
- suite: churn-mixed-10
|
||||||
type: chaos
|
type: chaos
|
||||||
scenario: chaos-10
|
scenario: churn-mixed
|
||||||
|
chaos_flags: "--nodes 10 --duration 120"
|
||||||
- suite: ethernet-mesh
|
- suite: ethernet-mesh
|
||||||
type: chaos
|
type: chaos
|
||||||
scenario: ethernet-mesh
|
scenario: ethernet-mesh
|
||||||
- suite: ethernet-only
|
- suite: ethernet-only
|
||||||
type: chaos
|
type: chaos
|
||||||
scenario: ethernet-only
|
scenario: ethernet-only
|
||||||
|
- suite: tcp-mesh
|
||||||
|
type: chaos
|
||||||
|
scenario: tcp-mesh
|
||||||
- suite: bottleneck-parent
|
- suite: bottleneck-parent
|
||||||
type: chaos
|
type: chaos
|
||||||
scenario: bottleneck-parent
|
scenario: bottleneck-parent
|
||||||
- suite: cost-avoidance
|
- suite: cost-avoidance
|
||||||
type: chaos
|
type: chaos
|
||||||
scenario: cost-avoidance
|
scenario: cost-avoidance
|
||||||
- suite: cost-mixed-7node
|
|
||||||
type: chaos
|
|
||||||
scenario: cost-mixed-7node
|
|
||||||
- suite: cost-reeval
|
- suite: cost-reeval
|
||||||
type: chaos
|
type: chaos
|
||||||
scenario: cost-reeval
|
scenario: cost-reeval
|
||||||
@@ -183,6 +184,9 @@ jobs:
|
|||||||
- suite: mixed-technology
|
- suite: mixed-technology
|
||||||
type: chaos
|
type: chaos
|
||||||
scenario: mixed-technology
|
scenario: mixed-technology
|
||||||
|
- suite: congestion-stress
|
||||||
|
type: chaos
|
||||||
|
scenario: congestion-stress
|
||||||
# ── Sidecar deployment ──────────────────────────────────────────
|
# ── Sidecar deployment ──────────────────────────────────────────
|
||||||
- suite: sidecar
|
- suite: sidecar
|
||||||
type: sidecar
|
type: sidecar
|
||||||
@@ -197,24 +201,22 @@ jobs:
|
|||||||
name: fips-linux
|
name: fips-linux
|
||||||
path: _bin
|
path: _bin
|
||||||
|
|
||||||
# ── Static topology ────────────────────────────────────────────────────
|
# Install binaries to unified docker context and build shared image
|
||||||
- name: Install binary (static)
|
- name: Install binaries and build Docker image
|
||||||
if: matrix.type == 'static'
|
|
||||||
run: |
|
run: |
|
||||||
chmod +x _bin/fips _bin/fipsctl
|
chmod +x _bin/fips _bin/fipsctl
|
||||||
cp _bin/fips testing/static/fips
|
[ -f _bin/fipstop ] && chmod +x _bin/fipstop || true
|
||||||
cp _bin/fipsctl testing/static/fipsctl
|
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)
|
- name: Generate configs (static)
|
||||||
if: matrix.type == 'static'
|
if: matrix.type == 'static'
|
||||||
run: bash testing/static/scripts/generate-configs.sh ${{ matrix.topology }}
|
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)
|
- name: Start containers (static)
|
||||||
if: matrix.type == 'static'
|
if: matrix.type == 'static'
|
||||||
run: |
|
run: |
|
||||||
@@ -238,25 +240,12 @@ jobs:
|
|||||||
--profile ${{ matrix.topology }} down --volumes --remove-orphans
|
--profile ${{ matrix.topology }} down --volumes --remove-orphans
|
||||||
|
|
||||||
# ── Rekey integration test ──────────────────────────────────────────────
|
# ── 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)
|
- name: Generate and inject configs (rekey)
|
||||||
if: matrix.type == 'rekey'
|
if: matrix.type == 'rekey'
|
||||||
run: |
|
run: |
|
||||||
bash testing/static/scripts/generate-configs.sh rekey
|
bash testing/static/scripts/generate-configs.sh rekey
|
||||||
bash testing/static/scripts/rekey-test.sh inject-config
|
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)
|
- name: Start containers (rekey)
|
||||||
if: matrix.type == 'rekey'
|
if: matrix.type == 'rekey'
|
||||||
run: |
|
run: |
|
||||||
@@ -284,20 +273,9 @@ jobs:
|
|||||||
if: matrix.type == 'chaos'
|
if: matrix.type == 'chaos'
|
||||||
run: pip3 install --quiet pyyaml jinja2
|
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
|
- name: Run chaos scenario
|
||||||
if: matrix.type == 'chaos'
|
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)
|
- name: Upload sim results on failure (chaos)
|
||||||
if: matrix.type == 'chaos' && failure()
|
if: matrix.type == 'chaos' && failure()
|
||||||
@@ -308,17 +286,9 @@ jobs:
|
|||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
# ── Sidecar deployment ──────────────────────────────────────────────
|
# ── 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
|
- name: Run sidecar integration test
|
||||||
if: matrix.type == 'sidecar'
|
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)
|
- name: Collect logs on failure (sidecar)
|
||||||
if: matrix.type == 'sidecar' && failure()
|
if: matrix.type == 'sidecar' && failure()
|
||||||
@@ -327,4 +297,4 @@ jobs:
|
|||||||
echo "--- sidecar-${node} logs ---"
|
echo "--- sidecar-${node} logs ---"
|
||||||
docker logs "sidecar-${node}-fips-1" 2>&1 || true
|
docker logs "sidecar-${node}-fips-1" 2>&1 || true
|
||||||
echo ""
|
echo ""
|
||||||
done
|
done
|
||||||
|
|||||||
+7
-1
@@ -22,4 +22,10 @@ reference/
|
|||||||
dist/
|
dist/
|
||||||
*.ipk
|
*.ipk
|
||||||
|
|
||||||
sim-results/
|
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:
|
scenario:
|
||||||
name: "bottleneck-parent"
|
name: "bottleneck-parent"
|
||||||
seed: 42
|
seed: 42
|
||||||
duration_secs: 120
|
duration_secs: 60
|
||||||
|
|
||||||
topology:
|
topology:
|
||||||
algorithm: explicit
|
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"
|
|
||||||
+8
-6
@@ -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
|
# Default: 20 nodes. Override num_nodes with --nodes flag in chaos.sh,
|
||||||
# random transport type by weight. Tests spanning tree convergence,
|
# or copy this file and adjust topology.num_nodes directly.
|
||||||
# link flaps, node churn, and traffic across heterogeneous transports.
|
#
|
||||||
|
# Tests spanning tree convergence, link flaps, node churn, and traffic
|
||||||
|
# across heterogeneous transports at configurable scale.
|
||||||
|
|
||||||
scenario:
|
scenario:
|
||||||
name: "churn-20-mixed"
|
name: "churn-mixed"
|
||||||
seed: 42
|
seed: 42
|
||||||
duration_secs: 600
|
duration_secs: 600
|
||||||
|
|
||||||
@@ -15,7 +17,7 @@ topology:
|
|||||||
params:
|
params:
|
||||||
p: 0.3
|
p: 0.3
|
||||||
ensure_connected: true
|
ensure_connected: true
|
||||||
subnet: "172.20.0.0/23"
|
subnet: "172.20.0.0/16"
|
||||||
ip_start: 10
|
ip_start: 10
|
||||||
transport_mix:
|
transport_mix:
|
||||||
udp: 0.6
|
udp: 0.6
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
scenario:
|
scenario:
|
||||||
name: "cost-avoidance"
|
name: "cost-avoidance"
|
||||||
seed: 42
|
seed: 42
|
||||||
duration_secs: 120
|
duration_secs: 45
|
||||||
|
|
||||||
topology:
|
topology:
|
||||||
algorithm: explicit
|
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:
|
scenario:
|
||||||
name: "depth-vs-cost"
|
name: "depth-vs-cost"
|
||||||
seed: 42
|
seed: 42
|
||||||
duration_secs: 120
|
duration_secs: 45
|
||||||
|
|
||||||
topology:
|
topology:
|
||||||
algorithm: explicit
|
algorithm: explicit
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
scenario:
|
scenario:
|
||||||
name: "ethernet-only"
|
name: "ethernet-only"
|
||||||
seed: 42
|
seed: 42
|
||||||
duration_secs: 90
|
duration_secs: 30
|
||||||
|
|
||||||
topology:
|
topology:
|
||||||
algorithm: explicit
|
algorithm: explicit
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
scenario:
|
scenario:
|
||||||
name: "mixed-technology"
|
name: "mixed-technology"
|
||||||
seed: 42
|
seed: 42
|
||||||
duration_secs: 180
|
duration_secs: 90
|
||||||
|
|
||||||
topology:
|
topology:
|
||||||
algorithm: explicit
|
algorithm: explicit
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
scenario:
|
scenario:
|
||||||
name: "smoke-10"
|
name: "smoke-10"
|
||||||
seed: 42
|
seed: 42
|
||||||
duration_secs: 60
|
duration_secs: 30
|
||||||
|
|
||||||
topology:
|
topology:
|
||||||
num_nodes: 10
|
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
|
# -v, --verbose Enable debug logging
|
||||||
# --seed <N> Override scenario seed
|
# --seed <N> Override scenario seed
|
||||||
# --duration <secs> Override scenario duration
|
# --duration <secs> Override scenario duration
|
||||||
|
# --nodes <N> Override topology.num_nodes
|
||||||
# --list List available scenarios
|
# --list List available scenarios
|
||||||
#
|
#
|
||||||
# Examples:
|
# Examples:
|
||||||
@@ -33,6 +34,7 @@ usage() {
|
|||||||
echo " -v, --verbose Enable debug logging"
|
echo " -v, --verbose Enable debug logging"
|
||||||
echo " --seed <N> Override scenario seed"
|
echo " --seed <N> Override scenario seed"
|
||||||
echo " --duration <secs> Override scenario duration"
|
echo " --duration <secs> Override scenario duration"
|
||||||
|
echo " --nodes <N> Override topology.num_nodes"
|
||||||
echo " --list List available scenarios"
|
echo " --list List available scenarios"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -62,12 +64,14 @@ SCENARIO_ARG=""
|
|||||||
VERBOSE=""
|
VERBOSE=""
|
||||||
SEED=""
|
SEED=""
|
||||||
DURATION=""
|
DURATION=""
|
||||||
|
NODES=""
|
||||||
|
|
||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-v|--verbose) VERBOSE="--verbose"; shift ;;
|
-v|--verbose) VERBOSE="--verbose"; shift ;;
|
||||||
--seed) SEED="$2"; shift 2 ;;
|
--seed) SEED="$2"; shift 2 ;;
|
||||||
--duration) DURATION="$2"; shift 2 ;;
|
--duration) DURATION="$2"; shift 2 ;;
|
||||||
|
--nodes) NODES="$2"; shift 2 ;;
|
||||||
--list) list_scenarios ;;
|
--list) list_scenarios ;;
|
||||||
-*) echo "Error: Unknown option '$1'" >&2; usage ;;
|
-*) echo "Error: Unknown option '$1'" >&2; usage ;;
|
||||||
*)
|
*)
|
||||||
@@ -119,9 +123,10 @@ if ! docker info &> /dev/null; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f "$CHAOS_DIR/fips" ]; then
|
DOCKER_DIR="$CHAOS_DIR/../docker"
|
||||||
echo "Error: FIPS binary not found at $CHAOS_DIR/fips" >&2
|
if [ ! -f "$DOCKER_DIR/fips" ]; then
|
||||||
echo "Run testing/chaos/scripts/build.sh first" >&2
|
echo "Error: FIPS binary not found at $DOCKER_DIR/fips" >&2
|
||||||
|
echo "Run testing/scripts/build.sh first" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -137,8 +142,17 @@ echo " Scenario: $(basename "$SCENARIO_FILE" .yaml)"
|
|||||||
echo " File: $SCENARIO_FILE"
|
echo " File: $SCENARIO_FILE"
|
||||||
[ -n "$SEED" ] && echo " Seed: $SEED (override)"
|
[ -n "$SEED" ] && echo " Seed: $SEED (override)"
|
||||||
[ -n "$DURATION" ] && echo " Duration: ${DURATION}s (override)"
|
[ -n "$DURATION" ] && echo " Duration: ${DURATION}s (override)"
|
||||||
|
[ -n "$NODES" ] && echo " Nodes: $NODES (override)"
|
||||||
echo ""
|
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)
|
# Run from testing/chaos directory (sim expects relative paths)
|
||||||
cd "$CHAOS_DIR"
|
cd "$CHAOS_DIR"
|
||||||
python3 -m sim "${PYTHON_ARGS[@]}"
|
python3 -m sim "${PYTHON_ARGS[@]}"
|
||||||
|
|||||||
@@ -9,8 +9,12 @@ from jinja2 import Template
|
|||||||
from .scenario import Scenario
|
from .scenario import Scenario
|
||||||
from .topology import SimTopology
|
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.
|
# 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(
|
_COMPOSE_TEMPLATE = Template(
|
||||||
"""\
|
"""\
|
||||||
networks:
|
networks:
|
||||||
@@ -21,8 +25,7 @@ networks:
|
|||||||
- subnet: {{ subnet }}
|
- subnet: {{ subnet }}
|
||||||
|
|
||||||
x-fips-common: &fips-common
|
x-fips-common: &fips-common
|
||||||
build:
|
image: {{ image }}
|
||||||
context: ../..
|
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
- NET_RAW
|
- NET_RAW
|
||||||
@@ -36,6 +39,7 @@ x-fips-common: &fips-common
|
|||||||
environment:
|
environment:
|
||||||
- RUST_LOG={{ rust_log }}
|
- RUST_LOG={{ rust_log }}
|
||||||
- RUST_BACKTRACE=1
|
- RUST_BACKTRACE=1
|
||||||
|
- FIPS_TEST_MODE=chaos
|
||||||
|
|
||||||
services:
|
services:
|
||||||
{% for node in nodes %}
|
{% for node in nodes %}
|
||||||
@@ -44,7 +48,6 @@ services:
|
|||||||
container_name: fips-node-{{ node.node_id }}
|
container_name: fips-node-{{ node.node_id }}
|
||||||
hostname: {{ node.node_id }}
|
hostname: {{ node.node_id }}
|
||||||
volumes:
|
volumes:
|
||||||
- ../../resolv.conf:/etc/resolv.conf:ro
|
|
||||||
- ./{{ node.node_id }}.yaml:/etc/fips/fips.yaml:ro
|
- ./{{ node.node_id }}.yaml:/etc/fips/fips.yaml:ro
|
||||||
networks:
|
networks:
|
||||||
fips-net:
|
fips-net:
|
||||||
@@ -67,6 +70,7 @@ def generate_compose(
|
|||||||
content = _COMPOSE_TEMPLATE.render(
|
content = _COMPOSE_TEMPLATE.render(
|
||||||
subnet=scenario.topology.subnet,
|
subnet=scenario.topology.subnet,
|
||||||
rust_log=scenario.logging.rust_log,
|
rust_log=scenario.logging.rust_log,
|
||||||
|
image=FIPS_SIM_IMAGE,
|
||||||
nodes=nodes,
|
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 os
|
||||||
|
import sys
|
||||||
|
|
||||||
# Import derive() from scripts/derive-keys.py
|
# Add testing/ to sys.path so we can import testing.lib.derive_keys
|
||||||
_SCRIPTS_DIR = os.path.join(os.path.dirname(__file__), "..", "scripts")
|
_TESTING_DIR = os.path.join(os.path.dirname(__file__), "..", "..")
|
||||||
_DERIVE_KEYS_PATH = os.path.join(_SCRIPTS_DIR, "derive-keys.py")
|
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)
|
from lib.derive_keys import derive # noqa: E402
|
||||||
_mod = importlib.util.module_from_spec(_spec)
|
|
||||||
_spec.loader.exec_module(_mod)
|
|
||||||
|
|
||||||
derive = _mod.derive # derive(mesh_name, node_name) -> (nsec_hex, npub_bech32)
|
|
||||||
|
|||||||
+22
-113
@@ -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
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
import logging
|
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__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Regex to strip ANSI escape codes from tracing output
|
# Re-export for existing callers
|
||||||
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
|
__all__ = ["AnalysisResult", "analyze_logs", "collect_logs", "write_sim_metadata"]
|
||||||
|
|
||||||
|
|
||||||
@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)
|
|
||||||
|
|
||||||
|
|
||||||
def collect_logs(container_names: list[str], output_dir: str) -> dict[str, str]:
|
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,
|
text=True,
|
||||||
timeout=30,
|
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
|
raw = result.stdout + result.stderr
|
||||||
log_text = _ANSI_RE.sub("", raw)
|
log_text = strip_ansi(raw)
|
||||||
logs[name] = log_text
|
logs[name] = log_text
|
||||||
|
|
||||||
path = os.path.join(output_dir, f"{name}.log")
|
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
|
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(
|
def write_sim_metadata(
|
||||||
output_dir: str,
|
output_dir: str,
|
||||||
scenario_name: str,
|
scenario_name: str,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import signal
|
import signal
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -119,9 +120,14 @@ class SimRunner:
|
|||||||
self.compose_file = generate_compose(self.topology, self.scenario, config_dir)
|
self.compose_file = generate_compose(self.topology, self.scenario, config_dir)
|
||||||
log.info("Wrote %s", self.compose_file)
|
log.info("Wrote %s", self.compose_file)
|
||||||
|
|
||||||
# 4. Build images (reuses Docker cache)
|
# 4. Build the test image once (avoids per-service build at scale)
|
||||||
log.info("Building Docker images...")
|
log.info("Building Docker image...")
|
||||||
docker_compose(self.compose_file, ["build"])
|
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
|
# 5. Start containers
|
||||||
log.info("Starting %d containers...", len(self.topology.nodes))
|
log.info("Starting %d containers...", len(self.topology.nodes))
|
||||||
|
|||||||
+58
-43
@@ -15,9 +15,10 @@
|
|||||||
#
|
#
|
||||||
# Integration suites:
|
# Integration suites:
|
||||||
# static-mesh, static-chain, rekey,
|
# static-mesh, static-chain, rekey,
|
||||||
# chaos-smoke-10, chaos-10, ethernet-mesh, ethernet-only,
|
# chaos-smoke-10, chaos-churn-mixed-10, chaos-ethernet-mesh,
|
||||||
# bottleneck-parent, cost-avoidance, cost-mixed-7node,
|
# chaos-ethernet-only, chaos-tcp-mesh, chaos-bottleneck-parent,
|
||||||
# cost-reeval, cost-stability, depth-vs-cost, mixed-technology,
|
# chaos-cost-avoidance, chaos-cost-reeval, chaos-cost-stability,
|
||||||
|
# chaos-depth-vs-cost, chaos-mixed-technology, chaos-congestion-stress,
|
||||||
# sidecar
|
# sidecar
|
||||||
#
|
#
|
||||||
# Exit codes:
|
# Exit codes:
|
||||||
@@ -47,11 +48,20 @@ ONLY_SUITE=""
|
|||||||
# All integration suites matching ci.yml
|
# All integration suites matching ci.yml
|
||||||
STATIC_SUITES=(static-mesh static-chain)
|
STATIC_SUITES=(static-mesh static-chain)
|
||||||
REKEY_SUITES=(rekey)
|
REKEY_SUITES=(rekey)
|
||||||
|
# Each entry: "display-name scenario [--flag value ...]"
|
||||||
CHAOS_SUITES=(
|
CHAOS_SUITES=(
|
||||||
chaos-smoke-10 chaos-10
|
"smoke-10 smoke-10"
|
||||||
ethernet-mesh ethernet-only
|
"churn-mixed-10 churn-mixed --nodes 10 --duration 120"
|
||||||
bottleneck-parent cost-avoidance cost-mixed-7node
|
"ethernet-mesh ethernet-mesh"
|
||||||
cost-reeval cost-stability depth-vs-cost mixed-technology
|
"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)
|
SIDECAR_SUITES=(sidecar)
|
||||||
|
|
||||||
@@ -83,7 +93,10 @@ list_suites() {
|
|||||||
for s in "${REKEY_SUITES[@]}"; do echo " $s"; done
|
for s in "${REKEY_SUITES[@]}"; do echo " $s"; done
|
||||||
echo ""
|
echo ""
|
||||||
echo " Chaos scenarios:"
|
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 ""
|
||||||
echo " Sidecar:"
|
echo " Sidecar:"
|
||||||
for s in "${SIDECAR_SUITES[@]}"; do echo " $s"; done
|
for s in "${SIDECAR_SUITES[@]}"; do echo " $s"; done
|
||||||
@@ -195,9 +208,6 @@ run_static() {
|
|||||||
info "[$topology] Generating configs"
|
info "[$topology] Generating configs"
|
||||||
bash testing/static/scripts/generate-configs.sh "$topology" || { record "static-$topology" 1; return; }
|
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"
|
info "[$topology] Starting containers"
|
||||||
docker compose -f "$compose" --profile "$topology" up -d || { record "static-$topology" 1; return; }
|
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/generate-configs.sh rekey || { record "rekey" 1; return; }
|
||||||
bash testing/static/scripts/rekey-test.sh inject-config || { 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"
|
info "[rekey] Starting containers"
|
||||||
docker compose -f "$compose" --profile rekey up -d || { record "rekey" 1; return; }
|
docker compose -f "$compose" --profile rekey up -d || { record "rekey" 1; return; }
|
||||||
|
|
||||||
@@ -244,17 +251,18 @@ run_rekey() {
|
|||||||
|
|
||||||
# Run a chaos scenario
|
# Run a chaos scenario
|
||||||
run_chaos() {
|
run_chaos() {
|
||||||
local scenario="$1"
|
local name="$1"
|
||||||
|
shift
|
||||||
local rc=0
|
local rc=0
|
||||||
|
|
||||||
info "[chaos/$scenario] Running simulation"
|
info "[chaos/$name] Running simulation"
|
||||||
if bash testing/chaos/scripts/chaos.sh "$scenario" 2>&1; then
|
if bash testing/chaos/scripts/chaos.sh "$@" 2>&1; then
|
||||||
rc=0
|
rc=0
|
||||||
else
|
else
|
||||||
rc=1
|
rc=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
record "chaos-$scenario" $rc
|
record "chaos-$name" $rc
|
||||||
}
|
}
|
||||||
|
|
||||||
# Run sidecar test
|
# Run sidecar test
|
||||||
@@ -262,7 +270,7 @@ run_sidecar() {
|
|||||||
local rc=0
|
local rc=0
|
||||||
|
|
||||||
info "[sidecar] Running integration test"
|
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
|
rc=0
|
||||||
else
|
else
|
||||||
rc=1
|
rc=1
|
||||||
@@ -275,24 +283,14 @@ run_sidecar() {
|
|||||||
run_integration() {
|
run_integration() {
|
||||||
stage "Stage 3: Integration Tests"
|
stage "Stage 3: Integration Tests"
|
||||||
|
|
||||||
# Install binaries to test directories
|
# Install binaries to shared docker context
|
||||||
info "Installing release binaries to test directories"
|
info "Installing release binaries"
|
||||||
install_binaries testing/static
|
install_binaries testing/docker
|
||||||
install_binaries testing/chaos
|
|
||||||
install_binaries testing/sidecar
|
|
||||||
|
|
||||||
# Build chaos Docker image once (shared by all chaos scenarios)
|
# Build unified test image once (used by all harnesses)
|
||||||
local need_chaos=false
|
info "Building fips-test Docker image"
|
||||||
if [[ -z "$ONLY_SUITE" && "$SKIP_CHAOS" != true ]]; then
|
docker build -t fips-test:latest testing/docker --quiet || { record "docker-build" 1; return; }
|
||||||
need_chaos=true
|
docker build -t fips-test-app:latest -f testing/docker/Dockerfile.app testing/docker --quiet || { record "docker-build-app" 1; return; }
|
||||||
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
|
|
||||||
|
|
||||||
# Single suite mode
|
# Single suite mode
|
||||||
if [[ -n "$ONLY_SUITE" ]]; then
|
if [[ -n "$ONLY_SUITE" ]]; then
|
||||||
@@ -300,7 +298,7 @@ run_integration() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Static topologies (sequential — they share the docker-compose)
|
# Static topologies (sequential — profiles share container names)
|
||||||
for topo in "${STATIC_SUITES[@]}"; do
|
for topo in "${STATIC_SUITES[@]}"; do
|
||||||
local topology="${topo#static-}"
|
local topology="${topo#static-}"
|
||||||
run_static "$topology"
|
run_static "$topology"
|
||||||
@@ -316,8 +314,11 @@ run_integration() {
|
|||||||
local suite_names=()
|
local suite_names=()
|
||||||
local running=0
|
local running=0
|
||||||
|
|
||||||
for suite in "${CHAOS_SUITES[@]}"; do
|
for entry in "${CHAOS_SUITES[@]}"; do
|
||||||
local scenario="${suite#chaos-}"
|
# Parse: "display-name scenario [flags...]"
|
||||||
|
read -ra parts <<< "$entry"
|
||||||
|
local name="${parts[0]}"
|
||||||
|
local args=("${parts[@]:1}")
|
||||||
|
|
||||||
# Throttle: wait for a slot
|
# Throttle: wait for a slot
|
||||||
while [[ $running -ge $PARALLEL_JOBS ]]; do
|
while [[ $running -ge $PARALLEL_JOBS ]]; do
|
||||||
@@ -327,12 +328,12 @@ run_integration() {
|
|||||||
|
|
||||||
# Run in background, capture output to temp file
|
# Run in background, capture output to temp file
|
||||||
local logfile
|
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+=($!)
|
pids+=($!)
|
||||||
suite_names+=("$scenario:$logfile")
|
suite_names+=("$name:$logfile")
|
||||||
running=$((running + 1))
|
running=$((running + 1))
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -369,7 +370,21 @@ run_suite() {
|
|||||||
rekey)
|
rekey)
|
||||||
run_rekey ;;
|
run_rekey ;;
|
||||||
chaos-*)
|
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)
|
sidecar)
|
||||||
run_sidecar ;;
|
run_sidecar ;;
|
||||||
*)
|
*)
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fips
|
||||||
|
fipsctl
|
||||||
|
fipstop
|
||||||
@@ -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
|
FROM debian:trixie-slim
|
||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
@@ -7,7 +10,7 @@ RUN apt-get update && \
|
|||||||
tcpdump netcat-openbsd curl iperf3 && \
|
tcpdump netcat-openbsd curl iperf3 && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
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 && \
|
RUN mkdir -p /var/run/sshd && \
|
||||||
ssh-keygen -A && \
|
ssh-keygen -A && \
|
||||||
sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config && \
|
sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config && \
|
||||||
@@ -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,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()
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Executable
+73
@@ -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:
|
services:
|
||||||
fips:
|
fips:
|
||||||
build:
|
image: fips-test:latest
|
||||||
context: .
|
|
||||||
hostname: fips-sidecar
|
hostname: fips-sidecar
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
@@ -20,6 +19,7 @@ services:
|
|||||||
restart: "no"
|
restart: "no"
|
||||||
environment:
|
environment:
|
||||||
- RUST_LOG=${RUST_LOG:-info}
|
- RUST_LOG=${RUST_LOG:-info}
|
||||||
|
- FIPS_TEST_MODE=sidecar
|
||||||
- FIPS_NSEC=${FIPS_NSEC}
|
- FIPS_NSEC=${FIPS_NSEC}
|
||||||
- FIPS_PEER_NPUB=${FIPS_PEER_NPUB:-}
|
- FIPS_PEER_NPUB=${FIPS_PEER_NPUB:-}
|
||||||
- FIPS_PEER_ADDR=${FIPS_PEER_ADDR:-}
|
- FIPS_PEER_ADDR=${FIPS_PEER_ADDR:-}
|
||||||
@@ -27,19 +27,15 @@ services:
|
|||||||
- FIPS_UDP_BIND=${FIPS_UDP_BIND:-0.0.0.0:2121}
|
- FIPS_UDP_BIND=${FIPS_UDP_BIND:-0.0.0.0:2121}
|
||||||
- FIPS_TUN_MTU=${FIPS_TUN_MTU:-1280}
|
- FIPS_TUN_MTU=${FIPS_TUN_MTU:-1280}
|
||||||
- FIPS_PEER_TRANSPORT=${FIPS_PEER_TRANSPORT:-udp}
|
- FIPS_PEER_TRANSPORT=${FIPS_PEER_TRANSPORT:-udp}
|
||||||
volumes:
|
|
||||||
- ./resolv.conf:/etc/resolv.conf:ro
|
|
||||||
networks:
|
networks:
|
||||||
fips-net:
|
fips-net:
|
||||||
ipv4_address: ${FIPS_IPV4:-172.20.1.20}
|
ipv4_address: ${FIPS_IPV4:-172.20.1.20}
|
||||||
|
|
||||||
app:
|
app:
|
||||||
build:
|
image: fips-test-app:latest
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile.app
|
|
||||||
network_mode: "service:fips"
|
network_mode: "service:fips"
|
||||||
depends_on:
|
depends_on:
|
||||||
- fips
|
- fips
|
||||||
volumes:
|
volumes:
|
||||||
- ./resolv.conf:/etc/resolv.conf:ro
|
- ../docker/resolv.conf:/etc/resolv.conf:ro
|
||||||
command: ["sleep", "infinity"]
|
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 ──────────────────────────────────────────────────────────────────
|
# ── Build ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
if [[ "${1:-}" != "--skip-build" ]]; then
|
if [[ "${1:-}" != "--skip-build" ]]; then
|
||||||
log "Building sidecar images..."
|
log "Building test images..."
|
||||||
docker compose $COMPOSE_BASE build
|
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
|
fi
|
||||||
|
|
||||||
# ── Start nodes ────────────────────────────────────────────────────────────
|
# ── 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
|
- subnet: 172.20.0.0/24
|
||||||
|
|
||||||
x-fips-common: &fips-common
|
x-fips-common: &fips-common
|
||||||
build:
|
image: fips-test:latest
|
||||||
context: .
|
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
devices:
|
devices:
|
||||||
@@ -28,7 +27,7 @@ services:
|
|||||||
container_name: fips-node-a
|
container_name: fips-node-a
|
||||||
hostname: node-a
|
hostname: node-a
|
||||||
volumes:
|
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
|
- ./generated-configs/mesh/node-a.yaml:/etc/fips/fips.yaml:ro
|
||||||
networks:
|
networks:
|
||||||
fips-net:
|
fips-net:
|
||||||
@@ -40,7 +39,7 @@ services:
|
|||||||
container_name: fips-node-b
|
container_name: fips-node-b
|
||||||
hostname: node-b
|
hostname: node-b
|
||||||
volumes:
|
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
|
- ./generated-configs/mesh/node-b.yaml:/etc/fips/fips.yaml:ro
|
||||||
networks:
|
networks:
|
||||||
fips-net:
|
fips-net:
|
||||||
@@ -52,7 +51,7 @@ services:
|
|||||||
container_name: fips-node-c
|
container_name: fips-node-c
|
||||||
hostname: node-c
|
hostname: node-c
|
||||||
volumes:
|
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
|
- ./generated-configs/mesh/node-c.yaml:/etc/fips/fips.yaml:ro
|
||||||
networks:
|
networks:
|
||||||
fips-net:
|
fips-net:
|
||||||
@@ -64,7 +63,7 @@ services:
|
|||||||
container_name: fips-node-d
|
container_name: fips-node-d
|
||||||
hostname: node-d
|
hostname: node-d
|
||||||
volumes:
|
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
|
- ./generated-configs/mesh/node-d.yaml:/etc/fips/fips.yaml:ro
|
||||||
networks:
|
networks:
|
||||||
fips-net:
|
fips-net:
|
||||||
@@ -76,7 +75,7 @@ services:
|
|||||||
container_name: fips-node-e
|
container_name: fips-node-e
|
||||||
hostname: node-e
|
hostname: node-e
|
||||||
volumes:
|
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
|
- ./generated-configs/mesh/node-e.yaml:/etc/fips/fips.yaml:ro
|
||||||
networks:
|
networks:
|
||||||
fips-net:
|
fips-net:
|
||||||
@@ -89,7 +88,7 @@ services:
|
|||||||
container_name: fips-node-a
|
container_name: fips-node-a
|
||||||
hostname: node-a
|
hostname: node-a
|
||||||
volumes:
|
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
|
- ./generated-configs/mesh-public/node-a.yaml:/etc/fips/fips.yaml:ro
|
||||||
networks:
|
networks:
|
||||||
fips-net:
|
fips-net:
|
||||||
@@ -101,7 +100,7 @@ services:
|
|||||||
container_name: fips-node-b
|
container_name: fips-node-b
|
||||||
hostname: node-b
|
hostname: node-b
|
||||||
volumes:
|
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
|
- ./generated-configs/mesh-public/node-b.yaml:/etc/fips/fips.yaml:ro
|
||||||
networks:
|
networks:
|
||||||
fips-net:
|
fips-net:
|
||||||
@@ -113,7 +112,7 @@ services:
|
|||||||
container_name: fips-node-c
|
container_name: fips-node-c
|
||||||
hostname: node-c
|
hostname: node-c
|
||||||
volumes:
|
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
|
- ./generated-configs/mesh-public/node-c.yaml:/etc/fips/fips.yaml:ro
|
||||||
networks:
|
networks:
|
||||||
fips-net:
|
fips-net:
|
||||||
@@ -125,7 +124,7 @@ services:
|
|||||||
container_name: fips-node-d
|
container_name: fips-node-d
|
||||||
hostname: node-d
|
hostname: node-d
|
||||||
volumes:
|
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
|
- ./generated-configs/mesh-public/node-d.yaml:/etc/fips/fips.yaml:ro
|
||||||
networks:
|
networks:
|
||||||
fips-net:
|
fips-net:
|
||||||
@@ -137,7 +136,7 @@ services:
|
|||||||
container_name: fips-node-e
|
container_name: fips-node-e
|
||||||
hostname: node-e
|
hostname: node-e
|
||||||
volumes:
|
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
|
- ./generated-configs/mesh-public/node-e.yaml:/etc/fips/fips.yaml:ro
|
||||||
networks:
|
networks:
|
||||||
fips-net:
|
fips-net:
|
||||||
@@ -150,7 +149,7 @@ services:
|
|||||||
container_name: fips-node-a
|
container_name: fips-node-a
|
||||||
hostname: node-a
|
hostname: node-a
|
||||||
volumes:
|
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
|
- ./generated-configs/chain/node-a.yaml:/etc/fips/fips.yaml:ro
|
||||||
networks:
|
networks:
|
||||||
fips-net:
|
fips-net:
|
||||||
@@ -162,7 +161,7 @@ services:
|
|||||||
container_name: fips-node-b
|
container_name: fips-node-b
|
||||||
hostname: node-b
|
hostname: node-b
|
||||||
volumes:
|
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
|
- ./generated-configs/chain/node-b.yaml:/etc/fips/fips.yaml:ro
|
||||||
networks:
|
networks:
|
||||||
fips-net:
|
fips-net:
|
||||||
@@ -174,7 +173,7 @@ services:
|
|||||||
container_name: fips-node-c
|
container_name: fips-node-c
|
||||||
hostname: node-c
|
hostname: node-c
|
||||||
volumes:
|
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
|
- ./generated-configs/chain/node-c.yaml:/etc/fips/fips.yaml:ro
|
||||||
networks:
|
networks:
|
||||||
fips-net:
|
fips-net:
|
||||||
@@ -186,7 +185,7 @@ services:
|
|||||||
container_name: fips-node-d
|
container_name: fips-node-d
|
||||||
hostname: node-d
|
hostname: node-d
|
||||||
volumes:
|
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
|
- ./generated-configs/chain/node-d.yaml:/etc/fips/fips.yaml:ro
|
||||||
networks:
|
networks:
|
||||||
fips-net:
|
fips-net:
|
||||||
@@ -198,7 +197,7 @@ services:
|
|||||||
container_name: fips-node-e
|
container_name: fips-node-e
|
||||||
hostname: node-e
|
hostname: node-e
|
||||||
volumes:
|
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
|
- ./generated-configs/chain/node-e.yaml:/etc/fips/fips.yaml:ro
|
||||||
networks:
|
networks:
|
||||||
fips-net:
|
fips-net:
|
||||||
@@ -211,7 +210,7 @@ services:
|
|||||||
container_name: fips-node-a
|
container_name: fips-node-a
|
||||||
hostname: node-a
|
hostname: node-a
|
||||||
volumes:
|
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
|
- ./generated-configs/rekey/node-a.yaml:/etc/fips/fips.yaml:ro
|
||||||
networks:
|
networks:
|
||||||
fips-net:
|
fips-net:
|
||||||
@@ -223,7 +222,7 @@ services:
|
|||||||
container_name: fips-node-b
|
container_name: fips-node-b
|
||||||
hostname: node-b
|
hostname: node-b
|
||||||
volumes:
|
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
|
- ./generated-configs/rekey/node-b.yaml:/etc/fips/fips.yaml:ro
|
||||||
networks:
|
networks:
|
||||||
fips-net:
|
fips-net:
|
||||||
@@ -235,7 +234,7 @@ services:
|
|||||||
container_name: fips-node-c
|
container_name: fips-node-c
|
||||||
hostname: node-c
|
hostname: node-c
|
||||||
volumes:
|
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
|
- ./generated-configs/rekey/node-c.yaml:/etc/fips/fips.yaml:ro
|
||||||
networks:
|
networks:
|
||||||
fips-net:
|
fips-net:
|
||||||
@@ -247,7 +246,7 @@ services:
|
|||||||
container_name: fips-node-d
|
container_name: fips-node-d
|
||||||
hostname: node-d
|
hostname: node-d
|
||||||
volumes:
|
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
|
- ./generated-configs/rekey/node-d.yaml:/etc/fips/fips.yaml:ro
|
||||||
networks:
|
networks:
|
||||||
fips-net:
|
fips-net:
|
||||||
@@ -259,7 +258,7 @@ services:
|
|||||||
container_name: fips-node-e
|
container_name: fips-node-e
|
||||||
hostname: node-e
|
hostname: node-e
|
||||||
volumes:
|
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
|
- ./generated-configs/rekey/node-e.yaml:/etc/fips/fips.yaml:ro
|
||||||
networks:
|
networks:
|
||||||
fips-net:
|
fips-net:
|
||||||
@@ -272,7 +271,7 @@ services:
|
|||||||
container_name: fips-node-a
|
container_name: fips-node-a
|
||||||
hostname: node-a
|
hostname: node-a
|
||||||
volumes:
|
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
|
- ./generated-configs/tcp-chain/node-a.yaml:/etc/fips/fips.yaml:ro
|
||||||
networks:
|
networks:
|
||||||
fips-net:
|
fips-net:
|
||||||
@@ -284,7 +283,7 @@ services:
|
|||||||
container_name: fips-node-b
|
container_name: fips-node-b
|
||||||
hostname: node-b
|
hostname: node-b
|
||||||
volumes:
|
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
|
- ./generated-configs/tcp-chain/node-b.yaml:/etc/fips/fips.yaml:ro
|
||||||
networks:
|
networks:
|
||||||
fips-net:
|
fips-net:
|
||||||
@@ -296,7 +295,7 @@ services:
|
|||||||
container_name: fips-node-c
|
container_name: fips-node-c
|
||||||
hostname: node-c
|
hostname: node-c
|
||||||
volumes:
|
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
|
- ./generated-configs/tcp-chain/node-c.yaml:/etc/fips/fips.yaml:ro
|
||||||
networks:
|
networks:
|
||||||
fips-net:
|
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"
|
CONFIG_DIR="$SCRIPT_DIR/../configs"
|
||||||
GENERATED_DIR="$SCRIPT_DIR/../generated-configs"
|
GENERATED_DIR="$SCRIPT_DIR/../generated-configs"
|
||||||
TEMPLATE_FILE="$CONFIG_DIR/node.template.yaml"
|
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
|
# Parse topology YAML to extract node attributes
|
||||||
# Usage: get_node_attr <topology_file> <node_id> <attr_name>
|
# Usage: get_node_attr <topology_file> <node_id> <attr_name>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ FAILED=0
|
|||||||
|
|
||||||
# Node identities (from generated env file)
|
# Node identities (from generated env file)
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$SCRIPT_DIR/../../lib/wait-converge.sh"
|
||||||
ENV_FILE="$SCRIPT_DIR/../generated-configs/npubs.env"
|
ENV_FILE="$SCRIPT_DIR/../generated-configs/npubs.env"
|
||||||
if [ ! -f "$ENV_FILE" ]; then
|
if [ ! -f "$ENV_FILE" ]; then
|
||||||
echo "Error: $ENV_FILE not found. Run generate-configs.sh first." >&2
|
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 "=== FIPS Ping Test ($PROFILE topology) ==="
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Wait for nodes to converge
|
# Wait for nodes to converge — peers + discovery propagation
|
||||||
echo "Waiting 5s for mesh convergence..."
|
echo "Waiting for mesh convergence..."
|
||||||
sleep 5
|
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
|
if [ "$PROFILE" = "mesh" ] || [ "$PROFILE" = "mesh-public" ]; then
|
||||||
# Sparse mesh topology: A-B, B-C, C-D, D-E, E-A, A-D
|
# 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
|
# [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
|
# 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
|
# the onion service via HiddenServiceDir. Node B connects outbound through
|
||||||
# outbound through its local Tor's SOCKS5 proxy.
|
# its local Tor's SOCKS5 proxy.
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
dir-test:
|
dir-test:
|
||||||
@@ -14,9 +14,7 @@ networks:
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
fips-a:
|
fips-a:
|
||||||
build:
|
image: fips-test:latest
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile.colocated
|
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
devices:
|
devices:
|
||||||
@@ -28,17 +26,15 @@ services:
|
|||||||
hostname: fips-dir-a
|
hostname: fips-dir-a
|
||||||
volumes:
|
volumes:
|
||||||
- ./configs/node-a.yaml:/etc/fips/fips.yaml:ro
|
- ./configs/node-a.yaml:/etc/fips/fips.yaml:ro
|
||||||
|
- ./torrc:/etc/tor/torrc:ro
|
||||||
environment:
|
environment:
|
||||||
- RUST_LOG=info,fips::transport::tor=debug
|
- RUST_LOG=info,fips::transport::tor=debug
|
||||||
|
- FIPS_TEST_MODE=tor-directory
|
||||||
networks:
|
networks:
|
||||||
dir-test:
|
dir-test:
|
||||||
|
|
||||||
fips-b:
|
fips-b:
|
||||||
build:
|
image: fips-test:latest
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile.colocated
|
|
||||||
args:
|
|
||||||
TORRC: torrc.socks5
|
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
devices:
|
devices:
|
||||||
@@ -50,7 +46,9 @@ services:
|
|||||||
hostname: fips-dir-b
|
hostname: fips-dir-b
|
||||||
volumes:
|
volumes:
|
||||||
- ./configs/node-b.yaml:/etc/fips/fips.yaml:ro
|
- ./configs/node-b.yaml:/etc/fips/fips.yaml:ro
|
||||||
|
- ./torrc.socks5:/etc/tor/torrc:ro
|
||||||
environment:
|
environment:
|
||||||
- RUST_LOG=info,fips::transport::tor=debug
|
- RUST_LOG=info,fips::transport::tor=debug
|
||||||
|
- FIPS_TEST_MODE=tor-directory
|
||||||
networks:
|
networks:
|
||||||
dir-test:
|
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)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
TEST_DIR="$SCRIPT_DIR/.."
|
TEST_DIR="$SCRIPT_DIR/.."
|
||||||
DERIVE_KEYS="$SCRIPT_DIR/../../../static/scripts/derive-keys.py"
|
DERIVE_KEYS="$SCRIPT_DIR/../../../lib/derive_keys.py"
|
||||||
cd "$TEST_DIR"
|
cd "$TEST_DIR"
|
||||||
|
|
||||||
PASSED=0
|
PASSED=0
|
||||||
@@ -84,7 +84,7 @@ echo ""
|
|||||||
# ── Phase 1: Start node A (Tor + FIPS co-located) ────────────────
|
# ── Phase 1: Start node A (Tor + FIPS co-located) ────────────────
|
||||||
echo "Phase 1: Starting node A (Tor+FIPS, directory-mode onion service)..."
|
echo "Phase 1: Starting node A (Tor+FIPS, directory-mode onion service)..."
|
||||||
docker compose down 2>/dev/null || true
|
docker compose down 2>/dev/null || true
|
||||||
docker compose up -d --build fips-a
|
docker compose up -d fips-a
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# ── Phase 2: Wait for onion service creation ─────────────────────
|
# ── Phase 2: Wait for onion service creation ─────────────────────
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ services:
|
|||||||
tor-test:
|
tor-test:
|
||||||
|
|
||||||
fips-a:
|
fips-a:
|
||||||
build:
|
image: fips-test:latest
|
||||||
context: ../common
|
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
devices:
|
devices:
|
||||||
@@ -36,16 +35,15 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- tor-daemon
|
- tor-daemon
|
||||||
volumes:
|
volumes:
|
||||||
- ../common/resolv.conf:/etc/resolv.conf:ro
|
|
||||||
- ./configs/node-a.yaml:/etc/fips/fips.yaml:ro
|
- ./configs/node-a.yaml:/etc/fips/fips.yaml:ro
|
||||||
environment:
|
environment:
|
||||||
- RUST_LOG=info,fips::transport::tor=debug
|
- RUST_LOG=info,fips::transport::tor=debug
|
||||||
|
- FIPS_TEST_MODE=tor-socks5
|
||||||
networks:
|
networks:
|
||||||
tor-test:
|
tor-test:
|
||||||
|
|
||||||
fips-b:
|
fips-b:
|
||||||
build:
|
image: fips-test:latest
|
||||||
context: ../common
|
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
devices:
|
devices:
|
||||||
@@ -58,9 +56,9 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- tor-daemon
|
- tor-daemon
|
||||||
volumes:
|
volumes:
|
||||||
- ../common/resolv.conf:/etc/resolv.conf:ro
|
|
||||||
- ./configs/node-b.yaml:/etc/fips/fips.yaml:ro
|
- ./configs/node-b.yaml:/etc/fips/fips.yaml:ro
|
||||||
environment:
|
environment:
|
||||||
- RUST_LOG=info,fips::transport::tor=debug
|
- RUST_LOG=info,fips::transport::tor=debug
|
||||||
|
- FIPS_TEST_MODE=tor-socks5
|
||||||
networks:
|
networks:
|
||||||
tor-test:
|
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)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
TOR_DIR="$SCRIPT_DIR/.."
|
TOR_DIR="$SCRIPT_DIR/.."
|
||||||
DERIVE_KEYS="$SCRIPT_DIR/../../../static/scripts/derive-keys.py"
|
DERIVE_KEYS="$SCRIPT_DIR/../../../lib/derive_keys.py"
|
||||||
cd "$TOR_DIR"
|
cd "$TOR_DIR"
|
||||||
|
|
||||||
PASSED=0
|
PASSED=0
|
||||||
@@ -80,7 +80,7 @@ echo ""
|
|||||||
# ── Phase 1: Build and start ─────────────────────────────────────
|
# ── Phase 1: Build and start ─────────────────────────────────────
|
||||||
echo "Phase 1: Starting Tor daemon and FIPS nodes..."
|
echo "Phase 1: Starting Tor daemon and FIPS nodes..."
|
||||||
docker compose down 2>/dev/null || true
|
docker compose down 2>/dev/null || true
|
||||||
docker compose up -d --build
|
docker compose up -d
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# ── Phase 2: Wait for Tor bootstrap ─────────────────────────────
|
# ── Phase 2: Wait for Tor bootstrap ─────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user