diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b0a9bf..836f49b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -125,8 +125,8 @@ jobs: # Runs only when both build and test succeed. Each topology / scenario is a # separate matrix entry so they run in parallel. # -# Static topologies → build Docker images, start containers, ping-test -# Chaos scenarios → build sim image, run stochastic simulation +# All harnesses share a single Docker image (fips-test:latest) built once +# in the setup step from testing/docker/. # ───────────────────────────────────────────────────────────────────────────── integration: name: Integration (${{ matrix.suite }}) @@ -153,24 +153,25 @@ jobs: - suite: chaos-smoke-10 type: chaos scenario: smoke-10 - - suite: chaos-10 + - suite: churn-mixed-10 type: chaos - scenario: chaos-10 + scenario: churn-mixed + chaos_flags: "--nodes 10 --duration 120" - suite: ethernet-mesh type: chaos scenario: ethernet-mesh - suite: ethernet-only type: chaos scenario: ethernet-only + - suite: tcp-mesh + type: chaos + scenario: tcp-mesh - suite: bottleneck-parent type: chaos scenario: bottleneck-parent - suite: cost-avoidance type: chaos scenario: cost-avoidance - - suite: cost-mixed-7node - type: chaos - scenario: cost-mixed-7node - suite: cost-reeval type: chaos scenario: cost-reeval @@ -183,6 +184,9 @@ jobs: - suite: mixed-technology type: chaos scenario: mixed-technology + - suite: congestion-stress + type: chaos + scenario: congestion-stress # ── Sidecar deployment ────────────────────────────────────────── - suite: sidecar type: sidecar @@ -197,24 +201,22 @@ jobs: name: fips-linux path: _bin - # ── Static topology ──────────────────────────────────────────────────── - - name: Install binary (static) - if: matrix.type == 'static' + # Install binaries to unified docker context and build shared image + - name: Install binaries and build Docker image run: | chmod +x _bin/fips _bin/fipsctl - cp _bin/fips testing/static/fips - cp _bin/fipsctl testing/static/fipsctl + [ -f _bin/fipstop ] && chmod +x _bin/fipstop || true + cp _bin/fips testing/docker/fips + cp _bin/fipsctl testing/docker/fipsctl + [ -f _bin/fipstop ] && cp _bin/fipstop testing/docker/fipstop || true + docker build -t fips-test:latest testing/docker + docker build -t fips-test-app:latest -f testing/docker/Dockerfile.app testing/docker + # ── Static topology ──────────────────────────────────────────────────── - name: Generate configs (static) if: matrix.type == 'static' run: bash testing/static/scripts/generate-configs.sh ${{ matrix.topology }} - - name: Build Docker images (static) - if: matrix.type == 'static' - run: | - docker compose -f testing/static/docker-compose.yml \ - --profile ${{ matrix.topology }} build - - name: Start containers (static) if: matrix.type == 'static' run: | @@ -238,25 +240,12 @@ jobs: --profile ${{ matrix.topology }} down --volumes --remove-orphans # ── Rekey integration test ────────────────────────────────────────────── - - name: Install binary (rekey) - if: matrix.type == 'rekey' - run: | - chmod +x _bin/fips _bin/fipsctl - cp _bin/fips testing/static/fips - cp _bin/fipsctl testing/static/fipsctl - - name: Generate and inject configs (rekey) if: matrix.type == 'rekey' run: | bash testing/static/scripts/generate-configs.sh rekey bash testing/static/scripts/rekey-test.sh inject-config - - name: Build Docker images (rekey) - if: matrix.type == 'rekey' - run: | - docker compose -f testing/static/docker-compose.yml \ - --profile rekey build - - name: Start containers (rekey) if: matrix.type == 'rekey' run: | @@ -284,20 +273,9 @@ jobs: if: matrix.type == 'chaos' run: pip3 install --quiet pyyaml jinja2 - - name: Install binary (chaos) - if: matrix.type == 'chaos' - run: | - chmod +x _bin/fips _bin/fipsctl - cp _bin/fips testing/chaos/fips - cp _bin/fipsctl testing/chaos/fipsctl - - - name: Build chaos Docker image - if: matrix.type == 'chaos' - run: docker build -t fips-chaos:latest testing/chaos - - name: Run chaos scenario if: matrix.type == 'chaos' - run: bash testing/chaos/scripts/chaos.sh ${{ matrix.scenario }} + run: bash testing/chaos/scripts/chaos.sh ${{ matrix.scenario }} ${{ matrix.chaos_flags }} - name: Upload sim results on failure (chaos) if: matrix.type == 'chaos' && failure() @@ -308,17 +286,9 @@ jobs: retention-days: 7 # ── Sidecar deployment ────────────────────────────────────────────── - - name: Install binary (sidecar) - if: matrix.type == 'sidecar' - run: | - chmod +x _bin/fips _bin/fipsctl _bin/fipstop - cp _bin/fips testing/sidecar/fips - cp _bin/fipsctl testing/sidecar/fipsctl - cp _bin/fipstop testing/sidecar/fipstop - - name: Run sidecar integration test if: matrix.type == 'sidecar' - run: bash testing/sidecar/scripts/test-sidecar.sh + run: bash testing/sidecar/scripts/test-sidecar.sh --skip-build - name: Collect logs on failure (sidecar) if: matrix.type == 'sidecar' && failure() @@ -327,4 +297,4 @@ jobs: echo "--- sidecar-${node} logs ---" docker logs "sidecar-${node}-fips-1" 2>&1 || true echo "" - done \ No newline at end of file + done diff --git a/.gitignore b/.gitignore index 627e9d7..8f39f87 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,10 @@ reference/ dist/ *.ipk -sim-results/ \ No newline at end of file +sim-results/ + +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +*.egg \ No newline at end of file diff --git a/testing/chaos/Dockerfile b/testing/chaos/Dockerfile deleted file mode 100644 index 8eb90b4..0000000 --- a/testing/chaos/Dockerfile +++ /dev/null @@ -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"] diff --git a/testing/chaos/entrypoint.sh b/testing/chaos/entrypoint.sh deleted file mode 100755 index c0e13c0..0000000 --- a/testing/chaos/entrypoint.sh +++ /dev/null @@ -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: " 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" diff --git a/testing/chaos/scenarios/bottleneck-parent.yaml b/testing/chaos/scenarios/bottleneck-parent.yaml index cb847ae..396059d 100644 --- a/testing/chaos/scenarios/bottleneck-parent.yaml +++ b/testing/chaos/scenarios/bottleneck-parent.yaml @@ -35,7 +35,7 @@ scenario: name: "bottleneck-parent" seed: 42 - duration_secs: 120 + duration_secs: 60 topology: algorithm: explicit diff --git a/testing/chaos/scenarios/chaos-10.yaml b/testing/chaos/scenarios/chaos-10.yaml deleted file mode 100644 index 27f8ce5..0000000 --- a/testing/chaos/scenarios/chaos-10.yaml +++ /dev/null @@ -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" diff --git a/testing/chaos/scenarios/churn-10.yaml b/testing/chaos/scenarios/churn-10.yaml deleted file mode 100644 index 1313138..0000000 --- a/testing/chaos/scenarios/churn-10.yaml +++ /dev/null @@ -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" diff --git a/testing/chaos/scenarios/churn-20.yaml b/testing/chaos/scenarios/churn-20.yaml deleted file mode 100644 index c4ab40c..0000000 --- a/testing/chaos/scenarios/churn-20.yaml +++ /dev/null @@ -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" diff --git a/testing/chaos/scenarios/churn-20-mixed.yaml b/testing/chaos/scenarios/churn-mixed.yaml similarity index 75% rename from testing/chaos/scenarios/churn-20-mixed.yaml rename to testing/chaos/scenarios/churn-mixed.yaml index 013beba..961ccf2 100644 --- a/testing/chaos/scenarios/churn-20-mixed.yaml +++ b/testing/chaos/scenarios/churn-mixed.yaml @@ -1,11 +1,13 @@ -# 20-node churn test with mixed transports (UDP, Ethernet, TCP) +# Parameterized churn test with mixed transports (UDP, Ethernet, TCP) # -# Based on churn-20 but with transport_mix assigning each edge a -# random transport type by weight. Tests spanning tree convergence, -# link flaps, node churn, and traffic across heterogeneous transports. +# Default: 20 nodes. Override num_nodes with --nodes flag in chaos.sh, +# or copy this file and adjust topology.num_nodes directly. +# +# Tests spanning tree convergence, link flaps, node churn, and traffic +# across heterogeneous transports at configurable scale. scenario: - name: "churn-20-mixed" + name: "churn-mixed" seed: 42 duration_secs: 600 @@ -15,7 +17,7 @@ topology: params: p: 0.3 ensure_connected: true - subnet: "172.20.0.0/23" + subnet: "172.20.0.0/16" ip_start: 10 transport_mix: udp: 0.6 diff --git a/testing/chaos/scenarios/cost-avoidance.yaml b/testing/chaos/scenarios/cost-avoidance.yaml index ffe63c2..c89861a 100644 --- a/testing/chaos/scenarios/cost-avoidance.yaml +++ b/testing/chaos/scenarios/cost-avoidance.yaml @@ -22,7 +22,7 @@ scenario: name: "cost-avoidance" seed: 42 - duration_secs: 120 + duration_secs: 45 topology: algorithm: explicit diff --git a/testing/chaos/scenarios/cost-mixed-7node.yaml b/testing/chaos/scenarios/cost-mixed-7node.yaml deleted file mode 100644 index cf13c96..0000000 --- a/testing/chaos/scenarios/cost-mixed-7node.yaml +++ /dev/null @@ -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" diff --git a/testing/chaos/scenarios/depth-vs-cost.yaml b/testing/chaos/scenarios/depth-vs-cost.yaml index 5fb845e..ab2d288 100644 --- a/testing/chaos/scenarios/depth-vs-cost.yaml +++ b/testing/chaos/scenarios/depth-vs-cost.yaml @@ -27,7 +27,7 @@ scenario: name: "depth-vs-cost" seed: 42 - duration_secs: 120 + duration_secs: 45 topology: algorithm: explicit diff --git a/testing/chaos/scenarios/ethernet-only.yaml b/testing/chaos/scenarios/ethernet-only.yaml index e3b0dc7..ee34089 100644 --- a/testing/chaos/scenarios/ethernet-only.yaml +++ b/testing/chaos/scenarios/ethernet-only.yaml @@ -15,7 +15,7 @@ scenario: name: "ethernet-only" seed: 42 - duration_secs: 90 + duration_secs: 30 topology: algorithm: explicit diff --git a/testing/chaos/scenarios/mixed-technology.yaml b/testing/chaos/scenarios/mixed-technology.yaml index 2da4c29..0fb240e 100644 --- a/testing/chaos/scenarios/mixed-technology.yaml +++ b/testing/chaos/scenarios/mixed-technology.yaml @@ -34,7 +34,7 @@ scenario: name: "mixed-technology" seed: 42 - duration_secs: 180 + duration_secs: 90 topology: algorithm: explicit diff --git a/testing/chaos/scenarios/smoke-10.yaml b/testing/chaos/scenarios/smoke-10.yaml index 3c1fde8..cc36915 100644 --- a/testing/chaos/scenarios/smoke-10.yaml +++ b/testing/chaos/scenarios/smoke-10.yaml @@ -1,7 +1,7 @@ scenario: name: "smoke-10" seed: 42 - duration_secs: 60 + duration_secs: 30 topology: num_nodes: 10 diff --git a/testing/chaos/scenarios/tcp-chain.yaml b/testing/chaos/scenarios/tcp-chain.yaml deleted file mode 100644 index 6630a3c..0000000 --- a/testing/chaos/scenarios/tcp-chain.yaml +++ /dev/null @@ -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" diff --git a/testing/chaos/scenarios/tcp-only.yaml b/testing/chaos/scenarios/tcp-only.yaml deleted file mode 100644 index 530a145..0000000 --- a/testing/chaos/scenarios/tcp-only.yaml +++ /dev/null @@ -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" diff --git a/testing/chaos/scripts/build.sh b/testing/chaos/scripts/build.sh deleted file mode 100755 index 77de126..0000000 --- a/testing/chaos/scripts/build.sh +++ /dev/null @@ -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: /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}" diff --git a/testing/chaos/scripts/chaos.sh b/testing/chaos/scripts/chaos.sh index 803b2b3..b6ed978 100755 --- a/testing/chaos/scripts/chaos.sh +++ b/testing/chaos/scripts/chaos.sh @@ -8,6 +8,7 @@ # -v, --verbose Enable debug logging # --seed Override scenario seed # --duration Override scenario duration +# --nodes Override topology.num_nodes # --list List available scenarios # # Examples: @@ -33,6 +34,7 @@ usage() { echo " -v, --verbose Enable debug logging" echo " --seed Override scenario seed" echo " --duration Override scenario duration" + echo " --nodes Override topology.num_nodes" echo " --list List available scenarios" exit 1 } @@ -62,12 +64,14 @@ SCENARIO_ARG="" VERBOSE="" SEED="" DURATION="" +NODES="" while [ $# -gt 0 ]; do case "$1" in -v|--verbose) VERBOSE="--verbose"; shift ;; --seed) SEED="$2"; shift 2 ;; --duration) DURATION="$2"; shift 2 ;; + --nodes) NODES="$2"; shift 2 ;; --list) list_scenarios ;; -*) echo "Error: Unknown option '$1'" >&2; usage ;; *) @@ -119,9 +123,10 @@ if ! docker info &> /dev/null; then exit 1 fi -if [ ! -f "$CHAOS_DIR/fips" ]; then - echo "Error: FIPS binary not found at $CHAOS_DIR/fips" >&2 - echo "Run testing/chaos/scripts/build.sh first" >&2 +DOCKER_DIR="$CHAOS_DIR/../docker" +if [ ! -f "$DOCKER_DIR/fips" ]; then + echo "Error: FIPS binary not found at $DOCKER_DIR/fips" >&2 + echo "Run testing/scripts/build.sh first" >&2 exit 1 fi @@ -137,8 +142,17 @@ echo " Scenario: $(basename "$SCENARIO_FILE" .yaml)" echo " File: $SCENARIO_FILE" [ -n "$SEED" ] && echo " Seed: $SEED (override)" [ -n "$DURATION" ] && echo " Duration: ${DURATION}s (override)" +[ -n "$NODES" ] && echo " Nodes: $NODES (override)" echo "" +# If --nodes is specified, create a patched copy of the scenario file +if [ -n "$NODES" ]; then + PATCHED=$(mktemp /tmp/chaos-scenario-XXXXXX.yaml) + sed "s/^\( num_nodes:\).*/\1 $NODES/" "$SCENARIO_FILE" > "$PATCHED" + PYTHON_ARGS[0]="$PATCHED" + trap 'rm -f "$PATCHED"' EXIT +fi + # Run from testing/chaos directory (sim expects relative paths) cd "$CHAOS_DIR" python3 -m sim "${PYTHON_ARGS[@]}" diff --git a/testing/chaos/sim/compose.py b/testing/chaos/sim/compose.py index 38a2e56..514ed67 100644 --- a/testing/chaos/sim/compose.py +++ b/testing/chaos/sim/compose.py @@ -9,8 +9,12 @@ from jinja2 import Template from .scenario import Scenario from .topology import SimTopology +# Image name for the pre-built FIPS test image. +# The runner builds this once before starting containers. +FIPS_SIM_IMAGE = "fips-test:latest" + # Jinja2 template for the compose file. -# build context points back to the testing/chaos root where the Dockerfile lives. +# Uses a pre-built image instead of per-service build to support large topologies. _COMPOSE_TEMPLATE = Template( """\ networks: @@ -21,8 +25,7 @@ networks: - subnet: {{ subnet }} x-fips-common: &fips-common - build: - context: ../.. + image: {{ image }} cap_add: - NET_ADMIN - NET_RAW @@ -36,6 +39,7 @@ x-fips-common: &fips-common environment: - RUST_LOG={{ rust_log }} - RUST_BACKTRACE=1 + - FIPS_TEST_MODE=chaos services: {% for node in nodes %} @@ -44,7 +48,6 @@ services: container_name: fips-node-{{ node.node_id }} hostname: {{ node.node_id }} volumes: - - ../../resolv.conf:/etc/resolv.conf:ro - ./{{ node.node_id }}.yaml:/etc/fips/fips.yaml:ro networks: fips-net: @@ -67,6 +70,7 @@ def generate_compose( content = _COMPOSE_TEMPLATE.render( subnet=scenario.topology.subnet, rust_log=scenario.logging.rust_log, + image=FIPS_SIM_IMAGE, nodes=nodes, ) diff --git a/testing/chaos/sim/keys.py b/testing/chaos/sim/keys.py index 6fd3ed4..dd2dd24 100644 --- a/testing/chaos/sim/keys.py +++ b/testing/chaos/sim/keys.py @@ -1,14 +1,11 @@ -"""Key derivation wrapper, reusing logic from scripts/derive-keys.py.""" +"""Key derivation wrapper, importing from shared testing library.""" -import importlib.util import os +import sys -# Import derive() from scripts/derive-keys.py -_SCRIPTS_DIR = os.path.join(os.path.dirname(__file__), "..", "scripts") -_DERIVE_KEYS_PATH = os.path.join(_SCRIPTS_DIR, "derive-keys.py") +# Add testing/ to sys.path so we can import testing.lib.derive_keys +_TESTING_DIR = os.path.join(os.path.dirname(__file__), "..", "..") +if _TESTING_DIR not in sys.path: + sys.path.insert(0, _TESTING_DIR) -_spec = importlib.util.spec_from_file_location("derive_keys", _DERIVE_KEYS_PATH) -_mod = importlib.util.module_from_spec(_spec) -_spec.loader.exec_module(_mod) - -derive = _mod.derive # derive(mesh_name, node_name) -> (nsec_hex, npub_bech32) +from lib.derive_keys import derive # noqa: E402 diff --git a/testing/chaos/sim/logs.py b/testing/chaos/sim/logs.py index 5aff9e6..0033635 100644 --- a/testing/chaos/sim/logs.py +++ b/testing/chaos/sim/logs.py @@ -1,72 +1,31 @@ -"""Log collection and post-run analysis.""" +"""Log collection and post-run analysis for chaos simulations. + +Delegates core analysis to the shared testing.lib.log_analysis module. +Adds chaos-specific collection (Docker container logs, sim metadata). +""" from __future__ import annotations -import os -import re -import subprocess import logging -from dataclasses import dataclass, field +import os +import subprocess + +# Import shared analysis from testing/lib/ +import sys +_TESTING_DIR = os.path.join(os.path.dirname(__file__), "..", "..") +if _TESTING_DIR not in sys.path: + sys.path.insert(0, _TESTING_DIR) + +from lib.log_analysis import ( # noqa: E402 + AnalysisResult, + analyze_logs, + strip_ansi, +) log = logging.getLogger(__name__) -# Regex to strip ANSI escape codes from tracing output -_ANSI_RE = re.compile(r"\x1b\[[0-9;]*m") - - -@dataclass -class AnalysisResult: - errors: list[tuple[str, str]] = field(default_factory=list) - warnings: list[tuple[str, str]] = field(default_factory=list) - sessions_established: list[tuple[str, str]] = field(default_factory=list) - peers_promoted: list[tuple[str, str]] = field(default_factory=list) - peer_removals: list[tuple[str, str]] = field(default_factory=list) - parent_switches: list[tuple[str, str]] = field(default_factory=list) - mmp_link_metrics: list[tuple[str, str]] = field(default_factory=list) - mmp_session_metrics: list[tuple[str, str]] = field(default_factory=list) - handshake_timeouts: list[tuple[str, str]] = field(default_factory=list) - panics: list[tuple[str, str]] = field(default_factory=list) - congestion_detected: list[tuple[str, str]] = field(default_factory=list) - kernel_drop_events: list[tuple[str, str]] = field(default_factory=list) - - def summary(self) -> str: - lines = [ - "=== Simulation Analysis ===", - "", - f"Panics: {len(self.panics)}", - f"Errors: {len(self.errors)}", - f"Warnings: {len(self.warnings)}", - f"Sessions established: {len(self.sessions_established)}", - f"Peers promoted: {len(self.peers_promoted)}", - f"Peer removals: {len(self.peer_removals)}", - f"Parent switches: {len(self.parent_switches)}", - f"Handshake timeouts: {len(self.handshake_timeouts)}", - f"MMP link samples: {len(self.mmp_link_metrics)}", - f"MMP session samples: {len(self.mmp_session_metrics)}", - f"Congestion events: {len(self.congestion_detected)}", - f"Kernel drop events: {len(self.kernel_drop_events)}", - ] - - if self.panics: - lines.append("") - lines.append("--- PANICS ---") - for container, line in self.panics[:10]: - lines.append(f" [{container}] {line.strip()}") - - if self.errors: - lines.append("") - lines.append("--- ERRORS (first 20) ---") - for container, line in self.errors[:20]: - lines.append(f" [{container}] {line.strip()}") - - if self.handshake_timeouts: - lines.append("") - lines.append("--- HANDSHAKE TIMEOUTS (first 10) ---") - for container, line in self.handshake_timeouts[:10]: - lines.append(f" [{container}] {line.strip()}") - - lines.append("") - return "\n".join(lines) +# Re-export for existing callers +__all__ = ["AnalysisResult", "analyze_logs", "collect_logs", "write_sim_metadata"] def collect_logs(container_names: list[str], output_dir: str) -> dict[str, str]: @@ -82,11 +41,8 @@ def collect_logs(container_names: list[str], output_dir: str) -> dict[str, str]: text=True, timeout=30, ) - # Combine stdout and stderr — tracing may go to either - # depending on the subscriber configuration. - # Strip ANSI escape codes for clean log files. raw = result.stdout + result.stderr - log_text = _ANSI_RE.sub("", raw) + log_text = strip_ansi(raw) logs[name] = log_text path = os.path.join(output_dir, f"{name}.log") @@ -100,53 +56,6 @@ def collect_logs(container_names: list[str], output_dir: str) -> dict[str, str]: return logs -def analyze_logs(logs: dict[str, str]) -> AnalysisResult: - """Parse structured tracing output and categorize events.""" - result = AnalysisResult() - - for container, log_text in logs.items(): - for raw_line in log_text.splitlines(): - # Strip ANSI escape codes for reliable matching - line = _ANSI_RE.sub("", raw_line) - - # Panics - if "panicked" in line or "PANIC" in line: - result.panics.append((container, line)) - # Errors and warnings - elif " ERROR " in line: - result.errors.append((container, line)) - elif " WARN " in line: - result.warnings.append((container, line)) - - # Session establishment - if "Session established" in line: - result.sessions_established.append((container, line)) - # Peer promotion - if "Inbound peer promoted" in line or "Outbound handshake completed" in line: - result.peers_promoted.append((container, line)) - # Peer removal - if "Peer removed" in line: - result.peer_removals.append((container, line)) - # Parent switches - if "Parent switched" in line: - result.parent_switches.append((container, line)) - # Handshake timeouts - if "timed out" in line and ("handshake" in line.lower() or "Handshake" in line): - result.handshake_timeouts.append((container, line)) - # MMP metrics - if "MMP link metrics" in line: - result.mmp_link_metrics.append((container, line)) - if "MMP session metrics" in line: - result.mmp_session_metrics.append((container, line)) - # Congestion events - if "Congestion detected" in line: - result.congestion_detected.append((container, line)) - if "Kernel recv drops first observed" in line: - result.kernel_drop_events.append((container, line)) - - return result - - def write_sim_metadata( output_dir: str, scenario_name: str, diff --git a/testing/chaos/sim/runner.py b/testing/chaos/sim/runner.py index f1280d1..b6dbd16 100644 --- a/testing/chaos/sim/runner.py +++ b/testing/chaos/sim/runner.py @@ -7,6 +7,7 @@ import logging import os import random import signal +import subprocess import sys import time @@ -119,9 +120,14 @@ class SimRunner: self.compose_file = generate_compose(self.topology, self.scenario, config_dir) log.info("Wrote %s", self.compose_file) - # 4. Build images (reuses Docker cache) - log.info("Building Docker images...") - docker_compose(self.compose_file, ["build"]) + # 4. Build the test image once (avoids per-service build at scale) + log.info("Building Docker image...") + from .compose import FIPS_SIM_IMAGE + docker_dir = os.path.join(os.path.dirname(__file__), "..", "..", "docker") + subprocess.run( + ["docker", "build", "-t", FIPS_SIM_IMAGE, docker_dir], + check=True, + ) # 5. Start containers log.info("Starting %d containers...", len(self.topology.nodes)) diff --git a/testing/ci-local.sh b/testing/ci-local.sh index dd2b0e4..c343e66 100755 --- a/testing/ci-local.sh +++ b/testing/ci-local.sh @@ -15,9 +15,10 @@ # # Integration suites: # static-mesh, static-chain, rekey, -# chaos-smoke-10, chaos-10, ethernet-mesh, ethernet-only, -# bottleneck-parent, cost-avoidance, cost-mixed-7node, -# cost-reeval, cost-stability, depth-vs-cost, mixed-technology, +# chaos-smoke-10, chaos-churn-mixed-10, chaos-ethernet-mesh, +# chaos-ethernet-only, chaos-tcp-mesh, chaos-bottleneck-parent, +# chaos-cost-avoidance, chaos-cost-reeval, chaos-cost-stability, +# chaos-depth-vs-cost, chaos-mixed-technology, chaos-congestion-stress, # sidecar # # Exit codes: @@ -47,11 +48,20 @@ ONLY_SUITE="" # All integration suites matching ci.yml STATIC_SUITES=(static-mesh static-chain) REKEY_SUITES=(rekey) +# Each entry: "display-name scenario [--flag value ...]" CHAOS_SUITES=( - chaos-smoke-10 chaos-10 - ethernet-mesh ethernet-only - bottleneck-parent cost-avoidance cost-mixed-7node - cost-reeval cost-stability depth-vs-cost mixed-technology + "smoke-10 smoke-10" + "churn-mixed-10 churn-mixed --nodes 10 --duration 120" + "ethernet-mesh ethernet-mesh" + "ethernet-only ethernet-only" + "tcp-mesh tcp-mesh" + "bottleneck-parent bottleneck-parent" + "cost-avoidance cost-avoidance" + "cost-reeval cost-reeval" + "cost-stability cost-stability" + "depth-vs-cost depth-vs-cost" + "mixed-technology mixed-technology" + "congestion-stress congestion-stress" ) SIDECAR_SUITES=(sidecar) @@ -83,7 +93,10 @@ list_suites() { for s in "${REKEY_SUITES[@]}"; do echo " $s"; done echo "" echo " Chaos scenarios:" - for s in "${CHAOS_SUITES[@]}"; do echo " $s"; done + for entry in "${CHAOS_SUITES[@]}"; do + read -ra parts <<< "$entry" + echo " chaos-${parts[0]} (${parts[*]:1})" + done echo "" echo " Sidecar:" for s in "${SIDECAR_SUITES[@]}"; do echo " $s"; done @@ -195,9 +208,6 @@ run_static() { info "[$topology] Generating configs" bash testing/static/scripts/generate-configs.sh "$topology" || { record "static-$topology" 1; return; } - info "[$topology] Building Docker images" - docker compose -f "$compose" --profile "$topology" build --quiet || { record "static-$topology" 1; return; } - info "[$topology] Starting containers" docker compose -f "$compose" --profile "$topology" up -d || { record "static-$topology" 1; return; } @@ -223,9 +233,6 @@ run_rekey() { bash testing/static/scripts/generate-configs.sh rekey || { record "rekey" 1; return; } bash testing/static/scripts/rekey-test.sh inject-config || { record "rekey" 1; return; } - info "[rekey] Building Docker images" - docker compose -f "$compose" --profile rekey build --quiet || { record "rekey" 1; return; } - info "[rekey] Starting containers" docker compose -f "$compose" --profile rekey up -d || { record "rekey" 1; return; } @@ -244,17 +251,18 @@ run_rekey() { # Run a chaos scenario run_chaos() { - local scenario="$1" + local name="$1" + shift local rc=0 - info "[chaos/$scenario] Running simulation" - if bash testing/chaos/scripts/chaos.sh "$scenario" 2>&1; then + info "[chaos/$name] Running simulation" + if bash testing/chaos/scripts/chaos.sh "$@" 2>&1; then rc=0 else rc=1 fi - record "chaos-$scenario" $rc + record "chaos-$name" $rc } # Run sidecar test @@ -262,7 +270,7 @@ run_sidecar() { local rc=0 info "[sidecar] Running integration test" - if bash testing/sidecar/scripts/test-sidecar.sh 2>&1; then + if bash testing/sidecar/scripts/test-sidecar.sh --skip-build 2>&1; then rc=0 else rc=1 @@ -275,24 +283,14 @@ run_sidecar() { run_integration() { stage "Stage 3: Integration Tests" - # Install binaries to test directories - info "Installing release binaries to test directories" - install_binaries testing/static - install_binaries testing/chaos - install_binaries testing/sidecar + # Install binaries to shared docker context + info "Installing release binaries" + install_binaries testing/docker - # Build chaos Docker image once (shared by all chaos scenarios) - local need_chaos=false - if [[ -z "$ONLY_SUITE" && "$SKIP_CHAOS" != true ]]; then - need_chaos=true - elif [[ "$ONLY_SUITE" == chaos-* ]]; then - need_chaos=true - fi - - if [[ "$need_chaos" == true ]]; then - info "Building chaos Docker image" - docker build -t fips-chaos:latest testing/chaos --quiet || { record "chaos-build" 1; return; } - fi + # Build unified test image once (used by all harnesses) + info "Building fips-test Docker image" + docker build -t fips-test:latest testing/docker --quiet || { record "docker-build" 1; return; } + docker build -t fips-test-app:latest -f testing/docker/Dockerfile.app testing/docker --quiet || { record "docker-build-app" 1; return; } # Single suite mode if [[ -n "$ONLY_SUITE" ]]; then @@ -300,7 +298,7 @@ run_integration() { return fi - # Static topologies (sequential — they share the docker-compose) + # Static topologies (sequential — profiles share container names) for topo in "${STATIC_SUITES[@]}"; do local topology="${topo#static-}" run_static "$topology" @@ -316,8 +314,11 @@ run_integration() { local suite_names=() local running=0 - for suite in "${CHAOS_SUITES[@]}"; do - local scenario="${suite#chaos-}" + for entry in "${CHAOS_SUITES[@]}"; do + # Parse: "display-name scenario [flags...]" + read -ra parts <<< "$entry" + local name="${parts[0]}" + local args=("${parts[@]:1}") # Throttle: wait for a slot while [[ $running -ge $PARALLEL_JOBS ]]; do @@ -327,12 +328,12 @@ run_integration() { # Run in background, capture output to temp file local logfile - logfile=$(mktemp "/tmp/ci-chaos-${scenario}.XXXXXX") + logfile=$(mktemp "/tmp/ci-chaos-${name}.XXXXXX") ( - run_chaos "$scenario" >"$logfile" 2>&1 + run_chaos "$name" "${args[@]}" >"$logfile" 2>&1 ) & pids+=($!) - suite_names+=("$scenario:$logfile") + suite_names+=("$name:$logfile") running=$((running + 1)) done @@ -369,7 +370,21 @@ run_suite() { rekey) run_rekey ;; chaos-*) - run_chaos "${suite#chaos-}" ;; + local chaos_name="${suite#chaos-}" + local found=false + for entry in "${CHAOS_SUITES[@]}"; do + read -ra parts <<< "$entry" + if [[ "${parts[0]}" == "$chaos_name" ]]; then + run_chaos "$chaos_name" "${parts[@]:1}" + found=true + break + fi + done + if [[ "$found" != true ]]; then + # Fall back to using the name as the scenario directly + run_chaos "$chaos_name" "$chaos_name" + fi + ;; sidecar) run_sidecar ;; *) diff --git a/testing/docker/.gitignore b/testing/docker/.gitignore new file mode 100644 index 0000000..4d658a8 --- /dev/null +++ b/testing/docker/.gitignore @@ -0,0 +1,3 @@ +fips +fipsctl +fipstop diff --git a/testing/docker/Dockerfile b/testing/docker/Dockerfile new file mode 100644 index 0000000..003bc00 --- /dev/null +++ b/testing/docker/Dockerfile @@ -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"] diff --git a/testing/sidecar/Dockerfile.app b/testing/docker/Dockerfile.app similarity index 76% rename from testing/sidecar/Dockerfile.app rename to testing/docker/Dockerfile.app index 9ec0940..3f6ab3d 100644 --- a/testing/sidecar/Dockerfile.app +++ b/testing/docker/Dockerfile.app @@ -1,3 +1,6 @@ +# Sidecar companion container: shares the FIPS container's network namespace. +# Provides basic networking tools for testing mesh connectivity. + FROM debian:trixie-slim RUN apt-get update && \ @@ -7,7 +10,7 @@ RUN apt-get update && \ tcpdump netcat-openbsd curl iperf3 && \ rm -rf /var/lib/apt/lists/* -# Setup SSH server with no authentication (test only!) +# SSH server with no authentication (test only!) RUN mkdir -p /var/run/sshd && \ ssh-keygen -A && \ sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config && \ diff --git a/testing/docker/entrypoint.sh b/testing/docker/entrypoint.sh new file mode 100644 index 0000000..9eb37bb --- /dev/null +++ b/testing/docker/entrypoint.sh @@ -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" </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 diff --git a/testing/sidecar/resolv.conf b/testing/docker/resolv.conf similarity index 100% rename from testing/sidecar/resolv.conf rename to testing/docker/resolv.conf diff --git a/testing/lib/__init__.py b/testing/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/testing/chaos/scripts/derive-keys.py b/testing/lib/derive_keys.py similarity index 100% rename from testing/chaos/scripts/derive-keys.py rename to testing/lib/derive_keys.py diff --git a/testing/lib/log_analysis.py b/testing/lib/log_analysis.py new file mode 100644 index 0000000..e4c73c3 --- /dev/null +++ b/testing/lib/log_analysis.py @@ -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 [ ...] + python3 -m lib.log_analysis --from-docker [ ...] + +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] [ ...]", + 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() diff --git a/testing/lib/wait-converge.sh b/testing/lib/wait-converge.sh new file mode 100644 index 0000000..3f64803 --- /dev/null +++ b/testing/lib/wait-converge.sh @@ -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 [timeout_secs] +# wait_for_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 +} diff --git a/testing/scripts/build.sh b/testing/scripts/build.sh new file mode 100755 index 0000000..c180a97 --- /dev/null +++ b/testing/scripts/build.sh @@ -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 diff --git a/testing/sidecar/Dockerfile b/testing/sidecar/Dockerfile deleted file mode 100644 index 9a131ba..0000000 --- a/testing/sidecar/Dockerfile +++ /dev/null @@ -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"] diff --git a/testing/sidecar/docker-compose.yml b/testing/sidecar/docker-compose.yml index a149491..f9f86f7 100644 --- a/testing/sidecar/docker-compose.yml +++ b/testing/sidecar/docker-compose.yml @@ -8,8 +8,7 @@ networks: services: fips: - build: - context: . + image: fips-test:latest hostname: fips-sidecar cap_add: - NET_ADMIN @@ -20,6 +19,7 @@ services: restart: "no" environment: - RUST_LOG=${RUST_LOG:-info} + - FIPS_TEST_MODE=sidecar - FIPS_NSEC=${FIPS_NSEC} - FIPS_PEER_NPUB=${FIPS_PEER_NPUB:-} - FIPS_PEER_ADDR=${FIPS_PEER_ADDR:-} @@ -27,19 +27,15 @@ services: - FIPS_UDP_BIND=${FIPS_UDP_BIND:-0.0.0.0:2121} - FIPS_TUN_MTU=${FIPS_TUN_MTU:-1280} - FIPS_PEER_TRANSPORT=${FIPS_PEER_TRANSPORT:-udp} - volumes: - - ./resolv.conf:/etc/resolv.conf:ro networks: fips-net: ipv4_address: ${FIPS_IPV4:-172.20.1.20} app: - build: - context: . - dockerfile: Dockerfile.app + image: fips-test-app:latest network_mode: "service:fips" depends_on: - fips volumes: - - ./resolv.conf:/etc/resolv.conf:ro + - ../docker/resolv.conf:/etc/resolv.conf:ro command: ["sleep", "infinity"] diff --git a/testing/sidecar/entrypoint.sh b/testing/sidecar/entrypoint.sh deleted file mode 100755 index bcfd114..0000000 --- a/testing/sidecar/entrypoint.sh +++ /dev/null @@ -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 <&2 - echo "Expected layout: /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" diff --git a/testing/sidecar/scripts/test-sidecar.sh b/testing/sidecar/scripts/test-sidecar.sh index c0d398c..2087c41 100755 --- a/testing/sidecar/scripts/test-sidecar.sh +++ b/testing/sidecar/scripts/test-sidecar.sh @@ -58,8 +58,10 @@ trap cleanup EXIT # ── Build ────────────────────────────────────────────────────────────────── if [[ "${1:-}" != "--skip-build" ]]; then - log "Building sidecar images..." - docker compose $COMPOSE_BASE build + log "Building test images..." + DOCKER_DIR="$(cd "$SIDECAR_DIR/../docker" && pwd)" + docker build -t fips-test:latest "$DOCKER_DIR" + docker build -t fips-test-app:latest -f "$DOCKER_DIR/Dockerfile.app" "$DOCKER_DIR" fi # ── Start nodes ──────────────────────────────────────────────────────────── diff --git a/testing/static/Dockerfile b/testing/static/Dockerfile deleted file mode 100644 index 27ca15a..0000000 --- a/testing/static/Dockerfile +++ /dev/null @@ -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"] diff --git a/testing/static/docker-compose.yml b/testing/static/docker-compose.yml index e673480..eb64dad 100644 --- a/testing/static/docker-compose.yml +++ b/testing/static/docker-compose.yml @@ -6,8 +6,7 @@ networks: - subnet: 172.20.0.0/24 x-fips-common: &fips-common - build: - context: . + image: fips-test:latest cap_add: - NET_ADMIN devices: @@ -28,7 +27,7 @@ services: container_name: fips-node-a hostname: node-a volumes: - - ./resolv.conf:/etc/resolv.conf:ro + - ../docker/resolv.conf:/etc/resolv.conf:ro - ./generated-configs/mesh/node-a.yaml:/etc/fips/fips.yaml:ro networks: fips-net: @@ -40,7 +39,7 @@ services: container_name: fips-node-b hostname: node-b volumes: - - ./resolv.conf:/etc/resolv.conf:ro + - ../docker/resolv.conf:/etc/resolv.conf:ro - ./generated-configs/mesh/node-b.yaml:/etc/fips/fips.yaml:ro networks: fips-net: @@ -52,7 +51,7 @@ services: container_name: fips-node-c hostname: node-c volumes: - - ./resolv.conf:/etc/resolv.conf:ro + - ../docker/resolv.conf:/etc/resolv.conf:ro - ./generated-configs/mesh/node-c.yaml:/etc/fips/fips.yaml:ro networks: fips-net: @@ -64,7 +63,7 @@ services: container_name: fips-node-d hostname: node-d volumes: - - ./resolv.conf:/etc/resolv.conf:ro + - ../docker/resolv.conf:/etc/resolv.conf:ro - ./generated-configs/mesh/node-d.yaml:/etc/fips/fips.yaml:ro networks: fips-net: @@ -76,7 +75,7 @@ services: container_name: fips-node-e hostname: node-e volumes: - - ./resolv.conf:/etc/resolv.conf:ro + - ../docker/resolv.conf:/etc/resolv.conf:ro - ./generated-configs/mesh/node-e.yaml:/etc/fips/fips.yaml:ro networks: fips-net: @@ -89,7 +88,7 @@ services: container_name: fips-node-a hostname: node-a volumes: - - ./resolv.conf:/etc/resolv.conf:ro + - ../docker/resolv.conf:/etc/resolv.conf:ro - ./generated-configs/mesh-public/node-a.yaml:/etc/fips/fips.yaml:ro networks: fips-net: @@ -101,7 +100,7 @@ services: container_name: fips-node-b hostname: node-b volumes: - - ./resolv.conf:/etc/resolv.conf:ro + - ../docker/resolv.conf:/etc/resolv.conf:ro - ./generated-configs/mesh-public/node-b.yaml:/etc/fips/fips.yaml:ro networks: fips-net: @@ -113,7 +112,7 @@ services: container_name: fips-node-c hostname: node-c volumes: - - ./resolv.conf:/etc/resolv.conf:ro + - ../docker/resolv.conf:/etc/resolv.conf:ro - ./generated-configs/mesh-public/node-c.yaml:/etc/fips/fips.yaml:ro networks: fips-net: @@ -125,7 +124,7 @@ services: container_name: fips-node-d hostname: node-d volumes: - - ./resolv.conf:/etc/resolv.conf:ro + - ../docker/resolv.conf:/etc/resolv.conf:ro - ./generated-configs/mesh-public/node-d.yaml:/etc/fips/fips.yaml:ro networks: fips-net: @@ -137,7 +136,7 @@ services: container_name: fips-node-e hostname: node-e volumes: - - ./resolv.conf:/etc/resolv.conf:ro + - ../docker/resolv.conf:/etc/resolv.conf:ro - ./generated-configs/mesh-public/node-e.yaml:/etc/fips/fips.yaml:ro networks: fips-net: @@ -150,7 +149,7 @@ services: container_name: fips-node-a hostname: node-a volumes: - - ./resolv.conf:/etc/resolv.conf:ro + - ../docker/resolv.conf:/etc/resolv.conf:ro - ./generated-configs/chain/node-a.yaml:/etc/fips/fips.yaml:ro networks: fips-net: @@ -162,7 +161,7 @@ services: container_name: fips-node-b hostname: node-b volumes: - - ./resolv.conf:/etc/resolv.conf:ro + - ../docker/resolv.conf:/etc/resolv.conf:ro - ./generated-configs/chain/node-b.yaml:/etc/fips/fips.yaml:ro networks: fips-net: @@ -174,7 +173,7 @@ services: container_name: fips-node-c hostname: node-c volumes: - - ./resolv.conf:/etc/resolv.conf:ro + - ../docker/resolv.conf:/etc/resolv.conf:ro - ./generated-configs/chain/node-c.yaml:/etc/fips/fips.yaml:ro networks: fips-net: @@ -186,7 +185,7 @@ services: container_name: fips-node-d hostname: node-d volumes: - - ./resolv.conf:/etc/resolv.conf:ro + - ../docker/resolv.conf:/etc/resolv.conf:ro - ./generated-configs/chain/node-d.yaml:/etc/fips/fips.yaml:ro networks: fips-net: @@ -198,7 +197,7 @@ services: container_name: fips-node-e hostname: node-e volumes: - - ./resolv.conf:/etc/resolv.conf:ro + - ../docker/resolv.conf:/etc/resolv.conf:ro - ./generated-configs/chain/node-e.yaml:/etc/fips/fips.yaml:ro networks: fips-net: @@ -211,7 +210,7 @@ services: container_name: fips-node-a hostname: node-a volumes: - - ./resolv.conf:/etc/resolv.conf:ro + - ../docker/resolv.conf:/etc/resolv.conf:ro - ./generated-configs/rekey/node-a.yaml:/etc/fips/fips.yaml:ro networks: fips-net: @@ -223,7 +222,7 @@ services: container_name: fips-node-b hostname: node-b volumes: - - ./resolv.conf:/etc/resolv.conf:ro + - ../docker/resolv.conf:/etc/resolv.conf:ro - ./generated-configs/rekey/node-b.yaml:/etc/fips/fips.yaml:ro networks: fips-net: @@ -235,7 +234,7 @@ services: container_name: fips-node-c hostname: node-c volumes: - - ./resolv.conf:/etc/resolv.conf:ro + - ../docker/resolv.conf:/etc/resolv.conf:ro - ./generated-configs/rekey/node-c.yaml:/etc/fips/fips.yaml:ro networks: fips-net: @@ -247,7 +246,7 @@ services: container_name: fips-node-d hostname: node-d volumes: - - ./resolv.conf:/etc/resolv.conf:ro + - ../docker/resolv.conf:/etc/resolv.conf:ro - ./generated-configs/rekey/node-d.yaml:/etc/fips/fips.yaml:ro networks: fips-net: @@ -259,7 +258,7 @@ services: container_name: fips-node-e hostname: node-e volumes: - - ./resolv.conf:/etc/resolv.conf:ro + - ../docker/resolv.conf:/etc/resolv.conf:ro - ./generated-configs/rekey/node-e.yaml:/etc/fips/fips.yaml:ro networks: fips-net: @@ -272,7 +271,7 @@ services: container_name: fips-node-a hostname: node-a volumes: - - ./resolv.conf:/etc/resolv.conf:ro + - ../docker/resolv.conf:/etc/resolv.conf:ro - ./generated-configs/tcp-chain/node-a.yaml:/etc/fips/fips.yaml:ro networks: fips-net: @@ -284,7 +283,7 @@ services: container_name: fips-node-b hostname: node-b volumes: - - ./resolv.conf:/etc/resolv.conf:ro + - ../docker/resolv.conf:/etc/resolv.conf:ro - ./generated-configs/tcp-chain/node-b.yaml:/etc/fips/fips.yaml:ro networks: fips-net: @@ -296,7 +295,7 @@ services: container_name: fips-node-c hostname: node-c volumes: - - ./resolv.conf:/etc/resolv.conf:ro + - ../docker/resolv.conf:/etc/resolv.conf:ro - ./generated-configs/tcp-chain/node-c.yaml:/etc/fips/fips.yaml:ro networks: fips-net: diff --git a/testing/static/resolv.conf b/testing/static/resolv.conf deleted file mode 100644 index bbc8559..0000000 --- a/testing/static/resolv.conf +++ /dev/null @@ -1 +0,0 @@ -nameserver 127.0.0.1 diff --git a/testing/static/scripts/build.sh b/testing/static/scripts/build.sh deleted file mode 100755 index 70aa296..0000000 --- a/testing/static/scripts/build.sh +++ /dev/null @@ -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: /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" diff --git a/testing/static/scripts/derive-keys.py b/testing/static/scripts/derive-keys.py deleted file mode 100755 index 37495e5..0000000 --- a/testing/static/scripts/derive-keys.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env python3 -"""Derive deterministic nostr nsec/npub from mesh-name and node-name. - -Usage: derive-keys.py -Output: nsec=\nnpub= - -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]} ", file=sys.stderr) - sys.exit(1) - nsec, npub = derive(sys.argv[1], sys.argv[2]) - print(f"nsec={nsec}") - print(f"npub={npub}") diff --git a/testing/static/scripts/generate-configs.sh b/testing/static/scripts/generate-configs.sh index 038c12c..94a0537 100755 --- a/testing/static/scripts/generate-configs.sh +++ b/testing/static/scripts/generate-configs.sh @@ -12,7 +12,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CONFIG_DIR="$SCRIPT_DIR/../configs" GENERATED_DIR="$SCRIPT_DIR/../generated-configs" TEMPLATE_FILE="$CONFIG_DIR/node.template.yaml" -DERIVE_KEYS="$SCRIPT_DIR/derive-keys.py" +DERIVE_KEYS="$SCRIPT_DIR/../../lib/derive_keys.py" # Parse topology YAML to extract node attributes # Usage: get_node_attr diff --git a/testing/static/scripts/ping-test.sh b/testing/static/scripts/ping-test.sh index 6cb75eb..2140a6e 100755 --- a/testing/static/scripts/ping-test.sh +++ b/testing/static/scripts/ping-test.sh @@ -18,6 +18,7 @@ FAILED=0 # Node identities (from generated env file) SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/../../lib/wait-converge.sh" ENV_FILE="$SCRIPT_DIR/../generated-configs/npubs.env" if [ ! -f "$ENV_FILE" ]; then echo "Error: $ENV_FILE not found. Run generate-configs.sh first." >&2 @@ -51,9 +52,15 @@ ping_test() { echo "=== FIPS Ping Test ($PROFILE topology) ===" echo "" -# Wait for nodes to converge -echo "Waiting 5s for mesh convergence..." -sleep 5 +# Wait for nodes to converge — peers + discovery propagation +echo "Waiting for mesh convergence..." +if [ "$PROFILE" = "chain" ]; then + wait_for_peers fips-node-b 2 15 || true +else + wait_for_peers fips-node-a 2 15 || true +fi +# Allow extra time for discovery to propagate across the mesh +sleep 3 if [ "$PROFILE" = "mesh" ] || [ "$PROFILE" = "mesh-public" ]; then # Sparse mesh topology: A-B, B-C, C-D, D-E, E-A, A-D diff --git a/testing/tor/common/Dockerfile b/testing/tor/common/Dockerfile deleted file mode 100644 index 8eec16f..0000000 --- a/testing/tor/common/Dockerfile +++ /dev/null @@ -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"] diff --git a/testing/tor/common/resolv.conf b/testing/tor/common/resolv.conf deleted file mode 100644 index bbc8559..0000000 --- a/testing/tor/common/resolv.conf +++ /dev/null @@ -1 +0,0 @@ -nameserver 127.0.0.1 diff --git a/testing/tor/directory-mode/Dockerfile.colocated b/testing/tor/directory-mode/Dockerfile.colocated deleted file mode 100644 index 983b23d..0000000 --- a/testing/tor/directory-mode/Dockerfile.colocated +++ /dev/null @@ -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"] diff --git a/testing/tor/directory-mode/docker-compose.yml b/testing/tor/directory-mode/docker-compose.yml index a33efa2..d6e37a2 100644 --- a/testing/tor/directory-mode/docker-compose.yml +++ b/testing/tor/directory-mode/docker-compose.yml @@ -5,8 +5,8 @@ # [fips-b] — Tor + FIPS co-located, socks5-only, connects to fips-a's .onion # # Both nodes run Tor and FIPS in the same container. Node A's Tor manages -# the onion service via HiddenServiceDir with Sandbox 1. Node B connects -# outbound through its local Tor's SOCKS5 proxy. +# the onion service via HiddenServiceDir. Node B connects outbound through +# its local Tor's SOCKS5 proxy. networks: dir-test: @@ -14,9 +14,7 @@ networks: services: fips-a: - build: - context: . - dockerfile: Dockerfile.colocated + image: fips-test:latest cap_add: - NET_ADMIN devices: @@ -28,17 +26,15 @@ services: hostname: fips-dir-a volumes: - ./configs/node-a.yaml:/etc/fips/fips.yaml:ro + - ./torrc:/etc/tor/torrc:ro environment: - RUST_LOG=info,fips::transport::tor=debug + - FIPS_TEST_MODE=tor-directory networks: dir-test: fips-b: - build: - context: . - dockerfile: Dockerfile.colocated - args: - TORRC: torrc.socks5 + image: fips-test:latest cap_add: - NET_ADMIN devices: @@ -50,7 +46,9 @@ services: hostname: fips-dir-b volumes: - ./configs/node-b.yaml:/etc/fips/fips.yaml:ro + - ./torrc.socks5:/etc/tor/torrc:ro environment: - RUST_LOG=info,fips::transport::tor=debug + - FIPS_TEST_MODE=tor-directory networks: dir-test: diff --git a/testing/tor/directory-mode/entrypoint.sh b/testing/tor/directory-mode/entrypoint.sh deleted file mode 100755 index e30c8f5..0000000 --- a/testing/tor/directory-mode/entrypoint.sh +++ /dev/null @@ -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 diff --git a/testing/tor/directory-mode/resolv.conf b/testing/tor/directory-mode/resolv.conf deleted file mode 100644 index bbc8559..0000000 --- a/testing/tor/directory-mode/resolv.conf +++ /dev/null @@ -1 +0,0 @@ -nameserver 127.0.0.1 diff --git a/testing/tor/directory-mode/scripts/directory-test.sh b/testing/tor/directory-mode/scripts/directory-test.sh index 9c9fbf6..9246ec7 100755 --- a/testing/tor/directory-mode/scripts/directory-test.sh +++ b/testing/tor/directory-mode/scripts/directory-test.sh @@ -19,7 +19,7 @@ trap 'echo ""; echo "Test interrupted — cleaning up..."; docker compose down 2 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TEST_DIR="$SCRIPT_DIR/.." -DERIVE_KEYS="$SCRIPT_DIR/../../../static/scripts/derive-keys.py" +DERIVE_KEYS="$SCRIPT_DIR/../../../lib/derive_keys.py" cd "$TEST_DIR" PASSED=0 @@ -84,7 +84,7 @@ echo "" # ── Phase 1: Start node A (Tor + FIPS co-located) ──────────────── echo "Phase 1: Starting node A (Tor+FIPS, directory-mode onion service)..." docker compose down 2>/dev/null || true -docker compose up -d --build fips-a +docker compose up -d fips-a echo "" # ── Phase 2: Wait for onion service creation ───────────────────── diff --git a/testing/tor/socks5-outbound/docker-compose.yml b/testing/tor/socks5-outbound/docker-compose.yml index d827e1d..7e6bc5c 100644 --- a/testing/tor/socks5-outbound/docker-compose.yml +++ b/testing/tor/socks5-outbound/docker-compose.yml @@ -22,8 +22,7 @@ services: tor-test: fips-a: - build: - context: ../common + image: fips-test:latest cap_add: - NET_ADMIN devices: @@ -36,16 +35,15 @@ services: depends_on: - tor-daemon volumes: - - ../common/resolv.conf:/etc/resolv.conf:ro - ./configs/node-a.yaml:/etc/fips/fips.yaml:ro environment: - RUST_LOG=info,fips::transport::tor=debug + - FIPS_TEST_MODE=tor-socks5 networks: tor-test: fips-b: - build: - context: ../common + image: fips-test:latest cap_add: - NET_ADMIN devices: @@ -58,9 +56,9 @@ services: depends_on: - tor-daemon volumes: - - ../common/resolv.conf:/etc/resolv.conf:ro - ./configs/node-b.yaml:/etc/fips/fips.yaml:ro environment: - RUST_LOG=info,fips::transport::tor=debug + - FIPS_TEST_MODE=tor-socks5 networks: tor-test: diff --git a/testing/tor/socks5-outbound/scripts/tor-test.sh b/testing/tor/socks5-outbound/scripts/tor-test.sh index 6b42852..68a21b5 100755 --- a/testing/tor/socks5-outbound/scripts/tor-test.sh +++ b/testing/tor/socks5-outbound/scripts/tor-test.sh @@ -28,7 +28,7 @@ trap 'echo ""; echo "Test interrupted — cleaning up..."; docker compose down 2 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TOR_DIR="$SCRIPT_DIR/.." -DERIVE_KEYS="$SCRIPT_DIR/../../../static/scripts/derive-keys.py" +DERIVE_KEYS="$SCRIPT_DIR/../../../lib/derive_keys.py" cd "$TOR_DIR" PASSED=0 @@ -80,7 +80,7 @@ echo "" # ── Phase 1: Build and start ───────────────────────────────────── echo "Phase 1: Starting Tor daemon and FIPS nodes..." docker compose down 2>/dev/null || true -docker compose up -d --build +docker compose up -d echo "" # ── Phase 2: Wait for Tor bootstrap ─────────────────────────────