magisk-zapret2/tests/shell/full-rollback.sh
loop-uh 71be18a1be
Some checks failed
Background release validation / Prepare Version (push) Successful in 2s
Background release validation / Fetch latest upstream zapret2 release (push) Successful in 5s
Magisk source guards / guards (push) Successful in 2s
Background release validation / Shell integration tests (push) Failing after 57s
Background release validation / Build Android APK (push) Has been skipped
Background release validation / Package Magisk Module (push) Has been skipped
Изолировать проверку отката от окружения CI
2026-08-07 02:56:56 +03:00

475 lines
22 KiB
Shell

#!/bin/sh
set -eu
ROOT=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd)
TMP=${Z2_TEST_TMP:?}
CASE="$TMP/full-rollback"
MOD="$CASE/module"
STATE="$CASE/state"
MOCK="$CASE/bin"
LOG="$CASE/iptables.mutations"
OUT="$CASE/out"
# The DNS manager publishes from /data and the module bind-mounts it, so the
# rollback preserves this path rather than one inside the module tree.
HOSTS_DIR="$CASE/zapret2-hosts"
export HOSTS_OVERLAY_DIR="$HOSTS_DIR"
fail() { echo "FAIL: rollback: $*" >&2; exit 1; }
assert_line() { grep -Fxq -- "$2" "$1" || fail "missing $2"; }
mkdir -p "$MOD/zapret2/scripts" "$MOD/zapret2/lists" "$MOD/system/etc" "$STATE" "$MOCK" "$HOSTS_DIR"
chmod 0700 "$STATE"
cp "$ROOT/zapret2/scripts/common.sh" "$MOD/zapret2/scripts/common.sh"
cp "$ROOT/zapret2/scripts/firewall-reconciler.sh" "$MOD/zapret2/scripts/firewall-reconciler.sh"
cp "$ROOT/zapret2/scripts/zapret-full-rollback.sh" "$MOD/zapret2/scripts/zapret-full-rollback.sh"
cp "$ROOT/zapret2/runtime.ini" "$MOD/zapret2/runtime.ini"
cat >> "$MOD/zapret2/runtime.ini" <<'EOF'
[dns_manager]
dns_preset_index=2
selected_dns=one|two
selected_direct=three
[strategy_order]
tcp=custom-user-order
EOF
printf '%s\n' 'user-list-content' > "$MOD/zapret2/lists/user.txt"
printf '%s\n' '127.0.0.1 localhost' '1.1.1.1 example.test' > "$HOSTS_DIR/hosts"
chmod 0644 "$MOD/zapret2/runtime.ini" "$HOSTS_DIR/hosts"
cat > "$MOD/zapret2/install-generation.meta" <<EOF
version=1
module_dir=$MOD
generation=test-install-generation
archive_sha256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
EOF
chmod 0600 "$MOD/zapret2/install-generation.meta"
cat > "$MOCK/iptables" <<'EOF'
#!/bin/sh
if [ "${Z2_MOCK_AMBIGUOUS:-0}" = 1 ]; then
case "$*" in
*"-S ${Z2_MOCK_OUT_CHAIN:?}") echo "-N $Z2_MOCK_OUT_CHAIN"; exit 0 ;;
*'-S Z2I_'*|*'-S ZAPRET2_PROBE') exit 1 ;;
*'-S') echo "-N $Z2_MOCK_OUT_CHAIN"; echo "-A FORWARD -j $Z2_MOCK_OUT_CHAIN"; exit 0 ;;
esac
fi
case "$*" in
*' -L OUTPUT -n') exit 0 ;;
*' -F '*|*' -X '*|*' -D '*) echo "$*" >> "${Z2_MOCK_LOG:?}"; exit 0 ;;
*'-S ZAPRET2_OUT'|*'-S ZAPRET2_IN'|*'-S ZAPRET2_PROBE') exit 1 ;;
*'-S') exit 0 ;;
*) exit 1 ;;
esac
EOF
cp "$MOCK/iptables" "$MOCK/ip6tables"
# The rollback contract requires both halves of the Android firewall backend:
# the query frontend and its atomic restore command. This fixture exercises
# an already-empty ruleset, so the restore mocks must be discoverable even
# though a correct cleanup never executes them. Keeping them inside PATH
# makes the test independent of whether the Linux CI image happens to ship
# host iptables tools.
cp "$MOCK/iptables" "$MOCK/iptables-restore"
cp "$MOCK/iptables" "$MOCK/ip6tables-restore"
cat > "$MOCK/sync" <<'EOF'
#!/bin/sh
[ "${Z2_MOCK_SYNC_FAIL:-0}" != 1 ] || exit 1
if [ -n "${Z2_MOCK_SYNC_COUNT:-}" ]; then
count=0
[ ! -f "$Z2_MOCK_SYNC_COUNT" ] || IFS= read -r count < "$Z2_MOCK_SYNC_COUNT"
count=$((count + 1))
printf '%s\n' "$count" > "$Z2_MOCK_SYNC_COUNT"
if [ "$count" = "${Z2_MOCK_SYNC_SIGNAL_AT:-0}" ]; then
kill -TERM "$PPID"
fi
fi
exit 0
EOF
chmod 0755 "$MOCK/iptables" "$MOCK/ip6tables" \
"$MOCK/iptables-restore" "$MOCK/ip6tables-restore"
chmod 0755 "$MOCK/sync"
before_list=$(sha256sum "$MOD/zapret2/lists/user.txt" | awk '{print $1}')
# Every noncanonical recovery namespace is evidence, never scratch space for
# rollback. Exercise lifecycle, dot-temp, reaper and quarantine forms.
for recovery_artifact in \
lifecycle.lock.reaper.recovery.orphan lifecycle.lock.quarantine.orphan \
.full-rollback.transaction.orphan full-rollback.meta.tmp.orphan; do
: > "$STATE/$recovery_artifact"
chmod 0600 "$STATE/$recovery_artifact"
set +e
PATH="$MOCK:$PATH" STATE_DIR="$STATE" Z2_MOCK_LOG="$LOG" sh "$MOD/zapret2/scripts/zapret-full-rollback.sh" --machine > "$OUT"
rc=$?
set -e
[ "$rc" = 2 ] || fail "recovery artifact was not blocked: $recovery_artifact"
assert_line "$OUT" 'Z2_RB_STATUS=blocked'
[ -f "$STATE/$recovery_artifact" ] || fail "rollback deleted recovery evidence: $recovery_artifact"
[ ! -e "$MOD/disable" ] || fail "artifact preflight mutated disable fence: $recovery_artifact"
rm -f "$STATE/$recovery_artifact"
done
# A malformed rollback journal is recovery evidence. It must fail closed before
# autostart or the module disable fence is changed, and it must remain available
# for diagnosis instead of being overwritten by a fresh transaction.
cat > "$STATE/full-rollback.transaction" <<EOF
version=1
module_dir=$MOD
token=malformed-journal
phase=unknown
EOF
chmod 0600 "$STATE/full-rollback.transaction"
set +e
PATH="$MOCK:$PATH" STATE_DIR="$STATE" Z2_MOCK_LOG="$LOG" sh "$MOD/zapret2/scripts/zapret-full-rollback.sh" --machine > "$OUT"
rc=$?
set -e
[ "$rc" = 2 ] || fail "malformed rollback journal was not blocked"
[ "$(wc -l < "$OUT")" = 10 ] || fail "blocked journal output is not exactly ten fields"
assert_line "$OUT" 'Z2_RB_STATUS=blocked'
assert_line "$OUT" 'Z2_RB_COMPLETE=1'
[ -f "$STATE/full-rollback.transaction" ] || fail "malformed rollback journal was discarded"
[ ! -e "$MOD/disable" ] || fail "malformed journal preflight mutated disable fence"
grep -Fqx 'autostart=1' "$MOD/zapret2/runtime.ini" || fail "malformed journal preflight mutated runtime.ini"
rm -f "$STATE/full-rollback.transaction"
# A failed durability barrier retains the exact disable fence and journal.
exec 9< "$HOSTS_DIR/hosts"
hosts_inode_before=$(stat -c %i "$HOSTS_DIR/hosts")
hosts_mode_before=$(stat -c %a "$HOSTS_DIR/hosts")
set +e
PATH="$MOCK:$PATH" STATE_DIR="$STATE" Z2_MOCK_LOG="$LOG" Z2_MOCK_SYNC_FAIL=1 sh "$MOD/zapret2/scripts/zapret-full-rollback.sh" --machine > "$OUT"
rc=$?
set -e
[ "$rc" = 1 ] || fail "sync failure did not fail rollback"
assert_line "$OUT" 'Z2_RB_STATUS=error'
[ -f "$MOD/disable" ] && [ -f "$STATE/full-rollback.transaction" ] || fail "sync failure did not retain fence/journal"
# Interrupt at the process-clean durability barrier. No hosts backup artifact
# may be created before that recovery phase has been synchronized.
rm -f "$CASE/sync.count"
set +e
PATH="$MOCK:$PATH" STATE_DIR="$STATE" Z2_MOCK_LOG="$LOG" \
Z2_MOCK_SYNC_COUNT="$CASE/sync.count" Z2_MOCK_SYNC_SIGNAL_AT=2 \
sh "$MOD/zapret2/scripts/zapret-full-rollback.sh" --machine > "$OUT.signal-process"
rc=$?
set -e
[ "$rc" = 1 ] || fail "process-clean sync signal did not interrupt rollback"
assert_line "$OUT.signal-process" 'Z2_RB_STATUS=partial'
[ -f "$HOSTS_DIR/hosts" ] && [ ! -e "$STATE/hosts.rollback.backup" ] || fail "hosts were touched before process-clean phase durability"
grep -Fqx 'phase=process-clean' "$STATE/full-rollback.transaction" || fail "process-clean phase was not retained"
# The next sync is the backup-publication durability barrier. Source must still
# exist beside the verified backup and the process-clean journal is recoverable.
rm -f "$CASE/sync.count"
set +e
PATH="$MOCK:$PATH" STATE_DIR="$STATE" Z2_MOCK_LOG="$LOG" \
Z2_MOCK_SYNC_COUNT="$CASE/sync.count" Z2_MOCK_SYNC_SIGNAL_AT=2 \
sh "$MOD/zapret2/scripts/zapret-full-rollback.sh" --machine > "$OUT.signal-hosts"
rc=$?
set -e
[ "$rc" = 1 ] || fail "hosts publication signal did not interrupt rollback"
assert_line "$OUT.signal-hosts" 'Z2_RB_STATUS=partial'
[ -f "$HOSTS_DIR/hosts" ] && [ -f "$STATE/hosts.rollback.backup" ] || fail "source was unlinked before backup durability/journal ordering"
grep -Fqx 'phase=process-clean' "$STATE/full-rollback.transaction" || fail "signal regressed or advanced an uncommitted hosts phase"
# The generation being rolled back committed a status snapshot claiming an
# owned, IPv6-active service. It describes what the rollback is about to
# remove, so it must not survive the rollback.
cat > "$STATE/status.snapshot" <<EOF
status=ok
boot_id=$(cat /proc/sys/kernel/random/boot_id)
ipv4_active=1
ipv6_active=1
ruleset_verified=1
owner_metadata_verified=1
qnum=200
EOF
chmod 0600 "$STATE/status.snapshot"
PATH="$MOCK:$PATH" STATE_DIR="$STATE" Z2_MOCK_LOG="$LOG" sh "$MOD/zapret2/scripts/zapret-full-rollback.sh" --machine > "$OUT"
[ "$(wc -l < "$OUT")" = 10 ] || fail "machine output is not exactly ten fields"
assert_line "$OUT" 'Z2_RB_STATUS=complete'
assert_line "$OUT" 'Z2_RB_PROCESS_CLEAN=1'
assert_line "$OUT" 'Z2_RB_FIREWALL_CLEAN=1'
assert_line "$OUT" 'Z2_RB_ROLLBACK_ARMED=1'
assert_line "$OUT" 'Z2_RB_HOSTS_PRESERVED=1'
assert_line "$OUT" 'Z2_RB_REBOOT_REQUIRED=1'
assert_line "$OUT" 'Z2_RB_USER_DATA_PRESERVED=1'
assert_line "$OUT" 'Z2_RB_COMPLETE=1'
[ -f "$MOD/disable" ] && [ ! -L "$MOD/disable" ] || fail "disable fence missing"
[ -f "$STATE/hosts.rollback.backup" ] || fail "hosts backup missing"
grep -Fqx '1.1.1.1 example.test' "$STATE/hosts.rollback.backup" || fail "hosts bytes not preserved"
[ ! -e "$HOSTS_DIR/hosts" ] || fail "hosts overlay was not moved"
[ "$(stat -c %a "$STATE/hosts.rollback.backup")" = 600 ] || fail "hosts backup is not root-private"
[ "$(stat -c %i "$STATE/hosts.rollback.backup")" != "$hosts_inode_before" ] || fail "hosts live inode was moved instead of copied"
[ "$(stat -Lc %i "/proc/$$/fd/9")" = "$hosts_inode_before" ] || fail "open hosts inode changed"
[ "$(stat -Lc %a "/proc/$$/fd/9")" = "$hosts_mode_before" ] || fail "live hosts inode mode changed"
grep -Fqx '1.1.1.1 example.test' "/proc/$$/fd/9" || fail "open hosts inode contents changed"
exec 9<&-
grep -Fqx 'autostart=0' "$MOD/zapret2/runtime.ini" || fail "autostart was not disabled"
! grep -Eq '^(dns_preset_index|selected_dns|selected_direct)=' "$MOD/zapret2/runtime.ini" || fail "DNS UI selections remain"
grep -Fqx 'tcp=custom-user-order' "$MOD/zapret2/runtime.ini" || fail "unrelated runtime data changed"
[ "$before_list" = "$(sha256sum "$MOD/zapret2/lists/user.txt" | awk '{print $1}')" ] || fail "user list changed"
[ -f "$STATE/full-rollback.meta" ] && [ ! -e "$STATE/full-rollback.transaction" ] || fail "commit artifacts invalid"
grep -Fqx 'generation=test-install-generation' "$STATE/full-rollback.meta" || fail "rollback meta does not bind install generation"
grep -Fqx 'archive_sha256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' "$STATE/full-rollback.meta" || fail "rollback meta does not bind archive hash"
# Idempotent completion keeps the original protected backup.
backup_hash=$(sha256sum "$STATE/hosts.rollback.backup" | awk '{print $1}')
PATH="$MOCK:$PATH" STATE_DIR="$STATE" Z2_MOCK_LOG="$LOG" sh "$MOD/zapret2/scripts/zapret-full-rollback.sh" --machine > "$OUT"
assert_line "$OUT" 'Z2_RB_STATUS=complete'
[ "$backup_hash" = "$(sha256sum "$STATE/hosts.rollback.backup" | awk '{print $1}')" ] || fail "idempotent run overwrote hosts backup"
# A signal while waiting for a foreign lifecycle lock still emits the exact
# machine contract and must not retire the foreign lock.
start=$(awk '{print $22}' "/proc/$$/stat")
mkdir "$STATE/lifecycle.lock"
cat > "$STATE/lifecycle.lock/owner" <<EOF
pid=$$
starttime=$start
token=foreign-lock
EOF
chmod 0600 "$STATE/lifecycle.lock/owner"
PATH="$MOCK:$PATH" STATE_DIR="$STATE" LIFECYCLE_LOCK_WAIT_SECONDS=30 Z2_MOCK_LOG="$LOG" \
sh "$MOD/zapret2/scripts/zapret-full-rollback.sh" --machine > "$OUT.signal" &
signal_pid=$!
sleep 1
kill -TERM "$signal_pid"
set +e
wait "$signal_pid"
rc=$?
set -e
[ "$rc" = 1 ] || fail "early signal did not return failure"
[ "$(wc -l < "$OUT.signal")" = 10 ] || fail "early signal output is not exactly ten fields"
assert_line "$OUT.signal" 'Z2_RB_STATUS=partial'
assert_line "$OUT.signal" 'Z2_RB_COMPLETE=1'
[ -f "$STATE/lifecycle.lock/owner" ] || fail "signal cleanup retired a foreign lifecycle lock"
rm -f "$STATE/lifecycle.lock/owner"
rmdir "$STATE/lifecycle.lock"
# A foreign reference to a same-named chain must leave the chain untouched and
# retain the durable fence/journal for recovery.
rm -f "$STATE/full-rollback.meta" "$STATE/legacy-direct-rules.migrated" "$MOD/disable"
rm -rf "$STATE/hosts.rollback.backup"
mkdir -p "$MOD/system/etc"
printf '%s\n' '127.0.0.1 localhost' > "$HOSTS_DIR/hosts"
sed -i 's/^autostart=0$/autostart=1/' "$MOD/zapret2/runtime.ini"
# Publish a real exact-path process and matching owner generation. This makes
# the firewall foreign-reference audit, rather than process preflight, the
# reason rollback stops.
cp "${Z2_TEST_EXECUTABLE_SHELL:-$(command -v sh)}" "$MOD/zapret2/nfqws2"
chmod 0755 "$MOD/zapret2/nfqws2"
"$MOD/zapret2/nfqws2" -c 'trap "exit 0" TERM INT; while :; do sleep 1; done' --qnum=200 &
nf_pid=$!
nf_start=$(awk '{print $22}' "/proc/$nf_pid/stat")
nf_argv_sha256=$(sha256sum "/proc/$nf_pid/cmdline")
nf_argv_sha256=${nf_argv_sha256%% *}
printf '%s\n' "$nf_pid" > "$STATE/nfqws2.pid"
cat > "$STATE/runtime.owner" <<EOF
version=1
module_dir=$MOD
nfqws=$MOD/zapret2/nfqws2
EOF
STATE_DIR="$STATE" SCRIPT_DIR="$MOD/zapret2/scripts" ZAPRET_DIR="$MOD/zapret2" MODDIR="$MOD" \
sh -c '. "$SCRIPT_DIR/common.sh"; QNUM=200; PORTS_TCP=80,443; PORTS_UDP=443; TCP_PKT_OUT=20; TCP_PKT_IN=10; UDP_PKT_OUT=20; UDP_PKT_IN=10; PKT_OUT=20; PKT_IN=10; DESYNC_MARK=0x40000000; IPV4_CONNBYTES=1; IPV4_MULTIPORT=1; IPV4_MARK=1; IPV6_CONNBYTES=1; IPV6_MULTIPORT=1; IPV6_MARK=1; prepare_owner_generation_spec 1 0 && write_owner_state "$1" "$2" "$3" 200 foreign-audit active' \
sh "$nf_pid" "$nf_start" "$nf_argv_sha256" || fail "could not publish valid v8 owner generation"
chmod 0600 "$STATE/nfqws2.pid" "$STATE/runtime.owner" "$STATE/owner.meta"
mock_out_chain="$(awk -F= '$1 == "out_chain" { print $2 }' "$STATE/owner.meta")"
[ -n "$mock_out_chain" ] || fail "owner generation did not publish its output chain"
# Runtime configuration may change after publication; rollback must continue
# to audit/delete only the persisted generation (qnum 200 / original ports).
sed -i 's/^qnum=200$/qnum=333/; s/^ports_tcp=80,443$/ports_tcp=8080,8443/; s/^ports_udp=443$/ports_udp=8443/' "$MOD/zapret2/runtime.ini"
: > "$LOG"
set +e
PATH="$MOCK:$PATH" STATE_DIR="$STATE" Z2_MOCK_LOG="$LOG" Z2_MOCK_AMBIGUOUS=1 \
Z2_MOCK_OUT_CHAIN="$mock_out_chain" \
sh "$MOD/zapret2/scripts/zapret-full-rollback.sh" --machine > "$OUT"
rc=$?
set -e
[ "$rc" = 1 ] || fail "ambiguous chain did not return partial"
assert_line "$OUT" 'Z2_RB_STATUS=partial'
[ ! -s "$LOG" ] || fail "ambiguous chain was mutated"
[ -r "/proc/$nf_pid/stat" ] || fail "foreign-reference audit killed the verified process"
[ -f "$MOD/disable" ] && [ -f "$STATE/full-rollback.transaction" ] || fail "partial rollback did not retain fence/journal"
kill -TERM "$nf_pid" 2>/dev/null || true
wait "$nf_pid" 2>/dev/null || true
# zapret-stop must reject corrupt PID publication before any legacy/current
# firewall mutation.
cp "$ROOT/zapret2/scripts/zapret-stop.sh" "$MOD/zapret2/scripts/zapret-stop.sh"
printf '%s\n' "$$" > "$STATE/nfqws2.pid"
chmod 0600 "$STATE/nfqws2.pid"
rm -f "$STATE/runtime.owner" "$STATE/owner.meta" "$STATE/full-rollback.transaction"
: > "$LOG"
set +e
PATH="$MOCK:$PATH" STATE_DIR="$STATE" Z2_MOCK_LOG="$LOG" sh "$MOD/zapret2/scripts/zapret-stop.sh" > "$OUT.stop"
rc=$?
set -e
[ "$rc" = 1 ] || fail "corrupt PID publication did not block stop"
[ ! -s "$LOG" ] || fail "corrupt PID publication allowed firewall mutation"
# A committed status snapshot describes the generation the rollback dismantled.
# Leaving it behind makes the next observation replay those facts — an owned,
# degraded service — over a module that is disabled and stopped, which the app
# reads as a failed rollback.
grep -Fqx 'status=stopped' "$STATE/status.snapshot" ||
fail "completed rollback left the status snapshot of the generation it removed"
grep -Fqx 'ipv6_active=0' "$STATE/status.snapshot" ||
fail "completed rollback kept an IPv6-active claim from the generation it removed"
grep -Fqx 'ruleset_verified=1' "$STATE/status.snapshot" ||
fail "completed rollback did not record that it verified the teardown"
# An IPv6 frontend that exists but cannot answer must never strand the journal:
# the artifact it would leave behind fences start, stop, uninstall and purge
# until someone deletes the state by hand, and the condition repeats on every
# boot of such a device. The rollback finishes, retires the journal and says
# in the receipt that IPv6 stayed unverified.
rb_ipv6_case() {
# No owner record and no committed receipt: nothing proves this generation
# published nothing over IPv6, so an unqueryable frontend must be reported
# as a reservation rather than skipped silently.
rm -f "$STATE/full-rollback.transaction" "$STATE/full-rollback.meta" \
"$STATE/nfqws2.pid" "$STATE/owner.meta" "$STATE/runtime.owner" \
"$STATE/status.snapshot" "$MOD/disable"
sed 's/^autostart=.*/autostart=1/' "$MOD/zapret2/runtime.ini" > "$MOD/zapret2/runtime.ini.tmp"
mv "$MOD/zapret2/runtime.ini.tmp" "$MOD/zapret2/runtime.ini"
chmod 0644 "$MOD/zapret2/runtime.ini"
cat > "$MOCK/ip6tables" <<'EOF'
#!/bin/sh
exit 42
EOF
chmod 0755 "$MOCK/ip6tables"
}
# A device with no ip6tables frontend at all: the receipt the rollback leaves
# behind must let a repeat run reach the same verdict instead of degrading,
# because nothing about the device changed between the two runs.
rm -f "$STATE/full-rollback.transaction" "$STATE/full-rollback.meta" \
"$STATE/nfqws2.pid" "$STATE/owner.meta" "$STATE/runtime.owner" "$MOD/disable"
mv "$MOCK/ip6tables" "$CASE/ip6tables.hidden"
PATH="$MOCK:$PATH" STATE_DIR="$STATE" Z2_MOCK_LOG="$LOG" \
sh "$MOD/zapret2/scripts/zapret-full-rollback.sh" --machine > "$OUT.noipv6.first"
rm -f "$STATE/full-rollback.meta"
PATH="$MOCK:$PATH" STATE_DIR="$STATE" Z2_MOCK_LOG="$LOG" \
sh "$MOD/zapret2/scripts/zapret-full-rollback.sh" --machine > "$OUT.noipv6.second"
first_verdict=$(grep -E '^Z2_RB_(STATUS|FIREWALL_CLEAN)=' "$OUT.noipv6.first")
second_verdict=$(grep -E '^Z2_RB_(STATUS|FIREWALL_CLEAN)=' "$OUT.noipv6.second")
[ "$first_verdict" = "$second_verdict" ] ||
fail "repeating an unchanged rollback changed its verdict: [$first_verdict] vs [$second_verdict]"
mv "$CASE/ip6tables.hidden" "$MOCK/ip6tables"
rb_ipv6_case
set +e
PATH="$MOCK:$PATH" STATE_DIR="$STATE" Z2_MOCK_LOG="$LOG" \
sh "$MOD/zapret2/scripts/zapret-full-rollback.sh" --machine > "$OUT.ipv6"
rc=$?
set -e
assert_line "$OUT.ipv6" 'Z2_RB_STATUS=partial'
assert_line "$OUT.ipv6" 'Z2_RB_FIREWALL_CLEAN=0'
assert_line "$OUT.ipv6" 'Z2_RB_HOSTS_PRESERVED=1'
[ ! -e "$STATE/full-rollback.transaction" ] ||
fail "an unverifiable IPv6 family stranded the rollback journal"
[ -f "$STATE/full-rollback.meta" ] || fail "rollback did not commit its completion record"
# The resumed branch must reach the same verdict: the journal records which
# phase completed, not why it was allowed to.
rm -f "$STATE/full-rollback.meta"
cat > "$STATE/full-rollback.transaction" <<EOF
version=1
module_dir=$MOD
token=resume-token
phase=firewall-clean
EOF
chmod 0600 "$STATE/full-rollback.transaction"
set +e
PATH="$MOCK:$PATH" STATE_DIR="$STATE" Z2_MOCK_LOG="$LOG" \
sh "$MOD/zapret2/scripts/zapret-full-rollback.sh" --machine > "$OUT.ipv6-resume"
rc=$?
set -e
assert_line "$OUT.ipv6-resume" 'Z2_RB_FIREWALL_CLEAN=0'
[ ! -e "$STATE/full-rollback.transaction" ] ||
fail "a resumed rollback stranded the journal on an unverifiable IPv6 family"
# With a committed receipt proving this generation published no IPv6 rules, the
# same unqueryable frontend hides nothing of ours, so there is no reservation
# to report and the rollback is simply complete.
rm -f "$STATE/full-rollback.transaction" "$STATE/full-rollback.meta" "$MOD/disable"
cat > "$STATE/status.snapshot" <<EOF
status=stopped
boot_id=$(cat /proc/sys/kernel/random/boot_id)
ipv4_active=0
ipv6_active=0
ruleset_verified=1
owner_metadata_verified=0
qnum=200
EOF
chmod 0600 "$STATE/status.snapshot"
PATH="$MOCK:$PATH" STATE_DIR="$STATE" Z2_MOCK_LOG="$LOG" \
sh "$MOD/zapret2/scripts/zapret-full-rollback.sh" --machine > "$OUT.ipv6-proven"
assert_line "$OUT.ipv6-proven" 'Z2_RB_STATUS=complete'
assert_line "$OUT.ipv6-proven" 'Z2_RB_FIREWALL_CLEAN=1'
# A stop on the same device must stay repeatable. Each stop retires the owner
# record that carried the IPv6 answer, so a receipt that withheld verification
# must not make the next stop refuse — otherwise the first successful stop
# poisons every stop until the next reboot, and uninstall inherits the refusal.
rm -f "$STATE/full-rollback.transaction" "$STATE/full-rollback.meta" \
"$STATE/hosts.rollback.backup" "$STATE/status.snapshot" \
"$STATE/owner.meta" "$STATE/nfqws2.pid" "$MOD/disable"
cat > "$MOCK/ip6tables" <<'EOF'
#!/bin/sh
exit 42
EOF
chmod 0755 "$MOCK/ip6tables"
stop_run=1
while [ "$stop_run" -le 3 ]; do
set +e
PATH="$MOCK:$PATH" STATE_DIR="$STATE" Z2_MOCK_LOG="$LOG" \
sh "$MOD/zapret2/scripts/zapret-stop.sh" > "$OUT.stop.$stop_run" 2>&1
stop_rc=$?
set -e
[ "$stop_rc" = 0 ] ||
fail "stop #$stop_run refused on an unqueryable IPv6 family: $(cat "$OUT.stop.$stop_run")"
stop_run=$((stop_run + 1))
done
cp "$MOCK/iptables" "$MOCK/ip6tables"
chmod 0755 "$MOCK/ip6tables"
# The preflight reads the IPv6 family and finds nothing of ours in it, then the
# frontend goes busy before the rollback verifies its own work. Asking the
# kernel again and discarding that proof turns a rollback that proved the
# family empty into one reporting it unverified — and then records an IPv6
# publication its own preflight had disproved. The preflight spends two probes
# and the teardown one, so anything past the third belongs to the verification.
rm -f "$STATE/full-rollback.transaction" "$STATE/full-rollback.meta" \
"$STATE/hosts.rollback.backup" "$STATE/status.snapshot" \
"$STATE/owner.meta" "$STATE/nfqws2.pid" "$STATE/runtime.owner" "$MOD/disable"
sed 's/^autostart=.*/autostart=1/' "$MOD/zapret2/runtime.ini" > "$MOD/zapret2/runtime.ini.tmp"
mv "$MOD/zapret2/runtime.ini.tmp" "$MOD/zapret2/runtime.ini"
chmod 0644 "$MOD/zapret2/runtime.ini"
cat > "$MOCK/ip6tables" <<'EOF'
#!/bin/sh
n=0
[ ! -f "$Z2_IP6_FLIP_COUNT" ] || IFS= read -r n < "$Z2_IP6_FLIP_COUNT"
n=$((n + 1))
printf '%s\n' "$n" > "$Z2_IP6_FLIP_COUNT"
[ "$n" -le "${Z2_IP6_FLIP_AFTER:-3}" ] || exit 42
exec iptables "$@"
EOF
chmod 0755 "$MOCK/ip6tables"
rm -f "$CASE/ip6flip"
set +e
PATH="$MOCK:$PATH" STATE_DIR="$STATE" Z2_MOCK_LOG="$LOG" \
Z2_IP6_FLIP_COUNT="$CASE/ip6flip" Z2_IP6_FLIP_AFTER=3 FIREWALL_PROBE_ATTEMPTS=1 \
sh "$MOD/zapret2/scripts/zapret-full-rollback.sh" --machine > "$OUT.ipv6-lost-proof"
rc=$?
set -e
probe_calls=0
[ ! -f "$CASE/ip6flip" ] || IFS= read -r probe_calls < "$CASE/ip6flip"
[ "$probe_calls" -gt 3 ] ||
fail "the rollback never re-probed IPv6 after teardown; the case proves nothing"
assert_line "$OUT.ipv6-lost-proof" 'Z2_RB_STATUS=complete'
assert_line "$OUT.ipv6-lost-proof" 'Z2_RB_FIREWALL_CLEAN=1'
[ "$rc" = 0 ] ||
fail "a rollback whose own preflight proved the family empty did not complete"
cp "$MOCK/iptables" "$MOCK/ip6tables"
chmod 0755 "$MOCK/ip6tables"
echo "Full rollback shell tests passed"