From 5236fd02bf4f9a09d71834ea2c57063fe261d103 Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Sat, 14 Feb 2026 18:38:26 +0000 Subject: [PATCH] Replace two-node-udp with Docker network test harness, fix dead code warnings Replace examples/two-node-udp/ (netns-based) with examples/docker-network/ (Docker compose, 5 nodes, mesh + chain topologies). The new harness uses auto-detecting build script and includes SVG topology diagrams. Remove unused get_session_mut, remote_addr(), remote_pubkey() methods. Gate test-only accessors with #[cfg(test)]. Zero warnings in release build. --- examples/docker-network/.gitignore | 1 + examples/docker-network/Dockerfile | 11 + examples/docker-network/README.md | 134 +++++++++ .../configs/chain/node-a.yaml} | 9 +- .../docker-network/configs/chain/node-b.yaml | 37 +++ .../docker-network/configs/chain/node-c.yaml | 37 +++ .../docker-network/configs/chain/node-d.yaml | 37 +++ .../docker-network/configs/chain/node-e.yaml | 31 +++ .../docker-network/configs/mesh/node-a.yaml | 37 +++ .../configs/mesh/node-b.yaml} | 13 +- .../docker-network/configs/mesh/node-c.yaml | 43 +++ .../docker-network/configs/mesh/node-d.yaml | 43 +++ .../docker-network/configs/mesh/node-e.yaml | 43 +++ .../docker-network/docker-chain-topology.svg | 113 ++++++++ examples/docker-network/docker-compose.yml | 142 ++++++++++ .../docker-network/docker-mesh-topology.svg | 130 +++++++++ examples/docker-network/resolv.conf | 1 + examples/docker-network/scripts/build.sh | 24 ++ examples/docker-network/scripts/ping-test.sh | 102 +++++++ examples/two-node-udp/README.md | 258 ------------------ examples/two-node-udp/two-node-udp.svg | 110 -------- src/node/mod.rs | 7 +- src/node/session.rs | 15 +- 23 files changed, 983 insertions(+), 395 deletions(-) create mode 100644 examples/docker-network/.gitignore create mode 100644 examples/docker-network/Dockerfile create mode 100644 examples/docker-network/README.md rename examples/{two-node-udp/fips-a.yaml => docker-network/configs/chain/node-a.yaml} (80%) create mode 100644 examples/docker-network/configs/chain/node-b.yaml create mode 100644 examples/docker-network/configs/chain/node-c.yaml create mode 100644 examples/docker-network/configs/chain/node-d.yaml create mode 100644 examples/docker-network/configs/chain/node-e.yaml create mode 100644 examples/docker-network/configs/mesh/node-a.yaml rename examples/{two-node-udp/fips-b.yaml => docker-network/configs/mesh/node-b.yaml} (66%) create mode 100644 examples/docker-network/configs/mesh/node-c.yaml create mode 100644 examples/docker-network/configs/mesh/node-d.yaml create mode 100644 examples/docker-network/configs/mesh/node-e.yaml create mode 100644 examples/docker-network/docker-chain-topology.svg create mode 100644 examples/docker-network/docker-compose.yml create mode 100644 examples/docker-network/docker-mesh-topology.svg create mode 100644 examples/docker-network/resolv.conf create mode 100755 examples/docker-network/scripts/build.sh create mode 100755 examples/docker-network/scripts/ping-test.sh delete mode 100644 examples/two-node-udp/README.md delete mode 100644 examples/two-node-udp/two-node-udp.svg diff --git a/examples/docker-network/.gitignore b/examples/docker-network/.gitignore new file mode 100644 index 0000000..52be687 --- /dev/null +++ b/examples/docker-network/.gitignore @@ -0,0 +1 @@ +fips diff --git a/examples/docker-network/Dockerfile b/examples/docker-network/Dockerfile new file mode 100644 index 0000000..6c4f042 --- /dev/null +++ b/examples/docker-network/Dockerfile @@ -0,0 +1,11 @@ +FROM debian:bookworm-slim + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + iproute2 iputils-ping dnsutils && \ + rm -rf /var/lib/apt/lists/* + +COPY fips /usr/local/bin/ +RUN chmod +x /usr/local/bin/fips + +ENTRYPOINT ["fips", "--config", "/etc/fips/fips.yaml"] diff --git a/examples/docker-network/README.md b/examples/docker-network/README.md new file mode 100644 index 0000000..3f17d06 --- /dev/null +++ b/examples/docker-network/README.md @@ -0,0 +1,134 @@ +# 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. + +## Prerequisites + +- Docker with the compose plugin +- Rust toolchain (for building the FIPS binary) + +## Quick Start + +Build the binary and copy it to the docker context: + +```bash +./scripts/build.sh +``` + +### Mesh Topology + +```bash +docker compose --profile mesh build +docker compose --profile mesh up -d +./scripts/ping-test.sh mesh # 20/20 expected +docker compose --profile mesh down +``` + +### Chain Topology + +```bash +docker compose --profile chain build +docker compose --profile chain up -d +./scripts/ping-test.sh chain # 6/6 expected +docker compose --profile chain down +``` + +## Mesh Topology + +![Mesh Topology](docker-mesh-topology.svg) + +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 Topology + +![Chain Topology](docker-chain-topology.svg) + +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) + +## 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 +- **TUN**: `fips0` interface, MTU 1280 + +Each node resolves `.fips` DNS names to FIPS IPv6 addresses via its +local DNS responder, which primes the identity cache for session establishment. + +## Troubleshooting + +**Stale images after code changes**: Docker compose may cache old layers. +Force a clean rebuild: + +```bash +docker compose --profile mesh 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 npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le.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. diff --git a/examples/two-node-udp/fips-a.yaml b/examples/docker-network/configs/chain/node-a.yaml similarity index 80% rename from examples/two-node-udp/fips-a.yaml rename to examples/docker-network/configs/chain/node-a.yaml index 3b89fc7..c8b9344 100644 --- a/examples/two-node-udp/fips-a.yaml +++ b/examples/docker-network/configs/chain/node-a.yaml @@ -1,4 +1,4 @@ -# FIPS Node A configuration +# FIPS Node A configuration (chain topology: A-B-C-D-E) # # Identity: npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m # FIPS address: fd69:e08d:65cc:3a6b:9c2c:2ac4:bd40:5e4b @@ -15,11 +15,11 @@ tun: dns: enabled: true bind_addr: "127.0.0.1" - port: 5354 + port: 53 transports: udp: - bind_addr: "10.0.0.1:4000" + bind_addr: "0.0.0.0:4000" mtu: 1280 peers: @@ -27,6 +27,5 @@ peers: alias: "node-b" addresses: - transport: udp - addr: "10.0.0.2:4000" - priority: 1 + addr: "172.20.0.11:4000" connect_policy: auto_connect diff --git a/examples/docker-network/configs/chain/node-b.yaml b/examples/docker-network/configs/chain/node-b.yaml new file mode 100644 index 0000000..5d272ef --- /dev/null +++ b/examples/docker-network/configs/chain/node-b.yaml @@ -0,0 +1,37 @@ +# FIPS Node B configuration (chain topology: A-B-C-D-E) +# +# Identity: npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le +# FIPS address: fd8e:302c:287e:b48d:6268:122f:da76:b77 + +node: + identity: + nsec: "b102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fb0" + +tun: + enabled: true + name: fips0 + mtu: 1280 + +dns: + enabled: true + bind_addr: "127.0.0.1" + port: 53 + +transports: + udp: + bind_addr: "0.0.0.0:4000" + mtu: 1280 + +peers: + - npub: "npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m" + alias: "node-a" + addresses: + - transport: udp + addr: "172.20.0.10:4000" + connect_policy: auto_connect + - npub: "npub1cld9yay0u24davpu6c35l4vldrhzvaq66pcqtg9a0j2cnjrn9rtsxx2pe6" + alias: "node-c" + addresses: + - transport: udp + addr: "172.20.0.12:4000" + connect_policy: auto_connect diff --git a/examples/docker-network/configs/chain/node-c.yaml b/examples/docker-network/configs/chain/node-c.yaml new file mode 100644 index 0000000..e15174d --- /dev/null +++ b/examples/docker-network/configs/chain/node-c.yaml @@ -0,0 +1,37 @@ +# FIPS Node C configuration (chain topology: A-B-C-D-E) +# +# Identity: npub1cld9yay0u24davpu6c35l4vldrhzvaq66pcqtg9a0j2cnjrn9rtsxx2pe6 +# FIPS address: fdac:a221:4069:5044:c41:d550:280e:d15a + +node: + identity: + nsec: "c102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fc0" + +tun: + enabled: true + name: fips0 + mtu: 1280 + +dns: + enabled: true + bind_addr: "127.0.0.1" + port: 53 + +transports: + udp: + bind_addr: "0.0.0.0:4000" + mtu: 1280 + +peers: + - npub: "npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le" + alias: "node-b" + addresses: + - transport: udp + addr: "172.20.0.11:4000" + connect_policy: auto_connect + - npub: "npub1n9lpnv0592cc2ps6nm0ca3qls642vx7yjsv35rkxqzj2vgds52sqgpverl" + alias: "node-d" + addresses: + - transport: udp + addr: "172.20.0.13:4000" + connect_policy: auto_connect diff --git a/examples/docker-network/configs/chain/node-d.yaml b/examples/docker-network/configs/chain/node-d.yaml new file mode 100644 index 0000000..f7e76f5 --- /dev/null +++ b/examples/docker-network/configs/chain/node-d.yaml @@ -0,0 +1,37 @@ +# FIPS Node D configuration (chain topology: A-B-C-D-E) +# +# Identity: npub1n9lpnv0592cc2ps6nm0ca3qls642vx7yjsv35rkxqzj2vgds52sqgpverl +# FIPS address: fdb6:8411:a191:6d48:efc6:b8bd:63bb:1cd7 + +node: + identity: + nsec: "d102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fd0" + +tun: + enabled: true + name: fips0 + mtu: 1280 + +dns: + enabled: true + bind_addr: "127.0.0.1" + port: 53 + +transports: + udp: + bind_addr: "0.0.0.0:4000" + mtu: 1280 + +peers: + - npub: "npub1cld9yay0u24davpu6c35l4vldrhzvaq66pcqtg9a0j2cnjrn9rtsxx2pe6" + alias: "node-c" + addresses: + - transport: udp + addr: "172.20.0.12:4000" + connect_policy: auto_connect + - npub: "npub1wf8akf8lu2zdkjkmwhl75pqvven654mpv4sz2x2tprl5265mgrzq8nhak4" + alias: "node-e" + addresses: + - transport: udp + addr: "172.20.0.14:4000" + connect_policy: auto_connect diff --git a/examples/docker-network/configs/chain/node-e.yaml b/examples/docker-network/configs/chain/node-e.yaml new file mode 100644 index 0000000..82e287c --- /dev/null +++ b/examples/docker-network/configs/chain/node-e.yaml @@ -0,0 +1,31 @@ +# FIPS Node E configuration (chain topology: A-B-C-D-E) +# +# Identity: npub1wf8akf8lu2zdkjkmwhl75pqvven654mpv4sz2x2tprl5265mgrzq8nhak4 +# FIPS address: fded:7dee:d386:a546:2a43:5103:16d8:4f3c + +node: + identity: + nsec: "e102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fe0" + +tun: + enabled: true + name: fips0 + mtu: 1280 + +dns: + enabled: true + bind_addr: "127.0.0.1" + port: 53 + +transports: + udp: + bind_addr: "0.0.0.0:4000" + mtu: 1280 + +peers: + - npub: "npub1n9lpnv0592cc2ps6nm0ca3qls642vx7yjsv35rkxqzj2vgds52sqgpverl" + alias: "node-d" + addresses: + - transport: udp + addr: "172.20.0.13:4000" + connect_policy: auto_connect diff --git a/examples/docker-network/configs/mesh/node-a.yaml b/examples/docker-network/configs/mesh/node-a.yaml new file mode 100644 index 0000000..30893a8 --- /dev/null +++ b/examples/docker-network/configs/mesh/node-a.yaml @@ -0,0 +1,37 @@ +# FIPS Node A configuration (mesh topology) +# +# Identity: npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m +# FIPS address: fd69:e08d:65cc:3a6b:9c2c:2ac4:bd40:5e4b + +node: + identity: + nsec: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" + +tun: + enabled: true + name: fips0 + mtu: 1280 + +dns: + enabled: true + bind_addr: "127.0.0.1" + port: 53 + +transports: + udp: + bind_addr: "0.0.0.0:4000" + mtu: 1280 + +peers: + - npub: "npub1n9lpnv0592cc2ps6nm0ca3qls642vx7yjsv35rkxqzj2vgds52sqgpverl" + alias: "node-d" + addresses: + - transport: udp + addr: "172.20.0.13:4000" + connect_policy: auto_connect + - npub: "npub1wf8akf8lu2zdkjkmwhl75pqvven654mpv4sz2x2tprl5265mgrzq8nhak4" + alias: "node-e" + addresses: + - transport: udp + addr: "172.20.0.14:4000" + connect_policy: auto_connect diff --git a/examples/two-node-udp/fips-b.yaml b/examples/docker-network/configs/mesh/node-b.yaml similarity index 66% rename from examples/two-node-udp/fips-b.yaml rename to examples/docker-network/configs/mesh/node-b.yaml index 13ef66d..f4e25e7 100644 --- a/examples/two-node-udp/fips-b.yaml +++ b/examples/docker-network/configs/mesh/node-b.yaml @@ -1,4 +1,4 @@ -# FIPS Node B configuration +# FIPS Node B configuration (mesh topology) # # Identity: npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le # FIPS address: fd8e:302c:287e:b48d:6268:122f:da76:b77 @@ -15,18 +15,17 @@ tun: dns: enabled: true bind_addr: "127.0.0.1" - port: 5354 + port: 53 transports: udp: - bind_addr: "10.0.0.2:4000" + bind_addr: "0.0.0.0:4000" mtu: 1280 peers: - - npub: "npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m" - alias: "node-a" + - npub: "npub1cld9yay0u24davpu6c35l4vldrhzvaq66pcqtg9a0j2cnjrn9rtsxx2pe6" + alias: "node-c" addresses: - transport: udp - addr: "10.0.0.1:4000" - priority: 1 + addr: "172.20.0.12:4000" connect_policy: auto_connect diff --git a/examples/docker-network/configs/mesh/node-c.yaml b/examples/docker-network/configs/mesh/node-c.yaml new file mode 100644 index 0000000..bd03d9e --- /dev/null +++ b/examples/docker-network/configs/mesh/node-c.yaml @@ -0,0 +1,43 @@ +# FIPS Node C configuration (mesh topology) +# +# Identity: npub1cld9yay0u24davpu6c35l4vldrhzvaq66pcqtg9a0j2cnjrn9rtsxx2pe6 +# FIPS address: fdac:a221:4069:5044:c41:d550:280e:d15a + +node: + identity: + nsec: "c102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fc0" + +tun: + enabled: true + name: fips0 + mtu: 1280 + +dns: + enabled: true + bind_addr: "127.0.0.1" + port: 53 + +transports: + udp: + bind_addr: "0.0.0.0:4000" + mtu: 1280 + +peers: + - npub: "npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le" + alias: "node-b" + addresses: + - transport: udp + addr: "172.20.0.11:4000" + connect_policy: auto_connect + - npub: "npub1n9lpnv0592cc2ps6nm0ca3qls642vx7yjsv35rkxqzj2vgds52sqgpverl" + alias: "node-d" + addresses: + - transport: udp + addr: "172.20.0.13:4000" + connect_policy: auto_connect + - npub: "npub1wf8akf8lu2zdkjkmwhl75pqvven654mpv4sz2x2tprl5265mgrzq8nhak4" + alias: "node-e" + addresses: + - transport: udp + addr: "172.20.0.14:4000" + connect_policy: auto_connect diff --git a/examples/docker-network/configs/mesh/node-d.yaml b/examples/docker-network/configs/mesh/node-d.yaml new file mode 100644 index 0000000..57f2eb0 --- /dev/null +++ b/examples/docker-network/configs/mesh/node-d.yaml @@ -0,0 +1,43 @@ +# FIPS Node D configuration (mesh topology) +# +# Identity: npub1n9lpnv0592cc2ps6nm0ca3qls642vx7yjsv35rkxqzj2vgds52sqgpverl +# FIPS address: fdb6:8411:a191:6d48:efc6:b8bd:63bb:1cd7 + +node: + identity: + nsec: "d102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fd0" + +tun: + enabled: true + name: fips0 + mtu: 1280 + +dns: + enabled: true + bind_addr: "127.0.0.1" + port: 53 + +transports: + udp: + bind_addr: "0.0.0.0:4000" + mtu: 1280 + +peers: + - npub: "npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m" + alias: "node-a" + addresses: + - transport: udp + addr: "172.20.0.10:4000" + connect_policy: auto_connect + - npub: "npub1cld9yay0u24davpu6c35l4vldrhzvaq66pcqtg9a0j2cnjrn9rtsxx2pe6" + alias: "node-c" + addresses: + - transport: udp + addr: "172.20.0.12:4000" + connect_policy: auto_connect + - npub: "npub1wf8akf8lu2zdkjkmwhl75pqvven654mpv4sz2x2tprl5265mgrzq8nhak4" + alias: "node-e" + addresses: + - transport: udp + addr: "172.20.0.14:4000" + connect_policy: auto_connect diff --git a/examples/docker-network/configs/mesh/node-e.yaml b/examples/docker-network/configs/mesh/node-e.yaml new file mode 100644 index 0000000..33494e6 --- /dev/null +++ b/examples/docker-network/configs/mesh/node-e.yaml @@ -0,0 +1,43 @@ +# FIPS Node E configuration (mesh topology) +# +# Identity: npub1wf8akf8lu2zdkjkmwhl75pqvven654mpv4sz2x2tprl5265mgrzq8nhak4 +# FIPS address: fded:7dee:d386:a546:2a43:5103:16d8:4f3c + +node: + identity: + nsec: "e102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fe0" + +tun: + enabled: true + name: fips0 + mtu: 1280 + +dns: + enabled: true + bind_addr: "127.0.0.1" + port: 53 + +transports: + udp: + bind_addr: "0.0.0.0:4000" + mtu: 1280 + +peers: + - npub: "npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m" + alias: "node-a" + addresses: + - transport: udp + addr: "172.20.0.10:4000" + connect_policy: auto_connect + - npub: "npub1cld9yay0u24davpu6c35l4vldrhzvaq66pcqtg9a0j2cnjrn9rtsxx2pe6" + alias: "node-c" + addresses: + - transport: udp + addr: "172.20.0.12:4000" + connect_policy: auto_connect + - npub: "npub1n9lpnv0592cc2ps6nm0ca3qls642vx7yjsv35rkxqzj2vgds52sqgpverl" + alias: "node-d" + addresses: + - transport: udp + addr: "172.20.0.13:4000" + connect_policy: auto_connect diff --git a/examples/docker-network/docker-chain-topology.svg b/examples/docker-network/docker-chain-topology.svg new file mode 100644 index 0000000..870d9ca --- /dev/null +++ b/examples/docker-network/docker-chain-topology.svg @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + Docker Chain Topology + Linear chain — A–B–C–D–E, multi-hop routing via discovery + + + + 172.20.0.0/24 (docker: fips-net) + + + + + + + + UDP + + + + UDP + + + + UDP + + + + UDP + + + + + A → E (4 hops, discovery-routed) + + + + + + + A + .10 + fd69:e08d:... + + + + B + .11 + fd8e:302c:... + + + + C + .12 + fdac:a221:... + + + + D + .13 + fdb6:8411:... + + + + E + .14 + fded:7dee:... + + + + + + Direct UDP peer link (port 4000, auto_connect) + + + Multi-hop path (discovery + session establishment) + + Routing flow: TUN packet → identity cache → initiate_session() + → find_next_hop() fails → discovery flood → route_cache populated + → session retry → Noise IK handshake → encrypted data delivery + diff --git a/examples/docker-network/docker-compose.yml b/examples/docker-network/docker-compose.yml new file mode 100644 index 0000000..9c04f0b --- /dev/null +++ b/examples/docker-network/docker-compose.yml @@ -0,0 +1,142 @@ +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" + 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 + - ./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 + - ./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 + - ./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 + - ./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 + - ./configs/mesh/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 + - ./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 + - ./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 + - ./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 + - ./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 + - ./configs/chain/node-e.yaml:/etc/fips/fips.yaml:ro + networks: + fips-net: + ipv4_address: 172.20.0.14 diff --git a/examples/docker-network/docker-mesh-topology.svg b/examples/docker-network/docker-mesh-topology.svg new file mode 100644 index 0000000..46d0eac --- /dev/null +++ b/examples/docker-network/docker-mesh-topology.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + Docker Mesh Topology + Sparse mesh — 5 nodes, 6 bidirectional UDP links + + + + 172.20.0.0/24 (docker: fips-net) + + + + + + + + + + + + + + + + + + + + + + + + + A + .10 + npub1sjlh2c3... + root + + + + B + .11 + npub1tdwa4vj... + + + + C + .12 + npub1cld9yay... + + + + D + .13 + npub1n9lpnv0... + + + + E + .14 + npub1wf8akf8... + + + + All-pairs ping test: 20 directed paths (direct + multi-hop via discovery) + + + + + + + UDP peer link (port 4000, auto_connect) + + + + Spanning tree edge (arrow toward parent/root) + + + Tree root (lowest NodeAddr = SHA-256(pubkey)[:16]) + + Each container: CAP_NET_ADMIN, /dev/net/tun, DNS on 127.0.0.1:53 + diff --git a/examples/docker-network/resolv.conf b/examples/docker-network/resolv.conf new file mode 100644 index 0000000..bbc8559 --- /dev/null +++ b/examples/docker-network/resolv.conf @@ -0,0 +1 @@ +nameserver 127.0.0.1 diff --git a/examples/docker-network/scripts/build.sh b/examples/docker-network/scripts/build.sh new file mode 100755 index 0000000..d29175d --- /dev/null +++ b/examples/docker-network/scripts/build.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# Build the FIPS binary and copy it to the docker build context. +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +DOCKER_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +# 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: /examples/docker-network/scripts/build.sh" >&2 + exit 1 +fi + +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" + +echo "Done. Binary at $DOCKER_DIR/fips" +echo "" +echo "Next: cd $DOCKER_DIR && docker compose --profile mesh build" diff --git a/examples/docker-network/scripts/ping-test.sh b/examples/docker-network/scripts/ping-test.sh new file mode 100755 index 0000000..a81a309 --- /dev/null +++ b/examples/docker-network/scripts/ping-test.sh @@ -0,0 +1,102 @@ +#!/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 + +PROFILE="${1:-mesh}" +COUNT=3 +TIMEOUT=10 +PASSED=0 +FAILED=0 + +# Node identities +NPUB_A="npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m" +NPUB_B="npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le" +NPUB_C="npub1cld9yay0u24davpu6c35l4vldrhzvaq66pcqtg9a0j2cnjrn9rtsxx2pe6" +NPUB_D="npub1n9lpnv0592cc2ps6nm0ca3qls642vx7yjsv35rkxqzj2vgds52sqgpverl" +NPUB_E="npub1wf8akf8lu2zdkjkmwhl75pqvven654mpv4sz2x2tprl5265mgrzq8nhak4" + +ping_test() { + local from="$1" + local to_npub="$2" + local label="$3" + + echo -n " $label ... " + if docker exec "fips-$from" ping6 -c "$COUNT" -W "$TIMEOUT" "${to_npub}.fips" > /dev/null 2>&1; then + echo "OK" + PASSED=$((PASSED + 1)) + else + echo "FAIL" + FAILED=$((FAILED + 1)) + fi +} + +echo "=== FIPS Ping Test ($PROFILE topology) ===" +echo "" + +# Wait for nodes to converge +echo "Waiting 5s for mesh convergence..." +sleep 5 + +if [ "$PROFILE" = "mesh" ]; 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 diff --git a/examples/two-node-udp/README.md b/examples/two-node-udp/README.md deleted file mode 100644 index 6996164..0000000 --- a/examples/two-node-udp/README.md +++ /dev/null @@ -1,258 +0,0 @@ -# Two-Node UDP Test - -This example demonstrates two FIPS nodes communicating over UDP using Linux -network namespaces. Both nodes establish an encrypted peer link, build a -spanning tree, and create end-to-end sessions. With TUN and DNS enabled, you -can `ping6` between nodes using raw IPv6 addresses. - -## Network Diagram - -![Two-Node UDP](two-node-udp.svg) - -## Prerequisites - -- Linux with network namespace support (requires root for namespace setup) -- IPv6 enabled (`sysctl net.ipv6.conf.all.disable_ipv6` should be `0`) -- `iproute2` tools (`ip`), `dig` (from `dnsutils` or `bind-utils`) -- Rust toolchain (to build the FIPS binary) - -## Node Identities - -| Node | npub | FIPS Address | -|------|------|-------------| -| A | `npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m` | `fd69:e08d:65cc:3a6b:9c2c:2ac4:bd40:5e4b` | -| B | `npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le` | `fd8e:302c:287e:b48d:6268:122f:da76:b77` | - -## Step 1: Build FIPS - -From the FIPS source root: - -```bash -cargo build -``` - -The binary will be at `target/debug/fips`. Note the absolute path — you'll -need it when running inside namespaces. - -## Step 2: Create Network Namespaces - -This creates two namespaces (`fips-a` and `fips-b`) connected by a virtual -ethernet pair. Each namespace has its own isolated network stack, including -its own routing table, TUN devices, and DNS resolver. - -```bash -# Create namespaces -sudo ip netns add fips-a -sudo ip netns add fips-b - -# Create a veth pair connecting them -sudo ip link add veth-a type veth peer name veth-b - -# Move each end into its namespace -sudo ip link set veth-a netns fips-a -sudo ip link set veth-b netns fips-b - -# Configure IPv4 addresses (used by UDP transport) -sudo ip netns exec fips-a ip addr add 10.0.0.1/24 dev veth-a -sudo ip netns exec fips-b ip addr add 10.0.0.2/24 dev veth-b - -# Bring interfaces up -sudo ip netns exec fips-a ip link set veth-a up -sudo ip netns exec fips-b ip link set veth-b up - -# Enable loopback in both (needed for DNS responder on 127.0.0.1) -sudo ip netns exec fips-a ip link set lo up -sudo ip netns exec fips-b ip link set lo up - -# Enable IPv6 in both namespaces -sudo ip netns exec fips-a sysctl -w net.ipv6.conf.all.disable_ipv6=0 -sudo ip netns exec fips-b sysctl -w net.ipv6.conf.all.disable_ipv6=0 -``` - -Verify connectivity between namespaces: - -```bash -sudo ip netns exec fips-a ping -c 1 10.0.0.2 -``` - -## Step 3: Start Node A - -Open **Terminal 1**. This runs the FIPS daemon for Node A inside its -namespace. The daemon creates a `fips0` TUN device, starts the DNS responder, -connects to Node B over UDP, and begins the Noise IK handshake. - -```bash -sudo ip netns exec fips-a \ - env RUST_LOG=info \ - /path/to/target/debug/fips --config /path/to/examples/two-node-udp/fips-a.yaml -``` - -Replace `/path/to/` with the actual absolute paths to your build and this -example directory. - -You should see output like: - -```text -INFO fips: FIPS starting -INFO fips: Node created: -INFO fips: npub: npub1sjlh2c3x9w7kjsqg2ay080n2lff2uvt325vpan33ke34rn8l5jcqawh57m -INFO fips: address: fd69:e08d:65cc:3a6b:9c2c:2ac4:bd40:5e4b -INFO fips: TUN device active: -INFO fips: name: fips0 -INFO fips: address: fd69:e08d:65cc:3a6b:9c2c:2ac4:bd40:5e4b -INFO fips: DNS responder started for .fips domain -INFO fips: Peer connection initiated (node-b) -``` - -## Step 4: Start Node B - -Open **Terminal 2**. Run Node B in its namespace: - -```bash -sudo ip netns exec fips-b \ - env RUST_LOG=info \ - /path/to/target/debug/fips --config /path/to/examples/two-node-udp/fips-b.yaml -``` - -Once both nodes are running, you should see handshake completion messages in -both terminals: - -```text -INFO fips::node::handlers::handshake: Peer promoted to active -``` - -The spanning tree will converge within a few seconds (TreeAnnounce exchange), -followed by bloom filter exchange (FilterAnnounce). - -## Step 5: Test DNS Resolution - -The FIPS daemon includes a DNS responder that resolves `.fips` queries -to FIPS IPv6 addresses. Open **Terminal 3** to test it. - -Query the DNS responder directly with `dig`: - -```bash -# From Node A, resolve Node B's name -sudo ip netns exec fips-a dig @127.0.0.1 -p 5354 AAAA \ - npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le.fips - -# Expected answer: fd8e:302c:287e:b48d:6268:122f:da76:b77 -``` - -Watch Terminal 1 — you should see a log line (requires `RUST_LOG=debug`): - -```text -DEBUG fips::dns: DNS resolved .fips name, registering identity -``` - -This confirms the identity cache was populated. The subsequent ping will be -able to route through the mesh. - -> **Note:** `resolvectl` and system resolver integration (e.g., `ping6 -> npub1...fips`) do not work inside network namespaces because -> `systemd-resolved` runs in the host namespace and is not accessible from -> within isolated namespaces. Use `dig @127.0.0.1 -p 5354` to query the -> DNS responder directly, and use raw IPv6 addresses for `ping6`. System -> resolver integration works when FIPS runs in the host namespace or with -> the future D-Bus auto-registration (Phase 2). - -## Step 6: Ping Between Nodes - -Now test end-to-end connectivity. The first ping triggers session -establishment (Noise IK handshake through the mesh), so it may take a moment -longer than subsequent pings. - -### Ping Node B from Node A - -From Terminal 3: - -```bash -sudo ip netns exec fips-a ping6 -c 4 fd8e:302c:287e:b48d:6268:122f:da76:b77 -``` - -### Ping Node A from Node B - -```bash -sudo ip netns exec fips-b ping6 -c 4 fd69:e08d:65cc:3a6b:9c2c:2ac4:bd40:5e4b -``` - -Ping replies are handled by the kernel's IPv6 stack — when a ping arrives -at the destination's TUN device, the kernel sees it addressed to its own -`fips0` address and replies natively. FIPS handles the encrypted transport -between nodes; the kernel handles ICMPv6 Echo Reply. - -## Step 7: Watch the Logs - -While pinging, watch the daemon terminals for the protocol flow: - -1. **DNS resolution** — `DNS resolved .fips name, registering identity` -2. **TUN packet** — `TUN packet received` with src/dst addresses -3. **Session initiation** — `Initiating session to ` -4. **SessionSetup sent** — Noise IK msg1 sent through mesh -5. **SessionSetup received** — Responder processes msg1 -6. **SessionAck** — Responder sends msg2 back -7. **Session established** — Both sides transition to Established -8. **DataPacket** — Encrypted IPv6 payload delivered - -Set `RUST_LOG=debug` for the full protocol trace, or `RUST_LOG=info` for -high-level events only. - -## Cleanup - -Stop both FIPS daemons with Ctrl+C in Terminals 1 and 2. Then tear down -the namespaces: - -```bash -sudo ip netns delete fips-a -sudo ip netns delete fips-b -``` - -This also removes the veth pair and TUN devices automatically. - -## Troubleshooting - -### "Permission denied" creating TUN device - -The FIPS binary needs `CAP_NET_ADMIN` to create TUN devices. Running via -`sudo ip netns exec` already provides root privileges. If running outside -a namespace, use: - -```bash -sudo setcap cap_net_admin+ep /path/to/target/debug/fips -``` - -### "Address already in use" on DNS port - -Another process is using port 5354. Change the `dns.port` in the YAML config -to a different port (e.g., 5355). - -### No handshake completion - -Check that the veth pair is up and the namespaces can reach each other: - -```bash -sudo ip netns exec fips-a ping -c 1 10.0.0.2 -``` - -If this fails, the namespace setup is incomplete. - -### IPv6 disabled - -```bash -sudo ip netns exec fips-a sysctl net.ipv6.conf.all.disable_ipv6 -# Should be 0 -``` - -### DNS query returns no answer - -Verify the DNS responder is running by querying it directly: - -```bash -sudo ip netns exec fips-a dig @127.0.0.1 -p 5354 AAAA \ - npub1tdwa4vjrjl33pcjdpf2t4p027nl86xrx24g4d3avg4vwvayr3g8qhd84le.fips -``` - -If `dig` works but `resolvectl query` or `ping6 .fips` doesn't, -this is expected — `systemd-resolved` runs in the host namespace and is -not accessible from inside network namespaces. Use raw IPv6 addresses -for testing within namespaces. diff --git a/examples/two-node-udp/two-node-udp.svg b/examples/two-node-udp/two-node-udp.svg deleted file mode 100644 index d72a3e8..0000000 --- a/examples/two-node-udp/two-node-udp.svg +++ /dev/null @@ -1,110 +0,0 @@ - - - - - - - - Two-Node UDP Example - Network namespaces with veth pair transport - - - - Namespace: fips-a - - - - FIPS Node A - fd69:e08d:65cc:3a6b:9c2c:2ac4:bd40:5e4b - npub1sjlh2c...awh57m - - - - fips0 - TUN device - - - - DNS - 127.0.0.1:5354 - - - - - - - - veth-a - 10.0.0.1/24 - - - - - - - Namespace: fips-b - - - - FIPS Node B - fd8e:302c:287e:b48d:6268:122f:da76:0b77 - npub1tdwa4v...d84le - - - - fips0 - TUN device - - - - DNS - 127.0.0.1:5354 - - - - - - - - veth-b - 10.0.0.2/24 - - - - - - - UDP :4000 - - - - Encrypted session - - - - - - Transport layer: UDP over IPv4 veth pair - - - Data plane: IPv6 over FIPS mesh (fd::/8 via fips0 TUN) - - - DNS: <npub>.fips → FIPS IPv6 address - diff --git a/src/node/mod.rs b/src/node/mod.rs index 2bc5f1b..6c41a4a 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -806,16 +806,13 @@ impl Node { // === End-to-End Sessions === /// Get a session by remote NodeAddr. + #[cfg(test)] pub(crate) fn get_session(&self, remote: &NodeAddr) -> Option<&SessionEntry> { self.sessions.get(remote) } - /// Get a mutable session by remote NodeAddr. - pub(crate) fn get_session_mut(&mut self, remote: &NodeAddr) -> Option<&mut SessionEntry> { - self.sessions.get_mut(remote) - } - /// Remove a session. + #[cfg(test)] pub(crate) fn remove_session(&mut self, remote: &NodeAddr) -> Option { self.sessions.remove(remote) } diff --git a/src/node/session.rs b/src/node/session.rs index bf93638..6d50290 100644 --- a/src/node/session.rs +++ b/src/node/session.rs @@ -42,12 +42,15 @@ impl EndToEndState { /// The state is `None` only transiently during handler processing. pub(crate) struct SessionEntry { /// Remote node's address (session table key). + #[allow(dead_code)] remote_addr: NodeAddr, /// Remote node's static public key (for Noise IK). + #[allow(dead_code)] remote_pubkey: PublicKey, /// Current session state. `None` only during state transitions. state: Option, /// When the session was created (Unix milliseconds). + #[cfg_attr(not(test), allow(dead_code))] created_at: u64, /// Last activity timestamp (Unix milliseconds). last_activity: u64, @@ -70,16 +73,6 @@ impl SessionEntry { } } - /// Get the remote node address. - pub(crate) fn remote_addr(&self) -> &NodeAddr { - &self.remote_addr - } - - /// Get the remote node's public key. - pub(crate) fn remote_pubkey(&self) -> &PublicKey { - &self.remote_pubkey - } - /// Get the current session state. pub(crate) fn state(&self) -> &EndToEndState { self.state.as_ref().expect("session state taken but not restored") @@ -114,11 +107,13 @@ impl SessionEntry { } /// Get creation time. + #[cfg(test)] pub(crate) fn created_at(&self) -> u64 { self.created_at } /// Get last activity time. + #[cfg(test)] pub(crate) fn last_activity(&self) -> u64 { self.last_activity }