From: Hauke Mehrtens Date: Sat, 27 Jun 2026 23:02:21 +0000 (+0200) Subject: banip: release 1.8.10-1 X-Git-Url: http://git.99rst.org/?a=commitdiff_plain;h=b4616e8060d9e3ee148dfebfa7a8f301906e4e72;p=openwrt-packages.git banip: release 1.8.10-1 - fixed log monitor source-IP spoofing (GHSA-r6hx-4f83-vp8m) Fix: per-ban_logterm source anchoring — the real source (last IP in the line) is used by default; opt-in 'first:' prefix for source-first formats (web-server access logs). - add f_mem() helper to read MemAvailable from /proc/meminfo - cap the auto-detected CPU core count by available memory in f_system: ban_cores is limited to MemAvailable / 48 (MiB per job), floored to at least 1 core; this bounds the number of feeds processed in parallel on constrained devices, a user-set ban_cores is still honored as a ceiling (the cap only ever lowers it). - derive the GNU sort buffer size from available memory, applied only when coreutils sort is present; busybox sort is a no-op Signed-off-by: Dirk Brenken --- diff --git a/net/banip/Makefile b/net/banip/Makefile index a35d88e9f..95c27b4ea 100644 --- a/net/banip/Makefile +++ b/net/banip/Makefile @@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=banip -PKG_VERSION:=1.8.9 -PKG_RELEASE:=2 +PKG_VERSION:=1.8.10 +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 7a838eef5..2c955cd3a 100644 --- a/net/banip/files/README.md +++ b/net/banip/files/README.md @@ -420,7 +420,7 @@ Be aware that bidirectional country blocking on large countries quickly inflates **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: +In the default config only the log terms to detect failed login attempts via dropbear and LuCI are in place. The following search patterns have been tested as well: ``` dropbear : 'Exit before auth from' @@ -433,6 +433,19 @@ AdGuard : 'AdGuardHome.*\[error\].*/control/login: from ip' Remote : 'received a suspicious remote IP' ``` +**Source IP position** +For every matched line banIP picks the attacker's address out of the log entry. By default it uses the *last* IP in the line, which is correct for all login-style formats above: the service always appends the real remote address as the final field, after any user-supplied data (such as a login name). This stays robust even when an attacker puts an IP-like string into a username or similar field. + +Some formats instead lead with the source IP — most notably web-server access logs, where the client address is the first field. For those, prefix the term with `first:`: + +``` +nginx : 'first:"[A-Z]+ /wp-login\.php[^"]*" 40[13]' +``` + +The `first:`/`last:` prefix is parsed off before matching and applies only to that single term, so terms with different anchors can be mixed freely. + +Please note: `first:` only fits formats where the source IP is genuinely the first address in the line. Do **not** use it for Suricata fast.log (its leading `[1:2013:7]` signature id) or netfilter `LOG` lines (their leading `MAC=` field), as that leading field would be picked instead of the source — keep those on the default `last`. + 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** diff --git a/net/banip/files/banip-functions.sh b/net/banip/files/banip-functions.sh index a1e51a846..8d1720973 100644 --- a/net/banip/files/banip-functions.sh +++ b/net/banip/files/banip-functions.sh @@ -57,6 +57,7 @@ ban_logratelimit="10" ban_logburstlimit="5" ban_logcount="1" ban_logterm="" +ban_logterm_map="" ban_region="" ban_country="" ban_countrysplit="0" @@ -98,6 +99,8 @@ ban_rdapparm="" ban_etagparm="" ban_geoparm="" ban_cores="" +ban_srtmem="8" +ban_srtopts="" ban_packages="" ban_trigger="" ban_resolver="" @@ -105,10 +108,48 @@ 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 cpu + local free_mem mem_cores ban_debug="$(uci_get banip global ban_debug "0")" ban_cores="$(uci_get banip global ban_cores)" @@ -134,34 +175,24 @@ f_system() { ban_sysver="$("${ban_ubuscmd}" -S call system board 2>>"${ban_errorlog}" | "${ban_jsoncmd}" -ql1 -e '@.model' -e '@.release.target' -e '@.release.distribution' -e '@.release.version' -e '@.release.revision' | "${ban_awkcmd}" 'BEGIN{RS="";FS="\n"}{printf "%s, %s, %s %s (%s)",$1,$2,$3,$4,$5}')" - if [ -z "${ban_cores}" ]; then - cpu="$("${ban_grepcmd}" -cm16 '^processor' /proc/cpuinfo 2>>"${ban_errorlog}")" - [ "${cpu}" = "0" ] && cpu="1" - ban_cores="${cpu}" - fi -} - -# command selector -# -f_cmd() { - local cmd pri_cmd="${1}" sec_cmd="${2}" - - # check primary command, - # if not found check secondary command if provided, otherwise log error + # detect cpu cores and cap them by available memory for memory-aware + # parallel processing (>= 48 MiB per job, floored to 1 core); a user-set + # ban_cores is only ever lowered by the cap, never raised # - 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}" + [ -z "${ban_cores}" ] && ban_cores="$("${ban_grepcmd}" -cm16 '^processor' /proc/cpuinfo 2>>"${ban_errorlog}")" + case "${ban_cores}" in "" | 0 | *[!0-9]*) ban_cores="1" ;; esac + free_mem="$(f_mem)" + mem_cores="$((free_mem / 48))" + [ "${mem_cores}" -lt "1" ] && mem_cores="1" + [ "${ban_cores}" -gt "1" ] && [ "${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 } @@ -304,18 +335,33 @@ f_conf() { esac } list_cb() { - local append option="${1}" value="${2//\"/\\\"}" + 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}\\|\${value}\"" + eval "${option}=\"\${append}\\|${pat}\"" else - eval "${option}=\"\${value}\"" + eval "${option}=\"${pat}\"" fi + ban_logterm_map="${ban_logterm_map}${anchor}$(printf '\037')${pat}$(printf '\036')" ;; *) eval "append=\"\${${option}}\"" @@ -1774,7 +1820,7 @@ f_genstatus() { # memory and nftables version information # - mem_free="$("${ban_awkcmd}" '/^MemAvailable/{printf "%.2f", $2/1024}' "/proc/meminfo" 2>>"${ban_errorlog}")" + 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 @@ -2076,7 +2122,7 @@ f_report() { "${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_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 @@ -2649,25 +2695,49 @@ f_monitor() { "${ban_logreadcmd}" -fe "${ban_logterm}" 2>/dev/null ;; esac - } | "${ban_awkcmd}" -v threshold="${ban_logcount}" -v limit=5000 ' + } | 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) - 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] + cand = pick_ip($0, pos) + if (cand ~ /\./) { + if (cand !~ /^127\./ && cand !~ /^0\./) { + ip = cand proto = ".v4" } - } - if (!ip && match($0, /([A-Fa-f0-9]{1,4}:){2,7}[A-Fa-f0-9]{1,4}/, m6)) { - if (m6[0] !~ /^[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}$/ && m6[0] !~ /^([A-Fa-f0-9]{2}:){5}[A-Fa-f0-9]{2}$/) { - ip = m6[0] + } 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" } }