Understanding Netlink: Linux IPC Mechanism, Data Structures, APIs and Development Guide
Netlink is a Linux‑specific inter‑process communication mechanism that provides asynchronous, full‑duplex, and multicast communication between user space and kernel space, offering advantages over traditional IPC methods and serving a wide range of networking, monitoring, and system‑management scenarios.
In the vast Linux world, inter‑process communication (IPC) is a core topic, and Netlink acts as a powerful IPC mechanism that connects user‑space processes with the kernel, offering asynchronous, full‑duplex and multicast capabilities.
1. What is Netlink?
Netlink sockets are a special type of IPC used primarily for communication between user space and kernel space. They are Linux‑specific sockets (similar to BSD's AF_ROUTE) that support bidirectional data transfer and are widely used for network configuration, routing management, and device driver interaction.
Traditional IPC methods such as pipes, message queues, and shared memory have limitations: pipes are half‑duplex and only work between related processes; message queues have lower throughput; shared memory requires complex synchronization.
2. User‑Space Data Structures
Applications use the standard socket API (socket(), bind(), sendmsg(), recvmsg(), close()). The address structure is struct sockaddr_nl :
struct sockaddr_nl {
__kernel_sa_family_t nl_family; /* AF_NETLINK */
unsigned short nl_pad; /* zero */
__u32 nl_pid; /* port ID */
__u32 nl_groups; /* multicast groups mask */
};The Netlink message header is struct nlmsghdr :
struct nlmsghdr {
__u32 nlmsg_len; /* Length including header */
__u16 nlmsg_type; /* Message type */
__u16 nlmsg_flags; /* Additional flags */
__u32 nlmsg_seq; /* Sequence number */
__u32 nlmsg_pid; /* Sending process PID */
};Common message type constants:
#define NLMSG_NOOP 0x1
#define NLMSG_ERROR 0x2
#define NLMSG_DONE 0x3
#define NLMSG_OVERRUN 0x4
#define NLMSG_MIN_TYPE 0x10Common flag constants (e.g., request, multi‑part, ack, echo, root, match, atomic, replace, excl, create, append) are defined in linux/netlink.h .
Creating a Netlink socket:
int fd = socket(AF_NETLINK, SOCK_RAW, netlink_type);Binding the socket:
bind(fd, (struct sockaddr *)&nladdr, sizeof(struct sockaddr_nl));Sending a message with sendmsg() requires filling a struct msghdr that references the destination address, the Netlink header and an iovec array.
struct msghdr msg;
memset(&msg, 0, sizeof(msg));
msg.msg_name = (void *)&nladdr;
msg.msg_namelen = sizeof(nladdr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;Receiving uses recvmsg() after allocating a buffer large enough for the header and payload.
3. Netlink Kernel Data Structures
The kernel side also uses struct nlmsghdr and struct sockaddr_nl . Netlink families (e.g., NETLINK_ROUTE, NETLINK_KOBJECT_UEVENT, NETLINK_GENERIC) are identified by integer constants defined in the kernel headers.
Kernel‑side helper macros simplify handling:
#define NLMSG_ALIGNTO 4
#define NLMSG_ALIGN(len) (((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1))
#define NLMSG_LENGTH(len) ((len) + NLMSG_ALIGN(sizeof(struct nlmsghdr)))
#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))
#define NLMSG_DATA(nlh) ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))
#define NLMSG_NEXT(nlh,len) ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), \
(struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))
#define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && \
(nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && \
(nlh)->nlmsg_len <= (len))
#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))Creating a kernel Netlink socket is done with netlink_kernel_create() :
static inline struct sock *
netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg);
struct netlink_kernel_cfg {
unsigned int groups;
unsigned int flags;
void (*input)(struct sk_buff *skb);
struct mutex *cb_mutex;
void (*bind)(int group);
bool (*compare)(struct net *net, struct sock *sk);
};Sending from kernel to user space uses netlink_unicast() (single destination) or netlink_broadcast() (multicast).
extern int netlink_unicast(struct sock *ssk, struct sk_buff *skb, __u32 portid, int nonblock);
extern int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, __u32 portid,
__u32 group, gfp_t allocation);4. Netlink Performance Advantages
Asynchronous communication : senders do not block waiting for a receiver.
Full‑duplex : data can flow simultaneously in both directions.
Multicast : a single send can deliver to up to 32 groups, reducing system‑call overhead.
Simple API : standard socket functions are used, lowering the learning curve.
Modular kernel implementation : kernel and user components can be compiled independently.
5. Common Netlink Scenarios
Network configuration (iproute2), routing table updates, device‑driver communication, system monitoring (e.g., top), firewall management (iptables/nftables), udev hot‑plug events, and custom kernel‑user protocols.
6. Netlink Working Principle
The communication flow is:
Create a Netlink socket in user space.
Bind it to a local sockaddr_nl (pid = getpid()).
Construct a nlmsghdr + payload.
Send with sendmsg() (or sendto() ) to the kernel (pid = 0).
The kernel validates the message, processes it according to the family, and optionally replies.
The user process receives the reply with recvmsg() .
7. Netlink Development Practice
7.1 User‑Space Example
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#define NETLINK_USER 31
#define MSG_LEN 1024
int main() {
int sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_USER);
if (sock_fd < 0) { perror("socket"); return -1; }
struct sockaddr_nl src_addr = {0};
src_addr.nl_family = AF_NETLINK;
src_addr.nl_pid = getpid();
src_addr.nl_groups = 0;
if (bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr)) < 0) {
perror("bind"); close(sock_fd); return -1; }
struct sockaddr_nl dest_addr = {0};
dest_addr.nl_family = AF_NETLINK;
dest_addr.nl_pid = 0; // kernel
dest_addr.nl_groups = 0;
struct nlmsghdr *nlh = (struct nlmsghdr*)malloc(NLMSG_SPACE(MSG_LEN));
memset(nlh, 0, NLMSG_SPACE(MSG_LEN));
nlh->nlmsg_len = NLMSG_SPACE(MSG_LEN);
nlh->nlmsg_pid = getpid();
strcpy((char*)NLMSG_DATA(nlh), "Hello from user space!");
if (sendto(sock_fd, nlh, nlh->nlmsg_len, 0,
(struct sockaddr*)&dest_addr, sizeof(dest_addr)) < 0) {
perror("sendto"); free(nlh); close(sock_fd); return -1; }
printf("Message sent to kernel: %s\n", (char*)NLMSG_DATA(nlh));
memset(nlh, 0, NLMSG_SPACE(MSG_LEN));
if (recv(sock_fd, nlh, NLMSG_SPACE(MSG_LEN), 0) < 0) {
perror("recv"); free(nlh); close(sock_fd); return -1; }
printf("Message received from kernel: %s\n", (char*)NLMSG_DATA(nlh));
free(nlh);
close(sock_fd);
return 0;
}7.2 Kernel‑Space Example
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/netlink.h>
#include <linux/skbuff.h>
#include <net/sock.h>
#define NETLINK_USER 31
static struct sock *nl_sk = NULL;
static void netlink_recv_msg(struct sk_buff *skb) {
struct nlmsghdr *nlh = (struct nlmsghdr*)skb->data;
printk(KERN_INFO "Kernel received: %s\n", (char*)NLMSG_DATA(nlh));
int pid = nlh->nlmsg_pid;
char *msg = "Hello from kernel!";
int msg_size = strlen(msg);
struct sk_buff *skb_out = nlmsg_new(msg_size, GFP_KERNEL);
if (!skb_out) { printk(KERN_ERR "Failed to allocate skb\n"); return; }
nlh = nlmsg_put(skb_out, 0, 0, NLMSG_DONE, msg_size, 0);
strncpy(NLMSG_DATA(nlh), msg, msg_size);
nlmsg_unicast(nl_sk, skb_out, pid);
}
static int __init netlink_init(void) {
struct netlink_kernel_cfg cfg = { .input = netlink_recv_msg };
nl_sk = netlink_kernel_create(&init_net, NETLINK_USER, &cfg);
if (!nl_sk) { printk(KERN_ALERT "Error creating Netlink socket\n"); return -10; }
printk(KERN_INFO "Netlink module loaded\n");
return 0;
}
static void __exit netlink_exit(void) {
netlink_kernel_release(nl_sk);
printk(KERN_INFO "Netlink module unloaded\n");
}
module_init(netlink_init);
module_exit(netlink_exit);
MODULE_LICENSE("GPL");7.3 Testing & Debugging
Compile the kernel module with make -C /lib/modules/$(uname -r)/build M=$(pwd) modules and load it using sudo insmod netlink_kernel.ko .
Compile the user program with gcc -o netlink_user netlink_user.c (add -g for GDB debugging).
Run the user program; it should exchange messages with the kernel module.
Use printk() in the kernel and dmesg to view kernel logs.
Use gdb for user‑space debugging and netstat -anp | grep netlink to inspect socket state.
8. Usage Considerations
8.1 Error Handling
Check return values of socket() , bind() , sendmsg() , and recvmsg() . Use perror() or printk() to log failures. Common errors include resource exhaustion, address conflicts, oversized messages, and permission issues.
8.2 Performance Optimisation
Adjust socket buffer sizes to match expected message volume.
Batch multiple small messages into a single Netlink packet to reduce system‑call overhead.
Offload heavy processing to worker threads or kernel threads to keep the Netlink callback short.
Choose efficient data structures (hash tables, linked lists) for message bookkeeping.
8.3 Security
Restrict Netlink family access with proper permissions; only trusted processes should open the socket.
Validate all incoming payload lengths before accessing NLMSG_DATA() to prevent buffer overflows.
Consider authenticating messages (e.g., using a shared secret) and optionally encrypting sensitive payloads.
Deepin Linux
Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.