# 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_logterm_map="" 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_monitorallowed="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_uplink_add="" ban_uplink_del="" ban_devup="" ban_fetchcmd="" ban_fetchparm="" ban_fetchinsecure="" ban_fetchretry="5" ban_rdapparm="" ban_etagparm="" ban_geoparm="" ban_cores="" ban_srtmem="8" ban_srtopts="" ban_packages="" ban_trigger="" ban_resolver="" ban_enabled="0" ban_confload="0" ban_debug="0" # 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 } # determine available system memory (MemAvailable) in MB # mode "float" returns MiB with two decimals, default is integer MiB # f_mem() { local mem mode="${1}" if [ "${mode}" = "float" ]; then mem="$("${ban_awkcmd}" '/^MemAvailable/{printf "%.2f", $2/1024}' "/proc/meminfo" 2>>"${ban_errorlog}")" else mem="$("${ban_awkcmd}" '/^MemAvailable/{printf "%s", int($2/1024)}' "/proc/meminfo" 2>>"${ban_errorlog}")" fi printf '%s' "${mem:-"0"}" } # gather system information # f_system() { local free_mem mem_cores 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}')" # detect cpu cores and available memory for memory-aware parallel processing # 'mem_cores' is only calculated for auto-detected cores, a manually set 'ban_cores' is never capped # free_mem="$(f_mem)" if [ -z "${ban_cores}" ]; then ban_cores="$("${ban_grepcmd}" -cm16 '^processor' /proc/cpuinfo 2>>"${ban_errorlog}")" mem_cores="$((free_mem / 48))" [ "${mem_cores}" -lt "1" ] && mem_cores="1" fi case "${ban_cores}" in "" | 0 | *[!0-9]*) ban_cores="1" ;; esac [ -n "${mem_cores}" ] && [ "${mem_cores}" -lt "${ban_cores}" ] && ban_cores="${mem_cores}" # derive the GNU sort buffer from available memory (>= 8 MiB per core); # only applied when a coreutils sort is present (busybox sort has no --buffer-size) # ban_srtmem="$((free_mem / 2 / ban_cores))" [ "${ban_srtmem}" -lt "8" ] && ban_srtmem="8" if "${ban_sortcmd}" --version 2>/dev/null | "${ban_grepcmd}" -q "coreutils"; then ban_srtopts="--buffer-size=${ban_srtmem}M" 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_]*) ;; *) [ -n "${value}" ] && eval "${option}=\"\${value}\"" ;; esac } list_cb() { local anchor pat append option="${1}" value="${2//\"/\\\"}" case "${option}" in *[!a-zA-Z0-9_]*) ;; "ban_logterm") case "${value}" in first:*) anchor="first" pat="${value#first:}" ;; last:*) anchor="last" pat="${value#last:}" ;; *) anchor="last" pat="${value}" ;; esac eval "append=\"\${${option}}\"" if [ -n "${append}" ]; then eval "${option}=\"\${append}\\|${pat}\"" else eval "${option}=\"${pat}\"" fi ban_logterm_map="${ban_logterm_map}${anchor}$(printf '\037')${pat}$(printf '\036')" ;; *) 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" # check if the configured fetch utility is available and has SSL support, # if not try to find an alternative with SSL support or log an error if not found # 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" # check the fetch retry value # case "${ban_fetchretry}" in 0* | *[!0-9]*) ban_fetchretry="5" ;; esac # set fetch parameters based on the fetch utility and check if insecure fetching is enabled # 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 --globoff --show-error --location -o"}" ban_rdapparm="--connect-timeout 5 --silent --globoff --location -o" ban_etagparm="--connect-timeout 5 --silent --globoff --location --head" ban_geoparm="--connect-timeout 5 --silent --globoff --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 dev iface timestamp ip old ban_uplink="" ban_uplink_add="" ban_uplink_del="" ban_devup="" # single pass over the wan interfaces, collects the current # devices and uplink addresses # network_flush_cache for iface in ${ban_ifv4} ${ban_ifv6}; do network_get_device dev "${iface}" if [ -n "${dev}" ]; then case " ${ban_devup} " in *" ${dev} "*) ;; *) ban_devup="${ban_devup}${dev} " ;; esac fi [ "${ban_autoallowlist}" = "1" ] && [ "${ban_autoallowuplink}" != "disable" ] || continue 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}")" if [ "${ban_autoallowlist}" = "1" ] && [ "${ban_autoallowuplink}" != "disable" ]; then # compare the detected uplink with the local allowlist and # track the differences for an in-place refresh (see f_refresh) # if [ -n "${ban_uplink}" ]; then for ip in $("${ban_sedcmd}" -n "/# uplink added on /s/[[:space:]].*$//p" "${ban_allowlist}" 2>/dev/null); do old="${old}${ip} " done for ip in ${old}; do case " ${ban_uplink} " in *" ${ip} "*) ;; *) ban_uplink_del="${ban_uplink_del}${ip} " ;; esac done for ip in ${ban_uplink}; do case " ${old} " in *" ${ip} "*) ;; *) ban_uplink_add="${ban_uplink_add}${ip} " ;; esac done if [ -n "${ban_uplink_add}" ] || [ -n "${ban_uplink_del}" ]; then "${ban_sedcmd}" -i "/# uplink added on /d" "${ban_allowlist}" timestamp="$(date "+%Y-%m-%d %H:%M:%S")" for ip in ${ban_uplink}; do printf '%-45s%s\n' "${ip}" "# uplink added on ${timestamp}" >>"${ban_allowlist}" done for ip in ${ban_uplink_add}; do f_log "info" "add uplink '${ip}' to local allowlist" done for ip in ${ban_uplink_del}; do f_log "info" "remove uplink '${ip}' from local allowlist" done fi fi elif [ "${ban_autoallowlist}" = "1" ] && [ "${ban_autoallowuplink}" = "disable" ]; then if "${ban_grepcmd}" -q "# uplink added on " "${ban_allowlist}"; then "${ban_sedcmd}" -i "/# uplink added on /d" "${ban_allowlist}" fi fi f_log "debug" "f_getup ::: auto-allow/auto-uplink: ${ban_autoallowlist}/${ban_autoallowuplink}, devices: ${ban_devup:-"-"}, uplink: ${ban_uplink:-"-"}, add/remove: ${ban_uplink_add:-"-"}/${ban_uplink_del:-"-"}" } # refresh the wan state in place, triggered by an interface event # return 0 if handled, 1 to escalate to a full service run # f_refresh() { local dev ip addv4 addv6 delv4 delv6 set_list set_name # require an initialized nft namespace # "${ban_nftcmd}" list chain inet banIP pre-routing >/dev/null 2>&1 || return 1 f_getup # escalate on new or renamed wan devices, the rulesets match on # ban_dev - a device that is merely gone means the interface is # currently down, that is handled by the uplink diff below # for dev in ${ban_devup}; do case " ${ban_dev} " in *" ${dev} "*) ;; *) return 1 ;; esac done [ -z "${ban_uplink_add}" ] && [ -z "${ban_uplink_del}" ] && return 0 # update the allowlist Sets in a single atomic transaction, # a rejected batch escalates to a full run # for ip in ${ban_uplink_del}; do if [ "${ip##*:}" = "${ip}" ]; then delv4="${delv4}${ip}, " else delv6="${delv6}${ip}, " fi done for ip in ${ban_uplink_add}; do if [ "${ip##*:}" = "${ip}" ]; then addv4="${addv4}${ip}, " else addv6="${addv6}${ip}, " fi done set_list="allowlist" if [ "${ban_allowlistonly}" = "1" ] && [ "${ban_monitorallowed}" = "1" ]; then set_list="${set_list} allowlist.local" fi if ! { for set_name in ${set_list}; do [ -n "${delv4}" ] && printf 'delete element inet banIP %s.v4 { %s }\n' "${set_name}" "${delv4%, }" [ -n "${delv6}" ] && printf 'delete element inet banIP %s.v6 { %s }\n' "${set_name}" "${delv6%, }" [ -n "${addv4}" ] && printf 'add element inet banIP %s.v4 { %s }\n' "${set_name}" "${addv4%, }" [ -n "${addv6}" ] && printf 'add element inet banIP %s.v6 { %s }\n' "${set_name}" "${addv6%, }" done } | "${ban_nftcmd}" -f - >/dev/null 2>&1; then return 1 fi f_log "debug" "f_refresh ::: devices: ${ban_devup}, uplink: ${ban_uplink}" return 0 } # get feed information # f_getfeed() { local feedlist quiet="${1}" json_init if [ -s "${ban_customfeedfile}" ]; then if json_load_file "${ban_customfeedfile}" >/dev/null 2>&1; then json_get_keys feedlist if [ -n "${feedlist}" ]; then [ -z "${quiet}" ] && f_log "info" "banIP custom feed file loaded successfully" return fi fi fi if [ -s "${ban_feedfile}" ] && json_load_file "${ban_feedfile}" >/dev/null 2>&1; then [ -z "${quiet}" ] && f_log "info" "banIP default feed file loaded successfully" 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}") };" } # resolve the chain direction of a feed, # an explicitly configured direction always wins over the feed default # f_direction() { local feed_name="${1}" feed_chain="${2}" if [ "${feed_chain}" = "none" ]; then printf '%s' "none" return 0 fi case " ${ban_feedin} " in *" ${feed_name} "*) printf '%s' "inbound" return 0 ;; esac case " ${ban_feedout} " in *" ${feed_name} "*) printf '%s' "outbound" return 0 ;; esac case " ${ban_feedinout} " in *" ${feed_name} "*) printf '%s' "inbound outbound" return 0 ;; esac case "${feed_chain}" in "in") printf '%s' "inbound" ;; "out") printf '%s' "outbound" ;; "inout") printf '%s' "inbound outbound" ;; *) printf '%s' "inbound" ;; esac } # check whether an external feed is redundant in allowlist-only mode # f_skipfeed() { local direction allow_direction feed_direction="${1}" [ "${ban_allowlistonly}" != "1" ] && return 1 allow_direction="$(f_direction "allowlist" "inout")" for direction in ${feed_direction}; do case " ${allow_direction} " in *" ${direction} "*) ;; *) return 1 ;; esac done return 0 } # build a country/asn feed url from a template # f_feedurl() { local url="${1}" key="${2}" value="${3}" case "${url}" in *"{${key}}"*) printf '%s%s%s' "${url%%"{${key}}"*}" "${value}" "${url#*"{${key}}"}" ;; *) case "${key}" in "country") printf '%s%s-aggregated.zone' "${url}" "${value}" ;; "asn") printf '%sAS%s' "${url}" "${value}" ;; esac ;; esac } # 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\.]+ /{code=$2} END{printf "%s",code}')" etag_id="$(printf '%s' "${http_head}" | "${ban_awkcmd}" 'tolower($0)~/^[[:space:]]*etag: /{gsub(/[\r"]/,"");id=$2} END{printf "%s",id}')" # 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:]]|,|:/,"");lm=$1} END{printf "%s",lm}')" 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%%.*}" feed_direction="$(f_direction "${feed_name}" "${feed_chain}")" case "${feed_direction}" in "inbound") feed_policy="in" ;; "outbound") feed_policy="out" ;; "inbound outbound") feed_policy="inout" ;; *) feed_policy="${feed_direction}" ;; esac # skip external feeds which are already covered by the allowlist in allowlist-only mode # case "${feed_name}" in "allowlist" | "blocklist") ;; *) if f_skipfeed "${feed_direction}"; then f_log "info" "skip feed '${feed}' in allowlistonly mode" return 0 fi ;; esac # 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" ] && [ "${feed%.*}" != "allowlist.local" ]; 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}" "$(f_feedurl "${feed_url}" "country" "${country}")" ".${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}" "$(f_feedurl "${feed_url}" "country" "${country}")" ".${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}" "$(f_feedurl "${feed_url}" "asn" "${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}" "$(f_feedurl "${feed_url}" "asn" "${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" ]; then if [ ! -f "${tmp_allow}" ]; then f_restore "allowlist" "-" "${tmp_allow}" "${etag_rc}" restore_rc="${?}" feed_rc="${restore_rc}" fi else f_restore "${feed}" "${feed_url}" "${tmp_load}" "${etag_rc}" restore_rc="${?}" feed_rc="${restore_rc}" fi fi fi # prepare the monitor-only allowlist, local entries only # if [ "${feed%.*}" = "allowlist.local" ] && [ ! -f "${tmp_allow}" ]; then "${ban_catcmd}" "${ban_allowlist}" 2>>"${ban_errorlog}" >"${tmp_allow}" feed_rc="${?}" 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}" "$(f_feedurl "${feed_url}" "country" "${country}")" 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}" "$(f_feedurl "${feed_url}" "country" "${country}")" 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}" "$(f_feedurl "${feed_url}" "asn" "${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}" "$(f_feedurl "${feed_url}" "asn" "${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 chain feed_chain feed_covered feed_rc f_getfeed "quiet" 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 if [ "${feed%.*}" = "allowlist.local" ]; then if [ "${ban_allowlistonly}" = "1" ] && [ "${ban_monitorallowed}" = "1" ]; then continue fi fi feed_covered="0" if [ "${ban_allowlistonly}" = "1" ]; then case "${feed%%.*}" in "allowlist" | "blocklist") ;; *) feed_chain="" if json_select "${feed%%.*}" >/dev/null 2>&1; then json_get_var feed_chain "chain" >/dev/null 2>&1 json_select ".." >/dev/null 2>&1 fi f_skipfeed "$(f_direction "${feed%%.*}" "${feed_chain:-"in"}")" && feed_covered="1" ;; esac fi # keep: active country split sets # if [ "${feed_covered}" = "0" ] && [ "${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_covered}" = "0" ] && [ "${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_covered}" = "0" ]; then continue fi 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="$(f_mem float)" 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 "+%Y-%m-%d %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 "trigger_interfaces" for object in ${ban_trigger:-"-"}; 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 "trigger_interfaces" >/dev/null 2>&1 value="trigger: ${values// /, } / " json_get_values values "${key}" >/dev/null 2>&1 value="${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}" != "trigger_interfaces" ] && [ "${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 set_list set_name # 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}" -ge "${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 # set_list="${feed}" if [ "${feed}" = "allowlist" ] && [ "${ban_allowlistonly}" = "1" ] && [ "${ban_monitorallowed}" = "1" ]; then set_list="${set_list} allowlist.local" fi for set_name in ${set_list}; do if [ -n "${elementsv4}" ]; then if ! "${ban_nftcmd}" add element inet banIP "${set_name}.v4" { ${elementsv4} } 2>>"${ban_errorlog}"; then f_log "info" "can't add lookup file to nfset '${set_name}.v4'" fi fi if [ -n "${elementsv6}" ]; then if ! "${ban_nftcmd}" add element inet banIP "${set_name}.v6" { ${elementsv6} } 2>>"${ban_errorlog}"; then f_log "info" "can't add lookup file to nfset '${set_name}.v6'" fi fi done # 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 local jsn table_jsn set_jsn map_jsn map_ts map_lookup geo_ts geo_now geo_skip chunk_no chunk_skip rsp_no chain set_elements uplink_ip 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" map_ts="${ban_reportdir}/ban_map.ts" if [ "${output}" != "json" ]; then # json output preparation # : >"${report_txt}" >"${report_jsn}" read -r geo_now _ <"/proc/uptime" geo_now="${geo_now%%.*}" geo_ts="" [ -s "${map_ts}" ] && read -r geo_ts <"${map_ts}" case "${geo_ts}" in "" | *[!0-9]*) ;; *) [ "${geo_ts}" -le "${geo_now}" ] && [ "$((geo_now - geo_ts))" -lt "60" ] && geo_skip="1" ;; esac [ "${geo_skip}" = "1" ] || : >"${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_srtopts} | "${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}" -ge "${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}" ] && [ "${geo_skip}" != "1" ]; then f_getdl map_lookup="${ban_rundir}/report.map.lookup" : >"${map_lookup}" # collect the uplink IPs # json_init if json_load_file "${ban_rtfile}" >/dev/null 2>&1; then json_get_values jsnval "active_uplink" >/dev/null 2>&1 for uplink_ip in ${jsnval}; do uplink_ip="${uplink_ip%%/*}" if [ -n "${uplink_ip}" ] && [ "${uplink_ip}" != "-" ]; then printf '%s\t%s\n' "${uplink_ip}" "homeIP" >>"${map_lookup}" fi done fi # collect the top listed IPs of all relevant Sets # 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 for item in ${table_sets}; do [ "${item%%_*}" = "allowlist" ] && continue json_select "${item}" jsnval="" json_get_values jsnval "set_elements" >/dev/null 2>&1 for ip in ${jsnval}; do printf '%s\t%s\n' "${ip}" "${item//_/.}" >>"${map_lookup}" done json_select ".." done fi # split the deduplicated IPs into batch requests of 100 IPs each, # the maximum the geo service accepts, capped at 15 requests per run # if [ -s "${map_lookup}" ]; then "${ban_awkcmd}" -F '\t' -v file="${map_jsn}" -v size="100" -v max="15" \ '!seen[$1]++{if(cnt>=size*max){skip++;next};no=int(cnt++/size)+1;printf "%s\"%s\"",(chunk[no]++?", ":""),$1 >(file ".req." no)}END{for(i=1;i<=no;i++)close(file ".req." i);printf "%s %s\n",no+0,skip+0 >(file ".num")}' "${map_lookup}" chunk_no="$("${ban_catcmd}" "${map_jsn}.num" 2>>"${ban_errorlog}")" chunk_skip="${chunk_no#* }" chunk_no="${chunk_no%% *}" [ "${chunk_skip:-0}" -gt "0" ] && f_log "info" "geo lookup capped at 15 requests, ${chunk_skip} IPs left out of the map" cnt="1" while [ "${cnt}" -le "${chunk_no:-0}" ]; do ( "${ban_fetchcmd}" ${ban_geoparm} "[ $("${ban_catcmd}" "${map_jsn}.req.${cnt}") ]" "${ban_geourl}" 2>>"${ban_errorlog}" | "${ban_jsoncmd}" -qe '@[*&&@.status="success"]' | "${ban_awkcmd}" -F '\t' 'NR==FNR{if(!($1 in feed))feed[$1]=$2;next} match($0,/"query"[ \t]*:[ \t]*"[^"]+"/){query=substr($0,RSTART,RLENGTH);sub(/^"query"[ \t]*:[ \t]*"/,"",query);sub(/"$/,"",query);if(query in feed)printf ",{\"%s\": %s}\n",feed[query],$0}' \ "${map_lookup}" - >"${map_jsn}.rsp.${cnt}" ) & [ "${cnt}" -ge "${ban_cores}" ] && wait -n cnt="$((cnt + 1))" done wait read -r geo_now _ <"/proc/uptime" printf '%s\n' "${geo_now%%.*}" >"${map_ts}" # assemble map data from the batch fragments # cnt="1" rsp_no="0" while [ "${cnt}" -le "${chunk_no:-0}" ]; do if [ -s "${map_jsn}.rsp.${cnt}" ]; then rsp_no="$((rsp_no + 1))" [ -s "${map_jsn}" ] || printf '%s' ",[{}" >>"${map_jsn}" "${ban_catcmd}" "${map_jsn}.rsp.${cnt}" >>"${map_jsn}" fi cnt="$((cnt + 1))" done [ "${rsp_no}" -lt "${chunk_no:-0}" ] && f_log "info" "$((chunk_no - rsp_no)) of ${chunk_no} geo requests returned no data" f_log "debug" "f_report ::: geo requests: ${chunk_no:-0}, skipped IPs: ${chunk_skip:-0}, map data: $([ -s "${map_jsn}" ] && printf '%s' "yes" || printf '%s' "no")" fi "${ban_rmcmd}" -f "${map_lookup}" "${map_jsn}".req.* "${map_jsn}".rsp.* "${map_jsn}.num" elif [ "${ban_map}" = "1" ] && [ "${geo_skip}" = "1" ]; then f_log "debug" "f_report ::: geo requests: 0, map data: reused" 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//_/.}" 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" ] && [ -s "${report_jsn}" ] && [ -s "${map_jsn}" ]; 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}" -ge "${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 monitor_set # 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 the allowlist Set used by the monitor # if [ "${ban_allowlistonly}" = "1" ] && [ "${ban_monitorallowed}" = "1" ]; then monitor_set="allowlist.local" else monitor_set="allowlist" fi # 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))+$'; 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)/, 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 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 "${monitor_set}.v4")" allow_v6="$(nft_cache "${monitor_set}.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_logterm_map="${ban_logterm_map}" "${ban_awkcmd}" -v threshold="${ban_logcount}" -v limit=5000 ' function pick_ip(s, mode, m, res) { res = "" while (match(s, /([0-9]{1,3}\.){3}[0-9]{1,3}|([A-Fa-f0-9]{0,4}:){2,7}[A-Fa-f0-9]{0,4}/, m)) { res = m[0] if (mode == "first") break s = substr(s, RSTART + RLENGTH) } return res } function anchor_for(line, k) { for (k = 1; k <= nterm; k++) if (line ~ pat[k]) return anc[k]; return "last" } BEGIN { unique = 0 map = ENVIRON["ban_logterm_map"] n = split(map, recs, "\036") nterm = 0 all_last = 1 for (i = 1; i <= n; i++) { if (recs[i] == "") continue split(recs[i], f, "\037") nterm++ anc[nterm] = f[1] pat[nterm] = f[2] if (f[1] != "last") all_last = 0 } } { pos = all_last ? "last" : anchor_for($0) $0 = gensub(/(([0-9]{1,3}\.){3}[0-9]{1,3}):[0-9]+/, "\\1", "g", $0) sub(/\]:[0-9]+/, "]", $0) gsub(/[<>[\]]/, "", $0) ip = "" proto = "" cand = pick_ip($0, pos) if (cand ~ /\./) { if (cand !~ /^127\./ && cand !~ /^0\./) { ip = cand proto = ".v4" } } else if (cand ~ /:/) { sub(/%.*/, "", cand) if (cand !~ /^[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}$/ && cand !~ /^([A-Fa-f0-9]{2}:){5}[A-Fa-f0-9]{2}$/) { ip = cand 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 "${monitor_set}${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 ${monitor_set}${proto} 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