# banIP shared function library/include - ban incoming and outgoing IPs via named nftables Sets
# Copyright (c) 2018-2026 Dirk Brenken (dev@brenken.org)
# This is free software, licensed under the GNU General Public License v3.

# (s)hellcheck exceptions
# shellcheck disable=all

# environment
#
export LC_ALL=C
export PATH="/usr/sbin:/usr/bin:/sbin:/bin"

# initial defaults
#
ban_basedir="/tmp"
ban_backupdir="/tmp/banIP-backup"
ban_reportdir="/tmp/banIP-report"
ban_errordir="/tmp/banIP-error"
ban_rundir="/var/run/banIP"
ban_feedfile="/etc/banip/banip.feeds"
ban_countryfile="/etc/banip/banip.countries"
ban_customfeedfile="/etc/banip/banip.custom.feeds"
ban_allowlist="/etc/banip/banip.allowlist"
ban_blocklist="/etc/banip/banip.blocklist"
ban_mailtemplate="/etc/banip/banip.tpl"
ban_pidfile="${ban_rundir}/banIP.pid"
ban_rtfile="${ban_rundir}/banIP.runtime.json"
ban_rdapfile="${ban_rundir}/banIP.rdap.json"
ban_lock="${ban_rundir}/banIP.lock"
ban_etaglock="${ban_rundir}/banIP.etag.lock"
ban_deduplock="${ban_rundir}/banIP.deduplicate.lock"
ban_rdapurl="https://rdap.db.ripe.net/ip/"
ban_geourl="http://ip-api.com/batch"
ban_errorlog="/dev/null"
ban_logreadfile=""
ban_logreadcmd=""
ban_mailsender="no-reply@banIP"
ban_mailreceiver=""
ban_mailtopic="banIP notification"
ban_mailprofile="ban_notify"
ban_mailnotification="0"
ban_remotelog="0"
ban_remotetoken=""
ban_nftloglevel="warn"
ban_nftpriority="-100"
ban_nftpolicy="memory"
ban_nftexpiry=""
ban_nftretry="3"
ban_nftcount="0"
ban_map="0"
ban_bcp38="0"
ban_icmplimit="25"
ban_synlimit="10"
ban_udplimit="100"
ban_loglimit="100"
ban_logratelimit="10"
ban_logburstlimit="5"
ban_logcount="1"
ban_logterm=""
ban_region=""
ban_country=""
ban_countrysplit="0"
ban_asn=""
ban_asnsplit="0"
ban_logprerouting="0"
ban_loginbound="0"
ban_logoutbound="0"
ban_allowurl=""
ban_allowflag=""
ban_allowlistonly="0"
ban_autoallowlist="1"
ban_autoallowuplink="subnet"
ban_autoblocklist="1"
ban_autoblocksubnet="0"
ban_deduplicate="1"
ban_splitsize="0"
ban_autodetect="1"
ban_feed=""
ban_feedin=""
ban_feedout=""
ban_feedinout=""
ban_feedcomplete=""
ban_feedreset=""
ban_blockpolicy="drop"
ban_protov4="0"
ban_protov6="0"
ban_ifv4=""
ban_ifv6=""
ban_dev=""
ban_vlanallow=""
ban_vlanblock=""
ban_uplink=""
ban_fetchcmd=""
ban_fetchparm=""
ban_fetchinsecure=""
ban_fetchretry="5"
ban_rdapparm=""
ban_etagparm=""
ban_geoparm=""
ban_cores=""
ban_packages=""
ban_trigger=""
ban_resolver=""
ban_enabled="0"
ban_debug="0"

# gather system information
#
f_system() {
	local cpu

	ban_debug="$(uci_get banip global ban_debug "0")"
	ban_cores="$(uci_get banip global ban_cores)"
	ban_basedir="$(uci_get banip global ban_basedir "/tmp")"

	# set debug log file
	#
	if [ "${ban_debug}" = "1" ] && [ -d "${ban_basedir}" ]; then
		ban_errorlog="${ban_basedir}/ban_error.log"
	else
		ban_errorlog="/dev/null"
	fi

	# create runtime directory
	#
	f_mkdir "${ban_rundir}"

	# get banIP version and system information
	#
	ban_packages="$("${ban_ubuscmd}" -S call rpc-sys packagelist '{ "all": true }' 2>>"${ban_errorlog}")"
	ban_bver="$(printf '%s' "${ban_packages}" | "${ban_jsoncmd}" -ql1 -e '@.packages.banip')"
	ban_fver="$(printf '%s' "${ban_packages}" | "${ban_jsoncmd}" -ql1 -e '@.packages["luci-app-banip"]')"
	ban_sysver="$("${ban_ubuscmd}" -S call system board 2>>"${ban_errorlog}" | "${ban_jsoncmd}" -ql1 -e '@.model' -e '@.release.target' -e '@.release.distribution' -e '@.release.version' -e '@.release.revision' |
		"${ban_awkcmd}" 'BEGIN{RS="";FS="\n"}{printf "%s, %s, %s %s (%s)",$1,$2,$3,$4,$5}')"

	if [ -z "${ban_cores}" ]; then
		cpu="$("${ban_grepcmd}" -cm16 '^processor' /proc/cpuinfo 2>>"${ban_errorlog}")"
		[ "${cpu}" = "0" ] && cpu="1"
		ban_cores="${cpu}"
	fi
}

# command selector
#
f_cmd() {
	local cmd pri_cmd="${1}" sec_cmd="${2}"

	# check primary command,
	# if not found check secondary command if provided, otherwise log error
	#
	cmd="$(command -v "${pri_cmd}" 2>>"${ban_errorlog}")"
	if [ ! -x "${cmd}" ]; then
		if [ -n "${sec_cmd}" ]; then
			[ "${sec_cmd}" = "optional" ] && return
			cmd="$(command -v "${sec_cmd}" 2>>"${ban_errorlog}")"
		fi
		if [ -x "${cmd}" ]; then
			printf '%s' "${cmd}"
		else
			f_log "emerg" "command '${pri_cmd:-"-"}'/'${sec_cmd:-"-"}' not found"
		fi
	else
		printf '%s' "${cmd}"
	fi
}

# create directories
#
f_mkdir() {
	local dir="${1}"

	if [ ! -d "${dir}" ]; then
		"${ban_rmcmd}" -f "${dir}"
		mkdir -p "${dir}"
		f_log "debug" "f_mkdir     ::: directory: ${dir}"
	fi
}

# create files
#
f_mkfile() {
	local file="${1}"

	if [ ! -f "${file}" ]; then
		: >"${file}"
		f_log "debug" "f_mkfile    ::: file: ${file}"
	fi
}

# create temporary files and directories
#
f_tmp() {
	f_mkdir "${ban_basedir}"
	ban_tmpdir="$(mktemp -p "${ban_basedir}" -d)"
	ban_tmpfile="$(mktemp -p "${ban_tmpdir}" -tu)"
	[ "${ban_debug}" = "1" ] && : >"${ban_errorlog}"

	f_log "debug" "f_tmp     ::: base_dir: ${ban_basedir:-"-"}, tmp_dir: ${ban_tmpdir:-"-"}"
}

# remove directories
#
f_rmdir() {
	local dir="${1}"

	if [ -d "${dir}" ]; then
		"${ban_rmcmd}" -rf "${dir}"
		f_log "debug" "f_rmdir   ::: directory: ${dir}"
	fi
}

# trim strings
#
f_trim() {
	local string="${1}"

	string="${string#"${string%%[![:space:]]*}"}"
	string="${string%"${string##*[![:space:]]}"}"
	printf '%s' "${string}"
}

# remove log monitor
#
f_rmpid() {
	local ppid pid pids_next pids_all childs newchilds

	# kill all descendant processes of the pid in pidfile
	#
	ppid="$("${ban_catcmd}" "${ban_pidfile}" 2>>"${ban_errorlog}")"
	if [ -n "${ppid}" ]; then
		pids_next="$("${ban_pgrepcmd}" -P "${ppid}" 2>>"${ban_errorlog}")"
		pids_all=""
		while [ -n "${pids_next}" ]; do
			for pid in ${pids_next}; do
				case " ${pids_all} " in
				*" ${pid} "*) ;;

				*)
					pids_all="${pids_all} ${pid}"
					;;
				esac
			done
			newchilds=""
			for pid in ${pids_next}; do
				childs="$("${ban_pgrepcmd}" -P "${pid}" 2>>"${ban_errorlog}")"
				[ -n "${childs}" ] && newchilds="${newchilds} ${childs}"
			done
			pids_next="$(f_trim "${newchilds}")"
		done
		for pid in ${pids_all}; do
			kill -INT "${pid}" >/dev/null 2>&1
		done
	fi
	: >"${ban_pidfile}"
}

# write log messages
#
f_log() {
	local class="${1}" log_msg="${2}"

	if [ -n "${log_msg}" ] && { [ "${class}" != "debug" ] || [ "${ban_debug}" = "1" ]; }; then
		if [ -x "${ban_logcmd}" ]; then
			"${ban_logcmd}" -p "${class}" -t "banIP-${ban_bver}[${$}]" "${log_msg::512}"
		else
			printf '%s %s %s\n' "${class}" "banIP-${ban_bver}[${$}]" "${log_msg::512}"
		fi
	fi
	if [ "${class}" = "err" ] || [ "${class}" = "emerg" ]; then
		if [ "${class}" = "err" ]; then
			"${ban_nftcmd}" delete table inet banIP >/dev/null 2>&1
			if [ "$(uci_get banip global ban_enabled)" = "1" ]; then
				f_genstatus "error"
				[ "${ban_mailnotification}" = "1" ] && [ -n "${ban_mailreceiver}" ] && [ -x "${ban_mailcmd}" ] && f_mail
			else
				f_genstatus "disabled"
			fi
		fi
		f_rmdir "${ban_tmpdir}"
		f_rmpid
		"${ban_rmcmd}" -rf "${ban_lock}"
		exit 1
	fi
}

# load config
#
f_conf() {
	local rir ccode region country

	config_cb() {
		option_cb() {
			local option="${1}" value="${2//\"/\\\"}"

			case "${option}" in
			*[!a-zA-Z0-9_]*) ;;

			*)
				eval "${option}=\"\${value}\""
				;;
			esac
		}
		list_cb() {
			local append option="${1}" value="${2//\"/\\\"}"

			case "${option}" in
			*[!a-zA-Z0-9_]*) ;;

			"ban_logterm")
				eval "append=\"\${${option}}\""
				if [ -n "${append}" ]; then
					eval "${option}=\"\${append}\\|\${value}\""
				else
					eval "${option}=\"\${value}\""
				fi
				;;
			*)
				eval "append=\"\${${option}}\""
				eval "${option}=\"\${append}\${value} \""
				;;
			esac
		}
	}
	config_load banip

	if [ -f "${ban_logreadfile}" ]; then
		ban_logreadcmd="$(command -v tail)"
	else
		ban_logreadcmd="$(command -v logread)"
	fi

	for rir in ${ban_region}; do
		while read -r ccode region country; do
			if [ "${rir}" = "${region}" ]; then
				case " ${ban_country} " in
				*" ${ccode} "*) ;;

				*)
					ban_country="${ban_country} ${ccode}"
					;;
				esac
			fi
		done <"${ban_countryfile}"
	done
}

# IPv4/IPv6 validation
#
f_chkip() {
	local ipv type prefix separator col1 col2

	ipv="${1}"
	type="${2}"
	case "${type}" in
	"feed" | "local")
		case "${3}" in
		[0-9][0-9])
			prefix=""
			col1="${3:0:1}"
			col2="${3:1:1}"
			separator="${4:-[[:space:]]+}"
			;;
		[0-9])
			prefix=""
			col1="${3}"
			col2=""
			separator="${4:-[[:space:]]+}"
			;;
		*)
			prefix="${3}"
			col1="${4}"
			col2=""
			separator="${5:-[[:space:]]+}"
			;;
		esac
		;;
	"suricata")
		prefix=""
		col1="${3}"
		col2=""
		separator="${4:-[[:space:]]+}"
		;;
	esac
	"${ban_awkcmd}" -v ipv="${ipv}" -v type="${type}" -v pre="${prefix}" -v col1="${col1}" -v col2="${col2}" -F "${separator}" '
	{
		# suricata pre-processing
		if (type == "suricata") {
			delete M
			if (ipv == "4") {
				match($0, /content:"(([0-9]{1,3}\.){3}[0-9]{1,3})"/, M)
			} else if (ipv == "6") {
				match($0, /content:"(([A-Fa-f0-9]{0,4}:){2,7}[A-Fa-f0-9]{0,4})"/, M)
			}
			if (M[1] == "") next
			$col1 = M[1]
		}
		ip = $col1
		gsub(/\r|^[[:space:]]+|[[:space:]]+$/, "", ip)
		# prefix filter
		if (pre != "" && index($0, pre) != 1) next
		# skip empty lines or comments
		if (ip == "" || ip ~ /^#/) next
		# reject invalid lengths
		len = length(ip)
		if (len < 3 || len > 43) next
		# reject MAC addresses when ipv=6
		if (ipv == "6" && ip ~ /^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/) next
		# reject IPv4 when ipv=6
		if (ipv == "6" && ip ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) next
		# reject IPv4-mapped IPv6 addresses
		if (ipv == "6" && tolower(ip) ~ /^::ffff:/) next
		# reject IPv6 when ipv=4
		if (ipv == "4" && ip ~ /:/) next
		# apply mask
		if (col2 != "") {
			mask = $col2
			lowip = (ipv == "4") ? ip "/" mask : tolower(ip "/" mask)
		} else {
			lowip = (ipv == "4") ? ip : tolower(ip)
		}
		# CIDR check
		if (lowip ~ /\//) {
			if (split(lowip, C, "/") != 2) next
			base = C[1]
			mask = C[2]
			if (mask !~ /^[0-9]+$/) next
			# IPv4 CIDR
			if (ipv == "4") {
				if (base ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) {
					if (mask > 32) next
					n = split(base, A, ".")
					# reject loopback and unspecified addresses
					if (A[1] == 127 || base == "0.0.0.0") next
					# reject leading zeros and octets > 255
					for (i=1; i<=4; i++) {
						if (length(A[i]) > 1 && substr(A[i], 1, 1) == "0") next
						if (A[i] > 255) next
					}
					print lowip ", "
					next
				}
			}
			# IPv6 CIDR
			if (ipv == "6") {
				if (base ~ /^[0-9a-f:]+$/ && base ~ /:/) {
					if (mask > 128) next
					if (base == "::1" || base == "::") next
					if (base ~ /^fe80:/) next
					print lowip ", "
					next
				}
			}
		}
		# IPv4 check
		if (ipv == "4") {
			if (lowip ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) {
				n = split(lowip, A, ".")
				# reject loopback and unspecified addresses
				if (A[1] == 127 || lowip == "0.0.0.0") next
				# reject leading zeros and octets > 255
				for (i=1; i<=4; i++) {
					if (length(A[i]) > 1 && substr(A[i], 1, 1) == "0") next
					if (A[i] > 255) next
				}
				print lowip ", "
				next
			}
		}
		# IPv6 check
		if (ipv == "6") {
			if (lowip ~ /^[0-9a-f:]+$/ && lowip ~ /:/) {
				# reject loopback and unspecified addresses
				if (lowip == "::1" || lowip == "::") next
				# reject link-local addresses
				if (lowip ~ /^fe80:/) next
				print lowip ", "
				next
			}
		}
	}'

	f_log "debug" "f_chkip   ::: feed: ${feed}, ipver: ${ipv}, type: ${type}, prefix: ${prefix:-"-"}, col1: ${col1:-"-"}, col2: ${col2:-"-"}, separator: ${separator:-"-"}"
}

# get nft/monitor actuals
#
f_actual() {
	local nft monitor ppid pids pid

	if "${ban_nftcmd}" -t list table inet banIP >/dev/null 2>&1; then
		nft="✔"
	else
		nft="✘"
	fi

	monitor="✘"
	ppid="$("${ban_catcmd}" "${ban_pidfile}" 2>>"${ban_errorlog}")"
	if [ -n "${ppid}" ]; then
		pids="${ppid} $("${ban_pgrepcmd}" -P "${ppid}" 2>>"${ban_errorlog}")"
		for pid in ${pids}; do
			if "${ban_pgrepcmd}" -f "${ban_logreadcmd##*/}" -P "${pid}" >/dev/null 2>&1; then
				monitor="✔"
				break
			fi
		done
	fi
	printf '%s' "nft: ${nft}, monitor: ${monitor}"
}

