Netlink:用户空间与内核空间交互

Reference

1 什么是Netlink

Netlink is a socket family that supplies a messaging facility based on the ++BSD socket interface++ to send and retrieve kernel-space information from user-space.  Netlink is portable, highly extensible and it supports ++event-based notifications++.

从这段描述来看Netlink可以提供类似socket接口,这意味着我们能够传输比较大量的,结构化的数据。另外,Netlink还提供了基于时间通知的功能,也适合我们时刻监控系统动态。

Netlink是一种面向数据表(datagram-oriented)的连通用户空间和内核空间的__++消息系统++__。同时,Netlink也可以用于进程间通信(InterProcess Communication, IPC)。我们这里只关注前者。Netlink构筑与通用的BSD scoket基础设施之上,因此支持使用socket(), bind(), sendmsg(), recvmsg()和其他通常的socket polling操作。

一般的BSD socket使用的是固定格式的数据结构(如AF_INET或者AF_RAW)。Netlink则提供更加可扩展的数据格式。

2 Netlink的典型应用场景

当前Netlink主要应用场景是网络相关应用,包括:

  • advanced routing
  • IPsec key management tools
  • firewall state synchronization
  • uesr-space packet enqueuing
  • border gateway routing protocols
  • wireless mesh routing protocols

这个应用场景与我们的需要时契合的

3 Netlink总线

Netlink允许最多32条内核空间总线。一般来说每个总线都关联到一个内核子系统中(多个子系统也可以共享一个总线)。总线共享的例子包括:

  1. nfnetlink:所有防火墙相关子系统共享
  2. rtnetlink:网络设备管理,路由和队列管理

关于Netlink总线,我发现了一个内核的patch,其中提到,"This patchset aims to improve this situation by add ing a new NETLINK_DESC bus with two commands..."

4 Netlink通信类型

Netlink支持两种通信类型:

  1. Unicast:一对一通信,即一个内核子系统对应一个用户空间程序。这种通信模式一般用来发送命令,或者获取命令执行的结果。
  2. Multicast:一对多通信。通常的场景是一个内核态模块向多个用户态监听者发送消息。这种监听者被划分为多个不同的组。一条Netlink总线可以提供多个组,用户空间可以订阅到一个或者多个组来获取对应的信息。最多可以创建
    个组。
Example scenario of unicast and multicast Netlink sockets

上图给出了Unicast和Multicast的图示。注意这里unicast是同步的,multicast是异步的。

5 Netlink消息格式

一般来说,Netlink消息对齐到32bit,其内部数据是host-byte order. 一个Netlink消息总由一段16bytes的header组成,header的格式为struct nlmsghdr(定义在<include/linux/netlink.h>中)

Layout of a Netlink message header

header包含如下字段:

  • 消息长度(32bits,  包含header的长度)
  • 消息类型(16bits)。消息类型的划分有两大类别:数据消息和控制消息。其中数据消息的类型取决于内核模块所允许的取值。控制消息类型则对所有Netlink子系统是一致的。控制消息的类型目前一共有四种。
    • NLMSG_NOOP: 不对对应任何实质操作,只用来检测Netlink总线是否可用
    • NLMSG_ERROR:该消息包含了错误信息
    • NLMSG_DONE:this is the trailing message that is part of a multi-part message. A  multi-part message is composed of a set of messages all with the NLM_F_MULTI flag set.
    • NLMSG_OVERRUN:没有使用
  • 消息标识(16bits)。一些例子如下:
    • NLM_F_REQUEST: 如果这个标识被设置了,表明这个消息代表了一个请求。从用户空间发往内核空间的请求必须要设置这个标识,否则内核子系统必须要回复一个invalid argument(EINVAL)的错误信息。
    • NLM_F_CREATE: 用户空间想要发布一个命令,或者创建一个新的配置。
    • NLM_F_EXCL: 通常和NLM_F_CREATE一起使用,用来出发配置已经存在的错误信息。
    • NLM_F_REPLACE: 用户空间想要替换现有配置。
    • NLM_F_APPEND: 想现有配置添加配置。这种操作一般针对的是有序的数据,如路由表。
    • NLM_F_DUMP: 用户应用想要和内核应用进行全面重新同步。这中消息的结果是一系列的multipart message。
    • NLM_F_MULTI: this is a multi-part message. A Netlink subsystem replies with a multi-part message if it has previously received a request from user-space with the NLM F DUMP flag set.
    • NLM_F_ACK: 设置了这个标识后,内核会返回一个确认信息表明一个请求已经执行。如果这个flag没有返回,那么错误信息会作为sendmsg()函数的返回值同步返回。
    • NLM_F_ECHO:  if this flag is set, the user-space application wants to get a report back via unicast of the request that it has send. 注意通过这种方式获取信息后,这个程序不会再通过事件通知系统获取同样的信息。
  • Sequence Number (32bits):  The sequence number is used as a tracking cookie since the kernel does not change the sequence number value at all
    • 可以和NLM_F_ACK一起使用,用户空间用来确认一个请求被正确地发出了。
    • Netlink uses the same sequence number in the messages that are sent as reply to a given request
    • For event-based notifications from kernel-space, this is always zero.
  • Port-ID (32bits): 包含了Netlink分配的一个数字ID。Netlink使用不同的port ID来确定同一个用户态进程打开的不同socket通道。第一个socket的默认port ID是这个进程的PID(Process ID)。在下面这些场景下,port ID为0:
    • 消息来自内核空间
    • 消息发送自用户空间,我们希望Netlink能够自动根据socket通道的port ID自动设置消息的port ID

