#!/bin/sh /etc/rc.common
# Copyright 2020-2024 MOSSDeF, Stan Grishin (stangri@melmac.ca)
# shellcheck disable=SC2018,SC2019,SC2034,SC3043,SC3057,SC3060

# sysctl net.ipv4.conf.default.rp_filter=1
# sysctl net.ipv4.conf.all.rp_filter=1

# shellcheck disable=SC2034
START=20
# shellcheck disable=SC2034
USE_PROCD=1

if type extra_command >/dev/null 2>&1; then
	extra_command 'netifd' "Netifd extensions operations"
	extra_command 'on_interface_reload' "Run service on indicated interface reload"
	extra_command 'status' "Generates output required to troubleshoot routing issues
		Use '-d' option for more detailed output
		Use '-p' option to automatically upload data under PBR paste.ee account
			WARNING: while paste.ee uploads are unlisted, they are still publicly available
		List domain names after options to include their lookup in report"
	extra_command 'support' "Show diagnostic info and mask sensitive data"
	extra_command 'version' "Show version information"
else
# shellcheck disable=SC2034
	EXTRA_COMMANDS='help netifd on_interface_reload status version'
# shellcheck disable=SC2034
	EXTRA_HELP="
\tstatus\tGenerates output required to troubleshoot routing issues
\t\tUse '-d' option for more detailed output
\t\tUse '-p' option to automatically upload data under PBR paste.ee account
\t\t\tWARNING: while paste.ee uploads are unlisted, they are still publicly available
\t\tList domain names after options to include their lookup in report
\tsupport\tShow diagnostic info and mask sensitive data
"
fi

readonly packageName='pbr'
readonly PKG_VERSION='1.2.2-r8'
readonly packageCompat='25'
readonly serviceName="$packageName $PKG_VERSION"
readonly packageConfigFile="/etc/config/${packageName}"
readonly packageDebugFile="/var/run/${packageName}.debug"
readonly packageLockFile="/var/run/${packageName}.lock"
readonly packageDnsmasqFile="/var/run/${packageName}.dnsmasq"
readonly runningStatusFile="/dev/shm/${packageName}.status.json"
readonly _OK_='\033[0;32m\xe2\x9c\x93\033[0m'
readonly __OK__='\033[0;32m[\xe2\x9c\x93]\033[0m'
readonly _OKB_='\033[1;34m\xe2\x9c\x93\033[0m'
readonly __OKB__='\033[1;34m[\xe2\x9c\x93]\033[0m'
readonly _FAIL_='\033[0;31m\xe2\x9c\x97\033[0m'
readonly __FAIL__='\033[0;31m[\xe2\x9c\x97]\033[0m'
readonly _ERROR_='\033[0;31mERROR:\033[0m'
readonly _WARNING_='\033[0;33mWARNING:\033[0m'
readonly _DOT_='.'
readonly __DOT__='[w]'
readonly ip_full='/usr/libexec/ip-full'
# shellcheck disable=SC2155
readonly ipTablePrefix="$packageName"
# shellcheck disable=SC2155
readonly agh="$(command -v AdGuardHome)"
# shellcheck disable=SC2155
readonly nft="$(command -v nft)"
readonly nftIPv4Flag='ip'
readonly nftIPv6Flag='ip6'
readonly nftTempFile="/var/run/${packageName}.nft"
readonly nftMainFile="/usr/share/nftables.d/ruleset-post/30-${packageName}.nft"
readonly nftNetifdFile="/usr/share/nftables.d/ruleset-post/20-${packageName}-netifd.nft"
readonly nftPrefix="$packageName"
readonly nftTable='fw4'
readonly chainsList='forward output prerouting'
readonly ssConfigFile='/etc/shadowsocks'
readonly torConfigFile='/etc/tor/torrc'
readonly xrayIfacePrefix='xray_'
readonly rtTablesFile='/etc/iproute2/rt_tables'

# Silence "Command failed: Not found" for redundant procd service delete calls
__UBUS_BIN="$(command -v ubus || echo /bin/ubus)"
ubus() {
	if [ "$1" = "call" ] && [ "$2" = "service" ] && [ "$3" = "delete" ]; then
		"$__UBUS_BIN" "$@" >/dev/null 2>&1 || true
	else
		"$__UBUS_BIN" "$@"
	fi
}
# Wrap ip to emulate `ip rule replace` on builds where it's unavailable.
# We only intercept "rule replace"; everything else is passed through to ip-full.
ip() {
	# If first arg is -4 or -6, we might be handling rules
	if [ "$1" = "-4" ] || [ "$1" = "-6" ]; then
		local fam="$1"
		shift
		# Intercept: ip -4|-6 rule replace ...
		if [ "$1" = "rule" ] && [ "$2" = "replace" ]; then
			shift 2
			# Parse args: capture priority/pref value and rebuild the rest
			local prio=
			local newargs=
			while [ -n "$1" ]; do
				case "$1" in
					priority|pref)
						shift
						prio="$1"
						shift
						continue
						;;
				esac
				newargs="${newargs}${newargs:+ }$1"
				shift
			done
			# If we found a priority, replace = del by priority + add with pref
			if [ -n "$prio" ]; then
				"$ip_full" "$fam" rule del priority "$prio" >/dev/null 2>&1 || true
				# shellcheck disable=SC2086
				"$ip_full" "$fam" rule add $newargs pref "$prio"
				return $?
			fi
			# No priority found; best-effort: just add what we have
			# shellcheck disable=SC2086
			"$ip_full" "$fam" rule add $newargs
			return $?
		fi
		# Not a rule replace: pass through
		"$ip_full" "$fam" "$@"
		return $?
	fi
	# No -4/-6 family: pass straight through
	"$ip_full" "$@"
}

# package config options
enabled=
fw_mask=
icmp_interface=
ignored_interface=
ipv6_enabled=
nft_user_set_policy=
nft_user_set_counter=
procd_boot_trigger_delay=
procd_reload_delay=
lan_device=
uplink_interface=
uplink_interface4=
uplink_interface6=
uplink_interface6_metric='128'
resolver_set=
resolver_instance=
strict_enforcement=
supported_interface=
verbosity=
uplink_ip_rules_priority=
uplink_mark=
prefixlength=
nft_rule_counter=
nft_set_auto_merge=
nft_set_counter=
nft_set_flags_interval=
nft_set_flags_timeout=
nft_set_flags_gc_interval=
nft_set_policy=
nft_set_timeout=
netifd_enabled=
netifd_strict_enforcement=
netifd_interface_default=
netifd_interface_default6=
netifd_interface_local=
config_compat=
config_version=

# run-time
aghConfigFile='/etc/AdGuardHome/AdGuardHome.yaml'
gatewaySummary=
ifaceMark=
ifaceTableID=
ifacePriority=
ifacesAll=
ifacesSupported=
ifacesTriggers=
firewallWanZone=
uplinkGW=
uplinkGW4=
uplinkGW6=
pbrBootFlag=
serviceStartTrigger=
processDnsPolicyError=
processPolicyError=
processPolicyWarning=
resolverSetSupported=
pbrNftPrevParam4=
pbrNftPrevParam6=
nftRuleParams=
nftSetParams=
torDnsPort=
torTrafficPort=
dnsmasq_features=
dnsmasq_ubus=
nft_fw4_dump=
loadEnvironmentFlag=
loadPackageConfigFlag=
resolverWorkingFlag=

# shellcheck disable=SC1091
. "${IPKG_INSTROOT}/lib/functions.sh"
# shellcheck disable=SC1091
. "${IPKG_INSTROOT}/lib/functions/network.sh"
# shellcheck disable=SC1091
. "${IPKG_INSTROOT}/usr/share/libubox/jshn.sh"

debug() { local i j; for i in "$@"; do eval "j=\$$i"; logger "${packageName:+-t ${packageName}}" "${i}: ${j} "; done; }
str_contains() { [ "${1//${2}}" != "$1" ]; }
str_contains_word() { echo "$1" | grep -qw "$2"; }
str_extras_to_underscore() { echo "$1" | sed -E 's/[\. ~`!@#$%^&*()+=,<>?;:\/\\-]/_/g; s/_+/_/g'; }
str_extras_to_space() { echo "$1" | tr ',;{}' ' '; }
str_first_value_interface() { local i; for i in $1; do is_supported_interface "$i" && { echo "$i"; break; }; done; }
str_first_value_ipv4() { local i; for i in $1; do is_ipv4 "$i" && { echo "$i"; break; }; done; }
str_first_value_ipv6() { local i; for i in $1; do is_ipv6 "$i" && { echo "$i"; break; }; done; }
str_first_word() { echo "${1%% *}"; }
str_replace() { echo "${1//${2}/${3}}"; }
str_to_dnsmasq_nftset() { echo "$1" | tr ' ' '/'; }
str_to_lower() { echo "$1" | tr 'A-Z' 'a-z'; }
str_to_upper() { echo "$1" | tr 'a-z' 'A-Z'; }
# shellcheck disable=SC3060
output() {
	[ -z "$verbosity" ] && verbosity="$(uci_get "$packageName" 'config' 'verbosity' '1')"
	[ "$#" -ne '1' ] && {
		case "$1" in [0-9]) [ $((verbosity & $1)) -gt 0 ] && shift || return 0;; esac }
	local msg="$*" queue="/dev/shm/$packageName-output"
	[ -t 1 ] && printf "%b" "$msg"
	[ "$msg" != "${msg//\\n}" ] && {
		[ -s "$queue" ] && msg="$(cat "$queue")${msg}" && rm -f "$queue"
		msg="$(printf "%b" "$msg" | sed 's/\x1b\[[0-9;]*m//g')"
		logger -t "$packageName [$$]" "$(printf "%b" "$msg")"
	} || printf "%b" "$msg" >> "$queue"
}
output_1_newline() { output 1 '\n'; }
output_ok() { output 1 "$_OK_"; output 2 "$__OK__\n"; }
output_okn() { output 1 "$_OK_\n"; output 2 "$__OK__\n"; }
output_okb() { output 1 "$_OKB_"; output 2 "$__OKB__\n"; }
output_okbn() { output 1 "$_OKB_\n"; output 2 "$__OKB__\n"; }
output_fail() { output 1 "$_FAIL_"; output 2 "$__FAIL__\n"; }
output_failn() { output 1 "$_FAIL_\n"; output 2 "$__FAIL__\n"; }
output_dot() { output 1 "$_DOT_"; output 2 "$__DOT__"; }
output_error() { output "${_ERROR_} $*!\n"; }
output_warning() { output "${_WARNING_} $*.\n"; }
quiet_mode() {
	case "$1" in
		on) verbosity=0;;
		off) verbosity="$(uci_get "$packageName" 'config' 'verbosity' '2')";;
	esac
}
pbr_find_iface() {
	local iface i param="$2"
	case "$param" in
		wan6)  iface="$uplink_interface6";;
		wan|*) iface="$uplink_interface4";;
	esac
	eval "$1"='${iface}'
}
pbr_get_gateway4() {
	local iface="$2" dev="$3" gw
	network_get_gateway gw "$iface" true
	if [ -z "$gw" ] || [ "$gw" = '0.0.0.0' ]; then
#		gw="$(ubus call "network.interface.${iface}" status | jsonfilter -e "@.route[0].nexthop")"
		gw="$(ip -4 a list dev "$dev" 2>/dev/null | grep inet | awk '{print $2}' | awk -F "/" '{print $1}')"
	fi
	eval "$1"='$gw'
}
pbr_get_gateway6() {
	local iface="$2" dev="$3" gw
	is_uplink4 "$iface" && iface="$uplink_interface6"
	network_get_gateway6 gw "$iface" true
	if [ -z "$gw" ] || [ "$gw" = '::/0' ] || [ "$gw" = '::0/0' ] || [ "$gw" = '::' ]; then
		gw="$(ip -6 a list dev "$dev" 2>/dev/null | grep inet6 | grep 'scope global' | awk '{print $2}')"
	fi
	eval "$1"='$gw'
}
filter_options() {
	local opt="$1" values="$2" v _ret
	for v in $values; do
		if str_contains "$opt" '_negative'; then
			is_negated "$v" || continue
			opt="${opt/_negative}"
		fi
		eval "is_$opt" "${v/\!}" || continue
		_ret="${_ret:+${_ret} }$v"
	done
	echo "$_ret"
	return 0
}
inline_set() {
	local value="$1" inline_set i
	for i in $value; do
		inline_set="${inline_set:+${inline_set}, }${i#[@\!]}"
	done
	echo "$inline_set"
}
# shellcheck disable=SC2016
is_bad_user_file_nft_call() { grep -q '"\$nft" list' "$1" || grep '"\$nft" -f' "$1"; }
# shellcheck disable=SC2317
is_config_enabled() {
# shellcheck disable=SC2329
	_check_config() { local en; config_get_bool en "$1" 'enabled' '1'; [ "$en" -gt '0' ] && _cfg_enabled=0; }
	local cfg="$1" _cfg_enabled=1
	[ -n "$1" ] || return 1
	config_load "$packageName"
	config_foreach _check_config "$cfg"
	return "$_cfg_enabled"
}
is_default_dev() { [ "$1" = "$(ip -4 route show default | awk '{for(i=1;i<=NF;i++) if($i=="dev"){print $(i+1);exit}}')" ]; }
is_netifd_interface_default() {
	is_netifd_interface "$1" || return 1
	[ "$netifd_interface_default" = "$1" ] && return 0
	[ "$netifd_interface_default6" = "$1" ] && return 0
	return 1
}
is_disabled_interface() { [ "$(uci_get 'network' "$1" 'disabled')" = '1' ]; }
is_host() { echo "$1" | grep -qE '^[a-zA-Z0-9][a-zA-Z0-9_-]{0,61}[a-zA-Z0-9]$|^[a-zA-Z0-9]$'; }
is_hostname() { echo "$1" | grep -qE '^([a-zA-Z0-9]([a-zA-Z0-9_-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'; }
is_domain() { ! is_ipv4 "$1" && ! is_mac_address_bad_notation "$1" && { is_host "$1" || is_hostname "$1"; }; }
is_dslite() { local p; network_get_protocol p "$1"; [ "${p:0:6}" = "dslite" ]; }
is_family_mismatch() { ( is_ipv4 "${1//!}" && is_ipv6 "${2//!}" ) || ( is_ipv6 "${1//!}" && is_ipv4 "${2//!}" ); }
is_greater() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; }
is_greater_or_equal() { test "$(printf '%s\n' "$@" | sort -V | head -n '1')" = "$2"; }
is_ignored_interface() { str_contains_word "$ignored_interface" "$1"; }
is_ignore_target() { [ "$(str_to_lower "$1")" = 'ignore' ]; }
is_integer() { case "$1" in ''|*[!0-9]*) return 1;; esac; }
is_ipv4() { echo "$1" | grep -qE '^((25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})\.){3}(25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})(/(3[0-2]|[12]?[0-9]))?$'; }
is_ipv6() { ! is_mac_address "$1" && str_contains "$1" ':'; }
is_ipv6_global_scope() { [ "${1:0:4}" = '2001' ]; }
is_ipv6_local_scope() { is_ipv6_local_link "$1" || is_ipv6_local_unique "$1"; }
is_ipv6_local_link() { [ "${1:0:4}" = 'fe80' ]; }
is_ipv6_local_unique() { [ "${1:0:2}" = 'fc' ] || [ "${1:0:2}" = 'fd' ]; }
is_list() { str_contains "$1" ',' || str_contains "$1" ' '; }
is_lan() { local d; network_get_device d "$1"; str_contains "$lan_device" "$d"; }
is_l2tp() { local p; network_get_protocol p "$1"; [ "${p:0:4}" = "l2tp" ]; }
is_mac_address() { echo "$1" | grep -qE '^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$'; }
is_mac_address_bad_notation() { echo "$1" | grep -qE '^([0-9A-Fa-f]{2}-){5}([0-9A-Fa-f]{2})$'; }
is_negated() { [ "${1:0:1}" = '!' ]; }
is_netifd_table() { grep -q "ip.table.*$1" /etc/config/network; }
is_netifd_interface() { local iface="$1"; [ -n "$(uci_get 'network' "$iface" 'ip4table')" ] || [ -n "$(uci_get 'network' "$iface" 'ip6table')" ]; }
is_oc() { local p; network_get_protocol p "$1"; [ "${p:0:11}" = "openconnect" ]; }
is_ovpn() { local d; uci_get_device d "$1"; [ "${d:0:3}" = "tun" ] || [ "${d:0:3}" = "tap" ] || [ -f "/sys/devices/virtual/net/${d}/tun_flags" ]; }
is_ovpn_valid() { local dev_net dev_ovpn; uci_get_device dev_net "$1"; dev_ovpn="$(uci_get 'openvpn' "$1" 'dev')"; [ -n "$dev_net" ] && [ -n "$dev_ovpn" ] && [ "$dev_net" = "$dev_ovpn" ]; }
is_phys_dev() { [ "${1:0:1}" = "@" ] && [ -L "/sys/class/net/${1#@}" ]; }
is_present() { command -v "$1" >/dev/null 2>&1; }
is_service_running() { is_service_running_nft; }
is_service_running_nft() { [ -x "$nft" ] && [ -n "$(get_mark_nft_chains)" ]; }
is_supported_iface_dev() { local n dev; for n in $ifacesSupported; do network_get_device dev "$n"; [ "$1" = "$dev" ] && return 0; done; return 1; }
is_supported_protocol() { grep -qi "^${1:--}" /etc/protocols;}
is_pptp() { local p; network_get_protocol p "$1"; [ "${p:0:4}" = "pptp" ]; }
is_softether() { local d; network_get_device d "$1"; [ "${d:0:4}" = "vpn_" ]; }
is_split_uplink() { [ -n "$ipv6_enabled" ] && [ "$uplink_interface4" != "$uplink_interface6" ]; }
is_supported_interface() { { is_lan "$1" || is_disabled_interface "$1"; } && return 1; str_contains_word "$supported_interface" "$1" || { ! is_ignored_interface "$1" && { is_uplink "$1" || is_wan "$1" || is_tunnel "$1"; }; } || is_ignore_target "$1" || is_xray "$1"; }
is_netbird() { local d; network_get_device d "$1"; [ "${d:0:2}" = "wt" ]; }
is_tailscale() { local d; network_get_device d "$1"; [ "${d:0:9}" = "tailscale" ]; }
is_tor() { [ "$(str_to_lower "$1")" = "tor" ]; }
is_tor_running() { ! is_ignored_interface 'tor' && [ -s "$torConfigFile" ] && str_contains "$(ubus call service list "{ 'name': 'tor' }" | jsonfilter -e '@.tor.instances.*.running')" 'true' && return 0 || return 1; }
is_tunnel() { is_dslite "$1" || is_l2tp "$1" || is_oc "$1" || is_ovpn "$1" || is_pptp "$1" || is_softether "$1" || is_netbird "$1" || is_tailscale "$1" || is_tor "$1" || is_wg "$1"; }
is_url() { is_url_file "$1" || is_url_dl "$1"; }
is_url_dl() { is_url_ftp "$1" || is_url_http "$1" || is_url_https "$1"; }
is_url_file() { [ "$1" != "${1#file://}" ]; }
is_url_ftp() { [ "$1" != "${1#ftp://}" ]; }
is_url_http() { [ "$1" != "${1#http://}" ]; }
is_url_https() { [ "$1" != "${1#https://}" ]; }
is_uplink4() { [ "$1" = "$uplink_interface4" ]; }
is_uplink6() { [ -n "$ipv6_enabled" ] && [ "$1" = "$uplink_interface6" ]; }
is_uplink() { is_uplink4 "$1" || is_uplink6 "$1"; }
is_wan6() { [ -n "$ipv6_enabled" ] || return 1; case "$1" in wan*6|*wan6) return 0;; *) return 1;; esac; }
is_wan4() { case "$1" in wan*6|*wan6) return 1;; wan*|*wan) return 0;; *) return 1;; esac; }
is_wan() { is_wan4 "$1" || is_wan6 "$1"; }
is_wg() { local p lp; network_get_protocol p "$1"; uci_get_listen_port lp "$1"; [ -z "$lp" ] && [ "${p:0:9}" = "wireguard" ]; }
is_wg_server() { local p lp; network_get_protocol p "$1"; uci_get_listen_port lp "$1"; [ -n "$lp" ] && [ "${p:0:9}" = "wireguard" ]; }
is_xray() { [ -n "$(get_xray_traffic_port "$1")" ]; }
dnsmasq_kill() { pidof dnsmasq >/dev/null && kill -HUP $(pidof dnsmasq); }
dnsmasq_restart() { output 3 'Restarting dnsmasq '; if /etc/init.d/dnsmasq restart >/dev/null 2>&1; then output_okn; else output_failn; fi; }
exists_lockfile() { [ -e "$packageLockFile" ]; }
# shellcheck disable=SC2155
get_ss_traffic_ports() { local i="$(jsonfilter -i "$ssConfigFile" -q -e "@.inbounds[*].port")"; echo "${i:-443}"; }
# shellcheck disable=SC2155
get_tor_dns_port() { local i="$(grep -m1 DNSPort "$torConfigFile" | awk -F: '{print $2}')"; echo "${i:-9053}"; }
# shellcheck disable=SC2155
get_tor_traffic_port() { local i="$(grep -m1 TransPort "$torConfigFile" | awk -F: '{print $2}')"; echo "${i:-9040}"; }
get_xray_traffic_port() { local i="${1//${xrayIfacePrefix}}"; [ "$i" = "$1" ] && unset i; echo "$i"; }
get_rt_tables_id() { local iface="$1"; grep "${ipTablePrefix}_${iface}\$" "$rtTablesFile" | awk '{print $1;}'; }
get_rt_tables_next_id() { echo "$(($(sort -r -n "$rtTablesFile" | grep -o -E -m 1 "^[0-9]+")+1))"; }
get_rt_tables_non_pbr_next_id() { echo "$(($(grep -v "${ipTablePrefix}_" "$rtTablesFile" | sort -r -n  | grep -o -E -m 1 "^[0-9]+")+1))"; }
# shellcheck disable=SC2016
resolveip_to_nftset() { resolver 'wait' && resolveip "$@" | sed -n 'H;${x;s/\n/,/g;s/^,//;p;};d'; }
resolveip_to_nftset4() { resolveip_to_nftset -4 "$@"; }
resolveip_to_nftset6() { [ -n "$ipv6_enabled" ] && resolveip_to_nftset -6 "$@"; }
# shellcheck disable=SC2016
ipv4_leases_to_nftset() { [ -s '/tmp/dhcp.leases' ] && awk -v arg="$1" 'BEGIN{fs=""};$0~arg{printf fs$3;fs=","}' /tmp/dhcp.leases;}
# shellcheck disable=SC2016
ipv6_leases_to_nftset() { [ -s '/tmp/hosts/odhcpd' ] && awk -v arg="$1" 'BEGIN{fs=""};$0~arg{printf fs$1;fs=","}' /tmp/hosts/odhcpd;}
# shellcheck disable=SC3037
ports_to_nftset() { echo -en "$1"; }
get_mark_nft_chains() { "$nft" list table inet "$nftTable" 2>/dev/null | grep chain | grep "${nftPrefix}_mark_" | awk '{ print $2 }'; }
get_nft_sets() { "$nft" list table inet "$nftTable" 2>/dev/null | grep 'set' | grep "${nftPrefix}_" | awk '{ print $2 }'; }
__ubus_get() { ubus call service list "{ 'name': '$packageName' }" | jsonfilter -e "$1"; }
ubus_get_status() { __ubus_get "@.${packageName}.instances.main.data.status.${1}"; }
ubus_get_interface() { __ubus_get "@.${packageName}.instances.main.data.gateways[@.name='${1}']${2:+.${2}}"; }
ubus_get_gateways() { __ubus_get "@.${packageName}.instances.main.data.gateways"; }
config_get_list() { config_get "$@"; }
uci_get_device() {
	local __tmp
	__tmp="$(uci_get 'network' "$2" 'device')"
	[ -z "$__tmp" ] && unset "$1" && return 1
	eval "$1=$__tmp"
}
uci_get_protocol() { uci_get 'network' "$1" 'proto'; }
uci_add_list_if_new() {
	local PACKAGE="$1"
	local CONFIG="$2"
	local OPTION="$3"
	local VALUE="$4"
	local i
	[ -n "$PACKAGE" ] && [ -n "$CONFIG" ] && [ -n "$OPTION" ] && [ -n "$VALUE" ] || return 1
	for i in $(uci_get "$PACKAGE" "$CONFIG" "$OPTION"); do
		[ "$i" = "$VALUE" ] && return 0
	done
	uci_add_list "$PACKAGE" "$CONFIG" "$OPTION" "$VALUE"
}
uci_changes() {
	local PACKAGE="$1"
	local CONFIG="$2"
	local OPTION="$3"
	[ -s "${UCI_CONFIG_DIR:-/etc/config/}${PACKAGE}" ] && \
	[ -n "$(/sbin/uci ${UCI_CONFIG_DIR:+-c ${UCI_CONFIG_DIR}} changes "$PACKAGE${CONFIG:+.${CONFIG}}${OPTION:+.${OPTION}}")" ]
}
uci_get_listen_port() {
	local __tmp
	__tmp="$(uci_get 'network' "$2" 'listen_port')"
	[ -z "$__tmp" ] && unset "$1" && return 1
	eval "$1=$__tmp"
}
sanitize_list() { sed 's/#.*//;s/^[ \t]*//;s/[ \t]*$//;s/[ \t][ \t]*/ /g;/^[ \t]*$/d' "$1" | sort -u | tr '\n' ' '; }

