#!/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 'version' "Show version information"
else
# shellcheck disable=SC2034
	EXTRA_COMMANDS='netifd on_interface_reload status version'
# shellcheck disable=SC2034
	EXTRA_HELP="	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"
fi

readonly packageName='pbr'
readonly PKG_VERSION='dev-test'
readonly packageCompat='19'
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 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 nftPermFile="/usr/share/nftables.d/ruleset-post/30-${packageName}.nft"
readonly nftNetifdPermFile="/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_interface6=
uplink_interface6_metric='128'
resolver_set=
resolver_instance=
strict_enforcement=
supported_interface=
verbosity=
uplink_ip_rules_priority=
uplink_mark=
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=
wanIface4=
wanIface6=
ifaceMark=
ifaceTableID=
ifacePriority=
ifacesAll=
ifacesSupported=
ifacesTriggers=
firewallWanZone=
wanGW4=
wanGW6=
pbrBootFlag=
serviceStartTrigger=
processDnsPolicyError=
processPolicyError=
processPolicyWarning=
resolverSetSupported=
pbrNftPrevParam4=
pbrNftPrevParam6=
nftRuleParams=
nftSetParams=
torDnsPort=
torTrafficPort=
dnsmasq_features=
dnsmasq_ubus=
nft_fw4_dump=
loadEnvironmentFlag=
loadPackageConfigFlag=

# 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_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_interface";;
	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
	[ "$iface" = "$uplink_interface" ] && 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"
}
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'; }
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')" ]; }
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_supported_interface() { { is_lan "$1" || is_disabled_interface "$1"; } && return 1; str_contains_word "$supported_interface" "$1" || { ! is_ignored_interface "$1" && { is_wan "$1" || is_wan6 "$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_wan() { [ "$1" = "$wanIface4" ] || { [ "${1##wan}" != "$1" ] && [ "${1##wan6}" = "$1" ]; } || [ "${1%%wan}" != "$1" ]; }
is_wan6() { [ -n "$wanIface6" ] && [ "$1" = "$wanIface6" ] || [ "${1##wan6}" != "$1" ] || [ "${1%%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; }
# 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() { 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"; }
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 "$nftPermFile" ]; }
is_running_nft() { "$nft" list table inet fw4 | grep chain | grep -q pbr_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" ]
}
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";;
		errorNoWanGateway) printf "The %s service failed to discover WAN 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";;
		errorFailedToResolve) printf "Failed to resolve '%s'" "$1";;
		errorInvalidOVPNConfig) printf "Invalid OpenVPN config for '%s' interface" "$1";;
		errorNftFileInstall) 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";;
		errorNetifdNftFileInstall) printf "Netifd setup: failed to install fw4 netifd nft file '%s'" "$1";;
		errorNetifdNftFileRemove) 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";;
		warningOutdatedWebUIApp) 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 '#WarningMessagesDetails')";;
		warningIncompatibleDHCPOption6) printf "Incompatible DHCP Option 6 for interface '%s'" "$1";;
		warningNetifdMissingInterfaceLocal) printf "Netifd setup: option netifd_interface_local is missing, assuming '%s'" "$1";;
		*) 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      ignored_interface         'config' 'ignored_interface'
	config_get_bool ipv6_enabled              'config' 'ipv6_enabled'             '0'
	config_get      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      procd_boot_trigger_delay  'config' 'procd_boot_trigger_delay' '5000'
	config_get      procd_reload_delay        'config' 'procd_reload_delay'       '0'
	config_get      resolver_instance         'config' 'resolver_instance'        '*'
	config_get      resolver_set              'config' 'resolver_set'
	config_get_bool strict_enforcement        'config' 'strict_enforcement'       '1'
	config_get      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 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'

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

	[ "$resolver_set" = 'none' ]      && unset resolver_set
	[ "$enabled" = '1' ]              || unset enabled
	[ "$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"
	[ -z "$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"
#			load_network "$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" "$1" && ifacesSupported="${ifacesSupported}${i} "
		done
		config_load 'network'
		config_foreach _build_ifaces_supported 'interface'
	fi
	wanIface4="$uplink_interface"
	network_get_device dev4 "$wanIface4"
	[ -z "$dev4" ] && network_get_physdev dev4 "$wanIface4"
	[ -z "$wanGW4" ] && pbr_get_gateway4 wanGW4 "$wanIface4" "$dev4"
	if [ -n "$ipv6_enabled" ]; then
		wanIface6="$uplink_interface6"
		network_get_device dev6 "$wanIface6"
		[ -z "$dev6" ] && network_get_physdev dev6 "$wanIface6"
		[ -z "$wanGW6" ] && pbr_get_gateway6 wanGW6 "$wanIface6" "$dev6"
	fi

	case "$param" in
		on_boot|on_start)
			[ -n "$wanIface4" ] && output 2 "Using uplink${wanIface6:+ IPv4} interface (${param}): $wanIface4 $__OK__\n"
			[ -n "$wanGW4" ] && output 2 "Found uplink${wanIface6:+ IPv4} gateway (${param}): $wanGW4 $__OK__\n"
			[ -n "$wanIface6" ] && output 2 "Using uplink IPv6 interface (${param}): $wanIface6 $__OK__\n"
			[ -n "$wanGW6" ] && output 2 "Found uplink IPv6 gateway (${param}): $wanGW6 $__OK__\n"
		;;
	esac
	wanGW="${wanGW4:-$wanGW6}"
}

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

