#!/bin/sh
export LC_ALL=C
export PATH="/sbin:/bin:/usr/sbin:/usr/bin:/opt/sbin:/opt/bin:/opt/usr/sbin:/opt/usr/bin"

which renice >/dev/null 2>&1 && renice -10 $$ >/dev/null 2>&1
ENABLED="yes"
WORK_DIR="/opt/etc/AdGuardHome"
LOG_FILE="syslog"
PID_FILE="/opt/var/run/AdGuardHome.pid"
PROCS="AdGuardHome"
DNS_HANDOFF_DIR="/tmp/AdGuardHome.dns-handoff"
DNS_HANDOFF_FILE="${DNS_HANDOFF_DIR}/active"
DNS_HANDOFF_LOCK="${DNS_HANDOFF_DIR}/lock"
WATCHD_NICE_SNAPSHOT=""

agh_timestamp() {
	date '+%Y/%m/%d %H:%M:%S'
}

agh_log() {
	_level="$1"
	_func="$2"
	shift 2
	logger -st "${PROCS}" "$(agh_timestamp) [${_level}] ${_func}: $*"
}
# Lower GOGC values reduce AdGuardHome memory growth, but increase garbage-collection CPU cost.
GOGC="${ADGUARDHOME_GOGC:-50}"
CPU_COUNT=""
if which grep >/dev/null 2>&1; then
	CPU_COUNT="$(grep -c '^processor' /proc/cpuinfo 2>/dev/null)"
fi
case "${CPU_COUNT}" in
	"" | *[!0-9]* | "0" | "1" | "2" | "3") DEFAULT_GOMAXPROCS="1" ;;
	*) DEFAULT_GOMAXPROCS="2" ;;
esac
GOMAXPROCS="${ADGUARDHOME_GOMAXPROCS:-${DEFAULT_GOMAXPROCS}}"
case "${GOMAXPROCS}" in
	"" | *[!0-9]* | "0") GOMAXPROCS="1" ;;
esac
MEMORY_LIMIT_MIB=""
if which awk >/dev/null 2>&1; then
	MEMORY_LIMIT_MIB="$(awk '
		$1 == "MemAvailable:" { mem_available = $2 }
		$1 == "MemFree:" { mem_free = $2 }
		$1 == "Buffers:" { buffers = $2 }
		$1 == "Cached:" { cached = $2 }
		END {
			total_available_memory = mem_available
			if (!total_available_memory) total_available_memory = mem_free + buffers + cached
			if (total_available_memory > 0) {
				limit = int(total_available_memory * 70 / 100 / 1024)
				if (limit > 384) limit = 384
				print limit
			}
		}
	' /proc/meminfo 2>/dev/null)"
fi
case "${MEMORY_LIMIT_MIB}" in
	"" | *[!0-9]*) MEMORY_LIMIT_MIB="128" ;;
esac
GOMEMLIMIT="${MEMORY_LIMIT_MIB}MiB"
GODEBUG="${GODEBUG:-}"
THP_ENABLED=""
if [ -r "/sys/kernel/mm/transparent_hugepage/enabled" ]; then
	IFS= read -r THP_ENABLED </sys/kernel/mm/transparent_hugepage/enabled || THP_ENABLED=""
fi
case "${THP_ENABLED}" in
	*"[always]"* | *"[madvise]"*)
		[ -z "${GODEBUG}" ] && GODEBUG="disablethp=1"
		GODEBUG="${GODEBUG:+${GODEBUG}}"
		;;
esac
ARGS="-s run -c ${WORK_DIR}/AdGuardHome.yaml -w ${WORK_DIR} --pidfile ${PID_FILE} --no-check-update -l ${LOG_FILE}"
PREARGS="env TZ=/etc/localtime GOGC=${GOGC} GOMAXPROCS=${GOMAXPROCS} GOMEMLIMIT=${GOMEMLIMIT} QUIC_GO_DISABLE_ECN=true"
[ -n "${GODEBUG}" ] && PREARGS="${PREARGS} GODEBUG=${GODEBUG}"

# Functions are grouped by purpose; names are sorted alpha-numerically within each group.

# DNS handoff helpers

dns_handoff_dependencies_available() {
	for _dns_command in awk chmod kill ln ls mkdir netstat pidof rm service sleep; do
		if ! which "${_dns_command}" >/dev/null 2>&1; then
			if which logger >/dev/null 2>&1; then
				agh_log ERROR dns_handoff_dependencies_available "Required DNS handoff command is unavailable: ${_dns_command}"
			else
				printf '%s\n' "${PROCS}: required DNS handoff command is unavailable: ${_dns_command}" >&2
			fi
			return 1
		fi
	done
	return 0
}

dns_handoff_path_has_owner_mode() {
	_dns_handoff_expected_type="$1"
	_dns_handoff_expected_mode="$2"
	_dns_handoff_path="$3"
	ls -ldn "${_dns_handoff_path}" 2>/dev/null |
		awk -v expected_type="${_dns_handoff_expected_type}" \
			-v expected_mode="${_dns_handoff_expected_mode}" '
			NR == 1 {
				exit(substr($1, 1, 1) == expected_type &&
					substr($1, 2, 9) == expected_mode &&
					$3 == 0 ? 0 : 1)
			}
			END {
				if (NR == 0) exit 1
			}
		'
}

dns_handoff_directory_is_private() {
	[ -d "${DNS_HANDOFF_DIR}" ] && [ ! -L "${DNS_HANDOFF_DIR}" ] || return 1
	dns_handoff_path_has_owner_mode d 'rwx------' "${DNS_HANDOFF_DIR}"
}

dns_handoff_marker_is_private() {
	[ -f "${DNS_HANDOFF_FILE}" ] && [ ! -L "${DNS_HANDOFF_FILE}" ] || return 1
	dns_handoff_path_has_owner_mode - 'rw-------' "${DNS_HANDOFF_FILE}"
}

dns_handoff_process_is_root() {
	awk '
		$1 == "Uid:" {
			exit($2 == 0 && $3 == 0 && $4 == 0 && $5 == 0 ? 0 : 1)
		}
		END {
			if (NR == 0) exit 1
		}
	' "/proc/$1/status" 2>/dev/null
}

dns_handoff_process_start_time() {
	_dns_handoff_stat="/proc/$1/stat"
	[ -r "${_dns_handoff_stat}" ] || return 1
	awk '{
		sub(/^.*\) /, "")
		print $20
	}' "${_dns_handoff_stat}" 2>/dev/null
}

dns_handoff_set_current_identity() {
	_dns_handoff_current_stat=""
	IFS= read -r _dns_handoff_current_stat </proc/self/stat || return 1
	DNS_HANDOFF_CURRENT_PID="${_dns_handoff_current_stat%% *}"
	_dns_handoff_current_fields="${_dns_handoff_current_stat##*) }"
	# Intentional word splitting: /proc/self/stat is a space-delimited record.
	set -- ${_dns_handoff_current_fields}
	[ "$#" -ge 20 ] || return 1
	shift 19
	DNS_HANDOFF_CURRENT_START_TIME="${1:-}"
	case "${DNS_HANDOFF_CURRENT_PID}:${DNS_HANDOFF_CURRENT_START_TIME}" in
		*[!0-9:]* | :* | *:) return 1 ;;
	esac
	return 0
}