# luci app specific
is_enabled() { uci_get "$1" 'config' 'enabled'; }
is_running_nft_file() { [ -s "$nftMainFile" ]; }
is_running_nft() { "$nft" list table inet "$nftTable" | grep chain | grep -q "${nftPrefix}_mark_" >/dev/null 2>&1; }
check_nft() { [ -x "$nft" ]; }
check_agh() { [ -x "$agh" ] && { [ -s "$aghConfigFile" ] || [ -s "${agh%/*}/AdGuardHome.yaml" ]; }; }
check_dnsmasq() { command -v dnsmasq >/dev/null 2>&1; }
check_unbound() { command -v unbound >/dev/null 2>&1; }
check_dnsmasq_nftset() {
	[ -z "$dnsmasq_features" ] && dnsmasq_features="$(dnsmasq --version | grep -m1 'Compile time options:' | cut -d: -f2) "
	[ "${dnsmasq_features#* nftset }" != "$dnsmasq_features" ]
}

# mwan4 detection and integration functions
mwan4_is_installed() {
	[ -x /etc/init.d/mwan4 ] && [ -f /etc/config/mwan4 ]
}

mwan4_is_running() {
	mwan4_is_installed && /etc/init.d/mwan4 running >/dev/null 2>&1
}

mwan4_get_iface_list() {
	# Get list of enabled mwan4 interfaces from UCI config
	# Output: space-separated list of interface names
	local iface_list=""
	mwan4_is_installed || return 1

	_mwan4_collect_iface() {
		local en
		config_get_bool en "$1" 'enabled' '0'
		[ "$en" -gt '0' ] && iface_list="${iface_list}${1} "
	}
	config_load 'mwan4'
	config_foreach _mwan4_collect_iface 'interface'

	echo "${iface_list% }"
}

mwan4_get_strategy_list() {
	# Get list of mwan4 strategies from UCI config
	# Output: space-separated list of strategy names
	local strategy_list=""
	mwan4_is_installed || return 1

	_mwan4_collect_strategy() { strategy_list="${strategy_list}${1} "; }
	config_load 'mwan4'
	config_foreach _mwan4_collect_strategy 'strategy'

	echo "${strategy_list% }"
}

mwan4_get_iface_mark_chain() {
	# Get the nftables marking chain name for a specific mwan4 interface
	# $1 = interface name
	# Output: chain name (e.g., "mwan4_iface_in_wan")
	local iface="$1"
	[ -z "$iface" ] && return 1
	mwan4_is_running || return 1

	# Check if the chain exists in nftables
	if "$nft" list chain inet fw4 "mwan4_iface_in_${iface}" >/dev/null 2>&1; then
		echo "mwan4_iface_in_${iface}"
		return 0
	fi
	return 1
}

mwan4_get_iface_nft_sets() {
	# Get the nftables set names used by a specific mwan4 interface
	# $1 = interface name
	# Output: space-separated list of set names
	local iface="$1"
	local family=""
	local sets=""
	[ -z "$iface" ] && return 1
	mwan4_is_installed || return 1

	# Get the family (ipv4/ipv6) for the interface
	config_load 'mwan4'
	config_get family "$iface" 'family' 'ipv4'

	# The sets used by mwan4 per interface family
	for settype in connected custom dynamic; do
		if "$nft" list set inet fw4 "mwan4_${settype}_${family}" >/dev/null 2>&1; then
			sets="${sets}mwan4_${settype}_${family} "
		fi
	done

	echo "${sets% }"
}

mwan4_get_strategy_chain() {
	# Get the nftables strategy chain name for a specific mwan4 strategy
	# $1 = strategy name
	# $2 = family (ipv4 or ipv6, defaults to ipv4)
	# Output: chain name (e.g., "mwan4_strategy_balanced_ipv4")
	local strategy="$1"
	local family="${2:-ipv4}"
	[ -z "$strategy" ] && return 1
	mwan4_is_running || return 1

	# Check if the chain exists in nftables
	if "$nft" list chain inet fw4 "mwan4_strategy_${strategy}_${family}" >/dev/null 2>&1; then
		echo "mwan4_strategy_${strategy}_${family}"
		return 0
	fi
	return 1
}

mwan4_get_mmx_mask() {
	# Get the MMX (Multi-WAN Mark) mask used by mwan4
	# Output: hex mask value (e.g., "0x3F00")
	mwan4_is_installed || return 1

	local mask
	config_load 'mwan4'
	config_get mask 'globals' 'mmx_mask' '0x3F00'
	echo "$mask"
}

print_json_bool() { json_init; json_add_boolean "$1" "$2"; json_dump; json_cleanup; }
print_json_string() { json_init; json_add_string "$1" "$2"; json_dump; json_cleanup; }
try() {
	if ! "$@" >/dev/null 2>&1; then
		json add error 'errorTryFailed' "$*"
		return 1
	fi
}

get_url() {
	printf "https://docs.openwrt.melmac.ca/%s/%s/%s" "$packageName" "${PKG_VERSION%%-*}" "$1"
}

get_text() {
	local r="$1"; shift;
	case "$r" in
		errorConfigValidation) printf "Config (%s) validation failure" "$packageConfigFile";;
		errorNoNft) printf "Resolver set support (%s) requires nftables, but nft binary cannot be found" "$resolver_set";;
		errorResolverNotSupported) printf "Resolver set (%s) is not supported on this system" "$resolver_set";;
		errorServiceDisabled) printf "The %s service is currently disabled" "$packageName";;
		errorNoUplinkGateway) printf "The %s service failed to discover uplink gateway" "$serviceName";;
		errorNoUplinkInterface) printf "The %s interface not found, you need to set the 'pbr.config.uplink_interface' option" "$1";;
		errorNoUplinkInterfaceHint) printf "Refer to %s" "$1";;
		errorNftsetNameTooLong) printf "The nft set name '%s' is longer than allowed 255 characters" "$1";;
		errorUnexpectedExit) printf "Unexpected exit or service termination: '%s'" "$1";;
		errorPolicyNoSrcDest) printf "Policy '%s' has no source/destination parameters" "$1";;
		errorPolicyNoInterface) printf "Policy '%s' has no assigned interface" "$1";;
		errorPolicyNoDns) printf "Policy '%s' has no assigned DNS" "$1";;
		errorPolicyProcessNoInterfaceDns) printf "Interface '%s' has no assigned DNS" "$1";;
		errorPolicyUnknownInterface) printf "Policy '%s' has an unknown interface" "$1";;
		errorPolicyProcessCMD) printf "'%s'" "$1";;
		errorFailedSetup) printf "Failed to set up '%s'" "$1";;
		errorFailedReload) printf "Failed to reload '%s'" "$1";;
		errorUserFileNotFound) printf "Custom user file '%s' not found or empty" "$1";;
		errorUserFileSyntax) printf "Syntax error in custom user file '%s'" "$1";;
		errorUserFileRunning) printf "Error running custom user file '%s'" "$1";;
		errorUserFileNoCurl) printf "Use of 'curl' is detected in custom user file '%s', but 'curl' isn't installed" "$1";;
		errorNoGateways) printf "Failed to set up any gateway";;
		errorResolver) printf "Resolver '%s'" "$1";;
		errorPolicyProcessNoIpv6) printf "Skipping IPv6 policy '%s' as IPv6 support is disabled" "$1";;
		errorPolicyProcessUnknownFwmark) printf "Unknown packet mark for interface '%s'" "$1";;
		errorPolicyProcessMismatchFamily) printf "Mismatched IP family between in policy '%s'" "$1";;
		errorPolicyProcessUnknownProtocol) printf "Unknown protocol in policy '%s'" "$1";;
		errorPolicyProcessInsertionFailed) printf "Insertion failed for both IPv4 and IPv6 for policy '%s'" "$1";;
		errorPolicyProcessInsertionFailedIpv4) printf "Insertion failed for IPv4 for policy '%s'" "$1";;
		errorPolicyProcessUnknownEntry) printf "Unknown entry in policy '%s'" "$1";;
		errorInterfaceRoutingEmptyValues) printf "Received empty tid/mark or interface name when setting up routing";;
		errorInterfaceMarkOverflow) printf "Interface mark for '%s' exceeds the fwmask value" "$1";;
		errorFailedToResolve) printf "Failed to resolve '%s'" "$1";;
		errorInvalidOVPNConfig) printf "Invalid OpenVPN config for '%s' interface" "$1";;
		errorNftMainFileInstall) printf "Failed to install fw4 nft file '%s'" "$1";;
		errorTryFailed) printf "Command failed: %s" "$1";;
		errorDownloadUrlNoHttps) printf "Failed to download '%s', HTTPS is not supported" "$1";;
		errorDownloadUrl) printf "Failed to download '%s'" "$1";;
		errorNoDownloadWithSecureReload) printf "Policy '%s' refers to URL which can't be downloaded in 'secure_reload' mode" "$1";;
		errorFileSchemaRequiresCurl) printf "The file:// schema requires curl, but it's not detected on this system";;
		errorIncompatibleUserFile) printf "Incompatible custom user file detected '%s'" "$1";;
		errorDefaultFw4TableMissing) printf "Default fw4 table '%s' is missing" "$1";;
		errorDefaultFw4ChainMissing) printf "Default fw4 chain '%s' is missing" "$1";;
		errorRequiredBinaryMissing) printf "Required binary '%s' is missing" "$1";;
		errorInterfaceRoutingUnknownDevType) printf "Unknown IPv6 Link type for device '%s'" "$1";;
		errorUplinkDown) printf "Uplink/WAN interface is still down, increase value of 'procd_boot_trigger_delay' option";;
		errorMktempFileCreate) printf "Failed to create temporary file with mktemp mask: '%s'" "$1";;
		errorSummary) printf "Errors encountered, please check %s" "$1";;
		errorNftNetifdFileInstall) printf "Netifd setup: failed to install fw4 netifd nft file '%s'" "$1";;
		errorNftNetifdFileDelete) printf "Netifd setup: failed to remove fw4 netifd nft file '%s'" "$1";;
		errorNetifdMissingOption) printf "Netifd setup: required option '%s' is missing" "$1";;
		errorNetifdInvalidGateway4) printf "Netifd setup: invalid value of netifd_interface_default option '%s'" "$1";;
		errorNetifdInvalidGateway6) printf "Netifd setup: invalid value of netifd_interface_default6 option '%s'" "$1";;
		warningInvalidOVPNConfig) printf "Invalid OpenVPN config for '%s' interface" "$1";;
		warningResolverNotSupported) printf "Resolver set (%s) is not supported on this system" "$resolver_set";;
		warningPolicyProcessCMD) printf "'%s'" "$1";;
		warningTorUnsetParams) printf "Please unset 'src_addr', 'src_port' and 'dest_port' for policy '%s'" "$1";;
		warningTorUnsetProto) printf "Please unset 'proto' or set 'proto' to 'all' for policy '%s'" "$1";;
		warningTorUnsetChainNft) printf "Please unset 'chain' or set 'chain' to 'prerouting' for policy '%s'" "$1";;
		warningOutdatedLuciPackage) printf "The WebUI application is outdated (version %s), please update it" "$1";;
		warningDnsmasqInstanceNoConfdir) printf "Dnsmasq instance '%s' targeted in settings, but it doesn't have its own confdir" "$1";;
		warningDhcpLanForce) printf "Please set 'dhcp.%s.force=1' to speed up service start-up" "$1";;
		warningSummary) printf "Warnings encountered, please check %s" "$(get_url '#warning-messages-details')";;
		warningIncompatibleDHCPOption6) printf "Incompatible DHCP Option 6 for interface '%s'" "$1";;
		warningNetifdMissingInterfaceLocal) printf "Netifd setup: option netifd_interface_local is missing, assuming '%s'" "$1";;
		warningUplinkDown) printf "Uplink/WAN interface is still down, going back to boot mode";;
		*) printf "Unknown error/warning '%s'" "$1";;
	esac
}

process_url() {
	local url="$1"
	local dl_command dl_https_supported dl_temp_file
# TODO: check for FILE schema and missing curl
	if is_present 'curl'; then
		dl_command="curl --silent --insecure"
		dl_flag="-o"
	elif is_present '/usr/libexec/wget-ssl'; then
		dl_command="/usr/libexec/wget-ssl --no-check-certificate -q"
		dl_flag="-O"
	elif is_present wget && wget --version 2>/dev/null | grep -q "+https"; then
		dl_command="wget --no-check-certificate -q"
		dl_flag="-O"
	else
		dl_command="uclient-fetch --no-check-certificate -q"
		dl_flag="-O"
	fi
	if curl --version 2>/dev/null | grep -q "Protocols: .*https.*" \
		|| wget --version 2>/dev/null | grep -q "+ssl"; then
		dl_https_supported=1
	else
		unset dl_https_supported
	fi
	dl_temp_file="$(mktemp -q -t "${packageName}_tmp.XXXXXXXX")"
	if [ -z "$dl_temp_file" ] || [ ! -e "$dl_temp_file"  ]; then
		json add error 'errorMktempFileCreate' "${packageName}_tmp.XXXXXXXX"
		return 1
	fi
	if is_url_file "$url" && ! is_present 'curl'; then
		json add error 'errorFileSchemaRequiresCurl' "$url"
	elif is_url_https "$url" && [ -z "$dl_https_supported" ]; then
		json add error 'errorDownloadUrlNoHttps' "$url"
	elif $dl_command "$url" "$dl_flag" "$dl_temp_file" 2>/dev/null; then
		sanitize_list "$dl_temp_file"
	else
		json add error 'errorDownloadUrl' "$url"
	fi
	rm -f "$dl_temp_file"
}

