Skip to content
FreeBSDDeep Dive Published Updated 8 min readViews unavailable

FreeBSD BPF Packet Capture: Filters, Buffers, and Loss-Aware Analysis

Build reliable FreeBSD packet captures by understanding BPF attachment, filter semantics, snap length, buffering, direction, and loss counters.

Packet capture is often treated as a command-line exercise: run tcpdump, reproduce a fault, and inspect the file. A defensible capture is a measurement pipeline. The selected interface determines where packets are observed; a filter decides which packets continue into the capture path; a snapshot length limits bytes retained per packet; kernel and user buffers absorb bursts; and the consumer must keep up. A capture can be syntactically successful and still omit the event that matters.

FreeBSD’s Berkeley Packet Filter, exposed through bpf(4), is a link-layer interface rather than a firewall or a guarantee of wire-level truth. tcpdump(1) normally uses this interface through libpcap. Knowing the boundaries helps distinguish “the packet was not present” from “the observation point, filter, direction, or buffer did not record it.”

Where BPF sees traffic

An application opens a BPF device under /dev/bpf, attaches it to an interface with BIOCSETIF, and reads packet records. Each open descriptor has its own filter. Multiple listeners can attach to one interface and each receives a view of the same packet stream, subject to its filter and capture settings. BPF can see link-layer traffic, including traffic not addressed to the host when promiscuous capture is requested and supported by the interface.

This does not make a host capture equivalent to a tap or switch mirror. Packets can be lost before they reach the capture hook, hardware and drivers can expose different representations, and traffic on another interface or VLAN path can bypass the selected observation point. Outbound packets may be visible, but direction behavior depends on the BPF settings and interface type. The manual documents caveats for direction and loopback behavior on some device classes. Confirm the actual path with a controlled test packet, not an assumption based on the interface name.

Promiscuous mode also deserves precise interpretation. BPF’s manual notes that a listener that did not itself request promiscuous mode may receive promiscuously received packets as a side effect when another listener on the same hardware interface requested it. Apply a capture filter appropriate to the incident, and do not treat a packet appearing in a file as proof that the local host accepted or processed it.

Filters are part of the measurement

tcpdump expressions are compiled into a packet filter and, when supported, executed in the kernel so that uninteresting packets are discarded before userspace reads them. Filtering early reduces copy and processing load, but it creates an evidence boundary: a packet excluded by the expression cannot be recovered from that capture. Check operator precedence, address family, VLAN encapsulation, and the actual link-layer type before narrowing a filter during a live incident.

Start with a readable expression and preserve the expression alongside the capture. For example:

tcpdump -D
tcpdump -ni em0 -s 65535 -B 512 -w /var/tmp/incident.pcap 'host 198.51.100.24 and (tcp port 443 or udp port 443)'

Run capture commands with the privileges required by the host, typically from a root shell or through the site’s configured privilege tool. Replace em0 and the documentation-only address with the real interface and endpoint. -n avoids name resolution during capture, -i selects the observation point, -w writes packet data for later analysis, and -B requests an operating-system capture buffer in KiB. The requested size may be capped or adjusted by the kernel, so treat it as a request, not a guaranteed allocation. The snap length shown is a practical ceiling for ordinary IP packet sizes, not a promise that every link-layer format or unusually large frame fits. Select a snap length that covers the packet bytes needed for the investigation and verify truncation indicators when reading the file.

When uncertain about encapsulation or packet types, take a short, broader capture on a controlled system, then refine the expression offline. The pcap-filter(7) grammar has protocol-qualified primitives and Boolean operators, but not every expression has the same IPv4 and IPv6 behavior. In particular, arithmetic accessors such as tcp[0] are not a portable way to match IPv6 transport headers. Keep filters simple enough to review, and test the exact expression against representative traffic or a saved sample before relying on it.

Snap length and buffer size solve different problems

Snap length is the maximum number of bytes captured from each packet. A short value may retain headers while omitting application payload; that can be ideal for flow diagnosis but useless when the failure depends on a later protocol field. A large value records more evidence per packet and increases memory and storage pressure. It does not increase the rate at which the application can drain records.

