#!/bin/sh
#
# Copyright (C) 2018-2025 Ycarus (Yannick Chabanois) <ycarus@zugaina.org> for OpenMPTCProuter
#
# This is free software, licensed under the GNU General Public License v2.
# See /LICENSE for more information.
#
# This script check services status and do appropriate actions

# Snapshot the config dump once instead of re-running `uci show
# openmptcprouter` for every substring test below (each dump forks uci and
# reserializes the whole config). Refreshed after the get_config commits,
# whose result the later tests must see. Read through the /var/state view:
# token_error, admin_error and set_firewall, tested below, are runtime
# state (see omr-state.sh), which a plain "uci show" doesn't include.
_omr_snapshot="$(uci -q -P /var/state show openmptcprouter)"
_omr_has() {
	case "$_omr_snapshot" in
		*"$1"*) return 0;;
		*) return 1;;
	esac
}

# The shipped shadowsocks configs give every server section a placeholder
# address and an empty credential: nothing answers there, so such a section
# is "not provisioned yet" and must not be treated as a proxy worth
# restarting or worth tracking. The current placeholder is 127.0.0.1;
# 192.168.1.3 is the one older releases shipped, still recognised for configs
# that were never provisioned before an upgrade. Same set as the shadowsocks
# init scripts, 021-proxy and the openmptcprouter rpcd API.
_ss_server_configured() {  # _ss_server_configured <package> <section>
	case "$(uci -q get "$1.$2.server")" in
		""|127.0.0.1|::1|192.168.1.3) return 1 ;;
	esac
	return 0
}

# _prog_name <uci option holding a binary path> <default name>: print the
# process name procd gives the daemon, i.e. the basename of the configured
# binary, falling back to <default name> when the option is unset. Used with
# `pgrep -x` so a watchdog test can't match its own tracker or this script.
_prog_name() {
	local _p
	_p="$(uci -q get "$1")"
	_p="${_p##*/}"
	printf '%s\n' "${_p:-$2}"
}

# openmptcprouter-vps keeps its errors per server in /var/state (admin_error,
# token_error, set_firewall). They used to be tested as substrings of the whole
# dump: one server in error held back the firewall push of every other server
# and restarted openmptcprouter-vps each minute, logging in to every VPS each
# time (a VPS fail2ban then bans the router). A disabled server is never
# visited by openmptcprouter-vps, so a flag left on it was never cleared and
# that went on until a reboot. _vps_scan_server looks at one server and sorts
# it into:
#   _vps_active       servers openmptcprouter-vps really talks to
#   _vps_admin_err    ...of them, the ones whose last exchange failed
#   _vps_token_err    ...the ones LuCI could not get a token from
#   _vps_fw           ...the ones with a firewall push pending, admin_error aside
#   _vps_get_config   1 if one of them asks for its config
#   _vps_has_get_config 1 if one of them has the get_config option at all
_vps_scan_server() {
	local _s="$1" _v _o
	config_get _v "$_s" disabled
	if [ "$_v" = "1" ]; then
		for _v in admin_error token_error set_firewall; do
			_omr_has "openmptcprouter.${_s}.${_v}=" && uci -q -P /var/state revert "openmptcprouter.${_s}.${_v}"
		done
		return
	fi
	# the same servers openmptcprouter-vps' _get_token logs in to
	for _o in username password port ip; do
		config_get _v "$_s" "$_o"
		[ -n "$_v" ] || return
	done
	_vps_active="$_vps_active $_s"
	_omr_has "openmptcprouter.${_s}.get_config=" && _vps_has_get_config=1
	_omr_has "openmptcprouter.${_s}.get_config='1'" && _vps_get_config=1
	_omr_has "openmptcprouter.${_s}.token_error='1'" && _vps_token_err="$_vps_token_err $_s"
	if _omr_has "openmptcprouter.${_s}.admin_error='1'"; then
		_vps_admin_err="$_vps_admin_err $_s"
	elif _omr_has "openmptcprouter.${_s}.set_firewall='1'"; then
		_vps_fw="$_vps_fw $_s"
	fi
}

# Back-off for a retry that keeps failing (an unreachable VPS, a wrong
# password): the first try is immediate, then 1, 2, 4... up to 30 min apart.
# openmptcprouter.settings.vps_<what>_lc/_delay in /var/state hold the last
# try and the current delay; _vps_retry_reset drops them once it worked.
_vps_retry_due() {
	local _lc _delay
	_lc="$(uci -q -P /var/state get openmptcprouter.settings.vps_${1}_lc)"
	[ -n "$_lc" ] || return 0
	_delay="$(uci -q -P /var/state get openmptcprouter.settings.vps_${1}_delay)"
	# a few seconds of slack: the schedule runs every ~60s, not every 60s
	[ $(($(date +%s) - _lc)) -ge $((${_delay:-60} - 5)) ]
}
_vps_retry_next() {
	local _delay
	_delay="$(uci -q -P /var/state get openmptcprouter.settings.vps_${1}_delay)"
	if [ -z "$(uci -q -P /var/state get openmptcprouter.settings.vps_${1}_lc)" ]; then
		_delay=60
	else
		_delay=$((${_delay:-60} * 2))
		[ "$_delay" -gt 1800 ] && _delay=1800
	fi
	uci -q -P /var/state batch <<-EOF >/dev/null
		revert openmptcprouter.settings.vps_${1}_lc
		set openmptcprouter.settings.vps_${1}_lc=$(date +%s)
		revert openmptcprouter.settings.vps_${1}_delay
		set openmptcprouter.settings.vps_${1}_delay=${_delay}
	EOF
}
_vps_retry_reset() {
	_omr_has "openmptcprouter.settings.vps_${1}_lc=" || return 0
	uci -q -P /var/state batch <<-EOF >/dev/null
		revert openmptcprouter.settings.vps_${1}_lc
		revert openmptcprouter.settings.vps_${1}_delay
	EOF
}

# Whether openmptcprouter-vps's set_vxlan is waiting before its next try on
# the server it would act on (the current or master one): its marker
# /tmp/.omr-vps-vxlan-<server> holds "<last try> <delay> <wanted setting>",
# and a change of the setting makes it try at once.
_vxlan_backing_off() {
	local marker server last delay wanted want now
	want="$(uci -q get openmptcprouter.settings.vxlan)"
	[ "$want" = "1" ] || want=0
	now=$(date +%s)
	for marker in /tmp/.omr-vps-vxlan-*; do
		[ -f "$marker" ] || continue
		server="${marker#/tmp/.omr-vps-vxlan-}"
		[ "$(uci -q -P /var/state get "openmptcprouter.${server}.current")" = "1" ] ||
			[ "$(uci -q get "openmptcprouter.${server}.master")" = "1" ] || continue
		last="" delay="" wanted=""
		read -r last delay wanted < "$marker" 2>/dev/null
		case "$last$delay" in ''|*[!0-9]*) continue ;; esac
		[ "$wanted" = "$want" ] && [ $((now - last)) -lt "$delay" ] && return 0
	done
	return 1
}

# _proxy_restart <init script> <name>: restart a proxy procd gave up on. One
# that dies again at once (a port in use, a config it refuses) was restarted
# every minute, and each restart runs fw4 three times, which empties the sets
# dnsmasq fills for the bypassed domains. The delay doubles from 1 minute to
# 30, and starts over once the proxy is seen running.
_proxy_restart() {
	local state="/tmp/omr-$1-restart" last="" delay="" now
	now=$(date +%s)
	[ -f "$state" ] && read -r last delay < "$state" 2>/dev/null
	case "$last$delay" in
		''|*[!0-9]*) last=0 delay=30 ;;
	esac
	[ $((now - last)) -lt $((delay - 5)) ] && return 0
	delay=$((delay * 2))
	[ "$delay" -gt 1800 ] && delay=1800
	echo "$now $delay" > "$state"
	_log "Can't find $2, restart it (if it dies again, next try in $((delay / 60)) min)..."
	"/etc/init.d/$1" restart >/dev/null 2>&1
	sleep 5
}