dns_handoff_marker_is_active() {
	_dns_handoff_marker_pid=""
	_dns_handoff_marker_start_time=""
	dns_handoff_directory_is_private || return 1
	dns_handoff_marker_is_private || return 1
	IFS=' ' read -r _dns_handoff_marker_pid _dns_handoff_marker_start_time <"${DNS_HANDOFF_FILE}" ||
		return 1
	case "${_dns_handoff_marker_pid}:${_dns_handoff_marker_start_time}" in
		*[!0-9:]* | :* | *:) return 1 ;;
	esac
	_dns_handoff_current_start_time="$(dns_handoff_process_start_time "${_dns_handoff_marker_pid}")" ||
		return 1
	dns_handoff_process_is_root "${_dns_handoff_marker_pid}" || return 1
	[ "${_dns_handoff_current_start_time}" = "${_dns_handoff_marker_start_time}" ]
}

dns_handoff_lock_file_is_active() {
	_dns_handoff_lock_file="$1"
	_dns_handoff_lock_pid=""
	_dns_handoff_lock_start_time=""
	[ -f "${_dns_handoff_lock_file}" ] && [ ! -L "${_dns_handoff_lock_file}" ] || return 1
	IFS=' ' read -r _dns_handoff_lock_pid _dns_handoff_lock_start_time <"${_dns_handoff_lock_file}" ||
		return 1
	case "${_dns_handoff_lock_pid}:${_dns_handoff_lock_start_time}" in
		*[!0-9:]* | :* | *:) return 1 ;;
	esac
	_dns_handoff_lock_current_start_time="$(dns_handoff_process_start_time "${_dns_handoff_lock_pid}")" ||
		return 1
	[ "${_dns_handoff_lock_current_start_time}" = "${_dns_handoff_lock_start_time}" ]
}

dns_handoff_lock_is_active() {
	dns_handoff_lock_file_is_active "${DNS_HANDOFF_LOCK}"
}

watchdog_pids() {
	ps | awk '/[w]atchdog/ { print $1 }'
}

pid_nice() {
	[ -r "/proc/$1/stat" ] || return 1
	awk '{
		# After removing the pid and comm fields, /proc/<pid>/stat field 19
		# (nice) is the seventeenth remaining whitespace-delimited field.
		sub(/^[^)]*\)[[:space:]]*/, "")
		print $17
		exit
	}' "/proc/$1/stat" 2>/dev/null
}

save_watchdog_nice() {
	_watchdog_nice_snapshot=""
	for _watchdog_nice_pid in $(watchdog_pids); do
		_watchdog_nice_value="$(pid_nice "${_watchdog_nice_pid}")" || continue
		[ -n "${_watchdog_nice_value}" ] || continue
		_watchdog_nice_snapshot="${_watchdog_nice_snapshot}${_watchdog_nice_pid}:${_watchdog_nice_value} "
	done
	WATCHD_NICE_SNAPSHOT="${_watchdog_nice_snapshot}"
	[ -n "${WATCHD_NICE_SNAPSHOT}" ]
}

restore_watchdog_nice() {
	for _watchdog_nice_item in ${WATCHD_NICE_SNAPSHOT}; do
		_watchdog_nice_pid="${_watchdog_nice_item%%:*}"
		_watchdog_nice_value="${_watchdog_nice_item#*:}"
		[ -d "/proc/${_watchdog_nice_pid}" ] || continue
		renice "${_watchdog_nice_value}" "${_watchdog_nice_pid}" >/dev/null 2>&1
	done
	WATCHD_NICE_SNAPSHOT=""
}

reap_the_watch_dog() {
	WATCHD_PID="$(watchdog_pids)"
	[ -n "${WATCHD_PID}" ] || return 1
	case "${1:-}" in
		STOP)
			# Only save once, so a second "stop" does not overwrite
			# the real original nice values with 19.
			[ -n "${WATCHD_NICE_SNAPSHOT}" ] || save_watchdog_nice
			renice 19 ${WATCHD_PID} >/dev/null 2>&1
			;;
		CONT)
			restore_watchdog_nice
			;;
		*)
			return 1
			;;
	esac
	kill -s "${1}" ${WATCHD_PID} >/dev/null 2>&1
	return 0
}

resume_dns_watchdog() {
	reap_the_watch_dog CONT >/dev/null 2>&1 || true
}

restore_dns_watchdog_traps() {
	_dns_watchdog_restore_traps="${1:-}"
	[ -n "${_dns_watchdog_restore_traps}" ] || return 1
	[ -f "${_dns_watchdog_restore_traps}" ] || return 1
	trap - HUP INT TERM EXIT
	eval "$(cat "${_dns_watchdog_restore_traps}")"
	rm -f "${_dns_watchdog_restore_traps}" 2>/dev/null
}

save_dns_watchdog_traps() {
	_dns_watchdog_trap_suffix="${1:-}"
	_dns_watchdog_trap_file="${DNS_HANDOFF_DIR}/watchdog-traps.$$"
	[ -n "${_dns_watchdog_trap_suffix}" ] && _dns_watchdog_trap_file="${_dns_watchdog_trap_file}.${_dns_watchdog_trap_suffix}"
	trap >"${_dns_watchdog_trap_file}" || return 1
	DNS_WATCHDOG_TRAP_FILE="${_dns_watchdog_trap_file}"
	return 0
}

suspend_dns_watchdog() {
	reap_the_watch_dog STOP >/dev/null 2>&1 || true
}

reclaim_stale_dns_handoff_lock() {
	_dns_handoff_stale_candidate="${DNS_HANDOFF_LOCK}.stale.$$"
	rm -f "${_dns_handoff_stale_candidate}" 2>/dev/null
	ln "${DNS_HANDOFF_LOCK}" "${_dns_handoff_stale_candidate}" 2>/dev/null || return 1
	if dns_handoff_lock_file_is_active "${_dns_handoff_stale_candidate}"; then
		rm -f "${_dns_handoff_stale_candidate}"
		return 1
	fi
	_dns_handoff_lock_identity="$(ls -din "${DNS_HANDOFF_LOCK}" 2>/dev/null |
		awk 'NR == 1 { print $1 }')" || {
		rm -f "${_dns_handoff_stale_candidate}"
		return 1
	}
	_dns_handoff_stale_identity="$(ls -din "${_dns_handoff_stale_candidate}" 2>/dev/null |
		awk 'NR == 1 { print $1 }')" || {
		rm -f "${_dns_handoff_stale_candidate}"
		return 1
	}
	if [ -z "${_dns_handoff_lock_identity}" ] ||
		[ "${_dns_handoff_lock_identity}" != "${_dns_handoff_stale_identity}" ]; then
		rm -f "${_dns_handoff_stale_candidate}"
		return 1
	fi
	rm -f "${DNS_HANDOFF_LOCK}" 2>/dev/null || {
		rm -f "${_dns_handoff_stale_candidate}"
		return 1
	}
	rm -f "${_dns_handoff_stale_candidate}"
}

