Block entire countries at the IP firewall, by using ipset and firewalld.

By Dimitris Dimitropoulos

We are not going to discuss if country blocking is efficient, ethical or useful, we assume the reader needs to block some countries which generate more offensive traffic than actual user traffic. In the process we will learn how to use ipset running underneath netfilter and the much controversial firewalld.

Some basic understanding of the firewall system is needed. The Linux kernel is using netfilter as an IP firewall and in the “old days” the userspace iptables was the default way of manipulating netfilter rules, which is why people wrongly think that iptables is the actual firewall. What was always an issue, is the fact that iptables requires considerable work to manipulate active rules (it will flush the entire rule chain for a single change), thus an alternative system was developed, now known as the firewalld.

The new firewalld system uses a Python daemon that allows us to actively monitor, modify and handle the netfilter rules via the kernelspace ip_tables, and this is done via the userspace firewall-cmd command. The new architecture designed along with NetworkManager is vastly different. It runs two environments, a “permanent” environment that remains set after a reboot of the server and a “runtime” environment which is currently active and running.

Underneath those two environments we have zones, by default these are: block, dmz, drop, external home, internal, public, trusted, work. Each zone may have it's own set of rules and the administrator may actively switch from one zone to another to activate different rules without actually doing any rule modifications. Our geoblocking will use the default active zone and set rules for both permanent and runtime environments.

The following piece of bash coding is the start of our script. We define a set of variables with sensible default values, like a temporary directory, the URL that contains our country IP zones, a temporary download location and the default name of our ipset list. We check for proper creation of the temporary directory before allowing the script to continue.

# the start

# temporary directory

# zone ipv4

# directories
rm -rf "$DIR/zones" 2>/dev/null
mkdir "$DIR/zones" 2>/dev/null
mkdir "$DIR/zones/4" 2>/dev/null

# check
if [ ! -d "$DIR/zones/4" ]; then

        echo "[ERROR] Directory not found: $DIR/zones/4"



The first step, is to download a list of IP net ranges per country. This is offered for free, as already defined in our ZONESIPV4 variable. We then proceed to unarchive the rules in the temporary directory. Note the tar parameters, which help secure our system from malicious archive content.

# download ipv4 archive
echo "[CURL] Downloading: $ZONESIPV4 to $IPV4GZ"
curl "$ZONESIPV4" -o "$IPV4GZ"

# check download
if [ ! -f "$IPV4GZ" ]; then

        echo "[ERROR] Error downloading file"


# unarchive ipv4
echo "[TAR] unarchive IPv4 zones"
tar --no-recursion --absolute-names --restrict --no-same-owner --no-same-permissions -zxf "$IPV4GZ" -C "$DIR/zones/4"


The second step, is to remove existing firewall rules and ipset lists that might have already been set by this script. Our method temporarily leaves the system open, but since this is a low risk issue we may ignore it. As an exercise to the reader, one could update the script to keep the firewall rules live while updating the ipset list.

# check for ipv4 firewall rule
rule=$( firewall-cmd --direct --query-rule ipv4 filter INPUT 2 -m set --match-set $IPSETLIST4 src -j DROP )
if [ $rule = "yes" ]; then

        echo "[FIREWALL-CMD] Remove firewall rule for ipset list: $IPSETLIST4"
        firewall-cmd --direct --remove-rule ipv4 filter INPUT 2 -m set --match-set $IPSETLIST4 src -j DROP


# delete old ipset lists
echo "[IPSET] Delete old $IPSETLIST4 list"
ipset destroy $IPSETLIST4 2>/dev/null


The third step, is to create a new ipset list and populate it by iterating the country zones. The reader should take extra care with the ipset parameters, in this case, we use a hash for network addresses with a hashsize 4096 and a maximum size of 200000 rules. The timeout is very important, it defines the expiration lifetime of each rule, zero timeout means that rules never expire.

The “for” loop defines an array of country TLD (top level domains), the reader is required to set his own list of countries based on his requirements. By default we list the top 20 most offending countries.

# create new ipset lists
echo "[IPSET] Create new $IPSETLIST4 list"
ipset create $IPSETLIST4 hash:net family inet hashsize 4096 maxelem 200000 timeout 0

# populate ipv4
echo -n "[IPSET] Populate $IPSETLIST4 list:"
for country in ar bg br by cn il in ir kp ly mn mu pa sd tw ua ro ru ve vn; do

        if [ -f "$DIR/zones/4/$" ]; then

                echo -n " $country"
                for ip in $( cat "$DIR/zones/4/$" ); do
                        ipset add $IPSETLIST4 $ip

echo " "


The forth and final step, is to create the firewall rules that will block access based on the above ipset list. The first check to make sure a permanent rule exists and if not then we create one, then we proceed to create the runtime rule.

While the new and improved firewall-cmd allows us to manipulate the kernelspace ip_tables rules, via a simple command line, some things are still missing from the implementation. Which is what the “--direct” parameter is all about, it defines an old style iptables command.

# check for ipv4 firewall rule
echo "[FIREWALL-CMD] Create firewall rule for ipset list: $IPSETLIST4"
rule=$( firewall-cmd --permanent --direct --query-rule ipv4 filter INPUT 2 -m set --match-set $IPSETLIST4 src -j DROP )
if [ $rule = "no" ]; then

        firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 2 -m set --match-set $IPSETLIST4 src -j DROP

firewall-cmd --direct --add-rule ipv4 filter INPUT 2 -m set --match-set $IPSETLIST4 src -j DROP

# clean up
rm -rf "$DIR/zones" 2>/dev/null

# the end


Our script gives the reader some basic functionality for geoblocking by using firewalld, ipset and netfilter rules. The script uses the IPv4 family but it can be easily modified to also use the IPv6 family.

View epilis's profile on LinkedIn Visit us on facebook Twitter epilis rss feed: Latest articles