diff --git a/SOURCES/0005-v2.0.0-feat-service-add-OpenTelemetry-OTLP-service.patch b/SOURCES/0005-v2.0.0-feat-service-add-OpenTelemetry-OTLP-service.patch new file mode 100644 index 0000000..e38e72c --- /dev/null +++ b/SOURCES/0005-v2.0.0-feat-service-add-OpenTelemetry-OTLP-service.patch @@ -0,0 +1,53 @@ +From a27f6afa21de35aa98e5309430dbcab9e6056f9c Mon Sep 17 00:00:00 2001 +From: Pat Riehecky +Date: Wed, 1 Feb 2023 09:52:43 -0600 +Subject: [PATCH 05/22] v2.0.0: feat(service): add OpenTelemetry (OTLP) service + +(cherry picked from commit 77c7061cc191bec6d8a36d2666c2d3c3e0ccbb4a) +--- + config/Makefile.am | 1 + + config/services/opentelemetry.xml | 7 +++++++ + po/POTFILES.in | 1 + + 3 files changed, 9 insertions(+) + create mode 100644 config/services/opentelemetry.xml + +diff --git a/config/Makefile.am b/config/Makefile.am +index d66398563ff2..47f30c1566e0 100644 +--- a/config/Makefile.am ++++ b/config/Makefile.am +@@ -247,6 +247,7 @@ CONFIG_FILES = \ + services/ntp.xml \ + services/nut.xml \ + services/openvpn.xml \ ++ services/opentelemetry.xml \ + services/ovirt-imageio.xml \ + services/ovirt-storageconsole.xml \ + services/ovirt-vmconsole.xml \ +diff --git a/config/services/opentelemetry.xml b/config/services/opentelemetry.xml +new file mode 100644 +index 000000000000..46c0e5258957 +--- /dev/null ++++ b/config/services/opentelemetry.xml +@@ -0,0 +1,7 @@ ++ ++ ++ OTLP ++ OpenTelemetry Protocol (OTLP) specification describes the encoding, transport, and delivery mechanism of telemetry data between telemetry sources, intermediate nodes such as collectors and telemetry backends. ++ ++ ++ +diff --git a/po/POTFILES.in b/po/POTFILES.in +index f3c0595980f9..1c990542ac4d 100644 +--- a/po/POTFILES.in ++++ b/po/POTFILES.in +@@ -180,6 +180,7 @@ config/services/nrpe.xml + config/services/ntp.xml + config/services/nut.xml + config/services/openvpn.xml ++config/services/opentelemetry.xml + config/services/ovirt-imageio.xml + config/services/ovirt-storageconsole.xml + config/services/ovirt-vmconsole.xml +-- +2.43.5 + diff --git a/SOURCES/0006-v2.1.0-feat-icmp-add-ICMPv6-Multicast-Listener-Disco.patch b/SOURCES/0006-v2.1.0-feat-icmp-add-ICMPv6-Multicast-Listener-Disco.patch new file mode 100644 index 0000000..0c38583 --- /dev/null +++ b/SOURCES/0006-v2.1.0-feat-icmp-add-ICMPv6-Multicast-Listener-Disco.patch @@ -0,0 +1,131 @@ +From 6f221d65193cda838e241a18dd07b6da2ae22f78 Mon Sep 17 00:00:00 2001 +From: Thomas Haller +Date: Wed, 29 Nov 2023 17:02:07 +0100 +Subject: [PATCH 06/22] v2.1.0: feat(icmp): add ICMPv6 Multicast Listener + Discovery (MLD) types + +Note that ip6tables does not support these ICMPv6 types. Currently, +the name of the ICMP types in firewalld must correspond to the names +in iptables. As ip6tables doesn't support it, it does not. If ip6tables +adds support for "mld-listener-query", but calls it differently, we have +a problem. Nothing that can be done about that. + +`man nft` also lists an alias "mld-listener-reduction" (for +"mld-listener-done", type 132). That alias is not supported. Use the +name as from RFC 4890. + +(cherry picked from commit dd88bbf812e0a50766b69c2bf12470ecf9d2466a) +--- + config/Makefile.am | 4 ++++ + config/icmptypes/mld-listener-done.xml | 7 +++++++ + config/icmptypes/mld-listener-query.xml | 7 +++++++ + config/icmptypes/mld-listener-report.xml | 7 +++++++ + config/icmptypes/mld2-listener-report.xml | 7 +++++++ + po/POTFILES.in | 4 ++++ + src/firewall/core/nftables.py | 4 ++++ + 7 files changed, 40 insertions(+) + create mode 100644 config/icmptypes/mld-listener-done.xml + create mode 100644 config/icmptypes/mld-listener-query.xml + create mode 100644 config/icmptypes/mld-listener-report.xml + create mode 100644 config/icmptypes/mld2-listener-report.xml + +diff --git a/config/Makefile.am b/config/Makefile.am +index 47f30c1566e0..edae25fd9de0 100644 +--- a/config/Makefile.am ++++ b/config/Makefile.am +@@ -83,6 +83,10 @@ CONFIG_FILES = \ + icmptypes/host-unknown.xml \ + icmptypes/host-unreachable.xml \ + icmptypes/ip-header-bad.xml \ ++ icmptypes/mld-listener-done.xml \ ++ icmptypes/mld-listener-query.xml \ ++ icmptypes/mld-listener-report.xml \ ++ icmptypes/mld2-listener-report.xml \ + icmptypes/neighbour-advertisement.xml \ + icmptypes/neighbour-solicitation.xml \ + icmptypes/network-prohibited.xml \ +diff --git a/config/icmptypes/mld-listener-done.xml b/config/icmptypes/mld-listener-done.xml +new file mode 100644 +index 000000000000..09b8bbba5b90 +--- /dev/null ++++ b/config/icmptypes/mld-listener-done.xml +@@ -0,0 +1,7 @@ ++ ++ ++ MLD Listener Done ++ ICMPv6 Link-Local Multicast Listener Discovery (MDL) of type Multicast Listener Done (type 132) (RFC 4890 section 4.4.1). Also known as mld-listener-reduction to nft. ++ ++ ++ +diff --git a/config/icmptypes/mld-listener-query.xml b/config/icmptypes/mld-listener-query.xml +new file mode 100644 +index 000000000000..418685578d1d +--- /dev/null ++++ b/config/icmptypes/mld-listener-query.xml +@@ -0,0 +1,7 @@ ++ ++ ++ MLD Listener Query ++ ICMPv6 Link-Local Multicast Listener Discovery (MDL) of type Multicast Listener Query (type 130) (RFC 4890 section 4.4.1). ++ ++ ++ +diff --git a/config/icmptypes/mld-listener-report.xml b/config/icmptypes/mld-listener-report.xml +new file mode 100644 +index 000000000000..98fb4161b298 +--- /dev/null ++++ b/config/icmptypes/mld-listener-report.xml +@@ -0,0 +1,7 @@ ++ ++ ++ MLD Listener Report ++ ICMPv6 Link-Local Multicast Listener Discovery (MDL) of type Multicast Listener Report (type 131) (RFC 4890 section 4.4.1). ++ ++ ++ +diff --git a/config/icmptypes/mld2-listener-report.xml b/config/icmptypes/mld2-listener-report.xml +new file mode 100644 +index 000000000000..faee68c95b20 +--- /dev/null ++++ b/config/icmptypes/mld2-listener-report.xml +@@ -0,0 +1,7 @@ ++ ++ ++ MLDv2 Multicast Listener Report ++ ICMPv6 Link-Local Multicast Listener Discovery (MDLv2) of type Multicast Listener Report (type 143) (RFC 4890 section 4.4.1). ++ ++ ++ +diff --git a/po/POTFILES.in b/po/POTFILES.in +index 1c990542ac4d..adeebdee3f55 100644 +--- a/po/POTFILES.in ++++ b/po/POTFILES.in +@@ -15,6 +15,10 @@ config/icmptypes/host-redirect.xml + config/icmptypes/host-unknown.xml + config/icmptypes/host-unreachable.xml + config/icmptypes/ip-header-bad.xml ++config/icmptypes/mld-listener-done.xml ++config/icmptypes/mld-listener-query.xml ++config/icmptypes/mld-listener-report.xml ++config/icmptypes/mld2-listener-report.xml + config/icmptypes/neighbour-advertisement.xml + config/icmptypes/neighbour-solicitation.xml + config/icmptypes/network-prohibited.xml +diff --git a/src/firewall/core/nftables.py b/src/firewall/core/nftables.py +index 6ad4b9168403..3df3fa3c3742 100644 +--- a/src/firewall/core/nftables.py ++++ b/src/firewall/core/nftables.py +@@ -140,6 +140,10 @@ ICMP_TYPES_FRAGMENTS = { + "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), +-- +2.43.5 + diff --git a/SOURCES/0007-v2.1.0-fix-rich-validate-service-name-of-rich-rule.patch b/SOURCES/0007-v2.1.0-fix-rich-validate-service-name-of-rich-rule.patch new file mode 100644 index 0000000..23f9e3c --- /dev/null +++ b/SOURCES/0007-v2.1.0-fix-rich-validate-service-name-of-rich-rule.patch @@ -0,0 +1,71 @@ +From 22b100b8ac9aeeacae851e2b9f11e4dc1741cd85 Mon Sep 17 00:00:00 2001 +From: Thomas Haller +Date: Tue, 12 Dec 2023 14:58:07 +0100 +Subject: [PATCH 07/22] v2.1.0: fix(rich): validate service name of rich rule + +Previously, validation of valid service names was not done. +That meant: + + $ firewall-cmd --add-rich-rule='rule priority="-100" family="ipv4" source address="10.0.0.10" service name="listen" accept' --permanent + success + $ firewall-cmd --reload + Error: INVALID_SERVICE: listen + +which left firewalld in a bad state. + +Now: + + $ firewall-cmd --add-rich-rule='rule priority="-100" family="ipv4" source address="10.0.0.10" service name="listen" accept' --permanent + Error: INVALID_SERVICE: Zone 'public': 'listen' not among existing services + +https://issues.redhat.com/browse/RHEL-5790 +(cherry picked from commit fbcdddd3e38c31a7b8325bf02764b84344c216b0) +--- + src/firewall/core/io/policy.py | 8 ++++++++ + src/tests/features/rich_rules.at | 7 ++++++- + 2 files changed, 14 insertions(+), 1 deletion(-) + +diff --git a/src/firewall/core/io/policy.py b/src/firewall/core/io/policy.py +index 7d383abb0a2d..f9a1114d7969 100644 +--- a/src/firewall/core/io/policy.py ++++ b/src/firewall/core/io/policy.py +@@ -471,6 +471,14 @@ def common_check_config(obj, config, item, all_config, all_io_objects): + log.debug1("{} (unsupported)".format(ex)) + else: + raise ex ++ elif isinstance(obj_rich.element, rich.Rich_Service): ++ if obj_rich.element.name not in all_io_objects["services"]: ++ raise FirewallError( ++ errors.INVALID_SERVICE, ++ "{} '{}': '{}' not among existing services".format( ++ obj_type, obj.name, obj_rich.element.name ++ ), ++ ) + + def common_writer(obj, handler): + # short +diff --git a/src/tests/features/rich_rules.at b/src/tests/features/rich_rules.at +index aadc76da57b4..f7d1a1d0abf4 100644 +--- a/src/tests/features/rich_rules.at ++++ b/src/tests/features/rich_rules.at +@@ -46,6 +46,10 @@ FWD_CHECK([--permanent --policy foobar --add-rich-rule='rule family=ipv4 priorit + FWD_CHECK([--permanent --policy foobar --add-rich-rule='rule family=ipv4 priority=0 source address=10.10.10.13 drop'], 0, ignore) + FWD_CHECK([--permanent --policy foobar --add-rich-rule='rule family=ipv4 priority=-1 source address=10.10.10.14 accept'], 0, ignore) + FWD_CHECK([--permanent --policy foobar --add-rich-rule='rule family=ipv4 priority=1 source address=10.10.10.15 accept'], 0, ignore) ++ ++dnl Invalid service name is rejected. ++FWD_CHECK([--permanent --policy foobar --add-rich-rule='rule priority="-100" family="ipv4" source address="10.0.0.10" service name="bogusservice" accept'], 101, ignore, ignore) ++ + FWD_RELOAD + NFT_LIST_RULES([inet], [filter_IN_policy_foobar_pre], 0, [dnl + table inet firewalld { +@@ -319,4 +323,5 @@ IP6TABLES_LIST_RULES([filter], [IN_foobar_post], 0, [dnl + ACCEPT 0 -- ::/0 ::/0 + ]) + +-FWD_END_TEST([-e '/ERROR: INVALID_ZONE:/d']) ++FWD_END_TEST([-e '/ERROR: INVALID_ZONE:/d' dnl ++ -e "/ERROR: INVALID_SERVICE: Policy 'foobar': 'bogusservice' not among existing services/d"]) +-- +2.43.5 + diff --git a/SOURCES/0008-v2.1.0-improvement-nftables-do-not-track-rule-handle.patch b/SOURCES/0008-v2.1.0-improvement-nftables-do-not-track-rule-handle.patch new file mode 100644 index 0000000..11cb584 --- /dev/null +++ b/SOURCES/0008-v2.1.0-improvement-nftables-do-not-track-rule-handle.patch @@ -0,0 +1,66 @@ +From 11ee9b9ed8da78bfc11edffc2c9386efa41be1cf Mon Sep 17 00:00:00 2001 +From: Eric Garver +Date: Mon, 18 Dec 2023 18:22:38 -0500 +Subject: [PATCH 08/22] v2.1.0: improvement(nftables): do not track rule + handles for policy table + +It's not necessary. This table is transient and we simply delete the +entire table when we're done with it. + +(cherry picked from commit 119dff1d86f841cd2f33ddbab278bc9257dae7b0) +--- + src/firewall/core/nftables.py | 24 +++++++----------------- + 1 file changed, 7 insertions(+), 17 deletions(-) + +diff --git a/src/firewall/core/nftables.py b/src/firewall/core/nftables.py +index 3df3fa3c3742..690a5dc067ab 100644 +--- a/src/firewall/core/nftables.py ++++ b/src/firewall/core/nftables.py +@@ -386,6 +386,11 @@ class nftables(object): + if verb not in output["nftables"][index]: + continue + ++ # don't bother tracking handles for the policy table as we simply ++ # delete the entire table. ++ if TABLE_NAME_POLICY == output["nftables"][index][verb]["rule"]["table"]: ++ continue ++ + self.rule_to_handle[rule_key] = output["nftables"][index][verb]["rule"]["handle"] + + def set_rule(self, rule, log_denied): +@@ -408,18 +413,8 @@ class nftables(object): + "name": table}}}] + + def build_flush_rules(self): +- # Policy is stashed in a separate table that we're _not_ going to +- # flush. As such, we retain the policy rule handles and ref counts. +- saved_rule_to_handle = {} +- saved_rule_ref_count = {} +- for rule in self._build_set_policy_rules_ct_rules(True): +- policy_key = self._get_rule_key(rule) +- if policy_key in self.rule_to_handle: +- saved_rule_to_handle[policy_key] = self.rule_to_handle[policy_key] +- saved_rule_ref_count[policy_key] = self.rule_ref_count[policy_key] +- +- self.rule_to_handle = saved_rule_to_handle +- self.rule_ref_count = saved_rule_ref_count ++ self.rule_to_handle = {} ++ self.rule_ref_count = {} + self.rich_rule_priority_counts = {} + self.policy_priority_counts = {} + self.zone_source_index_cache = {} +@@ -475,11 +470,6 @@ class nftables(object): + + rules += self._build_set_policy_rules_ct_rules(True) + elif policy == "ACCEPT": +- for rule in self._build_set_policy_rules_ct_rules(False): +- policy_key = self._get_rule_key(rule) +- if policy_key in self.rule_to_handle: +- rules.append(rule) +- + rules += self._build_delete_table_rules(TABLE_NAME_POLICY) + else: + raise FirewallError(UNKNOWN_ERROR, "not implemented") +-- +2.43.5 + diff --git a/SOURCES/0009-v2.1.0-improvement-fw-make-set_policy-DROP-more-flex.patch b/SOURCES/0009-v2.1.0-improvement-fw-make-set_policy-DROP-more-flex.patch new file mode 100644 index 0000000..cc01fd2 --- /dev/null +++ b/SOURCES/0009-v2.1.0-improvement-fw-make-set_policy-DROP-more-flex.patch @@ -0,0 +1,203 @@ +From c53dabcb9ca5c6d9ab2b076d961127a67afe8f8f Mon Sep 17 00:00:00 2001 +From: Thomas Haller +Date: Fri, 11 Aug 2023 18:16:20 +0200 +Subject: [PATCH 09/22] v2.1.0: improvement(fw): make set_policy("DROP") more + flexible + +We will add a reload-policy via + + ReloadPolicy=OUTPUT:{ACCEPT,REJECT,DROP},INPUT:{ACCEPT,REJECT,DROP},FORWARD:{ACCEPT,REJECT,DROP} + +Extend set_policy() so that the "DROP" policy can be overridden. + +(cherry picked from commit e3bb468ff469373d193398b471a59f7ab7d29f27) +--- + src/firewall/core/ebtables.py | 2 +- + src/firewall/core/fw.py | 27 +++++++++++++--- + src/firewall/core/ipXtables.py | 11 +++++-- + src/firewall/core/nftables.py | 56 ++++++++++++++++++++++++---------- + 4 files changed, 72 insertions(+), 24 deletions(-) + +diff --git a/src/firewall/core/ebtables.py b/src/firewall/core/ebtables.py +index c1c0b8587a5c..f059975724a5 100644 +--- a/src/firewall/core/ebtables.py ++++ b/src/firewall/core/ebtables.py +@@ -237,7 +237,7 @@ class ebtables(object): + rules.append(["-t", table, flag]) + return rules + +- def build_set_policy_rules(self, policy): ++ def build_set_policy_rules(self, policy, policy_details): + rules = [] + _policy = "DROP" if policy == "PANIC" else policy + for table in BUILT_IN_CHAINS.keys(): +diff --git a/src/firewall/core/fw.py b/src/firewall/core/fw.py +index f1bc124b9443..ccec875f3c3c 100644 +--- a/src/firewall/core/fw.py ++++ b/src/firewall/core/fw.py +@@ -983,7 +983,18 @@ class Firewall(object): + if use_transaction is None: + transaction.execute(True) + +- def set_policy(self, policy, use_transaction=None): ++ def _set_policy_build_rules(self, backend, policy, policy_details=None): ++ assert policy in ("ACCEPT", "DROP", "PANIC") ++ if policy_details is None: ++ dp = "ACCEPT" if policy == "ACCEPT" else "DROP" ++ policy_details = { ++ "INPUT": dp, ++ "OUTPUT": dp, ++ "FORWARD": dp, ++ } ++ return backend.build_set_policy_rules(policy, policy_details) ++ ++ def set_policy(self, policy, policy_details=None, use_transaction=None): + if use_transaction is None: + transaction = FirewallTransaction(self) + else: +@@ -992,7 +1003,7 @@ class Firewall(object): + log.debug1("Setting policy to '%s'", policy) + + for backend in self.enabled_backends(): +- rules = backend.build_set_policy_rules(policy) ++ rules = self._set_policy_build_rules(backend, policy, policy_details) + transaction.add_rules(backend, rules) + + if use_transaction is None: +@@ -1224,13 +1235,19 @@ class Firewall(object): + # for the old backend that was set to DROP above. + if not self._panic and old_firewall_backend != self._firewall_backend: + if old_firewall_backend == "nftables": +- for rule in self.nftables_backend.build_set_policy_rules("ACCEPT"): ++ for rule in self._set_policy_build_rules( ++ self.nftables_backend, "ACCEPT" ++ ): + self.nftables_backend.set_rule(rule, self._log_denied) + else: +- for rule in self.ip4tables_backend.build_set_policy_rules("ACCEPT"): ++ for rule in self._set_policy_build_rules( ++ self.ip4tables_backend, "ACCEPT" ++ ): + self.ip4tables_backend.set_rule(rule, self._log_denied) + if self.ip6tables_enabled: +- for rule in self.ip6tables_backend.build_set_policy_rules("ACCEPT"): ++ for rule in self._set_policy_build_rules( ++ self.ip6tables_backend, "ACCEPT" ++ ): + self.ip6tables_backend.set_rule(rule, self._log_denied) + + if start_exception: +diff --git a/src/firewall/core/ipXtables.py b/src/firewall/core/ipXtables.py +index e05a2bd4d7ed..1a0cea7a3b4e 100644 +--- a/src/firewall/core/ipXtables.py ++++ b/src/firewall/core/ipXtables.py +@@ -578,7 +578,7 @@ class ip4tables(object): + rules.append(["-t", table, flag]) + return rules + +- def build_set_policy_rules(self, policy): ++ def build_set_policy_rules(self, policy, policy_details): + rules = [] + _policy = "DROP" if policy == "PANIC" else policy + for table in BUILT_IN_CHAINS.keys(): +@@ -587,7 +587,14 @@ class ip4tables(object): + if table == "nat": + continue + for chain in BUILT_IN_CHAINS[table]: +- rules.append(["-t", table, "-P", chain, _policy]) ++ if table == "filter": ++ p = policy_details[chain] ++ if p == "REJECT": ++ rules.append(["-t", table, "-A", chain, "-j", "REJECT"]) ++ p = "DROP" ++ else: ++ p = _policy ++ rules.append(["-t", table, "-P", chain, p]) + return rules + + def supported_icmp_types(self, ipv=None): +diff --git a/src/firewall/core/nftables.py b/src/firewall/core/nftables.py +index 690a5dc067ab..e9816147ef8e 100644 +--- a/src/firewall/core/nftables.py ++++ b/src/firewall/core/nftables.py +@@ -421,20 +421,17 @@ class nftables(object): + + return self._build_delete_table_rules(TABLE_NAME) + +- def _build_set_policy_rules_ct_rules(self, enable): ++ def _build_set_policy_rules_ct_rule(self, enable, hook): + add_del = { True: "add", False: "delete" }[enable] +- rules = [] +- for hook in ["input", "forward", "output"]: +- rules.append({add_del: {"rule": {"family": "inet", +- "table": TABLE_NAME_POLICY, +- "chain": "%s_%s" % ("filter", hook), +- "expr": [{"match": {"left": {"ct": {"key": "state"}}, +- "op": "in", +- "right": {"set": ["established", "related"]}}}, +- {"accept": None}]}}}) +- return rules +- +- def build_set_policy_rules(self, policy): ++ return {add_del: {"rule": {"family": "inet", ++ "table": TABLE_NAME_POLICY, ++ "chain": "%s_%s" % ("filter", hook), ++ "expr": [{"match": {"left": {"ct": {"key": "state"}}, ++ "op": "in", ++ "right": {"set": ["established", "related"]}}}, ++ {"accept": None}]}}} ++ ++ def build_set_policy_rules(self, policy, policy_details): + # Policy is not exposed to the user. It's only to make sure we DROP + # packets while reloading and for panic mode. As such, using hooks with + # a higher priority than our base chains is sufficient. +@@ -459,16 +456,43 @@ class nftables(object): + + # To drop everything except existing connections we use + # "filter" because it occurs _after_ conntrack. +- for hook in ["input", "forward", "output"]: ++ for hook in ("INPUT", "FORWARD", "OUTPUT"): ++ d_policy = policy_details[hook] ++ assert d_policy in ("ACCEPT", "REJECT", "DROP") ++ hook = hook.lower() ++ chain_name = f"filter_{hook}" ++ + rules.append({"add": {"chain": {"family": "inet", + "table": TABLE_NAME_POLICY, +- "name": "%s_%s" % ("filter", hook), ++ "name": chain_name, + "type": "filter", + "hook": hook, + "prio": 0 + NFT_HOOK_OFFSET - 1, + "policy": "drop"}}}) + +- rules += self._build_set_policy_rules_ct_rules(True) ++ rules.append(self._build_set_policy_rules_ct_rule(True, hook)) ++ ++ if d_policy == "ACCEPT": ++ expr_fragment = {"accept": None} ++ elif d_policy == "DROP": ++ expr_fragment = {"drop": None} ++ else: ++ expr_fragment = { ++ "reject": {"type": "icmpx", "expr": "admin-prohibited"} ++ } ++ ++ rules.append( ++ { ++ "add": { ++ "rule": { ++ "family": "inet", ++ "table": TABLE_NAME_POLICY, ++ "chain": chain_name, ++ "expr": [expr_fragment], ++ } ++ } ++ } ++ ) + elif policy == "ACCEPT": + rules += self._build_delete_table_rules(TABLE_NAME_POLICY) + else: +-- +2.43.5 + diff --git a/SOURCES/0010-v2.1.0-feat-fw-add-ReloadPolicy-option-in-firewalld..patch b/SOURCES/0010-v2.1.0-feat-fw-add-ReloadPolicy-option-in-firewalld..patch new file mode 100644 index 0000000..6bd4f1a --- /dev/null +++ b/SOURCES/0010-v2.1.0-feat-fw-add-ReloadPolicy-option-in-firewalld..patch @@ -0,0 +1,303 @@ +From 67c8a0010ba6244c40e48a93560eb66d91a2ca09 Mon Sep 17 00:00:00 2001 +From: Thomas Haller +Date: Mon, 14 Aug 2023 14:53:55 +0200 +Subject: [PATCH 10/22] v2.1.0: feat(fw): add ReloadPolicy option in + firewalld.conf + +One interesting aspect is that during `firewall-cmd --reload`, the code +first sets the policy to "DROP", before reloading "firewalld.conf". That +means, changing the value only takes effect after the next reload. But +that seems expected as we set the policy before starting to reload. + +Fixes: https://bugzilla.redhat.com/show_bug.cgi?id=2149039 +(cherry picked from commit 0019371a8f42d376ac9cce79cc5e1e7d2049f021) +--- + config/firewalld.conf | 8 +++ + doc/xml/firewalld.conf.xml | 16 ++++++ + src/firewall/config/__init__.py.in | 1 + + src/firewall/core/fw.py | 13 ++++- + src/firewall/core/io/firewalld_conf.py | 56 ++++++++++++++++++++- + src/tests/features/features.at | 1 + + src/tests/features/reloadpolicy.at | 12 +++++ + src/tests/unit/test_firewalld_conf.py | 68 ++++++++++++++++++++++++++ + 8 files changed, 172 insertions(+), 3 deletions(-) + create mode 100644 src/tests/features/reloadpolicy.at + create mode 100644 src/tests/unit/test_firewalld_conf.py + +diff --git a/config/firewalld.conf b/config/firewalld.conf +index f8caf11c8a86..7a0be1ff1b76 100644 +--- a/config/firewalld.conf ++++ b/config/firewalld.conf +@@ -66,6 +66,14 @@ FirewallBackend=nftables + # Default: yes + FlushAllOnReload=yes + ++# ReloadPolicy ++# Policy during reload. By default all traffic except for established ++# connections is dropped while the rules are updated. Set to "DROP", "REJECT" ++# or "ACCEPT". Alternatively, specify it per table, like ++# "OUTPUT:ACCEPT,INPUT:DROP,FORWARD:REJECT". ++# Default: ReloadPolicy=INPUT:DROP,FORWARD:DROP,OUTPUT:DROP ++ReloadPolicy=INPUT:DROP,FORWARD:DROP,OUTPUT:DROP ++ + # RFC3964_IPv4 + # As per RFC 3964, filter IPv6 traffic with 6to4 destination addresses that + # correspond to IPv4 addresses that should not be routed over the public +diff --git a/doc/xml/firewalld.conf.xml b/doc/xml/firewalld.conf.xml +index e4312acc8e1c..022569ccf502 100644 +--- a/doc/xml/firewalld.conf.xml ++++ b/doc/xml/firewalld.conf.xml +@@ -195,6 +195,22 @@ + + + ++ ++ ++ ++ ++ The policy during reload. By default, all traffic except ++ established connections is dropped while reloading the ++ firewall rules. This can be overridden for INPUT, FORWARD ++ and OUTPUT. The accepted values are "DROP", "REJECT" and ++ "ACCEPT", which then applies to all tables. Alternatively, ++ the policy can be specified per table, like ++ "INPUT:REJECT,FORWARD:DROP,OUTPUT:ACCEPT". ++ Defaults to "INPUT:DROP,FORWARD:DROP,OUTPUT:DROP". ++ ++ ++ ++ + + + +diff --git a/src/firewall/config/__init__.py.in b/src/firewall/config/__init__.py.in +index d982384a0382..da1e31e10e58 100644 +--- a/src/firewall/config/__init__.py.in ++++ b/src/firewall/config/__init__.py.in +@@ -133,5 +133,6 @@ FALLBACK_LOG_DENIED = "off" + FALLBACK_AUTOMATIC_HELPERS = "no" + FALLBACK_FIREWALL_BACKEND = "nftables" + FALLBACK_FLUSH_ALL_ON_RELOAD = True ++FALLBACK_RELOAD_POLICY = "INPUT:DROP,FORWARD:DROP,OUTPUT:DROP" + FALLBACK_RFC3964_IPV4 = True + FALLBACK_ALLOW_ZONE_DRIFTING = False +diff --git a/src/firewall/core/fw.py b/src/firewall/core/fw.py +index ccec875f3c3c..ac13be122b66 100644 +--- a/src/firewall/core/fw.py ++++ b/src/firewall/core/fw.py +@@ -1000,7 +1000,13 @@ class Firewall(object): + else: + transaction = use_transaction + +- log.debug1("Setting policy to '%s'", policy) ++ log.debug1( ++ "Setting policy to '%s'%s", ++ policy, ++ f" (ReloadPolicy={firewalld_conf._unparse_reload_policy(policy_details)})" ++ if policy == "DROP" ++ else "", ++ ) + + for backend in self.enabled_backends(): + rules = self._set_policy_build_rules(backend, policy, policy_details) +@@ -1146,7 +1152,10 @@ class Firewall(object): + _ipset_objs.append(self.ipset.get_ipset(_name)) + + if not _panic: +- self.set_policy("DROP") ++ reload_policy = firewalld_conf._parse_reload_policy( ++ self._firewalld_conf.get("ReloadPolicy") ++ ) ++ self.set_policy("DROP", policy_details=reload_policy) + + self.flush() + self.cleanup() +diff --git a/src/firewall/core/io/firewalld_conf.py b/src/firewall/core/io/firewalld_conf.py +index b907c5b1e60b..d2879b319d1f 100644 +--- a/src/firewall/core/io/firewalld_conf.py ++++ b/src/firewall/core/io/firewalld_conf.py +@@ -31,7 +31,7 @@ valid_keys = [ "DefaultZone", "MinimalMark", "CleanupOnExit", + "CleanupModulesOnExit", "Lockdown", "IPv6_rpfilter", + "IndividualCalls", "LogDenied", "AutomaticHelpers", + "FirewallBackend", "FlushAllOnReload", "RFC3964_IPv4", +- "AllowZoneDrifting" ] ++ "AllowZoneDrifting", "ReloadPolicy" ] + + class firewalld_conf(object): + def __init__(self, filename): +@@ -77,6 +77,7 @@ class firewalld_conf(object): + self.set("AutomaticHelpers", config.FALLBACK_AUTOMATIC_HELPERS) + self.set("FirewallBackend", config.FALLBACK_FIREWALL_BACKEND) + self.set("FlushAllOnReload", "yes" if config.FALLBACK_FLUSH_ALL_ON_RELOAD else "no") ++ self.set("ReloadPolicy", config.FALLBACK_RELOAD_POLICY) + self.set("RFC3964_IPv4", "yes" if config.FALLBACK_RFC3964_IPV4 else "no") + self.set("AllowZoneDrifting", "yes" if config.FALLBACK_ALLOW_ZONE_DRIFTING else "no") + +@@ -208,6 +209,17 @@ class firewalld_conf(object): + config.FALLBACK_FLUSH_ALL_ON_RELOAD) + self.set("FlushAllOnReload", str(config.FALLBACK_FLUSH_ALL_ON_RELOAD)) + ++ value = self.get("ReloadPolicy") ++ try: ++ value = self._parse_reload_policy(value) ++ except ValueError: ++ log.warning( ++ "ReloadPolicy '%s' is not valid, using default value '%s'", ++ value, ++ config.FALLBACK_RELOAD_POLICY, ++ ) ++ self.set("ReloadPolicy", config.FALLBACK_RELOAD_POLICY) ++ + value = self.get("RFC3964_IPv4") + if not value or value.lower() not in [ "yes", "true", "no", "false" ]: + if value is not None: +@@ -330,3 +342,45 @@ class firewalld_conf(object): + raise IOError("Failed to create '%s': %s" % (self.filename, msg)) + else: + os.chmod(self.filename, 0o600) ++ ++ @staticmethod ++ def _parse_reload_policy(value): ++ valid = True ++ result = { ++ "INPUT": "DROP", ++ "FORWARD": "DROP", ++ "OUTPUT": "DROP", ++ } ++ if value: ++ value = value.strip() ++ v = value.upper() ++ if v in ("ACCEPT", "REJECT", "DROP"): ++ for k in result: ++ result[k] = v ++ else: ++ for a in value.replace(";", ",").split(","): ++ a = a.strip() ++ if not a: ++ continue ++ a2 = a.replace("=", ":").split(":", 2) ++ if len(a2) != 2: ++ valid = False ++ continue ++ k = a2[0].strip().upper() ++ if k not in result: ++ valid = False ++ continue ++ v = a2[1].strip().upper() ++ if v not in ("ACCEPT", "REJECT", "DROP"): ++ valid = False ++ continue ++ result[k] = v ++ ++ if not valid: ++ raise ValueError("Invalid ReloadPolicy") ++ ++ return result ++ ++ @staticmethod ++ def _unparse_reload_policy(value): ++ return ",".join(f"{k}:{v}" for k, v in value.items()) +diff --git a/src/tests/features/features.at b/src/tests/features/features.at +index f59baea1cd70..065cb2872e88 100644 +--- a/src/tests/features/features.at ++++ b/src/tests/features/features.at +@@ -20,3 +20,4 @@ m4_include([features/startup_failsafe.at]) + m4_include([features/ipset.at]) + m4_include([features/reset_defaults.at]) + m4_include([features/iptables_no_flush_on_shutdown.at]) ++m4_include([features/reloadpolicy.at]) +diff --git a/src/tests/features/reloadpolicy.at b/src/tests/features/reloadpolicy.at +new file mode 100644 +index 000000000000..fea1aa26aab4 +--- /dev/null ++++ b/src/tests/features/reloadpolicy.at +@@ -0,0 +1,12 @@ ++FWD_START_TEST([check ReloadPolicy]) ++AT_KEYWORDS(reloadpolicy rhbz2149039) ++ ++AT_CHECK([sed -i 's/^ReloadPolicy=.*/ReloadPolicy=INPUT:REJECT,FORWARD:ACCEPT/' ./firewalld.conf]) ++dnl call RELOAD twice, to see more action about the ReloadPolicy. ++FWD_RELOAD() ++FWD_RELOAD() ++ ++AT_CHECK([sed -i 's/^ReloadPolicy=.*/ReloadPolicy=REJECT/' ./firewalld.conf]) ++FWD_RELOAD() ++ ++FWD_END_TEST() +diff --git a/src/tests/unit/test_firewalld_conf.py b/src/tests/unit/test_firewalld_conf.py +new file mode 100644 +index 000000000000..0ce1fd279f91 +--- /dev/null ++++ b/src/tests/unit/test_firewalld_conf.py +@@ -0,0 +1,68 @@ ++# SPDX-License-Identifier: GPL-2.0-or-later ++ ++import firewall.core.io.firewalld_conf ++import firewall.config ++ ++ ++def test_reload_policy(): ++ def t(value, expected_valid=True, **kw): ++ ++ expected = { ++ "INPUT": "DROP", ++ "FORWARD": "DROP", ++ "OUTPUT": "DROP", ++ } ++ for k, v in kw.items(): ++ assert k in expected ++ expected[k] = v ++ ++ try: ++ parsed = ( ++ firewall.core.io.firewalld_conf.firewalld_conf._parse_reload_policy( ++ value ++ ) ++ ) ++ except ValueError: ++ assert not expected_valid ++ return ++ ++ assert parsed == expected ++ assert expected_valid ++ ++ unparsed = ( ++ firewall.core.io.firewalld_conf.firewalld_conf._unparse_reload_policy( ++ parsed ++ ) ++ ) ++ parsed2 = firewall.core.io.firewalld_conf.firewalld_conf._parse_reload_policy( ++ unparsed ++ ) ++ assert parsed2 == parsed ++ ++ t(None) ++ t("") ++ t(" ") ++ t(" input: ACCept ", INPUT="ACCEPT") ++ t( ++ "forward:DROP, forward : REJEct; input: ACCept ", ++ INPUT="ACCEPT", ++ FORWARD="REJECT", ++ ) ++ t(" accept ", INPUT="ACCEPT", FORWARD="ACCEPT", OUTPUT="ACCEPT") ++ t("REJECT", INPUT="REJECT", FORWARD="REJECT", OUTPUT="REJECT") ++ t("forward=REJECT", FORWARD="REJECT") ++ t("forward=REJECT , input=accept", FORWARD="REJECT", INPUT="ACCEPT") ++ t("forward=REJECT , xinput=accept", expected_valid=False) ++ t("forward=REJECT, ACCEPT", expected_valid=False) ++ ++ def _norm(reload_policy): ++ parsed = firewall.core.io.firewalld_conf.firewalld_conf._parse_reload_policy( ++ reload_policy ++ ) ++ return firewall.core.io.firewalld_conf.firewalld_conf._unparse_reload_policy( ++ parsed ++ ) ++ ++ assert firewall.config.FALLBACK_RELOAD_POLICY == _norm( ++ firewall.config.FALLBACK_RELOAD_POLICY ++ ) +-- +2.43.5 + diff --git a/SOURCES/0011-v2.2.0-test-functions-add-macro-CHECK_NFTABLES_FIB.patch b/SOURCES/0011-v2.2.0-test-functions-add-macro-CHECK_NFTABLES_FIB.patch new file mode 100644 index 0000000..5f1ecd4 --- /dev/null +++ b/SOURCES/0011-v2.2.0-test-functions-add-macro-CHECK_NFTABLES_FIB.patch @@ -0,0 +1,27 @@ +From 55e40954a8c596fabe03371e9a508d3518273ac1 Mon Sep 17 00:00:00 2001 +From: Eric Garver +Date: Tue, 14 May 2024 16:27:54 -0400 +Subject: [PATCH 11/22] v2.2.0: test(functions): add macro CHECK_NFTABLES_FIB + +(cherry picked from commit 0aeebef07bc57b1f56b107632cdfdd809384398c) +--- + src/tests/functions.at | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/src/tests/functions.at b/src/tests/functions.at +index f454ca980046..65a4ce078e05 100644 +--- a/src/tests/functions.at ++++ b/src/tests/functions.at +@@ -748,3 +748,9 @@ m4_define([CHECK_NM_CAPABILITY_OVS], [ + m4_define([IF_BACKEND_IS_DEFAULT], [ + m4_if(nftables, FIREWALL_BACKEND, [$1], []) + ]) ++ ++m4_define([CHECK_NFTABLES_FIB], [ ++ m4_if(nftables, FIREWALL_BACKEND, [ ++ IF_HOST_SUPPORTS_NFT_FIB([], [AT_SKIP_IF([:])]) ++ ]) ++]) +-- +2.43.5 + diff --git a/SOURCES/0012-v2.2.0-test-functions-add-macro-CHECK_NFTABLES_FIB_I.patch b/SOURCES/0012-v2.2.0-test-functions-add-macro-CHECK_NFTABLES_FIB_I.patch new file mode 100644 index 0000000..5aa3048 --- /dev/null +++ b/SOURCES/0012-v2.2.0-test-functions-add-macro-CHECK_NFTABLES_FIB_I.patch @@ -0,0 +1,31 @@ +From d368d579c78652a68273897d5f8b5099d251a9b5 Mon Sep 17 00:00:00 2001 +From: Eric Garver +Date: Tue, 14 May 2024 16:21:06 -0400 +Subject: [PATCH 12/22] v2.2.0: test(functions): add macro + CHECK_NFTABLES_FIB_IN_FORWARD + +(cherry picked from commit b9cf7b75c7d94efa98545a3b7ad5020b1896b22a) +--- + src/tests/functions.at | 9 +++++++++ + 1 file changed, 9 insertions(+) + +diff --git a/src/tests/functions.at b/src/tests/functions.at +index 65a4ce078e05..b2372dd4075b 100644 +--- a/src/tests/functions.at ++++ b/src/tests/functions.at +@@ -754,3 +754,12 @@ m4_define([CHECK_NFTABLES_FIB], [ + IF_HOST_SUPPORTS_NFT_FIB([], [AT_SKIP_IF([:])]) + ]) + ]) ++ ++m4_define([CHECK_NFTABLES_FIB_IN_FORWARD], [ ++ m4_if(nftables, FIREWALL_BACKEND, [ ++ NS_CHECK([nft add table inet firewalld_check]) ++ NS_CHECK([nft add chain inet firewalld_check foobar { type filter hook forward priority 0 \; }]) ++ AT_SKIP_IF([! NS_CMD([nft add rule inet firewalld_check foobar meta nfproto ipv6 fib saddr . mark . iif oif missing drop >/dev/null 2>&1])]) ++ NS_CHECK([nft delete table inet firewalld_check]) ++ ]) ++]) +-- +2.43.5 + diff --git a/SOURCES/0013-v2.2.0-test-rpfilter-use-CHECK-macros.patch b/SOURCES/0013-v2.2.0-test-rpfilter-use-CHECK-macros.patch new file mode 100644 index 0000000..cd67df8 --- /dev/null +++ b/SOURCES/0013-v2.2.0-test-rpfilter-use-CHECK-macros.patch @@ -0,0 +1,51 @@ +From c1620d5ad4c151382373a138ab0c36dd7561a4bb Mon Sep 17 00:00:00 2001 +From: Eric Garver +Date: Tue, 14 May 2024 16:29:50 -0400 +Subject: [PATCH 13/22] v2.2.0: test(rpfilter): use CHECK macros + +(cherry picked from commit 352f3fc7fc00b675178de1eff8f0197607741de7) +--- + src/tests/features/rpfilter.at | 27 +++++++++++---------------- + 1 file changed, 11 insertions(+), 16 deletions(-) + +diff --git a/src/tests/features/rpfilter.at b/src/tests/features/rpfilter.at +index 01fb81ea75ef..ccc8a6cf5e80 100644 +--- a/src/tests/features/rpfilter.at ++++ b/src/tests/features/rpfilter.at +@@ -1,22 +1,17 @@ +-FWD_START_TEST([rpfilter]) ++FWD_START_TEST([rpfilter - strict]) + AT_KEYWORDS(rpfilter) ++CHECK_NFTABLES_FIB() + +-IF_HOST_SUPPORTS_NFT_FIB([ +- NFT_LIST_RULES([inet], [filter_PREROUTING], 0, [dnl +- table inet firewalld { +- chain filter_PREROUTING { +- icmpv6 type { nd-router-advert, nd-neighbor-solicit } accept +- meta nfproto ipv6 fib saddr . mark . iif oif missing drop +- } +- } +- ]) +-], [ +- NFT_LIST_RULES([inet], [filter_PREROUTING], 0, [dnl +- table inet firewalld { +- chain filter_PREROUTING { +- } ++AT_CHECK([sed -i 's/^IPv6_rpfilter.*/IPv6_rpfilter=yes/' ./firewalld.conf]) ++FWD_RELOAD() ++ ++NFT_LIST_RULES([inet], [filter_PREROUTING], 0, [dnl ++ table inet firewalld { ++ chain filter_PREROUTING { ++ icmpv6 type { nd-router-advert, nd-neighbor-solicit } accept ++ meta nfproto ipv6 fib saddr . mark . iif oif missing drop + } +- ]) ++ } + ]) + + IP6TABLES_LIST_RULES([mangle], [PREROUTING], 0, [dnl +-- +2.43.5 + diff --git a/SOURCES/0014-v2.2.0-test-IPv6_rpfilter-verify-valid-values.patch b/SOURCES/0014-v2.2.0-test-IPv6_rpfilter-verify-valid-values.patch new file mode 100644 index 0000000..6ab9f2d --- /dev/null +++ b/SOURCES/0014-v2.2.0-test-IPv6_rpfilter-verify-valid-values.patch @@ -0,0 +1,41 @@ +From 0ba1eed533e4cd1dd77771ba7c16dc0edcea841e Mon Sep 17 00:00:00 2001 +From: Eric Garver +Date: Mon, 13 May 2024 13:53:55 -0400 +Subject: [PATCH 14/22] v2.2.0: test(IPv6_rpfilter): verify valid values + +Including the deprecated "yes" value. + +(cherry picked from commit 1e91792157d36355669b4f02a82c1ee603a9467d) +--- + src/tests/features/rpfilter.at | 18 +++++++++++++++++- + 1 file changed, 17 insertions(+), 1 deletion(-) + +diff --git a/src/tests/features/rpfilter.at b/src/tests/features/rpfilter.at +index ccc8a6cf5e80..755d9dfd33cc 100644 +--- a/src/tests/features/rpfilter.at ++++ b/src/tests/features/rpfilter.at +@@ -22,4 +22,20 @@ IP6TABLES_LIST_RULES([mangle], [PREROUTING], 0, [dnl + PREROUTING_ZONES 0 -- ::/0 ::/0 + ]) + +-FWD_END_TEST ++FWD_END_TEST() ++ ++FWD_START_TEST([rpfilter - config values]) ++AT_KEYWORDS(rpfilter) ++CHECK_NFTABLES_FIB() ++ ++dnl Verify other/deprecated configuration values are accepted. ++dnl ++m4_foreach([VALUE], [[no], [yes], [false], [true]], [ ++ AT_CHECK([sed -i 's/^IPv6_rpfilter.*/IPv6_rpfilter=VALUE/' ./firewalld.conf]) ++ FWD_RELOAD() ++]) ++dnl And a bogus one. ++AT_CHECK([sed -i 's/^IPv6_rpfilter.*/IPv6_rpfilter=bogus/' ./firewalld.conf]) ++FWD_RELOAD() ++ ++FWD_END_TEST([-e "/^WARNING: IPv6_rpfilter 'bogus' is not valid/d"]) +-- +2.43.5 + diff --git a/SOURCES/0015-v2.2.0-chore-IPv6_rpfilter-prepare-for-new-config-va.patch b/SOURCES/0015-v2.2.0-chore-IPv6_rpfilter-prepare-for-new-config-va.patch new file mode 100644 index 0000000..6cd768a --- /dev/null +++ b/SOURCES/0015-v2.2.0-chore-IPv6_rpfilter-prepare-for-new-config-va.patch @@ -0,0 +1,336 @@ +From 7973ddf8d9f972f0292c8c865da9e0ebaefd77cb Mon Sep 17 00:00:00 2001 +From: Eric Garver +Date: Thu, 16 May 2024 09:26:43 -0400 +Subject: [PATCH 15/22] v2.2.0: chore(IPv6_rpfilter): prepare for new config + values + +This is just prep work for supporting other configuration values. This +commits supports using "strict" as a value which is synonymous with +"yes" and is the current default behavior. + +(cherry picked from commit cd959f21a5ceb41057b76f817b0456c281408ae0) +--- + config/firewalld.conf | 15 ++++++++++----- + doc/xml/firewalld.conf.xml | 12 +++++++++--- + doc/xml/firewalld.dbus.xml | 8 ++++++-- + src/firewall/config/__init__.py.in | 3 ++- + src/firewall/core/fw.py | 19 ++++++++----------- + src/firewall/core/io/firewalld_conf.py | 14 ++++++++------ + src/firewall/server/config.py | 22 ++++++++++++++++++---- + src/firewall/server/firewalld.py | 2 +- + src/tests/dbus/firewalld.conf.at | 4 ++++ + src/tests/features/rpfilter.at | 2 +- + 10 files changed, 67 insertions(+), 34 deletions(-) + +diff --git a/config/firewalld.conf b/config/firewalld.conf +index 7a0be1ff1b76..48e2a5a6527a 100644 +--- a/config/firewalld.conf ++++ b/config/firewalld.conf +@@ -26,14 +26,19 @@ CleanupModulesOnExit=no + Lockdown=no + + # IPv6_rpfilter +-# Performs a reverse path filter test on a packet for IPv6. If a reply to the +-# packet would be sent via the same interface that the packet arrived on, the +-# packet will match and be accepted, otherwise dropped. ++# Performs reverse path filtering (RPF) on IPv6 packets as per RFC 3704. ++# Possible values: ++# - strict: Performs "strict" filtering as per RFC 3704. This check ++# verifies that the in ingress interface is the same interface ++# that would be used to send a packet reply to the source. That ++# is, ingress == egress. ++# - no: RPF is completely disabled. ++# + # The rp_filter for IPv4 is controlled using sysctl. + # Note: This feature has a performance impact. See man page FIREWALLD.CONF(5) + # for details. +-# Default: yes +-IPv6_rpfilter=yes ++# Default: strict ++IPv6_rpfilter=strict + + # IndividualCalls + # Do not use combined -restore calls, but individual calls. This increases the +diff --git a/doc/xml/firewalld.conf.xml b/doc/xml/firewalld.conf.xml +index 022569ccf502..be6972aa0d8a 100644 +--- a/doc/xml/firewalld.conf.xml ++++ b/doc/xml/firewalld.conf.xml +@@ -121,9 +121,15 @@ + + + +- If this option is enabled (it is by default), reverse path filter test on a packet for IPv6 is performed. +- If a reply to the packet would be sent via the same interface that the packet arrived on, the packet will match and be accepted, otherwise dropped. +- For IPv4 the rp_filter is controlled using sysctl. ++ Performs reverse path filtering (RPF) on IPv6 packets as per RFC 3704. ++ Possible values: ++ - strict: Performs "strict" filtering as per RFC 3704. This check ++ verifies that the in ingress interface is the same interface ++ that would be used to send a packet reply to the source. That ++ is, ingress == egress. ++ - no: RPF is completely disabled. ++ ++ The rp_filter for IPv4 is controlled using sysctl. + + + Note: This feature has a performance +diff --git a/doc/xml/firewalld.dbus.xml b/doc/xml/firewalld.dbus.xml +index a3196ea4af38..f04cf5ae757b 100644 +--- a/doc/xml/firewalld.dbus.xml ++++ b/doc/xml/firewalld.dbus.xml +@@ -2858,8 +2858,12 @@ + + + +- IPv6_rpfilter - s - (rw) +- Indicates whether the reverse path filter test on a packet for IPv6 is enabled. If a reply to the packet would be sent via the same interface that the packet arrived on, the packet will match and be accepted, otherwise dropped. ++ IPv6_rpfilter - b - (rw) ++ Deprecated. See org.fedoraproject.FirewallD1.config.Properties.IPv6_rpfilter2. ++ ++ ++ IPv6_rpfilter2 - s - (rw) ++ Indicates whether the reverse path filter (RFE 3704) test on a packet for IPv6 is enabled. + + + IndividualCalls - s - (ro) +diff --git a/src/firewall/config/__init__.py.in b/src/firewall/config/__init__.py.in +index da1e31e10e58..68e9bddce5a8 100644 +--- a/src/firewall/config/__init__.py.in ++++ b/src/firewall/config/__init__.py.in +@@ -120,6 +120,7 @@ COMMANDS = { + LOG_DENIED_VALUES = [ "all", "unicast", "broadcast", "multicast", "off" ] + AUTOMATIC_HELPERS_VALUES = [ "yes", "no", "system" ] + FIREWALL_BACKEND_VALUES = [ "nftables", "iptables" ] ++IPV6_RPFILTER_VALUES = ["yes", "true", "no", "false", "strict"] + + # fallbacks: will be overloaded by firewalld.conf + FALLBACK_ZONE = "public" +@@ -127,7 +128,7 @@ FALLBACK_MINIMAL_MARK = 100 + FALLBACK_CLEANUP_ON_EXIT = True + FALLBACK_CLEANUP_MODULES_ON_EXIT = False + FALLBACK_LOCKDOWN = False +-FALLBACK_IPV6_RPFILTER = True ++FALLBACK_IPV6_RPFILTER = "strict" + FALLBACK_INDIVIDUAL_CALLS = False + FALLBACK_LOG_DENIED = "off" + FALLBACK_AUTOMATIC_HELPERS = "no" +diff --git a/src/firewall/core/fw.py b/src/firewall/core/fw.py +index ac13be122b66..b2e150077958 100644 +--- a/src/firewall/core/fw.py ++++ b/src/firewall/core/fw.py +@@ -98,7 +98,7 @@ class Firewall(object): + self.ebtables_enabled, self._state, self._panic, + self._default_zone, self._module_refcount, self._marks, + self.cleanup_on_exit, self.cleanup_modules_on_exit, +- self.ipv6_rpfilter_enabled, self.ipset_enabled, ++ self._ipv6_rpfilter, self.ipset_enabled, + self._individual_calls, self._log_denied) + + def __init_vars(self): +@@ -112,7 +112,7 @@ class Firewall(object): + # 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._ipv6_rpfilter = config.FALLBACK_IPV6_RPFILTER + self._individual_calls = config.FALLBACK_INDIVIDUAL_CALLS + self._log_denied = config.FALLBACK_LOG_DENIED + self._firewall_backend = config.FALLBACK_FIREWALL_BACKEND +@@ -329,14 +329,11 @@ class Firewall(object): + if self._firewalld_conf.get("IPv6_rpfilter"): + value = self._firewalld_conf.get("IPv6_rpfilter") + if value is not None: +- if value.lower() in [ "no", "false" ]: +- self.ipv6_rpfilter_enabled = False +- if value.lower() in [ "yes", "true" ]: +- self.ipv6_rpfilter_enabled = True +- if self.ipv6_rpfilter_enabled: +- log.debug1("IPv6 rpfilter is enabled") +- else: +- log.debug1("IPV6 rpfilter is disabled") ++ if value.lower() in ["no", "false"]: ++ self._ipv6_rpfilter = "no" ++ elif value.lower() in ["yes", "true", "strict"]: ++ self._ipv6_rpfilter = "strict" ++ log.debug1(f"IPv6_rpfilter is set to '{self._ipv6_rpfilter}'") + + if self._firewalld_conf.get("IndividualCalls"): + value = self._firewalld_conf.get("IndividualCalls") +@@ -933,7 +930,7 @@ class Firewall(object): + if self.is_ipv_enabled("ipv6"): + ipv6_backend = self.get_backend_by_ipv("ipv6") + if "raw" in ipv6_backend.get_available_tables(): +- if self.ipv6_rpfilter_enabled: ++ if self._ipv6_rpfilter != "no": + rules = ipv6_backend.build_rpfilter_rules(self._log_denied) + transaction.add_rules(ipv6_backend, rules) + +diff --git a/src/firewall/core/io/firewalld_conf.py b/src/firewall/core/io/firewalld_conf.py +index d2879b319d1f..9ad64883b656 100644 +--- a/src/firewall/core/io/firewalld_conf.py ++++ b/src/firewall/core/io/firewalld_conf.py +@@ -71,7 +71,7 @@ class firewalld_conf(object): + 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("IPv6_rpfilter", config.FALLBACK_IPV6_RPFILTER) + self.set("IndividualCalls", "yes" if config.FALLBACK_INDIVIDUAL_CALLS else "no") + self.set("LogDenied", config.FALLBACK_LOG_DENIED) + self.set("AutomaticHelpers", config.FALLBACK_AUTOMATIC_HELPERS) +@@ -160,12 +160,14 @@ class firewalld_conf(object): + + # check ipv6_rpfilter + value = self.get("IPv6_rpfilter") +- if not value or value.lower() not in [ "yes", "true", "no", "false" ]: ++ if not value or value.lower() not in config.IPV6_RPFILTER_VALUES: + if value is not None: +- log.warning("IPv6_rpfilter '%s' is not valid, using default " +- "value %s", value if value else '', +- config.FALLBACK_IPV6_RPFILTER) +- self.set("IPv6_rpfilter","yes" if config.FALLBACK_IPV6_RPFILTER else "no") ++ log.warning( ++ "IPv6_rpfilter '%s' is not valid, using default " "value %s", ++ value if value else "", ++ config.FALLBACK_IPV6_RPFILTER, ++ ) ++ self.set("IPv6_rpfilter", config.FALLBACK_IPV6_RPFILTER) + + # check individual calls + value = self.get("IndividualCalls") +diff --git a/src/firewall/server/config.py b/src/firewall/server/config.py +index dfbbb889b520..b805e497bb05 100644 +--- a/src/firewall/server/config.py ++++ b/src/firewall/server/config.py +@@ -100,6 +100,7 @@ class FirewallDConfig(DbusServiceObject): + "CleanupOnExit": "readwrite", + "CleanupModulesOnExit": "readwrite", + "IPv6_rpfilter": "readwrite", ++ "IPv6_rpfilter2": "readwrite", + "Lockdown": "readwrite", + "MinimalMark": "readwrite", + "IndividualCalls": "readwrite", +@@ -564,7 +565,7 @@ class FirewallDConfig(DbusServiceObject): + "CleanupModulesOnExit", "Lockdown", "IPv6_rpfilter", + "IndividualCalls", "LogDenied", "AutomaticHelpers", + "FirewallBackend", "FlushAllOnReload", "RFC3964_IPv4", +- "AllowZoneDrifting" ]: ++ "AllowZoneDrifting", "IPv6_rpfilter2" ]: + raise dbus.exceptions.DBusException( + "org.freedesktop.DBus.Error.InvalidArgs: " + "Property '%s' does not exist" % prop) +@@ -594,8 +595,13 @@ class FirewallDConfig(DbusServiceObject): + value = "yes" if config.FALLBACK_LOCKDOWN else "no" + return dbus.String(value) + elif prop == "IPv6_rpfilter": ++ if value is None or value != "no": ++ return dbus.String("yes") ++ else: ++ return dbus.String("no") ++ elif prop == "IPv6_rpfilter2": + if value is None: +- value = "yes" if config.FALLBACK_IPV6_RPFILTER else "no" ++ value = config.FALLBACK_IPV6_RPFILTER + return dbus.String(value) + elif prop == "IndividualCalls": + if value is None: +@@ -640,6 +646,8 @@ class FirewallDConfig(DbusServiceObject): + return dbus.String(self._get_property(prop)) + elif prop == "IPv6_rpfilter": + return dbus.String(self._get_property(prop)) ++ elif prop == "IPv6_rpfilter2": ++ return dbus.String(self._get_property(prop)) + elif prop == "IndividualCalls": + return dbus.String(self._get_property(prop)) + elif prop == "LogDenied": +@@ -693,7 +701,7 @@ class FirewallDConfig(DbusServiceObject): + "CleanupModulesOnExit", "Lockdown", "IPv6_rpfilter", + "IndividualCalls", "LogDenied", "AutomaticHelpers", + "FirewallBackend", "FlushAllOnReload", "RFC3964_IPv4", +- "AllowZoneDrifting" ]: ++ "AllowZoneDrifting", "IPv6_rpfilter2" ]: + ret[x] = self._get_property(x) + elif interface_name in [ config.dbus.DBUS_INTERFACE_CONFIG_DIRECT, + config.dbus.DBUS_INTERFACE_CONFIG_POLICIES ]: +@@ -722,7 +730,7 @@ class FirewallDConfig(DbusServiceObject): + "IPv6_rpfilter", "IndividualCalls", + "LogDenied", + "FirewallBackend", "FlushAllOnReload", +- "RFC3964_IPv4"]: ++ "RFC3964_IPv4", "IPv6_rpfilter2"]: + if property_name in [ "CleanupOnExit", "CleanupModulesOnExit", + "Lockdown", "IPv6_rpfilter", + "IndividualCalls", "FlushAllOnReload", +@@ -742,6 +750,12 @@ class FirewallDConfig(DbusServiceObject): + raise FirewallError(errors.INVALID_VALUE, + "'%s' for %s" % \ + (new_value, property_name)) ++ elif property_name == "IPv6_rpfilter2": ++ if new_value not in config.IPV6_RPFILTER_VALUES: ++ raise FirewallError( ++ errors.INVALID_VALUE, ++ "'%s' for %s" % (new_value, property_name), ++ ) + else: + raise dbus.exceptions.DBusException( + "org.freedesktop.DBus.Error.InvalidArgs: " +diff --git a/src/firewall/server/firewalld.py b/src/firewall/server/firewalld.py +index e43ddb959f41..8b9593a22fd8 100644 +--- a/src/firewall/server/firewalld.py ++++ b/src/firewall/server/firewalld.py +@@ -165,7 +165,7 @@ class FirewallD(DbusServiceObject): + return dbus.Boolean(self.fw.is_ipv_enabled("ipv6")) + + elif prop == "IPv6_rpfilter": +- return dbus.Boolean(self.fw.ipv6_rpfilter_enabled) ++ return dbus.Boolean(False if self.fw._ipv6_rpfilter == "no" else True) + + elif prop == "IPv6ICMPTypes": + return dbus.Array(self.fw.ipv6_supported_icmp_types, "s") +diff --git a/src/tests/dbus/firewalld.conf.at b/src/tests/dbus/firewalld.conf.at +index 10b16cd9e06f..61c220f3e59c 100644 +--- a/src/tests/dbus/firewalld.conf.at ++++ b/src/tests/dbus/firewalld.conf.at +@@ -3,8 +3,10 @@ AT_KEYWORDS(dbus) + + IF_HOST_SUPPORTS_NFT_FIB([ + EXPECTED_IPV6_RPFILTER_VALUE=yes ++ EXPECTED_IPV6_RPFILTER2_VALUE=strict + ], [ + EXPECTED_IPV6_RPFILTER_VALUE=no ++ EXPECTED_IPV6_RPFILTER2_VALUE=no + ]) + + IF_HOST_SUPPORTS_NFT_RULE_INDEX([ +@@ -23,6 +25,7 @@ string "DefaultZone" : variant string "public" + string "FirewallBackend" : variant string "nftables" + string "FlushAllOnReload" : variant string "yes" + string "IPv6_rpfilter" : variant string m4_escape(["${EXPECTED_IPV6_RPFILTER_VALUE}"]) ++string "IPv6_rpfilter2" : variant string m4_escape(["${EXPECTED_IPV6_RPFILTER2_VALUE}"]) + string "IndividualCalls" : variant string m4_escape(["${EXPECTED_INDIVIDUAL_CALLS_VALUE}"]) + string "Lockdown" : variant string "no" + string "LogDenied" : variant string "off" +@@ -43,6 +46,7 @@ _helper([AutomaticHelpers], [string:"yes"], [variant string "no"]) + _helper([Lockdown], [string:"yes"], [variant string "yes"]) + _helper([LogDenied], [string:"all"], [variant string "all"]) + _helper([IPv6_rpfilter], [string:"yes"], [variant string "yes"]) ++_helper([IPv6_rpfilter2], [string:"no"], [variant string "no"]) + _helper([IndividualCalls], [string:"yes"], [variant string "yes"]) + _helper([FirewallBackend], [string:"iptables"], [variant string "iptables"]) + _helper([FlushAllOnReload], [string:"no"], [variant string "no"]) +diff --git a/src/tests/features/rpfilter.at b/src/tests/features/rpfilter.at +index 755d9dfd33cc..58a4b4500330 100644 +--- a/src/tests/features/rpfilter.at ++++ b/src/tests/features/rpfilter.at +@@ -2,7 +2,7 @@ FWD_START_TEST([rpfilter - strict]) + AT_KEYWORDS(rpfilter) + CHECK_NFTABLES_FIB() + +-AT_CHECK([sed -i 's/^IPv6_rpfilter.*/IPv6_rpfilter=yes/' ./firewalld.conf]) ++AT_CHECK([sed -i 's/^IPv6_rpfilter.*/IPv6_rpfilter=strict/' ./firewalld.conf]) + FWD_RELOAD() + + NFT_LIST_RULES([inet], [filter_PREROUTING], 0, [dnl +-- +2.43.5 + diff --git a/SOURCES/0016-v2.2.0-feat-IPv6_rpfilter-support-loose-rpfilter.patch b/SOURCES/0016-v2.2.0-feat-IPv6_rpfilter-support-loose-rpfilter.patch new file mode 100644 index 0000000..93e565d --- /dev/null +++ b/SOURCES/0016-v2.2.0-feat-IPv6_rpfilter-support-loose-rpfilter.patch @@ -0,0 +1,166 @@ +From 41828e0723b1ad195a33c535918a2fb6fabaf88f Mon Sep 17 00:00:00 2001 +From: Eric Garver +Date: Mon, 6 May 2024 10:22:09 -0400 +Subject: [PATCH 16/22] v2.2.0: feat(IPv6_rpfilter): support loose rpfilter + +Support "loose" mode as per RFC 3704. This guarantees only that there is +a patch back to the source, but does NOT guarantee that ingress == +egress. + +(cherry picked from commit 669524a10658761f614e1f199970844db7259960) +--- + config/firewalld.conf | 4 ++++ + doc/xml/firewalld.conf.xml | 4 ++++ + src/firewall/config/__init__.py.in | 2 +- + src/firewall/core/fw.py | 2 ++ + src/firewall/core/ipXtables.py | 16 ++++++++++------ + src/firewall/core/nftables.py | 8 +++++++- + src/tests/features/rpfilter.at | 26 ++++++++++++++++++++++++++ + 7 files changed, 54 insertions(+), 8 deletions(-) + +diff --git a/config/firewalld.conf b/config/firewalld.conf +index 48e2a5a6527a..c35caf9f152b 100644 +--- a/config/firewalld.conf ++++ b/config/firewalld.conf +@@ -32,6 +32,10 @@ Lockdown=no + # verifies that the in ingress interface is the same interface + # that would be used to send a packet reply to the source. That + # is, ingress == egress. ++# - loose: Performs "loose" filtering as per RFC 3704. This check only ++# verifies that there is a route back to the source through any ++# interface; even if it's not the same one on which the packet ++# arrived. + # - no: RPF is completely disabled. + # + # The rp_filter for IPv4 is controlled using sysctl. +diff --git a/doc/xml/firewalld.conf.xml b/doc/xml/firewalld.conf.xml +index be6972aa0d8a..279a149ab2a1 100644 +--- a/doc/xml/firewalld.conf.xml ++++ b/doc/xml/firewalld.conf.xml +@@ -127,6 +127,10 @@ + verifies that the in ingress interface is the same interface + that would be used to send a packet reply to the source. That + is, ingress == egress. ++ - loose: Performs "loose" filtering as per RFC 3704. This check only ++ verifies that there is a route back to the source through any ++ interface; even if it's not the same one on which the packet ++ arrived. + - no: RPF is completely disabled. + + The rp_filter for IPv4 is controlled using sysctl. +diff --git a/src/firewall/config/__init__.py.in b/src/firewall/config/__init__.py.in +index 68e9bddce5a8..6bfe96be35a6 100644 +--- a/src/firewall/config/__init__.py.in ++++ b/src/firewall/config/__init__.py.in +@@ -120,7 +120,7 @@ COMMANDS = { + LOG_DENIED_VALUES = [ "all", "unicast", "broadcast", "multicast", "off" ] + AUTOMATIC_HELPERS_VALUES = [ "yes", "no", "system" ] + FIREWALL_BACKEND_VALUES = [ "nftables", "iptables" ] +-IPV6_RPFILTER_VALUES = ["yes", "true", "no", "false", "strict"] ++IPV6_RPFILTER_VALUES = ["yes", "true", "no", "false", "strict", "loose"] + + # fallbacks: will be overloaded by firewalld.conf + FALLBACK_ZONE = "public" +diff --git a/src/firewall/core/fw.py b/src/firewall/core/fw.py +index b2e150077958..d9724e9c8534 100644 +--- a/src/firewall/core/fw.py ++++ b/src/firewall/core/fw.py +@@ -333,6 +333,8 @@ class Firewall(object): + self._ipv6_rpfilter = "no" + elif value.lower() in ["yes", "true", "strict"]: + self._ipv6_rpfilter = "strict" ++ elif value.lower() in ["loose"]: ++ self._ipv6_rpfilter = "loose" + log.debug1(f"IPv6_rpfilter is set to '{self._ipv6_rpfilter}'") + + if self._firewalld_conf.get("IndividualCalls"): +diff --git a/src/firewall/core/ipXtables.py b/src/firewall/core/ipXtables.py +index 1a0cea7a3b4e..339840c37ba4 100644 +--- a/src/firewall/core/ipXtables.py ++++ b/src/firewall/core/ipXtables.py +@@ -1456,13 +1456,17 @@ class ip6tables(ip4tables): + + def build_rpfilter_rules(self, log_denied=False): + rules = [] +- rules.append([ "-I", "PREROUTING", "-t", "mangle", +- "-m", "rpfilter", "--invert", "--validmark", +- "-j", "DROP" ]) ++ rpfilter_fragment = ["-m", "rpfilter", "--invert", "--validmark"] ++ if self._fw._ipv6_rpfilter == "loose": ++ rpfilter_fragment += ["--loose"] ++ ++ rules.append([ "-I", "PREROUTING", "-t", "mangle" ] ++ + rpfilter_fragment + ++ [ "-j", "DROP" ]) + if log_denied != "off": +- rules.append([ "-I", "PREROUTING", "-t", "mangle", +- "-m", "rpfilter", "--invert", "--validmark", +- "-j", "LOG", ++ rules.append([ "-I", "PREROUTING", "-t", "mangle" ] ++ + rpfilter_fragment + ++ [ "-j", "LOG", + "--log-prefix", "rpfilter_DROP: " ]) + rules.append([ "-I", "PREROUTING", "-t", "mangle", + "-p", "ipv6-icmp", +diff --git a/src/firewall/core/nftables.py b/src/firewall/core/nftables.py +index e9816147ef8e..9827e84042ef 100644 +--- a/src/firewall/core/nftables.py ++++ b/src/firewall/core/nftables.py +@@ -1614,10 +1614,16 @@ class nftables(object): + + def build_rpfilter_rules(self, log_denied=False): + rules = [] ++ ++ if self._fw._ipv6_rpfilter == "loose": ++ fib_flags = ["saddr", "mark"] ++ else: ++ fib_flags = ["saddr", "mark", "iif"] ++ + expr_fragments = [{"match": {"left": {"meta": {"key": "nfproto"}}, + "op": "==", + "right": "ipv6"}}, +- {"match": {"left": {"fib": {"flags": ["saddr", "iif", "mark"], ++ {"match": {"left": {"fib": {"flags": fib_flags, + "result": "oif"}}, + "op": "==", + "right": False}}] +diff --git a/src/tests/features/rpfilter.at b/src/tests/features/rpfilter.at +index 58a4b4500330..23cd9e0e8d7f 100644 +--- a/src/tests/features/rpfilter.at ++++ b/src/tests/features/rpfilter.at +@@ -24,6 +24,32 @@ IP6TABLES_LIST_RULES([mangle], [PREROUTING], 0, [dnl + + FWD_END_TEST() + ++FWD_START_TEST([rpfilter - loose]) ++AT_KEYWORDS(rpfilter) ++CHECK_NFTABLES_FIB() ++ ++AT_CHECK([sed -i 's/^IPv6_rpfilter.*/IPv6_rpfilter=loose/' ./firewalld.conf]) ++FWD_RELOAD() ++ ++NFT_LIST_RULES([inet], [filter_PREROUTING], 0, [dnl ++ table inet firewalld { ++ chain filter_PREROUTING { ++ icmpv6 type { nd-router-advert, nd-neighbor-solicit } accept ++ meta nfproto ipv6 fib saddr . mark oif missing drop ++ } ++ } ++]) ++ ++IP6TABLES_LIST_RULES([mangle], [PREROUTING], 0, [dnl ++ ACCEPT 58 -- ::/0 ::/0 ipv6-icmptype 134 ++ ACCEPT 58 -- ::/0 ::/0 ipv6-icmptype 135 ++ DROP 0 -- ::/0 ::/0 rpfilter loose validmark invert ++ PREROUTING_direct 0 -- ::/0 ::/0 ++ PREROUTING_ZONES 0 -- ::/0 ::/0 ++]) ++ ++FWD_END_TEST() ++ + FWD_START_TEST([rpfilter - config values]) + AT_KEYWORDS(rpfilter) + CHECK_NFTABLES_FIB() +-- +2.43.5 + diff --git a/SOURCES/0017-v2.2.0-feat-IPv6_rpfilter-support-loose-forward-rpfi.patch b/SOURCES/0017-v2.2.0-feat-IPv6_rpfilter-support-loose-forward-rpfi.patch new file mode 100644 index 0000000..e909c35 --- /dev/null +++ b/SOURCES/0017-v2.2.0-feat-IPv6_rpfilter-support-loose-forward-rpfi.patch @@ -0,0 +1,229 @@ +From a965defecfad03530f3374271fb4889ff895ab1c Mon Sep 17 00:00:00 2001 +From: Eric Garver +Date: Tue, 14 May 2024 12:16:44 -0400 +Subject: [PATCH 17/22] v2.2.0: feat(IPv6_rpfilter): support loose-forward + rpfilter + +This adds a new config value for IPv6_rpfilter, loose-forward. In this +mode the rpfilter occurs only for forwarded packets using the "loose" +algorithm from RFC 3704. This is a significant performance improvement +for end-stations that have a single default route because the RPF check +is completely absent on INPUT. + +This new value is NOT compatible with the iptables backend. This is +enforced by configuration checks. + +(cherry picked from commit fb692d2e560253c97c7fdd1e9fdbfcc8c61d0eba) +--- + config/firewalld.conf | 2 ++ + doc/xml/firewalld.conf.xml | 2 ++ + src/firewall/config/__init__.py.in | 3 ++- + src/firewall/core/fw.py | 2 ++ + src/firewall/core/io/firewalld_conf.py | 15 +++++++++++ + src/firewall/core/nftables.py | 30 ++++++++++++++------- + src/tests/features/rpfilter.at | 36 ++++++++++++++++++++++++++ + 7 files changed, 79 insertions(+), 11 deletions(-) + +diff --git a/config/firewalld.conf b/config/firewalld.conf +index c35caf9f152b..5d0777d54b15 100644 +--- a/config/firewalld.conf ++++ b/config/firewalld.conf +@@ -36,6 +36,8 @@ Lockdown=no + # verifies that there is a route back to the source through any + # interface; even if it's not the same one on which the packet + # arrived. ++# - loose-forward: This is almost identical to "loose", but does not perform ++# RPF for packets targeted to the host (INPUT). + # - no: RPF is completely disabled. + # + # The rp_filter for IPv4 is controlled using sysctl. +diff --git a/doc/xml/firewalld.conf.xml b/doc/xml/firewalld.conf.xml +index 279a149ab2a1..1303758347e2 100644 +--- a/doc/xml/firewalld.conf.xml ++++ b/doc/xml/firewalld.conf.xml +@@ -131,6 +131,8 @@ + verifies that there is a route back to the source through any + interface; even if it's not the same one on which the packet + arrived. ++ - loose-forward: This is almost identical to "loose", but does not perform ++ RPF for packets targeted to the host (INPUT). + - no: RPF is completely disabled. + + The rp_filter for IPv4 is controlled using sysctl. +diff --git a/src/firewall/config/__init__.py.in b/src/firewall/config/__init__.py.in +index 6bfe96be35a6..f2c4c9f5afa1 100644 +--- a/src/firewall/config/__init__.py.in ++++ b/src/firewall/config/__init__.py.in +@@ -120,7 +120,8 @@ COMMANDS = { + LOG_DENIED_VALUES = [ "all", "unicast", "broadcast", "multicast", "off" ] + AUTOMATIC_HELPERS_VALUES = [ "yes", "no", "system" ] + FIREWALL_BACKEND_VALUES = [ "nftables", "iptables" ] +-IPV6_RPFILTER_VALUES = ["yes", "true", "no", "false", "strict", "loose"] ++IPV6_RPFILTER_VALUES = ["yes", "true", "no", "false", "strict", "loose", ++ "loose-forward"] + + # fallbacks: will be overloaded by firewalld.conf + FALLBACK_ZONE = "public" +diff --git a/src/firewall/core/fw.py b/src/firewall/core/fw.py +index d9724e9c8534..8c20a4a606e2 100644 +--- a/src/firewall/core/fw.py ++++ b/src/firewall/core/fw.py +@@ -335,6 +335,8 @@ class Firewall(object): + self._ipv6_rpfilter = "strict" + elif value.lower() in ["loose"]: + self._ipv6_rpfilter = "loose" ++ elif value.lower() in ["loose-forward"]: ++ self._ipv6_rpfilter = "loose-forward" + log.debug1(f"IPv6_rpfilter is set to '{self._ipv6_rpfilter}'") + + if self._firewalld_conf.get("IndividualCalls"): +diff --git a/src/firewall/core/io/firewalld_conf.py b/src/firewall/core/io/firewalld_conf.py +index 9ad64883b656..159715df7ede 100644 +--- a/src/firewall/core/io/firewalld_conf.py ++++ b/src/firewall/core/io/firewalld_conf.py +@@ -26,6 +26,7 @@ import shutil + + from firewall import config + from firewall.core.logger import log ++from firewall import errors + + valid_keys = [ "DefaultZone", "MinimalMark", "CleanupOnExit", + "CleanupModulesOnExit", "Lockdown", "IPv6_rpfilter", +@@ -81,6 +82,18 @@ class firewalld_conf(object): + self.set("RFC3964_IPv4", "yes" if config.FALLBACK_RFC3964_IPV4 else "no") + self.set("AllowZoneDrifting", "yes" if config.FALLBACK_ALLOW_ZONE_DRIFTING else "no") + ++ def sanity_check(self): ++ if ( ++ self.get("FirewallBackend") == "iptables" ++ and self.get("IPv6_rpfilter") == "loose-forward" ++ ): ++ raise errors.FirewallError( ++ errors.INVALID_VALUE, ++ "IPv6_rpfilter=loose-forward is incompatible " ++ "with FirewallBackend=iptables. This is a limitation " ++ "of the iptables backend.", ++ ) ++ + # load self.filename + def read(self): + self.clear() +@@ -238,6 +251,8 @@ class firewalld_conf(object): + config.FALLBACK_ALLOW_ZONE_DRIFTING) + self.set("AllowZoneDrifting", "yes" if config.FALLBACK_ALLOW_ZONE_DRIFTING else "no") + ++ self.sanity_check() ++ + # save to self.filename if there are key/value changes + def write(self): + if len(self._config) < 1: +diff --git a/src/firewall/core/nftables.py b/src/firewall/core/nftables.py +index 9827e84042ef..da2d8eb8ec29 100644 +--- a/src/firewall/core/nftables.py ++++ b/src/firewall/core/nftables.py +@@ -1614,9 +1614,13 @@ class nftables(object): + + def build_rpfilter_rules(self, log_denied=False): + rules = [] ++ rpfilter_chain = "filter_PREROUTING" + + if self._fw._ipv6_rpfilter == "loose": + fib_flags = ["saddr", "mark"] ++ elif self._fw._ipv6_rpfilter == "loose-forward": ++ fib_flags = ["saddr", "mark"] ++ rpfilter_chain = "filter_FORWARD" + else: + fib_flags = ["saddr", "mark", "iif"] + +@@ -1633,17 +1637,19 @@ class nftables(object): + + rules.append({"insert": {"rule": {"family": "inet", + "table": TABLE_NAME, +- "chain": "filter_PREROUTING", ++ "chain": rpfilter_chain, + "expr": expr_fragments}}}) + # RHBZ#1058505, RHBZ#1575431 (bug in kernel 4.16-4.17) +- rules.append({"insert": {"rule": {"family": "inet", +- "table": TABLE_NAME, +- "chain": "filter_PREROUTING", +- "expr": [{"match": {"left": {"payload": {"protocol": "icmpv6", +- "field": "type"}}, +- "op": "==", +- "right": {"set": ["nd-router-advert", "nd-neighbor-solicit"]}}}, +- {"accept": None}]}}}) ++ if self._fw._ipv6_rpfilter != "loose-forward": ++ # this rule doesn't make sense for forwarded packets ++ rules.append({"insert": {"rule": {"family": "inet", ++ "table": TABLE_NAME, ++ "chain": rpfilter_chain, ++ "expr": [{"match": {"left": {"payload": {"protocol": "icmpv6", ++ "field": "type"}}, ++ "op": "==", ++ "right": {"set": ["nd-router-advert", "nd-neighbor-solicit"]}}}, ++ {"accept": None}]}}}) + return rules + + def build_rfc3964_ipv4_rules(self): +@@ -1674,7 +1680,11 @@ class nftables(object): + "chain": "filter_OUTPUT", + "index": 1, + "expr": expr_fragments}}}) +- forward_index = 4 if self._fw.get_log_denied() != "off" else 3 ++ forward_index = 3 ++ if self._fw.get_log_denied() != "off": ++ forward_index += 1 ++ if self._fw._ipv6_rpfilter == "loose-forward": ++ forward_index += 1 + rules.append({"add": {"rule": {"family": "inet", + "table": TABLE_NAME, + "chain": "filter_FORWARD", +diff --git a/src/tests/features/rpfilter.at b/src/tests/features/rpfilter.at +index 23cd9e0e8d7f..5c25ed7e16f0 100644 +--- a/src/tests/features/rpfilter.at ++++ b/src/tests/features/rpfilter.at +@@ -50,6 +50,42 @@ IP6TABLES_LIST_RULES([mangle], [PREROUTING], 0, [dnl + + FWD_END_TEST() + ++FWD_START_TEST([rpfilter - loose-forward]) ++AT_KEYWORDS(rpfilter) ++CHECK_NFTABLES_FIB() ++CHECK_NFTABLES_FIB_IN_FORWARD() ++ ++AT_CHECK([sed -i 's/^IPv6_rpfilter.*/IPv6_rpfilter=loose-forward/' ./firewalld.conf]) ++m4_if(iptables, FIREWALL_BACKEND, [ ++FWD_RELOAD(114, [ignore], [ignore]) ++], [ ++FWD_RELOAD() ++]) ++ ++NFT_LIST_RULES([inet], [filter_FORWARD], 0, [dnl ++ table inet firewalld { ++ chain filter_FORWARD { ++ meta nfproto ipv6 fib saddr . mark oif missing drop ++ ct state established,related accept ++ ct status dnat accept ++ iifname "lo" accept ++ ct state invalid drop ++ ip6 daddr { ::/96, ::ffff:0.0.0.0/96, 2002::/24, 2002:a00::/24, 2002:7f00::/24, 2002:a9fe::/32, 2002:ac10::/28, 2002:c0a8::/32, 2002:e000::/19 } reject with icmpv6 addr-unreachable ++ jump filter_FORWARD_ZONES ++ reject with icmpx admin-prohibited ++ } ++ } ++]) ++ ++NFT_LIST_RULES([inet], [filter_PREROUTING], 0, [dnl ++ table inet firewalld { ++ chain filter_PREROUTING { ++ } ++ } ++]) ++ ++FWD_END_TEST([-e "/^ERROR: INVALID_VALUE:/d"]) ++ + FWD_START_TEST([rpfilter - config values]) + AT_KEYWORDS(rpfilter) + CHECK_NFTABLES_FIB() +-- +2.43.5 + diff --git a/SOURCES/0018-v2.2.0-feat-IPv6_rpfilter-support-strict-forward-rpf.patch b/SOURCES/0018-v2.2.0-feat-IPv6_rpfilter-support-strict-forward-rpf.patch new file mode 100644 index 0000000..d6f2640 --- /dev/null +++ b/SOURCES/0018-v2.2.0-feat-IPv6_rpfilter-support-strict-forward-rpf.patch @@ -0,0 +1,183 @@ +From 7629e1c24b73d8c1e56d275c07076bc3bf99caad Mon Sep 17 00:00:00 2001 +From: Eric Garver +Date: Fri, 17 May 2024 10:05:04 -0400 +Subject: [PATCH 18/22] v2.2.0: feat(IPv6_rpfilter): support strict-forward + rpfilter + +(cherry picked from commit 98e5cbb862885e00981611a79f7a3186da5d1050) +--- + config/firewalld.conf | 2 ++ + doc/xml/firewalld.conf.xml | 5 +++- + src/firewall/config/__init__.py.in | 2 +- + src/firewall/core/fw.py | 2 ++ + src/firewall/core/io/firewalld_conf.py | 8 +++--- + src/firewall/core/nftables.py | 7 +++-- + src/tests/features/rpfilter.at | 36 ++++++++++++++++++++++++++ + 7 files changed, 54 insertions(+), 8 deletions(-) + +diff --git a/config/firewalld.conf b/config/firewalld.conf +index 5d0777d54b15..cf95af3eea8e 100644 +--- a/config/firewalld.conf ++++ b/config/firewalld.conf +@@ -36,6 +36,8 @@ Lockdown=no + # verifies that there is a route back to the source through any + # interface; even if it's not the same one on which the packet + # arrived. ++# - strict-forward: This is almost identical to "strict", but does not perform ++# RPF for packets targeted to the host (INPUT). + # - loose-forward: This is almost identical to "loose", but does not perform + # RPF for packets targeted to the host (INPUT). + # - no: RPF is completely disabled. +diff --git a/doc/xml/firewalld.conf.xml b/doc/xml/firewalld.conf.xml +index 1303758347e2..4b9bfdad15be 100644 +--- a/doc/xml/firewalld.conf.xml ++++ b/doc/xml/firewalld.conf.xml +@@ -131,6 +131,8 @@ + verifies that there is a route back to the source through any + interface; even if it's not the same one on which the packet + arrived. ++ - strict-forward: This is almost identical to "loose", but does not perform ++ RPF for packets targeted to the host (INPUT). + - loose-forward: This is almost identical to "loose", but does not perform + RPF for packets targeted to the host (INPUT). + - no: RPF is completely disabled. +@@ -144,7 +146,8 @@ + the established connections fast path. As such it can have a + significant performance impact if there is a lot of traffic. It's + enabled by default for security, but can be disabled if performance is +- a concern. ++ a concern. Alternatively one of the variants that only does RPF on ++ forwarded packets may be used. + + + +diff --git a/src/firewall/config/__init__.py.in b/src/firewall/config/__init__.py.in +index f2c4c9f5afa1..2eda337e8180 100644 +--- a/src/firewall/config/__init__.py.in ++++ b/src/firewall/config/__init__.py.in +@@ -121,7 +121,7 @@ LOG_DENIED_VALUES = [ "all", "unicast", "broadcast", "multicast", "off" ] + AUTOMATIC_HELPERS_VALUES = [ "yes", "no", "system" ] + FIREWALL_BACKEND_VALUES = [ "nftables", "iptables" ] + IPV6_RPFILTER_VALUES = ["yes", "true", "no", "false", "strict", "loose", +- "loose-forward"] ++ "loose-forward", "strict-forward"] + + # fallbacks: will be overloaded by firewalld.conf + FALLBACK_ZONE = "public" +diff --git a/src/firewall/core/fw.py b/src/firewall/core/fw.py +index 8c20a4a606e2..f91ff53fc37a 100644 +--- a/src/firewall/core/fw.py ++++ b/src/firewall/core/fw.py +@@ -337,6 +337,8 @@ class Firewall(object): + self._ipv6_rpfilter = "loose" + elif value.lower() in ["loose-forward"]: + self._ipv6_rpfilter = "loose-forward" ++ elif value.lower() in ["strict-forward"]: ++ self._ipv6_rpfilter = "strict-forward" + log.debug1(f"IPv6_rpfilter is set to '{self._ipv6_rpfilter}'") + + if self._firewalld_conf.get("IndividualCalls"): +diff --git a/src/firewall/core/io/firewalld_conf.py b/src/firewall/core/io/firewalld_conf.py +index 159715df7ede..20072e26cfcd 100644 +--- a/src/firewall/core/io/firewalld_conf.py ++++ b/src/firewall/core/io/firewalld_conf.py +@@ -83,13 +83,13 @@ class firewalld_conf(object): + self.set("AllowZoneDrifting", "yes" if config.FALLBACK_ALLOW_ZONE_DRIFTING else "no") + + def sanity_check(self): +- if ( +- self.get("FirewallBackend") == "iptables" +- and self.get("IPv6_rpfilter") == "loose-forward" ++ if self.get("FirewallBackend") == "iptables" and self.get("IPv6_rpfilter") in ( ++ "loose-forward", ++ "strict-forward", + ): + raise errors.FirewallError( + errors.INVALID_VALUE, +- "IPv6_rpfilter=loose-forward is incompatible " ++ f"IPv6_rpfilter={self.get('IPv6_rpfilter')} is incompatible " + "with FirewallBackend=iptables. This is a limitation " + "of the iptables backend.", + ) +diff --git a/src/firewall/core/nftables.py b/src/firewall/core/nftables.py +index da2d8eb8ec29..5a49b34e3a4f 100644 +--- a/src/firewall/core/nftables.py ++++ b/src/firewall/core/nftables.py +@@ -1621,6 +1621,9 @@ class nftables(object): + elif self._fw._ipv6_rpfilter == "loose-forward": + fib_flags = ["saddr", "mark"] + rpfilter_chain = "filter_FORWARD" ++ elif self._fw._ipv6_rpfilter == "strict-forward": ++ fib_flags = ["saddr", "mark", "iif"] ++ rpfilter_chain = "filter_FORWARD" + else: + fib_flags = ["saddr", "mark", "iif"] + +@@ -1640,7 +1643,7 @@ class nftables(object): + "chain": rpfilter_chain, + "expr": expr_fragments}}}) + # RHBZ#1058505, RHBZ#1575431 (bug in kernel 4.16-4.17) +- if self._fw._ipv6_rpfilter != "loose-forward": ++ if self._fw._ipv6_rpfilter not in ("loose-forward", "strict-forward"): + # this rule doesn't make sense for forwarded packets + rules.append({"insert": {"rule": {"family": "inet", + "table": TABLE_NAME, +@@ -1683,7 +1686,7 @@ class nftables(object): + forward_index = 3 + if self._fw.get_log_denied() != "off": + forward_index += 1 +- if self._fw._ipv6_rpfilter == "loose-forward": ++ if self._fw._ipv6_rpfilter in ("loose-forward", "strict-forward"): + forward_index += 1 + rules.append({"add": {"rule": {"family": "inet", + "table": TABLE_NAME, +diff --git a/src/tests/features/rpfilter.at b/src/tests/features/rpfilter.at +index 5c25ed7e16f0..9ad50c993ba0 100644 +--- a/src/tests/features/rpfilter.at ++++ b/src/tests/features/rpfilter.at +@@ -50,6 +50,42 @@ IP6TABLES_LIST_RULES([mangle], [PREROUTING], 0, [dnl + + FWD_END_TEST() + ++FWD_START_TEST([rpfilter - strict-forward]) ++AT_KEYWORDS(rpfilter) ++CHECK_NFTABLES_FIB() ++CHECK_NFTABLES_FIB_IN_FORWARD() ++ ++AT_CHECK([sed -i 's/^IPv6_rpfilter.*/IPv6_rpfilter=strict-forward/' ./firewalld.conf]) ++m4_if(iptables, FIREWALL_BACKEND, [ ++FWD_RELOAD(114, [ignore], [ignore]) ++], [ ++FWD_RELOAD() ++]) ++ ++NFT_LIST_RULES([inet], [filter_FORWARD], 0, [dnl ++ table inet firewalld { ++ chain filter_FORWARD { ++ meta nfproto ipv6 fib saddr . mark . iif oif missing drop ++ ct state established,related accept ++ ct status dnat accept ++ iifname "lo" accept ++ ct state invalid drop ++ ip6 daddr { ::/96, ::ffff:0.0.0.0/96, 2002::/24, 2002:a00::/24, 2002:7f00::/24, 2002:a9fe::/32, 2002:ac10::/28, 2002:c0a8::/32, 2002:e000::/19 } reject with icmpv6 addr-unreachable ++ jump filter_FORWARD_ZONES ++ reject with icmpx admin-prohibited ++ } ++ } ++]) ++ ++NFT_LIST_RULES([inet], [filter_PREROUTING], 0, [dnl ++ table inet firewalld { ++ chain filter_PREROUTING { ++ } ++ } ++]) ++ ++FWD_END_TEST([-e "/^ERROR: INVALID_VALUE:/d"]) ++ + FWD_START_TEST([rpfilter - loose-forward]) + AT_KEYWORDS(rpfilter) + CHECK_NFTABLES_FIB() +-- +2.43.5 + diff --git a/SOURCES/0019-v2.2.0-test-functions-start-firewalld-with-file-logg.patch b/SOURCES/0019-v2.2.0-test-functions-start-firewalld-with-file-logg.patch new file mode 100644 index 0000000..750810c --- /dev/null +++ b/SOURCES/0019-v2.2.0-test-functions-start-firewalld-with-file-logg.patch @@ -0,0 +1,31 @@ +From e031ce8e41d2fc23735be91f7070127f8812b490 Mon Sep 17 00:00:00 2001 +From: Eric Garver +Date: Fri, 21 Jun 2024 16:17:44 -0400 +Subject: [PATCH 19/22] v2.2.0: test(functions): start firewalld with file + logging + +This is a test environment. Trying to log to syslog does not make sense. +The default, mixed, sends to both file and syslog, but with different +log level settings. + +(cherry picked from commit 5e0c28d5f7f71a216485169610d8c72a3a3518d1) +--- + src/tests/functions.at | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/tests/functions.at b/src/tests/functions.at +index b2372dd4075b..d1c89ed5b982 100644 +--- a/src/tests/functions.at ++++ b/src/tests/functions.at +@@ -9,7 +9,7 @@ m4_define([FWD_STOP_FIREWALLD], [ + ]) + + m4_define([FWD_START_FIREWALLD], [ +- FIREWALLD_ARGS="--nofork --nopid --log-file ./firewalld.log --system-config ./" ++ FIREWALLD_ARGS="--nofork --nopid --log-file ./firewalld.log --log-target file --system-config ./" + dnl if testsuite ran with debug flag, add debug output + ${at_debug_p} && FIREWALLD_ARGS="--debug=9 ${FIREWALLD_ARGS}" + if test "x${FIREWALLD_DEFAULT_CONFIG}" != x ; then +-- +2.43.5 + diff --git a/SOURCES/0020-v2.2.0-feat-nftables-table-ownership.patch b/SOURCES/0020-v2.2.0-feat-nftables-table-ownership.patch new file mode 100644 index 0000000..0d116fe --- /dev/null +++ b/SOURCES/0020-v2.2.0-feat-nftables-table-ownership.patch @@ -0,0 +1,417 @@ +From ce75ad2920f457d5b2ce578ead5d8c64f0daa490 Mon Sep 17 00:00:00 2001 +From: Eric Garver +Date: Fri, 21 Jun 2024 17:38:29 -0400 +Subject: [PATCH 20/22] v2.2.0: feat(nftables): table ownership + +This allows using the nftables table flags "owner" and "persist". When +these are in use ONLY firewalld will be able to change the firewalld +table/rules. All other processes will be blocked. This enhances +firewalld robustness by guaranteeing that firewalld's rules can not be +modified without it's knowledge, e.g. by other entities. + +This also allows firewalld and nftables services to coexist. An "nft +flush ruleset" will NOT flush the "firewalld" table. + +Fixes: RHEL-17002 +(cherry picked from commit becd083fc2905921651af73cb15ce8c9aba9203b) +--- + config/firewalld.conf | 8 +++ + doc/xml/firewalld.conf.xml | 13 ++++ + doc/xml/firewalld.dbus.xml | 11 ++++ + src/firewall/config/__init__.py.in | 1 + + src/firewall/core/fw.py | 22 +++++++ + src/firewall/core/io/firewalld_conf.py | 6 +- + src/firewall/core/nftables.py | 83 ++++++++++++++++++++++---- + src/firewall/server/config.py | 16 +++-- + src/tests/dbus/firewalld.conf.at | 2 + + src/tests/regression/rhbz2222044.at | 5 ++ + 10 files changed, 150 insertions(+), 17 deletions(-) + +diff --git a/config/firewalld.conf b/config/firewalld.conf +index cf95af3eea8e..28345a13d54e 100644 +--- a/config/firewalld.conf ++++ b/config/firewalld.conf +@@ -93,3 +93,11 @@ ReloadPolicy=INPUT:DROP,FORWARD:DROP,OUTPUT:DROP + # internet. + # Defaults to "yes". + RFC3964_IPv4=yes ++ ++# NftablesTableOwner ++# If set to yes, the generated nftables rule set will be owned exclusively by ++# firewalld. This prevents other entities from mistakenly (or maliciously) ++# modifying firewalld's rule set. If you intentionally modify firewalld's ++# rules, then you will have to set this to "no". ++# Defaults to "yes". ++NftablesTableOwner=yes +diff --git a/doc/xml/firewalld.conf.xml b/doc/xml/firewalld.conf.xml +index 4b9bfdad15be..cf7134c04012 100644 +--- a/doc/xml/firewalld.conf.xml ++++ b/doc/xml/firewalld.conf.xml +@@ -247,6 +247,19 @@ + + + ++ ++ ++ ++ ++ If set to yes, the generated nftables rule set will be owned exclusively by ++ firewalld. This prevents other entities from mistakenly (or maliciously) ++ modifying firewalld's rule set. If you intentionally modify firewalld's ++ rules, then you will have to set this to "no". ++ Defaults to "yes". ++ ++ ++ ++ + + + +diff --git a/doc/xml/firewalld.dbus.xml b/doc/xml/firewalld.dbus.xml +index f04cf5ae757b..00d7e8fbe5b1 100644 +--- a/doc/xml/firewalld.dbus.xml ++++ b/doc/xml/firewalld.dbus.xml +@@ -2905,6 +2905,17 @@ + + + ++ ++ NftablesTableOwner - s - (rw) ++ ++ ++ If set to yes, the generated nftables rule set will be owned exclusively by ++ firewalld. This prevents other entities from mistakenly (or maliciously) ++ modifying firewalld's rule set. If you intentionally modify firewalld's ++ rules, then you will have to set this to "no". ++ ++ ++ + + + +diff --git a/src/firewall/config/__init__.py.in b/src/firewall/config/__init__.py.in +index 2eda337e8180..16291fcf795a 100644 +--- a/src/firewall/config/__init__.py.in ++++ b/src/firewall/config/__init__.py.in +@@ -138,3 +138,4 @@ FALLBACK_FLUSH_ALL_ON_RELOAD = True + FALLBACK_RELOAD_POLICY = "INPUT:DROP,FORWARD:DROP,OUTPUT:DROP" + FALLBACK_RFC3964_IPV4 = True + FALLBACK_ALLOW_ZONE_DRIFTING = False ++FALLBACK_NFTABLES_TABLE_OWNER = True +diff --git a/src/firewall/core/fw.py b/src/firewall/core/fw.py +index f91ff53fc37a..557b6e527dbd 100644 +--- a/src/firewall/core/fw.py ++++ b/src/firewall/core/fw.py +@@ -119,6 +119,7 @@ class Firewall(object): + self._flush_all_on_reload = config.FALLBACK_FLUSH_ALL_ON_RELOAD + self._rfc3964_ipv4 = config.FALLBACK_RFC3964_IPV4 + self._allow_zone_drifting = config.FALLBACK_ALLOW_ZONE_DRIFTING ++ self._nftables_table_owner = config.FALLBACK_NFTABLES_TABLE_OWNER + + if self._offline: + self.ip4tables_enabled = False +@@ -290,6 +291,17 @@ class Firewall(object): + log.debug1("ebtables-restore is not supporting the --noflush " + "option, will therefore not be used") + ++ self.nftables_backend.probe_support() ++ ++ if ( ++ self._nftables_table_owner ++ and not self.nftables_backend.supports_table_owner ++ ): ++ log.info1( ++ "Configuration has NftablesTableOwner=True, but it's " ++ "not supported by nftables. Table ownership will be disabled." ++ ) ++ + def _start_load_firewalld_conf(self): + # load firewalld config + log.debug1("Loading firewalld config file '%s'", config.FIREWALLD_CONF) +@@ -378,6 +390,16 @@ class Firewall(object): + log.debug1("RFC3964_IPv4 is set to '%s'", + self._rfc3964_ipv4) + ++ if self._firewalld_conf.get("NftablesTableOwner"): ++ value = self._firewalld_conf.get("NftablesTableOwner") ++ if value.lower() in ["no", "false"]: ++ self._nftables_table_owner = False ++ else: ++ self._nftables_table_owner = True ++ log.debug1( ++ "NftablesTableOwner is set to '%s'", self._nftables_table_owner ++ ) ++ + self.config.set_firewalld_conf(copy.deepcopy(self._firewalld_conf)) + + def _start_load_lockdown_whitelist(self): +diff --git a/src/firewall/core/io/firewalld_conf.py b/src/firewall/core/io/firewalld_conf.py +index 20072e26cfcd..d5b47666c0f0 100644 +--- a/src/firewall/core/io/firewalld_conf.py ++++ b/src/firewall/core/io/firewalld_conf.py +@@ -32,7 +32,7 @@ valid_keys = [ "DefaultZone", "MinimalMark", "CleanupOnExit", + "CleanupModulesOnExit", "Lockdown", "IPv6_rpfilter", + "IndividualCalls", "LogDenied", "AutomaticHelpers", + "FirewallBackend", "FlushAllOnReload", "RFC3964_IPv4", +- "AllowZoneDrifting", "ReloadPolicy" ] ++ "AllowZoneDrifting", "ReloadPolicy", "NftablesTableOwner" ] + + class firewalld_conf(object): + def __init__(self, filename): +@@ -81,6 +81,10 @@ class firewalld_conf(object): + self.set("ReloadPolicy", config.FALLBACK_RELOAD_POLICY) + self.set("RFC3964_IPv4", "yes" if config.FALLBACK_RFC3964_IPV4 else "no") + self.set("AllowZoneDrifting", "yes" if config.FALLBACK_ALLOW_ZONE_DRIFTING else "no") ++ self.set( ++ "NftablesTableOwner", ++ "yes" if config.FALLBACK_NFTABLES_TABLE_OWNER else "no", ++ ) + + def sanity_check(self): + if self.get("FirewallBackend") == "iptables" and self.get("IPv6_rpfilter") in ( +diff --git a/src/firewall/core/nftables.py b/src/firewall/core/nftables.py +index 5a49b34e3a4f..8115bcb9d7f4 100644 +--- a/src/firewall/core/nftables.py ++++ b/src/firewall/core/nftables.py +@@ -36,6 +36,7 @@ from nftables.nftables import Nftables + + TABLE_NAME = "firewalld" + TABLE_NAME_POLICY = TABLE_NAME + "_" + "policy_drop" ++TABLE_NAME_PROBE = TABLE_NAME + "_" + "probe" + POLICY_CHAIN_PREFIX = "policy_" + + # Map iptables (table, chain) to hooks and priorities. +@@ -169,6 +170,7 @@ class nftables(object): + def __init__(self, fw): + self._fw = fw + self.restore_command_exists = True ++ self.supports_table_owner = False + self.available_tables = [] + self.rule_to_handle = {} + self.rule_ref_count = {} +@@ -180,6 +182,57 @@ class nftables(object): + self.nftables.set_echo_output(True) + self.nftables.set_handle_output(True) + ++ def _probe_support_table_owner(self): ++ try: ++ rules = { ++ "nftables": [ ++ {"metainfo": {"json_schema_version": 1}}, ++ { ++ "add": { ++ "table": { ++ "family": "inet", ++ "name": TABLE_NAME_PROBE, ++ "flags": ["owner", "persist"], ++ } ++ } ++ }, ++ ] ++ } ++ ++ rc, output, _ = self.nftables.json_cmd(rules) ++ if rc: ++ raise ValueError("nftables probe table owner failed") ++ ++ # old nftables versions would ignore table flags in JSON, so we ++ # must parse back and verify the flags are set. ++ rules = { ++ "nftables": [ ++ {"metainfo": {"json_schema_version": 1}}, ++ {"list": {"table": {"family": "inet", "name": TABLE_NAME_PROBE}}}, ++ ] ++ } ++ self.nftables.set_echo_output(False) ++ rc, output, _ = self.nftables.json_cmd(rules) ++ self.nftables.set_echo_output(True) ++ flags = output["nftables"][1]["table"]["flags"] ++ ++ self.set_rule( ++ {"delete": {"table": {"family": "inet", "name": TABLE_NAME_PROBE}}}, ++ self._fw.get_log_denied(), ++ ) ++ ++ if "owner" not in flags or "persist" not in flags: ++ raise ValueError("nftables probe table owner failed") ++ ++ log.debug2("nftables: probe_support(): owner flag is supported.") ++ self.supports_table_owner = True ++ except: ++ log.debug2("nftables: probe_support(): owner flag is NOT supported.") ++ self.supports_table_owner = False ++ ++ def probe_support(self): ++ self._probe_support_table_owner() ++ + def _run_replace_zone_source(self, rule, zone_source_index_cache): + for verb in ["add", "insert", "delete"]: + if verb in rule: +@@ -401,16 +454,27 @@ class nftables(object): + # Tables always exist in nftables + return [table] if table else IPTABLES_TO_NFT_HOOK.keys() + ++ def _build_add_table_rules(self, table): ++ rule = {"add": {"table": {"family": "inet", "name": table}}} ++ ++ if ( ++ table == TABLE_NAME ++ and self._fw._nftables_table_owner ++ and self.supports_table_owner ++ ): ++ rule["add"]["table"]["flags"] = ["owner", "persist"] ++ ++ return [rule] ++ + def _build_delete_table_rules(self, table): + # To avoid nftables returning ENOENT we always add the table before + # deleting to guarantee it will exist. + # + # In the future, this add+delete should be replaced with "destroy", but + # that verb is too new to rely upon. +- return [{"add": {"table": {"family": "inet", +- "name": table}}}, +- {"delete": {"table": {"family": "inet", +- "name": table}}}] ++ return self._build_add_table_rules(table) + [ ++ {"delete": {"table": {"family": "inet", "name": table}}}, ++ ] + + def build_flush_rules(self): + self.rule_to_handle = {} +@@ -437,8 +501,7 @@ class nftables(object): + # a higher priority than our base chains is sufficient. + rules = [] + if policy == "PANIC": +- rules.append({"add": {"table": {"family": "inet", +- "name": TABLE_NAME_POLICY}}}) ++ rules.extend(self._build_add_table_rules(TABLE_NAME_POLICY)) + + # Use "raw" priority for panic mode. This occurs before + # conntrack, mangle, nat, etc +@@ -451,8 +514,7 @@ class nftables(object): + "prio": -300 + NFT_HOOK_OFFSET - 1, + "policy": "drop"}}}) + elif policy == "DROP": +- rules.append({"add": {"table": {"family": "inet", +- "name": TABLE_NAME_POLICY}}}) ++ rules.extend(self._build_add_table_rules(TABLE_NAME_POLICY)) + + # To drop everything except existing connections we use + # "filter" because it occurs _after_ conntrack. +@@ -511,10 +573,7 @@ class nftables(object): + return list(supported) + + def build_default_tables(self): +- default_tables = [] +- default_tables.append({"add": {"table": {"family": "inet", +- "name": TABLE_NAME}}}) +- return default_tables ++ return self._build_add_table_rules(TABLE_NAME) + + def build_default_rules(self, log_denied="off"): + default_rules = [] +diff --git a/src/firewall/server/config.py b/src/firewall/server/config.py +index b805e497bb05..c07a1e6a2503 100644 +--- a/src/firewall/server/config.py ++++ b/src/firewall/server/config.py +@@ -110,6 +110,7 @@ class FirewallDConfig(DbusServiceObject): + "FlushAllOnReload": "readwrite", + "RFC3964_IPv4": "readwrite", + "AllowZoneDrifting": "readwrite", ++ "NftablesTableOwner": "readwrite", + } + ) + +@@ -565,7 +566,7 @@ class FirewallDConfig(DbusServiceObject): + "CleanupModulesOnExit", "Lockdown", "IPv6_rpfilter", + "IndividualCalls", "LogDenied", "AutomaticHelpers", + "FirewallBackend", "FlushAllOnReload", "RFC3964_IPv4", +- "AllowZoneDrifting", "IPv6_rpfilter2" ]: ++ "AllowZoneDrifting", "IPv6_rpfilter2", "NftablesTableOwner" ]: + raise dbus.exceptions.DBusException( + "org.freedesktop.DBus.Error.InvalidArgs: " + "Property '%s' does not exist" % prop) +@@ -631,6 +632,10 @@ class FirewallDConfig(DbusServiceObject): + if value is None: + value = "yes" if config.FALLBACK_ALLOW_ZONE_DRIFTING else "no" + return dbus.String(value) ++ elif prop == "NftablesTableOwner": ++ if value is None: ++ value = "yes" if config.FALLBACK_NFTABLES_TABLE_OWNER else "no" ++ return dbus.String(value) + + @dbus_handle_exceptions + def _get_dbus_property(self, prop): +@@ -662,6 +667,8 @@ class FirewallDConfig(DbusServiceObject): + return dbus.String(self._get_property(prop)) + elif prop == "AllowZoneDrifting": + return dbus.String(self._get_property(prop)) ++ elif prop == "NftablesTableOwner": ++ return dbus.String(self._get_property(prop)) + else: + raise dbus.exceptions.DBusException( + "org.freedesktop.DBus.Error.InvalidArgs: " +@@ -701,7 +708,7 @@ class FirewallDConfig(DbusServiceObject): + "CleanupModulesOnExit", "Lockdown", "IPv6_rpfilter", + "IndividualCalls", "LogDenied", "AutomaticHelpers", + "FirewallBackend", "FlushAllOnReload", "RFC3964_IPv4", +- "AllowZoneDrifting", "IPv6_rpfilter2" ]: ++ "AllowZoneDrifting", "IPv6_rpfilter2", "NftablesTableOwner" ]: + ret[x] = self._get_property(x) + elif interface_name in [ config.dbus.DBUS_INTERFACE_CONFIG_DIRECT, + config.dbus.DBUS_INTERFACE_CONFIG_POLICIES ]: +@@ -730,11 +737,12 @@ class FirewallDConfig(DbusServiceObject): + "IPv6_rpfilter", "IndividualCalls", + "LogDenied", + "FirewallBackend", "FlushAllOnReload", +- "RFC3964_IPv4", "IPv6_rpfilter2"]: ++ "RFC3964_IPv4", "IPv6_rpfilter2", ++ "NftablesTableOwner" ]: + if property_name in [ "CleanupOnExit", "CleanupModulesOnExit", + "Lockdown", "IPv6_rpfilter", + "IndividualCalls", "FlushAllOnReload", +- "RFC3964_IPv4"]: ++ "RFC3964_IPv4", "NftablesTableOwner" ]: + if new_value.lower() not in [ "yes", "no", + "true", "false" ]: + raise FirewallError(errors.INVALID_VALUE, +diff --git a/src/tests/dbus/firewalld.conf.at b/src/tests/dbus/firewalld.conf.at +index 61c220f3e59c..2ead41baa00a 100644 +--- a/src/tests/dbus/firewalld.conf.at ++++ b/src/tests/dbus/firewalld.conf.at +@@ -30,6 +30,7 @@ string "IndividualCalls" : variant string m4_escape(["${EXPECTED_INDIVIDUAL_CALL + string "Lockdown" : variant string "no" + string "LogDenied" : variant string "off" + string "MinimalMark" : variant int32 100 ++string "NftablesTableOwner" : variant string "yes" + string "RFC3964_IPv4" : variant string "yes" + ]) + +@@ -54,6 +55,7 @@ _helper([CleanupModulesOnExit], [string:"yes"], [variant string "yes"]) + _helper([CleanupOnExit], [string:"no"], [variant string "no"]) + _helper([RFC3964_IPv4], [string:"no"], [variant string "no"]) + _helper([AllowZoneDrifting], [string:"yes"], [variant string "no"]) ++_helper([NftablesTableOwner], [string:"no"], [variant string "no"]) + dnl Note: DefaultZone is RO + m4_undefine([_helper]) + +diff --git a/src/tests/regression/rhbz2222044.at b/src/tests/regression/rhbz2222044.at +index 7e3454509188..2d0333865076 100644 +--- a/src/tests/regression/rhbz2222044.at ++++ b/src/tests/regression/rhbz2222044.at +@@ -2,6 +2,11 @@ FWD_START_TEST([duplicate rules after restart]) + AT_KEYWORDS(rhbz2222044) + AT_SKIP_IF([! NS_CMD([command -v wc >/dev/null 2>&1])]) + ++dnl Disable for this test because CI do not support table owner. It's very new ++dnl in nftables. ++AT_CHECK([sed -i 's/^NftablesTableOwner=.*/NftablesTableOwner=no/' ./firewalld.conf]) ++FWD_RELOAD() ++ + dnl rules have not changed so rule count should not change + m4_define([check_rule_count], [ + m4_if(nftables, FIREWALL_BACKEND, [ +-- +2.43.5 + diff --git a/SOURCES/0021-v2.2.0-test-nftables-table-ownership.patch b/SOURCES/0021-v2.2.0-test-nftables-table-ownership.patch new file mode 100644 index 0000000..2c90e8b --- /dev/null +++ b/SOURCES/0021-v2.2.0-test-nftables-table-ownership.patch @@ -0,0 +1,69 @@ +From bf91ea35e7faf66484bdae7d0b3260c4717ee39a Mon Sep 17 00:00:00 2001 +From: Eric Garver +Date: Tue, 18 Jun 2024 16:20:06 -0400 +Subject: [PATCH 21/22] v2.2.0: test(nftables): table ownership + +Coverage: RHEL-17002 +(cherry picked from commit e7728b843c2ec3a61dbe436575c977e2ad9c8674) +--- + src/tests/features/features.at | 1 + + src/tests/features/nftables_table_owner.at | 38 ++++++++++++++++++++++ + 2 files changed, 39 insertions(+) + create mode 100644 src/tests/features/nftables_table_owner.at + +diff --git a/src/tests/features/features.at b/src/tests/features/features.at +index 065cb2872e88..83ad9d122189 100644 +--- a/src/tests/features/features.at ++++ b/src/tests/features/features.at +@@ -21,3 +21,4 @@ m4_include([features/ipset.at]) + m4_include([features/reset_defaults.at]) + m4_include([features/iptables_no_flush_on_shutdown.at]) + m4_include([features/reloadpolicy.at]) ++m4_include([features/nftables_table_owner.at]) +diff --git a/src/tests/features/nftables_table_owner.at b/src/tests/features/nftables_table_owner.at +new file mode 100644 +index 000000000000..abc946da0ad7 +--- /dev/null ++++ b/src/tests/features/nftables_table_owner.at +@@ -0,0 +1,38 @@ ++m4_if(nftables, FIREWALL_BACKEND, [ ++FWD_START_TEST([nftables table owner]) ++AT_KEYWORDS(RHEL-17002) ++ ++AT_CHECK([sed -i 's/^NftablesTableOwner=.*/NftablesTableOwner=yes/' ./firewalld.conf]) ++FWD_RELOAD() ++ ++AT_SKIP_IF([grep "Configuration has NftablesTableOwner=True, but it's not supported by nftables." ./firewalld.log]) ++ ++NS_CHECK([nft list table inet firewalld | TRIM_WHITESPACE | head -n 2], 0, [m4_strip([dnl ++ table inet firewalld { # progname firewalld ++ flags owner,persist ++])]) ++ ++dnl Test the transitions from On to Off ++dnl ++ ++AT_CHECK([sed -i 's/^NftablesTableOwner=.*/NftablesTableOwner=no/' ./firewalld.conf]) ++FWD_RELOAD() ++ ++NS_CHECK([nft list table inet firewalld | TRIM_WHITESPACE | head -n 2], 0, [m4_strip([dnl ++ table inet firewalld { ++ chain mangle_PREROUTING { ++])]) ++ ++dnl Test the transitions from Off to On ++dnl ++ ++AT_CHECK([sed -i 's/^NftablesTableOwner=.*/NftablesTableOwner=yes/' ./firewalld.conf]) ++FWD_RELOAD() ++ ++NS_CHECK([nft list table inet firewalld | TRIM_WHITESPACE | head -n 2], 0, [m4_strip([dnl ++ table inet firewalld { # progname firewalld ++ flags owner,persist ++])]) ++ ++FWD_END_TEST() ++]) +-- +2.43.5 + diff --git a/SOURCES/0022-v2.2.0-chore-service-remove-Conflicts-with-nftables.patch b/SOURCES/0022-v2.2.0-chore-service-remove-Conflicts-with-nftables.patch new file mode 100644 index 0000000..f416968 --- /dev/null +++ b/SOURCES/0022-v2.2.0-chore-service-remove-Conflicts-with-nftables.patch @@ -0,0 +1,29 @@ +From 8d87974a34c055e0daee3a83f11c539fc98fd6bf Mon Sep 17 00:00:00 2001 +From: Eric Garver +Date: Wed, 26 Jun 2024 15:31:38 -0400 +Subject: [PATCH 22/22] v2.2.0: chore(service): remove Conflicts with nftables + +Now that firewalld uses the table owner flag it can coexist with the +nftables service. + +(cherry picked from commit 9c6cb982981ad0aee5b85773823614ca7fd69073) +--- + config/firewalld.service.in | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/config/firewalld.service.in b/config/firewalld.service.in +index afbe0ac5def7..b757a08f28dc 100644 +--- a/config/firewalld.service.in ++++ b/config/firewalld.service.in +@@ -4,7 +4,7 @@ Before=network-pre.target + Wants=network-pre.target + After=dbus.service + After=polkit.service +-Conflicts=iptables.service ip6tables.service ebtables.service ipset.service nftables.service ++Conflicts=iptables.service ip6tables.service ebtables.service ipset.service + Documentation=man:firewalld(1) + + [Service] +-- +2.43.5 + diff --git a/SPECS/firewalld.spec b/SPECS/firewalld.spec index b2b2a55..f211caf 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: 1%{?dist} +Release: 7%{?dist} URL: http://www.firewalld.org License: GPLv2+ Source0: https://github.com/firewalld/firewalld/releases/download/v%{version}/firewalld-%{version}.tar.bz2 @@ -9,6 +9,24 @@ Patch1: 0001-RHEL-only-Add-cockpit-by-default-to-some-zones.patch Patch2: 0002-v1.4.0-test-atlocal-pass-EBTABLES-to-testsuite.patch Patch3: 0003-v1.4.0-feat-direct-avoid-iptables-flush-if-using-nft.patch Patch4: 0004-v1.4.0-test-direct-avoid-iptables-flush-if-using-nft.patch +Patch5: 0005-v2.0.0-feat-service-add-OpenTelemetry-OTLP-service.patch +Patch6: 0006-v2.1.0-feat-icmp-add-ICMPv6-Multicast-Listener-Disco.patch +Patch7: 0007-v2.1.0-fix-rich-validate-service-name-of-rich-rule.patch +Patch8: 0008-v2.1.0-improvement-nftables-do-not-track-rule-handle.patch +Patch9: 0009-v2.1.0-improvement-fw-make-set_policy-DROP-more-flex.patch +Patch10: 0010-v2.1.0-feat-fw-add-ReloadPolicy-option-in-firewalld..patch +Patch11: 0011-v2.2.0-test-functions-add-macro-CHECK_NFTABLES_FIB.patch +Patch12: 0012-v2.2.0-test-functions-add-macro-CHECK_NFTABLES_FIB_I.patch +Patch13: 0013-v2.2.0-test-rpfilter-use-CHECK-macros.patch +Patch14: 0014-v2.2.0-test-IPv6_rpfilter-verify-valid-values.patch +Patch15: 0015-v2.2.0-chore-IPv6_rpfilter-prepare-for-new-config-va.patch +Patch16: 0016-v2.2.0-feat-IPv6_rpfilter-support-loose-rpfilter.patch +Patch17: 0017-v2.2.0-feat-IPv6_rpfilter-support-loose-forward-rpfi.patch +Patch18: 0018-v2.2.0-feat-IPv6_rpfilter-support-strict-forward-rpf.patch +Patch19: 0019-v2.2.0-test-functions-start-firewalld-with-file-logg.patch +Patch20: 0020-v2.2.0-feat-nftables-table-ownership.patch +Patch21: 0021-v2.2.0-test-nftables-table-ownership.patch +Patch22: 0022-v2.2.0-chore-service-remove-Conflicts-with-nftables.patch BuildArch: noarch BuildRequires: autoconf BuildRequires: automake @@ -111,6 +129,8 @@ end %autosetup -p1 %build +# must run automake since patches touch .am files +./autogen.sh %configure --enable-sysconfig --enable-rpmmacros PYTHON="%{__python3} %{py3_shbang_opts}" make %{?_smp_mflags} @@ -230,6 +250,26 @@ rm -rf %{buildroot}%{_datadir}/firewalld/testsuite %{_mandir}/man1/firewall-config*.1* %changelog +* Mon Jul 01 2024 Eric Garver - 1.3.4-7 +- feat(nftables): table ownership + +* Mon Jul 01 2024 Eric Garver - 1.3.4-6 +- feat(IPv6_rpfilter): support loose rpfilter +- feat(IPv6_rpfilter): support loose-forward rpfilter +- feat(IPv6_rpfilter): support strict-forward rpfilter + +* Mon Jul 01 2024 Eric Garver - 1.3.4-5 +- feat(fw): add ReloadPolicy option in firewalld.conf + +* Mon Jul 01 2024 Eric Garver - 1.3.4-4 +- fix(rich): validate service name of rich rule + +* Mon Jul 01 2024 Eric Garver - 1.3.4-3 +- feat(icmp): add ICMPv6 Multicast Listener Discovery (MLD) types + +* Mon Jul 01 2024 Eric Garver - 1.3.4-2 +- feat(service): add OpenTelemetry (OTLP) service + * Thu Oct 26 2023 Eric Garver - 1.3.4-1 - package rebase to v1.3.4