DIVERT(4)

πŸ’– DIVERT(4) πŸ’–

okay bestie, pull up a chair, because we're about to talk about the kernel packet diversion mechanism and honestly? it's giving main character energy. this is a page from the Kernel Interfaces Manual, and i'm gonna walk you through every single bit of it, no skips, no shortcuts, periodt. πŸŽ€


✨ NAME ✨

divert β€” the kernel packet diversion mechanism, aka the lil trick where the kernel hands packets to your userspace program like "here bestie, you deal with this" πŸ¦‹


✨ SYNOPSIS ✨

first up, the includes. you need all three, don't @ me:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

then you make the socket like this:

int
socket(PF_INET, SOCK_RAW, IPPROTO_DIVERT);

now, to actually enable support for divert sockets, you gotta drop these lines into your kernel config file:

options IPFIREWALL
options IPDIVERT

OR β€” and this is the glow-up option β€” you can load the driver as a module at boot time instead, by adding these lines to your loader.conf(5) file:

ipfw_load="YES"
ipdivert_load="YES"

literally so easy. pick your fighter. πŸ’…


✨ DESCRIPTION ✨

so divert sockets are basically raw IP sockets but with ✨extra✨. the difference? you can bind them to a specific divert port using the bind(2) system call. and here's the thing β€” the IP address you put in the bind? ignored. completely. only the port number matters, bestie.

a divert socket bound to a divert port will receive all the packets that get diverted to that port by some kernel mechanism (which this page is not gonna specify, iykyk). and plot twist: you can also write packets to a divert port, and when you do, they hop right back into kernel IP packet processing. slay. πŸ”₯

divert sockets are normally used together with FreeBSD's packet filtering implementation and the ipfw(8) program. by reading from and writing to a divert socket, matching packets can get passed through an arbitrary ``filter'' as they travel through the host machine, and you can do special routing tricks and stuff like that. it's giving "custom pipeline". 😌


✨ READING PACKETS ✨

packets get diverted either as incoming'' or outgoing.'' here's the tea:

  • incoming packets are diverted after reception on an IP interface.
  • outgoing packets are diverted before next hop forwarding.

diverted packets can be read unaltered via read(2), recv(2), or recvfrom(2). and with recvfrom(2) specifically, the address you get back is lowkey special:

  • its port is set to some tag supplied by the packet diverter (usually the ipfw rule number)
  • its IP address is set to the (first) address of the interface on which the packet was received β€” but only if the packet was incoming β€” or INADDR_ANY if the packet was outgoing
  • the interface name (if it's defined for the packet) gets placed in the 8 bytes following the address, if it fits

✨ WRITING PACKETS ✨

writing to a divert socket is similar to writing to a raw IP socket β€” the packet gets injected ``as is'' into the normal kernel IP packet processing using sendto(2), and minimal error checking is done. keep that in mind. πŸ™ˆ

packets are distinguished as either incoming or outgoing. here's how:

  • if you use sendto(2) with a destination IP address of INADDR_ANY, the packet is treated as if it were outgoing, i.e. destined for a non-local address.
  • otherwise, the packet is assumed to be incoming and full packet routing is done.

in that incoming case, the IP address you specified must match the address of some local interface, or an interface name must be found after the IP address. if an interface name is found, that interface gets used and the value of the IP address is ignored (other than the fact that it's not INADDR_ANY). this is how you indicate which interface the packet "arrived" on. πŸ’«

normally, packets read as incoming should be written as incoming; same deal for outgoing. and when you're reading and then writing back packets, passing the same socket address that recvfrom(2) gave you, unmodified, to sendto(2) just simplifies everything (see below, bestie).

the port part of the socket address you pass to sendto(2) contains a tag that should be meaningful to the diversion module. in the case of ipfw(8), that tag is interpreted as the rule number after which rule processing should restart. noted. πŸ‘‘


✨ LOOP AVOIDANCE ✨

so when packets get written into a divert socket (via sendto(2)), they re-enter the packet filter at the rule number following the tag given in the port part of the socket address β€” and that's usually already set at the rule number that caused the diversion in the first place (not the next rule, even if there are several at the same number, okay?).

⚠️ if the 'tag' gets altered to indicate an alternative re-entry point, be careful bestie β€” you gotta avoid loops, where the same packet gets diverted more than once at the same rule. that's the danger zone. πŸ’€

✨ DETAILS ✨

buckle up, this is the fine print and it matters:

  • if a packet is diverted but no socket is bound to the port, or if IPDIVERT is not enabled or loaded in the kernel, the packet is dropped. gone. bye. 😭
  • incoming packet fragments which get diverted are fully reassembled before delivery; the diversion of any one fragment causes the entire packet to get diverted. and if different fragments divert to different ports? which port ultimately gets chosen is unpredictable. so like… good luck with that one. πŸ™ƒ
  • note that packets arriving on the divert socket by the ipfw(8) tee action are delivered as-is, and packet fragments do not get reassembled in this case. (different vibe, different rules.)
  • packets are received and sent unchanged, EXCEPT:
    • packets read as outgoing have invalid IP header checksums
    • packets written as outgoing have their IP header checksums overwritten with the correct value
    • packets written as incoming and having incorrect checksums will be dropped
    • otherwise, all header fields are unchanged (and therefore in network order)
  • binding to port numbers less than 1024 requires super-user access, and so does creating a socket of type SOCK_RAW. πŸ’…

✨ ERRORS ✨

writing to a divert socket can return these errors, along with the usual errors possible when writing raw packets:

  • [EINVAL] β€” the packet had an invalid header, or the IP options in the packet and the socket options set were incompatible.
  • [EADDRNOTAVAIL] β€” the destination address contained an IP address not equal to INADDR_ANY that was not associated with any interface.

✨ SEE ALSO ✨

go read these next, bestie, they're all connected:

bind(2), recvfrom(2), sendto(2), socket(2), ipfw(4), ipfw(8)


✨ AUTHORS ✨

this iconic work was brought to you by Archie Cobbs <archie@FreeBSD.org>, Whistle Communications Corp. πŸ‘‘


✨ BUGS ✨

okay so, the real talk section:

  • this is an attempt to provide a clean way for user mode processes to implement various IP tricks like address translation, but ngl it could be cleaner, and it is too dependent on ipfw(8).
  • it is questionable whether incoming fragments should be reassembled before being diverted. for example, if only some fragments of a packet destined for another machine do not get routed through the local machine, the packet is lost. this should probably be a settable socket option in any case. 🫠

and that's the whole page, babe. every flag, every fragment, every checksum. we ate. πŸ’…βœ¨

this gem is from the Debian crew, dated December 17, 2004 β€” old but iconic, fr fr. πŸŽ€πŸ¦‹πŸ’–

πŸ“¦ where this page comes from