mac80211 Implementation Details
Now, let's shift our focus from the over-the-air protocol interactions back into the kernel. In the previous section, we watched how packets change identities and play hide-and-seek in power-save mode. But you might have had a lingering question: Who manages all this? What does the code actually look like?
The Linux wireless world isn't a free-for-all. It follows a standard set of "building codes" called mac80211.
Whether it's Intel's iwlwifi or Marvell's lbtf, anyone wanting to enter the Linux wireless ecosystem must play by mac80211's rules.
In this section, we're going to pop the hood and look at the pistons and connecting rods inside.
If you don't get your hands dirty with the core bones like ieee80211_hw and sta_info, your understanding of the wireless stack will always feel like looking at flowers through a glass window.
12.6 Core Skeleton: From ieee80211_hw to Driver Callbacks
The mac80211 API is mind-bogglingly complex, packed with dizzying details.
I'm not going to dump tens of megabytes of code here (that would be pointless anyway), but I will give you the most precise treasure map so you know where the entrances are and where the traps lie.
Everything starts with a structure called struct ieee80211_hw.
You can think of it as the "hardware device's ID card."
The kernel uses this structure to tell the wireless stack, "Hey, here's what this NIC looks like and what tricks it can do."
But there's a key design detail here: ieee80211_hw contains a priv pointer.
This isn't just any pointer—it's the driver developer's private stash.
This priv pointer is defined as void *, meaning the kernel doesn't care about or inspect what's stored inside.
Only the driver knows; it typically points to a larger, driver-defined private structure—for example, Intel's driver calls it iwl_priv, while Marvell's calls it lbtf_private.
This design cleanly separates the "generic framework" from the "private implementation."
To get to work, the driver needs to do a few things first:
- Allocate space: Call
ieee80211_alloc_hw(size_t priv_data_len, const struct ieee80211_ops *ops). This step doesn't just allocate theieee80211_hw; it also allocates the amount of private space you requested viapriv_data_lenand binds the two together. - Register the device: Take the allocated
hwand callieee80211_register_hw(struct ieee80211_hw *hw). This step is like registering a household—it officially exists in the kernel from this point on. - Receive data: When the hardware receives a packet, the driver typically calls
ieee80211_rx_irqsafe()(implemented innet/mac80211/rx.c). Why is it calledirqsafe? Because it's called in interrupt context, where sleeping is not allowed.
That architecture diagram you saw in the docs (Figure 12-5) actually depicts this exact relationship:
Between the driver layer and the mac80211 layer, the handshake is connected entirely through the ieee80211_ops set of callback functions.
12.6.1 Driver Operations
The ieee80211_ops mentioned earlier is essentially a "pledge" written by the driver for the kernel.
It's also a structure filled with function pointers. You don't have to fill in every single one, but a few are absolutely critical.
Let's look at a few of the most core callbacks:
tx(): This is the payroll department. Every time the kernel needs to send a packet, it calls this function. Under normal circumstances, it must returnNETDEV_TX_OK, unless something goes wrong.start()andstop(): This pair is responsible for power control.start()activates the hardware and turns on the frame reception switch;stop()shuts it down, typically cutting power to the hardware completely.add_interface()andremove_interface(): These sound abstract, but they're actually quite simple: when youifconfig wlan0 up, this callback gets triggered. The kernel is asking the driver, "Hey, I'm about to bind a virtual network device—is your hardware ready to accept traffic?"remove_interfaceis the reverse operation.config(): This is the tuning knob. For example, if you want to switch channels (from CH6 to CH11), the kernel calls this function to notify the hardware to change frequencies.configure_filter(): This is the bouncer configuration. It tells the hardware, "I only want these specific types of packets; don't bother me with the rest."
12.6.2 Station Management: The sta_info Structure
Besides the hardware itself, what's the most important thing in the wireless world? It's the "people" (Stations).
Whether it's a connected phone or the router next door, they are all represented in the kernel as a struct sta_info (defined in net/mac80211/sta_info.h).
This structure is incredibly rich in content—practically a black box for a station:
- Statistical counters: How many packets were received, how many were sent, how many were dropped.
- Flags: Is it in power-save mode? Is it authorized?
- ps_tx_buf: Remember the power-save buffer we talked about in the previous section? This is that array, used to store unicast packets for sleeping stations.
- debugfs entries: Allowing us to peek at its state from user space.
How does the kernel manage hundreds or thousands of such stations?
Using a hash table (sta_hash) and linked lists (sta_list).
When you need to find a station, you typically use these three tools:
sta_info_insert(struct sta_info *sta): Add a new station to the table.sta_info_destroy_addr(...): Kick someone out. Remove a station by its MAC address (internally, this calls__sta_info_destroy).sta_info_get(...): Look up a record. Given a MAC address (usually the BSSID), fish out thesta_infostructure for that station.
12.6.3 RX Path: When a Packet Flies In
The receive path is the busiest place in the entire stack.
The main character is the ieee80211_rx() function (in net/mac80211/rx.c).
When the driver passes an SKB up, it doesn't come empty-handed. It slips a little note into the SKB's control buffer called ieee80211_rx_status.
You can use the IEEE80211_SKB_RXCB() macro to pull out this note and read it.
What's written on it? It describes the reception quality of the packet: Did the FCS check fail? What's the signal strength?
If the flag field carries RX_FLAG_FAILED_FCS_CRC, congratulations—this packet is trash.
Next comes a series of pipeline operations:
-
Monitor mode handling:
ieee80211_rx_monitor()takes the stage. If you have Monitor Mode enabled (for packet capturing), it will first strip off the FCS (checksum) for you and handle any potentially presentradiotapheader. Note: Not all NICs support Monitor Mode. -
802.11n reordering: If you're using HT (802.11n), out-of-order packets need to be queued up. At this point,
ieee80211_rx_reorder_ampdu()gets called in to sort the out-of-order building blocks. -
The actual processing:
__ieee80211_rx_handle_packet()ultimately callsieee80211_invoke_rx_handlers().
Here is a very elegant design pattern known as the "Chain of Responsibility." The kernel has a chain of processors, and each one takes a look at the packet:
- If it says "I don't handle this, next," it returns
RX_CONTINUE. - If it says "I'm taking this packet," it returns
RX_QUEUED. - If it says "This packet is garbage," it returns
RX_DROP_MONITORorRX_DROP_UNUSABLE.
Let's look at a piece of real code (net/mac80211/rx.c):
static ieee80211_rx_result
ieee80211_rx_h_mgmt_check(struct ieee80211_rx_data *rx)
{
struct ieee80211_mgmt *mgmt = (struct ieee80211_mgmt *) rx->skb->data;
struct ieee80211_rx_status *status = IEEE80211_SKB_RXCB(rx->skb);
. . .
if (rx->skb->len < 24)
return RX_DROP_MONITOR;
if (!ieee80211_is_mgmt(mgmt->frame_control))
return RX_DROP_MONITOR;
. . .
}
What is this code doing?
It's checking: if this packet claims to be a management frame but isn't even 24 bytes long (the minimum 802.11 header length), or if it's a fake management frame, it immediately returns RX_DROP_MONITOR—tossed into the trash.
If it's a PS-Poll packet but the receiver isn't acting as an AP, that's also RX_DROP_UNUSABLE.
This layered filtering design ensures the kernel's CPU won't be maxed out by malformed packets.
12.6.4 TX Path: When a Packet Flies Out
The main character of the transmit path is ieee80211_tx() (in net/mac80211/tx.c).
Its logic is very similar to RX: prepare first, then go through the pipeline.
-
Preparation:
__ieee80211_tx_prepare(). Various pre-checks happen here. For example, if you're trying to send a packet smaller than 10 bytes, the kernel will think you're joking and immediatelydev_kfree_skb(skb)drop it. -
Transmit processor chain:
invoke_tx_handlers(). Just like RX, this is a row of processing functions strung together using theCALL_TXHmacro.TX_CONTINUE: Not my problem, pass to the next.TX_QUEUED: I've taken over this packet (e.g., it entered a buffer).TX_DROP: This packet has issues, do not transmit.
If it gets a green light all the way, it finally calls __ieee80211_tx() to push the packet down to the hardware driver's tx() callback.
12.6.5 Fragmentation: Fitting an Elephant into a Fridge
Wireless is fast nowadays, but in the past, to ensure reliability, even elephants had to be chopped up. This is fragmentation.
The 802.11 standard dictates that every station has a fragmentation threshold.
If a unicast packet exceeds this threshold, it must be sliced into smaller pieces.
Each slice requires a separate acknowledgment (ACK).
You can view this threshold from the command line using iwconfig, or change it:
iwconfig wlan0 frag 512
This sets the threshold to 512 bytes.
The smaller the threshold, the lower the collision probability, but the higher the overhead.
On the transmit side, the knife that does the chopping is the ieee80211_tx_h_fragment() method.
On the receive side, the magic tool that reassembles the meat is the ieee80211_rx_h_defragment() method.
But there's a massive gotcha here: Fragmentation and A-MPDU aggregation (an 802.11n technique used to boost speed) are fundamentally incompatible. Since modern 802.11n/ac speeds are so fast and airtime is so short, fragmentation is rarely seen anymore. If you enable fragmentation on a modern high-rate network, you might find that the network actually slows down, or throughput plummets because aggregation is disabled.
12.6.6 debugfs: The Kernel's Black Box
The kernel doesn't want you left guessing what it's thinking, so it left a backdoor called debugfs.
This is a virtual filesystem dedicated to spitting out debug information.
First, you need to mount it:
mount -t debugfs none_debugs /sys/kernel/debug
⚠️ Warning
Your kernel must have been compiled with CONFIG_DEBUG_FS enabled, otherwise this step will throw an error and you won't be able to see any of the content below.
Assuming your physical device is phy0, you can go digging for treasure under /sys/kernel/debug/ieee80211/phy0.
Here are a few particularly useful entries:
-
total_ps_buffered: This is the AP's "storage locker." It shows how many packets the AP has buffered for sleeping stations (PS mode), including both unicast and multicast. This number is the sum added up byieee80211_tx_h_unicast_ps_bufandieee80211_tx_h_multicast_ps_buf. -
statisticsdirectory: This is full of battle stats.frame_duplicate_count: How many duplicate packets were received (someone is retransmitting).transmitted_frame_count: How many packets were successfully transmitted.retry_count: How many retries occurred. If this number spikes, your signal quality is poor or there's heavy interference.fragmentation_threshold: The current fragmentation threshold.
-
netdev:wlan0directory (assuming your interface is called wlan0): Here you can see specific interface information.aid: Association ID.assoc_tries: How many attempts it took to connect.bssid: The address of the router you're connected to.
-
rc/name: This shows the name of the rate control algorithm currently in use. Choosing the right algorithm is more important than choosing the right router.
12.6.7 Wireless Modes: The NIC's ID Card
Finally, a NIC doesn't have just one form.
Depending on what you want to do, you can switch it into different modes.
This depends on whether the hardware supports it (you can check the wiphy field in interface_modes, or look up your driver list on linuxwireless.org).
- AP mode (
NL80211_IFTYPE_AP): The NIC becomes a router. It maintains a station list, and the network name (SSID) is defined by it. - Managed mode (
NL80211_IFTYPE_STATION): The NIC becomes a client. This is the mode your phone uses to connect to WiFi. - Monitor mode (
NL80211_IFTYPE_MONITOR): Sniffer mode. All packets passing through the air, whether addressed to it or not, are captured indiscriminately. This is the dependency for packet capture tools like airodump-ng. Transmitting in this mode is called injection, and the data packets are tagged with a specialIEEE80211_TX_CTL_INJECTEDmark. - Ad Hoc mode (
NL80211_IFTYPE_ADHOC): Point-to-point, no AP, everyone is equal. - WDS mode (
NL80211_IFTYPE_WDS): Wireless Distribution System, used for bridging. - Mesh mode (
NL80211_IFTYPE_MESH_POINT): Mesh networking, which we'll cover in detail in the next section.
At this point, we've finished looking at the skeleton and muscles of mac80211.
But in today's wireless world, it's not just about connectivity—it's about speed.
In the next section, we'll enter the 802.11n expressway and see how multiple antennas (MIMO) and aggregation technologies turn wireless networks into fiber optics.
The logic in that world is an entirely different beast.