6. PF rules!

The impact of CARP and pfsync on Packet Filter rules is really minimal. First, you need to let the PFSYNC and CARP protocols pass on their own interfaces:

pass quick on rl2 proto pfsync keep state (no-sync)
pass on { rl0, rl1 } proto carp keep state (no-sync)

Then, when writing firewall rules, keep in mind that, from pf(4)'s point of view, all traffic passes through the physical interface. Thus, in cases like:

pass in on $ext_if [...]

you can keep referring to the physical, not the virtual interface.

On the other hand, the virtual address is associated to the CARP interface; thus, you need to refer to it if the firewall offers any services on its virtual address:

# SSH on the virtual interface
pass in on $int_if inet proto tcp from $int_if:network to carp0 port ssh

or on a NATed server, through traffic redirection:

# Mail server accessible from the internet
pass in on $ext_if inet proto tcp from any to carp2 port $mail_ports rdr-to $mail_srv

In all other cases, CARP is perfectly transparent to pf(4), as for services offered by the firewall on its physical addresses:

# SSH on the physical address
pass in on $int_if inet proto tcp from $int_if:network to $int_if port ssh

or for normal filtering:

# External DNS
pass in  on $int_if inet proto { tcp, udp } from $int_if:network to $dns_srv \
     port domain
pass out on $ext_if inet proto { tcp, udp } from $ext_if to $dns_srv \ 
     port domain

As an example, let's see a basic PF ruleset for our external firewalls, Donald and Daisy:

/etc/pf.conf
################################################################################
# Macros and lists                                                             #
################################################################################

ext_if  = rl0                           # External interface
int_if  = rl1                           # DMZ interface
pfs_if  = rl2                           # Pfsync interface
carp_if = carp1                         # External CARP interface

mail_srv = "mail.kernel-panic.it"                               # Mail server
web_srv  = "{ www1.kernel-panic.it, www2.kernel-panic.it }"     # Web servers
dns_srv  = "{ dns1.isp.com, dns2.isp.com }"                     # DNS servers
int_fw   = "{ mickey.kernel-panic.it, minnie.kernel-panic.it }" # Internal fw

mail_ports = "{ smtp, imap, imaps }"    # Mail server ports
web_ports  = "{ www, https }"           # Web server ports

# Allowed incoming ICMP types
icmp_types = "{ echoreq, timex, paramprob, unreach code needfrag }"

# Private networks (RFC 1918)
priv_nets = "{ 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 }"

################################################################################
# Options, scrub and NAT                                                       #
################################################################################

set block-policy drop
set loginterface $ext_if
set skip on lo

# NAT outgoing connections
match out on $ext_if from !$ext_if to any nat-to $ext_if

# Redirect web services (with load balancing)
match in on $ext_if inet proto tcp from any to $carp_if port $web_ports \
    rdr-to $web_srv round-robin sticky-address

# Redirect mail services
match in on $ext_if inet proto tcp from any to $carp_if port $mail_ports \
    rdr-to $mail_srv

################################################################################
# Filtering rules                                                              #
################################################################################

block all                          # Default deny
block in quick from urpf-failed    # Spoofed address protection

# Scrub incoming packets
match in all scrub (no-df)

pass quick on $pfs_if proto pfsync keep state (no-sync)        # Enable pfsync
pass on { $int_if, $ext_if } proto carp keep state (no-sync)   # Enable CARP

block in  quick on $ext_if from $priv_nets to any
block out quick on $ext_if from any to $priv_nets

# Mail server
pass in  on $ext_if inet proto tcp from any to $mail_srv port $mail_ports
pass out on $int_if inet proto tcp from any to $mail_srv port $mail_ports
pass in  on $int_if inet proto tcp from $mail_srv to any port smtp
pass out on $ext_if inet proto tcp from $ext_if to any port smtp modulate state

# Web servers
pass in  on $ext_if inet proto tcp from any to $web_srv port $web_ports \
     synproxy state
pass out on $int_if inet proto tcp from any to $web_srv port $web_ports

# ICMP
pass in  inet proto icmp all icmp-type $icmp_types
pass out inet proto icmp all

# DNS
pass in  on $int_if inet proto { tcp, udp } from $int_if:network to $dns_srv \
     port domain
pass out on $ext_if inet proto { tcp, udp } from $ext_if to $dns_srv \
     port domain

# Internet web servers
pass in  on $int_if inet proto tcp from $int_fw to any port $web_ports
pass out on $ext_if inet proto tcp from $ext_if to any port $web_ports \
     modulate state