diff --git a/ddos-mitigator.conf b/ddos-mitigator.conf deleted file mode 100644 index abcd59e..0000000 --- a/ddos-mitigator.conf +++ /dev/null @@ -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" diff --git a/ddos-mitigator.sh b/ddos-mitigator.sh index 5dbc2b7..b9ef911 100755 --- a/ddos-mitigator.sh +++ b/ddos-mitigator.sh @@ -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}" diff --git a/geoip-lookup.py b/geoip-lookup.py index ccf4fc5..134dea4 100755 --- a/geoip-lookup.py +++ b/geoip-lookup.py @@ -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() - - return args.dbfile, args.address + 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] + 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") + (dbfile, ipaddress) = parse_command_line(argv) + code = get_county_code(ipaddress, dbfile) + print(code) 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