- deauth/kick: use full PINEAPPLE_DEAUTH_CLIENT hak5cmd app name (BUG 1)
- _allow_all_ssids: PINEAPPLE_NETWORK_FILTER_MODE deny so karma lets
clients associate after deploy (BUG 2)
- enterprise inner EAP (BUG 3), two root causes found by live experiment:
- hostapd never wildcard-matches a bare `*` identity for phase-2
lookups; eap_users now uses quoted empty prefix `""* ... [2]`,
which prefix-matches any inner identity
- pineape_auth_pass=1 forwards inner EAP to pineapd, which has no
standalone responder; deploy forces 0 and stop restores 1
Residual: plaintext MSCHAPv2 capture is firmware-blocked on 24.10.1
(MSG_DEBUG compiled out of the karma wpad; `-f` silently ignored);
documented in code comments and the validation report.
- ISSUE 4: point pineapd.@hostapd[0].mgmtiface at wlan1wpa during 5 GHz
WPA deploys so handshakes/loot populate; cleared on stop
- ISSUE 5: radio0 set_ap path polls 90 s across the wifi-reload window
and retries set_ap once if the iface still has not appeared
- ISSUE 6: skills docs use `llc && eth.type == 0x888e` (firmware tcpdump
matches 0 frames on `eapol`)
- ISSUE 7: capture state helper detects dead-pid / iface-down, cleans up,
status reports {running:false, stale:true} instead of zombie running
- tests: fix global os.path monkeypatch leaks between test modules that
broke test_mk8_events/test_reliability_api under discovery; add
regression coverage for all fixes above (463 tests green)
Live validation evidence and newly discovered firmware quirks
(DEVICE_FILTER_DELETE no-op, dropbear rate limiting) recorded in
docs/validation/2026-08-23-pineap-validation-report.md.
100 lines
4.1 KiB
Python
100 lines
4.1 KiB
Python
import os
|
|
import sys
|
|
import unittest
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'payload', 'user', 'remote_access', 'pager-webui'))
|
|
import server
|
|
|
|
|
|
def setUpModule():
|
|
__import__('importlib').reload(server)
|
|
|
|
_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):
|
|
def test_normalize(self):
|
|
self.assertEqual(server.normalize_mac(' 00:11:22:33:44:55 '), '00:11:22:33:44:55')
|
|
self.assertEqual(server.normalize_mac('aa:bb:cc:dd:ee:ff'), 'AA:BB:CC:DD:EE:FF')
|
|
|
|
def test_invalid_returns_none(self):
|
|
self.assertIsNone(server.normalize_mac('nope'))
|
|
|
|
|
|
class ClientsTest(unittest.TestCase):
|
|
def tearDown(self):
|
|
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}]
|
|
class Ctx:
|
|
args = ()
|
|
status, payload = server.h_clients(Ctx())
|
|
self.assertEqual(status, 200)
|
|
self.assertEqual(payload['count'], 1)
|
|
|
|
def _kick_env(self):
|
|
calls = []
|
|
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([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, 'PINEAPPLE_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'}})())
|
|
self.assertEqual(status, 400)
|
|
|
|
def test_deauth_client(self):
|
|
calls = []
|
|
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, 'PINEAPPLE_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__':
|
|
unittest.main()
|