DIY TCP/IP TCP模块的实现1

上一篇:DIY TCP/IP TCP模块的实现0
9.3 TCP数据帧的接收
基于9.1和9.2节的介绍,本节实现TCP数据帧的接收。IP模块将上层协议类型为0x06的IP数据帧交给TCP模块,TCP模块根据IP数据帧构造TCP伪头部,剥去IP头部后得到TCP数据帧。将TCP伪首部和TCP数据帧一起计算,检验TCP头部校验和。校验和检验通过则表明DIY TCP/IP能正确接收TCP数据帧。再解析TCP头部的options字段,打印出TCP数据帧头部信息。
测试方法,运行DIY TCP/IP的主机记为A,与A处于同一局域网的另外一台Android 手机设备记为C,主机B上运行iperf TCP client 端,iperf –c -i I -t 43200。iperft命令指定的IP地址是DIY TCP/IP的虚拟IP地址(局域网中不存在的IP地址)。iperf TCP client端与<虚拟IP地址:7000>的TCP Server建立TCP连接。7000是DIY TCP/IP的TCP模块监听的端口号,模拟TCP server。
重复说明一下DIY TCP/IP之所以能收到主机C发出的TCP SYN数据帧,是因为主机C构建封装TCP SYN的IP数据帧时会发出ARP Request 广播帧,查询虚拟IP地址对应的硬件地址。DIY TCP/IP的ARP模块回复该ARP Request,建立虚拟IP地址和主机A上eth0端口硬件地址的映射。主机C得到ARP Reply后,构建的IP数据帧的目标IP地址就是DIY TCP/IP的虚拟IP地址。由于主机A与主机C处于同一局域网中,A的eth0的硬件地址是真实存在的,主机A的eth0网卡收到该以太网帧后,将该数据帧通过PF_PACKET的socket交给libpcap,进而交给运行在Linux kernel用户空间的DIY TCP/IP。主机A上Linux kernel的TCP/IP协议栈不会处理该数据帧,是因为虚拟IP地址对于Linux kernel来说是不存在的,只有DIY TCP/IP处理该虚拟IP地址。
本节的实现目标是,TCP模块可以正确检验TCP校验和,并打印出TCP options的信息。6.1节已经引入了tcp.h头文件,本节基于9.1节TCP options的介绍,新增了TCP options 类型的宏定义,和TCP模块的接口函数tcppkt_recv,先来看tcp.h头文件。

 #ifndef _TCP_H_
 #define _TCP_H_
 
 /* tcp header, 20 bytes, options, 12 bytes */
 typedef struct _tcphdr {
  /* source port */
  unsigned short src_port;
  /* destination port */
  unsigned short dst_port;
  /* sequence number */
  unsigned int seq_num;
  /* acknowledge number */
  unsigned int ack_num;
  /* tcp header length and flags */
  unsigned short flags_len;
  /* window size */
  unsigned short wnd_sz;
  /* checksum */
  unsigned short cksum;
  /* urgent point */
  unsigned short ugtp;
  /* options
  * variable in length, may occupy space at the end
  * of TCP header and are a multiple of 8 bits in
  * length. All options are included in the checksum
  */
  unsigned char options[0];
 } __attribute__ ((packed)) tcphdr_t;
 
 /* tcp options */
 #define TCP_OPT_MSS      0x02
 #define TCP_OPT_SACK     0x01
 #define TCP_OPT_TIMESTAMP    0x08
 #define TCP_OPT_NOP      0x01
 #define TCP_OPT_WSCALE       0x03
 
 int tcppkt_recv(void *pkt, unsigned int sz);
 #endif

