Geoblocking with Linux and FirewallD

Sunday 08 June 2025


While I tend to avoid exposing services to the internet if only I am going to use them (I have a vpn for that), it sometimes makes sense. For the most part, I protect these a little further using PFBlockerNG on my PfSense firewall to block traffic from some countries, and lists of known malicious sources. Where this gets a little more difficult is the few services I have hosted in the cloud. Of course, I could roll out some services in the cloud to perform the same function as PfSense (most likely PfSense itself), but for the tiny amount of stuff I have in the cloud it just doesn't make sense. Plus, some of what's in the cloud is there because I want it to be accessible by everyone - The main example being the server that hosts this website. As such, I wanted to find a way to use FirewallD on the Linux box itself. I honestly thought this would be a solved problem, but I found surprisingly little on the internet hence writing this up here.

Some basics on FirewallD

FirewallD is the default firewall in most modern versions of RHEL (and rebuilds such as Rocky Linux). It's built on top of IPTables but offers a (slightly) easier to pick up interface, firewall-cmd. I'm far from an expert, so I won't go into huge detail. The important part is that traffic gets sorted into "zones", either by source IP, or interface (there are other options, but those are the important ones here). From there, a zone contains rules about which services are allowed.

The Theory

So, we need to create a zone within FirewallD that catches any traffic coming from our "bad" IP ranges. For small sets of IPs, that's easy enough to do:

$ firewall-cmd --permanent --zone=drop --add-source=10.0.0.0/8

But that doesn't help us when we want to block potentially thousands of IP ranges, and keep those ranges in sync with an online list. One feature of FirewallD that will help us out here is "ipsets". These are essentially lists of CIDR ranges, or straight IP addresses that we can then assign to a zone, for example:

$ firewall-cmd --permanent --new-ipset=test --type=hash:net
$ firewall-cmd --permanent --ipset=test --add-entry=10.0.0.0/8
$ firewall-cmd --permanent --ipset=test --add-entry=172.16.1.0/24
$ firewall-cmd --permanent --zone=drop --add-source=ipset:test

The above creates an ipset called "test", adds two ranges to it, then adds it as a source to the "drop" zone. From here, we can simply update the ipset moving forward, for example if we decide to allow 172.16.1.0/24:

$ firewall-cmd --permanent --ipset=test --remove-entry=172.16.1.0/24

That's close, but still a lot of manual work. The final option I'll talk about solves that issue (mostly). We can add IP ranges using a file. For example:

$ echo '172.17.0.0/24' > /tmp/ips.txt
$ firewall-cmd --permanent --ipset=test --add-entries-from-file=/tmp/ips.txt

In Practice

So we need some code to create, and maintain an ipset. There are a few scripts floating around online, but none of them seemed to work for me. Admittedly I didn't put too much effort into figuring out why as it quickly became clear that writing my own was going to be the simpler solution. My code, as always can be found on GitHub, but the basic description is:

  1. Download our blocklist
  2. Compare that list to our existing ipset
  3. Work out what needs to be added, and what needs to be removed
  4. Generate text files for each
  5. Add/Remove items fromt he ipset
  6. Reload FirewallD if we made any changes

Results

At the time of writing, I've had the script in place blocking a few countries for around 24 hours, and in that time I can see that FirewallD blocked 33 separate IP addresses. It's worth noting that the server already has a basic firewall closing all ports except the ones I'm running services on, so those will be purely GeoIP based blocks.