Understanding Packet Filter


OpenBSD: 3.4-stable (Jan 29, 2004 - custom kernel)
tutorial updated: Monday, February 16, 2004


preamble

This goal of this document will be i) to introduce the theory of the Packet Filter firewall system and ii) to provide a practical approach to setting up a three-legged firewall for use in a home lan. Packet Filter is the excellent firewall software made by the OpenBSD team. It began shipping with OpenBSD 3.0 (released December 1, 2001) where it replaced IP Filter. Considering its young age, Packet Filter never ceases to impress me. This tutorial is heavily based on the PF User's Guide.

The firewall will be set up to control access to:

  1. the internet
  2. a private lan
  3. a dmz

The table of contents:

  1. hardware and software requirements
  2. explaining the network, its services, and packet flow
  3. order of events and basic operating system configuration
  4. introduction to packet filter
  5. ruleset - part I (block by default)
  6. ruleset - part II (basic web access)
  7. intermission: configuring nat
  8. ruleset - part III (security)
  9. ruleset - part IV (more security)
  10. intermission: using pflogd
  11. ruleset - part V (more services)
  12. ruleset - part VI (add the dmz)
  13. intermission: more on pfctl
  14. ruleset - part VII (final config with comments)
  15. last words
  16. advertisement

As you can see, the practical approach I speak of is based on setting up the firewall in stages instead of providing a ready-made configuration file.


hardware and software requirements

You will need a system to act as firewall, a system to act as a lan client, and a system to act as a dmz server. The firewall must have internet connectivity and contain three network interfaces.

My firewall system will be running on OpenBSD 3.4, my lan client will be Windows 2000 Pro, and my dmz server will be a Linux system.

My firewall achieves internet connectivity via PPPoE running over an ADSL modem. For instructions on setting this up see my pppoe/adsl tutorial on the subject. This tutorial will use a similar approach so you may want to familiarize yourself with the aforementioned doc before continuing. I will also not be explaining how to configure a nic; how to set up routing tables; and kernel configuration/builds. I have other tutorials on those topics:

The firewall will house a DNS recursive (or caching) nameserver running Bind 9. This means that the lan clients will directly go to the firewall to resolve names instead of using my ISP's servers. Go to my Introduction to DNS tutorial on how to accomplish this.

There will be a web server that will be publicly available. This is achieved through a dynamic DNS arrangement (tutorial found here). The web server will initially be placed on the lan and then migrated to the dmz.

Those of you who do not have enough hardware for a dmz and a third network adapter can still benefit as I begin with a two system config and add the third leg afterwards.


explaining the network, its services, and packet flow

Here are the roles each system will play:

We will be controlling packet flow at two of the firewall's interfaces: the external (internet) side and the lan side. Depending on the context, these interfaces may be referred to as:

A consequence of filtering at two points is that for every packet that wishes to flow through the firewall two filter rules are required. For a packet passing only one of these points (such as a public address contacting our dmz server) only one rule is required. Some people may say that filtering on two interfaces is not needed and makes the ruleset larger for no good reason. Well, I like the idea of locking two doors to my place and, believe me, this arrangement is great for educative purposes (which is why we're here).

We also have the dc0 OpenBSD interface (and its corresponding address macro DMZ_firewall) but, again, we will not be filtering on it.

Note: You most certainly have different interfaces - so adjust accordingly. The interface tun0 is a virtual interface OpenBSD sets up when connecting via ppp so that, at least, should be common to your setup. The actual interface associated with tun0 happens, for me, to be ne3.

We can explain the desired packet flow using the denoted legs of the firewall (1, 2, and 3):

In addition, a lan client will be permitted to contact the firewall's internal interface (ne0) via SSH for administrative purposes and the firewall will be permitted to contact a lan server (again from ne0) via SCP/SSH for backup purposes.

Other than the specifically allowed requests and responses, packet flow is blocked by default. This is known as a deny by default stance.


summary of events and basic operating system configuration

I will begin by organizing the initialization of key components on the firewall into three stages:

  1. System is booted. IP forwarding (routing) is initialized via /etc/sysctl.conf.
  2. pppd is activated via /etc/netstart and the system receives its address from ISP.
  3. Packet Filter (including network address translation) and the PF logging daemon (pflogd) are turned on via /etc/ppp/ppp.linkup.

I will now provide some configuration through the editing of key files:

/etc/sysctl.conf
Turn on IP forwarding:

net.inet.ip.forwarding=1

For real time activation:

# /sbin/sysctl -w net.inet.ip.forwarding=1

/etc/netstart
This is what I appended to this file:

echo -n "Trying to establish PPPoE DSL"; ppp -ddial pppoe
for i in 10 9 8 7 6 5 4 3 2 1 0; do
    sleep 5
    echo -n "$i"
    if /usr/local/sbin/adsl-status>/dev/null; then
        break
    fi
done

/etc/ppp/ppp.conf
Here is a ppp config file that works for me. Your mileage may vary:

default:
 set log Phase Chat LCP IPCP CCP tun command
 set redial 15 0
 set reconnect 15 10000

pppoe:
 set device "!/usr/sbin/pppoe -i ne3"
 disable acfcomp protocomp
 deny acfcomp
 set mtu max 1492
 set speed sync
 enable lqr
 set lqrperiod 5
 set cd 5
 set dial
 set login
 set timeout 0
 set authname ********
 set authkey ********
 add! default HISADDR
 enable dns
 enable mssfixup

/usr/local/sbin/adsl-status
This script displays some useful information onscreen during boot time:

#!/bin/sh

IP=$(netstat -rn | grep tun0 | grep ^[0-9] | awk '{print$2}')

if [ -z "$IP" ]; then
   echo "ADSL link is down."
   exit 1
else
   echo "ADSL is up, IP address is $IP"
   exit 0
fi

/etc/ppp/ppp.linkup
Normally you would start pf via /etc/rc.conf. Well we can't do that because at boot time our PPPoE interface, tun0, does not yet have an IP address bound to it. Instead, we launch PF and pflogd via the pppd mechanism.

MYADDR:
 ! sh -c "/sbin/pfctl -e -f /etc/pf.conf"
 ! sh -c "/sbin/ifconfig pflog0 up"
 ! sh -c "/sbin/pflogd -d 5 -f /var/log/pflog"

{enable PF and load everything inside the specified configuration file}
{enable the PF logging device}
{log to the specified file with a delay of five seconds}

Note that logging occurs only for those filter rules containing the special "log" keyword. More on this later.

/etc/ppp/ppp.linkdown
We turn off PF when we disconnect:

MYADDR:
 !bg /sbin/pfctl -d

Note: I haven't found a way to disable pflogd without resorting to killing it using a pid. There is not much harm in keeping it running.

/etc/pf.conf
Here is a rudimentary PF config file. It allows all packets to pass in and out on all interfaces (packets are not inhibited in any way). The nat line permits lan clients residing on the 192.168.1.0 network to access the internet. It is assumed that the clients have the firewall host as their default gateway:

nat on tun0 from 192.168.1.0/24 to any -> tun0
pass all

That's it for the summary of events and how to configure the operating system. Keep in mind that you, dear reader, are responsible for routing, network adapter configuration, and ensuring your kernel has forwarding (routing) and filtering (firewall) capabilities. You should be fine with the default OpenBSD kernel.


introduction to packet filter

According to the PF man page:

The pf packet filter drops, passes and modifies packets according to the rules defined in this file [pf.conf]. For each packet inspected by the filter, the set of rules is evaluated from top to bottom, and the last matching rule decides what action is performed.

The concept of "last matching rule wins" is critical to developing a PF ruleset. Even if a packet matches a rule PF will continue evaluating as it may match another rule. It is the last matching rule that is applied to the packet. The corollary to this is that every rule is inspected by PF for every packet hitting any interface.

Some more facts about pf:

Before going on to building a ruleset we will examine common rule syntax (the '|' denotes an OR):

pass|block in|out on int from src_ip port src_port to dst_ip port dst_port

If one wants to apply a rule to all interfaces, and to all sources, and to all destinations, and to all ports the powerful all keyword should be employed:

pass|block in|out on all

Going even further, if one wants to include both directions as well we can use this form:

pass|block all

Let us see how to use the in and out keywords. They are relative to the packet source and the firewall machine itself.

For example, if we want to block packets requesting a particular port (say 6020) from leaving the firewall's external interface we could do:

block out on tun0 all port 6020 {the source is inside the firewall so we block it from going out}

The source port is not specified so any port is assumed.

Another example. Say we want a lan client (192.168.1.50) to be able to access the internal interface (192.168.1.51) via SSH, we could do:

pass in on ne0 from 192.168.1.50 to 192.168.1.51 port 22 {the source is outside the firewall so we pass it in}

General terms like "incoming" and "outgoing" mean, respectively, that outside packets are entering the firewall and that inside packets are exiting the firewall. Packets may be entering or exiting either side of the firewall. The terms "in" and "out" do not correspond to any "place" in particular. For instance, the corresponding "response" rule to the above would be:

pass out on ne0 from 192.168.1.51 to 192.168.1.50 port 22

Earlier I mentioned that PF will examine each and every rule for every packet passing an interface. There are at least three ways in which this does not hold true:

  1. quick keyword
    Using the quick keyword in a rule commands PF to apply the rule immediately. Further rule processing is abandoned.

    I will be employing this method.

  2. keeping state
    One of Packet Filter's important abilities is "keeping state" or "stateful inspection". Stateful inspection refers to PF's ability to track the state, or progress, of a network connection. By storing information about each connection in a state table (
    which can be examined), PF is able to determine if a packet passing through the firewall belongs to an already established connection. If it does, it is passed through the firewall without going through any ruleset evaluation. This feature is invoked by using the keep state or modulate state keywords in a rule. This state matching is based on the packet's TCP sequence numbers. Keeping state is also possible with ICMP and UDP but in a different way.

    I will be employing this method.

  3. skip steps scheme
    When a ruleset is loaded the following parameters are compared between successive rules (in this order):
    1. interface
    2. protocol
    3. source address
    4. source port
    5. destination address
    6. destination port

    PF determines how many successive rules (call it N) have the same parameter. If a packet does not match a rule then PF will not bother to inspect the following N rules with the same parameter.

    For example, if you are filtering on 2 interfaces (as we will be doing) and you manage to have all rules associated with one interface first and then all those associated with the second interface after then you will be chopping your inspection time in half. You can continue by organizing all "first-interface" rules by protocol, say TCP. In this way, we further optimize our ruleset.

    I will be taking advantage of this scheme.

We will now begin building our ruleset by editing pf.conf. Each upcoming section will involve a more complex configuration. Remember that every time you modify pf.conf you must reload the rules:

# pfctl -Rf /etc/pf.conf

To see a listing of your rules:

# pfctl -s rules

ruleset - part I (deny by default)

As explained, we will begin by ignoring the dmz. For now, our web server will reside on the lan.

# define the two interface macros
EXT = "tun0"
LAN = "ne0"

# define some address macros
LAN_server = "192.168.1.53"
LAN_firewall = "192.168.1.36"
LAN_clients = "192.168.1.0/24"

# expire state connections early
set optimization aggressive

# translate lan client addresses to that of EXT
nat on $EXT from $LAN_clients to any -> $EXT

# block everything from entering EXT
block in on $EXT all

# block everything from exiting EXT
block out on $EXT all

# block everything from entering LAN
block in on $LAN all

# block everything from exiting LAN
block out on $LAN all

Notes:

  1. We begin to use variables, or macros, to facilitate maintenance of the file
  2. We group by interfaces to take advantage of the skip steps scheme.
  3. We block packets from entering and exiting both interfaces.
  4. Since I'm using PPPoE, an aggressive state option has been chosen. This reduces the firewall's load but may disconnect idle connections early. Other options are:
    • normal (keep connections alive longer)
    • high-latency (keep connections alive even longer than that)
    • conservative (keep connections alive for as long as possible)
  5. Our firewall is airtight at this point.

ruleset - part II (basic web access)

We now add rules to allow internet hosts to access our web server and to allow lan clients to access internet web servers. This includes the ability for lan clients to perform DNS lookups. For now, our web server lies in our lan but later we will move it to the dmz (ruleset - part VI).

EXT = "tun0"
LAN = "ne0"

LAN_server = "192.168.1.53"
LAN_firewall = "192.168.1.36"
LAN_clients = "192.168.1.0/24"

set optimization aggressive

nat on $EXT from $LAN_clients to any -> $EXT

block in on $EXT all

# allow HTTP requests from the internet to enter EXT
# in order to contact our web server (keep state on this connection)
pass in on $EXT \
	proto tcp \
	from any to $LAN_server \
	port 80 \
	keep state

block out on $EXT all

# allow UDP requests to port 53 from firewall to exit EXT
# in order to contact internet nameservers (keep state on this connection)
pass out on $EXT \
	proto udp \
	from $EXT to any \
	port 53 \
	keep state

# allow HTTP requests from lan clients to exit EXT
# (after natting is performed) in order to contact internet web servers
# (keep state on this connection)
pass out on $EXT \
	proto tcp \
	from $EXT to any \
	port 80 \
	keep state

block in on $LAN all

# allow UDP requests to port 53 from lan clients to enter LAN
# in order to perform dns queries on the firewall (keep state on this connection)
pass in on $LAN \
	proto udp \
	from $LAN_clients to $LAN_firewall \
	port 53 \
	keep state

# allow HTTP requests from lan clients to enter LAN
# in order to contact internet web servers (keep state on this connection)
pass in on $LAN \
	proto tcp \
	from $LAN_clients to any \
	port 80 \
	keep state

block out on $LAN all

# allow HTTP requests from the internet to exit LAN
# in order to contact our web server (keep state on this connection)
pass out on $LAN \
	proto tcp \
	from any to $LAN_server \
	port 80 \
	keep state

Notes:

  1. We begin to abbreviate (ex: firewall external interface: EXT; firewall internal/lan interface: LAN)
  2. Notice how request rules must be in pairs. This is because we are filtering on both interfaces. For the initial lan client UDP request we go directly to LAN_firewall because that address is configured as DNS server on the clients.

intermission: configuring nat

Internet clients will never be able to reach our web server unless we reconfigure NAT. We need to tell NAT to redirect any incoming requests to port 80 to our internal web server. This is required because internet hosts only have a DNS name to go by. And that name is associated with the IP address bound to the external interface of the firewall. So the destination of these http requests will always be the firewall. We need to edit /etc/pf.conf:

nat on $EXT from $LAN_clients to any -> $EXT
rdr on $EXT proto tcp from any to any port 80 -> $LAN_server port 80

Now we reload our NAT rules only:

# pfctl -Nf /etc/pf.conf

This is how we can view the current nat rules:

# pfctl -s nat

ruleset - part III (security)

We become more security conscious and take into account the following:

  1. packet fragmentation attacks
  2. IP spoofing
  3. TCP sequence number prediction attacks
  4. we begin to differentiate between IPv4 and IPv6 packets
  5. lists are used for the first time

Address "spoofing" is where a malicious user fakes the source IP address in packets they transmit in order to either hide their real address or to impersonate another node on the network. Once the user has spoofed their address they can launch a network attack without revealing the true source of the attack or attempt to gain access to network services that are restricted to certain IP addresses. We will see how to block such attacks whose source is both the internet and the internal lan (yes, you can't trust anybody).

EXT = "tun0"
LAN = "ne0"

LAN_server = "192.168.1.53"
LAN_firewall = "192.168.1.36"
LAN_clients = "192.168.1.0/24"

# define some non-routeable addresses used in spoof attacks originating from the internet
PRIVATE_BLOCKS = "{
	   127.0.0.0/8
	   192.168.0.0/16
	   172.16.0.0/12
	   10.0.0.0/8
	 }"

set optimization aggressive

# normalize packets to prevent fragmentation attacks
scrub in on $EXT all

nat on $EXT from $LAN_clients to any -> $EXT
rdr on $EXT proto tcp from any to any port 80 -> $LAN_server port 80

# immediately prevent IPv6 traffic from entering or leaving all interfaces
block in quick inet6 all
block out quick inet6 all

# immediately prevent packets with invalid addresses from entering or exiting EXT (anti-spoofing measure)
block drop in quick on $EXT inet from $PRIVATE_BLOCKS to any
block drop out quick on $EXT inet from any to $PRIVATE_BLOCKS

# prevent lan originated spoofing from occurring
antispoof for $EXT inet

block in on $EXT all

pass in on $EXT \
	inet proto tcp \
	from any to $LAN_server \
	port 80 \
	flags S/AUPRFS modulate state

block out on $EXT all

pass out on $EXT \
	inet proto udp \
	from $EXT to any \
	keep state

pass out on $EXT \
	inet proto tcp \
	from $EXT to any \
	port 80 \
	flags S/AUPRFS modulate state

block in on $LAN all

pass in on $LAN \
	inet proto udp \
	from $LAN_clients to $LAN_firewall \
	keep state

pass in on $LAN \
	inet proto tcp \
	from $LAN_clients to any \
	port 80 \
	flags S/AUPRFS modulate state

block out on $LAN all

pass out on $LAN \
	inet proto tcp \
	from any to $LAN_server \
	port 80 \
	flags S/AUPRFS modulate state

Notes:

  1. The scrub keyword is employed to defragment (re-assemble) packets. This assists in deflecting IP fragmentation attacks. Such attacks consist of deliberately fragmenting packets that contain malicious data in order to avoid detection by other installed security tools (NIDS). This type of attack relies on the fact that some NIDS cannot re-assemble fragments and, therefore, are unable to detect the attack patterns. Using this (scrub) directive uses a non-trivial amount of resources. Here we scrub all packets passing EXT in either direction.
  2. We prevent sequence number prediction attacks by using unpredictable sequence numbers to maintain state ("keep state" replaced by "modulate state")
  3. We ensure all initial TCP requests have only the SYN flag set (out of the possible six flags). Some applications require other flags set so your mileage may vary. My setting is the most restrictive.
  4. We see the appearance of the inet and inet6 keywords. They tell PF that a particular rule applies either to IPv4 or IPv6 packets.
  5. The quick keyword is utilized to stop all further rule evaluation if the line containing this keyword is a positive match.
  6. RFC 1918 addresses will be blocked from entering or exiting the external interface. These addresses should never appear on the internet, and filtering them will ensure that the router does not "leak" these addresses out from the internal network and also block any incoming packets with a source address in one of those networks.

    These address blocks are named using a list. When you utilize a pair of parentheses to create a group of items you are using a list.

  7. Note that block drop is used to tell PF not to respond with a TCP RST or ICMP Unreachable packet. Since the RFC 1918 addresses don't exist on the Internet, any packets sent to those addresses will never make it there anyways. The quick option is used to tell PF not to bother evaluating the rest of the filter rules if one of the above rules matches; packets to or from the $PRIVATE_BLOCKS networks will be immediately dropped.
  8. The special antispoof keyword is used. This line expands to the following:
    	block drop in on ! ne0 inet from 192.168.1.0/24 to any
    	block drop in inet from 192.168.1.36 to any
    The first line blocks all traffic coming from the 192.168.1.0/24 network that does not pass in through ne0. Since the 192.168.1.0/24 network is on the ne0 interface, packets with a source address in that network block should never be seen coming in on any other interface.

    The second line blocks all incoming traffic from 192.168.1.36, the IP address on ne0. The host machine should never send packets to itself through an external interface, so any incoming packets with a source address belonging to the machine can be considered malicious.

ruleset - part IV (more security)

We continue to securitize our setup by:

  1. setting up logging for all block rules
  2. preventing invalid UDP and TCP requests from timing out
EXT = "tun0"
LAN = "ne0"

LAN_server = "192.168.1.53"
LAN_firewall = "192.168.1.36"
LAN_clients = "192.168.1.0/24"

PRIVATE_BLOCKS = "{
	   127.0.0.0/8
	   192.168.0.0/16
	   172.16.0.0/12
	   10.0.0.0/8
	 }"

