Linux内核IP Queue机制的分析(一)——用户态接收数据包(2)
三、一个实现接收内核态发送的IP Queue数据包的用户态例程
由于IP Queue是使用Netlink机制进行内核态和用户态通信的。因此,用户态要接收内核态发送的IP Queue数据包,就需要设计相应的Netlink程序,也就是设计相应的基于Netlink的socket程序即可。这里,我不会详细介绍如何使用Netlink机制实现用户态和内核态进行通信。我假设阅读本文的朋友,已经熟悉了Netlink的使用。如果对Netlink的使用还不是很熟悉
这篇文章提供了一个使用netlink的完整的例程,包括内核态和用户态。讲的非常清楚,我看完这篇文章,又跑了一下上面提供的例程,基本上熟悉了Netlink的使用方法。
当然,如果读者不想花时间再去了解netlink的话,也可以通过这篇文章熟悉Netlink的使用。因为我这里提供的是完整的用户态例程,我会将源码完全提供出来,对于急于通过执行程序观察结果来学习Netlink和IP Queue的朋友,也可以通过随后提供的方法编译并执行程序。
以下讲述用户态例程接收IP Queue数据包的程序设计。
其实,由于Netlink程序也是使用socket的方式进行通信。那么接收IP Queue报文的方式应该遵循socket的标准流程,具体流程如下:
(1)调用socket()创建一个地址类型为PF_NETLINK(AF_NETLINK)的套接字。该套接字使用SOCK_RAW方式传输数据,协议类型为NETLINK_FIREWALL,即使用IP Queue;
(2)调用bind()将本地地址(Netlink通信双方使用该协议特有的地址格式,见下面struct sockaddr_nl)绑定到已建立的套接字上;
struct sockaddr_nl {
sa_family_t nl_family; /* AF_NETLINK */
unsigned short nl_pad; /* Zero. */
pid_t nl_pid; /* Process ID. */
__u32 nl_groups; /* Multicast groups mask. */
};
(3)调用sendto()发送相关的配置信息,告诉内核应用程序准备接受的是数据包的元数据,还是同时包括数据包本身;
(4)调用recvfrom()接受内核态发送来的IP Queue报文;
(5)调用close()关闭套接字,结束通信。
看了以上流程,我相信很多熟悉socket编程的朋友已经可以写出接收IP Queue报文的用户态程序了。
本文中的示例代码的实现整体也是依照上面的步骤。但在细节的实现上,参考了iptables源码给给出的libipq库的实现代码。libipq库是iptables中封装的实现用户态接收和发送IP Queue报文操作的,也就相当于对上面总结的IP Queue报文接受流程进行封装。整个libipq库分别由libipq.c和libipq.h两个源文件。我这里将两个源文件移植(基于iptables-1.3.5版本)到示例代码中并裁剪,并编写了测试程序ip_user.c。因此,整个实现代码包含三个源文件:ip_user.c、libipq.c和libipq.h。
以下将对三个源文件进行分析。
1. libipq.h
该头文件定义了一个关键的数据结构,并提供了所有进行Netlink通信的API.
数据结构的定义如下:
struct ipq_handle
{
int fd;
struct sockaddr_nl local;
struct sockaddr_nl peer;
};
其中,fd是socket通信的描述符,local和peer分别是Netlink通信双方的地址。
除了定义数据结构,剩下的主要就是提供给用户调用的API,函数列表如下:
struct ipq_handle *ipq_create_handle(u_int32_t flags, u_int32_t protocol);
int ipq_destroy_handle(struct ipq_handle *h);
ssize_t ipq_read(const struct ipq_handle *h, unsigned char *buf, size_t len);
int ipq_set_mode(const struct ipq_handle *h, u_int8_t mode, size_t len);
ipq_packet_msg_t *ipq_get_packet(const unsigned char *buf);
int ipq_message_type(const unsigned char *buf);
int ipq_get_msgerr(const unsigned char *buf);
int ipq_set_verdict(const struct ipq_handle *h,
ipq_id_t id,
unsigned int verdict,
size_t data_len,
unsigned char *buf);
int ipq_ctl(const struct ipq_handle *h, int request, ...);
char *ipq_errstr(void);
我将在下面libipq.c的讲解中对若干我们将要用到的一些函数进行分析。
2. libipq.c
该源文件实现了libipq.h中定义的所有函数,并定义了一些出错信息。
(1)ipq_create_handle()函数申请了一个struct ipq_handle *h结构体,用来存储随后创建的IPv4 socket通信的fd,以及通信双方的地址。本函数完成了通信双方地址的初始化,并将本地地址绑定到已生成的fd上。
ipq_create_handle()函数的源码如下:
struct ipq_handle *ipq_create_handle()
{
int status;
struct ipq_handle *h;
h = (struct ipq_handle *)malloc(sizeof(struct ipq_handle));
if (h == NULL) {
ipq_errno = IPQ_ERR_HANDLE;
return NULL;
}
memset(h, 0, sizeof(struct ipq_handle));
if (protocol == PF_INET)
h->fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_FIREWALL);
else {
ipq_errno = IPQ_ERR_PROTOCOL;
free(h);
return NULL;
}
if (h->fd == -1) {
ipq_errno = IPQ_ERR_SOCKET;
close(h->fd);
free(h);
return NULL;
}
memset(&h->local, 0, sizeof(struct sockaddr_nl));
h->local.nl_family = AF_NETLINK;
/*传递本地的pid*/
h->local.nl_pid = getpid();
h->local.nl_groups = 0;
status = bind(h->fd, (struct sockaddr *)&h->local, sizeof(h->local));
if (status == -1) {
ipq_errno = IPQ_ERR_BIND;
close(h->fd);
free(h);
return NULL;
}
memset(&h->peer, 0, sizeof(struct sockaddr_nl));
h->peer.nl_family = AF_NETLINK;
/*代表通信的另一方为内核*/
h->peer.nl_pid = 0;
h->peer.nl_groups = 0;
return h;
}
ipq_destroy_handle()函数关闭由ipq_create_handle()建立起来的fd,并释放申请的内存。源码如下:
int ipq_destroy_handle(struct ipq_handle *h)
{
if (h) {
close(h->fd);
free(h);
}
return 0;
}
(2)向内核发送模式请求的函数
int ipq_set_mode(const struct ipq_handle *h,
u_int8_t mode, size_t range)
{
/*构造一个向内核发送报文的结构体*/
struct {
struct nlmsghdr nlh;
ipq_peer_msg_t pm;
} req;
memset(&req, 0, sizeof(req));
req.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(req));
req.nlh.nlmsg_flags = NLM_F_REQUEST;
req.nlh.nlmsg_type = IPQM_MODE;
req.nlh.nlmsg_pid = h->local.nl_pid;
/*告诉协议栈所请求的报文传递模式*/
req.pm.msg.mode.value = mode;
/*请求内核返回报文的长度*/
req.pm.msg.mode.range = range;
return ipq_netlink_sendto(h, (void *)&req, req.nlh.nlmsg_len);
}
在构造完向内核发送的结构体req并设置相关内容之后,调用ipq_netlink_sendto函数发送用户态的请求数据,该函数代码如下:
static ssize_t ipq_netlink_sendto(const struct ipq_handle *h,
const void *msg, size_t len)
{
int status = sendto(h->fd, msg, len, 0,
(struct sockaddr *)&h->peer, sizeof(h->peer));
if (status < 0)
ipq_errno = IPQ_ERR_SEND;
return status;
}
ipq_netlink_sendto函数直接调用了sendto系统调用发送用户态的数据,返回的是发送出去的数据长度。当sendto调用失败时,对全局变量ipq_errno 赋值IPQ_ERR_SEND。这样方便以后用专门返回出错信息的函数引用。
(3)用户态发送了请求数据包之后,就处于等待接收内核返回数据包的状态。一旦内核NF得到包处理函数返回NF_QUEUE时,该包就会被ip_queue模块发送到用户态。用户态接收IP Queue数据包的函数为:
ssize_t ipq_read(const struct ipq_handle *h, unsigned char *buf, size_t len)
该函数的代码如下。其中buf存储来自内核态的数据包,len为buf的长度。
ssize_t ipq_read(const struct ipq_handle *h,
unsigned char *buf, size_t len)
{
return ipq_netlink_recvfrom(h, buf, len);
}
该函数直接调用ipq_netlink_recvfrom()函数,其源码为:
static ssize_t ipq_netlink_recvfrom(const struct ipq_handle *h,
unsigned char *buf, size_t len)
{
unsigned int addrlen;
int status;
struct nlmsghdr *nlh;
/*buf长度的校验,不能小于Netlink Message的头部长度*/
if (len < sizeof(struct nlmsgerr)) {
ipq_errno = IPQ_ERR_RECVBUF;
return -1;
}
addrlen = sizeof(h->peer);
status = recvfrom(h->fd, buf, len, 0,
(struct sockaddr *)&h->peer, &addrlen);
if (status < 0) {
ipq_errno = IPQ_ERR_RECV;
return status;
}
/*判断接收到的发送方的地址长度是否正确*/
if (addrlen != sizeof(h->peer)) {
ipq_errno = IPQ_ERR_RECV;
return -1;
}
/*内核态向用户态发送数据报文时,其pid=0*/
if (h->peer.nl_pid != 0) {
ipq_errno = IPQ_ERR_RECV;
return -1;
}
if (status == 0) {
ipq_errno = IPQ_ERR_NLEOF;
return -1;
}
nlh = (struct nlmsghdr *)buf;
/*判断是否发生数据报文被截断的情况*/
if (nlh->nlmsg_flags & MSG_TRUNC || nlh->nlmsg_len > status) {
ipq_errno = IPQ_ERR_RTRUNC;
return -1;
}
return status;
}
该函数返回读取到报文的实际长度。
至此,我们已经可以通过上面几个函数实现从内核态接收到既定模式的IP Queue报文。
(4)输出出错信息
char *ipq_errstr(void)
{
return ipq_strerror(ipq_errno);
}
static char *ipq_strerror(int errcode)
{
if (errcode < 0 || errcode > IPQ_MAXERR)
errcode = IPQ_ERR_IMPL;
return ipq_errmap[errcode].message;
}
根据函数执行过程中记录的出错信息,打印对相关出错的具体提示。
文章来源CU社区:Linux内核IP Queue机制的分析(一)——用户态接收数据包
相关文章