Using ipsets to clean up your iptables rules and increase performance

In Linux, ipsets are structures inside kernel space that can be used to tidy up long lists of iptables rules and increase performance. They support a number of content types, but we will be looking at the two most basic and commonly useful: IP addresses and networks.

The problem, simple version

We frequently find ourselves (at least I have) managing growing lists of iptables rules that show no sign of finalizing. For example: a remotely hosted server that initially only needed ssh access from one or two addresses, but ends up supporting dozens over time. We might start with a FILTER INPUT chain that contains something like:

-A INPUT -s MAIN_OFFICE_IP/32 -i eth0 -p tcp –dport 22 -m comment –comment “accept ssh from the main office” -j ACCEPT
-A INPUT -s ALT_OFFICE_IP/32 -i eth0 -p tcp –dport 22 -m comment –comment “accept ssh from the alternate office” -j ACCEPT

But then we have a few key employees that we may wish to grant access to their home IPs for ease of incident response, and maybe there are some partners that also should have access. In most situations this isn’t a list that would grow to be out of control, but we can make the whole thing much tidier and easier to manage with an ipset.

To start we would need to make two decisions: what to name the set, and if it should be limited to single IP addresses or networks. To play it safe we could define the set for networks but use /32 masks to limit matches to single hosts. So, we start by creating the set

# ipset create SSH_SOURCES hash:net


Here the set is named SSH_SOURCES (I usually use all caps as a convention, but it is not required) and contains network addresses. To see all ipsets currently defined and the contents of our set:

# ipset list -n
SSH_SOURCES
# ipset list SSH_SOURCES
Name: SSH_SOURCES
Type: hash:net
Revision: 6
Header: family inet hashsize 1024 maxelem 65536
Size in memory: 344
References: 0
Number of entries: 0
Members:

Now we can add some hosts to our set:

# ipset add SSH_SOURCES 192.168.200.199/32
# ipset add SSH_SOURCES 172.16.1.1/32
# ipset list SSH_SOURCES
Name: SSH_SOURCES
Type: hash:net
Revision: 6
Header: family inet hashsize 1024 maxelem 65536
Size in memory: 472
References: 0
Number of entries: 2
Members:
192.168.200.199
172.16.1.1

Because we created this set as a type of hash:net, and not hash:ip, we can also define network ranges larger than /32:

# ipset add SSH_SOURCES 192.168.32.0/24
# ipset list SSH_SOURCES
Name: SSH_SOURCES
Type: hash:net
Revision: 6
Header: family inet hashsize 1024 maxelem 65536
Size in memory: 536
References: 0
Number of entries: 3
Members:
192.168.200.199
192.168.32.0/24
172.16.1.1

Now that we have our set defined, we can change our iptables rule to:

-A INPUT -i eth0 -p tcp –dport 22 –m set –match-set SSH_SOURCES src -m comment –comment “accept ssh from the SSH_SOURCES set” -j ACCEPT

This rule states that incoming traffic via interface eth0 on TCP port 22 where the source is in the SSH_SOURCES set will be accepted.

With this rule in place we can freely add and remove items from our set to grant or revoke ssh access without reloading our iptables rules. Changes to the ipset are effective immediately, but remember that a prior match state on RELATED,ESTABLISHED will prevent existing connections from being terminated when an ipset entry is deleted.

Performance impact (the more complicated version)

 

With our sample above, there wouldn’t be a substantial performance impact, but think about a situation where an iptables action needs to be taken based on matching hundreds or even thousands of sources and/or destinations. Consider a large block list where traffic should be denied or redirected based on the list. If they were all spelled out with individual rules each new session would need to be evaluated against every rule to determine if it applies. A query against an ipset to determine if a source or destination matches is incredibly efficient and must only be performed once. If there is a match then take the desired action. Especially when there is a complex set of rules that must be applied, but only to a subset of traffic. The ipset match can be used to trigger a jump to that complex chain. One check, and traffic either enters the complex chain or bypasses it completely. For a busy device with many rules this can greatly reduce the overhead of processing these complex rules. For example:

-A PREROUTING -m set –match-set BIG_SET src -j COMPLEX_CHAIN

-A COMPLEX_CHAIN <many complicated things>

With the match set as the entry to our COMPLEX_CHAIN we make a single match on the source against the BIG_SET. Traffic not matching will move on and not be slowed down by the contents of PREROUTING COMPLEX_CHAIN.

Initiating at boot time for CentOS

In order for our iptables and ipsets to load at boot time on CentOS 7 we install the appropriate services:

# yum install ipset-service iptables-services

Services is pluralized for iptables as there are distinct services for IPv4 and IPv6. Ipset is singular as it covers both. I prefer using iptables to firewalld, so I disable and enable the appropriate services:

# systemctl stop firewalld
# systemctl disable firewalld
# systemctl enable iptables
# systemctl enable ipset

The iptables and ipset services will look for two files at boot: /etc/sysconfig/iptables and /etc/sysconfig/ipset. Once we have built our sets, we use ipset save:

# ipset save > /etc/sysconfig/ipset

Generally, I build my iptables as a file and load them with iptables-restore. If you have been building them in memory then save them with:

# iptables-save > /etc/sysconfig/iptables

IPv6 things

IPv6 is supported in much the same way as v4. Creating a set without indicating a version will default to v4. To create a set for IPv6:

 

# ipset create TEST hash:ip family inet6


And, although there are separate systemd units for iptables and ip6tables, there is only one for both types of ipsets.

Further reading

Ipsets can be used to store more than just IP addresses and networks, see ‘ipset help’ for more details for the version you are running, or the official project man page.