release_dns_handoff_lock() {
	rm -f "${DNS_HANDOFF_LOCK}" 2>/dev/null
}

disable_dns_handoff() {
	_dns_handoff_pid=""
	_dns_handoff_current_pid=""
	if [ -f "${DNS_HANDOFF_FILE}" ]; then
		IFS=' ' read -r _dns_handoff_pid _dns_handoff_start_time <"${DNS_HANDOFF_FILE}" ||
			_dns_handoff_pid=""
	fi
	if [ -n "${_dns_handoff_pid}" ]; then
		if dns_handoff_set_current_identity; then
			_dns_handoff_current_pid="${DNS_HANDOFF_CURRENT_PID}"
		fi
	fi
	if [ -n "${_dns_handoff_pid}" ] && [ "${_dns_handoff_pid}" != "${_dns_handoff_current_pid}" ]; then
		agh_log ERROR disable_dns_handoff "Refusing to remove a dnsmasq handoff owned by PID ${_dns_handoff_pid}."
		return 1
	fi
	if ! rm -f "${DNS_HANDOFF_FILE}"; then
		agh_log ERROR disable_dns_handoff "Unable to disable the dnsmasq port 553 handoff."
		return 1
	fi
	return 0
}

enable_dns_handoff() {
	if [ ! -e "${DNS_HANDOFF_DIR}" ]; then
		(
			umask 077
			mkdir "${DNS_HANDOFF_DIR}"
		) 2>/dev/null || {
			logger -st "${PROCS}" "Unable to create the private dnsmasq handoff directory."
			return 1
		}
	fi
	if ! dns_handoff_directory_is_private; then
		logger -st "${PROCS}" "Refusing to use an insecure dnsmasq handoff directory."
		return 1
	fi
	dns_handoff_set_current_identity || {
		logger -st "${PROCS}" "Unable to identify the dnsmasq handoff owner."
		return 1
	}
	_dns_handoff_owner_pid="${DNS_HANDOFF_CURRENT_PID}"
	_dns_handoff_start_time="${DNS_HANDOFF_CURRENT_START_TIME}"
	_dns_handoff_lock_attempts="0"
	_dns_handoff_lock_candidate="${DNS_HANDOFF_LOCK}.${_dns_handoff_owner_pid}"
	if ! (
		umask 077
		printf '%s %s\n' "${_dns_handoff_owner_pid}" "${_dns_handoff_start_time}" >"${_dns_handoff_lock_candidate}"
	); then
		logger -st "${PROCS}" "Unable to prepare the dnsmasq handoff marker lock owner."
		rm -f "${_dns_handoff_lock_candidate}"
		return 1
	fi
	while ! ln "${_dns_handoff_lock_candidate}" "${DNS_HANDOFF_LOCK}" 2>/dev/null; do
		if dns_handoff_lock_is_active; then
			logger -st "${PROCS}" "Another dnsmasq handoff marker update is in progress."
			rm -f "${_dns_handoff_lock_candidate}"
			return 1
		fi
		_dns_handoff_lock_attempts="$((_dns_handoff_lock_attempts + 1))"
		if [ "${_dns_handoff_lock_attempts}" -lt 3 ]; then
			sleep 1s
			continue
		fi
		if ! reclaim_stale_dns_handoff_lock; then
			logger -st "${PROCS}" "Unable to remove a stale dnsmasq handoff marker lock."
			rm -f "${_dns_handoff_lock_candidate}"
			return 1
		fi
	done
	if ! rm -f "${_dns_handoff_lock_candidate}"; then
		logger -st "${PROCS}" "Unable to remove the prepared dnsmasq handoff marker lock."
		release_dns_handoff_lock
		return 1
	fi
	if [ -L "${DNS_HANDOFF_FILE}" ]; then
		logger -st "${PROCS}" "Refusing to replace a symbolic-link dnsmasq handoff marker."
		release_dns_handoff_lock
		return 1
	fi
	if dns_handoff_marker_is_active; then
		logger -st "${PROCS}" "Refusing to replace an active dnsmasq handoff marker."
		release_dns_handoff_lock
		return 1
	fi
	if [ -e "${DNS_HANDOFF_FILE}" ] && ! rm -f "${DNS_HANDOFF_FILE}"; then
		logger -st "${PROCS}" "Unable to remove a stale dnsmasq handoff marker."
		release_dns_handoff_lock
		return 1
	fi
	if ! (
		umask 077
		set -C
		printf '%s %s\n' "${_dns_handoff_owner_pid}" "${_dns_handoff_start_time}" >"${DNS_HANDOFF_FILE}"
	) 2>/dev/null; then
		logger -st "${PROCS}" "Unable to enable the dnsmasq port 553 handoff."
		release_dns_handoff_lock
		return 1
	fi
	if ! release_dns_handoff_lock; then
		logger -st "${PROCS}" "Unable to release the dnsmasq handoff marker lock."
		disable_dns_handoff
		return 1
	fi
	if ! service restart_dnsmasq >/dev/null 2>&1; then
		if disable_dns_handoff; then
			service restart_dnsmasq >/dev/null 2>&1 ||
				agh_log ERROR enable_dns_handoff "Unable to restore dnsmasq after the port 553 handoff failed."
		fi
		logger -st "${PROCS}" "Unable to regenerate dnsmasq configuration for the port 553 handoff."
		return 1
	fi
	return 0
}

adguardhome_config_valid() {
	[ -x "${WORK_DIR}/AdGuardHome" ] || return 1
	[ -f "${WORK_DIR}/AdGuardHome.yaml" ] || return 1
	"${WORK_DIR}/AdGuardHome" --check-config -c "${WORK_DIR}/AdGuardHome.yaml" --no-check-update -l /dev/null >/dev/null 2>&1
}

adguardhome_web_port() {
	_web_port="$(awk -F: '/^[[:space:]]*address:[[:space:]]*/ { print $NF; exit }' "${WORK_DIR}/AdGuardHome.yaml" 2>/dev/null | sed 's/[^0-9].*$//')"
	case "${_web_port}" in
		"" | *[!0-9]*) ;;
		*)
			[ "${_web_port}" -ge 1 ] 2>/dev/null && [ "${_web_port}" -le 65535 ] 2>/dev/null && {
				printf '%s\n' "${_web_port}"
				return 0
			}
			;;
	esac
	_web_port="$(awk -F= '/^ADGUARD_WEBUI_PORT=/ { print $2; exit }' "${WORK_DIR}/.config" 2>/dev/null | sed -e 's/^"//' -e 's/"$//')"
	case "${_web_port}" in
		"" | *[!0-9]*) return 1 ;;
	esac
	[ "${_web_port}" -ge 1 ] 2>/dev/null && [ "${_web_port}" -le 65535 ] 2>/dev/null || return 1
	printf '%s\n' "${_web_port}"
}

