本文分析用户态接收到IP Queue的数据包后,根据数据包的相关信息决定数据包的下一步处理,并将处理后的数据包和处理的结果传递到内核态。文中如有任何疏漏和差错,欢迎各位朋友指正。
本文欢迎自由转载,但请标明出处,并保证本文的完整性。
作者:Godbach
日期:2009/02/19
一、处理IP Queue数据报文的编程接口
本文中涉及到的Netlink和IP Queue的基础知识、以及如何接收内核的数据包已经在Linux内核IP Queue机制的分析的第一篇文章《Linux内核IP Queue机制的分析(一)——用户态接收数据包》(以下简称文一)中进行了讲解。这里不再赘述基础知识。
随后讲到的测试程序实际上也是在原先的源码基础上进行修改而成的。唯一和文一中不同的地方就在于,我们会根据获取到的数据报文的内容,进行一些判断或修改等处理,然后将处理后的数据报文以及对报文的处理意见一并传回内核。
因此,对于IP Queue报文的实现以及接口,都可以参考文一。这里将会对处理数据报文的接口做一个简单的介绍。
文一中提到IP Queue机制中,用户态发到内核态的消息,其数据结构如下所示:
typedef struct ipq_peer_msg {
union {
ipq_verdict_msg_t verdict;
ipq_mode_msg_t mode;
} msg;
} ipq_peer_msg_t;
可见用户态发到内核的消息有两类。一类是告诉“模式消息”,告诉内核我用户态需要得到数据包的哪些消息(只需要数据报文的概要信息或者是数据报文的内容也要),另外一类就是“断言消息”,即用户态对数据报文处理之后,发送给内核的消息。该消息的内容包含了上文提到的对数据报文的处理意见和报文
“断言消息”的数据结构定义如下:
另一子类即“断言消息”,其数据类型定义如下:
typedef struct ipq_verdict_msg {
unsigned int value;
unsigned long id;
size_t data_len;
unsigned char payload[0];
} ipq_verdict_msg_t;
其中,value是用户态程序回传给内核的对当前报文的处理意见,可以是NF_ACCEPT或NF_DROP等值。id则是用以区分报文的标识号,即内核传来的ipq_packet_msg_t结构中的packet_id。当用户态程序修改了当前报文以后,需要将报文重新传递回内核,此时,新的报文内容必须存储在payload的开始处,并由data_len指明新报文的长度。
可见,ipq_verdict_msg_t包含了对当前报文的处理意见value和处理后的报文内容payload。
另外一个socket通信中的数据结构struct msghdr,最终用户态处理完数据报文之后,就要通过调用sendmsg将该结构体传给内核:
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 */
socklen_t msg_controllen; /* ancillary data buffer len */
int msg_flags; /* flags on received message */
};
这个结构体的内容可以分为四组。
第一组是msg_name和msg_namelen,记录这个消息的名字,其实就是数据包的目的地址。通常,如果是在不同主机之间进行的通信,则msg_name是指向一个结构体struct sockaddr的指针。长度为16。但是我们这里用于本地主机上用户态和内核态的通信,msg_name实际指向的是struct sockaddr_nl结构体。
第二组是msg_iov和msg_iovlen,记录这个消息的内容。msg_iov是一个指向结构体struct iovec的指针,实际上,确切地说,应该是一个结构体strcut iovec的数组。下面是该结构体的定义:
struct iovec{
void __user *iov_base;
__kernel_size_t iov_len;
};
iov_base指向数据包缓冲区,即参数buff,iov_len是buff的长度。msghdr中允许一次传递多个buff,以数组的形式组织在 msg_iov中,msg_iovlen就记录数组的长度(即有多少个buff)。在我们的实例中,传送的IP Queue信息由两个部分组成:netlink的消息头struct nlmsghdr,IP Queue的消息ipq_peer_msg_t。 而IP Queue的消息主要是指断言消息和处理过的数据报文的内容。因此,我们的实例中使用了三个struct iovec分别存储nlmsghdr,ipq_peer_msg_t 和 数据报文的内容。
第三组是msg_control和msg_controllen,它们可被用于发送任何的控制信息,在我们的例子中,没有控制信息要发送。暂时略过。
第四组是msg_flags。其值即为传入的参数flags。raw协议不支持MSG_OOB标志,即带外数据。
更详细的关于struct msghdr介绍请参考man手册 man 2 recv。同时,本文对该结构体相关成员的解释参考了如下连接上的内容,这里对原文的作者表示感谢:
http://lameck.blog.163.com/blog/static/388113742008825104426803/
二、一个实现处理并回传数据报文的用户态例程
1. 用户态例程的功能及设计流程
本为例程实现的功能是对用户态接收到的ICMP Echo Request 报文的内容(主要就是ICMP报文数据部分的长度)进行判断。如果是Windows系统发送的Requset,则告诉返回给内核NF_ACCEPT,接受该报文;否则,如果是其他系统发送的Request,则告诉返回给内核NF_DROP,丢弃该报文。
同样,这里先总结一下用户态处理并回传数据报文的流程。实际上整个报文的接收部分和文一的流程是一样的,只是增加了对数据报文的处理和回传:
(1)调用socket()创建一个地址类型为PF_NETLINK(AF_NETLINK)的套接字。该套接字使用SOCK_RAW方式传输数据,协议类型为NETLINK_FIREWALL,即使用IP Queue;
(2)调用bind()将本地地址(Netlink通信双方使用该协议特有的地址格式,struct sockaddr_nl)绑定到已建立的套接字上;
(3)调用sendto()发送相关的配置信息,告诉内核应用程序准备接受的是数据包的元数据,还是同时包括数据包本身;
(4)调用recvfrom()接受内核态发送来的IP Queue报文;
(5)获取数据报文的内容,根据内容做出相关处理,并确定报文的处理意见;
(6)构造struct msghdr结构体(见上文对该结构体的分析),并调用sendmsg函数,将数据报文和处理意见发送到内核;
(7)调用close()关闭套接字,结束通信。
以上的流程中,第(1)~(4)和(7)和文一中的例程是一致的,仅有(5)和(6)是本文实例程序新增加的。这两个步骤的工作就是读取数据报文的内容,进行判断,然后通过调用ipq_set_verdict()函数对用户处理后的数据报文和处理意见封装成断言消息,并传回内核态。
2. 相关函数的调用
ipq_set_verdict()函数时将用户态的断言小心传递给内核的,该函数也是libipq.c中提供的,其原型和如下:
int ipq_set_verdict(const struct ipq_handle *h,
ipq_id_t id,
unsigned int verdict,
size_t data_len,
unsigned char *buf)
@: h是通过调用ipq_create_handle()返回的struct ipq_handle类型的结构体,用来存储IPv4 socket通信的fd,以及通信双方的地址;
@:id即ipq_verdict_msg_t结构体中的id,上文已做解释;
@:verdict,对报文的处理意见,NF_ACCEPT或NF_DROP等。
@: data_len,传给内核的数据报文的长度;
@: buf,传给内核的数据报文。
具体的实现代码如下:
{
unsigned char nvecs;
size_t tlen;
struct nlmsghdr nlh;
ipq_peer_msg_t pm;
struct iovec iov[3];
struct msghdr msg;
memset(&nlh, 0, sizeof(nlh));
/*构造netlink message的头部*/
nlh.nlmsg_flags = NLM_F_REQUEST;
nlh.nlmsg_type = IPQM_VERDICT;
nlh.nlmsg_pid = h->local.nl_pid;
memset(&pm, 0, sizeof(pm));
/*构造IP Queue断言消息的头部*/
pm.msg.verdict.value = verdict;
pm.msg.verdict.id = id;
pm.msg.verdict.data_len = data_len;
/*将netlink message的头部,IP Queue断言消息的头部以及数据报文的内核保存在struct iovec []中*/
iov[0].iov_base = &nlh;
iov[0].iov_len = sizeof(nlh);
iov[1].iov_base = ±
iov[1].iov_len = sizeof(pm);
tlen = sizeof(nlh) + sizeof(pm);
nvecs = 2;
/*根据IP Queue断言消息头部中data_len,将处理后的数据报文的内容添加在断言消息头部的后面*/
if (data_len && buf) {
iov[2].iov_base = buf;
iov[2].iov_len = data_len;
tlen += data_len;
nvecs++;
}
/*根据传入的参数以及上面已经构造好的struct iovec[],构造struct msghdr结构体 */
msg.msg_name = (void *)&h->peer;
msg.msg_namelen = sizeof(h->peer);
msg.msg_iov = iov;
msg.msg_iovlen = nvecs;
msg.msg_control = NULL;
msg.msg_controllen = 0;
msg.msg_flags = 0;
nlh.nlmsg_len = tlen;
return ipq_netlink_sendmsg(h, &msg, 0);
}
ipq_set_verdict()函数最后调用了ipq_netlink_sendmsg()函数将封装好的struct msghdr结构体发送到内核。而ipq_netlink_sendmsg()函数其实只是对sendmsg系统调用的简单封装。该函数的实现仍然在libipq.c中,具体代码如下:
static ssize_t ipq_netlink_sendmsg(const struct ipq_handle *h,
const struct msghdr *msg,
unsigned int flags)
{
int status = sendmsg(h->fd, msg, flags);
if (status < 0)
ipq_errno = IPQ_ERR_SEND;
return status;
}
3. 用户态的例程——ipq_user_rw.c
/*
* ipq_usr_rw.c
*
* Testing program for processing IP Queue packets from kernel 2.6.18.3
*
* Dec 1, 2008 Godbach created.
* Dec 10, 2008 Godbach modified.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <netinet/ip_icmp.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include "libipq.h"
#define ETH_HDRLEN 14
typedef struct{
struct iphdr iph;
struct icmphdr icmph;
} ip_icmp_packet_s;
struct ipq_handle *h = NULL;
static void sig_int(int signo)
{
ipq_destroy_handle(h);
printf("Exit: %s\n", ipq_errstr());
exit(0);
}
int main(void)
{
unsigned char buf[1024];
/* creat handle*/
h = ipq_create_handle(0, PF_INET);
if(h == NULL){
printf("%s\n", ipq_errstr());
return 0;
}
printf("ipq_creat_handle success!\n");
/*set mode*/
unsigned char mode = IPQ_COPY_PACKET;
int range = sizeof(buf);
int ret = ipq_set_mode(h, mode, range);
printf("ipq_set_mode: send bytes =%d, range=%d\n", ret, range);
/*register signal handler*/
signal(SIGINT, sig_int);
/*read packet from kernel*/
int status;
struct nlmsghdr *nlh;
ipq_packet_msg_t *ipq_packet;
while(1){
status = ipq_read(h, buf, sizeof(buf));
if(status > sizeof(struct nlmsghdr))
{
nlh = (struct nlmsghdr *)buf;
ipq_packet = ipq_get_packet(buf);
printf("recv bytes =%d, nlmsg_len=%d, indev=%s, datalen=%d, packet_id=%x\n", status, nlh->nlmsg_len,
ipq_packet->indev_name, ipq_packet->data_len, ipq_packet->packet_id);
unsigned char payload[128];
memset(payload, 0x00, sizeof(payload));
memcpy(payload + ETH_HDRLEN, ipq_packet->payload, ipq_packet->data_len);
/*display packet data in hex including 14 bytes of ETH hdr(set by 0x00)*/
int i;
for(i = 0; i < ipq_packet->data_len + ETH_HDRLEN; i++){
if(i%16 == 0)
printf("00%.2x: ", i);
printf("%.2x ", payload[i]);
if(i % 16 == 15)
printf("\n");
}
printf("\n");
/*convert data to struct pattern of icmp packet */
ip_icmp_packet_s *icmp_pkt = (ip_icmp_packet_s*)(payload + ETH_HDRLEN);
unsigned short ip_totlen = ntohs(icmp_pkt->iph.tot_len);
unsigned short iph_len = icmp_pkt->iph.ihl*4;
/*get icmp data len*/
unsigned short icmp_datalen = ip_totlen - iph_len - sizeof(struct icmphdr);
/*ICMP echo request from WINDOWS OS*/
if(icmp_datalen == 32)
{
ret = ipq_set_verdict(h, ipq_packet->packet_id, NF_ACCEPT,ipq_packet->data_len,payload + ETH_HDRLEN);
printf("Ping request from Win. Accepted!\n");
}
/*ICMP echo request from Linux OS*/
else
{
ret = ipq_set_verdict(h, ipq_packet->packet_id, NF_DROP,ipq_packet->data_len,payload + ETH_HDRLEN);
printf("Ping request from Linux. Dropped!\n");
}
}
}
return 0;
}
整个例程相对比较简单,也只是在文一的基础上添加了一部分代码。 但是,有两点需要解释一下:
(1)该例程按照16进制的方式显式了数据报文的内容,每行16 bytes(模仿了Ethreal/Wireshark的数据报文的显式方式)。由于这里获取的报文内容是从IP层开始的,因此MAC头部的14个字节置为0x00.
(2)通常Window系统下的Ping的 Echo Request报文数据长度为32bytes,Linux下为56 bytes。这里仅作为测试,就认为32 bytes的来自Windows,否则来自Linux。 该程序并不保证在任何与该判断有冲突的情形下正常工作。
三、例程的测试
1. 测试环境的建立
本文中的测试环境和文一的完全一样。内核态要保证已经正确加载ip_queue.ko模块,用户态添加一条规则:
iptables -I INPUT -p icmp -j QUEUE
以确保所有进入本地的ICMP报文都进入到ip_queue模块处理。
2. 例程的测试
搭建好上面提示的环境之后,可以对应用程序的源码进行编译:
gcc libipq.c ipq_user_rw.c -o ipq_user_rw
执行ipq_user_rw:
[root@localhost ipq_user]# ./ipq_user_rw
ipq_creat_handle success!
ipq_set_mode: send bytes =44, range=1024
随后,程序处于等待接受内核数据包的状态。我们分别从Windows系统和Linux系统发送ping包到本地主机,然后看到终端的输出如下:
recv bytes =148, nlmsg_len=148, indev=eth1, datalen=60, packet_id=d657c4c0
0000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 45 00
0010: 00 3c 42 a6 00 00 80 01 ae 89 0a 01 1a e5 0a 01
0020: 1a ab 08 00 29 5c 02 00 22 00 61 62 63 64 65 66
0030: 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76
0040: 77 61 62 63 64 65 66 67 68 69
Ping request from Win. Accepted!
recv bytes =172, nlmsg_len=172, indev=eth1, datalen=84, packet_id=d6369860
0000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 45 00
0010: 00 54 00 00 40 00 40 01 f1 32 0a 01 1a ca 0a 01
0020: 1a ab 08 00 9e 36 8f 4e 00 04 ae 0c a2 49 83 1d
0030: 0c 00 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15
0040: 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25
0050: 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35
0060: 36 37
Ping request from Linux. Dropped!
以上输出中可以分别看到Windows和Linux发送的Ping包的报文内容。同时,可以分别在发送ping包的Windows和Linux下观察是否有echo reply包,或者进行抓包,看能否收到echo reply包。很显然,Windows下有echo reply包,而Linux下没有。
四、总结
本文的例程演示了用户态如何接受内核态的IP Queue报文,如何处理报文并回传到内核。由此看出,IP Queue机制的一个应用场合,就是需要根据报文的数据部分,以决定是否修改该数据报文的内容,以及告诉内核接受或丢弃该报文的情形。
也许读者会有疑问:对于这种情形,为什么不在内核态直接进行处理呢?可以从两个方面进行解释:其一、从内核的设计上来看,内核统一对所有报文的处理主要集中在网络层,内核基本上也只关心报文的IP头部,并根据该报文采用什么样的协议再交付下一层处理。经过传输层的传输,报文就到了应用层了,应用层来处理报文的应用数据也更符合逻辑;其二、相对于内核态,用户态有更丰富和更高效的工具来对报文的数据部分进行处理。
此外,用户态对数据报文处理的时候要注意两个方面:
(1)网络字节序和主机字节序之间的相互转换问题。从内核得到的数据报文是网络字节,本地处理的时候要考虑到转换的问题。本地转换之后,构成新的报文时,还要考虑主机字节序到网络字节序的转换;
(2)本文的例程只是简单的根据报文的内容进行了判断,并未修改报文的内容。实际应用中,如果一旦修改了报文的内容,就考虑对应调整各个部分的校验和问题。读者要熟悉校验和的计算方法,以及各个层计算校验和时包含了数据报文的哪些部分。