load_package_config() {
	local param="$1"
	config_load "$packageName"
	config_get      config_compat             'config' 'config_compat'
	config_get      config_version            'config' 'config_version'
	config_get_bool enabled                   'config' 'enabled'                  '0'
	config_get      fw_mask                   'config' 'fw_mask'                  '00ff0000'
	config_get      icmp_interface            'config' 'icmp_interface'
	config_get_list ignored_interface         'config' 'ignored_interface'
	config_get_bool ipv6_enabled              'config' 'ipv6_enabled'             '0'
	config_get_list lan_device                'config' 'lan_device'               'br-lan'
	config_get_bool nft_rule_counter          'config' 'nft_rule_counter'         '0'
	config_get_bool nft_set_auto_merge        'config' 'nft_set_auto_merge'       '1'
	config_get_bool nft_set_counter           'config' 'nft_set_counter'          '0'
	config_get_bool nft_set_flags_interval    'config' 'nft_set_flags_interval'   '1'
	config_get_bool nft_set_flags_timeout     'config' 'nft_set_flags_timeout'    '0'
	config_get      nft_set_gc_interval       'config' 'nft_set_gc_interval'
	config_get      nft_set_policy            'config' 'nft_set_policy'          'performance'
	config_get      nft_set_timeout           'config' 'nft_set_timeout'
	config_get_bool nft_user_set_counter      'config' 'nft_user_set_counter'     '0'
	config_get      prefixlength              'config' 'prefixlength'             '1'
	config_get      procd_boot_trigger_delay  'config' 'procd_boot_trigger_delay' '5000'
	config_get      procd_reload_delay        'config' 'procd_reload_delay'       '0'
	config_get_list resolver_instance         'config' 'resolver_instance'        '*'
	config_get      resolver_set              'config' 'resolver_set'
	config_get_bool strict_enforcement        'config' 'strict_enforcement'       '1'
	config_get_list supported_interface       'config' 'supported_interface'
	config_get      uplink_interface          'config' 'uplink_interface'         'wan'
	config_get      uplink_interface6         'config' 'uplink_interface6'        'wan6'
	config_get      uplink_ip_rules_priority  'config' 'uplink_ip_rules_priority' '30000'
	config_get      uplink_mark               'config' 'uplink_mark'              '00010000'
	config_get      verbosity                 'config' 'verbosity'                '2'
	config_get_bool webui_show_ignore_target  'config' 'webui_show_ignore_target' '0'
	config_get_bool netifd_enabled            'config' 'netifd_enabled'
	config_get_bool netifd_strict_enforcement 'config' 'netifd_strict_enforcement'
	config_get      netifd_interface_default  'config' 'netifd_interface_default'
	config_get      netifd_interface_default6 'config' 'netifd_interface_default6'
	config_get      netifd_interface_local    'config' 'netifd_interface_local'

	uplink_interface4="$uplink_interface"
	fw_mask="0x${fw_mask}"
	uplink_mark="0x${uplink_mark}"

	[ "$resolver_set" = 'none' ]      && unset resolver_set
	[ "$enabled" = '1' ]              || unset enabled
	[ "$ipv6_enabled" = '1' ]         || unset uplink_interface6
	[ "$ipv6_enabled" = '1' ]         || unset ipv6_enabled
	[ "$strict_enforcement" = '1' ]   || unset strict_enforcement

	fw_maskXor="$(printf '%#x' "$((fw_mask ^ 0xffffffff))")"
	fw_maskXor="${fw_maskXor:-0xff00ffff}"

	is_integer "$procd_boot_trigger_delay" || procd_boot_trigger_delay='5000'
	[ "$procd_boot_trigger_delay" -lt '1000' ] && procd_boot_trigger_delay='1000'

	local nft_set_flags
	case "${nft_set_flags_interval}:${nft_set_flags_timeout}" in
		1:1) nft_set_flags="flags interval, timeout${nft_set_timeout:+; timeout ${nft_set_timeout}}";;
		1:0) nft_set_flags='flags interval';;
		0:1) nft_set_flags="flags timeout${nft_set_timeout:+; timeout ${nft_set_timeout}}";;
		0:0) nft_set_flags='';;
	esac

	[ "$nft_user_set_counter" = '1' ]    || unset nft_user_set_counter
	[ "$nft_rule_counter" = '1' ]        || unset nft_rule_counter
	[ "$nft_set_auto_merge" = '1' ]      || unset nft_set_auto_merge
	[ "$nft_set_counter" = '1' ]         || unset nft_set_counter
	[ "$nft_set_flags_interval" = '1' ]  || unset nft_set_flags_interval
	[ "$nft_set_flags_timeout" = '1' ]   || unset nft_set_flags_timeout
	[ -n "${nft_set_flags_timeout}${nft_set_timeout}" ] || unset nft_set_gc_interval

	nftRuleParams="${nft_rule_counter:+counter}"

	nftSetParams=" \
		${nft_set_auto_merge:+ auto-merge;} \
		${nft_set_counter:+ counter;} \
		${nft_set_flags:+ ${nft_set_flags};} \
		${nft_set_gc_interval:+ gc_interval "${nft_set_gc_interval}";} \
		${nft_set_policy:+ policy "${nft_set_policy}";} \
		${nft_set_timeout:+ timeout "${nft_set_timeout}";} \
		"

	if [ -x "$agh" ] && [ ! -s "$aghConfigFile" ]; then
		[ -s "${agh%/*}/AdGuardHome.yaml" ] && aghConfigFile="${agh%/*}/AdGuardHome.yaml"
	fi
	unset loadEnvironmentFlag
	loadPackageConfigFlag='true'
}

# shellcheck disable=SC2317
load_environment() {
	_system_health_check() {
# shellcheck disable=SC2329
		_check_lan_compatibility() {
			is_lan "$1" || return 0
			local force ipaddr dhcp_option i
			config_get force       "$1" force
			config_get ipaddr      "$1" ipaddr
			if [ "$force" = '0' ]; then
				json add warning 'warningDhcpLanForce' "$1"
			fi
			[ -n "$resolver_set" ] || return 0
			for i in $(uci_get 'dhcp' "$1" 'dhcp_option'); do
			local option="${i%%,*}" value="${i#*,}"
			if [ "$option" = '6' ] && [ "$value" != "${ipaddr%%/*}" ]; then
				json add warning 'warningIncompatibleDHCPOption6' "${1}: ${value}"
			fi
			done
		}
		local i _ret=0
		if ! check_nft; then
			json add error 'errorNoNft'
			_ret='1'
		fi
		if [ "$(uci_get 'firewall' 'defaults' 'auto_includes')" = '0' ]; then
			uci_remove 'firewall' 'defaults' 'auto_includes'
			uci_commit 'firewall'
		fi
		if [ "$(readlink /sbin/ip)" != "$ip_full" ]; then
			json add error 'errorRequiredBinaryMissing' 'ip-full'
			_ret='1'
		fi
		if ! nft_check_element 'table' 'fw4'; then
			json add error 'errorDefaultFw4TableMissing' 'fw4'
			_ret='1'
		fi
		if is_config_enabled 'dns_policy' || is_tor_running; then
			if ! nft_check_element 'chain' 'dstnat'; then
				json add error 'errorDefaultFw4ChainMissing' 'dstnat'
				_ret='1'
			fi
		fi
		for i in $chainsList; do
			if ! nft_check_element 'chain' "mangle_${i}"; then
				json add error 'errorDefaultFw4ChainMissing' "mangle_${i}"
				_ret='1'
			fi
		done
		config_load 'network'
		config_foreach _check_lan_compatibility 'interface'
		return "$_ret"
	}
	local param="$1" validation_result="$2"
	[ -n "$loadEnvironmentFlag" ] && return 0
	case "$param" in
		on_boot|on_start)
			output 1 "Loading environment ($param) "
			[ -n "$loadPackageConfigFlag" ] || load_package_config "$param"
			if [ -z "$enabled" ]; then
				output 1 "$_FAIL_\n"
				json add error 'errorServiceDisabled'
				output_error "$(get_text 'errorServiceDisabled')"
				output "Run the following commands before starting service again:\n"
				output "uci set ${packageName}.config.enabled='1'; uci commit $packageName;\n"
				return 1
			fi
			if [ -n "$validation_result" ] && [ "$validation_result" != '0' ]; then
				output 1 "$_FAIL_\n"
				json add error 'errorConfigValidation'
				output_error "$(get_text 'errorConfigValidation')"
				output "Please check if the '$packageConfigFile' contains correct values for config options.\n"
				return 1
			fi
			_system_health_check || { output 1 "$_FAIL_\n"; return 1; }
			resolver 'check_support'
			load_network "$param"
			output 1 "$_OK_\n"
		;;
		on_triggers)
			[ -n "$loadPackageConfigFlag" ] || load_package_config "$param"
		;;
		on_interface_reload|on_reload|on_stop|*)
			output 1 "Loading environment ($param) "
			[ -n "$loadPackageConfigFlag" ] || load_package_config "$param"
			load_network "$param"
			resolver 'check_support'
			output 1 "$_OK_\n"
		;;
	esac
	loadEnvironmentFlag='true'
}

# shellcheck disable=SC2317
load_network() {
# shellcheck disable=SC2329
	_build_ifaces_supported() { is_supported_interface "$1" && ! str_contains "$ifacesSupported" "$1" && ifacesSupported="${ifacesSupported}${1} "; }
# shellcheck disable=SC2329
	_find_firewall_wan_zone() { [ "$(uci_get 'firewall' "$1" 'name')" = 'wan' ] && firewallWanZone="$1"; }
	local i param="$1"
	local dev4 dev6
	if [ -z "$ifacesSupported" ]; then
		config_load 'firewall'
		config_foreach _find_firewall_wan_zone 'zone'
		for i in $(uci_get 'firewall' "$firewallWanZone" 'network'); do
			is_supported_interface "$i" && ! str_contains "$ifacesSupported" "$i" && ifacesSupported="${ifacesSupported}${i} "
		done
		config_load 'network'
		config_foreach _build_ifaces_supported 'interface'
	fi
	network_get_device dev4 "$uplink_interface4"
	[ -z "$dev4" ] && network_get_physdev dev4 "$uplink_interface4"
	[ -z "$uplinkGW4" ] && pbr_get_gateway4 uplinkGW4 "$uplink_interface4" "$dev4"
	if [ -n "$ipv6_enabled" ]; then
		network_get_device dev6 "$uplink_interface6"
		[ -z "$dev6" ] && network_get_physdev dev6 "$uplink_interface6"
		[ -z "$uplinkGW6" ] && pbr_get_gateway6 uplinkGW6 "$uplink_interface6" "$dev6"
	fi

	case "$param" in
		on_boot|on_start)
			[ -n "$uplink_interface4" ] && output 2 "Using uplink${uplink_interface6:+ IPv4} interface (${param}): $uplink_interface4 $__OK__\n"
			[ -n "$uplinkGW4" ] && output 2 "Found uplink${uplink_interface6:+ IPv4} gateway (${param}): $uplinkGW4 $__OK__\n"
			[ -n "$uplink_interface6" ] && output 2 "Using uplink IPv6 interface (${param}): $uplink_interface6 $__OK__\n"
			[ -n "$uplinkGW6" ] && output 2 "Found uplink IPv6 gateway (${param}): $uplinkGW6 $__OK__\n"
		;;
	esac
	uplinkGW="${uplinkGW4:-${uplinkGW6}}"
}

is_wan_up() {
	local param="$1"
	if [ -z "$(uci_get network "$uplink_interface4")" ]; then
		json add error 'errorNoUplinkInterface' "$uplink_interface4"
		json add error 'errorNoUplinkInterfaceHint' "$(get_url '#uplink_interface')"
		return 1
	fi
	network_flush_cache
	load_network "$param"
	if [ -n "$uplinkGW" ]; then
		return 0
	else
		json add error 'errorNoUplinkGateway'
		return 1
	fi
}

# nft() overrides the nft binary: all calls append to the atomic nft file.
# This captures both internal rules and user include script output.
# Use nft_call() for direct nft binary access.
nft() { [ -n "$*" ] && nft_file 'add' 'main' "$@"; }
nft4() { nft "$@"; }
nft6() { [ -n "$ipv6_enabled" ] || return 0; nft "$@"; }
nft_call() { "$nft" "$@" >/dev/null 2>&1; }
nft_check_element() {
	[ -n "$nft_fw4_dump" ] || nft_fw4_dump="$("$nft" list table inet fw4 2>&1)"
	case "${1}:${2}" in
		table:fw4)
			[ -n "$nft_fw4_dump" ]
		;;
		chain:*|*)
			echo "$nft_fw4_dump" | grep "$1" | grep -q "$2"
		;;
	esac
}
nft_file() {
	local i chain command="$1" target="$2"
	case "$command:$target" in
		add:*)
			shift 2
			echo "$*" >> "$nftTempFile"
		;;
		create:main)
			rm -f "$nftTempFile" "$nftMainFile"
			for i in "$nftTempFile" "$nftMainFile"; do
				mkdir -p "${i%/*}"
			done
			{ echo '#!/usr/sbin/nft -f'; echo ''; } > "$nftTempFile"
			# Create pbr chains in fw4 table
			for chain in dstnat $chainsList; do
				echo "add chain inet $nftTable ${nftPrefix}_${chain} {}" >> "$nftTempFile"
			done
			echo "" >> "$nftTempFile"
			# Add jump rules from fw4 chains to pbr chains
			echo "add rule inet $nftTable dstnat jump ${nftPrefix}_dstnat" >> "$nftTempFile"
			echo "add rule inet $nftTable mangle_prerouting jump ${nftPrefix}_prerouting" >> "$nftTempFile"
			echo "add rule inet $nftTable mangle_output jump ${nftPrefix}_output" >> "$nftTempFile"
			echo "add rule inet $nftTable mangle_forward jump ${nftPrefix}_forward" >> "$nftTempFile"
			echo "" >> "$nftTempFile"
			# Insert PBR guards at the top of pbr chains so first PBR match wins, while preserving foreign marks.
			for chain in $chainsList; do
				echo "add rule inet $nftTable ${nftPrefix}_${chain} ${nftRuleParams:+${nftRuleParams} }meta mark & $fw_mask != 0 return" >> "$nftTempFile"
			done
		;;
		create:netifd)
			rm -f "$nftTempFile" "$nftNetifdFile"
			for i in "$nftTempFile" "$nftNetifdFile"; do
				mkdir -p "${i%/*}"
			done
			{ echo '#!/usr/sbin/nft -f'; echo ''; } > "$nftTempFile"
		;;
		delete:main)
			rm -f "$nftTempFile" "$nftMainFile"
		;;
		delete:netifd)
			output "Removing fw4 netifd nft file "
			if rm -f "$nftNetifdFile"; then
				output_okbn
			else
				json add error 'errorNftNetifdFileDelete' "$nftNetifdFile"
				output_failn
			fi
		;;
		exists:main)
			[ -s "$nftMainFile" ] && return 0 || return 1
		;;
		exists:netifd)
			[ -s "$nftNetifdFile" ] && return 0 || return 1
		;;
		install:main)
			[ -s "$nftTempFile" ] || return 1
			output "Installing fw4 nft file "
			if nft_call -c -f "$nftTempFile" && \
				cp -f "$nftTempFile" "$nftMainFile"; then
				output_okn
				fw4 -q reload >/dev/null 2>&1
			else
				json add error 'errorNftMainFileInstall' "$nftTempFile"
				output_failn
			fi
		;;
		install:netifd)
			[ -s "$nftTempFile" ] || return 1
			output "Installing fw4 netifd nft file "
			if nft_call -c -f "$nftTempFile" && \
				cp -f "$nftTempFile" "$nftNetifdFile"; then
				output_okbn
			else
				json add error 'errorNftNetifdFileInstall' "$nftTempFile"
				output_failn
			fi
		;;
		match:temp)
			grep -q "$3" "$nftTempFile"
		;;
		sed:temp)
			shift 2
			sed -i "$*" "$nftTempFile" >/dev/null 2>&1
		;;
		sed:netifd)
			shift 2
			sed -i "$*" "$nftNetifdFile" >/dev/null 2>&1
		;;
		show:main)
			echo "$packageName fw4 nft file: $nftMainFile"
			sed '1d;2d;' "$nftMainFile"
		;;
		show:netifd)
			echo "$packageName fw4 netifd nft file: $nftNetifdFile"
			sed '1d;2d;' "$nftNetifdFile"
		;;
	esac
}
nftset() {
	local command="$1" iface="$2" target="${3:-dst}" type="${4:-ip}" uid="$5" comment="$6" param="$7" mark="$7"
	local nftset4 nftset6 i param4 param6
	local ipv4_error=1 ipv6_error=1
	nftset4="${nftPrefix}${iface:+_${iface}}_4${target:+_${target}}${type:+_${type}}${uid:+_${uid}}"
	nftset6="${nftPrefix}${iface:+_${iface}}_6${target:+_${target}}${type:+_${type}}${uid:+_${uid}}"

	if [ "${#nftset4}" -gt '255' ]; then
		json add error 'errorNftsetNameTooLong' "$nftset4"
		return 1
	fi

	case "$command" in
		add)
			if is_mac_address "$param" || is_list "$param"; then
				nft4 add element inet "$nftTable" "$nftset4" "{ $param }" && ipv4_error=0
				nft6 add element inet "$nftTable" "$nftset6" "{ $param }" && ipv6_error=0
			elif is_ipv4 "$param"; then
				nft4 add element inet "$nftTable" "$nftset4" "{ $param }" && ipv4_error=0
			elif is_ipv6 "$param"; then
				nft6 add element inet "$nftTable" "$nftset6" "{ $param }" && ipv6_error=0
			else
				if [ "$target" = 'src' ]; then
					param4="$(ipv4_leases_to_nftset "$param")"
					param6="$(ipv6_leases_to_nftset "$param")"
				fi
				[ -z "$param4" ] && param4="$(resolveip_to_nftset4 "$param")"
				[ -z "$param6" ] && param6="$(resolveip_to_nftset6 "$param")"
				if [ -z "$param4" ] && [ -z "$param6" ]; then
					json add error 'errorFailedToResolve' "$param"
				else
					[ -n "$param4" ] && nft4 add element inet "$nftTable" "$nftset4" "{ $param4 }" && ipv4_error=0
					[ -n "$param6" ] && nft6 add element inet "$nftTable" "$nftset6" "{ $param6 }" && ipv6_error=0
				fi
			fi
		;;
		add_dnsmasq_element)
			[ -n "$ipv6_enabled" ] || unset nftset6
			grep -qxF "nftset=/${param}/4#inet#${nftTable}#${nftset4}${nftset6:+,6#inet#${nftTable}#${nftset6}} # $comment" "$packageDnsmasqFile" && return 0
			echo "nftset=/${param}/4#inet#${nftTable}#${nftset4}${nftset6:+,6#inet#${nftTable}#${nftset6}} # $comment" >> "$packageDnsmasqFile" && ipv4_error=0
		;;
		create)
			case "$type" in
				ip|net)
					nft4 add set inet "$nftTable" "$nftset4" "{ type ipv4_addr; $nftSetParams comment \"$comment\";}" && ipv4_error=0
					nft6 add set inet "$nftTable" "$nftset6" "{ type ipv6_addr; $nftSetParams comment \"$comment\";}" && ipv6_error=0
				;;
				mac)
					nft4 add set inet "$nftTable" "$nftset4" "{ type ether_addr; $nftSetParams comment \"$comment\";}" && ipv4_error=0
					nft6 add set inet "$nftTable" "$nftset6" "{ type ether_addr; $nftSetParams comment \"$comment\";}" && ipv6_error=0
				;;
				esac
		;;
		create_dnsmasq_set)
			nft4 add set inet "$nftTable" "$nftset4" "{ type ipv4_addr; $nftSetParams comment \"$comment\";}" && ipv4_error=0
			nft6 add set inet "$nftTable" "$nftset6" "{ type ipv6_addr; $nftSetParams comment \"$comment\";}" && ipv6_error=0
		;;
		create_user_set)
			case "$type" in
				ip|net)
					nft4 add set inet "$nftTable" "$nftset4" "{ type ipv4_addr; $nftSetParams comment \"$comment\";}" && ipv4_error=0
					nft6 add set inet "$nftTable" "$nftset6" "{ type ipv6_addr; $nftSetParams comment \"$comment\";}" && ipv6_error=0
					case "$target" in
						dst)
							nft4 add rule inet "$nftTable" "${nftPrefix}_prerouting" "${nftIPv4Flag}" daddr "@${nftset4}" "${nftRuleParams}" goto "${nftPrefix}_mark_${mark}" && ipv4_error=0
							nft6 add rule inet "$nftTable" "${nftPrefix}_prerouting" "${nftIPv6Flag}" daddr "@${nftset6}" "${nftRuleParams}" goto "${nftPrefix}_mark_${mark}" && ipv6_error=0
						;;
						src)
							nft4 add rule inet "$nftTable" "${nftPrefix}_prerouting" "${nftIPv4Flag}" saddr "@${nftset4}" "${nftRuleParams}" goto "${nftPrefix}_mark_${mark}" && ipv4_error=0
							nft6 add rule inet "$nftTable" "${nftPrefix}_prerouting" "${nftIPv6Flag}" saddr "@${nftset6}" "${nftRuleParams}" goto "${nftPrefix}_mark_${mark}" && ipv6_error=0
						;;
					esac
					;;
				mac)
					nft4 add set inet "$nftTable" "$nftset4" "{ type ether_addr; $nftSetParams comment \"$comment\"; }" && ipv4_error=0
					nft6 add set inet "$nftTable" "$nftset6" "{ type ether_addr; $nftSetParams comment \"$comment\"; }" && ipv6_error=0
					nft4 add rule inet "$nftTable" "${nftPrefix}_prerouting" ether saddr "@${nftset4}" "${nftRuleParams}" goto "${nftPrefix}_mark_${mark}" && ipv4_error=0
					nft6 add rule inet "$nftTable" "${nftPrefix}_prerouting" ether saddr "@${nftset6}" "${nftRuleParams}" goto "${nftPrefix}_mark_${mark}" && ipv6_error=0
					;;
				esac
		;;
		delete|destroy)
			nft_call delete set inet "$nftTable" "$nftset4" && ipv4_error=0
			nft_call delete set inet "$nftTable" "$nftset6" && ipv6_error=0
		;;
		delete_user_set)
			nft_call delete set inet "$nftTable" "$nftset4" && ipv4_error=0
			nft_call delete set inet "$nftTable" "$nftset6" && ipv6_error=0
			case "$type" in
				ip|net)
					case "$target" in
						dst)
							nft_call delete rule inet "$nftTable" "${nftPrefix}_prerouting" "$nftIPv4Flag" daddr "@${nftset4}" goto "${nftPrefix}_mark_${mark}" && ipv4_error=0
							nft_call delete rule inet "$nftTable" "${nftPrefix}_prerouting" "$nftIPv6Flag" daddr "@${nftset6}" goto "${nftPrefix}_mark_${mark}" && ipv6_error=0
						;;
						src)
							nft_call delete rule inet "$nftTable" "${nftPrefix}_prerouting" "$nftIPv4Flag" saddr "@${nftset4}" goto "${nftPrefix}_mark_${mark}" && ipv4_error=0
							nft_call delete rule inet "$nftTable" "${nftPrefix}_prerouting" "$nftIPv6Flag" saddr "@${nftset6}" goto "${nftPrefix}_mark_${mark}" && ipv6_error=0
						;;
					esac
					;;
				mac)
					nft_call delete rule inet "$nftTable" "${nftPrefix}_prerouting" "ether" saddr "@${nftset4}" goto "${nftPrefix}_mark_${mark}" && ipv4_error=0
					nft_call delete rule inet "$nftTable" "${nftPrefix}_prerouting" "ether" saddr "@${nftset6}" goto "${nftPrefix}_mark_${mark}" && ipv6_error=0
					;;
				esac
		;;
		flush|flush_user_set)
			nft_call flush set inet "$nftTable" "$nftset4" && ipv4_error=0
			nft_call flush set inet "$nftTable" "$nftset6" && ipv6_error=0
		;;
	esac
