Logo
blank Skip to main content

Linux Wi-Fi Driver Tutorial: How to Write a Simple Linux Wireless Driver Prototype

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.

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.
wireless drivers in Linux

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.

main steps to design simple linux wireless driver

We use the init function to allocate the context for our driver. The exit function, in turn, is used to clean it out.

C
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:

C
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:

C
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:

C
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:

C
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:

C
/* 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.

C
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:

C
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.

C
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.)

C
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.

C
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.

C
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.

C
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.

C
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.

C
ret->ndev->netdev_ops = &nvf_ndev_ops;

The next step is registering a network device:

C
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:

C
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!

Learn more

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.

C
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.

C
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.

C
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!   

Project details
USB WiFi Driver Development

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.

C
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.

C
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.

C
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.

C
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:

  1. Linux 802.11 Driver Developer’s Guide
  2. ath6kl, a great example of a workable FullMAC driver
  3. virt_wifi, an interesting virtual driver that can be used as a wrapper around Ethernet
  4. An example of the Broadcom FullMAC WLAN driver
  5. 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.

Learn more

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 a team of experienced Linux kernel and driver developers 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!

Tell us about your project

Send us a request for proposal! We’ll get back to you with details and estimations.

By clicking Send you give consent to processing your data

Book an Exploratory Call

Do not have any specific task for us in mind but our skills seem interesting?

Get a quick Apriorit intro to better understand our team capabilities.

Book time slot

Contact us