# get fetch utility
#
f_getdl() {
	local fetch fetch_list insecure update="0"

	ban_fetchcmd="$(command -v "${ban_fetchcmd}")"
	if { [ "${ban_autodetect}" = "1" ] && [ -z "${ban_fetchcmd}" ]; } || [ ! -x "${ban_fetchcmd}" ]; then
		fetch_list="curl wget-ssl libustream-openssl libustream-wolfssl libustream-mbedtls"
		for fetch in ${fetch_list}; do
			case "${ban_packages}" in *"\"${fetch}\""*)
				case "${fetch}" in
				"wget-ssl")
					fetch="wget"
					;;
				"libustream-openssl" | "libustream-wolfssl" | "libustream-mbedtls")
					fetch="uclient-fetch"
					;;
				esac
				if [ -x "$(command -v "${fetch}")" ]; then
					update="1"
					ban_fetchcmd="$(command -v "${fetch}")"
					uci_set banip global ban_fetchcmd "${fetch}"
					uci_commit "banip"
					break
				fi
				;;
			esac
		done
	fi

	[ ! -x "${ban_fetchcmd}" ] && f_log "err" "download utility with SSL support not found, please set 'ban_fetchcmd' manually"
	case "${ban_fetchcmd##*/}" in
	"curl")
		[ "${ban_fetchinsecure}" = "1" ] && insecure="--insecure"
		ban_fetchparm="${ban_fetchparm:-"${insecure} --connect-timeout 20 --retry-delay 10 --retry $((ban_fetchretry - 1)) --retry-max-time $(((ban_fetchretry - 1) * 20)) --retry-all-errors --fail --silent --show-error --location -o"}"
		ban_rdapparm="--connect-timeout 5 --silent --location -o"
		ban_etagparm="--connect-timeout 5 --silent --location --head"
		ban_geoparm="--connect-timeout 5 --silent --location --data"
		;;
	"wget")
		[ "${ban_fetchinsecure}" = "1" ] && insecure="--no-check-certificate"
		ban_fetchparm="${ban_fetchparm:-"${insecure} --no-cache --no-cookies --timeout=20 --waitretry=10 --tries=${ban_fetchretry} --retry-connrefused -O"}"
		ban_rdapparm="--timeout=5 -O"
		ban_etagparm="--timeout=5 --spider --server-response"
		ban_geoparm="--timeout=5 --quiet -O- --post-data"
		;;
	"uclient-fetch")
		[ "${ban_fetchinsecure}" = "1" ] && insecure="--no-check-certificate"
		ban_fetchparm="${ban_fetchparm:-"${insecure} --timeout=20 -O"}"
		ban_rdapparm="--timeout=5 -O"
		ban_geoparm="--timeout=5 --quiet -O- --post-data"
		;;
	esac

	f_log "debug" "f_getdl   ::: auto/update: ${ban_autodetect}/${update}, cmd: ${ban_fetchcmd:-"-"}, parm: ${ban_fetchparm:-"-"}, rdapparm: ${ban_rdapparm:-"-"}, etagparm: ${ban_etagparm:-"-"}, geoparm: ${ban_geoparm:-"-"}"
}

# get wan interfaces
#
f_getif() {
	local iface iface_del update="0"

	if [ "${ban_autodetect}" = "1" ]; then
		network_flush_cache
		network_find_wan iface
		if [ -n "${iface}" ] && [ "${iface}" != "$(f_trim "${ban_ifv4}")" ] && "${ban_ubuscmd}" -t 10 wait_for network.interface."${iface}" >/dev/null 2>&1; then
			for iface_del in ${ban_ifv4}; do
				uci_remove_list banip global ban_ifv4 "${iface_del}"
				f_log "info" "remove IPv4 interface '${iface_del}' from config"
			done
			ban_protov4="1"
			ban_ifv4="${iface}"
			uci_set banip global ban_protov4 "1"
			uci_add_list banip global ban_ifv4 "${iface}"
			f_log "info" "add IPv4 interface '${iface}' to config"
		fi
		network_find_wan6 iface
		if [ -n "${iface}" ] && [ "${iface}" != "$(f_trim "${ban_ifv6}")" ] && "${ban_ubuscmd}" -t 10 wait_for network.interface."${iface}" >/dev/null 2>&1; then
			for iface_del in ${ban_ifv6}; do
				uci_remove_list banip global ban_ifv6 "${iface_del}"
				f_log "info" "remove IPv6 interface '${iface_del}' from config"
			done
			ban_protov6="1"
			ban_ifv6="${iface}"
			uci_set banip global ban_protov6 "1"
			uci_add_list banip global ban_ifv6 "${iface}"
			f_log "info" "add IPv6 interface '${iface}' to config"
		fi
	fi
	if [ -n "$(uci -q changes "banip")" ]; then
		update="1"
		uci_commit "banip"
	else
		for iface in ${ban_ifv4} ${ban_ifv6}; do
			if ! "${ban_ubuscmd}" -t 10 wait_for network.interface."${iface}" >/dev/null 2>&1; then
				f_log "err" "no wan interface '${iface}'"
			fi
		done
	fi
	ban_ifv4="$(f_trim "${ban_ifv4}")"
	ban_ifv6="$(f_trim "${ban_ifv6}")"
	[ -z "${ban_ifv4}" ] && [ -z "${ban_ifv6}" ] && f_log "err" "no wan interfaces"

	f_log "debug" "f_getif   ::: auto/update: ${ban_autodetect}/${update}, interfaces (4/6): ${ban_ifv4}/${ban_ifv6}, protocols (4/6): ${ban_protov4}/${ban_protov6}"
}

# get wan devices
#
f_getdev() {
	local dev dev_del iface update="0"

	if [ "${ban_autodetect}" = "1" ]; then
		network_flush_cache
		dev_del="${ban_dev}"
		for iface in ${ban_ifv4} ${ban_ifv6}; do
			network_get_device dev "${iface}"
			if [ -n "${dev}" ]; then
				dev_del="${dev_del/${dev} / }"
				case " ${ban_dev} " in
				*" ${dev} "*) ;;

				*)
					ban_dev="${ban_dev}${dev} "
					uci_add_list banip global ban_dev "${dev}"
					f_log "info" "add device '${dev}' to config"
					;;
				esac
			fi
		done
		for dev in ${dev_del}; do
			ban_dev="${ban_dev/${dev} / }"
			uci_remove_list banip global ban_dev "${dev}"
			f_log "info" "remove device '${dev}' from config"
		done
	fi
	if [ -n "$(uci -q changes "banip")" ]; then
		update="1"
		uci_commit "banip"
	fi
	ban_dev="$(f_trim "${ban_dev}")"
	[ -z "${ban_dev}" ] && f_log "err" "no wan devices"

	f_log "debug" "f_getdev  ::: auto/update: ${ban_autodetect}/${update}, wan_devices: ${ban_dev}"
}

# get local uplink
#
f_getup() {
	local uplink iface timestamp ip

	if [ "${ban_autoallowlist}" = "1" ] && [ "${ban_autoallowuplink}" != "disable" ]; then
		for iface in ${ban_ifv4} ${ban_ifv6}; do
			network_flush_cache
			if [ "${ban_autoallowuplink}" = "subnet" ]; then
				network_get_subnet uplink "${iface}"
			elif [ "${ban_autoallowuplink}" = "ip" ]; then
				network_get_ipaddr uplink "${iface}"
			fi
			if [ -n "${uplink}" ]; then
				case " ${ban_uplink} " in
				*" ${uplink} "*) ;;

				*)
					ban_uplink="${ban_uplink}${uplink} "
					;;
				esac
			fi
			if [ "${ban_autoallowuplink}" = "subnet" ]; then
				network_get_subnet6 uplink "${iface}"
			elif [ "${ban_autoallowuplink}" = "ip" ]; then
				network_get_ipaddr6 uplink "${iface}"
			fi
			if [ -n "${uplink%fe80::*}" ]; then
				case " ${ban_uplink} " in
				*" ${uplink} "*) ;;

				*)
					ban_uplink="${ban_uplink}${uplink} "
					;;
				esac
			fi
		done
		ban_uplink="$(f_trim "${ban_uplink}")"
		for ip in ${ban_uplink}; do
			if ! "${ban_grepcmd}" -q "${ip} " "${ban_allowlist}"; then
				"${ban_sedcmd}" -i "/# uplink added on /d" "${ban_allowlist}"
				break
			fi
		done
		timestamp="$(date "+%Y-%m-%d %H:%M:%S")"
		for ip in ${ban_uplink}; do
			if ! "${ban_grepcmd}" -q "${ip} " "${ban_allowlist}"; then
				printf '%-45s%s\n' "${ip}" "# uplink added on ${timestamp}" >>"${ban_allowlist}"
				f_log "info" "add uplink '${ip}' to local allowlist"
			fi
		done
	elif [ "${ban_autoallowlist}" = "1" ] && [ "${ban_autoallowuplink}" = "disable" ]; then
		"${ban_sedcmd}" -i "/# uplink added on /d" "${ban_allowlist}"
	fi

	f_log "debug" "f_getup   ::: auto-allow/auto-uplink: ${ban_autoallowlist}/${ban_autoallowuplink}, uplink: ${ban_uplink:-"-"}"
}

# get feed information
#
f_getfeed() {
	json_init
	if [ -s "${ban_customfeedfile}" ]; then
		if json_load_file "${ban_customfeedfile}" >/dev/null 2>&1; then
			return
		else
			f_log "info" "can't load banIP custom feed file"
		fi
	fi
	if [ -s "${ban_feedfile}" ] && json_load_file "${ban_feedfile}" >/dev/null 2>&1; then
		return
	else
		f_log "err" "can't load banIP feed file"
	fi
}

# get Set elements
#
f_getelements() {
	local file="${1}"

	[ -s "${file}" ] && printf '%s' "elements={ $("${ban_catcmd}" "${file}" 2>>"${ban_errorlog}") };"
}

# handle etag http header
#
f_etag() {
	local http_head http_code etag_id etag_cnt out_rc="4" feed="${1}" feed_url="${2}" feed_suffix="${3}" feed_cnt="${4:-"1"}"

	if [ -n "${ban_etagparm}" ]; then

		# ensure etag file exists
		#
		[ ! -f "${ban_backupdir}/banIP.etag" ] && : >"${ban_backupdir}/banIP.etag"

		# fetch http headers and extract http code and etag/last-modified header
		#
		http_head="$("${ban_fetchcmd}" ${ban_etagparm} "${feed_url}" 2>&1)"
		http_code="$(printf '%s' "${http_head}" | "${ban_awkcmd}" 'tolower($0)~/^http\/[0123\.]+ /{printf "%s",$2}')"
		etag_id="$(printf '%s' "${http_head}" | "${ban_awkcmd}" 'tolower($0)~/^[[:space:]]*etag: /{gsub("\"","");printf "%s",$2}')"

		# if etag header is not present, try to use last-modified header as fallback for change detection
		#
		if [ -z "${etag_id}" ]; then
			etag_id="$(printf '%s' "${http_head}" | "${ban_awkcmd}" 'tolower($0)~/^[[:space:]]*last-modified: /{gsub(/[Ll]ast-[Mm]odified:|[[:space:]]|,|:/,"");printf "%s\n",$1}')"
		fi

		# acquire exclusive lock on etag file to serialize concurrent read-modify-write from parallel feeds
		#
		exec 9>"${ban_etaglock}"
		"${ban_flockcmd}" -x 9

		# compare http code and etag id with stored values, update etag file and return code accordingly
		#
		etag_cnt="$("${ban_grepcmd}" -c "^${feed} " "${ban_backupdir}/banIP.etag")"
		if [ "${http_code}" = "200" ] && [ "${etag_cnt}" = "${feed_cnt}" ] && [ -n "${etag_id}" ] &&
			"${ban_grepcmd}" -q "^${feed} ${feed_suffix}[[:space:]]\+${etag_id}\$" "${ban_backupdir}/banIP.etag"; then
			out_rc="0"
		elif [ -n "${etag_id}" ]; then

			# if feed count is less than etag count, it means the feed source has been removed or disabled, so remove all entries for this feed,
			# otherwise only remove the entry with the matching feed suffix (feed url) to allow multiple sources for the same feed
			#
			if [ "${feed_cnt}" -lt "${etag_cnt}" ]; then
				"${ban_sedcmd}" -i "/^${feed} /d" "${ban_backupdir}/banIP.etag"
			else
				"${ban_sedcmd}" -i "/^${feed} ${feed_suffix//\//\\/}/d" "${ban_backupdir}/banIP.etag"
			fi
			printf '%-50s%s\n' "${feed} ${feed_suffix}" "${etag_id}" >>"${ban_backupdir}/banIP.etag"
			out_rc="2"
		fi

		# release lock
		#
		exec 9>&-
	fi

	f_log "debug" "f_etag    ::: feed: ${feed}, suffix: ${feed_suffix:-"-"}, http_code: ${http_code:-"-"}, feed/etag: ${feed_cnt}/${etag_cnt:-"0"}, rc: ${out_rc}"
	return "${out_rc}"
}

# load file in nftset
#
f_nftload() {
	local cnt="1" max_cnt="${ban_nftretry:-"3"}" load_rc="4" file="${1}" errmsg="${2}"

	while [ "${load_rc}" != "0" ]; do
		"${ban_nftcmd}" -f "${file}" >/dev/null 2>&1
		load_rc="${?}"
		if [ "${load_rc}" = "0" ]; then
			break
		elif [ "${cnt}" = "${max_cnt}" ]; then
			[ ! -d "${ban_errordir}" ] && f_mkdir "${ban_errordir}"
			"${ban_catcmd}" "${file}" 2>>"${ban_errorlog}" >"${ban_errordir}/err.${file##*/}"
			f_log "info" "${errmsg}"
			break
		fi
		cnt="$((cnt + 1))"
	done

	f_log "debug" "f_nftload ::: file: ${file##*/}, load_rc: ${load_rc}, cnt/max_cnt: ${cnt}/${max_cnt}"
	return "${load_rc}"
}

