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

上一篇:DIY TCP/IP IP模块和ICMP模块的实现5
8.8 IP分片的发送
本节在8.7节的基础上修改ICMP模块对ICMP Echo Ping Reuqest的处理,构建ICMP Echo Ping Reply 数据帧,将长度超过MTU_SIZE (1500字节)的ICMP Echo Ping Reply数据帧交给IP模块 ,在IP模块添加IP分片的实现,并发送IP分片。IP分片是重组IP分片的逆过程,8.6节已经结合wireshark的数据帧分析过IP分片,这里直接给出分片规则。
IP模块收到长度大于MTU_SIZE的数据帧时,对上层模块的数据帧分片,每个IP分片最多携带1500 – sizeof(iphdr_t)共1480字节的数据。为每个IP分片构建IP头部,并计算IP头部校验和。为属于同一个IP数据帧的IP分片分配同一个IP identification,根据分片数据的长度设置IP分片头部的offset字段,根据是否有后续分片设置IP分片头部的more_frag字段。
先来看ICMP模块中process_icmp_echo函数的修改

  static int process_icmp_echo(iphdr_t *ippkt, icmphdr_t *echo_req)
  {
      int ret = 0;
      unsigned short data_len = 0;
      unsigned char *echo_req_data = NULL;
      icmphdr_t *echo_reply = NULL;
      pdbuf_t *pdbuf = NULL;
      int ignore_mtu = 0;
  
      if (ippkt == NULL || echo_req == NULL) {
          ret = -1;
          goto out;
      }
  
      echo_req_data = strip_header(echo_req, sizeof(icmphdr_t));
      data_len = NTOHS(ippkt->total_len) -
              sizeof(iphdr_t) - sizeof(icmphdr_t);
      if (data_len > MTU_SIZE)
          ignore_mtu = 1;
      pdbuf = pdbuf_alloc(data_len, ignore_mtu);
      if (pdbuf == NULL) {
          ret = -1;
          goto out;
      }
      /* build icmp reply */
      pdbuf_push(pdbuf, data_len);
      memcpy(pdbuf->payload, echo_req_data, data_len);
      pdbuf_push(pdbuf, sizeof(icmphdr_t));
      echo_reply = (icmphdr_t *)pdbuf->payload;
      echo_reply->type = ICMP_TYPE_ECHO_REPLY;
      echo_reply->code = ICMP_CODE;
      echo_reply->cksum = 0;
      echo_reply->id = echo_req->id;
      echo_reply->seq = echo_req->seq;
      echo_reply->cksum = cksum(0, echo_reply,
                  data_len + sizeof(icmphdr_t), BENDIAN);
      echo_reply->cksum = HSTON(echo_reply->cksum);
      log_printf(INFO, "Echo (ping) %s, id: %u, seq: %u/%u, ttl=%u\n",
          echo_reply->type == ICMP_TYPE_ECHO ? "request" : "reply",
          NTOHS(echo_reply->id), NTOHS(echo_reply->seq), echo_reply->seq,
          ippkt->ttl);
      //dump_buf(pdbuf->payload, pdbuf->end - pdbuf->payload);
      ret = ippkt_send(ippkt->src_ip, IP_PROTO_ICMP, pdbuf);
  out:
      return ret;
  }

process_icmp_echo函数的实现在ICMP模块,处理接收到的Echo Ping Request数据帧,构建Echo Ping Reply数据帧并发送ICMP Echo Ping Reply到IP模块。8.4节实现的process_icmp_echo函数不能处理长度超过MTU_SIZE的ICMP Echo Ping Request,本节扩展该函数,处理这种情况。
Line 15-24:IP头部的total_len减去20个字节的IP头部和8个字节的ICMP头部后,得到ICMP Echo Ping Request携带的数据长度data_len。如果data_len大于MTU_SIZE,则设置新增变量ignore_mtu为1,否则ignore_mtu为0。调用pdbuf_alloc为Echo Ping Reply数据申请内存空间。在ignore_mtu为1的情况,pdbuf_alloc将忽略MTU_SIZE的限制,申请data_len + RESERVED_HEADER_SZ + sizeof(ethhdr_t) + sizeof(pdbuf_t)个字节的内存空间,存放Echo Ping Reply数据,ICMP头部,IP头部和以太网头部。
process_icmp_echo其余的代码,包括构建ICMP Echo Ping Reply头部,计算头部校验和,与8.4节的实现一致。完成了Echo Ping Reply数据帧的构建够,将Echo Ping Reply数据帧交给IP模块,IP模块判断如果数据帧的总长度超过MTU_SIZE时,对数据帧进行分片,将分片逐个发送给DIY TCP/IP的网络设备模块,接着修改IP模块的ippkt_send函数。

 int ippkt_send(unsigned char *dst, unsigned char proto, void *buf)
 {
     int ret = 0;
     pdbuf_t *pdbuf = (pdbuf_t *)buf;
     unsigned short payload_len = 0;
 
     if (dst == NULL || pdbuf == NULL) {
         ret = -1;
         goto out;
     }
     payload_len = (unsigned short)(pdbuf->end - pdbuf->payload);
     if (payload_len > MTU_SIZE)
         return ipfrag_send(dst, proto, buf);
 
     if ((ret = build_ip_header(dst, proto, pdbuf, 0))) {
         log_printf(ERROR, "failed to build ip header\n");
         goto out;
     }
 
     netdev_tx_pkt(pdbuf);
 out:
     if (ret && pdbuf)
         pdbuf_free(pdbuf);
     return ret;
 }

