The rule that an unprovable family is not a clean one held inside the module and was lost at each point where a result becomes visible. Status projected a committed stopped receipt through its "nothing is owned" branch, which asserted a verified ruleset unconditionally — so a teardown that deliberately withheld that claim had it restored by the next status read, on the only channel still carrying the reservation. Direct uninstall told the user the family "is cleared by the reboot". Only the root-manager branch schedules one; the direct branch leaves the module installed, requires no reboot, and has just deleted the state that could identify leftover rules. It now names the reboot as the action that ends the ambiguity instead of implying it is under way. A partial purge receipt hardcoded every removal fact to zero. An unremovable external staging workspace was enough to reach it with both managed trees already gone, denying a removal that did happen on the same receipt the app reads. The facts are measured now, and the diagnostic names what actually survived. Teardown also discarded the preflight's positive proof: a family read moments earlier and found empty raised a reservation anyway if the frontend went busy in between. That proof is now recorded and honoured, for the mode that produced it only. Tests: the owned baseline mode had no coverage at all, the boot-recovery fixture forced autostart off so service.sh's new recovery retry was never entered, and the purge assertions checked that a string was present rather than that the two scripts agree. Each new test was proven by reverting its fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
184 lines
6.1 KiB
Shell
184 lines
6.1 KiB
Shell
#!/bin/sh
|
|
set -eu
|
|
|
|
ROOT=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd)
|
|
CASE="${Z2_TEST_TMP:?}/status-snapshot-fast-path"
|
|
STATE="$CASE/state"
|
|
MODULE="$CASE/module"
|
|
OUTPUT="$CASE/status.out"
|
|
|
|
fail() { echo "FAIL: status-snapshot-fast-path: $*" >&2; exit 1; }
|
|
|
|
mkdir -p "$STATE" "$MODULE"
|
|
chmod 0700 "$STATE"
|
|
cp "$ROOT/zapret2/runtime.ini" "$MODULE/runtime.ini"
|
|
cp "$(command -v sleep)" "$MODULE/nfqws2"
|
|
chmod 0755 "$MODULE/nfqws2"
|
|
|
|
"$MODULE/nfqws2" 30 &
|
|
NFQWS_PID=$!
|
|
trap 'kill "$NFQWS_PID" 2>/dev/null || true; wait "$NFQWS_PID" 2>/dev/null || true' EXIT
|
|
|
|
NFQWS_START=$(awk '{print $22}' "/proc/$NFQWS_PID/stat")
|
|
NFQWS_ARGV_SHA=$(sha256sum "/proc/$NFQWS_PID/cmdline")
|
|
NFQWS_ARGV_SHA=${NFQWS_ARGV_SHA%% *}
|
|
printf '%s\n' "$NFQWS_PID" > "$STATE/nfqws2.pid"
|
|
chmod 0600 "$STATE/nfqws2.pid"
|
|
|
|
write_snapshot() {
|
|
argv_sha="$1"
|
|
cat > "$STATE/status.snapshot" <<EOF
|
|
status=ok
|
|
boot_id=$(cat /proc/sys/kernel/random/boot_id)
|
|
rules_total=2
|
|
own_pid=$NFQWS_PID
|
|
own_pid_starttime=$NFQWS_START
|
|
own_argv_sha256=$argv_sha
|
|
owner_generation=status-fast-path
|
|
owner_metadata_verified=1
|
|
ruleset_verified=1
|
|
rules_expected=2
|
|
qnum=200
|
|
ipv4_active=1
|
|
ipv6_active=0
|
|
ipv4_rules=2
|
|
ipv6_rules=0
|
|
chains=1
|
|
anchors=1
|
|
nfqueue_supported=1
|
|
queue_bypass_supported=1
|
|
connbytes_supported=0
|
|
multiport_supported=1
|
|
mark_supported=1
|
|
error_schema=1
|
|
error_status=OK
|
|
error_domain=NONE
|
|
error_code=NONE
|
|
error_stage=NONE
|
|
error_detail=
|
|
EOF
|
|
chmod 0600 "$STATE/status.snapshot"
|
|
}
|
|
|
|
run_status() {
|
|
rc=0
|
|
STATE_DIR="$STATE" MODDIR="$CASE" ZAPRET_DIR="$MODULE" \
|
|
sh "$ROOT/zapret2/scripts/zapret-status.sh" --machine-v6 > "$OUTPUT" 2>&1 || rc=$?
|
|
}
|
|
|
|
write_snapshot "$NFQWS_ARGV_SHA"
|
|
run_status
|
|
[ "$rc" -eq 0 ] || fail "valid snapshot returned $rc"
|
|
grep -Fxq 'Z2_STATUS=ok' "$OUTPUT" || fail "valid snapshot was not accepted"
|
|
grep -Fxq 'Z2_PID_VERIFIED=1' "$OUTPUT" || fail "live process identity was not verified"
|
|
grep -Fxq 'Z2_CHAINS=1' "$OUTPUT" || fail "published chain count was not projected"
|
|
|
|
write_snapshot 0000000000000000000000000000000000000000000000000000000000000000
|
|
run_status
|
|
[ "$rc" -eq 2 ] || fail "mismatched command digest returned $rc instead of degraded"
|
|
grep -Fxq 'Z2_STATUS=degraded' "$OUTPUT" ||
|
|
fail "mismatched command digest did not fail closed"
|
|
grep -Fxq 'Z2_PID_VERIFIED=0' "$OUTPUT" ||
|
|
fail "mismatched command digest retained verified process state"
|
|
|
|
# A snapshot describes processes and netfilter objects that a reboot destroys,
|
|
# so one stamped with another boot must be rejected whole: no facts from it may
|
|
# reach the projection, not even the capability flags or the queue number.
|
|
write_snapshot "$NFQWS_ARGV_SHA"
|
|
# Give the foreign snapshot a queue number that appears nowhere else, so a
|
|
# leak is distinguishable from the value runtime.ini would supply anyway.
|
|
sed -e 's/^boot_id=.*/boot_id=11111111-1111-1111-1111-111111111111/' \
|
|
-e 's/^qnum=.*/qnum=777/' \
|
|
"$STATE/status.snapshot" > "$STATE/status.snapshot.foreign"
|
|
mv "$STATE/status.snapshot.foreign" "$STATE/status.snapshot"
|
|
chmod 0600 "$STATE/status.snapshot"
|
|
run_status
|
|
grep -Fxq 'Z2_STATUS=ok' "$OUTPUT" && fail "a snapshot from another boot was accepted"
|
|
grep -Fxq 'Z2_QNUM=777' "$OUTPUT" && fail "queue number leaked from the rejected snapshot"
|
|
# and the projection still carries the configured queue number instead of none
|
|
grep -Fxq 'Z2_QNUM=200' "$OUTPUT" || fail "rejected snapshot left the queue number unresolved"
|
|
grep -Fxq 'Z2_EXPECTED_RULES=0' "$OUTPUT" ||
|
|
fail "expected rule count leaked from the rejected snapshot"
|
|
grep -Fxq 'Z2_NFQUEUE=0' "$OUTPUT" ||
|
|
fail "capability facts leaked from the rejected snapshot"
|
|
|
|
# The pre-2.2.0 format carries no boot binding at all and is equally stale.
|
|
grep -v '^boot_id=' "$STATE/status.snapshot" > "$STATE/status.snapshot.legacy"
|
|
mv "$STATE/status.snapshot.legacy" "$STATE/status.snapshot"
|
|
chmod 0600 "$STATE/status.snapshot"
|
|
run_status
|
|
grep -Fxq 'Z2_STATUS=ok' "$OUTPUT" && fail "an unbound legacy snapshot was accepted"
|
|
|
|
# A teardown that could not read one firewall family commits a stopped receipt
|
|
# that deliberately withholds its verification claim. By then nothing of ours
|
|
# is owned, so the projection takes its "nothing is owned" branch — and that
|
|
# branch asserted a proof this device cannot produce, laundering the single
|
|
# reservation the module kept on the only channel that still carries it.
|
|
MOCK="$CASE/bin"
|
|
mkdir -p "$MOCK"
|
|
cat > "$MOCK/iptables" <<'EOF'
|
|
#!/bin/sh
|
|
case " $* " in
|
|
*' -t mangle -L OUTPUT -n '*) exit 0 ;;
|
|
*' -t mangle -S ZAPRET2_OUT '*|*' -t mangle -S ZAPRET2_IN '*) exit 1 ;;
|
|
' -t mangle -S ') exit 0 ;;
|
|
esac
|
|
exit 1
|
|
EOF
|
|
cp "$MOCK/iptables" "$MOCK/ip6tables"
|
|
chmod 0755 "$MOCK/iptables" "$MOCK/ip6tables"
|
|
|
|
write_stopped_snapshot() {
|
|
cat > "$STATE/status.snapshot" <<EOF
|
|
status=stopped
|
|
boot_id=$(cat /proc/sys/kernel/random/boot_id)
|
|
rules_total=0
|
|
rules_expected=0
|
|
qnum=200
|
|
ipv4_active=0
|
|
ipv6_active=$2
|
|
ipv4_rules=0
|
|
ipv6_rules=0
|
|
chains=0
|
|
anchors=0
|
|
nfqueue_supported=1
|
|
queue_bypass_supported=1
|
|
connbytes_supported=0
|
|
multiport_supported=1
|
|
mark_supported=1
|
|
ruleset_verified=$1
|
|
owner_metadata_verified=0
|
|
error_schema=1
|
|
error_status=OK
|
|
error_domain=NONE
|
|
error_code=NONE
|
|
error_stage=NONE
|
|
error_detail=
|
|
EOF
|
|
chmod 0600 "$STATE/status.snapshot"
|
|
}
|
|
|
|
run_unowned_status() {
|
|
rc=0
|
|
PATH="$MOCK:$PATH" STATE_DIR="$STATE" MODDIR="$CASE" ZAPRET_DIR="$MODULE" \
|
|
sh "$ROOT/zapret2/scripts/zapret-status.sh" --machine-v6 > "$OUTPUT" 2>&1 || rc=$?
|
|
grep -Fxq 'Z2_OWNED=0' "$OUTPUT" ||
|
|
fail "the reservation case never reached the unowned projection"
|
|
}
|
|
|
|
# This is the exact shape a reserved teardown commits: it never claims an
|
|
# active IPv6 family, it only declines to certify the ruleset.
|
|
rm -f "$STATE/nfqws2.pid"
|
|
write_stopped_snapshot 0 0
|
|
run_unowned_status
|
|
grep -Fxq 'Z2_RULESET_VERIFIED=0' "$OUTPUT" ||
|
|
fail "a stopped receipt that withheld its verification claim was reported as verified"
|
|
|
|
# The ordinary stopped receipt still projects a verified ruleset, so the branch
|
|
# above narrows the claim rather than abandoning it.
|
|
write_stopped_snapshot 1 0
|
|
run_unowned_status
|
|
grep -Fxq 'Z2_RULESET_VERIFIED=1' "$OUTPUT" ||
|
|
fail "a fully verified stopped receipt lost its verification claim"
|
|
|
|
echo "Status snapshot fast-path tests passed"
|