nft() { [ -n "$*" ] && nft_file 'add_command' "$@"; }
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
	case "$1" in
		add|add_command)
			shift
			echo "$*" >> "$nftTempFile"
		;;
		create)
			rm -f "$nftTempFile" "$nftPermFile"
			for i in "$nftTempFile" "$nftPermFile"; do
				mkdir -p "${i%/*}"
			done
			{ echo '#!/usr/sbin/nft -f'; echo ''; } > "$nftTempFile"
			# Insert PBR guards at the top of main caller 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
		;;
		delete|rm|remove)
			rm -f "$nftTempFile" "$nftPermFile"
		;;
		enabled)
			return 0
		;;
		exists)
			[ -s "$nftPermFile" ] && return 0 || return 1
		;;
		install)
			[ -s "$nftTempFile" ] || return 1
			output "Installing fw4 nft file "
			if nft_call -c -f "$nftTempFile" && \
				cp -f "$nftTempFile" "$nftPermFile"; then
				output_okn
			else
				json add error 'errorNftFileInstall' "$nftTempFile"
				output_failn
			fi
		;;
		netifd_exists)
			[ -s "$nftNetifdPermFile" ] && return 0 || return 1
		;;
		netifd_create)
			rm -f "$nftTempFile" "$nftNetifdPermFile"
			for i in "$nftTempFile" "$nftNetifdPermFile"; do
				mkdir -p "${i%/*}"
			done
			{ echo '#!/usr/sbin/nft -f'; echo ''; } > "$nftTempFile"
		;;
		netifd_delete|netifd_rm)
			rm -f "$nftNetifdPermFile"
		;;
		netifd_install)
			[ -s "$nftTempFile" ] || return 1
			output "Installing fw4 netifd nft file "
			if nft_call -c -f "$nftTempFile" && \
				cp -f "$nftTempFile" "$nftNetifdPermFile"; then
				output_okbn
			else
				json add error 'errorNetifdNftFileInstall' "$nftTempFile"
				output_failn
			fi
		;;
		netifd_remove)
			output "Removing fw4 netifd nft file "
			if rm -f "$nftNetifdPermFile"; then
				output_okbn
			else
				json add error 'errorNetifdNftFileRemove' "$nftTempFile"
				output_failn
			fi
		;;
	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_rt_tables() {
	local i
# shellcheck disable=SC2013
	for i in $(grep -oh "${ipTablePrefix}_.*" "$rtTablesFile"); do
		! is_netifd_table "$i" && sed -i "/${i}/d" "$rtTablesFile"
	done
	sync
}

cleanup_main_table() {
	for prio in $(ip -4 rule show \
    | awk -v mask="$fw_mask" '/fwmark/ && $0 ~ mask && /lookup main/ && /suppress_prefixlength 0/ {sub(":", "", $1); print $1}'
); do
    ip -4 rule del priority "$prio"
done
}

