文章目录
- 前言
- netlink hello world
- netlink 用户层接口说明
- 示例代码
- 示例代码
- 安装依赖
- 使用cmake构建内核模块
- 运行
- 最后
前言
Netlink用于在内核进程和用户空间进程之间传输信息。它有两套API,一套API在用户空间使用,一套API在内核空间使用。它旨在成为比 ioctl
更灵活的后继者,主要提供与网络相关的内核配置(kernel configuration)和接口监控(monitoring interfaces)。
libnl是一组基于 netlink
协议的API库的集合。它是 netlink
更高层次的封装。
本文介绍netlink API的最简单使用,为之后使用libnl打下基础。
本文完整示例代码见仓库:https://github.com/da1234cao/demo-2/tree/laboratory/37-ntlink-1
netlink hello world
本节参考自:Linux Netlink 详解 、netlink(7) — Linux manual page 、Introduction to Netlink
netlink 用户层接口说明
Netlink通信通过套接字进行,需要先打开套接字。socket
的 domain
为 AF_NETLINK
。type
为 SOCK_DGRAM
或者 SOCK_DGRAM
,netlink 协议不区分这两者。protocol
是 Netlink
可用的协议,目前已经使用了二十多个,而最多允许定义32个。后面的示例中,我们将使用自定义的协议类型。系统已有的协议类型涉及到具体的网络内容,本文不涉及。
// socket (int domain, int type, int protocol)
fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC);
既然是通信,那至少有接收方和发送方。Netlink的接收方/发送方通常是用户进程/内核。socket网络编程中,通信双方使用四元组(源IP:源port-目的IP:目的port)来在网络中进行唯一标识。Netlink只能用于本机通信,所以不需要IP标识。端口方面,Netlink的端口并不是真的占用一个网络端口号,它只是一个唯一标识。内核的端口号总是0,用户进程的端口号可以为进程ID,如果是在多线程中,可以使用线程ID。
所以我们接下来是给套接字绑定端口号。socket网络编程中,我们通常填充一个 sockaddr_in
结构,然后调用bind()函数。Netlink
也差不多,不过填充的是 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 */ // 不使用广播时,这里设置为0
};
之后,用户空间程序调用 socket的 recv/send 等函数,即可与内核互相发送内容。
send(fd, &request, sizeof(request));
n = recv(fd, &buffer, RSP_BUFFER_SIZE);
发送的内容一定得遵循某种格式,这样双方才能互相解析。Netlink
使用TLV(type, length, value)格式,即一个消息头,消息头后面时负载,消息头中记录整个消息得长度。Netlink
协议的标头格式如下。
struct nlmsghdr {__u32 nlmsg_len; /* Length of message including header */__u16 nlmsg_type; /* Message content */__u16 nlmsg_flags; /* Additional flags */__u32 nlmsg_seq; /* Sequence number */__u32 nlmsg_pid; /* Sending process port ID */
};
示例代码
上面的接口说明是比较无聊的。我们实际来跑一个示例。
示例代码
演示代码来自:Linux Netlink 详解
演示代码的功能:
- 用户空间的进程,向内核空间,发送 ”Hello kernel“。
- 内核空间的进程,向该用户进程,回复 ”Hello userspace“ 。
首先是用户空间的代码。
#include <linux/netlink.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>#define NETLINK_TEST 30
#define MAX_PAYLOAD 1024 /* maximum payload size*/
#define MAX_NL_BUFSIZ NLMSG_SPACE(MAX_PAYLOAD)// int PORTID = getpid();
int PORTID = 1;int create_nl_socket(uint32_t pid, uint32_t groups) {int fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST);if (fd == -1) {return -1;}struct sockaddr_nl addr;memset(&addr, 0, sizeof(addr));addr.nl_family = AF_NETLINK;addr.nl_pid = pid;addr.nl_groups = groups;if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {close(fd);return -1;}return fd;
}ssize_t nl_recv(int fd) {char nl_tmp_buffer[MAX_NL_BUFSIZ];struct nlmsghdr *nlh;ssize_t ret;// 设置 Netlink 消息缓冲区nlh = (struct nlmsghdr *)&nl_tmp_buffer;memset(nlh, 0, MAX_NL_BUFSIZ);ret = recvfrom(fd, nlh, MAX_NL_BUFSIZ, 0, NULL, NULL);if (ret < 0) {return ret;}printf("==== LEN(%d) TYPE(%d) FLAGS(%d) SEQ(%d) PID(%d)\n\n", nlh->nlmsg_len,nlh->nlmsg_type, nlh->nlmsg_flags, nlh->nlmsg_seq, nlh->nlmsg_pid);printf("Received data: %s\n", NLMSG_DATA(nlh));return ret;
}int nl_sendto(int fd, void *buffer, size_t size, uint32_t pid,uint32_t groups) {char nl_tmp_buffer[MAX_NL_BUFSIZ];struct nlmsghdr *nlh;if (NLMSG_SPACE(size) > MAX_NL_BUFSIZ) {return -1;}struct sockaddr_nl addr;memset(&addr, 0, sizeof(addr));addr.nl_family = AF_NETLINK;addr.nl_pid = pid; /* Send messages to the linux kernel. */addr.nl_groups = groups; /* unicast */// 设置 Netlink 消息缓冲区nlh = (struct nlmsghdr *)&nl_tmp_buffer;memset(nlh, 0, MAX_NL_BUFSIZ);nlh->nlmsg_len = NLMSG_LENGTH(size);nlh->nlmsg_pid = PORTID;memcpy(NLMSG_DATA(nlh), buffer, size);return sendto(fd, nlh, NLMSG_LENGTH(size), 0, (struct sockaddr *)&addr,sizeof(addr));
}int main(void) {char data[] = "Hello kernel";int sockfd = create_nl_socket(PORTID, 0);if (sockfd == -1) {return 1;}int ret;ret = nl_sendto(sockfd, data, sizeof(data), 0, 0);if (ret < 0) {printf("Fail to send\n");return 1;}printf("Sent %d bytes\n", ret);ret = nl_recv(sockfd);if (ret < 0) {printf("Fail to receive\n");}printf("Received %d bytes\n", ret);// while (1) {// nl_recv(sockfd);// nl_sendto(sockfd, data, sizeof(data), 0, 0);// }return 0;
}
然后是内核空间的代码。
#include <linux/module.h>
#include <linux/netlink.h>
#include <linux/skbuff.h>
#include <net/sock.h>#define NETLINK_TEST 30static struct sock *nl_sk = NULL;/** Send the data of `data`, whose length is `size`, to the socket whose port is* `pid` through the unicast.** @param data: the data which will be sent.* @param size: the size of `data`.* @param pid: the port of the socket to which will be sent.* @return: if successfully, return 0; or, return -1.*/
int test_unicast(void *data, size_t size, __u32 pid) {struct sk_buff *skb_out;skb_out = nlmsg_new(size, GFP_ATOMIC);if (!skb_out) {printk(KERN_ERR "Failed to allocate a new sk_buff\n");return -1;}// struct nlmsghdr* nlmsg_put(struct sk_buff *skb, u32 portid, u32 seq, int// type, int len, int flags);struct nlmsghdr *nlh;nlh = nlmsg_put(skb_out, 0, 0, NLMSG_DONE, size, 0);memcpy(nlmsg_data(nlh), data, size);// 设置 SKB 的控制块(CB)// 控制块是 struct sk_buff// 结构特有的,用于每个协议层的控制信息(如:IP层、TCP层) 对于 Netlink// 来说,其控制信息是如下结构体: struct netlink_skb_parms {// struct scm_credscreds; // Skb credentials// __u32portid; // 发送此SKB的Socket的Port号// __u32dst_group; // 目的多播组,即接收此消息的多播组// __u32flags;// struct sock*sk;// };// 对于此结构体,一般只需要设置 portid 和 dst_group 字段。// 但对于不同的Linux版本,其结构体会所有变化:早期版本 portid 字段名为 pid。// NETLINK_CB(skb_out).pid = pid;NETLINK_CB(skb_out).portid = pid;NETLINK_CB(skb_out).dst_group = 0; /* not in mcast group */// 单播/多播if (nlmsg_unicast(nl_sk, skb_out, pid) < 0) {printk(KERN_INFO "Error while sending a msg to userspace\n");return -1;}return 0;
}
EXPORT_SYMBOL(test_unicast);static void nl_recv_msg(struct sk_buff *skb) {struct nlmsghdr *nlh = (struct nlmsghdr *)skb->data;char *data = "Hello userspace";printk(KERN_INFO "==== LEN(%d) TYPE(%d) FLAGS(%d) SEQ(%d) PORTID(%d)\n",nlh->nlmsg_len, nlh->nlmsg_type, nlh->nlmsg_flags, nlh->nlmsg_seq,nlh->nlmsg_pid);printk("Received %d bytes: %s\n", nlmsg_len(nlh), (char *)nlmsg_data(nlh));test_unicast(data, strlen(data), nlh->nlmsg_pid);
}static int __init test_init(void) {printk("Loading the netlink module\n");// This is for 3.8 kernels and above.struct netlink_kernel_cfg cfg = {.input = nl_recv_msg,};nl_sk = netlink_kernel_create(&init_net, NETLINK_TEST, &cfg);if (!nl_sk) {printk(KERN_ALERT "Error creating socket.\n");return -10;}return 0;
}static void __exit test_exit(void) {printk(KERN_INFO "Unloading the netlink module\n");netlink_kernel_release(nl_sk);
}module_init(test_init);
module_exit(test_exit);
MODULE_LICENSE("GPL");
安装依赖
# 当前内核版本
uname -a
Linux bogon 4.18.0-553.8.1.el8_10.x86_64# https://rockylinux.pkgs.org/8/rockylinux-devel-x86_64/kernel-headers-4.18.0-553.el8_10.x86_64.rpm.html
dnf config-manager --set-enable devel
dnf install kernel-headers-$(uname -r) kernel-devel-$(uname -r)# 如果安装的kernel-devel和当前正在运行的内核不匹配
# https://forums.rockylinux.org/t/installing-kernel-devel-does-not-match-the-running-kernel/12619/4
dnf upgrade kernel
使用cmake构建内核模块
我没有使用MakeFile,而是使用cmake来构建内核模块,所有有点麻烦。具体构建方式见仓库。
- 首先我们得知道MakeFile是如何构建内核模块的。可见:Linux内核模块编写之1: Hello World及Makefile
- 但是我想用cmake构建,这样显得比较厉害。这里有可用的示例:cmake : specify linux kernel module output build directory、cmake-kernel-module
- 上面的cmake-kernel-module,还是有的不好使。当有多个源文件时,不够优雅。使用 configure_file 来配置Kbuild是个好主意。可见:Using CMake for a Linux kernel module (a template project)
构建脚本这里就不粘贴了。具体见仓库。
运行
# 安装上面编译生成的内核模块
insmod netlink_demo.ko# 运行用户空间进程,可以看到下面输出
./netlink_user
Sent 29 bytes
==== LEN(31) TYPE(3) FLAGS(0) SEQ(0) PID(0)Received data: Hello userspace
Received 32 bytes# 查看内核输出
dmesg
[ 9169.846147] netlink_demo: loading out-of-tree module taints kernel.
[ 9169.846194] netlink_demo: module verification failed: signature and/or required key missing - tainting kernel
[ 9169.847580] Loading the netlink module
[ 9189.842531] ==== LEN(29) TYPE(0) FLAGS(0) SEQ(0) PORTID(1)
[ 9189.842536] Received 13 bytes: Hello kernel
最后
目前,我不需要写netlink的内核模块,只需要关注netlink在用户空间的使用即可。
通常,我应该也不会直接调用netlink API , 而是调用libnl API,它更高层一些。
唉,又得去看libnl的文档。可怜的程序员。