magisk-zapret2/zapret2/scripts/zapret-stop.sh
2026-07-25 21:02:48 +03:00

281 lines
13 KiB
Shell

#!/system/bin/sh
# Transactional stop: detach owned firewall first, then stop only a verified PID.
# 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"
log_msg() {
z2_log_stamp_read
append_lifecycle_log "$Z2_LOG_STAMP [STOP] $1"
if command -v log >/dev/null 2>&1; then log -t Zapret2 "$1" 2>/dev/null; fi
}
stop_error_exit() {
local domain="$1" code="$2" stage="$3" retryable="$4" message="$5"
z2_error_set "$domain" "$code" "$stage" "$message" ||
z2_error_set LIFECYCLE LIFECYCLE_FAILED STOP "$message"
z2_error_emit_machine
echo "ERROR: $message"
exit 1
}
write_stop_status() {
local state="$1" message="$2"
# main() restores the status facts once for the whole transaction; only a
# signal arriving before that point needs a fallback restore here.
[ "${STOP_FACTS_RESTORED:-0}" = 1 ] || restore_status_facts
STATUS_RULES_OK=0
if [ "$state" = stopped ]; then STATUS_RULES_FAIL=0; STATUS_RULES_TOTAL=0
else STATUS_RULES_FAIL=1; STATUS_RULES_TOTAL=1; fi
STATUS_ERRORS="$message"; STATUS_OWN_PID=""; STATUS_OWN_PID_STARTTIME=""
STATUS_OWN_ARGV_SHA256=""; STATUS_OWNER_GENERATION=""
STATUS_PID_VERIFIED=0; STATUS_OWNER_METADATA_VERIFIED=0
# Only a completed stop that proved every family proves the owned ruleset
# is gone. A failed stop records what it could not prove, and a stop that
# had to skip an unqueryable family proved one family less — claiming
# verification in either case would let the next teardown read this
# snapshot back as proof there is nothing left, a claim that then sustains
# itself because this stop also retires the owner record.
if [ "$state" = stopped ] && [ "${FIREWALL_IPV6_SKIPPED_UNPROVEN:-0}" != 1 ]; then
STATUS_RULESET_VERIFIED=1
else
STATUS_RULESET_VERIFIED=0
fi
# ipv6_active answers "did this module publish there", and a teardown that
# skipped an unqueryable family has no evidence that it did. Recording 1
# would invent one, and every later teardown would read it back as proof.
# The withheld verification above is what carries the uncertainty.
STATUS_RULES_EXPECTED=0; STATUS_QNUM="${STOP_QNUM:-${STATUS_QNUM:-${QNUM:-}}}"
STATUS_IPV4_ACTIVE=0; STATUS_IPV6_ACTIVE=0; STATUS_CHAINS=0; STATUS_ANCHORS=0
STATUS_IPV4_RULES=0; STATUS_IPV6_RULES=0
STATUS_NFQUEUE_SUPPORTED="${STATUS_NFQUEUE_SUPPORTED:-0}"
STATUS_QUEUE_BYPASS_SUPPORTED="${STATUS_QUEUE_BYPASS_SUPPORTED:-0}"
STATUS_CONNBYTES_SUPPORTED="${STATUS_CONNBYTES_SUPPORTED:-0}"
STATUS_MULTIPORT_SUPPORTED="${STATUS_MULTIPORT_SUPPORTED:-0}"
STATUS_MARK_SUPPORTED="${STATUS_MARK_SUPPORTED:-0}"
# A note is diagnostic only: a completed stop must not publish an errors=
# line, and an error must not lose its message to a note.
STATUS_FALLBACK_MODE=0
if [ "$state" = stopped ]; then
STATUS_ERRORS=""; STATUS_DIAGNOSTICS="${STOP_NOTE:-}"
else
STATUS_DIAGNOSTICS="$message"
fi
if [ "$state" = stopped ]; then
STATUS_ERROR_STATUS=OK; STATUS_ERROR_DOMAIN=NONE; STATUS_ERROR_CODE=NONE
STATUS_ERROR_STAGE=NONE; STATUS_ERROR_DETAIL=""
else
STATUS_ERROR_STATUS=ERROR
STATUS_ERROR_DOMAIN="${STOP_ERROR_DOMAIN:-LIFECYCLE}"
STATUS_ERROR_CODE="${STOP_ERROR_CODE:-LIFECYCLE_FAILED}"
STATUS_ERROR_STAGE="${STOP_ERROR_STAGE:-STOP}"
STATUS_ERROR_DETAIL="$(z2_error_detail_normalize "$message")"
fi
write_iptables_status "$state"
}
remove_transient_diagnostics() {
local path rc=0
[ "${STOP_RUNTIME_OWNED:-0}" = 1 ] || return 0
for path in "$CMDLINE_FILE" "$STARTUP_LOG" "$ERROR_LOG" "$DEBUG_LOG"; do
if [ -e "$path" ] || [ -L "$path" ]; then rm -f "$path" 2>/dev/null || rc=1; fi
done
return "$rc"
}
stop_interrupted() {
local message="stop interrupted by signal"
trap '' HUP INT TERM
STOP_ERROR_DOMAIN=LIFECYCLE
STOP_ERROR_CODE=LIFECYCLE_FAILED
STOP_ERROR_STAGE=STOP_SIGNAL
write_stop_status error "$message" >/dev/null 2>&1 || true
release_lifecycle_lock
trap - HUP INT TERM
exit 1
}
main() {
ensure_state_dir ||
stop_error_exit STATE STATE_UNAVAILABLE STOP_STATE 0 \
"insecure or unavailable zapret2 state directory: $STATE_DIR"
acquire_lifecycle_lock ||
stop_error_exit LIFECYCLE LIFECYCLE_BUSY STOP_LOCK 1 "zapret2 lifecycle is busy"
if ! audit_recovery_artifacts lifecycle; then
message="stop blocked by recovery state: ${RECOVERY_ARTIFACT_DIAGNOSTIC:-unsafe recovery artifact}$(recovery_block_remedy)"
release_lifecycle_lock
stop_error_exit LIFECYCLE RECOVERY_BLOCKED STOP_RECOVERY 0 "$message"
fi
if ! uninstall_tombstone_allows_stop; then
message="stop blocked by uninstall serialization: $UNINSTALL_TOMBSTONE_ERROR"
release_lifecycle_lock
stop_error_exit LIFECYCLE UNINSTALL_BLOCKED STOP_UNINSTALL 1 "$message"
fi
trap stop_interrupted HUP INT TERM
if ! prepare_lifecycle_log; then
LOG_READY=0
if command -v log >/dev/null 2>&1; then log -p w -t Zapret2 "Lifecycle file logging disabled: unsafe or unavailable path" 2>/dev/null; fi
fi
restore_status_facts
STOP_FACTS_RESTORED=1
STOP_RUNTIME_OWNED=0
OWNER_STATE_AVAILABLE=0
if read_owner_state >/dev/null 2>&1; then OWNER_STATE_AVAILABLE=1; fi
if [ "$OWNER_STATE_AVAILABLE" = 1 ] || read_runtime_owner_marker >/dev/null 2>&1 ||
read_verified_pidfile >/dev/null 2>&1; then
STOP_RUNTIME_OWNED=1
fi
STOP_QNUM="${STATUS_FILE_QNUM:-}"
[ "$OWNER_STATE_AVAILABLE" != 1 ] || STOP_QNUM="$OWNER_STATE_QNUM"
resolve_ipv6_ownership_expectation "$OWNER_STATE_AVAILABLE"
if [ -z "$STOP_QNUM" ]; then
# Neither the committed snapshot nor an owner record carries the queue
# number (first stop on a fresh boot); fall back to the configured one.
load_effective_core_config_readonly >/dev/null 2>&1 || true
STOP_QNUM="${QNUM:-}"
fi
log_msg "Stopping Zapret2"
[ -z "$UNINSTALL_TOMBSTONE_DIAGNOSTIC" ] || log_msg "$UNINSTALL_TOMBSTONE_DIAGNOSTIC"
rc=0
STOP_STATUS_COMMITTED=0
errors=""
STOP_ERROR_DOMAIN=NONE; STOP_ERROR_CODE=NONE
STOP_ERROR_STAGE=NONE; STOP_ERROR_RETRYABLE=0
# Prove the complete process identity/publication before any chain
# mutation. The later stop consumes this same snapshot and generation, so
# a replacement process cannot be killed after teardown.
if ! preflight_owned_process_cleanup; then
errors="process cleanup preflight blocked: $PROCESS_CLEANUP_PREFLIGHT_ERROR; firewall and daemon teardown were not attempted"
STOP_ERROR_DOMAIN=PROCESS; STOP_ERROR_CODE=PROCESS_STOP_FAILED; STOP_ERROR_STAGE=STOP_PREFLIGHT
elif ! audit_owned_firewall_for_cleanup; then
errors="firewall generation preflight blocked: $FIREWALL_CLEANUP_PREFLIGHT_ERROR; firewall and daemon teardown were not attempted"
STOP_ERROR_DOMAIN=FIREWALL; STOP_ERROR_CODE=FIREWALL_CLEANUP_FAILED; STOP_ERROR_STAGE=STOP_PREFLIGHT
fi
if [ -n "$errors" ]; then
trap '' HUP INT TERM
write_stop_status error "$errors" >/dev/null 2>&1 || true
release_lifecycle_lock
trap - HUP INT TERM
log_msg "ERROR: $errors"
z2_error_set "$STOP_ERROR_DOMAIN" "$STOP_ERROR_CODE" "$STOP_ERROR_STAGE" "$errors" ||
z2_error_set LIFECYCLE LIFECYCLE_FAILED STOP "$errors"
z2_error_emit_machine
echo "ERROR: Zapret2 stop incomplete: $errors"
exit 1
fi
firewall_detached=0
if ! cleanup_owned_firewall audited; then
rc=1
STOP_ERROR_DOMAIN=FIREWALL; STOP_ERROR_CODE=FIREWALL_CLEANUP_FAILED; STOP_ERROR_STAGE=STOP_FIREWALL
if [ -n "$errors" ]; then errors="$errors; owned firewall cleanup failed"
else errors="owned firewall cleanup failed: ${FIREWALL_CLEANUP_PREFLIGHT_ERROR:-ambiguous ownership}; daemon teardown was not attempted"; fi
elif ! command -v ip6tables >/dev/null 2>&1 && [ "$IPV6_PUBLICATION_RECORDED" = 1 ]; then
# cleanup_owned_firewall proves per-family absence as the postcondition
# of each family it touches, and fails when a present frontend cannot
# be queried while IPv6 ownership is expected. The one fact left is a
# device that lost its ip6tables frontend entirely while this
# generation did publish IPv6 rules.
rc=1
STOP_ERROR_DOMAIN=FIREWALL; STOP_ERROR_CODE=POSTCONDITION_FAILED; STOP_ERROR_STAGE=STOP_FIREWALL
if [ -n "$errors" ]; then errors="$errors; owned firewall artifacts remain"
else errors="IPv6 owned rules cannot be disproved because ip6tables is unavailable; daemon teardown was not attempted"; fi
else
firewall_detached=1
[ "${FIREWALL_IPV6_SKIPPED_UNPROVEN:-0}" != 1 ] ||
log_msg "WARNING: the IPv6 mangle table could not be queried; rules from an earlier generation, if any, were not removed"
fi
if [ "$firewall_detached" = 1 ] && ! stop_pidfile_process; then
rc=1
STOP_ERROR_DOMAIN=PROCESS; STOP_ERROR_CODE=PROCESS_STOP_FAILED; STOP_ERROR_STAGE=STOP_PROCESS
if [ -n "$errors" ]; then errors="$errors; verified process stop failed"
else errors="verified process stop failed"; fi
fi
if [ "$firewall_detached" = 1 ] && read_verified_pidfile; then
rc=1
STOP_ERROR_DOMAIN=PROCESS; STOP_ERROR_CODE=POSTCONDITION_FAILED; STOP_ERROR_STAGE=STOP_VERIFY
if [ -n "$errors" ]; then errors="$errors; verified nfqws2 process remains"
else errors="verified nfqws2 process remains"; fi
elif [ -e "$PIDFILE" ]; then
if read_live_pidfile; then
rc=1
STOP_ERROR_DOMAIN=PROCESS; STOP_ERROR_CODE=POSTCONDITION_FAILED; STOP_ERROR_STAGE=STOP_VERIFY
if [ -n "$errors" ]; then errors="$errors; unverified live PID remains"
else errors="unverified live PID remains"; fi
fi
fi
if [ "$rc" -eq 0 ]; then
# stop_pidfile_process just proved no exact process remains and
# removed the publication files itself; asserting their absence is
# the whole retirement postcondition without another /proc walk.
if [ -e "$PIDFILE" ] || [ -L "$PIDFILE" ] || [ -e "$OWNER_STATE" ] || [ -L "$OWNER_STATE" ]; then
rc=1
if [ -n "$errors" ]; then errors="$errors; ownership publication survived a verified teardown"
else errors="ownership publication survived a verified teardown"; fi
fi
fi
if [ "$rc" -eq 0 ]; then
if ! remove_transient_diagnostics; then
log_msg "WARNING: one or more transient diagnostics could not be removed safely"
fi
STOP_NOTE=""
[ "${FIREWALL_IPV6_SKIPPED_UNPROVEN:-0}" != 1 ] ||
STOP_NOTE="IPv6 mangle table unavailable; rules from an earlier generation, if any, were left for the next reboot"
if write_stop_status stopped ""; then
STOP_STATUS_COMMITTED=1
else
# Process and firewall cleanup is already verified. A diagnostic
# status-write failure must not turn a completed stop into an
# unverifiable process-cleanup failure after metadata retirement.
log_msg "WARNING: stopped state is clean but status file update failed"
fi
else
write_stop_status error "$errors" >/dev/null 2>&1 || true
fi
receipt_lifecycle_state=idle; receipt_owner_kind=none
if [ "$LOCK_HELD" = inherited ]; then
receipt_lifecycle_state=owned; receipt_owner_kind=android-mutation
fi
if ! release_lifecycle_lock; then
rc=1
STOP_ERROR_DOMAIN=LIFECYCLE; STOP_ERROR_CODE=LIFECYCLE_FAILED
STOP_ERROR_STAGE=STOP_LOCK_RELEASE
errors="${errors:+$errors; }lifecycle ownership release failed"
fi
trap - HUP INT TERM
if [ "$rc" -eq 0 ]; then
log_msg "Zapret2 stopped and owned state is clean"
# The receipt is this operation's own report, reservation included. It
# used to be withheld when a family had to be skipped, because the
# caller rejected a stopped receipt that declined to certify the
# ruleset — which only moved the problem to the observation the caller
# made instead, where the same reservation had to survive a second
# trip through the snapshot. It travels on the receipt now.
if [ "$STOP_STATUS_COMMITTED" = 1 ]; then
emit_committed_status_v6 stopped "$receipt_lifecycle_state" "$receipt_owner_kind" || true
fi
echo "Zapret2 stopped"
else
log_msg "ERROR: $errors"
z2_error_set "$STOP_ERROR_DOMAIN" "$STOP_ERROR_CODE" "$STOP_ERROR_STAGE" "$errors" ||
z2_error_set LIFECYCLE LIFECYCLE_FAILED STOP "$errors"
z2_error_emit_machine
echo "ERROR: Zapret2 stop incomplete: $errors"
fi
exit "$rc"
}
main "$@"