Learn how to write a network driver in Linux from scratch. This beginner-friendly guide covers kernel modules, net_device, sk_buff, ndo operations, and everything you need to build your first Linux network driver.
If you’ve ever wondered what happens between the moment your application sends data and the moment it actually leaves your NIC (network interface card), you’re asking the right question. The answer lives inside the Linux kernel, specifically inside a network driver in Linux.
Writing one from scratch sounds scary. It’s not. It’s actually one of the most satisfying things you can do as a Linux developer once you understand the moving parts. So let’s walk through it together, step by step, the way a senior kernel developer would explain it to you over coffee.
What Is a Network Driver in Linux, Really?
A Linux network driver is a piece of kernel code that acts as the translator between the Linux networking stack and your actual hardware (or virtual hardware). Think of it as a contract: the kernel says “I need you to send this data,” and your driver figures out how to talk to the hardware to make that happen.
Unlike a userspace program, a Linux driver lives inside the kernel. That means it runs with full hardware access, no memory protection between itself and the kernel, and absolutely zero tolerance for bugs. One NULL pointer dereference and the whole system panics.
That’s part of what makes Linux kernel module programming exciting, and also why understanding the fundamentals before writing a single line is worth your time.
Types of Devices in the Linux Kernel
Before writing anything, you should understand where network drivers sit in the kernel hierarchy.
The Linux kernel organizes drivers into three main categories:
Character devices handle data as a stream of bytes, like a serial port or a keyboard. Block devices deal with fixed-size blocks of data, like hard drives. Network devices are completely different from both.
Network devices don’t have a corresponding file in /dev. You don’t open() or read() them like character devices. Instead, the Linux networking subsystem talks to them through a well-defined set of operations registered via a structure called net_device. That’s the core abstraction you need to understand for Linux network interface driver development.
The Big Picture: How Linux Network Driver Development Works
Here’s the high-level flow before we write a single line of code:
- You write a kernel module that registers itself with the networking subsystem.
- That module allocates and configures a
net_devicestructure. - You fill in the network device operations (the
net_device_opsstruct) that tell the kernel how to use your driver. - The kernel’s networking stack calls those operations whenever it needs to send or receive data.
- On receive, your driver hands a packet up to the kernel using an
sk_buff(socket buffer). - On transmit, the kernel hands your driver an
sk_buffand says “send this.”
That’s the whole model. Everything else is implementation details.
Setting Up Your Linux Driver Development Environment
You’ll need the right tools before you start writing your Linux network driver example. Here’s what to set up on a typical Ubuntu or Debian system:
sudo apt update
sudo apt install build-essential linux-headers-$(uname -r) git
The linux-headers package gives you the kernel header files you need to compile kernel modules. Always match them to your running kernel version using uname -r.
Create a working directory:
mkdir ~/mynetdrv && cd ~/mynetdrv
You’ll need two files to get started: your driver source file (let’s call it mynetdrv.c) and a Makefile.
Writing Your First Linux Kernel Module
Every kernel module starts with two functions: module_init() and module_exit(). These are the entry and exit points, similar to main() in a regular C program but for kernel space.
Here’s the skeleton:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#include <linux/skbuff.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple virtual network driver");
static int __init mynetdrv_init(void)
{
printk(KERN_INFO "mynetdrv: loaded\n");
return 0;
}
static void __exit mynetdrv_exit(void)
{
printk(KERN_INFO "mynetdrv: unloaded\n");
}
module_init(mynetdrv_init);
module_exit(mynetdrv_exit);
This compiles and loads cleanly but does nothing useful yet. Let’s add the actual network device.
Understanding the net_device Structure
The net_device structure is the heart of Linux network driver development. It’s a massive struct defined in <linux/netdevice.h> that holds everything the kernel needs to know about your network interface: its name, its MAC address, its MTU, its statistics, and most importantly, a pointer to your net_device_ops.
You don’t allocate net_device directly with kmalloc. You use alloc_netdev() or, for Ethernet specifically, alloc_etherdev(). These functions allocate the structure properly and set up sensible defaults.
static struct net_device *mydev;
mydev = alloc_etherdev(sizeof(struct mynetdrv_priv));
if (!mydev) {
printk(KERN_ERR "mynetdrv: alloc_etherdev failed\n");
return -ENOMEM;
}
The sizeof(struct mynetdrv_priv) argument lets you piggyback your own private data right after the net_device in memory. You retrieve it later using netdev_priv(dev).
Defining Your Private Driver Data
Almost every real-world driver needs to track its own state, things like hardware registers, spin locks, statistics counters, or DMA buffers. This is where your private structure comes in:
struct mynetdrv_priv {
struct net_device_stats stats;
spinlock_t lock;
/* Add hardware-specific fields here */
};
You initialize this after alloc_etherdev():
struct mynetdrv_priv *priv = netdev_priv(mydev);
memset(priv, 0, sizeof(struct mynetdrv_priv));
spin_lock_init(&priv->lock);
Filling in the net_device_ops
This is where you define what your driver can actually do. The net_device_ops structure contains function pointers for every operation the kernel might call on your device. For a minimal network driver in Linux, you need at least these:
static const struct net_device_ops mynetdrv_ops = {
.ndo_open = mynetdrv_open,
.ndo_stop = mynetdrv_stop,
.ndo_start_xmit = mynetdrv_tx,
.ndo_get_stats = mynetdrv_stats,
};
Let’s implement each one.
Implementing ndo_open and ndo_stop
ndo_open is called when someone runs ifconfig mydev0 up or ip link set mydev0 up. This is where you allocate hardware resources, enable interrupts, and tell the kernel the device is ready to transmit.
static int mynetdrv_open(struct net_device *dev)
{
/* In a real driver: enable hardware, request IRQ, start DMA */
netif_start_queue(dev);
printk(KERN_INFO "mynetdrv: interface opened\n");
return 0;
}
netif_start_queue() tells the networking subsystem that this device is ready to accept outgoing packets. Without this call, the kernel won’t pass anything to your ndo_start_xmit.
ndo_stop is the reverse. It’s called when the interface goes down:
static int mynetdrv_stop(struct net_device *dev)
{
netif_stop_queue(dev);
/* In a real driver: disable hardware, free IRQ, stop DMA */
printk(KERN_INFO "mynetdrv: interface stopped\n");
return 0;
}
The Most Important Function: ndo_start_xmit
If there’s one function in Linux network driver development you need to truly understand, it’s ndo_start_xmit. This is what the kernel calls every time it wants to send a packet through your interface.
The function receives a pointer to an sk_buff (socket buffer) and the net_device. Your job is to take that packet, do whatever hardware-specific magic is needed to transmit it, and return a status code.
static netdev_tx_t mynetdrv_tx(struct sk_buff *skb, struct net_device *dev)
{
struct mynetdrv_priv *priv = netdev_priv(dev);
int len = skb->len;
/* For a loopback/virtual driver: just receive the packet back */
/* In a real driver: write to hardware TX ring, trigger DMA */
priv->stats.tx_packets++;
priv->stats.tx_bytes += len;
/* Free the socket buffer when done */
dev_kfree_skb(skb);
return NETDEV_TX_OK;
}
For a virtual or loopback driver, you can simulate reception by calling netif_rx() with a copy of the packet. For a real hardware driver, this is where you’d write to your hardware’s transmit ring buffer and fire off the DMA engine.
Understanding sk_buff: The Linux Socket Buffer
The sk_buff structure (socket buffer) is how the Linux networking stack passes packets between layers. It’s one of the most important data structures in the entire Linux networking subsystem. Understanding it is non-negotiable for serious Linux kernel networking work.
Key fields you’ll use:
skb->data— pointer to the start of the packet dataskb->len— total length of the packetskb->headandskb->end— boundaries of the bufferskb->next/skb->prev— for when packets are queued
When receiving a packet from hardware, you allocate a new sk_buff using dev_alloc_skb(), copy your packet data in, and hand it to the kernel with netif_rx() or napi_gro_receive().
/* Simulated receive path */
static void mynetdrv_rx(struct net_device *dev, int len, unsigned char *buf)
{
struct sk_buff *skb;
struct mynetdrv_priv *priv = netdev_priv(dev);
skb = dev_alloc_skb(len + 2);
if (!skb) {
priv->stats.rx_dropped++;
return;
}
skb_reserve(skb, 2); /* align IP header on 16-byte boundary */
memcpy(skb_put(skb, len), buf, len);
skb->dev = dev;
skb->protocol = eth_type_trans(skb, dev);
skb->ip_summed = CHECKSUM_UNNECESSARY;
priv->stats.rx_packets++;
priv->stats.rx_bytes += len;
netif_rx(skb);
}
Getting Statistics Back to the Kernel
The ndo_get_stats function is simple but important. This is what populates the output when you run ifconfig or ip -s link:
static struct net_device_stats *mynetdrv_stats(struct net_device *dev)
{
struct mynetdrv_priv *priv = netdev_priv(dev);
return &priv->stats;
}
Always keep your TX/RX packet and byte counters accurate. Tools like ethtool, iftop, and monitoring systems like Prometheus depend on these numbers being right.
Registering and Unregistering the Device
Once all your operations are set up, you wire everything together in module_init and register the device with the kernel using register_netdev():
static int __init mynetdrv_init(void)
{
int ret;
mydev = alloc_etherdev(sizeof(struct mynetdrv_priv));
if (!mydev)
return -ENOMEM;
/* Set a fake MAC address */
eth_hw_addr_random(mydev);
mydev->netdev_ops = &mynetdrv_ops;
strncpy(mydev->name, "mydev%d", IFNAMSIZ);
ret = register_netdev(mydev);
if (ret) {
printk(KERN_ERR "mynetdrv: register_netdev failed: %d\n", ret);
free_netdev(mydev);
return ret;
}
printk(KERN_INFO "mynetdrv: registered as %s\n", mydev->name);
return 0;
}
static void __exit mynetdrv_exit(void)
{
unregister_netdev(mydev);
free_netdev(mydev);
printk(KERN_INFO "mynetdrv: unregistered\n");
}
register_netdev() is what makes your driver visible to the system. After this call succeeds, you’ll see the interface when you run ip link show.
Writing the Makefile
Compiling a Linux kernel module requires a specific Makefile format that hands control off to the kernel build system (kbuild):
obj-m += mynetdrv.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
Build and load it:
make
sudo insmod mynetdrv.ko
dmesg | tail -10
ip link show
You should see your new interface mydev0 appear in the output of ip link show. To remove it:
sudo rmmod mynetdrv
Interrupt Handling in Real Hardware Drivers
A virtual driver gets away without interrupts, but a real Linux device driver development project requires an interrupt handler. When hardware finishes sending or receives a new packet, it fires an interrupt to tell the CPU.
You request an IRQ line in ndo_open:
ret = request_irq(dev->irq, mynetdrv_interrupt, IRQF_SHARED, dev->name, dev);
And release it in ndo_stop:
free_irq(dev->irq, dev);
Your interrupt handler processes the event and schedules NAPI polling (more on that below) or directly calls netif_rx() if you’re on a simple non-NAPI design.
NAPI: The Modern Way to Handle Receive in Linux Drivers
If you’re writing a high-performance Linux network interface driver, you’ll want to use NAPI (New API). NAPI is a hybrid interrupt/polling mechanism that dramatically reduces interrupt overhead under high network load.
Here’s how it works: when a packet arrives, your interrupt handler disables further RX interrupts and schedules a NAPI poll. The kernel then calls your poll function in a softirq context to drain as many packets as possible from the hardware ring buffer. When the ring is empty, you re-enable interrupts.
/* In your private struct */
struct napi_struct napi;
/* During init */
netif_napi_add(dev, &priv->napi, mynetdrv_poll, 64);
/* In interrupt handler */
napi_schedule(&priv->napi);
/* Your poll function */
static int mynetdrv_poll(struct napi_struct *napi, int budget)
{
struct mynetdrv_priv *priv = container_of(napi, struct mynetdrv_priv, napi);
int work_done = 0;
while (work_done < budget && /* packets available */) {
/* process one packet */
work_done++;
}
if (work_done < budget) {
napi_complete_done(napi, work_done);
/* re-enable hardware RX interrupts */
}
return work_done;
}
NAPI is used by virtually every production-quality Linux network driver today, from Intel’s igb to Broadcom’s bnxt_en.
Debugging Your Linux Network Driver
Debugging kernel code is different from debugging userspace apps. You can’t attach GDB directly (well, you can with KGDB but it’s complex). Your main tools are:
printk / pr_info / netdev_info — Use these liberally during development. netdev_info(dev, "message\n") is preferred because it automatically prefixes your message with the interface name.
dmesg — This is where your printk messages land. Use dmesg -w to watch in real time.
dynamic debug — The kernel has a powerful mechanism to enable/disable debug messages at runtime without recompiling. Add pr_debug() calls and enable them with:
echo "module mynetdrv +p" > /sys/kernel/debug/dynamic_debug/control
QEMU + KVM — Seriously, do all your early development inside a VM. Crashing a virtual machine is painless. Crashing your workstation mid-session is not.
addr2line — When you get an Oops in dmesg, the stack trace contains hex addresses. addr2line turns those back into file names and line numbers in your source.
Common Mistakes Beginners Make
Forgetting to free the sk_buff. If your transmit path doesn’t call dev_kfree_skb(skb), you leak memory on every sent packet. The system will eventually OOM.
Not calling netif_start_queue. Your ndo_start_xmit will never be called if you forget this in ndo_open.
Sleeping in interrupt context. You cannot call any function that might sleep (like kmalloc with GFP_KERNEL, mutex_lock, etc.) from an interrupt handler or softirq. Use GFP_ATOMIC for allocations in those contexts.
Incorrect locking. The networking subsystem makes calls to your driver from multiple contexts. Use spin locks (not mutexes) for protecting shared state in paths that can run in interrupt context.
Not handling errors. alloc_etherdev, register_netdev, request_irq — all of these can fail. Always check return values and unwind cleanly on failure.
Real-World Linux Network Driver Examples to Study
Once you have the basics down, the best way to level up is reading production drivers in the kernel source. Start with these:
drivers/net/loopback.c — The Linux loopback driver. Tiny, clean, and a perfect reference for virtual devices.
drivers/net/virtio_net.c — The VirtIO network driver used in QEMU/KVM virtual machines. Great for understanding the full NAPI receive path.
drivers/net/ethernet/intel/e1000/ — Intel’s classic Gigabit Ethernet driver. Older but very well documented and readable.
drivers/net/tun.c — The TUN/TAP driver used by VPNs and virtual networks. Shows how to bridge kernel and userspace cleanly.
You can browse all of these at https://elixir.bootlin.com/linux/latest/source, which is the cross-referenced Linux kernel source. It’s an invaluable resource for Linux kernel module programming.
Where to Go From Here
Writing a network driver in Linux gives you a perspective on the system that most developers never get. You stop thinking about the network as a black box and start seeing it for what it is: a chain of well-defined interfaces, from your physical NIC all the way up to your application’s socket.
Once you’re comfortable with the basics covered here, the natural next steps are:
- Learn ethtool support so users can query driver settings from userspace
- Add netpoll support for use with network console and net dumping
- Study XDP (eXpress Data Path) for high-performance packet processing that bypasses the normal networking stack
- Understand device tree bindings if you’re writing drivers for embedded ARM boards
- Look into PCIe driver initialization using
pci_register_driver()for real hardware drivers
The Linux kernel documentation at https://www.kernel.org/doc/html/latest/networking/index.html is solid and worth bookmarking. The book Linux Device Drivers by Corbet, Rubini, and Kroah-Hartman (available free at lwn.net) is the canonical reference.
Summary
Here’s everything we covered in one place:
- A network driver in Linux connects the kernel’s networking stack to your hardware
- Use
alloc_etherdev()to allocate anet_device, not rawkmalloc - Fill in
net_device_opswith at minimumndo_open,ndo_stop,ndo_start_xmit, andndo_get_stats sk_buff(socket buffer) is how packets move through the kernel — know its key fields- Use
register_netdev()to make your interface visible to the system - For real hardware, request an IRQ in
ndo_openand use NAPI for receive processing - Study
loopback.candvirtio_net.cas clean reference implementations - Always develop inside a VM to keep your main system safe
The Linux networking stack is one of the best-designed subsystems in the entire kernel. Once you’ve written a driver that registers an interface, sends and receives real packets, and handles errors cleanly, you’ll have a deep appreciation for why Linux powers everything from smartphones to the largest data centers in the world.
Go build something.
FAQ: How to Write a Network Driver in Linux
Q1. What is a network driver in Linux?
A network driver in Linux is a piece of kernel code that sits between the Linux networking stack and your physical or virtual hardware. It translates high-level kernel requests like “send this packet” into hardware-specific instructions. Unlike character or block device drivers, network drivers don’t appear as files in /dev. Instead they register a net_device structure with the kernel and expose a set of operations the networking subsystem calls directly.
Q2. Do I need to know C to write a Linux network driver?
Yes. Linux kernel development is done entirely in C, specifically a subset of C that avoids certain features like floating point and standard library functions. You don’t need to be a C expert to get started, but you should be comfortable with pointers, structs, function pointers, and memory management. If you can write a linked list implementation in C from scratch, you have enough foundation to follow along.
Q3. What is the net_device structure in Linux?
The net_device structure is the central data structure for any Linux network interface driver. It holds everything the kernel needs to know about your network interface including its name, MAC address, MTU, flags, and a pointer to your net_device_ops. You never allocate it directly with malloc or kmalloc. Instead you use alloc_etherdev() for Ethernet devices or alloc_netdev() for other types. After filling it in, you register it with register_netdev().
Q4. What is sk_buff in Linux and why does it matter?
sk_buff stands for socket buffer. It is the data structure the Linux networking stack uses to pass packets between layers, from your application all the way down to the driver and back up again. When you receive a packet from hardware, you allocate an sk_buff, copy the data in, set the protocol, and hand it to the kernel via netif_rx(). When the kernel wants to send a packet, it passes you an sk_buff in your ndo_start_xmit function. Understanding skb->data, skb->len, skb_put(), and skb_reserve() is essential for any real network driver work.
Q5. What is ndo_start_xmit and how does it work?
ndo_start_xmit is the transmit function in your net_device_ops. The kernel calls it every time it wants your driver to send a packet. It receives a pointer to an sk_buff containing the packet data and a pointer to the net_device. Your job is to take that data, push it to the hardware transmit ring or buffer, update your TX statistics, free the sk_buff with dev_kfree_skb(), and return NETDEV_TX_OK. If your hardware buffer is full you can return NETDEV_TX_BUSY, but you must also call netif_stop_queue() before returning and wake the queue back up with netif_wake_queue() once the hardware has room again.
Q6. What is the difference between netif_rx and napi_gro_receive?
netif_rx() is the older, simpler way to pass a received packet up to the kernel. You call it directly from your interrupt handler or any context where you have a ready sk_buff. It works fine for low-traffic or virtual drivers. napi_gro_receive() is the modern approach used with NAPI. GRO stands for Generic Receive Offload. It allows the kernel to coalesce multiple small TCP segments into fewer larger ones before passing them up the stack, which reduces CPU overhead significantly at high packet rates. For production hardware drivers, napi_gro_receive() is the right choice.
Q7. What is NAPI and should I use it in my driver?
NAPI stands for New API. It is a hybrid interrupt-driven and polling mechanism designed to improve network performance under high load. Without NAPI, every incoming packet triggers a hardware interrupt. At high packet rates this causes interrupt storms that can saturate the CPU. With NAPI, your interrupt handler fires once to signal that packets are available, then disables further RX interrupts and schedules a polling function. The kernel calls your poll function in a softirq context to drain as many packets as possible up to a configurable budget. When the ring is empty you re-enable interrupts. For any driver that will handle real traffic loads, yes, you should use NAPI.
Q8. How do I compile a Linux kernel module?
You need the linux-headers package matching your running kernel, which you get with sudo apt install linux-headers-$(uname -r) on Debian-based systems. Your Makefile should use the kbuild system like this: obj-m += yourdriver.o and then call make with the -C flag pointing at /lib/modules/$(shell uname -r)/build. Run make to build, sudo insmod yourdriver.ko to load, and sudo rmmod yourdriver to unload. Use dmesg to see your printk output.
Q9. How do I debug a Linux network driver?
Your main tool is printk or the preferred wrapper netdev_info(dev, “message”). All output goes to the kernel ring buffer which you read with dmesg or dmesg -w for live output. For more control, use pr_debug() calls combined with the dynamic debug system, which lets you enable and disable specific debug messages at runtime without recompiling. For serious development, run everything inside a QEMU virtual machine so kernel panics don’t affect your host system. When you do get an Oops, the stack trace in dmesg contains hex addresses you can decode back to source lines using addr2line against your compiled module.
Q10. Can I write a network driver without real hardware?
Yes, and it is actually the best way to learn. You can write a fully functional virtual network driver that creates a real interface visible to tools like ip link and ifconfig, sends and receives packets, and maintains proper statistics, all without touching any hardware registers. The Linux loopback driver in drivers/net/loopback.c is a great example of this. The TUN/TAP driver in drivers/net/tun.c is another one. These are legitimate kernel drivers used in production and they work entirely in software.
Q11. What is register_netdev and when do I call it?
register_netdev() is the function that makes your network interface visible to the rest of the Linux system. You call it after you have allocated your net_device with alloc_etherdev(), set the netdev_ops pointer, configured the interface name, and set the MAC address. Once register_netdev() returns successfully, the interface will appear in ip link show and users can bring it up. You must call unregister_netdev() in your cleanup path before calling free_netdev(), otherwise you will leak the device or cause a kernel warning.
Q12. What is the difference between alloc_netdev and alloc_etherdev?
alloc_etherdev() is just a convenience wrapper around alloc_netdev() that additionally sets up Ethernet-specific defaults like the header operations, the hardware header length, and the address length. If you are writing an Ethernet driver, use alloc_etherdev(). If you are writing a non-Ethernet driver such as a point-to-point link or a tunnel, use alloc_netdev() directly and configure the type-specific fields yourself.
Q13. How do I handle interrupts in a network driver?
In your ndo_open function, call request_irq() with your interrupt number, your handler function, the IRQF_SHARED flag if needed, your driver name, and a pointer to your net_device as the dev_id. Your interrupt handler should be fast. It should determine if the interrupt was from your hardware, acknowledge the interrupt to clear it, and then either directly call netif_rx() for simple designs or schedule NAPI polling for production designs. Release the IRQ in your ndo_stop function using free_irq().
Q14. What are the best Linux network driver examples to study?
Start with drivers/net/loopback.c because it is tiny and clean. Then look at drivers/net/virtio_net.c which is a full NAPI-based virtual Ethernet driver used in QEMU and KVM virtual machines. For real hardware, the Intel e1000 driver in drivers/net/ethernet/intel/e1000/ is well documented and widely studied. The TUN driver at drivers/net/tun.c is excellent for understanding userspace-kernel bridging. You can browse all of these online at elixir.bootlin.com with full cross-referencing.
Q15. Is it safe to develop kernel drivers on my main machine?
No, not when you are learning. A bug in kernel code can panic the entire system instantly. Always use a virtual machine for development. QEMU with KVM works great and you can snapshot your VM state before loading any new module. Once your driver is stable and well-tested, then you can consider running it on real hardware. Many developers keep a dedicated test machine for that purpose.
Next Steps: Check out our guides on SPI driver development, platform device drivers, and writing device tree overlays on embeddedprep.com.
Mr. Raj Kumar is a highly experienced Technical Content Engineer with 7 years of dedicated expertise in the intricate field of embedded systems. At Embedded Prep, Raj is at the forefront of creating and curating high-quality technical content designed to educate and empower aspiring and seasoned professionals in the embedded domain.
Throughout his career, Raj has honed a unique skill set that bridges the gap between deep technical understanding and effective communication. His work encompasses a wide range of educational materials, including in-depth tutorials, practical guides, course modules, and insightful articles focused on embedded hardware and software solutions. He possesses a strong grasp of embedded architectures, microcontrollers, real-time operating systems (RTOS), firmware development, and various communication protocols relevant to the embedded industry.
Raj is adept at collaborating closely with subject matter experts, engineers, and instructional designers to ensure the accuracy, completeness, and pedagogical effectiveness of the content. His meticulous attention to detail and commitment to clarity are instrumental in transforming complex embedded concepts into easily digestible and engaging learning experiences. At Embedded Prep, he plays a crucial role in building a robust knowledge base that helps learners master the complexities of embedded technologies.







