PF makes its filtering decisions based on user-defined rules, i.e. statements specifying the action to take (block or pass) whenever a packet meets certain criteria; typically, packet filtering is done based on source and destination addresses and ports, layer 3 and 4 protocols, traffic direction and so on.
Rules are represented by the PF.PFRule class:
PFRule instances have the following attributes:
Below are a few examples; for each example, the same rule in pf.conf(5) format is provided:
from socket import *
# Interfaces
ext_if = PFAddr(type=PF_ADDR_DYNIFTL, ifname="sis0")
int_if = "sis1"
# Internal servers
www_srv = PFAddr("192.168.30.10")
www_prt = PFPort("www", IPPROTO_TCP)
smtp_srv = PFAddr("192.168.30.11")
smtp_prt = PFPort("smtp", IPPROTO_TCP)
# NAT outgoing connections
# rule: match out on $ext_if inet from !($ext_if) to any nat-to ($ext_if)
r0 = PFRule(action=PF_MATCH,
direction=PF_OUT,
ifname=ext_if.ifname,
af=AF_INET,
src=PFRuleAddr(addr=ext_if, neg=True),
nat=PFPool(PF_POOL_NAT, ext_if))
# Redirect web services (with load balancing)
# rule: match in on $ext_if inet proto tcp from any to ($ext_if) port $www_prt \
# rdr-to $www_srv round-robin sticky-address
r1 = PFRule(action=PF_MATCH,
direction=PF_IN,
ifname=ext_if.ifname,
af=AF_INET,
proto=IPPROTO_TCP,
dst=PFRuleAddr(ext_if, www_prt),
rdr=PFPool(PF_POOL_RDR, www_srv,
opts=PF_POOL_ROUNDROBIN|PF_POOL_STICKYADDR))
# Default deny
# rule: block drop all
r2 = PFRule(action=PF_DROP)
# Spoofed address protection
# rule: block drop in quick from urpf-failed
r3 = PFRule(action=PF_DROP,
direction=PF_IN,
quick=True,
src=PFRuleAddr(PFAddr(type=PF_ADDR_URPFFAILED)))
# Allow traffic to web server
# rules: pass in on $ext_if inet proto tcp from any to $www_srv port $www_prt synproxy state
# pass out on $int_if inet proto tcp from any to $www_srv port $www_prt
r4 = PFRule(action=PF_PASS,
direction=PF_IN,
ifname=ext_if.ifname,
af=AF_INET,
proto=IPPROTO_TCP,
dst=PFRuleAddr(www_srv, www_prt),
flags="S", flagset="SA",
keep_state=PF_STATE_SYNPROXY)
r5 = PFRule(action=PF_PASS,
direction=PF_OUT,
ifname=int_if,
af=AF_INET,
proto=IPPROTO_TCP,
dst=PFRuleAddr(www_srv, www_prt),
flags="S", flagset="SA",
keep_state=PF_STATE_NORMAL)
# Creating template rules can make writing rules easier and more readable.
# The two previous rules could have been written also as follows:
class PassRule(PFRule):
def __init__(self, **kw):
d = {"action": PF_PASS, "af": AF_INET, "proto": IPPROTO_TCP,
"flags": "S", "flagset": "SA", "keep_state": PF_STATE_NORMAL}
d.update(kw)
super(PassRule, self).__init__(**d)
r4 = PassRule(direction=PF_IN, ifname=ext_if.ifname,
dst=PFRuleAddr(www_srv, www_prt), keep_state=PF_STATE_SYNPROXY)
r5 = PassRule(direction=PF_OUT, ifname=int_if, dst=PFRuleAddr(www_srv, www_prt))
# Allow incoming "unreach code needfrag" ICMP packets and all outgoing ICMP traffic.
# rules: pass in inet proto icmp all icmp-type unreach code needfrag
# pass out inet proto icmp all
r6 = PFRule(action=PF_PASS,
direction=PF_IN,
af=AF_INET,
proto=IPPROTO_ICMP,
type=ICMP_UNREACH+1,
code=ICMP_UNREACH_NEEDFRAG+1,
keep_state=PF_STATE_NORMAL)
r7 = PFRule(action=PF_PASS,
direction=PF_OUT,
af=AF_INET,
proto=IPPROTO_ICMP,
keep_state=PF_STATE_NORMAL)
# Allow smtp traffic from all except for addresses in the <spammers> table
# rules: table <spammers> persist
# pass in on $ext_if inet proto tcp from !<spammers> to $smtp_srv port $smtp_prt
# pass out on $int_if inet proto tcp from !<spammers> to $smtp_srv port $smtp_prt
t1 = PFTable("spammers", flags=PFR_TFLAG_PERSIST)
r8 = PFRule(action=PF_PASS,
direction=PF_IN,
ifname=ext_if.ifname,
af=AF_INET,
proto=IPPROTO_TCP,
src=PFRuleAddr(PFAddr("<{0}>".format(t1.name)), neg=True),
dst=PFRuleAddr(smtp_srv, smtp_prt),
flags="S", flagset="SA")
r9 = PFRule(action=PF_PASS,
direction=PF_OUT,
ifname=int_if,
af=AF_INET,
proto=IPPROTO_TCP,
src=PFRuleAddr(PFAddr("<{0}>".format(t1.name)), neg=True),
dst=PFRuleAddr(smtp_srv, smtp_prt),
flags="S", flagset="SA")
The next paragraph explains how these rules can be actually loaded onto the system.
Rules are grouped together in rulesets; each ruleset has a name (the name of the main ruleset is the empty string) and may contain filtering rules, tables and other rulesets. Nested rulesets are accessed through their absolute path (i.e. the name of the parent rulesets, separated by "/") and can be manipulated at runtime.
Rulesets are represented by the PF.PFRuleset class, which is a subclass of PF.PFRule; in fact, rulesets can specify packet filtering parameters just like normal rules, in order to apply their inner rules only to packets with specific characteristics.
Besides the attributes inherited from PF.PFRule (which will be omitted for conciseness), PF.PFRuleset instances support the following attributes and methods:
In the following example, the rules created in the previous paragraph, will be loaded onto the system:
# Initialize and populate the ruleset rs = PFRuleset() rs.append(r0, r1, r2, r3, r4, r5, r6, r7, t1, r8, r9) # Load rules pf = PacketFilter() pf.load_ruleset(rs) # Retrieve rules and print them print pf.get_ruleset()