mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-30 07:37:36 +00:00
Start the no_std core as a second workspace member. The crate is empty: it holds the rule that code in it builds for any target with an allocator (no std, no async runtime, no OS calls, no target cfg), and the root crate does not depend on it yet, so nothing that builds or ships changes. The root package stays first in Cargo.toml with its version line untouched, since the packaging scripts read the release version from it, and the core is fixed at 0.0.0 with publish = false so releases keep a single bump site. default-members names both crates, so the bare cargo clippy and test commands in local CI and GitHub CI cover the core as well as the root package. Add a fence to both runners: clippy and the tests of the core on its own, so a dependency feature that only the root crate enables cannot hide one the core needs. Pin the privileged interface-binding step to the fips package's lib test binary. With two lib test executables, `tail -1` picked whichever cargo listed last, and the core's binary passes the ethernet filter having run nothing. The step now also runs under bash with pipefail and fails unless at least one test passed. Widen the comment-reference, portable-atomics and log-string guards to core/src/, and add core/ to the stale-binary checks of the mdns, native-api and deb-install harnesses, so code that later moves into the core stays checked. Copy core/ into the two nostr relay example images, whose build would otherwise fail to load the workspace manifest, and give the standalone BLE spike under testing/ble an empty workspace table of its own, so cargo keeps loading it as its own project rather than refusing it as an unlisted member of the root workspace. Add core/README.md for anyone reading or adding code under core/: what the crate is for, the rules its code follows, how to add a dependency without letting workspace feature unification hide a missing feature, how tests work under no_std, a checklist for moving a module in from the root crate, and the checks to run before a change.
256 lines
9.8 KiB
Bash
256 lines
9.8 KiB
Bash
#!/bin/bash
|
|
# ── mDNS LAN discovery, end to end ──────────────────────────────────────────
|
|
# Two fips daemons in separate containers on one user-defined Docker bridge,
|
|
# with LAN rendezvous on, matching scopes, no configured peers and Nostr off.
|
|
# The only way they can find each other is the mDNS advert one multicasts and
|
|
# the other browses for, so each node listing the other as an authenticated
|
|
# peer, at the other's address on this bridge, is the receive path working end
|
|
# to end.
|
|
#
|
|
# This is the coverage the unit tests in src/mdns/tests.rs cannot give. Their
|
|
# positive test needs two mDNS daemons in one process, which multicast
|
|
# loopback does not support reliably, so it is #[ignore]d; the two that run
|
|
# assert only that something was NOT seen, and would pass with the receive
|
|
# path removed. Separate containers are separate processes in separate
|
|
# network namespaces, which is the real cross-daemon path, and a user-defined
|
|
# bridge forwards link-local multicast between its containers the way a
|
|
# switch does (tested for 224.0.0.251:5353 before this harness was written).
|
|
#
|
|
# The network takes a docker-assigned subnet rather than a fixed one, so two
|
|
# concurrent runs cannot collide on address space; nothing here depends on
|
|
# the addresses beyond reading them back.
|
|
#
|
|
# Two settings exist for break-checking the harness, and leave it red when
|
|
# used; a normal run sets neither:
|
|
# MDNS_SCOPE_B=<scope> give node B a scope other than node A's
|
|
# MDNS_SPLIT_BRIDGES=1 put node B on a bridge of its own
|
|
#
|
|
# Image: FIPS_TEST_IMAGE if set (ci-local.sh passes its per-run image; the
|
|
# GitHub job passes the image it built). Otherwise a minimal image is built
|
|
# from target/release, refusing binaries older than the source.
|
|
#
|
|
# Usage: ./test.sh
|
|
# Exit 0 = both nodes discovered and peered with each other. Exit 1 = they did
|
|
# not. Exit 2 = the harness could not run; never treated as a pass.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
set -uo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
LABEL="com.corganlabs.fips-ci=1"
|
|
NET_A="fips-mdns-net-$$"
|
|
NET_B="fips-mdns-net-b-$$"
|
|
NODE_A="fips-mdns-a-$$"
|
|
NODE_B="fips-mdns-b-$$"
|
|
UDP_PORT=2121
|
|
SCOPE_A="fips-mdns-test"
|
|
SCOPE_B="${MDNS_SCOPE_B:-$SCOPE_A}"
|
|
SPLIT_BRIDGES="${MDNS_SPLIT_BRIDGES:-0}"
|
|
# Seconds from both daemons running until each must list the other. mDNS
|
|
# resolution and the handshake take well under a second on a quiet host; the
|
|
# rest is headroom for a loaded CI runner.
|
|
DISCOVERY_TIMEOUT="${MDNS_DISCOVERY_TIMEOUT:-60}"
|
|
IMAGE=""
|
|
BUILT_IMAGE=""
|
|
WORK="$(mktemp -d)"
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
log() { echo "=== $*"; }
|
|
pass() { echo " PASS: $*"; PASS=$((PASS + 1)); }
|
|
fail() { echo " FAIL: $*"; FAIL=$((FAIL + 1)); }
|
|
|
|
cleanup() {
|
|
docker rm -f "$NODE_A" "$NODE_B" >/dev/null 2>&1
|
|
docker network rm "$NET_A" "$NET_B" >/dev/null 2>&1
|
|
[[ -n "$BUILT_IMAGE" ]] && docker rmi -f "$BUILT_IMAGE" >/dev/null 2>&1
|
|
rm -rf "$WORK"
|
|
return 0
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
# Set IMAGE to FIPS_TEST_IMAGE, or build a minimal one from target/release.
|
|
resolve_image() {
|
|
if [[ -n "${FIPS_TEST_IMAGE:-}" ]]; then
|
|
IMAGE="$FIPS_TEST_IMAGE"
|
|
log "Using FIPS_TEST_IMAGE=$IMAGE"
|
|
return 0
|
|
fi
|
|
local bin="$REPO_ROOT/target/release"
|
|
if [[ ! -x "$bin/fips" || ! -x "$bin/fipsctl" ]]; then
|
|
echo "No FIPS_TEST_IMAGE and no release fips/fipsctl; build them: cargo build --release" >&2
|
|
return 1
|
|
fi
|
|
# A stale daemon would give a verdict about code that is not the tree's.
|
|
local newer
|
|
newer="$(find "$REPO_ROOT/src" "$REPO_ROOT/core" "$REPO_ROOT/Cargo.toml" -newer "$bin/fips" -print -quit 2>/dev/null)"
|
|
if [[ -n "$newer" ]]; then
|
|
echo "target/release/fips is older than $newer; rebuild: cargo build --release" >&2
|
|
return 1
|
|
fi
|
|
BUILT_IMAGE="fips-mdns-test:$$"
|
|
log "Building $BUILT_IMAGE from target/release"
|
|
local context="$WORK/context"
|
|
mkdir -p "$context"
|
|
cp "$bin/fips" "$bin/fipsctl" "$context/"
|
|
cat > "$context/Dockerfile" <<'DOCKERFILE'
|
|
FROM debian:trixie-slim
|
|
# libdbus-1-3 and libsystemd0 are the daemon's dynamic dependencies.
|
|
RUN apt-get update && \
|
|
apt-get install -y --no-install-recommends ca-certificates libdbus-1-3 libsystemd0 && \
|
|
rm -rf /var/lib/apt/lists/*
|
|
COPY fips fipsctl /usr/local/bin/
|
|
RUN chmod +x /usr/local/bin/fips /usr/local/bin/fipsctl && mkdir -p /run/fips
|
|
DOCKERFILE
|
|
docker build -t "$BUILT_IMAGE" --label "$LABEL" "$context" --quiet >/dev/null || return 1
|
|
IMAGE="$BUILT_IMAGE"
|
|
return 0
|
|
}
|
|
|
|
# write_config <file> <nsec> <scope>: LAN rendezvous on, Nostr off, no peers.
|
|
write_config() {
|
|
cat > "$1" <<YAML
|
|
node:
|
|
identity:
|
|
nsec: "$2"
|
|
rendezvous:
|
|
nostr:
|
|
enabled: false
|
|
lan:
|
|
enabled: true
|
|
scope: "$3"
|
|
|
|
tun:
|
|
enabled: false
|
|
|
|
dns:
|
|
enabled: false
|
|
|
|
transports:
|
|
udp:
|
|
bind_addr: "0.0.0.0:$UDP_PORT"
|
|
mtu: 1472
|
|
YAML
|
|
}
|
|
|
|
# start_node <container> <network> <config file>
|
|
start_node() {
|
|
# fips::mdns at debug so a failed run shows what the browser saw.
|
|
docker create --name "$1" --hostname "$1" --label "$LABEL" --network "$2" \
|
|
-e RUST_LOG=info,fips::mdns=debug \
|
|
--entrypoint /usr/local/bin/fips "$IMAGE" --config /fips-mdns.yaml >/dev/null \
|
|
&& docker cp "$3" "$1:/fips-mdns.yaml" >/dev/null \
|
|
&& docker start "$1" >/dev/null
|
|
}
|
|
|
|
# wait_for_log <container> <fixed string> <timeout>: 0 once the line appears.
|
|
wait_for_log() {
|
|
local waited=0
|
|
while [[ $waited -lt $3 ]]; do
|
|
docker logs "$1" 2>&1 | grep -qF "$2" && return 0
|
|
sleep 1
|
|
waited=$((waited + 1))
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# peer_addr <container> <npub>: the transport address at which the node lists
|
|
# <npub> as a peer. Empty when it does not list it; status 1 when the node
|
|
# could not be asked, so a dead reader is never taken for "not yet".
|
|
peer_addr() {
|
|
local out
|
|
out="$(docker exec "$1" fipsctl show peers 2>/dev/null)" || return 1
|
|
python3 -c '
|
|
import json, sys
|
|
peers = json.loads(sys.argv[1])["peers"]
|
|
print(next((p.get("transport_addr", "?") for p in peers if p["npub"] == sys.argv[2]), ""))
|
|
' "$out" "$2" 2>/dev/null
|
|
}
|
|
|
|
# container_ip <container> <network>
|
|
container_ip() {
|
|
docker inspect -f "{{(index .NetworkSettings.Networks \"$2\").IPAddress}}" "$1" 2>/dev/null
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────
|
|
|
|
resolve_image || exit 2
|
|
|
|
keys_a="$(python3 "$REPO_ROOT/testing/lib/derive_keys.py" fips-mdns node-a)"
|
|
keys_b="$(python3 "$REPO_ROOT/testing/lib/derive_keys.py" fips-mdns node-b)"
|
|
nsec_a="$(sed -n 's/^nsec=//p' <<<"$keys_a")"; npub_a="$(sed -n 's/^npub=//p' <<<"$keys_a")"
|
|
nsec_b="$(sed -n 's/^nsec=//p' <<<"$keys_b")"; npub_b="$(sed -n 's/^npub=//p' <<<"$keys_b")"
|
|
if [[ -z "$nsec_a" || -z "$npub_a" || -z "$nsec_b" || -z "$npub_b" ]]; then
|
|
echo "could not derive node keys" >&2
|
|
exit 2
|
|
fi
|
|
write_config "$WORK/a.yaml" "$nsec_a" "$SCOPE_A"
|
|
write_config "$WORK/b.yaml" "$nsec_b" "$SCOPE_B"
|
|
|
|
net_b="$NET_A"
|
|
where="one user-defined bridge"
|
|
if [[ "$SPLIT_BRIDGES" == "1" ]]; then
|
|
net_b="$NET_B"
|
|
where="two separate bridges"
|
|
fi
|
|
log "Two nodes on $where, scopes '$SCOPE_A' and '$SCOPE_B'"
|
|
for net in $(printf '%s\n' "$NET_A" "$net_b" | sort -u); do
|
|
if ! docker network create --driver bridge --label "$LABEL" "$net" >/dev/null; then
|
|
echo "could not create network $net" >&2
|
|
exit 2
|
|
fi
|
|
done
|
|
if ! start_node "$NODE_A" "$NET_A" "$WORK/a.yaml" || ! start_node "$NODE_B" "$net_b" "$WORK/b.yaml"; then
|
|
echo "could not start the two nodes" >&2
|
|
exit 2
|
|
fi
|
|
|
|
# Both daemons must be up with LAN discovery running before an absence of
|
|
# peers means anything: a node that never started mDNS cannot find a peer, and
|
|
# that is a setup failure, not a discovery verdict.
|
|
for node in "$NODE_A" "$NODE_B"; do
|
|
if ! wait_for_log "$node" "lan: mDNS discovery started" 30; then
|
|
echo "$node did not start LAN discovery; last log lines:" >&2
|
|
docker logs "$node" 2>&1 | tail -20 >&2
|
|
exit 2
|
|
fi
|
|
done
|
|
pass "both daemons running with LAN discovery started"
|
|
|
|
ip_a="$(container_ip "$NODE_A" "$NET_A")"
|
|
ip_b="$(container_ip "$NODE_B" "$net_b")"
|
|
log "Waiting up to ${DISCOVERY_TIMEOUT}s for each to list the other ($NODE_A $ip_a, $NODE_B $ip_b)"
|
|
addr_ab=""; addr_ba=""; unreadable=0
|
|
for ((waited = 0; waited < DISCOVERY_TIMEOUT; waited++)); do
|
|
addr_ab="$(peer_addr "$NODE_A" "$npub_b")" || { unreadable=$((unreadable + 1)); addr_ab=""; }
|
|
addr_ba="$(peer_addr "$NODE_B" "$npub_a")" || { unreadable=$((unreadable + 1)); addr_ba=""; }
|
|
[[ -n "$addr_ab" && -n "$addr_ba" ]] && break
|
|
sleep 1
|
|
done
|
|
[[ "$unreadable" -gt 0 ]] && echo " note: show peers could not be read $unreadable time(s) while waiting"
|
|
|
|
for row in "$NODE_A|$addr_ab|$ip_b|B" "$NODE_B|$addr_ba|$ip_a|A"; do
|
|
IFS='|' read -r node addr want other <<<"$row"
|
|
# transport_addr is host:port; the host must be the other node's address on
|
|
# this bridge, which is where its advert came from.
|
|
if [[ -z "$addr" ]]; then
|
|
fail "$node does not list node $other as a peer after ${DISCOVERY_TIMEOUT}s"
|
|
elif [[ "${addr%:*}" == "$want" ]]; then
|
|
pass "$node lists node $other as an authenticated peer at $addr"
|
|
else
|
|
fail "$node lists node $other at $addr, not at its bridge address $want"
|
|
fi
|
|
done
|
|
|
|
if [[ "$FAIL" -ne 0 ]]; then
|
|
for node in "$NODE_A" "$NODE_B"; do
|
|
echo "--- $node: lan and handshake lines"
|
|
docker logs "$node" 2>&1 | grep -iE "lan:|mdns|handshake|peer" | tail -20
|
|
done
|
|
fi
|
|
|
|
echo ""
|
|
echo "=== mdns: $PASS passed, $FAIL failed"
|
|
[[ "$FAIL" -eq 0 ]] || exit 1
|
|
exit 0
|