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

上一篇:DIY TCP/IP IP模块和ICMP模块的实现1
本节在8.2节的基础上扩展icmp_recv函数,检验接收到的ICMP数据帧的校验和,解析ICMP数据帧头部的type字段,根据ICMP数据帧的类型做相应处理。

  int icmp_recv(unsigned char *pkt, unsigned int sz)
  {
      int ret = 0;
      iphdr_t *ippkt = NULL;
      icmphdr_t *icmppkt = NULL;
      unsigned short icmppkt_len = 0;
      unsigned char icmp_type = 0;
      unsigned char *payload = NULL;
  
      if (pkt == NULL || sz == 0) {
          ret = -1;
          goto out;
      }
  
      ippkt = (iphdr_t *)pkt;
      icmppkt_len = NTOHS(ippkt->total_len) - sizeof(iphdr_t);
      icmppkt = (icmphdr_t *)strip_header(ippkt, sizeof(iphdr_t));
      if (cksum(0, icmppkt, icmppkt_len, BENDIAN)) {
          log_printf(ERROR, "Invalid ICMP checksum\n");
          ret = -1;
          goto out;
      }
      icmp_type = icmppkt->type;
      log_printf(INFO, "Echo (ping) %s, id: %u, seq: %u/%u, ttl=%u\n",
          icmp_type == ICMP_TYPE_ECHO ? "request" : "reply",
          NTOHS(icmppkt->id), NTOHS(icmppkt->seq), icmppkt->seq,
          ippkt->ttl);
      dump_buf(icmppkt, icmppkt_len);
      switch(icmp_type) {
          case ICMP_TYPE_ECHO:
              ret = process_icmp_echo(ippkt, icmppkt);
              break;
          case ICMP_TYPE_ECHO_REPLY:
              ret = process_icmp_echo_reply();
              break;
          default:
              log_printf(WARNING,
                  "Unhandled ICMP PKT, type: %u\n", icmp_type);
              ret = -1;
              goto out;
      }
  out:
      return ret;
  }


Line 1-13: icmp_recv函数的参数pkt指向IP数据帧的首字节地址,sz是IP数据帧的长度,包括IP头部和IP Payload。pkt的类型是void *,没有定义成iphdr_t *类型,因为icmp_recv函数是ICMP模块暴露给其他模块使用的函数接口,icmp_recv函数声明在icmp.h头文件中,如果定义icmp_recv的入参pkt为iphdr_t *类型,就需要在icmp.h头文件中引入ip.h头文件,造成ICMP模块和IP模块头文件的循环引用。读者还会在其他模块的实现中看到类似的void *类型的函数入参的定义,都是为了避免头文件的循环引用和增加模块之间的独立性。
Line 14-17: 判断入参pkt不为空且sz不为0时继续执行,将pkt强制转换为iphdr_t *类型,根据IP首部的total_len字段计算ICMP数据帧的长度,strip_header将IP数据帧的头部剥去后赋值给icmppkt,strip_header之前已经介绍过,将ippkt指针转换为unsigned char *类型,再加上sizeof(iphdr_t)。
Line 18-22: 检验ICMP数据帧的校验和,8.1节中介绍的ICMP的头部数据和ICMP的payload数据都要参与计算校验和,icmppkt指向的ICMP数据帧的头部中包含发送方的校验和数据,所以此处检验校验和的结果应当为0,否则认为接收到的ICMP数据帧不合法。
Line 23-44: 校验和检验通过后,打印输出接收的到ICMP数据帧的头部信息,dump_buf做为debug使用,输出Echo (Ping) Request数据帧的内容。根据ICMP头部中的type判断,接收到的ICMP数据帧是Echo Ping Rquest或者是Echo Ping Reply,再交给process_icmp_echo和process_icmp_echo_reply函数处理。
8.4 ICMP数据帧的发送
本节在8.3节的基础上扩展process_icmp_echo函数,构造ICMP Echo (Ping) Reply数据帧,复制ICMP Echo Ping Request数据帧的payload数据,计算ICMP数据帧的校验和,将构造好的ICMP Echo Ping Reply数据帧交给IP模块发送。在介绍代码实现之前,先来通过wireshark抓取PING的数据帧交互过程,查看如何构造ICMP Echo (Ping) Reply数据帧。
DIY TCP/IP Ping Router Sample
上图是本人所在局域网中的一台windows主机PING路由器的交互过程,192.168.0.105发出Echo (ping) Request,路由器192.168.0.1回复Echo (ping) Reply。
Echo (ping) Requset数据帧
DIY TCP/IP Ping Request
Echo (Ping) Request数据帧头部结构已经在8.1节详细介绍过,data部分含有32个字节的数据,从wireshark的raw byte数据显示可以看出,32个字节的data是ascii码小写的a到w,再重复小写的a到i。
Echo (ping) Reply数据帧
DIY TCP/IP Ping Reply
Echo (ping) Reply的identifier,sequence number与Echo (ping) Request相等,在8.1节已经介绍过,RFC792 ICMP协议中有说明,ICMP头部code为0时,Echo ping Request和Echo Ping Reply的identifier,sequence number字段分别相等。表示Echo Ping Reply回复对应的Echo ping request数据帧。再来看数据部分,32个字节的数据也是相等的,与ICMP Echo数据帧的名字相呼应,接收到Echo (Ping) Request数据帧时,将数据原封不动的Echo回去。Echo (Ping) Reply除了type和校验和部分与Echo (Ping) Request不相等之外,其余部分均相等。弄清楚Echo (Ping) Reply数据帧的回复规则后,再来看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;
 
  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);
  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 = HSTON(icmp_seq ++);
  echo_reply->cksum = cksum(0, echo_reply,
              data_len + sizeof(icmphdr_t), BENDIAN);
  echo_reply->cksum = HSTON(echo_reply->cksum);
  dump_buf(echo_reply, data_len + sizeof(icmphdr_t));
 out:
  return ret;
 }
 
 static int process_icmp_echo_reply()
 {
  int ret = 0;
  return ret;
 }

