The claim, stated plainly
NØNOS now associates with a WPA2 network, completes the four-way handshake on real silicon, decrypts and encrypts CCMP frames, obtains a DHCP lease, and loads real websites over WiFi on a stock consumer laptop. No Linux underneath. No vendor blob doing the hard parts behind our back. The 802.11 driver, the supplicant, the crypto, the TCP/IP stack, the DHCP client, and the browser are all NØNOS code, and each one runs as an isolated capability-gated capsule.
This is the story of getting there. It is mostly a story about being wrong in specific, instructive ways, which is what driver bring-up actually is.
The shape of the stack
On NØNOS a network card is not owned by the kernel. The RTL8821CE driver runs in ring 3 as a signed capsule. It reaches the hardware only through a broker that mediates PCI configuration and MMIO, and it talks to the rest of the system only through capability-checked IPC. The transport stack, net_core, is a separate capsule with its own DHCP client. The WiFi supplicant logic lives in a chip-independent core so the WPA2 state machine is not tangled up with one vendor's registers.
That separation is the whole point of the system, but it also means a packet crosses several trust boundaries between the antenna and the browser, and every one of them is a place a bring-up can quietly fail. Several of them did.
Getting the radio to admit who it is
The first sign of life was association failing for a reason that made no sense until we sniffed it: the station's own MAC address was all zeros. The driver had never read the address out of the chip. The RTL8821CE keeps its MAC in efuse at offset 0xD0, and it has to be read and programmed into REG_MACID before the MAC engine will behave. We found it the honest way, by putting a second laptop next to it running a packet sniffer and watching frames go out from 00:00:00:00:00:00.
With a real MAC, association proceeded and the four-way handshake began, and then message three of the handshake kept failing its MIC check. The MIC was being computed over four bytes too many. The verification was hashing the frame check sequence along with the EAPOL payload. Bounding the MIC computation to the EAPOL length fixed it, and the handshake completed on real hardware for the first time.
There was a related trap on this particular laptop: the timestamp counter reads dead, so any code that assumed a working TSC for timing had to fall back to poll counts. Consumer hardware is full of small betrayals like this, and a sovereign stack has to survive all of them without a vendor driver to hide behind.
The quirk that nearly stopped everything
The handshake succeeded, the link showed connected, and no data moved. This was the hard one.
The RTL8821CE has a hardware security engine that is supposed to encrypt and decrypt CCMP frames for you. Ours decrypted incoming frames correctly. It refused to produce usable encrypted frames on transmit. After a long time in the register maps and cross-checking against how the mainline rtw88 driver drives the same silicon, the cause became clear: the chip's hardware transmit encryption resolves keys by a default-key search keyed on MACID, and that search only works once the firmware has been told the station's MACID and media-connection status. Our driver was not doing that firmware handshake, so every hardware-encrypted transmit produced garbage the access point dropped.
We had two choices: reverse-engineer and reproduce the exact firmware MACID and media-status sequence, or stop asking the hardware to do the part it was doing wrong. We chose the second, because it is both faster and more honest about what actually works.
The result is a hybrid CCMP mode we call software-transmit, hardware-receive. On transmit the frame takes the plaintext path through the hardware, with security type set to zero, and NØNOS encrypts the CCMP payload itself in software before handing it down, and sets the 802.11 Protected bit on the outgoing frame so the access point treats it as encrypted. On receive we keep the hardware engine, but strictly in receive-only mode: receive decrypt enabled, unicast and broadcast default-key use enabled, and every transmit-side security bit left off so the broken transmit path can never touch a frame we already encrypted.
This is the kind of decision that only a stack you fully own lets you make. We could reach into the exact layer that misbehaves, route around it, and keep the layer that works. A monolithic vendor driver gives you the whole engine or nothing.
Earning an address
With CCMP flowing, the link carried data, and the last mile was DHCP. net_core had to be serving from the moment it booted, because the link comes up opportunistically and there is no cable event to wait for. It binds its interface from the serve loop and runs its own DHCP client against smoltcp's state machine, setting the lease into shared state the moment the interface is configured.
The subtle bug here was in how the driver and net_core talk. NØNOS IPC delivers a request to a service by name, and the server has to reply to the specific caller. Using a first-in-first-out endpoint cross-routed replies to the wrong waiter under load. The fix was to receive with the sender's identity captured and reply to that exact identity, so a lease request and its answer stay correlated even while beacon frames are streaming in. We also bounded the receive drain so a flood of beacons could not starve the request path, and tightened the per-device call timeouts so a slow radio degrades instead of hanging.
Then it bound. The sniffer showed the machine sending unicast frames to the gateway, over a hundred of them in the first exchange, and the DHCP client came up with a real lease. The browser, itself a capsule, opened a socket over net_core and rendered live websites over WiFi.
The last betrayal was one of our own making
There was a final twist that is worth telling because it shows the system working as designed. After everything above was proven, the WiFi settings panel still reported that the network stack was not running, even while the browser was happily loading pages over that same stack.
The panel was not lying, and the stack was not down. The panel asks net_core for the DHCP lease over IPC, and that call was being rejected before it ever reached net_core. NØNOS gates network services behind a capability: to call the DHCP service a capsule must hold both the IPC and the Network capability. The settings capsule had been granted IPC but not Network. The kernel's router did exactly what a capability system is supposed to do, it refused the call and dropped it, and the panel correctly reported no answer.
The fix was to grant the settings capsule the Network capability it legitimately needs, one bit in its manifest, after which the panel reads the lease and shows the address. The lesson is the reassuring one: the boundary held. Nothing crossed it that had not been authorized, and the failure was visible and precise once we made the panel report which layer said no.
What Rust actually bought us
We get asked whether Rust is doing real work in a driver or is just fashionable, so it is worth being concrete, because the parts of this bring-up that hurt least are the parts the language made hard to get wrong.
The chip-specific and chip-independent halves meet at two traits. LinkPort carries frames, and KeyStore installs keys. The 802.11 and WPA2 brains, the supplicant, the MLME, the CCMP default, and the frame encode and decode, live once in a shared crate. The Realtek and Intel drivers implement the two traits over their own rings and never fork the shared logic. That sounds like ordinary interface design until you meet the hardware, because key installation is exactly where chips stop agreeing with each other:
pub trait KeyStore {
/// Install the pairwise temporal key for `peer`. `key_id` is the 802.11 key
/// index; the implementation picks the hardware slot.
fn install_ptk(&mut self, key: &[u8; 16], key_id: u8, peer: &[u8; 6]) -> bool;
fn install_gtk(&mut self, key: &[u8; 16], key_id: u8) -> bool;
fn remove_key(&mut self, key_id: u8, peer: Option<&[u8; 6]>);
}
Some chips want the key stashed so software encrypts each frame. Others want a slot programmed in a hardware security CAM so the radio encrypts. The supplicant derives a key and hands it over, and it never names a hardware slot, because it does not get to know there is one. The implementation chooses.
That seam is what let us survive the RTL8821CE. Its hardware decrypts received frames correctly, but its transmit encryption does not work without a firmware MACID binding, and worse, leaving the engine's transmit crypto enabled corrupts a frame software has already encrypted, so the access point silently drops it. This chip therefore runs half in hardware and half in software: receive through the engine, transmit encrypted by us. The shared supplicant has no idea, and none of that asymmetry leaked upward into code the Intel driver has to read.
The signatures above are doing quiet work too. A temporal key is &[u8; 16] and a station address is &[u8; 6], not a pointer and a length you are trusted to get right. In C both are uint8_t * with a comment, and a wrong length is a runtime corruption you find with a sniffer at two in the morning. Here it is a compile error. The Option<&[u8; 6]> on remove_key is the same idea applied to protocol semantics: a pairwise key names its peer, a group key cannot, and the type says so instead of the documentation saying so.
The crate is #![cfg_attr(not(test), no_std)], a small line with a large payoff. In the capsule it is bare metal with no standard library. Under cargo test on a development machine it is ordinary Rust, so the WPA2 four-way state machine, the EAPOL parsing, and the frame encode and decode are all unit-tested on a laptop against captured bytes with no radio involved. When the handshake failed on real silicon we already knew the state machine was not the liar, which is how we ended up looking at the MAC address instead.
One last thing that is easy to miss: this driver runs in ring 3. Rust removes a class of bugs at compile time, and the capability broker removes the consequences of the ones that survive. A pointer mistake in a kernel driver takes the machine down. Here the worst case is a dead capsule holding no authority it was never granted, and the rest of the system keeps running while you read the log.
What is actually true today
The RTL8821CE path is real and proven end to end on a specific consumer laptop: association, WPA2, CCMP, DHCP, and a browser loading real sites over WiFi. We are honest that this is one chip on one class of hardware, and that other radios are at earlier stages. The value is not that WiFi works in the abstract. It is that when it works, every layer between the antenna and the pixel is NØNOS code, running with least privilege, auditable, and replaceable, and when it breaks, it breaks at a named boundary you can see.
That is the difference a sovereign stack makes. Not that bring-up is easy, it is not, but that when the hardware lies to you, and it will, you own every layer you need to route around the lie.