Using FreeBSD’s BPF device with C/C++
Tags: freebsd, programming, howtos
This is a small HOWTO about the BPF device under FreeBSD. I will show you how to access and configure this device. You will also learn how to send and receive ethernet frames. If you want to see an example for possible BPF uses, you might want to consider taking a look at in medias res. Any C compiler should be able to compile the example code. Thanks to Pedro for pointing out several syntax errors. I am also indebted to Bertrand Petit, who suggested several additions to the text.
Caveat lector: This article essentially represents my knowledge about the BPF device from more than a decade ago. You have been warned.
What is the BPF?
The Berkeley Packet Filter is one of FreeBSD’s most
impressive devices. It provides you full (“raw”) access to your NICs
data link layers, i.e. you are totally protocol-independent. In
general, you should be able to capture and send all packets that arrive
on your network card, even if they are meant to reach other hosts (for
example: if you are using a hub instead of a switch, higher-leveled raw
interfaces will probably discard frames that are not for your MAC
address. The BPF won’t…). To use this really powerful device, you need
a kernel that contains device bpf
. If you don’t know how to
create your own kernel, take a look at the excellent FreeBSD
handbook.
More information about the BPF is readily available via man 4
bpf
.
Creating and configuring a BPF device
In order to create a functional, readable instance of the BPF device, you have to:
- Open
/dev/bpfn
, where n depends on how many other applications are using a BPF - Associate your file descriptor with one network interface
- Set the “immediate mode” so that a call to
read
will return immediately if a packet has been received - Request the BPF’s buffer size
Let’s proceed chronologically. First, we will try to open the next available BPF device:
char buf[ 11 ] = { 0 };
int bpf = 0;
for( int i = 0; i < 99; i++ )
{
sprintf( buf, "/dev/bpf%i", i );
bpf = open( buf, O_RDWR );
if( bpf != -1 )
break;
}
Now we are going to associate it with a specific network device, such as
fxp0
:
const char* interface = "fxp0";
struct ifreq bound_if;
strcpy(bound_if.ifr_name, interface);
if(ioctl( bpf, BIOCSETIF, &bound_if ) > 0)
return(-1);
All’s well at the moment, so let’s enable immediate mode and request the
buffer size. The last point is very important, as the BPF is allowed
to provide you with more than one packet after issuing a call to
read
. If you know the buffer size, you can advance to the
next packet.
int buf_len = 1;
// activate immediate mode (therefore, buf_len is initially set to "1")
if( ioctl( bpf, BIOCIMMEDIATE, &buf_len ) == -1 )
return( -1 );
// request buffer length
if( ioctl( bpf, BIOCGBLEN, &buf_len ) == -1 )
return( -1 );
Reading packets
Now, as we are completely done with the initialization and have a working file descriptor, we want to capture incoming traffic. The good thing about BPF is that you can set up filter rules if you only want to receive specific traffic, such as TCP/IP packets.
In theory, there is no need to do more than making a call to
read
. The resulting buffer contains a bpf_hdr
and following after that, a packet. So one could just do something like
that to convert this buffer into a valid ethernet frame:
frame = (ethernet_frame*) ( (char*) bpf_buf + bpf_buf->bh_hdrlen);
Unfortunately, sometimes the kernel likes to add more than one packet to your buffer. Well, the lazy approach would just read one packet per buffer, and wait for the TCP retransmissions that may arrive. But being lazy is not a good solution. Therefore, we need a loop to read all packets that are in the buffer:
int read_byes = 0;
ethernet_frame* frame;
struct bpf_hdr* bpf_buf = new bpf_hdr[buf_len];
struct bpf_hdr* bpf_packet;
while(run_loop)
{
memset(bpf_buf, 0, buf_len);
if((read_bytes = read(bpf, bpf_buf, buf_len)) > 0)
{
int i = 0;
// read all packets that are included in bpf_buf. BPF_WORDALIGN is used
// to proceed to the next BPF packet that is available in the buffer.
char* ptr = reinterpret_cast<char*>(bpf_buf);
while(ptr < (reinterpret_cast<char*>(bpf_buf) + read_bytes))
{
bpf_packet = reinterpret_cast<bpf_hdr*>(ptr);
frame = (ethernet_frame*)((char*) bpf_packet + bpf_packet->bh_hdrlen);
// do something with the Ethernet frame
// [...]
ptr += BPF_WORDALIGN(bpf_packet->bh_hdrlen + bpf_packet->bh_caplen);
}
}
}
The above loop does the following things:
-
As long as the “distance” between the original
bpf_buf
and the auxiliary pointerptr
is not bigger as the number of bytes actually read… -
…the auxiliary pointer is advanced to the next ethernet frame.
BPF_WORDALIGN
rounds up to the next even multiple ofBPF_ALIGNMENT
. This means that you will jump over all bytes that are used for padding purposes. Hence,bpf_packet
always points to the nextbpf_hdr
structure, always given the fact that there is more than one.
Please note that ethernet_frame
is my own structure used to
describe one ethernet frame (802.3). Read the standard RFCs or use
Wireshark if you want to learn more.
Update (2024-04-04): If you request nanosecond timestamps via
BIOCSTSTAMP
, you need to use bpf_xhdr
instead of bpf_hdr
. When
reading packets, make sure that your buffer is using the size provided
by BIOCGBLEN
or BIOCSBLEN
. You can obtain these values via ioctl
.
Sending (your own) packets
Sometimes, you might want to send your own packets instead of sticking
to the analysis of captured ones. No problem with the BPF. If the BPF is
initialised as aforementioned, sending packets is really no problem at
all. A quick call to write
will do the trick:
write(bpf, frame, bpf_buf->bh_caplen);
In this snippet, bpf
is the BPF’s file descriptor,
frame
is a pointer to an ethernet frame that has a TCP/IP
packet attached (remember the initialization of frame
above?). Of course, this is totally useless, but if you want to write a
little broadcast router or something like that, you could just change
the destination MAC address and write the more or less unchanged frame
plus the payload to the BPF. You won’t have to care about the source MAC
address, as the BPF does that for you (look at the man page and search
for BIOCGHDRCMPLT
if you want to disable this feature).
Ethernet frames
An ethernet frame is the basic structure that is sent through your network cables. You have to use it if you need to access the link layer, i.e. if you want to send your own raw packets. This is how an ethernet frame (802.3, ethernet version 2.0) could look like:
destination hardware (MAC) address [6 bytes] | source hardware (MAC) address [6 bytes] | layer-3 protocol type [2 bytes] | payload [46 - 1500 bytes] | FCS [4 bytes] |
The FCS field is not necessarily needed. The other attributes should be initialised, except the source MAC address (see above for explanation). This is what you should do if you want to send your own packets:
- Prepare one ethernet frame and supply it with the proper values
- Pay particular attention to the
type
field. Otherwise, you might experience errors (for example: IP packets with an ARP type field). - Attach the payload. For an arbitrary TCP/IP packet, you would need:
- IP header
- TCP header
- TCP payload
- Send it!
- For debugging purposes, you should have a network sniffer which will tell you if something went wrong.
Following the given example, your frame could look like this:
01:02:03:04:05:06 | Destination MAC |
01:02:03:04:05:06 | Source MAC |
0x0800 | Type: IP |
IP header | |
TCP header | |
TCP payload |
Conclusion
The BPF clearly is a very powerful thing. If you know something about the underlying network structure, you can do unbelievable things with it. Of course, you do not need to stick to the TCP. For a nice example of using the BPF, take a look at IMR, a man-in-the-middle application that uses ARP and directs traffic between two victim hosts.
Additional information is available through these documents:
- RFC 768 (UDP)
- RFC 793 (TCP)
- RFC 826 (ARP)
- Wireshark’s and tcpdump’s capture files
You could also take a closer look on the additional BPF flags, for
example BIOCGHDRCMPLT
. This flag allows you to fill in the
link level source address of an ethernet frame by yourself, thus
allowing you to create arbitrary spoofed packets that may trick other
hosts in your network.