# Token retry, openmptcprouter-vps restart and firewall push, from the flags
# of the enabled servers only
_vps_watchdog() {
	_vps_active="" _vps_admin_err="" _vps_token_err="" _vps_fw=""
	_vps_get_config=0 _vps_has_get_config=0
	config_load openmptcprouter
	config_foreach _vps_scan_server server
	[ -n "$_vps_active" ] || return 0

	if [ -n "$_vps_token_err" ]; then
		if [ "$(pgrep -f openmptcprouter-vps)" = "" ] && _vps_retry_due token; then
			_log "No token for server(s)${_vps_token_err}, ask for one..."
			_vps_retry_next token
			/etc/init.d/openmptcprouter-vps token >/dev/null 2>&1 &
			sleep 5
		fi
	else
		_vps_retry_reset token
	fi
	if [ "$_vps_has_get_config" = "0" ] || [ "$_vps_get_config" = "1" ] || [ -n "$_vps_admin_err" ]; then
		if [ "$(pgrep -f openmptcprouter-vps)" = "" ] && _vps_retry_due restart; then
			[ -n "$_vps_admin_err" ] && _log "Error with server(s)${_vps_admin_err}, restart openmptcprouter-vps..."
			_vps_retry_next restart
			/etc/init.d/openmptcprouter-vps restart >/dev/null 2>&1 &
			sleep 5
		fi
	else
		_vps_retry_reset restart
	fi
	#if [ "$(uci -q show openmptcprouter | grep server)" != "" ] && [ "$(uci -q show openmptcprouter | grep password)" != "" ] && [ "$(pgrep -f openmptcprouter-vps)" = "" ] && [ "$(uci -q show openmptcprouter | grep admin_error=\'1\')" = "" ] && ([ "$(uci -q show openmptcprouter | grep set_firewall=\'1\')" != "" ] || (([ -e /usr/sbin/iptables-nft-save ] && [ -z "$(iptables-save 2>/dev/null | grep omr_dst_bypass_${OMR_TRACKER_DEVICE})" ]) || [ -z "$(iptables-save 2>/dev/null | grep omr_dst_bypass_${OMR_TRACKER_DEVICE})" ])); then
	# A server in error is left out until openmptcprouter-vps got through to it
	if [ -n "$_vps_fw" ] && [ "$(pgrep -f set_vps_firewall)" = "" ] && [ "$(pgrep -f openmptcprouter-vps)" = "" ]; then
		local _s
		for _s in $_vps_fw; do
			_log "Set firewall on server $_s"
			/etc/init.d/openmptcprouter-vps set_vps_firewall $_s >/dev/null 2>&1
		done

	#	run_fw_include() {
	#		[ -n "$1" ] && [ "$(uci -q get firewall.$1.reload)" = "0" ] && sh $(uci -q get firewall.$1.path) >/dev/null 2>&1
	#	}
	#	config_load firewall
	#	config_foreach run_fw_include include

		#/etc/init.d/shadowsocks-libev rules_up >/dev/null 2>&1
		#/etc/init.d/v2ray rules_up >/dev/null 2>&1
		#/etc/init.d/omr-bypass reload_rules >/dev/null 2>&1
		#sh /etc/firewall.gre-tunnel >/dev/null 2>&1
		/bin/blocklanfw >/dev/null 2>&1
		sleep 5
	fi
}

