From 325f87eb0c3f10476d098f5032aa0460964e1f8b Mon Sep 17 00:00:00 2001 From: Eric Garver Date: Thu, 13 Jun 2024 17:00:56 -0400 Subject: [PATCH] feat(rich): support using ipset in destination Resolves: RHEL-29875 --- ...h-support-using-ipset-in-destination.patch | 242 ++++++++++++++++++ 0019-v1.0.0-test-rich-destination-ipset.patch | 60 +++++ ...-destination-ipset-verify-policy-sup.patch | 63 +++++ firewalld.spec | 8 +- 4 files changed, 372 insertions(+), 1 deletion(-) create mode 100644 0018-v1.0.0-feat-rich-support-using-ipset-in-destination.patch create mode 100644 0019-v1.0.0-test-rich-destination-ipset.patch create mode 100644 0020-v1.0.0-test-rich-destination-ipset-verify-policy-sup.patch diff --git a/0018-v1.0.0-feat-rich-support-using-ipset-in-destination.patch b/0018-v1.0.0-feat-rich-support-using-ipset-in-destination.patch new file mode 100644 index 0000000..3d5e0a1 --- /dev/null +++ b/0018-v1.0.0-feat-rich-support-using-ipset-in-destination.patch @@ -0,0 +1,242 @@ +From 0715e07a68d50d33797a724d24157a96afee3de6 Mon Sep 17 00:00:00 2001 +From: Derek Dai +Date: Tue, 10 Nov 2020 20:37:36 +0800 +Subject: [PATCH 18/26] v1.0.0: feat(rich): support using ipset in destination + +Fixes: #706 +Closes: #711 +(cherry picked from commit 286d00031f431f3c3d0f94028975a409e78be8c8) +--- + doc/xml/firewalld.richlanguage.xml | 2 +- + src/firewall/core/io/policy.py | 21 ++++++++++---- + src/firewall/core/io/zone.py | 4 +-- + src/firewall/core/ipXtables.py | 25 +++++++++++------ + src/firewall/core/nftables.py | 7 ++++- + src/firewall/core/rich.py | 44 ++++++++++++++++++++++-------- + 6 files changed, 74 insertions(+), 29 deletions(-) + +diff --git a/doc/xml/firewalld.richlanguage.xml b/doc/xml/firewalld.richlanguage.xml +index e336bfd0b464..19bd038fc1fd 100644 +--- a/doc/xml/firewalld.richlanguage.xml ++++ b/doc/xml/firewalld.richlanguage.xml +@@ -129,7 +129,7 @@ source [not] address="address[/mask]"|mac="mac-address"|ipset="ipset" + Destination + + +-destination [not] address="address[/mask]" ++destination [not] address="address[/mask]"|ipset="ipset" + + With the destination address the target can be limited to the destination address. The destination address is using the same syntax as the source address. + +diff --git a/src/firewall/core/io/policy.py b/src/firewall/core/io/policy.py +index c543aa1b42a6..3b951545e975 100644 +--- a/src/firewall/core/io/policy.py ++++ b/src/firewall/core/io/policy.py +@@ -186,11 +186,18 @@ def common_startElement(obj, name, attrs): + str(obj._rule)) + return True + invert = False ++ address = None ++ if "address" in attrs: ++ address = attrs["address"] ++ ipset = None ++ if "ipset" in attrs: ++ ipset = attrs["ipset"] + if "invert" in attrs and \ + attrs["invert"].lower() in [ "yes", "true" ]: + invert = True +- obj._rule.destination = rich.Rich_Destination(attrs["address"], +- invert) ++ obj._rule.destination = rich.Rich_Destination(address, ++ ipset, ++ invert) + + elif name in [ "accept", "reject", "drop", "mark" ]: + if not obj._rule: +@@ -447,7 +454,11 @@ def common_writer(obj, handler): + + # destination + if rule.destination: +- attrs = { "address": rule.destination.addr } ++ attrs = { } ++ if rule.destination.addr: ++ attrs["address"] = rule.destination.addr ++ if rule.destination.ipset: ++ attrs["ipset"] = rule.destination.ipset + if rule.destination.invert: + attrs["invert"] = "True" + handler.ignorableWhitespace(" ") +@@ -607,7 +618,7 @@ class Policy(IO_Object): + "forward-port": [ "port", "protocol" ], + "rule": None, + "source": None, +- "destination": [ "address" ], ++ "destination": None, + "protocol": [ "value" ], + "source-port": [ "port", "protocol" ], + "log": None, +@@ -625,7 +636,7 @@ class Policy(IO_Object): + "forward-port": [ "to-port", "to-addr" ], + "rule": [ "family", "priority" ], + "source": [ "address", "mac", "invert", "family", "ipset" ], +- "destination": [ "invert" ], ++ "destination": [ "address", "invert", "ipset" ], + "log": [ "prefix", "level" ], + "reject": [ "type" ], + } +diff --git a/src/firewall/core/io/zone.py b/src/firewall/core/io/zone.py +index 4291ec9cba00..0c419ee0f2bd 100644 +--- a/src/firewall/core/io/zone.py ++++ b/src/firewall/core/io/zone.py +@@ -73,7 +73,7 @@ class Zone(IO_Object): + "interface": [ "name" ], + "rule": None, + "source": None, +- "destination": [ "address" ], ++ "destination": None, + "protocol": [ "value" ], + "source-port": [ "port", "protocol" ], + "log": None, +@@ -91,7 +91,7 @@ class Zone(IO_Object): + "forward-port": [ "to-port", "to-addr" ], + "rule": [ "family", "priority" ], + "source": [ "address", "mac", "invert", "family", "ipset" ], +- "destination": [ "invert" ], ++ "destination": [ "address", "invert", "ipset" ], + "log": [ "prefix", "level" ], + "reject": [ "type" ], + } +diff --git a/src/firewall/core/ipXtables.py b/src/firewall/core/ipXtables.py +index cf6c6e03e7ad..401377104ce1 100644 +--- a/src/firewall/core/ipXtables.py ++++ b/src/firewall/core/ipXtables.py +@@ -1093,15 +1093,22 @@ class ip4tables(object): + return [] + + rule_fragment = [] +- if rich_dest.invert: +- rule_fragment.append("!") +- if check_single_address("ipv6", rich_dest.addr): +- rule_fragment += [ "-d", normalizeIP6(rich_dest.addr) ] +- elif check_address("ipv6", rich_dest.addr): +- addr_split = rich_dest.addr.split("/") +- rule_fragment += [ "-d", normalizeIP6(addr_split[0]) + "/" + addr_split[1] ] +- else: +- rule_fragment += [ "-d", rich_dest.addr ] ++ if rich_dest.addr: ++ if rich_dest.invert: ++ rule_fragment.append("!") ++ if check_single_address("ipv6", rich_dest.addr): ++ rule_fragment += [ "-d", normalizeIP6(rich_dest.addr) ] ++ elif check_address("ipv6", rich_dest.addr): ++ addr_split = rich_dest.addr.split("/") ++ rule_fragment += [ "-d", normalizeIP6(addr_split[0]) + "/" + addr_split[1] ] ++ else: ++ rule_fragment += [ "-d", rich_dest.addr ] ++ elif rich_dest.ipset: ++ rule_fragment += [ "-m", "set" ] ++ if rich_dest.invert: ++ rule_fragment.append("!") ++ flags = self._fw.zone._ipset_match_flags(rich_dest.ipset, "dst") ++ rule_fragment += [ "--match-set", rich_dest.ipset, flags ] + + return rule_fragment + +diff --git a/src/firewall/core/nftables.py b/src/firewall/core/nftables.py +index 2a13b2678a94..d238451ebd5d 100644 +--- a/src/firewall/core/nftables.py ++++ b/src/firewall/core/nftables.py +@@ -1253,7 +1253,12 @@ class nftables(object): + def _rich_rule_destination_fragment(self, rich_dest): + if not rich_dest: + return {} +- return self._rule_addr_fragment("daddr", rich_dest.addr, invert=rich_dest.invert) ++ if rich_dest.addr: ++ address = rich_dest.addr ++ elif rich_dest.ipset: ++ address = "ipset:" + rich_dest.ipset ++ ++ return self._rule_addr_fragment("daddr", address, invert=rich_dest.invert) + + def _rich_rule_source_fragment(self, rich_source): + if not rich_source: +diff --git a/src/firewall/core/rich.py b/src/firewall/core/rich.py +index 03bc194c2b28..6a03eeca5d8a 100644 +--- a/src/firewall/core/rich.py ++++ b/src/firewall/core/rich.py +@@ -63,13 +63,27 @@ class Rich_Source(object): + "no address, mac and ipset") + + class Rich_Destination(object): +- def __init__(self, addr, invert=False): ++ def __init__(self, addr, ipset, invert=False): + self.addr = addr ++ if self.addr == "": ++ self.addr = None ++ self.ipset = ipset ++ if self.ipset == "": ++ self.ipset = None + self.invert = invert ++ if self.addr is None and self.ipset is None: ++ raise FirewallError(errors.INVALID_RULE, ++ "no address and ipset") + + def __str__(self): +- return 'destination %saddress="%s"' % ("not " if self.invert else "", +- self.addr) ++ ret = 'destination%s ' % (" NOT" if self.invert else "") ++ if self.addr is not None: ++ return ret + 'address="%s"' % self.addr ++ elif self.ipset is not None: ++ return ret + 'ipset="%s"' % self.ipset ++ else: ++ raise FirewallError(errors.INVALID_RULE, ++ "no address and ipset") + + class Rich_Service(object): + def __init__(self, name): +@@ -404,12 +418,12 @@ class Rich_Rule(object): + attrs.clear() + index = index -1 # return token to input + elif in_element == 'destination': +- if attr_name in ['address', 'invert']: ++ if attr_name in ['address', 'ipset', 'invert']: + attrs[attr_name] = attr_value + elif element in ['not', 'NOT']: + attrs['invert'] = True + else: +- self.destination = Rich_Destination(attrs.get('address'), attrs.get('invert')) ++ self.destination = Rich_Destination(attrs.get('address'), attrs.get('ipset'), attrs.get('invert', False)) + in_elements.pop() # destination + attrs.clear() + index = index -1 # return token to input +@@ -587,12 +601,20 @@ class Rich_Rule(object): + + # destination + if self.destination is not None: +- if self.family is None: +- raise FirewallError(errors.INVALID_FAMILY) +- if self.destination.addr is None or \ +- not functions.check_address(self.family, +- self.destination.addr): +- raise FirewallError(errors.INVALID_ADDR, str(self.destination.addr)) ++ if self.destination.addr is not None: ++ if self.family is None: ++ raise FirewallError(errors.INVALID_FAMILY) ++ if self.destination.ipset is not None: ++ raise FirewallError(errors.INVALID_DESTINATION, "address and ipset") ++ if not functions.check_address(self.family, self.destination.addr): ++ raise FirewallError(errors.INVALID_ADDR, str(self.destination.addr)) ++ ++ elif self.destination.ipset is not None: ++ if not check_ipset_name(self.destination.ipset): ++ raise FirewallError(errors.INVALID_IPSET, str(self.destination.ipset)) ++ ++ else: ++ raise FirewallError(errors.INVALID_RULE, "invalid destination") + + # service + if type(self.element) == Rich_Service: +-- +2.43.0 + diff --git a/0019-v1.0.0-test-rich-destination-ipset.patch b/0019-v1.0.0-test-rich-destination-ipset.patch new file mode 100644 index 0000000..cc7e28c --- /dev/null +++ b/0019-v1.0.0-test-rich-destination-ipset.patch @@ -0,0 +1,60 @@ +From cf8a55d1fe769a9e4632fbccf5ae4738ab661421 Mon Sep 17 00:00:00 2001 +From: Eric Garver +Date: Thu, 12 Nov 2020 17:11:58 -0500 +Subject: [PATCH 19/26] v1.0.0: test(rich): destination ipset + +(cherry picked from commit f274bfd0f7bc0e466c42b732e03002e11e99ed88) +--- + src/tests/features/features.at | 1 + + src/tests/features/rich_destination_ipset.at | 30 ++++++++++++++++++++ + 2 files changed, 31 insertions(+) + create mode 100644 src/tests/features/rich_destination_ipset.at + +diff --git a/src/tests/features/features.at b/src/tests/features/features.at +index 2340853aeca7..381bf6dba0e4 100644 +--- a/src/tests/features/features.at ++++ b/src/tests/features/features.at +@@ -13,3 +13,4 @@ m4_include([features/rich_rules.at]) + m4_include([features/icmp_blocks.at]) + m4_include([features/rpfilter.at]) + m4_include([features/zone_combine.at]) ++m4_include([features/rich_destination_ipset.at]) +diff --git a/src/tests/features/rich_destination_ipset.at b/src/tests/features/rich_destination_ipset.at +new file mode 100644 +index 000000000000..c07809141851 +--- /dev/null ++++ b/src/tests/features/rich_destination_ipset.at +@@ -0,0 +1,30 @@ ++FWD_START_TEST([rich destination ipset]) ++AT_KEYWORDS(rich ipset) ++ ++FWD_CHECK([--permanent --new-ipset=foobar --type=hash:ip], 0, [ignore]) ++FWD_RELOAD ++ ++FWD_CHECK([--permanent --add-rich-rule='rule family=ipv4 destination ipset=foobar accept'], 0, [ignore]) ++FWD_CHECK([ --add-rich-rule='rule family=ipv4 destination ipset=foobar accept'], 0, [ignore]) ++NFT_LIST_RULES([inet], [filter_IN_public_allow], 0, [dnl ++ table inet firewalld { ++ chain filter_IN_public_allow { ++ tcp dport 22 ct state new,untracked accept ++ ip6 daddr fe80::/64 udp dport 546 ct state new,untracked accept ++ ip daddr @foobar accept ++ } ++ } ++]) ++IPTABLES_LIST_RULES([filter], [IN_public_allow], 0, [dnl ++ ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:22 ctstate NEW,UNTRACKED ++ ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 match-set foobar dst ++]) ++ ++dnl negative tests ++FWD_CHECK([--permanent --add-rich-rule='rule family=ipv4 destination bogus=foobar accept'], 122, [ignore], [ignore]) ++FWD_CHECK([ --add-rich-rule='rule family=ipv4 destination bogus=foobar accept'], 122, [ignore], [ignore]) ++FWD_CHECK([--permanent --add-rich-rule='rule family=ipv4 destination address=10.0.0.1 ipset=foobar accept'], 121, [ignore], [ignore]) ++FWD_CHECK([ --add-rich-rule='rule family=ipv4 destination address=10.0.0.1 ipset=foobar accept'], 121, [ignore], [ignore]) ++ ++FWD_END_TEST([-e '/ERROR: INVALID_RULE: bad attribute/d'dnl ++ -e '/ERROR: INVALID_DESTINATION: address and ipset/d']) +-- +2.43.0 + diff --git a/0020-v1.0.0-test-rich-destination-ipset-verify-policy-sup.patch b/0020-v1.0.0-test-rich-destination-ipset-verify-policy-sup.patch new file mode 100644 index 0000000..5472393 --- /dev/null +++ b/0020-v1.0.0-test-rich-destination-ipset-verify-policy-sup.patch @@ -0,0 +1,63 @@ +From 63100ca625942e6be2c68422e7a48bc68f8d01c5 Mon Sep 17 00:00:00 2001 +From: Eric Garver +Date: Fri, 13 Nov 2020 13:32:22 -0500 +Subject: [PATCH 20/26] v1.0.0: test(rich): destination ipset: verify policy + support + +(cherry picked from commit fdd120572cd45a6ea2515bc906b89482de6560ea) +--- + src/tests/features/rich_destination_ipset.at | 23 ++++++++++++++++++++ + 1 file changed, 23 insertions(+) + +diff --git a/src/tests/features/rich_destination_ipset.at b/src/tests/features/rich_destination_ipset.at +index c07809141851..3286755d2252 100644 +--- a/src/tests/features/rich_destination_ipset.at ++++ b/src/tests/features/rich_destination_ipset.at +@@ -1,9 +1,14 @@ + FWD_START_TEST([rich destination ipset]) + AT_KEYWORDS(rich ipset) + ++FWD_CHECK([--permanent --new-policy=mypolicy], 0, [ignore]) ++FWD_CHECK([--permanent --policy=mypolicy --add-ingress-zone ANY], 0, [ignore]) ++FWD_CHECK([--permanent --policy=mypolicy --add-egress-zone HOST], 0, [ignore]) ++ + FWD_CHECK([--permanent --new-ipset=foobar --type=hash:ip], 0, [ignore]) + FWD_RELOAD + ++dnl zone + FWD_CHECK([--permanent --add-rich-rule='rule family=ipv4 destination ipset=foobar accept'], 0, [ignore]) + FWD_CHECK([ --add-rich-rule='rule family=ipv4 destination ipset=foobar accept'], 0, [ignore]) + NFT_LIST_RULES([inet], [filter_IN_public_allow], 0, [dnl +@@ -20,11 +25,29 @@ IPTABLES_LIST_RULES([filter], [IN_public_allow], 0, [dnl + ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 match-set foobar dst + ]) + ++dnl policy ++FWD_CHECK([--permanent --policy mypolicy --add-rich-rule='rule family=ipv4 destination ipset=foobar accept'], 0, [ignore]) ++FWD_CHECK([ --policy mypolicy --add-rich-rule='rule family=ipv4 destination ipset=foobar accept'], 0, [ignore]) ++NFT_LIST_RULES([inet], [filter_IN_policy_mypolicy_allow], 0, [dnl ++ table inet firewalld { ++ chain filter_IN_policy_mypolicy_allow { ++ ip daddr @foobar accept ++ } ++ } ++]) ++IPTABLES_LIST_RULES([filter], [IN_mypolicy_allow], 0, [dnl ++ ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 match-set foobar dst ++]) ++ + dnl negative tests + FWD_CHECK([--permanent --add-rich-rule='rule family=ipv4 destination bogus=foobar accept'], 122, [ignore], [ignore]) + FWD_CHECK([ --add-rich-rule='rule family=ipv4 destination bogus=foobar accept'], 122, [ignore], [ignore]) + FWD_CHECK([--permanent --add-rich-rule='rule family=ipv4 destination address=10.0.0.1 ipset=foobar accept'], 121, [ignore], [ignore]) + FWD_CHECK([ --add-rich-rule='rule family=ipv4 destination address=10.0.0.1 ipset=foobar accept'], 121, [ignore], [ignore]) ++FWD_CHECK([--permanent --policy mypolicy --add-rich-rule='rule family=ipv4 destination bogus=foobar accept'], 122, [ignore], [ignore]) ++FWD_CHECK([ --policy mypolicy --add-rich-rule='rule family=ipv4 destination bogus=foobar accept'], 122, [ignore], [ignore]) ++FWD_CHECK([--permanent --policy mypolicy --add-rich-rule='rule family=ipv4 destination address=10.0.0.1 ipset=foobar accept'], 121, [ignore], [ignore]) ++FWD_CHECK([ --policy mypolicy --add-rich-rule='rule family=ipv4 destination address=10.0.0.1 ipset=foobar accept'], 121, [ignore], [ignore]) + + FWD_END_TEST([-e '/ERROR: INVALID_RULE: bad attribute/d'dnl + -e '/ERROR: INVALID_DESTINATION: address and ipset/d']) +-- +2.43.0 + diff --git a/firewalld.spec b/firewalld.spec index a21d348..868bea1 100644 --- a/firewalld.spec +++ b/firewalld.spec @@ -1,7 +1,7 @@ Summary: A firewall daemon with D-Bus interface providing a dynamic firewall Name: firewalld Version: 0.9.11 -Release: 4%{?dist} +Release: 5%{?dist} URL: http://www.firewalld.org License: GPLv2+ Source0: https://github.com/firewalld/firewalld/releases/download/v%{version}/firewalld-%{version}.tar.gz @@ -22,6 +22,9 @@ Patch14: 0014-v1.2.0-chore-nftables-add-delete-table-helper.patch Patch15: 0015-v1.2.0-fix-nftables-always-flush-main-table-on-start.patch Patch16: 0016-v1.2.0-test-CleanUpOnExit-verify-restart-does-not-du.patch Patch17: 0017-v1.2.0-chore-nftables-policy-use-delete-table-helper.patch +Patch18: 0018-v1.0.0-feat-rich-support-using-ipset-in-destination.patch +Patch19: 0019-v1.0.0-test-rich-destination-ipset.patch +Patch20: 0020-v1.0.0-test-rich-destination-ipset-verify-policy-sup.patch BuildArch: noarch BuildRequires: autoconf @@ -223,6 +226,9 @@ desktop-file-install --delete-original \ %{_mandir}/man1/firewall-config*.1* %changelog +* Thu Jun 13 2024 Eric Garver - 0.9.11-5 +- feat(rich): support using ipset in destination + * Fri Nov 03 2023 Eric Garver - 0.9.11-4 - fix(nftables): always flush main table on start