set optimization aggressive

scrub in log on $EXT all

nat on $EXT from $LAN_clients to any -> $EXT
rdr on $EXT proto tcp from any to any port 80 -> $LAN_server port 80

block in log quick inet6 all
block out log quick inet6 all

block in log quick on $EXT inet from $PRIVATE_BLOCKS to any
block out log quick on $EXT inet from any to $PRIVATE_BLOCKS

antispoof for $EXT inet

block in log on $EXT all

# preventing invalid internet UDP and TCP requests from timing out
block return in log on $EXT proto { udp, tcp } all

pass in on $EXT \
	inet proto tcp \
	from any to $LAN_server \
	port 80 \
	flags S/AUPRFS modulate state

block out log on $EXT all

pass out on $EXT \
	inet proto udp \
	from $EXT to any \
	keep state

pass out on $EXT \
	inet proto tcp \
	from $EXT to any \
	port 80 \
	flags S/AUPRFS modulate state

block in log on $LAN all

pass in on $LAN \
	inet proto udp \
	from $LAN_clients to $LAN_firewall \
	keep state

pass in on $LAN \
	inet proto tcp \
	from $LAN_clients to any \
	port 80 \
	flags S/AUPRFS modulate state

block out log on $LAN all

pass out on $LAN \
	inet proto tcp \
	from any to $LAN_server \
	port 80 \
	flags S/AUPRFS modulate state

