magisk-zapret2/tests/shell/firewall-reconciler.sh
loop-uh 852d42c02e Answer in builtins what the replace path kept re-buying in forks
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>
2026-07-25 16:55:22 +03:00

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"