The checks that run after a teardown re-ask the kernel whether the IPv6 family is clean. A frontend that goes busy in that window makes the question unanswerable, and the answer this transaction already had — the baseline was captured and the removal committed — was thrown away. A rollback that succeeded then reported artifacts remaining. The teardown now records that it removed the family, and both checks accept it. It stops recording it the moment the removal did not happen: a skipped family and a failed removal both leave the flag clear. Same defect as the rollback's lost preflight proof, one step later in the same transaction, and on the start path too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
555 lines
26 KiB
Shell
555 lines
26 KiB
Shell
#!/system/bin/sh
|
|
# Durable, fail-closed rollback of live Zapret2 effects. User strategy data is retained.
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
. "$SCRIPT_DIR/common.sh"
|
|
|
|
RB_STATUS=error
|
|
RB_PROCESS_CLEAN=0
|
|
RB_FIREWALL_CLEAN=0
|
|
RB_IPV6_UNVERIFIED=0
|
|
RB_STATUS_RECEIPT_FAILED=0
|
|
RB_ROLLBACK_ARMED=0
|
|
RB_HOSTS_PRESERVED=0
|
|
RB_USER_DATA_PRESERVED=1
|
|
RB_LEGACY_AMBIGUOUS=0
|
|
RB_DIAGNOSTIC="rollback failed"
|
|
RB_TOKEN=""
|
|
RB_PHASE=""
|
|
RB_LOCKED=0
|
|
RB_EMITTED=0
|
|
RB_ACTIVE_TEMP=""
|
|
RB_INSTALL_GENERATION=""
|
|
RB_INSTALL_ARCHIVE_SHA256=""
|
|
|
|
safe_diagnostic() {
|
|
printf '%s' "$1" | tr '\r\n' ' ' | sed 's/[^A-Za-z0-9 .,:_+\/@=-]/_/g; s/[[:space:]][[:space:]]*/ /g; s/^ //; s/ $//' | cut -c1-240
|
|
}
|
|
|
|
emit_result() {
|
|
local rc="$1"
|
|
[ "$RB_EMITTED" = 0 ] || exit "$rc"
|
|
RB_EMITTED=1
|
|
RB_DIAGNOSTIC="$(safe_diagnostic "$RB_DIAGNOSTIC")"
|
|
[ -n "$RB_DIAGNOSTIC" ] || RB_DIAGNOSTIC="unspecified"
|
|
printf 'Z2_RB_STATUS=%s\n' "$RB_STATUS"
|
|
printf 'Z2_RB_PROCESS_CLEAN=%s\n' "$RB_PROCESS_CLEAN"
|
|
printf 'Z2_RB_FIREWALL_CLEAN=%s\n' "$RB_FIREWALL_CLEAN"
|
|
printf 'Z2_RB_ROLLBACK_ARMED=%s\n' "$RB_ROLLBACK_ARMED"
|
|
printf 'Z2_RB_HOSTS_PRESERVED=%s\n' "$RB_HOSTS_PRESERVED"
|
|
printf 'Z2_RB_REBOOT_REQUIRED=1\n'
|
|
printf 'Z2_RB_USER_DATA_PRESERVED=%s\n' "$RB_USER_DATA_PRESERVED"
|
|
printf 'Z2_RB_LEGACY_AMBIGUOUS=%s\n' "$RB_LEGACY_AMBIGUOUS"
|
|
printf 'Z2_RB_DIAGNOSTIC=%s\n' "$RB_DIAGNOSTIC"
|
|
printf 'Z2_RB_COMPLETE=1\n'
|
|
exit "$rc"
|
|
}
|
|
|
|
finish_result() {
|
|
local rc="$1"
|
|
trap '' HUP INT TERM
|
|
cleanup_active_temp >/dev/null 2>&1 || true
|
|
if [ "$RB_LOCKED" = 1 ] || [ "${LOCK_HELD:-0}" = 1 ]; then
|
|
release_lifecycle_lock >/dev/null 2>&1 || true
|
|
RB_LOCKED=0
|
|
elif [ -n "${LIFECYCLE_ACQUIRE_CANDIDATE:-}" ]; then
|
|
abort_lifecycle_lock_acquire >/dev/null 2>&1 || true
|
|
fi
|
|
trap - HUP INT TERM
|
|
emit_result "$rc"
|
|
}
|
|
|
|
blocked() { RB_STATUS=blocked; RB_DIAGNOSTIC="$1"; finish_result 2; }
|
|
failed() { RB_STATUS=error; RB_DIAGNOSTIC="$1"; finish_result 1; }
|
|
partial() { RB_STATUS=partial; RB_DIAGNOSTIC="$1"; finish_result 1; }
|
|
|
|
interrupted() {
|
|
trap '' HUP INT TERM
|
|
cleanup_active_temp >/dev/null 2>&1 || true
|
|
if [ "$RB_LOCKED" = 1 ] && is_safe_token "$RB_TOKEN"; then
|
|
# Complete the non-negotiable fence even when a catchable signal lands
|
|
# between its individual atomic publications.
|
|
arm_runtime_config >/dev/null 2>&1 || true
|
|
arm_disable >/dev/null 2>&1 || true
|
|
if regular_root_file "$MODDIR/disable"; then
|
|
if [ -e "$FULL_ROLLBACK_TRANSACTION" ] || [ -L "$FULL_ROLLBACK_TRANSACTION" ]; then
|
|
# Never regress a durable recovery phase when a signal lands
|
|
# during hosts publication/removal or the final commit.
|
|
if read_transaction >/dev/null 2>&1 && durability_sync >/dev/null 2>&1; then
|
|
RB_ROLLBACK_ARMED=1
|
|
fi
|
|
else
|
|
case "$RB_PHASE" in
|
|
armed|legacy-clean|firewall-clean|process-clean|hosts-backed-up|hosts-preserved)
|
|
signal_phase="$RB_PHASE" ;;
|
|
*) signal_phase=armed ;;
|
|
esac
|
|
if write_transaction "$signal_phase" >/dev/null 2>&1 && durability_sync >/dev/null 2>&1; then
|
|
RB_ROLLBACK_ARMED=1
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
RB_STATUS=partial
|
|
if [ "$RB_ROLLBACK_ARMED" = 1 ]; then
|
|
RB_DIAGNOSTIC="rollback interrupted; durable disable fence and recovery journal retained"
|
|
else
|
|
RB_DIAGNOSTIC="rollback interrupted before the durable fence was fully verified"
|
|
fi
|
|
finish_result 1
|
|
}
|
|
|
|
regular_root_file() { [ -f "$1" ] && [ ! -L "$1" ] && path_uid_is_root "$1"; }
|
|
|
|
durability_sync() { command -v sync >/dev/null 2>&1 && sync >/dev/null 2>&1; }
|
|
|
|
cleanup_active_temp() {
|
|
local path="$RB_ACTIVE_TEMP"
|
|
[ -n "$path" ] || return 0
|
|
case "$path" in
|
|
"$STATE_DIR"/*|"$ZAPRET_DIR"/*|"$MODDIR"/.disable.full-rollback.*) ;;
|
|
*) return 1 ;;
|
|
esac
|
|
if [ -e "$path" ] || [ -L "$path" ]; then
|
|
[ -f "$path" ] && [ ! -L "$path" ] && path_uid_is_root "$path" || return 1
|
|
rm -f "$path" 2>/dev/null || return 1
|
|
fi
|
|
RB_ACTIVE_TEMP=""
|
|
}
|
|
|
|
new_temp_nonce() {
|
|
local value
|
|
value="$(new_lifecycle_token)" || return 1
|
|
is_safe_token "$value" || return 1
|
|
printf '%s\n' "$value"
|
|
}
|
|
|
|
read_transaction() {
|
|
local key value version="" module="" token="" phase="" seen=""
|
|
regular_root_file "$FULL_ROLLBACK_TRANSACTION" || return 1
|
|
while IFS='=' read -r key value; do
|
|
case "$key" in
|
|
version) case "$seen" in *v*) return 1;; esac; version="$value"; seen="${seen}v" ;;
|
|
module_dir) case "$seen" in *m*) return 1;; esac; module="$value"; seen="${seen}m" ;;
|
|
token) case "$seen" in *t*) return 1;; esac; token="$value"; seen="${seen}t" ;;
|
|
phase) case "$seen" in *p*) return 1;; esac; phase="$value"; seen="${seen}p" ;;
|
|
*) return 1 ;;
|
|
esac
|
|
done < "$FULL_ROLLBACK_TRANSACTION"
|
|
[ "$seen" = vmtp ] && [ "$version" = "$FULL_ROLLBACK_VERSION" ] && [ "$module" = "$MODDIR" ] || return 1
|
|
is_safe_token "$token" || return 1
|
|
case "$phase" in armed|legacy-clean|firewall-clean|process-clean|hosts-backed-up|hosts-preserved) ;; *) return 1 ;; esac
|
|
RB_TOKEN="$token"; RB_PHASE="$phase"
|
|
}
|
|
|
|
phase_rank() {
|
|
case "$1" in
|
|
armed) printf '1\n';; legacy-clean) printf '2\n';; firewall-clean) printf '3\n';;
|
|
process-clean) printf '4\n';; hosts-backed-up) printf '5\n';; hosts-preserved) printf '6\n';;
|
|
*) return 1;;
|
|
esac
|
|
}
|
|
|
|
phase_at_least() {
|
|
local have want
|
|
have="$(phase_rank "$RB_PHASE")" || return 1
|
|
want="$(phase_rank "$1")" || return 1
|
|
[ "$have" -ge "$want" ] 2>/dev/null
|
|
}
|
|
|
|
write_transaction() {
|
|
local phase="$1" nonce tmp requested_token existing_rank requested_rank
|
|
requested_token="$RB_TOKEN"
|
|
requested_rank="$(phase_rank "$phase")" || return 1
|
|
if [ -e "$FULL_ROLLBACK_TRANSACTION" ] || [ -L "$FULL_ROLLBACK_TRANSACTION" ]; then
|
|
read_transaction || return 1
|
|
[ "$RB_TOKEN" = "$requested_token" ] || return 1
|
|
existing_rank="$(phase_rank "$RB_PHASE")" || return 1
|
|
[ "$existing_rank" -le "$requested_rank" ] 2>/dev/null || return 0
|
|
[ "$existing_rank" -lt "$requested_rank" ] 2>/dev/null || return 0
|
|
fi
|
|
nonce="$(new_temp_nonce)" || return 1
|
|
tmp="$FULL_ROLLBACK_TRANSACTION.tmp.$$.$RB_TOKEN.$nonce"
|
|
state_file_target_is_safe "$FULL_ROLLBACK_TRANSACTION" || return 1
|
|
state_path_is_managed_file "$tmp" || return 1
|
|
[ ! -e "$tmp" ] && [ ! -L "$tmp" ] || return 1
|
|
RB_ACTIVE_TEMP="$tmp"
|
|
umask 077
|
|
printf 'version=%s\nmodule_dir=%s\ntoken=%s\nphase=%s\n' "$FULL_ROLLBACK_VERSION" "$MODDIR" "$RB_TOKEN" "$phase" > "$tmp" || { cleanup_active_temp; return 1; }
|
|
chmod 0600 "$tmp" 2>/dev/null || { cleanup_active_temp; return 1; }
|
|
mv -f "$tmp" "$FULL_ROLLBACK_TRANSACTION" || { cleanup_active_temp; return 1; }
|
|
RB_ACTIVE_TEMP=""
|
|
RB_PHASE="$phase"
|
|
}
|
|
|
|
meta_is_valid() {
|
|
local key value version="" module="" complete="" generation="" archive="" seen=""
|
|
regular_root_file "$FULL_ROLLBACK_META" || return 1
|
|
while IFS='=' read -r key value; do
|
|
case "$key" in
|
|
version) case "$seen" in *v*) return 1;; esac; version="$value"; seen="${seen}v";;
|
|
module_dir) case "$seen" in *m*) return 1;; esac; module="$value"; seen="${seen}m";;
|
|
generation) case "$seen" in *g*) return 1;; esac; generation="$value"; seen="${seen}g";;
|
|
archive_sha256) case "$seen" in *a*) return 1;; esac; archive="$value"; seen="${seen}a";;
|
|
complete) case "$seen" in *c*) return 1;; esac; complete="$value"; seen="${seen}c";;
|
|
token|completed_epoch|diagnostic) :;; *) return 1;; esac
|
|
done < "$FULL_ROLLBACK_META"
|
|
[ "$seen" = vmgac ] && [ "$version" = "$FULL_ROLLBACK_VERSION" ] && [ "$module" = "$MODDIR" ] && [ "$complete" = 1 ] &&
|
|
[ "$generation" = "$RB_INSTALL_GENERATION" ] && [ "$archive" = "$RB_INSTALL_ARCHIVE_SHA256" ]
|
|
}
|
|
|
|
write_meta() {
|
|
local nonce tmp
|
|
nonce="$(new_temp_nonce)" || return 1
|
|
tmp="$FULL_ROLLBACK_META.tmp.$$.$RB_TOKEN.$nonce"
|
|
state_file_target_is_safe "$FULL_ROLLBACK_META" || return 1
|
|
[ ! -e "$tmp" ] && [ ! -L "$tmp" ] || return 1
|
|
RB_ACTIVE_TEMP="$tmp"
|
|
umask 077
|
|
{
|
|
printf 'version=%s\nmodule_dir=%s\ntoken=%s\n' "$FULL_ROLLBACK_VERSION" "$MODDIR" "$RB_TOKEN"
|
|
printf 'generation=%s\narchive_sha256=%s\n' "$RB_INSTALL_GENERATION" "$RB_INSTALL_ARCHIVE_SHA256"
|
|
printf 'completed_epoch=%s\ncomplete=1\ndiagnostic=full rollback complete; reboot required\n' "$(date +%s 2>/dev/null || echo 0)"
|
|
} > "$tmp" || { cleanup_active_temp; return 1; }
|
|
chmod 0600 "$tmp" 2>/dev/null || { cleanup_active_temp; return 1; }
|
|
mv -f "$tmp" "$FULL_ROLLBACK_META" || { cleanup_active_temp; return 1; }
|
|
RB_ACTIVE_TEMP=""
|
|
}
|
|
|
|
preflight_runtime_config() {
|
|
regular_root_file "$RUNTIME_CONFIG" && [ -r "$RUNTIME_CONFIG" ] || return 1
|
|
awk '
|
|
function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t\r]+$/, "", s); return s }
|
|
BEGIN { section=""; cores=0; autos=0 }
|
|
{ t=trim($0); if (t ~ /^\[[^]]+\]$/) { section=tolower(substr(t,2,length(t)-2)); if(section=="core") cores++; next }
|
|
if (section=="core" && tolower(t) ~ /^autostart[ \t]*=/) autos++ }
|
|
END { exit !(cores==1 && autos<=1) }
|
|
' "$RUNTIME_CONFIG" >/dev/null 2>&1
|
|
}
|
|
|
|
arm_runtime_config() {
|
|
local nonce tmp
|
|
nonce="$(new_temp_nonce)" || return 1
|
|
tmp="$RUNTIME_CONFIG.full-rollback.tmp.$$.$RB_TOKEN.$nonce"
|
|
[ ! -e "$tmp" ] && [ ! -L "$tmp" ] || return 1
|
|
RB_ACTIVE_TEMP="$tmp"
|
|
awk '
|
|
function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t\r]+$/, "", s); return s }
|
|
function close_core() { if (section=="core" && !autowritten) { print "autostart=0"; autowritten=1 } }
|
|
BEGIN { section=""; autowritten=0 }
|
|
{
|
|
raw=$0; t=trim(raw)
|
|
if (t ~ /^\[[^]]+\]$/) { close_core(); section=tolower(substr(t,2,length(t)-2)); print raw; next }
|
|
if (section=="core" && tolower(t) ~ /^autostart[ \t]*=/) { if(!autowritten) print "autostart=0"; autowritten=1; next }
|
|
if (section=="dns_manager" && tolower(t) ~ /^(dns_preset_index|selected_dns|selected_direct)[ \t]*=/) next
|
|
print raw
|
|
}
|
|
END { close_core() }
|
|
' "$RUNTIME_CONFIG" > "$tmp" || { cleanup_active_temp; return 1; }
|
|
chmod 0644 "$tmp" 2>/dev/null || { cleanup_active_temp; return 1; }
|
|
mv -f "$tmp" "$RUNTIME_CONFIG" || { cleanup_active_temp; return 1; }
|
|
RB_ACTIVE_TEMP=""
|
|
regular_root_file "$RUNTIME_CONFIG"
|
|
}
|
|
|
|
arm_disable() {
|
|
local path="$MODDIR/disable" nonce tmp size
|
|
if [ -e "$path" ] || [ -L "$path" ]; then
|
|
regular_root_file "$path" || return 1
|
|
size="$(wc -c < "$path" 2>/dev/null)" || return 1
|
|
[ "$size" = 0 ] || return 1
|
|
chmod 0600 "$path" 2>/dev/null || return 1
|
|
return 0
|
|
fi
|
|
nonce="$(new_temp_nonce)" || return 1
|
|
tmp="$MODDIR/.disable.full-rollback.$$.$RB_TOKEN.$nonce"
|
|
[ ! -e "$tmp" ] && [ ! -L "$tmp" ] || return 1
|
|
RB_ACTIVE_TEMP="$tmp"
|
|
umask 077; : > "$tmp" || { cleanup_active_temp; return 1; }
|
|
chmod 0600 "$tmp" 2>/dev/null || { cleanup_active_temp; return 1; }
|
|
ln "$tmp" "$path" 2>/dev/null || { cleanup_active_temp; return 1; }
|
|
rm -f "$tmp" 2>/dev/null || { cleanup_active_temp; return 1; }
|
|
RB_ACTIVE_TEMP=""
|
|
regular_root_file "$path"
|
|
}
|
|
|
|
preflight_hosts() {
|
|
local hosts="$MODDIR/system/etc/hosts" artifact
|
|
if [ -e "$hosts" ] || [ -L "$hosts" ]; then regular_root_file "$hosts" || return 1; fi
|
|
if [ -e "$FULL_ROLLBACK_HOSTS_BACKUP" ] || [ -L "$FULL_ROLLBACK_HOSTS_BACKUP" ]; then
|
|
regular_root_file "$FULL_ROLLBACK_HOSTS_BACKUP" || return 1
|
|
if [ -e "$hosts" ] || [ -L "$hosts" ]; then
|
|
case "$RB_PHASE" in process-clean|hosts-backed-up) : ;; *) return 1 ;; esac
|
|
regular_root_file "$hosts" && cmp -s "$hosts" "$FULL_ROLLBACK_HOSTS_BACKUP" 2>/dev/null || return 1
|
|
fi
|
|
fi
|
|
for artifact in "$FULL_ROLLBACK_HOSTS_BACKUP".tmp.*; do
|
|
[ -e "$artifact" ] || [ -L "$artifact" ] || continue
|
|
case "$artifact" in "$FULL_ROLLBACK_HOSTS_BACKUP.tmp.$RB_TOKEN."*) ;;
|
|
*) return 1 ;;
|
|
esac
|
|
regular_root_file "$artifact" || return 1
|
|
done
|
|
}
|
|
|
|
preserve_hosts() {
|
|
local hosts="$MODDIR/system/etc/hosts" artifact nonce tmp staged="" staged_count=0
|
|
for artifact in "$FULL_ROLLBACK_HOSTS_BACKUP.tmp.$RB_TOKEN."*; do
|
|
[ -e "$artifact" ] || [ -L "$artifact" ] || continue
|
|
regular_root_file "$artifact" || return 1
|
|
staged_count=$((staged_count + 1)); staged="$artifact"
|
|
done
|
|
[ "$staged_count" -le 1 ] || return 1
|
|
if [ "$staged_count" = 1 ]; then
|
|
if [ -e "$FULL_ROLLBACK_HOSTS_BACKUP" ]; then
|
|
cmp -s "$staged" "$FULL_ROLLBACK_HOSTS_BACKUP" 2>/dev/null || return 1
|
|
rm -f "$staged" 2>/dev/null || return 1
|
|
durability_sync || return 1
|
|
else
|
|
regular_root_file "$hosts" && cmp -s "$hosts" "$staged" 2>/dev/null || return 1
|
|
mv "$staged" "$FULL_ROLLBACK_HOSTS_BACKUP" 2>/dev/null || return 1
|
|
durability_sync || return 1
|
|
fi
|
|
fi
|
|
if [ -e "$FULL_ROLLBACK_HOSTS_BACKUP" ]; then
|
|
regular_root_file "$FULL_ROLLBACK_HOSTS_BACKUP" || return 1
|
|
if [ -e "$hosts" ] || [ -L "$hosts" ]; then
|
|
case "$RB_PHASE" in process-clean|hosts-backed-up) : ;; *) return 1 ;; esac
|
|
regular_root_file "$hosts" && cmp -s "$hosts" "$FULL_ROLLBACK_HOSTS_BACKUP" 2>/dev/null || return 1
|
|
if [ "$RB_PHASE" = process-clean ]; then
|
|
durability_sync || return 1
|
|
write_transaction hosts-backed-up || return 1
|
|
durability_sync || return 1
|
|
fi
|
|
rm -f "$hosts" 2>/dev/null || return 1
|
|
durability_sync || return 1
|
|
fi
|
|
RB_HOSTS_PRESERVED=1
|
|
return 0
|
|
fi
|
|
if [ ! -e "$hosts" ] && [ ! -L "$hosts" ]; then RB_HOSTS_PRESERVED=1; return 0; fi
|
|
nonce="$(new_temp_nonce)" || return 1
|
|
tmp="$FULL_ROLLBACK_HOSTS_BACKUP.tmp.$RB_TOKEN.$nonce"
|
|
[ ! -e "$tmp" ] && [ ! -L "$tmp" ] || return 1
|
|
RB_ACTIVE_TEMP="$tmp"
|
|
umask 077
|
|
cp "$hosts" "$tmp" 2>/dev/null || { cleanup_active_temp; return 1; }
|
|
chmod 0600 "$tmp" 2>/dev/null || { cleanup_active_temp; return 1; }
|
|
regular_root_file "$tmp" && cmp -s "$hosts" "$tmp" 2>/dev/null || { cleanup_active_temp; return 1; }
|
|
mv "$tmp" "$FULL_ROLLBACK_HOSTS_BACKUP" 2>/dev/null || { cleanup_active_temp; return 1; }
|
|
RB_ACTIVE_TEMP=""
|
|
regular_root_file "$FULL_ROLLBACK_HOSTS_BACKUP" &&
|
|
cmp -s "$hosts" "$FULL_ROLLBACK_HOSTS_BACKUP" 2>/dev/null || return 1
|
|
# Make the new private inode durable before recording that both paths are
|
|
# expected, then make that recovery phase durable before unlinking source.
|
|
durability_sync || return 1
|
|
write_transaction hosts-backed-up || return 1
|
|
durability_sync || return 1
|
|
rm -f "$hosts" 2>/dev/null || return 1
|
|
[ ! -e "$hosts" ] && [ ! -L "$hosts" ] || return 1
|
|
durability_sync || return 1
|
|
RB_HOSTS_PRESERVED=1
|
|
}
|
|
|
|
firewall_clean() {
|
|
command -v iptables >/dev/null 2>&1 || return 1
|
|
owned_family_absent iptables || return 1
|
|
# An IPv6 frontend that cannot be queried is acceptable on two different
|
|
# proofs, and asking the kernel again is not one of them. Either this
|
|
# generation is known never to have published IPv6 rules, or this very
|
|
# transaction's preflight already read the family and found nothing of
|
|
# ours in it — both callers below run one first, and nothing publishes
|
|
# between the two. Discarding the second proof and re-probing turns a
|
|
# rollback that proved the family empty into one that reports it
|
|
# unverified, and then records an IPv6 publication its own preflight
|
|
# disproved.
|
|
if command -v ip6tables >/dev/null 2>&1; then
|
|
owned_family_absent ip6tables ||
|
|
{ ! z2_fw_tool_available ip6tables &&
|
|
{ [ "${CLEANUP_IPV6_OWNERSHIP_EXPECTED:-1}" = 0 ] ||
|
|
[ "${FIREWALL_IPV6_AUDITED_EMPTY:-0}" = 1 ] ||
|
|
[ "${FIREWALL_IPV6_TEARDOWN_PROVEN:-0}" = 1 ]; }; } ||
|
|
return 1
|
|
elif [ "${IPV6_PUBLICATION_RECORDED:-0}" = 1 ]; then
|
|
# No frontend at all: the module could only have published there while
|
|
# one existed, so absence of a record is the answer. Stop asks the
|
|
# same question the same way.
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
process_clean() { scan_exact_owned_nfqws >/dev/null 2>&1; [ -z "$OWNED_SCAN_PIDS" ]; }
|
|
|
|
cleanup_diagnostics() {
|
|
local path
|
|
for path in "$CMDLINE_FILE" "$STARTUP_LOG" "$ERROR_LOG" "$DEBUG_LOG"; do
|
|
if state_file_is_secure "$path"; then rm -f "$path" 2>/dev/null || true; fi
|
|
done
|
|
}
|
|
|
|
# The committed snapshot describes the generation this rollback dismantled.
|
|
# Leaving it makes the next observation replay those facts — an owned,
|
|
# degraded service — over a module that is disabled and stopped. Deleting it
|
|
# is no better: absence reads as "verified stopped", which would claim the
|
|
# very proof a rollback that skipped an unqueryable family does not have. So
|
|
# publish what this rollback actually established.
|
|
write_rollback_status() {
|
|
restore_status_facts
|
|
STATUS_RULES_OK=0; STATUS_RULES_FAIL=0; STATUS_RULES_TOTAL=0
|
|
STATUS_ERRORS=""; STATUS_OWN_PID=""; STATUS_OWN_PID_STARTTIME=""
|
|
STATUS_OWN_ARGV_SHA256=""; STATUS_OWNER_GENERATION=""
|
|
STATUS_PID_VERIFIED=0; STATUS_OWNER_METADATA_VERIFIED=0
|
|
STATUS_RULES_EXPECTED=0; STATUS_QNUM="${STOP_QNUM:-${STATUS_QNUM:-}}"
|
|
STATUS_IPV4_ACTIVE=0; STATUS_CHAINS=0; STATUS_ANCHORS=0
|
|
STATUS_IPV4_RULES=0; STATUS_IPV6_RULES=0; STATUS_FALLBACK_MODE=0
|
|
STATUS_ERROR_STATUS=OK; STATUS_ERROR_DOMAIN=NONE; STATUS_ERROR_CODE=NONE
|
|
STATUS_ERROR_STAGE=NONE; STATUS_ERROR_DETAIL=""
|
|
if [ "${RB_IPV6_UNVERIFIED:-0}" = 1 ]; then
|
|
STATUS_RULESET_VERIFIED=0; STATUS_IPV6_ACTIVE=1
|
|
STATUS_DIAGNOSTICS="full rollback finished; the IPv6 ruleset stayed unverified until the reboot"
|
|
else
|
|
STATUS_RULESET_VERIFIED=1; STATUS_IPV6_ACTIVE=0
|
|
STATUS_DIAGNOSTICS="full rollback complete; reboot required"
|
|
fi
|
|
write_iptables_status stopped
|
|
}
|
|
|
|
# Install signal handling before usage validation, lock acquisition/waiting, or
|
|
# any preflight. Every catchable termination therefore emits the same exact
|
|
# machine contract and retires only this process's lock-acquisition artifacts.
|
|
trap interrupted HUP INT TERM
|
|
|
|
[ "$#" = 1 ] && [ "$1" = --machine ] || {
|
|
RB_STATUS=blocked
|
|
RB_DIAGNOSTIC="usage: zapret-full-rollback.sh --machine"
|
|
emit_result 2
|
|
}
|
|
|
|
ensure_state_dir || blocked "state directory is unavailable or unsafe"
|
|
acquire_lifecycle_lock || blocked "another lifecycle owner is active"
|
|
RB_LOCKED=1
|
|
|
|
audit_recovery_artifacts full-rollback || blocked "recovery artifact blocks rollback: $RECOVERY_ARTIFACT_DIAGNOSTIC"
|
|
read_install_generation_meta || blocked "install generation metadata is missing, unsafe, or malformed"
|
|
RB_INSTALL_GENERATION="$INSTALL_META_GENERATION"; RB_INSTALL_ARCHIVE_SHA256="$INSTALL_META_ARCHIVE_SHA256"
|
|
{ [ -e "$UNINSTALL_TOMBSTONE" ] || [ -L "$UNINSTALL_TOMBSTONE" ]; } && blocked "uninstall tombstone blocks rollback"
|
|
module_removal_pending && blocked "Root-manager removal marker blocks rollback"
|
|
|
|
if [ -e "$FULL_ROLLBACK_TRANSACTION" ] || [ -L "$FULL_ROLLBACK_TRANSACTION" ]; then
|
|
read_transaction || blocked "rollback transaction is malformed or unsafe"
|
|
else
|
|
RB_TOKEN="$(new_lifecycle_token)"; is_safe_token "$RB_TOKEN" || failed "cannot create rollback token"
|
|
fi
|
|
if [ -e "$FULL_ROLLBACK_META" ] || [ -L "$FULL_ROLLBACK_META" ]; then meta_is_valid || blocked "rollback metadata is malformed or unsafe"; fi
|
|
preflight_runtime_config || blocked "runtime.ini is missing, unsafe, or ambiguous"
|
|
preflight_hosts || blocked "hosts overlay or existing backup is unsafe or conflicts"
|
|
load_effective_core_config_readonly >/dev/null 2>&1 || blocked "runtime.ini core values are invalid"
|
|
restore_status_facts
|
|
STOP_QNUM="${STATUS_FILE_QNUM:-${QNUM:-}}"
|
|
RB_OWNER_AVAILABLE=0
|
|
if read_owner_state >/dev/null 2>&1; then
|
|
RB_OWNER_AVAILABLE=1
|
|
STOP_QNUM="$OWNER_STATE_QNUM"
|
|
fi
|
|
resolve_ipv6_ownership_expectation "$RB_OWNER_AVAILABLE"
|
|
|
|
arm_runtime_config || failed "cannot atomically disable autostart in runtime.ini"
|
|
arm_disable || failed "cannot publish exact module disable fence"
|
|
write_transaction armed || failed "cannot publish rollback recovery journal"
|
|
durability_sync || failed "cannot durably publish rollback fence and recovery journal"
|
|
RB_ROLLBACK_ARMED=1
|
|
|
|
if ! phase_at_least process-clean; then
|
|
if ! preflight_owned_process_cleanup; then partial "process ownership is ambiguous; firewall and listener retained: $PROCESS_CLEANUP_PREFLIGHT_ERROR"; fi
|
|
fi
|
|
# The legacy-clean journal phase survives only for resume compatibility with
|
|
# transaction journals written by older releases. The direct-rule migration
|
|
# itself was removed: iptables state does not survive a reboot and updated
|
|
# module code never coexists with rules from an older module generation.
|
|
if ! phase_at_least legacy-clean; then
|
|
write_transaction legacy-clean || failed "cannot advance rollback journal past the retired legacy phase"
|
|
fi
|
|
|
|
if ! phase_at_least firewall-clean; then
|
|
if ! audit_owned_firewall_for_cleanup; then partial "persisted firewall generation is ambiguous; firewall and listener retained: $FIREWALL_CLEANUP_PREFLIGHT_ERROR"; fi
|
|
# Z2_RB_FIREWALL_CLEAN asserts a verified-clean firewall, so a family that
|
|
# could not be queried cannot be reported under it. But refusing outright
|
|
# is worse than reporting it: the journal would stay at an earlier phase
|
|
# and fence start, stop, uninstall and purge until someone deleted the
|
|
# state by hand — on a device where the condition repeats every boot. The
|
|
# IPv4 family is the one this rollback can always prove; if that is gone
|
|
# and the IPv6 frontend is unusable — missing outright, or unqueryable
|
|
# through repeated probes — finish and say so.
|
|
if ! cleanup_owned_firewall audited || ! firewall_clean; then
|
|
if firewall_family_persistently_unavailable ip6tables && owned_family_absent iptables; then
|
|
RB_IPV6_UNVERIFIED=1
|
|
else
|
|
partial "verified owned firewall cleanup is incomplete; listener retained"
|
|
fi
|
|
elif [ "${FIREWALL_IPV6_SKIPPED_UNPROVEN:-0}" = 1 ]; then
|
|
RB_IPV6_UNVERIFIED=1
|
|
fi
|
|
write_transaction firewall-clean || failed "cannot advance rollback journal after firewall cleanup"
|
|
else
|
|
# The journal records which phase completed, not why it was allowed to. A
|
|
# family the earlier pass had to skip is still there, so retry the teardown
|
|
# rather than only re-verifying it: whatever made the frontend unqueryable
|
|
# then may well be gone now. Teardown is idempotent.
|
|
if audit_owned_firewall_for_cleanup && cleanup_owned_firewall audited && firewall_clean; then
|
|
[ "${FIREWALL_IPV6_SKIPPED_UNPROVEN:-0}" != 1 ] || RB_IPV6_UNVERIFIED=1
|
|
elif firewall_family_persistently_unavailable ip6tables && owned_family_absent iptables; then
|
|
RB_IPV6_UNVERIFIED=1
|
|
else
|
|
partial "rollback journal says firewall-clean but a clean full snapshot cannot be proved"
|
|
fi
|
|
fi
|
|
if [ "${RB_IPV6_UNVERIFIED:-0}" = 1 ]; then RB_FIREWALL_CLEAN=0; else RB_FIREWALL_CLEAN=1; fi
|
|
|
|
if ! phase_at_least process-clean; then
|
|
if ! stop_pidfile_process || ! process_clean; then partial "verified module process cleanup is incomplete"; fi
|
|
retire_owner_metadata >/dev/null 2>&1 || partial "process is stopped but ownership metadata remains ambiguous"
|
|
write_transaction process-clean || failed "cannot advance rollback journal after process cleanup"
|
|
durability_sync || failed "process-clean recovery phase could not be synchronized; hosts were not touched"
|
|
else
|
|
process_clean || partial "rollback journal says process-clean but the process state is not clean"
|
|
fi
|
|
RB_PROCESS_CLEAN=1
|
|
|
|
if ! phase_at_least hosts-preserved; then
|
|
preserve_hosts || partial "hosts overlay could not be copied to its protected backup"
|
|
write_transaction hosts-preserved || failed "cannot advance rollback journal after hosts preservation"
|
|
else
|
|
preserve_hosts || partial "hosts-preserved recovery state is inconsistent"
|
|
fi
|
|
durability_sync || failed "hosts preservation phase could not be synchronized; recovery journal retained"
|
|
cleanup_diagnostics
|
|
# The receipt is diagnostic. Everything the rollback owns is already done and
|
|
# durable at this point, so a failed status write must not abort the commit —
|
|
# that would leave the journal in place and fence start, stop, uninstall and
|
|
# purge over a bookkeeping error.
|
|
write_rollback_status || RB_STATUS_RECEIPT_FAILED=1
|
|
write_meta || failed "cleanup is verified but rollback metadata commit failed"
|
|
durability_sync || failed "rollback metadata could not be synchronized; recovery journal retained"
|
|
RB_COMMIT_TOKEN="$RB_TOKEN"
|
|
read_transaction && [ "$RB_TOKEN" = "$RB_COMMIT_TOKEN" ] || failed "rollback journal changed before commit"
|
|
rm -f "$FULL_ROLLBACK_TRANSACTION" 2>/dev/null || failed "rollback committed but recovery journal could not be retired"
|
|
if ! durability_sync; then
|
|
RB_TOKEN="$RB_COMMIT_TOKEN"
|
|
write_transaction hosts-preserved >/dev/null 2>&1 || true
|
|
durability_sync >/dev/null 2>&1 || true
|
|
failed "rollback journal removal could not be synchronized; recovery journal retained"
|
|
fi
|
|
|
|
RB_RECEIPT_NOTE=""
|
|
[ "${RB_STATUS_RECEIPT_FAILED:-0}" != 1 ] ||
|
|
RB_RECEIPT_NOTE="; the status receipt could not be written"
|
|
if [ "${RB_IPV6_UNVERIFIED:-0}" = 1 ]; then
|
|
# Everything the rollback owns is done and the journal is retired, so the
|
|
# module is not fenced — but "complete" asserts a verified-clean firewall,
|
|
# and this run could not query IPv6. Report what is true.
|
|
partial "full rollback finished and the module is disabled, but the IPv6 mangle table is unavailable; the required reboot clears any remaining IPv6 rules$RB_RECEIPT_NOTE"
|
|
fi
|
|
RB_STATUS=complete
|
|
RB_DIAGNOSTIC="full rollback complete; reboot required; user strategies and lists preserved$RB_RECEIPT_NOTE"
|
|
finish_result 0
|