#!/bin/sh ################################################################################################################# # █████╗ ███████╗██╗ ██╗███████╗██╗ ██╗██████╗ ████████╗ ███╗ ███╗███████╗██████╗ ██╗ ██╗███╗ ██╗# #██╔══██╗██╔════╝██║ ██║██╔════╝██║ ██║██╔══██╗╚══██╔══╝ ████╗ ████║██╔════╝██╔══██╗██║ ██║████╗ ██║# #███████║███████╗██║ ██║███████╗██║ █╗ ██║██████╔╝ ██║█████╗██╔████╔██║█████╗ ██████╔╝██║ ██║██╔██╗ ██║# #██╔══██║╚════██║██║ ██║╚════██║██║███╗██║██╔══██╗ ██║╚════╝██║╚██╔╝██║██╔══╝ ██╔══██╗██║ ██║██║╚██╗██║# #██║ ██║███████║╚██████╔╝███████║╚███╔███╔╝██║ ██║ ██║ ██║ ╚═╝ ██║███████╗██║ ██║███████╗██║██║ ╚████║# #╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝╚═╝╚═╝ ╚═══╝# # █████╗ ██████╗ ██████╗ ██╗ ██╗ █████╗ ██████╗ ██████╗ ██╗ ██╗ ██████╗ ███╗ ███╗███████╗ # #██╔══██╗██╔══██╗██╔════╝ ██║ ██║██╔══██╗██╔══██╗██╔══██╗██║ ██║██╔═══██╗████╗ ████║██╔════╝ # #███████║██║ ██║██║ ███╗██║ ██║███████║██████╔╝██║ ██║███████║██║ ██║██╔████╔██║█████╗ # #██╔══██║██║ ██║██║ ██║██║ ██║██╔══██║██╔══██╗██║ ██║██╔══██║██║ ██║██║╚██╔╝██║██╔══╝ # #██║ ██║██████╔╝╚██████╔╝╚██████╔╝██║ ██║██║ ██║██████╔╝██║ ██║╚██████╔╝██║ ╚═╝ ██║███████╗ # #╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ # #██╗███╗ ██╗███████╗████████╗ █████╗ ██╗ ██╗ ███████╗██████╗ Author: # #██║████╗ ██║██╔════╝╚══██╔══╝██╔══██╗██║ ██║ ██╔════╝██╔══██╗ SomeWhereOverTheRainBow # #██║██╔██╗ ██║███████╗ ██║ ███████║██║ ██║ █████╗ ██████╔╝ Contributors: # #██║██║╚██╗██║╚════██║ ██║ ██╔══██║██║ ██║ ██╔══╝ ██╔══██╗ JackYaz,gspannu,thelonelycoder # #██║██║ ╚████║███████║ ██║ ██║ ██║███████╗███████╗███████╗██║ ██║ v2.7.0 # #╚═╝╚═╝ ╚═══╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝ # ################################################################################################################# export LC_ALL=C export PATH="/sbin:/bin:/usr/sbin:/usr/bin${PATH:+:$PATH}" umask 077 AI_VERSION="v2.7.0" export AI_VERSION readonly INSTALLER_ACTION_RESTORE="RESTORE" readonly INSTALLER_ACTION_INSTALL="install" readonly INSTALLER_ACTION_UPDATE="update" readonly INSTALLER_ROLLBACK_COMPLETE="rollback complete" readonly INSTALLER_ROLLBACK_PARTIAL="rollback partial" readonly INSTALLER_ROLLBACK_FAILED="rollback failed" readonly INSTALLER_RESTORE_FAILED="restore-failed" readonly INSTALLER_ROLLBACK_UNAVAILABLE="rollback unavailable" readonly INSTALLER_BACKUP_RESTORE="backup restore" readonly INSTALLER_DNS_TRANSACTION="DNS NVRAM transaction" readonly INSTALLER_YAML_VALIDATION="yaml validation" readonly INSTALLER_BLOCKLIST_REPLACEMENT="blocklist yaml replacement" readonly INSTALLER_BINARY_ROLLBACK="AdGuardHome binary rollback" readonly INSTALLER_DIRECTORY_ROLLBACK="AdGuardHome directory rollback" SUPPORT_EXPORT_SOURCE_REF="unknown" SUPPORT_EXPORT_RECOVERY_STATUS="not-reported" readonly ADGUARD_METADATA_DIR="/tmp/AGH_STATIC_METADATA_$$.d" readonly ADGUARD_METADATA_FILE="${ADGUARD_METADATA_DIR}/channels.txt" ADGUARD_METADATA_DIR_OWNED=0 ADGUARD_SERVICE_STAGE_DIR="" ADGUARD_SERVICE_STAGE_OWNED=0 ADGUARD_SERVICE_PUBLISH_DIR="" ADGUARD_SERVICE_PUBLISH_OWNED=0 readonly ADGUARDHOME_WAIT_TIMEOUT="60" readonly ADGUARDHOME_FIRMWARE_WAIT_TIMEOUT="15" readonly ADGUARDHOME_READY_TIMEOUT="180" DNS_ENV_READY_TIMEOUT="${DNS_ENV_READY_TIMEOUT:-60}" DNS_ENV_RECOVERY_TIMEOUT="${DNS_ENV_RECOVERY_TIMEOUT:-15}" readonly ADDON_DIR="/jffs/addons/AdGuardHome.d" readonly BASE_DIR="/opt/etc" readonly TARG_DIR="${BASE_DIR}/AdGuardHome" readonly AGH_FILE="${TARG_DIR}/AdGuardHome" readonly CONF_FILE="${TARG_DIR}/.config" readonly YAML_FILE="${AGH_FILE}.yaml" readonly YAML_BAK="${YAML_FILE}.bak" readonly YAML_ERR="${YAML_FILE}.err" readonly YAML_ORI="${TARG_DIR}/.AdGuardHome.yaml.ori" readonly ROLLBACK_RESULT_FILE="${BASE_DIR}/.AdGuardHome.rollback_result" readonly BLOCKLIST_ANALYZER_FILE="${TARG_DIR}/blocklist_analyzer.py" readonly BLOCKLIST_ANALYZER_URL="https://gist.githubusercontent.com/jumpsmm7/da69039cbee6eadd3b1d483c07c4bf19/raw/a4be7655095d6ff880c2f3748964b825d7c45bd2/blocklilst_analyzer.py" readonly BLOCKLIST_ANALYZER_SHA256_DEFAULT="77c21b6faeb7fea86b0729996dec5cc20d2d309fe8c6946c7371dd64134a6e5f" BLOCKLIST_ANALYZER_SHA256="" BLOCKLIST_ANALYZER_CANDIDATES_FILE="" BLOCKLIST_ANALYZER_CONFIGURED_IDS_FILE="" BLOCKLIST_ANALYZER_ORPHANED_IDS_FILE="" BLOCKLIST_ANALYZER_SELECTED_IDS_FILE="" SCRIPT_LOC="" SETUP_YAML_TMP_FILE="" BLOCKLIST_YAML_TMP_FILE="" MANAGED_HOOK_TMP_FILE="" if [ -r /proc/sys/kernel/random/uuid ]; then INSTALLER_CACHE_NONCE="$(cat /proc/sys/kernel/random/uuid 2>/dev/null)" fi [ -n "${INSTALLER_CACHE_NONCE:-}" ] || INSTALLER_CACHE_NONCE="$(date +%s)-$(sed 's/^.*) //' "/proc/$$/stat" 2>/dev/null | awk '{ print $20; exit }')-$$" readonly INSTALLER_CACHE_NONCE DOWNLOAD_CACHE_SEQUENCE=0 BOLD="$(printf "\033[1m")" || true readonly BOLD NORM="$(printf "\033[0m")" || true readonly NORM INFO="$(printf "%s" "${BOLD} Info: ${NORM}")" || true readonly INFO ERROR="$(printf "%s" "${BOLD} *** Error: ${NORM}")" || true readonly ERROR WARNING="$(printf "%s" "${BOLD} * Warning: ${NORM}")" || true readonly WARNING INPUT="$(printf "%s" "${BOLD} => ${NORM}")" || true readonly INPUT # Functions are grouped by purpose; names are sorted alpha-numerically within each group. # Bootstrap and package helpers ai_have_cmd() { if [ "$1" = "nvram" ]; then installer_nvram_path >/dev/null 2>&1 return "$?" fi which "$1" >/dev/null 2>&1 } # installer_nvram_path resolves nvram with which under a stock-only PATH. # It accepts only executable files at the two supported firmware locations. installer_nvram_path() { local nvram_binary nvram_binary="$(PATH=/sbin:/bin:/usr/sbin:/usr/bin which nvram 2>/dev/null)" || return 1 case "${nvram_binary}" in /bin/nvram | /usr/sbin/nvram) [ -f "${nvram_binary}" ] && [ -x "${nvram_binary}" ] || return 1 ;; *) return 1 ;; esac printf '%s\n' "${nvram_binary}" } installer_nvram() { local nvram_binary nvram_binary="$(installer_nvram_path)" || return 127 "${nvram_binary}" "$@" } # Installer-managed NVRAM transactions. Each key is read once into a private # snapshot before any write is attempted. Snapshot files are deliberately # simple because NVRAM values used by this installer do not contain newlines. nvram_transaction_begin() { local name status name="$1" shift case "${name}" in "" | *[!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.-]*) return 1 ;; esac NVRAM_TRANSACTION_BEGIN_STAGE="lock" nvram_transaction_lock_acquire || return 1 nvram_transaction_begin_locked "${name}" "$@" && return 0 status="$?" nvram_transaction_lock_release || true return "${status}" } # nvram_transaction_begin_locked creates a transaction snapshot while the caller owns the global lock. nvram_transaction_begin_locked() { local exists name key service_action snapshot_dir temp_dir name="$1" shift NVRAM_TRANSACTION_BEGIN_STAGE="prior-recovery" nvram_transaction_finalize_setup_pair recovery || return 1 NVRAM_TRANSACTION_BEGIN_STAGE="snapshot-storage" snapshot_dir="${BASE_DIR}/.AdGuardHome.nvram/${name}" temp_dir="${snapshot_dir}.tmp.$$" [ ! -L "${BASE_DIR}/.AdGuardHome.nvram" ] || return 1 if [ -e "${snapshot_dir}" ] || [ -L "${snapshot_dir}" ]; then [ ! -L "${snapshot_dir}" ] || return 1 if [ -f "${snapshot_dir}/dirty" ]; then case "${name}" in dnsfilter) service_action="restart_firewall;restart_dnsmasq" ;; dns-preparation | lan-domain) service_action="restart_dnsmasq" ;; *) service_action="-" ;; esac NVRAM_TRANSACTION_BEGIN_STAGE="prior-recovery" NVRAM_TRANSACTION_DIR="${snapshot_dir}" export NVRAM_TRANSACTION_DIR rollback_result_write "NVRAM transaction" "incomplete prior rollback detected" "restoring snapshot at ${snapshot_dir}" if nvram_transaction_restore "${service_action}" 0; then : # Successful restore, continue with new transaction else return 1 fi else rm -rf "${snapshot_dir}" || return 1 fi fi NVRAM_TRANSACTION_BEGIN_STAGE="snapshot-storage" rm -rf "${temp_dir}" || return 1 mkdir -p "${temp_dir}" || return 1 chmod 700 "${BASE_DIR}/.AdGuardHome.nvram" "${temp_dir}" 2>/dev/null || { rm -rf "${temp_dir}" return 1 } : >"${temp_dir}/keys" || return 1 chmod 600 "${temp_dir}/keys" || return 1 NVRAM_TRANSACTION_BEGIN_STAGE="snapshot-inventory" if ! installer_nvram show >"${temp_dir}/inventory" 2>/dev/null; then # Some HND firmware returns a failure status after emitting a usable # inventory. Reject only a failed read that produced no snapshot data. if [ ! -s "${temp_dir}/inventory" ]; then rm -rf "${temp_dir}" return 1 fi fi NVRAM_TRANSACTION_BEGIN_STAGE="snapshot-storage" chmod 600 "${temp_dir}/inventory" || { rm -rf "${temp_dir}" return 1 } for key in "$@"; do case "${key}" in "" | *[!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_]*) rm -rf "${temp_dir}" return 1 ;; esac exists="0" if /bin/grep -q "^${key}=" "${temp_dir}/inventory"; then exists="1" fi NVRAM_TRANSACTION_BEGIN_STAGE="snapshot-key:${key}" if ! installer_nvram get "${key}" >"${temp_dir}/${key}" 2>/dev/null && [ "${exists}" = "1" ]; then rm -rf "${temp_dir}" return 1 fi NVRAM_TRANSACTION_BEGIN_STAGE="snapshot-storage" chmod 600 "${temp_dir}/${key}" || { rm -rf "${temp_dir}" return 1 } if [ "${exists}" = "1" ]; then : >"${temp_dir}/exists.${key}" || { rm -rf "${temp_dir}" return 1 } chmod 600 "${temp_dir}/exists.${key}" || { rm -rf "${temp_dir}" return 1 } fi printf '%s\n' "${key}" >>"${temp_dir}/keys" || { rm -rf "${temp_dir}" return 1 } done rm -f "${temp_dir}/inventory" || { rm -rf "${temp_dir}" return 1 } rm -rf "${snapshot_dir}" || { rm -rf "${temp_dir}" return 1 } mv "${temp_dir}" "${snapshot_dir}" || { rm -rf "${temp_dir}" return 1 } NVRAM_TRANSACTION_DIR="${snapshot_dir}" NVRAM_TRANSACTION_CHANGED="0" export NVRAM_TRANSACTION_DIR NVRAM_TRANSACTION_CHANGED } nvram_transaction_finalize_setup_pair() { local marker marker_tmp mode snapshot_root mode="${1:-commit}" snapshot_root="${BASE_DIR}/.AdGuardHome.nvram" marker="${snapshot_root}/setup-committed" marker_tmp="${marker}.tmp.$$" if [ "${mode}" != "recovery" ]; then if [ ! -d "${snapshot_root}/lan-domain" ] && [ ! -d "${snapshot_root}/dnsfilter" ] && [ ! -d "${snapshot_root}/setup-files" ]; then return 0 fi mkdir -p "${snapshot_root}" || return 1 printf '%s\n' committed >"${marker_tmp}" || return 1 chmod 600 "${marker_tmp}" || { rm -f "${marker_tmp}" return 1 } mv "${marker_tmp}" "${marker}" || { rm -f "${marker_tmp}" return 1 } elif [ ! -f "${marker}" ]; then return 0 fi # The shared marker is the commit point. Once published, neither paired # transaction may be restored even if interruption splits this cleanup. rm -f "${snapshot_root}/lan-domain/dirty" "${snapshot_root}/dnsfilter/dirty" 2>/dev/null || true rm -rf "${snapshot_root}/lan-domain" "${snapshot_root}/dnsfilter" 2>/dev/null || true rm -rf "${snapshot_root}/setup-files" 2>/dev/null || true if [ -e "${snapshot_root}/lan-domain" ] || [ -L "${snapshot_root}/lan-domain" ] || [ -e "${snapshot_root}/dnsfilter" ] || [ -L "${snapshot_root}/dnsfilter" ] || [ -e "${snapshot_root}/setup-files" ] || [ -L "${snapshot_root}/setup-files" ]; then [ "${mode}" != "recovery" ] || return 1 elif [ ! -f "${snapshot_root}/dns-preparation/dirty" ] && [ ! -f "${snapshot_root}/dns-preparation/stubby-stopped" ] && { ! rm -f "${marker}" 2>/dev/null || [ -e "${marker}" ] || [ -L "${marker}" ]; }; then [ "${mode}" != "recovery" ] || return 1 fi return 0 } nvram_transaction_setup_committed() { [ -f "${BASE_DIR}/.AdGuardHome.nvram/setup-committed" ] } # nvram_transaction_setup_files_failure records setup-file failure context. nvram_transaction_setup_files_failure() { local artifact operation path operation="$1" path="$2" artifact="${3:-}" if [ -n "${artifact}" ]; then nvram_transaction_lock_failure "operation=${operation} path=${path} rolled-back=${artifact}" || true else nvram_transaction_lock_failure "operation=${operation} path=${path}" || true fi return 1 } # nvram_transaction_setup_files_begin snapshots setup files under the transaction lock. nvram_transaction_setup_files_begin() { local journal_entry journal_root journal_tmp source journal_root="${BASE_DIR}/.AdGuardHome.nvram/setup-files" journal_tmp="${journal_root}.tmp.$$" # Setup-file publication shares the NVRAM transaction lock so startup # recovery cannot mistake an active journal for an interrupted operation. nvram_transaction_lock_acquire || return 1 [ ! -L "${BASE_DIR}/.AdGuardHome.nvram" ] || nvram_transaction_setup_files_failure validate-journal-parent "${BASE_DIR}/.AdGuardHome.nvram" || return 1 [ ! -e "${journal_root}" ] && [ ! -L "${journal_root}" ] || nvram_transaction_setup_files_failure validate-journal-path "${journal_root}" || return 1 rm -rf "${journal_tmp}" || nvram_transaction_setup_files_failure remove-stale-journal "${journal_tmp}" || return 1 mkdir -p "${journal_tmp}" || nvram_transaction_setup_files_failure create-journal "${journal_tmp}" || return 1 chmod 700 "${journal_tmp}" || { nvram_transaction_setup_files_failure chmod-journal "${journal_tmp}" "${journal_tmp}" || true rm -rf "${journal_tmp}" return 1 } for source in yaml-file yaml-original config; do journal_entry="${journal_tmp}/${source}" case "${source}" in yaml-file) if [ "${YAML_BACKED_UP:-0}" -eq 1 ] && [ -f "${YAML_BAK}" ]; then cp -p "${YAML_BAK}" "${journal_entry}" || { nvram_transaction_setup_files_failure copy-journal-entry "${journal_entry}" "${journal_tmp}" || true rm -rf "${journal_tmp}" return 1 } elif [ -f "${YAML_FILE}" ]; then cp -p "${YAML_FILE}" "${journal_entry}" || { nvram_transaction_setup_files_failure copy-journal-entry "${journal_entry}" "${journal_tmp}" || true rm -rf "${journal_tmp}" return 1 } else journal_entry="${journal_entry}.absent" : >"${journal_entry}" || { nvram_transaction_setup_files_failure create-journal-entry "${journal_entry}" "${journal_tmp}" || true rm -rf "${journal_tmp}" return 1 } fi ;; yaml-original) if [ -f "${YAML_ORI}" ]; then cp -p "${YAML_ORI}" "${journal_entry}" || { nvram_transaction_setup_files_failure copy-journal-entry "${journal_entry}" "${journal_tmp}" || true rm -rf "${journal_tmp}" return 1 } else journal_entry="${journal_entry}.absent" : >"${journal_entry}" || { nvram_transaction_setup_files_failure create-journal-entry "${journal_entry}" "${journal_tmp}" || true rm -rf "${journal_tmp}" return 1 } fi ;; config) if [ -f "${CONF_FILE}" ]; then cp -p "${CONF_FILE}" "${journal_entry}" || { nvram_transaction_setup_files_failure copy-journal-entry "${journal_entry}" "${journal_tmp}" || true rm -rf "${journal_tmp}" return 1 } else journal_entry="${journal_entry}.absent" : >"${journal_entry}" || { nvram_transaction_setup_files_failure create-journal-entry "${journal_entry}" "${journal_tmp}" || true rm -rf "${journal_tmp}" return 1 } fi ;; esac chmod 600 "${journal_entry}" || { nvram_transaction_setup_files_failure chmod-journal-entry "${journal_entry}" "${journal_tmp}" || true rm -rf "${journal_tmp}" return 1 } done mv "${journal_tmp}" "${journal_root}" || { nvram_transaction_setup_files_failure publish-journal "${journal_root}" "${journal_tmp}" || true rm -rf "${journal_tmp}" return 1 } } nvram_transaction_setup_files_restore() { local journal_done journal_root source stage target journal_root="${BASE_DIR}/.AdGuardHome.nvram/setup-files" [ ! -L "${journal_root}" ] || return 1 [ -d "${journal_root}" ] || return 0 for source in yaml-file yaml-original config; do case "${source}" in yaml-file) target="${YAML_FILE}" ;; yaml-original) target="${YAML_ORI}" ;; config) target="${CONF_FILE}" ;; esac stage="${target}.setup-restore.$$" if [ -f "${journal_root}/${source}" ]; then if cp -p "${journal_root}/${source}" "${stage}" && mv -f "${stage}" "${target}"; then : else rm -f "${stage}" return 1 fi elif [ -f "${journal_root}/${source}.absent" ]; then rm -f "${target}" || return 1 else return 1 fi done journal_done="${journal_root}.restored.$$" [ ! -e "${journal_done}" ] && [ ! -L "${journal_done}" ] || return 1 # Atomically remove the completed journal from the pending path before # best-effort cleanup so interrupted deletion cannot trigger a partial retry. mv "${journal_root}" "${journal_done}" || return 1 rm -rf "${journal_done}" 2>/dev/null || true } nvram_transaction_recover_pending() { local failed paired_committed safe_failure service_action snapshot_dir snapshot_root stubby_recovery transaction NVRAM_TRANSACTION_RECOVERY_DIAGNOSTIC_SAFE="0" export NVRAM_TRANSACTION_RECOVERY_DIAGNOSTIC_SAFE snapshot_root="${BASE_DIR}/.AdGuardHome.nvram" [ ! -L "${snapshot_root}" ] || return 1 if [ ! -f "${snapshot_root}/setup-committed" ] && [ ! -d "${snapshot_root}/setup-files" ] && [ ! -f "${snapshot_root}/lan-domain/dirty" ] && [ ! -f "${snapshot_root}/dnsfilter/dirty" ] && [ ! -f "${snapshot_root}/dns-preparation/dirty" ] && [ ! -f "${snapshot_root}/dns-preparation/stubby-stopped" ] && [ ! -f "${snapshot_root}/jffs-enable/dirty" ]; then return 0 fi nvram_transaction_lock_acquire || return 1 failed="0" safe_failure="0" paired_committed="0" [ ! -f "${snapshot_root}/setup-committed" ] || paired_committed="1" if ! nvram_transaction_finalize_setup_pair recovery; then failed="1" [ "${paired_committed}" = "0" ] || safe_failure="1" fi if [ "${paired_committed}" = "0" ] && ! nvram_transaction_setup_files_restore; then failed="1" safe_failure="0" fi for transaction in lan-domain dnsfilter dns-preparation jffs-enable; do case "${paired_committed}:${transaction}" in 1:lan-domain | 1:dnsfilter) continue ;; esac snapshot_dir="${snapshot_root}/${transaction}" [ ! -L "${snapshot_dir}" ] || { failed="1" safe_failure="0" continue } if [ "${paired_committed}:${transaction}" = "1:dns-preparation" ]; then # Committed DNS preparation intentionally leaves stubby stopped. rm -f "${snapshot_dir}/dirty" "${snapshot_dir}/stubby-stopped" || { failed="1" safe_failure="0" continue } rm -rf "${snapshot_dir}" 2>/dev/null || true if [ "${failed}" = "0" ] && ! rm -f "${snapshot_root}/setup-committed"; then failed="1" safe_failure="1" fi continue fi [ -f "${snapshot_dir}/dirty" ] || { [ "${transaction}" = "dns-preparation" ] && [ -f "${snapshot_dir}/stubby-stopped" ]; } || continue case "${transaction}" in dnsfilter) service_action="restart_firewall;restart_dnsmasq" ;; jffs-enable) service_action="-" ;; *) service_action="restart_dnsmasq" ;; esac NVRAM_TRANSACTION_DIR="${snapshot_dir}" export NVRAM_TRANSACTION_DIR stubby_recovery="0" [ "${transaction}" != "dns-preparation" ] || [ ! -f "${snapshot_dir}/stubby-stopped" ] || stubby_recovery="1" if ! nvram_transaction_restore "${service_action}" "${stubby_recovery}"; then failed="1" safe_failure="0" elif [ "${stubby_recovery}" = "1" ] && ! service restart_stubby >/dev/null 2>&1; then failed="1" safe_failure="0" : >"${snapshot_dir}/dirty" || continue elif [ "${stubby_recovery}" = "1" ]; then rm -f "${snapshot_dir}/dirty" || { failed="1" safe_failure="0" continue } rm -rf "${snapshot_dir}" 2>/dev/null || true fi done if ! nvram_transaction_lock_release; then failed="1" safe_failure="0" fi if [ "${failed}" != "0" ] && [ "${safe_failure}" = "1" ]; then NVRAM_TRANSACTION_RECOVERY_DIAGNOSTIC_SAFE="1" export NVRAM_TRANSACTION_RECOVERY_DIAGNOSTIC_SAFE fi [ "${failed}" = "0" ] } nvram_transaction_recover_startup() { # A validated update re-exec still owns the live transaction lock and its # DNS preparation snapshot. Leave both intact for the resumed update. if [ "${AI_REEXECED_INSTALLER:-0}" = "1" ] && nvram_transaction_lock_owned; then return 0 fi nvram_transaction_recover_pending } # Recognize diagnostics permitted after a recoverable NVRAM cleanup failure. nvram_transaction_read_only_diagnostic_requested() { local argument for argument; do shift [ "${argument}" = "--no-auto-update" ] || set -- "$@" "${argument}" done case "${1:-}" in status | support-export) return 0 ;; doctor) [ -z "${2:-}" ] && return 0 ;; esac case "${2:-}" in status) return 0 ;; doctor) [ -z "${3:-}" ] && return 0 ;; esac return 1 } nvram_transaction_lock_cross_reaper_release() { nvram_transaction_lock_reaper_release "$1.reaper" "$2" } # nvram_transaction_lock_failure publishes an actionable lock diagnostic. nvram_transaction_lock_failure() { NVRAM_TRANSACTION_LOCK_DIAGNOSTIC="$1" export NVRAM_TRANSACTION_LOCK_DIAGNOSTIC return 1 } # nvram_transaction_lock_acquire acquires a shared descriptor or portable lock. nvram_transaction_lock_acquire() { local lock_dir lock_owner lock_path owner status lock_path="${BASE_DIR}/.AdGuardHome.nvram.lock" NVRAM_TRANSACTION_LOCK_DIAGNOSTIC="" export NVRAM_TRANSACTION_LOCK_DIAGNOSTIC if [ -n "${NVRAM_TRANSACTION_LOCK_RELEASE_PHASE:-}" ]; then nvram_transaction_lock_release || return 1 fi case "${NVRAM_TRANSACTION_LOCK_MODE:-}" in flock) nvram_transaction_lock_flock_owned "${lock_path}" && return 0 ;; symlink) owner="$(nvram_transaction_lock_owner_current)" || return 1 [ "$(nvram_transaction_lock_readlink "${lock_path}.symlink" 2>/dev/null)" = "${owner}" ] && return 0 ;; mkdir) owner="$(nvram_transaction_lock_owner_current)" || return 1 [ "$(cat "${lock_path}.d/pid" 2>/dev/null)" = "${owner}" ] && return 0 ;; esac NVRAM_TRANSACTION_LOCK_MODE="" if nvram_transaction_lock_flock_supports_fd && nvram_transaction_lock_readlink /proc/$$/fd/0 >/dev/null 2>&1; then [ ! -L "${lock_path}" ] || nvram_transaction_lock_failure "operation=validate-flock-path path=${lock_path} reason=symlink-not-allowed" || return 1 owner="$(nvram_transaction_lock_owner_current)" || nvram_transaction_lock_failure "operation=identify-current-process path=/proc/$$/stat" || return 1 nvram_transaction_lock_reaper_acquire "${lock_path}.reaper" "${owner}" || { [ -n "${NVRAM_TRANSACTION_LOCK_DIAGNOSTIC:-}" ] || nvram_transaction_lock_failure "operation=acquire-reaper path=${lock_path}.reaper" || true return 1 } lock_dir="${lock_path}.d" if [ -e "${lock_dir}" ] || [ -L "${lock_dir}" ]; then [ -d "${lock_dir}" ] && [ ! -L "${lock_dir}" ] || { nvram_transaction_lock_failure "operation=validate-mkdir-lock path=${lock_dir} reason=unsafe-artifact" || true nvram_transaction_lock_reaper_release "${lock_path}.reaper" "${owner}" return 1 } lock_owner="$(cat "${lock_dir}/pid" 2>/dev/null)" if nvram_transaction_lock_owner_live "${lock_owner}"; then nvram_transaction_lock_failure "operation=validate-stale-lock path=${lock_dir}/pid reason=live-owner owner=${lock_owner}" || true nvram_transaction_lock_reaper_release "${lock_path}.reaper" "${owner}" return 1 fi rm -rf "${lock_dir}" || { nvram_transaction_lock_failure "operation=remove-stale-lock path=${lock_dir} reason=cleanup-failed" || true nvram_transaction_lock_reaper_release "${lock_path}.reaper" "${owner}" return 1 } fi # Publish the same ownership artifact used by fallback locking so every # capability mode contends on one mutually exclusive gate. Reclaim a # descriptor owner's stale publication after its kernel lock disappeared. if [ -L "${lock_path}.symlink" ]; then lock_owner="$(nvram_transaction_lock_readlink "${lock_path}.symlink" 2>/dev/null)" if [ -n "${lock_owner}" ] && nvram_transaction_lock_owner_live "${lock_owner}"; then nvram_transaction_lock_failure "operation=validate-stale-lock path=${lock_path}.symlink reason=live-owner owner=${lock_owner}" || true nvram_transaction_lock_cross_reaper_release "${lock_path}" "${owner}" return 1 fi [ "$(nvram_transaction_lock_readlink "${lock_path}.symlink" 2>/dev/null)" = "${lock_owner}" ] || { nvram_transaction_lock_cross_reaper_release "${lock_path}" "${owner}" return 1 } rm -f "${lock_path}.symlink" || { nvram_transaction_lock_cross_reaper_release "${lock_path}" "${owner}" return 1 } fi ln -s "${owner}" "${lock_path}.symlink" 2>/dev/null || { nvram_transaction_lock_failure "operation=publish-symlink path=${lock_path}.symlink" || true nvram_transaction_lock_cross_reaper_release "${lock_path}" "${owner}" return 1 } : >>"${lock_path}" 2>/dev/null || { nvram_transaction_lock_failure "operation=create-flock-file path=${lock_path} rolled-back=${lock_path}.symlink" || true rm -f "${lock_path}.symlink" nvram_transaction_lock_cross_reaper_release "${lock_path}" "${owner}" return 1 } if ! (nvram_transaction_lock_flock_open_fd probe "${lock_path}") 2>/dev/null; then nvram_transaction_lock_failure "operation=open-flock-file path=${lock_path} rolled-back=${lock_path}.symlink reason=probe-failed" || true rm -f "${lock_path}.symlink" nvram_transaction_lock_cross_reaper_release "${lock_path}" "${owner}" return 1 fi nvram_transaction_lock_flock_open_fd acquire "${lock_path}" || { nvram_transaction_lock_failure "operation=open-flock-file path=${lock_path} rolled-back=${lock_path}.symlink reason=acquire-failed" || true rm -f "${lock_path}.symlink" nvram_transaction_lock_cross_reaper_release "${lock_path}" "${owner}" return 1 } if /usr/bin/flock -n 8 >/dev/null 2>&1; then nvram_transaction_lock_cross_reaper_release "${lock_path}" "${owner}" || { [ -n "${NVRAM_TRANSACTION_LOCK_DIAGNOSTIC:-}" ] || nvram_transaction_lock_failure "operation=release-reaper path=${lock_path}.reaper rolled-back=${lock_path}.symlink" || true /usr/bin/flock -u 8 >/dev/null 2>&1 exec 8>&- rm -f "${lock_path}.symlink" return 1 } NVRAM_TRANSACTION_LOCK_MODE="flock" NVRAM_TRANSACTION_LOCK_DIAGNOSTIC="" return 0 fi nvram_transaction_lock_failure "operation=flock-acquire path=${lock_path} rolled-back=${lock_path}.symlink" || true exec 8>&- rm -f "${lock_path}.symlink" nvram_transaction_lock_cross_reaper_release "${lock_path}" "${owner}" return 1 fi # An unavailable descriptor lock is a capability miss, not the reason a # later portable fallback failed. NVRAM_TRANSACTION_LOCK_DIAGNOSTIC="" export NVRAM_TRANSACTION_LOCK_DIAGNOSTIC nvram_transaction_lock_symlink_acquire status="$?" case "${status}" in 0) NVRAM_TRANSACTION_LOCK_MODE="symlink" NVRAM_TRANSACTION_LOCK_DIAGNOSTIC="" return 0 ;; 1) [ -n "${NVRAM_TRANSACTION_LOCK_DIAGNOSTIC:-}" ] || nvram_transaction_lock_failure "operation=symlink-lock-acquire path=${lock_path}.symlink reason=terminal-fallback-failure" || true return 1 ;; 2) ;; *) nvram_transaction_lock_failure "operation=symlink-lock-acquire path=${lock_path}.symlink reason=unexpected-status-${status}" || true return 1 ;; esac owner="$(nvram_transaction_lock_owner_current)" || nvram_transaction_lock_failure "operation=identify-current-process path=/proc/$$/stat" || return 1 lock_dir="${lock_path}.d" nvram_transaction_lock_reaper_acquire "${lock_path}.reaper" "${owner}" || { [ -n "${NVRAM_TRANSACTION_LOCK_DIAGNOSTIC:-}" ] || nvram_transaction_lock_failure "operation=acquire-reaper path=${lock_path}.reaper" || true return 1 } if [ -e "${lock_dir}" ] || [ -L "${lock_dir}" ]; then [ -d "${lock_dir}" ] && [ ! -L "${lock_dir}" ] || { nvram_transaction_lock_failure "operation=validate-mkdir-lock path=${lock_dir} reason=unsafe-artifact" || true nvram_transaction_lock_reaper_release "${lock_path}.reaper" "${owner}" return 1 } lock_owner="$(cat "${lock_dir}/pid" 2>/dev/null)" if nvram_transaction_lock_owner_live "${lock_owner}"; then nvram_transaction_lock_failure "operation=validate-stale-lock path=${lock_dir}/pid reason=live-owner owner=${lock_owner}" || true nvram_transaction_lock_reaper_release "${lock_path}.reaper" "${owner}" return 1 fi rm -rf "${lock_dir}" || { nvram_transaction_lock_failure "operation=remove-stale-lock path=${lock_dir} reason=cleanup-failed" || true nvram_transaction_lock_reaper_release "${lock_path}.reaper" "${owner}" return 1 } fi mkdir "${lock_dir}" 2>/dev/null || { nvram_transaction_lock_failure "operation=mkdir-lock path=${lock_dir} reason=publication-failed" || true nvram_transaction_lock_reaper_release "${lock_path}.reaper" "${owner}" return 1 } printf '%s\n' "${owner}" >"${lock_dir}/pid" || { nvram_transaction_lock_failure "operation=publish-owner-file path=${lock_dir}/pid rolled-back=${lock_dir}" || true rm -rf "${lock_dir}" nvram_transaction_lock_reaper_release "${lock_path}.reaper" "${owner}" return 1 } chmod 700 "${lock_dir}" 2>/dev/null || { nvram_transaction_lock_failure "operation=chmod-lock path=${lock_dir} rolled-back=${lock_dir}" || true rm -rf "${lock_dir}" nvram_transaction_lock_reaper_release "${lock_path}.reaper" "${owner}" return 1 } [ "$(cat "${lock_dir}/pid" 2>/dev/null)" = "${owner}" ] || { nvram_transaction_lock_failure "operation=validate-owner-file path=${lock_dir}/pid rolled-back=${lock_dir}" || true rm -rf "${lock_dir}" nvram_transaction_lock_reaper_release "${lock_path}.reaper" "${owner}" return 1 } nvram_transaction_lock_reaper_release "${lock_path}.reaper" "${owner}" || { [ -n "${NVRAM_TRANSACTION_LOCK_DIAGNOSTIC:-}" ] || nvram_transaction_lock_failure "operation=release-reaper path=${lock_path}.reaper rolled-back=${lock_dir}" || true # Do not continue while the reaper remains active. Roll back only a # transaction lock whose ownership can still be verified as ours. if [ "$(cat "${lock_dir}/pid" 2>/dev/null)" = "${owner}" ]; then if ! rm -rf "${lock_dir}" 2>/dev/null || [ -e "${lock_dir}" ] || [ -L "${lock_dir}" ]; then printf '%s\n' "Error: unable to roll back owned NVRAM transaction lock ${lock_dir}; stopping recovery." >&2 exit 1 fi fi return 1 } NVRAM_TRANSACTION_LOCK_MODE="mkdir" NVRAM_TRANSACTION_LOCK_DIAGNOSTIC="" } nvram_transaction_lock_owned() { local lock_path owner lock_path="${BASE_DIR}/.AdGuardHome.nvram.lock" case "${NVRAM_TRANSACTION_LOCK_MODE:-}" in flock) nvram_transaction_lock_flock_owned "${lock_path}" ;; symlink) owner="$(nvram_transaction_lock_owner_current)" || return 1 [ "$(nvram_transaction_lock_readlink "${lock_path}.symlink" 2>/dev/null)" = "${owner}" ] ;; mkdir) owner="$(nvram_transaction_lock_owner_current)" || return 1 [ "$(cat "${lock_path}.d/pid" 2>/dev/null)" = "${owner}" ] ;; *) return 1 ;; esac } nvram_transaction_lock_owner_current() { local owner_pid owner_start owner_pid="${1:-$$}" case "${owner_pid}" in "" | *[!0-9]*) return 1 ;; esac owner_start="$(sed 's/^.*) //' "/proc/${owner_pid}/stat" 2>/dev/null | awk '{print $20}')" case "${owner_start}" in "" | *[!0-9]*) return 1 ;; esac printf '%s:%s\n' "${owner_pid}" "${owner_start}" } nvram_transaction_lock_owner_live() { local current owner owner_pid owner_start owner="$1" case "${owner}" in *:*) ;; *) return 1 ;; esac owner_pid="${owner%%:*}" owner_start="${owner#*:}" case "${owner_pid}:${owner_start}" in *[!0123456789:]*) return 1 ;; esac current="$(nvram_transaction_lock_owner_current "${owner_pid}")" || return 1 [ "${current}" = "${owner}" ] } # nvram_transaction_lock_owner_filename_suffix validates an owner identity and prints a filename-safe suffix. nvram_transaction_lock_owner_filename_suffix() { local owner owner_pid owner_start owner="$1" case "${owner}" in *:*:*) return 1 ;; *:*) ;; *) return 1 ;; esac owner_pid="${owner%%:*}" owner_start="${owner#*:}" [ -n "${owner_pid}" ] && [ -n "${owner_start}" ] || return 1 case "${owner_pid}:${owner_start}" in *[!0123456789:]*) return 1 ;; esac printf '%s.%s\n' "${owner_pid}" "${owner_start}" } nvram_transaction_lock_flock_owned() { local descriptor_path lock_path owner resolved_lock_path lock_path="$1" [ -e "/proc/$$/fd/8" ] || return 1 descriptor_path="$(nvram_transaction_lock_readlink -f "/proc/$$/fd/8" 2>/dev/null)" || return 1 resolved_lock_path="$(nvram_transaction_lock_readlink -f "${lock_path}" 2>/dev/null)" || return 1 [ "${descriptor_path}" = "${resolved_lock_path}" ] || return 1 owner="$(nvram_transaction_lock_owner_current)" || return 1 [ "$(nvram_transaction_lock_readlink "${lock_path}.symlink" 2>/dev/null)" = "${owner}" ] } nvram_transaction_lock_flock_unlock() { /usr/bin/flock -u 8 >/dev/null 2>&1 } nvram_transaction_lock_reaper_flock_acquire() { /usr/bin/flock -n 9 >/dev/null 2>&1 } # nvram_transaction_lock_reaper_claim_mkdir creates a candidate reaper claim directory. nvram_transaction_lock_reaper_claim_mkdir() { /bin/mkdir "$1" } # nvram_transaction_lock_reaper_claim_owner_write records the candidate owner. nvram_transaction_lock_reaper_claim_owner_write() { printf '%s\n' "$2" >"$1/pid" } # nvram_transaction_lock_reaper_claim_owner_secure restricts the candidate owner record. nvram_transaction_lock_reaper_claim_owner_secure() { /bin/chmod 600 "$1/pid" } # nvram_transaction_lock_reaper_claim_publish atomically publishes a prepared sibling directory. nvram_transaction_lock_reaper_claim_publish() { /bin/mv "$1" "$2" } # nvram_transaction_lock_reaper_claim_remove removes a verified current-process candidate. nvram_transaction_lock_reaper_claim_remove() { /bin/rm -rf "$1" } # nvram_transaction_lock_reaper_claim_cleanup removes every bounded candidate still owned by this process. nvram_transaction_lock_reaper_claim_cleanup() { local cleanup_path owner owner_suffix reaper_path retry suffix reaper_path="$1" owner="$2" owner_suffix="$(nvram_transaction_lock_owner_filename_suffix "${owner}")" || return 1 retry=0 while [ "${retry}" -lt 16 ]; do if [ "${retry}" -eq 0 ]; then suffix="" else suffix=".${retry}" fi cleanup_path="${reaper_path}.claim.${owner_suffix}${suffix}" if [ -d "${cleanup_path}" ] && [ ! -L "${cleanup_path}" ] && [ -f "${cleanup_path}/pid" ] && [ ! -L "${cleanup_path}/pid" ] && [ "$(cat "${cleanup_path}/pid" 2>/dev/null)" = "${owner}" ]; then nvram_transaction_lock_reaper_claim_remove "${cleanup_path}" || { nvram_transaction_lock_failure "operation=cleanup-reaper-claim candidate=${cleanup_path} destination=${reaper_path} reason=remove-failed owner=${owner} after-failed-operation=$3" || true return 1 } fi retry="$((retry + 1))" done } # nvram_transaction_lock_reaper_temp_symlink_create creates a candidate reaper ownership symlink. nvram_transaction_lock_reaper_temp_symlink_create() { /bin/ln -s "$1" "$2" } # nvram_transaction_lock_reaper_remove_owned removes a reaper directory after its owner is verified. nvram_transaction_lock_reaper_remove_owned() { /bin/rm -rf "$1" } # nvram_transaction_lock_reaper_remove_empty removes an empty unowned reaper directory. nvram_transaction_lock_reaper_remove_empty() { /bin/rmdir "$1" } # nvram_transaction_lock_reaper_legacy_release releases a portable reaper lock. nvram_transaction_lock_reaper_legacy_release() { local owner reaper_path reaper_path="$1" owner="$2" if [ ! -e "${reaper_path}" ] && [ ! -L "${reaper_path}" ]; then # A returning signal trap may have completed this release and cleared # the tracked state while the interrupted release was still running. if [ -z "${NVRAM_TRANSACTION_REAPER_LOCK_MODE:-}" ] && [ -z "${NVRAM_TRANSACTION_REAPER_LOCK_PATH:-}" ]; then return 0 fi [ "${NVRAM_TRANSACTION_REAPER_LOCK_PATH:-}" = "${reaper_path}" ] || return 1 if [ "${NVRAM_TRANSACTION_REAPER_LOCK_MODE:-}" = "legacy" ]; then NVRAM_TRANSACTION_REAPER_LOCK_MODE="" NVRAM_TRANSACTION_REAPER_LOCK_PATH="" fi return 0 fi if [ -f "${reaper_path}/pid" ]; then [ "$(cat "${reaper_path}/pid" 2>/dev/null)" = "${owner}" ] || { nvram_transaction_lock_failure "operation=release-reaper path=${reaper_path}/pid reason=owner-mismatch owner=${owner}" || true return 1 } nvram_transaction_lock_reaper_remove_owned "${reaper_path}" || { nvram_transaction_lock_failure "operation=release-reaper path=${reaper_path} reason=remove-owned-reaper-failed owner=${owner}" || true return 1 } else [ "${NVRAM_TRANSACTION_REAPER_LOCK_MODE:-}" = "legacy" ] || return 1 nvram_transaction_lock_reaper_remove_empty "${reaper_path}" 2>/dev/null || { nvram_transaction_lock_failure "operation=release-reaper path=${reaper_path} reason=remove-empty-reaper-failed" || true return 1 } fi if [ "${NVRAM_TRANSACTION_REAPER_LOCK_MODE:-}" = "legacy" ] && [ "${NVRAM_TRANSACTION_REAPER_LOCK_PATH:-}" = "${reaper_path}" ]; then NVRAM_TRANSACTION_REAPER_LOCK_MODE="" NVRAM_TRANSACTION_REAPER_LOCK_PATH="" fi } # nvram_transaction_lock_reaper_legacy_claim publishes a portable claim directory with its original owner identity. nvram_transaction_lock_reaper_legacy_claim() { local claim_name claim_path cleanup_diagnostic cleanup_path owner owner_suffix primary_diagnostic reaper_path retry slot suffix reaper_path="$1" owner="$2" owner_suffix="$(nvram_transaction_lock_owner_filename_suffix "${owner}")" || { nvram_transaction_lock_failure "operation=create-reaper-claim candidate=unavailable destination=${reaper_path} reason=invalid-owner owner=${owner}" || true return 1 } retry=0 claim_path="" while [ "${retry}" -lt 32 ]; do slot="$((retry % 16))" if [ "${slot}" -eq 0 ]; then suffix="" else suffix=".${slot}" fi claim_path="${reaper_path}.claim.${owner_suffix}${suffix}" if nvram_transaction_lock_reaper_claim_mkdir "${claim_path}"; then break fi if [ ! -e "${claim_path}" ] && [ ! -L "${claim_path}" ]; then nvram_transaction_lock_failure "operation=create-reaper-claim candidate=${claim_path} destination=${reaper_path} reason=mkdir-failed" || true nvram_transaction_lock_reaper_claim_cleanup "${reaper_path}" "${owner}" create-reaper-claim || true return 1 fi if [ -d "${claim_path}" ] && [ ! -L "${claim_path}" ] && [ -f "${claim_path}/pid" ] && [ ! -L "${claim_path}/pid" ] && [ "$(cat "${claim_path}/pid" 2>/dev/null)" = "${owner}" ]; then nvram_transaction_lock_reaper_claim_remove "${claim_path}" || { nvram_transaction_lock_failure "operation=cleanup-reaper-claim candidate=${claim_path} destination=${reaper_path} reason=remove-failed owner=${owner}" || true return 1 } fi retry="$((retry + 1))" done if [ "${retry}" -ge 32 ]; then nvram_transaction_lock_failure "operation=create-reaper-claim candidate=${claim_path} destination=${reaper_path} reason=candidates-exhausted owner=${owner}" || true nvram_transaction_lock_reaper_claim_cleanup "${reaper_path}" "${owner}" create-reaper-claim || true return 1 fi claim_name="${claim_path##*/}" nvram_transaction_lock_reaper_claim_owner_write "${claim_path}" "${owner}" || { nvram_transaction_lock_failure "operation=write-reaper-claim-owner candidate=${claim_path} destination=${reaper_path} path=${claim_path}/pid reason=write-failed owner=${owner}" || true primary_diagnostic="${NVRAM_TRANSACTION_LOCK_DIAGNOSTIC:-}" nvram_transaction_lock_reaper_claim_remove "${claim_path}" || { nvram_transaction_lock_failure "operation=cleanup-reaper-claim candidate=${claim_path} destination=${reaper_path} reason=remove-failed owner=${owner} after-failed-operation=write-reaper-claim-owner" || true cleanup_diagnostic="${NVRAM_TRANSACTION_LOCK_DIAGNOSTIC:-}" nvram_transaction_lock_failure "${primary_diagnostic} after-failed-operation=write-reaper-claim-owner cleanup-diagnostic=${cleanup_diagnostic}" || true return 1 } nvram_transaction_lock_reaper_claim_cleanup "${reaper_path}" "${owner}" write-reaper-claim-owner || true return 1 } nvram_transaction_lock_reaper_claim_owner_secure "${claim_path}" || { nvram_transaction_lock_failure "operation=secure-reaper-claim-owner candidate=${claim_path} destination=${reaper_path} path=${claim_path}/pid reason=permission-failed owner=${owner}" || true nvram_transaction_lock_reaper_claim_cleanup "${reaper_path}" "${owner}" secure-reaper-claim-owner || true return 1 } if [ ! -f "${claim_path}/pid" ] || [ -L "${claim_path}/pid" ] || [ "$(cat "${claim_path}/pid" 2>/dev/null)" != "${owner}" ]; then nvram_transaction_lock_failure "operation=verify-reaper-claim-owner candidate=${claim_path} destination=${reaper_path} path=${claim_path}/pid reason=owner-verification-failed owner=${owner}" || true nvram_transaction_lock_reaper_claim_cleanup "${reaper_path}" "${owner}" verify-reaper-claim-owner || true return 1 fi if nvram_transaction_lock_reaper_claim_publish "${claim_path}" "${reaper_path}"; then if [ ! -d "${reaper_path}/${claim_name}" ] && [ -f "${reaper_path}/pid" ] && [ ! -L "${reaper_path}/pid" ] && [ "$(cat "${reaper_path}/pid" 2>/dev/null)" = "${owner}" ]; then return 0 fi nvram_transaction_lock_failure "operation=publish-reaper-claim candidate=${claim_path} destination=${reaper_path} reason=contender-published owner=${owner}" || true else if [ -e "${reaper_path}" ] || [ -L "${reaper_path}" ]; then nvram_transaction_lock_failure "operation=publish-reaper-claim candidate=${claim_path} destination=${reaper_path} reason=contender-published owner=${owner}" || true else nvram_transaction_lock_failure "operation=publish-reaper-claim candidate=${claim_path} destination=${reaper_path} reason=rename-rejected owner=${owner}" || true fi fi # A destination directory may cause mv to nest the candidate. Remove only # artifacts whose non-symlink owner record still identifies this process. for cleanup_path in "${claim_path}" "${reaper_path}/${claim_name}"; do if [ -d "${cleanup_path}" ] && [ ! -L "${cleanup_path}" ] && [ -f "${cleanup_path}/pid" ] && [ ! -L "${cleanup_path}/pid" ] && [ "$(cat "${cleanup_path}/pid" 2>/dev/null)" = "${owner}" ]; then nvram_transaction_lock_reaper_claim_remove "${cleanup_path}" || { nvram_transaction_lock_failure "operation=cleanup-reaper-claim candidate=${cleanup_path} destination=${reaper_path} reason=remove-failed owner=${owner} after-failed-operation=publish-reaper-claim" || true return 1 } fi done nvram_transaction_lock_reaper_claim_cleanup "${reaper_path}" "${owner}" publish-reaper-claim || return 1 return 1 } # nvram_transaction_lock_reaper_acquire_impl acquires the best supported reaper lock. nvram_transaction_lock_reaper_acquire_impl() { local claimed contending_owner owner owner_suffix reaper_owner reaper_path reaper_path="$1" claimed=0 NVRAM_TRANSACTION_REAPER_LOCK_MODE="" NVRAM_TRANSACTION_REAPER_LOCK_PATH="${reaper_path}" NVRAM_TRANSACTION_REAPER_TEMP_PATH="" if [ -n "${2:-}" ]; then owner="$2" else owner="$(nvram_transaction_lock_owner_current)" || nvram_transaction_lock_failure "operation=acquire-reaper path=${reaper_path} reason=current-owner-unavailable" || return 1 fi owner_suffix="$(nvram_transaction_lock_owner_filename_suffix "${owner}")" || { nvram_transaction_lock_failure "operation=acquire-reaper path=${reaper_path} reason=invalid-owner owner=${owner}" || true NVRAM_TRANSACTION_REAPER_LOCK_PATH="" return 1 } # Publish the directory and its owner together. A prepared sibling is # renamed into place so SIGKILL cannot leave a new ownerless mutex behind. # Arm cleanup before the rename because a returning signal trap may run # after mv publishes the complete directory but before mv returns. NVRAM_TRANSACTION_REAPER_LOCK_MODE="legacy" if nvram_transaction_lock_reaper_legacy_claim "${reaper_path}" "${owner}"; then claimed=1 else NVRAM_TRANSACTION_REAPER_LOCK_MODE="" if [ ! -e "${reaper_path}" ] && [ ! -L "${reaper_path}" ]; then [ -n "${NVRAM_TRANSACTION_LOCK_DIAGNOSTIC:-}" ] || nvram_transaction_lock_failure "operation=acquire-reaper path=${reaper_path} reason=publication-failed owner=${owner}" || true NVRAM_TRANSACTION_REAPER_LOCK_PATH="" return 1 fi reaper_owner="$(cat "${reaper_path}/pid" 2>/dev/null)" if [ -z "${reaper_owner}" ]; then # Older installers publish the PID after mkdir. An empty record may # therefore still have a live publisher and cannot be reclaimed safely. nvram_transaction_lock_failure "operation=acquire-reaper path=${reaper_path}/pid reason=missing-owner" || true NVRAM_TRANSACTION_REAPER_LOCK_PATH="" return 1 fi if [ "${reaper_owner}" != "${owner}" ]; then if nvram_transaction_lock_owner_live "${reaper_owner}"; then nvram_transaction_lock_failure "operation=acquire-reaper path=${reaper_path} reason=live-owner owner=${reaper_owner}" || true return 1 fi [ "$(cat "${reaper_path}/pid" 2>/dev/null)" = "${reaper_owner}" ] || nvram_transaction_lock_failure "operation=acquire-reaper path=${reaper_path}/pid reason=owner-changed owner=${reaper_owner}" || return 1 rm -rf "${reaper_path}" || nvram_transaction_lock_failure "operation=acquire-reaper path=${reaper_path} reason=cleanup-failed owner=${reaper_owner}" || return 1 NVRAM_TRANSACTION_REAPER_LOCK_MODE="legacy" nvram_transaction_lock_reaper_legacy_claim "${reaper_path}" "${owner}" || { [ -n "${NVRAM_TRANSACTION_LOCK_DIAGNOSTIC:-}" ] || nvram_transaction_lock_failure "operation=acquire-reaper path=${reaper_path} reason=publication-failed owner=${owner}" || true NVRAM_TRANSACTION_REAPER_LOCK_MODE="" NVRAM_TRANSACTION_REAPER_LOCK_PATH="" return 1 } NVRAM_TRANSACTION_LOCK_DIAGNOSTIC="" export NVRAM_TRANSACTION_LOCK_DIAGNOSTIC claimed=1 fi fi if [ "${claimed}" -ne 1 ]; then nvram_transaction_lock_failure "operation=acquire-reaper path=${reaper_path} reason=ownership-not-claimed owner=${owner}" || true return 1 fi if [ "${NVRAM_TRANSACTION_REAPER_LOCK_MODE:-}" != "legacy" ] || [ "${NVRAM_TRANSACTION_REAPER_LOCK_PATH:-}" != "${reaper_path}" ] || [ "$(cat "${reaper_path}/pid" 2>/dev/null)" != "${owner}" ]; then [ "$(cat "${reaper_path}/pid" 2>/dev/null)" != "${owner}" ] || rm -rf "${reaper_path}" 2>/dev/null nvram_transaction_lock_failure "operation=acquire-reaper path=${reaper_path} reason=ownership-lost owner=${owner}" || true return 1 fi [ "$(cat "${reaper_path}/pid" 2>/dev/null)" = "${owner}" ] || { if [ "${NVRAM_TRANSACTION_REAPER_LOCK_PATH:-}" = "${reaper_path}" ]; then NVRAM_TRANSACTION_REAPER_LOCK_MODE="" NVRAM_TRANSACTION_REAPER_LOCK_PATH="" fi nvram_transaction_lock_failure "operation=acquire-reaper path=${reaper_path}/pid reason=owner-changed owner=${owner}" || true return 1 } if nvram_transaction_lock_flock_supports_fd; then [ ! -L "${reaper_path}.lock" ] || { nvram_transaction_lock_failure "operation=open-reaper-flock-file path=${reaper_path}.lock reason=symlink-not-allowed" || true nvram_transaction_lock_reaper_release "${reaper_path}" "${owner}" return 1 } : >>"${reaper_path}.lock" 2>/dev/null || { nvram_transaction_lock_reaper_legacy_release "${reaper_path}" "${owner}" return 1 } exec 9<>"${reaper_path}.lock" || { nvram_transaction_lock_reaper_legacy_release "${reaper_path}" "${owner}" return 1 } # Arm descriptor cleanup before flock can complete. BusyBox ash may run a # pending signal trap before executing the next shell assignment. NVRAM_TRANSACTION_REAPER_LOCK_MODE="flock" if nvram_transaction_lock_reaper_flock_acquire; then # A returning signal trap may have released the reaper after flock # succeeded. Revalidate every published ownership record before the # caller is allowed to enter the transaction critical section. if [ "${NVRAM_TRANSACTION_REAPER_LOCK_MODE:-}" = "flock" ] && [ "${NVRAM_TRANSACTION_REAPER_LOCK_PATH:-}" = "${reaper_path}" ] && [ "$(cat "${reaper_path}/pid" 2>/dev/null)" = "${owner}" ]; then return 0 fi return 1 fi NVRAM_TRANSACTION_REAPER_LOCK_MODE="legacy" exec 9>&- nvram_transaction_lock_reaper_legacy_release "${reaper_path}" "${owner}" return 1 fi NVRAM_TRANSACTION_LOCK_DIAGNOSTIC="" export NVRAM_TRANSACTION_LOCK_DIAGNOSTIC if nvram_transaction_lock_readlink; then # Arm signal cleanup before publishing the capability marker. A signal # may be delivered after ln completes but before the next shell command. NVRAM_TRANSACTION_REAPER_LOCK_MODE="symlink" if [ ! -e "${reaper_path}.symlink" ] && [ ! -L "${reaper_path}.symlink" ] && ln -s "${owner}" "${reaper_path}.symlink" 2>/dev/null; then # A returning signal trap may have released the reaper after ln # succeeded. Revalidate all active and published ownership state. if [ "${NVRAM_TRANSACTION_REAPER_LOCK_MODE:-}" = "symlink" ] && [ "${NVRAM_TRANSACTION_REAPER_LOCK_PATH:-}" = "${reaper_path}" ] && [ "$(nvram_transaction_lock_readlink "${reaper_path}.symlink" 2>/dev/null)" = "${owner}" ] && [ "$(cat "${reaper_path}/pid" 2>/dev/null)" = "${owner}" ]; then return 0 fi return 1 fi if [ "${NVRAM_TRANSACTION_REAPER_LOCK_MODE:-}" = "symlink" ] && [ "${NVRAM_TRANSACTION_REAPER_LOCK_PATH:-}" = "${reaper_path}" ]; then NVRAM_TRANSACTION_REAPER_LOCK_MODE="legacy" fi reaper_owner="$(nvram_transaction_lock_readlink "${reaper_path}.symlink" 2>/dev/null)" if [ -n "${reaper_owner}" ] && nvram_transaction_lock_owner_live "${reaper_owner}"; then nvram_transaction_lock_failure "operation=acquire-reaper path=${reaper_path}.symlink reason=live-owner owner=${reaper_owner}" || true fi if [ -n "${reaper_owner}" ] && ! nvram_transaction_lock_owner_live "${reaper_owner}" && [ "$(nvram_transaction_lock_readlink "${reaper_path}.symlink" 2>/dev/null)" = "${reaper_owner}" ]; then # Atomically reclaim stale symlink: create temp symlink, rename, verify ownership # Arm cleanup before either artifact can identify this process. This also # covers a signal delivered after mv publishes the replacement. NVRAM_TRANSACTION_REAPER_LOCK_MODE="symlink" NVRAM_TRANSACTION_REAPER_TEMP_PATH="${reaper_path}.symlink.${owner_suffix}" if ! nvram_transaction_lock_reaper_temp_symlink_create "${owner}" "${NVRAM_TRANSACTION_REAPER_TEMP_PATH}" 2>/dev/null; then if [ ! -e "${NVRAM_TRANSACTION_REAPER_TEMP_PATH}" ] && [ ! -L "${NVRAM_TRANSACTION_REAPER_TEMP_PATH}" ]; then NVRAM_TRANSACTION_REAPER_TEMP_PATH="" NVRAM_TRANSACTION_REAPER_LOCK_MODE="legacy" nvram_transaction_lock_reaper_legacy_release "${reaper_path}" "${owner}" return 1 fi NVRAM_TRANSACTION_REAPER_TEMP_PATH="${reaper_path}.symlink.${owner_suffix}.1" if ! nvram_transaction_lock_reaper_temp_symlink_create "${owner}" "${NVRAM_TRANSACTION_REAPER_TEMP_PATH}" 2>/dev/null; then NVRAM_TRANSACTION_REAPER_TEMP_PATH="" NVRAM_TRANSACTION_REAPER_LOCK_MODE="legacy" nvram_transaction_lock_reaper_legacy_release "${reaper_path}" "${owner}" return 1 fi fi # BusyBox 1.25.1 rejects mv -T. The legacy directory is held for this # entire reclaim, so after revalidating the stale owner it is safe to # unlink the marker and rename the temporary symlink into the absent path. if { [ "$(nvram_transaction_lock_readlink "${reaper_path}.symlink" 2>/dev/null)" = "${reaper_owner}" ] && rm -f "${reaper_path}.symlink" && mv "${NVRAM_TRANSACTION_REAPER_TEMP_PATH}" "${reaper_path}.symlink" 2>/dev/null; }; then # Verify we won the race by checking the symlink target if [ "$(nvram_transaction_lock_readlink "${reaper_path}.symlink" 2>/dev/null)" = "${owner}" ]; then NVRAM_TRANSACTION_REAPER_TEMP_PATH="" NVRAM_TRANSACTION_REAPER_LOCK_MODE="symlink" return 0 fi # Another contender won; clean up and fail contending_owner="$(nvram_transaction_lock_readlink "${reaper_path}.symlink" 2>/dev/null)" nvram_transaction_lock_failure "operation=acquire-reaper path=${reaper_path}.symlink reason=ownership-lost owner=${contending_owner:-unknown}" || true rm -f "${NVRAM_TRANSACTION_REAPER_TEMP_PATH}" 2>/dev/null NVRAM_TRANSACTION_REAPER_TEMP_PATH="" NVRAM_TRANSACTION_REAPER_LOCK_MODE="legacy" nvram_transaction_lock_reaper_release "${reaper_path}" "${owner}" || true return 1 fi rm -f "${NVRAM_TRANSACTION_REAPER_TEMP_PATH}" 2>/dev/null NVRAM_TRANSACTION_REAPER_TEMP_PATH="" NVRAM_TRANSACTION_REAPER_LOCK_MODE="legacy" fi # A present symlink is an active or unsuccessfully reclaimed lock. If no # artifact was published, symlink creation is unavailable on this # filesystem, so continue with the portable mkdir/PID implementation. [ ! -e "${reaper_path}.symlink" ] && [ ! -L "${reaper_path}.symlink" ] || { nvram_transaction_lock_reaper_legacy_release "${reaper_path}" "${owner}" return 1 } fi # A returning signal trap may have released the legacy mutex after a failed # symlink publication. Revalidate it before selecting the mkdir fallback. if [ "${NVRAM_TRANSACTION_REAPER_LOCK_MODE:-}" = "legacy" ] && [ "${NVRAM_TRANSACTION_REAPER_LOCK_PATH:-}" = "${reaper_path}" ] && [ "$(cat "${reaper_path}/pid" 2>/dev/null)" = "${owner}" ]; then NVRAM_TRANSACTION_REAPER_LOCK_MODE="mkdir" return 0 fi return 1 } # nvram_transaction_lock_reaper_acquire adds a generic diagnostic when needed. nvram_transaction_lock_reaper_acquire() { local diagnostic_path diagnostic_path="$1" nvram_transaction_lock_reaper_acquire_impl "$@" && return 0 [ -n "${NVRAM_TRANSACTION_LOCK_DIAGNOSTIC:-}" ] || nvram_transaction_lock_failure "operation=acquire-reaper path=${diagnostic_path}" || true return 1 } # nvram_transaction_lock_reaper_release_impl releases the active reaper lock. nvram_transaction_lock_reaper_release_impl() { local owner reaper_path reaper_path="$1" case "${NVRAM_TRANSACTION_REAPER_LOCK_MODE:-}" in legacy) if [ -n "${2:-}" ]; then owner="$2" else owner="$(nvram_transaction_lock_owner_current)" || return 1 fi nvram_transaction_lock_reaper_legacy_release "${reaper_path}" "${owner}" || return 1 return 0 ;; flock) if [ -n "${2:-}" ]; then owner="$2" else owner="$(nvram_transaction_lock_owner_current)" || return 1 fi # Keep the descriptor lock held until the shared legacy mutex is # removed so a transient removal failure remains retryable. nvram_transaction_lock_reaper_legacy_release "${reaper_path}" "${owner}" || return 1 # A signal trap may have completed the same release while rm was # running. In that case fd 9 and all active state are already clear. [ -n "${NVRAM_TRANSACTION_REAPER_LOCK_MODE:-}" ] || return 0 /usr/bin/flock -u 9 >/dev/null 2>&1 || return 1 # The shared mutex is already gone and the descriptor is unlocked. # Make a signal-triggered nested release idempotent before closing fd 9. NVRAM_TRANSACTION_REAPER_LOCK_MODE="legacy" exec 9>&- NVRAM_TRANSACTION_REAPER_LOCK_MODE="" NVRAM_TRANSACTION_REAPER_LOCK_PATH="" NVRAM_TRANSACTION_REAPER_TEMP_PATH="" return 0 ;; symlink) if [ -n "${2:-}" ]; then owner="$2" else owner="$(nvram_transaction_lock_owner_current)" || return 1 fi if [ -n "${NVRAM_TRANSACTION_REAPER_TEMP_PATH:-}" ] && [ "$(nvram_transaction_lock_readlink "${NVRAM_TRANSACTION_REAPER_TEMP_PATH}" 2>/dev/null)" = "${owner}" ]; then rm -f "${NVRAM_TRANSACTION_REAPER_TEMP_PATH}" || return 1 fi if [ -e "${reaper_path}.symlink" ] || [ -L "${reaper_path}.symlink" ]; then [ "$(nvram_transaction_lock_readlink "${reaper_path}.symlink" 2>/dev/null)" = "${owner}" ] || { [ -n "${NVRAM_TRANSACTION_REAPER_TEMP_PATH:-}" ] || { nvram_transaction_lock_failure "operation=release-reaper path=${reaper_path}.symlink reason=owner-mismatch owner=${owner}" || true return 1 } nvram_transaction_lock_reaper_legacy_release "${reaper_path}" "${owner}" || return 1 NVRAM_TRANSACTION_REAPER_LOCK_MODE="" NVRAM_TRANSACTION_REAPER_LOCK_PATH="" NVRAM_TRANSACTION_REAPER_TEMP_PATH="" return 0 } rm -f "${reaper_path}.symlink" || return 1 fi # Arm the idempotent post-publication phase before removing the shared # mutex so a nested signal cleanup can finish this release safely. NVRAM_TRANSACTION_REAPER_LOCK_MODE="legacy" ;; mkdir) if [ -n "${2:-}" ]; then owner="$2" else owner="$(nvram_transaction_lock_owner_current)" || return 1 fi [ "$(cat "${reaper_path}/pid" 2>/dev/null)" = "${owner}" ] || { nvram_transaction_lock_failure "operation=release-reaper path=${reaper_path}/pid reason=owner-mismatch owner=${owner}" || true return 1 } NVRAM_TRANSACTION_REAPER_LOCK_MODE="legacy" ;; *) return 1 ;; esac if [ -z "${owner:-}" ]; then owner="$(nvram_transaction_lock_owner_current)" || return 1 fi nvram_transaction_lock_reaper_legacy_release "${reaper_path}" "${owner}" || return 1 NVRAM_TRANSACTION_REAPER_LOCK_MODE="" NVRAM_TRANSACTION_REAPER_LOCK_PATH="" NVRAM_TRANSACTION_REAPER_TEMP_PATH="" } # nvram_transaction_lock_reaper_release preserves primary failure context. nvram_transaction_lock_reaper_release() { local diagnostic_path primary_diagnostic release_diagnostic diagnostic_path="$1" primary_diagnostic="${NVRAM_TRANSACTION_LOCK_DIAGNOSTIC:-}" nvram_transaction_lock_reaper_release_impl "$@" && return 0 if [ -n "${primary_diagnostic}" ]; then release_diagnostic="${NVRAM_TRANSACTION_LOCK_DIAGNOSTIC:-}" if [ -n "${release_diagnostic}" ] && [ "${release_diagnostic}" != "${primary_diagnostic}" ]; then nvram_transaction_lock_failure "${primary_diagnostic} after-failed-operation=release-reaper:${diagnostic_path} release-diagnostic=${release_diagnostic}" || true else nvram_transaction_lock_failure "${primary_diagnostic} after-failed-operation=release-reaper:${diagnostic_path}" || true fi return 1 fi [ -n "${NVRAM_TRANSACTION_LOCK_DIAGNOSTIC:-}" ] && return 1 nvram_transaction_lock_failure "operation=release-reaper path=${diagnostic_path}" || true return 1 } nvram_transaction_lock_reaper_release_active() { [ -z "${NVRAM_TRANSACTION_REAPER_LOCK_MODE:-}" ] && return 0 [ -n "${NVRAM_TRANSACTION_REAPER_LOCK_PATH:-}" ] || return 1 if [ -n "${NVRAM_TRANSACTION_LOCK_RELEASE_PHASE:-}" ] && [ "${NVRAM_TRANSACTION_REAPER_LOCK_PATH}" = "${BASE_DIR}/.AdGuardHome.nvram.lock.reaper" ]; then return 0 fi nvram_transaction_lock_reaper_release "${NVRAM_TRANSACTION_REAPER_LOCK_PATH}" } # nvram_transaction_lock_flock_supports_fd probes descriptor-based flock support. nvram_transaction_lock_flock_supports_fd() { local probe status [ -x /usr/bin/flock ] || return 1 probe="${BASE_DIR}/.AdGuardHome.nvram.flock-probe.$$" ( exec 9>"${probe}" || exit 1 /usr/bin/flock -n 9 >/dev/null 2>&1 ) status="$?" rm -f "${probe}" || { nvram_transaction_lock_failure "operation=flock-probe-cleanup path=${probe}" || true return 1 } return "${status}" } # nvram_transaction_lock_flock_open_fd opens descriptor 8 for flock operations. nvram_transaction_lock_flock_open_fd() { [ "$#" -eq 2 ] || return 1 exec 8<>"$2" } nvram_transaction_lock_readlink() { which readlink >/dev/null 2>&1 || return 127 [ "$#" -gt 0 ] || return 0 readlink "$@" } # nvram_transaction_lock_symlink_acquire acquires or classifies the symlink lock. nvram_transaction_lock_symlink_acquire() { local lock_dir lock_owner lock_path owner lock_path="${BASE_DIR}/.AdGuardHome.nvram.lock.symlink" lock_dir="${BASE_DIR}/.AdGuardHome.nvram.lock.d" owner="$(nvram_transaction_lock_owner_current)" || nvram_transaction_lock_failure "operation=identify-current-process path=/proc/$$/stat" || return 1 if ! nvram_transaction_lock_readlink; then if [ -L "${lock_path}" ]; then nvram_transaction_lock_failure "operation=inspect-symlink-lock path=${lock_path} reason=readlink-unavailable" || true printf '%s\n' "Error: cannot inspect NVRAM transaction lock ${lock_path}; readlink is unavailable." >&2 return 1 fi return 2 fi nvram_transaction_lock_reaper_acquire "${BASE_DIR}/.AdGuardHome.nvram.lock.reaper" || { [ -n "${NVRAM_TRANSACTION_LOCK_DIAGNOSTIC:-}" ] || nvram_transaction_lock_failure "operation=acquire-reaper path=${BASE_DIR}/.AdGuardHome.nvram.lock.reaper" || true return 1 } if [ -e "${lock_dir}" ] || [ -L "${lock_dir}" ]; then [ -d "${lock_dir}" ] && [ ! -L "${lock_dir}" ] || { nvram_transaction_lock_failure "operation=validate-mkdir-lock path=${lock_dir} reason=unsafe-artifact" || true nvram_transaction_lock_reaper_release "${BASE_DIR}/.AdGuardHome.nvram.lock.reaper" || return 1 return 1 } lock_owner="$(cat "${lock_dir}/pid" 2>/dev/null)" if nvram_transaction_lock_owner_live "${lock_owner}"; then nvram_transaction_lock_failure "operation=validate-stale-lock path=${lock_dir}/pid reason=live-owner owner=${lock_owner}" || true nvram_transaction_lock_reaper_release "${BASE_DIR}/.AdGuardHome.nvram.lock.reaper" || return 1 return 1 fi [ "$(cat "${lock_dir}/pid" 2>/dev/null)" = "${lock_owner}" ] || { nvram_transaction_lock_failure "operation=validate-stale-lock path=${lock_dir}/pid reason=owner-changed owner=${lock_owner}" || true nvram_transaction_lock_reaper_release "${BASE_DIR}/.AdGuardHome.nvram.lock.reaper" || return 1 return 1 } rm -rf "${lock_dir}" || { nvram_transaction_lock_failure "operation=remove-stale-lock path=${lock_dir} reason=cleanup-failed" || true nvram_transaction_lock_reaper_release "${BASE_DIR}/.AdGuardHome.nvram.lock.reaper" || return 1 return 1 } fi if [ -L "${lock_path}" ]; then lock_owner="$(nvram_transaction_lock_readlink "${lock_path}" 2>/dev/null)" if nvram_transaction_lock_owner_live "${lock_owner}"; then nvram_transaction_lock_failure "operation=validate-stale-lock path=${lock_path} reason=live-owner owner=${lock_owner}" || true nvram_transaction_lock_reaper_release "${BASE_DIR}/.AdGuardHome.nvram.lock.reaper" || return 1 return 1 fi [ "$(nvram_transaction_lock_readlink "${lock_path}" 2>/dev/null)" = "${lock_owner}" ] || { nvram_transaction_lock_failure "operation=validate-stale-lock path=${lock_path} reason=owner-changed owner=${lock_owner}" || true nvram_transaction_lock_reaper_release "${BASE_DIR}/.AdGuardHome.nvram.lock.reaper" || return 1 return 1 } rm -f "${lock_path}" || { nvram_transaction_lock_failure "operation=remove-stale-lock path=${lock_path} reason=cleanup-failed" || true nvram_transaction_lock_reaper_release "${BASE_DIR}/.AdGuardHome.nvram.lock.reaper" || return 1 return 1 } elif [ -e "${lock_path}" ]; then nvram_transaction_lock_failure "operation=validate-symlink-lock path=${lock_path} reason=unsafe-artifact" || true nvram_transaction_lock_reaper_release "${BASE_DIR}/.AdGuardHome.nvram.lock.reaper" || return 1 return 1 fi if ln -s "${owner}" "${lock_path}" 2>/dev/null; then if [ "$(nvram_transaction_lock_readlink "${lock_path}" 2>/dev/null)" != "${owner}" ]; then nvram_transaction_lock_failure "operation=validate-symlink-publication path=${lock_path} rolled-back=${lock_path}" || true nvram_transaction_lock_reaper_release "${BASE_DIR}/.AdGuardHome.nvram.lock.reaper" || return 1 return 1 fi nvram_transaction_lock_reaper_release "${BASE_DIR}/.AdGuardHome.nvram.lock.reaper" || { [ -n "${NVRAM_TRANSACTION_LOCK_DIAGNOSTIC:-}" ] || nvram_transaction_lock_failure "operation=release-reaper path=${BASE_DIR}/.AdGuardHome.nvram.lock.reaper rolled-back=${lock_path}" || true if ! rm -f "${lock_path}" 2>/dev/null || [ -e "${lock_path}" ] || [ -L "${lock_path}" ]; then printf '%s\n' "Error: unable to roll back owned NVRAM transaction lock ${lock_path}; stopping recovery." >&2 exit 1 fi return 1 } return 0 fi nvram_transaction_lock_failure "operation=publish-symlink path=${lock_path}" || true nvram_transaction_lock_reaper_release "${BASE_DIR}/.AdGuardHome.nvram.lock.reaper" || return 1 [ ! -e "${lock_path}" ] && [ ! -L "${lock_path}" ] || return 1 NVRAM_TRANSACTION_LOCK_DIAGNOSTIC="" export NVRAM_TRANSACTION_LOCK_DIAGNOSTIC return 2 } nvram_transaction_lock_release() { local lock_path owner release_phase lock_path="${BASE_DIR}/.AdGuardHome.nvram.lock" case "${NVRAM_TRANSACTION_LOCK_MODE:-}" in flock) release_phase="${NVRAM_TRANSACTION_LOCK_RELEASE_PHASE:-}" if [ -z "${release_phase}" ]; then nvram_transaction_lock_flock_owned "${lock_path}" || return 1 owner="$(nvram_transaction_lock_owner_current)" || return 1 [ "$(nvram_transaction_lock_readlink "${lock_path}.symlink" 2>/dev/null)" = "${owner}" ] || return 1 nvram_transaction_lock_reaper_acquire "${lock_path}.reaper" "${owner}" || return 1 NVRAM_TRANSACTION_LOCK_RELEASE_OWNER="${owner}" NVRAM_TRANSACTION_LOCK_RELEASE_PHASE="reaper" release_phase="reaper" else owner="${NVRAM_TRANSACTION_LOCK_RELEASE_OWNER:-}" [ -n "${owner}" ] || return 1 [ "$(nvram_transaction_lock_owner_current)" = "${owner}" ] || { nvram_transaction_lock_failure "operation=resume-flock-release path=${lock_path} reason=owner-mismatch owner=${owner}" || true return 1 } fi if [ "${release_phase}" = "reaper" ]; then if ! rm -f "${lock_path}.symlink"; then nvram_transaction_lock_failure "operation=release-flock-lock path=${lock_path}.symlink reason=remove-owner-failed owner=${owner}" || true return 1 fi NVRAM_TRANSACTION_LOCK_RELEASE_PHASE="symlink-removed" release_phase="symlink-removed" fi if [ "${release_phase}" = "symlink-removed" ]; then nvram_transaction_lock_flock_unlock || { nvram_transaction_lock_failure "operation=release-flock-lock path=${lock_path} reason=unlock-failed owner=${owner}" || true return 1 } exec 8>&- || return 1 NVRAM_TRANSACTION_LOCK_RELEASE_PHASE="unlocked" fi nvram_transaction_lock_reaper_release "${lock_path}.reaper" "${owner}" || return 1 NVRAM_TRANSACTION_LOCK_RELEASE_OWNER="" NVRAM_TRANSACTION_LOCK_RELEASE_PHASE="" ;; symlink) owner="$(nvram_transaction_lock_owner_current)" || return 1 [ "$(nvram_transaction_lock_readlink "${lock_path}.symlink" 2>/dev/null)" = "${owner}" ] || return 1 rm -f "${lock_path}.symlink" || return 1 ;; mkdir) owner="$(nvram_transaction_lock_owner_current)" || return 1 [ "$(cat "${lock_path}.d/pid" 2>/dev/null)" = "${owner}" ] || return 1 rm -rf "${lock_path}.d" || return 1 ;; "") return 0 ;; *) return 1 ;; esac NVRAM_TRANSACTION_LOCK_MODE="" } nvram_transaction_set() { local key old value_file key="$1" value_file="${NVRAM_TRANSACTION_DIR:-}/new.${key}" [ -f "${NVRAM_TRANSACTION_DIR:-}/${key}" ] || return 1 old="$(cat "${NVRAM_TRANSACTION_DIR}/${key}")" || return 1 [ "${old}" = "$2" ] && return 0 printf '%s' "$2" >"${value_file}" || return 1 chmod 600 "${value_file}" || return 1 NVRAM_TRANSACTION_CHANGED="1" export NVRAM_TRANSACTION_CHANGED } nvram_transaction_restore() { local current expected expected_file failed inventory_file inventory_status keep_snapshot key old service_action snapshot_dir snapshot_dir="${NVRAM_TRANSACTION_DIR:-}" service_action="$1" keep_snapshot="${2:-0}" failed="0" [ -f "${snapshot_dir}/keys" ] || return 1 if [ ! -f "${snapshot_dir}/dirty" ]; then if [ "${keep_snapshot}" != "1" ]; then rm -rf "${snapshot_dir}" [ ! -e "${snapshot_dir}" ] && [ ! -L "${snapshot_dir}" ] || return 1 fi return 0 fi inventory_file="${snapshot_dir}/restore-inventory.$$" if ! installer_nvram show >"${inventory_file}" 2>/dev/null; then if [ ! -s "${inventory_file}" ]; then rm -f "${inventory_file}" 2>/dev/null || true rollback_result_write "NVRAM transaction" "${INSTALLER_ROLLBACK_PARTIAL:-rollback partial}" "snapshot preserved at ${snapshot_dir}" return 1 fi fi chmod 600 "${inventory_file}" || { rm -f "${inventory_file}" 2>/dev/null || true return 1 } while IFS= read -r key; do expected_file="" if [ -f "${snapshot_dir}/new.${key}" ]; then expected_file="${snapshot_dir}/new.${key}" elif [ -f "${snapshot_dir}/applied.${key}" ]; then expected_file="${snapshot_dir}/applied.${key}" fi [ -n "${expected_file}" ] || continue if [ -n "${expected_file}" ]; then expected="$(cat "${expected_file}")" || { failed="1" continue } /bin/grep -q "^${key}=" "${inventory_file}" inventory_status="$?" case "${inventory_status}" in 0) ;; 1) continue ;; *) failed="1" continue ;; esac current="$(installer_nvram get "${key}" 2>/dev/null)" || { failed="1" continue } [ "${current}" = "${expected}" ] || continue fi old="$(cat "${snapshot_dir}/${key}")" || { failed="1" continue } if [ -f "${snapshot_dir}/exists.${key}" ]; then installer_nvram set "${key}=${old}" || failed="1" else installer_nvram unset "${key}" || failed="1" fi done <"${snapshot_dir}/keys" rm -f "${inventory_file}" || failed="1" installer_nvram commit || failed="1" nvram_transaction_restart_services "${service_action}" || failed="1" if [ "${failed}" = "0" ]; then if [ "${keep_snapshot}" != "1" ]; then # Clearing dirty commits the restore before best-effort snapshot cleanup. rm -f "${snapshot_dir}/dirty" || return 1 rm -rf "${snapshot_dir}" 2>/dev/null || true fi NVRAM_TRANSACTION_CHANGED="0" export NVRAM_TRANSACTION_CHANGED return 0 fi rollback_result_write "NVRAM transaction" "${INSTALLER_ROLLBACK_PARTIAL:-rollback partial}" "snapshot preserved at ${snapshot_dir}" return 1 } # nvram_transaction_restart_services runs each validated transaction restart action separately. nvram_transaction_restart_services() { local failed service_action service_action="$1" failed="0" case "${service_action}" in -) return 0 ;; restart_dnsmasq | restart_firewall) service "${service_action}" >/dev/null 2>&1 || failed="1" ;; 'restart_firewall;restart_dnsmasq') service restart_firewall >/dev/null 2>&1 || failed="1" service restart_dnsmasq >/dev/null 2>&1 || failed="1" ;; *) return 1 ;; esac [ "${failed}" = "0" ] } nvram_transaction_apply() { local keep_snapshot key new service_action snapshot_dir service_action="$1" keep_snapshot="${2:-0}" snapshot_dir="${NVRAM_TRANSACTION_DIR:-}" [ -f "${snapshot_dir}/keys" ] || return 1 if [ "${NVRAM_TRANSACTION_CHANGED:-0}" = "0" ]; then if [ "${keep_snapshot}" != "1" ]; then rm -rf "${snapshot_dir}" [ ! -e "${snapshot_dir}" ] && [ ! -L "${snapshot_dir}" ] || return 1 fi NVRAM_TRANSACTION_CHANGED="0" export NVRAM_TRANSACTION_CHANGED return 0 fi if ! : >"${snapshot_dir}/dirty"; then rm -rf "${snapshot_dir}" return 1 fi if ! chmod 600 "${snapshot_dir}/dirty"; then rm -rf "${snapshot_dir}" return 1 fi while IFS= read -r key; do [ -f "${snapshot_dir}/new.${key}" ] || continue new="$(cat "${snapshot_dir}/new.${key}")" || { nvram_transaction_restore "${service_action}" || true return 1 } if ! installer_nvram set "${key}=${new}"; then nvram_transaction_restore "${service_action}" || true return 1 fi done <"${snapshot_dir}/keys" if ! installer_nvram commit; then nvram_transaction_restore "${service_action}" || true return 1 fi if ! nvram_transaction_restart_services "${service_action}"; then nvram_transaction_restore "${service_action}" || true return 1 fi if [ "${keep_snapshot}" = "1" ]; then while IFS= read -r key; do [ -f "${snapshot_dir}/new.${key}" ] || continue if ! mv -f "${snapshot_dir}/new.${key}" "${snapshot_dir}/applied.${key}"; then nvram_transaction_restore "${service_action}" 1 || true NVRAM_TRANSACTION_CHANGED="1" export NVRAM_TRANSACTION_CHANGED return 1 fi done <"${snapshot_dir}/keys" else # Clearing dirty commits the apply before best-effort snapshot cleanup. # An interrupted recursive removal must not make a partial snapshot # eligible for startup rollback. rm -f "${snapshot_dir}/dirty" || return 1 rm -rf "${snapshot_dir}" 2>/dev/null || true fi NVRAM_TRANSACTION_CHANGED="0" export NVRAM_TRANSACTION_CHANGED return 0 } installer_lan_domain_set() { local keep_snapshot keep_snapshot="${2:-0}" if ! nvram_transaction_begin lan-domain lan_domain; then return 1 fi if ! nvram_transaction_set lan_domain "$1"; then rm -rf "${NVRAM_TRANSACTION_DIR}" return 1 fi nvram_transaction_apply restart_dnsmasq "${keep_snapshot}" } installer_lan_domain_restore() { ! nvram_transaction_setup_committed || return 0 installer_lan_domain_restore_uninstall } installer_lan_domain_restore_uninstall() { NVRAM_TRANSACTION_DIR="${BASE_DIR}/.AdGuardHome.nvram/lan-domain" export NVRAM_TRANSACTION_DIR [ -d "${NVRAM_TRANSACTION_DIR}" ] || return 0 nvram_transaction_restore restart_dnsmasq } rollback_result_write() { local context result detail marker_dir tmp_file context="$1" result="$2" detail="${3:-}" ROLLBACK_RESULT_UPDATED="1" marker_dir="${ROLLBACK_RESULT_FILE%/*}" [ -d "${marker_dir}" ] || mkdir -p "${marker_dir}" 2>/dev/null || return 0 tmp_file="${ROLLBACK_RESULT_FILE}.$$" { printf 'time=%s\n' "$(date '+%Y-%m-%d %H:%M:%S' 2>/dev/null)" printf 'context=%s\n' "${context}" printf 'result=%s\n' "${result}" [ -z "${detail}" ] || printf 'detail=%s\n' "${detail}" } >"${tmp_file}" 2>/dev/null && mv -f "${tmp_file}" "${ROLLBACK_RESULT_FILE}" 2>/dev/null rm -f "${tmp_file}" 2>/dev/null chmod 600 "${ROLLBACK_RESULT_FILE}" 2>/dev/null } rollback_result_summary() { [ -s "${ROLLBACK_RESULT_FILE}" ] || return 1 awk ' index($0, "=") { key = substr($0, 1, index($0, "=") - 1) value = substr($0, index($0, "=") + 1) } key == "time" { time = value } key == "context" { context = value } key == "result" { result = value } key == "detail" { detail = value } END { if (result == "") exit 1 printf "%s%s%s%s%s", result, context ? " during " context : "", detail ? ": " detail : "", time ? " (" time ")" : "", "" } ' "${ROLLBACK_RESULT_FILE}" } rollback_result_notice() { local summary summary="$(rollback_result_summary 2>/dev/null)" || return 0 PTXT "${WARNING} Last rollback result: ${summary}" } rollback_result_needs_attention() { local result [ -f "${ROLLBACK_RESULT_FILE}" ] || return 1 result="$(awk -F= '$1 == "result" { print substr($0, 8); exit }' "${ROLLBACK_RESULT_FILE}" 2>/dev/null)" case "${result}" in rollback\ failed | rollback\ partial | rollback\ unavailable | restore-failed | replace-failed | yaml-quarantine-failed | staged-yaml-quarantine-failed | failed:* | *-failed) return 0 ;; esac return 1 } entware_available() { [ -d /opt ] || return 1 [ -x /opt/bin/opkg ] || return 1 [ -f /opt/etc/opkg.conf ] || [ -f /opt/etc/init.d/rc.unslung ] || return 1 /opt/bin/opkg --version >/dev/null 2>&1 } installer_script_path() { local BASE DIR PATH_VALUE RESOLVED PATH_VALUE="$1" if ai_have_cmd readlink; then RESOLVED="$(readlink -f "${PATH_VALUE}" 2>/dev/null)" || RESOLVED="" if [ -n "${RESOLVED}" ]; then PTXT "${RESOLVED}" return 0 fi fi case "${PATH_VALUE}" in /*) ;; *) PATH_VALUE="${PWD}/${PATH_VALUE}" ;; esac BASE="${PATH_VALUE##*/}" DIR="${PATH_VALUE%/*}" [ -n "${BASE}" ] && [ -d "${DIR}" ] || return 1 DIR="$(cd "${DIR}" 2>/dev/null && pwd -P)" || return 1 PTXT "${DIR}/${BASE}" } cleanup_api_files() { [ "${ADGUARD_METADATA_DIR_OWNED:-0}" = "1" ] || return 0 metadata_workspace_is_private || return 1 rm -f "${ADGUARD_METADATA_FILE}" "${ADGUARD_METADATA_FILE}.release" \ "${ADGUARD_METADATA_FILE}.beta" "${ADGUARD_METADATA_FILE}.edge" || return 1 rmdir "${ADGUARD_METADATA_DIR}" || return 1 ADGUARD_METADATA_DIR_OWNED=0 } metadata_workspace_create() { [ ! -e "${ADGUARD_METADATA_DIR}" ] && [ ! -L "${ADGUARD_METADATA_DIR}" ] || return 1 (umask 077 && mkdir "${ADGUARD_METADATA_DIR}") 2>/dev/null || return 1 ADGUARD_METADATA_DIR_OWNED=1 metadata_workspace_is_private || { ADGUARD_METADATA_DIR_OWNED=0 return 1 } } metadata_workspace_is_private() { [ "${ADGUARD_METADATA_DIR_OWNED:-0}" = "1" ] || return 1 [ -d "${ADGUARD_METADATA_DIR}" ] && [ ! -L "${ADGUARD_METADATA_DIR}" ] || return 1 ls -ldn "${ADGUARD_METADATA_DIR}" 2>/dev/null | /usr/bin/awk 'NR == 1 { exit(substr($1, 1, 10) == "drwx------" && $3 == 0 ? 0 : 1) } END { if (NR == 0) exit 1 }' } clear_screen() { ai_have_cmd clear && clear return 0 } go_tool_available() { [ -x /opt/bin/go/bin/go ] } ensure_bcrypt_tool() { if bcrypt_tool_available; then return 0 fi if ! go_tool_available; then case "$(/bin/uname -m)" in "aarch64" | "arm64") ensure_opkg_package go --force-depends --force-overwrite --force-reinstall || return 1 ;; "armv7l" | *) if opkg_pkg_available go_nohf; then ensure_opkg_package go_nohf --force-depends --force-overwrite --force-reinstall || return 1 else ensure_opkg_package go --force-depends --force-overwrite --force-reinstall || return 1 fi ;; esac fi export GOROOT="/opt/bin/go" export GOBIN="/opt/bin/" GO_CACHE_DIR="/tmp/AdGuardHome-go-build-cache" GO_PATH_DIR="/tmp/AdGuardHome-go-path" go_tool_available || return 1 mkdir -p "${GO_CACHE_DIR}" "${GO_PATH_DIR}" || return 1 env -i LC_ALL="${LC_ALL:-C}" PATH="/sbin:/bin:/usr/sbin:/usr/bin:/opt/sbin:/opt/bin:/opt/usr/sbin:/opt/usr/bin:/opt/bin/go/bin" HOME="${GO_PATH_DIR}" GOCACHE="${GO_CACHE_DIR}" GOPATH="${GO_PATH_DIR}" GOROOT="${GOROOT}" GOBIN="${GOBIN}" /opt/bin/go/bin/go install gophers.dev/cmds/bcrypt-tool@latest >/dev/null 2>&1 || return 1 bcrypt_tool_available } bcrypt_tool_available() { local hash [ -x /opt/bin/bcrypt-tool ] || return 1 hash="$(/opt/bin/bcrypt-tool hash preflight 10 2>/dev/null)" || return 1 case "${hash}" in \$2a\$10\$*) return 0 ;; esac return 1 } ensure_opkg_package() { _pkg="$1" _force_reinstall=0 shift case "${_pkg}" in "" | *[!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.+-]*) return 1 ;; esac for _option in "$@"; do case "${_option}" in --force-depends | --force-overwrite) ;; --force-reinstall) _force_reinstall=1 ;; *) return 1 ;; esac done if opkg_pkg_installed "${_pkg}" && [ "${_force_reinstall:-0}" -eq 0 ]; then return 0 fi if ! opkg_clean_env install "${_pkg}" "$@" >/dev/null 2>&1; then return 1 fi opkg_pkg_installed "${_pkg}" } sha256sum_available() { local sha256sum_bin sha256sum_bin="$(which sha256sum 2>/dev/null)" || sha256sum_bin="" if [ -n "${sha256sum_bin}" ] && [ -x "${sha256sum_bin}" ] && "${sha256sum_bin}" /dev/null >/dev/null 2>&1; then return 0 fi if [ -x /opt/bin/sha256sum ] && /opt/bin/sha256sum /dev/null >/dev/null 2>&1; then return 0 fi if [ -x /bin/busybox ] && /bin/busybox sha256sum /dev/null >/dev/null 2>&1; then return 0 fi if ai_have_cmd busybox && busybox sha256sum /dev/null >/dev/null 2>&1; then return 0 fi if ai_have_cmd openssl && openssl dgst -sha256 /dev/null >/dev/null 2>&1; then return 0 fi return 1 } jq_executable_usable() { [ -n "${1:-}" ] && [ -x "$1" ] || return 1 printf '%s\n' '{}' | "$1" -e . >/dev/null 2>&1 } jq_available() { local resolved_jq resolved_jq="$(which jq 2>/dev/null)" || resolved_jq="" if jq_executable_usable "${resolved_jq}"; then return 0 fi if [ "${resolved_jq}" != "/opt/bin/jq" ] && jq_executable_usable /opt/bin/jq; then return 0 fi return 1 } ensure_jq_tool() { if jq_available; then return 0 fi ptxt_warn "No usable jq executable was found; JSON parsing support is required." PTXT "${INFO} Installing Entware package: opkg install jq-full --force-depends --force-overwrite --force-reinstall" ptxt_step "Installing Entware jq-full package." if opkg_pkg_installed jq-full; then opkg_clean_env install jq-full --force-depends --force-overwrite --force-reinstall >/dev/null 2>&1 || { ptxt_fail "Unable to repair Entware jq-full." return 1 } elif ! ensure_opkg_package jq-full --force-depends --force-overwrite --force-reinstall; then ptxt_fail "Unable to install Entware jq-full." return 1 fi if jq_available; then ptxt_ok "jq JSON parsing support is available." return 0 fi ptxt_fail "jq-full was installed, but jq is still unusable." return 1 } ensure_sha256sum_tool() { if sha256sum_available; then return 0 fi ptxt_warn "Neither sha256sum nor OpenSSL SHA-256 support was found in PATH; checksum verification is required." PTXT "${INFO} Some Asuswrt-Merlin firmware builds do not include sha256sum." PTXT "${INFO} Installing Entware package: opkg install coreutils-sha256sum --force-depends --force-overwrite --force-reinstall" ptxt_step "Installing Entware coreutils-sha256sum package." if ! ensure_opkg_package coreutils-sha256sum --force-depends --force-overwrite --force-reinstall; then ptxt_fail "Unable to install Entware coreutils-sha256sum." return 1 fi if sha256sum_available; then ptxt_ok "SHA-256 checksum support is available." return 0 fi ptxt_fail "coreutils-sha256sum was installed, but sha256sum is still unavailable." return 1 } ensure_password_hash_tool() { if python_bcrypt_available; then return 0 fi ensure_opkg_package python3 --force-depends --force-overwrite --force-reinstall || true ensure_opkg_package python3-bcrypt --force-depends --force-overwrite --force-reinstall || true if python_bcrypt_available; then return 0 fi ensure_bcrypt_tool } hash_password_python() { _password="$1" PTXT -n "${_password}" | /opt/bin/python3 -c 'import sys, bcrypt; password = sys.stdin.buffer.read(); print(bcrypt.hashpw(password, bcrypt.gensalt(prefix=b"2a", rounds=10)).decode("ascii"))' } opkg_pkg_available() { _pkg="$1" opkg_clean_env list "${_pkg}" 2>/dev/null | awk -v pkg="${_pkg}" '$1 == pkg { found = 1; exit } END { exit(found ? 0 : 1) }' } opkg_pkg_installed() { _pkg="$1" opkg_clean_env list-installed 2>/dev/null | awk -v pkg="${_pkg}" '$1 == pkg { found = 1; exit } END { exit(found ? 0 : 1) }' } opkg_clean_env() { [ -x /opt/bin/opkg ] || return 127 env -i LC_ALL="${LC_ALL:-C}" PATH="/sbin:/bin:/usr/sbin:/usr/bin:/opt/sbin:/opt/bin:/opt/usr/sbin:/opt/usr/bin" /opt/bin/opkg "$@" } # installer_cleanup_tmp_file removes the temporary path in $2 only when it # matches the setup/blocklist filename, a current-process analyzer staging # filename, or a registered blocklist-analysis path for the cleanup kind in $1. # Fixed current-process analyzer paths and registered analyzer paths are allowed. # Unmatched paths are left untouched and return 0. When removal is attempted, # returns the rm exit status, including failures, with error output suppressed. installer_cleanup_tmp_file() { case "$1:$2" in setup:"${YAML_ORI}.new.$$" | blocklist:"${YAML_FILE}.blocklists.$$.tmp" | \ blocklist-analysis:"${TARG_DIR:-}/blocklist_analyzer.$$.candidates" | \ blocklist-analysis:"${TARG_DIR:-}/blocklist_analyzer.$$.configured.ids" | \ blocklist-analysis:"${TARG_DIR:-}/blocklist_analyzer.$$.orphaned.ids" | \ blocklist-analysis:"${TARG_DIR:-}/blocklist_analyzer.$$.selected" | \ blocklist-analysis:"${TARG_DIR:-}/blocklist_analyzer.$$.out" | \ blocklist-analysis:"${TARG_DIR:-}/blocklist_analyzer.$$.ids") rm -f "$2" 2>/dev/null ;; blocklist-analysis:*) [ -n "$2" ] || return 0 if [ "$2" = "${BLOCKLIST_ANALYZER_CANDIDATES_FILE:-}" ] || [ "$2" = "${BLOCKLIST_ANALYZER_CONFIGURED_IDS_FILE:-}" ] || [ "$2" = "${BLOCKLIST_ANALYZER_ORPHANED_IDS_FILE:-}" ] || [ "$2" = "${BLOCKLIST_ANALYZER_SELECTED_IDS_FILE:-}" ] || [ "$2" = "${BLOCKLIST_ANALYZER_OUTPUT_FILE:-}" ] || [ "$2" = "${BLOCKLIST_ANALYZER_IDS_FILE:-}" ]; then rm -f "$2" 2>/dev/null fi ;; managed-hook:*) case "$2" in /jffs/scripts/*.agh.$$ | /jffs/scripts/*.agh.$$.content) if [ "$2" = "${MANAGED_HOOK_TMP_FILE:-}" ] || [ "$2" = "${MANAGED_HOOK_TMP_FILE:-}.content" ]; then rm -f "$2" 2>/dev/null fi ;; esac ;; esac } # on_installer_exit runs best-effort recovery and cleanup, returning failure when an active event-hook or setup-file journal cannot be restored. on_installer_exit() { local setup_journal_restore_status setup_journal_restore_status=0 if [ -n "${EVENT_SCRIPTS_ACTIVE_SNAPSHOT:-}" ]; then all_event_scripts_transaction_rollback || setup_journal_restore_status=1 fi if nvram_transaction_lock_owned; then # Restore setup's pre-migration files before mode rollback restores the # matching hooks and configuration. Reversing this order can republish # post-migration YAML over an otherwise complete migration rollback. if ! setup_restore_nvram_journal; then setup_journal_restore_status=1 PTXT "${ERROR} Unable to restore the journaled installer configuration files." fi fi if [ "${setup_journal_restore_status}" -eq 0 ] && [ -n "${MODE_MIGRATION_YAML_FILE_BACKUP:-}" ]; then if rollback_pending_mode_migration; then adguard_restart_after_install_abort "${ADGUARD_INSTALL_WAS_RUNNING:-0}" else PTXT "${ERROR} Unable to roll back the interrupted mode migration." fi fi if [ "${ADGUARD_INSTALL_RESTART_PENDING:-0}" = "1" ]; then adguard_restart_after_failed_replace 1 || PTXT "${ERROR} Unable to restart the restored AdGuardHome installation during exit cleanup." fi if [ -n "${ADGUARD_COMMITTED_BINARY_CLEANUP_PENDING:-}" ]; then adguard_committed_binary_cleanup_finalize "${ADGUARD_COMMITTED_BINARY_CLEANUP_PENDING}" || true fi # An interrupted uncommitted replacement may have stopped AdGuardHome; active state means its backup remains available, so restore both the old binary and prior running state. if [ "${ADGUARD_INSTALL_REPLACE_ACTIVE:-0}" = "1" ] && [ "${ADGUARD_INSTALL_COMMIT_PENDING:-0}" != "1" ]; then adguard_restart_after_install_abort "${ADGUARD_INSTALL_WAS_RUNNING:-0}" || PTXT "${ERROR} Unable to restore the previous AdGuardHome installation during exit cleanup." fi adguard_committed_binary_cleanup_retry || true [ "${ADGUARD_SERVICE_STAGE_OWNED:-0}" != "1" ] || adguard_service_bundle_cleanup [ "${ADGUARD_SERVICE_PUBLISH_OWNED:-0}" != "1" ] || adguard_service_publish_cleanup cleanup_api_files installer_cleanup_tmp_file setup "${SETUP_YAML_TMP_FILE:-}" installer_cleanup_tmp_file blocklist "${BLOCKLIST_YAML_TMP_FILE:-}" installer_cleanup_tmp_file managed-hook "${MANAGED_HOOK_TMP_FILE:-}" installer_cleanup_tmp_file managed-hook "${MANAGED_HOOK_TMP_FILE:-}.content" installer_cleanup_tmp_file blocklist-analysis "${BLOCKLIST_ANALYZER_CANDIDATES_FILE:-}" installer_cleanup_tmp_file blocklist-analysis "${BLOCKLIST_ANALYZER_CONFIGURED_IDS_FILE:-}" installer_cleanup_tmp_file blocklist-analysis "${BLOCKLIST_ANALYZER_ORPHANED_IDS_FILE:-}" installer_cleanup_tmp_file blocklist-analysis "${BLOCKLIST_ANALYZER_SELECTED_IDS_FILE:-}" installer_cleanup_tmp_file blocklist-analysis "${BLOCKLIST_ANALYZER_OUTPUT_FILE:-}" installer_cleanup_tmp_file blocklist-analysis "${BLOCKLIST_ANALYZER_IDS_FILE:-}" if [ "${_DNS_STUBBY_STOPPED:-0}" = "1" ] && ! restore_stubby_after_dns_failure; then PTXT "${ERROR} Unable to restart stubby after interrupted DNS preparation." fi if nvram_transaction_lock_owned; then # Restore an uncommitted setup journal before recovery can remove the # shared commit marker used to distinguish cleanup from rollback. if [ "${setup_journal_restore_status}" -ne 0 ]; then : elif setup_restore_nvram_journal; then # Finalize setup-pair for both WAN and LAN modes only after the # journal is either restored or known to have been committed. nvram_transaction_finalize_setup_pair recovery || PTXT "${ERROR} Unable to finish the committed installer-managed NVRAM cleanup." else PTXT "${ERROR} Unable to restore the journaled installer configuration files." fi if [ "${ADGUARD_INSTALL_MODE:-wan}" = "wan" ]; then # Restore any LAN-domain transaction interrupted before setup finalized it. installer_lan_domain_restore || PTXT "${ERROR} Unable to restore the LAN domain NVRAM setting." # Restore any DNSFilter transaction interrupted before setup finalized it. if ! restore_dns_filter_settings; then PTXT "${ERROR} Unable to restore the DNSFilter NVRAM settings; review ${ROLLBACK_RESULT_FILE} and the preserved snapshot before restarting setup." fi # Restore pre-check DNS settings only when exiting without a local adguardhome installation. if [ ! -f "${AGH_FILE}" ]; then (check_dns_environment 1) || PTXT "${ERROR} Unable to restore the pre-check DNS NVRAM settings." fi fi if [ -f "${BASE_DIR}/.AdGuardHome.nvram/jffs-enable/dirty" ]; then NVRAM_TRANSACTION_DIR="${BASE_DIR}/.AdGuardHome.nvram/jffs-enable" export NVRAM_TRANSACTION_DIR nvram_transaction_restore - || PTXT "${ERROR} Unable to restore the interrupted JFFS NVRAM transaction." fi fi nvram_transaction_lock_release || PTXT "${ERROR} Unable to release the installer NVRAM transaction lock." return "${setup_journal_restore_status}" } python_bcrypt_available() { [ -x /opt/bin/python3 ] && /opt/bin/python3 -c 'import bcrypt' >/dev/null 2>&1 } remove_conflicting_apache() { if opkg_pkg_installed apache || opkg_pkg_installed apache-utils; then opkg_clean_env flag user apache apache-utils >/dev/null 2>&1 opkg_clean_env remove apache apache-utils --force-removal-of-dependent-packages >/dev/null 2>&1 fi } # HTTP/download helpers # http_url_with_cache_token gives each integrity-check attempt a distinct URL. # This prevents independently cached GitHub raw payloads and sidecars from # repeatedly returning different revisions while preserving HTTPS validation. http_url_with_cache_token() { local token url url="$1" token="$2" case "${url}" in *\?*) printf '%s&installer_check=%s\n' "${url}" "${token}" ;; *) printf '%s?installer_check=%s\n' "${url}" "${token}" ;; esac } curl_common_args() { if [ -z "${CURL_COMMON_ARGS_SET:-}" ]; then CURL_COMMON_ARGS="" curl_has_option '--retry' && CURL_COMMON_ARGS="${CURL_COMMON_ARGS} --retry 5" curl_has_option '--connect-timeout' && CURL_COMMON_ARGS="${CURL_COMMON_ARGS} --connect-timeout 25" curl_has_option '--retry-delay' && CURL_COMMON_ARGS="${CURL_COMMON_ARGS} --retry-delay 5" curl_has_option '--max-time' && CURL_COMMON_ARGS="${CURL_COMMON_ARGS} --max-time $((5 * 25))" curl_has_option '--retry-connrefused' && CURL_COMMON_ARGS="${CURL_COMMON_ARGS} --retry-connrefused" CURL_COMMON_ARGS_SET="1" fi PTXT -n "${CURL_COMMON_ARGS}" } curl_has_option() { curl_help | grep -q -e "$1" } curl_help() { if [ -z "${CURL_HELP_CACHE_SET:-}" ]; then CURL_HELP_CACHE="$(curl --help all 2>&1 || curl --help 2>&1)" CURL_HELP_CACHE_SET="1" fi PTXT "${CURL_HELP_CACHE}" } http_get_file() { local URL OUT HEADERS CURL_ARGS WGET_ARGS URL="$1" OUT="$2" HEADERS="${3:-}" if ai_have_cmd curl; then CURL_ARGS="$(curl_common_args)" if [ -n "${HEADERS}" ]; then curl ${CURL_ARGS} -D "${HEADERS}" -fsSL "${URL}" -o "${OUT}" else curl ${CURL_ARGS} -fsSL "${URL}" -o "${OUT}" fi elif ai_have_cmd wget; then WGET_ARGS="$(wget_common_args)" wget_has_option '-nv' && WGET_ARGS="${WGET_ARGS} -nv" if [ -n "${HEADERS}" ] && wget_has_option '--server-response'; then wget ${WGET_ARGS} --server-response -O "${OUT}" "${URL}" 2>"${HEADERS}" else [ -n "${HEADERS}" ] && : >"${HEADERS}" wget ${WGET_ARGS} -O "${OUT}" "${URL}" fi else return 127 fi } http_get_stdout() { local URL CURL_ARGS WGET_ARGS URL="$1" if ai_have_cmd curl; then CURL_ARGS="$(curl_common_args)" curl ${CURL_ARGS} -f -sL "${URL}" elif ai_have_cmd wget; then WGET_ARGS="$(wget_common_args)" wget ${WGET_ARGS} -q -O - "${URL}" else return 127 fi } http_probe() { local URL PROBE_TIMEOUT CURL_ARGS WGET_ARGS URL="$1" PROBE_TIMEOUT="${2:-}" if ai_have_cmd curl; then CURL_ARGS="$(curl_common_args)" if [ -n "${PROBE_TIMEOUT}" ]; then curl_has_option '--retry' && CURL_ARGS="${CURL_ARGS} --retry 0" curl_has_option '--connect-timeout' && CURL_ARGS="${CURL_ARGS} --connect-timeout ${PROBE_TIMEOUT}" curl_has_option '--max-time' && CURL_ARGS="${CURL_ARGS} --max-time ${PROBE_TIMEOUT}" fi curl ${CURL_ARGS} -f -sL -I -o /dev/null "${URL}" elif ai_have_cmd wget; then WGET_ARGS="$(wget_common_args)" if [ -n "${PROBE_TIMEOUT}" ]; then wget_has_option '--tries' && WGET_ARGS="${WGET_ARGS} --tries=1" wget_has_option '--timeout' && WGET_ARGS="${WGET_ARGS} --timeout=${PROBE_TIMEOUT}" wget_has_option '--waitretry' && WGET_ARGS="${WGET_ARGS} --waitretry=0" fi if wget_has_option '--spider'; then wget ${WGET_ARGS} -q --spider "${URL}" else wget ${WGET_ARGS} -q -O /dev/null "${URL}" fi else return 127 fi } wget_common_args() { if [ -z "${WGET_COMMON_ARGS_SET:-}" ]; then WGET_COMMON_ARGS="" wget_has_option '--no-cache' && WGET_COMMON_ARGS="${WGET_COMMON_ARGS} --no-cache" wget_has_option '--no-cookies' && WGET_COMMON_ARGS="${WGET_COMMON_ARGS} --no-cookies" wget_has_option '--tries' && WGET_COMMON_ARGS="${WGET_COMMON_ARGS} --tries=5" wget_has_option '--timeout' && WGET_COMMON_ARGS="${WGET_COMMON_ARGS} --timeout=25" wget_has_option '--waitretry' && WGET_COMMON_ARGS="${WGET_COMMON_ARGS} --waitretry=5" wget_has_option '--retry-connrefused' && WGET_COMMON_ARGS="${WGET_COMMON_ARGS} --retry-connrefused" WGET_COMMON_ARGS_SET="1" fi PTXT -n "${WGET_COMMON_ARGS}" } wget_has_option() { wget_help | grep -q -e "$1" } wget_help() { if [ -z "${WGET_HELP_CACHE_SET:-}" ]; then WGET_HELP_CACHE="$(wget --help 2>&1)" WGET_HELP_CACHE_SET="1" fi PTXT "${WGET_HELP_CACHE}" } # Console and config helpers _quote() { PTXT "$1" | sed 's/[]\/()$*.^|[]/\\&/g' } PTXT() { case "$1" in -n) shift while [ $# -gt 0 ]; do printf "%s" "$1" shift done ;; *) while [ $# -gt 0 ]; do printf "%s\n" "$1" shift done ;; esac } ptxt_phase() { PTXT "${INFO} ====================================================" PTXT "${INFO} PHASE: $1" PTXT "${INFO} ====================================================" } ptxt_step() { PTXT "${INFO} -> $1" } ptxt_ok() { PTXT "${INFO} OK: $1" } ptxt_warn() { PTXT "${WARNING} WARN: $1" } ptxt_fail() { PTXT "${ERROR} FAIL: $1" } SCRIPT_LOC="$(installer_script_path "$0")" || { PTXT "Unable to resolve installer path: $0" >&2 exit 1 } readonly SCRIPT_LOC branch_is_safe() { case "$1" in "" | /* | */ | *..* | *//* | *\\* | *[!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._/-]*) return 1 ;; esac return 0 } conf_value() { [ -f "${CONF_FILE}" ] || return 1 awk -v KEY="$1" ' index($0, KEY "=") == 1 { VALUE = substr($0, length(KEY) + 2) gsub(/^"|"$/, "", VALUE) print VALUE exit } ' "${CONF_FILE}" } conf_has_key() { [ -f "${CONF_FILE}" ] || return 1 grep -q "^$1=" "${CONF_FILE}" } adguard_ipset_allowed() { case "${ADGUARD_INSTALL_MODE:-$(conf_value ADGUARD_INSTALL_MODE 2>/dev/null)}" in wan) return 0 ;; lan | ap | bridge) wan_iptables_state_active 2>/dev/null return $? ;; esac return 1 } write_conf_if_absent() { conf_has_key "$1" && return 0 write_conf "$1" "$2" } adguard_install_feature_defaults() { local dnsmasq_mode ipset_value lan_gateway case "${ADGUARD_INSTALL_MODE:-}" in wan) ipset_value="$(conf_value ADGUARD_IPSET)" dnsmasq_mode="$(conf_value ADGUARD_DNSMASQ_MODE 2>/dev/null)" case "${ipset_value}" in NO) ;; *) ipset_value="YES" ;; esac write_conf ADGUARD_IPSET "\"${ipset_value}\"" || return 1 case "${dnsmasq_mode}" in disabled) ;; *) write_conf ADGUARD_DNSMASQ_MODE "\"enabled\"" || return 1 ;; esac ;; lan) ipset_value="$(conf_value ADGUARD_IPSET 2>/dev/null)" if adguard_ipset_allowed && [ "${ipset_value}" = "YES" ]; then write_conf ADGUARD_IPSET "\"YES\"" || return 1 else write_conf ADGUARD_IPSET "\"NO\"" || return 1 fi dnsmasq_mode="$(conf_value ADGUARD_DNSMASQ_MODE 2>/dev/null)" case "${dnsmasq_mode}" in disabled) ;; *) write_conf ADGUARD_DNSMASQ_MODE "\"auto\"" || return 1 ;; esac lan_gateway="$(installer_nvram get lan_gateway 2>/dev/null)" || lan_gateway="" case "${lan_gateway}" in "" | 0.0.0.0) ;; *) if ipv4_is_valid "${lan_gateway}"; then write_conf_if_absent ADGUARD_LAN_REVERSE_UPSTREAM "\"${lan_gateway}\"" || return 1 fi ;; esac ;; *) PTXT "${ERROR} Unknown AdGuardHome install mode: ${ADGUARD_INSTALL_MODE:-}. Setup was aborted." return 1 ;; esac return 0 } # configure_runtime_defaults saves missing runtime preferences for $1=new-install # or upgrade, preserving explicit choices. New installs require wan/lan in $2; # invalid install mode or a failed preference write returns 1. configure_runtime_defaults() { local NETCHECK_MODE case "${1:-}" in new-install) case "${2:-}" in wan) NETCHECK_MODE="wan" ;; lan) NETCHECK_MODE="lan" ;; *) PTXT "${ERROR} A confirmed router install mode is required before saving runtime defaults." return 1 ;; esac write_conf_if_absent ADGUARDHOME_REFUSE_UNKNOWN_DNS_PORT_KILL "\"1\"" || return 1 write_conf ADGUARD_INSTALL_MODE "\"${NETCHECK_MODE}\"" || return 1 ADGUARD_INSTALL_MODE="${NETCHECK_MODE}" adguard_install_feature_defaults || return 1 write_conf_if_absent ADGUARD_NETCHECK_MODE "\"${NETCHECK_MODE}\"" || return 1 write_conf_if_absent ADGUARD_PROC_OPTIMIZE "\"YES\"" || return 1 write_conf_if_absent ADGUARD_PROC_PROFILE "\"aggressive\"" || return 1 ptxt_ok "Runtime defaults saved: DNS port refusal enabled, netcheck mode ${NETCHECK_MODE}, performance profile aggressive." ;; upgrade) write_conf_if_absent ADGUARDHOME_REFUSE_UNKNOWN_DNS_PORT_KILL "\"1\"" || return 1 case "${ADGUARD_INSTALL_MODE_DETECTION:-unknown}" in wan | lan) NETCHECK_MODE="${ADGUARD_INSTALL_MODE_DETECTION}" ;; *) NETCHECK_MODE="legacy" ;; esac write_conf_if_absent ADGUARD_NETCHECK_MODE "\"${NETCHECK_MODE}\"" || return 1 write_conf_if_absent ADGUARD_PROC_OPTIMIZE "\"YES\"" || return 1 write_conf_if_absent ADGUARD_PROC_PROFILE "\"aggressive\"" || return 1 PTXT "${INFO} Existing runtime defaults were retained for compatibility." \ "${INFO} Legacy values remain active unless you migrate with:" \ "${INFO} sh installer migrate-runtime-defaults --yes" ;; esac } md5_is_valid() { case "$1" in "" | *[!0123456789abcdefABCDEF]*) return 1 ;; esac [ "${#1}" -eq 32 ] } sha256_is_valid() { case "$1" in "" | *[!0123456789abcdefABCDEF]*) return 1 ;; esac [ "${#1}" -eq 64 ] } file_md5() { local FILE MD5_OUTPUT MD5_VALUE FILE="$1" ai_have_cmd md5sum || return 1 MD5_OUTPUT="$(md5sum "${FILE}")" || return 1 MD5_VALUE="$(PTXT "${MD5_OUTPUT}" | awk 'NF {print $1; found = 1; exit} END {if (!found) exit 1}')" || return 1 md5_is_valid "${MD5_VALUE}" || return 1 PTXT "${MD5_VALUE}" } file_sha256() { local FILE SHA256_OUTPUT SHA256_VALUE FILE="$1" SHA256_OUTPUT="" if ai_have_cmd sha256sum; then SHA256_OUTPUT="$(sha256sum "${FILE}")" || SHA256_OUTPUT="" fi if [ -z "${SHA256_OUTPUT}" ] && [ -x /opt/bin/sha256sum ]; then SHA256_OUTPUT="$(/opt/bin/sha256sum "${FILE}")" || SHA256_OUTPUT="" fi if [ -z "${SHA256_OUTPUT}" ] && [ -x /bin/busybox ]; then SHA256_OUTPUT="$(/bin/busybox sha256sum "${FILE}")" || SHA256_OUTPUT="" fi if [ -z "${SHA256_OUTPUT}" ] && ai_have_cmd busybox; then SHA256_OUTPUT="$(busybox sha256sum "${FILE}")" || SHA256_OUTPUT="" fi if [ -z "${SHA256_OUTPUT}" ] && ai_have_cmd openssl; then SHA256_OUTPUT="$(openssl dgst -sha256 "${FILE}")" || SHA256_OUTPUT="" SHA256_OUTPUT="$(PTXT "${SHA256_OUTPUT}" | awk '{printf "%s", $2}')" || SHA256_OUTPUT="" else SHA256_OUTPUT="$(PTXT "${SHA256_OUTPUT}" | awk '{printf "%s", $1}')" || SHA256_OUTPUT="" fi if ! sha256_is_valid "${SHA256_OUTPUT}"; then PTXT "${ERROR} Invalid SHA-256 checksum." return 1 else SHA256_VALUE="${SHA256_OUTPUT}" PTXT -n "${SHA256_VALUE}" fi } sha256_manifest_digest() { local MANIFEST DIGEST MANIFEST="$1" [ -s "${MANIFEST}" ] || return 1 DIGEST="$(awk 'NF { count++; if (count == 1 && NF == 1) print $1; else invalid = 1 } END { if (count != 1 || invalid) exit 1 }' "${MANIFEST}")" || return 1 sha256_is_valid "${DIGEST}" || return 1 PTXT -n "${DIGEST}" } md5_manifest_digest() { local MANIFEST DIGEST MANIFEST="$1" [ -s "${MANIFEST}" ] || return 1 DIGEST="$(awk 'NF { count++; if (count == 1) print $1 } END { if (count != 1) exit 1 }' "${MANIFEST}")" || return 1 md5_is_valid "${DIGEST}" || return 1 PTXT -n "${DIGEST}" } ipv4_is_valid() { PTXT "$1" | awk -F. ' NF != 4 { exit 1 } { for (i = 1; i <= 4; i++) { if ($i !~ /^[0-9][0-9]*$/ || $i < 0 || $i > 255) exit 1 } } ' } ipv4_is_usable_unicast() { ipv4_is_valid "$1" || return 1 PTXT "$1" | awk -F. ' $1 == 0 || $1 == 127 || $1 >= 224 { exit 1 } ' } ipv4_is_private() { ipv4_is_valid "$1" || return 1 PTXT "$1" | awk -F. ' $1 == 10 || $1 == 127 || ($1 == 172 && $2 >= 16 && $2 <= 31) || ($1 == 192 && $2 == 168) { found = 1 } END { exit(found ? 0 : 1) } ' } adguard_install_mode_detect() { local lan_ipaddr sw_mode sw_mode="$(installer_nvram get sw_mode 2>/dev/null)" || sw_mode="" ADGUARD_INSTALL_MODE_DETECTION="unknown" ADGUARD_INSTALL_MODE_RAW="${sw_mode}" ADGUARD_INSTALL_MODE_SW_MODE="${sw_mode}" case "${sw_mode}" in 1) ADGUARD_INSTALL_MODE_DETECTION="wan" ADGUARD_INSTALL_MODE="wan" ;; 2 | 3 | 4) lan_ipaddr="$(installer_nvram get lan_ipaddr 2>/dev/null)" || lan_ipaddr="" if ! ipv4_is_usable_unicast "${lan_ipaddr}"; then ADGUARD_INSTALL_MODE_RAW="sw_mode=${sw_mode}, lan_ipaddr=${lan_ipaddr:-unreadable}" PTXT "${WARN:-WARNING:} Router install mode is unknown/unreadable (${ADGUARD_INSTALL_MODE_RAW}); retaining any persisted install mode." return 0 fi ADGUARD_INSTALL_MODE_DETECTION="lan" ADGUARD_INSTALL_MODE="lan" ;; *) PTXT "${WARN:-WARNING:} Router install mode is unknown/unreadable (sw_mode=${sw_mode:-unreadable}); retaining any persisted install mode." ;; esac return 0 } adguard_install_mode_confirmed() { case "${ADGUARD_INSTALL_MODE_DETECTION:-unknown}" in wan | lan) return 0 ;; esac return 1 } port_is_valid() { case "$1" in "" | *[!0123456789]*) return 1 ;; esac [ "$1" -ge 3000 ] 2>/dev/null && [ "$1" -le 65535 ] 2>/dev/null } runtime_port_is_valid() { case "$1" in "" | *[!0123456789]*) return 1 ;; esac [ "$1" -ge 1 ] 2>/dev/null && [ "$1" -le 65535 ] 2>/dev/null } web_port_in_use() { netstat -nl 2>/dev/null | awk -v port=":$1" ' $4 ~ port "$" { found = 1 } END { exit(found ? 0 : 1) } ' } web_port_owned_by_agh() { netstat -nlp 2>/dev/null | awk -v port=":$1" ' $0 ~ /^(tcp)6?[[:space:]]+/ && $4 ~ port "$" && $0 ~ "/AdGuardHome([[:space:]]|$)" { found = 1 } END { exit(found ? 0 : 1) } ' } installer_dependencies_available() { local REQUIRED_COMMAND for REQUIRED_COMMAND in awk chmod chown cp cut grep kill ln md5sum mkdir mv netstat nvram pidof rm sed service tar; do if ! ai_have_cmd "${REQUIRED_COMMAND}"; then PTXT "${ERROR} Required command is unavailable: ${REQUIRED_COMMAND}" >&2 return 1 fi done if ! entware_available; then PTXT "${ERROR} Entware is not available. Install or mount Entware before running this installer." >&2 return 1 fi return 0 } nvram_transaction_recovery_dependencies_available() { local required_command for required_command in awk cat chmod kill ln mkdir nvram rm sed service; do if ! ai_have_cmd "${required_command}"; then PTXT "${ERROR} Required NVRAM recovery command is unavailable: ${required_command}" >&2 return 1 fi done return 0 } preflight_action_requires_entware() { case "${1:-install}" in status | preflight) return 1 ;; *) return 0 ;; esac } preflight_action_requires_jq() { case "${1:-install}" in "" | install | update | reconfigure | restore | 1 | 4 | [rR]) return 0 ;; esac return 1 } preflight_action_requires_password_hash() { case "${1:-install}" in "" | install | reconfigure | changepw | 3 | 4) return 0 ;; esac return 1 } preflight_action_requires_sha256() { case "${1:-install}" in "" | install | update | restore | switchbranch | 1 | 7 | [rR] | blocklists | unusedblocklists | 9) return 0 ;; esac return 1 } preflight_action_requires_timezone_column() { case "${1:-install}" in "" | install | reconfigure | restore | 4) return 0 ;; esac return 1 } preflight_action_requires_jffs_ready() { case "${1:-install}" in "" | install | reconfigure | 4) return 0 ;; esac return 1 } preflight_action_requires_router_eligibility() { case "${1:-install}" in uninstall | status | preflight) return 1 ;; esac return 0 } preflight_action_requires_downloader() { case "${1:-install}" in status | preflight) return 1 ;; esac return 0 } preflight_action_requires_service_tools() { case "${1:-install}" in status | preflight) return 1 ;; esac return 0 } preflight_action_requires_cru() { case "${1:-install}" in status | preflight) return 1 ;; esac return 0 } adguard_install_mode_detect_once() { if [ "${PREFLIGHT_INSTALL_MODE_DETECTED:-0}" != "1" ]; then adguard_install_mode_detect >/dev/null 2>&1 fi } preflight_action_requires_firewall_tools() { case "${1:-install}" in status | preflight) return 1 ;; esac adguard_install_mode_detect_once adguard_install_mode_confirmed || return 1 [ "${ADGUARD_INSTALL_MODE}" = "lan" ] && return 1 return 0 } # jffs_read_settings reads only JFFS state. Inventory is needed solely to # distinguish legacy absent keys from a failed read of an existing setting. jffs_read_settings() { local format_status scripts_status enable_status inventory key value read_status jffs_read_reason="" jffs_enable_key="jffs2_enable" format_status=0 scripts_status=0 enable_status=0 jffs2_format_value="$(installer_nvram get jffs2_format 2>/dev/null)" || format_status=1 jffs2_scripts_value="$(installer_nvram get jffs2_scripts 2>/dev/null)" || scripts_status=1 jffs2_enabled_value="$(installer_nvram get jffs2_enable 2>/dev/null)" || enable_status=1 inventory="" # Stream large HND inventories only when needed; never pass their values # as argv or retain unrelated secrets. HND may fail after usable output. if { [ "${format_status}${scripts_status}${enable_status}" != "000" ] || [ -z "${jffs2_format_value}" ] || [ -z "${jffs2_scripts_value}" ] || [ -z "${jffs2_enabled_value}" ]; } && ! inventory="$(installer_nvram show 2>/dev/null | awk ' /^[[:alnum:]_][[:alnum:]_]*=/ { valid = 1 } /^jffs2_format=/ { format = 1 } /^jffs2_scripts=/ { scripts = 1 } /^jffs2_enable=/ { enable = 1 } /^jffs2_on=/ { legacy = 1 } END { if (!valid) exit 1 print "inventory=1" if (format) print "jffs2_format=" if (scripts) print "jffs2_scripts=" if (enable) print "jffs2_enable=" if (legacy) print "jffs2_on=" } ')"; then jffs_read_reason="read-failed:nvram-inventory" return 1 fi for key in jffs2_format jffs2_scripts jffs2_enable; do case "${key}" in jffs2_format) value="${jffs2_format_value}" read_status="${format_status}" ;; jffs2_scripts) value="${jffs2_scripts_value}" read_status="${scripts_status}" ;; jffs2_enable) value="${jffs2_enabled_value}" read_status="${enable_status}" ;; *) : ;; esac if [ "${read_status}" -ne 0 ] && { [ -n "${value}" ] || printf '%s\n' "${inventory}" | grep -q "^${key}="; }; then jffs_read_reason="read-failed:${key}" return 1 fi case "${key}:${value}" in jffs2_format:) if printf '%s\n' "${inventory}" | grep -q '^jffs2_format='; then jffs_read_reason="missing-value:jffs2_format" return 1 fi ;; jffs2_scripts:) if printf '%s\n' "${inventory}" | grep -q '^jffs2_scripts='; then jffs_read_reason="missing-value:jffs2_scripts" return 1 fi ;; jffs2_enable:) ;; *:0 | *:1) ;; *) jffs_read_reason="invalid-value:${key}" return 1 ;; esac done if [ -z "${jffs2_enabled_value}" ]; then jffs_enable_key="jffs2_on" read_status=0 jffs2_enabled_value="$(installer_nvram get jffs2_on 2>/dev/null)" || read_status=1 if [ "${read_status}" -ne 0 ] && { [ -n "${jffs2_enabled_value}" ] || printf '%s\n' "${inventory}" | grep -q '^jffs2_on='; }; then jffs_read_reason="read-failed:jffs2_on" return 1 fi case "${jffs2_enabled_value}" in "") if printf '%s\n' "${inventory}" | grep -q '^jffs2_on='; then jffs_read_reason="missing-value:jffs2_on" return 1 fi ;; 0 | 1) ;; *) jffs_read_reason="invalid-value:jffs2_on" return 1 ;; esac fi return 0 } preflight_check_jffs_ready() { local format_state read_ok result result="OK" read_ok=1 if ! jffs_read_settings; then result="FAIL" read_ok=0 elif [ "${jffs2_format_value}" = "1" ]; then result="FAIL" jffs_read_reason="format-pending" elif [ "${jffs2_scripts_value}" != "1" ] || [ "${jffs2_enabled_value}" != "1" ]; then result="FAIL" jffs_read_reason="enablement-required" fi format_state="${jffs2_format_value:-missing-or-unreadable}" if [ -z "${jffs2_format_value}" ] && [ "${read_ok}" = 1 ]; then format_state="legacy-absent" fi PTXT "preflight.jffs.required=yes" PTXT "preflight.jffs.format_pending=${format_state}" PTXT "preflight.jffs.scripts=${jffs2_scripts_value:-missing}" PTXT "preflight.jffs.enabled=${jffs2_enabled_value:-missing}" PTXT "preflight.jffs.enable_key=${jffs_enable_key}" [ -z "${jffs_read_reason}" ] || PTXT "preflight.jffs.reason=${jffs_read_reason}" PTXT "preflight.jffs.result=${result}" [ "${result}" = "OK" ] } preflight_check_router_eligibility() { local arch model odmpid os result result="OK" PTXT "preflight.router.required=yes" if [ -z "${ROUTER_MODEL:-}" ]; then odmpid="$(installer_nvram get odmpid 2>/dev/null)" || odmpid="" [ -z "${odmpid}" ] && model="$(installer_nvram get productid 2>/dev/null)" || model="${odmpid}" else model="${ROUTER_MODEL}" fi os="$(/bin/uname 2>/dev/null)" || os="" case "${model}" in *AC*) case "${model}" in *RT-AC86* | *RT-AC2900*) arch="$(/bin/uname -m 2>/dev/null)" || arch="" ;; *) arch="armv7" ;; esac ;; *) arch="$(/bin/uname -m 2>/dev/null)" || arch="" ;; esac PTXT "preflight.router.model=${model:-missing}" if [ -z "${model}" ]; then PTXT "preflight.router.model.result=FAIL" result="FAIL" else PTXT "preflight.router.model.result=OK" fi adguard_install_mode_detect_once PTXT "preflight.router.sw_mode=${ADGUARD_INSTALL_MODE_SW_MODE:-missing}" case "${ADGUARD_INSTALL_MODE_DETECTION:-unknown}" in wan | lan) PTXT "preflight.router.mode=${ADGUARD_INSTALL_MODE_DETECTION}" PTXT "preflight.router.mode.result=OK" ;; *) PTXT "preflight.router.mode=unknown" PTXT "preflight.router.mode.value=${ADGUARD_INSTALL_MODE_RAW:-unreadable}" PTXT "preflight.router.mode.result=FAIL" PTXT "preflight.router.mode.reason=unknown-or-unreadable-router-mode" result="FAIL" ;; esac case "${os}" in Linux) PTXT "preflight.router.os=linux" PTXT "preflight.router.os.result=OK" ;; *) PTXT "preflight.router.os=${os:-missing}" PTXT "preflight.router.os.result=FAIL" result="FAIL" ;; esac case "${arch}" in aarch64 | arm64 | armv7l | armv7) PTXT "preflight.router.arch=${arch}" PTXT "preflight.router.arch.result=OK" ;; *) PTXT "preflight.router.arch=${arch:-missing}" PTXT "preflight.router.arch.result=FAIL" result="FAIL" ;; esac if [ "${result}" = "OK" ]; then PTXT "preflight.router.result=OK" return 0 fi PTXT "preflight.router.result=FAIL" return 1 } preflight_check_path() { local description path result which_path description="$1" path="$2" result="FAIL" which_path="$(which "${path##*/}" 2>/dev/null)" || which_path="" if [ -x "${path}" ]; then result="OK" fi PTXT "preflight.${description}.required=${path}" PTXT "preflight.${description}.which=${which_path:-missing}" PTXT "preflight.${description}.result=${result}" [ "${result}" = "OK" ] } preflight_check_command() { local command_name result which_path command_name="$1" result="FAIL" which_path="$(which "${command_name}" 2>/dev/null)" || which_path="" if [ -n "${which_path}" ] && [ -x "${which_path}" ]; then result="OK" fi PTXT "preflight.stock.${command_name}.required=yes" PTXT "preflight.stock.${command_name}.which=${which_path:-missing}" PTXT "preflight.stock.${command_name}.result=${result}" [ "${result}" = "OK" ] } preflight_check_nvram() { local nvram_binary result result="FAIL" nvram_binary="$(installer_nvram_path)" || nvram_binary="" [ -z "${nvram_binary}" ] || result="OK" PTXT "preflight.nvram.required=/bin/nvram or /usr/sbin/nvram" PTXT "preflight.nvram.which=${nvram_binary:-missing}" PTXT "preflight.nvram.result=${result}" [ "${result}" = "OK" ] } preflight_check_stock_commands() { local failed required_command failed="0" for required_command in awk chmod chown cp cut grep kill ln md5sum mkdir mv netstat pidof rm sed tar; do preflight_check_command "${required_command}" || failed="1" done [ "${failed}" = "0" ] } preflight_check_downloader() { local curl_path wget_path result curl_path="$(which curl 2>/dev/null)" || curl_path="" wget_path="$(which wget 2>/dev/null)" || wget_path="" result="FAIL" if [ -x /usr/sbin/curl ] || [ -x /usr/sbin/wget ]; then result="OK" fi PTXT "preflight.downloader.required=/usr/sbin/curl or /usr/sbin/wget" PTXT "preflight.downloader.curl.which=${curl_path:-missing}" PTXT "preflight.downloader.curl.stock=$(if [ -x /usr/sbin/curl ]; then PTXT OK; else PTXT missing; fi)" PTXT "preflight.downloader.wget.which=${wget_path:-missing}" PTXT "preflight.downloader.wget.stock=$(if [ -x /usr/sbin/wget ]; then PTXT OK; else PTXT missing; fi)" PTXT "preflight.downloader.result=${result}" [ "${result}" = "OK" ] } preflight_check_entware() { local result result="FAIL" if entware_available; then result="OK" fi PTXT "preflight.entware.required=yes" PTXT "preflight.entware.opt_dir=$(if [ -d /opt ]; then PTXT OK; else PTXT missing; fi)" PTXT "preflight.entware.opkg=$(if [ -x /opt/bin/opkg ]; then PTXT OK; else PTXT missing; fi)" PTXT "preflight.entware.config=$(if [ -f /opt/etc/opkg.conf ] || [ -f /opt/etc/init.d/rc.unslung ]; then PTXT OK; else PTXT missing; fi)" PTXT "preflight.entware.result=${result}" [ "${result}" = "OK" ] } preflight_check_entware_package() { local install_hint pkg result pkg="$1" install_hint="${2:-opkg install ${pkg} --force-depends --force-overwrite --force-reinstall}" result="MISSING" if opkg_pkg_installed "${pkg}"; then result="OK" fi PTXT "preflight.entware.package.${pkg}.required=yes" PTXT "preflight.entware.package.${pkg}.result=${result}" [ "${result}" = "OK" ] || PTXT "preflight.entware.package.${pkg}.install_hint=${install_hint}" [ "${result}" = "OK" ] } preflight_check_jq() { local allow_entware entware_result jq_path result stock_result allow_entware="$1" jq_path="$(which jq 2>/dev/null)" || jq_path="" result="FAIL" stock_result="missing" entware_result="missing" [ ! -e /usr/bin/jq ] || stock_result="unusable" [ ! -e /opt/bin/jq ] || entware_result="unusable" if jq_executable_usable "${jq_path}"; then result="OK" fi if jq_executable_usable /usr/bin/jq; then stock_result="OK" fi if jq_executable_usable /opt/bin/jq; then entware_result="OK" if [ "${allow_entware}" = "1" ]; then result="OK" fi fi PTXT "preflight.jq.required=yes" PTXT "preflight.jq.which=${jq_path:-missing}" PTXT "preflight.jq.stock=${stock_result}" if [ "${allow_entware}" = "1" ]; then PTXT "preflight.jq.entware=${entware_result}" if [ "${result}" != "OK" ]; then preflight_check_entware_package jq-full "opkg install jq-full --force-depends --force-overwrite --force-reinstall" || true fi else PTXT "preflight.jq.entware=not_checked" fi PTXT "preflight.jq.result=${result}" [ "${result}" = "OK" ] } preflight_check_password_hash_support() { local failed result failed="0" result="MISSING" if python_bcrypt_available || bcrypt_tool_available; then result="OK" fi PTXT "preflight.entware.password_hash.required=yes" PTXT "preflight.entware.password_hash.result=${result}" if [ "${result}" != "OK" ]; then PTXT "preflight.entware.password_hash.install_hint=opkg install python3 python3-bcrypt --force-depends --force-overwrite --force-reinstall" preflight_check_entware_package python3 || failed="1" preflight_check_entware_package python3-bcrypt || failed="1" fi [ "${failed}" = "0" ] && [ "${result}" = "OK" ] } preflight_check_sha256_support() { if sha256sum_available; then PTXT "preflight.sha256.required=yes" PTXT "preflight.sha256.result=OK" PTXT "preflight.entware.package.coreutils-sha256sum.required=no" return 0 fi PTXT "preflight.sha256.required=yes" PTXT "preflight.sha256.result=MISSING" if [ "${1:-0}" = "1" ]; then preflight_check_entware_package coreutils-sha256sum || true else PTXT "preflight.entware.package.coreutils-sha256sum.required=not_checked" fi return 1 } preflight_check_timezone_column() { if [ -x /opt/bin/column ]; then PTXT "preflight.entware.column.required=yes" PTXT "preflight.entware.column.result=OK" return 0 fi PTXT "preflight.entware.column.required=yes" PTXT "preflight.entware.column.result=MISSING" preflight_check_entware_package column || true return 1 } preflight() { local action entware_available_now entware_required failed action="${1:-install}" entware_available_now="0" entware_required="0" failed="0" PTXT "preflight.version=${AI_VERSION}" PTXT "preflight.action=${action}" PTXT "preflight.safe_to_paste=yes" PTXT "preflight.path=${PATH}" adguard_install_mode_detect >/dev/null 2>&1 PREFLIGHT_INSTALL_MODE_DETECTED="1" preflight_check_path "sh" "/bin/sh" || failed="1" preflight_check_nvram || failed="1" if preflight_action_requires_downloader "${action}"; then preflight_check_downloader || failed="1" else PTXT "preflight.downloader.required=no" PTXT "preflight.downloader.result=SKIP" fi if preflight_action_requires_service_tools "${action}"; then preflight_check_path "service" "/sbin/service" || failed="1" else PTXT "preflight.service.required=no" PTXT "preflight.service.result=SKIP" fi if preflight_action_requires_cru "${action}"; then preflight_check_path "cru" "/usr/sbin/cru" || failed="1" else PTXT "preflight.cru.required=no" PTXT "preflight.cru.result=SKIP" fi if preflight_action_requires_firewall_tools "${action}"; then preflight_check_path "iptables" "/usr/sbin/iptables" || failed="1" preflight_check_path "ip6tables" "/usr/sbin/ip6tables" || failed="1" else PTXT "preflight.iptables.required=no" PTXT "preflight.iptables.result=SKIP" PTXT "preflight.ip6tables.required=no" PTXT "preflight.ip6tables.result=SKIP" fi preflight_check_stock_commands || failed="1" if preflight_action_requires_jffs_ready "${action}"; then preflight_check_jffs_ready || failed="1" else PTXT "preflight.jffs.required=no" PTXT "preflight.jffs.result=SKIP" fi if preflight_action_requires_router_eligibility "${action}"; then preflight_check_router_eligibility || failed="1" else PTXT "preflight.router.required=no" PTXT "preflight.router.result=SKIP" fi if preflight_action_requires_entware "${action}"; then entware_required="1" if preflight_check_entware; then entware_available_now="1" else failed="1" fi else PTXT "preflight.entware.required=no" PTXT "preflight.entware.result=SKIP" fi if preflight_action_requires_jq "${action}"; then preflight_check_jq "${entware_required}" || failed="1" else PTXT "preflight.jq.required=no" PTXT "preflight.jq.result=SKIP" fi if preflight_action_requires_sha256 "${action}"; then preflight_check_sha256_support "${entware_available_now}" || failed="1" else PTXT "preflight.sha256.required=no" PTXT "preflight.sha256.result=SKIP" fi if [ "${entware_required}" = "1" ] && [ "${entware_available_now}" = "1" ]; then if preflight_action_requires_password_hash "${action}"; then preflight_check_password_hash_support || failed="1" else PTXT "preflight.entware.password_hash.required=no" PTXT "preflight.entware.password_hash.result=SKIP" fi if preflight_action_requires_timezone_column "${action}"; then preflight_check_timezone_column || failed="1" else PTXT "preflight.entware.column.required=no" PTXT "preflight.entware.column.result=SKIP" fi elif [ "${entware_required}" = "1" ]; then PTXT "preflight.entware.dependent_checks=SKIP_ENTWARE_MISSING" fi if [ "${failed}" = "0" ]; then PTXT "preflight.result=PASS" return 0 fi PTXT "preflight.result=FAIL" return 1 } sanitize_branch() { local CANDIDATE CANDIDATE="$1" if branch_is_safe "${CANDIDATE}"; then PTXT "${CANDIDATE}" else PTXT "${ERROR} Unsupported installer source: ${CANDIDATE}; refusing to select another source." >&2 return 1 fi } # AdGuardHome process helpers adguard_pid_list_has_new_pid() { local new_pid old_pids old_pids=" ${1:-} " shift for new_pid in "$@"; do case "${old_pids}" in *" ${new_pid} "*) ;; *) return 0 ;; esac done return 1 } # Poll for completion of $1 (default restart), using $2 as the old PID list. # $3 overrides the wait in seconds; otherwise use ADGUARDHOME_WAIT_TIMEOUT, # capped by ADGUARDHOME_FIRMWARE_WAIT_TIMEOUT for $4's default firmware phase. # The fallback phase uses the full wait. Return 0 and report final status when # complete, or print the transitional state and return 1 when the wait expires. adguard_service_status_after_action() { local action elapsed maxwait old_pids phase phase_label status_message transition_message action="${1:-restart}" old_pids="${2:-}" elapsed="0" maxwait="${3:-${ADGUARDHOME_WAIT_TIMEOUT}}" phase="${4:-firmware}" case "${phase}" in fallback) phase_label=" (direct init fallback)" ;; *) phase="firmware" phase_label=" (firmware service)" ;; esac # Firmware requests run asynchronously; allow the RC hook time to complete # its monitor and DNS preflight before falling back to the init script. if [ "${phase}" = "firmware" ] && [ -z "${3:-}" ]; then [ "${maxwait}" -le "${ADGUARDHOME_FIRMWARE_WAIT_TIMEOUT}" ] || maxwait="${ADGUARDHOME_FIRMWARE_WAIT_TIMEOUT}" fi case "${action}" in stop | kill) status_message="Waiting for AdGuardHome to stop cleanly${phase_label}..." transition_message="Stopping..." ;; start) status_message="Waiting for AdGuardHome to report running state after start${phase_label}..." transition_message="Restarting..." ;; restart | *) status_message="Waiting for AdGuardHome to report running state after restart${phase_label}..." transition_message="Restarting..." ;; esac PTXT "${INFO} ${status_message}" case "${action}" in stop | kill) while [ "$(agh_process_count)" -ge "1" ]; do if [ "${elapsed}" -ge "${maxwait}" ]; then PTXT "${INFO} ${transition_message}" return 1 fi sleep 1s elapsed="$((elapsed + 1))" done ;; restart) while ! { agh_is_running && { [ -z "${old_pids}" ] || adguard_pid_list_has_new_pid "${old_pids}" "$(pidof AdGuardHome 2>/dev/null)"; }; }; do if [ "${elapsed}" -ge "${maxwait}" ]; then PTXT "${INFO} ${transition_message}" return 1 fi sleep 1s elapsed="$((elapsed + 1))" done ;; *) while ! agh_is_running; do if [ "${elapsed}" -ge "${maxwait}" ]; then PTXT "${INFO} ${transition_message}" return 1 fi sleep 1s elapsed="$((elapsed + 1))" done ;; esac agh_check return 0 } valid_adguardhome_username() { case "${1:-}" in "" | *[!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-]*) return 1 ;; *) return 0 ;; esac } valid_adguardhome_password_input() { [ -n "${1:-}" ] || return 1 [ "${1}" = "${2:-}" ] || return 1 case "${1}" in " "* | *" " | *[[:cntrl:]]*) return 1 ;; esac return 0 } AdGuardHome_authen() { local YAML_TARGET="${2:-${YAML_ORI}}" local YAML_VALIDATE="${3:-1}" if [ -z "${PW1}" ] || [ -z "${PW2}" ]; then local USERNAME PTXT -n "${INPUT} Please enter AdGuardHome username${NORM}: " if ! IFS= read -r USERNAME; then return 1 fi fi if ! valid_adguardhome_username "${USERNAME:-}"; then PTXT "${ERROR} Invalid AdGuardHome username. Accepted characters: letters, digits, dot (.), underscore (_), and hyphen (-)." return 1 fi local PW1 PW2 PTXT -n "${INPUT} Please enter AdGuardHome password${NORM}: " local PASSWORD_ECHO_DISABLED=0 if stty -echo 2>/dev/null; then PASSWORD_ECHO_DISABLED=1 fi if ! IFS= read -r PW1; then [ "${PASSWORD_ECHO_DISABLED}" -eq 0 ] || stty echo 2>/dev/null PTXT " " return 1 fi [ "${PASSWORD_ECHO_DISABLED}" -eq 0 ] || stty echo 2>/dev/null PTXT " " PTXT -n "${INPUT} Please reenter AdGuardHome password${NORM}: " PASSWORD_ECHO_DISABLED=0 if stty -echo 2>/dev/null; then PASSWORD_ECHO_DISABLED=1 fi if ! IFS= read -r PW2; then [ "${PASSWORD_ECHO_DISABLED}" -eq 0 ] || stty echo 2>/dev/null PTXT " " return 1 fi [ "${PASSWORD_ECHO_DISABLED}" -eq 0 ] || stty echo 2>/dev/null PTXT " " if ! valid_adguardhome_password_input "${PW1}" "${PW2}"; then PTXT "${ERROR} Password entered incorrectly!" AdGuardHome_authen "$1" "${YAML_TARGET}" "${YAML_VALIDATE}" return fi remove_conflicting_apache if ! ensure_password_hash_tool; then PTXT "${ERROR} Unable to install a password hashing tool from Entware." end_op_message 1 return fi local PW1_ENCRYPTED PW1_ENCRYPTED="" if python_bcrypt_available; then PW1_ENCRYPTED="$(hash_password_python "${PW1}")" || PW1_ENCRYPTED="" fi if [ -z "${PW1_ENCRYPTED}" ] && [ -f "/opt/bin/bcrypt-tool" ]; then PW1_ENCRYPTED="$(/opt/bin/bcrypt-tool hash "${PW1}" 10)" || PW1_ENCRYPTED="" fi case "${PW1_ENCRYPTED}" in \$2a\$10\$*) ;; *) PTXT "${ERROR} Password could not be set!" "${ERROR} Please contact dev." end_op_message 1 return ;; esac if [ "$1" -eq 0 ]; then yaml_nvars_file_action delete "- name:" "${YAML_FILE}" yaml_nvars_file_action delete " password:" "${YAML_FILE}" yaml_nvars_file_action insert "users:" "\ password: ${PW1_ENCRYPTED}" "${YAML_FILE}" yaml_nvars_file_action insert "users:" "- name: ${USERNAME}" "${YAML_FILE}" check_AdGuardHome_yaml "${YAML_FILE}" ptxt_step "Waiting for AdGuardHome post-install readiness checks." if ! agh_complete_startup; then end_op_message 1 return fi end_op_message 0 else PTXT "users:" \ "- name: ${USERNAME}" \ " password: ${PW1_ENCRYPTED}" >>"${YAML_TARGET}" if [ "${YAML_VALIDATE}" -ne 0 ] && ! check_AdGuardHome_yaml "${YAML_TARGET}"; then return 1 fi fi } agh_check() { /opt/etc/init.d/S99AdGuardHome check } agh_config_valid() { [ -x "${AGH_FILE}" ] || return 1 [ -f "${YAML_FILE}" ] || return 1 "${AGH_FILE}" --check-config -c "${YAML_FILE}" --no-check-update -l /dev/null >/dev/null 2>&1 } agh_dns_bound() { local socket_table pidof AdGuardHome >/dev/null 2>&1 || return 2 socket_table="$(netstat -nlp 2>/dev/null)" || return 1 PTXT "${socket_table}" | awk ' $0 ~ /^(tcp)6?[[:space:]]+/ && $0 ~ /:53[[:space:]]/ { if ($0 ~ "/AdGuardHome([[:space:]]|$)") tcp_found = 1 } $0 ~ /^(udp)6?[[:space:]]+/ && $0 ~ /:53[[:space:]]/ { if ($0 ~ "/AdGuardHome([[:space:]]|$)") udp_found = 1 } END { exit(tcp_found && udp_found ? 0 : 1) } ' } agh_web_port() { local conf_port yaml_port yaml_port="$(awk -F: '/^[[:space:]]*address:[[:space:]]*/ { print $NF; exit }' "${YAML_FILE}" 2>/dev/null | sed 's/[^0-9].*$//')" if runtime_port_is_valid "${yaml_port}"; then PTXT "${yaml_port}" return 0 fi conf_port="$(awk -F= '/^ADGUARD_WEBUI_PORT=/ { print $2; exit }' "${CONF_FILE}" 2>/dev/null | sed -e 's/^"//' -e 's/"$//')" if port_is_valid "${conf_port}"; then PTXT "${conf_port}" return 0 fi return 1 } agh_web_bound() { local web_port web_port="$(agh_web_port)" || return 1 web_port_owned_by_agh "${web_port}" } agh_log_start_failure() { ADGUARDHOME_START_FAILURE_LOGGED=1 if ! agh_is_running; then PTXT "${ERROR} AdGuardHome startup failed: process is not running." return 0 fi if ! agh_dns_bound; then PTXT "${ERROR} AdGuardHome startup failed: process is running but DNS is not bound." return 0 fi if ! agh_web_bound; then PTXT "${ERROR} AdGuardHome startup failed: WebUI port is unavailable." return 0 fi if ! agh_config_valid; then PTXT "${ERROR} AdGuardHome startup failed: configuration check failed." return 0 fi ADGUARDHOME_START_FAILURE_LOGGED= return 1 } agh_startup_check() { agh_is_running || return 1 agh_dns_bound || return 1 agh_web_bound || return 1 agh_config_valid || return 1 return 0 } agh_startup_ready() { local elapsed maxwait elapsed="0" maxwait="${ADGUARDHOME_READY_TIMEOUT}" while ! agh_startup_check; do if ! agh_is_running; then agh_log_start_failure return 1 fi if [ "${elapsed}" -ge "${maxwait}" ]; then agh_log_start_failure return 1 fi sleep 1s elapsed="$((elapsed + 1))" done return 0 } agh_complete_startup() { ptxt_phase "Starting AdGuardHome service." if ! agh_stop; then ptxt_fail "Unable to establish a stopped service state before startup." return 1 fi ADGUARDHOME_START_FAILURE_LOGGED= if ! agh_start; then [ "${ADGUARDHOME_START_FAILURE_LOGGED:-}" = "1" ] || agh_start_error return 1 fi ptxt_ok "AdGuardHome setup is complete." return 0 } agh_is_running() { [ -n "$(pidof AdGuardHome)" ] } # agh_start_transition_active returns 0 when at least one managed monitor is # detected, even if the daemon is absent. Returns nonzero for a zero, empty, or # invalid monitor count; daemon readiness is not checked. agh_start_transition_active() { local monitor_count monitor_count="$(agh_monitor_count 2>/dev/null)" case "${monitor_count}" in "" | *[!0-9]*) return 1 ;; esac [ "${monitor_count}" -ge "1" ] } agh_prepare_binary_replace() { if { [ -f "${AGH_FILE}" ] || [ -e "/opt/sbin/AdGuardHome" ]; } && [ "$(agh_process_count)" -ge "1" ]; then ptxt_step "Stopping AdGuardHome service before replacing ${AGH_FILE}." if ! agh_stop; then ptxt_fail "Unable to safely stop AdGuardHome service before replacing ${AGH_FILE}." return 1 fi fi return 0 } agh_monitor_count() { local count pid count="0" for pid in $(pidof AdGuardHome.sh S99AdGuardHome rc.func.AdGuardHome 2>/dev/null); do if awk '{ print }' "/proc/${pid}/cmdline" 2>/dev/null | grep -q 'monitor-start'; then count="$((count + 1))" fi done PTXT "${count}" } agh_process_count() { local daemon_count monitor_count daemon_count="$(pidof AdGuardHome 2>/dev/null | wc -w)" monitor_count="$(agh_monitor_count)" PTXT "$((daemon_count + monitor_count))" } doctor_status() { local next next="${3:-}" [ "$1" = "FAIL" ] && DOCTOR_FAILED="1" case "$1" in WARN | FAIL) [ -n "${next}" ] || next="Review the installer output and repair manually." PTXT "[$1] $2 | Next: ${next}" ;; *) PTXT "[$1] $2" ;; esac } doctor_fix_msg() { PTXT "[OK] fixed: $1" } doctor_file_state() { local path mode expected path="$1" expected="$2" if [ ! -e "${path}" ]; then doctor_status WARN "${path} missing" "Re-run install or restore ${path}." return 1 fi mode="$(ls -ld "${path}" 2>/dev/null | awk '{ print $1 }')" if [ -n "${expected}" ] && [ "${mode}" != "${expected}" ]; then doctor_status WARN "${path} mode ${mode}, expected ${expected}" "Run: sh installer doctor --fix" return 1 fi doctor_status OK "${path} present${mode:+ (${mode})}" return 0 } # doctor_managed_script_state diagnoses complete hook invariants and repairs only recognized owned content when requested. doctor_managed_script_state() { local operation script_path expected_line fix script_path="$1" operation="$2" fix="${3:-0}" expected_line="$(managed_hook_expected_line "${operation}")" || return 1 if [ -L "${script_path}" ] || { [ -e "${script_path}" ] && [ ! -f "${script_path}" ]; }; then doctor_status FAIL "${script_path} unsafe managed hook target" "Replace the symlink or special file manually; doctor will not follow it." return 1 fi if [ ! -f "${script_path}" ]; then doctor_status WARN "${script_path} managed hook missing" "Re-run install to recreate the managed hook." return 1 fi if ! managed_hook_state_valid "${script_path}" "${expected_line}"; then if [ "${fix}" = 1 ] && managed_hook_recognized "${script_path}" "${operation}"; then if [ "${operation}" = '@service-event-end' ]; then write_command_script "${script_path}" "${expected_line}" >/dev/null 2>&1 else write_manager_script "${script_path}" "${operation}" >/dev/null 2>&1 fi && managed_hook_state_valid "${script_path}" "${expected_line}" || { doctor_status FAIL "${script_path} managed hook repair failed" "Inspect the hook target and filesystem permissions, then rerun doctor --fix." return 1 } doctor_fix_msg "${script_path} managed hook header, invocation and permissions" else doctor_status WARN "${script_path} managed hook has an invalid header, permissions or AdGuardHome call count" "Run: sh installer doctor --fix; reinstall if the managed invocation is missing." return 1 fi fi doctor_status OK "${script_path} managed hook present with valid header, unique invocation and executable permissions" return 0 } # doctor_check_managed_hooks respects persisted DNS integration and current topology without changing either. doctor_check_managed_hooks() { local fix dnsmasq_mode fix="${1:-0}" doctor_managed_script_state /jffs/scripts/init-start 'init-start &' "${fix}" || true doctor_managed_script_state /jffs/scripts/services-stop 'services-stop &' "${fix}" || true doctor_managed_script_state /jffs/scripts/service-event-end '@service-event-end' "${fix}" || true dnsmasq_mode="$(conf_value ADGUARD_DNSMASQ_MODE 2>/dev/null)" if [ "${dnsmasq_mode}" != disabled ]; then if [ "${dnsmasq_mode}" = enabled ] || [ -e /jffs/scripts/dnsmasq.postconf ] || [ -L /jffs/scripts/dnsmasq.postconf ]; then doctor_managed_script_state /jffs/scripts/dnsmasq.postconf 'dnsmasq pre_start' "${fix}" || true fi case " $(installer_nvram get rc_support 2>/dev/null) " in *' mtlancfg '*) if [ "${dnsmasq_mode}" = enabled ] || [ -e /jffs/scripts/dnsmasq-sdn.postconf ] || [ -L /jffs/scripts/dnsmasq-sdn.postconf ]; then doctor_managed_script_state /jffs/scripts/dnsmasq-sdn.postconf 'dnsmasq-sdn $2' "${fix}" || true fi ;; esac fi if adguard_ipset_allowed; then doctor_managed_script_state /jffs/scripts/firewall-start 'firewall "$1"' "${fix}" || true fi } doctor_dns53_state() { local dns53 owners dns53="$(netstat -nlp 2>/dev/null | awk '$0 ~ /:53[[:space:]]/ { print; found = 1 } END { exit(found ? 0 : 1) }')" if agh_dns_bound; then doctor_status OK "DNS port 53 TCP and UDP are owned by AdGuardHome" return 0 fi if [ -n "${dns53}" ]; then owners="$(PTXT "${dns53}" | awk '{ print $NF }' | sort | uniq | tr '\n' ' ' | sed 's/[[:space:]]*$//')" doctor_status WARN "DNS port 53 TCP and UDP are not both owned by AdGuardHome${owners:+ (owners: ${owners})}" "Inspect DNS settings; do not use --fix for DNS changes." return 1 fi doctor_status FAIL "DNS port 53 is not listening" "Start AdGuardHome or restore dnsmasq DNS service manually." return 1 } # doctor_fix_permissions restores owned service/config modes; managed hooks use the separate invariant-aware repair. doctor_fix_permissions() { [ -d "${TARG_DIR}" ] && ensure_adguardhome_directory_permissions >/dev/null 2>&1 && doctor_fix_msg "AdGuardHome directory permissions" [ -f "/opt/etc/init.d/S99AdGuardHome" ] && chmod 755 "/opt/etc/init.d/S99AdGuardHome" 2>/dev/null && doctor_fix_msg "/opt/etc/init.d/S99AdGuardHome executable bit" [ -f "/opt/etc/init.d/rc.func.AdGuardHome" ] && chmod 644 "/opt/etc/init.d/rc.func.AdGuardHome" 2>/dev/null && doctor_fix_msg "/opt/etc/init.d/rc.func.AdGuardHome permissions" [ -f "${ADDON_DIR}/AdGuardHome.sh" ] && chmod 755 "${ADDON_DIR}/AdGuardHome.sh" 2>/dev/null && doctor_fix_msg "${ADDON_DIR}/AdGuardHome.sh executable bit" [ -f "${YAML_FILE}" ] && chmod 600 "${YAML_FILE}" 2>/dev/null && doctor_fix_msg "${YAML_FILE} permissions" [ -f "${CONF_FILE}" ] && chmod 600 "${CONF_FILE}" 2>/dev/null && doctor_fix_msg "${CONF_FILE} permissions" } doctor_pid_file_is_active() { local marker_pid marker_pid="$(sed -n '1s/[[:space:]].*$//p' "$1" 2>/dev/null)" case "${marker_pid}" in "" | *[!0-9]*) return 1 ;; esac [ -d "/proc/${marker_pid}" ] } # doctor_run_lock_is_active preserves held or unverified current/legacy service state using read-only descriptor probes. doctor_run_lock_is_active() { local lock_file pid_file status private_dir owner private_dir="/tmp/AdGuardHome-service-lock" if [ -e "${private_dir}" ] || [ -L "${private_dir}" ]; then [ -d "${private_dir}" ] && [ ! -L "${private_dir}" ] || return 0 owner="$(awk '/^Uid:/ { print $3; exit }' "/proc/$$/status" 2>/dev/null)" || return 0 case "${owner}" in "" | *[!0-9]*) return 0 ;; esac ls -ldn "${private_dir}" 2>/dev/null | awk -v owner="${owner}" '$1 == "drwx------" && $3 == owner { valid = 1 } END { exit valid ? 0 : 1 }' || return 0 # Action and ownership-transition markers are busy or unverified. Doctor never reaps them. for status in "${private_dir}/action" "${private_dir}/action.claim" "${private_dir}/action.transition"; do if [ -e "${status}" ] || [ -L "${status}" ]; then return 0 fi done lock_file="${private_dir}/flock" if [ -e "${lock_file}" ] || [ -L "${lock_file}" ]; then [ -f "${lock_file}" ] && [ ! -L "${lock_file}" ] || return 0 ls -ln "${lock_file}" 2>/dev/null | awk -v owner="${owner}" '$1 == "-rw-------" && $2 == 1 && $3 == owner { valid = 1 } END { exit valid ? 0 : 1 }' || return 0 ai_have_cmd flock || return 0 ( exec 9<"${lock_file}" || exit 1 flock -n 9 >/dev/null 2>&1 status="$?" [ "${status}" -eq 0 ] && flock -u 9 >/dev/null 2>&1 exit "${status}" ) [ "$?" -eq 0 ] || return 0 fi fi lock_file="/tmp/AdGuardHome.lock" pid_file="/tmp/AdGuardHome/pid" [ ! -L /tmp/AdGuardHome ] && [ ! -L "${pid_file}" ] && [ ! -L "${lock_file}" ] || return 0 if [ -e /tmp/AdGuardHome ] || [ -e "${lock_file}" ]; then owner="$(awk '/^Uid:/ { print $3; exit }' "/proc/$$/status" 2>/dev/null)" || return 0 case "${owner}" in "" | *[!0-9]*) return 0 ;; esac if [ -e /tmp/AdGuardHome ]; then [ -d /tmp/AdGuardHome ] || return 0 ls -ldn /tmp/AdGuardHome 2>/dev/null | awk -v owner="${owner}" '($1 == "drwx------" || $1 == "drwxr-xr-x") && $3 == owner { valid = 1 } END { exit valid ? 0 : 1 }' || return 0 # A legacy mkdir owner may not have published its PID yet. [ -e "${pid_file}" ] || [ -e "${lock_file}" ] || return 0 fi for status in "${pid_file}" "${lock_file}"; do [ -e "${status}" ] || continue [ -f "${status}" ] || return 0 ls -ln "${status}" 2>/dev/null | awk -v owner="${owner}" '($1 == "-rw-------" || $1 == "-rw-r--r--") && $2 == 1 && $3 == owner { valid = 1 } END { exit valid ? 0 : 1 }' || return 0 done fi if [ -f "${pid_file}" ] && doctor_pid_file_is_active "${pid_file}"; then return 0 fi if ai_have_cmd flock && [ -e "${lock_file}" ]; then [ -f "${lock_file}" ] || return 0 ( exec 9<"${lock_file}" || exit 1 flock -n 9 >/dev/null 2>&1 status="$?" [ "${status}" -eq 0 ] && flock -u 9 >/dev/null 2>&1 exit "${status}" ) status="$?" [ "${status}" -ne 0 ] && return 0 fi return 1 } # doctor_fix_safe repairs supported file permissions and links while preserving active locks and recovery evidence. doctor_fix_safe() { local link_target stale doctor_fix_permissions link_target="$(readlink "/opt/sbin/AdGuardHome" 2>/dev/null)" if [ -f "${AGH_FILE}" ] && { [ ! -L "/opt/sbin/AdGuardHome" ] || [ "${link_target}" != "${AGH_FILE}" ]; }; then mkdir -p "/opt/sbin" 2>/dev/null && ln -sf "${AGH_FILE}" "/opt/sbin/AdGuardHome" 2>/dev/null && doctor_fix_msg "/opt/sbin/AdGuardHome symlink" fi # Historical descriptor locks retain their inode across old holders, waiters and new-version probes. for stale in /tmp/AdGuardHome.dnsmasq.handoff /tmp/AdGuardHome.dnsmasq.lock /tmp/AdGuardHome.dns-handoff/active /tmp/AdGuardHome.dns-handoff/lock /tmp/AdGuardHome /tmp/AdGuardHome.pid /opt/var/run/AdGuardHome.pid /tmp/AdGuardHome.*.tmp "${TARG_DIR}"/.*.$$.* "${TARG_DIR}"/*.tmp.$$; do [ -e "${stale}" ] || continue case "${stale}" in /tmp/AdGuardHome.dnsmasq.handoff | /tmp/AdGuardHome.dnsmasq.lock | /tmp/AdGuardHome.dns-handoff/active | /tmp/AdGuardHome.dns-handoff/lock) doctor_pid_file_is_active "${stale}" && continue [ "$(agh_monitor_count)" -eq 0 ] || continue ;; /tmp/AdGuardHome) doctor_run_lock_is_active && continue ;; /opt/var/run/AdGuardHome.pid | /tmp/AdGuardHome.pid) doctor_pid_file_is_active "${stale}" && continue ;; /tmp/AdGuardHome/pid) doctor_run_lock_is_active && continue ;; /tmp/AdGuardHome.*.tmp) [ -f "${stale}" ] || continue ;; esac rm -rf "${stale}" 2>/dev/null && doctor_fix_msg "removed stale ${stale}" done } doctor_show_nvram_dns() { local key value if ! ai_have_cmd nvram; then doctor_status WARN "nvram command unavailable; DNS NVRAM values not checked" "Run doctor on the router where nvram is available." return 0 fi for key in dnspriv_enable dns_local_cache dhcpd_dns_router dhcp_dns1_x dhcp_dns2_x dnsfilter_enable_x dnsfilter_mode dnsfilter_custom1 dnsfilter_custom2 dnsfilter_custom3 lan_dnsenable_x lan_dns1_x lan_dns2_x wan_dnsenable_x wan_dns1_x wan_dns2_x; do value="$(installer_nvram get "${key}" 2>/dev/null)" case "${key}:${value}" in dnsfilter_enable_x:0 | dnsfilter_enable_x:) doctor_status OK "nvram ${key}=${value}" ;; dnsfilter_enable_x:*) doctor_status WARN "nvram ${key}=${value}; DNSFilter may redirect client DNS" "Review DNSFilter settings manually; --fix will not change NVRAM." ;; *) doctor_status OK "nvram ${key}=${value}" ;; esac done } # doctor reports installation and runtime health, with explicit safe repairs gated on Entware availability. doctor() { local count fix ipset_file link_target marker port version branch backup_file entware_ready DOCTOR_FAILED="0" fix="0" case "${1:-}" in --fix) fix="1" ;; "") ;; *) doctor_status FAIL "usage: sh installer doctor [--fix]" "Run: sh installer doctor [--fix]" return 1 ;; esac entware_ready="0" if entware_available; then entware_ready="1" doctor_status OK "Entware is available" else doctor_status FAIL "Entware is not available" "Mount or install Entware, then rerun doctor." fi if [ "${fix}" = "1" ]; then if [ "${entware_ready}" = "1" ]; then doctor_fix_safe else doctor_status WARN "skipping --fix repairs because Entware is not available" "Mount or install Entware before using --fix." fi fi doctor_file_state "${TARG_DIR}" "drwx------" || true if [ -f "${ROLLBACK_RESULT_FILE}" ]; then if rollback_result_needs_attention; then doctor_status FAIL "last rollback result: $(rollback_result_summary)" "Review the rollback detail, restore manually if needed, then rerun doctor." else doctor_status WARN "last rollback result: $(rollback_result_summary)" "Confirm the restored state, then remove ${ROLLBACK_RESULT_FILE} if no action is needed." fi else doctor_status OK "last rollback result: none" fi link_target="$(readlink "/opt/sbin/AdGuardHome" 2>/dev/null)" [ -L "/opt/sbin/AdGuardHome" ] && [ "${link_target}" = "${AGH_FILE}" ] && doctor_status OK "/opt/sbin/AdGuardHome -> ${link_target}" || doctor_status FAIL "/opt/sbin/AdGuardHome target is ${link_target:-missing}" "Run: sh installer doctor --fix" doctor_file_state "${ADDON_DIR}/AdGuardHome.sh" "-rwxr-xr-x" || true if [ "${entware_ready}" = 1 ]; then doctor_check_managed_hooks "${fix}" else doctor_check_managed_hooks 0 fi doctor_file_state "/opt/etc/init.d/S99AdGuardHome" "-rwxr-xr-x" || true doctor_file_state "/opt/etc/init.d/rc.func.AdGuardHome" "-rw-r--r--" || true doctor_file_state "${YAML_FILE}" "-rw-------" || true doctor_file_state "${CONF_FILE}" "-rw-------" || true doctor_dns53_state || true for marker in /tmp/AdGuardHome.dnsmasq.handoff /tmp/AdGuardHome.dnsmasq.lock /tmp/AdGuardHome.dns-handoff/active /tmp/AdGuardHome.dns-handoff/lock /tmp/AdGuardHome/pid; do { [ -e "${marker}" ] || [ -L "${marker}" ]; } && doctor_status WARN "dnsmasq handoff marker/lock exists: ${marker}" "Run: sh installer doctor --fix; if still present, check active installer/service processes." || doctor_status OK "dnsmasq handoff marker/lock absent: ${marker}" done marker="/tmp/AdGuardHome.lock" if [ ! -e "${marker}" ] && [ ! -L "${marker}" ]; then doctor_status OK "historical service descriptor lock absent: ${marker}" elif ai_have_cmd flock && ! doctor_run_lock_is_active; then doctor_status OK "historical service descriptor lock retained intentionally: ${marker}; no active service lock detected" else doctor_status WARN "historical service descriptor lock retained intentionally: ${marker}; service lock activity is active or unverified" "Check active installer/service processes. Do not remove this inode; --fix preserves it for holders and waiters. A router reboot clears historical /tmp lock state." fi count="$(agh_monitor_count)" [ "${count}" -eq 1 ] && doctor_status OK "monitor process count: ${count}" || doctor_status WARN "monitor process count: ${count}" "Restart the AdGuardHome service if the monitor count is unexpected." count="$(pidof AdGuardHome 2>/dev/null | wc -w)" [ "${count}" -eq 1 ] && doctor_status OK "AdGuardHome daemon process count: ${count}" || doctor_status WARN "AdGuardHome daemon process count: ${count}" "Start or restart AdGuardHome manually; --fix will not restart services." port="$(agh_web_port 2>/dev/null)" if [ -n "${port}" ]; then web_port_owned_by_agh "${port}" && doctor_status OK "WebUI port ${port} owned by AdGuardHome" || doctor_status WARN "WebUI port ${port} not owned by AdGuardHome" "Inspect the WebUI port owner and AdGuardHome config manually." else doctor_status WARN "WebUI port not configured" "Set ADGUARD_WEBUI_PORT or repair AdGuardHome.yaml manually." fi branch="$(conf_value INSTALLER_BRANCH 2>/dev/null)" version="$([ -x "${AGH_FILE}" ] && "${AGH_FILE}" --version 2>/dev/null | head -1)" doctor_status OK "installer version ${AI_VERSION}, branch ${branch:-unknown}" doctor_status OK "AdGuardHome version ${version:-unknown}" backup_file="${BASE_DIR}/backup_AdGuardHome.tar.gz" if [ -f "${backup_file}" ]; then adguard_archive_is_safe "${backup_file}" 1 && doctor_status OK "backup archive safety passed" || doctor_status FAIL "backup archive safety failed" "Remove or replace the unsafe backup archive manually." else doctor_status WARN "backup archive missing" "Create a fresh backup after confirming the installation is healthy." fi for ipset_file in "${TARG_DIR}/ipset.conf" "${TARG_DIR}/ipset.user" "$(adguardhome_yaml_ipset_file "${YAML_FILE}" 2>/dev/null)"; do [ -n "${ipset_file}" ] || continue case "${ipset_file}" in /*) ;; *) ipset_file="${TARG_DIR}/${ipset_file}" ;; esac [ -f "${ipset_file}" ] && doctor_status OK "IPSET file present: ${ipset_file}" || doctor_status WARN "IPSET file missing: ${ipset_file}" "Recreate the IPSET file or update AdGuardHome.yaml manually." done doctor_show_nvram_dns [ "${DOCTOR_FAILED}" = "0" ] } status_line() { PTXT "$1: ${2:-unknown}" } status_dnsmasq_handoff_state() { local marker markers state state="inactive" markers="" for marker in /tmp/AdGuardHome.dnsmasq.handoff /tmp/AdGuardHome.dnsmasq.lock /tmp/AdGuardHome.dns-handoff/active /tmp/AdGuardHome.dns-handoff/lock; do if [ -e "${marker}" ] || [ -L "${marker}" ]; then state="active/stale marker present" markers="${markers}${markers:+, }${marker}" fi done PTXT "${state}${markers:+ (${markers})}" } status_last_startup_result() { local result result="" if ai_have_cmd logread; then result="$(logread 2>/dev/null | awk ' /AdGuardHome/ && /AdGuardHome startup completed/ { last = $0 } /AdGuardHome/ && /AdGuardHome startup failed/ { last = $0 } /AdGuardHome/ && /DNS\/WebUI readiness checks (passed|failed)/ { last = $0 } END { if (last != "") print last } ')" fi [ -n "${result}" ] && PTXT "${result}" || PTXT "unknown (no startup marker/log entry found)" } status_port53_ownership() { local dns53 dns53="$(netstat -nlp 2>/dev/null | awk '$0 ~ /:53[[:space:]]/ { print }')" if [ -z "${dns53}" ]; then PTXT "not listening" return 0 fi PTXT "${dns53}" | awk ' function owner(line, i, field) { for (i = NF; i >= 1; i--) { field = $i if (field ~ /^[0-9]+\/[^[:space:]]+$/) return field } return "unknown" } $0 ~ /^(tcp)6?[[:space:]]+/ { tcp = tcp ? tcp ", " owner($0) : owner($0) } $0 ~ /^(udp)6?[[:space:]]+/ { udp = udp ? udp ", " owner($0) : owner($0) } END { if (tcp != "" && udp != "") print "TCP " tcp "; UDP " udp else if (tcp != "") print "TCP " tcp else if (udp != "") print "UDP " udp else print "unknown" } ' } status_webui_address() { local address host port address="$(awk ' /^[[:space:]]*address:[[:space:]]*/ { sub(/^[[:space:]]*address:[[:space:]]*/, "") gsub(/^"|"$/, "") print exit } ' "${YAML_FILE}" 2>/dev/null)" port="$(agh_web_port 2>/dev/null)" if [ -n "${address}" ]; then PTXT "${address}${port:+ (port ${port})}" return 0 fi host="$(installer_nvram get lan_ipaddr 2>/dev/null)" [ -n "${port}" ] && PTXT "${host:-router}:${port}" || PTXT "unknown" } status() { local branch count monitor_count monitor_state service_state version count="$(pidof AdGuardHome 2>/dev/null | wc -w)" monitor_count="$(agh_monitor_count)" if [ "${count}" -gt 0 ]; then if agh_startup_check; then service_state="running (ready)" else service_state="running (readiness check incomplete)" fi else service_state="stopped" fi case "${monitor_count}" in 0) monitor_state="stopped" ;; 1) monitor_state="running (1 process)" ;; *) monitor_state="running (${monitor_count} processes)" ;; esac branch="${1:-}" [ -n "${branch}" ] || branch="$(conf_value INSTALLER_BRANCH 2>/dev/null)" version="$([ -x "${AGH_FILE}" ] && "${AGH_FILE}" --version 2>/dev/null | head -1)" PTXT "AdGuardHome Installer Status" status_line "AdGuardHome service state" "${service_state}" status_line "Monitor process state" "${monitor_state}" status_line "AdGuardHome PID count" "${count}" status_line "Port 53 ownership" "$(status_port53_ownership)" status_line "AdGuardHome version" "${version:-unknown}" status_line "Installer version" "${AI_VERSION}" status_line "Selected branch" "${branch:-unknown}" status_line "WebUI address/port" "$(status_webui_address)" status_line "dnsmasq handoff state" "$(status_dnsmasq_handoff_state)" status_line "Last startup result" "$(status_last_startup_result)" status_line "Last rollback result" "$(rollback_result_summary 2>/dev/null || PTXT "none")" } adguard_service_without_nvram_lock_fd() { ( exec 8>&- "$@" ) } # agh_restart_transition_complete confirms that a restart replaced the daemon, # rather than accepting a still-running process left by an asynchronous request. agh_restart_transition_complete() { local current_pids old_pids old_pids="${1:-}" current_pids="$(pidof AdGuardHome 2>/dev/null)" [ -n "${current_pids}" ] || return 1 [ -z "${old_pids}" ] && return 0 # Word splitting is intentional: pidof returns a whitespace-separated PID list. # shellcheck disable=SC2086 set -- ${current_pids} adguard_pid_list_has_new_pid "${old_pids}" "$@" } # agh_request_start requests firmware startup and falls back to the init script # if the request fails or no daemon appears. After the initial firmware poll, # an active managed monitor gets an additional ADGUARDHOME_WAIT_TIMEOUT seconds # to launch the daemon before fallback. A successful command alone is not proof # that the asynchronous router service action completed. The x argument keeps # direct init actions from dispatching another firmware event. # Returns 0 when the daemon is observed, or 1 if direct init fails or its # ADGUARDHOME_WAIT_TIMEOUT-second poll expires. DNS/WebUI readiness is checked # separately by the caller. agh_request_start() { if adguard_service_without_nvram_lock_fd service start_AdGuardHome >/dev/null 2>&1; then if adguard_service_status_after_action start; then return 0 fi agh_is_running && return 0 if agh_start_transition_active; then ptxt_step "Firmware service start is still in progress; waiting for the managed monitor to launch the daemon." if agh_wait_started "${ADGUARDHOME_WAIT_TIMEOUT}"; then return 0 fi agh_is_running && return 0 fi ptxt_step "Firmware service start did not create the daemon; trying the init script directly." fi adguard_service_without_nvram_lock_fd /opt/etc/init.d/S99AdGuardHome start x || return 1 adguard_service_status_after_action start "" "${ADGUARDHOME_WAIT_TIMEOUT}" fallback } # agh_request_restart requests a firmware restart and verifies daemon replacement. # $1 is the optional whitespace-separated pre-restart PID list; when empty, # observing any running AdGuardHome daemon is sufficient for completion. # After the initial firmware grace period, an active managed monitor gets an # additional ADGUARDHOME_WAIT_TIMEOUT seconds to finish its sleep and DNS handoff. # Recheck completion after each wait before falling back to direct init with x, # which prevents dispatching another firmware event. Return 0 on completion, # or 1 if direct init fails or its ADGUARDHOME_WAIT_TIMEOUT-second poll expires. # DNS/WebUI readiness is checked separately by the caller. agh_request_restart() { local old_pids old_pids="${1:-}" if adguard_service_without_nvram_lock_fd service restart_AdGuardHome >/dev/null 2>&1; then if adguard_service_status_after_action restart "${old_pids}"; then return 0 fi agh_restart_transition_complete "${old_pids}" && return 0 if agh_start_transition_active; then ptxt_step "Firmware service restart is still in progress; waiting for the managed monitor to replace the daemon." if adguard_service_status_after_action restart "${old_pids}" "${ADGUARDHOME_WAIT_TIMEOUT}"; then return 0 fi agh_restart_transition_complete "${old_pids}" && return 0 fi ptxt_step "Firmware service restart did not replace the daemon; trying the init script directly." fi adguard_service_without_nvram_lock_fd /opt/etc/init.d/S99AdGuardHome restart x || return 1 adguard_service_status_after_action restart "${old_pids}" "${ADGUARDHOME_WAIT_TIMEOUT}" fallback } # agh_request_stop escalates from the asynchronous firmware event to the init # script and its kill action when a successful command leaves the daemon or its # monitor alive. Each transition is verified before the next action runs. agh_request_stop() { if adguard_service_without_nvram_lock_fd service stop_AdGuardHome >/dev/null 2>&1; then if adguard_service_status_after_action stop; then return 0 fi [ "$(agh_process_count)" -eq 0 ] && return 0 ptxt_step "Firmware service stop did not finish; trying the init script directly." fi if adguard_service_without_nvram_lock_fd /opt/etc/init.d/S99AdGuardHome stop x; then if adguard_service_status_after_action stop "" "${ADGUARDHOME_WAIT_TIMEOUT}" fallback; then return 0 fi [ "$(agh_process_count)" -eq 0 ] && return 0 fi if adguard_service_without_nvram_lock_fd /opt/etc/init.d/S99AdGuardHome kill x; then if adguard_service_status_after_action kill "" "${ADGUARDHOME_WAIT_TIMEOUT}" fallback; then return 0 fi [ "$(agh_process_count)" -eq 0 ] && return 0 fi if ai_have_cmd killall; then killall -q -9 AdGuardHome 2>/dev/null rm -rf /opt/var/run/AdGuardHome.pid 2>/dev/null adguard_service_status_after_action kill "" "${ADGUARDHOME_WAIT_TIMEOUT}" fallback || true else PTXT "${ERROR} Unable to force-stop AdGuardHome because killall is unavailable." fi [ "$(agh_process_count)" -eq 0 ] } agh_restart() { local old_pids if agh_is_running; then ptxt_step "Restarting AdGuardHome service." old_pids="$(pidof AdGuardHome 2>/dev/null)" if ! agh_request_restart "${old_pids}"; then ptxt_fail "AdGuardHome service restart command failed." return 1 fi else agh_start || return 1 fi sleep 1s if agh_startup_ready; then ptxt_ok "AdGuardHome service restarted." return 0 fi ptxt_fail "AdGuardHome service did not become ready after restart." return 1 } agh_start() { if ! agh_is_running; then ptxt_step "Starting AdGuardHome service." if ! agh_request_start; then ptxt_fail "AdGuardHome service start command failed." return 1 fi fi sleep 1s if ! agh_wait_started "${ADGUARDHOME_WAIT_TIMEOUT}"; then agh_log_start_failure || PTXT "${ERROR} AdGuardHome startup failed: process is not running." return 1 fi sleep 1s if agh_startup_ready; then ptxt_ok "AdGuardHome service is running." return 0 fi ptxt_fail "AdGuardHome service did not become ready after start." return 1 } agh_start_error() { PTXT "${ERROR} Couldn't start AdGuardHome!" \ "${ERROR} Please send WebUI System Log to dev." } # agh_stop stops both the daemon and its monitor, distinguishes a lingering # monitor from a live daemon, and verifies that neither remains before return. agh_stop() { local daemon_count monitor_count daemon_count="$(pidof AdGuardHome 2>/dev/null | wc -w)" monitor_count="$(agh_monitor_count)" if [ "${daemon_count}" -ge 1 ] || [ "${monitor_count}" -ge 1 ]; then if [ "${daemon_count}" -ge 1 ]; then ptxt_step "Stopping AdGuardHome service." else ptxt_step "Stopping the lingering AdGuardHome monitor after the daemon exited." fi agh_request_stop || true fi sleep 1s if ! agh_wait_stopped "${ADGUARDHOME_WAIT_TIMEOUT}"; then ptxt_fail "Timed out waiting for AdGuardHome service to stop." return 1 fi sleep 1s agh_check ptxt_ok "AdGuardHome service is stopped." return 0 } agh_wait_ready() { local elapsed maxwait now remaining start maxwait="${1:-${ADGUARDHOME_READY_TIMEOUT}}" start="$(monotonic_seconds)" || return 1 while :; do now="$(monotonic_seconds)" || return 1 elapsed="$((now - start))" if [ "${elapsed}" -ge "${maxwait}" ]; then return 1; fi remaining="$((maxwait - elapsed))" if check_connection "${remaining}" && agh_is_running; then return 0; fi now="$(monotonic_seconds)" || return 1 elapsed="$((now - start))" if [ "${elapsed}" -ge "${maxwait}" ]; then return 1; fi sleep 1s done } agh_wait_started() { local elapsed maxwait elapsed="0" maxwait="${1:-${ADGUARDHOME_WAIT_TIMEOUT}}" while ! agh_is_running; do if [ "${elapsed}" -ge "${maxwait}" ]; then return 1; fi sleep 1s elapsed="$((elapsed + 1))" done return 0 } agh_wait_stopped() { local elapsed maxwait elapsed="0" maxwait="${1:-${ADGUARDHOME_WAIT_TIMEOUT}}" while [ "$(agh_process_count)" -ge "1" ]; do if [ "${elapsed}" -ge "${maxwait}" ]; then return 1; fi sleep 1s elapsed="$((elapsed + 1))" done return 0 } monotonic_seconds() { local uptime if read -r uptime _ /dev/null 2>&1; then PTXT -n "${INPUT} Press Enter to continue: " (IFS= read -r -t 30 pause_input) 2>/dev/null || : fi } blocklist_analyzer_ids() { awk ' /^[[:space:]]*UNUSED BLOCKLISTS[[:space:]]*\(/ { in_unused = 1; next } in_unused && /^[-=][-=]*$/ { next } in_unused && /^$/ { exit } in_unused { line = $0 sub(/^[[:space:]]*/, "", line) if (line ~ /^[0-9]+\.txt([[:space:]]|$)/) { sub(/\.txt.*/, "", line) print line } } ' "$1" } ensure_blocklist_analyzer_dependencies() { ptxt_step "Checking for Entware python3." if [ ! -x /opt/bin/python3 ]; then ptxt_warn "python3 is required to run the analyzer, but it was not found." PTXT "${INFO} Entware python3 can be installed with: opkg install python3 --force-depends --force-overwrite --force-reinstall" if read_yesno "Do you want Entware to install python3 now?"; then ptxt_step "Installing Entware python3 package." if ! ensure_opkg_package python3 --force-depends --force-overwrite --force-reinstall || [ ! -x /opt/bin/python3 ]; then ptxt_fail "Unable to install or find Entware python3." return 1 fi ptxt_ok "Entware python3 is installed." else ptxt_fail "python3 is required to run the blocklist analyzer." return 1 fi fi ptxt_ok "Entware python3 is available." ptxt_step "Checking for SHA-256 checksum support." ensure_sha256sum_tool || return 1 ptxt_ok "SHA-256 checksum support is available." } install_blocklist_analyzer() { ptxt_phase "Preparing AdGuardHome blocklist analyzer." ptxt_step "Checking AdGuardHome target directory ${TARG_DIR}." if [ ! -d "${TARG_DIR}" ] || [ ! -d "${TARG_DIR}/data" ]; then ptxt_fail "${TARG_DIR} and ${TARG_DIR}/data are required before blocklists can be analyzed." return 1 fi ptxt_ok "AdGuardHome data directory exists." ensure_blocklist_analyzer_dependencies || return 1 if [ -z "${BLOCKLIST_ANALYZER_SHA256}" ]; then ptxt_fail "Blocklist analyzer SHA-256 is not pinned; refusing to download or run mutable remote code." return 1 fi ptxt_step "Downloading blocklist analyzer to ${BLOCKLIST_ANALYZER_FILE}." rm -f "${BLOCKLIST_ANALYZER_FILE}.tmp" if ! http_get_file "${BLOCKLIST_ANALYZER_URL}" "${BLOCKLIST_ANALYZER_FILE}.tmp"; then rm -f "${BLOCKLIST_ANALYZER_FILE}.tmp" ptxt_fail "Unable to download blocklist analyzer." return 1 fi ptxt_ok "Downloaded blocklist analyzer." ptxt_step "Verifying blocklist analyzer SHA-256 checksum." if ! ensure_sha256sum_tool; then rm -f "${BLOCKLIST_ANALYZER_FILE}.tmp" return 1 fi BLOCKLIST_ANALYZER_CALCULATED_SHA256="$(file_sha256 "${BLOCKLIST_ANALYZER_FILE}.tmp" 2>/dev/null)" || { rm -f "${BLOCKLIST_ANALYZER_FILE}.tmp" ptxt_fail "Unable to calculate the blocklist analyzer SHA-256 checksum." return 1 } if [ "${BLOCKLIST_ANALYZER_CALCULATED_SHA256}" != "${BLOCKLIST_ANALYZER_SHA256}" ]; then rm -f "${BLOCKLIST_ANALYZER_FILE}.tmp" ptxt_fail "Blocklist analyzer checksum verification failed." return 1 fi ptxt_ok "Blocklist analyzer checksum verified." ptxt_step "Installing blocklist analyzer with restrictive permissions." if ! chmod 700 "${BLOCKLIST_ANALYZER_FILE}.tmp" || ! mv "${BLOCKLIST_ANALYZER_FILE}.tmp" "${BLOCKLIST_ANALYZER_FILE}"; then rm -f "${BLOCKLIST_ANALYZER_FILE}.tmp" ptxt_fail "Unable to install blocklist analyzer." return 1 fi ptxt_ok "Blocklist analyzer is ready." } run_blocklist_analyzer() { local filter_file found_filter output_file ids_file python_cmd output_file="${TARG_DIR}/blocklist_analyzer.$$.out" ids_file="${TARG_DIR}/blocklist_analyzer.$$.ids" ptxt_phase "Running AdGuardHome blocklist analyzer." ptxt_step "Checking analyzer input files." if [ ! -d "${TARG_DIR}/data/filters" ]; then ptxt_fail "AdGuardHome filter files are missing; expected ${TARG_DIR}/data/filters." return 1 fi found_filter="" for filter_file in "${TARG_DIR}/data/filters"/*.txt; do if [ -f "${filter_file}" ]; then found_filter="1" break fi done if [ -z "${found_filter}" ]; then ptxt_fail "AdGuardHome filter files are missing; ${TARG_DIR}/data/filters contains no filter files." return 1 fi if [ ! -s "${TARG_DIR}/data/querylog.json" ]; then ptxt_fail "AdGuardHome query log is missing or empty; enable query logging and let AdGuardHome collect queries before analyzing blocklists." return 1 fi ptxt_ok "Analyzer input files are available." python_cmd="/opt/bin/python3" if [ ! -x "${python_cmd}" ]; then ptxt_fail "Entware python3 is required to run the blocklist analyzer." return 1 fi ptxt_step "Running analyzer from ${TARG_DIR} so ./data resolves correctly." if ! (cd "${TARG_DIR}" && "${python_cmd}" "${BLOCKLIST_ANALYZER_FILE}") >"${output_file}" 2>&1; then cat "${output_file}" rm -f "${output_file}" "${ids_file}" ptxt_fail "Blocklist analyzer failed." return 1 fi ptxt_ok "Blocklist analyzer completed." PTXT "" "${INFO} Analyzer output:" "----------------------------------------------------" cat "${output_file}" PTXT "----------------------------------------------------" blocklist_analyzer_ids "${output_file}" >"${ids_file}" if [ ! -s "${ids_file}" ]; then rm -f "${output_file}" "${ids_file}" ptxt_ok "No unused blocklists were detected." return 2 fi PTXT "${INFO} Unused blocklist IDs detected:" cat "${ids_file}" BLOCKLIST_ANALYZER_IDS_FILE="${ids_file}" BLOCKLIST_ANALYZER_OUTPUT_FILE="${output_file}" return 0 } blocklist_yaml_candidates() { local ids_file yaml_target ids_file="$1" yaml_target="${2:-${YAML_FILE}}" awk -v idsfile="${ids_file}" ' BEGIN { while ((getline wanted < idsfile) > 0) ids[wanted] = 1; close(idsfile); in_filters = 0; in_entry = 0 } function emit_entry() { if (in_entry && id != "" && ids[id]) print id "|" name "|" url id = ""; name = ""; url = ""; in_entry = 0 } /^[^[:space:]][^:]*:/ { if (in_filters && $0 !~ /^filters:/) { emit_entry(); in_filters = 0 } } /^filters:[[:space:]]*$/ { emit_entry(); in_filters = 1; next } in_filters && /^[[:space:]]*-[[:space:]]*enabled:/ { emit_entry(); in_entry = 1; next } in_filters && in_entry { if ($0 ~ /^[[:space:]]*id:[[:space:]]*[0-9]+[[:space:]]*$/) { id = $0; sub(/.*id:[[:space:]]*/, "", id); sub(/[[:space:]]*$/, "", id) } if ($0 ~ /^[[:space:]]*name:[[:space:]]*/) { name = $0; sub(/^[[:space:]]*name:[[:space:]]*/, "", name) } if ($0 ~ /^[[:space:]]*url:[[:space:]]*/) { url = $0; sub(/^[[:space:]]*url:[[:space:]]*/, "", url) } } END { emit_entry() } ' "${yaml_target}" } # select_unused_blocklists_for_removal reads analyzer IDs from $1 and prompts # only for entries configured in YAML_FILE, reporting stale cache IDs separately. # Registers temporary paths for exit cleanup and leaves confirmed IDs in # BLOCKLIST_ANALYZER_SELECTED_IDS_FILE. Returns 0 for a selection, 3 when no # configured candidates exist, 1 on file errors or an empty selection, or the # prompt's non-yes/no status when input is interrupted. select_unused_blocklists_for_removal() { local ids_file candidates_file configured_ids_file orphaned_ids_file selected_file id name url list_label choice_status ids_file="$1" candidates_file="${TARG_DIR}/blocklist_analyzer.$$.candidates" configured_ids_file="${TARG_DIR}/blocklist_analyzer.$$.configured.ids" orphaned_ids_file="${TARG_DIR}/blocklist_analyzer.$$.orphaned.ids" selected_file="${TARG_DIR}/blocklist_analyzer.$$.selected" BLOCKLIST_ANALYZER_CANDIDATES_FILE="${candidates_file}" BLOCKLIST_ANALYZER_CONFIGURED_IDS_FILE="${configured_ids_file}" BLOCKLIST_ANALYZER_ORPHANED_IDS_FILE="${orphaned_ids_file}" BLOCKLIST_ANALYZER_SELECTED_IDS_FILE="${selected_file}" if ! blocklist_yaml_candidates "${ids_file}" >"${candidates_file}" || ! awk -F'|' '{ print $1 }' "${candidates_file}" >"${configured_ids_file}" || ! awk -v configuredfile="${configured_ids_file}" ' BEGIN { while ((getline id < configuredfile) > 0) configured[id] = 1; close(configuredfile) } !configured[$0] { print } ' "${ids_file}" >"${orphaned_ids_file}"; then rm -f "${candidates_file}" "${configured_ids_file}" "${orphaned_ids_file}" "${selected_file}" return 1 fi if [ -s "${orphaned_ids_file}" ]; then PTXT "${WARNING} Stale or historical filter-cache IDs were reported by the analyzer:" while IFS= read -r id; do PTXT " id=${id} cache=${TARG_DIR}/data/filters/${id}.txt" done <"${orphaned_ids_file}" PTXT "${WARNING} These IDs have no current YAML blocklist to remove; AdGuard Home remains responsible for its filter cache." fi if [ ! -s "${candidates_file}" ]; then rm -f "${candidates_file}" "${configured_ids_file}" "${orphaned_ids_file}" "${selected_file}" ptxt_ok "No configured unused blocklists were found." return 3 fi PTXT "${INFO} Configured unused blocklists found in AdGuardHome.yaml:" while IFS='|' read -r id name url; do PTXT " id=${id} name=${name} url=${url}" done <"${candidates_file}" PTXT "" \ "${WARNING} Review these lists before removal." \ "${WARNING} Unused means the list did not uniquely block anything in the analyzed query log period." if read_yesno "Do you want to remove all matching unused blocklists?"; then if ! cp "${configured_ids_file}" "${selected_file}"; then rm -f "${candidates_file}" "${configured_ids_file}" "${orphaned_ids_file}" "${selected_file}" return 1 fi rm -f "${candidates_file}" "${configured_ids_file}" "${orphaned_ids_file}" BLOCKLIST_ANALYZER_SELECTED_IDS_FILE="${selected_file}" return 0 else choice_status="$?" fi if [ "${choice_status}" -ne 1 ]; then rm -f "${candidates_file}" "${configured_ids_file}" "${orphaned_ids_file}" "${selected_file}" return "${choice_status}" fi : >"${selected_file}" || { rm -f "${candidates_file}" "${configured_ids_file}" "${orphaned_ids_file}" return 1 } while IFS='|' read -r id name url; do [ -n "${name}" ] && list_label="${name}" || list_label="id ${id}" PTXT "" "${INFO} Unused blocklist candidate:" " id=${id}" " name=${name}" " url=${url}" read_yesno "Remove blocklist ${list_label} from AdGuardHome.yaml?" choice_status="$?" case "${choice_status}" in 0) PTXT "${id}" >>"${selected_file}" ;; 1) ptxt_ok "Keeping blocklist id ${id}." ;; *) rm -f "${candidates_file}" "${configured_ids_file}" "${orphaned_ids_file}" "${selected_file}" return "${choice_status}" ;; esac done <"${candidates_file}" rm -f "${candidates_file}" "${configured_ids_file}" "${orphaned_ids_file}" if [ ! -s "${selected_file}" ]; then rm -f "${selected_file}" ptxt_warn "No unused blocklists were selected for removal." return 1 fi BLOCKLIST_ANALYZER_SELECTED_IDS_FILE="${selected_file}" return 0 } blocklist_yaml_replace_abort_on_signal() { local backup_file removed_file tmp_file backup_file="${BLOCKLIST_YAML_BACKUP_FILE:-}" removed_file="${BLOCKLIST_YAML_REMOVED_FILE:-}" tmp_file="${BLOCKLIST_YAML_TMP_FILE:-}" trap - HUP INT QUIT ABRT TERM if [ -n "${backup_file}" ] && [ -f "${backup_file}" ]; then if cp "${backup_file}" "${YAML_FILE}" || { rm -f "${YAML_FILE}" && mv "${backup_file}" "${YAML_FILE}"; }; then chmod 600 "${YAML_FILE}" rollback_result_write "${INSTALLER_BLOCKLIST_REPLACEMENT:-blocklist yaml replacement}" "restored" "interrupted; restored ${YAML_FILE}" rollback_result_notice ptxt_fail "Blocklist cleanup interrupted; restored ${YAML_FILE} from ${backup_file}." else rollback_result_write "${INSTALLER_BLOCKLIST_REPLACEMENT:-blocklist yaml replacement}" "${INSTALLER_RESTORE_FAILED:-restore-failed}" "interrupted; backup ${backup_file}" rollback_result_notice ptxt_fail "Blocklist cleanup interrupted, and ${YAML_FILE} could not be restored from ${backup_file}." fi fi [ -z "${tmp_file}" ] || rm -f "${tmp_file}" [ -z "${removed_file}" ] || rm -f "${removed_file}" BLOCKLIST_YAML_BACKUP_FILE="" BLOCKLIST_YAML_REMOVED_FILE="" BLOCKLIST_YAML_TMP_FILE="" clear_screen exit 2 } blocklist_yaml_replace_trap_disable() { trap 'END_OP_SIGNAL=1; clear_screen; end_op_message 2' HUP INT QUIT ABRT TERM BLOCKLIST_YAML_BACKUP_FILE="" BLOCKLIST_YAML_REMOVED_FILE="" BLOCKLIST_YAML_TMP_FILE="" } blocklist_yaml_replace_trap_enable() { BLOCKLIST_YAML_BACKUP_FILE="$1" BLOCKLIST_YAML_REMOVED_FILE="$2" BLOCKLIST_YAML_TMP_FILE="${3:-}" trap 'blocklist_yaml_replace_abort_on_signal' HUP INT QUIT ABRT TERM } remove_unused_blocklists_from_yaml() { local ids_file backup_file tmp_file removed_file ids_file="$1" backup_file="${YAML_FILE}.blocklists.$(date +%Y%m%d%H%M%S).$$.bak" tmp_file="${YAML_FILE}.blocklists.$$.tmp" removed_file="${YAML_FILE}.blocklists.$$.removed" ptxt_phase "Removing unused blocklists from AdGuardHome.yaml." if [ ! -f "${YAML_FILE}" ]; then ptxt_fail "${YAML_FILE} does not exist." return 1 fi ptxt_step "Lists selected for removal from the filters section:" blocklist_yaml_candidates "${ids_file}" "${YAML_FILE}" | while IFS='|' read -r id name url; do PTXT " id=${id} name=${name} url=${url}" done ptxt_step "Creating backup ${backup_file}." cp "${YAML_FILE}" "${backup_file}" || return 1 ptxt_ok "Backup created." awk -v idsfile="${ids_file}" -v removedfile="${removed_file}" ' BEGIN { while ((getline id < idsfile) > 0) ids[id] = 1; close(idsfile); in_filters = 0; in_entry = 0 } function flush_entry( n) { if (!in_entry) return if (remove_entry) { print removed_id >> removedfile } else { printf "%s", entry } entry = ""; in_entry = 0; remove_entry = 0; removed_id = "" } /^[^[:space:]][^:]*:/ { if (in_filters && $0 !~ /^filters:/) { flush_entry(); in_filters = 0 } } /^filters:[[:space:]]*$/ { flush_entry(); in_filters = 1; print; next } in_filters && /^[[:space:]]*-[[:space:]]*enabled:/ { flush_entry(); entry = $0 ORS; in_entry = 1; next } in_filters && in_entry { entry = entry $0 ORS if ($0 ~ /^[[:space:]]*id:[[:space:]]*[0-9]+[[:space:]]*$/) { id = $0; sub(/.*id:[[:space:]]*/, "", id); sub(/[[:space:]]*$/, "", id); if (ids[id]) { remove_entry = 1; removed_id = id } } next } { flush_entry(); print } END { flush_entry() } ' "${YAML_FILE}" >"${tmp_file}" || { rm -f "${tmp_file}" "${removed_file}" rollback_result_write "blocklist-yaml" "failed: no change" "could not build edited YAML" rollback_result_notice return 1 } if [ ! -s "${removed_file}" ]; then rm -f "${tmp_file}" "${removed_file}" ptxt_warn "No matching numeric filter IDs were found under filters:." return 1 fi if ! adguardhome_yaml_secure_file "${tmp_file}"; then rm -f "${tmp_file}" "${removed_file}" rollback_result_write "${INSTALLER_BLOCKLIST_REPLACEMENT:-blocklist yaml replacement}" "failed: no change" "could not secure staged YAML" rollback_result_notice return 1 fi blocklist_yaml_replace_trap_enable "${backup_file}" "${removed_file}" "${tmp_file}" if ! mv "${tmp_file}" "${YAML_FILE}"; then rm -f "${tmp_file}" "${removed_file}" rollback_result_write "${INSTALLER_BLOCKLIST_REPLACEMENT:-blocklist yaml replacement}" "replace-failed" "${YAML_FILE}" rollback_result_notice blocklist_yaml_replace_trap_disable return 1 fi chmod 600 "${YAML_FILE}" ptxt_step "Validating edited AdGuardHome configuration." if ! check_AdGuardHome_yaml "${YAML_FILE}"; then if cp "${backup_file}" "${YAML_FILE}" || { rm -f "${YAML_FILE}" && mv "${backup_file}" "${YAML_FILE}"; }; then chmod 600 "${YAML_FILE}" rm -f "${removed_file}" blocklist_yaml_replace_trap_disable rollback_result_write "${INSTALLER_BLOCKLIST_REPLACEMENT:-blocklist yaml replacement}" "restored" "validation failed; restored ${YAML_FILE}" rollback_result_notice ptxt_fail "Validation failed; restored ${YAML_FILE} from ${backup_file}." else rm -f "${removed_file}" blocklist_yaml_replace_trap_disable rollback_result_write "${INSTALLER_BLOCKLIST_REPLACEMENT:-blocklist yaml replacement}" "${INSTALLER_RESTORE_FAILED:-restore-failed}" "validation failed; backup ${backup_file}" rollback_result_notice ptxt_fail "Validation failed, and ${YAML_FILE} could not be restored from ${backup_file}." fi return 1 fi blocklist_yaml_replace_trap_disable ptxt_ok "Removed unused blocklist IDs:" cat "${removed_file}" rm -f "${removed_file}" ptxt_warn "Restarting AdGuardHome temporarily interrupts AdGuardHome DNS filtering." OLD_PIDS="$(pidof AdGuardHome 2>/dev/null)" if adguard_service_without_nvram_lock_fd service restart_AdGuardHome >/dev/null 2>&1; then adguard_service_status_after_action restart "${OLD_PIDS}" || true ptxt_ok "AdGuardHome restarted after blocklist cleanup." else ptxt_warn "Configuration was saved, but AdGuardHome could not be restarted automatically." fi } # cleanup_unused_blocklists runs the analyzer, selects configured unused lists, # and removes confirmed entries from YAML through the removal helper. Takes no # arguments; uses the installer paths and analyzer file globals. Cleans analyzer # results and reports completion, treating no unused/configured lists as success # and cancellation or analyzer/removal errors as failure. Leaves stale cache # files untouched. cleanup_unused_blocklists() { install_blocklist_analyzer || { blocklist_analyzer_pause end_op_message 1 return 1 } run_blocklist_analyzer case "$?" in 0) ;; 2) blocklist_analyzer_pause end_op_message 0 return 0 ;; *) blocklist_analyzer_pause end_op_message 1 return 1 ;; esac select_unused_blocklists_for_removal "${BLOCKLIST_ANALYZER_IDS_FILE}" case "$?" in 0) ;; 3) rm -f "${BLOCKLIST_ANALYZER_IDS_FILE:-}" "${BLOCKLIST_ANALYZER_OUTPUT_FILE:-}" "${BLOCKLIST_ANALYZER_SELECTED_IDS_FILE:-}" blocklist_analyzer_pause end_op_message 0 return 0 ;; *) rm -f "${BLOCKLIST_ANALYZER_IDS_FILE:-}" "${BLOCKLIST_ANALYZER_OUTPUT_FILE:-}" "${BLOCKLIST_ANALYZER_SELECTED_IDS_FILE:-}" ptxt_warn "Blocklist cleanup cancelled or nothing was selected." blocklist_analyzer_pause end_op_message 1 return 1 ;; esac if remove_unused_blocklists_from_yaml "${BLOCKLIST_ANALYZER_SELECTED_IDS_FILE}"; then rm -f "${BLOCKLIST_ANALYZER_IDS_FILE:-}" "${BLOCKLIST_ANALYZER_OUTPUT_FILE:-}" "${BLOCKLIST_ANALYZER_SELECTED_IDS_FILE:-}" blocklist_analyzer_pause end_op_message 0 else rm -f "${BLOCKLIST_ANALYZER_IDS_FILE:-}" "${BLOCKLIST_ANALYZER_OUTPUT_FILE:-}" "${BLOCKLIST_ANALYZER_SELECTED_IDS_FILE:-}" blocklist_analyzer_pause end_op_message 1 return 1 fi } ########################Modified Version of @Adamm Check_Connection#################################################### check_connection() { local deadline i livecheck="0" maxwait now remaining sleep_for maxwait="${1:-}" if [ -n "${maxwait}" ]; then now="$(monotonic_seconds)" deadline="$((now + maxwait))" fi while [ "${livecheck}" != "4" ]; do for i in google.com github.com snbforums.com; do if [ -n "${deadline}" ]; then now="$(monotonic_seconds)" [ "${now}" -ge "${deadline}" ] && return 1 fi if { ! nslookup "${i}" 127.0.0.1 >/dev/null 2>&1; } && { ping -q -w3 -c1 "${i}" >/dev/null 2>&1; }; then remaining="" if [ -n "${deadline}" ]; then now="$(monotonic_seconds)" remaining="$((deadline - now))" [ "${remaining}" -le 0 ] && return 1 fi if ! http_probe "http://${i}" "${remaining}" >/dev/null 2>&1; then if [ -n "${deadline}" ]; then now="$(monotonic_seconds)" [ "${now}" -ge "${deadline}" ] && return 1 fi sleep 1s continue fi fi return 0 done livecheck="$((livecheck + 1))" if [ "${livecheck}" != "4" ]; then if [ -n "${deadline}" ]; then now="$(monotonic_seconds)" sleep_for="$((deadline - now))" [ "${sleep_for}" -le 0 ] && return 1 [ "${sleep_for}" -gt 10 ] && sleep_for="10" sleep "${sleep_for}s" else sleep 10s fi continue fi return 1 done } ######################################################################################################################## check_dns_environment() { local changed deadline lookup_pid maxwait mode now status stubby_recovery_attempted stubby_stopped mode="$1" ptxt_phase "Preparing DNS environment." if [ "${mode}" = "0" ] && { [ -f "/opt/etc/init.d/S61stubby" ] || [ -f "/opt/sbin/stubby" ] || [ -f "/opt/bin/install_stubby" ] || [ -f "/jffs/scripts/install_stubby.sh" ] || [ -d "/jffs/dnscrypt" ] || [ -f "/opt/etc/init.d/S09dnscrypt-proxy" ]; }; then PTXT "${ERROR} Potential stubby or dnscrypt-proxy installation detected." \ "${ERROR} Please remove before attempting to continue." \ "${ERROR} Exiting..." exit 1 fi dns_env_local_ready() { maxwait="$1" now="$(monotonic_seconds)" || return 1 deadline="$((now + maxwait))" while [ "${now}" -lt "${deadline}" ]; do nslookup localhost 127.0.0.1 >/dev/null 2>&1 & lookup_pid="$!" while kill -0 "${lookup_pid}" >/dev/null 2>&1; do now="$(monotonic_seconds)" || { kill "${lookup_pid}" >/dev/null 2>&1 || : wait "${lookup_pid}" 2>/dev/null || : return 1 } if [ "${now}" -ge "${deadline}" ]; then kill "${lookup_pid}" >/dev/null 2>&1 || : wait "${lookup_pid}" 2>/dev/null || : return 1 fi sleep 1s done wait "${lookup_pid}" status="$?" [ "${status}" -eq 0 ] && return 0 now="$(monotonic_seconds)" || return 1 sleep 1s now="$(monotonic_seconds)" || return 1 done return 1 } case "${DNS_ENV_READY_TIMEOUT}" in *[!0-9]* | "" | 0*) DNS_ENV_READY_TIMEOUT="60" ;; esac case "${DNS_ENV_RECOVERY_TIMEOUT}" in *[!0-9]* | "" | 0*) DNS_ENV_RECOVERY_TIMEOUT="15" ;; esac case "${mode}" in 0) stubby_stopped="0" stubby_recovery_attempted="0" ptxt_step "Saving current DNS NVRAM settings." if ! nvram_transaction_begin dns-preparation dnspriv_enable dhcpd_dns_router dhcp_dns1_x dhcp_dns2_x; then PTXT "${ERROR} Unable to read all required DNS NVRAM settings; no changes were made." return 1 fi _DNS_NVRAM_SAVED="1" export _DNS_NVRAM_SAVED if [ "$(pidof stubby)" ]; then ptxt_step "Stopping conflicting stubby process." _DNS_STUBBY_STOPPED="1" export _DNS_STUBBY_STOPPED : >"${NVRAM_TRANSACTION_DIR}/stubby-stopped" || return 1 chmod 600 "${NVRAM_TRANSACTION_DIR}/stubby-stopped" || return 1 killall -q -9 stubby 2>/dev/null || : [ -z "$(pidof stubby)" ] || return 1 stubby_stopped="1" fi if ! nvram_transaction_set dnspriv_enable "0" || ! nvram_transaction_set dhcpd_dns_router "1" || ! nvram_transaction_set dhcp_dns1_x "" || ! nvram_transaction_set dhcp_dns2_x ""; then [ "${stubby_stopped}" = "0" ] || restore_stubby_after_dns_failure || PTXT "${ERROR} Unable to restart stubby after DNS preparation failed." return 1 fi changed="${NVRAM_TRANSACTION_CHANGED:-0}" if ! nvram_transaction_apply restart_dnsmasq 1; then PTXT "${ERROR} Unable to apply the DNS NVRAM transaction." [ "${stubby_stopped}" = "0" ] || restore_stubby_after_dns_failure || PTXT "${ERROR} Unable to restart stubby after DNS preparation failed." return 1 fi if [ "${stubby_stopped}" = "1" ] && [ "${changed}" = "0" ]; then if ! service restart_dnsmasq >/dev/null 2>&1; then PTXT "${ERROR} Unable to restart dnsmasq after stopping stubby." if ! : >"${NVRAM_TRANSACTION_DIR}/dirty" || ! chmod 600 "${NVRAM_TRANSACTION_DIR}/dirty"; then PTXT "${ERROR} Unable to preserve the DNS recovery snapshot." fi if service restart_dnsmasq >/dev/null 2>&1 && dns_env_local_ready "${DNS_ENV_RECOVERY_TIMEOUT}"; then # Keep the persisted marker until the resolver it represents is # running again; EXIT/startup recovery can then retry safely. if restore_stubby_after_dns_failure; then stubby_stopped="0" if rm -f "${NVRAM_TRANSACTION_DIR}/dirty"; then rm -rf "${NVRAM_TRANSACTION_DIR}" 2>/dev/null || true else rollback_result_write "${INSTALLER_DNS_TRANSACTION:-DNS NVRAM transaction}" "${INSTALLER_ROLLBACK_PARTIAL:-rollback partial}" "snapshot preserved at ${NVRAM_TRANSACTION_DIR}" fi else rollback_result_write "${INSTALLER_DNS_TRANSACTION:-DNS NVRAM transaction}" "${INSTALLER_ROLLBACK_PARTIAL:-rollback partial}" "snapshot preserved at ${NVRAM_TRANSACTION_DIR}" fi else rollback_result_write "${INSTALLER_DNS_TRANSACTION:-DNS NVRAM transaction}" "${INSTALLER_ROLLBACK_PARTIAL:-rollback partial}" "snapshot preserved at ${NVRAM_TRANSACTION_DIR}" fi if [ "${stubby_stopped}" != "0" ]; then restore_stubby_after_dns_failure || PTXT "${ERROR} Unable to restart stubby after DNS preparation failed." fi return 1 fi changed="1" fi if [ "${changed}" != "0" ] && ! dns_env_local_ready "${DNS_ENV_READY_TIMEOUT}"; then PTXT "${ERROR} DNS readiness did not recover after applying NVRAM changes." if nvram_transaction_restore restart_dnsmasq 1; then if [ "${stubby_stopped}" = "0" ]; then # NVRAM rollback is complete. Clear its recovery marker before # best-effort readiness checks and snapshot cleanup. if rm -f "${NVRAM_TRANSACTION_DIR}/dirty"; then if dns_env_local_ready "${DNS_ENV_RECOVERY_TIMEOUT}"; then rm -rf "${NVRAM_TRANSACTION_DIR}" 2>/dev/null || true else rollback_result_write "${INSTALLER_DNS_TRANSACTION:-DNS NVRAM transaction}" "${INSTALLER_ROLLBACK_COMPLETE:-rollback complete}" "DNS readiness did not recover" fi else rollback_result_write "${INSTALLER_DNS_TRANSACTION:-DNS NVRAM transaction}" "${INSTALLER_ROLLBACK_PARTIAL:-rollback partial}" "snapshot preserved at ${NVRAM_TRANSACTION_DIR}" fi elif dns_env_local_ready "${DNS_ENV_RECOVERY_TIMEOUT}"; then # Retain the recovery marker until the stopped resolver is running again. stubby_recovery_attempted="1" if ! restore_stubby_after_dns_failure; then rollback_result_write "${INSTALLER_DNS_TRANSACTION:-DNS NVRAM transaction}" "${INSTALLER_ROLLBACK_PARTIAL:-rollback partial}" "snapshot preserved at ${NVRAM_TRANSACTION_DIR}" fi else rollback_result_write "${INSTALLER_DNS_TRANSACTION:-DNS NVRAM transaction}" "${INSTALLER_ROLLBACK_PARTIAL:-rollback partial}" "snapshot preserved at ${NVRAM_TRANSACTION_DIR}" fi else if [ -d "${NVRAM_TRANSACTION_DIR}" ]; then rollback_result_write "${INSTALLER_DNS_TRANSACTION:-DNS NVRAM transaction}" "${INSTALLER_ROLLBACK_PARTIAL:-rollback partial}" "snapshot preserved at ${NVRAM_TRANSACTION_DIR}" else rollback_result_write "${INSTALLER_DNS_TRANSACTION:-DNS NVRAM transaction}" "${INSTALLER_ROLLBACK_PARTIAL:-rollback partial}" fi fi if [ "${stubby_stopped}" != "0" ] && [ "${stubby_recovery_attempted}" = "0" ]; then restore_stubby_after_dns_failure || PTXT "${ERROR} Unable to restart stubby after DNS preparation failed." fi return 1 fi ;; 1) NVRAM_TRANSACTION_DIR="${BASE_DIR}/.AdGuardHome.nvram/dns-preparation" export NVRAM_TRANSACTION_DIR if [ ! -d "${NVRAM_TRANSACTION_DIR}" ]; then _DNS_NVRAM_SAVED="0" export _DNS_NVRAM_SAVED ptxt_ok "No saved DNS NVRAM settings to restore." return 0 fi if [ -f "${NVRAM_TRANSACTION_DIR}/stubby-stopped" ]; then if restore_stubby_after_dns_failure; then : else rollback_result_write "${INSTALLER_DNS_TRANSACTION:-DNS NVRAM transaction}" "${INSTALLER_ROLLBACK_PARTIAL:-rollback partial}" "snapshot preserved at ${NVRAM_TRANSACTION_DIR}" PTXT "${ERROR} Unable to restart stubby before restoring the saved DNS NVRAM settings." return 1 fi fi if ! nvram_transaction_restore restart_dnsmasq 1 || ! dns_env_local_ready "${DNS_ENV_RECOVERY_TIMEOUT}"; then if [ -d "${NVRAM_TRANSACTION_DIR}" ]; then rollback_result_write "${INSTALLER_DNS_TRANSACTION:-DNS NVRAM transaction}" "${INSTALLER_ROLLBACK_PARTIAL:-rollback partial}" "snapshot preserved at ${NVRAM_TRANSACTION_DIR}" else rollback_result_write "${INSTALLER_DNS_TRANSACTION:-DNS NVRAM transaction}" "${INSTALLER_ROLLBACK_PARTIAL:-rollback partial}" fi PTXT "${ERROR} Unable to restore all saved DNS NVRAM settings." return 1 fi # Clearing dirty commits the completed restore before best-effort snapshot cleanup. if ! rm -f "${NVRAM_TRANSACTION_DIR}/dirty"; then rollback_result_write "${INSTALLER_DNS_TRANSACTION:-DNS NVRAM transaction}" "${INSTALLER_ROLLBACK_PARTIAL:-rollback partial}" "snapshot preserved at ${NVRAM_TRANSACTION_DIR}" PTXT "${ERROR} Unable to restore all saved DNS NVRAM settings." return 1 fi rm -rf "${NVRAM_TRANSACTION_DIR}" 2>/dev/null || true _DNS_NVRAM_SAVED="0" export _DNS_NVRAM_SAVED ;; *) return 1 ;; esac ptxt_ok "DNS Environment is Ready." return 0 } restore_stubby_after_dns_failure() { service restart_stubby >/dev/null 2>&1 || return 1 _DNS_STUBBY_STOPPED="0" export _DNS_STUBBY_STOPPED rm -f "${BASE_DIR}/.AdGuardHome.nvram/dns-preparation/stubby-stopped" || return 1 } finalize_dns_environment() { local snapshot_dir snapshot_dir="${BASE_DIR}/.AdGuardHome.nvram/dns-preparation" # Publish the shared setup commit point before removing the DNS preparation # recovery marker. Once this succeeds, startup recovery can finish all # snapshot cleanup without rolling back either half of the setup. nvram_transaction_finalize_setup_pair || return 1 if [ -e "${snapshot_dir}" ] || [ -L "${snapshot_dir}" ]; then # The shared marker is now the commit point; cleanup is best-effort. rm -f "${snapshot_dir}/dirty" 2>/dev/null || true rm -rf "${snapshot_dir}" 2>/dev/null || true fi # Do not discard the shared commit point while paired setup cleanup is # incomplete; startup recovery must still recognize those snapshots as # committed rather than rollback candidates. nvram_transaction_finalize_setup_pair recovery || return 1 _DNS_NVRAM_SAVED="0" export _DNS_NVRAM_SAVED _DNS_STUBBY_STOPPED="0" export _DNS_STUBBY_STOPPED } check_dns_filter() { local changed key mode use_some yesno_status mode="$1" case "${mode}" in 0) ;; 1) PTXT "${INFO} You can choose to keep any custom dnsfilter values by only redirect non-custom traffic or send all traffic through to AdGuardHome." read_yesno "Do you want to redirect only NON-CUSTOM DNS resolutions on your network through to AdGuardHome?" yesno_status="$?" case "${yesno_status}" in 0) use_some="0" ;; 1) use_some="1" ;; *) return "${yesno_status}" ;; esac ;; *) return 1 ;; esac if [ "${NVRAM_TRANSACTION_DIR:-}" != "${BASE_DIR}/.AdGuardHome.nvram/dnsfilter" ] || [ ! -f "${NVRAM_TRANSACTION_DIR}/keys" ]; then if ! nvram_transaction_begin dnsfilter dnsfilter_enable_x dnsfilter_mode dhcpd_dns_router dnsfilter_custom1 dnsfilter_custom2 dnsfilter_custom3 dnsfilter_rulelist dnsfilter_rulelist1 dnsfilter_rulelist2 dnsfilter_rulelist3 dnsfilter_rulelist4 dnsfilter_rulelist5 dhcp_dns1_x dhcp_dns2_x; then PTXT "${ERROR} Unable to read all required DNSFilter NVRAM settings; no changes were made." return 1 fi fi case "${mode}" in 0) nvram_transaction_set dnsfilter_enable_x "0" || return 1 PTXT "${INFO} DNS will not be forced through to AdGuardHome." ;; 1) nvram_transaction_set dnsfilter_enable_x "1" || return 1 nvram_transaction_set dnsfilter_mode "11" || return 1 if [ "${use_some}" = "1" ]; then nvram_transaction_set dhcpd_dns_router "1" || return 1 for key in dnsfilter_custom1 dnsfilter_custom2 dnsfilter_custom3 dnsfilter_rulelist dnsfilter_rulelist1 dnsfilter_rulelist2 dnsfilter_rulelist3 dnsfilter_rulelist4 dnsfilter_rulelist5 dhcp_dns1_x dhcp_dns2_x; do nvram_transaction_set "${key}" "" || return 1 done PTXT "${INFO} DNS is set to redirect All DNS resolutions through to AdGuardHome." else PTXT "${INFO} DNSFilter is set to control DNS through to AdGuardHome, while leaving any Custom Rules and Values." fi ;; *) return 1 ;; esac changed="${NVRAM_TRANSACTION_CHANGED:-0}" if ! nvram_transaction_apply "restart_firewall;restart_dnsmasq" 1; then PTXT "${ERROR} Unable to apply the DNSFilter NVRAM transaction." if ! restore_dns_filter_settings; then PTXT "${ERROR} Unable to restore the DNSFilter NVRAM transaction; the recovery snapshot was preserved." fi return 1 fi if [ "${changed}" != "0" ] && ! check_connection 300; then PTXT "${WARNING} Timed out waiting for DNS connectivity; restoring DNSFilter settings." if nvram_transaction_restore "restart_firewall;restart_dnsmasq" 1 && check_connection 300; then # Clearing dirty commits the completed rollback before best-effort snapshot cleanup. if rm -f "${NVRAM_TRANSACTION_DIR}/dirty"; then rm -rf "${NVRAM_TRANSACTION_DIR}" 2>/dev/null || true else rollback_result_write "DNSFilter NVRAM transaction" "${INSTALLER_ROLLBACK_PARTIAL:-rollback partial}" "snapshot preserved at ${NVRAM_TRANSACTION_DIR}" fi else rollback_result_write "DNSFilter NVRAM transaction" "${INSTALLER_ROLLBACK_PARTIAL:-rollback partial}" "snapshot preserved at ${NVRAM_TRANSACTION_DIR}" fi return 1 fi } save_dns_filter_settings() { # The argument is retained for compatibility with older setup callers. nvram_transaction_begin dnsfilter dnsfilter_enable_x dnsfilter_mode dhcpd_dns_router dnsfilter_custom1 dnsfilter_custom2 dnsfilter_custom3 dnsfilter_rulelist dnsfilter_rulelist1 dnsfilter_rulelist2 dnsfilter_rulelist3 dnsfilter_rulelist4 dnsfilter_rulelist5 dhcp_dns1_x dhcp_dns2_x } restore_dns_filter_settings() { ! nvram_transaction_setup_committed || return 0 NVRAM_TRANSACTION_DIR="${BASE_DIR}/.AdGuardHome.nvram/dnsfilter" export NVRAM_TRANSACTION_DIR [ -d "${NVRAM_TRANSACTION_DIR}" ] || return 0 nvram_transaction_restore "restart_firewall;restart_dnsmasq" } check_ipset() { local IPSET_ENABLED case "$1" in 0) IPSET_ENABLED="NO" ;; 1) if adguard_ipset_allowed; then IPSET_ENABLED="YES" else IPSET_ENABLED="NO" fi ;; *) return 1 ;; esac write_conf ADGUARD_IPSET "\"${IPSET_ENABLED}\"" } adguardhome_owner_account() { local account account="$(installer_nvram get http_username 2>/dev/null)" || return 1 [ -n "${account}" ] || account="root" case "${account}" in *[!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.-]* | [.-]* | "") return 1 ;; esac [ "${#account}" -le 32 ] || return 1 case "${account}" in *[!0123456789]*) ;; *) return 1 ;; esac if [ -r /etc/passwd ]; then /usr/bin/awk -F: -v account="${account}" '$1 == account && (account != "root" || $3 == 0) { found = 1; exit } END { exit(found ? 0 : 1) }' /etc/passwd || return 1 fi printf '%s\n' "${account}" } adguardhome_yaml_secure_file() { local http_username yaml_target yaml_target="${1:-${YAML_FILE}}" [ -f "${yaml_target}" ] || return 0 chmod 600 "${yaml_target}" || return 1 if ai_have_cmd nvram; then http_username="$(adguardhome_owner_account)" || return 1 chown "${http_username}:root" "${yaml_target}" || return 1 fi } adguardhome_yaml_remove_ipset_file() { local has_ipset tmp_file yaml_target yaml_target="${1:-${YAML_FILE}}" [ -f "${yaml_target}" ] || return 0 has_ipset="$(awk ' function indentation(line, text) { text = line; sub(/[^[:space:]].*$/, "", text); return length(text) } /^(dns|\047dns\047|"dns"):[[:space:]]*(&[^][{},[:space:]]+[[:space:]]*)?(#.*)?$/ { in_dns = 1; next } in_dns && /^[^[:space:]]/ { exit } in_dns && /^[[:space:]]*($|#)/ { next } in_dns && !child_indent { child_indent = indentation($0) } in_dns && indentation($0) == child_indent && substr($0, child_indent + 1) ~ /^(ipset|ipset_file|\047ipset\047|\047ipset_file\047|"ipset"|"ipset_file"):[[:space:]]*/ { print "1"; exit } ' "${yaml_target}")" || return 1 [ "${has_ipset}" = "1" ] || return 0 chmod 600 "${yaml_target}" || return 1 tmp_file="${yaml_target}.ipset.$$" cp -p "${yaml_target}" "${tmp_file}" || return 1 awk ' function indentation(line, text) { text = line; sub(/[^[:space:]].*$/, "", text); return length(text) } function block_marker(value) { gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) return value ~ /^[>|]([+-]|[+-]?[1-9]|[1-9][+-]?)?([[:space:]]+#.*)?$/ } /^(dns|\047dns\047|"dns"):[[:space:]]*(&[^][{},[:space:]]+[[:space:]]*)?(#.*)?$/ { in_dns = 1; print; next } in_dns && /^[^[:space:]]/ { in_dns = 0 } in_dns && skip_block && /^[[:space:]]*($|#)/ { next } in_dns && skip_block && indentation($0) > skip_indent { next } in_dns && skip_block && indentation($0) == skip_indent && substr($0, skip_indent + 1) ~ /^-[[:space:]]/ { next } in_dns && skip_block { skip_block = 0 } in_dns && /^[[:space:]]*($|#)/ { print; next } in_dns && !child_indent { child_indent = indentation($0) } in_dns && indentation($0) == child_indent && substr($0, child_indent + 1) ~ /^(ipset|\047ipset\047|"ipset"):[[:space:]]*/ { print substr($0, 1, child_indent) "ipset: []" skip_block = 1 skip_indent = child_indent next } in_dns && indentation($0) == child_indent && substr($0, child_indent + 1) ~ /^(ipset_file|\047ipset_file\047|"ipset_file"):[[:space:]]*/ { value = substr($0, child_indent + 1) sub(/^(ipset_file|\047ipset_file\047|"ipset_file"):[[:space:]]*/, "", value) if (block_marker(value)) { skip_block = 1 skip_indent = child_indent } next } { print } ' "${yaml_target}" >"${tmp_file}" && adguardhome_yaml_secure_file "${tmp_file}" && mv -f "${tmp_file}" "${yaml_target}" && adguardhome_yaml_secure_file "${yaml_target}" || { rm -f "${tmp_file}" return 1 } } adguard_enforce_lan_ipset_disabled() { local restored_mode case "${ADGUARD_INSTALL_MODE:-}" in wan) restored_mode="$(conf_value ADGUARD_INSTALL_MODE 2>/dev/null)" write_conf ADGUARD_INSTALL_MODE '"wan"' || return 1 case "${restored_mode}" in lan) case "$(conf_value ADGUARD_IPSET 2>/dev/null)" in NO) ;; *) write_conf ADGUARD_IPSET '"YES"' || return 1 ;; esac write_conf ADGUARD_NETCHECK_MODE '"wan"' || return 1 ADGUARD_FORCE_SETUP_YAML="1" ;; esac return 0 ;; lan) restored_mode="$(conf_value ADGUARD_INSTALL_MODE 2>/dev/null)" case "${restored_mode}" in wan | "") write_conf ADGUARD_NETCHECK_MODE '"lan"' || return 1 ADGUARD_FORCE_SETUP_YAML="1" ;; esac ;; *) case "$(conf_value ADGUARD_INSTALL_MODE 2>/dev/null)" in lan) ADGUARD_INSTALL_MODE="lan" ;; *) return 0 ;; esac ;; esac ADGUARD_INSTALL_MODE="lan" write_conf ADGUARD_INSTALL_MODE "\"lan\"" || return 1 if adguard_ipset_allowed; then return 0 fi write_conf ADGUARD_IPSET "\"NO\"" || return 1 adguardhome_yaml_remove_ipset_file || return 1 PTXT "${INFO} IPSET is disabled in LAN/AP/Bridge mode because IPTABLES/IPSET rules are not manageable there." return 0 } check_dns_local() { local LOCAL_CACHE case "$1" in 0) LOCAL_CACHE="NO" write_conf ADGUARD_LOCAL "\"${LOCAL_CACHE}\"" ;; 1) LOCAL_CACHE="YES" write_conf ADGUARD_LOCAL "\"${LOCAL_CACHE}\"" ;; esac } check_jffs_enabled() { local jffs2_format_value jffs2_scripts_value jffs2_enabled_value jffs_enable_key jffs_read_reason jffs2_enable if ! jffs_read_settings; then PTXT "${ERROR} JFFS readiness failed at ${jffs_read_reason}; review the JFFS settings and retry." return 1 fi if [ "${jffs2_format_value}" = "1" ]; then PTXT "${ERROR} JFFS partition is scheduled to be reformatted." \ "${ERROR} Please reboot to format or disable that setting and try again." \ "${ERROR} Exiting..." exit 1 fi if [ "${jffs2_format_value}" = "0" ] && [ "${jffs2_enabled_value}" = "1" ] && [ "${jffs2_scripts_value}" = "1" ]; then PTXT "${INFO} JFFS custom scripts and configs are already enabled." return 0 fi if ! nvram_transaction_begin jffs-enable jffs2_format jffs2_scripts jffs2_enable jffs2_on; then PTXT "${ERROR} JFFS transaction failed at ${NVRAM_TRANSACTION_BEGIN_STAGE:-unknown}; review the transaction recovery result before retrying." return 1 fi jffs2_format_value="$(cat "${NVRAM_TRANSACTION_DIR}/jffs2_format")" || return 1 if [ "${jffs2_format_value}" = "1" ]; then rm -rf "${NVRAM_TRANSACTION_DIR}" PTXT "${ERROR} JFFS partition is scheduled to be reformatted." \ "${ERROR} Please reboot to format or disable that setting and try again." \ "${ERROR} Exiting..." exit 1 fi jffs2_scripts_value="$(cat "${NVRAM_TRANSACTION_DIR}/jffs2_scripts")" || return 1 jffs2_enable="$(cat "${NVRAM_TRANSACTION_DIR}/jffs2_enable")" || return 1 if [ -z "${jffs2_enable}" ]; then jffs2_enabled_value="$(cat "${NVRAM_TRANSACTION_DIR}/jffs2_on")" || return 1 jffs_enable_key="jffs2_on" else jffs2_enabled_value="${jffs2_enable}" jffs_enable_key="jffs2_enable" fi if [ "${jffs2_enabled_value}" != "1" ] || [ "${jffs2_scripts_value}" != "1" ]; then PTXT "${INFO} JFFS custom scripts and configs are not enabled." \ "${INFO} Enabling them now!" nvram_transaction_set "${jffs_enable_key}" "1" || return 1 nvram_transaction_set jffs2_scripts "1" || return 1 if [ ! -f "${NVRAM_TRANSACTION_DIR}/exists.jffs2_format" ]; then nvram_transaction_set jffs2_format "0" || return 1 fi nvram_transaction_apply - || return 1 elif [ ! -f "${NVRAM_TRANSACTION_DIR}/exists.jffs2_format" ]; then PTXT "${INFO} Adding the missing legacy JFFS format setting." nvram_transaction_set jffs2_format "0" || return 1 nvram_transaction_apply - || return 1 else rm -rf "${NVRAM_TRANSACTION_DIR}" PTXT "${INFO} JFFS custom scripts and configs are already enabled." fi } # installer_version_value reads one exact stable/RC identity without executing source. # Keep this grammar aligned with tools/check-release-consistency.sh. installer_version_value() { local version_file version_file="$1" awk ' /^AI_VERSION=/ { count++ if ($0 !~ /^AI_VERSION="v[0-9][0-9]?[.][0-9][0-9]?[.][0-9][0-9]?(-rc[.][1-9][0-9]?)?"$/) invalid = 1 value = $0 sub(/^AI_VERSION="v/, "", value) sub(/"$/, "", value) } END { if (count != 1 || invalid) exit 1 print value } ' "${version_file}" } # Compare installed and verified remote identities and report available updates. check_version() { local RMNSTALL LINSTALL MD5SUM_L MD5SUM_R MD5SUM_EXPECTED REMOTE_INSTALLER REMOTE_INSTALLER_MD5 REMOTE_INSTALLER_URL REMOTE_INSTALLER_MD5_URL REMOTE_CACHE_TOKEN ADGUARD_BRANCH ADGUARD_VER ADGUARD_WEB_URL REMOTE_ADGUARD_VER varcnt if [ "${AI_NO_AUTO_UPDATE:-0}" = "1" ]; then PTXT "${INFO} Installer version ${AI_VERSION}; automatic installer-component checks are disabled by --no-auto-update." return 0 fi if [ -f "${TARG_DIR}/installer" ] && [ -f "${AGH_FILE}" ] && [ -z "$2" ]; then if [ -n "$1" ] || { check_connection; }; then REMOTE_INSTALLER="${TARG_DIR}/.installer.remote.$$" REMOTE_INSTALLER_MD5="${REMOTE_INSTALLER}.md5sum" varcnt="0" until [ -n "${LINSTALL}" ] && [ -n "${RMNSTALL}" ] && [ -n "${MD5SUM_L}" ] && [ -n "${MD5SUM_R}" ]; do if [ -z "${LINSTALL}" ]; then LINSTALL="$(installer_version_value "${TARG_DIR}/installer")"; fi if [ -z "${MD5SUM_L}" ]; then MD5SUM_L="$(file_md5 "${TARG_DIR}/installer" 2>/dev/null)"; fi if [ -z "${RMNSTALL}" ] || [ -z "${MD5SUM_R}" ]; then rm -f "${REMOTE_INSTALLER}" "${REMOTE_INSTALLER_MD5}" REMOTE_CACHE_TOKEN="${INSTALLER_CACHE_NONCE}-version-${varcnt}" REMOTE_INSTALLER_URL="$(http_url_with_cache_token "${RURL}/installer" "${REMOTE_CACHE_TOKEN}")" REMOTE_INSTALLER_MD5_URL="$(http_url_with_cache_token "${RURL}/installer.md5sum" "${REMOTE_CACHE_TOKEN}")" if http_get_file "${REMOTE_INSTALLER_URL}" "${REMOTE_INSTALLER}" && [ -s "${REMOTE_INSTALLER}" ]; then RMNSTALL="$(installer_version_value "${REMOTE_INSTALLER}")" MD5SUM_R="$(file_md5 "${REMOTE_INSTALLER}" 2>/dev/null)" MD5SUM_EXPECTED="" if http_get_file "${REMOTE_INSTALLER_MD5_URL}" "${REMOTE_INSTALLER_MD5}" && [ -s "${REMOTE_INSTALLER_MD5}" ]; then MD5SUM_EXPECTED="$(awk 'NR == 1 {print $1; exit}' "${REMOTE_INSTALLER_MD5}")" fi if md5_is_valid "${MD5SUM_EXPECTED}" && [ "${MD5SUM_R}" != "${MD5SUM_EXPECTED}" ]; then PTXT "${WARNING} Remote installer checksum mismatch; retrying version check." RMNSTALL="" MD5SUM_R="" fi fi fi [ -n "${LINSTALL}" ] && [ -n "${RMNSTALL}" ] && [ -n "${MD5SUM_L}" ] && [ -n "${MD5SUM_R}" ] && break varcnt="$((varcnt + 1))" if [ "${varcnt}" -ge 2 ]; then rm -f "${REMOTE_INSTALLER}" "${REMOTE_INSTALLER_MD5}" PTXT "One or more critical variables could not be set in a timely manner." "Please check your internet connection, or try again later." "The installer script will now exit." sleep 3s exit 1 fi done rm -f "${REMOTE_INSTALLER}" "${REMOTE_INSTALLER_MD5}" ADGUARD_BRANCH="$(conf_value ADGUARD_BRANCH)" ADGUARD_VER="$("${AGH_FILE}" --version | cut -d" " -f4-)" [ -z "${LINSTALL}" ] && exit 1 [ -z "${RMNSTALL}" ] && exit 1 [ -z "${ADGUARD_VER}" ] && exit 1 REMOTE_ADGUARD_VER="$(adguard_remote_version "${ADGUARD_BRANCH}")" [ -z "${REMOTE_ADGUARD_VER}" ] && exit 1 if [ "${RMNSTALL}" != "${LINSTALL}" ]; then PTXT "${INFO} New AI_VERSION=v${RMNSTALL} Available!" \ "${INFO} Run Option 1 of the Installer to upgrade Asuswrt-Merlin-AdGuardHome-Installer." AUTO_UPDATE="${INSTALLER_ACTION_UPDATE:-update}" elif [ "${MD5SUM_R}" = "${MD5SUM_L}" ]; then PTXT "${INFO} AI_VERSION=v${LINSTALL}" else PTXT "${INFO} AI_VERSION=v${LINSTALL}, but a New Minor Update is Available!" \ "${INFO} Run Option 1 of the Installer to upgrade Asuswrt-Merlin-AdGuardHome-Installer." AUTO_UPDATE="${INSTALLER_ACTION_UPDATE:-update}" fi if [ "${REMOTE_ADGUARD_VER}" != "${ADGUARD_VER}" ] || [ "${ADGUARD_BRANCH}" = "edge" ]; then case "${ADGUARD_BRANCH}" in release) PTXT "${INFO} New RELEASE ADGUARDHOME_VER=${REMOTE_ADGUARD_VER} Available!" ;; beta) PTXT "${INFO} New BETA ADGUARDHOME_VER=${REMOTE_ADGUARD_VER} Available!" ;; edge) if [ "${REMOTE_ADGUARD_VER}" = "${ADGUARD_VER}" ]; then PTXT "${INFO} ADGUARDHOME_BUILD=Edge" \ "${INFO} Edge builds can refresh without a version change." else PTXT "${INFO} New EDGE ADGUARDHOME_VER=${REMOTE_ADGUARD_VER} Available!" fi ;; esac AUTO_UPDATE="${INSTALLER_ACTION_UPDATE:-update}" else case "${ADGUARD_BRANCH}" in release) PTXT "${INFO} ADGUARDHOME_BUILD=Release" ;; beta) PTXT "${INFO} ADGUARDHOME_BUILD=Beta" ;; edge) PTXT "${INFO} ADGUARDHOME_BUILD=Edge" ;; esac fi # A new AdGuardHome installation intentionally exposes its setup WebUI over # HTTP on the router LAN until the administrator configures HTTPS in AdGuardHome. ADGUARD_WEB_URL="$(printf '%s://%s:%s' 'http' "$(installer_nvram get lan_ipaddr)" "$(awk -F: '/.*address:.*/{ print $3 }' "${YAML_FILE}")")" # NOSONAR PTXT "${INFO} ADGUARDHOME_VER=${ADGUARD_VER}" \ "${INFO} Run Option 1 to if you want to change builds or upgrade when updates are available." \ "${INFO} To visit AdGuardHome," \ "${INFO} please go to ${ADGUARD_WEB_URL}." \ "${INFO} You can use the WebUI to change things to your liking!" \ "${INFO} Stop by https://github.com/AdguardTeam/AdGuardHome/wiki " \ "${INFO} for any configuration needs. " elif [ -z "${LINSTALL}" ] && [ -z "${RMNSTALL}" ] && [ -z "$1" ]; then if check_connection 300; then check_version x else PTXT "${ERROR} Timed out waiting for an Internet connection." fi else check_version x x fi fi } choose_branch() { if [ "$1" = "1" ]; then ADGUARD_BRANCH_OLD="$(conf_value ADGUARD_BRANCH)" export ADGUARD_BRANCH_OLD del_conf ADGUARD_BRANCH fi local BUILD if [ "${CLI_MODE:-0}" = "1" ] && [ -n "$(conf_value ADGUARD_BRANCH)" ]; then return 0 fi if [ ! -f "${CONF_FILE}" ] || [ -z "$(conf_value ADGUARD_BRANCH)" ]; then PTXT "${INFO} Choose which build of AdGuardHome to install:" \ " 1) Release" \ " 2) Beta" \ " 3) Edge" if ! read_input_num "Select your mode" 1 3; then PTXT "${ERROR} Unable to select an AdGuardHome build." if [ -n "${ADGUARD_BRANCH_OLD:-}" ]; then write_conf ADGUARD_BRANCH "\"${ADGUARD_BRANCH_OLD}\"" fi return 1 fi case "${CHOSEN}" in 1) BUILD=release write_conf ADGUARD_BRANCH "\"${BUILD}\"" ;; 2) BUILD=beta write_conf ADGUARD_BRANCH "\"${BUILD}\"" ;; 3) BUILD=edge write_conf ADGUARD_BRANCH "\"${BUILD}\"" ;; esac if [ -n "${ADGUARD_BRANCH_OLD:-}" ] && [ "${ADGUARD_BRANCH_OLD}" != "${BUILD}" ]; then ADGUARD_BRANCH_CHANGED="1" export ADGUARD_BRANCH_CHANGED fi return 0 else read_yesno "Do you want to switch AdGuardHome builds?" CONFIRM_STATUS=$? if [ "${CONFIRM_STATUS}" -eq 0 ]; then choose_branch 1 elif [ "${CONFIRM_STATUS}" -eq 1 ]; then PTXT "${INFO} continuing without changing builds." return 3 else return "${CONFIRM_STATUS}" fi fi } # Installer file and setup helpers adguard_archive_is_safe() { local ARCHIVE REQUIRE_STATE ARCHIVE="$1" REQUIRE_STATE="${2:-0}" case "${REQUIRE_STATE}" in 1) REQUIRE_STATE="1" ;; *) REQUIRE_STATE="0" ;; esac [ -n "${ARCHIVE}" ] || return 1 # First pass: # Validate tar can list verbosely, reject unsafe file types, # and reject unsafe symlink targets. if ! tar -tzvf "${ARCHIVE}" 2>/dev/null | awk -v require_state="${REQUIRE_STATE}" ' function clean_path(path) { while (path ~ /^\.\//) { sub(/^\.\//, "", path) } sub(/\/$/, "", path) return path } function path_from_verbose(text, p) { p = index(text, " ./AdGuardHome") if (p > 0) { return substr(text, p + 1) } p = index(text, " AdGuardHome") if (p > 0) { return substr(text, p + 1) } return "" } function symlink_target_is_safe(link_path, link_target, base_path, depth, i, n, part, parts) { base_path = link_path sub(/\/[^\/]*$/, "", base_path) n = split(base_path "/" link_target, parts, "/") depth = 0 for (i = 1; i <= n; i++) { part = parts[i] if (part == "" || part == ".") { continue } if (part == "..") { depth-- if (depth < 1) { return 0 } continue } depth++ if (depth == 1 && part != "AdGuardHome") { return 0 } } return depth >= 1 } BEGIN { entries = 0 data_dir = 0 } { type = substr($0, 1, 1) if (type != "-" && type != "d" && type != "l") { exit 1 } entries = 1 if (type == "l") { line = $0 arrow_count = 0 while ((arrow_pos = index(line, " -> ")) > 0) { arrow_count++ line = substr(line, arrow_pos + 4) } if (arrow_count != 1) { exit 1 } arrow_pos = index($0, " -> ") link_path = substr($0, 1, arrow_pos - 1) link_target = substr($0, arrow_pos + 4) link_path = clean_path(path_from_verbose(link_path)) if (link_path == "") { exit 1 } if (link_path == "AdGuardHome/AdGuardHome") { exit 1 } if (require_state && (link_path == "AdGuardHome/AdGuardHome.yaml" || link_path == "AdGuardHome/.AdGuardHome.yaml.ori")) { exit 1 } if (require_state && link_path == "AdGuardHome/data") { exit 1 } if (link_target == "" || link_target ~ /^\//) { exit 1 } if (!symlink_target_is_safe(link_path, link_target)) { exit 1 } next } entry_path = clean_path(path_from_verbose($0)) if (entry_path == "") { exit 1 } if (entry_path == "AdGuardHome/AdGuardHome" && type != "-") { exit 1 } if (require_state && entry_path == "AdGuardHome/AdGuardHome.yaml" && type != "-") { exit 1 } if (require_state && entry_path == "AdGuardHome/.AdGuardHome.yaml.ori" && type != "-") { exit 1 } if (require_state && entry_path == "AdGuardHome/data" && type != "d") { exit 1 } if (type == "d" && entry_path == "AdGuardHome/data") { data_dir = 1 } } END { if (!entries) { exit 1 } if (require_state && !data_dir) { exit 1 } } '; then return 1 fi # Second pass: # Validate archive paths and required AdGuardHome files. tar -tzf "${ARCHIVE}" 2>/dev/null | awk -v require_state="${REQUIRE_STATE}" ' function clean_path(path) { while (path ~ /^\.\//) { sub(/^\.\//, "", path) } sub(/\/$/, "", path) return path } BEGIN { binary = 0 yaml = 0 yaml_ori = 0 data = 0 unsafe = 0 } { path = clean_path($0) if (path == "") { unsafe = 1 exit } if (path ~ /^\//) { unsafe = 1 exit } if (path ~ /(^|\/)\.\.(\/|$)/) { unsafe = 1 exit } if (path !~ /^AdGuardHome(\/|$)/) { unsafe = 1 exit } if (path == "AdGuardHome/AdGuardHome") { binary = 1 } if (path == "AdGuardHome/AdGuardHome.yaml") { yaml = 1 } if (path == "AdGuardHome/.AdGuardHome.yaml.ori") { yaml_ori = 1 } if (path == "AdGuardHome/data") { data = 1 } } END { if (unsafe) { exit 1 } if (!binary) { exit 1 } if (require_state && ((!yaml && !yaml_ori) || !data)) { exit 1 } exit 0 } ' } # adguard_restart_after_failed_replace restarts the prior service when $1 is 1 and leaves recovery state unchanged if startup fails. adguard_restart_after_failed_replace() { [ "$1" = "1" ] || return 0 if agh_start; then PTXT "${INFO} Restored the previously running AdGuardHome service." rollback_result_write "service-restart" "${INSTALLER_ROLLBACK_COMPLETE:-rollback complete}" "previous AdGuardHome service restarted" ADGUARD_INSTALL_RESTART_PENDING="0" ADGUARD_INSTALL_WAS_RUNNING="0" return 0 fi rollback_result_write "AdGuardHome service restart" "restart-failed" "previous binary restored" rollback_result_notice PTXT "${ERROR} The previous AdGuardHome binary was restored but could not be restarted." rollback_result_write "service-restart" "${INSTALLER_ROLLBACK_PARTIAL:-rollback partial}" "previous AdGuardHome binary restored but service restart failed" return 1 } # adguard_restart_after_install_abort restores any active binary replacement and restarts the service when $1 indicates it was previously running. adguard_restart_after_install_abort() { local OLD_BINARY if [ "${ADGUARD_INSTALL_RESTART_PENDING:-0}" = "1" ]; then if ! adguard_restart_after_failed_replace 1; then return 1 fi adguard_install_abort_trap_disable_preserve_defer return 0 fi if [ "${ADGUARD_INSTALL_REPLACE_ACTIVE:-0}" = "1" ] && [ -n "${ADGUARD_INSTALL_OLD_BINARY:-}" ]; then OLD_BINARY="${ADGUARD_INSTALL_OLD_BINARY}" if ! adguard_restore_after_failed_replace "${OLD_BINARY}" "$1"; then return 1 fi adguard_install_abort_trap_disable_preserve_defer return 0 fi adguard_install_abort_trap_disable_preserve_defer [ "$1" = "1" ] || return 0 if agh_is_running; then return 0 fi if agh_start && agh_wait_started; then PTXT "${INFO} Restarted AdGuardHome after the installer aborted." rollback_result_write "install-abort" "${INSTALLER_ROLLBACK_COMPLETE:-rollback complete}" "AdGuardHome restarted after abort" return 0 fi rollback_result_write "installer abort" "restart-failed" "AdGuardHome could not be restarted" rollback_result_notice PTXT "${ERROR} AdGuardHome could not be restarted after the installer aborted." rollback_result_write "install-abort" "${INSTALLER_ROLLBACK_PARTIAL:-rollback partial}" "AdGuardHome could not be restarted after abort" return 1 } adguard_recover_after_event_hook_abort() { local MODE_ROLLBACK_STATUS NVRAM_ROLLBACK_STATUS RESTART_RECOVERY_STATUS MODE_ROLLBACK_STATUS=0 NVRAM_ROLLBACK_STATUS=0 RESTART_RECOVERY_STATUS=0 if [ "${2:-0}" = "1" ]; then if ! agh_stop; then PTXT "${ERROR} Unable to stop AdGuardHome; retaining the recovery snapshot and current installation." return 1 fi fi if nvram_transaction_lock_owned && ! nvram_transaction_setup_committed; then setup_restore_nvram_journal || NVRAM_ROLLBACK_STATUS=1 installer_lan_domain_restore || NVRAM_ROLLBACK_STATUS=1 restore_dns_filter_settings || NVRAM_ROLLBACK_STATUS=1 # The persisted snapshot is authoritative if an earlier failure left the # in-memory saved-state marker unset or stale. check_dns_environment 1 || NVRAM_ROLLBACK_STATUS=1 fi if [ -n "${MODE_MIGRATION_YAML_FILE_BACKUP:-}" ]; then if rollback_pending_mode_migration; then if [ "${3:-0}" != "1" ]; then all_event_scripts_transaction_detach_after_mode_rollback fi else MODE_ROLLBACK_STATUS=1 fi fi # Restore the previously running AdGuardHome service even when mode rollback fails. adguard_restart_after_install_abort "$1" || RESTART_RECOVERY_STATUS=1 [ "${MODE_ROLLBACK_STATUS}" -eq 0 ] || PTXT "${ERROR} Unable to roll back the pending mode migration." [ "${NVRAM_ROLLBACK_STATUS}" -eq 0 ] || PTXT "${ERROR} Unable to restore the installer-managed NVRAM settings after the installation abort." [ "${RESTART_RECOVERY_STATUS}" -eq 0 ] || PTXT "${ERROR} Unable to restore the AdGuardHome service after the installation abort." [ "${MODE_ROLLBACK_STATUS}" -eq 0 ] && [ "${NVRAM_ROLLBACK_STATUS}" -eq 0 ] && [ "${RESTART_RECOVERY_STATUS}" -eq 0 ] } adguard_recover_after_uninstall_hook_abort() { local DOMAIN_RESTORE_STATUS HOOK_ROLLBACK_STATUS RESTART_RECOVERY_STATUS DOMAIN_RESTORE_STATUS=0 HOOK_ROLLBACK_STATUS=0 RESTART_RECOVERY_STATUS=0 all_event_scripts_transaction_rollback || HOOK_ROLLBACK_STATUS=1 if [ "${2:-0}" = "1" ]; then installer_lan_domain_restore_uninstall || DOMAIN_RESTORE_STATUS=1 fi # Restore the service only when it was running before the uninstall stopped it. if [ "${1:-0}" = "1" ]; then agh_start || RESTART_RECOVERY_STATUS=1 fi [ "${HOOK_ROLLBACK_STATUS}" -eq 0 ] || PTXT "${ERROR} Unable to restore installer-managed event scripts after the uninstall abort." [ "${DOMAIN_RESTORE_STATUS}" -eq 0 ] || PTXT "${ERROR} Unable to restore the LAN domain after the uninstall abort." [ "${RESTART_RECOVERY_STATUS}" -eq 0 ] || PTXT "${ERROR} Unable to restart AdGuardHome after the uninstall abort." [ "${HOOK_ROLLBACK_STATUS}" -eq 0 ] && [ "${DOMAIN_RESTORE_STATUS}" -eq 0 ] && [ "${RESTART_RECOVERY_STATUS}" -eq 0 ] } # adguard_install_signal_traps_disable removes installation signal handlers without clearing recovery state. adguard_install_signal_traps_disable() { trap - HUP INT QUIT ABRT TERM } # adguard_install_abort_trap_disable removes installation signal handlers and clears all tracked install and restore recovery state. adguard_install_abort_trap_disable() { [ "${ADGUARD_SERVICE_STAGE_OWNED:-0}" != "1" ] || adguard_service_bundle_cleanup [ "${ADGUARD_SERVICE_PUBLISH_OWNED:-0}" != "1" ] || adguard_service_publish_cleanup adguard_install_signal_traps_disable ADGUARD_INSTALL_REPLACE_ACTIVE="0" ADGUARD_INSTALL_OLD_BINARY="" ADGUARD_INSTALL_STAGE_DIR="" ADGUARD_INSTALL_WAS_RUNNING="0" ADGUARD_INSTALL_RESTART_PENDING="0" ADGUARD_INSTALL_COMMIT_PENDING="0" ADGUARD_RESTORE_ACTIVE="0" ADGUARD_RESTORE_ROLLBACK_DIR="" ADGUARD_RESTORE_STAGE_DIR="" ADGUARD_RESTORE_TARG_DIR="" ADGUARD_RESTORE_TARGET_INSTALLED="0" ADGUARD_TIMEZONE_TMP="" } adguard_install_abort_trap_disable_preserve_defer() { local DEFER_END_OP RESTORE_ACTIVE RESTORE_ROLLBACK_DIR RESTORE_STAGE_DIR RESTORE_TARGET_INSTALLED RESTORE_TARG_DIR WAS_RUNNING DEFER_END_OP="${ADGUARD_DEFER_END_OP:-0}" WAS_RUNNING="${ADGUARD_INSTALL_WAS_RUNNING:-0}" RESTORE_ACTIVE="${ADGUARD_RESTORE_ACTIVE:-0}" RESTORE_ROLLBACK_DIR="${ADGUARD_RESTORE_ROLLBACK_DIR:-}" RESTORE_STAGE_DIR="${ADGUARD_RESTORE_STAGE_DIR:-}" RESTORE_TARG_DIR="${ADGUARD_RESTORE_TARG_DIR:-}" RESTORE_TARGET_INSTALLED="${ADGUARD_RESTORE_TARGET_INSTALLED:-0}" adguard_install_abort_trap_disable ADGUARD_DEFER_END_OP="${DEFER_END_OP}" if [ "${DEFER_END_OP}" = "1" ] && [ "${RESTORE_ACTIVE}" = "1" ]; then ADGUARD_INSTALL_WAS_RUNNING="${WAS_RUNNING}" ADGUARD_RESTORE_ACTIVE="${RESTORE_ACTIVE}" ADGUARD_RESTORE_ROLLBACK_DIR="${RESTORE_ROLLBACK_DIR}" ADGUARD_RESTORE_STAGE_DIR="${RESTORE_STAGE_DIR}" ADGUARD_RESTORE_TARG_DIR="${RESTORE_TARG_DIR}" ADGUARD_RESTORE_TARGET_INSTALLED="${RESTORE_TARGET_INSTALLED}" trap 'adguard_install_abort_on_signal' HUP INT QUIT ABRT TERM fi } # adguard_install_abort_on_signal cleans staged files and attempts to restore interrupted event-hook, migration, install, or restore state. adguard_install_abort_on_signal() { END_OP_SIGNAL=1 local MIGRATION_ROLLED_BACK=0 MIGRATION_ROLLBACK_STATUS=0 OLD_BINARY REPLACE_ACTIVE RESTORE_ACTIVE RESTORE_ROLLBACK_DIR RESTORE_STAGE_DIR RESTORE_TARGET_INSTALLED RESTORE_TARG_DIR STAGE_DIR TIMEZONE_TMP WAS_RUNNING OLD_BINARY="${ADGUARD_INSTALL_OLD_BINARY:-}" REPLACE_ACTIVE="${ADGUARD_INSTALL_REPLACE_ACTIVE:-0}" STAGE_DIR="${ADGUARD_INSTALL_STAGE_DIR:-}" WAS_RUNNING="${ADGUARD_INSTALL_WAS_RUNNING:-0}" RESTORE_ACTIVE="${ADGUARD_RESTORE_ACTIVE:-0}" RESTORE_ROLLBACK_DIR="${ADGUARD_RESTORE_ROLLBACK_DIR:-}" RESTORE_STAGE_DIR="${ADGUARD_RESTORE_STAGE_DIR:-}" RESTORE_TARG_DIR="${ADGUARD_RESTORE_TARG_DIR:-}" RESTORE_TARGET_INSTALLED="${ADGUARD_RESTORE_TARGET_INSTALLED:-0}" TIMEZONE_TMP="${ADGUARD_TIMEZONE_TMP:-}" adguard_install_signal_traps_disable [ "${ADGUARD_SERVICE_STAGE_OWNED:-0}" != "1" ] || adguard_service_bundle_cleanup [ "${ADGUARD_SERVICE_PUBLISH_OWNED:-0}" != "1" ] || adguard_service_publish_cleanup if [ "${ADGUARD_INSTALL_COMMIT_PENDING:-0}" = "1" ] && [ -z "${EVENT_SCRIPTS_ACTIVE_SNAPSHOT:-}" ] && [ "${REPLACE_ACTIVE}" = "1" ] && [ -n "${OLD_BINARY}" ]; then adguard_committed_binary_cleanup_finalize "${OLD_BINARY}" || true REPLACE_ACTIVE=0 OLD_BINARY="" fi if [ -n "${EVENT_SCRIPTS_ACTIVE_SNAPSHOT:-}" ]; then all_event_scripts_transaction_rollback || PTXT "${ERROR} Unable to restore interrupted event-hook transaction; recovery snapshot retained at ${EVENT_SCRIPTS_ACTIVE_SNAPSHOT}." fi [ -z "${STAGE_DIR}" ] || rm -rf "${STAGE_DIR}" [ -z "${TIMEZONE_TMP}" ] || rm -f "${TIMEZONE_TMP}" # Restore setup's pre-migration file journal before rolling back migration state. # EXIT cleanup will then observe no journal and cannot republish migrated files. if [ -n "${MODE_MIGRATION_YAML_FILE_BACKUP:-}" ]; then if ! setup_restore_nvram_journal; then MIGRATION_ROLLBACK_STATUS=1 PTXT "${ERROR} Unable to restore the journaled installer configuration files." elif rollback_pending_mode_migration; then MIGRATION_ROLLED_BACK=1 all_event_scripts_transaction_detach_after_mode_rollback else MIGRATION_ROLLBACK_STATUS=1 fi fi if [ "${MIGRATION_ROLLBACK_STATUS}" -ne 0 ]; then PTXT "${ERROR} Unable to roll back the interrupted mode migration; attempting previous installation recovery." fi # A migrated process may still be running even when journal or migration # rollback failed. Stop it before recovering the known-good installation. if [ -n "${MODE_MIGRATION_YAML_FILE_BACKUP:-}" ] || [ "${MIGRATION_ROLLED_BACK}" -ne 0 ]; then agh_stop fi if [ "${RESTORE_ACTIVE}" = "1" ]; then adguard_restore_after_failed_directory_restore "${RESTORE_ROLLBACK_DIR}" "${RESTORE_TARG_DIR}" "${RESTORE_STAGE_DIR}" "${WAS_RUNNING}" "${RESTORE_TARGET_INSTALLED}" elif [ "${REPLACE_ACTIVE}" = "1" ] && [ -n "${OLD_BINARY}" ]; then adguard_restore_after_failed_replace "${OLD_BINARY}" "${WAS_RUNNING}" else adguard_restart_after_install_abort "${WAS_RUNNING}" fi clear_screen ADGUARD_DEFER_END_OP="0" end_op_message 2 } # adguard_install_abort_trap_enable records whether AdGuardHome was running in $1, tracks the optional staging directory in $2, and installs recovery handlers. adguard_install_abort_trap_enable() { ADGUARD_INSTALL_REPLACE_ACTIVE="0" ADGUARD_INSTALL_OLD_BINARY="" ADGUARD_INSTALL_WAS_RUNNING="$1" ADGUARD_INSTALL_STAGE_DIR="${2:-}" ADGUARD_INSTALL_RESTART_PENDING="0" ADGUARD_INSTALL_COMMIT_PENDING="0" ADGUARD_RESTORE_ACTIVE="0" trap 'adguard_install_abort_on_signal' HUP INT QUIT ABRT TERM } # adguard_restore_abort_trap_enable tracks the rollback, target, optional staging, and prior-running values from $1 through $4 and installs recovery handlers. adguard_restore_abort_trap_enable() { ADGUARD_INSTALL_REPLACE_ACTIVE="0" ADGUARD_INSTALL_OLD_BINARY="" ADGUARD_INSTALL_STAGE_DIR="" ADGUARD_INSTALL_RESTART_PENDING="0" ADGUARD_INSTALL_WAS_RUNNING="${4:-0}" ADGUARD_RESTORE_ACTIVE="1" ADGUARD_RESTORE_ROLLBACK_DIR="$1" ADGUARD_RESTORE_TARG_DIR="$2" ADGUARD_RESTORE_STAGE_DIR="${3:-}" ADGUARD_RESTORE_TARGET_INSTALLED="0" trap 'adguard_install_abort_on_signal' HUP INT QUIT ABRT TERM } # adguard_restore_after_failed_replace stops the replacement, restores the backup at $1 when available, and restarts it when the prior-running flag in $2 is 1. # A failed stop or restart leaves the corresponding recovery state available for a later attempt. adguard_restore_after_failed_replace() { local OLD_BINARY WAS_RUNNING OLD_BINARY="$1" WAS_RUNNING="$2" if [ "$(agh_process_count)" -gt 0 ] && ! agh_stop; then rollback_result_write "${INSTALLER_BINARY_ROLLBACK:-AdGuardHome binary rollback}" "${INSTALLER_RESTORE_FAILED:-restore-failed}" "could not stop AdGuardHome" PTXT "${ERROR} Unable to stop AdGuardHome before restoring the previous binary." return 1 fi if [ -f "${OLD_BINARY}" ]; then if ! mv "${OLD_BINARY}" "${AGH_FILE}"; then rollback_result_write "${INSTALLER_BINARY_ROLLBACK:-AdGuardHome binary rollback}" "${INSTALLER_RESTORE_FAILED:-restore-failed}" "${OLD_BINARY}" rollback_result_notice PTXT "${ERROR} Unable to restore the previous AdGuardHome binary; it remains at ${OLD_BINARY}." rollback_result_write "binary-replace" "${INSTALLER_ROLLBACK_FAILED:-rollback failed}" "previous binary remains at ${OLD_BINARY}" return 1 fi rollback_result_write "${INSTALLER_BINARY_ROLLBACK:-AdGuardHome binary rollback}" "restored" "${AGH_FILE}" else if ! rm -f "${AGH_FILE}"; then rollback_result_write "${INSTALLER_BINARY_ROLLBACK:-AdGuardHome binary rollback}" "cleanup-failed" "${AGH_FILE}" rollback_result_notice PTXT "${ERROR} Unable to remove the failed AdGuardHome binary." rollback_result_write "binary-replace" "${INSTALLER_ROLLBACK_FAILED:-rollback failed}" "failed binary could not be removed" return 1 fi if [ "${WAS_RUNNING}" = "1" ]; then rollback_result_write "${INSTALLER_BINARY_ROLLBACK:-AdGuardHome binary rollback}" "${INSTALLER_RESTORE_FAILED:-restore-failed}" "previous binary unavailable" rollback_result_notice PTXT "${ERROR} The previous AdGuardHome binary is unavailable for rollback." rollback_result_write "binary-replace" "${INSTALLER_ROLLBACK_UNAVAILABLE:-rollback unavailable}" "previous binary backup is missing" return 1 fi rollback_result_write "${INSTALLER_BINARY_ROLLBACK:-AdGuardHome binary rollback}" "removed-failed-install" "${AGH_FILE}" fi rollback_result_write "binary-replace" "${INSTALLER_ROLLBACK_COMPLETE:-rollback complete}" "previous AdGuardHome binary restored" ADGUARD_INSTALL_REPLACE_ACTIVE="0" ADGUARD_INSTALL_OLD_BINARY="" ADGUARD_INSTALL_STAGE_DIR="" if [ "${WAS_RUNNING}" = "1" ]; then ADGUARD_INSTALL_RESTART_PENDING="1" ADGUARD_INSTALL_WAS_RUNNING="1" adguard_restart_after_failed_replace 1 return $? fi ADGUARD_INSTALL_RESTART_PENDING="0" ADGUARD_INSTALL_WAS_RUNNING="0" return 0 } # adguard_committed_binary_cleanup_path_valid accepts only a numeric .AdGuardHome.previous suffix directly beneath TARG_DIR. adguard_committed_binary_cleanup_path_valid() { local CLEANUP_PATH CLEANUP_SUFFIX CLEANUP_PATH="$1" case "${CLEANUP_PATH}" in "${TARG_DIR}"/.AdGuardHome.previous.*) CLEANUP_SUFFIX="${CLEANUP_PATH##*.AdGuardHome.previous.}" case "${CLEANUP_SUFFIX}" in "" | *[!0-9]*) return 1 ;; esac [ "${CLEANUP_PATH%/*}" = "${TARG_DIR}" ] ;; *) return 1 ;; esac } # adguard_committed_binary_cleanup_record_write atomically records the validated backup path in $1 for cleanup after commit. adguard_committed_binary_cleanup_record_write() { local CLEANUP_MARKER CLEANUP_PATH CLEANUP_STAGE CLEANUP_PATH="$1" adguard_committed_binary_cleanup_path_valid "${CLEANUP_PATH}" || return 1 CLEANUP_MARKER="${BASE_DIR}/.AdGuardHome.binary-cleanup" CLEANUP_STAGE="${CLEANUP_MARKER}.$$" [ ! -e "${CLEANUP_MARKER}" ] && [ ! -L "${CLEANUP_MARKER}" ] || return 1 [ ! -e "${CLEANUP_STAGE}" ] && [ ! -L "${CLEANUP_STAGE}" ] || return 1 (umask 077 && set -C && printf '%s\n' "${CLEANUP_PATH}" >"${CLEANUP_STAGE}") && chmod 600 "${CLEANUP_STAGE}" && mv -f "${CLEANUP_STAGE}" "${CLEANUP_MARKER}" || { rm -f "${CLEANUP_STAGE}" return 1 } } # adguard_committed_binary_cleanup_retry validates a pending cleanup record, then removes its committed backup and the record. adguard_committed_binary_cleanup_retry() { local CLEANUP_MARKER CLEANUP_PATH CLEANUP_MARKER="${BASE_DIR}/.AdGuardHome.binary-cleanup" [ -e "${CLEANUP_MARKER}" ] || [ -L "${CLEANUP_MARKER}" ] || return 0 [ -f "${CLEANUP_MARKER}" ] && [ ! -L "${CLEANUP_MARKER}" ] || { PTXT "${ERROR} Unsafe committed-binary cleanup record retained at ${CLEANUP_MARKER}." return 1 } IFS= read -r CLEANUP_PATH <"${CLEANUP_MARKER}" || CLEANUP_PATH="" if /bin/sed -n '2p' "${CLEANUP_MARKER}" | /bin/grep -q . || ! adguard_committed_binary_cleanup_path_valid "${CLEANUP_PATH}"; then PTXT "${ERROR} Invalid committed-binary cleanup record retained at ${CLEANUP_MARKER}; no file was removed." return 1 fi if ! rm -f "${CLEANUP_PATH}"; then PTXT "${ERROR} Unable to remove committed binary backup ${CLEANUP_PATH}; cleanup record retained at ${CLEANUP_MARKER}." return 1 fi if ! rm -f "${CLEANUP_MARKER}"; then PTXT "${ERROR} Removed committed binary backup ${CLEANUP_PATH}, but cleanup record removal failed at ${CLEANUP_MARKER}." return 1 fi return 0 } # adguard_committed_binary_cleanup_finalize disarms rollback and removes or durably records the committed backup in $1. adguard_committed_binary_cleanup_finalize() { local COMMITTED_OLD_BINARY COMMITTED_OLD_BINARY="$1" [ -n "${COMMITTED_OLD_BINARY}" ] || return 0 ADGUARD_INSTALL_COMMIT_PENDING="1" ADGUARD_COMMITTED_BINARY_CLEANUP_PENDING="${COMMITTED_OLD_BINARY}" if adguard_committed_binary_cleanup_record_write "${COMMITTED_OLD_BINARY}"; then ADGUARD_INSTALL_REPLACE_ACTIVE="0" ADGUARD_INSTALL_OLD_BINARY="" ADGUARD_INSTALL_COMMIT_PENDING="0" ADGUARD_COMMITTED_BINARY_CLEANUP_PENDING="" adguard_install_signal_traps_disable adguard_committed_binary_cleanup_retry || return 1 elif rm -f "${COMMITTED_OLD_BINARY}"; then ADGUARD_INSTALL_REPLACE_ACTIVE="0" ADGUARD_INSTALL_OLD_BINARY="" ADGUARD_INSTALL_COMMIT_PENDING="0" ADGUARD_COMMITTED_BINARY_CLEANUP_PENDING="" adguard_install_signal_traps_disable else PTXT "${ERROR} Unable to create ${BASE_DIR}/.AdGuardHome.binary-cleanup or remove retained committed binary backup ${COMMITTED_OLD_BINARY}." return 1 fi return 0 } adguard_restore_after_failed_directory_restore() { local COMMAND_LINK ROLLBACK_DIR STAGE_DIR TARGET_DIR TARGET_INSTALLED WAS_RUNNING ROLLBACK_DIR="$1" TARGET_DIR="$2" STAGE_DIR="$3" WAS_RUNNING="${4:-0}" TARGET_INSTALLED="${5:-1}" COMMAND_LINK="${ADGUARD_COMMAND_LINK_PATH:-/opt/sbin/AdGuardHome}" [ -z "${STAGE_DIR}" ] || rm -rf "${STAGE_DIR}" if agh_is_running; then PTXT "${INFO} Stopping AdGuardHome before restoring the previous installation." if ! agh_stop; then rollback_result_write "${INSTALLER_DIRECTORY_ROLLBACK:-AdGuardHome directory rollback}" "${INSTALLER_RESTORE_FAILED:-restore-failed}" "could not stop AdGuardHome" rollback_result_notice PTXT "${ERROR} Unable to safely stop AdGuardHome before restoring the previous installation." rollback_result_write "directory-restore" "${INSTALLER_ROLLBACK_FAILED:-rollback failed}" "could not stop AdGuardHome before restore" return 1 fi fi if [ -d "${ROLLBACK_DIR}" ] || [ "${TARGET_INSTALLED}" = "1" ]; then rm -rf "${TARGET_DIR}" if [ ! -d "${ROLLBACK_DIR}" ]; then ADGUARD_RESTORE_TARGET_INSTALLED="0" fi fi if [ -d "${ROLLBACK_DIR}" ]; then if ! mv "${ROLLBACK_DIR}" "${TARGET_DIR}"; then rollback_result_write "${INSTALLER_DIRECTORY_ROLLBACK:-AdGuardHome directory rollback}" "${INSTALLER_RESTORE_FAILED:-restore-failed}" "${ROLLBACK_DIR}" rollback_result_notice PTXT "${ERROR} Unable to restore the previous AdGuardHome installation; it remains at ${ROLLBACK_DIR}." rollback_result_write "directory-restore" "${INSTALLER_ROLLBACK_FAILED:-rollback failed}" "previous installation remains at ${ROLLBACK_DIR}" return 1 fi rollback_result_write "${INSTALLER_DIRECTORY_ROLLBACK:-AdGuardHome directory rollback}" "restored" "${TARGET_DIR}" ADGUARD_RESTORE_ROLLBACK_DIR="" ADGUARD_RESTORE_TARGET_INSTALLED="0" ADGUARD_RESTORE_ACTIVE="0" elif [ -L "${COMMAND_LINK}" ] && [ "$(readlink "${COMMAND_LINK}")" = "${TARGET_DIR}/AdGuardHome" ]; then if ! rm -f "${COMMAND_LINK}"; then rollback_result_write "${INSTALLER_DIRECTORY_ROLLBACK:-AdGuardHome directory rollback}" "cleanup-failed" "${COMMAND_LINK}" rollback_result_notice PTXT "${ERROR} Unable to remove the failed AdGuardHome command symlink." rollback_result_write "directory-restore" "${INSTALLER_ROLLBACK_FAILED:-rollback failed}" "could not remove failed symlink ${COMMAND_LINK}" return 1 fi rollback_result_write "${INSTALLER_DIRECTORY_ROLLBACK:-AdGuardHome directory rollback}" "removed-failed-install" "${COMMAND_LINK}" ADGUARD_RESTORE_TARGET_INSTALLED="0" ADGUARD_RESTORE_ACTIVE="0" fi if [ ! -d "${ROLLBACK_DIR}" ] && [ "${ADGUARD_RESTORE_TARGET_INSTALLED:-0}" = "0" ]; then ADGUARD_RESTORE_ACTIVE="0" fi rm -rf "${STAGE_DIR}" adguard_restart_after_failed_replace "${WAS_RUNNING}" return 0 } adguardhome_yaml_ipset_file() { local yaml_target yaml_target="${1:-${YAML_FILE}}" [ -f "${yaml_target}" ] || return 0 awk ' function indentation(line, text) { text = line; sub(/[^[:space:]].*$/, "", text); return length(text) } function block_marker(value) { gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) return value ~ /^[>|]([+-]|[+-]?[1-9]|[1-9][+-]?)?([[:space:]]+#.*)?$/ } function block_scalar(parent_indent, line, text) { while ((getline line) > 0) { if (line ~ /^[[:space:]]*($|#)/) continue if (indentation(line) <= parent_indent) exit text = line gsub(/^[[:space:]]+|[[:space:]]+$/, "", text) print text exit } } function scalar(value, ch, decoded, i, next_ch, quote, rest) { gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) quote = substr(value, 1, 1) if (quote != "\"" && quote != "\047") { sub(/[[:space:]]+#.*$/, "", value) gsub(/[[:space:]]+$/, "", value) if (value ~ /^(~|null|Null|NULL)$/) return "" return value } decoded = "" for (i = 2; i <= length(value); i++) { ch = substr(value, i, 1) next_ch = substr(value, i + 1, 1) if (quote == "\"" && ch == "\\") { if (next_ch == "\"" || next_ch == "\\" || next_ch == "/" || next_ch == " ") { decoded = decoded next_ch i++ continue } exit 1 } if (quote == "\047" && ch == quote && next_ch == quote) { decoded = decoded quote i++ continue } if (ch == quote) { rest = substr(value, i + 1) if (rest !~ /^[[:space:]]*(#.*)?$/) exit 1 return decoded } decoded = decoded ch } exit 1 } /^(dns|\047dns\047|"dns"):[[:space:]]*(&[^][{},[:space:]]+[[:space:]]*)?(#.*)?$/ { in_dns = 1; next } /^(dns|\047dns\047|"dns"):/ { exit 1 } in_dns && /^[^[:space:]]/ { exit } in_dns && /^[[:space:]]*($|#)/ { next } in_dns && !child_indent { child_indent = indentation($0) } in_dns && indentation($0) == child_indent && substr($0, child_indent + 1) ~ /^(ipset_file|\047ipset_file\047|"ipset_file"):[[:space:]]*/ { value = substr($0, child_indent + 1) sub(/^(ipset_file|\047ipset_file\047|"ipset_file"):[[:space:]]*/, "", value) if (block_marker(value)) { block_scalar(child_indent) exit } print scalar(value) exit } ' "${yaml_target}" } ipset_status_path() { case "$1" in "") return 1 ;; /*) printf '%s\n' "$1" ;; *) printf '%s\n' "${TARG_DIR}/$1" ;; esac } ipset_status_sets() { local file file="$1" [ -f "${file}" ] || return 0 awk ' /^[[:space:]]*(#|$)/ { next } { line = $0 sub(/[[:space:]]+#.*$/, "", line) gsub(/^[[:space:]]+|[[:space:]]+$/, "", line) if (line == "") next slash = 0 for (i = length(line); i > 0; i--) { if (substr(line, i, 1) == "/") { slash = i break } } if (!slash) next sets = substr(line, slash + 1) n = split(sets, set, ",") for (i = 1; i <= n; i++) { gsub(/^[[:space:]]+|[[:space:]]+$/, "", set[i]) if (set[i] == "") continue print set[i] } } ' "${file}" } ipset_status_set_family() { ipset list "$1" 2>/dev/null | awk ' /^Header:/ { for (i = 1; i <= NF; i++) { if ($i == "family" && (i + 1) <= NF) { print $(i + 1) exit } } } ' } ipset_status_report_set() { local family set set="$1" family="$(ipset_status_set_family "${set}")" if [ -z "${family}" ]; then PTXT "${WARNING} IPSET set missing: ${set}" return 0 fi case "${family}" in inet) PTXT "${INFO} IPSET set exists: ${set} (family ${family})" PTXT "${INFO} IPv4 set-family check: ${set} OK (family ${family})" PTXT "${WARNING} IPv6 set-family mismatch likely: ${set} uses family ${family}" ;; inet6) PTXT "${INFO} IPSET set exists: ${set} (family ${family})" PTXT "${WARNING} IPv4 set-family mismatch likely: ${set} uses family ${family}" PTXT "${INFO} IPv6 set-family check: ${set} OK (family ${family})" ;; *) PTXT "${WARNING} IPSET set exists with unknown family: ${set} (family ${family})" PTXT "${WARNING} IPv4 set-family check unknown: ${set} uses family ${family}" PTXT "${WARNING} IPv6 set-family check unknown: ${set} uses family ${family}" ;; esac } ipset_status() { local current_file enabled found generated_file ipset_file seen_sets set_entry yaml_file enabled="$(conf_value ADGUARD_IPSET)" case "${enabled}" in NO) PTXT "${INFO} IPSET integration enabled: NO" ;; YES) PTXT "${INFO} IPSET integration enabled: YES" ;; *) PTXT "${INFO} IPSET integration enabled: YES (default)" ;; esac yaml_file="$(adguardhome_yaml_ipset_file "${YAML_FILE}" 2>/dev/null)" if [ -n "${yaml_file}" ]; then current_file="$(ipset_status_path "${yaml_file}")" if [ "${current_file}" = "${TARG_DIR}/ipset.conf" ]; then PTXT "${INFO} AdGuardHome.yaml managed dns.ipset_file: YES (${current_file})" else PTXT "${WARNING} AdGuardHome.yaml dns.ipset_file is not installer-managed: ${current_file}" fi else current_file="" PTXT "${WARNING} AdGuardHome.yaml managed dns.ipset_file: NO" fi for ipset_file in "${TARG_DIR}/ipset.user" "${TARG_DIR}/ipset.conf"; do [ -f "${ipset_file}" ] && PTXT "${INFO} IPSET file exists: ${ipset_file}" || PTXT "${WARNING} IPSET file missing: ${ipset_file}" done if ! ai_have_cmd ipset; then PTXT "${WARNING} router-stock ipset command unavailable; set existence and family checks skipped." return 0 fi found="0" seen_sets="|" for ipset_file in "${TARG_DIR}/ipset.user" "${TARG_DIR}/ipset.conf"; do [ -f "${ipset_file}" ] || continue generated_file="$(ipset_status_sets "${ipset_file}")" [ -n "${generated_file}" ] || continue found="1" PTXT "${INFO} Referenced sets from ${ipset_file}:" while read -r set_entry; do [ -n "${set_entry}" ] || continue case "${seen_sets}" in *"|${set_entry}|"*) continue ;; esac seen_sets="${seen_sets}${set_entry}|" ipset_status_report_set "${set_entry}" done </dev/null 2>&1; then rm -f "${BACKUP_TMP}" PTXT "${ERROR} Unable to create the AdGuardHome backup archive." return 1 fi ptxt_ok "Created AdGuardHome backup archive." ptxt_step "Verifying AdGuardHome backup archive." if ! adguard_archive_is_safe "${BACKUP_TMP}" 1; then PTXT "${ERROR} The new AdGuardHome backup archive failed safety validation." PTXT "${INFO} Backup archives must contain AdGuardHome/AdGuardHome, AdGuardHome/AdGuardHome.yaml, and AdGuardHome/data." PTXT "${INFO} Archive contents:" tar -tzvf "${BACKUP_TMP}" 2>/dev/null | sed -n '1,40p' rm -f "${BACKUP_TMP}" return 1 fi ptxt_ok "Verified AdGuardHome backup archive." ptxt_step "Installing AdGuardHome backup archive." if ! chmod 600 "${BACKUP_TMP}" || ! mv -f "${BACKUP_TMP}" "${BACKUP_FILE}"; then rm -f "${BACKUP_TMP}" PTXT "${ERROR} Unable to install the new AdGuardHome backup archive." return 1 fi ptxt_ok "Installed AdGuardHome backup archive." return 0 } # install_adguard_archive validates and installs the archive in $1 while preserving any previous binary and recovery state until final commit. install_adguard_archive() { local ARCHIVE_FILE OLD_BINARY STAGE_BINARY STAGE_DIR WAS_RUNNING ARCHIVE_FILE="$1" STAGE_DIR="${BASE_DIR}/.AdGuardHome.extract.$$" STAGE_BINARY="${STAGE_DIR}/AdGuardHome/AdGuardHome" OLD_BINARY="${TARG_DIR}/.AdGuardHome.previous.$$" rm -rf "${STAGE_DIR}" rm -f "${OLD_BINARY}" adguard_install_abort_trap_enable 0 "${STAGE_DIR}" ptxt_step "Validating AdGuardHome archive layout." if ! adguard_archive_is_safe "${ARCHIVE_FILE}"; then adguard_install_abort_trap_disable rollback_result_write "package install" "${INSTALLER_ROLLBACK_UNAVAILABLE:-rollback unavailable}" "archive validation failed before changes" rollback_result_notice PTXT "${ERROR} The AdGuardHome archive contains an unsafe or incomplete file layout." return 1 fi ptxt_ok "AdGuardHome archive layout is valid." ptxt_step "Extracting AdGuardHome archive into staging." if ! create_dir "${STAGE_DIR}" || ! tar xzf "${ARCHIVE_FILE}" -C "${STAGE_DIR}" >/dev/null 2>&1; then adguard_install_abort_trap_disable rm -rf "${STAGE_DIR}" rollback_result_write "package install" "${INSTALLER_ROLLBACK_UNAVAILABLE:-rollback unavailable}" "archive extraction failed before changes" rollback_result_notice PTXT "${ERROR} Unable to extract the AdGuardHome archive into staging." return 1 fi ptxt_ok "Extracted AdGuardHome archive into staging." ptxt_step "Validating staged AdGuardHome binary." if [ ! -f "${STAGE_BINARY}" ] || [ -L "${STAGE_BINARY}" ] || ! chmod 755 "${STAGE_BINARY}" || [ -z "$("${STAGE_BINARY}" --version 2>/dev/null | cut -d" " -f4-)" ]; then adguard_install_abort_trap_disable rm -rf "${STAGE_DIR}" rollback_result_write "package install" "${INSTALLER_ROLLBACK_UNAVAILABLE:-rollback unavailable}" "staged binary validation failed before changes" rollback_result_notice PTXT "${ERROR} The staged AdGuardHome binary failed validation." return 1 fi ptxt_ok "Staged AdGuardHome binary is valid." if [ "$(agh_process_count)" -gt 0 ]; then WAS_RUNNING="1"; else WAS_RUNNING="0"; fi ADGUARD_INSTALL_WAS_RUNNING="${WAS_RUNNING}" if ! agh_prepare_binary_replace; then adguard_install_abort_trap_disable rm -rf "${STAGE_DIR}" return 1 fi ptxt_step "Preserving installed AdGuardHome binary for rollback." if [ -f "${AGH_FILE}" ] && ! cp -p "${AGH_FILE}" "${OLD_BINARY}"; then adguard_install_abort_trap_disable rm -rf "${STAGE_DIR}" PTXT "${ERROR} Unable to preserve the installed AdGuardHome binary." adguard_restart_after_failed_replace "${WAS_RUNNING}" return 1 fi ptxt_ok "Installed AdGuardHome binary preserved for rollback." ADGUARD_INSTALL_OLD_BINARY="${OLD_BINARY}" ADGUARD_INSTALL_REPLACE_ACTIVE="1" ptxt_step "Installing staged AdGuardHome binary." if ! mv "${STAGE_BINARY}" "${AGH_FILE}"; then adguard_install_abort_trap_disable rm -rf "${STAGE_DIR}" PTXT "${ERROR} Unable to install the staged AdGuardHome binary." adguard_restore_after_failed_replace "${OLD_BINARY}" "${WAS_RUNNING}" return 1 fi ptxt_ok "Staged AdGuardHome binary installed." ptxt_step "Validating installed AdGuardHome binary." if ! ensure_adguardhome_directory_permissions || [ -z "$("${AGH_FILE}" --version 2>/dev/null | cut -d" " -f4-)" ]; then adguard_install_abort_trap_disable rm -rf "${STAGE_DIR}" PTXT "${ERROR} Installed AdGuardHome binary validation failed." adguard_restore_after_failed_replace "${OLD_BINARY}" "${WAS_RUNNING}" return 1 fi ptxt_ok "Installed AdGuardHome binary is valid." rm -rf "${STAGE_DIR}" ADGUARD_INSTALL_STAGE_DIR="" return 0 } # Back up the current installation or restore a verified archive with directory rollback. backup_restore() { local backup_action backup_action="${1:-}" local COMMAND_LINK COMMAND_LINK_DIR INSTALL_STATUS MIGRATION_ROLLBACK_STATUS RESTORE_ROLLBACK_DIR RESTORE_STAGE_DIR RESTORE_TARG_DIR WAS_RUNNING if [ "${AI_NO_AUTO_UPDATE:-0}" = "1" ] && [ "${backup_action}" = "${INSTALLER_ACTION_RESTORE:-RESTORE}" ]; then installer_bundle_update_allowed || return 1 fi COMMAND_LINK="${ADGUARD_COMMAND_LINK_PATH:-/opt/sbin/AdGuardHome}" COMMAND_LINK_DIR="${COMMAND_LINK%/*}" if [ "$1" = "BACKUP" ] && [ -d "${TARG_DIR}" ] && [ -f "${AGH_FILE}" ]; then ptxt_phase "Backing up AdGuardHome." if [ -f "${BASE_DIR}/backup_AdGuardHome.tar.gz" ]; then PTXT "${INFO} There is an old backup detected." local USE_OLD read_yesno "Do you want to continue?(this will remove the old backup)" CONFIRM_STATUS=$? if [ "${CONFIRM_STATUS}" -eq 0 ]; then USE_OLD="NO" elif [ "${CONFIRM_STATUS}" -eq 1 ]; then USE_OLD="YES" else return "${CONFIRM_STATUS}" fi if [ "${USE_OLD}" = "YES" ]; then PTXT "${INFO} Leaving Old Backup." end_op_message 1 elif [ "${USE_OLD}" = "NO" ]; then PTXT "${INFO} The old backup will be replaced only after the new backup is verified." fi fi PTXT "${INFO} This operation will backup AdGuardHome(<4MB)to ENTWARE /opt/etc." \ "${INFO} Please wait a moment." if ! create_backup_archive; then [ -z "$2" ] && end_op_message 1 return 1 fi ptxt_ok "Backup complete at ${BASE_DIR}/backup_AdGuardHome.tar.gz." [ -z "$2" ] && end_op_message 0 || return 0 elif [ "$1" = "BACKUP" ]; then PTXT "${ERROR} No ${AGH_FILE} to Backup!" end_op_message 1 return 1 fi if [ -f "${BASE_DIR}/backup_AdGuardHome.tar.gz" ] && [ "${backup_action}" = "${INSTALLER_ACTION_RESTORE:-RESTORE}" ]; then ptxt_phase "Restoring AdGuardHome backup." ptxt_step "Restoring AdGuardHome from ${BASE_DIR}/backup_AdGuardHome.tar.gz; please wait." if ! adguard_archive_is_safe "${BASE_DIR}/backup_AdGuardHome.tar.gz" 1; then rollback_result_write "${INSTALLER_BACKUP_RESTORE:-backup restore}" "${INSTALLER_ROLLBACK_UNAVAILABLE:-rollback unavailable}" "unsafe backup archive refused before changes" rollback_result_notice ptxt_fail "Refusing to restore unsafe or incomplete backup ${BASE_DIR}/backup_AdGuardHome.tar.gz." return 1 fi ptxt_ok "Backup archive safety check passed." RESTORE_STAGE_DIR="${BASE_DIR}/.AdGuardHome.restore.$$" RESTORE_TARG_DIR="${RESTORE_STAGE_DIR}/AdGuardHome" RESTORE_ROLLBACK_DIR="${BASE_DIR}/.AdGuardHome.rollback.$$" if [ "$(agh_process_count)" -gt 0 ]; then WAS_RUNNING="1"; else WAS_RUNNING="0"; fi rm -rf "${RESTORE_STAGE_DIR}" "${RESTORE_ROLLBACK_DIR}" adguard_restore_abort_trap_enable "${RESTORE_ROLLBACK_DIR}" "${TARG_DIR}" "${RESTORE_STAGE_DIR}" "${WAS_RUNNING}" ptxt_step "Staging AdGuardHome backup archive." if ! create_dir "${RESTORE_STAGE_DIR}" || ! tar -xzvf "${BASE_DIR}/backup_AdGuardHome.tar.gz" -C "${RESTORE_STAGE_DIR}" >/dev/null 2>&1; then adguard_install_abort_trap_disable rm -rf "${RESTORE_STAGE_DIR}" rollback_result_write "${INSTALLER_BACKUP_RESTORE:-backup restore}" "${INSTALLER_ROLLBACK_UNAVAILABLE:-rollback unavailable}" "backup staging failed before changes" rollback_result_notice PTXT "${ERROR} Unable to stage ${BASE_DIR}/backup_AdGuardHome.tar.gz for restore." return 1 fi ptxt_ok "Staged AdGuardHome backup archive." ptxt_step "Validating staged AdGuardHome backup." if [ ! -d "${RESTORE_TARG_DIR}" ] || [ -L "${RESTORE_TARG_DIR}" ] || [ ! -f "${RESTORE_TARG_DIR}/AdGuardHome" ] || [ ! -d "${RESTORE_TARG_DIR}/data" ] || [ -L "${RESTORE_TARG_DIR}/data" ] || { [ ! -f "${RESTORE_TARG_DIR}/AdGuardHome.yaml" ] && [ ! -f "${RESTORE_TARG_DIR}/.AdGuardHome.yaml.ori" ]; }; then adguard_install_abort_trap_disable rm -rf "${RESTORE_STAGE_DIR}" rollback_result_write "${INSTALLER_BACKUP_RESTORE:-backup restore}" "${INSTALLER_ROLLBACK_UNAVAILABLE:-rollback unavailable}" "staged backup validation failed before changes" rollback_result_notice PTXT "${ERROR} The staged AdGuardHome backup is incomplete." return 1 fi ptxt_ok "Validated staged AdGuardHome backup." # Resolve and verify every online dependency before replacing the live directory. if ! adguard_restore_prepare_online_dependencies; then adguard_install_abort_trap_disable rm -rf "${RESTORE_STAGE_DIR}" return 1 fi ptxt_step "Preserving current AdGuardHome installation for rollback." if [ -e "${TARG_DIR}" ] && ! mv "${TARG_DIR}" "${RESTORE_ROLLBACK_DIR}"; then adguard_install_abort_trap_disable rm -rf "${RESTORE_STAGE_DIR}" rollback_result_write "${INSTALLER_BACKUP_RESTORE:-backup restore}" "${INSTALLER_ROLLBACK_UNAVAILABLE:-rollback unavailable}" "could not preserve current installation" rollback_result_notice PTXT "${ERROR} Unable to preserve the current AdGuardHome installation before restore." return 1 fi ptxt_ok "Current AdGuardHome installation preserved for rollback." ptxt_step "Installing staged AdGuardHome backup." if ! mv "${RESTORE_TARG_DIR}" "${TARG_DIR}"; then adguard_restore_after_failed_directory_restore "${RESTORE_ROLLBACK_DIR}" "${TARG_DIR}" "${RESTORE_STAGE_DIR}" "${WAS_RUNNING}" "1" adguard_install_abort_trap_disable PTXT "${ERROR} Unable to install the staged AdGuardHome backup." return 1 fi ptxt_ok "Installed staged AdGuardHome backup." ADGUARD_RESTORE_TARGET_INSTALLED="1" rm -rf "${RESTORE_STAGE_DIR}" # Migration rollback must describe the restored installation, not the # installation that was active before the archive was staged. PREVIOUS_ADGUARD_INSTALL_MODE="$(conf_value ADGUARD_INSTALL_MODE 2>/dev/null)" ptxt_step "Restoring AdGuardHome ownership and permissions." if ! ensure_adguardhome_directory_permissions; then adguard_restore_after_failed_directory_restore "${RESTORE_ROLLBACK_DIR}" "${TARG_DIR}" "${RESTORE_STAGE_DIR}" "${WAS_RUNNING}" "1" adguard_install_abort_trap_disable PTXT "${ERROR} Unable to restore AdGuardHome ownership or permissions." return 1 fi ptxt_ok "Restored AdGuardHome ownership and permissions." ptxt_step "Restoring AdGuardHome command symlink." if ! create_dir "${COMMAND_LINK_DIR}" || ! ln -sf "${AGH_FILE}" "${COMMAND_LINK}"; then adguard_restore_after_failed_directory_restore "${RESTORE_ROLLBACK_DIR}" "${TARG_DIR}" "${RESTORE_STAGE_DIR}" "${WAS_RUNNING}" "1" adguard_install_abort_trap_disable PTXT "${ERROR} Unable to restore the AdGuardHome command symlink." return 1 fi ptxt_ok "Restored AdGuardHome command symlink." if adguard_install_mode_confirmed && ! adguard_enforce_lan_ipset_disabled; then adguard_restore_after_failed_directory_restore "${RESTORE_ROLLBACK_DIR}" "${TARG_DIR}" "${RESTORE_STAGE_DIR}" "${WAS_RUNNING}" "1" adguard_install_abort_trap_disable PTXT "${ERROR} Unable to disable IPSET settings for restored LAN/AP/Bridge mode configuration." return 1 fi ADGUARD_DEFER_END_OP="1" ptxt_step "Reinstalling AdGuardHome service scripts after restore." inst_AdGuardHome "${1:-RESTORE}" INSTALL_STATUS=$? if [ "${INSTALL_STATUS}" -ne 0 ]; then ADGUARD_DEFER_END_OP="0" MIGRATION_ROLLBACK_STATUS=0 MIGRATION_HOOKS_RECOVERY="" if [ "${INSTALL_STATUS}" -eq 2 ]; then MIGRATION_ROLLBACK_STATUS=1 else rollback_pending_mode_migration || MIGRATION_ROLLBACK_STATUS=1 fi if [ "${MIGRATION_ROLLBACK_STATUS}" -eq 0 ]; then adguard_restore_after_failed_directory_restore "${RESTORE_ROLLBACK_DIR}" "${TARG_DIR}" "${RESTORE_STAGE_DIR}" "${WAS_RUNNING}" "1" else # Event scripts live outside TARG_DIR, so preserve their snapshot # outside the directory that is about to be replaced. A failed hook # restore must not make its only recovery copy disappear with TARG_DIR. if [ -d "${MODE_MIGRATION_HOOKS_BACKUP:-}" ]; then MIGRATION_HOOKS_RECOVERY="${RESTORE_ROLLBACK_DIR}.mode-migration-hooks.$$" rm -rf "${MIGRATION_HOOKS_RECOVERY}" if ! mv "${MODE_MIGRATION_HOOKS_BACKUP}" "${MIGRATION_HOOKS_RECOVERY}"; then MIGRATION_HOOKS_RECOVERY="" fi fi # The remaining snapshots are below TARG_DIR and are superseded by # restoring the known-good directory. Detach them so EXIT cleanup # cannot retry from paths removed by that directory replacement. MODE_MIGRATION_YAML_FILE_BACKUP="" MODE_MIGRATION_YAML_ORI_BACKUP="" MODE_MIGRATION_CONFIG_BACKUP="" MODE_MIGRATION_HOOKS_BACKUP="${MIGRATION_HOOKS_RECOVERY}" adguard_restore_after_failed_directory_restore "${RESTORE_ROLLBACK_DIR}" "${TARG_DIR}" "${RESTORE_STAGE_DIR}" "0" "1" if [ -n "${MIGRATION_HOOKS_RECOVERY}" ]; then if restore_mode_migration_wan_hooks "${MIGRATION_HOOKS_RECOVERY}"; then MODE_MIGRATION_HOOKS_BACKUP="" else PTXT "${ERROR} Unable to restore the previous event scripts; recovery snapshot retained at ${MIGRATION_HOOKS_RECOVERY}." fi fi fi adguard_install_abort_trap_disable rollback_result_notice ptxt_fail "Unable to complete AdGuardHome restore setup; restoring previous installation." [ "${MIGRATION_ROLLBACK_STATUS}" -eq 0 ] || return 2 return 1 fi ptxt_ok "Reinstalled AdGuardHome service scripts after restore." # Commit the restored directory before discarding migration snapshots so # signal cleanup cannot restore the old directory without its mode state. adguard_install_abort_trap_disable if ! finalize_pending_mode_migration; then ADGUARD_DEFER_END_OP="0" adguard_restore_after_failed_directory_restore "${RESTORE_ROLLBACK_DIR}" "${TARG_DIR}" "${RESTORE_STAGE_DIR}" "${WAS_RUNNING}" "1" PTXT "${ERROR} Unable to finalize mode migration after restore." return 1 fi ADGUARD_DEFER_END_OP="0" rm -rf "${RESTORE_ROLLBACK_DIR}" end_op_message 0 "${1:-RESTORE}" elif [ ! -f "${BASE_DIR}/backup_AdGuardHome.tar.gz" ] && [ "${backup_action}" = "${INSTALLER_ACTION_RESTORE:-RESTORE}" ]; then rollback_result_write "${INSTALLER_BACKUP_RESTORE:-backup restore}" "${INSTALLER_ROLLBACK_UNAVAILABLE:-rollback unavailable}" "backup archive missing" rollback_result_notice PTXT "${ERROR} No Backup found!" \ "${ERROR} Please make sure Backup Resides in ${BASE_DIR}" end_op_message 1 return fi } cleanup() { mv "/opt/etc/init.d/S61AdGuardHome" "/opt/etc/init.d/S99AdGuardHome" >/dev/null 2>&1 if ai_have_cmd cru && [ "$(cru l | grep -c "S99AdGuardHome")" -gt 0 ]; then cru -d "S99AdGuardHome"; fi if [ -f "${TARG_DIR}/localtime" ]; then mv "${TARG_DIR}/localtime" "${ADDON_DIR}/localtime"; fi remove_conflicting_apache } cleanup_legacy_firewall() { local failed raw_rules failed="0" if [ -x "${ADDON_DIR}/AdGuardHome.sh" ] && grep -q 'Unload_IPTables' "${ADDON_DIR}/AdGuardHome.sh"; then "${ADDON_DIR}/AdGuardHome.sh" firewall unload 2>/dev/null fi while iptables -t raw -D OUTPUT -j ADGUARDHOME 2>/dev/null; do :; done if iptables -t raw -nL ADGUARDHOME >/dev/null 2>&1; then iptables -t raw -F ADGUARDHOME 2>/dev/null || failed="1" iptables -t raw -X ADGUARDHOME 2>/dev/null || failed="1" fi del_jffs_script /jffs/scripts/firewall-start || failed="1" raw_rules="$(iptables -t raw -S 2>/dev/null)" || failed="1" if printf '%s\n' "${raw_rules}" | grep -q 'ADGUARDHOME'; then failed="1" fi if [ -f /jffs/scripts/firewall-start ] && grep -q -F "[ -x ${ADDON_DIR}/AdGuardHome.sh ]" /jffs/scripts/firewall-start; then failed="1" fi [ "${failed}" = "0" ] } legacy_firewall_cleanup_needed() { local firewall_start raw_rules firewall_start="${1:-/jffs/scripts/firewall-start}" raw_rules="$(iptables -t raw -S 2>/dev/null)" || return 0 if printf '%s\n' "${raw_rules}" | /bin/grep -q 'ADGUARDHOME'; then return 0 fi if [ -f "${firewall_start}" ] && /bin/grep -q -F "[ -x ${ADDON_DIR}/AdGuardHome.sh ]" "${firewall_start}"; then return 0 fi return 1 } wan_iptables_state_active() { local ifname key nat_rules unit nat_rules="$(/usr/sbin/iptables -t nat -S POSTROUTING 2>/dev/null)" || return 1 for unit in 0 1; do for key in ifname gw_ifname pppoe_ifname; do ifname="$(installer_nvram get "wan${unit}_${key}" 2>/dev/null)" || ifname="" case "${ifname}" in "" | *[!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.:-]*) continue ;; esac if printf '%s\n' "${nat_rules}" | /usr/bin/awk -v wan_if="${ifname}" ' function comment_ends(value, i, slashes) { if (length(value) < 2) return 0 if (substr(value, length(value), 1) != "\"") return 0 for (i = length(value) - 1; i > 0 && substr(value, i, 1) == "\\"; i--) slashes++ return slashes % 2 == 0 } { output = target = input = negated = 0 for (i = 1; i <= NF; i++) { if ($i == "--comment") { i++; if ($i ~ /^"/ && !comment_ends($i)) while (i <= NF && !comment_ends($i)) i++; continue } if (($i == "-i" || $i == "--in-interface") && i < NF) input = 1 if (($i == "-o" || $i == "--out-interface") && i < NF) { output = ($(i + 1) == wan_if) negated = (i > 1 && $(i - 1) == "!") } if ($i == "-j" && i < NF && ($(i + 1) == "MASQUERADE" || $(i + 1) == "SNAT")) target = 1 } if (output && target && !input && !negated) { found = 1; exit } } END { exit(found ? 0 : 1) } '; then return 0 fi done done return 1 } create_dir() { if ! mkdir -p "${1}"; then PTXT "${ERROR} Unable to create ${1}!" return 1 fi } del_between_magic() { local TARG MAGIC BOUNDS TARG="$1" MAGIC="$2" [ -f "${TARG}" ] || return 0 BOUNDS="$(awk -v PATT="${MAGIC}" '($0 ~ PATT) {printf NR","}' "${TARG}")" if [ "${BOUNDS}" ]; then sed -i "${BOUNDS%,}d" "${TARG}" fi } del_conf() { [ ! -f "${CONF_FILE}" ] && return local KEY for KEY in "$@"; do sed -i "/^${KEY}=.*$/d" "${CONF_FILE}" done } # del_jffs_script removes selected installer-owned manager lines while preserving unrelated shared hook commands. del_jffs_script() { local TARG LINE_NUM LINE_ABOVE OP TARG="$1" [ -f "${TARG}" ] || return 0 LINE_NUM="$(awk -v prefix="[ -x ${ADDON_DIR}/" ' { line = $0; sub(/^[[:space:]]*/, "", line) } index(line, prefix) == 1 { print NR ":" $0 } ' "${TARG}")" || return 1 if [ "${2:-}" ]; then OP="$(printf '%.1s' "$2")" if [ "$OP" = "!" ]; then LINE_NUM="$(printf '%s\n' "${LINE_NUM}" | grep -v "$(_quote "$2")" | cut -d':' -f1)" else LINE_NUM="$(printf '%s\n' "${LINE_NUM}" | grep "$(_quote "$2")" | cut -d':' -f1)" fi else LINE_NUM="$(printf '%s\n' "${LINE_NUM}" | cut -d':' -f1)" fi [ -z "${LINE_NUM}" ] && return LINE_NUM="$(printf "%s\n" "${LINE_NUM}" | sort -rn)" || return 1 for LINE_NUM in ${LINE_NUM}; do [ -z "${LINE_NUM}" ] && continue sed -i "${LINE_NUM}d" "${TARG}" || return 1 if [ "${LINE_NUM}" -gt 1 ]; then LINE_NUM="$((LINE_NUM - 1))" LINE_ABOVE="$(sed "${LINE_NUM}q;d" "${TARG}")" || return 1 if [ -z "${LINE_ABOVE}" ]; then sed -i "${LINE_NUM}d" "${TARG}" || return 1 fi fi done LINE_ABOVE="$(awk '{ print }' "${TARG}")" || return 1 if [ "${LINE_ABOVE}" = "#!/bin/sh" ]; then rm -f "${TARG}" || return 1 fi return 0 } download_file() { local TARG PERM URL URL_BASE URL_QUERY RET FILENAME TMP_FILE TMP_MD5 TMP_SHA256 MD5SUM_OLD MD5SUM_CURR MD5SUM_EXPECTED SHA256SUM_CURR SHA256SUM_EXPECTED SHA256_AVAILABLE COUNT CACHE_TOKEN PAYLOAD_URL MD5_URL SHA256_URL TARG="$1" shift PERM="$1" shift RET="0" DOWNLOAD_CACHE_SEQUENCE="$((${DOWNLOAD_CACHE_SEQUENCE:-0} + 1))" ptxt_phase "Downloading and verifying files." for URL in "$@"; do case "${URL}" in *\?*) URL_BASE="${URL%%\?*}" URL_QUERY="?${URL#*\?}" ;; *) URL_BASE="${URL}" URL_QUERY="" ;; esac FILENAME="${URL_BASE##*/}" TMP_FILE="${TARG}/.${FILENAME}.$$" TMP_MD5="${TMP_FILE}.md5sum" TMP_SHA256="${TMP_FILE}.sha256sum" MD5SUM_OLD="" MD5SUM_CURR="" MD5SUM_EXPECTED="" SHA256SUM_CURR="" SHA256SUM_EXPECTED="" COUNT="0" [ -f "${TARG}/${FILENAME}" ] && MD5SUM_OLD="$(file_md5 "${TARG}/${FILENAME}" 2>/dev/null)" while [ "${COUNT}" -lt 3 ]; do CACHE_TOKEN="${INSTALLER_CACHE_NONCE:-$$}-download-${DOWNLOAD_CACHE_SEQUENCE}-${COUNT}" PAYLOAD_URL="$(http_url_with_cache_token "${URL}" "${CACHE_TOKEN}")" SHA256_URL="$(http_url_with_cache_token "${URL_BASE}.sha256sum${URL_QUERY}" "${CACHE_TOKEN}")" MD5_URL="$(http_url_with_cache_token "${URL_BASE}.md5sum${URL_QUERY}" "${CACHE_TOKEN}")" MD5SUM_CURR="" MD5SUM_EXPECTED="" SHA256SUM_CURR="" SHA256SUM_EXPECTED="" SHA256_AVAILABLE="1" ptxt_step "Downloading ${FILENAME} to ${TARG}/${FILENAME}." rm -f "${TMP_FILE}" "${TMP_MD5}" "${TMP_SHA256}" if http_get_file "${PAYLOAD_URL}" "${TMP_FILE}" && [ -s "${TMP_FILE}" ]; then ptxt_ok "Downloaded ${FILENAME}." if ! chmod 600 "${TMP_FILE}"; then ptxt_warn "Unable to secure the staged ${FILENAME}; retrying download." rm -f "${TMP_FILE}" COUNT="$((COUNT + 1))" continue fi ptxt_step "Checking remote checksum metadata for ${FILENAME}." if ! http_get_file "${SHA256_URL}" "${TMP_SHA256}"; then SHA256_AVAILABLE="0" ptxt_warn "SHA-256 metadata is unavailable for ${FILENAME}; using the intentional legacy-compatible MD5 verification path." elif ! SHA256SUM_EXPECTED="$(sha256_manifest_digest "${TMP_SHA256}")"; then ptxt_warn "SHA-256 metadata is empty or invalid for ${FILENAME}; retrying download." else ptxt_step "Verifying SHA-256 checksum for ${FILENAME}." if ! SHA256SUM_CURR="$(file_sha256 "${TMP_FILE}" 2>/dev/null)"; then SHA256_AVAILABLE="0" ptxt_warn "SHA-256 digest calculation is unavailable for ${FILENAME}; using the intentional legacy-compatible MD5 verification path." elif [ "${SHA256SUM_CURR}" != "${SHA256SUM_EXPECTED}" ]; then ptxt_warn "SHA-256 checksum mismatch for ${FILENAME}; retrying download." else MD5SUM_CURR="$(file_md5 "${TMP_FILE}" 2>/dev/null)" ptxt_ok "SHA-256 checksum verified for ${FILENAME}." fi fi if sha256_is_valid "${SHA256SUM_EXPECTED}" && [ "${SHA256SUM_CURR}" = "${SHA256SUM_EXPECTED}" ]; then break fi if [ "${SHA256_AVAILABLE}" = "0" ]; then if ! http_get_file "${MD5_URL}" "${TMP_MD5}"; then ptxt_warn "MD5 metadata is missing or unavailable for ${FILENAME}; retrying download." elif ! MD5SUM_EXPECTED="$(md5_manifest_digest "${TMP_MD5}")"; then ptxt_warn "MD5 metadata is empty or invalid for ${FILENAME}; retrying download." fi fi if [ "${SHA256_AVAILABLE}" = "0" ] && md5_is_valid "${MD5SUM_EXPECTED}"; then ptxt_step "Verifying MD5 checksum for ${FILENAME}." if ! MD5SUM_CURR="$(file_md5 "${TMP_FILE}" 2>/dev/null)"; then ptxt_warn "MD5 digest calculation failed for ${FILENAME}; retrying download." elif [ "${MD5SUM_CURR}" != "${MD5SUM_EXPECTED}" ]; then ptxt_warn "MD5 checksum mismatch for ${FILENAME}; retrying download." else ptxt_ok "MD5 checksum verified for ${FILENAME}." break fi fi rm -f "${TMP_FILE}" "${TMP_MD5}" "${TMP_SHA256}" else ptxt_warn "Download attempt failed for ${FILENAME}; retrying if attempts remain." fi COUNT="$((COUNT + 1))" done rm -f "${TMP_MD5}" "${TMP_SHA256}" if [ "${COUNT}" -eq 3 ]; then rm -f "${TMP_FILE}" ptxt_fail "Unable to download ${BOLD}${URL}${NORM}." RET="$((RET + 1))" continue fi if md5_is_valid "${MD5SUM_CURR}" && [ "${MD5SUM_CURR}" = "${MD5SUM_OLD}" ]; then rm -f "${TMP_FILE}" ptxt_ok "${FILENAME} at ${TARG}/${FILENAME} is up to date; continuing." continue fi # Set permissions before replacing the working copy. If chmod fails, # preserve the existing file instead of leaving a partially installed one. ptxt_step "Installing ${FILENAME} to ${TARG}/${FILENAME}." if chmod "${PERM}" "${TMP_FILE}" && mv "${TMP_FILE}" "${TARG}/${FILENAME}"; then ptxt_ok "Installed ${FILENAME} to ${TARG}/${FILENAME}." continue fi rm -f "${TMP_FILE}" ptxt_fail "Unable to install ${BOLD}${FILENAME}${NORM} to ${TARG}/${FILENAME}." RET="$((RET + 1))" done if [ "${RET}" -ne 0 ]; then ptxt_fail "One or more download failures occurred; rerun the installer or restore from backup." fi return "${RET}" } installer_update_reexec() { local ACTION NEW_MD5 OLD_MD5 if [ "${AI_NO_AUTO_UPDATE:-0}" = "1" ]; then installer_bundle_update_allowed || return 1 fi ACTION="$1" OLD_MD5="$2" [ "${ACTION}" = "${INSTALLER_ACTION_UPDATE:-update}" ] || return 0 [ "${AI_REEXECED_INSTALLER:-0}" = "1" ] && return 0 [ -n "${OLD_MD5}" ] || return 0 [ -f "${TARG_DIR}/installer" ] || return 0 NEW_MD5="$(file_md5 "${TARG_DIR}/installer" 2>/dev/null)" [ -n "${NEW_MD5}" ] || return 0 [ "${OLD_MD5}" != "${NEW_MD5}" ] || return 0 ptxt_ok "Installer was updated at ${TARG_DIR}/installer; restarting the update with the new installer." chmod 755 "${TARG_DIR}/installer" >/dev/null 2>&1 AI_REEXECED_INSTALLER="1" # Preserve the process-owned transaction lock across exec. The PID is # unchanged, and flock mode also inherits descriptor 8 into the new script. export AI_REEXECED_INSTALLER NVRAM_TRANSACTION_LOCK_MODE AI_NO_AUTO_UPDATE exec "${TARG_DIR}/installer" "${BRANCH}" "${ACTION}" } install_wan_event_scripts() { local failed=0 add_dnsmasq_event_scripts || failed=1 [ "${failed}" -ne 0 ] || add_init_event_scripts || failed=1 [ "${failed}" -ne 0 ] || add_services_event_scripts || failed=1 [ "${failed}" -ne 0 ] || add_firewall_event_scripts || failed=1 if [ "${failed}" -ne 0 ]; then return 1 fi return 0 } # event_scripts_snapshot captures regular hook/config files and their metadata before aggregate event changes. event_scripts_snapshot() { local SNAPSHOT_DIR SOURCE SNAPSHOT SNAPSHOT_DIR="$1" shift [ ! -e "${SNAPSHOT_DIR}" ] && [ ! -L "${SNAPSHOT_DIR}" ] || return 1 for SOURCE in "$@"; do [ ! -L "${SOURCE}" ] && { [ ! -e "${SOURCE}" ] || [ -f "${SOURCE}" ]; } || return 1 done (umask 077 && mkdir -p "${SNAPSHOT_DIR}") || return 1 chmod 700 "${SNAPSHOT_DIR}" || { rm -rf "${SNAPSHOT_DIR}" return 1 } for SOURCE in "$@"; do SNAPSHOT="${SOURCE##*/}" if [ -f "${SOURCE}" ]; then cp -p "${SOURCE}" "${SNAPSHOT_DIR}/${SNAPSHOT}" || { rm -rf "${SNAPSHOT_DIR}" return 1 } else : >"${SNAPSHOT_DIR}/${SNAPSHOT}.absent" || { rm -rf "${SNAPSHOT_DIR}" return 1 } fi done } event_scripts_restore() { local RET SNAPSHOT_DIR SOURCE SNAPSHOT STAGE SNAPSHOT_DIR="$1" shift RET=0 for SOURCE in "$@"; do SNAPSHOT="${SOURCE##*/}" if [ -f "${SNAPSHOT_DIR}/${SNAPSHOT}" ]; then STAGE="${SOURCE}.restore.$$" if ! cp -p "${SNAPSHOT_DIR}/${SNAPSHOT}" "${STAGE}" || ! mv -f "${STAGE}" "${SOURCE}"; then rm -f "${STAGE}" RET=1 fi elif [ -f "${SNAPSHOT_DIR}/${SNAPSHOT}.absent" ]; then rm -f "${SOURCE}" || RET=1 else RET=1 fi done return "${RET}" } all_event_scripts_snapshot() { event_scripts_snapshot "$1" \ /jffs/scripts/dnsmasq.postconf \ /jffs/scripts/dnsmasq-sdn.postconf \ /jffs/scripts/init-start \ /jffs/scripts/services-stop \ /jffs/scripts/service-event-end \ /jffs/scripts/firewall-start \ "${CONF_FILE}" \ "${YAML_FILE}" \ "${YAML_ORI}" } all_event_scripts_restore() { event_scripts_restore "$1" \ /jffs/scripts/dnsmasq.postconf \ /jffs/scripts/dnsmasq-sdn.postconf \ /jffs/scripts/init-start \ /jffs/scripts/services-stop \ /jffs/scripts/service-event-end \ /jffs/scripts/firewall-start \ "${CONF_FILE}" \ "${YAML_FILE}" \ "${YAML_ORI}" } all_event_scripts_rollback() { local SNAPSHOT_DIR SNAPSHOT_DIR="$1" if ! all_event_scripts_restore "${SNAPSHOT_DIR}"; then PTXT "${ERROR} Event-script rollback failed; recovery snapshot retained at ${SNAPSHOT_DIR}." return 1 fi rm -rf "${SNAPSHOT_DIR}" 2>/dev/null || true return 0 } # event_scripts_recovery_marker_write durably records the aggregate snapshot that must be restored after interruption. event_scripts_recovery_marker_write() { local MARKER_FILE MARKER_STAGE SNAPSHOT_DIR SNAPSHOT_NAME SNAPSHOT_DIR="$1" SNAPSHOT_NAME="${SNAPSHOT_DIR##*/}" case "${SNAPSHOT_DIR}" in "${BASE_DIR}"/.AdGuardHome.event-hooks.* | "${BASE_DIR}"/.AdGuardHome.uninstall-hooks.*) ;; *) return 1 ;; esac [ -d "${SNAPSHOT_DIR}" ] && [ ! -L "${SNAPSHOT_DIR}" ] || return 1 MARKER_FILE="${BASE_DIR}/.AdGuardHome.event-hooks-recovery" MARKER_STAGE="${MARKER_FILE}.$$" [ ! -e "${MARKER_FILE}" ] && [ ! -L "${MARKER_FILE}" ] || return 1 [ ! -e "${MARKER_STAGE}" ] && [ ! -L "${MARKER_STAGE}" ] || return 1 (umask 077 && set -C && printf '%s\n' "${SNAPSHOT_NAME}" >"${MARKER_STAGE}") && chmod 600 "${MARKER_STAGE}" && mv -f "${MARKER_STAGE}" "${MARKER_FILE}" || { rm -f "${MARKER_STAGE}" return 1 } } # event_scripts_recovery_marker_read validates and prints the aggregate snapshot retained for startup recovery. event_scripts_recovery_marker_read() { local MARKER_FILE SNAPSHOT_DIR SNAPSHOT_NAME MARKER_FILE="${BASE_DIR}/.AdGuardHome.event-hooks-recovery" [ -f "${MARKER_FILE}" ] && [ ! -L "${MARKER_FILE}" ] || return 1 IFS= read -r SNAPSHOT_NAME <"${MARKER_FILE}" || return 1 if sed -n '2p' "${MARKER_FILE}" | grep -q .; then return 1 fi case "${SNAPSHOT_NAME}" in .AdGuardHome.event-hooks.* | .AdGuardHome.uninstall-hooks.*) ;; *) return 1 ;; esac SNAPSHOT_DIR="${BASE_DIR}/${SNAPSHOT_NAME}" [ -d "${SNAPSHOT_DIR}" ] && [ ! -L "${SNAPSHOT_DIR}" ] || return 1 printf '%s\n' "${SNAPSHOT_DIR}" } all_event_scripts_transaction_begin() { local SNAPSHOT_DIR SNAPSHOT_DIR="$1" [ -z "${EVENT_SCRIPTS_ACTIVE_SNAPSHOT:-}" ] || return 1 all_event_scripts_snapshot "${SNAPSHOT_DIR}" || return 1 event_scripts_recovery_marker_write "${SNAPSHOT_DIR}" || { rm -rf "${SNAPSHOT_DIR}" return 1 } EVENT_SCRIPTS_ACTIVE_SNAPSHOT="${SNAPSHOT_DIR}" return 0 } # all_event_scripts_transaction_commit removes the recovery marker before detaching the active snapshot; marker cleanup failure leaves rollback available. # Snapshot residue after marker removal is reported but does not make the committed transaction fail. all_event_scripts_transaction_commit() { local MARKER_FILE SNAPSHOT_DIR [ -n "${EVENT_SCRIPTS_ACTIVE_SNAPSHOT:-}" ] || return 0 MARKER_FILE="${BASE_DIR}/.AdGuardHome.event-hooks-recovery" if ! rm -f "${MARKER_FILE}" && ! rm -f "${MARKER_FILE}"; then PTXT "${ERROR} Event-hook recovery marker cleanup failed; recovery snapshot retained at ${EVENT_SCRIPTS_ACTIVE_SNAPSHOT}." return 1 fi SNAPSHOT_DIR="${EVENT_SCRIPTS_ACTIVE_SNAPSHOT}" EVENT_SCRIPTS_ACTIVE_SNAPSHOT="" if ! rm -rf "${SNAPSHOT_DIR}" 2>/dev/null; then PTXT "${WARNING} Event-hook transaction committed, but stale snapshot residue could not be removed at ${SNAPSHOT_DIR}; rollback will not be replayed." fi return 0 } # all_event_scripts_transaction_detach_after_mode_rollback prevents EXIT cleanup from replaying a newer aggregate snapshot after the older mode snapshot has been restored. all_event_scripts_transaction_detach_after_mode_rollback() { local SNAPSHOT_DIR SNAPSHOT_DIR="${EVENT_SCRIPTS_ACTIVE_SNAPSHOT:-}" [ -n "${SNAPSHOT_DIR}" ] || return 0 if ! rm -f "${BASE_DIR}/.AdGuardHome.event-hooks-recovery"; then PTXT "${WARNING} Superseded event-hook snapshot could not be removed; it remains at ${SNAPSHOT_DIR}." return 1 fi EVENT_SCRIPTS_ACTIVE_SNAPSHOT="" if ! rm -rf "${SNAPSHOT_DIR}"; then PTXT "${WARNING} Superseded event-hook snapshot could not be removed; it remains at ${SNAPSHOT_DIR}." return 1 fi PTXT "${INFO} Superseded event-hook rollback snapshot removed after mode rollback." } # all_event_scripts_transaction_rollback restores the active snapshot and retains it for retry unless both restoration and marker removal succeed. all_event_scripts_transaction_rollback() { local SNAPSHOT_DIR SNAPSHOT_DIR="${EVENT_SCRIPTS_ACTIVE_SNAPSHOT:-}" [ -n "${SNAPSHOT_DIR}" ] || return 0 if ! all_event_scripts_restore "${SNAPSHOT_DIR}"; then PTXT "${ERROR} Event-script rollback failed; recovery snapshot retained at ${SNAPSHOT_DIR}." return 1 fi if ! rm -f "${BASE_DIR}/.AdGuardHome.event-hooks-recovery"; then PTXT "${ERROR} Event-script rollback restored files but could not remove its recovery marker; recovery snapshot retained at ${SNAPSHOT_DIR}." return 1 fi EVENT_SCRIPTS_ACTIVE_SNAPSHOT="" rm -rf "${SNAPSHOT_DIR}" 2>/dev/null || true return 0 } # all_event_scripts_recover_startup retries a retained aggregate event-hook rollback before new installer work begins. all_event_scripts_recover_startup() { local SNAPSHOT_DIR [ ! -e "${BASE_DIR}/.AdGuardHome.event-hooks-recovery" ] || { SNAPSHOT_DIR="$(event_scripts_recovery_marker_read)" || return 1 EVENT_SCRIPTS_ACTIVE_SNAPSHOT="${SNAPSHOT_DIR}" all_event_scripts_transaction_rollback } } init_event_scripts_snapshot() { event_scripts_snapshot "$1" /jffs/scripts/init-start } init_event_scripts_restore() { event_scripts_restore "$1" /jffs/scripts/init-start } add_init_event_scripts() { local SNAPSHOT_DIR SNAPSHOT_DIR="${BASE_DIR}/.AdGuardHome.init-hooks.$$" init_event_scripts_snapshot "${SNAPSHOT_DIR}" || return 1 if ! write_manager_script /jffs/scripts/init-start "init-start &"; then init_event_scripts_restore "${SNAPSHOT_DIR}" || true rm -rf "${SNAPSHOT_DIR}" 2>/dev/null || true return 1 fi rm -rf "${SNAPSHOT_DIR}" 2>/dev/null || true return 0 } remove_init_event_scripts() { local SNAPSHOT_DIR SNAPSHOT_DIR="${BASE_DIR}/.AdGuardHome.init-hooks-remove.$$" init_event_scripts_snapshot "${SNAPSHOT_DIR}" || return 1 if ! del_jffs_script /jffs/scripts/init-start >/dev/null 2>&1; then init_event_scripts_restore "${SNAPSHOT_DIR}" || true rm -rf "${SNAPSHOT_DIR}" 2>/dev/null || true return 1 fi rm -rf "${SNAPSHOT_DIR}" 2>/dev/null || true return 0 } services_event_scripts_snapshot() { # Asuswrt-Merlin names the event-end hook service-event-end (singular). event_scripts_snapshot "$1" /jffs/scripts/services-stop /jffs/scripts/service-event-end } services_event_scripts_restore() { event_scripts_restore "$1" /jffs/scripts/services-stop /jffs/scripts/service-event-end } # add_services_event_scripts publishes managed service hooks together and restores their snapshot on failure. add_services_event_scripts() { local failed=0 SNAPSHOT_DIR SNAPSHOT_DIR="${BASE_DIR}/.AdGuardHome.services-hooks.$$" services_event_scripts_snapshot "${SNAPSHOT_DIR}" || return 1 del_between_magic /jffs/scripts/service-event-end '# Asuswrt-Merlin-AdGuardHome-Installer' || failed=1 if [ "${failed}" -eq 0 ]; then write_manager_script /jffs/scripts/services-stop "services-stop &" || failed=1 fi if [ "${failed}" -eq 0 ]; then write_command_script /jffs/scripts/service-event-end "$(service_event_hook_command)" || failed=1 fi if [ "${failed}" -ne 0 ]; then services_event_scripts_restore "${SNAPSHOT_DIR}" || failed=1 rm -rf "${SNAPSHOT_DIR}" 2>/dev/null || true return 1 fi rm -rf "${SNAPSHOT_DIR}" 2>/dev/null || true return 0 } remove_services_event_scripts() { local failed=0 SNAPSHOT_DIR SNAPSHOT_DIR="${BASE_DIR}/.AdGuardHome.services-hooks-remove.$$" services_event_scripts_snapshot "${SNAPSHOT_DIR}" || return 1 { del_jffs_script /jffs/scripts/services-stop >/dev/null 2>&1; } || failed=1 { del_between_magic /jffs/scripts/service-event-end '# Asuswrt-Merlin-AdGuardHome-Installer' >/dev/null 2>&1; } || failed=1 if [ "${failed}" -ne 0 ]; then services_event_scripts_restore "${SNAPSHOT_DIR}" || failed=1 rm -rf "${SNAPSHOT_DIR}" 2>/dev/null || true return 1 fi rm -rf "${SNAPSHOT_DIR}" 2>/dev/null || true return 0 } firewall_event_scripts_snapshot() { event_scripts_snapshot "$1" /jffs/scripts/firewall-start } firewall_event_scripts_restore() { event_scripts_restore "$1" /jffs/scripts/firewall-start } add_firewall_event_scripts() { local SNAPSHOT_DIR SNAPSHOT_DIR="${BASE_DIR}/.AdGuardHome.firewall-hooks.$$" firewall_event_scripts_snapshot "${SNAPSHOT_DIR}" || return 1 if ! write_manager_script /jffs/scripts/firewall-start 'firewall "$1"'; then firewall_event_scripts_restore "${SNAPSHOT_DIR}" || true rm -rf "${SNAPSHOT_DIR}" 2>/dev/null || true return 1 fi rm -rf "${SNAPSHOT_DIR}" 2>/dev/null || true return 0 } # Remove only known current-process service staging files from a directory we created. adguard_service_bundle_cleanup() { local name [ "${ADGUARD_SERVICE_STAGE_OWNED:-0}" = "1" ] || return 0 [ "${ADGUARD_SERVICE_STAGE_DIR:-}" = "${BASE_DIR}/.AdGuardHome.services.$$" ] && [ -d "${ADGUARD_SERVICE_STAGE_DIR}" ] && [ ! -L "${ADGUARD_SERVICE_STAGE_DIR}" ] || return 1 for name in installer AdGuardHome.sh S99AdGuardHome rc.func.AdGuardHome; do rm -f "${ADGUARD_SERVICE_STAGE_DIR}/${name}" "${ADGUARD_SERVICE_STAGE_DIR}/.${name}.$$" \ "${ADGUARD_SERVICE_STAGE_DIR}/.${name}.$$.md5sum" "${ADGUARD_SERVICE_STAGE_DIR}/.${name}.$$.sha256sum" || break done if ! rmdir "${ADGUARD_SERVICE_STAGE_DIR}"; then PTXT "${ERROR} Service staging cleanup is incomplete; retained at ${ADGUARD_SERVICE_STAGE_DIR}." return 1 fi ADGUARD_SERVICE_STAGE_DIR="" ADGUARD_SERVICE_STAGE_OWNED=0 return 0 } # Download through the existing checksum policy while AdGuard Home DNS is available. # Restore additionally stages the installer, which its final setup normally downloads. adguard_service_bundle_stage() { local include_installer include_installer="${1:-0}" if [ "${ADGUARD_SERVICE_STAGE_OWNED:-0}" = "1" ]; then [ "${ADGUARD_SERVICE_STAGE_DIR}" = "${BASE_DIR}/.AdGuardHome.services.$$" ] && [ ! -L "${ADGUARD_SERVICE_STAGE_DIR}" ] && [ "${ADGUARD_SERVICE_STAGE_SOURCE:-}" = "${RURL}" ] && [ -f "${ADGUARD_SERVICE_STAGE_DIR}/AdGuardHome.sh" ] && [ -f "${ADGUARD_SERVICE_STAGE_DIR}/S99AdGuardHome" ] && [ -f "${ADGUARD_SERVICE_STAGE_DIR}/rc.func.AdGuardHome" ] && { [ "${include_installer}" != "1" ] || [ -f "${ADGUARD_SERVICE_STAGE_DIR}/installer" ]; } return $? fi ADGUARD_SERVICE_STAGE_DIR="${BASE_DIR}/.AdGuardHome.services.$$" if ! (umask 077 && mkdir "${ADGUARD_SERVICE_STAGE_DIR}"); then PTXT "${ERROR} Unable to create private service staging at ${ADGUARD_SERVICE_STAGE_DIR}." return 1 fi ADGUARD_SERVICE_STAGE_OWNED=1 ADGUARD_SERVICE_STAGE_SOURCE="${RURL}" if ! download_file "${ADGUARD_SERVICE_STAGE_DIR}" 755 "${RURL}/AdGuardHome.sh" || ! download_file "${ADGUARD_SERVICE_STAGE_DIR}" 755 "${RURL}/S99AdGuardHome" || ! download_file "${ADGUARD_SERVICE_STAGE_DIR}" 644 "${RURL}/rc.func.AdGuardHome" || { [ "${include_installer}" = "1" ] && ! download_file "${ADGUARD_SERVICE_STAGE_DIR}" 755 "${RURL}/installer"; }; then PTXT "${ERROR} Required AdGuardHome service files could not be prefetched; the installed service was left unchanged." adguard_service_bundle_cleanup || true return 1 fi if [ "${include_installer}" = "1" ] && ! installer_version_value "${ADGUARD_SERVICE_STAGE_DIR}/installer" >/dev/null; then PTXT "${ERROR} The prefetched installer failed version validation." adguard_service_bundle_cleanup || true return 1 fi return 0 } # Restore prepares checksum support and optional timezone selection before the # live directory moves. Later restore setup must use these local dependencies. adguard_restore_prepare_online_dependencies() { if ! ensure_sha256sum_tool; then ptxt_warn "SHA-256 support is unavailable; downloads will require matching MD5 metadata." fi if [ ! -f "${ADDON_DIR}/localtime" ]; then PTXT "${INFO} Before restoring AdGuardHome, please choose your right timezone!" if ! create_dir "${ADDON_DIR}" || ! chmod 755 "${ADDON_DIR}" || ! set_timezone; then PTXT "${ERROR} Unable to prepare the timezone before restoring AdGuardHome." return 1 fi fi adguard_service_bundle_stage 1 } # Remove the known publication file and retain ownership if cleanup is incomplete. adguard_service_publish_cleanup() { [ "${ADGUARD_SERVICE_PUBLISH_OWNED:-0}" = "1" ] || return 0 case "${ADGUARD_SERVICE_PUBLISH_DIR:-}" in "${ADDON_DIR}/.AdGuardHome.service.$$" | "/opt/etc/init.d/.AdGuardHome.service.$$" | "${TARG_DIR}/.AdGuardHome.service.$$") ;; *) return 1 ;; esac [ -d "${ADGUARD_SERVICE_PUBLISH_DIR}" ] && [ ! -L "${ADGUARD_SERVICE_PUBLISH_DIR}" ] || return 1 if ! rm -f "${ADGUARD_SERVICE_PUBLISH_DIR}/file" || ! rmdir "${ADGUARD_SERVICE_PUBLISH_DIR}"; then PTXT "${ERROR} Service publication staging cleanup is incomplete; retained at ${ADGUARD_SERVICE_PUBLISH_DIR}." return 1 fi ADGUARD_SERVICE_PUBLISH_DIR="" ADGUARD_SERVICE_PUBLISH_OWNED=0 return 0 } # Copy into an owned directory on the destination filesystem before atomic publication. # A failed copy, chmod or rename leaves the existing target available. adguard_service_publish_file() { local target_dir name perm target_dir="$1" name="$2" perm="$3" [ "${ADGUARD_SERVICE_STAGE_OWNED:-0}" = "1" ] && [ "${ADGUARD_SERVICE_STAGE_DIR:-}" = "${BASE_DIR}/.AdGuardHome.services.$$" ] && [ -f "${ADGUARD_SERVICE_STAGE_DIR}/${name}" ] && [ ! -L "${ADGUARD_SERVICE_STAGE_DIR}/${name}" ] && [ ! -d "${target_dir}/${name}" ] || return 1 adguard_service_publish_cleanup || return 1 ADGUARD_SERVICE_PUBLISH_DIR="${target_dir}/.AdGuardHome.service.$$" if ! (umask 077 && mkdir "${ADGUARD_SERVICE_PUBLISH_DIR}"); then return 1 fi ADGUARD_SERVICE_PUBLISH_OWNED=1 if ! cp "${ADGUARD_SERVICE_STAGE_DIR}/${name}" "${ADGUARD_SERVICE_PUBLISH_DIR}/file" || ! chmod "${perm}" "${ADGUARD_SERVICE_PUBLISH_DIR}/file" || ! mv -f "${ADGUARD_SERVICE_PUBLISH_DIR}/file" "${target_dir}/${name}"; then adguard_service_publish_cleanup || true return 1 fi adguard_service_publish_cleanup } remove_firewall_event_scripts() { local SNAPSHOT_DIR SNAPSHOT_DIR="${BASE_DIR}/.AdGuardHome.firewall-hooks-remove.$$" firewall_event_scripts_snapshot "${SNAPSHOT_DIR}" || return 1 if ! del_jffs_script /jffs/scripts/firewall-start 'firewall "$1"' >/dev/null 2>&1; then firewall_event_scripts_restore "${SNAPSHOT_DIR}" || true rm -rf "${SNAPSHOT_DIR}" 2>/dev/null || true return 1 fi rm -rf "${SNAPSHOT_DIR}" 2>/dev/null || true return 0 } remove_dnsmasq_event_scripts() { local failed=0 SNAPSHOT_DIR SNAPSHOT_DIR="${BASE_DIR}/.AdGuardHome.dnsmasq-hooks-remove.$$" event_scripts_snapshot "${SNAPSHOT_DIR}" /jffs/scripts/dnsmasq.postconf /jffs/scripts/dnsmasq-sdn.postconf "${CONF_FILE}" || { rm -rf "${SNAPSHOT_DIR}" 2>/dev/null || true return 1 } { del_jffs_script /jffs/scripts/dnsmasq.postconf dnsmasq >/dev/null; } || failed=1 { del_jffs_script /jffs/scripts/dnsmasq-sdn.postconf 'dnsmasq-sdn $2' >/dev/null; } || failed=1 if [ "${failed}" -eq 0 ]; then write_conf ADGUARD_DNSMASQ_MODE '"disabled"' || failed=1 fi if [ "${failed}" -ne 0 ]; then event_scripts_restore "${SNAPSHOT_DIR}" /jffs/scripts/dnsmasq.postconf /jffs/scripts/dnsmasq-sdn.postconf "${CONF_FILE}" || failed=1 rm -rf "${SNAPSHOT_DIR}" 2>/dev/null || true return 1 fi rm -rf "${SNAPSHOT_DIR}" 2>/dev/null || true return 0 } add_dnsmasq_event_scripts() { local RESTORE_STATUS SNAPSHOT_DIR if ! pidof dnsmasq >/dev/null 2>&1 || [ ! -f /etc/dnsmasq.conf ]; then remove_dnsmasq_event_scripts || return 1 return 0 fi SNAPSHOT_DIR="${BASE_DIR}/.AdGuardHome.dnsmasq-hooks.$$" event_scripts_snapshot "${SNAPSHOT_DIR}" /jffs/scripts/dnsmasq.postconf /jffs/scripts/dnsmasq-sdn.postconf "${CONF_FILE}" || { rm -rf "${SNAPSHOT_DIR}" return 1 } RESTORE_STATUS=0 write_manager_script /jffs/scripts/dnsmasq.postconf 'dnsmasq pre_start' || RESTORE_STATUS=1 if [ "${RESTORE_STATUS}" -eq 0 ] && installer_nvram get rc_support | grep -q 'mtlancfg'; then write_manager_script /jffs/scripts/dnsmasq-sdn.postconf 'dnsmasq-sdn $2' || RESTORE_STATUS=1 elif [ "${RESTORE_STATUS}" -eq 0 ]; then del_jffs_script /jffs/scripts/dnsmasq-sdn.postconf 'dnsmasq-sdn $2' >/dev/null || RESTORE_STATUS=1 fi if [ "${RESTORE_STATUS}" -eq 0 ]; then write_conf ADGUARD_DNSMASQ_MODE '"enabled"' || RESTORE_STATUS=1 fi if [ "${RESTORE_STATUS}" -ne 0 ]; then if ! event_scripts_restore "${SNAPSHOT_DIR}" /jffs/scripts/dnsmasq.postconf /jffs/scripts/dnsmasq-sdn.postconf "${CONF_FILE}"; then PTXT "${ERROR} Unable to restore dnsmasq event scripts; recovery snapshot retained at ${SNAPSHOT_DIR}." return 1 fi if ! rm -rf "${SNAPSHOT_DIR}"; then PTXT "${ERROR} Unable to remove the restored dnsmasq event-script snapshot; retained at ${SNAPSHOT_DIR}." fi return 1 fi if ! rm -rf "${SNAPSHOT_DIR}"; then PTXT "${ERROR} Unable to remove the dnsmasq event-script snapshot; retained at ${SNAPSHOT_DIR}." fi return 0 } # inst_AdGuardHome orchestrates the action in $1; an optional build channel in $2 enters package installation or service refresh directly instead of branch discovery. inst_AdGuardHome() { local install_action install_channel install_action="${1-}" install_channel="${2-}" local ADGUARD_TAR ARCHIVE_MD5 ARCHIVE_SHA256 COMMITTED_OLD_BINARY EVENT_HOOK_ROLLBACK_FAILED INSTALLER_MD5_OLD MIGRATE_STATUS REMOTE_ADGUARD_ARCHIVE REMOTE_ADGUARD_MD5 REMOTE_ADGUARD_SHA256 REMOTE_ADGUARD_URL REMOTE_ADGUARD_VER RESTART_AFTER_ABORT if [ "${AI_NO_AUTO_UPDATE:-0}" = "1" ]; then installer_bundle_update_allowed || return 1 fi ADGUARD_TAR="AdGuardHome_${ADGUARD_ARCH}.tar.gz" ptxt_phase "Running AdGuardHome ${install_action:-install} orchestration." if [ "${install_action:-RESTORE}" != "${INSTALLER_ACTION_RESTORE:-RESTORE}" ] && ! ensure_sha256sum_tool; then ptxt_warn "SHA-256 support is unavailable; downloads will require matching MD5 metadata." fi INSTALLER_MD5_OLD="" RESTART_AFTER_ABORT="0" local SERVICE_REFRESH_ONLY="${SERVICE_REFRESH_ONLY:-0}" [ -f "${SCRIPT_LOC}" ] && INSTALLER_MD5_OLD="$(file_md5 "${SCRIPT_LOC}" 2>/dev/null)" if [ -z "${install_channel}" ]; then if [ "${install_action:-RESTORE}" != "${INSTALLER_ACTION_RESTORE:-RESTORE}" ]; then if [ ! -d "${TARG_DIR}" ] && [ -f "${BASE_DIR}/backup_AdGuardHome.tar.gz" ]; then PTXT "${INFO} Backup is detected." local USE_OLD read_yesno "Do you want Restore instead?" CONFIRM_STATUS=$? if [ "${CONFIRM_STATUS}" -eq 0 ]; then USE_OLD="YES" elif [ "${CONFIRM_STATUS}" -eq 1 ]; then USE_OLD="NO" else return "${CONFIRM_STATUS}" fi if [ "${USE_OLD}" = "YES" ]; then PTXT "${INFO} Installing from an old backup!" backup_restore RESTORE return "$?" elif [ "${USE_OLD}" = "NO" ]; then PTXT "${INFO} Continuing without restoring from backup!" fi elif [ -d "${TARG_DIR}" ] && [ -f "${AGH_FILE}" ] && [ ! -f "${BASE_DIR}/backup_AdGuardHome.tar.gz" ]; then read_yesno "Do you want create a backup before updating?" CONFIRM_STATUS=$? if [ "${CONFIRM_STATUS}" -eq 0 ]; then if ! backup_restore BACKUP 0; then PTXT "${ERROR} Unable to create the requested pre-update backup." end_op_message 1 "${install_action}" return 1 fi elif [ "${CONFIRM_STATUS}" -eq 1 ]; then PTXT "${INFO} continuing without making a backup." else return "${CONFIRM_STATUS}" fi fi fi if [ "${install_action:-RESTORE}" = "${INSTALLER_ACTION_RESTORE:-RESTORE}" ]; then # backup_restore prepared this before changing the target directory. if ! adguard_service_bundle_stage 1 || ! adguard_service_publish_file "${TARG_DIR}" installer 755; then PTXT "${ERROR} Unable to install the prefetched restore installer." end_op_message 1 "${install_action}" return 1 fi else if [ -z "${REMOTE_VER}" ] || [ -z "${REMOTE_BETA}" ] || [ -z "${REMOTE_EDGE}" ] || { ! check_connection; }; then PTXT "${ERROR} Unable to detect the Internet!" end_op_message 1 "${install_action}" return 1 fi ptxt_ok "Internet and remote version metadata are available." ptxt_step "Preparing AdGuardHome target directory." if ! create_dir "${TARG_DIR}" || ! ensure_adguardhome_directory_permissions; then end_op_message 1 "${install_action}" return 1 fi ptxt_ok "AdGuardHome target directory is ready." ptxt_step "Downloading installer update." if ! download_file "${TARG_DIR}" 755 "${RURL}/installer" || ! installer_version_value "${TARG_DIR}/installer" >/dev/null; then PTXT "${ERROR} Failed to download installer." end_op_message 1 "${install_action}" return 1 fi ptxt_ok "Installer update downloaded and validated." fi installer_update_reexec "${install_action:-install}" "${INSTALLER_MD5_OLD}" if [ "${install_action:-RESTORE}" != "${INSTALLER_ACTION_RESTORE:-RESTORE}" ]; then if [ "${install_action:-install}" != "${INSTALLER_ACTION_UPDATE:-update}" ]; then choose_branch CHOOSE_BRANCH_STATUS=$? if [ "${CHOOSE_BRANCH_STATUS}" -ne 0 ] && [ "${CHOOSE_BRANCH_STATUS}" -ne 3 ]; then PTXT "${ERROR} Unable to select an AdGuardHome build. Installation was aborted." end_op_message 1 "${install_action}" return 1 fi fi local ADGUARD_BRANCH ADGUARD_VER ADGUARD_BRANCH="$(conf_value ADGUARD_BRANCH)" if [ ! -f "${AGH_FILE}" ]; then inst_AdGuardHome "${install_action:-install}" "$ADGUARD_BRANCH" else ADGUARD_VER="$("${AGH_FILE}" --version | cut -d" " -f4-)" [ -z "${ADGUARD_VER}" ] && exit 1 REMOTE_ADGUARD_VER="$(adguard_remote_version "${ADGUARD_BRANCH}")" [ -z "${REMOTE_ADGUARD_VER}" ] && exit 1 if [ "${REMOTE_ADGUARD_VER}" != "${ADGUARD_VER}" ] || [ "${ADGUARD_BRANCH}" = "edge" ] || [ "${ADGUARD_BRANCH_CHANGED:-0}" = "1" ]; then if [ "${ADGUARD_BRANCH_CHANGED:-0}" = "1" ]; then case "${ADGUARD_BRANCH}" in release) PTXT "${INFO} Switching ADGUARDHOME_BUILD=Release" \ "${INFO} Installing RELEASE ADGUARDHOME_VER=${REMOTE_ADGUARD_VER}." ;; beta) PTXT "${INFO} Switching ADGUARDHOME_BUILD=Beta" \ "${INFO} Installing BETA ADGUARDHOME_VER=${REMOTE_ADGUARD_VER}." ;; edge) PTXT "${INFO} Switching ADGUARDHOME_BUILD=Edge" \ "${INFO} Installing EDGE ADGUARDHOME_VER=${REMOTE_ADGUARD_VER}." ;; esac else case "${ADGUARD_BRANCH}" in release) PTXT "${INFO} New RELEASE ADGUARDHOME_VER=${REMOTE_ADGUARD_VER} Available!" \ "${INFO} Updating ADGUARDHOME_VER=${ADGUARD_VER} to ${REMOTE_ADGUARD_VER}." ;; beta) PTXT "${INFO} New BETA ADGUARDHOME_VER=${REMOTE_ADGUARD_VER} Available!" \ "${INFO} Updating ADGUARDHOME_VER=${ADGUARD_VER} to ${REMOTE_ADGUARD_VER}." ;; edge) if [ "${REMOTE_ADGUARD_VER}" = "${ADGUARD_VER}" ]; then PTXT "${INFO} Refreshing EDGE ADGUARDHOME_VER=${REMOTE_ADGUARD_VER}." \ "${INFO} Edge builds can refresh without a version change." else PTXT "${INFO} New EDGE ADGUARDHOME_VER=${REMOTE_ADGUARD_VER} Available!" \ "${INFO} Updating ADGUARDHOME_VER=${ADGUARD_VER} to ${REMOTE_ADGUARD_VER}." fi ;; esac fi inst_AdGuardHome "${install_action:-update}" "${ADGUARD_BRANCH}" return $? else if [ "${install_action:-update}" = "${INSTALLER_ACTION_UPDATE:-update}" ]; then PTXT "${INFO} Refreshing installer-managed service integration." SERVICE_REFRESH_ONLY=1 inst_AdGuardHome update "${ADGUARD_BRANCH}" return $? fi case "${ADGUARD_BRANCH}" in release) PTXT "${INFO} ADGUARDHOME_BUILD=Release" \ "${INFO} No new release version available." \ "${INFO} ADGUARDHOME_VER=${ADGUARD_VER}" ;; beta) PTXT "${INFO} ADGUARDHOME_BUILD=Beta" \ "${INFO} No new beta version available." \ "${INFO} ADGUARDHOME_VER=${ADGUARD_VER}" ;; edge) PTXT "${INFO} ADGUARDHOME_BUILD=Edge" \ "${INFO} No new edge version available." \ "${INFO} ADGUARDHOME_VER=${ADGUARD_VER}" ;; esac fi fi fi else if [ "${SERVICE_REFRESH_ONLY}" = "1" ]; then if [ "$(agh_process_count)" -gt 0 ]; then RESTART_AFTER_ABORT="1"; fi ptxt_phase "Refreshing AdGuardHome service integration." else ptxt_phase "Installing AdGuardHome package." REMOTE_ADGUARD_ARCHIVE="$(adguard_remote_archive "${install_channel}")" REMOTE_ADGUARD_MD5="$(adguard_remote_md5 "${install_channel}")" REMOTE_ADGUARD_SHA256="$(adguard_remote_sha256 "${install_channel}")" REMOTE_ADGUARD_URL="$(adguard_remote_url "${install_channel}")" [ -z "${REMOTE_ADGUARD_ARCHIVE}" ] && REMOTE_ADGUARD_ARCHIVE="${ADGUARD_TAR}" [ -z "${REMOTE_ADGUARD_URL}" ] && REMOTE_ADGUARD_URL="${URL_ARCH}/${REMOTE_ADGUARD_ARCHIVE}" if ! download_file "${BASE_DIR}" 644 "${REMOTE_ADGUARD_URL}"; then PTXT "${ERROR} Unable to download AdGuardHome package for your router" end_op_message 1 "${install_action}" return 1 fi if sha256_is_valid "${REMOTE_ADGUARD_SHA256}"; then if ARCHIVE_SHA256="$(file_sha256 "${BASE_DIR}/${REMOTE_ADGUARD_ARCHIVE}" 2>/dev/null)"; then if [ "${ARCHIVE_SHA256}" != "${REMOTE_ADGUARD_SHA256}" ]; then PTXT "${ERROR} SHA-256 checksum mismatch for downloaded AdGuardHome package metadata." rm -f "${BASE_DIR:?}/${REMOTE_ADGUARD_ARCHIVE}" end_op_message 1 "${install_action}" return 1 fi elif md5_is_valid "${REMOTE_ADGUARD_MD5}"; then ARCHIVE_MD5="$(file_md5 "${BASE_DIR}/${REMOTE_ADGUARD_ARCHIVE}" 2>/dev/null)" || ARCHIVE_MD5="" if [ "${ARCHIVE_MD5}" = "${REMOTE_ADGUARD_MD5}" ]; then ptxt_warn "SHA-256 digest calculation is unavailable; the AdGuard Home archive was verified through the intentional legacy-compatible MD5 verification path." else PTXT "${ERROR} MD5 checksum mismatch for downloaded AdGuardHome package metadata." rm -f "${BASE_DIR:?}/${REMOTE_ADGUARD_ARCHIVE}" end_op_message 1 "${install_action}" return 1 fi else PTXT "${ERROR} SHA-256 digest calculation is unavailable and channel MD5 metadata is invalid." rm -f "${BASE_DIR:?}/${REMOTE_ADGUARD_ARCHIVE}" end_op_message 1 "${install_action}" return 1 fi elif md5_is_valid "${REMOTE_ADGUARD_MD5}"; then ARCHIVE_MD5="$(file_md5 "${BASE_DIR}/${REMOTE_ADGUARD_ARCHIVE}" 2>/dev/null)" || ARCHIVE_MD5="" if [ "${ARCHIVE_MD5}" != "${REMOTE_ADGUARD_MD5}" ]; then PTXT "${ERROR} MD5 checksum mismatch for downloaded AdGuardHome package metadata." rm -f "${BASE_DIR:?}/${REMOTE_ADGUARD_ARCHIVE}" end_op_message 1 "${install_action}" return 1 fi ptxt_warn "The AdGuard Home archive was verified through the intentional legacy-compatible MD5 verification path because SHA-256 metadata is unavailable." fi if [ "$(agh_process_count)" -gt 0 ]; then RESTART_AFTER_ABORT="1"; fi # Fresh CLI installs have no menu signal handler. Arm owned-prefetch # cleanup before the first service download, while service state is intact. adguard_install_abort_trap_enable "${RESTART_AFTER_ABORT}" if ! adguard_service_bundle_stage; then rm -f "${BASE_DIR:?}/${REMOTE_ADGUARD_ARCHIVE}" end_op_message 1 "${install_action}" return 1 fi ptxt_step "Replacing AdGuardHome package with downloaded archive." if ! install_adguard_archive "${BASE_DIR}/${REMOTE_ADGUARD_ARCHIVE}"; then rm -f "${BASE_DIR:?}/${REMOTE_ADGUARD_ARCHIVE}" rollback_result_notice end_op_message 1 "${install_action}" return 1 fi ptxt_ok "AdGuardHome package was replaced." rm -f "${BASE_DIR:?}/${REMOTE_ADGUARD_ARCHIVE}" [ -f "/opt/sbin/AdGuardHome" ] && rm -rf /opt/sbin/AdGuardHome ptxt_step "Creating AdGuardHome command symlink." if ! ln -sf "${AGH_FILE}" /opt/sbin/AdGuardHome || [ -z "$("${AGH_FILE}" --version | cut -d" " -f4-)" ]; then PTXT "${ERROR} Failed to download AdGuardHome package for your router" adguard_restart_after_install_abort "${RESTART_AFTER_ABORT}" end_op_message 1 "${install_action}" return 1 fi ptxt_ok "AdGuardHome command symlink is ready." fi fi if [ "${install_action:-install}" != "${INSTALLER_ACTION_RESTORE:-RESTORE}" ] && [ "${ADGUARD_INSTALL_REPLACE_ACTIVE:-0}" != "1" ]; then if [ "$(agh_process_count)" -gt 0 ]; then RESTART_AFTER_ABORT="1"; fi adguard_install_abort_trap_enable "${RESTART_AFTER_ABORT}" fi if ! adguard_service_bundle_stage; then end_op_message 1 "${install_action}" return 1 fi ptxt_phase "Installing service integration." ptxt_step "Installing AdGuardHome service scripts and addon hooks." if ! create_dir "${ADDON_DIR}"; then adguard_restart_after_install_abort "${RESTART_AFTER_ABORT}" end_op_message 1 "${install_action}" return 1 fi if ! chmod 755 "${ADDON_DIR}"; then PTXT "${ERROR} Unable to make ${ADDON_DIR} traversable." adguard_restart_after_install_abort "${RESTART_AFTER_ABORT}" end_op_message 1 "${install_action}" return 1 fi ptxt_ok "AdGuardHome addon directory is ready." if [ "${install_action:-update}" = "${INSTALLER_ACTION_UPDATE:-update}" ]; then configure_runtime_defaults upgrade || { adguard_restart_after_install_abort "${RESTART_AFTER_ABORT}" end_op_message 1 "${install_action}" return 1 } fi if ! adguard_service_publish_file "${ADDON_DIR}" AdGuardHome.sh 755 || ! adguard_service_publish_file "/opt/etc/init.d" S99AdGuardHome 755 || ! adguard_service_publish_file "/opt/etc/init.d" rc.func.AdGuardHome 644; then PTXT "${ERROR} Required AdGuardHome service files could not be installed." adguard_restart_after_install_abort "${RESTART_AFTER_ABORT}" end_op_message 1 "${install_action}" return 1 fi adguard_service_bundle_cleanup || true ptxt_ok "AdGuardHome service files installed." if ! adguard_install_mode_confirmed; then PTXT "${WARN:-WARNING:} Router mode remains unknown/unreadable (${ADGUARD_INSTALL_MODE_RAW:-unreadable}); retained ADGUARD_INSTALL_MODE=${ADGUARD_INSTALL_MODE} and deferred all mode-dependent migration." if [ "${ADGUARD_INSTALL_REPLACE_ACTIVE:-0}" = "1" ]; then COMMITTED_OLD_BINARY="${ADGUARD_INSTALL_OLD_BINARY}" if ! adguard_committed_binary_cleanup_finalize "${COMMITTED_OLD_BINARY}"; then end_op_message 1 "${install_action}" return 1 fi fi adguard_install_abort_trap_disable_preserve_defer return 0 fi # Run mode-dependent YAML migration before this orchestration changes # firewall state or event hooks, preserving the previous mode on failure. adguard_migrate_detected_install_mode "${PREVIOUS_ADGUARD_INSTALL_MODE:-}" MIGRATE_STATUS=$? if [ "${MIGRATE_STATUS}" -ne 0 ]; then adguard_restart_after_install_abort "${RESTART_AFTER_ABORT}" end_op_message 1 "${install_action}" return "${MIGRATE_STATUS}" fi if ! all_event_scripts_transaction_begin "${BASE_DIR}/.AdGuardHome.event-hooks.$$"; then PTXT "${ERROR} Existing Asuswrt-Merlin event scripts could not be protected for rollback." adguard_restart_after_install_abort "${RESTART_AFTER_ABORT}" end_op_message 1 "${install_action}" return 1 fi case "${ADGUARD_INSTALL_MODE:-wan}" in wan) if ! cleanup_legacy_firewall; then all_event_scripts_transaction_rollback || true PTXT "${ERROR} Unable to clear legacy firewall/IPTABLES state for WAN mode." adguard_recover_after_event_hook_abort "${RESTART_AFTER_ABORT}" end_op_message 1 "${install_action}" return 1 fi ;; lan) if legacy_firewall_cleanup_needed; then if ! cleanup_legacy_firewall; then all_event_scripts_transaction_rollback || true PTXT "${ERROR} Unable to clear legacy firewall/IPTABLES state for LAN mode." adguard_recover_after_event_hook_abort "${RESTART_AFTER_ABORT}" end_op_message 1 "${install_action}" return 1 fi PTXT "${INFO} LAN mode selected; cleared legacy firewall/IPTABLES state and skipping firewall/IPTABLES management." else PTXT "${INFO} LAN mode selected; no legacy firewall/IPTABLES state requires cleanup." fi ;; esac if ! yaml_nvars_file_action delete "#Asuswrt-Merlin AdGuardHome Installer" /jffs/scripts/dnsmasq.postconf; then all_event_scripts_transaction_rollback || true PTXT "${ERROR} Unable to clean the legacy dnsmasq event hook." adguard_recover_after_event_hook_abort "${RESTART_AFTER_ABORT}" end_op_message 1 "${install_action}" return 1 fi for i in init-start services-stop; do if ! /bin/grep -q "${ADDON_DIR}/AdGuardHome.sh $i &" "/jffs/scripts/${i}" 2>/dev/null && /bin/grep -q "${ADDON_DIR}/AdGuardHome.sh $i" "/jffs/scripts/${i}" 2>/dev/null; then if ! del_jffs_script "/jffs/scripts/${i}"; then all_event_scripts_transaction_rollback || true PTXT "${ERROR} Unable to clean the legacy ${i} event hook." adguard_recover_after_event_hook_abort "${RESTART_AFTER_ABORT}" end_op_message 1 "${install_action}" return 1 fi fi done case "${ADGUARD_INSTALL_MODE:-wan}" in wan) if ! install_wan_event_scripts; then all_event_scripts_transaction_rollback || true PTXT "${ERROR} Required Asuswrt-Merlin event scripts could not be configured." adguard_recover_after_event_hook_abort "${RESTART_AFTER_ABORT}" end_op_message 1 "${install_action}" return 1 fi ptxt_ok "Asuswrt-Merlin event scripts configured." ;; lan) if [ "${ADGUARD_DNSMASQ_MODE:-$(conf_value ADGUARD_DNSMASQ_MODE 2>/dev/null)}" = "disabled" ]; then if ! remove_dnsmasq_event_scripts; then all_event_scripts_transaction_rollback || true PTXT "${ERROR} Required dnsmasq event scripts could not be removed." adguard_recover_after_event_hook_abort "${RESTART_AFTER_ABORT}" end_op_message 1 "${install_action}" return 1 fi else if ! pidof dnsmasq >/dev/null 2>&1 || [ ! -f /etc/dnsmasq.conf ]; then ptxt_warn "dnsmasq is not running or /etc/dnsmasq.conf is unavailable; removing AdGuardHome dnsmasq event hooks." fi if ! add_dnsmasq_event_scripts; then all_event_scripts_transaction_rollback || true PTXT "${ERROR} Required dnsmasq event scripts could not be configured." adguard_recover_after_event_hook_abort "${RESTART_AFTER_ABORT}" end_op_message 1 "${install_action}" return 1 fi fi if wan_iptables_state_active; then if ! add_firewall_event_scripts; then all_event_scripts_transaction_rollback || true PTXT "${ERROR} Required firewall event script could not be configured for the active WAN IPTABLES state." adguard_recover_after_event_hook_abort "${RESTART_AFTER_ABORT}" end_op_message 1 "${install_action}" return 1 fi else if ! remove_firewall_event_scripts; then all_event_scripts_transaction_rollback || true PTXT "${ERROR} Required firewall event script could not be removed for LAN mode." adguard_recover_after_event_hook_abort "${RESTART_AFTER_ABORT}" end_op_message 1 "${install_action}" return 1 fi if ! adguardhome_yaml_remove_ipset_file "${YAML_FILE}" || ! adguardhome_yaml_remove_ipset_file "${YAML_ORI}" || ! write_conf ADGUARD_IPSET '"NO"'; then all_event_scripts_transaction_rollback || true PTXT "${ERROR} IPSET could not be disabled after the qualifying WAN IPTABLES state was lost." adguard_recover_after_event_hook_abort "${RESTART_AFTER_ABORT}" end_op_message 1 "${install_action}" return 1 fi fi if ! add_init_event_scripts || ! add_services_event_scripts; then all_event_scripts_transaction_rollback || true PTXT "${ERROR} Required Asuswrt-Merlin event scripts could not be configured." adguard_recover_after_event_hook_abort "${RESTART_AFTER_ABORT}" end_op_message 1 "${install_action}" return 1 fi ptxt_ok "Asuswrt-Merlin event scripts configured." ;; *) all_event_scripts_transaction_rollback || true PTXT "${ERROR} Unknown AdGuardHome install mode: ${ADGUARD_INSTALL_MODE:-}. Setup was aborted." adguard_recover_after_event_hook_abort "${RESTART_AFTER_ABORT}" end_op_message 1 "${install_action}" return 1 ;; esac if [ "${install_action:-update}" != "${INSTALLER_ACTION_UPDATE:-update}" ]; then ptxt_phase "Configuring AdGuardHome first-run settings." if [ ! -f "${ADDON_DIR}/localtime" ]; then if [ "${install_action:-install}" = "${INSTALLER_ACTION_RESTORE:-RESTORE}" ]; then PTXT "${ERROR} The prepared restore timezone is unavailable." else PTXT "${INFO} Before setting up AdGuardHome, please choose your right timezone!" fi if [ "${install_action:-install}" = "${INSTALLER_ACTION_RESTORE:-RESTORE}" ] || ! set_timezone; then all_event_scripts_transaction_rollback || true MIGRATE_STATUS=1 if ! adguard_recover_after_event_hook_abort "${RESTART_AFTER_ABORT}"; then MIGRATE_STATUS=2 fi end_op_message 1 "${install_action}" return "${MIGRATE_STATUS}" fi fi if ! setup_AdGuardHome "" "${install_action:-install}"; then all_event_scripts_transaction_rollback || true MIGRATE_STATUS=1 if ! adguard_recover_after_event_hook_abort "${RESTART_AFTER_ABORT}"; then MIGRATE_STATUS=2 fi end_op_message 1 return "${MIGRATE_STATUS}" fi ptxt_ok "AdGuardHome first-run configuration completed." fi ptxt_phase "Final readiness summary." ptxt_step "Waiting for AdGuardHome post-install readiness checks." if ! agh_complete_startup; then all_event_scripts_transaction_rollback || true MIGRATE_STATUS=1 if ! adguard_recover_after_event_hook_abort "${RESTART_AFTER_ABORT}" 1; then MIGRATE_STATUS=2 fi if [ "${ADGUARD_INSTALL_RESTART_PENDING:-0}" != "1" ]; then adguard_install_abort_trap_disable_preserve_defer fi end_op_message 1 "${install_action}" return "${MIGRATE_STATUS}" fi if [ "${ADGUARD_INSTALL_MODE:-wan}" = "wan" ]; then if ! finalize_dns_environment; then all_event_scripts_transaction_rollback || true PTXT "${ERROR} Unable to commit the completed DNS and setup NVRAM transactions." adguard_recover_after_event_hook_abort "${RESTART_AFTER_ABORT}" 1 end_op_message 1 "${install_action}" return 1 fi elif ! nvram_transaction_finalize_setup_pair; then all_event_scripts_transaction_rollback || true PTXT "${ERROR} Unable to commit the completed setup NVRAM transaction." adguard_recover_after_event_hook_abort "${RESTART_AFTER_ABORT}" 1 end_op_message 1 "${install_action}" return 1 fi if [ "${install_action:-update}" != "${INSTALLER_ACTION_RESTORE:-RESTORE}" ] && ! finalize_pending_mode_migration; then all_event_scripts_transaction_rollback || true PTXT "${ERROR} Unable to remove completed mode-migration recovery backups." adguard_recover_after_event_hook_abort "${RESTART_AFTER_ABORT}" 1 end_op_message 1 "${install_action}" return 1 fi ADGUARD_INSTALL_COMMIT_PENDING="1" if ! all_event_scripts_transaction_commit; then ADGUARD_INSTALL_COMMIT_PENDING="0" EVENT_HOOK_ROLLBACK_FAILED=0 all_event_scripts_transaction_rollback || EVENT_HOOK_ROLLBACK_FAILED=1 PTXT "${ERROR} Unable to finalize the event-hook recovery transaction." adguard_recover_after_event_hook_abort "${RESTART_AFTER_ABORT}" 1 "${EVENT_HOOK_ROLLBACK_FAILED}" end_op_message 1 "${install_action}" return 1 fi if [ "${ADGUARD_INSTALL_REPLACE_ACTIVE:-0}" = "1" ]; then COMMITTED_OLD_BINARY="${ADGUARD_INSTALL_OLD_BINARY}" if ! adguard_committed_binary_cleanup_finalize "${COMMITTED_OLD_BINARY}"; then end_op_message 1 "${install_action}" return 1 fi fi adguard_install_abort_trap_disable_preserve_defer ptxt_ok "AdGuardHome ${install_action:-install} orchestration complete." ptxt_ok "Final readiness summary: service is installed, started, and ready." end_op_message 0 "${install_action}" } filter_timezone_members() { awk ' !/\/$/ { raw = $0 timezone = $0 sub(/^\.\//, "", timezone) if (timezone ~ /^usr\/share\/zoneinfo\/posix\// && timezone !~ /(^|\/)\.\.(\/|$)/ && !(timezone in seen)) { seen[timezone] = 1 print raw } } ' } format_timezone_menu() { filter_timezone_members | sort | awk -v INDEX=0 '{timezone=$0; sub(/^\.\//, "", timezone); sub(/^usr\/share\/zoneinfo\/posix\//, "", timezone); ++INDEX; printf " %d) %s\n", INDEX, timezone}' } set_timezone() { local TMP TZ_ARCH TZ_DATA INDEX TZ_FILE TZ_LIST TZ_LOCALTIME_TMP TZ_ZONEINFO_DIR TMP="/root" TZ_ARCH="$(uname -m)" case "${TZ_ARCH}" in "aarch64" | "arm64") TZ_ARCH="aarch64" ;; "armv7l") TZ_ARCH="arm" ;; esac TZ_DATA="tzdata-2026f-1-${TZ_ARCH}.pkg.tar.bz2" ensure_opkg_package column --force-depends --force-overwrite --force-reinstall || return 1 ai_have_cmd sort || { PTXT "${ERROR} Required timezone selection command is unavailable: sort" return 1 } [ -x /opt/bin/column ] || { PTXT "${ERROR} Required Entware timezone selection command is unavailable: /opt/bin/column" return 1 } download_file "${TMP}" 644 "${RURL}/${TZ_DATA}" || return 1 TZ_LIST="$(tar tjf "${TMP}/${TZ_DATA}" 2>/dev/null)" || { PTXT "${ERROR} Unable to read timezone archive ${TZ_DATA}." return 1 } INDEX="$(PTXT "${TZ_LIST}" | filter_timezone_members | awk 'END {print NR + 0}')" [ "${INDEX}" -gt 0 ] || { PTXT "${ERROR} No timezone files were found in ${TZ_DATA}." return 1 } PTXT "${INFO} Available timezones/locations:" PTXT "${TZ_LIST}" | format_timezone_menu | /opt/bin/column if ! read_input_num "Select your timezone/location" 1 "${INDEX}"; then PTXT "${ERROR} Unable to select a timezone." return 1 fi TZ_FILE="$(PTXT "${TZ_LIST}" | filter_timezone_members | sort | awk -v INDEX="$CHOSEN" '{++i}i==INDEX{print $0}')" [ -n "${TZ_FILE}" ] || return 1 PTXT "${INFO} ${TZ_FILE##*/} selected" case "${TZ_FILE}" in ./*) TZ_ZONEINFO_DIR="./usr/share/zoneinfo" ;; *) TZ_ZONEINFO_DIR="usr/share/zoneinfo" ;; esac if ! PTXT "${TZ_LIST}" | awk ' { member = $0 sub(/^\.\//, "", member) if (($0 != "." && $0 != "./") && (member == "" || member ~ /^\// || member ~ /(^|\/)\.\.(\/|$)/)) exit 1 } '; then PTXT "${ERROR} Unsafe path found in timezone archive ${TZ_DATA}." return 1 fi # BusyBox tar does not consistently recurse into a named archive directory. # Extract the validated signed package so aliases may target any zoneinfo peer. tar xjf "${TMP}/${TZ_DATA}" -C "${TMP}" || return 1 TZ_LOCALTIME_TMP="${ADDON_DIR}/.localtime.$$" ADGUARD_TIMEZONE_TMP="${TZ_LOCALTIME_TMP}" if [ -e "${TZ_LOCALTIME_TMP}" ] || [ -L "${TZ_LOCALTIME_TMP}" ] || ! (umask 077 && set -C && : >"${TZ_LOCALTIME_TMP}"); then ADGUARD_TIMEZONE_TMP="" PTXT "${ERROR} Unable to create a temporary timezone file" return 1 fi # Copying into a fresh regular file dereferences upstream timezone aliases. if ! cp "${TMP}/${TZ_FILE}" "${TZ_LOCALTIME_TMP}"; then rm -f "${TZ_LOCALTIME_TMP}" ADGUARD_TIMEZONE_TMP="" PTXT "${ERROR} Unable to set your timezone file" return 1 fi if ! chmod 755 "${ADDON_DIR}" || ! chmod 644 "${TZ_LOCALTIME_TMP}"; then rm -f "${TZ_LOCALTIME_TMP}" ADGUARD_TIMEZONE_TMP="" PTXT "${ERROR} Unable to make the timezone data readable" return 1 fi if ! mv -f "${TZ_LOCALTIME_TMP}" "${ADDON_DIR}/localtime"; then rm -f "${TZ_LOCALTIME_TMP}" ADGUARD_TIMEZONE_TMP="" PTXT "${ERROR} Unable to replace the timezone file" return 1 fi ADGUARD_TIMEZONE_TMP="" rm -rf "${TMP:?}/${TZ_DATA}" "${TMP:?}/usr" } setup_AdGuardHome() { if [ ! -d "${TARG_DIR}" ] || [ ! -f "${AGH_FILE}" ]; then PTXT "${ERROR} AdGuardHome is not installed. Aborting...." return 1 fi ptxt_phase "Configuring AdGuardHome using ${YAML_FILE}." setup_AdGuardHome_impl "$@" local RET="$?" if [ "$1" = "reconfig" ]; then if [ "$RET" -eq 0 ]; then ptxt_step "Restarting AdGuardHome service with ${YAML_FILE}." if ! agh_restart; then agh_start_error end_op_message 1 return fi ptxt_ok "AdGuardHome setup is complete." end_op_message 0 else end_op_message 1 fi fi return "${RET}" } save_installer_config() { local CONFIG_BACKUP="$1" rm -f "${CONFIG_BACKUP}" "${CONFIG_BACKUP}.absent" if [ -f "${CONF_FILE}" ]; then cp -p "${CONF_FILE}" "${CONFIG_BACKUP}" || return 1 else : >"${CONFIG_BACKUP}.absent" || return 1 fi } restore_installer_config() { local CONFIG_BACKUP="$1" RETAIN_BACKUP="${2:-0}" CONFIG_STAGE="${CONF_FILE}.mode-migration.restore.$$" if [ -f "${CONFIG_BACKUP}" ]; then rm -f "${CONFIG_STAGE}" || return 1 cp -p "${CONFIG_BACKUP}" "${CONFIG_STAGE}" || { rm -f "${CONFIG_STAGE}" return 1 } mv -f "${CONFIG_STAGE}" "${CONF_FILE}" || { rm -f "${CONFIG_STAGE}" return 1 } elif [ -f "${CONFIG_BACKUP}.absent" ]; then rm -f "${CONF_FILE}" || return 1 fi [ "${RETAIN_BACKUP}" = "1" ] || discard_installer_config_backup "${CONFIG_BACKUP}" } discard_installer_config_backup() { rm -f "$1" "$1.absent" } setup_restore_previous_yaml() { if [ "${YAML_BACKED_UP:-0}" -eq 1 ]; then if mv -f "${YAML_BAK}" "${YAML_FILE}"; then rollback_result_write "setup-yaml" "${INSTALLER_ROLLBACK_COMPLETE:-rollback complete}" "previous ${YAML_FILE} restored" rollback_result_notice return 0 fi rollback_result_write "setup-yaml" "${INSTALLER_ROLLBACK_FAILED:-rollback failed}" "could not restore previous ${YAML_FILE}" rollback_result_notice PTXT "${ERROR} Unable to restore the previous AdGuardHome configuration." return 1 fi return 0 } setup_restore_nvram_journal() { if [ "${SETUP_FILES_JOURNALED:-0}" -ne 1 ] && [ ! -d "${BASE_DIR}/.AdGuardHome.nvram/setup-files" ]; then return 0 fi # The shared marker is the setup commit point. Deferred journal cleanup # must never turn a committed file publication back into a rollback. ! nvram_transaction_setup_committed || return 0 if nvram_transaction_setup_files_restore; then SETUP_FILES_JOURNALED=0 else PTXT "${ERROR} Unable to restore the setup journal; recovery state was preserved." return 1 fi } # Shared guard for setup_AdGuardHome_impl call sites: journals the current # setup files on first use only, so repeated calls do not re-journal. Relies # on the caller's SETUP_FILES_JOURNALED local variable. setup_files_begin_if_needed() { [ "${SETUP_FILES_JOURNALED}" -eq 0 ] || return 0 # A mode-migration or earlier setup stage can create the shared journal # before this setup_AdGuardHome_impl frame is entered. Reuse it only while # this installer process still owns the transaction lock. if [ -d "${BASE_DIR}/.AdGuardHome.nvram/setup-files" ] && [ ! -L "${BASE_DIR}/.AdGuardHome.nvram/setup-files" ]; then nvram_transaction_lock_owned || { nvram_transaction_lock_failure "operation=journal-reuse path=${BASE_DIR}/.AdGuardHome.nvram/setup-files reason=lock-ownership-rejected" || true return 1 } SETUP_FILES_JOURNALED=1 return 0 fi nvram_transaction_setup_files_begin || return 1 SETUP_FILES_JOURNALED=1 } # setup_files_journal_diagnostic reports why setup journal reuse is unsafe. setup_files_journal_diagnostic() { local artifact current_process_identity journal_root lock_owned publication_artifact publication_listing publication_owner journal_root="${BASE_DIR}/.AdGuardHome.nvram/setup-files" current_process_identity="$(nvram_transaction_lock_owner_current 2>/dev/null)" || current_process_identity="unavailable" lock_owned="no" nvram_transaction_lock_owned && lock_owned="yes" PTXT "${WARNING} Setup journal diagnostic: path=${journal_root}, frame-journaled=${SETUP_FILES_JOURNALED:-0}, lock-mode=${NVRAM_TRANSACTION_LOCK_MODE:-unset}, lock-owned=${lock_owned}, current-process-identity=${current_process_identity}, failure=${NVRAM_TRANSACTION_LOCK_DIAGNOSTIC:-none}." if [ -e "${journal_root}" ] || [ -L "${journal_root}" ]; then PTXT "${WARNING} Setup journal state: $(ls -ld "${journal_root}" 2>/dev/null)." else PTXT "${WARNING} Setup journal state: absent." fi for artifact in "${BASE_DIR}/.AdGuardHome.nvram.lock"*; do [ -e "${artifact}" ] || [ -L "${artifact}" ] || continue PTXT "${WARNING} Setup journal lock artifact: $(ls -ld "${artifact}" 2>/dev/null)." if [ -f "${artifact}/pid" ] && [ ! -L "${artifact}/pid" ]; then PTXT "${WARNING} Setup journal lock owner file ${artifact}/pid: $(cat "${artifact}/pid" 2>/dev/null)." elif [ -L "${artifact}" ]; then PTXT "${WARNING} Setup journal lock target ${artifact}: $(nvram_transaction_lock_readlink "${artifact}" 2>/dev/null)." fi done for publication_artifact in "${BASE_DIR}/.AdGuardHome.nvram.lock.reaper.claim."* "${BASE_DIR}/.AdGuardHome.nvram.lock.reaper.symlink."*; do [ -e "${publication_artifact}" ] || [ -L "${publication_artifact}" ] || continue publication_listing="$(ls -ld "${publication_artifact}" 2>/dev/null)" PTXT "${WARNING} Setup journal lock publication artifact: ${publication_listing}." if [ -d "${publication_artifact}" ] && [ ! -L "${publication_artifact}" ] && [ -f "${publication_artifact}/pid" ] && [ ! -L "${publication_artifact}/pid" ]; then publication_owner="" IFS= read -r publication_owner <"${publication_artifact}/pid" || true PTXT "${WARNING} Setup journal lock publication owner file ${publication_artifact}/pid: ${publication_owner}." elif [ -L "${publication_artifact}" ]; then PTXT "${WARNING} Setup journal lock publication target ${publication_artifact}: $(nvram_transaction_lock_readlink "${publication_artifact}" 2>/dev/null)." fi done } setup_resolve_lan_addresses() { local LAN_IF NVRAM_ADDR4 NVRAM_ADDR6 NET_ADDR="" NET_ADDR6="" LAN_IF="$(installer_nvram get lan_ifname)" SETUP_LAN_IF="${LAN_IF}" if [ -n "${LAN_IF}" ] && ai_have_cmd ip; then NET_ADDR="$(ip -o -4 addr list "${LAN_IF}" scope global 2>/dev/null | /usr/bin/awk ' $0 !~ /(^|[[:space:]])(tentative|deprecated)([[:space:]]|$)/ { split($4, ip_addr, "/") if (!seen[ip_addr[1]]++) { print ip_addr[1]; exit } }')" NET_ADDR6="$(ip -o -6 addr list "${LAN_IF}" scope global 2>/dev/null | /usr/bin/awk ' $0 !~ /(^|[[:space:]])(tentative|deprecated|dadfailed|temporary)([[:space:]]|$)/ { split($4, ip_addr, "/") if (!seen[ip_addr[1]]++) { print ip_addr[1]; exit } }')" fi if [ -z "${NET_ADDR}" ]; then NVRAM_ADDR4="$(installer_nvram get lan_ipaddr)" if [ -n "${LAN_IF}" ] && ai_have_cmd ip; then NET_ADDR="$(ip -o -4 addr list "${LAN_IF}" scope global 2>/dev/null | /usr/bin/awk -v candidate="${NVRAM_ADDR4}" ' $0 !~ /(^|[[:space:]])(tentative|deprecated)([[:space:]]|$)/ { split($4, ip_addr, "/") if (ip_addr[1] == candidate) { print candidate; exit } }')" else NET_ADDR="${NVRAM_ADDR4}" fi fi if [ -z "${NET_ADDR6}" ] && [ -n "${LAN_IF}" ] && ai_have_cmd ip; then NVRAM_ADDR6="$(installer_nvram get ipv6_rtr_addr)" case "${NVRAM_ADDR6}" in "" | ::) ;; *:*) NET_ADDR6="$(ip -o -6 addr list "${LAN_IF}" scope global 2>/dev/null | /usr/bin/awk -v candidate="${NVRAM_ADDR6}" ' $0 !~ /(^|[[:space:]])(tentative|deprecated|dadfailed|temporary)([[:space:]]|$)/ { split($4, ip_addr, "/") if (ip_addr[1] == candidate) { print candidate; exit } }')" ;; esac fi } setup_resolve_bind_addresses() { SETUP_WEB_ADDRESS="" SETUP_DNS_BIND_HOST="" SETUP_DNS_BIND_HOST6="" case "${ADGUARD_INSTALL_MODE:-wan}" in wan) SETUP_WEB_ADDRESS="0.0.0.0:${WEB_PORT}" SETUP_DNS_BIND_HOST="0.0.0.0" ;; lan) setup_resolve_lan_addresses if [ -z "${NET_ADDR}" ]; then PTXT "${ERROR} Unable to resolve the LAN IPv4 address for AdGuardHome LAN mode. Setup was aborted." return 1 fi SETUP_WEB_ADDRESS="${NET_ADDR}:${WEB_PORT}" SETUP_DNS_BIND_HOST="${NET_ADDR}" SETUP_DNS_BIND_HOST6="${NET_ADDR6}" ;; *) PTXT "${ERROR} Unknown AdGuardHome install mode: ${ADGUARD_INSTALL_MODE}. Setup was aborted." return 1 ;; esac } setup_reverse_upstream_target() { case "${ADGUARD_INSTALL_MODE:-wan}" in wan) SETUP_REVERSE_UPSTREAM="[::]:553" ;; lan) # Use explicit config first, otherwise detected main-router IP. SETUP_REVERSE_UPSTREAM="${ADGUARD_LAN_REVERSE_UPSTREAM:-}" [ -n "${SETUP_REVERSE_UPSTREAM}" ] || SETUP_REVERSE_UPSTREAM="$(conf_value ADGUARD_LAN_REVERSE_UPSTREAM 2>/dev/null)" case "${SETUP_REVERSE_UPSTREAM}" in 0.0.0.0) return 1 ;; esac if [ -z "${SETUP_REVERSE_UPSTREAM}" ]; then SETUP_REVERSE_UPSTREAM="$(installer_nvram get lan_gateway 2>/dev/null)" case "${SETUP_REVERSE_UPSTREAM}" in "" | 0.0.0.0) return 1 ;; esac fi ipv4_is_valid "${SETUP_REVERSE_UPSTREAM}" || return 1 SETUP_REVERSE_UPSTREAM="${SETUP_REVERSE_UPSTREAM}:53" ;; *) PTXT "${ERROR} Unknown AdGuardHome install mode: ${ADGUARD_INSTALL_MODE}. Setup was aborted." return 1 ;; esac } setup_private_ipv4_bridge_dns_binds() { local bridge_binds primary_if bridge_binds="" primary_if="${1:-${SETUP_LAN_IF:-}}" [ -n "${primary_if}" ] || return 0 if ai_have_cmd ip; then bridge_binds="$(ip -o -4 addr show scope global 2>/dev/null | /usr/bin/awk -v lan_addr="${NET_ADDR}" -v primary_if="${primary_if}" ' function usable_ip(ip, broadcast, octets) { split(ip, octets, ".") return ip != broadcast && ip ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ && (octets[1] == 10 || (octets[1] == 172 && octets[2] >= 16 && octets[2] <= 31) || (octets[1] == 192 && octets[2] == 168)) && octets[3] <= 255 && octets[4] <= 255 } $2 ~ /^br/ && $2 != primary_if && $0 !~ /(^|[[:space:]])(tentative|deprecated)([[:space:]]|$)/ { broadcast = "" for (i = 1; i < NF; i++) if ($i == "brd") broadcast = $(i + 1) for (i = 1; i <= NF; i++) { if ($i == "inet") { split($(i + 1), ip_addr, "/") if (ip_addr[1] != lan_addr && usable_ip(ip_addr[1], broadcast) && !seen[ip_addr[1]]++) { print ip_addr[1] } } } } ')" if [ -z "${bridge_binds}" ]; then bridge_binds="$(ip -4 addr show scope global 2>/dev/null | /usr/bin/awk -v lan_addr="${NET_ADDR}" -v primary_if="${primary_if}" ' function usable_ip(ip, broadcast, octets) { split(ip, octets, ".") return ip != broadcast && ip ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ && (octets[1] == 10 || (octets[1] == 172 && octets[2] >= 16 && octets[2] <= 31) || (octets[1] == 192 && octets[2] == 168)) && octets[3] <= 255 && octets[4] <= 255 } /^[0-9]+: / { iface = $2 sub(/:$/, "", iface) } $1 == "inet" && iface ~ /^br/ && iface != primary_if && $0 !~ /(^|[[:space:]])(tentative|deprecated)([[:space:]]|$)/ { split($2, ip_addr, "/") broadcast = "" for (i = 1; i < NF; i++) if ($i == "brd") broadcast = $(i + 1) if (ip_addr[1] != lan_addr && usable_ip(ip_addr[1], broadcast) && !seen[ip_addr[1]]++) { print ip_addr[1] } } ')" fi fi printf "%s\n" "${bridge_binds}" } setup_write_dns_bind_hosts() { local target_file="$1" PTXT "dns:" " bind_hosts:" >>"${target_file}" case "${ADGUARD_INSTALL_MODE:-wan}" in lan) { PTXT "127.0.0.1" PTXT "${SETUP_DNS_BIND_HOST}" PTXT "${SETUP_DNS_BIND_HOST6}" setup_private_ipv4_bridge_dns_binds "${SETUP_LAN_IF:-}" } | awk 'NF && !seen[$0]++ { print " - " $0 }' >>"${target_file}" ;; *) PTXT " - ${SETUP_DNS_BIND_HOST}" >>"${target_file}" ;; esac } setup_default_web_host() { local LAN_ADDR case "${ADGUARD_INSTALL_MODE:-wan}" in wan) PTXT "0.0.0.0" ;; lan) setup_resolve_lan_addresses LAN_ADDR="${NET_ADDR}" if ! ipv4_is_valid "${LAN_ADDR}"; then PTXT "${ERROR} Unable to resolve the LAN IPv4 address for AdGuardHome LAN mode. Setup was aborted." return 1 fi PTXT "${LAN_ADDR}" ;; *) PTXT "${ERROR} Unknown AdGuardHome install mode: ${ADGUARD_INSTALL_MODE}. Setup was aborted." return 1 ;; esac } setup_sync_webui_port() { local NEW_PORT CURRENT_WEB_LINE CURRENT_WEB_ADDRESS CURRENT_WEB_HOST CURRENT_WEB_PORT NEW_WEB_ADDRESS YAML_SYNC_TMP_FILE NEW_PORT="$1" port_is_valid "${NEW_PORT}" || return 1 CURRENT_WEB_LINE="$(awk ' function yaml_key_is(line, expected, text, separator, key) { text = line sub(/^[[:space:]]*/, "", text) separator = index(text, ":") if (!separator) return 0 key = substr(text, 1, separator - 1) sub(/[[:space:]]*$/, "", key) return key == expected || key == "\"" expected "\"" || key == sprintf("%c%s%c", 39, expected, 39) } function block_http_header(line, text, separator, value) { text = line separator = index(text, ":") value = substr(text, separator + 1) sub(/^[[:space:]]*/, "", value) return value ~ /^(|#.*|&[^[:space:]#]+([[:space:]]*#.*)?)$/ } /^[^[:space:]]/ && yaml_key_is($0, "http") { if (!block_http_header($0)) exit in_http = 1 next } in_http && /^[^[:space:]#]/ { exit } in_http && yaml_key_is($0, "address") { print; exit } ' "${YAML_FILE}")" CURRENT_WEB_ADDRESS="$(printf '%s\n' "${CURRENT_WEB_LINE}" | sed 's/^[[:space:]]*["'"'"']\{0,1\}address["'"'"']\{0,1\}[[:space:]]*:[[:space:]]*//; s/[[:space:]][[:space:]]*#.*$//; s/[[:space:]]*$//')" case "${CURRENT_WEB_ADDRESS}" in \"*\") CURRENT_WEB_ADDRESS="${CURRENT_WEB_ADDRESS#\"}" CURRENT_WEB_ADDRESS="${CURRENT_WEB_ADDRESS%\"}" ;; \'*\') CURRENT_WEB_ADDRESS="${CURRENT_WEB_ADDRESS#\'}" CURRENT_WEB_ADDRESS="${CURRENT_WEB_ADDRESS%\'}" ;; esac CURRENT_WEB_PORT="$(printf '%s\n' "${CURRENT_WEB_ADDRESS}" | awk -F':' 'NF > 1 { print $NF }')" if [ -n "${CURRENT_WEB_ADDRESS}" ] && runtime_port_is_valid "${CURRENT_WEB_PORT}"; then CURRENT_WEB_HOST="${CURRENT_WEB_ADDRESS%:${CURRENT_WEB_PORT}}" fi [ -n "${CURRENT_WEB_HOST:-}" ] || CURRENT_WEB_HOST="$(setup_default_web_host)" || return 1 NEW_WEB_ADDRESS="${CURRENT_WEB_HOST}:${NEW_PORT}" case "${CURRENT_WEB_HOST}" in \[*\]) NEW_WEB_ADDRESS="\"${NEW_WEB_ADDRESS}\"" ;; esac if [ -n "${CURRENT_WEB_LINE}" ]; then yaml_nvars_file_action replace "${CURRENT_WEB_LINE}" " address: ${NEW_WEB_ADDRESS}" "${YAML_FILE}" else YAML_SYNC_TMP_FILE="${YAML_FILE}.webui-port.$$" awk -v address=" address: ${NEW_WEB_ADDRESS}" ' function http_header(line, text, separator, key, value) { text = line separator = index(text, ":") if (!separator) return 0 key = substr(text, 1, separator - 1) sub(/[[:space:]]*$/, "", key) value = substr(text, separator + 1) sub(/^[[:space:]]*/, "", value) if (value !~ /^(|#.*|&[^[:space:]#]+([[:space:]]*#.*)?)$/) return 0 return key == "http" || key == "\"http\"" || key == sprintf("%chttp%c", 39, 39) } { print } !inserted && /^[^[:space:]]/ && http_header($0) { print address inserted = 1 } END { exit(inserted ? 0 : 1) } ' "${YAML_FILE}" >"${YAML_SYNC_TMP_FILE}" && adguardhome_yaml_secure_file "${YAML_SYNC_TMP_FILE}" && mv -f "${YAML_SYNC_TMP_FILE}" "${YAML_FILE}" || { rm -f "${YAML_SYNC_TMP_FILE}" return 1 } fi } setup_sync_mode_dependent_yaml() { local bind_hosts old_reverse reverse_upstream sync_file target_file web_address web_port target_file="${1:-${YAML_FILE}}" web_port="$(conf_value ADGUARD_WEBUI_PORT 2>/dev/null)" if ! port_is_valid "${web_port}"; then web_port="$(awk ' function yaml_key_is(line, expected, text, separator, key) { text = line sub(/^[[:space:]]*/, "", text) separator = index(text, ":") if (!separator) return 0 key = substr(text, 1, separator - 1) sub(/[[:space:]]*$/, "", key) return key == expected || key == "\"" expected "\"" || key == sprintf("%c%s%c", 39, expected, 39) } function http_header(line, text, separator, key, value) { text = line sub(/^[[:space:]]*/, "", text) separator = index(text, ":") if (!separator) return 0 key = substr(text, 1, separator - 1) sub(/[[:space:]]*$/, "", key) value = substr(text, separator + 1) sub(/^[[:space:]]*/, "", value) return (key == "http" || key == "\"http\"" || key == sprintf("%chttp%c", 39, 39)) && (value == "" || value ~ /^#/ || value ~ /^&[^][{},[:space:]]+([[:space:]]*#.*)?$/) } /^[^[:space:]]/ && http_header($0) { in_http = 1; next } in_http && /^[^[:space:]]/ { exit } in_http && yaml_key_is($0, "address") { value = $0 sub(/[[:space:]]+#.*$/, "", value) gsub(/[[:space:]"'\''"]/, "", value) component_count = split(value, address_components, ":") print address_components[component_count] exit } ' "${target_file}" 2>/dev/null)" fi runtime_port_is_valid "${web_port}" || web_port="3000" case "${ADGUARD_INSTALL_MODE:-wan}" in lan) WEB_PORT="${web_port}" setup_resolve_bind_addresses || return 1 setup_reverse_upstream_target || return 1 web_address="${SETUP_WEB_ADDRESS}" reverse_upstream="${SETUP_REVERSE_UPSTREAM}" old_reverse="[::]:553" bind_hosts="$( { PTXT "127.0.0.1" PTXT "${SETUP_DNS_BIND_HOST}" PTXT "${SETUP_DNS_BIND_HOST6}" setup_private_ipv4_bridge_dns_binds "${SETUP_LAN_IF:-}" } | awk 'NF && !seen[$0]++ { print }' )" ;; wan) old_reverse="$(conf_value ADGUARD_LAN_REVERSE_UPSTREAM 2>/dev/null)" case "${old_reverse}" in "") old_reverse="$(installer_nvram get lan_gateway 2>/dev/null)" ;; esac case "${old_reverse}" in *:*) ;; ?*) old_reverse="${old_reverse}:53" ;; esac web_address="0.0.0.0:${web_port}" reverse_upstream="[::]:553" bind_hosts="0.0.0.0" ;; *) return 1 ;; esac sync_file="${target_file}.wan-sync.$$" awk -v bind_hosts="${bind_hosts}" -v install_mode="${ADGUARD_INSTALL_MODE:-wan}" -v old_reverse="${old_reverse}" -v new_reverse="${reverse_upstream}" -v web_address="${web_address}" ' function indentation(line) { match(line, /^[[:space:]]*/); return RLENGTH } function yaml_key_is(line, expected, text, separator, key) { text = line sub(/^[[:space:]]*/, "", text) separator = index(text, ":") if (!separator) return 0 key = substr(text, 1, separator - 1) sub(/[[:space:]]*$/, "", key) return key == expected || key == "\"" expected "\"" || key == sprintf("%c%s%c", 39, expected, 39) } function yaml_mapping_header_is(line, expected, text, separator, value) { if (!yaml_key_is(line, expected)) return 0 text = line separator = index(text, ":") value = substr(text, separator + 1) sub(/^[[:space:]]*/, "", value) return value == "" || value ~ /^#/ || value ~ /^&[^][{},[:space:]]+([[:space:]]*#.*)?$/ } function replace_reverse_endpoint(line, old, replacement, allow_plain, value, prefix, position) { value = line sub(/[[:space:]]+#.*$/, "", value) sub(/[[:space:]]*$/, "", value) sub(/^[[:space:]]*-[[:space:]]*/, "", value) if (substr(value, 1, 1) == "\"" || substr(value, 1, 1) == sprintf("%c", 39)) value = substr(value, 2) if (substr(value, length(value), 1) == "\"" || substr(value, length(value), 1) == sprintf("%c", 39)) value = substr(value, 1, length(value) - 1) position = index(value, old) if (!position || substr(value, position + length(old)) != "") return line prefix = substr(value, 1, position - 1) if (prefix == "" && !allow_plain) return line if (prefix != "" && (substr(prefix, 1, 1) != "[" || substr(prefix, length(prefix), 1) != "]")) return line position = index(line, old) return substr(line, 1, position - 1) replacement substr(line, position + length(old)) } indentation($0) == 0 && (yaml_key_is($0, "http") || yaml_key_is($0, "dns")) && !yaml_mapping_header_is($0, "http") && !yaml_mapping_header_is($0, "dns") { exit 1 } indentation($0) == 0 && yaml_mapping_header_is($0, "http") { in_http = 1; http_indent = indentation($0); print; next } in_http && indentation($0) <= http_indent && $0 !~ /^[[:space:]]*($|#)/ { in_http = 0 } in_http && yaml_key_is($0, "address") { separator = index($0, ":") print substr($0, 1, separator) " " web_address next } indentation($0) == 0 && yaml_mapping_header_is($0, "dns") { in_dns = 1; dns_indent = indentation($0); print; next } in_dns && indentation($0) <= dns_indent && $0 !~ /^[[:space:]]*($|#)/ { in_dns = 0; in_binds = 0 } in_dns && yaml_key_is($0, "bind_hosts") { bind_indent = indentation($0); in_binds = 1 separator = index($0, ":") bind_value = substr($0, separator + 1) sub(/^[[:space:]]*/, "", bind_value) if (install_mode == "wan" && bind_value != "" && bind_value !~ /^#/) { comment = "" comment_at = index(bind_value, "#") if (comment_at) comment = " " substr(bind_value, comment_at) print substr($0, 1, separator) " [0.0.0.0]" comment in_binds = 0 } else { print substr($0, 1, separator) count = split(bind_hosts, hosts, "\n") for (host = 1; host <= count; host++) if (hosts[host] != "") print substr($0, 1, bind_indent) " - " hosts[host] } next } in_binds && $0 ~ /^[[:space:]]*($|#)/ { next } in_binds && indentation($0) >= bind_indent && $0 ~ /^[[:space:]]*-[[:space:]]/ { next } in_binds && indentation($0) > bind_indent { next } in_binds { in_binds = 0 } in_dns && (yaml_key_is($0, "upstream_dns") || yaml_key_is($0, "local_ptr_upstreams")) && !yaml_mapping_header_is($0, "upstream_dns") && !yaml_mapping_header_is($0, "local_ptr_upstreams") { exit 1 } in_dns && (yaml_mapping_header_is($0, "upstream_dns") || yaml_mapping_header_is($0, "local_ptr_upstreams")) { reverse_indent = indentation($0); in_reverse_upstreams = 1 reverse_plain_allowed = yaml_key_is($0, "local_ptr_upstreams") print; next } in_reverse_upstreams && indentation($0) <= reverse_indent && $0 !~ /^[[:space:]]*($|#)/ { in_reverse_upstreams = 0 } in_reverse_upstreams && old_reverse != "" { $0 = replace_reverse_endpoint($0, old_reverse, new_reverse, reverse_plain_allowed) } { print } ' "${target_file}" >"${sync_file}" && adguardhome_yaml_secure_file "${sync_file}" && mv -f "${sync_file}" "${target_file}" && adguardhome_yaml_secure_file "${target_file}" || { rm -f "${sync_file}" return 1 } } setup_sync_restored_yaml_for_wan() { setup_sync_mode_dependent_yaml "$@" } setup_sync_mode_dependent_yaml_and_snapshot() { local yaml_file_stage="${YAML_FILE}.mode-sync.$$" yaml_ori_stage="${YAML_ORI}.mode-sync.$$" yaml_file_rollback="${YAML_FILE}.mode-sync.rollback.$$" if [ -f "${YAML_FILE}" ]; then cp -p "${YAML_FILE}" "${yaml_file_stage}" && cp -p "${YAML_FILE}" "${yaml_file_rollback}" || { rm -f "${yaml_file_stage}" "${yaml_ori_stage}" "${yaml_file_rollback}" return 1 } setup_sync_mode_dependent_yaml "${yaml_file_stage}" || { rm -f "${yaml_file_stage}" "${yaml_ori_stage}" "${yaml_file_rollback}" return 1 } case "${ADGUARD_INSTALL_MODE:-}" in lan) if ! adguard_ipset_allowed && ! adguardhome_yaml_remove_ipset_file "${yaml_file_stage}"; then rm -f "${yaml_file_stage}" "${yaml_ori_stage}" "${yaml_file_rollback}" return 1 fi ;; esac fi if [ -f "${YAML_ORI}" ]; then cp -p "${YAML_ORI}" "${yaml_ori_stage}" && setup_sync_mode_dependent_yaml "${yaml_ori_stage}" || { rm -f "${yaml_file_stage}" "${yaml_ori_stage}" "${yaml_file_rollback}" return 1 } case "${ADGUARD_INSTALL_MODE:-}" in lan) if ! adguard_ipset_allowed && ! adguardhome_yaml_remove_ipset_file "${yaml_ori_stage}"; then rm -f "${yaml_file_stage}" "${yaml_ori_stage}" "${yaml_file_rollback}" return 1 fi ;; esac fi if [ -f "${yaml_file_stage}" ]; then mv -f "${yaml_file_stage}" "${YAML_FILE}" || { rm -f "${yaml_file_stage}" "${yaml_ori_stage}" "${yaml_file_rollback}" return 1 } fi if [ -f "${yaml_ori_stage}" ] && ! mv -f "${yaml_ori_stage}" "${YAML_ORI}"; then [ ! -f "${yaml_file_rollback}" ] || mv -f "${yaml_file_rollback}" "${YAML_FILE}" rm -f "${yaml_file_stage}" "${yaml_ori_stage}" "${yaml_file_rollback}" return 1 fi rm -f "${yaml_file_rollback}" } setup_sync_restored_yaml_and_snapshot_for_wan() { setup_sync_mode_dependent_yaml_and_snapshot } restore_mode_migration_yaml() { local yaml_file_backup="$1" yaml_ori_backup="$2" retain_backup="${3:-0}" restore_status=0 local yaml_file_stage="${YAML_FILE}.mode-migration.restore.$$" yaml_ori_stage="${YAML_ORI}.mode-migration.restore.$$" rm -f "${yaml_file_stage}" "${yaml_ori_stage}" || return 1 if [ -f "${yaml_file_backup}" ]; then if cp -p "${yaml_file_backup}" "${yaml_file_stage}"; then mv -f "${yaml_file_stage}" "${YAML_FILE}" || restore_status=1 else restore_status=1 fi else rm -f "${YAML_FILE}" || restore_status=1 fi if [ -f "${yaml_ori_backup}" ]; then if cp -p "${yaml_ori_backup}" "${yaml_ori_stage}"; then mv -f "${yaml_ori_stage}" "${YAML_ORI}" || restore_status=1 else restore_status=1 fi else rm -f "${YAML_ORI}" || restore_status=1 fi rm -f "${yaml_file_stage}" "${yaml_ori_stage}" if [ "${restore_status}" -eq 0 ] && [ "${retain_backup}" != "1" ]; then rm -f "${yaml_file_backup}" "${yaml_ori_backup}" fi return "${restore_status}" } backup_mode_migration_wan_hooks() { local backup_dir="$1" hook stage_dir="${1}.stage.$$" rm -rf "${stage_dir}" || return 1 mkdir -p "${stage_dir}" || return 1 for hook in init-start services-stop dnsmasq.postconf firewall-start dnsmasq-sdn.postconf service-event-end; do if [ -f "/jffs/scripts/${hook}" ]; then cp -p "/jffs/scripts/${hook}" "${stage_dir}/${hook}" || { rm -rf "${stage_dir}" return 1 } else : >"${stage_dir}/.${hook}.missing" || { rm -rf "${stage_dir}" return 1 } fi done mv "${stage_dir}" "${backup_dir}" || { rm -rf "${stage_dir}" return 1 } } restore_mode_migration_wan_hooks() { local backup_dir="$1" retain_backup="${2:-0}" hook restore_status=0 hook_stage for hook in init-start services-stop dnsmasq.postconf firewall-start dnsmasq-sdn.postconf service-event-end; do if [ -f "${backup_dir}/.${hook}.missing" ]; then rm -f "/jffs/scripts/${hook}" || restore_status=1 elif [ -f "${backup_dir}/${hook}" ]; then hook_stage="/jffs/scripts/.${hook}.mode-migration.restore.$$" if rm -f "${hook_stage}" && cp -p "${backup_dir}/${hook}" "${hook_stage}"; then mv -f "${hook_stage}" "/jffs/scripts/${hook}" || restore_status=1 else restore_status=1 fi rm -f "${hook_stage}" fi done [ "${restore_status}" -ne 0 ] || [ "${retain_backup}" = "1" ] || rm -rf "${backup_dir}" return "${restore_status}" } finalize_pending_mode_migration() { local cleanup_status=0 yaml_file_backup yaml_ori_backup config_backup hooks_backup [ -n "${MODE_MIGRATION_YAML_FILE_BACKUP:-}" ] || return 0 yaml_file_backup="${MODE_MIGRATION_YAML_FILE_BACKUP}" yaml_ori_backup="${MODE_MIGRATION_YAML_ORI_BACKUP}" config_backup="${MODE_MIGRATION_CONFIG_BACKUP}" hooks_backup="${MODE_MIGRATION_HOOKS_BACKUP}" # Detach the pending marker before nonfatal cleanup so a signal cannot # attempt recovery from a snapshot that is being deleted. MODE_MIGRATION_YAML_FILE_BACKUP="" MODE_MIGRATION_YAML_ORI_BACKUP="" MODE_MIGRATION_CONFIG_BACKUP="" MODE_MIGRATION_HOOKS_BACKUP="" rm -f "${yaml_file_backup}" "${yaml_ori_backup}" || cleanup_status=1 discard_installer_config_backup "${config_backup}" || cleanup_status=1 rm -rf "${hooks_backup}" || cleanup_status=1 [ "${cleanup_status}" -eq 0 ] || PTXT "${WARN} Unable to remove completed mode-migration recovery backups; continuing with the completed migration." } rollback_pending_mode_migration() { local restore_status=0 cleanup_status=0 yaml_file_backup yaml_ori_backup config_backup hooks_backup [ -n "${MODE_MIGRATION_YAML_FILE_BACKUP:-}" ] || return 0 restore_mode_migration_yaml "${MODE_MIGRATION_YAML_FILE_BACKUP}" "${MODE_MIGRATION_YAML_ORI_BACKUP}" 1 || { PTXT "${ERROR} Unable to restore the previous AdGuardHome YAML configuration." restore_status=1 } restore_installer_config "${MODE_MIGRATION_CONFIG_BACKUP}" 1 || { PTXT "${ERROR} Unable to restore the previous installer mode configuration." restore_status=1 } restore_mode_migration_wan_hooks "${MODE_MIGRATION_HOOKS_BACKUP}" 1 || { PTXT "${ERROR} Unable to restore the previous event scripts." restore_status=1 } [ "${restore_status}" -eq 0 ] || return 1 if { [ "${PREVIOUS_ADGUARD_INSTALL_MODE:-}" = "wan" ] || [ -z "${PREVIOUS_ADGUARD_INSTALL_MODE:-}" ]; } && [ -x "${ADDON_DIR}/AdGuardHome.sh" ]; then "${ADDON_DIR}/AdGuardHome.sh" firewall >/dev/null 2>&1 || { PTXT "${ERROR} Unable to restore the previous WAN firewall state." return 1 } fi yaml_file_backup="${MODE_MIGRATION_YAML_FILE_BACKUP}" yaml_ori_backup="${MODE_MIGRATION_YAML_ORI_BACKUP}" config_backup="${MODE_MIGRATION_CONFIG_BACKUP}" hooks_backup="${MODE_MIGRATION_HOOKS_BACKUP}" # Recovery is complete. Detach it before best-effort artifact cleanup. MODE_MIGRATION_YAML_FILE_BACKUP="" MODE_MIGRATION_YAML_ORI_BACKUP="" MODE_MIGRATION_CONFIG_BACKUP="" MODE_MIGRATION_HOOKS_BACKUP="" rm -f "${yaml_file_backup}" "${yaml_ori_backup}" || cleanup_status=1 discard_installer_config_backup "${config_backup}" || cleanup_status=1 rm -rf "${hooks_backup}" || cleanup_status=1 [ "${cleanup_status}" -eq 0 ] || PTXT "${WARN} Unable to remove completed mode-migration recovery backups; continuing with the restored installation." } adguard_migrate_detected_install_mode() { local config_backup hooks_backup previous_mode yaml_file_backup yaml_ori_backup previous_mode="$1" if ! adguard_install_mode_confirmed; then PTXT "${WARN:-WARNING:} Skipping YAML, hooks, firewall, IPSET, dnsmasq, and NVRAM mode migration until router-mode detection is confirmed (value: ${ADGUARD_INSTALL_MODE_RAW:-unreadable})." return 0 fi case "${previous_mode}:${ADGUARD_INSTALL_MODE:-}" in wan:lan | lan:wan | :lan) if [ -n "${previous_mode}" ]; then PTXT "${INFO} Router mode changed from ${previous_mode} to ${ADGUARD_INSTALL_MODE}; updating mode-dependent AdGuardHome settings." else PTXT "${INFO} Existing installation detected in ${ADGUARD_INSTALL_MODE} mode; updating mode-dependent AdGuardHome settings." fi yaml_file_backup="${YAML_FILE}.mode-migration.rollback.$$" yaml_ori_backup="${YAML_ORI}.mode-migration.rollback.$$" config_backup="${CONF_FILE}.mode-migration.rollback.$$" hooks_backup="${CONF_FILE}.mode-migration-hooks.rollback.$$" rm -f "${yaml_file_backup}" "${yaml_ori_backup}" if ! rm -rf "${hooks_backup}"; then PTXT "${ERROR} Unable to clear stale mode-migration event-script backups." return 1 fi if { [ ! -f "${YAML_FILE}" ] || cp -p "${YAML_FILE}" "${yaml_file_backup}"; } && { [ ! -f "${YAML_ORI}" ] || cp -p "${YAML_ORI}" "${yaml_ori_backup}"; } && save_installer_config "${config_backup}"; then : else rm -f "${yaml_file_backup}" "${yaml_ori_backup}" discard_installer_config_backup "${config_backup}" PTXT "${ERROR} Unable to preserve the current mode configuration." return 1 fi MODE_MIGRATION_YAML_ORI_BACKUP="${yaml_ori_backup}" MODE_MIGRATION_CONFIG_BACKUP="${config_backup}" MODE_MIGRATION_HOOKS_BACKUP="${hooks_backup}" MODE_MIGRATION_YAML_FILE_BACKUP="${yaml_file_backup}" if ! setup_sync_mode_dependent_yaml_and_snapshot; then PTXT "${ERROR} Unable to update the AdGuardHome configuration for ${ADGUARD_INSTALL_MODE} mode." rollback_pending_mode_migration || return 2 return 1 fi if ! backup_mode_migration_wan_hooks "${hooks_backup}"; then PTXT "${ERROR} Unable to preserve the previous event scripts." rollback_pending_mode_migration || return 2 return 1 fi if [ "${previous_mode}" = "lan" ] && ! install_wan_event_scripts; then PTXT "${ERROR} Unable to install the required WAN-mode event scripts." rollback_pending_mode_migration || return 2 return 1 fi if ! write_conf ADGUARD_INSTALL_MODE "\"${ADGUARD_INSTALL_MODE}\"" || ! adguard_install_feature_defaults || ! write_conf ADGUARD_NETCHECK_MODE "\"${ADGUARD_INSTALL_MODE}\"" || { [ "${ADGUARD_INSTALL_MODE}" = "lan" ] && ! adguard_ipset_allowed && ! adguardhome_yaml_remove_ipset_file; }; then rollback_pending_mode_migration || return 2 return 1 fi return 0 ;; esac write_conf ADGUARD_INSTALL_MODE "\"${ADGUARD_INSTALL_MODE}\"" } setup_AdGuardHome_impl() { local CHOOSE_NEW=0 CONFIG_ROLLBACK="" DNS_FILTER_ROLLBACK_DIR="" DOMAIN_CONFIG_SELECTION="" DOMAIN_LAN_CHANGED=0 IPSET_SELECTION LAN_DOMAIN_REARM_FAILED=0 LAN_DOMAIN_ROLLBACK="" SETUP_FILES_JOURNALED="${3:-0}" WEB_PORT_SELECTION YAML_ORI_NEW="" YAML_ORI_ROLLBACK="" YAML_ORI_WAS_PRESENT=0 YAML_BACKED_UP=0 SCHEMA_VER="$("${AGH_FILE}" -v --version | awk '/Schema[[:space:]]version:/{ print $NF }' 2>/dev/null)" if [ -z "${SCHEMA_VER}" ]; then SCHEMA_VER="27"; fi if [ "${ADGUARD_FORCE_SETUP_YAML:-0}" = "1" ] && { [ -f "${YAML_FILE}" ] || [ -f "${YAML_ORI}" ]; }; then PTXT "${INFO} Restored mode differs from detected router mode; updating mode-dependent AdGuardHome settings." if ! setup_sync_mode_dependent_yaml_and_snapshot; then PTXT "${ERROR} Unable to update the restored AdGuardHome configuration. Setup was aborted." return 1 fi ADGUARD_FORCE_SETUP_YAML=0 fi if [ -z "$1" ] && [ -f "${YAML_FILE}" ]; then ############################################################ Drop-in code to be removed/modified as needed. ############################################################################################################################################################# if ! awk -F'=' '/ADGUARD_WEBUI_PORT/ {print $2}' "${CONF_FILE}" 2>/dev/null | sed -e 's/^"//' -e 's/"$//' | grep -qoE '^(3000|[3-9][0-9]{3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$'; then PTXT "${INFO} The Web Port for AdGuardHome WebUI has not been manually configured." \ "${INFO} Please set the preferred Web Port for AdGuardHome WebUI." \ "${INFO} The chosen port must reside within the port range 3000-65535" \ "${INFO} and not already in use by any other service." if ! read_input_port "Default is" 3000; then return 1 fi if nvram_transaction_setup_files_begin; then SETUP_FILES_JOURNALED=1 else PTXT "${ERROR} Unable to journal the current installer configuration before WebUI port setup." return 1 fi CONFIG_ROLLBACK="${TMPDIR:-/tmp}/AdGuardHome.config.webui-port.$$" if ! save_installer_config "${CONFIG_ROLLBACK}"; then setup_restore_nvram_journal || true PTXT "${ERROR} Unable to preserve the previous installer preferences." return 1 fi if ! write_conf ADGUARD_WEBUI_PORT "\"${WEB_PORT}\""; then restore_installer_config "${CONFIG_ROLLBACK}" || PTXT "${ERROR} Unable to restore the previous installer preferences." setup_restore_nvram_journal || true PTXT "${ERROR} Unable to save the AdGuardHome WebUI port." return 1 fi if ! setup_sync_webui_port "${WEB_PORT}"; then restore_installer_config "${CONFIG_ROLLBACK}" || PTXT "${ERROR} Unable to restore the previous installer preferences." setup_restore_nvram_journal || true PTXT "${ERROR} Unable to update the AdGuardHome WebUI address." return 1 fi discard_installer_config_backup "${CONFIG_ROLLBACK}" CONFIG_ROLLBACK="" elif awk -F'=' '/ADGUARD_WEBUI_PORT/ {print $2}' "${CONF_FILE}" 2>/dev/null | sed -e 's/^"//' -e 's/"$//' | grep -qoE '^(3000|[3-9][0-9]{3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$' && [ "$(awk -F'=' '/ADGUARD_WEBUI_PORT/ {print $2}' "${CONF_FILE}" | sed -e 's/^"//' -e 's/"$//' | grep -oE '^(3000|[3-9][0-9]{3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$')" != "$(awk -F':' '/.*address:.*/{ print $3 }' "${YAML_FILE}" | grep -oE '^(3000|[3-9][0-9]{3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$')" ]; then if ! setup_files_begin_if_needed; then setup_restore_previous_yaml || true PTXT "${ERROR} Unable to journal the current installer configuration before WebUI port synchronization." return 1 fi if ! setup_sync_webui_port "$(awk -F'=' '/ADGUARD_WEBUI_PORT/ {print $2}' "${CONF_FILE}" | sed -e 's/^"//' -e 's/"$//' | grep -oE '^(3000|[3-9][0-9]{3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$')"; then setup_restore_nvram_journal || true PTXT "${ERROR} Unable to update the AdGuardHome WebUI address." return 1 fi fi ########################################################################################################################################################################################################################################################################## if [ -n "$(awk -F': ' '/.*schema_version:.*/{ print $2 }' "${YAML_FILE}")" ] && [ "$(awk -F': ' '/.*schema_version:.*/{ print $2 }' "${YAML_FILE}")" != "${SCHEMA_VER}" ]; then if ! setup_files_begin_if_needed; then setup_restore_previous_yaml || true PTXT "${ERROR} Unable to journal the current installer configuration before schema version update." return 1 fi if ! yaml_nvars_file_action replace "schema_version: $(awk -F': ' '/.*schema_version:.*/{ print $2 }' "${YAML_FILE}")" "schema_version: ${SCHEMA_VER}" "${YAML_FILE}"; then setup_restore_nvram_journal || true PTXT "${ERROR} Unable to update the schema version in the configuration file." return 1 fi fi if ! check_AdGuardHome_yaml; then setup_AdGuardHome_impl x "${2:-}" "${SETUP_FILES_JOURNALED}" return fi PTXT "${INFO} Found previous AdGuardHome config file" read_yesno "Do you want to use this file without reconfiguring?" CONFIRM_STATUS=$? if [ "${CONFIRM_STATUS}" -eq 0 ]; then if [ "${SETUP_FILES_JOURNALED}" -eq 1 ] && [ -z "${EVENT_SCRIPTS_ACTIVE_SNAPSHOT:-}" ]; then if ! nvram_transaction_finalize_setup_pair; then setup_restore_nvram_journal || true PTXT "${ERROR} Unable to finalize the installer configuration updates." return 1 fi fi PTXT "${INFO} Use previous settings file" elif [ "${CONFIRM_STATUS}" -eq 1 ]; then setup_AdGuardHome_impl x "${2:-}" "${SETUP_FILES_JOURNALED}" else setup_restore_nvram_journal || true return "${CONFIRM_STATUS}" fi else if [ -f "${YAML_FILE}" ]; then if [ "$1" = "reconfig" ]; then if ! check_AdGuardHome_yaml; then setup_AdGuardHome_impl x "${2:-}" "${SETUP_FILES_JOURNALED}" return fi PTXT "${INFO} Found previous AdGuardHome config file" fi PTXT "${INFO} How do you want to reconfigure:" \ "${INFO} 1) Use previous settings file" \ "${INFO} 2) Restore original configuration selections" \ "${INFO} 3) Choose new configuration selections" if ! read_input_num "Your selection" 1 3; then setup_restore_nvram_journal || true PTXT "${ERROR} Unable to select a reconfiguration mode. Setup was aborted." return 1 fi case "${CHOSEN}" in 1) PTXT "${INFO} Using previous settings file" ;; 2 | 3) if ! setup_files_begin_if_needed; then [ -z "${YAML_ORI_NEW:-}" ] || rm -f "${YAML_ORI_NEW}" setup_restore_previous_yaml || true PTXT "${ERROR} Unable to journal the current installer configuration before YAML rotation." return 1 fi PTXT "${INFO} Backing up previous settings file..." if ! mv "${YAML_FILE}" "${YAML_BAK}"; then setup_restore_nvram_journal || true PTXT "${ERROR} Unable to back up the previous AdGuardHome configuration. Setup was aborted." return 1 fi YAML_BACKED_UP=1 if [ "${CHOSEN}" = "3" ]; then CHOOSE_NEW=1 else cp -f "${YAML_ORI}" "${YAML_FILE}" fi ;; esac elif [ ! -f "${YAML_FILE}" ] && [ -f "${YAML_ORI}" ]; then if ! setup_files_begin_if_needed; then setup_restore_previous_yaml || true PTXT "${ERROR} Unable to journal the current installer configuration before YAML copy." return 1 fi cp -f "${YAML_ORI}" "${YAML_FILE}" fi case "${2:-reconfig}" in "install" | "reconfig" | "RESTORE") if [ "${2:-reconfig}" != "RESTORE" ] && { [ ! -f "${YAML_FILE}" ] || [ "$2" = "reconfig" ]; }; then local DNS_FILTER_SELECTION LOCAL_CACHE_SELECTION YESNO_STATUS if [ "${ADGUARD_INSTALL_MODE:-wan}" = "wan" ]; then read_yesno "Do you want to redirect all DNS resolutions on your network through to AdGuardHome?" YESNO_STATUS="$?" case "${YESNO_STATUS}" in 0) DNS_FILTER_SELECTION=1 ;; 1) DNS_FILTER_SELECTION=0 ;; *) setup_restore_previous_yaml || true setup_restore_nvram_journal || true return "${YESNO_STATUS}" ;; esac if [ "$(installer_nvram get dns_local_cache)" != "1" ]; then read_yesno "Do you want to run AdGuardHome as a local caching DNS service which includes router traffic?" YESNO_STATUS="$?" case "${YESNO_STATUS}" in 0) LOCAL_CACHE_SELECTION=1 ;; 1) LOCAL_CACHE_SELECTION=0 ;; *) setup_restore_previous_yaml || true setup_restore_nvram_journal || true return "${YESNO_STATUS}" ;; esac fi fi if adguard_ipset_allowed; then read_yesno "Do you want to enable AdGuardHome IPSET integration?" YESNO_STATUS="$?" case "${YESNO_STATUS}" in 0) IPSET_SELECTION=1 ;; 1) IPSET_SELECTION=0 ;; *) setup_restore_previous_yaml || true setup_restore_nvram_journal || true return "${YESNO_STATUS}" ;; esac else IPSET_SELECTION=0 PTXT "${INFO} IPSET is disabled in LAN/AP/Bridge mode because IPTABLES/IPSET rules are not manageable there." fi if [ "$2" != "reconfig" ]; then if ! setup_files_begin_if_needed; then setup_files_journal_diagnostic setup_restore_previous_yaml || true PTXT "${ERROR} Unable to journal the current installer configuration before check_ipset." return 1 fi if ! check_ipset "${IPSET_SELECTION}"; then PTXT "${WARNING} Unable to save the optional AdGuardHome IPSET integration setting." \ "${WARNING} Continuing setup with the previous or default IPSET preference." fi fi fi if { { [ ! -f "${YAML_ORI}" ] && [ ! -f "${YAML_FILE}" ]; } || [ "${CHOOSE_NEW}" -eq 1 ] || [ -n "${DNS_FILTER_SELECTION:-}" ]; }; then if ! setup_files_begin_if_needed; then setup_restore_previous_yaml || true PTXT "${ERROR} Unable to journal the current installer configuration before setup publication." return 1 fi fi if { [ ! -f "${YAML_ORI}" ] && [ ! -f "${YAML_FILE}" ]; } || [ "${CHOOSE_NEW}" -eq 1 ]; then PTXT "${INFO} Requesting entries for AdGuardHome initial configuration..." \ "${INFO} Please set the preferred Web Port for AdGuardHome WebUI." \ "${INFO} The chosen port must reside within the port range 3000-65535" \ "${INFO} and not already in use by any other service." if ! read_input_port "Default is" 3000; then setup_restore_previous_yaml || true setup_restore_nvram_journal || true return 1 fi WEB_PORT_SELECTION="${WEB_PORT}" local DOMAIN="" NET_ADDR NET_ADDR6 setup_resolve_lan_addresses if [ "${ADGUARD_INSTALL_MODE:-wan}" = "lan" ] && [ -z "${NET_ADDR}" ]; then PTXT "${ERROR} Unable to resolve the LAN IPv4 address for AdGuardHome LAN mode. Setup was aborted." setup_restore_previous_yaml || true setup_restore_nvram_journal || true return 1 fi if ! setup_resolve_bind_addresses; then setup_restore_previous_yaml || true setup_restore_nvram_journal || true return 1 fi if ! setup_reverse_upstream_target; then PTXT "${ERROR} Unable to resolve the reverse upstream target. Setup was aborted." setup_restore_previous_yaml || true setup_restore_nvram_journal || true return 1 fi YAML_ORI_NEW="${YAML_ORI}.new.$$" SETUP_YAML_TMP_FILE="${YAML_ORI_NEW}" PTXT "http:" \ " address: ${SETUP_WEB_ADDRESS}" >"${YAML_ORI_NEW}" PTXT "${INFO} Set the Username and Password which will be encrypted to the yaml file." if ! AdGuardHome_authen 1 "${YAML_ORI_NEW}" 0; then rm -f "${YAML_ORI_NEW}" setup_restore_previous_yaml || true setup_restore_nvram_journal || true return 1 fi setup_write_dns_bind_hosts "${YAML_ORI_NEW}" PTXT " port: 53" >>"${YAML_ORI_NEW}" PTXT "${INFO} Set the DNS server(s) for initializing AdGuardHome" \ "${INFO} and router services (e.g. ntp) at boot" if ! read_input_dns "Default is" 9.9.9.9 || ! read_input_dns "2nd Default is" 8.8.8.8; then rm -f "${YAML_ORI_NEW}" setup_restore_previous_yaml || true setup_restore_nvram_journal || true return 1 fi if ! LAN_DOMAIN_ROLLBACK="$(installer_nvram get lan_domain 2>/dev/null)"; then rm -f "${YAML_ORI_NEW}" setup_restore_previous_yaml || true setup_restore_nvram_journal || true PTXT "${ERROR} Unable to read the LAN domain NVRAM setting; no changes were made." return 1 fi if [ -n "${LAN_DOMAIN_ROLLBACK}" ]; then DOMAIN="${LAN_DOMAIN_ROLLBACK}" DOMAIN_CONFIG_SELECTION="no" else DOMAIN="lan" DOMAIN_CONFIG_SELECTION="yes" fi PTXT " upstream_dns:" >>"${YAML_ORI_NEW}" [ -n "${NET_ADDR6}" ] && PTXT " - '[/$(printf "%s\n" "${NET_ADDR6}" | sed 's/.$//' | awk -F: '{for(i=1;i<=NF;i++)x=x""sprintf (":%4s", $i);gsub(/ /,"0",x); printf x}' | cut -c 2- | cut -c 1-20 | sed 's/://g;s/^.*$/\n&\n/;tx;:x;s/\(\n.\)\(.*\)\(.\n\)/\3\2\1/;tx;s/\n//g;s/\(.\)/\1./g;s/$/ip6.arpa/')/]${SETUP_REVERSE_UPSTREAM}'" >>"${YAML_ORI_NEW}" PTXT " - '[/router.asus.com/]${SETUP_REVERSE_UPSTREAM}'" \ " - '[/www.asusnetwork.net/]${SETUP_REVERSE_UPSTREAM}'" \ " - '[/www.asusrouter.com/]${SETUP_REVERSE_UPSTREAM}'" \ " - '[/use-application-dns.net/]${SETUP_REVERSE_UPSTREAM}'" \ " - '[/dns.resolver.arpa/]${SETUP_REVERSE_UPSTREAM}'" \ " - '[/${DOMAIN}/]${SETUP_REVERSE_UPSTREAM}'" \ " - '[//]${SETUP_REVERSE_UPSTREAM}'" \ " - ${BOOTSTRAP1}" \ " - ${BOOTSTRAP2}" \ " - tcp://${BOOTSTRAP1}" \ " - tcp://${BOOTSTRAP2}" \ " bootstrap_dns:" \ " - ${BOOTSTRAP1}" \ " - ${BOOTSTRAP2}" \ " fallback_dns:" \ " - ${BOOTSTRAP1}" \ " - ${BOOTSTRAP2}" \ " - tcp://${BOOTSTRAP1}" \ " - tcp://${BOOTSTRAP2}" \ " resolve_clients: true" \ " use_private_ptr_resolvers: true" \ " local_ptr_upstreams:" \ " - '${SETUP_REVERSE_UPSTREAM}'" \ " - '[/10.in-addr.arpa/]${SETUP_REVERSE_UPSTREAM}'" \ " - '[/$(printf "%s\n" "${NET_ADDR}" | awk 'BEGIN{FS="."}{print $2"."$1".in-addr.arpa"}')/]${SETUP_REVERSE_UPSTREAM}'" \ "schema_version: ${SCHEMA_VER}" >>"${YAML_ORI_NEW}" if ! check_AdGuardHome_yaml "${YAML_ORI_NEW}"; then rm -f "${YAML_ORI_NEW}" setup_restore_previous_yaml || true setup_restore_nvram_journal || true return 1 fi PTXT "${INFO} Writing AdGuardHome configuration..." if ! cp -f "${YAML_ORI_NEW}" "${YAML_FILE}"; then rm -f "${YAML_ORI_NEW}" setup_restore_previous_yaml || true setup_restore_nvram_journal || true PTXT "${ERROR} Writing AdGuardHome configuration failed " return 1 fi fi if ! check_AdGuardHome_yaml; then [ -z "${YAML_ORI_NEW:-}" ] || rm -f "${YAML_ORI_NEW}" setup_restore_previous_yaml || true setup_restore_nvram_journal || true PTXT "${ERROR} Writing AdGuardHome configuration failed " \ "${ERROR} Please send ${YAML_ERR} file and screen log of " \ "${ERROR} all ssh terminal operations to the script developer." return 1 fi if [ "${ADGUARD_INSTALL_MODE:-wan}" = "wan" ] && [ -n "${DNS_FILTER_SELECTION:-}" ]; then if ! setup_files_begin_if_needed; then [ -z "${YAML_ORI_NEW:-}" ] || rm -f "${YAML_ORI_NEW}" setup_restore_previous_yaml || true PTXT "${ERROR} Unable to journal the current installer configuration before DNSFilter reconfiguration." return 1 fi CONFIG_ROLLBACK="${TMPDIR:-/tmp}/AdGuardHome.config.$$" if ! save_installer_config "${CONFIG_ROLLBACK}"; then [ -z "${YAML_ORI_NEW:-}" ] || rm -f "${YAML_ORI_NEW}" setup_restore_previous_yaml || true setup_restore_nvram_journal || true PTXT "${ERROR} Unable to preserve the current installer preferences." return 1 fi if [ -n "${DOMAIN_CONFIG_SELECTION}" ] && ! write_conf ADGUARD_DOMAIN "\"${DOMAIN_CONFIG_SELECTION}\""; then restore_installer_config "${CONFIG_ROLLBACK}" || PTXT "${ERROR} Unable to restore the previous installer preferences." [ -z "${YAML_ORI_NEW:-}" ] || rm -f "${YAML_ORI_NEW}" setup_restore_previous_yaml || true setup_restore_nvram_journal || true PTXT "${ERROR} Unable to save the AdGuardHome domain setting." return 1 fi if [ -n "${DOMAIN_CONFIG_SELECTION}" ]; then if ! installer_lan_domain_set "${DOMAIN}" 1; then restore_installer_config "${CONFIG_ROLLBACK}" || PTXT "${ERROR} Unable to restore the previous installer preferences." [ -z "${YAML_ORI_NEW:-}" ] || rm -f "${YAML_ORI_NEW}" setup_restore_previous_yaml || true setup_restore_nvram_journal || true PTXT "${ERROR} Unable to persist the LAN domain setting." return 1 fi DOMAIN_LAN_CHANGED=1 fi DNS_FILTER_ROLLBACK_DIR="${TMPDIR:-/tmp}/AdGuardHome.dnsfilter.$$" rm -rf "${DNS_FILTER_ROLLBACK_DIR}" if ! save_dns_filter_settings "${DNS_FILTER_ROLLBACK_DIR}"; then rm -rf "${DNS_FILTER_ROLLBACK_DIR}" restore_installer_config "${CONFIG_ROLLBACK}" || PTXT "${ERROR} Unable to restore the previous installer preferences." if [ "${DOMAIN_LAN_CHANGED}" -eq 1 ]; then installer_lan_domain_restore || PTXT "${ERROR} Unable to restore the LAN domain NVRAM setting." fi [ -z "${YAML_ORI_NEW:-}" ] || rm -f "${YAML_ORI_NEW}" setup_restore_previous_yaml || true setup_restore_nvram_journal || true PTXT "${ERROR} Unable to preserve the current DNSFilter settings." return 1 fi check_dns_filter "${DNS_FILTER_SELECTION}" YESNO_STATUS="$?" if [ "${YESNO_STATUS}" -ne 0 ]; then if restore_dns_filter_settings "${DNS_FILTER_ROLLBACK_DIR}"; then rm -rf "${DNS_FILTER_ROLLBACK_DIR}" else PTXT "${ERROR} Unable to restore the previous DNSFilter settings; the recovery snapshot was preserved." fi restore_installer_config "${CONFIG_ROLLBACK}" || PTXT "${ERROR} Unable to restore the previous installer preferences." if [ "${DOMAIN_LAN_CHANGED}" -eq 1 ]; then installer_lan_domain_restore || PTXT "${ERROR} Unable to restore the LAN domain NVRAM setting." fi [ -z "${YAML_ORI_NEW:-}" ] || rm -f "${YAML_ORI_NEW}" setup_restore_previous_yaml || true setup_restore_nvram_journal || true return "${YESNO_STATUS}" fi if [ "$(installer_nvram get dns_local_cache)" != "1" ]; then if ! check_dns_local "${LOCAL_CACHE_SELECTION}"; then restore_installer_config "${CONFIG_ROLLBACK}" || PTXT "${ERROR} Unable to restore the previous installer preferences." if [ "${DOMAIN_LAN_CHANGED}" -eq 1 ]; then installer_lan_domain_restore || PTXT "${ERROR} Unable to restore the LAN domain NVRAM setting." fi if ! restore_dns_filter_settings "${DNS_FILTER_ROLLBACK_DIR}"; then PTXT "${ERROR} Unable to restore the previous DNSFilter settings." fi [ -z "${YAML_ORI_NEW:-}" ] || rm -f "${YAML_ORI_NEW}" setup_restore_previous_yaml || true setup_restore_nvram_journal || true PTXT "${ERROR} Unable to save the AdGuardHome local cache setting." return 1 fi else if ! check_dns_local 0; then restore_installer_config "${CONFIG_ROLLBACK}" || PTXT "${ERROR} Unable to restore the previous installer preferences." if [ "${DOMAIN_LAN_CHANGED}" -eq 1 ]; then installer_lan_domain_restore || PTXT "${ERROR} Unable to restore the LAN domain NVRAM setting." fi if ! restore_dns_filter_settings "${DNS_FILTER_ROLLBACK_DIR}"; then PTXT "${ERROR} Unable to restore the previous DNSFilter settings." fi [ -z "${YAML_ORI_NEW:-}" ] || rm -f "${YAML_ORI_NEW}" setup_restore_previous_yaml || true setup_restore_nvram_journal || true PTXT "${ERROR} Unable to save the AdGuardHome local cache setting." return 1 fi fi fi if [ "$2" = "reconfig" ] && [ -n "${IPSET_SELECTION:-}" ]; then if ! setup_files_begin_if_needed; then [ -z "${YAML_ORI_NEW:-}" ] || rm -f "${YAML_ORI_NEW}" setup_restore_previous_yaml || true PTXT "${ERROR} Unable to journal the current installer configuration before IPSET reconfiguration." return 1 fi if ! check_ipset "${IPSET_SELECTION}"; then PTXT "${WARNING} Unable to save the optional AdGuardHome IPSET integration setting." \ "${WARNING} Continuing reconfiguration with the previous IPSET preference." fi fi if [ "${ADGUARD_INSTALL_MODE:-wan}" = "lan" ] || [ -n "${DNS_FILTER_SELECTION:-}" ]; then if ! setup_files_begin_if_needed; then [ -z "${CONFIG_ROLLBACK}" ] || restore_installer_config "${CONFIG_ROLLBACK}" || PTXT "${ERROR} Unable to restore the previous installer preferences." if [ "${DOMAIN_LAN_CHANGED}" -eq 1 ]; then installer_lan_domain_restore || PTXT "${ERROR} Unable to restore the LAN domain NVRAM setting." fi if [ -n "${DNS_FILTER_ROLLBACK_DIR}" ] && ! restore_dns_filter_settings "${DNS_FILTER_ROLLBACK_DIR}"; then PTXT "${ERROR} Unable to restore the previous DNSFilter settings." fi [ -z "${YAML_ORI_NEW:-}" ] || rm -f "${YAML_ORI_NEW}" setup_restore_previous_yaml || true PTXT "${ERROR} Unable to journal the current installer configuration before configure_runtime_defaults." return 1 fi if ! configure_runtime_defaults new-install "${ADGUARD_INSTALL_MODE:-wan}" "${LOCAL_CACHE_SELECTION:-0}"; then [ -z "${CONFIG_ROLLBACK}" ] || restore_installer_config "${CONFIG_ROLLBACK}" || PTXT "${ERROR} Unable to restore the previous installer preferences." if [ "${DOMAIN_LAN_CHANGED}" -eq 1 ]; then installer_lan_domain_restore || PTXT "${ERROR} Unable to restore the LAN domain NVRAM setting." fi if [ -n "${DNS_FILTER_ROLLBACK_DIR}" ] && ! restore_dns_filter_settings "${DNS_FILTER_ROLLBACK_DIR}"; then PTXT "${ERROR} Unable to restore the previous DNSFilter settings." fi [ -z "${YAML_ORI_NEW:-}" ] || rm -f "${YAML_ORI_NEW}" setup_restore_previous_yaml || true setup_restore_nvram_journal || true PTXT "${ERROR} Unable to save the AdGuardHome runtime defaults." return 1 fi fi if [ -n "${WEB_PORT_SELECTION:-}" ] && ! write_conf ADGUARD_WEBUI_PORT "\"${WEB_PORT_SELECTION}\""; then [ -z "${CONFIG_ROLLBACK}" ] || restore_installer_config "${CONFIG_ROLLBACK}" || PTXT "${ERROR} Unable to restore the previous installer preferences." if [ "${DOMAIN_LAN_CHANGED}" -eq 1 ]; then installer_lan_domain_restore || PTXT "${ERROR} Unable to restore the LAN domain NVRAM setting." fi if [ -n "${DNS_FILTER_ROLLBACK_DIR}" ] && ! restore_dns_filter_settings "${DNS_FILTER_ROLLBACK_DIR}"; then PTXT "${ERROR} Unable to restore the previous DNSFilter settings." fi [ -z "${YAML_ORI_NEW:-}" ] || rm -f "${YAML_ORI_NEW}" setup_restore_previous_yaml || true setup_restore_nvram_journal || true PTXT "${ERROR} Unable to save the AdGuardHome WebUI port." return 1 fi if [ -n "${YAML_ORI_NEW:-}" ]; then YAML_ORI_ROLLBACK="${YAML_ORI}.rollback.$$" if [ -e "${YAML_ORI}" ]; then if ! cp -f "${YAML_ORI}" "${YAML_ORI_ROLLBACK}"; then [ -z "${CONFIG_ROLLBACK}" ] || restore_installer_config "${CONFIG_ROLLBACK}" || PTXT "${ERROR} Unable to restore the previous installer preferences." if [ "${DOMAIN_LAN_CHANGED}" -eq 1 ]; then installer_lan_domain_restore || PTXT "${ERROR} Unable to restore the LAN domain NVRAM setting." fi if [ -n "${DNS_FILTER_ROLLBACK_DIR}" ] && ! restore_dns_filter_settings "${DNS_FILTER_ROLLBACK_DIR}"; then PTXT "${ERROR} Unable to restore the previous DNSFilter settings." fi rm -f "${YAML_ORI_NEW}" setup_restore_previous_yaml || true setup_restore_nvram_journal || true PTXT "${ERROR} Unable to preserve the previous original AdGuardHome configuration snapshot." return 1 fi YAML_ORI_WAS_PRESENT=1 fi fi if [ -n "${YAML_ORI_NEW:-}" ] && ! mv -f "${YAML_ORI_NEW}" "${YAML_ORI}"; then rm -f "${YAML_ORI_ROLLBACK}" [ -z "${CONFIG_ROLLBACK}" ] || restore_installer_config "${CONFIG_ROLLBACK}" || PTXT "${ERROR} Unable to restore the previous installer preferences." if [ "${DOMAIN_LAN_CHANGED}" -eq 1 ]; then installer_lan_domain_restore || PTXT "${ERROR} Unable to restore the LAN domain NVRAM setting." fi if [ -n "${DNS_FILTER_ROLLBACK_DIR}" ] && ! restore_dns_filter_settings "${DNS_FILTER_ROLLBACK_DIR}"; then PTXT "${ERROR} Unable to restore the previous DNSFilter settings." fi setup_restore_previous_yaml || true setup_restore_nvram_journal || true PTXT "${ERROR} Unable to publish the original AdGuardHome configuration snapshot." return 1 fi SETUP_YAML_TMP_FILE="" if [ -z "${EVENT_SCRIPTS_ACTIVE_SNAPSHOT:-}" ] && ! nvram_transaction_finalize_setup_pair; then [ -z "${CONFIG_ROLLBACK}" ] || restore_installer_config "${CONFIG_ROLLBACK}" || PTXT "${ERROR} Unable to restore the previous installer preferences." if [ "${DOMAIN_LAN_CHANGED}" -eq 1 ]; then installer_lan_domain_restore || PTXT "${ERROR} Unable to restore the LAN domain NVRAM setting." fi if [ -n "${DNS_FILTER_ROLLBACK_DIR}" ] && ! restore_dns_filter_settings "${DNS_FILTER_ROLLBACK_DIR}"; then PTXT "${ERROR} Unable to restore the previous DNSFilter settings." fi if [ -n "${YAML_ORI_ROLLBACK}" ]; then if [ "${YAML_ORI_WAS_PRESENT}" -eq 1 ]; then mv -f "${YAML_ORI_ROLLBACK}" "${YAML_ORI}" || PTXT "${ERROR} Unable to restore the previous original AdGuardHome configuration snapshot." else rm -f "${YAML_ORI}" || PTXT "${ERROR} Unable to remove the aborted original AdGuardHome configuration snapshot." fi fi setup_restore_previous_yaml || true setup_restore_nvram_journal || true PTXT "${ERROR} Unable to finalize the installer-managed NVRAM settings." return 1 fi rm -f "${YAML_ORI_ROLLBACK}" [ -z "${DNS_FILTER_ROLLBACK_DIR}" ] || rm -rf "${DNS_FILTER_ROLLBACK_DIR}" [ -z "${CONFIG_ROLLBACK}" ] || discard_installer_config_backup "${CONFIG_ROLLBACK}" ;; esac fi } setup_amtmupdate() { local AMTM_UPDATE CURRENT_AMTM_UPDATE CURRENT_AMTM_UPDATE="$(conf_value AMTM_UPDATE)" if [ -z "${CURRENT_AMTM_UPDATE}" ] || [ "${CURRENT_AMTM_UPDATE}" = "DISABLED" ]; then AMTM_UPDATE="ENABLED" else AMTM_UPDATE="DISABLED" fi PTXT "${INFO} The installer will now set AMTM Update to ${AMTM_UPDATE}." write_conf AMTM_UPDATE "\"${AMTM_UPDATE}\"" end_op_message 0 } amtm_update_check() { local AMTM_UPDATE [ "${1:-}" = "amtmupdate" ] && [ "${2:-}" = "check" ] || return 1 AMTM_UPDATE="$(conf_value AMTM_UPDATE)" if [ ! -f "${CONF_FILE}" ] || [ "${AMTM_UPDATE}" = "DISABLED" ]; then exit 1 fi if [ "${AMTM_UPDATE}" = "ENABLED" ]; then exit 0 fi exit 1 } uninst_all() { local ADGUARD_WAS_RUNNING LAN_DOMAIN_CHANGED ADGUARD_WAS_RUNNING="0" LAN_DOMAIN_CHANGED="0" if [ -f "${BASE_DIR}/backup_AdGuardHome.tar.gz" ]; then PTXT "${INFO} Old Backup Detected!" read_yesno "Do you want to remove backup?(this will prevent restoring from backups later)" CONFIRM_STATUS=$? if [ "${CONFIRM_STATUS}" -eq 0 ]; then rm -rf "${BASE_DIR}/backup_AdGuardHome.tar.gz" elif [ "${CONFIRM_STATUS}" -eq 1 ]; then PTXT "${INFO} Keeping backup instead." else return "${CONFIRM_STATUS}" fi fi if [ "$(conf_value ADGUARD_DOMAIN)" = "yes" ]; then if ! installer_lan_domain_set "" 1; then PTXT "${ERROR} Unable to clear the installer-managed LAN domain setting; uninstall was aborted before stopping AdGuardHome." return 1 fi LAN_DOMAIN_CHANGED="1" fi if agh_is_running; then ADGUARD_WAS_RUNNING="1" fi if ! agh_stop; then PTXT "${ERROR} Unable to stop AdGuardHome; uninstall was aborted before removing installed files." [ "${LAN_DOMAIN_CHANGED}" = "0" ] || installer_lan_domain_restore_uninstall || PTXT "${ERROR} Unable to restore the LAN domain after the uninstall abort." return 1 fi if { [ -e "${TARG_DIR}/proc-sys-state" ] || [ -L "${TARG_DIR}/proc-sys-state" ]; } && { [ ! -d "${TARG_DIR}/proc-sys-state" ] || [ -L "${TARG_DIR}/proc-sys-state" ] || [ ! -x "${ADDON_DIR}/AdGuardHome.sh" ]; }; then PTXT "${ERROR} Unable to restore installer-managed kernel settings." [ "${LAN_DOMAIN_CHANGED}" = "0" ] || installer_lan_domain_restore_uninstall || PTXT "${ERROR} Unable to restore the LAN domain after the uninstall abort." [ "${ADGUARD_WAS_RUNNING}" = "0" ] || agh_start || true return 1 fi if [ -d "${TARG_DIR}/proc-sys-state" ] && ! "${ADDON_DIR}/AdGuardHome.sh" proc-restore x; then PTXT "${ERROR} Unable to restore installer-managed kernel settings." [ "${LAN_DOMAIN_CHANGED}" = "0" ] || installer_lan_domain_restore_uninstall || PTXT "${ERROR} Unable to restore the LAN domain after the uninstall abort." [ "${ADGUARD_WAS_RUNNING}" = "0" ] || agh_start || true return 1 fi if ! all_event_scripts_transaction_begin "${BASE_DIR}/.AdGuardHome.uninstall-hooks.$$"; then PTXT "${ERROR} Existing Asuswrt-Merlin event scripts could not be protected for uninstall rollback." [ "${LAN_DOMAIN_CHANGED}" = "0" ] || installer_lan_domain_restore_uninstall || PTXT "${ERROR} Unable to restore the LAN domain after the uninstall abort." [ "${ADGUARD_WAS_RUNNING}" = "0" ] || agh_start || true return 1 fi if ! remove_dnsmasq_event_scripts || ! remove_init_event_scripts || ! remove_services_event_scripts || ! remove_firewall_event_scripts; then PTXT "${ERROR} Unable to remove installer-managed event scripts; uninstall was aborted." adguard_recover_after_uninstall_hook_abort "${ADGUARD_WAS_RUNNING}" "${LAN_DOMAIN_CHANGED}" return 1 fi if ! cleanup_legacy_firewall || ! yaml_nvars_file_action delete "#Asuswrt-Merlin AdGuardHome Installer" /jffs/scripts/dnsmasq.postconf; then PTXT "${ERROR} Unable to complete installer-managed event-hook cleanup; uninstall was aborted." adguard_recover_after_uninstall_hook_abort "${ADGUARD_WAS_RUNNING}" "${LAN_DOMAIN_CHANGED}" return 1 fi if ! nvram_transaction_finalize_setup_pair; then PTXT "${ERROR} Unable to finalize the installer-managed LAN domain cleanup; uninstall was aborted." adguard_recover_after_uninstall_hook_abort "${ADGUARD_WAS_RUNNING}" "${LAN_DOMAIN_CHANGED}" return 1 fi if ! all_event_scripts_transaction_commit; then PTXT "${ERROR} Unable to finalize the uninstall event-hook transaction." adguard_recover_after_uninstall_hook_abort "${ADGUARD_WAS_RUNNING}" "${LAN_DOMAIN_CHANGED}" return 1 fi mv "${TARG_DIR}/installer" "${HOME}/installer" rm -rf "${TARG_DIR}" "${ROLLBACK_RESULT_FILE}" "/opt/etc/init.d/S99AdGuardHome" "/opt/etc/init.d/rc.func.AdGuardHome" "/opt/sbin/AdGuardHome" "/opt/bin/bcrypt-tool" "${ADDON_DIR}" "/opt/var/log/AdGuardHome.log" service restart_dnsmasq >/dev/null 2>&1 end_op_message 0 uninstall } # service_event_hook_command is the canonical command-format firmware hook, distinct from manager dispatch calls. service_event_hook_command() { printf '%s\n' 'if printf "%s" "$@" | /bin/grep -qE "^(((start|stop|restart|kill|reload)_?.*AdGuardHome)$)"; then { sh /jffs/addons/AdGuardHome.d/AdGuardHome.sh "$(printf "%s" "$@" | /bin/grep -oE "(start|stop|restart|kill|reload)")" x & }; fi # Asuswrt-Merlin-AdGuardHome-Installer' } # managed_hook_expected_line returns the complete owned line used by generation and diagnosis. managed_hook_expected_line() { case "$1" in '@service-event-end') service_event_hook_command ;; *) printf '%s\n' "[ -x ${ADDON_DIR}/AdGuardHome.sh ] && ${ADDON_DIR}/AdGuardHome.sh $1" ;; esac } # managed_hook_header_valid accepts an executable absolute interpreter and preserves its optional arguments. managed_hook_header_valid() { local interpreter interpreter="$(sed -n '1s/^#![[:space:]]*\([^[:space:]]*\).*/\1/p' "$1")" || return 1 case "${interpreter}" in /*) [ -f "${interpreter}" ] && [ -x "${interpreter}" ] ;; *) return 1 ;; esac } # managed_hook_recognized limits doctor repair to existing owned content. managed_hook_recognized() { local operation operation="$2" awk -v manager="${ADDON_DIR}/AdGuardHome.sh" -v operation="${operation}" ' { line = $0; sub(/^[[:space:]]*/, "", line) } operation == "@service-event-end" { if (index(line, "# Asuswrt-Merlin-AdGuardHome-Installer")) found = 1 next } index(line, "[ -x " manager " ] && " manager " ") == 1 || index(line, manager " ") == 1 || line == manager { found = 1 } END { exit found ? 0 : 1 } ' "$1" } # managed_hook_state_valid requires the final header, mode and exactly one complete owned invocation. managed_hook_state_valid() { [ -f "$1" ] && [ ! -L "$1" ] && [ -x "$1" ] || return 1 managed_hook_header_valid "$1" || return 1 [ "$(ls -ld "$1" 2>/dev/null | awk '{ print $1 }')" = '-rwxr-xr-x' ] || return 1 awk -v expected="$2" -v manager="${ADDON_DIR}/AdGuardHome.sh" ' $0 == expected { exact++ } { line = $0; sub(/^[[:space:]]*/, "", line) if (index(expected, "# Asuswrt-Merlin-AdGuardHome-Installer")) { if (index(line, "# Asuswrt-Merlin-AdGuardHome-Installer")) owned++ } else if (index(expected, "[ -x " manager " ] && " manager " ") == 1) { if (index(line, "[ -x " manager " ] && " manager " ") == 1 || index(line, manager " ") == 1 || line == manager) owned++ } else if ($0 == expected) owned++ } END { exit exact == 1 && owned == 1 ? 0 : 1 } ' "$1" } # write_managed_hook stages cleanup, header normalization and final permissions before publishing any content. write_managed_hook() { local TARG LINE KIND STAGE CONTENT HEADER TARG="$1" LINE="$2" KIND="$3" STAGE="${TARG}.agh.$$" CONTENT="${STAGE}.content" [ ! -L "${TARG}" ] && { [ ! -e "${TARG}" ] || [ -f "${TARG}" ]; } || return 1 [ ! -e "${STAGE}" ] && [ ! -L "${STAGE}" ] && [ ! -e "${CONTENT}" ] && [ ! -L "${CONTENT}" ] || return 1 (umask 077 && set -C && : >"${STAGE}") || return 1 MANAGED_HOOK_TMP_FILE="${STAGE}" if [ -f "${TARG}" ] && ! cp -p "${TARG}" "${STAGE}"; then rm -f "${STAGE}" return 1 fi if ! del_between_magic "${STAGE}" Asuswrt-Merlin-AdGuardHome-Installer || { [ "${KIND}" = manager ] && ! del_jffs_script "${STAGE}" !manager; }; then rm -f "${STAGE}" return 1 fi if [ ! -f "${STAGE}" ]; then (umask 077 && set -C && : >"${STAGE}") || return 1 fi HEADER="$(sed -n '1p' "${STAGE}")" || { rm -f "${STAGE}" return 1 } if ! managed_hook_header_valid "${STAGE}"; then case "${HEADER}" in '#!'*) rm -f "${STAGE}" return 1 ;; *) HEADER='#!/bin/sh' ;; esac fi if ! (umask 077 && set -C && awk -v header="${HEADER}" -v expected="${LINE}" -v manager="${ADDON_DIR}/AdGuardHome.sh" -v kind="${KIND}" ' BEGIN { print header } NR == 1 && $0 == header { next } { line = $0; sub(/^[[:space:]]*/, "", line) if (kind == "manager" && (index(line, "[ -x " manager " ] && " manager " ") == 1 || index(line, manager " ") == 1 || line == manager)) next if ($0 != expected) print } ' "${STAGE}" >"${CONTENT}") || ! mv -f "${CONTENT}" "${STAGE}" || ! PTXT "" >>"${STAGE}" || ! PTXT "${LINE}" >>"${STAGE}" || ! chmod 755 "${STAGE}" || ! managed_hook_state_valid "${STAGE}" "${LINE}" || ! mv -f "${STAGE}" "${TARG}"; then rm -f "${STAGE}" "${CONTENT}" PTXT "${ERROR} Unable to publish executable hook ${TARG}" return 1 fi MANAGED_HOOK_TMP_FILE="" return 0 } # write_command_script publishes a command-format hook through the common staged invariant check. write_command_script() { write_managed_hook "$1" "$2" command } write_conf() { local VAR VALUE CONF_DIR VAR="$1" VALUE="$2" CONF_DIR="${CONF_FILE%/*}" if [ "${CONF_DIR}" != "${CONF_FILE}" ] && [ ! -d "${CONF_DIR}" ]; then create_dir "${CONF_DIR}" || return 1 fi if [ ! -f "${CONF_FILE}" ]; then if ! { touch "${CONF_FILE}" && chmod 644 "${CONF_FILE}"; }; then PTXT "${ERROR} Unable to create ${CONF_FILE}" return 1 fi fi if grep -q "^${VAR}=" "${CONF_FILE}"; then VALUE=$(_quote "${VALUE}") if ! sed -i "/^${VAR}=/s/=.*/=${VALUE}/" "${CONF_FILE}"; then PTXT "${ERROR} Unable to update ${VAR} in ${CONF_FILE}" return 1 fi else if ! PTXT "${VAR}=${VALUE}" >>"${CONF_FILE}"; then PTXT "${ERROR} Unable to append ${VAR} to ${CONF_FILE}" return 1 fi fi return 0 } # write_manager_script verifies the manager entry point and publishes its single intended firmware hook call. write_manager_script() { local COMMAND LINE COMMAND="${ADDON_DIR}/AdGuardHome.sh" [ -f "${COMMAND}" ] && [ ! -L "${COMMAND}" ] && chmod 755 "${COMMAND}" || return 1 LINE="$(managed_hook_expected_line "$2")" || return 1 write_managed_hook "$1" "${LINE}" manager } yaml_nvars_append() { echo "$1" >>"$2" } yaml_nvars_delete() { PATTERN="$(_quote "$1")" sed -i "/${PATTERN}/d" "$2" } yaml_nvars_file_action() { local ACTION FILE [ "$#" -gt 0 ] || return 1 ACTION="$1" shift case "${ACTION}" in append | delete) FILE="${2:-}" ;; insert | replace) FILE="${3:-}" ;; *) return 1 ;; esac [ -n "${FILE}" ] || return 1 [ -f "${FILE}" ] || return 0 case "${ACTION}" in append) yaml_nvars_append "$@" ;; delete) yaml_nvars_delete "$@" ;; insert) yaml_nvars_insert "$@" ;; replace) yaml_nvars_replace "$@" ;; esac } yaml_nvars_insert() { PATTERN="$(_quote "$1")" CONTENT="$(_quote "$2")" sed -i "/${PATTERN}/a${CONTENT}" "$3" } yaml_nvars_replace() { PATTERN="$(_quote "$1")" CONTENT="$(_quote "$2")" sed -i "s/${PATTERN}/${CONTENT}/" "$3" } # Interactive menu helpers end_op_header() { sed -n -e "1,$(($(grep -wn 'esac' "$0" | cut -d':' -f1 | tail -n1) + 1))p" "$0" >"${0}.tmp" chmod 755 "${0}.tmp" exec "${0}.tmp" "${BRANCH}" && rm -rf "${0}.tmp" } # Clean owned staging, preserve operation status, and return or relaunch the menu. end_op_message() { local end_action [ "${ADGUARD_SERVICE_STAGE_OWNED:-0}" != "1" ] || adguard_service_bundle_cleanup [ "${ADGUARD_SERVICE_PUBLISH_OWNED:-0}" != "1" ] || adguard_service_publish_cleanup case "${1:-0}" in 1) if [ "${ROLLBACK_RESULT_UPDATED:-0}" != "1" ] && ! rollback_result_needs_attention; then rollback_result_write "operation" "no rollback attempted" "operation aborted before rollback was needed" fi ;; 2) if [ "${ROLLBACK_RESULT_UPDATED:-0}" != "1" ] && ! rollback_result_needs_attention; then rollback_result_write "operation" "interrupted: no rollback attempted" "operation interrupted before rollback was needed" fi ;; *) : ;; esac nvram_transaction_lock_reaper_release_active || { PTXT "${ERROR} Unable to release the installer NVRAM transaction reaper before completing the operation." if [ "${END_OP_SIGNAL:-0}" = "1" ] || { [ "${CLI_MODE:-0}" != "1" ] && [ "${ADGUARD_DEFER_END_OP:-0}" != "1" ]; }; then exit 1 fi return 1 } # Signal traps must not return into an interrupted lock/recovery operation # after releasing its reaper. CLI and deferred non-signal callers still use # the normal status-specific return paths below. if [ "${END_OP_SIGNAL:-0}" = "1" ] && [ "${1:-0}" = "2" ]; then exit 2 fi if [ "${CLI_MODE:-0}" = "1" ]; then case "${1:-0}" in 0) return 0 ;; 1) return 1 ;; 2) return 2 ;; *) : ;; esac fi if [ "${ADGUARD_DEFER_END_OP:-0}" = "1" ]; then case "${1:-0}" in 0) return 0 ;; 1) return 1 ;; 2) return 2 ;; *) : ;; esac fi case "${1:-0}" in 0) end_action="$2" [ "${end_action}" = "${INSTALLER_ACTION_UPDATE:-update}" ] && exit 0 if [ "${2:-}" = uninstall ] && [ "${AI_NO_AUTO_UPDATE:-0}" = 1 ]; then exit 0 fi PTXT "${INFO} Operation completed, returning to Main Menu. You can quit or continue." ;; 1) end_action="$2" if [ "${end_action}" = "${INSTALLER_ACTION_UPDATE:-update}" ]; then [ -f "${ROLLBACK_RESULT_FILE}" ] && PTXT "${INFO} Last rollback result: $(rollback_result_summary)" exit 1 fi PTXT "${INFO} Operation aborted, returning to Main Menu. You can quit or continue." [ -f "${ROLLBACK_RESULT_FILE}" ] && PTXT "${INFO} Last rollback result: $(rollback_result_summary)" ;; 2) PTXT "${INFO} Abnormal operations, returning to Main Menu. You can quit or continue." [ -f "${ROLLBACK_RESULT_FILE}" ] && PTXT "${INFO} Last rollback result: $(rollback_result_summary)" ;; *) : ;; esac PTXT "=====================================================" PTXT " " PTXT " " sleep 3s && clear_screen nvram_transaction_lock_release || { PTXT "${ERROR} Unable to release the installer NVRAM transaction lock before restarting the installer." exit 1 } if [ "${AI_NO_AUTO_UPDATE:-0}" = "1" ]; then if [ ! -f "${SCRIPT_LOC}" ]; then PTXT "${ERROR} The current installer is unavailable; refusing a menu relaunch that could lose --no-auto-update." exit 1 fi chmod 755 "${SCRIPT_LOC}" >/dev/null 2>&1 || exit 1 exec "${SCRIPT_LOC}" --no-auto-update "${BRANCH}" fi if [ -f "${TARG_DIR}/installer" ]; then chmod 755 "${TARG_DIR}/installer" >/dev/null 2>&1 exec "${TARG_DIR}/installer" "${BRANCH}" elif [ ! -f "${TARG_DIR}/installer" ] && [ -f "${SCRIPT_LOC}" ]; then chmod 755 "${SCRIPT_LOC}" >/dev/null 2>&1 exec "${SCRIPT_LOC}" "${BRANCH}" elif [ -f "${HOME}/installer" ]; then chmod 755 "${HOME}/installer" exec "${HOME}/installer" "${BRANCH}" else clear_screen && { end_op_header 2>/dev/null; } fi exit $? } # menu displays interactive choices when $1 is empty or dispatches the supplied # action, including persisted Local Cache selection and immediate activation. # Failed preference saves abort the action; readiness deferral retains the choice. menu() { local IPSET_SELECTION LOCAL_CACHE_SELECTION trap - HUP INT QUIT ABRT TERM case "$1" in "") PTXT "${INFO} Choose what you want to do:" \ " 1) Install/Update AdGuardHome" \ " 2) Uninstall" if { [ -d "${TARG_DIR}" ] && [ -f "${AGH_FILE}" ]; }; then PTXT " 3) Change Username/Password" " 4) Reconfigure" " 5) Enable/Disable AMTM updates" " 6) Enable/Disable AdGuardHome Local Cache" " 7) Switch AdGuardHome Branch" " 8) Enable/Disable AdGuardHome IPSET Integration" if [ -n "${BLOCKLIST_ANALYZER_SHA256}" ]; then PTXT " 9) Analyze and remove unused blocklists" fi PTXT " m) Migrate Runtime Defaults" PTXT " b) Backup" fi if { [ -f "${BASE_DIR}/backup_AdGuardHome.tar.gz" ]; }; then { PTXT " r) Restore"; }; fi PTXT " q) Quit" if { [ ! -d "${TARG_DIR}" ] || [ ! -f "${AGH_FILE}" ]; } && [ ! -f "${BASE_DIR}/backup_AdGuardHome.tar.gz" ]; then { read_input_num "Please enter the number that designates your selection:" 1 2 q; }; fi if { [ ! -d "${TARG_DIR}" ] || [ ! -f "${AGH_FILE}" ]; } && [ -f "${BASE_DIR}/backup_AdGuardHome.tar.gz" ]; then { read_input_num "Please enter the number that designates your selection:" 1 2 r q; }; fi if { [ -d "${TARG_DIR}" ] && [ -f "${AGH_FILE}" ] && [ ! -f "${BASE_DIR}/backup_AdGuardHome.tar.gz" ]; }; then { if [ -n "${BLOCKLIST_ANALYZER_SHA256}" ]; then { read_input_num "Please enter the number that designates your selection:" 1 9 b m q; }; else { read_input_num "Please enter the number that designates your selection:" 1 8 b m q; }; fi; }; fi if { [ -d "${TARG_DIR}" ] && [ -f "${AGH_FILE}" ] && [ -f "${BASE_DIR}/backup_AdGuardHome.tar.gz" ]; }; then { if [ -n "${BLOCKLIST_ANALYZER_SHA256}" ]; then { read_input_num "Please enter the number that designates your selection:" 1 9 b r m q; }; else { read_input_num "Please enter the number that designates your selection:" 1 8 b r m q; }; fi; }; fi ;; *) CHOSEN="$1" ;; esac [ -n "${CHOSEN}" ] && trap 'END_OP_SIGNAL=1; clear_screen; end_op_message 2' HUP INT QUIT ABRT TERM if [ "${AI_NO_AUTO_UPDATE:-0}" = "1" ]; then case "${CHOSEN}" in 1 | install | update | 7 | switchbranch | r | R | restore) installer_bundle_update_allowed || { end_op_message 1 return 1 } ;; *) : ;; esac fi case "${CHOSEN}" in "1" | "${INSTALLER_ACTION_INSTALL:-install}" | "${INSTALLER_ACTION_UPDATE:-update}") ptxt_phase "Selecting install or update operation." if [ "${CHOSEN}" = "${INSTALLER_ACTION_UPDATE:-update}" ]; then ensure_jq_tool || { end_op_message 1 return 1 } ptxt_step "Starting requested AdGuardHome update." AUTO_UPDATE="${INSTALLER_ACTION_UPDATE:-update}" inst_AdGuardHome "${AUTO_UPDATE:-update}" else PTXT "${INFO} This operation will install AdGuardHome and related files (<6MB)" \ "${INFO} to ENTWARE, no other data will be changed." \ "${INFO} Also some start scripts will be installed/modified as required." if read_yesno "Do you want to install AdGuardHome?"; then ptxt_ok "Install operation confirmed." ensure_jq_tool || { end_op_message 1 return 1 } inst_AdGuardHome "${AUTO_UPDATE:-install}" else ptxt_warn "Install operation declined." end_op_message 1 fi fi ;; "2" | "uninstall") PTXT "${INFO} This operation will cleanup everything installed by this script." if read_yesno "Do you want to continue?"; then uninst_all; else end_op_message 1; fi ;; "3" | "changepw") PTXT "${INFO} This operation will allow you to change your username and password." if read_yesno "Do you want to continue?"; then AdGuardHome_authen 0; else end_op_message 1; fi ;; "4" | "reconfigure") PTXT "${INFO} This operation allows you to configure AdGuardHome" if read_yesno "Do you want to proceed?"; then ensure_jq_tool || { end_op_message 1 return 1 } setup_AdGuardHome reconfig else end_op_message 1 fi ;; "5" | "setamtmupdate") PTXT "${INFO} This operation allows you to enable or disable AMTM's ability to manage updates for this script and by extension, AdGuardHome." \ "${INFO} If the option is already disabled and you choose to proceed, then you will be enabling." \ "${INFO} The same is true if the opposite is true and you choose to proceed." if read_yesno "Do you want to proceed?"; then setup_amtmupdate; else end_op_message 1; fi ;; "6" | "setlocalcache") if [ "$(installer_nvram get dns_local_cache)" != "1" ]; then PTXT "${INFO} This operation allows you to switch to using AdGuardHome as a local caching dns service for the router's own requests." \ "${INFO} A simple Yes or No will allow you to decide whether AdGuardhome handles DNS request for both the router and clients, or only clients." read_yesno "Do you want to run AdGuardHome as a local caching DNS service?" CONFIRM_STATUS=$? if [ "${CONFIRM_STATUS}" -eq 0 ]; then LOCAL_CACHE_SELECTION=1 elif [ "${CONFIRM_STATUS}" -eq 1 ]; then LOCAL_CACHE_SELECTION=0 else end_op_message 1 return "${CONFIRM_STATUS}" fi if ! check_dns_local "${LOCAL_CACHE_SELECTION}"; then PTXT "${ERROR} Unable to save the AdGuardHome local cache setting." end_op_message 1 return 1 fi if [ -x "${ADDON_DIR}/AdGuardHome.sh" ]; then "${ADDON_DIR}/AdGuardHome.sh" local-cache x >/dev/null 2>&1 || PTXT "${INFO} Preference saved; Local Cache activation is deferred until DNS services are ready." fi PTXT "${INFO} Local Cache is applied after AdGuardHome and dnsmasq are ready; the monitor retries after service restarts." end_op_message 0 else PTXT "${INFO} The router's own nvram built-in dns local caching feature is enabled." \ "${INFO} Hence why you cannot configure the installers method at this time." end_op_message 1 fi ;; "7" | "switchbranch") PTXT "${INFO} This operation allows you to switch between AdGuardHome branches installed." \ "${INFO} Before you choose to proceed, it is recommended to perform a backup." if read_yesno "Do you want to proceed?"; then { if ! choose_branch x; then end_op_message 1 return 1 fi AUTO_UPDATE="${INSTALLER_ACTION_UPDATE:-update}" inst_AdGuardHome "${AUTO_UPDATE:-update}" }; else end_op_message 1; fi ;; "8" | "setipset") if ! adguard_ipset_allowed; then check_ipset 0 || PTXT "${ERROR} Unable to persist ADGUARD_IPSET=NO." PTXT "${ERROR} IPSET integration is disabled in LAN/AP/Bridge mode because IPTABLES/IPSET rules are not manageable there." end_op_message 1 return 1 fi PTXT "${INFO} IPSET integration imports compatible domain-to-IPSET mappings for routing and firewall add-ons." \ "${INFO} Disable it if this router does not use IPSET-based domain routing or filtering." read_yesno "Do you want to enable AdGuardHome IPSET integration?" CONFIRM_STATUS=$? if [ "${CONFIRM_STATUS}" -eq 0 ]; then IPSET_SELECTION=1 elif [ "${CONFIRM_STATUS}" -eq 1 ]; then IPSET_SELECTION=0 else end_op_message 1 return "${CONFIRM_STATUS}" fi if ! check_ipset "${IPSET_SELECTION}"; then PTXT "${ERROR} Unable to save the AdGuardHome IPSET integration setting. AdGuardHome was not restarted." end_op_message 1 return 1 fi OLD_PIDS="$(pidof AdGuardHome 2>/dev/null)" if ! adguard_service_without_nvram_lock_fd service restart_AdGuardHome >/dev/null 2>&1; then PTXT "${WARNING} The setting was saved, but AdGuardHome could not be restarted automatically." else adguard_service_status_after_action restart "${OLD_PIDS}" || true fi end_op_message 0 ;; "m" | "M" | "migrate-runtime-defaults") PTXT "${INFO} This operation inspects legacy runtime defaults and can migrate them to safer v2.6.0 values." read_yesno "Do you want to migrate runtime defaults now?" CONFIRM_STATUS=$? if [ "${CONFIRM_STATUS}" -eq 0 ]; then cli_migrate_runtime_defaults --yes end_op_message "$?" elif [ "${CONFIRM_STATUS}" -eq 1 ]; then end_op_message 1 else end_op_message 1 return "${CONFIRM_STATUS}" fi ;; "9" | "blocklists" | "unusedblocklists") if [ -z "${BLOCKLIST_ANALYZER_SHA256}" ]; then ptxt_fail "Blocklist cleanup is unavailable until the analyzer SHA-256 is pinned." end_op_message 1 else cleanup_unused_blocklists fi ;; "b" | "B" | "backup") ptxt_phase "Selecting backup operation." PTXT "${INFO} This operation will backup everything!" if read_yesno "Do you want to continue?"; then ptxt_ok "Backup operation confirmed." backup_restore BACKUP else ptxt_warn "Backup operation declined." end_op_message 1 fi ;; "r" | "R" | "restore") ptxt_phase "Selecting restore operation." PTXT "${INFO} This operation will restore everything!" if read_yesno "Do you want to continue?"; then ptxt_ok "Restore operation confirmed." ensure_jq_tool || { end_op_message 1 return 1 } backup_restore RESTORE else ptxt_warn "Restore operation declined." end_op_message 1 fi ;; "d" | "D" | "doctor") doctor ;; "status") status "${BRANCH}" ;; "q" | "Q") PTXT "${INFO} Operations have been applied if any has been made" \ "${INFO} In case of anomaly, please reboot your router!" if [ -f "${HOME}/installer" ]; then rm -rf "${HOME}/installer"; fi sleep 3s clear_screen ;; *) : ;; esac } read_input_dns() { local DNS_SERVER while :; do PTXT -n "${INPUT} ${1} ${BOLD}${2}: ${NORM}" if ! read -r DNS_SERVER; then PTXT "" "${ERROR} Unable to read DNS server input." return 1 fi [ -z "${DNS_SERVER}" ] && DNS_SERVER="$2" if ! ipv4_is_valid "${DNS_SERVER}"; then PTXT "${ERROR} Invalid DNS server address entered" continue fi if [ "${DNS_SERVER1:-}" = "${DNS_SERVER}" ]; then PTXT "${ERROR} ${DNS_SERVER} DNS server address already entered, please try again!" continue fi case "$1" in "Default is") BOOTSTRAP1="${DNS_SERVER}" DNS_SERVER1="${DNS_SERVER}" ;; "2nd Default is") BOOTSTRAP2="${DNS_SERVER}" ;; esac return 0 done } read_input_num() { local RANGE RANGE="[${2}-${3}" [ -n "${4:-}" ] && RANGE="${RANGE}/${4}" [ -n "${5:-}" ] && RANGE="${RANGE}/${5}" [ -n "${6:-}" ] && RANGE="${RANGE}/${6}" [ -n "${7:-}" ] && RANGE="${RANGE}/${7}" RANGE="${RANGE}]" while :; do PTXT -n "${INPUT} ${1}, ${BOLD}${RANGE}${NORM}: " if ! read -r CHOSEN; then PTXT "" "${ERROR} Unable to read menu selection." return 1 fi if [ -z "${CHOSEN}" ]; then PTXT "${ERROR} Invalid character(s) entered! Retrying..." continue fi case "${CHOSEN}" in "${4:-}" | "${5:-}" | "${6:-}" | "${7:-}") return 1 ;; *) if ! PTXT "${CHOSEN}" | grep -qE '^[0-9]+$'; then PTXT "${ERROR} Invalid character(s) entered! Retrying..." continue fi if [ "${CHOSEN}" -lt "$2" ] || [ "${CHOSEN}" -gt "$3" ]; then PTXT "${ERROR} Chosen number is not in range! Retrying..." continue fi return 0 ;; esac done } read_input_port() { while :; do PTXT -n "${INPUT} ${1} ${BOLD}${2}: ${NORM}" if ! read -r WEB_PORT; then PTXT "" "${ERROR} Unable to read Web Port input." return 1 fi [ -z "${WEB_PORT}" ] && WEB_PORT="$2" if ! port_is_valid "${WEB_PORT}"; then PTXT "${ERROR} Invalid Web Port entered, only Web Ports residing within the port range 3000-65535 are selectable." continue fi WEB_PORT="$(PTXT "${WEB_PORT}" | sed 's/^0*//')" if ! ai_have_cmd netstat; then PTXT "${ERROR} Unable to verify the Web Port because netstat is unavailable." return 1 fi if web_port_in_use "${WEB_PORT}"; then PTXT "${ERROR} ${WEB_PORT} already in use, please choose a different port!" continue fi return 0 done } read_yesno() { local YESNO if [ "${AI_ASSUME_YES:-0}" = "1" ]; then PTXT "${INPUT} ${1} ${BOLD}[y/n]${NORM}: y" return 0 fi while :; do PTXT -n "${INPUT} ${1} ${BOLD}[y/n]${NORM}: " if ! read -r YESNO; then PTXT "" "${ERROR} Unable to read confirmation input." return 2 fi case "${YESNO}" in y | Y) return 0 ;; n | N) return 1 ;; *) PTXT "${ERROR} Invalid input!" ;; esac done } menu_action_allowed() { case "$1" in "1" | "${INSTALLER_ACTION_INSTALL:-install}" | "${INSTALLER_ACTION_UPDATE:-update}" | "2" | "uninstall") return 0 ;; [dD] | "doctor" | "status") return 0 ;; "3" | "changepw" | "4" | "reconfigure" | "5" | "setamtmupdate" | "6" | "setlocalcache" | "7" | "switchbranch" | "8" | "setipset" | [mM] | "migrate-runtime-defaults" | [bB] | "backup") [ -d "${TARG_DIR}" ] && [ -f "${AGH_FILE}" ] && return 0 return 1 ;; "9" | "blocklists" | "unusedblocklists") [ -d "${TARG_DIR}" ] && [ -f "${AGH_FILE}" ] && [ -n "${BLOCKLIST_ANALYZER_SHA256}" ] && return 0 return 1 ;; [rR] | "restore") [ -f "${BASE_DIR}/backup_AdGuardHome.tar.gz" ] && return 0 return 1 ;; *) return 1 ;; esac } single_arg_menu_action() { case "$1" in "migrate-runtime-defaults") return 1 ;; esac menu_action_allowed "$1" } cli_action_requires_install_mode() { case "${1:-}" in install | update | restore | migrate-runtime-defaults) return 0 ;; *) return 1 ;; esac } startup_action_allows_unknown_install_mode() { case "${1:-}:${2:-}" in uninstall:* | *:uninstall | *:2) return 0 ;; *) return 1 ;; esac } # Support export: bounded, read-only collection with a fixed typed schema. # support_export_clock reads the kernel monotonic clock without an external probe. support_export_clock() { local rest uptime IFS=' ' read -r uptime rest /dev/null <"/proc/${probe_pid}/stat" || return 1 record="${record##*) }" set -f set -- ${record} [ "$#" -ge 20 ] || return 1 SX_PID_STATE="$1" SX_PID_PARENT="$2" SX_PID_START="${20}" case "${SX_PID_PARENT}" in '' | *[!0-9]*) return 1 ;; *) : ;; esac case "${SX_PID_START}" in '' | *[!0-9]*) return 1 ;; *) : ;; esac } # Fixed stock probes do not spawn persistent workers. On interruption, one # bounded procfs scan also records their immediate children, where visible. # This is cleanup for observed workers, not a supervisor for daemonized programs. support_export_children() { local entry pid scanned SX_CHILDREN='' scanned=0 set +f for entry in /proc/[0-9]*/stat; do scanned="$((scanned + 1))" [ "${scanned}" -le 512 ] || break pid="${entry#/proc/}" pid="${pid%/stat}" if support_export_pid "${pid}" && [ "${SX_PID_PARENT}" = "${SX_CHILD_PID}" ]; then SX_CHILDREN="${SX_CHILDREN} ${pid}:${SX_PID_START}" fi done set -f } # Signal only still-identical probe processes, and never wait on a live task. support_export_stop() { local attempt child pid start signal [ -n "${SX_CHILD_PID:-}" ] || return 0 support_export_children for attempt in 1 2; do if [ "${attempt}" -eq 1 ]; then signal=TERM; else signal=KILL; fi for child in ${SX_CHILDREN}; do pid="${child%:*}" start="${child#*:}" if support_export_pid "${pid}" && [ "${SX_PID_START}" = "${start}" ]; then kill -"${signal}" "${pid}" 2>/dev/null || true fi done if support_export_pid "${SX_CHILD_PID}" && [ "${SX_PID_START}" = "${SX_CHILD_START}" ] && [ "${SX_PID_STATE}" != Z ]; then kill -"${signal}" "${SX_CHILD_PID}" 2>/dev/null || true fi usleep 100000 2>/dev/null || sleep 1 done if ! support_export_pid "${SX_CHILD_PID}" || [ "${SX_PID_STATE}" = Z ]; then wait "${SX_CHILD_PID}" 2>/dev/null || true SX_CHILD_PID='' return 0 fi # A kernel task stuck in uninterruptible I/O must not block diagnostics. return 1 } # support_export_probe caps output and waits at most two seconds for one fixed # external command. Failed, oversized and expired probes are never parsed. support_export_probe() { local deadline size status SX_PROBE_STATUS=125 [ -z "${SX_CHILD_PID:-}" ] || return 1 support_export_clock || return 1 [ "${SX_NOW}" -lt "${SX_DEADLINE}" ] || { SX_FAILURES="$((SX_FAILURES + 1))" return 1 } deadline="$((SX_NOW + 2))" ( # BusyBox ash expresses file-size limits in 512-byte blocks. ulimit -f 16 || exit 1 exec "$@" ) >"${SX_STAGE}/work/probe" 2>/dev/null /dev/null || sleep 1 done wait "${SX_CHILD_PID}" 2>/dev/null status="$?" SX_CHILD_PID='' SX_PROBE_STATUS="${status}" size="$(wc -c <"${SX_STAGE}/work/probe")" || size=8192 case "${size}" in '' | *[!0-9]*) size=8192 ;; *) : ;; esac [ "${size}" -lt 8192 ] || { SX_PROBE_STATUS=125 SX_FAILURES="$((SX_FAILURES + 1))" return 1 } [ "${status}" -eq 0 ] || { SX_FAILURES="$((SX_FAILURES + 1))" return 1 } } # support_export_emit accepts only the named value type and one short line. support_export_emit() { local key type value key="$1" type="$2" value="$3" if [ "${#value}" -gt 96 ]; then value=unknown; fi case "${value}" in '' | *[[:cntrl:]]*) value=unknown ;; *) : ;; esac case "${type}:${value}" in integer:*) case "${value}" in *[!0-9]* | '') value=unknown ;; *) : ;; esac [ "${#value}" -le 12 ] || value=unknown ;; version:*) value="$(printf '%s\n' "${value}" | awk 'length($0) <= 64 && /^v?[0-9]+[.][0-9]+[.][0-9]+(-([abr][.][0-9]+|rc[.][1-9][0-9]?))?([+][0-9a-f]+)?$/ { print; found=1 } END { if (!found) print "unknown" }')" || value=unknown ;; model:*) value="$(printf '%s\n' "${value}" | awk 'length($0) <= 32 && /^(RT|GT|TUF|RP)-[A-Z]+[0-9][0-9A-Z_+-]*$|^(ET|XT|XD)[0-9][0-9A-Z_+-]*$/ { print; found=1 } END { if (!found) print "unknown" }')" || value=unknown ;; firmware:*) case "${value}" in *[!0-9._-]* | '' | [!0-9]* | *[!0-9]) value=unknown ;; *) : ;; esac [ "${#value}" -le 32 ] || value=unknown ;; reference:*) case "${value}" in master | dev | dev[0-9] | unknown) ;; *) if [ "${#value}" -eq 40 ]; then case "${value}" in *[!0-9a-fA-F]*) value=custom ;; *) : ;; esac else value="$(printf '%s\n' "${value}" | awk 'length($0) <= 32 && /^v[0-9]+[.][0-9]+[.][0-9]+(-rc[.][1-9][0-9]?)?$/ { print; found=1 } END { if (!found) print "custom" }')" || value=custom fi ;; esac ;; architecture:armv5l | architecture:armv5tel | architecture:armv6l | architecture:armv7l | architecture:armv8l | architecture:aarch64 | architecture:mips | architecture:mipsel) ;; architecture:*) value=unknown ;; category:*) case "${value}" in unknown | none | absent | present | available | missing | unsafe | valid | invalid | yes | no | release | stable | beta | edge | complete | incomplete | pending | failed | checked | not-attempted | not-reported | completed | adguardhome | dnsmasq | foreign | mixed | unowned | binary-replace | directory-restore | restore | setup | nvram | event-hooks | package | yaml | blocklist | operation | service | install) ;; *) value=unknown ;; esac ;; *) value=unknown ;; esac printf '%s=%s\n' "${key}" "${value}" || SX_WRITE_FAILED=1 } # support_export_snapshot reads at most 8192 bytes of a fixed regular input. support_export_snapshot() { local snapshot_file snapshot_file="$1" [ -f "${snapshot_file}" ] && [ ! -L "${snapshot_file}" ] || return 1 support_export_probe dd "if=${snapshot_file}" bs=1024 count=8 } # support_export_config returns only two selected preference keys; it never # sources private configuration or copies any other key into the report. support_export_config() { local key value SX_SOURCE="${SUPPORT_EXPORT_SOURCE_REF:-unknown}" SX_CHANNEL=unknown if support_export_snapshot /opt/etc/AdGuardHome/.config; then while IFS='=' read -r key value; do case "${key}" in INSTALLER_BRANCH | ADGUARD_BRANCH) case "${value}" in \"*\") value="${value#\"}" value="${value%\"}" ;; *) : ;; esac if [ "${key}" = INSTALLER_BRANCH ]; then [ "${SX_SOURCE}" != unknown ] || SX_SOURCE="${value}" else case "${value}" in release | stable | beta | edge) SX_CHANNEL="${value}" ;; *) : ;; esac fi ;; *) : ;; esac done <"${SX_STAGE}/work/probe" fi support_export_emit installer.source reference "${SX_SOURCE}" support_export_emit adguardhome.channel category "${SX_CHANNEL}" } # support_export_hook reports file categories, never a hook's actual content. support_export_hook() { local first hook state hook="$1" state=missing [ ! -e "/jffs/scripts/${hook}" ] || state=present [ ! -L "/jffs/scripts/${hook}" ] || state=unsafe support_export_emit "hook.${hook}.state" category "${state}" state=unknown if support_export_snapshot "/jffs/scripts/${hook}"; then IFS= read -r first <"${SX_STAGE}/work/probe" || first='' if [ "${first}" = '#!/bin/sh' ]; then state=valid; else state=invalid; fi fi support_export_emit "hook.${hook}.header" category "${state}" state=no [ ! -x "/jffs/scripts/${hook}" ] || state=yes support_export_emit "hook.${hook}.executable" category "${state}" } # support_export_marker classifies fixed recovery artifacts without reading paths. support_export_marker() { local marker_key marker_path state marker_key="$1" marker_path="$2" state=absent [ ! -e "${marker_path}" ] || state=present [ ! -L "${marker_path}" ] || state=unsafe support_export_emit "${marker_key}" category "${state}" } # support_export_collect renders only fixed keys from bounded snapshots. support_export_collect() { local count field first hook key nvram_binary owner port result stage transport value version SX_WRITE_FAILED=0 printf '%s\n' 'support.schema=1' 'support.privacy=allowlist' 'support.upload=none' || SX_WRITE_FAILED=1 support_export_emit installer.version version "${AI_VERSION:-unknown}" support_export_config version=unknown if [ -f /opt/etc/AdGuardHome/AdGuardHome ] && [ ! -L /opt/etc/AdGuardHome/AdGuardHome ] && [ -x /opt/etc/AdGuardHome/AdGuardHome ] && support_export_probe /opt/etc/AdGuardHome/AdGuardHome --version; then IFS= read -r first <"${SX_STAGE}/work/probe" || first='' case "${first}" in 'AdGuard Home, version '*) version="${first#AdGuard Home, version }" ;; *) : ;; esac fi support_export_emit adguardhome.version version "${version}" nvram_binary="$(installer_nvram_path)" || nvram_binary="" for key in productid firmver buildno extendno; do value=unknown if [ -n "${nvram_binary}" ] && support_export_probe "${nvram_binary}" get "${key}"; then # A second line makes the entire value invalid rather than silently # accepting a valid prefix followed by injected report content. value="$(cat "${SX_STAGE}/work/probe")" fi case "${key}" in productid) support_export_emit router.model model "${value}" ;; *) support_export_emit "router.${key}" firmware "${value}" ;; esac done value=unknown if support_export_probe uname -m; then value="$(cat "${SX_STAGE}/work/probe")"; fi support_export_emit router.architecture architecture "${value}" value=missing [ ! -x /opt/bin/opkg ] || value=available support_export_emit entware.state category "${value}" support_export_marker installation.state /opt/etc/AdGuardHome value=unknown if support_export_probe df -Pk /opt; then value="$(awk 'NR == 2 && $2 ~ /^[0-9]+$/ && $4 ~ /^[0-9]+$/ { print $4; exit }' "${SX_STAGE}/work/probe")" fi support_export_emit storage.opt.available_kib integer "${value}" value=unknown if support_export_probe df -Pk /jffs; then value="$(awk 'NR == 2 && $2 ~ /^[0-9]+$/ && $4 ~ /^[0-9]+$/ { print $4; exit }' "${SX_STAGE}/work/probe")" fi support_export_emit storage.jffs.available_kib integer "${value}" if support_export_snapshot /proc/meminfo; then for field in MemTotal MemAvailable MemFree SwapTotal SwapFree; do value="$(awk -v key="${field}:" '$1 == key && $2 ~ /^[0-9]+$/ { print $2; exit }' "${SX_STAGE}/work/probe")" support_export_emit "memory.${field}.kib" integer "${value}" done else for field in MemTotal MemAvailable MemFree SwapTotal SwapFree; do support_export_emit "memory.${field}.kib" integer unknown; done fi count=unknown if support_export_probe pidof AdGuardHome; then count="$(awk '{ for (i=1; i<=NF; i++) { if ($i !~ /^[0-9]+$/) { invalid=1; exit } n++ } } END { if (!invalid && n <= 128) print n+0 }' "${SX_STAGE}/work/probe")" elif [ "${SX_PROBE_STATUS}" -eq 1 ] && [ ! -s "${SX_STAGE}/work/probe" ]; then count=0; fi support_export_emit process.daemon.count integer "${count}" count=unknown if support_export_probe ps -ww; then count="$(awk '/AdGuardHome[.]sh|S99AdGuardHome|rc[.]func[.]AdGuardHome/ { for (i=1; i<=NF; i++) if ($i == "monitor-start") { n++; break } } END { print n+0 }' "${SX_STAGE}/work/probe")" fi support_export_emit process.monitor.count integer "${count}" result=unknown if support_export_probe netstat -nltup; then result=complete; fi for port in 53 553; do for transport in tcp udp; do owner=unknown if [ "${result}" = complete ]; then owner="$(awk -v protocol="${transport}" -v port=":${port}" ' $1 ~ ("^" protocol "6?$") && $4 ~ (port "$") { value="unowned" for (i=5; i<=NF; i++) if ($i ~ /^[0-9]+\//) { if ($i ~ /\/AdGuardHome$/) value="adguardhome" else if ($i ~ /\/dnsmasq$/) value="dnsmasq" else value="foreign" } if (owner != "" && owner != value) owner="mixed"; else if (owner == "") owner=value } END { print owner == "" ? "none" : owner } ' "${SX_STAGE}/work/probe")" fi support_export_emit "listener.${transport}.${port}.owner" category "${owner}" done done for hook in dnsmasq.postconf dnsmasq-sdn.postconf init-start services-stop service-event-end firewall-start; do support_export_hook "${hook}"; done support_export_marker recovery.nvram /opt/etc/.AdGuardHome.nvram support_export_marker recovery.event_hooks /opt/etc/.AdGuardHome.event-hooks-recovery support_export_marker recovery.binary_cleanup /opt/etc/.AdGuardHome.binary-cleanup support_export_marker recovery.dns_handoff /tmp/AdGuardHome.dns-handoff/active support_export_emit recovery.prior_startup category "${SUPPORT_EXPORT_RECOVERY_STATUS:-not-reported}" result=none stage=none if [ -L /opt/etc/.AdGuardHome.rollback_result ]; then result=unsafe elif support_export_snapshot /opt/etc/.AdGuardHome.rollback_result; then result=unknown stage=unknown while IFS='=' read -r key value; do case "${key}" in result) case "${value}" in 'rollback restored' | 'restore complete' | "${INSTALLER_ROLLBACK_COMPLETE:-rollback complete}" | restored | removed-failed-install) result=complete ;; 'no rollback attempted' | 'interrupted: no rollback attempted' | 'failed: no change') result=not-attempted ;; "${INSTALLER_ROLLBACK_FAILED:-rollback failed}" | "${INSTALLER_ROLLBACK_PARTIAL:-rollback partial}" | "${INSTALLER_ROLLBACK_UNAVAILABLE:-rollback unavailable}" | 'incomplete prior rollback detected' | restore-failed | cleanup-failed | restart-failed | replace-failed | yaml-invalid-moved | yaml-quarantine-failed | staged-yaml-invalid-moved | staged-yaml-quarantine-failed) result=incomplete ;; *) : ;; esac ;; context) case "${value}" in 'package install') stage=package ;; "${INSTALLER_BACKUP_RESTORE:-backup restore}") stage=restore ;; 'NVRAM transaction' | "${INSTALLER_DNS_TRANSACTION:-DNS NVRAM transaction}" | 'DNSFilter NVRAM transaction') stage=nvram ;; "${INSTALLER_YAML_VALIDATION:-yaml validation}") stage=yaml ;; "${INSTALLER_BLOCKLIST_REPLACEMENT:-blocklist yaml replacement}" | blocklist-yaml) stage=blocklist ;; 'AdGuardHome service restart' | service-restart) stage=service ;; 'installer abort' | install-abort) stage=install ;; "${INSTALLER_BINARY_ROLLBACK:-AdGuardHome binary rollback}" | binary-replace) stage=binary-replace ;; "${INSTALLER_DIRECTORY_ROLLBACK:-AdGuardHome directory rollback}" | directory-restore) stage=directory-restore ;; setup-yaml | setup) stage=setup ;; operation | restore | nvram | event-hooks) stage="${value}" ;; *) : ;; esac ;; *) : ;; esac done <"${SX_STAGE}/work/probe" fi support_export_emit recovery.rollback.result category "${result}" support_export_emit recovery.rollback.stage category "${stage}" support_export_emit collection.failed_probes integer "${SX_FAILURES}" support_export_emit collection.timed_out_probes integer "${SX_TIMEOUTS}" [ "${SX_WRITE_FAILED}" -eq 0 ] } # support_export_cleanup removes only this invocation's still-identical private # staging directory. The published destination is never removed on any path. support_export_cleanup() { local identity inode mode links owner rest support_export_stop || true [ "${SX_OWNED:-0}" -eq 1 ] && [ -d "${SX_STAGE}" ] && [ ! -L "${SX_STAGE}" ] || return 0 identity="$(ls -ldni "${SX_STAGE}" 2>/dev/null)" || { printf '%s\n' 'support-export: staging ownership could not be verified; private staging retained.' >&2 return 0 } read -r inode mode links owner rest <&2 return 0 } rm -rf "${SX_STAGE}" 2>/dev/null || printf '%s\n' 'support-export: private staging cleanup failed.' >&2 } # support_export accepts exactly --output /absolute/new-file. Collection runs # in a subshell so traps, limits, cwd and umask never modify installer state. support_export() { ( local destination parent physical name mode links owner rest key value size output_option output_option="${1-}" [ "$#" -eq 2 ] && [ "${output_option}" = --output ] || { printf '%s\n' 'support-export: usage: support-export --output /absolute/new-report.txt' >&2 exit 2 } destination="$2" [ "${#destination}" -le 512 ] || exit 2 case "${destination}" in /*) ;; *) exit 2 ;; esac case "${destination}" in *[[:cntrl:]]* | */ | *//* | */../* | */./* | */.. | */.) exit 2 ;; *) : ;; esac parent="${destination%/*}" [ -n "${parent}" ] || parent=/ name="${destination##*/}" CDPATH='' cd -P "${parent}" 2>/dev/null || exit 1 physical="$(pwd -P)" || exit 1 [ "${physical}" = "${parent}" ] || exit 1 [ ! -e "./${name}" ] && [ ! -L "./${name}" ] || exit 1 umask 077 SX_STAGE="./.AdGuardHome.support-export.$$" SX_OWNED=0 SX_CHILD_PID='' SX_CHILD_START='' SX_FAILURES=0 SX_TIMEOUTS=0 SX_INODE='' SX_UID='' while read -r key value owner rest; do if [ "${key}" = Uid: ]; then SX_UID="${owner}" break fi done /dev/null || exit 1 SX_OWNED=1 mkdir -m 700 "${SX_STAGE}/work" "${SX_STAGE}/publish" 2>/dev/null || exit 1 support_export_clock || exit 1 SX_DEADLINE="$((SX_NOW + 30))" support_export_probe ls -ldni "${SX_STAGE}" || exit 1 read -r SX_INODE mode links owner rest <"${SX_STAGE}/work/probe" || exit 1 [ "${owner}" = "${SX_UID}" ] && [ "${mode}" = drwx------ ] || exit 1 support_export_probe ls -ldn . || exit 1 read -r mode links owner rest <"${SX_STAGE}/work/probe" || exit 1 [ "${owner}" = "${SX_UID}" ] || exit 1 case "${mode}" in d????w???? | d???????w?) # A root-owned sticky directory such as /tmp permits exclusive # publication; other shared writable parents are unsafe. [ "${owner}" = 0 ] || exit 1 case "${mode}" in *t | *T) ;; *) exit 1 ;; esac ;; *) : ;; esac # Allow a 16 KiB report with BusyBox ash's 512-byte blocks. ulimit -f 32 || exit 1 support_export_collect >"${SX_STAGE}/publish/${name}" || exit 1 size="$(wc -c <"${SX_STAGE}/publish/${name}")" || exit 1 case "${size}" in '' | *[!0-9]*) exit 1 ;; *) : ;; esac [ "${size}" -le 16384 ] || exit 1 # Linking into the held parent directory creates exactly name and # refuses every existing object, including a raced symlink/directory. ln "${SX_STAGE}/publish/${name}" . 2>/dev/null || exit 1 printf '%s\n' 'support-export: sanitized report created; sharing remains under operator control.' ) } # List supported CLI actions, required arguments, and update-policy restrictions. cli_usage() { PTXT "${INFO} Usage:" \ "${INFO} sh installer --no-auto-update [installer-ref] [menu-action]" \ "${INFO} Per-invocation opt-out also accepts a CLI --no-auto-update flag. Bundle-changing install/update/restore refuse before requested work, including --dry-run; AGH-only compatibility is not established." \ "${INFO} sh installer install (interactive fresh setup)" \ "${INFO} sh installer install [--installer-branch master] [--adguardhome-branch {release|beta|edge}] --yes [--allow-dns-nvram]" \ "${INFO} Flagged install requires an existing AdGuardHome environment; --yes does not configure first-run inputs." \ "${INFO} sh installer update [--installer-branch master] [--adguardhome-branch {release|beta|edge}] --yes [--dry-run]" \ "${INFO} sh installer backup --yes" \ "${INFO} sh installer restore --file /path/to/backup_AdGuardHome.tar.gz --yes [--dry-run]" \ "${INFO} sh installer preflight [install|reconfigure|update|restore|uninstall|status]" \ "${INFO} sh installer doctor [--fix]" \ "${INFO} sh installer support-export --output /absolute/new-report.txt" \ "${INFO} sh installer ipset refresh [--yes] [--dry-run]" \ "${INFO} sh installer ipset status" \ "${INFO} sh installer ipset doctor" \ "${INFO} sh installer netcheck --mode {legacy|wan|lan} [--hosts host-list] [--dns server] [--require-http {YES|NO}] [--timeout seconds]" \ "${INFO} sh installer dns-port-policy --policy {legacy|refuse-unknown}" \ "${INFO} sh installer performance --profile {balanced|low-memory|fast}" \ "${INFO} sh installer migrate-runtime-defaults [--yes] [--dry-run]" \ "${INFO} sh installer uninstall --yes [--dry-run] [--allow-dns-nvram]" } # Installer and service/helper scripts are one compatible bundle. Until local # compatibility can be established, opt-out refuses any operation replacing it. installer_bundle_update_allowed() { [ "${AI_NO_AUTO_UPDATE:-0}" = "1" ] || return 0 PTXT "${ERROR} --no-auto-update prevents installer and service/helper bundle replacement." PTXT "${ERROR} This operation is refused because compatibility with the existing bundle cannot be established; an AdGuardHome-only update is not supported." return 1 } installer_update_policy_from_args() { while [ "$#" -gt 0 ]; do case "$1" in --no-auto-update) AI_NO_AUTO_UPDATE=1 export AI_NO_AUTO_UPDATE shift ;; --installer-branch | --branch | --adguardhome-branch | --file | --profile | --mode | --hosts | --dns | --require-http | --timeout | --policy) [ "$#" -gt 1 ] || return 0 shift 2 ;; *) shift ;; esac done } # Refuse an explicitly requested bundle operation before ordinary startup writes. installer_requested_bundle_update_allowed() { [ "${AI_NO_AUTO_UPDATE:-0}" = "1" ] || return 0 case "${1:-}:${2:-}" in amtmupdate:check) return 0 ;; amtmupdate:* | 1:* | install:* | update:* | 7:* | switchbranch:* | r:* | R:* | restore:* | *:1 | *:install | *:update | *:7 | *:switchbranch | *:r | *:R | *:restore) installer_bundle_update_allowed return "$?" ;; *) : ;; esac # Keep non-bundle actions available, including recovery of partial installs. case "${1:-}" in 2 | uninstall | 3 | changepw | 4 | reconfigure | 5 | setamtmupdate | 6 | setlocalcache | 8 | setipset | 9 | blocklists | unusedblocklists | b | B | backup | d | D | doctor | status | m | M | migrate-runtime-defaults | ipset | netcheck | dns-port-policy | performance) return 0 ;; *) : ;; esac # A fresh default/ref-only menu enters first-run setup before showing choices. if [ ! -f "${AGH_FILE}" ] && [ -z "${2:-}" ]; then installer_bundle_update_allowed return "$?" fi return 0 } # Flagged install reuses an existing environment; first-run inputs remain interactive. cli_fresh_install_supported() { [ "${1:-}" = "${INSTALLER_ACTION_INSTALL:-install}" ] && [ ! -f "${AGH_FILE}" ] || return 0 PTXT "${ERROR} Non-interactive install cannot complete fresh first-run setup." PTXT "${ERROR} Run sh installer install with no flags for interactive setup, or use restore --file with a validated backup." return 1 } cli_require_yes() { if [ "${CLI_YES:-0}" != "1" ]; then PTXT "${ERROR} Refusing destructive non-interactive action without --yes." return 1 fi return 0 } cli_enable_assume_yes() { AI_ASSUME_YES="1" export AI_ASSUME_YES } cli_dry_run() { PTXT "${INFO} Dry-run: would run $1." } cli_pre_remote_dry_run() { local cmd opt restore_file sub cmd="${1:-}" shift || true restore_file="" case "${cmd}" in install | update | restore | uninstall) ;; ipset) sub="${1:-}" [ "${sub}" = "refresh" ] || return 1 shift || true ;; *) return 1 ;; esac while [ "$#" -gt 0 ]; do opt="$1" case "${opt}" in --dry-run) case "${cmd}" in restore) cli_dry_run "restore ${restore_file:-${BASE_DIR}/backup_AdGuardHome.tar.gz}" ;; ipset) cli_dry_run "IPSET refresh" ;; *) cli_dry_run "${cmd}" ;; esac return 0 ;; --file) [ "$#" -gt 1 ] || return 1 restore_file="$2" shift 2 ;; --branch | --adguardhome-branch | --installer-branch | --profile) [ "$#" -gt 1 ] || return 1 shift 2 ;; *) shift ;; esac done return 1 } cli_pre_runtime_defaults_preview() { local apply preview_args [ "${1:-}" = "migrate-runtime-defaults" ] || return 3 shift || true apply="0" preview_args="" while [ "$#" -gt 0 ]; do case "$1" in --yes) apply="1" ;; --dry-run) preview_args="${preview_args} --dry-run" ;; *) return 2 ;; esac shift done [ "${apply}" = "0" ] || return 3 # Intentional unquoted expansion: preview_args is built from fixed option tokens only. cli_migrate_runtime_defaults ${preview_args} return "$?" } cli_backup_restore_file() { local restore_default restore_file restore_save restore_defer_saved cli_ret restore_file="$1" restore_default="${BASE_DIR}/backup_AdGuardHome.tar.gz" restore_defer_saved="${ADGUARD_DEFER_END_OP:-0}" if [ -z "${restore_file}" ]; then PTXT "${ERROR} restore requires --file /path/to/backup_AdGuardHome.tar.gz." return 1 fi if [ ! -f "${restore_file}" ]; then PTXT "${ERROR} Restore file not found: ${restore_file}" return 1 fi if ! adguard_archive_is_safe "${restore_file}" 1; then PTXT "${ERROR} Restore file failed backup safety validation: ${restore_file}" return 1 fi if [ "${restore_file}" = "${restore_default}" ]; then ADGUARD_DEFER_END_OP="1" backup_restore RESTORE cli_ret="$?" ADGUARD_DEFER_END_OP="${restore_defer_saved}" return "${cli_ret}" fi restore_save="${restore_default}.cli.$$" [ -f "${restore_default}" ] && mv "${restore_default}" "${restore_save}" if ! cp "${restore_file}" "${restore_default}"; then [ -f "${restore_save}" ] && mv "${restore_save}" "${restore_default}" return 1 fi if ! chmod 600 "${restore_default}"; then rm -f "${restore_default}" [ -f "${restore_save}" ] && mv "${restore_save}" "${restore_default}" return 1 fi ADGUARD_DEFER_END_OP="1" backup_restore RESTORE cli_ret="$?" ADGUARD_DEFER_END_OP="${restore_defer_saved}" if [ -f "${restore_save}" ]; then mv "${restore_save}" "${BASE_DIR}/backup_AdGuardHome.tar.gz" fi return "${cli_ret}" } cli_bool_value() { case "$1" in YES | yes | Yes | ON | on | On | TRUE | true | True | 1) PTXT "YES" ;; NO | no | No | OFF | off | Off | FALSE | false | False | 0) PTXT "NO" ;; *) return 1 ;; esac } cli_adguard_branch_is_valid() { case "$1" in release | beta | edge) return 0 ;; *) return 1 ;; esac } cli_simple_value_is_safe() { case "$1" in "" | *[!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._:-]*) return 1 ;; *) return 0 ;; esac } cli_host_list_is_safe() { [ -n "$1" ] || return 1 [ -z "$(printf '%s' "$1" | tr -d 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._: -')" ] } cli_write_quoted_conf() { if ! write_conf "$1" "\"$2\""; then return 1 fi return 0 } cli_netcheck_config_values() { local dns host_list http_required mode timeout mode="$1" host_list="$2" dns="$3" http_required="$4" timeout="$5" case "${mode}" in legacy | wan | lan) ;; *) PTXT "${ERROR} netcheck requires --mode {legacy|wan|lan}." return 1 ;; esac [ -z "${host_list}" ] || cli_host_list_is_safe "${host_list}" || { PTXT "${ERROR} netcheck hosts contain unsupported characters." return 1 } [ -z "${dns}" ] || cli_simple_value_is_safe "${dns}" || { PTXT "${ERROR} netcheck DNS server contains unsupported characters." return 1 } case "${timeout}" in "") ;; *[!0123456789]*) PTXT "${ERROR} netcheck timeout must be a positive number of seconds." return 1 ;; *) [ "${timeout}" -gt 0 ] 2>/dev/null || { PTXT "${ERROR} netcheck timeout must be greater than zero." return 1 } ;; esac cli_write_quoted_conf ADGUARD_NETCHECK_MODE "${mode}" || return 1 [ -z "${host_list}" ] || cli_write_quoted_conf ADGUARD_NETCHECK_HOSTS "${host_list}" || return 1 [ -z "${dns}" ] || cli_write_quoted_conf ADGUARD_NETCHECK_DNS "${dns}" || return 1 [ -z "${http_required}" ] || cli_write_quoted_conf ADGUARD_NETCHECK_REQUIRE_HTTP "${http_required}" || return 1 [ -z "${timeout}" ] || cli_write_quoted_conf ADGUARD_NETCHECK_TIMEOUT "${timeout}" || return 1 ptxt_ok "Netcheck mode set to ${mode}." } cli_dns_port_policy() { local policy policy="" while [ "$#" -gt 0 ]; do case "$1" in --policy) [ "$#" -gt 1 ] || return 2 policy="$2" shift 2 ;; *) return 2 ;; esac done case "${policy}" in legacy) cli_write_quoted_conf ADGUARDHOME_REFUSE_UNKNOWN_DNS_PORT_KILL "0" || return 1 ptxt_ok "DNS port-owner cleanup policy set to legacy." ;; refuse-unknown) cli_write_quoted_conf ADGUARDHOME_REFUSE_UNKNOWN_DNS_PORT_KILL "1" || return 1 ptxt_ok "DNS port-owner cleanup policy set to refuse unknown owners." ;; *) PTXT "${ERROR} dns-port-policy requires --policy {legacy|refuse-unknown}." return 1 ;; esac } cli_migrate_runtime_default() { local current key legacy safe status key="$1" legacy="$2" safe="$3" current="$(conf_value "${key}")" if [ "${current}" = "${legacy}" ]; then PTXT "${WARNING} Legacy runtime default: ${key}=\"${current}\"; v2.6.0 safer value is \"${safe}\"." status=1 elif [ -z "${current}" ]; then PTXT "${INFO} Missing runtime default: ${key}; v2.6.0 safer value is \"${safe}\"." status=1 else PTXT "${INFO} Runtime default already set: ${key}=\"${current}\"." status=0 fi return "${status}" } cli_migrate_runtime_defaults() { local apply dry_run changes dns_port_change ipset_change netcheck_change proc_optimize_change proc_profile_change proc_optimize_current proc_profile_current install_mode netcheck_target apply="0" dry_run="0" changes="0" while [ "$#" -gt 0 ]; do case "$1" in --yes) apply="1" ;; --dry-run) dry_run="1" ;; *) return 2 ;; esac shift done adguard_install_mode_detect || return 1 if ! adguard_install_mode_confirmed; then PTXT "${ERROR} Runtime-default migration requires a confirmed router install mode; no settings were inspected or changed." return 1 fi if [ ! -f "${CONF_FILE}" ]; then PTXT "${ERROR} Runtime config not found: ${CONF_FILE}" return 1 fi PTXT "${INFO} Inspecting runtime defaults in ${CONF_FILE}." dns_port_change="0" ipset_change="0" netcheck_change="0" proc_optimize_change="0" proc_profile_change="0" install_mode="${ADGUARD_INSTALL_MODE}" case "${install_mode}" in lan) netcheck_target="lan" if ! adguard_ipset_allowed && { [ "$(conf_value ADGUARD_IPSET)" != "NO" ] || awk ' /^[[:space:]]*(ipset|ipset_file|\047ipset\047|\047ipset_file\047|"ipset"|"ipset_file"):[[:space:]]*/ { found = 1 } END { exit !found } ' "${YAML_FILE}" 2>/dev/null; }; then PTXT "${WARNING} LAN/AP/Bridge mode config has IPSET enabled; migration will disable it because IPTABLES/IPSET rules are not manageable there." changes="1" ipset_change="1" fi ;; *) netcheck_target="wan" ;; esac cli_migrate_runtime_default ADGUARDHOME_REFUSE_UNKNOWN_DNS_PORT_KILL 0 1 || { changes="1" dns_port_change="1" } cli_migrate_runtime_default ADGUARD_NETCHECK_MODE legacy "${netcheck_target}" || { changes="1" netcheck_change="1" } proc_optimize_current="$(conf_value ADGUARD_PROC_OPTIMIZE)" if [ -z "${proc_optimize_current}" ]; then PTXT "${INFO} Missing runtime default: ADGUARD_PROC_OPTIMIZE; optimization remains enabled with the compatible aggressive profile." changes="1" proc_optimize_change="1" else PTXT "${INFO} Runtime default already set: ADGUARD_PROC_OPTIMIZE=\"${proc_optimize_current}\"." fi proc_profile_current="$(conf_value ADGUARD_PROC_PROFILE)" if [ -z "${proc_profile_current}" ]; then PTXT "${INFO} Missing runtime default: ADGUARD_PROC_PROFILE; compatible value is \"aggressive\"." changes="1" proc_profile_change="1" else PTXT "${INFO} Runtime default already set: ADGUARD_PROC_PROFILE=\"${proc_profile_current}\"." fi if [ "${changes}" = "0" ]; then ptxt_ok "Runtime defaults already use v2.6.0 safer values or custom overrides." return 0 fi if [ "${dry_run}" = "1" ]; then PTXT "${INFO} Dry-run: would write v2.6.0 safer runtime defaults." return 0 fi if [ "${apply}" != "1" ]; then PTXT "${INFO} No changes made. Re-run with --yes to migrate runtime defaults." return 0 fi if [ "${dns_port_change}" = "1" ]; then cli_write_quoted_conf ADGUARDHOME_REFUSE_UNKNOWN_DNS_PORT_KILL 1 || return 1 fi if [ "${netcheck_change}" = "1" ]; then cli_write_quoted_conf ADGUARD_NETCHECK_MODE "${netcheck_target}" || return 1 fi if [ "${ipset_change}" = "1" ]; then ADGUARD_INSTALL_MODE="lan" write_conf ADGUARD_IPSET "\"NO\"" || return 1 adguardhome_yaml_remove_ipset_file || return 1 fi if [ "${proc_optimize_change}" = "1" ]; then cli_write_quoted_conf ADGUARD_PROC_OPTIMIZE YES || return 1 fi if [ "${proc_profile_change}" = "1" ]; then cli_write_quoted_conf ADGUARD_PROC_PROFILE aggressive || return 1 fi ptxt_ok "Runtime defaults migrated to v2.6.0 safer values." } cli_installer_branch_from_args() { local selected_branch selected_branch="" while [ "$#" -gt 0 ]; do case "$1" in --installer-branch) [ "$#" -gt 1 ] && [ -z "${selected_branch}" ] || return 1 branch_is_safe "$2" || return 1 selected_branch="$2" shift 2 ;; --branch | --adguardhome-branch | --file | --profile | --mode | --hosts | --dns | --require-http | --timeout | --policy) [ "$#" -gt 1 ] || return 1 shift 2 ;; *) shift ;; esac done PTXT "${selected_branch}" } # resolve_installer_branch establishes one source before preferences or downloads. resolve_installer_branch() { BRANCH="" INSTALLER_SOURCE_ORIGIN="explicit" case "${1:-}" in install | update | backup | restore | doctor | ipset | netcheck | dns-port-policy | performance | migrate-runtime-defaults | uninstall) BRANCH="$(cli_installer_branch_from_args "$@")" || { PTXT "${ERROR} Invalid or repeated --installer-branch; no installer source was selected." >&2 return 1 } ;; amtmupdate | "") ;; *) if [ -n "${2:-}" ]; then BRANCH="$1" else # Menu availability can depend on state loaded after source selection. case "${1:-}" in [1-9] | [qQbBrRdDmM] | install | update | uninstall | changepw | reconfigure | setamtmupdate | setlocalcache | switchbranch | setipset | blocklists | unusedblocklists | backup | restore | doctor | status | migrate-runtime-defaults) ;; *) BRANCH="$1" ;; esac fi ;; esac if [ -z "${BRANCH}" ]; then if [ -e "${CONF_FILE}" ] || [ -L "${CONF_FILE}" ]; then BRANCH="$(conf_value INSTALLER_BRANCH)" || { PTXT "${ERROR} Unable to read installer source from existing configuration ${CONF_FILE}; refusing to select a default source." >&2 return 1 } fi INSTALLER_SOURCE_ORIGIN="saved" fi if [ -z "${BRANCH}" ]; then BRANCH="master" INSTALLER_SOURCE_ORIGIN="default" fi BRANCH="$(sanitize_branch "${BRANCH}")" || return 1 PTXT "${INFO} Installer source: ${BRANCH} (${INSTALLER_SOURCE_ORIGIN}); AdGuardHome channel is selected separately." } cli_write_adguard_branch() { local old_branch new_branch new_branch="$1" old_branch="$(conf_value ADGUARD_BRANCH)" if ! write_conf ADGUARD_BRANCH "\"${new_branch}\""; then return 1 fi if [ -n "${old_branch}" ] && [ "${old_branch}" != "${new_branch}" ]; then ADGUARD_BRANCH_CHANGED="1" export ADGUARD_BRANCH_CHANGED fi return 0 } cli_run() { local cmd sub opt restore_file profile proc_profile adguard_branch installer_branch netcheck_mode netcheck_hosts netcheck_dns netcheck_http netcheck_timeout dns_policy migrate_args operation_status CLI_MODE="1" export CLI_MODE cmd="${1:-}" shift || true CLI_YES="0" CLI_DRY_RUN="0" CLI_ALLOW_DNS_NVRAM="0" restore_file="" profile="" adguard_branch="" installer_branch="" netcheck_mode="" netcheck_hosts="" netcheck_dns="" netcheck_http="" netcheck_timeout="" dns_policy="" migrate_args="" case "${cmd}" in install | update | backup | restore | doctor | ipset | netcheck | dns-port-policy | performance | migrate-runtime-defaults | uninstall) ;; *) return 2 ;; esac if [ "${cmd}" = "ipset" ]; then sub="${1:-}" [ -n "${sub}" ] && shift else sub="" fi while [ "$#" -gt 0 ]; do opt="$1" case "${opt}" in --no-auto-update) AI_NO_AUTO_UPDATE=1 export AI_NO_AUTO_UPDATE shift ;; --yes) [ "${cmd}" != "migrate-runtime-defaults" ] || migrate_args="${migrate_args} --yes" CLI_YES="1" shift ;; --dry-run) [ "${cmd}" != "migrate-runtime-defaults" ] || migrate_args="${migrate_args} --dry-run" CLI_DRY_RUN="1" shift ;; --allow-dns-nvram) CLI_ALLOW_DNS_NVRAM="1" shift ;; --fix) [ "${cmd}" = "doctor" ] || return 2 sub="--fix" shift ;; --branch | --adguardhome-branch) [ "$#" -gt 1 ] || return 2 adguard_branch="$2" shift 2 ;; --installer-branch) [ "$#" -gt 1 ] || return 2 installer_branch="$2" shift 2 ;; --file) [ "$#" -gt 1 ] || return 2 restore_file="$2" shift 2 ;; --profile) [ "$#" -gt 1 ] || return 2 profile="$2" shift 2 ;; --mode) [ "$#" -gt 1 ] || return 2 [ "${cmd}" = "netcheck" ] || return 2 netcheck_mode="$2" shift 2 ;; --hosts) [ "$#" -gt 1 ] || return 2 [ "${cmd}" = "netcheck" ] || return 2 netcheck_hosts="$2" shift 2 ;; --dns) [ "$#" -gt 1 ] || return 2 [ "${cmd}" = "netcheck" ] || return 2 netcheck_dns="$2" shift 2 ;; --require-http) [ "$#" -gt 1 ] || return 2 [ "${cmd}" = "netcheck" ] || return 2 netcheck_http="$(cli_bool_value "$2")" || return 2 shift 2 ;; --timeout) [ "$#" -gt 1 ] || return 2 [ "${cmd}" = "netcheck" ] || return 2 netcheck_timeout="$2" shift 2 ;; --policy) [ "$#" -gt 1 ] || return 2 [ "${cmd}" = "dns-port-policy" ] || return 2 dns_policy="$2" shift 2 ;; *) return 2 ;; esac done installer_requested_bundle_update_allowed "${cmd}" || return 1 if [ "${CLI_DRY_RUN}" != "1" ]; then cli_fresh_install_supported "${cmd}" || return 1 fi if [ -n "${installer_branch}" ]; then branch_is_safe "${installer_branch}" || { PTXT "${ERROR} Unsupported installer branch: ${installer_branch}" return 1 } write_conf INSTALLER_BRANCH "\"${installer_branch}\"" || return 1 fi if [ -n "${adguard_branch}" ]; then cli_adguard_branch_is_valid "${adguard_branch}" || { PTXT "${ERROR} Unsupported AdGuardHome branch: ${adguard_branch}" return 1 } fi case "${cmd}" in doctor) doctor "${sub}" return "$?" ;; update) [ "${CLI_DRY_RUN}" = "1" ] && { cli_dry_run "${INSTALLER_ACTION_UPDATE:-update}" return 0 } cli_require_yes || return 1 [ -z "${adguard_branch}" ] || cli_write_adguard_branch "${adguard_branch}" || return 1 cli_enable_assume_yes AUTO_UPDATE="${INSTALLER_ACTION_UPDATE:-update}" menu update operation_status=$? [ "${operation_status}" -eq 2 ] && return 1 return "${operation_status}" ;; backup) cli_require_yes || return 1 cli_enable_assume_yes backup_restore BACKUP CLI return "$?" ;; restore) [ "${CLI_DRY_RUN}" = "1" ] && { cli_dry_run "restore ${restore_file:-${BASE_DIR}/backup_AdGuardHome.tar.gz}" return 0 } cli_require_yes || return 1 ensure_jq_tool || return 1 cli_enable_assume_yes cli_backup_restore_file "${restore_file}" operation_status=$? [ "${operation_status}" -eq 2 ] && return 1 return "${operation_status}" ;; uninstall) [ "${CLI_DRY_RUN}" = "1" ] && { cli_dry_run "uninstall" return 0 } cli_require_yes || return 1 if [ "$(conf_value ADGUARD_DOMAIN)" = "yes" ] && [ "${CLI_ALLOW_DNS_NVRAM}" != "1" ]; then PTXT "${ERROR} Refusing uninstall DNS/NVRAM rewrite without --allow-dns-nvram." return 1 fi cli_enable_assume_yes uninst_all ;; ipset) case "${sub}" in status | doctor) if ! adguard_ipset_allowed; then PTXT "${INFO} IPSET status skipped: LAN/AP/Bridge mode does not manage IPTABLES/IPSET rules." return 0 fi ipset_status return "$?" ;; refresh) [ "${CLI_DRY_RUN}" = "1" ] && { cli_dry_run "IPSET refresh" return 0 } adguard_install_mode_detect || return 1 if ! adguard_install_mode_confirmed; then PTXT "${ERROR} IPSET refresh requires a confirmed router install mode; no service or IPSET state was changed." return 1 fi if ! adguard_ipset_allowed; then check_ipset 0 || true PTXT "${ERROR} IPSET refresh refused: LAN/AP/Bridge mode does not manage IPTABLES/IPSET rules." return 1 fi ;; *) return 2 ;; esac if [ "$(conf_value ADGUARD_IPSET)" = "NO" ]; then ptxt_ok "IPSET integration is disabled; no refresh needed." return 0 fi if [ "${CLI_YES}" = "1" ]; then OLD_PIDS="$(pidof AdGuardHome 2>/dev/null)" if adguard_service_without_nvram_lock_fd service restart_AdGuardHome >/dev/null 2>&1 && adguard_service_status_after_action restart "${OLD_PIDS}"; then return 0 fi PTXT "${WARNING} IPSET refresh requested, but AdGuardHome could not be restarted." return 1 else ptxt_ok "IPSET preference unchanged. Use --yes to restart AdGuardHome now." fi ;; netcheck) cli_netcheck_config_values "${netcheck_mode}" "${netcheck_hosts}" "${netcheck_dns}" "${netcheck_http}" "${netcheck_timeout}" return "$?" ;; dns-port-policy) cli_dns_port_policy --policy "${dns_policy}" return "$?" ;; migrate-runtime-defaults) # Intentional unquoted expansion: migrate_args is built from fixed option tokens only. cli_migrate_runtime_defaults ${migrate_args} return "$?" ;; performance) case "${profile}" in balanced) proc_profile="balanced" ;; low-memory) proc_profile="safe" ;; fast) proc_profile="aggressive" ;; *) PTXT "${ERROR} performance requires --profile {balanced|low-memory|fast}." return 1 ;; esac if ! write_conf ADGUARD_PROC_PROFILE "\"${proc_profile}\""; then return 1 fi ptxt_ok "Performance profile set to ${profile}." ;; install) [ "${CLI_DRY_RUN}" = "1" ] && { cli_dry_run "${INSTALLER_ACTION_INSTALL:-install}" return 0 } cli_require_yes || return 1 if [ "${ADGUARD_INSTALL_MODE}" = "wan" ] && [ "${CLI_ALLOW_DNS_NVRAM}" != "1" ]; then PTXT "${ERROR} Refusing install DNS/NVRAM rewrite without --allow-dns-nvram." return 1 fi cli_enable_assume_yes cleanup check_jffs_enabled || return 1 if [ "${ADGUARD_INSTALL_MODE}" = "wan" ]; then check_dns_environment 0 || return 1 fi ensure_jq_tool || return 1 [ -n "${adguard_branch}" ] || adguard_branch="$(conf_value ADGUARD_BRANCH)" [ -n "${adguard_branch}" ] || adguard_branch="release" cli_adguard_branch_is_valid "${adguard_branch}" || { PTXT "${ERROR} Unsupported AdGuardHome branch: ${adguard_branch}" return 1 } create_dir "${TARG_DIR}" || return 1 [ -z "${installer_branch}" ] || write_conf INSTALLER_BRANCH "\"${installer_branch}\"" || return 1 cli_write_adguard_branch "${adguard_branch}" || return 1 inst_AdGuardHome install operation_status=$? [ "${operation_status}" -eq 2 ] && return 1 return "${operation_status}" ;; esac } nvram_transaction_recovery_dependencies_available || exit 1 trap 'on_installer_exit' EXIT if ! adguard_committed_binary_cleanup_retry; then PTXT "${ERROR} Committed AdGuardHome binary-backup cleanup remains pending; aborting before new installer work." exit 1 fi if ! all_event_scripts_recover_startup; then PTXT "${ERROR} Unable to restore the retained event-hook recovery snapshot; aborting before new installer work." exit 1 fi if ! nvram_transaction_recover_startup; then if [ "${NVRAM_TRANSACTION_RECOVERY_DIAGNOSTIC_SAFE:-0}" = "1" ] && nvram_transaction_read_only_diagnostic_requested "$@"; then PTXT "${WARNING} Unable to remove a completed installer-managed NVRAM snapshot; continuing with read-only diagnostics." SUPPORT_EXPORT_RECOVERY_STATUS="pending" else PTXT "${ERROR} Unable to release the installer NVRAM transaction lock after recovery; review ${ROLLBACK_RESULT_FILE} and any preserved snapshot." exit 1 fi fi [ "${SUPPORT_EXPORT_RECOVERY_STATUS}" != "not-reported" ] || SUPPORT_EXPORT_RECOVERY_STATUS="checked" if [ "${1:-}" = "--no-auto-update" ]; then AI_NO_AUTO_UPDATE=1 export AI_NO_AUTO_UPDATE shift fi installer_update_policy_from_args "$@" # Keep diagnostic suffix flags on the same early, read-only dispatch path. case "${1:-}:${2:-}" in preflight:* | doctor:* | status:* | support-export:* | *:doctor | *:status) for DIAGNOSTIC_ARGUMENT; do shift [ "${DIAGNOSTIC_ARGUMENT}" = "--no-auto-update" ] || set -- "$@" "${DIAGNOSTIC_ARGUMENT}" done ;; *) : ;; esac if [ "${1:-}" = "support-export" ]; then shift support_export "$@" exit "$?" fi if [ "${1:-}" = "preflight" ]; then preflight "${2:-install}" exit "$?" fi if [ "${1:-}" = "doctor" ]; then case "${2:-}" in "" | "--fix") doctor "${2:-}" exit "$?" ;; esac fi if [ "${1:-}" = "status" ]; then status exit "$?" fi if [ "${2:-}" = "status" ]; then status "${1:-}" exit "$?" fi if [ "${2:-}" = "doctor" ]; then case "${3:-}" in "" | "--fix") doctor "${3:-}" exit "$?" ;; esac fi installer_requested_bundle_update_allowed "$@" || exit 1 case "${1:-}" in install | update | restore | uninstall | ipset) if cli_pre_remote_dry_run "$@"; then exit 0 fi ;; esac cli_pre_runtime_defaults_preview "$@" CLI_STATUS="$?" case "${CLI_STATUS}" in 0) exit 0 ;; 2) cli_usage exit 2 ;; esac if [ "${1:-}" = "${INSTALLER_ACTION_INSTALL:-install}" ] && [ "$#" -gt 1 ]; then cli_fresh_install_supported install || exit 1 fi resolve_installer_branch "$@" || exit 1 # Resolve the router mode before any installer preference is written. An # existing install may continue in its persisted mode, but all mode migration # remains blocked until a later confirmed detection. PREVIOUS_ADGUARD_INSTALL_MODE="$(conf_value ADGUARD_INSTALL_MODE 2>/dev/null)" adguard_install_mode_detect if ! adguard_install_mode_confirmed; then case "${PREVIOUS_ADGUARD_INSTALL_MODE}" in wan | lan) ADGUARD_INSTALL_MODE="${PREVIOUS_ADGUARD_INSTALL_MODE}" ;; *) if [ -f "${AGH_FILE}" ] || [ -d "${TARG_DIR}" ]; then ADGUARD_INSTALL_MODE="${PREVIOUS_ADGUARD_INSTALL_MODE}" else if ! startup_action_allows_unknown_install_mode "${1:-}" "${2:-}"; then PTXT "${ERROR} A new installation requires a confirmed WAN or LAN/AP/bridge router mode; no persistent state was modified." exit 1 fi fi ;; esac fi amtm_update_check "$@" installer_dependencies_available || exit 1 if [ -n "${BLOCKLIST_ANALYZER_SHA256}" ]; then BLOCKLIST_ANALYZER_SHA256="$(conf_value BLOCKLIST_ANALYZER_SHA256)" else del_conf BLOCKLIST_ANALYZER_SHA256 fi if ! conf_has_key BLOCKLIST_ANALYZER_SHA256; then write_conf BLOCKLIST_ANALYZER_SHA256 "\"${BLOCKLIST_ANALYZER_SHA256_DEFAULT}\"" BLOCKLIST_ANALYZER_SHA256="${BLOCKLIST_ANALYZER_SHA256_DEFAULT}" fi if [ -z "${2:-}" ] && single_arg_menu_action "${1:-}"; then CHOSEN="${1}" set -- "${BRANCH}" "${CHOSEN}" fi if [ "${1:-}" = "amtmupdate" ] && [ -z "${2:-}" ]; then set -- "${BRANCH}" update fi write_conf INSTALLER_BRANCH "\"${BRANCH}\"" readonly BLOCKLIST_ANALYZER_SHA256 [ -z "$(installer_nvram get odmpid)" ] && ROUTER_MODEL="$(installer_nvram get productid)" || ROUTER_MODEL="$(installer_nvram get odmpid)" RURL="https://raw.githubusercontent.com/jumpsmm7/Asuswrt-Merlin-AdGuardHome-Installer/${BRANCH}" ROUTER_OS="$(/bin/uname)" ! PTXT "${ROUTER_MODEL}" | grep -vE "RT-AC(86|2900)" | grep -q "AC" && ROUTER_ARCH="$(/bin/uname -m)" || ROUTER_ARCH="armv7" # Exception for version ARMv5 WAN_IPADDR="$(installer_nvram get wan_ipaddr 2>/dev/null)" if ipv4_is_private "${WAN_IPADDR}"; then NAT_ENV="${WAN_IPADDR}"; else NAT_ENV=""; fi [ -z "${ROUTER_MODEL}" ] && PTXT "${ERROR} This is an unsupported router, sorry." && sleep 3s && exit 1 if [ -z "$2" ]; then printf '\e[8;50;125t' printf '\033[?7l' clear_screen sed -n '2,21p' "$0" printf '\033[?7h' fi case "${ROUTER_MODEL}" in *) [ -z "$2" ] && PTXT "${INFO} Detected ${ROUTER_MODEL} router." ;; esac case "${ROUTER_OS}" in "Linux") [ -z "$2" ] && PTXT "${INFO} Detected ${ROUTER_OS} platform." ROUTER_OS="linux" ;; *) PTXT "${ERROR} This is an unsupported platform, sorry." exit 1 ;; esac case "${ROUTER_ARCH}" in "aarch64" | "arm64") ROUTER_ARCH="arm64" URL_ARCH="${RURL}/armv8" ADGUARD_ARCH="${ROUTER_OS}_${ROUTER_ARCH}" [ -z "$2" ] && PTXT "${INFO} Detected ARMv8 architecture." ;; "armv7l") ROUTER_ARCH="armv7" URL_ARCH="${RURL}/${ROUTER_ARCH}" ADGUARD_ARCH="${ROUTER_OS}_${ROUTER_ARCH}" [ -z "$2" ] && PTXT "${INFO} Detected ARMv7 architecture." ;; "armv7") ROUTER_ARCH="armv5" URL_ARCH="${RURL}/${ROUTER_ARCH}" ADGUARD_ARCH="${ROUTER_OS}_${ROUTER_ARCH}" [ -z "$2" ] && PTXT "${INFO} Detected ARMv7 architecture." ;; *) PTXT "${ERROR} This is an unsupported architecture, sorry." exit 1 ;; esac case "${1:-}" in install | update) init_remote_adguard_metadata || exit 1 ;; backup | restore | doctor | ipset | netcheck | dns-port-policy | performance | migrate-runtime-defaults | uninstall) ;; *) init_remote_adguard_metadata || exit 1 ;; esac case "${1:-}" in install | update | backup | restore | doctor | ipset | netcheck | dns-port-policy | performance | migrate-runtime-defaults | uninstall) if cli_action_requires_install_mode "${1:-}"; then adguard_install_mode_detect || exit 1 if ! adguard_install_mode_confirmed; then PTXT "${ERROR} ${1:-Install} requires a confirmed router install mode; persisted mode ${PREVIOUS_ADGUARD_INSTALL_MODE:-unset} was retained without migration." exit 1 fi fi cli_run "$@" CLI_STATUS="$?" if [ "${CLI_STATUS}" -ne 0 ]; then [ "${CLI_STATUS}" -eq 2 ] && cli_usage exit "${CLI_STATUS}" fi exit 0 ;; status) status "${BRANCH}" exit "$?" ;; esac case "$2" in "") adguard_install_mode_detect || { sleep 3s exit 1 } adguard_install_mode_confirmed || { PTXT "${ERROR} Interactive setup requires a confirmed router install mode; persisted mode ${PREVIOUS_ADGUARD_INSTALL_MODE:-unset} was retained without migration." sleep 3s exit 1 } if [ "${ADGUARD_INSTALL_MODE}" = "wan" ] && [ -n "${NAT_ENV}" ]; then PTXT "${WARNING} This router is in a Double-Nat Environment. While for some users of AdGuardHome this may work fine," \ "${WARNING} Consider this a warning that there is no guarentee that it will." \ "${WARNING} The script developer cannot help in this scenario." \ "${WARNING} However you are still welcome to use the script." sleep 3s fi cleanup check_jffs_enabled || exit 1 if [ "${ADGUARD_INSTALL_MODE}" = "wan" ]; then check_dns_environment 0 || exit 1 fi check_version menu ;; *) case "$2" in "2" | uninstall | doctor | status | [dD]) # Commands that can run without install mode detection ;; *) adguard_install_mode_detect || { sleep 3s exit 1 } adguard_install_mode_confirmed || { PTXT "${ERROR} This setup action requires a confirmed router install mode; persisted mode ${PREVIOUS_ADGUARD_INSTALL_MODE:-unset} was retained without migration." sleep 3s exit 1 } ;; esac if menu_action_allowed "$2"; then menu "$2" else PTXT "${INFO} Usage: sh installer {master|dev?|""} (1 - {install/update} | 2 - uninstall | 3 - changepw | 4 - reconfigure | 5 - setamtmupdate | 6 - setlocalcache | 7 - switchbranch | 8 - setipset | ${BLOCKLIST_ANALYZER_SHA256:+9 - unusedblocklists | }{b/B} - backup | {r/R} - restore | migrate-runtime-defaults | doctor | status)" \ "${INFO} Doctor can also be run as: sh installer doctor [--fix]. Status can also be run as: sh installer status." \ "${INFO} Branch or Tag must be represented by a value or empty quotes in place of string e.g. \"\" in the \$1 string position." \ "${INFO} Action must be represented by a value (1 - {install/update} | 2 - uninstall | 3 - changepw | 4 - reconfigure | 5 - setamtmupdate | 6 - setlocalcache | 7 - switchbranch | 8 - setipset | ${BLOCKLIST_ANALYZER_SHA256:+9 - unusedblocklists | }{b/B} - backup | {r/R} - restore | migrate-runtime-defaults | doctor | status) in the \$2 string position." \ "${INFO} An update example sh installer master update, or just use the regular menu by not specifying an Action." fi ;; esac