ddos-mitigator/ddos-mitigator.sh

382 lines
11 KiB
Bash
Raw Normal View History

2020-06-23 02:16:06 +02:00
#!/bin/sh
2020-06-23 03:07:09 +02:00
################################################################################
# #
# Try and prevent apache overloads by banning IP addresses that have (too) #
# many open connections. #
# This script uses netstat to determine the connections to the HTTPS port of #
# the host machine and provides automated whois information retrieval based on #
# the address or the /24- /16- or /8-subnet thereof. Addresses (or subnets) #
# are presented to the user in order of descending connection count. For each #
# address (or subnet), the user can choose to ban or ignore it. Addresses (or #
# subnets) chosen to be banned will be blocked by the apache-badbots jail of #
# fail2ban. #
# Author: Manuel Friedli, <manuel@fritteli.ch> #
# This script is licenced under the GNU General Public Licence, version 3 or #
# later. #
# #
################################################################################
# Set the host's own IP address. So far, only an IPv4 address is supported.
2020-06-23 02:16:06 +02:00
MY_IP="94.199.214.20"
2020-06-23 03:07:09 +02:00
# Set the desired port to monitor.
MY_PORT="443"
2020-06-23 02:16:06 +02:00
2020-06-23 03:07:09 +02:00
# After this point, no editing is required.
# Define the files that will contain the addresses an subnets.
fileraw="raw-http.txt"
2020-06-23 02:16:06 +02:00
file8="sorted-http-8.txt"
file16="sorted-http-16.txt"
file24="sorted-http-24.txt"
file32="sorted-http-32.txt"
2020-06-23 03:07:09 +02:00
# This file will contain the addresses to be banned.
2020-06-23 02:16:06 +02:00
banlist="banlist.txt"
# This file contains the output of the last invocation of whois
whoisoutput="whois.txt"
2020-06-23 02:16:06 +02:00
2020-06-23 03:07:09 +02:00
# These suffixes must be appended to the respective addresses and subnets.
2020-06-23 02:16:06 +02:00
ext8=".0.0.0/8"
ext16=".0.0/16"
ext24=".0/24"
ext32="/32"
2020-06-23 03:07:09 +02:00
# Define some constants to format the output in a colorful way.
2020-06-23 02:16:06 +02:00
red="\033[38;2;255;0;43m"
yellow="\033[38;2;255;204;0m"
green="\033[38;2;0;179;89m"
blue="\033[38;2;0;85;255m"
bold="\033[1m"
reset="\033[0m"
# Clean up when the script exits.
trap 'sudo -k; popd >/dev/null; rm -r ${tmpdir}' EXIT
# Create a temp directory, chdir into it and create the (initially empty)
# banlist file.
tmpdir=$(mktemp -d)
# Suppress output of dir stack.
pushd "${tmpdir}" > /dev/null
touch "${banlist}"
function printHelp() {
cat <<ENDOFHELP
Usage: $(basename $0) [OPTION...]
-a, --auto[=LIMIT] Enable the autopilot for automatically banning chinese IP
addresses (whois output must contain "source: APNIC" and
"country: CN").
When LIMIT is given, only auto-ban IP addresses with at
least LIMIT current connections.
When LIMIT is omitted, assume LIMIT=1.
-n, --netmask=SIZE SIZE defines the subnet size in bytes to be analyzed.
Valid values are:
- 1 or 8 for class A networks (X.0.0.0/8)
- 2 or 16 for class B networks (X.X.0.0/16)
- 3 or 24 for class C networks (X.X.X.0/24)
- 4 or 32 for class D networks (X.X.X.X/32)
-h, --help Show this help message
Mandatory or optional arguments to long options are also mandatory or optional
for any corresponding short options.
When invoked without options, the autopilot is disabled and the netmask SIZE is
inquired interactively.
ENDOFHELP
}
function parseCommandline() {
TEMP=$(getopt -o 'a::,n:,h' -l 'auto::,netmask:,help' -- "$@")
if [ $? -ne 0 ] ; then
echo 'Error parsing command line options. Terminating. Invoke with --help for help.' >&2
exit 1
fi
eval set -- "${TEMP}"
unset TEMP
while true ; do
case "$1" in
'-a'|'--auto')
case $2 in
'')
autopilot=1
;;
*[!0-9]*)
echo "Invalid argument for parameter 'auto': '$2'. Invoke with --help for help." >&2
exit 1
;;
*)
autopilot=$2
;;
esac
shift
;;
'-n'|'--netmask')
case "$2" in
'1'|'8')
netmask=8
;;
'2'|'16')
netmask=16
;;
'3'|'24')
netmask=24
;;
'4'|'32')
netmask=32
;;
*)
echo "Invalid argument for parameter 'netmask': '$2'. Invoke with --help for help." >&2
exit 1
;;
esac
shift
;;
'-h'|'--help')
printHelp
exit
;;
'--')
shift
break
;;
*)
echo "Unknown error on command line argument '$1'. Terminating." >&2
exit 1
;;
esac
shift
done
}
# Parse the command line options
autopilot=0
netmask=0
parseCommandline "$@"
2020-06-23 03:07:09 +02:00
# Determine the current connections to the desired port; store the raw data in
# $fileraw.
netstat -nt | grep "${MY_IP}:${MY_PORT}" | tr -s '[:blank:]' | cut -d' ' -f5 \
| cut -d: -f1 | sort > "${fileraw}"
2020-06-23 02:16:06 +02:00
2020-06-23 03:07:09 +02:00
# Group and sort the data into the subnet-specific files.
uniq -c "${fileraw}" | sort -rn > "${file32}"
cut -d. -f1-3 "${fileraw}" | sort | uniq -c | sort -rn > "${file24}"
cut -d. -f1-2 "${fileraw}" | sort | uniq -c | sort -rn > "${file16}"
cut -d. -f1 "${fileraw}" | sort | uniq -c | sort -rn > "${file8}"
2020-06-23 02:16:06 +02:00
2020-06-23 03:07:09 +02:00
# Determine the number of entries per file.
2020-06-23 02:16:06 +02:00
nlines32=$(cat "${file32}" | wc -l)
nlines24=$(cat "${file24}" | wc -l)
nlines16=$(cat "${file16}" | wc -l)
nlines8=$(cat "${file8}" | wc -l)
if [ ${netmask} -eq 0 ] ; then
# Now let the user choose which file to process.
echo "We've got:"
echo "[1] 8bit: ${nlines8} entries"
echo "[2] 16bit: ${nlines16} entries"
echo "[3] 24bit: ${nlines24} entries"
echo "[4] 32bit: ${nlines32} entries"
read -p 'Which one do you want to work with (q=Quit) [1-4]? ' choice
# Based on the user's choice, initialize the variables $file, $ext and
# $nlines, which will be used after this point. Also, $choice will be
# used to color the output based on subnet-type.
case "${choice}" in
"1" )
netmask=8
;;
"2" )
netmask=16
;;
"3" )
netmask=24
;;
"4" )
netmask=32
;;
"Q" | "q" )
echo "You chose to abort. That's fine! Have a nice day!"
exit
;;
* )
echo "Invalid input: ${choice}. I'm out of here."
exit 1
;;
esac
fi
# Now initialize the variables $file, $ext and $nlines based on the chosen $netmask
TEMP="file${netmask}"
file="${!TEMP}"
TEMP="ext${netmask}"
ext="${!TEMP}"
TEMP="nlines${netmask}"
nlines="${!TEMP}"
unset TEMP
2020-06-23 02:16:06 +02:00
echo "Processing ${file}."
2020-06-23 03:07:09 +02:00
################################################################################
# Set the highlighting color for the address count. The color goes from green
# (low count) over yellow (intermediate count) to red (high count). Depending
# on the size of the subnet (/8, /16, /24 or /32), the transitions from one
# color to the next happen at different values.
################################################################################
2020-06-23 02:16:06 +02:00
function setHilite() {
local count=$1
case "${choice}" in
"1" )
2020-06-23 03:07:09 +02:00
# /32: 0 <= green < 3 <= yellow < 5 <= red
2020-06-23 02:16:06 +02:00
if [ $count -ge 5 ] ; then
hilite="${red}"
elif [ $count -ge 3 ] ; then
hilite="${yellow}"
else
hilite="${green}"
fi
;;
"2" )
2020-06-23 03:07:09 +02:00
# /24: 0 <= green < 7 <= yellow < 13 <= red
2020-06-23 02:16:06 +02:00
if [ $count -ge 13 ] ; then
hilite="${red}"
elif [ $count -ge 7 ] ; then
hilite="${yellow}"
else
hilite="${green}"
fi
;;
"3" )
2020-06-23 03:07:09 +02:00
# /16: 0 <= green < 13 <= yellow < 25 <= red
2020-06-23 02:16:06 +02:00
if [ $count -ge 25 ] ; then
hilite="${red}"
elif [ $count -ge 13 ] ; then
hilite="${yellow}"
else
hilite="${green}"
fi
;;
"4" )
2020-06-23 03:07:09 +02:00
# /8: 0 <= green < 21 <= yellow < 49 <= red
2020-06-23 02:16:06 +02:00
if [ $count -ge 49 ] ; then
hilite="${red}"
elif [ $count -ge 21 ] ; then
hilite="${yellow}"
else
hilite="${green}"
fi
;;
* )
2020-06-23 03:07:09 +02:00
# ???: We should never get here. As a fall-back, just use no
# highlighting.
2020-06-23 02:16:06 +02:00
hilite=""
;;
esac
}
2020-06-23 03:07:09 +02:00
################################################################################
# Process the file denoted by $1. For each line in the file, the count and the
# address are displayed and a whois request is made. The user can then choose to
# ban or ignore the address. Addresses chosen to be banned are appended to the
# $banlist.
################################################################################
2020-06-23 02:16:06 +02:00
function processFile () {
local file="${1}"
local line=''
local count=0
local addr=''
local banaction=''
local nline=1
local country_cn=1
local source_apnic=1
2020-06-23 03:07:09 +02:00
# Read the contents from filedescriptor 3 (important: Don's use the
# standard filedescriptor because we need to handle user input from
# within the loop).
2020-06-23 02:16:06 +02:00
while IFS= read -r -u3 line ; do
line="$(echo "${line}" | tr -s '[:blank:]')"
count="$(echo "${line}" | cut -d' ' -f2)"
addr="$(echo "${line}" | cut -d' ' -f3-)${ext}"
setHilite "${count}"
if [[ autopilot -eq 0 ]] ; then
whois "${addr}" | tee "${whoisoutput}"
else
whois "${addr}" > "${whoisoutput}"
fi
grep -iq "^country: *cn$" "${whoisoutput}"
country_cn=$?
grep -iq "^source: *apnic$" "${whoisoutput}"
source_apnic=$?
2020-06-23 03:07:09 +02:00
echo -en "Address ${bold}$((nline++)) of ${nlines}${reset}: \
Found '${blue}${addr}${reset}' ${hilite}${count}${reset} times."
if [[ ${autopilot} -eq 0 ]] ; then
echo -en "Ban [y/N/s=No, and skip remaining]? "
read banaction
else
if [[ ${country_cn} -eq 0 && ${source_apnic} -eq 0 ]] ; then
if [[ $count -ge $autopilot ]] ; then
echo -en "\n${red}Autopilot active. ${reset}"
banaction=y
else
echo -e "\n${yellow}Autopilot active. Ignoring remaining addresses due to limit of ${autopilot}.${reset}"
return
fi
else
if [[ $count -ge $autopilot ]] ; then
echo -en "\n${green}Autopilot active. ${reset}"
banaction=n
else
echo -e "\n${green}Autopilot active.${reset} ${yellow}Ignoring remaining addresses due to limit of ${autopilot}.${reset}"
return
fi
fi
fi
2020-06-23 02:16:06 +02:00
case "${banaction}" in
2020-06-23 03:07:09 +02:00
"s" | "S" )
echo -e "Not banning '${blue}${addr}${reset}', \
skipping remaining addresses."
return
;;
"y" | "Y" )
echo -e "Adding '${blue}${addr}${reset}' to \
banlist."
echo "${addr}" >> "${banlist}"
;;
"n" | "N" | * )
echo -e "Not banning '${blue}${addr}${reset}'."
;;
2020-06-23 02:16:06 +02:00
esac
2020-06-23 03:07:09 +02:00
# Here goes: Pipe the file contents via filedescriptor 3.
2020-06-23 02:16:06 +02:00
done 3< "${file}"
2020-06-23 03:07:09 +02:00
echo "Processed all entries in ${file}."
2020-06-23 02:16:06 +02:00
}
2020-06-23 03:07:09 +02:00
# Invoke the processing function on the chosen file.
2020-06-23 02:16:06 +02:00
processFile "${file}"
echo "These are the addresses to be banned:"
cat "${banlist}"
2020-06-23 03:07:09 +02:00
# Make sure the user has to (re)-identify him- or herself before actually
# banning anything.
sudo -k
# Iterate over all addresses in $banlist and invoke fail2ban-client on each
# one of them.
2020-06-23 02:16:06 +02:00
while read -r addr ; do
echo "Banning ${addr} ..."
if [[ $(id -un) == "root" ]] ; then
# Don't use sudo when we're running as root.
fail2ban-client set apache-auth banip "${addr}"
else
sudo fail2ban-client set apache-auth banip "${addr}"
fi
2020-06-23 02:16:06 +02:00
done < "${banlist}"
2020-06-23 03:07:09 +02:00
echo -e "${green}All done!${reset}"