# build initial nft file with base table, chains and rules
#
f_nftinit() {
	local wan_dev vlan_allow vlan_block log_ct log_icmp log_syn log_udp log_tcp nft_cnt flag tmp_proto tmp_port allow_dport feed_rc="0" file="${1}"

	# format wan devices, allowed and blocked vlans as nft sets
	#
	wan_dev="$(printf '%s' "${ban_dev}" | "${ban_sedcmd}" 's/^/\"/;s/$/\"/;s/ /\", \"/g')"
	[ -n "${ban_vlanallow}" ] && vlan_allow="$(printf '%s' "${ban_vlanallow%%?}" | "${ban_sedcmd}" 's/^/\"/;s/$/\"/;s/ /\", \"/g')"
	[ -n "${ban_vlanblock}" ] && vlan_block="$(printf '%s' "${ban_vlanblock%%?}" | "${ban_sedcmd}" 's/^/\"/;s/$/\"/;s/ /\", \"/g')"

	# set nft counter flag
	#
	if [ "${ban_nftcount}" = "1" ]; then
		nft_cnt="counter"
	fi

	# format allowed protocols and ports
	#
	for flag in ${ban_allowflag}; do
		case "${flag}" in
		"tcp" | "udp")
			if [ -z "${tmp_proto}" ]; then
				tmp_proto="${flag}"
			else
				case ", ${tmp_proto}, " in
				*", ${flag}, "*) ;;

				*)
					tmp_proto="${tmp_proto}, ${flag}"
					;;
				esac
			fi
			;;
		"${flag//[![:digit:]-]/}")
			if [ -z "${tmp_port}" ]; then
				tmp_port="${flag}"
			else
				case ", ${tmp_port}, " in
				*", ${flag}, "*) ;;

				*)
					tmp_port="${tmp_port}, ${flag}"
					;;
				esac
			fi
			;;
		esac
	done
	if [ -n "${tmp_proto}" ] && [ -n "${tmp_port}" ]; then
		allow_dport="meta l4proto { ${tmp_proto} } th dport { ${tmp_port} }"
	fi

	# build log rules for pre-routing chains if enabled, with dynamic log level and prefix
	#
	if [ "${ban_logprerouting}" = "1" ]; then
		if [ "${ban_logratelimit}" = "0" ]; then
			log_icmp="log level ${ban_nftloglevel} prefix \"banIP/pre-icmp/drop: \""
			log_syn="log level ${ban_nftloglevel} prefix \"banIP/pre-syn/drop: \""
			log_udp="log level ${ban_nftloglevel} prefix \"banIP/pre-udp/drop: \""
			log_tcp="log level ${ban_nftloglevel} prefix \"banIP/pre-tcp/drop: \""
			log_ct="log level ${ban_nftloglevel} prefix \"banIP/pre-ct/drop: \""
		else
			log_icmp="limit name \"loglimit\" log level ${ban_nftloglevel} prefix \"banIP/pre-icmp/drop: \""
			log_syn="limit name \"loglimit\" log level ${ban_nftloglevel} prefix \"banIP/pre-syn/drop: \""
			log_udp="limit name \"loglimit\" log level ${ban_nftloglevel} prefix \"banIP/pre-udp/drop: \""
			log_tcp="limit name \"loglimit\" log level ${ban_nftloglevel} prefix \"banIP/pre-tcp/drop: \""
			log_ct="limit name \"loglimit\" log level ${ban_nftloglevel} prefix \"banIP/pre-ct/drop: \""
		fi
	fi

	{
		# nft header, optional table cleanup, base table, chains, counters and default rules
		#
		printf '%s\n' "#!${ban_nftcmd} -f" ""
		"${ban_nftcmd}" -t list table inet banIP >/dev/null 2>&1 && printf '%s\n' "delete table inet banIP"
		printf '%s\n' "add table inet banIP"
		[ "${ban_logratelimit}" != "0" ] && printf '%s\n' "add limit inet banIP loglimit { rate ${ban_logratelimit}/second burst ${ban_logburstlimit} packets }"
		printf '%s\n' \
			"add chain inet banIP pre-routing { type filter hook prerouting priority -175; policy accept; }" \
			"add chain inet banIP wan-input { type filter hook input priority ${ban_nftpriority}; policy accept; }" \
			"add chain inet banIP wan-forward { type filter hook forward priority ${ban_nftpriority}; policy accept; }" \
			"add chain inet banIP lan-forward { type filter hook forward priority ${ban_nftpriority}; policy accept; }" \
			"add chain inet banIP _inbound" \
			"add chain inet banIP _outbound" \
			"add chain inet banIP _reject" \
			"add counter inet banIP cnt_icmpflood" \
			"add counter inet banIP cnt_udpflood" \
			"add counter inet banIP cnt_synflood" \
			"add counter inet banIP cnt_tcpinvalid" \
			"add counter inet banIP cnt_ctinvalid" \
			"add counter inet banIP cnt_bcp38" \
			"add rule inet banIP _reject iifname != { ${wan_dev} } meta l4proto tcp reject with tcp reset" \
			"add rule inet banIP _reject reject with icmpx host-unreachable" \
			"add rule inet banIP pre-routing iifname != { ${wan_dev} } ${nft_cnt} accept"

		# ct state invalid
		#
		[ "${ban_logprerouting}" = "1" ] &&
			printf '%s\n' "add rule inet banIP pre-routing ct state invalid ${log_ct}"
		printf '%s\n' "add rule inet banIP pre-routing ct state invalid counter name cnt_ctinvalid drop"

		# ICMP Flood
		#
		if [ "${ban_icmplimit}" -gt "0" ]; then
			[ "${ban_logprerouting}" = "1" ] &&
				printf '%s\n' "add rule inet banIP pre-routing meta nfproto . meta l4proto { ipv4 . icmp , ipv6 . icmpv6 } limit rate over ${ban_icmplimit}/second ${log_icmp}"
			printf '%s\n' "add rule inet banIP pre-routing meta nfproto . meta l4proto { ipv4 . icmp , ipv6 . icmpv6 } limit rate over ${ban_icmplimit}/second counter name cnt_icmpflood drop"
		fi

		# UDP Flood
		#
		if [ "${ban_udplimit}" -gt "0" ]; then
			[ "${ban_logprerouting}" = "1" ] &&
				printf '%s\n' "add rule inet banIP pre-routing meta l4proto udp ct state new limit rate over ${ban_udplimit}/second ${log_udp}"
			printf '%s\n' "add rule inet banIP pre-routing meta l4proto udp ct state new limit rate over ${ban_udplimit}/second counter name cnt_udpflood drop"
		fi

		# SYN Flood
		#
		if [ "${ban_synlimit}" -gt "0" ]; then
			[ "${ban_logprerouting}" = "1" ] &&
				printf '%s\n' "add rule inet banIP pre-routing tcp flags & (fin|syn|rst|ack) == syn limit rate over ${ban_synlimit}/second ${log_syn}"
			printf '%s\n' "add rule inet banIP pre-routing tcp flags & (fin|syn|rst|ack) == syn limit rate over ${ban_synlimit}/second counter name cnt_synflood drop"
		fi

		# TCP Invalid
		#
		if [ "${ban_logprerouting}" = "1" ]; then
			printf '%s\n' \
				"add rule inet banIP pre-routing tcp flags & (fin|syn) == (fin|syn) ${log_tcp}" \
				"add rule inet banIP pre-routing tcp flags & (syn|rst) == (syn|rst) ${log_tcp}" \
				"add rule inet banIP pre-routing tcp flags & (fin|syn|rst|psh|ack|urg) < (fin) ${log_tcp}" \
				"add rule inet banIP pre-routing tcp flags & (fin|syn|rst|psh|ack|urg) == (fin|psh|urg) ${log_tcp}"
		fi
		printf '%s\n' \
			"add rule inet banIP pre-routing tcp flags & (fin|syn) == (fin|syn) counter name cnt_tcpinvalid drop" \
			"add rule inet banIP pre-routing tcp flags & (syn|rst) == (syn|rst) counter name cnt_tcpinvalid drop" \
			"add rule inet banIP pre-routing tcp flags & (fin|syn|rst|psh|ack|urg) < (fin) counter name cnt_tcpinvalid drop" \
			"add rule inet banIP pre-routing tcp flags & (fin|syn|rst|psh|ack|urg) == (fin|psh|urg) counter name cnt_tcpinvalid drop"

		# default wan-input rules
		#
		printf '%s\n' \
			"add rule inet banIP wan-input ct state established,related ${nft_cnt} accept" \
			"add rule inet banIP wan-input iifname != { ${wan_dev} } ${nft_cnt} accept" \
			"add rule inet banIP wan-input meta nfproto ipv4 udp sport 67-68 udp dport 67-68 ${nft_cnt} accept" \
			"add rule inet banIP wan-input meta nfproto ipv6 udp sport 547 udp dport 546 ${nft_cnt} accept" \
			"add rule inet banIP wan-input meta nfproto ipv6 icmpv6 type { nd-neighbor-solicit, nd-neighbor-advert, nd-router-advert } ip6 hoplimit 255 ${nft_cnt} accept"
		[ -n "${allow_dport}" ] &&
			printf '%s\n' "add rule inet banIP wan-input ${allow_dport} ${nft_cnt} accept"
		[ "${ban_bcp38}" = "1" ] &&
			printf '%s\n' "add rule inet banIP wan-input fib saddr . iif oif missing counter name cnt_bcp38 drop"
		if [ "${ban_loginbound}" = "1" ]; then
			printf '%s\n' "add rule inet banIP wan-input meta mark set 1 ${nft_cnt} jump _inbound"
		else
			printf '%s\n' "add rule inet banIP wan-input ${nft_cnt} jump _inbound"
		fi

		# default wan-forward rules
		#
		printf '%s\n' \
			"add rule inet banIP wan-forward iifname != { ${wan_dev} } ${nft_cnt} accept" \
			"add rule inet banIP wan-forward ct state established,related ${nft_cnt} accept"
		[ -n "${allow_dport}" ] &&
			printf '%s\n' "add rule inet banIP wan-forward ${allow_dport} ${nft_cnt} accept"
		[ "${ban_bcp38}" = "1" ] &&
			printf '%s\n' "add rule inet banIP wan-forward fib saddr . iif oif missing counter name cnt_bcp38 drop"
		if [ "${ban_loginbound}" = "1" ]; then
			printf '%s\n' "add rule inet banIP wan-forward meta mark set 2 ${nft_cnt} jump _inbound"
		else
			printf '%s\n' "add rule inet banIP wan-forward ${nft_cnt} jump _inbound"
		fi

		# default lan-forward rules
		#
		printf '%s\n' \
			"add rule inet banIP lan-forward ct state established,related ${nft_cnt} accept" \
			"add rule inet banIP lan-forward oifname != { ${wan_dev} } ${nft_cnt} accept"
		[ -n "${vlan_allow}" ] &&
			printf '%s\n' "add rule inet banIP lan-forward iifname { ${vlan_allow} } ${nft_cnt} accept"
		[ -n "${vlan_block}" ] &&
			printf '%s\n' "add rule inet banIP lan-forward iifname { ${vlan_block} } ${nft_cnt} goto _reject"
		[ "${ban_bcp38}" = "1" ] &&
			printf '%s\n' "add rule inet banIP lan-forward fib saddr . iif oif missing counter name cnt_bcp38 drop"
		printf '%s\n' "add rule inet banIP lan-forward ${nft_cnt} jump _outbound"
	} >"${file}"

	# load initial banIP table/rules to nftset
	#
	f_nftload "${file}" "can't initialize banIP nftables namespace"
	feed_rc="${?}"
	[ "${feed_rc}" = "0" ] && f_log "info" "initialize banIP nftables namespace"

	f_log "debug" "f_nftinit ::: wan_dev: ${wan_dev}, vlan_allow: ${vlan_allow:-"-"}, vlan_block: ${vlan_block:-"-"}, allowed_dports: ${allow_dport:-"-"}, priority: ${ban_nftpriority}, policy: ${ban_nftpolicy}, icmp_limit: ${ban_icmplimit}, syn_limit: ${ban_synlimit}, udp_limit: ${ban_udplimit}, loglevel: ${ban_nftloglevel}, rc: ${feed_rc:-"-"}"
	: >"${file}"
	return "${feed_rc}"
}

