A stopped receipt hardcoded a verified ruleset, so a teardown that had to skip an unqueryable family could not use one: it withheld the receipt and left the caller to infer the reservation from a separate status observation. That second trip is where the reservation kept getting lost — the last round fixed the projection so it survived, which then produced a payload asserting stopped-but-unverified, a shape the app treats as invalid and reports as "a process or stale rules remain" for a stop that removed everything it could see. The receipt now carries the fact the operation established, and an unrecorded verification defaults to withholding the claim rather than asserting one. The app side that accepts this shape and names the reservation to the user ships with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
272 lines
13 KiB
Shell
272 lines
13 KiB
Shell
#!/system/bin/sh
|
|
# Transactional stop: detach owned firewall first, then stop only a verified PID.
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
. "$SCRIPT_DIR/common.sh"
|
|
|
|
log_msg() {
|
|
append_lifecycle_log "$(date '+%Y-%m-%d %H:%M:%S') [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 "$@"
|