PFIL(9)

πŸ’– PFIL(9) β€” the packet filter interface, bestie edition πŸ’–

okay so this is pfil(9) from the Kernel Developer's Manual (section 9, kernel-land, we're in the deep end and we're thriving πŸ’…). it's the whole packet filter interface: registering filtering points, hooking your lil filter functions onto them, and running those hooks on every packet that comes or goes. no cap, it's iconic.


✨ NAME ✨

pfil, pfil_head_register, pfil_head_unregister, pfil_head_get, pfil_add_hook, pfil_add_hook_flags, pfil_remove_hook, pfil_remove_hook_flags, pfil_run_hooks, pfil_rlock, pfil_runlock, pfil_wlock, pfil_wunlock β€” packet filter interface 🫢


🌸 SYNOPSIS 🌸

first, the includes, because of course:

#include <sys/param.h>
#include <sys/mbuf.h>
#include <net/if.h>
#include <net/pfil.h>

now the two hook function types, literally the blueprint of your filter:

typedef int (*pfil_func_t)(void *arg, struct mbuf **mp, struct ifnet *, int dir, struct inpcb);

typedef int (*pfil_func_flags_t)(void *arg, struct mbuf **mp, struct ifnet *, int dir, int flags, struct inpcb);

and here's every single function, verbatim, no edits, we don't do that here:

int
pfil_head_register(struct pfil_head *head);

int
pfil_head_unregister(struct pfil_head *head);

struct pfil_head *
pfil_head_get(int af, u_long dlt);

int
pfil_add_hook(pfil_func_t, void *arg, struct pfil_head *);

int
pfil_add_hook_flags(pfil_func_flags_t, void *arg, int flags, struct pfil_head *);

int
pfil_remove_hook(pfil_func_t, void *arg, struct pfil_head *);

int
pfil_remove_hook_flags(pfil_func_flags_t, void *arg, int flags, struct pfil_head *);

int
pfil_run_hooks(struct pfil_head *head, struct mbuf **mp, struct ifnet *, int dir, int flags, struct inpcb *);

void
pfil_rlock(struct pfil_head *, struct rm_priotracker *);

void
pfil_runlock(struct pfil_head *, struct rm_priotracker *);

void
pfil_wlock(struct pfil_head *);

void
pfil_wunlock(struct pfil_head *);

πŸ¦‹ DESCRIPTION πŸ¦‹

the pfil framework is the way you get a function of yours invoked for every incoming or outgoing packet on a particular network I/O stream. yes bestie, every single one. these hooks are how you build a firewall, or do packet transformations. main character energy fr.

πŸŽ€ registering filtering points

packet filtering points get registered with pfil_head_register().

each filtering point is identified by a key (void *) and a data link type (int) inside the pfil_head structure. packet filters use that key and data link type to look up the filtering point they want to register themselves with. the key is unique to the filtering point πŸ”‘. the data link type is a bpf(4) DLT constant β€” it tells you what kind of header is sitting on the packet at that filtering point.

each filtering point uses a common per-VNET rmlock by default. you can change that by specifying PFIL_FLAG_PRIVATE_LOCK as the flags field in the pfil_head structure.

⚠️ heads up: specifying a private lock can break filters that share the same ruleset and/or state between different data link types. so like, be careful, it's giving consequences.

filtering points can be unregistered with pfil_head_unregister().

πŸ’… adding and removing hooks

packet filters register and unregister themselves with a filtering point using pfil_add_hook() and pfil_remove_hook() respectively. the head gets looked up with pfil_head_get(), which takes the key and data link type that the packet filter expects. filters can also pass an argument that gets handed to the filter whenever it's invoked on a packet.

⭐ what happens when a filter runs

when a filter is invoked, the packet looks exactly as if it β€œcame off the wire”. that means all protocol fields are in network byte order, no exceptions, periodt.

the filter gets called with:

  • its specified argument,
  • the pointer to the pointer to the mbuf containing the packet,
  • the pointer to the network interface the packet is traversing,
  • and the direction the packet is traveling β€” PFIL_IN or PFIL_OUT.

the flags argument will indicate if an outgoing packet is simply being forwarded, with the value PFIL_FWD.

the filter may change which mbuf the mbuf ** argument references πŸ‘€.

the filter returns an error (errno) if the packet processing is to stop, or 0 if processing is to continue. and if the packet processing is to stop β€” it is the responsibility of the filter to free the packet. that one's on you, bestie, nobody's coming to clean up.

πŸ”’ the locking situation

every filter hook is called with the pfil read lock held. all heads use the same lock within the same VNET instance. a packet filter can use this lock instead of its own locking model to improve performance. since pfil uses rmlock(9), pfil_rlock() and pfil_runlock() require a struct rm_priotracker to be passed as an argument. a filter can acquire and release the writer lock via pfil_wlock() and pfil_wunlock() functions. see rmlock(9) for more details πŸ’–


🌈 FILTERING POINTS 🌈

right now, filtering points are implemented for these link types:

  • AF_INET β€” IPv4 packets.
  • AF_INET6 β€” IPv6 packets.
  • AF_LINK β€” Link-layer packets.

πŸ‘‘ RETURN VALUES πŸ‘‘

  • if it works, pfil_head_get() returns the pfil_head structure for the given key/dlt. slay.
  • pfil_add_hook() and pfil_remove_hook() return 0 if successful.
  • if called with the flag PFIL_WAITOK, pfil_remove_hook() is expected to always succeed. πŸ’―
😭 and just so you know: the pfil_head_unregister() function might sleep!

🧚 SEE ALSO 🧚

bpf(4), if_bridge(4), rmlock(9)


πŸ’œ HISTORY πŸ’œ

the pfil interface first appeared in NetBSD 1.3. the pfil input and output lists were originally implemented as <sys/queue.h> LIST structures; however this was changed in NetBSD 1.4 to TAILQ structures. this change was to allow the input and output filters to be processed in reverse order, to allow the same path to be taken, in or out of the kernel. the way that they thought about ordering… obsessed.

the pfil interface was changed in 1.4T to accept a 3rd parameter to both pfil_add_hook() and pfil_remove_hook(), introducing the capability of per-protocol filtering. this was done primarily in order to support filtering of IPv6. πŸ¦‹

in 1.5K, the pfil framework was changed to work with an arbitrary number of filtering points, as well as be less IP-centric. glow up.

fine-grained locking was added in FreeBSD 5.2. pfil lock export was added in FreeBSD 10.0. ✨


πŸ’€ BUGS πŸ’€

when a pfil_head is being modified, no traffic is diverted (to avoid deadlock). this means traffic may be dropped unconditionally for a short period of time. pfil_run_hooks() will return ENOBUFS to indicate this. so like, it's not a bug so much as a vibe, but it IS documented here so now you know πŸ™ˆ


okay and that's the whole page, no notes. this came from the Debian build of the page, dated March 10, 2018 β€” so if you're reading this years later, bestie, that's just how old the lore is. mwah πŸ’‹ stay safe out there in kernel space πŸ«ΆπŸ’–

πŸ“¦ where this page comes from