adguardhome_web_port_owned_status() {
	_web_port="$(adguardhome_web_port)" || return 1
	_socket_table="$(netstat -nlp 2>/dev/null)" || return 1
	printf '%s\n' "${_socket_table}" | awk -v proc="${PROCS}" -v port=":${_web_port}" '
		$0 ~ /^(tcp)6?[[:space:]]+/ && $0 ~ port "[[:space:]]" {
			bound = 1
			owner = ""
			for (field = 1; field <= NF; field++) {
				if ($field ~ /^[0-9]+\/[^[:space:]]+$/) {
					owner = $field
					break
				}
			}
			if (owner ~ "/" proc "$") owner_found = 1
			else if (owner != "") foreign_owner = 1
		}
		END {
			if (owner_found) exit 0
			if (bound && !foreign_owner) exit 2
			if (foreign_owner) exit 3
			exit 1
		}
	'
	return "$?"
}

adguardhome_web_port_available() {
	adguardhome_web_port_owned_status
	_web_status="$?"
	[ "${_web_status}" -eq 0 ] && return 0
	[ "${_web_status}" -eq 2 ] || return 1
	adguardhome_single_process_running
}

adguardhome_startup_checks_ready() {
	pidof "${PROCS}" >/dev/null 2>&1 || return 2
	adguardhome_web_port_owned_status
	_web_status="$?"
	if [ "${_web_status}" -eq 2 ]; then
		adguardhome_single_process_running || return 1
	elif [ "${_web_status}" -ne 0 ]; then
		[ "${_web_status}" -eq 3 ] && return 3
		return 1
	fi
	adguardhome_config_valid || return 4
	return 0
}

wait_for_adguardhome_startup_checks_failure_reason() {
	case "${1:-}" in
		2) printf '%s\n' "process exited before readiness completed" ;;
		3) printf '%s\n' "WebUI port is not owned by AdGuardHome" ;;
		4) printf '%s\n' "configuration validation failed" ;;
		*) printf '%s\n' "readiness checks failed" ;;
	esac
}

wait_for_adguardhome_startup_checks() {
	_startup_check_attempts="0"
	_startup_check_limit="$(dns_retry_limit "${ADGUARDHOME_STARTUP_CHECK_RETRIES:-}" 30)"
	agh_log INFO wait_for_adguardhome_startup_checks "Waiting for DNS/WebUI readiness checks."
	while [ "${_startup_check_attempts}" -lt "${_startup_check_limit}" ]; do
		adguardhome_startup_checks_ready
		_startup_check_status="$?"
		if [ "${_startup_check_status}" -eq 0 ]; then
			agh_log INFO wait_for_adguardhome_startup_checks "DNS/WebUI readiness checks passed after ${_startup_check_attempts} attempt(s)."
			return 0
		fi
		[ "${_startup_check_status}" -eq 2 ] && break
		_startup_check_attempts="$((_startup_check_attempts + 1))"
		if [ "${_startup_check_attempts}" = "5" ] || [ "${_startup_check_attempts}" = "15" ]; then
			agh_log INFO wait_for_adguardhome_startup_checks "Still waiting for DNS/WebUI readiness after ${_startup_check_attempts} attempt(s)."
		fi
		sleep 1s
	done
	adguardhome_startup_checks_ready
	_startup_check_status="$?"
	if [ "${_startup_check_status}" -eq 0 ]; then
		agh_log INFO wait_for_adguardhome_startup_checks "DNS/WebUI readiness checks passed on final check."
		return 0
	fi
	_startup_check_failure_reason="$(wait_for_adguardhome_startup_checks_failure_reason "${_startup_check_status}")"
	agh_log ERROR wait_for_adguardhome_startup_checks "DNS/WebUI readiness checks failed after ${_startup_check_attempts} attempt(s): ${_startup_check_failure_reason}."
	return 1
}

log_adguardhome_start_failure() {
	if ! pidof "${PROCS}" >/dev/null 2>&1; then
		agh_log ERROR log_adguardhome_start_failure "AdGuardHome startup failed: process is not running."
		return 0
	fi
	if ! adguardhome_owns_dns; then
		agh_log ERROR log_adguardhome_start_failure "AdGuardHome startup failed: process is running but DNS is not bound."
		return 0
	fi
	if ! adguardhome_web_port_available; then
		agh_log ERROR log_adguardhome_start_failure "AdGuardHome startup failed: WebUI port is unavailable."
		return 0
	fi
	if ! adguardhome_config_valid; then
		agh_log ERROR log_adguardhome_start_failure "AdGuardHome startup failed: configuration check failed."
		return 0
	fi
	return 1
}

dns_retry_limit() {
	case "$1" in
		"" | *[!0-9]*) printf '%s\n' "$2" ;;
		*) printf '%s\n' "$1" ;;
	esac
}

adguardhome_single_process_running() {
	_agh_pids="$(pidof "${PROCS}" 2>/dev/null)" || return 1
	set -- ${_agh_pids}
	[ "$#" -eq 1 ] || return 1
	case "${1:-}" in
		"" | *[!0-9]*) return 1 ;;
	esac
	return 0
}

adguardhome_owns_dns() {
	_dns_socket_table=""
	pidof "${PROCS}" >/dev/null 2>&1 || return 2
	_dns_socket_table="$(netstat -nlp 2>/dev/null)" || return 1
	printf '%s\n' "${_dns_socket_table}" | awk -v proc="${PROCS}" '
		$0 ~ /^(tcp)6?[[:space:]]+/ && $0 ~ /:53[[:space:]]/ {
			tcp_bound=1
			owner=""
			for (field = 1; field <= NF; field++) {
				if ($field ~ /^[0-9]+\/[^[:space:]]+$/) { owner=$field; break }
			}
			if (owner ~ "/" proc "$") tcp_owner=1; else if (owner != "") other_found=1
		}
		$0 ~ /^(udp)6?[[:space:]]+/ && $0 ~ /:53[[:space:]]/ {
			udp_bound=1
			owner=""
			for (field = 1; field <= NF; field++) {
				if ($field ~ /^[0-9]+\/[^[:space:]]+$/) { owner=$field; break }
			}
			if (owner ~ "/" proc "$") udp_owner=1; else if (owner != "") other_found=1
		}
		END {
			if (tcp_owner && udp_owner && !other_found) exit 0
			if (tcp_bound && udp_bound && !other_found) exit 2
			exit 1
		}
	'
	_dns_status="$?"
	[ "${_dns_status}" -eq 0 ] && return 0
	[ "${_dns_status}" -eq 2 ] || return 1
	adguardhome_single_process_running
}

dns_port_owner_command() {
	_dns_owner_pid="${1:-}"
	_dns_owner_command=""
	case "${_dns_owner_pid}" in
		"" | *[!0-9]*) return 1 ;;
	esac
	if [ -r "/proc/${_dns_owner_pid}/cmdline" ]; then
		_dns_owner_command="$(tr '\000' ' ' <"/proc/${_dns_owner_pid}/cmdline" 2>/dev/null)"
	fi
	if [ -z "${_dns_owner_command}" ]; then
		_dns_owner_command="$(ps 2>/dev/null | awk -v pid="${_dns_owner_pid}" '$1 == pid { $1=""; sub(/^[[:space:]]+/, ""); print; exit }')"
	fi
	[ -n "${_dns_owner_command}" ] || return 1
	printf '%s\n' "${_dns_owner_command}"
}

