diff --git a/ddos-mitigator/superscript.sh b/ddos-mitigator/superscript.sh index 51eadb9..cbcda82 100755 --- a/ddos-mitigator/superscript.sh +++ b/ddos-mitigator/superscript.sh @@ -1,17 +1,43 @@ #!/bin/sh -MY_IP="94.199.214.20" +################################################################################ +# # +# 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, # +# 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. +MY_IP="94.199.214.20" +# Set the desired port to monitor. +MY_PORT="443" + +# After this point, no editing is required. +# Define the files that will contain the addresses an subnets. +fileraw="raw-http.txt" file8="sorted-http-8.txt" file16="sorted-http-16.txt" file24="sorted-http-24.txt" file32="sorted-http-32.txt" +# This file will contain the addresses to be banned. banlist="banlist.txt" +# These suffixes must be appended to the respective addresses and subnets. ext8=".0.0.0/8" ext16=".0.0/16" ext24=".0/24" ext32="/32" +# Define some constants to format the output in a colorful way. red="\033[38;2;255;0;43m" yellow="\033[38;2;255;204;0m" green="\033[38;2;0;179;89m" @@ -19,24 +45,33 @@ blue="\033[38;2;0;85;255m" bold="\033[1m" reset="\033[0m" +# Clean up when the script exits. trap 'sudo -k; popd; rm -r ${tmpdir}' EXIT +# Create a temp directory, chdir into it and create the (initially empty) +# banlist file. tmpdir=$(mktemp -d) pushd "${tmpdir}" touch "${banlist}" -netstat -nt | grep "${MY_IP}:443" | tr -s '[:blank:]' | cut -d' ' -f5 | cut -d: -f1 | sort > raw-http.txt +# 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}" -uniq -c raw-http.txt | sort -rn > "${file32}" -cut -d. -f1-3 raw-http.txt | sort | uniq -c | sort -rn > "${file24}" -cut -d. -f1-2 raw-http.txt | sort | uniq -c | sort -rn > "${file16}" -cut -d. -f1 raw-http.txt | sort | uniq -c | sort -rn > "${file8}" +# 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}" +# Determine the number of entries per file. nlines32=$(cat "${file32}" | wc -l) nlines24=$(cat "${file24}" | wc -l) nlines16=$(cat "${file16}" | wc -l) nlines8=$(cat "${file8}" | wc -l) +# Now let the user choose which file to process. echo "We've got:" echo "[1] 32bit: ${nlines32} entries" echo "[2] 24bit: ${nlines24} entries" @@ -44,22 +79,53 @@ echo "[3] 16bit: ${nlines16} entries" echo "[4] 8bit: ${nlines8} 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" ) file="${file32}" ; ext="${ext32}" ; nlines="${nlines32}" ;; - "2" ) file="${file24}" ; ext="${ext24}" ; nlines="${nlines24}" ;; - "3" ) file="${file16}" ; ext="${ext16}" ; nlines="${nlines16}" ;; - "4" ) file="${file8}" ; ext="${ext8}" ; nlines="${nlines8}" ;; - "Q" | "q" ) echo 'Kthxbye.'; exit ;; - * ) echo "Invalid input: ${infile}. I'm out of here."; exit 1 ;; + "1" ) + file="${file32}" + ext="${ext32}" + nlines="${nlines32}" + ;; + "2" ) + file="${file24}" + ext="${ext24}" + nlines="${nlines24}" + ;; + "3" ) + file="${file16}" + ext="${ext16}" + nlines="${nlines16}" + ;; + "4" ) + file="${file8}" + ext="${ext8}" + nlines="${nlines8}" + ;; + "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 echo "Processing ${file}." +################################################################################ +# 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. +################################################################################ function setHilite() { local count=$1 case "${choice}" in "1" ) - # /32 + # /32: 0 <= green < 3 <= yellow < 5 <= red if [ $count -ge 5 ] ; then hilite="${red}" elif [ $count -ge 3 ] ; then @@ -69,7 +135,7 @@ function setHilite() { fi ;; "2" ) - # /24 + # /24: 0 <= green < 7 <= yellow < 13 <= red if [ $count -ge 13 ] ; then hilite="${red}" elif [ $count -ge 7 ] ; then @@ -79,7 +145,7 @@ function setHilite() { fi ;; "3" ) - # /16 + # /16: 0 <= green < 13 <= yellow < 25 <= red if [ $count -ge 25 ] ; then hilite="${red}" elif [ $count -ge 13 ] ; then @@ -89,7 +155,7 @@ function setHilite() { fi ;; "4" ) - # /8 + # /8: 0 <= green < 21 <= yellow < 49 <= red if [ $count -ge 49 ] ; then hilite="${red}" elif [ $count -ge 21 ] ; then @@ -99,12 +165,19 @@ function setHilite() { fi ;; * ) - # ??? + # ???: We should never get here. As a fall-back, just use no + # highlighting. hilite="" ;; esac } +################################################################################ +# 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. +################################################################################ function processFile () { local file="${1}" local line='' @@ -112,29 +185,54 @@ function processFile () { local addr='' local banaction='' local nline=1 + # Read the contents from filedescriptor 3 (important: Don's use the + # standard filedescriptor because we need to handle user input from + # within the loop). 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}" whois "${addr}" - echo -en "Address ${bold}$((nline++)) of ${nlines}${reset}: Found '${blue}${addr}${reset}' ${hilite}${count}${reset} times. Ban [y/N/q]? " + echo -en "Address ${bold}$((nline++)) of ${nlines}${reset}: \ +Found '${blue}${addr}${reset}' ${hilite}${count}${reset} times. Ban [y/N/s=No, \ +and skip remaining]? " read banaction case "${banaction}" in - "q" | "Q" ) echo "Aborting." ; return ;; - "y" | "Y" ) echo -e "Adding '${blue}${addr}${reset}' to banlist."; echo "${addr}" >> "${banlist}" ;; - "n" | "N" | * ) echo -e "Not banning '${blue}${addr}${reset}'." ;; + "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}'." + ;; esac + # Here goes: Pipe the file contents via filedescriptor 3. done 3< "${file}" - echo "Processed all entries." + echo "Processed all entries in ${file}." } +# Invoke the processing function on the chosen file. processFile "${file}" echo "These are the addresses to be banned:" cat "${banlist}" +# 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. while read -r addr ; do echo "Banning ${addr} ..." sudo fail2ban-client set apache-badbots banip "${addr}" done < "${banlist}" + +echo -e "${green}All done!${reset}"