#!/bin/sh ################################################################################################################# # █████╗ ███████╗██╗ ██╗███████╗██╗ ██╗██████╗ ████████╗ ███╗ ███╗███████╗██████╗ ██╗ ██╗███╗ ██╗# #██╔══██╗██╔════╝██║ ██║██╔════╝██║ ██║██╔══██╗╚══██╔══╝ ████╗ ████║██╔════╝██╔══██╗██║ ██║████╗ ██║# #███████║███████╗██║ ██║███████╗██║ █╗ ██║██████╔╝ ██║█████╗██╔████╔██║█████╗ ██████╔╝██║ ██║██╔██╗ ██║# #██╔══██║╚════██║██║ ██║╚════██║██║███╗██║██╔══██╗ ██║╚════╝██║╚██╔╝██║██╔══╝ ██╔══██╗██║ ██║██║╚██╗██║# #██║ ██║███████║╚██████╔╝███████║╚███╔███╔╝██║ ██║ ██║ ██║ ╚═╝ ██║███████╗██║ ██║███████╗██║██║ ╚████║# #╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝╚═╝╚═╝ ╚═══╝# # █████╗ ██████╗ ██████╗ ██╗ ██╗ █████╗ ██████╗ ██████╗ ██╗ ██╗ ██████╗ ███╗ ███╗███████╗ # #██╔══██╗██╔══██╗██╔════╝ ██║ ██║██╔══██╗██╔══██╗██╔══██╗██║ ██║██╔═══██╗████╗ ████║██╔════╝ # #███████║██║ ██║██║ ███╗██║ ██║███████║██████╔╝██║ ██║███████║██║ ██║██╔████╔██║█████╗ # #██╔══██║██║ ██║██║ ██║██║ ██║██╔══██║██╔══██╗██║ ██║██╔══██║██║ ██║██║╚██╔╝██║██╔══╝ # #██║ ██║██████╔╝╚██████╔╝╚██████╔╝██║ ██║██║ ██║██████╔╝██║ ██║╚██████╔╝██║ ╚═╝ ██║███████╗ # #╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ # #██╗███╗ ██╗███████╗████████╗ █████╗ ██╗ ██╗ ███████╗██████╗ Author: # #██║████╗ ██║██╔════╝╚══██╔══╝██╔══██╗██║ ██║ ██╔════╝██╔══██╗ SomeWhereOverTheRainBow # #██║██╔██╗ ██║███████╗ ██║ ███████║██║ ██║ █████╗ ██████╔╝ Contributors: # #██║██║╚██╗██║╚════██║ ██║ ██╔══██║██║ ██║ ██╔══╝ ██╔══██╗ JackYaz,gspannu,thelonelycoder # #██║██║ ╚████║███████║ ██║ ██║ ██║███████╗███████╗███████╗██║ ██║ v2.6.0 # #╚═╝╚═╝ ╚═══╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝ # ################################################################################################################# export LC_ALL=C export PATH="/sbin:/bin:/usr/sbin:/usr/bin${PATH:+:$PATH}" AI_VERSION="v2.6.0" export AI_VERSION readonly ADGUARD_METADATA_FILE="/tmp/AGH_STATIC_METADATA_$$.txt" readonly ADGUARDHOME_WAIT_TIMEOUT="60" readonly ADGUARDHOME_READY_TIMEOUT="180" 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/graysky2/8035291d1bf87b8fe3693668965337e1/raw/a4be7655095d6ff880c2f3748964b825d7c45bd2/blocklilst_analyzer.py" readonly BLOCKLIST_ANALYZER_SHA256_DEFAULT="77c21b6faeb7fea86b0729996dec5cc20d2d309fe8c6946c7371dd64134a6e5f" BLOCKLIST_ANALYZER_SHA256="" SCRIPT_LOC="" SETUP_YAML_TMP_FILE="" BLOCKLIST_YAML_TMP_FILE="" 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() { which "$1" >/dev/null 2>&1 } 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() { rm -f "${ADGUARD_METADATA_FILE}" } clear_screen() { ai_have_cmd clear && clear return 0 } ensure_bcrypt_tool() { if bcrypt_tool_available; then return 0 fi case "$(/bin/uname -m)" in "aarch64" | "arm64") ensure_opkg_package go || return 1 ;; "armv7l" | *) if opkg_pkg_available go_nohf; then ensure_opkg_package go_nohf || return 1 else ensure_opkg_package go || return 1 fi ;; esac export GOROOT="/opt/bin/go" export GOBIN="/opt/bin/" GO_CACHE_DIR="/tmp/AdGuardHome-go-build-cache" GO_PATH_DIR="/tmp/AdGuardHome-go-path" [ -x /opt/bin/go/bin/go ] || 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" case "${_pkg}" in "" | *[!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.+-]*) return 1 ;; esac if opkg_pkg_installed "${_pkg}"; then return 0 fi if ! opkg_clean_env install "${_pkg}" >/dev/null 2>&1; then return 1 fi opkg_pkg_installed "${_pkg}" } sha256sum_available() { if ai_have_cmd sha256sum && sha256sum /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 } 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" ptxt_step "Installing Entware coreutils-sha256sum package." if ! ensure_opkg_package coreutils-sha256sum; 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() { ensure_opkg_package python3 || true ensure_opkg_package python3-bcrypt || 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() { case "$1:$2" in setup:"${YAML_ORI}.new.$$" | blocklist:"${YAML_FILE}.blocklists.$$.tmp") rm -f "$2" 2>/dev/null ;; esac } on_installer_exit() { cleanup_api_files installer_cleanup_tmp_file setup "${SETUP_YAML_TMP_FILE:-}" installer_cleanup_tmp_file blocklist "${BLOCKLIST_YAML_TMP_FILE:-}" # Restore pre-check DNS settings only when exiting without a local adguardhome installation. if [ ! -f "${AGH_FILE}" ]; then check_dns_environment 1 fi } 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 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}" } curl_insecure_arg() { if [ -z "${CURL_INSECURE_ARG_SET:-}" ]; then CURL_INSECURE_ARG="" curl_has_option '--insecure' && CURL_INSECURE_ARG=" -k" CURL_INSECURE_ARG_SET="1" fi PTXT -n "${CURL_INSECURE_ARG}" } http_get_file() { local URL OUT HEADERS INSECURE CURL_ARGS WGET_ARGS URL="$1" OUT="$2" HEADERS="${3:-}" INSECURE="${4:-}" if ai_have_cmd curl; then CURL_ARGS="$(curl_common_args)" [ "${INSECURE}" = "insecure" ] && CURL_ARGS="${CURL_ARGS}$(curl_insecure_arg)" if [ -n "${HEADERS}" ]; then curl ${CURL_ARGS} -D "${HEADERS}" -f -sL "${URL}" -o "${OUT}" else curl ${CURL_ARGS} -f -sL "${URL}" -o "${OUT}" fi elif ai_have_cmd wget; then WGET_ARGS="$(wget_common_args)" [ "${INSECURE}" = "insecure" ] && WGET_ARGS="${WGET_ARGS}$(wget_insecure_arg)" if [ -n "${HEADERS}" ] && wget_has_option '--server-response'; then wget ${WGET_ARGS} --server-response -q -O "${OUT}" "${URL}" 2>"${HEADERS}" else [ -n "${HEADERS}" ] && : >"${HEADERS}" wget ${WGET_ARGS} -q -O "${OUT}" "${URL}" fi else return 127 fi } http_get_stdout() { local URL INSECURE CURL_ARGS WGET_ARGS URL="$1" INSECURE="${2:-}" if ai_have_cmd curl; then CURL_ARGS="$(curl_common_args)" [ "${INSECURE}" = "insecure" ] && CURL_ARGS="${CURL_ARGS}$(curl_insecure_arg)" curl ${CURL_ARGS} -f -sL "${URL}" elif ai_have_cmd wget; then WGET_ARGS="$(wget_common_args)" [ "${INSECURE}" = "insecure" ] && WGET_ARGS="${WGET_ARGS}$(wget_insecure_arg)" 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}" } wget_insecure_arg() { if [ -z "${WGET_INSECURE_ARG_SET:-}" ]; then WGET_INSECURE_ARG="" wget_has_option '--no-check-certificate' && WGET_INSECURE_ARG=" --no-check-certificate" WGET_INSECURE_ARG_SET="1" fi PTXT -n "${WGET_INSECURE_ARG}" } # 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}" } write_conf_if_absent() { conf_has_key "$1" && return 0 write_conf "$1" "$2" } configure_runtime_defaults() { local NETCHECK_MODE case "${1:-}" in new-install) case "${2:-}" in 1 | YES | yes | Yes) NETCHECK_MODE="wan" ;; *) NETCHECK_MODE="lan" ;; esac write_conf_if_absent ADGUARDHOME_REFUSE_UNKNOWN_DNS_PORT_KILL "\"1\"" || 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 "\"balanced\"" || return 1 ptxt_ok "Runtime defaults saved: DNS port refusal enabled, netcheck mode ${NETCHECK_MODE}, performance profile balanced." ;; upgrade) write_conf_if_absent ADGUARDHOME_REFUSE_UNKNOWN_DNS_PORT_KILL "\"0\"" || return 1 write_conf_if_absent ADGUARD_NETCHECK_MODE "\"legacy\"" || 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}" | awk '{printf "%s", $1}')" || SHA256_OUTPUT="" fi if [ -z "${SHA256_OUTPUT}" ] && [ -x /opt/bin/sha256sum ]; then SHA256_OUTPUT="$(/opt/bin/sha256sum "${FILE}" | awk '{printf "%s", $1}')" || SHA256_OUTPUT="" fi if [ -z "${SHA256_OUTPUT}" ] && [ -x /bin/busybox ]; then SHA256_OUTPUT="$(/bin/busybox sha256sum "${FILE}" | awk '{printf "%s", $1}')" || SHA256_OUTPUT="" fi if [ -z "${SHA256_OUTPUT}" ] && ai_have_cmd busybox; then SHA256_OUTPUT="$(busybox sha256sum "${FILE}" | awk '{printf "%s", $1}')" || SHA256_OUTPUT="" fi if [ -z "${SHA256_OUTPUT}" ] && ai_have_cmd openssl; then SHA256_OUTPUT="$(openssl dgst -sha256 "${FILE}" | awk '{printf "%s", $2}')" || 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 } 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_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) } ' } 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 } preflight_action_requires_entware() { case "${1:-install}" in status | preflight) return 1 ;; *) return 0 ;; esac } preflight_action_requires_jq() { case "${1:-install}" in *) return 1 ;; esac } 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 | 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 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 } preflight_action_requires_firewall_tools() { case "${1:-install}" in status | preflight) return 1 ;; esac return 0 } preflight_check_jffs_ready() { local jffs2_format result jffs2_format="$(nvram get jffs2_format 2>/dev/null)" || jffs2_format="" result="OK" PTXT "preflight.jffs.required=yes" PTXT "preflight.jffs.format_pending=${jffs2_format:-missing}" if [ "${jffs2_format}" = "1" ]; then result="FAIL" fi PTXT "preflight.jffs.result=${result}" [ "${result}" = "OK" ] } preflight_check_router_eligibility() { local arch model odmpid os result sw_mode result="OK" PTXT "preflight.router.required=yes" if [ -z "${ROUTER_MODEL:-}" ]; then odmpid="$(nvram get odmpid 2>/dev/null)" || odmpid="" [ -z "${odmpid}" ] && model="$(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 sw_mode="$(nvram get sw_mode 2>/dev/null)" || sw_mode="" PTXT "preflight.router.model=${model:-missing}" PTXT "preflight.router.sw_mode=${sw_mode:-missing}" if [ -z "${model}" ]; then PTXT "preflight.router.model.result=FAIL" result="FAIL" else PTXT "preflight.router.model.result=OK" fi if [ "${sw_mode}" = "1" ]; then PTXT "preflight.router.mode.result=OK" else PTXT "preflight.router.mode.result=FAIL" result="FAIL" fi 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_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=$([ -x /usr/sbin/curl ] && PTXT OK || PTXT missing)" PTXT "preflight.downloader.wget.which=${wget_path:-missing}" PTXT "preflight.downloader.wget.stock=$([ -x /usr/sbin/wget ] && PTXT OK || PTXT missing)" 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=$([ -d /opt ] && PTXT OK || PTXT missing)" PTXT "preflight.entware.opkg=$([ -x /opt/bin/opkg ] && PTXT OK || PTXT missing)" PTXT "preflight.entware.config=$({ [ -f /opt/etc/opkg.conf ] || [ -f /opt/etc/init.d/rc.unslung ]; } && PTXT OK || PTXT missing)" PTXT "preflight.entware.result=${result}" [ "${result}" = "OK" ] } preflight_check_entware_package() { local pkg result pkg="$1" 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=opkg install ${pkg}" [ "${result}" = "OK" ] } preflight_check_jq() { local allow_entware jq_path result allow_entware="$1" jq_path="$(which jq 2>/dev/null)" || jq_path="" result="FAIL" if [ -x /usr/bin/jq ]; then result="OK" fi PTXT "preflight.jq.required=yes" PTXT "preflight.jq.which=${jq_path:-missing}" PTXT "preflight.jq.stock=$([ -x /usr/bin/jq ] && PTXT OK || PTXT missing)" if [ "${allow_entware}" = "1" ]; then [ "${result}" = "OK" ] || [ ! -x /opt/bin/jq ] || result="OK" PTXT "preflight.jq.entware=$([ -x /opt/bin/jq ] && PTXT OK || PTXT missing)" PTXT "preflight.jq.install_hint=opkg install jq" 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" 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" preflight_check_entware_package coreutils-sha256sum || true 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}" preflight_check_path "sh" "/bin/sh" || failed="1" preflight_check_path "nvram" "/bin/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 [ "${entware_required}" = "1" ] && [ "${entware_available_now}" = "1" ]; then if preflight_action_requires_sha256 "${action}"; then preflight_check_sha256_support || failed="1" else PTXT "preflight.sha256.required=no" PTXT "preflight.sha256.result=SKIP" fi 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 "${WARNING} Ignoring unsafe installer branch ${CANDIDATE}, using master instead." >&2 PTXT "master" 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 } adguard_service_status_after_action() { local action elapsed maxwait old_pids status_message transition_message action="${1:-restart}" old_pids="${2:-}" elapsed="0" maxwait="${ADGUARDHOME_WAIT_TIMEOUT}" [ "${maxwait}" -le "5" ] || maxwait="5" case "${action}" in stop | kill) status_message="Waiting for AdGuardHome to stop cleanly..." transition_message="Stopping..." ;; start) status_message="Waiting for AdGuardHome to report running state after start..." transition_message="Restarting..." ;; restart | *) status_message="Waiting for AdGuardHome to report running state after restart..." 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_delete "- name:" "${YAML_FILE}" yaml_nvars_delete " password:" "${YAML_FILE}" yaml_nvars_insert "users:" "\ password: ${PW1_ENCRYPTED}" "${YAML_FILE}" yaml_nvars_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." agh_stop ADGUARDHOME_START_FAILURE_LOGGED= if ! agh_start; then [ "${ADGUARDHOME_START_FAILURE_LOGGED:-}" = "1" ] || agh_start_error return 1 fi ptxt_step "Waiting for AdGuardHome readiness check to complete." ADGUARDHOME_START_FAILURE_LOGGED= if ! agh_restart; 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_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() { local operation script_path expected_line script_path="$1" operation="$2" expected_line="[ -x ${ADDON_DIR}/AdGuardHome.sh ] && ${ADDON_DIR}/AdGuardHome.sh ${operation}" 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 grep -q -F "${expected_line}" "${script_path}" 2>/dev/null; then if [ ! -x "${script_path}" ]; then doctor_status WARN "${script_path} managed hook present but not executable" "Run: sh installer doctor --fix" return 1 fi doctor_status OK "${script_path} managed hook present and executable" return 0 fi doctor_status WARN "${script_path} managed hook missing expected AdGuardHome call" "Re-run install or add the managed AdGuardHome hook line." return 1 } 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() { [ -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 "/jffs/scripts/init-start" ] && chmod 755 "/jffs/scripts/init-start" 2>/dev/null && doctor_fix_msg "/jffs/scripts/init-start executable bit" [ -f "/jffs/scripts/services-stop" ] && chmod 755 "/jffs/scripts/services-stop" 2>/dev/null && doctor_fix_msg "/jffs/scripts/services-stop executable bit" [ -f "/jffs/scripts/dnsmasq.postconf" ] && chmod 755 "/jffs/scripts/dnsmasq.postconf" 2>/dev/null && doctor_fix_msg "/jffs/scripts/dnsmasq.postconf executable bit" [ -f "/jffs/scripts/firewall-start" ] && chmod 755 "/jffs/scripts/firewall-start" 2>/dev/null && doctor_fix_msg "/jffs/scripts/firewall-start 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() { local lock_file pid_file status lock_file="/tmp/AdGuardHome.lock" pid_file="/tmp/AdGuardHome/pid" if [ -f "${pid_file}" ] && doctor_pid_file_is_active "${pid_file}"; then return 0 fi if ai_have_cmd flock && [ -e "${lock_file}" ]; then ( 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() { 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 for stale in /tmp/AdGuardHome.dnsmasq.handoff /tmp/AdGuardHome.dnsmasq.lock /tmp/AdGuardHome.dns-handoff/active /tmp/AdGuardHome.dns-handoff/lock /tmp/AdGuardHome.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.lock | /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="$(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() { 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 doctor_managed_script_state "/jffs/scripts/init-start" "init-start &" || true doctor_managed_script_state "/jffs/scripts/services-stop" "services-stop &" || true doctor_managed_script_state "/jffs/scripts/dnsmasq.postconf" "dnsmasq" || true doctor_managed_script_state "/jffs/scripts/firewall-start" 'firewall "$1"' || true 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.lock /tmp/AdGuardHome/pid; do [ -e "${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 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 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}" ]; 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="$(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")" } agh_restart() { local old_pids if agh_is_running; then ptxt_step "Restarting AdGuardHome service." old_pids="$(pidof AdGuardHome 2>/dev/null)" if service restart_AdGuardHome >/dev/null 2>&1; then adguard_service_status_after_action restart "${old_pids}" || true elif /opt/etc/init.d/S99AdGuardHome restart; then adguard_service_status_after_action restart "${old_pids}" || true else 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 service start_AdGuardHome >/dev/null 2>&1; then adguard_service_status_after_action start || true elif /opt/etc/init.d/S99AdGuardHome start; then adguard_service_status_after_action start || true else 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() { if [ "$(agh_process_count)" -ge "1" ]; then ptxt_step "Stopping AdGuardHome service." if service stop_AdGuardHome >/dev/null 2>&1; then adguard_service_status_after_action stop || true elif /opt/etc/init.d/S99AdGuardHome stop; then adguard_service_status_after_action stop || true elif /opt/etc/init.d/S99AdGuardHome kill; then adguard_service_status_after_action kill || true else if ai_have_cmd killall; then killall -q -9 AdGuardHome 2>/dev/null else PTXT "${ERROR} Unable to force-stop AdGuardHome because killall is unavailable." return 1 fi rm -rf /opt/var/run/AdGuardHome.pid 2>/dev/null adguard_service_status_after_action kill || true fi 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)" != "${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 ! 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 chmod 700 "${BLOCKLIST_ANALYZER_FILE}" 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() { local ids_file candidates_file selected_file id name url list_label choice_status ids_file="$1" candidates_file="${TARG_DIR}/blocklist_analyzer.$$.candidates" selected_file="${TARG_DIR}/blocklist_analyzer.$$.selected" blocklist_yaml_candidates "${ids_file}" >"${candidates_file}" if [ ! -s "${candidates_file}" ]; then rm -f "${candidates_file}" "${selected_file}" ptxt_warn "No matching unused IDs were found under the AdGuardHome filters section." return 1 fi PTXT "${INFO} Matching 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 awk -F'|' '{ print $1 }' "${candidates_file}" >"${selected_file}" rm -f "${candidates_file}" BLOCKLIST_ANALYZER_SELECTED_IDS_FILE="${selected_file}" return 0 else choice_status="$?" fi if [ "${choice_status}" -ne 1 ]; then rm -f "${candidates_file}" "${selected_file}" return "${choice_status}" fi : >"${selected_file}" || { rm -f "${candidates_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}" "${selected_file}" return "${choice_status}" ;; esac done <"${candidates_file}" rm -f "${candidates_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 "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 "blocklist yaml replacement" "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 '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 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 "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 "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 "blocklist yaml replacement" "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 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() { install_blocklist_analyzer || { end_op_message 1 return 1 } run_blocklist_analyzer case "$?" in 0) ;; 2) end_op_message 0 return 0 ;; *) end_op_message 1 return 1 ;; esac if ! select_unused_blocklists_for_removal "${BLOCKLIST_ANALYZER_IDS_FILE}"; then rm -f "${BLOCKLIST_ANALYZER_IDS_FILE:-}" "${BLOCKLIST_ANALYZER_OUTPUT_FILE:-}" "${BLOCKLIST_ANALYZER_SELECTED_IDS_FILE:-}" ptxt_warn "Blocklist cleanup cancelled or nothing was selected." end_op_message 1 return 1 fi 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:-}" end_op_message 0 else rm -f "${BLOCKLIST_ANALYZER_IDS_FILE:-}" "${BLOCKLIST_ANALYZER_OUTPUT_FILE:-}" "${BLOCKLIST_ANALYZER_SELECTED_IDS_FILE:-}" 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() { ptxt_phase "Preparing DNS environment." if [ -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 local MODE NVCHECK dns_env_set_nvram() { local key expected cur changed key="$1" expected="$2" cur="$(nvram get "${key}" 2>/dev/null)" if [ "${cur}" = "${expected}" ]; then return 1 fi nvram set "${key}=${expected}" changed="1" return 0 } dns_env_apply_profile() { local changed changed="0" if dns_env_set_nvram "dnspriv_enable" "0"; then changed="$((changed + 1))"; fi if dns_env_set_nvram "dhcpd_dns_router" "1"; then changed="$((changed + 1))"; fi if dns_env_set_nvram "dhcp_dns1_x" ""; then changed="$((changed + 1))"; fi if dns_env_set_nvram "dhcp_dns2_x" ""; then changed="$((changed + 1))"; fi if [ "${changed}" != "0" ]; then return 0; else return 1; fi } dns_env_restore_profile() { local changed key cur old changed="0" for key in dnspriv_enable dhcpd_dns_router dhcp_dns1_x dhcp_dns2_x; do cur="$(nvram get "${key}" 2>/dev/null)" case "${key}" in dnspriv_enable) old="${_OLD_dnspriv_enable}" ;; dhcpd_dns_router) old="${_OLD_dhcpd_dns_router}" ;; dhcp_dns1_x) old="${_OLD_dhcp_dns1_x}" ;; dhcp_dns2_x) old="${_OLD_dhcp_dns2_x}" ;; esac if [ "${cur}" != "${old}" ]; then nvram set "${key}=${old}" changed="$((changed + 1))" fi done if [ "${changed}" != "0" ]; then return 0; else return 1; fi } MODE="$1" NVCHECK="0" case "${MODE}" in 0) # Save original values only once. if [ "${_DNS_NVRAM_SAVED:-0}" != "1" ]; then ptxt_step "Saving current DNS NVRAM settings." save_dns_nvram_environment ptxt_ok "Current DNS NVRAM settings saved." fi if [ "$(pidof stubby)" ]; then ptxt_step "Stopping conflicting stubby process." { kill_processes stubby; } ptxt_ok "Conflicting stubby process stopped." NVCHECK="$((NVCHECK + 1))" fi ptxt_step "Applying installer DNS preparation profile." if dns_env_apply_profile; then ptxt_ok "Installer DNS preparation profile applied." NVCHECK="$((NVCHECK + 1))" else ptxt_ok "Installer DNS preparation profile was already active." fi ;; 1) # Do not restore if we never saved anything. if [ "${_DNS_NVRAM_SAVED:-0}" != "1" ]; then ptxt_ok "No saved DNS NVRAM settings to restore." return 0 fi ptxt_step "Restoring saved DNS NVRAM settings." if dns_env_restore_profile; then ptxt_ok "Saved DNS NVRAM settings restored." NVCHECK="$((NVCHECK + 1))" else ptxt_ok "Saved DNS NVRAM settings were already restored." fi ;; *) return 1 ;; esac if [ "${NVCHECK}" != "0" ]; then ptxt_step "Committing DNS NVRAM changes and restarting dnsmasq." { nvram commit; } { service restart_dnsmasq >/dev/null 2>&1; } (while { ! check_connection; }; do sleep 1s; done) & local PID="$!" wait "${PID}" 2>/dev/null ptxt_ok "DNS services refreshed after DNS environment changes." fi ptxt_ok "DNS Environment is Ready." return 0 } check_dns_filter() { local NVCHECK USE_SOME YESNO_STATUS NVCHECK="0" if [ "$1" -eq 0 ]; then if [ "$(nvram get dnsfilter_enable_x)" != "0" ]; then { nvram set dnsfilter_enable_x="0"; } NVCHECK="$((NVCHECK + 1))" fi PTXT "${INFO} DNS will not be forced through to AdGuardHome." fi if [ "$1" -eq 1 ]; then 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 if [ "$(nvram get dnsfilter_enable_x)" != "1" ]; then { nvram set dnsfilter_enable_x="1"; } NVCHECK="$((NVCHECK + 1))" fi if [ "$USE_SOME" -eq 0 ]; then if [ "$(nvram get dnsfilter_mode)" != "11" ]; then { nvram set dnsfilter_mode="11"; } NVCHECK="$((NVCHECK + 1))" fi PTXT "${INFO} DNSFilter is set to control DNS through to AdGuardHome, while leaving any Custom Rules and Values." fi if [ "$USE_SOME" -eq 1 ]; then for i in 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; do VAR="$(nvram get "${i}" 2>/dev/null)" if [ "${i}" = "dnsfilter_mode" ]; then if [ "$VAR" != "11" ]; then { nvram set "${i}"="11" }; else continue; fi elif [ "${i}" = "dhcpd_dns_router" ]; then if [ "$VAR" != "1" ]; then { nvram set "${i}"="1" }; else continue; fi elif [ "$VAR" ]; then { nvram set "${i}"="" }; else continue; fi NVCHECK="$((NVCHECK + 1))" done PTXT "${INFO} DNS is set to redirect All DNS resolutions through to AdGuardHome." fi fi if [ "${NVCHECK}" != "0" ]; then { nvram commit; } { service "restart_firewall;restart_dnsmasq" >/dev/null 2>&1; } if ! check_connection 300; then PTXT "${WARNING} Timed out waiting for DNS connectivity." fi fi } save_dns_filter_settings() { local DNS_FILTER_BACKUP_DIR="$1" DNS_FILTER_KEY mkdir -p "${DNS_FILTER_BACKUP_DIR}" || return 1 for DNS_FILTER_KEY in 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; do nvram get "${DNS_FILTER_KEY}" >"${DNS_FILTER_BACKUP_DIR}/${DNS_FILTER_KEY}" 2>/dev/null || return 1 done } restore_dns_filter_settings() { local DNS_FILTER_BACKUP_DIR="$1" DNS_FILTER_KEY DNS_FILTER_VALUE [ -d "${DNS_FILTER_BACKUP_DIR}" ] || return 0 for DNS_FILTER_KEY in 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; do [ -f "${DNS_FILTER_BACKUP_DIR}/${DNS_FILTER_KEY}" ] || return 1 DNS_FILTER_VALUE="$(cat "${DNS_FILTER_BACKUP_DIR}/${DNS_FILTER_KEY}")" || return 1 nvram set "${DNS_FILTER_KEY}=${DNS_FILTER_VALUE}" || return 1 done nvram commit || return 1 service "restart_firewall;restart_dnsmasq" >/dev/null 2>&1 || return 1 rm -rf "${DNS_FILTER_BACKUP_DIR}" } check_ipset() { local IPSET_ENABLED case "$1" in 0) IPSET_ENABLED="NO" ;; 1) IPSET_ENABLED="YES" ;; esac write_conf ADGUARD_IPSET "\"${IPSET_ENABLED}\"" } 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() { if [ "$(nvram get jffs2_format)" = "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 local JFFS2_SCRIPTS JFFS2_ENABLED jffs2_on JFFS2_SCRIPTS="$(nvram get jffs2_scripts)" [ -z "$(nvram get jffs2_enable)" ] && JFFS2_ENABLED="$(nvram get jffs2_on)" || JFFS2_ENABLED="$(nvram get jffs2_enable)" [ -z "$(nvram get jffs2_enable)" ] && jffs2_on="jffs2_on" || jffs2_on="jffs2_enable" if [ "${JFFS2_ENABLED}" != "1" ] || [ "${JFFS2_SCRIPTS}" != "1" ]; then PTXT "${INFO} JFFS custom scripts and configs are not enabled." \ "${INFO} Enabling them now!" nvram set ${jffs2_on}="1" nvram set jffs2_scripts="1" nvram commit else PTXT "${INFO} JFFS custom scripts and configs are already enabled." fi } check_version() { local RMNSTALL LINSTALL MD5SUM_L MD5SUM_R MD5SUM_EXPECTED REMOTE_INSTALLER REMOTE_INSTALLER_MD5 ADGUARD_BRANCH ADGUARD_VER REMOTE_ADGUARD_VER varcnt 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="$(awk -F= '/^AI_VERSION=/ { gsub(/"/, "", $2); sub(/^v/, "", $2); if ($2 ~ /^[0-9]{1,2}(\.[0-9]{1,2}){2}$/) print $2; exit }' "${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}" if http_get_file "${RURL}/installer" "${REMOTE_INSTALLER}" && [ -s "${REMOTE_INSTALLER}" ]; then RMNSTALL="$(awk -F= '/^AI_VERSION=/ { gsub(/"/, "", $2); sub(/^v/, "", $2); if ($2 ~ /^[0-9]{1,2}(\.[0-9]{1,2}){2}$/) print $2; exit }' "${REMOTE_INSTALLER}")" MD5SUM_R="$(file_md5 "${REMOTE_INSTALLER}" 2>/dev/null)" MD5SUM_EXPECTED="" if http_get_file "${RURL}/installer.md5sum" "${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="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="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="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 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 http://$(nvram get lan_ipaddr):$(awk -F: '/.*address\:.*/{ print $3 }' "${YAML_FILE}")." \ "${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 } save_dns_nvram_environment() { local VAR VALUE for VAR in dnspriv_enable dhcpd_dns_router dhcp_dns1_x dhcp_dns2_x; do VALUE="$(nvram get "${VAR}" 2>/dev/null)" case "${VAR}" in dnspriv_enable) _OLD_dnspriv_enable="${VALUE}" ;; dhcpd_dns_router) _OLD_dhcpd_dns_router="${VALUE}" ;; dhcp_dns1_x) _OLD_dhcp_dns1_x="${VALUE}" ;; dhcp_dns2_x) _OLD_dhcp_dns2_x="${VALUE}" ;; esac done export _OLD_dnspriv_enable _OLD_dhcpd_dns_router _OLD_dhcp_dns1_x _OLD_dhcp_dns2_x _DNS_NVRAM_SAVED="1" export _DNS_NVRAM_SAVED } # 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") { 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/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 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/data") { data = 1 } } END { if (unsafe) { exit 1 } if (!binary) { exit 1 } if (require_state && (!yaml || !data)) { exit 1 } exit 0 } ' } 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" "rollback complete" "previous AdGuardHome service restarted" 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" "rollback partial" "previous AdGuardHome binary restored but service restart failed" return 1 } adguard_restart_after_install_abort() { adguard_install_abort_trap_disable_preserve_defer [ "$1" = "1" ] || return 0 if agh_is_running; then return 0 fi if agh_start; then PTXT "${INFO} Restarted AdGuardHome after the installer aborted." rollback_result_write "install-abort" "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" "rollback partial" "AdGuardHome could not be restarted after abort" return 1 } adguard_install_abort_trap_disable() { trap - HUP INT QUIT ABRT TERM ADGUARD_INSTALL_REPLACE_ACTIVE="0" ADGUARD_INSTALL_OLD_BINARY="" ADGUARD_INSTALL_STAGE_DIR="" ADGUARD_INSTALL_WAS_RUNNING="0" ADGUARD_RESTORE_ACTIVE="0" ADGUARD_RESTORE_ROLLBACK_DIR="" ADGUARD_RESTORE_STAGE_DIR="" ADGUARD_RESTORE_TARG_DIR="" ADGUARD_RESTORE_TARGET_INSTALLED="0" } 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() { local OLD_BINARY REPLACE_ACTIVE RESTORE_ACTIVE RESTORE_ROLLBACK_DIR RESTORE_STAGE_DIR RESTORE_TARGET_INSTALLED RESTORE_TARG_DIR STAGE_DIR 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}" adguard_install_abort_trap_disable [ -z "${STAGE_DIR}" ] || rm -rf "${STAGE_DIR}" 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() { ADGUARD_INSTALL_REPLACE_ACTIVE="0" ADGUARD_INSTALL_OLD_BINARY="" ADGUARD_INSTALL_WAS_RUNNING="$1" ADGUARD_INSTALL_STAGE_DIR="${2:-}" ADGUARD_RESTORE_ACTIVE="0" trap 'adguard_install_abort_on_signal' HUP INT QUIT ABRT TERM } adguard_restore_abort_trap_enable() { ADGUARD_INSTALL_REPLACE_ACTIVE="0" ADGUARD_INSTALL_OLD_BINARY="" ADGUARD_INSTALL_STAGE_DIR="" 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() { local OLD_BINARY WAS_RUNNING OLD_BINARY="$1" WAS_RUNNING="$2" if [ -f "${OLD_BINARY}" ]; then if ! mv "${OLD_BINARY}" "${AGH_FILE}"; then rollback_result_write "AdGuardHome binary rollback" "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" "rollback failed" "previous binary remains at ${OLD_BINARY}" return 1 fi rollback_result_write "AdGuardHome binary rollback" "restored" "${AGH_FILE}" else if ! rm -f "${AGH_FILE}"; then rollback_result_write "AdGuardHome binary rollback" "cleanup-failed" "${AGH_FILE}" rollback_result_notice PTXT "${ERROR} Unable to remove the failed AdGuardHome binary." rollback_result_write "binary-replace" "rollback failed" "failed binary could not be removed" return 1 fi if [ "${WAS_RUNNING}" = "1" ]; then rollback_result_write "AdGuardHome binary rollback" "restore-failed" "previous binary unavailable" rollback_result_notice PTXT "${ERROR} The previous AdGuardHome binary is unavailable for rollback." rollback_result_write "binary-replace" "rollback unavailable" "previous binary backup is missing" return 1 fi rollback_result_write "AdGuardHome binary rollback" "removed-failed-install" "${AGH_FILE}" fi rollback_result_write "binary-replace" "rollback complete" "previous AdGuardHome binary restored" adguard_restart_after_failed_replace "${WAS_RUNNING}" } 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 "AdGuardHome directory rollback" "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" "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 "AdGuardHome directory rollback" "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" "rollback failed" "previous installation remains at ${ROLLBACK_DIR}" return 1 fi rollback_result_write "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 "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" "rollback failed" "could not remove failed symlink ${COMMAND_LINK}" return 1 fi rollback_result_write "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() { [ -f "${YAML_FILE}" ] || 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 ~ /^[>|][+-]?([[: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_FILE}" } 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 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() { 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" "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" "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" "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." ADGUARD_INSTALL_REPLACE_ACTIVE="0" ADGUARD_INSTALL_OLD_BINARY="" rm -f "${OLD_BINARY}" rm -rf "${STAGE_DIR}" return 0 } backup_restore() { local COMMAND_LINK COMMAND_LINK_DIR RESTORE_ROLLBACK_DIR RESTORE_STAGE_DIR RESTORE_TARG_DIR WAS_RUNNING 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" ] && [ "$1" = "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 "backup restore" "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 "backup restore" "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" ]; then adguard_install_abort_trap_disable rm -rf "${RESTORE_STAGE_DIR}" rollback_result_write "backup restore" "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." 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 "backup restore" "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}" 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." ADGUARD_DEFER_END_OP="1" ptxt_step "Reinstalling AdGuardHome service scripts after restore." if ! inst_AdGuardHome "${1:-RESTORE}"; then ADGUARD_DEFER_END_OP="0" adguard_restore_after_failed_directory_restore "${RESTORE_ROLLBACK_DIR}" "${TARG_DIR}" "${RESTORE_STAGE_DIR}" "${WAS_RUNNING}" "1" adguard_install_abort_trap_disable rollback_result_notice ptxt_fail "Unable to complete AdGuardHome restore setup; restoring previous installation." return 1 fi ptxt_ok "Reinstalled AdGuardHome service scripts after restore." ADGUARD_DEFER_END_OP="0" adguard_install_abort_trap_disable rm -rf "${RESTORE_ROLLBACK_DIR}" end_op_message 0 "${1:-RESTORE}" elif [ ! -f "${BASE_DIR}/backup_AdGuardHome.tar.gz" ] && [ "$1" = "RESTORE" ]; then rollback_result_write "backup restore" "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() { 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 iptables -t raw -F ADGUARDHOME 2>/dev/null iptables -t raw -X ADGUARDHOME 2>/dev/null del_jffs_script /jffs/scripts/firewall-start } 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 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() { local TARG LINE_NUM LINE_ABOVE OP TARG="$1" [ -f "${TARG}" ] || return if [ "${2:-}" ]; then OP="$(printf '%.1s' "$2")" if [ "$OP" = "!" ]; then LINE_NUM="$(grep -n -F "[ -x ${ADDON_DIR}/" "${TARG}" | grep -v "$(_quote "$2")" | cut -d':' -f1)" else LINE_NUM="$(grep -n -F "[ -x ${ADDON_DIR}/" "${TARG}" | grep "$(_quote "$2")" | cut -d':' -f1)" fi else LINE_NUM="$(grep -n -F "[ -x ${ADDON_DIR}/" "${TARG}" | 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 RET FILENAME TMP_FILE TMP_MD5 TMP_SHA256 MD5SUM_OLD MD5SUM_CURR MD5SUM_EXPECTED SHA256SUM_CURR SHA256SUM_EXPECTED COUNT TARG="$1" shift PERM="$1" shift RET="0" ptxt_phase "Downloading and verifying files." for URL in "$@"; do FILENAME="${URL##*/}" 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 MD5SUM_CURR="" MD5SUM_EXPECTED="" SHA256SUM_CURR="" SHA256SUM_EXPECTED="" ptxt_step "Downloading ${FILENAME} to ${TARG}/${FILENAME}." rm -f "${TMP_FILE}" "${TMP_MD5}" "${TMP_SHA256}" if http_get_file "${URL}" "${TMP_FILE}" "" insecure && [ -s "${TMP_FILE}" ]; then ptxt_ok "Downloaded ${FILENAME}." ptxt_step "Checking remote checksum metadata for ${FILENAME}." if http_get_file "${URL}.sha256sum" "${TMP_SHA256}" "" insecure && [ -s "${TMP_SHA256}" ]; then SHA256SUM_EXPECTED="$(awk 'NR == 1 {print $1; exit}' "${TMP_SHA256}")" fi if http_get_file "${URL}.md5sum" "${TMP_MD5}" "" insecure && [ -s "${TMP_MD5}" ]; then MD5SUM_EXPECTED="$(awk 'NR == 1 {print $1; exit}' "${TMP_MD5}")" fi if sha256_is_valid "${SHA256SUM_EXPECTED}"; then ptxt_step "Verifying SHA-256 checksum for ${FILENAME}." if ! SHA256SUM_CURR="$(file_sha256 "${TMP_FILE}" 2>/dev/null)"; then SHA256SUM_CURR="" fi if [ "${SHA256SUM_CURR}" = "${SHA256SUM_EXPECTED}" ]; then MD5SUM_CURR="$(file_md5 "${TMP_FILE}" 2>/dev/null)" ptxt_ok "SHA-256 checksum verified for ${FILENAME}." break fi if [ -n "${SHA256SUM_CURR}" ]; then ptxt_warn "SHA-256 checksum mismatch for ${FILENAME}; retrying download." else ptxt_warn "SHA-256 checksum could not be calculated for ${FILENAME}; falling back to MD5 if available." fi fi if ! sha256_is_valid "${SHA256SUM_EXPECTED}" || [ -z "${SHA256SUM_CURR}" ]; then if md5_is_valid "${MD5SUM_EXPECTED}"; then ptxt_step "Verifying MD5 checksum for ${FILENAME}." MD5SUM_CURR="$(file_md5 "${TMP_FILE}" 2>/dev/null)" if [ "${MD5SUM_CURR}" = "${MD5SUM_EXPECTED}" ]; then ptxt_warn "Only MD5 checksum metadata was usable for ${FILENAME}; SHA-256 is preferred." break fi ptxt_warn "MD5 checksum mismatch for ${FILENAME}; retrying download." else ptxt_warn "No valid remote checksum metadata for ${FILENAME}; using download result." MD5SUM_CURR="$(file_md5 "${TMP_FILE}" 2>/dev/null)" break fi fi 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 ACTION="$1" OLD_MD5="$2" [ "${ACTION}" = "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" export AI_REEXECED_INSTALLER exec "${TARG_DIR}/installer" "${BRANCH}" "${ACTION}" } inst_AdGuardHome() { local ADGUARD_TAR ARCHIVE_MD5 ARCHIVE_SHA256 INSTALLER_MD5_OLD REMOTE_ADGUARD_ARCHIVE REMOTE_ADGUARD_MD5 REMOTE_ADGUARD_SHA256 REMOTE_ADGUARD_URL REMOTE_ADGUARD_VER RESTART_AFTER_ABORT SERVICE_REFRESH_ONLY ADGUARD_TAR="AdGuardHome_${ADGUARD_ARCH}.tar.gz" ptxt_phase "Running AdGuardHome ${1:-install} orchestration." ensure_sha256sum_tool || return 1 INSTALLER_MD5_OLD="" RESTART_AFTER_ABORT="0" SERVICE_REFRESH_ONLY="${SERVICE_REFRESH_ONLY:-0}" [ -f "${SCRIPT_LOC}" ] && INSTALLER_MD5_OLD="$(file_md5 "${SCRIPT_LOC}" 2>/dev/null)" if [ -z "$2" ]; then if [ "${1:-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 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 "$1" return 1 fi elif [ "${CONFIRM_STATUS}" -eq 1 ]; then PTXT "${INFO} continuing without making a backup." else return "${CONFIRM_STATUS}" fi fi fi if [ -z "${REMOTE_VER}" ] || [ -z "${REMOTE_BETA}" ] || [ -z "${REMOTE_EDGE}" ] || { ! check_connection; }; then PTXT "${ERROR} Unable to detect the Internet!" end_op_message 1 "$1" 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 "$1" return 1 fi ptxt_ok "AdGuardHome target directory is ready." ptxt_step "Downloading installer update." if ! download_file "${TARG_DIR}" 755 "${RURL}/installer" || ! awk '{ print }' "${TARG_DIR}/installer" | grep -m1 "^AI_VERSION=" | grep -qoE '[0-9]{1,2}([.][0-9]{1,2})([.][0-9]{1,2})'; then PTXT "${ERROR} Failed to download installer." end_op_message 1 "$1" return 1 fi ptxt_ok "Installer update downloaded and validated." installer_update_reexec "${1:-install}" "${INSTALLER_MD5_OLD}" if [ "${1:-RESTORE}" != "RESTORE" ]; then if [ "${1:-install}" != "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 "$1" return 1 fi fi local ADGUARD_BRANCH ADGUARD_VER ADGUARD_BRANCH="$(conf_value ADGUARD_BRANCH)" if [ ! -f "${AGH_FILE}" ]; then inst_AdGuardHome "${1:-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 "${1:-update}" "${ADGUARD_BRANCH}" else if [ "${1:-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 ptxt_phase "Refreshing AdGuardHome service integration." else ptxt_phase "Installing AdGuardHome package." REMOTE_ADGUARD_ARCHIVE="$(adguard_remote_archive "$2")" REMOTE_ADGUARD_MD5="$(adguard_remote_md5 "$2")" REMOTE_ADGUARD_SHA256="$(adguard_remote_sha256 "$2")" REMOTE_ADGUARD_URL="$(adguard_remote_url "$2")" [ -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 "$1" return 1 fi if sha256_is_valid "${REMOTE_ADGUARD_SHA256}"; then ptxt_step "Verifying SHA-256 checksum for ${BASE_DIR}/${REMOTE_ADGUARD_ARCHIVE}." ARCHIVE_SHA256="$(file_sha256 "${BASE_DIR}/${REMOTE_ADGUARD_ARCHIVE}" 2>/dev/null)" || { PTXT "${ERROR} Unable to calculate the downloaded AdGuardHome package SHA-256 checksum." rm -f "${BASE_DIR:?}/${REMOTE_ADGUARD_ARCHIVE}" end_op_message 1 "$1" return 1 } 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 "$1" return 1 fi ptxt_ok "SHA-256 checksum verified for ${BASE_DIR}/${REMOTE_ADGUARD_ARCHIVE}." elif md5_is_valid "${REMOTE_ADGUARD_MD5}"; then ptxt_step "Verifying MD5 checksum for ${BASE_DIR}/${REMOTE_ADGUARD_ARCHIVE}." ARCHIVE_MD5="$(file_md5 "${BASE_DIR}/${REMOTE_ADGUARD_ARCHIVE}" 2>/dev/null)" || { PTXT "${ERROR} Unable to calculate the downloaded AdGuardHome package MD5 checksum." rm -f "${BASE_DIR:?}/${REMOTE_ADGUARD_ARCHIVE}" end_op_message 1 "$1" return 1 } 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 "$1" return 1 fi ptxt_warn "Only MD5 package metadata was available; SHA-256 is preferred." fi if [ "$(agh_process_count)" -gt 0 ]; then RESTART_AFTER_ABORT="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 "$1" 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 "$1" return 1 fi ptxt_ok "AdGuardHome command symlink is ready." fi 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 "$1" return 1 fi ptxt_ok "AdGuardHome addon directory is ready." if [ "${1:-update}" = "update" ]; then configure_runtime_defaults upgrade || { adguard_restart_after_install_abort "${RESTART_AFTER_ABORT}" end_op_message 1 "$1" return 1 } fi cleanup_legacy_firewall if ! download_file "${ADDON_DIR}" 755 "${RURL}/AdGuardHome.sh" || ! download_file "/opt/etc/init.d" 755 "${RURL}/S99AdGuardHome" || ! download_file "/opt/etc/init.d" 644 "${RURL}/rc.func.AdGuardHome"; then PTXT "${ERROR} Required AdGuardHome service files could not be installed." adguard_restart_after_install_abort "${RESTART_AFTER_ABORT}" end_op_message 1 "$1" return 1 fi ptxt_ok "AdGuardHome service files installed." [ -f "/jffs/scripts/dnsmasq.postconf" ] && yaml_nvars_delete "#Asuswrt-Merlin AdGuardHome Installer" /jffs/scripts/dnsmasq.postconf for i in init-start services-stop; do { if { ! grep -q "${ADDON_DIR}/AdGuardHome.sh $i &" "/jffs/scripts/${i}" && grep -q "${ADDON_DIR}/AdGuardHome.sh $i" "/jffs/scripts/${i}"; }; then del_jffs_script "/jffs/scripts/${i}"; fi; }; done if ! write_manager_script /jffs/scripts/init-start "init-start &" || ! write_manager_script /jffs/scripts/services-stop "services-stop &" || ! write_manager_script /jffs/scripts/dnsmasq.postconf dnsmasq || ! write_manager_script /jffs/scripts/firewall-start 'firewall "$1"'; then PTXT "${ERROR} Required Asuswrt-Merlin event scripts could not be configured." adguard_restart_after_install_abort "${RESTART_AFTER_ABORT}" end_op_message 1 "$1" return 1 fi ptxt_ok "Asuswrt-Merlin event scripts configured." if nvram get rc_support | grep -q 'mtlancfg'; then del_jffs_script /jffs/scripts/dnsmasq-sdn.postconf; fi if nvram get rc_support | grep -q 'mtlancfg' && ! write_manager_script /jffs/scripts/dnsmasq-sdn.postconf 'dnsmasq-sdn $2'; then PTXT "${ERROR} Required SDN dnsmasq event script could not be configured." adguard_restart_after_install_abort "${RESTART_AFTER_ABORT}" end_op_message 1 "$1" return 1 fi del_between_magic /jffs/scripts/service-event-end '# Asuswrt-Merlin-AdGuardHome-Installer' if ! write_command_script /jffs/scripts/service-event-end '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'; then PTXT "${ERROR} Required service-event-end hook could not be configured." adguard_restart_after_install_abort "${RESTART_AFTER_ABORT}" end_op_message 1 "$1" return 1 fi if [ "${1:-update}" != "update" ]; then ptxt_phase "Configuring AdGuardHome first-run settings." if [ ! -f "${ADDON_DIR}/localtime" ]; then PTXT "${INFO} Before setting up AdGuardHome, please choose your right timezone!" if ! set_timezone; then adguard_restart_after_install_abort "${RESTART_AFTER_ABORT}" end_op_message 1 "$1" return 1 fi fi if ! setup_AdGuardHome "" "${1:-install}"; then adguard_restart_after_install_abort "${RESTART_AFTER_ABORT}" end_op_message 1 return 1 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 adguard_install_abort_trap_disable_preserve_defer end_op_message 1 "$1" return 1 fi adguard_install_abort_trap_disable_preserve_defer ptxt_ok "AdGuardHome ${1:-install} orchestration complete." ptxt_ok "Final readiness summary: service is installed, started, and ready." end_op_message 0 "$1" } set_timezone() { local TMP TZ_ARCH TZ_DATA INDEX TZ_FILE TZ_LIST TMP="/root" TZ_ARCH="$(uname -m)" case "${TZ_ARCH}" in "aarch64" | "arm64") TZ_ARCH="aarch64" ;; "armv7l") TZ_ARCH="arm" ;; esac TZ_DATA="tzdata-2021e-1-${TZ_ARCH}.pkg.tar.bz2" ensure_opkg_package column || 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}" | awk -F'/' '!/\/$/ && /\/posix\// {count++} END {print count + 0}')" [ "${INDEX}" -gt 0 ] || { PTXT "${ERROR} No timezone files were found in ${TZ_DATA}." return 1 } PTXT "${INFO} Available timezones/locations:" PTXT "${TZ_LIST}" | awk -F'/' '!/\/$/ && /\/posix\//' | sort | cut -d'/' -f2- | awk -v INDEX=0 -F'/' '!/\/$/ {++INDEX;printf " " INDEX") ";for (i=5; i"${CONFIG_BACKUP}.absent" || return 1 fi } restore_installer_config() { local CONFIG_BACKUP="$1" if [ -f "${CONFIG_BACKUP}" ]; then mv -f "${CONFIG_BACKUP}" "${CONF_FILE}" || return 1 elif [ -f "${CONFIG_BACKUP}.absent" ]; then rm -f "${CONF_FILE}" || return 1 fi rm -f "${CONFIG_BACKUP}.absent" } 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" "rollback complete" "previous ${YAML_FILE} restored" rollback_result_notice return 0 fi rollback_result_write "setup-yaml" "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_AdGuardHome_impl() { local CHOOSE_NEW=0 CONFIG_ROLLBACK="" DNS_FILTER_ROLLBACK_DIR="" DOMAIN_CONFIG_SELECTION="" DOMAIN_LAN_CHANGED=0 IPSET_SELECTION LAN_DOMAIN_ROLLBACK="" WEB_PORT_SELECTION YAML_ORI_NEW="" 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 [ -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 ! write_conf ADGUARD_WEBUI_PORT "\"${WEB_PORT}\""; then PTXT "${ERROR} Unable to save the AdGuardHome WebUI port." return 1 fi yaml_nvars_replace " address: 0.0.0.0:$(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])$')" " address: 0.0.0.0:${WEB_PORT}" "${YAML_FILE}" 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 yaml_nvars_replace " address: 0.0.0.0:$(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])$')" " address: 0.0.0.0:$(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])$')" "${YAML_FILE}" fi ########################################################################################################################################################################################################################################################################## if [ -n "$(awk -F': ' '/.*schema\:.*/{ print $2 }' "${YAML_FILE}")" ] && [ "$(awk -F': ' '/.*schema\:.*/{ print $2 }' "${YAML_FILE}")" != "${SCHEMA_VER}" ]; then yaml_nvars_replace "schema_version: $(awk -F': ' '/.*schema\:.*/{ print $2 }' "${YAML_FILE}")" "schema_version: ${SCHEMA_VER}" "${YAML_FILE}"; fi if ! check_AdGuardHome_yaml; then setup_AdGuardHome_impl x 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 PTXT "${INFO} Use previous settings file" elif [ "${CONFIRM_STATUS}" -eq 1 ]; then setup_AdGuardHome_impl x else return "${CONFIRM_STATUS}" fi else if [ -f "${YAML_FILE}" ]; then if [ "$1" = "reconfig" ]; then if ! check_AdGuardHome_yaml; then setup_AdGuardHome_impl x 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 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) PTXT "${INFO} Backing up previous settings file..." if ! mv "${YAML_FILE}" "${YAML_BAK}"; then 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 cp -f "${YAML_ORI}" "${YAML_FILE}" fi case "${2:-reconfig}" in "install" | "reconfig") if [ ! -f "${YAML_FILE}" ] || [ "$2" = "reconfig" ]; then local DNS_FILTER_SELECTION LOCAL_CACHE_SELECTION YESNO_STATUS 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 return "${YESNO_STATUS}" ;; esac if [ "$(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 return "${YESNO_STATUS}" ;; esac fi 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 return "${YESNO_STATUS}" ;; esac if [ "$2" != "reconfig" ] && ! 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 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 return 1 fi WEB_PORT_SELECTION="${WEB_PORT}" YAML_ORI_NEW="${YAML_ORI}.new.$$" SETUP_YAML_TMP_FILE="${YAML_ORI_NEW}" PTXT "http:" \ " address: 0.0.0.0:${WEB_PORT}" >"${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 return 1 fi PTXT "dns:" \ " bind_hosts:" \ " - 0.0.0.0" \ " 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 return 1 fi local DOMAIN="" NET_ADDR NET_ADDR6 LAN_IF LAN_IF="$(nvram get lan_ifname)" if [ -n "${LAN_IF}" ] && ai_have_cmd ip; then NET_ADDR="$(ip -o -4 addr list "${LAN_IF}" 2>/dev/null | awk 'NR==1{ split($4, ip_addr, "/"); print ip_addr[1] }')" NET_ADDR6="$(ip -o -6 addr list "${LAN_IF}" scope global 2>/dev/null | awk 'NR==1{ split($4, ip_addr, "/"); print ip_addr[1] }')" fi [ -n "${NET_ADDR:-}" ] || NET_ADDR="$(nvram get lan_ipaddr)" [ -n "${NET_ADDR6:-}" ] || NET_ADDR6="$(nvram get ipv6_rtr_addr)" LAN_DOMAIN_ROLLBACK="$(nvram get lan_domain)" 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/')/][::]:553'" >>"${YAML_ORI_NEW}" PTXT " - '[/router.asus.com/][::]:553'" \ " - '[/www.asusnetwork.net/][::]:553'" \ " - '[/www.asusrouter.com/][::]:553'" \ " - '[/use-application-dns.net/][::]:553'" \ " - '[/dns.resolver.arpa/][::]:553'" \ " - '[/${DOMAIN}/][::]:553'" \ " - '[//][::]:553'" \ " - ${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:" \ " - '[::]:553'" \ " - '[/10.in-addr.arpa/][::]:553'" \ " - '[/$(printf "%s\n" "${NET_ADDR}" | awk 'BEGIN{FS="."}{print $2"."$1".in-addr.arpa"}')/][::]:553'" \ "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 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 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 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 [ -n "${DNS_FILTER_SELECTION:-}" ]; then 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 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 PTXT "${ERROR} Unable to save the AdGuardHome domain setting." return 1 fi if [ "${DOMAIN_CONFIG_SELECTION}" = "yes" ] && [ -z "${LAN_DOMAIN_ROLLBACK}" ]; then nvram set lan_domain="${DOMAIN}" && nvram commit 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 nvram set lan_domain="${LAN_DOMAIN_ROLLBACK}" nvram commit fi [ -z "${YAML_ORI_NEW:-}" ] || rm -f "${YAML_ORI_NEW}" setup_restore_previous_yaml || 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 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 nvram set lan_domain="${LAN_DOMAIN_ROLLBACK}" nvram commit fi [ -z "${YAML_ORI_NEW:-}" ] || rm -f "${YAML_ORI_NEW}" setup_restore_previous_yaml || true return "${YESNO_STATUS}" fi if [ "$(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 nvram set lan_domain="${LAN_DOMAIN_ROLLBACK}" nvram commit 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 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 nvram set lan_domain="${LAN_DOMAIN_ROLLBACK}" nvram commit 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 PTXT "${ERROR} Unable to save the AdGuardHome local cache setting." return 1 fi fi fi if [ "$2" = "reconfig" ] && [ -n "${IPSET_SELECTION:-}" ] && ! 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 if [ -n "${DNS_FILTER_SELECTION:-}" ] && ! configure_runtime_defaults new-install "${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 nvram set lan_domain="${LAN_DOMAIN_ROLLBACK}" nvram commit 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 save the AdGuardHome runtime defaults." return 1 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 nvram set lan_domain="${LAN_DOMAIN_ROLLBACK}" nvram commit 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 save the AdGuardHome WebUI port." return 1 fi if [ -n "${YAML_ORI_NEW:-}" ] && ! mv -f "${YAML_ORI_NEW}" "${YAML_ORI}"; 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 nvram set lan_domain="${LAN_DOMAIN_ROLLBACK}" nvram commit 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 PTXT "${ERROR} Unable to publish the original AdGuardHome configuration snapshot." return 1 fi SETUP_YAML_TMP_FILE="" [ -z "${CONFIG_ROLLBACK}" ] || discard_installer_config_backup "${CONFIG_ROLLBACK}" [ -z "${DNS_FILTER_ROLLBACK_DIR}" ] || rm -rf "${DNS_FILTER_ROLLBACK_DIR}" ;; 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() { 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 agh_stop cleanup_legacy_firewall [ "$(conf_value ADGUARD_DOMAIN)" = "yes" ] && nvram set lan_domain="" && nvram commit 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" yaml_nvars_delete "#Asuswrt-Merlin AdGuardHome Installer" /jffs/scripts/dnsmasq.postconf del_jffs_script /jffs/scripts/init-start del_jffs_script /jffs/scripts/dnsmasq.postconf del_jffs_script /jffs/scripts/firewall-start if nvram get rc_support | grep -q 'mtlancfg'; then del_jffs_script /jffs/scripts/dnsmasq-sdn.postconf; fi del_jffs_script /jffs/scripts/services-stop del_between_magic /jffs/scripts/service-event-end '# Asuswrt-Merlin-AdGuardHome-Installer' service restart_dnsmasq >/dev/null 2>&1 end_op_message 0 } write_command_script() { local TARG COMMAND FILENAME TARG="$1" COMMAND="$2" FILENAME="${TARG##*/}" if [ ! -f "${TARG}" ]; then PTXT "${INFO} Creating ${FILENAME} file" if ! PTXT "#!/bin/sh" >"${TARG}"; then PTXT "${ERROR} Unable to create ${TARG}" return 1 fi fi if ! chmod 755 "${TARG}"; then PTXT "${ERROR} Unable to set executable permissions on ${TARG}" return 1 fi if [ "$(grep -c -F "${COMMAND}" "${TARG}")" -gt 0 ]; then PTXT "${INFO} ${FILENAME} file already configured" else PTXT "${INFO} Configure ${FILENAME} file" if ! PTXT "${COMMAND}" >>"${TARG}"; then PTXT "${ERROR} Unable to update ${TARG}" return 1 fi fi return 0 } write_conf() { local VAR VALUE VAR="$1" VALUE="$2" 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() { local TARG OP FILENAME COMMAND TARG="$1" OP="$2" FILENAME="${TARG##*/}" COMMAND="${ADDON_DIR}/AdGuardHome.sh" if [ ! -f "${TARG}" ]; then PTXT "${INFO} Creating ${FILENAME} file" if ! PTXT "#!/bin/sh" >"${TARG}"; then PTXT "${ERROR} Unable to create ${TARG}" return 1 fi fi if ! chmod 755 "${TARG}" "${COMMAND}"; then PTXT "${ERROR} Unable to set executable permissions for ${FILENAME}" return 1 fi del_between_magic "${TARG}" Asuswrt-Merlin-AdGuardHome-Installer || return 1 if [ "$(grep -c -F "[ -x ${COMMAND} ] && ${COMMAND} ${OP}" "${TARG}")" -gt 0 ]; then PTXT "${INFO} ${FILENAME} file already configured" else PTXT "${INFO} Configure ${FILENAME} file" if grep -q "^${COMMAND}" "${TARG}"; then if ! sed -i "s~^${COMMAND}~[ -x ${COMMAND} ] \&\& ${COMMAND} ${OP}~" "${TARG}"; then PTXT "${ERROR} Unable to update ${TARG}" return 1 fi else del_jffs_script "${TARG}" !manager || return 1 if [ "$(tail -1 "${TARG}" | grep -c '^$')" -eq 0 ] && ! PTXT "" >>"${TARG}"; then PTXT "${ERROR} Unable to update ${TARG}" return 1 fi if ! PTXT "[ -x ${COMMAND} ] && ${COMMAND} ${OP}" >>"${TARG}"; then PTXT "${ERROR} Unable to update ${TARG}" return 1 fi fi fi return 0 } yaml_nvars_append() { echo "$1" >>"$2" } yaml_nvars_delete() { PATTERN="$(_quote "$1")" sed -i "/${PATTERN}/d" "$2" } 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" } end_op_message() { 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 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) [ "$2" = "update" ] && exit 0 PTXT "${INFO} Operation completed, returning to Main Menu. You can quit or continue." ;; 1) if [ "$2" = "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 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() { 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" " m) Migrate Runtime Defaults" if [ -n "${BLOCKLIST_ANALYZER_SHA256}" ]; then PTXT " 9) Analyze and remove unused blocklists" fi 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 'clear_screen; end_op_message 2' HUP INT QUIT ABRT TERM case "${CHOSEN}" in "1" | "install" | "update") ptxt_phase "Selecting install or update operation." if [ "${CHOSEN}" = "update" ]; then ptxt_step "Starting requested AdGuardHome update." AUTO_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." 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 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 [ "$(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 PTXT "${INFO} For best results, please reboot the router after making these changes." 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="update" inst_AdGuardHome "${AUTO_UPDATE:-update}" }; else end_op_message 1; fi ;; "8" | "setipset") 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 ! 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." 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" | "install" | "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_usage() { PTXT "${INFO} Usage:" \ "${INFO} sh installer install [--installer-branch master] [--adguardhome-branch {release|beta|edge}] --yes [--allow-dns-nvram]" \ "${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 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]" } 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 netcheck_change proc_optimize_change proc_profile_change proc_optimize_current 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 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" netcheck_change="0" proc_optimize_change="0" proc_profile_change="0" 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 wan || { 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; v2.6.0 keeps optimization enabled only with the safer balanced profile." changes="1" proc_optimize_change="1" else PTXT "${INFO} Runtime default already set: ADGUARD_PROC_OPTIMIZE=\"${proc_optimize_current}\"." fi cli_migrate_runtime_default ADGUARD_PROC_PROFILE aggressive balanced || { changes="1" proc_profile_change="1" } 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 wan || 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 balanced || return 1 fi ptxt_ok "Runtime defaults migrated to v2.6.0 safer values." } cli_installer_branch_from_args() { while [ "$#" -gt 0 ]; do case "$1" in --installer-branch) [ "$#" -gt 1 ] || return 1 branch_is_safe "$2" || return 1 PTXT "$2" return 0 ;; --branch | --adguardhome-branch | --file | --profile | --mode | --hosts | --dns | --require-http | --timeout | --policy) [ "$#" -gt 1 ] || return 1 shift 2 ;; *) shift ;; esac done return 1 } 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 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 --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 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 "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="update" menu update ;; 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 cli_enable_assume_yes cli_backup_restore_file "${restore_file}" ;; 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) ipset_status return "$?" ;; refresh) ;; *) return 2 ;; esac [ "${CLI_DRY_RUN}" = "1" ] && { cli_dry_run "IPSET refresh" return 0 } 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 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 "install" return 0 } cli_require_yes || return 1 if [ "${CLI_ALLOW_DNS_NVRAM}" != "1" ]; then PTXT "${ERROR} Refusing install DNS/NVRAM rewrite without --allow-dns-nvram." return 1 fi if [ ! -f "${AGH_FILE}" ]; then PTXT "${ERROR} Non-interactive install cannot complete fresh first-run setup." PTXT "${ERROR} Run the interactive installer for fresh installs, or use restore --file with an existing backup." return 1 fi cli_enable_assume_yes cleanup check_jffs_enabled check_dns_environment 0 [ -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 ;; esac } 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 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 amtm_update_check "$@" installer_dependencies_available || exit 1 trap 'on_installer_exit' EXIT 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}" BRANCH="$(conf_value INSTALLER_BRANCH)" if [ -n "${BRANCH}" ]; then BRANCH="$(sanitize_branch "${BRANCH}")" else BRANCH="master" fi set -- "${BRANCH}" "${CHOSEN}" fi case "${1}" in install | update | backup | restore | doctor | ipset | netcheck | dns-port-policy | performance | migrate-runtime-defaults | uninstall) BRANCH="$(cli_installer_branch_from_args "$@" 2>/dev/null)" [ -n "${BRANCH}" ] || BRANCH="$(conf_value INSTALLER_BRANCH)" [ -n "${BRANCH}" ] && BRANCH="$(sanitize_branch "${BRANCH}")" || BRANCH="master" ;; "amtmupdate") BRANCH="$(conf_value INSTALLER_BRANCH)" [ -n "${BRANCH}" ] && BRANCH="$(sanitize_branch "${BRANCH}")" || BRANCH="master" case "$2" in "") CHOSEN="${1#amtm}" set -- "$BRANCH" "$CHOSEN" ;; esac ;; "") BRANCH="master" ;; *) BRANCH="${1}" ;; esac BRANCH="$(sanitize_branch "${BRANCH}")" write_conf INSTALLER_BRANCH "\"${BRANCH}\"" readonly BLOCKLIST_ANALYZER_SHA256 [ -z "$(nvram get odmpid)" ] && ROUTER_MODEL="$(nvram get productid)" || ROUTER_MODEL="$(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="$(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 [ "$(nvram get sw_mode)" != "1" ] && PTXT "${ERROR} You are not running in router mode, sorry." && sleep 3s && exit 1 if [ -n "${NAT_ENV}" ] && [ -z "$2" ]; 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 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 ;; backup | restore | doctor | ipset | netcheck | dns-port-policy | performance | migrate-runtime-defaults | uninstall) ;; *) init_remote_adguard_metadata ;; esac case "${1:-}" in install | update | backup | restore | doctor | ipset | netcheck | dns-port-policy | performance | migrate-runtime-defaults | uninstall) 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 "") cleanup check_jffs_enabled check_dns_environment 0 check_version menu ;; *) 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