to-do: rewrite & convert to html for docs pages # Wi-Fi MontaukOS drives Intel AX210-family adapters (the reference part is the AX211). The driver scans, joins open and WPA2/WPA3-PSK networks, and presents itself to the network stack as an ordinary Ethernet interface, so `dhcp`, `ping`, `nslookup` and anything speaking sockets work over Wi-Fi exactly as they do over a cable. ``` wifi scan list nearby networks wifi connect join one dhcp pick up an address wifi status what you are connected to ``` ## Layout ``` kernel/src/Drivers/Net/Wifi/ IwxTrans.cpp PCIe transport: MMIO, MSI-X, DMA rings, firmware boot, host commands, RX processing, frame TX, key installation IwxFw.cpp .ucode / .pnvm TLV parsing IwxMvm.cpp post-ALIVE init, NVM, UMAC scan, RX dispatch IwxConnect.cpp MLME: contexts, authenticate, associate, 802.11 <-> Ethernet Wpa.cpp WPA2/WPA3-PSK supplicant (EAPOL 4-way + group rekey) Ieee80211.hpp frame, element and RSN constants Wifi.cpp subsystem facade: probe, scan table, syscalls, netif hooks kernel/src/Libraries/Crypto.cpp SHA-1/SHA-256, HMAC, PBKDF2, AES, CMAC kernel/src/Net/NetIf.cpp interface registry the Ethernet layer uses ``` ## Joining a network `SYS_WIFI_CONNECT` blocks until the link is up or the attempt fails, and returns a `WIFI_ERR_*` code the `wifi` tool turns into a specific message (wrong passphrase, unsupported security, AP out of range, and so on). The sequence: 1. **Look up the BSS.** The SSID is matched against the scan table, strongest signal first. If it is not there, one scan is run automatically and the lookup retried, so `wifi connect` works without scanning first. 2. **Negotiate ciphers.** The AP's RSN element (kept verbatim in the scan table) picks the pairwise cipher and AKM. CCMP is preferred over GCMP, plain PSK over PSK-SHA256. 3. **Derive the PMK.** PBKDF2-HMAC-SHA1 over the passphrase with the SSID as salt, 4096 iterations. A 64-character hex string is taken as a raw PSK instead. 4. **Bring up firmware contexts.** PHY, MAC, binding and station, then one TX queue on the management TID. 5. **Authenticate and associate.** Open-system authentication, then an association request carrying the SSID, supported rates and, for encrypted networks, the RSN element the supplicant built. Both are retransmitted up to four times at 400 ms. 6. **Run the 4-way handshake.** EAPOL-Key messages 1-4, then the pairwise key and the group key go into the firmware with `ADD_STA_KEY`. 7. **Report the link up.** Only now does `NetIf` see `wlan0` as usable. ## The data path Once associated the driver translates between 802.11 and Ethernet II: - **TX** - an Ethernet frame becomes a to-DS 802.11 data header plus an RFC 1042 LLC/SNAP shim carrying the EtherType. The protected bit is set once keys are installed and the firmware does the CCMP encryption. - **RX** - the firmware decrypts and strips the MIC but leaves the 8-byte CCMP header, which is skipped; the LLC/SNAP shim is replaced by an Ethernet header built from addresses 1 and 3. EAPOL frames are diverted to the supplicant instead. One TX queue carries management frames, EAPOL and non-QoS data. The firmware maps non-QoS data onto the management TID anyway, and the driver never negotiates block-ack sessions that would need per-TID queues. ### Threading The RX path runs under `IwxProcessEvents()`'s reentrancy guard and **must not send a host command** - the command's completion is pumped by the very function it would be re-entering. Anything needing a command (post-association context updates, key installation, and therefore the whole EAPOL handshake) is queued and applied from `IwxConnectService()`, which every idling core calls and which is itself serialized. Transmitting is safe from either context: it only writes a descriptor and rings the doorbell. ### Never wait on the clock under a spinlock `kcp::Spinlock::Acquire()` does `cli`, and `Timekeeping::GetMilliseconds()` is driven by the APIC timer interrupt. A wall-clock timeout inside a spinlock therefore cannot expire: the counter never advances, the loop never ends, and the machine locks solid with interrupts off - no mouse, no keyboard, no scheduler. This is the same trap `ApicTimer.cpp` documents for the idle path. `IwxSendCmd` holds a lock across a wait for the firmware's reply, so that lock is a `kcp::Mutex` (which keeps interrupts enabled), and the wait carries a spin cap as well as the clock check so it terminates even if the clock is somehow stuck. It also bails immediately once the firmware is known to have asserted, because nothing after that will ever be answered. A firmware command that goes unanswered now dumps the firmware's own error table on the first timeout - that names the command that asserted - and gives up on the adapter after three, rather than stalling for seconds per command. ### Sample the clock after the work, not before it `IwxConnectService()` sends host commands, and every one of them is a round trip to the adapter that takes real milliseconds. Each step also stamps the timestamp its deadline is measured from - `EnterState()` sets `g_stateEnteredMs`, transmitting sets `g_lastTxMs`. A `now` read at the top of the pass is therefore *older* than the stamps it is about to be compared against, and because these are unsigned counters, `now - g_stateEnteredMs` wraps to about 2^64 instead of going negative. Every deadline in the pass then reads as long expired. The symptom was a join that failed the instant it succeeded: ``` WiFi: [OK] Associated, AID 3 WiFi: [WARNING] Connection failed: timed out while joining the network WiFi: [INFO] EAPOL frame received (99 bytes) ``` The two post-association context commands moved the clock forward, the timeout check compared a stale `now` against the `g_stateEnteredMs` they had just set, and the contexts came down before the access point's first EAPOL frame could arrive - which is why message 1 shows up *after* the failure. So the clock is read only once the command-sending work in the pass is done, and the comparisons go through `Elapsed()`, which refuses to underflow. ### Teardown unwinds contexts in the order they depend on each other Firmware 89 asserts when a context is taken away while another still points at it, and the assert names the command rather than the reason. Three of these have been hit so far: | UMAC error | Cause | |---|---| | 0x2010330F | PHY binding and link activation folded into one LINK_CONFIG | | 0x2010330E | link removed while its station still existed | | 0x2000320F | link deactivated while the MAC was still marked associated | The last one is the teardown side of the same rule. While `MAC_CONFIG.is_assoc` is set, the firmware's MAC context owns the link carrying the BSS, so `TearDown()` sends a `MAC_CONFIG` MODIFY clearing `is_assoc` first, and only then removes the station, deactivates the link, removes the link, removes the MAC and drops the PHY context - the order `iwl_mvm_mld_vif_cfg_changed_station` and the paths below it use on the way down. `tests/wifi/ap_mlme.py` pins that order. ## The interface registry `Net::NetIf` replaced the Ethernet layer's direct calls into the E1000 drivers. Drivers register a name, a kind, and three function pointers; the stack sends through `NetIf::Active()`, which prefers a wired interface with a link and otherwise takes the first interface reporting one. A Wi-Fi-only machine therefore has no link until it joins a network, and a machine with a cable plugged in keeps using it. `SYS_NETSTATUS` reports whichever interface is active, so `ifconfig` shows the wireless counters once Wi-Fi is carrying traffic. ## What is supported | | | |---|---| | Open networks | yes | | WPA2-PSK, CCMP or GCMP | yes | | WPA2-PSK-SHA256 | yes | | WPA3 transition mode (PSK advertised alongside SAE) | yes, joins via PSK | | Group key rekeying | yes | | WPA3-only (SAE) | no | | Management frame protection required (MFPR) | no | | WEP, original WPA / TKIP | no | | 802.1X enterprise (EAP) | no | | Block-ack aggregation, HT/VHT/HE rates | no - legacy rates only | SAE needs finite-field or elliptic-curve arithmetic that does not belong in this kernel, and MFP needs BIP. Both are rejected up front with a specific log line rather than failing partway through a handshake. Mixed WPA/WPA2 networks that still broadcast under TKIP are refused for the same reason: the pairwise key would install but every broadcast frame would be dropped, which looks like a working connection that cannot get a DHCP lease. ## Crypto `kernel/src/Libraries/Crypto.cpp` exists because the supplicant runs in the kernel and BearSSL is a userspace library. It provides SHA-1, SHA-256, HMAC over both, PBKDF2-HMAC-SHA1, AES-128/256, RFC 3394 key wrap/unwrap and AES-CMAC. It is not a general-purpose crypto library and should not be used as one. ## Testing `./tests/wifi/run.sh` compiles the shipping sources for the host against a small shim and drives them from Python. It is the real `Crypto.cpp`, `Wpa.cpp` and `IwxConnect.cpp`, not a copy, with only the transport stubbed. - **Crypto primitives** against the published vectors - FIPS-197 for AES, RFC 2202/4231 for HMAC, RFC 3394 for key wrap, RFC 4493 for CMAC, and the IEEE 802.11i Annex H.4 WPA passphrase vectors for PBKDF2. - **The supplicant** against an independent authenticator using `hashlib` and `cryptography`: messages 2 and 4 carry MICs that verify under a PTK the AP derived itself, the installed TK and GTK match the AP's, a wrong passphrase produces a MIC the AP rejects, group rekeys and message-3 retransmissions are answered, and RSN negotiation picks the right suites across eight real-world information elements. - **The MLME and data path** against a simulated AP that decodes every frame the driver emits: the authentication request, the association request and its elements (SSID, rates, capabilities, RSN), the handshake carried inside real 802.11 data frames, key installation arguments, and the encapsulation both ways - to-DS addressing, the protected bit, LLC/SNAP, sequence numbers, broadcast delivery, and the filtering of foreign-BSSID and null-data frames. Also the branches: open networks, retransmission and give-up when the AP is silent, authentication and association rejections, and an AP-initiated deauthentication bringing the link down. - **Firmware context ordering** - the MLD command sizes and field offsets against the decoded Linux trace, and the order the teardown unwinds the contexts in, which is what the asserts above are about. The harness clock advances on every host command (`CMD_ROUND_TRIP_MS` in `mlme_harness.cpp`) rather than standing still. That detail matters: a frozen clock makes every elapsed-time comparison in the service loop trivially true or trivially false, and hid the underflow described under "Sample the clock after the work, not before it" - the host tests passed while the adapter could not join a network at all. Anything that reads `Timekeeping::GetMilliseconds()` should be tested with time actually moving. What is left needs the adapter, because it is the firmware's opinion rather than the driver's logic: whether the firmware accepts the TX command and TFD layout and actually radiates the frames, whether `ADD_STA_KEY` installs the keys the driver asks for, whether the RX MPDU descriptor is read correctly off real receptions, and whether association succeeds against a real AP's timing and rate expectations. None of that can be exercised in QEMU, which has no AX210-family device to emulate.