Skip to content

Implementing an IP blocklist with firewalld

2017 January 27
by Kirk Kosinski

In 2013 I wrote about using IP sets and iptables to block IP addresses from a blocklist provided by organizations such as OpenBL.  The Bash script I wrote for that was usable at the time, but in the intervening years many Linux distributions (including CentOS and RHEL 7) enabled firewalld by default, so I needed to update the script.

Just as a quick recap, IP sets can be useful when creating firewall rules for multiple elements of a common type.  I’m using IP addresses, but IP sets can store other things.  With an IP set, a single firewall rule can take action (e.g. accept, reject) on a large number of elements.  Imagine if you wanted to block 1,000 IPs, would you really want to have 1,000 rules showing up in firewall-cmd –list-all and iptables –list?  For the sake of simplicity I’d prefer a single rule and an IP set enables exactly that.

Initially my plan was to simply update my original Bash script to use firewall-cmd to create a appropriate direct rule instead of an iptables command.  But that would be too easy. 🙂 Also, I found that in firewalld 0.4.0 support was added for IP sets containing IPs, networks (CIDR notation), and MAC addresses, so I decided to write a new script to take advantage of that.  Since firewalld is written in Python and provides convenient, easy-to-use modules, I used Python for the new script.

The initial script, written for Python 2.7 for compatibility reasons, behaves similarly to the old and is pasted below.  It does some basic error handling and logging and avoids using temporary files.  The main difference is the IP set and firewall rule are configured through firewalld rather than ipset and iptables, respectively.  As with the previous script, multiple rules can be easily created, this time by simply adding them to the “rules” list.  As of firewalld 0.4.3 a rich rule is needed to use IP sets, so the firewalld.richlanguage man page should help.

#!/usr/bin/env python
"""blockips2.py: Block IPs with firewalld."""
from firewall.client import FirewallClient, FirewallClientIPSetSettings
from zlib import decompress
from syslog import syslog
from urllib2 import urlopen, HTTPError

__author__ = 'Kirk Kosinski'
__version__ = '0.1'

def get_ips(url):
    """Get a list of IPs from a blocklist.
    url: The URL of the block list.
    returns: List of IPs from the blocklist.
    """
    try:
        response = urlopen(url)
    except HTTPError as e:
        syslog('Download failed: HTTP Error %i' % e.code)
        return []
    except:
        syslog('Download failed.')
        return []
    try:
        ip_list_gz = response.read()
        ip_list_txt = decompress(ip_list_gz, 16).decode()
    except:
        syslog('Error decompressing file.')
        return []
    ip_list = []
    for entry in ip_list_txt.split('\n'):
        # Only add entries that are IP addresses.
        if (len(entry) > 0 and entry[0] != '#' and entry.count('.') == 3):
            ip_list.append(entry)
    return ip_list

def mk_ipset(name, type, client):
    """Make a new IP Set.
    name: The name to use.
    type: The type create.
    client: A FirewallClient.
    returns: The new IP Set.
    """
    settings = FirewallClientIPSetSettings()
    settings.setType(type)
    config = client.config()
    return config.addIPSet(name, settings)

def main():
    syslog('Starting IP blocklist script %s.' % __version__)
    # The name to use for the IP Set.
    ipset_name = 'blocklist'
    # The type of IP Set (only 'hash:ip' for now).
    ipset_type = 'hash:ip'
    # List of rules that use the ipset.
    rules = ['rule family=\"ipv4\" source ipset=\"' + ipset_name + '\" drop']
    # URL of the blocklist (gzip format).
    listurl = 'https://vm7.lab.local/blocklist.txt.gz'
    # FirewallClient to configure firewalld
    fw_client = FirewallClient()
    # The firewall zone to use (detect the default or choose one explicitly).
    zone_name = fw_client.getDefaultZone()
    iplist = get_ips(listurl)
    if (fw_client.connected and len(iplist) > 0):
        if (ipset_name in fw_client.config().getIPSetNames()):
            # ipset already exists so use it.
            ipset = fw_client.config().getIPSetByName(ipset_name)
            syslog('Using existing ipset \"%s\".' % ipset_name)
        else:
            # ipset doesn't exist so create it.
            ipset = mk_ipset(ipset_name, ipset_type, fw_client)
            syslog('Created new ipset \"%s\".' % ipset_name)
        # Reloading firewall flushes existing ipset / makes new one available.
        fw_client.reload()
        # Add the IPs to the ipset.
        syslog('Adding %i IP(s) to ipset \"%s\".' % (len(iplist), ipset_name))
        for ip in iplist:
            fw_client.addEntry(ipset_name, ip)
        # Create the rich rule(s) that use the ipset.
        syslog('Creating %i rich rule(s).' % len(rules))
        for rule in rules:
            fw_client.addRichRule(zone_name, rule)
    else:
        syslog('An error occurred.')

