Compare commits

..

28 commits

Author SHA1 Message Date
9b4a55dfb2 Merge pull request 'Don't call python3.8, but python.' (#6) from feature/fix-python into master
Reviewed-on: #6
2021-06-09 00:45:22 +02:00
64918328c9 Don't call python3.8, but python. 2021-06-08 22:45:25 +02:00
22bc52d665 Merge pull request 'feature/handle-python-exceptions' (#5) from feature/handle-python-exceptions into master
Reviewed-on: #5
2020-11-24 23:10:37 +01:00
31f0901134 Fix bugs and improve command line argument parsing and error handling. 2020-11-24 23:07:38 +01:00
70d41673f7 Try and handle python exceptions more gracefully. 2020-11-24 22:10:28 +01:00
a1d5821cc7 Merge pull request 'Fix silly, SILLY syntax error.' (#4) from bugfix/syntax-error into master
Reviewed-on: #4
2020-11-24 22:09:58 +01:00
b3afda4932 Fix silly, SILLY syntax error. 2020-11-24 22:08:37 +01:00
142db098be Merge pull request 'feature/config-file' (#3) from feature/config-file into master
Reviewed-on: #3
2020-11-24 21:35:12 +01:00
5078363348 Harmonize parameter processing order. 2020-11-24 21:28:30 +01:00
f5180a5e57 Improve parameter validation. 2020-11-24 21:28:30 +01:00
11ad1b471a Change the way default parameters are set. 2020-11-24 21:28:28 +01:00
8e0f22da8f Make it actually work.
To do: validate input from config file.
2020-11-24 21:28:24 +01:00
0871a25bf7 Add the possibility of using a config file instead of CLI parameters. 2020-11-24 21:28:15 +01:00
b65bd6ea55 Merge pull request 'feature/ss-instead-of-netstat' (#2) from feature/ss-instead-of-netstat into master
Reviewed-on: #2
2020-09-18 14:24:41 +02:00
36a5fbc2e5 Remove leftover comment. 2020-09-18 14:16:42 +02:00
2f58a65f7b Cosmetics. 2020-09-18 14:14:56 +02:00
877a2723e5 Minor bugfix: Don't trip if there is NOTHING to block. 2020-09-18 14:14:56 +02:00
7f3432672b Complete the frame :) 2020-09-18 14:14:56 +02:00
265507452c Omit empty lines. 2020-09-18 14:14:56 +02:00
11c246b02c Use ss instead of netstat. 2020-09-18 14:14:56 +02:00
3a15ac1ba3 Fiddling around with ss and grep. 2020-09-18 14:14:53 +02:00
c4600f34b6 Add the magic command as a comment. Now we can build upon that. 2020-09-18 14:12:04 +02:00
0c2f8efe6d Merge pull request 'Update description.' (#1) from feature/fix-description into master
Reviewed-on: #1
2020-09-18 14:11:38 +02:00
8fa1414f18 Update description. 2020-09-18 14:10:21 +02:00
f2c8ad8b9d Fix typo. 2020-09-18 14:08:21 +02:00
e8280e5916 Reword help message. 2020-09-17 11:47:43 +02:00
bfbbe0c976 Reformatting. 2020-09-17 11:47:43 +02:00
a396d1cf4e Add .editorconfig file and implement dependency check that can be
invoked from the command line.
2020-09-17 11:47:43 +02:00
4 changed files with 423 additions and 230 deletions

5
.editorconfig Normal file
View file

@ -0,0 +1,5 @@
root = true
[*.sh]
indent_size = 4
indent_style = tab

30
ddos-mitigator.conf Normal file
View file

@ -0,0 +1,30 @@
# Example configuration file for ddos-mitigator.sh.
# PLEASE TAKE CARE not to put any whitespace around the '=' signs, as this file is directly sourced by the script
# file and this needs to conform to the BASH syntax. Also, make sure to declare the COUNTRIES variable with the
# correct array syntax: COUNTRIES=("AA" "BB" "CC"), or to comment it out altogether.
# The path to the GeoIP2 database file (must be either country or city database). This parameter is mandatory. If it is
# not specified here, it must be given on the command line (through the -d option).
DATABASE_FILE="/path/to/geoip/country-or-city-database.mmdb"
# Enable the autopilot for automatically banning IP addresses of the desired countries (see also COUNTRIES option).
# Only ban IP addresses with at least AUTOPILOT current connections. If the value is not specified or 0, don't
# automatically ban IP addresses, but run in interactive mode.
AUTOPILOT="0"
# Defines the subnet size in bytes to be analyzed. Valid values are:
# - 8 for class A networks (X.0.0.0/8)
# - 16 for class B networks (X.X.0.0/16)
# - 24 for class C networks (X.X.X.0/24)
# - 32 for class D networks (X.X.X.X/32)
# If not specified, run in interactive mode and prompt for the netmask size.
NETMASK="8"
# The country-codes to block as an array. Defaults to "CN" (China).
#COUNTRIES=("CN" "HK" "TW")
# Specify the JAIL to use for banning the IP addresses. Defaults to 'apache-auth'.
#JAIL="apache-auth"
# The desired port to monitor. Defaults to 443 (https).
#PORT="443"

View file

@ -1,21 +1,17 @@
#!/bin/sh #!/bin/sh
################################################################################
################################################################################
########### FIXME: This text is outdated and needs to be rewritten. ###########
################################################################################
################################################################################
################################################################################ ################################################################################
# # # #
# Try and prevent apache overloads by banning IP addresses that have (too) # # Try and prevent apache overloads by banning IP addresses that have (too) #
# many open connections. # # many open connections. #
# This script uses netstat to determine the connections to the HTTPS port of # # This script uses ss to determine the connections to a configurable port #
# the host machine and provides automated whois information retrieval based on # # on the host machine and provides automated GeoIP information retrieval based #
# the address or the /24-, /16- or /8-subnet thereof. Addresses (or subnets) # # the address or the /24-, /16- or /8-subnet thereof. A GeoIP city- or country #
# are presented to the user in order of descending connection count. For each # # database must be installed separately and is provided to the script via a #
# address (or subnet), the user can choose to ban or ignore it. Addresses (or # # command line parameter. #
# subnets) chosen to be banned will be blocked by the apache-badbots jail of # # Addresses (or subnets) are presented to the user in order of descending #
# fail2ban. # # 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 #
# a configurable jail of fail2ban. #
# Author: Manuel Friedli, <manuel@fritteli.ch> # # Author: Manuel Friedli, <manuel@fritteli.ch> #
# This script is licenced under the GNU General Public Licence, version 3 or # # This script is licenced under the GNU General Public Licence, version 3 or #
# later. # # later. #
@ -30,17 +26,32 @@
# - net-analyzer/fail2ban (`fail2ban-client`) # # - net-analyzer/fail2ban (`fail2ban-client`) #
# - sys-apps/coreutils (`cut`, `id`, `sort`, `touch`, `tr`, `uniq`) # # - sys-apps/coreutils (`cut`, `id`, `sort`, `touch`, `tr`, `uniq`) #
# - sys-apps/grep (`grep`) # # - sys-apps/grep (`grep`) #
# - sys-apps/iproute2 (`ss`) #
# - sys-apps/moreutils (`sponge`) # # - sys-apps/moreutils (`sponge`) #
# - sys-apps/net_tools (`netstat`) #
# - sys-apps/util-linux (`getopt`) # # - sys-apps/util-linux (`getopt`) #
# # # #
################################################################################ ################################################################################
# Set the host's own IP address. So far, only an IPv4 address is supported. # Store the start time; this enables us to output the total runtime at the end.
MY_IP="94.199.214.20" start="$(date +%s)"
# After this point, no editing is required. # Dependencies of this script. Simple array with the following structure:
start=$(date +%s) # (command package [...])
dependencies=(
"sudo" "app-admin/sudo"
"python" "dev-lang/python:3.8"
"fail2ban-client" "net-analyzer/fail2ban"
"cut" "sys-apps/coreutils"
"id" "sys-apps/coreutils"
"sort" "sys-apps/coreutils"
"touch" "sys-apps/coreutils"
"tr" "sys-apps/coreutils"
"uniq" "sys-apps/coreutils"
"grep" "sys-apps/grep"
"ss" "sys-apps/iproute2"
"sponge" "sys-apps/moreutils"
"getopt" "sys-apps/util-linux"
)
# These suffixes must be appended to the respective addresses and subnets. # These suffixes must be appended to the respective addresses and subnets.
suffix8="/8" suffix8="/8"
@ -61,18 +72,40 @@ bold="$(printf '\033[1m')"
reset="$(printf '\033[0m')" reset="$(printf '\033[0m')"
# Clean up when the script exits. # Clean up when the script exits.
trap 'sudo -k; rm -r ${tmpdir}' EXIT trap 'sudo -k 2>/dev/null >&2; rm -r ${tmpdir}' EXIT
function check_installed() { function is_installed() {
local command="$1" which "${1}" 2>/dev/null >&2
local package="$2" return $?
which "${command}" 2>/dev/null >&2 }
local result=$?
if [[ "${result}" -ne 0 ]] ; then function print_missing_dependency() {
echo "${red}Command ${bold}${command}${reset}${red} not found.${reset} Please install package ${blue}${package}${reset}." local command="${1}"
exit 1 local package="${2}"
fi
echo "${red}Command ${bold}${command}${reset}${red} not found.${reset} Please install package ${blue}${package}${reset}." >&2
}
function check_dependencies() {
local arraylength=${#dependencies[@]}
local res=
local command=
local package=
# 0: true, all installed; 1: false, at least one command/package missing
local all_installed=0
for ((i = 0; i < ${arraylength}; i += 2)); do
command="${dependencies[$i]}"
package="${dependencies[$i + 1]}"
is_installed "${command}" "${package}"
res=$?
if [[ ${res} -ne 0 ]]; then
print_missing_dependency "${command}" "${package}"
all_installed=1
fi
done
return ${all_installed}
} }
function print_help() { function print_help() {
@ -96,6 +129,20 @@ Usage: $(basename $0) -d FILE [OPTION...]
comma-separated values; defaults to 'CN' comma-separated values; defaults to 'CN'
(China). (China).
-e, --dependencies Check if all required dependencies are
installed. If all dependencies are found,
the program terminates with exit code 0.
Otherwise, missing dependencies are
printed to stderr and the program
terminates with exit code 1.
-f, --config-file=FILENAME Specify the full path to the configuration
file. If omitted, all options are read
from the command line.
Any parameter specified on the command
line takes precedence over configuration
option read from the file.
-j, --jail=JAIL Specify the JAIL to use for banning the IP -j, --jail=JAIL Specify the JAIL to use for banning the IP
addresses. addresses.
Defaults to 'apache-auth'. Defaults to 'apache-auth'.
@ -122,7 +169,7 @@ ENDOFHELP
} }
function exec_as_root() { function exec_as_root() {
if [[ $(id -un) == "root" ]] ; then if [[ $(id -un) == "root" ]]; then
"$@" "$@"
else else
sudo "$@" sudo "$@"
@ -131,30 +178,70 @@ function exec_as_root() {
function filter() { function filter() {
# list of current connections # list of current connections
file="$1" file="${1}"
# subnet extension, e.g. ".0.0" # subnet extension, e.g. ".0.0"
ext="$2" ext="${2}"
# subnet suffix, e.g. "/16" # subnet suffix, e.g. "/16"
suffix="$3" suffix="${3}"
rm -f "${filtered}" rm -f "${filtered}"
touch "${filtered}" touch "${filtered}"
# Reject already banned addresses # Reject already banned addresses
while read -r -u3 address ; do while read -r -u3 address; do
if [[ "${banned}" != *"${address}${ext}${suffix}"* ]] ; then if [[ "${banned}" != *"${address}${ext}${suffix}"* ]]; then
echo "${address}" >> "${filtered}" echo "${address}" >>"${filtered}"
else else
echo "IGNORING ${address}${ext}${suffix}, already banned." echo "IGNORING ${address}${ext}${suffix}, already banned."
fi fi
done 3< "${file}" done 3<"${file}"
mv "${filtered}" "${file}" mv "${filtered}" "${file}"
} }
function parse_command_line_args() { function set_default_values() {
TEMP=$(getopt -o 'a::,c:,d:,j:,n:,p:,h' -l 'auto::,country:,database:,jail:,netmask:,port:,help' -- "$@") if [[ -z "${autopilot}" ]]; then
autopilot=0
fi
if [[ -z "${bancountries}" ]]; then
bancountries=("CN")
fi
if [[ -z "${jail}" ]]; then
jail="apache-auth"
fi
if [[ -z "${netmask}" ]]; then
netmask=0
fi
if [[ -z "${port}" ]]; then
port=443
fi
}
if [ $? -ne 0 ] ; then function parse_config_file() {
source "${configfile}"
if [[ -z "${autopilot}" ]]; then
autopilot="${AUTOPILOT}"
fi
if [[ -z "${bancountries}" ]]; then
bancountries=(${COUNTRIES[@]})
fi
if [[ -z "${database}" ]]; then
database="${DATABASE_FILE}"
fi
if [[ -z "${jail}" ]]; then
jail="${JAIL}"
fi
if [[ -z "${netmask}" ]]; then
netmask="${NETMASK}"
fi
if [[ -z "${port}" ]]; then
port="${PORT}"
fi
}
function parse_command_line_args() {
TEMP=$(getopt -o 'a::,c:,d:,e,f:,j:,n:,p:,h' -l 'auto::,country:,database:,dependencies,config-file:,jail:,netmask:,port:,help' -- "$@")
if [ $? -ne 0 ]; then
echo 'Error parsing command line options. Terminating. Invoke with --help for help.' >&2 echo 'Error parsing command line options. Terminating. Invoke with --help for help.' >&2
exit 1 exit 1
fi fi
@ -162,87 +249,152 @@ function parse_command_line_args() {
eval set -- "${TEMP}" eval set -- "${TEMP}"
unset TEMP unset TEMP
while true ; do while true; do
case "$1" in case "${1}" in
'-a'|'--auto') '-a' | '--auto')
case "$2" in case "${2}" in
'') '')
autopilot=1 autopilot=1
;; ;;
*[!0-9]*)
echo "Invalid argument for parameter 'auto': '$2'. Invoke with --help for help." >&2
exit 1
;;
*)
autopilot="$2"
;;
esac
shift
;;
'-c'|'--country')
IFS=',' read -ra bancountries <<< "$2"
if [[ -z ${bancountries[@]// } ]] ; then
echo "Invalid argument for parameter 'country': '$2'. Invoke with --help for help." >&2
exit 1
fi
shift
;;
'-d'|'--database')
database="$2"
shift
;;
'-j'|'--jail')
jail="$2"
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
;;
'-p'|'--port')
port="$2"
shift
;;
'-h'|'--help')
print_help
exit
;;
'--')
shift
break
;;
*) *)
echo "Unknown error on command line argument '$1'. Terminating." >&2 autopilot="$2"
exit 1 ;;
esac
shift
;;
'-c' | '--country')
IFS=',' read -ra bancountries <<<"${2}"
shift
;;
'-d' | '--database')
database="${2}"
shift
;;
'-e' | '--dependencies')
check_dependencies
exit $?
;;
'-f' | '--config-file')
configfile="${2}"
shift
;;
'-j' | '--jail')
jail="${2}"
shift
;;
'-n' | '--netmask')
case "${2}" in
'1')
netmask=8
;;
'2')
netmask=16
;;
'3')
netmask=24
;;
'4')
netmask=32
;;
*)
netmask="${2}"
;;
esac
shift
;;
'-p' | '--port')
port="${2}"
shift
;;
'-h' | '--help')
print_help
exit
;;
'--')
shift
break
;;
*)
echo "Unknown error on command line argument '${1}'. Terminating." >&2
exit 1
;; ;;
esac esac
shift shift
done done
if [[ -z "${database}" ]] ; then # If the config file option is set, parse the config file.
echo "No GeoIP database specified. Invoke with --help for more information." >&2 if [[ ! -z ${configfile+x} ]]; then
if [[ ! -f "${configfile}" || ! -r "${configfile}" ]]; then
echo "Can not read configuration file '${2}'. Invoke with --help for help." >&2
exit 1
fi
parse_config_file
fi
# Here, we set the default values for all options that have not been set yet.
set_default_values
}
function validate_parameter_values() {
# GeoIP-Database
if [[ -z "${database}" ]]; then
echo "No GeoIP database specified. Invoke with --help for more information." >&2
exit 1 exit 1
fi fi
if [[ ! -r "${database}" ]] ; then if [[ ! -f "${database}" || ! -r "${database}" ]]; then
echo "Database '${database}' is not accessible." >&2 echo "Database '${database}' is not accessible." >&2
exit 1
fi
# Autopilot
case "${autopilot}" in
*[!0-9]*)
echo "Invalid value for parameter 'auto' / 'AUTOPILOT': '${autopilot}'." >&2
echo "Invoke with --help for help." >&2
exit 1
;;
esac
# Countries
if [[ -z ${bancountries[@]// /} ]]; then
echo "Invalid value for parameter 'country' / 'COUNTRIES': '${bancountries[*]}'." >&2
echo "Invoke with --help for help." >&2
exit 1
fi
# Jail
if [[ -z "${jail}" ]]; then
echo "Invalid value for parameter 'jail' / 'JAIL': '${jail}'." >&2
echo "Invoke with --help for help." >&2
exit 1
fi
# Netmask
case "${netmask}" in
'0' | '8' | '16' | '24' | '32')
# Everything OK.
;;
*)
echo "Invalid value for parameter 'netmask': '${2}'." >&2
echo "Invoke with --help for help." >&2
exit 1
;;
esac
# Port
case "${port}" in
*[!0-9]*)
echo "Invalid value for parameter 'port' / 'PORT': '${autopilot}'." >&2
echo "Invoke with --help for help." >&2
exit 1
;;
esac
if [[ ${port} -lt 0 || ${port} -gt 65535 ]]; then
echo "Invalid value for parameter 'port' / 'PORT': '${autopilot}'." >&2
echo "Value must be between 0 ... 65535 (inclusive)." >&2
echo "Invoke with --help for help." >&2
exit 1 exit 1
fi fi
} }
@ -254,52 +406,52 @@ function parse_command_line_args() {
# color to the next happen at different values. # color to the next happen at different values.
################################################################################ ################################################################################
function set_highlight_color() { function set_highlight_color() {
local count=$1 local count=${1}
case "${choice}" in case "${choice}" in
"1" ) "1")
# /32: 0 <= green < 3 <= yellow < 5 <= red # /32: 0 <= green < 3 <= yellow < 5 <= red
if [ $count -ge 5 ] ; then if [ ${count} -ge 5 ]; then
hilite="${red}" hilite="${red}"
elif [ $count -ge 3 ] ; then elif [ ${count} -ge 3 ]; then
hilite="${yellow}" hilite="${yellow}"
else else
hilite="${green}" hilite="${green}"
fi fi
;; ;;
"2" ) "2")
# /24: 0 <= green < 7 <= yellow < 13 <= red # /24: 0 <= green < 7 <= yellow < 13 <= red
if [ $count -ge 13 ] ; then if [ ${count} -ge 13 ]; then
hilite="${red}" hilite="${red}"
elif [ $count -ge 7 ] ; then elif [ ${count} -ge 7 ]; then
hilite="${yellow}" hilite="${yellow}"
else else
hilite="${green}" hilite="${green}"
fi fi
;; ;;
"3" ) "3")
# /16: 0 <= green < 13 <= yellow < 25 <= red # /16: 0 <= green < 13 <= yellow < 25 <= red
if [ $count -ge 25 ] ; then if [ ${count} -ge 25 ]; then
hilite="${red}" hilite="${red}"
elif [ $count -ge 13 ] ; then elif [ ${count} -ge 13 ]; then
hilite="${yellow}" hilite="${yellow}"
else else
hilite="${green}" hilite="${green}"
fi fi
;; ;;
"4" ) "4")
# /8: 0 <= green < 21 <= yellow < 49 <= red # /8: 0 <= green < 21 <= yellow < 49 <= red
if [ $count -ge 49 ] ; then if [ ${count} -ge 49 ]; then
hilite="${red}" hilite="${red}"
elif [ $count -ge 21 ] ; then elif [ ${count} -ge 21 ]; then
hilite="${yellow}" hilite="${yellow}"
else else
hilite="${green}" hilite="${green}"
fi fi
;; ;;
* ) *)
# ???: We should never get here. As a fall-back, just use no # ???: We should never get here. As a fall-back, just use no
# highlighting. # highlighting.
hilite="" hilite=""
;; ;;
esac esac
} }
@ -310,7 +462,7 @@ function set_highlight_color() {
# GeoIP database. The user can then choose to ban or ignore the address. # GeoIP database. The user can then choose to ban or ignore the address.
# Addresses chosen to be banned are appended to the $banlist. # Addresses chosen to be banned are appended to the $banlist.
################################################################################ ################################################################################
function process_file () { function process_file() {
local file="${1}" local file="${1}"
local line='' local line=''
local count=0 local count=0
@ -322,25 +474,25 @@ function process_file () {
# Read the contents from filedescriptor 3 (important: Don's use the # Read the contents from filedescriptor 3 (important: Don's use the
# standard filedescriptor because we need to handle user input from # standard filedescriptor because we need to handle user input from
# within the loop). # within the loop).
while IFS= read -r -u3 line ; do while IFS= read -r -u3 line; do
line="$(echo "${line}" | tr -s '[:blank:]')" line="$(echo "${line}" | tr -s '[:blank:]')"
count="$(echo "${line}" | cut -d' ' -f2)" count="$(echo "${line}" | cut -d' ' -f2)"
addronly="$(echo "${line}" | cut -d' ' -f3-)${ext}" addronly="$(echo "${line}" | cut -d' ' -f3-)${ext}"
addrwithsuffix="${addronly}${suffix}" addrwithsuffix="${addronly}${suffix}"
set_highlight_color "${count}" set_highlight_color "${count}"
country="$("${curdir}/geoip-lookup.py" -f "${database}" "${addronly}")" country="$("${curdir}/geoip-lookup.py" -f "${database}" "${addronly}")"
if [[ autopilot -eq 0 ]] ; then if [[ ${autopilot} -eq 0 ]]; then
echo "Country: '${yellow}${country}${reset}'" echo "Country: '${yellow}${country}${reset}'"
fi fi
echo -n "Address ${bold}$((nline++)) of ${nlines}${reset}: \ echo -n "Address ${bold}$((nline++)) of ${nlines}${reset}: \
Found '${blue}${addrwithsuffix}${reset}' ${hilite}${count}${reset} times." Found '${blue}${addrwithsuffix}${reset}' ${hilite}${count}${reset} times."
if [[ ${autopilot} -eq 0 ]] ; then if [[ ${autopilot} -eq 0 ]]; then
echo -n " Ban [y/N/s=No, and skip remaining]? " echo -n " Ban [y/N/s=No, and skip remaining]? "
read banaction read banaction
else else
if [[ " ${bancountries[@]} " =~ " ${country} " ]] ; then if [[ " ${bancountries[@]} " =~ " ${country} " ]]; then
if [[ $count -ge $autopilot ]] ; then if [[ ${count} -ge ${autopilot} ]]; then
echo -en "\n${red}Autopilot active. ${reset}" echo -en "\n${red}Autopilot active. ${reset}"
banaction=y banaction=y
else else
@ -348,7 +500,7 @@ Found '${blue}${addrwithsuffix}${reset}' ${hilite}${count}${reset} times."
return return
fi fi
else else
if [[ $count -ge $autopilot ]] ; then if [[ ${count} -ge ${autopilot} ]]; then
echo -en "\n${green}Autopilot active. ${reset}" echo -en "\n${green}Autopilot active. ${reset}"
banaction=n banaction=n
else else
@ -359,28 +511,28 @@ Found '${blue}${addrwithsuffix}${reset}' ${hilite}${count}${reset} times."
fi fi
case "${banaction}" in case "${banaction}" in
"s" | "S" ) "s" | "S")
echo "Not banning '${blue}${addrwithsuffix}${reset}', \ echo "Not banning '${blue}${addrwithsuffix}${reset}', \
skipping remaining addresses." skipping remaining addresses."
return return
;; ;;
"y" | "Y" ) "y" | "Y")
echo "Adding '${blue}${addrwithsuffix}${reset}' to \ echo "Adding '${blue}${addrwithsuffix}${reset}' to \
banlist (country=${yellow}${country}${reset})." banlist (country=${yellow}${country}${reset})."
echo "${addrwithsuffix}" >> "${banlist}" echo "${addrwithsuffix}" >>"${banlist}"
;; ;;
"n" | "N" | * ) "n" | "N" | *)
echo "Not banning '${blue}${addrwithsuffix}${reset}' (country=${yellow}${country}${reset})." echo "Not banning '${blue}${addrwithsuffix}${reset}' (country=${yellow}${country}${reset})."
;; ;;
esac esac
# Here goes: Pipe the file contents via filedescriptor 3. # Here goes: Pipe the file contents via filedescriptor 3.
done 3< "${file}" done 3<"${file}"
echo "Processed all entries in ${file}." echo "Processed all entries in ${file}."
} }
# Create a temp directory, chdir into it and create the (initially empty) # Create a temp directory, chdir into it and create the (initially empty)
# banlist file. # banlist file.
tmpdir=$(mktemp -d) tmpdir="$(mktemp -d)"
# Set up all file paths # Set up all file paths
curdir="$(dirname "$0")" curdir="$(dirname "$0")"
@ -393,48 +545,43 @@ file24="${tmpdir}/sorted-http-24.txt"
file32="${tmpdir}/sorted-http-32.txt" file32="${tmpdir}/sorted-http-32.txt"
# This file will contain the addresses to be banned. # This file will contain the addresses to be banned.
banlist="${tmpdir}/banlist.txt" banlist="${tmpdir}/banlist.txt"
# This file contains the output of the last invocation of whois
whoisoutput="${tmpdir}/whois.txt"
touch "${banlist}" touch "${banlist}"
# Parse the command line options # Parse the command line options
autopilot=0 autopilot=
netmask=0 netmask=
jail="apache-auth" jail=
bancountries=("CN") bancountries=
database= database=
port=443 port=
parse_command_line_args "$@" parse_command_line_args "$@"
validate_parameter_values
check_installed "sudo" "app-admin/sudo" check_dependencies
check_installed "python" "dev-lang/python:3.8" dependencies_ok=$?
check_installed "fail2ban-client" "net-analyzer/fail2ban" if [[ ${dependencies_ok} -ne 0 ]]; then
check_installed "cut" "sys-apps/coreutils" exit ${dependencies_ok}
check_installed "id" "sys-apps/coreutils" fi
check_installed "sort" "sys-apps/coreutils"
check_installed "touch" "sys-apps/coreutils"
check_installed "tr" "sys-apps/coreutils"
check_installed "uniq" "sys-apps/coreutils"
check_installed "grep" "sys-apps/grep"
check_installed "sponge" "sys-apps/moreutils"
check_installed "netstat" "sys-apps/net_tools"
check_installed "getopt" "sys-apps/util-linux"
# List already banned addresses in the chosen jail # List already banned addresses in the chosen jail
banned="$(exec_as_root fail2ban-client get "${jail}" banip)" banned="$(exec_as_root fail2ban-client get "${jail}" banip)"
# Determine the current connections to the desired port; store the raw data in # Determine the current connections to the desired port; store the raw data in
# $fileraw. # $fileraw.
netstat -nt | grep "${MY_IP}:${port}" | tr -s '[:blank:]' | cut -d' ' -f5 \ connections=$(ss -HOn state established "( sport = :${port} )" | tr -s '[:blank:]' | cut -d' ' -f5)
| cut -d: -f1 | sort > "${fileraw}"
# IPv6-mapped-IPv4: [::ffff:192.168.0.1]:443
echo "${connections}" | grep '^\[::ffff:' - | cut -d: -f4 | cut -d] -f1 | grep -v '^$' >"${fileraw}"
# Pure IPv4: 192.168.0.1:443
echo "${connections}" | grep -v '^\[' - | cut -d: -f1 | grep -v '^$' >>"${fileraw}"
# Group and sort the data into the subnet-specific files. # Group and sort the data into the subnet-specific files.
cp "${fileraw}" "${file32}" sort "${fileraw}" >"${file32}"
cut -d. -f1-3 "${fileraw}" | sort > "${file24}" cut -d. -f1-3 "${fileraw}" | sort >"${file24}"
cut -d. -f1-2 "${fileraw}" | sort > "${file16}" cut -d. -f1-2 "${fileraw}" | sort >"${file16}"
cut -d. -f1 "${fileraw}" | sort > "${file8}" cut -d. -f1 "${fileraw}" | sort >"${file8}"
# Filter already banned addresses # Filter already banned addresses
filter "${file32}" "${ext32}" "${suffix32}" filter "${file32}" "${ext32}" "${suffix32}"
@ -454,7 +601,7 @@ nlines24=$(cat "${file24}" | wc -l)
nlines16=$(cat "${file16}" | wc -l) nlines16=$(cat "${file16}" | wc -l)
nlines8=$(cat "${file8}" | wc -l) nlines8=$(cat "${file8}" | wc -l)
if [ ${netmask} -eq 0 ] ; then if [ ${netmask} -eq 0 ]; then
# Now let the user choose which file to process. # Now let the user choose which file to process.
echo "We've got:" echo "We've got:"
echo "[1] 8bit: ${nlines8} entries" echo "[1] 8bit: ${nlines8} entries"
@ -467,25 +614,25 @@ if [ ${netmask} -eq 0 ] ; then
# $nlines, which will be used after this point. Also, $choice will be # $nlines, which will be used after this point. Also, $choice will be
# used to color the output based on subnet-type. # used to color the output based on subnet-type.
case "${choice}" in case "${choice}" in
"1" ) "1")
netmask=8 netmask=8
;; ;;
"2" ) "2")
netmask=16 netmask=16
;; ;;
"3" ) "3")
netmask=24 netmask=24
;; ;;
"4" ) "4")
netmask=32 netmask=32
;; ;;
"Q" | "q" ) "Q" | "q")
echo "You chose to abort. That's fine! Have a nice day!" echo "You chose to abort. That's fine! Have a nice day!"
exit exit
;; ;;
* ) *)
echo "Invalid input: ${choice}. I'm out of here." echo "Invalid input: ${choice}. I'm out of here."
exit 1 exit 1
;; ;;
esac esac
fi fi
@ -515,11 +662,11 @@ sudo -k
# Iterate over all addresses in $banlist and invoke fail2ban-client on each # Iterate over all addresses in $banlist and invoke fail2ban-client on each
# one of them. # one of them.
while read -r addrwithsuffix ; do while read -r addrwithsuffix; do
echo "Banning ${addrwithsuffix} ..." echo "Banning ${addrwithsuffix} ..."
exec_as_root fail2ban-client set "${jail}" banip "${addrwithsuffix}" exec_as_root fail2ban-client set "${jail}" banip "${addrwithsuffix}"
done < "${banlist}" done <"${banlist}"
end=$(date +%s) end="$(date +%s)"
echo "${green}All done in $((end - start)) seconds!${reset}" echo "${green}All done in $((end - start)) seconds!${reset}"