# nft6 returns true if IPv6 support is not enabled
	[ -z "$ipv6_enabled" ] && ipv6_error='1'
	if [ "$ipv4_error" -eq '0' ] || [ "$ipv6_error" -eq '0' ]; then
		return 0
	else
		return 1
	fi
}

cleanup() {
	local action i prio
	for action in "$@"; do
		case "$action" in
			rt_tables)
				# shellcheck disable=SC2013
				for i in $(grep -oh "${ipTablePrefix}_.*" "$rtTablesFile"); do
					! is_netifd_table "$i" && sed -i "/${i}/d" "$rtTablesFile"
				done
				sync
			;;
			main_table)
				# Delete rules by priority range instead of parsing table names.
				# pbr rules occupy: uplink_ip_rules_priority (down) for interfaces,
				# uplink_ip_rules_priority+1 for suppress_prefixlength.
				local prio_min prio_max prio table line
				# max interfaces = fw_mask / uplink_mark (e.g. 0x00ff0000/0x00010000 = 255)
				local max_ifaces="$((fw_mask / uplink_mark))"
				prio_max="$((uplink_ip_rules_priority))"
				prio_min="$((uplink_ip_rules_priority - max_ifaces))"
				[ "$prio_min" -lt 1 ] && prio_min="1"
				ip -4 rule show | while IFS= read -r line; do
					prio="${line%%:*}"
					[ "$prio" -ge "$prio_min" ] 2>/dev/null && [ "$prio" -le "$prio_max" ] 2>/dev/null || continue
					# Skip netifd-managed fwmark rules (but not WG sport rules)
					case "$line" in
						*fwmark*"lookup ${ipTablePrefix}_"*)
							table="${line##*lookup }"
							table="${table%% *}"
							is_netifd_table "$table" && continue
							;;
					esac
					ip -4 rule del priority "$prio" 2>/dev/null
				done
				# Legacy: remove suppress_prefixlength rules created without explicit priority
				ip -4 rule del lookup 'main' suppress_prefixlength "$prefixlength" 2>/dev/null
				# Always attempt IPv6 cleanup regardless of current ipv6_enabled setting
				# since rules might exist from when IPv6 was previously enabled
				ip -6 rule show 2>/dev/null | while IFS= read -r line; do
					prio="${line%%:*}"
					[ "$prio" -ge "$prio_min" ] 2>/dev/null && [ "$prio" -le "$prio_max" ] 2>/dev/null || continue
					case "$line" in
						*fwmark*"lookup ${ipTablePrefix}_"*)
							table="${line##*lookup }"
							table="${table%% *}"
							is_netifd_table "$table" && continue
							;;
					esac
					ip -6 rule del priority "$prio" 2>/dev/null
				done
				ip -6 rule del lookup 'main' suppress_prefixlength "$prefixlength" 2>/dev/null
			;;
			main_chains)
				for i in $chainsList dstnat; do
					i="$(str_to_lower "$i")"
					nft_call flush chain inet "$nftTable" "${i}"
				done
			;;
			marking_chains)
				for i in $(get_mark_nft_chains); do
				#	nft_call flush chain inet "$nftTable" "$i"
					nft_call delete chain inet "$nftTable" "$i"
				done
			;;
			sets)
				for i in $(get_nft_sets); do
				#	nft_call flush set inet "$nftTable" "$i"
					nft_call delete set inet "$nftTable" "$i"
				done
			;;
		esac
	done
}

json() {
	local status message stats i
	local action="$1" param="$2" value="$3"; shift 3; local info="$*";
	local _current_namespace="$_JSON_PREFIX"
	json_set_namespace "${packageName//-/_}_"
	[ "$param" = 'error' ] && param='errors'
	[ "$param" = 'warning' ] && param='warnings'
	{ json_load_file "$runningStatusFile" || json_init; } >/dev/null 2>&1
	case "$action" in
		'get')
			json_select "$param" >/dev/null 2>&1 || return
			if [ -n "$value" ]; then
				{
				if json_select "$value"; then
					json_get_var 'i' "${info:-code}"
					json_select ..
				fi
				} >/dev/null 2>&1
			else
				json_get_keys i
			fi
			printf "%b" "$i"
			json_select ..
			json_set_namespace "$_current_namespace"
			return
		;;
		'add')
			{ json_select "$param" || json_add_array "$param"; } >/dev/null 2>&1
			json_add_object ""
			json_add_string 'code' "$value"
			json_add_string 'info' "$info"
			json_close_object
			json_select ..
		;;
		'init')
			mkdir -p "${runningStatusFile%/*}"
			json_init
			json_add_array 'errors'
			json_close_array
			json_add_array 'warnings'
			json_close_array
		;;
	esac
	json_dump > "$runningStatusFile"
	sync
	json_set_namespace "$_current_namespace"
}

resolver() {
	_dnsmasq_instance_get_confdir() {
		local cfg cfg_file
		cfg="$(uci -q show "dhcp.${1}" | awk -F'[.=]' 'NR==1{print $2}')"
		[ -z "$dnsmasq_ubus" ] && dnsmasq_ubus="$(ubus call service list '{"name":"dnsmasq"}')"
		cfg_file="$(echo "$dnsmasq_ubus" | jsonfilter -e "@.dnsmasq.instances.${cfg}.command" \
			| awk '{gsub(/\\\//,"/");gsub(/[][",]/,"");for(i=1;i<=NF;i++)if($i=="-C"){print $(i+1);exit}}')"
		awk -F= '/^conf-dir=/{print $2; exit}' "$cfg_file"
	}
	_dnsmasq_instance_config() {
		local cfg="$1" param="$2" confdir
		[ -s "/etc/config/dhcp" ] || return 0
		[ -n "$(uci_get dhcp "$cfg")" ] || return 1
		case "$param" in
			cleanup)
				# clean up all dnsmasq configs
				confdir="$(_dnsmasq_instance_get_confdir "$cfg")"
				[ -n "$confdir" ] && rm -f "${confdir}/${packageName}"
				uci_remove_list 'dhcp' "$cfg" 'addnmount' "$packageDnsmasqFile"
			;;
			setup)
				# add dnsmasq conf addnmounts to point to pbr file
				uci_add_list_if_new 'dhcp' "$cfg" 'addnmount' "$packageDnsmasqFile"
				# add softlink to pbr file
				confdir="$(_dnsmasq_instance_get_confdir "$cfg")"
				[ -n "$confdir" ] || return 1
				ln -sf "$packageDnsmasqFile" "${confdir}/${packageName}"
				chmod 660 "${confdir}/${packageName}"
				chown -h root:dnsmasq "${confdir}/${packageName}" >/dev/null 2>/dev/null
			;;
		esac
	}
	local agh_version
	local param="$1" iface="$2" target="$3" type="$4" uid="$5" name="$6" value="$7"
	shift

	case "$resolver_set" in
		''|none)
			case "$param" in
				add_resolver_element) return 1;;
				create_resolver_set) return 1;;
				check_support) return 0;;
				cleanup) return 0;;
				configure) return 0;;
				kill) return 0;;
				reload) return 0;;
				restart) return 0;;
				compare_hash) return 0;;
				store_hash) return 0;;
				wait)
					[ -n "$resolverWorkingFlag" ] && return 0
					local timeout="${iface:-30}" count=0
					local hostname="$(uci_get 'system' '@system[0]' 'hostname' 'OpenWrt')"
					while [ "$count" -lt "$timeout" ]; do
						if resolveip "$hostname" >/dev/null 2>&1; then
							resolverWorkingFlag='true'
							return 0
						fi
						sleep 1
						count=$((count + 1))
					done
					return 1
				;;
			esac
		;;
		dnsmasq.nftset)
			case "$param" in
				add_resolver_element)
					[ -n "$resolverSetSupported" ] || return 1
					[ "$target" = 'src' ] && return 1 # dnsmasq doesn't populate nft sets for local addresses
					local d
					for d in $value; do
						nftset 'add_dnsmasq_element' "$iface" "$target" "$type" "$uid" "$name" "$d"
					done
				;;
				create_resolver_set)
					[ -n "$resolverSetSupported" ] || return 1
					[ "$target" = 'src' ] && return 1 # dnsmasq doesn't populate nft sets for local addresses
					nftset 'create_dnsmasq_set' "$iface" "$target" "$type" "$uid" "$name" "$value"
				;;
				check_support)
					if check_dnsmasq_nftset; then
						resolverSetSupported='true'
						return 0
					else
						json add warning 'warningResolverNotSupported'
						return 1
					fi
				;;
				cleanup)
					[ -n "$resolverSetSupported" ] || return 1
					rm -f "$packageDnsmasqFile"
					config_load 'dhcp'
					config_foreach _dnsmasq_instance_config 'dnsmasq' 'cleanup'
					return 0
				;;
				configure)
					[ -n "$resolverSetSupported" ] || return 1
					rm -f "$packageDnsmasqFile"
					touch "$packageDnsmasqFile"
					config_load 'dhcp'
					if [ "$resolver_instance" = "*" ]; then
						config_foreach _dnsmasq_instance_config 'dnsmasq' 'setup'
					else
						config_foreach _dnsmasq_instance_config 'dnsmasq' 'cleanup'
						for i in $resolver_instance; do
							_dnsmasq_instance_config "@dnsmasq[$i]" 'setup' \
							|| _dnsmasq_instance_config "$i" 'setup'
						done
					fi
				;;
				kill)
					[ -n "$resolverSetSupported" ] && killall -q -s HUP dnsmasq;;
				reload)
					[ -z "$resolverSetSupported" ] && return 1
					output 3 'Reloading dnsmasq '
					if /etc/init.d/dnsmasq reload >/dev/null 2>&1; then
						output_okn
						return 0
					else
						output_failn
						return 1
					fi
				;;
				restart)
					[ -z "$resolverSetSupported" ] && return 1
					output 3 'Restarting dnsmasq '
					if /etc/init.d/dnsmasq restart >/dev/null 2>&1; then
						output_okn
						return 0
					else
						output_failn
						return 1
					fi
				;;
				compare_hash)
					[ -z "$resolverSetSupported" ] && return 1
					uci_changes 'dhcp' && uci_commit 'dhcp'
					local resolverNewHash
					if [ -s "$packageDnsmasqFile" ]; then
						resolverNewHash="$(md5sum "$packageDnsmasqFile" | awk '{ print $1; }')"
					fi
					[ "$resolverNewHash" != "$resolverStoredHash" ]
				;;
				store_hash)
					[ -s "$packageDnsmasqFile" ] && resolverStoredHash="$(md5sum "$packageDnsmasqFile" | awk '{ print $1; }')"
					return 0
				;;
				wait)
					[ -n "$resolverWorkingFlag" ] && return 0
					local timeout="${iface:-30}" count=0
					local hostname="$(uci_get 'system' '@system[0]' 'hostname' 'OpenWrt')"
					while [ "$count" -lt "$timeout" ]; do
						if resolveip "$hostname" >/dev/null 2>&1; then
							resolverWorkingFlag='true'
							return 0
						fi
						sleep 1
						count=$((count + 1))
					done
					return 1
				;;
			esac
		;;
		unbound.nftset)
			case "$param" in
				add_resolver_element) :;;
				create_resolver_set) :;;
				check_support) :;;
				cleanup) :;;
				configure) :;;
				kill) :;;
				reload) :;;
				restart) :;;
				compare_hash) :;;
				store_hash) :;;
			esac
		;;
	esac
}

netifd() {
	# Usage: netifd install [iface] | netifd remove [iface] | netifd uninstall
	_netifd_process_interface() {
		local iface="$1" action="${2:-install}"
		# Normalize table name for split uplink scenarios
		local rt_name="${ipTablePrefix}_${iface}"
		if is_split_uplink && [ "$iface" = "$uplink_interface6" ]; then
			rt_name="${ipTablePrefix}_${uplink_interface4}"
		fi

		# clean-up first for repeated netifd install calls in different netifd_strict_enforcement modes
		uci_remove 'network' "${iface}" 'ip4table' 2>/dev/null
		uci_remove 'network' "${iface}" 'ip6table' 2>/dev/null
		uci_remove 'network' 'rule' "${rt_name}_ipv4"  2>/dev/null
		uci_remove 'network' 'rule6' "${rt_name}_ipv6" 2>/dev/null

		# process local interfaces and set up rules for LANs if netifd_strict_enforcement is enabled
		if [ -n "$netifd_strict_enforcement" ] && str_contains "$netifd_interface_local" "$iface"; then
			case "$action" in
				install)
					if [ -n "$netifd_interface_default" ]; then
						uci_add 'network' 'rule' "${rt_name}_ipv4"
						uci_set 'network' "${rt_name}_ipv4" 'in' "${iface}"
						uci_set 'network' "${rt_name}_ipv4" 'lookup' "${ipTablePrefix}_${netifd_interface_default}"
						uci_set 'network' "${rt_name}_ipv4" 'priority' "${lan_priority}"
					fi
					if [ -n "$ipv6_enabled" ] && [ -n "$netifd_interface_default6" ]; then
						local ipv6_default_lookup="${ipTablePrefix}_${netifd_interface_default6}"
						if is_split_uplink && [ "$netifd_interface_default6" = "$uplink_interface6" ]; then
							ipv6_default_lookup="${ipTablePrefix}_${uplink_interface4}"
						fi
						uci_add 'network' 'rule6' "${rt_name}_ipv6"
						uci_set 'network' "${rt_name}_ipv6" 'in' "${iface}"
						uci_set 'network' "${rt_name}_ipv6" 'lookup' "$ipv6_default_lookup"
						uci_set 'network' "${rt_name}_ipv6" 'priority' "${lan_priority}"
					fi
					lan_priority="$((lan_priority + 1))"
				;;
				remove|uninstall)
					: # rules already removed above
				;;
			esac
		fi

		# process only WAN and supported tunnels below
		is_supported_interface "$iface" || return 0

		local _mark="$mark" _priority="$priority" _tid="$tid"
		local splitUplinkSecondIface

		if is_split_uplink; then
			if is_uplink4 "$iface" || is_uplink6 "$iface"; then
				if [ -n "$_uplinkMark" ] && [ -n "$_uplinkPriority" ] && [ -n "$_uplinkTableID" ]; then
					_mark="$_uplinkMark"
					_priority="$_uplinkPriority"
					_tid="$_uplinkTableID"
					splitUplinkSecondIface='true'
				else
					_uplinkMark="$_mark"
					_uplinkPriority="$_priority"
					_uplinkTableID="$_tid"
				fi
			fi
		fi

		# use main table for default uplink if netifd_strict_enforcement is not enabled
		[ -z "$netifd_strict_enforcement" ] && [ "$netifd_interface_default" = "$iface" ] && \
			rt_name='main'

		if [ -z "$target_iface" ] || [ "$target_iface" = "$iface" ]; then
			case "$action" in
				install)
					output 2 "Setting up netifd extensions for $iface... "
					if ! is_split_uplink || ! is_uplink6 "$iface"; then
						uci_set 'network' "${iface}" 'ip4table' "${rt_name}"
						uci_add 'network' 'rule' "${rt_name}_ipv4"
						uci_set 'network' "${rt_name}_ipv4" 'priority' "${_priority}"
						uci_set 'network' "${rt_name}_ipv4" 'lookup' "${rt_name}"
						uci_set 'network' "${rt_name}_ipv4" 'mark' "${_mark}"
						uci_set 'network' "${rt_name}_ipv4" 'mask' "${fw_mask}"
					fi
					if [ -n "$ipv6_enabled" ] && { ! is_split_uplink || ! is_uplink4 "$iface"; }; then
						uci_set 'network' "${iface}" 'ip6table' "${rt_name}"
						uci_add 'network' 'rule6' "${rt_name}_ipv6"
						uci_set 'network' "${rt_name}_ipv6" 'priority' "${_priority}"
						uci_set 'network' "${rt_name}_ipv6" 'lookup' "${rt_name}"
						uci_set 'network' "${rt_name}_ipv6" 'mark' "${_mark}"
						uci_set 'network' "${rt_name}_ipv6" 'mask' "${fw_mask}"
					fi
					if ! is_split_uplink || ! is_uplink6 "$iface"; then
					[ "$rt_name" = 'main' ] || sed -i "\#${rt_name}\$#d" "$rtTablesFile" >/dev/null 2>&1
					[ "$rt_name" = 'main' ] || echo "${_tid} ${rt_name}" >> "$rtTablesFile"
						nft_file 'sed' 'temp' "\#${_mark}#d"
					fi
					if ! nft_file 'match' 'temp' "${nftPrefix}_mark_${_mark}"; then
						nft add chain inet "$nftTable" "${nftPrefix}_mark_${_mark}"
						nft add rule inet "$nftTable" "${nftPrefix}_mark_${_mark} ${nftRuleParams} meta mark set (meta mark & ${fw_maskXor}) | ${_mark}"
						nft add rule inet "$nftTable" "${nftPrefix}_mark_${_mark} return"
					fi
					local dscp="$(uci_get "$packageName" 'config' "${iface}_dscp")"
					if [ "${dscp:-0}" -ge '1' ] && [ "${dscp:-0}" -le '63' ]; then
						if ! is_split_uplink || ! is_uplink6 "$iface"; then
							nft add rule inet "$nftTable" "${nftPrefix}_prerouting ${nftIPv4Flag} dscp ${dscp} ${nftRuleParams} goto ${nftPrefix}_mark_${_mark}"
						fi
						if [ -n "$ipv6_enabled" ] && { ! is_split_uplink || ! is_uplink4 "$iface"; }; then
							nft add rule inet "$nftTable" "${nftPrefix}_prerouting ${nftIPv6Flag} dscp ${dscp} ${nftRuleParams} goto ${nftPrefix}_mark_${_mark}"
						fi
					fi
					if [ "$iface" = "$icmp_interface" ]; then
						if ! is_split_uplink || ! is_uplink6 "$iface"; then
							nft add rule inet "$nftTable" "${nftPrefix}_output ${nftIPv4Flag} protocol icmp ${nftRuleParams} goto ${nftPrefix}_mark_${_mark}"
						fi
						if [ -n "$ipv6_enabled" ] && { ! is_split_uplink || ! is_uplink4 "$iface"; }; then
							nft add rule inet "$nftTable" "${nftPrefix}_output ${nftIPv6Flag} protocol icmp ${nftRuleParams} goto ${nftPrefix}_mark_${_mark}"
						fi
					fi

					output_okb
				;;
				remove|uninstall)
					output 2 "Removing netifd extensions for $iface... "
					[ "$rt_name" = 'main' ] || sed -i "\#${rt_name}\$#d" "$rtTablesFile" >/dev/null 2>&1
					nft_file 'sed' 'netifd' "\#${_mark}#d"
					output_okb
				;;
			esac
		fi

		if [ -z "$splitUplinkSecondIface" ]; then
			mark="$(printf '0x%06x' $((_mark + uplink_mark)))"
			priority="$((_priority - 1))"
			tid="$((_tid + 1))"
		fi
	}

	load_package_config
	json 'init'

	local action="${1:-install}"
	local target_iface="$2"
	local lan_priority="$((uplink_ip_rules_priority + 1000))"
	local mark="$(printf '0x%06x' "$uplink_mark")"
	local priority="$uplink_ip_rules_priority"
	local tid="$(get_rt_tables_non_pbr_next_id)"
	local _uplinkMark _uplinkPriority _uplinkTableID

	case "$action" in
		check)
			[ "$netifd_enabled" = '1' ]
			return "$?"
		;;
		install)
			if [ -z "$netifd_strict_enforcement" ]; then
				json add error 'errorNetifdMissingOption' 'netifd_strict_enforcement'
				output_error 'errorNetifdMissingOption' 'netifd_strict_enforcement'
				return 1
			fi
			if [ -z "$netifd_interface_default" ]; then
				json add error 'errorNetifdMissingOption' 'netifd_interface_default'
				output_error 'errorNetifdMissingOption' 'netifd_interface_default'
				return 1
			fi
			if [ "$(uci_get 'network' "$netifd_interface_default")" != 'interface' ]; then
				json add error 'errorNetifdInvalidGateway4' "$netifd_interface_default"
				output_error 'errorNetifdInvalidGateway4' "$netifd_interface_default"
				return 1
			fi
			if [ -n "$netifd_interface_default6" ] && [ "$(uci_get 'network' "$netifd_interface_default6")" != 'interface' ]; then
				json add error 'errorNetifdInvalidGateway6' "$netifd_interface_default6"
				output_error 'errorNetifdInvalidGateway6' "$netifd_interface_default6"
				return 1
			fi
			if [ -z "$netifd_interface_local" ]; then
				json add warning 'warningNetifdMissingInterfaceLocal' 'lan'
				output_warning 'warningNetifdMissingInterfaceLocal' 'lan'
				netifd_interface_local='lan'
			fi
			[ "$netifd_strict_enforcement" = '1' ] || unset netifd_strict_enforcement