cleanup_main_chains() {
	local i j
	for i in $chainsList dstnat; do
		i="$(str_to_lower "$i")"
		nft_call flush chain inet "$nftTable" "${nftPrefix}_${i}"
	done
}

cleanup_marking_chains() {
	local i j
	for i in $(get_mark_nft_chains); do
		nft_call flush chain inet "$nftTable" "$i"
		nft_call delete chain inet "$nftTable" "$i"
	done
}

cleanup_sets() {
	local i
	for i in $(get_nft_sets); do
		nft_call flush set inet "$nftTable" "$i"
		nft_call delete set inet "$nftTable" "$i"
	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_file
		[ -z "$dnsmasq_ubus" ] && dnsmasq_ubus="$(ubus call service list '{"name":"dnsmasq"}')"
		cfg_file="$(echo "$dnsmasq_ubus" | jsonfilter -e "@.dnsmasq.instances.${1}.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
		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;;
			esac
		;;
		dnsmasq.nftset)
			case "$param" in
				add_resolver_element)
					[ -n "$resolverSetSupported" ] || return 1
					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
					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'
				;;
				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]" \
							|| _dnsmasq_instance_config "$i"
						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; }')";;
			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}"
		local rt_name="${ipTablePrefix}_${iface%6}"

		uci_remove 'network' 'rule' "${rt_name}_ipv4"  2>/dev/null
		uci_remove 'network' 'rule6' "${rt_name}_ipv6" 2>/dev/null

		if [ -n "$netifd_strict_enforcement" ] && str_contains "$netifd_interface_local" "$iface"; then
			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
				uci_add 'network' 'rule6' "${rt_name}_ipv6"
				uci_set 'network' "${rt_name}_ipv6" 'in' "${iface}"
				uci_set 'network' "${rt_name}_ipv6" 'lookup' "${ipTablePrefix}_${netifd_interface_default6}"
				uci_set 'network' "${rt_name}_ipv6" 'priority' "${lan_priority}"
			fi
		lan_priority="$((lan_priority + 1))"
		fi
		is_supported_interface "$iface" || return 0

		if [ -z "$target_iface" ] || [ "$target_ifance" = "$iface" ]; then
			is_wan6 "$iface" && return # TODO: properly process wan/wan6 at some point
			if [ -z "$netifd_strict_enforcement" ] && [ "$netifd_interface_default" = "$iface" ]; then
				rt_name='main'
			fi
			case "$action" in
				install)
					output 2 "Setting up netifd extensions for $iface... "
					[ "$rt_name" = 'main' ] || sed -i "\#${rt_name}\$#d" "$rtTablesFile" >/dev/null 2>&1
					[ "$rt_name" = 'main' ] || echo "${tid} ${rt_name}" >> "$rtTablesFile"
					uci_set 'network' "${iface}" 'ip4table' "${rt_name}"
					uci_set 'network' "${iface}" 'ip6table' "${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}"
					if [ -n "$ipv6_enabled" ]; then
						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
					sed -i "\#${mark}#d" "$nftTempFile" >/dev/null 2>&1
					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"
					output_okb
				;;
				remove|uninstall)
					output 2 "Removing netifd extensions for $iface... "
					[ "$rt_name" = 'main' ] || sed -i "\#${rt_name}\$#d" "$rtTablesFile" >/dev/null 2>&1
					sed -i "\#${mark}#d" "$nftNetifdPermFile" >/dev/null 2>&1
					uci_remove 'network' "${iface}" 'ip4table' "${rt_name}" 2>/dev/null
					uci_remove 'network' "${iface}" 'ip6table' "${rt_name}" 2>/dev/null
					output_okb
				;;
			esac
		fi
		mark="$(printf '0x%06x' $((mark + uplink_mark)))"
		priority="$((priority - 1))"
		tid="$((tid + 1))"
	}

	load_package_config
	json 'init'

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

	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_error '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 'netifd_create'
	output 1 "Netifd extensions $action ${target_iface:+on $target_iface }"
	config_load 'network'
	config_foreach _netifd_process_interface 'interface' "$action"
	output_1_newline

	case "$action" in
		install)
			nft_file 'netifd_install'
			if [ -z "$target_iface" ]; then
				uci_set "$packageName" 'config' 'netifd_enabled' '1'
			fi
		;;
		remove)
			if [ -z "$target_iface" ]; then
				nft_file 'netifd_remove'
				uci_remove "$packageName" 'config' 'netifd_enabled' 2>/dev/null
			fi
		;;
		uninstall)
			nft_file 'netifd_remove'
		;;
	esac
	uci_commit "$packageName"