process_icmp_echo函数的入参是ippkt指针和icmppkt指针,分别指向IP数据帧的首字节地址,和ICMP数据帧的首字节地址。之所以需要IP数据帧,是因为ICMP Echo (Ping) Reply数据帧构造完成之后,需要将其交给IP模块的发送函数处理,发送的目标IP地址就是ICMP Request数据帧的IP头部中的源IP地址。
Line 1-16: 判断ippkt和icmppkt指针都不为空时继续执行,虽然icmp_recv到process_ecmp_echo的调用,ippkt和icmppkt指针必然不为空,本人编写C语言习惯先检查指针的合法性,再使用指针。调用strip_header将ICMP数据帧的头部剥去,返回的指针赋值给echo_req_data,echo_req_data指向ICMP Echo (Ping) Request的数据部分的首字节地址。通过IP头部的total_len字段减去IP头部长度和ICMP头部长度,得到Echo (ping) Request数据部分的长度。
Line 17-21: pdbuf_alloc申请长度为data_len的buffer,6.4节已经介绍过pdbuf_alloc申请的内存空间包括data_len,Layer3和Layer4最大的协议头部,以太网头部和pdbuf_t描述符本身占用的全部内存空间。pdbuf_alloc的第二个参数是!IGNORE_MTU,表示最大申请1500字节的内存空间。介绍IP分片的重组时,会再次改写该函数,重组后的IP分片,封装的ICMP数据帧的长度会超过1500字节。
Line 22-26: pdbuf_push调整pdbuf->payload向地址减小的方向移动data_len个字节,首先将Echo Request的数据部分复制到pdbuf->payload指向的内存空间。再将pdbuf->payload向地址减小的方向移动sizeof(icmphdr_t)个字节,用于存放ICMP的头部数据。将pdbuf->payload指针强制转换为icmphdr_t *类型,用于构造ICMP头部数据。
Line 27-38: type为0x0,code为0x0,cksum字段先赋值为0,最后计算校验和的数值。Identifier和sequence number都来自Echo (ping) Request数据帧,已经是大端格式,所以不需要再次转换为大端。计算校验和,参与计算的数据包括Echo (ping) Reply的头部数据和Payload数据。校验和计算完成后再转换为大端格式填入Echo (ping) Reply数据帧的头部cksum字段中。dump_buf做为debug使用,在将Echo (ping) Reply数据帧交给IP模块发送之前,先通过dump_buf查看构造的数据帧是否符合预期。
与8.2的测试方法一致,运行DIY TCP/IP的主机记为A,与主机A在同一个局域网的主机B上Ping主机A,8.3节在完成ICMP数据帧的校验和检查后,dump_buf打印Echo Ping Request数据帧的内容,本节在ICMP数据帧的校验和计算完成后,dump_buf打印回复构造的Echo Ping Reply数据帧的内容。
主机B PING 192.168.0.7,该IP地址是DIY TCP/IP的虚拟IP地址(局域网中不存在),本节只是构造了ICMP Echo Ping Reply,并没有将数据帧交给IP模块发送,所以主机B上PING的结果仍然是失败的。
DIY TCP/IP Ping Result
再来看DIY TCP/IP的运行结果

gannicus@ubuntu:~/guojia/tasks/DIY_USER_SPACE_TCPIP/ch5/2$ sudo ./tcp_ip_stack -i 192.168.0.7
[sudo] password for gannicus: 
Network device init
filter: ether proto 0x0800 or ether proto 0x0806
Network device RX init
Network device TX init
Net device ip address: 192.168.0.7
192.168.0.7 is at 00:0c:29:2e:0a:ed
ARP Table
IP Address      MAC Address
192.168.0.105       8c:a9:82:11:d1:de
Echo (ping) request, id: 1, seq: 16/4096, ttl=64
08 00 4d 4b 00 01 00 10 61 62 63 64 65 66 67 68
69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 61
62 63 64 65 66 67 68 69 
Echo (ping) reply, id: 1, seq: 16/4096, ttl=64
00 00 55 4b 00 01 00 10 61 62 63 64 65 66 67 68
69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 61
62 63 64 65 66 67 68 69 
Echo (ping) request, id: 1, seq: 17/4352, ttl=64
…

从运行结果可以看出,ICMP模块接收到了Echo (PIing) Request,并且校验和检验成功,Echo (ping) request的type为0x08,0x0为code,4d 4b 为大端格式的校验和,00 01是大端格式的identification,00 10也是大端格式的sequence number,从61向后,一直到第40个字节是Echo (ping) Request的数据内容。
再来看本节构造的Echo (Ping) Reply,type为0x00,0x0为code,55 4b是大端格式的校验和,identification和sequence number与Echo (Ping) Request相等,后面的数据部分也相等,运行结果符合预期。
下一篇:DIY TCP/IP IP模块和ICMP模块的实现3

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值