Linux 基于IP queue在内核态修改数据包(1)——发送到用户态数据包的结构

Linux 基于IP queue在内核态修改数据包(一)——发送到用户态数据包的结构

首先感谢以上三篇博客本来之前的工作是在参考了 这两篇博客的基础之上:

(1)Linux 内核IP Queue机制的分析(一)——用户态接受数据包

(2)Linux 内核IP Queue机制的分析(二)——用户态处理并传回数据包

这篇博客只是简单的对内核态发送到用户空间的数据报文的结构进行简单的分析。

并且此篇博文还有姊妹篇,综合来看一共有两篇博文:

(1)Linux基于IP Queue在内核态修改数据包(一)——发送到用户态数据包的结构

(2)Linux基于IP Queue在内核态修改数据包(二)——修改内核态数据包的内容


首先我们简单复述一下用户态接受并传回数据包的内容——基于libipq库

llibipq库由两个文件组成:libipq.h,libipq.c至于libipq库具体的内容可以参照上面给出的两篇博客。那么接下来我们谈一谈具体的数据报文结构。

(1)首先我们可以通过IP Queue机制将数据报文发送到用户空间。从内核态发送数据包到用户空间的程序如下。

/*
 *main.c
 */
#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



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),0);
		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);

			//printf("before\n");
			unsigned char payload[1024*1024];
			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");

			ret = ipq_set_verdict(h, ipq_packet->packet_id, 1,ipq_packet->data_len,payload + ETH_HDRLEN);
			printf("Accepted!\n");
		}
	}
	return 0;
}


在实验中,通过一个小小的UDP通信测试实例先简单的分析一下IP Queue机制发送到用户态的UDP报文结构。

<1>首先我们通过uclient端向userver端发送一段字符:fasfsafsasfas

效果如图:

Linux 基于IP queue在内核态修改数据包(1)——发送到用户态数据包的结构

    <2>通过userver端接受到的数据如下图所示也为字符串:fasfsafsasfas 并且输出接受到的字符串长度位 13

Linux 基于IP queue在内核态修改数据包(1)——发送到用户态数据包的结构

<3>此时在用户空间捕捉到数据信息并以16进制的形式输出,结果如下。

Linux 基于IP queue在内核态修改数据包(1)——发送到用户态数据包的结构

分析输出数据的结果,可知前14个字节为预留的MAC地址长度,其中一个为 源地址一个为目的地址,每个MAC地址所占的长度为7个字节

那么

                                                                         45 00

 00 29 00 00 40 00 40 11 3c  c2  7f  00 00 01 7f 00

 00 01 8e  f2 1f  40 00 15 65 1d  66 61 73 66 73 61

 66 73 61  73 66 61 73

分别表示什么含义呢?

我们从后面往前分析由于这是UDP报文所以采用的是UDP的传输结构。我们从后往前看。

     由于在程序输出的过程中用16进制表示数据,那么根据16进制可以计算出后面的一段:

66 61 73 66 73 61 66 73 61 73 66 61 73

ASCII码分别

102 97 115 102 115  97 102 115 97 115 102 97 115

还原到字符串刚好为:fasfsafsasfas 是我们发送到出去的数据。

那么根据UDP的结构可知:

Linux 基于IP queue在内核态修改数据包(1)——发送到用户态数据包的结构

根据报文结构可知:65 1d 表示的校验和字段,长度为16bit  而接下来的00 15表示的是报文的长度,报文长度 = UDP首部长度+数据长度 = 8 + 13 = 21 用16进制表示为00 15

同样往上面追溯8e  f2表示的源端口1f  40 表示的是目的端口。在通信测试实例中采用的是8000的端口,刚好对应16进制端口号为:1f 40。测试实例采用的本机通信地址进行通信端口号为127.0.0.1可知7f  00 00 01 7f 00 00 01分别通信的源地址和目的地址。

对于前面剩余的部分字段:45 00 00 29 00 00 40 00 40 11 3c  c2 在这里我们先暂时不做讨论,在接下来的部分中我们将讨论TCP的报文结构。与UDP相比TCP报文结构相对来说要简单的多。

接下来我们采用TCP通信来让IP queue通信小程序输出测试结果。

首先我们照样打开一个IP Queue小程序手机内核态传递过来的数据信息。通过小程序获取数据报文。然后用一个TCP通信的客户端给服务端发送信息。

客户端发送信息如下所示:发送了一串ABCDEFG的字符串。

Linux 基于IP queue在内核态修改数据包(1)——发送到用户态数据包的结构

如图所示的字符串那么ip queue发送到用户态的数据包邮那些呢?

Linux 基于IP queue在内核态修改数据包(1)——发送到用户态数据包的结构

此时发现内核态发送到用户空间的数据包有三个,至于为什么有三个我这里就不解释了,大家只要了解TCP协议通信的原理就知道了。在这里只有最后一个数据包才是我们测试程序实际发送数据的数据报文。与前面相同我们同样根据TCP报文结构从后面往前面分析。

可知:41 42 43 44 45 46 47 0a 分别对应ASCII码的字符为 A B C D E F G \n

并且TCP报文结构如下图所示:

Linux 基于IP queue在内核态修改数据包(1)——发送到用户态数据包的结构

根据TCP数据报文结构可知TCP报文结构好尼玛复杂。由于选项填充的长度无法确定我们从前往后看,c0 a8 da 81 c0 a8 da 81 分别为源地址和目的地址。那么接下来的是什么呢?即使整个TCP报文的框架了。其中d4 e4 1f 40分别为源端口和目的端口。f1 bd 18 46对应图中32bit的序号,接下来c4 12 f2 ef为确认序号,不管是序号还是确认序号以及接下来的32个bit 8018 04 00 我们都不要去过分关注。接下来为校验和字段61 84这是值得关注的一个位置。根据结构图可知填充字段的长度为12个字节。在数据中对应的为01 01 08 0a 00 02 b4 aa 00 02 5f 23.

对于采用TCP协议的数据报文而言填充字段要注意两个特点:

(1)填充字段的长度可以任意,但有一个长度限定的范围。

(2)TCP协议填充字段数据内容的修改并不影响数据传输。

与UDP不同TCP的报文结构不同,并且 TCP的结构中没有数据长度这一字段。

那么在TCP与UDP首部多出来的12个字节 的长度是什么 呢?

IP首部:

对上述的数据字段进行分析:45 00 00 29 00 00 40 00 40 11 3c  c2

3c c2:表示16bit IP首部校验和。

11:表示8bit的协议号,在此处为17表示的是TCP协议。

40:表示生存时间

00 00 40 00:分别表示3bit标志和13bit偏移

45 00 00 29:表示16bit表示符号

根据上述的分析可知内核态发送到用户空间的报文数据,而在对内核态发送到数据报文的数据进行修改时要清楚的知道数据报文的结构,以及清晰的了解到那些数据报文的修改会影响数据报文测传输。

Linux 基于IPqueue机制在用户修改数据包(一)——内核数据包结构