Detect a Wi-Fi Deauth Attack with tcpdump or a Short Python Script
📅 September 29, 2026 · ⏱️ 13 min read
If your devices keep dropping off Wi-Fi for a second and reconnecting, one possible cause is a deauthentication attack: someone in radio range forging the frames that tell your devices to disconnect. This page shows three things you can use right now — a one-line tcpdump filter, a short Python script that tells a real attack apart from an ordinary disconnect, and a downloadable sample capture so you can confirm both work on your own machine without attacking anyone or owning special hardware.
Honesty rail, up front. The machine that wrote this article has no Wi-Fi adapter in monitor mode, so the live-capture commands here (the ones with sudo tcpdump -i <iface>) were not run on real radio. Everything printed below — every tcpdump line, the detector’s output — comes from running the tools offline against the sample capture, which is generated deterministically by a committed script. Where a command needs monitor mode you will see it written out, but its output on this page is always the sample.
This is a detection page. It contains no command that sends deauth frames and is not an attack tutorial.
What a deauthentication frame actually is
A deauthentication frame is an 802.11 management frame (type 0, subtype 12) that tells a station it is no longer authenticated; disassociation is the closely related subtype 10. Wi-Fi (802.11) frames come in three classes, set by a two-bit type field: management (type 0), control (type 1), and data (type 2). Within management there is a four-bit subtype. Two subtypes matter here:
- Deauthentication — type 0, subtype 12. “You are no longer authenticated to this network.”
- Disassociation — type 0, subtype 10. The gentler cousin: “you are still authenticated but no longer associated.” Attack tools use both; this page focuses on deauth because it is the more disruptive and the more common.
A beacon — the frame an access point broadcasts ten times a second to announce itself — is also management, subtype 8. You will see beacons in the sample too, because a real capture is mostly beacons with the interesting frames buried in them.
The body of a deauth or disassoc frame is tiny: a two-byte reason code and nothing else. That code is the “why”, and it is the fingerprint an attack leaves. More on the codes below.
Why it can be forged
On a network without Protected Management Frames, management frames are sent unprotected in the open, so anyone in range can forge one with the access point’s address and the client cannot tell it apart from the real thing. They carry no signature and no encryption. Nothing stops a third party from building a deauth frame with the access point’s MAC address in the sender field and broadcasting it. Your laptop receives a frame that says “the router told you to leave”, and it has no way to know the router never said any such thing.
The fix is Protected Management Frames (PMF), defined in the IEEE 802.11w amendment. The Wi-Fi Alliance describes what PMF does plainly: it “enforces the encryption of frames for disconnection, which enables APs and clients to detect forged disconnect frames and ignore them” (Protected Management Frames enhance Wi-Fi network security, wi-fi.org). PMF is mandatory in WPA3 and optional in WPA2. So the honest caveat on this whole article: if your network is WPA3, or WPA2 with PMF required, forged deauths are rejected and there is nothing for a detector to catch. The technique below matters for the enormous installed base that is not there yet — and for spotting an attacker who is knocking WPA3 clients down to a weaker mode.
The prerequisite: monitor mode
To see management frames at all you need an adapter in monitor mode, capturing raw 802.11 rather than the Ethernet-shaped frames a normal connection hands up. That is the one piece you cannot fake: a built-in laptop chip usually will not do it, and you will likely need a USB adapter that supports it. Rather than repeat the setup, the project page has the exact commands — see Install and enable monitor mode, which walks through airmon-ng check kill, enabling the mode, and confirming with iwconfig. The interface name it prints back (often something like wlan0mon) is what goes in the commands below.
Everything from here can be tested without monitor mode, because you can point the same tools at a capture file.
The one-line tcpdump filter
Put an adapter into monitor mode and run tcpdump with the filter type mgt subtype deauth, which prints one line per deauthentication frame it sees. tcpdump understands 802.11 filters directly. On a live monitor-mode interface:
sudo tcpdump -i wlan0mon -e -n 'type mgt subtype deauth'
type mgt subtype deauth keeps only management frames of subtype deauthentication; -e prints the link-layer addresses (so you can see who the frame claims to be from) and -n skips name resolution. To try it right now against the sample capture instead, read from the file — no adapter, no sudo:
tcpdump -r deauth-sample.pcap -e -nn -tt 'type mgt subtype deauth'
Run offline against the sample on this machine (tcpdump 4.99.1), that prints 65 lines — one ordinary disconnect, then a cluster of 64. The first line and the first burst line look like this (-tt prints the raw capture timestamp, so the output does not depend on your time zone):
1700000005.000000 BSSID:02:00:00:00:00:01 DA:02:00:00:00:00:01 SA:02:00:00:00:00:02 DeAuthentication: Deauthenticated because sending STA is leaving (or has left) IBSS or ESS
1700000012.000000 BSSID:02:00:00:00:00:01 DA:ff:ff:ff:ff:ff:ff SA:02:00:00:00:00:01 DeAuthentication: Class 3 frame received from nonassociated STA
Read the addresses. The first frame is one client (SA:02:00:00:00:00:02) telling the AP it is leaving — a normal event. The second is sent from the AP’s own address (SA:02:00:00:00:00:01) to the broadcast address (DA:ff:ff:ff:ff:ff:ff): one frame addressed to every client at once, claiming to be the router. That is the shape of the attack. All addresses in the sample are locally administered (first octet 02) or broadcast — no real device or vendor appears.
tcpdump is perfect for seeing the frames. What it will not do is decide for you whether what you are looking at is an attack. For that you need to count.
A short Python detector
The difference between a disconnect and an attack is rate: one device leaving is a single frame; an attack is a flood. So the detector counts deauth frames per sender in a sliding time window and alerts when one sender crosses a threshold. This is standard-library Python only — no scapy, nothing to install — and it reads either a capture file (offline) or a monitor-mode interface through an AF_PACKET raw socket (live):
#!/usr/bin/env python3
"""Flag Wi-Fi deauthentication floods. Standard library only.
Offline: python3 deauth-detect.py deauth-sample.pcap
Live: sudo python3 deauth-detect.py --iface wlan0mon (interface in monitor mode)
"""
import socket
import struct
import sys
from collections import deque
WINDOW = 1.0 # seconds: the sliding window deauths are counted over
THRESHOLD = 10 # deauths from one sender in one window before it is an alert
def mac(b):
return ":".join("%02x" % x for x in b)
def parse(data):
"""Yield (transmitter, bssid, reason) for every deauth frame in one radiotap packet."""
if len(data) < 4:
return
rt_len = struct.unpack_from("<H", data, 2)[0] # radiotap header length, bytes 2-3
fc0 = data[rt_len] # 802.11 frame control, first octet
ftype, subtype = (fc0 >> 2) & 0x3, (fc0 >> 4) & 0xF
if ftype != 0 or subtype != 12: # management type 0, deauth subtype 12
return
mac_hdr = data[rt_len:rt_len + 24] # 24-byte management header
transmitter, bssid = mac(mac_hdr[10:16]), mac(mac_hdr[16:22]) # addr2, addr3
reason = struct.unpack_from("<H", data, rt_len + 24)[0]
yield transmitter, bssid, reason
def alert(hits, key, reason):
tx, bssid = key
print("ALERT: %d deauths in %.1fs from %s -> BSSID %s (reason %d)"
% (len(hits), WINDOW, tx, bssid, reason))
def watch(source):
"""source yields (timestamp, packet_bytes). Emits alerts, returns (frames, deauths)."""
windows, armed, frames, deauths = {}, set(), 0, 0
for ts, data in source:
frames += 1
for tx, bssid, reason in parse(data):
deauths += 1
key = (tx, bssid)
hits = windows.setdefault(key, deque())
hits.append(ts)
while hits and ts - hits[0] > WINDOW:
hits.popleft()
if len(hits) >= THRESHOLD and key not in armed:
armed.add(key)
alert(hits, key, reason)
elif len(hits) < THRESHOLD:
armed.discard(key)
return frames, deauths
def from_pcap(path):
with open(path, "rb") as f:
magic = f.read(24) # global header
le = "<" if magic[:4] == b"\xd4\xc3\xb2\xa1" else ">"
while True:
hdr = f.read(16)
if len(hdr) < 16:
break
ts_sec, ts_usec, incl, _orig = struct.unpack(le + "IIII", hdr)
yield ts_sec + ts_usec / 1_000_000, f.read(incl)
def from_iface(iface):
s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(3))
s.bind((iface, 0))
while True:
data, _ = s.recvfrom(65535)
yield __import__("time").time(), data
if __name__ == "__main__":
if len(sys.argv) == 3 and sys.argv[1] == "--iface":
print("watching %s (live)" % sys.argv[2])
watch(from_iface(sys.argv[2]))
else:
path = sys.argv[1]
frames, deauths = watch(from_pcap(path))
print("%s: %d frames, %d deauth" % (path, frames, deauths))
The parse function is the load-bearing part. A capture on a monitor-mode interface is prefixed with a radiotap header carrying signal metadata; its length is not fixed, so you read it from bytes 2-3 and skip that many bytes to reach the 802.11 frame. The first octet of the frame control field holds the type and subtype; the 24-byte management header puts the transmitter address at offset 10 and the BSSID at offset 16; the reason code is the first two bytes of the body, right after the header.
Run it offline against the sample:
python3 deauth-detect.py deauth-sample.pcap
ALERT: 10 deauths in 1.0s from 02:00:00:00:00:01 -> BSSID 02:00:00:00:00:01 (reason 7)
deauth-sample.pcap: 105 frames, 65 deauth
It read 105 frames, 65 of them deauthentications, and raised exactly one alert. It fired on the broadcast burst from 02:00:00:00:00:01 the moment ten frames landed inside a one-second window, and it stayed silent about the single ordinary disconnect from the client — because one frame never crosses a threshold of ten. That is the whole point of counting instead of just printing: the detector tells the attack apart from normal churn. To run it live once you have monitor mode, point it at the interface instead of the file with sudo python3 deauth-detect.py --iface wlan0mon.
The reason codes you will see
The two-byte reason code is worth knowing because attack tools and normal disconnects tend to use different ones. These descriptions are from the Wireshark 802.11 dissector’s reason-code table (packet-ieee80211.c, wireshark/wireshark on GitLab):
| Code | Meaning | In the sample |
|---|---|---|
| 1 | Unspecified reason | not present; the most common attack-tool default |
| 3 | Deauthenticated because sending STA is leaving (or has left) the BSS | the single ordinary disconnect at t=5 s |
| 7 | Class 3 frame received from nonassociated STA | all 64 frames of the burst |
| 8 | Disassociated because sending STA is leaving (or has left) BSS | not present; the disassociation equivalent of code 3 |
The reason code alone does not prove intent — a real AP can legitimately send code 7. What proves it is the code plus the rate plus the spoofed source, which is exactly what the detector keys on.
What a burst looks like versus a normal disconnect
Here is the difference, straight from the sample:
- Normal disconnect: a single deauth at t=5 s, sent by the client
02:00:00:00:00:02to the AP, reason 3. One frame, one sender, done. - Attack burst: 64 deauths starting at t=12 s, all within one second (one every 0.015625 s), all sent from the AP’s address
02:00:00:00:00:01to the broadcast address, reason 7.
One device leaving sends a single deauth; an attack is a burst, so counting deauth frames per sender in a sliding window and alerting over a threshold separates the two. The client-leaving frame and the flood are the same frame type with the same subtype. Nothing about a single frame is suspicious. It is the 64-in-a-second from one source, aimed at everyone that is impossible to mistake once you are counting — and that is why a bare tcpdump view, which shows both as identical lines, is not enough on its own.
Download the sample and try it yourself
The capture used throughout this page is here: deauth-sample.pcap (a libpcap file, radiotap + 802.11, link type 127). It is generated by a committed, deterministic script — astro/scripts/make-deauth-sample.py, standard-library Python, no scapy — so anyone can regenerate the identical bytes and audit exactly what is in it. It holds 40 beacons, one ordinary client disconnect, and the 64-frame broadcast burst, and every MAC address in it is locally administered so no real device is named. Can you test a deauth detector without running an attack? Yes. Download the sample capture and run tcpdump or the Python detector against the file offline, which reproduces both a normal disconnect and a 64-frame broadcast burst without sending a single frame. Open it in Wireshark, run the tcpdump line, or feed it to the Python detector; all three read the same file.
If you want a detector that does more than flag a burst — one that scores each attack 0-100 by weighing timing, signal strength and MAC-spoofing signals, hops channels across both bands, and logs forensic evidence — WiFi Security Monitor is a full detector that scores each attack, and the WiFi Security Monitor v1 write-up introduces the two programs and what it takes to run them. The script on this page is the smallest thing that works; that project is what you graduate to.
Follow new posts by RSS: paste dankdev.com/feed.xml into any feed reader. It is a plain XML file — no signup, no email address, no account. Posts land there the same day they go up, and there is nothing to unsubscribe from later.