mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
testing: port admission-cap integration test to the Noise XX handshake
The admission-cap suite asserted IK wire sizes (inbound Msg1 len 84, outbound Msg2 len 104) to verify the inbound max_peers cap. On the XX handshake those sizes differ and Msg2 is always sent before identity is known, so the IK discriminator matched nothing and the suite failed despite the cap working correctly. Replace the wire-size discriminator with a protocol-agnostic cap-holds invariant: each denied peer must keep re-initiating handshakes (sustained inbound, size-agnostic), no denied peer may be promoted to an active session, the node must log cap-enforcement events, and the capped node must hold exactly max_peers. Raise node-c's handshake logging to debug in the mesh profile so the early-gate drop is visible alongside the late-check rejection. Also record in the changelog that for peers the node also dials, the cap is enforced by the retained late check rather than the early msg3 drop.
This commit is contained in:
+5
-1
@@ -275,7 +275,11 @@ with v0.3.x peers.
|
||||
the value is local CPU / allocation savings (no `ActivePeer` build,
|
||||
no `peers_by_index` insert, no link transition) and cleaner peer-
|
||||
side semantics — no fake-promotion whose subsequent data frames
|
||||
fail decryption on this side.
|
||||
fail decryption on this side. Note the known-peer / cross-connection
|
||||
bypass also covers peers the node is itself dialing (e.g. configured
|
||||
`auto_connect` peers): for those the cap is enforced by the retained
|
||||
late check, so the early drop primarily benefits unsolicited inbound
|
||||
peers the node is not contacting.
|
||||
- Mesh-size estimator (`compute_mesh_size`) no longer double-counts the
|
||||
parent's bloom cardinality during the transient cache window after a
|
||||
local parent-switch. Symptom: `fipsctl show status` / fipstop displayed
|
||||
|
||||
@@ -63,6 +63,8 @@ services:
|
||||
profiles: ["mesh"]
|
||||
container_name: fips-node-c
|
||||
hostname: node-c
|
||||
environment:
|
||||
- RUST_LOG=info,fips::node::handlers::handshake=debug
|
||||
volumes:
|
||||
- ../docker/resolv.conf:/etc/resolv.conf:ro
|
||||
- ./generated-configs/mesh/node-c.yaml:/etc/fips/fips.yaml:ro
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
#!/bin/bash
|
||||
# Integration test for the inbound max_peers admission gate.
|
||||
# Integration test for the inbound max_peers admission cap (Noise XX / next).
|
||||
#
|
||||
# Verifies the silent-drop behavior of the early-gate in handle_msg1 at
|
||||
# scale, using the mesh topology with one node's node.max_peers lowered
|
||||
# to 1. This forces 2 of node-c's 3 configured peers (b, d, e) into a
|
||||
# sustained denied state, and asserts via tcpdump that no Msg2 responses
|
||||
# go back to those denied peers across a 60s capture window.
|
||||
# Verifies the cap holds under sustained denied-peer load, using the mesh
|
||||
# topology with one node's node.limits.max_peers lowered to 1. This forces
|
||||
# 2 of node-c's 3 configured peers (b, d, e) into a sustained denied state.
|
||||
#
|
||||
# Unlike the IK version on maint/master — which gates at handle_msg1 before
|
||||
# Msg2 and asserts "no Msg2 to denied peers" — on Noise XX the identity is
|
||||
# not known until Msg3, so Msg1/Msg2/Msg3 all cross the wire before the cap
|
||||
# can act. Moreover, because the cap'd node also dials its configured peers
|
||||
# (auto_connect), denied peers match the early handle_msg3 gate's
|
||||
# is_pending_outbound bypass and the cap is enforced by the late
|
||||
# promote_connection check. The test therefore asserts a path-agnostic
|
||||
# cap-holds invariant rather than a wire-size discriminator:
|
||||
#
|
||||
# Tested behavior:
|
||||
# - Denied peers DO arrive at the cap'd node (inbound FMP-IK Msg1, 84 B)
|
||||
# - Cap'd node sends NO Msg2 responses (104 B) to denied peers
|
||||
# - Denied peers keep re-initiating handshakes (sustained inbound UDP)
|
||||
# - No denied peer is ever promoted to an active session
|
||||
# - The daemon actively refuses over-cap promotions (enforcement events)
|
||||
# - Cap'd node maintains exactly max_peers active sessions
|
||||
# - Admitted peer's session stays healthy throughout the window
|
||||
#
|
||||
# Usage:
|
||||
# ./admission-cap-test.sh Run the test (containers must be up)
|
||||
@@ -161,26 +168,31 @@ wait $LOAD_PID 2>/dev/null || true
|
||||
captured=$(wc -l < "$CAP_FILE")
|
||||
info "captured $captured tcpdump lines → $CAP_FILE"
|
||||
|
||||
# ── Phase 3: per-denied-peer wire-level assertion ────────────────────
|
||||
info "phase 3: per-denied-peer assertion (inbound Msg1 > 0, outbound Msg2 == 0)"
|
||||
# ── Phase 3: per-denied-peer cap-holds assertion ─────────────────────
|
||||
# Size-agnostic: the XX handshake message sizes differ from IK and may
|
||||
# change, so we only need evidence that each denied peer keeps trying and
|
||||
# is never promoted.
|
||||
info "phase 3: per-denied-peer assertion (sustained inbound retries > 0, not promoted)"
|
||||
OVERALL=0
|
||||
TOTAL_MSG1_IN=0
|
||||
TOTAL_MSG2_OUT=0
|
||||
TOTAL_IN=0
|
||||
FINAL_PEERS=$(docker exec fips-node-$CAP_NODE fipsctl show peers 2>/dev/null \
|
||||
| grep -oE 'npub1[a-z0-9]+' | sort -u || true)
|
||||
for n in $DENIED; do
|
||||
n_ip=$(node_ip "$n")
|
||||
# Inbound: src=n_ip:* → dst=cap_ip:2121; FMP-IK Msg1 wire size = 84 B
|
||||
msg1_in=$(grep -cE "IP $n_ip\.[0-9]+ > $CAP_IP\.2121: UDP, length 84" "$CAP_FILE" || true)
|
||||
# Outbound: src=cap_ip:2121 → dst=n_ip:*; FMP-IK Msg2 wire size = 104 B
|
||||
msg2_out=$(grep -cE "IP $CAP_IP\.2121 > $n_ip\.[0-9]+: UDP, length 104" "$CAP_FILE" || true)
|
||||
info " node-$n ($n_ip): inbound Msg1 (len 84) = $msg1_in, outbound Msg2 (len 104) = $msg2_out"
|
||||
TOTAL_MSG1_IN=$((TOTAL_MSG1_IN + msg1_in))
|
||||
TOTAL_MSG2_OUT=$((TOTAL_MSG2_OUT + msg2_out))
|
||||
if [ "$msg1_in" -eq 0 ]; then
|
||||
info " FAIL: expected inbound Msg1 retries from denied peer (peer not sustained-retrying?)"
|
||||
n_npub=$(node_npub "$n")
|
||||
# Sustained retry: any inbound UDP from the denied peer to the cap'd
|
||||
# node, regardless of handshake message size.
|
||||
in_count=$(grep -cE "IP $n_ip\.[0-9]+ > $CAP_IP\.2121:" "$CAP_FILE" || true)
|
||||
promoted="no"
|
||||
if echo "$FINAL_PEERS" | grep -q "$n_npub"; then promoted="yes"; fi
|
||||
info " node-$n ($n_ip): inbound packets = $in_count, promoted = $promoted"
|
||||
TOTAL_IN=$((TOTAL_IN + in_count))
|
||||
if [ "$in_count" -eq 0 ]; then
|
||||
info " FAIL: no inbound from denied peer (peer not sustained-retrying?)"
|
||||
OVERALL=1
|
||||
fi
|
||||
if [ "$msg2_out" -gt 0 ]; then
|
||||
info " FAIL: silent-drop gate leaked — expected 0 outbound Msg2, got $msg2_out"
|
||||
if [ "$promoted" = "yes" ]; then
|
||||
info " FAIL: denied peer was promoted to an active session — cap leaked"
|
||||
OVERALL=1
|
||||
fi
|
||||
done
|
||||
@@ -189,17 +201,35 @@ done
|
||||
pc_final=$(docker exec fips-node-$CAP_NODE fipsctl show status 2>/dev/null \
|
||||
| grep -m1 peer_count | sed 's/.*: *//' | tr -d ',' || echo 0)
|
||||
info "node-$CAP_NODE final peer_count=$pc_final (expected $MAX_PEERS)"
|
||||
[ "$pc_final" = "$MAX_PEERS" ] || OVERALL=1
|
||||
[ "$pc_final" = "$MAX_PEERS" ] || { info " FAIL: peer_count drifted from cap"; OVERALL=1; }
|
||||
|
||||
# ── Phase 5: daemon actively refused over-cap promotions ─────────────
|
||||
# Path-agnostic enforcement evidence. The early handle_msg3 gate logs
|
||||
# "Silent-dropping Msg3 at max_peers cap" (debug); the late
|
||||
# promote_connection check logs "Failed to promote inbound connection"
|
||||
# (warn, "max peers exceeded"). In the mesh topology denied peers are
|
||||
# also dialed by the cap'd node, so the late check is the active enforcer
|
||||
# — but counting both keeps the test correct regardless of which path
|
||||
# fires (and survives a future change to the early gate).
|
||||
clogs=$(docker logs fips-node-$CAP_NODE 2>&1 || true)
|
||||
gate_drops=$(echo "$clogs" | grep -c "Silent-dropping Msg3 at max_peers cap" || true)
|
||||
late_rejects=$(echo "$clogs" | grep -c "Failed to promote inbound connection" || true)
|
||||
enforcement=$((gate_drops + late_rejects))
|
||||
info "enforcement events on node-$CAP_NODE: early-gate=$gate_drops, late-check=$late_rejects (total $enforcement)"
|
||||
[ "$enforcement" -gt 0 ] || { info " FAIL: no cap-enforcement events observed in node-$CAP_NODE logs"; OVERALL=1; }
|
||||
|
||||
if [ "$OVERALL" -eq 0 ]; then
|
||||
pass "admission-cap: silent-drop gate verified at scale"
|
||||
pass "admission-cap: cap holds under sustained denied-peer load"
|
||||
pass " denied peers: $(echo $DENIED | wc -w), capture: ${CAPTURE_SECS}s"
|
||||
pass " total inbound Msg1 from denied: $TOTAL_MSG1_IN (sustained retries observed)"
|
||||
pass " total outbound Msg2 to denied: $TOTAL_MSG2_OUT (silent-drop holds)"
|
||||
pass " total inbound from denied: $TOTAL_IN (sustained retries observed)"
|
||||
pass " cap enforcement events: $enforcement (early-gate $gate_drops + late-check $late_rejects)"
|
||||
pass " cap'd node held peer_count=$pc_final (max=$MAX_PEERS)"
|
||||
rm -f "$CAP_FILE"
|
||||
exit 0
|
||||
else
|
||||
info "--- tcpdump capture tail (last 50 lines) ---"
|
||||
tail -50 "$CAP_FILE"
|
||||
info "--- node-$CAP_NODE log tail (last 30 lines) ---"
|
||||
echo "$clogs" | tail -30
|
||||
fail "admission-cap: see failures above (capture preserved at $CAP_FILE)"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user