#			[ -n "$netifd_interface_default6" ] || unset ipv6_enabled
		;;
		uninstall)
			unset target_iface
		;;
	esac

	nft_file 'create' 'netifd'
	output 1 "Netifd extensions $action ${target_iface:+on ${target_iface} }"
	uci_remove 'network' 'rule' "main_ipv4"  2>/dev/null
	uci_remove 'network' 'rule6' "main_ipv6" 2>/dev/null
	config_load 'network'
	config_foreach _netifd_process_interface 'interface' "$action"
	output_1_newline

	case "$action" in
		install)
			nft_file 'install' 'netifd'
			if [ -z "$target_iface" ]; then
				uci_set "$packageName" 'config' 'netifd_enabled' '1'
			fi
		;;
		remove)
			if [ -z "$target_iface" ]; then
				nft_file 'delete' 'netifd'
				uci_remove "$packageName" 'config' 'netifd_enabled' 2>/dev/null
			fi
		;;
		uninstall)
			if [ -z "$target_iface" ]; then
				nft_file 'delete' 'netifd'
			fi
		;;
	esac
	uci_commit "$packageName"
	uci_commit 'network'
	sync
	output "Restarting network ${action:+(on_${action}) }"
	{ /etc/init.d/network 'reload'; /etc/init.d/firewall 'reload'; } >/dev/null 2>&1 && output_okbn || output_failn
}

# original idea by @egc112: https://github.com/egc112/OpenWRT-egc-add-on/tree/main/stop-dns-leak
dns_policy_routing() {
	local mark i nftInsertOption='add' proto='tcp udp' proto_i
	local param4 param6
	local negation value dest4 dest6 first_value
	local inline_set_ipv4_empty_flag inline_set_ipv6_empty_flag
	local name="$1" src_addr="$2" dest_dns="$3" uid="$4" dest_dns_port="$5"
	local dest_dns_ipv4="$6" dest_dns_ipv6="$7"
	local chain='dstnat' iface='dns'

	if [ -z "${dest_dns_ipv4}${dest_dns_ipv6}" ]; then
		processDnsPolicyError='true'
		json add error 'errorPolicyProcessNoInterfaceDns' "'$dest_dns'"
		return 1
	fi

	if [ -z "$ipv6_enabled" ] && is_ipv6 "$(str_first_word "$src_addr")"; then
		processDnsPolicyError='true'
		json add error 'errorPolicyProcessNoIpv6' "$name"
		return 1
	fi

	if { is_ipv4 "$(str_first_word "$src_addr")" && [ -z "$dest_dns_ipv4" ]; } || \
		{ is_ipv6 "$(str_first_word "$src_addr")" && [ -z "$dest_dns_ipv6" ]; }; then
		processDnsPolicyError='true'
		json add error 'errorPolicyProcessMismatchFamily' "${name}: '$src_addr' '$dest_dns':'$dest_dns_port'"
		return 1
	fi

	for proto_i in $proto; do
		unset param4
		unset param6

		dest4="dport 53 dnat ip to ${dest_dns_ipv4}:${dest_dns_port}"
		dest6="dport 53 dnat ip6 to ${dest_dns_ipv6}:${dest_dns_port}"

		if [ -n "$src_addr" ]; then
			if [ "${src_addr:0:1}" = "!" ]; then
				negation='!='; src_addr="${src_addr//\!}"; nftset_suffix='_neg';
			else
				unset negation; unset nftset_suffix;
			fi
			value="$src_addr"
			first_value="$(str_first_word "$value")"
			if is_phys_dev "$first_value"; then
				param4="${param4:+${param4} }iifname ${negation:+${negation} }{ $(inline_set "$value") }"
				param6="${param6:+${param6} }iifname ${negation:+${negation} }{ $(inline_set "$value") }"
			elif is_mac_address "$first_value"; then
				param4="${param4:+${param4} }ether saddr ${negation:+${negation} }{ $(inline_set "$value") }"
				param6="${param6:+${param6} }ether saddr ${negation:+${negation} }{ $(inline_set "$value") }"
			elif is_domain "$first_value"; then
				local inline_set_ipv4='' inline_set_ipv6='' d=''
				for d in $value; do
					local resolved_ipv4 resolved_ipv6
					resolved_ipv4="$(resolveip_to_nftset4 "$d")"
					resolved_ipv6="$(resolveip_to_nftset6 "$d")"
					if [ -z "${resolved_ipv4}${resolved_ipv6}" ]; then
						json add error 'errorFailedToResolve' "$d"
					else
					[ -n "$resolved_ipv4" ] && inline_set_ipv4="${inline_set_ipv4:+${inline_set_ipv4}, }$resolved_ipv4"
					[ -n "$resolved_ipv6" ] && inline_set_ipv6="${inline_set_ipv6:+${inline_set_ipv6}, }$resolved_ipv6"
					fi
				done
				[ -n "$inline_set_ipv4" ] || inline_set_ipv4_empty_flag='true'
				[ -n "$inline_set_ipv6" ] || inline_set_ipv6_empty_flag='true'
				param4="${param4:+${param4} }${nftIPv4Flag} saddr ${negation:+${negation} }{ $inline_set_ipv4 }"
				param6="${param6:+${param6} }${nftIPv6Flag} saddr ${negation:+${negation} }{ $inline_set_ipv6 }"
			else
				param4="${param4:+${param4} }${nftIPv4Flag} saddr ${negation:+${negation} }{ $(inline_set "$value") }"
				param6="${param6:+${param6} }${nftIPv6Flag} saddr ${negation:+${negation} }{ $(inline_set "$value") }"
			fi
		fi

		param4="$nftInsertOption rule inet ${nftTable} ${nftPrefix}_${chain} ${param4} ${nftRuleParams} meta nfproto ipv4 ${proto_i} ${dest4} comment \"$name\""
		param6="$nftInsertOption rule inet ${nftTable} ${nftPrefix}_${chain} ${param6} ${nftRuleParams} meta nfproto ipv6 ${proto_i} ${dest6} comment \"$name\""

		local ipv4_error='0' ipv6_error='0'
		if [ "$pbrNftPrevParam4" != "$param4" ] && \
			[ -n "$first_value" ] && ! is_ipv6 "$first_value" && \
			[ -z "$inline_set_ipv4_empty_flag" ] && [ -n "$dest_dns_ipv4" ]; then
				nft4 "$param4" || ipv4_error='1'
				pbrNftPrevParam4="$param4"
		fi
		if [ "$pbrNftPrevParam6" != "$param6" ] && [ "$param4" != "$param6" ] && \
			[ -n "$first_value" ] && ! is_ipv4 "$first_value" && \
			[ -z "$inline_set_ipv6_empty_flag" ] && [ -n "$dest_dns_ipv6" ]; then
				nft6 "$param6" || ipv6_error='1'
				pbrNftPrevParam6="$param6"
		fi

		if [ -n "$ipv6_enabled" ] && [ "$ipv4_error" -eq '1' ] && [ "$ipv6_error" -eq '1' ]; then
			processDnsPolicyError='true'
			json add error 'errorPolicyProcessInsertionFailed' "$name"
			json add error 'errorPolicyProcessCMD' "nft $param4"
			json add error 'errorPolicyProcessCMD' "nft $param6"
			logger -t "$packageName" "ERROR: nft $param4"
			logger -t "$packageName" "ERROR: nft $param6"
		elif [ -z "$ipv6_enabled" ] && [ "$ipv4_error" -eq '1' ]; then
			processDnsPolicyError='true'
			json add error 'errorPolicyProcessInsertionFailedIpv4' "$name"
			json add error 'errorPolicyProcessCMD' "nft $param4"
			logger -t "$packageName" "ERROR: nft $param4"
		fi
	done
}

policy_routing() {
	local mark i nftInsertOption='add'
	local param4 param6 proto_i negation value dest4 dest6
	local nftset_suffix first_value_src first_value_dest
	local src_inline_set_ipv4_empty_flag src_inline_set_ipv6_empty_flag
	local dest_inline_set_ipv4_empty_flag dest_inline_set_ipv6_empty_flag
	local name="$1" iface="$2" src_addr="$3" src_port="$4" dest_addr="$5" dest_port="$6" proto chain uid="$9"
	proto="$(str_to_lower "$7")"
	chain="$(str_to_lower "$8")"
	chain="${chain:-prerouting}"
	mark=$(eval echo "\$mark_${iface//-/_}")

	if [ -z "$ipv6_enabled" ] && \
		{ is_ipv6 "$(str_first_word "$src_addr")" || is_ipv6 "$(str_first_word "$dest_addr")"; }; then
		processPolicyError='true'
		json add error 'errorPolicyProcessNoIpv6' "$name"
		return 1
	fi

	if is_tor "$iface"; then
		unset dest_port
		unset proto
	elif is_xray "$iface"; then
		unset dest_port
		[ -z "$src_port" ] && src_port='0-65535'
		dest4="tproxy $nftIPv4Flag to: $(get_xray_traffic_port "$iface") accept"
		dest6="tproxy $nftIPv6Flag to: $(get_xray_traffic_port "$iface") accept"
	elif [ -n "$mark" ]; then
		dest4="goto ${nftPrefix}_mark_${mark}"
		dest6="goto ${nftPrefix}_mark_${mark}"
	elif [ "$iface" = "ignore" ]; then
		dest4="return"
		dest6="return"
	else
		processPolicyError='true'
		json add error 'errorPolicyProcessUnknownFwmark' "$iface"
		return 1
	fi

	# TODO: implement actual family mismatch check on lists
#	if is_family_mismatch "$src_addr" "$dest_addr"; then
#		processPolicyError='true'
#		json add error 'errorPolicyProcessMismatchFamily' "${name}: '$src_addr' '$dest_addr'"
#		return 1
#	fi

	if [ -z "$proto" ]; then
		if [ -n "${src_port}${dest_port}" ]; then
			proto='tcp udp'
		else
			proto='all'
		fi
	fi

	for proto_i in $proto; do
		unset param4
		unset param6
		if [ "$proto_i" = 'all' ]; then
			unset proto_i
		elif ! is_supported_protocol "$proto_i"; then
			processPolicyError='true'
			json add error 'errorPolicyProcessUnknownProtocol' "${name}: '$proto_i'"
			return 1
		fi

		if [ -n "$src_addr" ]; then
			if [ "${src_addr:0:1}" = "!" ]; then
				negation='!='; value="${src_addr//\!}"; nftset_suffix='_neg';
			else
				unset negation; value="$src_addr"; unset nftset_suffix;
			fi
			first_value_src="$(str_first_word "$value")"
			if is_phys_dev "$first_value_src"; then
				param4="${param4:+${param4} }iifname ${negation:+${negation} }{ $(inline_set "$value") }"
				param6="${param6:+${param6} }iifname ${negation:+${negation} }{ $(inline_set "$value") }"
			elif is_mac_address "$first_value_src"; then
				param4="${param4:+${param4} }ether saddr ${negation:+${negation} }{ $(inline_set "$value") }"
				param6="${param6:+${param6} }ether saddr ${negation:+${negation} }{ $(inline_set "$value") }"
			elif is_domain "$first_value_src"; then
				local target='src' type='ip'
				if resolver 'create_resolver_set' "$iface" "$target" "$type" "$uid" "$name" && \
					resolver 'add_resolver_element' "$iface" "$target" "$type" "$uid" "$name" "$value"; then
					param4="${param4:+${param4} }${nftIPv4Flag} saddr ${negation:+${negation} }@${nftPrefix}_${iface}_4_${target}_${type}_${uid}${nftset_suffix}"
					param6="${param6:+${param6} }${nftIPv6Flag} saddr ${negation:+${negation} }@${nftPrefix}_${iface}_6_${target}_${type}_${uid}${nftset_suffix}"
				else
					local inline_set_ipv4='' inline_set_ipv6='' d=''
					unset src_inline_set_ipv4_empty_flag
					unset src_inline_set_ipv6_empty_flag
					for d in $value; do
						local resolved_ipv4 resolved_ipv6
						resolved_ipv4="$(resolveip_to_nftset4 "$d")"
						resolved_ipv6="$(resolveip_to_nftset6 "$d")"
						if [ -z "${resolved_ipv4}${resolved_ipv6}" ]; then
							json add error 'errorFailedToResolve' "$d"
						else
						[ -n "$resolved_ipv4" ] && inline_set_ipv4="${inline_set_ipv4:+${inline_set_ipv4}, }$resolved_ipv4"
						[ -n "$resolved_ipv6" ] && inline_set_ipv6="${inline_set_ipv6:+${inline_set_ipv6}, }$resolved_ipv6"
						fi
					done
					[ -n "$inline_set_ipv4" ] || src_inline_set_ipv4_empty_flag='true'
					[ -n "$inline_set_ipv6" ] || src_inline_set_ipv6_empty_flag='true'
					param4="${param4:+${param4} }${nftIPv4Flag} saddr ${negation:+${negation} }{ $inline_set_ipv4 }"
					param6="${param6:+${param6} }${nftIPv6Flag} saddr ${negation:+${negation} }{ $inline_set_ipv6 }"
				fi
			else
				param4="${param4:+${param4} }${nftIPv4Flag} saddr ${negation:+${negation} }{ $(inline_set "$value") }"
				param6="${param6:+${param6} }${nftIPv6Flag} saddr ${negation:+${negation} }{ $(inline_set "$value") }"
			fi
		fi

		if [ -n "$dest_addr" ]; then
			if [ "${dest_addr:0:1}" = "!" ]; then
				negation='!='; value="${dest_addr//\!}"; nftset_suffix='_neg';
			else
				unset negation; value="$dest_addr"; unset nftset_suffix;
			fi
			first_value_dest="$(str_first_word "$value")"
			if is_phys_dev "$first_value_dest"; then
				param4="${param4:+${param4} }oifname ${negation:+${negation} }{ $(inline_set "$value") }"
				param6="${param6:+${param6} }oifname ${negation:+${negation} }{ $(inline_set "$value") }"
			elif is_domain "$first_value_dest"; then
				local target='dst' type='ip'
				if resolver 'create_resolver_set' "$iface" "$target" "$type" "$uid" "$name" && \
					resolver 'add_resolver_element' "$iface" "$target" "$type" "$uid" "$name" "$value"; then
					param4="${param4:+${param4} }${nftIPv4Flag} daddr ${negation:+${negation} }@${nftPrefix}_${iface}_4_${target}_${type}_${uid}${nftset_suffix}"
					param6="${param6:+${param6} }${nftIPv6Flag} daddr ${negation:+${negation} }@${nftPrefix}_${iface}_6_${target}_${type}_${uid}${nftset_suffix}"
				else
					local inline_set_ipv4='' inline_set_ipv6='' d=''
					unset dest_inline_set_ipv4_empty_flag
					unset dest_inline_set_ipv6_empty_flag
					for d in $value; do
						local resolved_ipv4 resolved_ipv6
						resolved_ipv4="$(resolveip_to_nftset4 "$d")"
						resolved_ipv6="$(resolveip_to_nftset6 "$d")"
						if [ -z "${resolved_ipv4}${resolved_ipv6}" ]; then
							json add error 'errorFailedToResolve' "$d"
						else
						[ -n "$resolved_ipv4" ] && inline_set_ipv4="${inline_set_ipv4:+${inline_set_ipv4}, }$resolved_ipv4"
						[ -n "$resolved_ipv6" ] && inline_set_ipv6="${inline_set_ipv6:+${inline_set_ipv6}, }$resolved_ipv6"
						fi
					done
					[ -n "$inline_set_ipv4" ] || dest_inline_set_ipv4_empty_flag='true'
					[ -n "$inline_set_ipv6" ] || dest_inline_set_ipv6_empty_flag='true'
					param4="${param4:+${param4} }${nftIPv4Flag} daddr ${negation:+${negation} }{ $inline_set_ipv4 }"
					param6="${param6:+${param6} }${nftIPv6Flag} daddr ${negation:+${negation} }{ $inline_set_ipv6 }"
				fi
			else
				param4="${param4:+${param4} }${nftIPv4Flag} daddr ${negation:+${negation} }{ $(inline_set "$value") }"
				param6="${param6:+${param6} }${nftIPv6Flag} daddr ${negation:+${negation} }{ $(inline_set "$value") }"
			fi
		fi

		if [ -n "$src_port" ]; then
			if [ "${src_port:0:1}" = "!" ]; then
				negation='!='; value="${src_port:1}"
			else
				unset negation; value="$src_port";
			fi
			param4="${param4:+${param4} }${proto_i:+${proto_i} }sport ${negation:+${negation} }{ $(inline_set "$value") }"
			param6="${param6:+${param6} }${proto_i:+${proto_i} }sport ${negation:+${negation} }{ $(inline_set "$value") }"
		fi

		if [ -n "$dest_port" ]; then
			if [ "${dest_port:0:1}" = "!" ]; then
				negation='!='; value="${dest_port:1}"
			else
				unset negation; value="$dest_port";
			fi
			param4="${param4:+${param4} }${proto_i:+${proto_i} }dport ${negation:+${negation} }{ $(inline_set "$value") }"
			param6="${param6:+${param6} }${proto_i:+${proto_i} }dport ${negation:+${negation} }{ $(inline_set "$value") }"
		fi

		if is_tor "$iface"; then
			local dest_udp_53 dest_tcp_80 dest_udp_80 dest_tcp_443 dest_udp_443
			local ipv4_error='0' ipv6_error='0'
			local dest_i dest4 dest6
			chain='dstnat'
			param4="$nftInsertOption rule inet $nftTable ${nftPrefix}_${chain} ${nftRuleParams} meta nfproto ipv4 $param4"
			param6="$nftInsertOption rule inet $nftTable ${nftPrefix}_${chain} ${nftRuleParams} meta nfproto ipv6 $param6"
			dest_udp_53="udp dport 53 redirect to :${torDnsPort} comment \"Tor-DNS-UDP\""
			dest_tcp_80="tcp dport 80 redirect to :${torTrafficPort} comment \"Tor-HTTP-TCP\""
			dest_udp_80="udp dport 80 redirect to :${torTrafficPort} comment \"Tor-HTTP-UDP\""
			dest_tcp_443="tcp dport 443 redirect to :${torTrafficPort} comment \"Tor-HTTPS-TCP\""
			dest_udp_443="udp dport 443 redirect to :${torTrafficPort} comment \"Tor-HTTPS-UDP\""
			for dest_i in dest_udp_53 dest_tcp_80 dest_udp_80 dest_tcp_443 dest_udp_443; do
				eval "dest4=\$$dest_i"
				eval "dest6=\$$dest_i"
				nft4 "$param4" "$dest4" || ipv4_error='1'
				nft6 "$param6" "$dest6" || ipv6_error='1'
				if [ -n "$ipv6_enabled" ] && [ "$ipv4_error" -eq '1' ] && [ "$ipv6_error" -eq '1' ]; then
					processPolicyError='true'
					json add error 'errorPolicyProcessInsertionFailed' "$name"
					json add error 'errorPolicyProcessCMD' "nft $param4 $dest4"
					json add error 'errorPolicyProcessCMD' "nft $param6 $dest6"
					logger -t "$packageName" "ERROR: nft $param4 $dest4"
					logger -t "$packageName" "ERROR: nft $param6 $dest6"
				elif [ -z "$ipv6_enabled" ] && [ "$ipv4_error" -eq '1' ]; then
					processPolicyError='true'
					json add error 'errorPolicyProcessInsertionFailedIpv4' "$name"
					json add error 'errorPolicyProcessCMD' "nft $param4 $dest4"
					logger -t "$packageName" "ERROR: nft $param4 $dest4"
				fi
			done
		else
			param4="$nftInsertOption rule inet $nftTable ${nftPrefix}_${chain} ${param4} ${nftRuleParams} ${dest4} comment \"$name\""
			param6="$nftInsertOption rule inet $nftTable ${nftPrefix}_${chain} ${param6} ${nftRuleParams} ${dest6} comment \"$name\""
			local ipv4_error='0' ipv6_error='0'
			if [ "$pbrNftPrevParam4" != "$param4" ] && \
				[ -z "$src_inline_set_ipv4_empty_flag" ] && [ -z "$dest_inline_set_ipv4_empty_flag" ] && \
				[ "$filter_group_src_addr" != 'ipv6' ] && [ "$filter_group_src_addr" != 'ipv6_negative' ] && \
				[ "$filter_group_dest_addr" != 'ipv6' ] && [ "$filter_group_dest_addr" != 'ipv6_negative' ]; then
					nft4 "$param4" || ipv4_error='1'
					pbrNftPrevParam4="$param4"
			fi
			if [ "$pbrNftPrevParam6" != "$param6" ] && [ "$param4" != "$param6" ] && \
				[ -z "$src_inline_set_ipv6_empty_flag" ] && [ -z "$dest_inline_set_ipv6_empty_flag" ] && \
				[ "$filter_group_src_addr" != 'ipv4' ] && [ "$filter_group_src_addr" != 'ipv4_negative' ] && \
				[ "$filter_group_dest_addr" != 'ipv4' ] && [ "$filter_group_dest_addr" != 'ipv4_negative' ]; then
					nft6 "$param6" || ipv6_error='1'
					pbrNftPrevParam6="$param6"
			fi
			if [ -n "$ipv6_enabled" ] && [ "$ipv4_error" -eq '1' ] && [ "$ipv6_error" -eq '1' ]; then
				processPolicyError='true'
				json add error 'errorPolicyProcessInsertionFailed' "$name"
				json add error 'errorPolicyProcessCMD' "nft $param4"
				json add error 'errorPolicyProcessCMD' "nft $param6"
				logger -t "$packageName" "ERROR: nft $param4"
				logger -t "$packageName" "ERROR: nft $param6"
			elif [ -z "$ipv6_enabled" ] && [ "$ipv4_error" -eq '1' ]; then
				processPolicyError='true'
				json add error 'errorPolicyProcessInsertionFailedIpv4' "$name"
				json add error 'errorPolicyProcessCMD' "nft $param4"
				logger -t "$packageName" "ERROR: nft $param4"
			fi
		fi
	done
}

