diff --git a/admin/zabbix/Makefile b/admin/zabbix/Makefile index c1da4eda03528..c6f86103cee97 100644 --- a/admin/zabbix/Makefile +++ b/admin/zabbix/Makefile @@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=zabbix PKG_VERSION:=7.0.23 -PKG_RELEASE:=2 +PKG_RELEASE:=3 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://cdn.zabbix.com/zabbix/sources/stable/$(basename $(PKG_VERSION))/ \ diff --git a/admin/zabbix/files/zabbix-network-ubus-acl.json b/admin/zabbix/files/zabbix-network-ubus-acl.json index f19f51b16ee04..e60a72abe0173 100644 --- a/admin/zabbix/files/zabbix-network-ubus-acl.json +++ b/admin/zabbix/files/zabbix-network-ubus-acl.json @@ -1,8 +1,10 @@ { - "user": "zabbix", + "user": "zabbix-agent", "access": { "network.interface": { - "methods": [ "dump" ] + "methods": [ + "dump" + ] } } } diff --git a/admin/zabbix/files/zabbix-wifi-ubus-acl.json b/admin/zabbix/files/zabbix-wifi-ubus-acl.json index 9d9b093cfbc48..041cbf88d4a3a 100644 --- a/admin/zabbix/files/zabbix-wifi-ubus-acl.json +++ b/admin/zabbix/files/zabbix-wifi-ubus-acl.json @@ -1,8 +1,10 @@ { - "user": "zabbix", + "user": "zabbix-agent", "access": { "network.wireless": { - "methods": [ "status" ] + "methods": [ + "status" + ] } } } diff --git a/lang/python/python3-pyinotify/Makefile b/lang/python/python3-pyinotify/Makefile deleted file mode 100644 index ea64bd719cb7a..0000000000000 --- a/lang/python/python3-pyinotify/Makefile +++ /dev/null @@ -1,40 +0,0 @@ -# -# This is free software, licensed under the GNU General Public License v2. -# See /LICENSE for more information. -# - -include $(TOPDIR)/rules.mk - -PKG_NAME:=python3-pyinotify -PKG_VERSION:=0.9.6 -PKG_RELEASE:=1 - -PYPI_NAME:=pyinotify -PKG_HASH:=9c998a5d7606ca835065cdabc013ae6c66eb9ea76a00a1e3bc6e0cfe2b4f71f4 - -PKG_MAINTAINER:=Gerald Kerma -PKG_LICENSE:=MIT -PKG_LICENSE_FILES:=COPYING - -include ../pypi.mk -include $(INCLUDE_DIR)/package.mk -include ../python3-package.mk - -define Package/python3-pyinotify - SUBMENU:=Python - SECTION:=lang - CATEGORY:=Languages - TITLE:=Linux filesystem events monitoring - URL:=https://github.com/seb-m/pyinotify - DEPENDS:= +python3-light \ - +python3-ctypes \ - +python3-logging -endef - -define Package/python3-pyinotify/description - Pyinotify is a Python module for monitoring filesystems changes. -endef - -$(eval $(call Py3Package,python3-pyinotify)) -$(eval $(call BuildPackage,python3-pyinotify)) -$(eval $(call BuildPackage,python3-pyinotify-src)) diff --git a/net/banip/Makefile b/net/banip/Makefile index e279d36429216..d4e2b22aabf6b 100644 --- a/net/banip/Makefile +++ b/net/banip/Makefile @@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=banip -PKG_VERSION:=1.8.1 -PKG_RELEASE:=3 +PKG_VERSION:=1.8.5 +PKG_RELEASE:=1 PKG_LICENSE:=GPL-3.0-or-later PKG_MAINTAINER:=Dirk Brenken diff --git a/net/banip/files/README.md b/net/banip/files/README.md index 79887f7622fb5..51f12c589aefb 100644 --- a/net/banip/files/README.md +++ b/net/banip/files/README.md @@ -69,8 +69,8 @@ IP address blocking is commonly used to protect against brute force attacks, pre * All local input types support ranges in CIDR notation * Auto-add the uplink subnet or uplink IP to the local allowlist * Prevent common ICMP, UDP and SYN flood attacks and drop spoofed tcp flags & invalid conntrack packets (DoS attacks) in an additional prerouting chain -* Provides a small background log monitor to ban unsuccessful login attempts in real-time (like fail2ban, crowdsec etc.) -* Auto-add unsuccessful LuCI, nginx, Asterisk or ssh login attempts to the local blocklist +* Provides a background log monitor to ban unsuccessful login attempts in real-time (like fail2ban, crowdsec etc.) with three-tier IP deduplication, dynamic cache management and optional RDAP-based subnet blocking +* Auto-add unsuccessful LuCI, Asterisk or ssh login attempts to the local blocklist * Auto-add entire subnets to the blocklist Set based on an additional RDAP request with the monitored suspicious IP * Fast feed processing as they are handled in parallel as background jobs (on capable multi-core hardware) * Per feed it can be defined whether the inbound chain (wan-input, wan-forward) or the outbound chain (lan-forward) should be blocked @@ -101,7 +101,7 @@ IP address blocking is commonly used to protect against brute force attacks, pre * A certificate store like 'ca-bundle', as banIP checks the validity of the SSL certificates of all download sites by default * For E-Mail notifications you need to install and setup the additional 'msmtp' package -**Please note:** +**Please note:** * Devices with less than 256MB of RAM are **_not_** supported * After system upgrades it's recommended to start with a fresh banIP default config @@ -111,7 +111,7 @@ IP address blocking is commonly used to protect against brute force attacks, pre * Install the LuCI companion package 'luci-app-banip' which also installs the main 'banip' package as a dependency * Enable the banIP system service (System -> Startup) and enable banIP itself (banIP -> General Settings) * It's strongly recommended to use the LuCI frontend to easily configure all aspects of banIP, the application is located in LuCI under the 'Services' menu -* It's also recommended to configure a 'Reload Trigger Interface' to depend on your WAN ifup events during boot or restart of your router +* It's also recommended to configure a 'Startup Trigger Interface' to depend on your WAN ifup events during boot or restart of your router * To be able to use banIP in a meaningful way, you must activate the service and possibly also activate a few blocklist feeds * If you're using a complex network setup, e.g. special tunnel interfaces, than untick the 'Auto Detection' option under the 'General Settings' tab and set the required options manually * Start the service with '/etc/init.d/banip start' and check everything is working by running '/etc/init.d/banip status', also check the 'Processing Log' tab @@ -150,8 +150,8 @@ Available commands: | ban_nicelimit | option | 0 | ulimit nice level of the banIP service (range 0-19) | | ban_filelimit | option | 1024 | ulimit max open/number of files (range 1024-4096) | | ban_loglimit | option | 100 | scan only the last n log entries permanently. A value of '0' disables the monitor | -| ban_logcount | option | 1 | how many times the IP must appear in the log to be considered as suspicious | -| ban_logterm | list | regex | various regex for logfile parsing (default: dropbear, sshd, luci, nginx, asterisk and cgi-remote events) | +| ban_logcount | option | 1 | how many times the IP must appear in the log per blocking cycle to trigger auto-blocking | +| ban_logterm | list | regex | various regex for logfile parsing (default: dropbear, sshd, luci, asterisk and cgi-remote events) | | ban_logreadfile | option | /var/log/messages | alternative location for parsing a log file via tail, to deactivate the standard parsing via logread | | ban_autodetect | option | 1 | auto-detect wan interfaces, devices and subnets | | ban_debug | option | 0 | enable banIP related debug logging | @@ -163,7 +163,7 @@ Available commands: | ban_logoutbound | option | 0 | log suspicious packets in the outbound chain (lan-forward) | | ban_autoallowlist | option | 1 | add wan IPs/subnets and resolved domains automatically to the local allowlist (not only to the Sets) | | ban_autoblocklist | option | 1 | add suspicious attacker IPs and resolved domains automatically to the local blocklist (not only to the Sets) | -| ban_autoblocksubnet | option | 0 | add entire subnets to the blocklist Sets based on an additional RDAP request with the suspicious IP | +| ban_autoblocksubnet | option | 0 | add entire subnets to the blocklist Sets based on a rate-limited, non-blocking RDAP lookup for the suspicious IP | | ban_autoallowuplink | option | subnet | limit the uplink autoallow function to: 'subnet', 'ip' or 'disable' it at all | | ban_allowlistonly | option | 0 | restrict the internet access only to specific, explicitly allowed IP segments | | ban_allowflag | option | - | always allow certain protocols(tcp or udp) plus destination ports or port ranges, e.g.: 'tcp 80 443-445' | @@ -187,7 +187,7 @@ Available commands: | ban_nftloglevel | option | warn | nft loglevel, values: emerg, alert, crit, err, warn, notice, info, debug | | ban_nftpriority | option | -100 | nft priority for the banIP table (the prerouting table is fixed to priority -150) | | ban_nftpolicy | option | memory | nft policy for banIP-related Sets, values: memory, performance | -| ban_nftexpiry | option | - | expiry time (ms|s|m|h|d|w) for auto added blocklist members, e.g. '5m', '2h' or '1d' | +| ban_nftexpiry | option | - | expiry time (ms|s|m|h|d|w) for auto added blocklist members (also controls the monitor cache refresh interval) | | ban_nftretry | option | 3 | number of Set load attempts in case of an error | | ban_nftcount | option | 0 | enable nft counter for every Set element | | ban_bcp38 | option | 0 | block packets with spoofed source IP addresses in all supported chains | @@ -259,7 +259,7 @@ Available commands: 13 | 138155 | 4 (362) | 13 (2851) | 10 | 5 ``` -**banIP runtime information** +**banIP runtime information** ```sh ~# /etc/init.d/banip status @@ -278,7 +278,7 @@ Available commands: + system_info : cores: 4, log: logread, fetch: curl, Bananapi BPI-R3, mediatek/filogic, OpenWrt SNAPSHOT (r32542-bf46d119a2) ``` -**banIP search information** +**banIP search information** ```sh ~# /etc/init.d/banip search 8.8.8.8 @@ -291,7 +291,7 @@ Available commands: IP found in Set 'doh.v4' ``` -**banIP Set content information** +**banIP Set content information** List all elements of a given Set with hit counters, e.g.: ```sh @@ -346,14 +346,14 @@ nftables supports the atomic loading of firewall rules (incl. elements), which i * set 'ban_splitsize' e.g. to '1024' to split the load of an external Set after every 1024 lines/elements * set 'ban_nftcount' to '0' to deactivate the CPU- and memory-intensive creation of counter elements at Set level -**Sensible choice of blocklists** +**Sensible choice of blocklists** The following feeds are just my personal recommendation as an initial setup: * cinsscore, debl, turris and doh in their default chains In total, this feed selection blocks about 20K IP addresses. It may also be useful to include some countries to the country feed. Please note: don't just blindly activate (too) many feeds at once, sooner or later this will lead to OOM conditions. -**Log Terms for logfile parsing** +**Log Terms for logfile parsing** Like fail2ban and crowdsec, banIP supports logfile scanning and automatic blocking of suspicious attacker IPs. In the default config only the log terms to detect failed login attempts via dropbear and LuCI are in place. The following search pattern has been tested as well: @@ -363,24 +363,24 @@ LuCI : 'luci: failed login' sshd1 : 'error: maximum authentication attempts exceeded' sshd2 : 'sshd.*Connection closed by.*\[preauth\]' asterisk : 'SecurityEvent=\"InvalidAccountID\".*RemoteAddress=' -nginx : 'received a suspicious remote IP .*' openvpn : 'TLS Error: could not determine wrapping from \[AF_INET\]' AdGuard : 'AdGuardHome.*\[error\].*/control/login: from ip' +Remote : 'received a suspicious remote IP' ``` You find the 'Log Terms' option in LuCI under the 'Log Settings' tab. Feel free to add more log terms to meet your needs and protect additional services. -**Allow-/Blocklist handling** +**Allow-/Blocklist handling** banIP supports local allow- and block-lists, MAC/IPv4/IPv6 addresses (incl. ranges in CIDR notation) or domain names. These files are located in /etc/banip/banip.allowlist and /etc/banip/banip.blocklist. Unsuccessful login attempts or suspicious requests will be tracked and added to the local blocklist (see the 'ban_autoblocklist' option). The blocklist behaviour can be further tweaked with the 'ban_nftexpiry' option. Depending on the options 'ban_autoallowlist' and 'ban_autoallowuplink' the uplink subnet or the uplink IP will be added automatically to local allowlist. Furthermore, you can reference external Allowlist URLs with additional IPv4 and IPv6 feeds (see 'ban_allowurl'). Both local lists also accept domain names as input to allow IP filtering based on these names. The corresponding IPs (IPv4 & IPv6) will be extracted and added to the Sets. -**Allowlist-only mode** +**Allowlist-only mode** banIP supports an "allowlist only" mode. This option restricts Internet access only to certain, explicitly permitted IP segments - and blocks access to the rest of the Internet. All IPs that are _not_ listed in the allowlist or in the external allowlist URLs are blocked. In this mode it might be useful to limit the allowlist feed to the inbound chain, to still allow outbound communication to the rest of the world. -**MAC/IP-binding** +**MAC/IP-binding** banIP supports concatenation of local MAC addresses/ranges with IPv4/IPv6 addresses, e.g. to enforce dhcp assignments or to free connected clients from outbound blocking. The following notations in the local allow- and block-list are supported: @@ -406,7 +406,7 @@ C8:C2:9B:F7:80:12 192.168.1.10 => this will be populated to C8:C2:9B:F7:80:12 => this will be populated to v6MAC-Set with the IP-wildcard ::/0 ``` -**MAC-address logging in nftables** +**MAC-address logging in nftables** The MAC-address logging format in nftables is a little bit unusual. It is generated by the kernel's NF_LOG module and places all MAC-related data into one flat field, without separators or labels. For example, the field MAC=7e:1a:2f:fc:ee:29:68:34:21:1f:a7:b1:08:00 is actually a concatenation of the following: ``` @@ -415,14 +415,14 @@ The MAC-address logging format in nftables is a little bit unusual. It is genera 68:34:21:1f:a7:b1 → the destination MAC address 08:00 → the EtherType for IPv4 (0x0800) ``` -**BCP38** +**BCP38** BCP38 (**B**est **C**urrent **P**ractice, RFC 2827) defines ingress filtering to prevent IP address spoofing. In practice, this means: * dropping packets arriving on the WAN whose source address is not valid or routable via that interface * dropping packets leaving LAN => WAN whose source address does not belong to the local/internal prefixes In banIP, the BCP38 implementation uses nftables’ FIB lookup to enforce this. It checks whether the packet’s source address is not valid for the incoming interface or whether the routing table reports no route for this source on this interface. Packets that fail this check are dropped. -**Set reporting, enable the GeoIP Map** +**Set reporting, enable the GeoIP Map** banIP includes a powerful reporting tool on the Set Reporting tab which shows the latest NFT banIP Set statistics. To get the latest statistics always press the "Refresh" button. In addition to a tabular overview banIP reporting includes a GeoIP map in a modal popup window/iframe that shows the geolocation of your own uplink addresses (in green) and the locations of potential attackers (in red). To enable the GeoIP Map set the following options (in "Feed/Set Settings" config tab): @@ -435,11 +435,12 @@ To make this work, banIP uses the following external components: * [CARTO basemap styles](https://github.com/CartoDB/basemap-styles) based on [OpenMapTiles](https://openmaptiles.org/schema) * The free and quite fast [IP Geolocation API](https://ip-api.com/) to resolve the required IP/geolocation information -**CGI interface to receive remote logging events** +**CGI interface to receive remote logging events** banIP ships a basic cgi interface in '/www/cgi-bin/banip' to receive remote logging events (disabled by default). The cgi interface evaluates logging events via GET or POST request (see examples below). To enable the cgi interface set the following options: * set 'ban_remotelog' to '1' to enable the cgi interface * set 'ban_remotetoken' to a secret transfer token, allowed token characters consist of '[A-Za-z]', '[0-9]', '.' and ':' + * add the remote logging event to the logterm Examples to transfer remote logging events from an internal server to banIP via cgi interface: @@ -448,7 +449,7 @@ banIP ships a basic cgi interface in '/www/cgi-bin/banip' to receive remote logg Please note: for security reasons use this cgi interface only internally and only encrypted via https transfer protocol. -**Download options** +**Download options** By default banIP uses the following pre-configured download options: ``` @@ -459,7 +460,7 @@ By default banIP uses the following pre-configured download options: To override the default set 'ban_fetchretry', 'ban_fetchinsecure' or globally 'ban_fetchparm' to your needs. -**Configure E-Mail notifications via 'msmtp'** +**Configure E-Mail notifications via 'msmtp'** To use the email notification you must install and configure the package 'msmtp'. Modify the file '/etc/msmtprc', e.g.: @@ -482,7 +483,7 @@ password Finally add a valid E-Mail receiver address in banIP. -**Send status E-Mails and update the banIP lists via cron job** +**Send status E-Mails and update the banIP lists via cron job** For a regular, automatic status mailing and update of the used lists on a daily basis set up a cron job, e.g. ``` @@ -490,10 +491,10 @@ For a regular, automatic status mailing and update of the used lists on a daily 00 04 * * * /etc/init.d/banip reload ``` -**Redirect asterisk security logs to lodg/logread** +**Redirect asterisk security logs to syslog/logread** By default banIP scans the logfile via logread, so to monitor attacks on asterisk, its security log must be available via logread. To do this, edit '/etc/asterisk/logger.conf' and add the line 'syslog.local0 = security', then run 'asterisk -rx reload logger' to update the running asterisk configuration. -**Change/add banIP feeds and set optional feed flags** +**Change/add banIP feeds and set optional feed flags** The banIP default blocklist feeds are stored in an external JSON file '/etc/banip/banip.feeds'. All custom changes should be stored in an external JSON file '/etc/banip/banip.custom.feeds' (empty by default). It's recommended to use the LuCI based Custom Feed Editor to make changes to this file. A valid JSON source object contains the following information, e.g.: @@ -519,7 +520,7 @@ The rule consist of max. 4 individual, space separated parameters: Please note: the flag field is optional, it's a space separated list of options: supported are 'gz' as an archive format and protocols 'tcp' or 'udp' with port numbers/port ranges for destination port limitations. -**Debug options** +**Debug options** banIP provides an optional debug mode that writes diagnostic information to the system log and captures internal error output in a dedicated error logfile - by default located in the banIP base directory as '/tmp/ban_error.log'. The log file is automatically cleared at the beginning of each run. Under normal conditions, all error messages are discarded to keep regular runs clean and silent. Whenever you encounter banIP related processing problems, please enable "Verbose Debug Logging", restart banIP and check the "Processing Log" tab. diff --git a/net/banip/files/banip-functions.sh b/net/banip/files/banip-functions.sh index e57e7879971f4..a661264d853bd 100644 --- a/net/banip/files/banip-functions.sh +++ b/net/banip/files/banip-functions.sh @@ -22,12 +22,12 @@ 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="/var/run/banip.pid" -ban_rtfile="/var/run/banip_runtime.json" -ban_rdapfile="/var/run/banip_rdap.json" +ban_pidfile="/var/run/banIP/banIP.pid" +ban_rtfile="/var/run/banIP/banIP_runtime.json" +ban_rdapfile="/var/run/banIP/banIP_rdap.json" ban_rdapurl="https://rdap.db.ripe.net/ip/" ban_geourl="http://ip-api.com/batch" -ban_lock="/var/run/banip.lock" +ban_lock="/var/run/banIP/banIP.lock" ban_errorlog="/dev/null" ban_logreadfile="" ban_logreadcmd="" @@ -161,7 +161,7 @@ f_mkdir() { local dir="${1}" if [ ! -d "${dir}" ]; then - rm -f "${dir}" + "${ban_rmcmd}" -f "${dir}" mkdir -p "${dir}" f_log "debug" "f_mkdir ::: directory: ${dir}" fi @@ -195,7 +195,7 @@ f_rmdir() { local dir="${1}" if [ -d "${dir}" ]; then - rm -rf "${dir}" + "${ban_rmcmd}" -rf "${dir}" f_log "debug" "f_rmdir ::: directory: ${dir}" fi } @@ -280,7 +280,7 @@ f_log() { fi f_rmdir "${ban_tmpdir}" f_rmpid - rm -rf "${ban_lock}" + "${ban_rmcmd}" -rf "${ban_lock}" exit 1 fi } @@ -311,14 +311,14 @@ f_conf() { "ban_logterm") eval "append=\"\${${option}}\"" if [ -n "${append}" ]; then - eval "${option}=\"${append}\\|${value}\"" + eval "${option}=\"\${append}\\|\${value}\"" else - eval "${option}=\"${value}\"" + eval "${option}=\"\${value}\"" fi ;; *) eval "append=\"\${${option}}\"" - eval "${option}=\"${append}${value} \"" + eval "${option}=\"\${append}\${value} \"" ;; esac } @@ -333,8 +333,14 @@ f_conf() { for rir in ${ban_region}; do while read -r ccode region country; do - if [ "${rir}" = "${region}" ] && ! printf "%s" "${ban_country}" | "${ban_grepcmd}" -qw "${ccode}"; then - ban_country="${ban_country} ${ccode}" + if [ "${rir}" = "${region}" ]; then + case " ${ban_country} " in + *" ${ccode} "*) + ;; + *) + ban_country="${ban_country} ${ccode}" + ;; + esac fi done <"${ban_countryfile}" done @@ -492,7 +498,7 @@ f_actual() { ppid="$("${ban_catcmd}" "${ban_pidfile}" 2>>"${ban_errorlog}")" if [ -n "${ppid}" ]; then monitor="$(f_char "0")" - pids="$("${ban_pgrepcmd}" -P "${ppid}" 2>>"${ban_errorlog}")" + 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="$(f_char "1")" @@ -514,7 +520,7 @@ f_getdl() { if { [ "${ban_autodetect}" = "1" ] && [ -z "${ban_fetchcmd}" ]; } || [ ! -x "${ban_fetchcmd}" ]; then fetch_list="curl wget-ssl libustream-openssl libustream-wolfssl libustream-mbedtls" for fetch in ${fetch_list}; do - if printf "%s" "${ban_packages}" | "${ban_grepcmd}" -q "\"${fetch}"; then + case "${ban_packages}" in *"\"${fetch}"*) case "${fetch}" in "wget-ssl") fetch="wget" @@ -530,7 +536,8 @@ f_getdl() { uci_commit "banip" break fi - fi + ;; + esac done fi @@ -622,11 +629,15 @@ f_getdev() { network_get_device dev "${iface}" if [ -n "${dev}" ]; then dev_del="${dev_del/${dev} / }" - if ! printf " %s " "${ban_dev}" | "${ban_grepcmd}" -q " ${dev} "; then - ban_dev="${ban_dev}${dev} " - uci_add_list banip global ban_dev "${dev}" - f_log "info" "add device '${dev}' to config" - fi + 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 @@ -658,16 +669,28 @@ f_getup() { elif [ "${ban_autoallowuplink}" = "ip" ]; then network_get_ipaddr uplink "${iface}" fi - if [ -n "${uplink}" ] && ! printf " %s " "${ban_uplink}" | "${ban_grepcmd}" -q " ${uplink} "; then - ban_uplink="${ban_uplink}${uplink} " + 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::*}" ] && ! printf " %s " "${ban_uplink}" | "${ban_grepcmd}" -q " ${uplink} "; then - ban_uplink="${ban_uplink}${uplink} " + if [ -n "${uplink%fe80::*}" ]; then + case " ${ban_uplink} " in + *" ${uplink} "*) + ;; + *) + ban_uplink="${ban_uplink}${uplink} " + ;; + esac fi done ban_uplink="$(f_trim "${ban_uplink}")" @@ -786,15 +809,27 @@ f_nftinit() { "tcp" | "udp") if [ -z "${tmp_proto}" ]; then tmp_proto="${flag}" - elif ! printf "%s" "${tmp_proto}" | "${ban_grepcmd}" -qw "${flag}"; then - tmp_proto="${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}" - elif ! printf "%s" "${tmp_port}" | "${ban_grepcmd}" -qw "${flag}"; then - tmp_port="${tmp_port}, ${flag}" + else + case ", ${tmp_port}, " in + *", ${flag}, "*) + ;; + *) + tmp_port="${tmp_port}, ${flag}" + ;; + esac fi ;; esac @@ -959,7 +994,8 @@ f_down() { local expr cnt_set cnt_dl restore_rc feed_direction feed_policy feed_rc feed_comp feed_complete feed_target feed_dport chain flag local tmp_proto tmp_port asn country feed="${1}" feed_ipv="${2}" feed_url="${3}" feed_rule="${4}" feed_chain="${5}" feed_flag="${6}" - start_ts="$(date +%s)" + 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" @@ -988,21 +1024,48 @@ f_down() { element_count="counter" fi - # set feed complete flag +# set feed complete flag # - if printf "%s" "${ban_feedcomplete}" | "${ban_grepcmd}" -q "${feed%%.*}"; then - feed_complete="true" - fi + case " ${ban_feedcomplete} " in + *" ${feed%%.*} "*) + feed_complete="true" + ;; + esac # set feed direction # - if printf "%s" "${ban_feedin}" | "${ban_grepcmd}" -q "${feed%%.*}"; then + feed_name="${feed%%.*}" + if case " ${ban_feedin} " in + *" ${feed_name} "*) + true + ;; + *) + false + ;; + esac + then feed_policy="in" feed_direction="inbound" - elif printf "%s" "${ban_feedout}" | "${ban_grepcmd}" -q "${feed%%.*}"; then + elif case " ${ban_feedout} " in + *" ${feed_name} "*) + true + ;; + *) + false + ;; + esac + then feed_policy="out" feed_direction="outbound" - elif printf "%s" "${ban_feedinout}" | "${ban_grepcmd}" -q "${feed%%.*}"; then + elif case " ${ban_feedinout} " in + *" ${feed_name} "*) + true + ;; + *) + false + ;; + esac + then feed_policy="inout" feed_direction="inbound outbound" else @@ -1023,7 +1086,7 @@ f_down() { esac fi - # prepare feed flags +# prepare feed flags # for flag in ${feed_flag}; do case "${flag}" in @@ -1033,25 +1096,41 @@ f_down() { "tcp" | "udp") if [ -z "${tmp_proto}" ]; then tmp_proto="${flag}" - elif ! printf "%s" "${tmp_proto}" | "${ban_grepcmd}" -qw "${flag}"; then - tmp_proto="${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}" - elif ! printf "%s" "${tmp_port}" | "${ban_grepcmd}" -qw "${flag}"; then - tmp_port="${tmp_port}, ${flag}" + else + case ", ${tmp_port}, " in + *", ${flag}, "*) + ;; + *) + tmp_port="${tmp_port}, ${flag}" + ;; + esac fi ;; esac done - if ! printf "%s" "${ban_feedreset}" | "${ban_grepcmd}" -q "${feed%%.*}"; then - if [ -n "${tmp_proto}" ] && [ -n "${tmp_port}" ]; then - feed_dport="meta l4proto { ${tmp_proto} } th dport { ${tmp_port} }" - fi - fi + 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 # @@ -1385,7 +1464,7 @@ f_down() { 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="${?}" - rm -f "${tmp_file}".* + "${ban_rmcmd}" -f "${tmp_file}".* f_log "info" "can't split nfset '${feed}' to size '${ban_splitsize//[![:digit:]]/}'" fi else @@ -1482,7 +1561,8 @@ f_down() { fi fi : >"${tmp_nft}" - end_ts="$(date +%s)" + 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:-"-"}" } @@ -1519,7 +1599,7 @@ f_restore() { # remove staled Sets # f_rmset() { - local feedlist tmp_del table_json feed country asn table_sets handles handle expr del_set feed_rc + local feedlist tmp_del table_json feed country asn table_sets handles handle expr del_set feed_rc _rmset_skip f_getfeed json_get_keys feedlist @@ -1529,33 +1609,101 @@ f_rmset() { { printf "%s\n\n" "#!${ban_nftcmd} -f" for feed in ${table_sets}; do - if ! printf "%s" "allowlist blocklist ${ban_feed}" | "${ban_grepcmd}" -q "${feed%.*}" || - ! printf "%s" "allowlist blocklist ${feedlist}" | "${ban_grepcmd}" -q "${feed%.*}" || - { [ "${feed%.*}" = "country" ] && [ "${ban_countrysplit}" = "1" ]; } || - { [ "${feed%.*}" = "asn" ] && [ "${ban_asnsplit}" = "1" ]; } || - { [ "${feed%.*}" != "allowlist" ] && [ "${feed%.*}" != "blocklist" ] && [ "${ban_allowlistonly}" = "1" ] && - ! printf "%s" "${ban_feedin}" | "${ban_grepcmd}" -q "allowlist" && - ! printf "%s" "${ban_feedout}" | "${ban_grepcmd}" -q "allowlist"; }; then + _rmset_skip="0" + case " allowlist blocklist ${ban_feed} " in + *" ${feed%.*} "*) + ;; + *) + _rmset_skip="1" + ;; + esac + case " allowlist blocklist ${feedlist} " in + *" ${feed%.*} "*) + ;; + *) + _rmset_skip="1" + ;; + esac + { [ "${feed%.*}" = "country" ] && [ "${ban_countrysplit}" = "1" ]; } && _rmset_skip="1" + { [ "${feed%.*}" = "asn" ] && [ "${ban_asnsplit}" = "1" ]; } && _rmset_skip="1" + if [ "${feed%.*}" != "allowlist" ] && [ "${feed%.*}" != "blocklist" ] && [ "${ban_allowlistonly}" = "1" ]; then + if case " ${ban_feedin} " in + *" allowlist "*) + true + ;; + *) + false + ;; + esac + then + : + elif case " ${ban_feedout} " in + *" allowlist "*) + true + ;; + *) + false + ;; + esac + then + : + else + _rmset_skip="1" + fi + fi + if [ "${_rmset_skip}" = "1" ]; then case "${feed%%.*}" in "country") country="${feed%.*}" country="${country#*.}" - if [ "${ban_countrysplit}" = "1" ] && printf "%s" "${ban_feed}" | "${ban_grepcmd}" -q "${feed%%.*}" && - printf "%s" "${ban_country}" | "${ban_grepcmd}" -q "${country}"; then + if [ "${ban_countrysplit}" = "1" ] && + case " ${ban_feed} " in + *" ${feed%%.*} "*) + true + ;; + *) + false + ;; + esac && + case " ${ban_country} " in + *" ${country} "*) + true + ;; + *) + false + ;; + esac + then continue fi ;; "asn") asn="${feed%.*}" asn="${asn#*.}" - if [ "${ban_asnsplit}" = "1" ] && printf "%s" "${ban_feed}" | "${ban_grepcmd}" -q "${feed%%.*}" && - printf "%s" "${ban_asn}" | "${ban_grepcmd}" -q "${asn}"; then + if [ "${ban_asnsplit}" = "1" ] && + case " ${ban_feed} " in + *" ${feed%%.*} "*) + true + ;; + *) + false + ;; + esac && + case " ${ban_asn} " in + *" ${asn} "*) + true + ;; + *) + false + ;; + esac + then continue fi ;; esac [ -z "${del_set}" ] && del_set="${feed}" || del_set="${del_set}, ${feed}" - rm -f "${ban_backupdir}/banIP.${feed}.gz" + "${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}")" @@ -1602,10 +1750,11 @@ f_genstatus() { 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}" ] && [ "${ban_action}" != "boot" ]; then - end_time="$(date "+%s")" + 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 "+%Y-%m-%d %H:%M:%S"), duration: ${duration:-"-"}, memory: ${mem_free} MB available" + runtime="mode: ${ban_action}, $(date "+%d/%m/%Y %H:%M:%S"), duration: ${duration:-"-"}, memory: ${mem_free} MB available" fi [ -s "${ban_customfeedfile}" ] && custom_feed="1" [ "${ban_splitsize:-"0"}" -gt "0" ] && split="1" @@ -1698,7 +1847,8 @@ f_getstatus() { f_lookup() { local cnt list domain lookup ip elementsv4 elementsv6 start_time end_time duration cnt_domain="0" cnt_ip="0" feed="${1}" - start_time="$(date "+%s")" + read -r start_time _ < "/proc/uptime" + start_time="${start_time%%.*}" if [ "${feed}" = "allowlist" ]; then list="$("${ban_awkcmd}" '/^([[:alnum:]_-]{1,63}\.)+[[:alpha:]]+([[:space:]]|$)/{printf "%s ",tolower($1)}' "${ban_allowlist}" 2>>"${ban_errorlog}")" elif [ "${feed}" = "blocklist" ]; then @@ -1732,7 +1882,8 @@ f_lookup() { f_log "info" "can't add lookup file to nfset '${feed}.v6'" fi fi - end_time="$(date "+%s")" + 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}" @@ -2106,7 +2257,7 @@ f_report() { : >"${report_txt}" ;; "gen") - printf "%s\n" "$(date "+%s")" >"/var/run/banIP.report" + printf "%s\n" "1" >"/var/run/banIP/banIP.report" ;; *) : >"${report_txt}" @@ -2119,8 +2270,8 @@ f_search() { # prepare result file # - tmp_result="/var/run/banIP.search.tmp" - result="/var/run/banIP.search" + tmp_result="/var/run/banIP/banIP.search.tmp" + result="/var/run/banIP/banIP.search" # validate input # @@ -2276,132 +2427,286 @@ f_mail() { # log monitor # f_monitor() { - local logread_cmd loglimit_cmd logread_filter nft_expiry line ip_proto ip proto log_count idx base cidr rdap_log rdap_rc rdap_idx rdap_info + 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 block_cache file cache_ts date_stamp time_now time_elapsed cache_interval rdap_interval rdap_tsfile rdap_lock rdap_jobs + + # intervals for periodic cache refresh and RDAP queries + # + cache_interval=300 + rdap_interval=2 + rdap_tsfile="/var/run/banIP/banIP_rdap_ts" + printf "%s" "0" > "${rdap_tsfile}" - # log reading configuration + # determine log reader type # if [ -f "${ban_logreadfile}" ] && [ -x "${ban_logreadcmd}" ] && [ "${ban_logreadcmd##*/}" = "tail" ]; then - logread_cmd="${ban_logreadcmd} -qf ${ban_logreadfile} 2>/dev/null" - loglimit_cmd="${ban_logreadcmd} -qn ${ban_loglimit} ${ban_logreadfile} 2>/dev/null" - logread_filter="${ban_grepcmd} -e \"${ban_logterm}\" 2>/dev/null" + log_type="tail" elif [ -x "${ban_logreadcmd}" ] && [ "${ban_logreadcmd##*/}" = "logread" ]; then - logread_cmd="${ban_logreadcmd} -fe \"${ban_logterm}\" 2>/dev/null" - loglimit_cmd="${ban_logreadcmd} -l ${ban_loglimit} 2>/dev/null" - logread_filter="" + log_type="logread" fi # start log monitoring # - if [ -n "${logread_cmd}" ] && [ -n "${loglimit_cmd}" ] && [ -n "${ban_logterm}" ] && [ "${ban_loglimit}" != "0" ]; then + if [ -n "${log_type}" ] && [ -n "${ban_logterm}" ] && [ "${ban_loglimit}" != "0" ]; then f_log "info" "start detached banIP log service (${ban_logreadcmd})" + + # determine nft timeout expression and cache interval + # if printf "%s" "${ban_nftexpiry}" | grep -qE '^([1-9][0-9]*(ms|s|m|h|d|w))+$'; then nft_expiry="timeout ${ban_nftexpiry}" + cache_interval="$(printf "%s" "${ban_nftexpiry}" | "${ban_awkcmd}" '{ + s = 0 + str = $0 + while (match(str, /([0-9]+)(ms|s|m|h|d|w)/, a)) { + if (a[2] == "ms") s += a[1] / 1000 + else if (a[2] == "s") s += a[1] + else if (a[2] == "m") s += a[1] * 60 + else if (a[2] == "h") s += a[1] * 3600 + else if (a[2] == "d") s += a[1] * 86400 + else if (a[2] == "w") s += a[1] * 604800 + str = substr(str, RSTART + RLENGTH) + } + interval = int(s / 2) + if (interval < 30) interval = 30 + if (interval > 300) interval = 300 + printf "%d", interval + }')" fi + # helper function to extract space-padded bare IPs/CIDRs from nft set listing + # + nft_cache() { + "${ban_nftcmd}" list set inet banIP "${1}" 2>/dev/null | \ + "${ban_awkcmd}" '{gsub(/[,{}]/, " "); for(i=1;i<=NF;i++) if($i~/^[0-9A-Fa-f].*[.:]/) printf " %s ",$i}' + } + # retrieve/cache current allowlist/blocklist content # - allow_v4="$("${ban_nftcmd}" list set inet banIP allowlist.v4 2>/dev/null)" - allow_v6="$("${ban_nftcmd}" list set inet banIP allowlist.v6 2>/dev/null)" - block_v4="$("${ban_nftcmd}" list set inet banIP blocklist.v4 2>/dev/null)" - block_v6="$("${ban_nftcmd}" list set inet banIP blocklist.v6 2>/dev/null)" + allow_v4="$(nft_cache allowlist.v4)" + allow_v6="$(nft_cache allowlist.v6)" + block_v4="$(nft_cache blocklist.v4)" + block_v6="$(nft_cache blocklist.v6)" + + # initial cache timestamp and datestamp + # + read -r cache_ts _ < "/proc/uptime" + cache_ts="${cache_ts%%.*}" + date_stamp="$(date "+%Y-%m-%d %H:%M:%S")" + block_cache="" + + # clean up stale RDAP lock/done markers from previous runs + # + "${ban_rmcmd}" -f "${ban_rdapfile}".*.lock "${ban_rdapfile}".*.done >/dev/null 2>&1 # 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 # - pipeline_cmd="${logread_cmd}" - [ -n "${logread_filter}" ] && pipeline_cmd="${pipeline_cmd} | ${logread_filter}" - eval "${pipeline_cmd}" | while read -r line; do - proto="" - base="" - : >"${ban_rdapfile}" - - # IP detection - # - ip_proto=$(printf "%s" "${line}" | "${ban_awkcmd}" ' - { - gsub(/[<>[\]]/, "", $0) - sub(/%.*/, "", $0) - sub(/:[0-9]+([ >]|$)/, "\\1", $0) - if (match($0, /([0-9]{1,3}\.){3}[0-9]{1,3}/, m4)) { - if (m4[0] !~ /^127\./ && m4[0] !~ /^0\./) { - print m4[0] " .v4" - exit - } + { + case "${log_type}" in + tail) + "${ban_logreadcmd}" -qf "${ban_logreadfile}" 2>/dev/null \ + | "${ban_grepcmd}" -e "${ban_logterm}" 2>/dev/null + ;; + logread) + "${ban_logreadcmd}" -fe "${ban_logterm}" 2>/dev/null + ;; + esac + } | "${ban_awkcmd}" -v threshold="${ban_logcount}" -v limit=5000 ' + BEGIN { + unique = 0 + } + { + gsub(/[<>[\]]/, "", $0) + sub(/%.*/, "", $0) + sub(/:[0-9]+([ >]|$)/, "\\1", $0) + ip = "" + proto = "" + if (match($0, /([0-9]{1,3}\.){3}[0-9]{1,3}/, m4)) { + if (m4[0] !~ /^127\./ && m4[0] !~ /^0\./) { + ip = m4[0] + proto = ".v4" } - if (match($0, /([A-Fa-f0-9]{1,4}:){2,7}[A-Fa-f0-9]{1,4}/, m6)) { - if (m6[0] ~ /^[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}$/) next - if (m6[0] !~ /^([A-Fa-f0-9]{2}:){5}[A-Fa-f0-9]{2}$/) { - print m6[0] " .v6" - exit - } + } + if (!ip && match($0, /([A-Fa-f0-9]{1,4}:){2,7}[A-Fa-f0-9]{1,4}/, m6)) { + if (m6[0] !~ /^[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}$/ && m6[0] !~ /^([A-Fa-f0-9]{2}:){5}[A-Fa-f0-9]{2}$/) { + ip = m6[0] + proto = ".v6" } - }' - ) - ip="${ip_proto% *}" - proto="${ip_proto#* }" + } + 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 detected IP + # process only BLOCK/RESET actions emitted by awk # - if [ -n "${proto}" ]; then - case "${proto}" in - .v4) - case "${allow_v4} ${block_v4}" in - *" ${ip} "* ) - continue - ;; - esac - ;; - .v6) - case "${allow_v6} ${block_v6}" in - *" ${ip} "* ) - continue - ;; - esac - ;; - esac - f_log "info" "suspicious IP '${ip}'" - log_count="$(${loglimit_cmd} | "${ban_grepcmd}" -F -c "suspicious IP '${ip}'")" - if [ "${log_count}" -ge "${ban_logcount}" ]; then + 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="" + "${ban_rmcmd}" -f "${ban_rdapfile}".*.done >/dev/null 2>&1 + f_log "debug" "f_monitor ::: refreshed monitor cache at ${date_stamp}" + fi + + # fast exact string match against cached Set content + # + case "${proto}" in + .v4) + case "${allow_v4} ${block_v4} ${block_cache}" in + *" ${ip} "*) + f_log "debug" "f_monitor ::: skip IP '${ip}', found in cached IP${proto} Sets" + continue + ;; + esac + ;; + .v6) + case "${allow_v6} ${block_v6} ${block_cache}" in + *" ${ip} "*) + f_log "debug" "f_monitor ::: skip IP '${ip}', found in cached IP${proto} Sets" + continue + ;; + esac + ;; + esac + + # CIDR-aware allowlist lookup (only at block-time, not every IP) + # + if "${ban_nftcmd}" get element inet banIP "allowlist${proto}" { ${ip} } >/dev/null 2>&1; then + block_cache="${block_cache} ${ip} " + f_log "debug" "f_monitor ::: skip IP '${ip}', found via allowlist CIDR lookup" + continue + fi + + # try to add IP to the blocklist Set with appropriate expiry + # if "${ban_nftcmd}" add element inet banIP "blocklist${proto}" { ${ip} ${nft_expiry} } >/dev/null 2>&1; then - f_log "info" "add IP '${ip}' (expiry: ${ban_nftexpiry:-"-"}) to blocklist${proto} set" - [ "${proto}" = ".v4" ] && block_v4="$("${ban_nftcmd}" list set inet banIP blocklist.v4 2>/dev/null)" - [ "${proto}" = ".v6" ] && block_v6="$("${ban_nftcmd}" list set inet banIP blocklist.v6 2>/dev/null)" + 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" + 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 - rdap_log="$("${ban_fetchcmd}" ${ban_rdapparm} "${ban_rdapfile}" "${ban_rdapurl}${ip}" 2>&1)" - rdap_rc="${?}" - if [ "${rdap_rc}" = "0" ] && [ -s "${ban_rdapfile}" ]; then - [ "${proto}" = ".v4" ] && rdap_idx="$("${ban_jsoncmd}" -i "${ban_rdapfile}" -qe '@.cidr0_cidrs[@.v4prefix].*' | "${ban_awkcmd}" '{ORS=" "; print}')" - [ "${proto}" = ".v6" ] && rdap_idx="$("${ban_jsoncmd}" -i "${ban_rdapfile}" -qe '@.cidr0_cidrs[@.v6prefix].*' | "${ban_awkcmd}" '{ORS=" "; print}')" - rdap_info="$("${ban_jsoncmd}" -l1 -i "${ban_rdapfile}" -qe '@.country' -qe '@.notices[@.title="Source"].description[1]' | "${ban_awkcmd}" 'BEGIN{RS="";FS="\n"}{printf "%s, %s",$1,$2}')" - [ -z "${rdap_info}" ] && rdap_info="$("${ban_jsoncmd}" -l1 -i "${ban_rdapfile}" -qe '@.notices[0].links[0].value' | "${ban_awkcmd}" 'BEGIN{FS="[/.]"}{printf"%s, %s","n/a",toupper($4)}')" - for idx in ${rdap_idx}; do - if [ -z "${base}" ]; then - base="${idx}" - continue - else - if [ -n "${base%%::*}" ] && [ "${base%%.*}" != "127" ] && [ "${base%%.*}" != "0" ]; then - 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 - fi + + # 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 + ( + # rate limiting via shared timestamp file + # + : >"${rdap_lock}" + 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}" + + : >"${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"}{printf "%s, %s",$1,$2}')" + [ -z "${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 + if [ -n "${base%%::*}" ] && [ "${base%%.*}" != "127" ] && [ "${base%%.*}" != "0" ]; then + 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 + fi + base="" + fi + done + else + f_log "info" "rdap request failed (rc: ${rdap_rc:-"-"}/log: ${rdap_log:-"-"}) for IP '${ip}'" fi - done - else - f_log "info" "rdap request failed (rc: ${rdap_rc:-"-"}/log: ${rdap_log})" + "${ban_rmcmd}" -f "${ban_rdapfile}.${ip}" "${rdap_lock}" + : >"${ban_rdapfile}.${ip}.done" + ) & + fi fi fi - if [ -z "${ban_nftexpiry}" ] && [ "${ban_autoblocklist}" = "1" ] && ! "${ban_grepcmd}" -q "^${ip}" "${ban_blocklist}"; then - printf "%-45s%s\n" "${ip}" "# added on $(date "+%Y-%m-%d %H:%M:%S")" >>"${ban_blocklist}" + + # 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 - fi - 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 @@ -2436,6 +2741,7 @@ ban_gzipcmd="$(f_cmd gzip)" ban_sortcmd="$(f_cmd sort)" ban_wccmd="$(f_cmd wc)" ban_mvcmd="$(f_cmd mv)" +ban_rmcmd="$(f_cmd rm)" f_system if [ "${ban_action}" != "stop" ]; then diff --git a/net/banip/files/banip-service.sh b/net/banip/files/banip-service.sh index b59cbb5c34dbc..b0952f033075b 100755 --- a/net/banip/files/banip-service.sh +++ b/net/banip/files/banip-service.sh @@ -7,7 +7,8 @@ # shellcheck disable=all ban_action="${1}" -ban_starttime="$(date "+%s")" +read -r ban_starttime _ < "/proc/uptime" +ban_starttime="${ban_starttime%%.*}" ban_funlib="/usr/lib/banip-functions.sh" [ -z "${ban_bver}" ] && . "${ban_funlib}" @@ -61,10 +62,20 @@ for feed in allowlist ${ban_feed} blocklist; do # skip external feeds in allowlistonly mode # - if [ "${ban_allowlistonly}" = "1" ] && - ! printf "%s" "${ban_feedin}" | "${ban_grepcmd}" -q "allowlist" && - ! printf "%s" "${ban_feedout}" | "${ban_grepcmd}" -q "allowlist"; then - continue + if [ "${ban_allowlistonly}" = "1" ]; then + case "${ban_feedin}" in + *" allowlist "*) + ;; + *) + case "${ban_feedout}" in + *" allowlist "*) + ;; + *) + continue + ;; + esac + ;; + esac fi # external feeds (parallel processing on multicore hardware) @@ -77,7 +88,7 @@ for feed in allowlist ${ban_feed} blocklist; do fi json_objects="url_4 url_6 rule chain flag" for object in ${json_objects}; do - eval json_get_var feed_"${object}" '${object}' >/dev/null 2>&1 + json_get_var "feed_${object}" "${object}" >/dev/null 2>&1 done json_select .. @@ -95,11 +106,15 @@ for feed in allowlist ${ban_feed} blocklist; do feed_ipv="4" if [ "${feed}" = "country" ] && [ "${ban_countrysplit}" = "1" ]; then for country in ${ban_country}; do - f_down "${feed}.${country}" "${feed_ipv}" "${feed_url_4}" "${feed_rule}" "${feed_chain:-"in"}" "${feed_flag}" + (f_down "${feed}.${country}" "${feed_ipv}" "${feed_url_4}" "${feed_rule}" "${feed_chain:-"in"}" "${feed_flag}") & + [ "${cnt}" -gt "${ban_cores}" ] && wait -n + cnt="$((cnt + 1))" done elif [ "${feed}" = "asn" ] && [ "${ban_asnsplit}" = "1" ]; then for asn in ${ban_asn}; do - f_down "${feed}.${asn}" "${feed_ipv}" "${feed_url_4}" "${feed_rule}" "${feed_chain:-"in"}" "${feed_flag}" + (f_down "${feed}.${asn}" "${feed_ipv}" "${feed_url_4}" "${feed_rule}" "${feed_chain:-"in"}" "${feed_flag}") & + [ "${cnt}" -gt "${ban_cores}" ] && wait -n + cnt="$((cnt + 1))" done else if [ "${feed_url_4}" = "${feed_url_6}" ]; then @@ -119,11 +134,15 @@ for feed in allowlist ${ban_feed} blocklist; do feed_ipv="6" if [ "${feed}" = "country" ] && [ "${ban_countrysplit}" = "1" ]; then for country in ${ban_country}; do - f_down "${feed}.${country}" "${feed_ipv}" "${feed_url_6}" "${feed_rule}" "${feed_chain:-"in"}" "${feed_flag}" + (f_down "${feed}.${country}" "${feed_ipv}" "${feed_url_6}" "${feed_rule}" "${feed_chain:-"in"}" "${feed_flag}") & + [ "${cnt}" -gt "${ban_cores}" ] && wait -n + cnt="$((cnt + 1))" done elif [ "${feed}" = "asn" ] && [ "${ban_asnsplit}" = "1" ]; then for asn in ${ban_asn}; do - f_down "${feed}.${asn}" "${feed_ipv}" "${feed_url_6}" "${feed_rule}" "${feed_chain:-"in"}" "${feed_flag}" + (f_down "${feed}.${asn}" "${feed_ipv}" "${feed_url_6}" "${feed_rule}" "${feed_chain:-"in"}" "${feed_flag}") & + [ "${cnt}" -gt "${ban_cores}" ] && wait -n + cnt="$((cnt + 1))" done else (f_down "${feed}" "${feed_ipv}" "${feed_url_6}" "${feed_rule}" "${feed_chain:-"in"}" "${feed_flag}") & diff --git a/net/banip/files/banip.cgi b/net/banip/files/banip.cgi index 770bce53e3433..7fedfd168461b 100644 --- a/net/banip/files/banip.cgi +++ b/net/banip/files/banip.cgi @@ -6,31 +6,56 @@ # (s)hellcheck exceptions # shellcheck disable=all -# handle post/get requests +# output HTTP response header # -post_string="$(cat)" +cat <|] Check if an element exists in a banIP Set" extra_command "content" "[] [true|false] Listing of all or only elements with hits of a given banIP Set" +extra_command "actual" "Print nft/monitor actuals" ban_init="/etc/init.d/banip" ban_service="/usr/bin/banip-service.sh" ban_funlib="/usr/lib/banip-functions.sh" -ban_pidfile="/var/run/banip.pid" -ban_lock="/var/run/banip.lock" +ban_pidfile="/var/run/banIP/banIP.pid" +ban_lock="/var/run/banIP/banIP.lock" if [ -z "${IPKG_INSTROOT}" ]; then - if [ "${action}" = "boot" ] && "${ban_init}" running; then - exit 0 - elif { [ "${action}" = "stop" ] || [ "${action}" = "report" ] || [ "${action}" = "search" ] || [ "${action}" = "content" ]; } && ! "${ban_init}" running; then - exit 0 - fi - if [ ! -d "${ban_lock}" ] && - { [ "${action}" = "boot" ] || [ "${action}" = "start" ] || [ "${action}" = "restart" ] || [ "${action}" = "reload" ] || [ "${action}" = "search" ]; }; then - mkdir -p "${ban_lock}" - elif [ -d "${ban_lock}" ] && - { [ "${action}" = "boot" ] || [ "${action}" = "start" ] || [ "${action}" = "restart" ] || [ "${action}" = "reload" ] || [ "${action}" = "search" ]; }; then - exit 1 - fi + case "${action}" in + "boot") + "${ban_init}" running && exit 0 + ;; + "stop"|"report"|"content") + "${ban_init}" running || exit 0 + ;; + esac + case "${action}" in + "boot"|"start"|"restart"|"reload"|"search") + if [ -d "${ban_lock}" ]; then + exit 1 + fi + mkdir -p "${ban_lock}" + ;; + esac . "${ban_funlib}" fi @@ -95,6 +100,10 @@ content() { f_content "${1}" "${2:-"false"}" } +actual() { + f_actual +} + service_triggers() { local iface trigger delay diff --git a/net/banip/files/banip.tpl b/net/banip/files/banip.tpl index a542f6b0d5c4e..cd786a5d61b76 100644 --- a/net/banip/files/banip.tpl +++ b/net/banip/files/banip.tpl @@ -2,33 +2,46 @@ # Copyright (c) 2018-2026 Dirk Brenken (dev@brenken.org) # This is free software, licensed under the GNU General Public License v3. -# info preparation -# -local banip_info report_info log_info system_info mail_text logread_cmd +local banip_info report_info log_info system_info mail_text +# log info preparation +# if [ -f "${ban_logreadfile}" ] && [ -x "${ban_logreadcmd}" ] && [ "${ban_logreadcmd##*/}" = "tail" ]; then - logread_cmd="${ban_logreadcmd} -qn ${ban_loglimit} ${ban_logreadfile} 2>/dev/null | ${ban_grepcmd} -e \"banIP/\" 2>/dev/null" + log_info="$("${ban_logreadcmd}" -qn "${ban_loglimit}" "${ban_logreadfile}" 2>/dev/null | "${ban_grepcmd}" -e "banIP/" 2>/dev/null)" elif [ -x "${ban_logreadcmd}" ] && [ "${ban_logreadcmd##*/}" = "logread" ]; then - logread_cmd="${ban_logreadcmd} -l ${ban_loglimit} -e \"banIP/\" 2>/dev/null" + log_info="$("${ban_logreadcmd}" -l "${ban_loglimit}" -e "banIP-" 2>/dev/null)" fi +# banIP status and report info preparation +# banip_info="$(/etc/init.d/banip status 2>/dev/null)" -report_info="$("${ban_catcmd}" "${ban_reportdir}/ban_report.txt" 2>/dev/null)" -log_info="$(eval "${logread_cmd}" 2>/dev/null)" +report_info="$(< "${ban_reportdir}/ban_report.txt")" 2>/dev/null system_info="$(strings /etc/banner 2>/dev/null; "${ban_ubuscmd}" call system board | \ "${ban_awkcmd}" 'BEGIN{FS="[{}\"]"}{if($2=="kernel"||$2=="hostname"||$2=="system"||$2=="model"||$2=="description")printf " + %-12s: %s\n",$2,$4}')" -# content header -# -mail_text="$(printf "%s\n" "
")"
-
-# content body
-#
-mail_text="$(printf "%s\n" "${mail_text}\n++\n++ System Information ++\n++\n${system_info:-"-"}")"
-mail_text="$(printf "%s\n" "${mail_text}\n\n++\n++ banIP Status ++\n++\n${banip_info:-"-"}")"
-[ -n "${report_info}" ] && mail_text="$(printf "%s\n" "${mail_text}\n\n++\n++ banIP Report ++\n++\n${report_info}")"
-[ -n "${log_info}" ] && mail_text="$(printf "%s\n" "${mail_text}\n\n++\n++ Logfile Information ++\n++\n${log_info}")"
-
-# content footer
+# mail text preparation
 #
-mail_text="$(printf "%s\n" "${mail_text}
")" +mail_text="$( + printf "%s\n" "
"
+	printf "\n%s\n" "++
+++ System Information ++
+++"
+	printf "%s\n" "${system_info:-"-"}"
+	printf "\n%s\n" "++
+++ banIP Status ++
+++"
+	printf "%s\n" "${banip_info:-"-"}"
+	[ -n "${report_info}" ] && {
+		printf "\n%s\n" "++
+++ banIP Report ++
+++"
+		printf "%s\n" "${report_info}"
+	}
+	[ -n "${log_info}" ] && {
+		printf "\n%s\n" "++
+++ Logfile Information ++
+++"
+		printf "%s\n" "${log_info}"
+	}
+	printf "%s\n" "
" +)"