Decoding WiFi Packets from Scratch: Inside the Radiotap and 802.11 Frame Parser
Hook
Every WiFi packet you've ever sent carries more metadata in its Radiotap header than actual payload data in most acknowledgment frames—and parsing that metadata manually teaches you more about wireless networking than any textbook.
Context
When you need to understand what's really happening on a WiFi network, tools like Wireshark present you with neatly formatted packet dissections. But this abstraction hides the fascinating complexity of how WiFi actually works at the binary level. Between your application data and the radio waves sits a stack of headers: the 802.11 frame structure defining management, control, and data frames, and the Radiotap header that captures per-packet radio metadata like signal strength, channel frequency, and transmission rate.
Most developers never need to parse these structures manually—Scapy and similar frameworks handle it for you. But if you're debugging wireless driver behavior, building custom network monitoring tools, researching WiFi security, or simply want to understand how monitor mode packet captures actually work, you eventually need to read the IEEE 802.11 specification and parse raw bytes yourself. The wifidec repository represents exactly this learning journey: a collection of Python scripts that decode Radiotap headers and 802.11 frames without the safety net of a mature library, forcing direct engagement with protocol specifications and bit-level data structures.
Technical Insight
The core challenge in parsing WiFi packets lies in handling variable-length headers with alignment requirements and optional fields. A Radiotap header starts with a version byte, a pad byte, a 16-bit length field, and a 32-bit presence bitmap indicating which optional fields follow. Each set bit in this bitmap corresponds to a specific field type (like TSFT timestamp, flags, data rate, channel frequency), and fields must be naturally aligned—a 2-byte field starts on an even byte boundary, a 4-byte field on a 4-byte boundary.
Here's what a minimal Radiotap parser structure looks like, handling the presence bitmap and field extraction:
import struct
class RadiotapHeader:
def __init__(self, packet_bytes):
# First 8 bytes: version, pad, length, present flags
self.version, self.pad, self.length, self.present = struct.unpack('<BBHI', packet_bytes[:8])
self.offset = 8
self.fields = {}
# Parse fields based on presence bitmap
if self.present & (1 << 0): # TSFT
self.fields['tsft'] = struct.unpack('<Q', packet_bytes[self.offset:self.offset+8])[0]
self.offset += 8
if self.present & (1 << 1): # Flags
self.fields['flags'] = packet_bytes[self.offset]
self.offset += 1
if self.present & (1 << 2): # Rate (in 500kbps units)
self.fields['rate'] = packet_bytes[self.offset] * 0.5 # Convert to Mbps
self.offset += 1
if self.present & (1 << 3): # Channel
self.offset = (self.offset + 1) & ~1 # Align to 2-byte boundary
freq, flags = struct.unpack('<HH', packet_bytes[self.offset:self.offset+4])
self.fields['channel'] = {'frequency': freq, 'flags': flags}
self.offset += 4
def get_80211_frame(self, packet_bytes):
return packet_bytes[self.length:] # 802.11 data starts after Radiotap
After the Radiotap header, the 802.11 frame begins with a 2-byte Frame Control field that encodes the frame type (management, control, or data), subtype (beacon, probe request, ACK, etc.), and various flags. The next bytes vary dramatically based on frame type: management frames include addressing fields and information elements, data frames carry encrypted payloads, and control frames like ACKs can be as short as 10 bytes total.
Parsing the Frame Control field reveals the frame's purpose:
class Frame80211:
def __init__(self, frame_bytes):
fc = struct.unpack('<H', frame_bytes[:2])[0]
self.version = fc & 0x03
self.type = (fc >> 2) & 0x03
self.subtype = (fc >> 4) & 0x0F
self.to_ds = bool(fc & 0x0100)
self.from_ds = bool(fc & 0x0200)
# Frame type meanings
self.type_name = ['Management', 'Control', 'Data', 'Reserved'][self.type]
# Duration/ID field (2 bytes)
self.duration = struct.unpack('<H', frame_bytes[2:4])[0]
# Address fields vary by frame type
if self.type == 0: # Management
self.addr1 = frame_bytes[4:10] # Destination
self.addr2 = frame_bytes[10:16] # Source
self.addr3 = frame_bytes[16:22] # BSSID
The educational value of wifidec-style parsing emerges when you start seeing patterns in real captures. You notice that beacon frames (management subtype 8) repeat every 102.4ms broadcasting SSID information elements. You observe probe requests (management subtype 4) from devices scanning for known networks, leaking privacy through previously connected SSIDs. You catch retransmissions when the sequence control field shows duplicate sequence numbers, revealing congestion or interference.
This byte-level perspective also exposes the verbosity of WiFi overhead. A simple TCP ACK might carry zero bytes of application data, but it's wrapped in a 20-byte IP header, 20-byte TCP header, 8-byte LLC header, 24-byte 802.11 data header, and a 13-byte Radiotap header—85 bytes of overhead before even considering encryption (which adds another 20+ bytes for WPA2). Understanding this stack makes wireless performance optimization concrete rather than abstract.
Gotcha
The prototype nature of wifidec means you'll encounter incomplete implementations and edge cases that crash the parser. Extended presence bitmaps (when bit 31 is set, indicating another 32-bit bitmap follows) likely aren't handled. Encrypted frame payloads remain opaque blobs without implementing the full WPA2/WPA3 decryption process, which requires capturing the four-way handshake and knowing the pre-shared key. Frame check sequences (FCS) at the end of 802.11 frames may or may not be included depending on your capture interface configuration, breaking length calculations.
More fundamentally, parsing WiFi packets requires monitor mode, which not all wireless adapters support properly—especially built-in laptop WiFi cards. You'll need specific chipsets (like Atheros or Ralink) and appropriate drivers. Even then, you only capture packets on a single channel at a time, missing most network activity unless you implement channel hopping. The scripts also don't handle pcap file formats, so you're limited to live capture processing or need to add your own pcap parsing layer. For anything beyond learning exercises, you'll quickly reach the limits of these prototype scripts and need to migrate to Scapy or write significantly more error handling and protocol coverage.
Verdict
Use if: You're learning WiFi protocol internals and want to understand frame structures through hands-on byte parsing, building custom packet analysis tools where you control the entire pipeline, or need a starting template for experimental monitor mode captures where robustness doesn't matter. Skip if: You need production-ready packet analysis (use Scapy or PyShark instead), you're doing security research requiring reliable parsing of malformed packets, you want comprehensive protocol coverage without implementing it yourself, or you need maintained code with community support and bug fixes. This is a teaching tool and prototype playground, not a dependency for serious projects.