Whitelisting IP Address Ranges with iptables and ipset

Whitelisting IP Address Ranges with iptables and ipset

If you run a firewall you know how important it is to block out the bad guys.

*** INITIAL POST Feb, 2012 ***

*** UPDATED June, 2013 (ipset syntax changed)***

*** UPDATED Feb, 2016 - Four years later and still my favorite tool! (blog formatting fixes, updated php script, removed references to compiling from source)***

Many people have taken to blocking out certain country netblocks (say, China for some reason) at their firewall. Of course there are ways around this for the determined attacker, but it's a good first line of defense.

A better approach in security is to whitelist rather than blacklist. I try to always use a policy of "block everything, allow through certain things". You can never think of everything to block so a policy of "allow everything, block certain things" just isn't good practice.

The same is true of IP address blocks in my opinion. In my business there is absolutely no reason for anyone outside of the USA or Canada to talk to my servers. I'm sure there are millions of people in China (or Europe or Japan for that matter) who are really nice and would never try to attack or spam me, but why should I even give them the chance if there's no legitimate reason for them to contact my server?

Managing a blacklist of a huge volume in iptables is a problem. Even managing a small whitelist is a problem.

Enter "ipset".

With ipset you can specify a set of ip addresses that you can then use as matches in iptables rules. The net result is that your iptables ruleset is tiny by comparison, and you can make changes to one without affecting or redoing the other. ipset works hand in hand with iptables. The one thing about it is that ip blocks are constantly changing, so you need to update your ipset scripts every few months.

Installing ipset

First though, you need to install ipset.

sudo apt-get update
sudo apt-get install ipset

Once installed, verify it works by issuing the following:

ipset create testme hash:net
ipset destroy testme

Both commands should return with no errors.

Grab some zone lists

Now we need to populate it. A great source of netblocks is at www.ipdeny.com You can download either a single country or the entire list in one big gz file. I found that the aggregated zone files did not whitelist all IP's as expected, so I recommend using the larger zone files.

To turn those zone files into a script I could run I wrote the following PHP script. I'm sure you could do this with a bash script or python or something, I was just more familiar with PHP. Run this script from the console in the directory with the zone files you want to use.

 <?php
 
 // must rename zones after download to country.zone
 $files = glob("*.zone");
 $of = fopen('out.sh','w');
  
 fwrite($of,'#!/bin/sh' . "\n");
 fwrite($of,"# Get zone files from http://www.ipdeny.com/ipblocks/\n");
 fwrite($of,"# Script generator written by Anthony Maro\n");
 fwrite($of,"# https://www.ossramblings.com/whitelisting-ipaddress-with-iptables-ipset\n");
 fwrite($of,'IPSET="/sbin/ipset"' . "\n");
 foreach ($files as $file) {
   echo "Working on $file\n";
   $i = 0;
   $if = file($file);
   $info = pathinfo($file);
   $country = $info['filename'];
   $zone = "zone-" . $country;
   fwrite($of,"echo \"Allowing $country\"\n");
   fwrite($of,'$IPSET create ' . $zone . " hash:net\n");
   fwrite($of,'$IPSET flush ' . $zone . "\n");
   foreach($if as $line) {
     $line = trim($line);
     fwrite($of, '$IPSET ' . "add $zone $line\n");
     $i++;
     if ($i > 30000) {
       $i = 0;
       echo "splitting $country\n";
       $zone = "zone-" . $country . "-2";
       fwrite($of,"echo \"Splitting $zone\"\n");
       fwrite($of,'$IPSET ' . "create $zone hash:net\n");
       fwrite($of,'$IPSET ' . "flush $zone\n");
     }
   }
 }
 fwrite($of,"echo \"Countries allowed.\"\n");
 fclose($of);
  
 ?>

This will generate a shell script called "out.sh" that looks something like:

#!/bin/sh
IPSET="/sbin/ipset"
echo "Allowing ca"
$IPSET create zone-ca hash:net
$IPSET flush zone-ca
$IPSET add zone-ca 23.16.0.0/15
$IPSET add zone-ca 24.36.0.0/16
$IPSET add zone-ca 24.37.0.0/16
$IPSET add zone-ca 24.38.144.0/20
$IPSET add zone-ca 24.48.0.0/17
$IPSET add zone-ca 24.48.176.0/20
...

If a zone gets split into multiple sets, it names the extra ones numerically. For instance, US gets split into "zone-us" and "zone-us-2". I found this splitting necessary for US in Ubuntu Lucid, but the newer Oneiric didn't really need the split. All the documentation I found said it should handle 65535 entries per set, but Lucid would complain before hitting the end of the zone. I arbitrarily picked 30,000 entries as the split point.

Take that output script and have your server run it at every boot by wedging it into the end of /etc/rc.local for instance. This makes sure the set of IP addresses is always loaded up and ready for iptables to use. You do not need to run this every time you restart your firewall script because they are stored separately from the iptables rules.

Use it as a whitelist in iptables

Many people would stop here and just block based on the groups of IP addresses in the sets. I want to reverse that logic.

In order to do a whitelist instead of a blacklist we simply create a new chain to hold the checks, and return from the chain if it passes. NOTE, this is NOT a complete firewall script. I'm simply demonstrating how to wedge this into your own firewall:

iptables -N countryfilter
iptables -A INPUT -i eth0 -p tcp -m state --state NEW -j countryfilter
iptables -A countryfilter -m set --set zone-ca src -j RETURN
iptables -A countryfilter -m set --set zone-us src -j RETURN
iptables -A countryfilter -m set --set zone-us-2 src -j RETURN
iptables -A countryfilter -j DROP

Here you see I've added a chain and I send all new TCP connections into it. Obviously this would need tweaked if you want to filter UDP as well. Once in the chain it checks with ipset to see if the ip address is in the list. If so, it returns from the chain, allowing the remaining of your firewall rules after this snippet to continue running and filtering on the packet. If it is NOT found in the chain, the end of the chain will drop the packet.

A very elegant solution to whitelisting ip address ranges.

Posted by Tony on Feb 25, 2016 | Servers, Network Security, networking