Linux Iptables Just Block By Country

by on March 2, 2009 · 167 comments· LAST UPDATED August 28, 2013

in , ,

I admin ecom website and a lot of bogus traffic comes from countries that do not offer much in commercial value. How do I just configure Apache or iptables to just refuse connections to certain countries?

You can block traffic at both Apache or iptables level. I recommend iptables to save some resources. First, you need to get list of netblocks for each country. Simply visit this page and download IP block files are provided in CIDR format. Use the following shell script:

WARNING!People from other countries may use proxy server or think of spoofing their IP address. In such case, this may not work and it will only protect your box from automated scans or spam.
#!/bin/bash
# Purpose: Block all traffic from AFGHANISTAN (af) and CHINA (CN). Use ISO code. #
# See url for more info - http://www.cyberciti.biz/faq/?p=3402
# Author: nixCraft <www.cyberciti.biz> under GPL v.2.0+
# -------------------------------------------------------------------------------
ISO="af cn"
 
### Set PATH ###
IPT=/sbin/iptables
WGET=/usr/bin/wget
EGREP=/bin/egrep
 
### No editing below ###
SPAMLIST="countrydrop"
ZONEROOT="/root/iptables"
DLROOT="http://www.ipdeny.com/ipblocks/data/countries"
 
cleanOldRules(){
$IPT -F
$IPT -X
$IPT -t nat -F
$IPT -t nat -X
$IPT -t mangle -F
$IPT -t mangle -X
$IPT -P INPUT ACCEPT
$IPT -P OUTPUT ACCEPT
$IPT -P FORWARD ACCEPT
}
 
# create a dir
[ ! -d $ZONEROOT ] && /bin/mkdir -p $ZONEROOT
 
# clean old rules
cleanOldRules
 
# create a new iptables list
$IPT -N $SPAMLIST
 
for c  in $ISO
do
	# local zone file
	tDB=$ZONEROOT/$c.zone
 
	# get fresh zone file
	$WGET -O $tDB $DLROOT/$c.zone
 
	# country specific log message
	SPAMDROPMSG="$c Country Drop"
 
	# get 
	BADIPS=$(egrep -v "^#|^$" $tDB)
	for ipblock in $BADIPS
	do
	   $IPT -A $SPAMLIST -s $ipblock -j LOG --log-prefix "$SPAMDROPMSG"
	   $IPT -A $SPAMLIST -s $ipblock -j DROP
	done
done
 
# Drop everything 
$IPT -I INPUT -j $SPAMLIST
$IPT -I OUTPUT -j $SPAMLIST
$IPT -I FORWARD -j $SPAMLIST
 
# call your other iptable script
# /path/to/other/iptables.sh
 
exit 0

Save above script as root user and customize ISO variable to point out country name using ISO country names. Once done install the script as follows using crontab:
@weekly /path/to/country.block.iptables.sh
To start blocking immediately type:
# /path/to/country.block.iptables.sh
And you are done with blocking the whole country from your server.

iptables geoip patch

Another, alternative to above shell script is to use geoip iptables patch. This is not standard iptables modules. You need to download patch and compile Linux kernel.

  • Grab geoipt patch from the official website.
  • Download and install Linux kernel and iptables source code.
  • Grab and install tool called patch-o-matic (required for geoip modules).
  • Finally, grab GEO IP database from MaxMind.

The details of kernel compile and iptables patching are beyond the scope of this FAQ. This is left as an exercise to readers.

Further Enhancements (ready to use scripts)

See discussion below in the comments:

TwitterFacebookGoogle+PDF versionFound an error/typo on this page? Help us!

{ 167 comments… read them below or add one }

1 Josh March 2, 2009 at 8:53 pm

This was a very useful post, something I’ve been looking at doing for some time. Thank you for the great information!

Reply

2 Xaero March 3, 2009 at 7:30 am

i am not interested in iptables itself all that much but I have to say that the script is one of best formatted ones on the internet.

The nixCraft posts themselves are of excellent quality.
Keep it up,
Happy *nixing

Reply

3 Sudanking March 3, 2009 at 8:36 am

Nice topic, I like it and I like they way script is made. Good job.

Reply

4 Avin March 3, 2009 at 9:11 am

I definitely use this in my office.
Can I block this IP’s from Squid ?
Thank you for your post.

Reply

5 piavlo March 3, 2009 at 11:25 am

Creating so many iptables rules creates huge memory overhead loading all these rules as well as delay and cpu overhead while scanning all these rules for each new connection – so it’s not practical.
While you can use ipset module – http://ipset.netfilter.org
to make it work fast

Reply

6 Shantanu Oak March 3, 2009 at 11:47 am

# I am using the following script to ban all IP except a couple of known IP’s
# Can I improve the script?
#!/bin/sh
`iptables -F INPUT`

while read server; do
`iptables -A INPUT -p tcp –dport 3306 -s $server -j ACCEPT`
done <<HERE
17.29.0.21
17.29.0.22
HERE

`iptables -A INPUT -p tcp –dport 3306 -j REJECT`
`service iptables save`

Reply

7 ajp March 3, 2009 at 11:49 am

While I’m not commenting on the script per se, the motivation behind the script seems highly questionable…to broadly block users from entire countries from accessing e-commerce sites is unethical.
Myself, I’m an American engineer working in the oil fields of Nigeria who has to rely on internet access to banking, insurance, educational and technical sites just as you ALL do. If you block my access because i’m on a nigerian IP address, i’m screwed.
Thanks for making working in this place all the more difficult.

Reply

8 Herb B. D. Derbidy September 6, 2010 at 9:44 pm

Yeah, that’s a common scenario. You’re right, we should allow unrestricted access from Nigeria and every other country well known for scamming, phishing, hacking, cyber-crime, etc. just so you can get to your sites as you please. Why on earth would anyone expect you to be personally responsible for assuring personal, individual access to your personal bank? We really made a huge mistake. I mean it’s not like you can set up a U.S. proxy server, or use an existing one, or pay even a half-wit server nerd $50 to do it for you. All of the servers in this country should acquiesce to your needs immediately without regard to our own financial and data security. I’m going to go self-flagellate for my unethical, selfish behavior (right after I delete all passwords from my server). I’m so, so, so sorry.

Reply

9 David Picard September 7, 2010 at 10:41 am

Herb – if you read on a bit further you’ll see that I modified the script to allow access on ports you can define. HTTP vulnerabilities are out there, but leaving that port open and blocking traffic from the named countries on all other ports seems relatively harmless. If you want to leave HTTP open specify 80,443 as the open ports in my modified script which can be downloaded from my blog

Reply

10 David Picard February 19, 2011 at 3:05 pm

My blog url was changed during a recent reinstall of wordpress – I posted a comment on the end with the new blog link and here at my first (of many) comments

Reply

11 nixCraft March 3, 2009 at 2:17 pm

@piavlo,
Yes, ipset would be nice due to speed factor and to avoid errors.

Reply

12 Johan March 3, 2009 at 2:47 pm

Hi Vivek, Is there a way to log all traffic on a server or Linux router, blocked or not, maybe like a report to round off the above handy script?

Reply

13 nixCraft March 3, 2009 at 2:57 pm

@Johan,

You can setup a central logging Linux host using syslogd itself. You can send all logs from system or router to loghost. No need to write a script.

Reply

14 Paul March 3, 2009 at 5:40 pm

That’s fine for just logging, Vivek, but this script isn’t about mere logging.

Reply

15 Paul March 3, 2009 at 5:41 pm

Oh, nvm, missed Johan’s post just before, please delete.

Reply

16 TwinReverb March 3, 2009 at 8:03 pm

Blocking by country may make sense in some settings, but may be offensive in others. Be careful how you define “no commercial value”, etc. I know of some people who block IP ranges like those originating in South Korea, forgetting that we have US military members stationed in that country. A common lock-out occurs on IRC.

What I am saying is that it is possible that blocking by country may not be the most direct way to fix some problems.

Reply

17 Johannes March 4, 2009 at 12:56 pm

I’m not touching the moral question, of blocking on the basis of country of origin. I just checked a random IP from the country codes of the given web site and it appears they don’t mach:
$ whois 89.149.128.5 |grep ^country
country: US
$ grep 89.149.128.0 eu.zone
89.149.128.0/18

So the script would think that the USA is a member of the EU. Other IPs of the EU zone seem to be wrong as well.

Reply

18 incidence March 10, 2009 at 6:00 am

For ubuntu & ufw:

#!/bin/bash
### Block all traffic from AFGHANISTAN (af) and CHINA (CN). Use ISO code ###
ISO="af cn vn"
### Set PATH ###
WGET=/usr/bin/wget
EGREP=/bin/egrep
### No editing below ###
ZONEROOT="/root/ufwzones/"
DLROOT="http://www.ipdeny.com/ipblocks/data/countries"
# create a dir
[ ! -d $ZONEROOT ] && /bin/mkdir -p $ZONEROOT
for c  in $ISO
do
        # local zone file
        tDB=$ZONEROOT/$c.zone
        echo "Downloading $c.zone .."
        # get fresh zone file
        $WGET -O $tDB $DLROOT/$c.zone >> /dev/null 2>&1
        BADIPS=$(egrep -v "^#|^$" $tDB)
        for ipblock in $BADIPS
        do
           echo "Blocking IP $ipblock.."
           /usr/sbin/ufw deny from $ipblock
        done
done
exit 0

Reply

19 Cobro March 11, 2009 at 7:57 am

Great post have been looking for this along time now :) thank you

Reply

20 incidence March 11, 2009 at 2:17 pm

Note, Ubuntu’s UFW stores the rules to file /var/lib/ufw/user.rules</. I still haven’t found a good way to clean the old rules before adding new ones (like if you want to delete china zone). Just delete ~1000 IP-addresses from there with vim, but remember to leave the end rules marker:

### RULES ###
*delete from here*
### END RULES ###
-A ufw-user-input -j RETURN
-A ufw-user-output -j RETURN
-A ufw-user-forward -j RETURN
COMMIT

Reply

21 cmanley November 30, 2010 at 1:10 pm

@incidence
I’ve got the same problem but use a Perl 1-liner to do it:

USERFILE=”/lib/ufw/user.rules”