# handle downloads
#
f_down() {
	local log_inbound log_outbound start_ts end_ts tmp_raw tmp_load tmp_file split_file table_json handles handle etag_rc etag_cnt nft_cnt nft_setcnt tmp_allow feed_name
	local expr cnt_set cnt_dl restore_rc feed_direction feed_policy feed_rc feed_comp feed_complete feed_target feed_dport chain flag tmp_flush tmp_nft
	local tmp_split tmp_proto tmp_port asn country feed="${1}" feed_ipv="${2}" feed_url="${3}" feed_rule="${4}" feed_chain="${5}" feed_flag="${6}"

	# get feed start timestamp and prepare temporary file names based on feed name and type
	#
	read -r start_ts _ <"/proc/uptime"
	start_ts="${start_ts%%.*}"
	feed="${feed}.v${feed_ipv}"
	tmp_load="${ban_tmpfile}.${feed}.load"
	tmp_raw="${ban_tmpfile}.${feed}.raw"
	tmp_split="${ban_tmpfile}.${feed}.split"
	tmp_file="${ban_tmpfile}.${feed}.file"
	tmp_flush="${ban_tmpfile}.${feed}.flush"
	tmp_nft="${ban_tmpfile}.${feed}.nft"
	tmp_allow="${ban_tmpfile}.${feed%.*}"

	# set log target
	#
	if [ "${ban_logratelimit}" = "0" ]; then
		[ "${ban_loginbound}" = "1" ] && log_inbound="log level ${ban_nftloglevel} prefix \"banIP/inbound/${ban_blockpolicy}/${feed}: \""
		[ "${ban_logoutbound}" = "1" ] && log_outbound="log level ${ban_nftloglevel} prefix \"banIP/outbound/reject/${feed}: \""
	else
		[ "${ban_loginbound}" = "1" ] && log_inbound="limit name \"loglimit\" log level ${ban_nftloglevel} prefix \"banIP/inbound/${ban_blockpolicy}/${feed}: \""
		[ "${ban_logoutbound}" = "1" ] && log_outbound="limit name \"loglimit\" log level ${ban_nftloglevel} prefix \"banIP/outbound/reject/${feed}: \""
	fi

	# set feed target
	#
	if [ "${ban_blockpolicy}" = "reject" ]; then
		feed_target="goto _reject"
	else
		feed_target="drop"
	fi

	# set nft counter flag
	#
	if [ "${ban_nftcount}" = "1" ]; then
		nft_cnt="counter"
		nft_setcnt="; counter"
	fi

	# set feed complete flag
	#
	case " ${ban_feedcomplete} " in
	*" ${feed%%.*} "*)
		feed_complete="true"
		;;
	esac

	# set feed direction
	#
	feed_name="${feed%%.*}"
	if case " ${ban_feedin} " in
		*" ${feed_name} "*)
			true
			;;
		*)
			false
			;;
		esac; then
		feed_policy="in"
		feed_direction="inbound"
	elif case " ${ban_feedout} " in
		*" ${feed_name} "*)
			true
			;;
		*)
			false
			;;
		esac; then
		feed_policy="out"
		feed_direction="outbound"
	elif case " ${ban_feedinout} " in
		*" ${feed_name} "*)
			true
			;;
		*)
			false
			;;
		esac; then
		feed_policy="inout"
		feed_direction="inbound outbound"
	else
		feed_policy="${feed_chain}"
		case "${feed_chain}" in
		"in")
			feed_direction="inbound"
			;;
		"out")
			feed_direction="outbound"
			;;
		"inout")
			feed_direction="inbound outbound"
			;;
		*)
			feed_direction="inbound"
			;;
		esac
	fi

	# prepare feed flags
	#
	for flag in ${feed_flag}; do
		case "${flag}" in
		"gz")
			feed_comp="${flag}"
			;;
		"tcp" | "udp")
			if [ -z "${tmp_proto}" ]; then
				tmp_proto="${flag}"
			else
				case ", ${tmp_proto}, " in
				*", ${flag}, "*) ;;

				*)
					tmp_proto="${tmp_proto}, ${flag}"
					;;
				esac
			fi
			;;
		"${flag//[![:digit:]-]/}")
			if [ -z "${tmp_port}" ]; then
				tmp_port="${flag}"
			else
				case ", ${tmp_port}, " in
				*", ${flag}, "*) ;;

				*)
					tmp_port="${tmp_port}, ${flag}"
					;;
				esac
			fi
			;;
		esac
	done

	case " ${ban_feedreset} " in
	*" ${feed%%.*} "*) ;;

	*)
		if [ -n "${tmp_proto}" ] && [ -n "${tmp_port}" ]; then
			feed_dport="meta l4proto { ${tmp_proto} } th dport { ${tmp_port} }"
		fi
		;;
	esac

	# chain/rule maintenance
	#
	if [ "${ban_action}" = "reload" ] && "${ban_nftcmd}" -t list set inet banIP "${feed}" >/dev/null 2>&1; then
		table_json="$("${ban_nftcmd}" -tja list table inet banIP 2>>"${ban_errorlog}")"
		{
			for chain in _inbound _outbound; do
				for expr in 0 1 2; do
					handles="$(printf '%s\n' "${table_json}" | "${ban_jsoncmd}" -q -e "@.nftables[@.rule.chain=\"${chain}\"][@.expr[${expr}].match.right=\"@${feed}\"].handle" | "${ban_xargscmd}")"
					for handle in ${handles}; do
						printf '%s\n' "delete rule inet banIP ${chain} handle ${handle}"
					done
				done
			done
			printf '%s\n' "flush set inet banIP ${feed}"
			printf '%s\n\n' "delete set inet banIP ${feed}"
		} >"${tmp_flush}"
	fi

	# restore local backups
	#
	if [ "${feed%%.*}" != "blocklist" ]; then
		if [ -n "${ban_etagparm}" ] && [ "${ban_action}" = "reload" ] && [ "${feed_url}" != "local" ] && [ "${feed%%.*}" != "allowlist" ]; then
			etag_rc="0"
			case "${feed%%.*}" in
			"country")
				if [ "${ban_countrysplit}" = "1" ]; then
					country="${feed%.*}"
					country="${country#*.}"
					f_etag "${feed}" "${feed_url}${country}-aggregated.zone" ".${country}"
					etag_rc="${?}"
				else
					etag_rc="0"
					etag_cnt="$(printf '%s' "${ban_country}" | "${ban_wccmd}" -w)"
					for country in ${ban_country}; do
						if ! f_etag "${feed}" "${feed_url}${country}-aggregated.zone" ".${country}" "${etag_cnt}"; then
							etag_rc="$((etag_rc + 1))"
						fi
					done
				fi
				;;
			"asn")
				if [ "${ban_asnsplit}" = "1" ]; then
					asn="${feed%.*}"
					asn="${asn#*.}"
					f_etag "${feed}" "${feed_url}AS${asn}" ".${asn}"
					etag_rc="${?}"
				else
					etag_rc="0"
					etag_cnt="$(printf '%s' "${ban_asn}" | "${ban_wccmd}" -w)"
					for asn in ${ban_asn}; do
						if ! f_etag "${feed}" "${feed_url}AS${asn}" ".${asn}" "${etag_cnt}"; then
							etag_rc="$((etag_rc + 1))"
						fi
					done
				fi
				;;
			*)
				f_etag "${feed}" "${feed_url}"
				etag_rc="${?}"
				;;
			esac
		fi
		if [ "${etag_rc}" = "0" ] || [ "${ban_action}" != "reload" ] || [ "${feed_url}" = "local" ]; then
			if [ "${feed%%.*}" = "allowlist" ] && [ ! -f "${tmp_allow}" ]; then
				f_restore "allowlist" "-" "${tmp_allow}" "${etag_rc}"
				restore_rc="${?}"
			else
				f_restore "${feed}" "${feed_url}" "${tmp_load}" "${etag_rc}"
				restore_rc="${?}"
			fi
			feed_rc="${restore_rc}"
		fi
	fi

	# prepare local/remote allowlist
	#
	if [ "${feed%%.*}" = "allowlist" ] && [ ! -f "${tmp_allow}" ]; then
		"${ban_catcmd}" "${ban_allowlist}" 2>>"${ban_errorlog}" >"${tmp_allow}"
		feed_rc="${?}"
		for feed_url in ${ban_allowurl}; do
			if "${ban_fetchcmd}" ${ban_fetchparm} "${tmp_load}" "${feed_url}" 2>>"${ban_errorlog}"; then
				if [ -s "${tmp_load}" ]; then
					"${ban_catcmd}" "${tmp_load}" 2>>"${ban_errorlog}" >>"${tmp_allow}"
					feed_rc="${?}"
				fi
			else
				f_log "info" "download for feed '${feed%%.*}' failed"
				feed_rc="4"
				break
			fi
		done

		if [ "${feed_rc}" = "0" ]; then
			f_backup "allowlist" "${tmp_allow}"
		elif [ -z "${restore_rc}" ] && [ "${feed_rc}" != "0" ]; then
			f_restore "allowlist" "-" "${tmp_allow}" "${feed_rc}"
		fi
		feed_rc="${?}"
	fi

	# handle local feeds
	#
	if [ "${feed%%.*}" = "allowlist" ]; then
		{
			printf '%s\n\n' "#!${ban_nftcmd} -f"
			[ -s "${tmp_flush}" ] && "${ban_catcmd}" "${tmp_flush}"
			case "${feed_ipv}" in
			"4MAC")
				"${ban_awkcmd}" '{gsub(/\r/,"")}/^([0-9A-f]{2}:){5}[0-9A-f]{2}(\/([0-9]|[1-3][0-9]|4[0-8]))?([[:space:]]+([1-9][0-9]?[0-9]?\.){1}([0-9]{1,3}\.){2}(1?[0-9][0-9]?|2[0-4][0-9]|25[0-5])(\/(1?[0-9]|2?[0-9]|3?[0-2]))?([[:space:]]+#.*$|[[:space:]]*$)|[[:space:]]+#.*$|$)/{if(!$2||$2~/#/)$2="0.0.0.0/0";if(!seen[$1]++)printf "%s . %s, ",tolower($1),$2}' "${tmp_allow}" >"${tmp_file}"
				printf '%s\n' "add set inet banIP ${feed} { type ether_addr . ipv4_addr; flags interval; auto-merge; policy ${ban_nftpolicy}${nft_setcnt}; $(f_getelements "${tmp_file}") }"
				[ -z "${feed_direction##*outbound*}" ] && printf '%s\n' "add rule inet banIP _outbound ether saddr . ip saddr @${feed} ${nft_cnt} accept"
				;;
			"6MAC")
				"${ban_awkcmd}" '{gsub(/\r/,"")}/^([0-9A-f]{2}:){5}[0-9A-f]{2}(\/([0-9]|[1-3][0-9]|4[0-8]))?([[:space:]]+([0-9A-f]{0,4}:){1,7}[0-9A-f]{0,4}:?(\/(1?[0-2][0-8]|[0-9][0-9]))?([[:space:]]+#.*$|[[:space:]]*$)|[[:space:]]+#.*$|$)/{if(!$2||$2~/#/)$2="::/0";if(!seen[$1]++)printf "%s . %s, ",tolower($1),$2}' "${tmp_allow}" >"${tmp_file}"
				printf '%s\n' "add set inet banIP ${feed} { type ether_addr . ipv6_addr; flags interval; auto-merge; policy ${ban_nftpolicy}${nft_setcnt}; $(f_getelements "${tmp_file}") }"
				[ -z "${feed_direction##*outbound*}" ] && printf '%s\n' "add rule inet banIP _outbound ether saddr . ip6 saddr @${feed} ${nft_cnt} accept"
				;;
			"4")
				f_chkip ${feed_ipv} local 1 <"${tmp_allow}" >"${tmp_file}"
				printf '%s\n' "add set inet banIP ${feed} { type ipv4_addr; flags interval; auto-merge; policy ${ban_nftpolicy}${nft_setcnt}; $(f_getelements "${tmp_file}") }"
				if [ -z "${feed_direction##*inbound*}" ]; then
					if [ "${ban_allowlistonly}" = "1" ]; then
						if [ "${ban_loginbound}" = "1" ]; then
							printf '%s\n' "add rule inet banIP _inbound ip saddr != @${feed} ${log_inbound}"
						fi
						printf '%s\n' "add rule inet banIP _inbound ip saddr != @${feed} ${nft_cnt} ${feed_target}"
					else
						printf '%s\n' "add rule inet banIP _inbound ip saddr @${feed} ${nft_cnt} accept"
					fi
				fi
				if [ -z "${feed_direction##*outbound*}" ]; then
					if [ "${ban_allowlistonly}" = "1" ]; then
						if [ "${ban_logoutbound}" = "1" ]; then
							printf '%s\n' "add rule inet banIP _outbound ip daddr != @${feed} ${log_outbound}"
						fi
						printf '%s\n' "add rule inet banIP _outbound ip daddr != @${feed} ${nft_cnt} ${feed_target}"
					else
						printf '%s\n' "add rule inet banIP _outbound ip daddr @${feed} ${nft_cnt} accept"
					fi
				fi
				;;
			"6")
				f_chkip ${feed_ipv} local 1 <"${tmp_allow}" >"${tmp_file}"
				printf '%s\n' "add set inet banIP ${feed} { type ipv6_addr; flags interval; auto-merge; policy ${ban_nftpolicy}${nft_setcnt}; $(f_getelements "${tmp_file}") }"
				if [ -z "${feed_direction##*inbound*}" ]; then
					if [ "${ban_allowlistonly}" = "1" ]; then
						if [ "${ban_loginbound}" = "1" ]; then
							printf '%s\n' "add rule inet banIP _inbound ip6 saddr != @${feed} ${log_inbound}"
						fi
						printf '%s\n' "add rule inet banIP _inbound ip6 saddr != @${feed} ${nft_cnt} ${feed_target}"
					else
						printf '%s\n' "add rule inet banIP _inbound ip6 saddr @${feed} ${nft_cnt} accept"
					fi
				fi
				if [ -z "${feed_direction##*outbound*}" ]; then
					if [ "${ban_allowlistonly}" = "1" ]; then
						if [ "${ban_logoutbound}" = "1" ]; then
							printf '%s\n' "add rule inet banIP _outbound ip6 daddr != @${feed} ${log_outbound}"
						fi
						printf '%s\n' "add rule inet banIP _outbound ip6 daddr != @${feed} ${nft_cnt} ${feed_target}"
					else
						printf '%s\n' "add rule inet banIP _outbound ip6 daddr @${feed} ${nft_cnt} accept"
					fi
				fi
				;;
			esac
		} >"${tmp_nft}"
		: >"${tmp_flush}" >"${tmp_raw}" >"${tmp_file}"
		feed_rc="0"
	elif [ "${feed%%.*}" = "blocklist" ]; then
		{
			printf '%s\n\n' "#!${ban_nftcmd} -f"
			[ -s "${tmp_flush}" ] && "${ban_catcmd}" "${tmp_flush}"
			case "${feed_ipv}" in
			"4MAC")
				"${ban_awkcmd}" '{gsub(/\r/,"")}/^([0-9A-f]{2}:){5}[0-9A-f]{2}(\/([0-9]|[1-3][0-9]|4[0-8]))?([[:space:]]+([1-9][0-9]?[0-9]?\.){1}([0-9]{1,3}\.){2}(1?[0-9][0-9]?|2[0-4][0-9]|25[0-5])(\/(1?[0-9]|2?[0-9]|3?[0-2]))?([[:space:]]+#.*$|[[:space:]]*$)|[[:space:]]+#.*$|$)/{if(!$2||$2~/#/)$2="0.0.0.0/0";if(!seen[$1]++)printf "%s . %s, ",tolower($1),$2}' "${ban_blocklist}" >"${tmp_file}"
				printf '%s\n' "add set inet banIP ${feed} { type ether_addr . ipv4_addr; flags interval; auto-merge; policy ${ban_nftpolicy}${nft_setcnt}; $(f_getelements "${tmp_file}") }"
				if [ -z "${feed_direction##*outbound*}" ]; then
					if [ "${ban_logoutbound}" = "1" ]; then
						printf '%s\n' "add rule inet banIP _outbound ether saddr . ip saddr @${feed} ${log_outbound}"
					fi
					printf '%s\n' "add rule inet banIP _outbound ether saddr . ip saddr @${feed} ${nft_cnt} goto _reject"
				fi
				;;
			"6MAC")
				"${ban_awkcmd}" '{gsub(/\r/,"")}/^([0-9A-f]{2}:){5}[0-9A-f]{2}(\/([0-9]|[1-3][0-9]|4[0-8]))?([[:space:]]+([0-9A-f]{0,4}:){1,7}[0-9A-f]{0,4}:?(\/(1?[0-2][0-8]|[0-9][0-9]))?([[:space:]]+#.*$|[[:space:]]*$)|[[:space:]]+#.*$|$)/{if(!$2||$2~/#/)$2="::/0";if(!seen[$1]++)printf "%s . %s, ",tolower($1),$2}' "${ban_blocklist}" >"${tmp_file}"
				printf '%s\n' "add set inet banIP ${feed} { type ether_addr . ipv6_addr; flags interval; auto-merge; policy ${ban_nftpolicy}${nft_setcnt}; $(f_getelements "${tmp_file}") }"
				if [ -z "${feed_direction##*outbound*}" ]; then
					if [ "${ban_logoutbound}" = "1" ]; then
						printf '%s\n' "add rule inet banIP _outbound ether saddr . ip6 saddr @${feed} ${log_outbound}"
					fi
					printf '%s\n' "add rule inet banIP _outbound ether saddr . ip6 saddr @${feed} ${nft_cnt} goto _reject"
				fi
				;;
			"4")
				f_chkip ${feed_ipv} local 1 <"${ban_blocklist}" >"${tmp_file}"
				printf '%s\n' "add set inet banIP ${feed} { type ipv4_addr; flags interval, timeout; auto-merge; policy ${ban_nftpolicy}${nft_setcnt}; $(f_getelements "${tmp_file}") }"
				if [ -z "${feed_direction##*inbound*}" ]; then
					if [ "${ban_loginbound}" = "1" ]; then
						printf '%s\n' "add rule inet banIP _inbound ip saddr @${feed} ${log_inbound}"
					fi
					printf '%s\n' "add rule inet banIP _inbound ip saddr @${feed} ${nft_cnt} ${feed_target}"
				fi
				if [ -z "${feed_direction##*outbound*}" ]; then
					if [ "${ban_logoutbound}" = "1" ]; then
						printf '%s\n' "add rule inet banIP _outbound ip daddr @${feed} ${log_outbound}"
					fi
					printf '%s\n' "add rule inet banIP _outbound ip daddr @${feed} ${nft_cnt} goto _reject"
				fi
				;;
			"6")
				f_chkip ${feed_ipv} local 1 <"${ban_blocklist}" >"${tmp_file}"
				printf '%s\n' "add set inet banIP ${feed} { type ipv6_addr; flags interval, timeout; auto-merge; policy ${ban_nftpolicy}${nft_setcnt}; $(f_getelements "${tmp_file}") }"
				if [ -z "${feed_direction##*inbound*}" ]; then
					if [ "${ban_loginbound}" = "1" ]; then
						printf '%s\n' "add rule inet banIP _inbound ip6 saddr @${feed} ${log_inbound}"
					fi
					printf '%s\n' "add rule inet banIP _inbound ip6 saddr @${feed} ${nft_cnt} ${feed_target}"
				fi
				if [ -z "${feed_direction##*outbound*}" ]; then
					if [ "${ban_logoutbound}" = "1" ]; then
						printf '%s\n' "add rule inet banIP _outbound ip6 daddr @${feed} ${log_outbound}"
					fi
					printf '%s\n' "add rule inet banIP _outbound ip6 daddr @${feed} ${nft_cnt} goto _reject"
				fi
				;;
			esac
		} >"${tmp_nft}"
		: >"${tmp_flush}" >"${tmp_raw}" >"${tmp_file}"
		feed_rc="0"

	# handle external feeds
	#
	elif [ "${restore_rc}" != "0" ] && [ "${feed_url}" != "local" ]; then

		# handle country downloads
		#
		if [ "${feed%%.*}" = "country" ]; then
			if [ "${ban_countrysplit}" = "0" ]; then
				for country in ${ban_country}; do
					if "${ban_fetchcmd}" ${ban_fetchparm} "${tmp_raw}" "${feed_url}${country}-aggregated.zone" 2>>"${ban_errorlog}"; then
						if [ -s "${tmp_raw}" ]; then
							"${ban_catcmd}" "${tmp_raw}" 2>>"${ban_errorlog}" >>"${tmp_load}"
							feed_rc="${?}"
						fi
					else
						f_log "info" "download for feed '${feed}/${country}' failed"
					fi
				done
				: >"${tmp_raw}"
			else
				country="${feed%.*}"
				country="${country#*.}"
				if "${ban_fetchcmd}" ${ban_fetchparm} "${tmp_load}" "${feed_url}${country}-aggregated.zone" 2>>"${ban_errorlog}"; then
					feed_rc="${?}"
				else
					feed_rc="4"
				fi
			fi

		# handle asn downloads
		#
		elif [ "${feed%%.*}" = "asn" ]; then
			if [ "${ban_asnsplit}" = "0" ]; then
				for asn in ${ban_asn}; do
					if "${ban_fetchcmd}" ${ban_fetchparm} "${tmp_raw}" "${feed_url}AS${asn}" 2>>"${ban_errorlog}"; then
						if [ -s "${tmp_raw}" ]; then
							"${ban_catcmd}" "${tmp_raw}" 2>>"${ban_errorlog}" >>"${tmp_load}"
							feed_rc="${?}"
						fi
					else
						f_log "info" "download for feed '${feed}/${asn}' failed"
					fi
				done
				: >"${tmp_raw}"
			else
				asn="${feed%.*}"
				asn="${asn#*.}"
				if "${ban_fetchcmd}" ${ban_fetchparm} "${tmp_load}" "${feed_url}AS${asn}" 2>>"${ban_errorlog}"; then
					feed_rc="${?}"
				else
					feed_rc="4"
				fi
			fi

		# handle compressed downloads
		#
		elif [ "${feed_comp}" = "gz" ]; then
			if "${ban_fetchcmd}" ${ban_fetchparm} "${tmp_raw}" "${feed_url}" 2>>"${ban_errorlog}"; then
				if [ -s "${tmp_raw}" ]; then
					"${ban_zcatcmd}" "${tmp_raw}" 2>>"${ban_errorlog}" >"${tmp_load}"
					feed_rc="${?}"
				fi
			else
				feed_rc="4"
			fi
			: >"${tmp_raw}"

		# handle normal downloads
		#
		else
			if "${ban_fetchcmd}" ${ban_fetchparm} "${tmp_load}" "${feed_url}" 2>>"${ban_errorlog}"; then
				feed_rc="${?}"
			else
				feed_rc="4"
			fi
		fi
	fi
	[ "${feed_rc}" != "0" ] && f_log "info" "download for feed '${feed}' failed, rc: ${feed_rc:-"-"}"

	# backup/restore
	#
	if [ "${restore_rc}" != "0" ] && [ "${feed_rc}" = "0" ] && [ "${feed_url}" != "local" ] && [ ! -s "${tmp_nft}" ]; then
		f_backup "${feed}" "${tmp_load}"
		feed_rc="${?}"
	elif [ -z "${restore_rc}" ] && [ "${feed_rc}" != "0" ] && [ "${feed_url}" != "local" ] && [ ! -s "${tmp_nft}" ]; then
		f_restore "${feed}" "${feed_url}" "${tmp_load}" "${feed_rc}"
		feed_rc="${?}"
	fi

	# final file & Set preparation for regular downloads
	#
	if [ "${feed_rc}" = "0" ] && [ ! -s "${tmp_nft}" ]; then

		# deduplicate Sets
		#
		if [ "${ban_deduplicate}" = "1" ] && [ "${feed_url}" != "local" ] && [ -z "${feed_complete}" ]; then
			f_chkip ${feed_ipv} ${feed_rule} <"${tmp_load}" >"${tmp_raw}"
			{
				"${ban_flockcmd}" -x 8
				"${ban_awkcmd}" 'NR==FNR{member[$0];next}!($0 in member)' "${ban_tmpfile}.deduplicate" "${tmp_raw}" 2>>"${ban_errorlog}" | tee -a "${ban_tmpfile}.deduplicate" >"${tmp_split}"
				feed_rc="${?}"
			} 8>"${ban_deduplock}"
		else
			f_chkip ${feed_ipv} ${feed_rule} <"${tmp_load}" >"${tmp_split}"
			feed_rc="${?}"
		fi
		: >"${tmp_raw}" >"${tmp_load}"

		# split Sets
		#
		if [ "${feed_rc}" = "0" ]; then
			if [ -n "${ban_splitsize//[![:digit:]]/}" ] && [ "${ban_splitsize//[![:digit:]]/}" -ge "512" ]; then
				if ! "${ban_awkcmd}" "NR%${ban_splitsize//[![:digit:]]/}==1{file=\"${tmp_file}.\"++i;}{ORS=\" \";print > file}" "${tmp_split}" 2>>"${ban_errorlog}"; then
					feed_rc="${?}"
					"${ban_rmcmd}" -f "${tmp_file}".*
					f_log "info" "can't split nfset '${feed}' to size '${ban_splitsize//[![:digit:]]/}'"
				fi
			else
				"${ban_awkcmd}" '{ORS=" ";print}' "${tmp_split}" 2>>"${ban_errorlog}" >"${tmp_file}.1"
				feed_rc="${?}"
			fi
		fi

		# build nft file
		#
		if [ "${feed_rc}" = "0" ] && [ -s "${tmp_file}.1" ]; then
			if [ "${feed_ipv}" = "4" ]; then
				{
					# nft header (IPv4 Set) incl. inbound and outbound rules
					#
					printf '%s\n\n' "#!${ban_nftcmd} -f"
					[ -s "${tmp_flush}" ] && "${ban_catcmd}" "${tmp_flush}"
					printf '%s\n' "add set inet banIP ${feed} { type ipv4_addr; flags interval; auto-merge; policy ${ban_nftpolicy}${nft_setcnt}; $(f_getelements "${tmp_file}.1") }"
					if [ -z "${feed_direction##*inbound*}" ]; then
						if [ "${ban_loginbound}" = "1" ]; then
							printf '%s\n' "add rule inet banIP _inbound ${feed_dport} ip saddr @${feed} ${log_inbound}"
						fi
						printf '%s\n' "add rule inet banIP _inbound ${feed_dport} ip saddr @${feed} ${nft_cnt} ${feed_target}"
					fi
					if [ -z "${feed_direction##*outbound*}" ]; then
						if [ "${ban_logoutbound}" = "1" ]; then
							printf '%s\n' "add rule inet banIP _outbound ${feed_dport} ip daddr @${feed} ${log_outbound}"
						fi
						printf '%s\n' "add rule inet banIP _outbound ${feed_dport} ip daddr @${feed} ${nft_cnt} goto _reject"
					fi
				} >"${tmp_nft}"
			elif [ "${feed_ipv}" = "6" ]; then
				{
					# nft header (IPv6 Set) incl. inbound and outbound rules
					#
					printf '%s\n\n' "#!${ban_nftcmd} -f"
					[ -s "${tmp_flush}" ] && "${ban_catcmd}" "${tmp_flush}"
					printf '%s\n' "add set inet banIP ${feed} { type ipv6_addr; flags interval; auto-merge; policy ${ban_nftpolicy}${nft_setcnt}; $(f_getelements "${tmp_file}.1") }"
					if [ -z "${feed_direction##*inbound*}" ]; then
						if [ "${ban_loginbound}" = "1" ]; then
							printf '%s\n' "add rule inet banIP _inbound ${feed_dport} ip6 saddr @${feed} ${log_inbound}"
						fi
						printf '%s\n' "add rule inet banIP _inbound ${feed_dport} ip6 saddr @${feed} ${nft_cnt} ${feed_target}"
					fi
					if [ -z "${feed_direction##*outbound*}" ]; then
						if [ "${ban_logoutbound}" = "1" ]; then
							printf '%s\n' "add rule inet banIP _outbound ${feed_dport} ip6 daddr @${feed} ${log_outbound}"
						fi
						printf '%s\n' "add rule inet banIP _outbound ${feed_dport} ip6 daddr @${feed} ${nft_cnt} goto _reject"
					fi
				} >"${tmp_nft}"
			fi
		fi
		: >"${tmp_flush}" >"${tmp_file}.1"
	fi

	# load generated nft file in banIP table
	#
	if [ "${feed_rc}" = "0" ]; then
		if [ "${feed%%.*}" = "allowlist" ]; then
			cnt_dl="$("${ban_awkcmd}" 'END{printf "%d",NR}' "${tmp_allow}" 2>>"${ban_errorlog}")"
		elif [ "${feed%%.*}" = "blocklist" ]; then
			cnt_dl="$("${ban_awkcmd}" 'END{printf "%d",NR}' "${ban_blocklist}" 2>>"${ban_errorlog}")"
		else
			cnt_dl="$("${ban_awkcmd}" 'END{printf "%d",NR}' "${tmp_split}" 2>>"${ban_errorlog}")"
			: >"${tmp_split}"
		fi
		if [ "${cnt_dl:-"0"}" -gt "0" ] || [ "${feed%%.*}" = "allowlist" ] || [ "${feed%%.*}" = "blocklist" ]; then

			# load initial file to nftset
			#
			f_nftload "${tmp_nft}" "can't load initial file to nfset '${feed}'"
			feed_rc="${?}"

			# load additional split files
			#
			if [ "${feed_rc}" = "0" ]; then
				for split_file in "${tmp_file}".*; do
					if [ -s "${split_file}" ]; then
						"${ban_sedcmd}" -i "1 i #!${ban_nftcmd} -f\nadd element inet banIP ${feed} { " "${split_file}"
						printf '%s\n' "}" >>"${split_file}"

						# load split file to nftset
						#
						f_nftload "${split_file}" "can't load split file '${split_file##*.}' to nfset '${feed}'"
						feed_rc="${?}"
						: >"${split_file}"
					fi
				done
				cnt_set="$("${ban_nftcmd}" -j list set inet banIP "${feed}" 2>/dev/null | "${ban_jsoncmd}" -qe '@.nftables[*].set.elem[*]' | "${ban_wccmd}" -l 2>/dev/null)"
			fi
		else
			f_log "info" "skip empty feed '${feed}'"
		fi
	fi
	: >"${tmp_nft}"
	read -r end_ts _ <"/proc/uptime"
	end_ts="${end_ts%%.*}"

	f_log "debug" "f_down    ::: feed: ${feed}, policy: ${feed_policy}, complete: ${feed_complete:-"-"}, cnt_dl: ${cnt_dl:-"-"}, cnt_set: ${cnt_set:-"-"}, split_size: ${ban_splitsize:-"-"}, time: $((end_ts - start_ts)), rc: ${feed_rc:-"-"}"
}