Notes:

  1. The all important logging feature is now in use via the log keyword. Logging is accomplished by the pflogd daemon which must be enabled. I have activated logging on all block rules but this is not necessary. Use it judiciously as this feature consumes resources. Logging can be applied to any rule.
  2. Important: when using log and state keywords in the same rule: only the packet that establishes the state is logged.
  3. The return keyword is used when doing standard blocking. This new blocking rule is tagged onto the main external interface blocking rule for incoming TCP and UDP packets (other protocols will be taken care of by the first main rule). They prevent invalid requests from timing out (which tells the sender that a packet filter is in use). Notice the use of a list in this rule.
  4. They can also be viewed as being "nice" to valid requests as a "no such service" reply is sent back immediately thereby preventing hanging.
  5. This blocking pair can be also placed after the other 3 main blocking rules (IN on LAN; OUT on EXT; OUT on LAN) as well but the one we used (IN on EXT) is obviously the most important in this context.
  6. One plus is that internet scanning software that look for Windows machines on the vulnerable udp ports of 137 and 139 will not tag you as a possible victim. Hence you will be pestered less and your logs will not fill up as fast.

intermission: using pflogd

For debugging purposes, pflogd is your friend. Now that you know how to turn on logging for a particular rule, if something doesn't work the way you think it should, you can view what rule is blocking or passing. To view logs that display the block/pass rules (including rule numbers) proceed in this manner:

# tcpdump -n -e -q -ttt -r /var/log/pflog

For more info on controlling tcpdump, see my tcpdump tutorial.

Once you determine the rule number that is blocking (or passing) a packet you are interested in, you can identify the rule itself by viewing the current rules:

# pfctl -s rules

Note: there is always a delay between a request and being able to view its corresponding log entry. The way I started pflogd in ppp.linkup above specifies a delay of 5 seconds but I believe the default is 60 seconds.

To view log activity in real-time you access the virtual log device. In ppp.linkup I initialized this device but to make sure it is "up" use the ifconfig command:

# ifconfig pflog0

To see the logging:

# tcpdump -n -e -q -ttt -i pflog0

Besides having the wide array of stock tcpdump options to use the OpenBSD version of tcpdump offers some extensions for viewing pflogd (or pflog0) output:

  • ip - address family is IPv4
  • ip6 - address family is IPv6
  • on int - packet passed through the interface int
  • rulenum num - the filter rule that the packet matched was rule number num
  • action act - the action taken on the packet. Possible actions are pass and block
  • reason res - the reason that action was taken. Possible reasons are match, bad-offset, fragment, short, normalize, and memory
  • inbound - packet was inbound
  • outbound - packet was outbound