dns_policy_process() {
	local i j uid="$1"

	[ "$enabled" = '1' ] || return 0

	src_addr="$(str_extras_to_space "$src_addr")"
	dest_dns="$(str_extras_to_space "$dest_dns")"

	unset j
	for i in $src_addr; do
		if is_url "$i"; then
			i="$(process_url "$i")"
		fi
		j="${j:+${j} }$i"
	done
	src_addr="$j"

	local dest_dns_interface dest_dns_ipv4 dest_dns_ipv6
	dest_dns_interface="$(str_first_value_interface "$dest_dns")"
	dest_dns_ipv4="$(str_first_value_ipv4 "$dest_dns")"
	dest_dns_ipv6="$(str_first_value_ipv6 "$dest_dns")"
	if is_supported_interface "$dest_dns_interface"; then
		local d
		for d in $(uci -q get network."$dest_dns_interface".dns); do
				if ! is_family_mismatch "$src_addr" "$d"; then
					if is_ipv4 "$d"; then
						dest_dns_ipv4="${dest_dns_ipv4:-${d}}"
					elif is_ipv6 "$d"; then
						dest_dns_ipv6="${dest_dns_ipv6:-${d}}"
					fi
				fi
			done
	fi

	unset processDnsPolicyError
	output 2 "Routing '$name' DNS to $dest_dns:$dest_dns_port "
	if [ -z "$src_addr" ]; then
		json add error 'errorPolicyNoSrcDest' "$name"
		output_fail; return 1;
	fi
	if [ -z "$dest_dns" ]; then
		json add error 'errorPolicyNoDns' "$name"
		output_fail; return 1;
	fi

	# group by type of src_addr values so that one nft set can be created per type within policy
	local filter_list_src_addr='phys_dev phys_dev_negative mac_address mac_address_negative domain domain_negative ipv4 ipv4_negative ipv6 ipv6_negative'
	local filter_group_src_addr filtered_value_src_addr
	for filter_group_src_addr in $filter_list_src_addr; do
		filtered_value_src_addr="$(filter_options "$filter_group_src_addr" "$src_addr")"
		if [ -n "$src_addr" ] && [ -n "$filtered_value_src_addr" ]; then
			if str_contains "$filter_group_src_addr" 'ipv4' && [ -z "$dest_dns_ipv4" ] ; then
					continue
			fi
			if str_contains "$filter_group_src_addr" 'ipv6' && [ -z "$dest_dns_ipv6" ] ; then
					continue
			fi
			dns_policy_routing "$name" "$filtered_value_src_addr" "$dest_dns" "$uid" "$dest_dns_port" "$dest_dns_ipv4" "$dest_dns_ipv6"
		fi
	done

	if [ -n "$processDnsPolicyError" ]; then
		output_fail
	else
		output_ok
	fi
}

policy_process() {
	local i j uid="$1"

	[ "$enabled" = '1' ] || return 0

	src_addr="$(str_extras_to_space "$src_addr")"
	src_port="$(str_extras_to_space "$src_port")"
	dest_addr="$(str_extras_to_space "$dest_addr")"
	dest_port="$(str_extras_to_space "$dest_port")"

	unset processPolicyError
	proto="$(str_to_lower "$proto")"
	[ "$proto" = 'auto' ] && unset proto
	[ "$proto" = 'all' ] && unset proto
	output 2 "Routing '$name' via $interface "
	if [ -z "${src_addr}${src_port}${dest_addr}${dest_port}" ]; then
		json add error 'errorPolicyNoSrcDest' "$name"
		output_fail; return 1;
	fi
	if [ -z "$interface" ]; then
		json add error 'errorPolicyNoInterface' "$name"
		output_fail; return 1;
	fi
	if ! is_supported_interface "$interface"; then
		json add error 'errorPolicyUnknownInterface' "$name"
		output_fail; return 1;
	fi

	unset j
	for i in $src_addr; do
		if is_url "$i"; then
			i="$(process_url "$i")"
		fi
		j="${j:+${j} }$i"
	done
	src_addr="$j"

	unset j
	for i in $dest_addr; do
		if is_url "$i"; then
			i="$(process_url "$i")"
		fi
		j="${j:+${j} }$i"
	done
	dest_addr="$j"

	# TODO: if only src_addr is set add option 121 to dhcp leases?

	local filter_list_src_addr='phys_dev phys_dev_negative mac_address mac_address_negative domain domain_negative ipv4 ipv4_negative ipv6 ipv6_negative'
	local filter_list_dest_addr='domain domain_negative ipv4 ipv4_negative ipv6 ipv6_negative'
	local filter_group_src_addr filtered_value_src_addr filter_group_dest_addr filtered_value_dest_addr
	local processed_value_src_addr processed_value_dest_addr
	[ -z "$src_addr" ] && filter_list_src_addr='none'
	for filter_group_src_addr in $filter_list_src_addr; do
		filtered_value_src_addr="$(filter_options "$filter_group_src_addr" "$src_addr")"
		if [ -z "$src_addr" ] || { [ -n "$src_addr" ] && [ -n "$filtered_value_src_addr" ]; }; then
			[ -z "$dest_addr" ] && filter_list_dest_addr='none'
			for filter_group_dest_addr in $filter_list_dest_addr; do
				filtered_value_dest_addr="$(filter_options "$filter_group_dest_addr" "$dest_addr")"
				if [ -z "$dest_addr" ] || { [ -n "$dest_addr" ] && [ -n "$filtered_value_dest_addr" ]; }; then
					if str_contains "$filter_group_src_addr" 'ipv4' && str_contains "$filter_group_dest_addr" 'ipv6'; then
							continue
					fi
					if str_contains "$filter_group_src_addr" 'ipv6' && str_contains "$filter_group_dest_addr" 'ipv4'; then
							continue
					fi
					policy_routing "$name" "$interface" "$filtered_value_src_addr" "$src_port" "$filtered_value_dest_addr" "$dest_port" "$proto" "$chain" "$uid"
					processed_value_src_addr="${processed_value_src_addr:+${processed_value_src_addr} }$filtered_value_src_addr"
					processed_value_dest_addr="${processed_value_dest_addr:+${processed_value_dest_addr} }$filtered_value_dest_addr"
				fi
			done
		fi
	done

	for i in $src_addr; do
		if ! str_contains "$processed_value_src_addr" "$i"; then
			processPolicyError='true'
			json add error 'errorPolicyProcessUnknownEntry' "$name: $i"
		fi
	done

	for i in $dest_addr; do
		if ! str_contains "$processed_value_dest_addr" "$i"; then
			processPolicyError='true'
			json add error 'errorPolicyProcessUnknownEntry' "$name: $i"
		fi
	done

	if [ -n "$processPolicyError" ]; then
		output_fail
	else
		output_ok
	fi
}

interface_routing() {
	local action="$1" tid="$2" mark="$3" iface="$4" gw4="$5" dev4="$6" gw6="$7" dev6="$8" priority="$9"
	local dscp s=0 i ipv4_error=1 ipv6_error=1
	if [ -z "$tid" ] || [ -z "$mark" ] || [ -z "$iface" ]; then
		json add error 'errorInterfaceRoutingEmptyValues'
		return 1
	fi
	case "$action" in
		create)
			is_netifd_interface "$iface" && return 0
			# Normalize table name for split uplink scenarios
			local table_iface="$iface"
			if is_split_uplink && [ "$iface" = "$uplink_interface6" ]; then
				table_iface="$uplink_interface4"
			fi
			if ! grep -q "$tid ${ipTablePrefix}_${table_iface}" "$rtTablesFile"; then
				sed -i "/${ipTablePrefix}_${table_iface}/d" "$rtTablesFile"
				echo "$tid ${ipTablePrefix}_${table_iface}" >> "$rtTablesFile"
				sync
			fi

			if [ -n "$dev4" ]; then
				ipv4_error=0
				ip -4 rule flush table "$tid" >/dev/null 2>&1
				ip -4 route flush table "$tid" >/dev/null 2>&1

				if [ -n "$gw4" ] || [ -n "$strict_enforcement" ]; then
					if [ -z "$gw4" ]; then
						try ip -4 route replace unreachable default table "$tid" || ipv4_error=1
					else
						try ip -4 route replace default via "$gw4" dev "$dev4" table "$tid" || ipv4_error=1
					fi
					try ip -4 rule replace fwmark "${mark}/${fw_mask}" table "$tid" priority "$priority" || ipv4_error=1
				fi

				dscp="$(uci_get "$packageName" 'config' "${iface}_dscp" '0')"
				if [ "$dscp" -ge '1' ] && [ "$dscp" -le '63' ]; then
					nft add rule inet "$nftTable" "${nftPrefix}_prerouting ${nftIPv4Flag} dscp ${dscp} ${nftRuleParams} goto ${nftPrefix}_mark_${mark}"
				fi
				if [ "$iface" = "$icmp_interface" ]; then
					nft add rule inet "$nftTable" "${nftPrefix}_output ${nftIPv4Flag} protocol icmp ${nftRuleParams} goto ${nftPrefix}_mark_${mark}"
				fi
			elif [ -n "$strict_enforcement" ] && ! { is_split_uplink && [ "$iface" = "$uplink_interface6" ]; }; then
				ipv4_error=0
				ip -4 rule flush table "$tid" >/dev/null 2>&1
				ip -4 route flush table "$tid" >/dev/null 2>&1
				try ip -4 route replace unreachable default table "$tid" || ipv4_error=1
				try ip -4 rule replace fwmark "${mark}/${fw_mask}" table "$tid" priority "$priority" || ipv4_error=1
			fi

			if [ -n "$ipv6_enabled" ] && [ -n "$dev6" ]; then
				ipv6_error=0
				ip -6 rule flush table "$tid" >/dev/null 2>&1
				ip -6 route flush table "$tid" >/dev/null 2>&1

				if { [ -n "$gw6" ] && [ "$gw6" != "::/0" ]; } || [ -n "$strict_enforcement" ]; then
					if [ -z "$gw6" ] || [ "$gw6" = "::/0" ]; then
						try ip -6 route replace unreachable default table "$tid" || ipv6_error=1
					elif ip -6 route list table main | grep -q " dev $dev6 "; then
						if ip -6 address show dev "$dev6" | grep -q "BROADCAST"; then
							try ip -6 route replace default via "$gw6" dev "$dev6" table "$tid" metric "$uplink_interface6_metric" || ipv6_error=1
						elif ip -6 address show dev "$dev6" | grep -q "POINTOPOINT"; then
							try ip -6 route replace default dev "$dev6" table "$tid" metric "$uplink_interface6_metric" || ipv6_error=1
						else
							json add error 'errorInterfaceRoutingUnknownDevType' "$dev6"
						fi
					else
						try ip -6 route replace "$(ip -6 -o a show "$dev6" | awk '{print $4}')" dev "$dev6" table "$tid" || ipv6_error=1
						try ip -6 route replace default dev "$dev6" table "$tid" || ipv6_error=1
					fi
					try ip -6 rule replace fwmark "${mark}/${fw_mask}" table "$tid" priority "$priority" || ipv6_error=1
				fi

				dscp="$(uci_get "$packageName" 'config' "${iface}_dscp" '0')"
				if [ "$dscp" -ge '1' ] && [ "$dscp" -le '63' ]; then
					nft add rule inet "$nftTable" "${nftPrefix}_prerouting ${nftIPv6Flag} dscp ${dscp} ${nftRuleParams} goto ${nftPrefix}_mark_${mark}"
				fi
				if [ "$iface" = "$icmp_interface" ]; then
					nft add rule inet "$nftTable" "${nftPrefix}_output ${nftIPv6Flag} protocol icmp ${nftRuleParams} goto ${nftPrefix}_mark_${mark}"
				fi
			elif [ -n "$ipv6_enabled" ] && [ -n "$strict_enforcement" ] && ! { is_split_uplink && [ "$iface" = "$uplink_interface4" ]; }; then
				ipv6_error=0
				ip -6 rule flush table "$tid" >/dev/null 2>&1
				ip -6 route flush table "$tid" >/dev/null 2>&1
				try ip -6 route replace unreachable default table "$tid" || ipv6_error=1
				try ip -6 rule replace fwmark "${mark}/${fw_mask}" table "$tid" priority "$priority" || ipv6_error=1
			fi

			# Always create the nft mark chain so policies can reference it
			if ! nft_file 'match' 'temp' "${nftPrefix}_mark_${mark}"; then
				nft add chain inet "$nftTable" "${nftPrefix}_mark_${mark}"
				nft add rule inet "$nftTable" "${nftPrefix}_mark_${mark} ${nftRuleParams} meta mark set (meta mark & ${fw_maskXor}) | ${mark}"
				nft add rule inet "$nftTable" "${nftPrefix}_mark_${mark} return"
			fi

			if [ "$ipv4_error" -eq '0' ] || [ "$ipv6_error" -eq '0' ]; then
				s=0
			else
				s=1
			fi
			return "$s"
		;;
		create_user_set)
			nftset 'create_user_set' "$iface" 'dst' 'ip' 'user' '' "$mark" || s=1
			nftset 'create_user_set' "$iface" 'src' 'ip' 'user' '' "$mark" || s=1
			nftset 'create_user_set' "$iface" 'src' 'mac' 'user' '' "$mark" || s=1
			return "$s"
		;;
		delete|destroy)
			is_netifd_interface "$iface" && return 0
			ip -4 rule del table 'main' prio "$((priority - 1000))" >/dev/null 2>&1
			ip -4 rule del table "$tid" prio "$priority" >/dev/null 2>&1
			ip -6 rule del table 'main' prio "$((priority - 1000))" >/dev/null 2>&1
			ip -6 rule del table "$tid" prio "$priority" >/dev/null 2>&1
			ip -4 rule flush table "$tid" >/dev/null 2>&1
			ip -4 route flush table "$tid" >/dev/null 2>&1
			ip -6 rule flush table "$tid" >/dev/null 2>&1
			ip -6 route flush table "$tid" >/dev/null 2>&1
			# Normalize table name for split uplink scenarios
			local table_iface="$iface"
			if is_split_uplink && [ "$iface" = "$uplink_interface6" ]; then
				table_iface="$uplink_interface4"
			fi
			sed -i "/${ipTablePrefix}_${table_iface}\$/d" "$rtTablesFile"
			sync
			return "$s"
		;;
		reload_interface)
			is_netifd_interface "$iface" && return 0

			if [ -n "$dev4" ]; then
				ipv4_error=0
				ip -4 rule flush fwmark "${mark}/${fw_mask}" table "$tid" >/dev/null 2>&1
				ip -4 route flush table "$tid" >/dev/null 2>&1
				if [ -n "$gw4" ] || [ -n "$strict_enforcement" ]; then
					if [ -z "$gw4" ]; then
						try ip -4 route replace unreachable default table "$tid" || ipv4_error=1
					else
						try ip -4 route replace default via "$gw4" dev "$dev4" table "$tid" || ipv4_error=1
					fi
					try ip -4 rule replace fwmark "${mark}/${fw_mask}" table "$tid" priority "$priority" || ipv4_error=1
				fi
			elif [ -n "$strict_enforcement" ] && ! { is_split_uplink && [ "$iface" = "$uplink_interface6" ]; }; then
				ipv4_error=0
				ip -4 rule flush table "$tid" >/dev/null 2>&1
				ip -4 route flush table "$tid" >/dev/null 2>&1
				try ip -4 route replace unreachable default table "$tid" || ipv4_error=1
				try ip -4 rule replace fwmark "${mark}/${fw_mask}" table "$tid" priority "$priority" || ipv4_error=1
			fi

			if [ -n "$ipv6_enabled" ] && [ -n "$dev6" ]; then
				ipv6_error=0
				ip -6 rule flush fwmark "${mark}/${fw_mask}" table "$tid" >/dev/null 2>&1
				ip -6 route flush table "$tid" >/dev/null 2>&1
				if { [ -n "$gw6" ] && [ "$gw6" != "::/0" ]; } || [ -n "$strict_enforcement" ]; then
					if [ -z "$gw6" ] || [ "$gw6" = "::/0" ]; then
						try ip -6 route replace unreachable default table "$tid" || ipv6_error=1
					elif ip -6 route list table main | grep -q " dev $dev6 "; then
						if ip -6 address show dev "$dev6" | grep -q "BROADCAST"; then
							try ip -6 route replace default via "$gw6" dev "$dev6" table "$tid" metric "$uplink_interface6_metric" || ipv6_error=1
						elif ip -6 address show dev "$dev6" | grep -q "POINTOPOINT"; then
							try ip -6 route replace default dev "$dev6" table "$tid" metric "$uplink_interface6_metric" || ipv6_error=1
						else
							json add error 'errorInterfaceRoutingUnknownDevType' "$dev6"
						fi
					else
						try ip -6 route replace "$(ip -6 -o a show "$dev6" | awk '{print $4}')" dev "$dev6" table "$tid" || ipv6_error=1
						try ip -6 route replace default dev "$dev6" table "$tid" || ipv6_error=1
					fi
					try ip -6 rule replace fwmark "${mark}/${fw_mask}" table "$tid" priority "$priority" || ipv6_error=1
				fi
			elif [ -n "$ipv6_enabled" ] && [ -n "$strict_enforcement" ] && ! { is_split_uplink && [ "$iface" = "$uplink_interface4" ]; }; then
				ipv6_error=0
				ip -6 rule flush table "$tid" >/dev/null 2>&1
				ip -6 route flush table "$tid" >/dev/null 2>&1
				try ip -6 route replace unreachable default table "$tid" || ipv6_error=1
				try ip -6 rule replace fwmark "${mark}/${fw_mask}" table "$tid" priority "$priority" || ipv6_error=1
			fi

			if [ "$ipv4_error" -eq '0' ] || [ "$ipv6_error" -eq '0' ]; then
				s=0
			else
				s=1
			fi
			return "$s"
		;;
	esac
}

