/* * mlme_harness.cpp * Host harness for the 802.11 MLME and data path. * * Compiles the real IwxConnect.cpp (and the supplicant behind it) against a * stubbed transport, so everything the driver puts on the air and everything * it makes of what comes back can be checked without the adapter. The * firmware/radio interaction is what remains untestable here; the frame * construction, parsing, encapsulation and state machine are not. * * Speaks hex over stdio; ap_mlme.py is the peer. * * Copyright (c) 2026 Daniel Hammer */ #include #include #include #include #include #include "Drivers/Net/Wifi/Iwx.hpp" #include "Drivers/Net/Wifi/Ieee80211.hpp" #include "Drivers/Net/Wifi/Wpa.hpp" namespace Timekeeping { uint64_t g_ms = 1000; uint64_t GetMilliseconds() { return g_ms; } } // A host command is a round trip to the firmware: it takes real time, and the // clock moves on while the service loop is inside one. Modelling that is what // catches elapsed-time arithmetic that samples the clock once and then compares // it against timestamps taken later in the same pass (an unsigned underflow // that reads as an instant timeout). A frozen clock hides that class of bug // entirely, which is why this is not simply left at a constant. static constexpr uint64_t CMD_ROUND_TRIP_MS = 1; using namespace Drivers::Net::Wifi; static void puthex(const char* tag, const uint8_t* p, uint32_t n) { printf("%s ", tag); for (uint32_t i = 0; i < n; i++) printf("%02x", p[i]); printf("\n"); } // ============================================================================= // Stubbed transport // ============================================================================= namespace Drivers::Net::Wifi { IwxState g_iwx; // Host commands: record the opcode and the exact bytes, always succeed. // The payload matters -- a struct that does not match the version the // firmware advertises asserts it on real hardware. static void DumpCmd(uint32_t id, const void* data, uint32_t len) { printf("CMD %u ", id); const uint8_t* p = (const uint8_t*)data; for (uint32_t i = 0; i < len; i++) printf("%02x", p[i]); printf("\n"); Timekeeping::g_ms += CMD_ROUND_TRIP_MS; } bool IwxSendCmdPdu(uint32_t id, const void* data, uint32_t len) { DumpCmd(id, data, len); return true; } bool IwxSendCmdStatus(uint32_t id, const void* data, uint32_t len, uint32_t* statusOut) { DumpCmd(id, data, len); // ADD_STA reports success in the low byte; everything else uses 0. if (statusOut) *statusOut = (id == IWX_ADD_STA || id == IWX_ADD_STA_KEY) ? IWX_ADD_STA_SUCCESS : 0; return true; } bool IwxSendCmd(IwxHostCmd& cmd) { (void)cmd; return true; } int IwxLookupCmdVer(uint8_t group, uint8_t cmd) { // Match what AX211 firmware 89 advertises for the versions the MLME // branches on. if (group == IWX_DATA_PATH_GROUP && cmd == IWX_RLC_CONFIG_CMD) return 2; if (group == IWX_DATA_PATH_GROUP && cmd == IWX_SCD_QUEUE_CONFIG_CMD) return 3; return -1; } int IwxLookupNotifVer(uint8_t, uint8_t) { return 7; } bool IwxAbortScan() { return true; } bool IwxEnableTxq(IwxTxRing& ring, int staId, int qid, int tid) { (void)staId; (void)tid; ring.Qid = qid; ring.Active = true; ring.StageSlots = IWX_TX_STAGE_SLOTS; printf("TXQ-UP %d\n", qid); return true; } void IwxDisableTxq(IwxTxRing& ring, int staId, int tid) { (void)staId; (void)tid; ring.Active = false; printf("TXQ-DOWN\n"); } // Capture what the driver wants to put on the air. bool IwxTxFrame(IwxTxRing& ring, const uint8_t* hdr, uint32_t hdrLen, const uint8_t* payload, uint32_t payloadLen, bool encrypt, bool fixedRate) { if (!ring.Active) { printf("TX-DROP queue-down\n"); return false; } printf("TX enc=%d rate=%d ", encrypt ? 1 : 0, fixedRate ? 1 : 0); for (uint32_t i = 0; i < hdrLen; i++) printf("%02x", hdr[i]); printf(" "); for (uint32_t i = 0; i < payloadLen; i++) printf("%02x", payload[i]); printf("\n"); return true; } bool IwxSetKey(const uint8_t* key, uint32_t keyLen, uint8_t keyIdx, bool pairwise, uint8_t cipher, const uint8_t* rsc) { printf("KEY pairwise=%d idx=%u cipher=%u ", pairwise ? 1 : 0, keyIdx, cipher); for (uint32_t i = 0; i < keyLen; i++) printf("%02x", key[i]); printf(" "); if (rsc) for (int i = 0; i < 6; i++) printf("%02x", rsc[i]); printf("\n"); return true; } bool IwxRemoveKey(uint8_t keyIdx, bool pairwise, uint8_t cipher, uint32_t keyLen) { printf("KEY-REMOVE pairwise=%d idx=%u cipher=%u len=%u\n", pairwise ? 1 : 0, keyIdx, cipher, keyLen); return true; } // Sink normally provided by Wifi.cpp. void WifiRxEthernet(const uint8_t* frame, uint32_t len) { puthex("ETH", frame, len); } } // ============================================================================= // Driver // ============================================================================= static int unhex(const std::string& s, uint8_t* out) { int n = 0; for (size_t i = 0; i + 1 < s.size(); i += 2) { unsigned v; sscanf(s.c_str() + i, "%2x", &v); out[n++] = (uint8_t)v; } return n; } int main() { // A firmware state good enough for the MLME: alive, one antenna, a MAC. g_iwx.State = IwxFwState::Running; g_iwx.Fw.PhyConfig = (1u << IWX_FW_PHY_CFG_TX_CHAIN_POS) | (1u << IWX_FW_PHY_CFG_RX_CHAIN_POS); g_iwx.Nvm.ValidTxAnt = 1; g_iwx.Nvm.ValidRxAnt = 1; std::string line; uint8_t buf[4096]; while (std::getline(std::cin, line)) { if (line.rfind("MAC ", 0) == 0) { unhex(line.substr(4), g_iwx.Nvm.HwAddr); printf("DONE\n"); } else if (line.rfind("CAPA ", 0) == 0) { unsigned bit, on; sscanf(line.c_str(), "CAPA %u %u", &bit, &on); if (on) g_iwx.Fw.Capa[bit / 8] |= (uint8_t)(1 << (bit % 8)); else g_iwx.Fw.Capa[bit / 8] &= (uint8_t)~(1 << (bit % 8)); printf("DONE\n"); } else if (line.rfind("CONNECT ", 0) == 0) { // CONNECT char bssid[64], ssid[64], pass[128], rsn[256]; unsigned chan, is5; sscanf(line.c_str(), "CONNECT %63s %u %u %63s %127s %255s", bssid, &chan, &is5, ssid, pass, rsn); uint8_t bs[6]; unhex(bssid, bs); uint8_t ie[128]; int ieLen = 0; if (strcmp(rsn, "-") != 0) ieLen = unhex(rsn, ie); bool ok = IwxConnectStart(bs, (uint8_t)chan, is5 != 0, ssid, strcmp(pass, "-") == 0 ? nullptr : pass, ieLen ? ie : nullptr, (uint32_t)ieLen, 100, 2); printf("CONNECT-OK %d STATE %d\n", ok ? 1 : 0, IwxConnectState()); } else if (line.rfind("RXMGMT ", 0) == 0) { int n = unhex(line.substr(7), buf); IwxConnectRxMgmt(buf, (uint32_t)n); printf("DONE STATE %d\n", IwxConnectState()); } else if (line.rfind("RXDATA ", 0) == 0) { int n = unhex(line.substr(7), buf); IwxConnectRxData(buf, (uint32_t)n); printf("DONE STATE %d\n", IwxConnectState()); } else if (line.rfind("TXETH ", 0) == 0) { int n = unhex(line.substr(6), buf); bool ok = IwxConnectSendEthernet(buf, (uint32_t)n); printf("TXETH-OK %d\n", ok ? 1 : 0); } else if (line.rfind("TXSTATUS ", 0) == 0) { // TXSTATUS -- report N transmit outcomes, the way // IwxTxComplete does off the firmware's TX response. Drives the // link supervision that notices an access point which stopped // acknowledging without ever deauthenticating. unsigned acked, count; sscanf(line.c_str(), "TXSTATUS %u %u", &acked, &count); for (unsigned i = 0; i < count; i++) IwxConnectNoteTx(acked != 0); printf("DONE STATE %d LINK %d\n", IwxConnectState(), IwxLinkUp() ? 1 : 0); } else if (line.rfind("SERVICE", 0) == 0) { IwxConnectService(); printf("DONE STATE %d LINK %d\n", IwxConnectState(), IwxLinkUp() ? 1 : 0); } else if (line.rfind("TICK ", 0) == 0) { unsigned ms; sscanf(line.c_str(), "TICK %u", &ms); Timekeeping::g_ms += ms; printf("DONE\n"); } else if (line.rfind("ABORT", 0) == 0) { IwxConnectAbort(); printf("DONE STATE %d\n", IwxConnectState()); } else if (line.rfind("STATE", 0) == 0) { printf("STATE %d LINK %d\n", IwxConnectState(), IwxLinkUp() ? 1 : 0); } else if (line.rfind("QUIT", 0) == 0) { break; } fflush(stdout); } return 0; }