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 also serves as a starting point for a robust Linux Wi-Fi driver. Once you create the simplest driver, you can add extra features to it and customize it to meet your needs.
In this article, we show you how to write drivers for Linux and achieve basic functionality by implementing 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 developers 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:
Creating init and exit functions
Before we start Linux driver development, letโs briefly overview three types of wireless driver configurations in Linux:
- 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.
In this example, we create a dummy FullMAC driver based on cfg80211 that only supports STA mode. We decided to call our sample Linux device driver โnavifly.โ
Writing a dummy Wi-Fi driver involves four major steps and starts with creating init
and exit
functions, which are required by every driver.
We use the init
function to allocate the context for our driver. The exit
function, in turn, is used to clean it out.
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;
}
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);
}
Outside of navifly_create_context()
, we initialize work_structs
and variables such as g_ctx->connecting_ssid
and g_ctx->scan_request
to show the basic functionality of Linux Wi-Fi drivers. However, a full-value customized driver may not include these functions and may require more complex and flexible structures in its context.
Letโs take a look at the context for our simple driver:
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;
};
Now letโs move to creating and initializing the context.
Looking to enhance your Linux system?
Letโs discuss how we can develop custom driver solutions to optimize your software performance!
Creating and initializing the context
The wiphy
structure describes a physical wireless device. You can list all your physical wireless devices with the iw list
command.
The ndev
command shows network devices. There should be at least one wireless device in your network. When implemented, the FullMAC driver can support several virtual network interfaces. The net_device
structure together with the wireless_dev
structure represents a wireless network device.
In our demo, the wireless_dev
structure is stored in the private data of net_device
:
struct navifly_ndev_priv_context {
struct navifly_context *navi;
struct wireless_dev wdev;
};
Also, the ndev
private context stores a pointer to the navifly
context. Each net_device
should have its own wireless_dev
context if the device is wireless. Other fields of the navifly_context
structure for our prototype are described later in this article.
Letโs now create the navifly
context. To do that, we need to allocate and register the wiphy
structure first.
Letโs take a look at the navifly_create_context()
function:
static struct navifly_context *navifly_create_context(void) {
struct navifly_context *ret = NULL;
struct navifly_wiphy_priv_context *wiphy_data = NULL;
struct navifly_ndev_priv_context *ndev_data = NULL;
Hereโs how to allocate memory for the navifly_context
structure:
/* allocate for navifly context*/
ret = kmalloc(sizeof(*ret), GFP_KERNEL);
if (!ret) {
goto l_error;
}
The next step is to initialize the context by allocating a new wiphy
structure. Functions like wiphy_new()
require the cfg80211_ops
structure. This structure has a lot of functions that, when implemented, represent features of the wireless device.
The next argument of the wiphy_new()
function is the size of the private data that will be allocated with the wiphy
structure. Also, the wiphy_new_nm()
function allows us to set the device name. By default, itโs phy%d(phy0, phy1 etc)
, but in our driver example we used the name navifly
.
ret->wiphy = wiphy_new_nm(&nvf_cfg_ops, sizeof(struct navifly_wiphy_priv_context), WIPHY_NAME);
if (ret->wiphy == NULL) {
goto l_error_wiphy;
}
Letโs initialize the private data of the wiphy
structure and set the navifly
context so we can get the navifly_context
parameter out of the wiphy
structure:
wiphy_data = wiphy_get_navi_context(ret->wiphy);
wiphy_data->navi = ret;
The following code sets modes that our device can support. It can support several modes, which can be set using the bitwise OR operators. Our demo supports only STA mode.
ret->wiphy->interface_modes = BIT(NL80211_IFTYPE_STATION);
The code below sets supported bands and channels. The nf_band_2ghz
structure only describes channel 6. (We picked this channel randomly from the list of WLAN channels for demo purposes.)
ret->wiphy->bands[NL80211_BAND_2GHZ] = &nf_band_2ghz;
The following code is needed to set a value if the device supports scan requests. This value represents the maximum number of SSIDs the device can scan.
ret->wiphy->max_scan_ssids = 69;
Next, we use wiphy_register
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. The wiphy
weโve created has no network interface yet. However, we can already call functions that donโt require a network interface, such as the iw phy phy0 set
channel function.
if (wiphy_register(ret->wiphy) < 0) {
goto l_error_wiphy_register;
}
At this point, weโre done allocating the wiphy
context and we move further to allocating the net_device
context.
To set the Ethernet device, 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 a function that will be called during allocation. In most cases, using the default ether_setup
function 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;
}
Next, we initialize the private data of the network device and set the navifly
context pointer and the wireless_dev
structure. But first, we need to set up the wiphy
structure and the net_device
pointers for the wireless_dev
structure. Thanks to setting up the ieee80211_ptr
pointer for the net_device
structure, the system recognizes that the current net_device
is wireless.
ndev_data = ndev_get_navi_context(ret->ndev);
ndev_data->navi = ret;
ndev_data->wdev.wiphy = ret->wiphy;
ndev_data->wdev.netdev = ret->ndev;
ndev_data->wdev.iftype = NL80211_IFTYPE_STATION;
ret->ndev->ieee80211_ptr = &ndev_data->wdev;
Now we set up functions for net_device
. The net_device_ops nvf_ndev_ops
structure should implement at least the ndo_start_xmit()
function. This function is called when a packet should be transmitted to the network. In our demo, the ndo_start_xmit()
function does nothing but free the packet memory to avoid a memory leak.
ret->ndev->netdev_ops = &nvf_ndev_ops;
The next step is registering a network device:
if (register_netdev(ret->ndev)) {
goto l_error_ndev_register;
}
If everything goes well, you may list it with the command ip a.
Donโt forget to clean up everything 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. This 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.
Read also
Hooking Linux Kernel Functions, Part 1: Looking for the Perfect Solution
Discover how Linux function hooking can elevate your product’s functionality and foster adaptability! Dive into our guide for actionable insights and position your product for success!
Setting up a scanning function
If a user requests a scan, the scanning function will be called from the cfg80211_ops nvf_cfg_ops
. If the nvf_cfg_ops
doesnโt have a scanning function, the user will get an error such as โoperation is not supported.โ Our demo only has the nvf_scan
function, which should initiate a scan routine and return 0 if everything is okay.
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. It specifies whether the scan request is active, is set for specific channels, etc. However, we ignore that for now, as weโre working on the simplest possible Wi-Fi driver.
A semaphore is required for synchronized access to navi->scan_request.
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;
}
Our sample driver only imitates the work of the scanning function. To inform the kernel about new basic service sets (BSS), we need to use the cfg80211_inform_bss_data()
or cfg80211_inform_bss()
function, both of which can be called inside the inform_dummy_bss()
function.
The cfg80211_inform_bss*()
function can be called outside of the scan routine if scanning wasnโt requested or planned. 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.
A semaphore is required for synchronized access to navi->scan_request
.
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);
}
At this point, we have to inform the system about the dummy BSS so it can prepare a hardcoded response. The cfg80211_inform_bss
data structure contains information about basic service sets: channels, signal strength, etc.
The information element (ie
) is an element that 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.
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);
}
Related project
USB WiFi Driver Development
Learn how the Apriorit team ported an existing Linux driver to Windows, saving our clients time and money, as well as enhancing their product’s performance and reliability!
Implementing the connect and disconnect operations
Connect and disconnect operations should be implemented together; otherwise, the wiphy_register()
function will fail. When a user decides to connect to a network, a connect function from the cfg80211_ops
structure is called. In our demo, itโs the nvf_connect()
function.
The connect function should return 0 if everything is okay. The function has to initiate the connect routine, which should be completed with the following calls:
cfg80211_connect_bss()
cfg80211_connect_result()
cfg80211_connect_done()
cfg80211_connect_timeout()
After that, the navifly_connect_routine()
function is executed. In our prototype, this function just saves the SSID and starts the work of the ws_connect
function.
Usually, the cfg80211_connect_params
structure also contains other information about the connection that workable drivers should look for. However, using our sample, we will only take a look at the SSID.
A semaphore is required for synchronized access to navi->connecting_ssid
.
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;
}
The navifly_connect_routine()
function imitates a connection to the Wi-Fi network. In our demo, it checks whether the SSID is โMyAwesomeWiFiโ. If itโs not, 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.
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.
A semaphore is required for synchronized access to navi->connecting_ssid
.
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 in 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. It should start the disconnect routine (in our demo, it works with the navifly_disconnect_routine()
function). The routine will be terminated with the cfg80211_disconnected()
function if the connection is interrupted.
A semaphore is required for synchronized access to navi->disconnect_reason_code
.
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;
}
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, such as when the connection is dropped.
A semaphore is required for synchronized access to the navi->disconnect_reason_code
.
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, the work on our dummy FullMAC Linux Wi-Fi driver prototype is done.
The Linux driver tutorial presented in this article shows you how to write dummy drivers that can be improved and customized. Hereโs a list of useful resources that may help you create more advanced solutions:
- Linux 802.11 Driver Developerโs Guide
ath6kl
, a great example of a workable FullMAC drivervirt_wifi
, an interesting virtual driver that can be used as a wrapper around Ethernet- An example of the Broadcom FullMAC WLAN driver
- An example of the driver for RNDIS, based on wireless USB devices
Read also
Most Common Embedded Linux System Project Estimation Issues
Improve estimation accuracy for your embedded Linux project and get it delivered on time and within budget! Explore the common pitfalls, best practices, and strategies to overcome estimation hurdles laid out in our guide.
Conclusion
In this article, we showed you how to write a Linux driver for Wi-Fi that can be implemented with minimum configurations. You can access the full code of our sample driver from our Apriorit GitHub profile.
Sure, a full-value device driver should implement a device context (PCI, USB, platform). To create useful Linux wireless drivers, we have to define the context: set up the hardware address and implement cfg80211_ops
and net_device_ops
functions. Also, itโs better to add another interface mode for wiphy, such as access point mode.
At Apriorit, we have developers skilled at Linux kernel and driver, as well as network management, who are ready to help you implement a project of any complexity.
Looking for a dedicated driver development team?
Harness our unique expertise in specialized driver development to strengthen your product and unlock its full potential!