By default, firewalld has persistent (permanent) rules that are applied on startup, thus offering constant protection even after a reboot. The default configuration of ipset does not work the same way, lists and rules are not persistent across reboots, but this can be arranged by using a custom systemd service.

Now is a good opportunity to learn a few basic concepts about systemd. While previous rc.sysinit execution method was linear, the new systemd method is parallel loading and offers advanced transactional dependency-based service control logic. The old /etc/rc.local method of quickly running some scripts on boot is still there as a service, but it is suggested to create custom units instead.

Targets is the new runlevels. The first step to understanding systemd, is to look at targets, which is a way to describe system states. When a script requires network access, for example when a daemon like ssh binds to port 22, it will depend on the “network.target”. The network target guarantees that the network interface is ready and the daemon can actually bind to its IP address and/or port number, but does not guarantee that the system is connected to the network. The target “network-online.target” is used for services that require network access, for example when mounting network shares.

To view all (active & inactive) targets, run the following command:

systemctl list-units --type=target –all

Unit files represent services. The second step to understanding systemd, is to look at the unit files which represent the system services. The system-wide services are stored under subdirectories within /etc/systemd/system. Each subdirectory under /etc/systemd/system represents a target.

By default, system unit files are symbolic links and they usually point to /usr/lib/systemd/system/ but in practice the location is not important because we can view any unit file via systemctl cat command, for example:

systemctl cat sshd.service

Now that we have established targets and unit files, lets create the custom ipset service for saving (on shutdown) and loading (on boot) lists and rules. Create the file /etc/systemd/system/ipset.service with the following contents:

/etc/systemd/system/ipset.service

[Unit]

Description=ipset persistent rule service

Before=firewalld.service

ConditionFileNotEmpty=/etc/sysconfig/ipset

[Service]

Type=oneshot

RemainAfterExit=yes

ExecStart=/usr/sbin/ipset -exist -file /etc/sysconfig/ipset restore

ExecStop=/usr/sbin/ipset -file /etc/sysconfig/ipset save

[Install]

WantedBy=multi-user.target

The Unit section defines a global scope for this unit, including a description, requirements that must be met before firewalld has been started and a condition that our saved rules file exists and it is not empty. If we try to start firewalld without any ipset lists, then the firewalld rules related to ipset will fail to load.

The Service section defines the “oneshot” type, which tells systemd that this unit will execute and terminate without leaving a memory resident daemon or any other type (like D-Bus service). The remain after exit tells systemd that this service will be considered “running” once it has executed, thus we can manipulate it eventually with “stop”, manually or on halt/reboot. The ExecStart and ExecStop parameters define the two commands to ipset for saving and loading rules.

The Install section defines a target which is, in this case, the multi-user target. This means two things: the ipset service will be able to start/stop via the systemctl command and will also start/stop when the multi-user target does so.

Finally, we can now reload the changes in systemd, enable the ipset service and also generate a current saved state:

systemctl daemon-reload

systemctl enable ipset

/usr/sbin/ipset -file /etc/sysconfig/ipset save

Our script gives the reader some basic understanding of systemd, while solving a common issue with ipset. It is possible to write a single service that defines multiple ExecStart commands, thus emulating the effects of the old rc.local script, but beware that systemd runs services in parallel based on their After/Before requirements, thus it is possible to get into synchronisation problems. A quick solution would be to create a custom target that runs after multi-user and guarantee execution at the end of the boot process, but that is left as an exercise to the reader.