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
+51 -15
View File
@@ -9,8 +9,10 @@ import server
def setUpModule():
__import__('importlib').reload(server)
_orig_hak5 = server.hak5
_orig_device_run = server.device_run
_orig_assoc_clients = server.assoc_clients
_orig_iface_ap_info = server._iface_ap_info
_orig_pineap = server._pineap
class NormalizeTest(unittest.TestCase):
@@ -24,8 +26,10 @@ class NormalizeTest(unittest.TestCase):
class ClientsTest(unittest.TestCase):
def tearDown(self):
server.hak5 = _orig_hak5
server.device_run = _orig_device_run
server.assoc_clients = _orig_assoc_clients
server._iface_ap_info = _orig_iface_ap_info
server._pineap = _orig_pineap
def test_clients_handler(self):
server.assoc_clients = lambda: [{'mac': 'AA:BB:CC:DD:EE:FF', 'iface': 'wlan0open', 'rssi': -55}]
@@ -35,15 +39,37 @@ class ClientsTest(unittest.TestCase):
self.assertEqual(status, 200)
self.assertEqual(payload['count'], 1)
def test_kick_validates_and_deny_adds(self):
def _kick_env(self):
calls = []
def fake(*args):
calls.append(args)
return 'ok'
server.hak5 = fake
def fake_run(argv, timeout=30):
calls.append(argv)
return 0, '', ''
server.device_run = fake_run
server.assoc_clients = lambda: [{'mac': '00:11:22:33:44:55', 'iface': 'wlan0open', 'rssi': -55}]
server._iface_ap_info = lambda iface: ('AA:BB:CC:DD:EE:FF', 6)
server._pineap = lambda *a, **k: (0, '', '')
return calls
def test_kick_validates_and_deny_adds(self):
calls = self._kick_env()
server.h_client_kick(type('C', (), {'args': (), 'body': {'mac': '00:11:22:33:44:55'}})())
self.assertIn(('PINEAPPLE_DEVICE_FILTER_ADD', 'deny', '00:11:22:33:44:55'), calls)
self.assertTrue(any(c[0] == 'PINEAPPLE_DEAUTH_CLIENT' for c in calls))
self.assertIn([server.HAK5CMD, 'PINEAPPLE_DEVICE_FILTER_ADD', 'deny', '00:11:22:33:44:55'], calls)
# The immediate deauth must use the full bssid/target/channel form.
self.assertTrue(any(c[:4] == [server.HAK5CMD, 'DEAUTH_CLIENT', 'AA:BB:CC:DD:EE:FF',
'00:11:22:33:44:55'] and c[4] == '6' for c in calls))
def test_kick_not_associated_still_filters(self):
calls = []
def fake_run(argv, timeout=30):
calls.append(argv)
return 0, '', ''
server.device_run = fake_run
server.assoc_clients = lambda: []
status, payload = server.h_client_kick(type('C', (), {'args': (), 'body': {'mac': '00:11:22:33:44:55'}})())
self.assertEqual(status, 200)
self.assertIs(payload['deauth'], False)
self.assertTrue(any(c == [server.HAK5CMD, 'PINEAPPLE_DEVICE_FILTER_ADD', 'deny',
'00:11:22:33:44:55'] for c in calls))
def test_kick_bad_mac_400(self):
status, payload = server.h_client_kick(type('C', (), {'args': (), 'body': {'mac': 'x'}})())
@@ -51,12 +77,22 @@ class ClientsTest(unittest.TestCase):
def test_deauth_client(self):
calls = []
def fake(*args):
calls.append(args)
return 'ok'
server.hak5 = fake
server.h_deauth_client(type('C', (), {'args': (), 'body': {'mac': '00:11:22:33:44:55'}})())
self.assertEqual(calls[0][0], 'PINEAPPLE_DEAUTH_CLIENT')
def fake_run(argv, timeout=30):
calls.append(argv)
return 0, '', ''
server.device_run = fake_run
server.assoc_clients = lambda: [{'mac': '00:11:22:33:44:55', 'iface': 'wlan1wpa', 'rssi': -60}]
server._iface_ap_info = lambda iface: ('AA:BB:CC:DD:EE:FF', 149)
status, payload = server.h_deauth_client(type('C', (), {'args': (), 'body': {'mac': '00:11:22:33:44:55'}})())
self.assertEqual(status, 200)
# 5 GHz client -> wlan1mon inject, no _pineap pin needed.
self.assertTrue(any(c[:4] == [server.HAK5CMD, 'DEAUTH_CLIENT', 'AA:BB:CC:DD:EE:FF',
'00:11:22:33:44:55'] and c[4] == '149' for c in calls))
def test_deauth_client_not_associated_502(self):
server.assoc_clients = lambda: []
status, payload = server.h_deauth_client(type('C', (), {'args': (), 'body': {'mac': '00:11:22:33:44:55'}})())
self.assertEqual(status, 502)
if __name__ == '__main__':