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
This commit is contained in:
Johnathan Corgan
2026-02-18 13:20:03 +00:00
parent a1649ba4b7
commit a50473fe9f
10 changed files with 620 additions and 208 deletions
+2
View File
@@ -0,0 +1,2 @@
# Default docker compose profile (mesh topology)
COMPOSE_PROFILES=mesh
+6 -3
View File
@@ -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"]
+206 -87
View File
@@ -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 <topology> [mesh-name]
```
This reads the topology definition and generates:
1. Per-node YAML config files in `generated-configs/<topology>/`
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/<name>.yaml` following the format of
`mesh.yaml`
2. Add corresponding service definitions to `docker-compose.yml` with
`profiles: ["<name>"]`
3. Run `./scripts/generate-configs.sh <name>` 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 <ms>` | Fixed delay in milliseconds |
| `--jitter <ms>` | Delay variation (requires `--delay`) |
| `--loss <percent>` | 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 `<npub>.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 `<npub>.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 <npub>.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.
@@ -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)
@@ -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
+15 -3
View File
@@ -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"
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""Derive deterministic nostr nsec/npub from mesh-name and node-name.
Usage: derive-keys.py <mesh-name> <node-name>
Output: nsec=<hex>\nnpub=<bech32>
Derivation: nsec = sha256(mesh_name + "|" + node_name)
npub = bech32("npub", secp256k1_pubkey_x(nsec))
Pure Python, no external dependencies.
"""
import hashlib
import sys
# --- secp256k1 ---
P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
def _modinv(a, m):
return pow(a, m - 2, m)
def _point_add(p1, p2):
if p1 is None:
return p2
if p2 is None:
return p1
x1, y1 = p1
x2, y2 = p2
if x1 == x2 and y1 != y2:
return None
if x1 == x2:
lam = (3 * x1 * x1) * _modinv(2 * y1, P) % P
else:
lam = (y2 - y1) * _modinv(x2 - x1, P) % P
x3 = (lam * lam - x1 - x2) % P
y3 = (lam * (x1 - x3) - y1) % P
return (x3, y3)
def _scalar_mult(k, point):
result = None
addend = point
while k:
if k & 1:
result = _point_add(result, addend)
addend = _point_add(addend, addend)
k >>= 1
return result
# --- bech32 (BIP-173) ---
_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
def _bech32_polymod(values):
gen = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3]
chk = 1
for v in values:
b = chk >> 25
chk = (chk & 0x1FFFFFF) << 5 ^ v
for i in range(5):
chk ^= gen[i] if ((b >> i) & 1) else 0
return chk
def _bech32_encode(hrp, data_5bit):
hrp_expand = [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
polymod = _bech32_polymod(hrp_expand + data_5bit + [0] * 6) ^ 1
checksum = [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
return hrp + "1" + "".join(_CHARSET[d] for d in data_5bit + checksum)
def _convertbits(data, frombits, tobits):
acc, bits, ret = 0, 0, []
maxv = (1 << tobits) - 1
for value in data:
acc = (acc << frombits) | value
bits += frombits
while bits >= tobits:
bits -= tobits
ret.append((acc >> bits) & maxv)
if bits:
ret.append((acc << (tobits - bits)) & maxv)
return ret
# --- public API ---
def derive(mesh_name, node_name):
nsec_hex = hashlib.sha256(f"{mesh_name}|{node_name}".encode()).hexdigest()
k = int(nsec_hex, 16)
pub = _scalar_mult(k, (Gx, Gy))
x_hex = format(pub[0], "064x")
data_5bit = _convertbits(list(bytes.fromhex(x_hex)), 8, 5)
npub = _bech32_encode("npub", data_5bit)
return nsec_hex, npub
if __name__ == "__main__":
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <mesh-name> <node-name>", file=sys.stderr)
sys.exit(1)
nsec, npub = derive(sys.argv[1], sys.argv[2])
print(f"nsec={nsec}")
print(f"npub={npub}")
@@ -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 <topology> [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,78 +12,96 @@ 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 <topology_file> <node_id> <attr_name>
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=<hex>\nnpub=<bech32>
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 <<EOF
- npub: "$(get_npub "$peer_id")"
- npub: "$peer_npub"
alias: "node-$peer_id"
addresses:
- transport: udp
addr: "$(get_docker_ip "$peer_id"):4000"
addr: "$peer_ip:4000"
connect_policy: auto_connect
EOF
}
generate_config() {
local node_id="$1"
local topology="$2"
local peers="$3"
local topology_file="$2"
local output_file="$3"
local node_name=$(echo "$node_id" | tr '[:lower:]' '[:upper:]')
local template
template=$(cat "$TEMPLATE_FILE")
local node_npub="${RESOLVED_NPUB[$node_id]}"
local node_nsec="${RESOLVED_NSEC[$node_id]}"
local peers=$(get_peers "$topology_file" "$node_id")
# Generate peers section
local peers_config=""
@@ -89,67 +110,97 @@ generate_config() {
if [ -n "$peers_config" ]; then
peers_config="$peers_config"$'\n'
fi
peers_config="$peers_config$(generate_peer_block "$peer_id")"
peers_config="$peers_config$(generate_peer_block "$topology_file" "$peer_id")"
done
else
peers_config=" []"
fi
# Replace template variables
# Read and process template
local template=$(cat "$TEMPLATE_FILE")
local config="$template"
config="${config//\{\{NODE_NAME\}\}/$node_name}"
config="${config//\{\{TOPOLOGY\}\}/$topology}"
config="${config//\{\{NPUB\}\}/$(get_npub "$node_id")}"
config="${config//\{\{NSEC\}\}/$(get_nsec "$node_id")}"
config="${config//\{\{NODE_NAME\}\}/$(echo "$node_id" | tr '[:lower:]' '[:upper:]')}"
config="${config//\{\{TOPOLOGY\}\}/$(basename "$topology_file" .yaml)}"
config="${config//\{\{NPUB\}\}/$node_npub}"
config="${config//\{\{NSEC\}\}/$node_nsec}"
config="${config//\{\{PEERS\}\}/$peers_config}"
echo "$config"
echo "$config" > "$output_file"
}
generate_topology() {
local topology="$1"
local output_dir="$GENERATED_DIR/$topology"
# Associative arrays for resolved keys (populated in generate_topology)
declare -A RESOLVED_NSEC
declare -A RESOLVED_NPUB
echo "Generating $topology topology configs..."
generate_topology() {
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}"
local requested="${1:-mesh}"
case "$requested" in
mesh)
generate_topology "mesh"
;;
chain)
generate_topology "chain"
;;
all)
generate_topology "mesh"
generate_topology "chain"
;;
*)
# 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|chain|all]"
echo "Usage: $0 <topology> [mesh-name]"
echo ""
echo "Available topologies:"
ls -1 "$CONFIG_DIR/topologies/" | sed 's/\.yaml$//' | sed 's/^/ - /'
exit 1
;;
esac
fi
echo ""
echo "✓ All configurations generated successfully!"
}
MESH_NAME="${2:-}"
main "$@"
+9 -13
View File
@@ -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"
+10 -7
View File
@@ -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 ""