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
475 lines
22 KiB
Shell
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"
|