Netlink

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

Netlink

库的具体使用参考

https://www.infradead.org/~tgr/libnl/

源码的tests下面有一些有趣的例子。

上一篇:TLA+的 AND 运算符和UNCHANGED运算符


下一篇:TLA+ Specifying System (3)