# backup feeds
#
f_backup() {
	local backup_rc="4" feed="${1}" feed_file="${2}"

	if [ -s "${feed_file}" ]; then
		"${ban_gzipcmd}" -cf "${feed_file}" >"${ban_backupdir}/banIP.${feed}.gz"
		backup_rc="${?}"
	fi

	f_log "debug" "f_backup  ::: feed: ${feed}, file: banIP.${feed}.gz, rc: ${backup_rc}"
	return "${backup_rc}"
}

# restore feeds
#
f_restore() {
	local tmp_feed restore_rc="4" feed="${1}" feed_url="${2}" feed_file="${3}" in_rc="${4}"

	[ "${feed_url}" = "local" ] && tmp_feed="${feed%.*}.v4" || tmp_feed="${feed}"
	if [ -s "${ban_backupdir}/banIP.${tmp_feed}.gz" ]; then
		"${ban_zcatcmd}" "${ban_backupdir}/banIP.${tmp_feed}.gz" 2>>"${ban_errorlog}" >"${feed_file}"
		restore_rc="${?}"
	fi

	f_log "debug" "f_restore ::: feed: ${feed}, file: banIP.${tmp_feed}.gz, in_rc: ${in_rc:-"-"}, rc: ${restore_rc}"
	return "${restore_rc}"
}

# remove staled Sets
#
f_rmset() {
	local feedlist tmp_del table_json feed country asn table_sets handles handle expr del_set feed_rc

	f_getfeed
	json_get_keys feedlist
	tmp_del="${ban_tmpfile}.final.delete"
	table_json="$("${ban_nftcmd}" -tj list table inet banIP 2>>"${ban_errorlog}")"
	table_sets="$(printf '%s\n' "${table_json}" | "${ban_jsoncmd}" -qe '@.nftables[@.set.family="inet"].set.name')"
	{
		printf '%s\n\n' "#!${ban_nftcmd} -f"
		for feed in ${table_sets}; do

			# keep: active country split sets
			#
			if [ "${feed%%.*}" = "country" ] && [ "${ban_countrysplit}" = "1" ]; then
				country="${feed%.*}"
				country="${country#*.}"
				case " ${ban_feed} " in
				*" country "*)
					case " ${ban_country} " in
					*" ${country} "*) continue ;;
					esac
					;;
				esac
			fi

			# keep: active asn split sets
			#
			if [ "${feed%%.*}" = "asn" ] && [ "${ban_asnsplit}" = "1" ]; then
				asn="${feed%.*}"
				asn="${asn#*.}"
				case " ${ban_feed} " in
				*" asn "*)
					case " ${ban_asn} " in
					*" ${asn} "*) continue ;;
					esac
					;;
				esac
			fi

			# keep: set is active in config and feed file, respecting split/allowlistonly exclusions
			#
			case " allowlist blocklist ${ban_feed} " in
			*" ${feed%.*} "*)
				case " allowlist blocklist ${feedlist} " in
				*" ${feed%.*} "*)
					if [ "${feed%.*}" != "country" ] || [ "${ban_countrysplit}" != "1" ]; then
						if [ "${feed%.*}" != "asn" ] || [ "${ban_asnsplit}" != "1" ]; then
							if [ "${feed%.*}" = "allowlist" ] || [ "${feed%.*}" = "blocklist" ] || [ "${ban_allowlistonly}" != "1" ]; then
								continue
							fi
							case " ${ban_feedin} ${ban_feedout} " in
							*" allowlist "*) continue ;;
							esac
						fi
					fi
					;;
				esac
				;;
			esac

			# delete: set is stale or no longer active
			#
			[ -z "${del_set}" ] && del_set="${feed}" || del_set="${del_set}, ${feed}"
			"${ban_rmcmd}" -f "${ban_backupdir}/banIP.${feed}.gz"
			for chain in _inbound _outbound; do
				for expr in 0 1 2; do
					handles="$(printf '%s\n' "${table_json}" | "${ban_jsoncmd}" -q -e "@.nftables[@.rule.chain=\"${chain}\"][@.expr[${expr}].match.right=\"@${feed}\"].handle" | "${ban_xargscmd}")"
					for handle in ${handles}; do
						printf '%s\n' "delete rule inet banIP ${chain} handle ${handle}"
					done
				done
			done
			printf '%s\n' "flush set inet banIP ${feed}"
			printf '%s\n\n' "delete set inet banIP ${feed}"
		done
	} >"${tmp_del}"

	if [ -n "${del_set}" ]; then
		if "${ban_nftcmd}" -f "${tmp_del}" >/dev/null 2>&1; then
			feed_rc="${?}"
		else
			feed_rc="4"
		fi
	fi
	: >"${tmp_del}"

	f_log "debug" "f_rmset   ::: feed: ${del_set:-"-"}, rc: ${feed_rc:-"-"}"
}

# generate status information
#
f_genstatus() {
	local s_auto s_v4 s_v6 s_bcp s_lpre s_lin s_lout s_cnt s_dedup s_split s_cf s_allow s_dbg runtime
	local mem_free nft_ver chain_cnt set_cnt rule_cnt object end_time duration table table_sets element_cnt="0" custom_feed="0" split="0" status="${1}"

	# memory and nftables version information
	#
	mem_free="$("${ban_awkcmd}" '/^MemAvailable/{printf "%.2f", $2/1024}' "/proc/meminfo" 2>>"${ban_errorlog}")"
	nft_ver="$(printf '%s' "${ban_packages}" | "${ban_jsoncmd}" -ql1 -e '@.packages["nftables-json"]')"

	# read config information if not already available
	#
	[ -z "${ban_dev}" ] && f_conf

	# only gather detailed information if banIP is active
	#
	if [ "${status}" = "active" ]; then
		table="$("${ban_nftcmd}" -tj list table inet banIP 2>>"${ban_errorlog}")"
		table_sets="$(printf '%s' "${table}" | "${ban_jsoncmd}" -qe '@.nftables[@.set.family="inet"].set.name')"
		for object in ${table_sets}; do
			element_cnt="$((element_cnt + $("${ban_nftcmd}" -j list set inet banIP "${object}" 2>>"${ban_errorlog}" | "${ban_jsoncmd}" -qe '@.nftables[*].set.elem[*]' | "${ban_wccmd}" -l 2>>"${ban_errorlog}")))"
		done
		chain_cnt="$(printf '%s' "${table}" | "${ban_jsoncmd}" -qe '@.nftables[*].chain.name' | "${ban_wccmd}" -l 2>>"${ban_errorlog}")"
		set_cnt="$(printf '%s' "${table}" | "${ban_jsoncmd}" -qe '@.nftables[*].set.name' | "${ban_wccmd}" -l 2>>"${ban_errorlog}")"
		rule_cnt="$(printf '%s' "${table}" | "${ban_jsoncmd}" -qe '@.nftables[*].rule' | "${ban_wccmd}" -l 2>>"${ban_errorlog}")"
		element_cnt="$("${ban_awkcmd}" -v cnt="${element_cnt}" 'BEGIN{res="";pos=0;for(i=length(cnt);i>0;i--){res=substr(cnt,i,1)res;pos++;if(pos==3&&i>1){res=" "res;pos=0;}}; printf"%s",res}')"
		if [ -n "${ban_starttime}" ]; then
			read -r end_time _ <"/proc/uptime"
			end_time="${end_time%%.*}"
			duration="$(((end_time - ban_starttime) / 60))m $(((end_time - ban_starttime) % 60))s"
		fi
		runtime="mode: ${ban_action}, date / time: $(date "+%d/%m/%Y %H:%M:%S"), duration: ${duration:-"-"}, memory: ${mem_free} MB available"
	fi
	[ -s "${ban_customfeedfile}" ] && custom_feed="1"
	[ "${ban_splitsize:-"0"}" -gt "0" ] && split="1"

	# map flag values to status characters
	#
	case "${ban_autodetect}" in 1) s_auto="✔" ;; *) s_auto="✘" ;; esac
	case "${ban_protov4}" in 1) s_v4="✔" ;; *) s_v4="✘" ;; esac
	case "${ban_protov6}" in 1) s_v6="✔" ;; *) s_v6="✘" ;; esac
	case "${ban_bcp38}" in 1) s_bcp="✔" ;; *) s_bcp="✘" ;; esac
	case "${ban_logprerouting}" in 1) s_lpre="✔" ;; *) s_lpre="✘" ;; esac
	case "${ban_loginbound}" in 1) s_lin="✔" ;; *) s_lin="✘" ;; esac
	case "${ban_logoutbound}" in 1) s_lout="✔" ;; *) s_lout="✘" ;; esac
	case "${ban_nftcount}" in 1) s_cnt="✔" ;; *) s_cnt="✘" ;; esac
	case "${ban_deduplicate}" in 1) s_dedup="✔" ;; *) s_dedup="✘" ;; esac
	case "${split}" in 1) s_split="✔" ;; *) s_split="✘" ;; esac
	case "${custom_feed}" in 1) s_cf="✔" ;; *) s_cf="✘" ;; esac
	case "${ban_allowlistonly}" in 1) s_allow="✔" ;; *) s_allow="✘" ;; esac
	case "${ban_debug}" in 1) s_dbg="✔" ;; *) s_dbg="✘" ;; esac

	# generate JSON status file
	#
	: >"${ban_rtfile}"
	json_init
	json_load_file "${ban_rtfile}" >/dev/null 2>&1
	json_add_string "status" "${status}"
	json_add_string "frontend_ver" "${ban_fver}"
	json_add_string "backend_ver" "${ban_bver}"
	json_add_string "element_count" "${element_cnt} (chains: ${chain_cnt:-"0"}, sets: ${set_cnt:-"0"}, rules: ${rule_cnt:-"0"})"
	json_add_array "active_feeds"
	for object in ${table_sets:-"-"}; do
		json_add_string "${object}" "${object}"
	done
	json_close_array
	json_add_array "wan_devices"
	for object in ${ban_dev:-"-"}; do
		json_add_string "${object}" "${object}"
	done
	json_close_array
	json_add_array "wan_interfaces"
	for object in ${ban_ifv4:-"-"} ${ban_ifv6:-"-"}; do
		json_add_string "${object}" "${object}"
	done
	json_close_array
	json_add_array "vlan_allow"
	for object in ${ban_vlanallow:-"-"}; do
		json_add_string "${object}" "${object}"
	done
	json_close_array
	json_add_array "vlan_block"
	for object in ${ban_vlanblock:-"-"}; do
		json_add_string "${object}" "${object}"
	done
	json_close_array
	json_add_array "active_uplink"
	for object in ${ban_uplink:-"-"}; do
		json_add_string "${object}" "${object}"
	done
	json_close_array
	json_add_string "nft_info" "ver: ${nft_ver:-"-"}, priority: ${ban_nftpriority}, policy: ${ban_nftpolicy}, loglevel: ${ban_nftloglevel}, expiry: ${ban_nftexpiry:-"-"}, limit (icmp/syn/udp): ${ban_icmplimit}/${ban_synlimit}/${ban_udplimit}, loglimit (rate/burst): ${ban_logratelimit}/${ban_logburstlimit}"
	json_add_string "run_info" "base: ${ban_basedir}, backup: ${ban_backupdir}, report: ${ban_reportdir}, error: ${ban_errordir}"
	json_add_string "run_flags" "auto: ${s_auto}, proto (4/6): ${s_v4}/${s_v6}, bcp38: ${s_bcp}, log (pre/in/out): ${s_lpre}/${s_lin}/${s_lout}, count: ${s_cnt}, dedup: ${s_dedup}, split: ${s_split}, custom feed: ${s_cf}, allowed only: ${s_allow}, debug: ${s_dbg}"
	json_add_string "last_run" "${runtime:-"-"}"
	json_add_string "system_info" "cores: ${ban_cores}, log: ${ban_logreadcmd##*/}, fetch: ${ban_fetchcmd##*/}, ${ban_sysver}"
	json_dump >"${ban_rtfile}"
}

