feat: wi-fi - join WPA2/WPA3-PSK networks and carry traffic like ethernet
This commit is contained in:
+234
@@ -0,0 +1,234 @@
|
||||
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.
|
||||
Reference in New Issue
Block a user