if (__name__ == '__main__'):
    main()

I’ve tested this successfully on CentOS 7 with Python 2.7.5 and Fedora 23 with Python 2.7.11, but it is only the initial version so no guarantees.  It needs to be run by a user that has permissions to actually make changes in firewalld (e.g. root).  Here are some commands that may be useful while working with IP sets in firewalld.

  • firewall-cmd –get-ipsets: View IP sets in the runtime (active) configuration. Add –permanent to see IP sets in the permanent configuration.
  • firewall-cmd –delete-ipset=<name>: Delete an IP set in the runtime configuration, or in the permanent configuration with –permanent.  Note that my script creates the IP set in the permanent configuration.
  • firewall-cmd –ipset=<name> –get-entries: Lists the entries in an IP set.  Note that my script only adds and removes entries from the runtime configuration, not permanent.
  • ipset list <name>: Basically the same as the above but operating directly on the IP set rather than through firewalld.
  • firewall-cmd –reload: Reloads the firewall configuration (i.e. overwrites the runtime configuration with with the permanent configuration).  When using my script this has the effect of clearing the entries from the IP set and removing any of the rich rules it created.

For more detailed information on firewall-cmd I recommend RHCSA/RHCE Red Hat Linux Certification Study Guide.  For getting started with Python I recommend the free Think Python.

Lastly, for reference, here are some sample entries the script might add in /var/log/messages.

New IP set created successfully:

Jan 27 16:24:53 cent7vm blockips2.py: Starting IP blocklist script 0.1.
Jan 27 16:24:53 cent7vm blockips2.py: Created new ipset "blocklist".
Jan 27 16:24:53 cent7vm blockips2.py: Adding 470 IP(s) to ipset "blocklist".
Jan 27 16:24:54 cent7vm blockips2.py: Creating 1 rich rule(s).

Existing IP set updated:

Jan 27 16:26:13 cent7vm blockips2.py: Starting IP blocklist script 0.1.
Jan 27 16:26:17 cent7vm blockips2.py: Using existing ipset "blocklist".
Jan 27 16:26:17 cent7vm blockips2.py: Adding 474 IP(s) to ipset "blocklist".
Jan 27 16:26:20 cent7vm blockips2.py: Creating 1 rich rule(s).

Error:

Jan 27 16:27:07 cent7vm blockips2.py: Starting IP blocklist script 0.1.
Jan 27 16:27:08 cent7vm blockips2.py: Download failed: HTTP Error 404
Jan 27 16:27:08 cent7vm blockips2.py: An error occurred.

 

2 Responses Post a comment
  1. Futur Fusionneur permalink
    March 11, 2017

    Good job Kirk ! I am trying to implement your script on my test webserver. However i get this error in messages log “firewall_reader.py: Error decompressing file.”.

    Since your link ‘https://vm7.lab.local/blacklist.txt.gz’ is broken, i used another hosts list taken from:
    http://www.openbl.org/lists/base_all.txt.gz

    Problem is the gzip file contains a folder structure: .\..\\htdocs\lists\base_all.txt .
    Since i am pretty new with python, maybe you can help me decompress this thing from the subfolder. Thanks a lot!

  2. April 1, 2017

    The vm7 host is on my internal network to avoid wasting OpenBL’s bandwidth while testing my script. I actually use an OpenBL list for my script. I guess some of their lists have a layout I didn’t expect. Unfortunately it looks like OpenBL is down right now so I will have to look into this later.

Leave a Reply

Note: You may use basic HTML in your comments. Your email address will not be published.

Subscribe to this comment feed via RSS

This site uses Akismet to reduce spam. Learn how your comment data is processed.