目录
linux用户空间与内核空间通信——Netlink通信机制 - 知乎
参考:
linux用户空间与内核空间通信——Netlink通信机制 - 知乎
一、什么是netlink
Netlink套接字是用以实现用户进程与内核进程通信的一种特殊的进程间通信(IPC) ,也是网络应用程序与内核通信的最常用的接口。
在Linux 内核中,使用netlink 进行应用与内核通信的应用有很多,如
路由 daemon(NETLINK_ROUTE)
用户态 socket 协议(NETLINK_USERSOCK)
防火墙(NETLINK_FIREWALL)
netfilter 子系统(NETLINK_NETFILTER)
内核事件向用户态通知(NETLINK_KOBJECT_UEVENT)
通用netlink(NETLINK_GENERIC)
Netlink 是一种在内核与用户应用间进行双向数据传输的非常好的方式,用户态应用使用标准的 socket API 就可以使用 netlink 提供的强大功能,内核态需要使用专门的内核 API 来使用 netlink。一般来说用户空间和内核空间的通信方式有三种:/proc、ioctl、Netlink。而前两种都是单向的,而Netlink可以实现双工通信。
Netlink 相对于系统调用,ioctl 以及 /proc文件系统而言,具有以下优点:
1、netlink使用简单,只需要在include/linux/netlink.h中增加一个新类型的 netlink 协议定义即可,(如 #define NETLINK_TEST 20 然后,内核和用户态应用就可以立即通过 socket API 使用该 netlink 协议类型进行数据交换)
2、netlink是一种异步通信机制,在内核与用户态应用之间传递的消息保存在socket缓存队列中,发送消息只是把消息保存在接收者的socket的接收队列,而不需要等待接收者收到消息。使用 netlink 的内核部分可以采用模块的方式实现,使用 netlink 的应用部分和内核部分没有编译时依赖
3、netlink 支持多播,内核模块或应用可以把消息多播给一个netlink组,属于该neilink 组的任何内核模块或应用都能接收到该消息,内核事件向用户态的通知机制就使用了这一特性。内核可以使用 netlink 首先发起会话。
netlink具有以下特点:
① 支持全双工、异步通信(当然同步也支持)
② 用户空间可使用标准的BSD socket接口(但netlink并没有屏蔽掉协议包的构造与解析过程,推荐使用libnl等第三方库)
③ 在内核空间使用专用的内核API接口
④ 支持多播(因此支持“总线”式通信,可实现消息订阅)
⑤ 在内核端可用于进程上下文与中断上下文
Netlink协议基于BSD socket和AF_NETLINK地址簇,使用32位的端口号寻址,每个Netlink协议通常与一个或一组内核服务/组件相关联,如NETLINK_ROUTE用于获取和设置路由与链路信息、NETLINK_KOBJECT_UEVENT用于内核向用户空间的udev进程发送通知等。
netlink架构图
二、用户态下使用netlink
用户态使用标准的socket API如socket,bind,sendmsg,recvmsg和close等接口就能很容易地使用netlink socket。注意,使用 netlink 的应用必须包含头文件 linux/netlink.h。当然 socket 需要的头文件也必不可少,sys/socket.h
1)、创建netlink socket
sock_fd = socket(AF_NETLINK/PF_NETLINK, int type, int protocol)
其中,type可以取SOCK_RAW或者SOCK_DGRAM。protocol指定netlink协议类型。当前支持的协议类型定义如下:
目前 netlink 协议族支持32种协议类型,内核使用了21种,定义在include/uapi/linux/netlink.h:
#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,用于uevent消息通信
#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
#define MAX_LINKS 32</span>
2)、将netlink socket和进程绑定
bind(sock_fd, (struct sockaddr*)&nl_addr, sizeof(&nl_addr));
函数bind用于把一个打开的netlink socket与进程进行绑定,需要进行绑定的netlink socket地址结构如下:
//netlink socket地址结构体
struct sockaddr_nl {
__kernel_sa_family_t nl_family; /* 固定取AF_NETLINK或者PF_NETLINK,无差别 */
unsigned short nl_pad; /* 填充字段,取zero */
__u32 nl_pid; /* 当前进程port ID,一般通过getpid获取 */
__u32 nl_groups; //multicast groups mask ,用于指定多播组,每一个bit对应一个多播组,如果设置为0,表示不加入任何多播组
};
struct sockaddr_ln
struct sockaddr_ln为Netlink的地址,和我们通常socket编程中的sockaddr_in作用一样,他们的结构对比如下:
(1) nl_pid:在Netlink规范里,PID全称是Port-ID(32bits),其主要作用是用于唯一的标识一个基于netlink的socket通道。通常情况下nl_pid都设置为当前进程的进程号。前面我们也说过,Netlink不仅可以实现用户-内核空间的通信还可使现实用户空间两个进程之间,或内核空间两个进程之间的通信。该属性为0时一般指内核。
(2) nl_groups:如果用户空间的进程希望加入某个多播组,则必须执行bind()系统调用。该字段指明了调用者希望加入的多播组号的掩码(注意不是组号,后面我们会详细讲解这个字段)。如果该字段为0则表示调用者不希望加入任何多播组。对于每个隶属于Netlink协议域的协议,最多可支持32个多播组(因为nl_groups的长度为32比特),每个多播组用一个比特来表示。
3)、发送netlink消息
为了能够把netlink消息发送给内核或者别的用户进程,需要使用另外一个结构体struct sockaddr_nl作为目的地址。如果消息是发往内核的话,nl_pid和nl_groups都应该设置为0,;如果消息是发往另一个进程,nl_pid应该设置为接受者进程的PID,nl_groups用于设置需要发往的多播组。
填充好了目的地址后,就可以将netlink地址应用到结构体struct msghdr中,供函数sendmsg来调用:
使用函数 sendmsg 发送 netlink 消息时还需要引用结构 struct msghdr、struct nlmsghdr 和 struct iovec
①struct msghdr {
void *msg_name; /* optional address */
socklen_t msg_namelen; /* size of address */
struct iovec *msg_iov; /* scatter/gather array */
size_t msg_iovlen; /* # elements in msg_iov */
void *msg_control; /* ancillary data, see below */
size_t msg_controllen; /* ancillary data buffer len */
int msg_flags; /* flags on received message */
};
结构 struct msghdr 需如下设置:
struct msghdr msg;
memset(&msg, 0, sizeof(msg));
msg.msg_name = (void *)&nladdr; /* nladdr 为消息接收者的 netlink 地址 */
msg.msg_namelen = sizeof(nladdr);
由于linux内核的netlink部分总是认为每个netlink消息体中已经包含了下面的消息头,netlink socket 自己的消息头。所以每个应用程序在发送netlink消息之前需要提供这个头信息:
②//netlink message header
struct nlmsghdr {
__u32 nlmsg_len; /* Length of message including header 指定消息的总长度,包括紧跟该结构的数据部分长度以及该结构的大小*/
__u16 nlmsg_type; /* Message content 用于应用内部定义消息的类型,它对 netlink 内核实现是透明的,因此大部分情况下设置为 0*/
__u16 nlmsg_flags; /* Additional flags */
__u32 nlmsg_seq; /* 用于应用追踪消息,表示顺序号 */
__u32 nlmsg_pid; /* Sending process port ID, 消息来源进程 ID*/
};
消息头中各成员属性的解释及说明:
(1) nlmsg_len:整个消息的长度,按字节计算。包括了Netlink消息头本身。
(2) nlmsg_type:消息的类型,即是数据还是控制消息。目前(内核版本2.6.21)Netlink仅支持四种类型的控制消息,如下:
a) NLMSG_NOOP-空消息,什么也不做;
b) NLMSG_ERROR-指明该消息中包含一个错误;
c) NLMSG_DONE-如果内核通过Netlink队列返回了多个消息,那么队列的最后一条消息的类型为NLMSG_DONE,其余所有消息的nlmsg_flags属性都被设置NLM_F_MULTI位有效。
d) NLMSG_OVERRUN-暂时没用到。
(3) nlmsg_flags:附加在消息上的额外说明信息,如上面提到的NLM_F_MULTI。
标注:字段 nlmsg_flags 用于设置消息标志,可用的标志包括:
/* Flags values */
#define NLM_F_REQUEST 1 /* It is request message. 用于表示消息是一个请求,所有应用首先发起的消息都应设置该标志。 */
#define NLM_F_MULTI 2 /* Multipart message, terminated by NLMSG_DONE 指示该消息是一个多部分消息的一部分,后续的消息可以通过宏NLMSG_NEXT来获得*/
#define NLM_F_ACK 4 /* Reply with ack, with zero or error code 表示该消息是前一个请求消息的响应,顺序号与进程ID可以把请求与响应关联起来*/
#define NLM_F_ECHO 8 /* Echo this request 表示该消息是相关的一个包的回传 */
/* Modifiers to GET request */
#define NLM_F_ROOT 0x100 /* specify tree root */
/* 标志NLM_F_ROOT 被许多netlink协议的各种数据获取操作使用,该标志指示被请求的数据表应当整体返回用户应用,而不是一个条目一个条目地返回。有该标志的请求通常导致响应消息设置 NLM_F_MULTI标志。注意,当设置了该标志时,请求是协议特定的,因此,需要在字段 nlmsg_type 中指定协议类型。*/
#define NLM_F_MATCH 0x200 /* return all matching 表示该协议特定的请求只需要一个数据子集,数据子集由指定的协议特定的过滤器来匹配 */
#define NLM_F_ATOMIC 0x400 /* atomic GET 指示请求返回的数据应当原子地收集,这预防数据在获取期间被修改 */
#define NLM_F_DUMP (NLM_F_ROOT|NLM_F_MATCH) 未实现
/* Modifiers to NEW request */
#define NLM_F_REPLACE 0x100 /* Override existing 取代在数据表中的现有条目*/
#define NLM_F_EXCL 0x200 /* Do not touch, if it exists 用于和 CREATE 和 APPEND 配合使用,如果条目已经存在,将失败*/
#define NLM_F_CREATE 0x400 /* Create, if it does not exist指示应当在指定的表中创建一个条目 */
#define NLM_F_APPEND 0x800 /* Add to end of list 在表末尾添加新的条目 */
内核需要读取和修改这些标志,对于一般的使用,用户把它设置为 0 就可以,只是一些高级应用(如 netfilter 和路由 daemon 需要它进行一些复杂的操作)
填充完消息头后,在消息头后面就可以填充消息体的内容了,填充完消息体,使用struct iovec结构体,使iov_base指向包含netlink消息的缓冲区,即可调用sendmsg接口发送netlink消息。struct iovec结构定义如下:
③struct iovec
{
void __user *iov_base; /* BSD uses caddr_t (1003.1g requires void *) iov_base指向数据包缓冲区,即参数buff*/
__kernel_size_t iov_len; /* Must be size_t (1003.1g) */
/* iov_len是buff的长度。msghdr中允许一次传递多个buff,以数组的形式组织在 msg_iov中,msg_iovlen就记录数组的长度 (即有多少个buff) */
};
在完成以上步骤后,消息就可以通过下面语句直接发送:发送netlink消息的代码如下:
③struct iovec
{
void __user *iov_base; /* BSD uses caddr_t (1003.1g requires void *) iov_base指向数据包缓冲区,即参数buff*/
__kernel_size_t iov_len; /* Must be size_t (1003.1g) */
/* iov_len是buff的长度。msghdr中允许一次传递多个buff,以数组的形式组织在 msg_iov中,msg_iovlen就记录数组的长度 (即有多少个buff) */
};
4)、接收消息
接收程序需要申请足够大的空间来存储netlink消息头和消息的payload部分。用如下的方式填充结构体struct msghdr,然后调用recvmsg接口来接收netlink消息:
struct iovec iov;
iov.iov_base = (void *)msg_buffer;
iov.iov_len = nlh->nlmsg_len;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
sendmsg(sock_fd, &msg, 0);
当消息正确接收后,msg_buffer里保存包含netlink消息头的整个netlink消息,nlhdr指向接收到的消息的消息头,nladdr包含接收到的消息体的目的地址信息。宏NLMSG_DATA(nlhdr)返回指向消息的数据部分的指针(宏部分会介绍到)。
5)、关闭netlink socket
使用完前面创建的netlink socket后,就可以使用close接口关闭netlink socket,释放socket资源。关闭netlink socket的代码与关闭其他socket一致,代码如下: close(sock_fd);
netlink常用宏:在linux/netlink.h中定义了一些方便对消息进行处理的宏
#define NLMSG_ALIGNTO 4
①/* 宏NLMSG_ALIGN(len)用于得到不小于len且字节对齐的最小数值 */
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )
②/* Netlink 头部长度 */
#define NLMSG_HDRLEN ((int) NLMSG_ALIGN(sizeof(struct nlmsghdr)))
③/* 计算消息数据len的真实消息长度(消息体 + 消息头),一般用于分配消息缓存*/
#define NLMSG_LENGTH(len) ((len) + NLMSG_HDRLEN)
④/* 宏NLMSG_SPACE(len)返回不小于NLMSG_LENGTH(len)且字节对齐的最小数值,也用于分配消息缓存 */
#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))
⑤/* 宏NLMSG_DATA(nlh)用于取得消息的数据部分的首地址,设置和读取接收消息数据部分时需要使用该宏 */
#define NLMSG_DATA(nlh) ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))
⑥/* 宏NLMSG_NEXT(nlh,len)用于得到下一个消息的首地址, 同时len 变为剩余消息的长度。该宏一般在一个消息被分成几个部分发送或接收时使用。 */
#define NLMSG_NEXT(nlh,len) ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), \
(struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))
⑦断消息是否 >len */
#define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && (nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && (nlh)->nlmsg_len <= (len))
⑧/* NLMSG_PAYLOAD(nlh,len) 用于返回payload的长度*/
#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))
三、内核态netlink使用
内核空间的netlink API接口是由内核中的netlink核心代码支持的,在net/core/af_netlink.c中实现。内核模块通过这些API访问netlink socket并与用户空间的程序进行通信。
1) 添加自定义协议类型
内核已经支持的协议类型在linux/netlink.h中定义,如果要使用自定义的协议类型,需要在此文件中新增一个协议类型,然后就可以在linux内核模块中使用这个协议类型了。也就是内核模块要想使用netlink,必须包含头文件linux /netlink.h。内核使用netlink需要专门的API,这完全不同于用户态应用对netlink的使用。如果用户需要增加新的netlink协 议类型,必须通过修改linux/netlink.h来实现,当然,目前的netlink实现已经包含了一个通用的协议类型 NETLINK_GENERIC以方便用户使用,用户可以直接使用它而不必增加新的协议类型。
2) 在内核创建netlink socket
在内核空间,通过如下接口创建netlink socket:
struct sock *netlink_kernel_create(struct net *net, int unit, unsigned int groups, void (*input)(struct sk_buff *skb), struct mutex *cb_mutex, struct module *module);
参数net为网络设备名称,一般传入&init_net即可;unit为协议类型,传入自定义的协议类型;groups指定netlink 消息有多少个组,一般情况下传入0即可;input是用于netlink socket在收到消息时调用的处理消息的函数指针;cb_mutex是用于内核处理netlink socket消息时使用的互斥锁,一般情况下传入NULL即可;module指定创建的netlink socket所属的内核模块,一般情况下传入THIS_MODULE。
在内核创建了netlink socket后,当用户程序发送一个netlink消息到内核时,回调函数input都会被调用。下面是一个实现了消息处理函数input的例子:
void input (struct sock *sk, int len)
{
struct sk_buff *skb;
struct nlmsghdr *nlh = NULL;
u8 *payload = NULL;
while ((skb = skb_dequeue(&sk->receive_queue))!= NULL)
{
/* process netlink message pointed by skb->data */
nlh = (struct nlmsghdr *)skb->data;
payload = NLMSG_DATA(nlh);
/ * process netlink message with header pointed by * nlh and payload pointed by payload */
}
}
回调函数input是在用户进程调用sendmsg系统调用时被调用的。因此该函数中的处理时间不能太长,否则会导致其他系统调用被阻塞。比较好的做法是在该函数中创建一个新的内核线程来处理netlink socket消息。而函数input的工作只是唤醒该内核线程,这样sendmsg将很快返回。
函数skb = skb_dequeue(&sk->receive_queue)用于取得socket sk的接收队列上的消息,返回为一个struct sk_buff的结构,skb->data指向实际的netlink消息。
函数skb_recv_datagram(nl_sk)也用于在netlink socket nl_sk上接收消息,与skb_dequeue的不同指出是,如果socket的接收队列上没有消息,
它将导致调用进程睡眠在等待队列 nl_sk->sk_sleep,因此它必须在进程上下文使用,刚才讲的内核线程就可以采用这种方式来接收消息。
下面的函数input就是这种使用的示例:
void input (struct sock *sk, int len)
{
wake_up_interruptible(sk->sk_sleep);
}
当内核中发送netlink消息时,也需要设置目标地址与源地址,而且内核中消息是通过struct sk_buff来管理的, linux/netlink.h中定义了一个宏来方便消息的地址设置。
#define NETLINK_CB(skb) ((struct netlink_skb_parms)&((skb)->cb))
下面是一个消息地址设置的例子:
NETLINK_CB(skb).pid = 0;
NETLINK_CB(skb).dst_pid = 0;
NETLINK_CB(skb).dst_group = 1;
字段pid表示消息发送者进程ID,也即源地址,对于内核,它为 0, dst_pid 表示消息接收者进程 ID,也即目标地址,如果目标为组或内核,它设置为 0,否则 dst_group 表示目标组地址,如果它目标为某一进程或内核,dst_group 应当设置为 0。
3) 在内核中发送netlink消息
在内核空间发送netlink消息时有两个接口可以使用,netlink_unicast接口用来发送一个单播消息,其定义如下:
int netlink_unicast(struct sock *ssk, struct sk_buff *skb, u32 pid, int nonblock)
ssk是调用netlink_kernel_create接口所创建的socket控制块,为函数netlink_kernel_create()返回的socket;
skb中的data指向需要发送的netlink消息体,而skb的控制块保存了消息的地址信息,前面的宏NETLINK_CB(skb)就用于方便设置该控制块
pid为要发往的用户进程的PID;参数nonblock表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回,而如果为0,该函数在没有接收缓存可利用时睡眠
netlink消息的源地址和目的地址可以通过如下代码进行设置:
NETLINK_CB(skb).groups = local_groups;
NETLINK_CB(skb).pid = 0; /* from kernel */
NETLINK_CB(skb).dst_groups = dst_groups;
NETLINK_CB(skb).dst_pid = dst_pid;
使用netlink_broadcast接口可以发送一个多播消息,其定义如下:
int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 pid, u32 group, gfp_t allocation)
其中group是所有目标多播组对应掩码进行或运算的结果。allocation是内核申请内存的类型,通常情况下在中断上下文中使用GFP_ATOMIC(即不可以睡眠),否则使用GFP_KERNEL。
4)在内核中关闭netlink socket
假设netlink_kernel_create函数返回的netlink socket为struct sock *nl_sk,可以通过API来关闭这个netlink socket,注意函数netlink_kernel_create()返回的类型为struct sock,因此函数sock_release应该这种调用 :sock_release(nl_sk->socket); nl_sk为函数netlink_kernel_create()的返回值。
四、netlink子系统初始化
内核Netlink的初始化在系统启动阶段完成,初始化代码在af_netlink.c的netlink_proto_init()函数中,整个初始化流程如下:
netlink_table结构体:
struct netlink_table {
struct rhashtable hash; //用来索引同种协议类型的不同netlink套接字实例
struct hlist_head mc_list;//多播使用的sock散列表
struct listeners __rcu *listeners; //监听者掩码
unsigned int flags;
unsigned int groups; //协议支持最大多播组数目
struct mutex *cb_mutex;
struct module *module;
int (*bind)(struct net *net, int group);
void (*unbind)(struct net *net, int group);
bool (*compare)(struct net *net, struct sock *sock);
int registered;
};
netlink子系统初始化:
static int __init netlink_proto_init(void)
{
int i;
int err = proto_register(&netlink_proto, 0); //向内核注册netlink协议
if (err != 0)
goto out;
BUILD_BUG_ON(sizeof(struct netlink_skb_parms) > FIELD_SIZEOF(struct sk_buff, cb));
nl_table = kcalloc(MAX_LINKS, sizeof(*nl_table), GFP_KERNEL);//创建并初始化netlink_table表数组nl_table,每种协议占nl_table表数组的一项
if (!nl_table)
goto panic;
for (i = 0; i < MAX_LINKS; i++) {
if (rhashtable_init(&nl_table[i].hash,
&netlink_rhashtable_params) < 0) {
while (--i > 0)
rhashtable_destroy(&nl_table[i].hash);
kfree(nl_table);
goto panic;
}
}
INIT_LIST_HEAD(&netlink_tap_all);
netlink_add_usersock_entry();//初始化应用层使用的NETLINK_USERSOCK协议类型的netlink(用于应用层进程间通信)
sock_register(&netlink_family_ops);//向内核注册协议处理函数,即将netlink的socket创建处理函数注册到内核中,如此以后应用层创建netlink类型的socket时将会调用该协议处理函数,
register_pernet_subsys(&netlink_net_ops);//向内核所有的网络命名空间注册”子系统“的初始化和去初始化函数,这里的"子系统”并非指的是netlink子系统,而是一种通用的处理方式,在网络命名空间创建和注销时会调用这里注册的初始化和去初始化函数(当然对于已经存在的网络命名空间,在注册的过程中也会调用其初始化函数),后文中创建各种协议类型的netlink也是通过这种方式实现的。
/* The netlink device handler may be needed early. */
rtnetlink_init();//创建NETLINK_ROUTE协议类型的netlink
out:
return err;
panic:
panic("netlink_init: Cannot allocate nl_table\n");
}
core_initcall(netlink_proto_init);
五、总结
下图是netlink消息的内存分布图:
1 NETLINK_FIREWALL 协议
netlink消息头nlmsghdr.nlmsg_type成员的取值和socket中的protocol有关, 现在就介绍protocol为NETLINK_FIREWALL时的情况。
这个协议是非常有用的开发协议。首先它是有意被设计来在用户空间来调试iptables模块的架构,这个协议与很多iptables模块相关联。ip-queue模块就是其中一个(详见:linux IP_QUEUE机制应用层编程_菜鸟九段的博客-CSDN博客),在创建此协议的netlink套接字之前都需要先安装相关模块,发往相关模块的报文都会同样发给由NETLINK_FIREWALL协议创建的netlink套接字, 由此实现在用户空间监听流经netfilter的报文的目的。例如:
iptables -I OUTPUT -j QUEUE -p tcp –destination-port 7551
上面一条命令表示在OUTPUT链上, 发往端口7551的TCP报文都交给QUEUE链来处理, 而ip-queue模块正好和NETLINK_FIREWALL协议的套接字关联(内核实现的), 所以套接字同样也会收到这样的报文, 实现了监听的目的, 自行修改iptables命令可达到监听多种类型报文的目的。
1.1 创建和使用
socket fd=socket(AF_NETLINK, SOCK_RAW, NETLINK_FIREWALL);
此协议没有使用多播组, 所以地址结构struct sockaddr_nl中的nl_groups应该总是设置为0,并且此协议的套接字不需要bind函数,因为报文只是在进程和内核中传输,所以从进程发向内核的报文struct sockaddr_nl中的nl_pid应该设置为0。
1.2 消息类型
NETLINK_FIREWALL协议的套接字有三种消息类型(如3中所述, 成员nlmsg_type的取值),每个消息类型都有它各自的数据结构来描述。
- IPQM_MODE
- IPQM_PACKET < 这个是内核向用户空间返回的报文类型 >
- IPQM_VERDICT
1.2.1 IPQM_MODE
此类型是使用NETLINK_FIREWALL协议需要第一个发向内核的包, 内核收到之后才会将匹配的报文从内核发至用户空间的netlink套接字。此报文的数据结构如下, 它是紧随在struct nlmsghdr之后的:
typedef struct ipq mode msg { unsigned char value; size t range; };
value的取值有三种:
- IPQ_COPY_NONE - 不常用, 设置此值发给内核将导致iptables将所有到QUEUE链的报文丢弃。
- IPQ_COPY_META - 表示我希望内核返回报文的元数据(我理解是struct nlmsghdr + struct ipq_packet_msg 两个头部)部分。
- IPQ_COPY_PACKET - 表示希望内核返回报文, 报文长度由range控制, 若range为0表示返回整个报文。如果你需要在用户空间分析流经QUEUE链的报文应该设置此项并将range设置为0。
range:
此域只在value = IPQ_COPY_PACKET时才有效。
也就是说, 用户进程使用IPQM_MODE类型的报文告诉内核, 我需要你返回给我的报文是什么样的(不需要 or 要元数据 or 要range长的报文)
1.2.2 IPQM_PACKET
这个类型的报文是根据4.2.1之后内核根据需求返回的报文。只要之前设置的value不是IPQ_COPY_NONE, socket就会收到此类型的报文, 结构如下:
typedef struct ipq packet msg { unsigned long packet_id; unsigned long mark; long timestamp sec; long timestamp usec; unsigned int hook; char indev name[IFNAMSIZ]; char outdev name[IFNAMSIZ]; unsigned short hw_protocol; unsigned short hw_type; unsigned char hw_addrlen; unsigned char hw_addr[8]; size t data len; unsigned char payload[0]; };
- packet_id - 这个是内核产生的独一无二的标识, 在4.2.3中发送IPQM_VERDICT报文需要。
- mark - //
- timestap_sec - 报文抵达时间(秒)
- timestap_usec - 报文抵达时间(微秒)
- hook - 报文被重定向到QUEUE的hook number
- indev_name - //
- outdev_name - //
- hw_protocol - 通常是ETH_P_IP
- hw_type - 通常是ARPHDR_ETHER
- hw_addrlen - 通常为6
- hw_addr - 报文的源MAC地址
- data_len - payload数据长度
- payload - 柔性数组头部, 指向了payload数据的头部
1.2.3 IPQM_VERDICT
此类型的报文是在收到内核的回复报文之后, 用户经过自己的检测, 决定对此报文执行何种操作, IPQM_MODE --> IPQM_PACKET <----> IPQM_VERDICT, 是顺序的过程。也就是说, 只有你向内核发送IPQM_VERDICT说明了报文处理方式之后, 你才能recvmsg下一个到达QUEUE链的报文, 否则recvmsg会一直阻塞。结构如下:
typedef struct ipq verdict msg { unsigned int value; unsigned long id; size t data_len; unsigned char payload; };
value 指示了对报文的处理方式:
- NF_DROP - 立即丢弃报文
- NF_ACCEPT - 接收报文(不参与之后的iptables链了)
- NF_STOLEN - //
- NF_QUEUE - 不常使用
- NF_REPEAT - 将报文移入下一个iptables链
id - 指示了要对哪个报文进行处理, 对应4.2.2的packet_id成员, 这个成员唯一关联了一个进入QUEUE的报文
data_len - 指verdict报文的payload数据长度, 因为verdict是用户发向内核的, 此域一般设置为0
payload - //
2 NETLINK_ROUTE 协议
2.1 创建和使用
socket fd=socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
NETLINK_ROUTE协议是netlink套接字最大且是最成熟的协议, 它有它自己消息的处理宏, 这些NETLINK_ROUTE的宏是为了添加它的辅助数据段和定制化特别的消息类型而做的。
每个族都有同样的命名空间和辅助数据结构, 辅助数据结构后又跟着一个或多个消息。
每个族都包含三个方法, NEW, DEL, GET; 这些方法用于创建、删除和接收路由相关条目。
2.2 NETLINK_ROUTE消息宏
NETLINK_ROUTE消息实际有自己的数据结构, 如下所示。
struct rtattr { unsigned short rta_len; unsigned short rta_type; }
下面是NETLINK_ROUTE的消息内存布局:
对于struct rtattr结构, 与netlink消息头struct nlmsghdr结构相似, 有一些宏进行辅助处理, 参考上面宏部分。
• int RTA OK(struct rtattr *rta, int rtabuflen); - Verify the data integrity of the data which succedes this rtattr header. • void * RTA DATA(struct rtattr *rta); - Return a pointer to the ancilliary data associated with this rtattr header. • struct rtattr *RTA NEXT(struct rtattr *rta); - Return a pointer to the next rtattr header in the chain. • unsigned int RTA PAYLOAD(struct rtattr *rta); - Return the length of the ancilliary data associated with the passed rtattr header. • unsigned int RTA LENGTH(unsigned int length); - Return the aligned length for the passed payload length. This value is assigned to the rta len field of the rtattr header • unsigned int RTA SPACE(unsigned int length); - Return the length of the ancilliary data, when aligned.
2.3 消息类型
在使用NETLINK_ROUTE协议的情况下, netlink控制块struct nlmsghdr中的nlmsg_type域标识了多种消息类型,举例如下:
2.3.1 LINK消息
LINK消息族允许设置和获取关于系统接口的消息nlmsg_type有如下取值:
- RTM_NEWLINK - 创建一个新接口/有一个新接口被创建
- RTM_DELLINK - 删除一个接口
- RTM_GETLINK - 接收一个接口消息
每个消息的辅助数据结构是struct ifinfomsg:
struct ifinfomsg { unsigned char ifi_family; unsigned short ifi_type; int ifi index; unsigned int ifi_flags; unsigned int ifi_change; };
2.3.2 LINK消息struct rtattr结构rta_type取值
2.3.3 LINK消息内存布局
2.4 其它消息类型
比如ADDR消息, ROUTE消息等和LINK消息类似, 不同的是它们有各自的struct ifinfomsg消息和支持不同的rta_type。