View file

@ -1,16 +1,24 @@
#!/usr/bin/python3.8 #!/usr/bin/python
import getopt import argparse
import sys import sys
try: try:
import geoip2.database import geoip2.database
import geoip2.errors
except ImportError: except ImportError:
print( print(
"Required module geoip2.database not found. On Gentoo Linux, please install dev-python/geoip2 from the 'fritteli' overlay.", "Required modules geoip2.database and geoip2.errors not found. On Gentoo Linux, please install dev-python/geoip2 from the 'fritteli' overlay.",
file=sys.stderr) file=sys.stderr)
exit(1) exit(1)
try:
import maxminddb.errors
except ImportError:
print(
"Required module maxminddb.errors not found. On Gentoo Linux, please install dev-python/maxminddb from the 'fritteli' overlay.",
file=sys.stderr)
exit(1)
class LookupException(Exception): class LookupException(Exception):
""" """
@ -47,6 +55,14 @@ def get_county_code(ipaddress, dbfile):
raise LookupException("Unsupported DB type: " + dbtype) raise LookupException("Unsupported DB type: " + dbtype)
return country.iso_code return country.iso_code
except FileNotFoundError as e:
raise LookupException(e.args)
except maxminddb.errors.InvalidDatabaseError as e:
raise LookupException(e.args)
except geoip2.errors.AddressNotFoundError as e:
raise LookupException(e.args)
except ValueError as e:
raise LookupException(e.args)
finally: finally:
if reader: if reader:
reader.close() reader.close()
@ -60,21 +76,12 @@ def parse_command_line(argv):
:return: :return:
""" """
dbfile = None dbfile = None
try: parser = argparse.ArgumentParser(description='Get the country code from an IP address')
opts, args = getopt.getopt(argv, "f:") parser.add_argument('-f', dest='dbfile', required=True, help="Path to the GeoIP2 database file")
except getopt.GetoptError as e: parser.add_argument('address', help="The IP address to check")
raise LookupException("Error parsing command line") from e args = parser.parse_args()
for opt, arg in opts: return args.dbfile, args.address
if opt == "-f":
dbfile = arg
else:
raise LookupException("Unknown command line argument")
if len(args) == 0:
raise LookupException("No address given on command line")
return dbfile, args[0]
def main(argv): def main(argv):
""" """
@ -83,14 +90,18 @@ def main(argv):
:param argv: Format: "-f /path/to/database.mmdb ip.v4.add.ress" :param argv: Format: "-f /path/to/database.mmdb ip.v4.add.ress"
:return: :return:
""" """
(dbfile, ipaddress) = parse_command_line(argv) try:
code = get_county_code(ipaddress, dbfile) (dbfile, ipaddress) = parse_command_line(argv)
print(code) code = get_county_code(ipaddress, dbfile)
print(code)
except LookupException as e:
print(e.args, file=sys.stderr)
print("Unknown")
if __name__ == '__main__': if __name__ == '__main__':
try: try:
main(sys.argv[1:]) main(sys.argv[1:])
except BaseException as e: except BaseException as e:
print("Usage: geoip-lookup.py -f /path/to/geoip2-database.mmdb 192.168.1.1", file=sys.stderr) print("Unknown")
raise e raise e