Repository navigation
Virtual Router: passive FTP to guest VMs is broken on Debian 12 based systemVM templates (FTP conntrack helper is never attached) #14319
Description
Problem
The VR tries to turn on automatic conntrack helper assignment at boot:
systemvm/debian/opt/cloud/bin/setup/common.sh→enable_passive_ftp():
echo "$1" > /proc/sys/net/netfilter/nf_conntrack_helper- called as
enable_passive_ftp 1fromrouter.shandvpcrouter.sh
Linux 6.0 removed both the nf_conntrack_helper sysctl and the nf_conntrack.nf_conntrack_helper module parameter.
On the Debian 12 based VR (kernel 6.1):
/proc/sys/net/netfilter/nf_conntrack_helperdoes not exist, so theechofails on every boot- the FTP helper modules (
nf_conntrack_ftp/nf_nat_ftp) are not loaded - nothing else attaches the helper: there are no
-j CT --helper ftprules in the raw table
So the FTP helper is never attached to FTP control connections that go through the VR.
As a result:
- The PASV reply (
227 Entering Passive Mode (a,b,c,d,p1,p2)) is not rewritten.
The guest VM's private IP is sent to the client. - The passive data connection is not tracked as
RELATED.
With port forwarding, it is not DNATed to the VM.
With both port forwarding and static NAT, it is dropped by theFIREWALL_<public-ip>chain, which only acceptsRELATED,ESTABLISHEDtraffic and explicitly allowed ports.
Only the control port (21) is reachable.
This worked on older systemVM templates (kernel < 6.0), where the sysctl still existed.
PASV clients are affected:
- clients that honour the IP in the 227 reply connect to the guest's private IP, which can't be reached (point 1)
- clients that replace a private PASV IP with the server's address are blocked by the firewall (point 2).
This is the default in FileZilla, WinSCP, curl (since 7.74,--ftp-skip-pasv-ip) and Pythonftplib.
Versions
4.20.0.0 and later (Debian 12 systemVM template, kernel 6.1), also present on main.
4.19 and earlier (Debian 11 systemVM template, kernel 5.10) are not affected.
Steps to reproduce
-
Create an isolated network and deploy a guest VM.
-
Inside the VM, install vsftpd:
- enable anonymous login and anonymous upload
(anonymous_enable=YES,write_enable=YES,anon_upload_enable=YES) - create an
uploadfolder writable by the anonymous user
- enable anonymous login and anonymous upload
-
Acquire a public IP and enable static NAT to the VM.
-
Add a firewall rule on the public IP allowing TCP port 21 only.
-
From an external Ubuntu 24.04 VM, connect with the stock
ftpclient.
This works. The client uses EPSV, and the229reply carries only a port, no IP, so point 1 does not apply:# ftp 10.0.57.6 Connected to 10.0.57.6. 220 (vsFTPd 3.0.5) Name (10.0.57.6:user): anonymous 331 Please specify the password. Password: 230 Login successful. Remote system type is UNIX. Using binary mode to transfer files. ftp> cd upload 250 Directory successfully changed. ftp> put README.md local: README.md remote: README.md 229 Entering Extended Passive Mode (|||26040|) 150 Ok to send data. 100% |*************************************************| 12357 206.74 MiB/s 00:00 ETA 226 Transfer complete. 12357 bytes sent in 00:00 (105.30 KiB/s) -
Simulate a PASV client that uses the IP from the 227 reply (point 1):
curl -v --disable-epsv --no-ftp-skip-pasv-ip --list-only ftp://anonymous:x@10.0.57.6:21 -
Simulate a PASV client that replaces the PASV IP with the server's address, like FileZilla (point 2):
curl -v --disable-epsv --list-only ftp://anonymous:x@10.0.57.6:21
Expected results
The 227 reply contains the public IP, and the directory listing is returned.
Actual results
The 227 reply contains the guest VM's private IP. The data connection fails and the listing is never returned.
On the VR:
# ls /proc/sys/net/netfilter/nf_conntrack_helper
ls: cannot access '/proc/sys/net/netfilter/nf_conntrack_helper': No such file or directory
# lsmod | grep -E 'nf_(conntrack|nat)_ftp'
(no output)
# iptables -t raw -S
-P PREROUTING ACCEPT
-P OUTPUT ACCEPT
Workarounds
Workaround 1: attach the FTP helper explicitly on the VR (verified)
On the VR, add a raw-table rule for the public IP and the public FTP port:
iptables -t raw -A PREROUTING -d <public-ip> -p tcp --dport 21 -j CT --helper ftp
With this rule in place, the curl command from step 6 succeeds.
The 227 reply is rewritten to the public IP, and the data connection is allowed as RELATED.
No passive port range needs to be opened in the firewall.
Notes:
- The raw table is processed before DNAT, so
--dportmust be the public port.
With static NAT, or port forwarding from public 21 to private 21, this is21.
With port forwarding from a different public port (e.g. public 2121 → private 21), use--dport 2121. - The rule is not persistent. It is lost when the VR is restarted or recreated, and must be re-applied after that.
- To remove it:
iptables -t raw -D PREROUTING -d <public-ip> -p tcp --dport 21 -j CT --helper ftp
Workaround 2: advertise the public IP and a fixed passive port range from the FTP server (verified, no VR change)
In the guest VM, set the following in /etc/vsftpd.conf:
listen=YES
listen_ipv6=NO
pasv_enable=YES
pasv_address=<public-ip>
pasv_min_port=60001
pasv_max_port=60020
Restart vsftpd and confirm it is listening on IPv4 (0.0.0.0:21):
systemctl restart vsftpd && ss -ltnp | grep ':21 '
Then, in CloudStack, add a firewall rule on the public IP for TCP ports 60001–60020, in addition to port 21.
With port forwarding instead of static NAT, also add a port-forwarding rule for 60001–60020 to the same private ports.
With this, the curl command from step 6 succeeds.
The 227 reply contains the public IP and a port in 60001–60020.
Notes:
listen=YES/listen_ipv6=NOis required.
With Ubuntu's default (listen=NO,listen_ipv6=YES), vsftpd ignorespasv_addressand sends227 Entering Passive Mode (0,0,0,0,...).- This workaround is persistent and does not depend on the VR.
However, the passive port range must be exposed in the firewall, and the public IP is hard-coded in the server config. - Clients inside the guest network also receive the public IP in the 227 reply, so they rely on hairpin NAT.
Suggested fix
Replace the removed sysctl with explicit helper assignment (the approach the kernel developers recommend), generated by the VR scripts:
- Static NAT / port forwarding with private port 21:
add-t raw -A PREROUTING -d <public-ip> -p tcp --dport <public-port> -j CT --helper ftp.
Since the raw table runs before DNAT, port forwarding needs the public port. - Outbound active FTP from guest VMs through source NAT relied on the same automatic assignment and is likely affected too.
This would need e.g.-t raw -A PREROUTING -i <guest-if> -p tcp --dport 21 -j CT --helper ftp. - Remove or replace the no-op
enable_passive_ftp()incommon.sh.
Activity