This is an automated email from the ASF dual-hosted git repository.
DaanHoogland pushed a commit to branch 4.22
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/4.22 by this push:
new 8074c3e45e7 VPC egress ACL: end ACL_OUTBOUND with RETURN so its rules
stay in order (#14355)
8074c3e45e7 is described below
commit 8074c3e45e7005b5881dd02bb92634d80bb75a8b
Author: Brad House - Nexthop <[email protected]>
AuthorDate: Thu Oct 22 08:43:39 2026 -0400
VPC egress ACL: end ACL_OUTBOUND with RETURN so its rules stay in order
(#14355)
Signed-off-by: Brad House <[email protected]>
---
systemvm/debian/opt/cloud/bin/cs/CsAddress.py | 10 ++++
systemvm/test/TestCsAddress.py | 77 ++++++++++++++++++++++++++-
2 files changed, 86 insertions(+), 1 deletion(-)
diff --git a/systemvm/debian/opt/cloud/bin/cs/CsAddress.py
b/systemvm/debian/opt/cloud/bin/cs/CsAddress.py
index e15eb7a2e3d..c95aec0be3e 100755
--- a/systemvm/debian/opt/cloud/bin/cs/CsAddress.py
+++ b/systemvm/debian/opt/cloud/bin/cs/CsAddress.py
@@ -586,6 +586,10 @@ class CsIP:
["mangle", "front", "-A ACL_OUTBOUND_%s -d 225.0.0.50/32
-j ACCEPT" % self.dev])
self.fw.append(
["mangle", "front", "-A ACL_OUTBOUND_%s -d 224.0.0.18/32
-j ACCEPT" % self.dev])
+ # ACL rules are inserted ahead of the chain's last rule, so
like ACL_INBOUND's DROP
+ # this has to stay last; RETURN is what falling off the end of
the chain does anyway
+ self.fw.append(
+ ["mangle", "", "-A ACL_OUTBOUND_%s -j RETURN" % self.dev])
else:
self.fw.append(["filter", "", "-A FORWARD -d %s -o %s -j
ACCEPT" % (guestNetworkCidr, self.dev)])
@@ -639,6 +643,11 @@ class CsIP:
self.fw.append(["filter", "front",
"-A FORWARD -d %s -o %s -m state --state
RELATED,ESTABLISHED -j ACCEPT" %
(static_route['network'], self.dev)])
+ if self.get_type() in ["guest"] and not
self.config.has_public_network():
+ # Without a public network a tier's ACL chains get no
last rule of their own, and are
+ # only jumped to from here; close them too, so the ACL
rules stay in order
+ self.fw.append(["filter", "", "-A ACL_INBOUND_%s -j
RETURN" % self.dev])
+ self.fw.append(["mangle", "", "-A ACL_OUTBOUND_%s -j
RETURN" % self.dev])
if self.is_private_gateway():
self.fw.append(["filter", "front", "-A FORWARD -d %s -o %s -j
ACL_INBOUND_%s" %
@@ -646,6 +655,7 @@ class CsIP:
self.fw.append(["filter", "front", "-A FORWARD -d %s -o %s -m
state --state RELATED,ESTABLISHED -j ACCEPT" %
(self.address['network'], self.dev)])
self.fw.append(["filter", "", "-A ACL_INBOUND_%s -j DROP" %
self.dev])
+ self.fw.append(["mangle", "", "-A ACL_OUTBOUND_%s -j RETURN" %
self.dev])
self.fw.append(["mangle", "",
"-A PREROUTING -m state --state NEW -i %s -s %s !
-d %s/32 -j ACL_OUTBOUND_%s" %
(self.dev, self.address['network'],
self.address['gateway'], self.dev)])
diff --git a/systemvm/test/TestCsAddress.py b/systemvm/test/TestCsAddress.py
index 0ad9ae861b9..695cc6d7ba4 100644
--- a/systemvm/test/TestCsAddress.py
+++ b/systemvm/test/TestCsAddress.py
@@ -16,10 +16,42 @@
# under the License.
import unittest
-from cs.CsAddress import CsAddress
+from unittest import mock
+from cs.CsAddress import CsAddress, CsIP
import merge
+class FakeConfig:
+
+ def __init__(self):
+ self.fw = []
+ self.nft_ipv4_fw = []
+ self.nft_ipv4_acl = []
+
+ def get_fw(self):
+ return self.fw
+
+ def get_nft_ipv4_fw(self):
+ return self.nft_ipv4_fw
+
+ def get_nft_ipv4_acl(self):
+ return self.nft_ipv4_acl
+
+ def cmdline(self):
+ cl = mock.Mock()
+ cl.get_vpccidr.return_value = "10.0.0.0/16"
+ return cl
+
+ def is_vpc(self):
+ return True
+
+ def is_routed(self):
+ return False
+
+ def has_public_network(self):
+ return True
+
+
class TestCsAddress(unittest.TestCase):
def setUp(self):
@@ -38,6 +70,49 @@ class TestCsAddress(unittest.TestCase):
def test_get_guest_netmask(self):
self.assertTrue(self.csaddress.get_guest_netmask() == "255.255.255.0")
+ def acl_rules(self, address, chain="ACL_OUTBOUND_eth3",
public_network=True, static_routes=None):
+ config = FakeConfig()
+ config.has_public_network = lambda: public_network
+ with mock.patch.object(CsIP, "list"):
+ ip = CsIP("eth3", config)
+ ip.setAddress(address)
+ routes = mock.Mock()
+ routes.get_bag.return_value = static_routes or {}
+ with mock.patch("cs.CsAddress.CsStaticRoutes", return_value=routes):
+ ip.fw_vpcrouter()
+ return [fw for fw in config.fw if "-A %s " % chain in fw[2]]
+
+ def acl_outbound_rules(self, address):
+ return self.acl_rules(address)
+
+ def test_acl_outbound_ends_with_return_on_guest_tier(self):
+ # ACL rules are inserted ahead of the last rule of the chain, so that
rule must be a
+ # terminal one, or it would end up behind the ACL rules
+ rules = self.acl_outbound_rules({"nw_type": "guest", "network":
"10.0.1.0/24", "gateway": "10.0.1.1"})
+ self.assertEqual(rules[-1], ["mangle", "", "-A ACL_OUTBOUND_eth3 -j
RETURN"])
+ self.assertTrue(all(fw[1] == "front" for fw in rules[:-1]))
+
+ def
test_acl_chains_end_with_return_on_static_route_tier_without_public_network(self):
+ # such a tier's ACL chains are only jumped to for the static route,
and get no last rule otherwise
+ address = {"nw_type": "guest", "network": "10.0.1.0/24", "gateway":
"10.0.1.1", "public_ip": "10.0.1.1"}
+ routes = {"id": "staticroutes", "192.168.50.0/24": {"network":
"192.168.50.0/24", "ip_address": "10.0.1.1", "revoke": False}}
+ for chain, table in (("ACL_INBOUND_eth3", "filter"),
("ACL_OUTBOUND_eth3", "mangle")):
+ rules = self.acl_rules(address, chain, public_network=False,
static_routes=routes)
+ self.assertEqual(rules, [[table, "", "-A %s -j RETURN" % chain]])
+
+ def
test_acl_chains_get_one_last_rule_on_static_route_tier_with_public_network(self):
+ address = {"nw_type": "guest", "network": "10.0.1.0/24", "gateway":
"10.0.1.1", "public_ip": "10.0.1.1"}
+ routes = {"id": "staticroutes", "192.168.50.0/24": {"network":
"192.168.50.0/24", "ip_address": "10.0.1.1", "revoke": False}}
+ inbound = self.acl_rules(address, "ACL_INBOUND_eth3",
static_routes=routes)
+ self.assertEqual([fw[2] for fw in inbound if fw[1] == ""], ["-A
ACL_INBOUND_eth3 -j DROP"])
+ outbound = self.acl_rules(address, "ACL_OUTBOUND_eth3",
static_routes=routes)
+ self.assertEqual([fw[2] for fw in outbound if fw[1] == ""], ["-A
ACL_OUTBOUND_eth3 -j RETURN"])
+
+ def test_acl_outbound_ends_with_return_on_private_gateway(self):
+ rules = self.acl_outbound_rules({"nw_type": "public",
"is_private_gateway": True, "network": "172.16.0.0/24",
+ "gateway": "172.16.0.1", "public_ip":
"172.16.0.10", "source_nat": False})
+ self.assertEqual(rules, [["mangle", "", "-A ACL_OUTBOUND_eth3 -j
RETURN"]])
+
if __name__ == '__main__':
unittest.main()