# get status information
#
f_getstatus() {
	local key keylist value values

	[ -z "${ban_dev}" ] && f_conf
	json_load_file "${ban_rtfile}" >/dev/null 2>&1
	if json_get_keys keylist; then
		printf '%s\n' "::: banIP runtime information"
		for key in ${keylist}; do
			if [ "${key}" = "active_feeds" ] || [ "${key}" = "active_uplink" ]; then
				json_get_values values "${key}" >/dev/null 2>&1
				value="${values// /, }"
			elif [ "${key}" = "wan_devices" ]; then
				json_get_values values "${key}" >/dev/null 2>&1
				value="wan: ${values// /, } / "
				json_get_values values "wan_interfaces" >/dev/null 2>&1
				value="${value}wan-if: ${values// /, } / "
				json_get_values values "vlan_allow" >/dev/null 2>&1
				value="${value}vlan-allow: ${values// /, } / "
				json_get_values values "vlan_block" >/dev/null 2>&1
				value="${value}vlan-block: ${values// /, }"
				key="active_devices"
			else
				json_get_var value "${key}" >/dev/null 2>&1
				if [ "${key}" = "status" ]; then
					[ "${value}" = "active" ] && value="${value} ($(f_actual))"
				fi
			fi
			if [ "${key}" != "wan_interfaces" ] && [ "${key}" != "vlan_allow" ] && [ "${key}" != "vlan_block" ]; then
				printf '  + %-17s : %s\n' "${key}" "${value:-"-"}"
			fi
		done
	else
		printf '%s\n' "::: no banIP runtime information available"
	fi
}

# domain lookup
#
f_lookup() {
	local cnt list timestamp domain lookup ip elementsv4 elementsv6 start_time end_time duration cnt_domain="0" cnt_ip="0" feed="${1}"

	read -r start_time _ <"/proc/uptime"
	start_time="${start_time%%.*}"
	if [ "${feed}" = "allowlist" ]; then
		list="$("${ban_awkcmd}" '{gsub(/\r/,"")}/^([[:alnum:]_-]{1,63}\.)+[[:alpha:]]+([[:space:]]|$)/{printf "%s ",tolower($1)}' "${ban_allowlist}" 2>>"${ban_errorlog}")"
	elif [ "${feed}" = "blocklist" ]; then
		list="$("${ban_awkcmd}" '{gsub(/\r/,"")}/^([[:alnum:]_-]{1,63}\.)+[[:alpha:]]+([[:space:]]|$)/{printf "%s ",tolower($1)}' "${ban_blocklist}" 2>>"${ban_errorlog}")"
	fi

	for domain in ${list}; do
		lookup="$("${ban_lookupcmd}" "${domain}" ${ban_resolver} 2>>"${ban_errorlog}" | "${ban_awkcmd}" '/^Address[ 0-9]*: /{if(!seen[$NF]++)printf "%s ",$NF}' 2>>"${ban_errorlog}")"
		[ -n "${lookup}" ] && timestamp="$(date "+%Y-%m-%d %H:%M:%S")"
		for ip in ${lookup}; do
			if [ "${ip%%.*}" = "127" ] || [ "${ip%%.*}" = "0" ] || [ -z "${ip%%::*}" ]; then
				continue
			else
				[ "${ip##*:}" = "${ip}" ] && elementsv4="${elementsv4} ${ip}," || elementsv6="${elementsv6} ${ip},"
				if [ "${feed}" = "allowlist" ] && [ "${ban_autoallowlist}" = "1" ] && ! "${ban_grepcmd}" -q "^${ip}[[:space:]]*#" "${ban_allowlist}"; then
					printf "%-45s%s\n" "${ip}" "# '${domain}' added on ${timestamp}" >>"${ban_allowlist}"
				elif [ "${feed}" = "blocklist" ] && [ "${ban_autoblocklist}" = "1" ] && ! "${ban_grepcmd}" -q "^${ip}[[:space:]]*#" "${ban_blocklist}"; then
					printf "%-45s%s\n" "${ip}" "# '${domain}' added on ${timestamp}" >>"${ban_blocklist}"
				fi
				cnt_ip="$((cnt_ip + 1))"
			fi
		done
		cnt_domain="$((cnt_domain + 1))"
	done
	if [ -n "${elementsv4}" ]; then
		if ! "${ban_nftcmd}" add element inet banIP "${feed}.v4" { ${elementsv4} } 2>>"${ban_errorlog}"; then
			f_log "info" "can't add lookup file to nfset '${feed}.v4'"
		fi
	fi
	if [ -n "${elementsv6}" ]; then
		if ! "${ban_nftcmd}" add element inet banIP "${feed}.v6" { ${elementsv6} } 2>>"${ban_errorlog}"; then
			f_log "info" "can't add lookup file to nfset '${feed}.v6'"
		fi
	fi
	read -r end_time _ <"/proc/uptime"
	end_time="${end_time%%.*}"
	duration="$(((end_time - start_time) / 60))m $(((end_time - start_time) % 60))s"

	f_log "debug" "f_lookup  ::: feed: ${feed}, domains: ${cnt_domain}, IPs: ${cnt_ip}, duration: ${duration}"
}

