#!/bin/sh
# banIP main service script - ban incoming and outgoing ip adresses/subnets via sets in nftables
# Copyright (c) 2018-2023 Dirk Brenken (dev@brenken.org)
# This is free software, licensed under the GNU General Public License v3.

# (s)hellcheck exceptions
# shellcheck disable=all

ban_action="${1}"
ban_starttime="$(date "+%s")"
ban_funlib="/usr/lib/banip-functions.sh"
[ -z "$(command -v "f_system")" ] && . "${ban_funlib}"

# load config and set banIP environment
#
f_conf
f_log "info" "start banIP processing (${ban_action})"
f_genstatus "processing"
f_tmp
f_fetch
f_getif
f_getdev
f_getsub
f_mkdir "${ban_backupdir}"
f_mkfile "${ban_blocklist}"
f_mkfile "${ban_allowlist}"

# firewall check
#
if [ "${ban_action}" != "reload" ]; then
	if [ -x "${ban_fw4cmd}" ]; then
		cnt=0
		while [ "${cnt}" -lt "10" ] && ! /etc/init.d/firewall status | grep -q "^active"; do
			cnt="$((cnt + 1))"
			sleep 1
		done
		if ! /etc/init.d/firewall status | grep -q "^active"; then
			f_log "err" "nft based firewall/fw4 not functional"
		fi
	else
		f_log "err" "nft based firewall/fw4 not found"
	fi
fi

# init nft namespace
#
if [ "${ban_action}" != "reload" ] || ! "${ban_nftcmd}" -t list table inet banIP >/dev/null 2>&1; then
	if f_nftinit "${ban_tmpfile}".init.nft; then
		f_log "info" "nft namespace initialized"
	else
		f_log "err" "nft namespace can't be initialized"
	fi
fi

# handle downloads
#
f_log "info" "start banIP download processes"
if [ "${ban_allowlistonly}" = "1" ]; then
	ban_feed=""
else
	json_init
	if ! json_load_file "${ban_basedir}/ban_feeds.json" >/dev/null 2>&1; then
		f_log "err" "banIP feed file can't be loaded"
	fi
	[ "${ban_deduplicate}" = "1" ] && printf "\n" >"${ban_tmpfile}.deduplicate"
fi

cnt="1"
for feed in allowlist ${ban_feed} blocklist; do
	# local feeds
	#
	if [ "${feed}" = "allowlist" ] || [ "${feed}" = "blocklist" ]; then
		for proto in MAC 4 6; do
			[ "${feed}" = "blocklist" ] && wait
			(f_down "${feed}" "${proto}") &
			[ "${feed}" = "blocklist" ] || { [ "${feed}" = "allowlist" ] && [ "${proto}" = "MAC" ]; } && wait
			hold="$((cnt % ban_cores))"
			[ "${hold}" = "0" ] && wait
			cnt="$((cnt + 1))"
		done
		wait
		continue
	fi

	# read external feed information
	#
	if ! json_select "${feed}" >/dev/null 2>&1; then
		continue
	fi
	json_objects="url_4 rule_4 url_6 rule_6 flag"
	for object in ${json_objects}; do
		eval json_get_var feed_"${object}" '${object}' >/dev/null 2>&1
	done
	json_select ..
	# handle IPv4/IPv6 feeds with the same/single download URL
	#
	if [ "${feed_url_4}" = "${feed_url_6}" ]; then
		if [ "${ban_protov4}" = "1" ] && [ -n "${feed_url_4}" ] && [ -n "${feed_rule_4}" ]; then
			(f_down "${feed}" "4" "${feed_url_4}" "${feed_rule_4}" "${feed_flag}") &
			feed_url_6="local"
			wait
		fi
		if [ "${ban_protov6}" = "1" ] && [ -n "${feed_url_6}" ] && [ -n "${feed_rule_6}" ]; then
			(f_down "${feed}" "6" "${feed_url_6}" "${feed_rule_6}" "${feed_flag}") &
			hold="$((cnt % ban_cores))"
			[ "${hold}" = "0" ] && wait
			cnt="$((cnt + 1))"
		fi
		continue
	fi
	# handle IPv4/IPv6 feeds with separated download URLs
	#
	if [ "${ban_protov4}" = "1" ] && [ -n "${feed_url_4}" ] && [ -n "${feed_rule_4}" ]; then
		(f_down "${feed}" "4" "${feed_url_4}" "${feed_rule_4}" "${feed_flag}") &
		hold="$((cnt % ban_cores))"
		[ "${hold}" = "0" ] && wait
		cnt="$((cnt + 1))"
	fi
	if [ "${ban_protov6}" = "1" ] && [ -n "${feed_url_6}" ] && [ -n "${feed_rule_6}" ]; then
		(f_down "${feed}" "6" "${feed_url_6}" "${feed_rule_6}" "${feed_flag}") &
		hold="$((cnt % ban_cores))"
		[ "${hold}" = "0" ] && wait
		cnt="$((cnt + 1))"
	fi
