feature/ss-instead-of-netstat #2
1 changed files with 176 additions and 174 deletions
|
@ -3,7 +3,7 @@
|
|||
# #
|
||||
# Try and prevent apache overloads by banning IP addresses that have (too) #
|
||||
# many open connections. #
|
||||
# This script uses netstat to determine the connections to a configurable port #
|
||||
# This script uses ss to determine the connections to a configurable port #
|
||||
# on the host machine and provides automated GeoIP information retrieval based #
|
||||
# the address or the /24-, /16- or /8-subnet thereof. A GeoIP city- or country #
|
||||
# database must be installed separately and is provided to the script via a #
|
||||
|
@ -26,8 +26,8 @@
|
|||
# - net-analyzer/fail2ban (`fail2ban-client`) #
|
||||
# - sys-apps/coreutils (`cut`, `id`, `sort`, `touch`, `tr`, `uniq`) #
|
||||
# - sys-apps/grep (`grep`) #
|
||||
# - sys-apps/iproute2 (`ss`) #
|
||||
# - sys-apps/moreutils (`sponge`) #
|
||||
# - sys-apps/net-tools (`netstat`) #
|
||||
# - sys-apps/util-linux (`getopt`) #
|
||||
# #
|
||||
################################################################################
|
||||
|
@ -36,7 +36,7 @@
|
|||
MY_IP="94.199.214.20"
|
||||
|
||||
# After this point, no editing is required.
|
||||
start=$(date +%s)
|
||||
start="$(date +%s)"
|
||||
|
||||
# Dependencies of this script. Simple array with the following structure:
|
||||
# (command package [...])
|
||||
|
@ -51,8 +51,8 @@ dependencies=(
|
|||
"tr" "sys-apps/coreutils"
|
||||
"uniq" "sys-apps/coreutils"
|
||||
"grep" "sys-apps/grep"
|
||||
"ss" "sys-apps/iproute2"
|
||||
"sponge" "sys-apps/moreutils"
|
||||
"netstat" "sys-apps/net-tools"
|
||||
"getopt" "sys-apps/util-linux"
|
||||
)
|
||||
|
||||
|
@ -83,8 +83,8 @@ function is_installed() {
|
|||
}
|
||||
|
||||
function print_missing_dependency() {
|
||||
local command="$1"
|
||||
local package="$2"
|
||||
local command="${1}"
|
||||
local package="${2}"
|
||||
|
||||
echo "${red}Command ${bold}${command}${reset}${red} not found.${reset} Please install package ${blue}${package}${reset}." >&2
|
||||
}
|
||||
|
@ -97,12 +97,12 @@ function check_dependencies() {
|
|||
# 0: true, all installed; 1: false, at least one command/package missing
|
||||
local all_installed=0
|
||||
|
||||
for (( i=0; i<${arraylength}; i+=2 )) ; do
|
||||
for ((i = 0; i < ${arraylength}; i += 2)); do
|
||||
command="${dependencies[$i]}"
|
||||
package="${dependencies[$i+1]}"
|
||||
package="${dependencies[$i + 1]}"
|
||||
is_installed "${command}" "${package}"
|
||||
res=$?
|
||||
if [[ $res -ne 0 ]] ; then
|
||||
if [[ ${res} -ne 0 ]]; then
|
||||
print_missing_dependency "${command}" "${package}"
|
||||
all_installed=1
|
||||
fi
|
||||
|
@ -165,7 +165,7 @@ ENDOFHELP
|
|||
}
|
||||
|
||||
function exec_as_root() {
|
||||
if [[ $(id -un) == "root" ]] ; then
|
||||
if [[ $(id -un) == "root" ]]; then
|
||||
"$@"
|
||||
else
|
||||
sudo "$@"
|
||||
|
@ -174,22 +174,22 @@ function exec_as_root() {
|
|||
|
||||
function filter() {
|
||||
# list of current connections
|
||||
file="$1"
|
||||
file="${1}"
|
||||
# subnet extension, e.g. ".0.0"
|
||||
ext="$2"
|
||||
ext="${2}"
|
||||
# subnet suffix, e.g. "/16"
|
||||
suffix="$3"
|
||||
suffix="${3}"
|
||||
rm -f "${filtered}"
|
||||
touch "${filtered}"
|
||||
|
||||
# Reject already banned addresses
|
||||
while read -r -u3 address ; do
|
||||
if [[ "${banned}" != *"${address}${ext}${suffix}"* ]] ; then
|
||||
echo "${address}" >> "${filtered}"
|
||||
while read -r -u3 address; do
|
||||
if [[ "${banned}" != *"${address}${ext}${suffix}"* ]]; then
|
||||
echo "${address}" >>"${filtered}"
|
||||
else
|
||||
echo "IGNORING ${address}${ext}${suffix}, already banned."
|
||||
fi
|
||||
done 3< "${file}"
|
||||
done 3<"${file}"
|
||||
|
||||
mv "${filtered}" "${file}"
|
||||
}
|
||||
|
@ -197,7 +197,7 @@ function filter() {
|
|||
function parse_command_line_args() {
|
||||
TEMP=$(getopt -o 'a::,c:,d:,e,j:,n:,p:,h' -l 'auto::,country:,database:,dependencies,jail:,netmask:,port:,help' -- "$@")
|
||||
|
||||
if [ $? -ne 0 ] ; then
|
||||
if [ $? -ne 0 ]; then
|
||||
echo 'Error parsing command line options. Terminating. Invoke with --help for help.' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
@ -205,91 +205,91 @@ function parse_command_line_args() {
|
|||
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
|
||||
;;
|
||||
'-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
|
||||
;;
|
||||
'-e'|'--dependencies')
|
||||
check_dependencies
|
||||
exit $?
|
||||
;;
|
||||
'-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
|
||||
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
|
||||
;;
|
||||
'-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
|
||||
;;
|
||||
'-e' | '--dependencies')
|
||||
check_dependencies
|
||||
exit $?
|
||||
;;
|
||||
'-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
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
if [[ -z "${database}" ]] ; then
|
||||
echo "No GeoIP database specified. Invoke with --help for more information." >&2
|
||||
if [[ -z "${database}" ]]; then
|
||||
echo "No GeoIP database specified. Invoke with --help for more information." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -r "${database}" ]] ; then
|
||||
echo "Database '${database}' is not accessible." >&2
|
||||
if [[ ! -r "${database}" ]]; then
|
||||
echo "Database '${database}' is not accessible." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
@ -301,52 +301,52 @@ function parse_command_line_args() {
|
|||
# color to the next happen at different values.
|
||||
################################################################################
|
||||
function set_highlight_color() {
|
||||
local count=$1
|
||||
local count=${1}
|
||||
case "${choice}" in
|
||||
"1" )
|
||||
"1")
|
||||
# /32: 0 <= green < 3 <= yellow < 5 <= red
|
||||
if [ $count -ge 5 ] ; then
|
||||
hilite="${red}"
|
||||
elif [ $count -ge 3 ] ; then
|
||||
hilite="${yellow}"
|
||||
else
|
||||
hilite="${green}"
|
||||
fi
|
||||
if [ ${count} -ge 5 ]; then
|
||||
hilite="${red}"
|
||||
elif [ ${count} -ge 3 ]; then
|
||||
hilite="${yellow}"
|
||||
else
|
||||
hilite="${green}"
|
||||
fi
|
||||
;;
|
||||
"2" )
|
||||
"2")
|
||||
# /24: 0 <= green < 7 <= yellow < 13 <= red
|
||||
if [ $count -ge 13 ] ; then
|
||||
hilite="${red}"
|
||||
elif [ $count -ge 7 ] ; then
|
||||
hilite="${yellow}"
|
||||
else
|
||||
hilite="${green}"
|
||||
fi
|
||||
if [ ${count} -ge 13 ]; then
|
||||
hilite="${red}"
|
||||
elif [ ${count} -ge 7 ]; then
|
||||
hilite="${yellow}"
|
||||
else
|
||||
hilite="${green}"
|
||||
fi
|
||||
;;
|
||||
"3" )
|
||||
"3")
|
||||
# /16: 0 <= green < 13 <= yellow < 25 <= red
|
||||
if [ $count -ge 25 ] ; then
|
||||
hilite="${red}"
|
||||
elif [ $count -ge 13 ] ; then
|
||||
hilite="${yellow}"
|
||||
else
|
||||
hilite="${green}"
|
||||
fi
|
||||
if [ ${count} -ge 25 ]; then
|
||||
hilite="${red}"
|
||||
elif [ ${count} -ge 13 ]; then
|
||||
hilite="${yellow}"
|
||||
else
|
||||
hilite="${green}"
|
||||
fi
|
||||
;;
|
||||
"4" )
|
||||
"4")
|
||||
# /8: 0 <= green < 21 <= yellow < 49 <= red
|
||||
if [ $count -ge 49 ] ; then
|
||||
hilite="${red}"
|
||||
elif [ $count -ge 21 ] ; then
|
||||
hilite="${yellow}"
|
||||
else
|
||||
hilite="${green}"
|
||||
fi
|
||||
if [ ${count} -ge 49 ]; then
|
||||
hilite="${red}"
|
||||
elif [ ${count} -ge 21 ]; then
|
||||
hilite="${yellow}"
|
||||
else
|
||||
hilite="${green}"
|
||||
fi
|
||||
;;
|
||||
* )
|
||||
*)
|
||||
# ???: We should never get here. As a fall-back, just use no
|
||||
# highlighting.
|
||||
hilite=""
|
||||
hilite=""
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
@ -357,7 +357,7 @@ function set_highlight_color() {
|
|||
# GeoIP database. The user can then choose to ban or ignore the address.
|
||||
# Addresses chosen to be banned are appended to the $banlist.
|
||||
################################################################################
|
||||
function process_file () {
|
||||
function process_file() {
|
||||
local file="${1}"
|
||||
local line=''
|
||||
local count=0
|
||||
|
@ -369,25 +369,25 @@ function process_file () {
|
|||
# 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
|
||||
while IFS= read -r -u3 line; do
|
||||
line="$(echo "${line}" | tr -s '[:blank:]')"
|
||||
count="$(echo "${line}" | cut -d' ' -f2)"
|
||||
addronly="$(echo "${line}" | cut -d' ' -f3-)${ext}"
|
||||
addrwithsuffix="${addronly}${suffix}"
|
||||
set_highlight_color "${count}"
|
||||
country="$("${curdir}/geoip-lookup.py" -f "${database}" "${addronly}")"
|
||||
if [[ autopilot -eq 0 ]] ; then
|
||||
if [[ ${autopilot} -eq 0 ]]; then
|
||||
echo "Country: '${yellow}${country}${reset}'"
|
||||
fi
|
||||
echo -n "Address ${bold}$((nline++)) of ${nlines}${reset}: \
|
||||
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]? "
|
||||
read banaction
|
||||
else
|
||||
if [[ " ${bancountries[@]} " =~ " ${country} " ]] ; then
|
||||
if [[ $count -ge $autopilot ]] ; then
|
||||
if [[ " ${bancountries[@]} " =~ " ${country} " ]]; then
|
||||
if [[ ${count} -ge ${autopilot} ]]; then
|
||||
echo -en "\n${red}Autopilot active. ${reset}"
|
||||
banaction=y
|
||||
else
|
||||
|
@ -395,7 +395,7 @@ Found '${blue}${addrwithsuffix}${reset}' ${hilite}${count}${reset} times."
|
|||
return
|
||||
fi
|
||||
else
|
||||
if [[ $count -ge $autopilot ]] ; then
|
||||
if [[ ${count} -ge ${autopilot} ]]; then
|
||||
echo -en "\n${green}Autopilot active. ${reset}"
|
||||
banaction=n
|
||||
else
|
||||
|
@ -406,28 +406,28 @@ Found '${blue}${addrwithsuffix}${reset}' ${hilite}${count}${reset} times."
|
|||
fi
|
||||
|
||||
case "${banaction}" in
|
||||
"s" | "S" )
|
||||
echo "Not banning '${blue}${addrwithsuffix}${reset}', \
|
||||
"s" | "S")
|
||||
echo "Not banning '${blue}${addrwithsuffix}${reset}', \
|
||||
skipping remaining addresses."
|
||||
return
|
||||
return
|
||||
;;
|
||||
"y" | "Y" )
|
||||
echo "Adding '${blue}${addrwithsuffix}${reset}' to \
|
||||
"y" | "Y")
|
||||
echo "Adding '${blue}${addrwithsuffix}${reset}' to \
|
||||
banlist (country=${yellow}${country}${reset})."
|
||||
echo "${addrwithsuffix}" >> "${banlist}"
|
||||
echo "${addrwithsuffix}" >>"${banlist}"
|
||||
;;
|
||||
"n" | "N" | * )
|
||||
echo "Not banning '${blue}${addrwithsuffix}${reset}' (country=${yellow}${country}${reset})."
|
||||
"n" | "N" | *)
|
||||
echo "Not banning '${blue}${addrwithsuffix}${reset}' (country=${yellow}${country}${reset})."
|
||||
;;
|
||||
esac
|
||||
# Here goes: Pipe the file contents via filedescriptor 3.
|
||||
done 3< "${file}"
|
||||
# Here goes: Pipe the file contents via filedescriptor 3.
|
||||
done 3<"${file}"
|
||||
echo "Processed all entries in ${file}."
|
||||
}
|
||||
|
||||
# Create a temp directory, chdir into it and create the (initially empty)
|
||||
# banlist file.
|
||||
tmpdir=$(mktemp -d)
|
||||
tmpdir="$(mktemp -d)"
|
||||
|
||||
# Set up all file paths
|
||||
curdir="$(dirname "$0")"
|
||||
|
@ -440,8 +440,6 @@ file24="${tmpdir}/sorted-http-24.txt"
|
|||
file32="${tmpdir}/sorted-http-32.txt"
|
||||
# This file will contain the addresses to be banned.
|
||||
banlist="${tmpdir}/banlist.txt"
|
||||
# This file contains the output of the last invocation of whois
|
||||
whoisoutput="${tmpdir}/whois.txt"
|
||||
|
||||
touch "${banlist}"
|
||||
|
||||
|
@ -457,7 +455,7 @@ parse_command_line_args "$@"
|
|||
|
||||
check_dependencies
|
||||
dependencies_ok=$?
|
||||
if [[ ${dependencies_ok} -ne 0 ]] ; then
|
||||
if [[ ${dependencies_ok} -ne 0 ]]; then
|
||||
exit ${dependencies_ok}
|
||||
fi
|
||||
|
||||
|
@ -466,14 +464,18 @@ banned="$(exec_as_root fail2ban-client get "${jail}" banip)"
|
|||
|
||||
# Determine the current connections to the desired port; store the raw data in
|
||||
# $fileraw.
|
||||
netstat -nt | grep "${MY_IP}:${port}" | tr -s '[:blank:]' | cut -d' ' -f5 \
|
||||
| cut -d: -f1 | sort > "${fileraw}"
|
||||
connections=$(ss -HOn state established "( sport = :${port} )" | tr -s '[:blank:]' | cut -d' ' -f5)
|
||||
|
||||
# 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.
|
||||
cp "${fileraw}" "${file32}"
|
||||
cut -d. -f1-3 "${fileraw}" | sort > "${file24}"
|
||||
cut -d. -f1-2 "${fileraw}" | sort > "${file16}"
|
||||
cut -d. -f1 "${fileraw}" | sort > "${file8}"
|
||||
sort "${fileraw}" >"${file32}"
|
||||
cut -d. -f1-3 "${fileraw}" | sort >"${file24}"
|
||||
cut -d. -f1-2 "${fileraw}" | sort >"${file16}"
|
||||
cut -d. -f1 "${fileraw}" | sort >"${file8}"
|
||||
|
||||
# Filter already banned addresses
|
||||
filter "${file32}" "${ext32}" "${suffix32}"
|
||||
|
@ -493,7 +495,7 @@ nlines24=$(cat "${file24}" | wc -l)
|
|||
nlines16=$(cat "${file16}" | 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.
|
||||
echo "We've got:"
|
||||
echo "[1] 8bit: ${nlines8} entries"
|
||||
|
@ -506,25 +508,25 @@ if [ ${netmask} -eq 0 ] ; then
|
|||
# $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
|
||||
"1")
|
||||
netmask=8
|
||||
;;
|
||||
"2" )
|
||||
netmask=16
|
||||
"2")
|
||||
netmask=16
|
||||
;;
|
||||
"3" )
|
||||
netmask=24
|
||||
"3")
|
||||
netmask=24
|
||||
;;
|
||||
"4" )
|
||||
netmask=32
|
||||
"4")
|
||||
netmask=32
|
||||
;;
|
||||
"Q" | "q" )
|
||||
echo "You chose to abort. That's fine! Have a nice day!"
|
||||
exit
|
||||
"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
|
||||
*)
|
||||
echo "Invalid input: ${choice}. I'm out of here."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
@ -554,11 +556,11 @@ sudo -k
|
|||
|
||||
# Iterate over all addresses in $banlist and invoke fail2ban-client on each
|
||||
# one of them.
|
||||
while read -r addrwithsuffix ; do
|
||||
while read -r addrwithsuffix; do
|
||||
echo "Banning ${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}"
|
||||
|
|
Loading…
Reference in a new issue