8.5节已经介绍过ippkt_send的实现,本节扩展实现该函数,发送IP分片。为保持其逻辑清晰,将发送IP分片的功能单独写成一个函数。先计算pdbuf的数据总长度,即上层模块发送的数据帧的长度,当长度超过MTU_SIZE时,调用ipfrag_send函数处理,并返回,其他实现与8.5节一致。
IP数据帧的分片,和分片的发送,均在ipfrag_send函数中,代码实现如下:

 static int ipfrag_send(unsigned char *dst, unsigned char proto, void *buf)
 {
     int ret = 0;
     pdbuf_t *ipfrag = NULL;
     unsigned short ipfrag_len = 0;
     unsigned short left_len = 0;
     unsigned short offset = 0;
     pdbuf_t *pdbuf = (pdbuf_t *)buf;
     iphdr_flags_t ipflags;
 
     if (dst == NULL || pdbuf == NULL) {
         ret = -1;
         goto out;
     }
 
     left_len = (unsigned short)(pdbuf->end - pdbuf->payload);
     while (left_len > 0) {
         ipflags.v = 0;
         ipfrag = pdbuf_alloc(0, !IGNORE_MTU);
         if (ipfrag == NULL) {
             log_printf(ERROR, "No memory for ip fragment\n");
             ret = -1;
             goto out;
         }
         ipfrag_len = MTU_SIZE - sizeof(iphdr_t);
         if (left_len < ipfrag_len)
             ipfrag_len = left_len;
         pdbuf_push(ipfrag, ipfrag_len);
         pdbuf_pop(pdbuf, ipfrag->payload, ipfrag_len);
         left_len -= ipfrag_len;
         if (left_len)
             ipflags.b.more_frag = 1;
         ipflags.b.offset = (offset >> 3);
         log_printf(INFO, "fragmentation, more: %u, len: %u, offset: %u\n", ipflags.b.more_frag, ipfrag_len, offset);
         if ((ret = build_ip_header(dst, proto, ipfrag, ipflags.v))) {
             pdbuf_free(ipfrag);
             log_printf(ERROR, "failed to build ip header\n");
             goto out;
         }
         netdev_tx_pkt(ipfrag);
         offset += ipfrag_len;
     }
     
 out:
     if (pdbuf)
         pdbuf_free(pdbuf);
     return ret;
 }