perl -e ‘$f=pop(@ARGV); -f($f)||die($!); open(IN,”<$f")||die($!); $s=join("",); close(IN); $s =~ s/(### RULES ###).*(### END RULES ###)/$1\n$2/s; open(OUT,”>$f”)||die($!); print OUT $s; close(OUT);’ $USERFILE

Reply

22 hel March 17, 2009 at 1:10 pm

Guys, you sucks….
You think with your IPTABLES you can block people from other countries to visit your website?? ARE YOU KIDDING!!! What about online proxies? and what about thousands (may be millions) USA PCs without any protection which allow hackers and other newbies to access whatever they want???

THIS IS NOT A GOOD IDEA AT ALL!!!

If you want to protect your network, you got to implement a good firewall.

firewall builder or ipcop would be enough for you.

cheers.

hel

Reply

23 nig December 29, 2010 at 9:49 pm

Stfu you tool.

Reply

24 Diego Sánchez March 4, 2012 at 4:06 pm

Damn ..
I was wrong =(
I supposed that iptables was a firewall =(

OF COURSE YOU CANNOT BLOCK AT 100%, but we can do a little bit more difficult to scriptkiddies blocking SSH, FTP or whatever ¬¬

You want true security?
Cut the UTP

Reply

25 john April 3, 2009 at 5:39 am

this post is very useful.
Keep it up !

Reply

26 VB April 21, 2009 at 4:32 am

It kills me people talking about the “morality” of blocking countries. Morality is about should you sleep with your best friends wife not blocking an IP. We never blocked anyone for eight years. During that time we had traffic from certain countries that never brought a penny into our company. Those same countries traffic brought hackers and crackers and credit card fraud scammers by the groves to our company. If a certain segment is doing nothing but costing me money and time why should I not block them? Why should I even spend the time with working firewall rules on those countries traffic? As far as getting by my block with a proxy, if you have ever fought with hackers, crackers and scammers you would know that most of them are too lazy to target one site. They are looking for the easy buck. This is not to say that one should rely on only blocking countries. But blocking a country that has always been nothing but problems is the *front line”. If you are in the online security war you know that you need to knock out the bulk of the problem first and then layers of security after that. For the Americans living outside the USA move back home or buy a subscription to a proxy service.

Reply

27 Ahmed June 27, 2009 at 11:11 pm

So, basically IPSet would store everything in it allows you to add 1 rule only in IPtables to block an entire country?

Reply

28 David July 18, 2009 at 6:50 pm

Another person commented about cpu overhead of having so many rules (I’m using over 6200 rules for just 4 countries). How efficient is iptables with lots of rules? Does it test each rule in turn (until a match and action is taken) or does it use some sort of decision tree based on the rules to quickly know which rules (or group of rules) to test and which to ignore?

If iptables does not optimize, would the cpu overhead be reduced if multiple chains were used, one for each octet of the ip? For example, group all the X.*.*.* rules into a “countrydropX” chain. And then have 254 rules (no 0 or 255) in the countrydrop chain, such as:
iptables -A countrydrop -s 1.0.0.0/8 -j countrydrop1

iptables -A countrydrop -s 254.0.0.0/8 -j countrydrop254
This design assumes that all the country ip’s to be checked are /8 or higher.
This design increases the complexity but should significantly reduce the cpu overhead (assuming that iptables doesn’t already do some sort of optimization).

Reply

29 nixCraft July 18, 2009 at 7:46 pm

@ David,

For lots of ip use IPSet, it offers speed and uses less CPU as compared to iptables. http://ipset.netfilter.org/

Reply

30 David July 19, 2009 at 2:55 am

I can only use iptables. My server does not come with ipset and I cannot and will not install ipset. That’s why I’m asking if iptables efficiency would be increased by grouping rules by the first octet of the IP. I’ll assume that it will, and I will re-organize my IP rules that way. Thanks.

Reply

31 Paul August 2, 2009 at 6:11 am

My personal server was aggressively probed, nmapped, and generally DoS’ed today. I use iptables normally and my server name is somewhat complicated, because it is used for personal file access. Non-the-less, some host in China took it upon themselves to determine what I was going to do with a few of my valuable hours.

I manually stopped the attack and then installed this script. Just finished nmapping the new config and all is well and good.

Thanks for the script!

Reply

32 Bremm October 6, 2009 at 10:42 am

IP Deny table is incomplete. For example, domains SU and TK aren’t there.

Reply

33 Chris DE October 28, 2009 at 10:48 am

Great script. It reduces the attacks and spam here about 95%. I noticed that the attackers switched the source system to countries that are not blocked yet. I added those countries, too and they switched to another. Kind of funny, but may leads to block the whole word exept the countries that should have access. How about a 2nd script that allows to use it as a whitelist instead? It´s much faster to config iptables just to allow connects from xx, yy, zz. .. Would be a great help.

Reply

34 Rick November 25, 2009 at 2:03 am

For internet radio and rights you have to pay for playing music, on a particular server you might want to think the other way around. In fact you do not want anyone outside your own country to be able to connect to your server so you avoid any conflicts in rights to be paid on the music for every country that listens. Is it possible for you to rewrite this so it will allow only ip’s from 1 specific country (i.c. nl) to attach to the streaming server so that it does not mess up processing power?

Reply

35 Jay January 12, 2010 at 4:43 pm

Thank you very much, easy to use and VERY effective.

Reply

36 Dave January 28, 2010 at 3:50 pm

Thanks,

Excellent bit of work here. Finally a reliable way to basically make some of the more annoying countries go away. Excellent work indeed!

Reply

37 David Picard January 28, 2010 at 6:50 pm

Nice script – however I have modified the script slightly to cut down on unnecessary rule processing. Effectively, I’ve made it only deny new connections and it only searches through the rules that match the first octet in the ip range for each packet rather than all rules. I also do not wipe the entire iptable on refresh as this would eliminate custom rules I’ve added on VPN startup and foul my VPN connections. See below:

#!/bin/bash
### Block all traffic from AFGHANISTAN (af) and CHINA (CN). Use ISO code ###
ISO="af cn"
### Set PATH ###
IPT=/sbin/iptables
#IPT=/bin/echo
WGET=/usr/bin/wget
EGREP=/bin/egrep
### No editing below ###
SPAMLIST="countrydrop"
ZONEROOT="/root/iptables"
DLROOT="http://www.ipdeny.com/ipblocks/data/countries"
cleanOldRules(){
#$IPT -F
#$IPT -X
#$IPT -t nat -F
#$IPT -t nat -X
#$IPT -t mangle -F
#$IPT -t mangle -X
#$IPT -P INPUT ACCEPT
#$IPT -P OUTPUT ACCEPT
#$IPT -P FORWARD ACCEPT
$IPT -D INPUT -j $SPAMLIST
$IPT -D OUTPUT -j $SPAMLIST
$IPT -D FORWARD -j $SPAMLIST
$IPT -X $SPAMLIST
TOPIP=`iptables -L -n | grep Chain | cut -f 2 -d ' ' | grep '\-$SPAMLIST'`
for i  in $TOPIP
do
    $IPT -F ${i}
    $IPT -X ${i}
done
}
# create a dir
[ ! -d $ZONEROOT ] && /bin/mkdir -p $ZONEROOT
# clean old rules
cleanOldRules
# create a new iptables list
$IPT -N $SPAMLIST
for c  in $ISO
do
	# local zone file
	tDB=$ZONEROOT/$c.zone
	# get fresh zone file
	$WGET -O $tDB $DLROOT/$c.zone
	# country specific log message
	SPAMDROPMSG="$c Country Drop"
	# get
	BADIPS=$(egrep -v "^#|^$" $tDB)
	for ipblock in $BADIPS
	do
	   topip=`echo $ipblock | cut -f 1 -d '.'`
	   $IPT -A $topip-$SPAMLIST -s $ipblock -j LOG --log-prefix "$SPAMDROPMSG"
	   $IPT -A $topip-$SPAMLIST -s $ipblock -j DROP
	done
done
TOPIP=`iptables -L -n | grep Chain | cut -f 2 -d ' ' | grep '\-$SPAMLIST'`
for i  in $TOPIP
do
    sip=`echo ${i} | cut -f 1 -d '-'`.0.0.0/8
    $IPT -A $SPAMLIST -s ${sip} -j ${i}
done
# Drop everything
$IPT -I INPUT -m state --state NEW -j $SPAMLIST
$IPT -I OUTPUT -m state --state NEW -j $SPAMLIST
$IPT -I FORWARD -m state --state NEW -j $SPAMLIST
# call your other iptable script
# /path/to/other/iptables.sh
exit 0

Reply

38 David Picard January 28, 2010 at 6:52 pm

Sorry about the last post – please switch out the IPT environment from echo back to iptables – posted the debug version of the script ;-)

Reply

39 David Picard February 5, 2010 at 2:02 pm

I’ve modified the script a little further as the initial load using iptables line by line took over 13 hours. A cleaner and higher performance approach will leverage iptables-restore which commits the tables once at the end of the load instead of after each additional range addition.

In addition, I’ve added a parameter for allowed ports to allow only traffic destined for certain ports from the country to be blocked – so my friends serving in Afghanistan are now able to get to my web site.

Note on performance – using iptables-restore loads the rules in less than a second – see the below script:

#!/bin/bash
### Block all traffic from AFGHANISTAN (af) and CHINA (CN). Use ISO code ###
ISO="af cn"
### Set PATH ###
IPT=/sbin/iptables
WGET=/usr/bin/wget
EGREP=/bin/egrep
### No editing below ###
CBLIST="countrydrop"
ZONEROOT="/var/iptables"
IPTCBRESTORE="/etc/sysconfig/iptables.cb"
ALLOWPORTS=80,443
MAXZONEAGE=7
DLROOT="http://www.ipdeny.com/ipblocks/data/countries"
cleanOldRules(){
    $IPT -L $CBLIST > /dev/null 2>&1
    if [ $? = 0 ] ; then
	$IPT -D INPUT -j $CBLIST
	$IPT -D OUTPUT -j $CBLIST
	$IPT -D FORWARD -j $CBLIST
    fi
    TOPIP=`$IPT -L -n | grep Chain | cut -f 2 -d ' ' | grep '\-$CBLIST'`
    for i  in $TOPIP
    do
	$IPT -F ${i}
	$IPT -X ${i}
    done
    $IPT -X $CBLIST
}
updateZoneFiles() {
    ZONEARCH=${ZONEROOT}/arch
    mkdir -p ${ZONEARCH}
    find ${ZONEROOT} -maxdepth 1 -mindepth 1 -ctime +${MAXZONEAGE} -exec mv {} ${ZONEARCH} \;
    for c  in $ISO
    do
	# local zone file
	tDB=$ZONEROOT/$c.zone
	if [ -f $tDB ] ; then
	    printf "Zone file %s is new enough - no update required.\n" $tDB
	else
	    # get fresh zone file if it is newer than MAXZONEAGE days
	    $WGET -O $tDB $DLROOT/$c.zone
	fi
    done
    oldzones=`find ${ZONEROOT} -mindepth 1 -maxdepth 1 -type f -exec basename {} \; | cut -f 1 -d '.'`
    # Archive old zones no longer blocked
    for z in $oldzones ; do
	archme=${c}
	for c  in $ISO ; do
	    if [ $c = $z ] ; then archme="X"; fi
	done
	if [ $archme = $z ] ; then
	    mv ${archme} ${ZONEARCH}
	else
	    printf "Working from previous zone file for %s\n" ${z}
	fi
    done
}
createIPTLoadFile() {
    printf "# Generated by %s on" $0 > ${IPTCBRESTORE}
    printf "%s " `date` >> ${IPTCBRESTORE}
    printf "\n*filter\n" >> ${IPTCBRESTORE}
    # Create CBLIST chain
    printf ":$CBLIST - [0:0]\n" >> ${IPTCBRESTORE}
    printf "%s INPUT -j $CBLIST\n" "-I" > ${IPTCBRESTORE}.tmp
    printf "%s OUTPUT -j $CBLIST\n" "-I" >> ${IPTCBRESTORE}.tmp
    printf "%s FORWARD -j $CBLIST\n" "-I" >> ${IPTCBRESTORE}.tmp
    if [ "Z${ALLOWPORTS}" = "Z" ] ; then
	printf "Blocking all traffic from country - no ports allowed\n"
    else
	printf "%s $CBLIST -p tcp -m multiport ! --dports ${ALLOWPORTS} -j RETURN\n" "-I">> ${IPTCBRESTORE}.tmp
    fi
    for c  in $ISO
    do
	# local zone file
	tDB=$ZONEROOT/$c.zone
	# country specific log message
	SPAMDROPMSG="$c Country Drop"
        # Create drop chain for identified packets
	CBLISTDROP=${c}-${CBLIST}-DROP
	printf ":${CBLISTDROP} - [0:0]\n" >> ${IPTCBRESTORE}
	printf "%s ${CBLISTDROP} -j LOG --log-prefix \"$SPAMDROPMSG\"\n" "-A" >> ${IPTCBRESTORE}.tmp
	printf "%s ${CBLISTDROP} -j DROP\n" "-A" >> ${IPTCBRESTORE}.tmp
	# Load IP ranges into chains correlating to first octet
	BADIPS=$(egrep -v "^#|^$" $tDB)
	for ipblock in $BADIPS
	do
	    topip=`echo $ipblock | cut -f 1 -d '.'`
	    chainExists=`grep -c :${topip}-${CBLIST} ${IPTCBRESTORE}`
	    if [ $chainExists = 0 ] ; then
		printf "Creating chain for octet %s\n" ${topip}
		printf ":$topip-$CBLIST - [0:0]\n" >> ${IPTCBRESTORE}
		sip=${topip}.0.0.0/8
		printf "%s $CBLIST -s ${sip} -j $topip-$CBLIST\n" "-A" >> ${IPTCBRESTORE}.tmp
	    fi
	    printf "  Adding rule for %s to chain for octet %s\n" ${ipblock} ${topip}
	    printf "%s $topip-$CBLIST -s $ipblock -j ${CBLISTDROP}\n" "-A" >> ${IPTCBRESTORE}.tmp
	done
    done
    cat ${IPTCBRESTORE}.tmp >> ${IPTCBRESTORE} && rm -f ${IPTCBRESTORE}.tmp
    printf "COMMIT\n# Completed on " >> ${IPTCBRESTORE}
    printf "%s " `date` >> ${IPTCBRESTORE}
    printf "\n" >> ${IPTCBRESTORE}
}
directLoadTables() {
    # Create CBLIST chain
    $IPT -N $CBLIST
    $IPT -I INPUT -j $CBLIST
    $IPT -I OUTPUT -j $CBLIST
    $IPT -I FORWARD -j $CBLIST
    if [ "Z${ALLOWPORTS}" = "Z" ] ; then
	printf "Blocking all traffic from country - no ports allowed\n"
    else
	$IPT -I $CBLIST -p tcp -m multiport ! --dports ${ALLOWPORTS} -j RETURN
    fi
    for c  in $ISO
    do
	# local zone file
	tDB=$ZONEROOT/$c.zone
	# country specific log message
	SPAMDROPMSG="$c Country Drop"
        # Create drop chain for identified packets
	CBLISTDROP=${c}-${CBLIST}-DROP
	$IPT -N ${CBLISTDROP}
	$IPT -A ${CBLISTDROP} -j LOG --log-prefix "$SPAMDROPMSG"
	$IPT -A ${CBLISTDROP} -j DROP
	# Load IP ranges into chains correlating to first octet
	BADIPS=$(egrep -v "^#|^$" $tDB)
	for ipblock in $BADIPS
	do
	    topip=`echo $ipblock | cut -f 1 -d '.'`
	    $IPT -L $topip-$CBLIST > /dev/null 2>&1
	    if [ $? = 1 ] ; then
		printf "Creating chain for octet %s\n" ${topip}
		$IPT -N $topip-$CBLIST
		sip=${topip}.0.0.0/8
		$IPT -A $CBLIST -s ${sip} -j $topip-$CBLIST
	    fi
	    printf "  Adding rule for %s to chain for octet %s\n" ${ipblock} ${topip}
	    $IPT -A $topip-$CBLIST -s $ipblock -j ${CBLISTDROP}
	done
    done
}
loadTables() {
    createIPTLoadFile
    ${IPT}-restore -n ${IPTCBRESTORE}
    #directLoadTables
}
# create a dir
[ ! -d $ZONEROOT ] && /bin/mkdir -p $ZONEROOT
# clean old rules
cleanOldRules
# update zone files as needed
updateZoneFiles
# create a new iptables list
loadTables
exit 0

Reply

40 David Picard February 5, 2010 at 2:06 pm

Forgot one point on the release notes – I don’t like to hammer people who provide a valuable resource – I also added a check to ensure the age of the zone file was greater than 7 days (configurable) before going to the ipdeny.com site to get new content. 30 days might be a better default – but 7 is better than all the time ;-)

Reply

41 nixCraft February 5, 2010 at 2:46 pm

@David,

Excellent work! I’ve updated faq and created a download link to your post. Thanks for your contribution.

Reply

42 IzFazt February 5, 2010 at 3:42 pm

Great Job! Following this topic and using the script for three months now on all servers. Unfortunately it seems it has come to a point where you can’t run a server without it. This should be part in the future of all major linux distro’s in utility’s like yast, yum to make it a little bit more accesible to everyone. Once again, great job!

Reply

43 David Picard February 8, 2010 at 1:59 pm

After a little monitoring and testing with the port exclusions, I realized the –dports options were inverted – the ‘!’ operator needs to be removed from those rules.

Reply

44 David Picard February 25, 2010 at 2:37 pm

Noticed the China block also includes the private class C subnet of 192.168.0.0/16 – modified the script again slightly to skip filtering the private subnet – as below. Note that the subnet can be changed to filter against a smaller subnet and exclude subnets you don’t serve:

#!/bin/bash
# Originally from http://www.cyberciti.biz/faq/block-entier-country-using-iptables
### Block all traffic from AFGHANISTAN (af) and CHINA (CN). Use ISO code ###
ISO="af cn kr"
### Set PATH ###
IPT=/sbin/iptables
WGET=/usr/bin/wget
EGREP=/bin/egrep
### No editing below ###
CBLIST="countrydrop"
ZONEROOT="/var/iptables"
IPTCBRESTORE="/etc/sysconfig/iptables.cb"
IPTCBDEVICE=eth0
ALLOWPORTS=80,443
ALLOWSUBNET=192.168.0.0/255.255.0.0
MAXZONEAGE=7
DLROOT="http://www.ipdeny.com/ipblocks/data/countries"
cleanOldRules(){
    $IPT -L $CBLIST > /dev/null 2>&1
    if [ $? = 0 ] ; then
	$IPT -D INPUT ${IPTCBDEVICE:+-i }${IPTCBDEVICE} -j $CBLIST
	$IPT -D OUTPUT ${IPTCBDEVICE:+-o }${IPTCBDEVICE} -j $CBLIST
	$IPT -D FORWARD ${IPTCBDEVICE:+-i }${IPTCBDEVICE} -j $CBLIST
    fi
    $IPT -F $CBLIST
    $IPT -X $CBLIST
    for i  in `$IPT -L -n | grep Chain | cut -f 2 -d ' ' | grep '\-$CBLIST'`
    do
	$IPT -F ${i}
	$IPT -X ${i}
    done
}
updateZoneFiles() {
    ZONEARCH=${ZONEROOT}/arch
    mkdir -p ${ZONEARCH}
    find ${ZONEROOT} -maxdepth 1 -mindepth 1 -ctime +${MAXZONEAGE} -exec mv {} ${ZONEARCH} \;
    for c  in $ISO
    do
	# local zone file
	tDB=$ZONEROOT/$c.zone
	if [ -f $tDB ] ; then
	    printf "Zone file %s is new enough - no update required.\n" $tDB
	else
	    # get fresh zone file if it is newer than MAXZONEAGE days
	    $WGET -O $tDB $DLROOT/$c.zone
	fi
    done
    oldzones=`find ${ZONEROOT} -mindepth 1 -maxdepth 1 -type f -exec basename {} \; | cut -f 1 -d '.'`
    # Archive old zones no longer blocked
    for z in $oldzones ; do
	archme=${c}
	for c  in $ISO ; do
	    if [ $c = $z ] ; then archme="X"; fi
	done
	if [ $archme = $z ] ; then
	    mv ${archme} ${ZONEARCH}
	else
	    printf "Working from previous zone file for %s\n" ${z}
	fi
    done
}
createIPTLoadFile() {
    printf "# Generated by %s on" $0 > ${IPTCBRESTORE}
    printf "%s " `date` >> ${IPTCBRESTORE}
    printf "\n*filter\n" >> ${IPTCBRESTORE}
    # Create CBLIST chain
    printf ":$CBLIST - [0:0]\n" >> ${IPTCBRESTORE}
    printf "%s INPUT ${IPTCBDEVICE:+-i }${IPTCBDEVICE} -j $CBLIST\n" "-I" > ${IPTCBRESTORE}.tmp
    printf "%s OUTPUT ${IPTCBDEVICE:+-o }${IPTCBDEVICE} -j $CBLIST\n" "-I"  >> ${IPTCBRESTORE}.tmp
    printf "%s FORWARD ${IPTCBDEVICE:+-i }${IPTCBDEVICE} -j $CBLIST\n" "-I" >> ${IPTCBRESTORE}.tmp
    if [ "Z${ALLOWPORTS}" = "Z" ] ; then
	printf "Blocking all traffic from country - no ports allowed\n"
    else
	printf "%s $CBLIST -p tcp -m multiport --dports ${ALLOWPORTS} -j RETURN\n" "-I">> ${IPTCBRESTORE}.tmp
    fi
    if [ "Z${ALLOWSUBNET}" = "Z" ] ; then
	printf "Blocking all traffic from country - no subnets excluded\n"
    else
	printf "%s $CBLIST -s ${ALLOWSUBNET} -j RETURN\n" "-I">> ${IPTCBRESTORE}.tmp
    fi
    for c  in $ISO
    do
	# local zone file
	tDB=$ZONEROOT/$c.zone
	# country specific log message
	SPAMDROPMSG="iptables: ${c}-Country-Drop: "
        # Create drop chain for identified packets
	CBLISTDROP=${c}-${CBLIST}-DROP
	printf ":${CBLISTDROP} - [0:0]\n" >> ${IPTCBRESTORE}
	printf "%s ${CBLISTDROP} -j LOG --log-prefix \"$SPAMDROPMSG\"\n" "-A" >> ${IPTCBRESTORE}.tmp
	printf "%s ${CBLISTDROP} -j DROP\n" "-A" >> ${IPTCBRESTORE}.tmp
	# Load IP ranges into chains correlating to first octet
	BADIPS=$(egrep -v "^#|^$" $tDB)
	for ipblock in $BADIPS
	do
	    topip=`echo $ipblock | cut -f 1 -d '.'`
	    chainExists=`grep -c :${topip}-${CBLIST} ${IPTCBRESTORE}`
	    if [ $chainExists = 0 ] ; then
		printf "Creating chain for octet %s\n" ${topip}
		printf ":$topip-$CBLIST - [0:0]\n" >> ${IPTCBRESTORE}
		sip=${topip}.0.0.0/8
		printf "%s $CBLIST -s ${sip} -j $topip-$CBLIST\n" "-A" >> ${IPTCBRESTORE}.tmp
	    fi
	    printf "  Adding rule for %s to chain for octet %s\n" ${ipblock} ${topip}
	    printf "%s $topip-$CBLIST -s $ipblock -j ${CBLISTDROP}\n" "-A" >> ${IPTCBRESTORE}.tmp
	done
    done
    cat ${IPTCBRESTORE}.tmp >> ${IPTCBRESTORE} && rm -f ${IPTCBRESTORE}.tmp
    printf "COMMIT\n# Completed on " >> ${IPTCBRESTORE}
    printf "%s " `date` >> ${IPTCBRESTORE}
    printf "\n" >> ${IPTCBRESTORE}
}
directLoadTables() {
    # Create CBLIST chain
    $IPT -N $CBLIST
    $IPT -I INPUT ${IPTCBDEVICE:+-i }${IPTCBDEVICE} -j $CBLIST
    $IPT -I OUTPUT ${IPTCBDEVICE:+-o }${IPTCBDEVICE} -j $CBLIST
    $IPT -I FORWARD ${IPTCBDEVICE:+-i }${IPTCBDEVICE} -j $CBLIST
    if [ "Z${ALLOWPORTS}" = "Z" ] ; then
	printf "Blocking all traffic from country - no ports allowed\n"
    else
	$IPT -I $CBLIST -p tcp -m multiport --dports ${ALLOWPORTS} -j RETURN
    fi
    if [ "Z${ALLOWSUBNET}" = "Z" ] ; then
	printf "Blocking all traffic from country - no subnets allowed\n"
    else
	$IPT -I $CBLIST -s ${ALLOWSUBNET} -j RETURN
    fi
    for c  in $ISO
    do
	# local zone file
	tDB=$ZONEROOT/$c.zone
	# country specific log message
	SPAMDROPMSG="$c Country Drop"
        # Create drop chain for identified packets
	CBLISTDROP=${c}-${CBLIST}-DROP
	$IPT -N ${CBLISTDROP}
	$IPT -A ${CBLISTDROP} -j LOG --log-prefix "$SPAMDROPMSG"
	$IPT -A ${CBLISTDROP} -j DROP
	# Load IP ranges into chains correlating to first octet
	BADIPS=$(egrep -v "^#|^$" $tDB)
	for ipblock in $BADIPS
	do
	    topip=`echo $ipblock | cut -f 1 -d '.'`
	    $IPT -L $topip-$CBLIST > /dev/null 2>&1
	    if [ $? = 1 ] ; then
		printf "Creating chain for octet %s\n" ${topip}
		$IPT -N $topip-$CBLIST
		sip=${topip}.0.0.0/8
		$IPT -A $CBLIST -s ${sip} -j $topip-$CBLIST
	    fi
	    printf "  Adding rule for %s to chain for octet %s\n" ${ipblock} ${topip}
	    $IPT -A $topip-$CBLIST -s $ipblock -j ${CBLISTDROP}
	done
    done
}
loadTables() {
    createIPTLoadFile
    ${IPT}-restore -n ${IPTCBRESTORE}
    #directLoadTables
    printf "Country block instituted for: %s\n" "$ISO"
}
# create a dir
[ ! -d $ZONEROOT ] && /bin/mkdir -p $ZONEROOT
# clean old rules
cleanOldRules
# update zone files as needed
updateZoneFiles
# create a new iptables list
loadTables
exit 0

Reply

45 Mikael Engström March 6, 2010 at 6:25 pm

Would this script alter the existing ip-tables settings?

Reply

46 David Picard March 7, 2010 at 12:10 pm

The version I modified alters the existing iptables settings by introducing and maintaining additional chains. It will not remove any customized settings you have added to iptables if you ensure the variable CBLIST does not overlap the namespace for existing chains.

Reply

47 Nilesh April 9, 2010 at 2:20 pm

I have devised a similar method but with a slight variation. It combines the idea of vivek, david and myself.

Check it out here

Reply

48 David Picard April 9, 2010 at 2:45 pm

Hi Nilesh –

Your changes will work, of course, but the issues here are in kernel performance in processing the chain.

It’s best to consider the path through the table each packet might take and to reduce the number of rules that might be checked. In the case of your script, every declared subnet will be checked, while in the optimized version I posted earlier, we are reducing the number of rules checked at runtime for each incoming packet by anywhere from 80-98% due to the hierarchical nature of the constructed chains.

Reply

49 Nilesh April 9, 2010 at 4:18 pm

Yeah; best is to use a custom compiled kernel along with ipset. :)

