Linux内核IP Queue机制的分析(一)——用户态接收数据包(1)
一、基础知识
1. Netfilter
Linux内核在Netfilter(下文简称NF)框架的基础上提供了IP Queue机制,使得基于用户态(User Mode)的防火墙开发成为可能。
内核中NF对网络报文的处理这里不做详细描述。假设读者已经熟悉NF的工作原理和工作流程。但这里还是要简单介绍一下NF中各个钩子(hook)函数对数据包处理的返回值,即该函数告诉内核对该数据包的处理意见。所有的返回值如下:
NF_DROP: 丢弃该报文,释放所有与该报文相关的资源;
NF_ACCEPT: 接受该报文,并继续处理;
NF_STOLEN: 该报文已经被HOOK函数接管,协议栈无须继续处理;
NF_QUEUE: 将该报文传递到用户态去做进一步的处理;
NF_REPEAT: 再次调用本HOOK函数。
当HOOK处理函数返回值为NF_QUEUE时,内核协议栈将通过IP Queue机制把当前报文传递到用户态,由用户态的应用程序进行处理。这样,只要能够在相应的HOOK点上返回NF_QUEUE值,就可以将符合要求的报文传送到用户态去做进一步对报文行处理。随后,用户态程序会将处理后的报文以及对报文的处理意见(ACCEPT,DROP等)传递给内核协议栈。内核协议栈就会按照用户态对报文的处理意见将报文做接受、丢弃等处理。整个处理的过程就相当于一个用户态的防火墙,所有数据包的实质性处理都放在用户态进行。这样,即使是不具有深入内核知识的开发人员,也可以开发出适应一定应用场合的用户态防火墙。
2. Netlink机制
前面讲到,所谓IP Queue机制,只是当NF上Hook函数对数据包处理的返回值为NF_QUEUE时,协议栈会将数据包交给内核中的ip_queue模块。而ip_queue又是怎么将数据包传递给用户态的呢?这里就涉及到在内核开发中常见的问题,如何将内核态的数据传递到用户态,实现内核空间和用户空间的通信。具体实现的方法有多种。本人的博客中也总结了若干种,并配有测试的例程:http://blog.chinaunix.net/u/33048/article.html.对于IP Queue,则是使用Netlink机制实现内核态和用户态的交互。
NetLink是Linux系统特有的、基于socket编程接口的通信机制。它是一个面向数据报文的服务,并提供NETLINK_ROUTE(更新和修改路由操作)、NETLINK_FIREWALL (接受和发送IPv4协议NF传输的包,基于内核的ip_queue模块),NETLINK_ARPD(用户态ARP表操作)等多种通信协议。在创建基于IP Queue的NetLink Socket时,将采用如下系统调用:
fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_FIREWALL);
这里,PF_NETLINK指明要创建NetLink Socket;SOCK_RAW指明采用原始套接字,也可以采用SOCK_DGRAM,因为NetLink机制的实现并不区分SOCK_RAW和SOCK_DGRAM;参数NETLINK_FIREWALL则指明通信协议采用IP Queue。
既然IP Queue是基于NetLink的,其消息格式自然也遵从NetLink的规范。NetLink消息由两部分组成:消息头(struct nlmsghdr)和数据负载(data payload)。
消息头的定义如下(include/linux/netlink.h):
struct nlmsghdr
{
__u32 nlmsg_len; /*消息长度*/
__u16 nlmsg_type;/*消息类型*/
__u16 nlmsg_flags;/*额外的标志*/
__u32 nlmsg_seq; /*序列号*/
__u32 nlmsg_pid; /*进程号*/
};
所有的IP Queue消息都将包含一个struct nlmsghdr消息头,具体的IP Queue消息则包含在NetLink消息的数据负载中。有关NetLink消息格式的详情可以参见手册页Netlink(7)。
二、IP Queue编程接口
使用IP Queue机制的程序必须包含如下的头文件:
#include<linux/netfilter_ipv4/ip_queue.h>
在这个头文件中定义了所有IP Queue消息的格式。以下谈到关于IP Queue的若干个数据结构以及宏定义都包含在该文头件。
IP Queue消息可以分为两大类:由内核协议栈发给用户态进程的IP Queue消息和由用户态进程发给内核的IP Queue消息。
由内核协议栈发给用户态进程的IP Queue消息(nlmsghdr.nlmsg_type = IPQM_PACKET),其数据结构为ipq_packet_msg_t,定义如下:
/* Messages sent from kernel */
typedef struct ipq_packet_msg {
unsigned long packet_id; /* 报文的ID号 */
unsigned long mark; /* NF标记值 */
long timestamp_sec; /*报文到达时间(秒) */
long timestamp_usec; /* 报文到达时间(毫秒) */
unsigned int hook; /* 报文所处的NF 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[]; /* 报文本身的数据,可选 */
} ipq_packet_msg_t;
这个数据结构也被称为“报文的元数据”。个人理解,所谓“报文的元数据”应该是关于报文的摘要信息,而不包括报文数据的本身。从上面的这个结构体中也可以看出来。内核除可以单独向用户进程传递“报文的元数据”以外,也可以同时传递报文本身。此时,报文本身的数据将存储在ipq_packet_msg_t数据成员payload开始的地方。
至于内核在什么情况下向用户传递报文的元数据,什么情况下向用户传递报文的元数据加报文本身的数据,那就要看用户所请求的模式了。下面将讲述用户态发到内核态消息的格式,也正好解答了我们这里提出的问题。
用户态发到内核态的消息,其数据结构如下所示:
typedef struct ipq_peer_msg {
union {
ipq_verdict_msg_t verdict;
ipq_mode_msg_t mode;
} msg;
} ipq_peer_msg_t;
通过该数据结构可知,这类消息又分为“模式设置消息(nlmsghdr.nlmsg_type = IPQM_MODE)”和“断言消息(nlmsghdr.nlmsg_type = IPQM_VERDICT)”两个子类。
“模式设置消息”的数据结构定义如下:
typedef struct ipq_mode_msg {
unsigned char value;/* 请求的模式 */
size_t range;/* 请求拷贝的报文长度*/
} ipq_mode_msg_t;
这里,请求模式value的值可以是IPQ_COPY_NONE、IPQ_COPY_META和IPQ_COPY_PACKET,具体解释如下:
(1)请求模式value为IPQ_COPY_NONE时,报文将被丢弃;
(2)请求模式value为IPQ_COPY_META时,内核将在其后的报文传递中只传递“报文的元数据”。
以上两种情形传递range的值将被内核忽略。内核中会将该值置为0。
(3)请求模式value为IPQ_COPY_PACKET时,内核将同时传递“报文的元数据”和报文本身,报文本身的传递长度由ipq_mode_msg_t的另一个数据成员range指定。 range的大值不能超过IP报文的大长度,也就是0xFFFF。否则,会被自动置为0xFFFF。如果请求的长度大于报文自身的长度,将会按照报文自身长度进行传递。
另一子类即“断言消息”,其数据类型定义如下:
typedef struct ipq_verdict_msg {
unsigned int value;
unsigned long id;
size_t data_len;
unsigned char payload[];
} ipq_verdict_msg_t;
其中,value是用户态程序回传给内核的对当前报文的处理意见,可以是NF_ACCEPT或NF_DROP等值。id则是用以区分报文的标识号,即内核传来的ipq_packet_msg_t结构中的packet_id。当用户态程序修改了当前报文以后,需要将报文重新传递回内核,此时,新的报文内容必须存储在payload的开始处,并由data_len指明新报文的长度。
从上述内容可以看出,在整个IP Queue的报文传递过程中,用户态程序和内核协议栈之间的互动顺序是:
(1)用户态程序利用“模式设置消息”告诉内核协议栈所请求的报文传递模式;
(2)根据这个模式,内核组织好等待传递的消息,通过NetLink Socket发给用户态程序;
(3)用户态程序对接收到的数据包进行处理,得出该报文的处理意见(可能同时修改当前报文),并回传给内核。
文章来源CU社区:Linux内核IP Queue机制的分析(一)——用户态接收数据包
相关文章