Line 4-28: TCP头部数据结构的定义,可选字段options是长度为0的变长数组,sizeof计算tcphdr_t数据结构的长度时,options占用的内存是0字节。tcphdr_t其余的成员与9.1节介绍的TCP头部的必选字段一致。
Line 30-35: TCP options的宏定义,9.1节已经介绍过TCP Option是TLV结构,这些宏定义的数值与9.1节介绍的MSS,SACK,TIMESTAMP,NOP,Window Scale的类型的数值一致。
Line 37: tcppkt_recv是TCP模块暴露给其他模块使用的接口,用于接收和处理TCP数据帧。函数的入参pkt指向IP数据帧的首字节地址,sz为IP数据帧的长度,包括IP头部和IP Payload。pkt定义为void *是为了减少头文件的相互依赖,增加模块独立性。
介绍tcppkt_recv的实现之前,先在IP模块添加对tcppkt_recv的调用。

 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;
…
     //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:
             tcppkt_recv(ippkt, sz);
             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;
 }

Line 14-15: tcppkt_recv的调用,将上层协议类型为0x06的IP数据帧交给TCP模块处理,ippkt指向IP数据帧的首字节地址,sz为IP数据帧的长度。ippkt_recv略去的代码与8.7节一致。
tcppkt_recv的实现在新增文件tcp.c中,先来看tcp.c文件中数据结构的定义。

  #include 
  
  #include "tcp.h"
  #include "ip.h"
  #include "utils.h"
  #include "common.h"
  #include "debug.h"
  
   typedef struct _tcp_pseudo_hdr {
      unsigned char src_ip[4];
      unsigned char dst_ip[4];
      unsigned char rsvd;
      unsigned char proto;
      unsigned short len;
  } tcp_pseudo_hdr_t;
  
  typedef union _tcphdr_flags {
          struct _bitmap {
                  unsigned short fin:1;
                  unsigned short syn:1;
                  unsigned short rst:1;
		          unsigned short psh:1;
		          unsigned short ack:1;
		          unsigned short urg:1;
		          unsigned short rsvd:6;
                  unsigned short hdr_len:4;
          } b;
          unsigned short v;
  } tcphdr_flags_t;

Line 9-15: tpc_pseudo_hdr_t,TCP伪头部数据结构,与9.2节介绍的TCP伪头部的结构一致。
Line 17-29: tcphd_flags_t,union类型,v是value的简写,对应TCP头部中Data offset和flags,共同占用的2个字节的无符号整型值。b是bitmap的简写,高位字节的高4个bit是hdr_len,对应TCP头部结构中的Data Offset,剩下的12个bit从低位到高位依次对应TCP头部中flags字段的定义。将TCP头部中的flags字段定义为数据结构,方便头部flags字段的解析。
tcphdr_t,tcp_pseudo_hdr_t和tcphdr_flags_t三个数据结构都将在tcppkt_recv函数中用到,tcppkt_recv的实现如下:

 int tcppkt_recv(void *pkt, unsigned int sz)
 {
     int ret = 0;
     iphdr_t *ippkt = NULL;
     tcp_pseudo_hdr_t pseudo_hdr;
     unsigned short tcppkt_len = 0;
     unsigned short data_offset = 0;
     unsigned short data_len = 0;
     tcphdr_t *tcppkt = NULL;
     tcphdr_flags_t flags;
 
     if (pkt == NULL || sz == 0) {
         log_printf(ERROR, "TCP receive packet failed,"
                 "invalid parameters\n");
         ret = -1;
         goto out;
     }
     ippkt = (iphdr_t *)pkt;
     tcppkt_len  = NTOHS(ippkt->total_len) - sizeof(iphdr_t);
     tcppkt = strip_header(ippkt, sizeof(iphdr_t));
     /* build tcp pesudo header */
     memset(&pseudo_hdr, 0, sizeof(pseudo_hdr));
     memcpy(pseudo_hdr.src_ip, ippkt->src_ip, sizeof(ippkt->src_ip));
     memcpy(pseudo_hdr.dst_ip, ippkt->dst_ip, sizeof(ippkt->dst_ip));
     pseudo_hdr.proto = ippkt->proto;
     pseudo_hdr.len = HSTON(tcppkt_len);
     /* tcp checksum validation */
     if (tcp_cksum(&pseudo_hdr, tcppkt, tcppkt_len)) {
         log_printf(ERROR, "Invalid TCP checksum\n");
         ret = -1;
         goto out;
     }
     flags.v = NTOHS(tcppkt->flags_len);
     data_offset = flags.b.hdr_len * 4;
     data_len = tcppkt_len - data_offset;
     log_printf(INFO, "RX TCP: Seq=%u, Len=%u\n",
             NTOHL(tcppkt->seq_num), data_len);
     tcp_parse_options(tcppkt->options, data_offset - sizeof(tcphdr_t));
 out:
     return ret;
 }

