FTP passive mode: why your firewall breaks it
FTP logs in fine and then the directory listing hangs. The data channel needs its own ports. Here is the passive range to declare, and the firewall rule.
Why FTP logs in but the directory listing hangs
FTP passive mode breaks on a firewall because FTP uses two TCP connections, not one. The connection to port 21 carries the login and the commands, so the username and password are accepted and the firewall looks correct. The first ls then needs a second connection on a different port. Nothing in the firewall allows that one, so the client sits there until it times out.
The fix is a fixed range of ports for those data connections, plus a firewall rule that allows the same range. A server behind NAT (network address translation) needs one more setting so it advertises the right address. Connection tracking helpers used to do this work for you. They do not any more, and the reason is worth knowing before you copy an old guide.
The control channel and the data channel
FTP (file transfer protocol) is specified in RFC 959 and predates both NAT and the stateful firewall. A session opens one control connection to TCP port 21 and keeps it open for the whole session. Commands go up as plain text. Replies come back as a three digit code and a line of text. That connection never carries file content.
Every piece of data gets its own TCP connection: one for a directory listing (LIST), one for each download (RETR), one for each upload (STOR). It is opened, used once, and closed. Authentication happens entirely on the control channel, so a broken data path always looks the same: a successful login, then a hang. If the client prints a 230 reply and then stops on the listing, you are looking at the data channel and not at credentials.
Active mode: the server connects back to the client
In active mode the client picks a port, listens on it, and tells the server where to knock:
PORT 192,168,1,50,195,80The first four numbers are the client's IP address. The last two are the port, encoded as two bytes: 195 * 256 + 80 = 50000. The server then opens the data connection from its own port 20 to port 50000 on the client.
That connection is inbound and unsolicited from the client's point of view, so a client firewall drops it. If the client sits behind a home router, the address in the PORT command is a private one the server cannot reach at all. Active mode is where FTP earned its reputation for not working.
Passive mode: the client opens both connections
Passive mode reverses the data connection. The client sends PASV and the server answers with an address and a port of its own:
227 Entering Passive Mode (203,0,113,10,195,80)Same encoding, so the client connects to 203.0.113.10 on port 50000. The client now opens both connections, which is why passive mode survives client side NAT and why every current client asks for it first.
The problem moved rather than disappeared. The unsolicited inbound connection now lands on your server, on a high port that changes with every transfer. That firewall is yours, so now the problem is yours.
EPSV (extended passive mode, RFC 2428) is the same idea with a cleaner reply:
229 Entering Extended Passive Mode (|||50000|)There is no address in it. The client reuses the address it already has for the control connection, which is what makes it work over IPv6 and removes one whole class of NAT bug. curl's manual states that curl normally tries EPSV before PASV. The port is still chosen at runtime, so EPSV changes nothing about your firewall rules.
Why a normal firewall rule cannot allow the data channel
Because the port number does not exist yet when you write the rule. The server picks it per transfer. Left at its defaults, vsftpd documents pasv_min_port and pasv_max_port as 0, meaning "use any port", so the data connection can arrive on anything above 1023. sudo ufw allow 21/tcp allows the control channel and nothing else, and that is exactly the configuration that produces a working login and a dead listing. If the idea of a service listening on one fixed port is still fuzzy, how ports and listening sockets work on Linux is the background for this.
A stateful firewall does track connections, and the kernel can admit a new connection as RELATED to an existing one. For FTP that requires something to read the control stream and pull the port out of a 227 or PORT line. Nothing does that by default.
Why the FTP connection tracking helper is not the answer any more
The kernel module nf_conntrack_ftp is what older guides point you at. It reads the plaintext control channel, finds the announced port, and registers an expectation, so the data connection is admitted without any rule naming its port. Four things have changed since those guides were written.
Automatic helper assignment is off. The kernel documents the nf_conntrack_helper sysctl as "0 - disabled (default)" and adds: "If disabled it is required to set up iptables rules to assign helpers to connections." Loading the module achieves nothing on its own.
On current kernels the switch is gone. Run sysctl net.netfilter.nf_conntrack_helper. A reply of sysctl: cannot stat /proc/sys/net/netfilter/nf_conntrack_helper: No such file or directory means the kernel has no automatic helper assignment left to turn on. If you get a number back instead, the switch is still present and its default is 0.
The firewall front ends have deprecated it too. man ufw-framework on Ubuntu 24.04 says of the IPT_MODULES line in /etc/default/ufw: "Unconditional loading of connection tracking modules (nf_conntrack_*) in this manner is deprecated", and adds that helper rules "must be managed in via the RULES FILES". firewalld documents AutomaticHelpers in firewalld.conf as "Deprecated. This option is ignored and no longer used." Attaching a helper now means writing an explicit rule with the CT target by hand, which is more work than the fix below and stops working the moment you enable TLS. iptables and nftables on Ubuntu covers where those rules actually live.
TLS ends the argument. A helper works by reading the control channel as text. Encrypt that channel and the helper sees ciphertext, so it cannot find the port. There is no fix for that, and there should not be: a middlebox able to read your control channel is a middlebox that read your password.
Declare a passive port range on the server
Every FTP server can be told to choose its passive ports from a range you pick. The option names differ, so check the documentation for the server you actually run.
vsftpd, in /etc/vsftpd.conf:
pasv_enable=YES
pasv_min_port=30000
pasv_max_port=30099pasv_enable already defaults to YES. The two port options default to 0, the "use any port" behaviour described above. Apply with sudo systemctl restart vsftpd, then confirm the service came back with systemctl status vsftpd. vsftpd refuses to start on a configuration line it cannot parse rather than ignoring it, so if the restart fails, read journalctl -u vsftpd -n 20 for the 500 OOPS: line naming the option you just typed.
ProFTPD, in proftpd.conf:
PassivePorts 30000 30099ProFTPD documents no default here: without the directive the kernel picks the port. Its documentation also states that when no port in your range is free, the server falls back to a kernel assigned port and logs a message. A range that is too small therefore fails now and then instead of failing cleanly, which is far harder to diagnose. Use non privileged ports, 1024 and above.
Pure-FTPd takes a flag, -p first:last, documented in man pure-ftpd as "Use only ports in the range first to last inclusive for passive-mode downloads", which "makes pure-ftpd more compatible with packet filters". Packaged builds usually wrap that flag in a config file, so check your distribution's own docs for the file name rather than guessing.
How many ports do you need? One per data connection in flight. A closed TCP port stays in TIME_WAIT for a couple of minutes before it can be reused, so allow several times your expected peak. A hundred ports is comfortable for a few users, and a busy public server needs far more.
Where should the range sit? Run sysctl net.ipv4.ip_local_port_range first. On a stock Ubuntu box it reads 32768 60999, the ports the kernel hands out for outgoing connections. A passive range inside that window can collide with an outbound connection that already holds the port, so keep the range below it. 30000 to 30099 is clear on a default box. Check your own box rather than trusting that number.
Open the same range in the firewall
ufw writes a range with a colon, and its manual states that a range or a list "may also be used to specify multiple ports, in which case the protocol is required":
sudo ufw allow 21/tcp
sudo ufw allow 30000:30099/tcp
sudo ufw status verboseufw status verbose should now list both entries. The same command without /tcp is rejected with an error telling you to name tcp or udp, because ufw will not guess. ufw rule syntax on a VPS covers the rest.
firewalld writes the range with a hyphen and needs a reload:
sudo firewall-cmd --permanent --add-service=ftp
sudo firewall-cmd --permanent --add-port=30000-30099/tcp
sudo firewall-cmd --reload
sudo firewall-cmd --list-all--add-service=ftp opens 21/tcp and requests the ftp helper, which the shipped service definition names. It does not open your passive range, so on its own it leaves you exactly where you started. firewalld zones and services on a VPS has the wider picture.
Straight nftables, inside your input chain:
tcp dport { 21, 30000-30099 } acceptOne more firewall to remember: most providers run a network firewall in the control panel, outside the operating system. If the rules on the box look right and packets still never arrive, open the same range there as well.
Tell the server its public address when it is behind NAT
Run ip -4 addr show on the server. If the address on the interface is the one clients connect to, skip this section. If the interface holds a private address (10.x, 172.16 to 172.31.x, 192.168.x) and the platform maps a public address onto it, the server does not know its own public address. vsftpd documents the default for pasv_address as "the address is taken from the incoming connected socket", so the 227 reply carries the private address and the client is sent somewhere it cannot reach.
FileZilla names this one exactly:
Server sent passive reply with unroutable address. Using server address instead.FileZilla repairs it and carries on. Many other clients do not. They dial 10.0.0.5 and hang.
curl hides the problem too, which matters if curl is your test. Its manual says --ftp-skip-pasv-ip "is enabled by default (added in 7.74.0)", so curl ignores the address in the 227 reply and reuses the control connection's address. A transfer that works with curl can still fail in a graphical client for this one reason.
Set the address explicitly. vsftpd takes pasv_address=203.0.113.10, plus pasv_addr_resolve=YES (default NO) if you would rather write a hostname. ProFTPD takes MasqueradeAddress, which accepts an address, a DNS name, or an interface name. Pure-FTPd takes -P, documented for the case where "the server is behind a masquerading (NAT) box". EPSV avoids the whole issue because its reply has no address field, but you cannot depend on that, since the client decides which command to send.
What TLS changes
FTPS is FTP over TLS (transport layer security). The client connects to port 21 as usual, sends AUTH TLS to secure the control channel, then sends PROT P to encrypt the data channel as well. Plain FTP puts the password on the wire as readable text, so if you have to keep FTP, run FTPS. vsftpd ships with ssl_enable defaulting to NO.
Two things follow. No connection tracking helper can work, which is the point above seen from the other side. And sudo tcpdump -nAi any 'tcp port 21' will no longer show you the 227 reply, so when you need to know which address and port the server advertised, read the server's own log instead of the wire.
Testing the change from outside
Run these from another machine. Testing from the server itself skips the firewall you are trying to fix.
sudo ss -ltnp | grep :21
curl -v --disable-epsv --user ftpuser:secret ftp://example.com/
nc -vz example.com 30000ss should show the FTP daemon listening on port 21. Nothing listens on the passive range while the server is idle, because those sockets are created for a transfer and closed after it.
--disable-epsv forces curl down the PASV path, which is the one that exposes the address problem. The trace prints the server's reply, then the address and port curl dials:
< 227 Entering Passive Mode (203,0,113,10,117,52)117 * 256 + 52 = 30004, inside the declared range. A private address on that line means pasv_address is unset. A port outside your range means the server never read the configuration change, so check that you edited the file the running service uses.
nc answers the firewall question on its own. An immediate Connection refused means the packet reached the server and found nothing listening, which is the correct result for an idle passive port: your rule works. A hang until nc gives up means something dropped the packet silently, which is a firewall, either the one on the box or the one in your provider's panel. That distinction is the same one described in refused versus timed out for SSH, and it applies to every port.
Should you still be running FTP?
For new work, no. SFTP (SSH file transfer protocol) runs inside a single SSH connection on port 22. There is no second channel, no passive range, no NAT setting, and no extra daemon to secure, because OpenSSH already provides it. sftp user@example.com works on a box where you never configured a file transfer service at all. To give somebody files and nothing else, sshd_config takes ForceCommand internal-sftp together with ChrootDirectory. That directory must be owned by root and must not be writable by the user, or sshd refuses the session and logs a bad ownership or modes for chroot directory line.
FTP still earns its place when the other end cannot change. Scanners and multifunction printers ship firmware that speaks FTP and nothing else. Lab and industrial equipment often runs a fixed image nobody will recertify. Business partners publish an FTPS drop and will not add a protocol for one supplier. In each of those cases the passive range plus the matching firewall rule is the whole job, and FTPS rather than plain FTP is the version to run. The two channel design is a decision from 1985 that now runs in a world it never anticipated, a story covered in the history of file transfer protocols.
FAQ
Why does FTP log in but the directory listing hang?
Logging in uses only the control connection on port 21, which your firewall allows. The directory listing needs a second TCP connection on a different port, and that connection is blocked. Declare a passive port range on the FTP server, open the same range in the firewall, and the listing completes. A hanging listing is a data channel problem, never a password problem.
Which ports do I need to open for FTP passive mode?
Port 21 for the control channel, plus whatever range you configured for passive data connections. There is no standard range, because you choose it. Something like 30000 to 30099 works: size it to your peak number of simultaneous transfers, and keep it clear of the kernel's outgoing port range, which you can read with sysctl net.ipv4.ip_local_port_range. If your provider runs a network firewall in its control panel, open the same range there too.
Do I still need nf_conntrack_ftp?
No, and on a current kernel you cannot rely on it. Automatic helper assignment is disabled by default, and on recent kernels the net.netfilter.nf_conntrack_helper switch has been removed, so sysctl reports that the file does not exist. ufw's manual calls unconditional loading of those modules deprecated, and firewalld ignores AutomaticHelpers completely. A helper also has to read the control channel as plain text, so it stops working the moment you enable FTPS. Declare a passive port range instead.
Why does my FTP client say the passive reply has an unroutable address?
The server answered PASV with the address it sees on its own interface, and that address is private. This happens when the platform maps a public address onto a private one. Set the public address explicitly: pasv_address in vsftpd, MasqueradeAddress in ProFTPD, or -P in Pure-FTPd. FileZilla works around it by reusing the address it already connected to, and logs "Using server address instead", which is why some clients survive the misconfiguration and others hang.
Should I use FTPS or SFTP?
SFTP for anything you control at both ends: one connection over SSH on port 22, no data channel to open, and it is running already. FTPS is FTP over TLS, so it keeps the two channel design and every firewall problem that comes with it. Choose it when the other side speaks nothing else. Do not use plain FTP across the internet, because the password crosses the network as readable text.