dns_port_owner_process_name() {
	_dns_owner_pid="${1:-}"
	_dns_owner_process_name=""
	case "${_dns_owner_pid}" in
		"" | *[!0-9]*) return 1 ;;
	esac
	if [ -r "/proc/${_dns_owner_pid}/comm" ]; then
		_dns_owner_process_name="$(sed 's/[[:space:]]*$//' "/proc/${_dns_owner_pid}/comm" 2>/dev/null)"
	fi
	if [ -z "${_dns_owner_process_name}" ]; then
		_dns_owner_process_name="$(ps 2>/dev/null | awk -v pid="${_dns_owner_pid}" '$1 == pid { print $5; exit }')"
	fi
	[ -n "${_dns_owner_process_name}" ] || return 1
	printf '%s\n' "${_dns_owner_process_name}"
}

dns_port_unknown_refusal_enabled() {
	_dns_refuse_unknown="${ADGUARDHOME_REFUSE_UNKNOWN_DNS_PORT_KILL:-}"
	if [ -z "${_dns_refuse_unknown}" ] && [ -f "${WORK_DIR}/.config" ]; then
		_dns_refuse_unknown="$(awk -F= '/^ADGUARDHOME_REFUSE_UNKNOWN_DNS_PORT_KILL=/ { print $2; exit }' "${WORK_DIR}/.config" 2>/dev/null | sed -e 's/^"//' -e 's/"$//')"
	fi
	[ -n "${_dns_refuse_unknown}" ] || _dns_refuse_unknown="1"
	case "${_dns_refuse_unknown}" in
		YES | yes | Yes | ON | on | On | TRUE | true | True | 1) return 0 ;;
		*) return 1 ;;
	esac
}

kill_dns_port_owners() {
	_dns_socket_table="$(netstat -nlp 2>/dev/null)" || return 1
	_dns_owner_actions="$(printf '%s\n' "${_dns_socket_table}" | awk -v proc="${PROCS}" '
		$0 ~ /^(tcp|udp)6?[[:space:]]+/ && $0 ~ /:53[[:space:]]/ && $0 !~ "/" proc "([[:space:]]|$)" {
			owner_pid = ""
			owner_name = "unknown"
			owner_key = ""
			owner_type = "unknown"
			for (field = 1; field <= NF; field++) {
				if ($field ~ /^[0-9]+\/[^[:space:]]+$/) {
					split($field, owner_parts, "/")
					owner_pid = owner_parts[1]
					owner_name = owner_parts[2]
					break
				}
			}
			if (owner_pid != "" && owner_pid <= 1) next
			if (owner_pid != "") owner_key = owner_pid
			else owner_key = "unavailable:" $1 ":" $4
			if (seen[owner_key]++) next
			if (owner_name == "dnsmasq") owner_type = "dnsmasq"
			if (owner_pid == "") owner_pid = "-"
			print owner_pid " " owner_type " " owner_name
		}
	')"
	[ -n "${_dns_owner_actions}" ] || return 0
	printf '%s\n' "${_dns_owner_actions}" | while read -r _dns_owner_pid _dns_owner_type _dns_owner_name; do
		[ -n "${_dns_owner_pid}" ] || continue
		_dns_owner_command=""
		_dns_owner_process_name=""
		case "${_dns_owner_pid}" in
			*[!0-9]* | "") ;;
			*)
				_dns_owner_process_name="$(dns_port_owner_process_name "${_dns_owner_pid}" 2>/dev/null)"
				_dns_owner_command="$(dns_port_owner_command "${_dns_owner_pid}" 2>/dev/null)"
				;;
		esac
		[ -n "${_dns_owner_process_name}" ] || _dns_owner_process_name="${_dns_owner_name:-unknown}"
		[ -n "${_dns_owner_command}" ] || _dns_owner_command="unavailable"
		case "${_dns_owner_type}" in
			dnsmasq)
				agh_log WARN kill_dns_port_owners "Escalating dnsmasq port 53 release for PID ${_dns_owner_pid}; netstat owner: ${_dns_owner_name}; process: ${_dns_owner_process_name}; command: ${_dns_owner_command}."
				;;
			*)
				agh_log ERROR kill_dns_port_owners "Port 53 is owned by unknown PID ${_dns_owner_pid}; netstat owner: ${_dns_owner_name:-unknown}; process: ${_dns_owner_process_name}; command: ${_dns_owner_command}."
				;;
		esac
	done
	_dns_unknown_owner="$(printf '%s\n' "${_dns_owner_actions}" | awk '$2 == "unknown" { found=1 } END { print found ? 1 : 0 }')"
	if dns_port_unknown_refusal_enabled; then
		_dns_refuse_unknown="1"
	else
		_dns_refuse_unknown="0"
	fi
	if [ "${_dns_unknown_owner}" = "1" ] && [ "${_dns_refuse_unknown}" = "1" ] && [ "${ADGUARDHOME_FORCE_DNS_PORT_KILL:-}" != "1" ]; then
		agh_log ERROR kill_dns_port_owners "Unknown port 53 owner detected after stopping dnsmasq; startup aborted because ADGUARDHOME_REFUSE_UNKNOWN_DNS_PORT_KILL=1. Set ADGUARDHOME_FORCE_DNS_PORT_KILL=1 to force termination."
		return 2
	fi
	_dns_unavailable_unknown_owner="$(printf '%s\n' "${_dns_owner_actions}" | awk '$1 == "-" && $2 == "unknown" { found=1 } END { print found ? 1 : 0 }')"
	if [ "${_dns_unavailable_unknown_owner}" = "1" ]; then
		agh_log ERROR kill_dns_port_owners "Unknown port 53 owner has no available PID; unable to terminate it safely."
		return 2
	fi
	_dns_pids="$(printf '%s\n' "${_dns_owner_actions}" | awk -v refuse="${_dns_refuse_unknown}" -v force="${ADGUARDHOME_FORCE_DNS_PORT_KILL:-0}" '$1 ~ /^[0-9]+$/ && ($2 == "dnsmasq" || force == "1" || refuse != "1") { printf "%s%s", sep, $1; sep=" " }')"
	if [ "${_dns_unknown_owner}" = "1" ] && [ "${ADGUARDHOME_FORCE_DNS_PORT_KILL:-}" = "1" ]; then
		agh_log WARN kill_dns_port_owners "ADGUARDHOME_FORCE_DNS_PORT_KILL=1 set; terminating unknown port 53 owner(s)."
	elif [ "${_dns_unknown_owner}" = "1" ] && [ "${_dns_refuse_unknown}" != "1" ]; then
		agh_log WARN kill_dns_port_owners "Legacy DNS port behavior active; terminating unknown port 53 owner(s). Set ADGUARDHOME_REFUSE_UNKNOWN_DNS_PORT_KILL=1 to abort instead."
	fi
	[ -n "${_dns_pids}" ] || return 0
	# Intentional word splitting: awk returns a whitespace-delimited PID list.
	if kill -s 9 ${_dns_pids} 2>/dev/null; then
		return 0
	fi
	# The normal dnsmasq stop can race with escalation: netstat may report a
	# dnsmasq PID that exits before kill(1) signals it. Treat that stale-PID
	# failure as success if the DNS port is now free.
	if dns_port_available; then
		agh_log INFO kill_dns_port_owners "Port 53 is free after a failed owner kill; continuing after stale PID race."
		return 0
	fi
	return 1
}