#	cat "$nftNetifdPermFile"
#	cat "$rtTablesFile"
#	uci changes
#	uci revert network
	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 chain='dstnat' iface='dns'

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

	if [ -z "$ipv6_enabled" ] && is_ipv6 "$(str_first_word "$src_addr")"; then
		processPolicyError='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
		processPolicyError='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
			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
	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 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 }"
			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")"

	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"
		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" dev="$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
			ifacesTriggers="${ifacesTriggers:+$ifacesTriggers }$iface"
			if ! grep -q "$tid ${ipTablePrefix}_${iface}" "$rtTablesFile"; then
				sed -i "/${ipTablePrefix}_${iface}/d" "$rtTablesFile"
				echo "$tid ${ipTablePrefix}_${iface}" >> "$rtTablesFile"
				sync
			fi
			# Ensure a clean slate for this table before adding routes/rules
			ip -4 rule flush table "$tid" >/dev/null 2>&1
			ip -4 route flush table "$tid" >/dev/null 2>&1
			if [ -n "$ipv6_enabled" ]; then
				ip -6 rule flush table "$tid" >/dev/null 2>&1
				ip -6 route flush table "$tid" >/dev/null 2>&1
			fi
			if [ -n "$gw4" ] || [ -n "$strict_enforcement" ]; then
				ipv4_error=0
				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 "$dev" table "$tid" || ipv4_error=1
				fi
				# try ip -4 rule replace fwmark "${mark}/${fw_mask}" lookup 'main' suppress_prefixlength 0 priority "$((priority - 1000))" || ipv4_error=1
				{
					for prio in $(ip -4 rule show | awk '/lookup main/ && /suppress_prefixlength 0/ {gsub(":", "", $1); print $1}'); do
						rule="$(ip -4 rule show | awk -v p="$prio" '($1==p":"){ $1=""; sub(/^ /,""); print }')"
						[ -n "$rule" ] || continue
						rule="${rule/lookup main/lookup $tid}"
						ip -4 rule replace priority "$prio" $rule >/dev/null 2>&1 || ipv4_error=1
					done
				}
				try ip -4 rule replace fwmark "${mark}/${fw_mask}" table "$tid" priority "$priority" || ipv4_error=1
			fi
			try nft add chain inet "$nftTable" "${nftPrefix}_mark_${mark}" || ipv4_error=1
			try nft add rule inet "$nftTable" "${nftPrefix}_mark_${mark} ${nftRuleParams} meta mark set (meta mark & ${fw_maskXor}) | ${mark}" || ipv4_error=1
			try nft add rule inet "$nftTable" "${nftPrefix}_mark_${mark} return" || ipv4_error=1
			if [ -n "$ipv6_enabled" ]; then
				ipv6_error=0
				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}" lookup 'main' suppress_prefixlength 0 priority "$((priority - 1000))" || ipv6_error=1
					{
						for prio in $(ip -6 rule show | awk '/lookup main/ && /suppress_prefixlength 0/ {gsub(":", "", $1); print $1}'); do
							rule="$(ip -6 rule show | awk -v p="$prio" '($1==p":"){ $1=""; sub(/^ /,""); print }')"
							[ -n "$rule" ] || continue
							rule="${rule/lookup main/lookup $tid}"
							ip -6 rule replace priority "$prio" $rule >/dev/null 2>&1 || ipv6_error=1
						done
					}
					try ip -6 rule replace fwmark "${mark}/${fw_mask}" table "$tid" priority "$priority" || ipv6_error=1
				fi
			fi
			if [ "$ipv4_error" -eq '0' ] || [ "$ipv6_error" -eq '0' ]; then
				dscp="$(uci_get "$packageName" 'config' "${iface}_dscp")"
				if [ "${dscp:-0}" -ge '1' ] && [ "${dscp:-0}" -le '63' ]; then
					try nft add rule inet "$nftTable" "${nftPrefix}_prerouting ${nftIPv4Flag} dscp ${dscp} ${nftRuleParams} goto ${nftPrefix}_mark_${mark}" || s=1
					if [ -n "$ipv6_enabled" ]; then
						try nft add rule inet "$nftTable" "${nftPrefix}_prerouting ${nftIPv6Flag} dscp ${dscp} ${nftRuleParams} goto ${nftPrefix}_mark_${mark}" || s=1
					fi
				fi
				if [ "$iface" = "$icmp_interface" ]; then
					try nft add rule inet "$nftTable" "${nftPrefix}_output ${nftIPv4Flag} protocol icmp ${nftRuleParams} goto ${nftPrefix}_mark_${mark}" || s=1
					if [ -n "$ipv6_enabled" ]; then
						try nft add rule inet "$nftTable" "${nftPrefix}_output ${nftIPv6Flag} protocol icmp ${nftRuleParams} goto ${nftPrefix}_mark_${mark}" || s=1
					fi
				fi
			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
			sed -i "/${ipTablePrefix}_${iface}\$/d" "$rtTablesFile"
			sync
			return "$s"
		;;
		reload_interface)
			is_netifd_interface "$iface" && return 0
			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 "$ipv6_enabled" ]; then
				ip -6 rule flush table "$tid" >/dev/null 2>&1
				ip -6 route flush table "$tid" >/dev/null 2>&1
			fi
			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 "$dev" table "$tid" || ipv4_error=1
				fi
				# try ip -4 rule replace fwmark "${mark}/${fw_mask}" lookup 'main' suppress_prefixlength 0 priority "$((priority - 1000))" || ipv4_error=1
				{
					for prio in $(ip -4 rule show | awk '/lookup main/ && /suppress_prefixlength 0/ {gsub(":", "", $1); print $1}'); do
						rule="$(ip -4 rule show | awk -v p="$prio" '($1==p":"){ $1=""; sub(/^ /,""); print }')"
						[ -n "$rule" ] || continue
						rule="${rule/lookup main/lookup $tid}"
						ip -4 rule replace priority "$prio" $rule >/dev/null 2>&1 || ipv4_error=1
					done
				}
				try ip -4 rule replace fwmark "${mark}/${fw_mask}" table "$tid" priority "$priority" || ipv4_error=1
			fi
			if [ -n "$ipv6_enabled" ]; then
				ipv6_error=0
				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}" lookup 'main' suppress_prefixlength 0 priority "$((priority - 1000))" || ipv6_error=1
					{
						for prio in $(ip -6 rule show | awk '/lookup main/ && /suppress_prefixlength 0/ {gsub(":", "", $1); print $1}'); do
							rule="$(ip -6 rule show | awk -v p="$prio" '($1==p":"){ $1=""; sub(/^ /,""); print }')"
							[ -n "$rule" ] || continue
							rule="${rule/lookup main/lookup $tid}"
							ip -6 rule replace priority "$prio" $rule >/dev/null 2>&1 || ipv6_error=1
						done
					}
					try ip -6 rule replace fwmark "${mark}/${fw_mask}" table "$tid" priority "$priority" || ipv6_error=1
				fi
			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 dev dev6 s=0 dscp iface="$1" action="$2" reloadedIface="$3"
	local displayText dispDev dispGw4 dispGw6 dispStatus

	if [ "$iface" = 'all' ] && [ "$action" = 'prepare' ]; then
		config_load 'network'
		ifaceMark="$(printf '0x%06x' "$uplink_mark")"
		ifacePriority="$uplink_ip_rules_priority"
		unset ifaceTableID
		return 0
	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
		local disabled listen_port
		disabled="$(uci_get 'network' "$iface" 'disabled')"
		listen_port="$(uci_get 'network' "$iface" 'listen_port')"
		case "$action" in
			create|reload|reload_interface)
				if [ "$disabled" != '1' ] && [ -n "$listen_port" ]; then
					if [ -n "$wanIface4" ]; then
						ip rule del sport "$listen_port" table "pbr_${wanIface4}" >/dev/null 2>&1
						ip rule add sport "$listen_port" table "pbr_${wanIface4}" >/dev/null 2>&1
					fi
					if [ -n "$ipv6_enabled" ] && [ -n "$wanIface6" ]; then
						ip -6 rule del sport "$listen_port" table "pbr_${wanIface4}" >/dev/null 2>&1
						ip -6 rule add sport "$listen_port" table "pbr_${wanIface4}" >/dev/null 2>&1
					fi
				fi
			;;
			destroy)
				if [ -n "$listen_port" ]; then
					ip rule del sport "$listen_port" table "pbr_${wanIface4}" >/dev/null 2>&1
					ip -6 rule del sport "$listen_port" table "pbr_${wanIface4}" >/dev/null 2>&1
				fi
			;;
		esac
		str_contains_word "$supported_interface" "$iface" || return 0
	fi

	is_supported_interface "$iface" || return 0
	is_wan6 "$iface" && return 0
	[ "$((ifaceMark))" -gt "$((fw_mask))" ] && return 1

	if is_ovpn "$iface" && ! is_ovpn_valid "$iface"; then
		: || json add warning 'warningInvalidOVPNConfig' "$iface"
	fi

	network_get_device dev "$iface"
	[ -z "$dev" ] && network_get_physdev dev "$iface"
	if is_wan "$iface" && [ -n "$wanIface6" ] && str_contains "$wanIface6" "$iface"; then
		network_get_device dev6 "$wanIface6"
		[ -z "$dev6" ] && network_get_physdev dev6 "$wanIface6"
	fi

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

	case "$action" in
		pre_init)
			[ -z "$ifaceTableID" ] && ifaceTableID="$(get_rt_tables_non_pbr_next_id)"
			eval "pre_init_mark_${iface//-/_}"='$ifaceMark'
			eval "pre_init_priority_${iface//-/_}"='$ifacePriority'
			eval "pre_init_tid_${iface//-/_}"='$ifaceTableID'
			ifaceMark="$(printf '0x%06x' $((ifaceMark + uplink_mark)))"
			ifacePriority="$((ifacePriority - 1))"
			ifaceTableID="$((ifaceTableID + 1))"
			return 0
		;;
		create)
			ifaceTableID="$(get_rt_tables_id "$iface")"
			[ -z "$ifaceTableID" ] && ifaceTableID="$(get_rt_tables_next_id)"
			eval "mark_${iface//-/_}"='$ifaceMark'
			eval "tid_${iface//-/_}"='$ifaceTableID'
			pbr_get_gateway4 gw4 "$iface" "$dev"
			pbr_get_gateway6 gw6 "$iface" "$dev6"
			dispGw4="${gw4:-0.0.0.0}"
			dispGw6="${gw6:-::/0}"
			[ "$iface" != "$dev" ] && dispDev="$dev"
			if is_default_dev "$dev"; 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' "$ifaceTableID" "$ifaceMark" "$iface" "$gw4" "$dev" "$gw6" "$dev6" "$ifacePriority"; then
				json_add_gateway 'create' "$ifaceTableID" "$ifaceMark" "$iface" "$gw4" "$dev" "$gw6" "$dev6" "$ifacePriority" "$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)
			ifaceTableID="$(get_rt_tables_id "$iface")"
			[ -z "$ifaceTableID" ] && ifaceTableID="$(get_rt_tables_next_id)"
			eval "mark_${iface//-/_}"='$ifaceMark'
			eval "tid_${iface//-/_}"='$ifaceTableID'
			pbr_get_gateway4 gw4 "$iface" "$dev"
			pbr_get_gateway6 gw6 "$iface" "$dev6"
			dispGw4="${gw4:-0.0.0.0}"
			dispGw6="${gw6:-::/0}"
			[ "$iface" != "$dev" ] && dispDev="$dev"
			if is_default_dev "$dev"; 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}"
			interface_routing 'create_user_set' "$ifaceTableID" "$ifaceMark" "$iface" "$gw4" "$dev" "$gw6" "$dev6" "$ifacePriority"
		;;
		destroy)
			ifaceTableID="$(get_rt_tables_id "$iface")"
			[ -z "$ifaceTableID" ] && ifaceTableID="$(get_rt_tables_next_id)"
			eval "mark_${iface//-/_}"='$ifaceMark'
			eval "tid_${iface//-/_}"='$ifaceTableID'
			pbr_get_gateway4 gw4 "$iface" "$dev"
			pbr_get_gateway6 gw6 "$iface" "$dev6"
			dispGw4="${gw4:-0.0.0.0}"
			dispGw6="${gw6:-::/0}"
			[ "$iface" != "$dev" ] && dispDev="$dev"
			if is_default_dev "$dev"; 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}"
			displayText="${iface}/${dispDev:+$dispDev/}${dispGw4}${ipv6_enabled:+/$dispGw6}"
			output 2 "Removing routing for '$displayText' "
			#interface_routing 'destroy' "${ifaceTableID}" "${ifaceMark}" "${iface}"
			interface_routing 'destroy' "$ifaceTableID" "$ifaceMark" "$iface" "$gw4" "$dev" "$gw6" "$dev6" "$ifacePriority"
			if is_netifd_interface "$iface"; then output_okb; else output_ok; fi
		;;
		reload)
			ifaceTableID="$(get_rt_tables_id "$iface")"
			[ -z "$ifaceTableID" ] && ifaceTableID="$(get_rt_tables_next_id)"
			eval "mark_${iface//-/_}"='$ifaceMark'
			eval "tid_${iface//-/_}"='$ifaceTableID'
			pbr_get_gateway4 gw4 "$iface" "$dev"
			pbr_get_gateway6 gw6 "$iface" "$dev6"
			dispGw4="${gw4:-0.0.0.0}"
			dispGw6="${gw6:-::/0}"
			[ "$iface" != "$dev" ] && dispDev="$dev"
			if is_default_dev "$dev"; 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)
			ifaceTableID="$(get_rt_tables_id "$iface")"
			[ -z "$ifaceTableID" ] && ifaceTableID="$(get_rt_tables_next_id)"
			eval "mark_${iface//-/_}"='$ifaceMark'
			eval "tid_${iface//-/_}"='$ifaceTableID'
			pbr_get_gateway4 gw4 "$iface" "$dev"
			pbr_get_gateway6 gw6 "$iface" "$dev6"
			dispGw4="${gw4:-0.0.0.0}"
			dispGw6="${gw6:-::/0}"
			[ "$iface" != "$dev" ] && dispDev="$dev"
			if is_default_dev "$dev"; 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' "$ifaceTableID" "$ifaceMark" "$iface" "$gw4" "$dev" "$gw6" "$dev6" "$ifacePriority"; then
					json_add_gateway 'reload_interface' "$ifaceTableID" "$ifaceMark" "$iface" "$gw4" "$dev" "$gw6" "$dev6" "$ifacePriority" "$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' "$ifaceTableID" "$ifaceMark" "$iface" "$gw4" "$dev" "$gw6" "$dev6" "$ifacePriority" "$dispStatus"
				gatewaySummary="${gatewaySummary}${displayText}${dispStatus:+ $dispStatus}\n"
			fi
		;;
	esac
	ifaceMark="$(printf '0x%06x' $((ifaceMark + uplink_mark)))"
	ifacePriority="$((ifacePriority - 1))"
	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'
	rc_procd start_service 'on_boot' && service_started 'on_boot'
}

