Add the Tkinter wizard UI and preflight checks

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHYXPc5u9FfVNS8auAUgC4
This commit is contained in:
Franck Thiaw 2026-09-03 08:35:20 +04:00
parent 5100abe75c
commit 0c147f9d8c

View file

@ -1,6 +1,19 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Wireless plumbing: interface discovery, monitor mode, card-state restore, """
and tracked airodump-ng / aireplay-ng subprocesses.""" 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 glob
import os import os
import re import re
@ -8,10 +21,19 @@ import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
import threading
import time 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_NET_SECS = 10
SCAN_CLIENT_SECS = 15 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) --- # --- child process registry (so "Stop & Reset" can kill mid-scan/mid-deauth) ---
_procs = set() _procs = set()
@ -48,6 +70,7 @@ def run(cmd, timeout=30):
return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout) return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
# --------------------------- interface discovery ------------------------------
def list_wifi_interfaces(): def list_wifi_interfaces():
"""[{iface, phy, type, addr, monitor_capable}] from `iw dev`.""" """[{iface, phy, type, addr, monitor_capable}] from `iw dev`."""
try: try:
@ -95,6 +118,7 @@ def _phy_has_monitor(phy):
return False return False
# ------------------------------ card state ----------------------------------
class CardState: class CardState:
def __init__(self, iface): def __init__(self, iface):
self.orig_iface = iface["iface"] self.orig_iface = iface["iface"]
@ -149,6 +173,7 @@ def reset_card(state):
run(["systemctl", "start", "NetworkManager"]) run(["systemctl", "start", "NetworkManager"])
# ------------------------------ scanning -----------------------------------
def airodump_scan(mon_iface, secs, bssid=None, channel=None): def airodump_scan(mon_iface, secs, bssid=None, channel=None):
tmpdir = tempfile.mkdtemp(prefix="wifiscan_") tmpdir = tempfile.mkdtemp(prefix="wifiscan_")
prefix = os.path.join(tmpdir, "dump") prefix = os.path.join(tmpdir, "dump")
@ -209,6 +234,7 @@ def parse_airodump_csv(path):
return aps, stations return aps, stations
# ------------------------------ deauth -------------------------------------
class Deauth: class Deauth:
def __init__(self): def __init__(self):
self.proc = None self.proc = None
@ -233,6 +259,325 @@ class Deauth:
return self.proc is not None and self.proc.poll() is None 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("<<TreeviewSelect>>", 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("<<TreeviewSelect>>",
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("<<TreeviewSelect>>",
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(): def _selftest():
sample = ( sample = (
"BSSID, First time seen, Last time seen, channel, Speed, Privacy, Cipher, " "BSSID, First time seen, Last time seen, channel, Speed, Privacy, Cipher, "
@ -266,3 +611,8 @@ if __name__ == "__main__":
if "--selftest" in sys.argv: if "--selftest" in sys.argv:
_selftest() _selftest()
sys.exit(0) sys.exit(0)
err = preflight()
if err:
print(err, file=sys.stderr)
sys.exit(1)
App().mainloop()