From a50473fe9fa6f829194d414a5ff30d385a35246c Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Wed, 18 Feb 2026 13:20:03 +0000 Subject: [PATCH] Improve Docker mesh setup: data-driven topologies, identity derivation, services Config system overhaul: - Add mesh-public topology (5 Docker nodes + external pub node at 217.77.8.91) - Refactor generate-configs.sh to read topology YAML files directly, replacing hardcoded lookup functions, with multi-char node ID support - Generate npubs.env with all node npubs; sourced by test scripts and injected into containers via docker-compose env_file directive - Add .env with COMPOSE_PROFILES=mesh so docker compose defaults to mesh topology without requiring --profile Deterministic mesh identity derivation: - Add derive-keys.py: pure Python tool (no deps) deriving nsec/npub from sha256(mesh-name|node-id) via secp256k1 and BIP-173 bech32 - generate-configs.sh and build.sh accept optional mesh-name argument; Docker node identities are derived while external nodes keep hardcoded keys from topology YAML Docker compose improvements: - Add mesh-public profile service definitions - build.sh now runs docker compose build automatically, providing a single command for the full binary+configs+images pipeline - Ping test script supports mesh-public topology Container services: - Add HTTP server on port 8000 (IPv6-bound) serving static page, accessible over FIPS overlay via npub.fips hostnames - Add rsync to container packages Documentation: - Comprehensive README update covering topology system, identity derivation, npubs.env, and container background services (SSH, iperf3, HTTP) with usage examples --- examples/docker-network/.env | 2 + examples/docker-network/Dockerfile | 9 +- examples/docker-network/README.md | 293 ++++++++++++------ .../configs/topologies/mesh-public.yaml | 52 ++++ examples/docker-network/docker-compose.yml | 63 ++++ examples/docker-network/scripts/build.sh | 18 +- .../docker-network/scripts/derive-keys.py | 111 +++++++ .../scripts/generate-configs.sh | 241 ++++++++------ examples/docker-network/scripts/iperf-test.sh | 22 +- examples/docker-network/scripts/ping-test.sh | 17 +- 10 files changed, 620 insertions(+), 208 deletions(-) create mode 100644 examples/docker-network/.env create mode 100644 examples/docker-network/configs/topologies/mesh-public.yaml create mode 100755 examples/docker-network/scripts/derive-keys.py diff --git a/examples/docker-network/.env b/examples/docker-network/.env new file mode 100644 index 0000000..11cfc7d --- /dev/null +++ b/examples/docker-network/.env @@ -0,0 +1,2 @@ +# Default docker compose profile (mesh topology) +COMPOSE_PROFILES=mesh diff --git a/examples/docker-network/Dockerfile b/examples/docker-network/Dockerfile index 96c0986..7be38c6 100644 --- a/examples/docker-network/Dockerfile +++ b/examples/docker-network/Dockerfile @@ -3,7 +3,7 @@ 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 && \ + dnsmasq curl python3 rsync && \ rm -rf /var/lib/apt/lists/* # Setup SSH server with no authentication (test only!) @@ -27,5 +27,8 @@ RUN printf '%s\n' \ COPY fips /usr/local/bin/ RUN chmod +x /usr/local/bin/fips -# Start dnsmasq, SSH server, and iperf3 in background, then run FIPS -ENTRYPOINT ["/bin/bash", "-c", "dnsmasq && /usr/sbin/sshd && iperf3 -s -D && exec fips --config /etc/fips/fips.yaml"] +# 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/examples/docker-network/README.md b/examples/docker-network/README.md index 03246e7..b4945c9 100644 --- a/examples/docker-network/README.md +++ b/examples/docker-network/README.md @@ -1,45 +1,46 @@ # Docker Network Test Harness -Multi-node integration test for FIPS using Docker containers. Two topologies -are provided: a sparse mesh (5 nodes, 6 links) and a linear chain (5 nodes, -4 links). Both exercise the full FIPS stack including TUN devices, DNS -resolution, peer link encryption, spanning tree construction, and -discovery-driven multi-hop routing. +Multi-node integration test for FIPS using Docker containers. Multiple +topologies are provided: a sparse mesh (5 nodes, 6 links), a linear chain +(5 nodes, 4 links), and a mesh with a public external node. All exercise the +full FIPS stack including TUN devices, DNS resolution, peer link encryption, +spanning tree construction, and discovery-driven multi-hop routing. ## Prerequisites - Docker with the compose plugin - Rust toolchain (for building the FIPS binary) +- Python 3 (for identity derivation; stdlib only, no packages required) ## Quick Start -Build the binary and copy it to the docker context: +Build the binary and generate configs: ```bash ./scripts/build.sh ``` -### Mesh Topology +Start the mesh (default topology): ```bash -docker compose --profile mesh build -docker compose --profile mesh up -d -./scripts/ping-test.sh mesh # 20/20 expected (with response times) +docker compose up -d +./scripts/ping-test.sh mesh # 20/20 expected ./scripts/iperf-test.sh mesh # bandwidth test -docker compose --profile mesh down +docker compose down ``` -### Chain Topology +The mesh profile is activated by default via `.env`. To use a different +topology, specify the profile explicitly: ```bash -docker compose --profile chain build docker compose --profile chain up -d -./scripts/ping-test.sh chain # 6/6 expected (with response times) -./scripts/iperf-test.sh chain # bandwidth test +./scripts/ping-test.sh chain docker compose --profile chain down ``` -## Mesh Topology +## Topologies + +### Mesh ![Mesh Topology](docker-mesh-topology.svg) @@ -63,7 +64,7 @@ covering both direct-peer and multi-hop paths. | D — E | non-tree link | | C — E | non-tree link | -## Chain Topology +### Chain ![Chain Topology](docker-chain-topology.svg) @@ -77,22 +78,172 @@ The ping test covers: - Multi-hop: A→C (2 hops), A→D (3 hops), A→E (4 hops) - Reverse: E→A (4 hops) -## Performance Testing +### Mesh-Public -The `iperf-test.sh` script measures bandwidth between nodes using iperf3: +Same five Docker nodes as the mesh topology, plus an external public node +(`pub`) at a remote IP. Nodes A, B, and C peer with the public node. This +topology is for testing mixed local/remote mesh operation. + +External nodes are not managed by Docker — only their identity and address +appear in the topology file so that Docker nodes can peer with them. + +## Configuration Management + +### File Structure + +```text +configs/ +├── node.template.yaml # Template for all node configs +└── topologies/ + ├── mesh.yaml # Mesh topology definition + ├── chain.yaml # Chain topology definition + └── mesh-public.yaml # Mesh + external public node + +generated-configs/ # Auto-generated (gitignored) +├── npubs.env # NPUB_A=..., NPUB_B=..., etc. +├── mesh/ +│ ├── node-a.yaml ... node-e.yaml +├── mesh-public/ +│ ├── node-a.yaml ... node-e.yaml +└── chain/ + ├── node-a.yaml ... node-e.yaml + +scripts/ +├── build.sh # Build binary + generate configs +├── generate-configs.sh # Generate node configs from topology +├── derive-keys.py # Deterministic nsec/npub derivation +├── ping-test.sh # Connectivity test +├── iperf-test.sh # Bandwidth test +└── netem.sh # Network impairment +``` + +### Topology Files + +Each topology file in `configs/topologies/` defines: + +- **Node identities**: nsec (hex) and npub (bech32) for each node +- **Addresses**: `docker_ip` for Docker-managed nodes, `external_ip` for + remote nodes not managed by Docker +- **Peer connections**: which nodes peer with each other + +Example entry: + +```yaml +nodes: + a: + nsec: "0102030405060708..." + npub: "npub1sjlh2c3..." + docker_ip: "172.20.0.10" + peers: [d, e] +``` + +External nodes use `external_ip` instead of `docker_ip`. Config generation +skips external nodes (they run outside Docker) but includes their identity +in peer blocks and the npubs environment file. + +### Generating Configs + +```bash +./scripts/generate-configs.sh [mesh-name] +``` + +This reads the topology definition and generates: + +1. Per-node YAML config files in `generated-configs//` +2. `generated-configs/npubs.env` with all node npubs as environment variables + +The `npubs.env` file is sourced by the test scripts and injected into +Docker containers via `env_file` in `docker-compose.yml`. + +The build script (`scripts/build.sh`) calls `generate-configs.sh` +automatically after compiling. + +### Adding a New Topology + +1. Create `configs/topologies/.yaml` following the format of + `mesh.yaml` +2. Add corresponding service definitions to `docker-compose.yml` with + `profiles: [""]` +3. Run `./scripts/generate-configs.sh ` to generate configs + +## Deterministic Mesh Identity Derivation + +When running multiple test meshes that may peer with the same external node, +each mesh needs unique node identities to avoid key conflicts. The optional +`mesh-name` parameter generates deterministic per-mesh identities: + +```bash +# Build with derived identities +./scripts/build.sh mesh my-mesh-1 + +# Or generate configs directly +./scripts/generate-configs.sh mesh my-mesh-1 +./scripts/generate-configs.sh mesh-public my-mesh-1 +``` + +### How It Works + +For each Docker node (those with `docker_ip`), the identity is derived as: + +```text +nsec = sha256(mesh_name + "|" + node_id) # e.g., sha256("my-mesh-1|a") +npub = bech32("npub", secp256k1_pubkey(nsec)) +``` + +External nodes (those with `external_ip`) always keep their hardcoded +identity from the topology YAML, since they represent real nodes outside +the test environment. + +Without a mesh name, the identities from the topology YAML are used as-is +(the original behavior). + +### The derive-keys.py Script + +The derivation is performed by `scripts/derive-keys.py`, a standalone tool +with no external dependencies (pure Python stdlib: hashlib for SHA-256, +manual secp256k1 scalar multiplication, and BIP-173 bech32 encoding): + +```bash +$ ./scripts/derive-keys.py my-mesh-1 a +nsec=<64-char-hex> +npub=npub1... +``` + +### The npubs.env File + +Every run of `generate-configs.sh` writes `generated-configs/npubs.env` +containing all node npubs, whether derived or from the topology YAML: + +```text +NPUB_A=npub1... +NPUB_B=npub1... +NPUB_C=npub1... +NPUB_D=npub1... +NPUB_E=npub1... +NPUB_PUB=npub1... # only present for topologies with a pub node +``` + +This file is: + +- **Sourced by test scripts** (`ping-test.sh`, `iperf-test.sh`) to resolve + node identities for DNS lookups +- **Injected into containers** via the `env_file` directive in + `docker-compose.yml`, making `$NPUB_A` etc. available as environment + variables inside each container + +## Performance Testing ```bash ./scripts/iperf-test.sh [mesh|chain] +./scripts/iperf-test.sh mesh --live # show live iperf3 output ``` -The test runs iperf3 with the following parameters: +Runs iperf3 with: - Duration: 10 seconds (`-t 10`) - Parallel streams: 8 (`-P 8`) - Protocol: TCP over IPv6 -This exercises the full FIPS stack including encryption, routing, and TUN device performance. - ## Network Impairment The `netem.sh` script simulates adverse network conditions using `tc`/`netem` @@ -105,7 +256,7 @@ on all running containers: ### Options | Option | Description | -|--------|-------------| +| ------ | ----------- | | `--delay ` | Fixed delay in milliseconds | | `--jitter ` | Delay variation (requires `--delay`) | | `--loss ` | Packet loss percentage | @@ -117,7 +268,7 @@ on all running containers: ### Presets | Preset | Parameters | -|--------|------------| +| ------ | ---------- | | `lossy` | 5% loss, 25% correlation | | `congested` | 50ms delay, 20ms jitter, 2% loss | | `terrible` | 100ms delay, 40ms jitter, 10% loss, 1% dup, 5% reorder | @@ -143,85 +294,49 @@ containers impaired equally, both directions of every link see the effect. The script uses `tc qdisc replace` so it can be re-run safely without removing rules first. -## Configuration Management - -Node configurations are generated from templates to ensure consistency across all nodes: - -### File Structure - -``` -configs/ -├── node.template.yaml # Single template for all node configs -├── topologies/ -│ ├── mesh.yaml # Mesh topology definition (reference) -│ └── chain.yaml # Chain topology definition (reference) -└── [mesh|chain]/ # Original hand-written configs (deprecated) - -generated-configs/ # Auto-generated configs (gitignored) -├── mesh/ -│ ├── node-a.yaml -│ ├── node-b.yaml -│ └── ... -└── chain/ - ├── node-a.yaml - └── ... -``` - -### Topology Files - -The `configs/topologies/` directory contains YAML files documenting each network topology: - -- Node identities (nsec, npub) -- Docker IP addresses -- Peer connections for each node - -These files serve as reference documentation and make it easy to understand and modify network topologies. - -### Generating Configs - -The `scripts/generate-configs.sh` script reads the topology definitions (embedded in the script) and generates node configs into `generated-configs/`: - -```bash -./scripts/generate-configs.sh [mesh|chain|all] -``` - -The build script (`scripts/build.sh`) automatically regenerates configs before building Docker images. - -### Modifying Topologies - -To change network topologies, edit the `get_mesh_peers()` or `get_chain_peers()` functions in `generate-configs.sh`, then update the corresponding YAML file in `configs/topologies/` for documentation. - -## Node Identities - -All nodes use deterministic test keys (not for production use). - -| Node | npub | FIPS IPv6 Address | Docker IP | -|------|------|-------------------|-----------| -| A | `npub1sjlh2c3...` | `fd69:e08d:65cc:3a6b:...` | 172.20.0.10 | -| B | `npub1tdwa4vj...` | `fd8e:302c:287e:b48d:...` | 172.20.0.11 | -| C | `npub1cld9yay...` | `fdac:a221:4069:5044:...` | 172.20.0.12 | -| D | `npub1n9lpnv0...` | `fdb6:8411:a191:6d48:...` | 172.20.0.13 | -| E | `npub1wf8akf8...` | `fded:7dee:d386:a546:...` | 172.20.0.14 | - ## Container Configuration - **Base image**: debian:bookworm-slim - **Capabilities**: `CAP_NET_ADMIN` (for TUN device creation) - **Devices**: `/dev/net/tun` mapped into each container - **DNS**: FIPS built-in resolver on `127.0.0.1:53` -- **Transport**: UDP on port 4000, MTU 1280 +- **Transport**: UDP on port 4000, MTU 1472 - **TUN**: `fips0` interface, MTU 1280 Each node resolves `.fips` DNS names to FIPS IPv6 addresses via its local DNS responder, which primes the identity cache for session establishment. +### Background Services + +Each container runs the following services alongside FIPS: + +| Service | Port | Description | +| ------- | ---- | --------------------------------------------- | +| SSH | 22 | Root login with no password (test only) | +| iperf3 | 5201 | Bandwidth testing server (`-s -D`) | +| HTTP | 8000 | Python HTTP server serving `/root/index.html` | + +All services bind to IPv6 (`::`) and are accessible over the FIPS overlay +using `.fips` hostnames: + +```bash +# HTTP over FIPS +docker exec fips-node-b curl http://$NPUB_A.fips:8000/ + +# SSH over FIPS +docker exec fips-node-b ssh $NPUB_A.fips + +# iperf3 over FIPS +docker exec fips-node-b iperf3 -c $NPUB_A.fips +``` + ## Troubleshooting **Stale images after code changes**: Docker compose may cache old layers. Force a clean rebuild: ```bash -docker compose --profile mesh build --no-cache +docker compose build --no-cache ``` **Check node logs**: @@ -234,7 +349,7 @@ docker logs -f fips-node-c # follow **Verify DNS resolution inside a container**: ```bash -docker exec fips-node-a dig AAAA npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le.fips @127.0.0.1 +docker exec fips-node-a dig AAAA .fips @127.0.0.1 ``` **Verify binary is up to date**: Compare hashes between the local build and @@ -248,3 +363,7 @@ docker exec fips-node-a md5sum /usr/local/bin/fips **Increase convergence time**: If tests fail intermittently, the 5-second convergence wait in `ping-test.sh` may be insufficient. Edit the `sleep` value at the top of the script. + +**Missing npubs.env**: If test scripts fail with "npubs.env not found", run +`./scripts/generate-configs.sh mesh` (or your topology) first, or use +`./scripts/build.sh` which generates configs automatically. diff --git a/examples/docker-network/configs/topologies/mesh-public.yaml b/examples/docker-network/configs/topologies/mesh-public.yaml new file mode 100644 index 0000000..2531615 --- /dev/null +++ b/examples/docker-network/configs/topologies/mesh-public.yaml @@ -0,0 +1,52 @@ +# Mesh Topology Definition +# +# Five nodes with 6 bidirectional UDP links forming a sparse, fully connected +# graph. Not all nodes are direct peers — non-adjacent pairs require +# discovery-driven multi-hop routing to establish end-to-end sessions. + +nodes: + a: + nsec: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" + npub: "npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m" + docker_ip: "172.20.0.10" + peers: [d, e, pub] + + b: + nsec: "b102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fb0" + npub: "npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le" + docker_ip: "172.20.0.11" + peers: [c, pub] + + c: + nsec: "c102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fc0" + npub: "npub1cld9yay0u24davpu6c35l4vldrhzvaq66pcqtg9a0j2cnjrn9rtsxx2pe6" + docker_ip: "172.20.0.12" + peers: [b, d, e, pub] + + d: + nsec: "d102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fd0" + npub: "npub1n9lpnv0592cc2ps6nm0ca3qls642vx7yjsv35rkxqzj2vgds52sqgpverl" + docker_ip: "172.20.0.13" + peers: [a, c, e] + + e: + nsec: "e102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fe0" + npub: "npub1wf8akf8lu2zdkjkmwhl75pqvven654mpv4sz2x2tprl5265mgrzq8nhak4" + docker_ip: "172.20.0.14" + peers: [a, c, d] + + # External/public node (not a Docker container) + pub: + nsec: "f102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1ff0" + npub: "npub13fhtmrk6f556acrjfkwrt37ppyhr5wcy89nhkcav9vcy27z5uygsd27sc3" + external_ip: "217.77.8.91" + peers: [a, b, c] + +# Spanning Tree Structure (rooted at node A): +# - A — D (tree edge, D's parent is A) +# - A — E (tree edge, E's parent is A) +# - C — D (tree edge, C's parent is D) +# - B — C (tree edge, B's parent is C) +# - D — E (non-tree link) +# - C — E (non-tree link) + diff --git a/examples/docker-network/docker-compose.yml b/examples/docker-network/docker-compose.yml index 61e0146..b340eef 100644 --- a/examples/docker-network/docker-compose.yml +++ b/examples/docker-network/docker-compose.yml @@ -15,6 +15,8 @@ x-fips-common: &fips-common sysctls: - net.ipv6.conf.all.disable_ipv6=0 restart: "no" + env_file: + - ./generated-configs/npubs.env environment: - RUST_LOG=trace @@ -80,6 +82,67 @@ services: fips-net: ipv4_address: 172.20.0.14 + # ── Mesh-public topology (mesh + external public node) ──────── + pub-a: + <<: *fips-common + profiles: ["mesh-public"] + container_name: fips-node-a + hostname: node-a + volumes: + - ./resolv.conf:/etc/resolv.conf:ro + - ./generated-configs/mesh-public/node-a.yaml:/etc/fips/fips.yaml:ro + networks: + fips-net: + ipv4_address: 172.20.0.10 + + pub-b: + <<: *fips-common + profiles: ["mesh-public"] + container_name: fips-node-b + hostname: node-b + volumes: + - ./resolv.conf:/etc/resolv.conf:ro + - ./generated-configs/mesh-public/node-b.yaml:/etc/fips/fips.yaml:ro + networks: + fips-net: + ipv4_address: 172.20.0.11 + + pub-c: + <<: *fips-common + profiles: ["mesh-public"] + container_name: fips-node-c + hostname: node-c + volumes: + - ./resolv.conf:/etc/resolv.conf:ro + - ./generated-configs/mesh-public/node-c.yaml:/etc/fips/fips.yaml:ro + networks: + fips-net: + ipv4_address: 172.20.0.12 + + pub-d: + <<: *fips-common + profiles: ["mesh-public"] + container_name: fips-node-d + hostname: node-d + volumes: + - ./resolv.conf:/etc/resolv.conf:ro + - ./generated-configs/mesh-public/node-d.yaml:/etc/fips/fips.yaml:ro + networks: + fips-net: + ipv4_address: 172.20.0.13 + + pub-e: + <<: *fips-common + profiles: ["mesh-public"] + container_name: fips-node-e + hostname: node-e + volumes: + - ./resolv.conf:/etc/resolv.conf:ro + - ./generated-configs/mesh-public/node-e.yaml:/etc/fips/fips.yaml:ro + networks: + fips-net: + ipv4_address: 172.20.0.14 + # ── Chain topology (A-B-C-D-E) ──────────────────────────────── chain-a: <<: *fips-common diff --git a/examples/docker-network/scripts/build.sh b/examples/docker-network/scripts/build.sh index a2680f4..55e2246 100755 --- a/examples/docker-network/scripts/build.sh +++ b/examples/docker-network/scripts/build.sh @@ -1,11 +1,18 @@ #!/bin/bash -# Build the FIPS binary and copy it to the docker build context. +# 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 @@ -14,6 +21,8 @@ if [ ! -f "$PROJECT_ROOT/Cargo.toml" ]; then exit 1 fi +echo "Using topology: $TOPOLOGY" + # Detect host OS UNAME_S=$(uname -s) CARGO_TARGET="x86_64-unknown-linux-musl" @@ -54,6 +63,9 @@ fi echo "Done. Binary at $DOCKER_DIR/fips" echo "" echo "Generating node configurations from templates..." -"$SCRIPT_DIR/generate-configs.sh" all +"$SCRIPT_DIR/generate-configs.sh" "$TOPOLOGY" $MESH_NAME echo "" -echo "Next: cd $DOCKER_DIR && docker compose --profile mesh build" +echo "Building Docker images..." +docker compose --profile "$TOPOLOGY" build +echo "" +echo "Ready: docker compose --profile $TOPOLOGY up -d" diff --git a/examples/docker-network/scripts/derive-keys.py b/examples/docker-network/scripts/derive-keys.py new file mode 100755 index 0000000..37495e5 --- /dev/null +++ b/examples/docker-network/scripts/derive-keys.py @@ -0,0 +1,111 @@ +#!/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/examples/docker-network/scripts/generate-configs.sh b/examples/docker-network/scripts/generate-configs.sh index aea6570..b57f872 100755 --- a/examples/docker-network/scripts/generate-configs.sh +++ b/examples/docker-network/scripts/generate-configs.sh @@ -1,7 +1,10 @@ #!/bin/bash # Generate FIPS node configuration files from template and topology definition. # -# Usage: ./generate-configs.sh [mesh|chain|all] +# Usage: ./generate-configs.sh [mesh-name] +# topology: mesh, mesh-public, chain, etc. +# mesh-name: optional; when given, docker node identities are derived +# deterministically via sha256(mesh-name|node-id) set -e @@ -9,79 +12,97 @@ 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" -# Node data lookup functions -get_nsec() { - case "$1" in - a) echo "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" ;; - b) echo "b102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fb0" ;; - c) echo "c102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fc0" ;; - d) echo "d102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fd0" ;; - e) echo "e102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fe0" ;; - esac +# Parse topology YAML to extract node attributes +# Usage: get_node_attr +get_node_attr() { + local topology_file="$1" + local node_id="$2" + local attr="$3" + # Handle both docker_ip and external_ip as "address" + if [ "$attr" = "address" ]; then + local ip=$(grep -A 10 "^ $node_id:" "$topology_file" | grep "docker_ip:" | head -1 | sed 's/.*: *"*\([^"]*\)".*/\1/') + if [ -z "$ip" ]; then + ip=$(grep -A 10 "^ $node_id:" "$topology_file" | grep "external_ip:" | head -1 | sed 's/.*: *"*\([^"]*\)".*/\1/') + fi + echo "$ip" + else + grep -A 10 "^ $node_id:" "$topology_file" | grep "${attr}:" | head -1 | sed 's/.*: *"*\([^"]*\)".*/\1/' + fi } -get_npub() { - case "$1" in - a) echo "npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m" ;; - b) echo "npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le" ;; - c) echo "npub1cld9yay0u24davpu6c35l4vldrhzvaq66pcqtg9a0j2cnjrn9rtsxx2pe6" ;; - d) echo "npub1n9lpnv0592cc2ps6nm0ca3qls642vx7yjsv35rkxqzj2vgds52sqgpverl" ;; - e) echo "npub1wf8akf8lu2zdkjkmwhl75pqvven654mpv4sz2x2tprl5265mgrzq8nhak4" ;; - esac +# Check if a node is external (has external_ip instead of docker_ip) +is_external_node() { + local topology_file="$1" + local node_id="$2" + local docker_ip=$(grep -A 10 "^ $node_id:" "$topology_file" | grep "docker_ip:" | head -1) + [ -z "$docker_ip" ] } -get_docker_ip() { - case "$1" in - a) echo "172.20.0.10" ;; - b) echo "172.20.0.11" ;; - c) echo "172.20.0.12" ;; - d) echo "172.20.0.13" ;; - e) echo "172.20.0.14" ;; - esac +# Get peers list from topology +get_peers() { + local topology_file="$1" + local node_id="$2" + grep -A 10 "^ $node_id:" "$topology_file" | grep "peers:" | head -1 | \ + sed 's/.*: *\[\(.*\)\].*/\1/' | \ + sed 's/,/ /g' | \ + tr -s ' ' | \ + sed 's/^ *//;s/ *$//' } -get_mesh_peers() { - case "$1" in - a) echo "d e" ;; - b) echo "c" ;; - c) echo "b d e" ;; - d) echo "a c e" ;; - e) echo "a c d" ;; - esac +# Get all node IDs from topology file +get_node_ids() { + local topology_file="$1" + grep "^ [a-z][a-z0-9_-]*:" "$topology_file" | sed 's/^ \([a-z][a-z0-9_-]*\):.*/\1/' } -get_chain_peers() { - case "$1" in - a) echo "b" ;; - b) echo "a c" ;; - c) echo "b d" ;; - d) echo "c e" ;; - e) echo "d" ;; - esac +# Resolve nsec and npub for a node. +# If MESH_NAME is set and node is not external, derive from mesh-name. +# Otherwise use the value from the topology YAML. +# Output: two lines: nsec=\nnpub= +resolve_keys() { + local topology_file="$1" + local node_id="$2" + + if [ -n "$MESH_NAME" ] && ! is_external_node "$topology_file" "$node_id"; then + python3 "$DERIVE_KEYS" "$MESH_NAME" "$node_id" + else + local nsec + local npub + nsec=$(get_node_attr "$topology_file" "$node_id" "nsec") + npub=$(get_node_attr "$topology_file" "$node_id" "npub") + echo "nsec=$nsec" + echo "npub=$npub" + fi } generate_peer_block() { - local peer_id="$1" + local topology_file="$1" + local peer_id="$2" + + local peer_npub="${RESOLVED_NPUB[$peer_id]}" + local peer_ip=$(get_node_attr "$topology_file" "$peer_id" "address") + cat < "$output_file" } +# Associative arrays for resolved keys (populated in generate_topology) +declare -A RESOLVED_NSEC +declare -A RESOLVED_NPUB + generate_topology() { - local topology="$1" - local output_dir="$GENERATED_DIR/$topology" - - echo "Generating $topology topology configs..." + local topology_name="$1" + local topology_file="$CONFIG_DIR/topologies/$topology_name.yaml" + local output_dir="$GENERATED_DIR/$topology_name" + + if [ ! -f "$topology_file" ]; then + echo "Error: Topology file not found: $topology_file" + exit 1 + fi + + echo "Generating $topology_name topology configs..." + if [ -n "$MESH_NAME" ]; then + echo " Mesh name: $MESH_NAME (deriving docker node identities)" + fi mkdir -p "$output_dir" - - for node_id in a b c d e; do - local peers - if [ "$topology" = "mesh" ]; then - peers=$(get_mesh_peers "$node_id") - else - peers=$(get_chain_peers "$node_id") + + # Phase 1: resolve keys for all nodes + for node_id in $(get_node_ids "$topology_file"); do + local keys="" + keys=$(resolve_keys "$topology_file" "$node_id") + RESOLVED_NSEC[$node_id]=$(echo "$keys" | grep "^nsec=" | cut -d= -f2) + RESOLVED_NPUB[$node_id]=$(echo "$keys" | grep "^npub=" | cut -d= -f2) + done + + # Phase 2: generate config files for docker nodes + for node_id in $(get_node_ids "$topology_file"); do + # Skip external nodes (they don't need Docker config files) + if is_external_node "$topology_file" "$node_id"; then + echo " ⚠ Skipping $node_id (external node)" + continue fi - + local output_file="$output_dir/node-$node_id.yaml" - generate_config "$node_id" "$topology" "$peers" > "$output_file" + generate_config "$node_id" "$topology_file" "$output_file" echo " ✓ Generated $output_file" done + + # Phase 3: write npubs.env + local env_file="$GENERATED_DIR/npubs.env" + echo "# Generated by generate-configs.sh (topology: $topology_name)" > "$env_file" + if [ -n "$MESH_NAME" ]; then + echo "# Mesh name: $MESH_NAME" >> "$env_file" + fi + for node_id in $(get_node_ids "$topology_file"); do + local var_name="NPUB_$(echo "$node_id" | tr '[:lower:]' '[:upper:]')" + echo "${var_name}=${RESOLVED_NPUB[$node_id]}" >> "$env_file" + done + echo " ✓ Generated $env_file" } main() { - local requested="${1:-all}" - - case "$requested" in - mesh) - generate_topology "mesh" - ;; - chain) - generate_topology "chain" - ;; - all) - generate_topology "mesh" - generate_topology "chain" - ;; - *) - echo "Error: Unknown topology '$requested'" - echo "Usage: $0 [mesh|chain|all]" - exit 1 - ;; - esac - + local requested="${1:-mesh}" + + # Support any topology file in the topologies directory + if [ -f "$CONFIG_DIR/topologies/$requested.yaml" ]; then + generate_topology "$requested" + else + echo "Error: Unknown topology '$requested'" + echo "Usage: $0 [mesh-name]" + echo "" + echo "Available topologies:" + ls -1 "$CONFIG_DIR/topologies/" | sed 's/\.yaml$//' | sed 's/^/ - /' + exit 1 + fi + echo "" echo "✓ All configurations generated successfully!" } -main "$@" \ No newline at end of file +MESH_NAME="${2:-}" +main "$@" diff --git a/examples/docker-network/scripts/iperf-test.sh b/examples/docker-network/scripts/iperf-test.sh index 0df6955..7c58b9d 100755 --- a/examples/docker-network/scripts/iperf-test.sh +++ b/examples/docker-network/scripts/iperf-test.sh @@ -23,19 +23,15 @@ PARALLEL=8 PASSED=0 FAILED=0 -# Node identities -NPUB_A="npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m" -NPUB_B="npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le" -NPUB_C="npub1cld9yay0u24davpu6c35l4vldrhzvaq66pcqtg9a0j2cnjrn9rtsxx2pe6" -NPUB_D="npub1n9lpnv0592cc2ps6nm0ca3qls642vx7yjsv35rkxqzj2vgds52sqgpverl" -NPUB_E="npub1wf8akf8lu2zdkjkmwhl75pqvven654mpv4sz2x2tprl5265mgrzq8nhak4" - -# FIPS IPv6 addresses (derived from npubs) -ADDR_A="fd69:e08d:65cc:3a6b:9c2c:2ac4:bd40:5e4b" -ADDR_B="fd8e:302c:287e:b48d:e5b7:6eee:e8a7:4f1e" -ADDR_C="fdac:a221:4069:5044:e0e5:7a5f:5a0f:4d43" -ADDR_D="fdb6:8411:a191:6d48:c5f7:e5f5:e5a5:2a50" -ADDR_E="fded:7dee:d386:a546:e5f7:e5f5:e5a5:2a50" +# Node identities (from generated env file) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +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 + exit 1 +fi +# shellcheck source=../generated-configs/npubs.env +source "$ENV_FILE" iperf_test() { local server_node="$1" diff --git a/examples/docker-network/scripts/ping-test.sh b/examples/docker-network/scripts/ping-test.sh index 2097921..09716ec 100755 --- a/examples/docker-network/scripts/ping-test.sh +++ b/examples/docker-network/scripts/ping-test.sh @@ -16,12 +16,15 @@ TIMEOUT=5 PASSED=0 FAILED=0 -# Node identities -NPUB_A="npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m" -NPUB_B="npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le" -NPUB_C="npub1cld9yay0u24davpu6c35l4vldrhzvaq66pcqtg9a0j2cnjrn9rtsxx2pe6" -NPUB_D="npub1n9lpnv0592cc2ps6nm0ca3qls642vx7yjsv35rkxqzj2vgds52sqgpverl" -NPUB_E="npub1wf8akf8lu2zdkjkmwhl75pqvven654mpv4sz2x2tprl5265mgrzq8nhak4" +# Node identities (from generated env file) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +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 + exit 1 +fi +# shellcheck source=../generated-configs/npubs.env +source "$ENV_FILE" ping_test() { local from="$1" @@ -52,7 +55,7 @@ echo "" echo "Waiting 3s for mesh convergence..." sleep 3 -if [ "$PROFILE" = "mesh" ]; then +if [ "$PROFILE" = "mesh" ] || [ "$PROFILE" = "mesh-public" ]; then # Sparse mesh topology: A-B, B-C, C-D, D-E, E-A, A-D # Test all 20 directed pairs (5 nodes × 4 targets each) echo ""