json_add_gateway() {
	local action="$1" tid="$2" mark="$3" iface="$4" gw4="$5" dev4="$6" gw6="$7" dev6="$8" priority="$9" default="${10}"
	json_add_object 'gateways'
	json_add_string 'name' "$iface"
	json_add_string 'device_ipv4' "$dev4"
	json_add_string 'gateway_ipv4' "$gw4"
	json_add_string 'device_ipv6' "$dev6"
	json_add_string 'gateway_ipv6' "$gw6"
	if [ -n "$default" ]; then
		json_add_boolean 'default' '1'
	else
		json_add_boolean 'default' '0'
	fi
	json_add_string 'action' "$action"
	json_add_string 'table_id' "$tid"
	json_add_string 'mark' "$mark"
	json_add_string 'priority' "$priority"
	json_close_object
}

process_interface() {
	local gw4 gw6 dev4 dev6 s=0 dscp iface="$1" action="$2" reloadedIface="$3"
	local displayText dispDev dispGw4 dispGw6 dispStatus

	if [ "$iface" = 'all' ]; then
		case "$action" in
			reset_globals)
				config_load 'network'
				ifaceMark="$(printf '0x%06x' "$uplink_mark")"
				ifacePriority="$uplink_ip_rules_priority"
				unset ifaceTableID
				unset _uplinkMark _uplinkPriority _uplinkTableID
				return 0
			;;
			create_global_rules)
				_wg_server() {
					local iface="$1"
					if is_wg_server "$iface" && ! is_ignored_interface "$iface"; then
						local disabled listen_port
						config_get disabled "$iface" 'disabled'
						config_get listen_port "$iface" 'listen_port'
						if [ "$disabled" != '1' ] && [ -n "$listen_port" ]; then
							if [ -n "$uplink_interface4" ]; then
								#ip rule del sport "$listen_port" table "${ipTablePrefix}_${uplink_interface4}" priority "$ifacePriority" >/dev/null 2>&1
								ip rule add sport "$listen_port" table "${ipTablePrefix}_${uplink_interface4}" priority "$ifacePriority" >/dev/null 2>&1
								if [ -n "$ipv6_enabled" ]; then
									#ip -6 rule del sport "$listen_port" table "${ipTablePrefix}_${uplink_interface4}" priority "$ifacePriority" >/dev/null 2>&1
									ip -6 rule add sport "$listen_port" table "${ipTablePrefix}_${uplink_interface4}" priority "$ifacePriority" >/dev/null 2>&1
								fi
								ifacePriority="$((ifacePriority - 1))"
							fi
						fi
					fi
				}
				config_foreach _wg_server 'interface'

				#ip -4 rule del priority "$ifacePriority" >/dev/null 2>&1
				#ip -4 rule del lookup 'main' suppress_prefixlength "$prefixlength" >/dev/null 2>&1
				try ip -4 rule add lookup 'main' suppress_prefixlength "$prefixlength" priority "$ifacePriority" || ipv4_error=1
				if [ -n "$ipv6_enabled" ]; then
					#ip -6 rule del priority "$ifacePriority" >/dev/null 2>&1
					#ip -6 rule del lookup 'main' suppress_prefixlength "$prefixlength" >/dev/null 2>&1
					try ip -6 rule add lookup 'main' suppress_prefixlength "$prefixlength" priority "$ifacePriority" || ipv6_error=1
				fi
				return 0
			;;
		esac
	fi

	if [ "$iface" = 'tor' ]; then
		case "$action" in
			create|reload|reload_interface)
				torDnsPort="$(get_tor_dns_port)"
				torTrafficPort="$(get_tor_traffic_port)"
				displayText="${iface}/53->${torDnsPort}/80,443->${torTrafficPort}"
				gatewaySummary="${gatewaySummary}${displayText}\n"
				;;
			destroy)
				;;
		esac
		return 0
	fi

	if is_wg_server "$iface" && ! is_ignored_interface "$iface"; then
		case "$action" in
			destroy)
				local listen_port="$(uci_get 'network' "$iface" 'listen_port')"
				if [ -n "$listen_port" ]; then
					ip rule del sport "$listen_port" table "pbr_${uplink_interface4}" >/dev/null 2>&1
					ip -6 rule del sport "$listen_port" table "pbr_${uplink_interface4}" >/dev/null 2>&1
				fi
			;;
		esac
		str_contains_word "$supported_interface" "$iface" || return 0
	fi

	is_supported_interface "$iface" || return 0
	if [ "$((ifaceMark))" -gt "$((fw_mask))" ]; then
		json add error 'errorInterfaceMarkOverflow' "$iface"
		return 1
	fi

	if is_ovpn "$iface" && ! is_ovpn_valid "$iface"; then
		: # output_warning 'warningInvalidOVPNConfig' "$iface"
	fi

	network_get_device dev4 "$iface"
	[ -z "$dev4" ] && network_get_physdev dev4 "$iface"
	if is_uplink4 "$iface" && [ -n "$uplink_interface6" ]; then
		network_get_device dev6 "$uplink_interface6"
		[ -z "$dev6" ] && network_get_physdev dev6 "$uplink_interface6"
	fi

	[ -z "$dev6" ] && dev6="$dev4"
	[ -z "$ifaceMark" ] && ifaceMark="$(printf '0x%06x' "$uplink_mark")"
	[ -z "$ifacePriority" ] && ifacePriority="$uplink_ip_rules_priority"

	local _mark="$ifaceMark" _priority="$ifacePriority" _tid="$ifaceTableID"
	local splitUplinkSecondIface

	if is_split_uplink; then
		if is_uplink4 "$iface" || is_uplink6 "$iface"; then
			if [ -n "$_uplinkMark" ] && [ -n "$_uplinkPriority" ] && [ -n "$_uplinkTableID" ]; then
				_mark="$_uplinkMark"
				_priority="$_uplinkPriority"
				_tid="$_uplinkTableID"
				splitUplinkSecondIface='true'
			else
				_uplinkMark="$ifaceMark"
				_uplinkPriority="$ifacePriority"
			fi
		fi
	fi

	case "$action" in
		enumerate_interface)
			if [ -z "$splitUplinkSecondIface" ] && [ -z "$_tid" ]; then
				_tid="$(get_rt_tables_non_pbr_next_id)"
				ifaceTableID="$_tid"
			fi

			eval "enum_mark_${iface//-/_}"='$_mark'
			eval "enum_priority_${iface//-/_}"='$_priority'
			eval "enum_tid_${iface//-/_}"='$_tid'
			ifacesTriggers="${ifacesTriggers:+${ifacesTriggers} }$iface"
		;;
		create)
			if [ -z "$splitUplinkSecondIface" ]; then
				_tid="$(get_rt_tables_id "$iface")"
				[ -z "$_tid" ] && _tid="$(get_rt_tables_next_id)"
				ifaceTableID="$_tid"
			fi
			eval "mark_${iface//-/_}"='$_mark'
			eval "tid_${iface//-/_}"='$_tid'
			pbr_get_gateway4 gw4 "$iface" "$dev4"
			pbr_get_gateway6 gw6 "$iface" "$dev6"
			dispGw4="${gw4:-0.0.0.0}"
			dispGw6="${gw6:-::/0}"
			if is_split_uplink; then
				if is_uplink4 "$iface"; then
					gw6=""; dev6=""
				elif is_uplink6 "$iface"; then
					gw4=""; dev4=""
				fi
			fi
			[ "$iface" != "$dev4" ] && dispDev="$dev4"
			if is_default_dev "$dev4"; then
				[ "$verbosity" = '1' ] && dispStatus="$_OK_" || dispStatus="$__OK__"
			fi
			if is_netifd_interface_default "$iface"; then
				[ "$verbosity" = '1' ] && dispStatus="$_OKB_" || dispStatus="$__OKB__"
			fi
			displayText="${iface}/${dispDev:+${dispDev}/}${dispGw4}${ipv6_enabled:+/${dispGw6}}"
			output 2 "Setting up routing for '$displayText' "
			if interface_routing 'create' "$_tid" "$_mark" "$iface" "$gw4" "$dev4" "$gw6" "$dev6" "$_priority"; then
				json_add_gateway 'create' "$_tid" "$_mark" "$iface" "$gw4" "$dev4" "$gw6" "$dev6" "$_priority" "$dispStatus"
				gatewaySummary="${gatewaySummary}${displayText}${dispStatus:+ ${dispStatus}}\n"
				if is_netifd_interface "$iface"; then output_okb; else output_ok; fi
			else
				json add error 'errorFailedSetup' "$displayText"
				output_fail
			fi
		;;
		create_user_set)
			if [ -z "$splitUplinkSecondIface" ]; then
				_tid="$(get_rt_tables_id "$iface")"
				[ -z "$_tid" ] && _tid="$(get_rt_tables_next_id)"
				ifaceTableID="$_tid"
			fi
			eval "mark_${iface//-/_}"='$_mark'
			eval "tid_${iface//-/_}"='$_tid'
			if is_split_uplink; then
				if is_uplink4 "$iface"; then
					dev6=""
				elif is_uplink6 "$iface"; then
					dev4=""
				fi
			fi
			[ "$iface" != "$dev4" ] && dispDev="$dev4"
			if is_default_dev "$dev4"; then
				[ "$verbosity" = '1' ] && dispStatus="$_OK_" || dispStatus="$__OK__"
			fi
			if is_netifd_interface_default "$iface"; then
				[ "$verbosity" = '1' ] && dispStatus="$_OKB_" || dispStatus="$__OKB__"
			fi
			displayText="${iface}/${dispDev:+${dispDev}/}"
			interface_routing 'create_user_set' "$_tid" "$_mark" "$iface" "" "$dev4" "" "$dev6" "$_priority"
		;;
		destroy)
			if [ -z "$splitUplinkSecondIface" ]; then
				_tid="$(get_rt_tables_id "$iface")"
				[ -z "$_tid" ] && _tid="$(get_rt_tables_next_id)"
				ifaceTableID="$_tid"
			fi
			eval "mark_${iface//-/_}"='$_mark'
			eval "tid_${iface//-/_}"='$_tid'
			if is_split_uplink; then
				if is_uplink4 "$iface"; then
					dev6=""
				elif is_uplink6 "$iface"; then
					dev4=""
				fi
			fi
			[ "$iface" != "$dev4" ] && dispDev="$dev4"
			if is_default_dev "$dev4"; then
				[ "$verbosity" = '1' ] && dispStatus="$_OK_" || dispStatus="$__OK__"
			fi
			if is_netifd_interface_default "$iface"; then
				[ "$verbosity" = '1' ] && dispStatus="$_OKB_" || dispStatus="$__OKB__"
			fi
			displayText="${iface}/${dispDev:+${dispDev}}"
			output 2 "Removing routing for '$displayText' "
			interface_routing 'destroy' "$_tid" "$_mark" "$iface" "" "$dev4" "" "$dev6" "$_priority"
			if is_netifd_interface "$iface"; then output_okb; else output_ok; fi
		;;
		reload)
			if [ -z "$splitUplinkSecondIface" ]; then
				_tid="$(get_rt_tables_id "$iface")"
				[ -z "$_tid" ] && _tid="$(get_rt_tables_next_id)"
				ifaceTableID="$_tid"
			fi
			eval "mark_${iface//-/_}"='$_mark'
			eval "tid_${iface//-/_}"='$_tid'
			pbr_get_gateway4 gw4 "$iface" "$dev4"
			pbr_get_gateway6 gw6 "$iface" "$dev6"
			dispGw4="${gw4:-0.0.0.0}"
			dispGw6="${gw6:-::/0}"
			if is_split_uplink; then
				if is_uplink4 "$iface"; then
					gw6=""; dev6=""
				elif is_uplink6 "$iface"; then
					gw4=""; dev4=""
				fi
			fi
			[ "$iface" != "$dev4" ] && dispDev="$dev4"
			if is_default_dev "$dev4"; then
				[ "$verbosity" = '1' ] && dispStatus="$_OK_" || dispStatus="$__OK__"
			fi
			if is_netifd_interface_default "$iface"; then
				[ "$verbosity" = '1' ] && dispStatus="$_OKB_" || dispStatus="$__OKB__"
			fi
			displayText="${iface}/${dispDev:+${dispDev}/}${dispGw4}${ipv6_enabled:+/${dispGw6}}"
			gatewaySummary="${gatewaySummary}${displayText}${dispStatus:+ ${dispStatus}}\n"
		;;
		reload_interface)
			if [ -z "$splitUplinkSecondIface" ]; then
				_tid="$(get_rt_tables_id "$iface")"
				[ -z "$_tid" ] && _tid="$(get_rt_tables_next_id)"
				ifaceTableID="$_tid"
			fi
			eval "mark_${iface//-/_}"='$_mark'
			eval "tid_${iface//-/_}"='$_tid'
			pbr_get_gateway4 gw4 "$iface" "$dev4"
			pbr_get_gateway6 gw6 "$iface" "$dev6"
			dispGw4="${gw4:-0.0.0.0}"
			dispGw6="${gw6:-::/0}"
			if is_split_uplink; then
				if is_uplink4 "$iface"; then
					gw6=""; dev6=""
				elif is_uplink6 "$iface"; then
					gw4=""; dev4=""
				fi
			fi
			[ "$iface" != "$dev4" ] && dispDev="$dev4"
			if is_default_dev "$dev4"; then
				[ "$verbosity" = '1' ] && dispStatus="$_OK_" || dispStatus="$__OK__"
			fi
			if is_netifd_interface_default "$iface"; then
				[ "$verbosity" = '1' ] && dispStatus="$_OKB_" || dispStatus="$__OKB__"
			fi
			displayText="${iface}/${dispDev:+${dispDev}/}${dispGw4}${ipv6_enabled:+/${dispGw6}}"
			if [ "$iface" = "$reloadedIface" ]; then
				output 2 "Reloading routing for '$displayText' "
				if interface_routing 'reload_interface' "$_tid" "$_mark" "$iface" "$gw4" "$dev4" "$gw6" "$dev6" "$_priority"; then
					json_add_gateway 'reload_interface' "$_tid" "$_mark" "$iface" "$gw4" "$dev4" "$gw6" "$dev6" "$_priority" "$dispStatus"
					gatewaySummary="${gatewaySummary}${displayText}${dispStatus:+ ${dispStatus}}\n"
					if is_netifd_interface "$iface"; then output_okb; else output_ok; fi
				else
					json add error 'errorFailedReload' "$displayText"
					output_fail
				fi
			else
				json_add_gateway 'skip_interface' "$_tid" "$_mark" "$iface" "$gw4" "$dev4" "$gw6" "$dev6" "$_priority" "$dispStatus"
				gatewaySummary="${gatewaySummary}${displayText}${dispStatus:+ ${dispStatus}}\n"
			fi
		;;
	esac

	if is_split_uplink && [ -z "$splitUplinkSecondIface" ]; then
		if is_uplink4 "$iface" || is_uplink6 "$iface"; then
			_uplinkTableID="$_tid"
		fi
	fi

	if [ -z "$splitUplinkSecondIface" ]; then
		ifaceMark="$(printf '0x%06x' $((ifaceMark + uplink_mark)))"
		ifacePriority="$((ifacePriority - 1))"
		ifaceTableID="$((ifaceTableID + 1))"
	fi
	return $s
}

user_file_process() {
	local shellBin="${SHELL:-/bin/ash}"
	[ "$enabled" = '1' ] || return 0
	if [ ! -s "$path" ]; then
		json add error 'errorUserFileNotFound' "$path"
		output_fail
		return 1
	fi
	if ! $shellBin -n "$path"; then
		json add error 'errorUserFileSyntax' "$path"
		output_fail
		return 1
	fi
	if is_bad_user_file_nft_call "$path"; then
		json add error 'errorIncompatibleUserFile' "$path"
		output_fail
		return 1
	fi
	output 2 "Running $path "
# shellcheck disable=SC1090
	if ! . "$path"; then
		json add error 'errorUserFileRunning' "$path"
		if grep -q -w 'curl' "$path" && ! is_present 'curl'; then
			json add error 'errorUserFileNoCurl' "$path"
		fi
		output_fail
		return 1
	else
		output_ok
		return 0
	fi
}

