Should be simple, I even have the Book of PF 2nd edition and examples of how to do it on Google from both 4.7 and 4.8. I just can't get mine to work though. I'm running 4.9.

Here is my pfctl -sr

Code:

block drop all
pass in quick on vic0 inet proto tcp from any to 10.220.100.0/24 port = 1022 flags S/SA keep state label "PassInMGMTSSH"
pass in quick on vic0 inet proto tcp from any to 10.220.100.0/24 port = ssh flags S/SA keep state label "PassInMGMTSSH"
pass out quick on vic0 inet proto tcp from 10.220.100.0/24 to any port = 1022 flags S/SA keep state label "PassOutMGMTSSH"
pass out quick on vic0 inet proto tcp from 10.220.100.0/24 to any port = ssh flags S/SA keep state label "PassOutMGMTSSH"
pass on vic0 proto udp from any to any port = domain keep state label "PassMGMTDNS"
pass on vic0 inet proto icmp all icmp-type echorep keep state label "PassMGMTICMP"
pass on vic0 inet proto icmp all icmp-type echoreq keep state label "PassMGMTICMP"
pass on vic0 inet proto icmp all icmp-type unreach keep state label "PassMGMTICMP"
pass quick on vic2 proto carp all keep state label "CUST-PassCarp"
pass quick on vic3 proto carp all keep state label "CUST-PassCarp"
pass in on vic2 inet proto icmp from any to XX.XX.XX.0/24 icmp-type echoreq keep state label "CUST-PingOut"
pass in on vic2 inet proto icmp from any to XX.XX.XX.0/24 icmp-type echorep keep state label "CUST-PingOut"
pass in on vic2 inet proto icmp from any to XX.XX.XX.0/24 icmp-type unreach keep state label "CUST-PingOut"
pass in on vic3 inet proto icmp from 10.221.181.0/24 to 10.221.181.10 icmp-type echoreq keep state label "CUST-PingIn"
pass in on vic3 inet proto icmp from 10.221.181.0/24 to 10.221.181.10 icmp-type echorep keep state label "CUST-PingIn"
pass in on vic3 inet proto icmp from 10.221.181.0/24 to 10.221.181.10 icmp-type unreach keep state label "CUST-PingIn"
match out on vic2 inet from 10.221.181.10 to any label "CUST-NATOut" nat-to (vic2) round-robin
match in on vic2 proto tcp from any to any port = smtp label "CUST-RDRFrontPool" rdr-to <CUST_FrontPool> round-robin
match in on vic2 proto tcp from any to any port = www label "CUST-RDRFrontPool" rdr-to <CUST_FrontPool> round-robin
match in on vic2 proto tcp from any to any port = ssh label "CUST-RDRFrontPool" rdr-to <CUST_FrontPool> round-robin
match in on vic2 inet proto tcp from any to any port = 5222 label "CUST-RDRBusinessPool" rdr-to 10.221.182.31 port 5222
pass in on vic2 inet proto tcp from any to 10.221.181.21 port = smtp flags S/SA keep state label "CUST-PassInFront"
pass in on vic2 inet proto tcp from any to 10.221.181.21 port = www flags S/SA keep state label "CUST-PassInFront"
pass in on vic2 inet proto tcp from any to 10.221.181.21 port = ssh flags S/SA keep state label "CUST-PassInFront"
pass in on vic2 inet proto tcp from any to 10.221.181.22 port = smtp flags S/SA keep state label "CUST-PassInFront"
pass in on vic2 inet proto tcp from any to 10.221.181.22 port = www flags S/SA keep state label "CUST-PassInFront"
pass in on vic2 inet proto tcp from any to 10.221.181.22 port = ssh flags S/SA keep state label "CUST-PassInFront"
pass in on vic2 inet proto tcp from any to 10.221.182.31 port = 5222 flags S/SA keep state label "CUST-PassInBusiness"
pass in on vic2 inet proto tcp from any to 10.221.182.32 port = 5222 flags S/SA keep state label "CUST-PassInBusiness"
anchor "ftp-proxy/*" all
pass in on vic3 inet proto tcp from any to any port = ftp flags S/SA keep state label "CUST-PassInRDRFTP" rdr-to 127.0.0.1 port 8021
pass out on vic2 proto tcp from any to any port = ftp flags S/SA keep state label "CUST-PassOutFTP"
pass on vic3 all flags S/SA keep state
pass on vic4 all flags S/SA keep state
pass out on vic2 all flags S/SA keep state

I've replaced the public IP but all of this works fine sans the FTP related rules.

Tcpdump shows packets coming in from the localnet on vic3 but even though I have set skip on lo0 I see no packets at all on that interface. Ftp-proxy listens with no special arguments on localhost:8021.

I can use ftp from the gateway using the standard ftp client, logged in over ssh that is.

What are you trying to achieve? Allow clients from the internal net to use ftp servers on the internet? Or allow external clients to access a ftp-server in a DMZ?

You have 4 interfaces: vic0, vic2, vic3 and vic4. vic2 and 3 are used for carp. Sometimes but now always carp is used for fail-over. What is the external interface and which one is the internal one?
A short description/network diagram would be helpful

__________________
You don't need to be a genius to debug a pf.conf firewall ruleset, you just need the guts to run tcpdump

What are you trying to achieve? Allow clients from the internal net to use ftp servers on the internet? Or allow external clients to access a ftp-server in a DMZ?

You have 4 interfaces: vic0, vic2, vic3 and vic4. vic2 and 3 are used for carp. Sometimes but now always carp is used for fail-over. What is the external interface and which one is the internal one?
A short description/network diagram would be helpful

Nevermind, thanks to Norman on the misc-list I was told that my NAT rule was still wrong and if I allowed it from all I would have more luck translating from localhost to my external vic2 interface. So now it works.

If anyone wants to use my rules as a reference then know that you should not match out on $ExtIF from $IntIF:network but instead match out on $ExtIF from inet all nat-to ($ExtIF) so NAT can be done from all addresses on your system out through your external one.