The login works, PWD works, and then LIST hangs until the client gives up or drops into a different data mode. That split is the signature: the control connection to port 21 is healthy, while the data connection is being built from an address or port the far side cannot reach. Behind a VPN gateway that also performs NAT, the usual culprit is an FTP command whose payload names a tunnel address such as 10.8.0.6. Header translation never touches that value.

What follows is where that value comes from, and which box is allowed to change it.

What breaks is the payload, not the tunnel

FTP puts the data endpoint inside the control stream. Active mode has the client send PORT with its own address and port, and the server dials back to that tuple. Passive mode has the server answer 227 with a tuple, and the client dials that instead.

Both forms carry the same kind of statement: connect to this address, on this port. A device that rewrites only IP and TCP headers leaves the statement alone, so one end keeps trying to reach an address that exists only on the other side of a tunnel. The data connection stalls after the control connection has already been declared good.

Three modes, three payloads

The table below shows which side is being told to dial, and what a header-only NAT can do about it.

Read the last column as the thing that fails first. With active mode the server is the one dialing into a private address; with passive mode it is the client; with FTPS the payload is ciphertext and the middlebox has even less to work with.

ModeValue in the control streamWho dials whomHeader-only NAT leaves
Active (PORT)client address and portserver dials the clienta server dialing an unreachable tunnel address
Passive (227)server address and portclient dials the servera client dialing an address it cannot route to
FTPSthe same values, inside TLSas aboveciphertext, so no rewrite happens at all

Put the helper where the translation happens

On Linux the payload rewriting is done by the nf_conntrack_ftp helper. It watches the control connection, learns the port that was negotiated, creates an expectation for the data flow, and on a NAT box rewrites the address and port inside the payload. It only helps on the device that translates the client address. Loading it on the client, or on a router that merely forwards the flow, changes nothing about the session.

These three commands show whether the module is present, whether it is attached, and whether an expectation exists while a session is open:

modprobe nf_conntrack_ftp
lsmod | grep nf_conntrack_ftp
conntrack -L expect | grep ftp

Loading the module is not the same as attaching it. Kernels from 4.7 onward ship with net.netfilter.nf_conntrack_helper=0, so the helper waits for an explicit rule, for example iptables -t raw -A PREROUTING -p tcp --dport 21 -j CT --helper ftp. If the expectation list stays empty while a transfer is attempted, the attachment is what to check, not the module.

Passive mode: tell the server what to advertise

Passive mode is easier to make deterministic because the server can be told what to put in the 227 reply. In vsftpd that is pasv_address, together with pasv_min_port and pasv_max_port; ProFTPD uses MasqueradeAddress and PassivePorts. Set those first, since a correct advertised address removes the need to rewrite anything on that side.

Then let exactly that port range through the gateway, for example -A FORWARD -i tun0 -p tcp --dport 30000:30100 -j ACCEPT, so the rule matches what the server advertises.

The two settings have to agree. A server told to advertise 30000-30100 while the gateway allows 40000-40100 fails in the least helpful way: the control conversation looks healthy and every data connection dies. Compare the numbers before debugging anything else.

Two failure signatures, and what FTPS removes

A hang on LIST points at the first data connection of the session, which is where address rewriting and the expectation both have to work. A hang on RETR after a working LIST points at the transfer itself, meaning the passive range or the tunnel MTU. That second signature is not a payload problem, and helper settings will not fix it.

FTP over TLS removes the payload the helper reads. A middlebox cannot rewrite a PORT or PASV value it cannot decrypt, so the remaining choices are a proxy that terminates TLS, a server that advertises a reachable address within a bounded range, or a different transfer protocol.

A short order of checks

Work outward from the symptom. Confirm that LIST fails while PWD succeeds, capture the address inside the PORT or 227 payload, and ask which device translates that address. Attach the helper there, watch the expectation list during one transfer, then fix passive mode on the server and mirror the range in the gateway rule.

Keep the data-path failure separate from the bulk-transfer failure, because they send you to different settings.

Erthyglow kevrennek