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));

为了发送 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);

接收过程是类似的。接收程序需要提前分配一个足够的 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 内核空间开发

除非要复用内核既有 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