EAPOL Packets Explained: What They Are and How They're Structured
When a device joins a WPA2 or WPA3 network, the client and the access point exchange a short burst of packets that prove both sides know the password without either side ever putting it on the air. Those packets are EAPOL frames. Every WiFi handshake you have ever captured is a sequence of EAPOL frames, and the security of the entire link rests on how they are built.
This article is a byte-level reference. It covers what EAPOL is, how it is encapsulated differently on wired Ethernet versus 802.11 (the LLC/SNAP layer that a lot of write-ups skip), every protocol version and packet type, the full EAPOL-Key field layout, how to decode the Key Information bitfield, and compact Rust routines for validating an EAPOL-Key frame and identifying which handshake message it is.
Table of Contents
EAP, 802.1X, and EAPOL
Encapsulation: Ethernet II vs 802.11 LLC/SNAP
EAPOL Protocol Versions
EAPOL Packet Types
The EAPOL Header
EAPOL-Key Frame Structure
Decoding the Key Information Bitfield
The Four WPA2 Handshake Messages
Parsing EAPOL-Key Frames in Practice
Why EAPOL Matters for Security Research
BLEShark Nano and EAPOL Capture
EAP, 802.1X, and EAPOL
EAP, 802.1X, and EAPOL are three distinct things.
EAP (Extensible Authentication Protocol) is an authentication framework defined in RFC 3748. It describes how authentication messages such as identity requests, challenges, and results are structured, but it deliberately says nothing about how those messages travel across any particular link. EAP is transport-agnostic on purpose so that the same method (EAP-TLS, EAP-PEAP, and so on) can run over dial-up, wired LAN, or WiFi without change.
IEEE 802.1X is the port-based network access control standard. It defines the roles in an authenticated exchange: the supplicant (the client), the authenticator (the switch or access point), and the authentication server (usually RADIUS). It also defines EAPOL as the mechanism for carrying EAP between the supplicant and the authenticator over a LAN.
EAPOL (EAP over LAN) is that carrier. It is a thin Layer 2 framing that wraps EAP messages, plus a small set of control and key-management message types of its own. The "LAN" in the name is historical; in practice EAPOL today is overwhelmingly a WiFi protocol.
On a WPA2 or WPA3 Personal (PSK) network there is no RADIUS server and no full EAP conversation at all. The only EAPOL traffic is the EAPOL-Key message type, which carries the 4-way handshake. On WPA2/WPA3 Enterprise, a complete EAP method exchange (identity, method negotiation, credential proof) rides inside EAPOL first, and the EAPOL-Key handshake follows to install the session keys. Either way, the byte-level framing below is identical.
Encapsulation: Ethernet II vs 802.11 LLC/SNAP
EAPOL is identified by EtherType 0x888E, but where that EtherType sits depends on the link type.
On wired Ethernet, EAPOL uses Ethernet II framing. The EtherType 0x888E sits directly in the standard EtherType field (bytes 12-13 of the frame), immediately after the destination and source MAC addresses, and the payload follows.
[ Dest MAC 6B ][ Src MAC 6B ][ 0x888E 2B ][ EAPOL ... ]
On 802.11 (WiFi), it does not work that way. An 802.11 data frame does not carry a bare EtherType. Its payload begins with an IEEE 802.2 LLC header followed by a SNAP (Subnetwork Access Protocol) header, per RFC 1042. Only inside SNAP does the 0x888E EtherType appear. So to find EAPOL in a wireless capture you have to walk past the 802.11 MAC header and then match the LLC/SNAP prefix:
DSAP SSAP Ctrl OUI EtherType 0xAA 0xAA 0x03 00 00 00 88 8E
Breaking that down:
- DSAP = 0xAA and SSAP = 0xAA: the Destination and Source Service Access Points. The value 0xAA is the reserved SAP that means "a SNAP header follows." This is the first byte your parser checks.
- Control = 0x03: the LLC control field, indicating an Unnumbered Information (UI) frame. This is connectionless framing with no sequence numbers.
- OUI = 00 00 00: the SNAP Organizationally Unique Identifier. All zeros is the RFC 1042 encapsulation, which tells you the next two bytes are a standard EtherType rather than a vendor-specific protocol ID.
- EtherType = 0x888E: finally, the EAPOL EtherType.
So on 802.11 the exact 8-byte sequence AA AA 03 00 00 00 88 8E is what marks the payload as EAPOL. Miss the SNAP layer and your offsets into the EAPOL body will be wrong by eight bytes.
The offset of that SNAP header is not fixed, because the 802.11 MAC header is not fixed length. A basic data frame with three addresses is 24 bytes. If the frame is a QoS Data subtype (very common on modern networks), a 2-byte QoS Control field is inserted, pushing the payload to offset 26. Frames with an HT Control field or a fourth address (WDS bridging) push it further. Your parser has to compute the header length from the Frame Control field before it can even look for the SNAP header.
graph TD
subgraph MAC["802.11 MAC Header (24B, +2B if QoS)"]
FC["Frame Control
2B"] --> DUR["Duration
2B"]
DUR --> A1["Address 1
6B (RA/DA)"]
A1 --> A2["Address 2
6B (TA/SA)"]
A2 --> A3["Address 3
6B (BSSID)"]
A3 --> SEQ["Sequence Control
2B"]
SEQ --> QOS["QoS Control
0 or 2B"]
end
subgraph SNAP["LLC + SNAP (8B, RFC 1042)"]
DSAP["DSAP
0xAA"] --> SSAP["SSAP
0xAA"]
SSAP --> CTRL["Control
0x03"]
CTRL --> OUI["OUI
00 00 00"]
OUI --> ET["EtherType
0x888E"]
end
subgraph EAP["EAPOL"]
VER["Version
1B"] --> PT["Packet Type
1B"]
PT --> BL["Body Length
2B"]
BL --> PB["Packet Body
e.g. EAPOL-Key"]
end
QOS --> DSAP
ET --> VER
How EAPOL is carried inside an 802.11 data frame. The LLC/SNAP shim between the MAC header and the EAPOL header is what wired Ethernet does not have.
EAPOL runs before IP. There are no IP or UDP headers anywhere in these frames, and the client has no IP address yet when the handshake happens. EAPOL is part of link establishment and completes before DHCP begins.
EAPOL Protocol Versions
The first byte of every EAPOL frame is the protocol version. It maps to the revision of 802.1X that defined the framing:
| Value | Standard | Notes |
|---|---|---|
| 0x01 | 802.1X-2001 | Original. Still emitted by many WPA2 supplicants for maximum compatibility. |
| 0x02 | 802.1X-2004 | Common in WPA2 handshakes. Adds clarifications, no wire-format change to EAPOL-Key. |
| 0x03 | 802.1X-2010 | Introduces MACsec Key Agreement (MKA). Seen on WPA3 and MACsec deployments. |
Do not rely on the version byte to tell you what kind of network you are on. Implementations frequently send 0x01 or 0x02 regardless of their actual capabilities, and a receiver is expected to accept any version it understands. The real key-management details live in the EAPOL-Key body, not the version byte.
EAPOL Packet Types
The second byte is the packet type, which selects what the body contains:
| Type | Name | Purpose |
|---|---|---|
| 0x00 | EAP-Packet | Carries a full EAP message. Used in WPA2/WPA3 Enterprise (EAP-TLS, PEAP, etc.). |
| 0x01 | EAPOL-Start | Supplicant asks the authenticator to begin authentication. |
| 0x02 | EAPOL-Logoff | Supplicant signals it is leaving the authorized state. |
| 0x03 | EAPOL-Key | Carries key-management data. This is the WPA2/WPA3 4-way handshake. |
| 0x04 | EAPOL-Encapsulated-ASF-Alert | Carries ASF alerts to an unauthenticated port. Rarely seen. |
| 0x05 | EAPOL-MKA | MACsec Key Agreement PDU (802.1X-2010). Not used by WiFi PSK. |
| 0x07 | EAPOL-Announcement | Advertises authentication parameters (802.1X-2010). |
For WPA2/WPA3 Personal, every one of the four handshake packets is type 0x03 (EAPOL-Key). If you are filtering for handshakes, the type byte being 0x03 is your primary gate.
The EAPOL Header
Regardless of type, every EAPOL frame opens with the same 4-byte header, and the body follows:
| Offset | Size | Field | Notes |
|---|---|---|---|
| 0 | 1B | Protocol Version | 0x01 / 0x02 / 0x03 (see above). |
| 1 | 1B | Packet Type | 0x03 for EAPOL-Key. |
| 2-3 | 2B | Packet Body Length | Big-endian length of the body only, not the 4-byte header. Usually 95+ for EAPOL-Key. |
| 4+ | var | Packet Body | The type-specific payload. For 0x03 this is the EAPOL-Key body below. |
EAPOL-Key Frame Structure
The EAPOL-Key body is the structure that carries the WPA2/WPA3 handshake. Offsets below are relative to the start of the EAPOL header (so the descriptor type sits at offset 4, right after the 4-byte header). The minimum body is 95 bytes, giving a minimum total EAPOL-Key PDU of 99 bytes before any Key Data:
| Offset | Size | Field | Notes |
|---|---|---|---|
| 4 | 1B | Descriptor Type | 0x02 = RSN (WPA2/WPA3). 0xFE = WPA1. 0x01 = legacy RC4. |
| 5-6 | 2B | Key Information | Bitfield of flags and the key descriptor version. Decoded below. |
| 7-8 | 2B | Key Length | Pairwise key length in bytes. 16 for CCMP, 32 for GCMP-256. |
| 9-16 | 8B | Key Replay Counter | Monotonic counter to defeat replay. AP sets it; client echoes it. |
| 17-48 | 32B | Key Nonce | ANonce (AP) in msg 1/3, SNonce (client) in msg 2. Core input to PTK derivation. |
| 49-64 | 16B | Key IV | Used with TKIP; all zeros under CCMP/WPA2. |
| 65-72 | 8B | Key RSC | Receive Sequence Counter for the GTK. Zero in msgs 1 and 2. |
| 73-80 | 8B | Reserved | Zero. Ignored on receive. |
| 81-96 | 16B | Key MIC | Integrity code over the whole EAPOL frame (MIC field zeroed during calc). Length can vary with AKM; 16B for WPA2. |
| 97-98 | 2B | Key Data Length | Big-endian length of the Key Data field that follows. |
| 99+ | var | Key Data | RSNIE, PMKID, and the encrypted GTK (in msg 3). Empty in msgs 1 and 4. |
The Descriptor Type (offset 4) and the Key Descriptor Version (inside Key Information) are two different fields. The descriptor type tells you the overall format (RSN vs the older WPA1 or RC4 formats). The descriptor version tells you which cryptographic primitives protect this specific frame:
| Descriptor Version | MIC Algorithm | Key Data Encryption | Typical Use |
|---|---|---|---|
| 1 | HMAC-MD5 | RC4 | TKIP |
| 2 | HMAC-SHA1-128 | AES Key Wrap | CCMP (classic WPA2) |
| 3 | AES-128-CMAC | AES Key Wrap | 802.11w / SHA-256 AKM |
| 0 | Defined by AKM | AES Key Wrap | WPA3 / SAE and other modern AKMs |
graph TD
KEY["EAPOL-Key Body"] --> DESC["Descriptor Type
1B: RSN 0x02"]
KEY --> INFO["Key Information
2B: flags + version"]
KEY --> KLEN["Key Length
2B"]
KEY --> REPLAY["Replay Counter
8B"]
KEY --> NONCE["Key Nonce
32B: ANonce/SNonce"]
KEY --> IV["Key IV
16B"]
KEY --> RSC["Key RSC
8B"]
KEY --> MIC["Key MIC
16B"]
KEY --> KDLEN["Key Data Length
2B"]
KDLEN --> KDATA["Key Data
var: GTK, PMKID, RSNIE"]
The EAPOL-Key body fields. The Key Nonce and Key MIC are the values a password-auditing tool needs; the Key Data carries the encrypted group key in message 3.
Decoding the Key Information Bitfield
The 2-byte Key Information field (offset 5-6, big-endian) is what actually distinguishes the four handshake messages from one another. Read as a 16-bit value, the bits are:
| Bit(s) | Name | Meaning |
|---|---|---|
| 0-2 | Key Descriptor Version | 1, 2, 3, or 0 as in the table above. |
| 3 | Key Type | 1 = Pairwise (PTK), 0 = Group (GTK). |
| 4-5 | Key Index | Group key slot; 0 for pairwise. |
| 6 | Install | 1 = install the negotiated key after this message. |
| 7 | Key ACK | 1 = sender expects a reply (set on AP-to-client messages). |
| 8 | Key MIC | 1 = the Key MIC field is populated and valid. |
| 9 | Secure | 1 = both sides now hold the PTK; the link is being secured. |
| 10 | Error | 1 = MIC failure report (TKIP countermeasures). |
| 11 | Request | 1 = supplicant is requesting a new handshake. |
| 12 | Encrypted Key Data | 1 = the Key Data field is encrypted (e.g. the GTK in msg 3). |
| 13 | SMK Message | 1 = part of an SMK handshake (802.11z peer keys). Rare. |
| 14-15 | Reserved | Zero. |
Identifying a handshake message is a matter of pattern matching on the ACK, MIC, Install, and Secure bits, which the next section reduces to a lookup function.
The Four WPA2 Handshake Messages
All four messages are EAPOL-Key frames with Descriptor Type 0x02 and Key Type = Pairwise. They differ in the Key Information flags and in what the nonce, MIC, and Key Data fields carry.
| Msg | Dir | ACK | MIC | Install | Secure | Enc Data | Carries |
|---|---|---|---|---|---|---|---|
| 1 | AP→STA | 1 | 0 | 0 | 0 | 0 | ANonce (+ optional PMKID in RSNIE) |
| 2 | STA→AP | 0 | 1 | 0 | 0 | 0 | SNonce + MIC + client RSNIE |
| 3 | AP→STA | 1 | 1 | 1 | 1 | 1 | ANonce + MIC + encrypted GTK |
| 4 | STA→AP | 0 | 1 | 0 | 1 | 0 | MIC only (ACK), empty Key Data |
Message 1 gives the client the ANonce. The client already has the PMK (derived from the passphrase and SSID), generates its own SNonce, and now has everything to compute the PTK. No MIC yet, because the PTK does not exist on the wire until message 2.
Message 2 returns the SNonce and the first MIC, computed with the KCK portion of the freshly derived PTK. Messages 1 and 2 together contain both nonces, both MAC addresses, and a MIC to test against. That is the complete input set for offline password verification.
Message 3 confirms the AP derived the same PTK (the client verifies its MIC), sets Install and Secure, and delivers the GTK encrypted in the Key Data field so the client can decrypt broadcast and multicast traffic.
Message 4 is the client's acknowledgement. After it is sent, both sides switch to the PTK for all data frames.
Those flag combinations map directly to code. Given the 16-bit Key Information value, this returns the handshake message number, or None when the frame is not one of the four pairwise messages:
/// EAPOL-Key key_info bits (big-endian on the wire).
pub const KI_PAIRWISE: u16 = 1 << 3;
pub const KI_INSTALL: u16 = 1 << 6;
pub const KI_ACK: u16 = 1 << 7;
pub const KI_MIC: u16 = 1 << 8;
pub const KI_SECURE: u16 = 1 << 9;
pub fn message_number(ki: u16) -> Option<u8> {
if ki & KI_PAIRWISE == 0 {
return None;
}
let (ack, mic, install, secure) = (
ki & KI_ACK != 0,
ki & KI_MIC != 0,
ki & KI_INSTALL != 0,
ki & KI_SECURE != 0,
);
match (ack, mic, install, secure) {
(true, false, _, _) => Some(1),
(false, true, false, false) => Some(2),
(true, true, true, _) => Some(3),
(false, true, false, true) => Some(4),
_ => None,
}
}
The match arms are the ACK/MIC/Install/Secure columns of the table above, read top to bottom. Feed it the key_info that parse_eapol returns in the next section and you get the message number straight from a captured frame.
sequenceDiagram
participant C as Client (STA)
participant AP as Access Point
Note over C,AP: Both already hold the PMK
AP->>C: Msg 1: ANonce (ACK=1)
Note over C: Derives PTK from PMK,
ANonce, SNonce, MACs
C->>AP: Msg 2: SNonce + MIC
Note over AP: Derives PTK,
verifies MIC
AP->>C: Msg 3: MIC + encrypted GTK
(Install=1, Secure=1)
Note over C: Installs PTK and GTK
C->>AP: Msg 4: MIC (ACK)
Note over C,AP: Encrypted data begins
The four EAPOL-Key messages. Capturing messages 1 and 2 is enough to attempt offline verification of a password on a network you are authorized to test.
Parsing EAPOL-Key Frames in Practice
Putting the encapsulation rules together, here is what a parser has to do with a raw 802.11 frame before it can trust that it holds an EAPOL-Key PDU:
graph TD
START["Raw 802.11 frame"] --> T{"Frame type
== Data?"}
T -->|no| REJECT["Reject"]
T -->|yes| DS["Read To/From DS
pick BSSID + STA"]
DS --> BASE["Header = 24B"]
BASE --> Q{"QoS Data
subtype?"}
Q -->|yes| ADD["+2B QoS Control"]
Q -->|no| SNAP["Locate SNAP"]
ADD --> SNAP
SNAP --> M{"AA AA 03 00 00 00
88 8E ?"}
M -->|no| REJECT
M -->|yes| E{"EAPOL type
== 3 (Key)?"}
E -->|no| REJECT
E -->|yes| OK["Extract nonce,
MIC, key data"]
The validation path from a raw 802.11 frame to a confirmed EAPOL-Key PDU. Each decision maps directly to a check in the code below.
The Rust routine below implements exactly that path. It takes the raw bytes of an 802.11 frame and returns a parsed view only if the frame is a genuine LLC/SNAP-encapsulated EAPOL-Key PDU. Everything else returns None. Every constant in it corresponds to a field in the tables above.
pub fn parse_eapol(f: &[u8]) -> Option<EapolView> {
if f.len() < 24 || (f[0] >> 2) & 0x3 != 2 {
return None; // not a data frame
}
// to/from distribution system
let to_ds = f[1] & 0x1 != 0;
let from_ds = f[1] & 0x2 != 0;
let a1: Mac = f[4..10].try_into().ok()?; // destination
let a2: Mac = f[10..16].try_into().ok()?; // source
let a3: Mac = f[16..22].try_into().ok()?; // bssid
let (bssid, station) = if from_ds {
(a2, a1)
} else if to_ds {
(a1, a2)
} else {
(a3, a2)
};
let snap = 24 + if (f[0] >> 4) & 0x8 != 0 { 2 } else { 0 }; // +2 for QoS control data
// check if snap header is valid (0xAA) and ethertype is 0x888E, the EAPOL type
if f.len() < snap + 8 || f[snap] != 0xAA || f[snap + 6] != 0x88 || f[snap + 7] != 0x8e {
return None; // not LLC/SNAP EAPOL
}
let e = &f[snap + 8..];
if e.len() < 99 || e[1] != 3 {
return None; // not an EAPOL-Key PDU (type 3)
}
let kd_len = u16::from_be_bytes([e[97], e[98]]) as usize;
Some(EapolView {
bssid,
station,
key_info: u16::from_be_bytes([e[5], e[6]]),
nonce: e[17..49].try_into().ok()?,
mic: e[81..97].try_into().ok()?,
key_data: e.get(99..99 + kd_len).unwrap_or(&[]).to_vec(),
eapol: e.to_vec(),
})
}
Reading it against the tables:
-
(f[0] >> 2) & 0x3 != 2pulls the Type subfield out of the Frame Control byte and requires Data (type 2). -
f[1] & 0x1andf[1] & 0x2read the To DS and From DS bits, which decide which of the three address fields is the BSSID and which is the station. -
(f[0] >> 4) & 0x8tests the QoS bit of the subtype, adding the 2-byte QoS Control field to the header length when present. That variable offset is exactly the "header is not fixed length" problem from the encapsulation section. -
f[snap] != 0xAAplus the0x88 0x8echeck atsnap + 6andsnap + 7matches theAA AA 03 00 00 00 88 8ELLC/SNAP prefix (it validates the first SAP byte and the EtherType, which is enough to gate in practice). -
e[1] != 3requires EAPOL packet type 3 (EAPOL-Key). Thene[5..7],e[17..49],e[81..97], ande[97..99]lift out Key Information, the 32-byte nonce, the 16-byte MIC, and the Key Data length exactly where the byte-layout table places them.
Caveats. The routine assumes a three-address frame, so it does not handle the 4-address WDS case (both To DS and From DS set), and it does not account for a 4-byte HT/VHT Control field that can appear when the Order bit is set on a QoS frame. For capturing PSK handshakes from ordinary client-to-AP traffic those cases are rare, and skipping them keeps the hot path branch-free and fast. If you feed frames that still carry a radiotap header, strip that first; this parser expects the 802.11 MAC header at offset 0.
Why EAPOL Matters for Security Research
Handshake capture for password auditing. Messages 1 and 2 (or 2 and 3, which also contain both nonces) give you everything needed to test a candidate password against a network you own or are authorized to audit. The check is deterministic: derive the PMK from the candidate, derive the PTK, recompute the MIC over the EAPOL frame, and compare it to the captured MIC. Tools like hashcat ingest a PCAP and run that loop at hardware-accelerated speed.
PMKID testing. Since 2018 there has been a way to test passwords without waiting for a full handshake. The PMKID is HMAC-SHA1-128(PMK, "PMK Name" || BSSID || STA MAC) and can appear in the RSNIE of message 1 or in the AP's response to an association request. Because it derives from the PMK, and the PMK derives from the passphrase, you can test candidates against the PMKID alone with no connected client present.
Protocol implementation review. Replay-counter handling, nonce reuse, and MIC verification are all places where real implementations have shipped bugs (KRACK being the famous example of nonce/key reinstallation). Being able to read raw EAPOL exchanges lets you confirm that an AP or client follows the state machine correctly rather than trusting that it does.
BLEShark Nano and EAPOL Capture
The BLEShark Nano captures EAPOL frames and writes them as standard PCAP files, ready for Wireshark, hashcat, or any tool that reads the format. Nothing proprietary, nothing locked in.
In standard mode, on networks you own or have written permission to test, it can prompt a client to reconnect with a deauthentication frame and then passively record all four EAPOL-Key messages as they arrive, producing a complete handshake capture. In EU configurations, deauth is disabled for RED compliance, so the device operates in passive-listen mode: it records EAPOL frames only from natural association events. That is well suited to monitoring your own network, spotting devices with failing handshakes, or confirming your AP emits well-formed EAPOL-Key frames.
PMKID extraction works passively too, since the PMKID rides in message 1 without any client interaction. For distributed monitoring across a larger site, a Shiver mesh of up to 16 BLEShark Nano nodes (ESP-NOW, 20-50m between hops) lets several capture points feed a single view of authentication activity.
BLEShark Nano is intended for authorized security research and education. Capture and analyze EAPOL traffic only on networks you own or have explicit written permission to test.