上一篇: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