≡ Menu

Linux Iptables Just Block By Country

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:

Tweet itFacebook itGoogle+ itPDF itFound an error/typo on this page?

{ 172 comments… add one }

  • Josh March 2, 2009, 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!

  • Xaero March 3, 2009, 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

  • Sudanking March 3, 2009, 8:36 am

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

  • Avin March 3, 2009, 9:11 am

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

  • piavlo March 3, 2009, 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

  • Shantanu Oak March 3, 2009, 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`

  • ajp March 3, 2009, 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.

    • Herb B. D. Derbidy September 6, 2010, 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.

      • David Picard September 7, 2010, 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

        • David Picard February 19, 2011, 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

  • nixCraft March 3, 2009, 2:17 pm

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

  • Johan March 3, 2009, 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?

  • Paul March 3, 2009, 5:40 pm

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

  • Paul March 3, 2009, 5:41 pm

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

  • TwinReverb March 3, 2009, 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.

  • Johannes March 4, 2009, 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.

  • incidence March 10, 2009, 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
    
  • Cobro March 11, 2009, 7:57 am

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

  • incidence March 11, 2009, 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

    • cmanley November 30, 2010, 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

  • hel March 17, 2009, 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

    • nig December 29, 2010, 9:49 pm

      Stfu you tool.

    • Diego Sánchez March 4, 2012, 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

  • john April 3, 2009, 5:39 am

    this post is very useful.
    Keep it up !

  • VB April 21, 2009, 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.

  • Ahmed June 27, 2009, 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?

  • David July 18, 2009, 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).

  • nixCraft July 18, 2009, 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/

  • David July 19, 2009, 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.

  • Paul August 2, 2009, 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!

  • Bremm October 6, 2009, 10:42 am

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

  • Chris DE October 28, 2009, 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.

  • Rick November 25, 2009, 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?

  • Jay January 12, 2010, 4:43 pm

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

  • Dave January 28, 2010, 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!

  • David Picard January 28, 2010, 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
  • David Picard January 28, 2010, 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 ;-)

  • David Picard February 5, 2010, 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
  • David Picard February 5, 2010, 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 ;-)

  • nixCraft February 5, 2010, 2:46 pm

    @David,

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

  • IzFazt February 5, 2010, 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!

  • David Picard February 8, 2010, 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.

  • David Picard February 25, 2010, 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
    
  • Mikael Engström March 6, 2010, 6:25 pm

    Would this script alter the existing ip-tables settings?

  • David Picard March 7, 2010, 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.

  • Nilesh April 9, 2010, 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

  • David Picard April 9, 2010, 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.

  • Nilesh April 9, 2010, 4:18 pm

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

  • David Picard April 9, 2010, 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.

  • M Agg April 20, 2010, 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.

  • David Picard April 20, 2010, 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.

    • M Agg April 20, 2010, 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).

  • David Picard April 20, 2010, 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

  • M Agg April 21, 2010, 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.

  • M Agg April 21, 2010, 2:50 am

    Here are some IP’s

    117.41.168.235, 119.147.116.157, 119.147.116.158

  • Chris April 23, 2010, 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

  • David Picard April 23, 2010, 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 ;-)

    • Chris April 23, 2010, 4:10 pm

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

      Thanks David! Kudos to you sir!

      -Chris

  • Travis Mayne April 26, 2010, 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…

  • David Picard April 29, 2010, 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

    • Mike August 19, 2010, 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!

  • FK May 25, 2010, 5:02 pm

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

  • Aaron July 6, 2010, 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?

  • David Picard July 6, 2010, 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.

  • Bremm July 6, 2010, 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

    • David Picard July 31, 2010, 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 ;-)

  • tlhackque September 29, 2010, 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;
    
    • nixCraft September 29, 2010, 5:51 pm

      Thanks for sharing the script with us.

  • an0nymous October 2, 2010, 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.

    • Josh December 2, 2010, 1:43 pm

      That’s why I block US entirely too.

  • Sushant Chawla November 1, 2010, 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)

  • tlhackque November 1, 2010, 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.

    • David Picard November 6, 2010, 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.

  • NachoR November 6, 2010, 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!

    • tlhackque November 6, 2010, 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.

      • David Picard November 6, 2010, 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.

        • tlhackque November 6, 2010, 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!

          • David Picard November 6, 2010, 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.

            • tlhackque November 6, 2010, 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.

              • tlhackque November 6, 2010, 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.

  • nachoR November 6, 2010, 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.

    • tlhackque November 7, 2010, 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;
      
      • tlhackque November 7, 2010, 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

        • tlhackque November 7, 2010, 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!

  • David Picard November 6, 2010, 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.

    • David Picard November 7, 2010, 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.

  • tlhackque November 7, 2010, 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.

  • IzFazt November 7, 2010, 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

    • tlhackque November 8, 2010, 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.

  • tlhackque November 8, 2010, 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.

  • tlhackque November 8, 2010, 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.

  • nixCraft November 8, 2010, 9:05 pm

    @tlhackque / David,

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

  • PacoSS November 9, 2010, 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.

    • tlhackque November 9, 2010, 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.

  • David Picard November 9, 2010, 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.

    • PacoSS November 9, 2010, 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.

  • tlhackque November 9, 2010, 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.

  • David Picard November 10, 2010, 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.

  • Valerio January 7, 2011, 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?

  • Nilesh January 9, 2011, 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.

  • David Picard February 19, 2011, 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.

  • Søren March 14, 2011, 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)…

  • Cameron November 5, 2011, 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 ?

    • Cameron November 6, 2011, 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?

  • milano94 November 20, 2011, 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?

  • Shaun December 9, 2011, 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?

  • DungX Mokahd February 24, 2012, 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

  • Joern April 10, 2012, 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.

    • David Picard April 13, 2012, 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.

    • George November 27, 2012, 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.

  • Ramon October 12, 2012, 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

  • Lee December 15, 2012, 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!

  • Giuseppe Urso January 2, 2013, 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.

  • Alan January 9, 2013, 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

    • David Picard January 13, 2013, 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.

      • Alan January 14, 2013, 11:39 am

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

  • Jouni "rautamiekka" Järvinen March 28, 2013, 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.

    • David Picard March 29, 2013, 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.

  • Joseph July 6, 2013, 2:38 am

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

    http://pastebin.com/NDTZdjBH

    • David Picard July 6, 2013, 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.

      • Joseph July 6, 2013, 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.

        • David Picard July 6, 2013, 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.

          • Joseph July 6, 2013, 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.

            • Glen Schaefer August 2, 2013, 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?

  • Glen Schaefer July 16, 2013, 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?

  • tlhackque July 26, 2013, 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.

  • Glen Schaefer July 31, 2013, 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’

  • tlhackque August 2, 2013, 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.

    • nixCraft August 2, 2013, 10:40 am

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

  • Glen Schaefer August 2, 2013, 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.

  • IronNighthawk August 13, 2013, 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?

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

      What are the kernel versions from to ?

      • IronNighthawk August 14, 2013, 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.

        • IronNighthawk August 14, 2013, 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…

          • David Picard August 15, 2013, 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?

            • IronNighthawk August 15, 2013, 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.

  • Geoff Jackson August 27, 2013, 12:32 pm

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

    (if even possible at all).

    Thanks.

    • nixCraft August 27, 2013, 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
      
      • Geoff Jackson August 27, 2013, 2:04 pm

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

    • David Picard August 27, 2013, 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.

      • Geoff Jackson August 27, 2013, 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.

  • David Picard August 27, 2013, 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.

    • Geoff Jackson August 28, 2013, 12:48 pm

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

    • Geoff Jackson August 28, 2013, 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… :(

  • tlhackque September 4, 2013, 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.

  • antiplex November 8, 2013, 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 ;)

    • David Picard March 2, 2014, 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"
      
  • stingray November 11, 2013, 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

  • Dimitrios March 1, 2014, 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
    
    • David Picard March 2, 2014, 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

  • simplewall April 1, 2014, 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.

  • Steve Rowe April 25, 2014, 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

  • daslicht July 9, 2014, 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?

    • tg July 9, 2014, 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.

  • Sakis July 11, 2014, 5:58 pm

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

  • Tchevass August 21, 2014, 12:44 pm

    Looks like they are empty again today..

  • Vito Villanueva October 7, 2014, 4:41 pm

    Love this. Thanks

  • Meiler October 10, 2014, 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

  • sidbyron April 25, 2015, 5:25 pm

    I have a question. If I have fial2ban rules will function of cleanOldRules delete the fail2ban rules??

    Kind regards.

  • lotusppl May 14, 2015, 6:00 pm

    How does one install the script on CentOS 6? The one on psind.com.

  • Christian Bach July 1, 2015, 7:09 am

    Hi,
    I really like your site, it has helped me countless times. :) Good stuff.

    We have a sites that only deal with western and nordic countries, all other traffic can easily be blocked. However to be sure we do not drop traffic anybody we do want to do business with, is it possible to forward all dropped http traffic on ports 443 and 80 to a specific site and port where we can have a default nginx page that explains that they have been geo blocked and to contact us if they need access. In which case we can specifically allow a certain IP on these ports.

    I hope someone can point me in the right direction for solving this. :)

  • Alex Litonjua July 12, 2015, 8:48 pm

    Re: Problems Running perl (CountryBlock) script on boot-up (start-up) in debian linux.
    Hi,
    Both of the programs of blocking countries are excellent jobs, But, I prefer running perl script in linux.
    When I run perl script in the terminal mode, I had no problem and it run perfectly, however, when i tried to run in cron job, it doesn’t run although I set it up in the /etc/init.d/program-names with the chmod 755 or chmod +x program-names.

    Would someone put me in the right directions how I can run perl BlocKCountries during start-up or boot-up in linux debian. Your help and assistance is much appreciated. Thank you.

    • Debian User July 15, 2015, 3:19 pm

      @Alex, did you try “sudo update-rc.d BlockCountries enable” to register the script with the init system? The cron job is supposed to run weekly and look something like this: “11 23 * * Sun /etc/init.d/BlockCountries start -update”

Leave a Comment