235 lines
12 KiB
Markdown
235 lines
12 KiB
Markdown
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 <ssid> <passphrase> 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.
|