Our Plesk servers need to be accessed from all over the world, but some services are better of when they are only accessible from selected countries.
I created a script that can be easily configured to block some countries.
It is also able to do the reverse. It can open up a port for a certain country.
The core mechanism is provided by ipset
If installed it can manage sets of subnets which can be referenced from iptables.
These country sets are automatically created using country codes and a website which provides all those subnets.
You can give a list of countries that should be whitelisted and a list that should be blacklisted.
To be reliable I can't just reference these sets from a static iptables firewall. These sets may not yet exist. Therefore one half of the script is creating the sets and the other half is adding them to the firewall.
The script does need an entry for that port already in the firewall.
It uses its position in iptables to add an extra iptables row just before that one.
I would recommend to put this script in an hourly cronjob.
Most of the time this script would do nothing, but after some time it will refresh those country lists.
Why an hourly cronjob?
Well, after a reboot all is gone. No sets will survive and also the firewall will be empty or the default one.
Within the hour these country-lines will be added.
In my example I am whitelisting my own country (nl = Netherlands) and blacklisting China and Seychelles (cn,sc)
The whitelist will seek the port 3306 entry and add a whitelist for the Netherlands in front.
It will search for an entry with either DROP or ACCEPT in it
It's not case sensitive so logdrop and logaccept will work too.
Port 8443 and 22 will then be blacklisted for China and Seychelles.
Of course you need to install ipset (apt install ipset).
ipset doesn't run on virtual network adapters.
If your firewall looks like this
iptables --line-numbers -nL INPUT | grep 'dpt:22'
It will look afterward like this:
iptables --line-numbers -nL INPUT | grep 'dpt:22'
You can create a ipset-country.conf to overwrite the variables
cat /usr/local/sbin/ipset-country.conf
ln -s /usr/local/sbin/ipset-country /etc/cron.hourly
cat /usr/local/sbin/ipset-country
I created a script that can be easily configured to block some countries.
It is also able to do the reverse. It can open up a port for a certain country.
The core mechanism is provided by ipset
If installed it can manage sets of subnets which can be referenced from iptables.
These country sets are automatically created using country codes and a website which provides all those subnets.
You can give a list of countries that should be whitelisted and a list that should be blacklisted.
To be reliable I can't just reference these sets from a static iptables firewall. These sets may not yet exist. Therefore one half of the script is creating the sets and the other half is adding them to the firewall.
The script does need an entry for that port already in the firewall.
It uses its position in iptables to add an extra iptables row just before that one.
I would recommend to put this script in an hourly cronjob.
Most of the time this script would do nothing, but after some time it will refresh those country lists.
Why an hourly cronjob?
Well, after a reboot all is gone. No sets will survive and also the firewall will be empty or the default one.
Within the hour these country-lines will be added.
In my example I am whitelisting my own country (nl = Netherlands) and blacklisting China and Seychelles (cn,sc)
The whitelist will seek the port 3306 entry and add a whitelist for the Netherlands in front.
It will search for an entry with either DROP or ACCEPT in it
It's not case sensitive so logdrop and logaccept will work too.
Port 8443 and 22 will then be blacklisted for China and Seychelles.
Of course you need to install ipset (apt install ipset).
ipset doesn't run on virtual network adapters.
If your firewall looks like this
iptables --line-numbers -nL INPUT | grep 'dpt:22'
Code:
18 ownservers tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
19 bruteprotect tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
20 logaccept tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
iptables --line-numbers -nL INPUT | grep 'dpt:22'
Code:
18 ownservers tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
19 bruteprotect tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
20 DROP tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:22 match-set ipset-country-sc src
21 DROP tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:22 match-set ipset-country-cn src
22 logaccept tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
You can create a ipset-country.conf to overwrite the variables
cat /usr/local/sbin/ipset-country.conf
Code:
WHITE_COUNTRIES=nl,be
BLACK_COUNTRIES=sc,cn,in,pk
WHITE_TCP=
BLACK_TCP=8443,22
WHITE_UDP=
BLACK_UDP=
ln -s /usr/local/sbin/ipset-country /etc/cron.hourly
cat /usr/local/sbin/ipset-country
Code:
#!/bin/bash
HEADLESS=
tty >/dev/null || HEADLESS=true
entries() {
ipset list $1 2>/dev/null | grep -c '\..*\..*\.'
}
THISSCRIPT="`readlink -f $0`"
BASE=${THISSCRIPT##*/}
[ -z "${BASE}" ] && BASE=${0##*/}
IPDENY_PREFIX=http://ipdeny.com/ipblocks/data/aggregated
IPDENY_POSTFIX=aggregated.zone
WHITE_COUNTRIES=nl,be
BLACK_COUNTRIES=sc,cn
WHITE_TCP=
BLACK_TCP=8443,22
WHITE_UDP=
BLACK_UDP=
# Using a seperate config file makes it easier to distribute the script over more servers
if grep -q "^WHITE_TCP" ${THISSCRIPT}.conf 2>/dev/null ; then
[ ${HEADLESS} ] || echo "Use the config file ${THISSCRIPT}.conf"
. ${THISSCRIPT}.conf
fi
# Some sanity checks first
IPTABLES_LENGTH=`iptables-save | grep -c INPUT`
if [ ${IPTABLES_LENGTH} -lt 10 ] ; then
echo "iptables is less than 10 rows long, there's not much use to go on" >&2
exit 1
fi
# Check if ipset is installed
if ! ipset -v >/dev/null 2>&1 ; then
echo "This script relies on ipset, without it this will NOT work!" >&2
echo "Use sudo apt-get install ipset !!!" >&2
exit 1
fi
# Check if aggregate is installed
if ! which aggregate 2>&1 >/dev/null ; then
echo "This script relies on aggregate, without it this will NOT work!" >&2
echo "Use sudo apt-get install aggregate !!!" >&2
exit 1
fi
# Now define the 2 core functions
create_set() {
# remove country IP-list when older than 30 days
find /tmp -maxdepth 1 -type f -name ${BASE}-${i} -mtime +30 -exec rm {} \;
if [ ! -f /tmp/${BASE}-${i} ] || [ `entries ${BASE}-${i}` -eq 0 ] ; then
# Fetch netblocks for this country from the site http://ipdeny.com/ipblocks/data/aggregated
if wget -qO /tmp/${BASE}-${i}.tmp ${IPDENY_PREFIX}/$i-${IPDENY_POSTFIX} ; then
# I noticed these aren't always aggregated, so I do this myself
egrep -o "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9/]+" /tmp/${BASE}-${i}.tmp | aggregate 2>/dev/null >/tmp/${BASE}-${i}
rm /tmp/${BASE}-${i}.tmp
# If the file contains CIDR-addresses then continue
if [ -s /tmp/${BASE}-${i} ] ; then
[ ${HEADLESS} ] || echo "Create set ${BASE}-${i}"
# Create the country-set if it does not exist yet
if ! ipset -exist create ${BASE}-${i} hash:net ; then
echo -e "\n\nIt seems iptables can't work with ipset, this is probably a virtual machine!!\n\n" >&2
rm /tmp/${BASE}-${i}
exit 1
fi
# Flush the addresses as we just picked up a fresh list
ipset flush ${BASE}-${i}
# Add all the subnets to the set
while read IPNET ; do
ipset add ${BASE}-${i} ${IPNET}
done</tmp/${BASE}-${i}
fi
fi
else
[ ${HEADLESS} ] || echo "The set ${BASE}-${i} is still current"
fi
}
add_to_firewall() {
[ `entries ${BASE}-${i}` -eq 0 ] && return # only useful if we have a filled set
# loop through all the ports
for j in ${PORT//,/ } ; do
[ ${HEADLESS} ] || echo -e " ${PROT}:${j} set ${BASE}-${i}"
# Find the catch-all of this port in the current firewall
ANCHOR=`iptables --line-numbers -nL INPUT | \
egrep -i "(ACCEPT|DROP) *${PROT}.*0\.0\.0\.0/0 *0\.0\.0\.0/0 *${PROT} dpt:$j$" | \
head -n1 | awk '{print $1}'`
# Did or didn't we find the anchor to attach our iptables lines?
if [ -z "${ANCHOR}" ] ; then
echo "Unable to find the anchor for ${PROT} port ${j} in iptables" >&2
else
# Find the ipset line for this protocol / port / ipset
INSERT=`iptables --line-numbers -nL INPUT | \
grep "${ACTION} *${PROT}.*0\.0\.0\.0/0 *0\.0\.0\.0/0.*${PROT} dpt:$j match-set ${BASE}-${i} src" | \
head -n1 | awk '{print $1}'`
if [ -n "${INSERT}" ] ; then
[ ${HEADLESS} ] || echo -e "\tA line for ${PROT} dpt:$j is already added (${BASE}-${i}) @ ${INSERT}"
else
if iptables -I INPUT ${ANCHOR} -p ${PROT} --dport ${j} -m set --match-set ${BASE}-${i} src -j ${ACTION} ; then
echo -e "\tAdd ${ACTION}-line for port ${j} (set ${BASE}-${i}) @ position ${ANCHOR}"
else
echo -e "\n\nIt seems iptables is giving some problem, abort!! " >&2
exit 1
fi
fi
fi
done
}
# Let's get to work
[ ${HEADLESS} ] || echo "Create the sets for whitelisting...."
for i in ${WHITE_COUNTRIES//,/ } ; do
[ ${HEADLESS} ] || echo -n "$i... "
create_set
done
[ ${HEADLESS} ] || echo -e "\nCreate the sets for blacklisting...."
for i in ${BLACK_COUNTRIES//,/ } ; do
[ ${HEADLESS} ] || echo -n "$i... "
create_set
done
[ ${HEADLESS} ] || echo -e "\n\nAdd entries to firewall"
n=1
# Loop 4 times through the firewall script with different parameters
while [ $n -le 4 ] ; do
[ $n -eq 1 ] && PORT=${WHITE_TCP} && PROT=tcp && COUNTRIES=${WHITE_COUNTRIES} && ACTION=ACCEPT
[ $n -eq 2 ] && PORT=${WHITE_UDP} && PROT=udp
[ $n -eq 3 ] && PORT=${BLACK_TCP} && PROT=tcp && COUNTRIES=${BLACK_COUNTRIES} && ACTION=DROP
[ $n -eq 4 ] && PORT=${BLACK_UDP} && PROT=udp
for i in ${COUNTRIES//,/ } ; do
add_to_firewall
done
let n+=1
done
Last edited: