From b9a8014e4140bf86c1318b8b71ec53718df1f8c6 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 6 Mar 2026 08:04:56 +0100 Subject: [PATCH] patches: lift iptables name length limit in firewalld A legacy name length limit in firewalld triggered problems with longer policy names. This patch to firewalld lifts that limit by checking the backend in use, no limit for nftables. Fixes #1389 Signed-off-by: Joachim Wiberg --- ...-Silence-warnings-about-old-backends.patch | 2 +- ...ft-iptables-name-length-limit-when-u.patch | 95 +++++++++++++++++++ test/case/firewall/basic/test.adoc | 2 +- test/case/firewall/basic/test.py | 22 +++-- 4 files changed, 109 insertions(+), 12 deletions(-) create mode 100644 patches/firewalld/2.3.1/0002-fix-functions-lift-iptables-name-length-limit-when-u.patch diff --git a/patches/firewalld/2.3.1/0001-Silence-warnings-about-old-backends.patch b/patches/firewalld/2.3.1/0001-Silence-warnings-about-old-backends.patch index 6bc17856..eed16ba6 100644 --- a/patches/firewalld/2.3.1/0001-Silence-warnings-about-old-backends.patch +++ b/patches/firewalld/2.3.1/0001-Silence-warnings-about-old-backends.patch @@ -1,7 +1,7 @@ From 03f273fc540082d1eaa23bd9b5847e695afd8283 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 25 Sep 2025 15:00:54 +0200 -Subject: [PATCH] Silence warnings about old backends +Subject: [PATCH 1/2] Silence warnings about old backends Organization: Wires Signed-off-by: Joachim Wiberg diff --git a/patches/firewalld/2.3.1/0002-fix-functions-lift-iptables-name-length-limit-when-u.patch b/patches/firewalld/2.3.1/0002-fix-functions-lift-iptables-name-length-limit-when-u.patch new file mode 100644 index 00000000..da603bd2 --- /dev/null +++ b/patches/firewalld/2.3.1/0002-fix-functions-lift-iptables-name-length-limit-when-u.patch @@ -0,0 +1,95 @@ +From 6ab218fe7f2c7027cc5347e3b082285870c502e6 Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Fri, 6 Mar 2026 07:44:38 +0100 +Subject: [PATCH 2/2] fix(functions): lift iptables name length limit when + using nftables +Organization: Wires + +The max_zone_name_len() and max_policy_name_len() functions return 17 +and 18 respectively, derived from iptables' 28-char netfilter chain name +limit. These limits are applied unconditionally in Zone.check_name() +and Policy.check_name() regardless of the active backend. + +When FirewallBackend=nftables nftables imposes no such restriction, so +user-defined zone and policy names (e.g. "appletv-to-lan-guest", 20 +chars) that exceed the iptables-derived limit are incorrectly rejected. + +Add _nftables_backend() which reads firewalld.conf directly so the check +can be skipped without threading backend context through to check_name() +call sites, which have no access to all_io_objects. When nftables is +active, both functions return sys.maxsize, effectively disabling the +length check. + +Signed-off-by: Joachim Wiberg +--- + src/firewall/functions.py | 31 ++++++++++++++++++++++++++++++- + 1 file changed, 30 insertions(+), 1 deletion(-) + +diff --git a/src/firewall/functions.py b/src/firewall/functions.py +index 27c862fd..1b8a32ce 100644 +--- a/src/firewall/functions.py ++++ b/src/firewall/functions.py +@@ -10,9 +10,10 @@ import os + import os.path + import shlex + import string ++import sys + import tempfile + from firewall.core.logger import log +-from firewall.config import FIREWALLD_TEMPDIR, FIREWALLD_PIDFILE ++from firewall.config import FIREWALLD_CONF, FIREWALLD_TEMPDIR, FIREWALLD_PIDFILE + + NOPRINT_TRANS_TABLE = { + # Limit to C0 and C1 code points. Building entries for all unicode code +@@ -576,12 +577,35 @@ def ppid_of_pid(pid): + return pid + + ++def _nftables_backend(): ++ """Return True if FirewallBackend=nftables is configured in firewalld.conf. ++ ++ When using nftables the iptables-derived 28-char chain name limit does not ++ apply. Reading the config file directly avoids threading backend context ++ through check_name() call sites, which have no access to all_io_objects. ++ """ ++ try: ++ with open(FIREWALLD_CONF) as f: ++ for line in f: ++ line = line.strip() ++ if line.startswith("FirewallBackend="): ++ return line.split("=", 1)[1].strip() == "nftables" ++ except OSError: ++ pass ++ return False ++ ++ + def max_policy_name_len(): + """ + iptables limits length of chain to (currently) 28 chars. + The longest chain we create is POST__allow, + which leaves 28 - 11 = 17 chars for . ++ ++ When using the nftables backend, nftables imposes no practical name length ++ restriction, so we return sys.maxsize to lift the check entirely. + """ ++ if _nftables_backend(): ++ return sys.maxsize + from firewall.core.ipXtables import POLICY_CHAIN_PREFIX + from firewall.core.base import SHORTCUTS + +@@ -594,7 +618,12 @@ def max_zone_name_len(): + Netfilter limits length of chain to (currently) 28 chars. + The longest chain we create is POST__allow, + which leaves 28 - 11 = 17 chars for . ++ ++ When using the nftables backend, nftables imposes no practical name length ++ restriction, so we return sys.maxsize to lift the check entirely. + """ ++ if _nftables_backend(): ++ return sys.maxsize + from firewall.core.base import SHORTCUTS + + longest_shortcut = max(map(len, SHORTCUTS.values())) +-- +2.43.0 + diff --git a/test/case/firewall/basic/test.adoc b/test/case/firewall/basic/test.adoc index 78ec4c4a..e04830c7 100644 --- a/test/case/firewall/basic/test.adoc +++ b/test/case/firewall/basic/test.adoc @@ -8,7 +8,7 @@ Firewall configuration suitable for end devices on untrusted networks. image::basic.svg[align=center, scaledwidth=50%] -- Single zone configuration, "public", with action=drop +- Single zone configuration, "public-untrusted-net", with `action=drop` - Allowed services: SSH (port 22), DHCPv6-client, mySSH (custom, port 222) - All other ports (HTTP, HTTPS, Telnet, etc.) blocked - Check that unused interfaces are automatically assigned to default zone diff --git a/test/case/firewall/basic/test.py b/test/case/firewall/basic/test.py index c93cc341..fa21fd18 100755 --- a/test/case/firewall/basic/test.py +++ b/test/case/firewall/basic/test.py @@ -5,7 +5,7 @@ Firewall configuration suitable for end devices on untrusted networks. image::basic.svg[align=center, scaledwidth=50%] -- Single zone configuration, "public", with action=drop +- Single zone configuration, "public-untrusted-net", with `action=drop` - Allowed services: SSH (port 22), DHCPv6-client, mySSH (custom, port 222) - All other ports (HTTP, HTTPS, Telnet, etc.) blocked - Check that unused interfaces are automatically assigned to default zone @@ -47,7 +47,7 @@ with infamy.Test() as test: target.put_config_dict("infix-firewall", { "firewall": { - "default": "public", + "default": "public-untrusted-net", "logging": "all", "service": [{ "name": "mySSH", @@ -69,7 +69,9 @@ with infamy.Test() as test: "interface": [mgmt_if], "service": ["ssh", "netconf", "restconf"] }, { - "name": "public", + # 20-char name, exceeds old iptables-derived 17-char limit + # Verifies we allow long names with nftables, issue #1389 + "name": "public-untrusted-net", "description": "Public untrusted network", "action": "drop", "interface": [data_if], @@ -80,7 +82,7 @@ with infamy.Test() as test: # Wait for configuration to be activated infamy.Firewall.wait_for_operational(target, { - "public": {"action": "drop"}, + "public-untrusted-net": {"action": "drop"}, "mgmt": {"action": "accept"} }) @@ -88,7 +90,7 @@ with infamy.Test() as test: data = target.get_data("/infix-firewall:firewall") fw = data["firewall"] - assert fw["default"] == "public" + assert fw["default"] == "public-untrusted-net" services = {svc["name"]: svc for svc in fw.get("service", [])} assert "mySSH" in services, "Custom service mySSH not found" @@ -106,8 +108,8 @@ with infamy.Test() as test: assert int(port_entry["lower"]) == 8080 zones = {zone["name"]: zone for zone in fw["zone"]} - assert "public" in zones, "Public zone not found in configuration" - public_zone = zones["public"] + assert "public-untrusted-net" in zones, "public-untrusted-net zone not found in configuration" + public_zone = zones["public-untrusted-net"] assert public_zone["action"] == "drop" assert data_if in public_zone["interface"] assert "ssh" in public_zone["service"] @@ -119,13 +121,13 @@ with infamy.Test() as test: data = target.get_data("/infix-firewall:firewall") fw = data["firewall"] - assert fw["default"] == "public", "Default zone should be 'public'" + assert fw["default"] == "public-untrusted-net", "Default zone should be 'public-untrusted-net'" zones = {zone["name"]: zone for zone in fw["zone"]} - public_zone = zones["public"] + public_zone = zones["public-untrusted-net"] assert unused_if in public_zone["interface"], \ - f"Unused interface {unused_if} should be in default zone 'public', got interfaces: {public_zone['interface']}" + f"Unused interface {unused_if} should be in default zone 'public-untrusted-net', got interfaces: {public_zone['interface']}" with infamy.IsolatedMacVlan(host_data) as ns: ns.addip(HOST_IP)