netlink
netlink是用户空间和内核空间通信的一种机制,是一种特殊的 socket,它是 Linux 所特有的。
由于传送的消息是暂存在socket接收缓存中,并不被接收者立即处理,所以netlink是一种异步通信机制。
而系统调用和ioctl 则是同步通信机制。
在一些网关产品中,使用netfilter把数据包hook住,然后通过netlink传到用户态,用户态的app对这些数据包识别,或做控制,或做安全扫描,然后通过再传到内核态发送出去。
netlink允许内核主动发消息给用户态。
用户空间进程可以通过标准socket API来实现消息的发送、接收,在Linux中,有很多用户空间和内核空间的交互都是通过Netlink机制完成的,比如:路由管理,ss,iptable
netlink套接字创建
fd = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_NETLINKTEST);
netlink地址
netlink中表示地址的数据结构是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 */ };
struct sockaddr_nl中包含四项信息:
1. nl_family:地址类型,固定为AF_NETLINK。
2. nl_pad:这是一个填充字段,不用管它。
3. nl_pid:这是最主要的字段,表示通信地址,你可以随便设置源端nl_pid,就像普通socket中可以随便设置源端口号一样,目的端的nl_pid就是你想通信的那个netlink socket的nl_pid。为了防止nl_pid冲突,最简单的做法是将进程的pid作为nl_pid,内核的nl_pid固定为0。
4. nl_groups:netlink具有组通信功能,你可以向一个netlink组发送数据,这样属于这个netlink组的所有socket都可以接收到消息。
netlink报文
netlink socket中报文也由两部分构成 netlink header + payload,但是你需要自己处理netlink header,但是不用担心 netlink header也不算复杂,结构如下:
struct nlmsghdr { // 这是报文长度: netlink header + payload __u32 nlmsg_len; /* Length of message including header */ // 消息类型 __u16 nlmsg_type; /* Message content */ // 一些标志 __u16 nlmsg_flags; /* Additional flags */ // 当前报文的序号 __u32 nlmsg_seq; /* Sequence number */ // 源端 port id __u32 nlmsg_pid; /* Sending process port ID */ };
这里需要说明的是nlmsg_type,netlink消息可以定义成不同的类型,其中编号小于0x10的是预定义类型,有特殊用途,从x010开始供各个模块使用。
每个模块都可以定义不同类型的消息,比如0x10表示创建table,0x11表示创建chain,0x12表示创建rule,等等。
内核空间创建netlink套接字
struct netlink_kernel_cfg cfg = { .input = nltest_rcv, }; netlink_kernel_create(&init_net, NETLINK_NETLINKTEST, &cfg);
这个函数需要三个参数:
1. net:这是命令空间,直接使用init_net就可以了。
2. unit:这是模块编号,用来区分使用netlink的各个模块。
3. cfg:这个参数保存了这个模块的处理函数,每个模块可以定义自己的处理函数,struct netlink_kernel_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); }
每个模块都向netlink注册了一个socket,内核接收到netlink消息后,会根据用户程序调用socket(2)时设置的第三个参数查找socket,然后调用相应的input()函数,由input()函数处理接收到的netlink消息。
helloworld(原始套接字)
server端
#include <linux/init.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/skbuff.h> #include <linux/netlink.h> #include <net/netlink.h> #include <net/net_namespace.h> MODULE_AUTHOR("test"); MODULE_LICENSE("GPL"); #define NETLINK_NETLINKTEST 22 static struct sock *nltest_sock; // make -C /lib/modules/`uname -r`/build M=$PWD' static int nltest_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh) { char *data; data = nlmsg_data(nlh); printk(KERN_INFO"Receive msg from userspace: %s\n", data); return 0; } static void nltest_rcv(struct sk_buff *skb) { netlink_rcv_skb(skb, &nltest_rcv_msg); } static int __init netlinktest_init(void) { struct netlink_kernel_cfg cfg = { .input = nltest_rcv, }; nltest_sock = netlink_kernel_create(&init_net, NETLINK_NETLINKTEST, &cfg); if (!nltest_sock) return -ENOMEM; printk(KERN_INFO"Enter netlink test module.\n"); return 0; } static void __exit netlinktest_exit(void) { printk(KERN_INFO"Leave netlink test module.\n"); netlink_kernel_release(nltest_sock); } module_init(netlinktest_init); module_exit(netlinktest_exit);
编译和运行:
ifneq ($(KERNELRELEASE),) obj-m += nl_server1.o else KERNELDIR ?= /lib/modules/$(shell uname -r)/build all: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules clean: $(MAKE) -C $(KERNELDIR) M=$(PWD) clean endif make -C /lib/modules/`uname -r`/build M=$PWD'' sudo insmod nl_server1.ko
client端
#include <stdio.h> #include <string.h> #include <unistd.h> #include <malloc.h> #include <sys/types.h> #include <sys/socket.h> #include <linux/netlink.h> #define NETLINK_NETLINKTEST 22 #define NETLINK_TEST_ECHO 0x10 int main(int argc, char *argv[]) { int fd; int res; int len; struct nlmsghdr *nlh = NULL; struct sockaddr_nl saddr, daddr; char *msg = "hello world"; len = strlen(msg); fd = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_NETLINKTEST); if (!fd) { printf("socket() error.\n"); return -1; } memset(&saddr, 0, sizeof(saddr)); saddr.nl_family = AF_NETLINK; saddr.nl_pid = getpid(); memset(&daddr, 0, sizeof(daddr)); daddr.nl_family = AF_NETLINK; daddr.nl_pid = 0; res = bind(fd, (const struct sockaddr *)&saddr, sizeof(saddr)); if (res == -1) { printf("bind() error.\n"); goto error; } nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(len + 1)); if (!nlh) { printf("malloc() error.\n"); goto error; } nlh->nlmsg_type = NETLINK_TEST_ECHO; nlh->nlmsg_flags = NLM_F_REQUEST; nlh->nlmsg_seq = 0; nlh->nlmsg_pid = getpid(); strcpy(NLMSG_DATA(nlh), msg); nlh->nlmsg_len = NLMSG_LENGTH(len + 1); res = sendto(fd, nlh, NLMSG_SPACE(len + 1), 0, (struct sockaddr *)&daddr, sizeof(daddr)); if (res == -1) printf("send() error.\n"); error: free(nlh); close(fd); return res; }
编译和运行:
gcc nl_client1.c -o nl_client ./nl_client
通过dmesg可以看到结果:
[36371.489892] Enter netlink test module. [36393.360358] Receive msg from userspace: hello world
libnl
libnl封装了netlink编程接口,使得netlink编程更方便。
为了避免冗余链接,libnl按功能分成4个子库:libnl,libnl-route,libnl-genl,libnl-nf
库的具体使用参考
https://www.infradead.org/~tgr/libnl/
源码的tests下面有一些有趣的例子。