dns_port_available() {
	_dns_socket_table="$(netstat -nlp 2>/dev/null)" || return 1
	printf '%s\n' "${_dns_socket_table}" | awk -v proc="${PROCS}" '
		$0 ~ /^(tcp|udp)6?[[:space:]]+/ && $0 ~ /:53[[:space:]]/ {
			bound = 1
			owner = ""
			for (field = 1; field <= NF; field++) {
				if ($field ~ /^[0-9]+\/[^[:space:]]+$/) {
					owner = $field
					break
				}
			}
			if (owner ~ "/" proc "$") owner_found = 1
			else if (owner != "") foreign_owner = 1
		}
		END {
			if (foreign_owner) exit 1
			if (owner_found) exit 0
			if (bound) exit 2
			exit 0
		}
	'
	_dns_status="$?"
	[ "${_dns_status}" -eq 0 ] && return 0
	[ "${_dns_status}" -eq 2 ] || return 1
	adguardhome_single_process_running
}

dns_port_has_foreign_owner() {
	_dns_socket_table="$(netstat -nlp 2>/dev/null)" || return 1
	printf '%s\n' "${_dns_socket_table}" | awk -v proc="${PROCS}" '
		$0 ~ /^(tcp|udp)6?[[:space:]]+/ && $0 ~ /:53[[:space:]]/ {
			for (field = 1; field <= NF; field++) {
				if ($field ~ /^[0-9]+\/[^[:space:]]+$/) {
					if ($field !~ "/" proc "$") foreign_owner = 1
					break
				}
			}
		}
		END { exit(foreign_owner ? 0 : 1) }
	'
}

dns_port_needs_release() {
	_dns_socket_table="$(netstat -nlp 2>/dev/null)" || return 1
	printf '%s\n' "${_dns_socket_table}" | awk -v proc="${PROCS}" '
		$0 ~ /^(tcp|udp)6?[[:space:]]+/ && $0 ~ /:53[[:space:]]/ {
			bound = 1
			owner = ""
			for (field = 1; field <= NF; field++) {
				if ($field ~ /^[0-9]+\/[^[:space:]]+$/) {
					owner = $field
					break
				}
			}
			if (owner ~ "/" proc "$") agh_owner = 1
			else if (owner != "") foreign_owner = 1
		}
		END {
			if (foreign_owner) exit 0
			if (bound && !agh_owner) exit 2
			exit 1
		}
	'
	_dns_status="$?"
	[ "${_dns_status}" -eq 0 ] && return 0
	[ "${_dns_status}" -eq 2 ] || return 1
	adguardhome_single_process_running && return 1
	return 0
}

release_dns_port_from_dnsmasq() {
	_dns_release_context="${1:-release_dns_port_from_dnsmasq}"
	agh_log INFO "${_dns_release_context}" "Stopping dnsmasq normally before checking port 53 ownership."
	service stop_dnsmasq >/dev/null 2>&1
	if dns_port_available; then
		agh_log INFO "${_dns_release_context}" "Port 53 released after stopping dnsmasq."
		return 0
	fi
	kill_dns_port_owners
}

dns_guard_wait_for_stop() {
	_dns_guard_wait_fifo="${DNS_HANDOFF_DIR}/guard-wait.$$"
	rm -f "${_dns_guard_wait_fifo}" 2>/dev/null
	if mkfifo "${_dns_guard_wait_fifo}" 2>/dev/null; then
		if exec 3<>"${_dns_guard_wait_fifo}"; then
			rm -f "${_dns_guard_wait_fifo}" 2>/dev/null
			# Block until stop_dns_port_guard() sends a trapped signal.
			# This avoids a repeated sleep loop while keeping the guard alive.
			while :; do
				IFS= read -r _dns_guard_wait_line <&3
			done
		fi
	fi
	rm -f "${_dns_guard_wait_fifo}" 2>/dev/null
	# Fallback only if FIFO setup fails. Avoid a hot CPU loop.
	while :; do
		sleep 1s
	done
}

stop_dns_port_guard() {
	[ -n "${ADGUARDHOME_DNS_GUARD_PID:-}" ] || return 0
	agh_log INFO stop_dns_port_guard "Stopping DNS port guard PID ${ADGUARDHOME_DNS_GUARD_PID}."
	kill "${ADGUARDHOME_DNS_GUARD_PID}" >/dev/null 2>&1
	wait "${ADGUARDHOME_DNS_GUARD_PID}" 2>/dev/null
	unset ADGUARDHOME_DNS_GUARD_PID
	agh_log INFO stop_dns_port_guard "DNS port guard stopped."
	return 0
}

log_adguardhome_dns_wait_failure() {
	if ! pidof "${PROCS}" >/dev/null 2>&1; then
		agh_log ERROR log_adguardhome_dns_wait_failure "AdGuardHome did not acquire DNS: AdGuardHome process is missing."
		return 0
	fi
	_dns_socket_table="$(netstat -nlp 2>/dev/null)" || {
		agh_log ERROR log_adguardhome_dns_wait_failure "AdGuardHome did not acquire DNS: unable to inspect port 53 ownership."
		return 0
	}
	_dns_wait_failure="$(printf '%s\n' "${_dns_socket_table}" | awk -v proc="${PROCS}" '
		$0 ~ /^(tcp)6?[[:space:]]+/ && $0 ~ /:53[[:space:]]/ {
			tcp_bound=1
			owner=""
			for (field = 1; field <= NF; field++) {
				if ($field ~ /^[0-9]+\/[^[:space:]]+$/) { owner=$field; break }
			}
			if (owner ~ "/" proc "$") tcp_found=1; else if (owner != "") other_found=1
		}
		$0 ~ /^(udp)6?[[:space:]]+/ && $0 ~ /:53[[:space:]]/ {
			udp_bound=1
			owner=""
			for (field = 1; field <= NF; field++) {
				if ($field ~ /^[0-9]+\/[^[:space:]]+$/) { owner=$field; break }
			}
			if (owner ~ "/" proc "$") udp_found=1; else if (owner != "") other_found=1
		}
		END {
			if (other_found) print "another process still owns port 53"
			else if (!tcp_bound) print "TCP port 53 is not bound"
			else if (!udp_bound) print "UDP port 53 is not bound"
			else if (!tcp_found || !udp_found) print "port 53 is bound without process ownership and fallback readiness was not accepted"
			else print "unknown DNS ownership state"
		}
	')"
	agh_log ERROR log_adguardhome_dns_wait_failure "AdGuardHome did not acquire DNS: ${_dns_wait_failure}."
}

