include $(TOPDIR)/rules.mk
PKG_NAME:=banip
-PKG_VERSION:=1.8.13
-PKG_RELEASE:=2
+PKG_VERSION:=1.9.0
+PKG_RELEASE:=1
PKG_LICENSE:=GPL-3.0-or-later
PKG_MAINTAINER:=Dirk Brenken <dev@brenken.org>
| 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 (also controls the monitor cache refresh interval) |
+| ban_nftexpiry | option | - | expiry time (ms|s|m|h|d) 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 |
~# /etc/init.d/banip status
::: banIP runtime information
+ status : active (nft: ✔, monitor: ✔)
- + frontend_ver : 1.8.9-r1
- + backend_ver : 1.8.9-r1
- + element_count : 138 148 (chains: 7, sets: 13, rules: 50)
- + active_feeds : allowlist.v4MAC, allowlist.v6MAC, allowlist.v4, allowlist.v6, dns.v4, blocklist.v4MAC, blocklist.v6MAC, doh.v6, blocklist.v4, doh.v4, blocklist.v6, dns.v6, hagezi.v4
- + active_devices : wan: pppoe-wan / wan-if: wan, wan_6 / vlan-allow: - / vlan-block: -
- + active_uplink : 5.73.162.23, 2a13:4800:204:319e:b26d:238b:d7fe:8213
- + nft_info : ver: 1.1.6-r1, priority: -100, policy: performance, loglevel: warn, expiry: 2h, limit (icmp/syn/udp): 25/10/100
+ + frontend_ver : 1.9.0-r1
+ + backend_ver : 1.9.0-r1
+ + element_count : 111 582 (chains: 7, sets: 21, rules: 50)
+ + active_feeds : allowlist.v4MAC, allowlist.v6MAC, allowlist.v4, allowlist.v6, cinsscore.v4, debl.v4, country.v6, debl.v6, doh.v4, doh.v6, spamhaus.v4, ipthreat.v4, country.v4, spamhaus.v6, threat.v4, turris.v4, turris.v6, blocklist.v4MAC, blocklist.v6MAC, blocklist.v4, blocklist.v6
+ + active_devices : trigger: wan / wan: pppoe-wan / wan-if: wan, wan_6 / vlan-allow: - / vlan-block: -
+ + active_uplink : 46.167.33.169, 2a04:5700:304:ea66:e1fa:9a7f:ff78:ec41
+ + nft_info : ver: 1.1.6-r4, priority: -100, policy: performance, loglevel: warn, expiry: 2h, limit (icmp/syn/udp): 25/10/100, loglimit (rate/burst): 10/5
+ run_info : base: /mnt/data/banIP, backup: /mnt/data/banIP/backup, report: /mnt/data/banIP/report, error: /mnt/data/banIP/error
- + run_flags : auto: â\9c\94, proto (4/6): â\9c\94/â\9c\94, bcp38: â\9c\94, log (pre/in/out): â\9c\98/â\9c\98/â\9c\94, count: â\9c\94, dedup: â\9c\94, split: â\9c\98, custom feed: â\9c\98, allowed only: â\9c\98
- + last_run : mode: restart, 2026-01-12 06:16:19, duration: 0m 36s, memory: 1446.84 MB available
- + system_info : cores: 4, log: logread, fetch: curl, Bananapi BPI-R3, mediatek/filogic, OpenWrt SNAPSHOT (r32542-bf46d119a2)
+ + run_flags : auto: â\9c\94, proto (4/6): â\9c\94/â\9c\94, bcp38: â\9c\94, log (pre/in/out): â\9c\98/â\9c\98/â\9c\98, count: â\9c\94, dedup: â\9c\94, split: â\9c\98, custom feed: â\9c\98, allowed only: â\9c\98, debug: â\9c\94
+ + last_run : mode: restart, date / time: 2026-09-19 15:27:29, duration: 0m 28s, memory: 1122.99 MB available
+ + system_info : cores: 4, log: logread, fetch: curl, Bananapi BPI-R3, mediatek/filogic, OpenWrt SNAPSHOT (r36370-5369699684)
```
**banIP search information**
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.
+Entries in CIDR notation always refer to the whole network - set host bits are masked by nftables, e.g. `192.168.1.5/24` ends up as `192.168.1.0/24` in the Set and `2001:db8::e65f:1:2:3/64` as `2001:db8::/64`. To allow or block a single host omit the prefix length or use /32 resp. /128.
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.
By default banIP uses the following pre-configured download options:
```
- * curl: --connect-timeout 20 --retry-delay 10 --retry 4 --retry-max-time 80 --retry-all-errors --fail --silent --show-error --location -o
+ * curl: --connect-timeout 20 --retry-delay 10 --retry 4 --retry-max-time 80 --retry-all-errors --fail --silent --globoff --show-error --location -o
* wget: --no-cache --no-cookies --timeout=20 --waitretry=10 --tries=5 --retry-connrefused -O
* uclient-fetch: --timeout=20 -O
```
The object supports the following fields:
| Field | Required | Description |
-| :------ | :------: | :--------------------------------------------------------------------------------------------------------------------- |
+| :------ | :------: | :----------------------------------------------------------------------------------------------------------------------- |
| url_4 | yes\* | download URL of the IPv4 list. \*at least one of `url_4`/`url_6` must be present |
-| url_6 | yes\* | download URL of the IPv6 list. May point to the **same** URL as `url_4` if the source mixes IPv4 and IPv6 in one file |
-| rule | yes | the parsing ruleset, max. 4 space separated parameters (see below) |
-| chain | yes | the default chain/direction: `in`, `out` or `inout` (see below) |
-| descr | yes | a short human-readable description shown in LuCI and the feed table |
-| flag | no | optional, space separated list of extra options: archive format and/or protocol/port limitations (see below) |
+| url_6 | yes\* | download URL of the IPv6 list. May point to the **same** URL as `url_4` if the source mixes IPv4 and IPv6 in one file |
+| rule | yes | the parsing ruleset, max. 4 space separated parameters (see below) |
+| chain | yes | the default chain/direction: `in`, `out` or `inout` (see below) |
+| descr | yes | a short human-readable description shown in LuCI and the feed table |
+| flag | no | optional, space separated list of extra options: archive format and/or protocol/port limitations (see below) |
**The `url_4` / `url_6` fields**
Each address family is fetched and processed independently. Three cases:
* separate IPv4 and IPv6 files: set both to their respective URLs (e.g. `doh`, `spamhaus`)
* a single dual-stack file that mixes v4 and v6 entries: point both `url_4` and `url_6` at that same URL. banIP fetches it only once and process it for each family and the per-family regex extracts the matching addresses; the non-matching lines are simply ignored. (e.g. `threatview`, which ships v4 and v6 in one file).
+The `country` and `asn` feeds are special: banIP fetches one file per selected country code / ASN, so their URLs are templates. The placeholder is replaced by the configured value as-is, so country codes have to be lowercase, just like in banip.countries. The placeholder `{country}` is replaced by the lowercase ISO country code, `{asn}` by the bare AS number, e.g. `https://www.ipdeny.com/ipblocks/data/aggregated/{country}-aggregated.zone` or `https://asn.ipinfo.app/api/text/list/AS{asn}`. A URL without placeholder is treated as a base path and gets the legacy suffix appended (`<cc>-aggregated.zone` resp. `AS<asn>`), so older custom feed files keep working. The placeholder can sit anywhere in the URL, which allows to switch the country source to a differently structured one. Example for a GeoLite2-based alternative, which ships IPv4 and IPv6 in one file per country:
+
+```json
+ "country":{
+ "url_4": "https://raw.githubusercontent.com/Loyalsoldier/geoip/release/text/{country}.txt",
+ "url_6": "https://raw.githubusercontent.com/Loyalsoldier/geoip/release/text/{country}.txt",
+ "rule": "feed 1",
+ "chain": "in",
+ "descr": "country blocks (GeoLite2)"
+ },
+```
+
+Pointing `url_4` and `url_6` at the same file is the dual-stack case described above, so each country file is downloaded only once and processed for both address families. This source includes GeoLite2 data created by MaxMind, available from https://www.maxmind.com - its use is subject to the MaxMind GeoLite End User License Agreement.
+
+Keep in mind that alternative sources come with their own license terms, which you have to check yourself, and that they differ in semantics (geolocation vs. RIR allocation), granularity (set sizes and memory footprint may grow considerably) and code coverage (a missing country file only logs a download failure for that code).
+
**The `rule` field**
The rule consists of max. 4 individual, space separated parameters:
1. **type**: `feed` or `suricata` (required)
case "${ban_fetchcmd##*/}" in
"curl")
[ "${ban_fetchinsecure}" = "1" ] && insecure="--insecure"
- ban_fetchparm="${ban_fetchparm:-"${insecure} --connect-timeout 20 --retry-delay 10 --retry $((ban_fetchretry - 1)) --retry-max-time $(((ban_fetchretry - 1) * 20)) --retry-all-errors --fail --silent --show-error --location -o"}"
- ban_rdapparm="--connect-timeout 5 --silent --location -o"
- ban_etagparm="--connect-timeout 5 --silent --location --head"
- ban_geoparm="--connect-timeout 5 --silent --location --data"
+ 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"
# 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
- return
- else
- f_log "info" "can't load banIP custom feed file"
+ 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"
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() {
if [ "${ban_countrysplit}" = "1" ]; then
country="${feed%.*}"
country="${country#*.}"
- f_etag "${feed}" "${feed_url}${country}-aggregated.zone" ".${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}" "${feed_url}${country}-aggregated.zone" ".${country}" "${etag_cnt}"; then
+ if ! f_etag "${feed}" "$(f_feedurl "${feed_url}" "country" "${country}")" ".${country}" "${etag_cnt}"; then
etag_rc="$((etag_rc + 1))"
fi
done
if [ "${ban_asnsplit}" = "1" ]; then
asn="${feed%.*}"
asn="${asn#*.}"
- f_etag "${feed}" "${feed_url}AS${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}" "${feed_url}AS${asn}" ".${asn}" "${etag_cnt}"; then
+ if ! f_etag "${feed}" "$(f_feedurl "${feed_url}" "asn" "${asn}")" ".${asn}" "${etag_cnt}"; then
etag_rc="$((etag_rc + 1))"
fi
done
if [ "${feed%%.*}" = "country" ]; then
if [ "${ban_countrysplit}" = "0" ]; then
for country in ${ban_country}; do
- if "${ban_fetchcmd}" ${ban_fetchparm} "${tmp_raw}" "${feed_url}${country}-aggregated.zone" 2>>"${ban_errorlog}"; then
+ if "${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="${?}"
else
country="${feed%.*}"
country="${country#*.}"
- if "${ban_fetchcmd}" ${ban_fetchparm} "${tmp_load}" "${feed_url}${country}-aggregated.zone" 2>>"${ban_errorlog}"; then
+ if "${ban_fetchcmd}" ${ban_fetchparm} "${tmp_load}" "$(f_feedurl "${feed_url}" "country" "${country}")" 2>>"${ban_errorlog}"; then
feed_rc="${?}"
else
feed_rc="4"
elif [ "${feed%%.*}" = "asn" ]; then
if [ "${ban_asnsplit}" = "0" ]; then
for asn in ${ban_asn}; do
- if "${ban_fetchcmd}" ${ban_fetchparm} "${tmp_raw}" "${feed_url}AS${asn}" 2>>"${ban_errorlog}"; then
+ if "${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="${?}"
else
asn="${feed%.*}"
asn="${asn#*.}"
- if "${ban_fetchcmd}" ${ban_fetchparm} "${tmp_load}" "${feed_url}AS${asn}" 2>>"${ban_errorlog}"; then
+ if "${ban_fetchcmd}" ${ban_fetchparm} "${tmp_load}" "$(f_feedurl "${feed_url}" "asn" "${asn}")" 2>>"${ban_errorlog}"; then
feed_rc="${?}"
else
feed_rc="4"
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
+ 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}")"
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}"
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="wan: ${values// /, } / "
+ 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}" = "active" ] && value="${value} ($(f_actual))"
fi
fi
- if [ "${key}" != "wan_interfaces" ] && [ "${key}" != "vlan_allow" ] && [ "${key}" != "vlan_block" ]; then
+ if [ "${key}" != "trigger_interfaces" ] && [ "${key}" != "wan_interfaces" ] && [ "${key}" != "vlan_allow" ] && [ "${key}" != "vlan_block" ]; then
printf ' + %-17s : %s\n' "${key}" "${value:-"-"}"
fi
done
# 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
+ 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|w)/, a)) {
+ 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
- else if (a[2] == "w") s += a[1] * 604800
str = substr(str, RSTART + RLENGTH)
}
interval = int(s / 2)
if [ "${ban_protov4}" = "1" ] && [ -n "${feed_url_4}" ] && [ -n "${feed_rule}" ]; then
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}") &
- [ "${cnt}" -ge "${ban_cores}" ] && wait -n
- cnt="$((cnt + 1))"
- done
+ if [ "${feed_url_4}" = "${feed_url_6}" ]; then
+ feed_url_6="local"
+ for country in ${ban_country}; do
+ f_down "${feed}.${country}" "${feed_ipv}" "${feed_url_4}" "${feed_rule}" "${feed_chain:-"in"}" "${feed_flag}"
+ done
+ else
+ for country in ${ban_country}; do
+ (f_down "${feed}.${country}" "${feed_ipv}" "${feed_url_4}" "${feed_rule}" "${feed_chain:-"in"}" "${feed_flag}") &
+ [ "${cnt}" -ge "${ban_cores}" ] && wait -n
+ cnt="$((cnt + 1))"
+ done
+ fi
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}") &
- [ "${cnt}" -ge "${ban_cores}" ] && wait -n
- cnt="$((cnt + 1))"
- done
+ if [ "${feed_url_4}" = "${feed_url_6}" ]; then
+ feed_url_6="local"
+ for asn in ${ban_asn}; do
+ f_down "${feed}.${asn}" "${feed_ipv}" "${feed_url_4}" "${feed_rule}" "${feed_chain:-"in"}" "${feed_flag}"
+ done
+ else
+ for asn in ${ban_asn}; do
+ (f_down "${feed}.${asn}" "${feed_ipv}" "${feed_url_4}" "${feed_rule}" "${feed_chain:-"in"}" "${feed_flag}") &
+ [ "${cnt}" -ge "${ban_cores}" ] && wait -n
+ cnt="$((cnt + 1))"
+ done
+ fi
else
if [ "${feed_url_4}" = "${feed_url_6}" ]; then
feed_url_6="local"
{
"asn":{
- "url_4": "https://asn.ipinfo.app/api/text/list/",
- "url_6": "https://asn.ipinfo.app/api/text/list/",
+ "url_4": "https://asn.ipinfo.app/api/text/list/AS{asn}",
+ "url_6": "https://asn.ipinfo.app/api/text/list/AS{asn}",
"rule": "feed 1",
"chain": "in",
"descr": "ASN IP segments"
"descr": "suspicious attacker IPs"
},
"country":{
- "url_4": "https://www.ipdeny.com/ipblocks/data/aggregated/",
- "url_6": "https://www.ipdeny.com/ipv6/ipaddresses/aggregated/",
+ "url_4": "https://www.ipdeny.com/ipblocks/data/aggregated/{country}-aggregated.zone",
+ "url_6": "https://www.ipdeny.com/ipv6/ipaddresses/aggregated/{country}-aggregated.zone",
"rule": "feed 1",
"chain": "in",
"descr": "country blocks"