[OpenBSD]

[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 192.168.0.0 / 255.255.255.0 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:

Preparation

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. PF is enabled by default on OpenBSD 4.6 and newer releases.

The Ruleset

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

Macros

The following macros are defined to make maintenance and reading of the ruleset easier:
int_if="xl0"

tcp_services="{ 22, 113 }"
icmp_types="echoreq"

comp3="192.168.0.3"

The first line defines the internal network interface that filtering will happen on. By defining it here, if we have to move this system to another machine with different hardware, we can change only this line, and the rest of the rule set will be still usable. (For this example, the external interface will be handled by using the egress interface group. This is automatically set on any interface holding a default route, in this case, fxp0). The second and third 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 pppoe0 interface and not on the egress interface (fxp0).

Options

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 egress

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 for all lo interfaces, this way, should we later add additional loopback interfaces, we won't have to worry about altering this portion of our existing rules file.

Firewall Rules

We will start with rules to support the use of ftp-proxy(8) so that FTP clients on the local network can connect to FTP servers on the Internet. This works by dynamically inserting rules when an ftp connection is made. This is done using an anchor:
anchor "ftp-proxy/*"

Now we will add the rule needed to divert FTP connections so they are seen by ftp-proxy(8):

pass in quick on $int_if inet proto tcp to any port ftp \
    divert-to 127.0.0.1 port 8021

This rule will intercept FTP connections to port 21 and divert them to an ftp-proxy(8) instance running on port 8021 and, through the use of the quick keyword, matching packets will not be further checked against the rest of the ruleset. If users regularly connect to FTP servers on other ports, then a list should be used to specify the destination port, for example: to any port { 21, 2121 }.

Note that both the anchor and the ftp-proxy(8) divert rule need to be located before any match rules for NAT or the ftp-proxy(8) will not work as expected.

Now we move on to some match rules. By itself, a match rule doesn't determine whether or not a packet is allowed to pass. Instead, packets matching this rule will have the parameters remembered; they will then be used in any pass rules which handle these packets.

This is powerful: parameters such as NAT or queueing can be applied to certain classes of packet, and then access permissions can be defined separately.

To perform NAT for the entire internal network the following match rule is used:

match out on egress inet from !(egress:network) to any nat-to (egress:0)

In this case, the "!(egress:network)" could easily be replaced by a "$int_if:network", 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. The :0 suffix is used so that, if the external interface has multiple addresses, only the first address is used for translation.

Lastly, the protocol family inet (IPv4) is specified. This avoids translating any inet6 (IPv6) packets which may be received.

Now the rules to control access permissions. Start with the default deny:

block in log

At this point all traffic attempting to come into an interface will be blocked, even that from the internal network. These packets will also be logged. 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 choose 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 quick
By using quick, outbound packets can avoid being checked against the following rules, improving performance.

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 egress inet proto tcp from any to (egress) \
    port $tcp_services

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.

The next 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:

pass in on egress inet proto tcp to (egress) port 80 rdr-to $comp3

ICMP traffic needs to be passed:

pass in inet proto icmp all icmp-type $icmp_types

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 on $int_if

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

The Complete Ruleset

# macros

int_if="xl0"

tcp_services="{ 22, 113 }"
icmp_types="echoreq"

comp3="192.168.0.3"

# options

set block-policy return
set loginterface egress
set skip on lo

# FTP Proxy rules

anchor "ftp-proxy/*"

pass in quick on $int_if inet proto tcp to any port ftp \
    divert-to 127.0.0.1 port 8021

# match rules

match out on egress inet from !(egress:network) to any nat-to (egress:0)

# filter rules

block in log
pass out quick

antispoof quick for { lo $int_if }

pass in on egress inet proto tcp from any to (egress) \
    port $tcp_services

pass in on egress inet proto tcp to (egress) port 80 rdr-to $comp3

pass in inet proto icmp all icmp-type $icmp_types

pass in on $int_if

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