wait_for_adguardhome_dns() {
	_dns_wait_attempts="0"
	_dns_wait_limit="$(dns_retry_limit "${ADGUARDHOME_DNS_WAIT_RETRIES:-}" 30)"
	agh_log INFO wait_for_adguardhome_dns "Waiting for AdGuardHome to bind DNS port 53."
	while [ "${_dns_wait_attempts}" -lt "${_dns_wait_limit}" ]; do
		adguardhome_owns_dns
		_dns_wait_status="$?"
		if [ "${_dns_wait_status}" -eq 0 ]; then
			agh_log INFO wait_for_adguardhome_dns "AdGuardHome owns DNS port 53 after ${_dns_wait_attempts} attempt(s)."
			return 0
		fi
		[ "${_dns_wait_status}" -eq 2 ] && break
		_dns_wait_attempts="$((_dns_wait_attempts + 1))"
		if [ "${_dns_wait_attempts}" = "5" ] || [ "${_dns_wait_attempts}" = "15" ]; then
			agh_log INFO wait_for_adguardhome_dns "Still waiting for DNS port 53 after ${_dns_wait_attempts} attempt(s)."
		fi
		sleep 1s
	done
	if adguardhome_owns_dns; then
		agh_log INFO wait_for_adguardhome_dns "AdGuardHome owns DNS port 53 on final check."
		return 0
	fi
	log_adguardhome_dns_wait_failure
	return 1
}

start_dns_port_guard() {
	_dns_guard_attempts="0"
	_dns_guard_limit="$(dns_retry_limit "${ADGUARDHOME_DNS_GUARD_RETRIES:-}" 10)"
	DNS_WATCHDOG_TRAP_FILE=""
	agh_log INFO start_dns_port_guard "Starting DNS port guard."
	if ! save_dns_watchdog_traps guard; then
		agh_log ERROR start_dns_port_guard "Unable to preserve DNS guard traps; guard startup aborted."
		return 1
	fi
	_dns_guard_saved_traps="${DNS_WATCHDOG_TRAP_FILE}"
	# Install cleanup before the first watchdog STOP so a parent-side shutdown
	# cannot terminate us during retry sleep without resuming the watchdog.
	trap 'trap - HUP INT TERM EXIT; exec 3<&- 3>&- 2>/dev/null; rm -f "${_dns_guard_wait_fifo:-}"; resume_dns_watchdog; restore_dns_watchdog_traps "${_dns_guard_saved_traps}"; exit 0' HUP INT TERM EXIT
	while [ "${_dns_guard_attempts}" -lt "${_dns_guard_limit}" ]; do
		if adguardhome_owns_dns; then
			break
		fi
		if dns_port_has_foreign_owner; then
			if [ "${_dns_guard_attempts}" = "0" ]; then
				agh_log INFO start_dns_port_guard "Stopping dnsmasq/releasing port 53 while guard is active."
			fi
			release_dns_port_from_dnsmasq start_dns_port_guard || return 1
		fi
		suspend_dns_watchdog
		_dns_guard_attempts="$((_dns_guard_attempts + 1))"
		sleep 1s
	done
	agh_log INFO start_dns_port_guard "DNS port guard is armed after ${_dns_guard_attempts} attempt(s)."
	dns_guard_wait_for_stop
}

# Service hook helpers

abort_pre_start_adguardhome() {
	_dns_watchdog_abort_saved_traps="${1:-}"
	trap - HUP INT TERM EXIT
	post_start_failure_adguardhome
	restore_dns_watchdog_traps "${_dns_watchdog_abort_saved_traps}"
	exit 1
}

post_start_adguardhome() {
	agh_log INFO post_start_adguardhome "AdGuardHome process started; waiting for DNS readiness."
	if ! wait_for_adguardhome_dns; then
		stop_dns_port_guard
		disable_dns_handoff || agh_log ERROR post_start_adguardhome "The dnsmasq port 553 handoff marker requires manual cleanup."
		log_adguardhome_start_failure
		return 1
	fi
	stop_dns_port_guard
	# The guard normally resumes the watchdog from its TERM/EXIT trap, but
	# AdGuardHome can acquire port 53 before the guard finishes arming it.
	# Resume parent-side after reaping the guard so a fast success cannot leave
	# the router watchdog suspended.
	resume_dns_watchdog
	if ! wait_for_adguardhome_startup_checks; then
		post_start_failure_adguardhome
		log_adguardhome_start_failure
		return 1
	fi
	disable_dns_handoff || return 1
	if [ "${ADGUARDHOME_SKIP_DNSMASQ_RESTART:-}" != "1" ]; then
		agh_log INFO post_start_adguardhome "Restarting dnsmasq after successful AdGuardHome startup."
		service restart_dnsmasq >/dev/null 2>&1 || {
			agh_log ERROR post_start_adguardhome "Unable to restart dnsmasq after successful AdGuardHome startup."
			return 1
		}
		agh_log INFO post_start_adguardhome "dnsmasq restarted after successful AdGuardHome startup."
	fi
	agh_log INFO post_start_adguardhome "AdGuardHome startup completed."
	return 0
}

post_start_failure_adguardhome() {
	_dns_recovery_status="0"
	agh_log INFO post_start_failure_adguardhome "Running AdGuardHome startup failure recovery."
	stop_dns_port_guard
	resume_dns_watchdog
	if ! disable_dns_handoff; then
		agh_log ERROR post_start_failure_adguardhome "Unable to disable dnsmasq handoff during failure recovery."
		_dns_recovery_status="1"
	fi
	if [ "${ADGUARDHOME_SKIP_DNSMASQ_RESTART:-}" != "1" ]; then
		agh_log INFO post_start_failure_adguardhome "Restarting dnsmasq after AdGuardHome startup failure."
		if ! service restart_dnsmasq >/dev/null 2>&1; then
			agh_log ERROR post_start_failure_adguardhome "Unable to restart dnsmasq after AdGuardHome startup failure."
			_dns_recovery_status="1"
		else
			agh_log INFO post_start_failure_adguardhome "dnsmasq restarted after AdGuardHome startup failure."
		fi
	fi
	return "${_dns_recovery_status}"
}

adguardhome_yaml_ipset_file() {
	[ -f "${WORK_DIR}/AdGuardHome.yaml" ] || 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
		}
	' "${WORK_DIR}/AdGuardHome.yaml"
}

chmod_regular_files_600() {
	_chmod_file_dir="$1"

	[ -d "${_chmod_file_dir}" ] || return 0
	[ ! -L "${_chmod_file_dir}" ] || return 0

	find "${_chmod_file_dir}" -exec sh -c '
		[ ! -L "$1" ] || exit 0
		[ -f "$1" ] || exit 0
		chmod 600 "$1"
	' sh {} \;
}

