mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
Merge branch 'maint'
Carries today's maint batch: the local CI image-scoping work, which gives each run its own build context and test image tag instead of writing the shared mutable one, and the retirement of the bloom-storm chaos scenario. Both apply unchanged here — the compose files, suite lists and matrix legs they touch are identical on the two lines, so no branch adaptation was needed. Parity stays symmetric across runners at 21 legs a side on this branch and 24 on maint, the gap being the three rekey Docker suites maint keeps by design.
This commit is contained in:
@@ -65,6 +65,8 @@ jobs:
|
|||||||
run: python3 testing/check-log-strings.py
|
run: python3 testing/check-log-strings.py
|
||||||
- name: Check no tested function's exit status is a log call's
|
- name: Check no tested function's exit status is a log call's
|
||||||
run: python3 testing/check-trailing-log.py
|
run: python3 testing/check-trailing-log.py
|
||||||
|
- name: Check nothing resolves the shared mutable test image
|
||||||
|
run: bash testing/check-image-scoping.sh
|
||||||
# Hermetic: synthetic ping functions, no containers, ~45s. Lives beside
|
# Hermetic: synthetic ping functions, no containers, ~45s. Lives beside
|
||||||
# the other two so both runners gate on it identically — putting it in
|
# the other two so both runners gate on it identically — putting it in
|
||||||
# only one would create exactly the drift check-ci-parity.sh exists to
|
# only one would create exactly the drift check-ci-parity.sh exists to
|
||||||
@@ -438,9 +440,6 @@ jobs:
|
|||||||
- suite: congestion-stress
|
- suite: congestion-stress
|
||||||
type: chaos
|
type: chaos
|
||||||
scenario: congestion-stress
|
scenario: congestion-stress
|
||||||
- suite: bloom-storm
|
|
||||||
type: chaos
|
|
||||||
scenario: bloom-storm
|
|
||||||
# ── Sidecar deployment ──────────────────────────────────────────
|
# ── Sidecar deployment ──────────────────────────────────────────
|
||||||
- suite: sidecar
|
- suite: sidecar
|
||||||
type: sidecar
|
type: sidecar
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ __pycache__/
|
|||||||
*.egg-info/
|
*.egg-info/
|
||||||
*.egg
|
*.egg
|
||||||
|
|
||||||
|
# Per-run build contexts created by testing/ci-local.sh. Its teardown normally
|
||||||
|
# removes them, but the CI worker's SIGKILL runs no trap, so one can survive a
|
||||||
|
# preempted run; ci-cleanup.sh sweeps the survivors.
|
||||||
|
/testing/docker-*/
|
||||||
|
|
||||||
# Runtime artifacts from running fips in-tree during local testing.
|
# Runtime artifacts from running fips in-tree during local testing.
|
||||||
# Root-anchored so legitimately-tracked fips.yaml under packaging/ and
|
# Root-anchored so legitimately-tracked fips.yaml under packaging/ and
|
||||||
# examples/ stays included.
|
# examples/ stays included.
|
||||||
|
|||||||
@@ -1,214 +0,0 @@
|
|||||||
//! Ethernet transport integration tests.
|
|
||||||
//!
|
|
||||||
//! Tests that the Ethernet transport works end-to-end using veth pairs.
|
|
||||||
//! All tests require root or CAP_NET_RAW and are marked `#[ignore]`.
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::config::EthernetConfig;
|
|
||||||
use crate::transport::ethernet::EthernetTransport;
|
|
||||||
use crate::transport::{TransportAddr, TransportHandle, TransportId, packet_channel};
|
|
||||||
use spanning_tree::{TestNode, cleanup_nodes, drain_all_packets, initiate_handshake};
|
|
||||||
|
|
||||||
use std::process::Command;
|
|
||||||
use std::sync::atomic::{AtomicU32, Ordering};
|
|
||||||
|
|
||||||
/// Atomic counter for unique veth names across tests.
|
|
||||||
static VETH_COUNTER: AtomicU32 = AtomicU32::new(0);
|
|
||||||
|
|
||||||
/// RAII wrapper for a veth pair.
|
|
||||||
///
|
|
||||||
/// Creates a pair of connected virtual Ethernet interfaces. Destroying
|
|
||||||
/// one end automatically destroys the other.
|
|
||||||
struct VethPair {
|
|
||||||
name_a: String,
|
|
||||||
name_b: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VethPair {
|
|
||||||
/// Create a new veth pair with unique interface names.
|
|
||||||
///
|
|
||||||
/// Names are kept under 15 chars (IFNAMSIZ limit). Format: `ftXXa`/`ftXXb`
|
|
||||||
/// where XX is an atomic counter combined with PID for cross-process uniqueness.
|
|
||||||
fn create() -> Self {
|
|
||||||
let id = VETH_COUNTER.fetch_add(1, Ordering::Relaxed);
|
|
||||||
let pid = std::process::id() % 10000;
|
|
||||||
let name_a = format!("ft{}{}a", pid, id);
|
|
||||||
let name_b = format!("ft{}{}b", pid, id);
|
|
||||||
|
|
||||||
assert!(name_a.len() <= 15, "veth name too long: {}", name_a);
|
|
||||||
assert!(name_b.len() <= 15, "veth name too long: {}", name_b);
|
|
||||||
|
|
||||||
// Create veth pair
|
|
||||||
let status = Command::new("ip")
|
|
||||||
.args([
|
|
||||||
"link", "add", &name_a, "type", "veth", "peer", "name", &name_b,
|
|
||||||
])
|
|
||||||
.status()
|
|
||||||
.expect("failed to run 'ip link add'");
|
|
||||||
assert!(status.success(), "failed to create veth pair");
|
|
||||||
|
|
||||||
// Bring both ends up
|
|
||||||
let status = Command::new("ip")
|
|
||||||
.args(["link", "set", &name_a, "up"])
|
|
||||||
.status()
|
|
||||||
.expect("failed to run 'ip link set up'");
|
|
||||||
assert!(status.success(), "failed to bring up {}", name_a);
|
|
||||||
|
|
||||||
let status = Command::new("ip")
|
|
||||||
.args(["link", "set", &name_b, "up"])
|
|
||||||
.status()
|
|
||||||
.expect("failed to run 'ip link set up'");
|
|
||||||
assert!(status.success(), "failed to bring up {}", name_b);
|
|
||||||
|
|
||||||
VethPair { name_a, name_b }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for VethPair {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
// Deleting one end destroys both
|
|
||||||
let _ = Command::new("ip")
|
|
||||||
.args(["link", "delete", &self.name_a])
|
|
||||||
.status();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a test node with a live Ethernet transport on the given interface.
|
|
||||||
///
|
|
||||||
/// Parallel to `make_test_node()` in spanning_tree.rs but uses
|
|
||||||
/// EthernetTransport instead of UDP.
|
|
||||||
async fn make_test_node_ethernet(interface: &str) -> TestNode {
|
|
||||||
let mut node = make_node();
|
|
||||||
let transport_id = TransportId::new(1);
|
|
||||||
|
|
||||||
let config = EthernetConfig {
|
|
||||||
interface: interface.to_string(),
|
|
||||||
listen: Some(false),
|
|
||||||
announce: Some(false),
|
|
||||||
accept_connections: Some(true),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let (packet_tx, packet_rx) = packet_channel(256);
|
|
||||||
let mut transport = EthernetTransport::new(transport_id, None, config, packet_tx);
|
|
||||||
transport.start_async().await.unwrap();
|
|
||||||
|
|
||||||
let mac = transport
|
|
||||||
.local_mac()
|
|
||||||
.expect("transport should have MAC after start");
|
|
||||||
let addr = TransportAddr::from_bytes(&mac);
|
|
||||||
|
|
||||||
node.transports
|
|
||||||
.insert(transport_id, TransportHandle::Ethernet(transport));
|
|
||||||
|
|
||||||
TestNode {
|
|
||||||
node,
|
|
||||||
transport_id,
|
|
||||||
packet_rx: spanning_tree::bridge_to_unbounded(packet_rx),
|
|
||||||
addr,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Two nodes on a veth pair complete a Noise handshake and establish peering.
|
|
||||||
#[tokio::test]
|
|
||||||
#[ignore] // Requires root or CAP_NET_RAW
|
|
||||||
async fn test_ethernet_two_node_handshake() {
|
|
||||||
let veth = VethPair::create();
|
|
||||||
|
|
||||||
let mut nodes = vec![
|
|
||||||
make_test_node_ethernet(&veth.name_a).await,
|
|
||||||
make_test_node_ethernet(&veth.name_b).await,
|
|
||||||
];
|
|
||||||
|
|
||||||
// Initiate handshake from node 0 to node 1
|
|
||||||
initiate_handshake(&mut nodes, 0, 1).await;
|
|
||||||
|
|
||||||
// Drain all packets (handshake + tree announce)
|
|
||||||
let total = drain_all_packets(&mut nodes, false).await;
|
|
||||||
assert!(total > 0, "should have processed packets");
|
|
||||||
|
|
||||||
// Verify bidirectional peering
|
|
||||||
let addr_0 = *nodes[0].node.node_addr();
|
|
||||||
let addr_1 = *nodes[1].node.node_addr();
|
|
||||||
assert!(
|
|
||||||
nodes[0].node.get_peer(&addr_1).is_some(),
|
|
||||||
"node 0 should have node 1 as peer"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
nodes[1].node.get_peer(&addr_0).is_some(),
|
|
||||||
"node 1 should have node 0 as peer"
|
|
||||||
);
|
|
||||||
|
|
||||||
cleanup_nodes(&mut nodes).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Two Ethernet nodes converge to a correct spanning tree (2-node tree).
|
|
||||||
#[tokio::test]
|
|
||||||
#[ignore] // Requires root or CAP_NET_RAW
|
|
||||||
async fn test_ethernet_data_exchange() {
|
|
||||||
use spanning_tree::verify_tree_convergence;
|
|
||||||
|
|
||||||
let veth = VethPair::create();
|
|
||||||
|
|
||||||
let mut nodes = vec![
|
|
||||||
make_test_node_ethernet(&veth.name_a).await,
|
|
||||||
make_test_node_ethernet(&veth.name_b).await,
|
|
||||||
];
|
|
||||||
|
|
||||||
initiate_handshake(&mut nodes, 0, 1).await;
|
|
||||||
let total = drain_all_packets(&mut nodes, false).await;
|
|
||||||
assert!(total > 0);
|
|
||||||
|
|
||||||
// Verify spanning tree convergence
|
|
||||||
verify_tree_convergence(&nodes);
|
|
||||||
|
|
||||||
// The root should be the node with the smallest NodeAddr
|
|
||||||
let expected_root = std::cmp::min(*nodes[0].node.node_addr(), *nodes[1].node.node_addr());
|
|
||||||
assert_eq!(*nodes[0].node.tree_state().root(), expected_root);
|
|
||||||
assert_eq!(*nodes[1].node.tree_state().root(), expected_root);
|
|
||||||
|
|
||||||
cleanup_nodes(&mut nodes).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mixed transport: 2 Ethernet nodes + 2 UDP nodes coexist.
|
|
||||||
///
|
|
||||||
/// Each transport forms its own connected component. Validates that
|
|
||||||
/// `process_available_packets()` handles heterogeneous transport types.
|
|
||||||
#[tokio::test]
|
|
||||||
#[ignore] // Requires root or CAP_NET_RAW
|
|
||||||
async fn test_mixed_transport_coexistence() {
|
|
||||||
use spanning_tree::{make_test_node, verify_tree_convergence_components};
|
|
||||||
|
|
||||||
let veth = VethPair::create();
|
|
||||||
|
|
||||||
// Create 2 Ethernet nodes and 2 UDP nodes
|
|
||||||
let eth_0 = make_test_node_ethernet(&veth.name_a).await;
|
|
||||||
let eth_1 = make_test_node_ethernet(&veth.name_b).await;
|
|
||||||
let udp_0 = make_test_node().await;
|
|
||||||
let udp_1 = make_test_node().await;
|
|
||||||
|
|
||||||
let mut nodes = vec![eth_0, eth_1, udp_0, udp_1];
|
|
||||||
|
|
||||||
// Handshake within each component
|
|
||||||
initiate_handshake(&mut nodes, 0, 1).await; // Ethernet pair
|
|
||||||
initiate_handshake(&mut nodes, 2, 3).await; // UDP pair
|
|
||||||
|
|
||||||
// Drain all packets across both transports
|
|
||||||
let total = drain_all_packets(&mut nodes, false).await;
|
|
||||||
assert!(total > 0);
|
|
||||||
|
|
||||||
// Verify each component converges independently
|
|
||||||
verify_tree_convergence_components(&nodes, &[vec![0, 1], vec![2, 3]]);
|
|
||||||
|
|
||||||
// Ethernet component has its own root
|
|
||||||
let eth_root = std::cmp::min(*nodes[0].node.node_addr(), *nodes[1].node.node_addr());
|
|
||||||
assert_eq!(*nodes[0].node.tree_state().root(), eth_root);
|
|
||||||
assert_eq!(*nodes[1].node.tree_state().root(), eth_root);
|
|
||||||
|
|
||||||
// UDP component has its own root
|
|
||||||
let udp_root = std::cmp::min(*nodes[2].node.node_addr(), *nodes[3].node.node_addr());
|
|
||||||
assert_eq!(*nodes[2].node.tree_state().root(), udp_root);
|
|
||||||
assert_eq!(*nodes[3].node.tree_state().root(), udp_root);
|
|
||||||
|
|
||||||
cleanup_nodes(&mut nodes).await;
|
|
||||||
}
|
|
||||||
@@ -15,8 +15,6 @@ mod decrypt_failure;
|
|||||||
mod disconnect;
|
mod disconnect;
|
||||||
mod discovery;
|
mod discovery;
|
||||||
mod establish_chartests;
|
mod establish_chartests;
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
mod ethernet;
|
|
||||||
mod forwarding;
|
mod forwarding;
|
||||||
mod handshake;
|
mod handshake;
|
||||||
mod heartbeat;
|
mod heartbeat;
|
||||||
|
|||||||
+15
-4
@@ -16,8 +16,6 @@ configurations.
|
|||||||
| ----------- | ----- | --------- | -------------------------------- |
|
| ----------- | ----- | --------- | -------------------------------- |
|
||||||
| mesh | 5 | UDP | Sparse mesh, 6 links, multi-hop |
|
| mesh | 5 | UDP | Sparse mesh, 6 links, multi-hop |
|
||||||
| chain | 5 | UDP | Linear chain, max 4-hop paths |
|
| chain | 5 | UDP | Linear chain, max 4-hop paths |
|
||||||
| mesh-public | 5+1 | UDP | Mesh with external public node |
|
|
||||||
| tcp-chain | 3 | TCP | Linear chain over TCP (port 8443) |
|
|
||||||
| rekey | 5 | UDP | Rekey integration test topology |
|
| rekey | 5 | UDP | Rekey integration test topology |
|
||||||
|
|
||||||
### [tor/](tor/) -- Tor Transport Integration
|
### [tor/](tor/) -- Tor Transport Integration
|
||||||
@@ -95,8 +93,21 @@ flight) never collide:
|
|||||||
- **Compose projects** are named `fipsci_<run-id>_<suite>`, so
|
- **Compose projects** are named `fipsci_<run-id>_<suite>`, so
|
||||||
container, network, and volume names are all prefixed per run.
|
container, network, and volume names are all prefixed per run.
|
||||||
- **Build images** are tagged `fips-test:<run-id>` and
|
- **Build images** are tagged `fips-test:<run-id>` and
|
||||||
`fips-test-app:<run-id>` (exported as `FIPS_TEST_IMAGE` /
|
`fips-test-app:<run-id>`, exported as `FIPS_TEST_IMAGE` /
|
||||||
`FIPS_TEST_APP_IMAGE` for the compose consumers).
|
`FIPS_TEST_APP_IMAGE`, and **every** compose file and suite script reads
|
||||||
|
those. The run does not write `fips-test:latest` at all: a bridge back to
|
||||||
|
that shared mutable name would let a consumer that had been missed keep
|
||||||
|
working while resolving whichever concurrent run wrote the tag last.
|
||||||
|
`:latest` stays the hand-build name, produced by
|
||||||
|
`testing/scripts/build.sh`, and remains the default every consumer falls
|
||||||
|
back to when the variables are unset.
|
||||||
|
- **The build context** is a per-run copy at `testing/docker-<run-id>/`,
|
||||||
|
exported as `FIPS_BUILD_CONTEXT`. It is absolute because compose resolves
|
||||||
|
a relative build context against the compose file's own directory rather
|
||||||
|
than the working directory. `testing/docker/` is the hand-run context and
|
||||||
|
a CI run does not write to it. Without this, two runs race on the contents
|
||||||
|
of one directory and either can build a correctly-per-run-tagged image
|
||||||
|
from the other's binaries.
|
||||||
- Each parallel chaos child gets a unique, non-overlapping `/24` in
|
- Each parallel chaos child gets a unique, non-overlapping `/24` in
|
||||||
`10.30.x` (via the sim `--subnet` override). `10.30.x` sits outside
|
`10.30.x` (via the sim `--subnet` override). `10.30.x` sits outside
|
||||||
Docker's default address pool and the fixed-subnet suites' `172.x`
|
Docker's default address pool and the fixed-subnet suites' `172.x`
|
||||||
|
|||||||
@@ -19,8 +19,12 @@ networks:
|
|||||||
|
|
||||||
x-fips-common: &fips-common
|
x-fips-common: &fips-common
|
||||||
build:
|
build:
|
||||||
context: ../docker
|
# The harness scopes its build context per run and passes it here; the
|
||||||
image: fips-test:latest
|
# shared directory is the hand-run default. Compose resolves a relative
|
||||||
|
# value against THIS file's directory, so the harness must export an
|
||||||
|
# absolute path.
|
||||||
|
context: ${FIPS_BUILD_CONTEXT:-../docker}
|
||||||
|
image: ${FIPS_TEST_IMAGE:-fips-test:latest}
|
||||||
entrypoint: ["/usr/local/bin/entrypoint.sh"]
|
entrypoint: ["/usr/local/bin/entrypoint.sh"]
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
|
|||||||
@@ -185,7 +185,15 @@ log "Generating ACL allowlist fixtures"
|
|||||||
|
|
||||||
log "Starting ACL allowlist harness"
|
log "Starting ACL allowlist harness"
|
||||||
docker compose -f "$COMPOSE_FILE" down >/dev/null 2>&1 || true
|
docker compose -f "$COMPOSE_FILE" down >/dev/null 2>&1 || true
|
||||||
docker compose -f "$COMPOSE_FILE" up -d --build
|
# --build only on the hand path. Under a harness, --skip-build means the caller
|
||||||
|
# has already built the image this compose file names, and rebuilding it here
|
||||||
|
# would overwrite that image from whatever the shared build context happens to
|
||||||
|
# hold — which is how a suite ends up certifying binaries it was never given.
|
||||||
|
if [ "$SKIP_BUILD" = false ]; then
|
||||||
|
docker compose -f "$COMPOSE_FILE" up -d --build
|
||||||
|
else
|
||||||
|
docker compose -f "$COMPOSE_FILE" up -d
|
||||||
|
fi
|
||||||
|
|
||||||
log "Waiting for expected peer convergence"
|
log "Waiting for expected peer convergence"
|
||||||
wait_for_peers_exact fips-acl-container-a${FIPS_CI_NAME_SUFFIX:-} 3 40
|
wait_for_peers_exact fips-acl-container-a${FIPS_CI_NAME_SUFFIX:-} 3 40
|
||||||
|
|||||||
@@ -67,19 +67,12 @@ Explicit topologies exercising non-UDP transports.
|
|||||||
| ------------- | ----- | -------------- | ----- | -------- | ----- | ---------- | ------------------------------------------ |
|
| ------------- | ----- | -------------- | ----- | -------- | ----- | ---------- | ------------------------------------------ |
|
||||||
| ethernet-only | 4 | Ethernet | Ring | 90s | yes | -- | AF_PACKET transport with beacon discovery |
|
| ethernet-only | 4 | Ethernet | Ring | 90s | yes | -- | AF_PACKET transport with beacon discovery |
|
||||||
| ethernet-mesh | 6 | UDP + Ethernet | Mesh | 120s | yes | yes | Mixed UDP/Ethernet, netem mutation + flaps |
|
| ethernet-mesh | 6 | UDP + Ethernet | Mesh | 120s | yes | yes | Mixed UDP/Ethernet, netem mutation + flaps |
|
||||||
| tcp-only | 4 | TCP | Ring | 90s | yes | -- | TCP transport with static peer config |
|
|
||||||
| tcp-chain | 4 | TCP | Chain | 90s | yes | -- | TCP multi-hop routing through chain |
|
|
||||||
| tcp-mesh | 6 | UDP + TCP | Mesh | 120s | yes | yes | Mixed UDP/TCP, netem mutation + flaps |
|
| tcp-mesh | 6 | UDP + TCP | Mesh | 120s | yes | yes | Mixed UDP/TCP, netem mutation + flaps |
|
||||||
|
|
||||||
- **ethernet-only**: 4-node ring on raw Ethernet (AF_PACKET). Peers discovered
|
- **ethernet-only**: 4-node ring on raw Ethernet (AF_PACKET). Peers discovered
|
||||||
via beacons, not static config. Minimal netem (1-5ms delay).
|
via beacons, not static config. Minimal netem (1-5ms delay).
|
||||||
- **ethernet-mesh**: Mirrors `tcp-mesh` topology but with Ethernet instead of
|
- **ethernet-mesh**: Mirrors `tcp-mesh` topology but with Ethernet instead of
|
||||||
TCP. UDP edges use static config; Ethernet edges use beacon discovery.
|
TCP. UDP edges use static config; Ethernet edges use beacon discovery.
|
||||||
- **tcp-only**: 4-node ring using TCP on port 8443. Tests connect-on-send,
|
|
||||||
FMP framing over TCP, and reconnection. Netem enabled (1-10ms delay, 0-1%
|
|
||||||
loss).
|
|
||||||
- **tcp-chain**: 4-node linear chain, all TCP. Tests multi-hop routing over
|
|
||||||
TCP-only mesh.
|
|
||||||
- **tcp-mesh**: 6-node mesh with 4 UDP and 3 TCP edges. Both transports use
|
- **tcp-mesh**: 6-node mesh with 4 UDP and 3 TCP edges. Both transports use
|
||||||
static peer config. Netem mutation (30% fraction, every 20-40s) and link
|
static peer config. Netem mutation (30% fraction, every 20-40s) and link
|
||||||
flaps (1 link max, 10-20s down).
|
flaps (1 link max, 10-20s down).
|
||||||
|
|||||||
@@ -75,12 +75,37 @@ bandwidth:
|
|||||||
#
|
#
|
||||||
# This one is calibrated rather than derived, because a scenario that
|
# This one is calibrated rather than derived, because a scenario that
|
||||||
# stops and starts nodes on purpose does not hold a single spanning
|
# stops and starts nodes on purpose does not hold a single spanning
|
||||||
# tree. The six provably-completed archived runs end with two or three
|
# tree.
|
||||||
# distinct roots, seven or eight of ten nodes parented, and fifteen to
|
#
|
||||||
# twenty sessions. The floors below sit one step outside those ranges
|
# Calibrated 2026-07-26 against fourteen runs that recorded a baseline
|
||||||
# so an unlucky run does not go red, which leaves them catching a mesh
|
# verdict, read from their assertions.txt files. Every one used this
|
||||||
# that collapsed rather than one that churned. Tighten them only
|
# file's fixed seed 42 and therefore an identical chaos schedule, so the
|
||||||
# against a larger sample than six runs.
|
# spread below is container timing rather than differing scenarios:
|
||||||
|
#
|
||||||
|
# distinct roots 1 2 3 4 5 -> 2 3 5 3 1 runs
|
||||||
|
# nodes parented 9 8 7 6 5 -> the exact complement, in all 14
|
||||||
|
# sessions 12 to 20, minimum 12
|
||||||
|
#
|
||||||
|
# The previous ceiling of 4 roots sat at roughly the 93rd percentile of
|
||||||
|
# that distribution: one run in fourteen exceeded it and four sat at or
|
||||||
|
# above it, so it reddened a share of runs whatever the daemon did. It
|
||||||
|
# was set from six runs whose observed maximum was 3, which is how a
|
||||||
|
# threshold one step outside a small sample ends up inside the real one.
|
||||||
|
#
|
||||||
|
# The ceiling is now 6, one step beyond the observed maximum of 5, and
|
||||||
|
# the parented floor is its complement at 4. That still fails a mesh
|
||||||
|
# that collapsed — seven or more of ten nodes islanded — which is the
|
||||||
|
# only thing this assertion was ever meant to catch. Do not read a pass
|
||||||
|
# as convergence: four of the six gating scenarios assert a floor of
|
||||||
|
# this kind, and it means the mesh formed and nobody errored.
|
||||||
|
#
|
||||||
|
# min_sessions stays at 10 against an observed minimum of 12. It has
|
||||||
|
# never fired and there is no evidence it is mis-set, but the margin is
|
||||||
|
# thin and a future failure there should be read as calibration before
|
||||||
|
# it is read as a defect.
|
||||||
|
#
|
||||||
|
# Tighten any of these only against a larger sample, and against one
|
||||||
|
# gathered at the invocation CI actually runs.
|
||||||
#
|
#
|
||||||
# READ THIS BEFORE RETUNING: the numbers above describe the invocation CI
|
# READ THIS BEFORE RETUNING: the numbers above describe the invocation CI
|
||||||
# gates on, which is not this file's own defaults. ci-local runs it as
|
# gates on, which is not this file's own defaults. ci-local runs it as
|
||||||
@@ -98,8 +123,8 @@ bandwidth:
|
|||||||
assertions:
|
assertions:
|
||||||
baseline:
|
baseline:
|
||||||
min_nodes_reporting: 10
|
min_nodes_reporting: 10
|
||||||
max_roots: 4
|
max_roots: 6
|
||||||
min_nodes_parented: 6
|
min_nodes_parented: 4
|
||||||
min_sessions: 10
|
min_sessions: 10
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
|
|||||||
@@ -50,10 +50,11 @@ traffic:
|
|||||||
# A 4-node mesh forms a spanning tree: one root and three nodes
|
# A 4-node mesh forms a spanning tree: one root and three nodes
|
||||||
# with a parent. All six provably-completed archived runs show exactly
|
# with a parent. All six provably-completed archived runs show exactly
|
||||||
# that, so these are the shape of a converged mesh rather than a
|
# that, so these are the shape of a converged mesh rather than a
|
||||||
# tolerance fitted to observations. This is currently the only
|
# tolerance fitted to observations. This is the only assertion covering
|
||||||
# assertion covering Ethernet transport anywhere in CI: the three
|
# Ethernet transport anywhere in CI, and it covers the control plane
|
||||||
# tests in src/node/tests/ethernet.rs are #[ignore]d for requiring
|
# only: traffic is disabled above, so no datagram crosses an Ethernet
|
||||||
# CAP_NET_RAW and neither runner passes --ignored.
|
# link in any test. Framing, the length field that trims NIC minimum-
|
||||||
|
# frame padding, and AEAD over Ethernet are all unexercised as a result.
|
||||||
assertions:
|
assertions:
|
||||||
baseline:
|
baseline:
|
||||||
min_nodes_reporting: 4
|
min_nodes_reporting: 4
|
||||||
|
|||||||
@@ -125,7 +125,11 @@ if ! docker info &> /dev/null; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
DOCKER_DIR="$CHAOS_DIR/../docker"
|
# A harness that scopes its build context per run passes it in
|
||||||
|
# FIPS_BUILD_CONTEXT and stops writing to the shared directory, so checking the
|
||||||
|
# shared one would either fail on a clean checkout or, worse, pass while
|
||||||
|
# reading a stale binary that is not the one under test.
|
||||||
|
DOCKER_DIR="${FIPS_BUILD_CONTEXT:-$CHAOS_DIR/../docker}"
|
||||||
if [ ! -f "$DOCKER_DIR/fips" ]; then
|
if [ ! -f "$DOCKER_DIR/fips" ]; then
|
||||||
echo "Error: FIPS binary not found at $DOCKER_DIR/fips" >&2
|
echo "Error: FIPS binary not found at $DOCKER_DIR/fips" >&2
|
||||||
echo "Run testing/scripts/build.sh first" >&2
|
echo "Run testing/scripts/build.sh first" >&2
|
||||||
|
|||||||
@@ -10,8 +10,13 @@ from .scenario import Scenario
|
|||||||
from .topology import SimTopology
|
from .topology import SimTopology
|
||||||
|
|
||||||
# Image name for the pre-built FIPS test image.
|
# Image name for the pre-built FIPS test image.
|
||||||
# The runner builds this once before starting containers.
|
#
|
||||||
FIPS_SIM_IMAGE = "fips-test:latest"
|
# A harness that has already built an image passes it in FIPS_TEST_IMAGE, and
|
||||||
|
# it is then the caller's image: the runner uses it and must not rebuild it.
|
||||||
|
# Unset means a bare run, where the shared tag is the right name and the runner
|
||||||
|
# still builds it. Read at import, which is safe because the simulation always
|
||||||
|
# starts as a child process with the environment already set.
|
||||||
|
FIPS_SIM_IMAGE = os.environ.get("FIPS_TEST_IMAGE", "fips-test:latest")
|
||||||
|
|
||||||
# Jinja2 template for the compose file.
|
# Jinja2 template for the compose file.
|
||||||
# Uses a pre-built image instead of per-service build to support large topologies.
|
# Uses a pre-built image instead of per-service build to support large topologies.
|
||||||
|
|||||||
@@ -268,14 +268,34 @@ class SimRunner:
|
|||||||
)
|
)
|
||||||
log.info("Wrote %s", self.compose_file)
|
log.info("Wrote %s", self.compose_file)
|
||||||
|
|
||||||
# 4. Build the test image once (avoids per-service build at scale)
|
# 4. Obtain the test image (once, rather than per-service at scale).
|
||||||
log.info("Building Docker image...")
|
#
|
||||||
|
# Building it is right for a bare run and wrong under a harness. When
|
||||||
|
# FIPS_TEST_IMAGE is set the image belongs to the caller, and every
|
||||||
|
# scenario of a parallel run would otherwise rebuild it into one shared
|
||||||
|
# name from one shared context — so assert it exists and fail loudly if
|
||||||
|
# it does not, rather than manufacture a substitute nobody asked for.
|
||||||
from .compose import FIPS_SIM_IMAGE
|
from .compose import FIPS_SIM_IMAGE
|
||||||
docker_dir = os.path.join(os.path.dirname(__file__), "..", "..", "docker")
|
if os.environ.get("FIPS_TEST_IMAGE"):
|
||||||
subprocess.run(
|
log.info("Using caller-supplied image %s", FIPS_SIM_IMAGE)
|
||||||
["docker", "build", "-t", FIPS_SIM_IMAGE, docker_dir],
|
probe = subprocess.run(
|
||||||
check=True,
|
["docker", "image", "inspect", FIPS_SIM_IMAGE],
|
||||||
)
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
if probe.returncode != 0:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"FIPS_TEST_IMAGE names {FIPS_SIM_IMAGE}, which is not present. "
|
||||||
|
"The harness that set it is expected to have built it."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
log.info("Building Docker image...")
|
||||||
|
docker_dir = os.environ.get("FIPS_BUILD_CONTEXT") or os.path.join(
|
||||||
|
os.path.dirname(__file__), "..", "..", "docker"
|
||||||
|
)
|
||||||
|
subprocess.run(
|
||||||
|
["docker", "build", "-t", FIPS_SIM_IMAGE, docker_dir],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
# 5. Start containers
|
# 5. Start containers
|
||||||
log.info("Starting %d containers...", len(self.topology.nodes))
|
log.info("Starting %d containers...", len(self.topology.nodes))
|
||||||
|
|||||||
Executable
+102
@@ -0,0 +1,102 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ── Test-image scoping guard ────────────────────────────────────────────────
|
||||||
|
# A local CI run builds fips-test:<run-id> and hands it to every suite through
|
||||||
|
# FIPS_TEST_IMAGE / FIPS_TEST_APP_IMAGE. It does NOT write fips-test:latest,
|
||||||
|
# deliberately: while a bridge back to that shared mutable name existed, a
|
||||||
|
# consumer that named it directly kept working while resolving whichever
|
||||||
|
# concurrent run wrote the tag last, and the verdict was then recorded against
|
||||||
|
# a commit whose binaries had not run. Nothing in the harness compares a
|
||||||
|
# running container's binary against the commit under test, so that failure is
|
||||||
|
# silent and leaves no artifact.
|
||||||
|
#
|
||||||
|
# The bridge is gone, so a consumer that names the shared tag now fails loudly
|
||||||
|
# at run time. This guard is the static half: it stops one being reintroduced,
|
||||||
|
# because the reintroduction is invisible on any host where a hand build has
|
||||||
|
# left an fips-test:latest lying around.
|
||||||
|
#
|
||||||
|
# What counts as a violation: a reference to the shared tag that is neither a
|
||||||
|
# comment, nor a documented default of the ${FIPS_TEST_IMAGE:-...} form, nor in
|
||||||
|
# a file on the allowlist below.
|
||||||
|
#
|
||||||
|
# Exit 0 = clean. Exit 1 = an unexpected reference. Exit 2 = the guard could
|
||||||
|
# not run; never treated as a pass.
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
# Files permitted to name the shared tag outright, each for a stated reason.
|
||||||
|
# Every one of these is a hand-run path: none is reachable from ci-local.sh
|
||||||
|
# with FIPS_TEST_IMAGE set.
|
||||||
|
#
|
||||||
|
# scripts/build.sh the developer build; :latest IS its product
|
||||||
|
# sidecar/scripts/test-sidecar.sh hand build, behind its --skip-build guard,
|
||||||
|
# which ci-local always passes
|
||||||
|
# static/scripts/iperf-compare-refs.sh hand-run A/B against two refs
|
||||||
|
# ci-cleanup.sh last-resort name for the image ip(8) runs in,
|
||||||
|
# after the caller's and the run's have failed
|
||||||
|
# check-image-scoping.sh this guard, which has to name what it looks
|
||||||
|
# for; it resolves no image
|
||||||
|
ALLOWED=(
|
||||||
|
"scripts/build.sh"
|
||||||
|
"sidecar/scripts/test-sidecar.sh"
|
||||||
|
"static/scripts/iperf-compare-refs.sh"
|
||||||
|
"ci-cleanup.sh"
|
||||||
|
"check-image-scoping.sh"
|
||||||
|
)
|
||||||
|
|
||||||
|
if ! command -v git >/dev/null 2>&1; then
|
||||||
|
echo "check-image-scoping: git not available, cannot sweep" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if [[ ! -d "$SCRIPT_DIR" ]]; then
|
||||||
|
echo "check-image-scoping: $SCRIPT_DIR missing" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Tracked files only, and no documentation: prose naming the tag is describing
|
||||||
|
# it, not resolving it.
|
||||||
|
mapfile -t files < <(git -C "$SCRIPT_DIR/.." ls-files -- testing/ | grep -vE '\.md$')
|
||||||
|
if [[ ${#files[@]} -eq 0 ]]; then
|
||||||
|
echo "check-image-scoping: no tracked files under testing/, refusing to pass" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
violations=0
|
||||||
|
for f in "${files[@]}"; do
|
||||||
|
rel="${f#testing/}"
|
||||||
|
skip=0
|
||||||
|
for a in "${ALLOWED[@]}"; do
|
||||||
|
[[ "$rel" == "$a" ]] && skip=1 && break
|
||||||
|
done
|
||||||
|
[[ $skip -eq 1 ]] && continue
|
||||||
|
[[ -f "$SCRIPT_DIR/../$f" ]] || continue
|
||||||
|
|
||||||
|
while IFS= read -r hit; do
|
||||||
|
n="${hit%%:*}"
|
||||||
|
text="${hit#*:}"
|
||||||
|
# A comment line is describing the tag, not resolving it. Shell, python
|
||||||
|
# and yaml all use #; nothing under testing/ uses // for comments.
|
||||||
|
[[ "$text" =~ ^[[:space:]]*# ]] && continue
|
||||||
|
# The documented indirection: the shared tag as a FALLBACK, which is
|
||||||
|
# what a bare hand run is supposed to get.
|
||||||
|
[[ "$text" == *'${FIPS_TEST_IMAGE:-fips-test:latest}'* ]] && continue
|
||||||
|
[[ "$text" == *'${FIPS_TEST_APP_IMAGE:-fips-test-app:latest}'* ]] && continue
|
||||||
|
[[ "$text" == *'os.environ.get("FIPS_TEST_IMAGE", "fips-test:latest")'* ]] && continue
|
||||||
|
echo "FAIL $f:$n names the shared test image directly:"
|
||||||
|
echo " $text"
|
||||||
|
violations=$((violations + 1))
|
||||||
|
done < <(grep -n 'fips-test:latest\|fips-test-app:latest' "$SCRIPT_DIR/../$f" 2>/dev/null)
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ $violations -gt 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "check-image-scoping: $violations reference(s) to the shared mutable test image."
|
||||||
|
echo "Read FIPS_TEST_IMAGE (default \${FIPS_TEST_IMAGE:-fips-test:latest}) instead."
|
||||||
|
echo "A run that resolves the shared tag can execute a concurrent run's binaries"
|
||||||
|
echo "and record the verdict against this commit."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "check-image-scoping: no unscoped references to the shared test image"
|
||||||
|
exit 0
|
||||||
+48
-5
@@ -66,9 +66,15 @@ RUN_ID="" # broad default: every CI run
|
|||||||
IMAGES=""
|
IMAGES=""
|
||||||
VETH_SUFFIXES="" # empty AND no --run-id: every simulation veth name
|
VETH_SUFFIXES="" # empty AND no --run-id: every simulation veth name
|
||||||
# ip(8) runs inside this image, the same way the simulation creates the
|
# ip(8) runs inside this image, the same way the simulation creates the
|
||||||
# interfaces, so the reap works wherever the simulation does. The chaos
|
# interfaces, so the reap works wherever the simulation does. Any fips test
|
||||||
# simulation builds it, and it carries iproute2.
|
# image will do; it is wanted only for its iproute2.
|
||||||
VETH_IMAGE="fips-test:latest"
|
#
|
||||||
|
# Empty here and resolved after the argument loop, because the resolution has
|
||||||
|
# to consider --veth-image. The old default of fips-test:latest is no longer
|
||||||
|
# safe on its own: ci-local.sh does not write that tag, so on a host that has
|
||||||
|
# only ever run the harness it need not exist at all, and the reap this script
|
||||||
|
# advertises as the remedy for orphaned interfaces would be a permanent no-op.
|
||||||
|
VETH_IMAGE=""
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -77,11 +83,26 @@ while [[ $# -gt 0 ]]; do
|
|||||||
--run-id) RUN_ID="$2"; shift 2 ;;
|
--run-id) RUN_ID="$2"; shift 2 ;;
|
||||||
--images) IMAGES="$2"; shift 2 ;;
|
--images) IMAGES="$2"; shift 2 ;;
|
||||||
--veth-suffixes) VETH_SUFFIXES="$2"; shift 2 ;;
|
--veth-suffixes) VETH_SUFFIXES="$2"; shift 2 ;;
|
||||||
|
--veth-image) VETH_IMAGE="$2"; shift 2 ;;
|
||||||
-h|--help) sed -n '2,/^set /{ /^set /d; s/^# \?//; p }' "$0"; exit 0 ;;
|
-h|--help) sed -n '2,/^set /{ /^set /d; s/^# \?//; p }' "$0"; exit 0 ;;
|
||||||
*) echo "Unknown option: $1" >&2; exit 2 ;;
|
*) echo "Unknown option: $1" >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Resolve the image to run ip(8) in: the caller's choice, then the run's own
|
||||||
|
# image, then any surviving fips test image. That last fallback is what keeps
|
||||||
|
# an unscoped `ci-local.sh --reap` working — it execs this script from inside
|
||||||
|
# its own argument loop, before the run identity is exported, so it can pass
|
||||||
|
# neither. The empty case is handled at the point of use, which already warns
|
||||||
|
# and skips rather than failing the sweep.
|
||||||
|
if [[ -z "$VETH_IMAGE" ]]; then
|
||||||
|
VETH_IMAGE="${FIPS_TEST_IMAGE:-}"
|
||||||
|
fi
|
||||||
|
if [[ -z "$VETH_IMAGE" ]] && command -v docker >/dev/null 2>&1; then
|
||||||
|
VETH_IMAGE="$(timeout 10 docker image ls --format '{{.Repository}}:{{.Tag}}' fips-test 2>/dev/null | head -n1)"
|
||||||
|
fi
|
||||||
|
[[ -z "$VETH_IMAGE" ]] && VETH_IMAGE="fips-test:latest"
|
||||||
|
|
||||||
if ! command -v docker >/dev/null 2>&1; then
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
# No docker, nothing to reap.
|
# No docker, nothing to reap.
|
||||||
exit 0
|
exit 0
|
||||||
@@ -217,8 +238,8 @@ reap_veths() {
|
|||||||
[[ -z "$pattern" ]] && return 0
|
[[ -z "$pattern" ]] && return 0
|
||||||
# Without the image there is no way to run ip(8). Orphans can outlive it —
|
# Without the image there is no way to run ip(8). Orphans can outlive it —
|
||||||
# `docker image prune -a`, a build host that prunes between runs, or a run
|
# `docker image prune -a`, a build host that prunes between runs, or a run
|
||||||
# aborted before ci-local.sh retags :latest all remove it while interfaces
|
# whose per-run image was already reaped all remove it while interfaces are
|
||||||
# are still up — so this is a real skip, not "nothing was ever run here".
|
# still up — so this is a real skip, not "nothing was ever run here".
|
||||||
if ! docker image inspect "$VETH_IMAGE" >/dev/null 2>&1; then
|
if ! docker image inspect "$VETH_IMAGE" >/dev/null 2>&1; then
|
||||||
veth_warn "image $VETH_IMAGE not present, cannot run ip(8)"
|
veth_warn "image $VETH_IMAGE not present, cannot run ip(8)"
|
||||||
return 0
|
return 0
|
||||||
@@ -247,6 +268,27 @@ reap_images() {
|
|||||||
timeout "$TMO" docker rmi -f "${imgs[@]}" >/dev/null 2>&1 || true
|
timeout "$TMO" docker rmi -f "${imgs[@]}" >/dev/null 2>&1 || true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Per-run build contexts left in the working tree. ci-local.sh removes its own
|
||||||
|
# from the EXIT trap, but the CI worker sends SIGKILL after SIGTERM and a
|
||||||
|
# SIGKILL runs no trap, so a preempted run can leave an 18 MB directory behind
|
||||||
|
# with nothing else that would ever notice it.
|
||||||
|
#
|
||||||
|
# Scoped mode takes only the named run's. Broad mode cannot tell a live run's
|
||||||
|
# context from an abandoned one by name, so it goes by age instead: a run lasts
|
||||||
|
# well under an hour, and a day is far outside that.
|
||||||
|
reap_build_contexts() {
|
||||||
|
local dir
|
||||||
|
if [[ -n "$RUN_ID" ]]; then
|
||||||
|
dir="$SCRIPT_DIR/docker-$RUN_ID"
|
||||||
|
[[ -d "$dir" ]] && rm -rf "$dir"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
while IFS= read -r dir; do
|
||||||
|
[[ -n "$dir" ]] && rm -rf "$dir"
|
||||||
|
done < <(find "$SCRIPT_DIR" -maxdepth 1 -type d -name 'docker-*' -mtime +0 2>/dev/null)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# Order matters: containers reference networks/volumes, so drop them first, and
|
# Order matters: containers reference networks/volumes, so drop them first, and
|
||||||
# the veth sweep needs an image to run ip(8) in, so it precedes the image reap.
|
# the veth sweep needs an image to run ip(8) in, so it precedes the image reap.
|
||||||
reap_containers
|
reap_containers
|
||||||
@@ -254,5 +296,6 @@ reap_networks
|
|||||||
reap_volumes
|
reap_volumes
|
||||||
reap_veths
|
reap_veths
|
||||||
reap_images
|
reap_images
|
||||||
|
reap_build_contexts
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
+74
-13
@@ -31,7 +31,6 @@
|
|||||||
# nat-lan, nostr-publish-consume, stun-faults,
|
# nat-lan, nostr-publish-consume, stun-faults,
|
||||||
# chaos-churn-mixed-10, chaos-ethernet-mesh,
|
# chaos-churn-mixed-10, chaos-ethernet-mesh,
|
||||||
# chaos-ethernet-only, chaos-tcp-mesh, chaos-congestion-stress,
|
# chaos-ethernet-only, chaos-tcp-mesh, chaos-congestion-stress,
|
||||||
# chaos-bloom-storm,
|
|
||||||
# sidecar, dns-resolver, deb-install
|
# sidecar, dns-resolver, deb-install
|
||||||
#
|
#
|
||||||
# Opt-in (require --with-tor; depend on live Tor network):
|
# Opt-in (require --with-tor; depend on live Tor network):
|
||||||
@@ -152,7 +151,6 @@ CHAOS_SUITES=(
|
|||||||
"ethernet-only ethernet-only"
|
"ethernet-only ethernet-only"
|
||||||
"tcp-mesh tcp-mesh"
|
"tcp-mesh tcp-mesh"
|
||||||
"congestion-stress congestion-stress"
|
"congestion-stress congestion-stress"
|
||||||
"bloom-storm bloom-storm"
|
|
||||||
)
|
)
|
||||||
# Scenarios retired 2026-07-23 because their subject was a pure decision the
|
# Scenarios retired 2026-07-23 because their subject was a pure decision the
|
||||||
# Docker harness could not test reliably, now covered by sans-IO unit tests:
|
# Docker harness could not test reliably, now covered by sans-IO unit tests:
|
||||||
@@ -175,6 +173,24 @@ CHAOS_SUITES=(
|
|||||||
# plus end-to-end datagram delivery (src/node/tests/forwarding.rs). Real-
|
# plus end-to-end datagram delivery (src/node/tests/forwarding.rs). Real-
|
||||||
# UDP convergence smoke still runs via static-mesh and the other chaos
|
# UDP convergence smoke still runs via static-mesh and the other chaos
|
||||||
# scenarios' baseline assertions, so no Docker coverage is lost.
|
# scenarios' baseline assertions, so no Docker coverage is lost.
|
||||||
|
#
|
||||||
|
# Retired 2026-07-26 for a third reason — the guard's power was never
|
||||||
|
# established, and unlike the two blocks above this one DOES lose coverage:
|
||||||
|
# - bloom-storm: guarded the regression in 0caef2a (fixed by 4cdf382), where
|
||||||
|
# a mid-chain tree update changing neither root nor depth leaked downstream
|
||||||
|
# as a sustained bloom announce storm. It was never once run against that
|
||||||
|
# regressed binary; its ceiling was inferred from a separate post-mortem
|
||||||
|
# harness that no longer exists in the tree. On the only regressed
|
||||||
|
# measurement that survives, the tail node's rate scales to ~7 sends per
|
||||||
|
# 30s, well under the scenario's ceiling of 40 — so it is not established
|
||||||
|
# that the assertion could ever have fired on its own bug class. The
|
||||||
|
# ceiling is also uniform per node, calibrated against the flap target
|
||||||
|
# (legitimately ~24) while the storm's actual signature is at the tail,
|
||||||
|
# which sits at 0 on fixed code. COVERAGE GAP: nothing now exercises
|
||||||
|
# downstream containment of a mid-chain ancestor swap. The scenario, its
|
||||||
|
# README, the link_swap sim primitive and the mesh-lab dispatch all stay
|
||||||
|
# on disk and it remains runnable by hand via
|
||||||
|
# testing/chaos/scripts/chaos.sh bloom-storm.
|
||||||
GATEWAY_SUITES=(gateway)
|
GATEWAY_SUITES=(gateway)
|
||||||
SIDECAR_SUITES=(sidecar)
|
SIDECAR_SUITES=(sidecar)
|
||||||
FIREWALL_SUITES=(firewall)
|
FIREWALL_SUITES=(firewall)
|
||||||
@@ -288,8 +304,9 @@ record() {
|
|||||||
#
|
#
|
||||||
# This script may be preempted (a CI worker sends SIGTERM, waits ~30s, then
|
# This script may be preempted (a CI worker sends SIGTERM, waits ~30s, then
|
||||||
# SIGKILL) so it can restart on a newer tip. To make that safe:
|
# SIGKILL) so it can restart on a newer tip. To make that safe:
|
||||||
# * every docker resource is namespaced to THIS run (compose project prefix
|
# * every docker resource is namespaced to THIS run (compose project prefix,
|
||||||
# + per-run image tags) so a restart never collides with a dying run;
|
# per-run image tags, per-run build context) so a restart never collides
|
||||||
|
# with a dying run, and neither does a concurrent one;
|
||||||
# * a trap tears down everything this run created on signal/exit, bounded by
|
# * a trap tears down everything this run created on signal/exit, bounded by
|
||||||
# `timeout` so a stuck `down` cannot wedge the trap (SIGKILL is the backstop).
|
# `timeout` so a stuck `down` cannot wedge the trap (SIGKILL is the backstop).
|
||||||
|
|
||||||
@@ -326,6 +343,14 @@ CI_LABEL_RUN="com.corganlabs.fips-ci.run=${CI_RUN_ID}"
|
|||||||
export FIPS_CI_RUN_ID="$CI_RUN_ID"
|
export FIPS_CI_RUN_ID="$CI_RUN_ID"
|
||||||
export FIPS_TEST_IMAGE="$CI_IMAGE_TEST"
|
export FIPS_TEST_IMAGE="$CI_IMAGE_TEST"
|
||||||
export FIPS_TEST_APP_IMAGE="$CI_IMAGE_APP"
|
export FIPS_TEST_APP_IMAGE="$CI_IMAGE_APP"
|
||||||
|
# The build context is this run's too. testing/docker/ is a single directory in
|
||||||
|
# the working tree, so two runs racing on its CONTENTS produce a per-run-tagged
|
||||||
|
# image built from the other run's binaries — scoping the tag alone does not
|
||||||
|
# close that. Absolute, and it has to be: compose interpolates this into
|
||||||
|
# build.context but resolves a relative result against the compose FILE's
|
||||||
|
# directory, not the working directory.
|
||||||
|
CI_BUILD_CONTEXT="$SCRIPT_DIR/docker-${CI_RUN_ID}"
|
||||||
|
export FIPS_BUILD_CONTEXT="$CI_BUILD_CONTEXT"
|
||||||
# Docker container names are GLOBAL — a compose project name does not scope
|
# Docker container names are GLOBAL — a compose project name does not scope
|
||||||
# them — so the suite compose files append this suffix to every explicit
|
# them — so the suite compose files append this suffix to every explicit
|
||||||
# container_name, and the suite scripts append it wherever they address a
|
# container_name, and the suite scripts append it wherever they address a
|
||||||
@@ -413,6 +438,15 @@ ci_teardown() {
|
|||||||
rm -rf "$SCRIPT_DIR/static/generated-configs${CI_RUN_NAME_SUFFIX}"
|
rm -rf "$SCRIPT_DIR/static/generated-configs${CI_RUN_NAME_SUFFIX}"
|
||||||
rm -rf "$SCRIPT_DIR/firewall/generated-configs${CI_RUN_NAME_SUFFIX}"
|
rm -rf "$SCRIPT_DIR/firewall/generated-configs${CI_RUN_NAME_SUFFIX}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 4. This run's build context. Unlike the generated configs it is removed
|
||||||
|
# on a red run too: it holds the binaries and the Dockerfiles, both
|
||||||
|
# reproducible from the commit, so it is never the evidence of a
|
||||||
|
# failure. Guarded on the path having been derived at all, so an early
|
||||||
|
# exit cannot turn this into `rm -rf $SCRIPT_DIR/docker-`.
|
||||||
|
if [[ -n "${CI_BUILD_CONTEXT:-}" && "$CI_BUILD_CONTEXT" != "$SCRIPT_DIR/docker-" ]]; then
|
||||||
|
rm -rf "$CI_BUILD_CONTEXT"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
on_signal() {
|
on_signal() {
|
||||||
@@ -786,24 +820,39 @@ run_tor_directory() {
|
|||||||
run_integration() {
|
run_integration() {
|
||||||
stage "Stage 3: Integration Tests"
|
stage "Stage 3: Integration Tests"
|
||||||
|
|
||||||
# Install binaries to shared docker context
|
# Populate THIS run's build context, then install the binaries into it.
|
||||||
|
# Everything but the binaries is copied from the tracked context directory;
|
||||||
|
# the binaries are installed fresh, and a previous run's are deliberately
|
||||||
|
# not carried over, since inheriting them is the failure this scoping
|
||||||
|
# exists to prevent.
|
||||||
|
info "Preparing build context $CI_BUILD_CONTEXT"
|
||||||
|
rm -rf "$CI_BUILD_CONTEXT"
|
||||||
|
mkdir -p "$CI_BUILD_CONTEXT" || { record "docker-build" 1; return; }
|
||||||
|
local _f
|
||||||
|
for _f in "$SCRIPT_DIR"/docker/*; do
|
||||||
|
case "$(basename "$_f")" in
|
||||||
|
fips|fipsctl|fipstop|fips-gateway) continue ;;
|
||||||
|
esac
|
||||||
|
cp -a "$_f" "$CI_BUILD_CONTEXT/" || { record "docker-build" 1; return; }
|
||||||
|
done
|
||||||
|
|
||||||
info "Installing release binaries"
|
info "Installing release binaries"
|
||||||
install_binaries testing/docker
|
install_binaries "$CI_BUILD_CONTEXT"
|
||||||
|
|
||||||
# Build unified test image once (used by all harnesses). Tag per-run
|
# Build unified test image once (used by all harnesses). Tag per-run
|
||||||
# (fips-test:${run}) so a build killed mid-flight never wedges the next
|
# (fips-test:${run}) so a build killed mid-flight never wedges the next
|
||||||
# run's rebuild, and concurrent runs never clobber each other's image.
|
# run's rebuild, and concurrent runs never clobber each other's image.
|
||||||
# Then retag :latest for the compose files / harness scripts that still
|
|
||||||
# reference fips-test:latest directly; the retag happens only after BOTH
|
|
||||||
# builds succeed, so :latest never points at a half-built image.
|
|
||||||
info "Building $CI_IMAGE_TEST Docker image"
|
info "Building $CI_IMAGE_TEST Docker image"
|
||||||
docker build -t "$CI_IMAGE_TEST" --label "$CI_LABEL" --label "$CI_LABEL_RUN" testing/docker --quiet \
|
docker build -t "$CI_IMAGE_TEST" --label "$CI_LABEL" --label "$CI_LABEL_RUN" "$CI_BUILD_CONTEXT" --quiet \
|
||||||
|| { record "docker-build" 1; return; }
|
|| { record "docker-build" 1; return; }
|
||||||
docker build -t "$CI_IMAGE_APP" --label "$CI_LABEL" --label "$CI_LABEL_RUN" \
|
docker build -t "$CI_IMAGE_APP" --label "$CI_LABEL" --label "$CI_LABEL_RUN" \
|
||||||
-f testing/docker/Dockerfile.app testing/docker --quiet \
|
-f "$CI_BUILD_CONTEXT/Dockerfile.app" "$CI_BUILD_CONTEXT" --quiet \
|
||||||
|| { record "docker-build-app" 1; return; }
|
|| { record "docker-build-app" 1; return; }
|
||||||
docker tag "$CI_IMAGE_TEST" fips-test:latest
|
# Deliberately NOT retagged to fips-test:latest. Every consumer reads
|
||||||
docker tag "$CI_IMAGE_APP" fips-test-app:latest
|
# FIPS_TEST_IMAGE, and a bridge back to the shared mutable name would let a
|
||||||
|
# consumer that does not keep working silently — resolving whichever
|
||||||
|
# concurrent run wrote the tag last, and recording that run's binaries under
|
||||||
|
# this run's commit. Without the bridge a missed consumer fails loudly.
|
||||||
|
|
||||||
# Single suite mode
|
# Single suite mode
|
||||||
if [[ -n "$ONLY_SUITE" ]]; then
|
if [[ -n "$ONLY_SUITE" ]]; then
|
||||||
@@ -1011,6 +1060,17 @@ run_ci_parity() {
|
|||||||
record "ci-parity" $rc
|
record "ci-parity" $rc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Nothing may resolve the shared mutable test image. This run does not write
|
||||||
|
# fips-test:latest, so a consumer naming it fails loudly here and now — but only
|
||||||
|
# on a host with no hand-built copy lying around, which is not a property to
|
||||||
|
# rely on. This is the static half of that.
|
||||||
|
run_image_scoping() {
|
||||||
|
local rc=0
|
||||||
|
info "[image-scoping] Checking that nothing names the shared test image"
|
||||||
|
"$SCRIPT_DIR/check-image-scoping.sh" || rc=$?
|
||||||
|
record "image-scoping" $rc
|
||||||
|
}
|
||||||
|
|
||||||
# Every daemon log string a test matches on must still be emitted by src/.
|
# Every daemon log string a test matches on must still be emitted by src/.
|
||||||
# A stale one does not fail — it stops observing, and an expect-zero assertion
|
# A stale one does not fail — it stops observing, and an expect-zero assertion
|
||||||
# built on it then passes for the wrong reason.
|
# built on it then passes for the wrong reason.
|
||||||
@@ -1064,6 +1124,7 @@ main() {
|
|||||||
run_ci_parity
|
run_ci_parity
|
||||||
run_log_strings
|
run_log_strings
|
||||||
run_trailing_log
|
run_trailing_log
|
||||||
|
run_image_scoping
|
||||||
run_wait_converge
|
run_wait_converge
|
||||||
|
|
||||||
if [[ "$TEST_ONLY" == true ]]; then
|
if [[ "$TEST_ONLY" == true ]]; then
|
||||||
|
|||||||
@@ -19,8 +19,12 @@ networks:
|
|||||||
|
|
||||||
x-fips-common: &fips-common
|
x-fips-common: &fips-common
|
||||||
build:
|
build:
|
||||||
context: ../docker
|
# The harness scopes its build context per run and passes it here; the
|
||||||
image: fips-test:latest
|
# shared directory is the hand-run default. Compose resolves a relative
|
||||||
|
# value against THIS file's directory, so the harness must export an
|
||||||
|
# absolute path.
|
||||||
|
context: ${FIPS_BUILD_CONTEXT:-../docker}
|
||||||
|
image: ${FIPS_TEST_IMAGE:-fips-test:latest}
|
||||||
entrypoint: ["/usr/local/bin/entrypoint.sh"]
|
entrypoint: ["/usr/local/bin/entrypoint.sh"]
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
|
|||||||
@@ -191,7 +191,15 @@ log "Generating firewall fixtures"
|
|||||||
|
|
||||||
log "Starting firewall harness"
|
log "Starting firewall harness"
|
||||||
docker compose -f "$COMPOSE_FILE" down >/dev/null 2>&1 || true
|
docker compose -f "$COMPOSE_FILE" down >/dev/null 2>&1 || true
|
||||||
docker compose -f "$COMPOSE_FILE" up -d --build
|
# --build only on the hand path. Under a harness, --skip-build means the caller
|
||||||
|
# has already built the image this compose file names, and rebuilding it here
|
||||||
|
# would overwrite that image from whatever the shared build context happens to
|
||||||
|
# hold — which is how a suite ends up certifying binaries it was never given.
|
||||||
|
if [ "$SKIP_BUILD" = false ]; then
|
||||||
|
docker compose -f "$COMPOSE_FILE" up -d --build
|
||||||
|
else
|
||||||
|
docker compose -f "$COMPOSE_FILE" up -d
|
||||||
|
fi
|
||||||
|
|
||||||
log "Waiting for fips0 on both nodes"
|
log "Waiting for fips0 on both nodes"
|
||||||
wait_for_fips0 "$CONTAINER_A" 40
|
wait_for_fips0 "$CONTAINER_A" 40
|
||||||
|
|||||||
@@ -148,8 +148,11 @@ def _analyze_lines(result: AnalysisResult, source: str, log_text: str):
|
|||||||
# Session establishment
|
# Session establishment
|
||||||
if "Session established" in line:
|
if "Session established" in line:
|
||||||
result.sessions_established.append((source, line))
|
result.sessions_established.append((source, line))
|
||||||
# Peer promotion
|
# Peer promotion. Match only the info-level string. "Outbound handshake
|
||||||
if "Peer promoted to active" in line or "Outbound handshake completed" in line:
|
# completed" is emitted from handle_msg2 on the same call path for the
|
||||||
|
# same promotion, so matching it too double-counted every outbound
|
||||||
|
# promotion in any run at debug level.
|
||||||
|
if "Peer promoted to active" in line:
|
||||||
result.peers_promoted.append((source, line))
|
result.peers_promoted.append((source, line))
|
||||||
# Peer removal
|
# Peer removal
|
||||||
if "Peer removed" in line:
|
if "Peer removed" in line:
|
||||||
|
|||||||
@@ -135,9 +135,10 @@ require_docker() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
require_test_image() {
|
require_test_image() {
|
||||||
if ! docker image inspect fips-test:latest >/dev/null 2>&1; then
|
local img="${FIPS_TEST_IMAGE:-fips-test:latest}"
|
||||||
echo "ERROR: fips-test:latest not present" >&2
|
if ! docker image inspect "$img" >/dev/null 2>&1; then
|
||||||
echo "Build it once with: bash testing/ci-local.sh --build-only" >&2
|
echo "ERROR: $img not present" >&2
|
||||||
|
echo "Build it once with: bash testing/scripts/build.sh" >&2
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ volumes:
|
|||||||
relay-data:
|
relay-data:
|
||||||
|
|
||||||
x-fips-common: &fips-common
|
x-fips-common: &fips-common
|
||||||
image: fips-test:latest
|
image: ${FIPS_TEST_IMAGE:-fips-test:latest}
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
devices:
|
devices:
|
||||||
@@ -325,7 +325,7 @@ services:
|
|||||||
ipv4_address: 172.31.10.51
|
ipv4_address: 172.31.10.51
|
||||||
|
|
||||||
stun-fault-shim:
|
stun-fault-shim:
|
||||||
image: fips-test:latest
|
image: ${FIPS_TEST_IMAGE:-fips-test:latest}
|
||||||
profiles: ["stun-faults"]
|
profiles: ["stun-faults"]
|
||||||
container_name: fips-nat-stun-fault-shim${FIPS_CI_NAME_SUFFIX:-}
|
container_name: fips-nat-stun-fault-shim${FIPS_CI_NAME_SUFFIX:-}
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -262,10 +262,21 @@ dump_lan_diagnostics() {
|
|||||||
trap 'echo ""; echo "NAT test interrupted"; cleanup; exit 130' INT TERM
|
trap 'echo ""; echo "NAT test interrupted"; cleanup; exit 130' INT TERM
|
||||||
|
|
||||||
require_test_image() {
|
require_test_image() {
|
||||||
if ! docker image inspect fips-test:latest >/dev/null 2>&1; then
|
local img="${FIPS_TEST_IMAGE:-fips-test:latest}"
|
||||||
echo "fips-test:latest not found; building test image"
|
if docker image inspect "$img" >/dev/null 2>&1; then
|
||||||
"$BUILD_SCRIPT"
|
return 0
|
||||||
fi
|
fi
|
||||||
|
# Building here is right for a hand run and wrong under a harness. When
|
||||||
|
# FIPS_TEST_IMAGE is set the caller has already built the image it named, so
|
||||||
|
# a miss means something upstream is broken; building a substitute would
|
||||||
|
# hide that and run binaries nobody asked for.
|
||||||
|
if [ -n "${FIPS_TEST_IMAGE:-}" ]; then
|
||||||
|
echo "ERROR: $img not present, and FIPS_TEST_IMAGE names the caller's own image" >&2
|
||||||
|
echo "The harness that set it is expected to have built it." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "$img not found; building test image"
|
||||||
|
"$BUILD_SCRIPT"
|
||||||
}
|
}
|
||||||
|
|
||||||
require_docker_daemon() {
|
require_docker_daemon() {
|
||||||
|
|||||||
@@ -49,10 +49,21 @@ require_docker_daemon() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
require_test_image() {
|
require_test_image() {
|
||||||
if ! docker image inspect fips-test:latest >/dev/null 2>&1; then
|
local img="${FIPS_TEST_IMAGE:-fips-test:latest}"
|
||||||
echo "fips-test:latest not found; building test image"
|
if docker image inspect "$img" >/dev/null 2>&1; then
|
||||||
"$BUILD_SCRIPT"
|
return 0
|
||||||
fi
|
fi
|
||||||
|
# Building here is right for a hand run and wrong under a harness. When
|
||||||
|
# FIPS_TEST_IMAGE is set the caller has already built the image it named, so
|
||||||
|
# a miss means something upstream is broken; building a substitute would
|
||||||
|
# hide that and run binaries nobody asked for.
|
||||||
|
if [ -n "${FIPS_TEST_IMAGE:-}" ]; then
|
||||||
|
echo "ERROR: $img not present, and FIPS_TEST_IMAGE names the caller's own image" >&2
|
||||||
|
echo "The harness that set it is expected to have built it." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "$img not found; building test image"
|
||||||
|
"$BUILD_SCRIPT"
|
||||||
}
|
}
|
||||||
|
|
||||||
dump_diagnostics() {
|
dump_diagnostics() {
|
||||||
|
|||||||
@@ -60,10 +60,21 @@ require_docker_daemon() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
require_test_image() {
|
require_test_image() {
|
||||||
if ! docker image inspect fips-test:latest >/dev/null 2>&1; then
|
local img="${FIPS_TEST_IMAGE:-fips-test:latest}"
|
||||||
echo "fips-test:latest not found; building test image"
|
if docker image inspect "$img" >/dev/null 2>&1; then
|
||||||
"$BUILD_SCRIPT"
|
return 0
|
||||||
fi
|
fi
|
||||||
|
# Building here is right for a hand run and wrong under a harness. When
|
||||||
|
# FIPS_TEST_IMAGE is set the caller has already built the image it named, so
|
||||||
|
# a miss means something upstream is broken; building a substitute would
|
||||||
|
# hide that and run binaries nobody asked for.
|
||||||
|
if [ -n "${FIPS_TEST_IMAGE:-}" ]; then
|
||||||
|
echo "ERROR: $img not present, and FIPS_TEST_IMAGE names the caller's own image" >&2
|
||||||
|
echo "The harness that set it is expected to have built it." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "$img not found; building test image"
|
||||||
|
"$BUILD_SCRIPT"
|
||||||
}
|
}
|
||||||
|
|
||||||
dump_diagnostics() {
|
dump_diagnostics() {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ networks:
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
fips:
|
fips:
|
||||||
image: fips-test:latest
|
image: ${FIPS_TEST_IMAGE:-fips-test:latest}
|
||||||
hostname: fips-sidecar
|
hostname: fips-sidecar
|
||||||
labels:
|
labels:
|
||||||
- "com.corganlabs.fips-ci=1"
|
- "com.corganlabs.fips-ci=1"
|
||||||
@@ -40,7 +40,7 @@ services:
|
|||||||
ipv4_address: ${FIPS_IPV4:-172.20.1.20}
|
ipv4_address: ${FIPS_IPV4:-172.20.1.20}
|
||||||
|
|
||||||
app:
|
app:
|
||||||
image: fips-test-app:latest
|
image: ${FIPS_TEST_APP_IMAGE:-fips-test-app:latest}
|
||||||
labels:
|
labels:
|
||||||
- "com.corganlabs.fips-ci=1"
|
- "com.corganlabs.fips-ci=1"
|
||||||
network_mode: "service:fips"
|
network_mode: "service:fips"
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ trap cleanup EXIT
|
|||||||
|
|
||||||
if [[ "${1:-}" != "--skip-build" ]]; then
|
if [[ "${1:-}" != "--skip-build" ]]; then
|
||||||
log "Building test images..."
|
log "Building test images..."
|
||||||
DOCKER_DIR="$(cd "$SIDECAR_DIR/../docker" && pwd)"
|
DOCKER_DIR="${FIPS_BUILD_CONTEXT:-$(cd "$SIDECAR_DIR/../docker" && pwd)}"
|
||||||
docker build -t fips-test:latest "$DOCKER_DIR"
|
docker build -t fips-test:latest "$DOCKER_DIR"
|
||||||
docker build -t fips-test-app:latest -f "$DOCKER_DIR/Dockerfile.app" "$DOCKER_DIR"
|
docker build -t fips-test-app:latest -f "$DOCKER_DIR/Dockerfile.app" "$DOCKER_DIR"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -119,19 +119,13 @@ testing/static/
|
|||||||
│ └── topologies/
|
│ └── topologies/
|
||||||
│ ├── mesh.yaml # Mesh topology definition
|
│ ├── mesh.yaml # Mesh topology definition
|
||||||
│ ├── chain.yaml # Chain topology definition
|
│ ├── chain.yaml # Chain topology definition
|
||||||
│ ├── mesh-public.yaml # Mesh + external public node
|
|
||||||
│ ├── tcp-chain.yaml # TCP chain (3 nodes, port 8443)
|
|
||||||
│ └── rekey.yaml # Rekey integration test (5 nodes)
|
│ └── rekey.yaml # Rekey integration test (5 nodes)
|
||||||
├── generated-configs/ # Auto-generated, run-scoped (gitignored)
|
├── generated-configs/ # Auto-generated, run-scoped (gitignored)
|
||||||
│ ├── npubs.env # NPUB_A=..., NPUB_B=..., etc.
|
│ ├── npubs.env # NPUB_A=..., NPUB_B=..., etc.
|
||||||
│ ├── mesh/
|
│ ├── mesh/
|
||||||
│ │ ├── node-a.yaml ... node-e.yaml
|
│ │ ├── node-a.yaml ... node-e.yaml
|
||||||
│ ├── mesh-public/
|
│ └── chain/
|
||||||
│ │ ├── node-a.yaml ... node-e.yaml
|
│ ├── node-a.yaml ... node-e.yaml
|
||||||
│ ├── chain/
|
|
||||||
│ │ ├── node-a.yaml ... node-e.yaml
|
|
||||||
│ └── tcp-chain/
|
|
||||||
│ ├── node-a.yaml ... node-c.yaml
|
|
||||||
├── scripts/
|
├── scripts/
|
||||||
│ ├── build.sh # Build binary + generate configs
|
│ ├── build.sh # Build binary + generate configs
|
||||||
│ ├── generate-configs.sh # Generate node configs from topology
|
│ ├── generate-configs.sh # Generate node configs from topology
|
||||||
@@ -219,7 +213,7 @@ each mesh needs unique node identities to avoid key conflicts. The optional
|
|||||||
|
|
||||||
# Or generate configs directly
|
# Or generate configs directly
|
||||||
./testing/static/scripts/generate-configs.sh mesh my-mesh-1
|
./testing/static/scripts/generate-configs.sh mesh my-mesh-1
|
||||||
./testing/static/scripts/generate-configs.sh mesh-public my-mesh-1
|
./testing/static/scripts/generate-configs.sh chain my-mesh-1
|
||||||
```
|
```
|
||||||
|
|
||||||
### How It Works
|
### How It Works
|
||||||
|
|||||||
@@ -1,51 +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:
|
|
||||||
npub: "npub1qmc3cvfz0yu2hx96nq3gp55zdan2qclealn7xshgr448d3nh6lks7zel98"
|
|
||||||
external_ip: "test-us01.fips.network"
|
|
||||||
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,29 +0,0 @@
|
|||||||
# TCP Chain Topology Definition
|
|
||||||
#
|
|
||||||
# Three nodes with TCP links forming a linear chain. Tests basic TCP
|
|
||||||
# transport connectivity, spanning tree convergence, and multi-hop
|
|
||||||
# routing over TCP. All peer connections use TCP on port 443.
|
|
||||||
#
|
|
||||||
# default_transport: tcp tells the config generator to use TCP
|
|
||||||
# transport and port 443 instead of the default UDP/2121.
|
|
||||||
|
|
||||||
default_transport: tcp
|
|
||||||
|
|
||||||
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]
|
|
||||||
@@ -98,62 +98,6 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
fips-net:
|
fips-net:
|
||||||
|
|
||||||
# ── Mesh-public topology (mesh + external public node) ────────
|
|
||||||
pub-a:
|
|
||||||
<<: *fips-common
|
|
||||||
profiles: ["mesh-public"]
|
|
||||||
container_name: fips-node-a${FIPS_CI_NAME_SUFFIX:-}
|
|
||||||
hostname: node-a
|
|
||||||
volumes:
|
|
||||||
- ../docker/resolv.conf:/etc/resolv.conf:ro
|
|
||||||
- ./generated-configs${FIPS_CI_NAME_SUFFIX:-}/mesh-public/node-a.yaml:/etc/fips/fips.yaml:ro
|
|
||||||
networks:
|
|
||||||
fips-net:
|
|
||||||
|
|
||||||
pub-b:
|
|
||||||
<<: *fips-common
|
|
||||||
profiles: ["mesh-public"]
|
|
||||||
container_name: fips-node-b${FIPS_CI_NAME_SUFFIX:-}
|
|
||||||
hostname: node-b
|
|
||||||
volumes:
|
|
||||||
- ../docker/resolv.conf:/etc/resolv.conf:ro
|
|
||||||
- ./generated-configs${FIPS_CI_NAME_SUFFIX:-}/mesh-public/node-b.yaml:/etc/fips/fips.yaml:ro
|
|
||||||
networks:
|
|
||||||
fips-net:
|
|
||||||
|
|
||||||
pub-c:
|
|
||||||
<<: *fips-common
|
|
||||||
profiles: ["mesh-public"]
|
|
||||||
container_name: fips-node-c${FIPS_CI_NAME_SUFFIX:-}
|
|
||||||
hostname: node-c
|
|
||||||
volumes:
|
|
||||||
- ../docker/resolv.conf:/etc/resolv.conf:ro
|
|
||||||
- ./generated-configs${FIPS_CI_NAME_SUFFIX:-}/mesh-public/node-c.yaml:/etc/fips/fips.yaml:ro
|
|
||||||
networks:
|
|
||||||
fips-net:
|
|
||||||
|
|
||||||
pub-d:
|
|
||||||
<<: *fips-common
|
|
||||||
profiles: ["mesh-public"]
|
|
||||||
container_name: fips-node-d${FIPS_CI_NAME_SUFFIX:-}
|
|
||||||
hostname: node-d
|
|
||||||
volumes:
|
|
||||||
- ../docker/resolv.conf:/etc/resolv.conf:ro
|
|
||||||
- ./generated-configs${FIPS_CI_NAME_SUFFIX:-}/mesh-public/node-d.yaml:/etc/fips/fips.yaml:ro
|
|
||||||
networks:
|
|
||||||
fips-net:
|
|
||||||
|
|
||||||
pub-e:
|
|
||||||
<<: *fips-common
|
|
||||||
profiles: ["mesh-public"]
|
|
||||||
container_name: fips-node-e${FIPS_CI_NAME_SUFFIX:-}
|
|
||||||
hostname: node-e
|
|
||||||
volumes:
|
|
||||||
- ../docker/resolv.conf:/etc/resolv.conf:ro
|
|
||||||
- ./generated-configs${FIPS_CI_NAME_SUFFIX:-}/mesh-public/node-e.yaml:/etc/fips/fips.yaml:ro
|
|
||||||
networks:
|
|
||||||
fips-net:
|
|
||||||
|
|
||||||
# ── Chain topology (A-B-C-D-E) ────────────────────────────────
|
# ── Chain topology (A-B-C-D-E) ────────────────────────────────
|
||||||
chain-a:
|
chain-a:
|
||||||
<<: *fips-common
|
<<: *fips-common
|
||||||
@@ -421,40 +365,6 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
fips-net:
|
fips-net:
|
||||||
|
|
||||||
# ── TCP chain topology (A-B-C) ───────────────────────────────
|
|
||||||
tcp-a:
|
|
||||||
<<: *fips-common
|
|
||||||
profiles: ["tcp-chain"]
|
|
||||||
container_name: fips-node-a${FIPS_CI_NAME_SUFFIX:-}
|
|
||||||
hostname: node-a
|
|
||||||
volumes:
|
|
||||||
- ../docker/resolv.conf:/etc/resolv.conf:ro
|
|
||||||
- ./generated-configs${FIPS_CI_NAME_SUFFIX:-}/tcp-chain/node-a.yaml:/etc/fips/fips.yaml:ro
|
|
||||||
networks:
|
|
||||||
fips-net:
|
|
||||||
|
|
||||||
tcp-b:
|
|
||||||
<<: *fips-common
|
|
||||||
profiles: ["tcp-chain"]
|
|
||||||
container_name: fips-node-b${FIPS_CI_NAME_SUFFIX:-}
|
|
||||||
hostname: node-b
|
|
||||||
volumes:
|
|
||||||
- ../docker/resolv.conf:/etc/resolv.conf:ro
|
|
||||||
- ./generated-configs${FIPS_CI_NAME_SUFFIX:-}/tcp-chain/node-b.yaml:/etc/fips/fips.yaml:ro
|
|
||||||
networks:
|
|
||||||
fips-net:
|
|
||||||
|
|
||||||
tcp-c:
|
|
||||||
<<: *fips-common
|
|
||||||
profiles: ["tcp-chain"]
|
|
||||||
container_name: fips-node-c${FIPS_CI_NAME_SUFFIX:-}
|
|
||||||
hostname: node-c
|
|
||||||
volumes:
|
|
||||||
- ../docker/resolv.conf:/etc/resolv.conf:ro
|
|
||||||
- ./generated-configs${FIPS_CI_NAME_SUFFIX:-}/tcp-chain/node-c.yaml:/etc/fips/fips.yaml:ro
|
|
||||||
networks:
|
|
||||||
fips-net:
|
|
||||||
|
|
||||||
# ── Gateway integration test (gateway + server + non-FIPS client) ─
|
# ── Gateway integration test (gateway + server + non-FIPS client) ─
|
||||||
gw-gateway:
|
gw-gateway:
|
||||||
<<: *fips-common
|
<<: *fips-common
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ ping_path() {
|
|||||||
# steady state, which is what real-world FIPS deployments see. To
|
# steady state, which is what real-world FIPS deployments see. To
|
||||||
# force on-wire multihop, isolate nodes on distinct docker networks.
|
# force on-wire multihop, isolate nodes on distinct docker networks.
|
||||||
case "$PROFILE" in
|
case "$PROFILE" in
|
||||||
mesh|mesh-public)
|
mesh)
|
||||||
# Static-peer paths only — without mDNS / a Nostr relay in the
|
# Static-peer paths only — without mDNS / a Nostr relay in the
|
||||||
# test container set, non-adjacent pairs (A↔B, A↔C) can't
|
# test container set, non-adjacent pairs (A↔B, A↔C) can't
|
||||||
# establish direct UDP and the bench would either fail
|
# establish direct UDP and the bench would either fail
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Generate FIPS node configuration files from template and topology definition.
|
# Generate FIPS node configuration files from template and topology definition.
|
||||||
#
|
#
|
||||||
# Usage: ./generate-configs.sh <topology> [mesh-name]
|
# Usage: ./generate-configs.sh <topology> [mesh-name]
|
||||||
# topology: mesh, mesh-public, chain, etc.
|
# topology: mesh, chain, etc.
|
||||||
# mesh-name: optional; when given, docker node identities are derived
|
# mesh-name: optional; when given, docker node identities are derived
|
||||||
# deterministically via sha256(mesh-name|node-id)
|
# deterministically via sha256(mesh-name|node-id)
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ echo ""
|
|||||||
# nodes — labels DO NOT encode hop count, since peer discovery
|
# nodes — labels DO NOT encode hop count, since peer discovery
|
||||||
# converges every same-subnet pair onto a direct UDP path within a
|
# converges every same-subnet pair onto a direct UDP path within a
|
||||||
# few ticks regardless of the static `peers:` list.
|
# few ticks regardless of the static `peers:` list.
|
||||||
if [ "$PROFILE" = "mesh" ] || [ "$PROFILE" = "mesh-public" ]; then
|
if [ "$PROFILE" = "mesh" ]; then
|
||||||
echo "Topology (static \`peers:\` from mesh.yaml):"
|
echo "Topology (static \`peers:\` from mesh.yaml):"
|
||||||
echo " A peers with: D, E"
|
echo " A peers with: D, E"
|
||||||
echo " B peers with: C"
|
echo " B peers with: C"
|
||||||
@@ -109,7 +109,7 @@ echo ""
|
|||||||
echo "Waiting ${SETTLE_SECONDS}s for mesh convergence..."
|
echo "Waiting ${SETTLE_SECONDS}s for mesh convergence..."
|
||||||
sleep "$SETTLE_SECONDS"
|
sleep "$SETTLE_SECONDS"
|
||||||
|
|
||||||
if [ "$PROFILE" = "mesh" ] || [ "$PROFILE" = "mesh-public" ]; then
|
if [ "$PROFILE" = "mesh" ]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo "Testing mesh topology paths:"
|
echo "Testing mesh topology paths:"
|
||||||
iperf_test node-d node-a "$NPUB_D" "A→D"
|
iperf_test node-d node-a "$NPUB_D" "A→D"
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ if [ "$PROFILE" = "chain" ]; then
|
|||||||
wait_for_peers fips-node-c${FIPS_CI_NAME_SUFFIX:-} 2 20 || true
|
wait_for_peers fips-node-c${FIPS_CI_NAME_SUFFIX:-} 2 20 || true
|
||||||
wait_for_peers fips-node-d${FIPS_CI_NAME_SUFFIX:-} 2 20 || true
|
wait_for_peers fips-node-d${FIPS_CI_NAME_SUFFIX:-} 2 20 || true
|
||||||
wait_for_peers fips-node-e${FIPS_CI_NAME_SUFFIX:-} 1 20 || true
|
wait_for_peers fips-node-e${FIPS_CI_NAME_SUFFIX:-} 1 20 || true
|
||||||
elif [ "$PROFILE" = "mesh" ] || [ "$PROFILE" = "mesh-public" ]; then
|
elif [ "$PROFILE" = "mesh" ]; then
|
||||||
# Mesh: check all nodes reach their configured peer counts
|
# Mesh: check all nodes reach their configured peer counts
|
||||||
wait_for_peers fips-node-a${FIPS_CI_NAME_SUFFIX:-} 2 20 || true
|
wait_for_peers fips-node-a${FIPS_CI_NAME_SUFFIX:-} 2 20 || true
|
||||||
wait_for_peers fips-node-b${FIPS_CI_NAME_SUFFIX:-} 1 20 || true
|
wait_for_peers fips-node-b${FIPS_CI_NAME_SUFFIX:-} 1 20 || true
|
||||||
@@ -105,7 +105,7 @@ else
|
|||||||
# section below is guarded by these same profile names, so the script
|
# section below is guarded by these same profile names, so the script
|
||||||
# ran no assertion at all and exited 0. A typo in the caller's profile
|
# ran no assertion at all and exited 0. A typo in the caller's profile
|
||||||
# argument produced a green run that tested nothing.
|
# argument produced a green run that tested nothing.
|
||||||
echo "ERROR: unknown profile '$PROFILE' (expected: chain, mesh, mesh-public)" >&2
|
echo "ERROR: unknown profile '$PROFILE' (expected: chain, mesh)" >&2
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
# Wait for full pairwise connectivity, progress-aware: the actual pings
|
# Wait for full pairwise connectivity, progress-aware: the actual pings
|
||||||
@@ -119,7 +119,7 @@ wait_until_connected ping_all_quiet 45 15 || true
|
|||||||
PASSED=0
|
PASSED=0
|
||||||
FAILED=0
|
FAILED=0
|
||||||
|
|
||||||
if [ "$PROFILE" = "mesh" ] || [ "$PROFILE" = "mesh-public" ]; then
|
if [ "$PROFILE" = "mesh" ]; then
|
||||||
# Sparse mesh topology: A-B, B-C, C-D, D-E, E-A, A-D
|
# 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)
|
# Test all 20 directed pairs (5 nodes × 4 targets each)
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ networks:
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
fips-a:
|
fips-a:
|
||||||
image: fips-test:latest
|
image: ${FIPS_TEST_IMAGE:-fips-test:latest}
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
devices:
|
devices:
|
||||||
@@ -36,7 +36,7 @@ services:
|
|||||||
dir-test:
|
dir-test:
|
||||||
|
|
||||||
fips-b:
|
fips-b:
|
||||||
image: fips-test:latest
|
image: ${FIPS_TEST_IMAGE:-fips-test:latest}
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
devices:
|
devices:
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ services:
|
|||||||
tor-test:
|
tor-test:
|
||||||
|
|
||||||
fips-a:
|
fips-a:
|
||||||
image: fips-test:latest
|
image: ${FIPS_TEST_IMAGE:-fips-test:latest}
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
devices:
|
devices:
|
||||||
@@ -45,7 +45,7 @@ services:
|
|||||||
tor-test:
|
tor-test:
|
||||||
|
|
||||||
fips-b:
|
fips-b:
|
||||||
image: fips-test:latest
|
image: ${FIPS_TEST_IMAGE:-fips-test:latest}
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
devices:
|
devices:
|
||||||
|
|||||||
Reference in New Issue
Block a user