Exchanging data between Linux user-space applications and kernel modules is a fundamental requirement in system engineering. While character devices (/dev), sysfs, procfs, and ioctl() system calls support basic interactions, Netlink sockets (AF_NETLINK) offer a far more flexible, asynchronous, bi-directional IPC mechanism.

Netlink uses standard BSD socket APIs (socket(), bind(), sendto(), recvmsg()) to pass structured socket buffers (struct sk_buff) between kernel space and user processes. In this article, we build a complete Linux kernel module and matching user-space C application to demonstrate bi-directional Netlink message passing.

  • Asynchronous Kernel-to-User Initiated Alerts: Unlike ioctl() or character device read() loops where user-space must continually poll the kernel, Netlink allows the kernel to spontaneously send packets to user-space applications.

  • Multicast Support: A single kernel event (such as a udev USB insertion or network route change) can be broadcast to multiple listening user processes simultaneously using Netlink multicast groups (nl_groups).

  • Standard Socket Paradigm: Developers can integrate Netlink file descriptors directly into standard event loops (poll(), epoll(), select()).

Part 1: Writing the C User-Space Application

The user-space program opens an AF_NETLINK socket, populates a struct nlmsghdr header containing payload data, sends the packet to the kernel, and waits for a reply:

netlink_user_app.cc
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <linux/netlink.h>
 
#define NETLINK_USER 31
#define MAX_PAYLOAD 1024
 
int main() {
    int sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_USER);
    if (sock_fd < 0) {
        perror("Socket creation failed");
        return -1;
    }
 
    struct sockaddr_nl src_addr, dest_addr;
    memset(&src_addr, 0, sizeof(src_addr));
    src_addr.nl_family = AF_NETLINK;
    src_addr.nl_pid = getpid(); // Unique User Process ID
    src_addr.nl_groups = 0;     // Unicast
 
    if (bind(sock_fd, (struct sockaddr *)&src_addr, sizeof(src_addr)) < 0) {
        perror("Bind failed");
        close(sock_fd);
        return -1;
    }
 
    memset(&dest_addr, 0, sizeof(dest_addr));
    dest_addr.nl_family = AF_NETLINK;
    dest_addr.nl_pid = 0;      // Target Kernel
    dest_addr.nl_groups = 0;   // Unicast
 
    struct nlmsghdr *nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
    memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
    nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
    nlh->nlmsg_pid = getpid();
    nlh->nlmsg_flags = 0;
 
    strcpy((char *)NLMSG_DATA(nlh), "Hello from User-Space!");
 
    struct iovec iov = { .iov_base = (void *)nlh, .iov_len = nlh->nlmsg_len };
    struct msghdr msg = { .msg_name = (void *)&dest_addr, .msg_namelen = sizeof(dest_addr), .msg_iov = &iov, .msg_iovlen = 1 };
 
    printf("Sending message to kernel...
");
    sendmsg(sock_fd, &msg, 0);
 
    /* Read reply from kernel */
    printf("Waiting for kernel response...
");
    recvmsg(sock_fd, &msg, 0);
    printf("Received kernel reply: %s
", (char *)NLMSG_DATA(nlh));
 
    free(nlh);
    close(sock_fd);
    return 0;
}

Part 2: Writing the Linux Kernel Module

The kernel module registers a custom Netlink protocol family (NETLINK_USER = 31) using netlink_kernel_create(). Upon receiving a Netlink socket buffer (sk_buff), it extracts the user payload, formats a response buffer using nlmsg_new(), and returns it to the calling PID via nlmsg_unicast():

netlink_kernel_module.cc
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <net/sock.h>
#include <linux/netlink.h>
#include <linux/skbuff.h>
 
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Lynxbee Systems");
MODULE_DESCRIPTION("Netlink Bi-Directional User-Kernel IPC");
MODULE_VERSION("1.0");
 
#define NETLINK_USER 31
 
static struct sock *nl_sk = NULL;
 
static void nl_recv_msg(struct sk_buff *skb) {
    struct nlmsghdr *nlh = (struct nlmsghdr *)skb->data;
    char *user_msg = (char *)NLMSG_DATA(nlh);
    int pid = nlh->nlmsg_pid; // User process PID
 
    pr_info("Netlink Module: Received '%s' from user PID %d
", user_msg, pid);
 
    /* Prepare reply packet back to user process */
    char *reply_text = "ACK from Linux Kernel";
    int msg_size = strlen(reply_text) + 1;
 
    struct sk_buff *skb_out = nlmsg_new(msg_size, GFP_KERNEL);
    if (!skb_out) {
        pr_err("Netlink Module: Failed to allocate skb_out
");
        return;
    }
 
    struct nlmsghdr *nlh_out = nlmsg_put(skb_out, 0, 0, NLMSG_DONE, msg_size, 0);
    NETLINK_CB(skb_out).dst_group = 0;
    memcpy(nlmsg_data(nlh_out), reply_text, msg_size);
 
    int res = nlmsg_unicast(nl_sk, skb_out, pid);
    if (res < 0) {
        pr_err("Netlink Module: Error sending unicast reply to user PID %d
", pid);
    }
}
 
static int __init netlink_init(void) {
    pr_info("Netlink Module: Initializing Netlink Socket...
");
 
    struct netlink_kernel_cfg cfg = {
        .input = nl_recv_msg,
    };
 
    nl_sk = netlink_kernel_create(&init_net, NETLINK_USER, &cfg);
    if (!nl_sk) {
        pr_err("Netlink Module: Error creating Netlink socket
");
        return -ENOMEM;
    }
 
    return 0;
}
 
static void __exit netlink_exit(void) {
    pr_info("Netlink Module: Unloading Netlink Socket...
");
    if (nl_sk) {
        netlink_kernel_release(nl_sk);
    }
}
 
module_init(netlink_init);
module_exit(netlink_exit);

Compile the kernel module against active kernel headers using a standard Kbuild Makefile:

Makefilemakefile
obj-m += netlink_kernel_module.o
 
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
 
all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules
	gcc -O2 netlink_user_app.c -o netlink_user_app
 
clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean
	rm -f netlink_user_app

Execution sequence in terminal:

Terminal Workflowbash
# 1. Build kernel module and user client
make
 
# 2. Insert kernel module into kernel space
sudo insmod netlink_kernel_module.ko
 
# 3. Run user-space C application
./netlink_user_app
 
# Output:
# Sending message to kernel...
# Waiting for kernel response...
# Received kernel reply: ACK from Linux Kernel
 
# 4. Inspect kernel dmesg log output
dmesg | tail -n 10
 
# 5. Remove kernel module
sudo rmmod netlink_kernel_module
  • `NLMSG_SPACE(len)`: Calculates total memory byte length needed for a Netlink header + payload, padded to 4-byte boundaries.

  • `NLMSG_DATA(nlh)`: Returns a pointer to the start of the payload data payload immediately following struct nlmsghdr.

  • `nlmsg_unicast(sock, skb, pid)`: Transmits the socket buffer (sk_buff) directly to the target user process ID (pid). Memory management of skb is handled automatically upon transmission.