Introduction to Packet Filter
Introduction
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.
Installation
FreeBSD
Insert the following entries in /etc/rc.conf
:
OpenBSD
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.
Configuration
We'll look at several examples to learn how to use it.
Macros & Lists
Macros
We can define macros to replace variables that are used frequently (IPs, interfaces...):
Lists
Lists allow us to group criteria into a variable:
This line allows us to execute 4 rules at once! For beginners, this line will block:
- 192.168.0.1 on port 80
- 192.168.0.1 on port 22
- 10.8.0.6 on port 80
- 10.8.0.6 on port 22
Mix of Macros and Lists
To mix everything:
Tables
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:
And now, if we want to add or remove IPs:
To delete everything:
NAT
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
:
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:
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
- a network interface
- a Table or a List
- 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:
That's it, it's not the same as with Iptables!
Packet Redirections
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:
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:
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
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.
Here's the syntax:
- 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:
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).
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.
Usage
The binary for using pf is pfctl. Before starting, you need to enable PF at the kernel level. 2 solutions:
or add pf=YES to /etc/rc.conf.local
.
To disable pf, simply do:
If you want to do a syntactic analysis of the file without loading the filtering rules, use the -n argument:
If you want to optimize PF, add the -O option, which will remove duplicates and reorder rules.
On-the-fly Modifications
- -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:
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:
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:
- -v, -vv, -g, -x, -q: for more verbose modes, and even debug mode (-v -> -x). The -q will put it back in quiet mode.
Practical Examples
Simple
Requirements:
- We have a machine connected to the internet via a network interface with a fixed IP called bge0
- It must be accessible from outside via 3 trusted IPs
- The Apache server must be accessible to everyone
- It must respond to pings from the outside
- We want to be able to access the internet from this machine
- We will silently block all other packets
Now we activate our new configuration:
This configuration is quite restrictive in the sense that generally, we put all instead of "from any to $iface" and we use "quick" extensively.
Advanced
Requirements:
- Our gateway (renton) has 2 interfaces, r10 on the internet side and ne3 on the local network side
- The first card has a fixed IP (that of our ISP)
- The second interface on the local network side is configured with the address 192.168.1.1 on the local network in 192.168.1.0/24
- We will protect all of this from the outside world, and since we're paranoid, we'll do a bit of logging
- We want to be able to access the internet from our local network without limitations
- A particular machine (diane) must be accessible via SSH and HTTPS
- We want to access Free Multipost on the machine (sickboy), it's just a standard RTSP/UDP stream
- Our main machine (tommy) would like to make clicka-compliant things like Jabber file transfer work
- We will allow SSH from the outside
My Config
I have a Soekris with several network interfaces:
- Wan
- DMZ
- Lan
- Wifi
- VPN
And I want the configuration to be as follows:
- Wifi and Lan have access to everything
- The DMZ only has access to Wan
- There is NAT on all interfaces except wan obviously
- Protection against SSH bruteforce
- VPN is accessible on the gateway from the outside
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 |
|
Logs
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:
We can check that everything is working properly:
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:
It will directly read what's happening live on pflog0, so pflogd will be useful.
- Passive:
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:
Finally, tcpdump also understands PF configuration syntax. So we can ask it things like:
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.
Advanced PF Functions
If you want to block Windows 95/98 machines for example:
To get the list of OS that PF recognizes:
To do OS analysis with tcpdump:
Obviously these techniques are CPU intensive and not infallible.
FAQ
no IP address found for tun0
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:
And if parentheses already exist, try to remove them.