Files
packages/net/banip/files/banip-functions.sh
T
Dirk Brenken faeecaeb14 banip: update 1.8.8-5
* bugfix: only load the configuration once per run: a new `ban_confload`
  guard short-circuits `f_conf()` on subsequent calls, avoiding
  repeated `config_load` invocations
* new: the per-set report now sorts elements by their packet counter in
  descending order before truncating to the top 50, so the report
  shows the most active elements instead of just the first 50 found

Signed-off-by: Dirk Brenken <dev@brenken.org>
2026-05-29 13:57:31 +02:00

2899 lines
99 KiB
Bash

# 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_confload="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>/dev/null)"
if [ -z "${cmd}" ]; then
if [ -n "${sec_cmd}" ]; then
[ "${sec_cmd}" = "optional" ] && return
cmd="$(command -v "${sec_cmd}" 2>>"${ban_errorlog}")"
fi
if [ -n "${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}" >&2
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
[ -s "${ban_rtfile}" ] && f_genstatus "error"
[ "${ban_mailnotification}" = "1" ] && [ -n "${ban_mailreceiver}" ] && [ -x "${ban_mailcmd}" ] && f_mail
else
[ -s "${ban_rtfile}" ] && 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
[ "${ban_confload}" = "1" ] && return 0
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
ban_confload="1"
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 feed="${feed}"
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}" 2>/dev/null)"
if [ -z "${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
ban_fetchcmd="$(command -v "${fetch}" 2>/dev/null)"
if [ -n "${ban_fetchcmd}" ]; then
update="1"
uci_set banip global ban_fetchcmd "${fetch}"
uci_commit "banip"
break
fi
;;
esac
done
fi
[ -z "${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 etag_match result 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)~/^[[:space:]]*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
#
result="$("${ban_awkcmd}" -v f="${feed}" -v s="${feed_suffix}" -v e="${etag_id}" '
BEGIN { p = f " " s; pl = length(p); m = 1 }
$1 == f { n++ }
index($0, p) == 1 {
rest = substr($0, pl + 1)
sub(/^[[:space:]]+/, "", rest)
if (rest == e) m = 0
}
END { print n+0, m }' "${ban_backupdir}/banIP.etag")"
etag_cnt="${result% *}"
etag_match="${result#* }"
if [ "${http_code}" = "200" ] && [ "${etag_cnt}" = "${feed_cnt}" ] && [ -n "${etag_id}" ] && [ "${etag_match}" = "0" ]; 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_awkcmd}" -v f="${feed}" '$1 != f' \
"${ban_backupdir}/banIP.etag" >"${ban_backupdir}/banIP.etag.new"
else
"${ban_awkcmd}" -v f="${feed}" -v s="${feed_suffix}" '
BEGIN { p = f " " s }
index($0, p) != 1' "${ban_backupdir}/banIP.etag" >"${ban_backupdir}/banIP.etag.new"
fi
"${ban_mvcmd}" -f "${ban_backupdir}/banIP.etag.new" "${ban_backupdir}/banIP.etag"
printf '%s\t%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
# 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
[ "${feed_rc}" != "0" ] && f_log "info" "processing for feed '${feed}' failed, rc: ${feed_rc:-"-"}"
# 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_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 domain lookup ip dom ts proto elementsv4 elementsv6 start_time end_time duration cnt_domain="0" cnt_ip="0" feed="${1}"
local record_file tmp_dir target_file auto_flag
# measure runtime of lookup function for performance insights
#
read -r start_time _ <"/proc/uptime"
start_time="${start_time%%.*}"
# prepare list of domains to lookup, target file for auto-adding new entries and auto-add flag based on feed type
#
if [ "${feed}" = "allowlist" ]; then
list="$("${ban_awkcmd}" '{gsub(/\r/,"")}/^([[:alnum:]_-]{1,63}\.)+[[:alpha:]]+([[:space:]]|$)/{printf "%s ",tolower($1)}' "${ban_allowlist}" 2>>"${ban_errorlog}")"
target_file="${ban_allowlist}"
auto_flag="${ban_autoallowlist}"
elif [ "${feed}" = "blocklist" ]; then
list="$("${ban_awkcmd}" '{gsub(/\r/,"")}/^([[:alnum:]_-]{1,63}\.)+[[:alpha:]]+([[:space:]]|$)/{printf "%s ",tolower($1)}' "${ban_blocklist}" 2>>"${ban_errorlog}")"
target_file="${ban_blocklist}"
auto_flag="${ban_autoblocklist}"
fi
# prepare temporary directory for parallel lookups
#
tmp_dir="${ban_tmpfile}.lookup.${feed}"
f_mkdir "${tmp_dir}"
# parallel DNS lookups: one record file per domain, network-bound work runs concurrently
#
cnt="1"
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}")"
[ -z "${lookup}" ] && exit 0
ts="$(date "+%Y-%m-%d %H:%M:%S")"
for ip in ${lookup}; do
if [ "${ip%%.*}" = "127" ] || [ "${ip%%.*}" = "0" ] || [ -z "${ip%%::*}" ]; then
continue
fi
if [ "${ip##*:}" = "${ip}" ]; then
printf 'v4 %s %s %s\n' "${ip}" "${domain}" "${ts}"
else
printf 'v6 %s %s %s\n' "${ip}" "${domain}" "${ts}"
fi
done >"${tmp_dir}/${cnt}"
) &
[ "${cnt}" -gt "${ban_cores}" ] && wait -n
cnt_domain="${cnt}"
cnt="$((cnt + 1))"
done
wait
# collect results: aggregate IPs, persist new entries serially (no append race)
#
for record_file in "${tmp_dir}"/*; do
[ -s "${record_file}" ] || continue
while read -r proto ip dom ts; do
cnt_ip="$((cnt_ip + 1))"
if [ "${proto}" = "v4" ]; then
elementsv4="${elementsv4} ${ip},"
else
elementsv6="${elementsv6} ${ip},"
fi
if [ "${auto_flag}" = "1" ] && ! "${ban_grepcmd}" -q "^${ip}[[:space:]]*#" "${target_file}"; then
printf "%-45s%s\n" "${ip}" "# '${dom}' added on ${ts}" >>"${target_file}"
fi
done <"${record_file}"
done
f_rmdir "${tmp_dir}"
# add resolved IPs to nftables Sets
#
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
# measure end time and log performance insights
#
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 table_jsn set_jsn map_jsn chain set_elements 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}"
[ "${output}" = "gen" ] && printf '%s\n' "0" >"${ban_rundir}/banIP.report"
table_jsn="${ban_rundir}/report.table.jsn"
"${ban_nftcmd}" -tj list table inet banIP 2>>"${ban_errorlog}" >"${table_jsn}"
table_sets="$("${ban_jsoncmd}" -i "${table_jsn}" -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="$("${ban_jsoncmd}" -i "${table_jsn}" -qe '@.nftables[@.counter.name="cnt_synflood"].*.packets')"
sum_udpflood="$("${ban_jsoncmd}" -i "${table_jsn}" -qe '@.nftables[@.counter.name="cnt_udpflood"].*.packets')"
sum_icmpflood="$("${ban_jsoncmd}" -i "${table_jsn}" -qe '@.nftables[@.counter.name="cnt_icmpflood"].*.packets')"
sum_ctinvalid="$("${ban_jsoncmd}" -i "${table_jsn}" -qe '@.nftables[@.counter.name="cnt_ctinvalid"].*.packets')"
sum_tcpinvalid="$("${ban_jsoncmd}" -i "${table_jsn}" -qe '@.nftables[@.counter.name="cnt_tcpinvalid"].*.packets')"
sum_bcp38="$("${ban_jsoncmd}" -i "${table_jsn}" -qe '@.nftables[@.counter.name="cnt_bcp38"].*.packets')"
timestamp="$(date "+%Y-%m-%d %H:%M:%S")"
cnt="1"
for item in ${table_sets}; do
(
set_jsn="${ban_rundir}/report.set.jsn.${item}"
"${ban_nftcmd}" -j list set inet banIP "${item}" 2>>"${ban_errorlog}" >"${set_jsn}"
set_cnt="$("${ban_jsoncmd}" -i "${set_jsn}" -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="$("${ban_jsoncmd}" -i "${table_jsn}" -ql1 -e "@.nftables[@.rule.chain=\"${chain}\"][@.expr[${expr}].match.right=\"@${item}\"].expr[*].counter.packets")"
elif [ "${chain}" = "_outbound" ] && [ -z "${set_cntoutbound}" ]; then
set_cntoutbound="$("${ban_jsoncmd}" -i "${table_jsn}" -ql1 -e "@.nftables[@.rule.chain=\"${chain}\"][@.expr[${expr}].match.right=\"@${item}\"].expr[*].counter.packets")"
fi
[ -z "${set_proto}" ] && set_proto="$("${ban_jsoncmd}" -i "${table_jsn}" -ql1 -e "@.nftables[@.rule.chain=\"${chain}\"][@.expr[2].match.right=\"@${item}\"].expr[0].match.right.set")"
[ -z "${set_proto}" ] && set_proto="$("${ban_jsoncmd}" -i "${table_jsn}" -ql1 -e "@.nftables[@.rule.chain=\"${chain}\"][@.expr[1].match.right=\"@${item}\"].expr[0].match.left.payload.protocol")"
[ -z "${set_dport}" ] && set_dport="$("${ban_jsoncmd}" -i "${table_jsn}" -ql1 -e "@.nftables[@.rule.chain=\"${chain}\"][@.expr[2].match.right=\"@${item}\"].expr[1].match.right.set")"
[ -z "${set_dport}" ] && set_dport="$("${ban_jsoncmd}" -i "${table_jsn}" -ql1 -e "@.nftables[@.rule.chain=\"${chain}\"][@.expr[2].match.right=\"@${item}\"].expr[1].match.right")"
[ -z "${set_dport}" ] && set_dport="$("${ban_jsoncmd}" -i "${table_jsn}" -ql1 -e "@.nftables[@.rule.chain=\"${chain}\"][@.expr[1].match.right=\"@${item}\"].expr[0].match.right.set")"
[ -z "${set_dport}" ] && set_dport="$("${ban_jsoncmd}" -i "${table_jsn}" -ql1 -e "@.nftables[@.rule.chain=\"${chain}\"][@.expr[1].match.right=\"@${item}\"].expr[0].match.right")"
done
[ -n "${set_cntinbound}" ] && [ -n "${set_cntoutbound}" ] && [ -n "${set_proto}" ] && [ -n "${set_dport}" ] && break
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
"${ban_jsoncmd}" -i "${set_jsn}" -qe '@.nftables[*].set.elem[*][@.counter.packets>0].counter.packets' >"${set_jsn}.cnt"
"${ban_jsoncmd}" -i "${set_jsn}" -qe '@.nftables[*].set.elem[*][@.counter.packets>0].val' >"${set_jsn}.val"
set_elements="$("${ban_awkcmd}" 'NR==FNR{p[FNR]=$0;next}{print p[FNR]"\t"$0}' "${set_jsn}.cnt" "${set_jsn}.val" |
"${ban_sortcmd}" -k1,1nr |
"${ban_awkcmd}" -F '\t' 'NR<=50{split($2,a,/[ ,]/);ORS=" ";if(a[2]=="\"range\":"||a[2]=="\"concat\":")printf"%s, ",a[4];else if(a[2]=="\"prefix\":")printf"%s, ",a[5];else printf"\"%s\", ",a[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}"
"${ban_rmcmd}" -f "${set_jsn}"*
) &
[ "${cnt}" -gt "${ban_cores}" ] && wait -n
cnt="$((cnt + 1))"
done
wait
"${ban_rmcmd}" -f "${table_jsn}"
# 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
}
# 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)"
# 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
# initial system check
#
[ -S "/var/run/ubus/ubus.sock" ] && f_system
if [ -n "${ban_action}" ] && [ "${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