diff --git a/SOURCES/0047-feat-config-add-CleanupModulesOnExit-configuration-o.patch b/SOURCES/0047-feat-config-add-CleanupModulesOnExit-configuration-o.patch
new file mode 100644
index 0000000..fdccfe5
--- /dev/null
+++ b/SOURCES/0047-feat-config-add-CleanupModulesOnExit-configuration-o.patch
@@ -0,0 +1,302 @@
+From fb11903b8efd287f72e634fb8a4b4ff2034151fe Mon Sep 17 00:00:00 2001
+From: Paul Laufer <50234787+refual@users.noreply.github.com>
+Date: Fri, 27 Nov 2020 12:23:11 +0100
+Subject: [PATCH 47/48] feat(config): add CleanupModulesOnExit configuration
+ option
+
+Fixes: rhbz 1520532
+Fixes: #533
+Closes: #721
+(cherry picked from commit 152a51537a7840afd0879ab4b60178bef4ec16a2)
+---
+ config/firewalld.conf | 9 +++++++-
+ doc/xml/firewalld.conf.xml | 11 ++++++++++
+ doc/xml/firewalld.dbus.xml | 9 ++++++++
+ src/firewall/config/__init__.py.in | 1 +
+ src/firewall/core/fw.py | 29 +++++++++++++++++++-------
+ src/firewall/core/io/firewalld_conf.py | 19 +++++++++++++----
+ src/firewall/server/config.py | 23 +++++++++++++-------
+ src/tests/dbus/firewalld.conf.at | 2 ++
+ 8 files changed, 82 insertions(+), 21 deletions(-)
+
+diff --git a/config/firewalld.conf b/config/firewalld.conf
+index a0556c0bbf5b..3abbc9c998c1 100644
+--- a/config/firewalld.conf
++++ b/config/firewalld.conf
+@@ -7,10 +7,17 @@ DefaultZone=public
+
+ # Clean up on exit
+ # If set to no or false the firewall configuration will not get cleaned up
+-# on exit or stop of firewalld
++# on exit or stop of firewalld.
+ # Default: yes
+ CleanupOnExit=yes
+
++# Clean up kernel modules on exit
++# If set to yes or true the firewall related kernel modules will be
++# unloaded on exit or stop of firewalld. This might attempt to unload
++# modules not originally loaded by firewalld.
++# Default: no
++CleanupModulesOnExit=no
++
+ # Lockdown
+ # If set to enabled, firewall changes with the D-Bus interface will be limited
+ # to applications that are listed in the lockdown whitelist.
+diff --git a/doc/xml/firewalld.conf.xml b/doc/xml/firewalld.conf.xml
+index 0bf4c2d4d011..dd6ffb214eb3 100644
+--- a/doc/xml/firewalld.conf.xml
++++ b/doc/xml/firewalld.conf.xml
+@@ -88,6 +88,17 @@
+
+
+
++
++
++
++
++ Setting this option to yes or true unloads all firewall-related
++ kernel modules when firewalld is stopped. The default value is no
++ or false.
++
++
++
++
+
+
+
+diff --git a/doc/xml/firewalld.dbus.xml b/doc/xml/firewalld.dbus.xml
+index d17cb8b6c1ec..466220b40b21 100644
+--- a/doc/xml/firewalld.dbus.xml
++++ b/doc/xml/firewalld.dbus.xml
+@@ -2798,6 +2798,15 @@
+
+
+
++
++ CleanupModulesOnExit - s - (rw)
++
++
++ Setting this option to yes or true unloads all firewall-related
++ kernel modules when firewalld is stopped.
++
++
++
+
+ CleanupOnExit - s - (rw)
+
+diff --git a/src/firewall/config/__init__.py.in b/src/firewall/config/__init__.py.in
+index 0dec7913f694..5d6d769fbf15 100644
+--- a/src/firewall/config/__init__.py.in
++++ b/src/firewall/config/__init__.py.in
+@@ -125,6 +125,7 @@ FIREWALL_BACKEND_VALUES = [ "nftables", "iptables" ]
+ FALLBACK_ZONE = "public"
+ FALLBACK_MINIMAL_MARK = 100
+ FALLBACK_CLEANUP_ON_EXIT = True
++FALLBACK_CLEANUP_MODULES_ON_EXIT = False
+ FALLBACK_LOCKDOWN = False
+ FALLBACK_IPV6_RPFILTER = True
+ FALLBACK_INDIVIDUAL_CALLS = False
+diff --git a/src/firewall/core/fw.py b/src/firewall/core/fw.py
+index 3eb54e37ab5c..4171697bdb94 100644
+--- a/src/firewall/core/fw.py
++++ b/src/firewall/core/fw.py
+@@ -105,12 +105,13 @@ class Firewall(object):
+ self.__init_vars()
+
+ def __repr__(self):
+- return '%s(%r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r)' % \
++ return '%s(%r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r, %r)' % \
+ (self.__class__, self.ip4tables_enabled, self.ip6tables_enabled,
+ self.ebtables_enabled, self._state, self._panic,
+ self._default_zone, self._module_refcount, self._marks,
+- self.cleanup_on_exit, self.ipv6_rpfilter_enabled,
+- self.ipset_enabled, self._individual_calls, self._log_denied)
++ self.cleanup_on_exit, self.cleanup_modules_on_exit,
++ self.ipv6_rpfilter_enabled, self.ipset_enabled,
++ self._individual_calls, self._log_denied)
+
+ def __init_vars(self):
+ self._state = "INIT"
+@@ -120,6 +121,7 @@ class Firewall(object):
+ self._marks = [ ]
+ # fallback settings will be overloaded by firewalld.conf
+ self.cleanup_on_exit = config.FALLBACK_CLEANUP_ON_EXIT
++ self.cleanup_modules_on_exit = config.FALLBACK_CLEANUP_MODULES_ON_EXIT
+ self.ipv6_rpfilter_enabled = config.FALLBACK_IPV6_RPFILTER
+ self._individual_calls = config.FALLBACK_INDIVIDUAL_CALLS
+ self._log_denied = config.FALLBACK_LOG_DENIED
+@@ -232,6 +234,13 @@ class Firewall(object):
+ log.debug1("CleanupOnExit is set to '%s'",
+ self.cleanup_on_exit)
+
++ if self._firewalld_conf.get("CleanupModulesOnExit"):
++ value = self._firewalld_conf.get("CleanupModulesOnExit")
++ if value is not None and value.lower() in [ "yes", "true" ]:
++ self.cleanup_modules_on_exit = True
++ log.debug1("CleanupModulesOnExit is set to '%s'",
++ self.cleanup_modules_on_exit)
++
+ if self._firewalld_conf.get("Lockdown"):
+ value = self._firewalld_conf.get("Lockdown")
+ if value is not None and value.lower() in [ "yes", "true" ]:
+@@ -667,11 +676,15 @@ class Firewall(object):
+ self.__init_vars()
+
+ def stop(self):
+- if self.cleanup_on_exit and not self._offline:
+- self.flush()
+- self.ipset.flush()
+- self.set_policy("ACCEPT")
+- self.modules_backend.unload_firewall_modules()
++ if not self._offline:
++ if self.cleanup_on_exit:
++ self.flush()
++ self.ipset.flush()
++ self.set_policy("ACCEPT")
++
++ if self.cleanup_modules_on_exit:
++ log.debug1('Unloading firewall kernel modules')
++ self.modules_backend.unload_firewall_modules()
+
+ self.cleanup()
+
+diff --git a/src/firewall/core/io/firewalld_conf.py b/src/firewall/core/io/firewalld_conf.py
+index 7c7092120676..70258400ef06 100644
+--- a/src/firewall/core/io/firewalld_conf.py
++++ b/src/firewall/core/io/firewalld_conf.py
+@@ -28,10 +28,11 @@ from firewall import config
+ from firewall.core.logger import log
+ from firewall.functions import b2u, u2b, PY2
+
+-valid_keys = [ "DefaultZone", "MinimalMark", "CleanupOnExit", "Lockdown",
+- "IPv6_rpfilter", "IndividualCalls", "LogDenied",
+- "AutomaticHelpers", "FirewallBackend", "FlushAllOnReload",
+- "RFC3964_IPv4", "AllowZoneDrifting" ]
++valid_keys = [ "DefaultZone", "MinimalMark", "CleanupOnExit",
++ "CleanupModulesOnExit", "Lockdown", "IPv6_rpfilter",
++ "IndividualCalls", "LogDenied", "AutomaticHelpers",
++ "FirewallBackend", "FlushAllOnReload", "RFC3964_IPv4",
++ "AllowZoneDrifting" ]
+
+ class firewalld_conf(object):
+ def __init__(self, filename):
+@@ -75,6 +76,7 @@ class firewalld_conf(object):
+ self.set("DefaultZone", config.FALLBACK_ZONE)
+ self.set("MinimalMark", str(config.FALLBACK_MINIMAL_MARK))
+ self.set("CleanupOnExit", "yes" if config.FALLBACK_CLEANUP_ON_EXIT else "no")
++ self.set("CleanupModulesOnExit", "yes" if config.FALLBACK_CLEANUP_MODULES_ON_EXIT else "no")
+ self.set("Lockdown", "yes" if config.FALLBACK_LOCKDOWN else "no")
+ self.set("IPv6_rpfilter","yes" if config.FALLBACK_IPV6_RPFILTER else "no")
+ self.set("IndividualCalls", "yes" if config.FALLBACK_INDIVIDUAL_CALLS else "no")
+@@ -135,6 +137,15 @@ class firewalld_conf(object):
+ config.FALLBACK_CLEANUP_ON_EXIT)
+ self.set("CleanupOnExit", "yes" if config.FALLBACK_CLEANUP_ON_EXIT else "no")
+
++ # check module cleanup on exit
++ value = self.get("CleanupModulesOnExit")
++ if not value or value.lower() not in [ "no", "false", "yes", "true" ]:
++ if value is not None:
++ log.warning("CleanupModulesOnExit '%s' is not valid, using default "
++ "value %s", value if value else '',
++ config.FALLBACK_CLEANUP_MODULES_ON_EXIT)
++ self.set("CleanupModulesOnExit", "yes" if config.FALLBACK_CLEANUP_MODULES_ON_EXIT else "no")
++
+ # check lockdown
+ value = self.get("Lockdown")
+ if not value or value.lower() not in [ "yes", "true", "no", "false" ]:
+diff --git a/src/firewall/server/config.py b/src/firewall/server/config.py
+index 031ef5d1afaa..8815920c6893 100644
+--- a/src/firewall/server/config.py
++++ b/src/firewall/server/config.py
+@@ -100,6 +100,7 @@ class FirewallDConfig(slip.dbus.service.Object):
+ dbus_introspection_prepare_properties(self,
+ config.dbus.DBUS_INTERFACE_CONFIG,
+ { "CleanupOnExit": "readwrite",
++ "CleanupModulesOnExit": "readwrite",
+ "IPv6_rpfilter": "readwrite",
+ "Lockdown": "readwrite",
+ "MinimalMark": "readwrite",
+@@ -554,9 +555,9 @@ class FirewallDConfig(slip.dbus.service.Object):
+ @dbus_handle_exceptions
+ def _get_property(self, prop):
+ if prop not in [ "DefaultZone", "MinimalMark", "CleanupOnExit",
+- "Lockdown", "IPv6_rpfilter", "IndividualCalls",
+- "LogDenied", "AutomaticHelpers", "FirewallBackend",
+- "FlushAllOnReload", "RFC3964_IPv4",
++ "CleanupModulesOnExit", "Lockdown", "IPv6_rpfilter",
++ "IndividualCalls", "LogDenied", "AutomaticHelpers",
++ "FirewallBackend", "FlushAllOnReload", "RFC3964_IPv4",
+ "AllowZoneDrifting" ]:
+ raise dbus.exceptions.DBusException(
+ "org.freedesktop.DBus.Error.InvalidArgs: "
+@@ -578,6 +579,10 @@ class FirewallDConfig(slip.dbus.service.Object):
+ if value is None:
+ value = "yes" if config.FALLBACK_CLEANUP_ON_EXIT else "no"
+ return dbus.String(value)
++ elif prop == "CleanupModulesOnExit":
++ if value is None:
++ value = "yes" if config.FALLBACK_CLEANUP_MODULES_ON_EXIT else "no"
++ return dbus.String(value)
+ elif prop == "Lockdown":
+ if value is None:
+ value = "yes" if config.FALLBACK_LOCKDOWN else "no"
+@@ -623,6 +628,8 @@ class FirewallDConfig(slip.dbus.service.Object):
+ return dbus.Int32(self._get_property(prop))
+ elif prop == "CleanupOnExit":
+ return dbus.String(self._get_property(prop))
++ elif prop == "CleanupModulesOnExit":
++ return dbus.String(self._get_property(prop))
+ elif prop == "Lockdown":
+ return dbus.String(self._get_property(prop))
+ elif prop == "IPv6_rpfilter":
+@@ -679,9 +686,9 @@ class FirewallDConfig(slip.dbus.service.Object):
+ ret = { }
+ if interface_name == config.dbus.DBUS_INTERFACE_CONFIG:
+ for x in [ "DefaultZone", "MinimalMark", "CleanupOnExit",
+- "Lockdown", "IPv6_rpfilter", "IndividualCalls",
+- "LogDenied", "AutomaticHelpers", "FirewallBackend",
+- "FlushAllOnReload", "RFC3964_IPv4",
++ "CleanupModulesOnExit", "Lockdown", "IPv6_rpfilter",
++ "IndividualCalls", "LogDenied", "AutomaticHelpers",
++ "FirewallBackend", "FlushAllOnReload", "RFC3964_IPv4",
+ "AllowZoneDrifting" ]:
+ ret[x] = self._get_property(x)
+ elif interface_name in [ config.dbus.DBUS_INTERFACE_CONFIG_DIRECT,
+@@ -706,12 +713,12 @@ class FirewallDConfig(slip.dbus.service.Object):
+ self.accessCheck(sender)
+
+ if interface_name == config.dbus.DBUS_INTERFACE_CONFIG:
+- if property_name in [ "CleanupOnExit", "Lockdown",
++ if property_name in [ "CleanupOnExit", "Lockdown", "CleanupModulesOnExit",
+ "IPv6_rpfilter", "IndividualCalls",
+ "LogDenied",
+ "FirewallBackend", "FlushAllOnReload",
+ "RFC3964_IPv4", "AllowZoneDrifting" ]:
+- if property_name in [ "CleanupOnExit", "Lockdown",
++ if property_name in [ "CleanupOnExit", "Lockdown", "CleanupModulesOnExit",
+ "IPv6_rpfilter", "IndividualCalls" ]:
+ if new_value.lower() not in [ "yes", "no",
+ "true", "false" ]:
+diff --git a/src/tests/dbus/firewalld.conf.at b/src/tests/dbus/firewalld.conf.at
+index 9fc5502a8d0b..9a04a3bd491c 100644
+--- a/src/tests/dbus/firewalld.conf.at
++++ b/src/tests/dbus/firewalld.conf.at
+@@ -17,6 +17,7 @@ dnl Verify defaults over dbus. Should be inline with default firewalld.conf.
+ DBUS_GETALL([config], [config], 0, [dnl
+ string "AllowZoneDrifting" : variant string "no"
+ string "AutomaticHelpers" : variant string "no"
++string "CleanupModulesOnExit" : variant string "no"
+ string "CleanupOnExit" : variant string "no"
+ string "DefaultZone" : variant string "public"
+ string "FirewallBackend" : variant string "nftables"
+@@ -45,6 +46,7 @@ _helper([IPv6_rpfilter], [string:"yes"], [variant string "yes"])
+ _helper([IndividualCalls], [string:"yes"], [variant string "yes"])
+ _helper([FirewallBackend], [string:"iptables"], [variant string "iptables"])
+ _helper([FlushAllOnReload], [string:"no"], [variant string "no"])
++_helper([CleanupModulesOnExit], [string:"yes"], [variant string "yes"])
+ _helper([CleanupOnExit], [string:"yes"], [variant string "yes"])
+ _helper([RFC3964_IPv4], [string:"no"], [variant string "no"])
+ _helper([AllowZoneDrifting], [string:"yes"], [variant string "yes"])
+--
+2.31.1
+
diff --git a/SOURCES/0048-RHEL-only-default-to-CleanupModulesOnExit-yes.patch b/SOURCES/0048-RHEL-only-default-to-CleanupModulesOnExit-yes.patch
new file mode 100644
index 0000000..1d05ba3
--- /dev/null
+++ b/SOURCES/0048-RHEL-only-default-to-CleanupModulesOnExit-yes.patch
@@ -0,0 +1,95 @@
+From 1aef58a8ff6d232cefcc6bd19ea63c0f071bfee3 Mon Sep 17 00:00:00 2001
+From: Eric Garver
+Date: Mon, 20 Dec 2021 13:56:55 -0500
+Subject: [PATCH 48/48] RHEL only: default to CleanupModulesOnExit=yes
+
+Resolves: rhbz1980206
+---
+ config/firewalld.conf | 4 ++--
+ doc/xml/firewalld.conf.xml | 4 ++--
+ src/firewall/config/__init__.py.in | 2 +-
+ src/firewall/core/fw.py | 2 ++
+ src/tests/dbus/firewalld.conf.at | 4 ++--
+ 5 files changed, 9 insertions(+), 7 deletions(-)
+
+diff --git a/config/firewalld.conf b/config/firewalld.conf
+index 3abbc9c998c1..c387f87c28be 100644
+--- a/config/firewalld.conf
++++ b/config/firewalld.conf
+@@ -15,8 +15,8 @@ CleanupOnExit=yes
+ # If set to yes or true the firewall related kernel modules will be
+ # unloaded on exit or stop of firewalld. This might attempt to unload
+ # modules not originally loaded by firewalld.
+-# Default: no
+-CleanupModulesOnExit=no
++# Default: yes
++CleanupModulesOnExit=yes
+
+ # Lockdown
+ # If set to enabled, firewall changes with the D-Bus interface will be limited
+diff --git a/doc/xml/firewalld.conf.xml b/doc/xml/firewalld.conf.xml
+index dd6ffb214eb3..12d9f5fc563e 100644
+--- a/doc/xml/firewalld.conf.xml
++++ b/doc/xml/firewalld.conf.xml
+@@ -93,8 +93,8 @@
+
+
+ Setting this option to yes or true unloads all firewall-related
+- kernel modules when firewalld is stopped. The default value is no
+- or false.
++ kernel modules when firewalld is stopped. The default value is yes
++ or true.
+
+
+
+diff --git a/src/firewall/config/__init__.py.in b/src/firewall/config/__init__.py.in
+index 5d6d769fbf15..285e2f034b6b 100644
+--- a/src/firewall/config/__init__.py.in
++++ b/src/firewall/config/__init__.py.in
+@@ -125,7 +125,7 @@ FIREWALL_BACKEND_VALUES = [ "nftables", "iptables" ]
+ FALLBACK_ZONE = "public"
+ FALLBACK_MINIMAL_MARK = 100
+ FALLBACK_CLEANUP_ON_EXIT = True
+-FALLBACK_CLEANUP_MODULES_ON_EXIT = False
++FALLBACK_CLEANUP_MODULES_ON_EXIT = True
+ FALLBACK_LOCKDOWN = False
+ FALLBACK_IPV6_RPFILTER = True
+ FALLBACK_INDIVIDUAL_CALLS = False
+diff --git a/src/firewall/core/fw.py b/src/firewall/core/fw.py
+index 4171697bdb94..5cef18b5f889 100644
+--- a/src/firewall/core/fw.py
++++ b/src/firewall/core/fw.py
+@@ -238,6 +238,8 @@ class Firewall(object):
+ value = self._firewalld_conf.get("CleanupModulesOnExit")
+ if value is not None and value.lower() in [ "yes", "true" ]:
+ self.cleanup_modules_on_exit = True
++ if value is not None and value.lower() in [ "no", "false" ]:
++ self.cleanup_modules_on_exit = False
+ log.debug1("CleanupModulesOnExit is set to '%s'",
+ self.cleanup_modules_on_exit)
+
+diff --git a/src/tests/dbus/firewalld.conf.at b/src/tests/dbus/firewalld.conf.at
+index 9a04a3bd491c..68832bca33bc 100644
+--- a/src/tests/dbus/firewalld.conf.at
++++ b/src/tests/dbus/firewalld.conf.at
+@@ -17,7 +17,7 @@ dnl Verify defaults over dbus. Should be inline with default firewalld.conf.
+ DBUS_GETALL([config], [config], 0, [dnl
+ string "AllowZoneDrifting" : variant string "no"
+ string "AutomaticHelpers" : variant string "no"
+-string "CleanupModulesOnExit" : variant string "no"
++string "CleanupModulesOnExit" : variant string "yes"
+ string "CleanupOnExit" : variant string "no"
+ string "DefaultZone" : variant string "public"
+ string "FirewallBackend" : variant string "nftables"
+@@ -46,7 +46,7 @@ _helper([IPv6_rpfilter], [string:"yes"], [variant string "yes"])
+ _helper([IndividualCalls], [string:"yes"], [variant string "yes"])
+ _helper([FirewallBackend], [string:"iptables"], [variant string "iptables"])
+ _helper([FlushAllOnReload], [string:"no"], [variant string "no"])
+-_helper([CleanupModulesOnExit], [string:"yes"], [variant string "yes"])
++_helper([CleanupModulesOnExit], [string:"no"], [variant string "no"])
+ _helper([CleanupOnExit], [string:"yes"], [variant string "yes"])
+ _helper([RFC3964_IPv4], [string:"no"], [variant string "no"])
+ _helper([AllowZoneDrifting], [string:"yes"], [variant string "yes"])
+--
+2.31.1
+
diff --git a/SOURCES/v1.0.0-0049-fix-ipset-reduce-cost-of-entry-overlap-detection.patch b/SOURCES/v1.0.0-0049-fix-ipset-reduce-cost-of-entry-overlap-detection.patch
new file mode 100644
index 0000000..ebebd8b
--- /dev/null
+++ b/SOURCES/v1.0.0-0049-fix-ipset-reduce-cost-of-entry-overlap-detection.patch
@@ -0,0 +1,140 @@
+From 34967402eda57d051b239c1551ecc0259881e7d4 Mon Sep 17 00:00:00 2001
+From: Eric Garver
+Date: Tue, 30 Nov 2021 14:54:20 -0500
+Subject: [PATCH 49/51] fix(ipset): reduce cost of entry overlap detection
+
+This increases peak memory usage to reduce the duration it takes to
+apply the set entries. Building the list of IPv4Network objects up front
+means we don't have to build them multiple times inside the for loop.
+
+Fixes: #881
+(cherry picked from commit 7f5b736378c0133f46470c42e0c1fb3b95087de5)
+---
+ src/firewall/client.py | 10 ++++------
+ src/firewall/core/fw_ipset.py | 9 +++------
+ src/firewall/core/ipset.py | 27 ++++++++++++++++++++++-----
+ src/firewall/server/config_ipset.py | 10 ++++------
+ 4 files changed, 33 insertions(+), 23 deletions(-)
+
+diff --git a/src/firewall/client.py b/src/firewall/client.py
+index 3715ffd29316..fdc88ac7946b 100644
+--- a/src/firewall/client.py
++++ b/src/firewall/client.py
+@@ -34,7 +34,8 @@ from firewall.core.base import DEFAULT_ZONE_TARGET, DEFAULT_POLICY_TARGET, DEFAU
+ from firewall.dbus_utils import dbus_to_python
+ from firewall.functions import b2u
+ from firewall.core.rich import Rich_Rule
+-from firewall.core.ipset import normalize_ipset_entry, check_entry_overlaps_existing
++from firewall.core.ipset import normalize_ipset_entry, check_entry_overlaps_existing, \
++ check_for_overlapping_entries
+ from firewall import errors
+ from firewall.errors import FirewallError
+
+@@ -1617,11 +1618,8 @@ class FirewallClientIPSetSettings(object):
+ if "timeout" in self.settings[4] and \
+ self.settings[4]["timeout"] != "0":
+ raise FirewallError(errors.IPSET_WITH_TIMEOUT)
+- _entries = set()
+- for _entry in dbus_to_python(entries, list):
+- check_entry_overlaps_existing(_entry, _entries)
+- _entries.add(normalize_ipset_entry(_entry))
+- self.settings[5] = list(_entries)
++ check_for_overlapping_entries(entries)
++ self.settings[5] = entries
+ @handle_exceptions
+ def addEntry(self, entry):
+ if "timeout" in self.settings[4] and \
+diff --git a/src/firewall/core/fw_ipset.py b/src/firewall/core/fw_ipset.py
+index a285fd4a4aab..d7878c01921e 100644
+--- a/src/firewall/core/fw_ipset.py
++++ b/src/firewall/core/fw_ipset.py
+@@ -25,7 +25,8 @@ __all__ = [ "FirewallIPSet" ]
+
+ from firewall.core.logger import log
+ from firewall.core.ipset import remove_default_create_options as rm_def_cr_opts, \
+- normalize_ipset_entry, check_entry_overlaps_existing
++ normalize_ipset_entry, check_entry_overlaps_existing, \
++ check_for_overlapping_entries
+ from firewall.core.io.ipset import IPSet
+ from firewall import errors
+ from firewall.errors import FirewallError
+@@ -244,11 +245,7 @@ class FirewallIPSet(object):
+ def set_entries(self, name, entries):
+ obj = self.get_ipset(name, applied=True)
+
+- _entries = set()
+- for _entry in entries:
+- check_entry_overlaps_existing(_entry, _entries)
+- _entries.add(normalize_ipset_entry(_entry))
+- entries = list(_entries)
++ check_for_overlapping_entries(entries)
+
+ for entry in entries:
+ IPSet.check_entry(entry, obj.options, obj.type)
+diff --git a/src/firewall/core/ipset.py b/src/firewall/core/ipset.py
+index d6defa395241..66ea4335536d 100644
+--- a/src/firewall/core/ipset.py
++++ b/src/firewall/core/ipset.py
+@@ -309,9 +309,26 @@ def check_entry_overlaps_existing(entry, entries):
+ if len(entry.split(",")) > 1:
+ return
+
++ try:
++ entry_network = ipaddress.ip_network(entry, strict=False)
++ except ValueError:
++ # could not parse the new IP address, maybe a MAC
++ return
++
+ for itr in entries:
+- try:
+- if ipaddress.ip_network(itr, strict=False).overlaps(ipaddress.ip_network(entry, strict=False)):
+- raise FirewallError(errors.INVALID_ENTRY, "Entry '{}' overlaps with existing entry '{}'".format(itr, entry))
+- except ValueError:
+- pass
++ if entry_network.overlaps(ipaddress.ip_network(itr, strict=False)):
++ raise FirewallError(errors.INVALID_ENTRY, "Entry '{}' overlaps with existing entry '{}'".format(entry, itr))
++
++def check_for_overlapping_entries(entries):
++ """ Check if any entry overlaps any entry in the list of entries """
++ try:
++ entries = [ipaddress.ip_network(x, strict=False) for x in entries]
++ except ValueError:
++ # at least one entry can not be parsed
++ return
++
++ while entries:
++ entry = entries.pop()
++ for itr in entries:
++ if entry.overlaps(itr):
++ raise FirewallError(errors.INVALID_ENTRY, "Entry '{}' overlaps entry '{}'".format(entry, itr))
+diff --git a/src/firewall/server/config_ipset.py b/src/firewall/server/config_ipset.py
+index f33c2a02926f..499ffcb9227a 100644
+--- a/src/firewall/server/config_ipset.py
++++ b/src/firewall/server/config_ipset.py
+@@ -34,7 +34,8 @@ from firewall.dbus_utils import dbus_to_python, \
+ dbus_introspection_add_properties
+ from firewall.core.io.ipset import IPSet
+ from firewall.core.ipset import IPSET_TYPES, normalize_ipset_entry, \
+- check_entry_overlaps_existing
++ check_entry_overlaps_existing, \
++ check_for_overlapping_entries
+ from firewall.core.logger import log
+ from firewall.server.decorators import handle_exceptions, \
+ dbus_handle_exceptions, dbus_service_method
+@@ -407,11 +408,8 @@ class FirewallDConfigIPSet(slip.dbus.service.Object):
+ in_signature='as')
+ @dbus_handle_exceptions
+ def setEntries(self, entries, sender=None):
+- _entries = set()
+- for _entry in dbus_to_python(entries, list):
+- check_entry_overlaps_existing(_entry, _entries)
+- _entries.add(normalize_ipset_entry(_entry))
+- entries = list(_entries)
++ entries = dbus_to_python(entries, list)
++ check_for_overlapping_entries(entries)
+ log.debug1("%s.setEntries('[%s]')", self._log_prefix,
+ ",".join(entries))
+ self.parent.accessCheck(sender)
+--
+2.31.1
+
diff --git a/SOURCES/v1.0.0-0050-test-ipset-huge-set-of-entries-benchmark.patch b/SOURCES/v1.0.0-0050-test-ipset-huge-set-of-entries-benchmark.patch
new file mode 100644
index 0000000..32e8a22
--- /dev/null
+++ b/SOURCES/v1.0.0-0050-test-ipset-huge-set-of-entries-benchmark.patch
@@ -0,0 +1,56 @@
+From 344753267f6b40d029a3b690cce74720a355cb4d Mon Sep 17 00:00:00 2001
+From: Eric Garver
+Date: Tue, 30 Nov 2021 14:50:17 -0500
+Subject: [PATCH 50/51] test(ipset): huge set of entries benchmark
+
+Coverage: #881
+(cherry picked from commit 114936c71ab1b12a5598d06805b7e9e13f7ee190)
+---
+ src/tests/regression/gh881.at | 25 +++++++++++++++++++++++++
+ src/tests/regression/regression.at | 1 +
+ 2 files changed, 26 insertions(+)
+ create mode 100644 src/tests/regression/gh881.at
+
+diff --git a/src/tests/regression/gh881.at b/src/tests/regression/gh881.at
+new file mode 100644
+index 000000000000..c7326805b555
+--- /dev/null
++++ b/src/tests/regression/gh881.at
+@@ -0,0 +1,25 @@
++FWD_START_TEST([ipset entry overlap detect perf])
++AT_KEYWORDS(ipset gh881)
++
++dnl build a large ipset
++dnl
++AT_DATA([./deny_cidr], [])
++NS_CHECK([sh -c '
++for I in $(seq 10); do
++ for J in $(seq 250); do
++ echo "10.${I}.${J}.0/24" >> ./deny_cidr
++ done
++done
++'])
++
++dnl verify non-overlapping does not error
++dnl
++FWD_CHECK([--permanent --new-ipset=deny_set --type=hash:net --option=family=inet --option=hashsize=16384 --option=maxelem=20000], 0, [ignore])
++NS_CHECK([time timeout 300 firewall-cmd --permanent --ipset=deny_set --add-entries-from-file=./deny_cidr], 0, [ignore], [ignore])
++
++dnl verify overlap detection actually detects an overlap
++dnl
++NS_CHECK([echo "10.1.0.0/16" >> ./deny_cidr])
++NS_CHECK([time timeout 300 firewall-cmd --permanent --ipset=deny_set --add-entries-from-file=./deny_cidr], 136, [ignore], [ignore])
++
++FWD_END_TEST()
+diff --git a/src/tests/regression/regression.at b/src/tests/regression/regression.at
+index a20b913fbe59..4045563d0b91 100644
+--- a/src/tests/regression/regression.at
++++ b/src/tests/regression/regression.at
+@@ -45,3 +45,4 @@ m4_include([regression/rhbz1914935.at])
+ m4_include([regression/gh696.at])
+ m4_include([regression/rhbz1917766.at])
+ m4_include([regression/rhbz2014383.at])
++m4_include([regression/gh881.at])
+--
+2.31.1
+
diff --git a/SOURCES/v1.0.0-0051-fix-ipset-further-reduce-cost-of-entry-overlap-detec.patch b/SOURCES/v1.0.0-0051-fix-ipset-further-reduce-cost-of-entry-overlap-detec.patch
new file mode 100644
index 0000000..7d1e8a1
--- /dev/null
+++ b/SOURCES/v1.0.0-0051-fix-ipset-further-reduce-cost-of-entry-overlap-detec.patch
@@ -0,0 +1,150 @@
+From 33b10b9112f2f51df049315438204efec7a5434c Mon Sep 17 00:00:00 2001
+From: Eric Garver
+Date: Tue, 25 Jan 2022 09:29:32 -0500
+Subject: [PATCH 51/51] fix(ipset): further reduce cost of entry overlap
+ detection
+
+This makes the complexity linear by sorting the networks ahead of time.
+
+Fixes: #881
+Fixes: rhbz2043289
+(cherry picked from commit 36c170db265265e838a089858be4b20dbbd582eb)
+---
+ src/firewall/core/ipset.py | 59 ++++++++++++++++++++++++++++++++---
+ src/tests/regression/gh881.at | 42 ++++++++++++++++++++++---
+ 2 files changed, 92 insertions(+), 9 deletions(-)
+
+diff --git a/src/firewall/core/ipset.py b/src/firewall/core/ipset.py
+index 66ea4335536d..b160d8345669 100644
+--- a/src/firewall/core/ipset.py
++++ b/src/firewall/core/ipset.py
+@@ -327,8 +327,57 @@ def check_for_overlapping_entries(entries):
+ # at least one entry can not be parsed
+ return
+
+- while entries:
+- entry = entries.pop()
+- for itr in entries:
+- if entry.overlaps(itr):
+- raise FirewallError(errors.INVALID_ENTRY, "Entry '{}' overlaps entry '{}'".format(entry, itr))
++ # We can take advantage of some facts of IPv4Network/IPv6Network and
++ # how Python sorts the networks to quickly detect overlaps.
++ #
++ # Facts:
++ #
++ # 1. IPv{4,6}Network are normalized to remove host bits, e.g.
++ # 10.1.1.0/16 will become 10.1.0.0/16.
++ #
++ # 2. IPv{4,6}Network objects are sorted by:
++ # a. IP address (network bits)
++ # then
++ # b. netmask (significant bits count)
++ #
++ # Because of the above we have these properties:
++ #
++ # 1. big networks (netA) are sorted before smaller networks (netB)
++ # that overlap the big network (netA)
++ # - e.g. 10.1.128.0/17 (netA) sorts before 10.1.129.0/24 (netB)
++ # 2. same value addresses (network bits) are grouped together even
++ # if the number of network bits vary. e.g. /16 vs /24
++ # - recall that address are normalized to remove host bits
++ # - e.g. 10.1.128.0/17 (netA) sorts before 10.1.128.0/24 (netC)
++ # 3. non-overlapping networks (netD, netE) are always sorted before or
++ # after networks that overlap (netB, netC) the current one (netA)
++ # - e.g. 10.1.128.0/17 (netA) sorts before 10.2.128.0/16 (netD)
++ # - e.g. 10.1.128.0/17 (netA) sorts after 9.1.128.0/17 (netE)
++ # - e.g. 9.1.128.0/17 (netE) sorts before 10.1.129.0/24 (netB)
++ #
++ # With this we know the sorted list looks like:
++ #
++ # list: [ netE, netA, netB, netC, netD ]
++ #
++ # netE = non-overlapping network
++ # netA = big network
++ # netB = smaller network that overlaps netA (subnet)
++ # netC = smaller network that overlaps netA (subnet)
++ # netD = non-overlapping network
++ #
++ # If networks netB and netC exist in the list, they overlap and are
++ # adjacent to netA.
++ #
++ # Checking for overlaps on a sorted list is thus:
++ #
++ # 1. compare adjacent elements in the list for overlaps
++ #
++ # Recall that we only need to detect a single overlap. We do not need to
++ # detect them all.
++ #
++ entries.sort()
++ prev_network = entries.pop(0)
++ for current_network in entries:
++ if prev_network.overlaps(current_network):
++ raise FirewallError(errors.INVALID_ENTRY, "Entry '{}' overlaps entry '{}'".format(prev_network, current_network))
++ prev_network = current_network
+diff --git a/src/tests/regression/gh881.at b/src/tests/regression/gh881.at
+index c7326805b555..a5cf7e4eb912 100644
+--- a/src/tests/regression/gh881.at
++++ b/src/tests/regression/gh881.at
+@@ -5,21 +5,55 @@ dnl build a large ipset
+ dnl
+ AT_DATA([./deny_cidr], [])
+ NS_CHECK([sh -c '
+-for I in $(seq 10); do
++for I in $(seq 250); do
+ for J in $(seq 250); do
+ echo "10.${I}.${J}.0/24" >> ./deny_cidr
+ done
+ done
+ '])
++NS_CHECK([echo "10.254.0.0/16" >> ./deny_cidr])
+
+ dnl verify non-overlapping does not error
+ dnl
+ FWD_CHECK([--permanent --new-ipset=deny_set --type=hash:net --option=family=inet --option=hashsize=16384 --option=maxelem=20000], 0, [ignore])
+-NS_CHECK([time timeout 300 firewall-cmd --permanent --ipset=deny_set --add-entries-from-file=./deny_cidr], 0, [ignore], [ignore])
++NS_CHECK([time firewall-cmd --permanent --ipset=deny_set --add-entries-from-file=./deny_cidr], 0, [ignore], [ignore])
++
++dnl still no overlap
++dnl
++AT_DATA([./deny_cidr], [
++9.0.0.0/8
++11.1.0.0/16
++])
++NS_CHECK([time firewall-cmd --permanent --ipset=deny_set --add-entries-from-file=./deny_cidr], 0, [ignore], [ignore])
+
+ dnl verify overlap detection actually detects an overlap
+ dnl
+-NS_CHECK([echo "10.1.0.0/16" >> ./deny_cidr])
+-NS_CHECK([time timeout 300 firewall-cmd --permanent --ipset=deny_set --add-entries-from-file=./deny_cidr], 136, [ignore], [ignore])
++AT_DATA([./deny_cidr], [
++10.1.0.0/16
++10.2.0.0/16
++10.250.0.0/16
++])
++NS_CHECK([time firewall-cmd --permanent --ipset=deny_set --add-entries-from-file=./deny_cidr], 136, [ignore], [ignore])
++
++AT_DATA([./deny_cidr], [
++10.253.0.0/16
++10.253.128.0/17
++])
++NS_CHECK([time firewall-cmd --permanent --ipset=deny_set --add-entries-from-file=./deny_cidr], 136, [ignore], [ignore])
++
++AT_DATA([./deny_cidr], [
++10.1.1.1/32
++])
++NS_CHECK([time firewall-cmd --permanent --ipset=deny_set --add-entries-from-file=./deny_cidr], 136, [ignore], [ignore])
++
++AT_DATA([./deny_cidr], [
++10.0.0.0/8
++10.0.0.0/25
++])
++NS_CHECK([time firewall-cmd --permanent --ipset=deny_set --add-entries-from-file=./deny_cidr], 136, [ignore], [ignore])
++
++dnl empty file, no additions, but previous ones will remain
++AT_DATA([./deny_cidr], [])
++FWD_CHECK([--permanent --ipset=deny_set --add-entries-from-file=./deny_cidr], 0, [ignore], [ignore])
+
+ FWD_END_TEST()
+--
+2.31.1
+
diff --git a/SPECS/firewalld.spec b/SPECS/firewalld.spec
index e66d386..a4ad0e7 100644
--- a/SPECS/firewalld.spec
+++ b/SPECS/firewalld.spec
@@ -1,7 +1,7 @@
Summary: A firewall daemon with D-Bus interface providing a dynamic firewall
Name: firewalld
Version: 0.9.3
-Release: 11%{?dist}
+Release: 13%{?dist}
URL: http://www.firewalld.org
License: GPLv2+
Source0: https://github.com/firewalld/firewalld/releases/download/v%{version}/firewalld-%{version}.tar.gz
@@ -51,6 +51,11 @@ Patch43: 0043-fix-fw_config-zone-on-rename-remove-then-add.patch
Patch44: 0044-fix-io-functions-check_config-against-on-disk-conf.patch
Patch45: 0045-fix-zone-detect-same-source-interface-in-zones.patch
Patch46: 0046-test-zone-detect-same-source-interface-in-zones.patch
+Patch47: 0047-feat-config-add-CleanupModulesOnExit-configuration-o.patch
+Patch48: 0048-RHEL-only-default-to-CleanupModulesOnExit-yes.patch
+Patch49: v1.0.0-0049-fix-ipset-reduce-cost-of-entry-overlap-detection.patch
+Patch50: v1.0.0-0050-test-ipset-huge-set-of-entries-benchmark.patch
+Patch51: v1.0.0-0051-fix-ipset-further-reduce-cost-of-entry-overlap-detec.patch
BuildArch: noarch
BuildRequires: autoconf
@@ -252,6 +257,13 @@ desktop-file-install --delete-original \
%{_mandir}/man1/firewall-config*.1*
%changelog
+* Thu Feb 03 2022 Eric Garver - 0.9.3-13
+- change default CleanupModulesOnExit=yes
+
+* Mon Dec 20 2021 Eric Garver - 0.9.3-12
+- feat(config): add CleanupModulesOnExit configuration option
+- change default CleanupModulesOnExit=yes
+
* Tue Nov 16 2021 Eric Garver - 0.9.3-11
- fix(zone): detect same source/interface in zones