Key takeaways:
- Your team might require a custom Linux Wi-Fi driver if off-the-shelf solutions are unable to cover all project requirements.
- If your project involves working with custom hardware, building a tailored driver solution is essential to establish proper compatibility and performance.
- Driver development hides many pitfalls, even when building a simple solution with limited functionality. This Linux Wi-Fi developer guide will prepare your team to face them.
- Developing a Linux Wi-Fi driver dummy will give your team a foundation that you can grow into a full-scale product by expanding its functionality.
If you work with projects related to device and hardware manufacturing, embedded systems, or kernel and OS tasks, you are likely to use Linux Wi-Fi drivers as part of your routine.
But sometimes, your team might find themselves in need of a custom driver.
There are two common reasons for this:
- Custom hardware. Say you’re creating a new device from scratch (e.g. for an IoT, robotics, or automotive project) and are planning to run it on Linux. In this case, you’ll need a Wi-Fi driver to enable reliable connectivity. Off-the-shelf drivers may not support your custom hardware design, but a tailored driver can ensure proper integration and performance.
- Limitations of existing drivers. Sometimes, standard drivers don’t meet project requirements. You might need a driver that’s more efficient, more stable, or equipped with additional features like a built-in firewall. In such cases, creating a custom driver allows you to extend functionality and fine-tune performance.
In this Linux Wi-Fi driver tutorial, we show you how to create a simple driver with basic functionality and a minimal interface. Our driver will be able to scan for a dummy Wi-Fi network, connect to it, and disconnect from it.
This article will be useful for development and project leaders who are already familiar with device drivers and know how to create kernel modules but want to learn how to design a customizable FullMAC Linux Wi-Fi driver prototype.
Contents:
Preparing for Linux driver development
Previously, we explored how to create a simple Linux device driver. Now, it’s time to talk about how to create a simple Linux Wi-Fi driver.
A dummy Wi-Fi driver is usually created solely for educational purposes. However, such a driver can serve as a starting point for a fully-fledged Linux Wi-Fi driver. Once you create a simple driver, you can add extra features and customize it to meet your needs.
Wireless driver development usually involves four steps:

Read also
Linux Driver Development with Rust: Benefits, Challenges, and a Practical Example
Explore how Rust can bring safety and reliability to Linux driver development without sacrificing performance.

Before we start developing our driver, let’s briefly overview three types of wireless driver configurations in Linux that will be mentioned further in this article:
- cfg80211 — The configuration API for 802.11 devices in Linux. It works together with FullMAC drivers, which also should implement the MAC Sublayer Management Entity (MLME).
- mac80211 — A subsystem of the Linux kernel that works with soft-MAC/half-MAC wireless devices. MLME is mostly implemented by the kernel, at least for station mode (STA).
- WEXT — Stands for Wireless-Extensions, which is a driver API that was replaced by cfg80211. New drivers should no longer implement WEXT.

