DIY TCP/IP IP模块和ICMP模块的实现5

上一篇: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地址。
DIY TCP/IP Ping Sample
上图是主机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。
DIY TCP/IP Runtime Log0
主机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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值