OpenBSD: 3.4-stable (Jan 29, 2004 - custom kernel)
tutorial updated: Monday, February 16, 2004
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:
The table of contents:
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.
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.
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.
I will begin by organizing the initialization of key components on the firewall into three stages:
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.
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:
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 |
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:
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:
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 |
We become more security conscious and take into account the following:
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:
block drop in on ! ne0 inet from 192.168.1.0/24 to any block drop in inet from 192.168.1.36 to anyThe 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.
We continue to securitize our setup by:
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:
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:
|
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:
|
|
Go to dshield.org to get an overview of attacked ports on a worldwide scale.
Let us now add more service access:
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:
The time has come to add our dmz:
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:
pass out on $LAN \ inet proto tcp \ from any to $LAN_server \ port $INT_to_LAN_services \ flags S/AUPRFS modulate stateThat'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.
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 |
Here is the final, liberally commented, Packet Filter configuration file:
|
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.