PF Fundamentals of packet filtering with pf

The purpose of this post is to try and clarify a few basic ideas in packet filtering that I'm having trouble reducing to firm principles in practice.

0. PF lives in the kernel and handles all packets as they pass between NI(C)'s and daemons
1. Packets are identified by the NIC of origin and header information (only!!...?)
2. Header information may be changed by translation rules (rdr and nat)
3. Packets are passed or blocked by tallying the influence of matching rules (...somehow)
4. Special factors affect how these rules are tallied (eg. state, quick, set <options>...? )

I've found it hard to get to the root of how the configuration translates to actual behavior.

Some productive nitpicking would be greatly appreciated.
 
with PF is the last matched rule wins unless the quick word is used.

Code:
block in from $IP
pass in from $IP
this allows traffic from the $IP


Code:
block in quick from $IP
pass in from $IP
this doesn't.

When creating a PF ruleset this is the order of the statements options, normalization, queueing, translation, filtering

A state is created for every filtering rule by default. if you don't want if you have to specify that with no state

https://en.wikipedia.org/wiki/Stateful_firewall

pf.conf(5)
 
I'm not allowed to give you direct link to this book but you can find it in internet or buy it yourself it will explain most of the pf

The Book of PF​

ISBN-10: 1-59327-165-4
ISBN-13: 978-1-59327-165-7
 
Keep in mind that FreeBSD's PF is based on PF from OpenBSD 4.8. So any OpenBSD PF features that have been added after that are not included in FreeBSD's PF. Some smaller features have been backported if possible but both PF implementations have diverged quite a bit.
 
The Book of PF does cover FreeBSD to some extent.

