From 22556225ffc4bf423f4b5e9a14c2d0e1635a97ee Mon Sep 17 00:00:00 2001 From: Brad House Date: Thu, 8 Oct 2026 00:49:05 +0000 Subject: [PATCH 1/3] Match protocol-number ACL rules with ip protocol in routed mode In routed mode the VR renders IPv4 ACL rules as nftables, and a rule for a protocol number (e.g. 47, GRE) came out as "ip nexthdr 47". nexthdr is an IPv6 header field; nft rejects it in an ip rule with a syntax error, which is only logged, so the rule was silently missing from ip4_acl. Use the IPv4 field, "ip protocol". The IPv6 path's "ip6 nexthdr" is correct and unchanged. Fixes: #14351 Signed-off-by: Brad House --- systemvm/debian/opt/cloud/bin/configure.py | 2 +- systemvm/test/TestCsAclRouting.py | 63 ++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 systemvm/test/TestCsAclRouting.py diff --git a/systemvm/debian/opt/cloud/bin/configure.py b/systemvm/debian/opt/cloud/bin/configure.py index bf48be66694c..44dc45abf114 100755 --- a/systemvm/debian/opt/cloud/bin/configure.py +++ b/systemvm/debian/opt/cloud/bin/configure.py @@ -449,7 +449,7 @@ def __process_routing_ip4(self, direction, rule_list): if protocol != "all": icmp_type = "" if protocol == "protocol": - protocol = "ip nexthdr %d" % rule['protocol'] + protocol = "ip protocol %d" % rule['protocol'] proto = protocol if proto == "icmp": proto = proto_str = "icmp" diff --git a/systemvm/test/TestCsAclRouting.py b/systemvm/test/TestCsAclRouting.py new file mode 100644 index 000000000000..cc02cd28685a --- /dev/null +++ b/systemvm/test/TestCsAclRouting.py @@ -0,0 +1,63 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import unittest +from configure import CsAcl + + +class FakeConfig: + + def __init__(self): + self.fw = [] + self.ipv6_acl = [] + self.nft_ipv4_acl = [] + + def get_fw(self): + return self.fw + + def get_ipv6_acl(self): + return self.ipv6_acl + + def get_nft_ipv4_acl(self): + return self.nft_ipv4_acl + + def is_vpc(self): + return True + + def is_routed(self): + return True + + +class TestCsAclRouting(unittest.TestCase): + + def test_protocol_number_rule(self): + config = FakeConfig() + obj = {"device": "eth3", "nic_ip": "10.1.1.1", "nic_netmask": "24", "nic_ip6_cidr": "fd00:1::/64", + "ingress_rules": [{"type": "protocol", "protocol": 47, "cidr": "1.2.3.4/32,2001:db8::/64", "allowed": True}], + "egress_rules": []} + acl = CsAcl.AclDevice(obj, config) + acl.process("ingress", acl.ingress, acl.FIXED_RULES_INGRESS, True) + + # nexthdr is an IPv6 header field, nft rejects it in an ip rule + ip4 = [r['rule'] for r in config.nft_ipv4_acl if r.get('chain') == "eth3_ingress_policy" and 'rule' in r] + self.assertIn("ip saddr 1.2.3.4/32 ip protocol 47 accept", ip4) + ip6 = [r['rule'] for r in config.ipv6_acl if r.get('chain') == "eth3_ingress_policy" and 'rule' in r] + self.assertIn("ip6 saddr 2001:db8::/64 ip6 nexthdr 47 accept", ip6) + + +if __name__ == '__main__': + unittest.main() From af6f502d016f68a0e02b515a3fc9d6aa0824609a Mon Sep 17 00:00:00 2001 From: Brad House Date: Thu, 8 Oct 2026 01:53:31 +0000 Subject: [PATCH 2/3] Cover the egress direction in the routed ACL protocol test Signed-off-by: Brad House --- systemvm/test/TestCsAclRouting.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/systemvm/test/TestCsAclRouting.py b/systemvm/test/TestCsAclRouting.py index cc02cd28685a..775dc570cc53 100644 --- a/systemvm/test/TestCsAclRouting.py +++ b/systemvm/test/TestCsAclRouting.py @@ -48,15 +48,17 @@ def test_protocol_number_rule(self): config = FakeConfig() obj = {"device": "eth3", "nic_ip": "10.1.1.1", "nic_netmask": "24", "nic_ip6_cidr": "fd00:1::/64", "ingress_rules": [{"type": "protocol", "protocol": 47, "cidr": "1.2.3.4/32,2001:db8::/64", "allowed": True}], - "egress_rules": []} + "egress_rules": [{"type": "protocol", "protocol": 47, "cidr": "5.6.7.8/32", "allowed": False}]} acl = CsAcl.AclDevice(obj, config) - acl.process("ingress", acl.ingress, acl.FIXED_RULES_INGRESS, True) + acl.create() # nexthdr is an IPv6 header field, nft rejects it in an ip rule ip4 = [r['rule'] for r in config.nft_ipv4_acl if r.get('chain') == "eth3_ingress_policy" and 'rule' in r] self.assertIn("ip saddr 1.2.3.4/32 ip protocol 47 accept", ip4) ip6 = [r['rule'] for r in config.ipv6_acl if r.get('chain') == "eth3_ingress_policy" and 'rule' in r] self.assertIn("ip6 saddr 2001:db8::/64 ip6 nexthdr 47 accept", ip6) + ip4 = [r['rule'] for r in config.nft_ipv4_acl if r.get('chain') == "eth3_egress_policy" and 'rule' in r] + self.assertIn("ip daddr 5.6.7.8/32 ip protocol 47 drop", ip4) if __name__ == '__main__': From d94d544e6cde316fe5e90fffe6e40d4bc15434ef Mon Sep 17 00:00:00 2001 From: Brad House Date: Thu, 8 Oct 2026 10:31:47 +0000 Subject: [PATCH 3/3] Do not tie the routed ACL test to the form of the IPv6 rule Check only that the IPv4 match does not reach the IPv6 rule. Signed-off-by: Brad House --- systemvm/test/TestCsAclRouting.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/systemvm/test/TestCsAclRouting.py b/systemvm/test/TestCsAclRouting.py index 775dc570cc53..7d67d711fb9e 100644 --- a/systemvm/test/TestCsAclRouting.py +++ b/systemvm/test/TestCsAclRouting.py @@ -56,7 +56,9 @@ def test_protocol_number_rule(self): ip4 = [r['rule'] for r in config.nft_ipv4_acl if r.get('chain') == "eth3_ingress_policy" and 'rule' in r] self.assertIn("ip saddr 1.2.3.4/32 ip protocol 47 accept", ip4) ip6 = [r['rule'] for r in config.ipv6_acl if r.get('chain') == "eth3_ingress_policy" and 'rule' in r] - self.assertIn("ip6 saddr 2001:db8::/64 ip6 nexthdr 47 accept", ip6) + # the IPv4 match must not leak into the IPv6 rule + self.assertTrue(any(r.startswith("ip6 saddr 2001:db8::/64 ") and r.endswith(" 47 accept") for r in ip6)) + self.assertFalse(any("ip protocol" in r for r in ip6)) ip4 = [r['rule'] for r in config.nft_ipv4_acl if r.get('chain') == "eth3_egress_policy" and 'rule' in r] self.assertIn("ip daddr 5.6.7.8/32 ip protocol 47 drop", ip4)