> your AI agent picks dependencies from memory; give it dated facts — try starlog.dev ↗ vet your agent's deps ↗ vibe-coding is fine. vibe-importing isn’t. — try starlog.dev ↗ vibe-importing isn’t fine ↗ your agent has never seen your private packages — try starlog.dev ↗ facts for private packages ↗ a linter for the dependencies your AI agent picks — try starlog.dev ↗ a linter for agent deps ↗

Back to Articles

Decoding WiFi Packets from Scratch: Inside the Radiotap and 802.11 Frame Parser

[ View on GitHub ]

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

MAC Layer

Radiotap Layer

Yes

No

All bits processed

Raw WiFi Packet

Monitor Mode

Radiotap Parser

Parse Header

8 bytes

Extract Presence Bitmap

32-bit flags

Field Present?

Bit check loop

Align & Extract Field

TSFT/Flags/Rate/Channel

Skip to Next Bit

802.11 Frame Parser

Decoded Frame Data

Human-readable

System architecture — auto-generated

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.