Packet Filter (or PF) is the official software firewall for OpenBSD, originally written by Daniel Hartmeier. It is a free Open Source software.
It replaced Darren Reed’s IPFilter since OpenBSD version 3.0, due to licensing issues and also Reed’s systematic refusal to incorporate code modifications from OpenBSD developers.
It has been ported to DragonflyBSD 1.2 and NetBSD 3.0; it is provided as standard on FreeBSD (version 5.3 and later).
A free port of PF has also been created for Windows 2000 and XP operating systems by the Core FORCE community. However, this port is only a personal firewall: it does not implement PF functions that allow NAT or the use of ALTQ.
pf_enable="YES"# Load PFpf_rules="/etc/pf.conf"# Rules, default path.pf_flags=""# Bonuspflog_enable="YES"# start loggingpflog_logfile="/var/log/pflog"# where to find the logpflog_flags=""# Bonus
Vive OpenBSD :-), no need to install anything, it’s included by default in the system. Just configure and enable it. To configure it, edit the /etc/pf.conf file.
Tables allow you to store a large number of addresses (50 or 50,000, it’s the same), which are then used directly in filtering/NAT/redirection rules. Searching for an address in a memory table is much faster and less CPU/memory intensive than searching through a set of rules each corresponding to a value in an address list.
There are several keywords for tables with different functions:
cont: used when you want the table to not be modifiable
persist: tells PF not to delete a table that isn’t referenced by a rule
The advantage of a non-const table compared to lists is that you can add/remove addresses or subnets on-the-fly, useful for temporarily blocking a spammer’s address, a script-kiddie or managing redirection to a set of high-availability servers.
Finally, as the icing on the cake, you can initialize a table with a file containing a list of addresses:
1
2
3
table <privateip> const { 192.168.0.0/24, 10.8.0.0/8 }table <spammers> persist file "/etc/spammers"block in on $if_ext from { <privateip>, <spammer> } to any
I won’t explain here what NAT does, but how to use it with PF. First, we activate packet forwarding by adding this to /etc/sysctl.conf:
1
net.inet.ip.forwarding=1
This will make the NAT persistent. Remember that packets will pass through the packet filter after being modified, unless the pass keyword is used. This also applies to RDR which we’ll see below. Here is a NAT rule:
1
nat [pass] on interface [address_family] from src_addr [port src_port] to dst_addr [port dst_port] -> ext_addr
If we break it down (in brackets: variable, italic: optional):
nat: indicates that this is a nat rule
pass: the packet is NATed and sent directly without going through the packet filter
on interface: the packet arrived on this network interface ($if_ext, ne0…)
address_family: inet or niet6, this is a detail that could be important
from src_addr: the packet comes from this address. For the address, you can specify many things:
an IP address
a CIDR
a DNS that will be resolved by PF when loading the rules
any of these notations, preceded by a ! to signify negation
finally any for any address
port src_port: if you want to NAT only a certain port or range of ports…rarely used
to dst_addr: the packet is destined for this address. Same possibilities as for src_addr
-> ext: replace the source address with this address. The return will be handled automatically. And if this address changes (assigned by DHCP), you can specify the name of the network interface in parentheses (rl0), and the address will be automatically updated in the rule.
Now a small example. If you want to share your internet connection with your local network:
RDR is NAT’s hidden little brother, in that it does exactly the opposite: it takes packets coming from the outside to redirect them to the local network. Here’s an example of syntax:
1
rdr pass on interface [address_family] from src_addr [port src_port] to dst_addr [port dst_port] -> int_addr [port int_port]
It’s the same syntax as for NAT with a few exceptions like: I redirect what was destined for dst_addr:dst_port to int_addr:int_port and as with NAT, the return will be handled automatically.
For example, if I want to access the SSH of one of my machines on the local network from the outside:
1
rdr pass on $external proto tcp to port 35422 -> $diane port ssh
And now, you can access the machine in my LAN from outside via my external IP address and port 35422.
You’ll notice that I used ssh as a name. All names present in /etc/services can be used.
Filtering rules are evaluated sequentially from the first to the last (from top to bottom in the rules files used). This means that each packet will be evaluated by each rule, and the last rule matching the packet wins the decision (block or pass). On the contrary, if we use the quick keyword, the evaluation stops as soon as a rule matches the packet. The first (implicit) rule is “let everything through” so that if no rule applies to a packet, it is accepted. This is why the first explicit rule is usually a block all.
action: choose between block or pass. The policy for blocked packets will be drop or return depending on the block-policy option. By default it’s drop.
direction: in or out, if you want to filter incoming or outgoing traffic on the interface. If nothing is specified, the rule will be evaluated for both directions.
log: if this flag is present, we record the decision made by this rule concerning the packet. To analyze this, pflog will be our friend.
quick: I’ve already mentioned this - if this flag is also present and the packet matches the rule, it will no longer be analyzed/manipulated, and the decision made by this rule will be final.
proto protocol: a level 4 protocol, generally tcp, udp or icmp, but we can also encounter any level 4 protocol referenced in /etc/protocols. We can even call it by its little number!
port dst_port: Here you can specify a complex range of ports with operators <, <=, >=, >, <> and :, see man pf.conf.
flags tcp_flags_check/mask: you can specify additional checks on the flags of a TCP packet, for example to handle TCP session openings. We often use flag S/SA which I would translate as “this rule applies to TCP packets which, on the two SYN and ACK flags, only have SYN set”. If both flags are set, the packet will not match the rule. For other flags, RTFM a bit.
state: here we generally use two possibilities:
keep state: used when we want to create an entry in the connection state table when a packet matches the rule, and apply the same policy to subsequent packets taking part in the connection. All these packets are therefore attached to this entry, and we can also check that the TCP packet sequence is respected.
synproxy state is used when we want PF to act as a TCP proxy for establishing a connection. In this case, PF will handle the request in place of the recipient and will only forward the packets to the latter afterwards. No packets are forwarded to the recipient before the client has completed the initial exchange. This technique helps protect the recipient from TCP SYN flood attacks, where a large number of connection openings are requested in order to cause a denial of service.
Here’s a small example to clarify all this:
1
pass in quick on $external inet proto tcp from any to any port { http, https, smtp, imaps } flags S/SA keep state
I allow all TCP/IPv4 packets arriving on the external interface destined for http/https/smtp/imaps ports to pass. I check that these are TCP connection openings, I record their state in the table, and I stop the analysis of these packets at this rule (quick).
1
block in log on $external from { 192.168.0.0/16, 172.16.0.0/12 } to any.
Here, I block packets arriving on the external interface with a private source address, and I log the information of the blocked packet. This helps prevent certain spoofing attacks where spoofed packets are sent in order to mislead network equipment.
Of course, there are still plenty of detailed options and particularities (such as anchors, scrubbing, antispoofing…) that I haven’t mentioned. For more information, refer to the pf.conf documentation.
-T (kill/flush/add/delete/show/test..): used with -t table, allows you to manipulate a table: delete it, empty it, add an address, delete, display it, check if it’s in the table. Example:
1
pfctl -t blocked-hosts -T show
This will display the addresses of all machines that have been added to the blocked-hosts table, declared a little earlier in /etc/pf.conf.
-F (nat/rules/state/Tables/..): resets NAT rules, filtering rules, states of open connections or tables, respectively. Useful if you want to clean up a bit, reset counters or connections, disable NAT, delete all entries from all tables, etc…
-k (host/network): Allows you to kill all entries in the state table concerning connections from a machine/network. If you use this option twice, you delete the states of connections from the first address to the second. Example:
1
pfctl -k 192.168.1.0/24 -k 172.16.0.0/16
This will delete all states of connections between these 2 subnets.
-s modifier: This option allows you to get a lot of information about the status of PF. If you use it with -r, PF will do reverse-dns lookup for the addresses it displays. The most interesting values for modifier are:
rules: display the loaded filtering rules in memory
nat: NAT rules
state: open connections
info: global statistics on PF
all: will display everything PF has to tell us
e.g:
1
pfctl -sr
-v, -vv, -g, -x, -q: for more verbose modes, and even debug mode (-v -> -x). The -q will put it back in quiet mode.
# Our network interfaceiface="bge0"# Trusted machines for SSH connections:trusted_hosts="{ 131.25.4.12, 88.12.74.5, 207.124.20.9 }"# We don't worry about the loopback interface used by several internal services on the machine:set skip on lo
# We enable packet normalization on input. PF will reassemble fragmented packets and perform additional checks on them:match in all scrub (no-df)# By default, we block all packets:block all
# We allow ICMP packets of type "echo request" for pings from outside, and echo reply/time exceeded/destination# unreachable for responses to pings that we had initiated towards the outside:pass in inet proto icmp form any to $iface icmp-type { echoreq, echorep, timex, unreach }# We allow connections to the Apache server and record them in the state table:pass in inet proto tcp from any to $iface port www flags S/SA keep state
# We only allow SSH connections from trusted machines:pass in inet proto tcp from $trusted_hosts to $iface port ssh flags S/SA keep state
# Finally we allow all outgoing traffic:pass out inet proto tcp from $iface to any flags S/SA keep state
pass out inet proto { udp, icmp } from $iface to any keep state
Now we activate our new configuration:
1
pfctl -e -f /etc/pf.conf
This configuration is quite restrictive in the sense that generally, we put all instead of “from any to $iface” and we use “quick” extensively.
# /etc/pf.conf# Network interface declarationext="r10"int="ne3"# Declaration of ports not to logports_not_logged="{ netbios-ssn, microsoft-ds, epmap, ms-sql-s, 5900 }"# Declaration of hosts on my local networkdiane="192.168.1.2"tommy="192.168.1.20"sickboy="192.168.1.21"# Declaration of the port used on the outside to access diane's SSHssh_diane="65322"set skip on lo
match in all scrub (no-df)# First, the main NAT rule for the local network. Here, I don't put "pass" because later I explicitly allow all# outgoing traffic. The :network suffix is used to say "the subnet corresponding to the address of this interface"nat on $ext from $int:network to any -> $ext# Next, the redirection rules. I added the 'pass' keyword to not do additional filtering on these connections,# otherwise I would have had to add the corresponding ports in the filtering rule for traffic from the outside.# The rtsp stream from Free's multiposte is UDP coming from freeplayer.freebox.frrdr pass on $ext protoudp from 212.27.38.253 -> $sickboy# We redirect port 8010 used by Jabber for opening incoming file transfersrdr pass on $ext proto tcp to port 8010 -> $tommy# Finally, we redirect SSH (on the specific port used from the outside) and https to dianerdr pass on $ext proto tcp to port $ssh_diane -> $diane port ssh
rdr pass on $ext proto tcp to port https -> $diane# We enable antispoofing on the external interface to block packets coming from outside trying to fraudulently# use our address to pass through the filterantispoof for$ext# We don't filter packets on the internal interfacepass quick on $int# Now the filtering rules# By default we block and log all packets from the outsideblock in on $ext log all
# We don't want to fill our logs in 5 minutes with the few well-known worms floating around the net. So we block# certain packets without logging themblock in on $ext inet proto tcp from any to any port $ports_not_logged# We allow pings from the outsidepass in on $ext inet proto icmp from any to any icmp-type { chorep, echoreq, timex, unreach }# We allow SSH from the outsidepass in on $ext inet $proto tcp from any to any port ssh flags S/SA keep state
# We allow all outgoing traffic (the NATed from the local network will go through these rules)pass out inet proto tcp from $iface to any flags S/SA keep state
pass out inet proto { udp, icmp } from $iface to any keep state
# $OpenBSD: pf.conf,v 1.37 2008/05/09 06:04:08 reyk Exp $## See pf.conf(5) for syntax and examples.# Remember to set net.inet.ip.forwarding=1 and/or net.inet6.ip6.forwarding=1# in /etc/sysctl.conf if packets are to be forwarded between interfaces.#-------------------------------------------------------------------------# Physical and virtual interfaces definitions#-------------------------------------------------------------------------# Physical Interfaceswan_if="sis0"lan_if="vr0"dmz_if="sis1"wifi_if="vr1"# VLAN Interfacesvlan99_if="vlan99"vlan110_if="vlan110"vlan120_if="vlan120"# TUN Interfacesopenvpn_if="tun0"sshvpn_if="tun1"#-------------------------------------------------------------------------# Networks definitions#-------------------------------------------------------------------------# Wanwan_net="192.168.10.0/24"# Trusted networkslan_net="192.168.0.0/24"vlan99_net="192.168.99.0/24"wifi_net="192.168.200.0/24"# Remote trusted networksopenvpn_net="192.168.20.0/24"openvpn_net_nat="10.0.0.0/24"sshvpn_net="192.168.30.0/24"# DMZdmz_net="192.168.100.0/24"vlan110_net="192.168.110.0/24"vlan120_net="192.168.120.0/24"# OpenVPN Shenzi networkopenvpn_shenzi_net="192.168.90.0/24"#-------------------------------------------------------------------------# IP definitions#-------------------------------------------------------------------------# Router IP interfaceswan_sks_ip="192.168.10.254"dmz_sks_ip="192.168.100.254"vlan110_sks_ip="192.168.110.254"vlan120_sks_ip="192.168.120.254"# Others IPdedibox_ip="x.x.x.x"work_ip="x.x.x.x"freebox_tv_ip="212.27.38.253"# Services IPdmz_mail_ip="192.168.100.3"dmz_web_ip="192.168.110.2"dmz_dns_ip="192.168.100.3"dmz_sftp_ip="192.168.100.6"apt_cacher_ip="192.168.120.2"#-------------------------------------------------------------------------# Ports definitions and options#-------------------------------------------------------------------------# Port descriptionsimaps_ports="143, 993"smtps_ports="25, 465"ssh_ports="22, 222"dns_port="53"webs_ports="80, 443"openvpn_port="1194"proxy_port="3128"apt_cacher_port="3142"mysql_port="3306"git_port="9418"free_multiposte="31336, 31337"# Whitelist / Blacklist tabletable <blacklist> persist
table <whitelist> persist file "/etc/ssh/whitelist"# Do not touch lo interfaceset skip on lo0
#-------------------------------------------------------------------------# Packet Normalization: reassemble fragments#-------------------------------------------------------------------------match in all scrub (no-df)#-------------------------------------------------------------------------# Nat for all internal interfaces#-------------------------------------------------------------------------match out on $wan_if from !($wan_if) nat-to ($wan_if)#-------------------------------------------------------------------------# block in all with no usurpation#-------------------------------------------------------------------------block in log all
block in log quick from urpf-failed
#-------------------------------------------------------------------------# Redirections for incoming connections (wan)#-------------------------------------------------------------------------# From WANpass in on $wan_if proto tcp from any to $wan_if port 25 rdr-to $dmz_mail_ip port 25pass in on $wan_if proto udp from any to $wan_if port $dns_port rdr-to $dmz_dns_ip port $dns_portpass in on $wan_if proto tcp from any to $wan_if port $dns_port rdr-to $dmz_dns_ip port $dns_portpass in on $wan_if proto tcp from any to $wan_if port 80 rdr-to $dmz_web_ip port 80pass in on $wan_if proto tcp from any to $wan_if port 143 rdr-to $dmz_mail_ip port 143pass in on $wan_if proto tcp from any to $wan_if port 222 rdr-to $dmz_sftp_ip port 22pass in on $wan_if proto tcp from any to $wan_if port 443 rdr-to 127.0.0.1 port 443pass in on $wan_if proto tcp from any to $wan_if port 465 rdr-to $dmz_mail_ip port 465pass in on $wan_if proto tcp from any to $wan_if port 993 rdr-to $dmz_mail_ip port 993pass in on $wan_if proto tcp from any to $wan_if port 9418 rdr-to $dmz_web_ip port 9418# Apt-cacher-ngpass in on $vlan120_if proto tcp from any to $vlan120_if port $apt_cacher_port rdr-to $apt_cacher_ip port $apt_cacher_portpass in on $wan_if proto tcp from $dedibox_ip to $wan_if port $mysql_port rdr-to $dmz_web_ip port $mysql_portpass in on $wan_if proto udp from $freebox_tv_ip to $wan_if rdr-to 192.168.0.100
pass in on $wan_if proto tcp from $freebox_tv_ip to $wan_if rdr-to 192.168.0.100
#-------------------------------------------------------------------------# Global Rules pass and block#-------------------------------------------------------------------------# Allow Free Multipostepass in quick on $wan_if proto udp from $freebox_tv_ip to 192.168.0.100
pass out quick on $wan_if proto udp from $freebox_tv_ip to 192.168.0.100
pass in quick on $wan_if proto tcp from $freebox_tv_ip to 192.168.0.100
pass out quick on $wan_if proto tcp from $freebox_tv_ip to 192.168.0.100
# Allow all outgoing from $lan_net, $wifi_net and $openvpn_netpass in on {$lan_if, $vlan99_if, $wifi_if, $openvpn_if, $sshvpn_if} from {$lan_net, $vlan99_net, $wifi_net, $openvpn_net, $sshvpn_net} to any
pass out on {$lan_if, $vlan99_if, $wifi_if, $openvpn_if, $sshvpn_if} from {$lan_net, $vlan99_net, $wifi_net, $openvpn_net, $sshvpn_net} to any
pass out on $wan_if from $wan_net to any
antispoof quick for{$wan_if, $dmz_if, $vlan110_if, $vlan120_if}# block all incoming on lan_if, wifi_if and openvpn_ifblock out log on {$lan_if, $vlan99_if, $wifi_if, $openvpn_if, $sshvpn_if} from { !$lan_if, !$vlan99_if, !$wifi_if, !($openvpn_if), !($sshvpn_if)} to any
#-------------------------------------------------------------------------# VPN Access#-------------------------------------------------------------------------# Allow to access shenzi VEpass out on $openvpn_if from any to $openvpn_shenzi_net#-------------------------------------------------------------------------# Specific ports on dmz#-------------------------------------------------------------------------# DNSpass in on $dmz_if proto tcp from $dmz_dns_ip to $dmz_sks_ip port $dns_portpass in on $dmz_if proto udp from $dmz_dns_ip to $dmz_sks_ip port $dns_port# Apt-cacherpass out on $vlan120_if proto tcp to ($vlan120_if) port $apt_cacher_portpass in on $vlan120_if proto tcp to ($vlan120_if) port $apt_cacher_port# DMZ and Vlan autorisations#pass in on $vlan110_if from $vlan110_net to { !$lan_net, !$wifi_net, !$openvpn_net, !$sshvpn_net, !$vlan120_net, !$vlan110_sks_ip }# Arrive pas a avoir juste any -> 3142pass in on $vlan110_if from $vlan110_net to { !$lan_net, !$wifi_net, !$openvpn_net, !$sshvpn_net, !$vlan110_sks_ip}pass in on $vlan120_if from $vlan120_net to { !$lan_net, !$wifi_net, !$openvpn_net, !$sshvpn_net, !$vlan120_sks_ip}pass in on $dmz_if from $dmz_net to { !$lan_net, !$wifi_net, !$openvpn_net, !$sshvpn_net, !$dmz_sks_ip}#-------------------------------------------------------------------------# Specific Rules pass and block#-------------------------------------------------------------------------# Allow all incoming ICMPpass in on $wan_if proto icmp to any
# Autoblacklist on SSHpass in on $wan_if proto tcp from !<whitelist> to ($wan_if) port {$ssh_ports}\
flags S/SA keep state \
(max-src-conn-rate 3/60, \
overload <blacklist> flush global)pass in on $wan_if proto tcp from <whitelist> to $wan_if port {$ssh_ports} flags S/SA keep state
# block the ssh bruteforce bastardsblock drop in on $wan_if from <blacklist>
pass in on $wan_if proto tcp from <whitelist> port {$ssh_ports}# Allow OpenVPNpass in on $wan_if proto tcp to $wan_if port $openvpn_port# Allow DNS TCP from DMZ (Bind SRV) to secondary (Soekris)#pass in quick on $dmz_if proto tcp from $dmz_dns_ip to $dmz_sks_ip port $dns_port# Allow on wan interface from wan for tcppass out on $wan_if proto tcp to ($wan_if) port {$ssh_ports, $smtps_ports, $imaps_ports, $dns_port, $webs_ports, $git_port, $mysql_port}# Allow on dmz interface from wan for udppass out on $wan_if proto udp to ($dmz_if) port {$dns_port}# Allow all outbound trafficpass out inet from !($wan_if) to any flags S/SA keep state
When PF wants to report something, it will send binary information (to make it more fun, it’s standard PCAP/TCPdump) to a pseudo interface (pflog0), and one of its good friends pflogd will store everything in /var/log/pflog.
First, you need to enable/start the pflogd daemon. Normally it should start automatically if PF is enabled when the machine boots. If not:
1
ifconfig pflog0 up && pflogd
We can check that everything is working properly:
1
ps waux
Now that PF is talking on pflog0, when a packet matches a rule where the log keyword is used, let’s move on to tcpdump. It can be used in 2 modes:
Interactive:
1
tcpdump -n -e -ttt -i pflog0
It will directly read what’s happening live on pflog0, so pflogd will be useful.
Passive:
1
tcpdump -r /var/log/pflog
It will read what has been recorded by pflogd in its output file.
You can also pass an expression to tcpdump for it to filter its output according to specific criteria:
1
tcpdump -ttt -r /var/log/pflog port 80 and host 192.168.1.2
Finally, tcpdump also understands PF configuration syntax. So we can ask it things like:
1
tcpdump -ttt -i pflog0 inbound and action pass and on wi0
With this command, it will only show packets allowed to pass through, logged and incoming on the wi0 interface. tcpdump can also read information such as passive OS fingerprint.
If you get these kinds of messages, it’s because an interface (here tun0) is trying to be initialized with PF, while the associated service (supposed to create this device) hasn’t started yet.
To avoid chaos, just try to put your devices in parentheses (!($vpn_if)) eg:
1
block out quick on {$wifi_if} from { !$lan_if, !$wifi_if, !($vpn_if)} to any
And if parentheses already exist, try to remove them.