if [ -n "$(printf '%s\n' "$_omr_snapshot" | awk -F= '/password/ { gsub("\047","",$2);print $2 }')" ]; then
	# If a service is down, force restart it
	omr_schedule_vpn_setting="$(uci -q get openmptcprouter.settings.vpn)"
	if [ -f /etc/init.d/shadowsocks-libev ] && [ "$(pgrep -f omr-tracker-ss)" = "" ] && [ "$(pgrep -f ss-redir)" = "" ] && [ "$(pgrep -f ss-local)" = "" ] && [ "$(uci -q get shadowsocks-libev.sss0.disabled)" != "1" ] && _ss_server_configured shadowsocks-libev sss0 && [ "$(uci -q get shadowsocks-libev.sss0.key)" != "" ]; then
		_log "Can't find Shadowsocks, restart it..."
		/etc/init.d/shadowsocks-libev restart >/dev/null 2>&1
		sleep 5
	fi
	if [ -f /etc/init.d/shadowsocks-rust ] && [ "$(pgrep -f omr-tracker-ss)" = "" ] && [ "$(pgrep -f sslocal)" = "" ] && [ "$(uci -q get shadowsocks-rust.sss0.disabled)" != "1" ] && _ss_server_configured shadowsocks-rust sss0 && [ "$(uci -q get shadowsocks-rust.sss0.password)" != "" ]; then
		_log "Can't find Shadowsocks Rust, restart it..."
		/etc/init.d/shadowsocks-rust restart >/dev/null 2>&1
		sleep 5
	fi
	if [ -f /etc/init.d/glorytun ] && [ "$(pgrep glorytun)" = "" ] && [ "$(uci -q get glorytun.vpn.enable)" = "1" ] && [ "$(uci -q get glorytun.vpn.key)" != "" ] && [ "$omr_schedule_vpn_setting" != "none" ]; then
		# Shared restart cooldown with omr-tracker's 002-error hook (see the
		# MQVPN watchdog further below for the full rationale)
		glorytun_last_restart="$(cat /tmp/omr-glorytun-last-restart 2>/dev/null)"
		[ -z "$glorytun_last_restart" ] && glorytun_last_restart=0
		glorytun_now="$(date +%s)"
		if [ "$((glorytun_now - glorytun_last_restart))" -ge 60 ]; then
			_log "Can't find Glorytun, restart it..."
			echo "$glorytun_now" > /tmp/omr-glorytun-last-restart
			/etc/init.d/glorytun restart >/dev/null 2>&1
			sleep 5
		else
			_log "Can't find Glorytun, restart skipped (already restarted less than 60s ago)"
		fi
	fi
	if [ -f /etc/init.d/glorytun-udp ] && [ "$(pgrep glorytun-udp)" = "" ] && [ "$(uci -q get glorytun-udp.vpn.enable)" = "1" ] && [ "$(uci -q get glorytun-udp.vpn.key)" != "" ] && [ "$omr_schedule_vpn_setting" != "none" ]; then
		# Shares the same cooldown file as omr-tracker's 002-error Glorytun UDP
		# hold-down logic so the two watchdogs never restart it back-to-back
		gtudp_last_restart="$(cat /tmp/omr-tracker-gtudp-last-restart 2>/dev/null)"
		[ -z "$gtudp_last_restart" ] && gtudp_last_restart=0
		gtudp_now="$(date +%s)"
		if [ "$((gtudp_now - gtudp_last_restart))" -ge 60 ]; then
			_log "Can't find Glorytun UDP, restart it..."
			echo "$gtudp_now" > /tmp/omr-tracker-gtudp-last-restart
			/etc/init.d/glorytun-udp restart >/dev/null 2>&1
			sleep 5
		else
			_log "Can't find Glorytun UDP, restart skipped (already restarted less than 60s ago)"
		fi
	fi
	if [ "$(pgrep openvpn)" = "" ] && [ -f /etc/init.d/openvpn ] && [ "$omr_schedule_vpn_setting" != "none" ]; then
		openvpn_enable=0
		openvpn_enabled() {
			[ "$(uci -q get openvpn.$1.enabled)" = "1" ] && [ -n "$(uci -q get openvpn.$1.ca)" ] && openvpn_enable=1
		}
		config_load openvpn
		config_foreach openvpn_enabled openvpn
		if [ "$openvpn_enable" = "1" ]; then
			# Shared restart cooldown with omr-tracker's 002-error hook (see
			# the MQVPN watchdog further below for the full rationale)
			openvpn_last_restart="$(cat /tmp/omr-openvpn-last-restart 2>/dev/null)"
			[ -z "$openvpn_last_restart" ] && openvpn_last_restart=0
			openvpn_now="$(date +%s)"
			if [ "$((openvpn_now - openvpn_last_restart))" -ge 60 ]; then
				_log "Can't find OpenVPN, restart it"
				echo "$openvpn_now" > /tmp/omr-openvpn-last-restart
				/etc/init.d/openvpn restart >/dev/null 2>&1
				sleep 5
			else
				_log "Can't find OpenVPN, restart skipped (already restarted less than 60s ago)"
			fi
		fi
	fi

	if [ "$(pgrep mlvpn)" = "" ] && [ "$(uci -q get mlvpn.general.enable)" = "1" ] && [ -f /etc/init.d/mlvpn ] && [ "$(uci -q get mlvpn.general.password)" != "" ] && [ "$omr_schedule_vpn_setting" != "none" ]; then
		# Shared restart cooldown with omr-tracker's 002-error hook (see the
		# MQVPN watchdog further below for the full rationale)
		mlvpn_last_restart="$(cat /tmp/omr-mlvpn-last-restart 2>/dev/null)"
		[ -z "$mlvpn_last_restart" ] && mlvpn_last_restart=0
		mlvpn_now="$(date +%s)"
		if [ "$((mlvpn_now - mlvpn_last_restart))" -ge 60 ]; then
			_log "Can't find MLVPN, restart it..."
			echo "$mlvpn_now" > /tmp/omr-mlvpn-last-restart
			/etc/init.d/mlvpn restart >/dev/null 2>&1
			sleep 5
		else
			_log "Can't find MLVPN, restart skipped (already restarted less than 60s ago)"
		fi
	fi
	if [ "$(pgrep mqvpn)" = "" ] && [ "$(uci -q get mqvpn.settings.enable)" = "1" ] && [ -f /etc/init.d/mqvpn ] && [ "$(uci -q get mqvpn.auth.key)" != "" ] && [ "$omr_schedule_vpn_setting" != "none" ]; then
		# Rate-limit: omr-tracker's post-tracking 002-error hook also restarts
		# MQVPN as soon as its interface reports down. Without this shared
		# cooldown, both watchdogs can fire within the same outage and restart
		# it back-to-back, force-closing every open MPTCP connection through
		# it (confirmed live: a single MQVPN outage produced 4 tun0 up/down
		# flaps and a shadowsocks-rust crash loop from the resulting
		# scheduler-restart storm)
		mqvpn_last_restart="$(cat /tmp/omr-mqvpn-last-restart 2>/dev/null)"
		[ -z "$mqvpn_last_restart" ] && mqvpn_last_restart=0
		mqvpn_now="$(date +%s)"
		if [ "$((mqvpn_now - mqvpn_last_restart))" -ge 60 ]; then
			_log "Can't find MQVPN, restart it..."
			echo "$mqvpn_now" > /tmp/omr-mqvpn-last-restart
			/etc/init.d/mqvpn restart >/dev/null 2>&1
			sleep 5
		else
			_log "Can't find MQVPN, restart skipped (already restarted less than 60s ago)"
		fi
	fi
	if [ "$(pgrep dsvpn)" = "" ] && [ "$(uci -q get dsvpn.vpn.enable)" = "1" ] && [ -f /etc/init.d/dsvpn ] && [ "$(uci -q get dsvpn.vpn.key)" != "" ] && [ "$omr_schedule_vpn_setting" != "none" ]; then
		# Shared restart cooldown with omr-tracker's 002-error hook (see the
		# MQVPN watchdog above for the full rationale)
		dsvpn_last_restart="$(cat /tmp/omr-dsvpn-last-restart 2>/dev/null)"
		[ -z "$dsvpn_last_restart" ] && dsvpn_last_restart=0
		dsvpn_now="$(date +%s)"
		if [ "$((dsvpn_now - dsvpn_last_restart))" -ge 60 ]; then
			_log "Can't find DSVPN, restart it..."
			echo "$dsvpn_now" > /tmp/omr-dsvpn-last-restart
			/etc/init.d/dsvpn restart >/dev/null 2>&1
			sleep 5
		else
			_log "Can't find DSVPN, restart skipped (already restarted less than 60s ago)"
		fi
	fi
	if [ "$(pgrep vpnclient)" = "" ] && [ "$(uci -q get softethervpn.openmptcprouter.enable)" = "1" ] && [ -f /etc/init.d/softethervpnclient ] && [ "$omr_schedule_vpn_setting" != "none" ]; then
		# Shared restart cooldown with omr-tracker's 002-error hook (see the
		# MQVPN watchdog above for the full rationale)
		softethervpn_last_restart="$(cat /tmp/omr-softethervpn-last-restart 2>/dev/null)"
		[ -z "$softethervpn_last_restart" ] && softethervpn_last_restart=0
		softethervpn_now="$(date +%s)"
		if [ "$((softethervpn_now - softethervpn_last_restart))" -ge 60 ]; then
			_log "Can't find SoftEther VPN, restart it..."
			echo "$softethervpn_now" > /tmp/omr-softethervpn-last-restart
			/etc/init.d/softethervpnclient restart >/dev/null 2>&1
			sleep 5
		else
			_log "Can't find SoftEther VPN, restart skipped (already restarted less than 60s ago)"
		fi
	fi
	# pgrep -f matches the whole cmdline, so "v2ray" also matched
	# omr-tracker-v2ray (and this script's own /etc/init.d/omr-tracker
	# children): the dead-daemon branch could never fire. -x matches the
	# process name alone, which procd takes from v2ray.main.v2ray_file.
	if [ "$(pgrep -x "$(_prog_name v2ray.main.v2ray_file v2ray)")" = "" ] && [ "$(uci -q get v2ray.main.enabled)" = "1" ] && [ -f /etc/init.d/v2ray ]; then
		_proxy_restart v2ray V2Ray
	else
		rm -f /tmp/omr-v2ray-restart
	fi
	# Same as V2Ray above: -f "xray" also matches omr-tracker-xray.
	if [ "$(pgrep -x "$(_prog_name xray.main.xray_file xray)")" = "" ] && [ "$(uci -q get xray.main.enabled)" = "1" ] && [ -f /etc/init.d/xray ]; then
		_proxy_restart xray XRay
	else
		rm -f /tmp/omr-xray-restart
	fi
	if [ "$(uci -q get v2ray.main.enabled)" = "1" ] && [ -f /etc/init.d/v2ray ] && [ "$(pgrep -f omr-tracker-v2ray)" = "" ] && [ "$(pgrep -f '/etc/init.d/omr-tracker')" = "" ]; then
		_log "Can't find omr-tracker-v2ray, start it (procd keeps running trackers)..."
		/etc/init.d/omr-tracker start >/dev/null 2>&1
	fi
	if [ "$(uci -q get xray.main.enabled)" = "1" ] && [ -f /etc/init.d/xray ] && [ "$(pgrep -f omr-tracker-xray)" = "" ] && [ "$(pgrep -f '/etc/init.d/omr-tracker')" = "" ]; then
		_log "Can't find omr-tracker-xray, start it (procd keeps running trackers)..."
		/etc/init.d/omr-tracker start >/dev/null 2>&1
	fi

	set_get_config() {
		local server=$1
		[ -n "$server" ] && uci -q set openmptcprouter.${server}.get_config=1
	}

	if ([ -f /etc/init.d/shadowsocks-libev ] && [ "$(uci -q get shadowsocks-libev.sss0.disabled)" != "1" ] && [ "$(uci -q get shadowsocks-libev.sss0.key)" = "" ] && _ss_server_configured shadowsocks-libev sss0) || ([ -f /etc/init.d/shadowsocks-rust ] && [ "$(uci -q get shadowsocks-rust.sss0.disabled)" != "1" ] && [ "$(uci -q get shadowsocks-rust.sss0.password)" = "" ] && _ss_server_configured shadowsocks-rust sss0); then
		config_load openmptcprouter
		config_foreach set_get_config server
		[ -n "$(uci -q changes openmptcprouter)" ] && uci -q commit openmptcprouter
	fi
	if [ -f /etc/init.d/shadowsocks-libev ] && [ "$(uci -q get shadowsocks-libev.sss0.disabled)" != "1" ] && [ "$(uci -q get shadowsocks-libev.sss0.key)" != "" ] && _ss_server_configured shadowsocks-libev sss0 && [ "$(pgrep -f omr-tracker-ss)" = "" ] && [ "$(pgrep -f '/etc/init.d/omr-tracker')" = "" ]; then
		_log "Can't find omr-tracker-ss for Shadowsocks libev, start it (procd keeps running trackers)..."
		/etc/init.d/omr-tracker start >/dev/null 2>&1
	fi

	if [ -f /etc/init.d/shadowsocks-rust ] && [ "$(uci -q get shadowsocks-rust.sss0.disabled)" != "1" ] && [ "$(uci -q get shadowsocks-rust.sss0.password)" != "" ] && _ss_server_configured shadowsocks-rust sss0 && [ "$(pgrep -f omr-tracker-ss)" = "" ] && [ "$(pgrep -f '/etc/init.d/omr-tracker')" = "" ]; then
		_log "Can't find omr-tracker-ss for Shadowsocks Rust, start it (procd keeps running trackers)..."
		/etc/init.d/omr-tracker start >/dev/null 2>&1
	fi

	if [ "$(uci -q get glorytun.vpn.enable)" = "1" ] && [ "$(uci -q get glorytun.vpn.key)" = "" ]; then
		config_load openmptcprouter
		config_foreach set_get_config server
		[ -n "$(uci -q changes openmptcprouter)" ] && uci -q commit openmptcprouter
	elif [ "$(uci -q get mqvpn.settings.enable)" = "1" ] && [ "$(uci -q get mqvpn.auth.key)" = "" ]; then
		config_load openmptcprouter
		config_foreach set_get_config server
		[ -n "$(uci -q changes openmptcprouter)" ] && uci -q commit openmptcprouter
	elif [ "$(uci -q get v2ray.main.enabled)" = "1" ] && [ "$(uci -q get v2ray.omrout.s_vless_user_id)" = "" ]; then
		config_load openmptcprouter
		config_foreach set_get_config server
		[ -n "$(uci -q changes openmptcprouter)" ] && uci -q commit openmptcprouter
	elif [ "$(uci -q get xray.main.enabled)" = "1" ] && [ "$(uci -q get xray.omrout.s_vless_user_id)" = "" ]; then
		config_load openmptcprouter
		config_foreach set_get_config server
		[ -n "$(uci -q changes openmptcprouter)" ] && uci -q commit openmptcprouter
	fi

	# The set_get_config/token blocks above may have just committed new flags
	_omr_snapshot="$(uci -q -P /var/state show openmptcprouter)"
	_vps_watchdog

	# VXLAN is toggled purely via UCI (openmptcprouter.settings.vxlan) but only
	# takes effect once openmptcprouter-vps pushes/pulls it against the VPS, so
	# catch a UCI flag that drifted from the actual omrvxlan interface state.
	# set_vxlan backs off on its own when the tunnel can't be set up (see
	# _set_vxlan_service in openmptcprouter-vps): while it waits, this logged
	# and slept 5 s every minute for a call that did nothing.
	if [ "$(pgrep -f openmptcprouter-vps)" = "" ] && ! _vxlan_backing_off; then
		if [ "$(uci -q get openmptcprouter.settings.vxlan)" = "1" ] && [ -z "$(uci -q get network.omrvxlan)" ]; then
			_log "VXLAN enabled but not set up, enable it..."
			/etc/init.d/openmptcprouter-vps set_vxlan >/dev/null 2>&1
			sleep 5
		elif [ "$(uci -q get openmptcprouter.settings.vxlan)" != "1" ] && [ -n "$(uci -q get network.omrvxlan)" ]; then
			_log "VXLAN disabled but still set up, disable it..."
			/etc/init.d/openmptcprouter-vps set_vxlan >/dev/null 2>&1
			sleep 5
		fi
	fi