on_interface_reload() {
	if [ ! -e "$packageLockFile" ]; 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}) "
	is_wan_up "$param" || { output_error "$(get_text 'errorUplinkDown')";  return 1; }

	process_interface 'all' 'prepare'
	config_foreach process_interface 'interface' 'pre_init'

	case "$param" in
		on_boot)
			serviceStartTrigger='on_start'
		;;
		on_firewall_reload)
			serviceStartTrigger='on_start'
		;;
		on_interface_reload)
			reloadedIface="$2"
			local tid pre_init_tid
			tid="$(get_rt_tables_id "$reloadedIface")"
			pre_init_tid="$(eval echo "\$pre_init_tid_${reloadedIface//-/_}")"
			if [ "$tid" = "$pre_init_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' 'prepare'
			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
#			cleanup_main_chains
#			cleanup_sets
#			cleanup_marking_chains
			cleanup_rt_tables
			nft_file 'create'
			output_okn
			output 1 'Processing interfaces '
			json_add_array 'gateways'
			process_interface 'all' 'prepare'
			config_foreach process_interface 'interface' 'create'
			process_interface 'tor' 'destroy'
			is_tor_running && process_interface 'tor' 'create'
			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' 'prepare'
				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'
			resolver 'compare_hash' && resolver 'restart'
		;;
	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() { procd_set_config_changed firewall; }
service_started() {
	[ -n "$pbrBootFlag" ] && return 0
	local error warning c
	if nft_file 'exists'; then
		procd_set_config_changed firewall
		[ -n "$gatewaySummary" ] && output "$serviceName (fw4 nft file mode) started with gateways:\n${gatewaySummary}"
	else
		output "$serviceName FAILED TO START in fw4 nft file mode!!!\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 '#WarningMessagesDetails')")"
	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 '#ErrorMessagesDetails')")"
	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
	[ "$1" = 'quiet' ] && quiet_mode 'on'
	load_environment 'on_stop'
	if nft_file 'exists'; then
		nft_file_mode=1
	fi
	output 'Resetting chains and sets '
#	if nft_file 'delete' && cleanup_main_table && cleanup_main_chains && cleanup_sets && cleanup_marking_chains; then
	if nft_file 'delete'; then
		output_okn
	else
		output_failn
	fi
	output 1 'Resetting interfaces '
	config_load 'network'
	config_foreach process_interface 'interface' 'destroy'
	process_interface 'tor' 'destroy'
	cleanup_rt_tables
	output 1 "\n"
	ip route flush cache
	unset ifaceMark
	unset ifaceTableID
	resolver 'store_hash'
	resolver 'cleanup'
	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
	rm -f "$packageLockFile"
}

