py-pf provides multiple classes to represent the various types of network addresses that can be contained in PF rules:
In Packet Filter, network addresses are a rather broad concept: besides the familiar IP/netmask format, an address may also be specified as a (dynamic) interface name, a table, a labeled route, and so on.
Addresses are represented by the pf.PFAddr class:
PFAddr instances have the following attributes:
An example will best illustrate how a PF address can be handled:
def print_addr(addr):
"""Display address information about the provided PFAddr object."""
if addr.type == pf.PF_ADDR_DYNIFTL:
# The address is a dynamically configured interface or interface group
print "(" + addr.ifname,
# Check the 'iflags' modifiers
if addr.iflags & pf.PFI_AFLAG_NETWORK:
print ":network",
if addr.iflags & pf.PFI_AFLAG_BROADCAST:
print ":broadcast",
if addr.iflags & pf.PFI_AFLAG_PEER:
print ":peer",
if addr.iflags & pf.PFI_AFLAG_NOALIAS:
print ":0",
print ")"
elif addr.type == pf.PF_ADDR_TABLE:
# The address is a PF table
print "<{.tblname}>".format(addr)
elif addr.type == pf.PF_ADDR_ADDRMASK:
# IPv4 or IPv6 address
if addr.addr is None:
print "any"
else:
print "{0.addr}/{0.mask}".format(addr)
elif addr.type == pf.PF_ADDR_RANGE:
# Address range
print "{0[0]} - {0[1]}".format(addr)
elif addr.type == pf.PF_ADDR_NOROUTE:
# Address not routable
print "no-route"
elif addr.type == pf.PF_ADDR_URPFFAILED:
# Source address failing the uRPF check
print "urpf-failed"
elif addr.type == pf.PF_ADDR_RTLABEL:
# Labeled route
print "route \"{.rtlabelname}\"".format(addr)
else:
# Unknown address type
print "?"
PFAddr objects can be easily created by passing the address as a string or by specifying its attributes as keywords; for example:
net = PFAddr("10.0.0.0/8")
rng = PFAddr("192.168.1.0 - 192.168.1.31")
ifnet = PFAddr("rl0:network", af=AF_INET)
any = PFAddr()
tbl = PFAddr("<spammers>")
ifnet = PFAddr(type=pf.PF_ADDR_DYNIFTL, ifname="rl0",
iflags=pf.PFI_AFLAG_NETWORK, af=AF_INET)
Also ports are a rather broad concept in Packet Filter. They're not just a port number and a protocol; instead, they are made up of a comparison operator and one or two port numbers (depending on the operator). This allows you to specify single port numbers, port ranges or ports higher/lower than a certain value.
PFPort instances have the following attributes:
A simple example is better than a long talk:
def print_port(port):
s = {PF_OP_NONE: "{0[0]}",
PF_OP_IRG: "{0[0]} >< {0[1]}",
PF_OP_XRG: "{0[0]} <> {0[1]}",
PF_OP_EQ: "= {0[0]}",
PF_OP_NE: "!= {0[0]}",
PF_OP_LT: "< {0[0]}",
PF_OP_LE: "<= {0[0]}",
PF_OP_GT: "> {0[0]}",
PF_OP_GE: ">= {0[0]}",
PF_OP_RRG: "{0[0]}:{0[1]}"}
print s[port.op].format(port.num)
PFPort objects can be created by passing the port as an integer, a string or a tuple; for example:
p1 = PFPort("www", socket.IPPROTO_TCP)
p2 = PFPort("> 1024", socket.IPPROTO_TCP)
p3 = PFPort("2000 >< 3000", socket.IPPROTO_UDP)
p4 = PFPort(22, socket.IPPROTO_TCP, pf.PF_OP_NE)
p5 = PFPort((2000, 3000), socket.IPPROTO_TCP, pf.PF_OP_IRG) # Same as p3
A pf.PFRuleAddr object represents an address/port pair, which is used as source or destination in filtering rules; it is basically a container for a PFAddr and PFPort pair.
PFRuleAddr instances have the following attributes:
Below are a few examples:
addr = pf.PFAddr("1.2.3.4")
port = pf.PFPort("80", socket.IPPROTO_TCP)
www_srv = pf.PFRuleAddr(addr, port)
spam_tbl = pf.PFAddr("<spammers>")
not_spam = pf.PFRuleAddr(spam_tbl, neg=True)
An address pool is a group of addresses which is typically used as the target for address translation and traffic redirection (nat-to, rdr-to, route-to, reply-to and dup-to filter options). Addresses in the pool can be either a single IP address or a table, and are represented by means of PFAddr objects.
Address pools are represented by the pf.PFPool class.
PFPool objects support the following methods and attributes:
Below are a few examples of creating and managing address pools:
# Create a NAT address pool (assuming that the table <int_net> already exists)
pool = pf.PFPool(pf.PF_POOL_NAT, pf.PFAddr("<int_net>"))
# Create a RDR address pool
pool = PFPool(pf.PF_POOL_RDR, pf.PFAddr("192.168.23.4"), proxy_port=pf.PFPort(80))
# Create a "round-robin" and "sticky-address" pool
p = pf.PFPool(pf.PF_RDR, PFAddr("<web_servers>"),
opts=pf.PF_POOL_ROUNDROBIN|pf.PF_POOL_STICKYADDR)
Tables are used to hold a group of IPv4 and/or IPv6 addresses; they are normally used to store a large number of addresses as table lookups are very fast and resource-efficient.
py-pf represents tables by means of pf.PFTable objects, while methods for actually creating, populating and managing tables on the system are provided by the pf.PacketFilter class.
There are two different ways to actually load tables in the firewall: they can be either created at runtime, using the PacketFilter.add_table() method or loaded along with a ruleset, using PacketFilter.load_ruleset(). Please note that tables without the PFR_TFLAG_PERSIST flag set and not referenced by any rule are automatically removed by the kernel: in this case only the latter method can be used, as it commits the transaction only when all tables and rules have been created.
PFTable instances have the following attributes:
Below are a few examples of creating and managing address tables:
# Define a new constant table
web_srv = ["192.168.23.11", "192.168.23.12", "192.168.23.13"]
t = PFTable("web_servers", *web_srv, flags=pf.PFR_TFLAG_CONST)
# Print all currently loaded tables
def print_table(tbl):
flags = 'c' if (tbl.flags & pf.PFR_TFLAG_CONST) else '-'
flags += 'p' if (tbl.flags & pf.PFR_TFLAG_PERSIST) else '-'
flags += 'a' if (tbl.flags & pf.PFR_TFLAG_ACTIVE) else '-'
flags += 'i' if (tbl.flags & pf.PFR_TFLAG_INACTIVE) else '-'
flags += 'r' if (tbl.flags & pf.PFR_TFLAG_REFERENCED) else '-'
flags += 'h' if (tbl.flags & pf.PFR_TFLAG_REFDANCHOR) else '-'
flags += 'C' if (tbl.flags & pf.PFR_TFLAG_COUNTERS) else '-'
print "{0}\t{1.name}".format(flags, tbl)
filter = pf.PacketFilter()
for table in filter.get_tables():
print_table(table)
Network addresses in tables are represented by pf.PFTableAddr objects; they are quite simpler than PFAddr addresses, because they can only hold IPv4 and IPv6 addresses.
PFTableAddr instances have the following attributes:
Packet filtering may also be performed based on the owner (user or group) of the socket that sends/receives the packets.
User and group IDs are represented by pf.PFUid and pf.PFGid instances respectively, which are very similar to PFPort objects. In fact, they are made up of a comparison operator and one or two ID numbers (depending on the operator); this allows you to specify single users/groups, as well as ranges of user IDs.
PFUid and PFGid instances have the following attributes: