Skip to content

linkside-cap

Captures one monitor interface with an address filter and truncation per frame type, and writes pcap to stdout. The Mac starts it via SSH and decodes the stream with tshark.

Truncation

Per frame type, no fixed snaplen. The same rules apply to the export of the app (Truncation.swift) and to tools/anonymize.

Frame Kept
Management and control Full
EAPOL-Key Full
EAP Up to and including the EAP type; EAP-Response/Identity in full, so the app can hash the outer identity
Other EAPOL Up to and including the EAPOL header
Encrypted data The MAC header plus the CCMP/GCMP header (8 bytes)
Data without a body (Null, QoS Null) and unencrypted A-MSDU The MAC header only
Other unencrypted data Up to and including the LLC/SNAP header
Extension frames (type 3) The first 24 bytes

The MAC header length follows Frame Control: QoS +2, four addresses +6, +HTC +4. The radiotap header is kept whole; its length is little-endian in bytes 2–3.

Usage

linkside-cap -i capA -f '<filter>' [-B <KiB>]   # live, pcap to stdout
linkside-cap -r in.pcap -f '<filter>'           # offline, same filter and truncation
linkside-cap -d -f '<filter>'                   # show the BPF program
  • -f: a libpcap filter. Without -f every frame passes, still with truncation.
  • -B: ring buffer in KiB, default 16384.
  • Monitor interface, channel and width come from the radio scripts. linkside-cap does not enable monitor mode.

stderr

One line per event, with key=value pairs:

start iface=capA insns=172 kernel_filter=yes usb=5000
stats ts=1760000000.123 recv=812 drop=0 ifdrop=0 written=812 utrunc=0 bad=0
end ts=1760000060.124 recv=48211 drop=0 ifdrop=0 written=48211 utrunc=0 bad=0
Field Meaning
usb Mbit/s of the USB port the radio sits on (5000 for USB 3, 480 for USB 2), from sysfs; absent offline and for a radio not on USB. The app takes a focus on 5 or 6 GHz over USB 2 for a measurement limit (rules, usb-port)
kernel_filter yes: the kernel runs the program and truncates the frames. no: libpcap filters in user space; the truncation is still correct, but the Pi copies every frame in full
recv, drop, ifdrop pcap_stats, cumulative since the start. drop: ring full. ifdrop: drops by the interface
written frames to stdout
utrunc frames truncated only in user space. With kernel_filter=yes this should be 0
bad frames in which the length block read outside the frame; these are not written

stats comes every second. Each line is also a sign of life: after an error writing to stdout or stderr the program stops. It also stops when the reader of stdout is gone, with nothing to write (How it works).

How it works

  • libpcap compiles the filter. Each accepting ret #k becomes a jump to a custom length block that returns the number of bytes to keep (ret a). The kernel runs that program on the socket and truncates each frame before it reaches user space.
  • The same length block then runs once more in user space on each frame (pcap_offline_filter). This keeps the truncation correct when the kernel rejects the program and libpcap falls back to filtering in user space. The length block only reads bytes it keeps itself, so a second run gives the same length.
  • No immediate mode: it would make libpcap select TPACKET_V2, with ring slots of the full snaplen (256 KiB per frame). With TPACKET_V3 the truncated frames sit close together, and the kernel delivers a block at least every 100 ms. The output is flushed after each block; the latency is therefore at most 100 ms.
  • The pcap global header is flushed at the start, before any frame: stdout is a pipe and fully buffered, and a capture whose filter keeps no frame (capB on a channel where the client never goes) would otherwise end without a header when the Mac stops it, an empty file in the recording.
  • It watches stdout beside the radio: a pipe whose reader is gone reports POLLERR, and the capture ends. Over an SSH ControlMaster connection, which the app's first ssh opens with the getting started's ControlMaster auto, sshd closes the reader of stdout when the session's channel closes and keeps that of stderr, so the stats line goes on; without a master the connection closes, and both pipes with it. Nothing else on the Pi ends a session's capture: the claim's end removes the claim file only. Until 2026-11-03 a capture over the master ended with its next frame, and one on a channel where its filter keeps nothing lived on: capB's focus capture after Change capB… to a channel of no BSS it named, 15 minutes after its channel closed, until capB came back to its BSS; of item 041's fifteen CLI sessions that day, ten ended during a search and three left a capture on capA. Measured on sensor-1 on 2026-11-03 with a capture whose filter keeps nothing, started over ssh and its ssh ended with SIGTERM as Subprocess ends it: before, it lived on over the master and was gone within 4 s without one. Now it is gone 3.2 s after SIGTERM either way, as is one whose filter keeps beacons. The ssh itself ends within 0.03 s, and on the Pi the capture ends 0.03 s after its reader goes, so the rest lies between the ssh's end and the pipe's close.
  • Timestamps in nanoseconds.