#!/bin/bash
# anti-flood script
# Example usage: ./log_flood_block.sh /home/portalwakatobika/access-logs/portal.wakatobikab.go.id-ssl_log
# the script will:
# - filter out any CloudFlare owned IPs and any IPs already present in /etc/csf/csf.deny
# - anything else from that log file will be added to /etc/csf/csf.deny
# - the access-log logfile will be truncated afterwards so the script don't have to process the same IPs again
# - all IPs listed by LiteSpeed (in the /tmp/lshttpd/.rtreport* files under the BLOCKED_IP section) will be blocked,
# excluding CloudFlare IPs and IPs that are already blocked
# - it runs in a loop with 30 seconds pause per round until stopped
#
# 19-11-2023 lukasz@worldhost.group
# 07-03-2025 added auto exit when the flood is over + removal of blocked ips within 24h
# 25-04-2025 cleanup unused function, update selected fragments flagged by shellcheck, added domain check:
# refuse to run if domain is behind cloudflare (provide .htaccess 500 hint);
# added never_block list of IPs
##
bin_grepcidr=$(which grepcidr)
if [[ ! -x ${bin_grepcidr} ]]; then
echo "grepcidr missing, try: yum install grepcidr (epel)"
exit 1
fi
# parse httpd log file, ban any non-cloudflare IPs
function log_parse_and_ban() {
csf_blocked=$(grep "^[1-9]" /etc/csf/csf.deny|awk '{print $1}'|grep ':' -v)
for cur_logline in $(awk '{print $1}' "${log_file}" |sort|uniq|grep -Ev '(:|127.0.0)'); do
# echo "processing ${cur_logline}"
is_cf=$(${bin_grepcidr} 1>/dev/null -f /root/cf_ips.txt <<<"${cur_logline}";echo $?)
is_never_block=$(grep -qw "${cur_logline}" <<<"${never_block}";echo $?)
if [[ ${is_cf} -eq 0 ]]; then
echo "${cur_logline} skipped, CloudFlare IP"
elif [[ ${is_never_block} -eq 0 ]]; then
echo "${cur_logline} skipped, IP on never_block list"
else
# echo -n "checking if ${cur_logline} is already in csf.deny: "
is_csf_blocked=$(grep -Eq "^${cur_logline}$" <<<"${csf_blocked}";echo $?)
if [[ ${is_csf_blocked} -eq 0 ]]; then
echo "${cur_logline} already blocked, skipped"
else
new_block=1
echo "${cur_logline} # log_flood_block.sh: $(date)" >>/etc/csf/csf.deny
echo "${cur_logline} blocked"
fi
fi
done
# flush log file to avoid processing the same IPs
:>"${log_file}"
}
usage() {
echo -e "\nusage: $0 <log_file> <domain>"
echo "example: $0 /home/malakak2/access-logs/layanan.malakakab.go.id-ssl_log layanan.malakakab.go.id"
exit 0
}
##main
# get cloudflare ips
log_file=$1
domain=$2
# never block IPs:
never_block="77.95.113.232 65.181.111.253"
# check CSF
if [[ ! -d /etc/csf ]]; then
echo "CSF not found, exiting."
exit 1
fi
# startup checks
if [[ -z ${log_file} ]]; then usage; fi
if [[ ! -f ${log_file} ]]; then echo "\"${log_file}\": no such file"; usage; fi
if [[ -z ${domain} ]]; then usage; fi
# get cloudflare ranges
curl -s https://www.cloudflare.com/ips-v4 >/root/cf_ips.txt
# make sure the domain is not behind cloudflare
dm_ip=$(dig -t a "${domain}" +short|head -1)
is_dm_cf=$(${bin_grepcidr} 1>/dev/null -f /root/cf_ips.txt <<<"${dm_ip}";echo $?)
if [[ ${is_dm_cf} -eq 0 ]]; then
echo "ERROR: domain ${domain} is behind CloudFlare - blocking IPs from the log would not block the flood, instead please add rewrite rule on top of relevant .htaccess file with the following content:"
echo -e "\nRewriteEngine On\nRewriteRule .* - [R=500,L]\n"
echo "exiting."
exit 0
fi
# report current DENY_IP_LIMIT value
ip_limit=$(grep ^DENY_IP_LIMIT /etc/csf/csf.conf|awk '{print $NF}'|cut -d'"' -f2)
if [[ ${ip_limit} -lt 7000 ]]; then
echo "NOTE: csf.conf: DENY_IP_LIMIT is ${ip_limit}, if the flood has many IPs, this may need adjusting"
else
echo "NOTE: csf.conf: DENY_IP_LIMIT is ${ip_limit}"
fi
# initial flush to skip old entries
:>"${log_file}"
echo "starting in 5 seconds." ; sleep 5
sleep_time=$3
if [[ -z ${sleep_time} ]]; then
sleep_time=30
fi
empty_run=1
empty_run_max=30
while true; do
new_block=0
log_parse_and_ban
if [[ ${new_block} -eq 1 ]]; then
csf -r
empty_run=1
else
echo "nothing blocked, sleeping ${sleep_time}s (${empty_run} / ${empty_run_max})"
(( empty_run++ ))
fi
# stop the script if there are no new IPs to block for next 30mins
# and schedule removal of blocked IPs in 24 hours
if [[ ${empty_run} -gt ${empty_run_max} ]]; then
echo "no new blocks for ${empty_run_max} iterations, scheduling blocked IPs removal in 24hours"
echo "sed -i '/log_flood_block.sh/d' /etc/csf/csf.deny && csf -r" |at now + 24 hours
echo "done, exiting."
exit 0
else
echo -e "\nsleeping ${sleep_time}s..\n"
sleep "${sleep_time}"
fi
done