Reply

50 David Picard April 9, 2010 at 5:47 pm

Custom compilation of the kernel is not required – the rules should be organized such that the minimum number of rules need be checked. Hierarchical structuring of the rule configuration is sufficient for these purposes. Neither is ipset required to provide optimal firewall performance.

Reply

51 M Agg April 20, 2010 at 3:12 am

Can you please post the script for ubuntu ? I tried this script but it gave error that /etc/sysconfig/iptables.cb not found.
This script will really be helpful, I am getting too much hacking attempts from china.

Reply

52 David Picard April 20, 2010 at 10:26 am

Hi M –

First – try running the script via sudo as this may be a permissions issue for access to the /etc/sysconfig directory.

You may need to change the ZONEROOT, IPTCBRESTORE, IPTCBDEVICE, and ALLOWSUBNET parameters at the top of the script posted on this thread to fit your system and network configuration. IPTCBRESTORE defines the filename where the generated rules will be saved and restored from and by default is set to “/etc/sysconfig/iptables.cb” – you might try changing the /etc/sysconfig part to a directory that exists in Ubuntu if Ubunutu does not have a /etc/sysconfig directory.

Reply

53 M Agg April 20, 2010 at 2:18 pm

Thanks for responding David. Sorry for a noob question but does iptables.cb file created by you or is it standard iptables config file in your linux distro. If it is a file created by you, then does the location of file matter ? i.e. can I just put in /etc/network (or any other folder).

Reply

54 David Picard April 20, 2010 at 2:50 pm

iptables.cb is created by this script and saved to the /etc/sysconfig directory to provide better visibility as this is normally where iptables rules are saved. You can save the rules anywhere in your filesystem – if Ubuntu normally exports iptables rules to a different location, I would suggest defining this file to be located there as well

Reply

55 M Agg April 21, 2010 at 2:42 am

Thanks, it worked. However, it looks like the ipdeny site is missing several big segments. I had several ips in my logs which as per this http://remote.12dt.com/lookup.php site belong to china. Can I suggest a further improvement ? How about having it take an additional custom file with ip blocks and block that too ? That would allow other scripts to add to this file and call your script to block certain ips.

Reply

56 M Agg April 21, 2010 at 2:50 am

Here are some IP’s

117.41.168.235, 119.147.116.157, 119.147.116.158

Reply

57 Chris April 23, 2010 at 11:12 am

Hi David,

I am having an error loading the script… I have flushed iptables tried and tried again but keep getting this error:

iptables-restore v1.3.0: error creating chain ‘mk-countrydrop-DROP':File exists

Error occurred at line: 251

any ideas?

Thanks,

Chris

Reply

58 David Picard April 23, 2010 at 11:53 am

Chris –

Looking at the script I can think of two reasons this might occur:
1. The table was not properly cleared and the drop rule already exists. If you flushed the table properly we can rule this out.
2. You have listed ‘mk’ twice as a blocked country.

If neither is the cause, I’m stumped ;-)

Reply

59 Chris April 23, 2010 at 4:10 pm

Ahhhhaaaa! Double entry! Got it! Works like a charm!

Thanks David! Kudos to you sir!

-Chris

Reply

60 Travis Mayne April 26, 2010 at 10:04 pm

Thank you so much to all the people who contributed to this article, especially Vivek and Dave!!!! You guys were a big help as I been having this problem with my home server and not familiar with configuring iptables. So thanks again…

Reply

61 David Picard April 29, 2010 at 2:27 pm

I had a problem the other day trying to download the firmware for my Samsung BD player – turns out the outbound request was going to Korea, and I had Korea categorically blocked.

So I added the following line to the script allowing responses to outbound requests from my network just before the first ‘if’ statement in the createIPLoadFile function and can now access Korean sites for my BD updates.

printf “%s $CBLIST -m state –state RELATED,ESTABLISHED -j ACCEPT\n” “-I”>> ${IPTCBRESTORE}.tmp

Reply

62 Mike August 19, 2010 at 11:54 am

Fantastic! Thanks Vivek and David P.

I’ve been using fail2ban for years and it works great. I’ve noticed, however, that when fail2ban blocks an IP from say China, often over the next hour or so, more from the same country will be blocked. It appears that the probes or attacks are coordinated. So, if they figure out my fail2ban thresholds, they could sustain an attack for quite some time. This script will be a perfect addition!

David, when I add the following line per your updated post (fixing the smartquotes when pasted of course)

printf “%s $CBLIST -m state –state RELATED,ESTABLISHED -j ACCEPT\n” “-I”>> ${IPTCBRESTORE}.tmp

I get the below error. Removing that line allows the script to function properly. Any ideas? Using Centos 4.8 32bit.

Adding rule for 223.255.0.0/17 to chain for octet 223
Bad argument `–state’
Error occurred at line: 54
Try `iptables-restore -h’ or ‘iptables-restore –help’ for more information.
Country block instituted for: cn

Thanks again! Most excellent!

Reply

63 David Picard August 19, 2010 at 3:02 pm

Mike – the point changes on each line can get confusing and line numbers can change. I’ve started a blog entry of my own where the updated script is maintained and can be copied and pasted directly. Please refer to http://psind.com/blog/2010/07/31/targeted-ip-blocking-align-web-services-to-your-target-markets/ for the full script and my thinking around the changes.

Reply

64 FK May 25, 2010 at 5:02 pm

Very very very nice effort. Can not thank you enough :)

Reply

65 Aaron July 6, 2010 at 6:03 pm

Cool script, I’m using it for a couple of countries that brute force my ssh server all day. Iptables -L struggles to list all of the rules that’s for sure and it made me curious.

In terms of network overhead, what costs more, denying all of the non US IP blocks or allowing ONLY US ip blocks?

Reply

66 David Picard July 6, 2010 at 9:44 pm

Performance depends on how you have the tables structured within iptables. I suspect that US IP blocks are distributed across more of the first octet which would cause more rules to be checked. I’m not sure that all the US networks are listed either, which would mean that well wishers and potential customers might be denied out of hand based on the missing US sub-nets if you inverted the rule to be allow instead of deny.

The script arranges the tables hierarchically – so we have an iptable defined for each octet that is blocked. This reduces the number of rules checked during runtime significantly – possibly by as much as 99% but more realistically somewhere around 75-85%.

Nilesh had mentioned using ipset a little further back in this thread. ipset may improve performance, but it also may not be part of your distribution and could require custom compilation and installation.

Under modest loads of 10-20 requests per second I would suspect that you are not likely to observe much of a difference from a CPU utilization perspective. If your traffic is higher than this you might consider using ipset or possibly getting a more dedicated solution.

Reply

67 Bremm July 6, 2010 at 11:59 pm

Aaron, instead of filtering a IP range on iptables, you might give a try on ‘fail2ban’ to protect services like ssh against bruteforce attacks.

http://www.fail2ban.org/wiki/index.php/Main_Page

Reply

68 David Picard July 31, 2010 at 10:47 pm

Bremm – good suggestion, I would suggest using both approaches as security measures are intended to delay a compromise – or to make the attempt so unappealing that the attacker gives up and searches for an easier target ;-)

Reply

69 tlhackque September 29, 2010 at 3:48 pm