fi


# Whether the main dnsmasq has exactly this upstream server: a grep matched
# 11.1.1.1, 1.1.1.10 or 1.1.1.1#53 for 1.1.1.1.
_dnsmasq_has_server() {
	case " $(uci -q get dhcp.@dnsmasq[0].server) " in
		*" $1 "*) return 0;;
		*) return 1;;
	esac
}

# unbound fails lookups while a root server answers: its root trust anchor
# may be stale (with a root key it can't validate, unbound answers SERVFAIL
# even with DNSSEC checking off). unbound follows the root key by RFC 5011,
# which has to watch a new key for 30 days and keeps that state in RAM
# (saved to /etc every 9 days only). A router installed or reset too close
# to a root KSK rollover, as on 2026-10-11, kept the old key and every
# lookup failed; restarting unbound changes nothing. unbound-anchor gets the
# current keys from IANA, checked against ICANN's certificate, and finds
# IANA's server without the local resolver. It works on a copy (unbound
# rewrites its own file), at most once an hour. Exit 1 means it updated the
# anchor, 0 is both "nothing to do" and an error.
# The new file is moved into place: copied over it, a power cut or a full
# overlay left a truncated root.key, and unbound did not start any more. A
# file with no key in it is not used as the base either: unbound-anchor then
# starts from its built-in anchor.
_unbound_anchor_update() {
	local cur=/var/lib/unbound/root.key new=/tmp/omr-root.key.new now last rc
	[ -x /usr/sbin/unbound-anchor ] || return 1
	[ -n "$(dig +time=3 +tries=1 . DNSKEY @198.41.0.4 | grep 'ANSWER: [1-9]')" ] || return 1
	now=$(date +%s)
	last=$(cat /tmp/omr-unbound-anchor-last 2>/dev/null)
	case "$last" in
		''|*[!0-9]*) ;;
		*) [ $((now - last)) -lt 3600 ] && return 1 ;;
	esac
	echo "$now" > /tmp/omr-unbound-anchor-last
	[ -f "$cur" ] || cur=/etc/unbound/root.key
	rm -f "$new"
	grep -q '^\. .*\(DS\|DNSKEY\)' "$cur" 2>/dev/null && cp "$cur" "$new"
	timeout 120 /usr/sbin/unbound-anchor -a "$new" >/dev/null 2>&1
	rc=$?
	if [ "$rc" = "1" ] && grep -q '^\. .*\(DS\|DNSKEY\)' "$new" &&
	   cp "$new" /etc/unbound/root.key.tmp && mv -f /etc/unbound/root.key.tmp /etc/unbound/root.key; then
		rm -f /var/lib/unbound/root.key "$new"
		return 0
	fi
	rm -f "$new" /etc/unbound/root.key.tmp
	return 1
}

if [ -f /etc/init.d/unbound ] && [ "$(uci -q get unbound.@unbound[0].enabled)" = "1" ] && _dnsmasq_has_server '127.0.0.1#5353'; then
	if [ "$(pgrep -f unbound)" = "" ]; then
		# A root.key unbound can't read stops it from starting at all:
		# the anchor is checked here too, not only when lookups fail
		if _unbound_anchor_update; then
			_log "Can't find unbound, updated its DNSSEC root trust anchor from IANA, restart it..."
		else
			_log "Can't find unbound, restart it..."
		fi
		/etc/init.d/unbound restart >/dev/null 2>&1
		sleep 5
	elif [ "$(uci -q get openmptcprouter.settings.external_check)" != "0" ] && [ "$(uci -q get unbound.ub_main.listen_port)" = "5353" ] && [ -n "$(dig +timeout=4 +tries=2 openmptcprouter.com -p 5353 | grep 'ANSWER: 0')" ]; then
		if _unbound_anchor_update; then
			_log "Can't resolve via unbound: its DNSSEC root trust anchor was stale, updated it from IANA, restart it..."
		else
			_log "Can't resolve via unbound, restart it..."
		fi
		/etc/init.d/unbound restart >/dev/null 2>&1
		sleep 5
	fi
fi

if [ -f /etc/init.d/unbound ] && [ "$(uci -q get unbound.@unbound[0].enabled)" = "1" ] && [ "$(uci -q get openmptcprouter.settings.external_check)" != "0" ]; then
	_dns_hijacked_flag="$(uci -q -P /var/state get openmptcprouter.settings.dns_hijacked)"
	# Nothing to do if dnsmasq isn't on unbound AND we're not already flagged hijacked
	# (some other/manual config, not ours to touch). Cheap uci reads, so check these
	# before bothering with the pricier WAN-status loop below.
	if _dnsmasq_has_server '127.0.0.1#5353' || [ "$_dns_hijacked_flag" = "1" ]; then
		_omr_wan_up=0
		_omr_wan_up_check() {
			local multipath
			config_get multipath "$1" multipath
			case "$multipath" in
				on|master|backup) ;;
				*) return ;;
			esac
			[ "$(uci -q -P /var/state get openmptcprouter.$1.state)" = "up" ] && _omr_wan_up=1
		}
		config_load network
		config_foreach _omr_wan_up_check interface
		if [ "$_omr_wan_up" = "1" ]; then
			# Throttle to every 15min
			_dnshijack_now="$(date +%s)"
			_dnshijack_last="$(cat /tmp/omr-dnshijack-last-check 2>/dev/null)"
			[ -z "$_dnshijack_last" ] && _dnshijack_last=0
			if [ "$((_dnshijack_now - _dnshijack_last))" -ge 900 ]; then
				echo "$_dnshijack_now" > /tmp/omr-dnshijack-last-check
				ROOT_CHECK=$(dig +short +time=3 +tries=1 google.fr @198.41.0.4 2>/dev/null)
				ROOT_CHECK_RC=$?
				if [ "$ROOT_CHECK_RC" -eq 0 ] && [ -n "$ROOT_CHECK" ] && ! _dnsmasq_has_server '1.1.1.1'; then
					_log "DNS port 53 hijacked by ISP, bypass unbound and use 1.1.1.1..."
					uci -q del_list dhcp.@dnsmasq[0].server='127.0.0.1#5353'
					uci -q add_list dhcp.@dnsmasq[0].server='1.1.1.1'
					# dns_hijacked marks the dhcp change above as ours, so it is kept
					# in the config with it: in /var/state, it was gone after a reboot
					# and unbound was never restored
					uci -q set openmptcprouter.settings.dns_hijacked=1
					uci -q commit dhcp
					uci -q commit openmptcprouter
					/etc/init.d/dnsmasq restart >/dev/null 2>&1
				elif [ "$ROOT_CHECK_RC" -eq 0 ] && [ -z "$ROOT_CHECK" ] && [ "$_dns_hijacked_flag" = "1" ]; then
					_log "DNS port 53 hijack resolved, restoring unbound..."
					uci -q del_list dhcp.@dnsmasq[0].server='1.1.1.1'
					uci -q add_list dhcp.@dnsmasq[0].server='127.0.0.1#5353'
					uci -q delete openmptcprouter.settings.dns_hijacked
					uci -q -P /var/state revert openmptcprouter.settings.dns_hijacked
					uci -q commit dhcp
					uci -q commit openmptcprouter
					/etc/init.d/dnsmasq restart >/dev/null 2>&1
				fi
			fi
		fi
	fi
