6. Addresses, address pools and tables

py-pf provides multiple classes to represent the various types of network addresses that can be contained in PF rules:

6.1 Addresses and ports

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.

6.1.1 PFAddr objects

Addresses are represented by the pf.PFAddr class:

class pf.PFAddr([addr[, af[, **kw]]])
The optional addr argument is the string representation of the address in pf.conf(5) format; if omitted, the object will match any address. af is the address family, which must be specified if not ascertainable from addr. The **kw parameter allows you to specify the value of any attribute by passing it as a keyword.

PFAddr instances have the following attributes:

PFAddr.type
A constant specifying the address type; it must be one of pf.PF_ADDR_ADDRMASK (IPv4 or IPv6 address), pf.PF_ADDR_NOROUTE (any non currently routable address), pf.PF_ADDR_DYNIFTL (a dynamically configured interface (e.g. DHCP or PPP) or an interface group), pf.PF_ADDR_TABLE (an address table), pf.PF_ADDR_RTLABEL (any address whose associated route has the specified label), pf.PF_ADDR_URPFFAILED (any source address failing the uRPF check), pf.PF_ADDR_RANGE (a range of addresses).
PFAddr.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. Note that the address family can be left unspecified only in a limited set of circumstances, such as in "block all" rules.
PFAddr.ifname
The interface name when PFAddr.type is pf.PF_ADDR_DYNIFTL.
PFAddr.iflags
Bitmask of flags for the optional modifiers to apply to the interface, or interface group; can be any of pf.PFI_AFLAG_NETWORK (":network" modifier), pf.PFI_AFLAG_BROADCAST (":broadcast" modifier), pf.PFI_AFLAG_PEER (":peer" modifier) and pf.PFI_AFLAG_NOALIAS (":0" modifier).
PFAddr.addr
String containing the IPv4 or IPv6 address when PFRule.type is pf.PF_ADDR_ADDRMASK or a tuple containing the first and last address of an address range when PFRule.type is pf.PF_ADDR_RANGE; set it to None to match any address.
PFAddr.mask
String containing the netmask of the address; it applies to both pf.PF_ADDR_ADDRMASK and pf.PF_ADDR_DYNIFTL addresses; set it to None to match any address.
PFAddr.tblname
The name of the table when PFRule.type is pf.PF_ADDR_TABLE.
PFAddr.rtlabelname
The name of the label when PFRule.type is pf.PF_ADDR_RTLABEL.

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)

6.1.2 PFPort objects

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.

class pf.PFPort([num[, proto[, op]]])
The num argument must contain the port number(s) either as an integer, a tuple or a string (service name). proto is the transport-layer protocol expressed as a constant (IPPROTO_TCP or IPPROTO_UDP); these constants are available through the socket module. op is the port comparison operator; it must be one of the PF_OP_* constants (see example below).

PFPort instances have the following attributes:

PFPort.num
A two-items tuple containing the port numbers; if PFPort.op is a unary operator, the second port is set to 0.
PFPort.proto
The transport layer protocol; it must be one of IPPROTO_TCP, IPPROTO_UDP or 0 (protocol unspecified).
PFPort.op
The port comparison operator; it must be one of the PF_OP_* constants.

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

6.1.3 PFRuleAddr objects

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.

class pf.PFRuleAddr([addr[, port[, neg]]])
The optional addr and port parameters must be a PFAddr and PFPort object respectively; neg is a boolean value that allows you to negate the address.

PFRuleAddr instances have the following attributes:

PFRuleAddr.addr
This is a PFAddr object that represents the address.
PFRuleAddr.port
A PFPort object that represents the port.
PFRuleAddr.neg
A boolean value that, if set to True, negates the address/port pair.

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)

6.2 Address pools

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.

6.2.1 PFPool objects

Address pools are represented by the pf.PFPool class.