For example:

# tcpdump -n -e -q -ttt -X -s 300 -i pflog0 dst port 135 and rulenum 15

Here I am interested in those packets that match my rule #15 ("block return in log on tun0 proto tcp all") associated with destination port 135. I used the standard switches to view the payload in ASCII and to capture the first 300 bytes of payload.

These logging techniques are indispensable for your journey with PF. Use them often.

One final note with using logs is that the "keep state connections" will only have their initial connection displayed. Use the keyword log-all to have everything show up.

Here is some sample log output. In the last section I explained the reality of scans on port 137 (EXT is 78.67.203.27):

Nov 04 11:01:06.375376 rule 14/0(match): block in on tun0: 213.97.87.49.60894 > 78.67.203.27.137:  udp 50
Nov 04 11:03:34.544924 rule 14/0(match): block in on tun0: 67.68.72.112.1033 > 78.67.203.27.137:  udp 50
Nov 04 11:11:48.603993 rule 14/0(match): block in on tun0: 67.68.72.112.1031 > 78.67.203.27.137:  udp 50
Nov 04 11:13:09.885771 rule 14/0(match): block in on tun0: 212.67.97.93.1028 > 78.67.203.27.137:  udp 50
Nov 04 11:14:12.669611 rule 14/0(match): block in on tun0: 193.152.239.129.1028 > 78.67.203.27.137:  udp 50
Nov 04 11:19:10.379094 rule 14/0(match): block in on tun0: 61.59.24.66.1027 > 78.67.203.27.137:  udp 50
Nov 04 11:19:58.950987 rule 14/0(match): block in on tun0: 208.213.98.86.11838 > 78.67.203.27.137:  udp 50
Nov 04 11:20:36.345201 rule 14/0(match): block in on tun0: 195.175.123.248.1025 > 78.67.203.27.137:  udp 50
Nov 04 11:22:20.750593 rule 14/0(match): block in on tun0: 148.246.131.64.1029 > 78.67.203.27.137:  udp 50
Nov 04 11:23:23.155869 rule 14/0(match): block in on tun0: 145.254.131.19.1026 > 78.67.203.27.137:  udp 50
Nov 04 11:24:07.358401 rule 14/0(match): block in on tun0: 210.222.144.157.1029 > 78.67.203.27.137:  udp 50
Nov 04 11:26:13.416570 rule 14/0(match): block in on tun0: 61.171.14.11.1026 > 78.67.203.27.137:  udp 50
Nov 04 11:38:55.697822 rule 14/0(match): block in on tun0: 193.108.54.86.1025 > 78.67.203.27.137:  udp 50
Nov 04 11:41:25.331719 rule 14/0(match): block in on tun0: 129.7.6.75.1034 > 78.67.203.27.137:  udp 50
Nov 04 11:42:04.580059 rule 14/0(match): block in on tun0: 148.223.74.12.1029 > 78.67.203.27.137:  udp 50
Nov 04 11:43:41.376028 rule 14/0(match): block in on tun0: 67.68.72.112.1030 > 78.67.203.27.137:  udp 50
Nov 04 11:44:11.549906 rule 14/0(match): block in on tun0: 200.67.248.54.1113 > 78.67.203.27.137:  udp 50
Nov 04 11:45:30.511827 rule 14/0(match): block in on tun0: 67.68.72.112.1032 > 78.67.203.27.137:  udp 50
Nov 04 11:48:59.728881 rule 14/0(match): block in on tun0: 200.60.206.219.1047 > 78.67.203.27.137:  udp 50
Nov 04 11:50:01.493447 rule 14/0(match): block in on tun0: 62.83.213.211.1025 > 78.67.203.27.137:  udp 50
Nov 04 11:50:57.058665 rule 14/0(match): block in on tun0: 209.42.37.151.1028 > 78.67.203.27.137:  udp 50
Nov 04 11:57:06.124479 rule 14/0(match): block in on tun0: 67.68.72.112.1031 > 78.67.203.27.137:  udp 50
Nov 04 11:59:20.850800 rule 14/0(match): block in on tun0: 61.59.219.206.1025 > 78.67.203.27.137:  udp 50
Nov 04 12:01:04.879562 rule 14/0(match): block in on tun0: 200.167.234.72.1192 > 78.67.203.27.137:  udp 50

That's one hour's worth. I bet you can't wait to get scanned like this. Note that this is not the total logs for that hour. Below is the command I used to gain this output (I then selected one hour's worth from that):

# tcpdump -qn -ttt -e -r /var/log/pflog port 137 and host 78.67.203.27

I think you can guess which rule is number 14 but for the sake of completeness:

# pfctl -vvs rules | grep @14
@14 block return in log on tun0 proto udp all

Go to dshield.org to get an overview of attacked ports on a worldwide scale.


ruleset - part V (more services)

Let us now add more service access:

  1. allow a lan admin system to administer the firewall via SSH
  2. allow the firewall to backup its data on the lan server via SCP/SSH
  3. allow lan clients to send and retrieve email over the internet
  4. allow lan clients to access usenet and SSL web pages
  5. enable a TCP SYN Proxy
EXT = "tun0"
LAN = "ne0"

LAN_server = "192.168.1.53"
LAN_firewall = "192.168.1.36"
LAN_clients = "192.168.1.0/24"
LAN_admin = "192.168.1.63"

PRIVATE_BLOCKS = "{
	   127.0.0.0/8,
	   192.168.0.0/16,
	   172.16.0.0/12,
	   10.0.0.0/8,
	 }"

# define some service macros
LAN_to_INT_services = "{ www, https, ssh, smtp, pop3, nntp }"
INT_to_LAN_services = "{ www }"
LAN_to_FW_services = "{ ssh }"
FW_to_LAN_services = "{ ssh }"

set optimization aggressive

scrub in log on $EXT all

nat on $EXT from $LAN_clients to any -> $EXT
rdr on $EXT proto tcp from any to any port 80 -> $LAN_server port 80

block in log quick inet6 all
block out log quick inet6 all

block in log quick on $EXT proto tcp all flags FUP/FUP