For demonstration purposes, in this article, we’ll create a dummy FullMAC driver based on cfg80211 that only supports station (STA) mode. Let’s call our sample Linux device driver “navifly.”
First, we need to create the navifly_context structure. This structure is essential to keep all context of our driver in one place and simplify its management:
struct navifly_context {
struct wiphy *wiphy;
struct net_device *ndev;
struct semaphore sem;
struct work_struct ws_connect;
char connecting_ssid[sizeof(SSID_DUMMY)];
struct work_struct ws_disconnect;
u16 disconnect_reason_code;
struct work_struct ws_scan;
struct cfg80211_scan_request *scan_request;
};As you can see, the navifly_context structure contains the following:
struct wiphy * wiphyー a structure defined in the mac80211/cfg80211 subsystem that represents a wireless device like a Wi-Fi chipset in the Linux kernelstruct net_device *ndevー a universal structure for representing network interfaces in the Linux kernelsemaphore semー a semaphore for synchronizing access to the structure fieldscfg80211_scan_request *ws_scanー a Linux kernel structure defined in the mac80211/cfg80211 subsystem that represents a request to scan Wi-Fi networks and contains all scan parameters- A few
work_structstructures ー responsible for connection, scanning, and disconnection operations - Other driver status or internal state information like network SSID, disconnection code, etc.
Now, we can proceed to the first step: creating init and exit functions, which are required by every driver.
1. Creating init and exit functions
The init function will initialize our driver and allocate its context. Here’s how to create it:
static int __init virtual_wifi_init(void) {
g_ctx = navifly_create_context();
if (g_ctx != NULL) {
sema_init(&g_ctx->sem, 1);
INIT_WORK(&g_ctx->ws_connect, navifly_connect_routine);
g_ctx->connecting_ssid[0] = 0;
INIT_WORK(&g_ctx->ws_disconnect, navifly_disconnect_routine);
g_ctx->disconnect_reason_code = 0;
INIT_WORK(&g_ctx->ws_scan, navifly_scan_routine);
g_ctx->scan_request = NULL;
}
return g_ctx == NULL;
}To demonstrate basic Wi-Fi driver functionality, we’ve split the context initialization process into two blocks:
- The first block is extracted to the
navifly_create_contextfunction. It contains functionality that’s directly related to Wi-Fi context initialization. We’ll implement this function in Step 2. - The second block is located directly in the
initfunction. It contains the initialization ofwork_structswith corresponding functions and the initialization of othernavifly_contextvariables with default values.
The exit function cleans out the driver’s context. Here’s the code for our exit function:
static void __exit virtual_wifi_exit(void) {
cancel_work_sync(&g_ctx->ws_connect);
cancel_work_sync(&g_ctx->ws_disconnect);
cancel_work_sync(&g_ctx->ws_scan);
navifly_free(g_ctx);
}Note: A full-value customized driver may not include these functions and may require more complex and flexible structures in its context.
Now, let’s move to creating and initializing the context by implementing the navifly_create_context function.
Need a custom Linux driver?
Let Apriorit’s experienced engineers develop a custom driver solution to enhance your Linux project!
2. Creating and initializing the context
Before implementing the navifly_create_context function, we must create additional structures: navifly_wiphy_priv_context and navifly_ndev_priv_context. Here’s the code for both:
struct navifly_wiphy_priv_context {
struct navifly_context *navi;
};
struct navifly_ndev_priv_context {
struct navifly_context *navi;
struct wireless_dev wdev;
};These structures will provide all context for Wi-Fi device callbacks via a special field in the net_device and wiphy structures:
- The
wiphystructure describes a physical wireless device. - The
net_devicestructure together with thewireless_devstructure represent a wireless network device. - Each
net_deviceshould have onewireless_devstructure that associatesnet_devicewithwiphy.
Now, we’re ready to implement the navifly_create_context() function. Below, we show four key actions to do this successfully:
Allocate memory
Let’s start with navifly_context structure allocation:
static struct navifly_context *navifly_create_context(void) {
struct navifly_context *ret = NULL;
/* allocate for navifly context*/
ret = kmalloc(sizeof(*ret), GFP_KERNEL);
if (!ret) {
goto l_error;
}Initialize the context
Now, we need to allocate, initialize, and register the wiphy device, represented by the wiphy structure. We’ll do that using two functions: wiphy_new_nm() and wiphy_register.
The wiphy_new_nm() function requires:
- The
cfg80211_opsstructure (with a lot of callbacks for different wireless device operations). - The size of the private data that will be allocated with the
wiphystructure. - The device name — this is
phy%d(phy0,phy1, etc.) by default, but you can change it. In our example, we will use navifly.
The wiphy_register function is used to register the wiphy structure in the system. If the wiphy structure is valid, a new device can be listed: for example, using the iw list command.
Here’s the final code:
ret->wiphy = wiphy_new_nm(&nvf_cfg_ops, sizeof(struct navifly_wiphy_priv_context), WIPHY_NAME);
if (ret->wiphy == NULL) {
goto l_error_wiphy;
}
/* wiphy structure initialization */
if (wiphy_register(ret->wiphy) < 0) {
goto l_error_wiphy_register;
}Searching for a trusted Linux driver development partner?
At Apriorit, we design, build, and optimize custom Linux drivers for diverse hardware, helping you meet your performance, security, and compatibility needs.
Create a network interface
The wiphy structure we’ve created has no network interface yet. However, we can already call functions that don’t require a network interface, like the iw phy phy0 set channel function.
To create a network interface for wiphy, we need to perform almost the same actions as with our net device: allocation, initialization, and registration:
- The
alloc_netdev()function takes the size of the private data, the name of the network device, and the value that describes the name origin. - The last argument of the
alloc_netdev()function is another function that will be called during allocation. - In most cases, using the default
ether_setupfunction is enough.
ret->ndev = alloc_netdev(sizeof(*ndev_data), NDEV_NAME, NET_NAME_ENUM, ether_setup);
if (ret->ndev == NULL) {
goto l_error_alloc_ndev;
}
/* net_device structure initialization */
if (register_netdev(ret->ndev)) {
goto l_error_ndev_register;
}If everything goes well, your new network interface will be listed when you run the ip a command.
Note: For simplicity, we have omitted some routine device initialization code in our snippet. Keep in mind that in addition to the code shown in the listing, you’ll also need to:
- Create
navifly_wiphy_priv_contextandnavifly_ndev_priv_contextstructures. - Initialize
wiphyandnet_devicestructures fornavifly_wiphy_priv_contextandnavifly_ndev_priv_context. - Set modes that will be supported by the device. In our case, it’s
NL80211_IFTYPE_STATION. - Set bands and channels that will be supported by the device. We used the 2.4 GHz band and randomly picked channel 6.
- Set the maximum number of SSIDs the device can scan. We used a randomly chosen 69.
- Implement any other required initializations.
Clean up
Don’t forget to clean up everything using this code to avoid a resource leak:
static void navifly_free(struct navifly_context *ctx) {
if (ctx == NULL) {
return;
}
unregister_netdev(ctx->ndev);
free_netdev(ctx->ndev);
wiphy_unregister(ctx->wiphy);
wiphy_free(ctx->wiphy);
kfree(ctx);
}The navifly_free() function, which is called when the driver is unloaded from the system, completely cleans out the context and frees the memory. Thus, if you remove the kernel module, the virtual device will disappear.
At this point, we have a proper context.
Now, let’s take a look at the callbacks for the wiphy structure implemented in the nvf_cfg_ops structure. The wiphy structure may not have any functions at all, making the device unusable. However, we want to show more possibilities. Therefore, for our driver prototype, we implement dummy variants of scan, connect, and disconnect functions in the nvf_cfg_ops structure.
Related project
USB Wi-Fi Driver Development
Learn how the Apriorit team ported an existing Linux driver to Windows, saving our client’s time and money as well as enhancing their product’s performance and reliability!
3. Setting up a scanning function
If a user requests a scan, the scanning function will be called from cfg80211_ops nvf_cfg_ops structures. If the nvf_cfg_ops structure doesn’t have a scanning function, the user will get an error saying that the operation isn’t supported.
Our demo only has the nvf_scan function, which should initiate a scan routine and return 0 if everything is okay:
static int nvf_scan(struct wiphy *wiphy, struct cfg80211_scan_request *request) {
struct navifly_context *navi = wiphy_get_navi_context(wiphy)->navi;
if(down_interruptible(&navi->sem)) {
return -ERESTARTSYS;
}
if (navi->scan_request != NULL) {
up(&navi->sem);
return -EBUSY;
}
navi->scan_request = request;
up(&navi->sem);
if (!schedule_work(&navi->ws_scan)) {
return -EBUSY;
}
return 0;
}
static void navifly_scan_routine(struct work_struct *w) {
struct navifly_context *navi = container_of(w, struct navifly_context, ws_scan);
struct cfg80211_scan_info info = {
.aborted = false,
};
msleep(100);
inform_dummy_bss(navi);
if(down_interruptible(&navi->sem)) {
return;
}
cfg80211_scan_done(navi->scan_request, &info);
navi->scan_request = NULL;
up(&navi->sem);
}
static void inform_dummy_bss(struct navifly_context *navi) {
struct cfg80211_bss *bss = NULL;
struct cfg80211_inform_bss data = {
.chan = &navi->wiphy->bands[NL80211_BAND_2GHZ]->channels[0],
.scan_width = NL80211_BSS_CHAN_WIDTH_20,
.signal = 1337,
};
char bssid[6] = {0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff};
char ie[SSID_DUMMY_SIZE + 2] = {WLAN_EID_SSID, SSID_DUMMY_SIZE};
memcpy(ie + 2, SSID_DUMMY, SSID_DUMMY_SIZE);
bss = cfg80211_inform_bss_data(navi->wiphy, &data, CFG80211_BSS_FTYPE_UNKNOWN, bssid, 0, WLAN_CAPABILITY_ESS, 100,
ie, sizeof(ie), GFP_KERNEL);
cfg80211_put_bss(navi->wiphy, bss);
}In our sample, we save the request and run ws_scan, which executes the navifly_scan_routine() function.
The request argument has a useful field that describes a scan request: whether it is active, what channels it is set for, etc. However, we will ignore that for now, as our driver only imitates the scan function. For the same reason, we’ve implemented the inform_dummy_bss function to notify the system with our hardcoded dummy (BSS). The cfg80211_inform_bss_data function is used specifically for system notification from the inform_dummy_bss function.
When scanning is done, we need to call the cfg80211_scan_done() function with a request for context and information that describes the results of the scan routine. If scanning was aborted for any reason — due to hardware, a driver management routine, or a user request (for this, we need to implement the abort_scan function into the cfg80211_ops structure) — the .aborted function should be set to true.
The information element (ie) can be taken from the Wi-Fi management frame. It’s also a parameter for some functions. In our demo, the information element packs only the “MyAwesomeWiFi” SSID. Then it calls the cfg80211_inform_bss_data() function, which returns the cfg80211_bss pointer. This pointer represents the BSS known to the system. We should apply the put method to this BSS if it’s no longer used; otherwise, it may lead to a memory leak.
Read also
How to Secure and Manage File Access with SELinux
Strengthen your Linux security by learning how SELinux policies help manage file access and prevent vulnerabilities.

4. Implementing connect and disconnect operations
Connect and disconnect operations should be implemented together; otherwise, the wiphy_register() function will fail. When a user connects to a network, a connect function from the cfg80211_ops structure is called. In our demo, it’s nvf_connect().
The connect function should return 0 if everything is okay and the connect routine is scheduled. The connect routine should be completed with one (and only one) of the following calls:
cfg80211_connect_result()ー should be called once execution of the connection request fromconnect()has been completed.cfg80211_connect_bss()ーis similar tocfg80211_connect_result(), but with the option of identifying the exact BSS entry for the connection.cfg80211_connect_done()ー is similar tocfg80211_connect_bss(), but takes a structure pointer for connection response parameters.cfg80211_connect_timeout()ー should be called ifconnect()has failed in a sequence where no explicit authentication/association rejection was received from the access point.
In our prototype, the navifly_connect_routine() function only checks whether the SSID is “MyAwesomeWiFi” and starts the work of the ws_connect function:
- If the SSID is not “MyAwesomeWiFi”, the driver informs the kernel that it didn’t find the requested SSID.
- Otherwise, the driver informs the kernel that the connection has been successfully established.
Note: Usually, the cfg80211_connect_params structure also contains more information about the connection that workable drivers should look for. However, in our example, we will only take a look at the SSID.
Before calling the cfg80211_connect_bss() function, we have to inform the system that we have already scanned for dummy BSS connection options. The informing step can be skipped, but in this case, the kernel will send a warning message.
static int nvf_connect(struct wiphy *wiphy, struct net_device *dev,
struct cfg80211_connect_params *sme) {
struct navifly_context *navi = wiphy_get_navi_context(wiphy)->navi;
size_t ssid_len = sme->ssid_len > 15 ? 15 : sme->ssid_len;
if(sme->ssid == NULL || sme->ssid_len == 0) {
return -EBUSY;
}
if(down_interruptible(&navi->sem)) {
return -ERESTARTSYS;
}
memcpy(navi->connecting_ssid, sme->ssid, ssid_len);
navi->connecting_ssid[ssid_len] = 0;
up(&navi->sem);
if (!schedule_work(&navi->ws_connect)) {
return -EBUSY;
}
return 0;
}
static void navifly_connect_routine(struct work_struct *w) {
struct navifly_context *navi = container_of(w, struct navifly_context, ws_connect);
if(down_interruptible(&navi->sem)) {
return;
}
if (memcmp(navi->connecting_ssid, SSID_DUMMY, sizeof(SSID_DUMMY)) != 0) {
cfg80211_connect_timeout(navi->ndev, NULL, NULL, 0, GFP_KERNEL, NL80211_TIMEOUT_SCAN);
} else {
inform_dummy_bss(navi);
cfg80211_connect_bss(navi->ndev, NULL, NULL, NULL, 0, NULL, 0, WLAN_STATUS_SUCCESS, GFP_KERNEL,
NL80211_TIMEOUT_UNSPECIFIED);
}
navi->connecting_ssid[0] = 0;
up(&navi->sem);
}The disconnect operation works the same way. In our demo, the nvf_disconnect() function is responsible for disconnecting from the Wi-Fi network, and it returns 0 if everything is okay and nvf_disconnect_routine() is scheduled. The routine will be terminated with the cfg80211_disconnected() function if the connection is interrupted.
While a full-value Linux WLAN driver should implement the interruption of the connection routine, we skipped it in our demo. Technically, the cfg80211_disconnected() function can be called at any time when the wiphy context is connected.
static int nvf_disconnect(struct wiphy *wiphy, struct net_device *dev,
u16 reason_code) {
struct navifly_context *navi = wiphy_get_navi_context(wiphy)->navi;
if(down_interruptible(&navi->sem)) {
return -ERESTARTSYS;
}
navi->disconnect_reason_code = reason_code;
up(&navi->sem);
if (!schedule_work(&navi->ws_disconnect)) {
return -EBUSY;
}
return 0;
}
static void navifly_disconnect_routine(struct work_struct *w) {
struct navifly_context *navi = container_of(w, struct navifly_context, ws_disconnect);
if(down_interruptible(&navi->sem)) {
return;
}
cfg80211_disconnected(navi->ndev, navi->disconnect_reason_code, NULL, 0, true, GFP_KERNEL);
navi->disconnect_reason_code = 0;
up(&navi->sem);
}Now that the connect and disconnect operations are implemented, work on our dummy FullMAC Linux Wi-Fi driver prototype is done. You can access the full code of this sample driver from Apriorit’s GitHub page.
We showed you how to write a Linux driver for Wi-Fi that can be implemented with minimum configurations but can be improved and customized later. Sure, a full-value device driver should implement a device context (PCI, USB, platform). To create a useful Linux wireless driver, we have to define the context: set up the hardware address and implement cfg80211_ops and net_device_ops functions. Also, it’s best to add another interface mode for wiphy, like wireless access point mode.
Conclusion
As you can see, even building a Linux Wi-Fi driver dummy involves many unobvious details. But once your team has a grasp of how to develop a simple solution with limited functionality, you will be well on your way to creating a full-scale product.
With 20+ years of experience in kernel and driver development and network management, Apriorit is ready to help you deliver secure and efficient solutions tailored to your hardware and project needs.
From device driver and kernel-level development to legacy driver modernization and driver migration, our engineers assist with nearly any task within established deadlines. At your request, we can:
- Develop custom driver solutions for blocking USB devices and for other security purposes
- Deliver software products that fit custom hardware, such as drivers for VR headsets
- Port USB Wi-Fi drivers across different operating systems and build custom APIs to help you maximize functionality
- And more
Looking for a skilled driver development team?
Harness our unique expertise to deliver the exact driver solution you envision, checking all the boxes to meet your business goals and technical requirements.
FAQ
Why build a custom Linux Wi-Fi driver?
A common reason for custom wireless driver development is to establish connectivity with custom hardware, as many off-the-shelf drivers likely will not support your custom hardware design. Another reason can be limitations of existing drivers that don’t allow them to fulfill all requirements of a certain project.
What are the main challenges of Linux Wi-Fi driver development?
The biggest challenge is that the majority of firmware is closed for proprietary devices, which limits access to low-level functionality. Another common difficulty, not just for Wi-Fi but for all Linux drivers, is kernel interface changes. Such changes require driver modifications as well if you’re planning to support several kernels.
Should I develop a Linux Wi-Fi driver from scratch or adapt an existing one?
In our experience, most projects start by adapting an existing driver, since this saves time. Writing a driver from scratch only makes sense when you don’t have a suitable base driver to modify and when you have full access to the hardware specifications and documentation.
What’s the difference between cfg80211, nl80211, and mac80211, and when should I use them?
<ul class=apriorit-list-markers-green>
<li><code>nl80211</code> is an interface between the Linux kernel and userspace that’s used for all Wi-Fi drivers.</li>
<li><code>cfg80211</code> is the configuration API that must be implemented in each Linux driver.</li>
<li><code>mac80211</code> is a Linux kernel subsystem used in Wi-Fi drivers for soft-MAC (half-MAC) devices.</li>
</ul>
How do I debug issues with Wi-Fi drivers (e.g., connection drops, low throughput, kernel panics)?
We suggest using the same tools as for debugging all other Linux drivers: dmesg, debugfs, tracepoints, and kgdb.
What tools can I use to test and validate my Wi-Fi driver?
In our experience, the most efficient tools to test and validate a Wi-Fi driver for Linux are tcpdump and iperf, as they allow for traffic analysis, performance testing, and load testing.
How do firmware and drivers interact for Wi-Fi microcontrollers?
A driver usually operates on a host and facilitates communication between a processor and a Wi-Fi microcontroller via the PCI bus. The firmware runs on the microcontroller itself and processes commands from the driver.


