7. Anatomy of a Packet Filter rule

PF makes its filtering decisions based on user-defined rules, i.e. statements specifying the action to take (block, pass or match) 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.

7.1 PFRule objects

Rules are represented by the pf.PFRule class:

class pf.PFRule([**kw])
A PFRule object can be instantiated by passing the value of its attributes as keywords; instantiating a PFRule with no arguments results in a "pass all" rule.

PFRule objects have the following attributes:

PFRule.action
A constant specifying the action to be taken by Packet Filter when the rule is matched; valid actions are (names should be pretty self-explanatory): pf.PF_PASS, pf.PF_DROP, pf.PF_SCRUB, pf.PF_NOSCRUB, pf.PF_NAT, pf.PF_NONAT, pf.PF_BINAT, pf.PF_NOBINAT, pf.PF_RDR, pf.PF_NORDR, pf.PF_SYNPROXY_DROP and pf.PF_MATCH.
PFRule.direction
The direction of traffic: it can be pf.PF_IN (incoming packets), pf.PF_OUT (outgoing packets) or pf.PF_INOUT (packets in both directions).
PFRule.log
A bitmask containing the logging flags; valid flags are pf.PF_LOG (log the packet that establishes the state), pf.PF_LOG_ALL (log all packets for the connection) and pf.PF_LOG_SOCKET_LOOKUP (additionally log the user ID and PID of the process that the socket belongs to).
PFRule.logif
The number of the pflog(4) interface where logs should be sent (e.g. 0 for pflog0).
PFRule.quick
A boolean value that, if True, forces Packet Filter to skip the evaluation of subsequent rules if the packet matches the current rule.
PFRule.src
A PFRuleAddr instance containing the source address/port pair.
PFRule.dst
A PFRuleAddr instance containing the destination address/port pair.
PFRule.ifname
The name of the interface to which the rule applies.
PFRule.ifnot
Boolean value that allows you to negate the interface specified by PFRule.ifname.
PFRule.rt
A constant specifying optional routing options for packets matching this rule; valid options are: pf.PF_FASTROUTE (normal route lookup), pf.PF_ROUTETO (route the packet to the specified interface, with an optional address for the next hop), pf.PF_DUPTO (create a duplicate of the packet and route it like pf.PF_ROUTETO), pf.PF_REPLYTO (similar to pf.PF_ROUTETO, but route packets that pass in the opposite direction (replies) to the specified interface). The interface and the optional next-hop address are specified by means of a PFPool object contained in the PFRule.route attribute.
PFRule.route
A PFPool object specifying the address pool for filtering rules with PFRule.rt set to pf.PF_ROUTETO, pf.PF_DUPTO or pf.PF_REPLYTO.
PFRule.nat
A PFPool object specifying the address pool for NATting rules (i.e. the target of the nat-to option in pf.conf(5) format).
PFRule.rdr
A PFPool object specifying the address pool for redirection rules (i.e. the target of the rdr-to option in pf.conf(5) format).
PFRule.af
The address family; it can be one of AF_INET (IPv4), AF_INET6 (IPv6) or AF_UNSPEC (address family not specified); these constants are available through the socket module.
PFRule.proto
A constant specifying the transport layer protocol; the socket module provides constants for most protocols.
PFRule.uid
A PFUid object; the rule only applies to packets to and from sockets owned by the specified user.
PFRule.gid
A PFGid object; the rule only applies to packets to and from sockets owned by the specified group.
PFRule.flags
A string containing the TCP flags that should be set out of PFRule.flagset.
PFRule.flagset
A string containing the TCP flags that must be checked by Packet Filter.
PFRule.type
A constant specifying the ICMP or ICMPv6 type that the rule applies to; it can be one of the ICMP_* type constants and is stored incremented by 1. It is valid only when PFRule.proto is IPPROTO_ICMP or IPPROTO_ICMPV6.
PFRule.code
A constant specifying the ICMP or ICMPv6 code that the rule applies to; it can be one of the ICMP_* code constants and is stored incremented by 1. It is valid only when PFRule.proto is IPPROTO_ICMP or IPPROTO_ICMPV6.
PFRule.tos
An integer containing the TOS bits that must be set in the packets for the rule to match; the following constants may be used: pf.IPTOS_LOWDELAY, pf.IPTOS_THROUGHPUT and pf.IPTOS_RELIABILITY.
PFRule.set_tos
The TOS bits to enforce on matching packets; the following constants may be used: pf.IPTOS_LOWDELAY, pf.IPTOS_THROUGHPUT and pf.IPTOS_RELIABILITY.
PFRule.keep_state
A constant specifying the state tracking mode for the rule; valid values are pf.PF_STATE_NORMAL (default state tracking), pf.PF_STATE_MODULATE (with randomized ISNs) and pf.PF_STATE_SYNPROXY (with Packet Filter acting as a SYN proxy).
PFRule.rule_flag
A bitmask containing various rule options, including:
PFRule.scrub_flags
A bitmask containing traffic normalization (scrub) options: pf.PFSTATE_NODF (clear the don't fragment bit), pf.PFSTATE_RANDOMID (randomize the IP identification field), pf.PFSTATE_SCRUB_TCP (statefully normalize TCP connections), pf.PFSTATE_SETTOS (enable enforcing of TOS, as specified by PFRule.set_tos, for matching packets);
PFRule.return_ttl
If the block-policy for this rule is pf.PFRULE_RETURNRST, force the TTL of the returned packets to the specified value.
PFRule.return_icmp
A constant specifying the ICMP code to return when the block-policy is set to pf.PFRULE_RETURNICMP.
PFRule.return_icmp6
A constant specifying the ICMPv6 code to return when the block-policy is set to pf.PFRULE_RETURNICMP.
PFRule.max_states
The maximum number of concurrent states that the rule may create.
PFRule.src_nodes
The current number of source addresses which have state table entries for this rule.
PFRule.max_src_nodes
The maximum number of source addresses which can simultaneously have state table entries for this rule.
PFRule.max_src_states
The maximum number of simultaneous state entries that a single source address can create with this rule.
PFRule.max_src_conn
The maximum number of simultaneous TCP connections which have completed the 3-way handshake that a single host can make.
PFRule.max_src_conn_rate
A two-items tuple (in the form (number, seconds)) specifying the maximum number of new connections over the given time interval.
PFRule.min_ttl
The minimum TTL to be enforced for matching IP packets.
PFRule.max_mss
The maximum MSS to be enforced for matching TCP packets.
PFRule.label
A string specifying a label for the rule.
PFRule.qname
The name of the queue to which packets matching this rule must be assigned.
PFRule.qid
The numeric ID of the queue specified by PFRule.qname.
PFRule.pqname
The optional queue to which packets which have a TOS of lowdelay and TCP ACKs with no data payload will be assigned.
PFRule.pqid
The numeric ID of the queue specified by PFRule.pqname.
PFRule.rcv_ifname
String specifying an interface or interface group name; the rule will only match packets which were received on this interface or interface group.
PFRule.tagname
The name of the tag to assign to packets matching this rule.
PFRule.match_tagname
The name of the tag that the packet must be tagged with for the rule to match.
PFRule.match_tag_not
A boolean value that, if set to True, allows you to negate PFRule.match_tagname.
PFRule.rtableid
The ID of the alternate routing table to use for the routing lookup.
PFRule.overload_tblname
The name of the table to which hosts connecting faster than the PFRule.max_src_conn_rate must be added.
PFRule.flush
A bitmask specifying the flush options for hosts added to the table specified by PFRule.overload_tblname; valid flags are: pf.PF_FLUSH (kill all states created by this rule and originating from the offending host) and pf.PF_FLUSH_GLOBAL (kill all states originating from the offending host).
PFRule.pktrate
A PFThreshold object specifying after how many matching packets in a certain time, the rule will stop maching.
PFRule.evaluations
The number of evaluations for this rule.
PFRule.packets
The number of packets that matched this rule.
PFRule.bytes
The total size of packets that matched this rule.
PFRule.onrdomain
The number of the routing domain that packets must come in on, or go out through for this rule to apply.
PFRule.timeout
A list of PFTM_MAX elements specifying the timeout values to be used for states created by this rule; PFTM_* constants can be used as indexes for this list.
PFRule.states_cur
The number of current states for this rule.
PFRule.states_tot
The total number of states created by this rule.
PFRule.nr
An integer that uniquely identifies the rule inside the ruleset.
PFRule.prob
The probability with which the rule should be honoured; the value must be between 0 and 1, bounds not included.
PFRule.cuid
The UID of the user who added this rule.
PFRule.cpid
The PID of the process which added this rule.
PFRule.allow_opts
A boolean value that, if set to True, allows the passing of IPv4 packets with IP options or IPv6 packets with routing extension headers.
PFRule.set_prio
A two-items tuple to assign a specific queueing priority to packets matching this rule; priorities are assigned as integers 0 through 7, with a default priority of 3. The second item, if different from the first, allows you to specify a different priority for packets which have a TOS of lowdelay and TCP ACKs with no data payload.
PFRule.prio
An integer (0 through 7) that allows PF to match only packets with that queueing priority.
PFRule.divert
A PFDivert object holding divert information.

Below are a few examples; for each example, the same rule in pf.conf(5) format is provided:

import socket

# Interfaces
ext_if = pf.PFAddr(type=pf.PF_ADDR_DYNIFTL, ifname="sis0")
int_if = "sis1"

# Internal servers
www_srv  = pf.PFAddr("192.168.30.10")
www_prt  = pf.PFPort("www", socket.IPPROTO_TCP)
smtp_srv = pf.PFAddr("192.168.30.11")
smtp_prt = pf.PFPort("smtp", socket.IPPROTO_TCP)

# NAT outgoing connections
# rule: match out on $ext_if inet from !($ext_if) to any nat-to ($ext_if)
r0 = pf.PFRule(action=pf.PF_MATCH,
               direction=pf.PF_OUT,
               ifname=ext_if.ifname,
               af=socket.AF_INET,
               src=PFRuleAddr(addr=ext_if, neg=True),
               nat=PFPool(pf.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 = pf.PFRule(action=pf.PF_MATCH,
               direction=pf.PF_IN,
               ifname=ext_if.ifname,
               af=socket.AF_INET,
               proto=socket.IPPROTO_TCP,
               dst=pf.PFRuleAddr(ext_if, www_prt),
               rdr=pf.PFPool(pf.PF_POOL_RDR, www_srv,
                             opts=pf.PF_POOL_ROUNDROBIN|pf.PF_POOL_STICKYADDR))

# Default deny
# rule: block drop all
r2 = pf.PFRule(action=pf.PF_DROP)

# Spoofed address protection
# rule: block drop in quick from urpf-failed
r3 = pf.PFRule(action=pf.PF_DROP,
               direction=pf.PF_IN,
               quick=True,
               src=pf.PFRuleAddr(pf.PFAddr(type=pf.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 = pf.PFRule(action=pf.PF_PASS,
               direction=pf.PF_IN,
               ifname=ext_if.ifname,
               af=socket.AF_INET,
               proto=socket.IPPROTO_TCP,
               dst=pf.PFRuleAddr(www_srv, www_prt),
               flags="S", flagset="SA",
               keep_state=pf.PF_STATE_SYNPROXY)
r5 = pf.PFRule(action=pf.PF_PASS,
               direction=pf.PF_OUT,
               ifname=int_if,
               af=socket.AF_INET,
               proto=socket.IPPROTO_TCP,
               dst=pf.PFRuleAddr(www_srv, www_prt),
               flags="S", flagset="SA",
               keep_state=pf.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(pf.PFRule):
    def __init__(self, **kw):
        d = {"action": pf.PF_PASS, "af": socket.AF_INET, "proto": socket.IPPROTO_TCP,
             "flags": "S", "flagset": "SA", "keep_state": pf.PF_STATE_NORMAL}
        d.update(kw)
        super(PassRule, self).__init__(**d)

r4 = PassRule(direction=pf.PF_IN, ifname=ext_if.ifname,
               dst=pf.PFRuleAddr(www_srv, www_prt), keep_state=pf.PF_STATE_SYNPROXY)
r5 = PassRule(direction=pf.PF_OUT, ifname=int_if, dst=pf.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 = pf.PFRule(action=pf.PF_PASS,
               direction=pf.PF_IN,
               af=socket.AF_INET,
               proto=socket.IPPROTO_ICMP,
               type=pf.ICMP_UNREACH+1,
               code=pf.ICMP_UNREACH_NEEDFRAG+1,
               keep_state=pf.PF_STATE_NORMAL)
r7 = pf.PFRule(action=pf.PF_PASS,
               direction=pf.PF_OUT,
               af=socket.AF_INET,
               proto=socket.IPPROTO_ICMP,
               keep_state=pf.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 = pf.PFTable("spammers", flags=pf.PFR_TFLAG_PERSIST)
r8 = pf.PFRule(action=pf.PF_PASS,
               direction=pf.PF_IN,
               ifname=ext_if.ifname,
               af=socket.AF_INET,
               proto=socket.IPPROTO_TCP,
               src=pf.PFRuleAddr(pf.PFAddr("<{0}>".format(t1.name)), neg=True),
               dst=pf.PFRuleAddr(smtp_srv, smtp_prt),
               flags="S", flagset="SA")
r9 = pf.PFRule(action=pf.PF_PASS,
               direction=pf.PF_OUT,
               ifname=int_if,
               af=socket.AF_INET,
               proto=socket.IPPROTO_TCP,
               src=pf.PFRuleAddr(pf.PFAddr("<{0}>".format(t1.name)), neg=True),
               dst=pf.PFRuleAddr(smtp_srv, smtp_prt),
               flags="S", flagset="SA")

The next paragraph explains how these rules can be actually loaded onto the system.

7.2 PFRuleset objects

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.

class pf.PFRuleset([name[, **kw]])
The name parameter allows you to assign a name to the ruleset; if omitted, it will default to the empty string, i.e. the name of the main ruleset. The **kw parameter allows you to specify the value of any attribute by passing it as a keyword.

Besides the attributes inherited from pf.PFRule (which will be omitted for conciseness), pf.PFRuleset objects support the following attributes and methods:

PFRuleset.name
The name of the ruleset; the main ruleset is named by the empty string.
PFRuleset.rules
A list containing all the rules (including sub rulesets) defined in this ruleset.
PFRuleset.tables
A list containing all the tables defined in this ruleset.
PFRuleset.append(*items)
Append one or more rules or tables to the ruleset. items can be either PFRule, PFRuleset or PFTable objects.
PFRuleset.insert(index, rule)
Insert one or more rules in the ruleset before index; rules must be PFRule or PFRuleset objects.
PFRuleset.remove([index])
Remove a rule from the ruleset. index allows you to specify the index of the rule to remove; if omitted, the last rule will be removed.

In the following example, the rules created in the previous paragraph, will be loaded onto the system:

# Initialize and populate the ruleset
rs = pf.PFRuleset()
rs.append(r0, r1, r2, r3, r4, r5, r6, r7, t1, r8, r9)

# Load rules
filter = pf.PacketFilter()
filter.load_ruleset(rs)

# Retrieve rules and print them
print filter.get_ruleset()