version() { echo "$PKG_VERSION"; }

status_service() {
	local i dev dev6 wanTID ipv6_enabled

	[ "$(uci_get "$packageName" config ipv6_enabled)" = "1" ] && ipv6_enabled='true'

	json_load "$(ubus call system board)"; json_select release; json_get_var dist distribution; json_get_var vers version
	if [ -n "$wanIface4" ]; then
		network_get_gateway wanGW4 "$wanIface4"
		network_get_device dev "$wanIface4"
	fi
	if [ -n "$wanIface6" ]; then
		network_get_device dev6 "$wanIface6"
		wanGW6="$(ip -6 route show | grep -m1 " dev $dev6 " | awk '{print $1}')"
		[ "$wanGW6" = "default" ] && wanGW6="$(ip -6 route show | grep -m1 " dev $dev6 " | awk '{print $3}')"
	fi
	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 installed on $dist $vers."
	[ -n "$wanIface4" ] && status="$status WAN (IPv4): ${wanIface4}/${dev}/${wanGW4:-0.0.0.0}."
	[ -n "$wanIface6" ] && status="$status WAN (IPv6): ${wanIface6}/${dev6}/${wanGW6:-::/0}."

	echo "$_SEPARATOR_"
	echo "$packageName - environment"
	echo "$status"
	echo "$_SEPARATOR_"
	dnsmasq --version 2>/dev/null | sed '/^$/,$d'
	if nft_file 'netifd_exists'; then
		echo "$_SEPARATOR_"
		echo "$packageName fw4 netifd nft file: $nftNetifdPermFile"
		sed '1d;2d;' "$nftNetifdPermFile"
	fi
	if nft_file 'exists'; then
		echo "$_SEPARATOR_"
		echo "$packageName fw4 nft file: $nftPermFile"
		sed '1d;2d;' "$nftPermFile"
	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
}

# 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:uinteger: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' \
		'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" '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' \
	;
}