done
wait

# start domain lookup
#
f_log "info" "start detached banIP domain lookup"
(f_lookup "allowlist") &
hold="$((cnt % ban_cores))"
[ "${hold}" = "0" ] && wait
(f_lookup "blocklist") &

# tidy up
#
f_rmset
f_rmdir "${ban_tmpdir}"
f_genstatus "active"
f_log "info" "finished banIP download processes"
rm -rf "${ban_lock}"

# start log service
#
if [ -x "${ban_logreadcmd}" ] && [ -n "${ban_logterm%%??}" ]; then
	f_log "info" "start detached banIP log service"

	nft_expiry="$(printf "%s" "${ban_nftexpiry}" | grep -oE "([0-9]+[h|m|s]$)")"
	[ -n "${nft_expiry}" ] && nft_expiry="timeout ${nft_expiry}"

	# read log continuously with given logterms
	#
	"${ban_logreadcmd}" -fe "${ban_logterm%%??}" 2>/dev/null |
		while read -r line; do
			# IPv4 log parsing
			#
			ip="$(printf "%s" "${line}" | "${ban_awkcmd}" 'BEGIN{RS="(([0-9]{1,3}\\.){3}[0-9]{1,3})+"}{if(!seen[RT]++)printf "%s ",RT}')"
			ip="$(f_trim "${ip}")"
			ip="${ip##* }"
			[ -n "${ip}" ] && proto="v4"
			if [ -z "${proto}" ]; then
				# IPv6 log parsing
				#
				ip="$(printf "%s" "${line}" | "${ban_awkcmd}" 'BEGIN{RS="([A-Fa-f0-9]{1,4}::?){3,7}[A-Fa-f0-9]{1,4}"}{if(!seen[RT]++)printf "%s ",RT}')"
				ip="$(f_trim "${ip}")"
				ip="${ip##* }"
				[ -n "${ip}" ] && proto="v6"
			fi
			if [ -n "${proto}" ] && ! "${ban_nftcmd}" get element inet banIP blocklist"${proto}" "{ ${ip} }" >/dev/null 2>&1; then
				f_log "info" "suspicious IP found '${ip}'"
				log_raw="$("${ban_logreadcmd}" -l "${ban_loglimit}" 2>/dev/null)"
				log_count="$(printf "%s\n" "${log_raw}" | grep -c "found '${ip}'")"
				if [ "${log_count}" -ge "${ban_logcount}" ]; then
					if "${ban_nftcmd}" add element inet banIP "blocklist${proto}" "{ ${ip} ${nft_expiry} }" >/dev/null 2>&1; then
						f_log "info" "added IP '${ip}' (${nft_expiry:-"-"}) to blocklist${proto} set"
						if [ "${ban_autoblocklist}" = "1" ] && ! grep -q "^${ip}" "${ban_blocklist}"; then
							printf "%-42s%s\n" "${ip}" "# added on $(date "+%Y-%m-%d %H:%M:%S")" >>"${ban_blocklist}"
							f_log "info" "added IP '${ip}' to local blocklist"
						fi
					fi
				fi
			fi
		done

# start no-op service loop
#
else
	f_log "info" "start detached no-op banIP service (logterms are missing)"
	while :; do
		sleep 1
	done
fi