feat: wi-fi - join WPA2/WPA3-PSK networks and carry traffic like ethernet
This commit is contained in:
1 parent
a01e63c717
commit
bbe1df62fd
40 files changed
+5878
-272
No files matched your search
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Ieee80211.hpp
|
||||
* 802.11 frame and RSN constants shared by the MLME and the supplicant.
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Drivers::Net::Wifi {
|
||||
|
||||
// =========================================================================
|
||||
// Frame control
|
||||
// =========================================================================
|
||||
|
||||
constexpr uint16_t IEEE80211_FC0_TYPE_MASK = 0x0c;
|
||||
constexpr uint16_t IEEE80211_FC0_TYPE_MGT = 0x00;
|
||||
constexpr uint16_t IEEE80211_FC0_TYPE_CTL = 0x04;
|
||||
constexpr uint16_t IEEE80211_FC0_TYPE_DATA = 0x08;
|
||||
constexpr uint16_t IEEE80211_FC0_SUBTYPE_MASK = 0xf0;
|
||||
|
||||
// Management subtypes (already shifted into the frame-control byte)
|
||||
constexpr uint8_t IEEE80211_SUBTYPE_ASSOC_REQ = 0x00;
|
||||
constexpr uint8_t IEEE80211_SUBTYPE_ASSOC_RESP = 0x10;
|
||||
constexpr uint8_t IEEE80211_SUBTYPE_REASSOC_REQ = 0x20;
|
||||
constexpr uint8_t IEEE80211_SUBTYPE_PROBE_REQ = 0x40;
|
||||
constexpr uint8_t IEEE80211_SUBTYPE_PROBE_RESP = 0x50;
|
||||
constexpr uint8_t IEEE80211_SUBTYPE_BEACON = 0x80;
|
||||
constexpr uint8_t IEEE80211_SUBTYPE_DISASSOC = 0xa0;
|
||||
constexpr uint8_t IEEE80211_SUBTYPE_AUTH = 0xb0;
|
||||
constexpr uint8_t IEEE80211_SUBTYPE_DEAUTH = 0xc0;
|
||||
constexpr uint8_t IEEE80211_SUBTYPE_ACTION = 0xd0;
|
||||
|
||||
// Data subtypes
|
||||
constexpr uint8_t IEEE80211_SUBTYPE_DATA = 0x00;
|
||||
constexpr uint8_t IEEE80211_SUBTYPE_QOS_DATA = 0x80;
|
||||
constexpr uint8_t IEEE80211_SUBTYPE_NULL = 0x40;
|
||||
|
||||
// Frame control byte 1
|
||||
constexpr uint8_t IEEE80211_FC1_TO_DS = 0x01;
|
||||
constexpr uint8_t IEEE80211_FC1_FROM_DS = 0x02;
|
||||
constexpr uint8_t IEEE80211_FC1_MORE_FRAG = 0x04;
|
||||
constexpr uint8_t IEEE80211_FC1_RETRY = 0x08;
|
||||
constexpr uint8_t IEEE80211_FC1_PWR_MGT = 0x10;
|
||||
constexpr uint8_t IEEE80211_FC1_MORE_DATA = 0x20;
|
||||
constexpr uint8_t IEEE80211_FC1_PROTECTED = 0x40;
|
||||
|
||||
constexpr uint32_t IEEE80211_HDR_LEN = 24;
|
||||
constexpr uint32_t IEEE80211_QOS_HDR_LEN = 26;
|
||||
|
||||
// Capability bits in beacons / association requests
|
||||
constexpr uint16_t IEEE80211_CAPINFO_ESS = 0x0001;
|
||||
constexpr uint16_t IEEE80211_CAPINFO_PRIVACY = 0x0010;
|
||||
constexpr uint16_t IEEE80211_CAPINFO_SHORT_PREAMBLE = 0x0020;
|
||||
constexpr uint16_t IEEE80211_CAPINFO_SHORT_SLOT = 0x0400;
|
||||
|
||||
// Authentication algorithms
|
||||
constexpr uint16_t IEEE80211_AUTH_ALG_OPEN = 0;
|
||||
constexpr uint16_t IEEE80211_AUTH_ALG_SAE = 3;
|
||||
|
||||
// Reason / status codes used here
|
||||
constexpr uint16_t IEEE80211_STATUS_SUCCESS = 0;
|
||||
constexpr uint16_t IEEE80211_REASON_UNSPECIFIED = 1;
|
||||
constexpr uint16_t IEEE80211_REASON_DEAUTH_LEAVING = 3;
|
||||
constexpr uint16_t IEEE80211_REASON_MIC_FAILURE = 14;
|
||||
constexpr uint16_t IEEE80211_REASON_4WAY_TIMEOUT = 15;
|
||||
|
||||
// =========================================================================
|
||||
// Information elements
|
||||
// =========================================================================
|
||||
|
||||
constexpr uint8_t IEEE80211_ELEMID_SSID = 0;
|
||||
constexpr uint8_t IEEE80211_ELEMID_RATES = 1;
|
||||
constexpr uint8_t IEEE80211_ELEMID_DSPARMS = 3;
|
||||
constexpr uint8_t IEEE80211_ELEMID_XRATES = 50;
|
||||
constexpr uint8_t IEEE80211_ELEMID_RSN = 48;
|
||||
constexpr uint8_t IEEE80211_ELEMID_HTCAPS = 45;
|
||||
constexpr uint8_t IEEE80211_ELEMID_VENDOR = 221;
|
||||
|
||||
// RSN cipher / AKM suite selectors (00-0F-AC:<type>)
|
||||
constexpr uint32_t RSN_OUI = 0x000fac;
|
||||
|
||||
constexpr uint8_t RSN_CIPHER_NONE = 0;
|
||||
constexpr uint8_t RSN_CIPHER_WEP40 = 1;
|
||||
constexpr uint8_t RSN_CIPHER_TKIP = 2;
|
||||
constexpr uint8_t RSN_CIPHER_CCMP = 4;
|
||||
constexpr uint8_t RSN_CIPHER_WEP104 = 5;
|
||||
constexpr uint8_t RSN_CIPHER_BIP_CMAC = 6;
|
||||
constexpr uint8_t RSN_CIPHER_GCMP = 8;
|
||||
constexpr uint8_t RSN_CIPHER_GCMP_256 = 9;
|
||||
constexpr uint8_t RSN_CIPHER_CCMP_256 = 10;
|
||||
|
||||
constexpr uint8_t RSN_AKM_8021X = 1;
|
||||
constexpr uint8_t RSN_AKM_PSK = 2;
|
||||
constexpr uint8_t RSN_AKM_FT_PSK = 4;
|
||||
constexpr uint8_t RSN_AKM_PSK_SHA256 = 6;
|
||||
constexpr uint8_t RSN_AKM_SAE = 8;
|
||||
constexpr uint8_t RSN_AKM_FT_SAE = 9;
|
||||
|
||||
// RSN capabilities
|
||||
constexpr uint16_t RSN_CAP_MFPR = 0x0040; // management protection required
|
||||
constexpr uint16_t RSN_CAP_MFPC = 0x0080; // management protection capable
|
||||
|
||||
// =========================================================================
|
||||
// LLC/SNAP: the 8-byte shim between an 802.11 data payload and an
|
||||
// Ethernet II EtherType.
|
||||
// =========================================================================
|
||||
|
||||
struct LlcSnapHeader {
|
||||
uint8_t dsap; // 0xaa
|
||||
uint8_t ssap; // 0xaa
|
||||
uint8_t control; // 0x03
|
||||
uint8_t oui[3]; // 00-00-00 (RFC 1042)
|
||||
uint16_t etherType; // big endian
|
||||
} __attribute__((packed));
|
||||
|
||||
constexpr uint32_t LLC_SNAP_LEN = 8;
|
||||
constexpr uint8_t LLC_SNAP_LSAP = 0xaa;
|
||||
|
||||
constexpr uint16_t ETHERTYPE_IPV4 = 0x0800;
|
||||
constexpr uint16_t ETHERTYPE_ARP = 0x0806;
|
||||
constexpr uint16_t ETHERTYPE_EAPOL = 0x888e;
|
||||
|
||||
// =========================================================================
|
||||
// Byte helpers (802.11 fields are little endian, EAPOL is big endian)
|
||||
// =========================================================================
|
||||
|
||||
inline uint16_t Get16Le(const uint8_t* p) {
|
||||
return (uint16_t)(p[0] | ((uint16_t)p[1] << 8));
|
||||
}
|
||||
inline void Put16Le(uint8_t* p, uint16_t v) {
|
||||
p[0] = (uint8_t)v; p[1] = (uint8_t)(v >> 8);
|
||||
}
|
||||
inline uint16_t Get16Be(const uint8_t* p) {
|
||||
return (uint16_t)(((uint16_t)p[0] << 8) | p[1]);
|
||||
}
|
||||
inline void Put16Be(uint8_t* p, uint16_t v) {
|
||||
p[0] = (uint8_t)(v >> 8); p[1] = (uint8_t)v;
|
||||
}
|
||||
|
||||
inline bool AddrEqual(const uint8_t* a, const uint8_t* b) {
|
||||
for (int i = 0; i < 6; i++) if (a[i] != b[i]) return false;
|
||||
return true;
|
||||
}
|
||||
inline bool AddrIsBroadcast(const uint8_t* a) {
|
||||
for (int i = 0; i < 6; i++) if (a[i] != 0xff) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <cstdint>
|
||||
#include <Pci/Pci.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <atomic>
|
||||
#include "IwxReg.hpp"
|
||||
|
||||
namespace Drivers::Net::Wifi {
|
||||
@@ -85,6 +86,13 @@ namespace Drivers::Net::Wifi {
|
||||
// Rings
|
||||
// =========================================================================
|
||||
|
||||
// Frames waiting in a TX queue are staged in single pages, one per
|
||||
// in-flight frame. The descriptor ring itself has IWX_TX_RING_COUNT
|
||||
// entries (the size the firmware was told about), but outstanding frames
|
||||
// are capped at the number of staging pages so a slot can never be reused
|
||||
// while the hardware is still reading it.
|
||||
constexpr uint32_t IWX_TX_STAGE_SLOTS = 64;
|
||||
|
||||
struct IwxTxRing {
|
||||
int Qid = 0;
|
||||
IwxDma Desc; // IwxTfhTfd[IWX_TX_RING_COUNT]
|
||||
@@ -94,6 +102,13 @@ namespace Drivers::Net::Wifi {
|
||||
uint32_t Cur = 0; // ring slot (0..count-1)
|
||||
uint32_t CurHw = 0; // hardware index (0..65535)
|
||||
uint32_t Queued = 0;
|
||||
|
||||
// TX staging (data/management queues only; the command queue stages
|
||||
// into its per-slot Cmd area instead).
|
||||
uint8_t* Stage[IWX_TX_STAGE_SLOTS] = {};
|
||||
uint64_t StagePhys[IWX_TX_STAGE_SLOTS] = {};
|
||||
uint32_t StageSlots = 0;
|
||||
bool Active = false; // configured with the firmware
|
||||
};
|
||||
|
||||
struct IwxRxRing {
|
||||
@@ -181,7 +196,11 @@ namespace Drivers::Net::Wifi {
|
||||
|
||||
IwxRxRing RxQ;
|
||||
IwxTxRing CmdQ; // queue 0: host commands
|
||||
IwxTxRing MgmtQ; // queue 1: management frames (connect path)
|
||||
// One 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.
|
||||
IwxTxRing MgmtQ;
|
||||
|
||||
// ALIVE / init-complete tracking (set from notification processing)
|
||||
volatile bool AliveIntr = false;
|
||||
@@ -194,18 +213,29 @@ namespace Drivers::Net::Wifi {
|
||||
uint32_t UmacErrorTable = 0;
|
||||
uint32_t LmacErrorTable = 0;
|
||||
uint32_t LastCmdId = 0;
|
||||
uint32_t LastCmdLen = 0;
|
||||
uint8_t LastCmdPayload[192] = {}; // dumped on a firmware assert
|
||||
bool LtrEnabled = false; // PCIe LTR capability advertised
|
||||
|
||||
// Synchronous-command bookkeeping (commands are fully serialized)
|
||||
kcp::Spinlock CmdLock; // serializes SendCmd callers
|
||||
// A Mutex, deliberately not a Spinlock: kcp::Spinlock disables
|
||||
// interrupts, and this lock is held across a wall-clock wait for the
|
||||
// firmware's response. With interrupts off the millisecond counter
|
||||
// (driven by the APIC timer interrupt) never advances, so the timeout
|
||||
// could never expire and one unanswered command would hang the machine
|
||||
// with interrupts disabled. Every caller is process or idle context;
|
||||
// the hard IRQ only latches WorkPending.
|
||||
kcp::Mutex CmdLock; // serializes SendCmd callers
|
||||
volatile bool CmdDone = false;
|
||||
volatile bool CmdWantResp = false;
|
||||
uint8_t CmdRespBuf[4096];
|
||||
volatile uint32_t CmdRespLen = 0;
|
||||
uint32_t CmdIdx = 0; // ring slot of in-flight command
|
||||
|
||||
// Reentrancy guard for ProcessEvents
|
||||
volatile bool InProcessEvents = false;
|
||||
// Reentrancy guard for ProcessEvents. Interrupts stay enabled while
|
||||
// commands wait, so two cores can genuinely race here; a plain bool
|
||||
// would let both through.
|
||||
std::atomic_flag InProcessEvents = ATOMIC_FLAG_INIT;
|
||||
|
||||
IwxNvmData Nvm;
|
||||
IwxChannel Channels[IWX_MAX_CHANNELS_TRACKED];
|
||||
@@ -215,8 +245,19 @@ namespace Drivers::Net::Wifi {
|
||||
volatile bool ScanActive = false;
|
||||
volatile bool ScanCompleted = false;
|
||||
|
||||
// Band of the BSS currently being joined; picks the legacy rate used
|
||||
// for management frames.
|
||||
bool Is5GHz = false;
|
||||
|
||||
// Serializes frame TX (the netif and the MLME both queue frames).
|
||||
// Process/idle context only, so it keeps interrupts enabled too.
|
||||
kcp::Mutex TxLock;
|
||||
|
||||
// Statistics/diagnostics
|
||||
uint64_t RxPackets = 0;
|
||||
uint64_t TxPackets = 0;
|
||||
uint64_t TxFailures = 0;
|
||||
uint64_t RxDataPackets = 0;
|
||||
uint64_t FwErrors = 0;
|
||||
};
|
||||
|
||||
@@ -271,8 +312,28 @@ namespace Drivers::Net::Wifi {
|
||||
|
||||
bool IwxCheckRfKill();
|
||||
|
||||
// TX queue management (used by connect path)
|
||||
bool IwxEnableTxq(int staId, int qid, int tid);
|
||||
// TX queue management (used by the connect path). On the new data-path
|
||||
// API the firmware picks the queue number and reports it back, so `qid` is
|
||||
// only a hint; the assigned value lands in ring.Qid.
|
||||
bool IwxEnableTxq(IwxTxRing& ring, int staId, int qid, int tid);
|
||||
void IwxDisableTxq(IwxTxRing& ring, int staId, int tid);
|
||||
|
||||
// Transmit one 802.11 frame. The header and payload are copied into a
|
||||
// staging page, wrapped in a TX_CMD and handed to the queue.
|
||||
// encrypt - let the firmware apply the installed key (data frames
|
||||
// after the handshake); cleared for auth/assoc/EAPOL
|
||||
// fixedRate - send at the lowest basic rate instead of letting the
|
||||
// firmware's rate control pick (required for management
|
||||
// frames, which have no rate table yet)
|
||||
bool IwxTxFrame(IwxTxRing& ring, const uint8_t* hdr, uint32_t hdrLen,
|
||||
const uint8_t* payload, uint32_t payloadLen,
|
||||
bool encrypt, bool fixedRate);
|
||||
|
||||
// Install or remove a hardware key for the connected station.
|
||||
bool IwxSetKey(const uint8_t* key, uint32_t keyLen, uint8_t keyIdx,
|
||||
bool pairwise, uint8_t cipher, const uint8_t* rsc);
|
||||
bool IwxRemoveKey(uint8_t keyIdx, bool pairwise, uint8_t cipher,
|
||||
uint32_t keyLen);
|
||||
|
||||
// =========================================================================
|
||||
// Firmware file parsing (IwxFw.cpp)
|
||||
@@ -300,15 +361,51 @@ namespace Drivers::Net::Wifi {
|
||||
void WifiRxMgmtFrame(const uint8_t* frame, uint32_t len, uint8_t channel,
|
||||
int8_t rssiDbm);
|
||||
|
||||
// TX completion, called from RX processing for a TX_CMD response.
|
||||
void IwxTxComplete(int qid, int idx, uint32_t status);
|
||||
|
||||
// =========================================================================
|
||||
// Connect groundwork (IwxConnect.cpp) - UNTESTED scaffolding
|
||||
// Connect / MLME (IwxConnect.cpp)
|
||||
// =========================================================================
|
||||
|
||||
// Association states reported through WifiInfo.connState.
|
||||
enum class IwxConnStateId : int {
|
||||
Idle = 0,
|
||||
ContextsUp,
|
||||
Authenticating,
|
||||
Authenticated,
|
||||
Associating,
|
||||
Associated, // 802.11 link up; open networks stop here
|
||||
Handshaking, // WPA 4-way in progress
|
||||
Connected, // keys installed (or open); data can flow
|
||||
Failed,
|
||||
};
|
||||
|
||||
// `rsnIe` is the AP's RSN element body (may be null for open networks).
|
||||
bool IwxConnectStart(const uint8_t* bssid, uint8_t channel, bool is5GHz,
|
||||
const char* ssid);
|
||||
const char* ssid, const char* password,
|
||||
const uint8_t* rsnIe, uint32_t rsnIeLen,
|
||||
uint16_t beaconInterval, uint8_t dtimPeriod);
|
||||
void IwxConnectAbort();
|
||||
// Why the last IwxConnectStart() refused: false means the radio or the
|
||||
// firmware failed, true means the network's security is unsupported. The
|
||||
// two need very different advice, so they must not be conflated.
|
||||
bool IwxConnectRefusedForSecurity();
|
||||
void IwxConnectRxMgmt(const uint8_t* frame, uint32_t len);
|
||||
// Inbound 802.11 data frame from the RX path.
|
||||
void IwxConnectRxData(const uint8_t* frame, uint32_t len);
|
||||
// Apply state changes the RX path queued (it cannot send commands itself).
|
||||
void IwxConnectService();
|
||||
int IwxConnectState();
|
||||
|
||||
// Link status for the network stack.
|
||||
bool IwxLinkUp(); // associated and keyed
|
||||
const uint8_t* IwxConnectBssid();
|
||||
const char* IwxConnectSsid();
|
||||
|
||||
// Send an Ethernet frame over the air (called by the netif).
|
||||
bool IwxConnectSendEthernet(const uint8_t* frame, uint32_t len);
|
||||
|
||||
// Sink for decapsulated Ethernet frames, implemented by Wifi.cpp.
|
||||
void WifiRxEthernet(const uint8_t* frame, uint32_t len);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
#include "Iwx.hpp"
|
||||
#include "Ieee80211.hpp"
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
@@ -629,12 +630,23 @@ namespace Drivers::Net::Wifi {
|
||||
uint32_t offset = (uint32_t)(frame - (const uint8_t*)pkt);
|
||||
if (offset + len > bufLen) return;
|
||||
|
||||
// The firmware pads the header to a 4-byte boundary when the flag is
|
||||
// set; the payload then starts two bytes later.
|
||||
// The firmware pads a MAC header that is not a multiple of four bytes
|
||||
// (in practice: every QoS data header) out to alignment by inserting
|
||||
// two bytes between the header -- and the crypto IV, if any -- and
|
||||
// the payload; mpdu_len counts them. Cut them out so the rest of the
|
||||
// driver sees a contiguous 802.11 frame. Getting this wrong loses
|
||||
// every QoS data frame while management frames keep working.
|
||||
if (desc->mac_flags2 & IWX_RX_MPDU_MFLG2_PAD) {
|
||||
if (len < 2) return;
|
||||
frame += 2;
|
||||
if (offset + 2 + len > bufLen) return;
|
||||
uint32_t hdrLen = 24;
|
||||
if ((frame[0] & IEEE80211_FC0_TYPE_MASK) == IEEE80211_FC0_TYPE_DATA
|
||||
&& (frame[0] & IEEE80211_SUBTYPE_QOS_DATA))
|
||||
hdrLen += 2;
|
||||
if (frame[1] & IEEE80211_FC1_PROTECTED)
|
||||
hdrLen += IWX_CCMP_HDR_LEN;
|
||||
if (len < hdrLen + 2) return;
|
||||
memmove((uint8_t*)frame + hdrLen, frame + hdrLen + 2,
|
||||
len - hdrLen - 2);
|
||||
len -= 2;
|
||||
}
|
||||
|
||||
int energyA = desc->v3.energy_a ? -(int)desc->v3.energy_a : -256;
|
||||
@@ -642,6 +654,26 @@ namespace Drivers::Net::Wifi {
|
||||
int rssi = energyA > energyB ? energyA : energyB;
|
||||
if (rssi < -128) rssi = -128;
|
||||
|
||||
uint8_t type = (uint8_t)(frame[0] & IEEE80211_FC0_TYPE_MASK);
|
||||
|
||||
if (type == IEEE80211_FC0_TYPE_DATA) {
|
||||
// Encrypted frames are decrypted by the firmware, which strips the
|
||||
// MIC but leaves the CCMP/GCMP header in place; drop anything that
|
||||
// failed to decrypt rather than handing up ciphertext.
|
||||
if (frame[1] & IEEE80211_FC1_PROTECTED) {
|
||||
uint32_t sec = desc->status & IWX_RX_MPDU_STATUS_SEC_MASK;
|
||||
bool ok = (desc->status & IWX_RX_MPDU_STATUS_DECRYPTED)
|
||||
&& (desc->status & IWX_RX_MPDU_STATUS_MIC_OK)
|
||||
&& (sec == IWX_RX_MPDU_STATUS_SEC_CCM
|
||||
|| sec == IWX_RX_MPDU_STATUS_SEC_GCM);
|
||||
if (!ok) return;
|
||||
}
|
||||
IwxConnectRxData(frame, len);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type != IEEE80211_FC0_TYPE_MGT) return;
|
||||
|
||||
WifiRxMgmtFrame(frame, len, desc->v3.channel, (int8_t)rssi);
|
||||
}
|
||||
|
||||
|
||||
@@ -567,7 +567,10 @@ inline uint32_t IwxRxPacketPayloadLen(const IwxRxPacket* pkt) {
|
||||
return IwxRxPacketLen(pkt) - sizeof(IwxCmdHeader);
|
||||
}
|
||||
|
||||
// DQA queue assignment
|
||||
// DQA queue assignment. The management queue number is only a hint: on the v3
|
||||
// data-path API the firmware assigns the queue and reports the number back.
|
||||
// TID 15 carries management frames, EAPOL and non-QoS data -- the firmware maps
|
||||
// non-QoS data onto this TID regardless.
|
||||
constexpr int IWX_DQA_CMD_QUEUE = 0;
|
||||
constexpr int IWX_DQA_MGMT_QUEUE = 1;
|
||||
constexpr int IWX_MGMT_TID = 15;
|
||||
@@ -949,6 +952,20 @@ constexpr uint8_t IWX_SCAN_ADWELL_N_APS_SOCIAL_CHS = 2;
|
||||
|
||||
constexpr uint32_t IWX_RX_MPDU_RES_STATUS_CRC_OK = 1u << 0;
|
||||
constexpr uint32_t IWX_RX_MPDU_RES_STATUS_OVERRUN_OK = 1u << 1;
|
||||
constexpr uint32_t IWX_RX_MPDU_STATUS_KEY_VALID = 1u << 3;
|
||||
constexpr uint32_t IWX_RX_MPDU_STATUS_ICV_OK = 1u << 5;
|
||||
constexpr uint32_t IWX_RX_MPDU_STATUS_MIC_OK = 1u << 6;
|
||||
constexpr uint32_t IWX_RX_MPDU_STATUS_SEC_MASK = 0x7u << 8;
|
||||
constexpr uint32_t IWX_RX_MPDU_STATUS_SEC_NONE = 0x0u << 8;
|
||||
constexpr uint32_t IWX_RX_MPDU_STATUS_SEC_WEP = 0x1u << 8;
|
||||
constexpr uint32_t IWX_RX_MPDU_STATUS_SEC_CCM = 0x2u << 8;
|
||||
constexpr uint32_t IWX_RX_MPDU_STATUS_SEC_TKIP = 0x3u << 8;
|
||||
constexpr uint32_t IWX_RX_MPDU_STATUS_SEC_EXT_ENC = 0x4u << 8;
|
||||
constexpr uint32_t IWX_RX_MPDU_STATUS_SEC_GCM = 0x5u << 8;
|
||||
constexpr uint32_t IWX_RX_MPDU_STATUS_DECRYPTED = 1u << 11;
|
||||
|
||||
// The hardware strips the CCMP/GCMP MIC but leaves the 8-byte IV in place.
|
||||
constexpr uint32_t IWX_CCMP_HDR_LEN = 8;
|
||||
constexpr uint8_t IWX_RX_MPDU_MFLG2_PAD = 0x20;
|
||||
constexpr uint8_t IWX_RX_MPDU_MFLG2_AMSDU = 0x40;
|
||||
|
||||
@@ -1000,21 +1017,46 @@ constexpr uint32_t IWX_FW_CTXT_ACTION_REMOVE = 3;
|
||||
constexpr uint32_t IWX_LMAC_24G_INDEX = 0;
|
||||
constexpr uint32_t IWX_LMAC_5G_INDEX = 1;
|
||||
|
||||
// PHY context
|
||||
struct IwxFwChannelInfoV1 {
|
||||
// PHY context.
|
||||
//
|
||||
// The channel-info sub-structure has two shapes and the firmware picks which
|
||||
// one it expects via IWX_UCODE_TLV_CAPA_ULTRA_HB_CHANNELS. AX211 firmware 89
|
||||
// sets that bit, so it wants the v2 form: a 32-bit channel number first, then
|
||||
// the band. Sending the 4-byte v1 form to a firmware expecting 8 bytes
|
||||
// shifts lmac_id/rxchain_info and asserts the firmware, which then stops
|
||||
// answering host commands entirely.
|
||||
struct IwxFwChannelInfoV1 { // CHANNEL_CONFIG_API_S_VER_1
|
||||
uint8_t band;
|
||||
uint8_t channel;
|
||||
uint8_t width;
|
||||
uint8_t ctrl_pos;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct IwxFwChannelInfo { // CHANNEL_CONFIG_API_S_VER_2
|
||||
uint32_t channel;
|
||||
uint8_t band;
|
||||
uint8_t width;
|
||||
uint8_t ctrl_pos;
|
||||
uint8_t reserved;
|
||||
} __attribute__((packed));
|
||||
|
||||
constexpr uint8_t IWX_PHY_VHT_CHANNEL_MODE20 = 0x0;
|
||||
constexpr uint8_t IWX_PHY_VHT_CTRL_POS_1_BELOW = 0x0;
|
||||
constexpr uint32_t IWX_PHY_RX_CHAIN_VALID_POS = 1;
|
||||
constexpr uint32_t IWX_PHY_RX_CHAIN_CNT_POS = 10;
|
||||
constexpr uint32_t IWX_PHY_RX_CHAIN_MIMO_CNT_POS = 12;
|
||||
|
||||
struct IwxPhyContextCmd { // PHY_CONTEXT_CMD_API_VER_3/4 (non-UHB)
|
||||
struct IwxPhyContextCmd { // PHY_CONTEXT_CMD_API_S_VER_3/4, UHB
|
||||
uint32_t id_and_color;
|
||||
uint32_t action;
|
||||
IwxFwChannelInfo ci;
|
||||
uint32_t lmac_id;
|
||||
uint32_t rxchain_info; // reserved from VER_4 on
|
||||
uint32_t dsp_cfg_flags;
|
||||
uint32_t reserved;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct IwxPhyContextCmdV1Chan { // same command, pre-UHB channel info
|
||||
uint32_t id_and_color;
|
||||
uint32_t action;
|
||||
IwxFwChannelInfoV1 ci;
|
||||
@@ -1024,11 +1066,15 @@ struct IwxPhyContextCmd { // PHY_CONTEXT_CMD_API_VER_3/4 (non-UHB)
|
||||
uint32_t reserved;
|
||||
} __attribute__((packed));
|
||||
|
||||
static_assert(sizeof(IwxPhyContextCmd) == 32, "PHY context (UHB) must be 32 bytes");
|
||||
static_assert(sizeof(IwxPhyContextCmdV1Chan) == 28, "PHY context (legacy) must be 28 bytes");
|
||||
|
||||
// MAC context
|
||||
constexpr uint32_t IWX_FW_MAC_TYPE_BSS_STA = 5;
|
||||
constexpr uint32_t IWX_TSF_ID_A = 0;
|
||||
constexpr uint32_t IWX_AC_NUM = 4;
|
||||
constexpr uint32_t IWX_MAC_QOS_FLG_UPDATE_EDCA = 1u << 0;
|
||||
constexpr uint32_t IWX_MAC_QOS_FLG_TGN = 1u << 1;
|
||||
constexpr uint32_t IWX_MAC_FILTER_IN_CONTROL_AND_MGMT = 1u << 1;
|
||||
constexpr uint32_t IWX_MAC_FILTER_ACCEPT_GRP = 1u << 2;
|
||||
constexpr uint32_t IWX_MAC_FILTER_IN_BEACON = 1u << 6;
|
||||
@@ -1055,6 +1101,16 @@ struct IwxMacDataSta {
|
||||
uint32_t assoc_beacon_arrive_time;
|
||||
} __attribute__((packed));
|
||||
|
||||
// The per-mac-type tail of MAC_CONTEXT_CMD is a union in the firmware API, so
|
||||
// the command length is that of its LARGEST member -- p2p_sta, which is
|
||||
// iwl_mac_data_sta plus a ctwin word. Sending only the sta member makes the
|
||||
// command four bytes short and asserts the firmware (observed on AX211 fw 89:
|
||||
// UMAC error 0x201002FF on command 0x128).
|
||||
struct IwxMacDataP2pSta {
|
||||
IwxMacDataSta sta;
|
||||
uint32_t ctwin;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct IwxMacCtxCmd { // IWX_MAC_CONTEXT_CMD_API_S_VER_1 (sta)
|
||||
uint32_t id_and_color;
|
||||
uint32_t action;
|
||||
@@ -1072,9 +1128,15 @@ struct IwxMacCtxCmd { // IWX_MAC_CONTEXT_CMD_API_S_VER_1 (sta)
|
||||
uint32_t filter_flags;
|
||||
uint32_t qos_flags;
|
||||
IwxAcQos ac[IWX_AC_NUM + 1];
|
||||
IwxMacDataSta sta;
|
||||
union {
|
||||
IwxMacDataSta sta;
|
||||
IwxMacDataP2pSta p2p_sta; // the largest member: sizes the command
|
||||
} u;
|
||||
} __attribute__((packed));
|
||||
|
||||
static_assert(sizeof(IwxMacCtxCmd) == 148,
|
||||
"MAC context command must stay 148 bytes (union sized by p2p_sta)");
|
||||
|
||||
// Binding context
|
||||
constexpr uint32_t IWX_MAX_MACS_IN_BINDING = 3;
|
||||
struct IwxBindingCmd {
|
||||
@@ -1178,8 +1240,9 @@ struct IwxRlcConfigCmd {
|
||||
uint8_t reserved[3];
|
||||
} __attribute__((packed));
|
||||
|
||||
// Session protection
|
||||
constexpr uint32_t IWX_SESSION_PROTECT_CONF_ASSOC = 1;
|
||||
// Session protection. ASSOC is the first value of
|
||||
// enum iwl_session_prot_conf_id, i.e. zero -- 1 is GO_CLIENT_ASSOC.
|
||||
constexpr uint32_t IWX_SESSION_PROTECT_CONF_ASSOC = 0;
|
||||
struct IwxSessionProtCmd {
|
||||
uint32_t id_and_color;
|
||||
uint32_t action;
|
||||
@@ -1196,6 +1259,313 @@ struct IwxSessionProtNotif {
|
||||
uint32_t conf_id;
|
||||
} __attribute__((packed));
|
||||
|
||||
// =============================================================================
|
||||
// MLD API (MAC_CONF group)
|
||||
//
|
||||
// Firmware that advertises IWX_UCODE_TLV_CAPA_MLD_API_SUPPORT -- which AX211
|
||||
// firmware 89 does -- implements these instead of the legacy MAC_CONTEXT_CMD /
|
||||
// BINDING_CONTEXT_CMD / ADD_STA. The legacy commands are simply absent, and
|
||||
// sending one asserts the firmware.
|
||||
//
|
||||
// Every layout and size below was confirmed against a host-command trace taken
|
||||
// from Linux driving this same adapter and firmware (see
|
||||
// tests/wifi/decode_iwl_trace.py), not inferred from a kernel header.
|
||||
// =============================================================================
|
||||
|
||||
constexpr uint8_t IWX_MAC_CONFIG_CMD = 0x08; // MAC_CONF group
|
||||
constexpr uint8_t IWX_LINK_CONFIG_CMD = 0x09;
|
||||
constexpr uint8_t IWX_STA_CONFIG_CMD = 0x0a;
|
||||
constexpr uint8_t IWX_AUX_STA_CMD = 0x0b;
|
||||
constexpr uint8_t IWX_STA_REMOVE_CMD = 0x0c;
|
||||
|
||||
// iwl_mac_config_filter_flags
|
||||
constexpr uint32_t IWX_MAC_CFG_FILTER_PROMISC = 1u << 0;
|
||||
constexpr uint32_t IWX_MAC_CFG_FILTER_ACCEPT_CTRL_MGMT = 1u << 1;
|
||||
constexpr uint32_t IWX_MAC_CFG_FILTER_ACCEPT_GRP = 1u << 2;
|
||||
constexpr uint32_t IWX_MAC_CFG_FILTER_ACCEPT_BEACON = 1u << 3;
|
||||
constexpr uint32_t IWX_MAC_CFG_FILTER_ACCEPT_BCAST_PROBE_RESP = 1u << 4;
|
||||
constexpr uint32_t IWX_MAC_CFG_FILTER_ACCEPT_PROBE_REQ = 1u << 5;
|
||||
|
||||
struct IwxMacClientData { // MAC_CONTEXT_CONFIG_CLIENT_DATA_API_S_VER_2
|
||||
uint8_t is_assoc;
|
||||
uint8_t esr_transition_timeout;
|
||||
uint16_t medium_sync_delay;
|
||||
uint16_t assoc_id;
|
||||
uint16_t reserved1;
|
||||
uint16_t data_policy;
|
||||
uint16_t reserved2;
|
||||
uint32_t ctwin;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct IwxMacConfigCmd { // MAC_CONTEXT_CONFIG_CMD_API_S_VER_2
|
||||
uint32_t id_and_color;
|
||||
uint32_t action;
|
||||
uint32_t mac_type;
|
||||
uint8_t local_mld_addr[6];
|
||||
uint16_t reserved_for_local_mld_addr;
|
||||
uint32_t filter_flags;
|
||||
uint16_t he_support;
|
||||
uint16_t he_ap_support;
|
||||
uint32_t eht_support;
|
||||
uint32_t nic_not_ack_enabled;
|
||||
IwxMacClientData client; // union with p2p_dev; client is the largest
|
||||
} __attribute__((packed));
|
||||
|
||||
static_assert(sizeof(IwxMacConfigCmd) == 52, "MAC_CONFIG_CMD must be 52 bytes");
|
||||
|
||||
// iwl_link_ctx_modify_flags
|
||||
constexpr uint32_t IWX_LINK_MODIFY_ACTIVE = 1u << 0;
|
||||
constexpr uint32_t IWX_LINK_MODIFY_RATES_INFO = 1u << 1;
|
||||
constexpr uint32_t IWX_LINK_MODIFY_PROTECT_FLAGS = 1u << 2;
|
||||
constexpr uint32_t IWX_LINK_MODIFY_QOS_PARAMS = 1u << 3;
|
||||
constexpr uint32_t IWX_LINK_MODIFY_BEACON_TIMING = 1u << 4;
|
||||
constexpr uint32_t IWX_LINK_MODIFY_HE_PARAMS = 1u << 5;
|
||||
constexpr uint32_t IWX_LINK_MODIFY_ALL = 0xff;
|
||||
|
||||
struct IwxHeBackoffConf { // AC_QOS_MU_EDCA_API_S
|
||||
uint16_t cwmin;
|
||||
uint16_t cwmax;
|
||||
uint16_t aifsn;
|
||||
uint16_t mu_time;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct IwxLinkConfigCmd { // LINK_CONTEXT_CONFIG_CMD_API_S_VER_1/2/3
|
||||
uint32_t action;
|
||||
uint32_t link_id;
|
||||
uint32_t mac_id;
|
||||
uint32_t phy_id; // IWX_FW_CTXT_INVALID until bound
|
||||
uint8_t local_link_addr[6];
|
||||
uint16_t reserved_for_local_link_addr;
|
||||
uint32_t modify_mask;
|
||||
uint32_t active;
|
||||
uint32_t listen_lmac;
|
||||
uint32_t cck_rates;
|
||||
uint32_t ofdm_rates;
|
||||
uint32_t cck_short_preamble;
|
||||
uint32_t short_slot;
|
||||
uint32_t protection_flags;
|
||||
uint32_t qos_flags;
|
||||
IwxAcQos ac[IWX_AC_NUM + 1];
|
||||
uint8_t htc_trig_based_pkt_ext;
|
||||
uint8_t rand_alloc_ecwmin;
|
||||
uint8_t rand_alloc_ecwmax;
|
||||
uint8_t ndp_fdbk_buff_th_exp;
|
||||
IwxHeBackoffConf trig_based_txf[IWX_AC_NUM];
|
||||
uint32_t bi;
|
||||
uint32_t dtim_interval;
|
||||
uint16_t puncture_mask; // removed in _VER_3
|
||||
uint16_t frame_time_rts_th;
|
||||
uint32_t flags;
|
||||
uint32_t flags_mask;
|
||||
uint8_t ref_bssid_addr[6];
|
||||
uint16_t reserved_for_ref_bssid_addr;
|
||||
uint8_t bssid_index;
|
||||
uint8_t bss_color;
|
||||
uint8_t spec_link_id;
|
||||
uint8_t reserved2;
|
||||
uint8_t ibss_bssid_addr[6];
|
||||
uint16_t reserved_for_ibss_bssid_addr;
|
||||
uint32_t reserved3[8];
|
||||
} __attribute__((packed));
|
||||
|
||||
static_assert(sizeof(IwxLinkConfigCmd) == 208, "LINK_CONFIG_CMD must be 208 bytes");
|
||||
|
||||
// 2 spatial streams x 5 bandwidth indices x 2 thresholds
|
||||
struct IwxHePktExtV2 {
|
||||
uint8_t pkt_ext_qam_th[2][5][2];
|
||||
} __attribute__((packed));
|
||||
|
||||
struct IwxStaConfigCmd { // STA_CMD_API_S_VER_1
|
||||
uint32_t sta_id;
|
||||
uint32_t link_id;
|
||||
uint8_t peer_mld_address[6];
|
||||
uint16_t reserved_for_peer_mld_address;
|
||||
uint8_t peer_link_address[6];
|
||||
uint16_t reserved_for_peer_link_address;
|
||||
uint32_t station_type;
|
||||
uint32_t assoc_id;
|
||||
uint32_t beamform_flags;
|
||||
uint32_t mfp;
|
||||
uint32_t mimo;
|
||||
uint32_t mimo_protection;
|
||||
uint32_t ack_enabled;
|
||||
uint32_t trig_rnd_alloc;
|
||||
uint32_t tx_ampdu_spacing;
|
||||
uint32_t tx_ampdu_max_size;
|
||||
uint32_t sp_length;
|
||||
uint32_t uapsd_acs;
|
||||
IwxHePktExtV2 pkt_ext;
|
||||
uint32_t htc_flags;
|
||||
} __attribute__((packed));
|
||||
|
||||
static_assert(sizeof(IwxStaConfigCmd) == 96, "STA_CONFIG_CMD must be 96 bytes");
|
||||
|
||||
struct IwxStaRemoveCmd {
|
||||
uint32_t sta_id;
|
||||
} __attribute__((packed));
|
||||
|
||||
// =============================================================================
|
||||
// TX path (AX210 / "new TX API")
|
||||
// =============================================================================
|
||||
|
||||
// TX_CMD on a data queue carries the *short* 4-byte command header and stays in
|
||||
// group 0; only host commands on the command queue are re-tagged into
|
||||
// LONG_GROUP. Layout of a queued frame:
|
||||
//
|
||||
// [IwxCmdHeader 4][IwxTxCmdGen3 28][802.11 header][pad to 4][payload]
|
||||
//
|
||||
// TB0 covers the first 20 bytes, TB1 the rest of the header block (dword
|
||||
// aligned) and TB2 the payload, matching iwl_txq_gen2_build_tx().
|
||||
|
||||
struct IwxDramSecInfo {
|
||||
uint32_t pn_low;
|
||||
uint16_t pn_high;
|
||||
uint16_t aux_info;
|
||||
} __attribute__((packed)); // DRAM_SEC_INFO_API_S_VER_1
|
||||
|
||||
struct IwxTxCmdGen3 {
|
||||
uint16_t len; // total 802.11 frame length, plaintext
|
||||
uint16_t flags; // IWX_TX_FLAGS_*
|
||||
uint32_t offload_assist;
|
||||
IwxDramSecInfo dram_info;
|
||||
uint32_t rate_n_flags;
|
||||
uint8_t reserved[8]; // named "ttl" in TX_CMD_API_S_VER_8
|
||||
// 802.11 header follows
|
||||
} __attribute__((packed)); // TX_CMD_API_S_VER_8 / _10
|
||||
|
||||
static_assert(sizeof(IwxTxCmdGen3) == 28, "TX command header must stay 28 bytes");
|
||||
|
||||
// iwl_tx_cmd_flags (TX_FLAGS_BITS_API_S_VER_3)
|
||||
constexpr uint16_t IWX_TX_FLAGS_CMD_RATE = 1 << 0; // use rate_n_flags
|
||||
constexpr uint16_t IWX_TX_FLAGS_ENCRYPT_DIS = 1 << 1; // send in the clear
|
||||
constexpr uint16_t IWX_TX_FLAGS_HIGH_PRI = 1 << 2;
|
||||
constexpr uint16_t IWX_TX_FLAGS_RTS = 1 << 3;
|
||||
constexpr uint16_t IWX_TX_FLAGS_CTS = 1 << 4;
|
||||
|
||||
// iwl_tx_offload_assist_flags_pos
|
||||
constexpr uint32_t IWX_TX_CMD_OFFLD_MH_SIZE_POS = 8; // header length in words
|
||||
constexpr uint32_t IWX_TX_CMD_OFFLD_MH_MASK = 0x1f;
|
||||
constexpr uint32_t IWX_TX_CMD_OFFLD_PAD = 1u << 13;
|
||||
constexpr uint32_t IWX_TX_CMD_OFFLD_AMSDU = 1u << 14;
|
||||
|
||||
// rate_n_flags. The firmware advertises which encoding it wants: TX_CMD
|
||||
// notification version 7 and later use the "version 2" layout, where bits 10-8
|
||||
// select the modulation and bits 3-0 index the legacy rate table. Older
|
||||
// firmware uses the version 1 layout, which carries the PLCP value directly and
|
||||
// flags CCK with bit 9. Antenna selection sits at bits 15-14 in both.
|
||||
constexpr uint32_t IWX_RATE_MCS_ANT_POS = 14;
|
||||
constexpr uint32_t IWX_RATE_MCS_ANT_A = 1u << IWX_RATE_MCS_ANT_POS;
|
||||
constexpr uint32_t IWX_RATE_MCS_ANT_B = 2u << IWX_RATE_MCS_ANT_POS;
|
||||
|
||||
// version 2
|
||||
constexpr uint32_t IWX_RATE_MCS_MOD_TYPE_POS = 8;
|
||||
constexpr uint32_t IWX_RATE_MCS_MOD_CCK = 0u << IWX_RATE_MCS_MOD_TYPE_POS;
|
||||
constexpr uint32_t IWX_RATE_MCS_MOD_LEGACY_OFDM = 1u << IWX_RATE_MCS_MOD_TYPE_POS;
|
||||
constexpr uint32_t IWX_RATE_LEGACY_RATE_MSK = 0x7;
|
||||
constexpr uint32_t IWX_RATE_MCS_CHAN_WIDTH_20 = 0u << 11;
|
||||
|
||||
// version 1 (PLCP encoded directly in bits 7-0)
|
||||
constexpr uint32_t IWX_RATE_MCS_CCK_MSK_V1 = 1u << 9;
|
||||
constexpr uint8_t IWX_RATE_1M_PLCP = 10;
|
||||
constexpr uint8_t IWX_RATE_6M_PLCP = 13;
|
||||
|
||||
// TX response (TX_CMD notification). Only the leading fields are used: the
|
||||
// driver just needs the slot back and a success/failure verdict.
|
||||
constexpr uint32_t IWX_TX_STATUS_MSK = 0x000000ff;
|
||||
constexpr uint32_t IWX_TX_STATUS_SUCCESS = 0x01;
|
||||
constexpr uint32_t IWX_TX_STATUS_DIRECT_DONE = 0x02;
|
||||
|
||||
struct IwxTxResp {
|
||||
uint8_t frame_count;
|
||||
uint8_t bt_kill_count;
|
||||
uint8_t failure_rts;
|
||||
uint8_t failure_frame;
|
||||
uint32_t initial_rate;
|
||||
uint16_t wireless_media_time;
|
||||
uint8_t pa_status;
|
||||
uint8_t pa_integ_res_a[3];
|
||||
uint8_t pa_integ_res_b[3];
|
||||
uint8_t pa_integ_res_c[3];
|
||||
uint16_t measurement_req_id;
|
||||
uint8_t reduced_tpc;
|
||||
uint8_t reserved;
|
||||
uint32_t tfd_info;
|
||||
uint16_t seq_ctl;
|
||||
uint16_t byte_cnt;
|
||||
uint8_t tlc_info;
|
||||
uint8_t ra_tid;
|
||||
uint16_t frame_ctrl;
|
||||
// followed by per-frame status entries; entry 0 is what matters here
|
||||
uint16_t status;
|
||||
uint16_t sequence;
|
||||
} __attribute__((packed));
|
||||
|
||||
// =============================================================================
|
||||
// ADD_STA_KEY (hardware key installation)
|
||||
// =============================================================================
|
||||
|
||||
// iwl_sta_key_flag
|
||||
constexpr uint16_t IWX_STA_KEY_FLG_NO_ENC = 0 << 0;
|
||||
constexpr uint16_t IWX_STA_KEY_FLG_WEP = 1 << 0;
|
||||
constexpr uint16_t IWX_STA_KEY_FLG_CCM = 2 << 0;
|
||||
constexpr uint16_t IWX_STA_KEY_FLG_TKIP = 3 << 0;
|
||||
constexpr uint16_t IWX_STA_KEY_FLG_EXT = 4 << 0;
|
||||
constexpr uint16_t IWX_STA_KEY_FLG_GCMP = 5 << 0;
|
||||
constexpr uint16_t IWX_STA_KEY_FLG_CMAC = 6 << 0;
|
||||
constexpr uint16_t IWX_STA_KEY_FLG_EN_MSK = 7 << 0;
|
||||
constexpr uint16_t IWX_STA_KEY_FLG_WEP_KEY_MAP = 1 << 3;
|
||||
constexpr uint16_t IWX_STA_KEY_FLG_KEYID_POS = 8;
|
||||
constexpr uint16_t IWX_STA_KEY_FLG_KEYID_MSK = 3 << IWX_STA_KEY_FLG_KEYID_POS;
|
||||
constexpr uint16_t IWX_STA_KEY_NOT_VALID = 1 << 11;
|
||||
constexpr uint16_t IWX_STA_KEY_FLG_KEY_32BYTES = 1 << 12;
|
||||
constexpr uint16_t IWX_STA_KEY_MULTICAST = 1 << 14;
|
||||
constexpr uint16_t IWX_STA_KEY_MFP = 1 << 15;
|
||||
|
||||
struct IwxAddStaKeyCommon {
|
||||
uint8_t sta_id;
|
||||
uint8_t key_offset;
|
||||
uint16_t key_flags;
|
||||
uint8_t key[32];
|
||||
uint8_t rx_secur_seq_cnt[16];
|
||||
} __attribute__((packed));
|
||||
|
||||
struct IwxAddStaKeyCmd {
|
||||
IwxAddStaKeyCommon common;
|
||||
uint64_t rx_mic_key;
|
||||
uint64_t tx_mic_key;
|
||||
uint64_t transmit_seq_cnt;
|
||||
} __attribute__((packed));
|
||||
|
||||
static_assert(sizeof(IwxAddStaKeyCmd) == 76, "ADD_STA_KEY layout changed");
|
||||
|
||||
// SEC_KEY_CMD (DATA_PATH group): key installation for the MLD firmware. The
|
||||
// legacy ADD_STA_KEY above is absent from fw 89 like the rest of the legacy
|
||||
// station API; the Linux trace installs both the PTK and the GTK with this.
|
||||
constexpr uint8_t IWX_SEC_KEY_CMD = 0x18;
|
||||
|
||||
// iwl_sec_key_flags: cipher in the low 3 bits, modifiers above.
|
||||
constexpr uint32_t IWX_SEC_KEY_FLAG_CIPHER_CCMP = 0x02;
|
||||
constexpr uint32_t IWX_SEC_KEY_FLAG_CIPHER_TKIP = 0x03;
|
||||
constexpr uint32_t IWX_SEC_KEY_FLAG_CIPHER_GCMP = 0x05;
|
||||
constexpr uint32_t IWX_SEC_KEY_FLAG_NO_TX = 0x08;
|
||||
constexpr uint32_t IWX_SEC_KEY_FLAG_KEY_SIZE = 0x10; // 256-bit key
|
||||
constexpr uint32_t IWX_SEC_KEY_FLAG_MFP = 0x20;
|
||||
constexpr uint32_t IWX_SEC_KEY_FLAG_MCAST_KEY = 0x40;
|
||||
|
||||
struct IwxSecKeyCmd { // SEC_KEY_CMD_API_S_VER_1 (the add form)
|
||||
uint32_t action; // IWX_FW_CTXT_ACTION_*
|
||||
uint32_t sta_mask;
|
||||
uint32_t key_id;
|
||||
uint32_t key_flags;
|
||||
uint8_t key[32];
|
||||
uint8_t tkip_mic_rx_key[8];
|
||||
uint8_t tkip_mic_tx_key[8];
|
||||
uint64_t rx_seq;
|
||||
uint64_t tx_seq;
|
||||
} __attribute__((packed));
|
||||
|
||||
static_assert(sizeof(IwxSecKeyCmd) == 80, "SEC_KEY_CMD must be 80 bytes");
|
||||
|
||||
// =============================================================================
|
||||
// Firmware error log (read from device SRAM after an assert)
|
||||
// =============================================================================
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
|
||||
#include "Iwx.hpp"
|
||||
#include "Ieee80211.hpp"
|
||||
#include <Pci/Pci.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Memory/Paging.hpp>
|
||||
@@ -459,11 +460,15 @@ namespace Drivers::Net::Wifi {
|
||||
asm volatile("" ::: "memory");
|
||||
}
|
||||
|
||||
static bool IwxAllocTxRing(IwxTxRing& ring, int qid) {
|
||||
// `stageSlots` reserves one page per concurrently queued frame; pass 0 for
|
||||
// queues that only ever carry host commands.
|
||||
static bool IwxAllocTxRing(IwxTxRing& ring, int qid, uint32_t stageSlots = 0) {
|
||||
ring.Qid = qid;
|
||||
ring.Cur = 0;
|
||||
ring.CurHw = 0;
|
||||
ring.Queued = 0;
|
||||
ring.StageSlots = 0;
|
||||
ring.Active = false;
|
||||
|
||||
if (!IwxDmaAlloc(ring.Desc, sizeof(IwxTfhTfd) * IWX_TX_RING_COUNT))
|
||||
return false;
|
||||
@@ -476,6 +481,15 @@ namespace Drivers::Net::Wifi {
|
||||
// are staged in this page instead of the per-slot command area.
|
||||
if (!IwxDmaAlloc(ring.Bounce, 4096))
|
||||
return false;
|
||||
|
||||
if (stageSlots > IWX_TX_STAGE_SLOTS) stageSlots = IWX_TX_STAGE_SLOTS;
|
||||
for (uint32_t i = 0; i < stageSlots; i++) {
|
||||
void* p = Memory::g_pfa->AllocateZeroed();
|
||||
if (!p) return false;
|
||||
ring.Stage[i] = (uint8_t*)p;
|
||||
ring.StagePhys[i] = Memory::SubHHDM(p);
|
||||
ring.StageSlots = i + 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -484,6 +498,14 @@ namespace Drivers::Net::Wifi {
|
||||
IwxDmaFree(ring.BcTbl);
|
||||
IwxDmaFree(ring.Cmd);
|
||||
IwxDmaFree(ring.Bounce);
|
||||
for (uint32_t i = 0; i < ring.StageSlots; i++) {
|
||||
if (ring.Stage[i]) {
|
||||
Memory::g_pfa->Free(ring.Stage[i]);
|
||||
ring.Stage[i] = nullptr;
|
||||
}
|
||||
}
|
||||
ring.StageSlots = 0;
|
||||
ring.Active = false;
|
||||
}
|
||||
|
||||
static void IwxResetTxRing(IwxTxRing& ring) {
|
||||
@@ -846,6 +868,9 @@ namespace Drivers::Net::Wifi {
|
||||
return -1;
|
||||
}
|
||||
|
||||
void IwxDumpFwError();
|
||||
static uint32_t g_cmdTimeouts = 0; // consecutive unanswered commands
|
||||
|
||||
bool IwxSendCmd(IwxHostCmd& hcmd) {
|
||||
if (g_iwx.State == IwxFwState::Error) return false;
|
||||
|
||||
@@ -909,6 +934,12 @@ namespace Drivers::Net::Wifi {
|
||||
|
||||
g_iwx.CmdDone = false;
|
||||
g_iwx.LastCmdId = code;
|
||||
// Keep the payload so a firmware assert can show exactly what it
|
||||
// choked on -- struct mismatches are invisible without the bytes.
|
||||
g_iwx.LastCmdLen = hcmd.Len;
|
||||
uint32_t keep = hcmd.Len < sizeof(g_iwx.LastCmdPayload)
|
||||
? hcmd.Len : (uint32_t)sizeof(g_iwx.LastCmdPayload);
|
||||
if (hcmd.Data && keep) memcpy(g_iwx.LastCmdPayload, hcmd.Data, keep);
|
||||
g_iwx.CmdWantResp = hcmd.WantResp;
|
||||
g_iwx.CmdRespLen = 0;
|
||||
g_iwx.CmdIdx = idx;
|
||||
@@ -921,18 +952,47 @@ namespace Drivers::Net::Wifi {
|
||||
// Wait for the firmware's response/ack. Commands are serialized by
|
||||
// CmdLock, so exactly one can be in flight and the completion is
|
||||
// unambiguous.
|
||||
// Two independent bounds. The wall clock is the intended one, but it
|
||||
// is driven by the timer interrupt, so anything that leaves this loop
|
||||
// running with interrupts disabled would spin forever and take the
|
||||
// whole machine down with it -- the spin cap makes that impossible.
|
||||
// Bail immediately if the firmware has asserted, because it will never
|
||||
// answer this or any later command.
|
||||
constexpr uint32_t MAX_SPINS = 20000; // ~2 s at 100 us
|
||||
bool ok = false;
|
||||
bool died = false;
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (Timekeeping::GetMilliseconds() - start < 1000) {
|
||||
for (uint32_t spins = 0; spins < MAX_SPINS; spins++) {
|
||||
IwxProcessEvents();
|
||||
if (g_iwx.CmdDone) { ok = true; break; }
|
||||
if (g_iwx.State == IwxFwState::Error) { died = true; break; }
|
||||
if (Timekeeping::GetMilliseconds() - start >= 1000) break;
|
||||
IwxDelayUs(100);
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
KernelLogStream(WARNING, "WiFi") << "Command 0x" << base::hex
|
||||
<< (uint64_t)code << base::dec << " timed out";
|
||||
<< (uint64_t)code << base::dec
|
||||
<< (died ? " abandoned: firmware has stopped responding"
|
||||
: " timed out");
|
||||
if (ring.Queued > 0) ring.Queued--;
|
||||
if (!died) {
|
||||
// Dump on the first silence: the firmware's error table names
|
||||
// the command that asserted, and it is overwritten as later
|
||||
// commands go unanswered.
|
||||
if (++g_cmdTimeouts == 1) IwxDumpFwError();
|
||||
// Repeated silence means it is wedged and every later command
|
||||
// would burn the same timeout, so stop trying. A single late
|
||||
// response is not worth disabling the adapter over.
|
||||
if (g_cmdTimeouts >= 3) {
|
||||
KernelLogStream(ERROR, "WiFi")
|
||||
<< "Firmware stopped responding to host commands";
|
||||
g_iwx.FwErrors++;
|
||||
g_iwx.State = IwxFwState::Error;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
g_cmdTimeouts = 0;
|
||||
}
|
||||
|
||||
g_iwx.CmdWantResp = false;
|
||||
@@ -969,10 +1029,10 @@ namespace Drivers::Net::Wifi {
|
||||
// TX queue configuration (used by the connect path)
|
||||
// =========================================================================
|
||||
|
||||
bool IwxEnableTxq(int staId, int qid, int tid) {
|
||||
IwxTxRing& ring = g_iwx.MgmtQ;
|
||||
bool IwxEnableTxq(IwxTxRing& ring, int staId, int qid, int tid) {
|
||||
IwxResetTxRing(ring);
|
||||
ring.Qid = qid;
|
||||
ring.Active = false;
|
||||
|
||||
int cmdVer = IwxLookupCmdVer(IWX_DATA_PATH_GROUP, IWX_SCD_QUEUE_CONFIG_CMD);
|
||||
|
||||
@@ -1016,14 +1076,280 @@ namespace Drivers::Net::Wifi {
|
||||
return false;
|
||||
auto* pkt = (IwxRxPacket*)g_iwx.CmdRespBuf;
|
||||
auto* resp = (IwxTxQueueCfgRsp*)pkt->data;
|
||||
|
||||
// On the v3 data-path API the firmware owns queue assignment: `qid` is
|
||||
// only a hint and the response names the queue we actually got.
|
||||
if (resp->queue_number != qid) {
|
||||
KernelLogStream(WARNING, "WiFi") << "Firmware assigned queue "
|
||||
<< (uint64_t)resp->queue_number << ", expected " << (uint64_t)qid;
|
||||
KernelLogStream(INFO, "WiFi") << "Firmware assigned TX queue "
|
||||
<< (uint64_t)resp->queue_number << " (asked for "
|
||||
<< (uint64_t)qid << ")";
|
||||
}
|
||||
ring.Qid = resp->queue_number;
|
||||
ring.Active = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void IwxDisableTxq(IwxTxRing& ring, int staId, int tid) {
|
||||
if (!ring.Active) return;
|
||||
ring.Active = false;
|
||||
|
||||
int cmdVer = IwxLookupCmdVer(IWX_DATA_PATH_GROUP, IWX_SCD_QUEUE_CONFIG_CMD);
|
||||
if (cmdVer == 3) {
|
||||
IwxScdQueueCfgCmd cmd = {};
|
||||
cmd.operation = IWX_SCD_QUEUE_REMOVE;
|
||||
cmd.u.remove.sta_mask = 1u << staId;
|
||||
cmd.u.remove.tid = (uint32_t)tid;
|
||||
IwxSendCmdPdu(IWX_WIDE_ID(IWX_DATA_PATH_GROUP, IWX_SCD_QUEUE_CONFIG_CMD),
|
||||
&cmd, sizeof(cmd));
|
||||
} else {
|
||||
IwxTxQueueCfgCmd cmd = {};
|
||||
cmd.sta_id = (uint8_t)staId;
|
||||
cmd.tid = (uint8_t)tid;
|
||||
cmd.flags = 0; // clear ENABLE_QUEUE
|
||||
cmd.cb_size = IWX_TFD_QUEUE_CB_SIZE(IWX_TX_RING_COUNT);
|
||||
cmd.byte_cnt_addr = ring.BcTbl.Phys;
|
||||
cmd.tfdq_addr = ring.Desc.Phys;
|
||||
IwxSendCmdPdu(IWX_SCD_QUEUE_CFG, &cmd, sizeof(cmd));
|
||||
}
|
||||
IwxResetTxRing(ring);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Frame transmission
|
||||
// =========================================================================
|
||||
|
||||
// Lowest usable transmit antenna, as a rate_n_flags antenna field.
|
||||
static uint32_t IwxTxAntBits() {
|
||||
uint8_t ant = (uint8_t)((g_iwx.Fw.PhyConfig & IWX_FW_PHY_CFG_TX_CHAIN)
|
||||
>> IWX_FW_PHY_CFG_TX_CHAIN_POS);
|
||||
if (g_iwx.Nvm.ValidTxAnt) ant &= g_iwx.Nvm.ValidTxAnt;
|
||||
if (!ant) ant = 1;
|
||||
uint8_t lowest = (uint8_t)(ant & (uint8_t)(~ant + 1)); // isolate low bit
|
||||
return (uint32_t)lowest << IWX_RATE_MCS_ANT_POS;
|
||||
}
|
||||
|
||||
// The lowest basic rate for the current band, in whichever rate_n_flags
|
||||
// encoding the firmware advertises: 1 Mbps CCK on 2.4 GHz, 6 Mbps OFDM on
|
||||
// 5 GHz. Management frames go out at this rate because rate control has
|
||||
// no table for the station until it is associated.
|
||||
static uint32_t IwxLowestRate() {
|
||||
uint32_t ant = IwxTxAntBits();
|
||||
|
||||
// Firmware exposing TX_CMD notification version 7 or later (equally,
|
||||
// command version 9+) uses the "version 2" rate layout.
|
||||
int notifVer = IwxLookupNotifVer(IWX_LONG_GROUP, IWX_TX_CMD);
|
||||
int cmdVer = IwxLookupCmdVer(IWX_LONG_GROUP, IWX_TX_CMD);
|
||||
bool v2 = notifVer > 6 || cmdVer >= 9;
|
||||
|
||||
if (v2) {
|
||||
uint32_t mod = g_iwx.Is5GHz ? IWX_RATE_MCS_MOD_LEGACY_OFDM
|
||||
: IWX_RATE_MCS_MOD_CCK;
|
||||
return ant | mod | IWX_RATE_MCS_CHAN_WIDTH_20 | 0u; // index 0
|
||||
}
|
||||
|
||||
uint32_t plcp = g_iwx.Is5GHz ? IWX_RATE_6M_PLCP : IWX_RATE_1M_PLCP;
|
||||
uint32_t cck = g_iwx.Is5GHz ? 0 : IWX_RATE_MCS_CCK_MSK_V1;
|
||||
return ant | cck | plcp;
|
||||
}
|
||||
|
||||
// Recover a queue whose completions stopped arriving. Without this a
|
||||
// single lost TX response would permanently consume a slot and, after
|
||||
// StageSlots of them, wedge the queue.
|
||||
static uint64_t g_txStallMs = 0;
|
||||
|
||||
static bool IwxTxQueueHasRoom(IwxTxRing& ring) {
|
||||
if (ring.Queued < ring.StageSlots) {
|
||||
g_txStallMs = 0;
|
||||
return true;
|
||||
}
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
if (g_txStallMs == 0) {
|
||||
g_txStallMs = now;
|
||||
return false;
|
||||
}
|
||||
if (now - g_txStallMs < 2000) return false;
|
||||
|
||||
KernelLogStream(WARNING, "WiFi")
|
||||
<< "TX queue " << (uint64_t)ring.Qid
|
||||
<< " stopped completing; resetting its outstanding count";
|
||||
ring.Queued = 0;
|
||||
g_txStallMs = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IwxTxFrame(IwxTxRing& ring, const uint8_t* hdr, uint32_t hdrLen,
|
||||
const uint8_t* payload, uint32_t payloadLen,
|
||||
bool encrypt, bool fixedRate) {
|
||||
if (g_iwx.State != IwxFwState::Running) return false;
|
||||
if (!ring.Active || ring.StageSlots == 0) return false;
|
||||
if (!hdr || hdrLen < IEEE80211_HDR_LEN || hdrLen > 32) return false;
|
||||
|
||||
// The 802.11 header is padded to a dword boundary before the payload;
|
||||
// TX_CMD_OFFLD_PAD tells the firmware to skip those bytes.
|
||||
uint32_t padded = (hdrLen + 3) & ~3u;
|
||||
uint32_t head = (uint32_t)(sizeof(IwxCmdHeader) + sizeof(IwxTxCmdGen3)) + padded;
|
||||
if (head + payloadLen > 4096) return false;
|
||||
|
||||
g_iwx.TxLock.Acquire();
|
||||
|
||||
if (!IwxTxQueueHasRoom(ring)) {
|
||||
g_iwx.TxLock.Release();
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t idx = ring.Cur;
|
||||
uint32_t slot = idx % ring.StageSlots;
|
||||
uint8_t* buf = ring.Stage[slot];
|
||||
uint64_t phys = ring.StagePhys[slot];
|
||||
|
||||
memset(buf, 0, head);
|
||||
|
||||
auto* ch = (IwxCmdHeader*)buf;
|
||||
ch->code = IWX_TX_CMD;
|
||||
ch->flags = 0; // TX_CMD stays in the legacy group
|
||||
ch->idx = (uint8_t)idx;
|
||||
ch->qid = (uint8_t)ring.Qid;
|
||||
|
||||
auto* tx = (IwxTxCmdGen3*)(buf + sizeof(IwxCmdHeader));
|
||||
tx->len = (uint16_t)(hdrLen + payloadLen);
|
||||
|
||||
uint16_t flags = 0;
|
||||
if (!encrypt) flags |= IWX_TX_FLAGS_ENCRYPT_DIS;
|
||||
if (fixedRate) {
|
||||
flags |= IWX_TX_FLAGS_CMD_RATE;
|
||||
tx->rate_n_flags = IwxLowestRate();
|
||||
}
|
||||
tx->flags = flags;
|
||||
|
||||
uint32_t offload = ((hdrLen / 2) & IWX_TX_CMD_OFFLD_MH_MASK)
|
||||
<< IWX_TX_CMD_OFFLD_MH_SIZE_POS;
|
||||
if (hdrLen % 4) offload |= IWX_TX_CMD_OFFLD_PAD;
|
||||
tx->offload_assist = offload;
|
||||
|
||||
uint8_t* body = buf + sizeof(IwxCmdHeader) + sizeof(IwxTxCmdGen3);
|
||||
memcpy(body, hdr, hdrLen);
|
||||
if (payloadLen) memcpy(body + padded, payload, payloadLen);
|
||||
|
||||
auto* desc = &((IwxTfhTfd*)ring.Desc.Virt)[idx];
|
||||
memset(desc, 0, sizeof(*desc));
|
||||
desc->tbs[0].tb_len = (uint16_t)IWX_FIRST_TB_SIZE;
|
||||
desc->tbs[0].addr = phys;
|
||||
desc->tbs[1].tb_len = (uint16_t)(head - IWX_FIRST_TB_SIZE);
|
||||
desc->tbs[1].addr = phys + IWX_FIRST_TB_SIZE;
|
||||
uint16_t numTbs = 2;
|
||||
if (payloadLen) {
|
||||
desc->tbs[2].tb_len = (uint16_t)payloadLen;
|
||||
desc->tbs[2].addr = phys + head;
|
||||
numTbs = 3;
|
||||
}
|
||||
desc->num_tbs = numTbs;
|
||||
|
||||
// Byte-count table: AX210 wants the frame length in bytes plus the
|
||||
// number of extra 64-byte chunks the firmware must fetch for the TFD.
|
||||
uint32_t filled = (uint32_t)(sizeof(uint16_t) + numTbs * sizeof(IwxTfhTb));
|
||||
uint32_t chunks = ((filled + 63) / 64) - 1;
|
||||
auto* bc = (IwxGen3BcTblEntry*)ring.BcTbl.Virt;
|
||||
bc[idx].tfd_offset = (uint16_t)((hdrLen + payloadLen) | (chunks << 14));
|
||||
|
||||
asm volatile("" ::: "memory");
|
||||
|
||||
ring.Queued++;
|
||||
ring.Cur = (ring.Cur + 1) % IWX_TX_RING_COUNT;
|
||||
ring.CurHw = (ring.CurHw + 1) % IWX_TFD_QUEUE_SIZE_MAX_GEN3;
|
||||
IwxWrite32(IWX_HBUS_TARG_WRPTR, ((uint32_t)ring.Qid << 16) | ring.CurHw);
|
||||
|
||||
g_iwx.TxLock.Release();
|
||||
return true;
|
||||
}
|
||||
|
||||
void IwxTxComplete(int qid, int idx, uint32_t status) {
|
||||
(void)idx;
|
||||
IwxTxRing* ring = nullptr;
|
||||
if (g_iwx.MgmtQ.Active && qid == g_iwx.MgmtQ.Qid) ring = &g_iwx.MgmtQ;
|
||||
if (!ring) return;
|
||||
|
||||
g_iwx.TxLock.Acquire();
|
||||
if (ring->Queued > 0) ring->Queued--;
|
||||
g_iwx.TxLock.Release();
|
||||
|
||||
if (status == IWX_TX_STATUS_SUCCESS || status == IWX_TX_STATUS_DIRECT_DONE)
|
||||
g_iwx.TxPackets++;
|
||||
else
|
||||
g_iwx.TxFailures++;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Hardware key installation
|
||||
// =========================================================================
|
||||
|
||||
// Keys go in and out through SEC_KEY_CMD: the MLD firmware does not
|
||||
// implement the legacy ADD_STA_KEY, like the rest of the legacy station
|
||||
// API. Values mirror the Linux trace: PTK as {sta_mask 1, key_id 0,
|
||||
// flags CIPHER}, GTK as {sta_mask 1, key_id N, flags CIPHER|MCAST}.
|
||||
|
||||
static uint32_t SecKeyFlags(uint8_t cipher, uint32_t keyLen, bool pairwise) {
|
||||
uint32_t flags;
|
||||
switch (cipher) {
|
||||
case RSN_CIPHER_CCMP:
|
||||
case RSN_CIPHER_CCMP_256:
|
||||
flags = IWX_SEC_KEY_FLAG_CIPHER_CCMP;
|
||||
break;
|
||||
case RSN_CIPHER_GCMP:
|
||||
case RSN_CIPHER_GCMP_256:
|
||||
flags = IWX_SEC_KEY_FLAG_CIPHER_GCMP;
|
||||
break;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
if (keyLen == 32) flags |= IWX_SEC_KEY_FLAG_KEY_SIZE;
|
||||
if (!pairwise) flags |= IWX_SEC_KEY_FLAG_MCAST_KEY;
|
||||
return flags;
|
||||
}
|
||||
|
||||
bool IwxSetKey(const uint8_t* key, uint32_t keyLen, uint8_t keyIdx,
|
||||
bool pairwise, uint8_t cipher, const uint8_t* rsc) {
|
||||
if (!key || (keyLen != 16 && keyLen != 32)) return false;
|
||||
|
||||
uint32_t flags = SecKeyFlags(cipher, keyLen, pairwise);
|
||||
if (!flags) {
|
||||
KernelLogStream(WARNING, "WiFi")
|
||||
<< "Cannot install a key for cipher " << (uint64_t)cipher;
|
||||
return false;
|
||||
}
|
||||
|
||||
IwxSecKeyCmd cmd = {};
|
||||
cmd.action = IWX_FW_CTXT_ACTION_ADD;
|
||||
cmd.sta_mask = 1u << IWX_STATION_ID;
|
||||
cmd.key_id = keyIdx;
|
||||
cmd.key_flags = flags;
|
||||
memcpy(cmd.key, key, keyLen);
|
||||
|
||||
// The EAPOL RSC carries the AP's packet number for the group key,
|
||||
// lowest byte first; it becomes the initial receive counter.
|
||||
if (rsc) {
|
||||
uint64_t pn = 0;
|
||||
for (int i = 5; i >= 0; i--) pn = (pn << 8) | rsc[i];
|
||||
cmd.rx_seq = pn;
|
||||
}
|
||||
|
||||
return IwxSendCmdPdu(IWX_WIDE_ID(IWX_DATA_PATH_GROUP, IWX_SEC_KEY_CMD),
|
||||
&cmd, sizeof(cmd));
|
||||
}
|
||||
|
||||
bool IwxRemoveKey(uint8_t keyIdx, bool pairwise, uint8_t cipher,
|
||||
uint32_t keyLen) {
|
||||
uint32_t flags = SecKeyFlags(cipher, keyLen, pairwise);
|
||||
if (!flags) return false;
|
||||
|
||||
IwxSecKeyCmd cmd = {};
|
||||
cmd.action = IWX_FW_CTXT_ACTION_REMOVE;
|
||||
cmd.sta_mask = 1u << IWX_STATION_ID;
|
||||
cmd.key_id = keyIdx;
|
||||
cmd.key_flags = flags;
|
||||
return IwxSendCmdPdu(IWX_WIDE_ID(IWX_DATA_PATH_GROUP, IWX_SEC_KEY_CMD),
|
||||
&cmd, sizeof(cmd));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// RX / notification processing
|
||||
// =========================================================================
|
||||
@@ -1083,7 +1409,24 @@ namespace Drivers::Net::Wifi {
|
||||
// between them they pin down which host command the firmware rejected.
|
||||
void IwxDumpFwError() {
|
||||
KernelLogStream(ERROR, "WiFi-FW") << "Firmware assert; last command sent: 0x"
|
||||
<< base::hex << (uint64_t)g_iwx.LastCmdId << base::dec;
|
||||
<< base::hex << (uint64_t)g_iwx.LastCmdId << base::dec
|
||||
<< " (" << (uint64_t)g_iwx.LastCmdLen << " byte payload)";
|
||||
|
||||
// Dump the payload: a struct that does not match the firmware's
|
||||
// expected layout is otherwise impossible to spot from the log.
|
||||
{
|
||||
uint32_t n = g_iwx.LastCmdLen;
|
||||
if (n > sizeof(g_iwx.LastCmdPayload)) n = sizeof(g_iwx.LastCmdPayload);
|
||||
for (uint32_t off = 0; off < n; off += 32) {
|
||||
auto line = KernelLogStream(INFO, "WiFi-FW");
|
||||
line << " +" << (uint64_t)off << ": " << base::hex;
|
||||
for (uint32_t i = off; i < n && i < off + 32; i++) {
|
||||
if (g_iwx.LastCmdPayload[i] < 0x10) line << "0";
|
||||
line << (uint64_t)g_iwx.LastCmdPayload[i];
|
||||
}
|
||||
line << base::dec;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t base_ = g_iwx.UmacErrorTable;
|
||||
if (base_ < 0x400000) {
|
||||
@@ -1141,6 +1484,16 @@ namespace Drivers::Net::Wifi {
|
||||
case IWX_WIDE_ID(IWX_REGULATORY_AND_NVM_GROUP, IWX_PNVM_INIT_COMPLETE):
|
||||
g_iwx.InitComplete |= 0x2;
|
||||
break;
|
||||
case IWX_TX_CMD: {
|
||||
// TX completion for a frame we queued on a data/mgmt queue.
|
||||
uint32_t status = 0;
|
||||
if (IwxRxPacketPayloadLen(pkt) >= sizeof(IwxTxResp)) {
|
||||
auto* r = (const IwxTxResp*)pkt->data;
|
||||
status = r->status & IWX_TX_STATUS_MSK;
|
||||
}
|
||||
IwxTxComplete(qid & ~0x80, pkt->hdr.idx, status);
|
||||
break;
|
||||
}
|
||||
case IWX_REPLY_ERROR: {
|
||||
if (IwxRxPacketPayloadLen(pkt) >= 8) {
|
||||
uint32_t errType = *(const uint32_t*)pkt->data;
|
||||
@@ -1208,8 +1561,9 @@ namespace Drivers::Net::Wifi {
|
||||
|
||||
void IwxProcessEvents() {
|
||||
if (!g_iwx.Mmio) return;
|
||||
if (g_iwx.InProcessEvents) return; // never nest
|
||||
g_iwx.InProcessEvents = true;
|
||||
// Never nest. With interrupts enabled during command waits this is a
|
||||
// genuine multi-core race, so it has to be an atomic test-and-set.
|
||||
if (g_iwx.InProcessEvents.test_and_set(std::memory_order_acquire)) return;
|
||||
|
||||
if (g_msix) {
|
||||
uint32_t fh = IwxRead32(IWX_CSR_MSIX_FH_INT_CAUSES_AD);
|
||||
@@ -1276,7 +1630,7 @@ namespace Drivers::Net::Wifi {
|
||||
IwxNotifIntr();
|
||||
|
||||
g_iwx.WorkPending = false;
|
||||
g_iwx.InProcessEvents = false;
|
||||
g_iwx.InProcessEvents.clear(std::memory_order_release);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -1435,7 +1789,8 @@ namespace Drivers::Net::Wifi {
|
||||
|| !IwxDmaAlloc(g_iwx.PrphInfo, 4096)
|
||||
|| !IwxAllocRxRing()
|
||||
|| !IwxAllocTxRing(g_iwx.CmdQ, IWX_DQA_CMD_QUEUE)
|
||||
|| !IwxAllocTxRing(g_iwx.MgmtQ, IWX_DQA_MGMT_QUEUE)) {
|
||||
|| !IwxAllocTxRing(g_iwx.MgmtQ, IWX_DQA_MGMT_QUEUE,
|
||||
IWX_TX_STAGE_SLOTS)) {
|
||||
KernelLogStream(ERROR, "WiFi") << "Could not allocate device DMA memory";
|
||||
IwxFreeRxRing();
|
||||
IwxFreeTxRing(g_iwx.CmdQ);
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "Wifi.hpp"
|
||||
#include "Iwx.hpp"
|
||||
#include "Wpa.hpp"
|
||||
#include <Fs/Vfs.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
@@ -20,6 +21,7 @@
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <Timekeeping/ApicTimer.hpp>
|
||||
#include <Hal/SmpBoot.hpp>
|
||||
#include <Sched/Scheduler.hpp>
|
||||
#include <atomic>
|
||||
|
||||
using namespace Kt;
|
||||
@@ -36,6 +38,10 @@ namespace Drivers::Net::Wifi {
|
||||
|
||||
static constexpr int MAX_SCAN_RESULTS = 64;
|
||||
|
||||
// Long enough for a personal-mode RSN element: version, group cipher, one
|
||||
// or two pairwise ciphers, a handful of AKMs and the capability field.
|
||||
static constexpr int MAX_RSN_IE = 64;
|
||||
|
||||
struct ScanEntry {
|
||||
uint8_t Bssid[6];
|
||||
char Ssid[33];
|
||||
@@ -45,6 +51,9 @@ namespace Drivers::Net::Wifi {
|
||||
uint8_t Band; // 0 = 2.4 GHz, 1 = 5 GHz
|
||||
uint8_t Security; // WifiSecurity value
|
||||
uint16_t BeaconInterval;
|
||||
uint8_t DtimPeriod;
|
||||
uint8_t RsnIe[MAX_RSN_IE]; // element body, without id/len
|
||||
uint8_t RsnIeLen;
|
||||
bool Used;
|
||||
};
|
||||
|
||||
@@ -71,6 +80,7 @@ namespace Drivers::Net::Wifi {
|
||||
// Element IDs used here.
|
||||
static constexpr uint8_t ELEMID_SSID = 0;
|
||||
static constexpr uint8_t ELEMID_DSPARMS = 3;
|
||||
static constexpr uint8_t ELEMID_TIM = 5;
|
||||
static constexpr uint8_t ELEMID_RSN = 48;
|
||||
static constexpr uint8_t ELEMID_VENDOR = 221;
|
||||
|
||||
@@ -108,6 +118,9 @@ namespace Drivers::Net::Wifi {
|
||||
uint8_t Channel = 0;
|
||||
uint8_t Security = WIFI_SEC_OPEN;
|
||||
uint16_t BeaconInterval = 0;
|
||||
uint8_t DtimPeriod = 0;
|
||||
const uint8_t* Rsn = nullptr; // RSN element body
|
||||
uint8_t RsnLen = 0;
|
||||
};
|
||||
|
||||
static bool ParseBeacon(const uint8_t* frame, uint32_t len, ParsedBeacon* out) {
|
||||
@@ -139,9 +152,18 @@ namespace Drivers::Net::Wifi {
|
||||
case ELEMID_DSPARMS:
|
||||
if (ielen >= 1) out->Channel = body[0];
|
||||
break;
|
||||
case ELEMID_TIM:
|
||||
// DTIM count, then DTIM period. The firmware needs the
|
||||
// period to schedule wake-ups once associated.
|
||||
if (ielen >= 2) out->DtimPeriod = body[1];
|
||||
break;
|
||||
case ELEMID_RSN:
|
||||
haveRsn = true;
|
||||
out->Security = ClassifyRsn(body, ielen);
|
||||
// Kept verbatim: the connect path negotiates ciphers and
|
||||
// AKMs straight out of it.
|
||||
out->Rsn = body;
|
||||
out->RsnLen = ielen;
|
||||
break;
|
||||
case ELEMID_VENDOR:
|
||||
// WPA1: Microsoft OUI 00:50:F2, type 1.
|
||||
@@ -220,6 +242,12 @@ namespace Drivers::Net::Wifi {
|
||||
slot->Band = ch > 14 ? 1 : 0;
|
||||
slot->Security = pb.Security;
|
||||
slot->BeaconInterval = pb.BeaconInterval;
|
||||
if (pb.DtimPeriod) slot->DtimPeriod = pb.DtimPeriod;
|
||||
slot->RsnIeLen = 0;
|
||||
if (pb.Rsn && pb.RsnLen && pb.RsnLen <= MAX_RSN_IE) {
|
||||
memcpy(slot->RsnIe, pb.Rsn, pb.RsnLen);
|
||||
slot->RsnIeLen = pb.RsnLen;
|
||||
}
|
||||
|
||||
g_resultLock.Release();
|
||||
}
|
||||
@@ -371,8 +399,18 @@ namespace Drivers::Net::Wifi {
|
||||
| (g_iwx.Nvm.Sku52GHz ? 2 : 0));
|
||||
out->channels = (uint16_t)g_iwx.ChannelCount;
|
||||
out->rxPackets = g_iwx.RxPackets;
|
||||
out->txPackets = g_iwx.TxPackets;
|
||||
out->fwErrors = (uint32_t)g_iwx.FwErrors;
|
||||
out->connState = (uint32_t)IwxConnectState();
|
||||
out->connected = IwxLinkUp() ? 1 : 0;
|
||||
|
||||
if (IwxConnectState() != (int)IwxConnStateId::Idle) {
|
||||
const char* ssid = IwxConnectSsid();
|
||||
int k = 0;
|
||||
for (; k < 32 && ssid[k]; k++) out->ssid[k] = ssid[k];
|
||||
out->ssid[k] = '\0';
|
||||
memcpy(out->bssid, IwxConnectBssid(), 6);
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
for (; i < 31 && g_iwx.Fw.Version[i]; i++) out->fwVersion[i] = g_iwx.Fw.Version[i];
|
||||
@@ -380,55 +418,136 @@ namespace Drivers::Net::Wifi {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Wait for the association + handshake to settle. The whole exchange is
|
||||
// driven from IwxConnectService(), so this pumps both while it waits
|
||||
// instead of relying on the idle loop getting scheduled.
|
||||
static int WaitForConnection(uint32_t timeoutMs) {
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
|
||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||
IwxProcessEvents();
|
||||
IwxConnectService();
|
||||
|
||||
auto state = (IwxConnStateId)IwxConnectState();
|
||||
if (state == IwxConnStateId::Connected) return 0;
|
||||
if (state == IwxConnStateId::Failed) {
|
||||
// A handshake that got as far as exchanging EAPOL frames and
|
||||
// then failed is almost always a wrong passphrase.
|
||||
int rc = WpaGetState() == WpaState::Failed
|
||||
? WIFI_ERR_AUTH : WIFI_ERR_FAILED;
|
||||
// A rejection seen on the RX path only marks the state; the
|
||||
// firmware contexts are still up and have to come back down.
|
||||
IwxConnectAbort();
|
||||
return rc;
|
||||
}
|
||||
if (state == IwxConnStateId::Idle) return WIFI_ERR_FAILED;
|
||||
if (g_iwx.State == IwxFwState::Error) {
|
||||
IwxConnectAbort();
|
||||
return WIFI_ERR_FAILED;
|
||||
}
|
||||
|
||||
// Yield rather than burn the core: this can wait seconds, and the
|
||||
// idle loop on other cores drives the same state machine.
|
||||
Sched::Schedule();
|
||||
}
|
||||
|
||||
IwxConnectAbort();
|
||||
return WIFI_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
int Connect(const char* ssid, const char* password) {
|
||||
if (!g_initialized || !ssid) return -1;
|
||||
if (!g_initialized || !ssid) return WIFI_ERR_NO_ADAPTER;
|
||||
if (g_iwx.State != IwxFwState::Running) return WIFI_ERR_NO_ADAPTER;
|
||||
|
||||
// Locate the network in the most recent scan results: the firmware
|
||||
// contexts need its BSSID and channel.
|
||||
// contexts need its BSSID and channel, and the connect path needs its
|
||||
// RSN element to negotiate ciphers.
|
||||
uint8_t bssid[6];
|
||||
uint8_t channel = 0;
|
||||
bool is5 = false;
|
||||
uint8_t security = WIFI_SEC_OPEN;
|
||||
uint8_t rsnIe[MAX_RSN_IE];
|
||||
uint8_t rsnIeLen = 0;
|
||||
uint16_t beaconInterval = 0;
|
||||
uint8_t dtimPeriod = 0;
|
||||
bool found = false;
|
||||
|
||||
g_resultLock.Acquire();
|
||||
for (int i = 0; i < MAX_SCAN_RESULTS; i++) {
|
||||
if (!g_results[i].Used) continue;
|
||||
const ScanEntry& e = g_results[i];
|
||||
bool match = true;
|
||||
for (int k = 0; k < 32; k++) {
|
||||
char a = e.Ssid[k], b = ssid[k];
|
||||
if (a != b) { match = false; break; }
|
||||
if (a == '\0') break;
|
||||
// Strongest first, so a network seen on several bands or repeaters is
|
||||
// joined through the best AP rather than whichever answered first.
|
||||
int8_t bestRssi = -128;
|
||||
|
||||
for (int attempt = 0; attempt < 2 && !found; attempt++) {
|
||||
if (attempt == 1) {
|
||||
// Nothing matched: the caller may never have scanned, or the
|
||||
// results may predate this network appearing.
|
||||
KernelLogStream(INFO, "WiFi")
|
||||
<< "\"" << ssid << "\" is not in the scan results; scanning again";
|
||||
WifiNetwork tmp[1];
|
||||
Scan(tmp, 1, 4000);
|
||||
}
|
||||
if (!match) continue;
|
||||
memcpy(bssid, e.Bssid, 6);
|
||||
channel = e.Channel;
|
||||
is5 = e.Band == 1;
|
||||
security = e.Security;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
g_resultLock.Release();
|
||||
|
||||
if (!found) {
|
||||
g_resultLock.Acquire();
|
||||
for (int i = 0; i < MAX_SCAN_RESULTS; i++) {
|
||||
if (!g_results[i].Used) continue;
|
||||
const ScanEntry& e = g_results[i];
|
||||
bool match = true;
|
||||
for (int k = 0; k < 32; k++) {
|
||||
char a = e.Ssid[k], b = ssid[k];
|
||||
if (a != b) { match = false; break; }
|
||||
if (a == '\0') break;
|
||||
}
|
||||
if (!match) continue;
|
||||
if (found && e.Rssi <= bestRssi) continue;
|
||||
memcpy(bssid, e.Bssid, 6);
|
||||
channel = e.Channel;
|
||||
is5 = e.Band == 1;
|
||||
security = e.Security;
|
||||
rsnIeLen = e.RsnIeLen;
|
||||
if (rsnIeLen) memcpy(rsnIe, e.RsnIe, rsnIeLen);
|
||||
beaconInterval = e.BeaconInterval;
|
||||
dtimPeriod = e.DtimPeriod;
|
||||
bestRssi = e.Rssi;
|
||||
found = true;
|
||||
}
|
||||
g_resultLock.Release();
|
||||
}
|
||||
|
||||
if (!found) return WIFI_ERR_NOT_FOUND;
|
||||
|
||||
bool havePassword = password && password[0];
|
||||
|
||||
// WEP and WPA1 use RC4/TKIP, which the supplicant deliberately does
|
||||
// not implement; say so rather than failing mid-handshake.
|
||||
if (security == WIFI_SEC_WEP || security == WIFI_SEC_WPA) {
|
||||
KernelLogStream(WARNING, "WiFi")
|
||||
<< "Network not in scan results; run a scan first";
|
||||
return -1;
|
||||
<< "WEP and the original WPA use ciphers this driver does not implement";
|
||||
return WIFI_ERR_UNSUPPORTED;
|
||||
}
|
||||
|
||||
// Encryption is checked before the passphrase: the WPA2/WPA3 key
|
||||
// exchange (PMK derivation, EAPOL 4-way, HW key install) is not
|
||||
// implemented at all, so a passphrase would not help and reporting
|
||||
// "needs a passphrase" would be misleading.
|
||||
if (security != WIFI_SEC_OPEN) {
|
||||
KernelLogStream(WARNING, "WiFi")
|
||||
<< "Encrypted networks are not supported yet (open only)";
|
||||
return -2;
|
||||
if (!rsnIeLen) {
|
||||
KernelLogStream(WARNING, "WiFi")
|
||||
<< "The beacon for this network carried no RSN element";
|
||||
return WIFI_ERR_UNSUPPORTED;
|
||||
}
|
||||
if (!havePassword) return WIFI_ERR_NEED_KEY;
|
||||
}
|
||||
(void)password;
|
||||
|
||||
return IwxConnectStart(bssid, channel, is5, ssid) ? 0 : -1;
|
||||
if (!IwxConnectStart(bssid, channel, is5, ssid,
|
||||
havePassword ? password : nullptr,
|
||||
security == WIFI_SEC_OPEN ? nullptr : rsnIe,
|
||||
security == WIFI_SEC_OPEN ? 0 : rsnIeLen,
|
||||
beaconInterval, dtimPeriod)) {
|
||||
// IwxConnectStart already logged the specific reason. Only report
|
||||
// a security problem when that is genuinely what happened; a radio
|
||||
// or firmware failure needs completely different advice.
|
||||
bool security_ = IwxConnectRefusedForSecurity();
|
||||
auto state = (IwxConnStateId)IwxConnectState();
|
||||
if (state != IwxConnStateId::Idle) IwxConnectAbort();
|
||||
return security_ ? WIFI_ERR_UNSUPPORTED : WIFI_ERR_FAILED;
|
||||
}
|
||||
|
||||
return WaitForConnection(15000);
|
||||
}
|
||||
|
||||
int Disconnect() {
|
||||
@@ -436,4 +555,26 @@ namespace Drivers::Net::Wifi {
|
||||
IwxConnectAbort();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Network interface
|
||||
// =========================================================================
|
||||
|
||||
static RxCallback g_rxCallback = nullptr;
|
||||
|
||||
const uint8_t* GetMacAddress() { return g_iwx.Nvm.HwAddr; }
|
||||
|
||||
bool IsLinkUp() { return g_initialized && IwxLinkUp(); }
|
||||
|
||||
bool SendPacket(const uint8_t* data, uint16_t length) {
|
||||
if (!IsLinkUp()) return false;
|
||||
return IwxConnectSendEthernet(data, length);
|
||||
}
|
||||
|
||||
void SetRxCallback(RxCallback callback) { g_rxCallback = callback; }
|
||||
|
||||
// Called by the connect path for every decapsulated Ethernet frame.
|
||||
void WifiRxEthernet(const uint8_t* frame, uint32_t len) {
|
||||
if (g_rxCallback && len <= 0xffff) g_rxCallback(frame, (uint16_t)len);
|
||||
}
|
||||
}
|
||||
@@ -32,9 +32,24 @@ namespace Drivers::Net::Wifi {
|
||||
// Fill in adapter/firmware status.
|
||||
int GetInfo(montauk::abi::WifiInfo* out);
|
||||
|
||||
// Association groundwork. Returns 0 when the firmware contexts came up,
|
||||
// negative on failure. See IwxConnect.cpp: the 802.11 handshake itself is
|
||||
// not implemented yet, so this cannot establish a usable link.
|
||||
// Join a network. Blocks (pumping firmware events) until the link is up
|
||||
// or the attempt fails. Returns 0 on success, or a negative WIFI_ERR_*
|
||||
// value describing why it could not connect.
|
||||
int Connect(const char* ssid, const char* password);
|
||||
int Disconnect();
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Network interface, registered with Net::NetIf once associated
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
const uint8_t* GetMacAddress();
|
||||
|
||||
// Send an Ethernet frame. Fails while the link is down.
|
||||
bool SendPacket(const uint8_t* data, uint16_t length);
|
||||
|
||||
// True once a network has been joined and, for encrypted networks, keyed.
|
||||
bool IsLinkUp();
|
||||
|
||||
using RxCallback = void(*)(const uint8_t* data, uint16_t length);
|
||||
void SetRxCallback(RxCallback callback);
|
||||
}
|
||||
@@ -0,0 +1,830 @@
|
||||
/*
|
||||
* Wpa.cpp
|
||||
* WPA2/WPA3-PSK supplicant: PMK derivation, the EAPOL-Key 4-way handshake
|
||||
* and the group-key handshake used for periodic GTK rekeying.
|
||||
*
|
||||
* Supported: RSN (WPA2) with CCMP or GCMP, AKM PSK (00-0F-AC:2) and
|
||||
* PSK-SHA256 (:6). Key descriptor versions 2 (HMAC-SHA1-128 MIC, AES key
|
||||
* wrap) and 3 (AES-128-CMAC MIC) are handled.
|
||||
*
|
||||
* Not supported: SAE (WPA3 needs finite-field / elliptic-curve crypto that
|
||||
* is well beyond what belongs in this kernel), TKIP and WEP (descriptor
|
||||
* version 1 needs HMAC-MD5 and RC4, and the ciphers are broken anyway), and
|
||||
* 802.1X/EAP enterprise authentication. Those are rejected up front with a
|
||||
* clear reason rather than failing halfway through the handshake.
|
||||
*
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "Wpa.hpp"
|
||||
#include "Ieee80211.hpp"
|
||||
#include <Libraries/Crypto.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Timekeeping/ApicTimer.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Drivers::Net::Wifi {
|
||||
|
||||
// =========================================================================
|
||||
// EAPOL-Key frame layout (IEEE 802.1X-2004 + IEEE 802.11 key descriptor)
|
||||
// =========================================================================
|
||||
|
||||
constexpr uint8_t EAPOL_TYPE_KEY = 3;
|
||||
constexpr uint8_t EAPOL_KEY_DESC_RSN = 2;
|
||||
constexpr uint8_t EAPOL_KEY_DESC_WPA = 254;
|
||||
|
||||
constexpr uint16_t KEY_INFO_VERSION_MASK = 0x0007;
|
||||
constexpr uint16_t KEY_INFO_KEY_TYPE = 0x0008; // set = pairwise
|
||||
constexpr uint16_t KEY_INFO_INSTALL = 0x0040;
|
||||
constexpr uint16_t KEY_INFO_ACK = 0x0080;
|
||||
constexpr uint16_t KEY_INFO_MIC = 0x0100;
|
||||
constexpr uint16_t KEY_INFO_SECURE = 0x0200;
|
||||
constexpr uint16_t KEY_INFO_ERROR = 0x0400;
|
||||
constexpr uint16_t KEY_INFO_REQUEST = 0x0800;
|
||||
constexpr uint16_t KEY_INFO_ENCRYPTED = 0x1000;
|
||||
|
||||
// Key descriptor versions: 1 is HMAC-MD5 + RC4 (unsupported), 2 is
|
||||
// HMAC-SHA1-128 + AES key wrap, 3 is AES-128-CMAC + AES key wrap.
|
||||
constexpr uint8_t KEY_DESC_VER_RC4 = 1;
|
||||
constexpr uint8_t KEY_DESC_VER_AES = 2;
|
||||
constexpr uint8_t KEY_DESC_VER_AES_CMAC = 3;
|
||||
|
||||
struct EapolKey {
|
||||
uint8_t version;
|
||||
uint8_t type;
|
||||
uint8_t length[2]; // big endian, bytes after this field
|
||||
uint8_t descType;
|
||||
uint8_t keyInfo[2]; // big endian
|
||||
uint8_t keyLength[2]; // big endian
|
||||
uint8_t replay[8];
|
||||
uint8_t nonce[32];
|
||||
uint8_t iv[16];
|
||||
uint8_t rsc[8];
|
||||
uint8_t keyId[8];
|
||||
uint8_t mic[16];
|
||||
uint8_t keyDataLen[2]; // big endian
|
||||
// key data follows
|
||||
} __attribute__((packed));
|
||||
|
||||
static_assert(sizeof(EapolKey) == 99, "EAPOL-Key header must stay 99 bytes");
|
||||
|
||||
constexpr uint32_t EAPOL_MIC_LEN = 16;
|
||||
constexpr uint32_t MAX_KEY_DATA = 512;
|
||||
constexpr uint32_t MAX_EAPOL_TX = sizeof(EapolKey) + 128;
|
||||
|
||||
// =========================================================================
|
||||
// Supplicant state
|
||||
// =========================================================================
|
||||
|
||||
static WpaConfig g_cfg = {};
|
||||
static WpaState g_state = WpaState::Idle;
|
||||
|
||||
static uint8_t g_pmk[32];
|
||||
static uint8_t g_ptk[64]; // KCK | KEK | TK
|
||||
static uint32_t g_kckLen = 16;
|
||||
static uint32_t g_kekLen = 16;
|
||||
static uint32_t g_tkLen = 16;
|
||||
|
||||
static uint8_t g_anonce[32];
|
||||
static uint8_t g_snonce[32];
|
||||
static uint8_t g_replay[8];
|
||||
static bool g_haveReplay = false;
|
||||
static uint8_t g_keyDescVer = KEY_DESC_VER_AES;
|
||||
static uint8_t g_descType = EAPOL_KEY_DESC_RSN;
|
||||
static uint8_t g_eapolVersion = 2;
|
||||
|
||||
static uint8_t g_rsnIe[32];
|
||||
static uint32_t g_rsnIeLen = 0;
|
||||
|
||||
// Retransmission of the last message we sent. The AP retries msg 1 and 3
|
||||
// on its own, but a lost msg 2 or 4 otherwise stalls the exchange until the
|
||||
// AP gives up and deauthenticates.
|
||||
static uint8_t g_lastTx[MAX_EAPOL_TX];
|
||||
static uint32_t g_lastTxLen = 0;
|
||||
static uint64_t g_lastTxMs = 0;
|
||||
static int g_retries = 0;
|
||||
static uint64_t g_startMs = 0;
|
||||
|
||||
static constexpr uint64_t RETRY_INTERVAL_MS = 500;
|
||||
static constexpr int MAX_RETRIES = 4;
|
||||
static constexpr uint64_t HANDSHAKE_TIMEOUT_MS = 5000;
|
||||
|
||||
// =========================================================================
|
||||
// Nonce generation
|
||||
// =========================================================================
|
||||
|
||||
// A SNonce only has to be unique per (PMK, ANonce) pair, but making it
|
||||
// unpredictable costs nothing: fold a run of TSC samples into SHA-256 along
|
||||
// with our MAC and a monotonic counter. RDRAND is deliberately avoided --
|
||||
// see Api/Random.hpp for why it is unreliable on this hardware.
|
||||
static void GenNonce(uint8_t out[32]) {
|
||||
static uint64_t counter = 0;
|
||||
|
||||
struct {
|
||||
uint64_t tsc[8];
|
||||
uint64_t counter;
|
||||
uint8_t mac[6];
|
||||
uint8_t pad[2];
|
||||
} pool;
|
||||
|
||||
for (int i = 0; i < 8; i++) {
|
||||
uint64_t tsc;
|
||||
asm volatile("rdtsc; shl $32, %%rdx; or %%rdx, %%rax"
|
||||
: "=a"(tsc) :: "rdx");
|
||||
pool.tsc[i] = tsc;
|
||||
// Space the samples out so the low bits differ between them.
|
||||
for (int k = 0; k < 64; k++) asm volatile("pause" ::: "memory");
|
||||
}
|
||||
pool.counter = ++counter;
|
||||
memcpy(pool.mac, g_cfg.OwnMac, 6);
|
||||
pool.pad[0] = pool.pad[1] = 0;
|
||||
|
||||
Crypto::Sha256(&pool, sizeof(pool), out);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Key derivation
|
||||
// =========================================================================
|
||||
|
||||
// IEEE 802.11 PRF-N built on HMAC-SHA1 (used by AKM PSK).
|
||||
static void Prf(const uint8_t* key, uint32_t keyLen, const char* label,
|
||||
const uint8_t* data, uint32_t dataLen,
|
||||
uint8_t* out, uint32_t outLen) {
|
||||
uint32_t labelLen = 0;
|
||||
while (label[labelLen]) labelLen++;
|
||||
|
||||
uint8_t counter = 0;
|
||||
uint32_t pos = 0;
|
||||
const uint8_t zero = 0;
|
||||
|
||||
while (pos < outLen) {
|
||||
const uint8_t* parts[4] = {
|
||||
(const uint8_t*)label, &zero, data, &counter
|
||||
};
|
||||
size_t lens[4] = { labelLen, 1, dataLen, 1 };
|
||||
|
||||
uint8_t digest[Crypto::SHA1_DIGEST_SIZE];
|
||||
Crypto::HmacSha1(key, keyLen, parts, lens, 4, digest);
|
||||
|
||||
uint32_t take = outLen - pos;
|
||||
if (take > Crypto::SHA1_DIGEST_SIZE) take = Crypto::SHA1_DIGEST_SIZE;
|
||||
memcpy(out + pos, digest, take);
|
||||
pos += take;
|
||||
counter++;
|
||||
Crypto::SecureZero(digest, sizeof(digest));
|
||||
}
|
||||
}
|
||||
|
||||
// IEEE 802.11 KDF built on HMAC-SHA256 (used by AKM PSK-SHA256 and SAE).
|
||||
static void KdfSha256(const uint8_t* key, uint32_t keyLen, const char* label,
|
||||
const uint8_t* data, uint32_t dataLen,
|
||||
uint8_t* out, uint32_t outLen) {
|
||||
uint32_t labelLen = 0;
|
||||
while (label[labelLen]) labelLen++;
|
||||
|
||||
uint16_t bits = (uint16_t)(outLen * 8);
|
||||
uint8_t lenLe[2] = { (uint8_t)bits, (uint8_t)(bits >> 8) };
|
||||
|
||||
uint16_t iter = 1;
|
||||
uint32_t pos = 0;
|
||||
while (pos < outLen) {
|
||||
uint8_t iterLe[2] = { (uint8_t)iter, (uint8_t)(iter >> 8) };
|
||||
const uint8_t* parts[4] = {
|
||||
iterLe, (const uint8_t*)label, data, lenLe
|
||||
};
|
||||
size_t lens[4] = { 2, labelLen, dataLen, 2 };
|
||||
|
||||
uint8_t digest[Crypto::SHA256_DIGEST_SIZE];
|
||||
Crypto::HmacSha256(key, keyLen, parts, lens, 4, digest);
|
||||
|
||||
uint32_t take = outLen - pos;
|
||||
if (take > Crypto::SHA256_DIGEST_SIZE) take = Crypto::SHA256_DIGEST_SIZE;
|
||||
memcpy(out + pos, digest, take);
|
||||
pos += take;
|
||||
iter++;
|
||||
Crypto::SecureZero(digest, sizeof(digest));
|
||||
}
|
||||
}
|
||||
|
||||
static bool UsesSha256Kdf(uint8_t akm) {
|
||||
return akm == RSN_AKM_PSK_SHA256 || akm == RSN_AKM_SAE
|
||||
|| akm == RSN_AKM_FT_SAE;
|
||||
}
|
||||
|
||||
// PTK = KDF(PMK, "Pairwise key expansion",
|
||||
// min(AA,SPA) || max(AA,SPA) || min(ANonce,SNonce) || max(...))
|
||||
static void DerivePtk() {
|
||||
const uint8_t* aa = g_cfg.Bssid;
|
||||
const uint8_t* spa = g_cfg.OwnMac;
|
||||
|
||||
uint8_t data[76];
|
||||
int cmp = memcmp(aa, spa, 6);
|
||||
const uint8_t* lo = (cmp < 0) ? aa : spa;
|
||||
const uint8_t* hi = (cmp < 0) ? spa : aa;
|
||||
memcpy(data, lo, 6);
|
||||
memcpy(data + 6, hi, 6);
|
||||
|
||||
cmp = memcmp(g_anonce, g_snonce, 32);
|
||||
const uint8_t* nlo = (cmp < 0) ? g_anonce : g_snonce;
|
||||
const uint8_t* nhi = (cmp < 0) ? g_snonce : g_anonce;
|
||||
memcpy(data + 12, nlo, 32);
|
||||
memcpy(data + 44, nhi, 32);
|
||||
|
||||
uint32_t ptkLen = g_kckLen + g_kekLen + g_tkLen;
|
||||
if (UsesSha256Kdf(g_cfg.Akm))
|
||||
KdfSha256(g_pmk, 32, "Pairwise key expansion", data, sizeof(data),
|
||||
g_ptk, ptkLen);
|
||||
else
|
||||
Prf(g_pmk, 32, "Pairwise key expansion", data, sizeof(data),
|
||||
g_ptk, ptkLen);
|
||||
|
||||
Crypto::SecureZero(data, sizeof(data));
|
||||
}
|
||||
|
||||
static const uint8_t* Kck() { return g_ptk; }
|
||||
static const uint8_t* Kek() { return g_ptk + g_kckLen; }
|
||||
static const uint8_t* Tk() { return g_ptk + g_kckLen + g_kekLen; }
|
||||
|
||||
// =========================================================================
|
||||
// MIC
|
||||
// =========================================================================
|
||||
|
||||
// The MIC covers the whole 802.1X frame with the MIC field zeroed.
|
||||
static void ComputeMic(const uint8_t* frame, uint32_t len, uint8_t out[16]) {
|
||||
if (g_keyDescVer == KEY_DESC_VER_AES_CMAC) {
|
||||
const uint8_t* parts[1] = { frame };
|
||||
size_t lens[1] = { len };
|
||||
Crypto::AesCmac(Kck(), g_kckLen, parts, lens, 1, out);
|
||||
} else {
|
||||
uint8_t digest[Crypto::SHA1_DIGEST_SIZE];
|
||||
Crypto::HmacSha1(Kck(), g_kckLen, frame, len, digest);
|
||||
memcpy(out, digest, 16);
|
||||
Crypto::SecureZero(digest, sizeof(digest));
|
||||
}
|
||||
}
|
||||
|
||||
static bool VerifyMic(const uint8_t* frame, uint32_t len) {
|
||||
// Copy so the received frame can keep its MIC for logging.
|
||||
static uint8_t scratch[sizeof(EapolKey) + MAX_KEY_DATA];
|
||||
if (len > sizeof(scratch)) return false;
|
||||
memcpy(scratch, frame, len);
|
||||
|
||||
auto* k = (EapolKey*)scratch;
|
||||
uint8_t received[16];
|
||||
memcpy(received, k->mic, 16);
|
||||
memset(k->mic, 0, 16);
|
||||
|
||||
uint8_t computed[16];
|
||||
ComputeMic(scratch, len, computed);
|
||||
return Crypto::SecureEqual(received, computed, 16);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// RSN information element
|
||||
// =========================================================================
|
||||
|
||||
static void PutSuite(uint8_t* p, uint8_t type) {
|
||||
p[0] = 0x00; p[1] = 0x0f; p[2] = 0xac; p[3] = type;
|
||||
}
|
||||
|
||||
static bool IsRsnSuite(const uint8_t* p) {
|
||||
return p[0] == 0x00 && p[1] == 0x0f && p[2] == 0xac;
|
||||
}
|
||||
|
||||
uint32_t WpaBuildRsnIe(uint8_t* out, uint32_t cap) {
|
||||
constexpr uint32_t BODY = 20;
|
||||
if (cap < BODY + 2) return 0;
|
||||
|
||||
uint8_t* p = out;
|
||||
*p++ = IEEE80211_ELEMID_RSN;
|
||||
*p++ = (uint8_t)BODY;
|
||||
Put16Le(p, 1); p += 2; // RSN version
|
||||
PutSuite(p, g_cfg.GroupCipher); p += 4;
|
||||
Put16Le(p, 1); p += 2; // one pairwise cipher
|
||||
PutSuite(p, g_cfg.PairwiseCipher); p += 4;
|
||||
Put16Le(p, 1); p += 2; // one AKM
|
||||
PutSuite(p, g_cfg.Akm); p += 4;
|
||||
// RSN capabilities: no PMF (BIP/IGTK is not implemented), no preauth,
|
||||
// one replay counter per key.
|
||||
Put16Le(p, 0); p += 2;
|
||||
|
||||
return (uint32_t)(p - out);
|
||||
}
|
||||
|
||||
bool WpaParseApRsn(const uint8_t* ie, uint32_t len, WpaConfig& cfg) {
|
||||
if (len < 8) return false;
|
||||
uint32_t off = 0;
|
||||
|
||||
uint16_t version = Get16Le(ie);
|
||||
off += 2;
|
||||
if (version != 1) return false;
|
||||
|
||||
if (off + 4 > len) return false;
|
||||
uint8_t groupCipher = RSN_CIPHER_CCMP;
|
||||
if (IsRsnSuite(ie + off)) groupCipher = ie[off + 3];
|
||||
off += 4;
|
||||
|
||||
if (off + 2 > len) return false;
|
||||
uint16_t pairwiseCount = Get16Le(ie + off);
|
||||
off += 2;
|
||||
|
||||
// Prefer CCMP; fall back to GCMP. TKIP-only networks are rejected.
|
||||
int bestPairwise = -1;
|
||||
for (uint16_t i = 0; i < pairwiseCount && off + 4 <= len; i++, off += 4) {
|
||||
if (!IsRsnSuite(ie + off)) continue;
|
||||
uint8_t c = ie[off + 3];
|
||||
if (c == RSN_CIPHER_CCMP) bestPairwise = c;
|
||||
else if (c == RSN_CIPHER_GCMP && bestPairwise < 0) bestPairwise = c;
|
||||
else if (c == RSN_CIPHER_GCMP_256 && bestPairwise < 0) bestPairwise = c;
|
||||
else if (c == RSN_CIPHER_CCMP_256 && bestPairwise < 0) bestPairwise = c;
|
||||
}
|
||||
if (bestPairwise < 0) {
|
||||
KernelLogStream(WARNING, "WiFi")
|
||||
<< "AP offers no supported pairwise cipher (CCMP/GCMP required)";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (off + 2 > len) return false;
|
||||
uint16_t akmCount = Get16Le(ie + off);
|
||||
off += 2;
|
||||
|
||||
// Prefer plain PSK; PSK-SHA256 works too but needs the SHA-256 KDF.
|
||||
int bestAkm = -1;
|
||||
bool sawSae = false;
|
||||
for (uint16_t i = 0; i < akmCount && off + 4 <= len; i++, off += 4) {
|
||||
if (!IsRsnSuite(ie + off)) continue;
|
||||
uint8_t a = ie[off + 3];
|
||||
if (a == RSN_AKM_SAE || a == RSN_AKM_FT_SAE) sawSae = true;
|
||||
if (a == RSN_AKM_PSK) bestAkm = a;
|
||||
else if (a == RSN_AKM_PSK_SHA256 && bestAkm < 0) bestAkm = a;
|
||||
}
|
||||
if (bestAkm < 0) {
|
||||
if (sawSae)
|
||||
KernelLogStream(WARNING, "WiFi")
|
||||
<< "Network is WPA3-only (SAE); SAE authentication is not implemented";
|
||||
else
|
||||
KernelLogStream(WARNING, "WiFi")
|
||||
<< "AP offers no pre-shared-key AKM (enterprise 802.1X is not supported)";
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t rsnCaps = 0;
|
||||
if (off + 2 <= len) rsnCaps = Get16Le(ie + off);
|
||||
|
||||
if (rsnCaps & RSN_CAP_MFPR) {
|
||||
KernelLogStream(WARNING, "WiFi")
|
||||
<< "AP requires management frame protection, which needs BIP; not supported";
|
||||
return false;
|
||||
}
|
||||
|
||||
// A mixed WPA/WPA2 network can pair with CCMP but still broadcast under
|
||||
// TKIP. The firmware key slot and the RX decryption check both only
|
||||
// handle CCMP/GCMP, so the group key would silently fail to install and
|
||||
// every broadcast frame -- ARP requests, broadcast DHCP replies --
|
||||
// would be dropped. Refuse up front instead of half connecting.
|
||||
if (groupCipher != RSN_CIPHER_CCMP && groupCipher != RSN_CIPHER_GCMP
|
||||
&& groupCipher != RSN_CIPHER_CCMP_256
|
||||
&& groupCipher != RSN_CIPHER_GCMP_256) {
|
||||
KernelLogStream(WARNING, "WiFi")
|
||||
<< "AP broadcasts with an unsupported group cipher ("
|
||||
<< (uint64_t)groupCipher
|
||||
<< "); this is usually a mixed WPA/WPA2 network still using TKIP";
|
||||
return false;
|
||||
}
|
||||
|
||||
cfg.GroupCipher = groupCipher;
|
||||
cfg.PairwiseCipher = (uint8_t)bestPairwise;
|
||||
cfg.Akm = (uint8_t)bestAkm;
|
||||
cfg.Mfp = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Start / reset
|
||||
// =========================================================================
|
||||
|
||||
static bool HexNibble(char c, uint8_t* out) {
|
||||
if (c >= '0' && c <= '9') { *out = (uint8_t)(c - '0'); return true; }
|
||||
if (c >= 'a' && c <= 'f') { *out = (uint8_t)(c - 'a' + 10); return true; }
|
||||
if (c >= 'A' && c <= 'F') { *out = (uint8_t)(c - 'A' + 10); return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
// A 64-character hex string is the raw 256-bit PSK; anything else is a
|
||||
// passphrase and goes through PBKDF2 with the SSID as salt.
|
||||
static bool DerivePmk() {
|
||||
if (g_cfg.PassLen == 64) {
|
||||
bool allHex = true;
|
||||
uint8_t tmp[32];
|
||||
for (int i = 0; i < 32; i++) {
|
||||
uint8_t hi, lo;
|
||||
if (!HexNibble(g_cfg.Passphrase[i * 2], &hi)
|
||||
|| !HexNibble(g_cfg.Passphrase[i * 2 + 1], &lo)) {
|
||||
allHex = false;
|
||||
break;
|
||||
}
|
||||
tmp[i] = (uint8_t)((hi << 4) | lo);
|
||||
}
|
||||
if (allHex) {
|
||||
memcpy(g_pmk, tmp, 32);
|
||||
Crypto::SecureZero(tmp, sizeof(tmp));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (g_cfg.PassLen < 8) {
|
||||
KernelLogStream(WARNING, "WiFi")
|
||||
<< "WPA passphrases must be at least 8 characters";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4096 iterations of HMAC-SHA1 over a 32-byte output: this is a couple
|
||||
// of hundred milliseconds of pure CPU, once per connect.
|
||||
Crypto::Pbkdf2Sha1(g_cfg.Passphrase, g_cfg.PassLen,
|
||||
g_cfg.Ssid, g_cfg.SsidLen, 4096, g_pmk, 32);
|
||||
return true;
|
||||
}
|
||||
|
||||
void WpaReset() {
|
||||
g_state = WpaState::Idle;
|
||||
g_haveReplay = false;
|
||||
g_lastTxLen = 0;
|
||||
g_retries = 0;
|
||||
Crypto::SecureZero(g_pmk, sizeof(g_pmk));
|
||||
Crypto::SecureZero(g_ptk, sizeof(g_ptk));
|
||||
Crypto::SecureZero(g_snonce, sizeof(g_snonce));
|
||||
Crypto::SecureZero(&g_cfg.Passphrase, sizeof(g_cfg.Passphrase));
|
||||
}
|
||||
|
||||
bool WpaStart(const WpaConfig& cfg) {
|
||||
WpaReset();
|
||||
g_cfg = cfg;
|
||||
|
||||
switch (g_cfg.PairwiseCipher) {
|
||||
case RSN_CIPHER_CCMP:
|
||||
case RSN_CIPHER_GCMP:
|
||||
g_tkLen = 16;
|
||||
break;
|
||||
case RSN_CIPHER_CCMP_256:
|
||||
case RSN_CIPHER_GCMP_256:
|
||||
g_tkLen = 32;
|
||||
break;
|
||||
default:
|
||||
KernelLogStream(WARNING, "WiFi")
|
||||
<< "Unsupported pairwise cipher " << (uint64_t)g_cfg.PairwiseCipher;
|
||||
return false;
|
||||
}
|
||||
g_kckLen = 16;
|
||||
g_kekLen = 16;
|
||||
|
||||
if (!DerivePmk()) return false;
|
||||
|
||||
g_rsnIeLen = WpaBuildRsnIe(g_rsnIe, sizeof(g_rsnIe));
|
||||
if (!g_rsnIeLen) return false;
|
||||
|
||||
GenNonce(g_snonce);
|
||||
g_state = WpaState::WaitMsg1;
|
||||
g_startMs = 0;
|
||||
|
||||
KernelLogStream(INFO, "WiFi") << "Starting WPA handshake (AKM "
|
||||
<< (uint64_t)g_cfg.Akm << ", pairwise cipher "
|
||||
<< (uint64_t)g_cfg.PairwiseCipher << ")";
|
||||
return true;
|
||||
}
|
||||
|
||||
WpaState WpaGetState() { return g_state; }
|
||||
bool WpaIsComplete() { return g_state == WpaState::Complete; }
|
||||
|
||||
// =========================================================================
|
||||
// Outbound messages
|
||||
// =========================================================================
|
||||
|
||||
static bool SendKeyFrame(uint16_t keyInfo, const uint8_t* keyData,
|
||||
uint32_t keyDataLen, const uint8_t* nonce) {
|
||||
uint32_t total = sizeof(EapolKey) + keyDataLen;
|
||||
if (total > sizeof(g_lastTx)) return false;
|
||||
|
||||
memset(g_lastTx, 0, total);
|
||||
auto* k = (EapolKey*)g_lastTx;
|
||||
|
||||
k->version = g_eapolVersion;
|
||||
k->type = EAPOL_TYPE_KEY;
|
||||
Put16Be(k->length, (uint16_t)(total - 4));
|
||||
k->descType = g_descType;
|
||||
Put16Be(k->keyInfo, keyInfo);
|
||||
Put16Be(k->keyLength, 0); // RSN: always zero from the STA
|
||||
memcpy(k->replay, g_replay, 8);
|
||||
if (nonce) memcpy(k->nonce, nonce, 32);
|
||||
Put16Be(k->keyDataLen, (uint16_t)keyDataLen);
|
||||
if (keyDataLen) memcpy(g_lastTx + sizeof(EapolKey), keyData, keyDataLen);
|
||||
|
||||
if (keyInfo & KEY_INFO_MIC) ComputeMic(g_lastTx, total, k->mic);
|
||||
|
||||
g_lastTxLen = total;
|
||||
g_lastTxMs = Timekeeping::GetMilliseconds();
|
||||
g_retries = 0;
|
||||
return WpaTxEapol(g_lastTx, total);
|
||||
}
|
||||
|
||||
static bool SendMsg2() {
|
||||
uint16_t keyInfo = (uint16_t)(g_keyDescVer | KEY_INFO_KEY_TYPE
|
||||
| KEY_INFO_MIC);
|
||||
return SendKeyFrame(keyInfo, g_rsnIe, g_rsnIeLen, g_snonce);
|
||||
}
|
||||
|
||||
static bool SendMsg4() {
|
||||
uint16_t keyInfo = (uint16_t)(g_keyDescVer | KEY_INFO_KEY_TYPE
|
||||
| KEY_INFO_MIC | KEY_INFO_SECURE);
|
||||
return SendKeyFrame(keyInfo, nullptr, 0, nullptr);
|
||||
}
|
||||
|
||||
// Group key handshake reply: same shape as msg 4 without the pairwise bit.
|
||||
static bool SendGroupAck() {
|
||||
uint16_t keyInfo = (uint16_t)(g_keyDescVer | KEY_INFO_MIC
|
||||
| KEY_INFO_SECURE);
|
||||
return SendKeyFrame(keyInfo, nullptr, 0, nullptr);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Key data (KDE) handling
|
||||
// =========================================================================
|
||||
|
||||
// Decrypt the key data field of msg 3 or a group-key message. Returns the
|
||||
// plaintext length, or 0 on failure.
|
||||
static uint32_t DecryptKeyData(const uint8_t* in, uint32_t inLen,
|
||||
uint8_t* out, uint32_t outCap) {
|
||||
if (g_keyDescVer == KEY_DESC_VER_RC4) {
|
||||
KernelLogStream(WARNING, "WiFi")
|
||||
<< "AP used RC4 key wrapping (WPA1/TKIP), which is not supported";
|
||||
return 0;
|
||||
}
|
||||
if (inLen < 24 || (inLen % 8) != 0 || inLen - 8 > outCap) return 0;
|
||||
if (!Crypto::AesKeyUnwrap(Kek(), g_kekLen, in, inLen, out)) {
|
||||
KernelLogStream(WARNING, "WiFi")
|
||||
<< "EAPOL key data failed its integrity check (wrong passphrase?)";
|
||||
return 0;
|
||||
}
|
||||
return inLen - 8;
|
||||
}
|
||||
|
||||
// Walk the KDE list looking for a GTK (00-0F-AC data type 1).
|
||||
static bool FindGtk(const uint8_t* data, uint32_t len,
|
||||
const uint8_t** gtk, uint32_t* gtkLen, uint8_t* keyIdx) {
|
||||
uint32_t off = 0;
|
||||
while (off + 2 <= len) {
|
||||
uint8_t id = data[off];
|
||||
uint8_t elen = data[off + 1];
|
||||
if (elen == 0 || off + 2 + elen > len) break;
|
||||
const uint8_t* body = data + off + 2;
|
||||
|
||||
if (id == IEEE80211_ELEMID_VENDOR && elen >= 6 && IsRsnSuite(body)
|
||||
&& body[3] == 1) {
|
||||
// GTK KDE: [OUI 3][type 1][keyid+tx 1][reserved 1][GTK ...]
|
||||
*keyIdx = (uint8_t)(body[4] & 0x03);
|
||||
*gtk = body + 6;
|
||||
*gtkLen = (uint32_t)(elen - 6);
|
||||
return true;
|
||||
}
|
||||
off += 2 + elen;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Inbound handling
|
||||
// =========================================================================
|
||||
|
||||
static void Fail(const char* why) {
|
||||
KernelLogStream(WARNING, "WiFi") << "WPA handshake failed: " << why;
|
||||
g_state = WpaState::Failed;
|
||||
Crypto::SecureZero(g_ptk, sizeof(g_ptk));
|
||||
}
|
||||
|
||||
static void HandleMsg1(const uint8_t* frame, uint32_t len) {
|
||||
(void)len;
|
||||
auto* k = (const EapolKey*)frame;
|
||||
|
||||
memcpy(g_anonce, k->nonce, 32);
|
||||
memcpy(g_replay, k->replay, 8);
|
||||
g_haveReplay = true;
|
||||
|
||||
DerivePtk();
|
||||
|
||||
if (!SendMsg2()) {
|
||||
Fail("could not transmit message 2");
|
||||
return;
|
||||
}
|
||||
g_state = WpaState::WaitMsg3;
|
||||
KernelLogStream(INFO, "WiFi") << "Handshake message 1 received; sent message 2";
|
||||
}
|
||||
|
||||
static void HandleMsg3(const uint8_t* frame, uint32_t len) {
|
||||
auto* k = (const EapolKey*)frame;
|
||||
KernelLogStream(INFO, "WiFi") << "Handshake message 3 received";
|
||||
|
||||
// The ANonce must not have changed; if it has, the AP restarted the
|
||||
// exchange and our PTK is stale.
|
||||
if (memcmp(k->nonce, g_anonce, 32) != 0) {
|
||||
Fail("the AP changed its nonce mid-handshake");
|
||||
return;
|
||||
}
|
||||
|
||||
memcpy(g_replay, k->replay, 8);
|
||||
|
||||
uint16_t keyInfo = Get16Be(k->keyInfo);
|
||||
uint32_t keyDataLen = Get16Be(k->keyDataLen);
|
||||
if (sizeof(EapolKey) + keyDataLen > len) {
|
||||
Fail("truncated key data");
|
||||
return;
|
||||
}
|
||||
const uint8_t* keyData = frame + sizeof(EapolKey);
|
||||
|
||||
uint8_t plain[MAX_KEY_DATA];
|
||||
uint32_t plainLen = 0;
|
||||
if (keyInfo & KEY_INFO_ENCRYPTED) {
|
||||
plainLen = DecryptKeyData(keyData, keyDataLen, plain, sizeof(plain));
|
||||
if (!plainLen) {
|
||||
Fail("could not decrypt the group key");
|
||||
return;
|
||||
}
|
||||
} else if (keyDataLen <= sizeof(plain)) {
|
||||
memcpy(plain, keyData, keyDataLen);
|
||||
plainLen = keyDataLen;
|
||||
}
|
||||
|
||||
// Message 4 goes out before the keys are installed: the AP is still
|
||||
// sending in the clear until it sees it.
|
||||
if (!SendMsg4()) {
|
||||
Fail("could not transmit message 4");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!WpaInstallPtk(Tk(), g_tkLen, g_cfg.PairwiseCipher)) {
|
||||
Fail("the firmware rejected the pairwise key");
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t* gtk = nullptr;
|
||||
uint32_t gtkLen = 0;
|
||||
uint8_t keyIdx = 0;
|
||||
if (FindGtk(plain, plainLen, >k, >kLen, &keyIdx)) {
|
||||
if (!WpaInstallGtk(gtk, gtkLen, keyIdx, g_cfg.GroupCipher, k->rsc))
|
||||
KernelLogStream(WARNING, "WiFi")
|
||||
<< "Group key install failed; broadcast traffic will not be received";
|
||||
} else {
|
||||
KernelLogStream(WARNING, "WiFi")
|
||||
<< "No group key in message 3; broadcast traffic will not be received";
|
||||
}
|
||||
|
||||
g_state = WpaState::Complete;
|
||||
g_lastTxLen = 0;
|
||||
KernelLogStream(OK, "WiFi") << "WPA handshake complete; link is encrypted";
|
||||
}
|
||||
|
||||
// Periodic GTK rekey initiated by the AP (a 2-way exchange).
|
||||
static void HandleGroupKey(const uint8_t* frame, uint32_t len) {
|
||||
auto* k = (const EapolKey*)frame;
|
||||
memcpy(g_replay, k->replay, 8);
|
||||
|
||||
uint16_t keyInfo = Get16Be(k->keyInfo);
|
||||
uint32_t keyDataLen = Get16Be(k->keyDataLen);
|
||||
if (sizeof(EapolKey) + keyDataLen > len) return;
|
||||
const uint8_t* keyData = frame + sizeof(EapolKey);
|
||||
|
||||
uint8_t plain[MAX_KEY_DATA];
|
||||
uint32_t plainLen = 0;
|
||||
if (keyInfo & KEY_INFO_ENCRYPTED) {
|
||||
plainLen = DecryptKeyData(keyData, keyDataLen, plain, sizeof(plain));
|
||||
if (!plainLen) return;
|
||||
} else if (keyDataLen <= sizeof(plain)) {
|
||||
memcpy(plain, keyData, keyDataLen);
|
||||
plainLen = keyDataLen;
|
||||
}
|
||||
|
||||
const uint8_t* gtk = nullptr;
|
||||
uint32_t gtkLen = 0;
|
||||
uint8_t keyIdx = 0;
|
||||
if (FindGtk(plain, plainLen, >k, >kLen, &keyIdx))
|
||||
WpaInstallGtk(gtk, gtkLen, keyIdx, g_cfg.GroupCipher, k->rsc);
|
||||
|
||||
SendGroupAck();
|
||||
KernelLogStream(INFO, "WiFi") << "Group key rekeyed";
|
||||
}
|
||||
|
||||
bool WpaOnEapol(const uint8_t* data, uint32_t len) {
|
||||
if (g_state == WpaState::Idle || g_state == WpaState::Failed) return false;
|
||||
if (len < sizeof(EapolKey)) return false;
|
||||
|
||||
auto* k = (const EapolKey*)data;
|
||||
if (k->type != EAPOL_TYPE_KEY) return false;
|
||||
if (k->descType != EAPOL_KEY_DESC_RSN && k->descType != EAPOL_KEY_DESC_WPA)
|
||||
return false;
|
||||
|
||||
// Trust the frame's own length field over the (possibly padded) buffer
|
||||
// the driver handed us.
|
||||
uint32_t declared = (uint32_t)Get16Be(k->length) + 4;
|
||||
if (declared < sizeof(EapolKey) || declared > len) return false;
|
||||
len = declared;
|
||||
|
||||
g_eapolVersion = k->version > 3 ? 2 : k->version;
|
||||
g_descType = k->descType;
|
||||
|
||||
uint16_t keyInfo = Get16Be(k->keyInfo);
|
||||
uint8_t ver = (uint8_t)(keyInfo & KEY_INFO_VERSION_MASK);
|
||||
|
||||
if (keyInfo & KEY_INFO_REQUEST) return false; // STA->AP direction
|
||||
|
||||
if (ver == KEY_DESC_VER_RC4) {
|
||||
Fail("the AP asked for RC4/TKIP key wrapping, which is not supported");
|
||||
return true;
|
||||
}
|
||||
if (ver != KEY_DESC_VER_AES && ver != KEY_DESC_VER_AES_CMAC) {
|
||||
Fail("unknown EAPOL key descriptor version");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool pairwise = (keyInfo & KEY_INFO_KEY_TYPE) != 0;
|
||||
bool hasMic = (keyInfo & KEY_INFO_MIC) != 0;
|
||||
bool hasAck = (keyInfo & KEY_INFO_ACK) != 0;
|
||||
|
||||
if (keyInfo & KEY_INFO_ERROR) {
|
||||
Fail("the AP reported a MIC failure");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Message 1 is the only frame that arrives before a PTK exists, so it
|
||||
// is also the only one whose descriptor version we can adopt.
|
||||
if (pairwise && hasAck && !hasMic) {
|
||||
if (g_state != WpaState::WaitMsg1 && g_state != WpaState::WaitMsg3) {
|
||||
// The AP restarted the handshake; take it from the top.
|
||||
GenNonce(g_snonce);
|
||||
}
|
||||
g_keyDescVer = ver;
|
||||
HandleMsg1(data, len);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Everything else is MIC-protected, so the PTK has to exist first.
|
||||
if (!hasMic) return false;
|
||||
if (g_state == WpaState::WaitMsg1) return false;
|
||||
|
||||
if (!VerifyMic(data, len)) {
|
||||
KernelLogStream(WARNING, "WiFi")
|
||||
<< "Discarding EAPOL frame with a bad MIC";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (pairwise && hasAck) {
|
||||
if (g_state == WpaState::Complete) {
|
||||
// Our message 4 did not reach the AP and it retried message 3.
|
||||
// The keys are already in place, so just answer again.
|
||||
memcpy(g_replay, k->replay, 8);
|
||||
SendMsg4();
|
||||
} else {
|
||||
HandleMsg3(data, len);
|
||||
}
|
||||
} else if (!pairwise && hasAck) {
|
||||
HandleGroupKey(data, len);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Retransmission / timeout
|
||||
// =========================================================================
|
||||
|
||||
// Unsigned deadline test that survives a `since` newer than `nowMs`. The
|
||||
// caller samples the clock once per service pass while g_lastTxMs is
|
||||
// stamped the moment a frame goes out, so the two can cross; a plain
|
||||
// subtraction then wraps and expires every timer at once.
|
||||
static bool Elapsed(uint64_t nowMs, uint64_t since, uint64_t ms) {
|
||||
return nowMs > since && nowMs - since > ms;
|
||||
}
|
||||
|
||||
void WpaService(uint64_t nowMs) {
|
||||
if (g_state != WpaState::WaitMsg1 && g_state != WpaState::WaitMsg3) return;
|
||||
|
||||
if (g_startMs == 0) g_startMs = nowMs;
|
||||
|
||||
if (Elapsed(nowMs, g_startMs, HANDSHAKE_TIMEOUT_MS)) {
|
||||
Fail("the AP stopped responding");
|
||||
return;
|
||||
}
|
||||
|
||||
// Only our own messages are worth retrying; while waiting for msg 1
|
||||
// there is nothing to resend, the AP drives that.
|
||||
if (g_state == WpaState::WaitMsg3 && g_lastTxLen
|
||||
&& Elapsed(nowMs, g_lastTxMs, RETRY_INTERVAL_MS)) {
|
||||
if (g_retries >= MAX_RETRIES) {
|
||||
Fail("no response to message 2");
|
||||
return;
|
||||
}
|
||||
g_retries++;
|
||||
g_lastTxMs = nowMs;
|
||||
WpaTxEapol(g_lastTx, g_lastTxLen);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Wpa.hpp
|
||||
* WPA2/WPA3-PSK supplicant: PMK derivation and the EAPOL-Key 4-way
|
||||
* handshake that unlocks the link after association.
|
||||
*
|
||||
* The supplicant lives in the kernel because the handshake sits between
|
||||
* association and the first IP packet: nothing above the driver can send or
|
||||
* receive until the pairwise key is installed in the firmware. It drives
|
||||
* the exchange but does not touch the hardware itself -- transmitting and
|
||||
* key installation are provided by the MLME through the three hooks at the
|
||||
* bottom of this header.
|
||||
*
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Drivers::Net::Wifi {
|
||||
|
||||
enum class WpaState : uint8_t {
|
||||
Idle = 0,
|
||||
WaitMsg1, // associated, waiting for the AP to start the exchange
|
||||
WaitMsg3, // msg 2 sent, waiting for the GTK
|
||||
Complete, // keys installed, link is usable
|
||||
Failed,
|
||||
};
|
||||
|
||||
struct WpaConfig {
|
||||
uint8_t OwnMac[6];
|
||||
uint8_t Bssid[6];
|
||||
uint8_t Ssid[32];
|
||||
uint8_t SsidLen;
|
||||
char Passphrase[64];
|
||||
uint8_t PassLen;
|
||||
uint8_t Akm; // RSN_AKM_*
|
||||
uint8_t PairwiseCipher; // RSN_CIPHER_*
|
||||
uint8_t GroupCipher;
|
||||
bool Mfp; // management frame protection negotiated
|
||||
};
|
||||
|
||||
// Derive the PMK and arm the handshake. Returns false when the
|
||||
// configuration names a cipher or AKM this supplicant cannot do.
|
||||
bool WpaStart(const WpaConfig& cfg);
|
||||
void WpaReset();
|
||||
|
||||
// Feed the 802.1X payload of an inbound EAPOL frame (everything after the
|
||||
// LLC/SNAP header). Returns true when the frame was consumed.
|
||||
bool WpaOnEapol(const uint8_t* data, uint32_t len);
|
||||
|
||||
// Re-send the last outbound message if the AP has gone quiet, and fail the
|
||||
// handshake once it has been silent for too long. Called from the idle
|
||||
// loop; `nowMs` is a monotonic millisecond clock.
|
||||
void WpaService(uint64_t nowMs);
|
||||
|
||||
WpaState WpaGetState();
|
||||
bool WpaIsComplete();
|
||||
|
||||
// Build the RSN information element advertising what WpaStart() was
|
||||
// configured with. Returns the number of bytes written, 0 on error.
|
||||
uint32_t WpaBuildRsnIe(uint8_t* out, uint32_t cap);
|
||||
|
||||
// Pick the pairwise/group cipher and AKM out of an AP's RSN IE. `ie`
|
||||
// points at the element body (after id/len). Returns false when nothing
|
||||
// in the IE is supported.
|
||||
bool WpaParseApRsn(const uint8_t* ie, uint32_t len, WpaConfig& cfg);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Hooks implemented by the MLME (IwxConnect.cpp)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// Transmit an EAPOL frame body (802.1X header included) to the AP.
|
||||
bool WpaTxEapol(const uint8_t* body, uint32_t len);
|
||||
|
||||
// Install the pairwise temporal key. `cipher` is an RSN_CIPHER_* value.
|
||||
bool WpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher);
|
||||
|
||||
// Install a group temporal key at `keyIdx`. `rsc` is the EAPOL key RSC
|
||||
// field: 8 bytes, of which the low 6 are the AP's packet number.
|
||||
bool WpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
|
||||
uint8_t cipher, const uint8_t* rsc);
|
||||
}
|
||||
Reference in new issue
Block a user