mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
Add static and stochastic Docker test harnesses
Move examples/docker-network/ to testing/static/ and add testing/chaos/ as a new stochastic simulation harness. testing/static/ — Static 5-node test harness: - Fixed mesh, chain, and mesh-public topologies with docker compose - Manual test scripts (ping, iperf, netem) - Build script, config generation, key derivation testing/chaos/ — Stochastic network simulation: - Python orchestrator generating N-node FIPS meshes with dynamic network conditions, driven by reproducible YAML scenarios - Topology generation: random geometric, Erdos-Renyi, or chain graphs with BFS connectivity guarantee - Per-link netem: HTB classful qdiscs with u32 filters for per-peer impairment (delay, loss, jitter), stochastic mutation across configurable policy profiles - Per-link bandwidth pacing: HTB rate limiting with configurable tiers (1/10/100/1000 mbps) randomly assigned per edge - Link flaps: tc netem 100% loss with graph connectivity protection - Node churn: docker stop/start with netem re-application on restart, shared down_nodes tracking across all managers - Traffic generation: random iperf3 sessions between node pairs - Down-node guards: all docker exec callers check container liveness, auto-detect crashed containers via is_container_running() safety net - Log collection and post-run analysis (panics, errors, sessions, MMP metrics, tree reconvergence) - chaos.sh wrapper with --seed, --duration, --verbose, --list options - Four scenarios: smoke-10, chaos-10, churn-10, churn-20
This commit is contained in:
@@ -1,2 +0,0 @@
|
||||
# Default docker compose profile (mesh topology)
|
||||
COMPOSE_PROFILES=mesh
|
||||
@@ -1,2 +0,0 @@
|
||||
fips
|
||||
generated-configs
|
||||
@@ -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 /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/fips
|
||||
|
||||
# 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"]
|
||||
@@ -1,369 +0,0 @@
|
||||
# Docker Network Test Harness
|
||||
|
||||
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 generate configs:
|
||||
|
||||
```bash
|
||||
./scripts/build.sh
|
||||
```
|
||||
|
||||
Start the mesh (default topology):
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
./scripts/ping-test.sh mesh # 20/20 expected
|
||||
./scripts/iperf-test.sh mesh # bandwidth test
|
||||
docker compose down
|
||||
```
|
||||
|
||||
The mesh profile is activated by default via `.env`. To use a different
|
||||
topology, specify the profile explicitly:
|
||||
|
||||
```bash
|
||||
docker compose --profile chain up -d
|
||||
./scripts/ping-test.sh chain
|
||||
docker compose --profile chain down
|
||||
```
|
||||
|
||||
## Topologies
|
||||
|
||||
### Mesh
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
The spanning tree is rooted at node A, which has the lexicographically
|
||||
smallest `NodeAddr` (the first 16 bytes of `SHA-256(pubkey)`). Tree edges
|
||||
are highlighted in blue in the diagram above.
|
||||
|
||||
The ping test exercises all 20 directed pairs (5 nodes x 4 targets each),
|
||||
covering both direct-peer and multi-hop paths.
|
||||
|
||||
| Link | Type |
|
||||
|------|------|
|
||||
| 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 |
|
||||
|
||||
### Chain
|
||||
|
||||

|
||||
|
||||
Five nodes in a linear chain: A — B — C — D — E. Each node peers only with
|
||||
its immediate neighbors. Multi-hop communication (e.g., A to E) requires the
|
||||
discovery protocol to find routes through intermediate nodes.
|
||||
|
||||
The ping test covers:
|
||||
|
||||
- Adjacent hops: A→B, B→C (1 hop each)
|
||||
- Multi-hop: A→C (2 hops), A→D (3 hops), A→E (4 hops)
|
||||
- Reverse: E→A (4 hops)
|
||||
|
||||
### Mesh-Public
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
Runs iperf3 with:
|
||||
|
||||
- Duration: 10 seconds (`-t 10`)
|
||||
- Parallel streams: 8 (`-P 8`)
|
||||
- Protocol: TCP over IPv6
|
||||
|
||||
## Network Impairment
|
||||
|
||||
The `netem.sh` script simulates adverse network conditions using `tc`/`netem`
|
||||
on all running containers:
|
||||
|
||||
```bash
|
||||
./scripts/netem.sh [mesh|chain] <apply|remove|status> [options]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Description |
|
||||
| ------ | ----------- |
|
||||
| `--delay <ms>` | Fixed delay in milliseconds |
|
||||
| `--jitter <ms>` | Delay variation (requires `--delay`) |
|
||||
| `--loss <percent>` | Packet loss percentage |
|
||||
| `--loss-corr <percent>` | Loss correlation for bursty loss |
|
||||
| `--duplicate <percent>` | Packet duplication percentage |
|
||||
| `--reorder <percent>` | Packet reordering probability (requires `--delay`) |
|
||||
| `--corrupt <percent>` | Bit-level corruption percentage |
|
||||
|
||||
### 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 |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Apply 50ms delay with 5% packet loss
|
||||
./scripts/netem.sh mesh apply --delay 50 --loss 5
|
||||
|
||||
# Use a preset
|
||||
./scripts/netem.sh chain apply --preset congested
|
||||
|
||||
# Check current rules
|
||||
./scripts/netem.sh mesh status
|
||||
|
||||
# Remove all impairment
|
||||
./scripts/netem.sh mesh remove
|
||||
```
|
||||
|
||||
Rules are applied to egress on each container's `eth0` interface. With all
|
||||
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.
|
||||
|
||||
## 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 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 build --no-cache
|
||||
```
|
||||
|
||||
**Check node logs**:
|
||||
|
||||
```bash
|
||||
docker logs fips-node-a
|
||||
docker logs -f fips-node-c # follow
|
||||
```
|
||||
|
||||
**Verify DNS resolution inside a container**:
|
||||
|
||||
```bash
|
||||
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
|
||||
the binary inside the container:
|
||||
|
||||
```bash
|
||||
md5sum examples/docker-network/fips
|
||||
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.
|
||||
@@ -1,30 +0,0 @@
|
||||
# FIPS Node {{NODE_NAME}} configuration ({{TOPOLOGY}} topology)
|
||||
#
|
||||
# Identity: {{NPUB}}
|
||||
|
||||
node:
|
||||
identity:
|
||||
nsec: "{{NSEC}}"
|
||||
|
||||
tun:
|
||||
enabled: true
|
||||
name: fips0
|
||||
mtu: 1280
|
||||
|
||||
dns:
|
||||
enabled: true
|
||||
bind_addr: "127.0.0.1"
|
||||
|
||||
transports:
|
||||
udp:
|
||||
bind_addr: "0.0.0.0:4000"
|
||||
mtu: 1472
|
||||
# recv_buf_size: 2097152 # 2 MB (default)
|
||||
# send_buf_size: 2097152 # 2 MB (default)
|
||||
# Note: The kernel clamps socket buffers to net.core.rmem_max / wmem_max.
|
||||
# Ensure these sysctls are >= the configured buffer sizes:
|
||||
# sysctl -w net.core.rmem_max=2097152
|
||||
# sysctl -w net.core.wmem_max=2097152
|
||||
|
||||
peers:
|
||||
{{PEERS}}
|
||||
@@ -1,42 +0,0 @@
|
||||
# Chain Topology Definition
|
||||
#
|
||||
# Five nodes in a linear chain: A — B — C — D — E
|
||||
# Each node peers only with its immediate neighbors. Multi-hop communication
|
||||
# (e.g., A to E) requires the discovery protocol to find routes through
|
||||
# intermediate nodes.
|
||||
|
||||
nodes:
|
||||
a:
|
||||
nsec: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
|
||||
npub: "npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m"
|
||||
docker_ip: "172.20.0.10"
|
||||
peers: [b]
|
||||
|
||||
b:
|
||||
nsec: "b102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fb0"
|
||||
npub: "npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le"
|
||||
docker_ip: "172.20.0.11"
|
||||
peers: [a, c]
|
||||
|
||||
c:
|
||||
nsec: "c102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fc0"
|
||||
npub: "npub1cld9yay0u24davpu6c35l4vldrhzvaq66pcqtg9a0j2cnjrn9rtsxx2pe6"
|
||||
docker_ip: "172.20.0.12"
|
||||
peers: [b, d]
|
||||
|
||||
d:
|
||||
nsec: "d102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fd0"
|
||||
npub: "npub1n9lpnv0592cc2ps6nm0ca3qls642vx7yjsv35rkxqzj2vgds52sqgpverl"
|
||||
docker_ip: "172.20.0.13"
|
||||
peers: [c, e]
|
||||
|
||||
e:
|
||||
nsec: "e102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fe0"
|
||||
npub: "npub1wf8akf8lu2zdkjkmwhl75pqvven654mpv4sz2x2tprl5265mgrzq8nhak4"
|
||||
docker_ip: "172.20.0.14"
|
||||
peers: [d]
|
||||
|
||||
# Test Coverage:
|
||||
# - Adjacent hops: A→B, B→C (1 hop each)
|
||||
# - Multi-hop: A→C (2 hops), A→D (3 hops), A→E (4 hops)
|
||||
# - Reverse: E→A (4 hops)
|
||||
@@ -1,52 +0,0 @@
|
||||
# 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)
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
# 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]
|
||||
|
||||
b:
|
||||
nsec: "b102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fb0"
|
||||
npub: "npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le"
|
||||
docker_ip: "172.20.0.11"
|
||||
peers: [c]
|
||||
|
||||
c:
|
||||
nsec: "c102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fc0"
|
||||
npub: "npub1cld9yay0u24davpu6c35l4vldrhzvaq66pcqtg9a0j2cnjrn9rtsxx2pe6"
|
||||
docker_ip: "172.20.0.12"
|
||||
peers: [b, d, e]
|
||||
|
||||
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]
|
||||
|
||||
# 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)
|
||||
@@ -1,113 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 400" font-family="monospace" font-size="13">
|
||||
<style>
|
||||
circle.node { fill: #1a1a2e; stroke: #e0e0e0; stroke-width: 2; }
|
||||
text { fill: #e0e0e0; }
|
||||
text.name { font-size: 14px; font-weight: bold; }
|
||||
text.ip { font-size: 10px; fill: #808890; }
|
||||
text.fips-addr { font-size: 9px; fill: #607090; }
|
||||
text.title { font-size: 16px; font-weight: bold; }
|
||||
text.subtitle { font-size: 11px; fill: #909090; }
|
||||
text.legend { font-size: 11px; }
|
||||
text.net-label { font-size: 10px; fill: #606880; }
|
||||
text.hop-label { font-size: 10px; fill: #909090; }
|
||||
line.link { stroke: #506090; stroke-width: 2; }
|
||||
line.path { stroke: #d0a040; stroke-width: 1.2; stroke-dasharray: 5 3; }
|
||||
rect.legend-box { fill: #151520; stroke: #404060; stroke-width: 1; rx: 4; }
|
||||
rect.net-box { fill: none; stroke: #303050; stroke-width: 1; stroke-dasharray: 6 3; rx: 8; }
|
||||
marker { fill: #d0a040; }
|
||||
</style>
|
||||
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 6" refX="10" refY="3"
|
||||
markerWidth="8" markerHeight="5" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 3 L 0 6 z"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="700" height="400" fill="#0d1117" rx="8"/>
|
||||
|
||||
<!-- Title -->
|
||||
<text x="350" y="30" text-anchor="middle" class="title">Docker Chain Topology</text>
|
||||
<text x="350" y="48" text-anchor="middle" class="subtitle">Linear chain — A–B–C–D–E, multi-hop routing via discovery</text>
|
||||
|
||||
<!-- Docker network boundary -->
|
||||
<rect x="30" y="65" width="640" height="175" class="net-box"/>
|
||||
<text x="40" y="82" class="net-label">172.20.0.0/24 (docker: fips-net)</text>
|
||||
|
||||
<!--
|
||||
Horizontal chain layout:
|
||||
A (110, 170) B (230, 170) C (350, 170) D (470, 170) E (590, 170)
|
||||
-->
|
||||
|
||||
<!-- === Links === -->
|
||||
|
||||
<!-- A—B -->
|
||||
<line x1="138" y1="170" x2="202" y2="170" class="link"/>
|
||||
<text x="170" y="160" text-anchor="middle" class="hop-label">UDP</text>
|
||||
|
||||
<!-- B—C -->
|
||||
<line x1="258" y1="170" x2="322" y2="170" class="link"/>
|
||||
<text x="290" y="160" text-anchor="middle" class="hop-label">UDP</text>
|
||||
|
||||
<!-- C—D -->
|
||||
<line x1="378" y1="170" x2="442" y2="170" class="link"/>
|
||||
<text x="410" y="160" text-anchor="middle" class="hop-label">UDP</text>
|
||||
|
||||
<!-- D—E -->
|
||||
<line x1="498" y1="170" x2="562" y2="170" class="link"/>
|
||||
<text x="530" y="160" text-anchor="middle" class="hop-label">UDP</text>
|
||||
|
||||
<!-- === Multi-hop path illustration (A→E, 4 hops) === -->
|
||||
<line x1="110" y1="210" x2="590" y2="210" class="path"
|
||||
marker-end="url(#arrow)"/>
|
||||
<text x="350" y="228" text-anchor="middle" class="hop-label">
|
||||
A → E (4 hops, discovery-routed)
|
||||
</text>
|
||||
|
||||
<!-- === Nodes === -->
|
||||
|
||||
<!-- Node A -->
|
||||
<circle cx="110" cy="170" r="28" class="node"/>
|
||||
<text x="110" y="166" text-anchor="middle" class="name">A</text>
|
||||
<text x="110" y="179" text-anchor="middle" class="ip">.10</text>
|
||||
<text x="110" y="130" text-anchor="middle" class="fips-addr">fd69:e08d:...</text>
|
||||
|
||||
<!-- Node B -->
|
||||
<circle cx="230" cy="170" r="28" class="node"/>
|
||||
<text x="230" y="166" text-anchor="middle" class="name">B</text>
|
||||
<text x="230" y="179" text-anchor="middle" class="ip">.11</text>
|
||||
<text x="230" y="130" text-anchor="middle" class="fips-addr">fd8e:302c:...</text>
|
||||
|
||||
<!-- Node C -->
|
||||
<circle cx="350" cy="170" r="28" class="node"/>
|
||||
<text x="350" y="166" text-anchor="middle" class="name">C</text>
|
||||
<text x="350" y="179" text-anchor="middle" class="ip">.12</text>
|
||||
<text x="350" y="130" text-anchor="middle" class="fips-addr">fdac:a221:...</text>
|
||||
|
||||
<!-- Node D -->
|
||||
<circle cx="470" cy="170" r="28" class="node"/>
|
||||
<text x="470" y="166" text-anchor="middle" class="name">D</text>
|
||||
<text x="470" y="179" text-anchor="middle" class="ip">.13</text>
|
||||
<text x="470" y="130" text-anchor="middle" class="fips-addr">fdb6:8411:...</text>
|
||||
|
||||
<!-- Node E -->
|
||||
<circle cx="590" cy="170" r="28" class="node"/>
|
||||
<text x="590" y="166" text-anchor="middle" class="name">E</text>
|
||||
<text x="590" y="179" text-anchor="middle" class="ip">.14</text>
|
||||
<text x="590" y="130" text-anchor="middle" class="fips-addr">fded:7dee:...</text>
|
||||
|
||||
<!-- === Legend === -->
|
||||
<rect x="30" y="260" width="640" height="120" class="legend-box"/>
|
||||
|
||||
<line x1="50" y1="282" x2="90" y2="282" stroke="#506090" stroke-width="2"/>
|
||||
<text x="100" y="286" class="legend">Direct UDP peer link (port 4000, auto_connect)</text>
|
||||
|
||||
<line x1="50" y1="304" x2="90" y2="304" stroke="#d0a040" stroke-width="1.2"
|
||||
stroke-dasharray="5 3" marker-end="url(#arrow)"/>
|
||||
<text x="100" y="308" class="legend">Multi-hop path (discovery + session establishment)</text>
|
||||
|
||||
<text x="50" y="332" class="legend" fill="#808890">Routing flow: TUN packet → identity cache → initiate_session()</text>
|
||||
<text x="50" y="347" class="legend" fill="#808890"> → find_next_hop() fails → discovery flood → route_cache populated</text>
|
||||
<text x="50" y="362" class="legend" fill="#808890"> → session retry → Noise IK handshake → encrypted data delivery</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.9 KiB |
@@ -1,205 +0,0 @@
|
||||
networks:
|
||||
fips-net:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.20.0.0/24
|
||||
|
||||
x-fips-common: &fips-common
|
||||
build:
|
||||
context: .
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
sysctls:
|
||||
- net.ipv6.conf.all.disable_ipv6=0
|
||||
restart: "no"
|
||||
env_file:
|
||||
- ./generated-configs/npubs.env
|
||||
environment:
|
||||
- RUST_LOG=info
|
||||
|
||||
services:
|
||||
# ── Mesh topology ──────────────────────────────────────────────
|
||||
node-a:
|
||||
<<: *fips-common
|
||||
profiles: ["mesh"]
|
||||
container_name: fips-node-a
|
||||
hostname: node-a
|
||||
volumes:
|
||||
- ./resolv.conf:/etc/resolv.conf:ro
|
||||
- ./generated-configs/mesh/node-a.yaml:/etc/fips/fips.yaml:ro
|
||||
networks:
|
||||
fips-net:
|
||||
ipv4_address: 172.20.0.10
|
||||
|
||||
node-b:
|
||||
<<: *fips-common
|
||||
profiles: ["mesh"]
|
||||
container_name: fips-node-b
|
||||
hostname: node-b
|
||||
volumes:
|
||||
- ./resolv.conf:/etc/resolv.conf:ro
|
||||
- ./generated-configs/mesh/node-b.yaml:/etc/fips/fips.yaml:ro
|
||||
networks:
|
||||
fips-net:
|
||||
ipv4_address: 172.20.0.11
|
||||
|
||||
node-c:
|
||||
<<: *fips-common
|
||||
profiles: ["mesh"]
|
||||
container_name: fips-node-c
|
||||
hostname: node-c
|
||||
volumes:
|
||||
- ./resolv.conf:/etc/resolv.conf:ro
|
||||
- ./generated-configs/mesh/node-c.yaml:/etc/fips/fips.yaml:ro
|
||||
networks:
|
||||
fips-net:
|
||||
ipv4_address: 172.20.0.12
|
||||
|
||||
node-d:
|
||||
<<: *fips-common
|
||||
profiles: ["mesh"]
|
||||
container_name: fips-node-d
|
||||
hostname: node-d
|
||||
volumes:
|
||||
- ./resolv.conf:/etc/resolv.conf:ro
|
||||
- ./generated-configs/mesh/node-d.yaml:/etc/fips/fips.yaml:ro
|
||||
networks:
|
||||
fips-net:
|
||||
ipv4_address: 172.20.0.13
|
||||
|
||||
node-e:
|
||||
<<: *fips-common
|
||||
profiles: ["mesh"]
|
||||
container_name: fips-node-e
|
||||
hostname: node-e
|
||||
volumes:
|
||||
- ./resolv.conf:/etc/resolv.conf:ro
|
||||
- ./generated-configs/mesh/node-e.yaml:/etc/fips/fips.yaml:ro
|
||||
networks:
|
||||
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
|
||||
profiles: ["chain"]
|
||||
container_name: fips-node-a
|
||||
hostname: node-a
|
||||
volumes:
|
||||
- ./resolv.conf:/etc/resolv.conf:ro
|
||||
- ./generated-configs/chain/node-a.yaml:/etc/fips/fips.yaml:ro
|
||||
networks:
|
||||
fips-net:
|
||||
ipv4_address: 172.20.0.10
|
||||
|
||||
chain-b:
|
||||
<<: *fips-common
|
||||
profiles: ["chain"]
|
||||
container_name: fips-node-b
|
||||
hostname: node-b
|
||||
volumes:
|
||||
- ./resolv.conf:/etc/resolv.conf:ro
|
||||
- ./generated-configs/chain/node-b.yaml:/etc/fips/fips.yaml:ro
|
||||
networks:
|
||||
fips-net:
|
||||
ipv4_address: 172.20.0.11
|
||||
|
||||
chain-c:
|
||||
<<: *fips-common
|
||||
profiles: ["chain"]
|
||||
container_name: fips-node-c
|
||||
hostname: node-c
|
||||
volumes:
|
||||
- ./resolv.conf:/etc/resolv.conf:ro
|
||||
- ./generated-configs/chain/node-c.yaml:/etc/fips/fips.yaml:ro
|
||||
networks:
|
||||
fips-net:
|
||||
ipv4_address: 172.20.0.12
|
||||
|
||||
chain-d:
|
||||
<<: *fips-common
|
||||
profiles: ["chain"]
|
||||
container_name: fips-node-d
|
||||
hostname: node-d
|
||||
volumes:
|
||||
- ./resolv.conf:/etc/resolv.conf:ro
|
||||
- ./generated-configs/chain/node-d.yaml:/etc/fips/fips.yaml:ro
|
||||
networks:
|
||||
fips-net:
|
||||
ipv4_address: 172.20.0.13
|
||||
|
||||
chain-e:
|
||||
<<: *fips-common
|
||||
profiles: ["chain"]
|
||||
container_name: fips-node-e
|
||||
hostname: node-e
|
||||
volumes:
|
||||
- ./resolv.conf:/etc/resolv.conf:ro
|
||||
- ./generated-configs/chain/node-e.yaml:/etc/fips/fips.yaml:ro
|
||||
networks:
|
||||
fips-net:
|
||||
ipv4_address: 172.20.0.14
|
||||
@@ -1,130 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 560" font-family="monospace" font-size="13">
|
||||
<style>
|
||||
circle.node { fill: #1a1a2e; stroke: #e0e0e0; stroke-width: 2; }
|
||||
circle.root { fill: #1a1a2e; stroke: #60a0e0; stroke-width: 2.5; }
|
||||
text { fill: #e0e0e0; }
|
||||
text.name { font-size: 14px; font-weight: bold; }
|
||||
text.root-label { font-size: 9px; fill: #60a0e0; }
|
||||
text.npub { font-size: 9px; fill: #808890; }
|
||||
text.title { font-size: 16px; font-weight: bold; }
|
||||
text.subtitle { font-size: 11px; fill: #909090; }
|
||||
text.legend { font-size: 11px; }
|
||||
text.net-label { font-size: 10px; fill: #606880; }
|
||||
text.test-label { font-size: 10px; fill: #909090; }
|
||||
line.link { stroke: #404860; stroke-width: 1.5; }
|
||||
line.link-tree { stroke: #60a0e0; stroke-width: 2; }
|
||||
rect.legend-box { fill: #151520; stroke: #404060; stroke-width: 1; rx: 4; }
|
||||
rect.net-box { fill: none; stroke: #303050; stroke-width: 1;
|
||||
stroke-dasharray: 6 3; rx: 8; }
|
||||
</style>
|
||||
|
||||
<defs>
|
||||
<marker id="tree-arrow" viewBox="0 0 10 6" refX="38" refY="3"
|
||||
markerWidth="10" markerHeight="7" orient="auto-start-reverse"
|
||||
markerUnits="userSpaceOnUse">
|
||||
<path d="M 0 0 L 10 3 L 0 6 z" fill="#60a0e0"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="700" height="560" fill="#0d1117" rx="8"/>
|
||||
|
||||
<!-- Title -->
|
||||
<text x="350" y="30" text-anchor="middle" class="title">Docker Mesh Topology</text>
|
||||
<text x="350" y="48" text-anchor="middle" class="subtitle">Sparse mesh — 5 nodes, 6 bidirectional UDP links</text>
|
||||
|
||||
<!-- Docker network boundary -->
|
||||
<rect x="40" y="62" width="620" height="310" class="net-box"/>
|
||||
<text x="52" y="78" class="net-label">172.20.0.0/24 (docker: fips-net)</text>
|
||||
|
||||
<!--
|
||||
Pentagon layout (centered at 350,225):
|
||||
A = top (350, 110)
|
||||
B = upper-right (550, 195)
|
||||
C = lower-right (480, 335)
|
||||
D = lower-left (220, 335)
|
||||
E = upper-left (150, 195)
|
||||
|
||||
Links: B-C, C-D, D-E, E-A, A-D, C-E
|
||||
|
||||
Spanning tree (root=A, lowest NodeAddr):
|
||||
A → D, A → E
|
||||
D → C
|
||||
C → B
|
||||
Tree edges highlighted; non-tree edges dim.
|
||||
-->
|
||||
|
||||
<!-- === Peer links (6 edges) === -->
|
||||
<!-- Non-tree edges (dim) -->
|
||||
<!-- D — E -->
|
||||
<line x1="220" y1="335" x2="150" y2="195" class="link"/>
|
||||
<!-- C — E -->
|
||||
<line x1="480" y1="335" x2="150" y2="195" class="link"/>
|
||||
|
||||
<!-- Tree edges (highlighted, arrow toward parent/root) -->
|
||||
<!-- B → C (parent) -->
|
||||
<line x1="550" y1="195" x2="480" y2="335" class="link-tree"
|
||||
marker-end="url(#tree-arrow)"/>
|
||||
<!-- C → D (parent) -->
|
||||
<line x1="480" y1="335" x2="220" y2="335" class="link-tree"
|
||||
marker-end="url(#tree-arrow)"/>
|
||||
<!-- E → A (parent) -->
|
||||
<line x1="150" y1="195" x2="350" y2="110" class="link-tree"
|
||||
marker-end="url(#tree-arrow)"/>
|
||||
<!-- D → A (parent) -->
|
||||
<line x1="220" y1="335" x2="350" y2="110" class="link-tree"
|
||||
marker-end="url(#tree-arrow)"/>
|
||||
|
||||
<!-- === Nodes (on top of links) === -->
|
||||
|
||||
<!-- Node A — root (2 peers: D, E) -->
|
||||
<circle cx="350" cy="110" r="26" class="root"/>
|
||||
<text x="350" y="108" text-anchor="middle" class="name">A</text>
|
||||
<text x="350" y="121" text-anchor="middle" class="npub">.10</text>
|
||||
<text x="350" y="75" text-anchor="middle" class="npub">npub1sjlh2c3...</text>
|
||||
<text x="350" y="148" text-anchor="middle" class="root-label">root</text>
|
||||
|
||||
<!-- Node B (1 peer: C) -->
|
||||
<circle cx="550" cy="195" r="26" class="node"/>
|
||||
<text x="550" y="193" text-anchor="middle" class="name">B</text>
|
||||
<text x="550" y="206" text-anchor="middle" class="npub">.11</text>
|
||||
<text x="550" y="162" text-anchor="middle" class="npub">npub1tdwa4vj...</text>
|
||||
|
||||
<!-- Node C (3 peers: B, D, E) -->
|
||||
<circle cx="480" cy="335" r="26" class="node"/>
|
||||
<text x="480" y="333" text-anchor="middle" class="name">C</text>
|
||||
<text x="480" y="346" text-anchor="middle" class="npub">.12</text>
|
||||
<text x="516" y="357" text-anchor="start" class="npub">npub1cld9yay...</text>
|
||||
|
||||
<!-- Node D (3 peers: A, C, E) -->
|
||||
<circle cx="220" cy="335" r="26" class="node"/>
|
||||
<text x="220" y="333" text-anchor="middle" class="name">D</text>
|
||||
<text x="220" y="346" text-anchor="middle" class="npub">.13</text>
|
||||
<text x="112" y="357" text-anchor="start" class="npub">npub1n9lpnv0...</text>
|
||||
|
||||
<!-- Node E (3 peers: A, C, D) -->
|
||||
<circle cx="150" cy="195" r="26" class="node"/>
|
||||
<text x="150" y="193" text-anchor="middle" class="name">E</text>
|
||||
<text x="150" y="206" text-anchor="middle" class="npub">.14</text>
|
||||
<text x="150" y="162" text-anchor="middle" class="npub">npub1wf8akf8...</text>
|
||||
|
||||
<!-- Test info -->
|
||||
<text x="350" y="390" text-anchor="middle" class="test-label">
|
||||
All-pairs ping test: 20 directed paths (direct + multi-hop via discovery)
|
||||
</text>
|
||||
|
||||
<!-- === Legend === -->
|
||||
<rect x="40" y="410" width="620" height="130" class="legend-box"/>
|
||||
|
||||
<line x1="60" y1="435" x2="100" y2="435" stroke="#404860" stroke-width="1.5"/>
|
||||
<text x="110" y="439" class="legend">UDP peer link (port 4000, auto_connect)</text>
|
||||
|
||||
<line x1="55" y1="457" x2="100" y2="457" stroke="#60a0e0" stroke-width="2"/>
|
||||
<path d="M 90 454 L 100 457 L 90 460 z" fill="#60a0e0"/>
|
||||
<text x="110" y="461" class="legend">Spanning tree edge (arrow toward parent/root)</text>
|
||||
|
||||
<circle cx="72" cy="479" r="8" fill="#1a1a2e" stroke="#60a0e0" stroke-width="2"/>
|
||||
<text x="110" y="483" class="legend">Tree root (lowest NodeAddr = SHA-256(pubkey)[:16])</text>
|
||||
|
||||
<text x="60" y="525" class="legend" fill="#808890">Each container: CAP_NET_ADMIN, /dev/net/tun, DNS on 127.0.0.1:53</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.5 KiB |
@@ -1 +0,0 @@
|
||||
nameserver 127.0.0.1
|
||||
@@ -1,71 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Build the FIPS binary, generate configs, and build Docker images.
|
||||
# Supports cross-compilation from macOS to Linux using cargo-zigbuild.
|
||||
# Usage: ./build.sh [topology] [mesh-name]
|
||||
# topology: mesh, mesh-public, chain, etc. (default: mesh)
|
||||
# mesh-name: optional; derives unique node identities via sha256(mesh-name|node-id)
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
DOCKER_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
# Topology to use (default: mesh)
|
||||
TOPOLOGY="${1:-mesh}"
|
||||
MESH_NAME="${2:-}"
|
||||
|
||||
# Find project root (directory containing Cargo.toml)
|
||||
PROJECT_ROOT="$(cd "$DOCKER_DIR/../.." && pwd)"
|
||||
if [ ! -f "$PROJECT_ROOT/Cargo.toml" ]; then
|
||||
echo "Error: Cannot find Cargo.toml at $PROJECT_ROOT" >&2
|
||||
echo "Expected layout: <project-root>/examples/docker-network/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 binary to docker context..."
|
||||
cp "$PROJECT_ROOT/target/$CARGO_TARGET/release/fips" "$DOCKER_DIR/fips"
|
||||
else
|
||||
# Native Linux build
|
||||
echo "Building FIPS (release)..."
|
||||
cargo build --release --manifest-path="$PROJECT_ROOT/Cargo.toml"
|
||||
|
||||
echo "Copying binary to docker context..."
|
||||
cp "$PROJECT_ROOT/target/release/fips" "$DOCKER_DIR/fips"
|
||||
fi
|
||||
|
||||
echo "Done. Binary at $DOCKER_DIR/fips"
|
||||
echo ""
|
||||
echo "Generating node configurations from templates..."
|
||||
"$SCRIPT_DIR/generate-configs.sh" "$TOPOLOGY" $MESH_NAME
|
||||
echo ""
|
||||
echo "Building Docker images..."
|
||||
docker compose --profile "$TOPOLOGY" build
|
||||
echo ""
|
||||
echo "Ready: docker compose --profile $TOPOLOGY up -d"
|
||||
@@ -1,111 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Derive deterministic nostr nsec/npub from mesh-name and node-name.
|
||||
|
||||
Usage: derive-keys.py <mesh-name> <node-name>
|
||||
Output: nsec=<hex>\nnpub=<bech32>
|
||||
|
||||
Derivation: nsec = sha256(mesh_name + "|" + node_name)
|
||||
npub = bech32("npub", secp256k1_pubkey_x(nsec))
|
||||
|
||||
Pure Python, no external dependencies.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import sys
|
||||
|
||||
# --- secp256k1 ---
|
||||
|
||||
P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
|
||||
Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
|
||||
Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
|
||||
|
||||
|
||||
def _modinv(a, m):
|
||||
return pow(a, m - 2, m)
|
||||
|
||||
|
||||
def _point_add(p1, p2):
|
||||
if p1 is None:
|
||||
return p2
|
||||
if p2 is None:
|
||||
return p1
|
||||
x1, y1 = p1
|
||||
x2, y2 = p2
|
||||
if x1 == x2 and y1 != y2:
|
||||
return None
|
||||
if x1 == x2:
|
||||
lam = (3 * x1 * x1) * _modinv(2 * y1, P) % P
|
||||
else:
|
||||
lam = (y2 - y1) * _modinv(x2 - x1, P) % P
|
||||
x3 = (lam * lam - x1 - x2) % P
|
||||
y3 = (lam * (x1 - x3) - y1) % P
|
||||
return (x3, y3)
|
||||
|
||||
|
||||
def _scalar_mult(k, point):
|
||||
result = None
|
||||
addend = point
|
||||
while k:
|
||||
if k & 1:
|
||||
result = _point_add(result, addend)
|
||||
addend = _point_add(addend, addend)
|
||||
k >>= 1
|
||||
return result
|
||||
|
||||
|
||||
# --- bech32 (BIP-173) ---
|
||||
|
||||
_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
||||
|
||||
|
||||
def _bech32_polymod(values):
|
||||
gen = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3]
|
||||
chk = 1
|
||||
for v in values:
|
||||
b = chk >> 25
|
||||
chk = (chk & 0x1FFFFFF) << 5 ^ v
|
||||
for i in range(5):
|
||||
chk ^= gen[i] if ((b >> i) & 1) else 0
|
||||
return chk
|
||||
|
||||
|
||||
def _bech32_encode(hrp, data_5bit):
|
||||
hrp_expand = [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
|
||||
polymod = _bech32_polymod(hrp_expand + data_5bit + [0] * 6) ^ 1
|
||||
checksum = [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
|
||||
return hrp + "1" + "".join(_CHARSET[d] for d in data_5bit + checksum)
|
||||
|
||||
|
||||
def _convertbits(data, frombits, tobits):
|
||||
acc, bits, ret = 0, 0, []
|
||||
maxv = (1 << tobits) - 1
|
||||
for value in data:
|
||||
acc = (acc << frombits) | value
|
||||
bits += frombits
|
||||
while bits >= tobits:
|
||||
bits -= tobits
|
||||
ret.append((acc >> bits) & maxv)
|
||||
if bits:
|
||||
ret.append((acc << (tobits - bits)) & maxv)
|
||||
return ret
|
||||
|
||||
|
||||
# --- public API ---
|
||||
|
||||
def derive(mesh_name, node_name):
|
||||
nsec_hex = hashlib.sha256(f"{mesh_name}|{node_name}".encode()).hexdigest()
|
||||
k = int(nsec_hex, 16)
|
||||
pub = _scalar_mult(k, (Gx, Gy))
|
||||
x_hex = format(pub[0], "064x")
|
||||
data_5bit = _convertbits(list(bytes.fromhex(x_hex)), 8, 5)
|
||||
npub = _bech32_encode("npub", data_5bit)
|
||||
return nsec_hex, npub
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
print(f"Usage: {sys.argv[0]} <mesh-name> <node-name>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
nsec, npub = derive(sys.argv[1], sys.argv[2])
|
||||
print(f"nsec={nsec}")
|
||||
print(f"npub={npub}")
|
||||
@@ -1,219 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Generate FIPS node configuration files from template and topology definition.
|
||||
#
|
||||
# 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
|
||||
|
||||
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"
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
# 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 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 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/'
|
||||
}
|
||||
|
||||
# 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 topology_file="$1"
|
||||
local peer_id="$2"
|
||||
|
||||
local peer_npub="$(get_key RESOLVED_NPUB "$peer_id")"
|
||||
local peer_ip=$(get_node_attr "$topology_file" "$peer_id" "address")
|
||||
|
||||
cat <<EOF
|
||||
- npub: "$peer_npub"
|
||||
alias: "node-$peer_id"
|
||||
addresses:
|
||||
- transport: udp
|
||||
addr: "$peer_ip:4000"
|
||||
connect_policy: auto_connect
|
||||
EOF
|
||||
}
|
||||
|
||||
generate_config() {
|
||||
local node_id="$1"
|
||||
local topology_file="$2"
|
||||
local output_file="$3"
|
||||
|
||||
local node_npub
|
||||
node_npub="$(get_key RESOLVED_NPUB "$node_id")"
|
||||
local node_nsec
|
||||
node_nsec="$(get_key RESOLVED_NSEC "$node_id")"
|
||||
local peers=$(get_peers "$topology_file" "$node_id")
|
||||
|
||||
# Generate peers section
|
||||
local peers_config=""
|
||||
if [ -n "$peers" ]; then
|
||||
for peer_id in $peers; do
|
||||
if [ -n "$peers_config" ]; then
|
||||
peers_config="$peers_config"$'\n'
|
||||
fi
|
||||
peers_config="$peers_config$(generate_peer_block "$topology_file" "$peer_id")"
|
||||
done
|
||||
else
|
||||
peers_config=" []"
|
||||
fi
|
||||
|
||||
# Read and process template
|
||||
local template=$(cat "$TEMPLATE_FILE")
|
||||
local config="$template"
|
||||
|
||||
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" > "$output_file"
|
||||
}
|
||||
|
||||
# Key storage for bash 3.2 compatibility (using prefixed variables instead of associative arrays)
|
||||
# Usage: set_key NSEC a "value" / get_key NSEC a
|
||||
set_key() {
|
||||
local prefix="$1"
|
||||
local key="$2"
|
||||
local value="$3"
|
||||
eval "${prefix}_${key}=\"${value}\""
|
||||
}
|
||||
|
||||
get_key() {
|
||||
local prefix="$1"
|
||||
local key="$2"
|
||||
eval "echo \"\$${prefix}_${key}\""
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
# 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")
|
||||
set_key RESOLVED_NSEC "$node_id" "$(echo "$keys" | grep "^nsec=" | cut -d= -f2)"
|
||||
set_key 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_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}=$(get_key RESOLVED_NPUB "$node_id")" >> "$env_file"
|
||||
done
|
||||
echo " ✓ Generated $env_file"
|
||||
}
|
||||
|
||||
main() {
|
||||
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 <topology> [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!"
|
||||
}
|
||||
|
||||
MESH_NAME="${2:-}"
|
||||
main "$@"
|
||||
@@ -1,123 +0,0 @@
|
||||
#!/bin/bash
|
||||
# End-to-end iperf3 bandwidth test between FIPS nodes via DNS resolution.
|
||||
# Usage: ./iperf-test.sh [mesh|chain] [--live]
|
||||
#
|
||||
# Requires containers to be running:
|
||||
# docker compose --profile mesh up -d
|
||||
# ./scripts/iperf-test.sh mesh
|
||||
# ./scripts/iperf-test.sh mesh --live # Show live iperf3 output
|
||||
set -e
|
||||
|
||||
# Exit entire script on Ctrl+C
|
||||
trap 'echo ""; echo "Test interrupted"; exit 130' INT
|
||||
|
||||
PROFILE="${1:-mesh}"
|
||||
LIVE_OUTPUT=false
|
||||
if [ "$2" = "--live" ] || [ "$1" = "--live" ]; then
|
||||
LIVE_OUTPUT=true
|
||||
[ "$1" = "--live" ] && PROFILE="mesh"
|
||||
fi
|
||||
|
||||
DURATION=10
|
||||
PARALLEL=8
|
||||
PASSED=0
|
||||
FAILED=0
|
||||
|
||||
# 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"
|
||||
local client_node="$2"
|
||||
local dest_npub="$3"
|
||||
local label="$4"
|
||||
|
||||
echo ""
|
||||
echo "=== $label ==="
|
||||
|
||||
# iperf3 server is already running in daemon mode in each container
|
||||
|
||||
if [ "$LIVE_OUTPUT" = true ]; then
|
||||
# Show live output
|
||||
echo "Running iperf3 test (live output):"
|
||||
if docker exec "fips-$client_node" iperf3 -c "${dest_npub}.fips" -t "$DURATION" -P "$PARALLEL"; then
|
||||
PASSED=$((PASSED + 1))
|
||||
else
|
||||
echo "FAIL"
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
else
|
||||
# Capture and summarize output
|
||||
echo -n "Running iperf3 test... "
|
||||
local output
|
||||
if output=$(docker exec "fips-$client_node" iperf3 -c "${dest_npub}.fips" -t "$DURATION" -P "$PARALLEL" 2>&1); then
|
||||
# Check if we got valid results
|
||||
if echo "$output" | grep -q "sender"; then
|
||||
# Extract and display results (get SUM line for aggregate bandwidth)
|
||||
local bandwidth=$(echo "$output" | grep "\[SUM\].*sender" | tail -1 | awk '{for(i=1;i<=NF;i++) if($i ~ /bits\/sec/) {print $(i-1), $i; exit}}')
|
||||
echo "OK"
|
||||
echo "Bandwidth: $bandwidth"
|
||||
PASSED=$((PASSED + 1))
|
||||
else
|
||||
echo "FAIL (no bandwidth data)"
|
||||
echo "Output: $output"
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
else
|
||||
echo "FAIL"
|
||||
echo "Error output:"
|
||||
echo "$output" | head -10
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
echo "=== FIPS iperf3 Bandwidth Test ($PROFILE topology) ==="
|
||||
echo ""
|
||||
|
||||
# Wait for nodes to converge
|
||||
echo "Waiting 3s for mesh convergence..."
|
||||
sleep 3
|
||||
|
||||
if [ "$PROFILE" = "mesh" ] || [ "$PROFILE" = "mesh-public" ]; then
|
||||
# Test key paths in mesh topology
|
||||
echo ""
|
||||
echo "Testing mesh topology paths:"
|
||||
|
||||
# Direct peer links (client on A, server on D/E)
|
||||
iperf_test node-d node-a "$NPUB_D" "A → D (direct peer)"
|
||||
iperf_test node-e node-a "$NPUB_E" "A → E (direct peer)"
|
||||
|
||||
# Multi-hop paths (client on A, server on B/C)
|
||||
iperf_test node-b node-a "$NPUB_B" "A → B (multi-hop)"
|
||||
iperf_test node-c node-a "$NPUB_C" "A → C (multi-hop)"
|
||||
|
||||
# Reverse test (client on E, server on A)
|
||||
iperf_test node-a node-e "$NPUB_A" "E → A (direct peer)"
|
||||
|
||||
elif [ "$PROFILE" = "chain" ]; then
|
||||
echo ""
|
||||
echo "Testing chain topology paths:"
|
||||
|
||||
# Adjacent hop (client on A, server on B)
|
||||
iperf_test node-b node-a "$NPUB_B" "A → B (1 hop)"
|
||||
|
||||
# Multi-hop tests (client on A, server on C/D/E)
|
||||
iperf_test node-c node-a "$NPUB_C" "A → C (2 hops)"
|
||||
iperf_test node-d node-a "$NPUB_D" "A → D (3 hops)"
|
||||
iperf_test node-e node-a "$NPUB_E" "A → E (4 hops)"
|
||||
|
||||
# Reverse multi-hop (client on E, server on A)
|
||||
iperf_test node-a node-e "$NPUB_A" "E → A (4 hops)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Results: $PASSED passed, $FAILED failed ==="
|
||||
[ "$FAILED" -eq 0 ] && exit 0 || exit 1
|
||||
@@ -1,245 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Network impairment simulation using tc/netem on FIPS Docker containers.
|
||||
#
|
||||
# Usage: ./netem.sh <mesh|chain> <apply|remove|status> [options]
|
||||
#
|
||||
# Actions:
|
||||
# apply - Apply netem rules to all containers in the profile
|
||||
# remove - Remove netem rules from all containers
|
||||
# status - Show current tc qdisc state on each container
|
||||
#
|
||||
# Options (for apply):
|
||||
# --delay <ms> Fixed delay in milliseconds
|
||||
# --jitter <ms> Delay variation (requires --delay)
|
||||
# --loss <percent> Packet loss percentage
|
||||
# --loss-corr <percent> Loss correlation for bursty loss
|
||||
# --duplicate <percent> Packet duplication percentage
|
||||
# --reorder <percent> Packet reordering probability
|
||||
# --corrupt <percent> Bit-level corruption percentage
|
||||
#
|
||||
# Presets (shorthand for common combinations):
|
||||
# --preset lossy 5% loss, 25% correlation
|
||||
# --preset congested 50ms delay, 20ms jitter, 2% loss
|
||||
# --preset terrible 100ms delay, 40ms jitter, 10% loss, 1% dup, 5% reorder
|
||||
#
|
||||
# Examples:
|
||||
# ./netem.sh mesh apply --delay 50 --loss 5
|
||||
# ./netem.sh chain apply --preset congested
|
||||
# ./netem.sh mesh status
|
||||
# ./netem.sh mesh remove
|
||||
set -e
|
||||
|
||||
trap 'echo ""; echo "Interrupted"; exit 130' INT
|
||||
|
||||
NODES="a b c d e"
|
||||
IFACE="eth0"
|
||||
|
||||
# Defaults
|
||||
DELAY=0
|
||||
JITTER=0
|
||||
LOSS=0
|
||||
LOSS_CORR=0
|
||||
DUPLICATE=0
|
||||
REORDER=0
|
||||
CORRUPT=0
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <mesh|chain> <apply|remove|status> [options]"
|
||||
echo ""
|
||||
echo "Actions:"
|
||||
echo " apply - Apply netem rules to all containers"
|
||||
echo " remove - Remove netem rules from all containers"
|
||||
echo " status - Show current tc qdisc on each container"
|
||||
echo ""
|
||||
echo "Options (for apply):"
|
||||
echo " --delay <ms> Fixed delay"
|
||||
echo " --jitter <ms> Delay variation (requires --delay)"
|
||||
echo " --loss <percent> Packet loss"
|
||||
echo " --loss-corr <percent> Loss correlation"
|
||||
echo " --duplicate <percent> Packet duplication"
|
||||
echo " --reorder <percent> Packet reordering"
|
||||
echo " --corrupt <percent> Bit-level corruption"
|
||||
echo " --preset <name> Use a named preset (lossy, congested, terrible)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
apply_preset() {
|
||||
case "$1" in
|
||||
lossy)
|
||||
LOSS=5
|
||||
LOSS_CORR=25
|
||||
;;
|
||||
congested)
|
||||
DELAY=50
|
||||
JITTER=20
|
||||
LOSS=2
|
||||
;;
|
||||
terrible)
|
||||
DELAY=100
|
||||
JITTER=40
|
||||
LOSS=10
|
||||
DUPLICATE=1
|
||||
REORDER=5
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown preset '$1'" >&2
|
||||
echo "Available presets: lossy, congested, terrible" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
[ $# -lt 2 ] && usage
|
||||
|
||||
PROFILE="$1"
|
||||
ACTION="$2"
|
||||
shift 2
|
||||
|
||||
case "$PROFILE" in
|
||||
mesh|chain) ;;
|
||||
*) echo "Error: Profile must be 'mesh' or 'chain'" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
case "$ACTION" in
|
||||
apply|remove|status) ;;
|
||||
*) echo "Error: Action must be 'apply', 'remove', or 'status'" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
# Parse options
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--delay) DELAY="$2"; shift 2 ;;
|
||||
--jitter) JITTER="$2"; shift 2 ;;
|
||||
--loss) LOSS="$2"; shift 2 ;;
|
||||
--loss-corr) LOSS_CORR="$2"; shift 2 ;;
|
||||
--duplicate) DUPLICATE="$2"; shift 2 ;;
|
||||
--reorder) REORDER="$2"; shift 2 ;;
|
||||
--corrupt) CORRUPT="$2"; shift 2 ;;
|
||||
--preset) apply_preset "$2"; shift 2 ;;
|
||||
*)
|
||||
echo "Error: Unknown option '$1'" >&2
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Build netem parameter string from non-zero values
|
||||
build_netem_params() {
|
||||
local params=""
|
||||
|
||||
if [ "$DELAY" != "0" ]; then
|
||||
params="delay ${DELAY}ms"
|
||||
if [ "$JITTER" != "0" ]; then
|
||||
params="$params ${JITTER}ms"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$LOSS" != "0" ]; then
|
||||
params="$params loss ${LOSS}%"
|
||||
if [ "$LOSS_CORR" != "0" ]; then
|
||||
params="$params ${LOSS_CORR}%"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$DUPLICATE" != "0" ]; then
|
||||
params="$params duplicate ${DUPLICATE}%"
|
||||
fi
|
||||
|
||||
if [ "$REORDER" != "0" ]; then
|
||||
if [ "$DELAY" = "0" ]; then
|
||||
echo "Error: --reorder requires --delay (reordering needs a delay queue)" >&2
|
||||
exit 1
|
||||
fi
|
||||
params="$params reorder ${REORDER}%"
|
||||
fi
|
||||
|
||||
if [ "$CORRUPT" != "0" ]; then
|
||||
params="$params corrupt ${CORRUPT}%"
|
||||
fi
|
||||
|
||||
echo "$params"
|
||||
}
|
||||
|
||||
# Check if a container is running
|
||||
container_running() {
|
||||
docker inspect -f '{{.State.Running}}' "$1" 2>/dev/null | grep -q true
|
||||
}
|
||||
|
||||
do_apply() {
|
||||
local params
|
||||
params=$(build_netem_params)
|
||||
|
||||
if [ -z "$params" ]; then
|
||||
echo "Error: No impairment parameters specified" >&2
|
||||
echo "Use --delay, --loss, --duplicate, --reorder, --corrupt, or --preset" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Applying netem: $params ==="
|
||||
echo ""
|
||||
|
||||
for node in $NODES; do
|
||||
local container="fips-node-$node"
|
||||
echo -n " $container ... "
|
||||
if ! container_running "$container"; then
|
||||
echo "SKIP (not running)"
|
||||
continue
|
||||
fi
|
||||
if docker exec "$container" tc qdisc replace dev "$IFACE" root netem $params 2>&1; then
|
||||
echo "OK"
|
||||
else
|
||||
echo "FAIL"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
do_remove() {
|
||||
echo "=== Removing netem rules ==="
|
||||
echo ""
|
||||
|
||||
for node in $NODES; do
|
||||
local container="fips-node-$node"
|
||||
echo -n " $container ... "
|
||||
if ! container_running "$container"; then
|
||||
echo "SKIP (not running)"
|
||||
continue
|
||||
fi
|
||||
# Suppress error if no qdisc exists
|
||||
if docker exec "$container" tc qdisc del dev "$IFACE" root 2>/dev/null; then
|
||||
echo "OK"
|
||||
else
|
||||
echo "OK (no rules)"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
do_status() {
|
||||
echo "=== netem status ==="
|
||||
echo ""
|
||||
|
||||
for node in $NODES; do
|
||||
local container="fips-node-$node"
|
||||
echo " $container:"
|
||||
if ! container_running "$container"; then
|
||||
echo " (not running)"
|
||||
continue
|
||||
fi
|
||||
local output
|
||||
output=$(docker exec "$container" tc qdisc show dev "$IFACE" 2>&1)
|
||||
if echo "$output" | grep -q "netem"; then
|
||||
echo " $output"
|
||||
else
|
||||
echo " (no netem rules)"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
case "$ACTION" in
|
||||
apply) do_apply ;;
|
||||
remove) do_remove ;;
|
||||
status) do_status ;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "Done."
|
||||
@@ -1,115 +0,0 @@
|
||||
#!/bin/bash
|
||||
# End-to-end ping test between FIPS nodes via DNS resolution.
|
||||
# Usage: ./ping-test.sh [mesh|chain]
|
||||
#
|
||||
# Requires containers to be running:
|
||||
# docker compose --profile mesh up -d
|
||||
# ./scripts/ping-test.sh mesh
|
||||
set -e
|
||||
|
||||
# Exit entire script on Ctrl+C
|
||||
trap 'echo ""; echo "Test interrupted"; exit 130' INT
|
||||
|
||||
PROFILE="${1:-mesh}"
|
||||
COUNT=1
|
||||
TIMEOUT=5
|
||||
PASSED=0
|
||||
FAILED=0
|
||||
|
||||
# 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"
|
||||
local to_npub="$2"
|
||||
local label="$3"
|
||||
|
||||
echo -n " $label ... "
|
||||
local output
|
||||
if output=$(docker exec "fips-$from" ping6 -c "$COUNT" -W "$TIMEOUT" "${to_npub}.fips" 2>&1); then
|
||||
# Extract round-trip time from ping output
|
||||
local rtt=$(echo "$output" | grep -oE 'time=[0-9.]+' | cut -d= -f2)
|
||||
if [ -n "$rtt" ]; then
|
||||
echo "OK (${rtt}ms)"
|
||||
else
|
||||
echo "OK"
|
||||
fi
|
||||
PASSED=$((PASSED + 1))
|
||||
else
|
||||
echo "FAIL"
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
echo "=== FIPS Ping Test ($PROFILE topology) ==="
|
||||
echo ""
|
||||
|
||||
# Wait for nodes to converge
|
||||
echo "Waiting 3s for mesh convergence..."
|
||||
sleep 3
|
||||
|
||||
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 ""
|
||||
echo "From node-a:"
|
||||
ping_test node-a "$NPUB_B" "A → B"
|
||||
ping_test node-a "$NPUB_C" "A → C"
|
||||
ping_test node-a "$NPUB_D" "A → D"
|
||||
ping_test node-a "$NPUB_E" "A → E"
|
||||
|
||||
echo ""
|
||||
echo "From node-b:"
|
||||
ping_test node-b "$NPUB_A" "B → A"
|
||||
ping_test node-b "$NPUB_C" "B → C"
|
||||
ping_test node-b "$NPUB_D" "B → D"
|
||||
ping_test node-b "$NPUB_E" "B → E"
|
||||
|
||||
echo ""
|
||||
echo "From node-c:"
|
||||
ping_test node-c "$NPUB_A" "C → A"
|
||||
ping_test node-c "$NPUB_B" "C → B"
|
||||
ping_test node-c "$NPUB_D" "C → D"
|
||||
ping_test node-c "$NPUB_E" "C → E"
|
||||
|
||||
echo ""
|
||||
echo "From node-d:"
|
||||
ping_test node-d "$NPUB_A" "D → A"
|
||||
ping_test node-d "$NPUB_B" "D → B"
|
||||
ping_test node-d "$NPUB_C" "D → C"
|
||||
ping_test node-d "$NPUB_E" "D → E"
|
||||
|
||||
echo ""
|
||||
echo "From node-e:"
|
||||
ping_test node-e "$NPUB_A" "E → A"
|
||||
ping_test node-e "$NPUB_B" "E → B"
|
||||
ping_test node-e "$NPUB_C" "E → C"
|
||||
ping_test node-e "$NPUB_D" "E → D"
|
||||
|
||||
elif [ "$PROFILE" = "chain" ]; then
|
||||
echo ""
|
||||
echo "Adjacent peer tests:"
|
||||
ping_test node-a "$NPUB_B" "A → B (1 hop)"
|
||||
ping_test node-b "$NPUB_C" "B → C (1 hop)"
|
||||
|
||||
echo ""
|
||||
echo "Multi-hop tests:"
|
||||
ping_test node-a "$NPUB_C" "A → C (2 hops)"
|
||||
ping_test node-a "$NPUB_D" "A → D (3 hops)"
|
||||
ping_test node-a "$NPUB_E" "A → E (4 hops)"
|
||||
|
||||
echo ""
|
||||
echo "Reverse multi-hop:"
|
||||
ping_test node-e "$NPUB_A" "E → A (4 hops)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Results: $PASSED passed, $FAILED failed ==="
|
||||
[ "$FAILED" -eq 0 ] && exit 0 || exit 1
|
||||
Reference in New Issue
Block a user