I’ve done further work on this script, turning it into a script that can be run as a service at startup as well as a cron job. It also does some reporting. This version was developed under fedora core, but should easily port to other distributions. For full documentation, run with –help (note that the help text varies with the configuration.

Run chkconfig (or equiv) to get the service installed.

This is a poor mechanism for distributing large scripts – I have provided it to ipdeny.com as a .tar file; you may be able to pick it up from their tools area sometime soon.

Cron job:

51 3 * * Wed /etc/init.d/BlockCountries start -update

Sample /etc/syconfig/BlockCountries – modify as required

# Configuration for BlockCountries service
# Countries
# This lists both ISO code and name for documentation (and as insurance against changes in the name)
# However, either would do.
ru "Russian federation"
ir Iran
# Allow https and inbound mail, which requires DNS
#
-atport https -atport smtp -atport submission -atport smtps -atport domain
-auport domain
# Enable logging
-log

/etc/init.d/BlockCountries

#!/usr/bin/perl
#
# BlockCountries     Block IP traffic from specified countries
#
# chkconfig: 2345 10 92
# description:  Blocks IP traffic from IP addresses assigned to specific countries
#
use strict;
use warnings;
# Copyright (c) 2010 Timothe Litt, litt__at__acm_dot_org
#  All rights reserved.
#
# This software is licensed under the terms of the Perl
# Artistic License (see http://dev.perl.org/licenses/artistic.html).
#
# This is free software - it works for me, and it may (or may not)
# work for you.  No warranty or support is provided.
#
# Consider carefully whether you want to use this software
# and the full consequences to your site and/or business.
#
# This is written as a technical means to assist in implementing
# your policy.  The author expressly disclaims any responsibility
# for the consequences of using this software.
### Block all traffic from specified countries. ###
#
# See Usage() for documentation
#
# List of country codes - specify yours in the config file
my @DEFAULT_ISO = qw /cn kr kp kz ru/;
# Local configuration
my $IPT = '/sbin/iptables';
my $IPTR = '/sbin/iptables-restore';
my $GREP = '/bin/grep';
my $CFGFILE = '/etc/sysconfig/BlockCountries';
my $ZONEDIR = '/root/blockips';
my $ZONETBL = "$ZONEDIR/tables.ipt";
my $BLOCKURL = 'http://www.ipdeny.com/ipblocks/data/countries';
my $LOGPFX = '[Blocked CC]: ';
my $LOG = '/var/log/messages*';  # Note: This is a wildcard to handle log rotation.  .gz files will decompressed on the fly and processed.
my $LOGPGM = 'kernel';
# ### End of configuration
# The following are either part of base perl, or available on CPAN
use File::Basename;
use File::Path;
use IO::Uncompress::Gunzip;
use Locale::Country;
use LWP::Simple;
use NetAddr::IP;
use Net::Domain;
use Parse::Syslog;
use POSIX;
use Text::ParseWords;
umask 0137;
my $prog = basename $0;
# new, old
my @IPCHAINS = ( 'BLOCKCC0', 'BLOCKCC1' );
@IPCHAINS = reverse( @IPCHAINS ) if( system( "$IPT -n -L $IPCHAINS[0] >/dev/null 2>&1" ) == 0 );
my $IPNEWCHAIN = $IPCHAINS[0];
my $IPOLDCHAIN = $IPCHAINS[1];
if( -e $CFGFILE ) {
    open( my $fh, '<', $CFGFILE ) or die( "Can't open $CFGFILE:$!" );
    while(  ) {
        s/\s*#.*$//;
        s/^\s+//;
        s/\s+$//;
        next unless length;
        push @ARGV, parse_line( '\s+', 0, $_ );
    }
    close $fh;
}
my $cmd = shift;
# Collect all arguments here, even though they are mostly for start
# This allows detailed status
my( $debug, $verbose, @iso, %iso, $update, $log, $days, $host, @atports, @auports, @aips );
while( (my $arg = shift) ) {
    if( $arg =~ /^-/ ) {
        if( $arg eq '-update' ) {
            $update = 1;
            next;
        }
        if( $arg eq '-log' ) {
            $log = 1;
            next;
        }
        if( $arg eq '-nolog' ) {
            $log = 0;
            next;
        }
        if( $arg eq '-d' ) {
            $debug = 1;
            next;
        }
        if( $arg eq '-v' ) {
            $verbose = 1;
            next;
        }
        if( $arg eq '-days' && $ARGV[0] && $ARGV[0] =~ /^\d+$/ ){
            $days = shift;
            next;
        }
        if( $arg eq '-host' && $ARGV[0] ){
            $host = shift;
            next;
        }
        if( $arg eq '-atport' && $ARGV[0] ){
            $arg = shift;
            if( $arg =~  /^(?:\d+)$/ ) {
                push @atports, $arg;
                next;
            }
            my $val = getservbyname( $arg, 'tcp' );
            if( defined $val ) {
                push @atports, $val;
                next;
            }
            print "Invalid port $arg\n";
            exit 1;
        }
        if( $arg eq '-auport' && $ARGV[0] ){
            $arg = shift;
            if( $arg =~  /^(?:\d+)$/ ) {
                push @auports, $arg;
                next;
            }
            my $val = getservbyname( $arg, 'udp' );
            if( defined $val ) {
                push @auports, $val;
                next;
            }
            print "Invalid port $arg\n";
            exit 1;
        }
        if( $arg eq '-aip' && $ARGV[0] ) {
            if( $ARGV[0] =~ /^\d{1,3}(?:\.\d{1,3}){0,3}(?:\/(?:\d+|(?:\d{1,3}(?:\.\d{1,3}){3})))?$/ ){
                push @aips, NetAddr::IP->new( shift );
                next;
            }
            my @h = gethostbyname( $ARGV[0] );
            unless( @h) {
                print "Unknown host $ARGV[0]\n";
                exit 1;
            }
            unless( $h[3] == 4 && $#h >= 4 ) {
                print "$ARGV[0] : not an IPV4 address\n";
                exit 1;
            }
            for my $a (@h[4..$#h]) {
                push @aips, NetAddr::IP->new( sprintf( "%vd", $a ) );
            }
            shift;
            next;
        }
        if( $arg eq '-h' || $arg eq '--help' ) {
            Usage();
            exit 0;
        }
        print "Unknown switch $arg";
        print ' ', $ARGV[0] if( defined $ARGV[0] );
        print "\n";
        exit 1;
    }
    if( defined code2country( $arg ) ) {
        $iso{lc $arg} = 1;
    } else {
        my $cc = country2code( $arg );
        if( defined $cc ) {
            $iso{lc $cc} = 1;
        } else {
            print "Unrecognized country/country code: $arg\n";
            exit 1;
        }
    }
}
@iso = sort keys %iso;
@iso = @DEFAULT_ISO unless( @iso );
sub Usage {
    print << "HELP";
IP filter manager for country filters
Usage: $prog command args
  status [-v]        Display filter status
  list               List available country names/codes
                       Contacts server for list.
  intercepts [-host name] [-days n]
                     List today's intercepts by host (from $LOG)
  stop               Stop filtering
  restart  args      Synonym for start (reloads with no open window)
  condrestart args   Restarts only if already running
  start args         Starts filter
Start uses tables of IP blocks assigned to country codes that are
stored in $ZONEDIR, which will be created if necessary.  The data
is obtained from $BLOCKURL when needed,
or when start -update is specified.
iptables filters are generated and installed by start.  The filters are
optimized and generally will not look identical to the input data.  However
they will match the same address (no more and no fewer.)
Arguments for start-class commands are:
 -update            Get latest data for active country codes.
                    Otherwise, only gets data if no local file exists for a CC.
 -log               Install a logging rule to log rejected packets.
 -nolog             Don't install a logging rule (default)
 -atport n          Allow connections to TCP port n even from banned addresses.
                    May specify any number of times.  May use a service name.
 -auport n          Allow connections to UDP port n even from banned addresses.
                    May specify any number of times.  May use a service name.
 -aip ip(/mask)     Allow connection from an otherwise banned IP address.
                    For a block, specify a netlength or mask. A hostname may
                    also be specified.
 -d                 Output random debugging messages
 -v                 Output extended status/statistics
  CC                ISO Country code or name to ban (as many as you like)
                    Default list:
HELP
    for my $cc (sort @DEFAULT_ISO ) {
        print "                      $cc - ", code2country($cc), "\n";
    }
    print <source/dev/null`);
    # Delete each one
    #  First, delete the rule in the main chain that reads '-s 1st octet, goto subchain'
    #  Then empty and delete the subchain
    for my $schain (@schains) {
        $schain =~ m/-(\d+)$/;
        system( "$IPT -D $chain -s $1.0.0.0/8 -g $schain" );
        system( "$IPT -F $schain" );
        system( "$IPT -X $schain" );
    }
}
sub delchainref {
    my $main = shift;  # e.g. INPUT
    my $chain = shift; # e.g. BLOCKchain
   my @crefs = map { (m/^$chain\s/ ? ( $chain, ) : ()) } split( /\n/, `$IPT -n -L $main 2>/dev/null`);
    for my $cref (@crefs) { # should be only one
        system( "$IPT -D $main -j $cref" );
    }
}
# Sort function for IP addresses for installation into filter chains
# Th whole chain must be processed if we miss, so there's nothing we can do.
# But on a hit, we can improve the expected time somewhat by checking the
# largest blocks first.  This corresponds to the smallest mask length.
# It is possible to do better if the traffic pattern is known, but there
# isn't a good way (short of active feedback) to determine it.
# In any case, we reduce the search length by hashing on the first
# octet of the address, so this is a secondary effect.
sub ipcmp {
    my $x = $a->masklen  $b->masklen;
    return $x if( $x );
    return $a  $b;
}
sub start {
    print "Starting blocked countries IP filter: " unless( $ENV{CRONJOB} );
    File::Path::make_path( $ZONEDIR, { mode => 0771 } ) unless( -d $ZONEDIR );
    # Delete any lingering references / parts of new chain
    delchainref( 'INPUT', $IPNEWCHAIN );
    # Perhaps someday if I want to spend core on -d rules
#    delchainref( 'OUTPUT', $IPNEWCHAIN );
#    delchainref( 'FORWARD', $IPNEWCHAIN );
    delsubchains( $IPNEWCHAIN );
    system( "$IPT -F $IPNEWCHAIN >/dev/null 2>&1" );
    system( "$IPT -X $IPNEWCHAIN >/dev/null 2>&1" );
    # (Log & ) drop chain
    system( "$IPT -F $IPNEWCHAIN-DLOG >/dev/null 2>&1" );
    system( "$IPT -X $IPNEWCHAIN-DLOG >/dev/null 2>&1" );
    return failure unless( system( "$IPT -N $IPNEWCHAIN-DLOG" ) == 0 );
    if( $log ) {
        # Note that we can not provide a per-country log prefix due to compaction.
        # However, the intercepts report will map IPs back to their (alleged) country of origin
        # To determine what countries are causing intercepts, the logs must be post-processed to
        # lookup each IP.
        return failure unless( system( "$IPT -A $IPNEWCHAIN-DLOG -j LOG --log-prefix \"$LOGPFX\"" ) == 0 );
    }
    return failure unless( system( "$IPT -A $IPNEWCHAIN-DLOG -j DROP" ) == 0 );
    return failure unless( system( "$IPT -N $IPNEWCHAIN" ) == 0 );
    return failure unless( system( "$IPT -A $IPNEWCHAIN -m state --state RELATED,ESTABLISHED -j RETURN" ) == 0 );
    # List any allowed ports - first since they have a netmask of 0
    # Allowed TCP ports - no more than 15 per rule (limit of multiport)
    my $exceptions = @aips + @auports + @atports;
    my $xrules = 0;
    while( @atports ) {
        my $n = @atports;
        $n = 15 if( $n > 15 );
        return failure unless( system( "$IPT -A $IPNEWCHAIN -p tcp -m multiport --dports " . join( ',', @atports[0..$n-1] ) . ' -j RETURN' ) == 0 );
        splice( @atports, 0, $n );
        $xrules++;
    }
    # Allowed UDP ports
    while( @auports ) {
        my $n = @auports;
        $n = 15 if( $n > 15 );
        return failure unless( system( "$IPT -A $IPNEWCHAIN -p udp -m multiport --dports " . join( ',', @auports[0..$n-1] ) . ' -j RETURN' ) == 0 );
        splice( @auports, 0, $n );
        $xrules++;
    }
    # Allowed IPs (with optional masklen/netmask); largest size first
    #
    # Local subnets - external firewalls prevent them from showing up here, but
    # a bogus zone file could do damage.
    unshift @aips, NetAddr::IP->new( '192.168.0.0/16' ), NetAddr::IP->new( '172.16.0.0/12' ), NetAddr::IP->new( '10.0.0.0/8' );
    $exceptions += 3;
    @aips = sort ipcmp NetAddr::IP::Compact( @aips );
    $xrules += @aips;
    while( @aips ) {
        return failure unless( system( "$IPT -A $IPNEWCHAIN -s " . shift( @aips ) . ' -j RETURN' ) == 0 );
    }
    if( $verbose ) {
        # Exception statistics
        print( "\n",
               "$exceptions exceptions generated $xrules rules." );
    }
    # Make sure we have a zone file for each country code
    # Fetch a new one if -update or we don't have one
    # If we fetch, only transfer the file if it's different from (usu. newer than) our copy.
    my @files;
    for my $c (@iso) {
        my $db = "$ZONEDIR/$c.zone";
        my $cn = code2country( $c );
        $cn = " ($cn)" if( defined $cn );
        # Fetch if updating or have no data
        if( $update || ! -f $db || -z $db ) {
            my $rc = mirror( "$BLOCKURL/$c.zone", $db );
            if( is_success( $rc ) ) {
                print "\nUpdated IP zone data for $c$cn", if( $debug || $update || $ENV{CRONJOB} );
                # Shouldn't ever get an empty file, but may as well check
                unless( -f $db && -s $db ) {
                    print "\nUpdated zone data for $c$cn is empty!";
                    unlink $db;
                    return failure;
                }
            } else {
                if( $rc == RC_NOT_MODIFIED ) { # Can only happen if file exists
                    print "\nNo new IP data available for $c$cn " if( $debug || ($update && !$ENV{CRONJOB}) );
                } else {
                    print "\nUnable to fetch IP zone data for $c$cn: $rc - ", status_message($rc), " ";
                }
                unless( -f $db && -s $db ) {
                    # No data - don't replace current filter
                    print "\nNo IP zone data available for $c$cn ";
                    if( $debug ) {
                        next;
                    }
                    return failure;
                }
                # Failed, but have old file, continue since other zones may be updated
            }
        }
        push @files, $db;
    }
    return failure unless( @files );
    # Parse the zone files and create a list of IP blocks
    my @addresses = ();
    for my $if (@files) {
        open( my $ifh, '<', $if ) or die( "Can't open $if: $!" );
        while(  ) {
            s/\s*#.*$//;
            next unless( length );
            push @addresses, NetAddr::IP->new( $_ );
        }
        close $ifh;
    }
    return failure unless( @addresses );
    # Compact the blocks into the minimal covering set
    my $inaddrs = @addresses;
    @addresses = sort ipcmp NetAddr::IP::Compact(@addresses);
    # Generate an iptables-restore file with the new rules
    my %subchains;
    open( my $fh, '>', $ZONETBL ) or die( "Can't open $ZONETBL: $!" );
    print $fh "# Generated by $prog on ", (scalar localtime), "\n",
              "*filter\n",           # table
              ":INPUT ACCEPT [0:0]\n"; # built-in chain, policy, counters
    foreach my $ipblock (@addresses) {
        $ipblock =~ /^(\d+)\./;
        unless( $subchains{$1} ) {
            print $fh ":$IPNEWCHAIN-$1 - [0:0]\n",    # subchain, no policy, zero counters
              "-A $IPNEWCHAIN -s $1.0.0.0/8 -g $IPNEWCHAIN-$1\n";  # Chain - branch on 1st octet to subchain
        }
        $subchains{$1}++;
        print $fh "-A $IPNEWCHAIN-$1 -s $ipblock -j $IPNEWCHAIN-DLOG\n"; # Subchain, branch on match to log & drop
    }
    print $fh "COMMIT\n",
              "# Completed on ", (scalar localtime), "\n";
    close $fh;
    if( $verbose ) {
        # Provide some statistics, mostly for debugging.
        my( $minlen, $maxlen );
        $minlen = $maxlen = $subchains{(keys %subchains)[0]};
        for my $s (values %subchains) {
            $minlen = $s if( $s  $maxlen );
        }
        print( "\n",
               "$inaddrs blocked address ranges generated ",
               (scalar @addresses), " rules, using ",
               (scalar keys %subchains), " sub-chains.  Savings: ",
               ($inaddrs - scalar @addresses), " rules (",
               sprintf( "%.2f", 100*(1- ((scalar @addresses))/$inaddrs)), "%).  Minimum chain length: $minlen",
               ", Maximum: $maxlen\n" );
    }
    # Install new ruleset
    # -- Mass-install new Chain, subchains & rules
    return failure unless( system( "$IPTR -n $ZONETBL" ) == 0 );
    # -- Link INPUT to the new chain
    return failure unless( system( "$IPT -I INPUT -j $IPNEWCHAIN" ) == 0 );
#    return failure unless( system( "$IPT -I OUTPUT -j $IPNEWCHAIN" ) == 0 );
#    return failure unless( system( "$IPT -I FORWARD -j $IPNEWCHAIN" ) == 0 );
    # Remove old rules
    delchainref( 'INPUT', $IPOLDCHAIN );
#    delchainref( 'OUTPUT', $IPOLDCHAIN );
#    delchainref( 'FORWARD', $IPOLDCHAIN );
    delsubchains( $IPOLDCHAIN );
    system( "$IPT -F $IPOLDCHAIN >/dev/null 2>&1" );
    system( "$IPT -X $IPOLDCHAIN >/dev/null 2>&1" );
    system( "$IPT -F $IPOLDCHAIN-DLOG >/dev/null 2>&1" );
    system( "$IPT -X $IPOLDCHAIN-DLOG >/dev/null 2>&1" );
    $IPOLDCHAIN = $IPNEWCHAIN;
    return success if( running );
    return failure;
}
sub stop {
    return 1 if( !running );
    print "Removing blocked countries IP filter";
    delchainref( 'INPUT', $IPOLDCHAIN );
    delsubchains( $IPOLDCHAIN );
    system( "$IPT -F $IPOLDCHAIN" );
    system( "$IPT -X $IPOLDCHAIN" );
    system( "$IPT -F $IPOLDCHAIN-DLOG" );
    system( "$IPT -X $IPOLDCHAIN-DLOG" );
    if( !running ) {
        success;
        return 1;
    }
    failure;
    return 0;
}
sub restart {
    # Don't stop since start will keep the current table alive until
    # the new one is active.
    return start();
}
# List intercepted IPs for today
# This can be run in a cron job just before midnight to get a list of
# IPs to report.  Or, you can use -days n to get the last n days worth
# of intercepts
# Only works if logging is on
sub intercepts {
    my( $fh, %ips );
    $days ||= 1;
    my $start = time() - ( $days * 24*60*60 );
    $host ||= Net::Domain::hostname();
    foreach my $logfile (glob $LOG) {
        my $lh = IO::Uncompress::Gunzip->new( $logfile, MultiStream => 1, Transparent => 1 );
        unless( $lh ) {
            print "Skipping system log file: $IO::Uncompress::Gunzip::GunzipError\n";
            next;
        }
        my $sl = Parse::Syslog->new( $lh, arrayref => 1 );
        # Record # intercepts for each ip => protocol => port
        while( my $l = $sl->next ) {
            next if( $l->[0] [2] eq $LOGPGM && $l->[1] =~ /$host/i;
            if( $l->[4] =~ /^\Q$LOGPFX\E.*?\bSRC=([0-9.]+).*?\bPROTO=(ICMP)\b.*?\bTYPE=(\d+)/ ) {
                $ips{$1}{lc $2}{$3}++;
            } elsif( $l->[4] =~ /^\Q$LOGPFX\E.*?\bSRC=([0-9.]+).*?\bPROTO=(\w+).*?\bDPT=(\d+)/ ) {
                $ips{$1}{lc $2}{$3}++;
            }
        }
        close $lh;
    }
    return 0 unless %ips;
    # List each intercepted IP, its country, the protocols, ports and number of packets for each
    print "Intercepts by host IP:\n";
    my( %ccip, %ccn );
    foreach (glob "$ZONEDIR/*.zone") {
        /$ZONEDIR\/(.*).zone/;
        my $cc = $1;
        next unless( defined code2country( $cc ) ); # Skip undocumented zone files
        open( my $ifh, '<', $_ ) or next;
        while(  ) {
            s/\s*#.*$//;
            next unless( length );
            push @{$ccip{$cc}}, NetAddr::IP->new( $_ );
        }
        close $ifh;
    }
    for my $ip (sort map {NetAddr::IP->new($_)} keys %ips) {
        my $ccn;
      CCSEARCH:
        foreach my $cc (keys %ccip) {
            foreach my $cip (@{$ccip{$cc}}) {
                if( $cip->contains($ip) ) {
                    print "$cc: ";
                    $ccn = $cc;
                    last CCSEARCH;
                }
            }
        }
        unless( $ccn ) {  # Possible if we've stopped blocking a country but have old log entries
            $ccn = '??';
            print '??: ';
        }
        print $ip->addr;
        my @plist = sort keys %{$ips{$ip->addr}}; # Protocol
        for my $p (@plist) {
            my @rlist = sort keys %{$ips{$ip->addr}{$p}}; # Ports
            print ' ', join( ' ', map { my $n = $ips{$ip->addr}{$p}{$_}; $ccn{$ccn} += $n; "$p-$_($n)" } @rlist );
        }
        print "\n";
    }
    print "Intercepts by country:\n";
    for my $cc (sort {$ccn{$b}  $ccn{$a} } keys %ccn) {
        my $cn = code2country($cc);
        if( defined $cn ) {
            $cn = "$cc ($cn)";
        } else {
            $cn = $cc;
        }
        printf "%10u %s\n", $ccn{$cc}, $cn;
    }
    return 1;
}
if( $cmd eq 'start' ) {
    exit !start();
}
if( $cmd eq 'stop' ) {
    exit !stop();
}
if( $cmd eq 'restart' ) {
    exit !restart();
}
if( $cmd eq 'condrestart' ) {
    exit !(running && restart());
}
if( $cmd eq 'status' ) {
    exit !status();
}
if( $cmd eq 'list' ) {
    exit !list();
}
if( $cmd eq 'intercepts' ) {
    exit !intercepts();
}
if( $cmd eq 'help' ) {
    Usage();
    exit;
}
print "Usage: $prog (start|stop|restart|condrestart|status|list|intercepts|help)\n";
exit 1;

Reply

70 nixCraft September 29, 2010 at 5:51 pm

Thanks for sharing the script with us.

Reply

71 an0nymous October 2, 2010 at 5:22 pm

Bugger the morals, I’m just sick of all the spam. That’s why I ban the entire Russian Federation IP space.

Reply

72 Josh December 2, 2010 at 1:43 pm

That’s why I block US entirely too.

Reply

73 Sushant Chawla November 1, 2010 at 7:40 am

Hi Vivek

I have ran this script on one of my VPS server but its not running successfully. Can you list the modules required to run this iptables script successfully?

Regds
Sushant Chawla
Sr. Systems Engineer (Linux)

Reply

74 tlhackque November 1, 2010 at 9:06 am

If you’re talking about the perl version, the list of modules is at the top of the script:

# The following are either part of base perl, or available on CPAN

use File::Basename;
use File::Path;
use IO::Uncompress::Gunzip;
use Locale::Country;
use LWP::Simple;
use NetAddr::IP;
use Net::Domain;
use Parse::Syslog;
use POSIX;
use Text::ParseWords;

This has been tested on perl 5.8.8 and 5.10.1 under fedora. CPAN is http://search.cpan.org, normally accessed via the cpan command.

Depending on your distribution, you may need to adjust some of the paths listed in the “local configuration” section.

Don’t forget to install the config file – especially if you setup to run at startup.

For SYSV-init under fedora, put the script under /etc/init.d, and run chkconfig (or system-config-services) to get it run at startup/shutdown. And install the cron job as noted above.

Check your permissions – and if you are running under selinux, look for audit errors.

Note that you can run the script from a terminal if you like.

iptables -nvL should show the filter chains – the output is typically large, so pipe it thru less or send it to a file.

For more specific assistance, you’ll have to provide the specific error(s) that you are experiencing and details of your configuration. “Not running successfully” is not much to go on.

Reply

75 David Picard November 6, 2010 at 1:04 pm

The scripted version runs on most standard linux installs – the reason why I espouse scripting in init files. It needs wget, egrep, and iptables, which can be installed through your distribution specific software installer. I’ll post an init compatible version of the script version to this thread within a few weeks.

Reply

76 NachoR November 6, 2010 at 12:24 am

Hello, I wan to block all and permit just Argenitna.
How can i change de script to bloclk all and permit one or two countrys?
Thanks!

Reply

77 tlhackque November 6, 2010 at 11:44 am

>>How can i change de script to bloclk all and permit one or two countrys?

Getting that right would be a non-trivial change, since the syntax/architecture is designed around blocking. You would still want to allow some ports/ips (notably your internal addresses), but would need more syntax to over-ride (block) the “allowed” IPs when you find errors in the database. You need to consider the logging/reporting functions.

It could be done, but it’s not as simple as it appears.

Computes are cheap. You can do this instead:

Put your configuration in /etc/sysconfig/BlockCountries.template, but omit the country codes.

In your cron job, run a script something like this:

# Untested -
cp /etc/sysconfig/BlockCountries.template /etc/sysconfig/BlockCountries.new
#
# Allow Argentina and Spain
# Note that there are significant spaces in all of the regexps
/etc/init.d/BlockCountries list | sed -e'/^[^ ]/d' -e's/ - .*/ /' | grep -vP ' (ar|es) ' >>/etc/sysconfig/BlockCountries.new
# Make sure we got a new list - if server fails, do not run with an empty blocking list.
if ! diff -q /etc/sysconfig/BlockCountries.template /etc/sysconfig/BlockCountries.new
; then
    echo "Failed to generate new blocked countries list, using previous configuration"
else
   mv /etc/sysconfig/BlockCountries.new /etc/sysconfig/BlockCountries
fi
/etc/init.d/BlockCountries start -update

This automatically generates a configuration file that will block all countires that IPDNEY knows about, except the ones in the regexp. (If there’s a country that IPDENY doesn’t know about, it will not be blocked, which may or may not matter to you.) It is not very efficient for the computer, but it is for you. The generated filter should be reasonably efficient because many of the IP ranges will be coallesced.

You should run the cron script by hand before running BlockCountries for the first time.

If you want a solution that directly implements your request, you’ll need to re-engineer and maintain the script. And share it with everyone else…

I’d try the simple approach before undertaking a re-engineering effort.

Reply

78 David Picard November 6, 2010 at 1:00 pm

Changing the script to block all traffic by default and allow a country or two through is not so difficult. Change the default input rule on the internet interface to deny and change the ‘drop’ actions to ‘return’. The problem with this approach is that any ipranges for the allowed country that are missing in the ipdeny database will not be allowed either (I mention this in an earlier discussion around this point).

tlhackque’s approach above addresses the missing iprange problem, but incurs an enormous processing penalty on allowed packets. Unless you have a fairly beefy firewall machine dedicated only to filtering, you should not follow that approach.

Reply

79 tlhackque November 6, 2010 at 2:10 pm

I won’t get into a religous war – scripts are fine. I write lots of them.

But you are almost certainly over-stating the drawbacks of my note.

1) Perl is univerally available – about as available as bash. Yes, you need to fetch modules – which are just as available as commands.

2) Perl does processing that you can’t do in a script,. In this case, IP subnet colsolidation (merging).

3) As I noted, the subnet merging will reduce the number of rules actually generated – the huge number of ranges will help because of more chances to merge blocks. The “enormous processing penalty” and “beefy machine” requirements are likely less than you think. But as I noted, it’s worth running the experiment. If measured data shows that for your environment, your time is worth less than the cost of computes, by all means invest in doing the engineering. Inverting the sense of the rules would be more machine-efficient. But do the whole job. And that mean adding a mode, valdidating it, and handling the corner cases.

Note that changing the INPUT policy to deny and swapping drop for return would only work if these are the ONLY firewall rules on the INPUT chain. That’s certainly not the case on any machine that I run. Country blocking is merely one (rather heavy-handed) filter in a series of rules. So if you go down that path, you have to consider what may come after the country blocking.

4) The engineering is not rocket science. I have no need for “permit except” functionality – and doing it right is non-trivial – so I’m not implementing it. I hope that anyone who does undertakes the whole job – and that it’s actually worthwhile based on measured data.

Have fun!

Reply

80 David Picard November 6, 2010 at 4:52 pm

Didn’t see mention of the merge in the release notes when the perl version was posted – nice touch and certainly a good argument for going with the Perl version vs the shell version if you’re blocking a lot of countries.

I don’t follow point 3 – if this is about cost accounting on development effort to make the changes to the posted Perl script, then it’s certainly worthwhile to invest the effort if you’re risking loss of access under high traffic conditions. These types of tools introduce business risk that should be considered when deploying them as mentioned earlier in these threads.

At this point I think that comment 4 highlights the need for people to post back comments on how system performance is impacted at various traffic levels for different country blocks in the configuration so we can have some hard data on where the problems might exist with any of the versions posted in this thread – so non-speculative comments with concrete measurements on performance are certainly welcome.

Reply

81 tlhackque November 6, 2010 at 7:51 pm

David,

See the help text for documentation:
“This version of the script merges all the IP address blocks; this saves over 1,000
rules for the default banned address list. It’s also somewhat faster than a shell
script, and contains a more complete and polished user and system interface.”

It turns out that even if you’re only blocking a single country, the distributed rulesets benefit from compaction. For example, my current ru.zone file shrinks ~12%; cn.zone shrinks by~45%. I provided the IPDENY folks with a simple perl script that does a better job (and statistics) – but they haven’t gotten around to implementing it.

You could incorporate the compression script into your shell script – if you do, note that you do NOT want to compress one file at a time; you want to merge adjacent blocks from different countries. But I think that the perl script is a more complete solution.

compressiplist
pipe data to stdin, or list files on command line. output to stdout
if you use -v, you’ll get statistics on stderr

#!/usr/bin/perl
use strict;
use warnings;
use NetAddr::IP;
my $stats;
if( $ARGV[0] eq '-v' ) {
        shift;
        $stats = 1;
}
my @addresses = ();
while(  ) {
    chomp;
    s/\s*#.*$//;
    next if( !length );
    push @addresses, NetAddr::IP->new( $_ );
}
exit unless( @addresses );
my $inn = (scalar @addresses);
print STDERR ( $inn, " addresses input, " ) if( $stats );
@addresses = NetAddr::IP::Compact(@addresses);
if( $stats ) {
        print STDERR ( (scalar @addresses), " addresses output" );
        printf STDERR (" Savings %f.2%%\n", 100*(1-(@addresses/$inn)));
}
print join( "\n", @addresses ), "\n";
exit;

I didn’t do a feature-by-feature comparison. But here’s a swag (it’s been a while, so this may not be complete):

I do everything that your script does, except I don’t archive old zone files. If I ever need an old config, I have backups. I also don’t deail with the FORWARD or OUTPUT chains; I don’t think your code works because it’s looking at the source address and would need a second ruleset that looks at the destinations. I didn’t need these, so I didn’t fix these, although there are hooks.

I also:
o Never remove the rules when updating; start builds a parallel rule set, installs it & then removes the old set. This eliminates a window that you have during update.
o Never block any RFC1918 subnets – routers should never let these in from the internet, and this prevents a bad configuration or zone file from locking you out.
o Don’t output debugging messages unless enabled – prevents cron from sending mail
o zone files are uploaded with a mirror function – this ensures that files are downloaded intact (you don’t check wget status)
o If you enable logging, stats are available on what has been blocked – see “intercepts”. Note that this works out per-country statistics despite the address compaction.
o You can block by country name as well as ISO code
o Most configuration is in a separate file
o Host and protocol names are accepted as well as numerics. Hostnames with multiple addresses are handled.
o allowed ports can exceed 15, and both TCP and UDP are supported
o script can be directly used as a SYSV init script – including success/failure that’s console sensitive (matches the redhat style)
o verbose status (status -v) will provide configuration summary
o list will provide the available country list
o help will attempt to be helpful.
o Code is more careful about checking for and handling errors.

The compaction happens in several places, but see the comment:
# Compact the blocks into the minimal covering set

You’ll also notice that the rules are sorted to drop the largest blocks first.

If you add -v to start, you’ll get some statistics.

I’m currently blocking 13 countries, and get this:

9 exceptions generated 5 rules.
9373 blocked address ranges generated 7184 rules, using 96 sub-chains. Savings: 2189 rules (23.35%). Minimum chain length: 1, Maximum: 976

That maximum chain length for my configuration has grown – perhaps to the point where another level of chain forking is in order. But note that the more countries one blocks, the more likely merges will happen. So until someone runs the experiment, we won’t know what happens with the inverted (“everything but”) block list. (It’s too bad that iptables/netfilter doesn’t provide a hash mechanism…)

As noted in the credits, a number of the ideas in my implementation came from your work. I’m glad that what you created met your needs and that it was shared to provide a basis for my re-interpretation.

I think that if you try the perl version, you’ll find that the address consolidation will reduce the number of rules that you have to deal with. For that reason, aside from the inverted rule discussion, it should perform better. But as always, real data would be interesting.

I don’t run a high-traffic site, so my analysis has been static.

The perl version works for me. If it helps others, or if others find it worthwhile to evolve it further, that’s great. If not, the shell script still works.

Reply

82 tlhackque November 6, 2010 at 7:55 pm

Er, typo in posting the compression script

printf STDERR (" Savings %f.2%%\n", 100*(1-(@addresses/

should be

printf STDERR (" Savings %.2f%%\n", 100*(1-(@addresses/

This effects display, not the statistics I quoted.

Reply

83 nachoR November 6, 2010 at 9:52 pm

Guys, I think you are wonderful, honest.

We´ve been trying all day with David´s bash script (great, man !). We do have a lot of traffic on our servers, and I confess we are being attacked by a ZeuS-like net-bot army. Not much sleeping here, and I still trying to block all countries (but Argentina) with the script never getting to finish adding all countries iptables rules. Later I would allow a few specific IPs from different countries. As I cannot figured to completely finish the script out, and my Perl knowledge is zero, I came back to you folks. Any chance to any of you to consolidate IP ranges for my need with the perl script (all countries except ar, and subnet 192.168.1.0 of course? No doubt will share all performance, stats, problems to anyone interested. Again thank you very much.

Reply

84 tlhackque November 7, 2010 at 2:49 pm

I hate to see anyone suffering a DDOS attack.

Here is a new version of the perl script. It has received only very limited testing, but I hope it will help your defense.

You don’t need programming knowledge of Perl to install/use this script, but you do need to know how to install CPAN modules. Some distributions provide many of the modules under yum, apt-get, etc. If yours doesn’t – or doesn’t have one that this needs, use cpan. There should be a cpan command installed with Perl. Just say cpan to the shell, then install modulename (like IO::Uncompress::Gunzip) and it should just work. ‘quit’ exits cpan. To find which modules are missing, run the script (e.g. /etc/init.d/BlockCountries help). If you get an error like “Can’t locate IO/Uncompress/Gunzip.pm in @INC….BEGIN failed–compilation aborted”, you need “IO::Uncompress::Gunzip” – you changed ‘/’ to “::” and drop the “.pm” to get the module name. Repeat for each missing module until you see the help display.

Don’t blindly install all the modules in the list, because if your distribution mechanism supplied a module, you want the distribution to handle updates.

See my post #65 for additional instructions and the sample configuration file.

Good luck!

Changes:

-permitonly inverts the country list – that is, listed countries are permitted, all others are denied.

-limit will limit the logging rate (see help for details) There is a default; use
-nolimit to get the old behavior

-dip will allow you to deny specific hosts/networks

The blocking chain will be inserted into the INPUT-HOOK chain if it exists, otherwise INPUT. (This allows more flexibility in when country blocking happens, particularly when multiple iptables-based tools all think they want to be ‘first’.)

#!/usr/bin/perl
#
# BlockCountries     Block IP traffic from specified countries
#
# chkconfig: 2345 10 92
# description:  Blocks IP traffic from IP addresses assigned to specific countries
#
use strict;
use warnings;
# Version 1.2
#
# Copyright (c) 2010 Timothe Litt, litt__at__acm_dot_org
#  All rights reserved.
#
# This software is licensed under the terms of the Perl
# Artistic License (see http://dev.perl.org/licenses/artistic.html).
#
# This is free software - it works for me, and it may (or may not)
# work for you.  No warranty or support is provided.
#
# Consider carefully whether you want to use this software
# and the full consequences to your site and/or business.
#
# This is written as a technical means to assist in implementing
# your policy.  The author expressly disclaims any responsibility
# for the consequences of using this software.
### Block all traffic from specified countries. ###
#
# See Usage() for documentation
#
# List of country codes - specify yours in the config file
my @DEFAULT_ISO = qw /cn kr kp kz ru/;
# Local configuration
my $IPT = '/sbin/iptables';
my $IPTR = '/sbin/iptables-restore';
my $GREP = '/bin/grep';
my $CFGFILE = '/etc/sysconfig/BlockCountries';
my $ZONEDIR = '/root/blockips';
my $ZONETBL = "$ZONEDIR/tables.ipt";
my $BLOCKURL = 'http://www.ipdeny.com/ipblocks/data/countries';
my $LOGPFX = '[Blocked CC]: ';
my $LOG = '/var/log/messages*';  # Note: This is a wildcard to handle log rotation.  .gz files will decompressed on the fly and processed.
my $LOGPGM = 'kernel';
my $IHOOK = 'INPUT-HOOK';       # Note: if this table is not found, INPUT will be used
#my $OHOOK = 'OUTPUT-HOOK';     # Note: if this table is not found, OUTPUT will be used
#my $FHOOK = 'FORWARD-HOOK';    # Note: if this table is not found, FORWARD will be used
# ### End of configuration
# The following are either part of base perl, or available on CPAN
use File::Basename;
use File::Path;
use IO::Uncompress::Gunzip;
use Locale::Country;
use LWP::Simple;
use NetAddr::IP;
use Net::Domain;
use Parse::Syslog;
use POSIX;
use Text::ParseWords;
# Changelog
# 1.0       Initial development
# 1.1       Add support for hook tables
# 1.2       By unpopular demand, add -permitonly
#           Add logging rate limit
#           Add -dip (deny IP)
umask 0137;
my $prog = basename $0;
# new, old
my @IPCHAINS = ( 'BLOCKCC0', 'BLOCKCC1' );
@IPCHAINS = reverse( @IPCHAINS ) if( system( "$IPT -n -L $IPCHAINS[0] >/dev/null 2>&1" ) == 0 );
my $IPNEWCHAIN = $IPCHAINS[0];
my $IPOLDCHAIN = $IPCHAINS[1];
$IHOOK = 'INPUT' unless( system( "$IPT -n -L $IHOOK >/dev/null 2>&1" ) == 0 );
#$OHOOK = 'OUTPUT' unless( system( "$IPT -n -L $OHOOK >/dev/null 2>&1" ) == 0 );
#$FHOOK = 'FORWARD' unless( system( "$IPT -n -L $FHOOK >/dev/null 2>&1" ) == 0 );
if( -e $CFGFILE ) {
    open( my $fh, '<', $CFGFILE ) or die( "Can't open $CFGFILE:$!" );
    while(  ) {
        s/\s*#.*$//;
        s/^\s+//;
        s/\s+$//;
        next unless length;
        push @ARGV, parse_line( '\s+', 0, $_ );
    }
    close $fh;
}
my $cmd = shift;
# Collect all arguments here, even though they are mostly for start
# This allows detailed status
my( $debug, $verbose, @iso, %iso, $update, $log, $days, $host, $permitonly, @loglimits, @atports, @auports, @aips, @dips );
@loglimits = ( '1/minute', 10 );
while( (my $arg = shift) ) {
    if( $arg =~ /^-/ ) {
        if( $arg eq '-update' ) {
            $update = 1;
            next;
        }
        if( $arg eq '-log' ) {
            $log = 1;
            next;
        }
        if( $arg eq '-limit' && $ARGV[0] ) {
            unless( $ARGV[0] =~ m#(\d+/(?:second|minute|hour|day))(?::(\d+))?# ) {
                print "Syntax error in -limit\n";
                exit 1;
            }
            shift;
            $loglimits[0] = $1;
            $loglimits[1] = $2 if( defined $2 );
            next;
        }
        if( $arg eq '-nolimit' ) {
            @loglimits = ();
            next;
        }
        if( $arg eq '-nolog' ) {
            $log = 0;
            next;
        }
        if( $arg eq '-permitonly' ) {
            $permitonly = 1;
            next;
        }
        if( $arg eq '-d' ) {
            $debug = 1;
            next;
        }
        if( $arg eq '-v' ) {
            $verbose = 1;
            next;
        }
        if( $arg eq '-days' && $ARGV[0] && $ARGV[0] =~ /^\d+$/ ){
            $days = shift;
            next;
        }
        if( $arg eq '-host' && $ARGV[0] ){
            $host = shift;
            next;
        }
        if( $arg eq '-atport' && $ARGV[0] ){
            $arg = shift;
            if( $arg =~  /^(?:\d+)$/ ) {
                push @atports, $arg;
                next;
            }
            my $val = getservbyname( $arg, 'tcp' );
            if( defined $val ) {
                push @atports, $val;
                next;
            }
            print "Invalid port $arg\n";
            exit 1;
        }
        if( $arg eq '-auport' && $ARGV[0] ){
            $arg = shift;
            if( $arg =~  /^(?:\d+)$/ ) {
                push @auports, $arg;
                next;
            }
            my $val = getservbyname( $arg, 'udp' );
            if( defined $val ) {
                push @auports, $val;
                next;
            }
            print "Invalid port $arg\n";
            exit 1;
        }
        if( $arg eq '-aip' && $ARGV[0] ) {
            if( $ARGV[0] =~ /^\d{1,3}(?:\.\d{1,3}){0,3}(?:\/(?:\d+|(?:\d{1,3}(?:\.\d{1,3}){3})))?$/ ){
                push @aips, NetAddr::IP->new( shift );
                next;
            }
            my @h = gethostbyname( $ARGV[0] );
            unless( @h) {
                print "Unknown host $ARGV[0]\n";
                exit 1;
            }
            unless( $h[3] == 4 && $#h >= 4 ) {
                print "$ARGV[0] : not an IPV4 address\n";
                exit 1;
            }
            for my $a (@h[4..$#h]) {
                push @aips, NetAddr::IP->new( sprintf( "%vd", $a ) );
            }
            shift;
            next;
        }
        if( $arg eq '-dip' && $ARGV[0] ) {
            if( $ARGV[0] =~ /^\d{1,3}(?:\.\d{1,3}){0,3}(?:\/(?:\d+|(?:\d{1,3}(?:\.\d{1,3}){3})))?$/ ){
                push @dips, NetAddr::IP->new( shift );
                next;
            }
            my @h = gethostbyname( $ARGV[0] );
            unless( @h) {
                print "Unknown host $ARGV[0]\n";
                exit 1;
            }
            unless( $h[3] == 4 && $#h >= 4 ) {
                print "$ARGV[0] : not an IPV4 address\n";
                exit 1;
            }
            for my $a (@h[4..$#h]) {
                push @dips, NetAddr::IP->new( sprintf( "%vd", $a ) );
            }
            shift;
            next;
        }
        if( $arg eq '-h' || $arg eq '--help' ) {
            Usage();
            exit 0;
        }
        print "Unknown switch $arg";
        print ' ', $ARGV[0] if( defined $ARGV[0] );
        print "\n";
        exit 1;
    }
    if( defined code2country( $arg ) ) {
        $iso{lc $arg} = 1;
    } else {
        my $cc = country2code( $arg );
        if( defined $cc ) {
            $iso{lc $cc} = 1;
        } else {
            print "Unrecognized country/country code: $arg\n";
            exit 1;
        }
    }
}
@iso = sort keys %iso;
@iso = @DEFAULT_ISO unless( @iso );
sub Usage {
    print << "HELP";
IP filter manager for country filters
Usage: $prog command args
  status [-v]        Display filter status
                     -v provides configuration from config file
                     and command file - NOT iptables.
  list               List available country names/codes
                       Contacts server for list.
  intercepts [-host name] [-days n]
                     List today's intercepts by host (from $LOG)
                     Requires -log
  stop               Stop filtering
  restart  args      Synonym for start (reloads with no open window)
  condrestart args   Restarts only if already running
  start args         Starts filter
Start uses tables of IP blocks assigned to country codes that are
stored in $ZONEDIR, which will be created if necessary.  The data
is obtained from $BLOCKURL when needed,
or when start -update is specified.
iptables filters are generated and installed by start.  The filters are
optimized and generally will not look identical to the input data.  However
they will match the same address (no more and no fewer.)
Arguments for start-class commands are:
 -update            Get latest data for active country codes.
                    Otherwise, only gets data if no local file exists for a CC.
 -log               Install a logging rule to log rejected packets.
 -nolog             Don't install a logging rule (default)
 -nolimit           Do not limit logging (can generate huge log files if under attack; not advised)
 -limit spec        Limit logging, default = $loglimits[0]:$loglimits[1] (see man iptables "limit")
 -atport n          Allow connections to TCP port n even from banned addresses.
                    May specify any number of times.  May use a service name.
 -auport n          Allow connections to UDP port n even from banned addresses.
                    May specify any number of times.  May use a service name.
 -aip ip(/mask)     Allow connection from an otherwise banned IP address.
                    For a block, specify a netlength or mask. A hostname may
                    also be specified.
 -dip ip(/mask)     Deny connections from an otherwise allowed IP address.
                    Same syntax as -aip
 -permitonly        Listed countries will be permited, all others denied
 -d                 Output random debugging messages
 -v                 Output extended status/statistics
  CC                ISO Country code or name to ban (as many as you like)
                    Default list:
HELP
    for my $cc (sort @DEFAULT_ISO ) {
        print "                      $cc - ", code2country($cc), "\n";
    }
    print <source/dev/null`);
    # Delete each one
    #  First, delete the rule in the main chain that reads '-s 1st octet, goto subchain'
    #  Then empty and delete the subchain
    for my $schain (@schains) {
        $schain =~ m/-(\d+)$/;
        system( "$IPT -D $chain -s $1.0.0.0/8 -g $schain" );
        system( "$IPT -F $schain" );
        system( "$IPT -X $schain" );
    }
}
sub delchainref {
    my $main = shift;  # e.g. INPUT
    my $chain = shift; # e.g. BLOCKchain
   my @crefs = map { (m/^$chain\s/ ? ( $chain, ) : ()) } split( /\n/, `$IPT -n -L $main 2>/dev/null`);
    for my $cref (@crefs) { # should be only one
        system( "$IPT -D $main -j $cref" );
    }
}
# Sort function for IP addresses for installation into filter chains
# The whole chain must be processed if we miss, so there's nothing we can do.
# But on a hit, we can improve the expected time somewhat by checking the
# largest blocks first.  This corresponds to the smallest mask length.
# It is possible to do better if the traffic pattern is known, but there
# isn't a good way (short of active feedback) to determine it.
# In any case, we reduce the search length by hashing on the first
# octet of the address, so this is a secondary effect.
sub ipcmp {
    my $x = $a->masklen  $b->masklen;
    return $x if( $x );
    return $a  $b;
}
sub start {
    print "Starting blocked countries IP filter: " unless( $ENV{CRONJOB} );
    File::Path::make_path( $ZONEDIR, { mode => 0771 } ) unless( -d $ZONEDIR );
    # Delete any lingering references / parts of new chain
    delchainref( $IHOOK, $IPNEWCHAIN );
    # Perhaps someday if I want to spend core on -d rules
#    delchainref( $OHOOK, $IPNEWCHAIN );
#    delchainref( $FHOOK, $IPNEWCHAIN );
    delsubchains( $IPNEWCHAIN );
    system( "$IPT -F $IPNEWCHAIN >/dev/null 2>&1" );
    system( "$IPT -X $IPNEWCHAIN >/dev/null 2>&1" );
    # (Log & ) drop chain
    system( "$IPT -F $IPNEWCHAIN-DLOG >/dev/null 2>&1" );
    system( "$IPT -X $IPNEWCHAIN-DLOG >/dev/null 2>&1" );
    return failure unless( system( "$IPT -N $IPNEWCHAIN-DLOG" ) == 0 );
    if( $log ) {
        # Note that we can not provide a per-country log prefix due to compaction.
        # However, the intercepts report will map IPs back to their (alleged) country of origin
        # To determine what countries are causing intercepts, the logs must be post-processed to
        # lookup each IP.
        my $limits = "";
        $limits = "-m limit --limit $loglimits[0] --limit-burst $loglimits[1] " if( @loglimits );
        return failure unless( system( "$IPT -A $IPNEWCHAIN-DLOG $limits-j LOG --log-prefix \"$LOGPFX\"" ) == 0 );
    }
    return failure unless( system( "$IPT -A $IPNEWCHAIN-DLOG -j DROP" ) == 0 );
    return failure unless( system( "$IPT -N $IPNEWCHAIN" ) == 0 );
    return failure unless( system( "$IPT -A $IPNEWCHAIN -m state --state RELATED,ESTABLISHED -j RETURN" ) == 0 );
    # List any allowed ports - first since they have a netmask of 0
    # Allowed TCP ports - no more than 15 per rule (limit of multiport)
    my $exceptions = @aips + @auports + @atports;
    my $xrules = 0;
    while( @atports ) {
        my $n = @atports;
        $n = 15 if( $n > 15 );
        return failure unless( system( "$IPT -A $IPNEWCHAIN -p tcp -m multiport --dports " . join( ',', @atports[0..$n-1] ) . ' -j RETURN' ) == 0 );
        splice( @atports, 0, $n );
        $xrules++;
    }
    # Allowed UDP ports
    while( @auports ) {
        my $n = @auports;
        $n = 15 if( $n > 15 );
        return failure unless( system( "$IPT -A $IPNEWCHAIN -p udp -m multiport --dports " . join( ',', @auports[0..$n-1] ) . ' -j RETURN' ) == 0 );
        splice( @auports, 0, $n );
        $xrules++;
    }
    # Allowed IPs (with optional masklen/netmask); largest size first
    #
    # Local subnets - external firewalls prevent them from showing up here, but
    # a bogus zone file could do damage.
    unshift @aips, NetAddr::IP->new( '192.168.0.0/16' ), NetAddr::IP->new( '172.16.0.0/12' ), NetAddr::IP->new( '10.0.0.0/8' );
    $exceptions += 3;
    @aips = sort ipcmp NetAddr::IP::Compact( @aips );
    $xrules += @aips;
    while( @aips ) {
        return failure unless( system( "$IPT -A $IPNEWCHAIN -s " . shift( @aips ) . ' -j RETURN' ) == 0 );
    }
    # Explicitly blocked IPs
    @dips = sort ipcmp NetAddr::IP::Compact( @dips );
    $xrules += @dips;
    while( @dips ) {
        return failure unless( system( "$IPT -A $IPNEWCHAIN -s " . shift( @dips ) . " -j $IPNEWCHAIN-DLOG" ) == 0 );
    }
    if( $verbose ) {
        # Exception statistics
        print( "\n",
               "$exceptions exceptions generated $xrules rules." );
    }
    # Make sure we have a zone file for each country code
    # Fetch a new one if -update or we don't have one
    # If we fetch, only transfer the file if it's different from (usu. newer than) our copy.
    my @files;
    for my $c (@iso) {
        my $db = "$ZONEDIR/$c.zone";
        my $cn = code2country( $c );
        $cn = " ($cn)" if( defined $cn );
        # Fetch if updating or have no data
        if( $update || ! -f $db || -z $db ) {
            my $rc = mirror( "$BLOCKURL/$c.zone", $db );
            if( is_success( $rc ) ) {
                print "\nUpdated IP zone data for $c$cn", if( $debug || $update || $ENV{CRONJOB} );
                # Shouldn't ever get an empty file, but may as well check
                unless( -f $db && -s $db ) {
                    print "\nUpdated zone data for $c$cn is empty!";
                    unlink $db;
                    return failure;
                }
            } else {
                if( $rc == RC_NOT_MODIFIED ) { # Can only happen if file exists
                    print "\nNo new IP data available for $c$cn " if( $debug || ($update && !$ENV{CRONJOB}) );
                } else {
                    print "\nUnable to fetch IP zone data for $c$cn: $rc - ", status_message($rc), " ";
                }
                unless( -f $db && -s $db ) {
                    # No data - don't replace current filter
                    print "\nNo IP zone data available for $c$cn ";
                    if( $debug ) {
                        next;
                    }
                    return failure;
                }
                # Failed, but have old file, continue since other zones may be updated
            }
        }
        push @files, $db;
    }
    return failure unless( @files );
    # Parse the zone files and create a list of IP blocks
    my @addresses = ();
    for my $if (@files) {
        open( my $ifh, '<', $if ) or die( "Can't open $if: $!" );
        while(  ) {
            s/\s*#.*$//;
            next unless( length );
            push @addresses, NetAddr::IP->new( $_ );
        }
        close $ifh;
    }
    return failure unless( @addresses );
    # Compact the blocks into the minimal covering set
    my $inaddrs = @addresses;
    @addresses = sort ipcmp NetAddr::IP::Compact(@addresses);
    # Generate an iptables-restore file with the new rules
    my %subchains;
    open( my $fh, '>', $ZONETBL ) or die( "Can't open $ZONETBL: $!" );
    print $fh "# Generated by $prog on ", (scalar localtime), "\n",
              "*filter\n";           # table
    if( $IHOOK eq 'INPUT' ) {
        print $fh ":INPUT ACCEPT [0:0]\n"; # built-in chain, policy, counters
    }
    # Note: Do not include input hook table declaration as this will clear it
    # It is guaranteed to exist because we checked earlier
    foreach my $ipblock (@addresses) {
        $ipblock =~ /^(\d+)\./;
        unless( $subchains{$1} ) {
            print $fh ":$IPNEWCHAIN-$1 - [0:0]\n",    # subchain, no policy, zero counters
              "-A $IPNEWCHAIN -s $1.0.0.0/8 -g $IPNEWCHAIN-$1\n";  # Chain - branch on 1st octet to subchain
        }
        $subchains{$1}++;
        if( $permitonly ) {
            print $fh "-A $IPNEWCHAIN-$1 -s $ipblock -j RETURN\n"; # Subchain, accept
        } else {
            print $fh "-A $IPNEWCHAIN-$1 -s $ipblock -j $IPNEWCHAIN-DLOG\n"; # Subchain, branch on match to log & drop
        }
    }
    if( $permitonly ) {
        foreach my $subchain (keys %subchains) {
            print $fh "-A $IPNEWCHAIN-$subchain -j $IPNEWCHAIN-DLOG\n";
        }
    }
    print $fh "COMMIT\n",
              "# Completed on ", (scalar localtime), "\n";
    close $fh;
    if( $verbose ) {
        # Provide some statistics, mostly for debugging.
        my( $minlen, $maxlen );
        $minlen = $maxlen = $subchains{(keys %subchains)[0]};
        for my $s (values %subchains) {
            $minlen = $s if( $s  $maxlen );
        }
        print( "\n",
               $inaddrs . ($permitonly? ' permitted' : ' blocked') . " address ranges generated ",
               (scalar @addresses), " rules, using ",
               (scalar keys %subchains), " sub-chains.  Savings: ",
               ($inaddrs - scalar @addresses), " rules (",
               sprintf( "%.2f", 100*(1- ((scalar @addresses))/$inaddrs)), "%).  Minimum chain length: $minlen",
               ", Maximum: $maxlen\n" );
    }
    # Install new ruleset
    # -- Mass-install new Chain, subchains & rules
    return failure unless( system( "$IPTR -n $ZONETBL" ) == 0 );
    # -- Link INPUT to the new chain
    return failure unless( system( "$IPT -I $IHOOK -j $IPNEWCHAIN" ) == 0 );
#    return failure unless( system( "$IPT -I $OHOOK -j $IPNEWCHAIN" ) == 0 );
#    return failure unless( system( "$IPT -I $FHOOK -j $IPNEWCHAIN" ) == 0 );
    # Remove old rules
    delchainref( $IHOOK, $IPOLDCHAIN );
#    delchainref( $OHOOK, $IPOLDCHAIN );
#    delchainref( $FHOOK, $IPOLDCHAIN );
    delsubchains( $IPOLDCHAIN );
    system( "$IPT -F $IPOLDCHAIN >/dev/null 2>&1" );
    system( "$IPT -X $IPOLDCHAIN >/dev/null 2>&1" );
    system( "$IPT -F $IPOLDCHAIN-DLOG >/dev/null 2>&1" );
    system( "$IPT -X $IPOLDCHAIN-DLOG >/dev/null 2>&1" );
    $IPOLDCHAIN = $IPNEWCHAIN;
    return success if( running );
    return failure;
}
sub stop {
    return 1 if( !running );
    print "Removing blocked countries IP filter";
    delchainref( $IHOOK, $IPOLDCHAIN );
    delsubchains( $IPOLDCHAIN );
    system( "$IPT -F $IPOLDCHAIN" );
    system( "$IPT -X $IPOLDCHAIN" );
    system( "$IPT -F $IPOLDCHAIN-DLOG" );
    system( "$IPT -X $IPOLDCHAIN-DLOG" );
    if( !running ) {
        success;
        return 1;
    }
    failure;
    return 0;
}
sub restart {
    # Don't stop since start will keep the current table alive until
    # the new one is active.
    return start();
}
# List intercepted IPs for today
# This can be run in a cron job just before midnight to get a list of
# IPs to report.  Or, you can use -days n to get the last n days worth
# of intercepts
# Only works if logging is on
sub intercepts {
    my( $fh, %ips );
    $days ||= 1;
    my $start = time() - ( $days * 24*60*60 );
    $host ||= Net::Domain::hostname();
    foreach my $logfile (glob $LOG) {
        my $lh = IO::Uncompress::Gunzip->new( $logfile, MultiStream => 1, Transparent => 1 );
        unless( $lh ) {
            print "Skipping system log file: $IO::Uncompress::Gunzip::GunzipError\n";
            next;
        }
        my $sl = Parse::Syslog->new( $lh, arrayref => 1 );
        # Record # intercepts for each ip => protocol => port
        while( my $l = $sl->next ) {
            next if( $l->[0] [2] eq $LOGPGM && $l->[1] =~ /$host/i;
            if( $l->[4] =~ /^\Q$LOGPFX\E.*?\bSRC=([0-9.]+).*?\bPROTO=(ICMP)\b.*?\bTYPE=(\d+)/ ) {
                $ips{$1}{lc $2}{$3}++;
            } elsif( $l->[4] =~ /^\Q$LOGPFX\E.*?\bSRC=([0-9.]+).*?\bPROTO=(\w+).*?\bDPT=(\d+)/ ) {
                $ips{$1}{lc $2}{$3}++;
            }
        }
        close $lh;
    }
    return 0 unless %ips;
    # List each intercepted IP, its country, the protocols, ports and number of packets for each
    print "Intercepts by host IP:\n";
    my( %ccip, %ccn );
    foreach (glob "$ZONEDIR/*.zone") {
        /$ZONEDIR\/(.*).zone/;
        my $cc = $1;
        next unless( defined code2country( $cc ) ); # Skip undocumented zone files
        open( my $ifh, '<', $_ ) or next;
        while(  ) {
            s/\s*#.*$//;
            next unless( length );
            push @{$ccip{$cc}}, NetAddr::IP->new( $_ );
        }
        close $ifh;
    }
    for my $ip (sort map {NetAddr::IP->new($_)} keys %ips) {
        my $ccn;
      CCSEARCH:
        foreach my $cc (keys %ccip) {
            foreach my $cip (@{$ccip{$cc}}) {
                if( $cip->contains($ip) ) {
                    print "$cc: ";
                    $ccn = $cc;
                    last CCSEARCH;
                }
            }
        }
        unless( $ccn ) {  # Possible if we've stopped blocking a country but have old log entries
            $ccn = '??';
            print '??: ';
        }
        print $ip->addr;
        my @plist = sort keys %{$ips{$ip->addr}}; # Protocol
        for my $p (@plist) {
            my @rlist = sort keys %{$ips{$ip->addr}{$p}}; # Ports
            print ' ', join( ' ', map { my $n = $ips{$ip->addr}{$p}{$_}; $ccn{$ccn} += $n; "$p-$_($n)" } @rlist );
        }
        print "\n";
    }
    print "Intercepts by country:\n";
    for my $cc (sort {$ccn{$b}  $ccn{$a} } keys %ccn) {
        my $cn = code2country($cc);
        if( defined $cn ) {
            $cn = "$cc ($cn)";
        } else {
            $cn = $cc;
        }
        printf "%10u %s\n", $ccn{$cc}, $cn;
    }
    return 1;
}
if( $cmd eq 'start' ) {
    exit !start();
}
if( $cmd eq 'stop' ) {
    exit !stop();
}
if( $cmd eq 'restart' ) {
    exit !restart();
}
if( $cmd eq 'condrestart' ) {
    exit !(running && restart());
}
if( $cmd eq 'status' ) {
    exit !status();
}
if( $cmd eq 'list' ) {
    exit !list();
}
if( $cmd eq 'intercepts' ) {
    exit !intercepts();
}
if( $cmd eq 'help' ) {
    Usage();
    exit;
}
print "Usage: $prog (start|stop|restart|condrestart|status|list|intercepts|help)\n";
exit 1;

Reply

85 tlhackque November 7, 2010 at 3:03 pm

I don’t know why the previous code post was htmll-formatted – I put it it in “code” tags, but it seems to have been reformatted anyway. View source shows HTML paragraphs and breaks. No wonder people have had trouble.

It would be helpful if this board had a way to simply attach a file to a post.

Meantime, you can find the unmangled source at http://pastebin.com/guudutHH

Reply

86 tlhackque November 7, 2010 at 6:41 pm

Further testing found a statistics display nit when -dip is used with start -v.

Fixed version is http://pastebin.com/Q3jjE22h

There is no functional impact – it only impacts the statistics display.

Feel free to report any other divots, bugs – and successes!

Reply

87 David Picard November 6, 2010 at 10:02 pm

The bash script was never intended to block more than a few countries – you’re better off using the Perl script from tlhackque to reduce the number of rules added to the firewall, but even then I suspect you’ll have a lot of rules in the table.

The bash script shouldn’t take so long to load. The latest version of the bash script can be downloaded at http://www.psind.com/products/iptables-cblock.tgz – the shell script posted initially by Vivek above takes a very long time to load, which is why I posted the optimized bash script some time ago.

Reply

88 David Picard November 7, 2010 at 6:57 pm

I’ve modified the bash script to function as an init script and have made it available at http://www.psind.com/products/iptables-cblock-init.tgz . tlhackque has noted a few unfinished or unaddressed features in the script and addressed them in his Perl version above. Some of these have been addressed as part of the init enablement in this version of the shell script, while others may be addressed in future versions.

I don’t believe either version defends against outbound communication by trojans, which was part of the reasoning behind including the block against the FORWARD and OUTPUT chain – although the rules there are unfinished. The other reason for having the FORWARD and OUTPUT rules is to block traffic forwarded from another interface on a gateway machine, although the rules currently do not address this risk completely.

Subscribe to this thread to receive an update when the outbound traffic restrictions are added to the script as an optional feature.

Reply

89 tlhackque November 7, 2010 at 10:55 pm

Because it was raining…

BlockCountries V1.3 is available at http://pastebin.com/cstR7Bve

CAUTION: If you are running an earlier version, you must stop blocking with that version, and restart with the new one. (And vice-versa should you want to go back.)

This is because the generated rules have a new, incompatible format, preventing either version from cleaning up the other’s rulesets.

This version adds the -blockout flag to start and stop. If you use this flag, I strongly recomend that you put it in the config file, not on a command line.

This will generate rules for blocking output & forwarding – with all the usual exceptions.

It will roughly double your actual rule count and memory requirements. (-v statistics won’t reflect this.) The output rulesets are identical to the input rules, except that they look at destination ports and addresses instead of source ports and addresses.

As David notes, it may be useful if you are infested with trojans or other spyware – but you really, really want to avoid that!

I would expect malware to evade this fairly easily.

In any case, if you want it, it’s there.

Feedback welcome. But please don’t wish for me to have more bad weather.

Reply

90 IzFazt November 7, 2010 at 11:16 pm

as I have been following this for some time and me myself been asking before for a reverse script, thus allowing only one country, it should be taken into account that certain important IP’s (Google and other search engines) should have access to your server because otherwise all sites can and wil not be indexed, which will get rid of about 70-90% of your visitors….I was asking this before for a shoutcast server. Because of rights payments only countries where broadcasting rights are paid should be able to reach your shoutcast servers, because otherwise you are worldwide accountable by every country for users who can listen

Reply

91 tlhackque November 8, 2010 at 10:59 am

The perl version supports the ‘allow just one (or a few) countries’ with -permitonly.

This is a sharp tool; as you point out, if you mis-use it, you will cut yourself.

It’s up to you to provide overrides for services from other countries that you care about. -aip will allow you to allow services by host or aubnet.

Reply

92 tlhackque November 8, 2010 at 11:11 am

Perl version 1.4 is available at http://pastebin.com/v1qeQED7. (Would you believe, it’s snowing?)

Improvements to -blockout as follows:

1) Output rules match on the destination port (as do input).

2) Output port overrides are now distinct from input port overrides. This is because you may want to allow, for example, http service from a banned country while blocking mail. But a trojan is likely to talk http outbound. So you don’t want to allow connections TO http in banned countries. Of course, if you also want to visit a website in an otherwise-banned country – your’re out of luck. But that just illustrates how difficult the trojan problem is. As I wrote earlier, you really want to avoid being infested in the first place. A better solution is non-country-specific deep packet inspection – for which there are other, more sophisticated engines.

