The DNS manager used to apply a selection by writing system/etc/hosts into the live module directory and asking for a reboot, so the root manager would pick it up while assembling /system. That put the module's own tree in the path of whatever the manager does between boots: KernelSU v3 moved that assembly into a separate metamodule, a second hosts module publishing the same path raced it silently, and an upgrade replaced the tree and discarded the user's selection with it. The published file now lives in /data/adb/zapret2-hosts and the module bind-mounts it over /system/etc/hosts from post-fs-data.sh, which is the same mechanism on Magisk, KernelSU and APatch. hosts-overlay.sh owns that mount: it snapshots the untouched system file before covering it, carries a pre-2.3.0 in-tree overlay over and takes it out of the tree, relabels the source so netd can still read it, and yields to another enabled hosts module instead of fighting it for the path. Because the module publishes the mount itself, an edit now takes effect immediately: publication is still an atomic rename, and --apply republishes the mount rather than waiting for a reboot. A rename leaves the live mount on the replaced inode, so the mount identity is recorded when it is published and consulted only against a live mount — a foreign mount is never released. Rollback, uninstall and purge follow the file to its new home, and the installer carries an existing selection across the upgrade that would otherwise drop it. Also releases the tethering runtime key and owner state v9, and lifts the packaged lifecycle contract to 9.
572 lines
27 KiB
Shell
572 lines
27 KiB
Shell
#!/system/bin/sh
|
|
# Durable, fail-closed rollback of live Zapret2 effects. User strategy data is retained.
|
|
|
|
# The wrappers invoke this script by an absolute, already-canonical path, so
|
|
# resolving it costs two forks (dirname plus the cd/pwd subshell) to return the
|
|
# string we were handed. Take the cheap route when the path is already clean
|
|
# and keep the canonicalizing fallback for every other invocation.
|
|
case "$0" in
|
|
/*//*|/*/./*|/*/../*|*/..|*/.) SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" ;;
|
|
/*/*) SCRIPT_DIR="${0%/*}" ;;
|
|
*) SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" ;;
|
|
esac
|
|
. "$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"
|
|
}
|
|
|
|
# Releases the bind mount this module owns so the published file can be
|
|
# unlinked. A hosts mount belonging to another module is never touched.
|
|
release_hosts_mount() {
|
|
[ -f "$HOSTS_OVERLAY_SCRIPT" ] && [ ! -L "$HOSTS_OVERLAY_SCRIPT" ] || return 0
|
|
/system/bin/sh "$HOSTS_OVERLAY_SCRIPT" --unmount >/dev/null 2>&1
|
|
}
|
|
|
|
preflight_hosts() {
|
|
local hosts="$HOSTS_OVERLAY_FILE" 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="$HOSTS_OVERLAY_FILE" 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
|
|
release_hosts_mount
|
|
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
|
|
release_hosts_mount
|
|
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
|