以上是通用Netlink header格式。一些内核子系统会进一步定义自己的header格式,这样不同的子系统可以共享同一个Netlink socket总线。这种情形成为GetNetlink。

6 Netlink负载

6.1 Type-Length-Value(TLV)格式

An example of a hypothetical Netlink payload in TLV format

Netlink的消息格式由TLV格式的属性组成。TLV属性分为Length,  Type和Payload三部分。这种格式具有很强的可扩展性。在内核中,TLV属性的header定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* <------- NLA_HDRLEN ------> <-- NLA_ALIGN(payload)-->
* +---------------------+- - -+- - - - - - - - - -+- - -+
* | Header | Pad | Payload | Pad |
* | (struct nlattr) | ing | | ing |
* +---------------------+- - -+- - - - - - - - - -+- - -+
* <-------------- nlattr->nla_len -------------->
*/

struct nlattr {
__u16 nla_len;
__u16 nla_type;
};
  • nla_type:属性的取值很大程度上取决于内核空间子系统定义。不过Netlink预先定了两个重要的比特位:
    • NLA_F_NETSTED: 是否是嵌套属性。即在payload部分,以TLV的格式存储了更多的属性。
    • NLA_F_NET_BYTEORDER: payload内容的字节顺序(是否是network byte order(1))
  • nla_len: 注意,尽管payload部分会按照32bit进行对齐,这里的长度内容是不包含对齐补全的bit的。另外,这里的长度值包含了header。

7 Netlink错误消息

Layout of a Netlink error message

Netlink提供了一种包含了Netlink error header的消息类型,其格式如上图所示。这个header定义为struct nlmsgerr (<include/linux/netlink.h>)

1
2
3
4
5
6
7
8
9
10
11
12
13
struct nlmsgerr {
int error;
struct nlmsghdr msg;
/*
* followed by the message contents unless NETLINK_CAP_ACK was set
* or the ACK indicates success (error == 0)
* message length is aligned with NLMSG_ALIGN()
*/
/*
* followed by TLVs defined in enum nlmsgerr_attrs
* if NETLINK_EXT_ACK was set
*/
};
  • error: 错误类型。定义在error.h中,可以用perror()解析。
  • Netlink消息,为触发此错误的消息内容。 > With regards to message integrity, the kernel subsystems that support Netlink usually report invalid argument (EINVAL) via recvmsg() if user-space sends a malformed message

8 GeNetlink

前文我们提到过GetNetlink了。这一技术是为了缓解Netlink总线数量过少的问题。GeNetlink allows to register up to 65520 families that share a single Netlink bus. Each family is intended to be equivalent to a virtual bus。其中,每个family通过一个唯一的string name and ID number来注册。其中string name作为主键,而ID number在不同的系统中可能不同。

9 Netlink开发

Netlink开发涉及到内核空间和用户空间双边的开发。Linux提供了很多帮助函数来见过Netlink开发中重复性的解析,验证,消息构建的操作。

9.1 用户空间开发

从用户空间这一侧来看,Netlink sockets实现在通用的BSD socket接口之上。因此,在用户空间开发Netlink和开发TCP/IP socket应用是很类似的。不过,同其他典型的BSD socket应用相比,Netlink存在以下的不同之处:

  1. Netlink sockets do not hide protocol details to user-space as other protocols to. 即,Netlink会直接处理原始数据本身,用户空间的开发也要直接处理原始数据格式的负载。
  2. Errors that comes from Netlink and kernel subsystems are not returned by recvmsg() as an integer. Instead, errors are encapsulated in the Netlink error message. 唯一的例外是No buffer space error (ENOBUFS),这个错误是表明无法将Netlink消息放入队列。标准的通用socket错误,同样也是从recvmsg()中以integer形式返回。

涉及用户空间的Netlink开发的有两个库:libnllibmnl。这些库都是用C开发,用来简化Netlink开发。Netlink用户空间的进一步开发可以参考这两个库的例子和教程。

原始API的文档:https://www.systutorials.com/docs/linux/man/7-netlink/