-atporto and -auporto correspond to -atport and -auport for output rules.

Enjoy.

Reply

93 tlhackque November 8, 2010 at 7:13 pm

For those who are not familiar with installing perl scripts, I have provided an installation aid (bcinstall) at http://pastebin.com/5B03KbBH

This script will determine if perl is installed on your system and will see whether all the required library modules are installed.

Simply download the script, make it executable, and run it. (No parameters are required.)

If it says “All needed perl modules are installed”, block countries should run (assuming that iptables is installed.)

If it says that you need to install perl, it should be packaged for your distribution (apt-get, yum, etc.) If you can’t find a current perl version for your distribution (which would be very, very surprising) – you can find it at http://www.perl.org.

If it says that you need to install a perl module, here is how to do this:

Some distributions provide many of the modules under yum, apt-get, etc. If yours doesn’t – or doesn’t have one that this needs, use cpan. There should be a cpan command installed with Perl. Just say cpan to the shell, then install modulename (like IO::Uncompress::Gunzip) and it should just work. ‘quit’ exits cpan.

Install the module, then run bcinstall again. Repeat until bcinstall reports that you have all the needed modules.

I hope that this helps.

Reply

94 nixCraft November 8, 2010 at 9:05 pm

@tlhackque / David,

The faq has been updated with direct links. Thanks for sharing your code!