# table statistics
#
f_report() {
	local report_jsn report_txt tmp_val table_json item sep table_sets set_cnt set_inbound set_outbound set_cntinbound set_cntoutbound set_proto set_dport set_details
	local cnt ip expr detail jsnval timestamp autoadd_allow autoadd_block sum_sets sum_setinbound sum_setoutbound sum_cntelements sum_cntinbound sum_cntoutbound quantity
	local chunk jsn map_jsn chain set_elements set_json sum_setelements sum_synflood sum_udpflood sum_icmpflood sum_ctinvalid sum_tcpinvalid sum_setports sum_bcp38 output="${1}"

	f_conf
	f_mkdir "${ban_reportdir}"
	report_jsn="${ban_reportdir}/ban_report.jsn"
	report_txt="${ban_reportdir}/ban_report.txt"
	map_jsn="${ban_reportdir}/ban_map.jsn"

	if [ "${output}" != "json" ]; then

		# json output preparation
		#
		: >"${report_txt}" >"${report_jsn}" >"${map_jsn}"
		table_json="$("${ban_nftcmd}" -tj list table inet banIP 2>>"${ban_errorlog}")"
		table_sets="$(printf '%s' "${table_json}" | "${ban_jsoncmd}" -qe '@.nftables[@.set.family="inet"].set.name')"
		sum_sets="0"
		sum_cntelements="0"
		sum_setinbound="0"
		sum_setoutbound="0"
		sum_cntinbound="0"
		sum_cntoutbound="0"
		sum_setports="0"
		sum_setelements="0"
		sum_synflood="$(printf '%s' "${table_json}" | "${ban_jsoncmd}" -qe '@.nftables[@.counter.name="cnt_synflood"].*.packets')"
		sum_udpflood="$(printf '%s' "${table_json}" | "${ban_jsoncmd}" -qe '@.nftables[@.counter.name="cnt_udpflood"].*.packets')"
		sum_icmpflood="$(printf '%s' "${table_json}" | "${ban_jsoncmd}" -qe '@.nftables[@.counter.name="cnt_icmpflood"].*.packets')"
		sum_ctinvalid="$(printf '%s' "${table_json}" | "${ban_jsoncmd}" -qe '@.nftables[@.counter.name="cnt_ctinvalid"].*.packets')"
		sum_tcpinvalid="$(printf '%s' "${table_json}" | "${ban_jsoncmd}" -qe '@.nftables[@.counter.name="cnt_tcpinvalid"].*.packets')"
		sum_bcp38="$(printf '%s' "${table_json}" | "${ban_jsoncmd}" -qe '@.nftables[@.counter.name="cnt_bcp38"].*.packets')"
		timestamp="$(date "+%Y-%m-%d %H:%M:%S")"

		cnt="1"
		for item in ${table_sets}; do
			(
				set_json="$("${ban_nftcmd}" -j list set inet banIP "${item}" 2>>"${ban_errorlog}")"
				set_cnt="$(printf '%s' "${set_json}" | "${ban_jsoncmd}" -qe '@.nftables[*].set.elem[*]' | "${ban_wccmd}" -l 2>>"${ban_errorlog}")"
				set_cntinbound=""
				set_cntoutbound=""
				set_inbound=""
				set_outbound=""
				set_proto=""
				set_dport=""
				set_elements=""
				for chain in _inbound _outbound; do
					for expr in 0 1 2; do
						if [ "${chain}" = "_inbound" ] && [ -z "${set_cntinbound}" ]; then
							set_cntinbound="$(printf '%s' "${table_json}" | "${ban_jsoncmd}" -ql1 -e "@.nftables[@.rule.chain=\"${chain}\"][@.expr[${expr}].match.right=\"@${item}\"].expr[*].counter.packets")"
						elif [ "${chain}" = "_outbound" ] && [ -z "${set_cntoutbound}" ]; then
							set_cntoutbound="$(printf '%s' "${table_json}" | "${ban_jsoncmd}" -ql1 -e "@.nftables[@.rule.chain=\"${chain}\"][@.expr[${expr}].match.right=\"@${item}\"].expr[*].counter.packets")"
						fi
						[ -z "${set_proto}" ] && set_proto="$(printf '%s' "${table_json}" | "${ban_jsoncmd}" -ql1 -e "@.nftables[@.rule.chain=\"${chain}\"][@.expr[2].match.right=\"@${item}\"].expr[0].match.right.set")"
						[ -z "${set_proto}" ] && set_proto="$(printf '%s' "${table_json}" | "${ban_jsoncmd}" -ql1 -e "@.nftables[@.rule.chain=\"${chain}\"][@.expr[1].match.right=\"@${item}\"].expr[0].match.left.payload.protocol")"
						[ -z "${set_dport}" ] && set_dport="$(printf '%s' "${table_json}" | "${ban_jsoncmd}" -ql1 -e "@.nftables[@.rule.chain=\"${chain}\"][@.expr[2].match.right=\"@${item}\"].expr[1].match.right.set")"
						[ -z "${set_dport}" ] && set_dport="$(printf '%s' "${table_json}" | "${ban_jsoncmd}" -ql1 -e "@.nftables[@.rule.chain=\"${chain}\"][@.expr[2].match.right=\"@${item}\"].expr[1].match.right")"
						[ -z "${set_dport}" ] && set_dport="$(printf '%s' "${table_json}" | "${ban_jsoncmd}" -ql1 -e "@.nftables[@.rule.chain=\"${chain}\"][@.expr[1].match.right=\"@${item}\"].expr[0].match.right.set")"
						[ -z "${set_dport}" ] && set_dport="$(printf '%s' "${table_json}" | "${ban_jsoncmd}" -ql1 -e "@.nftables[@.rule.chain=\"${chain}\"][@.expr[1].match.right=\"@${item}\"].expr[0].match.right")"
					done
				done
				if [ -n "${set_proto}" ] && [ -n "${set_dport}" ]; then
					set_proto="${set_proto//[\{\}\":]/}"
					set_proto="${set_proto#\[ *}"
					set_proto="${set_proto%* \]}"
					set_dport="${set_dport//[\{\}\":]/}"
					set_dport="${set_dport#\[ *}"
					set_dport="${set_dport%* \]}"
					set_dport="${set_proto}: $(f_trim "${set_dport}")"
				fi
				if [ "${ban_nftcount}" = "1" ]; then
					set_elements="$(printf '%s' "${set_json}" | "${ban_jsoncmd}" -l50 -qe '@.nftables[*].set.elem[*][@.counter.packets>0].val' |
						"${ban_awkcmd}" -F '[ ,]' '{ORS=" ";if($2=="\"range\":"||$2=="\"concat\":")printf"%s, ",$4;else if($2=="\"prefix\":")printf"%s, ",$5;else printf"\"%s\", ",$1}')"
				fi
				if [ -n "${set_cntinbound}" ]; then
					set_inbound="ON"
				else
					set_inbound="-"
					set_cntinbound=""
				fi
				if [ -n "${set_cntoutbound}" ]; then
					set_outbound="ON"
				else
					set_outbound="-"
					set_cntoutbound=""
				fi
				printf '%s\n' "\"${item}\":{ \"cnt_elements\": \"${set_cnt}\", \
					\"cnt_inbound\": \"${set_cntinbound}\", \
					\"inbound\": \"${set_inbound}\", \
					\"cnt_outbound\": \"${set_cntoutbound}\", \
					\"outbound\": \"${set_outbound}\", \
					\"port\": \"${set_dport:-"-"}\", \
					\"set_elements\": [ ${set_elements%%??} ] \
				}" >"${report_jsn}.${item}"
			) &
			[ "${cnt}" -gt "${ban_cores}" ] && wait -n
			cnt="$((cnt + 1))"
		done
		wait

		# assemble JSON from per-set fragments
		#
		printf '%s' "{ \"sets\":{ " >"${report_jsn}"
		sep=""
		for item in ${table_sets}; do
			if [ -s "${report_jsn}.${item}" ]; then
				printf '%s' "${sep}" >>"${report_jsn}"
				"${ban_catcmd}" "${report_jsn}.${item}" >>"${report_jsn}"
				sep=", "
			fi
			"${ban_rmcmd}" -f "${report_jsn}.${item}"
		done
		printf '\n%s\n' "} }" >>"${report_jsn}"

		# add sum statistics
		#
		json_init
		if json_load_file "${report_jsn}" >/dev/null 2>&1; then
			json_select "sets" >/dev/null 2>&1
			json_get_keys table_sets >/dev/null 2>&1
			if [ -n "${table_sets}" ]; then
				for item in ${table_sets}; do
					sum_sets="$((sum_sets + 1))"
					json_select "${item}"
					json_get_keys set_details
					for detail in ${set_details}; do
						case "${detail}" in
						"cnt_elements")
							json_get_var jsnval "${detail}" >/dev/null 2>&1
							sum_cntelements="$((sum_cntelements + jsnval))"
							;;
						"set_elements")
							json_get_values jsnval "${detail}" >/dev/null 2>&1
							if [ -n "${jsnval}" ]; then
								jsnval="$(printf '%s' "${jsnval}" | "${ban_wccmd}" -w)"
								sum_setelements="$((sum_setelements + jsnval))"
							fi
							;;
						"inbound")
							json_get_var jsnval "${detail}" >/dev/null 2>&1
							if [ "${jsnval}" = "ON" ]; then
								sum_setinbound="$((sum_setinbound + 1))"
							fi
							;;
						"outbound")
							json_get_var jsnval "${detail}" >/dev/null 2>&1
							if [ "${jsnval}" = "ON" ]; then
								sum_setoutbound="$((sum_setoutbound + 1))"
							fi
							;;
						"cnt_inbound")
							json_get_var jsnval "${detail}" >/dev/null 2>&1
							if [ -n "${jsnval}" ]; then
								sum_cntinbound="$((sum_cntinbound + jsnval))"
							fi
							;;
						"cnt_outbound")
							json_get_var jsnval "${detail}" >/dev/null 2>&1
							if [ -n "${jsnval}" ]; then
								sum_cntoutbound="$((sum_cntoutbound + jsnval))"
							fi
							;;
						"port")
							json_get_var jsnval "${detail}" >/dev/null 2>&1
							if [ "${jsnval}" != "-" ]; then
								jsnval="${jsnval//[^0-9 ]/}"
								jsnval="$(printf '%s' "${jsnval}" | "${ban_wccmd}" -w)"
								sum_setports="$((sum_setports + jsnval))"
							fi
							;;
						esac
					done
					json_select ".."
				done
				"${ban_sedcmd}" -i '$d' "${report_jsn}"
				printf '%s\n' "}, \
					\"timestamp\": \"${timestamp}\", \
					\"autoadd_allow\": \"$("${ban_grepcmd}" -c "added on ${timestamp% *}" "${ban_allowlist}")\", \
					\"autoadd_block\": \"$("${ban_grepcmd}" -c "added on ${timestamp% *}" "${ban_blocklist}")\", \
					\"sum_synflood\": \"${sum_synflood}\", \
					\"sum_udpflood\": \"${sum_udpflood}\", \
					\"sum_icmpflood\": \"${sum_icmpflood}\", \
					\"sum_ctinvalid\": \"${sum_ctinvalid}\", \
					\"sum_tcpinvalid\": \"${sum_tcpinvalid}\", \
					\"sum_bcp38\": \"${sum_bcp38}\", \
					\"sum_sets\": \"${sum_sets}\", \
					\"sum_setinbound\": \"${sum_setinbound}\", \
					\"sum_setoutbound\": \"${sum_setoutbound}\", \
					\"sum_cntelements\": \"${sum_cntelements}\", \
					\"sum_cntinbound\": \"${sum_cntinbound}\", \
					\"sum_cntoutbound\": \"${sum_cntoutbound}\", \
					\"sum_setports\": \"${sum_setports}\", \
					\"sum_setelements\": \"${sum_setelements}\" \
				}" >>"${report_jsn}"
			fi
		fi

		# retrieve/prepare map data
		#
		if [ "${ban_nftcount}" = "1" ] && [ "${ban_map}" = "1" ] && [ -s "${report_jsn}" ]; then
			cnt="1"
			f_getdl
			json_init
			if json_load_file "${ban_rtfile}" >/dev/null 2>&1; then
				json_get_values jsnval "active_uplink" >/dev/null 2>&1
				jsnval="${jsnval//\/[0-9][0-9]/}"
				jsnval="${jsnval//\/[0-9]/}"
				jsnval="\"${jsnval// /\", \"}\""
				if [ "${jsnval}" != '""' ]; then
					{
						printf '%s' ",[{}"
						"${ban_fetchcmd}" ${ban_geoparm} "[ ${jsnval} ]" "${ban_geourl}" 2>>"${ban_errorlog}" |
							"${ban_jsoncmd}" -qe '@[*&&@.status="success"]' | "${ban_awkcmd}" -v feed="homeIP" '{printf ",{\"%s\": %s}\n",feed,$0}'
					} >>"${map_jsn}"
				fi
			fi
			if [ -s "${map_jsn}" ]; then
				json_init
				if json_load_file "${report_jsn}" >/dev/null 2>&1; then
					json_select "sets" >/dev/null 2>&1
					json_get_keys table_sets >/dev/null 2>&1
					if [ -n "${table_sets}" ]; then
						for item in ${table_sets}; do
							[ "${item%%_*}" = "allowlist" ] && continue
							json_select "${item}"
							json_get_keys set_details
							for detail in ${set_details}; do
								if [ "${detail}" = "set_elements" ]; then
									json_get_values jsnval "${detail}" >/dev/null 2>&1
									jsnval="\"${jsnval// /\", \"}\""
								fi
							done
							if [ "${jsnval}" != '""' ]; then
								(
									quantity="0"
									chunk=""
									for ip in ${jsnval}; do
										chunk="${chunk} ${ip}"
										quantity="$((quantity + 1))"
										if [ "${quantity}" -eq "100" ]; then
											"${ban_fetchcmd}" ${ban_geoparm} "[ ${chunk} ]" "${ban_geourl}" 2>>"${ban_errorlog}" |
												"${ban_jsoncmd}" -qe '@[*&&@.status="success"]' | "${ban_awkcmd}" -v feed="${item//_v/.v}" '{printf ",{\"%s\": %s}\n",feed,$0}' >"${map_jsn}.${item}"
											chunk=""
											quantity="0"
										fi
									done
									if [ "${quantity}" -gt "0" ]; then
										"${ban_fetchcmd}" ${ban_geoparm} "[ ${chunk} ]" "${ban_geourl}" 2>>"${ban_errorlog}" |
											"${ban_jsoncmd}" -qe '@[*&&@.status="success"]' | "${ban_awkcmd}" -v feed="${item//_v/.v}" '{printf ",{\"%s\": %s}\n",feed,$0}' >>"${map_jsn}.${item}"
									fi
								) &
								[ "${cnt}" -gt "${ban_cores}" ] && wait -n
								cnt="$((cnt + 1))"
							fi
							json_select ".."
						done
						wait

						# assemble map data from per-set fragments
						#
						for item in ${table_sets}; do
							if [ -s "${map_jsn}.${item}" ]; then
								"${ban_catcmd}" "${map_jsn}.${item}" >>"${map_jsn}"
							fi
							"${ban_rmcmd}" -f "${map_jsn}.${item}"
						done
					fi
				fi
			fi
		fi

		# text output preparation
		#
		if [ "${output}" != "json" ] && [ -s "${report_jsn}" ]; then
			json_init
			if json_load_file "${report_jsn}" >/dev/null 2>&1; then
				json_get_var timestamp "timestamp" >/dev/null 2>&1
				json_get_var autoadd_allow "autoadd_allow" >/dev/null 2>&1
				json_get_var autoadd_block "autoadd_block" >/dev/null 2>&1
				json_get_var sum_synflood "sum_synflood" >/dev/null 2>&1
				json_get_var sum_udpflood "sum_udpflood" >/dev/null 2>&1
				json_get_var sum_icmpflood "sum_icmpflood" >/dev/null 2>&1
				json_get_var sum_ctinvalid "sum_ctinvalid" >/dev/null 2>&1
				json_get_var sum_tcpinvalid "sum_tcpinvalid" >/dev/null 2>&1
				json_get_var sum_bcp38 "sum_bcp38" >/dev/null 2>&1
				json_get_var sum_sets "sum_sets" >/dev/null 2>&1
				json_get_var sum_setinbound "sum_setinbound" >/dev/null 2>&1
				json_get_var sum_setoutbound "sum_setoutbound" >/dev/null 2>&1
				json_get_var sum_cntelements "sum_cntelements" >/dev/null 2>&1
				json_get_var sum_cntinbound "sum_cntinbound" >/dev/null 2>&1
				json_get_var sum_cntoutbound "sum_cntoutbound" >/dev/null 2>&1
				json_get_var sum_setports "sum_setports" >/dev/null 2>&1
				json_get_var sum_setelements "sum_setelements" >/dev/null 2>&1
				{
					printf '%s\n%s\n%s\n' ":::" "::: banIP Set Statistics" ":::"
					printf '%s\n' "    Timestamp: ${timestamp}"
					printf '%s\n' "    ------------------------------"
					printf '%s\n' "    blocked syn-flood packets  : ${sum_synflood}"
					printf '%s\n' "    blocked udp-flood packets  : ${sum_udpflood}"
					printf '%s\n' "    blocked icmp-flood packets : ${sum_icmpflood}"
					printf '%s\n' "    blocked invalid ct packets : ${sum_ctinvalid}"
					printf '%s\n' "    blocked invalid tcp packets: ${sum_tcpinvalid}"
					printf '%s\n' "    blocked bcp38 packets      : ${sum_bcp38}"
					printf '%s\n' "    ---"
					printf '%s\n' "    auto-added IPs to allowlist: ${autoadd_allow}"
					printf '%s\n\n' "    auto-added IPs to blocklist: ${autoadd_block}"
					json_select "sets" >/dev/null 2>&1
					json_get_keys table_sets >/dev/null 2>&1
					table_sets="$(printf '%s\n' ${table_sets} | "${ban_sortcmd}")"
					if [ -n "${table_sets}" ]; then
						printf '%-25s%-15s%-24s%-24s%-24s%-24s\n' "    Set" "| Count   " "| Inbound (packets)" "| Outbound (packets)" "| Port/Protocol      " "| Elements (max. 50) "
						printf '%s\n' "    ---------------------+--------------+-----------------------+-----------------------+-----------------------+------------------------"
						for item in ${table_sets}; do
							printf '    %-21s' "${item//_v/.v}"
							json_select "${item}"
							json_get_keys set_details
							for detail in ${set_details}; do
								case "${detail}" in
								"cnt_elements")
									json_get_var jsnval "${detail}" >/dev/null 2>&1
									printf '%-15s' "| ${jsnval}"
									;;
								"cnt_inbound" | "cnt_outbound")
									json_get_var jsnval "${detail}" >/dev/null 2>&1
									[ -n "${jsnval}" ] && tmp_val=": ${jsnval}"
									;;
								"set_elements")
									json_get_values jsnval "${detail}" >/dev/null 2>&1
									jsnval="${jsnval// /, }"
									printf '%-24s' "| ${jsnval:0:24}"
									jsnval="${jsnval:24}"
									while [ -n "${jsnval}" ]; do
										printf '\n%-25s%-15s%-24s%-24s%-24s%-24s' "" "|" "|" "|" "|" "| ${jsnval:0:24}"
										jsnval="${jsnval:24}"
									done
									;;
								*)
									json_get_var jsnval "${detail}" >/dev/null 2>&1
									printf '%-24s' "| ${jsnval}${tmp_val}"
									tmp_val=""
									;;
								esac
							done
							printf '\n'
							json_select ".."
						done
						printf '%s\n' "    ---------------------+--------------+-----------------------+-----------------------+-----------------------+------------------------"
						printf '%-25s%-15s%-24s%-24s%-24s%-24s\n' "    ${sum_sets}" "| ${sum_cntelements}" "| ${sum_setinbound} (${sum_cntinbound})" "| ${sum_setoutbound} (${sum_cntoutbound})" "| ${sum_setports}" "| ${sum_setelements}"
					fi
				} >>"${report_txt}"
			fi
		fi
	fi

	# output channel (text|json|mail)
	#
	case "${output}" in
	"text")
		[ -s "${report_txt}" ] && "${ban_catcmd}" "${report_txt}"
		;;
	"json")
		if [ "${ban_nftcount}" = "1" ] && [ "${ban_map}" = "1" ]; then
			jsn="$("${ban_catcmd}" ${report_jsn} ${map_jsn} 2>>"${ban_errorlog}")"
			[ -n "${jsn}" ] && printf '[%s]]\n' "${jsn}"
		else
			jsn="$("${ban_catcmd}" ${report_jsn} 2>>"${ban_errorlog}")"
			[ -n "${jsn}" ] && printf '[%s]\n' "${jsn}"
		fi
		;;
	"mail")
		[ -n "${ban_mailreceiver}" ] && [ -x "${ban_mailcmd}" ] && f_mail
		;;
	"gen")
		printf '%s\n' "1" >"${ban_rundir}/banIP.report"
		;;
	esac
	: >"${report_txt}"
}

f_search() {
	local item table_sets ip proto cnt tmp_result result res input="${1}"

	# prepare result file
	#
	tmp_result="${ban_rundir}/banIP.search.tmp"
	result="${ban_rundir}/banIP.search"

	# validate input
	#
	case "${input}" in
	'' | *[!0-9A-Fa-f:/.]*)
		printf '%s\n%s\n%s\n' ":::" "::: no valid search input" ":::"
		printf '%s\n%s\n%s\n' ":::" "::: no valid search input" ":::" >"${result}"
		return
		;;
	esac

	# determine protocol via awk
	#
	res="$(printf '%s' "${input}" | "${ban_awkcmd}" '
	{
		if (match($0,/(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(\/([0-9]|[12][0-9]|3[012]))?[[:space:]]*$/)) {
			printf "v4 %s",substr($0,RSTART,RLENGTH)
		} else if (match($0,/(([0-9A-Fa-f]{0,4}:){1,7}[0-9A-Fa-f]{0,4}:?(\/([0-9]|[1-9][0-9]|1[01][0-9]|12[0-8]))?)/)) {
			printf "v6 %s",substr($0,RSTART,RLENGTH)
		}
	}')"
	proto="${res%% *}"
	ip="${res#* }"
	[ "${proto}" != "v4" ] && [ "${proto}" != "v6" ] && proto="" && ip=""

	# get relevant Sets
	#
	if [ -n "${proto}" ]; then
		table_sets="$("${ban_nftcmd}" -tj list table inet banIP 2>>"${ban_errorlog}" |
			"${ban_jsoncmd}" -qe "@.nftables[@.set.type=\"ip${proto}_addr\"].set.name")"
	else
		printf '%s\n%s\n%s\n' ":::" "::: no valid search input" ":::"
		printf '%s\n%s\n%s\n' ":::" "::: no valid search input" ":::" >"${result}"
		return
	fi

	# initial output
	#
	{
		printf '%s\n%s\n%s\n' ":::" "::: banIP Search" ":::"
		printf '    %s\n' "Looking for IP '${ip}' on $(date "+%Y-%m-%d %H:%M:%S")"
		printf '    %s\n' "---"
	} >"${tmp_result}"

	# search for IP in Sets
	#
	cnt="1"
	for item in ${table_sets}; do
		case "${item}" in
		*[!a-zA-Z0-9_.]*)
			continue
			;;
		esac
		(
			if "${ban_nftcmd}" get element inet banIP "${item}" "{ ${ip} }" >/dev/null 2>&1; then
				printf '    %s\n' "IP found in Set '${item}'" >"${tmp_result}.${item}"
			fi
		) &
		[ "${cnt}" -gt "${ban_cores}" ] && wait -n
		cnt="$((cnt + 1))"
	done
	wait

	# assemble search results from per-set fragments
	#
	for item in ${table_sets}; do
		if [ -s "${tmp_result}.${item}" ]; then
			"${ban_catcmd}" "${tmp_result}.${item}" >>"${tmp_result}"
			"${ban_rmcmd}" -f "${tmp_result}.${item}"
		fi
	done

	# output result
	#
	if ! "${ban_grepcmd}" -qm1 "found" "${tmp_result}"; then
		printf '    %s\n' "IP not found" >>"${tmp_result}"
	fi
	"${ban_mvcmd}" -f "${tmp_result}" "${result}"
	"${ban_catcmd}" "${result}"
}

# Set content
#
f_content() {
	local set_raw set_elements input="${1}" filter="${2}"

	# load config if not already done
	#
	[ -z "${ban_dev}" ] && f_conf

	# validate input
	#
	case "${input}" in
	"" | *[!a-zA-Z0-9_.]*)
		printf '%s\n%s\n%s\n' ":::" "::: no valid Set input" ":::"
		return
		;;
	esac

	case "${filter}" in
	"" | "false")
		filter="false"
		;;
	"true")
		filter="true"
		;;
	*)
		printf '%s\n%s\n%s\n' ":::" "::: no valid filter input" ":::"
		return
		;;
	esac

	# check if Set exists
	#
	if ! "${ban_nftcmd}" -t list set inet banIP "${input}" >/dev/null 2>&1; then
		printf '%s\n%s\n%s\n' ":::" "::: Set '${input}' not found" ":::"
		return
	fi

	# get Set content
	#
	set_raw="$("${ban_nftcmd}" -j list set inet banIP "${input}" 2>>"${ban_errorlog}")"
	if [ "${ban_nftcount}" = "1" ]; then
		if [ "${filter}" = "true" ]; then
			set_elements="$(printf '%s' "${set_raw}" | "${ban_jsoncmd}" -qe '@.nftables[*].set.elem[*][@.counter.packets>0].*' |
				"${ban_awkcmd}" 'NR%2==1{ip=$0;next}BEGIN{FS="[:,{}\"]+"}{print ip ", packets: "$4 }')"
		else
			set_elements="$(printf '%s' "${set_raw}" | "${ban_jsoncmd}" -qe '@.nftables[*].set.elem[*].elem["val","counter"]' |
				"${ban_awkcmd}" 'NR%2==1{ip=$0;next}BEGIN{FS="[:,{}\"]+"}{print ip ", packets: "$4 }')"
		fi
	else
		set_elements="$(printf '%s' "${set_raw}" | "${ban_jsoncmd}" -qe '@.nftables[*].set.elem[*]')"
	fi

	# output result
	#
	printf '%s\n%s\n%s\n' ":::" "::: banIP Set Content" ":::"
	printf '    %s\n' "List elements of the Set '${input}' on $(date "+%Y-%m-%d %H:%M:%S")"
	printf '    %s\n' "---"
	[ -n "${set_elements}" ] && printf '%s\n' "${set_elements}" || printf '    %s\n' "no elements in Set"
}

