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

上一篇:DIY TCP/IP IP模块和ICMP模块的实现0
8.2 IP数据帧的接收
本节实现DIY TCP/IP的IP数据帧的接收,6.1节介绍pdbuf模块时已经引入了IP头部结构体的定义,iphdr_t数据结构定义在ip.h头文件中。本节围绕该数据结构的定义,实现IP模块对IP数据帧的解析,IP头部校验和的检验,并根据IP头部的上层协议字段将IP数据帧分发给DIY TCP/IP的对应模块处理。本节的测试方法是通过接收ICMP Echo Request数据帧,DIY TCP/IP的网络设备模块根据以太网头部的类型字段将IP数据帧分发给IP模块,IP模块判断目的地址是DIY TCP/IP的虚拟IP地址,检验IP头部校验和正确后,将IP数据帧分发给ICMP模块,打印出接收到ICMP数据帧的信息。
先来看ip.h头文件的内容

 #ifndef _IP_H_
 #define _IP_H_
 
 #define IP_VERSION       4
 #define IP_HDR_LEN       5
 #define IP_FRAG_OFFSET       0x1FFF
 #define REASSEMBLE_BUF_SZ    (8 * 1024 * 8 + 1500 - 20)
 
 #define IP_PROTO_ICMP    0x01
 #define IP_PROTO_TCP 0x06
 #define IP_PROTO_UDP 0x11
 
 /* ipv4 header, 20 bytes */
 typedef struct _iphdr {
  /* version and header length */
  unsigned char hdr_len:4;
  unsigned char ver:4;
  /* type of service */
  unsigned char tos;
  /* total length: hdr_len + payload len */
  unsigned short total_len;
  /* identification */
  unsigned short id;
  /* flags and fragment offset */
  unsigned short flags_offset;
  /* time to live */
  unsigned char ttl;
  /* payload protocol */
  unsigned char proto;
  /* header checksum */
  unsigned short hdr_cksum;
  /* source ip address */
  unsigned char src_ip[4];
  /* destination ip address */
  unsigned char dst_ip[4];
 } __attribute__((packed)) iphdr_t;
 
 int ippkt_recv(unsigned char *pkt, unsigned int sz);
 
 #endif

Line 1-11: 将IP模块中用到的常量定义成对应的宏,IP协议的版本,IP头部的长度,IP分片偏移的掩码,重组IP分片用到的Buffer大小,以及IP头部的上层协议类型。IP_FRAG_OFFSET和REASSEMBLE_BUF_SZ在IP分片的重组和发送章节介绍,本节暂时略过。
Line 13-36: iphdr_t数据结构的定义,与8.1节介绍的IP头部结构一致。
Line 38: ippkt_recv接收IP数据帧,第一个参数pkt指向IP数据帧的起始字节地址,sz是IP数据帧的长度,包括IP头部和IP Payload。ippkt_recv函数是IP模块暴露给其他模块使用的接口函数,网路设备模块判断以太网头部类型字段是IP类型(0x0800)时,通过ippkt_recv接收IP数据帧。
先来看网路设备模块dev_process_rxpkt的修改,再来介绍ippkt_recv的实现。

  static void dev_process_rxpkt(net_device_t *ndev, dev_rxpkt_t *rxpkt)
  {
      ethhdr_t *ethpkt = NULL;
      unsigned short ethtype = 0;
      void *uplayer_pkt = NULL;
  
      if (ndev == NULL || rxpkt == NULL)
          return;
      ethpkt = (ethhdr_t *)rxpkt->payload;
          log_printf(VERBOSE, "dev rx, ethernet type: %04x, "MACSTR " --> " MACSTR"\n",
          NTOHS(ethpkt->type), MAC2STR(ethpkt->src), MAC2STR(ethpkt->dst));
      ethtype = NTOHS(ethpkt->type);
      uplayer_pkt = strip_header(rxpkt->payload, sizeof(ethhdr_t));
      switch (ethtype) {
          case ETHERNET_IP:
              ippkt_recv(uplayer_pkt, (rxpkt->len - sizeof(ethhdr_t)));
              break;
          case ETHERNET_ARP:
              arp_recv(uplayer_pkt, (rxpkt->len - sizeof(ethhdr_t)));
              break;
          default:
              break;
      }
      free(rxpkt);
  }

