You can use FTP in one of two ways: passive or active. Generally, the choice of active or passive is made to determine who has the problem with firewalling. Realistically, you will have to support both to have happy users.
With active FTP, when a user connects to a remote FTP server and requests information or a file, the FTP server makes a new connection back to the client to transfer the requested data. This is called the data connection. To start, the FTP client chooses a random port to receive the data connection. The client sends the port number it chose to the FTP server and listens for an incoming connection on that port. The FTP server then initiates a connection to the client's address at the chosen port and transfers the data. This is a problem for users attempting to gain access to FTP servers from behind a NAT gateway. Because of how NAT works, the FTP server initiates the data connection by connecting to the external address of the NAT gateway on the chosen port. The NAT machine will receive this, but because it has no mapping for the packet in its state table, it will drop the packet and won't deliver it to the client.
With passive mode FTP (the default mode with OpenBSD's ftp(1) client), the client requests that the server pick a random port to listen on for the data connection. The server informs the client of the port it has chosen, and the client connects to this port to transfer the data. Unfortunately, this is not always possible or desirable because of the possibility of a firewall in front of the FTP server blocking the incoming data connection. OpenBSD's ftp(1) uses passive mode by default. To force active mode FTP, use the -A flag to ftp, or set passive mode to "off" by issuing the command "passive off" at the "ftp>" prompt.
Packet Filter provides a solution for this situation by diverting FTP traffic through an FTP proxy server. This process acts to "guide" your FTP traffic through the NAT gateway/firewall, by actively adding needed rules to PF system and removing them when done, by means of the PF anchors system. The FTP proxy used by PF is ftp-proxy(8).
To activate it, put something like this early in the rules section of pf.conf:
This diverts FTP from your clients to the ftp-proxy(8) program, which is listening on your machine to port 8021.pass in quick on $int_if inet proto tcp to port 21 divert-to 127.0.0.1 port 8021
You also need an anchor in the rules section:
The proxy server has to be started and running on the OpenBSD box.anchor "ftp-proxy/*"
The ftp-proxy utility listens on port 8021, the same port the above divert-to statement is sending FTP traffic to.# rcctl enable ftpproxy # rcctl start ftpproxy
To support active mode connections from certain (fussy) clients, you may need the -r switch on ftp-proxy(8).
You can tighten up that range of ports considerably if you want to. In the case of ftpd(8), that is done using the sysctl(8) variables net.inet.ip.porthifirst and net.inet.ip.porthilast.pass in on egress proto tcp to port 21 pass in on egress proto tcp to port > 49151
ftp-proxy(8) can be run in a mode that causes it to forward all FTP connections to a specific FTP server. Basically, we'll set up the proxy to listen on port 21 of the firewall and forward all connections to the backend server.
Here 10.10.10.1 is the IP address of the actual FTP server, 21 is the port we want ftp-proxy(8) to listen on, and 192.168.0.1 is the address on the firewall that we want the proxy to bind to.# rcctl set ftpproxy flags -R 10.10.10.1 -p 21 -b 192.168.0.1
Now for the pf.conf rules:
Here we allow the connection inbound to port 21 on the external interface, as well as the corresponding outbound connection to the FTP server. The "user _ftp_proxy" addition to the outbound rule ensures that only connections initiated by ftp-proxy(8) are permitted.ext_ip = "192.168.0.1" ftp_ip = "10.10.10.1" match out on egress inet from $int_if nat-to (egress) anchor "ftp-proxy/*" pass in on egress inet proto tcp to $ext_ip port 21 pass out on $int_if inet proto tcp to $ftp_ip port 21 user _ftp_proxy
Note that if you want to run ftp-proxy(8) to protect an FTP server as well as allow clients to FTP out from behind the firewall, two instances of ftp-proxy will be required.
tftp-proxy(8) is setup in much the same way as ftp-proxy(8) was in the FTP client behind the firewall section above.
The rules above allow TFTP outbound from the internal network to TFTP servers on the external network.match out on egress inet from $int_if nat-to (egress) anchor "tftp-proxy/*" pass in quick on $int_if inet proto udp from $int_if to port tftp \ divert-to 127.0.0.1 port 6969 anchor "tftp-proxy/*"
The last step is to enable tftp-proxy in inetd.conf(5) so that it listens on the same port that the divert-to rule specified above, in this case 6969.
Unlike ftp-proxy(8), tftp-proxy(8) is spawned from inetd.127.0.0.1:6969 dgram udp wait root /usr/libexec/tftp-proxy tftp-proxy