feat: wi-fi - join WPA2/WPA3-PSK networks and carry traffic like ethernet
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Independent WPA2 authenticator driving the kernel supplicant through a
|
||||
4-way handshake. All crypto here comes from hashlib / the `cryptography`
|
||||
package, so it is a genuine oracle for the C++ implementation."""
|
||||
import hashlib, hmac, os, subprocess, sys, struct
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.hazmat.primitives.keywrap import aes_key_wrap, aes_key_unwrap
|
||||
|
||||
HARNESS = sys.argv[1]
|
||||
|
||||
def prf(key, label, data, nbytes):
|
||||
r = b''
|
||||
i = 0
|
||||
while len(r) < nbytes:
|
||||
r += hmac.new(key, label.encode() + b'\x00' + data + bytes([i]), hashlib.sha1).digest()
|
||||
i += 1
|
||||
return r[:nbytes]
|
||||
|
||||
def kdf_sha256(key, label, data, nbytes):
|
||||
r = b''
|
||||
i = 1
|
||||
bits = nbytes * 8
|
||||
while len(r) < nbytes:
|
||||
r += hmac.new(key, struct.pack('<H', i) + label.encode() + data + struct.pack('<H', bits),
|
||||
hashlib.sha256).digest()
|
||||
i += 1
|
||||
return r[:nbytes]
|
||||
|
||||
def derive_ptk(pmk, aa, spa, anonce, snonce, nbytes, sha256=False):
|
||||
data = min(aa, spa) + max(aa, spa) + min(anonce, snonce) + max(anonce, snonce)
|
||||
f = kdf_sha256 if sha256 else prf
|
||||
return f(pmk, "Pairwise key expansion", data, nbytes)
|
||||
|
||||
def mic(kck, frame, ver):
|
||||
if ver == 3:
|
||||
from cryptography.hazmat.primitives.cmac import CMAC
|
||||
c = CMAC(algorithms.AES(kck)); c.update(frame); return c.finalize()[:16]
|
||||
return hmac.new(kck, frame, hashlib.sha1).digest()[:16]
|
||||
|
||||
HDR = 99
|
||||
def build(ver, desc, key_info, replay, nonce, rsc, key_data, kck=None, mic_ver=2):
|
||||
body = bytearray(HDR + len(key_data))
|
||||
body[0] = ver
|
||||
body[1] = 3 # EAPOL-Key
|
||||
struct.pack_into('>H', body, 2, HDR + len(key_data) - 4)
|
||||
body[4] = desc
|
||||
struct.pack_into('>H', body, 5, key_info)
|
||||
struct.pack_into('>H', body, 7, 16) # key length (CCMP)
|
||||
body[9:17] = replay
|
||||
body[17:49] = nonce
|
||||
body[65:73] = rsc
|
||||
struct.pack_into('>H', body, 97, len(key_data))
|
||||
body[HDR:] = key_data
|
||||
if key_info & 0x0100: # MIC bit
|
||||
body[81:97] = mic(kck, bytes(body), mic_ver)
|
||||
return bytes(body)
|
||||
|
||||
def parse(frame):
|
||||
return {
|
||||
'key_info': struct.unpack_from('>H', frame, 5)[0],
|
||||
'replay': frame[9:17],
|
||||
'nonce': frame[17:49],
|
||||
'mic': frame[81:97],
|
||||
'kdlen': struct.unpack_from('>H', frame, 97)[0],
|
||||
'kd': frame[HDR:],
|
||||
}
|
||||
|
||||
def check_mic(frame, kck, ver):
|
||||
f = bytearray(frame); f[81:97] = b'\x00' * 16
|
||||
return mic(kck, bytes(f), ver) == frame[81:97]
|
||||
|
||||
def run_case(name, ssid, passphrase, akm, pcipher, gcipher, desc_ver, corrupt_pass=False):
|
||||
print(f"\n=== {name} ===")
|
||||
aa = bytes.fromhex('001122334455') # AP
|
||||
spa = bytes.fromhex('aabbccddeeff') # station
|
||||
p = subprocess.Popen([HARNESS], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, bufsize=1)
|
||||
|
||||
def send(line):
|
||||
p.stdin.write(line + '\n'); p.stdin.flush()
|
||||
def readline():
|
||||
return p.stdout.readline().strip()
|
||||
|
||||
sta_pass = passphrase + ('X' if corrupt_pass else '')
|
||||
send(f"START {spa.hex()} {aa.hex()} {ssid} {sta_pass} {akm} {pcipher} {gcipher}")
|
||||
ok = readline()
|
||||
rsnie = readline()
|
||||
assert ok.startswith('START-OK 1'), ok
|
||||
sta_rsn = bytes.fromhex(rsnie.split()[1])
|
||||
print(f" supplicant RSN IE: {sta_rsn.hex()}")
|
||||
|
||||
pmk = hashlib.pbkdf2_hmac('sha1', passphrase.encode(), ssid.encode(), 4096, 32)
|
||||
anonce = os.urandom(32)
|
||||
replay = (1).to_bytes(8, 'big')
|
||||
|
||||
# --- message 1 ---
|
||||
m1 = build(2, 2, desc_ver | 0x0008 | 0x0080, replay, anonce, b'\x00'*8, b'')
|
||||
send("RX " + m1.hex())
|
||||
tx = readline()
|
||||
assert tx.startswith('TX '), f"expected msg2, got {tx}"
|
||||
m2 = bytes.fromhex(tx.split()[1])
|
||||
status = readline()
|
||||
print(f" msg2 {len(m2)} bytes, {status}")
|
||||
|
||||
i2 = parse(m2)
|
||||
snonce = i2['nonce']
|
||||
ptk = derive_ptk(pmk, aa, spa, anonce, snonce, 48, sha256=(akm == 6))
|
||||
kck, kek, tk = ptk[:16], ptk[16:32], ptk[32:48]
|
||||
|
||||
m2_ok = check_mic(m2, kck, desc_ver)
|
||||
print(f" msg2 MIC verifies against independently derived PTK: {m2_ok}")
|
||||
if corrupt_pass:
|
||||
assert not m2_ok, "a wrong passphrase must not produce a valid MIC"
|
||||
print(" (expected: wrong passphrase -> MIC mismatch)")
|
||||
p.stdin.close(); return
|
||||
assert m2_ok
|
||||
assert i2['replay'] == replay, "msg2 must echo the replay counter"
|
||||
assert i2['kd'] == sta_rsn, "msg2 key data must be the supplicant's RSN IE"
|
||||
assert i2['key_info'] & 0x0008, "msg2 must set the pairwise bit"
|
||||
assert i2['key_info'] & 0x0100, "msg2 must set the MIC bit"
|
||||
|
||||
# --- message 3, carrying an AES-wrapped GTK KDE ---
|
||||
gtk = os.urandom(16)
|
||||
gtk_kde = bytes([0xdd, 6 + len(gtk), 0x00, 0x0f, 0xac, 0x01, 0x02, 0x00]) + gtk
|
||||
kd = gtk_kde
|
||||
if len(kd) % 8:
|
||||
kd += b'\xdd' + b'\x00' * (7 - len(kd) % 8)
|
||||
wrapped = aes_key_wrap(kek, kd)
|
||||
replay3 = (2).to_bytes(8, 'big')
|
||||
rsc = bytes.fromhex('0102030405060000')
|
||||
m3 = build(2, 2, desc_ver | 0x0008 | 0x0040 | 0x0080 | 0x0100 | 0x0200 | 0x1000,
|
||||
replay3, anonce, rsc, wrapped, kck, desc_ver)
|
||||
send("RX " + m3.hex())
|
||||
|
||||
got = {}
|
||||
while True:
|
||||
l = readline()
|
||||
if l.startswith('RX-OK'):
|
||||
print(f" {l}")
|
||||
break
|
||||
k, _, v = l.partition(' ')
|
||||
got[k] = v
|
||||
if k == 'TX':
|
||||
m4 = bytes.fromhex(v)
|
||||
|
||||
assert 'TX' in got, "supplicant must answer message 3 with message 4"
|
||||
m4_ok = check_mic(m4, kck, desc_ver)
|
||||
i4 = parse(m4)
|
||||
print(f" msg4 MIC verifies: {m4_ok}")
|
||||
assert m4_ok
|
||||
assert i4['replay'] == replay3, "msg4 must echo message 3's replay counter"
|
||||
assert i4['key_info'] & 0x0200, "msg4 must set the secure bit"
|
||||
assert i4['kdlen'] == 0, "msg4 must carry no key data"
|
||||
|
||||
assert got['PTK'] == tk.hex(), f"installed TK {got['PTK']} != expected {tk.hex()}"
|
||||
print(f" installed TK matches the AP's derivation: True")
|
||||
assert got['GTK'] == gtk.hex(), f"installed GTK {got['GTK']} != {gtk.hex()}"
|
||||
print(f" installed GTK matches: True (key index {got['GTK-IDX']})")
|
||||
assert got['GTK-RSC'].startswith('010203040506')
|
||||
assert 'STATE 3' in l, f"supplicant should be Complete, got {l}"
|
||||
print(" PASS")
|
||||
p.stdin.close()
|
||||
|
||||
run_case("WPA2-PSK / CCMP / key descriptor v2", "MontaukTest", "supersecret123", 2, 4, 4, 2)
|
||||
run_case("WPA2-PSK-SHA256 / CCMP / key descriptor v3", "MontaukTest", "supersecret123", 6, 4, 4, 3)
|
||||
run_case("wrong passphrase is rejected", "MontaukTest", "supersecret123", 2, 4, 4, 2, corrupt_pass=True)
|
||||
print("\nAll supplicant cases passed.")
|
||||
@@ -0,0 +1,640 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Drives the real MLME through a complete join and validates every frame it
|
||||
puts on the air, plus the 802.11 <-> Ethernet translation in both directions.
|
||||
|
||||
The access point side is written independently here (hashlib / cryptography for
|
||||
the handshake, hand-decoded 802.11 for the frames), so this is a check of what
|
||||
the driver actually emits rather than a restatement of it.
|
||||
|
||||
Copyright (c) 2026 Daniel Hammer
|
||||
"""
|
||||
import hashlib, hmac, os, struct, subprocess, sys
|
||||
from cryptography.hazmat.primitives.keywrap import aes_key_wrap
|
||||
|
||||
HARNESS = sys.argv[1]
|
||||
AP = bytes.fromhex('001122334455')
|
||||
STA = bytes.fromhex('aabbccddeeff')
|
||||
SSID = "MontaukTest"
|
||||
PASS = "supersecret123"
|
||||
CHANNEL = 6
|
||||
|
||||
fails = []
|
||||
def state_of(result):
|
||||
f = result.split()
|
||||
return int(f[f.index('STATE') + 1]) if 'STATE' in f else -1
|
||||
|
||||
def link_of(result):
|
||||
f = result.split()
|
||||
return int(f[f.index('LINK') + 1]) if 'LINK' in f else -1
|
||||
|
||||
def check(cond, what):
|
||||
print(f" {'PASS' if cond else 'FAIL'} {what}")
|
||||
if not cond:
|
||||
fails.append(what)
|
||||
|
||||
# --------------------------------------------------------------- harness I/O
|
||||
class Driver:
|
||||
def __init__(self):
|
||||
self.p = subprocess.Popen([HARNESS], stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, text=True, bufsize=1)
|
||||
def cmd(self, line):
|
||||
"""Send a command; collect emitted events until the terminator."""
|
||||
self.p.stdin.write(line + '\n'); self.p.stdin.flush()
|
||||
ev = {'TX': [], 'KEY': [], 'KEYDEL': [], 'ETH': [], 'CMD': [], 'TXQ': []}
|
||||
while True:
|
||||
l = self.p.stdout.readline()
|
||||
if not l:
|
||||
raise RuntimeError("harness died")
|
||||
l = l.strip()
|
||||
k = l.split()[0] if l else ''
|
||||
if k == 'TX':
|
||||
f = l.split()
|
||||
ev['TX'].append({'enc': f[1] == 'enc=1', 'rate': f[2] == 'rate=1',
|
||||
'hdr': bytes.fromhex(f[3]),
|
||||
'body': bytes.fromhex(f[4]) if len(f) > 4 else b''})
|
||||
elif k == 'KEY':
|
||||
f = l.split()
|
||||
ev['KEY'].append({'pairwise': f[1] == 'pairwise=1',
|
||||
'idx': int(f[2].split('=')[1]),
|
||||
'cipher': int(f[3].split('=')[1]),
|
||||
'key': bytes.fromhex(f[4]),
|
||||
'rsc': bytes.fromhex(f[5]) if len(f) > 5 else b''})
|
||||
elif k == 'ETH':
|
||||
ev['ETH'].append(bytes.fromhex(l.split()[1]))
|
||||
elif k == 'CMD':
|
||||
f = l.split()
|
||||
ev['CMD'].append((int(f[1]),
|
||||
bytes.fromhex(f[2]) if len(f) > 2 else b''))
|
||||
elif k == 'KEY-REMOVE':
|
||||
ev['KEYDEL'].append(l)
|
||||
elif k in ('TXQ-UP', 'TXQ-DOWN'):
|
||||
ev['TXQ'].append(l)
|
||||
else:
|
||||
ev['result'] = l
|
||||
return ev
|
||||
|
||||
# --------------------------------------------------------------- 802.11 bits
|
||||
def mgmt(subtype, dst, src, bssid, body, seq=0):
|
||||
return bytes([0x00 | subtype, 0x00]) + b'\x00\x00' + dst + src + bssid \
|
||||
+ struct.pack('<H', seq << 4) + body
|
||||
|
||||
def data_from_ds(da, bssid, sa, ethertype, payload, protected=False):
|
||||
fc1 = 0x02 | (0x40 if protected else 0)
|
||||
hdr = bytes([0x08, fc1]) + b'\x00\x00' + da + bssid + sa + b'\x00\x00'
|
||||
# The firmware decrypts in place and strips the MIC, but leaves the
|
||||
# 8-byte CCMP header, so model that when the protected bit is set.
|
||||
iv = b'\x11\x22\x00\x20\x00\x00\x00\x00' if protected else b''
|
||||
snap = bytes([0xaa, 0xaa, 0x03, 0, 0, 0]) + struct.pack('>H', ethertype)
|
||||
return hdr + iv + snap + payload
|
||||
|
||||
def parse_ies(b):
|
||||
ies, off = {}, 0
|
||||
while off + 2 <= len(b):
|
||||
i, l = b[off], b[off + 1]
|
||||
if off + 2 + l > len(b): break
|
||||
ies[i] = b[off + 2:off + 2 + l]
|
||||
off += 2 + l
|
||||
return ies
|
||||
|
||||
def suite(t): return bytes([0x00, 0x0f, 0xac, t])
|
||||
AP_RSN = (struct.pack('<H', 1) + suite(4)
|
||||
+ struct.pack('<H', 1) + suite(4)
|
||||
+ struct.pack('<H', 1) + suite(2)
|
||||
+ struct.pack('<H', 0))
|
||||
|
||||
# ------------------------------------------------------------------- EAPOL
|
||||
def prf(key, label, data, n):
|
||||
r, i = b'', 0
|
||||
while len(r) < n:
|
||||
r += hmac.new(key, label.encode() + b'\x00' + data + bytes([i]), hashlib.sha1).digest(); i += 1
|
||||
return r[:n]
|
||||
def derive_ptk(pmk, an, sn):
|
||||
return prf(pmk, "Pairwise key expansion",
|
||||
min(AP, STA) + max(AP, STA) + min(an, sn) + max(an, sn), 48)
|
||||
def emic(kck, f): return hmac.new(kck, f, hashlib.sha1).digest()[:16]
|
||||
HDR = 99
|
||||
def eapol(ki, replay, nonce, rsc, kd, kck=None):
|
||||
b = bytearray(HDR + len(kd)); b[0] = 2; b[1] = 3
|
||||
struct.pack_into('>H', b, 2, HDR + len(kd) - 4); b[4] = 2
|
||||
struct.pack_into('>H', b, 5, ki); struct.pack_into('>H', b, 7, 16)
|
||||
b[9:17] = replay; b[17:49] = nonce; b[65:73] = rsc
|
||||
struct.pack_into('>H', b, 97, len(kd)); b[HDR:] = kd
|
||||
if ki & 0x0100: b[81:97] = emic(kck, bytes(b))
|
||||
return bytes(b)
|
||||
def check_emic(f, kck):
|
||||
x = bytearray(f); x[81:97] = b'\x00' * 16
|
||||
return emic(kck, bytes(x)) == f[81:97]
|
||||
def gtk_kde(gtk, idx):
|
||||
kd = bytes([0xdd, 6 + len(gtk), 0x00, 0x0f, 0xac, 0x01, idx, 0x00]) + gtk
|
||||
if len(kd) % 8: kd += b'\xdd' + b'\x00' * (7 - len(kd) % 8)
|
||||
return kd
|
||||
|
||||
# =============================================================================
|
||||
d = Driver()
|
||||
d.cmd("MAC " + STA.hex())
|
||||
|
||||
print("=== authentication ===")
|
||||
ev = d.cmd(f"CONNECT {AP.hex()} {CHANNEL} 0 {SSID} {PASS} {AP_RSN.hex()}")
|
||||
check(ev['result'].startswith('CONNECT-OK 1'), "connect starts")
|
||||
check(len(ev['TXQ']) == 1 and ev['TXQ'][0].startswith('TXQ-UP'),
|
||||
"a transmit queue is opened before any frame is sent")
|
||||
check(len(ev['TX']) == 1, "exactly one frame is sent (the authentication request)")
|
||||
auth = ev['TX'][0]
|
||||
h, b = auth['hdr'], auth['body']
|
||||
check(h[0] == 0xb0, "authentication frame: type management, subtype auth")
|
||||
check(h[4:10] == AP and h[10:16] == STA and h[16:22] == AP,
|
||||
"authentication frame: addressed to the AP, from us, BSSID correct")
|
||||
check(len(h) == 24, "authentication frame: 24-byte header")
|
||||
check(struct.unpack('<H', b[0:2])[0] == 0, "authentication: open system algorithm")
|
||||
check(struct.unpack('<H', b[2:4])[0] == 1, "authentication: transaction sequence 1")
|
||||
check(struct.unpack('<H', b[4:6])[0] == 0, "authentication: status 0")
|
||||
check(not auth['enc'] and auth['rate'],
|
||||
"authentication frame sent in the clear at a fixed rate")
|
||||
|
||||
print("\n=== association ===")
|
||||
d.cmd("RXMGMT " + mgmt(0xb0, STA, AP, AP, struct.pack('<HHH', 0, 2, 0)).hex())
|
||||
ev = d.cmd("SERVICE")
|
||||
check(len(ev['TX']) == 1, "authentication success triggers one association request")
|
||||
ar = ev['TX'][0]
|
||||
check(ar['hdr'][0] == 0x00, "association request: subtype assoc-req")
|
||||
check(ar['hdr'][4:10] == AP and ar['hdr'][10:16] == STA,
|
||||
"association request: addressing correct")
|
||||
caps = struct.unpack('<H', ar['body'][0:2])[0]
|
||||
check(caps & 0x0001, "association request: ESS capability set")
|
||||
check(caps & 0x0010, "association request: privacy bit set for an encrypted network")
|
||||
li = struct.unpack('<H', ar['body'][2:4])[0]
|
||||
check(li == 10, "association request: listen interval present")
|
||||
ies = parse_ies(ar['body'][4:])
|
||||
check(ies.get(0) == SSID.encode(), "association request: SSID element matches")
|
||||
check(1 in ies and len(ies[1]) >= 4, "association request: supported rates present")
|
||||
check(any(r & 0x80 for r in ies[1]), "association request: at least one basic rate")
|
||||
check(48 in ies, "association request: RSN element present")
|
||||
sta_rsn = ies[48]
|
||||
check(sta_rsn[0:2] == struct.pack('<H', 1), "RSN element: version 1")
|
||||
check(sta_rsn[2:6] == suite(4), "RSN element: CCMP group cipher")
|
||||
check(sta_rsn[8:12] == suite(4), "RSN element: CCMP pairwise cipher")
|
||||
check(sta_rsn[14:18] == suite(2), "RSN element: PSK AKM")
|
||||
|
||||
aid = 0x0007
|
||||
d.cmd("RXMGMT " + mgmt(0x10, STA, AP, AP,
|
||||
struct.pack('<HHH', 0x0431, 0, aid | 0xc000)).hex())
|
||||
ev = d.cmd("SERVICE")
|
||||
check(state_of(ev['result']) == 6, "association moves the link into the handshake")
|
||||
check(len(ev['CMD']) >= 2, "post-association context updates are sent")
|
||||
|
||||
print("\n=== 4-way handshake carried over 802.11 data frames ===")
|
||||
pmk = hashlib.pbkdf2_hmac('sha1', PASS.encode(), SSID.encode(), 4096, 32)
|
||||
anonce = os.urandom(32)
|
||||
m1 = eapol(0x0002 | 0x0008 | 0x0080, (1).to_bytes(8, 'big'), anonce, b'\x00' * 8, b'')
|
||||
d.cmd("RXDATA " + data_from_ds(STA, AP, AP, 0x888e, m1).hex())
|
||||
ev = d.cmd("SERVICE")
|
||||
check(len(ev['TX']) == 1, "EAPOL message 1 produces exactly one reply")
|
||||
m2f = ev['TX'][0]
|
||||
h = m2f['hdr']
|
||||
check(h[0] == 0x08 and (h[1] & 0x01), "EAPOL reply: data frame with to-DS set")
|
||||
check(not (h[1] & 0x40), "EAPOL reply: not marked protected (no key yet)")
|
||||
check(not m2f['enc'], "EAPOL reply: firmware told not to encrypt")
|
||||
check(h[4:10] == AP and h[10:16] == STA and h[16:22] == AP,
|
||||
"EAPOL reply: addr1 the AP, addr2 us, addr3 the AP")
|
||||
check(m2f['body'][0:6] == bytes([0xaa, 0xaa, 0x03, 0, 0, 0]),
|
||||
"EAPOL reply: RFC 1042 LLC/SNAP shim")
|
||||
check(m2f['body'][6:8] == struct.pack('>H', 0x888e),
|
||||
"EAPOL reply: EtherType 0x888e")
|
||||
m2 = m2f['body'][8:]
|
||||
snonce = m2[17:49]
|
||||
ptk = derive_ptk(pmk, anonce, snonce)
|
||||
kck, kek, tk = ptk[:16], ptk[16:32], ptk[32:48]
|
||||
check(check_emic(m2, kck), "EAPOL message 2 MIC verifies under the AP's own PTK")
|
||||
check(m2[HDR:] == bytes([48, len(sta_rsn)]) + sta_rsn,
|
||||
"EAPOL message 2 carries the same RSN element as the association request")
|
||||
|
||||
gtk = os.urandom(16)
|
||||
rsc = bytes.fromhex('0a0b0c0d0e0f0000')
|
||||
m3 = eapol(0x0002 | 0x0008 | 0x0040 | 0x0080 | 0x0100 | 0x0200 | 0x1000,
|
||||
(2).to_bytes(8, 'big'), anonce, rsc, aes_key_wrap(kek, gtk_kde(gtk, 1)), kck)
|
||||
d.cmd("RXDATA " + data_from_ds(STA, AP, AP, 0x888e, m3).hex())
|
||||
ev = d.cmd("SERVICE")
|
||||
check(len(ev['TX']) == 1, "EAPOL message 3 produces message 4")
|
||||
m4 = ev['TX'][0]['body'][8:]
|
||||
check(check_emic(m4, kck), "EAPOL message 4 MIC verifies")
|
||||
keys = {('pairwise' if k['pairwise'] else 'group'): k for k in ev['KEY']}
|
||||
check('pairwise' in keys and keys['pairwise']['key'] == tk,
|
||||
"pairwise key installed matches the AP's temporal key")
|
||||
check(keys.get('pairwise', {}).get('cipher') == 4, "pairwise key installed as CCMP")
|
||||
check('group' in keys and keys['group']['key'] == gtk, "group key installed matches")
|
||||
check(keys.get('group', {}).get('rsc') == rsc[:6],
|
||||
"group key installed with the AP's receive sequence counter")
|
||||
check(state_of(ev['result']) == 7 and link_of(ev['result']) == 1,
|
||||
"link reports up once keyed")
|
||||
|
||||
print("\n=== data path ===")
|
||||
ip = bytes.fromhex('4500002800010000401100000a0000010a000002') + b'payload-here'
|
||||
peer = bytes.fromhex('665544332211')
|
||||
eth_out = peer + STA + struct.pack('>H', 0x0800) + ip
|
||||
ev = d.cmd("TXETH " + eth_out.hex())
|
||||
check(ev['result'] == 'TXETH-OK 1', "an Ethernet frame is accepted for transmit")
|
||||
f = ev['TX'][0]
|
||||
h = f['hdr']
|
||||
check(h[0] == 0x08 and (h[1] & 0x01), "outbound data: to-DS data frame")
|
||||
check(h[1] & 0x40, "outbound data: protected bit set now that keys are installed")
|
||||
check(f['enc'], "outbound data: firmware asked to encrypt")
|
||||
check(not f['rate'], "outbound data: rate control left to the firmware")
|
||||
check(h[4:10] == AP, "outbound data: addr1 is the AP")
|
||||
check(h[10:16] == STA, "outbound data: addr2 is us")
|
||||
check(h[16:22] == peer, "outbound data: addr3 is the final destination")
|
||||
check(f['body'][0:8] == bytes([0xaa, 0xaa, 0x03, 0, 0, 0]) + struct.pack('>H', 0x0800),
|
||||
"outbound data: LLC/SNAP carries the EtherType")
|
||||
check(f['body'][8:] == ip, "outbound data: IP payload preserved byte for byte")
|
||||
seq1 = struct.unpack('<H', h[22:24])[0]
|
||||
ev2 = d.cmd("TXETH " + eth_out.hex())
|
||||
seq2 = struct.unpack('<H', ev2['TX'][0]['hdr'][22:24])[0]
|
||||
check(seq2 != seq1, "outbound data: the sequence number advances between frames")
|
||||
|
||||
reply = bytes.fromhex('450000300002000040110000' '0a000002' '0a000001') + b'inbound-payload'
|
||||
ev = d.cmd("RXDATA " + data_from_ds(STA, AP, peer, 0x0800, reply, protected=True).hex())
|
||||
check(len(ev['ETH']) == 1, "an encrypted inbound data frame yields one Ethernet frame")
|
||||
e = ev['ETH'][0]
|
||||
check(e[0:6] == STA, "inbound data: Ethernet destination is addr1")
|
||||
check(e[6:12] == peer, "inbound data: Ethernet source is addr3")
|
||||
check(e[12:14] == struct.pack('>H', 0x0800), "inbound data: EtherType recovered")
|
||||
check(e[14:] == reply, "inbound data: payload recovered past the CCMP header")
|
||||
|
||||
bcast = bytes.fromhex('ffffffffffff')
|
||||
ev = d.cmd("RXDATA " + data_from_ds(bcast, AP, peer, 0x0806, b'arp-request-body').hex())
|
||||
check(len(ev['ETH']) == 1 and ev['ETH'][0][0:6] == bcast,
|
||||
"inbound broadcast (ARP) is delivered")
|
||||
|
||||
ev = d.cmd("RXDATA " + data_from_ds(STA, bytes.fromhex('aa0000000001'),
|
||||
peer, 0x0800, b'from-a-stranger').hex())
|
||||
check(len(ev['ETH']) == 0, "a data frame from a different BSSID is ignored")
|
||||
|
||||
ev = d.cmd("RXDATA " + (bytes([0x48, 0x02]) + b'\x00\x00' + STA + AP + peer
|
||||
+ b'\x00\x00').hex())
|
||||
check(len(ev['ETH']) == 0, "a null-data keepalive produces no Ethernet frame")
|
||||
|
||||
print("\n=== teardown ===")
|
||||
ev = d.cmd("ABORT")
|
||||
check(any(x.startswith('TXQ-DOWN') for x in ev['TXQ']), "abort closes the transmit queue")
|
||||
check(len(ev['KEYDEL']) == 2, "abort removes both hardware keys before the station")
|
||||
check(state_of(ev['result']) == 0, "abort returns to idle")
|
||||
ev = d.cmd("TXETH " + eth_out.hex())
|
||||
check(ev['result'] == 'TXETH-OK 0', "transmit is refused once the link is down")
|
||||
|
||||
# =============================================================================
|
||||
# Other paths through the state machine, each on a fresh harness.
|
||||
# =============================================================================
|
||||
|
||||
print("\n=== open network ===")
|
||||
d2 = Driver()
|
||||
d2.cmd("MAC " + STA.hex())
|
||||
ev = d2.cmd(f"CONNECT {AP.hex()} 11 0 OpenNet - -")
|
||||
check(ev['result'].startswith('CONNECT-OK 1'), "open network: connect starts")
|
||||
caps_seen = []
|
||||
d2.cmd("RXMGMT " + mgmt(0xb0, STA, AP, AP, struct.pack('<HHH', 0, 2, 0)).hex())
|
||||
ev = d2.cmd("SERVICE")
|
||||
ar = ev['TX'][0]
|
||||
caps = struct.unpack('<H', ar['body'][0:2])[0]
|
||||
check(not (caps & 0x0010), "open network: privacy bit clear")
|
||||
ies = parse_ies(ar['body'][4:])
|
||||
check(48 not in ies, "open network: no RSN element in the association request")
|
||||
d2.cmd("RXMGMT " + mgmt(0x10, STA, AP, AP,
|
||||
struct.pack('<HHH', 0x0421, 0, 3 | 0xc000)).hex())
|
||||
ev = d2.cmd("SERVICE")
|
||||
check(state_of(ev['result']) == 7 and link_of(ev['result']) == 1,
|
||||
"open network: link comes up straight after association, no handshake")
|
||||
check(len(ev['KEY']) == 0, "open network: no keys installed")
|
||||
ev = d2.cmd("TXETH " + (peer + STA + struct.pack('>H', 0x0800) + ip).hex())
|
||||
f = ev['TX'][0]
|
||||
check(not (f['hdr'][1] & 0x40) and not f['enc'],
|
||||
"open network: data frames are sent unprotected")
|
||||
|
||||
print("\n=== the access point never answers ===")
|
||||
d3 = Driver()
|
||||
d3.cmd("MAC " + STA.hex())
|
||||
d3.cmd(f"CONNECT {AP.hex()} 6 0 Nowhere - -")
|
||||
retries = 0
|
||||
for _ in range(6):
|
||||
d3.cmd("TICK 500")
|
||||
ev = d3.cmd("SERVICE")
|
||||
retries += len(ev['TX'])
|
||||
if state_of(ev['result']) == 8:
|
||||
break
|
||||
check(retries >= 3, f"authentication is retransmitted ({retries} retries) before giving up")
|
||||
check(state_of(ev['result']) == 8, "the attempt eventually fails rather than hanging")
|
||||
check(any(x.startswith('TXQ-DOWN') for x in ev['TXQ']),
|
||||
"giving up tears the transmit queue back down")
|
||||
|
||||
print("\n=== the access point rejects us ===")
|
||||
d4 = Driver()
|
||||
d4.cmd("MAC " + STA.hex())
|
||||
d4.cmd(f"CONNECT {AP.hex()} 6 0 Rejects - -")
|
||||
ev = d4.cmd("RXMGMT " + mgmt(0xb0, STA, AP, AP, struct.pack('<HHH', 0, 2, 1)).hex())
|
||||
check(state_of(ev['result']) == 8, "an authentication rejection fails the attempt")
|
||||
|
||||
d5 = Driver()
|
||||
d5.cmd("MAC " + STA.hex())
|
||||
d5.cmd(f"CONNECT {AP.hex()} 6 0 Rejects - -")
|
||||
d5.cmd("RXMGMT " + mgmt(0xb0, STA, AP, AP, struct.pack('<HHH', 0, 2, 0)).hex())
|
||||
d5.cmd("SERVICE")
|
||||
ev = d5.cmd("RXMGMT " + mgmt(0x10, STA, AP, AP,
|
||||
struct.pack('<HHH', 0x0421, 17, 0)).hex())
|
||||
check(state_of(ev['result']) == 8, "an association rejection fails the attempt")
|
||||
|
||||
print("\n=== the access point drops us ===")
|
||||
d6 = Driver()
|
||||
d6.cmd("MAC " + STA.hex())
|
||||
d6.cmd(f"CONNECT {AP.hex()} 6 0 OpenNet - -")
|
||||
d6.cmd("RXMGMT " + mgmt(0xb0, STA, AP, AP, struct.pack('<HHH', 0, 2, 0)).hex())
|
||||
d6.cmd("SERVICE")
|
||||
d6.cmd("RXMGMT " + mgmt(0x10, STA, AP, AP,
|
||||
struct.pack('<HHH', 0x0421, 0, 3 | 0xc000)).hex())
|
||||
ev = d6.cmd("SERVICE")
|
||||
check(link_of(ev['result']) == 1, "link up before the deauthentication")
|
||||
d6.cmd("RXMGMT " + mgmt(0xc0, STA, AP, AP, struct.pack('<H', 3)).hex())
|
||||
ev = d6.cmd("SERVICE")
|
||||
check(state_of(ev['result']) == 0 and link_of(ev['result']) == 0,
|
||||
"a deauthentication from the AP brings the link down")
|
||||
ev = d6.cmd("TXETH " + (peer + STA + struct.pack('>H', 0x0800) + ip).hex())
|
||||
check(ev['result'] == 'TXETH-OK 0', "transmit is refused after being dropped")
|
||||
|
||||
# =============================================================================
|
||||
# PHY context command layout.
|
||||
#
|
||||
# Regression test for a lockup: firmware 89 advertises ULTRA_HB_CHANNELS, which
|
||||
# selects an 8-byte channel-info sub-structure (32-bit channel first). Sending
|
||||
# the older 4-byte form shifted every field after it, asserted the firmware, and
|
||||
# the driver then spun forever waiting for a reply that would never come.
|
||||
# =============================================================================
|
||||
|
||||
print("\n=== PHY context command layout ===")
|
||||
PHY_CONTEXT_CMD = 0x08
|
||||
ULTRA_HB = 48
|
||||
|
||||
def phy_cmd_for(uhb):
|
||||
dd = Driver()
|
||||
dd.cmd("MAC " + STA.hex())
|
||||
dd.cmd(f"CAPA {ULTRA_HB} {1 if uhb else 0}")
|
||||
ev = dd.cmd(f"CONNECT {AP.hex()} {CHANNEL} 0 OpenNet - -")
|
||||
dd.p.stdin.close()
|
||||
for cid, payload in ev['CMD']:
|
||||
if cid == PHY_CONTEXT_CMD:
|
||||
return payload
|
||||
return None
|
||||
|
||||
p_uhb = phy_cmd_for(True)
|
||||
check(p_uhb is not None and len(p_uhb) == 32,
|
||||
f"ultra-high-band firmware gets a 32-byte PHY context ({len(p_uhb) if p_uhb else 0})")
|
||||
if p_uhb and len(p_uhb) == 32:
|
||||
chan = struct.unpack_from('<I', p_uhb, 8)[0]
|
||||
band, width, ctrl = p_uhb[12], p_uhb[13], p_uhb[14]
|
||||
lmac = struct.unpack_from('<I', p_uhb, 16)[0]
|
||||
check(chan == CHANNEL, "UHB layout: channel is the 32-bit field at offset 8")
|
||||
check(band == 1, "UHB layout: band follows the channel (2.4 GHz = 1)")
|
||||
check(width == 0 and ctrl == 0, "UHB layout: 20 MHz, control position below")
|
||||
check(lmac == 0, "UHB layout: lmac_id lands at offset 16, not shifted")
|
||||
|
||||
p_v1 = phy_cmd_for(False)
|
||||
check(p_v1 is not None and len(p_v1) == 28,
|
||||
f"older firmware still gets the 28-byte form ({len(p_v1) if p_v1 else 0})")
|
||||
if p_v1 and len(p_v1) == 28:
|
||||
check(p_v1[8] == 1 and p_v1[9] == CHANNEL,
|
||||
"legacy layout: band then channel, both single bytes")
|
||||
check(struct.unpack_from('<I', p_v1, 12)[0] == 0,
|
||||
"legacy layout: lmac_id at offset 12")
|
||||
|
||||
# =============================================================================
|
||||
# MLD command sizes and layout.
|
||||
#
|
||||
# This firmware implements the MLD API, so the connect path uses MAC_CONFIG /
|
||||
# LINK_CONFIG / STA_CONFIG rather than the legacy MAC_CONTEXT / BINDING /
|
||||
# ADD_STA. Sizes and field offsets below are exactly what Linux puts on the
|
||||
# wire against this same firmware (tests/wifi/decode_iwl_trace.py).
|
||||
# =============================================================================
|
||||
|
||||
print("\n=== MLD command sizes ===")
|
||||
MAC_CONFIG = (3 << 8) | 0x08
|
||||
LINK_CONFIG = (3 << 8) | 0x09
|
||||
STA_CONFIG = (3 << 8) | 0x0a
|
||||
STA_REMOVE = (3 << 8) | 0x0c
|
||||
PHY_CONTEXT = 0x08
|
||||
|
||||
dd = Driver()
|
||||
dd.cmd("MAC " + STA.hex())
|
||||
dd.cmd(f"CAPA {ULTRA_HB} 1")
|
||||
ev = dd.cmd(f"CONNECT {AP.hex()} {CHANNEL} 0 OpenNet - -")
|
||||
sizes, first = {}, {}
|
||||
for cid, payload in ev['CMD']:
|
||||
sizes.setdefault(cid, len(payload))
|
||||
first.setdefault(cid, payload)
|
||||
|
||||
check(sizes.get(MAC_CONFIG) == 52, f"MAC_CONFIG_CMD is 52 bytes (got {sizes.get(MAC_CONFIG)})")
|
||||
check(sizes.get(LINK_CONFIG) == 208, f"LINK_CONFIG_CMD is 208 bytes (got {sizes.get(LINK_CONFIG)})")
|
||||
check(sizes.get(STA_CONFIG) == 96, f"STA_CONFIG_CMD is 96 bytes (got {sizes.get(STA_CONFIG)})")
|
||||
check(sizes.get(PHY_CONTEXT) == 32, f"PHY_CONTEXT_CMD is 32 bytes (got {sizes.get(PHY_CONTEXT)})")
|
||||
check(0x28 not in sizes and 0x2b not in sizes and 0x18 not in sizes,
|
||||
"no legacy MAC_CONTEXT / BINDING / ADD_STA is sent")
|
||||
|
||||
m = first.get(MAC_CONFIG)
|
||||
if m:
|
||||
check(struct.unpack_from('<I', m, 8)[0] == 5, "MAC_CONFIG: mac_type is BSS_STA")
|
||||
check(m[12:18] == STA, "MAC_CONFIG: local_mld_addr is our MAC")
|
||||
check(struct.unpack_from('<I', m, 20)[0] == 0x0c,
|
||||
"MAC_CONFIG: accepts group + beacon frames before association")
|
||||
check(m[36] == 0, "MAC_CONFIG: is_assoc clear on the initial add")
|
||||
|
||||
l = first.get(LINK_CONFIG)
|
||||
if l:
|
||||
check(struct.unpack_from('<I', l, 0)[0] == 1, "LINK_CONFIG: first one is an ADD")
|
||||
check(struct.unpack_from('<I', l, 12)[0] == 0xffffffff,
|
||||
"LINK_CONFIG: phy_id is INVALID until the link is bound")
|
||||
check(l[16:22] == STA, "LINK_CONFIG: local_link_addr is our MAC")
|
||||
check(struct.unpack_from('<I', l, 136)[0] == 0,
|
||||
"LINK_CONFIG: no beacon timing before association")
|
||||
|
||||
# The PHY binding and the activation must be two separate MODIFYs, in that
|
||||
# order, after the PHY context exists. Folding them into one command asserts
|
||||
# fw 89 (UMAC error 0x2010330F, seen on the AX211); Linux sends the binding
|
||||
# first with mask 0 (__iwl_mvm_mld_assign_vif_chanctx).
|
||||
links = [(i, p) for i, (c, p) in enumerate(ev['CMD']) if c == LINK_CONFIG]
|
||||
phy_add = next((i for i, (c, p) in enumerate(ev['CMD'])
|
||||
if c == PHY_CONTEXT and struct.unpack_from('<I', p, 4)[0] == 1), None)
|
||||
check(len(links) >= 3, f"link add, PHY binding and activation are three commands "
|
||||
f"(got {len(links)})")
|
||||
if len(links) >= 3 and phy_add is not None:
|
||||
bind_i, bind = links[1]
|
||||
act_i, act = links[2]
|
||||
check(phy_add < bind_i, "the PHY context exists before the link binds to it")
|
||||
check(struct.unpack_from('<I', bind, 0)[0] == 2
|
||||
and struct.unpack_from('<I', bind, 12)[0] == 0
|
||||
and struct.unpack_from('<I', bind, 24)[0] == 0
|
||||
and struct.unpack_from('<I', bind, 28)[0] == 0,
|
||||
"PHY binding is its own MODIFY: phy_id set, mask 0, still inactive")
|
||||
check(struct.unpack_from('<I', act, 0)[0] == 2
|
||||
and struct.unpack_from('<I', act, 24)[0] == 0x03
|
||||
and struct.unpack_from('<I', act, 28)[0] == 1,
|
||||
"activation is a later MODIFY with exactly ACTIVE|RATES_INFO")
|
||||
# Field values the Linux trace carries at this stage (fw 89 asserted with
|
||||
# UMAC error 0x2010303E when they differed).
|
||||
check(struct.unpack_from('<I', act, 136)[0] == 100
|
||||
and struct.unpack_from('<I', act, 140)[0] == 0,
|
||||
"activation carries the beacon interval but no DTIM interval yet")
|
||||
check(struct.unpack_from('<I', act, 56)[0] == 0x2,
|
||||
"pre-assoc qos_flags is TGN without UPDATE_EDCA")
|
||||
check(struct.unpack_from('<H', act, 82)[0] == 0
|
||||
and struct.unpack_from('<H', act, 62)[0] == 1023,
|
||||
"pre-assoc EDCA is the default contention set, not WMM")
|
||||
check(struct.unpack_from('<H', act, 146)[0] == 0,
|
||||
"no RTS threshold before association")
|
||||
|
||||
st = first.get(STA_CONFIG)
|
||||
if st:
|
||||
check(struct.unpack_from('<I', st, 0)[0] == 0, "STA_CONFIG: station id 0")
|
||||
check(st[8:14] == AP and st[16:22] == AP,
|
||||
"STA_CONFIG: both peer addresses are the BSSID")
|
||||
check(struct.unpack_from('<I', st, 28)[0] == 0,
|
||||
"STA_CONFIG: association id 0 before association")
|
||||
dd.p.stdin.close()
|
||||
|
||||
# =============================================================================
|
||||
# Beacon timing arrives with association, not before.
|
||||
# =============================================================================
|
||||
|
||||
print("\n=== link beacon timing ===")
|
||||
dd = Driver()
|
||||
dd.cmd("MAC " + STA.hex())
|
||||
dd.cmd(f"CAPA {ULTRA_HB} 1")
|
||||
dd.cmd(f"CONNECT {AP.hex()} {CHANNEL} 0 OpenNet - -")
|
||||
dd.cmd("RXMGMT " + mgmt(0xb0, STA, AP, AP, struct.pack('<HHH', 0, 2, 0)).hex())
|
||||
dd.cmd("SERVICE")
|
||||
dd.cmd("RXMGMT " + mgmt(0x10, STA, AP, AP,
|
||||
struct.pack('<HHH', 0x0421, 0, 7 | 0xc000)).hex())
|
||||
ev = dd.cmd("SERVICE")
|
||||
link_i = next((i for i, (c, p) in enumerate(ev['CMD']) if c == LINK_CONFIG), None)
|
||||
mac_i = next((i for i, (c, p) in enumerate(ev['CMD']) if c == MAC_CONFIG), None)
|
||||
link = ev['CMD'][link_i][1] if link_i is not None else None
|
||||
mac = ev['CMD'][mac_i][1] if mac_i is not None else None
|
||||
check(link is not None and mac is not None,
|
||||
"association updates the link and the MAC")
|
||||
check(link_i is not None and mac_i is not None and link_i < mac_i,
|
||||
"one-shot link timing is programmed before the MAC is marked associated")
|
||||
if link:
|
||||
mask = struct.unpack_from('<I', link, 24)[0]
|
||||
check(struct.unpack_from('<I', link, 0)[0] == 2, "post-assoc link is a MODIFY")
|
||||
check(mask & 0x10, "post-assoc link sets the BEACON_TIMING modify bit")
|
||||
check(mask == 0x1e,
|
||||
f"legacy post-assoc link changes exactly rates/protection/QoS/timing (got {mask:#x})")
|
||||
check(not (mask & 0x01),
|
||||
"post-assoc link does not re-assert ACTIVE on an active link")
|
||||
check(struct.unpack_from('<I', link, 136)[0] == 100,
|
||||
"post-assoc link carries the beacon interval")
|
||||
check(struct.unpack_from('<I', link, 140)[0] == 200,
|
||||
"post-assoc link carries bi * dtim period")
|
||||
check(struct.unpack_from('<I', link, 56)[0] == 0x3,
|
||||
"post-assoc qos_flags is TGN plus UPDATE_EDCA")
|
||||
check(struct.unpack_from('<H', link, 82)[0] == 3008,
|
||||
"post-assoc EDCA switches to the WMM set")
|
||||
if mac:
|
||||
check(mac[36] == 1, "post-assoc MAC has is_assoc set")
|
||||
check(struct.unpack_from('<H', mac, 40)[0] == 7, "post-assoc MAC carries the AID")
|
||||
check(struct.unpack_from('<I', mac, 20)[0] == 0x04,
|
||||
"post-assoc MAC stops asking for beacons")
|
||||
check(not any(c == STA_CONFIG for c, p in ev['CMD']),
|
||||
"client AID is not incorrectly written to STA_CONFIG's GO-only assoc_id")
|
||||
dd.p.stdin.close()
|
||||
|
||||
# =============================================================================
|
||||
# Deadlines are not allowed to expire the moment they are set.
|
||||
#
|
||||
# The service loop sampled the clock once at the top of the pass, then spent
|
||||
# real milliseconds inside the post-association context commands, each of which
|
||||
# stamps a *newer* timestamp. Comparing the stale `now` against those stamps
|
||||
# underflowed the unsigned subtraction, so a join that had just succeeded
|
||||
# reported "timed out while joining the network" straight after "Associated",
|
||||
# tore the contexts down, and the access point's first EAPOL frame arrived to
|
||||
# find the station already gone.
|
||||
#
|
||||
# The harness advances its clock on every host command (CMD_ROUND_TRIP_MS), so
|
||||
# a pass that sends commands and then checks a deadline reproduces this.
|
||||
# =============================================================================
|
||||
|
||||
print("\n=== deadlines survive the time spent sending commands ===")
|
||||
d7 = Driver()
|
||||
d7.cmd("MAC " + STA.hex())
|
||||
d7.cmd(f"CONNECT {AP.hex()} {CHANNEL} 0 {SSID} {PASS} {AP_RSN.hex()}")
|
||||
d7.cmd("RXMGMT " + mgmt(0xb0, STA, AP, AP, struct.pack('<HHH', 0, 2, 0)).hex())
|
||||
ev = d7.cmd("SERVICE")
|
||||
check(state_of(ev['result']) == 4,
|
||||
"sending the association request does not immediately expire its own retry timer")
|
||||
check(len(ev['TX']) == 1,
|
||||
"the association request is sent once per pass, not retransmitted on the spot")
|
||||
|
||||
d7.cmd("RXMGMT " + mgmt(0x10, STA, AP, AP,
|
||||
struct.pack('<HHH', 0x0421, 0, 7 | 0xc000)).hex())
|
||||
ev = d7.cmd("SERVICE")
|
||||
check(state_of(ev['result']) == 6,
|
||||
"association reaches the handshake instead of timing out in the same pass")
|
||||
check(not any(x.startswith('TXQ-DOWN') for x in ev['TXQ']),
|
||||
"a successful association does not tear the transmit queue back down")
|
||||
check(not any(c == STA_REMOVE for c, p in ev['CMD']),
|
||||
"a successful association does not remove the station it just added")
|
||||
|
||||
# The EAPOL exchange arrives a moment later, exactly as it does on the air.
|
||||
m1_late = eapol(0x0002 | 0x0008 | 0x0080, (1).to_bytes(8, 'big'),
|
||||
bytes(range(32)), b'\x00' * 8, b'')
|
||||
ev = d7.cmd("RXDATA " + data_from_ds(STA, AP, AP, 0x888e, m1_late).hex())
|
||||
ev = d7.cmd("SERVICE")
|
||||
check(len(ev['TX']) == 1,
|
||||
"an EAPOL message 1 that arrives after the context updates is still answered")
|
||||
check(state_of(ev['result']) == 6,
|
||||
"the station is still in the handshake when message 1 is answered")
|
||||
|
||||
# =============================================================================
|
||||
# Teardown order: the MAC gives up its association first.
|
||||
#
|
||||
# While MAC_CONFIG.is_assoc is set, the firmware's MAC context owns the link
|
||||
# carrying the BSS. Deactivating that link underneath it asserts the UMAC
|
||||
# (0x2000320F on firmware 89) -- the failure the log above ends in.
|
||||
# =============================================================================
|
||||
|
||||
print("\n=== teardown clears the association before the link ===")
|
||||
ev = d7.cmd("ABORT")
|
||||
seq = [(c, p) for c, p in ev['CMD']]
|
||||
|
||||
|
||||
def index_of(pred):
|
||||
return next((i for i, (c, p) in enumerate(seq) if pred(c, p)), None)
|
||||
|
||||
|
||||
deassoc = index_of(lambda c, p: c == MAC_CONFIG
|
||||
and struct.unpack_from('<I', p, 4)[0] == 2 and p[36] == 0)
|
||||
sta_rm = index_of(lambda c, p: c == STA_REMOVE)
|
||||
deact = index_of(lambda c, p: c == LINK_CONFIG
|
||||
and struct.unpack_from('<I', p, 0)[0] == 2
|
||||
and struct.unpack_from('<I', p, 24)[0] & 0x01
|
||||
and struct.unpack_from('<I', p, 28)[0] == 0)
|
||||
link_rm = index_of(lambda c, p: c == LINK_CONFIG
|
||||
and struct.unpack_from('<I', p, 0)[0] == 3)
|
||||
mac_rm = index_of(lambda c, p: c == MAC_CONFIG
|
||||
and struct.unpack_from('<I', p, 4)[0] == 3)
|
||||
|
||||
check(deassoc is not None,
|
||||
"teardown sends a MAC_CONFIG MODIFY clearing is_assoc")
|
||||
check(deact is not None,
|
||||
"teardown deactivates the link before removing it")
|
||||
check(deassoc is not None and deact is not None and deassoc < deact,
|
||||
"the association is cleared before the link is deactivated")
|
||||
check(deassoc is not None and sta_rm is not None and deassoc < sta_rm,
|
||||
"the association is cleared before the station is removed")
|
||||
check(sta_rm is not None and deact is not None and sta_rm < deact,
|
||||
"the station is removed before the link it sits on is deactivated")
|
||||
check(deact is not None and link_rm is not None and deact < link_rm,
|
||||
"the link is deactivated before it is removed")
|
||||
check(link_rm is not None and mac_rm is not None and link_rm < mac_rm,
|
||||
"the link is removed before the MAC that owns it")
|
||||
d7.p.stdin.close()
|
||||
|
||||
print()
|
||||
if fails:
|
||||
for f in fails: print("FAILURE:", f)
|
||||
sys.exit(1)
|
||||
print("All MLME and data-path cases passed.")
|
||||
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Second-round supplicant tests: GTK rekey, message-3 retransmission, and RSN
|
||||
negotiation against realistic access-point information elements."""
|
||||
import hashlib, hmac, os, subprocess, sys, struct
|
||||
from cryptography.hazmat.primitives.keywrap import aes_key_wrap
|
||||
HARNESS = sys.argv[1]
|
||||
fails = []
|
||||
|
||||
def prf(key, label, data, n):
|
||||
r=b''; i=0
|
||||
while len(r)<n:
|
||||
r+=hmac.new(key, label.encode()+b'\x00'+data+bytes([i]), hashlib.sha1).digest(); i+=1
|
||||
return r[:n]
|
||||
def derive_ptk(pmk, aa, spa, an, sn):
|
||||
return prf(pmk, "Pairwise key expansion", min(aa,spa)+max(aa,spa)+min(an,sn)+max(an,sn), 48)
|
||||
def mic(kck, f): return hmac.new(kck, f, hashlib.sha1).digest()[:16]
|
||||
HDR=99
|
||||
def build(ki, replay, nonce, rsc, kd, kck=None):
|
||||
b=bytearray(HDR+len(kd)); b[0]=2; b[1]=3
|
||||
struct.pack_into('>H',b,2,HDR+len(kd)-4); b[4]=2
|
||||
struct.pack_into('>H',b,5,ki); struct.pack_into('>H',b,7,16)
|
||||
b[9:17]=replay; b[17:49]=nonce; b[65:73]=rsc
|
||||
struct.pack_into('>H',b,97,len(kd)); b[HDR:]=kd
|
||||
if ki & 0x0100: b[81:97]=mic(kck, bytes(b))
|
||||
return bytes(b)
|
||||
def check_mic(f,kck):
|
||||
x=bytearray(f); x[81:97]=b'\x00'*16
|
||||
return mic(kck,bytes(x))==f[81:97]
|
||||
def gtk_kde(gtk, idx):
|
||||
kd = bytes([0xdd, 6+len(gtk), 0x00,0x0f,0xac, 0x01, idx, 0x00]) + gtk
|
||||
if len(kd)%8: kd += b'\xdd' + b'\x00'*(7-len(kd)%8)
|
||||
return kd
|
||||
|
||||
def start(p, ssid, passphrase, spa, aa):
|
||||
p.stdin.write(f"START {spa.hex()} {aa.hex()} {ssid} {passphrase} 2 4 4\n"); p.stdin.flush()
|
||||
p.stdout.readline(); p.stdout.readline()
|
||||
|
||||
def rd(p): return p.stdout.readline().strip()
|
||||
|
||||
# ---------------------------------------------------------------- handshake +
|
||||
print("=== GTK rekey and message-3 retransmission ===")
|
||||
aa=bytes.fromhex('001122334455'); spa=bytes.fromhex('aabbccddeeff')
|
||||
ssid="MontaukTest"; pw="supersecret123"
|
||||
p=subprocess.Popen([HARNESS],stdin=subprocess.PIPE,stdout=subprocess.PIPE,text=True,bufsize=1)
|
||||
start(p, ssid, pw, spa, aa)
|
||||
pmk=hashlib.pbkdf2_hmac('sha1',pw.encode(),ssid.encode(),4096,32)
|
||||
an=os.urandom(32)
|
||||
p.stdin.write("RX "+build(0x0002|0x0008|0x0080,(1).to_bytes(8,'big'),an,b'\x00'*8,b'').hex()+"\n"); p.stdin.flush()
|
||||
m2=bytes.fromhex(rd(p).split()[1]); rd(p)
|
||||
sn=m2[17:49]
|
||||
ptk=derive_ptk(pmk,aa,spa,an,sn); kck,kek,tk=ptk[:16],ptk[16:32],ptk[32:48]
|
||||
gtk1=os.urandom(16)
|
||||
m3=build(0x0002|0x0008|0x0040|0x0080|0x0100|0x0200|0x1000,(2).to_bytes(8,'big'),an,
|
||||
b'\x00'*8, aes_key_wrap(kek, gtk_kde(gtk1,1)), kck)
|
||||
p.stdin.write("RX "+m3.hex()+"\n"); p.stdin.flush()
|
||||
got={}
|
||||
while True:
|
||||
l=rd(p)
|
||||
if l.startswith('RX-OK'): break
|
||||
k,_,v=l.partition(' '); got[k]=v
|
||||
assert got['GTK']==gtk1.hex() and got['PTK']==tk.hex()
|
||||
print(f" initial handshake complete, state {l}")
|
||||
|
||||
# message 3 retransmitted (AP missed message 4)
|
||||
m3b=build(0x0002|0x0008|0x0040|0x0080|0x0100|0x0200|0x1000,(3).to_bytes(8,'big'),an,
|
||||
b'\x00'*8, aes_key_wrap(kek, gtk_kde(gtk1,1)), kck)
|
||||
p.stdin.write("RX "+m3b.hex()+"\n"); p.stdin.flush()
|
||||
got2={}
|
||||
while True:
|
||||
l=rd(p)
|
||||
if l.startswith('RX-OK'): break
|
||||
k,_,v=l.partition(' '); got2[k]=v
|
||||
if 'TX' not in got2:
|
||||
fails.append("no message 4 in response to a retransmitted message 3")
|
||||
else:
|
||||
m4=bytes.fromhex(got2['TX'])
|
||||
ok = check_mic(m4,kck) and m4[9:17]==(3).to_bytes(8,'big')
|
||||
print(f" answered retransmitted message 3 with a fresh message 4: {ok}")
|
||||
if not ok: fails.append("message 4 replay answer malformed")
|
||||
|
||||
# group key rekey (2-way, no pairwise bit)
|
||||
gtk2=os.urandom(16)
|
||||
gk=build(0x0002|0x0080|0x0100|0x0200|0x1000,(4).to_bytes(8,'big'),b'\x00'*32,
|
||||
bytes.fromhex('0a0b0c000000 0000'.replace(' ','')), aes_key_wrap(kek, gtk_kde(gtk2,2)), kck)
|
||||
p.stdin.write("RX "+gk.hex()+"\n"); p.stdin.flush()
|
||||
got3={}
|
||||
while True:
|
||||
l=rd(p)
|
||||
if l.startswith('RX-OK'): break
|
||||
k,_,v=l.partition(' '); got3[k]=v
|
||||
if got3.get('GTK')!=gtk2.hex():
|
||||
fails.append(f"group rekey installed {got3.get('GTK')} instead of {gtk2.hex()}")
|
||||
else:
|
||||
print(f" group rekey installed the new GTK (index {got3['GTK-IDX']})")
|
||||
if 'TX' not in got3:
|
||||
fails.append("no acknowledgement to the group key message")
|
||||
else:
|
||||
m=bytes.fromhex(got3['TX'])
|
||||
ki=struct.unpack_from('>H',m,5)[0]
|
||||
ok = bool(check_mic(m, kck) and not (ki & 0x0008) and (ki & 0x0200))
|
||||
print(f" group key acknowledged correctly (no pairwise bit, secure set): {ok}")
|
||||
if not ok: fails.append("group key acknowledgement malformed")
|
||||
p.stdin.close()
|
||||
|
||||
# ---------------------------------------------------------------- RSN parsing
|
||||
print("\n=== RSN negotiation against real-world information elements ===")
|
||||
def suite(t): return bytes([0x00,0x0f,0xac,t])
|
||||
def ie(group, pairwise, akms, caps=0):
|
||||
b = struct.pack('<H',1) + suite(group)
|
||||
b += struct.pack('<H',len(pairwise)) + b''.join(suite(x) for x in pairwise)
|
||||
b += struct.pack('<H',len(akms)) + b''.join(suite(x) for x in akms)
|
||||
b += struct.pack('<H',caps)
|
||||
return b
|
||||
|
||||
cases = [
|
||||
("WPA2-PSK, CCMP only", ie(4,[4],[2]), True, 2, 4),
|
||||
("WPA2 mixed TKIP+CCMP (TKIP group)",ie(2,[4,2],[2]), False,0, 0),
|
||||
("WPA2 mixed pairwise, CCMP group", ie(4,[4,2],[2]), True, 2, 4),
|
||||
("WPA2/WPA3 transition (PSK+SAE)", ie(4,[4],[2,8],0x0080), True, 2, 4),
|
||||
("WPA3-only SAE, MFP required", ie(4,[4],[8],0x00c0), False,0, 0),
|
||||
("TKIP-only", ie(2,[2],[2]), False,0, 0),
|
||||
("enterprise 802.1X", ie(4,[4],[1]), False,0, 0),
|
||||
("PSK-SHA256 only", ie(4,[4],[6]), True, 6, 4),
|
||||
("GCMP-256", ie(9,[9],[2]), True, 2, 9),
|
||||
]
|
||||
p=subprocess.Popen([HARNESS],stdin=subprocess.PIPE,stdout=subprocess.PIPE,text=True,bufsize=1)
|
||||
for name, body, expect_ok, expect_akm, expect_pc in cases:
|
||||
p.stdin.write("PARSE "+body.hex()+"\n"); p.stdin.flush()
|
||||
r=rd(p).split()
|
||||
ok = r[1]=='1'; akm=int(r[3]); pc=int(r[5])
|
||||
good = (ok==expect_ok) and (not expect_ok or (akm==expect_akm and pc==expect_pc))
|
||||
print(f" {'PASS' if good else 'FAIL'} {name:36} -> accepted={ok} akm={akm} pairwise={pc}")
|
||||
if not good: fails.append(f"RSN parse: {name}")
|
||||
p.stdin.close()
|
||||
|
||||
print()
|
||||
if fails:
|
||||
for f in fails: print("FAILURE:", f)
|
||||
sys.exit(1)
|
||||
print("All second-round supplicant cases passed.")
|
||||
@@ -0,0 +1,111 @@
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include "Libraries/Crypto.hpp"
|
||||
using namespace Kt::Crypto;
|
||||
|
||||
static int fails = 0;
|
||||
static void hexdump(const char* label, const uint8_t* p, size_t n) {
|
||||
printf("%s: ", label);
|
||||
for (size_t i = 0; i < n; i++) printf("%02x", p[i]);
|
||||
printf("\n");
|
||||
}
|
||||
static void check(const char* name, const uint8_t* got, const char* wantHex) {
|
||||
size_t n = strlen(wantHex) / 2;
|
||||
uint8_t want[128];
|
||||
for (size_t i = 0; i < n; i++) { unsigned v; sscanf(wantHex + 2*i, "%2x", &v); want[i] = (uint8_t)v; }
|
||||
if (memcmp(got, want, n) == 0) { printf("PASS %s\n", name); }
|
||||
else { printf("FAIL %s\n", name); hexdump(" got ", got, n); printf(" want: %s\n", wantHex); fails++; }
|
||||
}
|
||||
|
||||
int main() {
|
||||
uint8_t out[64];
|
||||
|
||||
Sha1("abc", 3, out);
|
||||
check("sha1(abc)", out, "a9993e364706816aba3e25717850c26c9cd0d89d");
|
||||
Sha1("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", 56, out);
|
||||
check("sha1(448bit)", out, "84983e441c3bd26ebaae4aa1f95129e5e54670f1");
|
||||
|
||||
Sha256("abc", 3, out);
|
||||
check("sha256(abc)", out, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
|
||||
Sha256("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", 56, out);
|
||||
check("sha256(448bit)", out, "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1");
|
||||
|
||||
{ uint8_t k[20]; memset(k, 0x0b, 20);
|
||||
HmacSha1(k, 20, "Hi There", 8, out);
|
||||
check("hmac-sha1 rfc2202#1", out, "b617318655057264e28bc0b6fb378c8ef146be00"); }
|
||||
{ HmacSha1((const uint8_t*)"Jefe", 4, "what do ya want for nothing?", 28, out);
|
||||
check("hmac-sha1 rfc2202#2", out, "effcdf6ae5eb2fa2d27416d5f184df9c259a7c79"); }
|
||||
{ uint8_t k[20]; memset(k, 0x0b, 20);
|
||||
HmacSha256(k, 20, "Hi There", 8, out);
|
||||
check("hmac-sha256 rfc4231#1", out, "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"); }
|
||||
|
||||
// FIPS-197 AES-128 / AES-256
|
||||
{ AesCtx c; uint8_t key[16], pt[16], ct[16], back[16];
|
||||
for (int i = 0; i < 16; i++) key[i] = (uint8_t)i;
|
||||
for (int i = 0; i < 16; i++) pt[i] = (uint8_t)(i * 0x11);
|
||||
AesInit(c, key, 16); AesEncryptBlock(c, pt, ct);
|
||||
check("aes128 fips197", ct, "69c4e0d86a7b0430d8cdb78070b4c55a");
|
||||
AesDecryptBlock(c, ct, back);
|
||||
check("aes128 decrypt", back, "00112233445566778899aabbccddeeff"); }
|
||||
{ AesCtx c; uint8_t key[32], pt[16], ct[16], back[16];
|
||||
for (int i = 0; i < 32; i++) key[i] = (uint8_t)i;
|
||||
for (int i = 0; i < 16; i++) pt[i] = (uint8_t)(i * 0x11);
|
||||
AesInit(c, key, 32); AesEncryptBlock(c, pt, ct);
|
||||
check("aes256 fips197", ct, "8ea2b7ca516745bfeafc49904b496089");
|
||||
AesDecryptBlock(c, ct, back);
|
||||
check("aes256 decrypt", back, "00112233445566778899aabbccddeeff"); }
|
||||
|
||||
// RFC 3394 section 4.1 (128-bit KEK, 128-bit key) and 4.6 (256/256)
|
||||
{ uint8_t kek[16], kd[16], wrapped[24], unwrapped[16];
|
||||
for (int i = 0; i < 16; i++) kek[i] = (uint8_t)i;
|
||||
for (int i = 0; i < 16; i++) kd[i] = (uint8_t)(i * 0x11);
|
||||
AesKeyWrap(kek, 16, kd, 16, wrapped);
|
||||
check("keywrap rfc3394 4.1", wrapped, "1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5");
|
||||
bool ok = AesKeyUnwrap(kek, 16, wrapped, 24, unwrapped);
|
||||
printf("%s keyunwrap integrity\n", ok ? "PASS" : "FAIL"); if (!ok) fails++;
|
||||
check("keyunwrap rfc3394 4.1", unwrapped, "00112233445566778899aabbccddeeff"); }
|
||||
{ // 256-bit KEK, 256-bit key data (RFC3394 4.6)
|
||||
uint8_t kek[32], kd[32], wrapped[40], unwrapped[32];
|
||||
for (int i = 0; i < 32; i++) kek[i] = (uint8_t)i;
|
||||
const char* kdhex = "00112233445566778899AABBCCDDEEFF000102030405060708090A0B0C0D0E0F";
|
||||
for (int i = 0; i < 32; i++) { unsigned v; sscanf(kdhex + 2*i, "%2x", &v); kd[i] = (uint8_t)v; }
|
||||
AesKeyWrap(kek, 32, kd, 32, wrapped);
|
||||
check("keywrap rfc3394 4.6", wrapped, "28c9f404c4b810f4cbccb35cfb87f8263f5786e2d80ed326cbc7f0e71a99f43bfb988b9b7a02dd21");
|
||||
bool ok = AesKeyUnwrap(kek, 32, wrapped, 40, unwrapped);
|
||||
printf("%s keyunwrap256 integrity\n", ok ? "PASS" : "FAIL"); if (!ok) fails++; }
|
||||
|
||||
// RFC 4493 AES-CMAC
|
||||
{ uint8_t key[16] = {0x2b,0x7e,0x15,0x16,0x28,0xae,0xd2,0xa6,0xab,0xf7,0x15,0x88,0x09,0xcf,0x4f,0x3c};
|
||||
const uint8_t* parts[1]; size_t lens[1];
|
||||
uint8_t empty[1] = {0};
|
||||
parts[0] = empty; lens[0] = 0;
|
||||
AesCmac(key, 16, parts, lens, 1, out);
|
||||
check("cmac rfc4493 len0", out, "bb1d6929e95937287fa37d129b756746");
|
||||
uint8_t msg[64] = {0x6b,0xc1,0xbe,0xe2,0x2e,0x40,0x9f,0x96,0xe9,0x3d,0x7e,0x11,0x73,0x93,0x17,0x2a,
|
||||
0xae,0x2d,0x8a,0x57,0x1e,0x03,0xac,0x9c,0x9e,0xb7,0x6f,0xac,0x45,0xaf,0x8e,0x51,
|
||||
0x30,0xc8,0x1c,0x46,0xa3,0x5c,0xe4,0x11,0xe5,0xfb,0xc1,0x19,0x1a,0x0a,0x52,0xef,
|
||||
0xf6,0x9f,0x24,0x45,0xdf,0x4f,0x9b,0x17,0xad,0x2b,0x41,0x7b,0xe6,0x6c,0x37,0x10};
|
||||
parts[0] = msg; lens[0] = 16;
|
||||
AesCmac(key, 16, parts, lens, 1, out);
|
||||
check("cmac rfc4493 len16", out, "070a16b46b4d4144f79bdd9dd04a287c");
|
||||
parts[0] = msg; lens[0] = 40;
|
||||
AesCmac(key, 16, parts, lens, 1, out);
|
||||
check("cmac rfc4493 len40", out, "dfa66747de9ae63030ca32611497c827");
|
||||
parts[0] = msg; lens[0] = 64;
|
||||
AesCmac(key, 16, parts, lens, 1, out);
|
||||
check("cmac rfc4493 len64", out, "51f0bebf7e3b9d92fc49741779363cfe");
|
||||
// split across parts to exercise the streaming path
|
||||
const uint8_t* sp[3] = {msg, msg+10, msg+33}; size_t sl[3] = {10, 23, 31};
|
||||
AesCmac(key, 16, sp, sl, 3, out);
|
||||
check("cmac split len64", out, "51f0bebf7e3b9d92fc49741779363cfe"); }
|
||||
|
||||
// WPA PSK vectors (IEEE 802.11i Annex H.4)
|
||||
{ uint8_t psk[32];
|
||||
Pbkdf2Sha1("password", 8, (const uint8_t*)"IEEE", 4, 4096, psk, 32);
|
||||
check("pbkdf2 wpa 'password'/IEEE", psk, "f42c6fc52df0ebef9ebb4b90b38a5f902e83fe1b135a70e23aed762e9710a12e");
|
||||
Pbkdf2Sha1("ThisIsAPassword", 15, (const uint8_t*)"ThisIsASSID", 11, 4096, psk, 32);
|
||||
check("pbkdf2 wpa 'ThisIsAPassword'", psk, "0dc0d6eb90555ed6419756b9a15ec3e3209b63df707dd508d14581f8982721af"); }
|
||||
|
||||
printf(fails ? "\n%d FAILURES\n" : "\nall vectors pass\n", fails);
|
||||
return fails != 0;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Decode an iwlwifi host-command trace captured from Linux on the same adapter.
|
||||
|
||||
Linux drives this exact firmware successfully, so its host commands are ground
|
||||
truth for what the firmware expects -- far more reliable than reading struct
|
||||
definitions out of a kernel tree and hoping the version matches.
|
||||
|
||||
Capture on a Linux box with the adapter, as root:
|
||||
|
||||
trace-cmd record -e iwlwifi_dev_hcmd -o /tmp/iwl.dat \
|
||||
sh -c 'nmcli radio wifi off; sleep 2; nmcli radio wifi on; sleep 20'
|
||||
|
||||
Then run this against /tmp/iwl.dat. dump_iwl_cmds.py prints full payload hex
|
||||
for each command instead of a decoded summary.
|
||||
|
||||
Copyright (c) 2026 Daniel Hammer
|
||||
"""
|
||||
import re, subprocess
|
||||
out = subprocess.run(['trace-cmd','report','-R','-i','/tmp/iwl.dat'],
|
||||
capture_output=True, text=True).stdout
|
||||
ACT={0:'STUB',1:'ADD',2:'MODIFY',3:'REMOVE'}
|
||||
def u32(p,o): return p[o]|(p[o+1]<<8)|(p[o+2]<<16)|(p[o+3]<<24)
|
||||
rows=[]
|
||||
for line in out.splitlines():
|
||||
m=re.search(r'hcmd=ARRAY\[(.*?)\]',line); t=re.search(r'\s(\d+\.\d+):',line)
|
||||
if not m: continue
|
||||
b=[int(x,16) for x in m.group(1).split(', ')]
|
||||
if len(b)<8: continue
|
||||
grp,op=b[1],b[0]; ln=b[4]|(b[5]<<8); p=b[8:8+ln]; ts=float(t.group(1)) if t else 0
|
||||
if (grp,op)==(3,0x08) and ln>=52:
|
||||
rows.append((ts,f"MAC_CONFIG action={ACT.get(u32(p,4)):6} id={u32(p,0)} is_assoc={p[36]} aid={p[40]|p[41]<<8} filter=0x{u32(p,20):x}"))
|
||||
elif (grp,op)==(3,0x09) and ln>=208:
|
||||
rows.append((ts,f"LINK_CONFIG action={ACT.get(u32(p,0)):6} link={u32(p,4)} mac={u32(p,8)} phy={u32(p,12)} mask=0x{u32(p,24):02x} active={p[28]} bi={u32(p,136)}"))
|
||||
elif (grp,op)==(3,0x0a) and ln>=96:
|
||||
rows.append((ts,f"STA_CONFIG sta={u32(p,0)} link={u32(p,4)} type={u32(p,24)} aid={u32(p,28)}"))
|
||||
elif (grp,op)==(1,0x08) and ln>=32:
|
||||
rows.append((ts,f"PHY_CONTEXT action={ACT.get(u32(p,4)):6} id={u32(p,0)} chan={u32(p,8)} band={p[12]} width={p[13]}"))
|
||||
elif (grp,op)==(3,0x05):
|
||||
rows.append((ts,f"SESSION_PROT action={ACT.get(u32(p,4)):6} conf_id={u32(p,8)} dur={u32(p,12)}"))
|
||||
elif (grp,op)==(5,0x17):
|
||||
rows.append((ts,f"SCD_QUEUE_CFG op={u32(p,0)}"))
|
||||
elif (grp,op)==(3,0x0c):
|
||||
rows.append((ts,"STA_REMOVE"))
|
||||
rows.sort(); base=rows[0][0]
|
||||
for ts,s in rows:
|
||||
if ts-base > 2.3: print(f"{ts-base:7.3f} {s}")
|
||||
@@ -0,0 +1,27 @@
|
||||
import re, sys, subprocess
|
||||
out = subprocess.run(['trace-cmd','report','-R','-i','/tmp/iwl.dat'],
|
||||
capture_output=True, text=True).stdout
|
||||
NAMES = {(3,0x08):'MAC_CONFIG', (3,0x09):'LINK_CONFIG', (3,0x0a):'STA_CONFIG',
|
||||
(3,0x0c):'STA_REMOVE', (1,0x08):'PHY_CONTEXT', (3,0x05):'SESSION_PROT'}
|
||||
seen = {}
|
||||
for line in out.splitlines():
|
||||
m = re.search(r'hcmd=ARRAY\[(.*?)\]', line)
|
||||
if not m: continue
|
||||
b = [int(x,16) for x in m.group(1).split(', ')]
|
||||
if len(b) < 8: continue
|
||||
op, grp = b[0], b[1]
|
||||
ln = b[4] | (b[5] << 8)
|
||||
key = (grp, op)
|
||||
if key not in NAMES: continue
|
||||
payload = b[8:8+ln]
|
||||
action = payload[0] | (payload[1]<<8) | (payload[2]<<16) | (payload[3]<<24) if len(payload)>=4 else -1
|
||||
seen.setdefault(key, []).append((ln, payload))
|
||||
for key, lst in seen.items():
|
||||
print(f"\n===== {NAMES[key]} (group 0x{key[0]:02x} cmd 0x{key[1]:02x}) x{len(lst)} =====")
|
||||
lens = {l for l,_ in lst}
|
||||
print(f"payload length(s): {sorted(lens)}")
|
||||
# show the most 'interesting' one (most non-zero bytes)
|
||||
ln, p = max(lst, key=lambda t: sum(1 for x in t[1] if x))
|
||||
for off in range(0, len(p), 16):
|
||||
chunk = p[off:off+16]
|
||||
print(f" +{off:3d}: " + " ".join(f"{x:02x}" for x in chunk))
|
||||
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* mlme_harness.cpp
|
||||
* Host harness for the 802.11 MLME and data path.
|
||||
*
|
||||
* Compiles the real IwxConnect.cpp (and the supplicant behind it) against a
|
||||
* stubbed transport, so everything the driver puts on the air and everything
|
||||
* it makes of what comes back can be checked without the adapter. The
|
||||
* firmware/radio interaction is what remains untestable here; the frame
|
||||
* construction, parsing, encapsulation and state machine are not.
|
||||
*
|
||||
* Speaks hex over stdio; ap_mlme.py is the peer.
|
||||
*
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
|
||||
#include "Drivers/Net/Wifi/Iwx.hpp"
|
||||
#include "Drivers/Net/Wifi/Ieee80211.hpp"
|
||||
#include "Drivers/Net/Wifi/Wpa.hpp"
|
||||
|
||||
namespace Timekeeping {
|
||||
uint64_t g_ms = 1000;
|
||||
uint64_t GetMilliseconds() { return g_ms; }
|
||||
}
|
||||
|
||||
// A host command is a round trip to the firmware: it takes real time, and the
|
||||
// clock moves on while the service loop is inside one. Modelling that is what
|
||||
// catches elapsed-time arithmetic that samples the clock once and then compares
|
||||
// it against timestamps taken later in the same pass (an unsigned underflow
|
||||
// that reads as an instant timeout). A frozen clock hides that class of bug
|
||||
// entirely, which is why this is not simply left at a constant.
|
||||
static constexpr uint64_t CMD_ROUND_TRIP_MS = 1;
|
||||
|
||||
using namespace Drivers::Net::Wifi;
|
||||
|
||||
static void puthex(const char* tag, const uint8_t* p, uint32_t n) {
|
||||
printf("%s ", tag);
|
||||
for (uint32_t i = 0; i < n; i++) printf("%02x", p[i]);
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Stubbed transport
|
||||
// =============================================================================
|
||||
|
||||
namespace Drivers::Net::Wifi {
|
||||
|
||||
IwxState g_iwx;
|
||||
|
||||
// Host commands: record the opcode and the exact bytes, always succeed.
|
||||
// The payload matters -- a struct that does not match the version the
|
||||
// firmware advertises asserts it on real hardware.
|
||||
static void DumpCmd(uint32_t id, const void* data, uint32_t len) {
|
||||
printf("CMD %u ", id);
|
||||
const uint8_t* p = (const uint8_t*)data;
|
||||
for (uint32_t i = 0; i < len; i++) printf("%02x", p[i]);
|
||||
printf("\n");
|
||||
Timekeeping::g_ms += CMD_ROUND_TRIP_MS;
|
||||
}
|
||||
bool IwxSendCmdPdu(uint32_t id, const void* data, uint32_t len) {
|
||||
DumpCmd(id, data, len);
|
||||
return true;
|
||||
}
|
||||
bool IwxSendCmdStatus(uint32_t id, const void* data, uint32_t len,
|
||||
uint32_t* statusOut) {
|
||||
DumpCmd(id, data, len);
|
||||
// ADD_STA reports success in the low byte; everything else uses 0.
|
||||
if (statusOut) *statusOut = (id == IWX_ADD_STA || id == IWX_ADD_STA_KEY)
|
||||
? IWX_ADD_STA_SUCCESS : 0;
|
||||
return true;
|
||||
}
|
||||
bool IwxSendCmd(IwxHostCmd& cmd) { (void)cmd; return true; }
|
||||
|
||||
int IwxLookupCmdVer(uint8_t group, uint8_t cmd) {
|
||||
// Match what AX211 firmware 89 advertises for the versions the MLME
|
||||
// branches on.
|
||||
if (group == IWX_DATA_PATH_GROUP && cmd == IWX_RLC_CONFIG_CMD) return 2;
|
||||
if (group == IWX_DATA_PATH_GROUP && cmd == IWX_SCD_QUEUE_CONFIG_CMD) return 3;
|
||||
return -1;
|
||||
}
|
||||
int IwxLookupNotifVer(uint8_t, uint8_t) { return 7; }
|
||||
|
||||
bool IwxAbortScan() { return true; }
|
||||
|
||||
bool IwxEnableTxq(IwxTxRing& ring, int staId, int qid, int tid) {
|
||||
(void)staId; (void)tid;
|
||||
ring.Qid = qid;
|
||||
ring.Active = true;
|
||||
ring.StageSlots = IWX_TX_STAGE_SLOTS;
|
||||
printf("TXQ-UP %d\n", qid);
|
||||
return true;
|
||||
}
|
||||
void IwxDisableTxq(IwxTxRing& ring, int staId, int tid) {
|
||||
(void)staId; (void)tid;
|
||||
ring.Active = false;
|
||||
printf("TXQ-DOWN\n");
|
||||
}
|
||||
|
||||
// Capture what the driver wants to put on the air.
|
||||
bool IwxTxFrame(IwxTxRing& ring, const uint8_t* hdr, uint32_t hdrLen,
|
||||
const uint8_t* payload, uint32_t payloadLen,
|
||||
bool encrypt, bool fixedRate) {
|
||||
if (!ring.Active) { printf("TX-DROP queue-down\n"); return false; }
|
||||
printf("TX enc=%d rate=%d ", encrypt ? 1 : 0, fixedRate ? 1 : 0);
|
||||
for (uint32_t i = 0; i < hdrLen; i++) printf("%02x", hdr[i]);
|
||||
printf(" ");
|
||||
for (uint32_t i = 0; i < payloadLen; i++) printf("%02x", payload[i]);
|
||||
printf("\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IwxSetKey(const uint8_t* key, uint32_t keyLen, uint8_t keyIdx,
|
||||
bool pairwise, uint8_t cipher, const uint8_t* rsc) {
|
||||
printf("KEY pairwise=%d idx=%u cipher=%u ", pairwise ? 1 : 0, keyIdx, cipher);
|
||||
for (uint32_t i = 0; i < keyLen; i++) printf("%02x", key[i]);
|
||||
printf(" ");
|
||||
if (rsc) for (int i = 0; i < 6; i++) printf("%02x", rsc[i]);
|
||||
printf("\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IwxRemoveKey(uint8_t keyIdx, bool pairwise, uint8_t cipher,
|
||||
uint32_t keyLen) {
|
||||
printf("KEY-REMOVE pairwise=%d idx=%u cipher=%u len=%u\n",
|
||||
pairwise ? 1 : 0, keyIdx, cipher, keyLen);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Sink normally provided by Wifi.cpp.
|
||||
void WifiRxEthernet(const uint8_t* frame, uint32_t len) {
|
||||
puthex("ETH", frame, len);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Driver
|
||||
// =============================================================================
|
||||
|
||||
static int unhex(const std::string& s, uint8_t* out) {
|
||||
int n = 0;
|
||||
for (size_t i = 0; i + 1 < s.size(); i += 2) {
|
||||
unsigned v; sscanf(s.c_str() + i, "%2x", &v); out[n++] = (uint8_t)v;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
int main() {
|
||||
// A firmware state good enough for the MLME: alive, one antenna, a MAC.
|
||||
g_iwx.State = IwxFwState::Running;
|
||||
g_iwx.Fw.PhyConfig = (1u << IWX_FW_PHY_CFG_TX_CHAIN_POS)
|
||||
| (1u << IWX_FW_PHY_CFG_RX_CHAIN_POS);
|
||||
g_iwx.Nvm.ValidTxAnt = 1;
|
||||
g_iwx.Nvm.ValidRxAnt = 1;
|
||||
|
||||
std::string line;
|
||||
uint8_t buf[4096];
|
||||
|
||||
while (std::getline(std::cin, line)) {
|
||||
if (line.rfind("MAC ", 0) == 0) {
|
||||
unhex(line.substr(4), g_iwx.Nvm.HwAddr);
|
||||
printf("DONE\n");
|
||||
} else if (line.rfind("CAPA ", 0) == 0) {
|
||||
unsigned bit, on;
|
||||
sscanf(line.c_str(), "CAPA %u %u", &bit, &on);
|
||||
if (on) g_iwx.Fw.Capa[bit / 8] |= (uint8_t)(1 << (bit % 8));
|
||||
else g_iwx.Fw.Capa[bit / 8] &= (uint8_t)~(1 << (bit % 8));
|
||||
printf("DONE\n");
|
||||
} else if (line.rfind("CONNECT ", 0) == 0) {
|
||||
// CONNECT <bssid> <channel> <is5> <ssid> <pass|-> <rsnie|->
|
||||
char bssid[64], ssid[64], pass[128], rsn[256];
|
||||
unsigned chan, is5;
|
||||
sscanf(line.c_str(), "CONNECT %63s %u %u %63s %127s %255s",
|
||||
bssid, &chan, &is5, ssid, pass, rsn);
|
||||
uint8_t bs[6]; unhex(bssid, bs);
|
||||
uint8_t ie[128]; int ieLen = 0;
|
||||
if (strcmp(rsn, "-") != 0) ieLen = unhex(rsn, ie);
|
||||
bool ok = IwxConnectStart(bs, (uint8_t)chan, is5 != 0, ssid,
|
||||
strcmp(pass, "-") == 0 ? nullptr : pass,
|
||||
ieLen ? ie : nullptr, (uint32_t)ieLen,
|
||||
100, 2);
|
||||
printf("CONNECT-OK %d STATE %d\n", ok ? 1 : 0, IwxConnectState());
|
||||
} else if (line.rfind("RXMGMT ", 0) == 0) {
|
||||
int n = unhex(line.substr(7), buf);
|
||||
IwxConnectRxMgmt(buf, (uint32_t)n);
|
||||
printf("DONE STATE %d\n", IwxConnectState());
|
||||
} else if (line.rfind("RXDATA ", 0) == 0) {
|
||||
int n = unhex(line.substr(7), buf);
|
||||
IwxConnectRxData(buf, (uint32_t)n);
|
||||
printf("DONE STATE %d\n", IwxConnectState());
|
||||
} else if (line.rfind("TXETH ", 0) == 0) {
|
||||
int n = unhex(line.substr(6), buf);
|
||||
bool ok = IwxConnectSendEthernet(buf, (uint32_t)n);
|
||||
printf("TXETH-OK %d\n", ok ? 1 : 0);
|
||||
} else if (line.rfind("SERVICE", 0) == 0) {
|
||||
IwxConnectService();
|
||||
printf("DONE STATE %d LINK %d\n", IwxConnectState(), IwxLinkUp() ? 1 : 0);
|
||||
} else if (line.rfind("TICK ", 0) == 0) {
|
||||
unsigned ms; sscanf(line.c_str(), "TICK %u", &ms);
|
||||
Timekeeping::g_ms += ms;
|
||||
printf("DONE\n");
|
||||
} else if (line.rfind("ABORT", 0) == 0) {
|
||||
IwxConnectAbort();
|
||||
printf("DONE STATE %d\n", IwxConnectState());
|
||||
} else if (line.rfind("STATE", 0) == 0) {
|
||||
printf("STATE %d LINK %d\n", IwxConnectState(), IwxLinkUp() ? 1 : 0);
|
||||
} else if (line.rfind("QUIT", 0) == 0) {
|
||||
break;
|
||||
}
|
||||
fflush(stdout);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Host-side tests for the Wi-Fi crypto and the WPA supplicant.
|
||||
#
|
||||
# These compile the real kernel sources (Libraries/Crypto.cpp and
|
||||
# Drivers/Net/Wifi/Wpa.cpp) for the host against the small shim in shim/, so
|
||||
# what is exercised is the code that ships, not a copy of it.
|
||||
#
|
||||
# Requires: g++ with C++20, python3 with the `cryptography` package.
|
||||
#
|
||||
# Copyright (c) 2026 Daniel Hammer
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
root="$(cd "$here/../.." && pwd)"
|
||||
src="$root/kernel/src"
|
||||
out="$(mktemp -d)"
|
||||
trap 'rm -rf "$out"' EXIT
|
||||
|
||||
CXX="${CXX:-g++}"
|
||||
CXXFLAGS="-O1 -std=c++20 -Wall -I$here/shim -I$src"
|
||||
|
||||
echo "== crypto primitives against published vectors =="
|
||||
$CXX $CXXFLAGS -o "$out/crypto_vectors" \
|
||||
"$here/crypto_vectors.cpp" "$src/Libraries/Crypto.cpp"
|
||||
"$out/crypto_vectors"
|
||||
|
||||
echo
|
||||
echo "== supplicant against an independent authenticator =="
|
||||
$CXX $CXXFLAGS -o "$out/supplicant" \
|
||||
"$here/supplicant_harness.cpp" \
|
||||
"$src/Drivers/Net/Wifi/Wpa.cpp" "$src/Libraries/Crypto.cpp"
|
||||
|
||||
python3 "$here/ap_handshake.py" "$out/supplicant"
|
||||
python3 "$here/ap_rekey_and_rsn.py" "$out/supplicant"
|
||||
|
||||
echo
|
||||
echo "== MLME and data path against a simulated access point =="
|
||||
$CXX $CXXFLAGS -o "$out/mlme" \
|
||||
"$here/mlme_harness.cpp" \
|
||||
"$src/Drivers/Net/Wifi/IwxConnect.cpp" \
|
||||
"$src/Drivers/Net/Wifi/Wpa.cpp" "$src/Libraries/Crypto.cpp"
|
||||
|
||||
python3 "$here/ap_mlme.py" "$out/mlme"
|
||||
|
||||
echo
|
||||
echo "All Wi-Fi host tests passed."
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
namespace kcp {
|
||||
class Spinlock {
|
||||
std::atomic_flag f{ATOMIC_FLAG_INIT};
|
||||
public:
|
||||
void Acquire() { while (f.test_and_set(std::memory_order_acquire)) {} }
|
||||
void Release() { f.clear(std::memory_order_release); }
|
||||
};
|
||||
// The kernel's Mutex is the non-interrupt-disabling variant; on the host
|
||||
// there is nothing to disable, so the two are the same here.
|
||||
class Mutex {
|
||||
std::atomic_flag f{ATOMIC_FLAG_INIT};
|
||||
public:
|
||||
void Acquire() { while (f.test_and_set(std::memory_order_acquire)) {} }
|
||||
void Release() { f.clear(std::memory_order_release); }
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
#pragma once
|
||||
namespace base { struct Manip {}; inline Manip hex, dec; }
|
||||
@@ -0,0 +1,2 @@
|
||||
#pragma once
|
||||
#include <cstring>
|
||||
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
// Only the type name is needed: the MLME never touches PCI.
|
||||
namespace Pci { struct PciDevice { uint8_t Bus, Device, Function; }; }
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
namespace Kt {
|
||||
enum KernelLogLevel { INFO, OK, WARNING, ERROR };
|
||||
class KernelLogStream {
|
||||
public:
|
||||
KernelLogStream(KernelLogLevel l, const char* c) { fprintf(stderr, " [%s] ", c); (void)l; }
|
||||
~KernelLogStream() { fprintf(stderr, "\n"); }
|
||||
KernelLogStream& operator<<(const char* s) { fprintf(stderr, "%s", s); return *this; }
|
||||
KernelLogStream& operator<<(uint64_t v) { fprintf(stderr, "%llu", (unsigned long long)v); return *this; }
|
||||
KernelLogStream& operator<<(int v) { fprintf(stderr, "%d", v); return *this; }
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
namespace Timekeeping { uint64_t GetMilliseconds(); }
|
||||
@@ -0,0 +1,94 @@
|
||||
// Host harness: drives the real kernel supplicant through a 4-way handshake.
|
||||
// Frames come in / go out as hex on stdio so an independent Python AP can be
|
||||
// the oracle.
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
#include "Drivers/Net/Wifi/Wpa.hpp"
|
||||
#include "Drivers/Net/Wifi/Ieee80211.hpp"
|
||||
|
||||
namespace Timekeeping { static uint64_t g_ms = 0; uint64_t GetMilliseconds() { return g_ms; } }
|
||||
|
||||
using namespace Drivers::Net::Wifi;
|
||||
|
||||
static void puthex(const char* tag, const uint8_t* p, size_t n) {
|
||||
printf("%s ", tag);
|
||||
for (size_t i = 0; i < n; i++) printf("%02x", p[i]);
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
// --- hooks the supplicant calls back into ---
|
||||
namespace Drivers::Net::Wifi {
|
||||
bool WpaTxEapol(const uint8_t* body, uint32_t len) {
|
||||
puthex("TX", body, len);
|
||||
return true;
|
||||
}
|
||||
bool WpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher) {
|
||||
printf("PTK-CIPHER %u\n", cipher);
|
||||
puthex("PTK", tk, tkLen);
|
||||
return true;
|
||||
}
|
||||
bool WpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
|
||||
uint8_t cipher, const uint8_t* rsc) {
|
||||
printf("GTK-IDX %u\nGTK-CIPHER %u\n", keyIdx, cipher);
|
||||
puthex("GTK", gtk, gtkLen);
|
||||
puthex("GTK-RSC", rsc, 8);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static int unhex(const std::string& s, uint8_t* out) {
|
||||
int n = 0;
|
||||
for (size_t i = 0; i + 1 < s.size(); i += 2) {
|
||||
unsigned v; sscanf(s.c_str() + i, "%2x", &v); out[n++] = (uint8_t)v;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::string line;
|
||||
uint8_t buf[2048];
|
||||
|
||||
while (std::getline(std::cin, line)) {
|
||||
if (line.rfind("START ", 0) == 0) {
|
||||
// START <ownmac> <bssid> <ssid> <passphrase> <akm> <pcipher> <gcipher>
|
||||
char own[64], bss[64], ssid[64], pass[128];
|
||||
unsigned akm, pc, gc;
|
||||
sscanf(line.c_str(), "START %63s %63s %63s %127s %u %u %u",
|
||||
own, bss, ssid, pass, &akm, &pc, &gc);
|
||||
WpaConfig cfg = {};
|
||||
unhex(own, cfg.OwnMac);
|
||||
unhex(bss, cfg.Bssid);
|
||||
cfg.SsidLen = (uint8_t)strlen(ssid);
|
||||
memcpy(cfg.Ssid, ssid, cfg.SsidLen);
|
||||
cfg.PassLen = (uint8_t)strlen(pass);
|
||||
memcpy(cfg.Passphrase, pass, cfg.PassLen);
|
||||
cfg.Akm = (uint8_t)akm;
|
||||
cfg.PairwiseCipher = (uint8_t)pc;
|
||||
cfg.GroupCipher = (uint8_t)gc;
|
||||
printf("START-OK %d\n", WpaStart(cfg) ? 1 : 0);
|
||||
uint8_t ie[64];
|
||||
uint32_t ieLen = WpaBuildRsnIe(ie, sizeof(ie));
|
||||
puthex("RSNIE", ie, ieLen);
|
||||
} else if (line.rfind("RX ", 0) == 0) {
|
||||
int n = unhex(line.substr(3), buf);
|
||||
bool consumed = WpaOnEapol(buf, (uint32_t)n);
|
||||
printf("RX-OK %d STATE %d\n", consumed ? 1 : 0, (int)WpaGetState());
|
||||
} else if (line.rfind("PARSE ", 0) == 0) {
|
||||
int n = unhex(line.substr(6), buf);
|
||||
WpaConfig cfg = {};
|
||||
bool ok = WpaParseApRsn(buf, (uint32_t)n, cfg);
|
||||
printf("PARSE-OK %d AKM %u PCIPHER %u GCIPHER %u\n", ok ? 1 : 0,
|
||||
(unsigned)cfg.Akm, (unsigned)cfg.PairwiseCipher,
|
||||
(unsigned)cfg.GroupCipher);
|
||||
} else if (line.rfind("STATE", 0) == 0) {
|
||||
printf("STATE %d\n", (int)WpaGetState());
|
||||
} else if (line.rfind("QUIT", 0) == 0) {
|
||||
break;
|
||||
}
|
||||
fflush(stdout);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user