Files
fips/testing/firewall/table-check-test.sh
Johnathan Corgan 4b72e995eb Check firewall case (e) against a captured nft listing
Case (e) of the firewall suite piped `docker exec ... nft list table inet
fips` into `grep -q` under pipefail. grep exits at its match; when the exec
client was still writing the rest of the listing it took SIGPIPE (141), and
pipefail turned a rule that was present into "the sed did not change the
scoping rule". It false-failed intermittently in local and GitHub CI.

The check moves into table_has in testing/firewall/table-check.sh, which
captures the whole listing and then matches it with grep -qF on a
here-string, so no pipe is left. It returns 0 when the rule is present, 1
when it is absent and 2 when the table could not be listed; case (e) gives
each failure its own message. The assertion is unchanged: same container,
same table, same rule text.

testing/firewall/table-check-test.sh replaces docker with a shell function
that prints the listing in two chunks, the rule in the first and a 0.2 s
pause before the second, which is the race's shape. It runs as a guard in
every local run (run_fwtable) and as a matching GitHub CI step.

With table_has written as the old pipeline, moved verbatim, the test failed
(the pipeline's status was 141, the producer's SIGPIPE):

  FAIL  rule present, writer still writing after the match (run 1): returns 0  (rc=1)
  ... runs 2 to 5 the same
  PASS  rule absent: returns 1  (rc=1)
  FAIL  listing could not be read: returns 2  (rc=1)
  6 table_has check(s) failed

With the capture form all seven checks pass.
2026-10-15 20:56:31 +00:00

96 lines
2.9 KiB
Bash
Executable File

#!/bin/bash
# Tests for table_has() in table-check.sh, the firewall suite's check that a
# loaded nftables table contains a given rule.
#
# `docker` is replaced by a shell function that prints a canned
# `nft list table inet fips` listing in two chunks, with the matching rule in
# the first and a pause before the second. That is the shape of the defect this
# guards against: the reader has its match and exits while the producer is still
# writing. A check that pipes the producer into `grep -q` under pipefail sees
# the producer's SIGPIPE (141) and reports a rule that is present as absent.
# No containers are involved, so the suite is hermetic; it takes about two
# seconds.
#
# Run:
# ./table-check-test.sh
# Exits 0 only if every case passes; non-zero if any case fails.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=SCRIPTDIR/table-check.sh
source "$SCRIPT_DIR/table-check.sh"
FAILURES=0
RULE='iifname != "notfips0" return'
STUB_MODE=present
# Stand-in for `docker exec <container> nft list table inet fips`. STUB_MODE
# picks a listing with the rule (present), one without it (absent), or an exec
# that fails (fail).
docker() {
case "$STUB_MODE" in
fail)
echo "Error response from daemon: container is not running" >&2
return 1
;;
present)
printf '%s\n' 'table inet fips {' ' chain input {' \
' type filter hook input priority filter; policy accept;' \
" $RULE"
;;
absent)
printf '%s\n' 'table inet fips {' ' chain input {' \
' type filter hook input priority filter; policy accept;' \
' iifname != "fips0" return'
;;
esac
sleep 0.2
printf '%s\n' ' ct state established,related accept' \
' icmpv6 type echo-request accept' ' counter drop' ' }' '}'
return 0
}
# Record a single assertion result.
check() {
local name="$1"
local ok="$2" # 0 = pass, anything else = fail
local detail="${3:-}"
if [ "$ok" -eq 0 ]; then
echo "PASS $name${detail:+ ($detail)}"
else
echo "FAIL $name${detail:+ ($detail)}"
FAILURES=$((FAILURES + 1))
fi
return 0
}
# Run table_has against the stand-in in the given mode; sets RC.
probe() {
STUB_MODE="$1"
RC=0
table_has fips-fw-container-b "$RULE" 2>/dev/null || RC=$?
return 0
}
for i in 1 2 3 4 5; do
probe present
check "rule present, writer still writing after the match (run $i): returns 0" \
"$([ "$RC" -eq 0 ] && echo 0 || echo 1)" "rc=$RC"
done
probe absent
check "rule absent: returns 1" "$([ "$RC" -eq 1 ] && echo 0 || echo 1)" "rc=$RC"
probe fail
check "listing could not be read: returns 2" \
"$([ "$RC" -eq 2 ] && echo 0 || echo 1)" "rc=$RC"
echo ""
if [ "$FAILURES" -eq 0 ]; then
echo "All table_has cases passed"
exit 0
fi
echo "$FAILURES table_has check(s) failed"
exit 1