Line 1-10: tcppkt_recv函数有两个入参,pkt指向IP数据帧首字节地址,sz是IP数据帧的长度,包括IP头部的长度和IP Palyload的长度。先来看一下用到的局部变量,ippkt指向IP数据帧,pseudo_hdr用于构造TCP伪头部,tcppkt_len存放TCP数据帧长度,data_offset存放TCP头部长度,data_len存放TCP数据部分的长度。tcppkt指向TCP数据帧,flags方便解析TCP头部flags字段。
Line 11-20: 判断pkt不为空,sz不为0时继续执行。pkt强制转换为iphdr_t类型,赋值给ippkt,IP头部的total_len减去IP头部长度即TCP数据帧的长度,ippkt剥去IP头部后返回的首字节地址为TCP数据帧的首字节地址,赋值给tcppkt。
Line 21-26: 根据9.2节TCP伪头部的结构构造TCP伪头部,复制IP头部中的源IP地址和目标IP地址到TCP伪头部。IP头部中的上层协议字段赋值给TCP伪头部的协议字段,TCP伪头部中的tcp_len是TCP数据帧的长度,包括TCP头部的长度和TCP Payload的长度,但不包括TCP伪头部的长度。TCP伪头部中的IP地址字段和协议字段均取自IP头部,为大端格式,tcp_len也转换为大端格式,准备计算TCP校验和。
Line 27-32: 调用tcp_cksum检验收到的TCP数据帧的校验和,收到的TCP数据帧的头部包含发送方的TCP校验和。检验的结果为0时,表明校验和正确,如果不为0则丢弃TCP数据帧。tcp_cksum的入参为伪头部指针,TCP数据帧指针,和TCP数据帧的长度。
Line 33-38: 将TCP头部中的flags_len字段转换为小端格式,赋值给flags.v,方便提取TCP头部中的Data offset和各个标志位的值。Data offset乘以4为TCP头部长度,TCP数据帧的长度减去data_offset为TCP数据部分的长度。打印输出TCP头部的sequence number和计算得到的数据部分的长度。再调用tcp_parse_options解析TCP头部中的options字段。tcppkt->options指向TCP数据帧头部选项的首字节地址,data_offset为TCP头部的长度,头部长度减去sizeof(tcphdr_t),即减去TCP头部的必选字段,得到TCP头部包含的所有选项字段的长度。长度为0的options变长数组通过sizeof计算时,占用的空间为0,所以sizeof(tcphdr_t)是TCP头部的必选字段长度。


  static unsigned short tcp_cksum(tcp_pseudo_hdr_t *pseudo_hdr,
                  tcphdr_t *tcppkt, unsigned int sz)
  {
      unsigned short check_sum = 0;
      unsigned short tmp = 0;
  
      tmp = cksum(0, pseudo_hdr, sizeof(tcp_pseudo_hdr_t), BENDIAN);
      tmp = ~tmp;
      check_sum = cksum(tmp, tcppkt, sz, BENDIAN);
      return check_sum;
  }