Reply

95 PacoSS November 9, 2010 at 11:06 am

Am I the only one that have noticed that the OUTPUT rule is not needed at all?
If you can’t receive from that country, you will never send an answer to that country.

But if you still need it (don’t know why), the rules are inverted: must be -d (destiny) instead of -s (source).

Great post, David.

Reply

96 tlhackque November 9, 2010 at 12:54 pm

Not exactly. These rules apply when a connection is initiated. Once a connection is established, data can flow in both directions. The INPUT rules block incoming connections. The OUTPUT rules block outgoing connections. And the FORWARD rules block packets forwarded from one interface to another.

The INPUT rules suffice for most circumstances.

As David pointed out, if machines on your internal network are infected – for example with a keystroke logger, they will try to make an outbound connection to deliver their stolen data to the criminals. Such infections can come from many sources – USB keys, websites in an unblocked country, “free” software downloads – and more. Or, one may even have humans who try to transfer data to countries in violation of export or business rules. (Accidentally, of course.)

The OUTPUT rules limit the countries to which malware can connect. We can argue about how effective this is (see my previous comments), but it can have some effect. If someone believes that it’s worthwhile to implement this blocking, it is technically possible.

The perl code optionally generates the necessary output (and forward) rules. And yes, it is smart enough (as I have mentioned) to apply destination filtering for outbound rules and source filtering for input rules.