ensure_adguardhome_work_dir_permissions() {
	_agh_perm_http_username=""
	_agh_perm_ipset_file=""
	_agh_perm_path=""
	if [ ! -d "${WORK_DIR}" ]; then
		logger -st "${PROCS}" "Missing AdGuardHome work directory ${WORK_DIR}."
		return 1
	fi
	_agh_perm_http_username="$(nvram get http_username 2>/dev/null)"
	[ -n "${_agh_perm_http_username}" ] || _agh_perm_http_username="root"
	if ! chown "${_agh_perm_http_username}:root" "${WORK_DIR}"; then
		logger -st "${PROCS}" "Unable to set ownership on ${WORK_DIR}."
		return 1
	fi
	for _agh_perm_path in "${WORK_DIR}"/*; do
		[ ! -L "${_agh_perm_path}" ] || continue
		[ -f "${_agh_perm_path}" ] || [ -d "${_agh_perm_path}" ] || continue
		if ! chown "${_agh_perm_http_username}:root" "${_agh_perm_path}"; then
			logger -st "${PROCS}" "Unable to set ownership on ${_agh_perm_path}."
			return 1
		fi
	done
	if ! chmod 700 "${WORK_DIR}" || ! find "${WORK_DIR}" -exec sh -c '[ ! -L "$1" ] || exit 0; [ -d "$1" ] || exit 0; chmod 700 "$1"' sh {} \;; then
		logger -st "${PROCS}" "Unable to set directory permissions for ${WORK_DIR}."
		return 1
	fi
	if [ -f "${WORK_DIR}/AdGuardHome.yaml" ] && ! chmod 600 "${WORK_DIR}/AdGuardHome.yaml"; then
		logger -st "${PROCS}" "Unable to set configuration permissions on ${WORK_DIR}/AdGuardHome.yaml."
		return 1
	fi
	if ! chmod_regular_files_600 "${WORK_DIR}/data"; then
		logger -st "${PROCS}" "Unable to set data file permissions for ${WORK_DIR}/data."
		return 1
	fi
	if ! chmod_regular_files_600 "${WORK_DIR}/data/filters"; then
		logger -st "${PROCS}" "Unable to set filter file permissions for ${WORK_DIR}/data/filters."
		return 1
	fi
	for _agh_perm_ipset_file in "${WORK_DIR}/ipset.conf" "${WORK_DIR}/ipset.user" "$(adguardhome_yaml_ipset_file)"; do
		[ -n "${_agh_perm_ipset_file}" ] || continue
		case "${_agh_perm_ipset_file}" in
			../* | */../* | */.. | ..) continue ;;
		esac
		case "${_agh_perm_ipset_file}" in
			/*)
				case "${_agh_perm_ipset_file}" in
					"${WORK_DIR}"/*) ;;
					*) continue ;;
				esac
				;;
			*) _agh_perm_ipset_file="${WORK_DIR}/${_agh_perm_ipset_file}" ;;
		esac
		[ ! -L "${_agh_perm_ipset_file}" ] || continue
		[ -f "${_agh_perm_ipset_file}" ] || continue
		if ! chown "${_agh_perm_http_username}:root" "${_agh_perm_ipset_file}"; then
			logger -st "${PROCS}" "Unable to set ownership on ${_agh_perm_ipset_file}."
			return 1
		fi
		if ! chmod 644 "${_agh_perm_ipset_file}"; then
			logger -st "${PROCS}" "Unable to set IPSET permissions on ${_agh_perm_ipset_file}."
			return 1
		fi
	done
	if [ -f "${WORK_DIR}/AdGuardHome" ] && ! chmod 755 "${WORK_DIR}/AdGuardHome"; then
		logger -st "${PROCS}" "Unable to set executable permissions on ${WORK_DIR}/AdGuardHome."
		return 1
	fi
	return 0
}

pre_start_adguardhome() {
	_dns_stop_attempts="0"
	_dns_stop_limit="$(dns_retry_limit "${ADGUARDHOME_DNSMASQ_STOP_RETRIES:-}" 10)"
	agh_log INFO pre_start_adguardhome "Validating AdGuardHome configuration and permissions."
	ensure_adguardhome_work_dir_permissions || return 1
	adguardhome_config_valid || {
		agh_log ERROR pre_start_adguardhome "AdGuardHome configuration validation failed; restoring dnsmasq."
		post_start_failure_adguardhome
		return 1
	}
	agh_log INFO pre_start_adguardhome "AdGuardHome configuration and permissions validation completed."
	dns_handoff_dependencies_available || return 1
	agh_log INFO pre_start_adguardhome "Preparing DNS handoff."
	enable_dns_handoff || {
		agh_log ERROR pre_start_adguardhome "DNS handoff preparation failed."
		return 1
	}
	agh_log INFO pre_start_adguardhome "DNS handoff prepared."
	# Keep Go memory behavior controlled by PREARGS runtime settings, not hard shell memory caps.
	ulimit -s 8192 2>/dev/null || true
	if [ "$(pidof syslog-ng logrotate | wc -w)" -gt 0 ] && which scribe >/dev/null 2>&1; then
		scribe restart
	fi
	DNS_WATCHDOG_TRAP_FILE=""
	if ! save_dns_watchdog_traps pre; then
		agh_log ERROR pre_start_adguardhome "Unable to preserve startup traps; startup aborted."
		post_start_failure_adguardhome
		return 1
	fi
	_dns_watchdog_saved_traps="${DNS_WATCHDOG_TRAP_FILE}"
	agh_log INFO pre_start_adguardhome "Stopping dnsmasq/releasing port 53 before starting AdGuardHome."
	while [ "${_dns_stop_attempts}" -lt "${_dns_stop_limit}" ]; do
		if dns_port_available; then
			start_dns_port_guard >/dev/null 2>&1 &
			ADGUARDHOME_DNS_GUARD_PID="$!"
			agh_log INFO pre_start_adguardhome "Port 53 released; starting AdGuardHome."
			restore_dns_watchdog_traps "${_dns_watchdog_saved_traps}"
			return 0
		fi
		if dns_port_needs_release; then
			if ! release_dns_port_from_dnsmasq pre_start_adguardhome; then
				agh_log ERROR pre_start_adguardhome "Unable to safely release port 53; startup aborted."
				post_start_failure_adguardhome
				restore_dns_watchdog_traps "${_dns_watchdog_saved_traps}"
				return 1
			fi
		fi
		# The DNS guard is not running yet, so protect this parent-side
		# watchdog suspension against aborts until the guard takes over.
		trap 'abort_pre_start_adguardhome "${_dns_watchdog_saved_traps}"' HUP INT TERM EXIT
		suspend_dns_watchdog
		_dns_stop_attempts="$((_dns_stop_attempts + 1))"
		if [ "${_dns_stop_attempts}" = "5" ]; then
			agh_log INFO pre_start_adguardhome "Still waiting for port 53 release after ${_dns_stop_attempts} attempt(s)."
		fi
		sleep 1s
	done
	agh_log ERROR pre_start_adguardhome "Unable to release port 53 after ${_dns_stop_limit} attempt(s); startup aborted."
	post_start_failure_adguardhome
	restore_dns_watchdog_traps "${_dns_watchdog_saved_traps}"
	return 1
}

PRECMD="pre_start_adguardhome"
POSTCMD="post_start_adguardhome"
POSTFAILCMD="post_start_failure_adguardhome"
DESC="${PROCS}"
TZ="$(sed -n '1p' /etc/TZ 2>/dev/null)"
export TZ
case "${1:-}" in
	"start" | "restart" | "reload")
		ensure_adguardhome_work_dir_permissions || exit 1
		;;
esac
[ -z "${SCRIPT_LOC}" ] && . /jffs/addons/AdGuardHome.d/AdGuardHome.sh