ipfrag_send的函数参数和返回值与ippkt_send一致,函数的入参由ippkt_send直接传入,分别为: dst,目的IP地址,proto上层协议类型,buf指针,指向要发送的上层模块数据的首字节地址。
Line 3-14: 先介绍下函数内部使用的局部变量,ipfrag指针,类型为pdbuf_t *,在对上层模块数据分片时,动态分配内存,存放分片数据。ipfrag_len,分片的长度,只包含上层模块数据分片的长度,不包括分片IP头部的长度。left_len,分片循环结束条件,初始值为上层模块数据长度。offset存放即将发送的IP分片的偏移,初始值为0,pdbuf指向上层模块数据的pdbuf,ipflags,方便构造分片IP头部的flags字段。在继续执行之前,先对dst和buf进行空指针检查。
Line 15-24: 计算上层模块的数据长度,用于初始化left_len变量,在left_len不为0时,进入while循环进行分片处理。ipflags用于构建分片的IP头部中的flags字段,初始化为0。pdbuf_alloc申请内存空间,存放分片数据,pdbuf_alloc的第一个参数为0,第二个参数是不能忽略MTU_SIZE的限制。
回顾6.3节pdbuf_alloc的实现,pdbuf_alloc的第一个参数为0时,申请(MTU_SIZE – RESERVED_HEADER_SZ) + RESERVED_HEADER_SZ + sizeof(ethhdr_t) + sizeof(pdbuf_t)字节的内存,实际大小是1500 + 14 + sizeof(pdbuf_t),pdbuf_t在32位和64为系统上的大小不一样,sizeof(pdbuf_t)用于存放pdbuf_t结构体成员,所以不必关心sizeof(pdbuf_t)的长度。1514字节,可以存放以太网数的最大协议数据单元。pdbuf_alloc成功时继续执行,否则结束循环,出错返回。
Line 25-29: 将ipfrag_len初始值设为1500 – 20字节,ipfrag_len在1480和尚未分片的上层协议数据长度left_len之间取最小值。调用pdbuf_push将ipfrag->payload指针向地址减小的方向移动ipfrag_len个字节。再调用pdbuf_pop从上层模块数据的pdbuf中弹出ipfrag_len个字节的数据,存放在ipfrag->payload指向的内存中。
再回顾一下6.4节pdbuf_pop的实现,pdbuf_pop通过memcpy,将指定长度的数据,从指定的原始内存空间拷贝到目标内存空间。此处指定的原始内存空间是,上层模块数据的pdbuf->payload,目标内存空间是ipfrag->payload,拷贝完成后,将上层模块数据的pbuf->payload向地址增大的方向移动ipfrag_len个字节。其实就是从pdbuf中提取ipfrag_len个字节的数据,存放在ipfrag->payload指向的内存中,将上层模块的数据分片。首次分片时上层模块的pdbuf->payload指向上层模块数据的首字节地址,pdbuf_pop之后,pdbuf->payload始终指向即将分片的上层模块数据的首字节地址。
Line 30-34: 参与分片的上层模块数据总长度left_len减去本次分片的长度ipfrag_len,如果left_len大于0,表明还有数据需要分片,即本次分片后还有后续分片,因此将本次分片IP头部的more_frag字段设置为1,对应的offset向右移动3位,即除以8,赋值给分片IP头部的offset字段。offset初始值为0,第一个分片的offset为0,每发送一个分片,offset的长度增加ipfrag_len,即offset的值是下一个即将发送的分片的偏移量。Line239打印分片的长度,偏移量,以及more_frag信息,用于debug。
Line 35-43: 此时分片数据已经存放在ipfrag中,调用build_ip_header为分片添加IP头部和以太网头部,build_ip_header的参数为,目标IP地址,上层协议类型,分片数据,分片IP头部的flags值,build_ip_header根据flags值为属于同一个IP数据帧的分片分配同一个IP identification,并计算IP头部校验和。为分片添加IP头部和以太网头部后,调用网络设备模块的netdev_tx_pkt函数,将IP分片加入DIY TCP/IP网络设备模块的发送队列等待调度发送。如果build_ip_header失败,则释放分片占用的内存,成功时,回顾5.4.2和7.3节,网络设备模块对发送数据帧的处理,由网络设备模块同一释放发送数据帧占用的内存。Line246更新offset值,offset增加的值为本次分片的数据长度(不包括分片的IP头部)ipfrag_len。
Line 44-48: 释放上层模块的pdbuf,由于真正发送的数据是动态分配的ipfrag,上层模块的pdbuf并没有交给DIY TCP/IP的网络设备模块,所以ipfrag_send返回之前,需要释放上层模块的pdbuf占用的内存。
本机除了修改ippkt_send函数,另外还修改了build_ip_header,分片循环中已经用到了build_ip_header,也提到了build_ip_header根据分片的IP头部的flags值,为属于同一个IP数据帧的IP分片分配相同的id,接下来看build_ip_header的修改。

 static int build_ip_header(unsigned char *dst, unsigned char proto,
                 pdbuf_t *pdbuf, unsigned short flags_offset)
 {
     int ret = 0;
     iphdr_t *iphdr = NULL;
     unsigned char *local_ip = NULL;
     unsigned short payload_len = 0;
     static unsigned short fragment_id = 0;
     iphdr_flags_t ipflags;
 
     local_ip = netdev_ipaddr();
     if (local_ip == NULL) {
         ret = -1;
         goto out;
     }
 
     ipflags.v = flags_offset;
     payload_len = (unsigned short)(pdbuf->end - pdbuf->payload);
     /* build ip header */
     pdbuf_push(pdbuf, sizeof(iphdr_t));
     iphdr = (iphdr_t *)pdbuf->payload;
     iphdr->hdr_len = IP_HDR_LEN;
     iphdr->ver = IP_VERSION;
     iphdr->tos = 0;
     iphdr->total_len = HSTON(payload_len + sizeof(iphdr_t));
     if (ipflags.b.offset == 0 && ipflags.b.more_frag == 1) {
         get_random(&iphdr->id, sizeof(iphdr->id));
         fragment_id = iphdr->id;
     } else  if ((ipflags.b.offset != 0 && ipflags.b.more_frag == 1) ||
             (ipflags.b.offset != 0 && ipflags.b.more_frag == 0)) {
         iphdr->id = fragment_id;
     } else  if (ipflags.b.more_frag == 0 && ipflags.b.offset == 0) {
         iphdr->id = get_random(&iphdr->id, sizeof(iphdr->id));
     } else {
         log_printf(ERROR, "invalid flags_offset: %02x\n", flags_offset);
         ret = -1;
         goto out;   
     }
     iphdr->id = HSTON(iphdr->id);
     iphdr->flags_offset = HSTON(flags_offset);
     iphdr->ttl = 64;
     iphdr->proto = proto;
     iphdr->hdr_cksum = 0;
     memcpy(iphdr->src_ip, local_ip, sizeof(iphdr->src_ip));
     memcpy(iphdr->dst_ip, dst, sizeof(iphdr->dst_ip));
     iphdr->hdr_cksum = cksum(0, iphdr, sizeof(iphdr_t), BENDIAN);
     iphdr->hdr_cksum = HSTON(iphdr->hdr_cksum);
     //dump_buf(iphdr, sizeof(iphdr_t));
     ret = build_ethernet_header(pdbuf, ETHERNET_IP,
                 local_ip, iphdr->dst_ip,
                 NULL, NULL);
 out:
     return ret;
 }