网络设备模块的接收线程从接收队列中取出数据帧后,调用dev_process_rxpkt处理dev_rxpkt_t数据帧,line79将以太网头部剥去,其实就是将rxpkt->payload + sizeof(ethhdr_t)的指针赋值给uplayer_pkt,根据以太网头部类型,如果是0x0800,则调用ippkt_recv完成IP数据帧的接收,uplayer_pkt指向IP头部第一个字节地址处,长度为以太网数据帧的长度减去以太网头部的长度。dev_process_rxpkt的其余代码与7.2节一致。
ippkt_recv的实现在本节新增C文件ip.c中。

 #include 
 
 #include "ip.h"
 #include "debug.h"
  #include "device.h"
 #include "icmp.h"
 #include "common.h"
 
 int ippkt_recv(unsigned char *pkt, unsigned int sz)
 {
  int ret = 0;
  unsigned char *local_ip = NULL;
  iphdr_t *ippkt = NULL;
 
  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;
  }
  /* ip check sum */
  switch (ippkt->proto) {
      case IP_PROTO_ICMP:
          icmp_recv(pkt, sz);
          break;
      case IP_PROTO_TCP:
          break;
      case IP_PROTO_UDP:
          break;
      default:
          ret = -1;
          break;
  }
 out:
  return ret;
 }

Line 9-18: ippkt_recv第一个参数pkt指针,指向IP数据帧的起始字节地址,sz是IP数据帧的长度。判断入参pkt不空且sz不为0时继续执行。
Line 19-29: 调用网络设备模块netdev_ipaddr获取DIY TCP/IP的虚拟IP地址,判断IP头部的目标IP地址是否与虚拟IP地址相等。ippkt_recv不负责转发IP数据帧,只处理目标IP是发送给虚拟IP地址的IP数据帧。
Line 30-34: 计算IP头部的校验和,IP头部校验和是16位无符号整型值,由发送端计算完成之后填入IP头部,接收端需按照如下步骤计算IP头部校验和:

  1. 将20字节的IP头部每两个字节组成一个16位无符号整型值,校验和字段也包括在内,进行两两二进制相加 。
  2. 最高位有进位时,将进位加到累加结果的最低位。
  3. 最后将累加结果取反,取反后的结果为0,则校验和是正确的,否则校验和出错。有关校验和更详细的解释请参考RFC1071。
    cksum是utils.c模块的新增函数,用于计算ICMP,IP和TCP头部的校验和。
    Line 35-50: 根据IP头部的协议字段判断IP Payload上层封包的类型,并调用对应模块的接收函数将IP数据帧交给对应模块处理,本节添加了icmp_recv的调用,接收ICMP数据帧。
    接下来看cksum在ultils.c中的实现
 unsigned short cksum(unsigned short init_val, void *data, unsigned int sz, int big_endian)
 {
     unsigned short check_sum = 0;
     unsigned char *p = NULL;
     unsigned short tmp = 0;
     unsigned int i = 0;
 
     if (data == NULL || sz == 0) {
         printf("Invalid data or size to calculate checksum\n");
         return ~check_sum;
     }
     p = data;
     check_sum = init_val;
     if (sz < 2) {
         tmp = *p;
         if (big_endian)
             tmp <<= 8;
         check_sum += tmp;
         goto out;
     }
     for (i = 0; i < (sz - sz % 2); i += 2) {
         tmp = *((unsigned short *)&p[i]);
         if (big_endian)
             tmp = NTOHS(tmp);
         check_sum += tmp;
         if (check_sum < tmp) {
             /* Add carry to low address byte */
             check_sum += 1;
         }
     }
     if (sz % 2) {
         tmp = p[i];
         if (big_endian)
             tmp <<= 8;
         check_sum += tmp;
         if (check_sum < tmp) {
             /* Add carry to low address byte */
             check_sum += 1;
         }
     }
 out:
     return ~check_sum;
 }