fi

# dnsmasq is intentionally stopped while this router is a keepalived HA
# backup (hotplug 551-dnsmasq); only skip the watchdog while keepalived
# is really alive so a crashed keepalived never leaves dnsmasq down
_keepalived_backup=0
[ "$(cat /var/run/keepalived.omr_ha.state 2>/dev/null)" = "BACKUP" ] &&
	pidof keepalived >/dev/null 2>&1 && _keepalived_backup=1

if [ -f /etc/init.d/dnsmasq ] && [ "$_keepalived_backup" != "1" ]; then
	if [ "$(pgrep -f dnsmasq)" = "" ]; then
		_log "Can't find dnsmasq, restart it..."
		/etc/init.d/dnsmasq restart >/dev/null 2>&1
		sleep 5
	elif [ "$(uci -q get openmptcprouter.settings.external_check)" != "0" ] && [ -n "$(dig +timeout=4 +tries=2 openmptcprouter.com 2>&1 | grep -i 'connection refused')" ]; then
		# Also refused while dnsmasq is being restarted (pgrep -f then finds
		# its init script): let that finish, then check again
		_wait=0
		while [ -n "$(pgrep -f /etc/init.d/dnsmasq)" ] && [ "$_wait" -lt 30 ]; do
			sleep 1
			_wait=$((_wait + 1))
		done
		sleep 2
		# The script that ran may have been 551-dnsmasq stopping dnsmasq
		# because this router just became the HA backup: don't revive it
		[ "$(cat /var/run/keepalived.omr_ha.state 2>/dev/null)" = "BACKUP" ] &&
			pidof keepalived >/dev/null 2>&1 && _keepalived_backup=1
		if [ "$_keepalived_backup" != "1" ] && [ -n "$(dig +timeout=4 +tries=2 openmptcprouter.com 2>&1 | grep -i 'connection refused')" ]; then
			_log "Can't resolve via dnsmasq, restart it..."
			/etc/init.d/dnsmasq restart >/dev/null 2>&1
			sleep 5
		fi
	fi
fi

# unbound only when it is there to answer: with unbound disabled or not
# installed, 127.0.0.1#5353 sent every lookup to a closed port
if [ -f /etc/init.d/dnsmasq ] && [ -z "$(uci -q get dhcp.@dnsmasq[0].server)" ]; then
	_unbound_server=""
	[ -f /etc/init.d/unbound ] && [ "$(uci -q get unbound.@unbound[0].enabled)" = "1" ] &&
		_unbound_server="add_list dhcp.@dnsmasq[0].server='127.0.0.1#5353'"
	uci -q batch <<-EOF >/dev/null
		${_unbound_server}
		add_list dhcp.@dnsmasq[0].server='/lan/'
		add_list dhcp.@dnsmasq[0].server='/use-application-dns.net/'
		commit dhcp
	EOF
fi

if [ "$(pgrep miniupnpd)" = "" ] && [ "$(uci -q get upnpd.config.enabled)" = "1" ] && [ -f /etc/init.d/miniupnpd ]; then
	_log "Can't find miniupnpd, restart it..."
	/etc/init.d/miniupnpd restart >/dev/null 2>&1
	sleep 5
fi

# rpcd registers its library plugins (luci, luci-rpc, file, iwinfo) only once
# it ran `list` on every /usr/libexec/rpcd script, one after the other and with
# no timeout, and all of that happens before it starts serving requests. A
# script that hangs there leaves a live rpcd process which answers nothing, and
# LuCI then fails every call with "RPC call to luci/getFeatures failed with
# error -32000: Object not found". A pid test can't see that state, so probe
# the object rpcd registers last instead. rpcd is legitimately object-less for
# the moment its plugins need at startup, hence the miss must be confirmed by a
# second pass before the restart.
_rpcd_no_objects="/tmp/.omr-rpcd-no-objects"
if [ -f /etc/init.d/rpcd ]; then
	if [ -z "$(pgrep -x rpcd)" ]; then
		_log "Can't find rpcd, restart it..."
		/etc/init.d/rpcd enable >/dev/null 2>&1
		/etc/init.d/rpcd restart >/dev/null 2>&1
		rm -f "$_rpcd_no_objects"
		sleep 5
	elif [ -f /usr/lib/rpcd/luci.so ]; then
		# an empty listing means ubusd itself is unreachable: restarting
		# rpcd would fix nothing there
		_ubus_objects="$(ubus -t 3 list 2>/dev/null)"
		if [ -n "$_ubus_objects" ] && ! echo "$_ubus_objects" | grep -qx luci; then
			if [ -f "$_rpcd_no_objects" ]; then
				# rpcd waits for `<plugin> list` without any timeout, so the
				# child still around names whatever it is stuck on: log it,
				# the restart makes that evidence disappear
				_stuck="$(ps ww 2>/dev/null | sed -n 's|.*/usr/libexec/rpcd/\([^ ]*\) list.*|\1|p' | sort -u | tr '\n' ' ' | sed 's/ *$//')"
				if [ -n "$_stuck" ]; then
					_log "rpcd is running but registers no ubus object (stuck on the list of: $_stuck), restart it..."
				else
					_log "rpcd is running but registers no ubus object, restart it..."
				fi
				/etc/init.d/rpcd restart >/dev/null 2>&1
				rm -f "$_rpcd_no_objects"
				sleep 5
			else
				touch "$_rpcd_no_objects"
			fi
		else
			rm -f "$_rpcd_no_objects"
		fi
	fi
fi
if [ "$(pgrep dbus)" = "" ] && [ -f /etc/init.d/dbus ]; then
	_log "Can't find dbus, restart it..."
	/etc/init.d/dbus enable >/dev/null 2>&1
	/etc/init.d/dbus restart >/dev/null 2>&1
	sleep 5
fi

if [ "$(pgrep uhttpd)" = "" ] && [ -f /etc/init.d/uhttpd ]; then
	_log "Can't find uhttpd, restart it..."
	/etc/init.d/uhttpd restart >/dev/null 2>&1
	sleep 5
fi

if [ "$(uci -q get openmptcprouter.settings.disable_modemmanager)" != "1" ]; then
	if [ -z "$(pgrep ModemManager)" ] && [ -f /etc/init.d/modemmanager ] && [ -n "$(uci -q show network | grep modemmanager)" ]; then
		_log "Can't find ModemManager, restart it..."
		/etc/init.d/modemmanager restart >/dev/null 2>&1
		sleep 5
	elif [ -n "$(pgrep ModemManager)" ] && [ -f /etc/init.d/modemmanager ] && [ -n "$(uci -q show network | grep modemmanager)" ] && [ -z "$(timeout 2 mmcli -L)" ]; then
		_log "ModemManager doesn't answer, restart it..."
		/etc/init.d/modemmanager restart >/dev/null 2>&1
		sleep 5
	#elif [ -n "$(pgrep ModemManager)" ] && [ -f /etc/init.d/modemmanager ] && [ -z "$(uci -q show network | grep modemmanager)" ]; then
	#	_log "ModemManager not used, stop it..."
	#	/etc/init.d/modemmanager stop >/dev/null 2>&1
	#	sleep 5
	fi
	/etc/init.d/modemmanager enable >/dev/null 2>&1
fi

# pgrep -f matches the tracker daemon AND its short-lived bash subshells
# (command substitutions, post-tracking blocks share the same cmdline), so a
# raw pid count sees phantom duplicates and — once the pid space wraps — the
# lowest pid can be a subshell, getting the real daemon killed. Only processes
# reparented to init/procd (PPid 1) are actual daemons.
_daemon_pids() {
	local pid ppid daemons=""
	for pid in $(pgrep -f "$1"); do
		ppid=$(awk '/^PPid:/ {print $2}' "/proc/$pid/status" 2>/dev/null)
		[ "$ppid" = "1" ] && daemons="$daemons $pid"
	done
	echo $daemons
}

