From 35d0eda37379796b988ebfe1a14a0db9aad4b25d Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Wed, 7 Oct 2026 05:08:47 -0400 Subject: [PATCH 1/3] fix passive ftp through the virtual router on newer system vm templates --- systemvm/debian/opt/cloud/bin/cs/CsAddress.py | 3 +++ systemvm/debian/opt/cloud/bin/setup/common.sh | 4 +++- systemvm/debian/opt/cloud/bin/setup/router.sh | 2 +- .../debian/opt/cloud/bin/setup/vpcrouter.sh | 2 +- systemvm/test/TestCsAddress.py | 18 +++++++++++++++++- 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/systemvm/debian/opt/cloud/bin/cs/CsAddress.py b/systemvm/debian/opt/cloud/bin/cs/CsAddress.py index fe95808f7d32..eee1147566a4 100755 --- a/systemvm/debian/opt/cloud/bin/cs/CsAddress.py +++ b/systemvm/debian/opt/cloud/bin/cs/CsAddress.py @@ -460,6 +460,8 @@ def fw_router(self): "-A POSTROUTING " + "-p udp -m udp --dport 68 -j CHECKSUM --checksum-fill"]) + self.fw.append(["raw", "", "-A PREROUTING -p tcp -m tcp --dport 21 -j CT --helper ftp"]) + if self.get_type() in ["public"]: self.fw.append(["mangle", "front", "-A PREROUTING " + @@ -541,6 +543,7 @@ def fw_vpcrouter(self): return self.fw.append(["filter", "", "-A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT"]) + self.fw.append(["raw", "", "-A PREROUTING -p tcp -m tcp --dport 21 -j CT --helper ftp"]) if self.get_type() in ["guest"]: self.fw.append(["mangle", "front", "-A PREROUTING " + diff --git a/systemvm/debian/opt/cloud/bin/setup/common.sh b/systemvm/debian/opt/cloud/bin/setup/common.sh index ef1576ab588c..285aeca06610 100755 --- a/systemvm/debian/opt/cloud/bin/setup/common.sh +++ b/systemvm/debian/opt/cloud/bin/setup/common.sh @@ -164,7 +164,9 @@ enable_fwding() { enable_passive_ftp() { log_it "cloud: enabling passive FTP for guest VMs" - echo "$1" > /proc/sys/net/netfilter/nf_conntrack_helper + # Linux 6.0 removed the nf_conntrack_helper sysctl, the ftp helper is + # attached to port 21 traffic by a raw table rule from CsAddress.py + modprobe nf_nat_ftp } disable_rpfilter() { diff --git a/systemvm/debian/opt/cloud/bin/setup/router.sh b/systemvm/debian/opt/cloud/bin/setup/router.sh index 5c72105f47a3..dc4fb9d0a691 100755 --- a/systemvm/debian/opt/cloud/bin/setup/router.sh +++ b/systemvm/debian/opt/cloud/bin/setup/router.sh @@ -70,7 +70,7 @@ setup_router() { disable_rpfilter_domR enable_fwding 1 enable_rpsrfs 1 - enable_passive_ftp 1 + enable_passive_ftp restore_ipv6 # Only allow DNS service for current network diff --git a/systemvm/debian/opt/cloud/bin/setup/vpcrouter.sh b/systemvm/debian/opt/cloud/bin/setup/vpcrouter.sh index 767f87848dd3..cf88e6cac480 100755 --- a/systemvm/debian/opt/cloud/bin/setup/vpcrouter.sh +++ b/systemvm/debian/opt/cloud/bin/setup/vpcrouter.sh @@ -82,7 +82,7 @@ setup_vpcrouter() { enable_vpc_rpsrfs 1 disable_rpfilter enable_fwding 1 - enable_passive_ftp 1 + enable_passive_ftp cp /etc/iptables/iptables-vpcrouter /etc/iptables/rules.v4 cp /etc/vpcdnsmasq.conf /etc/dnsmasq.conf cp /etc/cloud-nic.rules /etc/udev/rules.d/cloud-nic.rules diff --git a/systemvm/test/TestCsAddress.py b/systemvm/test/TestCsAddress.py index 0ad9ae861b91..658f6f6f046d 100644 --- a/systemvm/test/TestCsAddress.py +++ b/systemvm/test/TestCsAddress.py @@ -16,7 +16,8 @@ # under the License. import unittest -from cs.CsAddress import CsAddress +import mock +from cs.CsAddress import CsAddress, CsIP import merge @@ -38,6 +39,21 @@ def test_get_guest_ip(self): def test_get_guest_netmask(self): self.assertTrue(self.csaddress.get_guest_netmask() == "255.255.255.0") + @mock.patch.object(CsIP, 'list') + def test_ftp_helper_rule(self, _): + ftp_rule = ["raw", "", "-A PREROUTING -p tcp -m tcp --dport 21 -j CT --helper ftp"] + for is_vpc in [False, True]: + config = mock.MagicMock() + config.get_fw.return_value = [] + config.is_vpc.return_value = is_vpc + config.is_routed.return_value = False + config.is_dhcp.return_value = False + ip = CsIP("eth2", config) + ip.setAddress({"nw_type": "public", "public_ip": "10.0.0.2"}) + ip.fw_router() + ip.fw_vpcrouter() + self.assertIn(ftp_rule, ip.fw) + if __name__ == '__main__': unittest.main() From 949c9038faa59cfa4fb6952e2a6849483c37907b Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Wed, 7 Oct 2026 14:23:30 -0400 Subject: [PATCH 2/3] attach the ftp helper when a port forward sends another public port to port 21 --- systemvm/debian/opt/cloud/bin/configure.py | 19 +++++++++ systemvm/test/TestCsForwardingRules.py | 47 ++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 systemvm/test/TestCsForwardingRules.py diff --git a/systemvm/debian/opt/cloud/bin/configure.py b/systemvm/debian/opt/cloud/bin/configure.py index 77b56779d5b8..45e8ca6d1a11 100755 --- a/systemvm/debian/opt/cloud/bin/configure.py +++ b/systemvm/debian/opt/cloud/bin/configure.py @@ -1535,6 +1535,25 @@ def processForwardRule(self, rule): self.forward_vpc(rule) else: self.forward_vr(rule) + self.forward_ftp_helper(rule) + + def forward_ftp_helper(self, rule): + # Linux 6.0 no longer attaches the ftp helper after DNAT, so attach it + # to the public port that is forwarded to port 21. Public port 21 is + # already covered by the rule from CsAddress.py + if rule["protocol"] != "tcp" or "any" in [rule["public_ports"], rule["internal_ports"]]: + return + public_start = int(rule["public_ports"].split(":")[0]) + internal_ports = rule["internal_ports"].split(":") + internal_start = int(internal_ports[0]) + internal_end = int(internal_ports[-1]) + if not internal_start <= 21 <= internal_end: + return + public_port = public_start + 21 - internal_start + if public_port == 21: + return + self.fw.append(["raw", "", "-A PREROUTING -d %s/32 -p tcp -m tcp --dport %s -j CT --helper ftp" % + (rule["public_ip"], public_port)]) def forward_vr(self, rule): # Prefetch iptables variables diff --git a/systemvm/test/TestCsForwardingRules.py b/systemvm/test/TestCsForwardingRules.py new file mode 100644 index 000000000000..378e7fd62fca --- /dev/null +++ b/systemvm/test/TestCsForwardingRules.py @@ -0,0 +1,47 @@ +# 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 CsForwardingRules + + +class TestCsForwardingRules(unittest.TestCase): + + def ftp_helper_rules(self, protocol, public_ports, internal_ports): + forwarding = CsForwardingRules.__new__(CsForwardingRules) + forwarding.fw = [] + forwarding.forward_ftp_helper({"public_ip": "10.0.0.2", "internal_ip": "10.1.1.10", "protocol": protocol, + "public_ports": public_ports, "internal_ports": internal_ports}) + return forwarding.fw + + def test_ftp_helper_on_other_public_port(self): + self.assertEqual(self.ftp_helper_rules("tcp", "2121:2121", "21:21"), + [["raw", "", "-A PREROUTING -d 10.0.0.2/32 -p tcp -m tcp --dport 2121 -j CT --helper ftp"]]) + + def test_ftp_helper_on_port_range(self): + self.assertEqual(self.ftp_helper_rules("tcp", "1020:1030", "20:30"), + [["raw", "", "-A PREROUTING -d 10.0.0.2/32 -p tcp -m tcp --dport 1021 -j CT --helper ftp"]]) + + def test_no_ftp_helper(self): + self.assertEqual(self.ftp_helper_rules("tcp", "21:21", "21:21"), []) + self.assertEqual(self.ftp_helper_rules("tcp", "2121:2121", "2121:2121"), []) + self.assertEqual(self.ftp_helper_rules("udp", "2121:2121", "21:21"), []) + self.assertEqual(self.ftp_helper_rules("tcp", "any", "any"), []) + + +if __name__ == '__main__': + unittest.main() From 47e05d4d521fffecc7cb78ad60e46bd98174d9a5 Mon Sep 17 00:00:00 2001 From: Daman Arora Date: Wed, 7 Oct 2026 14:52:34 -0400 Subject: [PATCH 3/3] only attach the ftp helper on static nat and port forwarded public ips --- systemvm/debian/opt/cloud/bin/configure.py | 6 ++---- systemvm/debian/opt/cloud/bin/cs/CsAddress.py | 3 --- systemvm/debian/opt/cloud/bin/setup/common.sh | 2 +- systemvm/test/TestCsAddress.py | 18 +----------------- systemvm/test/TestCsForwardingRules.py | 18 +++++++++++++++++- 5 files changed, 21 insertions(+), 26 deletions(-) diff --git a/systemvm/debian/opt/cloud/bin/configure.py b/systemvm/debian/opt/cloud/bin/configure.py index 45e8ca6d1a11..19995af9c207 100755 --- a/systemvm/debian/opt/cloud/bin/configure.py +++ b/systemvm/debian/opt/cloud/bin/configure.py @@ -1539,8 +1539,7 @@ def processForwardRule(self, rule): def forward_ftp_helper(self, rule): # Linux 6.0 no longer attaches the ftp helper after DNAT, so attach it - # to the public port that is forwarded to port 21. Public port 21 is - # already covered by the rule from CsAddress.py + # to the public port that is forwarded to port 21 if rule["protocol"] != "tcp" or "any" in [rule["public_ports"], rule["internal_ports"]]: return public_start = int(rule["public_ports"].split(":")[0]) @@ -1550,8 +1549,6 @@ def forward_ftp_helper(self, rule): if not internal_start <= 21 <= internal_end: return public_port = public_start + 21 - internal_start - if public_port == 21: - return self.fw.append(["raw", "", "-A PREROUTING -d %s/32 -p tcp -m tcp --dport %s -j CT --helper ftp" % (rule["public_ip"], public_port)]) @@ -1698,6 +1695,7 @@ def processStaticNatRule(self, rule): self.fw.append(["mangle", "front", "-A %s -d %s -j RETURN" % (chain_name, static_route['network'])]) + self.fw.append(["raw", "", "-A PREROUTING -d %s/32 -p tcp -m tcp --dport 21 -j CT --helper ftp" % rule["public_ip"]]) self.fw.append(["nat", "front", "-A PREROUTING -d %s/32 -j DNAT --to-destination %s" % (rule["public_ip"], rule["internal_ip"])]) self.fw.append(["nat", "front", diff --git a/systemvm/debian/opt/cloud/bin/cs/CsAddress.py b/systemvm/debian/opt/cloud/bin/cs/CsAddress.py index eee1147566a4..fe95808f7d32 100755 --- a/systemvm/debian/opt/cloud/bin/cs/CsAddress.py +++ b/systemvm/debian/opt/cloud/bin/cs/CsAddress.py @@ -460,8 +460,6 @@ def fw_router(self): "-A POSTROUTING " + "-p udp -m udp --dport 68 -j CHECKSUM --checksum-fill"]) - self.fw.append(["raw", "", "-A PREROUTING -p tcp -m tcp --dport 21 -j CT --helper ftp"]) - if self.get_type() in ["public"]: self.fw.append(["mangle", "front", "-A PREROUTING " + @@ -543,7 +541,6 @@ def fw_vpcrouter(self): return self.fw.append(["filter", "", "-A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT"]) - self.fw.append(["raw", "", "-A PREROUTING -p tcp -m tcp --dport 21 -j CT --helper ftp"]) if self.get_type() in ["guest"]: self.fw.append(["mangle", "front", "-A PREROUTING " + diff --git a/systemvm/debian/opt/cloud/bin/setup/common.sh b/systemvm/debian/opt/cloud/bin/setup/common.sh index 285aeca06610..ef610c914079 100755 --- a/systemvm/debian/opt/cloud/bin/setup/common.sh +++ b/systemvm/debian/opt/cloud/bin/setup/common.sh @@ -165,7 +165,7 @@ enable_fwding() { enable_passive_ftp() { log_it "cloud: enabling passive FTP for guest VMs" # Linux 6.0 removed the nf_conntrack_helper sysctl, the ftp helper is - # attached to port 21 traffic by a raw table rule from CsAddress.py + # attached by raw table rules for static nat and port forwarding modprobe nf_nat_ftp } diff --git a/systemvm/test/TestCsAddress.py b/systemvm/test/TestCsAddress.py index 658f6f6f046d..0ad9ae861b91 100644 --- a/systemvm/test/TestCsAddress.py +++ b/systemvm/test/TestCsAddress.py @@ -16,8 +16,7 @@ # under the License. import unittest -import mock -from cs.CsAddress import CsAddress, CsIP +from cs.CsAddress import CsAddress import merge @@ -39,21 +38,6 @@ def test_get_guest_ip(self): def test_get_guest_netmask(self): self.assertTrue(self.csaddress.get_guest_netmask() == "255.255.255.0") - @mock.patch.object(CsIP, 'list') - def test_ftp_helper_rule(self, _): - ftp_rule = ["raw", "", "-A PREROUTING -p tcp -m tcp --dport 21 -j CT --helper ftp"] - for is_vpc in [False, True]: - config = mock.MagicMock() - config.get_fw.return_value = [] - config.is_vpc.return_value = is_vpc - config.is_routed.return_value = False - config.is_dhcp.return_value = False - ip = CsIP("eth2", config) - ip.setAddress({"nw_type": "public", "public_ip": "10.0.0.2"}) - ip.fw_router() - ip.fw_vpcrouter() - self.assertIn(ftp_rule, ip.fw) - if __name__ == '__main__': unittest.main() diff --git a/systemvm/test/TestCsForwardingRules.py b/systemvm/test/TestCsForwardingRules.py index 378e7fd62fca..50ec370ea9d4 100644 --- a/systemvm/test/TestCsForwardingRules.py +++ b/systemvm/test/TestCsForwardingRules.py @@ -16,6 +16,7 @@ # under the License. import unittest +import mock from configure import CsForwardingRules @@ -36,12 +37,27 @@ def test_ftp_helper_on_port_range(self): self.assertEqual(self.ftp_helper_rules("tcp", "1020:1030", "20:30"), [["raw", "", "-A PREROUTING -d 10.0.0.2/32 -p tcp -m tcp --dport 1021 -j CT --helper ftp"]]) + def test_ftp_helper_on_port_21(self): + self.assertEqual(self.ftp_helper_rules("tcp", "21:21", "21:21"), + [["raw", "", "-A PREROUTING -d 10.0.0.2/32 -p tcp -m tcp --dport 21 -j CT --helper ftp"]]) + def test_no_ftp_helper(self): - self.assertEqual(self.ftp_helper_rules("tcp", "21:21", "21:21"), []) self.assertEqual(self.ftp_helper_rules("tcp", "2121:2121", "2121:2121"), []) self.assertEqual(self.ftp_helper_rules("udp", "2121:2121", "21:21"), []) self.assertEqual(self.ftp_helper_rules("tcp", "any", "any"), []) + @mock.patch.object(CsForwardingRules, 'getStaticRoutes', return_value=[]) + @mock.patch.object(CsForwardingRules, 'getPrivateGatewayNetworks', return_value=[]) + @mock.patch.object(CsForwardingRules, 'getGuestIpByIp', return_value="10.1.1.1") + @mock.patch.object(CsForwardingRules, 'getNetworkByIp', return_value="10.1.1.0/24") + @mock.patch.object(CsForwardingRules, 'getDeviceByIp', return_value="eth2") + def test_ftp_helper_on_static_nat(self, *_): + forwarding = CsForwardingRules.__new__(CsForwardingRules) + forwarding.fw = [] + forwarding.processStaticNatRule({"public_ip": "10.0.0.2", "internal_ip": "10.1.1.10"}) + self.assertIn(["raw", "", "-A PREROUTING -d 10.0.0.2/32 -p tcp -m tcp --dport 21 -j CT --helper ftp"], + forwarding.fw) + if __name__ == '__main__': unittest.main()