Ok, so here's my working final ruleset. Unfortunately, I wasn't able to get port redirection working (port forwarding is working fine). As a benefit, pf seems to be much more efficient, I get double the LAN <-> LAN and WAN <-> LAN traffic throughput/bandwidth vs old IPFW and NATd - its actually pretty amazing. So here's the sanitized ruleset:
# /etc/pf.anchors/org.example.my.pf.conf
# Version 2014-11-07
##########
# Macros - User-defined variables may be defined and used later, simplifying the configuration file.
# Macros must be defined before they are referenced in pf.conf.
##########
# don't nest macros as it can lead to invalid ruleset
# external interface (WAN)
ext_if = "en4"
# internal interface (LAN)
int_if = "en0"
# static IP for external interface, from ISP
ext_ip = "xxx.xxx.xxx.xxx"
# LAN IP address for email and web server
email_web_server_ip = "xxx.xxx.xxx.xxx"
# tcp ports to open for dns and alt ssh for firewall_dns_server
firewall_dns_server_services = "{ 53, xxxxx }"
# tcp ports to open for email and web traffic for email_web_server
email_web_server_services = "{ 25, 80, 587, 995, xxxxx }"
# udp ports to open for dns traffic on firewall_dns_server (dns server)
udp_services = "{ 53 }"
# icmp types to allow echo reply, destination unreachable, echo, time exceeded
icmp_types= "{ 0, 3, 8, 11 }"
##########
# Tables - Tables provide a mechanism for increasing the performance and
# flexibility of rules with large numbers of source or destination addresses.
##########
# emerging threats, ips for the firewall to block
table <emerging_block_ips> persist file "/etc/pf.anchors/emerging-Block-IPs.txt"
# table to conatin automatic entries for IPs which try to brute force (to be implemented later)
# table <bruteforce> persist
# table to list hosts that I want to ban manually
table <bad_hosts> persist
# Sun Microsystems cluster interconnect (should not be coming in over the internet)
# table <sunmicro> const { 204.152.64.0/23 }
# Special Use Addresses (should not be coming in over the internet)
# table <rfc5735> const { 0.0.0.0/8, 10.0.0.0/8, \
# 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, \
# 192.0.0.0/24, 192.0.2.0/24, 192.168.0.0/16, \
# 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, \
# 224.0.0.0/4, 240.0.0.0/4, 255.255.255.255/32 }
# Do not block 192.88.99.0/24 RFC3068
# Special Use Addresses (should not be coming in over the internet)
table <blocked_net_addr> const { 0.0.0.0/8, 10.0.0.0/8, \
127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, \
192.0.0.0/24, 192.0.2.0/24, 192.168.0.0/16, \
198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, \
224.0.0.0/4, 240.0.0.0/4, 204.152.64.0/23 }
##########
# Options - Options tune the behavior of the packet filtering engine.
##########
# urgent -debug messages generated for serious errors
# misc - debug messages generated for various errors
# (e.g., to see status from the packet normalizer/scrubber and for state creation failures)
# loud - debug messages generated for common conditions
# (e.g., to see status from the passive OS fingerprinter)
set debug urgent
# Enable collection of packet and byte count statistics for the given interface.
set loginterface $ext_if
# A TCP RST is returned for blocked TCP packets,
# an ICMP UNREACHABLE is returned for blocked UDP packets,
# and all other packets are silently dropped.
set block-policy return
# list interfaces for which packets should not be filtered.
set skip on lo0
# Enable basic ruleset optimization. This is the default behaviour.
# Basic ruleset optimization does four things to improve the performance of ruleset evaluations:
# 1. remove duplicate rules
# 2. remove rules that are a subset of another rule
# 3. combine multiple rules into a table when advantageous
# 4. re-order the rules to improve evaluation performance
set ruleset-optimization basic
# Load fingerprints of known operating systems from the given filename.
set fingerprints "/etc/pf.os"
# Optimize state timeouts for a normal network environment.
set optimization normal
# if-bound - states are bound to the interface they're created on.
# If traffic matches a state table entry but is not crossing
# the interface recorded in that state entry, the match is rejected.
# The packet must then match a filter rule or will be dropped/rejected altogether.
# floating - states can match packets on any interface. As long as the packet matches
# a state entry and is passing in the same direction as it was on the interface
# when the state was created, it does not matter what interface it's crossing, it will pass
set state-policy floating
##########
# Traffic Normalization (e.g. scrub) - Traffic normalization protects internal machines against inconsistencies in Internet protocols and implementations.
##########
# normalize all incoming traffic
scrub in on $ext_if all no-df
# Replace IP identification fields with random values.
# Makes it more difficult to count costs hidden behind a NAT box.
scrub out on $ext_if all random-id
##########
# Queueing - Queueing provides rule-based bandwidth control.
##########
##########
# Translation (Various forms of NAT) - Translation rules specify how addresses are to be mapped or redirected to other addresses.
##########
# general nat rule
nat on $ext_if from $int_if:network to any -> ($ext_if)
# redirect email_web_server services to email_web_server
rdr on $ext_if proto tcp to any port $email_web_server_services -> $email_web_server_ip
# DENY rouge redirection
no rdr
##########
# Packet Filtering - Packet filtering provides rule-based blocking or passing of packets.
##########
# block and log everything by default
block log all
# spoofed address protection for external interface
antispoof for $ext_if
# spoofed address protection for internal interface
antispoof for $int_if
# block anything coming from source we have no back routes for
block quick from no-route
# block packets whose ingress interface does not match the one in
# the route back to their source address
block quick from urpf-failed
# silently drop broadcasts (ISP noise)
block in log quick on $ext_if to 255.255.255.255
# silently drop IGMP (ISP noise)
block in quick on $ext_if proto IGMP from xxx.xxx.xxx.xxx to 224.0.0.1
# block and log incoming packets from emerging threats
block in log quick from <emerging_block_ips>
# block and log incoming packets from spoofed or misconfigured addresses
block in log quick on $ext_if from <blocked_net_addr>
# block and log incoming packets from hosts trying to brute force attack - not yet implemented
# block in log quick from <bruteforce>
# Do not allow Windows 9x SMTP connections since they are typically
# a viral worm. Alternately we could limit these OSes to 1 connection each.
block in log quick inet proto tcp from any os {"Windows 95", "Windows 98"} to any port smtp
# block and log outgoing packets that do not have our address as source,
# they are either spoofed or something is misconfigured (NAT disabled,
# for instance), we want to be nice and do not send out garbage.
block out log quick on $ext_if from ! $ext_ip
# once the traffic is permitted into an interface, we won't try to obstruct it leaving
# don't use modulate state, it messes up the firewall_dns_server's ability to connect to the internet
pass out quick on $ext_if
# allow local traffic
pass quick on $int_if
# open the tcp ports used by those network services that will be available to the Internet
pass in proto tcp to any port $firewall_dns_server_services
# open the tcp ports used by those network services that will be available to the Internet
pass in proto tcp to any port $email_web_server_services
# open the udp ports used by those network services that will be available to the Internet
pass in proto udp to any port $udp_services
# allow ICMP traffic
pass in inet proto icmp to any icmp-type $icmp_types