9.1.1 打开socket

下面来阐述一下用户空间的Netlink开发的重要事项。前面提到Netlink使用了BSD socket的接口。一般而言,创建socket的接口长这样子(socket接口):

1
int socket (int family, int type, int protocol);
  • 第一个参数family是socket的大类。在开发TCP/IP应用的时候,这里总是AF_INET。而在Netlink中,这里总是设置为AF_NETLINK
  • type可以选择SOCK_RAW或者SOCK_DGRAM。不过Netlink并不会区分这两者。
  • protocol为Netlink场景下定义的具体协议类型,现有的主要协议包括:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define NETLINK_ROUTE		0	/* Routing/device hook				*/
#define NETLINK_UNUSED 1 /* Unused number */
#define NETLINK_USERSOCK 2 /* Reserved for user mode socket protocols */
#define NETLINK_FIREWALL 3 /* Unused number, formerly ip_queue */
#define NETLINK_SOCK_DIAG 4 /* socket monitoring */
#define NETLINK_NFLOG 5 /* netfilter/iptables ULOG */
#define NETLINK_XFRM 6 /* ipsec */
#define NETLINK_SELINUX 7 /* SELinux event notifications */
#define NETLINK_ISCSI 8 /* Open-iSCSI */
#define NETLINK_AUDIT 9 /* auditing */
#define NETLINK_FIB_LOOKUP 10
#define NETLINK_CONNECTOR 11
#define NETLINK_NETFILTER 12 /* netfilter subsystem */
#define NETLINK_IP6_FW 13
#define NETLINK_DNRTMSG 14 /* DECnet routing messages */
#define NETLINK_KOBJECT_UEVENT 15 /* Kernel messages to userspace */
#define NETLINK_GENERIC 16
/* leave room for NETLINK_DM (DM Events) */
#define NETLINK_SCSITRANSPORT 18 /* SCSI Transports */
#define NETLINK_ECRYPTFS 19
#define NETLINK_RDMA 20
#define NETLINK_CRYPTO 21 /* Crypto layer */

#define NETLINK_INET_DIAG NETLINK_SOCK_DIAG

我们可以直接使用NETLINK_USERSOCK供自己使用,或者自己定义一个新的量。

这里的protocol应当对应的是1.1.3中提到的总线。推理过程如下: 1. https://lwn.net/Articles/746776/ 这个链接中提叫的patch描述中称:This patch set aims to improve this situation by adding a new NETLINK_DESC bus with two commands 2. 在参考文献中谈论Netlink总线时,聚到了rtnetlink这个例子。根据rtnetlink的man page #include <asm/types.h> #include <linux/netlink.h> #include <linux/rtnetlink.h> #include <sys/socket.h>
rtnetlink_socket = socket(AF_NETLINK, int socket_type, NETLINK_ROUTE);

9.1.2 绑定socket地址

在打开了一个socket之后,我们需要为socket绑定一个本地地址。Netlink的地址格式如下:

1
2
3
4
5
6
7
struct sockaddr_nl
{
sa_family_t nl_family; /* AF_NETLINK */
unsigned short nl_pad; /* zero */
__u32 nl_pid; /* process pid */
__u32 ; /* mcast groups mask */
} nladdr;

这里的nl_pid可以通过getpid()这个函数来获取当前进程的pid来进行赋值

如果要在一个进程的多个线程中打开多个socket,可以用如下公式生成nl_pid

1
pthread_self() << 16 | getpid();

struct socketadd_nl中的nl_groups为bit mask,代表了广播分组。当设置为0时代表单播消息。

确定地址后可以将其绑定到socket

1
2
// fd为socket()返回的句柄
bind(fd, (struct sockaddr*)&nladdr, sizeof(nladdr));

9.1.3 发送Netlink消息

为了发送Netlink消息,我们还需要创建一个struct socketaddr_nl作为发送的目的地址。如果消息是发送给内核的,那么nl_pidnl_groups都要设置为0。如果这个消息是一个多播消息,那么需要设置nl_groups的对应比特。设置好目的地址之后,我们可以开始组装sentmsg()API需要的消息格式

1
2
3
struct msghdr msg;
msg.msg_name = (void *)&(nladdr);
msg.msg_namelen = sizeof(nladdr);

上面是socket的通用header,我们还需要设置Netlink自己的Message  header这里struct nlmsghdr定义为:

1
2
3
4
5
6
7
8
struct nlmsghdr
{
__u32 nlmsg_len; /* Length of message */
__u16 nlmsg_type; /* Message type*/
__u16 nlmsg_flags; /* Additional flags */
__u32 nlmsg_seq; /* Sequence number */
__u32 nlmsg_pid; /* Sending process PID */
};