As he notes, the current version of David’s code doesn’t do this – output and forward rules are “unfinished”.

As always, when choosing a tool it is important to evaluate the capabilities and limitations of each alternative.

David’s script is easier to install and provides an archiving function for previously-used zone blocking files. The perl script produces more efficient rules, supports output blocking and has a variety of other capabilities as noted in prevoius posts.

Whether to block at all, and what technology to use if you do, is a choice that each system administrator, in conjunction with business management, should make carefully.

Reply

97 David Picard November 9, 2010 at 12:30 pm

Hi PacoSS – to answer your question on the OUTPUT block, we allow responses on established sessions so that BluRay players on those home networks can establish connections out to Samsung in Korea to get the flash updates and continue playing those wonderful BluRay discs as well as suporting requests for other updates / drivers to web sites hosted in blocked countries.

An OUTPUT block eliminates requests on unspecified ports which may be used by trojans to communicate key logging results out on IRC chat or some other network protocol. Admitted, they could use port 80 or 443 (which you should leave unblocked to get those hardware drivers), but use of a proxy HTTP server for all outbound requests can give you better control of traffic out on port 80 and 443 and provide a different mechanism to block outbound traffic from trojans, especially if you use an authenticated proxy server. I block port 80 and 443 on my FORWARD chain for traffic destined outside of my local network, forcing all users to go through the proxy server.

Reply

98 PacoSS November 9, 2010 at 3:45 pm

But the iptables parameter -s appy to the source of the packet.
So the OUTPUT chain must use -d if you want to block any packet to get to the desired blacklisted country.
The same in the forward rule if the server is working as a firewall.
Kind regards.

Reply

99 tlhackque November 9, 2010 at 4:22 pm

You are correct that input rules must use -s and output rules -d.

There are two implementations of country blocking discussed here.

I’ll let David speak for himself on the shell script version.

The Perl script version 1.4+ provides output blocking. It uses -d for the output and forward rules and -s for the input rules. If you want this function, try it and see. See posts 87 & 88 for where to get it – the current source is NOT in this thread due to formatting issues.

Reply

100 David Picard November 10, 2010 at 2:29 am

As PacoSS has noted, the OUTPUT rules in the script version I provide are not yet properly formed to block on the destination – I still need to create a separate chain for those. As an inbound FORWARD rule is highly unlikely on most intranets using the private IP space, the FORWARD rule should also likely use the -d option for the blocked addresses. It’s on the roadmap to make the changes, but I’m not making firewall changes while I’m out of the office so probably will not get to it until this weekend or next weekend ;-)

I also want to take a little more time to think through how a FORWARD rule might be required in a non-redundant fashion as attack vectors are generally not addressed in both a simple and complete fashion.

Reply

101 Valerio January 7, 2011 at 7:12 pm

Thx for this great project!
I have some problems with EU geoip zones.
When i do update trought perl script i receive this error.
“Unrecognized country/country code: eu”
I have tried to update locale::country module but i have the last version!

How can i fix it?

Reply

102 ex January 7, 2011 at 9:06 pm

EU is continent. You need to set country names in EU such as UK, DE (Germany), ES (Spain) etc. See http://www.iso.org/iso/country_codes.htm

Reply

103 Valerio January 8, 2011 at 3:13 pm

OK but in more geoip site i see this ip class http://www.ipdeny.com/ipblocks/data/countries/eu.zone
what is?

Thx for your fast reply!

Reply

104 nixCraft January 8, 2011 at 4:24 pm

I’ve not tested this but I guess you need to make some modification to source code. The original code is designed for country only blocking and not for continent. I hope you’ve modified $BLOCKURL or configfile as documented by Timothe.

Reply

105 Nilesh January 9, 2011 at 2:17 am

In case anyone is interested, I have written the IPset version of this article. It was requested by a visitor on my site.
The article can be found here.
I’m sorry if this appears as SPAM to someone. @Vivek, please remove this comment if you consider this as SPAM.

Reply

106 David Picard February 19, 2011 at 2:49 pm

I’ve finally got my blog back up and running which outlines some of the business implications of using this script and also have the script available on my site again for downloading from here for those who still prefer the shell script.

Reply

107 Søren March 14, 2011 at 8:04 pm

Hi Guys

Thank you very much for this usefull (albeit long!) blog.

Just to let you know: As the last IP ranges have now been handed out there surely is no need to schedule this as a CRON job? A single run of the perl/sh script should be sufficient to generate the iptables stuff and allow it to be copied to internet facing servers.

Personally I plan to single out all US ips and route them through my US proxy, so that I’m able to stream both domestic (danish, europe) tv and Hulu/Netflix etc.. It’ll be fun to inject these settings into the router.
It’s a linux based DD-WRT firmware so it *should* work (with a bit of re-engineering – personally I’m a powershell guy not perl, but we’ll see)…

Reply

108 tlhackque July 26, 2013 at 2:02 pm

I don’t visit this forum so I’m a bit late.

IPv6 is handled by the latest Perl script. Those DO still change.

See https://github.com/tlhackque/BlockCountries for the latest (and all future) versions.

Reply

109 Cameron November 5, 2011 at 11:32 pm

I have installed the perl script BlockCountries, have it running as a service and see the /root/blockips directory populated with zones for countries I have added in /etc/sysconfig/BlockCountries but do not see any added rules in iptables. Is there something I need to do to get BlockCountries to add rules to iptables ?

Reply

110 Cameron November 6, 2011 at 9:48 am

Never mind iptables -L is showing the new chains. Thanks for the awesome script!!

Is there any way to add some custom ranges that are not one country specific so they are handled by the script? Can I makes something like a custom zone in /root/blockips?

Reply

111 milano94 November 20, 2011 at 3:27 pm

Nice scripts,
Another question, How do I block all country and only allow my country as allowed range of Ip’s.. adding to much list of country will not be a good practice right?

Reply

112 Shaun December 9, 2011 at 1:45 am

Hi guys. Thanks for the scripts. Quite a long read. Be nice having a summary of features, usage somewhere?

