fix: client kick/deauth use full bssid/target/channel deauth form

hak5cmd on this firmware has no CLIENT_KICK command and PINEAPPLE_DEAUTH_CLIENT
requires (bssid, target, channel) — the UI kick/deauth buttons and the MCP
pineap.kick_client tool previously passed only the client MAC, which printed
usage and silently did nothing.

- _deauth_target/_deauth_client_via_iface resolve the client's association
  interface via iwinfo (Access Point + Channel), pick the band-aware inject
  interface, and issue DEAUTH_CLIENT <bssid> <mac> <channel>
- h_client_kick: deny-filter (persistent) + immediate deauth with rc checks
- h_deauth_client: deauth with the full form; 502 with detail when the
  client is not associated
- MCP pineap.kick_client: resolves the client first (no side effects on
  failure), then deny-filter + deauth; verified on device (clean error for
  unassociated clients, filter list restored)
- tests updated for the new command chain
This commit is contained in:
2026-08-19 00:05:42 -05:00
parent d7ef0624f4
commit f6b4cadc39
2 changed files with 130 additions and 23 deletions
@@ -860,17 +860,25 @@ def h_client_kick(ctx):
mac = normalize_mac((ctx.body or {}).get('mac'))
if not mac:
return 400, {'error': 'invalid mac'}
hak5('PINEAPPLE_DEVICE_FILTER_MODE', 'deny')
hak5('PINEAPPLE_DEVICE_FILTER_ADD', 'deny', mac)
hak5('PINEAPPLE_DEAUTH_CLIENT', mac)
return 200, {'ok': True}
# Persistent kick: deny-filter the client so pineapd deauths every probe
# and connect, then deauth it once immediately with the full
# (bssid, target, channel) form hak5cmd requires.
for argv in ([HAK5CMD, 'PINEAPPLE_DEVICE_FILTER_MODE', 'deny'],
[HAK5CMD, 'PINEAPPLE_DEVICE_FILTER_ADD', 'deny', mac]):
rc, out, err = device_run(argv, timeout=20)
if rc != 0:
return 502, {'error': 'kick filter failed', 'detail': (err or out)[-300:]}
ok, detail = _deauth_client_via_iface(mac)
return 200, {'ok': True, 'deauth': ok, 'detail': detail or None}
def h_deauth_client(ctx):
mac = normalize_mac((ctx.body or {}).get('mac'))
if not mac:
return 400, {'error': 'invalid mac'}
hak5('PINEAPPLE_DEAUTH_CLIENT', mac)
ok, detail = _deauth_client_via_iface(mac)
if not ok:
return 502, {'error': 'deauth failed', 'detail': detail}
return 200, {'ok': True}
@@ -2298,6 +2306,53 @@ def assoc_clients(ifaces=None):
return clients
def _iface_ap_info(iface):
"""(bssid, channel) of the AP running on `iface`, via iwinfo."""
rc, out, err = device_run(['iwinfo', iface, 'info'])
bssid = None
channel = None
for line in out.splitlines():
m = re.search(r'Access Point:\s*([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5})', line)
if m:
bssid = m.group(1).upper()
m = re.search(r'Channel:\s*(\d+)', line)
if m:
channel = int(m.group(1))
return bssid, channel
def _deauth_target(mac):
"""(iface, bssid, channel) for an associated client, else None."""
for c in assoc_clients():
if c['mac'] != mac:
continue
bssid, channel = _iface_ap_info(c['iface'])
if not bssid or not channel:
return None
return c['iface'], bssid, channel
return None
def _deauth_client_via_iface(mac):
"""Deauth a client associated to one of our own APs.
hak5cmd's deauth needs the full (bssid, target, channel) triple; the
client's AP and channel are resolved from its association interface.
Returns (ok, detail).
"""
target = _deauth_target(mac)
if not target:
return False, 'client not associated'
iface, bssid, channel = target
band = _band_of_channel(channel)
inject = 'wlan1mon' if band == BAND_5G or band == BAND_6G else 'wlan0mon'
if inject != 'wlan1mon':
_pineap('INTERFACE', 'INJECT', inject)
rc, out, err = device_run([HAK5CMD, 'DEAUTH_CLIENT', bssid, mac,
str(channel)], timeout=30)
return rc == 0, (err or out)
def disk_data():
rc, out, err = device_run(['df', '-k', '/root'])
lines = out.splitlines()
@@ -3698,11 +3753,27 @@ def _mcp_tools():
return {'devices': _sql_table('wifi_device', args.get('limit', 50))}
def kick(args):
mac = (args.get('mac') or '').strip()
mac = normalize_mac(args.get('mac'))
if not mac:
return {'error': 'mac required'}
rc, out, err = device_run([HAK5CMD, 'CLIENT_KICK', mac], timeout=20)
return {'ok': rc == 0, 'detail': (err or out)[-300:]}
# This firmware's hak5cmd has no CLIENT_KICK command; mirror the web
# UI's kick: deny-filter the client (deauths every probe/connect) then
# deauth it once with the full (bssid, target, channel) form. The
# client must be associated first so a failed kick has no side effects.
if not _deauth_target(mac):
return {'ok': False, 'detail': 'client not associated', 'mac': mac}
ok = True
detail = ''
for argv in ([HAK5CMD, 'PINEAPPLE_DEVICE_FILTER_MODE', 'deny'],
[HAK5CMD, 'PINEAPPLE_DEVICE_FILTER_ADD', 'deny', mac]):
rc, out, err = device_run(argv, timeout=20)
if rc != 0:
ok = False
detail = (err or out)[-300:]
break
if ok:
ok, detail = _deauth_client_via_iface(mac)
return {'ok': ok, 'detail': detail, 'mac': mac}
def set_filter(args):
kind = (args.get('kind') or 'ssid').strip()