block drop in log quick on $EXT inet from $PRIVATE_BLOCKS to any
block drop out log quick on $EXT inet from any to $PRIVATE_BLOCKS

antispoof for $EXT inet

block in log on $EXT all
block return in log on $EXT proto { udp, tcp } all

pass in on $EXT \
	inet proto tcp \
	from any to $LAN_server \
	port $INT_to_LAN_services \
	flags S/AUPRFS modulate state

block out log on $EXT all

pass out on $EXT \
	inet proto udp \
	from $EXT to any \
	keep state

pass out on $EXT \
	inet proto tcp \
	from $EXT to any \
	port $LAN_to_INT_services \
	flags S/AUPRFS modulate state

block in log on $LAN all

pass in on $LAN \
	inet proto udp \
	from $LAN_clients to $LAN_firewall \
	keep state

pass in on $LAN \
	inet proto tcp \
	from $LAN_clients to any \
	port $LAN_to_INT_services \
	flags S/AUPRFS modulate state

# lan admin connects to firewall via ssh for administrative purposes
pass in on $LAN \
	inet proto tcp \
	from $LAN_admin to $LAN_firewall \
	port $LAN_to_FW_services \
	modulate state

block out log on $LAN all

pass out on $LAN \
	inet proto tcp \
	from any to $LAN_server \
	port $INT_to_LAN_services \
	flags S/AUPRFS synproxy state

# firewall connects to the lan server via scp/ssh for backup purposes
pass out on $LAN \
	inet proto tcp \
	from $LAN_firewall to $LAN_server \
	port $FW_to_LAN_services \
	modulate state

Notes:

  1. We apply the synproxy state option to our web server to prevent spoofed SYN floods. Normally when an internet host initiates a TCP connection to a server behind the firewall PF will pass the handshake packets between the two endpoints as they arrive. PF has the ability, however, to proxy the handshake. With the handshake proxied, PF itself will complete the handshake with the host, initiate a handshake with the server, and then pass packets between the two. The benefit of this process is that no packets are sent to the server before the host completes the handshake. This eliminates the threat of spoofed TCP SYN floods affecting the server because a spoofed client connection will be unable to complete the handshake. You should do this for any server that is publicly advertised on the net.
  2. The synproxy state option includes the functionality of the modulate state option previously described.

ruleset - part VI (add the dmz)

The time has come to add our dmz:

  1. the web server migrates to the dmz (DMZ_server)
  2. internet client HTTP request rules changed to reflect web server migration
  3. allow lan clients to contact the dmz server via SSH
  4. allow lan admin to ping/traceroute any hosts (firewall, dmz server, and internet hosts)
EXT = "tun0"
LAN = "ne0"

LAN_server = "192.168.1.53"
DMZ_server = "192.168.3.37"
LAN_firewall = "192.168.1.36"
LAN_clients = "192.168.1.0/24"
LAN_admin = "192.168.1.63"

PRIVATE_BLOCKS = "{
	   127.0.0.0/8,
	   192.168.0.0/16,
	   172.16.0.0/12,
	   10.0.0.0/8,
	 }"

LAN_to_INT_services = "{ www, https, ssh, smtp, pop3, nntp }"
LAN_to_FW_services = "{ ssh }"
LAN_to_DMZ_services = "{ www, ssh }"
INT_to_DMZ_services = "{ www }"
FW_to_LAN_services = "{ ssh }"

set optimization aggressive

scrub in log on $EXT all

nat on $EXT from $LAN_clients to any -> $EXT
rdr on $EXT proto tcp from any to any port 80 -> $DMZ_server port 80

block in log quick inet6 all
block out log quick inet6 all

block drop in log quick on $EXT inet from $PRIVATE_BLOCKS to any
block drop out log quick on $EXT inet from any to $PRIVATE_BLOCKS

antispoof for $EXT inet

block in log on $EXT all
block return in log on $EXT proto { udp, tcp } all

# allow TCP requests from internet clients to enter EXT
# in order to access pre-defined DMZ services
pass in on $EXT \
	inet proto tcp \
	from any to $DMZ_server \
	port $INT_to_DMZ_services \
	flags S/AUPRFS synproxy state

block out log on $EXT all

pass out on $EXT \
	inet proto udp \
	from $EXT to any \
	keep state

pass out on $EXT \
	inet proto tcp \
	from $EXT to any \
	port $LAN_to_INT_services \
	flags S/AUPRFS modulate state

# allow ICMP requests from firewall to exit EXT (after natting is performed)
# in order to ping/traceroute internet hosts on the behalf of lan admin
pass out on $EXT \
        inet proto icmp \
        from $EXT to any \
        icmp-type 8 \
        keep state

block in log on $LAN all

pass in on $LAN \
	inet proto udp \
	from $LAN_clients to $LAN_firewall \
	keep state

pass in on $LAN \
	inet proto tcp \
	from $LAN_clients to any \
	port $LAN_to_INT_services \
	flags S/AUPRFS modulate state

pass in on $LAN \
	inet proto tcp \
	from $LAN_admin to $LAN_firewall \
	port $LAN_to_FW_services \
	modulate state

# allow TCP requests from lan clients to enter LAN
# in order to access pre-defined DMZ services
pass in on $LAN \
        inet proto tcp \
        from $LAN_clients to $DMZ_server \
        port $LAN_to_DMZ_services \
        modulate state

# allow requests from lan admin to enter LAN
# in order to ping/traceroute any system (firewall, dmz server, and internet hosts)
pass in on $LAN \
        inet proto icmp \
        from $LAN_admin to any \
        icmp-type 8 \
        keep state

block out log on $LAN all

pass out on $LAN \
	inet proto tcp \
	from $LAN_firewall to $LAN_server \
	port $FW_to_LAN_services \
	modulate state

