Compare commits

..

1 commit

3 changed files with 82 additions and 194 deletions

View file

@ -1,30 +0,0 @@
# 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

@ -32,7 +32,10 @@
# #
################################################################################
# Store the start time; this enables us to output the total runtime at the end.
# Set the host's own IP address. So far, only an IPv4 address is supported.
MY_IP="94.199.214.20"
# After this point, no editing is required.
start="$(date +%s)"
# Dependencies of this script. Simple array with the following structure:
@ -58,10 +61,12 @@ suffix8="/8"
suffix16="/16"
suffix24="/24"
suffix32="/32"
suffixipv6=""
ext8=".0.0.0"
ext16=".0.0"
ext24=".0"
ext32=""
extipv6=""
# Define some constants to format the output in a colorful way.
red="$(printf '\033[38;2;255;0;43m')"
@ -136,13 +141,6 @@ Usage: $(basename $0) -d FILE [OPTION...]
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
addresses.
Defaults to 'apache-auth'.
@ -154,6 +152,12 @@ Usage: $(basename $0) -d FILE [OPTION...]
- 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)
- 5 or 128 for IPv6 addresses (no IPv4
addresses are considered)
-6, --enable-ipv6 Enable banning of IPv6 addresses in
addition to the IPv4 addresses/networks
defined by -n (if SIZE is 1, 2, 3 or 4)
-p, --port=PORT The desired port to monitor.
Defaults to 443 (https).
@ -198,48 +202,8 @@ function filter() {
mv "${filtered}" "${file}"
}
function set_default_values() {
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
}
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' -- "$@")
TEMP=$(getopt -o 'a::,c:,d:,e,j:,n:,p:,6,h' -l 'auto::,country:,database:,dependencies,jail:,netmask:,port:,enable-ipv6,help' -- "$@")
if [ $? -ne 0 ]; then
echo 'Error parsing command line options. Terminating. Invoke with --help for help.' >&2
@ -256,6 +220,10 @@ function parse_command_line_args() {
'')
autopilot=1
;;
*[!0-9]*)
echo "Invalid argument for parameter 'auto': '${2}'. Invoke with --help for help." >&2
exit 1
;;
*)
autopilot="$2"
;;
@ -264,6 +232,10 @@ function parse_command_line_args() {
;;
'-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')
@ -274,30 +246,30 @@ function parse_command_line_args() {
check_dependencies
exit $?
;;
'-f' | '--config-file')
configfile="${2}"
shift
;;
'-j' | '--jail')
jail="${2}"
shift
;;
'-n' | '--netmask')
case "${2}" in
'1')
'1' | '8')
netmask=8
;;
'2')
'2' | '16')
netmask=16
;;
'3')
'3' | '24')
netmask=24
;;
'4')
'4' | '32')
netmask=32
;;
'5'|'128')
netmask="ipv6"
;;
*)
netmask="${2}"
echo "Invalid argument for parameter 'netmask': '${2}'. Invoke with --help for help." >&2
exit 1
;;
esac
shift
@ -306,6 +278,9 @@ function parse_command_line_args() {
port="${2}"
shift
;;
'-6'|'--enable-ipv6')
enableipv6=1
;;
'-h' | '--help')
print_help
exit
@ -322,81 +297,15 @@ function parse_command_line_args() {
shift
done
# If the config file option is set, parse the config file.
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
fi
if [[ ! -f "${database}" || ! -r "${database}" ]]; then
if [[ ! -r "${database}" ]]; then
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
fi
}
################################################################################
@ -471,6 +380,7 @@ function process_file() {
local banaction=''
local nline=1
local country=
# Read the contents from filedescriptor 3 (important: Don's use the
# standard filedescriptor because we need to handle user input from
# within the loop).
@ -543,21 +453,22 @@ file8="${tmpdir}/sorted-http-8.txt"
file16="${tmpdir}/sorted-http-16.txt"
file24="${tmpdir}/sorted-http-24.txt"
file32="${tmpdir}/sorted-http-32.txt"
fileipv6="${tmpdir}/sorted-http-ipv6.txt"
# This file will contain the addresses to be banned.
banlist="${tmpdir}/banlist.txt"
touch "${banlist}"
# Parse the command line options
autopilot=
netmask=
jail=
bancountries=
autopilot=0
enableipv6=0
netmask=0
jail="apache-auth"
bancountries=("CN")
database=
port=
port=443
parse_command_line_args "$@"
validate_parameter_values
check_dependencies
dependencies_ok=$?
@ -576,6 +487,8 @@ connections=$(ss -HOn state established "( sport = :${port} )" | tr -s '[:blank:
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}"
# IPv6: [aaaa:...]:443
echo "${connections}" | grep '^\[[^:]' - | cut -d']' -f1 | sed -e 's/^\[//' | grep -v '^$' | sort > "${fileipv6}"
# Group and sort the data into the subnet-specific files.
sort "${fileraw}" >"${file32}"
@ -588,18 +501,21 @@ filter "${file32}" "${ext32}" "${suffix32}"
filter "${file24}" "${ext24}" "${suffix24}"
filter "${file16}" "${ext16}" "${suffix16}"
filter "${file8}" "${ext8}" "${suffix8}"
filter "${fileipv6}" "" ""
# Determine the number of connections per address
uniq -c "${file32}" | sort -rn | sponge "${file32}"
uniq -c "${file24}" | sort -rn | sponge "${file24}"
uniq -c "${file16}" | sort -rn | sponge "${file16}"
uniq -c "${file8}" | sort -rn | sponge "${file8}"
uniq -c "${fileipv6}" | sort -rn | sponge "${fileipv6}"
# 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)
nlinesipv6=$(cat "${fileipv6}" | wc -l)
if [ ${netmask} -eq 0 ]; then
# Now let the user choose which file to process.
@ -608,6 +524,7 @@ if [ ${netmask} -eq 0 ]; then
echo "[2] 16bit: ${nlines16} entries"
echo "[3] 24bit: ${nlines24} entries"
echo "[4] 32bit: ${nlines32} entries"
echo "[5] IPv6: ${nlinesipv6} 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
@ -626,6 +543,9 @@ if [ ${netmask} -eq 0 ]; then
"4")
netmask=32
;;
"5" )
netmask="ipv6"
;;
"Q" | "q")
echo "You chose to abort. That's fine! Have a nice day!"
exit
@ -650,6 +570,15 @@ unset TEMP
echo "Processing ${file}."
# Handle IPv6 option
if [[ "${enableipv6}" -eq 1 ]]; then
if [[ "${netmask}" != "ipv6" ]]; then
cat "${file}" "${fileipv6}" | sort -n | sponge "${file}"
nlines=$(( ${nlines} + ${nlinesipv6} ))
fi
fi
# Invoke the processing function on the chosen file.
process_file "${file}"

View file

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