tcp_cksum是TCP模块的静态函数,只在TCP模块内部使用,3个入参分别是,pseudo_hdr指向TCP伪头部首字节地址,tcppkt指向TCP数据帧首字节地址,sz为TCP头部和与TCP数据部分长度的和。tcp_cksum是对DIY TCP/IP的utils模块中的cksum函数的封装。9.2节已经详细介绍过TCP校验和的计算方法,依照9.2节的计算方法实现代码。
先将TCP伪头部的12个字节通过cksum计算出一个中间值,回顾cksum的实现,cksum返回的结果是取反的,所以将中间值再次取反,得到取反之前的值。
再将TCP数据帧的所有字节通过cksum计算校验和,初始值为tmp中间值,得到的结果即是TCP校验和,直接返回。tcp_cksum基于cksum函数,实现9.2节介绍的TCP校验和的算法。
再来看tcppkt_recv函数用到的tcp_parse_options函数的实现。

  static unsigned char *get_opt(void *options, unsigned short sz,
                  unsigned char opt_type)
  {
      unsigned char *p = NULL;
      unsigned char opt_len = 0;
      unsigned short offset = 0;
  
      if (options == NULL || sz == 0)
          return NULL;
      while (offset < sz) {
          p = (unsigned char *)options + offset;
          /* skip nop option */
          if (*p == TCP_OPT_NOP) {
                  p += 1;
                  offset += 1;
                  continue;
          }
          if (*p == opt_type) {
              p += 2;
              break;
          }
          opt_len = *(p + 1);
          p += opt_len;
          offset += opt_len;
      }
      return p;
  }
 
  static void tcp_parse_options(void *options, unsigned short sz)
  {
      unsigned char *popt = NULL;
      unsigned short MSS = 0;
      unsigned char SACK = 0;
      
      if ((popt = get_opt(options, sz, TCP_OPT_MSS))) {
          MSS = NTOHS(*(unsigned short *)popt);
      }
      if ((popt = get_opt(options, sz, TCP_OPT_SACK))) {
          SACK = 1;
      }
      log_printf(INFO, "MSS=%u, SACK_PERM=%u\n", MSS, SACK);
  }

tcp_parse_options也是TCP模块的静态函数,只在TCP模块内部使用。函数的入参,options指向TCP头部中的选项字段的首字节地址,sz为TCP头部中所有选项字段长度的总和。tcp_parse_options调用get_opt函数,指定需要的option的类型,获取option的TLV结构中的V的指针,再将指针转换为对应的数值。9.1节介绍过TCP头部中的选项,都是TLV结构,但是V的数据类型各不相同,在已知的TCP option的V的数据类型时,通过V的指针,根据已知类型将V的指针转换为对应的数值。这样可以将解析TCP 选项的代码实现统一化。
get_opt的入参由tcp_parse_options函数传入,前两个参数与tcp_parse_options的两个入参相同,最后一个参数是要获取的TCP选项的类型。
options指向TCP头部的选项字段的首字节地址,TLV中的T和L,分别占用一个字节,因此可以通过options指针先解析出第一个选项的T和L,再通过L找到下一个选项的首字节地址,这样就可以遍历TCP头部中的每个选项的TLV。在循环中判断如果找到了符合指定类型的选项,就返回选项的V的指针。遍历options时要跳过NOP option,NOP options只有T字段,如果还是按照TLV的方式解析,解析出的L就是错误的。tcp_parse_options目前先解析出MSS选项和SACK选项,并打印解析结果。
本节TCP数据帧的接收实现完成,修改Makfile如下,加入新增tcp.c文件。

 tcp_ip_stack:device.o init.o debug.o arp.o utils.o pdbuf.o ip.o icmp.o tcp.o
  gcc -o tcp_ip_stack device.o init.o debug.o arp.o utils.o pdbuf.o ip.o icmp.o tcp.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
 tcp.o:tcp.c tcp.h
  gcc -c tcp.c
 clean:
  rm -rf *.o
  rm -rf tcp_ip_stack