My understanding is if using proper chaining in ip tables having a large number of rules isn’t really an issue. Seems ever so slightly slower than ip set?
(Only benchmark I could find. Last couple charts on page 13. http://people.netfilter.org/kadlec/nftest.pdf)

Also found this script by BoneKracker.
http://forums.gentoo.org/viewtopic-t-863121-start-0.html
Any downside to using this over the two scripts found here other than needing ip sec installed – (which doesn’t seem like an issue).

Guessing BoneKrackers script is first choice then tlhackque’s perl script then David’s ip tables (chain) bash script…

Also nobody seems to be discussing the accuracy of IP Deny. What’s the quality of data like – no mention of it even on their site? How does it compare to MaxMind’s data?

Reply

113 DungX Mokahd February 24, 2012 at 3:06 pm

line 53: cn Country Drop: command not found
iptables v1.3.5: Unknown arg `–log-prefix’
i dont know, i see more and more this message, pls help me
line 53: done

Reply

114 Joern April 10, 2012 at 6:48 am

Hi guys,

Is it possible to make a whitelist-Script out of the Script?

I know this post is a bit old and someone mentioned this before:
I haven’t analyzed the script yet, but is it hard to change it?

Thanks.

Reply

115 David Picard April 13, 2012 at 1:03 pm

I’ve mentioned that a whitelist script is not necessarily the best idea as there could be an issue with the IP mappings where some groups are overly broad. In addition, a whitelist would need to include the private networks like 10.0.0.0/8 and 192.168.0.0/16. In addition, there are services like DLNA (video and audio streaming within the home) that use broadcast addresses for discovery that would likely be blocked unintentionally with a white list approach.

If your dead set on trying a white list, set the default policy on the interface to DENY and change all instances of DENY or DROP in the script to ACCEPT – that would theoretically create the white list. If you want to accept traffic from inside your own network or allow broadcast discovery of devices through DLNA, etc… you will have a lot more work to do.

Reply

116 George November 27, 2012 at 7:44 pm

did you ever get this sorted ? I only just read it – if not I have an automatic updating whitelist script that works off a MySQL database, some bash, and some PHP. It runs the script via crontab and produces a script that configures iptables to block everything and then lets the Countries that are selected on the database through. It also concatenates the ranges to minimize RAM usage. If you want a copy let me know.

Reply

117 Ramon October 12, 2012 at 4:50 am

Thanks for sharing. I just installed the script for my server. Hopefully the those f*kin bots and scanners from china are no longer able to connect. Is it correct they are looking for vulnerable config within php and apache?

cheers

Reply

118 Lee December 15, 2012 at 5:09 pm

Thanks for this guys! Man I was tired of all the hack attempts. I am using fail2ban, and it’s working pretty good, but this helps clean up the rest of it. Thanks again!

Reply

119 Giuseppe Urso January 2, 2013 at 11:13 am

Hi, very useful, my compliments.
I just addedd a counter during loop and a final echo with total rules created.
Thank you it’s a very good script.

Reply

120 Alan January 9, 2013 at 4:28 pm

Hi guys,

Nice scripts.I was going to use apache level restrictions but this works very well indeed. The only this is i am block a large number of countries but not necessarily for security purposes. Mostly this the what the project requires. But i don’t want potential customers (we will be opening it up to international later in the year) to get a ‘No connectivity’ message.
Ideally i’d like to redirect (forward) all those blocked incoming IP’s to another IP (which will have a single static page saying “thanks for your interest. at this time we are..etc..”). This second IP is on a different machine.
I’ve had a look around but i’m not sure if IPtables can do this for me. Most scripts i’ve seen can forward ALL port 80 packets to another IP but i only want the blocked IP’s forwarded.
Any suggestions would be welcomed.
cheers

Reply

121 David Picard January 13, 2013 at 9:48 pm

Alan – this can easily be done with iptables – but the rules must be applied on the nat table in the PREROUTING chain. Instead of jumping to the DROP rule simply create another chain – perhaps DROPXHTTP – and replace the jump to drop with the new chain. The DROPXHTTP chain should redirect if the packet is on port 80 and jump to the DROP chain otherwise.

Reply

122 Alan January 14, 2013 at 11:39 am

Thanks David,
Managed to get that working after i figured out to enable my IP_forwarding in sysctl.conf.

Reply

123 Jouni "rautamiekka" Järvinen March 28, 2013 at 4:18 pm

IMO this script is more than bad idea for 2 reasons: You
1) delete all previous rules in IPtables instead of leaving them there. I don’t see any reason why they should be deleted especially when you’re not gonna put them back after adding the addresses.

2) append all those addresses to the main table instead of making separate table for either all target countries or per country and then calling that table somewhere amongst the rules. If it’s true that alot of rules in single table is performance sink, then table per country or some other multitable solution.

Reply

124 David Picard March 29, 2013 at 11:12 am

Jouni –

You must be looking at a different script – the script I posted in the follow-up does not delete all the previously created rules. Also appending everything to the main table can adversely impact performance – we create the table groupings to reduce the number of rules that need to be checked as each packet comes in – I’m pretty sure all this was covered in the topic thread above if not in my blog posting on my site.

Reply

125 Jouni "rautamiekka" Järvinen March 29, 2013 at 11:29 am

Yeah, I looked at the script on the top of this article.

Reply

126 Joseph July 6, 2013 at 2:38 am

I hacked up your script, I think my version is a little better xD

http://pastebin.com/NDTZdjBH

Reply

127 David Picard July 6, 2013 at 12:58 pm

Joseph – you made some minor modifications to the original script. If you go back to the top of the post, there are some references to the enhanced scripts in bash and perl which have more features and better performance than the original started by Vivek. You should build on those scripts unless you’re taking this in a new direction.

Reply

128 Joseph July 6, 2013 at 3:25 pm

No I am not. I was just trying to keep it simple and improve the “original” script by adding the countries into their own chains. Also made it so it wouldn’t remove any current rules you already have saved and added a feature to remove only the rules added by the script by executing “./block_countries_iptables.sh flush”.

I did check the enhanced scripts and they may be overwhelming for a novice but good work by their authors.

Reply

129 David Picard July 6, 2013 at 4:10 pm

The enhanced scripts do that and also load the tables through iptables-restore – the script you are using can take hours to load if you have too many ip ranges as the iptables command commits the change in the kernel after every additional rule. The original script also blocks responses from any country you block – when I blocked Korea I couldn’t update my Samsung blue ray player. You might want to read through the discussions on the thread if you run into any of these issues with your modifications – the thread outlines a number of issues and their fixes.

Reply

130 Joseph July 6, 2013 at 4:58 pm

I don’t mean to go back and forth but I understand how saving the rules to a file and using iptables-restore < your_file would be faster and more efficient. I was only trying to improve the original script posted and not do a complete rewrite when they have already been done by 2 others. I wouldn't suggest running the script as a cron job but it is very simple and easy to understand to quickly block a few countries.

Also I blocked af, cn, kr, and th in less then a minute.

Reply

131 Glen Schaefer August 2, 2013 at 12:32 am

Hi Joeseph, I am not having any luck getting any of these scripts to work under Centos 5.9 on my dedicated VPS account.

Yours gives me this error

: command not foundline 4:
: command not foundline 11:
: command not foundline 16:
‘BlockCountries.sh: line 21: syntax error near unexpected token `do
‘BlockCountries.sh: line 21: ` do

Any ideas how to fix this for Centos?

Reply

132 Glen Schaefer July 16, 2013 at 10:26 am

Thanks heaps for pointing me to that solution,

I have edited the script to block India as per instructions
I uploaded the script to /root/scripts and gave it 755 permissions
I uploaded the zone file for india (in.zone) into the same directory
I then logged into as root via SSH ad and tried to run the script /root/scripts/country.block.iptables.sh

but am getting this error on Centos 5.0
-bash: country.block.iptables.sh: command not found

What am I doing wrong?

Reply

133 tlhackque July 26, 2013 at 1:43 pm

The BlockCountries perl script has been updated to V2.0, supporting improved chain creation and IPv6. It also gets the IP address allocations directly from the NICs.

I will not be monitoring or posting here. The new script can be found, and any updates posted on

https://github.com/tlhackque/BlockCountries

This should make it easier to find and also makes it possible to manage any updates.

I don’t promise to respond to issues, but they can be posted there.

Of course, positive feedback is also appreciated.

Reply

134 Glen Schaefer July 31, 2013 at 12:16 pm

tlhackque – i ran bcinstall first and installed a couple of required modules under CentOS release 5.9 (Final). Then uploaded BlockCountries 2.0 from github and did to a CHMOD to execute.

Am getting this error – any idea’s?

root@server2 [/]# bash BlockCountries.sh
BlockCountries.sh: line 9: use: command not found
BlockCountries.sh: line 10: use: command not found
BlockCountries.sh: line 52: my: command not found
BlockCountries.sh: line 56: my: command not found
BlockCountries.sh: line 57: my: command not found
BlockCountries.sh: line 59: my: command not found
BlockCountries.sh: line 60: my: command not found
BlockCountries.sh: line 61: my: command not found
BlockCountries.sh: line 63: syntax error near unexpected token `(‘
BlockCountries.sh: line 63: `my %config = ( # Parameters that can vary between IPV4 and IPV6′

Reply

135 tlhackque August 2, 2013 at 10:21 am

Please put any issues on the github site – it’s only random chance that I happened across this. (I came here to unsubscribe!)

BlockCountries is not a bash (.sh) script. It’s a Perl script.

If you downloaded it correctly, chmod +x and just execute it.

bash# chmod+x BlockCountries
bash# BlockCountries help
bash# BlockCountries -update -start

You said:

bash# bash BlockCountries

That tells tells bash to execute it as a shell script, which it is not.

(bcinstall is a bash script because it has to be able to tell you to install Perl.)

@vivek – please update your FAQ to point to https://github.com/tlhackque/BlockCountries

for all future versions of the perl BlockCountries script.

I appreciate this forum – but besides having grown too hard to follow, maintaining code on a BBS is not feasible.

I’m unsubscribing, so really, anyone who is interested in the Perl script should visit GitHub – there is an Issues page (https://github.com/tlhackque/BlockCountries/issues) where there’s some chance I’ll see questions.

Reply

136 nixCraft August 2, 2013 at 10:40 am

Done. Updated the link. Thank you for your contribution!

Reply

137 Glen Schaefer August 2, 2013 at 11:50 am

tlhackque – A MASSIVE THANKYOU !!

it’s working fine now, i had the config file in the wrong place as well not understanding the Centos cmd line variations very well.. it works with this cmd:

#perl BlockCountries start

My server has been plagued by hackers & spammers for nearly a year and I was going mad bailing out the ship with buckets! This is exactly what I was looking for so a million thanks. hopefully I don’t have to close my web business down now.

Reply

138 IronNighthawk August 13, 2013 at 8:13 pm

I´ve used the script from david without problems. Thank you for this great script, David! But now i have a problem. Since my serverprovider do an update to the kernel i get these error: “iptables-restore: line 2354 failed”. Line 2354 is “COMMIT”.
On my server running iptables v. 1.4.8, is something changed here? Sadly i dont know which version it was before and i dont know if it depends on the new kernel itself.

Can anyone help me?

Reply

139 Jouni "rautamiekka" Järvinen August 14, 2013 at 5:30 am

What are the kernel versions from to ?

Reply

140 IronNighthawk August 14, 2013 at 6:42 pm

Thanks for your reply Jouni!
The old kernel version was “2.6.32-5-xen-amd64″, the new kernel is “3.8.5-xen”. The server running debian on a xen virtualization.

Reply

141 IronNighthawk August 14, 2013 at 11:06 pm

Got it! It depends on the “log lines” like this for not working:
-A kr-countrydrop-DROP -j LOG –log-prefix “iptables: kr-Country-Drop:”

Since i´ve comment this out in the mainscript it works. I dont know why because the syntax seems to be ok…

Reply

142 David Picard August 15, 2013 at 2:10 am

I took a look at the latest iptables man pages on Fedora – there is a limit of 29 characters on the prefix and I’m wondering if the –log-level option is now required. If you add a log level of notice and uncomment one of the LOG lines with a prefix under 29 characters, does the logging work and the chain commit?

Reply

143 IronNighthawk August 15, 2013 at 1:45 pm

Thank you for your reply, David.
I´ve tested your ideas and nothing works. I´ve edited the generated iptables.cb like this:
-A af-countrydrop-DROP -j LOG –log-prefix “af-Drop:” –log-level notice
This should work but it dont work.
If i do this for example:
-A af-countrydrop-DROP -j LOG –log-level notice
it results in the same error.

I think it is a problem in the kernel config. I will have a look now if the Log-Module working or not. If not I will contact my provider.

Reply

144 Geoff Jackson August 27, 2013 at 12:32 pm

How could we add a sleep delay in between each iptables rule processed?

(if even possible at all).

Thanks.

Reply

145 nixCraft August 27, 2013 at 1:38 pm

Use sleep to suspend execution for an interval of time:

for c  in $ISO
do
        # sleep 60 seconds ##
        sleep 60
	# local zone file
	tDB=$ZONEROOT/$c.zone
	# get fresh zone file
	$WGET -O $tDB $DLROOT/$c.zone
	# country specific log message
	SPAMDROPMSG="$c Country Drop"
	# get
	BADIPS=$(egrep -v "^#|^$" $tDB)
	for ipblock in $BADIPS
	do
	   $IPT -A $SPAMLIST -s $ipblock -j LOG --log-prefix "$SPAMDROPMSG"
	   $IPT -A $SPAMLIST -s $ipblock -j DROP
	done
done

Reply

146 Geoff Jackson August 27, 2013 at 2:04 pm

Ace, thanks for a prompt response with this extension to your script… :)

Reply

147 David Picard August 27, 2013 at 1:51 pm

Why add a sleep delay? Are you using Vivek’s original script? If so, please use my modified script which gets around the issues with single rule commit that cause loading to take hours, my script does a batch load of the rules and takes seconds.

Reply

148 Geoff Jackson August 27, 2013 at 2:07 pm

Hey David,

Everytime I was trying to add lots of iptables rules at once, I kept receiving ‘iptables unknown errors’. After testing, it didn’t seem to be an issue with particular rules but more a case of randomly throwing the error when multiple iptables were being processed so I wanted to try adding a delay between rules to see if this resolved.

It subsequently didn’t by the way but did stop long enough at the start of the script for me to see this error:-

iptables v1.3.5: can’t initialize iptables table `nat': Table does not exist (do you need to insmod?)
Perhaps iptables or your kernel needs to be upgraded.
iptables v1.3.5: can’t initialize iptables table `nat': Table does not exist (do you need to insmod?)
Perhaps iptables or your kernel needs to be upgraded.

I am able to add individual iptables though. So not really sure where I’m going with this at the moment.

Trying to block the entire country of China by the way using iptables.

Reply

149 David Picard August 27, 2013 at 5:47 pm

The nat table allows you to implement forwarding and masquerade packets as the device connected to the internet – if you don’t want nat, simply remove those rules from the script.

I use nat because my machine is the gateway for my home network connecting every device to the internet. Many home cable or dsl routers now implement this capability for you so you don’t technically need to make your machine a gateway.

Reply

150 Geoff Jackson August 28, 2013 at 12:48 pm

Thanks, I’ll give this a go and see if it improves anything my end.

Reply

151 Geoff Jackson August 28, 2013 at 3:00 pm

No more than 18 iptables rules were being accepted. I think the issue is actually because I was trying to use iptables on a VPS that is a shared single kernel across multiple containers… :(

Reply

152 tlhackque September 4, 2013 at 12:40 pm

Folks using the Perl script:

Due to a format change in the RIR statistics files, a new version has been released on github. Download from:
https://github.com/tlhackque/BlockCountries/archive/master.zip

After updating, ‘BlockCountries help’ should report version 2.1
You should run ‘BlockCountries start -update’ to get the latest data.

Any issues, feedback at https://github.com/tlhackque/BlockCountries/issues, as I am no longer subscribed and do not monitor this BBS.

I recommend getting a github account (they’re free) and watching the repository for any future updates – I don’t promise to provide release notices here.

Reply

153 antiplex November 8, 2013 at 11:21 am

i might have gotten something wrong, but anybody else realized that most of the files at http://www.ipdeny.com/ipblocks/data/countries/ are of size 0 since sept 29 2013?
something wrong at ipdeny? anybody found a more reliable source?

anyway, thanks for the many interesting aspects and scripts around this issue ;)

Reply

154 David Picard March 2, 2014 at 8:28 pm

I took a look at the files and they look like they’re now empty – however maxmind.com also provides a source for the IP addresses but the dump of the data requires some translation to make it readable by the scripts here. The following shell script does this translation:

#!/bin/sh
CIP_CACHE=/var/cache/country-ips
DL_DATAFILE=GeoLite2-Country-CSV.zip
WGET=/usr/bin/wget
# Create cache location for country ip range files
[ ! -d ${CIP_CACHE} ] && mkdir -p ${CIP_CACHE}
cd ${CIP_CACHE}
# Download data if source file is not present
if [ ! -f ${DL_DATAFILE} ] ; then
    $WGET http://geolite.maxmind.com/download/geoip/database/${DL_DATAFILE}
    unzip -j ${DL_DATAFILE} */GeoLite2-Country-Locations.csv */GeoLite2-Country-Blocks.csv
fi
# Read each country from the country file
export IFS=","
typeset -l -g CN_CD
while read GEO_ID RG_CD RG_NAME CN_CD CN_NAME CN_LINE; do
    if [ "Z${CN_CD}" != "Z" ] ; then
    printf "# Zone file for country %s : %s\n" ${CN_NAME} ${CN_CD}
    printf "# Zone file for country %s : %s\n" ${CN_NAME} ${CN_CD} > ${CN_CD}.zone
    while read GEO_IP IP_MASK_LEN IP_RG_CD IP_CN_CD IP_LINE ; do
        declare -i CIDR=$((32-(128-${IP_MASK_LEN})))
        if [ "${IP_CN_CD}" = "${GEO_ID}" ] ; then
            printf "%s/%s\n" ${GEO_IP} ${CIDR} | sed -e 's/::ffff://g' >> ${CN_CD}.zone
        fi
    done < "GeoLite2-Country-Blocks.csv"
    fi
done < "GeoLite2-Country-Locations.csv"

Reply

155 stingray November 11, 2013 at 3:55 pm

Hello look at this script: https://gist.github.com/anonymous/7415239
its much simpler and does the same and its much faster than using iptables only
dont forget to install ipset befor running the script!
Greetings Stingray

Reply

156 Dimitrios March 1, 2014 at 10:25 pm

Hello everyone, i have modify the script in order to secure my Centos 6.4 server
Just place in your server the zone files and set your static ip address in order to manage your server only from your ip address

#!/bin/bash
### Block all traffic from AFGHANISTAN (af) and CHINA (cn). Use ISO code ###
### dloghis ###
### WARNING!People from other countries may use proxy server or think of spoofing their IP address.
### In such case, this may not work and it will only protect your box from automated scans or spam.
# Most common spammers
ISO="af cn br cl co hk in ir th ve ne ro rs ru pa jp tw tr my pk ng ua a1 kr kp sa etc"
# All options
# ISO="ad ae af ag ai al am an ao ap ar as at au aw ax az ba bb bd be bf bg bh bi bj bm bn bo bq br bs bt bw by bz ca cd cf cg ch ci ck cl cm cn co cr cs cu cv cw cy cz de dj dk dm do dz ec ee eg er es et eu fi fj fm fo fr ga gb gd ge gf gg gh gi gl gm gn gp gq gt gu gw gy hk hn hr ht hu id ie il im in io iq ir is it je jm jo jp ke kg kh ki km kn kp kr kw ky kz la lb lc li lk lr ls lt lu lv ly ma mc md me mf mg mh mk ml mm mn mo mp mq mr ms mt mu mv mw mx my mz na nc ne nf ng ni nl no np nr nu nz om pa pe pf pg ph pk pl pm pr ps pt pw py qa re ro rs ru rw sa sb sc sd se sg si sk sl sm sn so sr ss st sv sx sy sz tc td tg th tj tk tl tm tn to tr tt tv tw tz ua ug uk um us uy uz va vc ve vg vi vn vu wf ws ye za zm zw"
### Set PATH ###
IPT=/sbin/iptables
WGET=/usr/bin/wget
EGREP=/bin/egrep
### No editing below ###
SPAMLIST="countrydrop"
ZONEROOT="/root/cron-scripts/iptables"
DLROOT="http://www.yourserver.gr/ipblocks"
SERVERIP=$(hostname -i)				# Your Server IP
SERVERHOSTNAME=$(hostname -s)		# Your Server Host Name just for info is you have multiple servers
ACCESS_IP="xxx.xxx.xxx.xxx"			# Your static IP here, only from this IP you are going to SSH
SSHPort=xxx							# Your port here, only from this port you are going to SSH
adate=$(date +%Y-%m-%d,%X)
# Title and Version
echo "*----------------------------------------*"
echo "*          IPtables V-20140301           *"
echo "*----------------------------------------*"
echo "Server Name: "$SERVERHOSTNAME "IP="$SERVERIP
echo "***************  WARNING  ****************"
echo "Check your IPs & Ports and type your choice"
echo "After running this script you are going to"
echo "access with SSH only from:" $ACCESS_IP " port:" $SSHPort
echo ""
# Ask if you want to save settings
AskSaveSettings () {
PS3="Do you want to save settings?"
select result in Yes No
do
  case "$result" in
  Yes) SaveSettings && ListSettings && echo "" && echo "Settings saved..." && GiveDate && exit 0;;
  No)  ListSettings && GiveDate && echo "Settings not saved..." && exit 0;;
  *) echo "Invalid";;
  esac
done
}
# clean old rules command
CleanOldRules(){
$IPT -F
$IPT -X
$IPT -t nat -F
$IPT -t nat -X
$IPT -t mangle -F
$IPT -t mangle -X
$IPT -P INPUT ACCEPT
$IPT -P OUTPUT ACCEPT
$IPT -P FORWARD ACCEPT
}
# Set default policies for INPUT, FORWARD and OUTPUT chains (drop all incoming)
DropIncoming () {
$IPT -P INPUT DROP
$IPT -P FORWARD DROP
$IPT -P OUTPUT ACCEPT
}
# Accept the ports you need
AcceptPorts () {
# Set access for localhost
$IPT -A INPUT -i lo -j ACCEPT
# Allow SSH connections on tcp with ACCESS_IP and port SSHPort values
$IPT -A INPUT -p tcp -s $ACCESS_IP --dport $SSHPort -j ACCEPT
# All standard ports
# Allow ftp connections on tcp port 21
$IPT -A INPUT -p tcp --dport 21 -j ACCEPT
# Allow SSH connections on tcp port 22 ??
# Allow smtp connections on tcp port 25
$IPT -A INPUT -p tcp --dport 25 -j ACCEPT
# Allow http connections on tcp port 80
$IPT -A INPUT -p tcp --dport 80 -j ACCEPT
# Allow https connections on tcp port 443
$IPT -A INPUT -p tcp --dport 443 -j ACCEPT
# Allow POP3 connections on tcp port 110
$IPT -A INPUT -p tcp --dport 110 -j ACCEPT
# Allow SPOP3 connections on tcp port 995
$IPT -A INPUT -p tcp --dport 995 -j ACCEPT
# Allow IMAP connections on tcp port 143
$IPT -A INPUT -p tcp --dport 143 -j ACCEPT
# Allow IMAP connections on tcp port 993
$IPT -A INPUT -p tcp --dport 993 -j ACCEPT
# zimbra admin connections on tcp port 7071 from your ip address
$IPT -A INPUT -p tcp -s $ACCESS_IP --dport 7071 -j ACCEPT
}
DropCountries () {
# create a dir
[ ! -d $ZONEROOT ] && /bin/mkdir -p $ZONEROOT
# create a new iptables list
$IPT -N $SPAMLIST
for c  in $ISO
do
	# local zone file
	tDB=$ZONEROOT/$c.zone
	# get fresh zone file
	$WGET -O $tDB $DLROOT/$c.zone
	# country specific log message
	SPAMDROPMSG="$c Country Drop"
	# get
	BADIPS=$(egrep -v "^#|^$" $tDB)
	for ipblock in $BADIPS
	do
	   $IPT -A $SPAMLIST -s $ipblock -j LOG --log-prefix "$SPAMDROPMSG"
	   $IPT -A $SPAMLIST -s $ipblock -j DROP
	done
done
# Drop everything from SPAMLIST (-I=incoming)
$IPT -I INPUT -j $SPAMLIST
$IPT -I OUTPUT -j $SPAMLIST
$IPT -I FORWARD -j $SPAMLIST
}
# Save Settings Action
SaveSettings () {
/sbin/service iptables save && echo "" && echo "Saving settings please wait..." && echo ""
}
# List Settings
ListSettings () {
echo ""
echo "Your Settings are:"
$IPT  -L -n -v
}
# Information Just to know how long it took
GiveDate () {
echo ""
bdate=$(date +%Y-%m-%d,%X)
echo "Job  Start  in:     "$adate
echo "All Done! End time: "$bdate
}
# Ask for actions CleanOldRules DropIncoming AcceptPorts DropCountries SaveSettings
PS3="Are you soure you want to Continue?"
select result in  	Continue_Clean_Add_All_Rules Continue_Clean_Add_All_Rules_and_Save Continue_Add_New_Countries Cancel
do
  case "$result" in
  Continue_Clean_Add_All_Rules) CleanOldRules && DropIncoming && AcceptPorts && DropCountries && exit 0;;
  Continue_Clean_Add_All_Rules_and_Save) CleanOldRules && DropIncoming && AcceptPorts && DropCountries && SaveSettings && ListSettings && GiveDate && exit 0;;
  Continue_Add_New_Countries) DropCountries && AskSaveSettings && GiveDate && exit 0;;
  Cancel) exit 0;;
  *) echo "Invalid option";;
  esac
done

Reply

157 David Picard March 2, 2014 at 12:58 pm

Hi Dimitrios – your script appears to be a complete rewrite of the original script. It discards any previously established iptables rules on the system and does not warn the security admin that their rules are being discarded. It also updates iptables one rule at a time and would take many hours to run if the zone files were populated – the files on the server are empty though so none of the scripts are functional.

You should read the full thread to understand my bash version as referenced and vivek’s perl version which uses some add-on kernel modules – both of which have optimizations on initial loading and performance of the tables themselves as the rules are applied so as to minimize impact on network performance. Both scripts also maintain rules in a separate table and do not alter the base firewall settings and initialize in a few seconds

Reply

158 simplewall April 1, 2014 at 9:15 am

Intrusion Prevetion System Like Suricata Provides GeoIP block.

https://redmine.openinfosecfoundation.org/projects/suricata/wiki/GeoIP

Linux router with inline suricata performs well.

Reply

159 Steve Rowe April 25, 2014 at 8:35 pm

Hi all.
I have a few questions with regards the scripts listed.

1. How to i stop and reload the scripts properly. or do i just rerun the script?
2. i take it that the last version of the script is the most up to date and bug free?

and 3. Thanks very very much for the time and effort to everyone who has contributed to the thread. I very much appreciate it.

Steve

Reply

160 daslicht July 9, 2014 at 10:18 am

How about block all Countries and allow just the ones you like to allow instead of blacklisting you dont want ? I bet the whitelist would be shorter :D
Possible?

Reply

161 tg July 9, 2014 at 4:36 pm

Just use iptables with xtables-addons and the xt_geoip module. You can block by country or allow by country. Just google xtables-addons and check the instructions for the geoip module.

Reply

162 Sakis July 11, 2014 at 5:58 pm

Is there any way to exclude specific ip’s , for example googlebot or others crawlers?

Reply

163 Tchevass August 21, 2014 at 12:44 pm

Looks like they are empty again today..

Reply

164 Vito Villanueva October 7, 2014 at 4:41 pm

Love this. Thanks

Reply

165 Meiler October 10, 2014 at 11:07 pm

Can we use also the new aggregated format?http://www.ipdeny.com/ipblocks/data/aggregated/
If yes, how the configuration need to be changed?

thanks
Ed

Reply

166 David Picard December 7, 2014 at 7:44 pm

Yes – the aggregated zones can and should be used – you would need to change the ipdeny base URL – defined as IPDENYDLROOT – in the script I posted for this to work.

The updated script is available at http://blog.psind.com/solutions/products/iptables-cb/

Reply

167 David Picard December 7, 2014 at 7:45 pm

BTW – That script does not have the change in the base URL – you would need to change that yourself

Reply

Leave a Comment

Tagged as: , , , , , , , , ,

Previous Faq:

Next Faq: