[Previous: Firewall Redundancy with CARP and pfsync] [Contents]

PF: Example: Firewall for Home or Small Office

Table of Contents

The Scenario

In this example, PF is running on an OpenBSD machine acting as a firewall and NAT gateway for a small network in a home or office. The overall objective is to provide Internet access to the network and to allow limited access to the firewall machine from the Internet, and expose an internal web server to the external Internet. This document will go through a complete ruleset that does just that.

The Network

The network is setup like this: [ COMP1 ] [ COMP3 ] | | ---+------+-----+------- xl0 [ OpenBSD ] fxp0 -------- ( Internet ) | [ COMP2 ]

There are a number of computers on the internal network; the diagram shows three but the actual number is irrelevant. These computers are regular workstations used for web surfing, email, chatting, etc., except for COMP3 which is also running a small web server. The internal network is using the / network block.

The OpenBSD firewall is a Celeron 300 with two network cards: a 3com 3c905B (xl0) and an Intel EtherExpress Pro/100 (fxp0). The firewall has a cable connection to the Internet and is using NAT to share this connection with the internal network. The IP address on the external interface is dynamically assigned by the Internet Service Provider.

The Objective

The objectives are:


This document assumes that the OpenBSD host has been properly configured to act as a router, including verifying IP networking setup, Internet connectivity, and setting the sysctl(3) variables net.inet.ip.forwarding and/or net.inet6.ip6.forwarding to "1". You must also have enabled PF using pfctl(8) or by setting the appropriate variable in /etc/rc.conf.local.

The Ruleset

The following will step through a ruleset that will accomplish the above goals.


The following macros are defined to make maintenance and reading of the ruleset easier:

tcp_services="{ 22, 113 }"


The first two lines define the network interfaces that filtering will happen on. By definining them here, if we have to move this system to another machine with different hardware, we can change only those two lines, and the rest of the rule set will be still usable. The third and fourth lines list the TCP port numbers of the services that will be opened up to the Internet (SSH and ident/auth) and the ICMP packet types that will be accepted at the firewall machine. Finally, the last line defines the IP address of COMP3.

Note: If the Internet connection required PPPoE, then filtering and NAT would have to take place on the tun0 interface and not on fxp0.


The following two options will set the default response for block filter rules and turn statistics logging "on" for the external interface:
set block-policy return
set loginterface $ext_if

Every Unix system has a "loopback" interface. It's a virtual network interface that is used by applications to talk to each other inside the system. On OpenBSD, the loopback interface is lo(4). It is considered best practice to disable all filtering on loopback interfaces. Using set skip will accomplish this.

set skip on lo
Note that we are skipping the entire interface group lo, this way, should we later add additional loopback interfaces, we won't have to worry about altering this portion of our existing rules file.


There is no reason not to use the recommended scrubbing of all incoming traffic, so this is a simple one-liner:
scrub in

Network Address Translation

To perform NAT for the entire internal network the following nat rule is used:
nat on $ext_if from !($ext_if) to any -> ($ext_if)

The "!($ext_if)" could easily be replaced by a "$int_if" in this case, but if you added multiple internal interfaces, you would have to add additional NAT rules, whereas with this structure, NAT will be handled on all protected interfaces.

Since the IP address on the external interface is assigned dynamically, parenthesis are placed around the translation interface so that PF will notice when the address changes.

As we will want to have the FTP proxy working, we'll put the NAT anchor in, too:

nat-anchor "ftp-proxy/*"


The first redirection rules needed are for ftp-proxy(8) so that FTP clients on the local network can connect to FTP servers on the Internet.
rdr-anchor "ftp-proxy/*"
rdr on $int_if proto tcp from any to any port 21 -> port 8021

Note that this rule will only catch FTP connections to port 21. If users regularly connect to FTP servers on other ports, then a list should be used to specify the destination port, for example: from any to any port { 21, 2121 }.

The last redirection rule catches any attempts by someone on the Internet to connect to TCP port 80 on the firewall. Legitimate attempts to access this port will be from users trying to access the network's web server. These connection attempts need to be redirected to COMP3:

rdr on $ext_if proto tcp from any to any port 80 -> $comp3

Filter Rules

Now the filter rules. Start with the default deny:
block in

At this point all traffic attempting to come into an interface will be blocked, even that from the internal network. Later rules will open up the firewall as per the objectives above as well as open up any necessary virtual interfaces.

Keep in mind, pf can block traffic coming into or leaving out of an interface. It can simplify your life if you chose to filter traffic in one direction, rather than trying to keep it straight when filtering some things in, and some things out. In our case, we'll opt to filter the inbound traffic, but once the traffic is permitted into an interface, we won't try to obstruct it leaving, so we will do the following:

pass out keep state

We need to have an anchor for ftp-proxy(8):

anchor "ftp-proxy/*"
It is good to use the spoofed address protection:
antispoof quick for { lo $int_if }

Now open the ports used by those network services that will be available to the Internet. First, the traffic that is destined to the firewall itself:

pass in on $ext_if inet proto tcp from any to ($ext_if) \
   port $tcp_services flags S/SA keep state

Specifying the network ports in the macro $tcp_services makes it simple to open additional services to the Internet by simply editing the macro and reloading the ruleset. UDP services can also be opened up by creating a $udp_services macro and adding a filter rule, similar to the one above, that specifies proto udp.

In addition to having an rdr rule which passes the web server traffic to COMP3, we MUST also pass this traffic through the firewall:

pass in on $ext_if inet proto tcp from any to $comp3 port 80 \
    flags S/SA synproxy state

For an added bit of safety, we'll make use of the TCP SYN Proxy to further protect the web server.

ICMP traffic needs to be passed:

pass in inet proto icmp all icmp-type $icmp_types keep state

Similar to the $tcp_services macro, the $icmp_types macro can easily be edited to change the types of ICMP packets that will be allowed to reach the firewall. Note that this rule applies to all network interfaces.

Now traffic must be passed to and from the internal network. We'll assume that the users on the internal network know what they are doing and aren't going to be causing trouble. This is not necessarily a valid assumption; a much more restrictive ruleset would be appropriate for many environments.

pass in quick on $int_if

TCP, UDP, and ICMP traffic is permitted to exit the firewall towards the Internet due to the earlier "pass out keep state" line. State information is kept so that the returning packets will be passed back in through the firewall.

The Complete Ruleset

# macros ext_if="fxp0" int_if="xl0" tcp_services="{ 22, 113 }" icmp_types="echoreq" comp3="" # options set block-policy return set loginterface $ext_if set skip on lo # scrub scrub in # nat/rdr nat on $ext_if from !($ext_if) -> ($ext_if:0) nat-anchor "ftp-proxy/*" rdr-anchor "ftp-proxy/*" rdr pass on $int_if proto tcp to port ftp -> port 8021 rdr on $ext_if proto tcp from any to any port 80 -> $comp3 # filter rules block in pass out keep state anchor "ftp-proxy/*" antispoof quick for { lo $int_if } pass in on $ext_if inet proto tcp from any to ($ext_if) \ port $tcp_services flags S/SA keep state pass in on $ext_if inet proto tcp from any to $comp3 port 80 \ flags S/SA synproxy state pass in inet proto icmp all icmp-type $icmp_types keep state pass in quick on $int_if

[Previous: Firewall Redundancy with CARP and pfsync] [Contents]

[back] www@openbsd.org
$OpenBSD: example1.html,v 1.34 2007/12/20 17:20:16 joel Exp $