Line 1: 将目标文件tcp.o链接到可执行文件tcp_ip_stack,line 19-20新增target,tcp.o依赖tcp.c和tcp.h文件,编译tcp.c生成tcp.o目标文件。
按照本节开始时的测试方法,验证接收TCP数据帧的代码实现。主机A上运行DIY TCP/IP,虚拟地址为192.168.1.7,同时通过tcpdump -i eth0 -s 0 -w 在主机A上抓包,用于对比分析DIY TCP/IP的打印log和tcpdump捕获到的TCP数据帧。主机C上运行iperf -c 192.168.1.7 -t 43200 -i 1。
主机C是笔者的Android手机,运行iperf的截屏如下
DIY TCP/IP TCP模块的实现1_第1张图片
主机C运行iperf TCP客户端,指定TCP Server的IP地址是192.168.17,iperf TCP客户端默认TCP server端的port为5001。iperf客户端没有打印任何Log,说明该IP地址在局域网中不存在。
运行在主机A上的tcpdump的结果如下:
DIY TCP/IP TCP模块的实现1_第2张图片
主机A上tcpdump捕获eth0网络接口的数据帧,与DIY TCP/IP中pcap_open_live指定的网络接口一致。-s 0指定存放数据包的缓存为最大值65535,-w指定将捕获的数据帧存入tcp_recv.pcap文件。从tcpdump的结果来看,一共捕获到了11个数据帧。
运行在主机A上的DIY TCP/IP的结果如下:
DIY TCP/IP TCP模块的实现1_第3张图片
DIY TCP/IP首先是接收到了来自主机C的ARP Reuquest,并回复了ARP Reply数据帧,告知主机C,虚拟IP地址192.168.1.7对应的硬件地址是00:0c:29:2e:0a:ed,即eth0的硬件地址。
TCP模块打印输出,收到4个TCP数据帧,序号都是86944428(0x052eaaac),MSS值为1460,支持SACK选项,TCP数据帧携带的数据长度为0,说明TCP数据帧只包含TCP头部和TCP选项数据。可以猜测主机C向192.168.1.7发起TCP连接,发出TCP SYN数据帧。DIY TCP/IP的TCP模块目前还没实现TCP数据帧的发送,无法回复TCP SYN-ACK,所以主机C重传了3次TCP SYN数据帧。终端键入ctrl+c结束DIY TCP/IP的运行,pdbuf模块打印输出,申请并释放了一个pdbuf。DIY TCP/IP的接收逻辑,只有在重组IP分片时会申请pdbuf,这种情况接收逻辑不会申请pdbuf,只有发送逻辑申请并释放了一个pdbuf,即发出的ARP Reply数据帧。
TCP模块打印输出接收到4个TCP数据帧,和对应的TCP数据帧头部的MSS和SACK选项的值。说明本节实现的代码完成了TCP数据帧的接收,正确校验了TCP头部的校验和。再通过主机A上tcpdump的结果,对比验证DIY TCP/IP的运行结果。
DIY TCP/IP TCP Connect
从wireshark的分析中可以看到,主机C(192.168.1.150)首先发出了ARP Reuqest,然后主机A上运行的的DIY TCP/IP正确回复了ARP Reply,建立了虚拟IP地址192.168.1.7与eth0硬件地址的映射。主机C有发出了4个TCP SYN数据帧,其中有3帧是黑色背景,红色前景显示,说明3个TCP SYN是重传的。通过Sequence number也可以看出这四个TCP SYN的序号一样,MSS为1460,支持SACK选项。
这里TCP数据帧的Sequence number被wireshark友好的解析为0,将第一个TCP数据帧展开,来看Sequence number是否与DIY TCP/IP的打印输出一致。
DIY TCP/IP TCP模块的实现1_第4张图片
展开TCP数据帧的头部可以看到,sequence number 为0,后面括号中的内容是relative sequence number,这是wireshark软件解析的结果,说明该序号是相对值。在右边的raw byte显示栏可以看到sequence number的值为0x052eaaac(86944428), TCP数据帧携带的数据长度为0,与DIY TCP/IP的打印输出一致。Wireshark的分析结果再次验证了本节TCP数据帧的接收和头部校验和的检验是正确的。
下一篇:
DIY TCP/IP TCP模块的实现2

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