Notes:

  1. Notice how we removed this rule:
    	pass out on $LAN \
    		inet proto tcp \
    		from any to $LAN_server \
    		port $INT_to_LAN_services \
    		flags S/AUPRFS modulate state
    That's because the web server was on the lan before and we were filtering on the lan interface. The web server is now on the dmz and we're not filtering on the dmz interface we therefore do not need a "pass out on $DMZ" line. As long as incoming internet requests to the dmz are passed by the $EXT rule we let them by the dmz interface unrestricted. In principle that is what the dmz is for (it should only contain services we want the public to access). Remember, whatever is not blocked is passed by default.
  2. The rdr line was edited to reflect the migration.
  3. Naturally we transfer over the synproxy state option to the $DMZ_server line.
  4. It is important to realize that the dmz is on a separate subnet. Therefore routing must be set up accordingly (could be as simple as setting up the proper default gateway).

intermission: more on pfctl

info table (filter information)
This provides us with data not related to any one interface. Call this type of data "global". View filter information in this way:

# pfctl -s info

Here is some sample output:

Status: Enabled for 0 days 03:58:47             Debug: None

State Table                          Total             Rate
  current entries                        0
  searches                           12386           0.9/s
  inserts                              168           0.0/s
  removals                             168           0.0/s
Counters
  match                               8244            0.6/s
  bad-offset                             0            0.0/s
  fragment                               0            0.0/s
  short                                  0            0.0/s
  normalize                              3            0.0/s
  memory                                 0            0.0/s

We can also insert the set loginterface option in the PF configuration file to specify what interface to collect further statistics on. I chose tun0 (via the macro EXT), the firewall's external interface. This data collection continues until PF is disabled. The info table can also be flushed manually to start afresh. Here is the new addition to pf.conf (including the initial set option):

set optimization aggressive
set loginterface $EXT

Once we make this change we can have PF see them by telling it to load only the options configuration:

# pfctl -Of /etc/pf.conf

If you want to make an option like this temporary you may include it at the console. Currently there is no dedicated switch to do this so you will have to do something like this:

echo "set loginterface tun0" | pfctl -O -f -

Our info table now becomes:

Status: Enabled for 0 days 08:02:09             Debug: None

Interface Stats for tun0              IPv4             IPv6
  Bytes In                          619683                0
  Bytes Out                         215867                0
  Packets In
    Passed                            1239                0
    Blocked                            200                0
  Packets Out
    Passed                            1184                0
    Blocked                              0                0

State Table                          Total             Rate
  current entries                        0
  searches                            9733           0.3/s
  inserts                              126           0.0/s
  removals                             128           0.0/s
Counters
  match                               7254            0.3/s
  bad-offset                             0            0.0/s
  fragment                               0            0.0/s
  short                                  0            0.0/s
  normalize                              0            0.0/s
  memory                                 0            0.0/s

Note: In the next release (3.5) it will be possible to collect statistics on all interfaces. This feature is presently available in the -current branch of 3.4.

labels
To collect data on individual filter rules we append the label keyword and then a string to a rule. For instance, say I'm interested in the statistics on the lan to internet services. I would need to modify the appropriate rule:

pass out log-all on $EXT \
	inet proto tcp \
	from $EXT to any \
	port $LAN_to_INT_services \
	flags S/AUPRFS modulate state \
	label lan_to_int

To view the statistics on this rule:

# pfctl -s labels

Which gives:

lan_to_int 18 418 253300
lan_to_int 13 0 0
lan_to_int 13 0 0
lan_to_int 13 0 0
lan_to_int 13 60 3631
lan_to_int 13 40 20571

Given above is the rule label and then 3 columns. The three data columns represent evaluations of the rule, packets passed/blocked by the rule, and bytes passed/blocked by the rule.

If you look at those six lines it is not clear at all what they are telling us. I got some feedback from a reader on this (thanks Aaron Linville) and it was mentioned that we can essentially "label the label" by adding a few special macros. Making the line look like this clarifies the subsequent output considerably:

label "lan_to_int - $proto:$dstport ->"

This provides us with the following:

lan_to_int - tcp:80 -> 18 418 253300
lan_to_int - tcp:443 -> 13 0 0
lan_to_int - tcp:22 -> 13 0 0
lan_to_int - tcp:25 -> 13 0 0
lan_to_int - tcp:110 -> 13 60 3631
lan_to_int - tcp:119 -> 13 40 20571

Much better. Check the pf.conf man page for other macros you can use with labels.

To clear rule stats:

# pfctl -z

These rule stats are also cleared/flushed when the ruleset is reloaded.

state table
When I introduced the ability of PF to be stateful (keep state; modulate state; or synproxy state) I said that before any rules are evaluated, the filter checks whether the packet matches any state. Well this "check" is made against the state table that can also be examined by us:

# pfctl -s state

Here is some sample output:

tcp 192.168.1.36:22 <- 192.168.1.40:2213       ESTABLISHED:ESTABLISHED
tcp 192.168.3.37:445 <- 192.168.1.53:21095       TIME_WAIT:TIME_WAIT
tcp 192.168.3.37:139 <- 192.168.1.53:46701       FIN_WAIT_2:FIN_WAIT_2

flushing
Flushing is like cleaning the slate. Contents of the flushed object is reduced to zero. The info table, the state table, the filter rules, and the nat rules can be flushed:

# pfctl -F info
# pfctl -F state
# pfctl -F rules
# pfctl -F nat
# pfctl -F all

The last command flushes all four.

There are other entities that can be examined and flushed but we will not look at them in this tutorial.

starting and stopping PF
As we saw in the beginning when looking at the ppp files (ppp.linkup and ppp.linkdown) this is how we start and stop PF:

# pfctl -ef /etc/pf.conf
# pfctl -d

ruleset - part VII (final config with comments)

Here is the final, liberally commented, Packet Filter configuration file:



# * * * * * * * * * * * * * * * * * * * * * * * * * * * *
#
# first conceived:      Oct 18, 2002
# last updated:         Feb 16, 2004
# author:               Peter S. Matulis (Montreal, Quebec, Canada)
#
# * * * * * * * * * * * * * * * * * * * * * * * * * * * *

#
#       * NETWORK *
#
#     +-----+              +----------+                +----------+
#     | Hub |--------- ne0 | firewall | ne3/tun0 ------| Internet |
#     +-----+              +----------+                +----------+
#     | |                      dc0
#     | +-- Client A            |
#     +---- More clients        |
#                               |
#                               |
#                          +---------+
#                          |   DMZ   |
#                          +---------+
#