8.5节已经介绍过build_ip_header的实现,本节扩展该函数。build_ip_header根据第四个入参flags值,判断如果是第一个IP分片,即more_frag为1且offset为0,则通过get_radom获取一个2个字节的随机值,做为第一个分片到最后一个分片公用的IP头部id,并存放在静态变量fragment_id中。如果more_frag为1且offset不为0,则是中间分片,不再重新获取id,直接将IP头部的id赋值为fragment_id,如果more_frag为0且offeset不为0则表明是最后一个分片,同样将IP头部的id赋值为fragment_id。如果more_frag为0且offset为0,则说明该IP数据帧不是分片,则调用get_radom为其分配id。如果flags值不符合上诉所有情况,则返回出错。
确定IP头部的id后,继续执行,将id转换为大端格式,build_ip_header其余部分的代码与8.5节的一致。
编译运行,查看测试结果,测试方法,运行DIY TCP/IP的主机记为A,与主机A处于同一局域网的主机B上设置PING的数据长度为5000,ping局域网中不存在的IP地址192.168.0.7,将该IP设置为DIY TCP/IP的虚拟IP地址。如果主机B的Large Packet Ping能够得到主机A的正确回复,则说明本节的IP分片,和分片发送的代码实现是正确的。
DIY TCP/IP Large Packet Ping Success
上图是主机B的PING log,首先是不加-l参数的ping,确保本节添加的IP分片的代码不会影响到8.6节之前实现的代码。从ping虚拟IP地址192.168.0.7的结果来看,主机B发出的4个Echo Ping Request均收到了来自虚拟IP地址192.168.0.7的回复,非large packet ping的运行结果并未收到影响。再限定-l参数,指定Echo Ping Request的数据长度为5000,Larget Packet Ping 虚拟IP地址192.168.0.7也是成功的,4个Large Echo Ping Request均收到了来自虚拟地址192.168.0.7的正确回复,Echo Ping Reply数据帧的数据长度也是5000。
DIY TCP/IP Large Packet Ping Runtime log0
上图是主机A上DIY TCP/IP的运行Log,首先是对4帧非Larget Packet Ping Request进行了回复,从ICMP的sequence number可以看出,非Larget Packet Ping的4个Echo Ping Request的Sequence number分别是9,10,11,12,紧接着12,主机B发出了Sequence Number为13,14,15,16的Large Echo Ping Request。以13为例,说明一下本节IP分片的运行Log。
DIY TCP/IP的IP模块收到Squence Number为13的Larget ICMP Echo Ping Request时,先是重组IP分片,重组后的IP分片的总长度为5028,包括20个字节的IP头部,8个字节的ICMP头部,和5000个字节的Echo Ping Request数据,与8.6节已经介绍过的IP分片重组的运行log一致。将重组后的IP数据帧交给ICMP模块,ICMP头部校验和检验通过,打印出ICMP的头部信息,可以看到ICMP头部的是Sequence Number是13,ICMP模块处理收到的Echo Ping Request,将5000个字节的数据部分copy到Echo Ping Reply的数据部分,再添加ICMP头部,计算ICMP头部校验和后,将5008字节的ICMP 数据帧交给IP模块发送。
IP模块对收到的ICMP Echo Ping Reply分片,分片数据部分的长度分别是3个1480,1个568,刚好是5008字节,前3三个分片IP头部中的more_frag都被置位1,最后一个分片的IP头部的more_frag置为0。Offset是乘以8之后的打印,分别是0,1480,2960和4440,全部都符合预期。为每个分片添加IP头部,计算头部校验和, 并通过DIY TCP/IP的网络设备模块发送。发送完成之后icmp_recv函数返回,最终返回到ippkt_recv函数中,释放重组IP分片时分配的reassemble_pdbuf。主机B能正确接收到Larget Echo Ping Reply的回复,说明本节的IP分片,和分片的IP头部的构造是正确的。
DIY TCP/IP Large Packet Ping Runtime log1
DIY TCP/IP Large Packet Ping Runtime log2
DIY TCP/IP回复了4个Large Packet Ping Reply后,键入ctrl+c结束运行,查看pdbuf模块打印log,一共分配了29个pdbuf,释放了29个pdbuf。理清一下pdbuf的分配和释放,收到ARP Request时,回复ARP Reply,需要1个pdbuf。收到4个正常的Echo Ping Request,回复4个Echo Ping Reply需要4个pdbuf。每收到一个Large Echo Ping Request需要1个reassemble_pdbuf,回复Echo Ping Reply需要1个pdbuf存放Echo Ping Reply数据。Echo Ping Reply到IP模块后又需要4个pdbuf存放分片数据,所以每回复一个Large Echo Ping Request,需要6个pdbuf。所以一共是1 + 4 + 6 * 4 = 29,如pdbuf模块的运行Log显示分配了29个pdbuf,相应的也释放了29个pdbuf,没有内存泄漏。
8.9 小结
本章首先介绍了IP数据帧和ICMP数据帧的帧结构。代码实现介绍到这里,相信朋友们一定对DIY TCP/IP的各个模块的划分和实现有了较深的印象。网络设备模块的接收逻辑,从链路层接收数据帧,封装后交给网络设备模块的接收队列,由接收线程解析,剥去以太网头部后,调用上层模块的接收函数处理。网络设备模块的发送逻辑,负责将上层模块的数据帧封装后加入发送队列,由发送线程调度,通过PF_PACKET域的sock发送给指定网络接口的Linux 驱动。ARP模块负责ARP 数据帧的接收和发送,pdbuf模块负责接收和发送过程中内存的分配和释放。
本章在这些模块的基础上实现了ICMP模块和IP模块,IP模块负责IP数据帧的接收和发送,剥去IP头部后,调用上层模块接收函数处理。IP模块也具备重组IP分片和将上层模块发送的长度大于MTU SIZE的数据帧分片的能力。ICMP模块负责ICMP Echo (Ping) Request和ICMP Echo (Ping) Reply的接收和发送。
在介绍这些内容的实现时,utils模块又引入了校验和的计算函数cksum。Cksum函数既可以用于对接收到的数据帧的校验和的检验,也可以计算发送数据帧的校验和。构造IP头部时,引入了get_random,从linux kernel的/dev/urandom设备读取随机字节流。同时本章的测试结果验证了IP模块已经可以完成IP数据帧的正确接收接收和发送,IP分片的重组和构建。ICMP模块已经完成了ICMP Echo (Ping) Request/Reply数据帧的正确接收和发送。
至此DIY TCP/IP可以支持ARP 数据帧的接收和发送,IP数据帧的接收和发送,IP分片的重组和构建,ICMP数据帧的接收和发送。同时也具备ARP表的建立和基于ARP表的简单路由查询的功能。下一章介绍DIY TCP/IP的TCP模块的实现。
目录结构
DIY TCP/IP End of ICMP and IP Chapter
下一篇:DIY TCP/IP TCP模块的实现0

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值