mirror of
https://github.com/openwrt/packages.git
synced 2026-10-13 14:27:24 +04:00
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 <dev@brenken.org>
This commit is contained in:
1 parent
f03d40454e
commit
b4616e8060
3 files changed
+129
-46
No files matched your search
+2
-2
@@ -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 <dev@brenken.org>
|
||||
|
||||
|
||||
@@ -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**
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user