diff --git a/SOURCES/0035-v2.2.0-feat-add-iperf-2-3-services.patch b/SOURCES/0035-v2.2.0-feat-add-iperf-2-3-services.patch
new file mode 100644
index 0000000..28702ba
--- /dev/null
+++ b/SOURCES/0035-v2.2.0-feat-add-iperf-2-3-services.patch
@@ -0,0 +1,71 @@
+From 0829f9ec9d171894bda6695a87c15aba49f3e6a2 Mon Sep 17 00:00:00 2001
+From: cyqsimon <28627918+cyqsimon@users.noreply.github.com>
+Date: Tue, 30 Jan 2024 17:08:30 +0800
+Subject: [PATCH 35/48] v2.2.0: feat: add iperf{2,3} services
+
+(cherry picked from commit a21b401f6d8dd6eb65adba1878a29d63086b15e7)
+---
+ config/Makefile.am | 2 ++
+ config/services/iperf2.xml | 7 +++++++
+ config/services/iperf3.xml | 8 ++++++++
+ po/POTFILES.in | 2 ++
+ 4 files changed, 19 insertions(+)
+ create mode 100644 config/services/iperf2.xml
+ create mode 100644 config/services/iperf3.xml
+
+diff --git a/config/Makefile.am b/config/Makefile.am
+index 711f05afb799..a7a6dc039594 100644
+--- a/config/Makefile.am
++++ b/config/Makefile.am
+@@ -188,6 +188,8 @@ CONFIG_FILES = \
+ services/ident.xml \
+ services/imaps.xml \
+ services/imap.xml \
++ services/iperf2.xml \
++ services/iperf3.xml \
+ services/ipfs.xml \
+ services/ipp-client.xml \
+ services/ipp.xml \
+diff --git a/config/services/iperf2.xml b/config/services/iperf2.xml
+new file mode 100644
+index 000000000000..4475c4e58212
+--- /dev/null
++++ b/config/services/iperf2.xml
+@@ -0,0 +1,7 @@
++
++
++ iperf2
++ iperf2 is a TCP and UDP network bandwidth measurement tool. Enable this option if you want to run an iperf2 server on its default port.
++
++
++
+diff --git a/config/services/iperf3.xml b/config/services/iperf3.xml
+new file mode 100644
+index 000000000000..4a481b0ecdfb
+--- /dev/null
++++ b/config/services/iperf3.xml
+@@ -0,0 +1,8 @@
++
++
++ iperf3
++ iperf3 is a TCP, UDP, and SCTP network bandwidth measurement tool. Enable this option if you want to run an iperf3 server on its default port.
++
++
++
++
+diff --git a/po/POTFILES.in b/po/POTFILES.in
+index a03ff0ce1df3..bdd0d3fc939e 100644
+--- a/po/POTFILES.in
++++ b/po/POTFILES.in
+@@ -120,6 +120,8 @@ config/services/http.xml
+ config/services/ident.xml
+ config/services/imaps.xml
+ config/services/imap.xml
++config/services/iperf2.xml
++config/services/iperf3.xml
+ config/services/ipfs.xml
+ config/services/ipp-client.xml
+ config/services/ipp.xml
+--
+2.47.3
+
diff --git a/SOURCES/0036-v2.4.0-test-functions-add-macro-WAIT_UNTIL.patch b/SOURCES/0036-v2.4.0-test-functions-add-macro-WAIT_UNTIL.patch
new file mode 100644
index 0000000..f068e61
--- /dev/null
+++ b/SOURCES/0036-v2.4.0-test-functions-add-macro-WAIT_UNTIL.patch
@@ -0,0 +1,36 @@
+From 1f72a1c3bdf0dd727d76f205b87223dd29ef0c7c Mon Sep 17 00:00:00 2001
+From: Eric Garver
+Date: Mon, 6 Oct 2025 16:32:59 -0400
+Subject: [PATCH 36/48] v2.4.0: test(functions): add macro WAIT_UNTIL
+
+(cherry picked from commit 50890c62b00db91f15ba5802055afc293a3fe77a)
+---
+ src/tests/functions.at | 15 +++++++++++++++
+ 1 file changed, 15 insertions(+)
+
+diff --git a/src/tests/functions.at b/src/tests/functions.at
+index 8b07908c667c..df3fff6ad4b9 100644
+--- a/src/tests/functions.at
++++ b/src/tests/functions.at
+@@ -763,3 +763,18 @@ m4_define([CHECK_NFTABLES_FIB_IN_FORWARD], [
+ NS_CHECK([nft delete table inet firewalld_check])
+ ])
+ ])
++
++dnl $1 = command to run until zero exit code
++m4_define([WAIT_UNTIL], [
++ _fail=1
++ _timeout=120
++ for I in $(seq ${_timeout}); do
++ { $1 ; } && { _fail=0; break; }
++ sleep 1
++ done
++ if test ${_fail} -gt 0; then
++ printf "FAIL: Command failed succeed in ${_timeout} seconds:\n"
++ printf " $1\n"
++ AT_FAIL_IF([:])
++ fi
++])
+--
+2.47.3
+
diff --git a/SOURCES/0037-v2.4.0-fix-server-load-firewall-rules-before-claimin.patch b/SOURCES/0037-v2.4.0-fix-server-load-firewall-rules-before-claimin.patch
new file mode 100644
index 0000000..6e17b2e
--- /dev/null
+++ b/SOURCES/0037-v2.4.0-fix-server-load-firewall-rules-before-claimin.patch
@@ -0,0 +1,81 @@
+From 3a91e1c6a575572cabbc06460ad94f65234a7d98 Mon Sep 17 00:00:00 2001
+From: Eric Garver
+Date: Tue, 14 Oct 2025 16:41:49 -0400
+Subject: [PATCH 37/48] v2.4.0: fix(server): load firewall rules before
+ claiming dbus
+
+This guarantees that the firewall is loaded and ready before the daemon
+registers with dbus.
+
+(cherry picked from commit 63d2238d055ee193f18acb51f6362b64d11e0886)
+---
+ src/firewall/server/firewalld.py | 15 +++++++++++----
+ src/firewall/server/server.py | 10 +---------
+ 2 files changed, 12 insertions(+), 13 deletions(-)
+
+diff --git a/src/firewall/server/firewalld.py b/src/firewall/server/firewalld.py
+index 8b9593a22fd8..fc85d3e0c359 100644
+--- a/src/firewall/server/firewalld.py
++++ b/src/firewall/server/firewalld.py
+@@ -25,6 +25,7 @@ from gi.repository import GLib
+ import copy
+ import dbus
+ import dbus.service
++import dbus.mainloop.glib
+
+ from firewall import config
+ from firewall.core.fw import Firewall
+@@ -69,12 +70,18 @@ class FirewallD(DbusServiceObject):
+ """ Use config.dbus.PK_ACTION_CONFIG as a default """
+
+ @handle_exceptions
+- def __init__(self, *args, **kwargs):
+- super(FirewallD, self).__init__(*args, **kwargs)
++ def __init__(self):
+ self.fw = Firewall()
+- self.busname = args[0]
+- self.path = args[1]
+ self.start()
++
++ dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
++ bus = dbus.SystemBus()
++ name = dbus.service.BusName(config.dbus.DBUS_INTERFACE, bus=bus)
++ super(FirewallD, self).__init__(name, config.dbus.DBUS_PATH)
++
++ self.busname = name
++ self.path = config.dbus.DBUS_PATH
++
+ dbus_introspection_prepare_properties(self, config.dbus.DBUS_INTERFACE)
+ self.config = FirewallDConfig(self.fw.config, self.busname,
+ config.dbus.DBUS_PATH_CONFIG)
+diff --git a/src/firewall/server/server.py b/src/firewall/server/server.py
+index 2921ae9104f1..7f3404793f12 100644
+--- a/src/firewall/server/server.py
++++ b/src/firewall/server/server.py
+@@ -31,11 +31,6 @@ import signal
+
+ from gi.repository import GLib
+
+-import dbus
+-import dbus.service
+-import dbus.mainloop.glib
+-
+-from firewall import config
+ from firewall.core.logger import log
+ from firewall.server.firewalld import FirewallD
+
+@@ -83,10 +78,7 @@ def run_server(debug_gc=False):
+ GLib.timeout_add_seconds(gc_timeout, gc_collect)
+
+ try:
+- dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
+- bus = dbus.SystemBus()
+- name = dbus.service.BusName(config.dbus.DBUS_INTERFACE, bus=bus)
+- service = FirewallD(name, config.dbus.DBUS_PATH)
++ service = FirewallD()
+
+ mainloop = GLib.MainLoop()
+ if debug_gc:
+--
+2.47.3
+
diff --git a/SOURCES/0038-v2.4.0-Revert-fix-systemd-allow-start-code-251-RUNNI.patch b/SOURCES/0038-v2.4.0-Revert-fix-systemd-allow-start-code-251-RUNNI.patch
new file mode 100644
index 0000000..c06fa62
--- /dev/null
+++ b/SOURCES/0038-v2.4.0-Revert-fix-systemd-allow-start-code-251-RUNNI.patch
@@ -0,0 +1,29 @@
+From bbfef80c94f368130e4440d1624d2a2bc1daf28d Mon Sep 17 00:00:00 2001
+From: Eric Garver
+Date: Tue, 14 Oct 2025 16:05:41 -0400
+Subject: [PATCH 38/48] v2.4.0: Revert "fix(systemd): allow start code 251
+ (RUNNING_BUT_FAILED)"
+
+This reverts commit d52815e198f05378a3f34633adfedd29165cc64e.
+
+(cherry picked from commit d2af4c8de086b658b0f1a24be9d3bf55b514b3c3)
+---
+ config/firewalld.service.in | 2 --
+ 1 file changed, 2 deletions(-)
+
+diff --git a/config/firewalld.service.in b/config/firewalld.service.in
+index bd8690fd87a6..cd7f772b8581 100644
+--- a/config/firewalld.service.in
++++ b/config/firewalld.service.in
+@@ -11,8 +11,6 @@ Documentation=man:firewalld(1)
+ EnvironmentFile=-/etc/sysconfig/firewalld
+ ExecStart=@sbindir@/firewalld --nofork --nopid $FIREWALLD_ARGS
+ ExecStartPost=@bindir@/firewall-cmd --state
+-# don't fail ExecStartPost on RUNNING_BUT_FAILED
+-SuccessExitStatus=251
+ ExecReload=/bin/kill -HUP $MAINPID
+ StandardOutput=null
+ StandardError=null
+--
+2.47.3
+
diff --git a/SOURCES/0039-v2.4.0-Revert-fix-systemd-verify-firewalld-is-respon.patch b/SOURCES/0039-v2.4.0-Revert-fix-systemd-verify-firewalld-is-respon.patch
new file mode 100644
index 0000000..e0df397
--- /dev/null
+++ b/SOURCES/0039-v2.4.0-Revert-fix-systemd-verify-firewalld-is-respon.patch
@@ -0,0 +1,29 @@
+From 82a7c51975297fa185410ce37ae0c73b70d1924d Mon Sep 17 00:00:00 2001
+From: Eric Garver
+Date: Tue, 14 Oct 2025 16:05:54 -0400
+Subject: [PATCH 39/48] v2.4.0: Revert "fix(systemd): verify firewalld is
+ responsive to dbus"
+
+This reverts commit 4ddfe5672e3a51e1c081b410144155553f256e91.
+
+Fixes: #1492
+(cherry picked from commit 3e61bcccdb3efc12474eb99538bd52fc4f63f4dd)
+---
+ config/firewalld.service.in | 1 -
+ 1 file changed, 1 deletion(-)
+
+diff --git a/config/firewalld.service.in b/config/firewalld.service.in
+index cd7f772b8581..08c9f74dd924 100644
+--- a/config/firewalld.service.in
++++ b/config/firewalld.service.in
+@@ -10,7 +10,6 @@ Documentation=man:firewalld(1)
+ [Service]
+ EnvironmentFile=-/etc/sysconfig/firewalld
+ ExecStart=@sbindir@/firewalld --nofork --nopid $FIREWALLD_ARGS
+-ExecStartPost=@bindir@/firewall-cmd --state
+ ExecReload=/bin/kill -HUP $MAINPID
+ StandardOutput=null
+ StandardError=null
+--
+2.47.3
+
diff --git a/SOURCES/0040-v2.4.0-fix-nftables-ipset-add-entries-from-GLib-loop.patch b/SOURCES/0040-v2.4.0-fix-nftables-ipset-add-entries-from-GLib-loop.patch
new file mode 100644
index 0000000..4ce834e
--- /dev/null
+++ b/SOURCES/0040-v2.4.0-fix-nftables-ipset-add-entries-from-GLib-loop.patch
@@ -0,0 +1,66 @@
+From 85d9dfb83d49499b21fb3c97e4486bc944a2651e Mon Sep 17 00:00:00 2001
+From: Eric Garver
+Date: Tue, 14 Oct 2025 15:59:05 -0400
+Subject: [PATCH 40/48] v2.4.0: fix(nftables): ipset: add entries from GLib
+ loop when idle
+
+Sets with a large amount of entries can take a significant time to
+apply. Use the GLib mainloop to add them in chunks when the loop is
+idle. This allows dbus calls in between the chunks as the dbus
+events/calls have higher priority.
+
+Fixes: #1277
+(cherry picked from commit 3874bafc427139e647829d7662577567343aceb6)
+---
+ src/firewall/core/nftables.py | 23 +++++++++++++++++++----
+ 1 file changed, 19 insertions(+), 4 deletions(-)
+
+diff --git a/src/firewall/core/nftables.py b/src/firewall/core/nftables.py
+index 8115bcb9d7f4..254fe7cfbebe 100644
+--- a/src/firewall/core/nftables.py
++++ b/src/firewall/core/nftables.py
+@@ -18,6 +18,9 @@
+ # You should have received a copy of the GNU General Public License
+ # along with this program. If not, see .
+ #
++
++from gi.repository import GLib
++
+ import copy
+ import json
+ import ipaddress
+@@ -1958,15 +1961,27 @@ class nftables(object):
+ rules = []
+ rules.extend(self.build_set_create_rules(set_name, type_name, create_options))
+ rules.extend(self.build_set_flush_rules(set_name))
++ self.set_rules(rules, self._fw.get_log_denied())
++
++ def _idle_set_add_entries(rules):
++ try:
++ self.set_rules(rules, self._fw.get_log_denied())
++ except Exception as e:
++ log.error("While restoring ipset entries the following Error occurred:")
++ log.error(e)
+
+- # avoid large memory usage by chunking the entries
++ # Avoid large memory usage by chunking the entries. Additionally, add
++ # the entries from the GLib main loop when it's idle. This avoids
++ # blocking the main loop for too long.
++ #
+ chunk = 0
++ rules = []
+ for entry in entries:
+ rules.extend(self.build_set_add_rules(set_name, entry))
+ chunk += 1
+ if chunk >= 1000:
+- self.set_rules(rules, self._fw.get_log_denied())
+- rules.clear()
++ GLib.idle_add(lambda x: _idle_set_add_entries(x), rules)
++ rules = []
+ chunk = 0
+ else:
+- self.set_rules(rules, self._fw.get_log_denied())
++ GLib.idle_add(lambda x: _idle_set_add_entries(x), rules)
+--
+2.47.3
+
diff --git a/SOURCES/0041-v2.4.0-test-ipset-scale-verify-all-the-entries-were-.patch b/SOURCES/0041-v2.4.0-test-ipset-scale-verify-all-the-entries-were-.patch
new file mode 100644
index 0000000..a862e49
--- /dev/null
+++ b/SOURCES/0041-v2.4.0-test-ipset-scale-verify-all-the-entries-were-.patch
@@ -0,0 +1,39 @@
+From c2f4ff8d26d2d7510e32f8ec6bcaa222af95ed9a Mon Sep 17 00:00:00 2001
+From: Eric Garver
+Date: Tue, 14 Oct 2025 17:50:22 -0400
+Subject: [PATCH 41/48] v2.4.0: test(ipset): scale: verify all the entries were
+ added
+
+(cherry picked from commit ab35fc7a99536efc7dde2242507fad6bed577d68)
+---
+ src/tests/regression/ipset_scale.at | 9 +++++++++
+ 1 file changed, 9 insertions(+)
+
+diff --git a/src/tests/regression/ipset_scale.at b/src/tests/regression/ipset_scale.at
+index 0aef986434f0..d754e035f187 100644
+--- a/src/tests/regression/ipset_scale.at
++++ b/src/tests/regression/ipset_scale.at
+@@ -1,6 +1,8 @@
+ FWD_START_TEST([ipset scale])
+ AT_KEYWORDS(ipset gh738 scale)
+
++AT_SKIP_IF([! NS_CMD([which wc >/dev/null 2>&1])])
++
+ dnl Create a huge ipset
+ AT_CHECK([touch ./entries], 0, [ignore])
+ AT_CHECK([sh -c '
+@@ -22,4 +24,11 @@ ulimit -d $(expr 1024 \* 300)
+ FWD_RESTART() dnl required because we changed ulimit
+ FWD_RELOAD()
+
++dnl Verify all the entries are added.
++m4_if(nftables, FIREWALL_BACKEND, [
++ WAIT_UNTIL([test "$(NS_CMD([nft $NFT_NUMERIC_ARGS list set inet firewalld foobar |wc -l]))" -eq 31256])
++], [
++ WAIT_UNTIL([test "$(NS_CMD([$IPSET list foobar |wc -l]))" -eq 62508])
++])
++
+ FWD_END_TEST()
+--
+2.47.3
+
diff --git a/SOURCES/0042-v2.4.0-fix-systemd-Requires-dbus.patch b/SOURCES/0042-v2.4.0-fix-systemd-Requires-dbus.patch
new file mode 100644
index 0000000..b50453d
--- /dev/null
+++ b/SOURCES/0042-v2.4.0-fix-systemd-Requires-dbus.patch
@@ -0,0 +1,28 @@
+From f7c7f94908834824bc777334c5b61e7af4d629b5 Mon Sep 17 00:00:00 2001
+From: Eric Garver
+Date: Mon, 13 Oct 2025 15:44:17 -0400
+Subject: [PATCH 42/48] v2.4.0: fix(systemd): Requires dbus
+
+Use Requires so when dbus is restarted firewalld is also restarted.
+
+Fixes: RHEL-94927
+(cherry picked from commit b9595ea06e6159735300bb4668a3e7e84966219c)
+---
+ config/firewalld.service.in | 1 +
+ 1 file changed, 1 insertion(+)
+
+diff --git a/config/firewalld.service.in b/config/firewalld.service.in
+index 08c9f74dd924..7ace390c29d6 100644
+--- a/config/firewalld.service.in
++++ b/config/firewalld.service.in
+@@ -2,6 +2,7 @@
+ Description=firewalld - dynamic firewall daemon
+ Before=network-pre.target
+ Wants=network-pre.target
++Requires=dbus.service
+ After=dbus.service
+ After=polkit.service
+ Conflicts=iptables.service ip6tables.service ebtables.service ipset.service
+--
+2.47.3
+
diff --git a/SOURCES/0043-v2.4.0-chore-icmp-add-all-icmptypes-to-ICMP_TYPES-di.patch b/SOURCES/0043-v2.4.0-chore-icmp-add-all-icmptypes-to-ICMP_TYPES-di.patch
new file mode 100644
index 0000000..a19d7c9
--- /dev/null
+++ b/SOURCES/0043-v2.4.0-chore-icmp-add-all-icmptypes-to-ICMP_TYPES-di.patch
@@ -0,0 +1,96 @@
+From 496cadf080a047286dbbb0fef4a06a8d17261587 Mon Sep 17 00:00:00 2001
+From: Eric Garver
+Date: Tue, 28 Oct 2025 15:59:26 -0400
+Subject: [PATCH 43/48] v2.4.0: chore(icmp): add all icmptypes to ICMP_TYPES
+ dict
+
+We can then use this as a source of truth for types/codes.
+
+(cherry picked from commit b121faab666faddf7181d21691ee6e8a13b30dbe)
+---
+ src/firewall/core/icmp.py | 22 ++++++++++++++++++++++
+ 1 file changed, 22 insertions(+)
+
+diff --git a/src/firewall/core/icmp.py b/src/firewall/core/icmp.py
+index c293098da20f..2928052194ea 100644
+--- a/src/firewall/core/icmp.py
++++ b/src/firewall/core/icmp.py
+@@ -25,32 +25,42 @@ __all__ = [ "ICMP_TYPES", "ICMPV6_TYPES",
+ ICMP_TYPES = {
+ "echo-reply": "0/0",
+ "pong": "0/0",
++ "destination-unreachable": "3/0",
+ "network-unreachable": "3/0",
++ "tos-network-unreachable": "3/0",
+ "host-unreachable": "3/1",
++ "tos-host-unreachable": "3/1",
+ "protocol-unreachable": "3/2",
+ "port-unreachable": "3/3",
+ "fragmentation-needed": "3/4",
+ "source-route-failed": "3/5",
++ # RFC-1112 Section 3.2.2.1 defines type 3, code 6-12
+ "network-unknown": "3/6",
+ "host-unknown": "3/7",
+ "network-prohibited": "3/9",
+ "host-prohibited": "3/10",
+ "TOS-network-unreachable": "3/11",
+ "TOS-host-unreachable": "3/12",
++ # RFC-1812 Section 5.2.7.1 defines type 3, code 13-15
+ "communication-prohibited": "3/13",
+ "host-precedence-violation": "3/14",
+ "precedence-cutoff": "3/15",
+ "source-quench": "4/0",
+ "network-redirect": "5/0",
++ "redirect": "5/0",
+ "host-redirect": "5/1",
++ "tos-host-redirect": "5/1",
+ "TOS-network-redirect": "5/2",
++ "tos-network-redirect": "5/2",
+ "TOS-host-redirect": "5/3",
+ "echo-request": "8/0",
+ "ping": "8/0",
+ "router-advertisement": "9/0",
+ "router-solicitation": "10/0",
++ "time-exceeded": "11/0",
+ "ttl-zero-during-transit": "11/0",
+ "ttl-zero-during-reassembly": "11/1",
++ "parameter-problem": "12/0",
+ "ip-header-bad": "12/0",
+ "required-option-missing": "12/1",
+ "timestamp-request": "13/0",
+@@ -60,13 +70,19 @@ ICMP_TYPES = {
+ }
+
+ ICMPV6_TYPES = {
++ "destination-unreachable": "1/0",
+ "no-route": "1/0",
+ "communication-prohibited": "1/1",
++ "beyond-scope": "1/2",
+ "address-unreachable": "1/3",
+ "port-unreachable": "1/4",
++ "failed-policy": "1/5",
++ "reject-route": "1/6",
+ "packet-too-big": "2/0",
++ "time-exceeded": "3/0",
+ "ttl-zero-during-transit": "3/0",
+ "ttl-zero-during-reassembly": "3/1",
++ "parameter-problem": "4/0",
+ "bad-header": "4/0",
+ "unknown-header-type": "4/1",
+ "unknown-option": "4/2",
+@@ -81,6 +97,12 @@ ICMPV6_TYPES = {
+ "neighbour-advertisement": "136/0",
+ "neigbour-advertisement": "136/0",
+ "redirect": "137/0",
++ # MLD is RFC-2710
++ "mld-listener-query": "130/0",
++ "mld-listener-report": "131/0",
++ "mld-listener-done": "132/0",
++ # MLDv2 is RFC-9777
++ "mld2-listener-report": "143/0",
+ }
+
+ def check_icmp_name(_name):
+--
+2.47.3
+
diff --git a/SOURCES/0044-v2.4.0-chore-icmp-convert-type-code-map-to-tuple.patch b/SOURCES/0044-v2.4.0-chore-icmp-convert-type-code-map-to-tuple.patch
new file mode 100644
index 0000000..9700d80
--- /dev/null
+++ b/SOURCES/0044-v2.4.0-chore-icmp-convert-type-code-map-to-tuple.patch
@@ -0,0 +1,261 @@
+From 8c01d2e4f45c83c2f75e2c5038571f99a650b862 Mon Sep 17 00:00:00 2001
+From: Eric Garver
+Date: Wed, 29 Oct 2025 11:32:35 -0400
+Subject: [PATCH 44/48] v2.4.0: chore(icmp): convert type/code map to tuple
+
+This will make it easier to use/query.
+
+(cherry picked from commit 6c33bbcdb60e257ec1102baaff128f5b345c67f8)
+---
+ src/firewall/core/icmp.py | 156 +++++++++++++++++-----------------
+ src/firewall/core/io/ipset.py | 26 ++++--
+ 2 files changed, 99 insertions(+), 83 deletions(-)
+
+diff --git a/src/firewall/core/icmp.py b/src/firewall/core/icmp.py
+index 2928052194ea..fc828ae59e51 100644
+--- a/src/firewall/core/icmp.py
++++ b/src/firewall/core/icmp.py
+@@ -23,86 +23,88 @@ __all__ = [ "ICMP_TYPES", "ICMPV6_TYPES",
+ "check_icmp_type", "check_icmpv6_type" ]
+
+ ICMP_TYPES = {
+- "echo-reply": "0/0",
+- "pong": "0/0",
+- "destination-unreachable": "3/0",
+- "network-unreachable": "3/0",
+- "tos-network-unreachable": "3/0",
+- "host-unreachable": "3/1",
+- "tos-host-unreachable": "3/1",
+- "protocol-unreachable": "3/2",
+- "port-unreachable": "3/3",
+- "fragmentation-needed": "3/4",
+- "source-route-failed": "3/5",
++ # "type": (type, code, backend omit code)
++ "echo-reply": (0, 0, True),
++ "pong": (0, 0, True),
++ "destination-unreachable": (3, 0, True),
++ "network-unreachable": (3, 0, False),
++ "tos-network-unreachable": (3, 0, False),
++ "host-unreachable": (3, 1, False),
++ "tos-host-unreachable": (3, 1, False),
++ "protocol-unreachable": (3, 2, False),
++ "port-unreachable": (3, 3, False),
++ "fragmentation-needed": (3, 4, False),
++ "source-route-failed": (3, 5, False),
+ # RFC-1112 Section 3.2.2.1 defines type 3, code 6-12
+- "network-unknown": "3/6",
+- "host-unknown": "3/7",
+- "network-prohibited": "3/9",
+- "host-prohibited": "3/10",
+- "TOS-network-unreachable": "3/11",
+- "TOS-host-unreachable": "3/12",
++ "network-unknown": (3, 6, False),
++ "host-unknown": (3, 7, False),
++ "network-prohibited": (3, 9, False),
++ "host-prohibited": (3, 10, False),
++ "TOS-network-unreachable": (3, 11, False),
++ "TOS-host-unreachable": (3, 12, False),
+ # RFC-1812 Section 5.2.7.1 defines type 3, code 13-15
+- "communication-prohibited": "3/13",
+- "host-precedence-violation": "3/14",
+- "precedence-cutoff": "3/15",
+- "source-quench": "4/0",
+- "network-redirect": "5/0",
+- "redirect": "5/0",
+- "host-redirect": "5/1",
+- "tos-host-redirect": "5/1",
+- "TOS-network-redirect": "5/2",
+- "tos-network-redirect": "5/2",
+- "TOS-host-redirect": "5/3",
+- "echo-request": "8/0",
+- "ping": "8/0",
+- "router-advertisement": "9/0",
+- "router-solicitation": "10/0",
+- "time-exceeded": "11/0",
+- "ttl-zero-during-transit": "11/0",
+- "ttl-zero-during-reassembly": "11/1",
+- "parameter-problem": "12/0",
+- "ip-header-bad": "12/0",
+- "required-option-missing": "12/1",
+- "timestamp-request": "13/0",
+- "timestamp-reply": "14/0",
+- "address-mask-request": "17/0",
+- "address-mask-reply": "18/0",
++ "communication-prohibited": (3, 13, False),
++ "host-precedence-violation": (3, 14, False),
++ "precedence-cutoff": (3, 15, False),
++ "source-quench": (4, 0, True),
++ "network-redirect": (5, 0, False),
++ "redirect": (5, 0, True),
++ "host-redirect": (5, 1, False),
++ "tos-host-redirect": (5, 1, False),
++ "TOS-network-redirect": (5, 2, False),
++ "tos-network-redirect": (5, 2, False),
++ "TOS-host-redirect": (5, 3, False),
++ "echo-request": (8, 0, True),
++ "ping": (8, 0, True),
++ "router-advertisement": (9, 0, True),
++ "router-solicitation": (10, 0, True),
++ "time-exceeded": (11, 0, True),
++ "ttl-zero-during-transit": (11, 0, False),
++ "ttl-zero-during-reassembly": (11, 1, False),
++ "parameter-problem": (12, 0, True),
++ "ip-header-bad": (12, 0, False),
++ "required-option-missing": (12, 1, False),
++ "timestamp-request": (13, 0, True),
++ "timestamp-reply": (14, 0, True),
++ "address-mask-request": (17, 0, False),
++ "address-mask-reply": (18, 0, False),
+ }
+
+ ICMPV6_TYPES = {
+- "destination-unreachable": "1/0",
+- "no-route": "1/0",
+- "communication-prohibited": "1/1",
+- "beyond-scope": "1/2",
+- "address-unreachable": "1/3",
+- "port-unreachable": "1/4",
+- "failed-policy": "1/5",
+- "reject-route": "1/6",
+- "packet-too-big": "2/0",
+- "time-exceeded": "3/0",
+- "ttl-zero-during-transit": "3/0",
+- "ttl-zero-during-reassembly": "3/1",
+- "parameter-problem": "4/0",
+- "bad-header": "4/0",
+- "unknown-header-type": "4/1",
+- "unknown-option": "4/2",
+- "echo-request": "128/0",
+- "ping": "128/0",
+- "echo-reply": "129/0",
+- "pong": "129/0",
+- "router-solicitation": "133/0",
+- "router-advertisement": "134/0",
+- "neighbour-solicitation": "135/0",
+- "neigbour-solicitation": "135/0",
+- "neighbour-advertisement": "136/0",
+- "neigbour-advertisement": "136/0",
+- "redirect": "137/0",
++ # "type": (type, code, backend omit code)
++ "destination-unreachable": (1, 0, True),
++ "no-route": (1, 0, False),
++ "communication-prohibited": (1, 1, False),
++ "beyond-scope": (1, 2, False),
++ "address-unreachable": (1, 3, False),
++ "port-unreachable": (1, 4, False),
++ "failed-policy": (1, 5, False),
++ "reject-route": (1, 6, False),
++ "packet-too-big": (2, 0, True),
++ "time-exceeded": (3, 0, True),
++ "ttl-zero-during-transit": (3, 0, False),
++ "ttl-zero-during-reassembly": (3, 1, False),
++ "parameter-problem": (4, 0, True),
++ "bad-header": (4, 0, False),
++ "unknown-header-type": (4, 1, False),
++ "unknown-option": (4, 2, False),
++ "echo-request": (128, 0, True),
++ "ping": (128, 0, True),
++ "echo-reply": (129, 0, True),
++ "pong": (129, 0, True),
++ "router-solicitation": (133, 0, True),
++ "router-advertisement": (134, 0, True),
++ "neighbour-solicitation": (135, 0, True),
++ "neigbour-solicitation": (135, 0, True),
++ "neighbour-advertisement": (136, 0, True),
++ "neigbour-advertisement": (136, 0, True),
++ "redirect": (137, 0, True),
+ # MLD is RFC-2710
+- "mld-listener-query": "130/0",
+- "mld-listener-report": "131/0",
+- "mld-listener-done": "132/0",
++ "mld-listener-query": (130, 0, True),
++ "mld-listener-report": (131, 0, True),
++ "mld-listener-done": (132, 0, True),
+ # MLDv2 is RFC-9777
+- "mld2-listener-report": "143/0",
++ "mld2-listener-report": (143, 0, True),
+ }
+
+ def check_icmp_name(_name):
+@@ -110,8 +112,8 @@ def check_icmp_name(_name):
+ return True
+ return False
+
+-def check_icmp_type(_type):
+- if _type in ICMP_TYPES.values():
++def check_icmp_type_code(_type, _code):
++ if (_type, _code) in ICMP_TYPES.values():
+ return True
+ return False
+
+@@ -120,7 +122,7 @@ def check_icmpv6_name(_name):
+ return True
+ return False
+
+-def check_icmpv6_type(_type):
+- if _type in ICMPV6_TYPES.values():
++def check_icmpv6_type_code(_type, _code):
++ if (_type, _code) in ICMPV6_TYPES.values():
+ return True
+ return False
+diff --git a/src/firewall/core/io/ipset.py b/src/firewall/core/io/ipset.py
+index a2fe16725875..6a612c8380f4 100644
+--- a/src/firewall/core/io/ipset.py
++++ b/src/firewall/core/io/ipset.py
+@@ -35,8 +35,8 @@ from firewall.functions import checkIP, checkIP6, checkIPnMask, \
+ from firewall.core.io.io_object import IO_Object, \
+ IO_Object_ContentHandler, IO_Object_XMLGenerator
+ from firewall.core.ipset import IPSET_TYPES, IPSET_CREATE_OPTIONS
+-from firewall.core.icmp import check_icmp_name, check_icmp_type, \
+- check_icmpv6_name, check_icmpv6_type
++from firewall.core.icmp import check_icmp_name, check_icmp_type_code, \
++ check_icmpv6_name, check_icmpv6_type_code
+ from firewall.core.logger import log
+ from firewall import errors
+ from firewall.errors import FirewallError
+@@ -203,24 +203,38 @@ class IPSet(IO_Object):
+ errors.INVALID_ENTRY,
+ "invalid protocol for family '%s' in '%s'" % \
+ (family, entry))
+- if not check_icmp_name(splits[1]) and not \
+- check_icmp_type(splits[1]):
++ if not check_icmp_name(splits[1]) and "/" not in splits[1]:
+ raise FirewallError(
+ errors.INVALID_ENTRY,
+ "invalid icmp type '%s' in '%s'" % \
+ (splits[1], entry))
++ else:
++ (_type, _code) = splits[1].split("/")
++ if not check_icmp_type_code(_type, _code):
++ raise FirewallError(
++ errors.INVALID_ENTRY,
++ "invalid icmp type '%s' in '%s'"
++ % (splits[1], entry),
++ )
+ elif splits[0] in [ "icmpv6", "ipv6-icmp" ]:
+ if family != "ipv6":
+ raise FirewallError(
+ errors.INVALID_ENTRY,
+ "invalid protocol for family '%s' in '%s'" % \
+ (family, entry))
+- if not check_icmpv6_name(splits[1]) and not \
+- check_icmpv6_type(splits[1]):
++ if not check_icmpv6_name(splits[1]) and "/" not in splits[1]:
+ raise FirewallError(
+ errors.INVALID_ENTRY,
+ "invalid icmpv6 type '%s' in '%s'" % \
+ (splits[1], entry))
++ else:
++ (_type, _code) = splits[1].split("/")
++ if not check_icmpv6_type_code(_type, _code):
++ raise FirewallError(
++ errors.INVALID_ENTRY,
++ "invalid icmpv6 type '%s' in '%s'"
++ % (splits[1], entry),
++ )
+ elif splits[0] not in [ "tcp", "sctp", "udp", "udplite" ] \
+ and not checkProtocol(splits[0]):
+ raise FirewallError(
+--
+2.47.3
+
diff --git a/SOURCES/0045-v2.4.0-chore-nftables-simplify-icmp-match-fragments.patch b/SOURCES/0045-v2.4.0-chore-nftables-simplify-icmp-match-fragments.patch
new file mode 100644
index 0000000..99d37a7
--- /dev/null
+++ b/SOURCES/0045-v2.4.0-chore-nftables-simplify-icmp-match-fragments.patch
@@ -0,0 +1,139 @@
+From 92d92e4f9d58d354b214a678f85a4fe64d510699 Mon Sep 17 00:00:00 2001
+From: Eric Garver
+Date: Wed, 29 Oct 2025 11:49:12 -0400
+Subject: [PATCH 45/48] v2.4.0: chore(nftables): simplify icmp match fragments
+
+We can use ICMP_TYPES/ICMPV6_TYPES to get the codes and make generating
+the match code generic. This eliminates ICMP_TYPES_FRAGMENTS.
+
+(cherry picked from commit d9c36de285fc3df1f1226972f8f9826d07e30921)
+---
+ src/firewall/core/nftables.py | 89 +++++------------------------------
+ 1 file changed, 11 insertions(+), 78 deletions(-)
+
+diff --git a/src/firewall/core/nftables.py b/src/firewall/core/nftables.py
+index 254fe7cfbebe..2a48ff6ed7f5 100644
+--- a/src/firewall/core/nftables.py
++++ b/src/firewall/core/nftables.py
+@@ -35,6 +35,7 @@ from firewall.core.rich import Rich_Accept, Rich_Reject, Rich_Drop, Rich_Mark, \
+ Rich_Masquerade, Rich_ForwardPort, Rich_IcmpBlock, \
+ Rich_Tcp_Mss_Clamp, Rich_NFLog
+ from firewall.core.base import DEFAULT_ZONE_TARGET
++from firewall.core.icmp import ICMP_TYPES, ICMPV6_TYPES
+ from nftables.nftables import Nftables
+
+ TABLE_NAME = "firewalld"
+@@ -94,78 +95,6 @@ def _icmp_types_fragments(protocol, type, code=None):
+ "right": code}})
+ return fragments
+
+-# Most ICMP types are provided by nft, but for the codes we have to use numeric
+-# values.
+-#
+-ICMP_TYPES_FRAGMENTS = {
+- "ipv4": {
+- "communication-prohibited": _icmp_types_fragments("icmp", "destination-unreachable", 13),
+- "destination-unreachable": _icmp_types_fragments("icmp", "destination-unreachable"),
+- "echo-reply": _icmp_types_fragments("icmp", "echo-reply"),
+- "echo-request": _icmp_types_fragments("icmp", "echo-request"),
+- "fragmentation-needed": _icmp_types_fragments("icmp", "destination-unreachable", 4),
+- "host-precedence-violation": _icmp_types_fragments("icmp", "destination-unreachable", 14),
+- "host-prohibited": _icmp_types_fragments("icmp", "destination-unreachable", 10),
+- "host-redirect": _icmp_types_fragments("icmp", "redirect", 1),
+- "host-unknown": _icmp_types_fragments("icmp", "destination-unreachable", 7),
+- "host-unreachable": _icmp_types_fragments("icmp", "destination-unreachable", 1),
+- "ip-header-bad": _icmp_types_fragments("icmp", "parameter-problem", 1),
+- "network-prohibited": _icmp_types_fragments("icmp", "destination-unreachable", 8),
+- "network-redirect": _icmp_types_fragments("icmp", "redirect", 0),
+- "network-unknown": _icmp_types_fragments("icmp", "destination-unreachable", 6),
+- "network-unreachable": _icmp_types_fragments("icmp", "destination-unreachable", 0),
+- "parameter-problem": _icmp_types_fragments("icmp", "parameter-problem"),
+- "port-unreachable": _icmp_types_fragments("icmp", "destination-unreachable", 3),
+- "precedence-cutoff": _icmp_types_fragments("icmp", "destination-unreachable", 15),
+- "protocol-unreachable": _icmp_types_fragments("icmp", "destination-unreachable", 2),
+- "redirect": _icmp_types_fragments("icmp", "redirect"),
+- "required-option-missing": _icmp_types_fragments("icmp", "parameter-problem", 1),
+- "router-advertisement": _icmp_types_fragments("icmp", "router-advertisement"),
+- "router-solicitation": _icmp_types_fragments("icmp", "router-solicitation"),
+- "source-quench": _icmp_types_fragments("icmp", "source-quench"),
+- "source-route-failed": _icmp_types_fragments("icmp", "destination-unreachable", 5),
+- "time-exceeded": _icmp_types_fragments("icmp", "time-exceeded"),
+- "timestamp-reply": _icmp_types_fragments("icmp", "timestamp-reply"),
+- "timestamp-request": _icmp_types_fragments("icmp", "timestamp-request"),
+- "tos-host-redirect": _icmp_types_fragments("icmp", "redirect", 3),
+- "tos-host-unreachable": _icmp_types_fragments("icmp", "destination-unreachable", 12),
+- "tos-network-redirect": _icmp_types_fragments("icmp", "redirect", 2),
+- "tos-network-unreachable": _icmp_types_fragments("icmp", "destination-unreachable", 11),
+- "ttl-zero-during-reassembly": _icmp_types_fragments("icmp", "time-exceeded", 1),
+- "ttl-zero-during-transit": _icmp_types_fragments("icmp", "time-exceeded", 0),
+- },
+-
+- "ipv6": {
+- "address-unreachable": _icmp_types_fragments("icmpv6", "destination-unreachable", 3),
+- "bad-header": _icmp_types_fragments("icmpv6", "parameter-problem", 0),
+- "beyond-scope": _icmp_types_fragments("icmpv6", "destination-unreachable", 2),
+- "communication-prohibited": _icmp_types_fragments("icmpv6", "destination-unreachable", 1),
+- "destination-unreachable": _icmp_types_fragments("icmpv6", "destination-unreachable"),
+- "echo-reply": _icmp_types_fragments("icmpv6", "echo-reply"),
+- "echo-request": _icmp_types_fragments("icmpv6", "echo-request"),
+- "failed-policy": _icmp_types_fragments("icmpv6", "destination-unreachable", 5),
+- "mld-listener-done": _icmp_types_fragments("icmpv6", "mld-listener-done"),
+- "mld-listener-query": _icmp_types_fragments("icmpv6", "mld-listener-query"),
+- "mld-listener-report": _icmp_types_fragments("icmpv6", "mld-listener-report"),
+- "mld2-listener-report": _icmp_types_fragments("icmpv6", "mld2-listener-report"),
+- "neighbour-advertisement": _icmp_types_fragments("icmpv6", "nd-neighbor-advert"),
+- "neighbour-solicitation": _icmp_types_fragments("icmpv6", "nd-neighbor-solicit"),
+- "no-route": _icmp_types_fragments("icmpv6", "destination-unreachable", 0),
+- "packet-too-big": _icmp_types_fragments("icmpv6", "packet-too-big"),
+- "parameter-problem": _icmp_types_fragments("icmpv6", "parameter-problem"),
+- "port-unreachable": _icmp_types_fragments("icmpv6", "destination-unreachable", 4),
+- "redirect": _icmp_types_fragments("icmpv6", "nd-redirect"),
+- "reject-route": _icmp_types_fragments("icmpv6", "destination-unreachable", 6),
+- "router-advertisement": _icmp_types_fragments("icmpv6", "nd-router-advert"),
+- "router-solicitation": _icmp_types_fragments("icmpv6", "nd-router-solicit"),
+- "time-exceeded": _icmp_types_fragments("icmpv6", "time-exceeded"),
+- "ttl-zero-during-reassembly": _icmp_types_fragments("icmpv6", "time-exceeded", 1),
+- "ttl-zero-during-transit": _icmp_types_fragments("icmpv6", "time-exceeded", 0),
+- "unknown-header-type": _icmp_types_fragments("icmpv6", "parameter-problem", 1),
+- "unknown-option": _icmp_types_fragments("icmpv6", "parameter-problem", 2),
+- }
+-}
+-
+ class nftables(object):
+ name = "nftables"
+ policies_supported = True
+@@ -566,12 +495,12 @@ class nftables(object):
+ return rules
+
+ def supported_icmp_types(self, ipv=None):
+- # nftables supports any icmp_type via arbitrary type/code matching.
+- # We just need a translation for it in ICMP_TYPES_FRAGMENTS.
+ supported = set()
+
+- for _ipv in [ipv] if ipv else ICMP_TYPES_FRAGMENTS.keys():
+- supported.update(ICMP_TYPES_FRAGMENTS[_ipv].keys())
++ if ipv is None or ipv == "ipv4":
++ supported.update(ICMP_TYPES.keys())
++ if ipv is None or ipv == "ipv6":
++ supported.update(ICMPV6_TYPES.keys())
+
+ return list(supported)
+
+@@ -1577,8 +1506,12 @@ class nftables(object):
+ return [{add_del: {"rule": rule}}]
+
+ def _icmp_types_to_nft_fragments(self, ipv, icmp_type):
+- if icmp_type in ICMP_TYPES_FRAGMENTS[ipv]:
+- return ICMP_TYPES_FRAGMENTS[ipv][icmp_type]
++ if ipv == "ipv4" and icmp_type in ICMP_TYPES:
++ _type, _code, _omit_code = ICMP_TYPES[icmp_type]
++ return _icmp_types_fragments("icmp", _type, None if _omit_code else _code)
++ elif ipv == "ipv6" and icmp_type in ICMPV6_TYPES:
++ _type, _code, _omit_code = ICMPV6_TYPES[icmp_type]
++ return _icmp_types_fragments("icmpv6", _type, None if _omit_code else _code)
+ else:
+ raise FirewallError(INVALID_ICMPTYPE,
+ "ICMP type '%s' not supported by %s for %s" % (icmp_type, self.name, ipv))
+--
+2.47.3
+
diff --git a/SOURCES/0046-v2.4.0-chore-nftables-move-_icmp_types_fragments-ins.patch b/SOURCES/0046-v2.4.0-chore-nftables-move-_icmp_types_fragments-ins.patch
new file mode 100644
index 0000000..28a5a7a
--- /dev/null
+++ b/SOURCES/0046-v2.4.0-chore-nftables-move-_icmp_types_fragments-ins.patch
@@ -0,0 +1,68 @@
+From 47d8ef737fc2f7b5990e39a86efb9358dc761076 Mon Sep 17 00:00:00 2001
+From: Eric Garver
+Date: Wed, 29 Oct 2025 11:51:18 -0400
+Subject: [PATCH 46/48] v2.4.0: chore(nftables): move _icmp_types_fragments()
+ inside the class
+
+It was the only function outside of the class and is not useful to any
+other code. Put it inside the class with everything else.
+
+(cherry picked from commit 69ad16a4435a0b49b4196aa1a99ee963b72c4b69)
+---
+ src/firewall/core/nftables.py | 28 ++++++++++++++++------------
+ 1 file changed, 16 insertions(+), 12 deletions(-)
+
+diff --git a/src/firewall/core/nftables.py b/src/firewall/core/nftables.py
+index 2a48ff6ed7f5..0d3e31299d15 100644
+--- a/src/firewall/core/nftables.py
++++ b/src/firewall/core/nftables.py
+@@ -85,16 +85,6 @@ IPTABLES_TO_NFT_HOOK = {
+ },
+ }
+
+-def _icmp_types_fragments(protocol, type, code=None):
+- fragments = [{"match": {"left": {"payload": {"protocol": protocol, "field": "type"}},
+- "op": "==",
+- "right": type}}]
+- if code is not None:
+- fragments.append({"match": {"left": {"payload": {"protocol": protocol, "field": "code"}},
+- "op": "==",
+- "right": code}})
+- return fragments
+-
+ class nftables(object):
+ name = "nftables"
+ policies_supported = True
+@@ -1505,13 +1495,27 @@ class nftables(object):
+ rule.update(self._rich_rule_priority_fragment(rich_rule))
+ return [{add_del: {"rule": rule}}]
+
++ def _icmp_types_fragments(self, protocol, type, code=None):
++ fragments = [{"match": {"left": {"payload": {"protocol": protocol, "field": "type"}},
++ "op": "==",
++ "right": type}}]
++ if code is not None:
++ fragments.append({"match": {"left": {"payload": {"protocol": protocol, "field": "code"}},
++ "op": "==",
++ "right": code}})
++ return fragments
++
+ def _icmp_types_to_nft_fragments(self, ipv, icmp_type):
+ if ipv == "ipv4" and icmp_type in ICMP_TYPES:
+ _type, _code, _omit_code = ICMP_TYPES[icmp_type]
+- return _icmp_types_fragments("icmp", _type, None if _omit_code else _code)
++ return self._icmp_types_fragments(
++ "icmp", _type, None if _omit_code else _code
++ )
+ elif ipv == "ipv6" and icmp_type in ICMPV6_TYPES:
+ _type, _code, _omit_code = ICMPV6_TYPES[icmp_type]
+- return _icmp_types_fragments("icmpv6", _type, None if _omit_code else _code)
++ return self._icmp_types_fragments(
++ "icmpv6", _type, None if _omit_code else _code
++ )
+ else:
+ raise FirewallError(INVALID_ICMPTYPE,
+ "ICMP type '%s' not supported by %s for %s" % (icmp_type, self.name, ipv))
+--
+2.47.3
+
diff --git a/SOURCES/0047-v2.4.0-chore-ipXtables-simplify-icmp-match-fragments.patch b/SOURCES/0047-v2.4.0-chore-ipXtables-simplify-icmp-match-fragments.patch
new file mode 100644
index 0000000..818ab9f
--- /dev/null
+++ b/SOURCES/0047-v2.4.0-chore-ipXtables-simplify-icmp-match-fragments.patch
@@ -0,0 +1,115 @@
+From ea62b989b3ccb4bdf99cd42cfc17f08723c63407 Mon Sep 17 00:00:00 2001
+From: Eric Garver
+Date: Wed, 29 Oct 2025 16:33:17 -0400
+Subject: [PATCH 47/48] v2.4.0: chore(ipXtables): simplify icmp match fragments
+
+We can use ICMP_TYPES/ICMPV6_TYPES to get the codes and make rule
+generation generic while also supporting more types that don't have name
+support in iptables, e.g. mld.
+
+(cherry picked from commit 1562687ae6ca3cbcf6ece25126af9116adfb897b)
+---
+ src/firewall/core/ipXtables.py | 61 +++++++++++++++-------------------
+ 1 file changed, 27 insertions(+), 34 deletions(-)
+
+diff --git a/src/firewall/core/ipXtables.py b/src/firewall/core/ipXtables.py
+index 339840c37ba4..efe292b3f638 100644
+--- a/src/firewall/core/ipXtables.py
++++ b/src/firewall/core/ipXtables.py
+@@ -27,10 +27,12 @@ from firewall.core.logger import log
+ from firewall.functions import tempFile, readfile, splitArgs, check_mac, portStr, \
+ check_single_address, check_address, normalizeIP6
+ from firewall import config
+-from firewall.errors import FirewallError, INVALID_PASSTHROUGH, INVALID_RULE, UNKNOWN_ERROR, INVALID_ADDR
++from firewall.errors import FirewallError, INVALID_PASSTHROUGH, INVALID_RULE, UNKNOWN_ERROR, INVALID_ADDR, \
++ INVALID_ICMPTYPE
+ from firewall.core.rich import Rich_Accept, Rich_Reject, Rich_Drop, Rich_Mark, Rich_NFLog, \
+ Rich_Masquerade, Rich_ForwardPort, Rich_IcmpBlock, Rich_Tcp_Mss_Clamp
+ from firewall.core.base import DEFAULT_ZONE_TARGET
++from firewall.core.icmp import ICMP_TYPES, ICMPV6_TYPES
+ import string
+
+ POLICY_CHAIN_PREFIX = ""
+@@ -598,37 +600,14 @@ class ip4tables(object):
+ return rules
+
+ def supported_icmp_types(self, ipv=None):
+- """Return ICMP types that are supported by the iptables/ip6tables command and kernel"""
+- ret = [ ]
+- output = ""
+- try:
+- output = self.__run(["-p",
+- "icmp" if self.ipv == "ipv4" else "ipv6-icmp",
+- "--help"])
+- except ValueError as ex:
+- if self.ipv == "ipv4":
+- log.debug1("iptables error: %s" % ex)
+- else:
+- log.debug1("ip6tables error: %s" % ex)
+- lines = output.splitlines()
+-
+- in_types = False
+- for line in lines:
+- #print(line)
+- if in_types:
+- line = line.strip().lower()
+- splits = line.split()
+- for split in splits:
+- if split.startswith("(") and split.endswith(")"):
+- x = split[1:-1]
+- else:
+- x = split
+- if x not in ret:
+- ret.append(x)
+- if self.ipv == "ipv4" and line.startswith("Valid ICMP Types:") or \
+- self.ipv == "ipv6" and line.startswith("Valid ICMPv6 Types:"):
+- in_types = True
+- return ret
++ supported = set()
++
++ if ipv is None or self.ipv == "ipv4":
++ supported.update(ICMP_TYPES.keys())
++ if ipv is None or self.ipv == "ipv6":
++ supported.update(ICMPV6_TYPES.keys())
++
++ return list(supported)
+
+ def build_default_tables(self):
+ # nothing to do, they always exist
+@@ -1350,6 +1329,20 @@ class ip4tables(object):
+
+ return rules
+
++ def _icmp_types_fragment(self, icmp_type):
++ if self.ipv == "ipv4" and icmp_type in ICMP_TYPES:
++ _type, _code, _omit_code = ICMP_TYPES[icmp_type]
++ _type_str = str(_type) if _omit_code else str(_type) + "/" + str(_code)
++ return ["-m", "icmp", "--icmp-type", _type_str]
++ elif self.ipv == "ipv6" and icmp_type in ICMPV6_TYPES:
++ _type, _code, _omit_code = ICMPV6_TYPES[icmp_type]
++ _type_str = str(_type) if _omit_code else str(_type) + "/" + str(_code)
++ return ["-m", "icmp6", "--icmpv6-type", _type_str]
++ else:
++ raise FirewallError(
++ INVALID_ICMPTYPE, f"ICMP type {icmp_type} not supported by {self.name}"
++ )
++
+ def build_policy_icmp_block_rules(self, enable, policy, ict, rich_rule=None):
+ table = "filter"
+ _policy = self._fw.policy.policy_base_chain_name(policy, table, POLICY_CHAIN_PREFIX)
+@@ -1357,10 +1350,10 @@ class ip4tables(object):
+
+ if self.ipv == "ipv4":
+ proto = [ "-p", "icmp" ]
+- match = [ "-m", "icmp", "--icmp-type", ict.name ]
++ match = self._icmp_types_fragment(ict.name)
+ else:
+ proto = [ "-p", "ipv6-icmp" ]
+- match = [ "-m", "icmp6", "--icmpv6-type", ict.name ]
++ match = self._icmp_types_fragment(ict.name)
+
+ rules = []
+ if self._fw.policy.query_icmp_block_inversion(policy):
+--
+2.47.3
+
diff --git a/SOURCES/0048-v2.4.0-fix-policy-allow-host-ipv6-allow-MLD-packets.patch b/SOURCES/0048-v2.4.0-fix-policy-allow-host-ipv6-allow-MLD-packets.patch
new file mode 100644
index 0000000..a195df4
--- /dev/null
+++ b/SOURCES/0048-v2.4.0-fix-policy-allow-host-ipv6-allow-MLD-packets.patch
@@ -0,0 +1,165 @@
+From bf8461cacb5a87bea1206dbc4a33f05d1007de46 Mon Sep 17 00:00:00 2001
+From: Eric Garver
+Date: Mon, 27 Oct 2025 16:37:35 -0400
+Subject: [PATCH 48/48] v2.4.0: fix(policy): allow-host-ipv6: allow MLD packets
+
+RFC 4890 Section 4.4.1 makes it very clear that MLD packets must be
+allowed by default.
+
+Fixes: RHEL-54411
+Fixes: RHEL-123703
+(cherry picked from commit 3c42d02b770391686e6f7b202556ea4eee0722f5)
+---
+ config/policies/allow-host-ipv6.xml | 16 +++++++++++++
+ src/tests/dbus/policy_permanent_functional.at | 4 ++--
+ src/tests/dbus/policy_runtime_functional.at | 2 +-
+ src/tests/features/policy.at | 24 +++++++++++++++++++
+ src/tests/regression/rhbz2222044.at | 2 +-
+ 5 files changed, 44 insertions(+), 4 deletions(-)
+
+diff --git a/config/policies/allow-host-ipv6.xml b/config/policies/allow-host-ipv6.xml
+index 0de7629c3809..33b5d13206a6 100644
+--- a/config/policies/allow-host-ipv6.xml
++++ b/config/policies/allow-host-ipv6.xml
+@@ -20,4 +20,20 @@
+
+
+
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
+
+diff --git a/src/tests/dbus/policy_permanent_functional.at b/src/tests/dbus/policy_permanent_functional.at
+index a5e1af90cc1c..44b8ebeea18e 100644
+--- a/src/tests/dbus/policy_permanent_functional.at
++++ b/src/tests/dbus/policy_permanent_functional.at
+@@ -155,7 +155,7 @@ DBUS_CHECK([config/policy/${DBUS_BUILTIN_POLICY_OBJ}], [config.policy.getSetting
+ 'ingress_zones': m4_escape([<['ANY']>])
+ 'masquerade':
+ 'priority': <-15000>
+- 'rich_rules': m4_escape([<['rule family="ipv6" icmp-type name="neighbour-advertisement" accept', 'rule family="ipv6" icmp-type name="neighbour-solicitation" accept', 'rule family="ipv6" icmp-type name="router-advertisement" accept', 'rule family="ipv6" icmp-type name="redirect" accept']>])
++ 'rich_rules': m4_escape([<['rule family="ipv6" icmp-type name="neighbour-advertisement" accept', 'rule family="ipv6" icmp-type name="neighbour-solicitation" accept', 'rule family="ipv6" icmp-type name="router-advertisement" accept', 'rule family="ipv6" icmp-type name="redirect" accept', 'rule family="ipv6" icmp-type name="mld-listener-done" accept', 'rule family="ipv6" icmp-type name="mld-listener-query" accept', 'rule family="ipv6" icmp-type name="mld-listener-report" accept', 'rule family="ipv6" icmp-type name="mld2-listener-report" accept']>])
+ 'short': <'Allow host IPv6'>
+ 'target': <'DROP'>
+ 'version': <'1.2'>
+@@ -168,7 +168,7 @@ DBUS_CHECK([config/policy/${DBUS_BUILTIN_POLICY_OBJ}], [config.policy.getSetting
+ 'ingress_zones': m4_escape([<['ANY']>])
+ 'masquerade':
+ 'priority': <-15000>
+- 'rich_rules': m4_escape([<['rule family="ipv6" icmp-type name="neighbour-advertisement" accept', 'rule family="ipv6" icmp-type name="neighbour-solicitation" accept', 'rule family="ipv6" icmp-type name="router-advertisement" accept', 'rule family="ipv6" icmp-type name="redirect" accept']>])
++ 'rich_rules': m4_escape([<['rule family="ipv6" icmp-type name="neighbour-advertisement" accept', 'rule family="ipv6" icmp-type name="neighbour-solicitation" accept', 'rule family="ipv6" icmp-type name="router-advertisement" accept', 'rule family="ipv6" icmp-type name="redirect" accept', 'rule family="ipv6" icmp-type name="mld-listener-done" accept', 'rule family="ipv6" icmp-type name="mld-listener-query" accept', 'rule family="ipv6" icmp-type name="mld-listener-report" accept', 'rule family="ipv6" icmp-type name="mld2-listener-report" accept']>])
+ 'short': <'Allow host IPv6'>
+ 'target': <'CONTINUE'>
+ ])
+diff --git a/src/tests/dbus/policy_runtime_functional.at b/src/tests/dbus/policy_runtime_functional.at
+index ab9a43dda8f3..08cb3b1051ff 100644
+--- a/src/tests/dbus/policy_runtime_functional.at
++++ b/src/tests/dbus/policy_runtime_functional.at
+@@ -11,7 +11,7 @@ DBUS_CHECK([], [policy.getPolicySettings], ["allow-host-ipv6"], 0, [dnl
+ 'ingress_zones': m4_escape([<['ANY']>])
+ 'masquerade':
+ 'priority': <-15000>
+- 'rich_rules': m4_escape([<['rule family="ipv6" icmp-type name="neighbour-advertisement" accept', 'rule family="ipv6" icmp-type name="neighbour-solicitation" accept', 'rule family="ipv6" icmp-type name="router-advertisement" accept', 'rule family="ipv6" icmp-type name="redirect" accept']>])
++ 'rich_rules': m4_escape([<['rule family="ipv6" icmp-type name="neighbour-advertisement" accept', 'rule family="ipv6" icmp-type name="neighbour-solicitation" accept', 'rule family="ipv6" icmp-type name="router-advertisement" accept', 'rule family="ipv6" icmp-type name="redirect" accept', 'rule family="ipv6" icmp-type name="mld-listener-done" accept', 'rule family="ipv6" icmp-type name="mld-listener-query" accept', 'rule family="ipv6" icmp-type name="mld-listener-report" accept', 'rule family="ipv6" icmp-type name="mld2-listener-report" accept']>])
+ 'short': <'Allow host IPv6'>
+ 'target': <'CONTINUE'>
+ ])
+diff --git a/src/tests/features/policy.at b/src/tests/features/policy.at
+index 7136c0cf5ab9..e237c333651a 100644
+--- a/src/tests/features/policy.at
++++ b/src/tests/features/policy.at
+@@ -127,6 +127,10 @@ allow-host-ipv6 (active)
+ rule family="ipv6" icmp-type name="neighbour-solicitation" accept
+ rule family="ipv6" icmp-type name="router-advertisement" accept
+ rule family="ipv6" icmp-type name="redirect" accept
++ rule family="ipv6" icmp-type name="mld-listener-done" accept
++ rule family="ipv6" icmp-type name="mld-listener-query" accept
++ rule family="ipv6" icmp-type name="mld-listener-report" accept
++ rule family="ipv6" icmp-type name="mld2-listener-report" accept
+ ])])
+ FWD_CHECK([--permanent --info-policy allow-host-ipv6 | TRIM_WHITESPACE], 0, [m4_strip([dnl
+ allow-host-ipv6 (active)
+@@ -146,6 +150,10 @@ allow-host-ipv6 (active)
+ rule family="ipv6" icmp-type name="neighbour-solicitation" accept
+ rule family="ipv6" icmp-type name="router-advertisement" accept
+ rule family="ipv6" icmp-type name="redirect" accept
++ rule family="ipv6" icmp-type name="mld-listener-done" accept
++ rule family="ipv6" icmp-type name="mld-listener-query" accept
++ rule family="ipv6" icmp-type name="mld-listener-report" accept
++ rule family="ipv6" icmp-type name="mld2-listener-report" accept
+ ])])
+
+ FWD_CHECK([--list-all-policies | TRIM_WHITESPACE], 0, [m4_strip([dnl
+@@ -166,6 +174,10 @@ allow-host-ipv6 (active)
+ rule family="ipv6" icmp-type name="neighbour-solicitation" accept
+ rule family="ipv6" icmp-type name="router-advertisement" accept
+ rule family="ipv6" icmp-type name="redirect" accept
++ rule family="ipv6" icmp-type name="mld-listener-done" accept
++ rule family="ipv6" icmp-type name="mld-listener-query" accept
++ rule family="ipv6" icmp-type name="mld-listener-report" accept
++ rule family="ipv6" icmp-type name="mld2-listener-report" accept
+ ])])
+ FWD_CHECK([--permanent --list-all-policies | TRIM_WHITESPACE], 0, [m4_strip([dnl
+ allow-host-ipv6 (active)
+@@ -185,6 +197,10 @@ allow-host-ipv6 (active)
+ rule family="ipv6" icmp-type name="neighbour-solicitation" accept
+ rule family="ipv6" icmp-type name="router-advertisement" accept
+ rule family="ipv6" icmp-type name="redirect" accept
++ rule family="ipv6" icmp-type name="mld-listener-done" accept
++ rule family="ipv6" icmp-type name="mld-listener-query" accept
++ rule family="ipv6" icmp-type name="mld-listener-report" accept
++ rule family="ipv6" icmp-type name="mld2-listener-report" accept
+ ])])
+
+ FWD_CHECK([--policy allow-host-ipv6 --list-all | TRIM_WHITESPACE], 0, [m4_strip([dnl
+@@ -205,6 +221,10 @@ allow-host-ipv6 (active)
+ rule family="ipv6" icmp-type name="neighbour-solicitation" accept
+ rule family="ipv6" icmp-type name="router-advertisement" accept
+ rule family="ipv6" icmp-type name="redirect" accept
++ rule family="ipv6" icmp-type name="mld-listener-done" accept
++ rule family="ipv6" icmp-type name="mld-listener-query" accept
++ rule family="ipv6" icmp-type name="mld-listener-report" accept
++ rule family="ipv6" icmp-type name="mld2-listener-report" accept
+ ])])
+ FWD_CHECK([--permanent --policy allow-host-ipv6 --list-all | TRIM_WHITESPACE], 0, [m4_strip([dnl
+ allow-host-ipv6 (active)
+@@ -224,6 +244,10 @@ allow-host-ipv6 (active)
+ rule family="ipv6" icmp-type name="neighbour-solicitation" accept
+ rule family="ipv6" icmp-type name="router-advertisement" accept
+ rule family="ipv6" icmp-type name="redirect" accept
++ rule family="ipv6" icmp-type name="mld-listener-done" accept
++ rule family="ipv6" icmp-type name="mld-listener-query" accept
++ rule family="ipv6" icmp-type name="mld-listener-report" accept
++ rule family="ipv6" icmp-type name="mld2-listener-report" accept
+ ])])
+
+ FWD_END_TEST
+diff --git a/src/tests/regression/rhbz2222044.at b/src/tests/regression/rhbz2222044.at
+index 2d0333865076..cfa8b32d6c86 100644
+--- a/src/tests/regression/rhbz2222044.at
++++ b/src/tests/regression/rhbz2222044.at
+@@ -11,7 +11,7 @@ dnl rules have not changed so rule count should not change
+ m4_define([check_rule_count], [
+ m4_if(nftables, FIREWALL_BACKEND, [
+ NS_CHECK([nft list table inet firewalld | wc -l], 0, [dnl
+-326
++330
+ ])
+ ], [ dnl iptables
+ NS_CHECK([iptables-save | wc -l], 0, [dnl
+--
+2.47.3
+
diff --git a/SPECS/firewalld.spec b/SPECS/firewalld.spec
index 649cc18..0a0775d 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: 1.3.4
-Release: 15%{?dist}
+Release: 18%{?dist}
URL: http://www.firewalld.org
License: GPLv2+
Source0: https://github.com/firewalld/firewalld/releases/download/v%{version}/firewalld-%{version}.tar.bz2
@@ -39,6 +39,20 @@ Patch31: 0031-v2.4.0-fix-fw-start-remove-ipset-probe.patch
Patch32: 0032-v2.4.0-fix-systemd-allow-start-code-251-RUNNING_BUT_FAILED.patch
Patch33: 0033-v2.4.0-fix-policy-rich-verify-ipset-exists.patch
Patch34: 0034-v2.4.0-test-rich-rule-reference-invalid-ipset.patch
+Patch35: 0035-v2.2.0-feat-add-iperf-2-3-services.patch
+Patch36: 0036-v2.4.0-test-functions-add-macro-WAIT_UNTIL.patch
+Patch37: 0037-v2.4.0-fix-server-load-firewall-rules-before-claimin.patch
+Patch38: 0038-v2.4.0-Revert-fix-systemd-allow-start-code-251-RUNNI.patch
+Patch39: 0039-v2.4.0-Revert-fix-systemd-verify-firewalld-is-respon.patch
+Patch40: 0040-v2.4.0-fix-nftables-ipset-add-entries-from-GLib-loop.patch
+Patch41: 0041-v2.4.0-test-ipset-scale-verify-all-the-entries-were-.patch
+Patch42: 0042-v2.4.0-fix-systemd-Requires-dbus.patch
+Patch43: 0043-v2.4.0-chore-icmp-add-all-icmptypes-to-ICMP_TYPES-di.patch
+Patch44: 0044-v2.4.0-chore-icmp-convert-type-code-map-to-tuple.patch
+Patch45: 0045-v2.4.0-chore-nftables-simplify-icmp-match-fragments.patch
+Patch46: 0046-v2.4.0-chore-nftables-move-_icmp_types_fragments-ins.patch
+Patch47: 0047-v2.4.0-chore-ipXtables-simplify-icmp-match-fragments.patch
+Patch48: 0048-v2.4.0-fix-policy-allow-host-ipv6-allow-MLD-packets.patch
BuildArch: noarch
BuildRequires: autoconf
BuildRequires: automake
@@ -262,6 +276,17 @@ rm -rf %{buildroot}%{_datadir}/firewalld/testsuite
%{_mandir}/man1/firewall-config*.1*
%changelog
+* Tue Dec 02 2025 Eric Garver - 1.3.4-18
+- fix(policy): allow-host-ipv6: allow MLD packets
+
+* Tue Dec 02 2025 Eric Garver - 1.3.4-17
+- fix(server): load firewall rules before claiming dbus
+- fix(nftables): ipset: add entries from GLib loop when idle
+- fix(systemd): Requires dbus
+
+* Tue Dec 02 2025 Eric Garver - 1.3.4-16
+- feat: add iperf{2,3} services
+
* Tue Jun 17 2025 Eric Garver - 1.3.4-15
- fix(policy): rich: verify ipset exists