class pf.PFPool(id, pool[, **kw])
The id argument specifies the pool type; valid values are pf.PF_POOL_ROUTE (when the pool is the target of a route-to option), pf.PF_POOL_NAT (with nat-to) and pf.PF_POOL_RDR (with rdr-to). The pool parameter holds the PFAddr object containing the address(es) in the pool; though PFAddr objects can represent many address types, only a single address or a table are valid for a pool (i.e. PFAddr.type must be pf.PF_ADDR_ADDRMASK or pf.PF_ADDR_TABLE). The **kw parameter allows you to specify the value of any attribute by passing it as a keyword.

PFPool objects support the following methods and attributes:

PFPool.id
A constant specifying the pool type; it can be one of PF_POOL_ROUTE, PF_POOL_NAT or PF_POOL_RDR.
PFPool.pool
The PFAddr object containing the address(es) in the pool.
PFPool.proxy_port
A PFPort instance containing the range of port numbers (or, if the port numbers are the same, the single port) to use for pf.PF_POOL_NAT and pf.PF_POOL_RDR pools; by default, the port range for NAT is from pf.PF_NAT_PROXY_PORT_LOW to pf.PF_NAT_PROXY_PORT_HIGH. If both port numbers are 0, the original port is kept.
PFPool.opts
A bitmask specifying the options applied to the pool; it must be ANDed with the pf.PF_POOL_TYPEMASK constant before testing its value. Valid flags are pf.PF_POOL_BITMASK (corresponding to the "bitmask" option in pf.conf(5)), pf.PF_POOL_RANDOM ("random" option), pf.PF_POOL_SRCHASH ("source-hash" option) and pf.PF_POOL_ROUNDROBIN ("round-robin" option); pf.PF_POOL_RANDOM and pf.PF_POOL_ROUNDROBIN pools allow the setting of the pf.PF_POOL_STICKYADDR flag ("sticky-address" option).

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)

6.3 Tables

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.

6.3.1 PFTable objects

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.

class pf.PFTable(table[, *addrs[, **kw]])
The first parameter is the name of the table; *addrs allows you to insert one or more addresses in the table at creation time, while **kw allows you to specify the value of any attribute by passing it as a keyword.

PFTable instances have the following attributes:

PFtable.name
The table name.
PFtable.addrs
A tuple of PFTableAddr objects containing the addresses in the table.
PFtable.anchor
The name of the anchor the table is attached to.
PFtable.flags
A bitmask containing the flags associated with the table. Valid flags are: pf.PFR_TFLAG_CONST (constant table), pf.PFR_TFLAG_PERSIST (persistent table), pf.PFR_TFLAG_ACTIVE (table present in the active tableset), pf.PFR_TFLAG_INACTIVE (table defined in the inactive tableset), pf.PFR_TFLAG_REFERENCED (table referenced by one or more rules) and pf.PFR_TFLAG_REFDANCHOR (hidden table); only the first two flags can be manually set when creating a table.

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)

6.3.2 PFTableAddr objects

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.

class pf.PFTableAddr(addr[, **kw])
The first parameter is a string containing the IPv4/IPv6 address while **kw allows you to specify the value of any attribute by passing it as a keyword.

PFTableAddr instances have the following attributes:

PFtableAddr.af
The address family; it can be one of AF_INET (IPv4) or AF_INET6 (IPv6); these constants are available through the socket module.
PFtableAddr.addr
String containing the IPv4 or IPv6 address.
PFtableAddr.mask
String containing the netmask of the address.
PFtableAddr.neg
A boolean value that, if set to True, negates the address.

6.4 PFUid and PFGid objects

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.

class pf.PFUid([num[, op]])
class pf.PFGid([num[, op]])
The num argument must contain the user/group ID(s) either as a string, a tuple or an integer. op is the comparison operator; it must be one of the PF_OP_* constants.

PFUid and PFGid instances have the following attributes:

PFUid.num
PFGid.num
A two-items tuple containing the user/group IDs; if PFUid.op/PFGid.op is a unary operator, the second ID is set to 0.
PFUid.op
PFGid.op
The comparison operator; it must be one of the PF_OP_* constants.