# The tracker daemons of interface $1: "omr-tracker <interface>" has to be a
# whole word of the command line, as pgrep -f "omr-tracker wan1" also matches
# the trackers of wan10 to wan19
_tracker_pids() {
	local pid daemons=""
	for pid in $(_daemon_pids "omr-tracker $1"); do
		case " $(tr '\0' ' ' 2>/dev/null < "/proc/$pid/cmdline") " in
			*" /bin/omr-tracker $1 "*) daemons="$daemons $pid" ;;
		esac
	done
	echo $daemons
}

_oldest_pid() {
	local pid start oldest="" oldest_start=""
	for pid in "$@"; do
		start=$(awk '{print $22}' "/proc/$pid/stat" 2>/dev/null)
		[ -z "$start" ] && continue
		if [ -z "$oldest_start" ] || [ "$start" -lt "$oldest_start" ]; then
			oldest=$pid
			oldest_start=$start
		fi
	done
	echo "$oldest"
}

# Whether the tracker of interface $1 had been looked at by this watchdog,
# and omr-tracker's init chose not to start it, since the last change of its
# config. The same interfaces as _launch_tracker in the omr-tracker init
# are skipped by name or setting below; for the rest (no hosts to ping, ...)
# the init itself is the reference: when it defines no procd instance, the
# tracker is not retried, with a 10 s wait each time, every minute, but
# once an hour only.
_omrtracker_skipped() {
	local mark="/tmp/omr-tracker-skipped-$1" since
	[ -f "$mark" ] && [ "$mark" -nt /etc/config/omr-tracker ] && [ "$mark" -nt /etc/config/network ] || return 1
	read -r since < "$mark" 2>/dev/null
	case "$since" in ''|*[!0-9]*) return 1 ;; esac
	[ $(($(date +%s) - since)) -lt 3600 ]
}

restart_omrtracker() {
	config_get multipath "$1" multipath
	config_get ifenabled "$1" auto
	{ [ -z "$multipath" ] || [ "$multipath" = "off" ]; } && return
	[ "$ifenabled" = "0" ] && return
	case "$1" in
		loopback|lan*|if0*|*_6) return ;;
	esac
	local disabled tracker_enabled
	config_get disabled "$1" disabled
	[ "$disabled" = "1" ] && return
	tracker_enabled=$(uci -q get "omr-tracker.$1.enabled")
	[ -z "$tracker_enabled" ] && tracker_enabled=$(uci -q get omr-tracker.defaults.enabled)
	[ "$tracker_enabled" = "0" ] && return
	local daemons daemon_count keep pid
	daemons=$(_tracker_pids "$1")
	daemon_count=$(echo $daemons | wc -w)
	[ "$daemon_count" -gt 1 ] && {
		# keep the instance procd supervises (fall back to the oldest),
		# kill only genuine stray daemons
		keep=$(ubus call service list '{"name":"omr-tracker"}' 2>/dev/null | jsonfilter -q -e "@['omr-tracker'].instances['$1'].pid")
		case " $daemons " in
			*" $keep "*) ;;
			*) keep=$(_oldest_pid $daemons);;
		esac
		for pid in $daemons; do
			[ "$pid" != "$keep" ] && kill "$pid" 2>/dev/null
		done
		return
	}
	[ "$daemon_count" -eq 1 ] && return
	_omrtracker_skipped "$1" && return
	# Atomic lock to prevent concurrent starts from overlapping cron runs
	local lockdir="/tmp/omr-tracker-lock-${1}"
	if ! mkdir "$lockdir" 2>/dev/null; then
		local now mtime
		now=$(date +%s)
		mtime=$(stat -c "%Y" "$lockdir" 2>/dev/null || echo 0)
		[ $((now - mtime)) -lt 60 ] && return
		rmdir "$lockdir" 2>/dev/null
		mkdir "$lockdir" 2>/dev/null || return
	fi
	/etc/init.d/omr-tracker enable
	/etc/init.d/omr-tracker start_interface "$1"
	local instances
	if instances="$(ubus call service list '{"name":"omr-tracker"}' 2>/dev/null)" &&
	   [ -z "$(printf '%s' "$instances" | jsonfilter -q -e "@['omr-tracker'].instances['$1']")" ]; then
		date +%s > "/tmp/omr-tracker-skipped-$1"
	else
		rm -f "/tmp/omr-tracker-skipped-$1"
		sleep 10
	fi
	rmdir "$lockdir" 2>/dev/null
}

_intf_zone_forwards_to_wan() {
	# Only true if the firewall zone this interface belongs to actually
	# has a forwarding path to the wan zone (either it *is* the wan zone,
	# or a `config forwarding` src=<zone> dest=wan exists). Isolated zones
	# (forward=REJECT, no forwarding to wan) must return false so their
	# interfaces are never added to the ss-rust transparent-redirect scope.
	local intf="$1" zsection znetwork zname fsection fsrc fdest
	for zsection in $(uci -q show firewall | sed -n "s/^\(firewall\.[^.]*\)=zone\$/\1/p"); do
		znetwork=$(uci -q get ${zsection}.network 2>/dev/null)
		case " $znetwork " in
			*" $intf "*)
				zname=$(uci -q get ${zsection}.name 2>/dev/null)
				[ "$zname" = "wan" ] && return 0
				for fsection in $(uci -q show firewall | sed -n "s/^\(firewall\.[^.]*\)=forwarding\$/\1/p"); do
					fsrc=$(uci -q get ${fsection}.src 2>/dev/null)
					fdest=$(uci -q get ${fsection}.dest 2>/dev/null)
					[ "$fsrc" = "$zname" ] && [ "$fdest" = "wan" ] && return 0
				done
				;;
		esac
	done
	return 1
}
# _intf_in_zone <interface> <zone name>...: true if the interface is in the
# networks of a firewall zone with one of these names
_intf_in_zone() {
	local intf="$1" zsection zname
	shift
	for zsection in $(uci -q show firewall | sed -n "s/^\(firewall\.[^.]*\)=zone\$/\1/p"); do
		case " $(uci -q get ${zsection}.network 2>/dev/null) " in
			*" $intf "*) ;;
			*) continue ;;
		esac
		zname=$(uci -q get ${zsection}.name 2>/dev/null)
		case " $* " in
			*" $zname "*) return 0 ;;
		esac
	done
	return 1
}
# _set_list <list> <value> <0|1>: remove or add the value only when needed.
# Deleting and re-adding it each minute left a /tmp/.uci delta in which LAN was
# missing from the dnsmasq interfaces, so a dnsmasq restart at that moment
# listened on lo only, and dhcp/unbound/shadowsocks configs were rewritten.
_set_list() {
	[ -n "$2" ] || return
	case " $(uci -q get "$1") " in
		*" $2 "*) [ "$3" = "1" ] || uci -q del_list "$1=$2" ;;
		*) [ "$3" = "1" ] && uci -q add_list "$1=$2" ;;
	esac
}
# The device netifd brought a network up on: a Wi-Fi-only network (guest...)
# has no device option, the wifi-iface names the network instead
_lan_l3_device() {
	ifstatus "$1" 2>/dev/null | jsonfilter -q -e '@.l3_device' | tr -d '\n'
}
# ss_rules.ifnames lists devices, but the proxy is decided per interface, and
# several interfaces can share a device (lan static + lan6 dhcpv6 on br-lan):
# set per interface, the last one won and the lists flipped every minute. The
# devices are collected first (_lan_ss_device <device> <0|1>), and a device is
# listed when any interface on it is proxied (_lan_ss_apply).
_lan_ss_device() {
	case "$1" in
		''|*@*) return ;;
	esac
	case " $_lan_ss_devs " in
		*" $1 "*) ;;
		*) _lan_ss_devs="$_lan_ss_devs $1" ;;
	esac
	[ "$2" = "1" ] || return
	case " $_lan_ss_proxy " in
		*" $1 "*) ;;
		*) _lan_ss_proxy="$_lan_ss_proxy $1" ;;
	esac
}
_lan_ss_apply() {
	local _dev _proxy
	for _dev in $_lan_ss_devs; do
		_proxy=0
		case " $_lan_ss_proxy " in
			*" $_dev "*) _proxy=1 ;;
		esac
		_set_list shadowsocks-libev.ss_rules.ifnames "$_dev" $_proxy
		_set_list shadowsocks-rust.ss_rules.ifnames "$_dev" $_proxy
	done
}
set_lan_ips() {
	config_get ip4table "$1" ip4table
	config_get device "$1" device
	config_get proto "$1" proto
	config_get multipath "$1" multipath
	# the device option's name in configs from before OpenWrt 21.02
	config_get ifname "$1" ifname
	# A device name with a dash (br-lan, lan-1...) used to turn
	# restrict_to_lan off for good, for every interface: the proxy rules then
	# matched any ingress interface, the WANs included. nft takes these names
	# in the ss_rules iifname set like any other, so they are listed instead.
	local lan=0 wan=0 proxy=0 l3dev="$device"
	# A backup WAN is a WAN too: dnsmasq/unbound must not serve it as LAN
	# omrvpn is the VPN tunnel, not a LAN, even when its proto is dhcp
	# (Glorytun or SoftEther in DHCP mode, or left over from them), and
	# neither is any interface in the wan or vpn firewall zone. An alias
	# (@lan) is served through the interface it is an alias of.
	# dnsmasq and unbound take the interface name: a network without a device
	# option is a LAN all the same, only the proxy needs its device.
	if [ "$1" != "omrvpn" ] && [ "$multipath" != "on" ] && [ "$multipath" != "master" ] && [ "$multipath" != "backup" ] && [ -z "$(echo $device $ifname | grep @)" ] && ([ "$proto" = "dhcp" ] || [ "$proto" = "static" ]) && ! _intf_in_zone "$1" wan vpn; then
		lan=1
		[ -z "$l3dev" ] && l3dev="$(_lan_l3_device "$1")"
		_intf_zone_forwards_to_wan "$1" && proxy=1
	elif [ "$multipath" = "on" ] || [ "$multipath" = "master" ] || [ "$multipath" = "backup" ]; then
		wan=1
	fi
	_lan_ss_device "$l3dev" $proxy
	_set_list unbound.ub_main.iface_lan "$1" $lan
	_set_list unbound.ub_main.iface_wan "$1" $wan
	_set_list dhcp.@dnsmasq[0].interface "$1" $lan
	_set_list dhcp.@dnsmasq[0].notinterface "$1" $wan
}
config_load network
config_foreach restart_omrtracker interface
# Kill any duplicate omr-tracker-server daemons (PPid-1 only: pgrep -f also
# matches its subshells), keeping the oldest
_srv_daemons=$(_daemon_pids "omr-tracker-server")
_srv_count=$(echo $_srv_daemons | wc -w)
[ "$_srv_count" -gt 1 ] && {
	_srv_keep=$(_oldest_pid $_srv_daemons)
	for _srv_pid in $_srv_daemons; do
		[ "$_srv_pid" != "$_srv_keep" ] && kill "$_srv_pid" 2>/dev/null
	done
}
if [ "$(uci -q get openmptcprouter.settings.restrict_to_lan)" = "1" ]; then
	_lan_ss_devs="" _lan_ss_proxy=""
	config_foreach set_lan_ips interface
	_lan_ss_apply