# send status mail
#
f_mail() {
	local msmtp_debug

	# load mail template
	#
	if [ -r "${ban_mailtemplate}" ]; then
		. "${ban_mailtemplate}"
	else
		f_log "info" "no mail template"
	fi
	[ -z "${mail_text}" ] && f_log "info" "no mail content"
	[ "${ban_debug}" = "1" ] && msmtp_debug="--debug"

	# send mail
	#
	ban_mailhead="From: ${ban_mailsender}\nTo: ${ban_mailreceiver}\nSubject: ${ban_mailtopic}\nReply-to: ${ban_mailsender}\nMime-Version: 1.0\nContent-Type: text/html;charset=utf-8\nContent-Disposition: inline\n\n"
	printf '%b' "${ban_mailhead}${mail_text}" | "${ban_mailcmd}" --timeout=10 ${msmtp_debug} -a "${ban_mailprofile}" "${ban_mailreceiver}" >/dev/null 2>&1

	f_log "debug" "f_mail    ::: notification: ${ban_mailnotification}, template: ${ban_mailtemplate}, profile: ${ban_mailprofile}, receiver: ${ban_mailreceiver}, rc: ${?}"
}

# log monitor
#
f_monitor() {
	local nft_expiry ip proto idx base cidr rdap_log rdap_rc rdap_idx rdap_info log_type allow_v4 allow_v6 block_v4 block_v6
	local file cache_ts date_stamp time_now time_elapsed cache_interval rdap_interval rdap_tsfile rdap_lock rdap_jobs
	local rdap_ts block_cache block_cache_limit block_cache_cnt

	# intervals for periodic cache refresh and RDAP queries
	#
	cache_interval=300
	rdap_interval=2
	rdap_tsfile="${ban_rundir}/banIP_rdap_ts"
	printf '%s' "0" >"${rdap_tsfile}"

	# determine log reader type
	#
	if [ -f "${ban_logreadfile}" ] && [ -x "${ban_logreadcmd}" ] && [ "${ban_logreadcmd##*/}" = "tail" ]; then
		log_type="tail"
	elif [ -x "${ban_logreadcmd}" ] && [ "${ban_logreadcmd##*/}" = "logread" ]; then
		log_type="logread"
	fi

	# start log monitoring
	#
	if [ -n "${log_type}" ] && [ -n "${ban_logterm}" ] && [ "${ban_loglimit}" != "0" ]; then
		f_log "info" "start detached banIP log service (${ban_logreadcmd})"

		# determine nft timeout expression and cache interval
		#
		if printf '%s' "${ban_nftexpiry}" | grep -qE '^([1-9][0-9]*(ms|s|m|h|d|w))+$'; then
			nft_expiry="timeout ${ban_nftexpiry}"
			cache_interval="$(printf '%s' "${ban_nftexpiry}" | "${ban_awkcmd}" '{
				s = 0
				str = $0
				while (match(str, /([0-9]+)(ms|s|m|h|d|w)/, a)) {
					if      (a[2] == "ms") s += a[1] / 1000
					else if (a[2] == "s")  s += a[1]
					else if (a[2] == "m")  s += a[1] * 60
					else if (a[2] == "h")  s += a[1] * 3600
					else if (a[2] == "d")  s += a[1] * 86400
					else if (a[2] == "w")  s += a[1] * 604800
					str = substr(str, RSTART + RLENGTH)
				}
				interval = int(s / 2)
				if (interval < 30) interval = 30
				if (interval > 300) interval = 300
				printf "%d", interval
			}')"
		fi

		# helper function to extract space-padded bare IPs/CIDRs from nft set listing
		#
		nft_cache() {
			"${ban_nftcmd}" list set inet banIP "${1}" 2>/dev/null |
				"${ban_awkcmd}" '{gsub(/[,{}]/, " "); for(i=1;i<=NF;i++) if($i~/^[0-9A-Fa-f].*[.:]/) printf " %s ",$i}'
		}

		# retrieve/cache current allowlist/blocklist content
		#
		allow_v4="$(nft_cache allowlist.v4)"
		allow_v6="$(nft_cache allowlist.v6)"
		block_v4="$(nft_cache blocklist.v4)"
		block_v6="$(nft_cache blocklist.v6)"

		# initial cache timestamp and datestamp
		#
		read -r cache_ts _ <"/proc/uptime"
		cache_ts="${cache_ts%%.*}"
		date_stamp="$(date "+%Y-%m-%d %H:%M:%S")"
		block_cache=""
		block_cache_limit="500"
		block_cache_cnt="0"

		# clean up stale RDAP lock/done markers from previous runs
		#
		"${ban_rmcmd}" -f "${ban_rdapfile}".*

		# log monitoring loop
		# awk handles IP extraction, counting and threshold detection internally,
		# only IPs reaching ban_logcount are emitted as "BLOCK ip proto" to the shell loop
		#
		{
			case "${log_type}" in
			tail)
				"${ban_logreadcmd}" -qf "${ban_logreadfile}" 2>/dev/null |
					"${ban_grepcmd}" -e "${ban_logterm}" 2>/dev/null
				;;
			logread)
				"${ban_logreadcmd}" -fe "${ban_logterm}" 2>/dev/null
				;;
			esac
		} | "${ban_awkcmd}" -v threshold="${ban_logcount}" -v limit=5000 '
			BEGIN {
				unique = 0
			}
			{
				gsub(/[<>[\]]/, "", $0)
				sub(/%.*/, "", $0)
				sub(/:[0-9]+([ >]|$)/, "\\1", $0)
				ip = ""
				proto = ""
				if (match($0, /([0-9]{1,3}\.){3}[0-9]{1,3}/, m4)) {
					if (m4[0] !~ /^127\./ && m4[0] !~ /^0\./) {
						ip = m4[0]
						proto = ".v4"
					}
				}
				if (!ip && match($0, /([A-Fa-f0-9]{1,4}:){2,7}[A-Fa-f0-9]{1,4}/, m6)) {
					if (m6[0] !~ /^[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}$/ && m6[0] !~ /^([A-Fa-f0-9]{2}:){5}[A-Fa-f0-9]{2}$/) {
						ip = m6[0]
						proto = ".v6"
					}
				}
				if (!ip) {
					next
				}
				cnt[ip]++
				if (cnt[ip] == 1) {
					unique++
					if (unique >= limit) {
						delete cnt
						unique = 0
						print "RESET"
						fflush()
					}
				}
				if (cnt[ip] == threshold) {
					print "BLOCK " ip " " proto
					fflush()
					delete cnt[ip]
					unique--
				}
			}' | while read -r action ip proto; do

			# process only BLOCK/RESET actions emitted by awk
			#
			case "${action}" in
			BLOCK)
				f_log "debug" "f_monitor ::: block request for IP '${ip}' (protocol: IP${proto})"

				# periodic monitor cache refresh (only on BLOCK events to reduce /proc/uptime reads)
				#
				read -r time_now _ <"/proc/uptime"
				time_now="${time_now%%.*}"
				if [ "$((time_now - cache_ts))" -ge "${cache_interval}" ]; then
					block_v4="$(nft_cache blocklist.v4)"
					block_v6="$(nft_cache blocklist.v6)"
					date_stamp="$(date "+%Y-%m-%d %H:%M:%S")"
					cache_ts="${time_now}"
					block_cache=""
					block_cache_cnt="0"
					"${ban_rmcmd}" -f "${ban_rdapfile}".*.done
					f_log "debug" "f_monitor ::: refreshed monitor cache at ${date_stamp}"
				fi

				# fast exact string match against cached Set content
				#
				case "${proto}" in
				.v4)
					case "${allow_v4} ${block_v4} ${block_cache}" in
					*" ${ip} "*)
						f_log "debug" "f_monitor ::: skip IP '${ip}', found in cached IP${proto} Sets"
						continue
						;;
					esac
					;;
				.v6)
					case "${allow_v6} ${block_v6} ${block_cache}" in
					*" ${ip} "*)
						f_log "debug" "f_monitor ::: skip IP '${ip}', found in cached IP${proto} Sets"
						continue
						;;
					esac
					;;
				esac

				# CIDR-aware allowlist lookup (only at block-time, not every IP)
				#
				if "${ban_nftcmd}" get element inet banIP "allowlist${proto}" { ${ip} } >/dev/null 2>&1; then
					block_cache_cnt="$((block_cache_cnt + 1))"
					if [ "${block_cache_cnt}" -ge "${block_cache_limit}" ]; then
						block_cache=""
						block_cache_cnt="1"
						f_log "debug" "f_monitor ::: refreshed local monitor cache at ${date_stamp}"
					fi
					block_cache="${block_cache} ${ip} "
					f_log "debug" "f_monitor ::: skip IP '${ip}', found via allowlist CIDR lookup"
					continue
				fi

				# try to add IP to the blocklist Set with appropriate expiry
				#
				if "${ban_nftcmd}" add element inet banIP "blocklist${proto}" { ${ip} ${nft_expiry} } >/dev/null 2>&1; then
					block_cache_cnt="$((block_cache_cnt + 1))"
					if [ "${block_cache_cnt}" -ge "${block_cache_limit}" ]; then
						block_cache=""
						block_cache_cnt="1"
						f_log "debug" "f_monitor ::: refreshed local monitor cache at ${date_stamp}"
					fi
					block_cache="${block_cache} ${ip} "
					f_log "info" "add IP '${ip}' (cnt: ${ban_logcount}, expiry: ${ban_nftexpiry:-"0"}) to blocklist${proto} Set"
				else
					f_log "info" "failed to add IP '${ip}' to blocklist${proto} Set with rc '${?}'"
					continue
				fi

				# RDAP subnet lookup with rate limiting (background, non-blocking)
				#
				if [ "${ban_autoblocksubnet}" = "1" ]; then

					# per-IP dedup — skip if already in-flight or completed
					#
					rdap_lock="${ban_rdapfile}.${ip}.lock"
					if [ ! -f "${rdap_lock}" ] && [ ! -f "${ban_rdapfile}.${ip}.done" ]; then

						# global job limit — max. concurrent RDAP subshells (ban_cores),
						# to avoid excessive load and potential DoS against RDAP service when multiple IPs are blocked in a short time frame
						#
						rdap_jobs=0
						for file in "${ban_rdapfile}".*.lock; do
							[ -e "${file}" ] || continue
							rdap_jobs="$((rdap_jobs + 1))"
						done

						# only spawn new RDAP subshell if current number of in-flight RDAP lookups is below ban_cores limit
						#
						if [ "${rdap_jobs}" -lt "${ban_cores}" ]; then
							: >"${rdap_lock}"
							( 
								# rate limiting via shared timestamp file
								#
								(
									"${ban_flockcmd}" -x 9
									read -r rdap_ts <"${rdap_tsfile}" 2>/dev/null
									rdap_ts="${rdap_ts:-0}"
									read -r time_now _ <"/proc/uptime"
									time_now="${time_now%%.*}"
									time_elapsed=$((time_now - rdap_ts))
									if [ "${time_elapsed}" -lt "${rdap_interval}" ]; then
										sleep "$((rdap_interval - time_elapsed))"
									fi
									read -r rdap_ts _ <"/proc/uptime"
									rdap_ts="${rdap_ts%%.*}"
									printf '%s' "${rdap_ts}" >"${rdap_tsfile}"
								) 9>"${rdap_tsfile}.lock"
								: >"${ban_rdapfile}.${ip}"
								rdap_log="$("${ban_fetchcmd}" ${ban_rdapparm} "${ban_rdapfile}.${ip}" "${ban_rdapurl}${ip}" 2>&1)"
								rdap_rc="${?}"

								# process RDAP response if valid JSON with expected content, otherwise log error
								#
								if [ "${rdap_rc}" = "0" ] && [ -s "${ban_rdapfile}.${ip}" ]; then
									[ "${proto}" = ".v4" ] && rdap_idx="$("${ban_jsoncmd}" -i "${ban_rdapfile}.${ip}" -qe '@.cidr0_cidrs[@.v4prefix].*' | "${ban_awkcmd}" '{ORS=" "; print}')"
									[ "${proto}" = ".v6" ] && rdap_idx="$("${ban_jsoncmd}" -i "${ban_rdapfile}.${ip}" -qe '@.cidr0_cidrs[@.v6prefix].*' | "${ban_awkcmd}" '{ORS=" "; print}')"
									rdap_info="$("${ban_jsoncmd}" -l1 -i "${ban_rdapfile}.${ip}" -qe '@.country' -qe '@.notices[@.title="Source"].description[1]' | "${ban_awkcmd}" 'BEGIN{RS="";FS="\n"}{c=($1!=""?$1:"-"); s=($2!=""?$2:"-"); printf "%s, %s", c, s}')"
									[ -z "${rdap_info}" ] || [ "${rdap_info}" = "-, -" ] && rdap_info="$("${ban_jsoncmd}" -l1 -i "${ban_rdapfile}.${ip}" -qe '@.notices[0].links[0].value' | "${ban_awkcmd}" 'BEGIN{FS="[/.]"}{printf"%s, %s","n/a",toupper($4)}')"

									# if RDAP response contains (multiple) valid CIDR info,
									# attempt to add entire range to blocklist set with same expiry as individual IP
									#
									base=""
									for idx in ${rdap_idx}; do
										if [ -z "${base}" ]; then
											base="${idx}"
											continue
										else
											case "${base}" in
											"" | "::"* | "127."* | "0."* | "fe80:"*)
												base=""
												continue
												;;
											esac
											[ -z "${base}" ] && continue
											cidr="${base}/${idx}"
											if "${ban_nftcmd}" add element inet banIP "blocklist${proto}" { ${cidr} ${nft_expiry} } >/dev/null 2>&1; then
												f_log "info" "add IP range '${cidr}' (source: ${rdap_info:-"n/a"} ::: expiry: ${ban_nftexpiry:-"-"}) to blocklist${proto} set"
											fi
											base=""
										fi
									done
								else
									f_log "info" "rdap request failed (rc: ${rdap_rc:-"-"}/log: ${rdap_log:-"-"}) for IP '${ip}'"
								fi
								: >"${ban_rdapfile}.${ip}.done"
								"${ban_rmcmd}" -f "${ban_rdapfile}.${ip}" "${rdap_lock}"
							) &
						fi
					fi
				fi

				# persist to local blocklist file if no expiry
				#
				if [ -z "${ban_nftexpiry}" ] && [ "${ban_autoblocklist}" = "1" ] && ! "${ban_grepcmd}" -q "^${ip}[[:space:]]" "${ban_blocklist}"; then
					printf '%-45s%s\n' "${ip}" "# added on ${date_stamp}" >>"${ban_blocklist}"
					f_log "info" "add IP '${ip}' to local blocklist"
				fi
				;;
			RESET)
				f_log "debug" "monitor counter limit reached (5000 unique IPs), awk reset"
				;;
			esac
		done
	else

		# no valid log reader configuration, start detached no-op service to keep monitor option enabled
		#
		f_log "info" "start detached no-op banIP service"
		sleep infinity
	fi
}

# initial sourcing
#
if [ -r "/lib/functions.sh" ] && [ -r "/lib/functions/network.sh" ] && [ -r "/usr/share/libubox/jshn.sh" ]; then
	. "/lib/functions.sh"
	. "/lib/functions/network.sh"
	. "/usr/share/libubox/jshn.sh"
else
	f_log "emerg" "system libraries not found"
fi

# reference required system utilities
#
ban_awkcmd="$(f_cmd gawk)"
ban_catcmd="$(f_cmd cat)"
ban_grepcmd="$(f_cmd grep)"
ban_jsoncmd="$(f_cmd jsonfilter)"
ban_logcmd="$(f_cmd logger)"
ban_lookupcmd="$(f_cmd nslookup)"
ban_mailcmd="$(f_cmd msmtp optional)"
ban_nftcmd="$(f_cmd nft)"
ban_pgrepcmd="$(f_cmd pgrep)"
ban_xargscmd="$(f_cmd xargs)"
ban_flockcmd="$(f_cmd flock)"
ban_sedcmd="$(f_cmd sed)"
ban_ubuscmd="$(f_cmd ubus)"
ban_zcatcmd="$(f_cmd zcat)"
ban_gzipcmd="$(f_cmd gzip)"
ban_sortcmd="$(f_cmd sort)"
ban_wccmd="$(f_cmd wc)"
ban_mvcmd="$(f_cmd mv)"
ban_rmcmd="$(f_cmd rm)"

f_system
if [ "${ban_action}" != "stop" ]; then
	[ ! -d "/etc/banip" ] && f_log "err" "no banIP config directory"
	[ ! -r "/etc/config/banip" ] && f_log "err" "no banIP config"
	[ "$(uci_get banip global ban_enabled)" = "0" ] && f_log "err" "banIP is disabled"
fi