cksum函数函数有4个入参,init_val指定校验和的初始值,一般情况下为0,在计算TCP伪首部的过程中,init_val不为0。参与校验和计算的数据看成是unsigned char类型的数组data[],data指向unsigned char数组的首字节的地址,sz代表unsigned char数组的大小。big_endian标识unsgined char数组中的数据是否是大端格式,big_endian为1代表大端,为0代表小端。从网络上接收的数据帧都是大端格式的,所以big_endian一般情况都设置为1。返回值为取反后的校验和,类型是16位无符号整型值。
Line 8-20: 判断data不为空且sz不为0时,继续向下执行。如果sz小于2,则说明只有一个字节需要参与计算校验和,将*data转换成unsigned short类型赋值给中间变量tmp,再根据big_endian标识判断是否需要将tmp转换为小端类型,将tmp累加到cksum上,取反,返回。
Line 21-30: 遍历unsigned char数组data,步长为2,如果sz为偶数,则可以全部遍历到,如果sz为奇数,则最后一个字节单独补0处理。将data中每两个字节转换成一个16位无符号整形值,根据big_edian将16位无符号整型值转换为小端类型,累加到cksum变量上,如果有溢出,即cksum < tmp,则说明最高位有进位,此时将最高位的进位1,累加到溢出后的cksum上,继续累加下一个16位无符号整形值。
Line 31-40: 如果sz为偶数,该段处理不会执行,如果sz为奇数,则将data数组中的最后一个字节赋值给tmp,相当于在最后一个字节后面补0,根据big_edian判断是否将tmp转换为小端,再将转换后的tmp累加到cksum上,再次判断最高位是否有进位,并将最高位进位累加到溢出后的cksum上。
Line 41-42: 将累加得到的校验和cksum取反,返回。
cksum既可以计算校验和,也可以检验校验和。做为计算使用时cksum返回值不为0, 做为检验使用时,由于发送端的校验和字段也包含在data数组内,返回值为0。
再来看ippkt_recv函数中调用的icmp_recv的实现,本节只是完成IP数据帧的接收,此处的icmp_recv只是简单的打印收到ICMP数据帧,icmp_recv的实现在新增文件icmp.c中,下节实现ICMP Echo Request数据帧的接收和解析。

  #include "icmp.h"
  #include "debug.h"
  
  int icmp_recv(unsigned char *pkt, unsigned int sz)
  {
   int ret = 0;
   log_printf(INFO, "ICMP RX\n");
   return ret;
  }

修改Makefile,编译新增文件ip.c和icmp.c,并将目标文件链接到tcp_ip_stack。

 tcp_ip_stack:device.o init.o debug.o arp.o utils.o pdbuf.o ip.o icmp.o
  gcc -o tcp_ip_stack device.o init.o debug.o arp.o utils.o pdbuf.o ip.o icmp.o -lpcap -lpthread
 device.o:device.c device.h init.h common.h
  gcc -c device.c
 init.o:init.c init.h
  gcc -c init.c
 debug.o:debug.c debug.h
  gcc -c debug.c
 arp.o:arp.c arp.h
  gcc -c arp.c
 utils.o:utils.c utils.h
  gcc -c utils.c
 pdbuf.o:pdbuf.c pdbuf.h
  gcc -c pdbuf.c
 ip.o:ip.c ip.h
  gcc -c ip.c
 icmp.o:icmp.c icmp.h
  gcc -c icmp.c
 clean:
  rm -rf *.o
  rm -rf tcp_ip_stack
  

编译

gannicus@ubuntu:~/guojia/tasks/DIY_USER_SPACE_TCPIP/ch5/0$ make
gcc -c device.c
gcc -c init.c
gcc -c debug.c
gcc -c arp.c
gcc -c utils.c
gcc -c pdbuf.c
gcc -c ip.c
gcc -c icmp.c
gcc -o tcp_ip_stack device.o init.o debug.o arp.o utils.o pdbuf.o ip.o icmp.o -lpcap -lpthread

与验证ARP Request数据帧的接收一样,运行DIY TCP/IP的主机记为A,设置DIY TCP/IP的虚拟IP地址为192.168.0.7(局域网中不存在的IP地址),与本机处于同一局域网的另外一台主机记为B,在主机B上PING 192.168.0.7,运行结果如下:
DIY TCP/IP IP模块和ICMP模块的实现1_第1张图片
上图可以看到主机B PING 192.168.0.7的结果是失败的,请求超时。再来看主机A上DIY TCP/IP的运行Log:

gannicus@ubuntu:~/guojia/tasks/DIY_USER_SPACE_TCPIP/ch5/0$ 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
ICMP RX
ICMP RX
^Cpcap_loop ended
Network device deinit
Network device RX deinit
Dev rx routine exited
Dev rxq flushed 0 packets
Network device TX deinit
Dev tx routine exited
Dev txq flushed 0 packets
Destroy ARP table, 1 entry

#Internal Buffer Management#
Alloc: 1, Free: 1

从运行日志可以看出DIY TCP/IP先正确接收了ARP Request数据帧,构建ARP表,回复ARP Reply给主机B后,又正确的接收了ICMP Echo Request数据帧,证明本节实现的IP数据帧的接收是正确的,IP头部校验和检验正确,并判断IP头部的协议字段是ICMP协议,然后将IP数据帧交给ICMP模块,最终打印出ICMP RX。
下一篇:DIY TCP/IP IP模块和ICMP模块的实现2

你可能感兴趣的:(DIY,TCP/IP)