fi
[ "$(uci -q get openmptcprouter.settings.restrict_to_lan)" = "0" ] && ([ -n "$(uci -q get shadowsocks-libev.ss_rules.ifnames)" ] || [ -n "$(uci -q get shadowsocks-rust.ss_rules.ifnames)" ]) && {
	uci -q batch <<-EOF
		delete shadowsocks-libev.ss_rules.ifnames
		delete shadowsocks-rust.ss_rules.ifnames
		delete unbound.ub_main.iface_lan
		delete unbound.ub_main.iface_wan
		delete dhcp.@dnsmasq[0].interface
		delete dhcp.@dnsmasq[0].notinterface
	EOF
}
[ -n "$(uci -q changes shadowsocks-libev)" ] && uci -q commit shadowsocks-libev
[ -n "$(uci -q changes shadowsocks-rust)" ] && uci -q commit shadowsocks-rust
[ -n "$(uci -q changes unbound)" ] && uci -q commit unbound
[ -n "$(uci -q changes dhcp)" ] && uci -q commit dhcp
# Devices shared with a multipath-enabled interface must not be "fixed" off.
# Collect their l3_devices once up front: resolving them inside multipath_fix
# re-ran ifstatus+jsonfilter for every non-off interface per off-interface
# (O(n^2) forks each minute). An off-interface can't appear in this list
# (only non-off ones are collected), so no self-exclusion check is needed.
_mf_shared_devs=""
_mf_collect_shared() {
	config_get _mf_mp "$1" multipath
	[ "$_mf_mp" = "off" ] || [ -z "$_mf_mp" ] && return
	_mf_dev="$(ifstatus "$1" 2>/dev/null | jsonfilter -q -e '@.l3_device' | tr -d '\n')"
	[ -n "$_mf_dev" ] && _mf_shared_devs="$_mf_shared_devs $_mf_dev"
}
multipath_fix() {
	config_get multipath "$1" multipath
	[ "$multipath" != "off" ] && return
	config_get device "$1" device
	[ "$(echo $device | grep '@')" ] && return
	interface="$(ifstatus $1 | jsonfilter -q -e '@.l3_device' | tr -d '\n')"
	[ -z "$interface" ] && return
	case " $_mf_shared_devs " in
		*" $interface "*) return;;
	esac
	[ -n "$(multipath $interface | grep deactivated)" ] && return
	_log "Fix Multipath status on $1 ($interface)"
	/etc/init.d/mptcp reload $interface >/dev/null 2>&1
}
if [ "$(uci -q get network.globals.multipath)" = "enable" ]; then
	config_load network
	config_foreach _mf_collect_shared interface
	config_foreach multipath_fix interface
fi
# logread -l is supported by both ubox logread and the syslog-ng compat
# wrapper; a bare `logread | tail` dumps the whole ring buffer every minute
if [ -n "$(logread -l 2 2>/dev/null | grep 'Ring expansion failed')" ]; then
	_log "Workaround Ring expansion failed problem"
	echo 1 > /sys/bus/pci/devices/0000:00:00.0/remove
	sleep 2
	echo 1 > /sys/bus/pci/rescan
fi

if [ "$(ip r)" = "" ]; then
	_log "No route found, restart MPTCP..."
	/etc/init.d/mptcp restart >/dev/null 2>&1
fi

#if [ -f /etc/init.d/omr-bypass ] && [ -z "$(pgrep -f '/etc/init.d/omr-bypass ')" ] && [ "$(nft list ruleset 2>/dev/null | grep omr_dst_bypass)" = "" ] && [ "$(iptables-save 2>/dev/null | grep omr-bypass)" = "" ]; then
# nft -t (terse) omits set elements: the bypass chains/rules still show, but
# the dump stays small even with thousands of bypassed IPs in the sets
# Trailing space = the init script's own cmdline only. A bare "omr-bypass"
# also matches /usr/lib/omr-bypass/ndpid-flows-tracker, which runs forever.
# Not while fw4 runs: a restart (the proxies' rules_up/rules_down, an
# omr-bypass start) deletes the whole fw4 table and loads it back seconds
# later, so the sets are missing in between without anything being wrong.
if [ -f /etc/init.d/omr-bypass ] && [ -z "$(pgrep -f '/etc/init.d/omr-bypass ')" ] && [ -z "$(pgrep -f '/sbin/fw4 ')" ] && [ "$(nft -t list ruleset 2>/dev/null | grep omr_dst_bypass)" = "" ]; then
	_log "Can't find omr-bypass rules, restart omr-bypass..."
	/etc/init.d/omr-bypass start >/dev/null 2>&1
	sleep 5
fi

if [ -f /etc/backup/installed_packages.txt ] && [ -n "$(grep overlay /etc/backup/installed_packages.txt)" ] && ([ "$(uci -q -P /var/state get openmptcprouter.settings.sysupgrade_lc)" = "" ] || [ $(($(date +"%s") + $((30 + RANDOM % 31)) - $(uci -q -P /var/state get openmptcprouter.settings.sysupgrade_lc))) -gt 3600 ]) && [ $(($(date +"%s") - $(stat -c "%Y" /etc/backup/installed_packages.txt))) -gt 86400 ]; then
	_log "Reinstall packages..."
	uci -q -P /var/state revert openmptcprouter.settings.sysupgrade_lc; uci -q -P /var/state set openmptcprouter.settings.sysupgrade_lc=$(date +"%s")
	/etc/init.d/sysupgrade restart >/dev/null 2>&1
	sleep 10
fi