I'm not actually sure how useful it is for FreeBSD. I have my copy mostly because it's got a crude drawing by Henning in it. (Which is upsetting, because I'd asked for a *rude* drawing.)
 
The purpose of this post is to try and clarify a few basic ideas in packet filtering that I'm having trouble reducing to firm principles in practice.

0. PF lives in the kernel and handles all packets as they pass between NI(C)'s and daemons
Lives in the kernel. Handles packets in the network stack. Works at the network layer and therefore does not know anything about daemons.
1. Packets are identified by the NIC of origin and header information (only!!...?)
NIC and/or header information, yes
2. Header information may be changed by translation rules (rdr and nat)
There are others. State modulation and traffic normalization come to mind. Consult the pf.conf(5) man page.
3. Packets are passed or blocked by tallying the influence of matching rules (...somehow)
Rules are processed in the order in which they appear in pf.conf. The quick keyword can be used to stop the processing of the rules at the rule with the quick keyword if it matches.

Keep in mind that rules in that file must appear in order by statement type. The required order is explained at the beginning of the pf.conf(5) man page in the "STATEMENT ORDER" section.
4. Special factors affect how these rules are tallied (eg. state, quick, set <options>...? )
Yes, quick affects rule processing. Don't know that state or options do. Do you mean pf options or IP header options?
I've found it hard to get to the root of how the configuration translates to actual behavior.
Best thing to do it try it out and see what happens. Make sure you use the /usr/src/share/examples/ipfw/change_rules.sh script as Obsigna recommends.
 
I'm not allowed to give you direct link to this book but you can find it in internet or buy it yourself it will explain most of the pf

The Book of PF​

ISBN-10: 1-59327-165-4
ISBN-13: 978-1-59327-165-7
It was a good read, but its not exactly PF in a nutshell, only a small part ended up being relevant to my setup.
 
Last edited:
Keep in mind that FreeBSD's PF is based on PF from OpenBSD 4.8. So any OpenBSD PF features that have been added after that are not included in FreeBSD's PF. Some smaller features have been backported if possible but both PF implementations have diverged quite a bit.
SirDice , in your opinion, maybe in the future,the PF port in FreeBSD will be more closer to the OpenBSD port?
 
SirDice , in your opinion, maybe in the future,the PF port in FreeBSD will be more closer to the OpenBSD port?
No. There will be no wholesale import of OpenBSD's pf. FreeBSD's has diverted too much and grown too many features for that. Also, it'd break existing configuration files, and that's a significant pain to inflict on users.
There may be imports of specific features, but I've yet to actually have users point at any specific features they lack. It's always "we want an update" which is too vague to be useful.

There's some more information on the why of that here: https://lists.freebsd.org/pipermail/freebsd-pf/2019-August/009149.html
 
No. There will be no wholesale import of OpenBSD's pf. FreeBSD's has diverted too much and grown too many features for that. Also, it'd break existing configuration files, and that's a significant pain to inflict on users.
There may be imports of specific features, but I've yet to actually have users point at any specific features they lack. It's always "we want an update" which is too vague to be useful.

There's some more information on the why of that here: https://lists.freebsd.org/pipermail/freebsd-pf/2019-August/009149.html
thanks for the explanation, and the link is pretty easy to understand
they are multiple reasons why not...
now I prefer not to "catch up" the OpenBSD version
 
Lives in the kernel. Handles packets in the network stack. Works at the network layer and therefore does not know anything about daemons.

NIC and/or header information, yes

There are others. State modulation and traffic normalization come to mind. Consult the pf.conf(5) man page.

Rules are processed in the order in which they appear in pf.conf. The quick keyword can be used to stop the processing of the rules at the rule with the quick keyword if it matches.

Keep in mind that rules in that file must appear in order by statement type. The required order is explained at the beginning of the pf.conf(5) man page in the "STATEMENT ORDER" section.

Yes, quick affects rule processing. Don't know that state or options do. Do you mean pf options or IP header options?

Best thing to do it try it out and see what happens. Make sure you use the /usr/src/share/examples/ipfw/change_rules.sh script as Obsigna recommends.
Thats a beauty of a response,

0. I guess that within the networking stack, the NIC of origin could be considered just another attribute of the packet. Is it true then that packets will always enter and exit the networking stack through an NI(C)? (or is that only true when forwarding is enabled...)
2. Right, normalization as well.
3. The "quick" keyword bypasses the normal behavior, but I would like to understand normal behavior. Im seeing in the pflog,
Code:
0/0(match)
, which I guess relates to that tallying of matching rules. Im not sure exactly how those rules are tallied but I think I need to before I can trust my config.
4. I meant pf options.

I have read so much about PF that I can't remember where I heard what anymore. Looking back, the PF section in the Freebsd handbook was also quite good.
 
Found this quote relating to claim #3
"Rules are evaluated from top to bottom, in the sequence they are written. For each packet or connection evaluated by PF, the last matching rule in the ruleset is the one which is applied. However, when a packet matches a rule which contains the quick keyword, the rule processing stops and the packet is treated according to that rule." -Freebsd handbook 31.3.2.1. A Simple Gateway with NAT
 
No. There will be no wholesale import of OpenBSD's pf. FreeBSD's has diverted too much and grown too many features for that. Also, it'd break existing configuration files, and that's a significant pain to inflict on users.
There may be imports of specific features, but I've yet to actually have users point at any specific features they lack. It's always "we want an update" which is too vague to be useful.

There's some more information on the why of that here: https://lists.freebsd.org/pipermail/freebsd-pf/2019-August/009149.html
The traffic shaping improvements are not worth it?
 
Thats a beauty of a response,
Thank you.
0. I guess that within the networking stack, the NIC of origin could be considered just another attribute of the packet. Is it true then that packets will always enter and exit the networking stack through an NI(C)? (or is that only true when forwarding is enabled...)
I'm not a kernel hacker so I can't help you with the internals. I do know that you have to have forwarding enabled in order for pf to work.
I have read so much about PF that I can't remember where I heard what anymore. Looking back, the PF section in the Freebsd handbook was also quite good.
I'm more of an experiential learner. I read enough to understand the basics, and then I start trying things. I go back and re-read or read more in-depth when something piques my curiosity, I need to do something beyond the basics, or I encounter one of those "well that's peculiar" moments.
 
Thank you.

I'm not a kernel hacker so I can't help you with the internals. I do know that you have to have forwarding enabled in order for pf to work.

I'm more of an experiential learner. I read enough to understand the basics, and then I start trying things. I go back and re-read or read more in-depth when something piques my curiosity, I need to do something beyond the basics, or I encounter one of those "well that's peculiar" moments.
I've seen a fair bit of peculiar behavior with this one, Id be too ashamed to admit how long I spent trying to track down the pflog field names. Incidentally, do you have and idea what "rule 0/0(match)" means exactly?
 
rule 0/0 is usually the default block rule, in a ruleset that starts with e.g.
Code:
block log all
which is then followed by exceptions to this. You can always find those rule numbers with pfctl -sr -vv and you'll be able to cross-reference those with the output in /usr/sbin/tcpdump -l -s 0 -e -n -i pflog0 (assuming you have log statements on your block rules). P.S.: you can forget about the second /0 part. I don't think I've ever seen anything there.
 
Ahhh, so rule #0 was the last match for that packet. Thats terrific. Maybe the second zero relates to openbsd "match" rules or something freebsd doesnt use?
 
I've not looked at OpenBSD's shaping code. pf currently uses ALTQ, and that allegedly works (I don't use it myself). There have been aspirations to port dummynet (which is what Apple use with their pf version), but that's not an ongoing project as I understand it.

There's several months, if not a year or more, of work in porting OpenBSD's version (for reasons discussed in the mailing list post above). Realistically that's not going to happen. Especially not because I've yet to see someone point to a specific feature in OpenBSD's pf that make their life easier (never mind justify a year of work!).
 
Keep in mind that FreeBSD's PF is based on PF from OpenBSD 4.8. So any OpenBSD PF features that have been added after that are not included in FreeBSD's PF. Some smaller features have been backported if possible but both PF implementations have diverged quite a bit.
Any idea where I can find version overview and differences listed for OpenBSD and FreeBSD?
 
Any idea where I can find version overview and differences listed for OpenBSD and FreeBSD?
I doubt there is such a list/document.
Even for overlapping syntax there are often minor variations, even if they don't affect the actual behaviour.
On OpenBSD everything also got adapted to routing domains and to be aware of those, so essentially *every* command/rule is different for FreeBSD as it doesn't have the rdomain-specific arguments.

I'm using PF on OpenBSD for Routers, Gateways etc especially because routing domains make so many things much easier and safer. You *could* get the same result with FreeBSD and RIBs, but sometimes you'd have to do some weird tricks that will bite anyone (including yourself) if not thoroughly documented. Been there, done that, almost tripped a whole production host over...
On FreeBSD I mostly use PF for Firewalling and Routing specific to Jails (e.g. building whole private networks of several groups of jails within a single host), on Routers/Gateways I've completely switched to OpenBSD, mainly because of routing domains and the excellent OpenBGPd (which sadly often lacks a few versions behind on FreeBSD).

Switching between both PF variants isn't a problem IMHO. Sometimes you'll have to peek into the manpages to remember the correct notation for a seldomly used rule, but the main concept, structure and logic is identical. The differences are more like a minor variation in dialect (if we'd compare it to an actual language).
The already mentioned 'Book of PF' is an excellent introduction to understand the logic and architecture of PF. With this knowledge the variations between Open- and FreeBSD are relatively easy to grasp and can easily be learned on-the-fly by checking the manpages (always check the "examples" section first before reading the whole wall of text for a command/rule!).
 
My FreeBSD pf ruleset will work on an OpenBSD box with syntax change to one word on the FreeBSD outbound rule to "egress" on the OpenBSD ruleset:

My FreeBSD ruleset with OpenBSD change noted at bottom:

Code:
### Macro name for external interface
ext_if = "em0"
netbios_tcp = "{ 22, 23, 25, 80, 110, 111, 123, 512, 513, 514, 515, 6000, 6010 }"
netbios_udp = "{ 123, 512, 513, 514, 515, 5353, 6000, 6010 }"

### Reassemble fragmented packets
scrub in on $ext_if all fragment reassemble

### Default deny everything
block log all

### Pass loopback
set skip on lo0

### Block spooks
antispoof for lo0
antispoof for $ext_if inet
block in from no-route to any
block in from urpf-failed to any
block in quick on $ext_if from any to 255.255.255.255
block in quick log on $ext_if from { 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 255.255.255.255/32 } to any

### Block all IPv6
block in quick inet6 all
block out quick inet6 all

### Block to and from port 0
block quick proto { tcp, udp } from any port = 0 to any
block quick proto { tcp, udp } from any to any port = 0

### Block specific ports
block in quick log on $ext_if proto tcp from any to any port $netbios_tcp
block in quick log on $ext_if proto udp from any to any port $netbios_udp

### FreeBSD - Keep and modulate state of outbound tcp, udp and icmp traffic
pass out on $ext_if proto { tcp, udp, icmp } from any to any modulate state

### OpenBSD - Keep and modulate state of outbound tcp, udp and icmp traffic
#pass out on egress proto { tcp, udp, icmp } from any to any modulate state
 
Back
Top