From b3eac02716a8888153523d0edf6ffa6cbf7f2e26 Mon Sep 17 00:00:00 2001 From: Andrew Lukoshko Date: Wed, 11 Mar 2026 16:49:58 +0000 Subject: [PATCH] fix(nftables): batch ipset elements to avoid O(n^2) insertion cost Resolves: almalinux/almalinux-deploy#186 --- ...ch-ipset-elements-to-avoid-O-n2-inse.patch | 85 +++++++++++++++++++ SPECS/firewalld.spec | 9 +- 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 SOURCES/1000-fix-nftables-batch-ipset-elements-to-avoid-O-n2-inse.patch diff --git a/SOURCES/1000-fix-nftables-batch-ipset-elements-to-avoid-O-n2-inse.patch b/SOURCES/1000-fix-nftables-batch-ipset-elements-to-avoid-O-n2-inse.patch new file mode 100644 index 0000000..d5cbd97 --- /dev/null +++ b/SOURCES/1000-fix-nftables-batch-ipset-elements-to-avoid-O-n2-inse.patch @@ -0,0 +1,85 @@ +From ad48eb571ff4966c3ebfbbf12fe8adf293658e20 Mon Sep 17 00:00:00 2001 +From: Andrew Lukoshko +Date: Tue, 10 Mar 2026 13:30:54 +0100 +Subject: fix(nftables): batch ipset elements in set_restore to avoid O(n^2) + insertion cost + +set_restore() currently creates one "add element" nft operation per +ipset entry, then sends them in chunks of 1000 operations. On older +nftables/kernel combinations (e.g. nftables 1.0.4 / kernel 4.18 +shipped in RHEL 8) each incremental element insertion into an interval +set has O(n) cost proportional to the current set size, making the +overall complexity O(n^2). + +With 12,000 ipset entries this causes firewall-cmd --reload to take +~80 seconds on RHEL 8 with the nftables backend. Even on newer +systems (nftables 1.0.9 / kernel 5.14) batching yields a ~20% +improvement. + +This patch accumulates all element fragments and creates batched +"add element" operations - one per chunk of 1000 elements - instead +of one per entry. This lets nftables handle bulk element insertion +natively and reduces nft operations from N to ceil(N/1000). + +Benchmark (12k ipset entries, nftables backend): + RHEL 8 (nftables 1.0.4, kernel 4.18): ~79,100ms -> ~2,500ms + RHEL 9 (nftables 1.0.9, kernel 5.14): ~1,560ms -> ~1,240ms + +Backport of upstream PR #1544 for firewalld 0.9.11 (RHEL/AlmaLinux 8). +NOTE: erig0's original RHEL-8 backport was missing rules.clear() +between chunks, causing "No buffer space available" on large ipsets. +This version includes the fix. + +Fixes #933 +--- + src/firewall/core/nftables.py | 23 ++++++++++++----------- + 1 file changed, 12 insertions(+), 11 deletions(-) + +diff --git a/src/firewall/core/nftables.py b/src/firewall/core/nftables.py +index XXXXXXX..XXXXXXX 100644 +--- a/src/firewall/core/nftables.py ++++ b/src/firewall/core/nftables.py +@@ -1896,14 +1896,19 @@ class nftables(object): + fragment.append(entry_tokens[i]) + return [{"concat": fragment}] if len(type_format) > 1 else fragment + +- def build_set_add_rules(self, name, entry): ++ def build_set_add_rules(self, name, entries): + rules = [] +- element = self._set_entry_fragment(name, entry) ++ elements = [] ++ if not isinstance(entries, (list, tuple)): ++ entries = [entries] ++ for element in entries: ++ elements.extend(self._set_entry_fragment(name, element)) ++ + for family in ["inet", "ip", "ip6"]: + rules.append({"add": {"element": {"family": family, + "table": TABLE_NAME, + "name": name, +- "elem": element}}}) ++ "elem": elements}}}) + return rules + + def set_add(self, name, entry): +@@ -1952,13 +1957,9 @@ class nftables(object): + rules.extend(self.build_set_flush_rules(set_name)) + + # avoid large memory usage by chunking the entries +- chunk = 0 +- 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() +- chunk = 0 +- else: ++ for i in range(0, len(entries), 1000): ++ rules.extend(self.build_set_add_rules(set_name, entries[i : i + 1000])) ++ self.set_rules(rules, self._fw.get_log_denied()) ++ rules.clear() ++ else: + self.set_rules(rules, self._fw.get_log_denied()) +-- +2.43.5 diff --git a/SPECS/firewalld.spec b/SPECS/firewalld.spec index a1d729f..d866a90 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.11 -Release: 10%{?dist} +Release: 10%{?dist}.alma.1 URL: http://www.firewalld.org License: GPLv2+ Source0: https://github.com/firewalld/firewalld/releases/download/v%{version}/firewalld-%{version}.tar.gz @@ -37,6 +37,9 @@ Patch29: 0029-v2.0.0-feat-direct-avoid-iptables-flush-if-using-nft.patch Patch30: 0030-v2.0.0-test-direct-avoid-iptables-flush-if-using-nft.patch Patch31: 0031-v2.2.0-fix-service-update-highest-port-number-for-ce.patch +# AlmaLinux Patch +Patch1000: 1000-fix-nftables-batch-ipset-elements-to-avoid-O-n2-inse.patch + BuildArch: noarch BuildRequires: autoconf BuildRequires: automake @@ -237,6 +240,10 @@ desktop-file-install --delete-original \ %{_mandir}/man1/firewall-config*.1* %changelog +* Wed Mar 11 2026 Andrew Lukoshko - 0.9.11-10.alma.1 +- fix(nftables): batch ipset elements to avoid O(n^2) insertion cost +- Resolves: almalinux/almalinux-deploy#186 + * Tue Feb 04 2025 Eric Garver - 0.9.11-10 - fix(service): update highest port number for ceph