#!/bin/bash # Mixed-version interop harness: generate the per-node configs for a # node-spec. # # A NODE-SPEC is a multiset of image slots (`a`, `b`, `c`), of size >= 2. # Each spec entry is one node; the same slot may appear more than once. # The slots resolve to the three Docker images built by build-images.sh: # # a -> fips-interop:a ("version under test") # b -> fips-interop:b ("parent / comparison") # c -> fips-interop:c ("release baseline") # # Node identity vs image slot are SEPARATE: # - A spec entry is a slot letter. # - Node id = , ordinal counting occurrences of that # slot, 1-based. Spec `a a b c` -> node ids `a1 a2 b1 c1`. # - Container name = fips-interop-. # - IPv4 = 172.30.0.1, index = 0-based position in the spec # (10, 11, 12, 13, ...). # # Topology is a FULL MESH: every node auto-connects to every other node. # Node identities are derived deterministically via the shared # derive_keys.py helper, keyed by a unique per-node string so that two # nodes of the same slot (a1, a2) still get DISTINCT identities. # # Output goes to /generated-configs/ (see FIPS_INTEROP_RUNS_DIR # under Environment, below): # .yaml per-node daemon config (one per node) # docker-compose.generated.yml generated compose, one service per node # nodes.env manifest for the driver to source # npubs.env NPUB_ map (per-node npubs) # # Generation is deterministic and idempotent: same node-spec in -> same # files out. # # Usage: # ./generate-configs.sh [node-spec...] # # node-spec space-separated slot letters, default `a b c`. # # Environment: # REKEY_AFTER_SECS FSP/FMP rekey interval (default 35, matching # the static rekey suite). # FIPS_INTEROP_RUNS_DIR Root for harness scratch dirs (.build/, # .stress-runs/, generated-configs/). When # unset, falls back to in-tree paths under # testing/interop/ and prints a warning to # stderr; set it to a path outside the source # tree to keep generated artefacts out of the # checkout. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" DERIVE_KEYS="$SCRIPT_DIR/../lib/derive_keys.py" # ── Scratch-dir root ───────────────────────────────────────────────── # # FIPS_INTEROP_RUNS_DIR controls where the harness writes its scratch # directories (.build/, .stress-runs/, generated-configs/). When unset # we fall back to in-tree paths under testing/interop/ and warn the # operator, so the warning fires exactly once per invocation. When a # parent script has already warned it exports _FIPS_INTEROP_WARNED=1 # to suppress duplicate warnings in child scripts. if [[ -n "${FIPS_INTEROP_RUNS_DIR:-}" ]]; then RUNS_BASE="$FIPS_INTEROP_RUNS_DIR" mkdir -p "$RUNS_BASE" else RUNS_BASE="$SCRIPT_DIR" if [[ -z "${_FIPS_INTEROP_WARNED:-}" ]]; then echo >&2 "WARNING: FIPS_INTEROP_RUNS_DIR not set; harness output will be written under the source tree at $RUNS_BASE. Set FIPS_INTEROP_RUNS_DIR to a path outside the source tree to avoid this." export _FIPS_INTEROP_WARNED=1 fi fi OUT_DIR="$RUNS_BASE/generated-configs" REKEY_AFTER_SECS="${REKEY_AFTER_SECS:-35}" MESH_NAME="fips-interop-mesh" UDP_PORT=2121 SUBNET_PREFIX="172.30.0.1" # node N gets ${SUBNET_PREFIX} # ── Args: the node-spec ────────────────────────────────────────────── SPEC=("$@") if [ "${#SPEC[@]}" -eq 0 ]; then SPEC=(a b c) fi if [ "${#SPEC[@]}" -lt 2 ]; then echo "ERROR: node-spec must have at least 2 nodes (got ${#SPEC[@]})" >&2 echo "Usage: $0 [node-spec...] e.g. $0 a a b c" >&2 exit 1 fi for slot in "${SPEC[@]}"; do case "$slot" in a|b|c) ;; *) echo "ERROR: invalid slot '$slot' — must be one of a, b, c" >&2 exit 1 ;; esac done # ── Derive node ids, slots, containers, IPs from the spec ──────────── # # NODE_IDS preserves spec order. NODE_SLOT/NODE_IP/NODE_CTR are keyed by # node id. NODE_IDS=() declare -A NODE_SLOT NODE_IP NODE_CTR declare -A SLOT_COUNT=([a]=0 [b]=0 [c]=0) index=0 for slot in "${SPEC[@]}"; do SLOT_COUNT[$slot]=$(( SLOT_COUNT[$slot] + 1 )) nid="${slot}${SLOT_COUNT[$slot]}" NODE_IDS+=("$nid") NODE_SLOT[$nid]="$slot" NODE_CTR[$nid]="fips-interop-$nid" NODE_IP[$nid]="${SUBNET_PREFIX}${index}" index=$(( index + 1 )) done mkdir -p "$OUT_DIR" # Clear stale per-node configs from a previous (differently-shaped) spec # so generation is idempotent — a smaller spec must not leave orphans. rm -f "$OUT_DIR"/*.yaml # ── Phase 1: derive identities ─────────────────────────────────────── # # Key the derivation by node id (a1, a2, ...) so two same-slot nodes get # distinct identities. declare -A NSEC NPUB for nid in "${NODE_IDS[@]}"; do keys="$(python3 "$DERIVE_KEYS" "$MESH_NAME" "$nid")" NSEC[$nid]="$(echo "$keys" | sed -n 's/^nsec=//p')" NPUB[$nid]="$(echo "$keys" | sed -n 's/^npub=//p')" done # ── Phase 2: emit per-node configs ─────────────────────────────────── emit_peer_block() { # Args: peer node-id local pid="$1" cat < "$cfg" echo " generated $cfg" done # ── Phase 3: emit the generated docker-compose file ────────────────── # # One service per node, image pinned to the node's slot. This supersedes # the static docker-compose.yml. COMPOSE_FILE="$OUT_DIR/docker-compose.generated.yml" { echo "# Mixed-version interop harness: generated compose, one service per node." echo "#" echo "# Generated by testing/interop/generate-configs.sh — DO NOT EDIT." echo "# Node-spec: ${SPEC[*]}" echo "#" echo "# Each service is pinned to its node's image slot, so two nodes of the" echo "# same slot (e.g. a1, a2) run the SAME daemon build. Netem is applied" echo "# by the driver via 'docker exec ... tc qdisc' on each container eth0." echo "" echo "networks:" echo " fips-interop-net:" echo " driver: bridge" echo " ipam:" echo " config:" echo " - subnet: 172.30.0.0/24" echo "" echo "x-fips-interop-common: &fips-interop-common" echo " cap_add:" echo " - NET_ADMIN" echo " devices:" echo " - /dev/net/tun:/dev/net/tun" echo " sysctls:" echo " - net.ipv6.conf.all.disable_ipv6=0" echo " restart: \"no\"" echo " environment:" echo " - RUST_LOG=info,fips::node::handlers::rekey=debug,fips::node::handlers::handshake=debug" echo "" echo "services:" for nid in "${NODE_IDS[@]}"; do slot="${NODE_SLOT[$nid]}" echo " $nid:" echo " <<: *fips-interop-common" echo " image: fips-interop:$slot" echo " container_name: ${NODE_CTR[$nid]}" echo " hostname: $nid" echo " volumes:" echo " - $REPO_ROOT/testing/docker/resolv.conf:/etc/resolv.conf:ro" echo " - ./$nid.yaml:/etc/fips/fips.yaml:ro" echo " networks:" echo " fips-interop-net:" echo " ipv4_address: ${NODE_IP[$nid]}" done } > "$COMPOSE_FILE" echo " generated $COMPOSE_FILE" # ── Phase 4: nodes.env manifest ────────────────────────────────────── # # The driver sources this. NODE_IDS is the canonical ordered node list; # the per-node maps are emitted as space-separated key:value pairs so the # driver can rebuild associative arrays without re-deriving anything. MANIFEST="$OUT_DIR/nodes.env" { echo "# Generated by testing/interop/generate-configs.sh" echo "# Mesh name: $MESH_NAME" echo "# Node-spec: ${SPEC[*]}" echo "INTEROP_SPEC=\"${SPEC[*]}\"" echo "INTEROP_NODE_IDS=\"${NODE_IDS[*]}\"" # Per-node maps, one var each, space-separated 'nodeid:value' tokens. { printf 'INTEROP_NODE_SLOTS="' for nid in "${NODE_IDS[@]}"; do printf '%s:%s ' "$nid" "${NODE_SLOT[$nid]}"; done printf '"\n' } { printf 'INTEROP_NODE_CONTAINERS="' for nid in "${NODE_IDS[@]}"; do printf '%s:%s ' "$nid" "${NODE_CTR[$nid]}"; done printf '"\n' } { printf 'INTEROP_NODE_IPS="' for nid in "${NODE_IDS[@]}"; do printf '%s:%s ' "$nid" "${NODE_IP[$nid]}"; done printf '"\n' } { printf 'INTEROP_NODE_NPUBS="' for nid in "${NODE_IDS[@]}"; do printf '%s:%s ' "$nid" "${NPUB[$nid]}"; done printf '"\n' } } > "$MANIFEST" echo " generated $MANIFEST" # ── Phase 5: npubs.env — per-node npub map ─────────────────────────── ENV_FILE="$OUT_DIR/npubs.env" { echo "# Generated by testing/interop/generate-configs.sh" echo "# Mesh name: $MESH_NAME" for nid in "${NODE_IDS[@]}"; do upper="$(echo "$nid" | tr '[:lower:]' '[:upper:]')" echo "NPUB_${upper}=${NPUB[$nid]}" done } > "$ENV_FILE" echo " generated $ENV_FILE" echo "" echo "Mesh configs ready (spec='${SPEC[*]}', rekey.after_secs=$REKEY_AFTER_SECS):" for nid in "${NODE_IDS[@]}"; do echo " $nid slot ${NODE_SLOT[$nid]} ${NODE_IP[$nid]} ${NPUB[$nid]}" done