boot() {
	nft_file 'delete' 'main'
	rc_procd start_service 'on_boot' && service_started 'on_boot'
}

on_interface_reload() {
	if ! exists_lockfile; then
		logger -t "$packageName" "Reload on interface change aborted: service is stopped."
		return 0
	else
		rc_procd start_service 'on_interface_reload' "$1"
	fi
}

start_service() {
	local param="$1"
	local resolverStoredHash resolverNewHash reloadedIface
	local i k

	load_package_config "$param"
	[ "$param" = 'on_boot' ] && pbrBootFlag=1 && return 0
	json init
	load_environment "${param:-on_start}" "$(load_validate_config)" || return 1

	output "Processing environment (${param:-on_start}) "
	if ! is_wan_up "$param"; then
		output_failn
		output_warning "$(get_text 'warningUplinkDown')"
		pbrBootFlag=1
		return 0
	fi

	process_interface 'all' 'reset_globals'
	config_foreach process_interface 'interface' 'enumerate_interface'

	case "$param" in
		on_boot)
			serviceStartTrigger='on_start'
		;;
		on_firewall_reload)
			serviceStartTrigger='on_start'
		;;
		on_interface_reload)
			reloadedIface="$2"
			local tid enum_tid
			tid="$(get_rt_tables_id "$reloadedIface")"
			enum_tid="$(eval echo "\$enum_tid_${reloadedIface//-/_}")"
			if is_split_uplink && is_uplink6 "$reloadedIface" && { [ -z "$tid" ] || [ -z "$enum_tid" ]; }; then
				tid="$(get_rt_tables_id "$uplink_interface4")"
				enum_tid="$(eval echo "\$enum_tid_${uplink_interface4//-/_}")"
			fi
			if [ "$tid" = "$enum_tid" ]; then
				serviceStartTrigger='on_interface_reload'
			else
				serviceStartTrigger='on_start'
				unset reloadedIface
			fi
		;;
		on_reload)
			serviceStartTrigger='on_reload'
		;;
		on_restart)
			serviceStartTrigger='on_start'
		;;
	esac

	if [ -n "$reloadedIface" ] && ! is_supported_interface "$reloadedIface"; then
		return 0
	fi

	if [ -n "$(ubus_get_status error)" ] || [ -n "$(ubus_get_status warning)" ]; then
		serviceStartTrigger='on_start'
		unset reloadedIface
	elif ! is_service_running; then
		serviceStartTrigger='on_start'
		unset reloadedIface
	elif [ -z "$(ubus_get_status gateways)" ]; then
		serviceStartTrigger='on_start'
		unset reloadedIface
	else
		serviceStartTrigger="${serviceStartTrigger:-on_start}"
	fi

	procd_open_instance 'main'
	procd_set_param command /bin/true
	procd_set_param stdout 1
	procd_set_param stderr 1
	procd_open_data

	case $serviceStartTrigger in
		on_interface_reload)
			output_okn
			output 1 "Reloading Interface: $reloadedIface "
			json_add_array 'gateways'
			process_interface 'all' 'reset_globals'
			config_foreach process_interface 'interface' 'reload_interface' "$reloadedIface"
			json_close_array
			output_1_newline
		;;
		on_reload|on_start|*)
			resolver 'store_hash'
			resolver 'configure'
			cleanup 'main_table' 'rt_tables' 'main_chains' 'sets'
			nft_file 'create' 'main'
			output_okn
			output 1 'Processing interfaces '
			json_add_array 'gateways'
			process_interface 'all' 'reset_globals'
			config_foreach process_interface 'interface' 'create'
			process_interface 'tor' 'destroy'
			is_tor_running && process_interface 'tor' 'create'
			process_interface 'all' 'create_global_rules'
			json_close_array
			ip route flush cache
			output_1_newline
			if is_config_enabled 'policy'; then
				output 1 'Processing policies '
				config_load "$packageName"
				config_foreach load_validate_policy 'policy' policy_process
				output_1_newline
			fi
			if is_config_enabled 'dns_policy'; then
				output 1 'Processing dns policies '
				config_load "$packageName"
				config_foreach load_validate_dns_policy 'dns_policy' dns_policy_process
				output_1_newline
			fi
			if is_config_enabled 'include' || [ -d "/etc/${packageName}.d/" ]; then
				process_interface 'all' 'reset_globals'
				config_foreach process_interface 'interface' 'create_user_set'
				output 1 'Processing user file(s) '
				config_load "$packageName"
				config_foreach load_validate_include 'include' user_file_process
				if [ -d "/etc/${packageName}.d/" ]; then
					local i
					for i in "/etc/${packageName}.d/"*; do
						local enabled='1' path="$i"
						[ -f "$i" ] && user_file_process
					done
				fi
				output_1_newline
			fi
			nft_file 'install' 'main'
		;;
	esac

	json_add_int 'packageCompat' "$packageCompat"
	json_add_object 'status'
	[ -n "$gatewaySummary" ] && json_add_string 'gateways' "$gatewaySummary" || json add error 'errorNoGateways'
	json_close_object
	json_add_array 'errors'
		for k in $(json get errors); do
			json_add_object "$k"
			json_add_string 'code' "$(json get error "$k" 'code')"
			json_add_string 'info' "$(json get error "$k" 'info')"
			json_close_object
		done
	json_close_array
	json_add_array 'warnings'
		for k in $(json get warnings); do
			json_add_object "$k"
			json_add_string 'code' "$(json get warning "$k" 'code')"
			json_add_string 'info' "$(json get warning "$k" 'info')"
			json_close_object
		done
	json_close_array
	if [ -n "$strict_enforcement" ] && str_contains "$gatewaySummary" '0.0.0.0'; then
		json_add_string 'mode' 'strict'
	fi
	procd_close_data
	procd_close_instance
}

service_running() { is_service_running; }
service_started() {
	[ -n "$pbrBootFlag" ] && return 0
	local error warning c
	if nft_file 'exists' 'main'; then
		resolver 'compare_hash' && resolver 'restart'
		[ -n "$gatewaySummary" ] && output "$serviceName started with gateways:\n${gatewaySummary}"
	else
		output "$serviceName FAILED TO START!!!\n"
		output "Check the output of nft -c -f $nftTempFile\n"
	fi
	warning="$(json get warning)"
	if [ -n "$warning" ]; then
		for c in $warning; do
			code="$(json get warning "$c" 'code')"
			info="$(json get warning "$c" 'info')"
			output_warning "$(get_text "$code" "$info")"
		done
		output_warning "$(get_text 'warningSummary' "$(get_url '#warning-messages-details')")"
	fi
	error="$(json get error)"
	if [ -n "$error" ]; then
		for c in $error; do
			code="$(json get error "$c" 'code')"
			info="$(json get error "$c" 'info')"
			output_error "$(get_text "$code" "$info")"
		done
		output_error "$(get_text 'errorSummary' "$(get_url '#error-messages-details')")"
	fi
	touch "$packageLockFile"
	if [ -n "$error" ]; then
		return 2
	elif [ -n "$warning" ]; then
		return 1
	else
		return 0
	fi
}
service_stopped() { procd_set_config_changed firewall; }

# shellcheck disable=SC2015
service_triggers() {
	local n
	if [ -n "$pbrBootFlag" ]; then
		output "Setting trigger (on_boot) "
		procd_add_raw_trigger "interface.*.up" "$procd_boot_trigger_delay" "/etc/init.d/${packageName}" start && output_okn || output_failn
	else
		PROCD_RELOAD_DELAY=$(( procd_reload_delay * 1000 ))
		procd_open_validate
			load_validate_config
			load_validate_policy
			load_validate_include
		procd_close_validate
		procd_open_trigger
			procd_add_config_trigger "config.change" 'openvpn' "/etc/init.d/${packageName}" reload 'on_openvpn_change'
			procd_add_config_trigger "config.change" "${packageName}" "/etc/init.d/${packageName}" reload
			if [ -n "$ifacesTriggers" ]; then
				output 1 "Setting interface triggers "
				for n in $ifacesTriggers; do
					output 2 "Setting interface trigger for $n "
					procd_add_interface_trigger "interface.*" "$n" "/etc/init.d/${packageName}" on_interface_reload "$n" && output_ok || output_fail
				done
				output_1_newline
			fi
		procd_close_trigger
		if [ "$serviceStartTrigger" = 'on_start' ] && [ -n "$ifacesTriggers" ]; then
			output 3 "$serviceName monitoring interfaces: ${ifacesTriggers}\n"
		fi
	fi
}

# shellcheck disable=SC2015
stop_service() {
	local i nft_file_mode
	json init
	! is_service_running && [ "$(get_rt_tables_next_id)" = "$(get_rt_tables_non_pbr_next_id)" ] && return 0
	rm -f "$packageLockFile"
	[ "$1" = 'quiet' ] && quiet_mode 'on'
	load_environment 'on_stop'
	if nft_file 'exists' 'main'; then
		nft_file_mode=1
	fi
	output 'Resetting routing '
	if nft_file 'delete' 'main' && \
		cleanup 'main_table' 'rt_tables' && \
		ip route flush cache; then
		output_okn
	else
		output_failn
	fi
	unset ifaceMark
	unset ifaceTableID
	output 'Resetting resolver '
	if resolver 'store_hash' && resolver 'cleanup'; then
		output_okn
	else
		output_failn
	fi
	resolver 'compare_hash' && resolver 'restart'

	if [ -n "$enabled" ]; then
		if [ -n "$nft_file_mode" ]; then
			output "$serviceName (fw4 nft file mode) stopped "; output_okn;
		else
			output "$serviceName (nft mode) stopped "; output_okn;
		fi
	fi
}

version() { echo "$PKG_VERSION"; }

status_service() {
	local i dev4 dev6 wanTID ipv6_enabled

	load_package_config 'status'
	load_network 'status'

	[ -f "/etc/os-release" ] && . /etc/os-release
	while [ "${1:0:1}" = "-" ]; do param="${1//-/}"; eval "set_$param=1"; shift; done
	[ -e "/var/${packageName}-support" ] && rm -f "/var/${packageName}-support"
# shellcheck disable=SC2154
	status="$serviceName on $OPENWRT_RELEASE.\n"
	if [ -n "$uplink_interface4" ]; then
		network_get_device dev4 "$uplink_interface4"
		[ -z "$dev4" ] && network_get_physdev dev4 "$uplink_interface4"
		status="${status}Uplink (IPv4): ${uplink_interface4}${dev4:+/${dev4}}/${uplinkGW4:-0.0.0.0}.\n"
	fi
	if [ -n "$uplink_interface6" ]; then
		network_get_device dev6 "$uplink_interface6"
		[ -z "$dev6" ] && network_get_physdev dev6 "$uplink_interface6"
		[ -z "$dev6" ] && dev6="$dev4"
		status="${status}Uplink (IPv6): ${uplink_interface6}${dev6:+/${dev6}}/${uplinkGW6:-::/0}.\n"
	fi

	echo "$_SEPARATOR_"
	echo "$packageName - environment"
	echo -en "$status"
	echo "$_SEPARATOR_"
	dnsmasq --version 2>/dev/null | sed '/^$/,$d'
	if nft_file 'exists' 'netifd'; then
		echo "$_SEPARATOR_"
		nft_file 'show' 'netifd'
	fi
	if nft_file 'exists' 'main'; then
		echo "$_SEPARATOR_"
		nft_file 'show' 'main'
	fi
	echo "$_SEPARATOR_"
	echo "$packageName chains - policies"
	for i in $chainsList dstnat; do
		"$nft" -a list table inet "$nftTable" | sed -n "/chain ${nftPrefix}_${i} {/,/\t}/p"
	done
	echo "$_SEPARATOR_"
	echo "$packageName chains - marking"
	for i in $(get_mark_nft_chains); do
		"$nft" -a list table inet "$nftTable" | sed -n "/chain ${i} {/,/\t}/p"
	done
	echo "$_SEPARATOR_"
	echo "$packageName nft sets"
	for i in $(get_nft_sets); do
		"$nft" -a list table inet "$nftTable" | sed -n "/set ${i} {/,/\t}/p"
	done
	if [ -s "$packageDnsmasqFile" ]; then
		echo "$_SEPARATOR_"
		echo "dnsmasq nft sets in $packageDnsmasqFile"
		cat "$packageDnsmasqFile"
	fi
#	echo "$_SEPARATOR_"
#	ip rule list | grep "${packageName}_"
	echo "$_SEPARATOR_"
	echo "$packageName tables & routing"
	tableCount="$(grep -c "${packageName}_" "$rtTablesFile")" || tableCount=0
	wanTID=$(($(get_rt_tables_next_id)-tableCount))
	for tid in main $(seq "$wanTID" $((wanTID + tableCount - 1))); do
		status_table="$(grep "^${tid}[[:space:]]" "$rtTablesFile" | awk '{print $2}')"
		echo "IPv4 table ${tid}${status_table:+ (${status_table})} routes:"
		ip -4 route show table "$tid" | sed 's/^/    /'
		echo "IPv4 table ${tid}${status_table:+ (${status_table})} rules:"
		ip -4 rule list table "$tid" | sed 's/^/    /'
		if [ -n "$ipv6_enabled" ]; then
			echo "$_SEPARATOR_"
			echo "IPv6 table $tid routes:"
			ip -6 route show table "$tid" | sed 's/^/    /'
			echo "IPv6 table $tid rules:"
			ip -6 rule list table "$tid" | sed 's/^/    /'
		fi
		echo "$_SEPARATOR_"
	done
}

print_config_masked() {
	local file="$1"
	[ ! -f "/etc/config/$file" ] && return
	printf "\n===== %s config =====\n" "$file"

	awk -v sq="'" '
		BEGIN {
			masklist = "^(endpoint_host|key|password|preshared_key|private_key|psk|public_key|token|username)$"
		}

		/^[ \t]*(option|list)[ \t]+/ {
			orig = $0

			# capture indentation
			match(orig, /^[ \t]*/)
			indent = substr(orig, RSTART, RLENGTH)

			# capture kind: "option" or "list"
			tmp = orig
			sub(/^[ \t]*/, "", tmp)
			kind = tmp
			sub(/[ \t].*$/, "", kind)

			# remove leading indent + kind
			line = orig
			sub(/^[ \t]*(option|list)[ \t]+/, "", line)

			# extract key name
			key = line
			sub(/[ \t].*$/, "", key)

			# extract value (rest after key)
			sub(/^[^ \t]+[ \t]+/, "", line)
			val = line
			sub(/^[ \t]+/, "", val)
			sub(/[ \t]+$/, "", val)

			# If this key is in masklist, mask the value (preserve dots and length)
			if (key ~ masklist) {
				q = ""
				if (val ~ "^" sq ".*" sq "$")      { q = sq; val = substr(val, 2, length(val)-2) }
				else if (val ~ /^".*"$/)           { q = "\""; val = substr(val, 2, length(val)-2) }

				masked = val
				gsub(/[^.]/, "*", masked)

				if (q != "")
					print indent kind " " key " " q masked q
				else
					print indent kind " " key " " masked
				next
			}

			# Otherwise print unchanged for now
			print orig
			next
		}

		{ print }
	' "/etc/config/$file" \
	| awk '
		# Do NOT mask IPs (v4) in allowed_ips (option or list)
		/^[ \t]*(option|list)[ \t]+allowed_ips[ \t]+/ {
			print
			next
		}

		{
			line = $0
			result = ""

			# Mask digits inside IPv4-looking tokens, keep dots
			# RFC1918/loopback addresses are not sensitive — skip masking them
			while (match(line, /([0-9]{1,3}\.){3}[0-9]{1,3}/)) {
				ip = substr(line, RSTART, RLENGTH)
				result = result substr(line, 1, RSTART-1)
				line = substr(line, RSTART+RLENGTH)

				if (ip ~ /^(10\.|127\.|192\.168\.)/ || ip ~ /^172\.(1[6-9]|2[0-9]|3[01])\./) {
					result = result ip
				} else {
					masked = ip
					gsub(/[0-9]/, "*", masked)
					result = result masked
				}
			}

			print result line
		}
	' \
	| sed -E 's/([a-fA-F0-9:]{2,}:){1,7}[a-fA-F0-9]{2,}/***/g'
}

support() {
	echo "Setting counters and verbosity for diagnostics..."
	uci set pbr.config.nft_rule_counter='1'
	uci set pbr.config.nft_set_counter='1'
	uci set pbr.config.verbosity='2'
	uci commit pbr

	for cfg in dhcp firewall network pbr; do
		print_config_masked "$cfg"
	done

	printf "\n===== ubus call system board =====\n"
	ubus call system board

	printf "\n===== /etc/init.d/pbr restart =====\n"
	/etc/init.d/pbr restart

	printf "\n===== /etc/init.d/pbr status (after restart) =====\n"
	/etc/init.d/pbr status
}

# shellcheck disable=SC2120
load_validate_config() {
	uci_load_validate "$packageName" "$packageName" "$1" "${2}${3:+ ${3}}" \
		'enabled:bool:0' \
		'strict_enforcement:bool:1' \
		'ipv6_enabled:bool:0' \
		'resolver_set:or("", "none", "dnsmasq.nftset")' \
		'resolver_instance:list(or(integer, string)):*' \
		'verbosity:range(0,2):2' \
		'uplink_mark:regex("[A-Fa-f0-9]{8}"):00010000' \
		'uplink_ip_rules_priority:range(99,32765):30000' \
		'fw_mask:regex("[A-Fa-f0-9]{8}"):00ff0000' \
		'icmp_interface:or("", tor, uci("network", "@interface"))' \
		'ignored_interface:list(or(tor, uci("network", "@interface")))' \
		'supported_interface:list(or(ignore, tor, regex("xray_.*"), uci("network", "@interface")))' \
		'procd_boot_trigger_delay:range(1000,10000):5000' \
		'prefixlength:uinteger:1' \
		'lan_device:list(or(network)):br-lan' \
		'procd_reload_delay:uinteger:0' \
		'uplink_interface:network:wan' \
		'uplink_interface6:network:wan6' \
		'webui_supported_protocol:list(string)' \
		'nft_rule_counter:bool:0'\
		'nft_set_auto_merge:bool:1'\
		'nft_set_counter:bool:0'\
		'nft_set_flags_interval:bool:1'\
		'nft_set_flags_timeout:bool:0'\
		'nft_set_gc_interval:or("", string)'\
		'nft_set_policy:or("", memory, performance):performance'\
		'nft_set_timeout:or("", string)' \
	;
}

# shellcheck disable=SC2120
load_validate_dns_policy() {
	local name
	local enabled
	local src_addr
	local dest_dns
	local dest_dns_port
	uci_load_validate "$packageName" 'dns_policy' "$1" "${2}${3:+ ${3}}" \
		'name:string:Untitled' \
		'enabled:bool:1' \
		'src_addr:list(neg(or(host,network,macaddr,string)))' \
		'dest_dns:list(or(host,network,string))' \
		'dest_dns_port:port:53' \
	;
}

# shellcheck disable=SC2120
load_validate_policy() {
	local name
	local enabled
	local interface
	local proto
	local chain
	local src_addr
	local src_port
	local dest_addr
	local dest_port
	uci_load_validate "$packageName" 'policy' "$1" "${2}${3:+ ${3}}" \
		'name:string:Untitled' \
		'enabled:bool:1' \
		'interface:or("ignore", "tor", regex("xray_.*"), uci("network", "@interface")):wan' \
		'proto:or(string)' \
		'chain:or("", "forward", "output", "prerouting"):prerouting' \
		'src_addr:list(neg(or(host,network,macaddr,string)))' \
		'src_port:list(neg(or(portrange,string)))' \
		'dest_addr:list(neg(or(host,network,string)))' \
		'dest_port:list(neg(or(portrange,string)))' \
	;
}

# shellcheck disable=SC2120
load_validate_include() {
	local path=
	local enabled=
	uci_load_validate "$packageName" 'include' "$1" "${2}${3:+ ${3}}" \
		'path:file' \
		'enabled:bool:0' \
	;
}
