1 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 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 3 Netlink 总线
Netlink 允许最多 32 条内核空间总线。一般来说每个总线都关联到一个内核子系统中(多个子系统也可以共享一个总线)。总线共享的例子包括:
nfnetlink
:所有防火墙相关子系统共享rtnetlink
:网络设备管理,路由和队列管理
关于 Netlink 总线,我发现了一个内核的patch,其中提到,"This patchset aims to improve this situation by add ing a new NETLINK_DESC bus with two commands..."
4 4 Netlink 通信类型
Netlink 支持两种通信类型:
- Unicast:一对一通信,即一个内核子系统对应一个用户空间程序。这种通信模式一般用来发送命令,或者获取命令执行的结果。
- Multicast:一对多通信。通常的场景是一个内核态模块向多个用户态监听者发送消息。这种监听者被划分为多个不同的组。一条 Netlink 总线可以提供多个组,用户空间可以订阅到一个或者多个组来获取对应的信息。最多可以创建
上图给出了 Unicast 和 Multicast 的图示。注意这里 unicast 是同步的,multicast 是异步的。
5 5 Netlink 消息格式
一般来说,Netlink 消息对齐到 32bit,其内部数据是host-byte order. 一个 Netlink 消息总由一段 16bytes 的 header 组成,header 的格式为struct nlmsghdr
(定义在<include/linux/netlink.h>
中)
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 theNLM_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 6 Netlink 负载
6.1 6.1 Type-Length-Value(TLV)格式
Netlink 的消息格式由 TLV 格式的属性组成。TLV 属性分为 Length, Type 和 Payload 三部分。这种格式具有很强的可扩展性。在内核中,TLV 属性的 header 定义如下:
1 | /* |
nla_type
:属性的取值很大程度上取决于内核空间子系统定义。不过 Netlink 预先定了两个重要的比特位:- NLA_F_NETSTED: 是否是嵌套属性。即在 payload 部分,以 TLV 的格式存储了更多的属性。
- NLA_F_NET_BYTEORDER: payload 内容的字节顺序(是否是 network byte order(1))
nla_len
: 注意,尽管 payload 部分会按照 32bit 进行对齐,这里的长度内容是不包含对齐补全的 bit 的。另外,这里的长度值包含了 header。
7 7 Netlink 错误消息
Netlink 提供了一种包含了 Netlink error header 的消息类型,其格式如上图所示。这个 header 定义为struct nlmsgerr
(<include/linux/netlink.h>
)
1 | struct nlmsgerr { |
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 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 9 Netlink 开发
Netlink 开发涉及到内核空间和用户空间双边的开发。Linux 提供了很多帮助函数来见过 Netlink 开发中重复性的解析,验证,消息构建的操作。
9.1 9.1 用户空间开发
从用户空间这一侧来看,Netlink sockets 实现在通用的 BSD socket 接口之上。因此,在用户空间开发 Netlink 和开发 TCP/IP socket 应用是很类似的。不过,同其他典型的 BSD socket 应用相比,Netlink 存在以下的不同之处:
- Netlink sockets do not hide protocol details to user-space as other protocols to. 即,Netlink 会直接处理原始数据本身,用户空间的开发也要直接处理原始数据格式的负载。
- 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 开发的有两个库:libnl和libmnl。这些库都是用 C 开发,用来简化 Netlink 开发。Netlink 用户空间的进一步开发可以参考这两个库的例子和教程。
原始 API 的文档:https://www.systutorials.com/docs/linux/man/7-netlink/
9.1.1 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 |
|
我们可以直接使用 NETLINK_USERSOCK 供自己使用,或者自己定义一个新的量。
这里的 protocol 应当对应的是 1.1.3 中提到的总线。推理过程如下:
- https://lwn.net/Articles/746776/ 这个链接中提叫的 patch 描述中称:This patch set aims to improve this situation by adding a new NETLINK_DESC bus with two commands
- 在参考文献中谈论 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 9.1.2 绑定 socket 地址
在打开了一个 socket 之后,我们需要为 socket 绑定一个本地地址。Netlink 的地址格式如下:
1 | struct sockaddr_nl |
这里的 nl_pid 可以通过 getpid()这个函数来获取当前进程的 pid 来进行赋值
如果要在一个进程的多个线程中打开多个 socket,可以用如下公式生成nl_pid
:
1 | pthread_self() << 16 | getpid(); |
struct socketadd_nl
中的nl_groups
为 bit mask,代表了广播分组。当设置为 0 时代表单播消息。
确定地址后可以将其绑定到 socket
1 | // fd为socket()返回的句柄 |
9.1.3 9.1.3 发送 Netlink 消息
为了发送 Netlink 消息,我们还需要创建一个struct socketaddr_nl
作为发送的目的地址。如果消息是发送给内核的,那么nl_pid
和nl_groups
都要设置为 0。如果这个消息是一个多播消息,那么需要设置nl_groups
的对应比特。设置好目的地址之后,我们可以开始组装sentmsg()
API 需要的消息格式
1 | struct msghdr msg; |
上面是 socket 的通用 header,我们还需要设置 Netlink 自己的 Message header 这里struct nlmsghdr
定义为:
1 | struct nlmsghdr |
在 1.5 中我们队各个字段的含义有了详细的介绍。按照对应的含义进行设置。 Netlink 的消息由 Netlink header 和 payload 组成。因此我们需要一次性创建包含 header 和 payload 的内存块。
1 | struct nlmsghdr *nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)); |
此处使用的NLMSG_SPACE
宏定义是 Netlink 提供的工具,其定义如下:
1 |
这个宏做了两件事:
- 在长度上加上 header 的长度
- 将 Payload 进行 32bit 对齐
设置好负载内容后(负载数据段可以通过NLMSG_DATA
(nlh)
来获取),就可以发送了:
1 | struct iovec iov; |
9.1.4 9.1.3 接收 Netlink 消息
接收过程是类似的。接收程序需要提前分配一个足够的 buffer 来接收 Netlink 消息:
1 | struct sockaddr_nl nladdr; |
9.2 9.2 内核空间开发
9.2.1 9.2.1 创建新的 Netlink 协议类型
除非要复用内核既有 Netlink 协议类型,不然最好定义一个自己用的总线类型
1 |
这个定义可以加在netlink.h
中,或者放在模块的头文件里。
9.2.2 9.2.2 创建 socket
在用户态,我们通过socket()
接口来创建 socket,而在内核中,我们使用如下的 API:
1 | struct sock * |
net
一般固定为全局变量init_net
unit
即为协议类型,我们在这里填上NETLINK_TEST
cfg
为 Netlink 的内核设置
1 | struct netlink_kernel_cfg { |
其中input
是必须要设置的,是 socket 在接收到一个消息后的回调函数。回调函数的一个例子如下:
1 | static void hello_nl_recv_msg(struct sk_buff *skb) |
9.2.3 9.2.3 从内核向用户态程序发送消息
正如在用户空间的发送流程那样,发送消息需要先设置一个 socket 接收地址。设置接收地址需要通过NETLIN_CB
宏访问 skb 从 control buffer 中存储的 netlink 参数(struct netlink_skb_parms
)。
1 | struct netlink_skb_parms { |
其中重要的参数时dst_group
和flags
。 如果要发送的数据包是单播数据包,发送方式为:
1 | NETLINK_CB(skb_out).dst_group = 0; /* not in mcast group */ |
这里的目标 pid 可以通过接收到的消息
nlh->nlmsg_pid
获取
如果要发送的数据包是多播:
1 | res = nlmsg_multicast(nl_sk, skbout, own_pid, group, flags); |
- 此处的 own_pid 是传输自己的 pid 来纺织消息传递给自己。因此内核态在这里填写 0
- NETLNK_CB(skb_out).dst_group 会在发送函数内设置。