Android prices every fork at ~25ms and every exec at ~90ms, and the preset apply was paying that price for questions it had already answered: 95 separate stat calls re-proving file metadata one field at a time, command substitutions around proc readers whose parsing was already builtin-only, expected firewall rules rebuilt through captures on every verify, and printf — an external binary on Android's mksh — invoked once per line of every publication. One stat capture now serves every metadata question a proof asks (path_meta_capture, consumed by the uid/mode/nlink/size predicates and retired on proof exit); the state-dir proof and a fully-pinned process identity become lock-scoped facts on the same single-writer argument as the owner read cache; proc_starttime/proc_argv0 gain fork-free global-return forms with the printf wrappers kept as seams; the qnum argument check reuses the cmdline snapshot argv0 already paid for; expected firewall rules are built as data instead of captured; and multi-line writers emit once through z2_emit_line, which resolves to mksh's raw print builtin on the device and printf elsewhere. Load-time probes replace the per-call command -v PATH walks, and log stamps reuse one date exec per second via EPOCHREALTIME. Measured on the Pixel 9 Pro XL: preset apply 56s before this series, 40s after the owner read cache, 14.6s now. tests/shell/run.sh passes; the three test seams that named replaced internals (proc_starttime, z2_fw_restore_command, the owner printf format) follow the new forms. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
499 lines
21 KiB
Shell
499 lines
21 KiB
Shell
#!/bin/sh
|
|
set -eu
|
|
|
|
ROOT=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd)
|
|
TMP=${Z2_TEST_TMP:?}
|
|
CASE="$TMP/firewall-reconciler"
|
|
STATE="$CASE/state"
|
|
MOCK="$CASE/bin"
|
|
FW="$CASE/fw"
|
|
|
|
fail() { echo "FAIL: firewall-reconciler: $*" >&2; exit 1; }
|
|
|
|
mkdir -p "$STATE" "$MOCK" "$FW"
|
|
|
|
# Real backends re-render committed rules in their own save format instead of
|
|
# echoing the authored batch text: libxt_connbytes saves --connbytes-mode
|
|
# before --connbytes-dir, and nft-backed builds translate a single-port
|
|
# multiport match into the plain tcp match. Both mocks re-render through this
|
|
# shared filter so verification is exercised against kernel output, not
|
|
# against the module's own serialization.
|
|
cat > "$CASE/render-saved-rules" <<'EOF'
|
|
#!/bin/sh
|
|
sed \
|
|
-e 's/--connbytes \([0-9:]*\) --connbytes-dir \([a-z]*\) --connbytes-mode \([a-z]*\)/--connbytes \1 --connbytes-mode \3 --connbytes-dir \2/' \
|
|
-e 's/-p tcp -m multiport --dports \([0-9]*\) /-p tcp -m tcp --dport \1 /'
|
|
EOF
|
|
chmod 0755 "$CASE/render-saved-rules"
|
|
|
|
cat > "$MOCK/iptables" <<'EOF'
|
|
#!/bin/sh
|
|
state="$Z2_MOCK_FW"
|
|
args=" $* "
|
|
printf '%s\n' "$*" >> "$state/iptables.args"
|
|
case "$args" in
|
|
*' -t mangle -L OUTPUT -n '*) exit 0 ;;
|
|
*' -t mangle -C OUTPUT -j ZAPRET2_OUT '*) [ -f "$state/anchor.out" ] ;;
|
|
*' -t mangle -C INPUT -j ZAPRET2_IN '*) [ -f "$state/anchor.in" ] ;;
|
|
*' -t mangle -D OUTPUT -j ZAPRET2_OUT '*) rm -f "$state/anchor.out" ;;
|
|
*' -t mangle -D INPUT -j ZAPRET2_IN '*) rm -f "$state/anchor.in" ;;
|
|
*' -t mangle -S ZAPRET2_OUT '*)
|
|
[ -f "$state/chain.out" ] || exit 1
|
|
echo '-N ZAPRET2_OUT'
|
|
[ ! -f "$state/rules.out" ] || cat "$state/rules.out"
|
|
;;
|
|
*' -t mangle -S ZAPRET2_IN '*)
|
|
[ -f "$state/chain.in" ] || exit 1
|
|
echo '-N ZAPRET2_IN'
|
|
[ ! -f "$state/rules.in" ] || cat "$state/rules.in"
|
|
;;
|
|
*' -t mangle -C ZAPRET2_OUT '*)
|
|
needle="-A ZAPRET2_OUT ${args#* -C ZAPRET2_OUT }"
|
|
needle=$(printf '%s\n' "${needle% }" | "$Z2_MOCK_RENDER")
|
|
grep -Fqx -- "$needle" "$state/rules.out"
|
|
;;
|
|
*' -t mangle -C ZAPRET2_IN '*)
|
|
needle="-A ZAPRET2_IN ${args#* -C ZAPRET2_IN }"
|
|
needle=$(printf '%s\n' "${needle% }" | "$Z2_MOCK_RENDER")
|
|
grep -Fqx -- "$needle" "$state/rules.in"
|
|
;;
|
|
*' -t mangle -F ZAPRET2_OUT '*) : > "$state/rules.out" ;;
|
|
*' -t mangle -F ZAPRET2_IN '*) : > "$state/rules.in" ;;
|
|
*' -t mangle -X ZAPRET2_OUT '*)
|
|
[ ! -s "$state/rules.out" ] && [ ! -f "$state/anchor.out" ] || exit 1
|
|
rm -f "$state/chain.out" "$state/rules.out"
|
|
;;
|
|
*' -t mangle -X ZAPRET2_IN '*)
|
|
[ ! -s "$state/rules.in" ] && [ ! -f "$state/anchor.in" ] || exit 1
|
|
rm -f "$state/chain.in" "$state/rules.in"
|
|
;;
|
|
*' -t mangle -S '*)
|
|
[ ! -f "$state/chain.out" ] || echo '-N ZAPRET2_OUT'
|
|
[ ! -f "$state/chain.in" ] || echo '-N ZAPRET2_IN'
|
|
[ ! -f "$state/anchor.out" ] || echo '-A OUTPUT -j ZAPRET2_OUT'
|
|
[ ! -f "$state/anchor.in" ] || echo '-A INPUT -j ZAPRET2_IN'
|
|
[ "${Z2_FOREIGN_REF:-0}" != 1 ] || echo '-A FORWARD -j ZAPRET2_OUT'
|
|
[ ! -s "$state/rules.out" ] || cat "$state/rules.out"
|
|
[ ! -s "$state/rules.in" ] || cat "$state/rules.in"
|
|
;;
|
|
*) exit 1 ;;
|
|
esac
|
|
EOF
|
|
|
|
cat > "$MOCK/iptables-restore" <<'EOF'
|
|
#!/bin/sh
|
|
state="$Z2_MOCK_FW"
|
|
case " $* " in
|
|
*' --help '*)
|
|
count=0
|
|
[ ! -f "$state/restore-help.count" ] ||
|
|
IFS= read -r count < "$state/restore-help.count"
|
|
printf '%s\n' $((count + 1)) > "$state/restore-help.count"
|
|
[ "${Z2_RESTORE_WAIT_SUPPORTED:-1}" = 1 ] &&
|
|
echo 'Usage: iptables-restore [ --wait seconds ] [ --test ] [ --noflush ]' ||
|
|
echo 'Usage: iptables-restore [ --test ] [ --noflush ]'
|
|
exit 0
|
|
;;
|
|
esac
|
|
payload=$(cat)
|
|
count=0
|
|
[ ! -f "$state/restore.count" ] || IFS= read -r count < "$state/restore.count"
|
|
count=$((count + 1))
|
|
printf '%s\n' "$count" > "$state/restore.count"
|
|
printf '%s\n' "$*" >> "$state/restore.args"
|
|
printf '%s\n' "$payload" > "$state/restore.payload.$count"
|
|
if [ -f "$state/lock.remaining" ]; then
|
|
IFS= read -r remaining < "$state/lock.remaining"
|
|
if [ "$remaining" -gt 0 ]; then
|
|
printf '%s\n' $((remaining - 1)) > "$state/lock.remaining"
|
|
echo 'Another app is currently holding the xtables lock.' >&2
|
|
exit 4
|
|
fi
|
|
fi
|
|
case "$payload" in *Z2R_*) exit 90;; esac
|
|
if [ "${Z2_RESTORE_REJECT_CONNBYTES:-0}" = 1 ] &&
|
|
printf '%s\n' "$payload" | grep -q -- '-m connbytes'; then
|
|
echo 'connbytes match is unavailable' >&2
|
|
exit 1
|
|
fi
|
|
if [ "${Z2_RESTORE_REJECT_MARK:-0}" = 1 ] &&
|
|
printf '%s\n' "$payload" | grep -q -- '-m mark'; then
|
|
echo 'Warning: Extension mark revision 0 not supported, missing kernel module?' >&2
|
|
echo 'iptables-restore v1.8.11 (legacy): unknown option "--mark"' >&2
|
|
exit 2
|
|
fi
|
|
if [ "${Z2_RESTORE_REJECT_MULTIPORT:-0}" = 1 ] &&
|
|
printf '%s\n' "$payload" | grep -q -- '-m multiport'; then
|
|
# Verbatim from a device whose kernel lacks xt_multiport: the extension
|
|
# asks the kernel for a match revision while the batch is still being
|
|
# parsed, so the failure lands on --test, and the port list it can no
|
|
# longer parse is reported right after the missing-module warnings.
|
|
echo 'Warning: Extension multiport revision 0 not supported, missing kernel module?' >&2
|
|
echo 'Warning: Extension multiport is not supported, missing kernel module?' >&2
|
|
echo "iptables-restore v1.8.11 (legacy): invalid port/service \`443:65535' specified" >&2
|
|
echo 'Error occurred at line: 4' >&2
|
|
exit 2
|
|
fi
|
|
[ "${Z2_RESTORE_REJECT_ALL:-0}" != 1 ] || {
|
|
printf 'vendor parser rejected ruleset\033[31m\n' >&2
|
|
exit 1
|
|
}
|
|
case " $* " in *' --test '*) exit 0;; esac
|
|
if [ "${Z2_RESTORE_REJECT_CONNBYTES_COMMIT:-0}" = 1 ] &&
|
|
printf '%s\n' "$payload" | grep -q -- '-m connbytes'; then
|
|
echo 'Warning: Extension connbytes revision 0 not supported, missing kernel module?' >&2
|
|
echo 'iptables-restore: line 10 failed' >&2
|
|
exit 1
|
|
fi
|
|
[ "${Z2_RESTORE_FAIL_COMMIT:-0}" != 1 ] || {
|
|
echo 'vendor backend rejected COMMIT' >&2
|
|
exit 1
|
|
}
|
|
if printf '%s\n' "$payload" | grep -Fx -- '-D OUTPUT -j ZAPRET2_OUT' >/dev/null; then
|
|
rm -f "$state/anchor.out"
|
|
fi
|
|
if printf '%s\n' "$payload" | grep -Fx -- '-D INPUT -j ZAPRET2_IN' >/dev/null; then
|
|
rm -f "$state/anchor.in"
|
|
fi
|
|
if printf '%s\n' "$payload" | grep -Fx -- '-X ZAPRET2_OUT' >/dev/null; then
|
|
rm -f "$state/chain.out" "$state/rules.out"
|
|
fi
|
|
if printf '%s\n' "$payload" | grep -Fx -- '-X ZAPRET2_IN' >/dev/null; then
|
|
rm -f "$state/chain.in" "$state/rules.in"
|
|
fi
|
|
if printf '%s\n' "$payload" | grep -F -- ':ZAPRET2_OUT ' >/dev/null; then
|
|
: > "$state/chain.out"
|
|
printf '%s\n' "$payload" | grep -F -- '-A ZAPRET2_OUT ' |
|
|
"$Z2_MOCK_RENDER" > "$state/rules.out" || :
|
|
fi
|
|
if printf '%s\n' "$payload" | grep -F -- ':ZAPRET2_IN ' >/dev/null; then
|
|
: > "$state/chain.in"
|
|
printf '%s\n' "$payload" | grep -F -- '-A ZAPRET2_IN ' |
|
|
"$Z2_MOCK_RENDER" > "$state/rules.in" || :
|
|
fi
|
|
printf '%s\n' "$payload" | grep -Fx -- '-A OUTPUT -j ZAPRET2_OUT' >/dev/null &&
|
|
: > "$state/anchor.out"
|
|
if printf '%s\n' "$payload" | grep -Fx -- '-A INPUT -j ZAPRET2_IN' >/dev/null; then
|
|
: > "$state/anchor.in"
|
|
fi
|
|
if [ "${Z2_CORRUPT_AFTER_COMMIT:-0}" = 1 ] &&
|
|
printf '%s\n' "$payload" | grep -F -- ':ZAPRET2_OUT ' >/dev/null; then
|
|
sed '$d' "$state/rules.out" > "$state/rules.out.corrupt"
|
|
mv "$state/rules.out.corrupt" "$state/rules.out"
|
|
fi
|
|
EOF
|
|
|
|
chmod 0755 "$MOCK/iptables" "$MOCK/iptables-restore"
|
|
PATH="$MOCK:$PATH"
|
|
STATE_DIR="$STATE"
|
|
Z2_MOCK_FW="$FW"
|
|
Z2_MOCK_RENDER="$CASE/render-saved-rules"
|
|
export PATH STATE_DIR Z2_MOCK_FW Z2_MOCK_RENDER
|
|
|
|
state_path_is_managed_file() {
|
|
case "$1" in "$STATE"/*) return 0;; *) return 1;; esac
|
|
}
|
|
|
|
. "$ROOT/zapret2/scripts/firewall-reconciler.sh"
|
|
|
|
PORTS_TCP=80,443
|
|
PORTS_UDP=443,3478,5349,19302
|
|
TCP_PKT_OUT=20
|
|
TCP_PKT_IN=10
|
|
UDP_PKT_OUT=12
|
|
UDP_PKT_IN=6
|
|
QNUM=200
|
|
DESYNC_MARK=0x40000000
|
|
|
|
z2_fw_reconcile_family iptables || fail "atomic restore reconcile failed"
|
|
[ "$Z2_FW_BACKEND:$Z2_FW_CONNBYTES:$Z2_FW_RULES:$Z2_FW_CHAINS:$Z2_FW_ANCHORS" = restore:1:4:2:2 ] ||
|
|
fail "atomic restore result metadata changed"
|
|
[ -f "$FW/anchor.out" ] && [ -f "$FW/anchor.in" ] ||
|
|
fail "atomic restore did not publish both anchors"
|
|
[ "$(cat "$FW/restore.count")" = 2 ] ||
|
|
fail "atomic restore did not use exactly one test and one commit"
|
|
[ "$(cat "$FW/restore-help.count")" = 1 ] ||
|
|
fail "restore wait capability was probed more than once per backend"
|
|
grep -Fqx -- '--wait 5 --test --noflush' "$FW/restore.args" ||
|
|
fail "native restore lock wait was not used for validation"
|
|
grep -Fqx -- '--wait 5 --noflush' "$FW/restore.args" ||
|
|
fail "native restore lock wait was not used for commit"
|
|
: > "$FW/iptables.args"
|
|
z2_fw_verify_family iptables 1 || fail "published family did not verify"
|
|
[ "$(wc -l < "$FW/iptables.args")" = 1 ] ||
|
|
fail "final family verification used more than one kernel snapshot"
|
|
grep -Fqx -- '-t mangle -S' "$FW/iptables.args" ||
|
|
fail "final family verification did not use one complete mangle snapshot"
|
|
|
|
grep -q -- '--connbytes-mode packets --connbytes-dir original' "$FW/rules.out" ||
|
|
fail "mock backend did not re-render published rules in kernel save order"
|
|
cp "$FW/rules.out" "$FW/rules.out.published"
|
|
printf '%s\n' '-A ZAPRET2_OUT -p tcp -m multiport --dports 80,443 -j RETURN' >> "$FW/rules.out"
|
|
if z2_fw_verify_family iptables 1; then
|
|
fail "foreign rule inside the owned chain was accepted"
|
|
fi
|
|
case "$Z2_FW_VERIFY_DETAIL" in
|
|
*'reason=FOREIGN_OR_UNEXPECTED_RULE'*) ;;
|
|
*) fail "foreign rule lost its typed reason: $Z2_FW_VERIFY_DETAIL" ;;
|
|
esac
|
|
mv "$FW/rules.out.published" "$FW/rules.out"
|
|
z2_fw_verify_family iptables 1 ||
|
|
fail "restored owned chain did not verify after foreign rule removal"
|
|
|
|
z2_fw_cleanup_is_unambiguous iptables || fail "published baseline audit failed"
|
|
z2_fw_save_audit iptables || fail "published baseline audit was not retained"
|
|
PORTS_TCP=443
|
|
z2_fw_reconcile_family iptables audited ||
|
|
fail "audited atomic replacement failed"
|
|
[ "$(cat "$FW/restore.count")" = 4 ] ||
|
|
fail "atomic replacement did not use one test and one commit"
|
|
for command in \
|
|
'-D OUTPUT -j ZAPRET2_OUT' \
|
|
'-D INPUT -j ZAPRET2_IN' \
|
|
'-F ZAPRET2_OUT' \
|
|
'-X ZAPRET2_OUT' \
|
|
':ZAPRET2_OUT - [0:0]'; do
|
|
grep -Fqx -- "$command" "$FW/restore.payload.4" ||
|
|
fail "atomic replacement batch omitted: $command"
|
|
done
|
|
PORTS_TCP=80,443
|
|
|
|
z2_fw_cleanup_family iptables || fail "stable namespace cleanup failed"
|
|
z2_fw_cleanup_family iptables || fail "stable namespace cleanup is not idempotent"
|
|
z2_fw_family_absent iptables || fail "stable namespace remains after cleanup"
|
|
|
|
rm -f "$FW"/*
|
|
Z2_RESTORE_REJECT_CONNBYTES=1
|
|
export Z2_RESTORE_REJECT_CONNBYTES
|
|
z2_fw_reconcile_family iptables || fail "connbytes fallback reconcile failed"
|
|
[ "$Z2_FW_CONNBYTES:$Z2_FW_RULES:$Z2_FW_CHAINS:$Z2_FW_ANCHORS" = 0:2:1:1 ] ||
|
|
fail "outgoing-only fallback metadata changed"
|
|
[ -f "$FW/anchor.out" ] && [ ! -f "$FW/anchor.in" ] ||
|
|
fail "outgoing-only fallback published an input anchor"
|
|
case "$Z2_FW_FALLBACK_DETAIL" in
|
|
*'connbytes match is unavailable'*) ;;
|
|
*) fail "connbytes fallback diagnostic was not preserved" ;;
|
|
esac
|
|
unset Z2_RESTORE_REJECT_CONNBYTES
|
|
z2_fw_cleanup_family iptables || fail "fallback cleanup failed"
|
|
|
|
rm -f "$FW"/*
|
|
Z2_RESTORE_REJECT_CONNBYTES_COMMIT=1
|
|
export Z2_RESTORE_REJECT_CONNBYTES_COMMIT
|
|
z2_fw_reconcile_family iptables ||
|
|
fail "commit-time kernel connbytes rejection did not fall back"
|
|
[ "$Z2_FW_CONNBYTES:$Z2_FW_RULES:$Z2_FW_CHAINS:$Z2_FW_ANCHORS" = 0:2:1:1 ] ||
|
|
fail "commit-time connbytes fallback metadata changed"
|
|
[ -f "$FW/anchor.out" ] && [ ! -f "$FW/anchor.in" ] ||
|
|
fail "commit-time connbytes fallback published an input anchor"
|
|
[ "$(cat "$FW/restore.count")" = 4 ] ||
|
|
fail "commit-time connbytes fallback did not use one extra test and commit"
|
|
case "$Z2_FW_FALLBACK_DETAIL" in
|
|
*'commit failed'*'connbytes revision 0 not supported'*) ;;
|
|
*) fail "commit-time connbytes fallback diagnostic was not preserved" ;;
|
|
esac
|
|
unset Z2_RESTORE_REJECT_CONNBYTES_COMMIT
|
|
z2_fw_cleanup_family iptables || fail "commit-time fallback cleanup failed"
|
|
|
|
rm -f "$FW"/*
|
|
Z2_RESTORE_WAIT_SUPPORTED=0
|
|
export Z2_RESTORE_WAIT_SUPPORTED
|
|
z2_fw_reset_restore_wait_capabilities
|
|
z2_fw_reconcile_family iptables || fail "legacy restore without --wait failed"
|
|
if grep -Fq -- '--wait' "$FW/restore.args"; then
|
|
fail "legacy restore received unsupported --wait option"
|
|
fi
|
|
[ "$(cat "$FW/restore.count")" = 2 ] ||
|
|
fail "legacy restore did not use one validation and one commit"
|
|
z2_fw_cleanup_family iptables || fail "legacy restore cleanup failed"
|
|
|
|
rm -f "$FW"/*
|
|
printf '%s\n' 2 > "$FW/lock.remaining"
|
|
z2_fw_lock_retry_pause() { :; }
|
|
z2_fw_reconcile_family iptables || fail "legacy xtables lock wait did not recover"
|
|
[ "$(cat "$FW/restore.count")" = 4 ] ||
|
|
fail "legacy xtables lock wait did not retry only the two lock failures"
|
|
z2_fw_cleanup_family iptables || fail "legacy lock-wait cleanup failed"
|
|
|
|
rm -f "$FW"/*
|
|
printf '%s\n' 20 > "$FW/lock.remaining"
|
|
if z2_fw_reconcile_family iptables; then
|
|
fail "exhausted legacy xtables lock wait was accepted"
|
|
fi
|
|
[ "$Z2_FW_FAILURE_CLASS" = LOCK_TIMEOUT ] ||
|
|
fail "exhausted legacy xtables lock wait lost its failure class"
|
|
[ "$(cat "$FW/restore.count")" = 6 ] ||
|
|
fail "legacy xtables lock wait was not bounded to five seconds"
|
|
z2_fw_family_absent iptables || fail "lock timeout left live firewall state"
|
|
unset Z2_RESTORE_WAIT_SUPPORTED
|
|
z2_fw_reset_restore_wait_capabilities
|
|
|
|
rm -f "$FW"/*
|
|
Z2_RESTORE_REJECT_ALL=1
|
|
export Z2_RESTORE_REJECT_ALL
|
|
if z2_fw_reconcile_family iptables; then
|
|
fail "unsupported baseline ruleset was accepted"
|
|
fi
|
|
[ "$Z2_FW_FAILURE_CLASS" = RULESET_REJECTED ] ||
|
|
fail "unsupported baseline ruleset lost its failure class"
|
|
case "$Z2_FW_ERROR_DETAIL" in
|
|
*'test failed'*'vendor parser rejected ruleset'*)
|
|
case "$Z2_FW_ERROR_DETAIL" in
|
|
*"$(printf '\033')"*) fail "backend control character escaped normalization" ;;
|
|
esac
|
|
;;
|
|
*) fail "unsupported baseline ruleset diagnostic was discarded" ;;
|
|
esac
|
|
z2_fw_family_absent iptables || fail "unsupported baseline ruleset left live firewall state"
|
|
unset Z2_RESTORE_REJECT_ALL
|
|
|
|
rm -f "$FW"/*
|
|
Z2_RESTORE_FAIL_COMMIT=1
|
|
export Z2_RESTORE_FAIL_COMMIT
|
|
if z2_fw_reconcile_family iptables; then
|
|
fail "failed COMMIT was accepted"
|
|
fi
|
|
[ "$(cat "$FW/restore.count")" = 2 ] ||
|
|
fail "failed COMMIT incorrectly retried with a degraded topology"
|
|
z2_fw_family_absent iptables || fail "failed COMMIT left live firewall state"
|
|
[ "$Z2_FW_FAILURE_CLASS" = PUBLICATION_FAILED ] ||
|
|
fail "failed COMMIT did not retain its failure class"
|
|
case "$Z2_FW_ERROR_DETAIL" in
|
|
*'commit failed'*'vendor backend rejected COMMIT'*) ;;
|
|
*) fail "failed COMMIT diagnostic was discarded" ;;
|
|
esac
|
|
unset Z2_RESTORE_FAIL_COMMIT
|
|
|
|
rm -f "$FW"/*
|
|
Z2_CORRUPT_AFTER_COMMIT=1
|
|
export Z2_CORRUPT_AFTER_COMMIT
|
|
if z2_fw_reconcile_family iptables; then
|
|
fail "corrupted post-publication topology was accepted"
|
|
fi
|
|
[ "$Z2_FW_FAILURE_CLASS" = POSTCONDITION_FAILED ] ||
|
|
fail "post-publication mismatch lost its failure class"
|
|
case "$Z2_FW_ERROR_DETAIL" in
|
|
*'reason=OUT_RULE_COUNT:1'*) ;;
|
|
*) fail "post-publication mismatch lost its typed reason: $Z2_FW_ERROR_DETAIL" ;;
|
|
esac
|
|
z2_fw_family_absent iptables ||
|
|
fail "post-publication mismatch did not converge to absent state"
|
|
unset Z2_CORRUPT_AFTER_COMMIT
|
|
|
|
if find "$STATE" -maxdepth 1 -type f -name 'firewall-*' | grep -q .; then
|
|
fail "firewall transaction left private temporary files"
|
|
fi
|
|
|
|
: > "$FW/chain.out"
|
|
: > "$FW/rules.out"
|
|
: > "$FW/anchor.out"
|
|
z2_fw_cleanup_family iptables || fail "interrupted publication did not converge to absent"
|
|
z2_fw_family_absent iptables || fail "interrupted publication retained stable state"
|
|
|
|
: > "$FW/chain.out"
|
|
: > "$FW/rules.out"
|
|
Z2_FOREIGN_REF=1
|
|
export Z2_FOREIGN_REF
|
|
if z2_fw_cleanup_family iptables; then
|
|
fail "foreign reference to stable namespace was accepted"
|
|
fi
|
|
[ -f "$FW/chain.out" ] || fail "foreign-reference preflight partially mutated its chain"
|
|
unset Z2_FOREIGN_REF
|
|
z2_fw_cleanup_family iptables || fail "stable namespace did not recover after foreign reference disappeared"
|
|
|
|
# A kernel without xt_multiport rejects the batch while it is still parsing it.
|
|
# The port list has no single-rule form there, so each interval becomes its own
|
|
# rule, the published record stops claiming an extension that was never used,
|
|
# and verification has to accept the wider shape without letting anything
|
|
# foreign through.
|
|
z2_fw_cleanup_family iptables || fail "could not reset before the multiport case"
|
|
PORTS_TCP=80,443
|
|
PORTS_UDP=443:65535
|
|
Z2_RESTORE_REJECT_MULTIPORT=1
|
|
export Z2_RESTORE_REJECT_MULTIPORT
|
|
z2_fw_reconcile_family iptables ||
|
|
fail "a kernel without multiport could not publish any ruleset at all"
|
|
[ "$Z2_FW_MULTIPORT" = 0 ] || fail "the published record still claims the multiport extension"
|
|
[ "$Z2_FW_CONNBYTES" = 1 ] ||
|
|
fail "a multiport rejection spent the connbytes latch its diagnostic never names"
|
|
[ "$Z2_FW_RULES" = 6 ] ||
|
|
fail "expected one rule per interval per direction, got $Z2_FW_RULES"
|
|
if grep -q -- '-m multiport' "$FW/rules.out"; then
|
|
fail "the published ruleset still uses the extension the kernel rejected"
|
|
fi
|
|
grep -Fq -- '--dport 80 ' "$FW/rules.out" || fail "the 80 interval got no rule of its own"
|
|
grep -Fq -- '--dport 443 ' "$FW/rules.out" || fail "the 443 interval got no rule of its own"
|
|
grep -Fq -- '--dport 443:65535 ' "$FW/rules.out" || fail "the UDP range was not published"
|
|
z2_fw_verify_family iptables "$Z2_FW_CONNBYTES" "$Z2_FW_MULTIPORT" ||
|
|
fail "the split ruleset did not verify"
|
|
cp "$FW/rules.out" "$FW/rules.out.split"
|
|
printf '%s\n' '-A ZAPRET2_OUT -p tcp --dport 8080 -j RETURN' >> "$FW/rules.out"
|
|
if z2_fw_verify_family iptables "$Z2_FW_CONNBYTES" "$Z2_FW_MULTIPORT"; then
|
|
fail "a foreign rule was accepted once the ruleset was split per interval"
|
|
fi
|
|
mv "$FW/rules.out.split" "$FW/rules.out"
|
|
unset Z2_RESTORE_REJECT_MULTIPORT
|
|
|
|
# The 15-value limit is a userspace parser rule, not a kernel capability, so a
|
|
# list that cannot fit never reaches the backend as a multiport rule at all —
|
|
# there is nothing to reject and nothing to fall back from.
|
|
z2_fw_cleanup_family iptables || fail "could not reset before the port limit case"
|
|
PORTS_TCP=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16
|
|
PORTS_UDP=443
|
|
: > "$FW/restore.count"
|
|
printf '0\n' > "$FW/restore.count"
|
|
z2_fw_reconcile_family iptables || fail "an oversized port list could not publish"
|
|
[ "$Z2_FW_MULTIPORT" = 0 ] || fail "a port list past the limit was still offered to multiport"
|
|
[ "$(grep -c -- '-A ZAPRET2_OUT ' "$FW/rules.out")" = 17 ] ||
|
|
fail "an oversized list did not become one rule per interval"
|
|
[ "$(cat "$FW/restore.count")" = 2 ] ||
|
|
fail "the limit was discovered by rejection instead of being computed"
|
|
|
|
# A range spends two of the fifteen. Eight ranges are eight intervals but weigh
|
|
# sixteen, so counting intervals instead of values would wrongly keep multiport.
|
|
z2_fw_cleanup_family iptables || fail "could not reset before the range weight case"
|
|
PORTS_TCP=1:2,3:4,5:6,7:8,9:10,11:12,13:14,15:16
|
|
PORTS_UDP=443
|
|
z2_fw_reconcile_family iptables || fail "eight ranges could not publish"
|
|
[ "$Z2_FW_MULTIPORT" = 0 ] || fail "a range was counted as one value instead of two"
|
|
|
|
# Some extensions have no alternative. Without the mark match the module would
|
|
# re-queue the packets it reinjects itself, so refusing is the only correct
|
|
# answer — but the refusal has to say which extension is missing instead of
|
|
# handing the user several lines of backend warnings to interpret.
|
|
z2_fw_cleanup_family iptables || fail "could not reset before the required extension case"
|
|
PORTS_TCP=80,443
|
|
PORTS_UDP=443
|
|
Z2_RESTORE_REJECT_MARK=1
|
|
export Z2_RESTORE_REJECT_MARK
|
|
if z2_fw_reconcile_family iptables; then
|
|
fail "a kernel without the mark match still published a ruleset"
|
|
fi
|
|
case "$Z2_FW_ERROR_DETAIL" in
|
|
*'does not provide the mark extension'*) ;;
|
|
*) fail "the missing extension was not named: $Z2_FW_ERROR_DETAIL" ;;
|
|
esac
|
|
z2_fw_family_absent iptables ||
|
|
fail "a refused required extension left owned objects behind"
|
|
unset Z2_RESTORE_REJECT_MARK
|
|
|
|
# With the extension present and the list within the limit nothing changes.
|
|
z2_fw_cleanup_family iptables || fail "could not reset before the unchanged case"
|
|
PORTS_TCP=80,443
|
|
PORTS_UDP=443:65535
|
|
z2_fw_reconcile_family iptables || fail "a kernel with multiport failed to publish"
|
|
[ "$Z2_FW_MULTIPORT" = 1 ] || fail "multiport was abandoned on a kernel that supports it"
|
|
[ "$Z2_FW_RULES" = 4 ] || fail "the multiport ruleset changed shape"
|
|
grep -q -- '-m multiport' "$FW/rules.out" ||
|
|
fail "the extension was not used where it is available"
|
|
z2_fw_cleanup_family iptables || fail "could not tear down after the multiport cases"
|
|
|
|
z2_fw_restore_command_read() { Z2_FW_RESTORE_COMMAND=missing-iptables-restore; }
|
|
set +e
|
|
z2_fw_reconcile_family iptables
|
|
rc=$?
|
|
set -e
|
|
[ "$rc" = 3 ] || fail "missing restore backend did not return capability status"
|
|
z2_fw_family_absent iptables || fail "missing restore backend mutated firewall state"
|
|
|
|
echo "Firewall reconciler shell tests passed"
|