在1.5中我们队各个字段的含义有了详细的介绍。按照对应的含义进行设置。 Netlink的消息由Netlink header和payload组成。因此我们需要一次性创建包含header和payload的内存块。

1
2
3
4
5
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;

此处使用的NLMSG_SPACE宏定义是Netlink提供的工具,其定义如下:

1
2
#define NLMSG_LENGTH(len) ((len) + NLMSG_HDRLEN)
#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))

这个宏做了两件事:

  1. 在长度上加上header的长度
  2. 将Payload进行32bit对齐

设置好负载内容后(负载数据段可以通过NLMSG_DATA(nlh)来获取),就可以发送了:

1
2
3
4
5
6
7
8
9
struct iovec iov;

iov.iov_base = (void *)nlh;
iov.iov_len = nlh->nlmsg_len;

msg.msg_iov = &iov;
msg.msg_iovlen = 1;

sendmsg(fd, &msg, 0);

9.1.3 接收Netlink消息

接收过程是类似的。接收程序需要提前分配一个足够的buffer来接收Netlink消息:

1
2
3
4
5
6
7
8
9
10
11
12
struct sockaddr_nl nladdr;
struct msghdr msg;
struct iovec iov;

iov.iov_base = (void *)nlh;
iov.iov_len = MAX_NL_MSG_LEN;
msg.msg_name = (void *)&(nladdr);
msg.msg_namelen = sizeof(nladdr);

msg.msg_iov = &iov;
msg.msg_iovlen = 1;
recvmsg(fd, &msg, 0);

9.2 内核空间开发

9.2.1 创建新的Netlink协议类型

除非要复用内核既有Netlink协议类型,不然最好定义一个自己用的总线类型

1
#define NETLINK_TEST 31

这个定义可以加在netlink.h中,或者放在模块的头文件里。

9.2.2 创建socket

在用户态,我们通过socket()接口来创建socket,而在内核中,我们使用如下的API:

1
2
struct sock *
netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg);
  • net一般固定为全局变量init_net
  • unit即为协议类型,我们在这里填上NETLINK_TEST
  • cfg为Netlink的内核设置
1
2
3
4
5
6
7
8
9
struct netlink_kernel_cfg {
unsigned int groups;
unsigned int flags;
void (*input)(struct sk_buff *skb);
struct mutex *cb_mutex;
int (*bind)(struct net *net, int group);
void (*unbind)(struct net *net, int group);
bool (*compare)(struct net *net, struct sock *sk);
};

其中input是必须要设置的,是socket在接收到一个消息后的回调函数。回调函数的一个例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static void hello_nl_recv_msg(struct sk_buff *skb)
{

struct nlmsghdr *nlh;
int pid;
struct sk_buff *skb_out;
int msg_size;
char *msg = "Hello from kernel";
int res;

printk(KERN_INFO "Entering: %s\n", __FUNCTION__);

msg_size = strlen(msg);

nlh = (struct nlmsghdr *)skb->data;
printk(KERN_INFO "Netlink received msg payload:%s\n", (char *)nlmsg_data(nlh));
pid = nlh->nlmsg_pid; /*pid of sending process */

skb_out = nlmsg_new(msg_size, 0);

if (!skb_out)
{

printk(KERN_ERR "Failed to allocate new skb\n");
return;
}
nlh = nlmsg_put(skb_out, 0, 0, NLMSG_DONE, msg_size, 0);
NETLINK_CB(skb_out).dst_group = 0; /* not in mcast group */
strncpy(nlmsg_data(nlh), msg, msg_size);

res = nlmsg_unicast(nl_sk, skb_out, pid);

if (res < 0)
printk(KERN_INFO "Error while sending bak to user\n");
}

9.2.3 从内核向用户态程序发送消息

正如在用户空间的发送流程那样,发送消息需要先设置一个socket接收地址。设置接收地址需要通过NETLIN_CB宏访问skb从control buffer中存储的netlink参数(struct netlink_skb_parms)。

1
2
3
4
5
6
7
8
9
struct netlink_skb_parms {
struct scm_creds creds; /* Skb credentials */
__u32 portid;
__u32 dst_group;
__u32 flags;
struct sock *sk;
bool nsid_is_set;
int nsid;
};

其中重要的参数时dst_groupflags。 如果要发送的数据包是单播数据包,发送方式为:

1
2
NETLINK_CB(skb_out).dst_group = 0; /* not in mcast group */
res = nlmsg_unicast(nl_sk, skb_out, pid);

这里的目标pid可以通过接收到的消息nlh->nlmsg_pid获取

如果要发送的数据包是多播:

1
res = nlmsg_multicast(nl_sk, skbout, own_pid, group, flags);
  1. 此处的own_pid是传输自己的pid来纺织消息传递给自己。因此内核态在这里填写0
  2. NETLNK_CB(skb_out).dst_group会在发送函数内设置。

10 Further Reading