上一篇:DIY TCP/IP IP模块和ICMP模块的实现4
8.7 IP分片的接收
本节实现DIY TCP/IP对IP 分片的接收,需要正确检验每个IP分片首部的校验和,将属于同一个identification的IP分片全部正确接收后,按照分片数据的偏移量,重新组合上层协议数据帧,再将重组后的数据帧交给相应模块处理。本节的验证是通过Large Packet Ping,如果重组后的IP数据帧交给ICMP模块,能通过ICMP模块的校验和检验,则说明本节实现的IP分片重组的代码是正确的。
首先来看ip.c中新增的数据结构和静态变量。
static pdbuf_t *reassemble_pdbuf = NULL;
static unsigned short reassemble_id = 0;
static unsigned short reassemble_offset = 0;
typedef union iphdr_flags {
struct _flags_offset {
unsigned short offset:13;
unsigned short more_frag:1;
unsigned short dont_frag:1;
unsigned short rsvd:1;
} b;
unsigned short v;
} iphdr_flags_t;
Line 1: reassemble_buf,重组IP分片用到的pdbuf,静态指针,只在IP模块内部使用。
Line 2-3: reassemble_id,保存收到的第一个IP分片中IP头部的identification字段,用于判断后续IP分片是否属于同一个IP数据帧。
Line 4-12: 新增结构体iphdr_flags_t,将IP头部中的两个字节的flags字段定义为union类型,v是value的简写,存放flags的数值,b是bitmap的简写,bit0-12 存放分片偏移,bit13存放more_frag字段,bit14存放don’t_frag字段,最高位bit15保留未用。该结构体方便解析IP头部中的flags字段。
修改ippkt_recv,实现IP分片的重组。
int ippkt_recv(unsigned char *pkt, unsigned int sz)
{
int ret = 0;
unsigned char *local_ip = NULL;
iphdr_t *ippkt = NULL;
iphdr_flags_t flags;
if (pkt == NULL || sz ==0 ) {
ret = -1;
goto out;
}
local_ip = netdev_ipaddr();
if (local_ip == NULL) {
log_printf(ERROR, "ip_recv failed, no local ip address\n");
ret = -1;
goto out;
}
ippkt = (iphdr_t *)pkt;
if (memcmp(local_ip, ippkt->dst_ip, sizeof(ippkt->dst_ip)) != 0) {
log_printf(VERBOSE, "Drop ip packet not for local host\n");
goto out;
}
if (cksum(0, ippkt, sizeof(iphdr_t), BENDIAN)) {
log_printf(ERROR, "Invalid IP header checksum\n");
ret = -1;
goto out;
}
flags.v = NTOHS(ippkt->flags_offset);
if (flags.b.more_frag || flags.b.offset) {
ret = ipfrag_reassemble(ippkt);
/* reassemble complete */
if (ret == 1) {
ippkt = (iphdr_t *)reassemble_pdbuf->payload;
sz = NTOHS(ippkt->total_len);
log_printf(INFO, "ip reassemble finished, %u\n", sz);
goto process;
} else
return ret;
}
//dump_buf(ippkt, sizeof(iphdr_t));
process:
switch (ippkt->proto) {
case IP_PROTO_ICMP:
icmp_recv((unsigned char *)ippkt, sz);
break;
case IP_PROTO_TCP:
break;
case IP_PROTO_UDP:
break;
default:
ret = -1;
break;
}
/* reassemble cleanup */
if (reassemble_pdbuf) {
log_printf(INFO, "free ip reassemble buffer: %p\n", reassemble_pdbuf);
pdbuf_free(reassemble_pdbuf);
reassemble_pdbuf = NULL;
reassemble_id = 0;
reassemble_offset = 0;
ret = 0;
}
out:
return ret;
}
ippkt_recv函数在8.2节已经介绍过,本节扩展实现IP分片的接收和重组。
Line 6: 新增局部变量flags,类型为iphdr_flags_t,方便IP头部中flags的解析。
Line 29-40: 将IP头部的flags字段转换为小端,赋值给flags.v,如果flags字段中more_frag或offset,任意一个不为0,则说明该IP数据帧是一个IP分片,调用ipfrag_reassemble处理。ipfrag_reassemble返回为1时,表明属于同一IP数据帧的所有分片已经正确接收,重组后的IP数据帧存放在reassemble_pdbuf指向的内存中,跳转到process处,根据IP头部的协议类型字段,将reassemble_pdbuf交给对应上层模块处理。
Line 55-63: 上层模块处理函数返回后,如果reassemble_pdbuf不为空则,调用pdbuf_free释放重组IP分片占用的内存,重置reassemble_id和reassemble_offset,设置ippkt_recv的返回值为0。
保持ippkt_recv函数的逻辑清晰,对IP分片的重组主要放在新增函数ipfrag_reassemble中,该函数是IP模块的静态函数,只在IP模块内部使用。
/*
* return val:
* 1: reassemble complete
* 0: reassembling
* other: error
*/
int ipfrag_reassemble(iphdr_t *ippkt)
{
int ret = 0;
iphdr_flags_t flags;
unsigned char *frag_data = NULL;
unsigned short frag_data_sz = 0;
unsigned char *payload_end = NULL;
if (reassemble_pdbuf == NULL) {
reassemble_pdbuf = pdbuf_alloc(REASSEMBLE_BUF_SZ, IGNORE_MTU);
if (reassemble_pdbuf == NULL) {
ret = -1;
goto out;
}
}
flags.v = NTOHS(ippkt->flags_offset);
/* first fragment */
if (flags.b.offset == 0 && flags.b.more_frag == 1) {
reassemble_id = NTOHS(ippkt->id);
reassemble_offset = 0;
pdbuf_rewind(reassemble_pdbuf, 0);
pdbuf_insert(reassemble_pdbuf, ippkt, sizeof(iphdr_t));
}
/* ip id & fragment offset validation */
if (reassemble_id != NTOHS(ippkt->id) ||
(reassemble_offset >> 3) != flags.b.offset) {
log_printf(ERROR, "invalid ip fragment id: %u, or offset: %u\n",
NTOHS(ippkt->id), flags.b.offset);
ret = -1;
goto out;
}
/* reassemble ip fragment data */
frag_data = (unsigned char *)strip_header(ippkt, sizeof(iphdr_t));
frag_data_sz = NTOHS(ippkt->total_len) - sizeof(iphdr_t);
pdbuf_insert(reassemble_pdbuf, frag_data, frag_data_sz);
reassemble_offset += frag_data_sz;
/* last fragment */
if (flags.b.offset && flags.b.more_frag == 0) {
payload_end = reassemble_pdbuf->payload;
/* rewind reassemble_pdbuf */
pdbuf_rewind(reassemble_pdbuf, 0);
/* implementation specific */
((iphdr_t *)reassemble_pdbuf->payload)->total_len =
HSTON(payload_end - reassemble_pdbuf->payload);
ret = 1;
}
log_printf(INFO, "ip id: %u, reassemble offset: %u\n", NTOHS(ippkt->id), reassemble_offset);
out:
return ret;
}
Line 1-7: ipfrag_reassemble的入参为ippkt指针,由ippkt_recv函数传入,指向收到的IP数据帧的首字节地址。函数的返回1时,表明属于同一IP数据帧的所有IP分片重组完成,返回0时,表明还有分片未接收,返回其他值,表示出错。
Line 8-14: 定义局部变量flags,便于解析IP头部中的flags字段。frag_data指向分片数据的首字节地址,frag_data_sz存放分片数据长度,payload_end指向重组后的IP数据帧的末尾字节的下一个字节地址。
Line 15-21: 判断reassemble_pdbuf为空时,调用pdbuf_alloc分配的内存,大小为REASSEMBLE_BUF_SZ,忽略MTU_SIZE的限制。REASSEMBLE_BUF_SZ最初在6.1节介绍各个协议头部的时,在ip.h头文件中引入,定义为((2 << 16) - 1 – 20),IP头部的total_len字段共两个字节,也就是说IP数据帧携带的最大数据长度为2的16次方减1,total_len字段是包括20个字节的IP头部长度。因此IP数据帧携带的数据最大长度为2的16次方减1,再减20。pdbuf_alloc申请内存时,忽略MTU_SIZE的限制,将申请2<<16 – 1 – 20 + RESERVED_HEADER_SZ + sizeof(ethhdr_t) + sizeof(pdbuf_t),确保重组IP分片时申请的buffer长度可以存放最大长度的IP数据帧。pdbuf_alloc申请内存空间失败时,返回出错,成功时继续执行。
Line 22-30: 将IP头部的flags_offset数值转换成小端,赋值给flags.v,如果more_frag为1且offset为0,则说明是第一个IP分片。将IP头部中的identification字段转换为小端,存入reassemble_id。设置reassemble_offset为0,收到的IP分片是按照分片偏移递增的顺序重新组合,调用pdbuf_rewind将reassemble_pdbuf->payload赋值为reassemble_pdbuf->buf。调用pdbuf_insert将第一个IP分片的IP头部存放在reassmeble_pdbuf->payload指向的内存处,pdbuf_insert在复制数据后,调整reassmebl_pdbuf->payload指针,向地址增大的方向移动sizeof(iphdr_t)个字节,指向IP头部末尾字节后的下一个字节地址。之前的章节使用pdbuf_push较多,pdbuf_push是先移动payload指针的指向,再复制数据。pdbuf_insert刚好相反,先复制数据,再移动payload指针,两个函数移动payload指针的方向也是相反的,pdbuf_push将payload指针向地址减小的方向移动,pdbuf_insert将payload指针向地址增大的方向移动。朋友们可以参考6.4节pdbuf模块的函数接口,再次阅读pdbuf_push和push_insert代码实现。
Line 31-38: IP分片头部的identification和分片偏移的合法性检查。reassemble_id存放第一个IP分片的identificaton值,后续属于同一IP数据帧的IP分片,包括最后一个IP分片,都必须和第一个IP分片的identificaiton值相等,表明这些分片同属一个IP数据帧,否则返回出错。reassemble_id在收到下一个需要分片的IP数据帧的第一个IP分片时更新。reassmble_offset保存已经重组过的IP分片的数据长度,即下一个需要重组的IP分片的偏移量,收到的IP分片头部的偏移量必须和该值相等,否则返回出错。
Line 39-43: 上述检查都通过后,调用strip_header将分片的IP头部剥去,strip_header返回IP分片中的IP头部末尾字节后的下一个字节地址,即IP分片的数据首字节地址,将该地址复制给frag_data指针。再将分片IP头部的total_len字段转换为小端格式,减去IP头部的长度后,得到分片的数据长度,存放在frag_data_sz变量中。调用pdbuf_insert将分片数据存放到reassemble_pdbuf->payload指向的内存中,再调整reassemble_pdbuf->payload向地址增大的方向移动frag_data_sz_个字节。reassemble_pdbuf->payload指始终指向重组组后的IP数据帧的末尾字节的下一个字节地址处。
Line 44-57: 将IP分片重组到reassemble_pdbuf后,判断分片是否是属于同一个IP数据帧的最后一个分片,如果是,则reassemble_pdbuf中存放的是完整的重组后的IP数据帧。先保存reassemble_pdbuf->payload的值,再通过pdbuf_rewind将reassemble_pdbuf->payload指向重组后的IP数据帧的首字节地址。计算重组后的IP数据帧的总长度(包括IP头部的长度),转换为大端格式后,存放到重组后的IP数据帧的total_len字段,返回值设为1。如果不是属于同一个IP数据帧的最后一个分片,则返回0。
ippkt_recv的修改已经完成,ipfrag_reassemble返回1时,reassemble_pdbuf中存放的是重组后的IP数据帧,头部的total_len字段也已经修改为重组后的总长度。可以将IP数据帧交给DIY TCP/IP的上层模块处理。本节的测试仍然是通过Large Packet Ping,根据重组后的IP数据帧的头部协议字段,将IP数据帧交给ICMP模块处理。本节没有对ICMP模块做任何修改,所以ICMP模块会根据收到的IP数据帧头部的总长度检验ICMP头部的校验和,如果ICMP模块能正确打印出接收到Echo Ping Request,则说明本节实现的重组IP分片的代码实现是正确的,编译运行,查看测试结果。
测试方法,运行DIY TCP/IP的主机记为A,与主机A处于同一局域网的主机B上设置PING的数据长度为5000,ping局域网中不存在的IP地址192.168.0.7,将该IP设置为DIY TCP/IP的虚拟IP地址。
上图是主机B的PING log,首先是不加-l参数的ping,确保本节添加的重组IP分片的代码不影响到8.5节之前实现的代码,从ping虚拟IP地址192.168.0.7的结果来看,主机B发出的4个Echo Ping Request均收到了来自虚拟IP地址192.168.0.7的回复,非large packet ping的运行结果并未收到影响。再限定-l参数,指定Echo Ping Request的数据长度为5000,Ping 虚拟IP地址是失败的,再来看主机A上DIY TCP/IP的运行Log。
主机A上指定DIY TCP/IP的虚拟IP地址为192.168.0.7,对于非Larget Packet Ping的4个Echo Ping Request,DIY TCP/IP均给出了正确的Echo Ping Reply的回复,与主机B上看到的log一致。收到主机B发出的Larget Packet Ping后,IP模块中的ipfrag_reassmble重组IP分片,并打印收到的IP数据帧的id为5224,offset分别是1480,2960,4440,5008。回顾ipfrag_reassemble的实现,offset的数值是在重组分片后根据分片的长度先增加,再打印出的运行Log,从而可以判断offset 1448,增加之前对应的offset为0,分片的长度是1480,第一个IP分片包含8个字节的ICMP头部数据。后面几个offset一次类推,最后一个offset 5008是,重组了最后一个IP分片后,offset的长度为5000 + 8,8是第一个IP分片的ICMP头部的长度,符合主机B的Large Packet Ping –l的指定参数5000。
Ip reassemble request finished一行,打印出的重组后的IP数据帧的总长度为5028,包括20个字节的IP头部,8个字节的ICMP头部和5000字节的Echo Ping Request数据,符合预期。
ICMP 模块能正确打印,收到的ICMP Echo Ping Request的id为1,sequence是5,回顾8.3节ICMP数据帧的接收,icmp_recv是在检验过ICMP头部校验和后打印出ICMP头部的信息的。由此可见,ICMP头部校验和检验通过,ICMP校验和是根据8个字节的ICMP头部和5000字节的Echo Ping Request的数据计算的。从而再次说明IP模块重组的IP数据帧是正确的。
DIY TCP/IP运行出错也是符合预期,回顾8.4节process_icmp_echo的实现,该函数尚不能处理超过MTU_SIZE的Echo Ping Request数据帧,pdbuf_alloc申请内存空间时受MTU_SIZE的限制,在将5000字节的Echo Ping Request数据push到申请的内存时出现assert错误,再次说明ICMP模块正确接收了5000 + 8字节的Echo Ping Rquest 数据帧,在构建Echo Ping Reply时,运行出错,下节我们将首先修改process_icmp_echo的实现,使之能够处理Larget Packet Ping数据帧。
下一篇:DIY TCP/IP IP模块和ICMP模块的实现6