# A cake-autorate instance that died is not started again by the launcher
# (see sqm-autorate's launcher.sh), and this check went by
# OMR_TRACKER_INTERFACE, which cron never sets: it never ran. Every queue the
# init starts an instance for is checked. An instance that dies again at
# once (a config cake-autorate refuses) is not restarted every minute: the
# delay doubles from 1 minute to 1 hour, and starts over once all of them
# are seen running.
_sqm_autorate_watchdog() {
	local q o v dead="" state=/tmp/omr-sqm-autorate-restart fails next now delay
	[ -x /etc/init.d/sqm-autorate ] || return
	/etc/init.d/sqm-autorate enabled || return
	for q in $(uci -q show sqm | sed -n 's/^sqm\.\([^.=][^.=]*\)=queue$/\1/p'); do
		[ "$(uci -q get "sqm.${q}.enabled")" = "1" ] || continue
		[ "$(uci -q get "sqm.${q}.autorate")" = "1" ] || continue
		# The init starts no instance without all four rates
		for o in max_download download max_upload upload; do
			v="$(uci -q get "sqm.${q}.${o}")"
			[ -z "$v" ] || [ "$v" = "0" ] && continue 2
		done
		pgrep -f "/config\.${q}\.sh" >/dev/null 2>&1 || dead="$dead $q"
	done
	if [ -z "$dead" ]; then
		rm -f "$state"
		return
	fi
	now=$(date +%s)
	fails=0
	next=0
	[ -f "$state" ] && read -r fails next < "$state" 2>/dev/null
	case "$fails" in ''|*[!0-9]*) fails=0 ;; esac
	case "$next" in ''|*[!0-9]*) next=0 ;; esac
	[ "$now" -lt "$next" ] && return
	delay=60
	v=0
	while [ "$v" -lt "$fails" ] && [ "$delay" -lt 3600 ]; do
		delay=$((delay * 2))
		v=$((v + 1))
	done
	[ "$delay" -gt 3600 ] && delay=3600
	echo "$((fails + 1)) $((now + delay))" > "$state"
	_log "SQM autorate not running for${dead}, restart it..."
	/etc/init.d/sqm-autorate restart >/dev/null 2>&1
	sleep 5
}
_sqm_autorate_watchdog

# Hourly: the latest OMR and VPS versions (shown by LuCI) and a firewall check
_omr_latest_versions_check() {
	local _now _lc
	_now="$(date +%s)"
	_lc="$(uci -q -P /var/state get openmptcprouter.latest_versions.lc)"
	[ -n "$_lc" ] && [ $((_now - _lc)) -le 3600 ] && return 0
	# The hourly timestamp is an option of this section, and uci sets no
	# option on a missing section: it was only created when a version
	# changed, never on a distribution other than openmptcprouter (no
	# version kept), so that check ran every minute there. Created once, in
	# the config: the 1930/1970 uci-defaults also take a missing section for
	# a first install, and ran again after each sysupgrade.
	if [ -z "$(uci -q get openmptcprouter.latest_versions)" ]; then
		uci -q batch <<-EOF >/dev/null
			set openmptcprouter.latest_versions=latest_versions
			commit openmptcprouter
		EOF
	fi
	distribution="$(ubus call system board | jsonfilter -q -e '@.release.distribution' | tr -d '\n')"
	version="$(ubus call system board | jsonfilter -q -e '@.release.version' | tr -d '\n')"
	latestversions="$(curl -4 -s -m 3 -A "${distribution}/${version}" https://www.openmptcprouter.com/version/version.json)"
	if [ -n "$latestversions" ]; then
		# The versions stay in the config (they are worth keeping over a
		# reboot) but are only written when they change: this runs hourly,
		# and an unconditional set + commit rewrote the file on flash every
		# time. The check timestamp is runtime, in /var/state.
		if [ "${distribution}" = "openmptcprouter" ]; then
			_lv_omr="$(echo $latestversions | jsonfilter -q -e '@.omr')-$(ubus call system board | jsonfilter -e '@.kernel' | cut -d'.' -f1,2)"
			_lv_vps="$(echo $latestversions | jsonfilter -q -e '@.vps')"
		else
			_lv_omr=""
			_lv_vps=""
		fi
		if [ "$(uci -q get openmptcprouter.latest_versions.omr)" != "$_lv_omr" ] || [ "$(uci -q get openmptcprouter.latest_versions.vps)" != "$_lv_vps" ]; then
			uci -q set openmptcprouter.latest_versions=latest_versions
			if [ -n "$_lv_omr" ]; then
				uci -q set openmptcprouter.latest_versions.omr="$_lv_omr"
			else
				uci -q del openmptcprouter.latest_versions.omr
			fi
			if [ -n "$_lv_vps" ]; then
				uci -q set openmptcprouter.latest_versions.vps="$_lv_vps"
			else
				uci -q del openmptcprouter.latest_versions.vps
			fi
			uci -q commit openmptcprouter
		fi
	else
		# no answer (no WAN up yet, site down): try again in 10 minutes, not
		# on every run
		_now=$((_now - 3000))
	fi
	uci -q -P /var/state revert openmptcprouter.latest_versions.lc; uci -q -P /var/state set openmptcprouter.latest_versions.lc=$_now
	fw4check="$(fw4 check 2>&1)"
	[ -n "$(echo $fw4check | grep Error)" ] && [ -n "$(echo $fw4check | grep bypass)" ] && {
		_log "Restart OMR-ByPass due to firewall error"
		/etc/init.d/omr-bypass restart >/dev/null 2>&1
	}
}
_omr_latest_versions_check

# OMR-ByPass domain IP refresh (reload_rules) used to piggyback on the
# above once-an-hour version-check gate, so it always ran hourly with no
# way to change that. Each reload_rules call can force a full fw4 restart
# + dnsmasq restart when a bypassed domain's resolved IP actually changed,
# interrupting established TCP connections through the tunnel (issue
# #4353). reload_rules_hour lets a user move that disruption to a single
# low-traffic hour of their choosing instead of every hour.
#
# "hourly" is the explicit opt-back-in to the historical once-an-hour
# cadence; a plain empty/unset value is NOT treated the same, because uci
# can't tell "explicitly set to empty" from "key never created" (`uci get`
# returns the same empty result and exit status 1 either way) -- an empty
# value defaults to hour 2 (matches bypass_asn's existing 2 AM cron) rather
# than silently keeping everyone on the disruptive hourly cadence forever.
# 41_omr-bypass writes "2" explicitly for every router on upgrade, so this
# fallback is normally only hit for the brief window before that runs.
if [ -n "$(uci -q show omr-bypass | grep '=domains')" ]; then
	_bypass_reload_hour="$(uci -q get omr-bypass.global.reload_rules_hour)"
	[ -z "$_bypass_reload_hour" ] && _bypass_reload_hour="2"
	_bypass_reload_lc="$(uci -q get omr-bypass.global.reload_rules_lc)"
	_bypass_should_reload=0
	if [ "$_bypass_reload_hour" = "hourly" ]; then
		if [ -z "$_bypass_reload_lc" ] || [ $(($(date +"%s") - _bypass_reload_lc)) -gt 3600 ]; then
			_bypass_should_reload=1
		fi
	else
		_bypass_reload_lastday="$(uci -q get omr-bypass.global.reload_rules_lastday)"
		_bypass_today="$(date +%Y%m%d)"
		# Zero-pad to 2 digits without printf/arithmetic: busybox printf's
		# %d and ash's $(( )) both reject a leading-zero string like "08"/
		# "09" outright ("invalid number"/"arithmetic syntax error"), and
		# LuCI's own dropdown stores single digits unpadded (e.g. "8", "0"
		# for midnight), so a plain string compare against `date +%H`
		# (always 2 digits) would otherwise never match.
		_bypass_reload_hour_padded="$_bypass_reload_hour"
		[ "${#_bypass_reload_hour}" = "1" ] && _bypass_reload_hour_padded="0$_bypass_reload_hour"
		if [ "$(date +%H)" = "$_bypass_reload_hour_padded" ] && [ "$_bypass_reload_lastday" != "$_bypass_today" ]; then
			_bypass_should_reload=1
		fi
	fi
	if [ "$_bypass_should_reload" = "1" ]; then
		_log "Reload OMR-ByPass rules to refresh IPs"
		/etc/init.d/omr-bypass reload_rules >/dev/null 2>&1
		uci -q set omr-bypass.global=global
		uci -q set omr-bypass.global.reload_rules_lc=$(date +"%s")
		uci -q set omr-bypass.global.reload_rules_lastday="$(date +%Y%m%d)"
		uci -q commit omr-bypass
	fi
fi

# Remove old hidden config files
find /etc/config/ -mtime +1 -type f -name '\.*' -exec rm {} +