The BPF read interface uses fixed-sized buffers. Applications can query the effective read-buffer size with BIOCGBLEN; BIOCSBLEN requests a size before the descriptor is attached to an interface. FreeBSD may return the closest allowable size rather than the exact requested value. A packet larger than the buffer is truncated, and a read(2) buffer of the wrong size can fail with EINVAL. These details matter for custom capture programs. With tcpdump, -B requests the OS capture buffer size; it is a burst cushion, not an unlimited queue and not a substitute for sufficient consumer throughput.

Immediate mode changes latency and batching. With BIOCIMMEDIATE enabled, reads return as packets arrive; otherwise BPF can wait until its kernel buffer fills or a timeout expires. Immediate delivery can help a program that must react quickly, while batching can reduce wakeups. The trade-off should be measured: frequent wakeups may increase CPU use, and a batch-oriented reader may delay visibility by its timeout or buffer-fill interval.

FreeBSD also documents an optional zero-copy buffer mode. It uses two page-aligned, equal-sized user buffers and a generation-counter ownership protocol. A buffer owned by the kernel is unstable; once the kernel transfers it to userspace, the consumer may read it until it acknowledges the generation and returns ownership. Applications must use the documented atomic operations and memory-ordering rules. Processing one buffer for too long can prevent progress even though another buffer exists, so acknowledge completed buffers promptly. This is a specialized interface for software that has measured ordinary reads as a bottleneck, not a default optimization for every capture.

Loss counters are operational evidence

BIOCGSTATS reports bs_recv and bs_drop for a BPF descriptor. The receive count includes packets buffered since the last read. The drop count is specifically packets accepted by that descriptor’s filter but dropped by the kernel because its buffer overflowed. It is not a count of every frame the NIC or switch failed to deliver, and it says nothing about packets excluded by the filter.

Use a bounded capture and inspect the tool’s final packet-capture statistics. On systems where BPF peers are visible to netstat, netstat -B can show capture peer state and counters. Interpret these values within their scope: one process may be draining a different descriptor, and filtered packet totals are not the same as interface-wide wire counters. Compare available BPF, interface, and switch-port counters over the same time window. If the capture reports kernel drops, reduce the filter or snap length only if the lost evidence is not needed, increase buffers within memory limits, write to storage that can sustain the rate, or split analysis across a controlled design. Do not simply declare the capture complete because the process exited cleanly.

A disciplined incident workflow

First record the interface state, link-layer type, addresses, routes, VLANs, and capture host role. Start with a filter that includes the relevant endpoints and both directions. Write to a file, capture a narrow time window, and retain the exact command, FreeBSD version, interface name, wall-clock interval, and final drop statistics. For a long incident, use an explicit rotation strategy and storage budget rather than allowing one file to consume a filesystem.

After capture, read it independently of the live interface:

tcpdump -n -r /var/tmp/incident.pcap 'host 198.51.100.24 and (tcp port 443 or udp port 443)'

Check whether the file contains the expected handshake or test request before drawing conclusions. Confirm that the displayed link-layer format matches the interface and that packet lengths do not show unexpected truncation. Compare timestamps with the application’s clock and any upstream captures; BPF’s timestamp formats are configurable and include monotonic time elapsed since boot, so timestamp values from different systems should not be assumed directly comparable.

For a repeatable test, generate traffic with a known tuple, capture it on the intended interface, then verify direction, encapsulation, snap length, and filter behavior. Repeat after driver, VLAN, offload, or topology changes. A successful syntax check is not enough: acceptance means the test packet appears exactly where expected, the capture file is readable, and loss counters remain within a stated threshold under representative load.

BPF is a powerful way to inspect the traffic a FreeBSD host can observe. It is not an omniscient recorder. Treat interface selection, filter scope, packet truncation, buffering, capture loss, and timestamp interpretation as explicit parts of the evidence chain.

Related:

Sources:

Comments