#
#       * INTERFACES *
#
#       ne3/tun0 -> dynamically assigned
#       dc0 -> 192.168.3.36
#       ne0 -> 192.168.1.36
#

#
#       * LOGGING *
#
#       Logging (to /var/log/pflog) is activated via the 'log' keyword.
#       The logs are in tcpdump input file format and must be viewed as such:
#
#               # pflogd -d 5 -f /var/log/pflog (delay of 5 seconds)
#               # tcpdump -neq -ttt -r /var/log/pflog
#
#       To display real-time logging we access the block device:
#
#               # ifconfig pflog0 up
#               # tcpdump -neq -ttt -i pflog0 &
#

#
#       * VARIABLES *
#


# define two interface macros
EXT = "tun0"
LAN = "ne0"

# define some address macros
LAN_server	= "192.168.1.53"
DMZ_server	= "192.168.3.37"
LAN_firewall = "192.168.1.36"
LAN_clients = "192.168.1.0/24"
LAN_admin = "192.168.1.63"

# define some non-routeable address blocks
PRIVATE_BLOCKS = "{
	   127.0.0.0/8,
	   192.168.0.0/16,
	   172.16.0.0/12,
	   10.0.0.0/8,
	 }"

# define some service macros
LAN_to_INT_services	= "{ ftp, www, https, ssh, smtp, pop3, nntp }"
LAN_to_FW_services	= "{ ssh }"
LAN_to_DMZ_services	= "{ www, ssh }"
INT_to_DMZ_services	= "{ www }"
FW_to_LAN_services	= "{ ssh }"

#
#       * OPTIONS *
#

# expire state connections early and collect statistics on EXT
set optimization normal
set loginterface $EXT

#
#       * NORMALIZATION *
#

# normalize packets to prevent fragmentation attacks
scrub in on $EXT all

#
#       * NAT *
#

# translate lan client addresses to that of EXT
# and redirect internet HTTP requests to dmz server
nat on $EXT from $LAN_clients to any -> $EXT
rdr on $EXT proto tcp from any to any port 80 -> $DMZ_server port 80

#
#       * RULES *
#

# immediately prevent IPv6 traffic
# from entering or leaving all interfaces -- log matches
block in log quick inet6 all
block out log quick inet6 all

# immediately prevent (and silently drop) packets with invalid addresses
# from entering or exiting EXT -- log matches
block drop in log quick on $EXT inet from $PRIVATE_BLOCKS to any
block drop out log quick on $EXT inet from any to $PRIVATE_BLOCKS

# prevent lan originated spoofing from occurring
antispoof for $EXT inet

# block everything from entering EXT -- log matches
block in log on $EXT all

# prevent invalid internet UDP and TCP requests from timing out -- log matches
block return in log on $EXT proto { udp, tcp } all

# allow TCP requests from internet clients to enter EXT
# in order to access pre-defined DMZ services
pass in on $EXT \
	inet proto tcp \
	from any to $DMZ_server \
	port $INT_to_DMZ_services \
	flags S/AUPRFS \
	synproxy state

# block everything from exiting EXT -- log matches
block out log on $EXT all

# allow UDP requests to port 53 from firewall to exit EXT
# in order to contact internet nameservers
pass out on $EXT \
	proto udp \
	from $EXT to any \
	port 53 \
	keep state

# allow TCP requests from firewall to exit EXT (after natting is performed)
# in order to contact internet servers on the behalf of lan clients
pass out on $EXT \
	inet proto tcp \
	from $EXT to any \
	port $LAN_to_INT_services \
	flags S/AUPRFS modulate state

# allow ICMP requests from firewall to exit EXT (after natting is performed)
# in order to ping/traceroute internet hosts on the behalf of lan admin
pass out on $EXT \
	inet proto icmp \
	from $EXT to any \
	icmp-type 8 \
	keep state

# block everything from entering LAN -- log matches
block in log on $LAN all

# allow UDP requests to port 53 from lan clients to enter LAN
# in order to perform dns queries on the firewall
pass in on $LAN \
	proto udp \
	from $LAN_clients to $LAN_firewall \
	port 53 \
	keep state

# allow TCP requests from lan clients to enter LAN
# in order to access pre-defined internet services
pass in on $LAN \
	inet proto tcp \
	from $LAN_clients to any \
	port $LAN_to_INT_services \
	flags S/AUPRFS \
	modulate state

# allow TCP requests from the lan admin to enter LAN
# in order to access pre-defined firewall services
pass in on $LAN \
	inet proto tcp \
	from $LAN_admin to $LAN_firewall \
	port $LAN_to_FW_services \
	modulate state

# allow TCP requests from lan clients to enter LAN
# in order to access pre-defined DMZ services
pass in on $LAN \
	inet proto tcp \
	from $LAN_clients to $DMZ_server \
	port $LAN_to_DMZ_services \
	modulate state

# allow ICMP requests from the lan admin to enter LAN
# in order to access firewall, dmz server, and internet hosts
pass in on $LAN \
	inet proto icmp \
	from $LAN_admin to any \
	icmp-type 8 \
	keep state

# block everything from exiting LAN -- log matches
block out log on $LAN all

# allow TCP requests from firewall to exit LAN
# in order to access pre-defined services on lan server
pass out on $LAN \
	inet proto tcp \
	from $LAN_firewall to $LAN_server \
	port $FW_to_LAN_services \
	modulate state

advertisement


last words

If you want to test your setup with a remote host you can get yourself a free shell account. They do not tend to offer many utilities but lynx is good enough to attempt to reach your web server.

Most lan-firewall setups will require some kind of arrangement to deal with FTP exchanges. I have a separate tutorial on this subject. Here it is: Firewalling active and passive FTP clients with ftp-proxy and Packet Filter.

We have come to the end of the tutorial. I hope you enjoyed it.


This document was brought to you by:

Click here to access this document's forum.



Copyright © 2004 Peter Matulis
All Rights Reserved